From 61ce5edde50cf54f4bd89b9b0f39b3c19f171c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaakko=20Sir=C3=A9n?= Date: Wed, 15 Apr 2026 15:57:37 +0300 Subject: [PATCH] feat: add --k8s-aware support to envsubst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This routes the envsubst changes to go through to a new `kustomize.SubstituteEnvVariables` to the pkgs `kustomize` module which supports the `kustomize.toolkit.fluxcd.io/substitute` annotation. Requires https://github.com/fluxcd/pkg/pull/1176 Fixes https://github.com/fluxcd/flux2/issues/5108 Signed-off-by: Jaakko Sirén --- cmd/flux/envsubst.go | 50 +++++++----- cmd/flux/envsubst_test.go | 77 +++++++++++++++++++ .../testdata/envsubst/k8s-aware-bash.gold | 29 +++++++ .../testdata/envsubst/k8s-aware-bash.yaml | 29 +++++++ .../testdata/envsubst/k8s-aware-label.gold | 9 +++ .../testdata/envsubst/k8s-aware-label.yaml | 9 +++ cmd/flux/testdata/envsubst/k8s-aware.gold | 25 ++++++ cmd/flux/testdata/envsubst/k8s-aware.yaml | 25 ++++++ 8 files changed, 235 insertions(+), 18 deletions(-) create mode 100644 cmd/flux/testdata/envsubst/k8s-aware-bash.gold create mode 100644 cmd/flux/testdata/envsubst/k8s-aware-bash.yaml create mode 100644 cmd/flux/testdata/envsubst/k8s-aware-label.gold create mode 100644 cmd/flux/testdata/envsubst/k8s-aware-label.yaml create mode 100644 cmd/flux/testdata/envsubst/k8s-aware.gold create mode 100644 cmd/flux/testdata/envsubst/k8s-aware.yaml diff --git a/cmd/flux/envsubst.go b/cmd/flux/envsubst.go index 96ddefa792..53c9bb2575 100644 --- a/cmd/flux/envsubst.go +++ b/cmd/flux/envsubst.go @@ -17,10 +17,12 @@ limitations under the License. package main import ( - "bufio" "fmt" + "io" + "os" "github.com/fluxcd/pkg/envsubst" + "github.com/fluxcd/pkg/kustomize" "github.com/spf13/cobra" ) @@ -37,12 +39,16 @@ to replicate the behavior of the Flux Kustomization post-build substitutions.`), # Run env var substitutions and error out if a variable is not set kustomize build . | flux envsubst --strict + + # Run env var substitutions, skipping resources with substitute disabled + kustomize build . | flux envsubst --strict --k8s-aware `, RunE: runEnvsubstCmd, } type envsubstFlags struct { - strict bool + strict bool + k8sAware bool } var envsubstArgs envsubstFlags @@ -50,25 +56,33 @@ var envsubstArgs envsubstFlags func init() { envsubstCmd.Flags().BoolVar(&envsubstArgs.strict, "strict", false, "fail if a variable without a default value is declared in the input but is missing from the environment") + envsubstCmd.Flags().BoolVar(&envsubstArgs.k8sAware, "k8s-aware", false, + "treat the input as multi-doc Kubernetes YAML and skip substitution for resources "+ + "annotated or labeled with kustomize.toolkit.fluxcd.io/substitute: disabled") rootCmd.AddCommand(envsubstCmd) } func runEnvsubstCmd(cmd *cobra.Command, args []string) error { - stdin := bufio.NewScanner(rootCmd.InOrStdin()) - stdout := bufio.NewWriter(rootCmd.OutOrStdout()) - for stdin.Scan() { - line, err := envsubst.EvalEnv(stdin.Text(), envsubstArgs.strict) - if err != nil { - return err - } - _, err = fmt.Fprintln(stdout, line) - if err != nil { - return err - } - err = stdout.Flush() - if err != nil { - return err - } + data, err := io.ReadAll(rootCmd.InOrStdin()) + if err != nil { + return err + } + + mapping := envsubst.Getenv + if envsubstArgs.strict { + mapping = os.LookupEnv } - return nil + + var result string + if envsubstArgs.k8sAware { + result, err = kustomize.SubstituteEnvVariables(string(data), mapping) + } else { + result, err = envsubst.Eval(string(data), mapping) + } + if err != nil { + return err + } + + _, err = fmt.Fprint(rootCmd.OutOrStdout(), result) + return err } diff --git a/cmd/flux/envsubst_test.go b/cmd/flux/envsubst_test.go index 38010b5f43..81bca26c87 100644 --- a/cmd/flux/envsubst_test.go +++ b/cmd/flux/envsubst_test.go @@ -48,3 +48,80 @@ func TestEnvsubst_Strinct(t *testing.T) { g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("variable not set (strict mode)")) } + +func TestEnvsubst_K8sAware(t *testing.T) { + tests := []struct { + name string + args string + env map[string]string + input string + gold string + wantErr string + }{ + { + name: "annotation disabled", + args: "envsubst --k8s-aware", + env: map[string]string{"REPO_NAME": "test"}, + input: "testdata/envsubst/k8s-aware.yaml", + gold: "testdata/envsubst/k8s-aware.gold", + }, + { + name: "label disabled", + args: "envsubst --k8s-aware", + input: "testdata/envsubst/k8s-aware-label.yaml", + gold: "testdata/envsubst/k8s-aware-label.gold", + }, + { + name: "strict skips disabled resources", + args: "envsubst --strict --k8s-aware", + env: map[string]string{"REPO_NAME": "test"}, + input: "testdata/envsubst/k8s-aware.yaml", + gold: "testdata/envsubst/k8s-aware.gold", + }, + { + name: "strict errors on enabled resource with missing var", + args: "envsubst --strict --k8s-aware", + input: "testdata/envsubst/k8s-aware.yaml", + wantErr: "variable not set (strict mode)", + }, + { + name: "bash script in disabled resource", + args: "envsubst --k8s-aware", + env: map[string]string{"APP_NAME": "myapp"}, + input: "testdata/envsubst/k8s-aware-bash.yaml", + gold: "testdata/envsubst/k8s-aware-bash.gold", + }, + { + name: "strict with bash script in disabled resource", + args: "envsubst --strict --k8s-aware", + env: map[string]string{"APP_NAME": "myapp"}, + input: "testdata/envsubst/k8s-aware-bash.yaml", + gold: "testdata/envsubst/k8s-aware-bash.gold", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + for k, v := range tt.env { + t.Setenv(k, v) + } + + input, err := os.ReadFile(tt.input) + g.Expect(err).NotTo(HaveOccurred()) + + output, err := executeCommandWithIn(tt.args, bytes.NewReader(input)) + if tt.wantErr != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) + return + } + g.Expect(err).NotTo(HaveOccurred()) + + expected, err := os.ReadFile(tt.gold) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(Equal(string(expected))) + }) + } +} diff --git a/cmd/flux/testdata/envsubst/k8s-aware-bash.gold b/cmd/flux/testdata/envsubst/k8s-aware-bash.gold new file mode 100644 index 0000000000..bc0bdbd60a --- /dev/null +++ b/cmd/flux/testdata/envsubst/k8s-aware-bash.gold @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: myapp + namespace: default +data: + key: value +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: init-scripts + namespace: default + annotations: + kustomize.toolkit.fluxcd.io/substitute: disabled +data: + setup.sh: | + #!/bin/bash + process_args() { + echo "First arg: $1" + echo "Second arg: $2" + echo "All args: $@" + local name=${1:-default} + local count=${2:-0} + for i in $(seq 1 $count); do + echo "$i: processing $name" + done + } + process_args "$@" diff --git a/cmd/flux/testdata/envsubst/k8s-aware-bash.yaml b/cmd/flux/testdata/envsubst/k8s-aware-bash.yaml new file mode 100644 index 0000000000..bac967cd15 --- /dev/null +++ b/cmd/flux/testdata/envsubst/k8s-aware-bash.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: ${APP_NAME} + namespace: ${APP_NAMESPACE:=default} +data: + key: value +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: init-scripts + namespace: default + annotations: + kustomize.toolkit.fluxcd.io/substitute: disabled +data: + setup.sh: | + #!/bin/bash + process_args() { + echo "First arg: $1" + echo "Second arg: $2" + echo "All args: $@" + local name=${1:-default} + local count=${2:-0} + for i in $(seq 1 $count); do + echo "$i: processing $name" + done + } + process_args "$@" diff --git a/cmd/flux/testdata/envsubst/k8s-aware-label.gold b/cmd/flux/testdata/envsubst/k8s-aware-label.gold new file mode 100644 index 0000000000..90bc31a3b0 --- /dev/null +++ b/cmd/flux/testdata/envsubst/k8s-aware-label.gold @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard + namespace: monitoring + labels: + kustomize.toolkit.fluxcd.io/substitute: disabled +data: + dashboard.json: '{"panels": [{"datasource": "${DataSource}"}]}' diff --git a/cmd/flux/testdata/envsubst/k8s-aware-label.yaml b/cmd/flux/testdata/envsubst/k8s-aware-label.yaml new file mode 100644 index 0000000000..90bc31a3b0 --- /dev/null +++ b/cmd/flux/testdata/envsubst/k8s-aware-label.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard + namespace: monitoring + labels: + kustomize.toolkit.fluxcd.io/substitute: disabled +data: + dashboard.json: '{"panels": [{"datasource": "${DataSource}"}]}' diff --git a/cmd/flux/testdata/envsubst/k8s-aware.gold b/cmd/flux/testdata/envsubst/k8s-aware.gold new file mode 100644 index 0000000000..e5a46aff3b --- /dev/null +++ b/cmd/flux/testdata/envsubst/k8s-aware.gold @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: test + namespace: flux-system +data: + key: value +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard + namespace: monitoring + annotations: + kustomize.toolkit.fluxcd.io/substitute: disabled +data: + dashboard.json: '{"panels": [{"datasource": "${DataSource}"}]}' +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config + namespace: flux-system +data: + region: eu-central-1 diff --git a/cmd/flux/testdata/envsubst/k8s-aware.yaml b/cmd/flux/testdata/envsubst/k8s-aware.yaml new file mode 100644 index 0000000000..f808133acb --- /dev/null +++ b/cmd/flux/testdata/envsubst/k8s-aware.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: ${REPO_NAME} + namespace: ${REPO_NAMESPACE:=flux-system} +data: + key: value +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard + namespace: monitoring + annotations: + kustomize.toolkit.fluxcd.io/substitute: disabled +data: + dashboard.json: '{"panels": [{"datasource": "${DataSource}"}]}' +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: ${REPO_NAME}-config + namespace: ${REPO_NAMESPACE:=flux-system} +data: + region: ${CLUSTER_REGION:=eu-central-1}