diff --git a/.gitignore b/.gitignore index a66957a3..12d695fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ /dist __debug_bin* .vscode + .coverage +testdata/dependenciesExample/charts/.helm_ls_cache/ +helm-ls +helm_ls diff --git a/README.md b/README.md index fd4f6cc4..19be4f33 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. diff --git a/cmds/lint.go b/cmds/lint.go index 03ba3f05..680b48f2 100644 --- a/cmds/lint.go +++ b/cmds/lint.go @@ -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 diff --git a/internal/adapter/yamlls/diagnostics.go b/internal/adapter/yamlls/diagnostics.go index f7fa48cb..7a600a44 100644 --- a/internal/adapter/yamlls/diagnostics.go +++ b/internal/adapter/yamlls/diagnostics.go @@ -2,6 +2,7 @@ package yamlls import ( "context" + "fmt" "runtime" "strings" @@ -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)) diff --git a/internal/adapter/yamlls/yamlls_test.go b/internal/adapter/yamlls/yamlls_test.go index af0ae636..b1a42d16 100644 --- a/internal/adapter/yamlls/yamlls_test.go +++ b/internal/adapter/yamlls/yamlls_test.go @@ -1,6 +1,7 @@ package yamlls import ( + "os" "testing" lsplocal "github.com/mrjosh/helm-ls/internal/lsp" @@ -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) { diff --git a/internal/charts/chart.go b/internal/charts/chart.go index 2ca97c9f..3ea42df9 100644 --- a/internal/charts/chart.go +++ b/internal/charts/chart.go @@ -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() @@ -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, @@ -27,40 +33,27 @@ 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 { @@ -68,9 +61,11 @@ func (c *Chart) GetValueLocation(templateContext []string) (lsp.Location, error) 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 diff --git a/internal/charts/chart_dependecies.go b/internal/charts/chart_dependecies.go new file mode 100644 index 00000000..a139c32b --- /dev/null +++ b/internal/charts/chart_dependecies.go @@ -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 +} diff --git a/internal/charts/chart_for_document.go b/internal/charts/chart_for_document.go index de7fd68c..207ea6f1 100644 --- a/internal/charts/chart_for_document.go +++ b/internal/charts/chart_for_document.go @@ -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 } diff --git a/internal/charts/chart_for_document_test.go b/internal/charts/chart_for_document_test.go index d3996338..0aa6f294 100644 --- a/internal/charts/chart_for_document_test.go +++ b/internal/charts/chart_for_document_test.go @@ -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 @@ -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) @@ -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) @@ -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 @@ -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"))]) +} diff --git a/internal/charts/chart_store.go b/internal/charts/chart_store.go index 8e336592..738d6af7 100644 --- a/internal/charts/chart_store.go +++ b/internal/charts/chart_store.go @@ -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 && @@ -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)) } } @@ -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 } @@ -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) + } +} diff --git a/internal/charts/chart_store_test.go b/internal/charts/chart_store_test.go index 3cd00468..2ed94fe1 100644 --- a/internal/charts/chart_store_test.go +++ b/internal/charts/chart_store_test.go @@ -12,6 +12,8 @@ import ( "helm.sh/helm/v3/pkg/chartutil" ) +var addChartCallback = func(chart *Chart) {} + func TestSetValuesFilesConfigOverwrites(t *testing.T) { valuesFilesConfig := util.ValuesFilesConfig{ MainValuesFileName: "value.yaml", @@ -25,7 +27,7 @@ func TestSetValuesFilesConfigOverwrites(t *testing.T) { _ = os.WriteFile(filepath.Join(tempDir, "value.yaml"), []byte("foo: main"), 0o644) _ = os.WriteFile(filepath.Join(tempDir, "something.yaml"), []byte(valuesContent), 0o644) _ = os.WriteFile(filepath.Join(tempDir, "values.other.yaml"), []byte(valuesContent), 0o644) - s := NewChartStore(uri.File(tempDir), NewChart) + s := NewChartStore(uri.File(tempDir), NewChart, addChartCallback) chartOld, err := s.GetChartForURI(uri.File(tempDir)) assert.Equal(t, chartutil.Values{}, chartOld.ValuesFiles.MainValuesFile.Values) @@ -54,7 +56,7 @@ func TestSetValuesFilesConfigDoesNotOverwrite(t *testing.T) { _ = os.WriteFile(filepath.Join(tempDir, "something.yaml"), []byte(valuesContent), 0o644) _ = os.WriteFile(filepath.Join(tempDir, "values.lint.yaml"), []byte(valuesContent), 0o644) _ = os.WriteFile(filepath.Join(tempDir, "values.other.yaml"), []byte(valuesContent), 0o644) - s := NewChartStore(uri.File(tempDir), NewChart) + s := NewChartStore(uri.File(tempDir), NewChart, addChartCallback) chart, err := s.GetChartForURI(uri.File(tempDir)) assert.NoError(t, err) @@ -71,7 +73,7 @@ func TestGetChartForURIWhenChartYamlDoesNotExist(t *testing.T) { tempDir := t.TempDir() _ = os.WriteFile(filepath.Join(tempDir, "values.yaml"), []byte("foo: main"), 0o644) - s := NewChartStore(uri.File(tempDir), NewChart) + s := NewChartStore(uri.File(tempDir), NewChart, addChartCallback) chart, err := s.GetChartForURI(uri.File(tempDir)) assert.Error(t, err) @@ -90,7 +92,7 @@ func TestReloadValuesFiles(t *testing.T) { RootURI: uri.File(tempDir), ParentChart: ParentChart{}, } - s := NewChartStore(uri.File(tempDir), NewChart) + s := NewChartStore(uri.File(tempDir), NewChart, addChartCallback) s.Charts[chart.RootURI] = chart assert.Equal(t, "bar", chart.ValuesFiles.MainValuesFile.Values["foo"]) diff --git a/internal/charts/chart_test.go b/internal/charts/chart_test.go index d2057304..bc4a9c8d 100644 --- a/internal/charts/chart_test.go +++ b/internal/charts/chart_test.go @@ -9,10 +9,15 @@ import ( "github.com/mrjosh/helm-ls/internal/util" "go.lsp.dev/uri" "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + + lsp "go.lsp.dev/protocol" "github.com/stretchr/testify/assert" ) +var addChartCallback = func(chart *charts.Chart) {} + func TestNewChartsLoadsMetadata(t *testing.T) { tempDir := t.TempDir() @@ -76,16 +81,17 @@ func TestResolvesValuesFileOfParent(t *testing.T) { err = os.WriteFile(subChartChartFile, []byte{}, 0o644) assert.NoError(t, err) - chart := charts.NewChart(uri.File(filepath.Join(tempDir, "charts", "subchart")), util.ValuesFilesConfig{}) + sut := charts.NewChart(uri.File(filepath.Join(tempDir, "charts", "subchart")), util.ValuesFilesConfig{}) expectedChart := &charts.Chart{ RootURI: uri.File(tempDir), ChartMetadata: &charts.ChartMetadata{}, + HelmChart: &chart.Chart{}, } newChartFunc := func(_ uri.URI, _ util.ValuesFilesConfig) *charts.Chart { return expectedChart } - chartStore := charts.NewChartStore(uri.File(tempDir), newChartFunc) + chartStore := charts.NewChartStore(uri.File(tempDir), newChartFunc, addChartCallback) - valueFiles := chart.ResolveValueFiles([]string{"global", "foo"}, chartStore) + valueFiles := sut.ResolveValueFiles([]string{"global", "foo"}, chartStore) assert.Equal(t, 2, len(valueFiles)) } @@ -117,15 +123,135 @@ func TestResolvesValuesFileOfParentByName(t *testing.T) { Name: "parent", }, }, + HelmChart: &chart.Chart{}, } newChartFunc := func(_ uri.URI, _ util.ValuesFilesConfig) *charts.Chart { return expectedChart } - chartStore := charts.NewChartStore(uri.File(tempDir), newChartFunc) + chartStore := charts.NewChartStore(uri.File(tempDir), newChartFunc, addChartCallback) valueFiles := subchart.ResolveValueFiles([]string{"foo"}, chartStore) parentChart, err := chartStore.GetChartForURI(uri.File(tempDir)) assert.NoError(t, err) - assert.Equal(t, 2, len(valueFiles)) + assert.Len(t, valueFiles, 2) assert.Contains(t, valueFiles, &charts.QueriedValuesFiles{Selector: []string{"subchart", "foo"}, ValuesFiles: parentChart.ValuesFiles}) } + +func TestResolvesValuesFileOfDependencyWithGlobal(t *testing.T) { + var ( + rootDir = "../../testdata/dependenciesExample" + chartStore = charts.NewChartStore(uri.File(rootDir), charts.NewChart, addChartCallback) + chart, err = chartStore.GetChartForDoc(uri.File(filepath.Join(rootDir, "templates", "deployment.yaml"))) + valueFiles = chart.ResolveValueFiles([]string{"global"}, chartStore) + ) + + assert.NoError(t, err) + assert.Len(t, valueFiles, 3) + + selectors := [][]string{} + for _, valueFile := range valueFiles { + selectors = append(selectors, valueFile.Selector) + } + assert.Equal(t, selectors, [][]string{{"global"}, {"global"}, {"global"}}) +} + +func TestResolvesValuesFileOfDependencyWithChartName(t *testing.T) { + var ( + rootDir = "../../testdata/dependenciesExample" + chartStore = charts.NewChartStore(uri.File(rootDir), charts.NewChart, addChartCallback) + chart, err = chartStore.GetChartForDoc(uri.File(filepath.Join(rootDir, "templates", "deployment.yaml"))) + valueFiles = chart.ResolveValueFiles([]string{"subchartexample", "foo"}, chartStore) + ) + + assert.NoError(t, err) + assert.Len(t, valueFiles, 2) + + selectors := [][]string{} + for _, valueFile := range valueFiles { + selectors = append(selectors, valueFile.Selector) + } + assert.Contains(t, selectors, []string{"subchartexample", "foo"}) + assert.Contains(t, selectors, []string{"foo"}) +} + +func TestResolvesValuesFileOfDependencyWithOnlyChartName(t *testing.T) { + var ( + rootDir = "../../testdata/dependenciesExample" + chartStore = charts.NewChartStore(uri.File(rootDir), charts.NewChart, addChartCallback) + chart, err = chartStore.GetChartForDoc(uri.File(filepath.Join(rootDir, "templates", "deployment.yaml"))) + valueFiles = chart.ResolveValueFiles([]string{"subchartexample"}, chartStore) + ) + + assert.NoError(t, err) + assert.Len(t, valueFiles, 2) + + selectors := [][]string{} + for _, valueFile := range valueFiles { + selectors = append(selectors, valueFile.Selector) + } + assert.Contains(t, selectors, []string{"subchartexample"}) + assert.Contains(t, selectors, []string{}) +} + +func TestResolvesValuesFileOfDependencyWithChartNameForPackedDependency(t *testing.T) { + var ( + rootDir = "../../testdata/dependenciesExample" + chartStore = charts.NewChartStore(uri.File(rootDir), charts.NewChart, addChartCallback) + chart, err = chartStore.GetChartForDoc(uri.File(filepath.Join(rootDir, "templates", "deployment.yaml"))) + valueFiles = chart.ResolveValueFiles([]string{"common", "exampleValue"}, chartStore) + ) + + assert.NoError(t, err) + assert.Len(t, valueFiles, 2) + + selectors := [][]string{} + for _, valueFile := range valueFiles { + selectors = append(selectors, valueFile.Selector) + } + assert.Contains(t, selectors, []string{"common", "exampleValue"}) + assert.Contains(t, selectors, []string{"exampleValue"}) + + var commonValueFile *charts.ValuesFiles + for _, valueFile := range valueFiles { + if valueFile.Selector[0] == "exampleValue" { + commonValueFile = valueFile.ValuesFiles + } + } + assert.NotNil(t, commonValueFile) + assert.Equal(t, chartutil.Values{"exampleValue": "common-chart"}, commonValueFile.MainValuesFile.Values) +} + +func TestLoadsHelmChartWithDependecies(t *testing.T) { + chart := charts.NewChart(uri.File("../../testdata/dependenciesExample/"), util.ValuesFilesConfig{}) + + dependecyTemplates := chart.GetDependeciesTemplates() + assert.Len(t, dependecyTemplates, 23) + + filePaths := []string{} + for _, dependency := range dependecyTemplates { + filePaths = append(filePaths, dependency.Path) + } + path, _ := filepath.Abs("../../testdata/dependenciesExample/charts/subchartexample/templates/subchart.yaml") + assert.Contains(t, filePaths, path) + path, _ = filepath.Abs("../../testdata/dependenciesExample/charts/" + charts.DependencyCacheFolder + "/common/templates/_names.tpl") + assert.Contains(t, filePaths, path) +} + +func TestGetValueLocation(t *testing.T) { + chart := charts.NewChart(uri.File("../../testdata/dependenciesExample/"), util.ValuesFilesConfig{}) + + valueLocation, err := chart.GetMetadataLocation([]string{"Name"}) + assert.NoError(t, err) + + expected := lsp.Location{ + URI: uri.File("../../testdata/dependenciesExample/Chart.yaml"), + Range: lsp.Range{ + Start: lsp.Position{ + Line: 1, + Character: 0, + }, + }, + } + + assert.Equal(t, expected, valueLocation) +} diff --git a/internal/charts/chart_values_files.go b/internal/charts/chart_values_files.go new file mode 100644 index 00000000..73b0e769 --- /dev/null +++ b/internal/charts/chart_values_files.go @@ -0,0 +1,89 @@ +package charts + +import ( + "fmt" + + "go.lsp.dev/uri" +) + +type QueriedValuesFiles struct { + Selector []string + ValuesFiles *ValuesFiles +} + +// ResolveValueFiles returns a list of all values files in the chart +// and all parent and dependency charts with the adjusted query +func (c *Chart) ResolveValueFiles(query []string, chartStore *ChartStore) (result []*QueriedValuesFiles) { + recResult := map[uri.URI]*QueriedValuesFiles{} + c.ResolveValueFilesRecursive(query, chartStore, recResult) + + // TODO: @qvalentin use maps.Values once we have Go 1.23 + for _, valuesFiles := range recResult { + result = append(result, valuesFiles) + } + return result +} + +func (c *Chart) ResolveValueFilesRecursive(query []string, chartStore *ChartStore, result map[uri.URI]*QueriedValuesFiles) { + // check if chart was already processed + if _, ok := result[c.RootURI]; ok { + return + } + + if c == nil { + logger.Error("Could not resolve values files for nil chart") + return + } + + currentResult := &QueriedValuesFiles{Selector: query, ValuesFiles: c.ValuesFiles} + result[c.RootURI] = currentResult + if len(query) == 0 { + return + } + + c.resolveValuesFilesOfDependencies(query, chartStore, result) + c.resolveValuesFilesOfParent(chartStore, query, result) +} + +func (c *Chart) resolveValuesFilesOfParent(chartStore *ChartStore, query []string, result map[uri.URI]*QueriedValuesFiles) { + parentChart := c.ParentChart.GetParentChart(chartStore) + if parentChart == nil { + return + } + + if query[0] == "global" { + parentChart.ResolveValueFilesRecursive(query, chartStore, result) + } + + chartName := c.ChartMetadata.Metadata.Name + extendedQuery := append([]string{chartName}, query...) + + parentChart.ResolveValueFilesRecursive(extendedQuery, chartStore, result) +} + +func (c *Chart) resolveValuesFilesOfDependencies(query []string, chartStore *ChartStore, result map[uri.URI]*QueriedValuesFiles) { + if c.HelmChart == nil { + return + } + for _, dependency := range c.HelmChart.Dependencies() { + logger.Debug(fmt.Sprintf("Resolving dependency %s with query %s", dependency.Name(), query)) + + if dependency.Name() == query[0] || query[0] == "global" { + subQuery := query + + if dependency.Name() == query[0] { + if len(query) >= 1 { + subQuery = query[1:] + } + } + + dependencyChart := chartStore.Charts[c.GetDependecyURI(dependency.Name())] + if dependencyChart == nil { + logger.Error(fmt.Sprintf("Could not find dependency %s", dependency.Name())) + continue + } + + dependencyChart.ResolveValueFilesRecursive(subQuery, chartStore, result) + } + } +} diff --git a/internal/charts/dependency_files.go b/internal/charts/dependency_files.go new file mode 100644 index 00000000..bf65c0f3 --- /dev/null +++ b/internal/charts/dependency_files.go @@ -0,0 +1,56 @@ +package charts + +import ( + "os" + "path/filepath" + "strings" + + "helm.sh/helm/v3/pkg/chart" +) + +type DependencyTemplateFile struct { + Content []byte + Path string +} + +var DependencyCacheFolder = ".helm_ls_cache" + +func (c *Chart) NewDependencyTemplateFile(chartName string, file *chart.File) *DependencyTemplateFile { + path := filepath.Join(c.getDependencyDir(chartName), file.Name) + + return &DependencyTemplateFile{Content: file.Data, Path: path} +} + +type PossibleDependencyFile interface { + GetContent() []byte + GetPath() string +} + +func (c *Chart) getDependencyDir(chartName string) string { + extractedPath := filepath.Join(c.RootURI.Filename(), "charts", chartName) + _, err := os.Stat(extractedPath) + if err == nil { + return extractedPath + } + return filepath.Join(c.RootURI.Filename(), "charts", DependencyCacheFolder, chartName) +} + +// SyncToDisk writes the content of the document to disk if it is a dependency file. +// If it is a dependency file, it was read from a archive, so we need to write it back, +// to be able to open it in a editor when using go-to-definition or go-to-reference. +func SyncToDisk(d PossibleDependencyFile) { + if !IsDependencyFile(d) { + return + } + err := os.MkdirAll(filepath.Dir(d.GetPath()), 0o755) + if err == nil { + err = os.WriteFile(d.GetPath(), []byte(d.GetContent()), 0o444) + } + if err != nil { + logger.Error("Could not write dependency file", d.GetPath(), err) + } +} + +func IsDependencyFile(d PossibleDependencyFile) bool { + return strings.Contains(d.GetPath(), DependencyCacheFolder) +} diff --git a/internal/charts/helm_chart.go b/internal/charts/helm_chart.go new file mode 100644 index 00000000..a57a50e9 --- /dev/null +++ b/internal/charts/helm_chart.go @@ -0,0 +1,46 @@ +package charts + +import ( + "path/filepath" + + "go.lsp.dev/uri" + "gopkg.in/yaml.v3" + "helm.sh/helm/v3/pkg/chart" +) + +func NewChartFromHelmChart(helmChart *chart.Chart, rootURI uri.URI) *Chart { + return &Chart{ + ValuesFiles: &ValuesFiles{ + MainValuesFile: getValues(helmChart, rootURI), + OverlayValuesFile: &ValuesFile{}, + AdditionalValuesFiles: []*ValuesFile{}, + }, + ChartMetadata: NewChartMetadataForDependencyChart(helmChart.Metadata, rootURI), + RootURI: rootURI, + ParentChart: getParent(helmChart, rootURI), + HelmChart: helmChart, + } +} + +func getValues(helmChart *chart.Chart, rootURI uri.URI) *ValuesFile { + // Use Raw values if present because they also contain comments and documentation can be useful + uri := uri.File(filepath.Join(rootURI.Filename(), "values.yaml")) + for _, file := range helmChart.Raw { + if file.Name == "values.yaml" { + return NewValuesFileFromContent(uri, file.Data) + } + } + return &ValuesFile{ + ValueNode: yaml.Node{}, + Values: helmChart.Values, + URI: uri, + rawContent: []byte{}, + } +} + +func getParent(helmChart *chart.Chart, rootURI uri.URI) ParentChart { + if helmChart.Parent() != nil { + return newParentChart(rootURI) + } + return ParentChart{} +} diff --git a/internal/charts/metadata.go b/internal/charts/metadata.go index f5dd8e11..e8965944 100644 --- a/internal/charts/metadata.go +++ b/internal/charts/metadata.go @@ -1,6 +1,7 @@ package charts import ( + "os" "path/filepath" "github.com/mrjosh/helm-ls/internal/util" @@ -18,9 +19,15 @@ type ChartMetadata struct { func NewChartMetadata(rootURI uri.URI) *ChartMetadata { filePath := filepath.Join(rootURI.Filename(), chartutil.ChartfileName) - chartNode, err := util.ReadYamlFileToNode(filePath) + contents, err := os.ReadFile(filePath) if err != nil { - logger.Error("Error loading Chart.yaml file", rootURI, err) + logger.Error("Error loading Chart.yaml file ", filePath, err) + return nil + } + + chartNode, err := util.ReadYamlToNode(contents) + if err != nil { + logger.Error("Error loading Chart.yaml file ", rootURI, err) } return &ChartMetadata{ @@ -30,10 +37,19 @@ func NewChartMetadata(rootURI uri.URI) *ChartMetadata { } } +// Create a new ChartMetadata for a dependency chart, omitting the YamlNode since this is +// likely not required for dependency charts +func NewChartMetadataForDependencyChart(metadata *chart.Metadata, URI uri.URI) *ChartMetadata { + return &ChartMetadata{ + Metadata: *metadata, + URI: URI, + } +} + func loadChartMetadata(filePath string) chart.Metadata { chartMetadata, err := chartutil.LoadChartfile(filePath) if err != nil { - logger.Error("Error loading Chart.yaml file", filePath, err) + logger.Error("Error loading Chart.yaml file ", filePath, err) return chart.Metadata{} } return *chartMetadata diff --git a/internal/charts/values_file.go b/internal/charts/values_file.go index a27385eb..82624ab0 100644 --- a/internal/charts/values_file.go +++ b/internal/charts/values_file.go @@ -1,6 +1,9 @@ package charts import ( + "fmt" + "os" + "github.com/mrjosh/helm-ls/internal/util" "go.lsp.dev/uri" "helm.sh/helm/v3/pkg/chartutil" @@ -9,12 +12,13 @@ import ( ) type ValuesFile struct { - Values chartutil.Values - ValueNode yaml.Node - URI uri.URI + Values chartutil.Values + ValueNode yaml.Node + URI uri.URI + rawContent []byte } -func NewValuesFile(filePath string) *ValuesFile { +func NewValuesFileFromPath(filePath string) *ValuesFile { vals, valueNodes := readInValuesFile(filePath) return &ValuesFile{ @@ -24,6 +28,16 @@ func NewValuesFile(filePath string) *ValuesFile { } } +func NewValuesFileFromContent(uri uri.URI, data []byte) *ValuesFile { + vals, valueNode := parseYaml(data) + return &ValuesFile{ + ValueNode: valueNode, + Values: vals, + URI: uri, + rawContent: data, + } +} + func (v *ValuesFile) Reload() { vals, valueNodes := readInValuesFile(v.URI.Filename()) @@ -33,14 +47,34 @@ func (v *ValuesFile) Reload() { } func readInValuesFile(filePath string) (chartutil.Values, yaml.Node) { - vals, err := chartutil.ReadValuesFile(filePath) + content, err := os.ReadFile(filePath) + if err != nil { + logger.Error(fmt.Sprintf("Error loading values file %s ", filePath), err) + return chartutil.Values{}, yaml.Node{} + } + + return parseYaml(content) +} + +func parseYaml(content []byte) (chartutil.Values, yaml.Node) { + vals, err := chartutil.ReadValues(content) if err != nil { - logger.Error("Error loading values file ", filePath, err) + logger.Error("Error parsing values file ", err) } - valueNodes, err := util.ReadYamlFileToNode(filePath) + valueNodes, err := util.ReadYamlToNode(content) if err != nil { - logger.Error("Error loading values file ", filePath, err) + logger.Error("Error parsing values file ", err) } return vals, valueNodes } + +// GetContent implements PossibleDependencyFile. +func (v *ValuesFile) GetContent() []byte { + return v.rawContent +} + +// GetPath implements PossibleDependencyFile. +func (v *ValuesFile) GetPath() string { + return v.URI.Filename() +} diff --git a/internal/charts/values_file_test.go b/internal/charts/values_file_test.go index 64830f96..c3faf6ed 100644 --- a/internal/charts/values_file_test.go +++ b/internal/charts/values_file_test.go @@ -7,6 +7,7 @@ import ( "github.com/mrjosh/helm-ls/internal/charts" "github.com/stretchr/testify/assert" + "go.lsp.dev/uri" "gopkg.in/yaml.v3" "helm.sh/helm/v3/pkg/chartutil" ) @@ -16,7 +17,7 @@ func TestNewValuesFile(t *testing.T) { valuesContent := `foo: bar` _ = os.WriteFile(filepath.Join(tempDir, "values.yaml"), []byte(valuesContent), 0o644) - valuesFile := charts.NewValuesFile(filepath.Join(tempDir, "values.yaml")) + valuesFile := charts.NewValuesFileFromPath(filepath.Join(tempDir, "values.yaml")) assert.Equal(t, "bar", valuesFile.Values["foo"]) assert.NotEqual(t, yaml.Node{}, valuesFile.ValueNode) @@ -25,7 +26,7 @@ func TestNewValuesFile(t *testing.T) { func TestNewValuesFileFileNotFound(t *testing.T) { tempDir := t.TempDir() - valuesFile := charts.NewValuesFile(filepath.Join(tempDir, "values.yaml")) + valuesFile := charts.NewValuesFileFromPath(filepath.Join(tempDir, "values.yaml")) assert.Equal(t, chartutil.Values{}, valuesFile.Values) assert.Equal(t, yaml.Node{}, valuesFile.ValueNode) @@ -35,7 +36,7 @@ func TestReload(t *testing.T) { tempDir := t.TempDir() valuesContent := `foo: bar` _ = os.WriteFile(filepath.Join(tempDir, "values.yaml"), []byte(valuesContent), 0o644) - valuesFile := charts.NewValuesFile(filepath.Join(tempDir, "values.yaml")) + valuesFile := charts.NewValuesFileFromPath(filepath.Join(tempDir, "values.yaml")) assert.Equal(t, "bar", valuesFile.Values["foo"]) assert.NotEqual(t, yaml.Node{}, valuesFile.ValueNode) @@ -45,3 +46,10 @@ func TestReload(t *testing.T) { assert.Equal(t, "baz", valuesFile.Values["foo"]) assert.NotEqual(t, yaml.Node{}, valuesFile.ValueNode) } + +func TestGetContent(t *testing.T) { + tempDir := t.TempDir() + valuesContent := []byte(`foo: bar`) + valuesFile := charts.NewValuesFileFromContent(uri.File(filepath.Join(tempDir, "values.yaml")), []byte(valuesContent)) + assert.Equal(t, valuesContent, valuesFile.GetContent()) +} diff --git a/internal/charts/values_files.go b/internal/charts/values_files.go index 4f5ce580..05adf4ce 100644 --- a/internal/charts/values_files.go +++ b/internal/charts/values_files.go @@ -22,7 +22,7 @@ func NewValuesFiles(rootURI uri.URI, mainValuesFileName string, lintOverlayValue overlayValuesFile := getLintOverlayValuesFile(lintOverlayValuesFile, additionalValuesFiles, rootURI) return &ValuesFiles{ - MainValuesFile: NewValuesFile(filepath.Join(rootURI.Filename(), mainValuesFileName)), + MainValuesFile: NewValuesFileFromPath(filepath.Join(rootURI.Filename(), mainValuesFileName)), OverlayValuesFile: overlayValuesFile, AdditionalValuesFiles: additionalValuesFiles, } @@ -40,7 +40,7 @@ func getLintOverlayValuesFile(lintOverlayValuesFile string, additionalValuesFile } } if overlayValuesFile == nil { - overlayValuesFile = NewValuesFile(filepath.Join(rootURI.Filename(), lintOverlayValuesFile)) + overlayValuesFile = NewValuesFileFromPath(filepath.Join(rootURI.Filename(), lintOverlayValuesFile)) } } return overlayValuesFile @@ -58,7 +58,7 @@ func getAdditionalValuesFiles(additionalValuesFilesGlob string, rootURI uri.URI, if match == filepath.Join(rootURI.Filename(), mainValuesFileName) { continue } - additionalValuesFiles = append(additionalValuesFiles, NewValuesFile(match)) + additionalValuesFiles = append(additionalValuesFiles, NewValuesFileFromPath(match)) } } } diff --git a/internal/handler/completion.go b/internal/handler/completion.go index 2a7104ce..93eb4e80 100644 --- a/internal/handler/completion.go +++ b/internal/handler/completion.go @@ -13,6 +13,7 @@ import ( func (h *langHandler) Completion(ctx context.Context, params *lsp.CompletionParams) (result *lsp.CompletionList, err error) { logger.Debug("Running completion with params", params) + genericDocumentUseCase, err := h.NewGenericDocumentUseCase(params.TextDocumentPositionParams, lsplocal.NestedNodeAtPositionForCompletion) if err != nil { return nil, err diff --git a/internal/handler/completion_main_test.go b/internal/handler/completion_main_test.go index 75e69f5b..449350c7 100644 --- a/internal/handler/completion_main_test.go +++ b/internal/handler/completion_main_test.go @@ -227,7 +227,7 @@ func completionTestCall(fileURI uri.URI, buf string, pos lsp.Position) (*lsp.Com } documents.DidOpen(&d, util.DefaultConfig) h := &langHandler{ - chartStore: charts.NewChartStore(uri.File("."), charts.NewChart), + chartStore: charts.NewChartStore(uri.File("."), charts.NewChart, func(chart *charts.Chart) {}), documents: documents, yamllsConnector: &yamlls.Connector{}, } diff --git a/internal/handler/configuration_test.go b/internal/handler/configuration_test.go index a3298d80..012a0f2c 100644 --- a/internal/handler/configuration_test.go +++ b/internal/handler/configuration_test.go @@ -21,7 +21,7 @@ func TestConfigurationWorks(t *testing.T) { mockClient := mocks.NewMockClient(t) handler := &langHandler{ helmlsConfig: util.DefaultConfig, - chartStore: charts.NewChartStore(uri.File("/"), charts.NewChart), + chartStore: charts.NewChartStore(uri.File("/"), charts.NewChart, addChartCallback), } handler.client = mockClient @@ -43,7 +43,7 @@ func TestConfigurationWorksForEmptyConfig(t *testing.T) { mockClient := mocks.NewMockClient(t) handler := &langHandler{ helmlsConfig: util.DefaultConfig, - chartStore: charts.NewChartStore(uri.File("/"), charts.NewChart), + chartStore: charts.NewChartStore(uri.File("/"), charts.NewChart, addChartCallback), } handler.client = mockClient // disable yamlls to avoid configuring it in the test @@ -62,7 +62,7 @@ func TestConfigurationWorksForError(t *testing.T) { mockClient := mocks.NewMockClient(t) handler := &langHandler{ helmlsConfig: util.DefaultConfig, - chartStore: charts.NewChartStore(uri.File("/"), charts.NewChart), + chartStore: charts.NewChartStore(uri.File("/"), charts.NewChart, addChartCallback), } handler.client = mockClient @@ -84,7 +84,7 @@ func TestConfigurationWorksForJsonError(t *testing.T) { mockClient := mocks.NewMockClient(t) handler := &langHandler{ helmlsConfig: util.DefaultConfig, - chartStore: charts.NewChartStore(uri.File("/"), charts.NewChart), + chartStore: charts.NewChartStore(uri.File("/"), charts.NewChart, addChartCallback), } handler.client = mockClient diff --git a/internal/handler/definition.go b/internal/handler/definition.go index c62560d9..7cd2259a 100644 --- a/internal/handler/definition.go +++ b/internal/handler/definition.go @@ -13,11 +13,12 @@ func (h *langHandler) Definition(_ context.Context, params *lsp.DefinitionParams if err != nil { return nil, err } + usecases := []languagefeatures.DefinitionUseCase{ languagefeatures.NewBuiltInObjectsFeature(genericDocumentUseCase), // has to be before template context + languagefeatures.NewVariablesFeature(genericDocumentUseCase), languagefeatures.NewTemplateContextFeature(genericDocumentUseCase), languagefeatures.NewIncludesCallFeature(genericDocumentUseCase), - languagefeatures.NewVariablesFeature(genericDocumentUseCase), } for _, usecase := range usecases { diff --git a/internal/handler/definition_chart_test.go b/internal/handler/definition_chart_test.go new file mode 100644 index 00000000..1de28335 --- /dev/null +++ b/internal/handler/definition_chart_test.go @@ -0,0 +1,233 @@ +package handler + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/mrjosh/helm-ls/internal/adapter/yamlls" + "github.com/mrjosh/helm-ls/internal/charts" + lsplocal "github.com/mrjosh/helm-ls/internal/lsp" + "github.com/mrjosh/helm-ls/internal/util" + "github.com/stretchr/testify/assert" + lsp "go.lsp.dev/protocol" + "go.lsp.dev/uri" +) + +var ( + rootUri = uri.File("../../testdata/dependenciesExample/") + fileURI = uri.File("../../testdata/dependenciesExample/templates/deployment.yaml") + fileURIInSubchart = uri.File("../../testdata/dependenciesExample/charts/subchartexample/templates/subchart.yaml") +) + +type testCase struct { + // Must be content of a line in the file fileURI + templateLineWithMarker string + expectedFile string + expectedFileCount int + expectedStartPosition lsp.Position + expectedError error + inSubchart bool +} + +// Test definition on a real chart found in $rootUri +func TestDefinitionChart(t *testing.T) { + testCases := []testCase{ + { + `{{ include "common.na^mes.name" . }}`, + "charts/.helm_ls_cache/common/templates/_names.tpl", + 1, + lsp.Position{Line: 9, Character: 0}, + nil, + false, + }, + { + `{{- include "dependeciesEx^ample.labels" . | nindent 4 }}`, + "templates/_helpers.tpl", + 1, + lsp.Position{Line: 35, Character: 0}, + nil, + false, + }, + { + `{{ .Values.gl^obal.subchart }}`, + "values.yaml", + 2, + lsp.Position{Line: 7, Character: 0}, + nil, + false, + }, + { + `{{ .Values.gl^obal.subchart }}`, + "charts/subchartexample/values.yaml", + 2, + lsp.Position{Line: 0, Character: 0}, + nil, + false, + }, + { + `{{ .Values.common.exa^mpleValue }}`, + "charts/.helm_ls_cache/common/values.yaml", + 1, + // this tests, that the file also contains comments + lsp.Position{Line: 7, Character: 0}, + nil, + false, + }, + { + `{{ .Values.comm^on.exampleValue }}`, + "charts/.helm_ls_cache/common/values.yaml", + 1, + lsp.Position{Line: 7, Character: 0}, + nil, + false, + }, + { + `{{ .Values.subch^artexample.subchartWithoutGlobal }}`, + "values.yaml", + 2, + lsp.Position{Line: 49, Character: 0}, + nil, + false, + }, + { + `{{ .Values.subch^artexample.subchartWithoutGlobal }}`, + "charts/subchartexample/values.yaml", + 2, + lsp.Position{Line: 0, Character: 0}, + nil, + false, + }, + { + `{{ .Values.subchartexample.subchartWith^outGlobal }}`, + "values.yaml", + 2, + lsp.Position{Line: 50, Character: 2}, + nil, + false, + }, + { + `{{ .Values.subchartexample.subchart^WithoutGlobal }}`, + "charts/subchartexample/values.yaml", + 2, + lsp.Position{Line: 2, Character: 0}, + nil, + false, + }, + { + `{{ .Cha^rt.Name }}`, + "Chart.yaml", + 1, + lsp.Position{Line: 0, Character: 0}, + nil, + false, + }, + { + `{{ .Chart.Na^me }}`, + "Chart.yaml", + 1, + lsp.Position{Line: 1, Character: 0}, + nil, + false, + }, + { + `{{ .Values.subchart^WithoutGlobal }}`, + "charts/subchartexample/values.yaml", + 2, + lsp.Position{Line: 2, Character: 0}, + nil, + true, + }, + } + + for _, tc := range testCases { + t.Run("Definition on "+tc.templateLineWithMarker, func(t *testing.T) { + uri := fileURI + if tc.inSubchart { + uri = fileURIInSubchart + } + fileContent, err := os.ReadFile(uri.Filename()) + if err != nil { + t.Fatal(err) + } + lines := strings.Split(string(fileContent), "\n") + + pos, found := getPosition(tc, lines) + if !found { + t.Fatal(fmt.Sprintf("%s is not in the file %s", tc.templateLineWithMarker, fileURI.Filename())) + } + + documents := lsplocal.NewDocumentStore() + + chart := charts.NewChart(rootUri, util.DefaultConfig.ValuesFilesConfig) + + addChartCallback := func(chart *charts.Chart) {} + chartStore := charts.NewChartStore(rootUri, charts.NewChart, addChartCallback) + _, err = chartStore.GetChartForURI(rootUri) + h := &langHandler{ + chartStore: chartStore, + documents: documents, + yamllsConnector: &yamlls.Connector{}, + helmlsConfig: util.DefaultConfig, + } + + assert.NoError(t, err) + + h.LoadDocsOnNewChart(chart) + + locations, err := h.Definition(context.TODO(), &lsp.DefinitionParams{ + TextDocumentPositionParams: lsp.TextDocumentPositionParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: uri}, + Position: pos, + }, + }) + + assert.Equal(t, tc.expectedError, err) + assert.Len(t, locations, tc.expectedFileCount) + + // find the location with the correct file path + foundLocation := false + for _, location := range locations { + if location.URI.Filename() == filepath.Join(rootUri.Filename(), tc.expectedFile) { + locations = []lsp.Location{location} + foundLocation = true + break + } + } + + assert.True(t, foundLocation, fmt.Sprintf("Did not find a result with the expected file path %s ", filepath.Join(rootUri.Filename(), tc.expectedFile))) + + if len(locations) > 0 { + assert.Equal(t, filepath.Join(rootUri.Filename(), tc.expectedFile), locations[0].URI.Filename()) + assert.Equal(t, tc.expectedStartPosition, locations[0].Range.Start) + } + + for _, location := range locations { + assert.FileExists(t, location.URI.Filename()) + } + + os.RemoveAll(filepath.Join(rootUri.Filename(), "charts", charts.DependencyCacheFolder)) + }) + } +} + +func getPosition(tC testCase, lines []string) (lsp.Position, bool) { + col := strings.Index(tC.templateLineWithMarker, "^") + buf := strings.Replace(tC.templateLineWithMarker, "^", "", 1) + line := uint32(0) + found := false + + for i, v := range lines { + if strings.Contains(v, buf) { + found = true + line = uint32(i) + col = col + strings.Index(v, buf) + break + } + } + pos := lsp.Position{Line: line, Character: uint32(col)} + return pos, found +} diff --git a/internal/handler/definition_test.go b/internal/handler/definition_test.go index 6169cd92..064e5c41 100644 --- a/internal/handler/definition_test.go +++ b/internal/handler/definition_test.go @@ -3,6 +3,7 @@ package handler import ( "context" "reflect" + "strings" "testing" "github.com/mrjosh/helm-ls/internal/adapter/yamlls" @@ -10,12 +11,15 @@ import ( lsplocal "github.com/mrjosh/helm-ls/internal/lsp" "github.com/mrjosh/helm-ls/internal/util" "github.com/stretchr/testify/assert" + "go.lsp.dev/protocol" lsp "go.lsp.dev/protocol" "go.lsp.dev/uri" yamlv3 "gopkg.in/yaml.v3" + "helm.sh/helm/v3/pkg/chart" ) -var testFileContent = ` +var ( + testFileContent = ` {{ $variable := "text" }} # line 1 {{ $variable }} # line 2 @@ -31,8 +35,6 @@ var testFileContent = ` {{ end }} {{ range $index, $element := pipeline }}{{ $index }}{{ $element }}{{ end }} # line 14 ` - -var ( testDocumentTemplateURI = uri.URI("file:///templates/test.yaml") testValuesURI = uri.URI("file:///values.yaml") testOtherValuesURI = uri.URI("file:///values.other.yaml") @@ -56,7 +58,7 @@ func genericDefinitionTest(t *testing.T, position lsp.Position, expectedLocation fileURI := testDocumentTemplateURI rootUri := uri.File("/") - chart := &charts.Chart{ + testChart := &charts.Chart{ ChartMetadata: &charts.ChartMetadata{}, ValuesFiles: &charts.ValuesFiles{ MainValuesFile: &charts.ValuesFile{ @@ -66,7 +68,8 @@ func genericDefinitionTest(t *testing.T, position lsp.Position, expectedLocation }, AdditionalValuesFiles: []*charts.ValuesFile{}, }, - RootURI: "", + RootURI: "", + HelmChart: &chart.Chart{}, } d := lsp.DidOpenTextDocumentParams{ TextDocument: lsp.TextDocumentItem{ @@ -77,8 +80,8 @@ func genericDefinitionTest(t *testing.T, position lsp.Position, expectedLocation }, } documents.DidOpen(&d, util.DefaultConfig) - chartStore := charts.NewChartStore(rootUri, charts.NewChart) - chartStore.Charts = map[uri.URI]*charts.Chart{rootUri: chart} + chartStore := charts.NewChartStore(rootUri, charts.NewChart, addChartCallback) + chartStore.Charts = map[uri.URI]*charts.Chart{rootUri: testChart} h := &langHandler{ chartStore: chartStore, documents: documents, @@ -272,7 +275,7 @@ func genericDefinitionTestMultipleValuesFiles(t *testing.T, position lsp.Positio }, } documents.DidOpen(&d, util.DefaultConfig) - chartStore := charts.NewChartStore(rootUri, charts.NewChart) + chartStore := charts.NewChartStore(rootUri, charts.NewChart, addChartCallback) chartStore.Charts = map[uri.URI]*charts.Chart{rootUri: chart} h := &langHandler{ chartStore: chartStore, @@ -327,3 +330,70 @@ func TestDefinitionValueFileMulitpleValues(t *testing.T) { }, }, nil) } + +func TestDefinitionSingleLine(t *testing.T) { + testCases := []struct { + // defines a definition test where ^ is the position where the defintion is triggered + // and §result§ marks the range of the result + templateWithMarks string + }{ + {"{{ §$test := 1§ }} {{ $te^st }}"}, + {"{{ §$test := .Values.test§ }} {{ $te^st.with.selectorexpression }}"}, + {"{{ §$test := $.Values.test§ }} {{ $te^st.with.selectorexpression. }}"}, + {"{{ §$test := .Values.test§ }} {{ $te^st }}"}, + {"{{ range §$test := .Values.test§ }} {{ $te^st }} {{ end }}"}, + {"{{ range §$test := $.Values.test§ }} {{ $te^st.something }} {{ end }}"}, + {"{{ range §$test := $.Values.test§ }} {{ $te^st. }} {{ end }}"}, + {"{{ range §$test := $.Values.test§ }} {{ if not $te^st }} y {{ else }} n {{ end }}"}, + } + for _, tc := range testCases { + t.Run(tc.templateWithMarks, func(t *testing.T) { + col := strings.Index(tc.templateWithMarks, "^") + buf := strings.Replace(tc.templateWithMarks, "^", "", 1) + pos := protocol.Position{Line: 0, Character: uint32(col - 3)} + expectedColStart := strings.Index(buf, "§") + buf = strings.Replace(buf, "§", "", 1) + expectedColEnd := strings.Index(buf, "§") + buf = strings.Replace(buf, "§", "", 1) + + documents := lsplocal.NewDocumentStore() + fileURI := testDocumentTemplateURI + rootUri := uri.File("/") + + d := lsp.DidOpenTextDocumentParams{ + TextDocument: lsp.TextDocumentItem{ + URI: fileURI, + Text: buf, + }, + } + documents.DidOpen(&d, util.DefaultConfig) + h := &langHandler{ + chartStore: charts.NewChartStore(rootUri, charts.NewChart, addChartCallback), + documents: documents, + } + + locations, err := h.Definition(context.TODO(), &lsp.DefinitionParams{ + TextDocumentPositionParams: lsp.TextDocumentPositionParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: fileURI}, + Position: pos, + }, + }) + + assert.NoError(t, err) + + assert.Contains(t, locations, lsp.Location{ + URI: testDocumentTemplateURI, + Range: lsp.Range{ + Start: lsp.Position{ + Line: 0, + Character: uint32(expectedColStart), + }, + End: lsp.Position{ + Line: 0, + Character: uint32(expectedColEnd), + }, + }, + }) + }) + } +} diff --git a/internal/handler/hover_main_test.go b/internal/handler/hover_main_test.go index 4e013aac..bcbcfd52 100644 --- a/internal/handler/hover_main_test.go +++ b/internal/handler/hover_main_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "github.com/mrjosh/helm-ls/internal/adapter/yamlls" @@ -29,7 +30,7 @@ func TestHoverMain(t *testing.T) { Line: 85, Character: 26, }, - expected: fmt.Sprintf("### %s\n%s\n\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\nvalue\n```"), + expected: fmt.Sprintf("### %s\n%s\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\nvalue\n```"), expectedError: nil, }, { @@ -38,7 +39,7 @@ func TestHoverMain(t *testing.T) { Line: 74, Character: 50, }, - expected: fmt.Sprintf("### %s\n%s\n\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\nfirst:\n some: value\nsecond:\n some: value\n\n```"), + expected: fmt.Sprintf("### %s\n%s\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\nfirst:\n some: value\nsecond:\n some: value\n```"), expectedError: nil, }, { @@ -47,7 +48,7 @@ func TestHoverMain(t *testing.T) { Line: 80, Character: 31, }, - expected: fmt.Sprintf("### %s\n%s\n\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\nvalue\n```"), + expected: fmt.Sprintf("### %s\n%s\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\nvalue\n```"), expectedError: nil, }, { @@ -56,7 +57,7 @@ func TestHoverMain(t *testing.T) { Line: 17, Character: 19, }, - expected: fmt.Sprintf("### %s\n%s\n\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\n{}\n\n```"), + expected: fmt.Sprintf("### %s\n%s\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\n{}\n```"), expectedError: nil, }, { @@ -83,7 +84,7 @@ func TestHoverMain(t *testing.T) { Line: 25, Character: 28, }, - expected: fmt.Sprintf("### %s\n%s\n\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\nimagePullSecrets: []\n\n```"), + expected: fmt.Sprintf("### %s\n%s\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\nimagePullSecrets: []\n```"), expectedError: nil, }, { @@ -128,7 +129,7 @@ func TestHoverMain(t *testing.T) { Line: 71, Character: 35, }, - expected: fmt.Sprintf("### %s\n%s\n\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\ningress.hosts:\n- host: chart-example.local\n paths:\n - path: /\n pathType: ImplementationSpecific\n\n```"), + expected: fmt.Sprintf("### %s\n%s\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\ningress.hosts:\n- host: chart-example.local\n paths:\n - path: /\n pathType: ImplementationSpecific\n```"), expectedError: nil, }, { @@ -137,7 +138,28 @@ func TestHoverMain(t *testing.T) { Line: 8, Character: 28, }, - expected: fmt.Sprintf("### %s\n%s\n\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\n1\n```"), + expected: fmt.Sprintf("### %s\n%s\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "```yaml\n1\n```"), + expectedError: nil, + }, + { + desc: "Test hover include parameter", + position: lsp.Position{ + Line: 3, + Character: 28, + }, + expected: "### " + filepath.Join("..", "_helpers.tpl") + "\n```helm\n" + `{{- define "example.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} +` + "```\n", expectedError: nil, }, } @@ -161,11 +183,16 @@ func TestHoverMain(t *testing.T) { }, } documents.DidOpen(&d, util.DefaultConfig) + h := &langHandler{ - chartStore: charts.NewChartStore(uri.File("."), charts.NewChart), + chartStore: charts.NewChartStore(uri.File("."), charts.NewChart, addChartCallback), documents: documents, yamllsConnector: &yamlls.Connector{}, + helmlsConfig: util.DefaultConfig, } + h.chartStore = charts.NewChartStore(uri.File("."), charts.NewChart, h.AddChartCallback) + chart, _ := h.chartStore.GetChartOrParentForDoc(fileURI) + h.LoadDocsOnNewChart(chart) result, err := h.Hover(context.Background(), &lsp.HoverParams{ TextDocumentPositionParams: lsp.TextDocumentPositionParams{ TextDocument: lsp.TextDocumentIdentifier{ @@ -178,7 +205,7 @@ func TestHoverMain(t *testing.T) { if result == nil { t.Fatal("Result is nil") } - assert.Equal(t, tt.expected, result.Contents.Value) + assert.Equal(t, tt.expected, strings.ReplaceAll(result.Contents.Value, "\r\n", "\n")) }) } } diff --git a/internal/handler/initialization.go b/internal/handler/initialization.go index c372a056..7a4ba313 100644 --- a/internal/handler/initialization.go +++ b/internal/handler/initialization.go @@ -28,7 +28,7 @@ func (h *langHandler) Initialize(ctx context.Context, params *lsp.InitializePara } logger.Debug("Initializing chartStore") - h.chartStore = charts.NewChartStore(workspaceURI, h.NewChartWithInitActions) + h.chartStore = charts.NewChartStore(workspaceURI, charts.NewChart, h.AddChartCallback) logger.Debug("Initializing done") return &lsp.InitializeResult{ @@ -96,7 +96,7 @@ func configureLogLevel(helmlsConfig util.HelmlsConfiguration) { } } -func (h *langHandler) NewChartWithInitActions(rootURI uri.URI, valuesFilesConfig util.ValuesFilesConfig) *charts.Chart { - go h.LoadDocsOnNewChart(rootURI) - return h.NewChartWithWatchedFiles(rootURI, valuesFilesConfig) +func (h *langHandler) AddChartCallback(chart *charts.Chart) { + h.NewChartWithWatchedFiles(chart) + go h.LoadDocsOnNewChart(chart) } diff --git a/internal/handler/references_test.go b/internal/handler/references_test.go index ba106d66..07136e1c 100644 --- a/internal/handler/references_test.go +++ b/internal/handler/references_test.go @@ -15,6 +15,8 @@ import ( "go.lsp.dev/uri" ) +var addChartCallback = func(chart *charts.Chart) {} + func TestRefercesTemplateContext(t *testing.T) { content := ` {{ .Values.test }} @@ -94,7 +96,7 @@ func TestRefercesTemplateContext(t *testing.T) { } documents.DidOpen(&d, util.DefaultConfig) h := &langHandler{ - chartStore: charts.NewChartStore(uri.File("."), charts.NewChart), + chartStore: charts.NewChartStore(uri.File("."), charts.NewChart, addChartCallback), documents: documents, yamllsConnector: &yamlls.Connector{}, } @@ -161,7 +163,7 @@ func TestRefercesTemplateContextWithTestFile(t *testing.T) { } documents.DidOpen(&d, util.DefaultConfig) h := &langHandler{ - chartStore: charts.NewChartStore(uri.File("."), charts.NewChart), + chartStore: charts.NewChartStore(uri.File("."), charts.NewChart, addChartCallback), documents: documents, yamllsConnector: &yamlls.Connector{}, } @@ -219,7 +221,7 @@ func TestRefercesSingleLines(t *testing.T) { } documents.DidOpen(&d, util.DefaultConfig) h := &langHandler{ - chartStore: charts.NewChartStore(uri.File("."), charts.NewChart), + chartStore: charts.NewChartStore(uri.File("."), charts.NewChart, addChartCallback), documents: documents, yamllsConnector: &yamlls.Connector{}, } diff --git a/internal/handler/text_document.go b/internal/handler/text_document.go index dbde3de9..4ad67bf0 100644 --- a/internal/handler/text_document.go +++ b/internal/handler/text_document.go @@ -3,12 +3,12 @@ package handler import ( "context" "errors" - "io/fs" + "fmt" "path/filepath" + "github.com/mrjosh/helm-ls/internal/charts" lsplocal "github.com/mrjosh/helm-ls/internal/lsp" lsp "go.lsp.dev/protocol" - "go.lsp.dev/uri" ) func (h *langHandler) DidOpen(ctx context.Context, params *lsp.DidOpenTextDocumentParams) (err error) { @@ -57,7 +57,7 @@ func (h *langHandler) DidSave(ctx context.Context, params *lsp.DidSaveTextDocume return nil } -func (h *langHandler) DidChange(ctx context.Context, params *lsp.DidChangeTextDocumentParams) (err error) { +func (h *langHandler) DidChange(_ context.Context, params *lsp.DidChangeTextDocumentParams) (err error) { doc, ok := h.documents.Get(params.TextDocument.URI) if !ok { return errors.New("Could not get document: " + params.TextDocument.URI.Filename()) @@ -102,17 +102,17 @@ func (h *langHandler) DidRenameFiles(ctx context.Context, params *lsp.RenameFile return nil } -// TODO: maybe use the helm implementation of this once https://github.com/mrjosh/helm-ls/pull/77 is resolved -func (h *langHandler) LoadDocsOnNewChart(rootURI uri.URI) { - _ = filepath.WalkDir(filepath.Join(rootURI.Filename(), "templates"), - func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if !d.IsDir() { - return h.documents.Store(uri.File(path), h.helmlsConfig) - } - return nil - }, - ) +func (h *langHandler) LoadDocsOnNewChart(chart *charts.Chart) { + if chart.HelmChart == nil { + return + } + + for _, file := range chart.HelmChart.Templates { + h.documents.Store(filepath.Join(chart.RootURI.Filename(), file.Name), file.Data, h.helmlsConfig) + } + + for _, file := range chart.GetDependeciesTemplates() { + logger.Debug(fmt.Sprintf("Storing dependency %s", file.Path)) + h.documents.Store(file.Path, file.Content, h.helmlsConfig) + } } diff --git a/internal/handler/text_document_test.go b/internal/handler/text_document_test.go index e74c9e90..1a1319e4 100644 --- a/internal/handler/text_document_test.go +++ b/internal/handler/text_document_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + "github.com/mrjosh/helm-ls/internal/charts" lsplocal "github.com/mrjosh/helm-ls/internal/lsp" "github.com/mrjosh/helm-ls/internal/util" "github.com/stretchr/testify/assert" @@ -35,7 +36,7 @@ func TestLoadDocsOnNewChart(t *testing.T) { helmlsConfig: util.DefaultConfig, } - h.LoadDocsOnNewChart(rootURI) + h.LoadDocsOnNewChart(charts.NewChart(rootURI, util.DefaultConfig.ValuesFilesConfig)) for _, file := range templateFiles { doc, ok := h.documents.Get(uri.File(file)) @@ -70,7 +71,7 @@ func TestLoadDocsOnNewChartDoesNotOverwrite(t *testing.T) { }, }, util.DefaultConfig) - h.LoadDocsOnNewChart(rootURI) + h.LoadDocsOnNewChart(charts.NewChart(rootURI, util.DefaultConfig.ValuesFilesConfig)) doc, ok := h.documents.Get(uri.File(templateFile)) assert.True(t, ok) @@ -89,7 +90,7 @@ func TestLoadDocsOnNewChartWorksForMissingTemplateDir(t *testing.T) { helmlsConfig: util.DefaultConfig, } - h.LoadDocsOnNewChart(rootURI) + h.LoadDocsOnNewChart(charts.NewChart(rootURI, util.DefaultConfig.ValuesFilesConfig)) - h.LoadDocsOnNewChart(uri.File("non-existent-dir/hkjgfdshgkjfd")) + h.LoadDocsOnNewChart(charts.NewChart(uri.File("NonExisting"), util.DefaultConfig.ValuesFilesConfig)) } diff --git a/internal/handler/watched_files.go b/internal/handler/watched_files.go index 74e7eb05..3acd9504 100644 --- a/internal/handler/watched_files.go +++ b/internal/handler/watched_files.go @@ -4,16 +4,14 @@ import ( "context" "github.com/mrjosh/helm-ls/internal/charts" - "github.com/mrjosh/helm-ls/internal/util" lsp "go.lsp.dev/protocol" "go.lsp.dev/uri" "go.lsp.dev/jsonrpc2" ) -func (h *langHandler) NewChartWithWatchedFiles(rootURI uri.URI, valuesFilesConfig util.ValuesFilesConfig) *charts.Chart { - logger.Debug("NewChartWithWatchedFiles", rootURI, valuesFilesConfig) - chart := charts.NewChart(rootURI, valuesFilesConfig) +func (h *langHandler) NewChartWithWatchedFiles(chart *charts.Chart) { + logger.Debug("NewChartWithWatchedFiles ", chart.RootURI) uris := make([]uri.URI, 0) for _, valuesFile := range chart.ValuesFiles.AllValuesFiles() { @@ -21,10 +19,12 @@ func (h *langHandler) NewChartWithWatchedFiles(rootURI uri.URI, valuesFilesConfi } go h.RegisterWatchedFiles(context.Background(), h.connPool, uris) - return chart } func (h *langHandler) RegisterWatchedFiles(ctx context.Context, conn jsonrpc2.Conn, files []uri.URI) { + if conn == nil { + return + } watchers := make([]lsp.FileSystemWatcher, 0) for _, file := range files { diff --git a/internal/language_features/includes.go b/internal/language_features/includes.go index 656a59ad..1d8ab8e7 100644 --- a/internal/language_features/includes.go +++ b/internal/language_features/includes.go @@ -3,6 +3,7 @@ package languagefeatures import ( lsp "go.lsp.dev/protocol" + "github.com/mrjosh/helm-ls/internal/charts" lsplocal "github.com/mrjosh/helm-ls/internal/lsp" "github.com/mrjosh/helm-ls/internal/protocol" "github.com/mrjosh/helm-ls/internal/tree-sitter/gotemplate" @@ -90,6 +91,9 @@ func (f *IncludesFeature) getReferenceLocations(includeName string) []lsp.Locati for _, referenceRange := range referenceRanges { locations = append(locations, util.RangeToLocation(doc.URI, referenceRange)) } + if len(locations) > 0 { + charts.SyncToDisk(doc) + } } return locations @@ -102,6 +106,9 @@ func (f *IncludesFeature) getDefinitionLocations(includeName string) []lsp.Locat for _, referenceRange := range referenceRanges { locations = append(locations, util.RangeToLocation(doc.URI, referenceRange)) } + if len(locations) > 0 { + charts.SyncToDisk(doc) + } } return locations @@ -132,7 +139,7 @@ func (f *IncludesCallFeature) Hover() (string, error) { } result := f.getDefinitionsHover(includeName) - return result.Format(f.GenericDocumentUseCase.Document.URI), nil + return result.FormatHelm(f.GenericDocumentUseCase.Document.URI), nil } func (f *IncludesCallFeature) Definition() (result []lsp.Location, err error) { diff --git a/internal/language_features/template_context.go b/internal/language_features/template_context.go index cf088edf..8b61ea91 100644 --- a/internal/language_features/template_context.go +++ b/internal/language_features/template_context.go @@ -3,9 +3,11 @@ package languagefeatures import ( "fmt" "reflect" + "strings" lsp "go.lsp.dev/protocol" + "github.com/mrjosh/helm-ls/internal/charts" helmdocs "github.com/mrjosh/helm-ls/internal/documentation/helm" lsplocal "github.com/mrjosh/helm-ls/internal/lsp" "github.com/mrjosh/helm-ls/internal/protocol" @@ -69,11 +71,17 @@ func (f *TemplateContextFeature) getDefinitionLocations(templateContext lsplocal switch templateContext[0] { case "Values": for _, value := range f.Chart.ResolveValueFiles(templateContext.Tail(), f.ChartStore) { - locations = append(locations, value.ValuesFiles.GetPositionsForValue(value.Selector)...) + locs := value.ValuesFiles.GetPositionsForValue(value.Selector) + if len(locs) > 0 { + for _, valuesFile := range value.ValuesFiles.AllValuesFiles() { + charts.SyncToDisk(valuesFile) + } + } + locations = append(locations, locs...) } return locations case "Chart": - location, _ := f.Chart.GetValueLocation(templateContext.Tail()) + location, _ := f.Chart.GetMetadataLocation(templateContext.Tail()) return []lsp.Location{location} } return locations @@ -107,13 +115,15 @@ func (f *TemplateContextFeature) valuesHover(templateContext lsplocal.TemplateCo ) for _, valuesFiles := range valuesFiles { for _, valuesFile := range valuesFiles.ValuesFiles.AllValuesFiles() { + logger.Debug(fmt.Sprintf("Looking for selector: %s in values %v", strings.Join(valuesFiles.Selector, "."), valuesFile.Values)) result, err := util.GetTableOrValueForSelector(valuesFile.Values, valuesFiles.Selector) + if err == nil { hoverResults = append(hoverResults, protocol.HoverResultWithFile{URI: valuesFile.URI, Value: result}) } } } - return hoverResults.Format(f.ChartStore.RootURI), nil + return hoverResults.FormatYaml(f.ChartStore.RootURI), nil } func (f *TemplateContextFeature) getMetadataField(v *chart.Metadata, fieldName string) string { diff --git a/internal/language_features/template_context_completion_test.go b/internal/language_features/template_context_completion_test.go index 3ee0a594..c85a84b1 100644 --- a/internal/language_features/template_context_completion_test.go +++ b/internal/language_features/template_context_completion_test.go @@ -30,7 +30,7 @@ func TestGetValuesCompletions(t *testing.T) { }, }, }, - RootURI: "", + RootURI: "", HelmChart: &chart.Chart{}, } templateConextFeature := TemplateContextFeature{ @@ -69,7 +69,7 @@ func TestGetValuesCompletionsContainsNoDupliactes(t *testing.T) { }, }, }, - RootURI: "", + RootURI: "", HelmChart: &chart.Chart{}, } templateConextFeature := TemplateContextFeature{ diff --git a/internal/language_features/template_context_hover_test.go b/internal/language_features/template_context_hover_test.go index 2df5f468..4ba6dfde 100644 --- a/internal/language_features/template_context_hover_test.go +++ b/internal/language_features/template_context_hover_test.go @@ -13,9 +13,9 @@ import ( func Test_langHandler_getValueHover(t *testing.T) { type args struct { - chart *charts.Chart - parentCharts map[uri.URI]*charts.Chart - splittedVar []string + chart *charts.Chart + chartsInStore map[uri.URI]*charts.Chart + splittedVar []string } tests := []struct { name string @@ -36,6 +36,7 @@ func Test_langHandler_getValueHover(t *testing.T) { URI: "file://tmp/values.yaml", }, }, + HelmChart: &chart.Chart{}, }, splittedVar: []string{"key"}, }, @@ -43,7 +44,6 @@ func Test_langHandler_getValueHover(t *testing.T) { %s value %s - `, "```yaml", "```"), wantErr: false, }, @@ -56,6 +56,7 @@ value MainValuesFile: &charts.ValuesFile{Values: map[string]interface{}{"key": "value"}, URI: "file://tmp/values.yaml"}, AdditionalValuesFiles: []*charts.ValuesFile{{Values: map[string]interface{}{"key": ""}, URI: "file://tmp/values.other.yaml"}}, }, + HelmChart: &chart.Chart{}, }, splittedVar: []string{"key"}, }, @@ -63,10 +64,8 @@ value %s value %s - ### values.other.yaml "" - `, "```yaml", "```"), wantErr: false, }, @@ -78,15 +77,14 @@ value ValuesFiles: &charts.ValuesFiles{ MainValuesFile: &charts.ValuesFile{Values: map[string]interface{}{"key": map[string]interface{}{"nested": "value"}}, URI: "file://tmp/values.yaml"}, }, + HelmChart: &chart.Chart{}, }, splittedVar: []string{"key"}, }, want: fmt.Sprintf(`### values.yaml %s nested: value - %s - `, "```yaml", "```"), wantErr: false, }, @@ -98,6 +96,7 @@ nested: value ValuesFiles: &charts.ValuesFiles{ MainValuesFile: &charts.ValuesFile{Values: map[string]interface{}{"key": []map[string]interface{}{{"nested": "value"}}}, URI: "file://tmp/values.yaml"}, }, + HelmChart: &chart.Chart{}, }, splittedVar: []string{"key"}, }, @@ -105,9 +104,7 @@ nested: value %s key: - nested: value - %s - `, "```yaml", "```"), wantErr: false, }, @@ -119,17 +116,20 @@ key: ValuesFiles: &charts.ValuesFiles{ MainValuesFile: &charts.ValuesFile{Values: map[string]interface{}{"global": map[string]interface{}{"key": "value"}}, URI: "file://tmp/charts/subchart/values.yaml"}, }, + RootURI: uri.File("/tmp/charts/subchart"), ParentChart: charts.ParentChart{ ParentChartURI: uri.New("file://tmp/"), HasParent: true, }, + HelmChart: &chart.Chart{}, }, - parentCharts: map[uri.URI]*charts.Chart{ + chartsInStore: map[uri.URI]*charts.Chart{ uri.New("file://tmp/"): { ChartMetadata: &charts.ChartMetadata{}, ValuesFiles: &charts.ValuesFiles{ MainValuesFile: &charts.ValuesFile{Values: map[string]interface{}{"global": map[string]interface{}{"key": "parentValue"}}, URI: "file://tmp/values.yaml"}, }, + HelmChart: &chart.Chart{}, }, }, splittedVar: []string{"global", "key"}, @@ -138,12 +138,10 @@ key: %s parentValue %s - ### `+filepath.Join("charts", "subchart", "values.yaml")+` %s value %s - `, "```yaml", "```", "```yaml", "```"), wantErr: false, }, @@ -157,17 +155,20 @@ value ValuesFiles: &charts.ValuesFiles{ MainValuesFile: &charts.ValuesFile{Values: map[string]interface{}{"key": "value"}, URI: "file://tmp/charts/subchart/values.yaml"}, }, + RootURI: uri.File("/tmp/charts/subchart"), ParentChart: charts.ParentChart{ ParentChartURI: uri.New("file://tmp/"), HasParent: true, }, + HelmChart: &chart.Chart{}, }, - parentCharts: map[uri.URI]*charts.Chart{ + chartsInStore: map[uri.URI]*charts.Chart{ uri.New("file://tmp/"): { ChartMetadata: &charts.ChartMetadata{}, ValuesFiles: &charts.ValuesFiles{ MainValuesFile: &charts.ValuesFile{Values: map[string]interface{}{"subchart": map[string]interface{}{"key": "parentValue"}}, URI: "file://tmp/values.yaml"}, }, + HelmChart: &chart.Chart{}, }, }, splittedVar: []string{"key"}, @@ -176,12 +177,10 @@ value %s parentValue %s - ### `+filepath.Join("charts", "subchart", "values.yaml")+` %s value %s - `, "```yaml", "```", "```yaml", "```"), wantErr: false, }, @@ -193,21 +192,25 @@ value ValuesFiles: &charts.ValuesFiles{ MainValuesFile: &charts.ValuesFile{Values: map[string]interface{}{"key": "value"}, URI: "file://tmp/charts/subchart/charts/subsubchart/values.yaml"}, }, + RootURI: uri.File("/tmp/charts/subchart/charts/subsubchart"), ParentChart: charts.ParentChart{ ParentChartURI: uri.New("file://tmp/charts/subchart"), HasParent: true, }, + HelmChart: &chart.Chart{}, }, - parentCharts: map[uri.URI]*charts.Chart{ + chartsInStore: map[uri.URI]*charts.Chart{ uri.New("file://tmp/charts/subchart"): { ChartMetadata: &charts.ChartMetadata{Metadata: chart.Metadata{Name: "subchart"}}, ValuesFiles: &charts.ValuesFiles{ MainValuesFile: &charts.ValuesFile{Values: map[string]interface{}{"subsubchart": map[string]interface{}{"key": "middleValue"}}, URI: "file://tmp/charts/subchart/values.yaml"}, }, + RootURI: uri.File("/tmp/charts/subchart"), ParentChart: charts.ParentChart{ ParentChartURI: uri.New("file://tmp/"), HasParent: true, }, + HelmChart: &chart.Chart{}, }, uri.New("file://tmp/"): { ChartMetadata: &charts.ChartMetadata{ @@ -216,6 +219,7 @@ value ValuesFiles: &charts.ValuesFiles{ MainValuesFile: &charts.ValuesFile{Values: map[string]interface{}{"subchart": map[string]interface{}{"subsubchart": map[string]interface{}{"key": "parentValue"}}}, URI: "file://tmp/values.yaml"}, }, + HelmChart: &chart.Chart{}, }, }, splittedVar: []string{"key"}, @@ -224,17 +228,14 @@ value %s parentValue %s - ### `+filepath.Join("charts", "subchart", "values.yaml")+` %s middleValue %s - ### `+filepath.Join("charts", "subchart", "charts", "subsubchart", "values.yaml")+` %s value %s - `, "```yaml", "```", "```yaml", "```", "```yaml", "```"), wantErr: false, }, @@ -251,6 +252,7 @@ value URI: "file://tmp/values.yaml", }, }, + HelmChart: &chart.Chart{}, }, splittedVar: []string{"key"}, }, @@ -258,7 +260,6 @@ value %s 1.2345 %s - `, "```yaml", "```"), wantErr: false, }, @@ -275,6 +276,7 @@ value URI: "file://tmp/values.yaml", }, }, + HelmChart: &chart.Chart{}, }, splittedVar: []string{"key[]"}, }, @@ -282,7 +284,6 @@ value %s hello %s - `, "```yaml", "```"), wantErr: false, }, @@ -293,9 +294,8 @@ hello Chart: tt.args.chart, ChartStore: &charts.ChartStore{ RootURI: uri.New("file://tmp/"), - Charts: tt.args.parentCharts, + Charts: tt.args.chartsInStore, }, - // Node: tt.args.chart.ValuesFiles.MainValuesFile.Node, } valuesFeature := NewTemplateContextFeature(genericDocumentUseCase) got, err := valuesFeature.valuesHover(tt.args.splittedVar) diff --git a/internal/lsp/document.go b/internal/lsp/document.go index f20ffbf3..00fb9a57 100644 --- a/internal/lsp/document.go +++ b/internal/lsp/document.go @@ -45,7 +45,7 @@ func (d *Document) ApplyChanges(changes []lsp.TextDocumentContentChangeEvent) { d.Content = string(content) d.ApplyChangesToAst(d.Content) - d.SymbolTable = NewSymbolTable(d.Ast, []byte(d.Content)) + d.SymbolTable = NewSymbolTable(d.Ast, content) d.lines = nil } @@ -78,3 +78,13 @@ func (d *Document) getLines() []string { } return d.lines } + +// GetContent implements PossibleDependencyFile. +func (d *Document) GetContent() []byte { + return []byte(d.Content) +} + +// GetPath implements PossibleDependencyFile. +func (d *Document) GetPath() string { + return d.Path +} diff --git a/internal/lsp/document_store.go b/internal/lsp/document_store.go index 46432e77..b7cb0a41 100644 --- a/internal/lsp/document_store.go +++ b/internal/lsp/document_store.go @@ -2,7 +2,6 @@ package lsp import ( "fmt" - "os" "sync" "github.com/mrjosh/helm-ls/internal/util" @@ -51,32 +50,25 @@ func (s *DocumentStore) DidOpen(params *lsp.DidOpenTextDocumentParams, helmlsCon return doc, nil } -func (s *DocumentStore) Store(uri uri.URI, helmlsConfig util.HelmlsConfiguration) error { - _, ok := s.documents.Load(uri.Filename()) +func (s *DocumentStore) Store(filename string, content []byte, helmlsConfig util.HelmlsConfiguration) { + _, ok := s.documents.Load(filename) if ok { - return nil + return } - - content, err := os.ReadFile(uri.Filename()) - if err != nil { - logger.Error("Could not open file ", uri.Filename(), " ", err) - return err - } - ast := ParseAst(nil, string(content)) - s.documents.Store(uri.Filename(), + fileURI := uri.File(filename) + s.documents.Store(fileURI.Filename(), &Document{ - URI: uri, - Path: uri.Filename(), + URI: fileURI, + Path: filename, Content: string(content), Ast: ast, DiagnosticsCache: NewDiagnosticsCache(helmlsConfig), IsOpen: false, SymbolTable: NewSymbolTable(ast, content), - IsYaml: IsYamlDocument(uri, helmlsConfig.YamllsConfiguration), + IsYaml: IsYamlDocument(fileURI, helmlsConfig.YamllsConfiguration), }, ) - return nil } func (s *DocumentStore) Get(docuri uri.URI) (*Document, bool) { diff --git a/internal/protocol/hover.go b/internal/protocol/hover.go index 3b97ba62..db7bfc5f 100644 --- a/internal/protocol/hover.go +++ b/internal/protocol/hover.go @@ -16,7 +16,15 @@ type HoverResultWithFile struct { type HoverResultsWithFiles []HoverResultWithFile -func (h HoverResultsWithFiles) Format(rootURI uri.URI) string { +func (h HoverResultsWithFiles) FormatHelm(rootURI uri.URI) string { + return h.format(rootURI, "helm") +} + +func (h HoverResultsWithFiles) FormatYaml(rootURI uri.URI) string { + return h.format(rootURI, "yaml") +} + +func (h HoverResultsWithFiles) format(rootURI uri.URI, codeLanguage string) string { var formatted string sort.Slice(h, func(i, j int) bool { return h[i].URI > h[j].URI @@ -27,13 +35,13 @@ func (h HoverResultsWithFiles) Format(rootURI uri.URI) string { if value == "" { value = "\"\"" } else { - value = fmt.Sprintf("```yaml\n%s\n```", value) + value = fmt.Sprintf("```%s\n%s\n```", codeLanguage, value) } filepath, err := filepath.Rel(rootURI.Filename(), result.URI.Filename()) if err != nil { filepath = result.URI.Filename() } - formatted += fmt.Sprintf("### %s\n%s\n\n", filepath, value) + formatted += fmt.Sprintf("### %s\n%s\n", filepath, value) } return formatted } diff --git a/internal/protocol/hover_test.go b/internal/protocol/hover_test.go new file mode 100644 index 00000000..fbfd2f6b --- /dev/null +++ b/internal/protocol/hover_test.go @@ -0,0 +1,80 @@ +package protocol + +import ( + "fmt" + "path/filepath" + "testing" + + "go.lsp.dev/uri" + + "github.com/stretchr/testify/assert" +) + +func TestHoverResultsWithFiles_Format(t *testing.T) { + rootURI := uri.New("file:///home/user/project") + + results := HoverResultsWithFiles{ + {Value: "value1", URI: uri.New("file:///home/user/project/file1.yaml")}, + {Value: "value2", URI: uri.New("file:///home/user/project/file2.yaml")}, + {Value: "value3", URI: uri.New("file:///home/user/project/file3.yaml")}, + } + + expected := fmt.Sprintf(`### file3.yaml +%s +value3 +%s +### file2.yaml +%s +value2 +%s +### file1.yaml +%s +value1 +%s +`, "```yaml", "```", "```yaml", "```", "```yaml", "```") + + formatted := results.FormatYaml(rootURI) + assert.Equal(t, expected, formatted, "The formatted output should match the expected output") +} + +func TestHoverResultsWithFiles_Format_EmptyValue(t *testing.T) { + rootURI := uri.New("file:///home/user/project") + + results := HoverResultsWithFiles{ + {Value: "", URI: uri.New("file:///home/user/project/file1.yaml")}, + } + expected := `### file1.yaml +"" +` + + formatted := results.FormatYaml(rootURI) + assert.Equal(t, expected, formatted, "The formatted output should match the expected output") +} + +func TestHoverResultsWithFiles_Format_DifferenPath(t *testing.T) { + rootURI := uri.New("file:///home/user/project") + + results := HoverResultsWithFiles{ + {Value: "value", URI: uri.New("file:///invalid/uri")}, + } + + expected := fmt.Sprintf(`### %s +%s +value +%s +`, filepath.Join("..", "..", "..", "invalid", "uri"), "```yaml", "```") + formatted := results.FormatYaml(rootURI) + assert.Equal(t, expected, formatted, "The formatted output should match the expected output") +} + +func TestHoverResultWithFile_WithHelmCode(t *testing.T) { + hoverResult := HoverResultsWithFiles{ + { + Value: "some helm code", + URI: uri.New("file:///home/user/project/file1.yaml"), + }, + } + + expectedValue := "### file1.yaml\n```helm\nsome helm code\n```\n" + assert.Equal(t, expectedValue, hoverResult.FormatHelm(uri.New("file:///home/user/project")), "The value should be formatted with Helm code block") +} diff --git a/internal/util/values.go b/internal/util/values.go index b446b1a6..b4e409f4 100644 --- a/internal/util/values.go +++ b/internal/util/values.go @@ -200,6 +200,10 @@ func FormatToYAML(field reflect.Value, fieldName string) string { func toYAML(value interface{}) string { valBytes, _ := yaml.Marshal(value) + // remove trailing new line + if len(valBytes) > 0 && valBytes[len(valBytes)-1] == '\n' { + valBytes = valBytes[:len(valBytes)-1] + } return string(valBytes) } diff --git a/internal/util/values_test.go b/internal/util/values_test.go index 3672307e..911ae6b9 100644 --- a/internal/util/values_test.go +++ b/internal/util/values_test.go @@ -74,6 +74,6 @@ func TestValuesRangeLookupOnMapping(t *testing.T) { inputCopy := append([]string{}, input...) result, err := GetTableOrValueForSelector(values, input) assert.NoError(t, err) - assert.Equal(t, "a: 1\n", result) + assert.Equal(t, "a: 1", result) assert.Equal(t, inputCopy, input) } diff --git a/internal/util/yaml.go b/internal/util/yaml.go index ec2b9fea..77f614b0 100644 --- a/internal/util/yaml.go +++ b/internal/util/yaml.go @@ -2,7 +2,6 @@ package util import ( "fmt" - "os" "strings" lsp "go.lsp.dev/protocol" @@ -75,13 +74,8 @@ func getPositionOfNodeAfterRange(node *yamlv3.Node, query []string) (lsp.Positio return lsp.Position{}, fmt.Errorf("could not find Position of %s in values. Found no match", query) } -// ReadYamlFileToNode will parse a YAML file into a yaml Node. -func ReadYamlFileToNode(filename string) (node yamlv3.Node, err error) { - data, err := os.ReadFile(filename) - if err != nil { - return yamlv3.Node{}, err - } - +// ReadYamlToNode will parse a YAML file into a yaml Node. +func ReadYamlToNode(data []byte) (node yamlv3.Node, err error) { err = yamlv3.Unmarshal(data, &node) return node, err } diff --git a/testdata/dependenciesExample/.helmignore b/testdata/dependenciesExample/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/testdata/dependenciesExample/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/testdata/dependenciesExample/Chart.lock b/testdata/dependenciesExample/Chart.lock new file mode 100644 index 00000000..9d2ff4d0 --- /dev/null +++ b/testdata/dependenciesExample/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: common + repository: oci://registry-1.docker.io/bitnamicharts + version: 2.20.3 +- name: subchartexample + repository: "" + version: x.x.x +digest: sha256:17700eb1463d461f8c1a70e1ab96fadd0749adccf9970b6acb5b8c791374ac8a +generated: "2024-07-02T11:38:56.75677057Z" diff --git a/testdata/dependenciesExample/Chart.yaml b/testdata/dependenciesExample/Chart.yaml new file mode 100644 index 00000000..1049faca --- /dev/null +++ b/testdata/dependenciesExample/Chart.yaml @@ -0,0 +1,32 @@ +apiVersion: v2 +name: dependeciesExample +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" +dependencies: + - name: common + repository: oci://registry-1.docker.io/bitnamicharts + tags: + - bitnami-common + version: 2.20.3 + - name: subchartexample + version: x.x.x diff --git a/testdata/dependenciesExample/charts/common-2.20.3.tgz b/testdata/dependenciesExample/charts/common-2.20.3.tgz new file mode 100644 index 00000000..8624f69b Binary files /dev/null and b/testdata/dependenciesExample/charts/common-2.20.3.tgz differ diff --git a/testdata/dependenciesExample/charts/subchartexample/.helmignore b/testdata/dependenciesExample/charts/subchartexample/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/testdata/dependenciesExample/charts/subchartexample/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/testdata/dependenciesExample/charts/subchartexample/Chart.yaml b/testdata/dependenciesExample/charts/subchartexample/Chart.yaml new file mode 100644 index 00000000..1df061e3 --- /dev/null +++ b/testdata/dependenciesExample/charts/subchartexample/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: subchartexample +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/testdata/dependenciesExample/charts/subchartexample/templates/_helpers_subchart.tpl b/testdata/dependenciesExample/charts/subchartexample/templates/_helpers_subchart.tpl new file mode 100644 index 00000000..8c72ce3b --- /dev/null +++ b/testdata/dependenciesExample/charts/subchartexample/templates/_helpers_subchart.tpl @@ -0,0 +1,6 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "subchartexample.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} diff --git a/testdata/dependenciesExample/charts/subchartexample/templates/subchart.yaml b/testdata/dependenciesExample/charts/subchartexample/templates/subchart.yaml new file mode 100644 index 00000000..29a0b3d1 --- /dev/null +++ b/testdata/dependenciesExample/charts/subchartexample/templates/subchart.yaml @@ -0,0 +1,2 @@ +example2: {{ .Values.global.subchart }} +example3: {{ .Values.subchartWithoutGlobal }} diff --git a/testdata/dependenciesExample/charts/subchartexample/values.yaml b/testdata/dependenciesExample/charts/subchartexample/values.yaml new file mode 100644 index 00000000..6e49e9d3 --- /dev/null +++ b/testdata/dependenciesExample/charts/subchartexample/values.yaml @@ -0,0 +1,3 @@ +global: + subchart: works +subchartWithoutGlobal: works diff --git a/testdata/dependenciesExample/templates/NOTES.txt b/testdata/dependenciesExample/templates/NOTES.txt new file mode 100644 index 00000000..f2bda6bc --- /dev/null +++ b/testdata/dependenciesExample/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "dependeciesExample.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "dependeciesExample.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "dependeciesExample.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "dependeciesExample.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/testdata/dependenciesExample/templates/_helpers.tpl b/testdata/dependenciesExample/templates/_helpers.tpl new file mode 100644 index 00000000..fd2ea5b0 --- /dev/null +++ b/testdata/dependenciesExample/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "dependeciesExample.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "dependeciesExample.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "dependeciesExample.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "dependeciesExample.labels" -}} +helm.sh/chart: {{ include "dependeciesExample.chart" . }} +{{ include "dependeciesExample.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "dependeciesExample.selectorLabels" -}} +app.kubernetes.io/name: {{ include "dependeciesExample.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "dependeciesExample.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "dependeciesExample.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/testdata/dependenciesExample/templates/deployment.yaml b/testdata/dependenciesExample/templates/deployment.yaml new file mode 100644 index 00000000..e1aa3c39 --- /dev/null +++ b/testdata/dependenciesExample/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "common.names.name" . }} + labels: + {{- include "dependeciesExample.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "dependeciesExample.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "dependeciesExample.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "dependeciesExample.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + example: {{ .Values.common.exampleValue }} + example2: {{ .Values.global.subchart }} + example3: {{ .Values.subchartexample.subchartWithoutGlobal }} # todo fix this: missing hover, completion etc. + example4: {{ include "subchartexample.name" . }} diff --git a/testdata/dependenciesExample/values.yaml b/testdata/dependenciesExample/values.yaml new file mode 100644 index 00000000..4100784c --- /dev/null +++ b/testdata/dependenciesExample/values.yaml @@ -0,0 +1,54 @@ +affinity: {} +autoscaling: + enabled: false + maxReplicas: 100 + minReplicas: 1 + targetCPUUtilizationPercentage: 80 +fullnameOverride: "" +global: + subchart: works +image: + pullPolicy: IfNotPresent + repository: nginx + tag: "" +imagePullSecrets: [] +ingress: + annotations: {} + className: "" + enabled: false + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] +livenessProbe: + httpGet: + path: / + port: http +nameOverride: "" +nodeSelector: {} +podAnnotations: {} +podLabels: {} +podSecurityContext: {} +readinessProbe: + httpGet: + path: / + port: http +replicaCount: 1 +resources: {} +securityContext: {} +service: + port: 80 + type: ClusterIP +serviceAccount: + annotations: {} + automount: true + create: true + name: "" +subchartWithoutGlobal: works +subchartexample: + subchartWithoutGlobal: worksToo +tolerations: [] +volumeMounts: [] +volumes: []