From c758b02ab81de212b19bc7a0964acbcbf2f1cc09 Mon Sep 17 00:00:00 2001 From: Kimmo Lehto Date: Thu, 8 Feb 2024 11:45:06 +0200 Subject: [PATCH] Enable golangci-lint and go unit test github actions (#409) * Enable golangci-lint github action Signed-off-by: Kimmo Lehto Signed-off-by: Kimmo Lehto * Automatic fixes via --fix Signed-off-by: Kimmo Lehto * Manual lint fixes Signed-off-by: Kimmo Lehto * Make unit tests pass Signed-off-by: Kimmo Lehto * Manual fixes round 2 Signed-off-by: Kimmo Lehto * Normalize logging syntax to "%s: .." instead of "%s .." Signed-off-by: Kimmo Lehto * Restore the logo, exlude in linter Signed-off-by: Kimmo Lehto * Remove "hostKind" argument from GetDockerInfo Signed-off-by: Kimmo Lehto * Restore the RunHooks title test Signed-off-by: Kimmo Lehto * Disable perfsprint Signed-off-by: Kimmo Lehto * Fixed phase/manager logic for the phase success Signed-off-by: Dimitar Dimitrov --------- Signed-off-by: Kimmo Lehto Signed-off-by: Kimmo Lehto Signed-off-by: Dimitar Dimitrov Co-authored-by: Dimitar Dimitrov --- .github/workflows/golangci-lint.yaml | 34 ++++ .golangci.yml | 92 +++++++++-- cmd/apply.go | 29 ++-- cmd/client_config.go | 13 +- cmd/common.go | 14 +- cmd/describe.go | 27 ++-- cmd/download_launchpad.go | 24 +-- cmd/exec.go | 10 +- cmd/init.go | 21 ++- cmd/register.go | 23 ++- cmd/reset.go | 29 ++-- main.go | 8 +- pkg/analytics/analytics.go | 35 +++-- pkg/analytics/user.go | 6 +- pkg/cmd/register/register.go | 37 +++-- pkg/completion/completion.go | 9 +- pkg/config/config.go | 98 +++++++----- pkg/config/migration/migration.go | 4 +- pkg/config/migration/v1/v1.go | 119 ++++++++------ pkg/config/migration/v11/v11.go | 20 ++- pkg/config/migration/v12/v12.go | 5 +- pkg/config/migration/v13/v13.go | 5 +- pkg/config/migration/v1beta1/v1beta1.go | 44 +++--- pkg/config/migration/v1beta2/v1beta2.go | 4 +- pkg/config/migration/v1beta3/v1beta3.go | 2 + pkg/config/user/config.go | 17 +- pkg/configurer/centos/centos.go | 7 +- pkg/configurer/common.go | 17 +- pkg/configurer/enterpriselinux/el.go | 42 +++-- pkg/configurer/linux.go | 132 ++++++++++------ pkg/configurer/sles/sles.go | 38 +++-- pkg/configurer/ubuntu/ubuntu.go | 37 +++-- pkg/configurer/windows.go | 108 +++++++------ pkg/docker/hub/hub.go | 18 ++- pkg/docker/image.go | 15 +- pkg/log/formatter_hook.go | 9 +- pkg/log/host_logger.go | 156 +++++++++++++++++++ pkg/mke/mke.go | 54 ++++--- pkg/msr/msr.go | 48 +++--- pkg/phase/manager.go | 23 +-- pkg/phase/phase.go | 19 ++- pkg/product/common/phase/connect.go | 52 +++---- pkg/product/common/phase/run_hooks.go | 57 ++++--- pkg/product/mke/api/cluster.go | 14 +- pkg/product/mke/api/cluster_spec.go | 65 +++++--- pkg/product/mke/api/cluster_test.go | 10 +- pkg/product/mke/api/host.go | 74 +++++---- pkg/product/mke/api/hosts.go | 41 ++--- pkg/product/mke/api/mke_config.go | 69 ++++---- pkg/product/mke/api/msr_config.go | 18 ++- pkg/product/mke/api/node.go | 4 +- pkg/product/mke/api/node_test.go | 2 +- pkg/product/mke/apply.go | 11 +- pkg/product/mke/client_config.go | 8 +- pkg/product/mke/describe.go | 11 +- pkg/product/mke/exec.go | 102 +++++++----- pkg/product/mke/mke.go | 6 +- pkg/product/mke/phase/authenticate_docker.go | 12 +- pkg/product/mke/phase/clean_up.go | 10 +- pkg/product/mke/phase/configure_mcr.go | 20 ++- pkg/product/mke/phase/describe.go | 61 ++++---- pkg/product/mke/phase/detect_os.go | 13 +- pkg/product/mke/phase/download_bundle.go | 58 ++++--- pkg/product/mke/phase/download_installer.go | 26 ++-- pkg/product/mke/phase/gather_facts.go | 70 +++------ pkg/product/mke/phase/init_swarm.go | 13 +- pkg/product/mke/phase/install_mcr.go | 36 +++-- pkg/product/mke/phase/install_mke.go | 43 +++-- pkg/product/mke/phase/install_mke_certs.go | 29 ++-- pkg/product/mke/phase/install_msr.go | 6 +- pkg/product/mke/phase/join_controllers.go | 2 +- pkg/product/mke/phase/join_msr_replicas.go | 8 +- pkg/product/mke/phase/join_workers.go | 4 +- pkg/product/mke/phase/label_nodes.go | 8 +- pkg/product/mke/phase/prepare_host.go | 47 +++--- pkg/product/mke/phase/pull_mke_images.go | 44 ++++-- pkg/product/mke/phase/pull_msr_images.go | 24 ++- pkg/product/mke/phase/remove_nodes.go | 62 ++++---- pkg/product/mke/phase/restart_mcr.go | 16 +- pkg/product/mke/phase/uninstall_mcr.go | 17 +- pkg/product/mke/phase/uninstall_mke.go | 9 +- pkg/product/mke/phase/uninstall_msr.go | 8 +- pkg/product/mke/phase/upgrade_check.go | 14 +- pkg/product/mke/phase/upgrade_mcr.go | 45 +++--- pkg/product/mke/phase/upgrade_mke.go | 5 +- pkg/product/mke/phase/upgrade_msr.go | 9 +- pkg/product/mke/phase/upload_images.go | 20 ++- pkg/product/mke/phase/validate_facts.go | 38 +++-- pkg/product/mke/phase/validate_facts_test.go | 14 +- pkg/product/mke/phase/validate_hosts.go | 80 ++++++---- pkg/product/mke/phase/validate_mke_health.go | 49 ++++-- pkg/product/mke/reset.go | 7 +- pkg/retry/retry.go | 58 +++++++ pkg/swarm/swarm.go | 8 +- pkg/util/install.go | 4 +- pkg/util/io.go | 14 +- pkg/util/logo.go | 2 +- version/version.go | 12 +- 98 files changed, 1904 insertions(+), 1117 deletions(-) create mode 100644 .github/workflows/golangci-lint.yaml create mode 100644 pkg/log/host_logger.go create mode 100644 pkg/retry/retry.go diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml new file mode 100644 index 000000000..685a4379b --- /dev/null +++ b/.github/workflows/golangci-lint.yaml @@ -0,0 +1,34 @@ +name: Go lint +on: + pull_request: + paths: + - '**.go' + - 'go.mod' + - 'go.sum' + - '.golangci.yml' + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + if: github.ref != 'refs/heads/main' + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + check-latest: true + + - name: Check go.mod/go.sum to be consistent + run: go mod tidy -v && git diff --exit-code + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + skip-cache: true + only-new-issues: false + args: --verbose diff --git a/.golangci.yml b/.golangci.yml index df1220aa6..d624a60df 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,30 +1,88 @@ -# Visit https://golangci-lint.run/ for usage documentation -# and information on other useful linters -issues: - max-per-linter: 0 - max-same-issues: 0 +run: + timeout: 8m + + skip-dirs-use-default: false + skip-files: + - ".*\\.gen\\.go" + - examples/* + - test/* + - logo.go + - logo_windows.go + tests: false + allow-parallel-runners: true linters: - disable-all: true enable: + # enabled by default + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + # additional + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - dupword - durationcheck - #- errcheck + - errchkjson + - errname + - errorlint + - execinquery - exportloopref - #- forcetypeassert + - forcetypeassert + - gci + - gocritic - godot + - goerr113 - gofmt - - gosimple - - ineffassign - - makezero + - gofumpt + - goimports + - goprintffuncname + - gosec + - importas + - ireturn + - maintidx + - mirror - misspell - - nilerr + - nakedret + - nilnil + - nolintlint + - nosprintfhostport + - prealloc - predeclared - - staticcheck + - reassign + - revive + - stylecheck - tenv - unconvert - unparam - - unused - - vet + - usestdlibvars + - varnamelen + - wastedassign + - whitespace + - wrapcheck -run: - timeout: 10m +linters-settings: + varnamelen: + max-distance: 10 + ignore-decls: + - w http.ResponseWriter + - r *http.Request + - i int + - n int + - p []byte + - mu sync.Mutex + - wg sync.WaitGroup + - h Host + - h os.Host + - h *api.Host + - ok bool + - s string + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/cmd/apply.go b/cmd/apply.go index 2758f4208..c10c7dd41 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "os" "time" @@ -9,13 +10,14 @@ import ( "github.com/Mirantis/mcc/pkg/config" "github.com/Mirantis/mcc/pkg/util" "github.com/Mirantis/mcc/version" - "github.com/mattn/go-isatty" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" event "gopkg.in/segmentio/analytics-go.v3" ) +var errInvalidArguments = errors.New("invalid arguments") + // NewApplyCommand creates new apply command to be called from cli. func NewApplyCommand() *cli.Command { return &cli.Command{ @@ -47,7 +49,7 @@ func NewApplyCommand() *cli.Command { After: actions(closeAnalytics, upgradeCheckResult), Action: func(ctx *cli.Context) (err error) { if ctx.Int("concurrency") < 1 { - return fmt.Errorf("invalid --concurrency %d (must be 1 or more)", ctx.Int("concurrency")) + return fmt.Errorf("%w: invalid --concurrency %d (must be 1 or more)", errInvalidArguments, ctx.Int("concurrency")) } var logFile *os.File @@ -57,40 +59,39 @@ func NewApplyCommand() *cli.Command { product, err := config.ProductFromFile(ctx.String("config")) if err != nil { - return + return fmt.Errorf("failed to load product config: %w", err) } defer func() { if err != nil && logFile != nil { log.Infof("See %s for more logs ", logFile.Name()) } - }() // Add logger to dump all log levels to file logFile, err = addFileLogger(product.ClusterName(), "apply.log") if err != nil { - return + return fmt.Errorf("failed to add file logger: %w", err) } if isatty.IsTerminal(os.Stdout.Fd()) { os.Stdout.WriteString(util.Logo) - os.Stdout.WriteString(fmt.Sprintf(" Mirantis Launchpad (c) 2022 Mirantis, Inc. %s\n\n", version.Version)) + fmt.Fprintf(os.Stdout, " Mirantis Launchpad (c) 2022 Mirantis, Inc. %s\n\n", version.Version) } err = product.Apply(ctx.Bool("disable-cleanup"), ctx.Bool("force"), ctx.Int("concurrency")) - if err != nil { analytics.TrackEvent("Cluster Apply Failed", nil) - } else { - duration := time.Since(start) - props := event.Properties{ - "duration": duration.Seconds(), - } - analytics.TrackEvent("Cluster Apply Completed", props) + return fmt.Errorf("failed to apply cluster: %w", err) + } + + duration := time.Since(start) + props := event.Properties{ + "duration": duration.Seconds(), } + analytics.TrackEvent("Cluster Apply Completed", props) - return + return nil }, } } diff --git a/cmd/client_config.go b/cmd/client_config.go index 102bca6a9..b4adbc136 100644 --- a/cmd/client_config.go +++ b/cmd/client_config.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "os" "strings" @@ -36,17 +37,17 @@ func NewClientConfigCommand() *cli.Command { Action: func(ctx *cli.Context) error { product, err := config.ProductFromFile(ctx.String("config")) if err != nil { - return err + return fmt.Errorf("failed to read product configuration: %w", err) } - err = product.ClientConfig() - if err != nil { + if err := product.ClientConfig(); err != nil { analytics.TrackEvent("Client configuration download Failed", nil) - } else { - analytics.TrackEvent("Client configuration download Completed", nil) + return fmt.Errorf("failed to download client configuration: %w", err) } - return err + analytics.TrackEvent("Client configuration download Completed", nil) + + return nil }, } } diff --git a/cmd/common.go b/cmd/common.go index 61492217d..1ae6c22a9 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -12,10 +12,9 @@ import ( "github.com/Mirantis/mcc/pkg/product/mke/phase" "github.com/Mirantis/mcc/pkg/util" "github.com/Mirantis/mcc/version" - "github.com/mitchellh/go-homedir" - "github.com/k0sproject/rig" "github.com/k0sproject/rig/exec" + "github.com/mitchellh/go-homedir" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -163,7 +162,9 @@ func initExec(ctx *cli.Context) error { func checkLicense(ctx *cli.Context) error { if !ctx.Bool("accept-license") { - return analytics.RequireRegisteredUser() + if err := analytics.RequireRegisteredUser(); err != nil { + return fmt.Errorf("error while checking license agreement: %w", err) + } } return nil } @@ -171,7 +172,7 @@ func checkLicense(ctx *cli.Context) error { func addFileLogger(clusterName, filename string) (*os.File, error) { home, err := homedir.Dir() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get user home directory: %w", err) } clusterDir := path.Join(home, constant.StateBaseDir, "cluster", clusterName) @@ -179,10 +180,9 @@ func addFileLogger(clusterName, filename string) (*os.File, error) { return nil, fmt.Errorf("error while creating directory for logs: %w", err) } logFileName := path.Join(clusterDir, filename) - logFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) - + logFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) if err != nil { - return nil, fmt.Errorf("Failed to create log file at %s: %s", logFileName, err.Error()) + return nil, fmt.Errorf("failed to create log file at %s: %w", logFileName, err) } // Send all logs to named file, this ensures we always have debug logs also available when needed. diff --git a/cmd/describe.go b/cmd/describe.go index f87517ac0..d4d6abdab 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -1,16 +1,16 @@ package cmd import ( + "errors" "fmt" "strings" "time" "github.com/Mirantis/mcc/pkg/analytics" "github.com/Mirantis/mcc/pkg/config" + log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" event "gopkg.in/segmentio/analytics-go.v3" - - log "github.com/sirupsen/logrus" ) var reports = []string{"hosts", "mke", "msr", "config"} @@ -24,6 +24,8 @@ func reportIsKnown(n string) bool { return false } +var errInvalidReport = errors.New("invalid report") + // NewDescribeCommand creates new describe command to be called from cli. func NewDescribeCommand() *cli.Command { return &cli.Command{ @@ -40,10 +42,10 @@ func NewDescribeCommand() *cli.Command { Action: func(ctx *cli.Context) error { report := ctx.Args().First() if report == "" { - return fmt.Errorf("missing report name argument") + return fmt.Errorf("%w: missing report name", errInvalidReport) } if !reportIsKnown(report) { - return fmt.Errorf("unknown report %s - must be one of %s", report, strings.Join(reports, ",")) + return fmt.Errorf("%w: unknown report %s - must be one of %s", errInvalidReport, report, strings.Join(reports, ",")) } if !(ctx.Bool("debug") || ctx.Bool("trace")) { @@ -55,21 +57,20 @@ func NewDescribeCommand() *cli.Command { product, err := config.ProductFromFile(ctx.String("config")) if err != nil { - return err + return fmt.Errorf("failed to load product config: %w", err) } err = product.Describe(ctx.Args().First()) - if err != nil { analytics.TrackEvent("Cluster Describe Failed", nil) - } else { - duration := time.Since(start) - props := event.Properties{ - "duration": duration.Seconds(), - } - analytics.TrackEvent("Cluster Describe Completed", props) + return fmt.Errorf("failed to describe cluster: %w", err) + } + duration := time.Since(start) + props := event.Properties{ + "duration": duration.Seconds(), } - return err + analytics.TrackEvent("Cluster Describe Completed", props) + return nil }, } } diff --git a/cmd/download_launchpad.go b/cmd/download_launchpad.go index f91f1cdfd..fd6fcfe74 100644 --- a/cmd/download_launchpad.go +++ b/cmd/download_launchpad.go @@ -13,30 +13,31 @@ import ( "github.com/urfave/cli/v2" ) +var errNoDownloadAvailable = fmt.Errorf("no download available") + // NewDownloadLaunchpadCommand creates new 'download-launchpad' command to be called from cli. func NewDownloadLaunchpadCommand() *cli.Command { return &cli.Command{ Name: "download-launchpad", Usage: "Download the latest launchpad version", - Action: func(ctx *cli.Context) error { + Action: func(_ *cli.Context) error { latest := version.GetLatest(time.Second * 20) if !latest.IsNewer() { - return fmt.Errorf("No upgrade available") + return fmt.Errorf("%w: upgrade not available", errNoDownloadAvailable) } asset := latest.AssetForHost() if asset == nil { - return fmt.Errorf("No download available for the current host OS + architecture") + return fmt.Errorf("%w: no download available for the current host OS + architecture", errNoDownloadAvailable) } - req, err := http.NewRequest("GET", asset.URL, nil) + req, err := http.NewRequest(http.MethodGet, asset.URL, nil) if err != nil { - return err + return fmt.Errorf("failed to create download request: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return fmt.Errorf("failed to perform download request: %w", err) } - defer resp.Body.Close() var ext string @@ -44,9 +45,9 @@ func NewDownloadLaunchpadCommand() *cli.Command { ext = ".exe" } - f, err := os.OpenFile(fmt.Sprintf("launchpad%s", ext), os.O_CREATE|os.O_WRONLY, 0755) + f, err := os.OpenFile(fmt.Sprintf("launchpad%s", ext), os.O_CREATE|os.O_WRONLY, 0o755) if err != nil { - return err + return fmt.Errorf("failed to create file: %w", err) } defer f.Close() @@ -54,7 +55,10 @@ func NewDownloadLaunchpadCommand() *cli.Command { resp.ContentLength, "Downloading", ) - io.Copy(io.MultiWriter(f, bar), resp.Body) + _, err = io.Copy(io.MultiWriter(f, bar), resp.Body) + if err != nil { + return fmt.Errorf("failed to download: %w", err) + } return nil }, } diff --git a/cmd/exec.go b/cmd/exec.go index 6748d0be9..937115252 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/Mirantis/mcc/pkg/config" "github.com/kballard/go-shellquote" "github.com/urfave/cli/v2" @@ -56,12 +58,16 @@ func NewExecCommand() *cli.Command { Action: func(ctx *cli.Context) error { product, err := config.ProductFromFile(ctx.String("config")) if err != nil { - return err + return fmt.Errorf("failed to load product configuration: %w", err) } args := ctx.Args().Slice() - return product.Exec(ctx.StringSlice("target"), ctx.Bool("interactive"), ctx.Bool("first"), ctx.Bool("all"), ctx.Bool("parallel"), ctx.String("role"), ctx.String("os"), shellquote.Join(args...)) + err = product.Exec(ctx.StringSlice("target"), ctx.Bool("interactive"), ctx.Bool("first"), ctx.Bool("all"), ctx.Bool("parallel"), ctx.String("role"), ctx.String("os"), shellquote.Join(args...)) + if err != nil { + return fmt.Errorf("failed to execute command: %w", err) + } + return nil }, } } diff --git a/cmd/init.go b/cmd/init.go index 6b3cbbda3..6de4d0ee0 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,15 +1,15 @@ package cmd import ( + "errors" "fmt" "os" "strings" - "gopkg.in/yaml.v2" - "github.com/Mirantis/mcc/pkg/analytics" "github.com/Mirantis/mcc/pkg/config" "github.com/urfave/cli/v2" + "gopkg.in/yaml.v2" ) var kinds = []string{"mke", "mke+msr"} @@ -23,6 +23,8 @@ func kindIsKnown(n string) bool { return false } +var errUnknownConfigKind = errors.New("unknown config kind") + // NewInitCommand creates new init command to be called from cli. func NewInitCommand() *cli.Command { return &cli.Command{ @@ -44,24 +46,21 @@ func NewInitCommand() *cli.Command { Action: func(ctx *cli.Context) error { kind := ctx.String("kind") if !kindIsKnown(kind) { - return fmt.Errorf("unknown kind %s - must be one of %s", kind, strings.Join(kinds, ",")) + return fmt.Errorf("%w: unknown kind %s - must be one of %s", errUnknownConfigKind, kind, strings.Join(kinds, ",")) } analytics.TrackEvent("Cluster Init Started", nil) cfg, err := config.Init(kind) if err != nil { - return err + return fmt.Errorf("failed to initialize config: %w", err) } encoder := yaml.NewEncoder(os.Stdout) - err = encoder.Encode(cfg) - - if err != nil { - analytics.TrackEvent("Cluster Init Failed", nil) - } else { - analytics.TrackEvent("Cluster Init Completed", nil) + if err = encoder.Encode(cfg); err != nil { + return fmt.Errorf("failed to encode config: %w", err) } - return err + analytics.TrackEvent("Cluster Init Completed", nil) + return nil }, } } diff --git a/cmd/register.go b/cmd/register.go index 0ad6b8063..25600ca24 100644 --- a/cmd/register.go +++ b/cmd/register.go @@ -1,6 +1,9 @@ package cmd import ( + "errors" + "fmt" + "github.com/AlecAivazis/survey/v2/terminal" "github.com/Mirantis/mcc/pkg/analytics" "github.com/Mirantis/mcc/pkg/cmd/register" @@ -49,15 +52,21 @@ func RegisterCommand() *cli.Command { Eula: ctx.Bool("accept-license"), } err := register.Register(userConfig) - if err == terminal.InterruptErr { - analytics.TrackEvent("User Register Cancelled", nil) + if err != nil { + switch { + case errors.Is(err, register.ErrEULADeclined): + analytics.TrackEvent("User Register Declined", nil) + case errors.Is(err, terminal.InterruptErr): + analytics.TrackEvent("User Register Cancelled", nil) + default: + analytics.TrackEvent("User Register Failed", nil) + return fmt.Errorf("failed to register user: %w", err) + } return nil - } else if err != nil { - analytics.TrackEvent("User Register Failed", nil) - } else { - analytics.TrackEvent("User Register Completed", nil) } - return err + + analytics.TrackEvent("User Register Completed", nil) + return nil }, } } diff --git a/cmd/reset.go b/cmd/reset.go index 38075abe1..1f4749d83 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "os" "time" @@ -35,37 +36,41 @@ func NewResetCommand() *cli.Command { analytics.TrackEvent("Cluster Reset Started", nil) product, err := config.ProductFromFile(ctx.String("config")) if err != nil { - return err + return fmt.Errorf("failed to load product config: %w", err) } err = product.Reset() - if err != nil { analytics.TrackEvent("Cluster Reset Failed", nil) - } else { - duration := time.Since(start) - props := event.Properties{ - "duration": duration.Seconds(), - } - analytics.TrackEvent("Cluster Reset Completed", props) + return fmt.Errorf("failed to reset cluster: %w", err) + } + + duration := time.Since(start) + props := event.Properties{ + "duration": duration.Seconds(), } - return err + analytics.TrackEvent("Cluster Reset Completed", props) + return nil }, } } +var errForceRequired = errors.New("confirmation or --force required") + func requireForce(ctx *cli.Context) error { if !ctx.Bool("force") { if !isatty.IsTerminal(os.Stdout.Fd()) { - return fmt.Errorf("reset requires --force") + return fmt.Errorf("%w: reset requires --force", errForceRequired) } confirmed := false prompt := &survey.Confirm{ Message: "Going to reset all of the hosts, which will destroy all configuration and data, Are you sure?", } - survey.AskOne(prompt, &confirmed) + if err := survey.AskOne(prompt, &confirmed); err != nil { + return fmt.Errorf("failed to ask for confirmation: %w", err) + } if !confirmed { - return fmt.Errorf("Confirmation or --force required to proceed") + return errForceRequired } } return nil diff --git a/main.go b/main.go index b04d2943e..4b6d109fb 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "os" "path" @@ -9,7 +10,6 @@ import ( "github.com/Mirantis/mcc/pkg/completion" "github.com/Mirantis/mcc/version" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) @@ -17,6 +17,8 @@ func init() { log.SetOutput(os.Stdout) } +var errUnsupportedShell = errors.New("unsupported shell") + func main() { versionCmd := &cli.Command{ Name: "version", @@ -54,11 +56,11 @@ func main() { case "fish": t, err := ctx.App.ToFishCompletion() if err != nil { - return err + return fmt.Errorf("failed to generate fish completion: %w", err) } fmt.Print(t) default: - return fmt.Errorf("no completion script available for %s", ctx.String("shell")) + return fmt.Errorf("%w: no completion script available for %s", errUnsupportedShell, ctx.String("shell")) } return nil diff --git a/pkg/analytics/analytics.go b/pkg/analytics/analytics.go index 856bd9fcf..65503d093 100644 --- a/pkg/analytics/analytics.go +++ b/pkg/analytics/analytics.go @@ -1,6 +1,7 @@ package analytics import ( + "fmt" "io" logger "log" "runtime" @@ -14,9 +15,9 @@ import ( const ( // ProdSegmentToken is the API token we use for Segment in production. - ProdSegmentToken = "FlDwKhRvN6ts7GMZEgoCEghffy9HXu8Z" + ProdSegmentToken = "FlDwKhRvN6ts7GMZEgoCEghffy9HXu8Z" //nolint:gosec // intentionally public // DevSegmentToken is the API token we use for Segment in development. - DevSegmentToken = "DLJn53HXEhUHZ4fPO45MMUhvbHRcfkLE" + DevSegmentToken = "DLJn53HXEhUHZ4fPO45MMUhvbHRcfkLE" //nolint:gosec // intentionally public ) // Analytics is the interface used for our analytics client. @@ -50,13 +51,17 @@ func init() { } // NewSegmentClient returns a Segment client for uploading analytics data. -func NewSegmentClient(segmentToken string) (Analytics, error) { +func NewSegmentClient(segmentToken string) (Analytics, error) { //nolint:ireturn segmentLogger := analytics.StdLogger(logger.New(io.Discard, "segment ", logger.LstdFlags)) segmentConfig := analytics.Config{ Logger: segmentLogger, } - return analytics.NewWithConfig(segmentToken, segmentConfig) + client, err := analytics.NewWithConfig(segmentToken, segmentConfig) + if err != nil { + return nil, fmt.Errorf("failed to initialize segment client: %w", err) + } + return client, nil } // TrackEvent uploads the given event to segment if analytics tracking @@ -75,12 +80,15 @@ func (c *Client) TrackEvent(event string, properties analytics.Properties) error properties["os"] = runtime.GOOS properties["version"] = version.Version - return c.AnalyticsClient.Enqueue(analytics.Track{ + if err := c.AnalyticsClient.Enqueue(analytics.Track{ UserId: UserID(), AnonymousId: MachineID(), Event: event, Properties: properties, - }) + }); err != nil { + return fmt.Errorf("failed to enqueue analytics message: %w", err) + } + return nil } // IdentifyUser identifies user on analytics service if analytics @@ -99,12 +107,17 @@ func (c *Client) IdentifyUser(userConfig *user.Config) error { Set("company", userConfig.Company), } log.Debugf("identified analytics user %+v", msg) - return c.AnalyticsClient.Enqueue(msg) + if err := c.AnalyticsClient.Enqueue(msg); err != nil { + return fmt.Errorf("failed to enqueue analytics message: %w", err) + } + return nil } // TrackEvent uses the default analytics client to track an event. -func TrackEvent(event string, properties map[string]interface{}) error { - return defaultClient.TrackEvent(event, properties) +func TrackEvent(event string, properties map[string]interface{}) { + if err := defaultClient.TrackEvent(event, properties); err != nil { + log.Debugf("failed to track event '%s': %v", event, err) + } } // IdentifyUser uses the default analytics client to identify the user. @@ -120,7 +133,9 @@ func RequireRegisteredUser() error { // Close closes the default analytics client. func Close() error { if defaultClient.AnalyticsClient != nil { - return defaultClient.AnalyticsClient.Close() + if err := defaultClient.AnalyticsClient.Close(); err != nil { + return fmt.Errorf("failed to close analytics client: %w", err) + } } return nil } diff --git a/pkg/analytics/user.go b/pkg/analytics/user.go index 180cd6ec9..a32dd2c27 100644 --- a/pkg/analytics/user.go +++ b/pkg/analytics/user.go @@ -6,11 +6,13 @@ import ( "github.com/Mirantis/mcc/pkg/config/user" ) +var errRegistrationRequired = errors.New("registration or license acceptance is required. please use `launchpad register` command to register") + // RequireRegisteredUser checks if user has registered. func (c *Client) RequireRegisteredUser() error { if _, err := user.GetConfig(); err != nil { - c.TrackEvent("User Not Registered", nil) - return errors.New("Registration or license acceptance is required. Please use `launchpad register` command to register") + _ = c.TrackEvent("User Not Registered", nil) + return errRegistrationRequired } return nil diff --git a/pkg/cmd/register/register.go b/pkg/cmd/register/register.go index 74158fed1..0066ca5c4 100644 --- a/pkg/cmd/register/register.go +++ b/pkg/cmd/register/register.go @@ -2,6 +2,7 @@ package register import ( "errors" + "fmt" "regexp" "github.com/AlecAivazis/survey/v2" @@ -10,6 +11,12 @@ import ( log "github.com/sirupsen/logrus" ) +var ( + // ErrEULADeclined is an error returned when user declines EULA. + ErrEULADeclined = errors.New("EULA declined") + errEULA = errors.New("EULA check failed") +) + // Register ... func Register(userConfig *user.Config) error { icons := func(icons *survey.IconSet) { @@ -19,21 +26,21 @@ func Register(userConfig *user.Config) error { if validateName(userConfig.Name) != nil { err := survey.AskOne(&survey.Input{Message: "Name"}, &userConfig.Name, survey.WithValidator(validateName), survey.WithIcons(icons)) if err != nil { - return err + return fmt.Errorf("registration failed: %w", err) } } if validateEmail(userConfig.Email) != nil { err := survey.AskOne(&survey.Input{Message: "Email"}, &userConfig.Email, survey.WithValidator(validateEmail), survey.WithIcons(icons)) if err != nil { - return err + return fmt.Errorf("registration failed: %w", err) } } if userConfig.Company == "" { err := survey.AskOne(&survey.Input{Message: "Company"}, &userConfig.Company, survey.WithIcons(icons)) if err != nil { - return err + return fmt.Errorf("registration failed: %w", err) } } @@ -44,36 +51,38 @@ func Register(userConfig *user.Config) error { } err := survey.AskOne(prompt, &userConfig.Eula, survey.WithIcons(icons)) if err != nil { - return err + return fmt.Errorf("registration failed: %w", err) } } if !userConfig.Eula { - return errors.New("You must agree to Mirantis Launchpad Software Evaluation License Agreement before you can use the tool") + return fmt.Errorf("%w: you must agree to Mirantis Launchpad Software Evaluation License Agreement before you can use the tool", errEULA) } err := user.SaveConfig(userConfig) - if err == nil { - analytics.IdentifyUser(userConfig) - log.Info("Registration completed!") - } else { + if err != nil { log.Error("Registration failed!") + return fmt.Errorf("saving registration failed: %w", err) } - return err + _ = analytics.IdentifyUser(userConfig) + log.Info("Registration completed!") + return nil } func validateName(val interface{}) error { - if len(val.(string)) < 2 { - return errors.New("Name must have more than 2 characters") + valStr, ok := val.(string) + if !ok || len(valStr) < 2 { + return fmt.Errorf("%w: name must be at least 2 characters long", errEULA) } return nil } func validateEmail(val interface{}) error { rxEmail := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + valStr, ok := val.(string) - if len(val.(string)) > 254 || !rxEmail.MatchString(val.(string)) { - return errors.New("Email is not a valid email address") + if !ok || len(valStr) > 254 || !rxEmail.MatchString(valStr) { + return fmt.Errorf("%w: email is not a valid email address", errEULA) } return nil } diff --git a/pkg/completion/completion.go b/pkg/completion/completion.go index 8735f3ae2..70a4fe375 100644 --- a/pkg/completion/completion.go +++ b/pkg/completion/completion.go @@ -40,8 +40,7 @@ complete -o bashdefault -o default -o nospace -F _launchpad_bash_autocomplete %s // ZshTemplate returns a completion script for zsh. func ZshTemplate() string { - p := prog() - return fmt.Sprintf(`#compdef %s + return fmt.Sprintf(`#compdef %[1]s _launchpad_zsh_autocomplete() { local -a opts @@ -62,6 +61,8 @@ _launchpad_zsh_autocomplete() { return } -compdef _launchpad_zsh_autocomplete %s -`, p, p) +compdef _launchpad_zsh_autocomplete %[1]s +`, + prog(), + ) } diff --git a/pkg/config/config.go b/pkg/config/config.go index eb1ce74f7..e7d5d7994 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,6 +1,7 @@ package config import ( + "errors" "fmt" "io" "os" @@ -8,18 +9,7 @@ import ( "regexp" "strings" - "github.com/a8m/envsubst" - "gopkg.in/yaml.v2" - "github.com/Mirantis/mcc/pkg/config/migration" - "github.com/k0sproject/rig/exec" - - // needed to load the migrators. - _ "github.com/Mirantis/mcc/pkg/config/migration/v1beta1" - // needed to load the migrators. - _ "github.com/Mirantis/mcc/pkg/config/migration/v1beta2" - // needed to load the migrators. - _ "github.com/Mirantis/mcc/pkg/config/migration/v1beta3" // needed to load the migrators. _ "github.com/Mirantis/mcc/pkg/config/migration/v1" // needed to load the migrators. @@ -28,13 +18,22 @@ import ( _ "github.com/Mirantis/mcc/pkg/config/migration/v12" // needed to load the migrators. _ "github.com/Mirantis/mcc/pkg/config/migration/v13" + // needed to load the migrators. + _ "github.com/Mirantis/mcc/pkg/config/migration/v1beta1" + // needed to load the migrators. + _ "github.com/Mirantis/mcc/pkg/config/migration/v1beta2" + // needed to load the migrators. + _ "github.com/Mirantis/mcc/pkg/config/migration/v1beta3" "github.com/Mirantis/mcc/pkg/product" "github.com/Mirantis/mcc/pkg/product/mke" + "github.com/a8m/envsubst" + "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" ) // ProductFromFile loads a yaml file and returns a Product that matches its Kind or an error if the file loading or validation fails. -func ProductFromFile(path string) (product.Product, error) { +func ProductFromFile(path string) (product.Product, error) { //nolint:ireturn data, err := resolveClusterFile(path) if err != nil { return nil, err @@ -42,29 +41,31 @@ func ProductFromFile(path string) (product.Product, error) { return ProductFromYAML(data) } +var errMissingKind = errors.New("configuration does not contain the required keyword 'kind'") + // ProductFromYAML returns a Product from YAML bytes, or an error. -func ProductFromYAML(data []byte) (product.Product, error) { - c := make(map[string]interface{}) - if err := yaml.Unmarshal(data, c); err != nil { - return nil, err +func ProductFromYAML(data []byte) (product.Product, error) { //nolint:ireturn + config := make(map[string]interface{}) + if err := yaml.Unmarshal(data, config); err != nil { + return nil, fmt.Errorf("failed to unmarshal configuration: %w", err) } - if err := migration.Migrate(c); err != nil { - return nil, err + if err := migration.Migrate(config); err != nil { + return nil, fmt.Errorf("failed to migrate configuration: %w", err) } - if c["kind"] == nil { - return nil, fmt.Errorf("configuration does not contain the required keyword 'kind'") + if config["kind"] == nil { + return nil, errMissingKind } - data, err := yaml.Marshal(c) + data, err := yaml.Marshal(config) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to marshal configuration: %w", err) } plain, err := envsubst.Bytes(data) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to substitute environment variables: %w", err) } cfg := string(plain) @@ -75,49 +76,74 @@ func ProductFromYAML(data []byte) (product.Product, error) { log.Debugf("loaded configuration:\n%s", cfg) - switch c["kind"].(string) { + kindStr, ok := config["kind"].(string) + if !ok { + return nil, errMissingKind + } + switch kindStr { case "mke", "mke+msr": - return mke.NewMKE(plain) + mke, err := mke.NewMKE(plain) + if err != nil { + return nil, fmt.Errorf("failed to parse MKE configuration: %w", err) + } + return mke, nil default: - return nil, fmt.Errorf("unknown configuration kind '%s'", c["kind"].(string)) + return nil, fmt.Errorf("%w: %s", errUnknownConfigKind, kindStr) } } +var errUnknownConfigKind = errors.New("unknown configuration kind") + // Init returns an example cluster configuration. func Init(kind string) (interface{}, error) { switch kind { case "mke", "mke+msr": return mke.Init(kind), nil default: - return "", fmt.Errorf("unknown configuration kind '%s'", kind) + return "", fmt.Errorf("%w: %s", errUnknownConfigKind, kind) } } +var errIsCharDevice = errors.New("is a character device") + func resolveClusterFile(clusterFile string) ([]byte, error) { if clusterFile == "-" { stat, err := os.Stdin.Stat() - if err == nil { - if (stat.Mode() & os.ModeCharDevice) == 0 { - return io.ReadAll(os.Stdin) + if err != nil { + return nil, fmt.Errorf("can't open cluster configuration from stdin: stat: %w", err) + } + if (stat.Mode() & os.ModeCharDevice) == 0 { + data, err := io.ReadAll(os.Stdin) + if err != nil { + return nil, fmt.Errorf("error while reading cluster configuration from stdin: %w", err) } + return data, nil } - return nil, fmt.Errorf("can't open cluster configuration from stdin") + return nil, fmt.Errorf("can't read from stdin: %w", errIsCharDevice) } file, err := openClusterFile(clusterFile) - defer file.Close() //nolint:staticcheck if err != nil { return nil, err } + defer func() { + _ = file.Close() + }() - return io.ReadAll(file) + data, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("error while reading cluster file %s: %w", clusterFile, err) + } + return data, nil } +var errEmptyFileName = errors.New("empty file name") + func openClusterFile(clusterFile string) (*os.File, error) { clusterFileName := detectClusterFile(clusterFile) if clusterFileName == "" { - return nil, fmt.Errorf("can't find cluster configuration file %s", clusterFile) + return nil, fmt.Errorf("can't find cluster configuration file: %w", errEmptyFileName) } file, fp, err := openFile(clusterFileName) @@ -153,11 +179,11 @@ func detectClusterFile(clusterFile string) string { func openFile(fileName string) (file *os.File, path string, err error) { fp, err := filepath.Abs(fileName) if err != nil { - return nil, "", fmt.Errorf("failed to lookup current directory name: %v", err) + return nil, "", fmt.Errorf("failed to lookup current directory name: %w", err) } file, err = os.Open(fp) if err != nil { - return nil, fp, fmt.Errorf("can not find cluster configuration file: %v", err) + return nil, fp, fmt.Errorf("can not find cluster configuration file: %w", err) } return file, fp, nil } diff --git a/pkg/config/migration/migration.go b/pkg/config/migration/migration.go index 268a4d3ee..fc9507efb 100644 --- a/pkg/config/migration/migration.go +++ b/pkg/config/migration/migration.go @@ -15,8 +15,8 @@ func Register(apiVersion string, migrator func(map[string]interface{}) error) { // Migrate will run through the migrations until there is no more migrators found and returns an error if any of the migrations fail. func Migrate(data map[string]interface{}) error { for { - migrator := migrators[data["apiVersion"].(string)] - if migrator == nil { + migrator, ok := migrators[data["apiVersion"].(string)] + if migrator == nil || !ok { return nil } diff --git a/pkg/config/migration/v1/v1.go b/pkg/config/migration/v1/v1.go index 8ffc286db..9313f7c78 100644 --- a/pkg/config/migration/v1/v1.go +++ b/pkg/config/migration/v1/v1.go @@ -1,6 +1,7 @@ package v1 import ( + "fmt" "regexp" "strings" @@ -10,7 +11,7 @@ import ( ) // Migrate migrates an v1 format configuration into the v1.1 api format and replaces the contents of the supplied data byte slice. -func Migrate(plain map[string]interface{}) error { +func Migrate(plain map[string]interface{}) error { //nolint:maintidx plain["apiVersion"] = "launchpad.mirantis.com/mke/v1.1" // Need to marshal back to yaml to find $VARIABLES. @@ -23,24 +24,28 @@ func Migrate(plain map[string]interface{}) error { varsFound = true } if varsFound { - yaml.Unmarshal(re.ReplaceAll(s, []byte("$1$$$2")), plain) + if err := yaml.Unmarshal(re.ReplaceAll(s, []byte("$1$$$2")), plain); err != nil { + return fmt.Errorf("failed to escape variables: %w", err) + } } - var hasMsr = false + hasMsr := false // It gets ugly - scan for the admin username/pass in ucp/dtr installFlags and move over to ucp.username + ucp.password spec, ok := plain["spec"].(map[interface{}]interface{}) if ok { - hosts, ok := spec["hosts"] + hosts, ok := spec["hosts"].([]interface{}) if ok { - hslice := hosts.([]interface{}) - for _, h := range hslice { - host := h.(map[interface{}]interface{}) - if host["role"].(string) == "dtr" { - host["role"] = "msr" - log.Debugf("changed v1 host.role[dtr] to v1.1 host.role[msr]") - hasMsr = true + for _, h := range hosts { + host, ok := h.(map[interface{}]interface{}) + if ok { + role, ok := host["role"].(string) + if ok && role == "dtr" { + host["role"] = "msr" + log.Debugf("changed v1 host.role[dtr] to v1.1 host.role[msr]") + hasMsr = true + } } } } @@ -51,8 +56,9 @@ func Migrate(plain map[string]interface{}) error { if ok { drop := -1 for idx, val := range installFlags { - if strings.HasPrefix(val.(string), "--admin-username") { - user := val.(string)[strings.IndexAny(val.(string), `=" `)+1:] + valStr, ok := val.(string) + if ok && strings.HasPrefix(valStr, "--admin-username") { + user := valStr[strings.IndexAny(valStr, `=" `)+1:] user = strings.TrimSpace(user) user = strings.Trim(user, `"`) if user != "" { @@ -67,15 +73,18 @@ func Migrate(plain map[string]interface{}) error { } drop = -1 - installFlags = ucp["installFlags"].([]interface{}) - for idx, val := range installFlags { - if strings.HasPrefix(val.(string), "--admin-password") { - pass := val.(string)[strings.IndexAny(val.(string), `=" `)+1:] - pass = strings.TrimSpace(pass) - pass = strings.Trim(pass, `"`) - if pass != "" { - ucp["adminPassword"] = pass - drop = idx + installFlags, ok = ucp["installFlags"].([]interface{}) + if ok { + for idx, val := range installFlags { + valStr, ok := val.(string) + if ok && strings.HasPrefix(valStr, "--admin-password") { + pass := valStr[strings.IndexAny(valStr, `=" `)+1:] + pass = strings.TrimSpace(pass) + pass = strings.Trim(pass, `"`) + if pass != "" { + ucp["adminPassword"] = pass + drop = idx + } } } } @@ -97,19 +106,23 @@ func Migrate(plain map[string]interface{}) error { if ok { drop := -1 for idx, val := range installFlags { - if strings.HasPrefix(val.(string), "--ucp-username") { - user := val.(string)[strings.IndexAny(val.(string), `=" `):] + valStr, ok := val.(string) + if ok && strings.HasPrefix(valStr, "--ucp-username") { + user := valStr[strings.IndexAny(valStr, `=" `):] user = strings.TrimSpace(user) user = strings.Trim(user, `"`) if user != "" { - if spec["mke"] == nil { - spec["mke"] = make(map[interface{}]interface{}) - spec["mke"].(map[interface{}]interface{})["adminUsername"] = user - drop = idx - } else if spec["mke"].(map[interface{}]interface{})["adminUsername"] == nil { - spec["mke"].(map[interface{}]interface{})["adminUsername"] = user + mkeMap, ok := spec["mke"].(map[interface{}]interface{}) + if !ok { + mkeMap = make(map[interface{}]interface{}) + spec["mke"] = mkeMap + } + + switch { + case mkeMap["adminUsername"] == nil: + mkeMap["adminUsername"] = user drop = idx - } else if spec["mke"].(map[interface{}]interface{})["adminUsername"] != user { + case mkeMap["adminUsername"] != user: log.Warnf("spec.dtr.installFlags[--ucp-username] and spec.mke.adminUsername mismatch") } } @@ -121,22 +134,31 @@ func Migrate(plain map[string]interface{}) error { } drop = -1 - installFlags = dtr["installFlags"].([]interface{}) - for idx, val := range installFlags { - if strings.HasPrefix(val.(string), "--ucp-password") { - pass := val.(string)[strings.IndexAny(val.(string), `=" `)+1:] - pass = strings.TrimSpace(pass) - pass = strings.Trim(pass, `"`) - if pass != "" { - if spec["mke"] == nil { - spec["mke"] = make(map[interface{}]interface{}) - spec["mke"].(map[interface{}]interface{})["adminPassword"] = pass - drop = idx - } else if spec["mke"].(map[interface{}]interface{})["adminPassword"] == nil { - spec["mke"].(map[interface{}]interface{})["adminPassword"] = pass - drop = idx - } else if spec["mke"].(map[interface{}]interface{})["adminPassword"] != pass { - log.Warnf("spec.dtr.installFlags[--ucp-password] and spec.mke.adminPassword mismatch") + installFlags, ok = dtr["installFlags"].([]interface{}) + if ok { + for idx, val := range installFlags { + valStr, ok := val.(string) + if ok && strings.HasPrefix(valStr, "--ucp-password") { + pass := valStr[strings.IndexAny(valStr, `=" `)+1:] + pass = strings.TrimSpace(pass) + pass = strings.Trim(pass, `"`) + if pass != "" { + var specMke map[interface{}]interface{} + if s, ok := spec["mke"].(map[interface{}]interface{}); ok && s != nil { + specMke = s + } else { + specMke = make(map[interface{}]interface{}) + spec["mke"] = specMke + } + switch specMke["adminPassword"] { + case pass: + // do nothing + case nil: + specMke["adminPassword"] = pass + drop = idx + default: + log.Warnf("spec.dtr.installFlags[--ucp-password] and spec.mke.adminPassword mismatch") + } } } } @@ -159,7 +181,8 @@ func Migrate(plain map[string]interface{}) error { } } - if plain["kind"].(string) == "DockerEnterprise" { + kind, ok := plain["kind"].(string) + if ok && kind == "DockerEnterprise" { if hasMsr { plain["kind"] = "mke+msr" log.Debugf("migrated v1 kind[DockerEnterprise] to v1.1 kind[mke+msr]") diff --git a/pkg/config/migration/v11/v11.go b/pkg/config/migration/v11/v11.go index e4ce2bf26..dde89c9d4 100644 --- a/pkg/config/migration/v11/v11.go +++ b/pkg/config/migration/v11/v11.go @@ -9,15 +9,14 @@ import ( func Migrate(plain map[string]interface{}) error { plain["apiVersion"] = "launchpad.mirantis.com/mke/v1.2" - spec, ok := plain["spec"].(map[interface{}]interface{}) - if ok { - hosts, ok := spec["hosts"] - if ok { - hslice := hosts.([]interface{}) - for _, h := range hslice { - host := h.(map[interface{}]interface{}) - ec, ok := host["engineConfig"] - if ok { + if spec, ok := plain["spec"].(map[interface{}]interface{}); ok { + if hosts, ok := spec["hosts"].([]interface{}); ok { + for _, h := range hosts { + host, ok := h.(map[interface{}]interface{}) + if !ok { + continue + } + if ec, ok := host["engineConfig"]; ok { host["mcrConfig"] = ec delete(host, "engineConfig") log.Debugf("migrated v1.1 spec.hosts[*].engineConfig to v1.2 spec.hosts[*].mcrConfig") @@ -25,8 +24,7 @@ func Migrate(plain map[string]interface{}) error { } } - eng, ok := spec["engine"].(map[interface{}]interface{}) - if ok { + if eng, ok := spec["engine"].(map[interface{}]interface{}); ok { spec["mcr"] = eng delete(spec, "engine") log.Debugf("migrated v1.1 spec.engine to v1.2 spec.mcr") diff --git a/pkg/config/migration/v12/v12.go b/pkg/config/migration/v12/v12.go index ecb9df94f..5d207f4b0 100644 --- a/pkg/config/migration/v12/v12.go +++ b/pkg/config/migration/v12/v12.go @@ -10,9 +10,8 @@ func Migrate(plain map[string]interface{}) error { plain["apiVersion"] = "launchpad.mirantis.com/mke/v1.3" if spec, ok := plain["spec"].(map[interface{}]interface{}); ok { - if hosts, ok := spec["hosts"]; ok { - hslice := hosts.([]interface{}) - for _, h := range hslice { + if hosts, ok := spec["hosts"].([]interface{}); ok { + for _, h := range hosts { host, ok := h.(map[interface{}]interface{}) if ok { if addr, ok := host["address"].(string); ok { diff --git a/pkg/config/migration/v13/v13.go b/pkg/config/migration/v13/v13.go index e510350c3..d5c141fd8 100644 --- a/pkg/config/migration/v13/v13.go +++ b/pkg/config/migration/v13/v13.go @@ -29,9 +29,8 @@ func Migrate(plain map[string]interface{}) error { } else { kind, ok := plain["kind"].(string) if ok && kind != "mke+msr" { - if hosts, ok := spec["hosts"]; ok { - hslice := hosts.([]interface{}) - for _, h := range hslice { + if hosts, ok := spec["hosts"].([]interface{}); ok { + for _, h := range hosts { if host, ok := h.(map[interface{}]interface{}); ok { if role, ok := host["role"].(string); ok && role == "msr" { kind = "mke+msr" diff --git a/pkg/config/migration/v1beta1/v1beta1.go b/pkg/config/migration/v1beta1/v1beta1.go index 36d238818..d885d6ecf 100644 --- a/pkg/config/migration/v1beta1/v1beta1.go +++ b/pkg/config/migration/v1beta1/v1beta1.go @@ -9,30 +9,34 @@ import ( func Migrate(plain map[string]interface{}) error { plain["apiVersion"] = "launchpad.mirantis.com/v1beta2" - if plain["spec"] != nil { - hosts, ok := plain["spec"].(map[interface{}]interface{})["hosts"] - if ok { - hslice := hosts.([]interface{}) - - for _, h := range hslice { - host := h.(map[interface{}]interface{}) - host["ssh"] = make(map[string]interface{}) - ssh := host["ssh"].(map[string]interface{}) + if spec, ok := plain["spec"].(map[interface{}]interface{}); ok { + if hosts, ok := spec["hosts"].([]interface{}); ok { + for _, h := range hosts { + host, ok := h.(map[interface{}]interface{}) + if !ok { + continue + } + ssh := make(map[string]interface{}) + host["ssh"] = ssh - for k, v := range host { - switch k.(string) { + for key, val := range host { + keyStr, ok := key.(string) + if !ok { + continue + } + switch keyStr { case "sshKeyPath": - ssh["keyPath"] = v - delete(host, k) - log.Debugf("migrated v1beta1 host sshKeyPath '%s' to v1beta2 ssh[keyPath]", v) + ssh["keyPath"] = val + delete(host, key) + log.Debugf("migrated v1beta1 host sshKeyPath '%s' to v1beta2 ssh[keyPath]", val) case "sshPort": - ssh["port"] = v - delete(host, k) - log.Debugf("migrated v1beta1 host sshPort '%d' to v1beta2 ssh[port]", v) + ssh["port"] = val + delete(host, key) + log.Debugf("migrated v1beta1 host sshPort '%d' to v1beta2 ssh[port]", val) case "user": - ssh["user"] = v - delete(host, k) - log.Debugf("migrated v1beta1 host user '%s' to v1beta2 ssh[user]", v) + ssh["user"] = val + delete(host, key) + log.Debugf("migrated v1beta1 host user '%s' to v1beta2 ssh[user]", val) } } } diff --git a/pkg/config/migration/v1beta2/v1beta2.go b/pkg/config/migration/v1beta2/v1beta2.go index 4f7ee9f29..cfc53140b 100644 --- a/pkg/config/migration/v1beta2/v1beta2.go +++ b/pkg/config/migration/v1beta2/v1beta2.go @@ -17,8 +17,8 @@ func Migrate(plain map[string]interface{}) error { if plain["spec"] != nil { eint, ok := plain["spec"].(map[interface{}]interface{})["engine"] if ok { - engine := eint.(map[interface{}]interface{}) - if len(engine) > 0 { + engine, ok := eint.(map[interface{}]interface{}) + if ok && len(engine) > 0 { installURL := engine["installURL"] if installURL != nil { engine["installURLLinux"] = installURL diff --git a/pkg/config/migration/v1beta3/v1beta3.go b/pkg/config/migration/v1beta3/v1beta3.go index 8a732617d..a680e067a 100644 --- a/pkg/config/migration/v1beta3/v1beta3.go +++ b/pkg/config/migration/v1beta3/v1beta3.go @@ -8,7 +8,9 @@ import ( // Migrate migrates an v1beta3 format configuration into the v1 api format and replaces the contents of the supplied data byte slice. func Migrate(plain map[string]interface{}) error { plain["apiVersion"] = "launchpad.mirantis.com/v1" + log.Debugf("migrated configuration from v1beta3 to v1") + return nil } diff --git a/pkg/config/user/config.go b/pkg/config/user/config.go index 391359259..0a5fe9069 100644 --- a/pkg/config/user/config.go +++ b/pkg/config/user/config.go @@ -1,6 +1,7 @@ package user import ( + "fmt" "os" "path/filepath" @@ -25,14 +26,14 @@ type Config struct { func GetConfig() (*Config, error) { configFile, err := homedir.Expand(configFile) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to expand config file path: %w", err) } config := &Config{} // Open config file file, err := os.Open(configFile) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to open config file: %w", err) } defer file.Close() @@ -41,7 +42,7 @@ func GetConfig() (*Config, error) { // Start YAML decoding from file if err := d.Decode(&config); err != nil { - return nil, err + return nil, fmt.Errorf("failed to decode config file: %w", err) } return config, nil @@ -51,19 +52,19 @@ func GetConfig() (*Config, error) { func SaveConfig(config *Config) error { configFile, err := homedir.Expand(configFile) if err != nil { - return err + return fmt.Errorf("failed to expand config file path: %w", err) } configDir := filepath.Dir(configFile) if err = util.EnsureDir(configDir); err != nil { - return err + return fmt.Errorf("failed to ensure config dir: %w", err) } d, err := yaml.Marshal(&config) if err != nil { - return err + return fmt.Errorf("failed to marshal config: %w", err) } - err = os.WriteFile(configFile, d, 0644) + err = os.WriteFile(configFile, d, 0o600) if err != nil { - return err + return fmt.Errorf("failed to write config file: %w", err) } return nil } diff --git a/pkg/configurer/centos/centos.go b/pkg/configurer/centos/centos.go index 4379db83b..b6d764723 100644 --- a/pkg/configurer/centos/centos.go +++ b/pkg/configurer/centos/centos.go @@ -1,6 +1,8 @@ package centos import ( + "fmt" + "github.com/Mirantis/mcc/pkg/configurer/enterpriselinux" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os" @@ -14,7 +16,10 @@ type Configurer struct { // InstallMKEBasePackages install all the needed base packages on the host. func (c Configurer) InstallMKEBasePackages(h os.Host) error { - return c.InstallPackage(h, "curl", "socat", "iptables", "iputils", "gzip") + if err := c.InstallPackage(h, "curl", "socat", "iptables", "iputils", "gzip"); err != nil { + return fmt.Errorf("failed to install base packages: %w", err) + } + return nil } func init() { diff --git a/pkg/configurer/common.go b/pkg/configurer/common.go index 5021efdee..09a344b4f 100644 --- a/pkg/configurer/common.go +++ b/pkg/configurer/common.go @@ -2,6 +2,7 @@ package configurer import ( "encoding/json" + "errors" "fmt" common "github.com/Mirantis/mcc/pkg/product/common/api" @@ -11,30 +12,32 @@ import ( type DockerConfigurer struct{} -// GetDockerInfo gets docker info from the host -func (c DockerConfigurer) GetDockerInfo(h os.Host, hostKind string) (common.DockerInfo, error) { +// GetDockerInfo gets docker info from the host. +func (c DockerConfigurer) GetDockerInfo(h os.Host) (common.DockerInfo, error) { command := "docker info --format \"{{json . }}\"" log.Debugf("%s attempting to gather info with `%s`", h, command) info, err := h.ExecOutput(command) if err != nil { - log.Debugf("%s cmd `%s` failed with %s ", h, command, err) - return common.DockerInfo{}, err + log.Debugf("%s: cmd `%s` failed with %s ", h, command, err) + return common.DockerInfo{}, fmt.Errorf("failed to get docker info: %w", err) } var dockerInfo common.DockerInfo err = json.Unmarshal([]byte(info), &dockerInfo) if err != nil { log.Debugf("%s unmarshal failed of `%s` with %s ", h, command, err) - return common.DockerInfo{}, err + return common.DockerInfo{}, fmt.Errorf("failed to unmarshal docker info: %w", err) } return dockerInfo, nil } -// GetDockerDaemonConfig parses docker daemon json string and populate DockerDaemonConfig struct +var errConfigEmpty = errors.New("the docker daemon config is empty") + +// GetDockerDaemonConfig parses docker daemon json string and populate DockerDaemonConfig struct. func (c DockerConfigurer) GetDockerDaemonConfig(dockerDaemon string) (common.DockerDaemonConfig, error) { if dockerDaemon != "" { - return common.DockerDaemonConfig{}, fmt.Errorf("the docker daemon config is empty") + return common.DockerDaemonConfig{}, errConfigEmpty } var config common.DockerDaemonConfig diff --git a/pkg/configurer/enterpriselinux/el.go b/pkg/configurer/enterpriselinux/el.go index 3177a05aa..60c9b8dd0 100644 --- a/pkg/configurer/enterpriselinux/el.go +++ b/pkg/configurer/enterpriselinux/el.go @@ -1,9 +1,10 @@ package enterpriselinux import ( + "fmt" + "github.com/Mirantis/mcc/pkg/configurer" common "github.com/Mirantis/mcc/pkg/product/common/api" - "github.com/k0sproject/rig/os" "github.com/k0sproject/rig/os/linux" log "github.com/sirupsen/logrus" @@ -17,33 +18,37 @@ type Configurer struct { // InstallMKEBasePackages install all the needed base packages on the host. func (c Configurer) InstallMKEBasePackages(h os.Host) error { - return c.InstallPackage(h, "curl", "socat", "iptables", "iputils", "gzip") + if err := c.InstallPackage(h, "curl", "socat", "iptables", "iputils", "gzip"); err != nil { + return fmt.Errorf("failed to install base packages: %w", err) + } + return nil } // UninstallMCR uninstalls docker-ee engine. -func (c Configurer) UninstallMCR(h os.Host, scriptPath string, engineConfig common.MCRConfig) error { - var err error - info, getDockerError := c.GetDockerInfo(h, c.Kind()) +func (c Configurer) UninstallMCR(h os.Host, _ string, engineConfig common.MCRConfig) error { + info, getDockerError := c.GetDockerInfo(h) + if engineConfig.Prune { + defer c.CleanupLingeringMCR(h, info) + } if getDockerError == nil { - if err = h.Exec(c.DockerCommandf("system prune -f")); err != nil { - return err + if err := h.Exec("sudo docker system prune -f"); err != nil { + return fmt.Errorf("prune docker: %w", err) } - if err = c.StopService(h, "docker"); err != nil { - return err + if err := c.StopService(h, "docker"); err != nil { + return fmt.Errorf("stop docker: %w", err) } - if err = c.StopService(h, "containerd"); err != nil { - return err + if err := c.StopService(h, "containerd"); err != nil { + return fmt.Errorf("stop containerd: %w", err) } - err = h.Exec("sudo yum remove -y docker-ee docker-ee-cli") - } - if engineConfig.Prune { - c.CleanupLingeringMCR(h, info) + if err := h.Exec("sudo yum remove -y docker-ee docker-ee-cli"); err != nil { + return fmt.Errorf("remove docker-ee yum package: %w", err) + } } - return err + return nil } // InstallMCR install Docker EE engine on Linux. @@ -58,5 +63,8 @@ func (c Configurer) InstallMCR(h os.Host, scriptPath string, engineConfig common log.Infof("%s: enabled rhel-7-server-rhui-extras-rpms repository", h) } - return c.LinuxConfigurer.InstallMCR(h, scriptPath, engineConfig) + if err := c.LinuxConfigurer.InstallMCR(h, scriptPath, engineConfig); err != nil { + return fmt.Errorf("failed to install MCR: %w", err) + } + return nil } diff --git a/pkg/configurer/linux.go b/pkg/configurer/linux.go index 98b801982..87b4d47c6 100644 --- a/pkg/configurer/linux.go +++ b/pkg/configurer/linux.go @@ -1,6 +1,7 @@ package configurer import ( + "errors" "fmt" "path" "regexp" @@ -8,14 +9,12 @@ import ( "strings" "github.com/Mirantis/mcc/pkg/constant" + common "github.com/Mirantis/mcc/pkg/product/common/api" "github.com/Mirantis/mcc/pkg/util" + escape "github.com/alessio/shellescape" "github.com/k0sproject/rig/exec" "github.com/k0sproject/rig/os" - - common "github.com/Mirantis/mcc/pkg/product/common/api" log "github.com/sirupsen/logrus" - - escape "github.com/alessio/shellescape" ) // LinuxConfigurer is a generic linux host configurer. @@ -40,35 +39,46 @@ func (c LinuxConfigurer) InstallMCR(h os.Host, scriptPath string, engineConfig c err := h.Upload(scriptPath, installer) if err != nil { log.Errorf("failed: %s", err.Error()) - return err + return fmt.Errorf("upload %s to %s: %w", scriptPath, installer, err) } - defer c.riglinux.DeleteFile(h, installer) + defer func() { + if err := c.riglinux.DeleteFile(h, installer); err != nil { + log.Warnf("failed to delete installer script: %s", err.Error()) + } + }() cmd := fmt.Sprintf("DOCKER_URL=%s CHANNEL=%s VERSION=%s bash %s", engineConfig.RepoURL, engineConfig.Channel, engineConfig.Version, escape.Quote(installer)) log.Infof("%s: running installer", h) if err := h.Exec(cmd); err != nil { - return err + return fmt.Errorf("run MCR installer: %w", err) } if err := c.riglinux.EnableService(h, "docker"); err != nil { - return err + return fmt.Errorf("enable docker service: %w", err) + } + + if err := c.riglinux.StartService(h, "docker"); err != nil { + return fmt.Errorf("start docker service: %w", err) } - return c.riglinux.StartService(h, "docker") + return nil } // RestartMCR restarts Docker EE engine. func (c LinuxConfigurer) RestartMCR(h os.Host) error { - return c.riglinux.RestartService(h, "docker") + if err := c.riglinux.RestartService(h, "docker"); err != nil { + return fmt.Errorf("restart docker service: %w", err) + } + return nil } // ResolveInternalIP resolves internal ip from private interface. func (c LinuxConfigurer) ResolveInternalIP(h os.Host, privateInterface, publicIP string) (string, error) { output, err := h.ExecOutput(fmt.Sprintf("%s ip -o addr show dev %s scope global", SbinPath, privateInterface)) if err != nil { - return "", fmt.Errorf("failed to find private interface with name %s: %s. Make sure you've set correct 'privateInterface' for the host in config", privateInterface, output) + return "", fmt.Errorf("%w: failed to find private interface with name %s: %s. Make sure you've set correct 'privateInterface' for the host in config", err, privateInterface, output) } lines := strings.Split(output, "\n") @@ -78,7 +88,14 @@ func (c LinuxConfigurer) ResolveInternalIP(h os.Host, privateInterface, publicIP log.Debugf("not enough items in ip address line (%s), skipping...", items) continue } - addr := items[3][:strings.Index(items[3], "/")] + + idx := strings.Index(items[3], "/") + if idx == -1 { + log.Debugf("no CIDR mask in ip address line (%s), skipping...", items) + continue + } + addr := items[3][:idx] + if addr != publicIP { log.Infof("%s: using %s as private IP", h, addr) if util.IsValidAddress(addr) { @@ -101,15 +118,15 @@ func (c LinuxConfigurer) DockerCommandf(template string, args ...interface{}) st // ValidateLocalhost returns an error if "localhost" is not a local address. func (c LinuxConfigurer) ValidateLocalhost(h os.Host) error { if err := h.Exec("sudo ping -c 1 -w 1 -r localhost"); err != nil { - return fmt.Errorf("hostname 'localhost' does not resolve to an address local to the host") + return fmt.Errorf("hostname 'localhost' does not resolve to an address local to the host: %w", err) } return nil } // CheckPrivilege returns an error if the user does not have passwordless sudo enabled. func (c LinuxConfigurer) CheckPrivilege(h os.Host) error { - if h.Exec("sudo -n true") != nil { - return fmt.Errorf("user does not have passwordless sudo access") + if err := h.Exec("sudo -n true"); err != nil { + return fmt.Errorf("user does not have passwordless sudo access: %w", err) } return nil @@ -119,7 +136,7 @@ func (c LinuxConfigurer) CheckPrivilege(h os.Host) error { func (c LinuxConfigurer) LocalAddresses(h os.Host) ([]string, error) { output, err := h.ExecOutput("sudo hostname --all-ip-addresses") if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get local addresses: %w", err) } return strings.Split(output, " "), nil @@ -152,7 +169,7 @@ func (c LinuxConfigurer) AuthorizeDocker(h os.Host) error { } if err := h.Exec("sudo -i usermod -aG docker $USER"); err != nil { - return err + return fmt.Errorf("failed to add the current user to the 'docker' group: %w", err) } log.Warnf("%s: added the current user to the 'docker' group", h) @@ -160,12 +177,12 @@ func (c LinuxConfigurer) AuthorizeDocker(h os.Host) error { if h, ok := h.(reconnectable); ok { log.Infof("%s: reconnecting", h) if err := h.Reconnect(); err != nil { - return fmt.Errorf("failed to reconnect: %s", err.Error()) + return fmt.Errorf("failed to reconnect: %w", err) } } if err := h.Exec("groups | grep -q docker"); err != nil { - return fmt.Errorf("user is not in the 'docker' group") + return fmt.Errorf("user is not in the 'docker' group: %w", err) } return nil @@ -173,7 +190,10 @@ func (c LinuxConfigurer) AuthorizeDocker(h os.Host) error { // AuthenticateDocker performs a docker login on the host. func (c LinuxConfigurer) AuthenticateDocker(h os.Host, user, pass, imageRepo string) error { - return h.Exec(c.DockerCommandf("login -u %s --password-stdin %s", user, imageRepo), exec.Stdin(pass), exec.RedactString(user, pass)) + if err := h.Exec(c.DockerCommandf("login -u %s --password-stdin %s", user, imageRepo), exec.Stdin(pass), exec.RedactString(user, pass)); err != nil { + return fmt.Errorf("failed to login to the docker registry: %w", err) + } + return nil } // LineIntoFile tries to find a matching line in a file and replace it with a new entry @@ -182,11 +202,14 @@ func (c LinuxConfigurer) LineIntoFile(h os.Host, path, matcher, newLine string) if c.riglinux.FileExist(h, path) { err := h.Exec(fmt.Sprintf(`file=%s; match=%s; line=%s; sudo grep -q "${match}" "$file" && sudo sed -i "/${match}/c ${line}" "$file" || (echo "$line" | sudo tee -a "$file" > /dev/null)`, escape.Quote(path), escape.Quote(matcher), escape.Quote(newLine))) if err != nil { - return err + return fmt.Errorf("failed to update %s: %w", path, err) } return nil } - return c.riglinux.WriteFile(h, path, newLine, "0700") + if err := c.riglinux.WriteFile(h, path, newLine, "0600"); err != nil { + return fmt.Errorf("failed to create %s: %w", path, err) + } + return nil } // UpdateEnvironment updates the hosts's environment variables. @@ -201,7 +224,7 @@ func (c LinuxConfigurer) UpdateEnvironment(h os.Host, env map[string]string) err // Update current environment from the /etc/environment err := h.Exec(`while read -r pair; do if [[ $pair == ?* && $pair != \#* ]]; then export "$pair" || exit 2; fi; done < /etc/environment`) if err != nil { - return err + return fmt.Errorf("failed to update current environment: %w", err) } return c.ConfigureDockerProxy(h, env) @@ -216,7 +239,11 @@ func (c LinuxConfigurer) CleanupEnvironment(h os.Host, env map[string]string) er } } // remove empty lines - return h.Exec(`sudo sed -i '/^$/d' /etc/environment`) + if err := h.Exec(`sudo sed -i '/^$/d' /etc/environment`); err != nil { + return fmt.Errorf("failed to remove empty lines from /etc/environment: %w", err) + } + + return nil } // ConfigureDockerProxy creates a docker systemd configuration for the proxy environment variables. @@ -239,7 +266,7 @@ func (c LinuxConfigurer) ConfigureDockerProxy(h os.Host, env map[string]string) err := h.Exec(fmt.Sprintf("sudo mkdir -p %s", dir)) if err != nil { - return err + return fmt.Errorf("failed to create %s: %w", dir, err) } content := "[Service]\n" @@ -247,22 +274,27 @@ func (c LinuxConfigurer) ConfigureDockerProxy(h os.Host, env map[string]string) content += fmt.Sprintf("Environment=\"%s=%s\"\n", escape.Quote(k), escape.Quote(v)) } - return c.riglinux.WriteFile(h, cfg, content, "0600") + if err := c.riglinux.WriteFile(h, cfg, content, "0600"); err != nil { + return fmt.Errorf("failed to create %s: %w", cfg, err) + } + + return nil } +var errDetectPrivateInterface = errors.New("failed to detect a private network interface, define the host privateInterface manually") + // ResolvePrivateInterface tries to find a private network interface. func (c LinuxConfigurer) ResolvePrivateInterface(h os.Host) (string, error) { output, err := h.ExecOutput(fmt.Sprintf(`%s; (ip route list scope global | grep -P "\b(172|10|192\.168)\.") || (ip route list | grep -m1 default)`, SbinPath)) - if err == nil { - re := regexp.MustCompile(`\bdev (\w+)`) - match := re.FindSubmatch([]byte(output)) - if len(match) > 0 { - return string(match[1]), nil - } - err = fmt.Errorf("can't find 'dev' in output") + if err != nil { + return "", fmt.Errorf("%w: %w", errDetectPrivateInterface, err) } - - return "", fmt.Errorf("failed to detect a private network interface, define the host privateInterface manually (%s)", err.Error()) + re := regexp.MustCompile(`\bdev (\w+)`) + match := re.FindSubmatch([]byte(output)) + if len(match) == 0 { + return "", fmt.Errorf("can't find 'dev' in output: %w", errDetectPrivateInterface) + } + return string(match[1]), nil } // HTTPStatus makes a HTTP GET request to the url and returns the status code or an error. @@ -270,11 +302,11 @@ func (c LinuxConfigurer) HTTPStatus(h os.Host, url string) (int, error) { log.Debugf("%s: requesting %s", h, url) output, err := h.ExecOutput(fmt.Sprintf(`curl -kso /dev/null -w "%%{http_code}" "%s"`, url)) if err != nil { - return -1, err + return -1, fmt.Errorf("failed to perform http request: %w", err) } status, err := strconv.Atoi(output) if err != nil { - return -1, fmt.Errorf("%s: invalid response: %s", h, err.Error()) + return -1, fmt.Errorf("invalid http response: %w", err) } return status, nil @@ -295,25 +327,25 @@ func (c LinuxConfigurer) CleanupLingeringMCR(h os.Host, dockerInfo common.Docker // https://docs.docker.com/config/daemon/ if !c.riglinux.FileExist(h, dockerDaemonPath) { // Check if the default Rootless Docker daemon config file exists - log.Debugf("%s attempting to detect Rootless docker installation", h) - // Extract the value from the XDG_CONFIG_HOME environment variable - XDG_CONFIG_HOME, err := h.ExecOutput("echo $XDG_CONFIG_HOME") - if XDG_CONFIG_HOME != "" && err == nil { - log.Debugf("%s XDG_CONFIG_HOME set to %s", h, XDG_CONFIG_HOME) - dockerDaemonPath = path.Join(strings.TrimSpace(XDG_CONFIG_HOME), "docker", "daemon.json") + log.Debugf("%s: attempting to detect Rootless docker installation", h) + // Extract the value from the xdgConfigHome environment variable + xdgConfigHome, err := h.ExecOutput("echo $XDG_CONFIG_HOME") + if xdgConfigHome != "" && err == nil { + log.Debugf("%s: XDG_CONFIG_HOME set to %s", h, xdgConfigHome) + dockerDaemonPath = path.Join(strings.TrimSpace(xdgConfigHome), "docker", "daemon.json") } else { dockerDaemonPath = constant.LinuxDefaultRootlessDockerDaemonPath - log.Debugf("%s XDG_CONFIG_HOME not set, using default rootless daemon path %s", h, dockerDaemonPath) + log.Debugf("%s: XDG_CONFIG_HOME not set, using default rootless daemon path %s", h, dockerDaemonPath) } } dockerDaemonString, err := c.riglinux.ReadFile(h, dockerDaemonPath) if err != nil { - log.Debugf("%s couldn't read the Docker Daemon config file %s: %s", h, dockerDaemonPath, err) + log.Debugf("%s: couldn't read the Docker Daemon config file %s: %s", h, dockerDaemonPath, err) } dockerConfig, err := c.GetDockerDaemonConfig(dockerDaemonString) if err != nil { - log.Debugf("%s failed to create DockerDaemon config %s: %s", h, dockerConfig, err) + log.Debugf("%s: failed to create DockerDaemon config %s: %s", h, dockerConfig, err) } if dockerConfig.Root != "" { @@ -334,7 +366,7 @@ func (c LinuxConfigurer) CleanupLingeringMCR(h os.Host, dockerInfo common.Docker // /var/run/ Exec-root folder execRootNetnsUnmount := path.Join(dockerExecRootDir, "netns/default") if err := h.Exec(fmt.Sprintf("sudo umount %s", execRootNetnsUnmount)); err != nil { - log.Debugf("%s failed to umount %s: %s", h, execRootNetnsUnmount, err) + log.Debugf("%s: failed to umount %s: %s", h, execRootNetnsUnmount, err) } // Extras to delete if they exist @@ -355,7 +387,7 @@ func (c LinuxConfigurer) CleanupLingeringMCR(h os.Host, dockerInfo common.Docker func (c LinuxConfigurer) attemptPathDelete(h os.Host, path string) { fileInfo, err := c.riglinux.Stat(h, path) if err != nil { - log.Debugf("%s error getting file information for %s: %s", h, path, err) + log.Debugf("%s: error getting file information for %s: %s", h, path, err) } else { command := fmt.Sprintf("sudo rm %s", path) if fileInfo.IsDir() { @@ -363,9 +395,9 @@ func (c LinuxConfigurer) attemptPathDelete(h os.Host, path string) { } if c.riglinux.FileExist(h, path) { if err := h.Exec(command); err != nil { - log.Infof("%s failed to remove %s: %s", h, path, err) + log.Infof("%s: failed to remove %s: %s", h, path, err) } - log.Infof("%s removed %s successfully", h, path) + log.Infof("%s: removed %s successfully", h, path) } } } diff --git a/pkg/configurer/sles/sles.go b/pkg/configurer/sles/sles.go index def16de6f..fd5ed99ae 100644 --- a/pkg/configurer/sles/sles.go +++ b/pkg/configurer/sles/sles.go @@ -1,6 +1,7 @@ package sles import ( + "fmt" "strings" "github.com/Mirantis/mcc/pkg/configurer" @@ -20,39 +21,44 @@ type Configurer struct { // InstallMKEBasePackages installs the needed base packages on Ubuntu. func (c Configurer) InstallMKEBasePackages(h os.Host) error { - return c.InstallPackage(h, "curl", "socat") + if err := c.InstallPackage(h, "curl", "socat"); err != nil { + return fmt.Errorf("failed to install base packages: %w", err) + } + return nil } // UninstallMCR uninstalls docker-ee engine. -func (c Configurer) UninstallMCR(h os.Host, scriptPath string, engineConfig common.MCRConfig) error { - var err error - info, getDockerError := c.GetDockerInfo(h, c.Kind()) +func (c Configurer) UninstallMCR(h os.Host, _ string, engineConfig common.MCRConfig) error { + info, getDockerError := c.GetDockerInfo(h) + if engineConfig.Prune { + defer c.CleanupLingeringMCR(h, info) + } if getDockerError == nil { - if err = h.Exec(c.DockerCommandf("system prune -f")); err != nil { - return err + if err := h.Exec("sudo docker system prune -f"); err != nil { + return fmt.Errorf("prune docker: %w", err) } - if err = c.StopService(h, "docker"); err != nil { - return err + if err := c.StopService(h, "docker"); err != nil { + return fmt.Errorf("stop docker: %w", err) } - if err = c.StopService(h, "containerd"); err != nil { - return err + if err := c.StopService(h, "containerd"); err != nil { + return fmt.Errorf("stop containerd: %w", err) } - err = h.Exec("sudo zypper -n remove -y --clean-deps docker-ee docker-ee-cli") - } - if engineConfig.Prune { - c.CleanupLingeringMCR(h, info) + if err := h.Exec("sudo zypper -n remove -y --clean-deps docker-ee docker-ee-cli"); err != nil { + return fmt.Errorf("remove docker-ee zypper package: %w", err) + } } - return err + + return nil } // LocalAddresses returns a list of local addresses, SLES12 has an old version of "hostname" without "--all-ip-addresses" and because of that, ip addr show is used here. func (c Configurer) LocalAddresses(h os.Host) ([]string, error) { output, err := h.ExecOutput("ip addr show | grep 'inet ' | awk '{print $2}' | cut -d/ -f1") if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get local addresses: %w", err) } return strings.Fields(output), nil diff --git a/pkg/configurer/ubuntu/ubuntu.go b/pkg/configurer/ubuntu/ubuntu.go index 2e5db6af5..684fcc2ac 100644 --- a/pkg/configurer/ubuntu/ubuntu.go +++ b/pkg/configurer/ubuntu/ubuntu.go @@ -1,6 +1,8 @@ package ubuntu import ( + "fmt" + "github.com/Mirantis/mcc/pkg/configurer" common "github.com/Mirantis/mcc/pkg/product/common/api" "github.com/k0sproject/rig/os" @@ -16,30 +18,35 @@ type Configurer struct { // InstallMKEBasePackages installs the needed base packages on Ubuntu. func (c Configurer) InstallMKEBasePackages(h os.Host) error { - return c.InstallPackage(h, "curl", "apt-utils", "socat", "iputils-ping") + if err := c.InstallPackage(h, "curl", "apt-utils", "socat", "iputils-ping"); err != nil { + return fmt.Errorf("failed to install base packages: %w", err) + } + return nil } // UninstallMCR uninstalls docker-ee engine. -func (c Configurer) UninstallMCR(h os.Host, scriptPath string, engineConfig common.MCRConfig) error { - var err error - info, getDockerError := c.GetDockerInfo(h, c.Kind()) +func (c Configurer) UninstallMCR(h os.Host, _ string, engineConfig common.MCRConfig) error { + info, getDockerError := c.GetDockerInfo(h) + if engineConfig.Prune { + defer c.CleanupLingeringMCR(h, info) + } if getDockerError == nil { - if err = h.Exec(c.DockerCommandf("system prune -f")); err != nil { - return err + if err := h.Exec("sudo docker system prune -f"); err != nil { + return fmt.Errorf("prune docker: %w", err) } - if err = c.StopService(h, "docker"); err != nil { - return err + if err := c.StopService(h, "docker"); err != nil { + return fmt.Errorf("stop docker: %w", err) } - if err = c.StopService(h, "containerd"); err != nil { - return err + if err := c.StopService(h, "containerd"); err != nil { + return fmt.Errorf("stop containerd: %w", err) } - err = h.Exec("sudo apt-get remove -y docker-ee docker-ee-cli && sudo apt autoremove -y") - } - if engineConfig.Prune { - c.CleanupLingeringMCR(h, info) + if err := h.Exec("sudo apt-get remove -y docker-ee docker-ee-cli && sudo apt autoremove -y"); err != nil { + return fmt.Errorf("failed to uninstall docker-ee apt package: %w", err) + } } - return err + + return nil } diff --git a/pkg/configurer/windows.go b/pkg/configurer/windows.go index 52a616fbf..6f191b2bc 100644 --- a/pkg/configurer/windows.go +++ b/pkg/configurer/windows.go @@ -12,12 +12,11 @@ import ( common "github.com/Mirantis/mcc/pkg/product/common/api" "github.com/Mirantis/mcc/pkg/util" "github.com/avast/retry-go" + "github.com/hashicorp/go-version" "github.com/k0sproject/rig/exec" "github.com/k0sproject/rig/os" ps "github.com/k0sproject/rig/pkg/powershell" log "github.com/sirupsen/logrus" - - "github.com/hashicorp/go-version" ) // WindowsConfigurer is a generic windows host configurer. @@ -37,17 +36,21 @@ type rebootable interface { Reboot() error } +var errRebootRequired = fmt.Errorf("reboot required") + // InstallMCR install MCR on Windows. func (c WindowsConfigurer) InstallMCR(h os.Host, scriptPath string, engineConfig common.MCRConfig) error { pwd := c.Pwd(h) base := path.Base(scriptPath) installer := pwd + "\\" + base + ".ps1" - err := h.Upload(scriptPath, installer) - if err != nil { - return err + if err := h.Upload(scriptPath, installer); err != nil { + return fmt.Errorf("failed to upload MCR installer: %w", err) } - - defer c.DeleteFile(h, installer) + defer func() { + if err := c.DeleteFile(h, installer); err != nil { + log.Warnf("failed to delete MCR installer: %s", err.Error()) + } + }() installCommand := fmt.Sprintf("set DOWNLOAD_URL=%s && set DOCKER_VERSION=%s && set CHANNEL=%s && powershell -ExecutionPolicy Bypass -NoProfile -NonInteractive -File %s -Verbose", engineConfig.RepoURL, engineConfig.Version, engineConfig.Channel, ps.DoubleQuote(installer)) @@ -55,15 +58,17 @@ func (c WindowsConfigurer) InstallMCR(h os.Host, scriptPath string, engineConfig output, err := h.ExecOutput(installCommand) if err != nil { - return err + return fmt.Errorf("failed to run MCR installer: %w", err) } if strings.Contains(output, "Your machine needs to be rebooted") { log.Warnf("%s: host needs to be rebooted", h) if rh, ok := h.(rebootable); ok { - return rh.Reboot() + if err := rh.Reboot(); err != nil { + return fmt.Errorf("%s: failed to reboot host: %w", h, err) + } } - return fmt.Errorf("%s: host can't be rebooted", h) + return fmt.Errorf("%s: %w: host isn't rebootable", h, errRebootRequired) } return nil @@ -73,46 +78,56 @@ func (c WindowsConfigurer) InstallMCR(h os.Host, scriptPath string, engineConfig // This relies on using the http://get.mirantis.com/install.ps1 script with the '-Uninstall' option, and some cleanup as per // https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-docker/configure-docker-daemon#how-to-uninstall-docker func (c WindowsConfigurer) UninstallMCR(h os.Host, scriptPath string, engineConfig common.MCRConfig) error { - var err error - info, getDockerError := c.GetDockerInfo(h, c.Kind()) + info, getDockerError := c.GetDockerInfo(h) + if engineConfig.Prune { + defer c.CleanupLingeringMCR(h, info) + } if getDockerError == nil { - err = h.Exec(c.DockerCommandf("system prune --volumes --all -f")) - if err != nil { - return err + if err := h.Exec(c.DockerCommandf("system prune --volumes --all -f")); err != nil { + return fmt.Errorf("prune docker: %w", err) } pwd := c.Pwd(h) base := path.Base(scriptPath) uninstaller := pwd + "\\" + base + ".ps1" - err = h.Upload(scriptPath, uninstaller) - if err != nil { - return err + if err := h.Upload(scriptPath, uninstaller); err != nil { + return fmt.Errorf("upload MCR uninstaller: %w", err) } - defer c.DeleteFile(h, uninstaller) + defer func() { + if err := c.DeleteFile(h, uninstaller); err != nil { + log.Warnf("failed to delete MCR uninstaller: %s", err.Error()) + } + }() uninstallCommand := fmt.Sprintf("powershell -NonInteractive -NoProfile -ExecutionPolicy Bypass -File %s -Uninstall -Verbose", ps.DoubleQuote(uninstaller)) - err = h.Exec(uninstallCommand) - } - if engineConfig.Prune { - c.CleanupLingeringMCR(h, info) + if err := h.Exec(uninstallCommand); err != nil { + return fmt.Errorf("run MCR uninstaller: %w", err) + } } - return err + return nil } // RestartMCR restarts Docker EE engine. func (c WindowsConfigurer) RestartMCR(h os.Host) error { - h.Exec("net stop com.docker.service") - h.Exec("net start com.docker.service") - return retry.Do( + _ = h.Exec("net stop com.docker.service") + _ = h.Exec("net start com.docker.service") + err := retry.Do( func() error { - return h.Exec(c.DockerCommandf("ps")) + if err := h.Exec(c.DockerCommandf("ps")); err != nil { + return fmt.Errorf("failed to run docker ps after restart: %w", err) + } + return nil }, retry.DelayType(retry.CombineDelay(retry.FixedDelay, retry.RandomDelay)), retry.MaxJitter(time.Second*2), retry.Delay(time.Second*3), retry.Attempts(10), ) + if err != nil { + return fmt.Errorf("failed to restart docker service: %w", err) + } + return nil } // ResolveInternalIP resolves internal ip from private interface. @@ -143,7 +158,7 @@ func (c WindowsConfigurer) ResolveInternalIP(h os.Host, privateInterface, public func (c WindowsConfigurer) interfaceIP(h os.Host, iface string) (string, error) { output, err := h.ExecOutput(ps.Cmd(fmt.Sprintf(`(Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias %s).IPAddress`, ps.SingleQuote(iface)))) if err != nil { - return "", err + return "", fmt.Errorf("failed to get IP address for interface %s: %w", iface, err) } return strings.TrimSpace(output), nil } @@ -158,7 +173,7 @@ func (c WindowsConfigurer) DockerCommandf(template string, args ...interface{}) func (c WindowsConfigurer) ValidateLocalhost(h os.Host) error { err := h.Exec(ps.Cmd(`"$ips=[System.Net.Dns]::GetHostAddresses('localhost'); Get-NetIPAddress -IPAddress $ips"`)) if err != nil { - return fmt.Errorf("hostname 'localhost' does not resolve to an address local to the host") + return fmt.Errorf("hostname 'localhost' does not resolve to an address local to the host: %w", err) } return nil } @@ -167,7 +182,7 @@ func (c WindowsConfigurer) ValidateLocalhost(h os.Host) error { func (c WindowsConfigurer) LocalAddresses(h os.Host) ([]string, error) { output, err := h.ExecOutput(ps.Cmd(`(Get-NetIPAddress).IPV4Address`)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get local addresses: %w", err) } var lines []string // bufio used to split lines on windows @@ -182,8 +197,8 @@ func (c WindowsConfigurer) LocalAddresses(h os.Host) ([]string, error) { func (c WindowsConfigurer) CheckPrivilege(h os.Host) error { privCheck := "\"$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if (!$currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { $host.SetShouldExit(1) }\"" - if h.Exec(ps.Cmd(privCheck)) != nil { - return fmt.Errorf("user does not have administrator rights on the host") + if err := h.Exec(ps.Cmd(privCheck)); err != nil { + return fmt.Errorf("user does not have administrator rights on the host: %w", err) } return nil @@ -192,7 +207,10 @@ func (c WindowsConfigurer) CheckPrivilege(h os.Host) error { // AuthenticateDocker performs a docker login on the host. func (c WindowsConfigurer) AuthenticateDocker(h os.Host, user, pass, imageRepo string) error { // the --pasword-stdin seems to hang in windows - return h.Exec(c.DockerCommandf("login -u %s -p %s %s", user, pass, imageRepo), exec.RedactString(user, pass), exec.AllowWinStderr()) + if err := h.Exec(c.DockerCommandf("login -u %s -p %s %s", user, pass, imageRepo), exec.RedactString(user, pass), exec.AllowWinStderr()); err != nil { + return fmt.Errorf("failed to login to docker registry: %w", err) + } + return nil } // UpdateEnvironment updates the hosts's environment variables. @@ -200,7 +218,7 @@ func (c WindowsConfigurer) UpdateEnvironment(h os.Host, env map[string]string) e for k, v := range env { err := h.Exec(fmt.Sprintf(`setx %s %s`, ps.DoubleQuote(k), ps.DoubleQuote(v))) if err != nil { - return err + return fmt.Errorf("failed to set environment variable %s: %w", k, err) } } return nil @@ -209,8 +227,8 @@ func (c WindowsConfigurer) UpdateEnvironment(h os.Host, env map[string]string) e // CleanupEnvironment removes environment variable configuration. func (c WindowsConfigurer) CleanupEnvironment(h os.Host, env map[string]string) error { for k := range env { - h.Exec(ps.Cmd(fmt.Sprintf(`[Environment]::SetEnvironmentVariable(%s, $null, 'User')`, ps.SingleQuote(k)))) - h.Exec(ps.Cmd(fmt.Sprintf(`[Environment]::SetEnvironmentVariable(%s, $null, 'Machine')`, ps.SingleQuote(k)))) + _ = h.Exec(ps.Cmd(fmt.Sprintf(`[Environment]::SetEnvironmentVariable(%s, $null, 'User')`, ps.SingleQuote(k)))) + _ = h.Exec(ps.Cmd(fmt.Sprintf(`[Environment]::SetEnvironmentVariable(%s, $null, 'Machine')`, ps.SingleQuote(k)))) } return nil } @@ -222,7 +240,7 @@ func (c WindowsConfigurer) ResolvePrivateInterface(h os.Host) (string, error) { output, err = h.ExecOutput(ps.Cmd(`(Get-NetConnectionProfile | Select-Object -First 1).InterfaceAlias`)) } if err != nil || output == "" { - return "", fmt.Errorf("failed to detect a private network interface, define the host privateInterface manually") + return "", fmt.Errorf("failed to detect a private network interface, define the host privateInterface manually: %w", err) } return strings.TrimSpace(output), nil } @@ -232,17 +250,17 @@ func (c WindowsConfigurer) HTTPStatus(h os.Host, url string) (int, error) { log.Debugf("%s: requesting %s", h, url) output, err := h.ExecOutput(ps.Cmd(fmt.Sprintf(`[int][System.Net.WebRequest]::Create(%s).GetResponse().StatusCode`, ps.SingleQuote(url)))) if err != nil { - return -1, err + return -1, fmt.Errorf("failed to get HTTP status code: %w", err) } status, err := strconv.Atoi(output) if err != nil { - return -1, fmt.Errorf("invalid response: %s", err.Error()) + return -1, fmt.Errorf("invalid response: %w", err) } return status, nil } // AuthorizeDocker does nothing on windows. -func (c WindowsConfigurer) AuthorizeDocker(h os.Host) error { +func (c WindowsConfigurer) AuthorizeDocker(_ os.Host) error { return nil } @@ -259,13 +277,13 @@ func (c WindowsConfigurer) CleanupLingeringMCR(h os.Host, dockerInfo common.Dock log.Errorf("error checking if Docker Daemon configuration file exists at %s: %v", c.MCRConfigPath(), err) } if exists == "True" { - log.Infof("%s MCR configuration file exists at %s", h, c.MCRConfigPath()) + log.Infof("%s: MCR configuration file exists at %s", h, c.MCRConfigPath()) var dockerDaemon common.DockerDaemonConfig dockerDaemonString, err := h.ExecOutput(ps.Cmd(fmt.Sprintf("Get-Content -Path %s", ps.SingleQuote(c.MCRConfigPath())))) if err != nil { dockerDaemon, err := c.DockerConfigurer.GetDockerDaemonConfig(dockerDaemonString) if err != nil { - log.Errorf("%s error constructing dockerDaemon struct %+v: %s", h, dockerDaemon, err) + log.Errorf("%s: error constructing dockerDaemon struct %+v: %s", h, dockerDaemon, err) } } if dockerDaemon.Root != "" { @@ -281,8 +299,8 @@ func (c WindowsConfigurer) attemptPathDelete(h os.Host, path string) { removeCommand := fmt.Sprintf("powershell Remove-Item -LiteralPath %s -Force -Recurse ", ps.SingleQuote(path)) if err := h.Exec(removeCommand); err != nil { - log.Debugf("%s failed to remove %s: %s", h, path, err) + log.Debugf("%s: failed to remove %s: %s", h, path, err) } else { - log.Infof("%s removed %s successfully", h, path) + log.Infof("%s: removed %s successfully", h, path) } } diff --git a/pkg/docker/hub/hub.go b/pkg/docker/hub/hub.go index 694b71b5a..0ac7f1433 100644 --- a/pkg/docker/hub/hub.go +++ b/pkg/docker/hub/hub.go @@ -19,6 +19,8 @@ type tagListResponse struct { } `json:"results"` } +var errQueryFailed = fmt.Errorf("latest version query failed, you can try running with --disable-upgrade-check") + // LatestTag returns the latest tag name from a public docker hub repository. // If pre is true, also prereleases are considered. func LatestTag(org, image string, pre bool) (string, error) { @@ -29,34 +31,34 @@ func LatestTag(org, image string, pre bool) (string, error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - return "", err + return "", fmt.Errorf("%w: %w", errQueryFailed, err) } req.Header.Set("Accept", "application/json") res, err := client.Do(req) if err != nil { - return "", fmt.Errorf("latest version query failed, you can try running with --disable-upgrade-check: %s", err.Error()) + return "", fmt.Errorf("%w: %w", errQueryFailed, err) } if res == nil { - return "", fmt.Errorf("latest version query failed for an unknown reason, you can try running with --disable-upgrade-check") + return "", errQueryFailed } if res.Body != nil { defer res.Body.Close() } - if res.StatusCode > 299 || res.StatusCode < 200 { - return "", fmt.Errorf("response status %d", res.StatusCode) + if res.StatusCode > 299 || res.StatusCode < http.StatusOK { + return "", fmt.Errorf("%w: response status %d", errQueryFailed, res.StatusCode) } body, err := io.ReadAll(res.Body) if err != nil { - return "", err + return "", fmt.Errorf("%w: read response body: %w", errQueryFailed, err) } var taglist tagListResponse if err := json.Unmarshal(body, &taglist); err != nil { - return "", err + return "", fmt.Errorf("%w: unmarshal response: %w", errQueryFailed, err) } var tags []*version.Version @@ -70,7 +72,7 @@ func LatestTag(org, image string, pre bool) (string, error) { } } if len(tags) == 0 { - return "", fmt.Errorf("no tags received") + return "", fmt.Errorf("%w: no tags received", errQueryFailed) } sort.Sort(version.Collection(tags)) return tags[len(tags)-1].String(), nil diff --git a/pkg/docker/image.go b/pkg/docker/image.go index e84d226e7..4f13ca0c4 100644 --- a/pkg/docker/image.go +++ b/pkg/docker/image.go @@ -46,7 +46,7 @@ func (i *Image) String() string { // Pull pulls an image on a host. func (i *Image) Pull(h *api.Host) error { - return retry.Do( + err := retry.Do( func() error { log.Infof("%s: pulling image %s", h, i) if i.Exist(h) { @@ -55,26 +55,33 @@ func (i *Image) Pull(h *api.Host) error { } output, err := h.ExecOutput(h.Configurer.DockerCommandf("pull %s", i)) if err != nil { - return fmt.Errorf("%s: failed to pull image: %s", h, output) + return fmt.Errorf("%s: failed to pull image: %s: %w", h, output, err) } return nil }, retry.RetryIf(func(err error) bool { return !(strings.Contains(err.Error(), "pull access") || strings.Contains(err.Error(), "manifest unknown")) }), - retry.OnRetry(func(n uint, err error) { + retry.OnRetry(func(_ uint, err error) { if err != nil { log.Warnf("%s: failed to pull image %s - retrying", h, i) } }), retry.Attempts(2), ) + if err != nil { + return fmt.Errorf("retry count exceeded: %w", err) + } + return nil } // Retag retags image A to image B. func (i *Image) Retag(h *api.Host, a, b *Image) error { log.Debugf("%s: retag %s --> %s", h, a, b) - return h.Exec(h.Configurer.DockerCommandf("tag %s %s", a, b)) + if err := h.Exec(h.Configurer.DockerCommandf("tag %s %s", a, b)); err != nil { + return fmt.Errorf("%s: failed to retag image %s --> %s: %w", h, a, b, err) + } + return nil } // Exist returns true if a docker image exists on the host. diff --git a/pkg/log/formatter_hook.go b/pkg/log/formatter_hook.go index c8c36c257..05b5e05e8 100644 --- a/pkg/log/formatter_hook.go +++ b/pkg/log/formatter_hook.go @@ -30,10 +30,12 @@ func (hook *FormatterWriterHook) Fire(entry *log.Entry) error { line, err := hook.Formatter.Format(entry) if err != nil { fmt.Fprintf(os.Stderr, "Unable to format log entry: %v", err) - return err + return fmt.Errorf("unable to format log entry: %w", err) } - _, err = hook.Writer.Write(line) - return err + if _, err = hook.Writer.Write(line); err != nil { + return fmt.Errorf("unable to write log entry to writer: %w", err) + } + return nil } // Levels define on which log levels this hook would trigger. @@ -67,7 +69,6 @@ func NewStdoutHook() *FormatterWriterHook { // NewFileHook creates logrus hook for logging all levels to file. func NewFileHook(logFile *os.File) *FormatterWriterHook { - fileFormatter := &log.TextFormatter{ DisableColors: true, FullTimestamp: true, diff --git a/pkg/log/host_logger.go b/pkg/log/host_logger.go new file mode 100644 index 000000000..672a5e27c --- /dev/null +++ b/pkg/log/host_logger.go @@ -0,0 +1,156 @@ +package log + +import ( + log "github.com/sirupsen/logrus" +) + +// LogHost is an interface that can be implemented to provide a host name to the logger. +type Host interface { + Name() string +} + +type HostLogger struct { + Host Host +} + +func (l *HostLogger) withHost() *log.Entry { + if l.Host == nil { + return &log.Entry{} + } + return log.WithField("host", l.Host.Name()) +} + +// Trace logs a message at level Trace on the standard logger. +func (l *HostLogger) Trace(args ...interface{}) { + l.withHost().Trace(args...) +} + +// Debug logs a message at level Debug on the standard logger. +func (l *HostLogger) Debug(args ...interface{}) { + l.withHost().Debug(args...) +} + +// Print logs a message at level Info on the standard logger. +func (l *HostLogger) Print(args ...interface{}) { + l.withHost().Print(args...) +} + +// Info logs a message at level Info on the standard logger. +func (l *HostLogger) Info(args ...interface{}) { + l.withHost().Info(args...) +} + +// Warn logs a message at level Warn on the standard logger. +func (l *HostLogger) Warn(args ...interface{}) { + l.withHost().Warn(args...) +} + +// Warning logs a message at level Warn on the standard logger. +func (l *HostLogger) Warning(args ...interface{}) { + l.withHost().Warning(args...) +} + +// Error logs a message at level Error on the standard logger. +func (l *HostLogger) Error(args ...interface{}) { + l.withHost().Error(args...) +} + +// Panic logs a message at level Panic on the standard logger. +func (l *HostLogger) Panic(args ...interface{}) { + l.withHost().Panic(args...) +} + +// Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1. +func (l *HostLogger) Fatal(args ...interface{}) { + l.withHost().Fatal(args...) +} + +// Tracef logs a message at level Trace on the standard logger. +func (l *HostLogger) Tracef(format string, args ...interface{}) { + l.withHost().Tracef(format, args...) +} + +// Debugf logs a message at level Debug on the standard logger. +func (l *HostLogger) Debugf(format string, args ...interface{}) { + l.withHost().Debugf(format, args...) +} + +// Printf logs a message at level Info on the standard logger. +func (l *HostLogger) Printf(format string, args ...interface{}) { + l.withHost().Printf(format, args...) +} + +// Infof logs a message at level Info on the standard logger. +func (l *HostLogger) Infof(format string, args ...interface{}) { + l.withHost().Infof(format, args...) +} + +// Warnf logs a message at level Warn on the standard logger. +func (l *HostLogger) Warnf(format string, args ...interface{}) { + l.withHost().Warnf(format, args...) +} + +// Warningf logs a message at level Warn on the standard logger. +func (l *HostLogger) Warningf(format string, args ...interface{}) { + l.withHost().Warningf(format, args...) +} + +// Errorf logs a message at level Error on the standard logger. +func (l *HostLogger) Errorf(format string, args ...interface{}) { + l.withHost().Errorf(format, args...) +} + +// Panicf logs a message at level Panic on the standard logger. +func (l *HostLogger) Panicf(format string, args ...interface{}) { + l.withHost().Panicf(format, args...) +} + +// Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1. +func (l *HostLogger) Fatalf(format string, args ...interface{}) { + l.withHost().Fatalf(format, args...) +} + +// Traceln logs a message at level Trace on the standard logger. +func (l *HostLogger) Traceln(args ...interface{}) { + l.withHost().Traceln(args...) +} + +// Debugln logs a message at level Debug on the standard logger. +func (l *HostLogger) Debugln(args ...interface{}) { + l.withHost().Debugln(args...) +} + +// Println logs a message at level Info on the standard logger. +func (l *HostLogger) Println(args ...interface{}) { + l.withHost().Println(args...) +} + +// Infoln logs a message at level Info on the standard logger. +func (l *HostLogger) Infoln(args ...interface{}) { + l.withHost().Infoln(args...) +} + +// Warnln logs a message at level Warn on the standard logger. +func (l *HostLogger) Warnln(args ...interface{}) { + l.withHost().Warnln(args...) +} + +// Warningln logs a message at level Warn on the standard logger. +func (l *HostLogger) Warningln(args ...interface{}) { + l.withHost().Warningln(args...) +} + +// Errorln logs a message at level Error on the standard logger. +func (l *HostLogger) Errorln(args ...interface{}) { + l.withHost().Errorln(args...) +} + +// Panicln logs a message at level Panic on the standard logger. +func (l *HostLogger) Panicln(args ...interface{}) { + l.withHost().Panicln(args...) +} + +// Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1. +func (l *HostLogger) Fatalln(args ...interface{}) { + l.withHost().Fatalln(args...) +} diff --git a/pkg/mke/mke.go b/pkg/mke/mke.go index ad3a0926c..4681a27f0 100644 --- a/pkg/mke/mke.go +++ b/pkg/mke/mke.go @@ -7,6 +7,7 @@ import ( "crypto/x509" "encoding/json" "encoding/pem" + "errors" "fmt" "io" "net/http" @@ -17,7 +18,6 @@ import ( "github.com/Mirantis/mcc/pkg/product/mke/api" "github.com/hashicorp/go-version" "github.com/k0sproject/rig/exec" - log "github.com/sirupsen/logrus" ) @@ -34,18 +34,20 @@ type Credentials struct { Password string `json:"password,omitempty"` } +var errInvalidVersion = errors.New("invalid version") + // CollectFacts gathers the current status of installed mke setup. func CollectFacts(swarmLeader *api.Host, mkeMeta *api.MKEMetadata) error { output, err := swarmLeader.ExecOutput(swarmLeader.Configurer.DockerCommandf(`inspect --format '{{.Config.Image}}' ucp-proxy`)) if err != nil { mkeMeta.Installed = false mkeMeta.InstalledVersion = "" - return nil //nolint:nilerr + return nil } vparts := strings.Split(output, ":") if len(vparts) != 2 { - return fmt.Errorf("malformed version output: %s", output) + return fmt.Errorf("%w: malformed version output: %s", errInvalidVersion, output) } repo := vparts[0][:strings.LastIndexByte(vparts[0], '/')] @@ -83,7 +85,7 @@ func GetClientBundle(mkeURL *url.URL, tlsConfig *tls.Config, username, password // Login and get a token for the user token, err := GetToken(client, mkeURL, username, password) if err != nil { - return nil, fmt.Errorf("Failed to get token for (%s:%s) : %s", username, password, err) + return nil, fmt.Errorf("failed to get token for (%s:%s): %w", username, password, err) } mkeURL.Path = "/api/clientbundle" @@ -91,25 +93,28 @@ func GetClientBundle(mkeURL *url.URL, tlsConfig *tls.Config, username, password // Now download the bundle req, err := http.NewRequest(http.MethodGet, mkeURL.String(), nil) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) resp, err := client.Do(req) if err != nil { log.Debugf("Failed to get bundle: %v", err) - return nil, err + return nil, fmt.Errorf("failed to request client bundle: %w", err) } body, err := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != http.StatusOK { - if err == nil { - return nil, fmt.Errorf("Failed to get client bundle (%d): %s", resp.StatusCode, string(body)) - } - return nil, err + return nil, fmt.Errorf("failed to read client bundle (%d): %s: %w", resp.StatusCode, string(body), err) } - return zip.NewReader(bytes.NewReader(body), int64(len(body))) + reader, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) + if err != nil { + return nil, fmt.Errorf("failed to create a reader for client bundle: %w", err) + } + return reader, nil } +var errGetToken = errors.New("failed to get token") + // GetToken gets a mke Authtoken from the given mkeURL. func GetToken(client *http.Client, mkeURL *url.URL, username, password string) (string, error) { mkeURL.Path = "/auth/login" @@ -120,25 +125,27 @@ func GetToken(client *http.Client, mkeURL *url.URL, username, password string) ( reqJSON, err := json.Marshal(creds) if err != nil { - return "", err + return "", fmt.Errorf("failed to marshal credentials: %w", err) } resp, err := client.Post(mkeURL.String(), "application/json", bytes.NewBuffer(reqJSON)) if err != nil { log.Debugf("Failed to POST %s: %v", mkeURL.String(), err) - return "", err + return "", fmt.Errorf("failed to request token: %w", err) } body, _ := io.ReadAll(resp.Body) resp.Body.Close() - if resp.StatusCode == 200 { + if resp.StatusCode == http.StatusOK { var authToken AuthToken if err := json.Unmarshal(body, &authToken); err != nil { - return "", err + return "", fmt.Errorf("failed to unmarshal token response: %w", err) } return authToken.Token, nil } - return "", fmt.Errorf("Unexpected error logging in to mke: %s", string(body)) + return "", fmt.Errorf("%w: unexpected error logging in to mke: %s", errGetToken, string(body)) } +var errGetTLSConfig = errors.New("failed to get TLS config") + // GetTLSConfigFrom retrieves the valid tlsConfig from the given mke manager. func GetTLSConfigFrom(manager *api.Host, imageRepo, mkeVersion string) (*tls.Config, error) { runFlags := common.Flags{"--rm", "-v /var/run/docker.sock:/var/run/docker.sock"} @@ -147,33 +154,34 @@ func GetTLSConfigFrom(manager *api.Host, imageRepo, mkeVersion string) (*tls.Con } output, err := manager.ExecOutput(manager.Configurer.DockerCommandf(`run %s %s/ucp:%s dump-certs --ca`, runFlags.Join(), imageRepo, mkeVersion, exec.Redact(`[A-Za-z0-9+/=_\-]{64}`))) if err != nil { - return nil, fmt.Errorf("error while exec-ing into the container: %w", err) + return nil, fmt.Errorf("%w: error while exec-ing into the container: %w", errGetTLSConfig, err) } i := strings.Index(output, "-----BEGIN CERTIFICATE-----") if i < 0 { - return nil, fmt.Errorf("malformed certificate") + return nil, fmt.Errorf("%w: malformed certificate", errGetTLSConfig) } cert := []byte(output[i:]) block, _ := pem.Decode(cert) if block == nil { - return nil, fmt.Errorf("no certificates found in output") + return nil, fmt.Errorf("%w: no certificates found in output", errGetTLSConfig) } if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { - return nil, fmt.Errorf("invalid certificate: %#v", block) + return nil, fmt.Errorf("%w: invalid certificate: %#v", errGetTLSConfig, block) } if _, err = x509.ParseCertificate(block.Bytes); err != nil { - return nil, fmt.Errorf("failed to parse certificate: %w", err) + return nil, fmt.Errorf("%w: failed to parse certificate: %w", errGetTLSConfig, err) } caCertPool := x509.NewCertPool() ok := caCertPool.AppendCertsFromPEM(cert) if !ok { - return nil, fmt.Errorf("error while appending certs to PEM") + return nil, fmt.Errorf("%w: error while appending certs to PEM", errGetTLSConfig) } return &tls.Config{ - RootCAs: caCertPool, + RootCAs: caCertPool, + MinVersion: tls.VersionTLS12, }, nil } diff --git a/pkg/msr/msr.go b/pkg/msr/msr.go index 05265a9ed..c60bcf756 100644 --- a/pkg/msr/msr.go +++ b/pkg/msr/msr.go @@ -17,7 +17,7 @@ import ( func CollectFacts(h *api.Host) (*api.MSRMetadata, error) { rethinkdbContainerID, err := h.ExecOutput(h.Configurer.DockerCommandf(`ps -aq --filter name=dtr-rethinkdb`)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get MSR container ID: %w", err) } if rethinkdbContainerID == "" { return &api.MSRMetadata{Installed: false}, nil @@ -25,11 +25,11 @@ func CollectFacts(h *api.Host) (*api.MSRMetadata, error) { version, err := h.ExecOutput(h.Configurer.DockerCommandf(`inspect %s --format '{{ index .Config.Labels "com.docker.dtr.version"}}'`, rethinkdbContainerID)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get MSR version: %w", err) } replicaID, err := h.ExecOutput(h.Configurer.DockerCommandf(`inspect %s --format '{{ index .Config.Labels "com.docker.dtr.replica"}}'`, rethinkdbContainerID)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get MSR replicaID: %w", err) } if version == "" || replicaID == "" { // If we failed to obtain either label then this MSR version does not @@ -37,7 +37,7 @@ func CollectFacts(h *api.Host) (*api.MSRMetadata, error) { // wrong, attempt to pull these details with the old method output, err := h.ExecOutput(h.Configurer.DockerCommandf(`inspect %s --format '{{ index .Config.Labels "com.docker.compose.project"}}'`, rethinkdbContainerID)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get MSR rethink container labels: %w", err) } outputFields := strings.Fields(output) if version == "" { @@ -154,14 +154,14 @@ func Destroy(h *api.Host) error { log.Debugf("%s: Removing MSR containers", h) containersToRemove, err := h.ExecOutput(h.Configurer.DockerCommandf("ps -aq --filter name=dtr-")) if err != nil { - return err + return fmt.Errorf("failed to get MSR container list: %w", err) } if strings.TrimSpace(containersToRemove) == "" { log.Debugf("No MSR containers to remove") } else { containersToRemove = strings.Join(strings.Fields(containersToRemove), " ") if err := h.Exec(h.Configurer.DockerCommandf("rm -f %s", containersToRemove)); err != nil { - return err + return fmt.Errorf("failed to remove MSR containers: %w", err) } } @@ -169,7 +169,7 @@ func Destroy(h *api.Host) error { log.Debugf("%s: Removing MSR volumes", h) volumeOutput, err := h.ExecOutput(h.Configurer.DockerCommandf("volume ls -q")) if err != nil { - return err + return fmt.Errorf("failed to get MSR volume list: %w", err) } if strings.Trim(volumeOutput, " ") == "" { log.Debugf("No volumes in volume list") @@ -190,12 +190,14 @@ func Destroy(h *api.Host) error { volumes := strings.Join(volumesToRemove, " ") err = h.Exec(h.Configurer.DockerCommandf("volume rm -f %s", volumes)) if err != nil { - return err + return fmt.Errorf("failed to remove MSR volumes: %w", err) } } return nil } +var errMaxReplicaID = fmt.Errorf("max sequential msr replica id exceeded") + // AssignSequentialReplicaIDs goes through all the MSR hosts, finds the highest replica id and assigns sequential ones starting from that to all the hosts without replica ids. func AssignSequentialReplicaIDs(c *api.ClusterConfig) error { msrHosts := c.Spec.MSRs() @@ -209,7 +211,7 @@ func AssignSequentialReplicaIDs(c *api.ClusterConfig) error { if h.MSRMetadata.ReplicaID != "" { ri, err := strconv.ParseUint(h.MSRMetadata.ReplicaID, 16, 48) if err != nil { - return fmt.Errorf("%s: invalid MSR replicaID '%s': %s", h, h.MSRMetadata.ReplicaID, err) + return fmt.Errorf("%s: invalid MSR replicaID '%s': %w", h, h.MSRMetadata.ReplicaID, err) } if maxReplicaID < ri { maxReplicaID = ri @@ -218,18 +220,21 @@ func AssignSequentialReplicaIDs(c *api.ClusterConfig) error { return nil }) if err != nil { - return err + return fmt.Errorf("failed to find max MSR replicaID: %w", err) } if maxReplicaID+uint64(len(msrHosts)) > 0xffffffffffff { - return fmt.Errorf("can not assign sequential MSR replica ids: cluster already has replica id %012x which will overflow", maxReplicaID) + return fmt.Errorf("%w: cluster already has replica id %012x which will overflow", errMaxReplicaID, maxReplicaID) } - return msrHosts.Each(func(h *api.Host) error { + + _ = msrHosts.Each(func(h *api.Host) error { if h.MSRMetadata.ReplicaID == "" { maxReplicaID++ h.MSRMetadata.ReplicaID = FormatReplicaID(maxReplicaID) } return nil }) + + return nil } // Cleanup accepts a list of msrHosts to remove all containers, volumes @@ -240,12 +245,14 @@ func Cleanup(msrHosts []*api.Host, swarmLeader *api.Host) error { log.Debugf("%s: Destroying MSR host", h) err := Destroy(h) if err != nil { - return fmt.Errorf("failed to run MSR destroy: %s", err) + return fmt.Errorf("failed to run MSR destroy: %w", err) } } // Remove dtr-ol via the swarmLeader log.Infof("%s: Removing dtr-ol network", swarmLeader) - swarmLeader.Exec(swarmLeader.Configurer.DockerCommandf("network rm dtr-ol")) + if err := swarmLeader.Exec(swarmLeader.Configurer.DockerCommandf("network rm dtr-ol")); err != nil { + return fmt.Errorf("failed to remove dtr-ol network: %w", err) + } return nil } @@ -255,7 +262,7 @@ func WaitMSRNodeReady(h *api.Host, port int) error { func() error { output, err := h.ExecOutput(h.Configurer.DockerCommandf("ps -q -f health=healthy -f name=dtr-nginx")) if err != nil || strings.TrimSpace(output) == "" { - return fmt.Errorf("msr nginx container not running") + return fmt.Errorf("msr nginx container not running: %w", err) } return nil }, @@ -264,15 +271,14 @@ func WaitMSRNodeReady(h *api.Host, port int) error { retry.Delay(time.Second*3), retry.Attempts(60), ) - if err != nil { - return err + return fmt.Errorf("retry limit exceeded: %w", err) } - return retry.Do( + err = retry.Do( func() error { if err := h.CheckHTTPStatus(fmt.Sprintf("https://localhost:%d/_ping", port), 200); err != nil { - return fmt.Errorf("msr invalid ping response") + return fmt.Errorf("msr invalid ping response: %w", err) } return nil @@ -282,4 +288,8 @@ func WaitMSRNodeReady(h *api.Host, port int) error { retry.Delay(time.Second*3), retry.Attempts(120), ) + if err != nil { + return fmt.Errorf("retry limit exceeded: %w", err) + } + return nil } diff --git a/pkg/phase/manager.go b/pkg/phase/manager.go index 7ab978a29..311fbb986 100644 --- a/pkg/phase/manager.go +++ b/pkg/phase/manager.go @@ -61,24 +61,24 @@ func (m *Manager) AddPhase(p phase) { // Run executes all the added Phases in order. func (m *Manager) Run() error { - for _, p := range m.phases { - title := p.Title() + for _, phase := range m.phases { + title := phase.Title() - if p, ok := p.(withconfig); ok { + if p, ok := phase.(withconfig); ok { log.Debugf("preparing phase '%s'", title) if err := p.Prepare(m.config); err != nil { - return err + return fmt.Errorf("phase '%s' failed to prepare: %w", title, err) } } if m.SkipCleanup { - if p, ok := p.(cleanupdisabling); ok { + if p, ok := phase.(cleanupdisabling); ok { log.Debugf("disabling in-phase cleanup for '%s'", title) p.DisableCleanup() } } - if p, ok := p.(conditional); ok { + if p, ok := phase.(conditional); ok { if !p.ShouldRun() { log.Debugf("skipping phase '%s'", title) continue @@ -89,12 +89,12 @@ func (m *Manager) Run() error { log.Infof(text, title) start := time.Now() - result := p.Run() + result := phase.Run() duration := time.Since(start) log.Debugf("phase '%s' took %s", title, duration.Truncate(time.Minute)) - if e, ok := p.(Eventable); ok { + if e, ok := phase.(Eventable); ok { r := reflect.ValueOf(m.config).Elem() props := event.Properties{ "kind": r.FieldByName("Kind").String(), @@ -105,22 +105,23 @@ func (m *Manager) Run() error { props[k] = v } props["success"] = result == nil - defer analytics.TrackEvent(title, props) + defer func() { analytics.TrackEvent(title, props) }() } if result != nil { - if p, ok := p.(withcleanup); ok { + if p, ok := phase.(withcleanup); ok { if !m.SkipCleanup { defer p.CleanUp() } } + if m.IgnoreErrors { log.Debugf("ignoring phase '%s' error: %s", title, result.Error()) return nil } - return fmt.Errorf("phase failure: %s => %w", title, result) } + log.Debugf("phase '%s' completed successfully", title) } return nil diff --git a/pkg/phase/phase.go b/pkg/phase/phase.go index 6b50b59d3..ad87336f0 100644 --- a/pkg/phase/phase.go +++ b/pkg/phase/phase.go @@ -1,6 +1,7 @@ package phase import ( + "fmt" "strings" "github.com/Mirantis/mcc/pkg/product/mke/api" @@ -36,13 +37,19 @@ func (p *CleanupDisabling) CleanupDisabled() bool { // Prepare rceives the cluster config and stores it to the phase's config field. func (p *BasicPhase) Prepare(config interface{}) error { - p.Config = config.(*api.ClusterConfig) + if cfg, ok := config.(*api.ClusterConfig); ok { + p.Config = cfg + } return nil } // Prepare HostSelectPhase implementation which runs the supplied HostFilterFunc to populate the phase's hosts field. func (p *HostSelectPhase) Prepare(config interface{}) error { - p.Config = config.(*api.ClusterConfig) + cfg, ok := config.(*api.ClusterConfig) + if !ok { + return nil + } + p.Config = cfg hosts := p.Config.Spec.Hosts.Filter(p.HostFilterFunc) p.Hosts = hosts return nil @@ -54,7 +61,7 @@ func (p *HostSelectPhase) ShouldRun() bool { } // HostFilterFunc default implementation, matches all hosts. -func (p *HostSelectPhase) HostFilterFunc(host *api.Host) bool { +func (p *HostSelectPhase) HostFilterFunc(_ *api.Host) bool { return true } @@ -100,11 +107,15 @@ func (e *Error) Error() string { // RunParallelOnHosts runs a function parallelly on the listed hosts. func RunParallelOnHosts(hosts api.Hosts, config *api.ClusterConfig, action func(h *api.Host, config *api.ClusterConfig) error) error { - return hosts.ParallelEach(func(h *api.Host) error { + result := hosts.ParallelEach(func(h *api.Host) error { err := action(h, config) if err != nil { log.Error(err.Error()) } return err }) + if result != nil { + return fmt.Errorf("run parallel on hosts: %w", result) + } + return nil } diff --git a/pkg/product/common/phase/connect.go b/pkg/product/common/phase/connect.go index 444d42f0f..36bc3a182 100644 --- a/pkg/product/common/phase/connect.go +++ b/pkg/product/common/phase/connect.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "reflect" - "strings" "sync" "time" @@ -51,35 +50,28 @@ func (p *Connect) Title() string { // Run connects to all the hosts in parallel. func (p *Connect) Run() error { - var wg sync.WaitGroup - var errors []string - type erritem struct { - address string - err error - } - ec := make(chan erritem, 1) - - wg.Add(len(p.hosts)) + var ( + wg sync.WaitGroup + result error + mu sync.Mutex + ) for _, h := range p.hosts { + wg.Add(1) go func(h connectable) { - ec <- erritem{h.String(), p.connectHost(h)} + defer wg.Done() + if err := p.connectHost(h); err != nil { + mu.Lock() + result = errors.Join(result, fmt.Errorf("connect %s: %w", h, err)) + mu.Unlock() + } }(h) } - go func() { - for e := range ec { - if e.err != nil { - errors = append(errors, fmt.Sprintf("%s: %s", e.address, e.err.Error())) - } - wg.Done() - } - }() - wg.Wait() - if len(errors) > 0 { - return fmt.Errorf("failed on %d hosts:\n - %s", len(errors), strings.Join(errors, "\n - ")) + if result != nil { + return fmt.Errorf("failed to connect all hosts: %w", result) } return nil @@ -87,14 +79,17 @@ func (p *Connect) Run() error { const retries = 60 -func (p *Connect) connectHost(h connectable) error { +func (p *Connect) connectHost(host connectable) error { err := retry.Do( func() error { - return h.Connect() + if err := host.Connect(); err != nil { + return fmt.Errorf("connect: %w", err) + } + return nil }, retry.OnRetry( func(n uint, err error) { - log.Errorf("%s: attempt %d of %d.. failed to connect: %s", h, n+1, retries, err.Error()) + log.Errorf("%s: attempt %d of %d.. failed to connect: %s", host, n+1, retries, err.Error()) }, ), retry.RetryIf( @@ -107,19 +102,18 @@ func (p *Connect) connectHost(h connectable) error { retry.Delay(time.Second*3), retry.Attempts(retries), ) - if err != nil { - return err + return fmt.Errorf("failed to connect: %w", err) } - return p.testConnection(h) + return p.testConnection(host) } func (p *Connect) testConnection(h connectable) error { log.Infof("%s: testing connection", h) if err := h.Exec("echo"); err != nil { - return err + return fmt.Errorf("failed to test connection to %s: %w", h, err) } return nil diff --git a/pkg/product/common/phase/run_hooks.go b/pkg/product/common/phase/run_hooks.go index 9994c507f..60c7a0f0b 100644 --- a/pkg/product/common/phase/run_hooks.go +++ b/pkg/product/common/phase/run_hooks.go @@ -1,9 +1,9 @@ package phase import ( + "errors" "fmt" "reflect" - "strings" "sync" "unicode" "unicode/utf8" @@ -31,16 +31,21 @@ func (p *RunHooks) Prepare(config interface{}) error { spec := r.FieldByName("Spec").Elem() hosts := spec.FieldByName("Hosts") for i := 0; i < hosts.Len(); i++ { - h := hosts.Index(i) - hooksF := h.Elem().FieldByName("Hooks") + hostVal := hosts.Index(i) + hooksF := hostVal.Elem().FieldByName("Hooks") if hooksF.IsNil() { continue } - hooksI := hooksF.Interface().(common.Hooks) + hooksI, ok := hooksF.Interface().(common.Hooks) + if !ok { + continue + } if action := hooksI[p.Action]; action != nil { if steps := action[p.Stage]; steps != nil { - he := h.Interface().(host) - p.steps[he] = steps + he, ok := hostVal.Interface().(host) + if ok { + p.steps[he] = steps + } } } } @@ -68,35 +73,29 @@ func (p *RunHooks) Title() string { // Run does all the prep work on the hosts in parallel. func (p *RunHooks) Run() error { - var wg sync.WaitGroup - var errors []string - type erritem struct { - host string - err error - } - ec := make(chan erritem, 1) - - wg.Add(len(p.steps)) + var ( + wg sync.WaitGroup + result error + mu sync.Mutex + ) for h, steps := range p.steps { - go func(h host, steps []string) { - ec <- erritem{h.String(), h.ExecAll(steps)} - }(h, steps) - } - - go func() { - for e := range ec { - if e.err != nil { - errors = append(errors, fmt.Sprintf("%s: %s", e.host, e.err.Error())) + h, steps := h, steps // capture range variables + wg.Add(1) + go func() { + defer wg.Done() + if err := h.ExecAll(steps); err != nil { + mu.Lock() + result = errors.Join(result, fmt.Errorf("%s: %w", h.String(), err)) + mu.Unlock() } - wg.Done() - } - }() + }() + } wg.Wait() - if len(errors) > 0 { - return fmt.Errorf("failed on %d hosts:\n - %s", len(errors), strings.Join(errors, "\n - ")) + if result != nil { + return fmt.Errorf("hook execution failed: %w", result) } return nil diff --git a/pkg/product/mke/api/cluster.go b/pkg/product/mke/api/cluster.go index 0d158eb7f..035778322 100644 --- a/pkg/product/mke/api/cluster.go +++ b/pkg/product/mke/api/cluster.go @@ -1,6 +1,8 @@ package api import ( + "fmt" + "github.com/Mirantis/mcc/pkg/constant" "github.com/Mirantis/mcc/pkg/docker/hub" common "github.com/Mirantis/mcc/pkg/product/common/api" @@ -32,7 +34,7 @@ func (c *ClusterConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { yc := (*spec)(c) if err := unmarshal(yc); err != nil { - return err + return fmt.Errorf("failed to unmarshal cluster config: %w", err) } return nil @@ -43,11 +45,17 @@ func (c *ClusterConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { func (c *ClusterConfig) Validate() error { validator := validator.New(validator.WithRequiredStructEnabled()) validator.RegisterStructValidation(roleChecks, ClusterSpec{}) - return validator.Struct(c) + if err := validator.Struct(c); err != nil { + return fmt.Errorf("cluster config validation failed: %w", err) + } + return nil } func roleChecks(sl validator.StructLevel) { - spec := sl.Current().Interface().(ClusterSpec) + spec, ok := sl.Current().Interface().(ClusterSpec) + if !ok { + return + } hosts := spec.Hosts if hosts.Count(func(h *Host) bool { return h.Role == "manager" }) == 0 { sl.ReportError(hosts, "hosts", "", "manager required", "") diff --git a/pkg/product/mke/api/cluster_spec.go b/pkg/product/mke/api/cluster_spec.go index 0efa74d22..75629fbbf 100644 --- a/pkg/product/mke/api/cluster_spec.go +++ b/pkg/product/mke/api/cluster_spec.go @@ -1,7 +1,9 @@ package api import ( + "errors" "fmt" + "net/http" "net/url" "strconv" "strings" @@ -63,6 +65,8 @@ func (c *ClusterSpec) SwarmLeader() *Host { return m.First() } +var errGenerateURL = errors.New("unable to generate url") + // MKEURL returns a URL for MKE or an error if one can not be generated. func (c *ClusterSpec) MKEURL() (*url.URL, error) { // Easy route, user has provided one in MSR --ucp-url @@ -73,7 +77,7 @@ func (c *ClusterSpec) MKEURL() (*url.URL, error) { } u, err := url.Parse(f) if err != nil { - return nil, fmt.Errorf("invalid MSR --ucp-url install flag '%s': %s", f, err.Error()) + return nil, fmt.Errorf("invalid MSR --ucp-url install flag '%s': %w", f, err) } if u.Path == "" { u.Path = "/" @@ -90,7 +94,7 @@ func (c *ClusterSpec) MKEURL() (*url.URL, error) { // Option 3: Use the first manager's address mgrs := c.Managers() if len(mgrs) < 1 { - return nil, fmt.Errorf("unable to generate a url for mke") + return nil, fmt.Errorf("%w: mke managers count is zero", errGenerateURL) } mkeAddr = mgrs[0].Address() } @@ -98,7 +102,7 @@ func (c *ClusterSpec) MKEURL() (*url.URL, error) { if portstr := c.MKE.InstallFlags.GetValue("--controller-port"); portstr != "" { p, err := strconv.Atoi(portstr) if err != nil { - return nil, fmt.Errorf("invalid mke controller-port value: '%s': %s", portstr, err.Error()) + return nil, fmt.Errorf("invalid mke controller-port value: '%s': %w", portstr, err) } mkeAddr = fmt.Sprintf("%s:%d", mkeAddr, p) } @@ -120,7 +124,7 @@ func (c *ClusterSpec) MSRURL() (*url.URL, error) { } u, err := url.Parse(f) if err != nil { - return nil, fmt.Errorf("invalid MSR --dtr-external-url install flag '%s': %s", f, err.Error()) + return nil, fmt.Errorf("invalid MSR --dtr-external-url install flag '%s': %w", f, err) } if u.Scheme == "" { u.Scheme = "https" @@ -137,7 +141,7 @@ func (c *ClusterSpec) MSRURL() (*url.URL, error) { // Otherwise, use MSRLeaderAddress msrLeader := c.MSRLeader() if msrLeader == nil { - return nil, fmt.Errorf("unable to generate a MSR URL - no MSR nodes found") + return nil, fmt.Errorf("%w: no MSR nodes found", errGenerateURL) } msrAddr = msrLeader.Address() @@ -145,7 +149,7 @@ func (c *ClusterSpec) MSRURL() (*url.URL, error) { if portstr := c.MSR.InstallFlags.GetValue("--replica-https-port"); portstr != "" { p, err := strconv.Atoi(portstr) if err != nil { - return nil, fmt.Errorf("invalid msr --replica-https-port value '%s': %s", portstr, err.Error()) + return nil, fmt.Errorf("invalid msr --replica-https-port value '%s': %w", portstr, err) } msrAddr = fmt.Sprintf("%s:%d", msrAddr, p) } @@ -158,29 +162,29 @@ func (c *ClusterSpec) MSRURL() (*url.URL, error) { }, nil } +var errInvalidConfig = errors.New("invalid configuration") + // UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml. func (c *ClusterSpec) UnmarshalYAML(unmarshal func(interface{}) error) error { type spec ClusterSpec - yc := (*spec)(c) + specAlias := (*spec)(c) c.MCR = common.MCRConfig{} c.MKE = NewMKEConfig() - if err := unmarshal(yc); err != nil { + if err := unmarshal(specAlias); err != nil { return err } if c.Hosts.Count(func(h *Host) bool { return h.Role == "msr" }) > 0 { - if yc.MSR == nil { - return fmt.Errorf("configuration error: hosts with msr role present, but no spec.msr defined") - } - if err := defaults.Set(yc.MSR); err != nil { - return err + if specAlias.MSR == nil { + return fmt.Errorf("%w: hosts with msr role present, but no spec.msr defined", errInvalidConfig) } - } else { - if yc.MSR != nil { - yc.MSR = nil - log.Debugf("ignoring spec.msr configuration as there are no hosts having the msr role") + if err := defaults.Set(specAlias.MSR); err != nil { + return fmt.Errorf("set defaults: %w", err) } + } else if specAlias.MSR != nil { + specAlias.MSR = nil + log.Debugf("ignoring spec.msr configuration as there are no hosts having the msr role") } bastionHosts := c.Hosts.Filter(func(h *Host) bool { @@ -209,7 +213,10 @@ func (c *ClusterSpec) UnmarshalYAML(unmarshal func(interface{}) error) error { } } - return defaults.Set(c) + if err := defaults.Set(c); err != nil { + return fmt.Errorf("set defaults: %w", err) + } + return nil } func isSwarmLeader(h *Host) bool { @@ -256,13 +263,20 @@ func (c *ClusterSpec) CheckMKEHealthRemote(h *Host) error { } u.Path = "/_ping" - return retry.Do( + err = retry.Do( func() error { log.Infof("%s: waiting for MKE at %s to become healthy", h, u.Host) - return h.CheckHTTPStatus(u.String(), 200) + if err := h.CheckHTTPStatus(u.String(), http.StatusOK); err != nil { + return fmt.Errorf("check http status: %w", err) + } + return nil }, retry.Attempts(12), // last attempt should wait ~7min ) + if err != nil { + return fmt.Errorf("MKE health check failed: %w", err) + } + return nil } // CheckMKEHealthLocal will check the local mke health on a host and return an error if it failed. @@ -272,13 +286,20 @@ func (c *ClusterSpec) CheckMKEHealthLocal(h *Host) error { host = host + ":" + port } - return retry.Do( + err := retry.Do( func() error { log.Infof("%s: waiting for MKE to become healthy", h) - return h.CheckHTTPStatus(fmt.Sprintf("https://%s/_ping", host), 200) + if err := h.CheckHTTPStatus(fmt.Sprintf("https://%s/_ping", host), http.StatusOK); err != nil { + return fmt.Errorf("check http status: %w", err) + } + return nil }, retry.Attempts(12), // last attempt should wait ~7min ) + if err != nil { + return fmt.Errorf("MKE health check failed: %w", err) + } + return nil } // ContainsMSR returns true when the config has msr hosts. diff --git a/pkg/product/mke/api/cluster_test.go b/pkg/product/mke/api/cluster_test.go index 9f639152f..2fc3fa9af 100644 --- a/pkg/product/mke/api/cluster_test.go +++ b/pkg/product/mke/api/cluster_test.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "errors" "os" "strings" "testing" @@ -380,8 +381,8 @@ spec: ` c := loadYaml(t, data) - require.Equal(t, "root", c.Spec.Hosts[0].SSH.User) - require.Equal(t, 22, c.Spec.Hosts[0].SSH.Port) + require.Equal(t, c.Spec.Hosts[0].SSH.User, "root") + require.Equal(t, c.Spec.Hosts[0].SSH.Port, 22) } func TestHostWinRMDefaults(t *testing.T) { @@ -487,7 +488,10 @@ func validateErrorField(t *testing.T, err error, field string) { } func getAllErrorFields(err error) []string { - validationErrors := err.(validator.ValidationErrors) + var validationErrors validator.ValidationErrors + if !errors.As(err, &validationErrors) { + return nil + } fields := make([]string, len(validationErrors)) // Collect all fields that failed validation diff --git a/pkg/product/mke/api/host.go b/pkg/product/mke/api/host.go index 806e0ed1d..71ff7583c 100644 --- a/pkg/product/mke/api/host.go +++ b/pkg/product/mke/api/host.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "errors" "fmt" "os" "reflect" @@ -15,7 +16,6 @@ import ( "github.com/k0sproject/dig" "github.com/k0sproject/rig" "github.com/k0sproject/rig/os/registry" - log "github.com/sirupsen/logrus" ) @@ -40,23 +40,23 @@ type MSRMetadata struct { ReplicaID string } -type errors struct { +type errs struct { errors []string } -func (errors *errors) Count() int { +func (errors *errs) Count() int { return len(errors.errors) } -func (errors *errors) Add(e string) { +func (errors *errs) Add(e string) { errors.errors = append(errors.errors, e) } -func (errors *errors) Addf(template string, args ...interface{}) { +func (errors *errs) Addf(template string, args ...interface{}) { errors.errors = append(errors.errors, fmt.Sprintf(template, args...)) } -func (errors *errors) String() string { +func (errors *errs) String() string { if errors.Count() == 0 { return "" } @@ -78,7 +78,7 @@ type Host struct { Metadata *HostMetadata `yaml:"-"` MSRMetadata *MSRMetadata `yaml:"-"` Configurer HostConfigurer `yaml:"-"` - Errors errors `yaml:"-"` + Errors errs `yaml:"-"` } // UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml. @@ -94,7 +94,10 @@ func (h *Host) UnmarshalYAML(unmarshal func(interface{}) error) error { log.Warnf("%s: spec.hosts[*].ssh.hostKey is deprecated, please use ssh known hosts file instead (.ssh/config, SSH_KNOWN_HOSTS)", h) } - return defaults.Set(h) + if err := defaults.Set(yh); err != nil { + return fmt.Errorf("failed to set host defaults: %w", err) + } + return nil } // IsLocal returns true for localhost connections. @@ -109,7 +112,7 @@ func (h *Host) ExecAll(cmds []string) error { output, err := h.ExecOutput(cmd) if err != nil { log.Errorf("%s: %s", h, strings.ReplaceAll(output, "\n", fmt.Sprintf("\n%s: ", h))) - return err + return fmt.Errorf("failed to execute step command: %w", err) } if strings.TrimSpace(output) != "" { log.Infof("%s: %s", h, strings.ReplaceAll(output, "\n", fmt.Sprintf("\n%s: ", h))) @@ -118,20 +121,24 @@ func (h *Host) ExecAll(cmds []string) error { return nil } +var errAuthFailed = errors.New("authentication failed") + // AuthenticateDocker performs a docker login on the host using local REGISTRY_USERNAME // and REGISTRY_PASSWORD when set. func (h *Host) AuthenticateDocker(imageRepo string) error { if user := os.Getenv("REGISTRY_USERNAME"); user != "" { pass := os.Getenv("REGISTRY_PASSWORD") if pass == "" { - return fmt.Errorf("REGISTRY_PASSWORD not set") + return fmt.Errorf("%w: REGISTRY_PASSWORD not set", errAuthFailed) } log.Infof("%s: authenticating docker for image repo %s", h, imageRepo) if strings.HasPrefix(imageRepo, "docker.io/") { // docker.io is a special case for auth imageRepo = "" } - return h.Configurer.AuthenticateDocker(h, user, pass, imageRepo) + if err := h.Configurer.AuthenticateDocker(h, user, pass, imageRepo); err != nil { + return fmt.Errorf("%w: %s", errAuthFailed, err.Error()) + } } return nil } @@ -145,22 +152,24 @@ func (h *Host) SwarmAddress() string { func (h *Host) MCRVersion() (string, error) { version, err := h.ExecOutput(h.Configurer.DockerCommandf(`version -f "{{.Server.Version}}"`)) if err != nil { - return "", fmt.Errorf("failed to get container runtime version: %s", err.Error()) + return "", fmt.Errorf("failed to get container runtime version: %w", err) } return version, nil } +var errUnexpectedResponse = errors.New("unexpected response") + // CheckHTTPStatus will perform a web request to the url and return an error if the http status is not the expected. func (h *Host) CheckHTTPStatus(url string, expected int) error { status, err := h.Configurer.HTTPStatus(h, url) if err != nil { - return err + return fmt.Errorf("failed to get http status: %w", err) } log.Debugf("%s: response code: %d, expected %d", h, status, expected) if status != expected { - return fmt.Errorf("unexpected response code %d", status) + return fmt.Errorf("%w: code %d", errUnexpectedResponse, status) } return nil @@ -172,14 +181,14 @@ func (h *Host) WriteFileLarge(src, dst string) error { startTime := time.Now() stat, err := os.Stat(src) if err != nil { - return err + return fmt.Errorf("failed to stat file: %w", err) } size := stat.Size() log.Infof("%s: uploading %s to %s", h, util.FormatBytes(uint64(stat.Size())), dst) if err := h.Connection.Upload(src, dst); err != nil { - return fmt.Errorf("upload failed: %s", err.Error()) + return fmt.Errorf("upload failed: %w", err) } duration := time.Since(startTime).Seconds() @@ -194,22 +203,29 @@ func (h *Host) Reconnect() error { h.Disconnect() log.Infof("%s: waiting for reconnection", h) - return retry.Do( + err := retry.Do( func() error { - return h.Connect() + if err := h.Connect(); err != nil { + return fmt.Errorf("failed to reconnect: %w", err) + } + return nil }, retry.DelayType(retry.CombineDelay(retry.FixedDelay, retry.RandomDelay)), retry.MaxJitter(time.Second*2), retry.Delay(time.Second*3), retry.Attempts(60), ) + if err != nil { + return fmt.Errorf("retry count exceeded: %w", err) + } + return nil } // Reboot reboots the host and waits for it to become responsive. func (h *Host) Reboot() error { log.Infof("%s: rebooting", h) if err := h.Configurer.Reboot(h); err != nil { - return err + return fmt.Errorf("failed to reboot: %w", err) } log.Infof("%s: waiting for host to go offline", h) if err := h.waitForHost(false); err != nil { @@ -219,7 +235,7 @@ func (h *Host) Reboot() error { log.Infof("%s: waiting for reconnection", h) if err := h.Reconnect(); err != nil { - return fmt.Errorf("unable to reconnect after reboot") + return fmt.Errorf("unable to reconnect after reboot: %w", err) } log.Infof("%s: waiting for host to become active", h) @@ -228,7 +244,7 @@ func (h *Host) Reboot() error { } if err := h.Reconnect(); err != nil { - return fmt.Errorf("unable to reconnect after reboot: %s", err.Error()) + return fmt.Errorf("unable to reconnect after reboot: %w", err) } return nil @@ -277,7 +293,7 @@ func (h *Host) ConfigureMCR() error { } if err := h.Configurer.WriteFile(h, cfgPath, daemonJSONContent, "0600"); err != nil { - return err + return fmt.Errorf("failed to write daemon.json: %w", err) } if h.Metadata.MCRVersion != "" { @@ -288,15 +304,17 @@ func (h *Host) ConfigureMCR() error { return nil } +var errUnexpectedState = errors.New("unexpected state") + // when state is true wait for host to become active, when state is false, wait for connection to go down. func (h *Host) waitForHost(state bool) error { err := retry.Do( func() error { err := h.Exec("echo") if !state && err == nil { - return fmt.Errorf("still online") + return fmt.Errorf("%w: still online", errUnexpectedState) } else if state && err != nil { - return fmt.Errorf("still offline") + return fmt.Errorf("%w: still offline", errUnexpectedState) } return nil }, @@ -306,16 +324,18 @@ func (h *Host) waitForHost(state bool) error { retry.Attempts(60), ) if err != nil { - return fmt.Errorf("failed to wait for host to go offline") + return fmt.Errorf("failed to wait for host to go offline: %w", err) } return nil } +var errUnsupportedOS = errors.New("unsupported OS") + // ResolveConfigurer assigns a rig-style configurer to the Host (see configurer/). func (h *Host) ResolveConfigurer() error { bf, err := registry.GetOSModuleBuilder(*h.OSVersion) if err != nil { - return err + return fmt.Errorf("%w: failed to get OS module builder: %w", errUnsupportedOS, err) } if c, ok := bf().(HostConfigurer); ok { @@ -324,5 +344,5 @@ func (h *Host) ResolveConfigurer() error { return nil } - return fmt.Errorf("unsupported OS") + return errUnsupportedOS } diff --git a/pkg/product/mke/api/hosts.go b/pkg/product/mke/api/hosts.go index e57cf79d6..3840af15b 100644 --- a/pkg/product/mke/api/hosts.go +++ b/pkg/product/mke/api/hosts.go @@ -1,8 +1,8 @@ package api import ( + "errors" "fmt" - "strings" "sync" ) @@ -77,7 +77,7 @@ func (hosts *Hosts) IndexAll(filter func(h *Host) bool) []int { func (hosts *Hosts) Each(filter func(h *Host) error) error { for _, h := range *hosts { if err := filter(h); err != nil { - return fmt.Errorf("%s: %s", h, err.Error()) + return fmt.Errorf("%s: %w", h, err) } } return nil @@ -86,38 +86,27 @@ func (hosts *Hosts) Each(filter func(h *Host) error) error { // ParallelEach runs a function on every Host parallelly. The function should return nil or an error. // Any errors will be concatenated and returned. func (hosts *Hosts) ParallelEach(filter func(h *Host) error) error { - var wg sync.WaitGroup - var errors []string - type erritem struct { - address string - err error - } - ec := make(chan erritem, 1) - - wg.Add(len(*hosts)) + var ( + wg sync.WaitGroup + result error + mu sync.Mutex + ) for _, h := range *hosts { + wg.Add(1) go func(h *Host) { - ec <- erritem{h.String(), filter(h)} + defer wg.Done() + if err := filter(h); err != nil { + mu.Lock() + result = errors.Join(result, fmt.Errorf("%s: %w", h, err)) + mu.Unlock() + } }(h) } - go func() { - for e := range ec { - if e.err != nil { - errors = append(errors, fmt.Sprintf("%s: %s", e.address, e.err.Error())) - } - wg.Done() - } - }() - wg.Wait() - if len(errors) > 0 { - return fmt.Errorf("failed on %d hosts:\n - %s", len(errors), strings.Join(errors, "\n - ")) - } - - return nil + return result } // Map returns a new slice which is the result of running the map function on each host. diff --git a/pkg/product/mke/api/mke_config.go b/pkg/product/mke/api/mke_config.go index a2f74840b..ed5922624 100644 --- a/pkg/product/mke/api/mke_config.go +++ b/pkg/product/mke/api/mke_config.go @@ -1,13 +1,13 @@ package api import ( + "errors" "fmt" "strings" "github.com/Mirantis/mcc/pkg/constant" common "github.com/Mirantis/mcc/pkg/product/common/api" "github.com/Mirantis/mcc/pkg/util" - "github.com/hashicorp/go-version" ) @@ -54,6 +54,8 @@ type MKECloud struct { ConfigData string `yaml:"configData,omitempty"` } +var errMKEConfigInvalid = errors.New("invalid MKE config") + // UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml. func (c *MKEConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type mke MKEConfig @@ -68,7 +70,7 @@ func (c *MKEConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if raw.ConfigFile != "" { configData, err := util.LoadExternalFile(raw.ConfigFile) if err != nil { - return err + return fmt.Errorf("error in field spec.mke.configFile: %w", err) } raw.ConfigData = string(configData) } @@ -76,7 +78,7 @@ func (c *MKEConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if raw.Cloud != nil && raw.Cloud.ConfigFile != "" { cloudConfigData, err := util.LoadExternalFile(raw.Cloud.ConfigFile) if err != nil { - return err + return fmt.Errorf("error in field spec.mke.cloud.configFile: %w", err) } raw.Cloud.ConfigData = string(cloudConfigData) } @@ -86,7 +88,7 @@ func (c *MKEConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { raw.AdminUsername = flagValue raw.InstallFlags.Delete("--admin-username") } else if flagValue != raw.AdminUsername { - return fmt.Errorf("both Spec.mke.AdminUsername and Spec.mke.InstallFlags --admin-username set, only one allowed") + return fmt.Errorf("%w: both Spec.mke.AdminUsername and Spec.mke.InstallFlags --admin-username set, only one allowed", errMKEConfigInvalid) } } @@ -95,7 +97,7 @@ func (c *MKEConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { raw.AdminPassword = flagValue raw.InstallFlags.Delete("--admin-password") } else if flagValue != raw.AdminPassword { - return fmt.Errorf("both Spec.mke.AdminPassword and Spec.mke.InstallFlags --admin-password set, only one allowed") + return fmt.Errorf("%w: both Spec.mke.AdminPassword and Spec.mke.InstallFlags --admin-password set, only one allowed", errMKEConfigInvalid) } } @@ -104,7 +106,7 @@ func (c *MKEConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { raw.AdminUsername = flagValue raw.UpgradeFlags.Delete("--admin-username") } else if flagValue != raw.AdminUsername { - return fmt.Errorf("both Spec.mke.AdminUsername and Spec.mke.UpgradeFlags --admin-username set, only one allowed") + return fmt.Errorf("%w: both Spec.mke.AdminUsername and Spec.mke.UpgradeFlags --admin-username set, only one allowed", errMKEConfigInvalid) } } @@ -113,18 +115,18 @@ func (c *MKEConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { raw.AdminPassword = flagValue raw.UpgradeFlags.Delete("--admin-password") } else if flagValue != raw.AdminPassword { - return fmt.Errorf("both Spec.mke.AdminPassword and Spec.mke.UpgradeFlags --admin-password set, only one allowed") + return fmt.Errorf("%w: both Spec.mke.AdminPassword and Spec.mke.UpgradeFlags --admin-password set, only one allowed", errMKEConfigInvalid) } } if raw.SwarmInstallFlags.Include("--advertise-addr") { - return fmt.Errorf("Spec.mke.SwarmInstallFlags: specifying --advertise-addr is not allowed") + return fmt.Errorf("%w: spec.mke.SwarmInstallFlags: specifying --advertise-addr is not allowed", errMKEConfigInvalid) } if raw.CACertPath != "" { caCertData, err := util.LoadExternalFile(raw.CACertPath) if err != nil { - return err + return fmt.Errorf("failed to load CA cert file: %w", err) } raw.CACertData = string(caCertData) } @@ -132,7 +134,7 @@ func (c *MKEConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if raw.CertPath != "" { certData, err := util.LoadExternalFile(raw.CertPath) if err != nil { - return err + return fmt.Errorf("failed to load cert file: %w", err) } raw.CertData = string(certData) } @@ -140,7 +142,7 @@ func (c *MKEConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if raw.KeyPath != "" { keyData, err := util.LoadExternalFile(raw.KeyPath) if err != nil { - return err + return fmt.Errorf("failed to load key file: %w", err) } raw.KeyData = string(keyData) } @@ -151,12 +153,12 @@ func (c *MKEConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } if raw.Version == "" { - return fmt.Errorf("missing spec.mke.version") + return fmt.Errorf("%w: missing spec.mke.version", errMKEConfigInvalid) } v, err := version.NewVersion(raw.Version) if err != nil { - return fmt.Errorf("error in field spec.mke.version: %s", err.Error()) + return fmt.Errorf("%w: error in field spec.mke.version: %w", errMKEConfigInvalid, err) } if raw.ImageRepo == constant.ImageRepo && c.UseLegacyImageRepo(v) { @@ -176,23 +178,34 @@ func NewMKEConfig() MKEConfig { } // UseLegacyImageRepo returns true if the version number does not satisfy >=3.1.15 || >=3.2.8 || >=3.3.2. -func (c *MKEConfig) UseLegacyImageRepo(v *version.Version) bool { - +func (c *MKEConfig) UseLegacyImageRepo(mkeVersion *version.Version) bool { // Strip out anything after -, seems like go-version thinks // 3.1.16-rc1 does not satisfy >= 3.1.15 (nor >= 3.1.15-a) - vs := v.String() - var v2 *version.Version - if strings.Contains(vs, "-") { - v2, _ = version.NewVersion(vs[0:strings.Index(vs, "-")]) - } else { - v2 = v - } - - c1, _ := version.NewConstraint("< 3.2, >= 3.1.15") - c2, _ := version.NewConstraint("> 3.1, < 3.3, >= 3.2.8") - c3, _ := version.NewConstraint("> 3.3, < 3.4, >= 3.3.2") - c4, _ := version.NewConstraint(">= 3.4") - return !(c1.Check(v2) || c2.Check(v2) || c3.Check(v2) || c4.Check(v2)) + vs := mkeVersion.String() + if idx := strings.Index(vs, "-"); idx >= 0 { + vBase, err := version.NewVersion(vs[:idx]) + if err == nil { + mkeVersion = vBase + } + } + + constraints := []string{ + "< 3.2, >= 3.1.15", + "> 3.1, < 3.3, >= 3.2.8", + "> 3.2, < 3.4, >= 3.3.2", + ">= 3.4", + } + + for _, cs := range constraints { + constraint, err := version.NewConstraint(cs) + if err != nil { + return false + } + if constraint.Check(mkeVersion) { + return false + } + } + return true } // GetBootstrapperImage combines the bootstrapper image name based on user given config. diff --git a/pkg/product/mke/api/msr_config.go b/pkg/product/mke/api/msr_config.go index 9320dd6b3..a392e1667 100644 --- a/pkg/product/mke/api/msr_config.go +++ b/pkg/product/mke/api/msr_config.go @@ -26,6 +26,8 @@ type MSRConfig struct { KeyData string `yaml:"keyData,omitempty"` } +var errInvalidMSRConfig = fmt.Errorf("invalid MSR config") + // UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml. func (c *MSRConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type msr MSRConfig @@ -35,17 +37,17 @@ func (c *MSRConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } if c.Version == "" { - return fmt.Errorf("missing spec.msr.version") + return fmt.Errorf("%w: missing spec.msr.version", errInvalidMSRConfig) } if _, err := version.NewVersion(c.Version); err != nil { - return fmt.Errorf("error in field spec.msr.version: %s", err.Error()) + return fmt.Errorf("%w: error in field spec.msr.version: %w", errInvalidMSRConfig, err) } if c.CACertPath != "" { caCertData, err := util.LoadExternalFile(c.CACertPath) if err != nil { - return err + return fmt.Errorf("failed to load msr ca cert file: %w", err) } c.CACertData = string(caCertData) } @@ -53,7 +55,7 @@ func (c *MSRConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if c.CertPath != "" { certData, err := util.LoadExternalFile(c.CertPath) if err != nil { - return err + return fmt.Errorf("failed to load msr cert file: %w", err) } c.CertData = string(certData) } @@ -61,12 +63,15 @@ func (c *MSRConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if c.KeyPath != "" { keyData, err := util.LoadExternalFile(c.KeyPath) if err != nil { - return err + return fmt.Errorf("failed to load msr key file: %w", err) } c.KeyData = string(keyData) } - return defaults.Set(c) + if err := defaults.Set(c); err != nil { + return fmt.Errorf("set msr defaults: %w", err) + } + return nil } // SetDefaults sets default values. @@ -92,7 +97,6 @@ func (c *MSRConfig) GetBootstrapperImage() string { // UseLegacyImageRepo returns true if the version number does not satisfy >= 2.8.2 || >= 2.7.8 || >= 2.6.15. func (c *MSRConfig) UseLegacyImageRepo(v *version.Version) bool { - // Strip out anything after -, seems like go-version thinks vs := v.String() var v2 *version.Version diff --git a/pkg/product/mke/api/node.go b/pkg/product/mke/api/node.go index f8d18eb09..807ada939 100644 --- a/pkg/product/mke/api/node.go +++ b/pkg/product/mke/api/node.go @@ -5,7 +5,7 @@ import ( ) const ( - NODE_READY_STATE = "ready" + READY = "ready" ) // NodeDescriptionEnginePlugin node description struct for the engine plugin. @@ -111,5 +111,5 @@ type Node struct { // IsReady returns if node is in 'ready' state. func (n *Node) IsReady() bool { - return n.Status.State == NODE_READY_STATE + return n.Status.State == READY } diff --git a/pkg/product/mke/api/node_test.go b/pkg/product/mke/api/node_test.go index 8d098c008..09d548654 100644 --- a/pkg/product/mke/api/node_test.go +++ b/pkg/product/mke/api/node_test.go @@ -9,7 +9,7 @@ import ( func TestNodeReadyState(t *testing.T) { n := Node{ Status: NodeStatus{ - State: NODE_READY_STATE, + State: READY, }, } diff --git a/pkg/product/mke/apply.go b/pkg/product/mke/apply.go index 76025c985..252d2a208 100644 --- a/pkg/product/mke/apply.go +++ b/pkg/product/mke/apply.go @@ -1,14 +1,13 @@ package mke import ( - "crypto/sha1" + "crypto/sha1" //nolint:gosec // sha1 is used for simple analytics id generation "fmt" "github.com/Mirantis/mcc/pkg/analytics" "github.com/Mirantis/mcc/pkg/phase" common "github.com/Mirantis/mcc/pkg/product/common/phase" mke "github.com/Mirantis/mcc/pkg/product/mke/phase" - log "github.com/sirupsen/logrus" event "gopkg.in/segmentio/analytics-go.v3" ) @@ -59,7 +58,7 @@ func (p *MKE) Apply(disableCleanup, force bool, concurrency int) error { ) if err := phaseManager.Run(); err != nil { - return err + return fmt.Errorf("failed to apply MKE: %w", err) } windowsWorkersCount := 0 @@ -83,12 +82,10 @@ func (p *MKE) Apply(disableCleanup, force bool, concurrency int) error { "engine_version": p.ClusterConfig.Spec.MCR.Version, "cluster_id": clusterID, // send mke analytics user id as ucp_instance_id property - "ucp_instance_id": fmt.Sprintf("%x", sha1.Sum([]byte(clusterID))), + "ucp_instance_id": fmt.Sprintf("%x", sha1.Sum([]byte(clusterID))), //nolint:gosec // sha1 is used for simple analytics id generation } - if err := analytics.TrackEvent("Cluster Installed", props); err != nil { - log.Warnf("tracking failed: %v", err) - } + analytics.TrackEvent("Cluster Installed", props) return nil } diff --git a/pkg/product/mke/client_config.go b/pkg/product/mke/client_config.go index 5d0329284..521ca46c8 100644 --- a/pkg/product/mke/client_config.go +++ b/pkg/product/mke/client_config.go @@ -1,6 +1,8 @@ package mke import ( + "fmt" + "github.com/Mirantis/mcc/pkg/phase" common "github.com/Mirantis/mcc/pkg/product/common/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" @@ -9,7 +11,6 @@ import ( // ClientConfig downloads MKE client bundle. func (p *MKE) ClientConfig() error { - manager := p.ClusterConfig.Spec.Managers()[0] newHosts := make(api.Hosts, 1) newHosts[0] = manager @@ -25,5 +26,8 @@ func (p *MKE) ClientConfig() error { &common.Disconnect{}, ) - return phaseManager.Run() + if err := phaseManager.Run(); err != nil { + return fmt.Errorf("failed to download client bundle: %w", err) + } + return nil } diff --git a/pkg/product/mke/describe.go b/pkg/product/mke/describe.go index 56acb9432..f9ed30c44 100644 --- a/pkg/product/mke/describe.go +++ b/pkg/product/mke/describe.go @@ -1,6 +1,7 @@ package mke import ( + "fmt" "os" "github.com/Mirantis/mcc/pkg/phase" @@ -24,7 +25,10 @@ func (p *MKE) Describe(reportName string) error { if reportName == "config" { encoder := yaml.NewEncoder(os.Stdout) - return encoder.Encode(p.ClusterConfig) + if err := encoder.Encode(p.ClusterConfig); err != nil { + return fmt.Errorf("failed to encode cluster config: %w", err) + } + return nil } phaseManager := phase.NewManager(&p.ClusterConfig) @@ -38,5 +42,8 @@ func (p *MKE) Describe(reportName string) error { &de.Describe{MKE: mke, MSR: msr}, ) - return phaseManager.Run() + if err := phaseManager.Run(); err != nil { + return fmt.Errorf("failed to describe cluster: %w", err) + } + return nil } diff --git a/pkg/product/mke/exec.go b/pkg/product/mke/exec.go index 7493fc416..8b688b207 100644 --- a/pkg/product/mke/exec.go +++ b/pkg/product/mke/exec.go @@ -1,6 +1,7 @@ package mke import ( + "errors" "fmt" "io" "os" @@ -11,23 +12,25 @@ import ( "github.com/Mirantis/mcc/pkg/product/mke/api" "github.com/k0sproject/rig" "github.com/k0sproject/rig/exec" - log "github.com/sirupsen/logrus" ) +var errInvalidTarget = errors.New("invalid target") + // Exec runs commands or shell sessions on a configuration host. -func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, role, hostos, cmd string) error { +func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, role, hostos, cmd string) error { //nolint:maintidx var hosts api.Hosts for _, target := range targets { - if target == "localhost" { + switch { + case target == "localhost": hosts = append(hosts, &api.Host{Connection: rig.Connection{Localhost: &rig.Localhost{Enabled: true}}}) - } else if strings.Contains(target, ":") { + case strings.Contains(target, ":"): parts := strings.SplitN(target, ":", 2) addr := parts[0] port, err := strconv.Atoi(parts[1]) if err != nil { - return fmt.Errorf("invalid port: %s", parts[1]) + return fmt.Errorf("%w: invalid port: %s", errInvalidTarget, parts[1]) } host := p.ClusterConfig.Spec.Hosts.Find(func(h *api.Host) bool { @@ -40,15 +43,15 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol return h.SSH.Port == port }) if host == nil { - return fmt.Errorf("host %s not found in configuration", target) + return fmt.Errorf("%w: host %s not found in configuration", errInvalidTarget, target) } hosts = append(hosts, host) - } else { + default: host := p.ClusterConfig.Spec.Hosts.Find(func(h *api.Host) bool { return h.Address() == target }) if host == nil { - return fmt.Errorf("host %s not found in configuration", target) + return fmt.Errorf("%w: host %s not found in configuration", errInvalidTarget, target) } hosts = append(hosts, host) } @@ -72,10 +75,10 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol err := hosts.ParallelEach(func(h *api.Host) error { if err := h.Connect(); err != nil { - return err + return fmt.Errorf("failed to connect to host %s: %w", h.Address(), err) } if err := h.ResolveConfigurer(); err != nil { - return err + return fmt.Errorf("failed to resolve configurer for host %s: %w", h.Address(), err) } if h.IsWindows() { if hostos == "windows" { @@ -93,7 +96,7 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol return nil }) if err != nil { - return err + return fmt.Errorf("failed to filter hosts by OS: %w", err) } hosts = foundhosts } @@ -104,17 +107,17 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol if first { if len(hosts) == 0 { - return fmt.Errorf("no hosts found but --first given") + return fmt.Errorf("%w: no hosts found but --first given", errInvalidTarget) } hosts = hosts[0:1] } if len(hosts) > 1 { if !all { - return fmt.Errorf("found %d hosts but --all not given", len(hosts)) + return fmt.Errorf("%w: found %d hosts but --all not given", errInvalidTarget, len(hosts)) } if interactive { - return fmt.Errorf("can't use --interactive with multiple targets") + return fmt.Errorf("%w: can't use --interactive with multiple targets", errInvalidTarget) } } @@ -125,58 +128,85 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol var stdin string - stat, err := os.Stdin.Stat() - if err != nil { - return err - } - - if (stat.Mode() & os.ModeCharDevice) == 0 { - if interactive { - return fmt.Errorf("--interactive given but there's piped data in stdin") - } - data, err := io.ReadAll(os.Stdin) + if !interactive { + stat, err := os.Stdin.Stat() if err != nil { - return err + return fmt.Errorf("failed to stat stdin: %w", err) + } + + if (stat.Mode() & os.ModeCharDevice) == 0 { + data, err := io.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("failed to read stdin: %w", err) + } + stdin = string(data) } - stdin = string(data) } - if err := hosts.ParallelEach(func(h *api.Host) error { return h.Connect() }); err != nil { - return err + err := hosts.ParallelEach(func(h *api.Host) error { + if err := h.Connect(); err != nil { + return fmt.Errorf("connect to host %s: %w", h.Address(), err) + } + return nil + }) + if err != nil { + return fmt.Errorf("failed to connect to hosts: %w", err) } var linuxcount, windowscount int - hosts.Each(func(h *api.Host) error { + err = hosts.Each(func(h *api.Host) error { if h.IsWindows() { if linuxcount > 0 { - return fmt.Errorf("mixed target operating systems, use --os linux or --os windows") + return fmt.Errorf("%w mixed target operating systems, use --os linux or --os windows", errInvalidTarget) } windowscount++ } else { if windowscount > 0 { - return fmt.Errorf("mixed target operating systems, use --os linux or --os windows") + return fmt.Errorf("%w: mixed target operating systems, use --os linux or --os windows", errInvalidTarget) } linuxcount++ } return nil }) + if err != nil { + return fmt.Errorf("target operating system check failed: %w", err) + } if cmd == "" { if stdin != "" { - return fmt.Errorf("can't pipe to a remote shell without a command") + return fmt.Errorf("%w: can't pipe to a remote shell without a command", errInvalidTarget) } log.Tracef("assuming intention to run a shell with --interactive") - return hosts[0].Connection.ExecInteractive("") + err := hosts[0].Connection.ExecInteractive("") + if err != nil { + return fmt.Errorf("failed to run interactive shell: %w", err) + } } if interactive { log.Tracef("running interactive with cmd: %q", cmd) - return hosts[0].Connection.ExecInteractive(cmd) + if err := hosts[0].Connection.ExecInteractive(cmd); err != nil { + return fmt.Errorf("failed to run interactive shell: %w", err) + } + return nil } log.Tracef("running non-interactive with cmd: %q", cmd) + runFunc := func(h *api.Host) error { + if err := h.Exec(cmd, exec.Stdin(stdin), exec.StreamOutput()); err != nil { + return fmt.Errorf("failed on host %s: %w", h.Address(), err) + } + return nil + } if parallel { - return hosts.ParallelEach(func(h *api.Host) error { return h.Exec(cmd, exec.Stdin(stdin), exec.StreamOutput()) }) + err = hosts.ParallelEach(runFunc) + } else { + err = hosts.Each(runFunc) } - return hosts.Each(func(h *api.Host) error { return h.Exec(cmd, exec.Stdin(stdin), exec.StreamOutput()) }) + + if err != nil { + return fmt.Errorf("failed to run command on hosts: %w", err) + } + + return nil } diff --git a/pkg/product/mke/mke.go b/pkg/product/mke/mke.go index 6c565c3bf..2ec4d1a41 100644 --- a/pkg/product/mke/mke.go +++ b/pkg/product/mke/mke.go @@ -1,6 +1,8 @@ package mke import ( + "fmt" + "github.com/Mirantis/mcc/pkg/product/mke/api" "gopkg.in/yaml.v2" ) @@ -19,11 +21,11 @@ func (p *MKE) ClusterName() string { func NewMKE(data []byte) (*MKE, error) { c := api.ClusterConfig{} if err := yaml.UnmarshalStrict(data, &c); err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse cluster config: %w", err) } if err := c.Validate(); err != nil { - return nil, err + return nil, fmt.Errorf("failed to validate cluster config: %w", err) } return &MKE{ClusterConfig: c}, nil } diff --git a/pkg/product/mke/phase/authenticate_docker.go b/pkg/product/mke/phase/authenticate_docker.go index e51f15708..6ee9bbb67 100644 --- a/pkg/product/mke/phase/authenticate_docker.go +++ b/pkg/product/mke/phase/authenticate_docker.go @@ -1,6 +1,7 @@ package phase import ( + "fmt" "os" "github.com/Mirantis/mcc/pkg/phase" @@ -27,7 +28,14 @@ func (p *AuthenticateDocker) Title() string { func (p *AuthenticateDocker) Run() error { imageRepo := p.Config.Spec.MKE.ImageRepo - return phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, func(h *api.Host, c *api.ClusterConfig) error { - return h.AuthenticateDocker(imageRepo) + err := phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, func(h *api.Host, _ *api.ClusterConfig) error { + if err := h.AuthenticateDocker(imageRepo); err != nil { + return fmt.Errorf("%s: authenticate docker: %w", h, err) + } + return nil }) + if err != nil { + return fmt.Errorf("authentication failed: %w", err) + } + return nil } diff --git a/pkg/product/mke/phase/clean_up.go b/pkg/product/mke/phase/clean_up.go index c8ac7756b..db9e2dba5 100644 --- a/pkg/product/mke/phase/clean_up.go +++ b/pkg/product/mke/phase/clean_up.go @@ -1,6 +1,8 @@ package phase import ( + "fmt" + "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" ) @@ -19,15 +21,17 @@ func (p *CleanUp) Title() string { func (p *CleanUp) Run() error { err := phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, p.cleanupEnv) if err != nil { - return err + return fmt.Errorf("failed to cleanup environment: %w", err) } return nil } -func (p *CleanUp) cleanupEnv(h *api.Host, c *api.ClusterConfig) error { +func (p *CleanUp) cleanupEnv(h *api.Host, _ *api.ClusterConfig) error { if len(h.Environment) > 0 { - return h.Configurer.CleanupEnvironment(h, h.Environment) + if err := h.Configurer.CleanupEnvironment(h, h.Environment); err != nil { + return fmt.Errorf("failed to cleanup environment: %w", err) + } } return nil } diff --git a/pkg/product/mke/phase/configure_mcr.go b/pkg/product/mke/phase/configure_mcr.go index f9b0876ea..c908d031e 100644 --- a/pkg/product/mke/phase/configure_mcr.go +++ b/pkg/product/mke/phase/configure_mcr.go @@ -1,9 +1,10 @@ package phase import ( + "fmt" + "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" - log "github.com/sirupsen/logrus" ) @@ -20,7 +21,11 @@ func (p *ConfigureMCR) HostFilterFunc(h *api.Host) bool { // Prepare collects the hosts. func (p *ConfigureMCR) Prepare(config interface{}) error { - p.Config = config.(*api.ClusterConfig) + cfg, ok := config.(*api.ClusterConfig) + if !ok { + return errInvalidConfig + } + p.Config = cfg log.Debugf("collecting hosts for phase %s", p.Title()) hosts := p.Config.Spec.Hosts.Filter(p.HostFilterFunc) log.Debugf("found %d hosts for phase %s", len(hosts), p.Title()) @@ -38,8 +43,15 @@ func (p *ConfigureMCR) Run() error { p.EventProperties = map[string]interface{}{ "engine_version": p.Config.Spec.MCR.Version, } - return p.Hosts.ParallelEach(func(h *api.Host) error { + err := p.Hosts.ParallelEach(func(h *api.Host) error { log.Infof("%s: configuring container runtime", h) - return h.ConfigureMCR() + if err := h.ConfigureMCR(); err != nil { + return fmt.Errorf("failed to configure container runtime on %s: %w", h, err) + } + return nil }) + if err != nil { + return fmt.Errorf("failed to configure container runtime: %w", err) + } + return nil } diff --git a/pkg/product/mke/phase/describe.go b/pkg/product/mke/phase/describe.go index 745bd07c1..42b2ff04e 100644 --- a/pkg/product/mke/phase/describe.go +++ b/pkg/product/mke/phase/describe.go @@ -24,11 +24,12 @@ func (p *Describe) Title() string { // Run does the actual saving of the local state file. func (p *Describe) Run() error { - if p.MKE { + switch { + case p.MKE: p.mkeReport() - } else if p.MSR { + case p.MSR: p.msrReport() - } else { + default: p.hostReport() } @@ -40,12 +41,12 @@ func (p *Describe) mkeReport() { fmt.Println("Not installed") return } - w := new(tabwriter.Writer) + tabWriter := new(tabwriter.Writer) // minwidth, tabwidth, padding, padchar, flags - w.Init(os.Stdout, 8, 8, 1, '\t', 0) + tabWriter.Init(os.Stdout, 8, 8, 1, '\t', 0) - fmt.Fprintf(w, "%s\t%s\t\n", "VERSION", "ADMIN_UI") + fmt.Fprintf(tabWriter, "%s\t%s\t\n", "VERSION", "ADMIN_UI") uv := p.Config.Spec.MKE.Metadata.InstalledVersion mkeurl := "n/a" @@ -55,8 +56,8 @@ func (p *Describe) mkeReport() { mkeurl = url.String() } - fmt.Fprintf(w, "%s\t%s\t\n", uv, mkeurl) - w.Flush() + fmt.Fprintf(tabWriter, "%s\t%s\t\n", uv, mkeurl) + tabWriter.Flush() } func (p *Describe) msrReport() { @@ -66,12 +67,12 @@ func (p *Describe) msrReport() { return } - w := new(tabwriter.Writer) + tabWriter := new(tabwriter.Writer) // minwidth, tabwidth, padding, padchar, flags - w.Init(os.Stdout, 8, 8, 1, '\t', 0) + tabWriter.Init(os.Stdout, 8, 8, 1, '\t', 0) - fmt.Fprintf(w, "%s\t%s\t\n", "VERSION", "ADMIN_UI") + fmt.Fprintf(tabWriter, "%s\t%s\t\n", "VERSION", "ADMIN_UI") uv := msrLeader.MSRMetadata.InstalledVersion msrurl := "n/a" @@ -81,46 +82,46 @@ func (p *Describe) msrReport() { msrurl = url.String() } - fmt.Fprintf(w, "%s\t%s\t\n", uv, msrurl) - w.Flush() + fmt.Fprintf(tabWriter, "%s\t%s\t\n", uv, msrurl) + tabWriter.Flush() } func (p *Describe) hostReport() { - w := new(tabwriter.Writer) + tabWriter := new(tabwriter.Writer) // minwidth, tabwidth, padding, padchar, flags - w.Init(os.Stdout, 8, 8, 1, '\t', 0) + tabWriter.Init(os.Stdout, 8, 8, 1, '\t', 0) - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t\n", "ADDRESS", "INTERNAL_IP", "HOSTNAME", "ROLE", "OS", "RUNTIME") + fmt.Fprintf(tabWriter, "%s\t%s\t%s\t%s\t%s\t%s\t\n", "ADDRESS", "INTERNAL_IP", "HOSTNAME", "ROLE", "OS", "RUNTIME") for _, h := range p.Config.Spec.Hosts { - ev := "n/a" - os := "n/a" - ia := "n/a" - hn := "n/a" + mcrV := "n/a" + hostOS := "n/a" + internalAddr := "n/a" + hostname := "n/a" if h.Metadata != nil { if h.Metadata.MCRVersion != "" { - ev = h.Metadata.MCRVersion + mcrV = h.Metadata.MCRVersion } if h.OSVersion.ID != "" { - os = fmt.Sprintf("%s/%s", h.OSVersion.ID, h.OSVersion.Version) + hostOS = fmt.Sprintf("%s/%s", h.OSVersion.ID, h.OSVersion.Version) } if h.Metadata.InternalAddress != "" { - ia = h.Metadata.InternalAddress + internalAddr = h.Metadata.InternalAddress } if h.Metadata.Hostname != "" { - hn = h.Metadata.Hostname + hostname = h.Metadata.Hostname } } - fmt.Fprintf(w, + fmt.Fprintf(tabWriter, "%s\t%s\t%s\t%s\t%s\t%s\t\n", h.Address(), - ia, - hn, + internalAddr, + hostname, h.Role, - os, - ev, + hostOS, + mcrV, ) } - w.Flush() + tabWriter.Flush() } diff --git a/pkg/product/mke/phase/detect_os.go b/pkg/product/mke/phase/detect_os.go index e2ea98591..062fcbe96 100644 --- a/pkg/product/mke/phase/detect_os.go +++ b/pkg/product/mke/phase/detect_os.go @@ -1,7 +1,7 @@ package phase import ( - "github.com/Mirantis/mcc/pkg/product/mke/api" + "fmt" // anonymous import is needed to load the os configurers. _ "github.com/Mirantis/mcc/pkg/configurer/centos" @@ -15,9 +15,8 @@ import ( _ "github.com/Mirantis/mcc/pkg/configurer/ubuntu" // anonymous import is needed to load the os configurers. _ "github.com/Mirantis/mcc/pkg/configurer/windows" - "github.com/Mirantis/mcc/pkg/phase" - + "github.com/Mirantis/mcc/pkg/product/mke/api" log "github.com/sirupsen/logrus" ) @@ -34,13 +33,17 @@ func (p *DetectOS) Title() string { // Run the phase. func (p *DetectOS) Run() error { - return p.Config.Spec.Hosts.ParallelEach(func(h *api.Host) error { + err := p.Config.Spec.Hosts.ParallelEach(func(h *api.Host) error { if err := h.ResolveConfigurer(); err != nil { - return err + return fmt.Errorf("failed to resolve configurer for %s: %w", h, err) } os := h.OSVersion.String() log.Infof("%s: is running %s", h, os) return nil }) + if err != nil { + return fmt.Errorf("failed to detect OS: %w", err) + } + return nil } diff --git a/pkg/product/mke/phase/download_bundle.go b/pkg/product/mke/phase/download_bundle.go index 8f9e6bc1c..5f03f5bf6 100644 --- a/pkg/product/mke/phase/download_bundle.go +++ b/pkg/product/mke/phase/download_bundle.go @@ -14,7 +14,6 @@ import ( "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/util" "github.com/mitchellh/go-homedir" - log "github.com/sirupsen/logrus" ) @@ -28,6 +27,8 @@ func (p *DownloadBundle) Title() string { return "Download Client Bundle" } +var errInvalidConfig = fmt.Errorf("invalid config") + // Run collect all the facts from hosts in parallel. func (p *DownloadBundle) Run() error { m := p.Config.Spec.Managers()[0] @@ -39,30 +40,30 @@ func (p *DownloadBundle) Run() error { url, err := p.Config.Spec.MKEURL() if err != nil { - return err + return fmt.Errorf("get mke url: %w", err) } user := p.Config.Spec.MKE.AdminUsername if user == "" { - return fmt.Errorf("config Spec.MKE.AdminUsername not set") + return fmt.Errorf("%w: config Spec.MKE.AdminUsername not set", errInvalidConfig) } pass := p.Config.Spec.MKE.AdminPassword if pass == "" { - return fmt.Errorf("config Spec.MKE.AdminPassword not set") + return fmt.Errorf("%w: config Spec.MKE.AdminPassword not set", errInvalidConfig) } bundle, err := mke.GetClientBundle(url, tlsConfig, user, pass) if err != nil { - return fmt.Errorf("failed to download admin bundle: %s", err) + return fmt.Errorf("failed to download admin bundle: %w", err) } bundleDir, err := p.getBundleDir(p.Config.Metadata.Name, user) if err != nil { - return err + return fmt.Errorf("failed to get bundle directory: %w", err) } err = p.writeBundle(bundleDir, bundle) if err != nil { - return fmt.Errorf("failed to write admin bundle: %s", err) + return fmt.Errorf("failed to write admin bundle: %w", err) } return nil @@ -71,44 +72,61 @@ func (p *DownloadBundle) Run() error { func (p *DownloadBundle) getBundleDir(clusterName, username string) (string, error) { home, err := homedir.Dir() if err != nil { - return "", err + return "", fmt.Errorf("failed to get home directory: %w", err) } return path.Join(home, constant.StateBaseDir, "cluster", clusterName, "bundle", username), nil } +var errInvalidBundle = fmt.Errorf("invalid bundle") + +func safePath(base, rel string) (string, error) { + abs, err := filepath.Abs(filepath.Join(base, rel)) + if err != nil { + return "", fmt.Errorf("%w: error while getting absolute path: %w", errInvalidBundle, err) + } + if !strings.HasPrefix(abs, base) { + return "", fmt.Errorf("%w: zip slip detected", errInvalidBundle) + } + return abs, nil +} + func (p *DownloadBundle) writeBundle(bundleDir string, bundle *zip.Reader) error { if err := util.EnsureDir(bundleDir); err != nil { return fmt.Errorf("error while creating directory: %w", err) } log.Debugf("Writing out bundle to %s", bundleDir) - for _, zf := range bundle.File { - src, err := zf.Open() + for _, zipFile := range bundle.File { + src, err := zipFile.Open() if err != nil { - return err + return fmt.Errorf("error while opening file %s: %w", zipFile.Name, err) } defer src.Close() var data []byte data, err = io.ReadAll(src) if err != nil { - return err + return fmt.Errorf("error while reading file %s: %w", zipFile.Name, err) } - mode := int64(0644) - if strings.Contains(zf.Name, "key.pem") { - mode = 0600 + mode := int64(0o644) + if strings.Contains(zipFile.Name, "key.pem") { + mode = 0o600 } // mke bundle will contain folders as well as files, if folder exists fd will not be empty - dir, _ := filepath.Split(zf.Name) - if dir != "" { - if err := os.MkdirAll(filepath.Join(bundleDir, dir), 0700); err != nil { - return err + if dir := filepath.Dir(zipFile.Name); dir != "" && dir != "." { + if err := os.MkdirAll(filepath.Join(bundleDir, dir), 0o700); err != nil { + return fmt.Errorf("error while creating directory: %w", err) } } - err = os.WriteFile(filepath.Join(bundleDir, zf.Name), data, os.FileMode(mode)) + outFile, err := safePath(bundleDir, zipFile.Name) if err != nil { return err } + + err = os.WriteFile(outFile, data, os.FileMode(mode)) + if err != nil { + return fmt.Errorf("error while writing file %s: %w", zipFile.Name, err) + } } log.Infof("Successfully wrote client bundle to %s", bundleDir) return nil diff --git a/pkg/product/mke/phase/download_installer.go b/pkg/product/mke/phase/download_installer.go index d42992740..f7f7fceb8 100644 --- a/pkg/product/mke/phase/download_installer.go +++ b/pkg/product/mke/phase/download_installer.go @@ -36,28 +36,28 @@ func (p *DownloadInstaller) Run() error { } f, err := os.CreateTemp("", "installerLinux") if err != nil { - return err + return fmt.Errorf("failed to create temporary file: %w", err) } _, err = f.WriteString(linuxScript) if err != nil { - return err + return fmt.Errorf("failed to write to temporary file: %w", err) } p.linuxPath = f.Name() if p.Config.Spec.Hosts.Count(func(h *api.Host) bool { return h.IsWindows() }) > 0 { winScript, err := p.getScript(p.Config.Spec.MCR.InstallURLWindows) if err != nil { - return err + return fmt.Errorf("failed to get Windows installer script: %w", err) } f, err := os.CreateTemp("", "installerWindows") if err != nil { - return err + return fmt.Errorf("failed to create temporary file for windows installer script: %w", err) } _, err = f.WriteString(winScript) if err != nil { - return err + return fmt.Errorf("failed to write to temporary file for windows installer script: %w", err) } p.winPath = f.Name() } @@ -78,9 +78,15 @@ func (p *DownloadInstaller) parseURL(uri string) (*url.URL, error) { return &url.URL{Path: uri, Scheme: "file"}, nil } - return url.ParseRequestURI(uri) + u, err := url.ParseRequestURI(uri) + if err != nil { + return nil, fmt.Errorf("failed to parse installer URL: %w", err) + } + return u, nil } +var errInvalidScript = fmt.Errorf("invalid container runtime install script") + func (p *DownloadInstaller) getScript(uri string) (string, error) { u, err := p.parseURL(uri) if err != nil { @@ -103,7 +109,7 @@ func (p *DownloadInstaller) getScript(uri string) (string, error) { if len(data) < 10 { // cant fit an installer into that! - return "", fmt.Errorf("invalid container runtime install script in %s", uri) + return "", fmt.Errorf("%w: script is too short", errInvalidScript) } if !strings.HasPrefix(data, "#") { @@ -115,15 +121,15 @@ func (p *DownloadInstaller) getScript(uri string) (string, error) { func (p *DownloadInstaller) downloadFile(url string) (string, error) { log.Infof("downloading container runtime install script from %s", url) - resp, err := http.Get(url) + resp, err := http.Get(url) //nolint:gosec // "G107: Url provided to HTTP request as taint input" -- user-provided URL is ok here if err != nil { - return "", err + return "", fmt.Errorf("failed to download container runtime install script: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return "", err + return "", fmt.Errorf("failed to read response body: %w", err) } return string(body), nil } diff --git a/pkg/product/mke/phase/gather_facts.go b/pkg/product/mke/phase/gather_facts.go index 5cb719fbc..a8df7b753 100644 --- a/pkg/product/mke/phase/gather_facts.go +++ b/pkg/product/mke/phase/gather_facts.go @@ -2,28 +2,28 @@ package phase import ( "encoding/json" + "errors" "fmt" "net" - "github.com/Mirantis/mcc/pkg/mke" - "github.com/Mirantis/mcc/pkg/msr" - "github.com/Mirantis/mcc/pkg/phase" - "github.com/Mirantis/mcc/pkg/product/mke/api" - "github.com/Mirantis/mcc/pkg/swarm" - "github.com/k0sproject/dig" - // needed to load the build func in package init. _ "github.com/Mirantis/mcc/pkg/configurer/centos" // needed to load the build func in package init. _ "github.com/Mirantis/mcc/pkg/configurer/enterpriselinux" // needed to load the build func in package init. - _ "github.com/Mirantis/mcc/pkg/configurer/ubuntu" - // needed to load the build func in package init. _ "github.com/Mirantis/mcc/pkg/configurer/oracle" // needed to load the build func in package init. _ "github.com/Mirantis/mcc/pkg/configurer/sles" // needed to load the build func in package init. + _ "github.com/Mirantis/mcc/pkg/configurer/ubuntu" + // needed to load the build func in package init. _ "github.com/Mirantis/mcc/pkg/configurer/windows" + "github.com/Mirantis/mcc/pkg/mke" + "github.com/Mirantis/mcc/pkg/msr" + "github.com/Mirantis/mcc/pkg/phase" + "github.com/Mirantis/mcc/pkg/product/mke/api" + "github.com/Mirantis/mcc/pkg/swarm" + "github.com/k0sproject/dig" log "github.com/sirupsen/logrus" ) @@ -42,7 +42,7 @@ func (p *GatherFacts) Title() string { func (p *GatherFacts) Run() error { err := phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, p.investigateHost) if err != nil { - return err + return fmt.Errorf("failed to gather facts: %w", err) } // Gather MKE related facts @@ -52,7 +52,7 @@ func (p *GatherFacts) Run() error { if swarmLeader.Metadata.MCRVersion != "" { err := mke.CollectFacts(swarmLeader, p.Config.Spec.MKE.Metadata) if err != nil { - return fmt.Errorf("%s: failed to collect existing MKE details: %s", swarmLeader, err.Error()) + return fmt.Errorf("%s: failed to collect existing MKE details: %w", swarmLeader, err) } if p.Config.Spec.MKE.Metadata.Installed { log.Infof("%s: MKE has version %s", swarmLeader, p.Config.Spec.MKE.Metadata.InstalledVersion) @@ -68,7 +68,7 @@ func (p *GatherFacts) Run() error { } msrHosts := p.Config.Spec.MSRs() - msrHosts.ParallelEach(func(h *api.Host) error { + _ = msrHosts.ParallelEach(func(h *api.Host) error { if h.Metadata != nil && h.Metadata.MCRVersion != "" { msrMeta, err := msr.CollectFacts(h) if err != nil { @@ -88,14 +88,16 @@ func (p *GatherFacts) Run() error { return nil } -func (p *GatherFacts) investigateHost(h *api.Host, c *api.ClusterConfig) error { +var errInvalidIP = errors.New("invalid IP address") + +func (p *GatherFacts) investigateHost(h *api.Host, _ *api.ClusterConfig) error { log.Infof("%s: gathering host facts", h) if h.Metadata == nil { h.Metadata = &api.HostMetadata{} } if err := h.Configurer.CheckPrivilege(h); err != nil { - return err + return fmt.Errorf("privilege check failed: %w", err) } version, err := h.MCRVersion() @@ -125,7 +127,7 @@ func (p *GatherFacts) investigateHost(h *api.Host, c *api.ClusterConfig) error { if h.PrivateInterface == "" { i, err := h.Configurer.ResolvePrivateInterface(h) if err != nil { - return err + return fmt.Errorf("%s: failed to resolve private interface: %w", h, err) } log.Infof("%s: detected private interface '%s'", h, i) h.PrivateInterface = i @@ -133,10 +135,10 @@ func (p *GatherFacts) investigateHost(h *api.Host, c *api.ClusterConfig) error { a, err := h.Configurer.ResolveInternalIP(h, h.PrivateInterface, h.Address()) if err != nil { - return fmt.Errorf("%s: failed to resolve internal address: %s", h, err.Error()) + return fmt.Errorf("%s: failed to resolve internal address: %w", h, err) } if net.ParseIP(a) == nil { - return fmt.Errorf("%s: failed to resolve internal address: invalid IP address: %q", h, a) + return fmt.Errorf("%s: %w: failed to resolve internal address: invalid IP address: %q", h, errInvalidIP, a) } h.Metadata.InternalAddress = a @@ -147,37 +149,3 @@ func (p *GatherFacts) investigateHost(h *api.Host, c *api.ClusterConfig) error { return nil } - -//nolint:unused -func (p *GatherFacts) testConnection(h *api.Host) error { - testfn := "launchpad_connection_test.txt" - - // cleanup - if h.Configurer.FileExist(h, testfn) { - if err := h.Configurer.DeleteFile(h, testfn); err != nil { - return fmt.Errorf("failed to delete connection test file: %w", err) - } - } - - if err := h.Configurer.WriteFile(h, testfn, "test", "0600"); err != nil { - return fmt.Errorf("failed to write connection test file: %w", err) - } - - if !h.Configurer.FileExist(h, testfn) { - return fmt.Errorf("file does not exist after connection test file write") - } - - content, err := h.Configurer.ReadFile(h, testfn) - if content != "test" || err != nil { - h.Configurer.DeleteFile(h, testfn) - - return fmt.Errorf(`connection file write test failed, expected "test", received "%s" (%w)`, content, err) - } - - err = h.Configurer.DeleteFile(h, testfn) - if err != nil || h.Configurer.FileExist(h, testfn) { - return fmt.Errorf("connection file write test failed at file exist after delete check") - } - - return nil -} diff --git a/pkg/product/mke/phase/init_swarm.go b/pkg/product/mke/phase/init_swarm.go index 79cb71e9e..201ab1d81 100644 --- a/pkg/product/mke/phase/init_swarm.go +++ b/pkg/product/mke/phase/init_swarm.go @@ -6,7 +6,6 @@ import ( "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/swarm" "github.com/k0sproject/rig/exec" - log "github.com/sirupsen/logrus" ) @@ -27,18 +26,18 @@ func (p *InitSwarm) Run() error { if !swarm.IsSwarmNode(swarmLeader) { log.Infof("%s: initializing swarm", swarmLeader) - output, err := swarmLeader.ExecOutput(swarmLeader.Configurer.DockerCommandf("swarm init --advertise-addr=%s %s", swarmLeader.SwarmAddress(), p.Config.Spec.MKE.SwarmInstallFlags.Join()), exec.Redact(`--token \S+`)) + err := swarmLeader.Exec(swarmLeader.Configurer.DockerCommandf("swarm init --advertise-addr=%s %s", swarmLeader.SwarmAddress(), p.Config.Spec.MKE.SwarmInstallFlags.Join()), exec.Redact(`--token \S+`)) if err != nil { - return fmt.Errorf("failed to initialize swarm: %s", output) + return fmt.Errorf("failed to initialize swarm: %w", err) } // Execute all swarm-post-init commands. These take care of // things like setting cert-expiry which cannot be done at the // time of swarm install. for _, swarmCmd := range p.Config.Spec.MKE.SwarmUpdateCommands { - output, err := swarmLeader.ExecOutput(swarmLeader.Configurer.DockerCommandf("%s", swarmCmd)) + err := swarmLeader.Exec(swarmLeader.Configurer.DockerCommandf("%s", swarmCmd)) if err != nil { - return fmt.Errorf("post swarm init command (%s) failed: %s", swarmCmd, output) + return fmt.Errorf("post swarm init command (%s) failed: %w", swarmCmd, err) } } @@ -52,13 +51,13 @@ func (p *InitSwarm) Run() error { mgrToken, err := swarmLeader.ExecOutput(swarmLeader.Configurer.DockerCommandf("swarm join-token manager -q"), exec.HideOutput()) if err != nil { - return fmt.Errorf("%s: failed to get swarm manager join-token: %s", swarmLeader, err.Error()) + return fmt.Errorf("%s: failed to get swarm manager join-token: %w", swarmLeader, err) } p.Config.Spec.MKE.Metadata.ManagerJoinToken = mgrToken workerToken, err := swarmLeader.ExecOutput(swarmLeader.Configurer.DockerCommandf("swarm join-token worker -q"), exec.HideOutput()) if err != nil { - return fmt.Errorf("%s: failed to get swarm worker join-token: %s", swarmLeader, err.Error()) + return fmt.Errorf("%s: failed to get swarm worker join-token: %w", swarmLeader, err) } p.Config.Spec.MKE.Metadata.WorkerJoinToken = workerToken return nil diff --git a/pkg/product/mke/phase/install_mcr.go b/pkg/product/mke/phase/install_mcr.go index b136dee17..0932aa370 100644 --- a/pkg/product/mke/phase/install_mcr.go +++ b/pkg/product/mke/phase/install_mcr.go @@ -1,11 +1,11 @@ package phase import ( + "errors" "fmt" "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" - retry "github.com/avast/retry-go" log "github.com/sirupsen/logrus" ) @@ -23,7 +23,11 @@ func (p *InstallMCR) HostFilterFunc(h *api.Host) bool { // Prepare collects the hosts. func (p *InstallMCR) Prepare(config interface{}) error { - p.Config = config.(*api.ClusterConfig) + cfg, ok := config.(*api.ClusterConfig) + if !ok { + return errInvalidConfig + } + p.Config = cfg log.Debugf("collecting hosts for phase %s", p.Title()) hosts := p.Config.Spec.Hosts.Filter(p.HostFilterFunc) log.Debugf("found %d hosts for phase %s", len(hosts), p.Title()) @@ -42,49 +46,57 @@ func (p *InstallMCR) Run() error { "engine_version": p.Config.Spec.MCR.Version, } - return p.Hosts.ParallelEach(p.installMCR) + if err := p.Hosts.ParallelEach(p.installMCR); err != nil { + return fmt.Errorf("failed to install container runtime: %w", err) + } + return nil } +var errVersionMismatch = errors.New("version mismatch") + func (p *InstallMCR) installMCR(h *api.Host) error { err := retry.Do( func() error { log.Infof("%s: installing container runtime (%s)", h, p.Config.Spec.MCR.Version) - return h.Configurer.InstallMCR(h, h.Metadata.MCRInstallScript, p.Config.Spec.MCR) + if err := h.Configurer.InstallMCR(h, h.Metadata.MCRInstallScript, p.Config.Spec.MCR); err != nil { + log.Errorf("%s: failed to install container runtime: %s", h, err.Error()) + return fmt.Errorf("%s: failed to install container runtime: %w", h, err) + } + return nil }, ) if err != nil { - log.Errorf("%s: failed to install container runtime -> %s", h, err.Error()) - return err + return fmt.Errorf("retry count exceeded: %w", err) } if err := h.Configurer.AuthorizeDocker(h); err != nil { - return err + return fmt.Errorf("%s: failed to authorize docker: %w", h, err) } currentVersion, err := h.MCRVersion() if err != nil { if err := h.Reboot(); err != nil { - return err + return fmt.Errorf("%s: failed to reboot host after installation: %w", h, err) } currentVersion, err = h.MCRVersion() if err != nil { - return fmt.Errorf("%s: failed to query container runtime version after installation: %s", h, err.Error()) + return fmt.Errorf("%s: failed to query container runtime version after installation: %w", h, err) } } if currentVersion != p.Config.Spec.MCR.Version { err = h.Configurer.RestartMCR(h) if err != nil { - return fmt.Errorf("%s: failed to restart container runtime", h) + return fmt.Errorf("%s: failed to restart container runtime: %w", h, err) } currentVersion, err = h.MCRVersion() if err != nil { - return fmt.Errorf("%s: failed to query container runtime version after restart: %s", h, err.Error()) + return fmt.Errorf("%s: failed to query container runtime version after restart: %w", h, err) } } if currentVersion != p.Config.Spec.MCR.Version { - return fmt.Errorf("%s: container runtime version not %s after installation", h, p.Config.Spec.MCR.Version) + return fmt.Errorf("%w: expected container runtime version to be %s after installation but was %s", errVersionMismatch, p.Config.Spec.MCR.Version, currentVersion) } log.Infof("%s: mirantis container runtime version %s installed", h, p.Config.Spec.MCR.Version) diff --git a/pkg/product/mke/phase/install_mke.go b/pkg/product/mke/phase/install_mke.go index 34c98f600..b2b2b7a7d 100644 --- a/pkg/product/mke/phase/install_mke.go +++ b/pkg/product/mke/phase/install_mke.go @@ -1,6 +1,7 @@ package phase import ( + "errors" "fmt" "regexp" "strings" @@ -33,7 +34,7 @@ func (p *InstallMKE) Title() string { } // Run the installer container. -func (p *InstallMKE) Run() (err error) { +func (p *InstallMKE) Run() error { p.leader = p.Config.Spec.SwarmLeader() h := p.leader @@ -66,7 +67,7 @@ func (p *InstallMKE) Run() (err error) { configCmd := h.Configurer.DockerCommandf("config create %s -", configName) err := h.Exec(configCmd, exec.Stdin(p.Config.Spec.MKE.ConfigData)) if err != nil { - return err + return fmt.Errorf("%s: failed to create MKE configuration: %w", h, err) } } @@ -74,7 +75,7 @@ func (p *InstallMKE) Run() (err error) { log.Debugf("Installing MKE with LicenseFilePath: %s", licenseFilePath) licenseFlag, err := util.SetupLicenseFile(p.Config.Spec.MKE.LicenseFilePath) if err != nil { - return fmt.Errorf("error while reading license file %s: %v", licenseFilePath, err) + return fmt.Errorf("error while reading license file %s: %w", licenseFilePath, err) } installFlags.AddUnlessExist(licenseFlag) } @@ -84,7 +85,9 @@ func (p *InstallMKE) Run() (err error) { installFlags.AddUnlessExist("--cloud-provider " + p.Config.Spec.MKE.Cloud.Provider) } if p.Config.Spec.MKE.Cloud.ConfigData != "" { - applyCloudConfig(p.Config) + if err := applyCloudConfig(p.Config); err != nil { + return err + } } } @@ -112,7 +115,7 @@ func (p *InstallMKE) Run() (err error) { installCmd := h.Configurer.DockerCommandf("run %s %s install %s", runFlags.Join(), image, installFlags.Join()) output, err := h.ExecOutput(installCmd, exec.StreamOutput(), exec.RedactString(p.Config.Spec.MKE.AdminUsername, p.Config.Spec.MKE.AdminPassword)) if err != nil { - return fmt.Errorf("%s: failed to run MKE installer: \n output:%s \n error:%w", h, output, err) + return fmt.Errorf("%s: failed to run MKE installer: \n output: %s \n error: %w", h, output, err) } if installFlags.GetValue("--admin-password") == "" { @@ -130,42 +133,50 @@ func (p *InstallMKE) Run() (err error) { err = mke.CollectFacts(h, p.Config.Spec.MKE.Metadata) if err != nil { - return fmt.Errorf("%s: failed to collect existing MKE details: %s", h, err.Error()) + return fmt.Errorf("%s: failed to collect existing MKE details: %w", h, err) } return nil } +var errUnsupportedProvider = errors.New("unsupported cloud provider") + func applyCloudConfig(config *api.ClusterConfig) error { configData := config.Spec.MKE.Cloud.ConfigData provider := config.Spec.MKE.Cloud.Provider var destFile string - if provider == "azure" { + switch provider { + case "azure": destFile = "/etc/kubernetes/azure.json" - } else if provider == "openstack" { + case "openstack": destFile = "/etc/kubernetes/openstack.conf" - } else { - return fmt.Errorf("Spec.Cloud.configData is only supported with Azure and OpenStack cloud providers") + default: + return fmt.Errorf("%w: spec.Cloud.configData is only supported with Azure and OpenStack cloud providers", errUnsupportedProvider) } - err := phase.RunParallelOnHosts(config.Spec.Hosts, config, func(h *api.Host, c *api.ClusterConfig) error { + err := phase.RunParallelOnHosts(config.Spec.Hosts, config, func(h *api.Host, _ *api.ClusterConfig) error { if h.IsWindows() { log.Warnf("%s: cloud provider configuration is not suppported on windows", h) return nil } log.Infof("%s: copying cloud provider (%s) config to %s", h, provider, destFile) - return h.Configurer.WriteFile(h, destFile, configData, "0700") + if err := h.Configurer.WriteFile(h, destFile, configData, "0600"); err != nil { + return fmt.Errorf("%s: failed to write cloud provider config: %w", h, err) + } + return nil }) - - return err + if err != nil { + return fmt.Errorf("failed to apply cloud provider config: %w", err) + } + return nil } func cleanupmke(h *api.Host) error { containersToRemove, err := h.ExecOutput(h.Configurer.DockerCommandf("ps -aq --filter name=ucp-")) if err != nil { - return err + return fmt.Errorf("%s: failed to list mke containers: %w", h, err) } if strings.Trim(containersToRemove, " ") == "" { log.Debugf("No containers to remove") @@ -173,7 +184,7 @@ func cleanupmke(h *api.Host) error { } containersToRemove = strings.ReplaceAll(containersToRemove, "\n", " ") if err := h.Exec(h.Configurer.DockerCommandf("rm -f %s", containersToRemove)); err != nil { - return err + return fmt.Errorf("%s: failed to remove mke containers: %w", h, err) } return nil diff --git a/pkg/product/mke/phase/install_mke_certs.go b/pkg/product/mke/phase/install_mke_certs.go index 6bd2a356f..fce0ba376 100644 --- a/pkg/product/mke/phase/install_mke_certs.go +++ b/pkg/product/mke/phase/install_mke_certs.go @@ -1,6 +1,8 @@ package phase import ( + "fmt" + "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" log "github.com/sirupsen/logrus" @@ -41,33 +43,32 @@ func (p *InstallMKECerts) installCertificates(config *api.ClusterConfig) error { err := h.Exec(h.Configurer.DockerCommandf("volume inspect ucp-controller-server-certs")) if err != nil { log.Infof("%s: creating ucp-controller-server-certs volume", h) - err := h.Exec(h.Configurer.DockerCommandf("volume create ucp-controller-server-certs")) - if err != nil { - return err + if err := h.Exec(h.Configurer.DockerCommandf("volume create ucp-controller-server-certs")); err != nil { + return fmt.Errorf("create ucp-controller-server-certs volume: %w", err) } } dir, err := h.ExecOutput(h.Configurer.DockerCommandf(`volume inspect ucp-controller-server-certs --format "{{ .Mountpoint }}"`)) if err != nil { - return err + return fmt.Errorf("get ucp-controller-server-certs volume mountpoint: %w", err) } log.Infof("%s: installing certificate files to %s", h, dir) - err = h.Configurer.WriteFile(h, h.Configurer.JoinPath(dir, "ca.pem"), config.Spec.MKE.CACertData, "0600") - if err != nil { - return err + if err := h.Configurer.WriteFile(h, h.Configurer.JoinPath(dir, "ca.pem"), config.Spec.MKE.CACertData, "0600"); err != nil { + return fmt.Errorf("write ca.pem: %w", err) } - err = h.Configurer.WriteFile(h, h.Configurer.JoinPath(dir, "cert.pem"), config.Spec.MKE.CertData, "0600") - if err != nil { - return err + if err := h.Configurer.WriteFile(h, h.Configurer.JoinPath(dir, "cert.pem"), config.Spec.MKE.CertData, "0600"); err != nil { + return fmt.Errorf("write cert.pem: %w", err) } - err = h.Configurer.WriteFile(h, h.Configurer.JoinPath(dir, "key.pem"), config.Spec.MKE.KeyData, "0600") - if err != nil { - return err + if err := h.Configurer.WriteFile(h, h.Configurer.JoinPath(dir, "key.pem"), config.Spec.MKE.KeyData, "0600"); err != nil { + return fmt.Errorf("write key.pem: %w", err) } return nil }) + if err != nil { + return fmt.Errorf("install certificates: %w", err) + } - return err + return nil } diff --git a/pkg/product/mke/phase/install_msr.go b/pkg/product/mke/phase/install_msr.go index a7f7da8a3..60af3dc73 100644 --- a/pkg/product/mke/phase/install_msr.go +++ b/pkg/product/mke/phase/install_msr.go @@ -43,7 +43,7 @@ func (p *InstallMSR) Run() error { err := p.Config.Spec.CheckMKEHealthRemote(h) if err != nil { - return fmt.Errorf("%s: failed to health check mke, try to set `--ucp-url` installFlag and check connectivity", h) + return fmt.Errorf("%s: failed to health check mke, try to set `--ucp-url` installFlag and check connectivity: %w", h, err) } p.EventProperties = map[string]interface{}{ @@ -105,12 +105,12 @@ func (p *InstallMSR) Run() error { installCmd := h.Configurer.DockerCommandf("run %s %s install %s", runFlags.Join(), image, installFlags.Join()) err = h.Exec(installCmd, exec.StreamOutput(), exec.RedactString(redacts...)) if err != nil { - return fmt.Errorf("%s: failed to run MSR installer: %s", h, err.Error()) + return fmt.Errorf("%s: failed to run MSR installer: %w", h, err) } msrMeta, err := msr.CollectFacts(h) if err != nil { - return fmt.Errorf("%s: failed to collect existing MSR details: %s", h, err) + return fmt.Errorf("%s: failed to collect existing MSR details: %w", h, err) } h.MSRMetadata = msrMeta return nil diff --git a/pkg/product/mke/phase/join_controllers.go b/pkg/product/mke/phase/join_controllers.go index 3c5575a93..f0a805032 100644 --- a/pkg/product/mke/phase/join_controllers.go +++ b/pkg/product/mke/phase/join_controllers.go @@ -33,7 +33,7 @@ func (p *JoinManagers) Run() error { log.Debugf("%s: joining as manager", h) err := h.Exec(joinCmd, exec.StreamOutput(), exec.RedactString(p.Config.Spec.MKE.Metadata.ManagerJoinToken)) if err != nil { - return fmt.Errorf("%s: failed to join manager node to swarm: %s", h, err.Error()) + return fmt.Errorf("%s: failed to join manager node to swarm: %w", h, err) } log.Infof("%s: joined successfully", h) } diff --git a/pkg/product/mke/phase/join_msr_replicas.go b/pkg/product/mke/phase/join_msr_replicas.go index 68ef2915e..53bfd2147 100644 --- a/pkg/product/mke/phase/join_msr_replicas.go +++ b/pkg/product/mke/phase/join_msr_replicas.go @@ -26,7 +26,11 @@ func (p *JoinMSRReplicas) HostFilterFunc(h *api.Host) bool { // Prepare collects the hosts. func (p *JoinMSRReplicas) Prepare(config interface{}) error { - p.Config = config.(*api.ClusterConfig) + cfg, ok := config.(*api.ClusterConfig) + if !ok { + return errInvalidConfig + } + p.Config = cfg if !p.Config.Spec.ContainsMSR() { return nil } @@ -91,7 +95,7 @@ func (p *JoinMSRReplicas) Run() error { joinCmd := msrLeader.Configurer.DockerCommandf("run %s %s join %s", runFlags.Join(), msrLeader.MSRMetadata.InstalledBootstrapImage, joinFlags.Join()) err := msrLeader.Exec(joinCmd, exec.StreamOutput(), exec.RedactString(redacts...)) if err != nil { - return fmt.Errorf("%s: failed to run MSR join: %s", h, err.Error()) + return fmt.Errorf("%s: failed to run MSR join: %w", h, err) } } return nil diff --git a/pkg/product/mke/phase/join_workers.go b/pkg/product/mke/phase/join_workers.go index 40cdf7f7f..b198e6b19 100644 --- a/pkg/product/mke/phase/join_workers.go +++ b/pkg/product/mke/phase/join_workers.go @@ -37,7 +37,7 @@ func (p *JoinWorkers) Run() error { log.Debugf("%s: joining as worker", h) err := h.Exec(joinCmd, exec.RedactString(p.Config.Spec.MKE.Metadata.WorkerJoinToken)) if err != nil { - return fmt.Errorf("Failed to join worker %s node to swarm", h) + return fmt.Errorf("failed to join worker %s node to swarm: %w", h, err) } log.Infof("%s: joined successfully", h) if h.IsWindows() { @@ -56,7 +56,7 @@ func (p *JoinWorkers) Run() error { return nil }) if err != nil { - return err + return fmt.Errorf("retry count exceeded: %w", err) } log.Infof("%s: reconnected", h) } diff --git a/pkg/product/mke/phase/label_nodes.go b/pkg/product/mke/phase/label_nodes.go index 186349767..ccbe71686 100644 --- a/pkg/product/mke/phase/label_nodes.go +++ b/pkg/product/mke/phase/label_nodes.go @@ -47,14 +47,14 @@ func (p *LabelNodes) labelCurrentNodes(config *api.ClusterConfig, swarmLeader *a for _, h := range config.Spec.Hosts { nodeID, err := swarm.NodeID(h) if err != nil { - return err + return fmt.Errorf("failed to get node ID for %s: %w", h, err) } log.Infof("%s: labeling node", h) if h.Role == "manager" && len(sanList) > 0 { sanLabelCmd := swarmLeader.Configurer.DockerCommandf("node update --label-add com.docker.ucp.SANs=%s %s", sanList, nodeID) err = swarmLeader.Exec(sanLabelCmd) if err != nil { - return fmt.Errorf("failed to add SANs label for node %s (%v)", h, err) + return fmt.Errorf("failed to add SANs label for node %s: %w", h, err) } } if h.Role == "msr" { @@ -62,13 +62,13 @@ func (p *LabelNodes) labelCurrentNodes(config *api.ClusterConfig, swarmLeader *a msrLabelCmd := swarmLeader.Configurer.DockerCommandf("%s %s", constant.ManagedMSRLabelCmd, nodeID) err = swarmLeader.Exec(msrLabelCmd) if err != nil { - return fmt.Errorf("Failed to label node %s as MSR (%s)", h, nodeID) + return fmt.Errorf("failed to label node %s as MSR (%s): %w", h, nodeID, err) } } labelCmd := swarmLeader.Configurer.DockerCommandf("%s %s", constant.ManagedLabelCmd, nodeID) err = swarmLeader.Exec(labelCmd) if err != nil { - return fmt.Errorf("Failed to label node %s (%s)", h, nodeID) + return fmt.Errorf("failed to label node %s (%s): %w", h, nodeID, err) } } return nil diff --git a/pkg/product/mke/phase/prepare_host.go b/pkg/product/mke/phase/prepare_host.go index 2461f6eb7..4f52261cf 100644 --- a/pkg/product/mke/phase/prepare_host.go +++ b/pkg/product/mke/phase/prepare_host.go @@ -1,10 +1,11 @@ package phase import ( + "fmt" + "github.com/Mirantis/mcc/pkg/msr" "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" - retry "github.com/avast/retry-go" log "github.com/sirupsen/logrus" ) @@ -24,70 +25,80 @@ func (p *PrepareHost) Title() string { func (p *PrepareHost) Run() error { err := phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, p.updateEnvironment) if err != nil { - return err + return fmt.Errorf("failed to update environment variables: %w", err) } err = phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, p.installBasePackages) if err != nil { - return err + return fmt.Errorf("failed to install base packages: %w", err) } err = phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, p.fixContainerized) if err != nil { - return err + return fmt.Errorf("failed to apply containerized host fix: %w", err) } err = phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, p.authorizeDocker) if err != nil { - return err + return fmt.Errorf("failed to authorize docker: %w", err) } if p.Config.Spec.ContainsMSR() && p.Config.Spec.MSR.ReplicaIDs == "sequential" { err = msr.AssignSequentialReplicaIDs(p.Config) if err != nil { - return err + return fmt.Errorf("failed to assign sequential MSR replica IDs: %w", err) } } return nil } -func (p *PrepareHost) installBasePackages(h *api.Host, c *api.ClusterConfig) error { +func (p *PrepareHost) installBasePackages(h *api.Host, _ *api.ClusterConfig) error { err := retry.Do( func() error { log.Infof("%s: installing base packages", h) - err := h.Configurer.InstallMKEBasePackages(h) - - return err + if err := h.Configurer.InstallMKEBasePackages(h); err != nil { + log.Errorf("%s: %s", h, err) + } + return nil }, + retry.Attempts(3), + retry.Delay(5), ) if err != nil { - log.Errorf("%s: failed to install base packages -> %s", h, err.Error()) - return err + return fmt.Errorf("retry count exceeded: %w", err) } log.Infof("%s: base packages installed", h) return nil } -func (p *PrepareHost) updateEnvironment(h *api.Host, c *api.ClusterConfig) error { +func (p *PrepareHost) updateEnvironment(h *api.Host, _ *api.ClusterConfig) error { if len(h.Environment) > 0 { log.Infof("%s: updating environment", h) - return h.Configurer.UpdateEnvironment(h, h.Environment) + if err := h.Configurer.UpdateEnvironment(h, h.Environment); err != nil { + return fmt.Errorf("failed to update environment variables: %w", err) + } + return nil } log.Debugf("%s: no environment variables specified for the host", h) return nil } -func (p *PrepareHost) fixContainerized(h *api.Host, c *api.ClusterConfig) error { +func (p *PrepareHost) fixContainerized(h *api.Host, _ *api.ClusterConfig) error { if h.Configurer.IsContainer(h) { log.Infof("%s: is a container, applying a fix", h) - return h.Configurer.FixContainer(h) + if err := h.Configurer.FixContainer(h); err != nil { + return fmt.Errorf("failed to apply containerized host fix: %w", err) + } } return nil } -func (p *PrepareHost) authorizeDocker(h *api.Host, c *api.ClusterConfig) error { - return h.Configurer.AuthorizeDocker(h) +func (p *PrepareHost) authorizeDocker(h *api.Host, _ *api.ClusterConfig) error { + if err := h.Configurer.AuthorizeDocker(h); err != nil { + return fmt.Errorf("failed to authorize docker: %w", err) + } + return nil } diff --git a/pkg/product/mke/phase/pull_mke_images.go b/pkg/product/mke/phase/pull_mke_images.go index 87d1af23b..ed84663a5 100644 --- a/pkg/product/mke/phase/pull_mke_images.go +++ b/pkg/product/mke/phase/pull_mke_images.go @@ -7,7 +7,6 @@ import ( "github.com/Mirantis/mcc/pkg/phase" common "github.com/Mirantis/mcc/pkg/product/common/api" "github.com/Mirantis/mcc/pkg/product/mke/api" - log "github.com/sirupsen/logrus" ) @@ -34,7 +33,6 @@ func (p *PullMKEImages) isMKESwarmOnly() bool { // Run pulls images in parallel across nodes via a workerpool of 5. func (p *PullMKEImages) Run() error { - swarmOnly := p.isMKESwarmOnly() images, err := p.ListImages(false, swarmOnly) @@ -44,7 +42,7 @@ func (p *PullMKEImages) Run() error { log.Debugf("loaded linux images list: %v", images) var winImages []*docker.Image - var winHosts api.Hosts = p.Config.Spec.Hosts.Filter(func(h *api.Host) bool { return h.IsWindows() }) + winHosts := p.Config.Spec.Hosts.Filter(func(h *api.Host) bool { return h.IsWindows() }) if len(winHosts) > 0 { winImages, err = p.ListImages(true, swarmOnly) @@ -59,8 +57,7 @@ func (p *PullMKEImages) Run() error { if api.IsCustomImageRepo(imageRepo) { pullList := docker.AllToRepository(images, imageRepo) pullListWin := docker.AllToRepository(winImages, imageRepo) - return phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, func(h *api.Host, c *api.ClusterConfig) error { - var err error + err := phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, func(h *api.Host, _ *api.ClusterConfig) error { var list []*docker.Image if h.IsWindows() { @@ -69,29 +66,44 @@ func (p *PullMKEImages) Run() error { list = pullList } - if err = docker.PullImages(h, list); err != nil { - return err + if err := docker.PullImages(h, list); err != nil { + return fmt.Errorf("%s: failed to pull images: %w", h, err) } log.Debugf("%s: retagging images", h) - return docker.RetagAllToRepository(h, list, "mirantis") + if err := docker.RetagAllToRepository(h, list, "mirantis"); err != nil { + return fmt.Errorf("%s: failed to retag images: %w", h, err) + } + return nil }) + if err != nil { + return fmt.Errorf("pull images: %w", err) + } + return nil } - err = phase.RunParallelOnHosts(p.Config.Spec.Managers(), p.Config, func(h *api.Host, c *api.ClusterConfig) error { + err = phase.RunParallelOnHosts(p.Config.Spec.Managers(), p.Config, func(h *api.Host, _ *api.ClusterConfig) error { log.Infof("%s: pulling linux images", h) - return docker.PullImages(h, images) + if err := docker.PullImages(h, images); err != nil { + return fmt.Errorf("%s: failed to pull linux images: %w", h, err) + } + return nil }) - if err != nil { - return err + return fmt.Errorf("failed to pull linux images: %w", err) } if len(winHosts) > 0 { - return phase.RunParallelOnHosts(winHosts, p.Config, func(h *api.Host, c *api.ClusterConfig) error { + err := phase.RunParallelOnHosts(winHosts, p.Config, func(h *api.Host, _ *api.ClusterConfig) error { log.Infof("%s: pulling windows images", h) - return docker.PullImages(h, winImages) + if err := docker.PullImages(h, winImages); err != nil { + return fmt.Errorf("%s: failed to pull windows images: %w", h, err) + } + return nil }) + if err != nil { + return fmt.Errorf("failed to pull windows images: %w", err) + } } return nil @@ -104,7 +116,7 @@ func (p *PullMKEImages) ListImages(win, swarmOnly bool) ([]*docker.Image, error) if !bootstrap.Exist(manager) { if err := bootstrap.Pull(manager); err != nil { - return []*docker.Image{}, err + return nil, fmt.Errorf("%s: failed to pull MKE bootstrapper image: %w", manager, err) } } @@ -126,7 +138,7 @@ func (p *PullMKEImages) ListImages(win, swarmOnly bool) ([]*docker.Image, error) output, err := manager.ExecOutput(manager.Configurer.DockerCommandf("run %s %s images %s", runFlags.Join(), bootstrap, imageFlags.Join())) if err != nil { - return []*docker.Image{}, fmt.Errorf("%s: failed to get MKE image list", manager) + return nil, fmt.Errorf("%s: failed to get MKE image list: %w", manager, err) } return docker.AllFromString(output), nil diff --git a/pkg/product/mke/phase/pull_msr_images.go b/pkg/product/mke/phase/pull_msr_images.go index b2fbbf211..e3e341543 100644 --- a/pkg/product/mke/phase/pull_msr_images.go +++ b/pkg/product/mke/phase/pull_msr_images.go @@ -6,7 +6,6 @@ import ( "github.com/Mirantis/mcc/pkg/docker" "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" - log "github.com/sirupsen/logrus" ) @@ -25,7 +24,7 @@ func (p *PullMSRImages) Title() string { func (p *PullMSRImages) Run() error { images, err := p.ListImages() if err != nil { - return err + return fmt.Errorf("failed to get MSR images list: %w", err) } log.Debugf("loaded MSR images list: %v", images) @@ -33,15 +32,24 @@ func (p *PullMSRImages) Run() error { if api.IsCustomImageRepo(imageRepo) { pullList := docker.AllToRepository(images, imageRepo) // In case of custom image repo, we need to pull and retag all the images on all MSR hosts - return phase.RunParallelOnHosts(p.Config.Spec.MSRs(), p.Config, func(h *api.Host, c *api.ClusterConfig) error { + err := phase.RunParallelOnHosts(p.Config.Spec.MSRs(), p.Config, func(h *api.Host, _ *api.ClusterConfig) error { if err := docker.PullImages(h, pullList); err != nil { - return err + return fmt.Errorf("failed to pull MSR images: %w", err) + } + if err := docker.RetagAllToRepository(h, pullList, images[0].Repository); err != nil { + return fmt.Errorf("failed to retag MSR images: %w", err) } - return docker.RetagAllToRepository(h, pullList, images[0].Repository) + return nil }) + if err != nil { + return fmt.Errorf("pull MSR images: %w", err) + } } - return docker.PullImages(p.Config.Spec.MSRLeader(), images) + if err := docker.PullImages(p.Config.Spec.MSRLeader(), images); err != nil { + return fmt.Errorf("failed to pull MSR images: %w", err) + } + return nil } // ListImages obtains a list of images from MSR. @@ -51,12 +59,12 @@ func (p *PullMSRImages) ListImages() ([]*docker.Image, error) { if !bootstrap.Exist(msrLeader) { if err := bootstrap.Pull(msrLeader); err != nil { - return []*docker.Image{}, err + return []*docker.Image{}, fmt.Errorf("%s: failed to pull MSR bootstrapper image: %w", msrLeader, err) } } output, err := msrLeader.ExecOutput(msrLeader.Configurer.DockerCommandf("run --rm %s images", bootstrap)) if err != nil { - return []*docker.Image{}, fmt.Errorf("%s: failed to get MSR image list", msrLeader) + return nil, fmt.Errorf("%s: failed to get MSR image list: %w", msrLeader, err) } return docker.AllFromString(output), nil diff --git a/pkg/product/mke/phase/remove_nodes.go b/pkg/product/mke/phase/remove_nodes.go index eae19418f..61cbd1986 100644 --- a/pkg/product/mke/phase/remove_nodes.go +++ b/pkg/product/mke/phase/remove_nodes.go @@ -2,6 +2,7 @@ package phase import ( "encoding/json" + "errors" "fmt" "io" "net/http" @@ -17,7 +18,6 @@ import ( "github.com/Mirantis/mcc/pkg/swarm" "github.com/Mirantis/mcc/pkg/util" "github.com/k0sproject/rig/exec" - log "github.com/sirupsen/logrus" ) @@ -73,7 +73,11 @@ func (p *RemoveNodes) ShouldRun() bool { // Prepare finds the nodes/replica ids to be removed. func (p *RemoveNodes) Prepare(config interface{}) error { - p.Config = config.(*api.ClusterConfig) + cfg, ok := config.(*api.ClusterConfig) + if !ok { + return errInvalidConfig + } + p.Config = cfg swarmLeader := p.Config.Spec.SwarmLeader() @@ -103,7 +107,7 @@ func (p *RemoveNodes) Prepare(config interface{}) error { // Get the hostname from the nodeID inspect hostname, err := swarmLeader.ExecOutput(swarmLeader.Configurer.DockerCommandf(`node inspect %s --format {{.Description.Hostname}}`, nodeID)) if err != nil { - return fmt.Errorf("failed to obtain hostname of MSR managed node: %s from swarm: %s", nodeID, err) + return fmt.Errorf("failed to obtain hostname of MSR managed node: %s from swarm: %w", nodeID, err) } // Using an httpClient, reach out to the MKE API to obtain the // full list of running containers so replicaID associated with @@ -129,7 +133,7 @@ func (p *RemoveNodes) Run() error { if len(p.cleanupMSRs) > 0 { err := msr.Cleanup(p.cleanupMSRs, swarmLeader) if err != nil { - return err + return fmt.Errorf("failed to cleanup MSR nodes: %w", err) } } @@ -158,7 +162,7 @@ func (p *RemoveNodes) currentNodeIDs(config *api.ClusterConfig) ([]string, error for _, h := range config.Spec.Hosts { nodeID, err := swarm.NodeID(h) if err != nil { - return []string{}, err + return []string{}, fmt.Errorf("failed to get swarm node ID for host %s: %w", h, err) } nodeIDs = append(nodeIDs, nodeID) } @@ -169,7 +173,7 @@ func (p *RemoveNodes) swarmNodeIDs(h *api.Host) ([]string, error) { output, err := h.ExecOutput(h.Configurer.DockerCommandf(`node ls --format="{{.ID}}"`)) if err != nil { log.Errorln(output) - return []string{}, err + return []string{}, fmt.Errorf("failed to get node IDs: %w", err) } return strings.Split(output, "\n"), nil } @@ -177,27 +181,25 @@ func (p *RemoveNodes) swarmNodeIDs(h *api.Host) ([]string, error) { func (p *RemoveNodes) removeNode(h *api.Host, nodeID string) error { nodeAddr, err := h.ExecOutput(h.Configurer.DockerCommandf(`node inspect %s --format {{.Status.Addr}}`, nodeID)) if err != nil { - return err + return fmt.Errorf("failed to get node address for node %s: %w", nodeID, err) } log.Infof("%s: removing orphan node %s", h, nodeAddr) nodeRole, err := h.ExecOutput(h.Configurer.DockerCommandf(`node inspect %s --format {{.Spec.Role}}`, nodeID)) if err != nil { - return err + return fmt.Errorf("failed to get node role for node %s: %w", nodeID, err) } if nodeRole == "manager" { log.Infof("%s: demoting orphan node %s", h, nodeAddr) - err = h.Exec(h.Configurer.DockerCommandf(`node demote %s`, nodeID)) - if err != nil { - return err + if err := h.Exec(h.Configurer.DockerCommandf(`node demote %s`, nodeID)); err != nil { + return fmt.Errorf("failed to demote node %s: %w", nodeID, err) } log.Infof("%s: orphan node %s demoted", h, nodeAddr) } log.Infof("%s: draining orphan node %s", h, nodeAddr) drainCmd := h.Configurer.DockerCommandf("node update --availability drain %s", nodeID) - err = h.Exec(drainCmd) - if err != nil { - return err + if err := h.Exec(drainCmd); err != nil { + return fmt.Errorf("failed to drain node %s: %w", nodeID, err) } time.Sleep(30 * time.Second) log.Infof("%s: orphan node %s drained", h, nodeAddr) @@ -205,7 +207,7 @@ func (p *RemoveNodes) removeNode(h *api.Host, nodeID string) error { removeCmd := h.Configurer.DockerCommandf("node rm --force %s", nodeID) err = h.Exec(removeCmd) if err != nil { - return err + return fmt.Errorf("failed to remove node %s: %w", nodeID, err) } log.Infof("%s: removed orphan node %s", h, nodeAddr) return nil @@ -238,7 +240,7 @@ func (p *RemoveNodes) removemsrNode(config *api.ClusterConfig, replicaID string) log.Debugf("%s: Removing MSR replica %s from cluster", msrLeader, replicaID) err := msrLeader.Exec(removeCmd, exec.StreamOutput()) if err != nil { - return fmt.Errorf("%s: failed to run MSR remove: %s", msrLeader, err.Error()) + return fmt.Errorf("%s: failed to run MSR remove: %w", msrLeader, err) } return nil } @@ -256,6 +258,8 @@ func (p *RemoveNodes) isManagedByUs(h *api.Host, nodeID string) isManaged { return managed } +var errGetReplicaID = errors.New("failed to get replicaID") + // getReplicaIDFromHostname retreives the replicaID from the container name // associated with hostname. func (p *RemoveNodes) getReplicaIDFromHostname(config *api.ClusterConfig, h *api.Host, hostname string) (string, error) { @@ -271,13 +275,13 @@ func (p *RemoveNodes) getReplicaIDFromHostname(config *api.ClusterConfig, h *api } mkeURL, err := config.Spec.MKEURL() if err != nil { - return "", err + return "", fmt.Errorf("%w: failed to get MKE URL: %w", errGetReplicaID, err) } // Get a MKE token token, err := mke.GetToken(client, mkeURL, config.Spec.MKE.AdminUsername, config.Spec.MKE.AdminPassword) if err != nil { - return "", fmt.Errorf("failed to get auth token: %s", err.Error()) + return "", fmt.Errorf("%w: failed to get auth token: %w", errGetReplicaID, err) } // Build the query @@ -288,44 +292,46 @@ func (p *RemoveNodes) getReplicaIDFromHostname(config *api.ClusterConfig, h *api q.Add("size", "false") mkeURL.RawQuery = q.Encode() - req, err := http.NewRequest("GET", mkeURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, mkeURL.String(), nil) if err != nil { - return "", err + return "", fmt.Errorf("%w: create request: %w", errGetReplicaID, err) } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) resp, err := client.Do(req) if err != nil { - return "", err + return "", fmt.Errorf("%w: failed to get containers from MKE: %w", errGetReplicaID, err) } - if resp.StatusCode != 200 { - return "", fmt.Errorf("unexpected response code: %d from %s endpoint: %s", resp.StatusCode, mkeURL.String(), err) + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("%w: unexpected response code: %d from %s endpoint: %w", errGetReplicaID, resp.StatusCode, mkeURL.String(), err) } var containersResponse []dockerContainer respBody, err := io.ReadAll(resp.Body) if err != nil { - return "", err + return "", fmt.Errorf("%w: failed to read response body: %w", errGetReplicaID, err) } err = json.Unmarshal(respBody, &containersResponse) if err != nil { - return "", err + return "", fmt.Errorf("%w: failed to unmarshal response body: %w", errGetReplicaID, err) } // Iterate the containersResponse and check for hostname in the container // names, even though regex is slow it's the safer choice here var replicaID string - re, _ := regexp.Compile(`\s*(\d{12})`) + re := regexp.MustCompile(`\s*(\d{12})`) for _, container := range containersResponse { for _, n := range container.Names { if strings.HasPrefix(n, fmt.Sprintf("/%s", hostname)) { replicaID = re.FindString(n) if replicaID == "" { - return "", fmt.Errorf("retrieved blank replicaID from hostname: %s", hostname) + return "", fmt.Errorf("%w: retrieved blank replicaID from hostname: %s", errGetReplicaID, hostname) } return replicaID, nil } } } - return "", fmt.Errorf("failed to obtain replicaID from hostname: %s", hostname) + return "", fmt.Errorf("%w: failed to obtain replicaID from hostname: %s", errGetReplicaID, hostname) } diff --git a/pkg/product/mke/phase/restart_mcr.go b/pkg/product/mke/phase/restart_mcr.go index bec326228..4320e0a59 100644 --- a/pkg/product/mke/phase/restart_mcr.go +++ b/pkg/product/mke/phase/restart_mcr.go @@ -1,12 +1,12 @@ package phase import ( + "fmt" "math" "sync" "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" - "github.com/gammazero/workerpool" log "github.com/sirupsen/logrus" ) @@ -24,7 +24,11 @@ func (p *RestartMCR) HostFilterFunc(h *api.Host) bool { // Prepare collects the hosts. func (p *RestartMCR) Prepare(config interface{}) error { - p.Config = config.(*api.ClusterConfig) + cfg, ok := config.(*api.ClusterConfig) + if !ok { + return errInvalidConfig + } + p.Config = cfg log.Debugf("collecting hosts for phase %s", p.Title()) hosts := p.Config.Spec.Hosts.Filter(p.HostFilterFunc) log.Debugf("found %d hosts for phase %s", len(hosts), p.Title()) @@ -59,7 +63,7 @@ func (p *RestartMCR) restartMCRs() error { for _, h := range managers { if err := h.Configurer.RestartMCR(h); err != nil { - return err + return fmt.Errorf("failed to restart MCR on manager %s: %w", h, err) } } @@ -68,12 +72,12 @@ func (p *RestartMCR) restartMCRs() error { if concurrentRestarts == 0 { concurrentRestarts = 1 } - wp := workerpool.New(concurrentRestarts) + pool := workerpool.New(concurrentRestarts) mu := sync.Mutex{} restartErrors := &phase.Error{} for _, w := range others { h := w - wp.Submit(func() { + pool.Submit(func() { err := h.Configurer.RestartMCR(h) if err != nil { mu.Lock() @@ -83,7 +87,7 @@ func (p *RestartMCR) restartMCRs() error { h.Metadata.MCRRestartRequired = false }) } - wp.StopWait() + pool.StopWait() if restartErrors.Count() > 0 { return restartErrors } diff --git a/pkg/product/mke/phase/uninstall_mcr.go b/pkg/product/mke/phase/uninstall_mcr.go index 0b13d1141..9e82f4ab2 100644 --- a/pkg/product/mke/phase/uninstall_mcr.go +++ b/pkg/product/mke/phase/uninstall_mcr.go @@ -1,9 +1,10 @@ package phase import ( + "fmt" + "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" - log "github.com/sirupsen/logrus" ) @@ -20,16 +21,20 @@ func (p *UninstallMCR) Title() string { // Run installs the engine on each host. func (p *UninstallMCR) Run() error { - return phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, p.uninstallMCR) + if err := phase.RunParallelOnHosts(p.Config.Spec.Hosts, p.Config, p.uninstallMCR); err != nil { + return fmt.Errorf("uninstall container runtime: %w", err) + } + return nil } func (p *UninstallMCR) uninstallMCR(h *api.Host, c *api.ClusterConfig) error { log.Infof("%s: uninstalling container runtime", h) - err := h.Configurer.UninstallMCR(h, h.Metadata.MCRInstallScript, c.Spec.MCR) - if err == nil { - log.Infof("%s: mirantis container runtime uninstalled", h) + if err := h.Configurer.UninstallMCR(h, h.Metadata.MCRInstallScript, c.Spec.MCR); err != nil { + return fmt.Errorf("%s: uninstall container runtime: %w", h, err) } - return err + log.Infof("%s: mirantis container runtime uninstalled", h) + + return nil } diff --git a/pkg/product/mke/phase/uninstall_mke.go b/pkg/product/mke/phase/uninstall_mke.go index 6c203618b..ffea832f8 100644 --- a/pkg/product/mke/phase/uninstall_mke.go +++ b/pkg/product/mke/phase/uninstall_mke.go @@ -3,14 +3,13 @@ package phase import ( "fmt" - "github.com/k0sproject/rig/exec" - log "github.com/sirupsen/logrus" - mcclog "github.com/Mirantis/mcc/pkg/log" "github.com/Mirantis/mcc/pkg/phase" common "github.com/Mirantis/mcc/pkg/product/common/api" "github.com/Mirantis/mcc/pkg/product/mke/api" "github.com/Mirantis/mcc/pkg/swarm" + "github.com/k0sproject/rig/exec" + log "github.com/sirupsen/logrus" ) // UninstallMKE is the phase implementation for running MKE uninstall. @@ -46,12 +45,12 @@ func (p *UninstallMKE) Run() error { uninstallCmd := swarmLeader.Configurer.DockerCommandf("run %s %s uninstall-ucp %s", runFlags.Join(), image, uninstallFlags.Join()) err := swarmLeader.Exec(uninstallCmd, exec.StreamOutput(), exec.RedactString(p.Config.Spec.MKE.InstallFlags.GetValue("--admin-username"), p.Config.Spec.MKE.InstallFlags.GetValue("--admin-password"))) if err != nil { - return fmt.Errorf("%s: failed to run MKE uninstaller: %s", swarmLeader, err.Error()) + return fmt.Errorf("%s: failed to run MKE uninstaller: %w", swarmLeader, err) } if p.Config.Spec.MKE.CertData != "" { managers := p.Config.Spec.Managers() - managers.ParallelEach(func(h *api.Host) error { + _ = managers.ParallelEach(func(h *api.Host) error { log.Infof("%s: removing ucp-controller-server-certs volume", h) err := h.Exec(h.Configurer.DockerCommandf("volume rm --force ucp-controller-server-certs")) if err != nil { diff --git a/pkg/product/mke/phase/uninstall_msr.go b/pkg/product/mke/phase/uninstall_msr.go index 4055fcd83..f8a91742e 100644 --- a/pkg/product/mke/phase/uninstall_msr.go +++ b/pkg/product/mke/phase/uninstall_msr.go @@ -1,10 +1,11 @@ package phase import ( + "fmt" + "github.com/Mirantis/mcc/pkg/msr" "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" - log "github.com/sirupsen/logrus" ) @@ -35,5 +36,8 @@ func (p *UninstallMSR) Run() error { msrHosts = append(msrHosts, h) } } - return msr.Cleanup(msrHosts, swarmLeader) + if err := msr.Cleanup(msrHosts, swarmLeader); err != nil { + return fmt.Errorf("failed to clean up MSR: %w", err) + } + return nil } diff --git a/pkg/product/mke/phase/upgrade_check.go b/pkg/product/mke/phase/upgrade_check.go index f82b91655..9a08a90eb 100644 --- a/pkg/product/mke/phase/upgrade_check.go +++ b/pkg/product/mke/phase/upgrade_check.go @@ -1,6 +1,7 @@ package phase import ( + "fmt" "strings" "github.com/Mirantis/mcc/pkg/docker/hub" @@ -30,12 +31,13 @@ func (p *UpgradeCheck) ShouldRun() bool { // Run the installer container. func (p *UpgradeCheck) Run() (err error) { - mv, err := hub.LatestTag("mirantis", "ucp", strings.Contains(p.Config.Spec.MKE.Version, "-")) + mkeTag, err := hub.LatestTag("mirantis", "ucp", strings.Contains(p.Config.Spec.MKE.Version, "-")) if err != nil { - log.Errorf("failed to check for MKE upgrade: %s", err.Error()) + log.Errorf("failed to check for MKE upgrade: %v", err) + return nil } - mkeV, err := version.NewVersion(mv) + mkeV, err := version.NewVersion(mkeTag) if err != nil { log.Errorf("invalid MKE version response: %s", err.Error()) return nil @@ -44,11 +46,11 @@ func (p *UpgradeCheck) Run() (err error) { mkeTargetV, err := version.NewVersion(p.Config.Spec.MKE.Version) if err != nil { log.Errorf("invalid MKE version in configuration: %s", err.Error()) - return err + return fmt.Errorf("invalid MKE version in configuration: %w", err) } if mkeV.GreaterThan(mkeTargetV) { - log.Warnf("a newer version of MKE is available: %s (installing %s)", mv, mkeTargetV.String()) + log.Warnf("a newer version of MKE is available: %s (installing %s)", mkeTag, mkeTargetV.String()) } if !p.Config.Spec.ContainsMSR() { @@ -70,7 +72,7 @@ func (p *UpgradeCheck) Run() (err error) { msrTargetV, err := version.NewVersion(p.Config.Spec.MSR.Version) if err != nil { log.Errorf("invalid MSR version in configuration: %s", err.Error()) - return err + return fmt.Errorf("invalid MSR version in configuration: %w", err) } if msrV.GreaterThan(msrTargetV) { diff --git a/pkg/product/mke/phase/upgrade_mcr.go b/pkg/product/mke/phase/upgrade_mcr.go index 6a9aa238b..31100a6c2 100644 --- a/pkg/product/mke/phase/upgrade_mcr.go +++ b/pkg/product/mke/phase/upgrade_mcr.go @@ -1,6 +1,7 @@ package phase import ( + "errors" "fmt" "strconv" "sync" @@ -9,7 +10,6 @@ import ( "github.com/Mirantis/mcc/pkg/msr" "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" - retry "github.com/avast/retry-go" "github.com/gammazero/workerpool" log "github.com/sirupsen/logrus" @@ -29,7 +29,11 @@ func (p *UpgradeMCR) HostFilterFunc(h *api.Host) bool { // Prepare collects the hosts. func (p *UpgradeMCR) Prepare(config interface{}) error { - p.Config = config.(*api.ClusterConfig) + cfg, ok := config.(*api.ClusterConfig) + if !ok { + return errInvalidConfig + } + p.Config = cfg log.Debugf("collecting hosts for phase %s", p.Title()) hosts := p.Config.Spec.Hosts.Filter(p.HostFilterFunc) log.Debugf("found %d hosts for phase %s", len(hosts), p.Title()) @@ -50,6 +54,8 @@ func (p *UpgradeMCR) Run() error { return p.upgradeMCRs() } +var errUnknownRole = errors.New("unknown role") + // Upgrades host docker engines, first managers (one-by-one) and then ~10% rolling update to workers // TODO: should we drain? func (p *UpgradeMCR) upgradeMCRs() error { @@ -65,7 +71,7 @@ func (p *UpgradeMCR) upgradeMCRs() error { case "msr": msrs = append(msrs, h) default: - return fmt.Errorf("%s: unknown role: %s", h, h.Role) + return fmt.Errorf("%s: %w: %s", h, errUnknownRole, h.Role) } } @@ -78,7 +84,7 @@ func (p *UpgradeMCR) upgradeMCRs() error { if p.Config.Spec.MKE.Metadata.Installed { err := p.Config.Spec.CheckMKEHealthLocal(h) if err != nil { - return err + return fmt.Errorf("%s: %w", h, err) } } } @@ -96,7 +102,7 @@ func (p *UpgradeMCR) upgradeMCRs() error { for _, h := range msrs { if h.MSRMetadata.Installed { if err := msr.WaitMSRNodeReady(h, port); err != nil { - return err + return fmt.Errorf("%s: check msr node ready state: %w", h, err) } } if err := p.upgradeMCR(h); err != nil { @@ -104,12 +110,12 @@ func (p *UpgradeMCR) upgradeMCRs() error { } if h.MSRMetadata.Installed { if err := msr.WaitMSRNodeReady(h, port); err != nil { - return err + return fmt.Errorf("%s: check msr node ready state: %w", h, err) } err := retry.Do( func() error { if _, err := msr.CollectFacts(h); err != nil { - return err + return fmt.Errorf("%s: collect msr facts: %w", h, err) } return nil }, @@ -119,18 +125,18 @@ func (p *UpgradeMCR) upgradeMCRs() error { retry.Attempts(3), ) if err != nil { - return err + return fmt.Errorf("retry count exceeded: %w", err) } } } log.Debugf("concurrently upgrading workers in batches of %d", p.Concurrency) - wp := workerpool.New(p.Concurrency) + pool := workerpool.New(p.Concurrency) mu := sync.Mutex{} installErrors := &phase.Error{} for _, w := range workers { h := w - wp.Submit(func() { + pool.Submit(func() { err := p.upgradeMCR(h) if err != nil { mu.Lock() @@ -139,7 +145,7 @@ func (p *UpgradeMCR) upgradeMCRs() error { } }) } - wp.StopWait() + pool.StopWait() if installErrors.Count() > 0 { return installErrors } @@ -150,12 +156,15 @@ func (p *UpgradeMCR) upgradeMCR(h *api.Host) error { err := retry.Do( func() error { log.Infof("%s: upgrading container runtime (%s -> %s)", h, h.Metadata.MCRVersion, p.Config.Spec.MCR.Version) - return h.Configurer.InstallMCR(h, h.Metadata.MCRInstallScript, p.Config.Spec.MCR) + if err := h.Configurer.InstallMCR(h, h.Metadata.MCRInstallScript, p.Config.Spec.MCR); err != nil { + return fmt.Errorf("%s: failed to install container runtime: %w", h, err) + } + return nil }, ) if err != nil { log.Errorf("%s: failed to update container runtime -> %s", h, err.Error()) - return err + return fmt.Errorf("retry count exceeded: %w", err) } // TODO: This exercise is duplicated in InstallMCR, maybe @@ -163,27 +172,27 @@ func (p *UpgradeMCR) upgradeMCR(h *api.Host) error { currentVersion, err := h.MCRVersion() if err != nil { if err := h.Reboot(); err != nil { - return err + return fmt.Errorf("%s: failed to reboot after container runtime installation: %w", h, err) } currentVersion, err = h.MCRVersion() if err != nil { - return fmt.Errorf("%s: failed to query container runtime version after installation: %s", h, err.Error()) + return fmt.Errorf("%s: failed to query container runtime version after installation: %w", h, err) } } if currentVersion != p.Config.Spec.MCR.Version { err = h.Configurer.RestartMCR(h) if err != nil { - return fmt.Errorf("%s: failed to restart container runtime", h) + return fmt.Errorf("%s: failed to restart container runtime: %w", h, err) } currentVersion, err = h.MCRVersion() if err != nil { - return fmt.Errorf("%s: failed to query container runtime version after restart: %s", h, err.Error()) + return fmt.Errorf("%s: failed to query container runtime version after restart: %w", h, err) } } if currentVersion != p.Config.Spec.MCR.Version { - return fmt.Errorf("%s: container runtime version not %s after upgrade", h, p.Config.Spec.MCR.Version) + return fmt.Errorf("%s: %w: container runtime version not %s after upgrade", h, errVersionMismatch, p.Config.Spec.MCR.Version) } log.Infof("%s: upgraded to mirantis container runtime version %s", h, p.Config.Spec.MCR.Version) diff --git a/pkg/product/mke/phase/upgrade_mke.go b/pkg/product/mke/phase/upgrade_mke.go index 94db2e8ed..9c069f128 100644 --- a/pkg/product/mke/phase/upgrade_mke.go +++ b/pkg/product/mke/phase/upgrade_mke.go @@ -54,14 +54,15 @@ func (p *UpgradeMKE) Run() error { upgradeCmd := swarmLeader.Configurer.DockerCommandf("run %s %s upgrade %s %s", runFlags.Join(), p.Config.Spec.MKE.GetBootstrapperImage(), upgradeFlags.Join()) err := swarmLeader.Exec(upgradeCmd, exec.StreamOutput()) if err != nil { - return fmt.Errorf("%s: failed to run MKE upgrader: \n error:%w", swarmLeader, err) + return fmt.Errorf("%s: failed to run MKE upgrader: %w", swarmLeader, err) } originalInstalledVersion := p.Config.Spec.MKE.Metadata.InstalledVersion if err := mke.CollectFacts(swarmLeader, p.Config.Spec.MKE.Metadata); err != nil { - return fmt.Errorf("%s: failed to collect existing MKE details: %s", swarmLeader, err.Error()) + return fmt.Errorf("%s: failed to collect existing MKE details: %w", swarmLeader, err) } + p.EventProperties["upgraded"] = true p.EventProperties["installed_version"] = originalInstalledVersion p.EventProperties["upgraded_version"] = p.Config.Spec.MKE.Version diff --git a/pkg/product/mke/phase/upgrade_msr.go b/pkg/product/mke/phase/upgrade_msr.go index 900828abc..3a6bee959 100644 --- a/pkg/product/mke/phase/upgrade_msr.go +++ b/pkg/product/mke/phase/upgrade_msr.go @@ -7,7 +7,6 @@ import ( "github.com/Mirantis/mcc/pkg/phase" common "github.com/Mirantis/mcc/pkg/product/common/api" "github.com/k0sproject/rig/exec" - log "github.com/sirupsen/logrus" ) @@ -35,7 +34,7 @@ func (p *UpgradeMSR) Run() error { err := p.Config.Spec.CheckMKEHealthRemote(h) if err != nil { - return fmt.Errorf("%s: failed to health check mke, try to set `--ucp-url` installFlag and check connectivity", h) + return fmt.Errorf("%s: failed to health check mke, try to set `--ucp-url` installFlag and check connectivity: %w", h, err) } p.EventProperties = map[string]interface{}{ @@ -66,19 +65,19 @@ func (p *UpgradeMSR) Run() error { upgradeCmd := h.Configurer.DockerCommandf("run %s %s upgrade %s", runFlags.Join(), p.Config.Spec.MSR.GetBootstrapperImage(), upgradeFlags.Join()) log.Debugf("%s: Running msr upgrade via bootstrapper", h) if err := h.Exec(upgradeCmd, exec.StreamOutput()); err != nil { - return fmt.Errorf("%s: failed to run msr upgrade: %s", h, err.Error()) + return fmt.Errorf("%s: failed to run msr upgrade: %w", h, err) } msrMeta, err := msr.CollectFacts(h) if err != nil { - return fmt.Errorf("%s: failed to collect existing msr details: %s", h, err.Error()) + return fmt.Errorf("%s: failed to collect existing msr details: %w", h, err) } // Check to make sure installedversion matches bootstrapperVersion if msrMeta.InstalledVersion != p.Config.Spec.MSR.Version { // If our newly collected facts do not match the version we upgraded to // then the upgrade has failed - return fmt.Errorf("%s: upgraded msr version: %s does not match intended upgrade version: %s", h, msrMeta.InstalledVersion, p.Config.Spec.MSR.Version) + return fmt.Errorf("%s: %w: upgraded msr version: %s does not match intended upgrade version: %s", h, errVersionMismatch, msrMeta.InstalledVersion, p.Config.Spec.MSR.Version) } p.EventProperties["msr_upgraded"] = true diff --git a/pkg/product/mke/phase/upload_images.go b/pkg/product/mke/phase/upload_images.go index 9260a6b7b..56f9875cf 100644 --- a/pkg/product/mke/phase/upload_images.go +++ b/pkg/product/mke/phase/upload_images.go @@ -1,6 +1,7 @@ package phase import ( + "fmt" "os" "path" "path/filepath" @@ -8,7 +9,6 @@ import ( "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" "github.com/Mirantis/mcc/pkg/util" - "github.com/alessio/shellescape" log "github.com/sirupsen/logrus" ) @@ -60,7 +60,11 @@ func (p *LoadImages) HostFilterFunc(h *api.Host) bool { // Prepare collects the hosts. func (p *LoadImages) Prepare(config interface{}) error { - p.Config = config.(*api.ClusterConfig) + cfg, ok := config.(*api.ClusterConfig) + if !ok { + return errInvalidConfig + } + p.Config = cfg log.Debugf("collecting hosts for phase %s", p.Title()) hosts := p.Config.Spec.Hosts.Filter(p.HostFilterFunc) log.Debugf("found %d hosts for phase %s", len(hosts), p.Title()) @@ -71,14 +75,14 @@ func (p *LoadImages) Prepare(config interface{}) error { // Run does all the work. func (p *LoadImages) Run() error { var totalBytes uint64 - p.Hosts.Each(func(h *api.Host) error { + _ = p.Hosts.Each(func(h *api.Host) error { totalBytes += h.Metadata.TotalImageBytes return nil }) log.Infof("total %s of images to upload", util.FormatBytes(totalBytes)) - return p.Hosts.Each(func(h *api.Host) error { + err := p.Hosts.Each(func(h *api.Host) error { for idx, f := range h.Metadata.ImagesToUpload { log.Debugf("%s: uploading image %d/%d", h, idx+1, len(h.Metadata.ImagesToUpload)) @@ -86,15 +90,19 @@ func (p *LoadImages) Run() error { df := h.Configurer.JoinPath(h.Configurer.Pwd(h), base) err := h.WriteFileLarge(f, df) if err != nil { - return err + return fmt.Errorf("failed to write file %s: %w", f, err) } log.Infof("%s: loading image %d/%d : %s", h, idx+1, len(h.Metadata.ImagesToUpload), base) err = h.Exec(h.Configurer.DockerCommandf("load -i %s", shellescape.Quote(base))) if err != nil { - return err + return fmt.Errorf("failed to load image %s: %w", base, err) } } return nil }) + if err != nil { + return fmt.Errorf("failed to upload images: %w", err) + } + return nil } diff --git a/pkg/product/mke/phase/validate_facts.go b/pkg/product/mke/phase/validate_facts.go index b83475acf..edbf44497 100644 --- a/pkg/product/mke/phase/validate_facts.go +++ b/pkg/product/mke/phase/validate_facts.go @@ -1,13 +1,13 @@ package phase import ( + "errors" "fmt" "strconv" "github.com/Mirantis/mcc/pkg/mke" "github.com/Mirantis/mcc/pkg/phase" "github.com/Mirantis/mcc/pkg/product/mke/api" - "github.com/hashicorp/go-version" log "github.com/sirupsen/logrus" ) @@ -30,7 +30,7 @@ func (p *ValidateFacts) Run() error { p.populateSan() } - p.Config.Spec.Hosts.Each(func(h *api.Host) error { + _ = p.Config.Spec.Hosts.Each(func(h *api.Host) error { if h.Configurer != nil && h.Configurer.SELinuxEnabled(h) { h.DaemonConfig["selinux-enabled"] = true log.Infof("%s: adding 'selinux-enabled=true' to host container runtime config", h) @@ -41,7 +41,7 @@ func (p *ValidateFacts) Run() error { if err := p.validateMKEVersionJump(); err != nil { if p.Force { - log.Warnf("%s - continuing anyway because --force given", err.Error()) + log.Warnf("%s: continuing anyway because --force given", err.Error()) } else { return err } @@ -49,7 +49,7 @@ func (p *ValidateFacts) Run() error { if err := p.validateMSRVersionJump(); err != nil { if p.Force { - log.Warnf("%s - continuing anyway because --force given", err.Error()) + log.Warnf("%s: continuing anyway because --force given", err.Error()) } else { return err } @@ -57,7 +57,7 @@ func (p *ValidateFacts) Run() error { if err := p.validateDataPlane(); err != nil { if p.Force { - log.Warnf("%s - continuing anyway because --force given", err.Error()) + log.Warnf("%s: continuing anyway because --force given", err.Error()) } else { return err } @@ -75,20 +75,22 @@ func (p *ValidateFacts) populateSan() { } } +var errInvalidUpgradePath = errors.New("invalid upgrade path") + // validateMSRVersionJump validates MKE upgrade path. func (p *ValidateFacts) validateMKEVersionJump() error { if p.Config.Spec.MKE.Metadata.Installed && p.Config.Spec.MKE.Metadata.InstalledVersion != "" { installedMKE, err := version.NewVersion(p.Config.Spec.MKE.Metadata.InstalledVersion) if err != nil { - return err + return fmt.Errorf("can't parse installed MKE version: %w", err) } targetMKE, err := version.NewVersion(p.Config.Spec.MKE.Version) if err != nil { - return err + return fmt.Errorf("can't parse target MKE version: %w", err) } if mke.VersionGreaterThan(installedMKE, targetMKE) { - return fmt.Errorf("can't downgrade MKE %s to %s", installedMKE.String(), targetMKE.String()) + return fmt.Errorf("%w: can't downgrade MKE %s to %s", errInvalidUpgradePath, installedMKE, targetMKE) } installedSegments := installedMKE.Segments() @@ -96,7 +98,7 @@ func (p *ValidateFacts) validateMKEVersionJump() error { // This will fail if there's something like 2.x => 3.x or 3.x => 4.x. if installedSegments[0] == targetSegments[0] && targetSegments[1]-installedSegments[1] > 1 { - return fmt.Errorf("can't upgrade MKE directly from %s to %s - need to upgrade to %d.%d first", installedMKE.String(), targetMKE.String(), installedSegments[0], installedSegments[1]+1) + return fmt.Errorf("%w: can't upgrade MKE directly from %s to %s - need to upgrade to %d.%d first", errInvalidUpgradePath, installedMKE, targetMKE, installedSegments[0], installedSegments[1]+1) } } @@ -109,15 +111,15 @@ func (p *ValidateFacts) validateMSRVersionJump() error { if p.Config.Spec.MSR != nil && msrLeader.MSRMetadata != nil && msrLeader.MSRMetadata.Installed && msrLeader.MSRMetadata.InstalledVersion != "" { installedMSR, err := version.NewVersion(msrLeader.MSRMetadata.InstalledVersion) if err != nil { - return err + return fmt.Errorf("can't parse installed MSR version: %w", err) } targetMSR, err := version.NewVersion(p.Config.Spec.MSR.Version) if err != nil { - return err + return fmt.Errorf("can't parse target MSR version: %w", err) } if mke.VersionGreaterThan(installedMSR, targetMSR) { - return fmt.Errorf("can't downgrade MSR %s to %s", installedMSR.String(), targetMSR.String()) + return fmt.Errorf("%w: can't downgrade MSR %s to %s", errInvalidUpgradePath, installedMSR, targetMSR) } installedSegments := installedMSR.Segments() @@ -125,13 +127,15 @@ func (p *ValidateFacts) validateMSRVersionJump() error { // This will fail if there's something like 2.x => 3.x or 3.x => 4.x. if installedSegments[0] == targetSegments[0] && targetSegments[1]-installedSegments[1] > 1 { - return fmt.Errorf("can't upgrade MSR directly from %s to %s - need to upgrade to %d.%d first", installedMSR.String(), targetMSR.String(), installedSegments[0], installedSegments[1]+1) + return fmt.Errorf("%w: can't upgrade MSR directly from %s to %s - need to upgrade to %d.%d first", errInvalidUpgradePath, installedMSR, targetMSR, installedSegments[0], installedSegments[1]+1) } } return nil } +var errInvalidDataPlane = errors.New("invalid data plane settings") + // validateDataPlane checks if the calico data plane would get changed (VXLAN <-> VPIP). func (p *ValidateFacts) validateDataPlane() error { log.Debug("validating data plane settings") @@ -148,7 +152,7 @@ func (p *ValidateFacts) validateDataPlane() error { } else { v, err := strconv.ParseBool(val) if err != nil { - return err + return fmt.Errorf("can't parse --calico-vxlan value: %w", err) } valB = v } @@ -156,7 +160,7 @@ func (p *ValidateFacts) validateDataPlane() error { // User has explicitly defined --calico-vxlan=false but there is a windows host in the config if !valB { if p.Config.Spec.Hosts.Include(func(h *api.Host) bool { return h.IsWindows() }) { - return fmt.Errorf("calico IPIP can't be used on Windows") + return fmt.Errorf("%w: calico IPIP can't be used on Windows", errInvalidDataPlane) } log.Debug("no windows hosts found") @@ -171,13 +175,13 @@ func (p *ValidateFacts) validateDataPlane() error { if p.Config.Spec.MKE.Metadata.VXLAN { log.Debug("mke has been installed with calico + vxlan") if !valB { - return fmt.Errorf("calico configured with VXLAN, can't automatically change to IPIP") + return fmt.Errorf("%w: calico configured with VXLAN, can't automatically change to IPIP", errInvalidDataPlane) } } else { log.Debug("mke has been installed with calico + vpip") // User has explicitly defined --calico-vxlan=true but there is already a calico with ipip if valB { - return fmt.Errorf("calico configured with IPIP, can't automatically change to VXLAN") + return fmt.Errorf("%w: calico configured with IPIP, can't automatically change to VXLAN", errInvalidDataPlane) } } diff --git a/pkg/product/mke/phase/validate_facts_test.go b/pkg/product/mke/phase/validate_facts_test.go index 18a40c980..3ff90e4b0 100644 --- a/pkg/product/mke/phase/validate_facts_test.go +++ b/pkg/product/mke/phase/validate_facts_test.go @@ -23,7 +23,7 @@ func TestValidateFactsMKEVersionJumpFail(t *testing.T) { }, }, } - require.EqualError(t, phase.validateMKEVersionJump(), "can't upgrade MKE directly from 3.1.1 to 3.3.3-tp9 - need to upgrade to 3.2 first") + require.ErrorContains(t, phase.validateMKEVersionJump(), "can't upgrade MKE directly from 3.1.1 to 3.3.3-tp9 - need to upgrade to 3.2 first") } func TestValidateFactsMKEVersionJumpDowngradeFail(t *testing.T) { @@ -39,7 +39,7 @@ func TestValidateFactsMKEVersionJumpDowngradeFail(t *testing.T) { }, }, } - require.EqualError(t, phase.validateMKEVersionJump(), "can't downgrade MKE 3.3.3-tp9 to 3.2.8") + require.ErrorContains(t, phase.validateMKEVersionJump(), "can't downgrade MKE 3.3.3-tp9 to 3.2.8") } func TestValidateFactsMKEVersionJumpSuccess(t *testing.T) { @@ -73,7 +73,7 @@ func TestValidateFactsMSRVersionJumpFail(t *testing.T) { }, }, } - require.EqualError(t, phase.validateMSRVersionJump(), "can't upgrade MSR directly from 2.6.4 to 2.8.4 - need to upgrade to 2.7 first") + require.ErrorContains(t, phase.validateMSRVersionJump(), "can't upgrade MSR directly from 2.6.4 to 2.8.4 - need to upgrade to 2.7 first") } func TestValidateFactsMSRVersionJumpDowngradeFail(t *testing.T) { phase := ValidateFacts{} @@ -90,7 +90,7 @@ func TestValidateFactsMSRVersionJumpDowngradeFail(t *testing.T) { }, }, } - require.EqualError(t, phase.validateMSRVersionJump(), "can't downgrade MSR 2.8.4 to 2.7.6") + require.ErrorContains(t, phase.validateMSRVersionJump(), "can't downgrade MSR 2.8.4 to 2.7.6") } func TestValidateFactsMSRVersionJumpSuccess(t *testing.T) { @@ -129,13 +129,13 @@ func TestValidateFactsValidateDataPlane(t *testing.T) { } // Test meta-vxlan: false, --calico-vxlan=true - require.EqualError(t, phase.validateDataPlane(), "calico configured with IPIP, can't automatically change to VXLAN") + require.ErrorContains(t, phase.validateDataPlane(), "calico configured with IPIP, can't automatically change to VXLAN") // Test meta-vxlan: false, --calico-vxlan (should evaluate to true) phase.Config.Spec.MKE.InstallFlags = []string{ "--calico-vxlan", } - require.EqualError(t, phase.validateDataPlane(), "calico configured with IPIP, can't automatically change to VXLAN") + require.ErrorContains(t, phase.validateDataPlane(), "calico configured with IPIP, can't automatically change to VXLAN") // Test with meta-vxlan: true, --calico-vxlan true phase.Config.Spec.MKE.Metadata.VXLAN = true @@ -145,7 +145,7 @@ func TestValidateFactsValidateDataPlane(t *testing.T) { phase.Config.Spec.MKE.InstallFlags = []string{ "--calico-vxlan=false", } - require.EqualError(t, phase.validateDataPlane(), "calico configured with VXLAN, can't automatically change to IPIP") + require.ErrorContains(t, phase.validateDataPlane(), "calico configured with VXLAN, can't automatically change to IPIP") // Test with meta-vxlan: false, --calico-vxlan false phase.Config.Spec.MKE.Metadata.VXLAN = false diff --git a/pkg/product/mke/phase/validate_hosts.go b/pkg/product/mke/phase/validate_hosts.go index 574be5c09..c83eddc4f 100644 --- a/pkg/product/mke/phase/validate_hosts.go +++ b/pkg/product/mke/phase/validate_hosts.go @@ -1,6 +1,7 @@ package phase import ( + "crypto/rand" "fmt" "io" "os" @@ -11,9 +12,6 @@ import ( "github.com/Mirantis/mcc/pkg/product/mke/api" "github.com/Mirantis/mcc/pkg/util" "github.com/k0sproject/rig/exec" - - "crypto/rand" - log "github.com/sirupsen/logrus" ) @@ -44,6 +42,8 @@ func (p *ValidateHosts) Run() error { return p.formatErrors() } +var errValidationFailed = fmt.Errorf("validation failed") + func (p *ValidateHosts) formatErrors() error { errorHosts := p.Config.Spec.Hosts.Filter(func(h *api.Host) bool { return h.Errors.Count() > 0 }) @@ -52,68 +52,82 @@ func (p *ValidateHosts) formatErrors() error { return fmt.Sprintf("%s:\n%s\n", h, h.Errors.String()) }) - return fmt.Errorf("%d of %d hosts failed validation:\n%s", len(errorHosts), len(p.Config.Spec.Hosts), strings.Join(messages, "\n")) + return fmt.Errorf("%w: %d of %d hosts failed validation:\n%s", errValidationFailed, len(errorHosts), len(p.Config.Spec.Hosts), strings.Join(messages, "\n")) } return nil } +var errContentMismatch = fmt.Errorf("content mismatch") + func (p *ValidateHosts) validateHostConnection() error { - f, err := os.CreateTemp("", "uploadTest") + testFile, err := os.CreateTemp("", "uploadTest") if err != nil { - return err + return fmt.Errorf("connection test failed: create temp file: %w", err) } defer os.Remove("uploadTest") - _, err = io.CopyN(f, rand.Reader, 1048576) // create an 1MB temp file full of random data + _, err = io.CopyN(testFile, rand.Reader, 1048576) // create an 1MB temp file full of random data if err != nil { - return err + return fmt.Errorf("connection test failed: write test file content: %w", err) } // TODO: validate content err = p.Config.Spec.Hosts.Each(func(h *api.Host) error { log.Infof("%s: testing file upload", h) - defer h.Configurer.DeleteFile(h, "launchpad.test") - err := h.WriteFileLarge(f.Name(), h.Configurer.JoinPath(h.Configurer.Pwd(h), "launchpad.test")) + defer func() { + if err := h.Configurer.DeleteFile(h, "launchpad.test"); err != nil { + log.Debugf("%s: failed to delete test file: %s", h, err.Error()) + } + }() + err := h.WriteFileLarge(testFile.Name(), h.Configurer.JoinPath(h.Configurer.Pwd(h), "launchpad.test")) if err != nil { h.Errors.Add(err.Error()) } - return err + return fmt.Errorf("failed to upload file: %w", err) }) if err != nil { - return err + return fmt.Errorf("connection test failed: upload: %w", err) } - return p.Config.Spec.Hosts.Each(func(h *api.Host) error { - fn := "launchpad.test" + err = p.Config.Spec.Hosts.Each(func(h *api.Host) error { + filename := "launchpad.test" testStr := "hello world!\n" - defer h.Configurer.DeleteFile(h, fn) + defer func() { + if err := h.Configurer.DeleteFile(h, filename); err != nil { + log.Debugf("%s: failed to delete test file: %s", h, err.Error()) + } + }() log.Infof("%s: testing stdin redirection", h) if h.IsWindows() { - err := h.Exec(fmt.Sprintf(`findstr "^" > %s`, fn), exec.Stdin(testStr)) + err := h.Exec(fmt.Sprintf(`findstr "^" > %s`, filename), exec.Stdin(testStr)) if err != nil { - return err + return fmt.Errorf("failed to test stdin redirection: %w", err) } } else { - err := h.Exec(fmt.Sprintf("cat > %s", fn), exec.Stdin(testStr)) + err := h.Exec(fmt.Sprintf("cat > %s", filename), exec.Stdin(testStr)) if err != nil { - return err + return fmt.Errorf("failed to test stdin redirection: %w", err) } } - content, err := h.Configurer.ReadFile(h, fn) + content, err := h.Configurer.ReadFile(h, filename) if err != nil { - return err + return fmt.Errorf("failed to read file: %w", err) } if strings.TrimSpace(content) != strings.TrimSpace(testStr) { // Allow trailing linefeeds etc, mainly because windows is weird. - return fmt.Errorf("file write test content check mismatch: %q vs %q", strings.TrimSpace(content), strings.TrimSpace(testStr)) + return fmt.Errorf("%w: file write test content check mismatch: %q vs %q", errContentMismatch, strings.TrimSpace(content), strings.TrimSpace(testStr)) } return nil }) + if err != nil { + return fmt.Errorf("connection test failed: %w", err) + } + return nil } func (p *ValidateHosts) validateLocalhost() { - p.Config.Spec.Hosts.ParallelEach(func(h *api.Host) error { + _ = p.Config.Spec.Hosts.ParallelEach(func(h *api.Host) error { if err := h.Configurer.ValidateLocalhost(h); err != nil { h.Errors.Add(err.Error()) } @@ -122,19 +136,20 @@ func (p *ValidateHosts) validateLocalhost() { } func (p *ValidateHosts) validateHostLocalAddresses() { - p.Config.Spec.Hosts.ParallelEach(p.validateHostLocalAddress) + _ = p.Config.Spec.Hosts.ParallelEach(p.validateHostLocalAddress) } func (p *ValidateHosts) validateHostLocalAddress(h *api.Host) error { localAddresses, err := h.Configurer.LocalAddresses(h) if err != nil { h.Errors.Add(fmt.Sprintf("failed to find host local addresses: %s", err.Error())) - return err + return nil } if !util.StringSliceContains(localAddresses, h.Metadata.InternalAddress) { - h.Errors.Add(fmt.Sprintf("discovered private address %s does not seem to be a node local address (%s). Make sure you've set correct 'privateInterface' for the host in config", h.Metadata.InternalAddress, strings.Join(localAddresses, ","))) - return err + msg := fmt.Sprintf("discovered private address %s does not seem to be a node local address (%s). Make sure you've set correct 'privateInterface' for the host in config", h.Metadata.InternalAddress, strings.Join(localAddresses, ",")) + h.Errors.Add(msg) + return nil } return nil @@ -144,7 +159,7 @@ func (p *ValidateHosts) validateHostnameUniqueness() { log.Infof("validating hostname uniqueness") hostnames := make(map[string]api.Hosts) - p.Config.Spec.Hosts.Each(func(h *api.Host) error { + _ = p.Config.Spec.Hosts.Each(func(h *api.Host) error { hostnames[h.Metadata.Hostname] = append(hostnames[h.Metadata.Hostname], h) return nil }) @@ -152,7 +167,7 @@ func (p *ValidateHosts) validateHostnameUniqueness() { for hn, hosts := range hostnames { if len(hosts) > 1 { others := strings.Join(hosts.MapString(func(h *api.Host) string { return h.Address() }), ", ") - hosts.Each(func(h *api.Host) error { + _ = hosts.Each(func(h *api.Host) error { h.Errors.Addf("duplicate hostname '%s' found on hosts %s", hn, others) return nil }) @@ -161,13 +176,14 @@ func (p *ValidateHosts) validateHostnameUniqueness() { } func (p *ValidateHosts) validateDockerGroup() { - p.Config.Spec.Hosts.ParallelEach(func(h *api.Host) error { + _ = p.Config.Spec.Hosts.ParallelEach(func(h *api.Host) error { if !h.IsLocal() || h.IsWindows() { return nil } if err := h.Exec("getent group docker"); err != nil { - return fmt.Errorf("group 'docker' required to exist when running on localhost connection") + h.Errors.Addf("group 'docker' required to exist when running on localhost connection") + return nil } if h.Exec(`[ "$(id -u)" = 0 ]`) == nil { @@ -177,7 +193,7 @@ func (p *ValidateHosts) validateDockerGroup() { if err := h.Exec("groups | grep -q docker"); err != nil { log.Errorf("%s: user must be root or a member of the group 'docker' when running on localhost connection.", h) log.Errorf("%s: use 'sudo groupadd -f -g 999 docker && sudo usermod -aG docker $USER' and re-login before running launchpad again.", h) - return fmt.Errorf("user must be root or a member of the group 'docker'") + h.Errors.Addf("user must be root or a member of the group 'docker'") } return nil diff --git a/pkg/product/mke/phase/validate_mke_health.go b/pkg/product/mke/phase/validate_mke_health.go index 984d3ea2a..833cbabe7 100644 --- a/pkg/product/mke/phase/validate_mke_health.go +++ b/pkg/product/mke/phase/validate_mke_health.go @@ -3,6 +3,7 @@ package phase import ( "crypto/tls" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -35,7 +36,7 @@ func (p *ValidateMKEHealth) Run() error { swarmLeader := p.Config.Spec.SwarmLeader() if err := p.Config.Spec.CheckMKEHealthLocal(swarmLeader); err != nil { - return err + return fmt.Errorf("%w: failed to validate MKE health: %w", errValidationFailed, err) } retries := p.Config.Spec.MKE.NodesHealthRetry @@ -49,31 +50,42 @@ func (p *ValidateMKEHealth) Run() error { url, err := p.Config.Spec.MKEURL() if err != nil { - return err + return fmt.Errorf("%w: get mke url: %w", errValidationFailed, err) } user := p.Config.Spec.MKE.AdminUsername if user == "" { - return fmt.Errorf("config Spec.MKE.AdminUsername not set") + return fmt.Errorf("%w: config Spec.MKE.AdminUsername not set", errValidationFailed) } pass := p.Config.Spec.MKE.AdminPassword if pass == "" { - return fmt.Errorf("config Spec.MKE.AdminPassword not set") + return fmt.Errorf("%w: config Spec.MKE.AdminPassword not set", errValidationFailed) } delay, _ := time.ParseDuration("10s") // Retry for total of 150 seconds - return retry.Do( + err = retry.Do( func() error { log.Infof("%s: waiting for MKE nodes to become healthy", h) - return checkMKENodesReady(url, tlsConfig, user, pass) + if err := checkMKENodesReady(url, tlsConfig, user, pass); err != nil { + return fmt.Errorf("mke not ready: %w", err) + } + return nil }, retry.Attempts(retries), retry.Delay(delay), ) + if err != nil { + return fmt.Errorf("%w: failed to validate MKE health: %w", errValidationFailed, err) + } } return nil } +var ( + errRequestFailed = errors.New("request failed") + errNodeNotReady = errors.New("node not ready") +) + // checkMKENodesReady verifies the MKE nodes are in 'ready' state. func checkMKENodesReady(mkeURL *url.URL, tlsConfig *tls.Config, username, password string) error { client := &http.Client{ @@ -85,7 +97,7 @@ func checkMKENodesReady(mkeURL *url.URL, tlsConfig *tls.Config, username, passwo // Login and get a token for the user token, err := mke.GetToken(client, mkeURL, username, password) if err != nil { - return fmt.Errorf("failed to get token for (%s:%s) : %s", username, password, err) + return fmt.Errorf("failed to get token: %w", err) } mkeURL.Path = "/nodes" @@ -93,32 +105,35 @@ func checkMKENodesReady(mkeURL *url.URL, tlsConfig *tls.Config, username, passwo // Perform the request req, err := http.NewRequest(http.MethodGet, mkeURL.String(), nil) if err != nil { - return err + return fmt.Errorf("failed to create request for %s: %w", mkeURL.String(), err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) resp, err := client.Do(req) if err != nil { log.Debugf("Failed to get response from %s: %v", mkeURL.String(), err) - return err + return fmt.Errorf("failed to get response from %s: %w", mkeURL.String(), err) } - body, err := io.ReadAll(resp.Body) - resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to poll /nodes endpoint. (%d): %w", resp.StatusCode, err) + } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - if err == nil { - return fmt.Errorf("failed to poll /nodes endpoint. (%d): %s", resp.StatusCode, string(body)) - } - return err + return fmt.Errorf("%w: failed to poll /nodes endpoint. (http %d)", errRequestFailed, resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) } var nodes []api.Node if err := json.Unmarshal(body, &nodes); err != nil { - return err + return fmt.Errorf("failed to unmarshal response body: %w", err) } for _, node := range nodes { if !node.IsReady() { log.Debugf("node %+v is not in ready state. State: '%+s'", node, node.Status.State) - return fmt.Errorf("node %+v is in state '%+s'", node, node.Status.State) + return fmt.Errorf("%w: node %+v is in state '%+s'", errNodeNotReady, node, node.Status.State) } } diff --git a/pkg/product/mke/reset.go b/pkg/product/mke/reset.go index 7d7b6510e..0832c8780 100644 --- a/pkg/product/mke/reset.go +++ b/pkg/product/mke/reset.go @@ -1,6 +1,8 @@ package mke import ( + "fmt" + "github.com/Mirantis/mcc/pkg/phase" common "github.com/Mirantis/mcc/pkg/product/common/phase" mke "github.com/Mirantis/mcc/pkg/product/mke/phase" @@ -29,5 +31,8 @@ func (p *MKE) Reset() error { &common.Disconnect{}, ) - return phaseManager.Run() + if err := phaseManager.Run(); err != nil { + return fmt.Errorf("reset failed: %w", err) + } + return nil } diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go new file mode 100644 index 000000000..3dd9df6ef --- /dev/null +++ b/pkg/retry/retry.go @@ -0,0 +1,58 @@ +package retry + +import ( + "fmt" + "time" + + retry "github.com/avast/retry-go" +) + +var Forever = false + +func Infinite(task retry.RetryableFunc) error { + for { + if task() == nil { + return nil + } + } +} + +func Backoff(task retry.RetryableFunc, opts ...retry.Option) error { + if Forever { + return Infinite(task) + } + + opts = append( + []retry.Option{ + retry.LastErrorOnly(true), + }, + opts..., + ) + + if err := retry.Do(task, opts...); err != nil { + return fmt.Errorf("retry with backoff: %w", err) + } + return nil +} + +func Periodic(task retry.RetryableFunc, opts ...retry.Option) error { + if Forever { + return Infinite(task) + } + + opts = append( + []retry.Option{ + retry.DelayType(retry.CombineDelay(retry.FixedDelay, retry.RandomDelay)), + retry.MaxJitter(time.Second * 2), + retry.Delay(time.Second * 3), + retry.Attempts(60), + retry.LastErrorOnly(true), + }, + opts..., + ) + + if err := retry.Do(task, opts...); err != nil { + return fmt.Errorf("retry with periodic: %w", err) + } + return nil +} diff --git a/pkg/swarm/swarm.go b/pkg/swarm/swarm.go index 767b09ee0..b90d2f607 100644 --- a/pkg/swarm/swarm.go +++ b/pkg/swarm/swarm.go @@ -1,6 +1,8 @@ package swarm import ( + "fmt" + "github.com/Mirantis/mcc/pkg/product/mke/api" log "github.com/sirupsen/logrus" ) @@ -22,7 +24,11 @@ func IsSwarmNode(h *api.Host) bool { // NodeID returns the hosts node id in swarm cluster. func NodeID(h *api.Host) (string, error) { - return h.ExecOutput(h.Configurer.DockerCommandf(`info --format "{{.Swarm.NodeID}}"`)) + out, err := h.ExecOutput(h.Configurer.DockerCommandf(`info --format "{{.Swarm.NodeID}}"`)) + if err != nil { + return "", fmt.Errorf("failed to get host's swarm node id: %w", err) + } + return out, nil } // ClusterID digs the swarm cluster id from swarm leader host. diff --git a/pkg/util/install.go b/pkg/util/install.go index a48241f3b..98ddaaa5c 100644 --- a/pkg/util/install.go +++ b/pkg/util/install.go @@ -13,9 +13,11 @@ import ( func SetupLicenseFile(licenseFilePath string) (string, error) { license, err := os.ReadFile(licenseFilePath) if err != nil { - return "", err + return "", fmt.Errorf("failed to read license file: %w", err) } + licenseFlag := fmt.Sprintf("--license '%s'", string(license)) + return licenseFlag, nil } diff --git a/pkg/util/io.go b/pkg/util/io.go index ae9e6cf85..42c61c388 100644 --- a/pkg/util/io.go +++ b/pkg/util/io.go @@ -12,7 +12,7 @@ func EnsureDir(dirPath string) error { if _, serr := os.Stat(dirPath); os.IsNotExist(serr) { merr := os.MkdirAll(dirPath, os.ModePerm) if merr != nil { - return merr + return fmt.Errorf("failed to create directory %s: %w", dirPath, merr) } } return nil @@ -22,19 +22,19 @@ func EnsureDir(dirPath string) error { var LoadExternalFile = func(path string) ([]byte, error) { realpath, err := homedir.Expand(path) if err != nil { - return []byte{}, err + return []byte{}, fmt.Errorf("failed to expand path %s: %w", path, err) } filedata, err := os.ReadFile(realpath) if err != nil { - return []byte{}, err + return []byte{}, fmt.Errorf("failed to read file %s: %w", realpath, err) } return filedata, nil } // FormatBytes formats a number of bytes into something like "200 KiB". func FormatBytes(bytes uint64) string { - f := float64(bytes) + floatBytes := float64(bytes) units := []string{ "bytes", "KiB", @@ -42,9 +42,9 @@ func FormatBytes(bytes uint64) string { "GiB", } logBase1024 := 0 - for f > 1024.0 && logBase1024 < len(units) { - f /= 1024.0 + for floatBytes > 1024.0 && logBase1024 < len(units) { + floatBytes /= 1024.0 logBase1024++ } - return fmt.Sprintf("%d %s", uint64(f), units[logBase1024]) + return fmt.Sprintf("%d %s", uint64(floatBytes), units[logBase1024]) } diff --git a/pkg/util/logo.go b/pkg/util/logo.go index a58bcdef7..94144111b 100644 --- a/pkg/util/logo.go +++ b/pkg/util/logo.go @@ -3,7 +3,7 @@ package util -// Logo logo. +// Logo is the logo displayed on startup. var Logo = `                                                                                                         ..,,,,,..                                                 diff --git a/version/version.go b/version/version.go index 4787d275f..921b16c10 100644 --- a/version/version.go +++ b/version/version.go @@ -43,9 +43,9 @@ func (a *Asset) IsForHost() bool { return false } - os := runtime.GOOS - if os == "windows" { - os = "win" + goos := runtime.GOOS + if goos == "windows" { + goos = "win" } arch := runtime.GOARCH @@ -54,7 +54,7 @@ func (a *Asset) IsForHost() bool { } parts := strings.Split(strings.TrimSuffix(a.Name, ".exe"), "-") - return parts[1] == os && parts[2] == arch + return parts[1] == goos && parts[2] == arch } // LaunchpadRelease describes a launchpad release. @@ -105,7 +105,7 @@ func latestTag(timeout time.Duration) string { if resp.Body != nil { defer resp.Body.Close() } - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { log.Debugf("%s returned http %d", baseMsg, resp.StatusCode) return "" // ignore backend failures } @@ -169,7 +169,7 @@ func GetLatest(timeout time.Duration) *LaunchpadRelease { if resp.Body != nil { defer resp.Body.Close() } - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { log.Debugf("%s returned http %d", baseMsg, resp.StatusCode) return nil // ignore backend failures }