From 5536af9756b6a55139855b385b557f3f92ffca14 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Mon, 4 Apr 2022 14:11:10 +0200 Subject: [PATCH] Add an option to diff with a local kustomization file If implemented, users will be able to provide a local kustomization file to `flux build/diff`. Signed-off-by: Soule BA --- cmd/flux/build_kustomization.go | 23 ++++-- cmd/flux/build_kustomization_test.go | 103 ++++++++++++++++++++++++++- cmd/flux/diff_kustomization.go | 23 ++++-- internal/build/build.go | 50 ++++++++++--- 4 files changed, 179 insertions(+), 20 deletions(-) diff --git a/cmd/flux/build_kustomization.go b/cmd/flux/build_kustomization.go index 41280976..e093ac96 100644 --- a/cmd/flux/build_kustomization.go +++ b/cmd/flux/build_kustomization.go @@ -33,21 +33,28 @@ var buildKsCmd = &cobra.Command{ Short: "Build Kustomization", Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization. It then uses the fetched in cluster flux kustomization to perform needed transformation on the local kustomization.yaml -pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout.`, +pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout. + +It is possible to specify a Flux kustomization file using --kustomization-file.`, Example: `# Build the local manifests as they were built on the cluster -flux build kustomization my-app --path ./path/to/local/manifests`, +flux build kustomization my-app --path ./path/to/local/manifests + +# Build using a local flux kustomization file +flux build kustomization my-app --path ./path/to/local/manifests --kustomization-file ./path/to/local/my-app.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: buildKsCmdRun, } type buildKsFlags struct { - path string + kustomizationFile string + path string } var buildKsArgs buildKsFlags func init() { - buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.)") + buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.") + buildKsCmd.Flags().StringVar(&buildKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.") buildCmd.AddCommand(buildKsCmd) } @@ -65,7 +72,13 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid resource path %q", buildKsArgs.path) } - builder, err := build.NewBuilder(kubeconfigArgs, kubeclientOptions, name, buildKsArgs.path, build.WithTimeout(rootArgs.timeout)) + if buildKsArgs.kustomizationFile != "" { + if fs, err := os.Stat(buildKsArgs.kustomizationFile); os.IsNotExist(err) || fs.IsDir() { + return fmt.Errorf("invalid kustomization file %q", buildKsArgs.kustomizationFile) + } + } + + builder, err := build.NewBuilder(kubeconfigArgs, kubeclientOptions, name, buildKsArgs.path, build.WithTimeout(rootArgs.timeout), build.WithKustomizationFile(buildKsArgs.kustomizationFile)) if err != nil { return err } diff --git a/cmd/flux/build_kustomization_test.go b/cmd/flux/build_kustomization_test.go index 03826d04..aba1b16c 100644 --- a/cmd/flux/build_kustomization_test.go +++ b/cmd/flux/build_kustomization_test.go @@ -20,7 +20,10 @@ limitations under the License. package main import ( + "bytes" + "os" "testing" + "text/template" ) func setup(t *testing.T, tmpl map[string]string) { @@ -55,7 +58,7 @@ func TestBuildKustomization(t *testing.T) { assertFunc: "assertGoldenTemplateFile", }, { - name: "build deployment and configmpa with var substitution", + name: "build deployment and configmap with var substitution", args: "build kustomization podinfo --path ./testdata/build-kustomization/var-substitution", resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", assertFunc: "assertGoldenTemplateFile", @@ -87,3 +90,101 @@ func TestBuildKustomization(t *testing.T) { }) } } + +func TestBuildLocalKustomization(t *testing.T) { + podinfo := `apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +kind: Kustomization +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 5m0s + path: ./kustomize + force: true + prune: true + sourceRef: + kind: GitRepository + name: podinfo + targetNamespace: default + postBuild: + substitute: + cluster_env: "prod" + cluster_region: "eu-central-1" +` + + tests := []struct { + name string + args string + resultFile string + assertFunc string + }{ + { + name: "no args", + args: "build kustomization podinfo --kustomization-file ./wrongfile/ --path ./testdata/build-kustomization/podinfo", + resultFile: "invalid kustomization file \"./wrongfile/\"", + assertFunc: "assertError", + }, + { + name: "build podinfo", + args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/podinfo", + resultFile: "./testdata/build-kustomization/podinfo-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build podinfo without service", + args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/delete-service", + resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build deployment and configmap with var substitution", + args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/var-substitution", + resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + } + + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + + testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-source.yaml", tmpl, t) + + temp, err := template.New("podinfo").Parse(podinfo) + if err != nil { + t.Fatal(err) + } + + var b bytes.Buffer + err = temp.Execute(&b, tmpl) + if err != nil { + t.Fatal(err) + } + + err = os.WriteFile("./testdata/build-kustomization/podinfo.yaml", b.Bytes(), 0666) + if err != nil { + t.Fatal(err) + } + + defer os.Remove("./testdata/build-kustomization/podinfo.yaml") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var assert assertFunc + + switch tt.assertFunc { + case "assertGoldenTemplateFile": + assert = assertGoldenTemplateFile(tt.resultFile, tmpl) + case "assertError": + assert = assertError(tt.resultFile) + } + + cmd := cmdTestCase{ + args: tt.args + " -n " + tmpl["fluxns"], + assert: assert, + } + + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/diff_kustomization.go b/cmd/flux/diff_kustomization.go index 8be50fc1..baeda363 100644 --- a/cmd/flux/diff_kustomization.go +++ b/cmd/flux/diff_kustomization.go @@ -34,21 +34,26 @@ var diffKsCmd = &cobra.Command{ Long: `The diff command does a build, then it performs a server-side dry-run and prints the diff. Exit status: 0 No differences were found. 1 Differences were found. >1 diff failed with an error.`, Example: `# Preview local changes as they were applied on the cluster -flux diff kustomization my-app --path ./path/to/local/manifests`, +flux diff kustomization my-app --path ./path/to/local/manifests + +# Preview using a local flux kustomization file +flux diff kustomization my-app --path ./path/to/local/manifests --kustomization-file ./path/to/local/my-app.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: diffKsCmdRun, } type diffKsFlags struct { - path string - progressBar bool + kustomizationFile string + path string + progressBar bool } var diffKsArgs diffKsFlags func init() { - diffKsCmd.Flags().StringVar(&diffKsArgs.path, "path", "", "Path to a local directory that matches the specified Kustomization.spec.path.)") + diffKsCmd.Flags().StringVar(&diffKsArgs.path, "path", "", "Path to a local directory that matches the specified Kustomization.spec.path.") diffKsCmd.Flags().BoolVar(&diffKsArgs.progressBar, "progress-bar", true, "Boolean to set the progress bar. The default value is true.") + diffKsCmd.Flags().StringVar(&diffKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.") diffCmd.AddCommand(diffKsCmd) } @@ -66,12 +71,18 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { return &RequestError{StatusCode: 2, Err: fmt.Errorf("invalid resource path %q", diffKsArgs.path)} } + if diffKsArgs.kustomizationFile != "" { + if fs, err := os.Stat(diffKsArgs.kustomizationFile); os.IsNotExist(err) || fs.IsDir() { + return fmt.Errorf("invalid kustomization file %q", diffKsArgs.kustomizationFile) + } + } + var builder *build.Builder var err error if diffKsArgs.progressBar { - builder, err = build.NewBuilder(kubeconfigArgs, kubeclientOptions, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout), build.WithProgressBar()) + builder, err = build.NewBuilder(kubeconfigArgs, kubeclientOptions, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout), build.WithKustomizationFile(diffKsArgs.kustomizationFile), build.WithProgressBar()) } else { - builder, err = build.NewBuilder(kubeconfigArgs, kubeclientOptions, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout)) + builder, err = build.NewBuilder(kubeconfigArgs, kubeclientOptions, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout), build.WithKustomizationFile(diffKsArgs.kustomizationFile)) } if err != nil { diff --git a/internal/build/build.go b/internal/build/build.go index 3eb0844c..27fa06a7 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -22,6 +22,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "os" "sync" "time" @@ -30,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + k8syaml "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/genericclioptions" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/kustomize/api/resmap" @@ -60,11 +62,12 @@ var defaultTimeout = 80 * time.Second // It retrieves the kustomization object from the k8s cluster // and overlays the manifests with the resources specified in the resourcesPath type Builder struct { - client client.WithWatch - restMapper meta.RESTMapper - name string - namespace string - resourcesPath string + client client.WithWatch + restMapper meta.RESTMapper + name string + namespace string + resourcesPath string + kustomizationFile string // mu is used to synchronize access to the kustomization file mu sync.Mutex action kustomize.Action @@ -75,6 +78,13 @@ type Builder struct { type BuilderOptionFunc func(b *Builder) error +func WithKustomizationFile(file string) BuilderOptionFunc { + return func(b *Builder) error { + b.kustomizationFile = file + return nil + } +} + func WithTimeout(timeout time.Duration) BuilderOptionFunc { return func(b *Builder) error { b.timeout = timeout @@ -176,9 +186,18 @@ func (b *Builder) build() (m resmap.ResMap, err error) { defer cancel() // Get the kustomization object - k, err := b.getKustomization(ctx) - if err != nil { - return + k := &kustomizev1.Kustomization{} + if b.kustomizationFile != "" { + k, err = b.unMarshallKustomization() + if err != nil { + return + } + } else { + k, err = b.getKustomization(ctx) + if err != nil { + err = fmt.Errorf("failed to get kustomization object: %w", err) + return + } } // store the kustomization object @@ -225,6 +244,21 @@ func (b *Builder) build() (m resmap.ResMap, err error) { } +func (b *Builder) unMarshallKustomization() (*kustomizev1.Kustomization, error) { + data, err := os.ReadFile(b.kustomizationFile) + if err != nil { + return nil, fmt.Errorf("failed to read kustomization file %s: %w", b.kustomizationFile, err) + } + + k := &kustomizev1.Kustomization{} + decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(data), len(data)) + err = decoder.Decode(k) + if err != nil { + return nil, fmt.Errorf("failed to unmarshall kustomization file %s: %w", b.kustomizationFile, err) + } + return k, nil +} + func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (kustomize.Action, error) { data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization) if err != nil {