Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: read in dependency charts #80

Merged
merged 6 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
/dist
__debug_bin*
.vscode

.coverage
testdata/dependenciesExample/charts/.helm_ls_cache/
helm-ls
helm_ls
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Helm-ls is a [helm](https://github.com/helm/helm) language server protocol [LSP]
- [Manual download](#manual-download)
- [Make it executable](#make-it-executable)
- [Integration with yaml-language-server](#integration-with-yaml-language-server)
- [Dependency Charts](#dependency-charts)
- [Configuration options](#configuration-options)
- [General](#general)
- [Values Files](#values-files)
Expand Down Expand Up @@ -140,6 +141,15 @@ apiVersion: keda.sh/v1alpha1
kind: ScaledObject
```

### Dependency Charts

Helm-ls can process dependency charts to provide autocompletion, hover etc. with values from the dependencies.
For this the dependency charts have to be downloaded. Run the following command in your project to download them:

```bash
helm dependency build
```

## Configuration options

You can configure helm-ls with lsp workspace configurations.
Expand Down
2 changes: 1 addition & 1 deletion cmds/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func newLintCmd() *cobra.Command {
}

rootPath := uri.File(args[0])
chartStore := charts.NewChartStore(rootPath, charts.NewChart)
chartStore := charts.NewChartStore(rootPath, charts.NewChart, func(chart *charts.Chart) {})
chart, err := chartStore.GetChartForURI(rootPath)
if err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions internal/adapter/yamlls/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package yamlls

import (
"context"
"fmt"
"runtime"
"strings"

Expand All @@ -15,6 +16,7 @@ func (c Connector) PublishDiagnostics(ctx context.Context, params *protocol.Publ
doc, ok := c.documents.Get(params.URI)
if !ok {
logger.Println("Error handling diagnostic. Could not get document: " + params.URI.Filename())
return fmt.Errorf("Could not get document: %s", params.URI.Filename())
}

doc.DiagnosticsCache.SetYamlDiagnostics(filterDiagnostics(params.Diagnostics, doc.Ast.Copy(), doc.Content))
Expand Down
20 changes: 14 additions & 6 deletions internal/adapter/yamlls/yamlls_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package yamlls

import (
"os"
"testing"

lsplocal "github.com/mrjosh/helm-ls/internal/lsp"
Expand All @@ -17,13 +18,20 @@ func TestIsRelevantFile(t *testing.T) {
}

connector.documents = &lsplocal.DocumentStore{}
yamlFile := uri.File("../../../testdata/example/templates/deployment.yaml")
nonYamlFile := uri.File("../../../testdata/example/templates/_helpers.tpl")
connector.documents.Store(yamlFile, util.DefaultConfig)
connector.documents.Store(nonYamlFile, util.DefaultConfig)
yamlFile := "../../../testdata/example/templates/deployment.yaml"
nonYamlFile := "../../../testdata/example/templates/_helpers.tpl"

assert.True(t, connector.isRelevantFile(yamlFile))
assert.False(t, connector.isRelevantFile(nonYamlFile))
yamlFileContent, err := os.ReadFile(yamlFile)
assert.NoError(t, err)

nonYamlFileContent, err := os.ReadFile(nonYamlFile)
assert.NoError(t, err)

connector.documents.Store(yamlFile, yamlFileContent, util.DefaultConfig)
connector.documents.Store(nonYamlFile, nonYamlFileContent, util.DefaultConfig)

assert.True(t, connector.isRelevantFile(uri.File(yamlFile)))
assert.False(t, connector.isRelevantFile(uri.File(nonYamlFile)))
}

func TestShouldRun(t *testing.T) {
Expand Down
45 changes: 20 additions & 25 deletions internal/charts/chart.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package charts

import (
"fmt"
"strings"

"github.com/mrjosh/helm-ls/internal/log"
"github.com/mrjosh/helm-ls/internal/util"
lsp "go.lsp.dev/protocol"
"go.lsp.dev/uri"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
)

var logger = log.GetLogger()
Expand All @@ -16,9 +19,12 @@ type Chart struct {
ChartMetadata *ChartMetadata
RootURI uri.URI
ParentChart ParentChart
HelmChart *chart.Chart
}

func NewChart(rootURI uri.URI, valuesFilesConfig util.ValuesFilesConfig) *Chart {
helmChart := loadHelmChart(rootURI)

return &Chart{
ValuesFiles: NewValuesFiles(rootURI,
valuesFilesConfig.MainValuesFileName,
Expand All @@ -27,50 +33,39 @@ func NewChart(rootURI uri.URI, valuesFilesConfig util.ValuesFilesConfig) *Chart
ChartMetadata: NewChartMetadata(rootURI),
RootURI: rootURI,
ParentChart: newParentChart(rootURI),
HelmChart: helmChart,
}
}

type QueriedValuesFiles struct {
Selector []string
ValuesFiles *ValuesFiles
}

// ResolveValueFiles returns a list of all values files in the chart
// and all parent charts if the query tries to access global values
func (c *Chart) ResolveValueFiles(query []string, chartStore *ChartStore) []*QueriedValuesFiles {
ownResult := []*QueriedValuesFiles{{Selector: query, ValuesFiles: c.ValuesFiles}}
if len(query) == 0 {
return ownResult
func loadHelmChart(rootURI uri.URI) (helmChart *chart.Chart) {
chartLoader, err := loader.Loader(rootURI.Filename())
if err != nil {
logger.Error(fmt.Sprintf("Error loading chart %s: %s", rootURI.Filename(), err.Error()))
return nil
}

parentChart := c.ParentChart.GetParentChart(chartStore)
if parentChart == nil {
return ownResult
helmChart, err = chartLoader.Load()
if err != nil {
logger.Error(fmt.Sprintf("Error loading chart %s: %s", rootURI.Filename(), err.Error()))
}

if query[0] == "global" {
return append(ownResult,
parentChart.ResolveValueFiles(query, chartStore)...)
}

chartName := c.ChartMetadata.Metadata.Name
extendedQuery := append([]string{chartName}, query...)
return append(ownResult,
parentChart.ResolveValueFiles(extendedQuery, chartStore)...)
return helmChart
}

func (c *Chart) GetValueLocation(templateContext []string) (lsp.Location, error) {
modifyedVar := make([]string, len(templateContext))
func (c *Chart) GetMetadataLocation(templateContext []string) (lsp.Location, error) {
modifyedVar := []string{}
// make the first letter lowercase since in the template the first letter is
// capitalized, but it is not in the Chart.yaml file
for _, value := range templateContext {
restOfString := ""
if (len(value)) > 1 {
restOfString = value[1:]
}

firstLetterLowercase := strings.ToLower(string(value[0])) + restOfString
modifyedVar = append(modifyedVar, firstLetterLowercase)
}

position, err := util.GetPositionOfNode(&c.ChartMetadata.YamlNode, modifyedVar)

return lsp.Location{URI: c.ChartMetadata.URI, Range: lsp.Range{Start: position}}, err
Expand Down
35 changes: 35 additions & 0 deletions internal/charts/chart_dependecies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package charts

import (
"os"
"path/filepath"

"go.lsp.dev/uri"
)

func (c *Chart) GetDependecyURI(dependencyName string) uri.URI {
unpackedPath := filepath.Join(c.RootURI.Filename(), "charts", dependencyName)
fileInfo, err := os.Stat(unpackedPath)

if err == nil && fileInfo.IsDir() {
return uri.File(unpackedPath)
}

return uri.File(filepath.Join(c.RootURI.Filename(), "charts", DependencyCacheFolder, dependencyName))
}

func (c *Chart) GetDependeciesTemplates() []*DependencyTemplateFile {
result := []*DependencyTemplateFile{}
if c.HelmChart == nil {
return result
}

for _, dependency := range c.HelmChart.Dependencies() {
for _, file := range dependency.Templates {
dependencyTemplate := c.NewDependencyTemplateFile(dependency.Name(), file)
result = append(result, dependencyTemplate)
}
}

return result
}
3 changes: 2 additions & 1 deletion internal/charts/chart_for_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ func (s *ChartStore) GetChartForDoc(uri lsp.DocumentURI) (*Chart, error) {
}

chart, err := s.getChartFromFilesystemForTemplates(uri.Filename())
s.Charts[chart.RootURI] = chart
if err != nil {
return chart, ErrChartNotFound{
URI: uri,
}
}
s.AddChart(chart)

return chart, nil
}

Expand Down
32 changes: 26 additions & 6 deletions internal/charts/chart_for_document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import (
"github.com/mrjosh/helm-ls/internal/util"
"github.com/stretchr/testify/assert"
"go.lsp.dev/uri"
"helm.sh/helm/v3/pkg/chart"
)

func TestGetChartForDocumentWorksForAlreadyAddedCharts(t *testing.T) {
chartStore := charts.NewChartStore("file:///tmp", func(uri uri.URI, _ util.ValuesFilesConfig) *charts.Chart {
return &charts.Chart{RootURI: uri}
})
}, addChartCallback)

chart := &charts.Chart{}
chartStore.Charts["file:///tmp/chart"] = chart
Expand Down Expand Up @@ -53,10 +54,11 @@ func TestGetChartForDocumentWorksForNewToAddChart(t *testing.T) {
rootDir = t.TempDir()
expectedChartDirectory = filepath.Join(rootDir, "chart")
expectedChart = &charts.Chart{
RootURI: uri.File(expectedChartDirectory),
RootURI: uri.File(expectedChartDirectory),
HelmChart: &chart.Chart{},
}
newChartFunc = func(_ uri.URI, _ util.ValuesFilesConfig) *charts.Chart { return expectedChart }
chartStore = charts.NewChartStore(uri.File(rootDir), newChartFunc)
chartStore = charts.NewChartStore(uri.File(rootDir), newChartFunc, addChartCallback)
err = os.MkdirAll(expectedChartDirectory, 0o755)
)
assert.NoError(t, err)
Expand All @@ -78,10 +80,11 @@ func TestGetChartForDocumentWorksForNewToAddChartWithNestedFile(t *testing.T) {
rootDir = t.TempDir()
expectedChartDirectory = filepath.Join(rootDir, "chart")
expectedChart = &charts.Chart{
RootURI: uri.File(expectedChartDirectory),
RootURI: uri.File(expectedChartDirectory),
HelmChart: &chart.Chart{},
}
newChartFunc = func(_ uri.URI, _ util.ValuesFilesConfig) *charts.Chart { return expectedChart }
chartStore = charts.NewChartStore(uri.File(rootDir), newChartFunc)
chartStore = charts.NewChartStore(uri.File(rootDir), newChartFunc, addChartCallback)
err = os.MkdirAll(expectedChartDirectory, 0o755)
)
assert.NoError(t, err)
Expand All @@ -98,7 +101,7 @@ func TestGetChartForDocumentWorksForNewToAddChartWithNestedFile(t *testing.T) {
func TestGetChartOrParentForDocWorks(t *testing.T) {
chartStore := charts.NewChartStore("file:///tmp", func(uri uri.URI, _ util.ValuesFilesConfig) *charts.Chart {
return &charts.Chart{RootURI: uri}
})
}, addChartCallback)

chart := &charts.Chart{}
chartStore.Charts["file:///tmp/chart"] = chart
Expand Down Expand Up @@ -135,3 +138,20 @@ func TestGetChartOrParentForDocWorks(t *testing.T) {
assert.Error(t, error)
assert.Equal(t, &charts.Chart{RootURI: uri.File("/tmp")}, result5)
}

func TestGetChartForDocumentWorksForChartWithDependencies(t *testing.T) {
var (
rootDir = "../../testdata/dependenciesExample/"
chartStore = charts.NewChartStore(uri.File(rootDir), charts.NewChart, addChartCallback)
)

result1, error := chartStore.GetChartForDoc(uri.File(filepath.Join(rootDir, "templates", "deployment.yaml")))
assert.NoError(t, error)

assert.Len(t, result1.HelmChart.Dependencies(), 2)
assert.Len(t, chartStore.Charts, 3)

assert.NotNil(t, chartStore.Charts[uri.File(rootDir)])
assert.NotNil(t, chartStore.Charts[uri.File(filepath.Join(rootDir, "charts", "subchartexample"))])
assert.NotNil(t, chartStore.Charts[uri.File(filepath.Join(rootDir, "charts", charts.DependencyCacheFolder, "common"))])
}
25 changes: 22 additions & 3 deletions internal/charts/chart_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,27 @@ type ChartStore struct {
Charts map[uri.URI]*Chart
RootURI uri.URI
newChart func(uri.URI, util.ValuesFilesConfig) *Chart
addChartCallback func(chart *Chart)
valuesFilesConfig util.ValuesFilesConfig
}

func NewChartStore(rootURI uri.URI, newChart func(uri.URI, util.ValuesFilesConfig) *Chart) *ChartStore {
func NewChartStore(rootURI uri.URI, newChart func(uri.URI, util.ValuesFilesConfig) *Chart, addChartCallback func(chart *Chart)) *ChartStore {
return &ChartStore{
Charts: map[uri.URI]*Chart{},
RootURI: rootURI,
newChart: newChart,
addChartCallback: addChartCallback,
valuesFilesConfig: util.DefaultConfig.ValuesFilesConfig,
}
}

// AddChart adds a new chart to the store and loads its dependencies
func (s *ChartStore) AddChart(chart *Chart) {
s.Charts[chart.RootURI] = chart
s.loadChartDependencies(chart)
s.addChartCallback(chart)
}

func (s *ChartStore) SetValuesFilesConfig(valuesFilesConfig util.ValuesFilesConfig) {
logger.Debug("SetValuesFilesConfig", valuesFilesConfig)
if valuesFilesConfig.MainValuesFileName == s.valuesFilesConfig.MainValuesFileName &&
Expand All @@ -32,7 +41,7 @@ func (s *ChartStore) SetValuesFilesConfig(valuesFilesConfig util.ValuesFilesConf
}
s.valuesFilesConfig = valuesFilesConfig
for uri := range s.Charts {
s.Charts[uri] = s.newChart(uri, valuesFilesConfig)
s.AddChart(s.newChart(uri, valuesFilesConfig))
}
}

Expand All @@ -48,7 +57,7 @@ func (s *ChartStore) GetChartForURI(fileURI uri.URI) (*Chart, error) {
}

if chart != nil {
s.Charts[chart.RootURI] = chart
s.AddChart(chart)
return chart, nil
}

Expand All @@ -64,9 +73,19 @@ func (s *ChartStore) ReloadValuesFile(file uri.URI) {
logger.Error("Error reloading values file", file, err)
return
}

for _, valuesFile := range chart.ValuesFiles.AllValuesFiles() {
if valuesFile.URI == file {
valuesFile.Reload()
}
}
}

func (s *ChartStore) loadChartDependencies(chart *Chart) {
for _, dependency := range chart.HelmChart.Dependencies() {
dependencyURI := chart.GetDependecyURI(dependency.Name())
chart := NewChartFromHelmChart(dependency, dependencyURI)

s.AddChart(chart)
}
}
Loading
Loading