From 4d3cb00b76f3aee8db523cee835746d36e5d7142 Mon Sep 17 00:00:00 2001 From: Markus Vuorio Date: Thu, 7 Oct 2021 20:55:17 +0300 Subject: [PATCH] Separate JSON output, emit non-zero exit code (#82) * Allow for printing FIO and CSICheck JSON results to file Fixes #79 * Use the file output function in Baseline too, remove double printing * Emit non-zero exit code when error has occurred Fixes #80 * Don't output twice in error case * Added godoc and a note in command help text * Eliminate weird variable names, make godoc clearer * Error handling for file output --- cmd/rootCmd.go | 94 ++++++++++++++++++++++++++++++-------------------- main.go | 5 ++- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/cmd/rootCmd.go b/cmd/rootCmd.go index 279d904..917bb3b 100644 --- a/cmd/rootCmd.go +++ b/cmd/rootCmd.go @@ -18,6 +18,7 @@ import ( "context" "encoding/json" "fmt" + "os" "time" "github.com/kastenhq/kubestr/pkg/csi" @@ -29,16 +30,18 @@ import ( var ( output string + outfile string rootCmd = &cobra.Command{ Use: "kubestr", Short: "A tool to validate kubernetes storage", Long: `kubestr is a tool that will scan your k8s cluster and validate that the storage systems in place as well as run performance tests.`, - Run: func(cmd *cobra.Command, args []string) { + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - Baseline(ctx, output) + return Baseline(ctx, output) }, } @@ -53,10 +56,10 @@ var ( Use: "fio", Short: "Runs an fio test", Long: `Run an fio test`, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - Fio(ctx, output, storageClass, fioCheckerSize, namespace, fioCheckerTestName, fioCheckerFilePath, containerImage) + return Fio(ctx, output, outfile, storageClass, fioCheckerSize, namespace, fioCheckerTestName, fioCheckerFilePath, containerImage) }, } @@ -68,21 +71,22 @@ var ( Use: "csicheck", Short: "Runs the CSI snapshot restore check", Long: "Validates a CSI provisioners ability to take a snapshot of an application and restore it", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - CSICheck(ctx, output, namespace, storageClass, csiCheckVolumeSnapshotClass, csiCheckRunAsUser, containerImage, csiCheckCleanup, csiCheckSkipCFSCheck) + return CSICheck(ctx, output, outfile, namespace, storageClass, csiCheckVolumeSnapshotClass, csiCheckRunAsUser, containerImage, csiCheckCleanup, csiCheckSkipCFSCheck) }, } ) func init() { rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "", "Options(json)") + rootCmd.PersistentFlags().StringVarP(&outfile, "outfile", "e", "", "The file where test results will be written") rootCmd.AddCommand(fioCmd) fioCmd.Flags().StringVarP(&storageClass, "storageclass", "s", "", "The name of a Storageclass. (Required)") _ = fioCmd.MarkFlagRequired("storageclass") - fioCmd.Flags().StringVarP(&fioCheckerSize, "size", "z", fio.DefaultPVCSize, "The size of the volume used to run FIO.") + fioCmd.Flags().StringVarP(&fioCheckerSize, "size", "z", fio.DefaultPVCSize, "The size of the volume used to run FIO. Note that the FIO job definition is not scaled accordingly.") fioCmd.Flags().StringVarP(&namespace, "namespace", "n", fio.DefaultNS, "The namespace used to run FIO.") fioCmd.Flags().StringVarP(&fioCheckerFilePath, "fiofile", "f", "", "The path to a an fio config file.") fioCmd.Flags().StringVarP(&fioCheckerTestName, "testname", "t", "", "The Name of a predefined kubestr fio test. Options(default-fio)") @@ -106,19 +110,19 @@ func Execute() error { } // Baseline executes the baseline check -func Baseline(ctx context.Context, output string) { +func Baseline(ctx context.Context, output string) error { p, err := kubestr.NewKubestr() if err != nil { fmt.Println(err.Error()) - return + return err } fmt.Print(kubestr.Logo) result := p.KubernetesChecks() - if output == "json" { - jsonRes, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(jsonRes)) - return + + if PrintAndJsonOutput(result, output, outfile) { + return err } + for _, retval := range result { retval.Print() fmt.Println() @@ -128,13 +132,9 @@ func Baseline(ctx context.Context, output string) { provisionerList, err := p.ValidateProvisioners(ctx) if err != nil { fmt.Println(err.Error()) - return - } - if output == "json" { - jsonRes, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(jsonRes)) - return + return err } + fmt.Println("Available Storage Provisioners:") fmt.Println() time.Sleep(500 * time.Millisecond) // Added to introduce lag. @@ -143,42 +143,61 @@ func Baseline(ctx context.Context, output string) { fmt.Println() time.Sleep(500 * time.Millisecond) } + return err +} + +// PrintAndJsonOutput Print JSON output to stdout and to file if arguments say so +// Returns whether we have generated output or JSON +func PrintAndJsonOutput(result []*kubestr.TestOutput, output string, outfile string) bool { + if output == "json" { + jsonRes, _ := json.MarshalIndent(result, "", " ") + if len(outfile) > 0 { + err := os.WriteFile(outfile, jsonRes, 0666) + if err != nil { + fmt.Println("Error writing output:", err.Error()) + os.Exit(2) + } + } else { + fmt.Println(string(jsonRes)) + } + return true + } + return false } // Fio executes the FIO test. -func Fio(ctx context.Context, output, storageclass, size, namespace, jobName, fioFilePath string, containerImage string) { +func Fio(ctx context.Context, output, outfile, storageclass, size, namespace, jobName, fioFilePath string, containerImage string) error { cli, err := kubestr.LoadKubeCli() if err != nil { fmt.Println(err.Error()) - return + return err } fioRunner := &fio.FIOrunner{ Cli: cli, } testName := "FIO test results" var result *kubestr.TestOutput - if fioResult, err := fioRunner.RunFio(ctx, &fio.RunFIOArgs{ + fioResult, err := fioRunner.RunFio(ctx, &fio.RunFIOArgs{ StorageClass: storageclass, Size: size, Namespace: namespace, FIOJobName: jobName, FIOJobFilepath: fioFilePath, Image: containerImage, - }); err != nil { + }) + if err != nil { result = kubestr.MakeTestOutput(testName, kubestr.StatusError, err.Error(), fioResult) } else { result = kubestr.MakeTestOutput(testName, kubestr.StatusOK, fmt.Sprintf("\n%s", fioResult.Result.Print()), fioResult) } - - if output == "json" { - jsonRes, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(jsonRes)) - return + var wrappedResult = []*kubestr.TestOutput{result} + if !PrintAndJsonOutput(wrappedResult, output, outfile) { + result.Print() } - result.Print() + return err } -func CSICheck(ctx context.Context, output, +func CSICheck(ctx context.Context, output, outfile, namespace string, storageclass string, volumesnapshotclass string, @@ -186,17 +205,17 @@ func CSICheck(ctx context.Context, output, containerImage string, cleanup bool, skipCFScheck bool, -) { +) error { testName := "CSI checker test" kubecli, err := kubestr.LoadKubeCli() if err != nil { fmt.Printf("Failed to load kubeCLi (%s)", err.Error()) - return + return err } dyncli, err := kubestr.LoadDynCli() if err != nil { fmt.Printf("Failed to load kubeCLi (%s)", err.Error()) - return + return err } csiCheckRunner := &csi.SnapshotRestoreRunner{ KubeCli: kubecli, @@ -218,10 +237,9 @@ func CSICheck(ctx context.Context, output, result = kubestr.MakeTestOutput(testName, kubestr.StatusOK, "CSI application successfully snapshotted and restored.", csiCheckResult) } - if output == "json" { - jsonRes, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(jsonRes)) - return + var wrappedResult = []*kubestr.TestOutput{result} + if !PrintAndJsonOutput(wrappedResult, output, outfile) { + result.Print() } - result.Print() + return err } diff --git a/main.go b/main.go index cf2a242..9ed937e 100644 --- a/main.go +++ b/main.go @@ -18,10 +18,13 @@ package main import ( "github.com/kastenhq/kubestr/cmd" + "os" ) func main() { - _ = Execute() + if err := Execute(); err != nil { + os.Exit(1) + } } // Execute executes the main command