Skip to content

Commit

Permalink
support multiple exit codes based on what went wrong/right (#1135)
Browse files Browse the repository at this point in the history
0 = all passed, 3 = at least one failure, 4 = no failures but at least 1 warn

1 as a catch all (generic errors), 2 for invalid input/specs etc

ref #1131

docs replicatedhq/troubleshoot.sh#489
  • Loading branch information
Nathan Sullivan committed May 9, 2023
1 parent 766469b commit 3548b46
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 28 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/build-test-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,17 @@ jobs:
path: bin/
- run: chmod +x bin/preflight
- run: |
set +e
./bin/preflight --interactive=false --format=json https://preflight.replicated.com > result.json
EXIT_CODE=$?
cat result.json
EXIT_STATUS=0
if [ $EXIT_CODE -ne 3 ]; then
echo "Expected exit code of 3 (some checks failed), got $EXIT_CODE"
EXIT_STATUS=1
fi
if grep -q "was not collected" result.json; then
echo "Some files were not collected"
EXIT_STATUS=1
Expand Down
26 changes: 24 additions & 2 deletions cmd/preflight/cli/root.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package cli

import (
"errors"
"fmt"
"os"
"strings"

"github.com/replicatedhq/troubleshoot/cmd/util"
"github.com/replicatedhq/troubleshoot/internal/traces"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
"github.com/replicatedhq/troubleshoot/pkg/logger"
"github.com/replicatedhq/troubleshoot/pkg/preflight"
"github.com/replicatedhq/troubleshoot/pkg/types"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"k8s.io/klog/v2"
Expand All @@ -22,7 +25,8 @@ func RootCmd() *cobra.Command {
Short: "Run and retrieve preflight checks in a cluster",
Long: `A preflight check is a set of validations that can and should be run to ensure
that a cluster meets the requirements to run an application.`,
SilenceUsage: true,
SilenceUsage: true,
SilenceErrors: true,
PreRun: func(cmd *cobra.Command, args []string) {
v := viper.GetViper()
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
Expand All @@ -48,6 +52,7 @@ that a cluster meets the requirements to run an application.`,
if v.GetBool("debug") || v.IsSet("v") {
fmt.Printf("\n%s", traces.GetExporterInstance().GetSummary())
}

return err
},
PostRun: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -75,7 +80,24 @@ that a cluster meets the requirements to run an application.`,
}

func InitAndExecute() {
if err := RootCmd().Execute(); err != nil {
cmd := RootCmd()
err := cmd.Execute()

if err != nil {
var exitErr types.ExitError
if errors.As(err, &exitErr) {
// We need to do this, there's situations where we need the non-zero exit code (which comes as part of the custom error struct)
// but there's no actual error, just an exit code.
// If there's also an error to output (eg. invalid format etc) then print it as well
if exitErr.ExitStatus() != constants.EXIT_CODE_FAIL && exitErr.ExitStatus() != constants.EXIT_CODE_WARN {
cmd.PrintErrln("Error:", err.Error())
}

os.Exit(exitErr.ExitStatus())
}

// Fallback, should almost never be used (the above Exit() should handle almost all situations
cmd.PrintErrln("Error:", err.Error())
os.Exit(1)
}
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,10 @@ const (
CLUSTER_RESOURCES_ROLE_BINDINGS = "rolebindings"
CLUSTER_RESOURCES_CLUSTER_ROLES = "clusterroles"
CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterRoleBindings"

// Custom exit codes
EXIT_CODE_CATCH_ALL = 1
EXIT_CODE_SPEC_ISSUES = 2
EXIT_CODE_FAIL = 3
EXIT_CODE_WARN = 4
)
87 changes: 63 additions & 24 deletions pkg/preflight/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import (
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/cmd/util"
analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze"
analyzerunner "github.com/replicatedhq/troubleshoot/pkg/analyze"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
troubleshootclientsetscheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/docrewrite"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
"github.com/replicatedhq/troubleshoot/pkg/oci"
"github.com/replicatedhq/troubleshoot/pkg/specs"
"github.com/replicatedhq/troubleshoot/pkg/types"
"github.com/spf13/viper"
spin "github.com/tj/go-spin"
"go.opentelemetry.io/otel"
Expand All @@ -47,7 +49,8 @@ func RunPreflights(interactive bool, output string, format string, args []string
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
<-signalChan
os.Exit(0)
// exiting due to a signal shouldn't be considered successful
os.Exit(1)
}()

var preflightContent []byte
Expand All @@ -61,64 +64,66 @@ func RunPreflights(interactive bool, output string, format string, args []string
// format secret/namespace-name/secret-name
pathParts := strings.Split(v, "/")
if len(pathParts) != 3 {
return errors.Errorf("path %s must have 3 components", v)
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("path %s must have 3 components", v))
}

spec, err := specs.LoadFromSecret(pathParts[1], pathParts[2], "preflight-spec")
if err != nil {
return errors.Wrap(err, "failed to get spec from secret")
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to get spec from secret"))
}

preflightContent = spec
} else if _, err = os.Stat(v); err == nil {
b, err := os.ReadFile(v)
if err != nil {
return err
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}

preflightContent = b
} else if v == "-" {
b, err := io.ReadAll(os.Stdin)
if err != nil {
return err
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
}
preflightContent = b
} else {
u, err := url.Parse(v)
if err != nil {
return err
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}

if u.Scheme == "oci" {
content, err := oci.PullPreflightFromOCI(v)
if err != nil {
if err == oci.ErrNoRelease {
return errors.Errorf("no release found for %s.\nCheck the oci:// uri for errors or contact the application vendor for support.", v)
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("no release found for %s.\nCheck the oci:// uri for errors or contact the application vendor for support.", v))
}

return err
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}

preflightContent = content
} else {
if !util.IsURL(v) {
return fmt.Errorf("%s is not a URL and was not found (err %s)", v, err)
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, fmt.Errorf("%s is not a URL and was not found (err %s)", v, err))
}

req, err := http.NewRequest("GET", v, nil)
if err != nil {
return err
// exit code: should this be catch all or spec issues...?
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
}
req.Header.Set("User-Agent", "Replicated_Preflight/v1beta2")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
// exit code: should this be catch all or spec issues...?
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return err
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}

preflightContent = body
Expand All @@ -137,7 +142,7 @@ func RunPreflights(interactive bool, output string, format string, args []string

err := yaml.Unmarshal([]byte(doc), &parsedDocHead)
if err != nil {
return errors.Wrap(err, "failed to parse yaml")
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to parse yaml"))
}

if parsedDocHead.Kind != "Preflight" {
Expand All @@ -146,14 +151,14 @@ func RunPreflights(interactive bool, output string, format string, args []string

preflightContent, err = docrewrite.ConvertToV1Beta2([]byte(doc))
if err != nil {
return errors.Wrap(err, "failed to convert to v1beta2")
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to convert to v1beta2"))
}

troubleshootclientsetscheme.AddToScheme(scheme.Scheme)
decode := scheme.Codecs.UniversalDeserializer().Decode
obj, _, err := decode([]byte(preflightContent), nil, nil)
if err != nil {
return errors.Wrapf(err, "failed to parse %s", v)
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrapf(err, "failed to parse %s", v))
}

if spec, ok := obj.(*troubleshootv1beta2.Preflight); ok {
Expand Down Expand Up @@ -192,7 +197,7 @@ func RunPreflights(interactive bool, output string, format string, args []string
if preflightSpec != nil {
r, err := collectInCluster(ctx, preflightSpec, progressCh)
if err != nil {
return errors.Wrap(err, "failed to collect in cluster")
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect in cluster"))
}
collectResults = append(collectResults, *r)
preflightSpecName = preflightSpec.Name
Expand All @@ -201,7 +206,7 @@ func RunPreflights(interactive bool, output string, format string, args []string
for _, spec := range uploadResultSpecs {
r, err := collectInCluster(ctx, spec, progressCh)
if err != nil {
return errors.Wrap(err, "failed to collect in cluster")
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect in cluster"))
}
uploadResultsMap[spec.Spec.UploadResultsTo] = append(uploadResultsMap[spec.Spec.UploadResultsTo], *r)
uploadCollectResults = append(collectResults, *r)
Expand All @@ -212,22 +217,22 @@ func RunPreflights(interactive bool, output string, format string, args []string
if len(hostPreflightSpec.Spec.Collectors) > 0 {
r, err := collectHost(ctx, hostPreflightSpec, progressCh)
if err != nil {
return errors.Wrap(err, "failed to collect from host")
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect from host"))
}
collectResults = append(collectResults, *r)
}
if len(hostPreflightSpec.Spec.RemoteCollectors) > 0 {
r, err := collectRemote(ctx, hostPreflightSpec, progressCh)
if err != nil {
return errors.Wrap(err, "failed to collect remotely")
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect remotely"))
}
collectResults = append(collectResults, *r)
}
preflightSpecName = hostPreflightSpec.Name
}

if collectResults == nil && uploadCollectResults == nil {
return errors.New("no results")
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.New("no results"))
}

analyzeResults := []*analyzer.AnalyzeResult{}
Expand Down Expand Up @@ -255,14 +260,48 @@ func RunPreflights(interactive bool, output string, format string, args []string
stopProgressCollection()
progressCollection.Wait()

if len(analyzeResults) == 0 {
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.New("no data has been collected"))
}

if interactive {
if len(analyzeResults) == 0 {
return errors.New("no data has been collected")
err = showInteractiveResults(preflightSpecName, output, analyzeResults)
} else {
err = showTextResults(format, preflightSpecName, output, analyzeResults)
}

if err != nil {
return err
}

exitCode := checkOutcomesToExitCode(analyzeResults)

if exitCode == 0 {
return nil
}

return types.NewExitCodeError(exitCode, errors.New("preflights failed with warnings or errors"))
}

// Determine if any preflight checks passed vs failed vs warned
// If all checks passed: 0
// If 1 or more checks failed: 3
// If no checks failed, but 1 or more warn: 4
func checkOutcomesToExitCode(analyzeResults []*analyzerunner.AnalyzeResult) int {
// Assume pass until they don't
exitCode := 0

for _, analyzeResult := range analyzeResults {
if analyzeResult.IsWarn {
exitCode = constants.EXIT_CODE_WARN
} else if analyzeResult.IsFail {
exitCode = constants.EXIT_CODE_FAIL
// No need to check further, a fail is a fail
return exitCode
}
return showInteractiveResults(preflightSpecName, output, analyzeResults)
}

return showTextResults(format, preflightSpecName, output, analyzeResults)
return exitCode
}

func collectInteractiveProgress(ctx context.Context, progressCh <-chan interface{}) func() error {
Expand Down

0 comments on commit 3548b46

Please sign in to comment.