From 51b36f47adeed0516e593d3d648f3ec6bb2cdc69 Mon Sep 17 00:00:00 2001 From: Umberto Toniolo Date: Wed, 13 May 2026 07:33:47 +0200 Subject: [PATCH] feat: add recursive flag to interpolate command --- pkg/cmd/interpolate/interpolate.go | 41 +++++++++++++++++-- pkg/cmd/interpolate/interpolate_test.go | 11 +++++ .../results-recursive/inner/inner.yaml | 6 +++ .../results-recursive/no-interpolation.yaml | 10 +++++ .../testdata/results-recursive/other-file.yml | 11 +++++ 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 pkg/cmd/interpolate/testdata/results-recursive/inner/inner.yaml create mode 100644 pkg/cmd/interpolate/testdata/results-recursive/no-interpolation.yaml create mode 100644 pkg/cmd/interpolate/testdata/results-recursive/other-file.yml diff --git a/pkg/cmd/interpolate/interpolate.go b/pkg/cmd/interpolate/interpolate.go index e5c7d42..a650910 100644 --- a/pkg/cmd/interpolate/interpolate.go +++ b/pkg/cmd/interpolate/interpolate.go @@ -75,6 +75,11 @@ const ( allowMissingFilenamesFlagUsage = "set to allow missing input filenames" allowMissingFilenamesFlagDefault = false + recursiveFlagName = "recursive" + recursiveFlagShort = "r" + recursiveFlagUsage = "recursively interpolate files in subdirectories" + recursiveFlagDefault = false + outputFlagName = "out" outputFlagShort = "o" outputFlagUsage = "output directory where interpolated files are saved" @@ -97,6 +102,7 @@ type Flags struct { prefixes []string inputPaths []string allowMissingFilenames bool + recursive bool outputPath string } @@ -105,6 +111,7 @@ type Options struct { prefixes []string inputPaths []string allowMissingFilenames bool + recursive bool outputPath string fSys filesys.FileSystem reader io.Reader @@ -139,6 +146,7 @@ func (f *Flags) AddFlags(flags *pflag.FlagSet) { flags.StringSliceVarP(&f.prefixes, prefixesFlagName, prefixesFlagShort, nil, prefixesFlagUsage) flags.StringSliceVarP(&f.inputPaths, inputFlagName, inputFlagShort, nil, inputFlagUsage) flags.BoolVar(&f.allowMissingFilenames, allowMissingFilenamesFlagName, allowMissingFilenamesFlagDefault, allowMissingFilenamesFlagUsage) + flags.BoolVarP(&f.recursive, recursiveFlagName, recursiveFlagShort, recursiveFlagDefault, recursiveFlagUsage) flags.StringVarP(&f.outputPath, outputFlagName, outputFlagShort, "interpolated-files", outputFlagUsage) if err := cobra.MarkFlagDirname(flags, outputFlagName); err != nil { panic(err) @@ -151,6 +159,7 @@ func (f *Flags) ToOptions(reader io.Reader, fSys filesys.FileSystem) (*Options, inputPaths: f.inputPaths, prefixes: f.prefixes, allowMissingFilenames: f.allowMissingFilenames, + recursive: f.recursive, outputPath: f.outputPath, fSys: fSys, reader: reader, @@ -193,8 +202,20 @@ func (o *Options) Run(ctx context.Context) error { return err } + outputName := name + if o.recursive && path != stdinToken { + outputName = o.relativeOutputName(path) + } + + outputFile := filepath.Join(o.outputPath, outputName) + if dir := filepath.Dir(outputFile); dir != o.outputPath { + if err := o.fSys.MkdirAll(dir); err != nil { + return err + } + } + logger.V(10).Info("saving interpolated file", "path", path) - if err := o.fSys.WriteFile(filepath.Join(o.outputPath, name), interpolatedData); err != nil { + if err := o.fSys.WriteFile(outputFile, interpolatedData); err != nil { return err } } @@ -241,8 +262,11 @@ func (o *Options) filesToInterpolate(ctx context.Context) ([]string, error) { return err } if info.IsDir() && walkPath != path { - logger.V(10).Info("ignore folder inside a folder", "path", walkPath) - return fs.SkipDir + if !o.recursive { + logger.V(10).Info("ignore folder inside a folder", "path", walkPath) + return fs.SkipDir + } + return nil } addOnlyYAMLFiles(walkPath) @@ -267,6 +291,17 @@ func (o *Options) readFile(path string) ([]byte, string, error) { return data, filepath.Base(path), err } +func (o *Options) relativeOutputName(filePath string) string { + for _, inputPath := range o.inputPaths { + rel, err := filepath.Rel(inputPath, filePath) + if err == nil && !strings.HasPrefix(rel, "..") { + return rel + } + } + + return filepath.Base(filePath) +} + // Interpolate will interpolate the data content with values from env values func Interpolate(data []byte, envPrefixes []string) ([]byte, error) { for _, env := range envNamesToInterpolate(data) { diff --git a/pkg/cmd/interpolate/interpolate_test.go b/pkg/cmd/interpolate/interpolate_test.go index 7d0d25c..0970e5a 100644 --- a/pkg/cmd/interpolate/interpolate_test.go +++ b/pkg/cmd/interpolate/interpolate_test.go @@ -119,6 +119,17 @@ ZZZZZZZZZZZZZZZZZZZZZZZZZZ }, expectedResultsPath: filepath.Join(testdata, "results-contained"), }, + "interpolate recursive": { + option: &Options{ + prefixes: []string{"MLP_TEST_", "MLP_"}, + inputPaths: []string{filepath.Join(testdata, "folder")}, + recursive: true, + outputPath: filepath.Join(testTmpDir, "outputs-recursive"), + fSys: fSys, + reader: new(bytes.Buffer), + }, + expectedResultsPath: filepath.Join(testdata, "results-recursive"), + }, "interpolate from reader": { option: &Options{ prefixes: []string{"MLP_"}, diff --git a/pkg/cmd/interpolate/testdata/results-recursive/inner/inner.yaml b/pkg/cmd/interpolate/testdata/results-recursive/inner/inner.yaml new file mode 100644 index 0000000..52b647b --- /dev/null +++ b/pkg/cmd/interpolate/testdata/results-recursive/inner/inner.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: inner +data: + key: value diff --git a/pkg/cmd/interpolate/testdata/results-recursive/no-interpolation.yaml b/pkg/cmd/interpolate/testdata/results-recursive/no-interpolation.yaml new file mode 100644 index 0000000..72d9183 --- /dev/null +++ b/pkg/cmd/interpolate/testdata/results-recursive/no-interpolation.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: example +spec: + selector: + app: example + ports: + - port: 80 + targetPort: 8080 diff --git a/pkg/cmd/interpolate/testdata/results-recursive/other-file.yml b/pkg/cmd/interpolate/testdata/results-recursive/other-file.yml new file mode 100644 index 0000000..36c15f5 --- /dev/null +++ b/pkg/cmd/interpolate/testdata/results-recursive/other-file.yml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: example +data: + key: "{\"type\":\"type\",\"project_id\":\"id\",\"private_key_id\":\"key\",\"private_key\":\"-----BEGIN CERTIFICATE-----\nXXXXXXXXXXXXXXXXXXXXXXXXX\nYYYYYYYYYYYYYYY/4YYYYYYYYY\n-----END CERTIFICATE-----\n\",\"client_email\":\"email@example.com\",\"client_id\":\"client-id\",\"auth_uri\":\"https://example.com/auth\",\"token_uri\":\"https://example.com/token\",\"auth_provider_x509_cert_url\":\"https://example.com/certs\",\"client_x509_cert_url\":\"https://example.com/certs/fooo%40bar\"}" + key2: -----BEGIN CERTIFICATE-----\nXXXXXXXXXXXXXXXXXXXXXXXXX\nYYYYYYYYYYYYYYY/4YYYYYYYYY\nZZZZZZZZZZZZZZZZZZZZZZZZZZ\n-----END CERTIFICATE----- + key3: env\\first\line + key4: "$contains$dollars$otherstring" + key5: "otherstringtest" + key6: $contains$dollars$