diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml index 2f14acb..a7c6284 100644 --- a/helm-charts/secrets-operator/Chart.yaml +++ b/helm-charts/secrets-operator/Chart.yaml @@ -13,9 +13,9 @@ 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: v0.11.7 +version: v0.11.8 # 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: "v0.11.7" +appVersion: "v0.11.8" diff --git a/helm-charts/secrets-operator/values.yaml b/helm-charts/secrets-operator/values.yaml index 9939330..81ca61c 100644 --- a/helm-charts/secrets-operator/values.yaml +++ b/helm-charts/secrets-operator/values.yaml @@ -31,7 +31,7 @@ controllerManager: readOnlyRootFilesystem: true image: repository: infisical/kubernetes-operator - tag: v0.11.7 + tag: v0.11.8 resources: limits: cpu: 500m diff --git a/internal/model/model.go b/internal/model/model.go index 3f62611..7d0196f 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -40,6 +40,11 @@ func (o V1TemplateOptions) String() string { return o.Value } +type V1Folder struct { + Name string `json:"name"` + Path string `json:"path"` +} + type Project struct { ID string `json:"id"` Name string `json:"name"` diff --git a/internal/template/v1/template.go b/internal/template/v1/template.go index 59c31cf..d5f582b 100644 --- a/internal/template/v1/template.go +++ b/internal/template/v1/template.go @@ -3,6 +3,7 @@ package v1 import ( "bytes" "fmt" + "sort" "strings" tpl "text/template" @@ -165,5 +166,40 @@ func newTemplate(name, templateString string, ctx TemplateContext) (*tpl.Templat } return *child.Secret, nil } + funcs["foldersIn"] = func(dir string) []model.V1Folder { + current := tree + for _, seg := range strings.Split(strings.Trim(dir, "/"), "/") { + if seg == "" { + continue + } + if current.Children == nil { + return []model.V1Folder{} + } + child, exists := current.Children[seg] + if !exists { + return []model.V1Folder{} + } + current = child + } + + basePath := "/" + strings.Trim(dir, "/") + + result := make([]model.V1Folder, 0) + for childName, child := range current.Children { + if len(child.Children) == 0 { + // A pure leaf is a secret, not a subdirectory. A node may carry + // both a Secret and Children when a secret key name collides with + // a folder segment; such a node is still a valid subdirectory. + continue + } + result = append(result, model.V1Folder{ + Name: childName, + Path: strings.TrimRight(basePath, "/") + "/" + childName, + }) + } + + sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) + return result + } return tpl.New(name).Funcs(funcs).Parse(templateString) } diff --git a/internal/template/v1/template_test.go b/internal/template/v1/template_test.go index 2424509..47e5f77 100644 --- a/internal/template/v1/template_test.go +++ b/internal/template/v1/template_test.go @@ -504,3 +504,78 @@ other_path: "{{ (secretFrom "/folder/other" "API_KEY").SecretPath }}"` Expect(err.Error()).To(ContainSubstring("wrong number of args")) }) }) + +var _ = Describe("RenderPerKeyTemplates with subdirectories", func() { + + It("lists immediate subdirectory names under a path", func() { + tmpls := map[string]string{ + "dirs": `{{ range foldersIn "/folder" }}{{ .Name }},{{ end }}`, + } + + data, err := v1.RenderPerKeyTemplates(tmpls, subfolderCtx) + Expect(err).NotTo(HaveOccurred()) + Expect(data).To(HaveKeyWithValue("dirs", []byte("other,subfolder,"))) + }) + + It("exposes .Name and .Path for each subdirectory", func() { + tmpls := map[string]string{ + "detail": `{{ range foldersIn "/folder" }}{{ .Name }}|{{ .Path }};{{ end }}`, + } + + data, err := v1.RenderPerKeyTemplates(tmpls, subfolderCtx) + Expect(err).NotTo(HaveOccurred()) + Expect(data).To(HaveKeyWithValue("detail", []byte("other|/folder/other;subfolder|/folder/subfolder;"))) + }) + + It("lists top-level folders when passed the root path", func() { + tmpls := map[string]string{ + "dirs": `{{ range foldersIn "/" }}{{ .Name }}={{ .Path }},{{ end }}`, + } + + data, err := v1.RenderPerKeyTemplates(tmpls, subfolderCtx) + Expect(err).NotTo(HaveOccurred()) + Expect(data).To(HaveKeyWithValue("dirs", []byte("folder=/folder,"))) + }) + + It("returns an empty list without error for a path not in the tree", func() { + tmpls := map[string]string{ + "dirs": `[{{ range foldersIn "/missing" }}{{ .Name }}{{ end }}]`, + } + + data, err := v1.RenderPerKeyTemplates(tmpls, subfolderCtx) + Expect(err).NotTo(HaveOccurred()) + Expect(data).To(HaveKeyWithValue("dirs", []byte("[]"))) + }) + + It("does not include secret leaves, only folder nodes", func() { + tmpls := map[string]string{ + // /folder/subfolder holds only secrets (DB_HOST, DB_PORT, API_KEY) and no subdirectories. + "dirs": `[{{ range foldersIn "/folder/subfolder" }}{{ .Name }}{{ end }}]`, + } + + data, err := v1.RenderPerKeyTemplates(tmpls, subfolderCtx) + Expect(err).NotTo(HaveOccurred()) + Expect(data).To(HaveKeyWithValue("dirs", []byte("[]"))) + }) + + It("includes a folder whose name collides with a secret key at the same path", func() { + // A root secret key "db" and a "/db" folder produce a single tree node + // carrying both a Secret and Children. It must still be listed as a subdirectory. + collisionCtx := v1.NewTemplateContext( + v1.RenderContext{ + RawSecrets: []api.Secret{ + {SecretKey: "db", SecretValue: "some-value", SecretPath: "/"}, + {SecretKey: "PASSWORD", SecretValue: "secret", SecretPath: "/db"}, + }, + }, + ) + + tmpls := map[string]string{ + "dirs": `{{ range foldersIn "/" }}{{ .Name }}={{ .Path }},{{ end }}`, + } + + data, err := v1.RenderPerKeyTemplates(tmpls, collisionCtx) + Expect(err).NotTo(HaveOccurred()) + Expect(data).To(HaveKeyWithValue("dirs", []byte("db=/db,"))) + }) +})