diff --git a/.golangci.yml b/.golangci.yml index 93f310f2..d02fdc9b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -11,7 +11,7 @@ linters: - contextcheck - dupword - durationcheck - - err113 + # - err113 - errchkjson - errname - errorlint @@ -38,6 +38,7 @@ linters: - unparam - usestdlibvars - usetesting + - varnamelen - wastedassign - whitespace - wrapcheck @@ -54,7 +55,8 @@ linters: - wg sync.WaitGroup - h Host - h os.Host - - h *api.Host + - h mkeconfig.Host + - h *mkeconfig.Host - ok bool - s string exclusions: diff --git a/examples/terraform/aws-simple/launchpad.tf b/examples/terraform/aws-simple/launchpad.tf index 8ebcdd24..6b7d4732 100644 --- a/examples/terraform/aws-simple/launchpad.tf +++ b/examples/terraform/aws-simple/launchpad.tf @@ -14,7 +14,7 @@ variable "launchpad" { type = object({ drain = bool - mcr_version = string + mcr_channel = string mke_version = string msr_version = string // unused if you have no MSR hosts @@ -137,8 +137,8 @@ locals { // ------- Ye old launchpad yaml (just for debugging) locals { - launchpad_yaml_15 = <<-EOT -apiVersion: launchpad.mirantis.com/mke/v1.5 + launchpad_yaml = <<-EOT +apiVersion: launchpad.mirantis.com/mke/v1.6 kind: mke%{if local.has_msr}+msr%{endif} metadata: name: ${var.name} @@ -165,11 +165,9 @@ spec: insecure: ${h.winrm_insecure} %{~endfor} mcr: - version: ${var.launchpad.mcr_version} + channel: ${var.launchpad.mcr_channel} repoURL: https://repos.mirantis.com - installURLLinux: https://get.mirantis.com/ installURLWindows: https://get.mirantis.com/install.ps1 - channel: stable-25.0 prune: true mke: version: ${var.launchpad.mke_version} @@ -199,7 +197,7 @@ EOT output "launchpad_yaml" { description = "launchpad config file yaml (for debugging)" sensitive = true - value = local.launchpad_yaml_15 + value = local.launchpad_yaml } output "mke_connect" { diff --git a/examples/terraform/aws-simple/terraform.tfvars.template b/examples/terraform/aws-simple/terraform.tfvars.template index 117c9fa4..24597f30 100644 --- a/examples/terraform/aws-simple/terraform.tfvars.template +++ b/examples/terraform/aws-simple/terraform.tfvars.template @@ -8,13 +8,13 @@ aws = { launchpad = { drain = false - mcr_version = "25.0.13" + mcr_channel = "stable-25.0.14" mke_version = "3.8.8" msr_version = "2.9.28" mke_connect = { username = "admin" - password = "" // an MKE passwords must be provided + password = "" // an MKE password must be provided insecure = true } @@ -32,7 +32,7 @@ subnets = { "main" = { "cidr" = "172.31.0.0/17", "private" = false, - "nodegroups" = ["MngrA", "WrkA", "MsrA"] + "nodegroups" = ["MngrA", "Wrkubuntu", "Wrkrhel", "Wrksles"] } } @@ -46,22 +46,30 @@ nodegroups = { "role" = "manager", "user_data" = "sudo ufw allow 7946/tcp ; sudo ufw allow 10250/tcp " }, - "WrkA" = { - "platform" = "ubuntu_22.04", + "Wrkrhel" = { + "platform" = "rhel_9", "count" = 1, "type" = "c6a.xlarge", "volume_size" = "100", "role" = "worker", "user_data" = "sudo ufw allow 7946/tcp ; sudo ufw allow 10250/tcp " - } - "MsrA" = { + }, + "Wrkubuntu" = { "platform" = "ubuntu_22.04", "count" = 1, "type" = "c6a.xlarge", "volume_size" = "100", - "role" = "msr", + "role" = "worker", "user_data" = "sudo ufw allow 7946/tcp ; sudo ufw allow 10250/tcp " - } + }, + "Wrksles" = { + "platform" = "sles_15", + "count" = 1, + "type" = "c6a.xlarge", + "volume_size" = "100", + "role" = "worker", + "user_data" = "sudo ufw allow 7946/tcp ; sudo ufw allow 10250/tcp " + }, } // set a windows password, if you have windows nodes diff --git a/jn-todo.org b/jn-todo.org new file mode 100644 index 00000000..0da617ac --- /dev/null +++ b/jn-todo.org @@ -0,0 +1,20 @@ +* JNesbitt informal todo list + +** code-quality + +*** TODO Move product/mke/api/host.go:Host.MCRConfigure to /pkg/configurer interfaces + +The MCRConfigure() method likely exists on the host as a convenience, but as we have common +configurer functionality and the other MCR methods are on the configurer it doesn't belong +on the host. + +Another option would be to move functionality to the /pkg/mcr package. + +On top of this, the configurer.MCRConfigPath should perhaps be an accessor pair of content, +instead of a filepath. Maybe the configurer should have methods to write/read the MCR configuration. + +** linting + +*** general interface{} -> any replace + +There are a lot of `interface{}` types declared in the code base that should be changed to `any` diff --git a/pkg/analytics/analytics_test.go b/pkg/analytics/analytics_test.go index bcaae2a6..d6e437ff 100644 --- a/pkg/analytics/analytics_test.go +++ b/pkg/analytics/analytics_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/Mirantis/launchpad/pkg/config/user" - "github.com/stretchr/testify/require" "github.com/segmentio/analytics-go/v3" + "github.com/stretchr/testify/require" ) type mockClient struct { diff --git a/pkg/config/migration/v15/v15.go b/pkg/config/migration/v15/v15.go new file mode 100644 index 00000000..3037e3b6 --- /dev/null +++ b/pkg/config/migration/v15/v15.go @@ -0,0 +1,56 @@ +package v15 + +import ( + "fmt" + "strings" + + "github.com/Mirantis/launchpad/pkg/config/migration" + log "github.com/sirupsen/logrus" +) + +// Migrate migrates a v1.5 format configuration into the v1.6 api format and replaces the contents of the supplied data byte slice. +// +// v1.6 uses only an MCR channel, dropping the MCR Version value. A migrated channel can be made from the combined v1.5 channel-version +func Migrate(plain map[string]any) error { + plain["apiVersion"] = "launchpad.mirantis.com/mke/v1.6" + if spec, ok := plain["spec"].(map[any]any); ok { + if mcr, ok := spec["mcr"].(map[any]any); ok { + version := mcr["version"] // if "version: 25.0" it could be parsed as a float64 instead of a string + channel := mcr["channel"] + + channels, ok := channel.(string) + if !ok { + return fmt.Errorf("could not migrate non-string channel, expected something like 'test' or 'stable-25.0`, but got '%s'", channel) // this is not likely + } + + versions, ok := version.(string) + if !ok { // handle some frustrating yaml parsing issues for version v`alues + if versionf, ok := version.(float64); ok { // version: 25.0 : produces`` a float64 + versions = fmt.Sprintf("%.1f", versionf) + } else if versioni, ok := version.(int); ok { // version: 25 : produces an integer + versions = fmt.Sprintf("%d.0", versioni) + } else { + log.Warnf("unclear version for migration: %+v (%T)", version, version) + } + } + + // channel might already be `stable-25.0` but a version was passed, so we overrid it with a version passed + if channelsParts := strings.Split(channels, "-"); len(channelsParts) > 1 { + log.Warnf("when migrating to v1.6, a version (%s) and a channel (%s) were passed, but we combined them to use: %s-%s", version, channel, channels, versions) + channels = channelsParts[0] + } + + mcr["channel"] = fmt.Sprintf("%s-%s", channels, versions) + delete(mcr, "version") + } + } + + log.Debugf("migrated configuration from launchpad.mirantis.com/v1.4 to launchpad.mirantis.com/mke/v1.5") + log.Infof("Note: The configuration has been migrated from a previous version") + log.Infof(" to see the migrated configuration use: launchpad describe config") + return nil +} + +func init() { + migration.Register("launchpad.mirantis.com/mke/v1.5", Migrate) +} diff --git a/pkg/config/migration/v15/v15_test.go b/pkg/config/migration/v15/v15_test.go new file mode 100644 index 00000000..1af9ffe3 --- /dev/null +++ b/pkg/config/migration/v15/v15_test.go @@ -0,0 +1,110 @@ +package v15 + +import ( + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestVersionMigrationSimple(t *testing.T) { + v15 := []byte(`--- +apiVersion: launchpad.mirantis.com/mke/v1.5 +kind: mke +spec: + mcr: + channel: stable + repoURL: https://repos.mirantis.com + version: 25.0.14 + mke: + adminUsername: admin + imageRepo: docker.io/mirantis + version: 3.8.6 +`) + + v16 := []byte(`apiVersion: launchpad.mirantis.com/mke/v1.6 +kind: mke +spec: + mcr: + channel: stable-25.0.14 + repoURL: https://repos.mirantis.com + mke: + adminUsername: admin + imageRepo: docker.io/mirantis + version: 3.8.6 +`) + in := make(map[string]any) + require.NoError(t, yaml.Unmarshal(v15, in)) + require.NoError(t, Migrate(in)) + out, err := yaml.Marshal(in) + require.NoError(t, err) + require.Equal(t, string(v16), string(out)) +} + +func TestVersionMigrationDefault(t *testing.T) { + v15 := []byte(`--- +apiVersion: launchpad.mirantis.com/mke/v1.5 +kind: mke +spec: + mcr: + channel: stable + repoURL: https://repos.mirantis.com + version: 25.0 + mke: + adminUsername: admin + imageRepo: docker.io/mirantis + version: 3.8.6 +`) + + v16 := []byte(`apiVersion: launchpad.mirantis.com/mke/v1.6 +kind: mke +spec: + mcr: + channel: stable-25.0 + repoURL: https://repos.mirantis.com + mke: + adminUsername: admin + imageRepo: docker.io/mirantis + version: 3.8.6 +`) + in := make(map[string]any) + require.NoError(t, yaml.Unmarshal(v15, in)) + require.NoError(t, Migrate(in)) + out, err := yaml.Marshal(in) + require.NoError(t, err) + require.Equal(t, string(v16), string(out)) +} + +func TestVersionMigrationSlim(t *testing.T) { + v15 := []byte(`--- +apiVersion: launchpad.mirantis.com/mke/v1.5 +kind: mke +spec: + mcr: + channel: stable + repoURL: https://repos.mirantis.com + version: 25 + mke: + adminUsername: admin + imageRepo: docker.io/mirantis + version: 3.8.6 +`) + + v16 := []byte(`apiVersion: launchpad.mirantis.com/mke/v1.6 +kind: mke +spec: + mcr: + channel: stable-25.0 + repoURL: https://repos.mirantis.com + mke: + adminUsername: admin + imageRepo: docker.io/mirantis + version: 3.8.6 +`) + in := make(map[string]any) + require.NoError(t, yaml.Unmarshal(v15, in)) + require.NoError(t, Migrate(in)) + out, err := yaml.Marshal(in) + require.NoError(t, err) + require.Equal(t, string(v16), string(out)) +} diff --git a/pkg/configurer/centos/centos.go b/pkg/configurer/centos/centos.go index 000b2aff..464395d7 100644 --- a/pkg/configurer/centos/centos.go +++ b/pkg/configurer/centos/centos.go @@ -1,11 +1,8 @@ package centos import ( - "fmt" - "github.com/Mirantis/launchpad/pkg/configurer/enterpriselinux" "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os" "github.com/k0sproject/rig/os/registry" ) @@ -14,20 +11,12 @@ type Configurer struct { enterpriselinux.Configurer } -// InstallMKEBasePackages install all the needed base packages on the host. -func (c Configurer) InstallMKEBasePackages(h os.Host) error { - 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() { registry.RegisterOSModule( func(os rig.OSVersion) bool { return os.ID == "centos" }, - func() interface{} { + func() any { return Configurer{} }, ) diff --git a/pkg/configurer/enterpriselinux/el.go b/pkg/configurer/enterpriselinux/el.go index 1aea8961..f812fd38 100644 --- a/pkg/configurer/enterpriselinux/el.go +++ b/pkg/configurer/enterpriselinux/el.go @@ -26,8 +26,45 @@ func (c Configurer) InstallMKEBasePackages(h os.Host) error { return nil } +// InstallMCR install Docker EE engine on Linux. +func (c Configurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { + ver, verErr := configurer.ResolveLinux(h) + if verErr != nil { + return fmt.Errorf("could not discover Linux version information") + } + + if isEC2 := c.isAWSInstance(h); !isEC2 { + log.Debugf("%s: confirmed that this is not an AWS instance", h) + } else if c.InstallPackage(h, "rh-amazon-rhui-client") == nil { + log.Infof("%s: appears to be an AWS EC2 instance, installed rh-amazon-rhui-client", h) + } + + // e.g. https://repos.mirantis.com/rhel/$releasever/$basearch/ + baseURL := fmt.Sprintf("%s/%s/%s/%s/%s", engineConfig.RepoURL, ver.ID, "$releasever", "$basearch", engineConfig.Channel) + // e.g. https://repos.mirantis.com/oraclelinux/gpg + gpgURL := fmt.Sprintf("%s/%s/gpg", engineConfig.RepoURL, ver.ID) + elRepoFilePath := "/etc/yum.repos.d/docker-ee.repo" + elRepoTemplate := `[mirantis] +name=Mirantis Container Runtime +baseurl=%s +enabled=1 +gpgcheck=1 +gpgkey=%s +` + elRepo := fmt.Sprintf(elRepoTemplate, baseURL, gpgURL) + + if err := c.WriteFile(h, elRepoFilePath, elRepo, "0600"); err != nil { + return fmt.Errorf("could not write Yum repo file for MCR") + } + + if err := c.InstallPackage(h, "docker-ee"); err != nil { + return fmt.Errorf("package manager could not install docker-ee") + } + return nil +} + // UninstallMCR uninstalls docker-ee engine. -func (c Configurer) UninstallMCR(h os.Host, _ string, engineConfig commonconfig.MCRConfig) error { +func (c Configurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { info, getDockerError := c.GetDockerInfo(h) if engineConfig.Prune { defer c.CleanupLingeringMCR(h, info) @@ -53,20 +90,6 @@ func (c Configurer) UninstallMCR(h os.Host, _ string, engineConfig commonconfig. return nil } -// InstallMCR install Docker EE engine on Linux. -func (c Configurer) InstallMCR(h os.Host, scriptPath string, engineConfig commonconfig.MCRConfig) error { - if isEC2 := c.isAWSInstance(h); !isEC2 { - log.Debugf("%s: confirmed that this is not an AWS instance", h) - } else if c.InstallPackage(h, "rh-amazon-rhui-client") == nil { - log.Infof("%s: appears to be an AWS EC2 instance, installed rh-amazon-rhui-client", h) - } - - if err := c.LinuxConfigurer.InstallMCR(h, scriptPath, engineConfig); err != nil { - return fmt.Errorf("failed to install MCR: %w", err) - } - return nil -} - // function to check if the host is an AWS instance - https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html func (c Configurer) isAWSInstance(h os.Host) bool { found, err := h.ExecOutput("curl -s -m 5 http://169.254.169.254/latest/dynamic/instance-identity/document | grep region") diff --git a/pkg/configurer/enterpriselinux/rhel.go b/pkg/configurer/enterpriselinux/rhel.go index 63349717..c2597e85 100644 --- a/pkg/configurer/enterpriselinux/rhel.go +++ b/pkg/configurer/enterpriselinux/rhel.go @@ -15,7 +15,7 @@ func init() { func(os rig.OSVersion) bool { return os.ID == "rhel" }, - func() interface{} { + func() any { return Rhel{} }, ) diff --git a/pkg/configurer/enterpriselinux/rockylinux.go b/pkg/configurer/enterpriselinux/rockylinux.go index 101b9b95..8b7147ad 100644 --- a/pkg/configurer/enterpriselinux/rockylinux.go +++ b/pkg/configurer/enterpriselinux/rockylinux.go @@ -15,7 +15,7 @@ func init() { func(os rig.OSVersion) bool { return os.ID == "rocky" }, - func() interface{} { + func() any { return RockyLinux{} }, ) diff --git a/pkg/configurer/installer.go b/pkg/configurer/installer.go new file mode 100644 index 00000000..a1f977cc --- /dev/null +++ b/pkg/configurer/installer.go @@ -0,0 +1,124 @@ +package configurer + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + + "github.com/Mirantis/launchpad/pkg/util/fileutil" + log "github.com/sirupsen/logrus" +) + +var ( + downloadedInstallers = map[string]string{} // Global list of downloaded installers, to prevent repetition. + ErrInstallerDownloadFailed = errors.New("could not download installer") +) + +func GetInstaller(source string) (string, error) { + path, ok := downloadedInstallers[source] + if ok { + return path, nil + } + + if path == "" { + return "", fmt.Errorf("%w; skipping failed installer download", ErrInstallerDownloadFailed) + } + + path, getErr := downloadInstaller(source) + if getErr != nil { + return "", fmt.Errorf("%w, installer download failed; %s", ErrInstallerDownloadFailed, getErr.Error()) + } + downloadedInstallers[source] = path + return path, nil +} + +// Run does all the prep work on the hosts in parallel. +func downloadInstaller(path string) (string, error) { + winScript, err := getScript(path) + if err != nil { + return "", fmt.Errorf("failed to get Windows installer script: %w", err) + } + f, err := os.CreateTemp("", "installerWindows") + if err != nil { + return "", fmt.Errorf("failed to create temporary file for windows installer script: %w", err) + } + + _, err = f.WriteString(winScript) + if err != nil { + return "", fmt.Errorf("failed to write to temporary file for windows installer script: %w", err) + } + + return f.Name(), nil +} + +func parseURL(uri string) (*url.URL, error) { + if !strings.Contains(uri, "://") { + return &url.URL{Path: uri, Scheme: "file"}, nil + } + + 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 getScript(uri string) (string, error) { + u, err := parseURL(uri) + if err != nil { + return "", err + } + + var data string + + if u.Scheme == "file" { + data, err = readFile(u.Path) + } else { + data, err = downloadFile(uri) + } + + log.Debugf("read %d bytes from %s", len(data), uri) + + if err != nil { + return "", err + } + + if len(data) < 10 { + // cant fit an installer into that! + return "", fmt.Errorf("%w: script is too short", errInvalidScript) + } + + if !strings.HasPrefix(data, "#") { + log.Warnf("possibly invalid container runtime install script in %s", uri) + } + + return data, nil +} + +func downloadFile(url string) (string, error) { + log.Infof("downloading container runtime install script from %s", 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 "", 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 "", fmt.Errorf("failed to read response body: %w", err) + } + return string(body), nil +} + +func readFile(path string) (string, error) { + log.Infof("reading container runtime install script from %s", path) + + data, err := fileutil.LoadExternalFile(path) + return string(data), err +} diff --git a/pkg/configurer/linux.go b/pkg/configurer/linux.go index 9c0af06f..42c9af64 100644 --- a/pkg/configurer/linux.go +++ b/pkg/configurer/linux.go @@ -3,7 +3,6 @@ package configurer import ( "errors" "fmt" - "io/fs" "path" "path/filepath" "regexp" @@ -14,6 +13,7 @@ import ( "github.com/Mirantis/launchpad/pkg/constant" commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" "github.com/Mirantis/launchpad/pkg/util/iputil" + "github.com/k0sproject/rig" "github.com/k0sproject/rig/exec" "github.com/k0sproject/rig/os" log "github.com/sirupsen/logrus" @@ -26,6 +26,8 @@ const ( SbinPath = `PATH=/usr/local/sbin:/usr/sbin:/sbin:$PATH` ) +var ErrLinuxMCRInstall = errors.New("failed to install MCR on linux") + // LinuxConfigurer is a generic linux host configurer. type LinuxConfigurer struct { riglinux os.Linux @@ -54,60 +56,6 @@ func (c LinuxConfigurer) InstallMCRLicense(h os.Host, lic string) error { return nil } -// InstallMCR install MCR on Linux. -func (c LinuxConfigurer) InstallMCR(h os.Host, scriptPath string, engineConfig commonconfig.MCRConfig) error { - base := path.Base(scriptPath) - - installScriptDir := engineConfig.InstallScriptRemoteDirLinux - if installScriptDir == "" { - installScriptDir = c.riglinux.Pwd(h) - } - - _, err := h.ExecOutput(fmt.Sprintf("mkdir -p %s", installScriptDir)) - if err != nil { - return fmt.Errorf("failed to create directory %s: %w", installScriptDir, err) - } - - installer := path.Join(installScriptDir, base) - - err = h.Upload(scriptPath, installer, fs.FileMode(0o640)) - if err != nil { - log.Errorf("failed: %s", err.Error()) - return fmt.Errorf("upload %s to %s: %w", scriptPath, installer, err) - } - defer func() { - if err := c.riglinux.DeleteFile(h, installer); err != nil { - log.Warnf("failed to delete installer script: %s", err.Error()) - } - }() - - envs := fmt.Sprintf("DOCKER_URL=%s CHANNEL=%s VERSION=%s ", engineConfig.RepoURL, engineConfig.Channel, engineConfig.Version) - if engineConfig.AdditionalRuntimes != "" { - envs += fmt.Sprintf("ADDITIONAL_RUNTIMES=%s ", engineConfig.AdditionalRuntimes) - } - if engineConfig.DefaultRuntime != "" { - envs += fmt.Sprintf("DEFAULT_RUNTIME=%s ", engineConfig.DefaultRuntime) - } - cmd := envs + fmt.Sprintf("bash %s", escape.Quote(installer)) - - log.Infof("%s: running installer", h) - log.Debugf("%s: installer command: %s", h, cmd) - - if err := h.Exec(cmd); err != nil { - return fmt.Errorf("run MCR installer: %w", err) - } - - if err := c.riglinux.EnableService(h, "docker"); err != nil { - 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 nil -} - // RestartMCR restarts Docker EE engine. func (c LinuxConfigurer) RestartMCR(h os.Host) error { if err := c.riglinux.RestartService(h, "docker"); err != nil { @@ -153,7 +101,7 @@ func (c LinuxConfigurer) ResolveInternalIP(h os.Host, privateInterface, publicIP // DockerCommandf accepts a printf-like template string and arguments // and builds a command string for running the docker cli on the host. -func (c LinuxConfigurer) DockerCommandf(template string, args ...interface{}) string { +func (c LinuxConfigurer) DockerCommandf(template string, args ...any) string { return fmt.Sprintf("/usr/bin/docker %s", fmt.Sprintf(template, args...)) } @@ -406,3 +354,26 @@ func (c LinuxConfigurer) attemptPathSudoDelete(h os.Host, path string) { } log.Infof("%s: removed %s successfully", h, path) } + +var errAbort = errors.New("base os detected but version resolving failed") + +// ResolveLinux stolen from k0sproject/rig. +// +// We need os-release info in various scenarios, but rig doesn't really expose it. +func ResolveLinux(h os.Host) (rig.OSVersion, error) { + if err := h.Exec("uname | grep -q Linux"); err != nil { + return rig.OSVersion{}, fmt.Errorf("not a linux host (%w)", err) + } + + output, err := h.ExecOutput("cat /etc/os-release || cat /usr/lib/os-release") + if err != nil { + // at this point it is known that this is a linux host, so any error from here on should signal the resolver to not try the next + return rig.OSVersion{}, fmt.Errorf("%w: unable to read os-release file: %w", errAbort, err) + } + + var version rig.OSVersion + if err := rig.ParseOSReleaseFile(output, &version); err != nil { + return rig.OSVersion{}, errors.Join(errAbort, err) + } + return version, nil +} diff --git a/pkg/configurer/oracle/oracle.go b/pkg/configurer/oracle/oracle.go index c9f48046..1bdfb20b 100644 --- a/pkg/configurer/oracle/oracle.go +++ b/pkg/configurer/oracle/oracle.go @@ -16,7 +16,7 @@ func init() { func(os rig.OSVersion) bool { return os.ID == "ol" }, - func() interface{} { + func() any { return Configurer{} }, ) diff --git a/pkg/configurer/sles/sles.go b/pkg/configurer/sles/sles.go index 53b2e29d..20d75505 100644 --- a/pkg/configurer/sles/sles.go +++ b/pkg/configurer/sles/sles.go @@ -1,6 +1,8 @@ +// Package sles implements the mke/config/HostConfigurer for SLES machines package sles import ( + "errors" "fmt" "strings" @@ -11,6 +13,25 @@ import ( "github.com/k0sproject/rig/os" "github.com/k0sproject/rig/os/linux" "github.com/k0sproject/rig/os/registry" + log "github.com/sirupsen/logrus" +) + +func init() { + registry.RegisterOSModule( + func(os rig.OSVersion) bool { + return os.ID == "sles" + }, + func() any { + return Configurer{} + }, + ) +} + +const ( + ZypperRepoAlias = "mirantis" + ZypperPackageName = "docker-ee" + + ZypperPackageNotFound = "No matching items found" ) // Configurer is a generic Ubuntu level configurer implementation. Some of the configurer interface implementation @@ -28,8 +49,48 @@ func (c Configurer) InstallMKEBasePackages(h os.Host) error { return nil } +// InstallMCR install Docker EE engine on Linux. +func (c Configurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { + ver, verErr := configurer.ResolveLinux(h) + if verErr != nil { + return fmt.Errorf("could not discover Linux version information") + } + + if out, err := h.ExecOutput("zypper search --type=package --installed-only docker"); err == nil && !strings.Contains(out, ZypperPackageNotFound) { + if err := h.Exec("zypper remove -y --clean-deps docker", exec.Sudo(h)); err != nil { + return fmt.Errorf("could not remove existing docker-ce installation: %w", err) + } + } + + zypperRepoURL := fmt.Sprintf("%s/%s/%s/%s/%s", engineConfig.RepoURL, ver.ID, "$releasever_major", "$basearch", engineConfig.Channel) + zypperGpgURL := fmt.Sprintf("%s/%s/gpg", engineConfig.RepoURL, ver.ID) + + // remove the repo if it exists (always recreate the repo in case our values have changes) + if out, err := h.ExecOutput("zypper repos"); err != nil { + return fmt.Errorf("%s: could not list zypper repos", h) + } else if strings.Contains(out, ZypperRepoAlias) { + if err := h.Exec(fmt.Sprintf("zypper removerepo %s", ZypperRepoAlias), exec.Sudo(h)); err != nil { + return errors.Join(fmt.Errorf("failed to remove existing zypper MCR repo: %s", ZypperRepoAlias), err) + } + } + log.Debugf("%s: sles MCR GPG key import %s", h, zypperGpgURL) + if err := h.Exec(fmt.Sprintf("sudo rpm --import %s", zypperGpgURL), exec.Sudo(h)); err != nil { + return errors.Join(fmt.Errorf("failed to add zypper GPG key for MCR"), err) + } + if err := h.Exec(fmt.Sprintf("zypper addrepo --refresh '%s' mirantis", zypperRepoURL), exec.Sudo(h)); err != nil { + return errors.Join(fmt.Errorf("failed to add zypper MCR repo: %s", zypperRepoURL), err) + } + log.Debugf("%s: sles MCR install version", h) + if err := c.InstallPackage(h, "docker-ee"); err != nil { + return errors.Join(fmt.Errorf("failed to install zypper MCR packages"), err) + } + log.Debugf("%s: sles MCR installed from channel %s", h, engineConfig.Channel) + + return nil +} + // UninstallMCR uninstalls docker-ee engine. -func (c Configurer) UninstallMCR(h os.Host, _ string, engineConfig commonconfig.MCRConfig) error { +func (c Configurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { info, getDockerError := c.GetDockerInfo(h) if engineConfig.Prune { defer c.CleanupLingeringMCR(h, info) @@ -54,24 +115,3 @@ func (c Configurer) UninstallMCR(h os.Host, _ string, engineConfig commonconfig. 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, fmt.Errorf("failed to get local addresses: %w", err) - } - - return strings.Fields(output), nil -} - -func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "sles" - }, - func() interface{} { - return Configurer{} - }, - ) -} diff --git a/pkg/configurer/ubuntu/ubuntu.go b/pkg/configurer/ubuntu/ubuntu.go index 78ffd748..d5d38a4f 100644 --- a/pkg/configurer/ubuntu/ubuntu.go +++ b/pkg/configurer/ubuntu/ubuntu.go @@ -25,8 +25,52 @@ func (c Configurer) InstallMKEBasePackages(h os.Host) error { return nil } +// InstallMCR install Docker EE engine on Linux. +func (c Configurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { + ver, verErr := configurer.ResolveLinux(h) + if verErr != nil { + return fmt.Errorf("could not discover Linux version information") + } + + // WARNING: we do not confirm this is ubuntu - make sure that it is if you use this code (we did it elsewhere) + codename := ver.ExtraFields["VERSION_CODENAME"] // e.g. jammy + // e.g. https://repos.mirantis.com/rhel/$releasever/$basearch/ + baseURL := fmt.Sprintf("%s/%s", engineConfig.RepoURL, ver.ID) + gpgURL := fmt.Sprintf("%s/%s/gpg", engineConfig.RepoURL, ver.ID) + debRepoFilePath := "/etc/apt/sources.list.d/mirantis.sources" + debRepoTemplate := `Types: deb +URIs: %s +Suites: %s +Architectures: amd64 +Components: %s +Signed-by: /usr/share/keyrings/mirantis-archive-keyring.gpg +` + debRepo := fmt.Sprintf(debRepoTemplate, baseURL, codename, engineConfig.Channel) + + // https://docs.mirantis.com/mcr/25.0/install/mcr-linux/ubuntu.html instructions + + // 2. import the mirantis gpg key + if err := h.Exec(fmt.Sprintf("sudo gpg --batch --yes --output /usr/share/keyrings/mirantis-archive-keyring.gpg --dearmor <<< $(curl -fsSL %s)", gpgURL), exec.Sudo(h)); err != nil { + return fmt.Errorf("could not install the Mirantis Ubuntu GPG signing key") + } + + // 4. write the repo file + // @TODO check if we can use apt-add-repository instead of writing a file (probably has better validation) + if err := c.WriteFile(h, debRepoFilePath, debRepo, "0600"); err != nil { + return fmt.Errorf("could not write APT repo file for MCR") + } + if err := h.Exec("apt update", exec.Sudo(h)); err != nil { + return fmt.Errorf("could not update apt package info") + } + + if err := c.InstallPackage(h, "docker-ee"); err != nil { + return fmt.Errorf("package manager could not install docker-ee") + } + return nil +} + // UninstallMCR uninstalls docker-ee engine. -func (c Configurer) UninstallMCR(h os.Host, _ string, engineConfig commonconfig.MCRConfig) error { +func (c Configurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { info, getDockerError := c.GetDockerInfo(h) if engineConfig.Prune { defer c.CleanupLingeringMCR(h, info) diff --git a/pkg/configurer/windows.go b/pkg/configurer/windows.go index 5fbb175a..139c278a 100644 --- a/pkg/configurer/windows.go +++ b/pkg/configurer/windows.go @@ -63,11 +63,18 @@ func (c WindowsConfigurer) InstallMCRLicense(h os.Host, lic string) error { } // InstallMCR install MCR on Windows. -func (c WindowsConfigurer) InstallMCR(h os.Host, scriptPath string, engineConfig commonconfig.MCRConfig) error { +func (c WindowsConfigurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { + version := "latest" + + installerPath, getInstallerErr := GetInstaller(engineConfig.InstallURLWindows) + if getInstallerErr != nil { + return fmt.Errorf("could not install MCR; %w", getInstallerErr) + } + pwd := c.Pwd(h) - base := path.Base(scriptPath) + base := path.Base(installerPath) installer := pwd + "\\" + base + ".ps1" - if err := h.Upload(scriptPath, installer, fs.FileMode(0o640)); err != nil { + if err := h.Upload(installerPath, installer, fs.FileMode(0o640)); err != nil { return fmt.Errorf("failed to upload MCR installer: %w", err) } defer func() { @@ -76,7 +83,7 @@ func (c WindowsConfigurer) InstallMCR(h os.Host, scriptPath string, engineConfig } }() - 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)) + installCommand := fmt.Sprintf("set DOWNLOAD_URL=%s && set DOCKER_VERSION=%s && set CHANNEL=%s && powershell -ExecutionPolicy Bypass -NoProfile -NonInteractive -File %s -Verbose", engineConfig.RepoURL, version, engineConfig.Channel, ps.DoubleQuote(installer)) log.Infof("%s: running installer", h) @@ -101,7 +108,7 @@ func (c WindowsConfigurer) InstallMCR(h os.Host, scriptPath string, engineConfig // UninstallMCR uninstalls docker-ee engine // 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 commonconfig.MCRConfig) error { +func (c WindowsConfigurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { info, getDockerError := c.GetDockerInfo(h) if engineConfig.Prune { defer c.CleanupLingeringMCR(h, info) @@ -111,10 +118,15 @@ func (c WindowsConfigurer) UninstallMCR(h os.Host, scriptPath string, engineConf return fmt.Errorf("prune docker: %w", err) } + installerPath, getInstallerErr := GetInstaller(engineConfig.InstallURLWindows) + if getInstallerErr != nil { + return fmt.Errorf("could not uninstall MCR; %w", getInstallerErr) + } + pwd := c.Pwd(h) - base := path.Base(scriptPath) + base := path.Base(installerPath) uninstaller := pwd + "\\" + base + ".ps1" - if err := h.Upload(scriptPath, uninstaller, fs.FileMode(0o640)); err != nil { + if err := h.Upload(installerPath, uninstaller, fs.FileMode(0o640)); err != nil { return fmt.Errorf("upload MCR uninstaller: %w", err) } defer func() { diff --git a/pkg/configurer/windows/windows_2019.go b/pkg/configurer/windows/windows_2019.go index 1c580e60..586bd30a 100644 --- a/pkg/configurer/windows/windows_2019.go +++ b/pkg/configurer/windows/windows_2019.go @@ -22,7 +22,7 @@ func init() { func(os rig.OSVersion) bool { return os.ID == "windows" && os.Version == "10.0.17763" }, - func() interface{} { + func() any { return Windows2019Configurer{} }, ) diff --git a/pkg/configurer/windows/windows_2022.go b/pkg/configurer/windows/windows_2022.go index 2220100f..ba5647cf 100644 --- a/pkg/configurer/windows/windows_2022.go +++ b/pkg/configurer/windows/windows_2022.go @@ -22,7 +22,7 @@ func init() { func(os rig.OSVersion) bool { return os.ID == "windows" && os.Version == "10.0.20348" }, - func() interface{} { + func() any { return Windows2022Configurer{} }, ) diff --git a/pkg/configurer/windows/windows_2025.go b/pkg/configurer/windows/windows_2025.go index cb4fc871..69d32b94 100644 --- a/pkg/configurer/windows/windows_2025.go +++ b/pkg/configurer/windows/windows_2025.go @@ -22,7 +22,7 @@ func init() { func(os rig.OSVersion) bool { return os.ID == "windows" && os.Version == "10.0.26100" }, - func() interface{} { + func() any { return Windows2025Configurer{} }, ) diff --git a/pkg/constant/constant.go b/pkg/constant/constant.go index f00dd644..a5c47d0b 100644 --- a/pkg/constant/constant.go +++ b/pkg/constant/constant.go @@ -5,14 +5,10 @@ const ( ImageRepo = "docker.io/mirantis" // ImageRepoLegacy is the default image repo to use for older versions. ImageRepoLegacy = "docker.io/docker" - // MCRVersion is the default engine version. - MCRVersion = "20.10.13" // MCRChannel is the default engine channel. MCRChannel = "stable" // MCRRepoURL is the default engine repo. MCRRepoURL = "https://repos.mirantis.com" - // MCRInstallURLLinux is the default engine install script location for linux. - MCRInstallURLLinux = "https://get.mirantis.com/" // MCRInstallURLWindows is the default engine install script location for windows. MCRInstallURLWindows = "https://get.mirantis.com/install.ps1" // StateBaseDir defines the base dir for all local state. diff --git a/pkg/helm/upgrade.go b/pkg/helm/upgrade.go index f76fe8a0..e5a78c51 100644 --- a/pkg/helm/upgrade.go +++ b/pkg/helm/upgrade.go @@ -64,9 +64,9 @@ func (h *Helm) Upgrade(ctx context.Context, opts *Options) (rel *release.Release cfg.Capabilities.KubeVersion = chartutil.KubeVersion{Major: "1", Minor: "22", Version: "1.22.0"} settings := h.settings - u := action.NewUpgrade(&cfg) + upgrade := action.NewUpgrade(&cfg) - chartToUpgrade, err := getChart(u.ChartPathOptions, opts.ChartName, &settings) + chartToUpgrade, err := getChart(upgrade.ChartPathOptions, opts.ChartName, &settings) if err != nil { return nil, err } @@ -85,16 +85,16 @@ func (h *Helm) Upgrade(ctx context.Context, opts *Options) (rel *release.Release log.Infof("release %q found using chart: %q, upgrading to version: %q", opts.ReleaseName, opts.ChartName, opts.Version) - u.Namespace = settings.Namespace() - u.ReuseValues = opts.ReuseValues - u.Wait = opts.Wait - u.Atomic = opts.Atomic - u.Version = opts.Version + upgrade.Namespace = settings.Namespace() + upgrade.ReuseValues = opts.ReuseValues + upgrade.Wait = opts.Wait + upgrade.Atomic = opts.Atomic + upgrade.Version = opts.Version if opts.Timeout != nil { - u.Timeout = *opts.Timeout + upgrade.Timeout = *opts.Timeout } - release, err := u.RunWithContext(ctx, opts.ReleaseName, chartToUpgrade, opts.Values) + release, err := upgrade.RunWithContext(ctx, opts.ReleaseName, chartToUpgrade, opts.Values) if err != nil { return nil, fmt.Errorf("failed to upgrade Helm release %q: %w", opts.ReleaseName, err) } diff --git a/pkg/kubeclient/kubeclient_test.go b/pkg/kubeclient/kubeclient_test.go index eba582d8..6332336c 100644 --- a/pkg/kubeclient/kubeclient_test.go +++ b/pkg/kubeclient/kubeclient_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/Mirantis/launchpad/pkg/constant" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -11,8 +12,6 @@ import ( storagev1 "k8s.io/api/storage/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/Mirantis/launchpad/pkg/constant" ) func TestCRDReady(t *testing.T) { diff --git a/pkg/kubeclient/msr.go b/pkg/kubeclient/msr.go index 754fa2a8..3201f0c5 100644 --- a/pkg/kubeclient/msr.go +++ b/pkg/kubeclient/msr.go @@ -190,8 +190,8 @@ func (kc *KubeClient) PrepareNodeForMSR(ctx context.Context, name string) error // GetMSRResourceClient returns a dynamic client for the MSR custom resource. // -//nolint:ireturn -func (kc *KubeClient) GetMSRResourceClient() (dynamic.ResourceInterface, error) { + +func (kc *KubeClient) GetMSRResourceClient() (dynamic.ResourceInterface, error) { //nolint:ireturn client, err := dynamic.NewForConfig(kc.config) if err != nil { return nil, fmt.Errorf("failed to create dynamic client: %w", err) diff --git a/pkg/kubeclient/testutil.go b/pkg/kubeclient/testutil.go index b6551c67..d9cdaff9 100644 --- a/pkg/kubeclient/testutil.go +++ b/pkg/kubeclient/testutil.go @@ -1,5 +1,3 @@ -//go:build testing - package kubeclient import ( diff --git a/pkg/mcr/mcr.go b/pkg/mcr/mcr.go index 2a7fd1e2..8a51c644 100644 --- a/pkg/mcr/mcr.go +++ b/pkg/mcr/mcr.go @@ -4,35 +4,16 @@ import ( "errors" "fmt" + commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/Mirantis/launchpad/pkg/swarm" log "github.com/sirupsen/logrus" ) -var ErrInvalidMCRConfig = errors.New("MCR configuration is invalid") - -// EnsureMCRVersion ensure that MCR is running after install/upgrade, and update the host information -// @NOTE will reboot the machine if MCR isn't detected -// @SEE PRODENG-2789 : we no longer perform version checks, as the MCR versions don't always match the spec string. -func EnsureMCRVersion(host *mkeconfig.Host, specMcrVersion string) error { - currentVersion, err := host.MCRVersion() - if err != nil { - if err := host.Reboot(); err != nil { - return fmt.Errorf("%s: failed to reboot after container runtime installation: %w", host, err) - } - currentVersion, err = host.MCRVersion() - if err != nil { - return fmt.Errorf("%s: failed to query container runtime version after installation: %w", host, err) - } - // as we rebooted the machine, no need to also restart MCR - host.Metadata.MCRRestartRequired = false - } - - log.Infof("%s: MCR version %s (requested: %s)", host, currentVersion, specMcrVersion) - host.Metadata.MCRVersion = currentVersion - - return nil -} +var ( + ErrInvalidMCRConfig = errors.New("MCR configuration is invalid") + ErrMCRNotRunning = errors.New("MCR is not running") +) // DrainNode drains a node from the workload via docker drain command. func DrainNode(lead *mkeconfig.Host, h *mkeconfig.Host) error { @@ -52,3 +33,12 @@ func DrainNode(lead *mkeconfig.Host, h *mkeconfig.Host) error { log.Infof("%s: node %s drained", lead, nodeID) return nil } + +// EnsureMCRRunning ensure that MCR is running. +func EnsureMCRRunning(h *mkeconfig.Host, _ commonconfig.MCRConfig) error { + if _, err := h.MCRVersion(); err != nil { + return fmt.Errorf("%w; %s", ErrMCRNotRunning, err.Error()) + } + + return nil +} diff --git a/pkg/mke/bootstrap.go b/pkg/mke/bootstrap.go index 95606924..8f6d0ad1 100644 --- a/pkg/mke/bootstrap.go +++ b/pkg/mke/bootstrap.go @@ -63,7 +63,7 @@ func Bootstrap(operation string, config mkeconfig.ClusterConfig, bootoptions Boo cmdbuffer.LogrusLine(le) if le.Level == "fatal" { - err = errors.Join(err, fmt.Errorf("mke bootstrap %s failure; %s", operation, le.Msg)) //nolint + err = errors.Join(err, fmt.Errorf("mke bootstrap %s failure; %s", operation, le.Msg)) } } else { // output line was not logrus, so just output it diff --git a/pkg/msr/bootstrap.go b/pkg/msr/bootstrap.go index 6ec331d2..ad053cbb 100644 --- a/pkg/msr/bootstrap.go +++ b/pkg/msr/bootstrap.go @@ -71,7 +71,7 @@ func Bootstrap(operation string, config mkeconfig.ClusterConfig, bootoptions Boo cmdbuffer.LogrusLine(le) if le.Level == "fatal" { - err = errors.Join(err, fmt.Errorf("msr bootstrap %s failure; %s", operation, le.Msg)) //nolint + err = errors.Join(err, fmt.Errorf("msr bootstrap %s failure; %s", operation, le.Msg)) } } else { // output line was not logrus, so just output it diff --git a/pkg/product/common/config/mcr_config.go b/pkg/product/common/config/mcr_config.go index ff1590df..412e19e2 100644 --- a/pkg/product/common/config/mcr_config.go +++ b/pkg/product/common/config/mcr_config.go @@ -1,23 +1,7 @@ package config import ( - "errors" - "fmt" - "regexp" - "strings" - "github.com/Mirantis/launchpad/pkg/constant" - version "github.com/hashicorp/go-version" -) - -var ( - ErrInvalidVersion = errors.New("the MCR version is not valid") - // all versions from 25.0.0 need channel-version matching. - minVersionNeedsMatchingChannel, _ = version.NewVersion("25.0.0") - ErrChannelDoesntMatchVersion = errors.New("MCR version and channel don't match, which is required for versions >= 25.0.0") - - fipsChannelSuffix = "/fips" // this suffix is removed from channels for version comparison testing - suffixPattern = regexp.MustCompile(`-(tp|rc)\d+$`) // this filters out internal build suffix like -tp1 ) type DockerInfo struct { @@ -35,12 +19,10 @@ type DockerDaemonConfig struct { // MCRConfig holds the Mirantis Container Runtime installation specific options. type MCRConfig struct { - Version string `yaml:"version"` RepoURL string `yaml:"repoURL,omitempty"` AdditionalRuntimes string `yaml:"additionalRuntimes,omitempty"` DefaultRuntime string `yaml:"defaultRuntime,omitempty"` License string `yaml:"license"` - InstallURLLinux string `yaml:"installURLLinux,omitempty"` InstallScriptRemoteDirLinux string `yaml:"installScriptRemoteDirLinux,omitempty"` InstallURLWindows string `yaml:"installURLWindows,omitempty"` Channel string `yaml:"channel,omitempty"` @@ -58,7 +40,7 @@ type MCRMetadata struct { } // UnmarshalYAML puts in sane defaults when unmarshaling from yaml. -func (c *MCRConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (c *MCRConfig) UnmarshalYAML(unmarshal func(any) error) error { type mcrConfig MCRConfig c.Metadata = &MCRMetadata{} yc := (*mcrConfig)(c) @@ -75,10 +57,6 @@ func (c *MCRConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { // SetDefaults sets defaults on the object. func (c *MCRConfig) SetDefaults() { // Constants can't be used in tags, so yaml defaults can't be used here. - if c.Version == "" { - c.Version = constant.MCRVersion - } - if c.Channel == "" { c.Channel = constant.MCRChannel } @@ -87,51 +65,7 @@ func (c *MCRConfig) SetDefaults() { c.RepoURL = constant.MCRRepoURL } - if c.InstallURLLinux == "" { - c.InstallURLLinux = constant.MCRInstallURLLinux - } - if c.InstallURLWindows == "" { c.InstallURLWindows = constant.MCRInstallURLWindows } } - -// Validate mcr config values. -func (c *MCRConfig) Validate() error { - if err := processVersionChannelMatch(c); err != nil { - return err - } - - return nil -} - -// MCR versions 25.0 and later require that the channel uses the version specific part. -// -// If the channel doesn't contain the right version component then version pinning won't work -func processVersionChannelMatch(config *MCRConfig) error { - ver, vererr := version.NewSemver(config.Version) - if vererr != nil { - return fmt.Errorf("%w; %w", ErrInvalidVersion, vererr) - } - - if ver.LessThan(minVersionNeedsMatchingChannel) { - return nil - } - - channel := strings.TrimSuffix(config.Channel, fipsChannelSuffix) - chanParts := strings.Split(channel, "-") - if len(chanParts) == 1 { - return fmt.Errorf("%w; channel has no version (%s)", ErrChannelDoesntMatchVersion, config.Channel) - } - - if len(chanParts) > 2 { - return fmt.Errorf("%w; channel parts could not be interpreted", ErrChannelDoesntMatchVersion) - } - - configVerNum := suffixPattern.ReplaceAllString(config.Version, "") // remove build number - if !strings.HasPrefix(chanParts[1], configVerNum) { - return fmt.Errorf("%w; MCR version does not match channel-version '%s' vs '%s'", ErrChannelDoesntMatchVersion, chanParts[1], config.Version) - } - - return nil -} diff --git a/pkg/product/common/config/mcr_config_validate_test.go b/pkg/product/common/config/mcr_config_validate_test.go deleted file mode 100644 index f490af96..00000000 --- a/pkg/product/common/config/mcr_config_validate_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package config_test - -import ( - "testing" - - commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" - "github.com/stretchr/testify/require" -) - -func Test_ValidateNil(t *testing.T) { - config := commonconfig.MCRConfig{ - Version: "25.0", - Channel: "stable-25.0", - } - - require.Nil(t, config.Validate(), "unexpected sanitize error from valid MCR config") - - config2 := commonconfig.MCRConfig{ - Version: "25.0.10", - Channel: "stable-25.0.10", - } - - require.Nil(t, config2.Validate(), "unexpected sanitize error from valid MCR config") -} - -func Test_ValidateNilFIPS(t *testing.T) { - configFips := commonconfig.MCRConfig{ - Version: "25.0", - Channel: "stable-25.0/fips", - } - - require.Nil(t, configFips.Validate(), "unexpected sanitize error from valid MCR config w/ FIPS") - - configFips2 := commonconfig.MCRConfig{ - Version: "25.0.9", - Channel: "stable-25.0.9/fips", - } - - require.Nil(t, configFips2.Validate(), "unexpected sanitize error from valid MCR config w/ FIPS") -} - -// validation should fail if version is empty (we should likely never get to such a point, but just in case) -func Test_ValidateEmptyVersion(t *testing.T) { - config := commonconfig.MCRConfig{ - Version: "", - Channel: "stable", - } - - require.ErrorIs(t, config.Validate(), commonconfig.ErrInvalidVersion, "did not receive expected error from empty MCR config version") -} - -// invalid version passed, which can trigger a runtime error in GoVersion (thanks Hashicorp) -func Test_ValidateInvalidVersion(t *testing.T) { - config := commonconfig.MCRConfig{ - Version: "this-is-not-a-valid-version", - Channel: "stable", - } - - require.ErrorIs(t, config.Validate(), commonconfig.ErrInvalidVersion, "did not receive expected error from invalid MCR config version") -} - -// invalid version passed: reported in PRODENG-3129 -func Test_ValidateInvalidVersion_PRODENG3129(t *testing.T) { - config := commonconfig.MCRConfig{ - Version: "25.0.8m1-1", - Channel: "stable-25.0.9", - } - - require.Error(t, config.Validate(), "did not receive expected error from invalid MCR config version (PRODENG-3129)") -} - -// if a full maj.min.pat version is passed, then the channel should have the full maj.min.pat part -func Test_ValidateMissingChannelVersion(t *testing.T) { - config := commonconfig.MCRConfig{ - Version: "25.0.8", - Channel: "stable", - } - - require.ErrorIs(t, config.Validate(), commonconfig.ErrChannelDoesntMatchVersion, "did not receive expected error from invalid MCR config which is missing the channel version") - - configFips := commonconfig.MCRConfig{ - Version: "25.0.8", - Channel: "stable/fips", // This channel does not actually exist, but it should fail still because of the missing -25.0.8 - } - - err := configFips.Validate() - require.ErrorIs(t, err, commonconfig.ErrChannelDoesntMatchVersion, "did not receive expected error from invalid MCR config which is missing the channel version w/ FIPS") -} - -func Test_ValidateWrongChannelVersion(t *testing.T) { - config := commonconfig.MCRConfig{ - Version: "25.0.8", - Channel: "stable-25.0.9", - } - - require.ErrorIs(t, config.Validate(), commonconfig.ErrChannelDoesntMatchVersion, "did not receive expected error from invalid MCR config which has the wrong channel version") -} - -func Test_ValidateWrongChannelVersionFIPS(t *testing.T) { - configFips := commonconfig.MCRConfig{ - Version: "25.0.8", - Channel: "stable-25.0.9/fips", - } - - require.ErrorIs(t, configFips.Validate(), commonconfig.ErrChannelDoesntMatchVersion, "did not receive expected error from invalid MCR config which has the wrong channel version w/ FIPS") -} - -func Test_ValidateIncompleteChannelVersion(t *testing.T) { - config := commonconfig.MCRConfig{ - Version: "25.0.8", - Channel: "stable-25.0", - } - - require.ErrorIs(t, config.Validate(), commonconfig.ErrChannelDoesntMatchVersion, "did not receive expected error from invalid MCR config which is missing an incomplete channel version") - - config2 := commonconfig.MCRConfig{ - Version: "25.0.8", - Channel: "stable-25", - } - - require.ErrorIs(t, config2.Validate(), commonconfig.ErrChannelDoesntMatchVersion, "did not receive expected error from invalid MCR config which is missing an incomplete channel version") - - config3 := commonconfig.MCRConfig{ - Version: "25.0.8", - Channel: "stable-", - } - - require.ErrorIs(t, config3.Validate(), commonconfig.ErrChannelDoesntMatchVersion, "did not receive expected error from invalid MCR config which is missing an incomplete channel version") - -} - -func Test_ValidateIncompleteChannelVersionFIPS(t *testing.T) { - configFips := commonconfig.MCRConfig{ - Version: "25.0.8", - Channel: "stable-25.0/fips", - } - - require.ErrorIs(t, configFips.Validate(), commonconfig.ErrChannelDoesntMatchVersion, "did not receive expected error from invalid MCR config which is missing an incomplete channel version") -} - -func Test_ValidateWildcardChannelVersion(t *testing.T) { - config := commonconfig.MCRConfig{ - Version: "25.0", - Channel: "stable-25.0.9", - } - - require.Nil(t, config.Validate(), "received unexpected error for valid MCR config which uses a wildcard version and specific channel") -} - -func Test_ValidateWildcardChannelVersionFIPS(t *testing.T) { - configFips := commonconfig.MCRConfig{ - Version: "25.0", - Channel: "stable-25.0.9/fips", - } - - require.Nil(t, configFips.Validate(), "received unexpected error for valid MCR config which uses a wildcard version and specific channel w/ FIPS") -} - -func Test_ValidateInternalBuild(t *testing.T) { - config := commonconfig.MCRConfig{ - Version: "25.0.12-tp1", - Channel: "test-25.0.12", - } - - require.Nil(t, config.Validate(), "received unexpected error for valid MCR config which contains internal build suffix -tp1") -} - -func Test_ValidateInternalBuildFIPS(t *testing.T) { - config := commonconfig.MCRConfig{ - Version: "25.0.12-rc2", - Channel: "test-25.0.12/fips", - } - - require.Nil(t, config.Validate(), "received unexpected error for valid MCR config which contains internal build suffix -tp1 w/ FIPS") -} diff --git a/pkg/product/mke/apply.go b/pkg/product/mke/apply.go index b96b817c..87b9b2c7 100644 --- a/pkg/product/mke/apply.go +++ b/pkg/product/mke/apply.go @@ -24,7 +24,6 @@ func (p *MKE) Apply(disableCleanup, force bool, concurrency int, forceUpgrade bo &mke.GatherFacts{}, &mke.ValidateFacts{Force: force}, &mke.ValidateHosts{}, - &mke.DownloadInstaller{}, &common.RunHooks{Stage: "before", Action: "apply"}, &mke.PrepareHost{}, @@ -81,7 +80,7 @@ func (p *MKE) Apply(disableCleanup, force bool, concurrency int, forceUpgrade bo "dtrs": len(p.ClusterConfig.Spec.MSRs()), "linux_workers": linuxWorkersCount, "windows_workers": windowsWorkersCount, - "engine_version": p.ClusterConfig.Spec.MCR.Version, + "engine_channel": p.ClusterConfig.Spec.MCR.Channel, "cluster_id": clusterID, // send mke analytics user id as ucp_instance_id property "ucp_instance_id": fmt.Sprintf("%x", sha1.Sum([]byte(clusterID))), //nolint:gosec // sha1 is used for simple analytics id generation diff --git a/pkg/product/mke/config/cluster.go b/pkg/product/mke/config/cluster.go index b94bdcaa..ce6c8690 100644 --- a/pkg/product/mke/config/cluster.go +++ b/pkg/product/mke/config/cluster.go @@ -17,7 +17,7 @@ type ClusterMeta struct { // ClusterConfig describes launchpad.yaml configuration. type ClusterConfig struct { - APIVersion string `yaml:"apiVersion" validate:"eq=launchpad.mirantis.com/mke/v1.5"` + APIVersion string `yaml:"apiVersion" validate:"eq=launchpad.mirantis.com/mke/v1.6"` Kind string `yaml:"kind" validate:"oneof=mke mke+msr"` Metadata *ClusterMeta `yaml:"metadata"` Spec *ClusterSpec `yaml:"spec"` @@ -70,14 +70,14 @@ func Init(kind string) *ClusterConfig { } config := &ClusterConfig{ - APIVersion: "launchpad.mirantis.com/mke/v1.5", + APIVersion: "launchpad.mirantis.com/mke/v1.6", Kind: kind, Metadata: &ClusterMeta{ Name: "my-mke-cluster", }, Spec: &ClusterSpec{ MCR: common.MCRConfig{ - Version: constant.MCRVersion, + Channel: constant.MCRChannel, }, MKE: MKEConfig{ Version: mkeV, diff --git a/pkg/product/mke/config/cluster_test.go b/pkg/product/mke/config/cluster_test.go index e2980654..ce22f1cf 100644 --- a/pkg/product/mke/config/cluster_test.go +++ b/pkg/product/mke/config/cluster_test.go @@ -34,7 +34,7 @@ func TestHostRequireManagerValidationPass(t *testing.T) { kf, _ := os.CreateTemp("", "testkey") defer kf.Close() data := ` -apiVersion: "launchpad.mirantis.com/mke/v1.5" +apiVersion: "launchpad.mirantis.com/mke/v1.6" kind: mke spec: hosts: @@ -240,7 +240,7 @@ spec: ucp: version: 3.3.7 engine: - installURL: http://example.com/ + installURLWindows: http://example.com/install.ps1 hosts: - address: "1.2.3.4" sshPort: 9022 @@ -251,9 +251,9 @@ spec: c := loadAndMigrateYaml(t, data) err := c.Validate() require.NoError(t, err) - require.Equal(t, "launchpad.mirantis.com/mke/v1.5", c.APIVersion) + require.Equal(t, "launchpad.mirantis.com/mke/v1.6", c.APIVersion) - require.Equal(t, c.Spec.MCR.InstallURLLinux, "http://example.com/") + require.Equal(t, c.Spec.MCR.InstallURLWindows, "http://example.com/install.ps1") require.Equal(t, c.Spec.Hosts[0].SSH.Port, 9022) require.Equal(t, c.Spec.Hosts[0].SSH.User, "foofoo") } @@ -266,7 +266,7 @@ spec: ucp: version: 3.3.7 engine: - installURL: http://example.com/ + installURLWindows: http://example.com/install.ps1 hosts: - address: "1.2.3.4" role: manager @@ -276,7 +276,7 @@ spec: ` c := loadAndMigrateYaml(t, data) require.NoError(t, c.Validate()) - require.Equal(t, "launchpad.mirantis.com/mke/v1.5", c.APIVersion) + require.Equal(t, "launchpad.mirantis.com/mke/v1.6", c.APIVersion) } func TestMigrateFromV1Beta1WithoutInstallURL(t *testing.T) { @@ -298,9 +298,9 @@ spec: c := loadAndMigrateYaml(t, data) err := c.Validate() require.NoError(t, err) - require.Equal(t, "launchpad.mirantis.com/mke/v1.5", c.APIVersion) + require.Equal(t, "launchpad.mirantis.com/mke/v1.6", c.APIVersion) - require.Equal(t, constant.MCRInstallURLLinux, c.Spec.MCR.InstallURLLinux) + require.Equal(t, constant.MCRInstallURLWindows, c.Spec.MCR.InstallURLWindows) require.Equal(t, 9022, c.Spec.Hosts[0].SSH.Port) require.Equal(t, "foofoo", c.Spec.Hosts[0].SSH.User) } @@ -384,7 +384,7 @@ spec: func TestHostWinRMDefaults(t *testing.T) { data := ` -apiVersion: launchpad.mirantis.com/mke/v1.5 +apiVersion: launchpad.mirantis.com/mke/v1.6 kind: mke spec: mke: @@ -433,7 +433,7 @@ spec: t.Run("the role is msr", func(t *testing.T) { data := ` -apiVersion: launchpad.mirantis.com/mke/v1.5 +apiVersion: launchpad.mirantis.com/mke/v1.6 kind: mke+msr spec: mke: diff --git a/pkg/product/mke/config/configurer.go b/pkg/product/mke/config/configurer.go index b4cafdb8..4b5373ee 100644 --- a/pkg/product/mke/config/configurer.go +++ b/pkg/product/mke/config/configurer.go @@ -21,8 +21,8 @@ type HostConfigurer interface { CleanupEnvironment(os.Host, map[string]string) error MCRConfigPath() string InstallMCRLicense(os.Host, string) error - InstallMCR(os.Host, string, common.MCRConfig) error - UninstallMCR(os.Host, string, common.MCRConfig) error + InstallMCR(os.Host, common.MCRConfig) error + UninstallMCR(os.Host, common.MCRConfig) error DockerCommandf(template string, args ...any) string RestartMCR(os.Host) error AuthenticateDocker(h os.Host, user, pass, repo string) error diff --git a/pkg/product/mke/config/host.go b/pkg/product/mke/config/host.go index 1b337737..b29a56a7 100644 --- a/pkg/product/mke/config/host.go +++ b/pkg/product/mke/config/host.go @@ -28,7 +28,6 @@ type HostMetadata struct { LongHostname string InternalAddress string MCRVersion string - MCRInstallScript string MCRRestartRequired bool ImagesToUpload []string TotalImageBytes uint64 diff --git a/pkg/product/mke/exec.go b/pkg/product/mke/exec.go index 7b453fc0..68383119 100644 --- a/pkg/product/mke/exec.go +++ b/pkg/product/mke/exec.go @@ -9,7 +9,7 @@ import ( "strings" "sync" - "github.com/Mirantis/launchpad/pkg/product/mke/config" + mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/k0sproject/rig" "github.com/k0sproject/rig/exec" log "github.com/sirupsen/logrus" @@ -19,12 +19,12 @@ 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 { //nolint:maintidx - var hosts config.Hosts + var hosts mkeconfig.Hosts for _, target := range targets { switch { case target == "localhost": - hosts = append(hosts, &config.Host{Connection: rig.Connection{Localhost: &rig.Localhost{Enabled: true}}}) + hosts = append(hosts, &mkeconfig.Host{Connection: rig.Connection{Localhost: &rig.Localhost{Enabled: true}}}) case strings.Contains(target, ":"): parts := strings.SplitN(target, ":", 2) addr := parts[0] @@ -33,7 +33,7 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol return fmt.Errorf("%w: invalid port: %s", errInvalidTarget, parts[1]) } - host := p.ClusterConfig.Spec.Hosts.Find(func(h *config.Host) bool { + host := p.ClusterConfig.Spec.Hosts.Find(func(h *mkeconfig.Host) bool { if h.Address() != addr { return false } @@ -47,7 +47,7 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol } hosts = append(hosts, host) default: - host := p.ClusterConfig.Spec.Hosts.Find(func(h *config.Host) bool { + host := p.ClusterConfig.Spec.Hosts.Find(func(h *mkeconfig.Host) bool { return h.Address() == target }) if host == nil { @@ -59,9 +59,9 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol if role != "" { if len(hosts) == 0 { - hosts = p.ClusterConfig.Spec.Hosts.Filter(func(h *config.Host) bool { return h.Role == role }) + hosts = p.ClusterConfig.Spec.Hosts.Filter(func(h *mkeconfig.Host) bool { return h.Role == role }) } else { - hosts = hosts.Filter(func(h *config.Host) bool { return h.Role == role }) + hosts = hosts.Filter(func(h *mkeconfig.Host) bool { return h.Role == role }) } } @@ -70,26 +70,26 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol hosts = p.ClusterConfig.Spec.Hosts } - var foundhosts config.Hosts + var foundhosts mkeconfig.Hosts var mutex sync.Mutex - err := hosts.ParallelEach(func(h *config.Host) error { - if err := h.Connect(); err != nil { - return fmt.Errorf("failed to connect to host %s: %w", h.Address(), err) + err := hosts.ParallelEach(func(host *mkeconfig.Host) error { + if err := host.Connect(); err != nil { + return fmt.Errorf("failed to connect to host %s: %w", host.Address(), err) } - if err := h.ResolveConfigurer(); err != nil { - return fmt.Errorf("failed to resolve configurer for host %s: %w", h.Address(), err) + if err := host.ResolveConfigurer(); err != nil { + return fmt.Errorf("failed to resolve configurer for host %s: %w", host.Address(), err) } - if h.IsWindows() { + if host.IsWindows() { if hostos == "windows" { mutex.Lock() - foundhosts = append(foundhosts, h) + foundhosts = append(foundhosts, host) mutex.Unlock() } } else { - if hostos == "linux" || h.OSVersion.ID == hostos { + if hostos == "linux" || host.OSVersion.ID == hostos { mutex.Lock() - foundhosts = append(foundhosts, h) + foundhosts = append(foundhosts, host) mutex.Unlock() } } @@ -143,7 +143,7 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol } } - err := hosts.ParallelEach(func(h *config.Host) error { + err := hosts.ParallelEach(func(h *mkeconfig.Host) error { if err := h.Connect(); err != nil { return fmt.Errorf("connect to host %s: %w", h.Address(), err) } @@ -154,7 +154,7 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol } var linuxcount, windowscount int - err = hosts.Each(func(h *config.Host) error { + err = hosts.Each(func(h *mkeconfig.Host) error { if h.IsWindows() { if linuxcount > 0 { return fmt.Errorf("%w mixed target operating systems, use --os linux or --os windows", errInvalidTarget) @@ -192,7 +192,7 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol } log.Tracef("running non-interactive with cmd: %q", cmd) - runFunc := func(h *config.Host) error { + runFunc := func(h *mkeconfig.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) } diff --git a/pkg/product/mke/phase/configure_mcr.go b/pkg/product/mke/phase/configure_mcr.go index ee859249..cf814408 100644 --- a/pkg/product/mke/phase/configure_mcr.go +++ b/pkg/product/mke/phase/configure_mcr.go @@ -41,7 +41,7 @@ func (p *ConfigureMCR) Title() string { // Run installs the engine on each host. func (p *ConfigureMCR) Run() error { p.EventProperties = map[string]interface{}{ - "engine_version": p.Config.Spec.MCR.Version, + "engine_channel": p.Config.Spec.MCR.Channel, } err := p.Hosts.ParallelEach(func(h *mkeconfig.Host) error { log.Infof("%s: configuring container runtime", h) diff --git a/pkg/product/mke/phase/download_installer.go b/pkg/product/mke/phase/download_installer.go deleted file mode 100644 index 8be36995..00000000 --- a/pkg/product/mke/phase/download_installer.go +++ /dev/null @@ -1,159 +0,0 @@ -package phase - -import ( - "fmt" - "io" - "net/http" - "net/url" - "os" - "strings" - - "github.com/Mirantis/launchpad/pkg/phase" - mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" - "github.com/Mirantis/launchpad/pkg/util/fileutil" - log "github.com/sirupsen/logrus" -) - -// DownloadInstaller phase implementation does all the prep work we need for the hosts. -type DownloadInstaller struct { - phase.Analytics - phase.BasicPhase - - linuxPath string - winPath string -} - -// Title for the phase. -func (p *DownloadInstaller) Title() string { - return "Download Mirantis Container Runtime installer" -} - -// Run does all the prep work on the hosts in parallel. -func (p *DownloadInstaller) Run() error { - linuxScript, err := p.getScript(p.Config.Spec.MCR.InstallURLLinux) - if err != nil { - return err - } - f, err := os.CreateTemp("", "installerLinux") - if err != nil { - return fmt.Errorf("failed to create temporary file: %w", err) - } - - _, err = f.WriteString(linuxScript) - if err != nil { - return fmt.Errorf("failed to write to temporary file: %w", err) - } - p.linuxPath = f.Name() - - if p.Config.Spec.Hosts.Count(func(h *mkeconfig.Host) bool { return h.IsWindows() }) > 0 { - winScript, err := p.getScript(p.Config.Spec.MCR.InstallURLWindows) - if err != nil { - return fmt.Errorf("failed to get Windows installer script: %w", err) - } - f, err := os.CreateTemp("", "installerWindows") - if err != nil { - return fmt.Errorf("failed to create temporary file for windows installer script: %w", err) - } - - _, err = f.WriteString(winScript) - if err != nil { - return fmt.Errorf("failed to write to temporary file for windows installer script: %w", err) - } - p.winPath = f.Name() - } - - for _, h := range p.Config.Spec.Hosts { - if h.IsWindows() { - h.Metadata.MCRInstallScript = p.winPath - } else { - h.Metadata.MCRInstallScript = p.linuxPath - } - } - - return nil -} - -func (p *DownloadInstaller) parseURL(uri string) (*url.URL, error) { - if !strings.Contains(uri, "://") { - return &url.URL{Path: uri, Scheme: "file"}, nil - } - - 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 { - return "", err - } - - var data string - - if u.Scheme == "file" { - data, err = p.readFile(u.Path) - } else { - data, err = p.downloadFile(uri) - } - - log.Debugf("read %d bytes from %s", len(data), uri) - - if err != nil { - return "", err - } - - if len(data) < 10 { - // cant fit an installer into that! - return "", fmt.Errorf("%w: script is too short", errInvalidScript) - } - - if !strings.HasPrefix(data, "#") { - log.Warnf("possibly invalid container runtime install script in %s", uri) - } - - return data, nil -} - -func (p *DownloadInstaller) downloadFile(url string) (string, error) { - log.Infof("downloading container runtime install script from %s", 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 "", 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 "", fmt.Errorf("failed to read response body: %w", err) - } - return string(body), nil -} - -func (p *DownloadInstaller) readFile(path string) (string, error) { - log.Infof("reading container runtime install script from %s", path) - - data, err := fileutil.LoadExternalFile(path) - return string(data), err -} - -// CleanUp removes the temporary files from local filesystem. -func (p *DownloadInstaller) CleanUp() { - if p.winPath != "" { - removeIfExist(p.winPath) - } - if p.linuxPath != "" { - removeIfExist(p.linuxPath) - } -} - -func removeIfExist(path string) { - _, err := os.Stat(path) - if err == nil { - os.Remove(path) - } -} diff --git a/pkg/product/mke/phase/install_mcr.go b/pkg/product/mke/phase/install_mcr.go index a5ccec95..3291fcac 100644 --- a/pkg/product/mke/phase/install_mcr.go +++ b/pkg/product/mke/phase/install_mcr.go @@ -3,10 +3,8 @@ package phase import ( "fmt" - "github.com/Mirantis/launchpad/pkg/mcr" "github.com/Mirantis/launchpad/pkg/phase" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" - retry "github.com/avast/retry-go" log "github.com/sirupsen/logrus" ) @@ -22,7 +20,7 @@ func (p *InstallMCR) HostFilterFunc(h *mkeconfig.Host) bool { } // Prepare collects the hosts. -func (p *InstallMCR) Prepare(config interface{}) error { +func (p *InstallMCR) Prepare(config any) error { cfg, ok := config.(*mkeconfig.ClusterConfig) if !ok { return errInvalidConfig @@ -43,8 +41,8 @@ func (p *InstallMCR) Title() string { // Run installs the engine on each host. func (p *InstallMCR) Run() error { - p.EventProperties = map[string]interface{}{ - "engine_version": p.Config.Spec.MCR.Version, + p.EventProperties = map[string]any{ + "engine_channel": p.Config.Spec.MCR.Channel, } if err := p.Hosts.ParallelEach(p.installMCR); err != nil { @@ -54,28 +52,16 @@ func (p *InstallMCR) Run() error { } func (p *InstallMCR) installMCR(h *mkeconfig.Host) error { - if err := retry.Do( - func() error { - log.Infof("%s: installing container runtime (%s)", h, p.Config.Spec.MCR.Version) - 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 - }, - ); err != nil { - return fmt.Errorf("retry count exceeded: %w", err) + log.Infof("%s: installing container runtime (%s)", h, p.Config.Spec.MCR.Channel) + if err := h.Configurer.InstallMCR(h, 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) } if err := h.AuthorizeDocker(); err != nil { return fmt.Errorf("%s: failed to authorize docker: %w", h, err) } - // check MCR is running, maybe rebooting and updating metadata - if err := mcr.EnsureMCRVersion(h, p.Config.Spec.MCR.Version); err != nil { - return fmt.Errorf("failed while attempting to ensure the installed version %w", err) - } - log.Infof("%s: mcr installed", h) h.Metadata.MCRInstalled = true h.Metadata.MCRRestartRequired = false // we just installed, so a restart is not required diff --git a/pkg/product/mke/phase/restart_mcr.go b/pkg/product/mke/phase/restart_mcr.go index 7fab19d7..0ee39862 100644 --- a/pkg/product/mke/phase/restart_mcr.go +++ b/pkg/product/mke/phase/restart_mcr.go @@ -23,7 +23,7 @@ func (p *RestartMCR) HostFilterFunc(h *mkeconfig.Host) bool { } // Prepare collects the hosts. -func (p *RestartMCR) Prepare(config interface{}) error { +func (p *RestartMCR) Prepare(config any) error { cfg, ok := config.(*mkeconfig.ClusterConfig) if !ok { return errInvalidConfig @@ -44,7 +44,7 @@ func (p *RestartMCR) Title() string { // Run installs the engine on each host. func (p *RestartMCR) Run() error { p.EventProperties = map[string]interface{}{ - "engine_version": p.Config.Spec.MCR.Version, + "engine_channel": p.Config.Spec.MCR.Channel, } return p.restartMCRs() } diff --git a/pkg/product/mke/phase/uninstall_mcr.go b/pkg/product/mke/phase/uninstall_mcr.go index df3aee2e..e7226d23 100644 --- a/pkg/product/mke/phase/uninstall_mcr.go +++ b/pkg/product/mke/phase/uninstall_mcr.go @@ -64,7 +64,7 @@ func (p *UninstallMCR) uninstallMCR(h *mkeconfig.Host, config *mkeconfig.Cluster return fmt.Errorf("%s: failed to unmount dangling volumes: %w", h, err) } - if err := h.Configurer.UninstallMCR(h, h.Metadata.MCRInstallScript, config.Spec.MCR); err != nil { + if err := h.Configurer.UninstallMCR(h, config.Spec.MCR); err != nil { return fmt.Errorf("%s: uninstall container runtime failed: %w", h, err) } diff --git a/pkg/product/mke/phase/upgrade_mcr.go b/pkg/product/mke/phase/upgrade_mcr.go index 95ca590d..8b107d93 100644 --- a/pkg/product/mke/phase/upgrade_mcr.go +++ b/pkg/product/mke/phase/upgrade_mcr.go @@ -41,7 +41,7 @@ func (p *UpgradeMCR) HostFilterFunc(h *mkeconfig.Host) bool { } // the following version check prevents upgrades on MCR<25, but MCR25 should always upgrade. - return h.Metadata.MCRVersion != p.Config.Spec.MCR.Version + return true } // Prepare collects the hosts. @@ -67,7 +67,7 @@ func (p *UpgradeMCR) Title() string { // Run installs the engine on each host. func (p *UpgradeMCR) Run() error { p.EventProperties = map[string]interface{}{ - "engine_version": p.Config.Spec.MCR.Version, + "engine_channel": p.Config.Spec.MCR.Channel, } return p.upgradeMCRs() } @@ -171,21 +171,13 @@ func (p *UpgradeMCR) upgradeMCRs() error { } func (p *UpgradeMCR) upgradeMCR(h *mkeconfig.Host) error { - if err := retry.Do( - func() error { - log.Infof("%s: upgrading container runtime (%s -> %s)", h, h.Metadata.MCRVersion, p.Config.Spec.MCR.Version) - 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 - }, - ); err != nil { - log.Errorf("%s: failed to update container runtime -> %s", h, err.Error()) - return fmt.Errorf("retry count exceeded: %w", err) + log.Infof("%s: upgrading container runtime (%s)", h, p.Config.Spec.MCR.Channel) + if err := h.Configurer.InstallMCR(h, p.Config.Spec.MCR); err != nil { + return fmt.Errorf("%s: failed to install container runtime: %w", h, err) } // ensure that MCR is installed and running - if err := mcr.EnsureMCRVersion(h, p.Config.Spec.MCR.Version); err != nil { + if err := mcr.EnsureMCRRunning(h, p.Config.Spec.MCR); err != nil { return fmt.Errorf("failed while attempting to ensure the installed version: %w", err) } diff --git a/pkg/product/mke/phase/validate_facts.go b/pkg/product/mke/phase/validate_facts.go index f18d4d40..e1dd3622 100644 --- a/pkg/product/mke/phase/validate_facts.go +++ b/pkg/product/mke/phase/validate_facts.go @@ -41,14 +41,6 @@ func (p *ValidateFacts) Run() error { return nil }) - if err := p.Config.Spec.MCR.Validate(); err != nil { - if p.Force { - log.Warnf("%s: continuing anyway because --force given", err.Error()) - } else { - return errors.Join(ErrFactsArentValid, err) - } - } - if err := p.validateMKEVersionJump(); err != nil { if p.Force { log.Warnf("%s: continuing anyway because --force given", err.Error()) diff --git a/pkg/product/mke/phase/validate_facts_test.go b/pkg/product/mke/phase/validate_facts_test.go index 3db2fb89..7fe4fbde 100644 --- a/pkg/product/mke/phase/validate_facts_test.go +++ b/pkg/product/mke/phase/validate_facts_test.go @@ -162,7 +162,6 @@ func TestValidateFactsPopulateSan(t *testing.T) { &mkeconfig.Host{Connection: rig.Connection{SSH: &rig.SSH{Address: "10.0.0.3"}}, Role: "worker"}, }, MCR: commonconfig.MCRConfig{ - Version: "25.0", Channel: "stable-25.0", }, MKE: mkeconfig.MKEConfig{ @@ -199,7 +198,6 @@ func TestValidateFactsDontPopulateSan(t *testing.T) { &mkeconfig.Host{Connection: rig.Connection{SSH: &rig.SSH{Address: "10.0.0.3"}}, Role: "worker"}, }, MCR: commonconfig.MCRConfig{ - Version: "25.0", Channel: "stable-25.0", }, MKE: mkeconfig.MKEConfig{ diff --git a/pkg/product/mke/reset.go b/pkg/product/mke/reset.go index a2169642..c84eca22 100644 --- a/pkg/product/mke/reset.go +++ b/pkg/product/mke/reset.go @@ -25,7 +25,6 @@ func (p *MKE) Reset() error { // end MSR phases &mke.UninstallMKE{}, - &mke.DownloadInstaller{}, &mke.UninstallMCR{}, &mke.CleanUp{}, &common.RunHooks{Stage: "after", Action: "reset"}, diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go index bdc42d06..9176fb0d 100644 --- a/test/integration/integration_test.go +++ b/test/integration/integration_test.go @@ -16,33 +16,33 @@ import ( "github.com/stretchr/testify/assert" ) -var AWS = map[string]interface{}{ +var AWS = map[string]any{ "region": "us-east-1", } -var MKE_CONNECT = map[string]interface{}{ +var MKE_CONNECT = map[string]any{ "username": "admin", "password": "", "insecure": false, } -var LAUNCHPAD = map[string]interface{}{ +var LAUNCHPAD = map[string]any{ "drain": false, - "mcr_version": "25.0.13", + "mcr_channel": "stable-25.0", "mke_version": "3.8.8", "msr_version": "2.9.28", "mke_connect": MKE_CONNECT, } // configure the network stack -var NETWORK = map[string]interface{}{ +var NETWORK = map[string]any{ "cidr": "172.31.0.0/16", } -var SUBNETS = map[string]interface{}{ - "main": map[string]interface{}{ +var SUBNETS = map[string]any{ + "main": map[string]any{ "cidr": "172.31.0.0/17", "private": false, - "nodegroups": []string{"MngrUbuntu22", "WrkRhel9"}, + "nodegroups": []string{"MngrUbuntu22", "WrkUbuntu22", "WrkRhel9", "WrkSles15"}, }, } @@ -53,9 +53,11 @@ func TestMain(m *testing.M) { tempSSHKeyPathDir := t.TempDir() log.Println("TestMKEClientConfig") - nodegroups := map[string]interface{}{ + nodegroups := map[string]any{ "MngrUbuntu22": test.Platforms["Ubuntu22"].GetManager(), "WrkRhel9": test.Platforms["Ubuntu22"].GetWorker(), + "WrkUbuntu22": test.Platforms["Rhel9"].GetWorker(), + "WrkSles15": test.Platforms["Sles15"].GetWorker(), } uTestId := test.GenerateRandomAlphaNumericString(5) @@ -67,7 +69,7 @@ func TestMain(m *testing.M) { options := terraform.Options{ // The path to where the Terraform tf chart is located TerraformDir: "../../examples/terraform/aws-simple", - Vars: map[string]interface{}{ + Vars: map[string]any{ "name": name, "aws": AWS, "launchpad": LAUNCHPAD,