diff --git a/cmd/kubectl-testkube/commands/common/errors.go b/cmd/kubectl-testkube/commands/common/errors.go index 8279f2f49a2..e48d0147b63 100644 --- a/cmd/kubectl-testkube/commands/common/errors.go +++ b/cmd/kubectl-testkube/commands/common/errors.go @@ -3,6 +3,7 @@ package common import ( "fmt" "os" + "strings" "github.com/spf13/cobra" @@ -52,13 +53,14 @@ const ( const helpUrl = "https://testkubeworkspace.slack.com" type CLIError struct { - Code ErrorCode - Title string - Description string - ActualError error - StackTrace string - MoreInfo string - Telemetry *ErrorTelemetry + Code ErrorCode + Title string + Description string + ActualError error + StackTrace string + MoreInfo string + ExecutedCommand string + Telemetry *ErrorTelemetry } type ErrorTelemetry struct { @@ -86,6 +88,15 @@ func (e *CLIError) Print() { pterm.DefaultSection.Println("Error Details") + cmd := "" + if e.ExecutedCommand != "" { + pterm.FgDarkGray.Printfln("Executed command: %s", e.ExecutedCommand) + params := strings.Split(e.ExecutedCommand, " ") + if len(params) > 0 { + cmd = params[0] + } + } + 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)}, @@ -94,6 +105,9 @@ func (e *CLIError) Print() { items = append(items, pterm.BulletListItem{Level: 0, Text: pterm.Sprintf("%s", e.MoreInfo), TextStyle: pterm.NewStyle(pterm.FgGray)}) } pterm.DefaultBulletList.WithItems(items).Render() + if cmd != "" { + pterm.DefaultBox.Printfln("Error description is provided in context of binary execution %s", cmd) + } pterm.Println() pterm.Println("Let us help you!") @@ -111,6 +125,11 @@ func NewCLIError(code ErrorCode, title, moreInfoURL string, err error) *CLIError } } +func (err *CLIError) WithExecutedCommand(executedCommand string) *CLIError { + err.ExecutedCommand = executedCommand + return err +} + // HandleCLIError checks does the error exist, and if it does, prints the error and exits the program. func HandleCLIError(err *CLIError) { if err != nil { diff --git a/cmd/kubectl-testkube/commands/common/helper.go b/cmd/kubectl-testkube/commands/common/helper.go index 4e9e8e58f71..778ba132acd 100644 --- a/cmd/kubectl-testkube/commands/common/helper.go +++ b/cmd/kubectl-testkube/commands/common/helper.go @@ -28,6 +28,7 @@ type HelmOptions struct { Name, Namespace, Chart, Values string NoMinio, NoMongo, NoConfirm bool MinioReplicas, MongoReplicas int + SetOptions, ArgOptions map[string]string // On-prem LicenseKey string @@ -101,16 +102,11 @@ func HelmUpgradeOrInstallTestkubeAgent(options HelmOptions, cfg config.Data, isM } args := prepareTestkubeProHelmArgs(options, isMigration) - output, err := runHelmCommand(helmPath, args, options.DryRun) + _, err := runHelmCommand(helmPath, args, options.DryRun) if err != nil { return err } - ui.Debug("Helm command output:") - ui.Debug(helmPath, args...) - - ui.Debug("Helm install testkube output", output) - return nil } @@ -147,7 +143,7 @@ func lookupHelmPath() (string, *CLIError) { return helmPath, nil } -func updateHelmRepo(helmPath string, dryRun bool, isOnPrem bool) *CLIError { +func updateHelmRepo(helmPath string, dryRun, isOnPrem bool) *CLIError { registryURL := "https://kubeshop.github.io/helm-charts" registryName := "kubeshop" if isOnPrem { @@ -176,16 +172,18 @@ func CleanExistingCompletedMigrationJobs(namespace string) (cliErr *CLIError) { } // Clean the job only when it's found and it's state is successful - ignore pending migrations. - succeeded, _ := runKubectlCommand(kubectlPath, []string{"get", "job", "testkube-enterprise-api-migrations", "-n", namespace, "-o", "jsonpath={.status.succeeded}"}) + cmd := []string{"get", "job", "testkube-enterprise-api-migrations", "-n", namespace, "-o", "jsonpath={.status.succeeded}"} + succeeded, _ := runKubectlCommand(kubectlPath, cmd) if succeeded == "1" { - _, err := runKubectlCommand(kubectlPath, []string{"delete", "job", "testkube-enterprise-api-migrations", "--namespace", namespace}) + cmd = []string{"delete", "job", "testkube-enterprise-api-migrations", "--namespace", namespace} + _, err := runKubectlCommand(kubectlPath, cmd) if err != nil { return NewCLIError( TKErrCleanOldMigrationJobFailed, "Can't clean old migrations job", "Migration job can't be deleted from some reason, check for errors in installation namespace, check execution. As a workaround try to delete job manually and retry installation/upgrade process", err, - ) + ).WithExecutedCommand(strings.Join(cmd, " ")) } } @@ -193,82 +191,120 @@ func CleanExistingCompletedMigrationJobs(namespace string) (cliErr *CLIError) { } func runHelmCommand(helmPath string, args []string, dryRun bool) (commandOutput string, cliErr *CLIError) { + cmd := strings.Join(append([]string{helmPath}, args...), " ") + ui.DebugNL() + ui.Debug("Helm command:") + ui.Debug(cmd) + output, err := process.ExecuteWithOptions(process.Options{Command: helmPath, Args: args, DryRun: dryRun}) + ui.DebugNL() + ui.Debug("Helm output:") + ui.Debug(string(output)) if err != nil { 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", + "Retry the command with a bigger timeout by setting --helm-arg timeout=30m, if the error still persists, reach out to Testkube support", err, - ) + ).WithExecutedCommand(cmd) } return string(output), nil } +func appendHelmArgs(args []string, options HelmOptions, settings map[string]string) []string { + for key, value := range settings { + if _, ok := options.SetOptions[key]; !ok { + args = append(args, "--set", fmt.Sprintf("%s=%s", key, value)) + } + } + + for key, value := range options.SetOptions { + args = append(args, "--set", fmt.Sprintf("%s=%s", key, value)) + } + + for key, value := range options.ArgOptions { + args = append(args, fmt.Sprintf("--%s", key)) + if value != "" { + args = append(args, value) + } + } + + return args +} + func prepareTestkubeOnPremDemoArgs(options HelmOptions) []string { - return []string{ + args := []string{ "upgrade", "--install", "--create-namespace", "--namespace", options.Namespace, - "--set", "global.enterpriseLicenseKey=" + options.LicenseKey, - "--values", options.DemoValuesURL, + } + + settings := map[string]string{ + "global.enterpriseLicenseKey": options.LicenseKey, + } + + args = append(appendHelmArgs(args, options, settings), "--values", options.DemoValuesURL, "--wait", - "testkube", "testkubeenterprise/testkube-enterprise"} + "testkube", "testkubeenterprise/testkube-enterprise") + + return args } // prepareTestkubeProHelmArgs prepares Helm arguments for Testkube Pro installation. func prepareTestkubeProHelmArgs(options HelmOptions, isMigration bool) []string { - args := prepareCommonHelmArgs(options) + args, settings := 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, - ) + settings["testkube-api.cloud.url"] = options.Master.URIs.Agent + settings["testkube-api.cloud.key"] = options.Master.AgentToken + settings["testkube-api.cloud.uiURL"] = options.Master.URIs.Ui + settings["testkube-logs.pro.url"] = options.Master.URIs.Logs + settings["testkube-logs.pro.key"] = options.Master.AgentToken if isMigration { - args = append(args, "--set", "testkube-api.cloud.migrate=true") + settings["testkube-api.cloud.migrate"] = "true" } 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)) + settings["testkube-api.cloud.envId"] = options.Master.EnvId + settings["testkube-logs.pro.envId"] = 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)) + settings["testkube-api.cloud.orgId"] = options.Master.OrgId + settings["testkube-logs.pro.orgId"] = options.Master.OrgId } - return args + return appendHelmArgs(args, options, settings) } // prepareTestkubeHelmArgs prepares Helm arguments for Testkube OS installation. func prepareTestkubeHelmArgs(options HelmOptions) []string { - args := prepareCommonHelmArgs(options) + args, settings := prepareCommonHelmArgs(options) if options.NoMinio { - args = append(args, "--set", "testkube-api.logs.storage=mongo") + settings["testkube-api.logs.storage"] = "mongo" } else { - args = append(args, "--set", "testkube-api.logs.storage=minio") + settings["testkube-api.logs.storage"] = "minio" } - return args + return appendHelmArgs(args, options, settings) } // prepareCommonHelmArgs prepares common Helm arguments for both OS and Pro installation. -func prepareCommonHelmArgs(options HelmOptions) []string { +func prepareCommonHelmArgs(options HelmOptions) ([]string, map[string]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), + } + + settings := map[string]string{ + "global.features.logsV2": fmt.Sprintf("%v", options.Master.Features.LogsV2), + "testkube-api.multinamespace.enabled": fmt.Sprintf("%t", options.MultiNamespace), + "testkube-api.minio.enabled": fmt.Sprintf("%t", !options.NoMinio), + "testkube-api.minio.replicas": fmt.Sprintf("%d", options.MinioReplicas), + "testkube-operator.enabled": fmt.Sprintf("%t", !options.NoOperator), + "mongodb.enabled": fmt.Sprintf("%t", !options.NoMongo), + "mongodb.replicas": fmt.Sprintf("%d", options.MongoReplicas), } if options.Values != "" { @@ -277,12 +313,12 @@ func prepareCommonHelmArgs(options HelmOptions) []string { // 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") + settings["testkube-api.nats.enabled"] = "false" + settings["testkube-api.nats.embedded"] = "true" } args = append(args, options.Name, options.Chart) - return args + return args, settings } func PopulateHelmFlags(cmd *cobra.Command, options *HelmOptions) { @@ -463,6 +499,7 @@ func LoginUser(authUri string, customConnector bool) (string, string, error) { connectorID = ui.Select("Choose your login method", []string{github, gitlab}) } + ui.Debug("Logging into cloud with parameters", authUri, connectorID) authUrl, tokenChan, err := cloudlogin.CloudLogin(context.Background(), authUri, strings.ToLower(connectorID)) if err != nil { return "", "", fmt.Errorf("cloud login: %w", err) @@ -477,6 +514,7 @@ func LoginUser(authUri string, customConnector bool) (string, string, error) { return "", "", fmt.Errorf("login cancelled") } + ui.Debug("Opening login page in browser to get a token", authUrl) // open browser with login page and redirect to localhost open.Run(authUrl) @@ -732,14 +770,21 @@ func lookupKubectlPath() (string, *CLIError) { } func runKubectlCommand(kubectlPath string, args []string) (output string, cliErr *CLIError) { + cmd := strings.Join(append([]string{kubectlPath}, args...), " ") + ui.DebugNL() + ui.Debug("Kubectl command:") + ui.Debug(cmd) out, err := process.Execute(kubectlPath, args...) + ui.DebugNL() + ui.Debug("Kubectl output:") + ui.Debug(string(out)) 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, - ) + ).WithExecutedCommand(cmd) } return string(out), nil } @@ -766,7 +811,7 @@ func RunDockerCommand(args []string) (output string, cliErr *CLIError) { "Docker command failed", "Check is the Docker service installed and running on your computer by executing 'docker info' ", err, - ) + ).WithExecutedCommand(strings.Join(append([]string{"docker"}, args...), " ")) } return string(out), nil } diff --git a/cmd/kubectl-testkube/commands/dashboard.go b/cmd/kubectl-testkube/commands/dashboard.go index 3aec322e431..b54bc8f556f 100644 --- a/cmd/kubectl-testkube/commands/dashboard.go +++ b/cmd/kubectl-testkube/commands/dashboard.go @@ -71,22 +71,26 @@ func openOnPremDashboard(cmd *cobra.Command, cfg config.Data, verbose, skipBrows uri := fmt.Sprintf("http://localhost:%d", uiLocalPort) ctx, cancel := context.WithCancel(context.Background()) + + ui.Debug("Port forwarding for api", config.EnterpriseApiName) err = k8sclient.PortForward(ctx, cfg.Namespace, config.EnterpriseApiName, config.EnterpriseApiPort, config.EnterpriseApiForwardingPort, verbose) if err != nil { sendErrTelemetry(cmd, cfg, "port_forward", license, "port forwarding api", err) } ui.ExitOnError("port forwarding api", err) + ui.Debug("Port forwarding for ui", config.EnterpriseUiName) err = k8sclient.PortForward(ctx, cfg.Namespace, config.EnterpriseUiName, config.EnterpriseUiPort, uiLocalPort, verbose) if err != nil { sendErrTelemetry(cmd, cfg, "port_forward", license, "port forwarding ui", err) } ui.ExitOnError("port forwarding ui", err) + ui.Debug("Port forwarding for dex", config.EnterpriseDexName) err = k8sclient.PortForward(ctx, cfg.Namespace, config.EnterpriseDexName, config.EnterpriseDexPort, config.EnterpriseDexForwardingPort, verbose) if err != nil { sendErrTelemetry(cmd, cfg, "port_forward", license, "port forwarding dex", err) } ui.ExitOnError("port forwarding dex", err) - + ui.Debug("Port forwarding for minio", config.EnterpriseMinioName) err = k8sclient.PortForward(ctx, cfg.Namespace, config.EnterpriseMinioName, config.EnterpriseMinioPort, config.EnterpriseMinioPortFrwardingPort, verbose) if err != nil { sendTelemetry(cmd, cfg, license, "port forwarding minio") @@ -94,6 +98,7 @@ func openOnPremDashboard(cmd *cobra.Command, cfg config.Data, verbose, skipBrows ui.ExitOnError("port forwarding minio", err) if !skipBrowser { + ui.Debug("Opening dashboard in browser", uri) err = open.Run(uri) if err != nil { sendErrTelemetry(cmd, cfg, "open_dashboard", license, "opening dashboard", err) diff --git a/cmd/kubectl-testkube/commands/init.go b/cmd/kubectl-testkube/commands/init.go index aa434d03be2..4fe0ff53eea 100644 --- a/cmd/kubectl-testkube/commands/init.go +++ b/cmd/kubectl-testkube/commands/init.go @@ -61,6 +61,7 @@ func NewInitCmd() *cobra.Command { func NewInitCmdStandalone() *cobra.Command { var export bool var options common.HelmOptions + var setOptions, argOptions map[string]string cmd := &cobra.Command{ Use: standaloneAgentProfile, @@ -81,7 +82,11 @@ func NewInitCmdStandalone() *cobra.Command { } common.ProcessMasterFlags(cmd, &options, nil) - + options.SetOptions = setOptions + options.ArgOptions = argOptions + ui.NL() + ui.H2("Running Helm command...") + ui.NL() 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:`) @@ -97,6 +102,8 @@ func NewInitCmdStandalone() *cobra.Command { } cmd.Flags().BoolVarP(&export, "export", "", false, "Export the values.yaml") + cmd.Flags().StringToStringVarP(&setOptions, "helm-set", "", nil, "helm set option in form of key=value") + cmd.Flags().StringToStringVarP(&argOptions, "helm-arg", "", nil, "helm arg option in form of key=value") common.PopulateHelmFlags(cmd, &options) common.PopulateMasterFlags(cmd, &options, false) @@ -106,6 +113,7 @@ func NewInitCmdStandalone() *cobra.Command { func NewInitCmdDemo() *cobra.Command { var noConfirm, dryRun, export bool var license, namespace string + var setOptions, argOptions map[string]string cmd := &cobra.Command{ Use: demoProfile, @@ -132,6 +140,9 @@ func NewInitCmdDemo() *cobra.Command { sendTelemetry(cmd, cfg, license, "installation launched") + ui.NL() + ui.H2("Running Kubectl command...") + ui.NL() kubecontext, cliErr := common.GetCurrentKubernetesContext() if cliErr != nil { if cfg.TelemetryEnabled { @@ -206,7 +217,7 @@ func NewInitCmdDemo() *cobra.Command { } } - spinner := ui.NewSpinner("Installing Testkube On-Prem Demo...") + spinner := ui.NewSpinner("Running Kubectl command...") sendTelemetry(cmd, cfg, license, "installing started") options := common.HelmOptions{ Namespace: namespace, @@ -225,6 +236,10 @@ func NewInitCmdDemo() *cobra.Command { common.HandleCLIError(cliErr) } + spinner.Success() + spinner = ui.NewSpinner("Running Helm command...") + options.SetOptions = setOptions + options.ArgOptions = argOptions cliErr = common.HelmUpgradeOrInstallTestkubeOnPremDemo(options) if cliErr != nil { spinner.Fail("Failed to install Testkube On-Prem Demo") @@ -264,6 +279,10 @@ func NewInitCmdDemo() *cobra.Command { sendTelemetry(cmd, cfg, license, "opening dashboard") cfg, err = config.Load() ui.ExitOnError("Cannot open dashboard", err) + + ui.NL() + ui.H2("Launching web browser...") + ui.NL() openOnPremDashboard(cmd, cfg, false, false, license) }, } @@ -273,6 +292,8 @@ func NewInitCmdDemo() *cobra.Command { cmd.Flags().StringVarP(&license, "license", "l", "", "License key") cmd.Flags().BoolVarP(&dryRun, "dry-run", "", false, "Dry run") cmd.Flags().StringVarP(&namespace, "namespace", "n", "", "Namespace to install "+demoInstallationName) + cmd.Flags().StringToStringVarP(&setOptions, "helm-set", "", nil, "helm set option in form of key=value") + cmd.Flags().StringToStringVarP(&argOptions, "helm-arg", "", nil, "helm arg option in form of key=value") return cmd } diff --git a/cmd/kubectl-testkube/commands/pro/init.go b/cmd/kubectl-testkube/commands/pro/init.go index 7f54702a57d..3270480f603 100644 --- a/cmd/kubectl-testkube/commands/pro/init.go +++ b/cmd/kubectl-testkube/commands/pro/init.go @@ -16,6 +16,8 @@ import ( func NewInitCmd() *cobra.Command { var export bool var noLogin bool // ignore ask for login + var setOptions, argOptions map[string]string + options := common.HelmOptions{ NoMinio: true, NoMongo: true, @@ -56,6 +58,9 @@ func NewInitCmd() *cobra.Command { ui.Warn("Please be sure you're on valid kubectl context before continuing!") ui.NL() + ui.NL() + ui.H2("Running Kubectl command...") + ui.NL() currentContext, cliErr := common.GetCurrentKubernetesContext() if cliErr != nil { sendErrTelemetry(cmd, cfg, "k8s_context", err) @@ -72,7 +77,9 @@ func NewInitCmd() *cobra.Command { } } - spinner := ui.NewSpinner("Installing Testkube") + spinner := ui.NewSpinner("Running Helm command...") + options.SetOptions = setOptions + options.ArgOptions = argOptions if cliErr := common.HelmUpgradeOrInstallTestkubeAgent(options, cfg, false); cliErr != nil { spinner.Fail() sendErrTelemetry(cmd, cfg, "helm_install", cliErr) @@ -98,6 +105,9 @@ func NewInitCmd() *cobra.Command { ui.H2("Saving Testkube CLI Pro context") var token, refreshToken string if !common.IsUserLoggedIn(cfg, options) { + ui.NL() + ui.H2("Launching web browser...") + ui.NL() token, refreshToken, err = common.LoginUser(options.Master.URIs.Auth, options.Master.CustomAuth) sendErrTelemetry(cmd, cfg, "login", err) ui.ExitOnError("user login", err) @@ -119,6 +129,8 @@ func NewInitCmd() *cobra.Command { cmd.Flags().BoolVarP(&export, "export", "", false, "Export the values.yaml") cmd.Flags().BoolVar(&options.MultiNamespace, "multi-namespace", false, "multi namespace mode") cmd.Flags().BoolVar(&options.NoOperator, "no-operator", false, "should operator be installed (for more instances in multi namespace mode it should be set to true)") + cmd.Flags().StringToStringVarP(&setOptions, "helm-set", "", nil, "helm set option in form of key=value") + cmd.Flags().StringToStringVarP(&argOptions, "helm-arg", "", nil, "helm arg option in form of key=value") return cmd } diff --git a/pkg/ui/printers.go b/pkg/ui/printers.go index 0d567cb05e6..362c2a80c2b 100644 --- a/pkg/ui/printers.go +++ b/pkg/ui/printers.go @@ -37,6 +37,13 @@ func (ui *UI) NL(amount ...int) { fmt.Fprintln(ui.Writer) } +func (ui *UI) DebugNL(amount ...int) { + if !ui.Verbose { + return + } + ui.NL(amount...) +} + // Success shows success in terminal func (ui *UI) Success(message string, subMessages ...string) { fmt.Fprintf(ui.Writer, "%s", LightYellow(message)) diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go index 82563b7c39b..3425926fa9d 100644 --- a/pkg/ui/ui.go +++ b/pkg/ui/ui.go @@ -61,6 +61,7 @@ func WarnOnErrorAndOutputPretty(item string, outputPretty bool, errors ...error) func Logo() { ui.Logo() } func LogoNoColor() { ui.LogoNoColor() } func NL(amount ...int) { ui.NL(amount...) } +func DebugNL(amount ...int) { ui.DebugNL(amount...) } func H1(message string) { ui.H1(message) } func H2(message string) { ui.H2(message) } func Paragraph(message string) { ui.Paragraph(message) }