From 2084edf693806f49ff7974c44d082a08a3c497f2 Mon Sep 17 00:00:00 2001 From: Dejan Zele Pejchev Date: Thu, 25 Jul 2024 13:32:47 +0200 Subject: [PATCH] add support for better error handling in CLI (#5680) --- .../commands/common/errors.go | 79 +++++ .../commands/common/helper.go | 296 +++++++++++------- cmd/kubectl-testkube/commands/init.go | 12 +- cmd/kubectl-testkube/commands/pro/connect.go | 12 +- .../commands/pro/disconnect.go | 15 +- cmd/kubectl-testkube/commands/pro/init.go | 28 +- cmd/kubectl-testkube/commands/upgrade.go | 12 +- pkg/process/exec.go | 3 +- 8 files changed, 313 insertions(+), 144 deletions(-) create mode 100644 cmd/kubectl-testkube/commands/common/errors.go diff --git a/cmd/kubectl-testkube/commands/common/errors.go b/cmd/kubectl-testkube/commands/common/errors.go new file mode 100644 index 00000000000..195b62932f5 --- /dev/null +++ b/cmd/kubectl-testkube/commands/common/errors.go @@ -0,0 +1,79 @@ +package common + +import ( + "fmt" + "os" + + "github.com/pterm/pterm" +) + +const ( + // TKERR-1xx errors are to issues when running testkube CLI commands. + + // TKERR-11xx errors are related to missing dependencies. + + // TKErrMissingDependencyHelm is returned when kubectl is not found in $PATH. + TKErrMissingDependencyHelm = "TKERR-1101" + // TKErrMissingDependencyKubectl is returned when kubectl is not found in $PATH. + TKErrMissingDependencyKubectl = "TKERR-1102" + + // TKERR-12xx errors are related to configuration issues. + + // TKErrConfigLoadingFailed is returned when configuration loading fails. + TKErrConfigLoadingFailed = "TKERR-1201" + // TKErrInvalidInstallConfig is returned when invalid configuration is supplied when installing or upgrading. + TKErrInvalidInstallConfig = "TKERR-1202" + + // TKERR-13xx errors are related to install operations. + + // TKErrHelmCommandFailed is returned when a helm command fails. + TKErrHelmCommandFailed = "TKERR-1301" + // TKErrKubectlCommandFailed is returned when a kubectl command fail. + TKErrKubectlCommandFailed = "TKERR-1302" +) + +type CLIError struct { + Code string + Title string + Description string + ActualError error + StackTrace string + MoreInfo string +} + +func (e *CLIError) Error() string { + return fmt.Sprintf("%s: %s", e.Code, e.Description) +} + +func (e *CLIError) Print() { + pterm.DefaultHeader.Println("Testkube Init Error") + + pterm.DefaultSection.Println("Error Details") + + items := []pterm.BulletListItem{ + {Level: 0, Text: pterm.Sprintf("[%s]: %s", e.Code, e.Title), TextStyle: pterm.NewStyle(pterm.FgRed)}, + {Level: 0, Text: pterm.Sprintf("%s", e.Description), TextStyle: pterm.NewStyle(pterm.FgLightWhite)}, + } + if e.MoreInfo != "" { + items = append(items, pterm.BulletListItem{Level: 0, Text: pterm.Sprintf("%s", e.MoreInfo), TextStyle: pterm.NewStyle(pterm.FgGray)}) + } + pterm.DefaultBulletList.WithItems(items).Render() +} + +func NewCLIError(code, title, moreInfoURL string, err error) *CLIError { + return &CLIError{ + Code: code, + Title: title, + Description: err.Error(), + ActualError: err, + MoreInfo: moreInfoURL, + } +} + +// HandleCLIError checks does the error exist, and if it does, prints the error and exits the program. +func HandleCLIError(err *CLIError) { + if err != nil { + err.Print() + os.Exit(1) + } +} diff --git a/cmd/kubectl-testkube/commands/common/helper.go b/cmd/kubectl-testkube/commands/common/helper.go index 280194daed0..f8b944bedfd 100644 --- a/cmd/kubectl-testkube/commands/common/helper.go +++ b/cmd/kubectl-testkube/commands/common/helper.go @@ -42,21 +42,12 @@ func (o HelmOptions) GetApiURI() string { return o.Master.URIs.Api } -func GetCurrentKubernetesContext() (string, error) { - kubectl, err := exec.LookPath("kubectl") - if err != nil { - return "", err +func HelmUpgradeOrInstallTestkubeCloud(options HelmOptions, cfg config.Data, isMigration bool) *CLIError { + helmPath, cliErr := lookupHelmPath() + if cliErr != nil { + return cliErr } - out, err := process.Execute(kubectl, "config", "current-context") - if err != nil { - return "", err - } - - return strings.TrimSpace(string(out)), nil -} - -func HelmUpgradeOrInstallTestkubeCloud(options HelmOptions, cfg config.Data, isMigration bool) error { // disable mongo and minio for cloud options.NoMinio = true options.NoMongo = true @@ -67,124 +58,160 @@ func HelmUpgradeOrInstallTestkubeCloud(options HelmOptions, cfg config.Data, isM } if options.Master.AgentToken == "" { - return fmt.Errorf("agent key and agent uri are required, please pass it with `--agent-token` and `--agent-uri` flags") + return NewCLIError( + TKErrInvalidInstallConfig, + "Invalid install config", + "Provide the agent token by setting the '--agent-token' flag", + errors.New("agent key is required")) } - helmPath, err := exec.LookPath("helm") + if cliErr := updateHelmRepo(helmPath, options.DryRun); cliErr != nil { + return cliErr + } + + args := prepareTestkubeProHelmArgs(options, isMigration) + output, err := runHelmCommand(helmPath, args, options.DryRun) if err != nil { return err } - // repo update - args := []string{"repo", "add", "kubeshop", "https://kubeshop.github.io/helm-charts"} - _, err = process.ExecuteWithOptions(process.Options{Command: helmPath, Args: args, DryRun: options.DryRun}) - if err != nil && !strings.Contains(err.Error(), "Error: repository name (kubeshop) already exists, please specify a different name") { - ui.WarnOnError("adding testkube repo", err) - } + ui.Debug("Helm command output:") + ui.Debug(helmPath, args...) - _, err = process.ExecuteWithOptions(process.Options{Command: helmPath, Args: []string{"repo", "update"}, DryRun: options.DryRun}) - ui.ExitOnError("updating helm repositories", err) + ui.Debug("Helm install testkube output", output) - // upgrade cloud - args = []string{ - "upgrade", "--install", "--create-namespace", - "--namespace", options.Namespace, - "--set", "testkube-api.cloud.url=" + options.Master.URIs.Agent, - "--set", "testkube-api.cloud.key=" + options.Master.AgentToken, - "--set", "testkube-api.cloud.uiURL=" + options.Master.URIs.Ui, - "--set", "testkube-logs.pro.url=" + options.Master.URIs.Logs, - "--set", "testkube-logs.pro.key=" + options.Master.AgentToken, - } - if isMigration { - args = append(args, "--set", "testkube-api.cloud.migrate=true") - } + return nil +} - if options.Master.EnvId != "" { - args = append(args, "--set", fmt.Sprintf("testkube-api.cloud.envId=%s", options.Master.EnvId)) - args = append(args, "--set", fmt.Sprintf("testkube-logs.pro.envId=%s", options.Master.EnvId)) - } - if options.Master.OrgId != "" { - args = append(args, "--set", fmt.Sprintf("testkube-api.cloud.orgId=%s", options.Master.OrgId)) - args = append(args, "--set", fmt.Sprintf("testkube-logs.pro.orgId=%s", options.Master.OrgId)) +func HelmUpgradeOrInstallTestkube(options HelmOptions) *CLIError { + helmPath, err := lookupHelmPath() + if err != nil { + return err } - args = append(args, "--set", fmt.Sprintf("global.features.logsV2=%v", options.Master.Features.LogsV2)) - - args = append(args, "--set", fmt.Sprintf("testkube-api.multinamespace.enabled=%t", options.MultiNamespace)) - args = append(args, "--set", fmt.Sprintf("testkube-operator.enabled=%t", !options.NoOperator)) - args = append(args, "--set", fmt.Sprintf("mongodb.enabled=%t", !options.NoMongo)) - args = append(args, "--set", fmt.Sprintf("testkube-api.minio.enabled=%t", !options.NoMinio)) - - args = append(args, "--set", fmt.Sprintf("testkube-api.minio.replicas=%d", options.MinioReplicas)) - args = append(args, "--set", fmt.Sprintf("mongodb.replicas=%d", options.MongoReplicas)) + ui.Info("Helm installing testkube framework") + if err = updateHelmRepo(helmPath, options.DryRun); err != nil { + return err + } - // if embedded nats is enabled disable nats chart - if options.EmbeddedNATS { - args = append(args, "--set", "testkube-api.nats.enabled=false") - args = append(args, "--set", "testkube-api.nats.embedded=true") + args := prepareTestkubeHelmArgs(options) + output, err := runHelmCommand(helmPath, args, options.DryRun) + if err != nil { + return NewCLIError(TKErrHelmCommandFailed, "Helm command failed: install or upgrade", "", err) } - args = append(args, options.Name, options.Chart) + ui.Debug("Helm install testkube output", output) + return nil +} - if options.Values != "" { - args = append(args, "--values", options.Values) +func lookupHelmPath() (string, *CLIError) { + helmPath, err := exec.LookPath("helm") + if err != nil { + return "", NewCLIError( + TKErrMissingDependencyHelm, + "Required dependency not found: helm", + "Install Helm by following this guide: https://helm.sh/docs/intro/install/", + err, + ) + } + return helmPath, nil +} + +func updateHelmRepo(helmPath string, dryRun bool) *CLIError { + helmRepoURL := "https://kubeshop.github.io/helm-charts" + _, err := runHelmCommand(helmPath, []string{"repo", "add", "kubeshop", helmRepoURL}, dryRun) + if err != nil && !strings.Contains(err.Error(), "Error: repository name (kubeshop) already exists, please specify a different name") { + return err } - out, err := process.ExecuteWithOptions(process.Options{Command: helmPath, Args: args, DryRun: options.DryRun}) + _, err = runHelmCommand(helmPath, []string{"repo", "update"}, dryRun) if err != nil { return err } - ui.Debug("Helm command output:") - ui.Debug(helmPath, args...) - - ui.Debug("Helm install testkube output", string(out)) - return nil } -func HelmUpgradeOrInstalTestkube(options HelmOptions) error { - helmPath, err := exec.LookPath("helm") +func runHelmCommand(helmPath string, args []string, dryRun bool) (commandOutput string, cliErr *CLIError) { + output, err := process.ExecuteWithOptions(process.Options{Command: helmPath, Args: args, DryRun: dryRun}) if err != nil { - return err + return "", NewCLIError( + TKErrHelmCommandFailed, + "Helm command failed", + "Retry the command with a bigger timeout by setting --timeout 30m, if the error still persists, reach out to Testkube support", + err, + ) + } + return string(output), nil +} + +// prepareTestkubeProHelmArgs prepares Helm arguments for Testkube Pro installation. +func prepareTestkubeProHelmArgs(options HelmOptions, isMigration bool) []string { + args := prepareCommonHelmArgs(options) + + args = append(args, + "--set", "testkube-api.cloud.url="+options.Master.URIs.Agent, + "--set", "testkube-api.cloud.key="+options.Master.AgentToken, + "--set", "testkube-api.cloud.uiURL="+options.Master.URIs.Ui, + "--set", "testkube-logs.pro.url="+options.Master.URIs.Logs, + "--set", "testkube-logs.pro.key="+options.Master.AgentToken, + ) + + if isMigration { + args = append(args, "--set", "testkube-api.cloud.migrate=true") } - ui.Info("Helm installing testkube framework") - args := []string{"repo", "add", "kubeshop", "https://kubeshop.github.io/helm-charts"} - _, err = process.ExecuteWithOptions(process.Options{Command: helmPath, Args: args, DryRun: options.DryRun}) - if err != nil && !strings.Contains(err.Error(), "Error: repository name (kubeshop) already exists, please specify a different name") { - ui.WarnOnError("adding testkube repo", err) + if options.Master.EnvId != "" { + args = append(args, "--set", fmt.Sprintf("testkube-api.cloud.envId=%s", options.Master.EnvId)) + args = append(args, "--set", fmt.Sprintf("testkube-logs.pro.envId=%s", options.Master.EnvId)) } - _, err = process.ExecuteWithOptions(process.Options{Command: helmPath, Args: []string{"repo", "update"}, DryRun: options.DryRun}) - ui.ExitOnError("updating helm repositories", err) + if options.Master.OrgId != "" { + args = append(args, "--set", fmt.Sprintf("testkube-api.cloud.orgId=%s", options.Master.OrgId)) + args = append(args, "--set", fmt.Sprintf("testkube-logs.pro.orgId=%s", options.Master.OrgId)) + } + + return args +} + +// prepareTestkubeHelmArgs prepares Helm arguments for Testkube OS installation. +func prepareTestkubeHelmArgs(options HelmOptions) []string { + args := prepareCommonHelmArgs(options) - args = []string{"upgrade", "--install", "--create-namespace", "--namespace", options.Namespace} - args = append(args, "--set", fmt.Sprintf("testkube-api.multinamespace.enabled=%t", options.MultiNamespace)) - args = append(args, "--set", fmt.Sprintf("testkube-operator.enabled=%t", !options.NoOperator)) - args = append(args, "--set", fmt.Sprintf("mongodb.enabled=%t", !options.NoMongo)) - args = append(args, "--set", fmt.Sprintf("testkube-api.minio.enabled=%t", !options.NoMinio)) if options.NoMinio { args = append(args, "--set", "testkube-api.logs.storage=mongo") } else { args = append(args, "--set", "testkube-api.logs.storage=minio") } - args = append(args, "--set", fmt.Sprintf("global.features.logsV2=%v", options.Master.Features.LogsV2)) + return args +} - args = append(args, options.Name, options.Chart) +// prepareCommonHelmArgs prepares common Helm arguments for both OS and Pro installation. +func prepareCommonHelmArgs(options HelmOptions) []string { + args := []string{ + "upgrade", "--install", "--create-namespace", + "--namespace", options.Namespace, + "--set", fmt.Sprintf("global.features.logsV2=%v", options.Master.Features.LogsV2), + "--set", fmt.Sprintf("testkube-api.multinamespace.enabled=%t", options.MultiNamespace), + "--set", fmt.Sprintf("testkube-api.minio.enabled=%t", !options.NoMinio), + "--set", fmt.Sprintf("testkube-api.minio.replicas=%d", options.MinioReplicas), "--set", fmt.Sprintf("testkube-operator.enabled=%t", !options.NoOperator), + "--set", fmt.Sprintf("mongodb.enabled=%t", !options.NoMongo), + "--set", fmt.Sprintf("mongodb.replicas=%d", options.MongoReplicas), + } if options.Values != "" { args = append(args, "--values", options.Values) } - out, err := process.ExecuteWithOptions(process.Options{Command: helmPath, Args: args, DryRun: options.DryRun}) - if err != nil { - return err + // if embedded nats is enabled disable nats chart + if options.EmbeddedNATS { + args = append(args, "--set", "testkube-api.nats.enabled=false") + args = append(args, "--set", "testkube-api.nats.embedded=true") } - ui.Debug("Helm install testkube output", string(out)) - return nil + args = append(args, options.Name, options.Chart) + return args } func PopulateHelmFlags(cmd *cobra.Command, options *HelmOptions) { @@ -283,6 +310,7 @@ func IsUserLoggedIn(cfg config.Data, options HelmOptions) bool { } return false } + func UpdateTokens(cfg config.Data, token, refreshToken string) error { var updated bool if token != cfg.CloudContext.ApiKey { @@ -301,21 +329,6 @@ func UpdateTokens(cfg config.Data, token, refreshToken string) error { return nil } -func KubectlScaleDeployment(namespace, deployment string, replicas int) (string, error) { - kubectl, err := exec.LookPath("kubectl") - if err != nil { - return "", err - } - - // kubectl patch --namespace=$n deployment $1 -p "{\"spec\":{\"replicas\": $2}}" - out, err := process.Execute(kubectl, "patch", "--namespace", namespace, "deployment", deployment, "-p", fmt.Sprintf("{\"spec\":{\"replicas\": %d}}", replicas)) - if err != nil { - return "", err - } - - return strings.TrimSpace(string(out)), nil -} - func RunAgentMigrations(cmd *cobra.Command) (hasMigrations bool, err error) { client, _, err := GetClient(cmd) ui.ExitOnError("getting client", err) @@ -438,8 +451,37 @@ func uiGetToken(tokenChan chan cloudlogin.Tokens) (string, string, error) { return token.IDToken, token.RefreshToken, nil } +func GetCurrentKubernetesContext() (string, *CLIError) { + kubectlPath, cliErr := lookupKubectlPath() + if cliErr != nil { + return "", cliErr + } + + output, err := runKubectlCommand(kubectlPath, []string{"config", "current-context"}) + if err != nil { + return "", err + } + + return strings.TrimSpace(output), nil +} + +func KubectlScaleDeployment(namespace, deployment string, replicas int) (string, error) { + kubectl, cliErr := lookupKubectlPath() + if cliErr != nil { + return "", cliErr + } + + // kubectl patch --namespace=$n deployment $1 -p "{\"spec\":{\"replicas\": $2}}" + out, err := process.Execute(kubectl, "patch", "--namespace", namespace, "deployment", deployment, "-p", fmt.Sprintf("{\"spec\":{\"replicas\": %d}}", replicas)) + if err != nil { + return "", err + } + + return strings.TrimSpace(string(out)), nil +} + func KubectlLogs(namespace string, labels map[string]string) error { - kubectl, err := exec.LookPath("kubectl") + kubectl, err := lookupKubectlPath() if err != nil { return err } @@ -464,9 +506,9 @@ func KubectlLogs(namespace string, labels map[string]string) error { } func KubectlPrintEvents(namespace string) error { - kubectl, err := exec.LookPath("kubectl") - if err != nil { - return err + kubectl, cliErr := lookupKubectlPath() + if cliErr != nil { + return cliErr } args := []string{ @@ -478,7 +520,7 @@ func KubectlPrintEvents(namespace string) error { ui.ShellCommand(kubectl, args...) ui.NL() - err = process.ExecuteAndStreamOutput(kubectl, args...) + err := process.ExecuteAndStreamOutput(kubectl, args...) if err != nil { return err } @@ -496,7 +538,7 @@ func KubectlPrintEvents(namespace string) error { } func KubectlDescribePods(namespace string) error { - kubectl, err := exec.LookPath("kubectl") + kubectl, err := lookupKubectlPath() if err != nil { return err } @@ -514,7 +556,7 @@ func KubectlDescribePods(namespace string) error { } func KubectlPrintPods(namespace string) error { - kubectl, err := exec.LookPath("kubectl") + kubectl, err := lookupKubectlPath() if err != nil { return err } @@ -533,7 +575,7 @@ func KubectlPrintPods(namespace string) error { } func KubectlGetStorageClass(namespace string) error { - kubectl, err := exec.LookPath("kubectl") + kubectl, err := lookupKubectlPath() if err != nil { return err } @@ -550,7 +592,7 @@ func KubectlGetStorageClass(namespace string) error { } func KubectlGetServices(namespace string) error { - kubectl, err := exec.LookPath("kubectl") + kubectl, err := lookupKubectlPath() if err != nil { return err } @@ -568,7 +610,7 @@ func KubectlGetServices(namespace string) error { } func KubectlDescribeServices(namespace string) error { - kubectl, err := exec.LookPath("kubectl") + kubectl, err := lookupKubectlPath() if err != nil { return err } @@ -587,7 +629,7 @@ func KubectlDescribeServices(namespace string) error { } func KubectlGetIngresses(namespace string) error { - kubectl, err := exec.LookPath("kubectl") + kubectl, err := lookupKubectlPath() if err != nil { return err } @@ -605,7 +647,7 @@ func KubectlGetIngresses(namespace string) error { } func KubectlDescribeIngresses(namespace string) error { - kubectl, err := exec.LookPath("kubectl") + kubectl, err := lookupKubectlPath() if err != nil { return err } @@ -623,6 +665,32 @@ func KubectlDescribeIngresses(namespace string) error { return process.ExecuteAndStreamOutput(kubectl, args...) } +func lookupKubectlPath() (string, *CLIError) { + kubectlPath, err := exec.LookPath("kubectl") + if err != nil { + return "", NewCLIError( + TKErrMissingDependencyKubectl, + "Required dependency not found: kubectl", + "Install kubectl by following this guide: https://kubernetes.io/docs/tasks/tools/#kubectl", + err, + ) + } + return kubectlPath, nil +} + +func runKubectlCommand(kubectlPath string, args []string) (output string, cliErr *CLIError) { + out, err := process.Execute(kubectlPath, args...) + if err != nil { + return "", NewCLIError( + TKErrKubectlCommandFailed, + "Kubectl command failed", + "Check does the kubeconfig file (~/.kube/config) exist and has correct permissions and is the Kubernetes cluster reachable and has Ready nodes by running 'kubectl get nodes' ", + err, + ) + } + return string(out), nil +} + func UiGetNamespace(cmd *cobra.Command, defaultNamespace string) string { var namespace string var err error diff --git a/cmd/kubectl-testkube/commands/init.go b/cmd/kubectl-testkube/commands/init.go index 3ae4851bab3..de72d0fb29c 100644 --- a/cmd/kubectl-testkube/commands/init.go +++ b/cmd/kubectl-testkube/commands/init.go @@ -85,8 +85,7 @@ func NewInitCmdStandalone() *cobra.Command { common.ProcessMasterFlags(cmd, &options, nil) - err := common.HelmUpgradeOrInstalTestkube(options) - ui.ExitOnError("Cannot install Testkube", err) + common.HandleCLIError(common.HelmUpgradeOrInstallTestkube(options)) ui.Info(`To help improve the quality of Testkube, we collect anonymous basic telemetry data. Head out to https://docs.testkube.io/articles/telemetry to read our policy or feel free to:`) @@ -136,10 +135,10 @@ func NewInitCmdDemo() *cobra.Command { sendTelemetry(cmd, cfg, license, "installation launched") - kubecontext, err := common.GetCurrentKubernetesContext() - if err != nil { - ui.Failf("kubeconfig not found") + kubecontext, cliErr := common.GetCurrentKubernetesContext() + if cliErr != nil { sendErrTelemetry(cmd, cfg, "install_kubeconfig_not_found", license, "kubeconfig not found", err) + common.HandleCLIError(cliErr) } sendTelemetry(cmd, cfg, license, "kubeconfig found") @@ -249,7 +248,8 @@ func isContextApproved(isNoConfirm bool, installedComponent string) bool { ui.NL() currentContext, err := common.GetCurrentKubernetesContext() - ui.ExitOnError("getting current context", err) + common.HandleCLIError(err) + ui.Alert("Current kubectl context:", currentContext) ui.NL() diff --git a/cmd/kubectl-testkube/commands/pro/connect.go b/cmd/kubectl-testkube/commands/pro/connect.go index fb37a5d5eaf..cde04922600 100644 --- a/cmd/kubectl-testkube/commands/pro/connect.go +++ b/cmd/kubectl-testkube/commands/pro/connect.go @@ -52,9 +52,10 @@ func NewConnectCmd() *cobra.Command { common.ProcessMasterFlags(cmd, &opts, &cfg) var clusterContext string + var cliErr *common.CLIError if cfg.ContextType == config.ContextTypeKubeconfig { - clusterContext, err = common.GetCurrentKubernetesContext() - ui.ExitOnError("getting current kubernetes context", err) + clusterContext, cliErr = common.GetCurrentKubernetesContext() + common.HandleCLIError(cliErr) } // TODO: implement context info @@ -136,8 +137,11 @@ func NewConnectCmd() *cobra.Command { } spinner := ui.NewSpinner("Connecting Testkube Pro") - err = common.HelmUpgradeOrInstallTestkubeCloud(opts, cfg, true) - ui.ExitOnError("Installing Testkube Pro", err) + if cliErr = common.HelmUpgradeOrInstallTestkubeCloud(opts, cfg, true); cliErr != nil { + spinner.Fail() + common.HandleCLIError(cliErr) + } + spinner.Success() ui.NL() diff --git a/cmd/kubectl-testkube/commands/pro/disconnect.go b/cmd/kubectl-testkube/commands/pro/disconnect.go index c13d89ac23b..c10f30d8d8d 100644 --- a/cmd/kubectl-testkube/commands/pro/disconnect.go +++ b/cmd/kubectl-testkube/commands/pro/disconnect.go @@ -47,12 +47,10 @@ func NewDisconnectCmd() *cobra.Command { apiContext = actx } var clusterContext string + var cliErr *common.CLIError if cfg.ContextType == config.ContextTypeKubeconfig { - clusterContext, err = common.GetCurrentKubernetesContext() - if err != nil { - pterm.Error.Printfln("Failed to get current Kubernetes context: %s", err.Error()) - return - } + clusterContext, cliErr = common.GetCurrentKubernetesContext() + common.HandleCLIError(cliErr) } // TODO: implement context info @@ -80,8 +78,11 @@ func NewDisconnectCmd() *cobra.Command { spinner := ui.NewSpinner("Disconnecting from Testkube Pro") - err = common.HelmUpgradeOrInstalTestkube(opts) - ui.ExitOnError("Installing Testkube Pro", err) + if cliErr := common.HelmUpgradeOrInstallTestkube(opts); cliErr != nil { + spinner.Fail() + common.HandleCLIError(cliErr) + } + spinner.Success() // let's scale down deployment of mongo diff --git a/cmd/kubectl-testkube/commands/pro/init.go b/cmd/kubectl-testkube/commands/pro/init.go index f6f0866d473..17bf69473d3 100644 --- a/cmd/kubectl-testkube/commands/pro/init.go +++ b/cmd/kubectl-testkube/commands/pro/init.go @@ -2,6 +2,7 @@ package pro import ( "fmt" + "os" "github.com/spf13/cobra" @@ -33,7 +34,16 @@ func NewInitCmd() *cobra.Command { ui.Logo() cfg, err := config.Load() - ui.ExitOnError("loading config file", err) + if err != nil { + cliErr := common.NewCLIError( + common.TKErrConfigLoadingFailed, + "Error loading testkube config file", + "Check is the Testkube config file (~/.testkube/config.json) accessible and has right permissions", + err, + ) + cliErr.Print() + os.Exit(1) + } ui.NL() common.ProcessMasterFlags(cmd, &options, &cfg) @@ -45,11 +55,10 @@ func NewInitCmd() *cobra.Command { ui.Warn("Please be sure you're on valid kubectl context before continuing!") ui.NL() - currentContext, err := common.GetCurrentKubernetesContext() - - if err != nil { + currentContext, cliErr := common.GetCurrentKubernetesContext() + if cliErr != nil { sendErrTelemetry(cmd, cfg, "k8s_context", err) - ui.ExitOnError("getting current context", err) + common.HandleCLIError(cliErr) } ui.Alert("Current kubectl context:", currentContext) ui.NL() @@ -63,11 +72,12 @@ func NewInitCmd() *cobra.Command { } spinner := ui.NewSpinner("Installing Testkube") - err = common.HelmUpgradeOrInstallTestkubeCloud(options, cfg, false) - if err != nil { - sendErrTelemetry(cmd, cfg, "helm_install", err) - ui.ExitOnError("Installing Testkube", err) + if cliErr := common.HelmUpgradeOrInstallTestkubeCloud(options, cfg, false); cliErr != nil { + spinner.Fail() + sendErrTelemetry(cmd, cfg, "helm_install", cliErr) + common.HandleCLIError(cliErr) } + spinner.Success() ui.NL() diff --git a/cmd/kubectl-testkube/commands/upgrade.go b/cmd/kubectl-testkube/commands/upgrade.go index f0d7e1ef6e4..a0334197fa7 100644 --- a/cmd/kubectl-testkube/commands/upgrade.go +++ b/cmd/kubectl-testkube/commands/upgrade.go @@ -1,6 +1,8 @@ package commands import ( + "os" + "github.com/spf13/cobra" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" @@ -35,7 +37,9 @@ func NewUpgradeCmd() *cobra.Command { ui.Warn("Please be sure you're on valid kubectl context before continuing!") ui.NL() - currentContext, err := common.GetCurrentKubernetesContext() + currentContext, cliErr := common.GetCurrentKubernetesContext() + common.HandleCLIError(cliErr) + ui.ExitOnError("getting current context", err) ui.Alert("Current kubectl context:", currentContext) ui.NL() @@ -70,8 +74,10 @@ func NewUpgradeCmd() *cobra.Command { ui.Success("All agent migrations executed successfully") } - err = common.HelmUpgradeOrInstalTestkube(options) - ui.ExitOnError("Upgrading Testkube", err) + if cliErr := common.HelmUpgradeOrInstallTestkube(options); cliErr != nil { + cliErr.Print() + os.Exit(1) + } } }, diff --git a/pkg/process/exec.go b/pkg/process/exec.go index 8b591629aa5..259eaae1cd2 100644 --- a/pkg/process/exec.go +++ b/pkg/process/exec.go @@ -17,7 +17,8 @@ type Options struct { DryRun bool } -// Execute runs system command and returns whole output also in case of error +// ExecuteWithOptions runs system command and returns whole output also in case of error. +// It also supports dry-run mode, where it only prints the command to be executed. func ExecuteWithOptions(options Options) (out []byte, err error) { if options.DryRun { fmt.Println("$ " + strings.Join(append([]string{options.Command}, options.Args...), " "))