diff --git a/example-charts/files-values/Chart.yaml b/example-charts/files-values/Chart.yaml new file mode 100644 index 0000000..1848555 --- /dev/null +++ b/example-charts/files-values/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: best-values-example +description: One of the best values parsing example charts here, exhibits several more complicated examples +version: "0.2.0" +home: "https://github.com/norwoodj/helm-docs/tree/master/example-charts/best-values-example" +sources: ["https://github.com/norwoodj/helm-docs/tree/master/example-charts/best-values-example"] +engine: gotpl +type: application +maintainers: + - email: norwood.john.m@gmail.com + name: John Norwood diff --git a/example-charts/files-values/README.md b/example-charts/files-values/README.md new file mode 100644 index 0000000..d8a2d57 --- /dev/null +++ b/example-charts/files-values/README.md @@ -0,0 +1,60 @@ +# best-values-example + +One of the best values parsing example charts here, exhibits several more complicated examples + +![Version: 0.2.0](https://img.shields.io/badge/Version-0.2.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) + +## Additional Information + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore +et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut +aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse +cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in +culpa qui officia deserunt mollit anim id est laborum. + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```console +$ helm repo add foo-bar http://charts.foo-bar.com +$ helm install my-release foo-bar/best-values-example +``` + +Some file contents: + +``` +some: + data: "test" +``` + +Glob contents as config map: + +``` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + resource1.yaml: |- + some: + resource: "blah" + resource2.yaml: |- + some: + resource: "blah2" +``` + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| config.databasesToCreate[0] | string | `"postgresql"` | default database for storage of database metadata | +| config.databasesToCreate[1] | string | `"hashbash"` | database for the [hashbash](https://github.com/norwoodj/hashbash) project | +| config.usersToCreate[0] | object | `{"admin":true,"name":"root"}` | admin user | +| config.usersToCreate[1] | object | `{"name":"hashbash","readwriteDatabases":["hashbash"]}` | user with access to the database with the same name | +| statefulset.extraVolumes | list | `[{"emptyDir":{},"name":"data"}]` | Additional volumes to be mounted into the database container | +| statefulset.image.repository | string | `"jnorwood/postgresq"` | Image to use for deploying, must support an entrypoint which creates users/databases from appropriate config files | +| statefulset.image.tag | string | `"11"` | | +| statefulset.livenessProbe | object | `{"enabled":false}` | Configure the healthcheck for the database | +| statefulset.podLabels | object | `{}` | The labels to be applied to instances of the database | + diff --git a/example-charts/files-values/README.md.gotmpl b/example-charts/files-values/README.md.gotmpl new file mode 100644 index 0000000..87a949c --- /dev/null +++ b/example-charts/files-values/README.md.gotmpl @@ -0,0 +1,44 @@ +{{ template "chart.header" . }} +{{ template "chart.description" . }} + +{{ template "chart.versionBadge" . }}{{ template "chart.typeBadge" . }}{{ template "chart.appVersionBadge" . }} + +## Additional Information + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore +et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut +aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse +cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in +culpa qui officia deserunt mollit anim id est laborum. + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```console +$ helm repo add foo-bar http://charts.foo-bar.com +$ helm install my-release foo-bar/{{ template "chart.name" . }} +``` + +Some file contents: + +``` +{{ .Files.Get "somefile.yaml" }} +``` + +Glob contents as config map: + +``` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: +{{ (.Files.Glob "templates/**.yaml").AsConfig | indent 2 }} +``` + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/example-charts/files-values/somefile.yaml b/example-charts/files-values/somefile.yaml new file mode 100644 index 0000000..11c8c89 --- /dev/null +++ b/example-charts/files-values/somefile.yaml @@ -0,0 +1,2 @@ +some: + data: "test" \ No newline at end of file diff --git a/example-charts/files-values/templates/resource1.yaml b/example-charts/files-values/templates/resource1.yaml new file mode 100644 index 0000000..e287a9c --- /dev/null +++ b/example-charts/files-values/templates/resource1.yaml @@ -0,0 +1,2 @@ +some: + resource: "blah" \ No newline at end of file diff --git a/example-charts/files-values/templates/resource2.yaml b/example-charts/files-values/templates/resource2.yaml new file mode 100644 index 0000000..cf8accc --- /dev/null +++ b/example-charts/files-values/templates/resource2.yaml @@ -0,0 +1,2 @@ +some: + resource: "blah2" \ No newline at end of file diff --git a/example-charts/files-values/values.yaml b/example-charts/files-values/values.yaml new file mode 100644 index 0000000..a218d30 --- /dev/null +++ b/example-charts/files-values/values.yaml @@ -0,0 +1,32 @@ +statefulset: + image: + # -- Image to use for deploying, must support an entrypoint which creates users/databases from appropriate config files + repository: jnorwood/postgresq + tag: "11" + + # -- Additional volumes to be mounted into the database container + extraVolumes: + - name: data + emptyDir: {} + + # -- Configure the healthcheck for the database + livenessProbe: + enabled: false + + # -- The labels to be applied to instances of the database + podLabels: {} + +config: + databasesToCreate: + # -- default database for storage of database metadata + - postgresql + + # -- database for the [hashbash](https://github.com/norwoodj/hashbash) project + - hashbash + + usersToCreate: + # -- admin user + - {name: root, admin: true} + + # -- user with access to the database with the same name + - {name: hashbash, readwriteDatabases: [hashbash]} diff --git a/go.mod b/go.mod index 6c72bfd..cf38003 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( github.com/Masterminds/sprig/v3 v3.2.2 + github.com/gobwas/glob v0.2.3 github.com/sirupsen/logrus v1.2.0 github.com/spf13/cobra v0.0.5 github.com/spf13/viper v1.4.0 diff --git a/go.sum b/go.sum index 0c8c496..19ef50f 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= diff --git a/pkg/document/files.go b/pkg/document/files.go new file mode 100644 index 0000000..fdec92e --- /dev/null +++ b/pkg/document/files.go @@ -0,0 +1,136 @@ +package document + +import ( + "encoding/base64" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + + "github.com/gobwas/glob" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" +) + +// Near identical to https://github.com/helm/helm/blob/main/pkg/engine/files.go as to preserve the interface. + +type fileEntry struct { + Path string + data []byte +} + +func (f *fileEntry) GetData() []byte { + if f.data == nil { + data, err := ioutil.ReadFile(f.Path) + if err != nil { + log.Warnf("Error reading file contents for %s: %s", f.Path, err.Error()) + return []byte{} + } + f.data = data + } + + return f.data +} + +type files map[string]*fileEntry + +func getFiles(dir string) (files, error) { + result := make(files) + + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + result[path] = &fileEntry{Path: path} + return nil + }) + if err != nil { + return map[string]*fileEntry{}, err + } + + return result, nil +} + +func (f files) GetBytes(name string) []byte { + if v, ok := f[name]; ok { + return v.GetData() + } + return []byte{} +} + +func (f files) Get(name string) string { + return string(f.GetBytes(name)) +} + +func (f files) Glob(pattern string) files { + result := make(files) + g, err := glob.Compile(pattern, '/') + if err != nil { + log.Warnf("Error compiling Glob patten %s: %s", pattern, err.Error()) + return result + } + + for filePath, entry := range f { + if g.Match(filePath) { + result[filePath] = entry + } + } + + return result +} + +func (f files) AsConfig() string { + if f == nil { + return "" + } + + m := make(map[string]string) + + // Explicitly convert to strings, and file names + for k, v := range f { + m[path.Base(k)] = string(v.GetData()) + } + + return toYAML(m) +} + +func (f files) AsSecrets() string { + if f == nil { + return "" + } + + m := make(map[string]string) + + for k, v := range f { + m[path.Base(k)] = base64.StdEncoding.EncodeToString(v.GetData()) + } + + return toYAML(m) +} + +func (f files) Lines(path string) []string { + if f == nil { + return []string{} + } + entry, exists := f[path] + if !exists { + return []string{} + } + + return strings.Split(string(entry.GetData()), "\n") +} + +func toYAML(v interface{}) string { + data, err := yaml.Marshal(v) + if err != nil { + // Swallow errors inside a template. + return "" + } + return strings.TrimSuffix(string(data), "\n") +} diff --git a/pkg/document/files_test.go b/pkg/document/files_test.go new file mode 100644 index 0000000..769f33f --- /dev/null +++ b/pkg/document/files_test.go @@ -0,0 +1,134 @@ +package document + +import ( + "github.com/stretchr/testify/assert" + "os" + "path" + "testing" +) + +// As the interface has been kept the same as in Helm, the tests also work here. +// Tests similar to https://github.com/helm/helm/blob/main/pkg/engine/files_test.go. + +var cases = []struct { + path, data string +}{ + {"ship/captain.txt", "The Captain"}, + {"ship/stowaway.txt", "Legatt"}, + {"story/name.txt", "The Secret Sharer"}, + {"story/author.txt", "Joseph Conrad"}, + {"multiline/test.txt", "bar\nfoo"}, +} + +func getTestFiles() files { + a := make(files, len(cases)) + for _, c := range cases { + a[c.path] = &fileEntry{ + Path: c.path, + data: []byte(c.data), + } + } + return a +} + +func TestNewFiles(t *testing.T) { + files := getTestFiles() + + if len(files) != len(cases) { + t.Errorf("Expected len() = %d, got %d", len(cases), len(files)) + } + + for i, f := range cases { + if got := string(files.GetBytes(f.path)); got != f.data { + t.Errorf("%d: expected %q, got %q", i, f.data, got) + } + if got := files.Get(f.path); got != f.data { + t.Errorf("%d: expected %q, got %q", i, f.data, got) + } + } +} + +func TestFileGlob(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + + matched := f.Glob("story/**") + + as.Len(matched, 2, "Should be two files in glob story/**") + as.Equal("Joseph Conrad", matched.Get("story/author.txt")) +} + +func TestToConfig(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + out := f.Glob("**/captain.txt").AsConfig() + as.Equal("captain.txt: The Captain", out) + + out = f.Glob("ship/**").AsConfig() + as.Equal("captain.txt: The Captain\nstowaway.txt: Legatt", out) +} + +func TestToSecret(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + + out := f.Glob("ship/**").AsSecrets() + as.Equal("captain.txt: VGhlIENhcHRhaW4=\nstowaway.txt: TGVnYXR0", out) +} + +func TestLines(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + + out := f.Lines("multiline/test.txt") + as.Len(out, 2) + + as.Equal("bar", out[0]) +} + +func TestGetFiles(t *testing.T) { + chartDir, err := os.MkdirTemp("", "*-helm-docs-chart") + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + _ = os.RemoveAll(chartDir) + }) + + testFiles := getTestFiles() + for filePath, entry := range testFiles { + fullPath := path.Join(chartDir, filePath) + baseDir := path.Dir(fullPath) + if err = os.MkdirAll(baseDir, 0o755); err != nil { + t.Fatal(err) + } + data := entry.GetData() + + if err = os.WriteFile(fullPath, data, 0o644); err != nil { + t.Fatal(err) + } + } + + chartFiles, err := getFiles(chartDir) + if err != nil { + t.Fatal(err) + } + + if len(chartFiles) != len(testFiles) { + t.Errorf("chart files: expected %d, got %d", len(chartFiles), len(testFiles)) + } + + // Sanity check the files have been read + for filePath, entry := range chartFiles { + data := entry.GetData() + + if len(data) == 0 { + t.Errorf("%s: expected file contents, got 0 bytes", filePath) + } + } +} diff --git a/pkg/document/model.go b/pkg/document/model.go index 426a33d..026cb6c 100644 --- a/pkg/document/model.go +++ b/pkg/document/model.go @@ -30,6 +30,7 @@ type chartTemplateData struct { helm.ChartDocumentationInfo HelmDocsVersion string Values []valueRow + Files files } func sortValueRows(valueRows []valueRow) { @@ -135,10 +136,16 @@ func getChartTemplateData(info helm.ChartDocumentationInfo, helmDocsVersion stri sortValueRows(valuesTableRows) + files, err := getFiles(info.ChartDirectory) + if err != nil { + return chartTemplateData{}, err + } + return chartTemplateData{ ChartDocumentationInfo: info, HelmDocsVersion: helmDocsVersion, Values: valuesTableRows, + Files: files, }, nil }