diff --git a/kustomize/commands/localize/localize.go b/kustomize/commands/localize/localize.go index d557ebceb0..5aaa9bb41d 100644 --- a/kustomize/commands/localize/localize.go +++ b/kustomize/commands/localize/localize.go @@ -4,10 +4,14 @@ package localize import ( + "bytes" "log" + "path/filepath" "github.com/spf13/cobra" lclzr "sigs.k8s.io/kustomize/api/krusty/localizer" + "sigs.k8s.io/kustomize/kustomize/v5/commands/build" + "sigs.k8s.io/kustomize/kyaml/copyutil" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/filesys" ) @@ -20,23 +24,26 @@ type arguments struct { } type flags struct { - scope string + scope string + noVerify bool } // NewCmdLocalize returns a new localize command. func NewCmdLocalize(fs filesys.FileSystem) *cobra.Command { var f flags + var buildBuffer bytes.Buffer + buildCmd := build.NewCmdBuild(fs, &build.Help{}, &buildBuffer) cmd := &cobra.Command{ Use: "localize [target [destination]]", Short: "[Alpha] Creates localized copy of target kustomization root at destination", - Long: `[Alpha] Creates copy of target kustomization directory or -versioned URL at destination, where remote references in the original + Long: `[Alpha] Creates copy of target kustomization directory or +versioned URL at destination, where remote references in the original are replaced by local references to the downloaded remote content. -If target is not specified, the current working directory will be used. -Destination is a path to a new directory in an existing directory. If -destination is not specified, a new directory will be created in the current -working directory. +If target is not specified, the current working directory will be used. +Destination is a path to a new directory in an existing directory. If +destination is not specified, a new directory will be created in the current +working directory. For details, see: https://kubectl.docs.kubernetes.io/references/kustomize/cmd/ @@ -46,7 +53,7 @@ alphabetizes kustomization fields in the localized copy. `, Example: ` # Localize the current working directory, with default scope and destination -kustomize localize +kustomize localize # Localize some local directory, with scope and default destination kustomize localize /home/path/scope/target --scope /home/path/scope @@ -62,6 +69,30 @@ kustomize localize https://github.com/kubernetes-sigs/kustomize//api/krusty/test if err != nil { return errors.Wrap(err) } + + if !f.noVerify { + originalBuild, err := runBuildCmd(buildBuffer, buildCmd, args.target) + if err != nil { + return errors.Wrap(err) + } + + buildDst := dst + if f.scope != "" && f.scope != args.target { + buildDst = filepath.Join(dst, filepath.Base(args.target)) + } + + localizedBuild, err := runBuildCmd(buildBuffer, buildCmd, buildDst) + if err != nil { + return errors.Wrap(err) + } + + if localizedBuild != originalBuild { + copyutil.PrettyFileDiff(originalBuild, localizedBuild) + log.Fatalf("VERIFICATION FAILED: `kustomize build` for %s and %s are different after localization.\n", args.target, dst) + } + log.Printf("VERIFICATION SUCCESS: `kustomize build` for %s and %s are the same after localization.\n", args.target, dst) + } + log.Printf("SUCCESS: localized %q to directory %s\n", args.target, dst) return nil }, @@ -74,6 +105,12 @@ kustomize localize https://github.com/kubernetes-sigs/kustomize//api/krusty/test Cannot specify for remote targets, as scope is by default the containing repo. If not specified for local target, scope defaults to target. `) + cmd.Flags().BoolVar(&f.noVerify, + "no-verify", + false, + `Does not verify that the outputs of kustomize build for target and newDir are the same after localization. + If not specified, this flag defaults to false and will run kustomize build. + `) return cmd } @@ -92,3 +129,16 @@ func matchArgs(rawArgs []string) arguments { } return args } + +func runBuildCmd(buffer bytes.Buffer, cmd *cobra.Command, folder string) (buildOutput string, err error) { + buffer.Reset() + buildErr := cmd.RunE(cmd, []string{folder}) + if buildErr != nil { + log.Printf("If your target directory requires flags to build: \n"+ + "1. Add executable permissions for the downloaded exec binaries in '%s'. \n"+ + "2. Run kustomize build with the necessary flags and self-verify the outputs.", folder) + return "", errors.Wrap(buildErr) + } + + return buffer.String(), nil +} diff --git a/kustomize/commands/localize/localize_test.go b/kustomize/commands/localize/localize_test.go index 8052d2b86e..6daa8db495 100644 --- a/kustomize/commands/localize/localize_test.go +++ b/kustomize/commands/localize/localize_test.go @@ -39,6 +39,24 @@ spec: - containerPort: 80 ` +const helmKustomization = `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +helmCharts: +- name: external-dns + repo: oci://registry-1.docker.io/bitnamicharts + version: 6.19.2 + releaseName: test + valuesInline: + crd: + create: false + rbac: + create: false + serviceAccount: + create: false + service: + enabled: false +` + func TestScopeFlag(t *testing.T) { kustomizations := map[string]string{ filepath.Join("target", "kustomization.yaml"): fmt.Sprintf(`resources: @@ -66,6 +84,60 @@ func TestScopeFlag(t *testing.T) { loctest.CheckFs(t, testDir.String(), expected, actual) } +func TestNoVerifyFlag(t *testing.T) { + kustomization := map[string]string{ + "kustomization.yaml": `resources: +- deployment.yaml +`, + "deployment.yaml": deployment, + } + expected, actual, target := loctest.PrepareFs(t, nil, kustomization) + + buffy := new(bytes.Buffer) + log.SetOutput(buffy) + defer func() { + log.SetOutput(os.Stderr) + }() + cmd := localize.NewCmdLocalize(actual) + require.NoError(t, cmd.Flags().Set("no-verify", "true")) + err := cmd.RunE(cmd, []string{ + target.String(), + target.Join("dst"), + }) + require.NoError(t, err) + + loctest.SetupDir(t, expected, target.Join("dst"), kustomization) + loctest.CheckFs(t, target.String(), expected, actual) + + successMsg := fmt.Sprintf(`SUCCESS: localized "%s" to directory %s +`, target.String(), target.Join("dst")) + verifyMsg := "VERIFICATION" + require.NotContains(t, buffy.String(), verifyMsg) + require.Contains(t, buffy.String(), successMsg) +} + +func TestFailingBuildCmd(t *testing.T) { + kustomization := map[string]string{ + "kustomization.yaml": helmKustomization, + } + _, actual, target := loctest.PrepareFs(t, nil, kustomization) + + buffy := new(bytes.Buffer) + log.SetOutput(buffy) + defer func() { + log.SetOutput(os.Stderr) + }() + cmd := localize.NewCmdLocalize(actual) + err := cmd.RunE(cmd, []string{ + target.String(), + target.Join("dst"), + }) + require.Error(t, err) + + verifyMsg := "If your target directory requires flags to build" + require.Contains(t, buffy.String(), verifyMsg) +} + func TestOptionalArgs(t *testing.T) { for name, args := range map[string][]string{ "no_target": {}, @@ -99,6 +171,8 @@ func TestOptionalArgs(t *testing.T) { loctest.SetupDir(t, expected, dst, kust) loctest.CheckFs(t, testDir.String(), expected, actual) + verifyMsg := "VERIFICATION SUCCESS" + require.Contains(t, buffy.String(), verifyMsg) successMsg := fmt.Sprintf(`SUCCESS: localized "." to directory %s `, dst) require.Contains(t, buffy.String(), successMsg) @@ -128,6 +202,8 @@ func TestOutput(t *testing.T) { loctest.SetupDir(t, expected, target.Join("dst"), kustomization) loctest.CheckFs(t, target.String(), expected, actual) + verifyMsg := "VERIFICATION SUCCESS" + require.Contains(t, buffy.String(), verifyMsg) successMsg := fmt.Sprintf(`SUCCESS: localized "%s" to directory %s `, target.String(), target.Join("dst")) require.Contains(t, buffy.String(), successMsg)