This commit is contained in:
Jaakko Sirén 2026-05-22 07:12:14 +08:00 committed by GitHub
commit 7b9448b62d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 235 additions and 18 deletions

View file

@ -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
}
return nil
mapping := envsubst.Getenv
if envsubstArgs.strict {
mapping = os.LookupEnv
}
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
}

View file

@ -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)))
})
}
}

View file

@ -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 "$@"

View file

@ -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 "$@"

View file

@ -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}"}]}'

View file

@ -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}"}]}'

View file

@ -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

View file

@ -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}