From b86b195450f93f570644b244e52b65852892625a Mon Sep 17 00:00:00 2001 From: Soule BA Date: Thu, 3 Feb 2022 10:23:19 +0100 Subject: [PATCH] Add contextual error code for flux diff kustomization If implemented, calling the diff command on kustomization will return 0, 1(if changes are identified), >1 for errors. Signed-off-by: Soule BA --- cmd/flux/diff.go | 2 +- cmd/flux/diff_kustomization.go | 22 ++++++++++++++-------- cmd/flux/main.go | 15 +++++++++++++++ cmd/flux/main_test.go | 15 +++++++++++++++ internal/build/diff.go | 22 +++++++++++++--------- 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/cmd/flux/diff.go b/cmd/flux/diff.go index d4ee4804..55ebf4bb 100644 --- a/cmd/flux/diff.go +++ b/cmd/flux/diff.go @@ -23,7 +23,7 @@ import ( var diffCmd = &cobra.Command{ Use: "diff", Short: "Diff a flux resource", - Long: "The diff command is used to do a server-side dry-run on flux resources, then output the diff.", + Long: "The diff command is used to do a server-side dry-run on flux resources, then prints the diff.", } func init() { diff --git a/cmd/flux/diff_kustomization.go b/cmd/flux/diff_kustomization.go index e40f72c9..e23c6643 100644 --- a/cmd/flux/diff_kustomization.go +++ b/cmd/flux/diff_kustomization.go @@ -31,8 +31,9 @@ var diffKsCmd = &cobra.Command{ Use: "kustomization", Aliases: []string{"ks"}, Short: "Diff Kustomization", - Long: `The diff command does a build, then it performs a server-side dry-run and output the diff.`, - Example: `# Preview changes local changes as they were applied on the cluster + 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`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: diffKsCmdRun, @@ -56,16 +57,16 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if diffKsArgs.path == "" { - return fmt.Errorf("invalid resource path %q", diffKsArgs.path) + return &RequestError{StatusCode: 2, Err: fmt.Errorf("invalid resource path %q", diffKsArgs.path)} } if fs, err := os.Stat(diffKsArgs.path); err != nil || !fs.IsDir() { - return fmt.Errorf("invalid resource path %q", diffKsArgs.path) + return &RequestError{StatusCode: 2, Err: fmt.Errorf("invalid resource path %q", diffKsArgs.path)} } builder, err := build.NewBuilder(kubeconfigArgs, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout)) if err != nil { - return err + return &RequestError{StatusCode: 2, Err: err} } // create a signal channel @@ -74,13 +75,18 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { errChan := make(chan error) go func() { - output, err := builder.Diff() + output, hasChanged, err := builder.Diff() if err != nil { - errChan <- err + errChan <- &RequestError{StatusCode: 2, Err: err} } cmd.Print(output) - errChan <- nil + + if hasChanged { + errChan <- &RequestError{StatusCode: 1, Err: fmt.Errorf("identified at least one change, exiting with non-zero exit code")} + } else { + errChan <- nil + } }() select { diff --git a/cmd/flux/main.go b/cmd/flux/main.go index d61bcda9..5fb1a163 100644 --- a/cmd/flux/main.go +++ b/cmd/flux/main.go @@ -105,6 +105,16 @@ type rootFlags struct { defaults install.Options } +// RequestError is a custom error type that wraps an error returned by the flux api. +type RequestError struct { + StatusCode int + Err error +} + +func (r *RequestError) Error() string { + return r.Err.Error() +} + var rootArgs = NewRootFlags() var kubeconfigArgs = genericclioptions.NewConfigFlags(false) @@ -144,6 +154,11 @@ func main() { log.SetFlags(0) if err := rootCmd.Execute(); err != nil { logger.Failuref("%v", err) + + if err, ok := err.(*RequestError); ok { + os.Exit(err.StatusCode) + } + os.Exit(1) } } diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go index 26b4e99e..c41eb3e6 100644 --- a/cmd/flux/main_test.go +++ b/cmd/flux/main_test.go @@ -325,6 +325,12 @@ type cmdTestCase struct { func (cmd *cmdTestCase) runTestCmd(t *testing.T) { actual, testErr := executeCommand(cmd.args) + + // If the cmd error is a change, discard it + if isChangeError(testErr) { + testErr = nil + } + if assertErr := cmd.assert(actual, testErr); assertErr != nil { t.Error(assertErr) } @@ -366,3 +372,12 @@ func resetCmdArgs() { getArgs = GetFlags{} secretGitArgs = NewSecretGitFlags() } + +func isChangeError(err error) bool { + if reqErr, ok := err.(*RequestError); ok { + if strings.Contains(err.Error(), "identified at least one change, exiting with non-zero exit code") && reqErr.StatusCode == 1 { + return true + } + } + return false +} diff --git a/internal/build/diff.go b/internal/build/diff.go index 43b85aae..13a8270a 100644 --- a/internal/build/diff.go +++ b/internal/build/diff.go @@ -51,28 +51,29 @@ func (b *Builder) Manager() (*ssa.ResourceManager, error) { return ssa.NewResourceManager(b.client, statusPoller, owner), nil } -func (b *Builder) Diff() (string, error) { +func (b *Builder) Diff() (string, bool, error) { output := strings.Builder{} + createdOrDrifted := false res, err := b.Build() if err != nil { - return "", err + return "", createdOrDrifted, err } // convert the build result into Kubernetes unstructured objects objects, err := ssa.ReadObjects(bytes.NewReader(res)) if err != nil { - return "", err + return "", createdOrDrifted, err } resourceManager, err := b.Manager() if err != nil { - return "", err + return "", createdOrDrifted, err } ctx, cancel := context.WithTimeout(context.Background(), b.timeout) defer cancel() if err := ssa.SetNativeKindsDefaults(objects); err != nil { - return "", err + return "", createdOrDrifted, err } // create an inventory of objects to be reconciled @@ -101,20 +102,23 @@ func (b *Builder) Diff() (string, error) { if change.Action == string(ssa.CreatedAction) { output.WriteString(writeString(fmt.Sprintf("► %s created\n", change.Subject), bunt.Green)) + createdOrDrifted = true } if change.Action == string(ssa.ConfiguredAction) { output.WriteString(writeString(fmt.Sprintf("► %s drifted\n", change.Subject), bunt.WhiteSmoke)) liveFile, mergedFile, tmpDir, err := writeYamls(liveObject, mergedObject) if err != nil { - return "", err + return "", createdOrDrifted, err } defer cleanupDir(tmpDir) err = diff(liveFile, mergedFile, &output) if err != nil { - return "", err + return "", createdOrDrifted, err } + + createdOrDrifted = true } addObjectsToInventory(newInventory, change) @@ -125,7 +129,7 @@ func (b *Builder) Diff() (string, error) { if oldStatus.Inventory != nil { diffObjects, err := diffInventory(oldStatus.Inventory, newInventory) if err != nil { - return "", err + return "", createdOrDrifted, err } for _, object := range diffObjects { output.WriteString(writeString(fmt.Sprintf("► %s deleted\n", ssa.FmtUnstructured(object)), bunt.OrangeRed)) @@ -133,7 +137,7 @@ func (b *Builder) Diff() (string, error) { } } - return output.String(), nil + return output.String(), createdOrDrifted, nil } func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) {