diff --git a/cmd/flux/envsubst.go b/cmd/flux/envsubst.go new file mode 100644 index 0000000000..96ddefa792 --- /dev/null +++ b/cmd/flux/envsubst.go @@ -0,0 +1,74 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "fmt" + + "github.com/fluxcd/pkg/envsubst" + "github.com/spf13/cobra" +) + +var envsubstCmd = &cobra.Command{ + Use: "envsubst", + Args: cobra.NoArgs, + Short: "envsubst substitutes the values of environment variables", + Long: withPreviewNote(`The envsubst command substitutes the values of environment variables +in the string piped as standard input and writes the result to the standard output. This command can be used +to replicate the behavior of the Flux Kustomization post-build substitutions.`), + Example: ` # Run env var substitutions on the kustomization build output + export cluster_region=eu-central-1 + kustomize build . | flux envsubst + + # Run env var substitutions and error out if a variable is not set + kustomize build . | flux envsubst --strict +`, + RunE: runEnvsubstCmd, +} + +type envsubstFlags struct { + strict bool +} + +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") + 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 + } + } + return nil +} diff --git a/cmd/flux/envsubst_test.go b/cmd/flux/envsubst_test.go new file mode 100644 index 0000000000..38010b5f43 --- /dev/null +++ b/cmd/flux/envsubst_test.go @@ -0,0 +1,50 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "os" + "testing" + + . "github.com/onsi/gomega" +) + +func TestEnvsubst(t *testing.T) { + g := NewWithT(t) + input, err := os.ReadFile("testdata/envsubst/file.yaml") + g.Expect(err).NotTo(HaveOccurred()) + + t.Setenv("REPO_NAME", "test") + + output, err := executeCommandWithIn("envsubst", bytes.NewReader(input)) + g.Expect(err).NotTo(HaveOccurred()) + + expected, err := os.ReadFile("testdata/envsubst/file.gold") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(Equal(string(expected))) +} + +func TestEnvsubst_Strinct(t *testing.T) { + g := NewWithT(t) + input, err := os.ReadFile("testdata/envsubst/file.yaml") + g.Expect(err).NotTo(HaveOccurred()) + + _, err = executeCommandWithIn("envsubst --strict", bytes.NewReader(input)) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("variable not set (strict mode)")) +} diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go index d2f9622e8b..5f2afe6083 100644 --- a/cmd/flux/main_test.go +++ b/cmd/flux/main_test.go @@ -31,7 +31,6 @@ import ( "text/template" "time" - "github.com/fluxcd/flux2/v2/internal/utils" "github.com/google/go-cmp/cmp" "github.com/mattn/go-shellwords" "k8s.io/apimachinery/pkg/api/errors" @@ -40,6 +39,8 @@ import ( "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" + + "github.com/fluxcd/flux2/v2/internal/utils" ) var nextNamespaceId int64 @@ -393,6 +394,29 @@ func executeCommand(cmd string) (string, error) { return result, err } +// Run the command while passing the string as input and return the captured output. +func executeCommandWithIn(cmd string, in io.Reader) (string, error) { + defer resetCmdArgs() + args, err := shellwords.Parse(cmd) + if err != nil { + return "", err + } + + buf := new(bytes.Buffer) + + rootCmd.SetOut(buf) + rootCmd.SetErr(buf) + rootCmd.SetArgs(args) + if in != nil { + rootCmd.SetIn(in) + } + + _, err = rootCmd.ExecuteC() + result := buf.String() + + return result, err +} + // resetCmdArgs resets the flags for various cmd // Note: this will also clear default value of the flags set in init() func resetCmdArgs() { @@ -441,7 +465,7 @@ func resetCmdArgs() { versionArgs = versionFlags{ output: "yaml", } - + envsubstArgs = envsubstFlags{} } func isChangeError(err error) bool { diff --git a/cmd/flux/testdata/envsubst/file.gold b/cmd/flux/testdata/envsubst/file.gold new file mode 100644 index 0000000000..58ac924e1a --- /dev/null +++ b/cmd/flux/testdata/envsubst/file.gold @@ -0,0 +1,10 @@ +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: test + namespace: flux-system +spec: + ref: + branch: main + interval: 5m + url: ssh://git@github.com/example/test diff --git a/cmd/flux/testdata/envsubst/file.yaml b/cmd/flux/testdata/envsubst/file.yaml new file mode 100644 index 0000000000..e54aa24f0e --- /dev/null +++ b/cmd/flux/testdata/envsubst/file.yaml @@ -0,0 +1,10 @@ +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: ${REPO_NAME} + namespace: ${REPO_NAMESPACE:=flux-system} +spec: + ref: + branch: main + interval: 5m + url: ssh://git@github.com/example/${REPO_NAME} diff --git a/go.mod b/go.mod index b23f0a525b..6d902da2e1 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/fluxcd/notification-controller/api v1.2.4 github.com/fluxcd/pkg/apis/event v0.8.0 github.com/fluxcd/pkg/apis/meta v1.4.0 + github.com/fluxcd/pkg/envsubst v1.0.0 github.com/fluxcd/pkg/git v0.18.0 github.com/fluxcd/pkg/git/gogit v0.18.0 github.com/fluxcd/pkg/kustomize v1.9.0 @@ -117,7 +118,6 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fluxcd/pkg/apis/acl v0.2.0 // indirect github.com/fluxcd/pkg/apis/kustomize v1.4.0 // indirect - github.com/fluxcd/pkg/envsubst v1.0.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-fed/httpsig v1.1.0 // indirect