From 6e7bddfa33dc5945fef3113fce28f6ca1c415aa4 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Thu, 30 Jan 2025 17:22:04 -0800 Subject: [PATCH] fix(v2): report preflight failures (#1774) * fix(v2): report preflight failures * f * f * f * f --- .../cli/adminconsole_resetpassword.go | 9 ++- cmd/installer/cli/install.go | 36 +++++----- cmd/installer/cli/install2.go | 66 +++++++++-------- cmd/installer/cli/install_runpreflights.go | 27 +++---- cmd/installer/cli/install_test.go | 4 +- cmd/installer/cli/join.go | 19 ++--- cmd/installer/cli/join2.go | 31 ++++---- cmd/installer/cli/join_runpreflights.go | 21 +++--- cmd/installer/cli/materialize.go | 6 +- cmd/installer/cli/metrics.go | 70 +++++++++++++++++++ cmd/installer/cli/reset.go | 6 +- cmd/installer/cli/restore.go | 6 +- cmd/installer/cli/restore2.go | 51 +++++--------- cmd/installer/cli/root.go | 36 +++++++++- cmd/installer/cli/shell.go | 6 +- cmd/installer/cli/supportbundle.go | 13 ++-- cmd/installer/cli/update.go | 6 +- cmd/installer/cli/version.go | 6 +- cmd/installer/cli/version_embeddeddata.go | 6 +- cmd/installer/cli/version_listimages.go | 6 +- cmd/installer/cli/version_metadata.go | 6 +- cmd/installer/main.go | 10 +-- pkg/metrics/reporter.go | 59 ++++++++++------ pkg/metrics/reporter_test.go | 2 +- pkg/preflights/run.go | 34 +++++++-- 25 files changed, 321 insertions(+), 221 deletions(-) create mode 100644 cmd/installer/cli/metrics.go diff --git a/cmd/installer/cli/adminconsole_resetpassword.go b/cmd/installer/cli/adminconsole_resetpassword.go index 81086f499..cf9a5179b 100644 --- a/cmd/installer/cli/adminconsole_resetpassword.go +++ b/cmd/installer/cli/adminconsole_resetpassword.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "fmt" "os" @@ -13,10 +14,8 @@ import ( func AdminConsoleResetPasswordCmd(ctx context.Context, name string) *cobra.Command { cmd := &cobra.Command{ - Use: "reset-password", - Short: fmt.Sprintf("Reset the %s Admin Console password", name), - SilenceErrors: true, - SilenceUsage: true, + Use: "reset-password", + Short: fmt.Sprintf("Reset the %s Admin Console password", name), PreRunE: func(cmd *cobra.Command, args []string) error { if os.Getuid() != 0 { return fmt.Errorf("reset-password command must be run as root") @@ -34,7 +33,7 @@ func AdminConsoleResetPasswordCmd(ctx context.Context, name string) *cobra.Comma password := args[0] if !validateAdminConsolePassword(password, password) { - return ErrNothingElseToAdd + return NewErrorNothingElseToAdd(errors.New("password is not valid")) } if err := kotscli.ResetPassword(password); err != nil { diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index aa7212f81..7683c0915 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -64,10 +64,8 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { ) cmd := &cobra.Command{ - Use: "install", - Short: fmt.Sprintf("Install %s", name), - SilenceErrors: true, - SilenceUsage: true, + Use: "install", + Short: fmt.Sprintf("Install %s", name), PreRunE: func(cmd *cobra.Command, args []string) error { if os.Getuid() != 0 { return fmt.Errorf("install command must be run as root") @@ -134,7 +132,8 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { logrus.Warnf("You downloaded an air gap bundle but didn't provide it with --airgap-bundle.") logrus.Warnf("If you continue, the installation will not use an air gap bundle and will connect to the internet.") if !prompts.New().Confirm("Do you want to proceed with an online installation?", false) { - return ErrNothingElseToAdd + // TODO: send aborted metrics event + return NewErrorNothingElseToAdd(errors.New("user aborted: air gap bundle downloaded but flag not provided")) } } @@ -170,7 +169,7 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { if !isAirgap { if err := maybePromptForAppUpdate(cmd.Context(), prompts.New(), license, assumeYes); err != nil { - if errors.Is(err, ErrNothingElseToAdd) { + if errors.As(err, &ErrorNothingElseToAdd{}) { metrics.ReportApplyFinished(cmd.Context(), licenseFile, nil, err) return err } @@ -231,8 +230,8 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { if err := RunHostPreflights(cmd, applier, replicatedAPIURL, proxyRegistryURL, isAirgap, proxy, cidrCfg, nil, assumeYes); err != nil { metrics.ReportApplyFinished(cmd.Context(), licenseFile, nil, err) - if err == ErrPreflightsHaveFail { - return ErrNothingElseToAdd + if errors.Is(err, preflights.ErrPreflightsHaveFail) { + return NewErrorNothingElseToAdd(err) } return err } @@ -395,7 +394,8 @@ func maybePromptForAppUpdate(ctx context.Context, prompt prompts.Prompt, license text := fmt.Sprintf("Do you want to continue installing %s anyway?", channelRelease.VersionLabel) if !prompt.Confirm(text, true) { - return ErrNothingElseToAdd + // TODO: send aborted metrics event + return NewErrorNothingElseToAdd(errors.New("user aborted: app not up-to-date")) } logrus.Debug("User confirmed prompt to continue installing out-of-date release") @@ -441,11 +441,11 @@ const minAdminPasswordLength = 6 func validateAdminConsolePassword(password, passwordCheck string) bool { if password != passwordCheck { - logrus.Info("Passwords don't match. Please try again.") + logrus.Errorf("Passwords don't match. Please try again.") return false } if len(password) < minAdminPasswordLength { - logrus.Infof("Passwords must have more than %d characters. Please try again.", minAdminPasswordLength) + logrus.Errorf("Password must have more than %d characters. Please try again.", minAdminPasswordLength) return false } return true @@ -1108,11 +1108,11 @@ func runHostPreflights(cmd *cobra.Command, hpf *v1beta2.HostPreflightSpec, proxy } if ignoreHostPreflightsFlag { if assumeYes { - metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, *output, true, cmd.CalledAs()) + metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, metrics.ClusterID(), *output, true, cmd.CalledAs()) return nil } if prompts.New().Confirm("Are you sure you want to ignore these failures and continue installing?", false) { - metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, *output, true, cmd.CalledAs()) + metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, metrics.ClusterID(), *output, true, cmd.CalledAs()) return nil // user continued after host preflights failed } } @@ -1122,8 +1122,8 @@ func runHostPreflights(cmd *cobra.Command, hpf *v1beta2.HostPreflightSpec, proxy } else { logrus.Info("Please address this issue and try again.") } - metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, *output, false, cmd.CalledAs()) - return ErrPreflightsHaveFail + metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, metrics.ClusterID(), *output, false, cmd.CalledAs()) + return preflights.ErrPreflightsHaveFail } // Warnings found @@ -1138,16 +1138,16 @@ func runHostPreflights(cmd *cobra.Command, hpf *v1beta2.HostPreflightSpec, proxy // so we just print the warnings and continue pb.Close() output.PrintTableWithoutInfo() - metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, *output, true, cmd.CalledAs()) + metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, metrics.ClusterID(), *output, true, cmd.CalledAs()) return nil } pb.Close() output.PrintTableWithoutInfo() if prompts.New().Confirm("Do you want to continue?", false) { - metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, *output, true, cmd.CalledAs()) + metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, metrics.ClusterID(), *output, true, cmd.CalledAs()) return nil } - metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, *output, false, cmd.CalledAs()) + metrics.ReportPreflightsFailed(cmd.Context(), replicatedAPIURL, metrics.ClusterID(), *output, false, cmd.CalledAs()) return fmt.Errorf("user aborted") } diff --git a/cmd/installer/cli/install2.go b/cmd/installer/cli/install2.go index a7985bad1..5c2e4682f 100644 --- a/cmd/installer/cli/install2.go +++ b/cmd/installer/cli/install2.go @@ -73,11 +73,9 @@ func Install2Cmd(ctx context.Context, name string) *cobra.Command { var flags Install2CmdFlags cmd := &cobra.Command{ - Use: "install2", - Short: fmt.Sprintf("Experimental installer for %s", name), - Hidden: true, - SilenceUsage: true, - SilenceErrors: true, + Use: "install2", + Short: fmt.Sprintf("Experimental installer for %s", name), + Hidden: true, PreRunE: func(cmd *cobra.Command, args []string) error { if err := preRunInstall2(cmd, &flags); err != nil { return err @@ -89,12 +87,14 @@ func Install2Cmd(ctx context.Context, name string) *cobra.Command { runtimeconfig.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { - metrics.ReportInstallationStarted(ctx, flags.license) - if err := runInstall2(cmd.Context(), name, flags); err != nil { - metrics.ReportInstallationFailed(ctx, flags.license, err) + clusterID := metrics.ClusterID() + metricsReporter := NewInstallReporter(flags.license, clusterID, cmd.CalledAs()) + metricsReporter.ReportInstallationStarted(ctx) + if err := runInstall2(cmd.Context(), name, flags, metricsReporter); err != nil { + metricsReporter.ReportInstallationFailed(ctx, err) return err } - metrics.ReportInstallationSucceeded(ctx, flags.license) + metricsReporter.ReportInstallationSucceeded(ctx) return nil }, } @@ -217,11 +217,15 @@ func preRunInstall2(cmd *cobra.Command, flags *Install2CmdFlags) error { return nil } -func runInstall2(ctx context.Context, name string, flags Install2CmdFlags) error { +func runInstall2(ctx context.Context, name string, flags Install2CmdFlags, metricsReporter preflights.MetricsReporter) error { if err := runInstallVerifyAndPrompt(ctx, name, &flags); err != nil { return err } + if err := ensureAdminConsolePassword(&flags); err != nil { + return err + } + logrus.Debugf("materializing binaries") if err := materializeFiles(flags.airgapBundle); err != nil { return fmt.Errorf("unable to materialize files: %w", err) @@ -243,9 +247,12 @@ func runInstall2(ctx context.Context, name string, flags Install2CmdFlags) error return fmt.Errorf("unable to configure network manager: %w", err) } - logrus.Debugf("running host preflights") - if err := runInstallPreflights(ctx, flags); err != nil { - return fmt.Errorf("unable to run preflights: %w", err) + logrus.Debugf("running install preflights") + if err := runInstallPreflights(ctx, flags, metricsReporter); err != nil { + if errors.Is(err, preflights.ErrPreflightsHaveFail) { + return NewErrorNothingElseToAdd(err) + } + return fmt.Errorf("unable to run install preflights: %w", err) } k0sCfg, err := installAndStartCluster(ctx, flags.networkInterface, flags.airgapBundle, flags.proxy, flags.cidrCfg, flags.overrides, nil) @@ -339,7 +346,7 @@ func runInstallVerifyAndPrompt(ctx context.Context, name string, flags *Install2 logrus.Debugf("checking license matches") license, err := getLicenseFromFilepath(flags.licenseFile) if err != nil { - return err // do not return the metricErr, as we want the user to see the error message without a prefix + return err } if flags.isAirgap { logrus.Debugf("checking airgap bundle matches binary") @@ -350,11 +357,11 @@ func runInstallVerifyAndPrompt(ctx context.Context, name string, flags *Install2 if !flags.isAirgap { if err := maybePromptForAppUpdate(ctx, prompts.New(), license, flags.assumeYes); err != nil { - if errors.Is(err, ErrNothingElseToAdd) { + if errors.As(err, &ErrorNothingElseToAdd{}) { return err } - // If we get an error other than ErrNothingElseToAdd, we warn and continue as - // this check is not critical. + // If we get an error other than ErrorNothingElseToAdd, we warn and continue as this + // check is not critical. logrus.Debugf("WARNING: Failed to check for newer app versions: %v", err) } } @@ -363,14 +370,14 @@ func runInstallVerifyAndPrompt(ctx context.Context, name string, flags *Install2 return err } - if flags.adminConsolePassword != "" { - if !validateAdminConsolePassword(flags.adminConsolePassword, flags.adminConsolePassword) { - return fmt.Errorf("unable to set the Admin Console password") - } - } else { + return nil +} + +func ensureAdminConsolePassword(flags *Install2CmdFlags) error { + if flags.adminConsolePassword == "" { // no password was provided if flags.assumeYes { - logrus.Infof("The Admin Console password is set to %s", "password") + logrus.Infof("The Admin Console password is set to %q", "password") flags.adminConsolePassword = "password" } else { maxTries := 3 @@ -380,13 +387,15 @@ func runInstallVerifyAndPrompt(ctx context.Context, name string, flags *Install2 if validateAdminConsolePassword(promptA, promptB) { flags.adminConsolePassword = promptA - break + return nil } } + return NewErrorNothingElseToAdd(errors.New("password is not valid")) } } - if flags.adminConsolePassword == "" { - return fmt.Errorf("no admin console password") + + if !validateAdminConsolePassword(flags.adminConsolePassword, flags.adminConsolePassword) { + return NewErrorNothingElseToAdd(errors.New("password is not valid")) } return nil @@ -483,7 +492,8 @@ func verifyChannelRelease(cmdName string, isAirgap bool, assumeYes bool) error { logrus.Warnf("You downloaded an air gap bundle but didn't provide it with --airgap-bundle.") logrus.Warnf("If you continue, the %s will not use an air gap bundle and will connect to the internet.", cmdName) if !prompts.New().Confirm(fmt.Sprintf("Do you want to proceed with an online %s?", cmdName), false) { - return ErrNothingElseToAdd + // TODO: send aborted metrics event + return NewErrorNothingElseToAdd(errors.New("user aborted: air gap bundle downloaded but flag not provided")) } } return nil @@ -499,7 +509,7 @@ func verifyNoInstallation(name string, cmdName string) error { logrus.Infof("If you want to %s, you need to remove the existing installation first.", cmdName) logrus.Infof("You can do this by running the following command:") logrus.Infof("\n sudo ./%s reset\n", name) - return ErrNothingElseToAdd + return NewErrorNothingElseToAdd(errors.New("previous installation detected")) } return nil } diff --git a/cmd/installer/cli/install_runpreflights.go b/cmd/installer/cli/install_runpreflights.go index 41a437182..7244528a4 100644 --- a/cmd/installer/cli/install_runpreflights.go +++ b/cmd/installer/cli/install_runpreflights.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "fmt" "github.com/replicatedhq/embedded-cluster/pkg/configutils" @@ -11,17 +12,6 @@ import ( "github.com/spf13/cobra" ) -// ErrNothingElseToAdd is an error returned when there is nothing else to add to the -// screen. This is useful when we want to exit an error from a function here but -// don't want to print anything else (possibly because we have already printed the -// necessary data to the screen). -var ErrNothingElseToAdd = fmt.Errorf("") - -// ErrPreflightsHaveFail is an error returned when we managed to execute the -// host preflights but they contain failures. We use this to differentiate the -// way we provide user feedback. -var ErrPreflightsHaveFail = fmt.Errorf("host preflight failures detected") - func InstallRunPreflightsCmd(ctx context.Context, name string) *cobra.Command { var flags Install2CmdFlags @@ -71,9 +61,12 @@ func runInstallRunPreflights(ctx context.Context, name string, flags Install2Cmd return fmt.Errorf("unable to configure sysctl: %w", err) } - logrus.Debugf("running host preflights") - if err := runInstallPreflights(ctx, flags); err != nil { - return err + logrus.Debugf("running install preflights") + if err := runInstallPreflights(ctx, flags, nil); err != nil { + if errors.Is(err, preflights.ErrPreflightsHaveFail) { + return NewErrorNothingElseToAdd(err) + } + return fmt.Errorf("unable to run install preflights: %w", err) } logrus.Info("Host preflights completed successfully") @@ -81,7 +74,7 @@ func runInstallRunPreflights(ctx context.Context, name string, flags Install2Cmd return nil } -func runInstallPreflights(ctx context.Context, flags Install2CmdFlags) error { +func runInstallPreflights(ctx context.Context, flags Install2CmdFlags, metricsReported preflights.MetricsReporter) error { var replicatedAPIURL, proxyRegistryURL string if flags.license != nil { replicatedAPIURL = flags.license.Spec.Endpoint @@ -100,10 +93,8 @@ func runInstallPreflights(ctx context.Context, flags Install2CmdFlags) error { SkipHostPreflights: flags.skipHostPreflights, IgnoreHostPreflights: flags.ignoreHostPreflights, AssumeYes: flags.assumeYes, + MetricsReporter: metricsReported, }); err != nil { - if err == preflights.ErrPreflightsHaveFail { - return ErrNothingElseToAdd - } return err } diff --git a/cmd/installer/cli/install_test.go b/cmd/installer/cli/install_test.go index 1c26f11bb..5115c1303 100644 --- a/cmd/installer/cli/install_test.go +++ b/cmd/installer/cli/install_test.go @@ -301,9 +301,9 @@ func Test_maybePromptForAppUpdate(t *testing.T) { } if tt.isErrNothingElseToAdd { - assert.Equal(t, ErrNothingElseToAdd, err) + assert.ErrorAs(t, err, &ErrorNothingElseToAdd{}) } else { - assert.NotEqual(t, ErrNothingElseToAdd, err) + assert.NotErrorAs(t, err, &ErrorNothingElseToAdd{}) } }) } diff --git a/cmd/installer/cli/join.go b/cmd/installer/cli/join.go index c2a0c749c..0b4e8fa20 100644 --- a/cmd/installer/cli/join.go +++ b/cmd/installer/cli/join.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "fmt" "os" "strings" @@ -15,6 +16,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/netutils" + "github.com/replicatedhq/embedded-cluster/pkg/preflights" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -35,11 +37,9 @@ func JoinCmd(ctx context.Context, name string) *cobra.Command { ) cmd := &cobra.Command{ - Use: "join ", - Short: fmt.Sprintf("Join %s", name), - Args: cobra.ExactArgs(2), - SilenceErrors: true, - SilenceUsage: true, + Use: "join ", + Short: fmt.Sprintf("Join %s", name), + Args: cobra.ExactArgs(2), PreRunE: func(cmd *cobra.Command, args []string) error { if os.Getuid() != 0 { return fmt.Errorf("join command must be run as root") @@ -76,7 +76,8 @@ func JoinCmd(ctx context.Context, name string) *cobra.Command { logrus.Infof("You downloaded an air gap bundle but are performing an online join.") logrus.Infof("To do an air gap join, pass the air gap bundle with --airgap-bundle.") if !prompts.New().Confirm("Do you want to proceed with an online join?", false) { - return ErrNothingElseToAdd + // TODO: send aborted metrics event + return NewErrorNothingElseToAdd(errors.New("user aborted: air gap bundle downloaded but flag not provided")) } } @@ -110,7 +111,7 @@ func JoinCmd(ctx context.Context, name string) *cobra.Command { logrus.Errorf("This node's IP address %s is not included in the no-proxy list (%s).", localIP, jcmd.InstallationSpec.Proxy.NoProxy) logrus.Infof(`The no-proxy list cannot easily be modified after initial installation.`) logrus.Infof(`Recreate the first node and pass all node IP addresses to --no-proxy.`) - return ErrNothingElseToAdd + return NewErrorNothingElseToAdd(errors.New("node ip address not included in no-proxy list")) } isAirgap := false @@ -175,8 +176,8 @@ func JoinCmd(ctx context.Context, name string) *cobra.Command { proxyRegistryURL := fmt.Sprintf("https://%s", runtimeconfig.ProxyRegistryAddress) if err := RunHostPreflights(cmd, applier, replicatedAPIURL, proxyRegistryURL, isAirgap, jcmd.InstallationSpec.Proxy, cidrCfg, jcmd.TCPConnectionsRequired, assumeYes); err != nil { metrics.ReportJoinFailed(cmd.Context(), jcmd.InstallationSpec.MetricsBaseURL, jcmd.ClusterID, err) - if err == ErrPreflightsHaveFail { - return ErrNothingElseToAdd + if errors.Is(err, preflights.ErrPreflightsHaveFail) { + return NewErrorNothingElseToAdd(err) } return err } diff --git a/cmd/installer/cli/join2.go b/cmd/installer/cli/join2.go index 1dd0e019a..93789a806 100644 --- a/cmd/installer/cli/join2.go +++ b/cmd/installer/cli/join2.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "fmt" "os" "strings" @@ -15,8 +16,8 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/k0s" "github.com/replicatedhq/embedded-cluster/pkg/kotsadm" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" - "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/netutils" + "github.com/replicatedhq/embedded-cluster/pkg/preflights" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/spinner" @@ -45,12 +46,10 @@ func Join2Cmd(ctx context.Context, name string) *cobra.Command { var flags Join2CmdFlags cmd := &cobra.Command{ - Use: "join2 ", - Short: fmt.Sprintf("Join %s", name), - Args: cobra.ExactArgs(2), - SilenceErrors: true, - SilenceUsage: true, - Hidden: true, + Use: "join2 ", + Short: fmt.Sprintf("Join %s", name), + Args: cobra.ExactArgs(2), + Hidden: true, PreRunE: func(cmd *cobra.Command, args []string) error { if err := preRunJoin2(&flags); err != nil { return err @@ -67,13 +66,14 @@ func Join2Cmd(ctx context.Context, name string) *cobra.Command { if err != nil { return fmt.Errorf("unable to get join token: %w", err) } - metrics.ReportJoinStarted(ctx, jcmd.InstallationSpec.MetricsBaseURL, jcmd.ClusterID) - if err := runJoin2(cmd.Context(), name, flags, jcmd); err != nil { - metrics.ReportJoinFailed(ctx, jcmd.InstallationSpec.MetricsBaseURL, jcmd.ClusterID, err) + metricsReporter := NewJoinReporter(jcmd.InstallationSpec.MetricsBaseURL, jcmd.ClusterID, cmd.CalledAs()) + metricsReporter.ReportJoinStarted(ctx) + if err := runJoin2(cmd.Context(), name, flags, jcmd, metricsReporter); err != nil { + metricsReporter.ReportJoinFailed(ctx, err) return err } - metrics.ReportJoinSucceeded(ctx, jcmd.InstallationSpec.MetricsBaseURL, jcmd.ClusterID) + metricsReporter.ReportJoinSucceeded(ctx) return nil }, } @@ -121,7 +121,7 @@ func addJoinFlags(cmd *cobra.Command, flags *Join2CmdFlags) error { return nil } -func runJoin2(ctx context.Context, name string, flags Join2CmdFlags, jcmd *kotsadm.JoinCommandResponse) error { +func runJoin2(ctx context.Context, name string, flags Join2CmdFlags, jcmd *kotsadm.JoinCommandResponse, metricsReporter preflights.MetricsReporter) error { if err := runJoinVerifyAndPrompt(name, flags, jcmd); err != nil { return err } @@ -147,7 +147,10 @@ func runJoin2(ctx context.Context, name string, flags Join2CmdFlags, jcmd *kotsa } logrus.Debugf("running join preflights") - if err := runJoinPreflights(ctx, jcmd, flags, cidrCfg); err != nil { + if err := runJoinPreflights(ctx, jcmd, flags, cidrCfg, metricsReporter); err != nil { + if errors.Is(err, preflights.ErrPreflightsHaveFail) { + return NewErrorNothingElseToAdd(err) + } return fmt.Errorf("unable to run join preflights: %w", err) } @@ -233,7 +236,7 @@ func runJoinVerifyAndPrompt(name string, flags Join2CmdFlags, jcmd *kotsadm.Join logrus.Errorf("This node's IP address %s is not included in the no-proxy list (%s).", localIP, jcmd.InstallationSpec.Proxy.NoProxy) logrus.Infof(`The no-proxy list cannot easily be modified after initial installation.`) logrus.Infof(`Recreate the first node and pass all node IP addresses to --no-proxy.`) - return ErrNothingElseToAdd + return NewErrorNothingElseToAdd(errors.New("node ip address not included in no-proxy list")) } return nil diff --git a/cmd/installer/cli/join_runpreflights.go b/cmd/installer/cli/join_runpreflights.go index f4c02b987..9c0a95dd6 100644 --- a/cmd/installer/cli/join_runpreflights.go +++ b/cmd/installer/cli/join_runpreflights.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "fmt" "github.com/replicatedhq/embedded-cluster/pkg/configutils" @@ -16,11 +17,9 @@ func JoinRunPreflightsCmd(ctx context.Context, name string) *cobra.Command { var flags Join2CmdFlags cmd := &cobra.Command{ - Use: "run-preflights", - Short: fmt.Sprintf("Run join host preflights for %s", name), - Args: cobra.ExactArgs(2), - SilenceErrors: true, - SilenceUsage: true, + Use: "run-preflights", + Short: fmt.Sprintf("Run join host preflights for %s", name), + Args: cobra.ExactArgs(2), PreRunE: func(cmd *cobra.Command, args []string) error { if err := preRunJoin2(&flags); err != nil { return err @@ -73,7 +72,10 @@ func runJoinRunPreflights(ctx context.Context, name string, flags Join2CmdFlags, } logrus.Debugf("running join preflights") - if err := runJoinPreflights(ctx, jcmd, flags, cidrCfg); err != nil { + if err := runJoinPreflights(ctx, jcmd, flags, cidrCfg, nil); err != nil { + if errors.Is(err, preflights.ErrPreflightsHaveFail) { + return NewErrorNothingElseToAdd(err) + } return fmt.Errorf("unable to run join preflights: %w", err) } @@ -82,7 +84,7 @@ func runJoinRunPreflights(ctx context.Context, name string, flags Join2CmdFlags, return nil } -func runJoinPreflights(ctx context.Context, jcmd *kotsadm.JoinCommandResponse, flags Join2CmdFlags, cidrCfg *CIDRConfig) error { +func runJoinPreflights(ctx context.Context, jcmd *kotsadm.JoinCommandResponse, flags Join2CmdFlags, cidrCfg *CIDRConfig, metricsReported preflights.MetricsReporter) error { if err := preflights.PrepareAndRun(ctx, preflights.PrepareAndRunOptions{ ReplicatedAPIURL: jcmd.InstallationSpec.MetricsBaseURL, // MetricsBaseURL is the replicated.app endpoint url ProxyRegistryURL: fmt.Sprintf("https://%s", runtimeconfig.ProxyRegistryAddress), @@ -95,10 +97,7 @@ func runJoinPreflights(ctx context.Context, jcmd *kotsadm.JoinCommandResponse, f AssumeYes: flags.assumeYes, TCPConnectionsRequired: jcmd.TCPConnectionsRequired, }); err != nil { - if err == preflights.ErrPreflightsHaveFail { - return ErrNothingElseToAdd - } - return fmt.Errorf("unable to prepare and run preflights: %w", err) + return err } return nil diff --git a/cmd/installer/cli/materialize.go b/cmd/installer/cli/materialize.go index afadae5b4..ebca55355 100644 --- a/cmd/installer/cli/materialize.go +++ b/cmd/installer/cli/materialize.go @@ -17,10 +17,8 @@ func MaterializeCmd(ctx context.Context, name string) *cobra.Command { ) cmd := &cobra.Command{ - Use: "materialize", - Short: "Materialize embedded assets into the data directory", - SilenceErrors: true, - SilenceUsage: true, + Use: "materialize", + Short: "Materialize embedded assets into the data directory", PreRunE: func(cmd *cobra.Command, args []string) error { if os.Getuid() != 0 { return fmt.Errorf("materialize command must be run as root") diff --git a/cmd/installer/cli/metrics.go b/cmd/installer/cli/metrics.go new file mode 100644 index 000000000..153a0916d --- /dev/null +++ b/cmd/installer/cli/metrics.go @@ -0,0 +1,70 @@ +package cli + +import ( + "context" + + "github.com/google/uuid" + "github.com/replicatedhq/embedded-cluster/pkg/metrics" + preflightstypes "github.com/replicatedhq/embedded-cluster/pkg/preflights/types" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" +) + +type InstallReporter struct { + license *kotsv1beta1.License + clusterID uuid.UUID + cmd string +} + +func NewInstallReporter(license *kotsv1beta1.License, clusterID uuid.UUID, cmd string) *InstallReporter { + return &InstallReporter{ + license: license, + clusterID: clusterID, + cmd: cmd, + } +} + +func (r *InstallReporter) ReportInstallationStarted(ctx context.Context) { + metrics.ReportInstallationStarted(ctx, r.license, r.clusterID) +} + +func (r *InstallReporter) ReportInstallationSucceeded(ctx context.Context) { + metrics.ReportInstallationSucceeded(ctx, r.license, r.clusterID) +} + +func (r *InstallReporter) ReportInstallationFailed(ctx context.Context, err error) { + metrics.ReportInstallationFailed(ctx, r.license, r.clusterID, err) +} + +func (r *InstallReporter) ReportPreflightsFailed(ctx context.Context, output preflightstypes.Output, bypassed bool) { + metrics.ReportPreflightsFailed(ctx, r.license.Spec.Endpoint, r.clusterID, output, bypassed, r.cmd) +} + +type JoinReporter struct { + baseURL string + clusterID uuid.UUID + cmd string +} + +func NewJoinReporter(baseURL string, clusterID uuid.UUID, cmd string) *JoinReporter { + return &JoinReporter{ + baseURL: baseURL, + clusterID: clusterID, + cmd: cmd, + } +} + +func (r *JoinReporter) ReportJoinStarted(ctx context.Context) { + metrics.ReportJoinStarted(ctx, r.baseURL, r.clusterID) +} + +func (r *JoinReporter) ReportJoinSucceeded(ctx context.Context) { + metrics.ReportJoinSucceeded(ctx, r.baseURL, r.clusterID) +} + +func (r *JoinReporter) ReportJoinFailed(ctx context.Context, err error) { + metrics.ReportJoinFailed(ctx, r.baseURL, r.clusterID, err) +} + +func (r *JoinReporter) ReportPreflightsFailed(ctx context.Context, output preflightstypes.Output, bypassed bool) { + metrics.ReportPreflightsFailed(ctx, r.baseURL, r.clusterID, output, bypassed, r.cmd) +} diff --git a/cmd/installer/cli/reset.go b/cmd/installer/cli/reset.go index 799dd2cc8..4f9085f26 100644 --- a/cmd/installer/cli/reset.go +++ b/cmd/installer/cli/reset.go @@ -50,10 +50,8 @@ func ResetCmd(ctx context.Context, name string) *cobra.Command { ) cmd := &cobra.Command{ - Use: "reset", - Short: "Remove %s from the current node", - SilenceErrors: true, - SilenceUsage: true, + Use: "reset", + Short: "Remove %s from the current node", PreRunE: func(cmd *cobra.Command, args []string) error { if os.Getuid() != 0 { return fmt.Errorf("reset command must be run as root") diff --git a/cmd/installer/cli/restore.go b/cmd/installer/cli/restore.go index 64b8fcb52..575c6955b 100644 --- a/cmd/installer/cli/restore.go +++ b/cmd/installer/cli/restore.go @@ -28,10 +28,8 @@ func RestoreCmd(ctx context.Context, name string) *cobra.Command { var s3Store s3BackupStore cmd := &cobra.Command{ - Use: "restore", - Short: fmt.Sprintf("Restore a %s cluster", name), - SilenceErrors: true, - SilenceUsage: true, + Use: "restore", + Short: fmt.Sprintf("Restore a %s cluster", name), PreRunE: func(cmd *cobra.Command, args []string) error { if err := preRunInstall2(cmd, &flags); err != nil { return err diff --git a/cmd/installer/cli/restore2.go b/cmd/installer/cli/restore2.go index 95c666dd6..86671e4ea 100644 --- a/cmd/installer/cli/restore2.go +++ b/cmd/installer/cli/restore2.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" "encoding/json" + "errors" "fmt" "io" "net/url" @@ -39,7 +40,7 @@ import ( velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" @@ -88,10 +89,8 @@ func Restore2Cmd(ctx context.Context, name string) *cobra.Command { var skipStoreValidation bool cmd := &cobra.Command{ - Use: "restore2", - Short: fmt.Sprintf("Restore a %s cluster", name), - SilenceErrors: true, - SilenceUsage: true, + Use: "restore2", + Short: fmt.Sprintf("Restore a %s cluster", name), PreRunE: func(cmd *cobra.Command, args []string) error { if err := preRunInstall2(cmd, &flags); err != nil { return err @@ -359,22 +358,12 @@ func runRestoreStepNew(ctx context.Context, name string, flags Install2CmdFlags, return fmt.Errorf("unable to materialize binaries: %w", err) } - logrus.Debugf("running host preflights") - if err := preflights.PrepareAndRun(ctx, preflights.PrepareAndRunOptions{ - Proxy: flags.proxy, - PodCIDR: flags.cidrCfg.PodCIDR, - ServiceCIDR: flags.cidrCfg.ServiceCIDR, - GlobalCIDR: flags.cidrCfg.GlobalCIDR, - PrivateCAs: flags.privateCAs, - IsAirgap: flags.isAirgap, - SkipHostPreflights: flags.skipHostPreflights, - IgnoreHostPreflights: flags.ignoreHostPreflights, - AssumeYes: flags.assumeYes, - }); err != nil { - if err == preflights.ErrPreflightsHaveFail { - return ErrNothingElseToAdd + logrus.Debugf("running install preflights") + if err := runInstallPreflights(ctx, flags, nil); err != nil { + if errors.Is(err, preflights.ErrPreflightsHaveFail) { + return NewErrorNothingElseToAdd(err) } - return fmt.Errorf("unable to prepare and run preflights: %w", err) + return fmt.Errorf("unable to run install preflights: %w", err) } _, err = installAndStartCluster(ctx, flags.networkInterface, flags.airgapBundle, flags.proxy, flags.cidrCfg, flags.overrides, nil) @@ -648,7 +637,7 @@ func setECRestoreState(ctx context.Context, state ecRestoreState, backupName str }, } - if err := kcli.Create(ctx, ns); err != nil && !errors.IsAlreadyExists(err) { + if err := kcli.Create(ctx, ns); err != nil && !k8serrors.IsAlreadyExists(err) { return fmt.Errorf("unable to create namespace: %w", err) } @@ -667,14 +656,12 @@ func setECRestoreState(ctx context.Context, state ecRestoreState, backupName str } err = kcli.Create(ctx, cm) - if err != nil && !errors.IsAlreadyExists(err) { - return fmt.Errorf("unable to create config map: %w", err) - } - - if errors.IsAlreadyExists(err) { + if k8serrors.IsAlreadyExists(err) { if err := kcli.Update(ctx, cm); err != nil { return fmt.Errorf("unable to update config map: %w", err) } + } else if err != nil { + return fmt.Errorf("unable to create config map: %w", err) } return nil @@ -694,7 +681,7 @@ func resetECRestoreState(ctx context.Context) error { }, } - if err := kcli.Delete(ctx, cm); err != nil && !errors.IsNotFound(err) { + if err := kcli.Delete(ctx, cm); err != nil && !k8serrors.IsNotFound(err) { return fmt.Errorf("unable to delete config map: %w", err) } @@ -1157,7 +1144,7 @@ func ensureRestoreResourceModifiers(ctx context.Context, backup *velerov1.Backup return fmt.Errorf("unable to create kube client: %w", err) } - if err := kcli.Create(ctx, cm); err != nil && !errors.IsAlreadyExists(err) { + if err := kcli.Create(ctx, cm); err != nil && !k8serrors.IsAlreadyExists(err) { return fmt.Errorf("unable to create config map: %w", err) } @@ -1322,12 +1309,12 @@ func restoreAppFromBackup(ctx context.Context, backup *velerov1.Backup, restore // check if a restore object already exists rest := velerov1api.Restore{} err = kcli.Get(ctx, types.NamespacedName{Name: restoreName, Namespace: runtimeconfig.VeleroNamespace}, &rest) - if err != nil && !errors.IsNotFound(err) { + if err != nil && !k8serrors.IsNotFound(err) { return fmt.Errorf("unable to get restore: %w", err) } // create a new restore object if it doesn't exist - if errors.IsNotFound(err) { + if k8serrors.IsNotFound(err) { restore.Namespace = runtimeconfig.VeleroNamespace restore.Name = restoreName if restore.Annotations == nil { @@ -1364,12 +1351,12 @@ func restoreFromBackup(ctx context.Context, backup *velerov1.Backup, drComponent // check if a restore object already exists rest := velerov1api.Restore{} err = kcli.Get(ctx, types.NamespacedName{Name: restoreName, Namespace: runtimeconfig.VeleroNamespace}, &rest) - if err != nil && !errors.IsNotFound(err) { + if err != nil && !k8serrors.IsNotFound(err) { return fmt.Errorf("unable to get restore: %w", err) } // create a new restore object if it doesn't exist - if errors.IsNotFound(err) { + if k8serrors.IsNotFound(err) { restoreLabels := map[string]string{} switch drComponent { case disasterRecoveryComponentAdminConsole, disasterRecoveryComponentECO: diff --git a/cmd/installer/cli/root.go b/cmd/installer/cli/root.go index acce971e4..c6e3c61fa 100644 --- a/cmd/installer/cli/root.go +++ b/cmd/installer/cli/root.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "fmt" "os" @@ -12,11 +13,40 @@ import ( "github.com/spf13/cobra" ) +// ErrorNothingElseToAdd is an error returned when there is nothing else to add to the screen. This +// is useful when we want to exit an error from a function here but don't want to print anything +// else (possibly because we have already printed the necessary data to the screen). +type ErrorNothingElseToAdd struct { + Err error +} + +func (e ErrorNothingElseToAdd) Error() string { + return e.Err.Error() +} + +func NewErrorNothingElseToAdd(err error) ErrorNothingElseToAdd { + return ErrorNothingElseToAdd{ + Err: err, + } +} + +func InitAndExecute(ctx context.Context, name string) { + cmd := RootCmd(ctx, name) + err := cmd.Execute() + if err != nil { + if !errors.As(err, &ErrorNothingElseToAdd{}) { + fmt.Fprintln(os.Stderr, err) + } + os.Exit(1) + } +} + func RootCmd(ctx context.Context, name string) *cobra.Command { cmd := &cobra.Command{ - Use: name, - Short: name, - SilenceUsage: true, + Use: name, + Short: name, + SilenceUsage: true, + SilenceErrors: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { if dryrun.Enabled() { dryrun.RecordFlags(cmd.Flags()) diff --git a/cmd/installer/cli/shell.go b/cmd/installer/cli/shell.go index db6a72cb5..62b425c75 100644 --- a/cmd/installer/cli/shell.go +++ b/cmd/installer/cli/shell.go @@ -29,10 +29,8 @@ const welcome = ` func ShellCmd(ctx context.Context, name string) *cobra.Command { cmd := &cobra.Command{ - Use: "shell", - Short: "Start a shell with access to the cluster", - SilenceErrors: true, - SilenceUsage: true, + Use: "shell", + Short: "Start a shell with access to the cluster", PreRunE: func(cmd *cobra.Command, args []string) error { if os.Getuid() != 0 { return fmt.Errorf("shell command must be run as root") diff --git a/cmd/installer/cli/supportbundle.go b/cmd/installer/cli/supportbundle.go index 02d30f60f..83f2e339e 100644 --- a/cmd/installer/cli/supportbundle.go +++ b/cmd/installer/cli/supportbundle.go @@ -3,6 +3,7 @@ package cli import ( "bytes" "context" + "errors" "fmt" "io" "os" @@ -13,16 +14,13 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" rcutil "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig/util" "github.com/replicatedhq/embedded-cluster/pkg/spinner" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) func SupportBundleCmd(ctx context.Context, name string) *cobra.Command { cmd := &cobra.Command{ - Use: "support-bundle", - Short: "Generate a support bundle for the embedded-cluster", - SilenceErrors: true, - SilenceUsage: true, + Use: "support-bundle", + Short: "Generate a support bundle for the embedded-cluster", PreRunE: func(cmd *cobra.Command, args []string) error { if os.Getuid() != 0 { return fmt.Errorf("support-bundle command must be run as root") @@ -39,8 +37,7 @@ func SupportBundleCmd(ctx context.Context, name string) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { supportBundle := runtimeconfig.PathToEmbeddedClusterBinary("kubectl-support_bundle") if _, err := os.Stat(supportBundle); err != nil { - logrus.Errorf("support-bundle command can only be run after an install attempt") - return ErrNothingElseToAdd + return errors.New("support-bundle command can only be run after an install attempt") } hostSupportBundle := runtimeconfig.PathToEmbeddedClusterSupportFile("host-support-bundle.yaml") @@ -88,7 +85,7 @@ func SupportBundleCmd(ctx context.Context, name string) *cobra.Command { spin.CloseWithError() io.Copy(os.Stdout, stdout) io.Copy(os.Stderr, stderr) - return ErrNothingElseToAdd + return NewErrorNothingElseToAdd(errors.New("failed to generate support bundle")) } spin.Infof("Support bundle saved at %s", destination) diff --git a/cmd/installer/cli/update.go b/cmd/installer/cli/update.go index f2fb3644a..efb5f204b 100644 --- a/cmd/installer/cli/update.go +++ b/cmd/installer/cli/update.go @@ -19,10 +19,8 @@ func UpdateCmd(ctx context.Context, name string) *cobra.Command { ) cmd := &cobra.Command{ - Use: "update", - Short: fmt.Sprintf("Update %s", name), - SilenceErrors: true, - SilenceUsage: true, + Use: "update", + Short: fmt.Sprintf("Update %s", name), PreRunE: func(cmd *cobra.Command, args []string) error { if os.Getuid() != 0 { return fmt.Errorf("update command must be run as root") diff --git a/cmd/installer/cli/version.go b/cmd/installer/cli/version.go index a012e3546..8c53df03f 100644 --- a/cmd/installer/cli/version.go +++ b/cmd/installer/cli/version.go @@ -17,10 +17,8 @@ import ( func VersionCmd(ctx context.Context, name string) *cobra.Command { cmd := &cobra.Command{ - Use: "version", - Short: fmt.Sprintf("Show the %s component versions", name), - SilenceErrors: true, - SilenceUsage: true, + Use: "version", + Short: fmt.Sprintf("Show the %s component versions", name), RunE: func(cmd *cobra.Command, args []string) error { applierVersions, err := addons.NewApplier(addons.WithoutPrompt(), addons.OnlyDefaults(), addons.Quiet()).Versions(config.AdditionalCharts()) if err != nil { diff --git a/cmd/installer/cli/version_embeddeddata.go b/cmd/installer/cli/version_embeddeddata.go index a723c0d65..b26d54fb6 100644 --- a/cmd/installer/cli/version_embeddeddata.go +++ b/cmd/installer/cli/version_embeddeddata.go @@ -11,10 +11,8 @@ import ( func VersionEmbeddedDataCmd(ctx context.Context, name string) *cobra.Command { cmd := &cobra.Command{ - Use: "embedded-data", - Short: "Read the application data embedded in the cluster", - SilenceErrors: true, - SilenceUsage: true, + Use: "embedded-data", + Short: "Read the application data embedded in the cluster", RunE: func(cmd *cobra.Command, args []string) error { // Application app, err := release.GetApplication() diff --git a/cmd/installer/cli/version_listimages.go b/cmd/installer/cli/version_listimages.go index c97f2260f..63d4112b8 100644 --- a/cmd/installer/cli/version_listimages.go +++ b/cmd/installer/cli/version_listimages.go @@ -14,10 +14,8 @@ func VersionListImagesCmd(ctx context.Context, name string) *cobra.Command { ) cmd := &cobra.Command{ - Use: "list-images", - Short: "List images embedded in the cluster", - SilenceErrors: true, - SilenceUsage: true, + Use: "list-images", + Short: "List images embedded in the cluster", RunE: func(cmd *cobra.Command, args []string) error { k0sCfg := config.RenderK0sConfig() diff --git a/cmd/installer/cli/version_metadata.go b/cmd/installer/cli/version_metadata.go index 384f04cd6..5ad4c6f74 100644 --- a/cmd/installer/cli/version_metadata.go +++ b/cmd/installer/cli/version_metadata.go @@ -15,10 +15,8 @@ func VersionMetadataCmd(ctx context.Context, name string) *cobra.Command { ) cmd := &cobra.Command{ - Use: "metadata", - Short: "Print metadata about this release", - SilenceErrors: true, - SilenceUsage: true, + Use: "metadata", + Short: "Print metadata about this release", RunE: func(cmd *cobra.Command, args []string) error { k0sCfg := config.RenderK0sConfig() diff --git a/cmd/installer/main.go b/cmd/installer/main.go index 19c7c61cd..796396362 100644 --- a/cmd/installer/main.go +++ b/cmd/installer/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "fmt" "os" "path" @@ -22,12 +21,5 @@ func main() { name := path.Base(os.Args[0]) - InitAndExecute(ctx, name) -} - -func InitAndExecute(ctx context.Context, name string) { - if err := cli.RootCmd(ctx, name).Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } + cli.InitAndExecute(ctx, name) } diff --git a/pkg/metrics/reporter.go b/pkg/metrics/reporter.go index 83313fb1a..1a0425b4d 100644 --- a/pkg/metrics/reporter.go +++ b/pkg/metrics/reporter.go @@ -3,6 +3,7 @@ package metrics import ( "context" "encoding/json" + "errors" "os" "strings" "sync" @@ -23,6 +24,21 @@ import ( var clusterIDMut sync.Mutex var clusterID *uuid.UUID +// ErrorNoFail is an error that is excluded from metrics failures. +type ErrorNoFail struct { + Err error +} + +func NewErrorNoFail(err error) ErrorNoFail { + return ErrorNoFail{ + Err: err, + } +} + +func (e ErrorNoFail) Error() string { + return e.Err.Error() +} + // BaseURL determines the base url to be used when sending metrics over. func BaseURL(license *kotsv1beta1.License) string { if os.Getenv("EMBEDDED_CLUSTER_METRICS_BASEURL") != "" { @@ -67,7 +83,7 @@ func SetClusterID(id uuid.UUID) { } // ReportInstallationStarted reports that the installation has started. -func ReportInstallationStarted(ctx context.Context, license *kotsv1beta1.License) { +func ReportInstallationStarted(ctx context.Context, license *kotsv1beta1.License, clusterID uuid.UUID) { rel, _ := release.GetChannelRelease() appChannel, appVersion := "", "" if rel != nil { @@ -76,7 +92,7 @@ func ReportInstallationStarted(ctx context.Context, license *kotsv1beta1.License } Send(ctx, BaseURL(license), types.InstallationStarted{ - ClusterID: ClusterID(), + ClusterID: clusterID, Version: versions.Version, Flags: strings.Join(redactFlags(os.Args[1:]), " "), BinaryName: runtimeconfig.BinaryName(), @@ -88,14 +104,17 @@ func ReportInstallationStarted(ctx context.Context, license *kotsv1beta1.License } // ReportInstallationSucceeded reports that the installation has succeeded. -func ReportInstallationSucceeded(ctx context.Context, license *kotsv1beta1.License) { - Send(ctx, BaseURL(license), types.InstallationSucceeded{ClusterID: ClusterID(), Version: versions.Version}) +func ReportInstallationSucceeded(ctx context.Context, license *kotsv1beta1.License, clusterID uuid.UUID) { + Send(ctx, BaseURL(license), types.InstallationSucceeded{ClusterID: clusterID, Version: versions.Version}) } // ReportInstallationFailed reports that the installation has failed. -func ReportInstallationFailed(ctx context.Context, license *kotsv1beta1.License, err error) { +func ReportInstallationFailed(ctx context.Context, license *kotsv1beta1.License, clusterID uuid.UUID, err error) { + if errors.As(err, &ErrorNoFail{}) { + return + } Send(ctx, BaseURL(license), types.InstallationFailed{ - ClusterID: ClusterID(), + ClusterID: clusterID, Version: versions.Version, Reason: err.Error(), }) @@ -130,17 +149,19 @@ func ReportJoinSucceeded(ctx context.Context, baseURL string, clusterID uuid.UUI } // ReportJoinFailed reports that a join has failed. -func ReportJoinFailed(ctx context.Context, baseURL string, clusterID uuid.UUID, exterr error) { - hostname, err := os.Hostname() - if err != nil { - logrus.Warnf("unable to get hostname: %s", err) +func ReportJoinFailed(ctx context.Context, baseURL string, clusterID uuid.UUID, err error) { + if errors.As(err, &ErrorNoFail{}) { + return + } + hostname, _ := os.Hostname() + if hostname == "" { hostname = "unknown" } Send(ctx, baseURL, types.JoinFailed{ ClusterID: clusterID, Version: versions.Version, NodeName: hostname, - Reason: exterr.Error(), + Reason: err.Error(), }) } @@ -148,7 +169,7 @@ func ReportJoinFailed(ctx context.Context, baseURL string, clusterID uuid.UUID, func ReportApplyStarted(ctx context.Context, licenseFlag string) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - ReportInstallationStarted(ctx, License(licenseFlag)) + ReportInstallationStarted(ctx, License(licenseFlag), ClusterID()) } // ReportApplyFinished reports an InstallationSucceeded or an InstallationFailed. @@ -160,18 +181,14 @@ func ReportApplyFinished(ctx context.Context, licenseFlag string, license *kotsv ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err != nil { - ReportInstallationFailed(ctx, license, err) + ReportInstallationFailed(ctx, license, ClusterID(), err) return } - ReportInstallationSucceeded(ctx, license) + ReportInstallationSucceeded(ctx, license, ClusterID()) } // ReportPreflightsFailed reports that the preflights failed but were bypassed. -func ReportPreflightsFailed(ctx context.Context, url string, output preflightstypes.Output, bypassed bool, entryCommand string) { - if url == "" { - url = BaseURL(nil) - } - +func ReportPreflightsFailed(ctx context.Context, baseURL string, clusterID uuid.UUID, output preflightstypes.Output, bypassed bool, entryCommand string) { hostname, err := os.Hostname() if err != nil { logrus.Warnf("unable to get hostname: %s", err) @@ -190,12 +207,12 @@ func ReportPreflightsFailed(ctx context.Context, url string, output preflightsty } ev := types.PreflightsFailed{ - ClusterID: ClusterID(), + ClusterID: clusterID, Version: versions.Version, NodeName: hostname, PreflightOutput: string(outputJSON), EventType: eventType, EntryCommand: entryCommand, } - go Send(ctx, url, ev) + go Send(ctx, baseURL, ev) } diff --git a/pkg/metrics/reporter_test.go b/pkg/metrics/reporter_test.go index f33fb28d5..7c53736e3 100644 --- a/pkg/metrics/reporter_test.go +++ b/pkg/metrics/reporter_test.go @@ -59,7 +59,7 @@ func TestReportInstallationStarted(t *testing.T) { defer func() { os.Args = originalArgs }() os.Args = append([]string{os.Args[0]}, test.OSArgs...) - ReportInstallationStarted(context.Background(), license) + ReportInstallationStarted(context.Background(), license, ClusterID()) }) } } diff --git a/pkg/preflights/run.go b/pkg/preflights/run.go index 455af0421..e64b8fac0 100644 --- a/pkg/preflights/run.go +++ b/pkg/preflights/run.go @@ -7,6 +7,7 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/dryrun" + "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/preflights/types" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/release" @@ -16,10 +17,9 @@ import ( "github.com/sirupsen/logrus" ) -// ErrPreflightsHaveFail is an error returned when we managed to execute the -// host preflights but they contain failures. We use this to differentiate the -// way we provide user feedback. -var ErrPreflightsHaveFail = fmt.Errorf("host preflight failures detected") +// ErrPreflightsHaveFail is an error returned when we managed to execute the host preflights but +// they contain failures. We use this to differentiate the way we provide user feedback. +var ErrPreflightsHaveFail = metrics.NewErrorNoFail(fmt.Errorf("host preflight failures detected")) type PrepareAndRunOptions struct { ReplicatedAPIURL string @@ -34,6 +34,11 @@ type PrepareAndRunOptions struct { IgnoreHostPreflights bool AssumeYes bool TCPConnectionsRequired []string + MetricsReporter MetricsReporter +} + +type MetricsReporter interface { + ReportPreflightsFailed(ctx context.Context, output types.Output, bypassed bool) } func PrepareAndRun(ctx context.Context, opts PrepareAndRunOptions) error { @@ -144,9 +149,15 @@ func runHostPreflights(ctx context.Context, hpf *v1beta2.HostPreflightSpec, opts if opts.IgnoreHostPreflights { if opts.AssumeYes { + if opts.MetricsReporter != nil { + opts.MetricsReporter.ReportPreflightsFailed(ctx, *output, true) + } return nil } if prompts.New().Confirm("Are you sure you want to ignore these failures and continue installing?", false) { + if opts.MetricsReporter != nil { + opts.MetricsReporter.ReportPreflightsFailed(ctx, *output, true) + } return nil // user continued after host preflights failed } } @@ -157,6 +168,9 @@ func runHostPreflights(ctx context.Context, hpf *v1beta2.HostPreflightSpec, opts logrus.Info("Please address this issue and try again.") } + if opts.MetricsReporter != nil { + opts.MetricsReporter.ReportPreflightsFailed(ctx, *output, true) + } return ErrPreflightsHaveFail } @@ -173,6 +187,9 @@ func runHostPreflights(ctx context.Context, hpf *v1beta2.HostPreflightSpec, opts // so we just print the warnings and continue pb.Close() output.PrintTableWithoutInfo() + if opts.MetricsReporter != nil { + opts.MetricsReporter.ReportPreflightsFailed(ctx, *output, true) + } return nil } @@ -180,10 +197,15 @@ func runHostPreflights(ctx context.Context, hpf *v1beta2.HostPreflightSpec, opts output.PrintTableWithoutInfo() if !prompts.New().Confirm("Do you want to continue?", false) { - pb.Close() - return fmt.Errorf("user aborted") + if opts.MetricsReporter != nil { + opts.MetricsReporter.ReportPreflightsFailed(ctx, *output, true) + } + return ErrPreflightsHaveFail } + if opts.MetricsReporter != nil { + opts.MetricsReporter.ReportPreflightsFailed(ctx, *output, true) + } return nil }