Skip to content

Commit 5b1dcc4

Browse files
authored
feat: read in dependency charts (#80)
* feat: read in dependency charts * feat: sync values files to disk for definition * feat: test definition with dependencies * fix: add ParentChart for NewChartFromHelmChart * fix(hover): correct helm code formatting * docs(dependencies): add not about helm dependency build
1 parent 74c5712 commit 5b1dcc4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1425
-182
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
/dist
33
__debug_bin*
44
.vscode
5+
56
.coverage
7+
testdata/dependenciesExample/charts/.helm_ls_cache/
8+
helm-ls
9+
helm_ls

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Helm-ls is a [helm](https://github.com/helm/helm) language server protocol [LSP]
2727
- [Manual download](#manual-download)
2828
- [Make it executable](#make-it-executable)
2929
- [Integration with yaml-language-server](#integration-with-yaml-language-server)
30+
- [Dependency Charts](#dependency-charts)
3031
- [Configuration options](#configuration-options)
3132
- [General](#general)
3233
- [Values Files](#values-files)
@@ -140,6 +141,15 @@ apiVersion: keda.sh/v1alpha1
140141
kind: ScaledObject
141142
```
142143
144+
### Dependency Charts
145+
146+
Helm-ls can process dependency charts to provide autocompletion, hover etc. with values from the dependencies.
147+
For this the dependency charts have to be downloaded. Run the following command in your project to download them:
148+
149+
```bash
150+
helm dependency build
151+
```
152+
143153
## Configuration options
144154

145155
You can configure helm-ls with lsp workspace configurations.

cmds/lint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func newLintCmd() *cobra.Command {
2020
}
2121

2222
rootPath := uri.File(args[0])
23-
chartStore := charts.NewChartStore(rootPath, charts.NewChart)
23+
chartStore := charts.NewChartStore(rootPath, charts.NewChart, func(chart *charts.Chart) {})
2424
chart, err := chartStore.GetChartForURI(rootPath)
2525
if err != nil {
2626
return err

internal/adapter/yamlls/diagnostics.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package yamlls
22

33
import (
44
"context"
5+
"fmt"
56
"runtime"
67
"strings"
78

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

2022
doc.DiagnosticsCache.SetYamlDiagnostics(filterDiagnostics(params.Diagnostics, doc.Ast.Copy(), doc.Content))

internal/adapter/yamlls/yamlls_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package yamlls
22

33
import (
4+
"os"
45
"testing"
56

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

1920
connector.documents = &lsplocal.DocumentStore{}
20-
yamlFile := uri.File("../../../testdata/example/templates/deployment.yaml")
21-
nonYamlFile := uri.File("../../../testdata/example/templates/_helpers.tpl")
22-
connector.documents.Store(yamlFile, util.DefaultConfig)
23-
connector.documents.Store(nonYamlFile, util.DefaultConfig)
21+
yamlFile := "../../../testdata/example/templates/deployment.yaml"
22+
nonYamlFile := "../../../testdata/example/templates/_helpers.tpl"
2423

25-
assert.True(t, connector.isRelevantFile(yamlFile))
26-
assert.False(t, connector.isRelevantFile(nonYamlFile))
24+
yamlFileContent, err := os.ReadFile(yamlFile)
25+
assert.NoError(t, err)
26+
27+
nonYamlFileContent, err := os.ReadFile(nonYamlFile)
28+
assert.NoError(t, err)
29+
30+
connector.documents.Store(yamlFile, yamlFileContent, util.DefaultConfig)
31+
connector.documents.Store(nonYamlFile, nonYamlFileContent, util.DefaultConfig)
32+
33+
assert.True(t, connector.isRelevantFile(uri.File(yamlFile)))
34+
assert.False(t, connector.isRelevantFile(uri.File(nonYamlFile)))
2735
}
2836

2937
func TestShouldRun(t *testing.T) {

internal/charts/chart.go

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package charts
22

33
import (
4+
"fmt"
45
"strings"
56

67
"github.com/mrjosh/helm-ls/internal/log"
78
"github.com/mrjosh/helm-ls/internal/util"
89
lsp "go.lsp.dev/protocol"
910
"go.lsp.dev/uri"
11+
"helm.sh/helm/v3/pkg/chart"
12+
"helm.sh/helm/v3/pkg/chart/loader"
1013
)
1114

1215
var logger = log.GetLogger()
@@ -16,9 +19,12 @@ type Chart struct {
1619
ChartMetadata *ChartMetadata
1720
RootURI uri.URI
1821
ParentChart ParentChart
22+
HelmChart *chart.Chart
1923
}
2024

2125
func NewChart(rootURI uri.URI, valuesFilesConfig util.ValuesFilesConfig) *Chart {
26+
helmChart := loadHelmChart(rootURI)
27+
2228
return &Chart{
2329
ValuesFiles: NewValuesFiles(rootURI,
2430
valuesFilesConfig.MainValuesFileName,
@@ -27,50 +33,39 @@ func NewChart(rootURI uri.URI, valuesFilesConfig util.ValuesFilesConfig) *Chart
2733
ChartMetadata: NewChartMetadata(rootURI),
2834
RootURI: rootURI,
2935
ParentChart: newParentChart(rootURI),
36+
HelmChart: helmChart,
3037
}
3138
}
3239

33-
type QueriedValuesFiles struct {
34-
Selector []string
35-
ValuesFiles *ValuesFiles
36-
}
37-
38-
// ResolveValueFiles returns a list of all values files in the chart
39-
// and all parent charts if the query tries to access global values
40-
func (c *Chart) ResolveValueFiles(query []string, chartStore *ChartStore) []*QueriedValuesFiles {
41-
ownResult := []*QueriedValuesFiles{{Selector: query, ValuesFiles: c.ValuesFiles}}
42-
if len(query) == 0 {
43-
return ownResult
40+
func loadHelmChart(rootURI uri.URI) (helmChart *chart.Chart) {
41+
chartLoader, err := loader.Loader(rootURI.Filename())
42+
if err != nil {
43+
logger.Error(fmt.Sprintf("Error loading chart %s: %s", rootURI.Filename(), err.Error()))
44+
return nil
4445
}
4546

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

51-
if query[0] == "global" {
52-
return append(ownResult,
53-
parentChart.ResolveValueFiles(query, chartStore)...)
54-
}
55-
56-
chartName := c.ChartMetadata.Metadata.Name
57-
extendedQuery := append([]string{chartName}, query...)
58-
return append(ownResult,
59-
parentChart.ResolveValueFiles(extendedQuery, chartStore)...)
52+
return helmChart
6053
}
6154

62-
func (c *Chart) GetValueLocation(templateContext []string) (lsp.Location, error) {
63-
modifyedVar := make([]string, len(templateContext))
55+
func (c *Chart) GetMetadataLocation(templateContext []string) (lsp.Location, error) {
56+
modifyedVar := []string{}
6457
// make the first letter lowercase since in the template the first letter is
6558
// capitalized, but it is not in the Chart.yaml file
6659
for _, value := range templateContext {
6760
restOfString := ""
6861
if (len(value)) > 1 {
6962
restOfString = value[1:]
7063
}
64+
7165
firstLetterLowercase := strings.ToLower(string(value[0])) + restOfString
7266
modifyedVar = append(modifyedVar, firstLetterLowercase)
7367
}
68+
7469
position, err := util.GetPositionOfNode(&c.ChartMetadata.YamlNode, modifyedVar)
7570

7671
return lsp.Location{URI: c.ChartMetadata.URI, Range: lsp.Range{Start: position}}, err

internal/charts/chart_dependecies.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package charts
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"go.lsp.dev/uri"
8+
)
9+
10+
func (c *Chart) GetDependecyURI(dependencyName string) uri.URI {
11+
unpackedPath := filepath.Join(c.RootURI.Filename(), "charts", dependencyName)
12+
fileInfo, err := os.Stat(unpackedPath)
13+
14+
if err == nil && fileInfo.IsDir() {
15+
return uri.File(unpackedPath)
16+
}
17+
18+
return uri.File(filepath.Join(c.RootURI.Filename(), "charts", DependencyCacheFolder, dependencyName))
19+
}
20+
21+
func (c *Chart) GetDependeciesTemplates() []*DependencyTemplateFile {
22+
result := []*DependencyTemplateFile{}
23+
if c.HelmChart == nil {
24+
return result
25+
}
26+
27+
for _, dependency := range c.HelmChart.Dependencies() {
28+
for _, file := range dependency.Templates {
29+
dependencyTemplate := c.NewDependencyTemplateFile(dependency.Name(), file)
30+
result = append(result, dependencyTemplate)
31+
}
32+
}
33+
34+
return result
35+
}

internal/charts/chart_for_document.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ func (s *ChartStore) GetChartForDoc(uri lsp.DocumentURI) (*Chart, error) {
1818
}
1919

2020
chart, err := s.getChartFromFilesystemForTemplates(uri.Filename())
21-
s.Charts[chart.RootURI] = chart
2221
if err != nil {
2322
return chart, ErrChartNotFound{
2423
URI: uri,
2524
}
2625
}
26+
s.AddChart(chart)
27+
2728
return chart, nil
2829
}
2930

internal/charts/chart_for_document_test.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import (
1010
"github.com/mrjosh/helm-ls/internal/util"
1111
"github.com/stretchr/testify/assert"
1212
"go.lsp.dev/uri"
13+
"helm.sh/helm/v3/pkg/chart"
1314
)
1415

1516
func TestGetChartForDocumentWorksForAlreadyAddedCharts(t *testing.T) {
1617
chartStore := charts.NewChartStore("file:///tmp", func(uri uri.URI, _ util.ValuesFilesConfig) *charts.Chart {
1718
return &charts.Chart{RootURI: uri}
18-
})
19+
}, addChartCallback)
1920

2021
chart := &charts.Chart{}
2122
chartStore.Charts["file:///tmp/chart"] = chart
@@ -53,10 +54,11 @@ func TestGetChartForDocumentWorksForNewToAddChart(t *testing.T) {
5354
rootDir = t.TempDir()
5455
expectedChartDirectory = filepath.Join(rootDir, "chart")
5556
expectedChart = &charts.Chart{
56-
RootURI: uri.File(expectedChartDirectory),
57+
RootURI: uri.File(expectedChartDirectory),
58+
HelmChart: &chart.Chart{},
5759
}
5860
newChartFunc = func(_ uri.URI, _ util.ValuesFilesConfig) *charts.Chart { return expectedChart }
59-
chartStore = charts.NewChartStore(uri.File(rootDir), newChartFunc)
61+
chartStore = charts.NewChartStore(uri.File(rootDir), newChartFunc, addChartCallback)
6062
err = os.MkdirAll(expectedChartDirectory, 0o755)
6163
)
6264
assert.NoError(t, err)
@@ -78,10 +80,11 @@ func TestGetChartForDocumentWorksForNewToAddChartWithNestedFile(t *testing.T) {
7880
rootDir = t.TempDir()
7981
expectedChartDirectory = filepath.Join(rootDir, "chart")
8082
expectedChart = &charts.Chart{
81-
RootURI: uri.File(expectedChartDirectory),
83+
RootURI: uri.File(expectedChartDirectory),
84+
HelmChart: &chart.Chart{},
8285
}
8386
newChartFunc = func(_ uri.URI, _ util.ValuesFilesConfig) *charts.Chart { return expectedChart }
84-
chartStore = charts.NewChartStore(uri.File(rootDir), newChartFunc)
87+
chartStore = charts.NewChartStore(uri.File(rootDir), newChartFunc, addChartCallback)
8588
err = os.MkdirAll(expectedChartDirectory, 0o755)
8689
)
8790
assert.NoError(t, err)
@@ -98,7 +101,7 @@ func TestGetChartForDocumentWorksForNewToAddChartWithNestedFile(t *testing.T) {
98101
func TestGetChartOrParentForDocWorks(t *testing.T) {
99102
chartStore := charts.NewChartStore("file:///tmp", func(uri uri.URI, _ util.ValuesFilesConfig) *charts.Chart {
100103
return &charts.Chart{RootURI: uri}
101-
})
104+
}, addChartCallback)
102105

103106
chart := &charts.Chart{}
104107
chartStore.Charts["file:///tmp/chart"] = chart
@@ -135,3 +138,20 @@ func TestGetChartOrParentForDocWorks(t *testing.T) {
135138
assert.Error(t, error)
136139
assert.Equal(t, &charts.Chart{RootURI: uri.File("/tmp")}, result5)
137140
}
141+
142+
func TestGetChartForDocumentWorksForChartWithDependencies(t *testing.T) {
143+
var (
144+
rootDir = "../../testdata/dependenciesExample/"
145+
chartStore = charts.NewChartStore(uri.File(rootDir), charts.NewChart, addChartCallback)
146+
)
147+
148+
result1, error := chartStore.GetChartForDoc(uri.File(filepath.Join(rootDir, "templates", "deployment.yaml")))
149+
assert.NoError(t, error)
150+
151+
assert.Len(t, result1.HelmChart.Dependencies(), 2)
152+
assert.Len(t, chartStore.Charts, 3)
153+
154+
assert.NotNil(t, chartStore.Charts[uri.File(rootDir)])
155+
assert.NotNil(t, chartStore.Charts[uri.File(filepath.Join(rootDir, "charts", "subchartexample"))])
156+
assert.NotNil(t, chartStore.Charts[uri.File(filepath.Join(rootDir, "charts", charts.DependencyCacheFolder, "common"))])
157+
}

internal/charts/chart_store.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,27 @@ type ChartStore struct {
1111
Charts map[uri.URI]*Chart
1212
RootURI uri.URI
1313
newChart func(uri.URI, util.ValuesFilesConfig) *Chart
14+
addChartCallback func(chart *Chart)
1415
valuesFilesConfig util.ValuesFilesConfig
1516
}
1617

17-
func NewChartStore(rootURI uri.URI, newChart func(uri.URI, util.ValuesFilesConfig) *Chart) *ChartStore {
18+
func NewChartStore(rootURI uri.URI, newChart func(uri.URI, util.ValuesFilesConfig) *Chart, addChartCallback func(chart *Chart)) *ChartStore {
1819
return &ChartStore{
1920
Charts: map[uri.URI]*Chart{},
2021
RootURI: rootURI,
2122
newChart: newChart,
23+
addChartCallback: addChartCallback,
2224
valuesFilesConfig: util.DefaultConfig.ValuesFilesConfig,
2325
}
2426
}
2527

28+
// AddChart adds a new chart to the store and loads its dependencies
29+
func (s *ChartStore) AddChart(chart *Chart) {
30+
s.Charts[chart.RootURI] = chart
31+
s.loadChartDependencies(chart)
32+
s.addChartCallback(chart)
33+
}
34+
2635
func (s *ChartStore) SetValuesFilesConfig(valuesFilesConfig util.ValuesFilesConfig) {
2736
logger.Debug("SetValuesFilesConfig", valuesFilesConfig)
2837
if valuesFilesConfig.MainValuesFileName == s.valuesFilesConfig.MainValuesFileName &&
@@ -32,7 +41,7 @@ func (s *ChartStore) SetValuesFilesConfig(valuesFilesConfig util.ValuesFilesConf
3241
}
3342
s.valuesFilesConfig = valuesFilesConfig
3443
for uri := range s.Charts {
35-
s.Charts[uri] = s.newChart(uri, valuesFilesConfig)
44+
s.AddChart(s.newChart(uri, valuesFilesConfig))
3645
}
3746
}
3847

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

5059
if chart != nil {
51-
s.Charts[chart.RootURI] = chart
60+
s.AddChart(chart)
5261
return chart, nil
5362
}
5463

@@ -64,9 +73,19 @@ func (s *ChartStore) ReloadValuesFile(file uri.URI) {
6473
logger.Error("Error reloading values file", file, err)
6574
return
6675
}
76+
6777
for _, valuesFile := range chart.ValuesFiles.AllValuesFiles() {
6878
if valuesFile.URI == file {
6979
valuesFile.Reload()
7080
}
7181
}
7282
}
83+
84+
func (s *ChartStore) loadChartDependencies(chart *Chart) {
85+
for _, dependency := range chart.HelmChart.Dependencies() {
86+
dependencyURI := chart.GetDependecyURI(dependency.Name())
87+
chart := NewChartFromHelmChart(dependency, dependencyURI)
88+
89+
s.AddChart(chart)
90+
}
91+
}

0 commit comments

Comments
 (0)