diff --git a/pkg/configurer/enterpriselinux/el.go b/pkg/configurer/enterpriselinux/el.go index d7f4dc03..b3c7f432 100644 --- a/pkg/configurer/enterpriselinux/el.go +++ b/pkg/configurer/enterpriselinux/el.go @@ -73,8 +73,9 @@ gpgkey=%s if err := c.InstallPackage(h, "containerd.io"); err != nil { return fmt.Errorf("package manager could not install containerd.io") } - if err := c.InstallPackage(h, "docker-ee"); err != nil { - return fmt.Errorf("package manager could not install docker-ee") + mcrPackages := configurer.MCRPackages(engineConfig) + if err := c.InstallPackage(h, mcrPackages...); err != nil { + return fmt.Errorf("package manager could not install %s", strings.Join(mcrPackages, ", ")) } if err := c.EnableMCR(h, engineConfig); err != nil { diff --git a/pkg/configurer/linux.go b/pkg/configurer/linux.go index 6178e26a..33673ea6 100644 --- a/pkg/configurer/linux.go +++ b/pkg/configurer/linux.go @@ -26,6 +26,29 @@ const ( SbinPath = `PATH=/usr/local/sbin:/usr/sbin:/sbin:$PATH` ) +const ( + // mcrPackage is the Mirantis Container Runtime package name. It is identical + // across the rpm and deb repositories on repos.mirantis.com. + mcrPackage = "docker-ee" + // mcrCLIPackage is the docker CLI package name, likewise identical across + // rpm and deb. + mcrCLIPackage = "docker-ee-cli" +) + +// MCRPackages returns the packages to install for MCR, in the order they should +// be passed to the package manager. +// +// The CLI is only listed when spec.mcr.installCLI is set. It is returned in the +// same slice rather than installed separately so that both land in a single +// package manager transaction and cannot be resolved to mismatched versions. +func MCRPackages(engineConfig commonconfig.MCRConfig) []string { + if engineConfig.InstallCLI { + return []string{mcrPackage, mcrCLIPackage} + } + + return []string{mcrPackage} +} + var ErrLinuxMCRInstall = errors.New("failed to install MCR on linux") // LinuxConfigurer is a generic linux host configurer. diff --git a/pkg/configurer/mcr_packages_test.go b/pkg/configurer/mcr_packages_test.go new file mode 100644 index 00000000..06688e97 --- /dev/null +++ b/pkg/configurer/mcr_packages_test.go @@ -0,0 +1,37 @@ +package configurer_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/Mirantis/launchpad/pkg/configurer" + commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" +) + +// TestMCRPackages covers the package set every Linux configurer installs. +// +// The runtime package lists the CLI only as a Recommends (a weak dependency), +// not a Requires/Depends, so the default must stay runtime-only: adding the CLI +// unconditionally would change what every existing cluster installs. +// spec.mcr.installCLI opts in for hosts where weak-dependency resolution is +// disabled or otherwise cannot be relied on. See PRODENG-3641. +func TestMCRPackages(t *testing.T) { + t.Run("default installs the runtime only", func(t *testing.T) { + require.Equal(t, []string{"docker-ee"}, configurer.MCRPackages(commonconfig.MCRConfig{})) + }) + + t.Run("installCLI adds the cli package", func(t *testing.T) { + require.Equal(t, []string{"docker-ee", "docker-ee-cli"}, + configurer.MCRPackages(commonconfig.MCRConfig{InstallCLI: true})) + }) + + t.Run("both packages are returned together for a single transaction", func(t *testing.T) { + // Returned as one slice, and the runtime stays first, so callers hand both + // to the package manager in one invocation. Installing them separately + // would allow the runtime and CLI to resolve to mismatched versions. + pkgs := configurer.MCRPackages(commonconfig.MCRConfig{InstallCLI: true}) + require.Len(t, pkgs, 2) + require.Equal(t, "docker-ee", pkgs[0]) + }) +} diff --git a/pkg/configurer/sles/sles.go b/pkg/configurer/sles/sles.go index b1fa12ca..f1035416 100644 --- a/pkg/configurer/sles/sles.go +++ b/pkg/configurer/sles/sles.go @@ -107,8 +107,9 @@ func (c Configurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) e if err := h.Exec("zypper -n install -y --allow-vendor-change containerd.io", exec.Sudo(h)); err != nil { return fmt.Errorf("package manager could not install containerd.io: %w", err) } - if err := h.Exec("zypper -n install -y --allow-vendor-change docker-ee", exec.Sudo(h)); err != nil { - return fmt.Errorf("package manager could not install docker-ee: %w", err) + mcrPackages := configurer.MCRPackages(engineConfig) + if err := h.Exec("zypper -n install -y --allow-vendor-change "+strings.Join(mcrPackages, " "), exec.Sudo(h)); err != nil { + return fmt.Errorf("package manager could not install %s: %w", strings.Join(mcrPackages, ", "), err) } if err := c.EnableMCR(h, engineConfig); err != nil { diff --git a/pkg/configurer/ubuntu/ubuntu.go b/pkg/configurer/ubuntu/ubuntu.go index 6cd212af..29b5bb1f 100644 --- a/pkg/configurer/ubuntu/ubuntu.go +++ b/pkg/configurer/ubuntu/ubuntu.go @@ -2,6 +2,7 @@ package ubuntu import ( "fmt" + "strings" "github.com/Mirantis/launchpad/pkg/configurer" commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" @@ -80,8 +81,9 @@ Signed-by: /usr/share/keyrings/mirantis-archive-keyring.gpg if err := c.InstallPackage(h, "containerd.io"); err != nil { return fmt.Errorf("package manager could not install containerd.io") } - if err := c.InstallPackage(h, "docker-ee"); err != nil { - return fmt.Errorf("package manager could not install docker-ee") + mcrPackages := configurer.MCRPackages(engineConfig) + if err := c.InstallPackage(h, mcrPackages...); err != nil { + return fmt.Errorf("package manager could not install %s", strings.Join(mcrPackages, ", ")) } if err := c.EnableMCR(h, engineConfig); err != nil { diff --git a/pkg/product/common/config/mcr_config.go b/pkg/product/common/config/mcr_config.go index f1bf7bff..6547e8a5 100644 --- a/pkg/product/common/config/mcr_config.go +++ b/pkg/product/common/config/mcr_config.go @@ -31,6 +31,25 @@ type MCRConfig struct { SwarmInstallFlags Flags `yaml:"swarmInstallFlags,omitempty,flow"` SwarmUpdateCommands []string `yaml:"swarmUpdateCommands,omitempty,flow"` + // InstallCLI additionally installs the docker CLI package (docker-ee-cli) + // alongside the runtime, in the same package manager transaction so the two + // cannot end up at mismatched versions. + // + // The runtime package lists the CLI only as a Recommends (a weak + // dependency), not a Requires/Depends, on every repos.mirantis.com package + // checked -- rpm (dnf/yum) and deb (apt) alike. Package managers install + // Recommends by default, so this is normally not needed. It is needed when + // weak-dependency installation is disabled, which common hardening + // baselines do explicitly (dnf/yum: install_weak_deps=false; apt: + // APT::Install-Recommends "false"). Set it on hosts where that applies, or + // where the runtime has otherwise installed without the CLI. See + // PRODENG-3641. + // + // Linux only. Windows installs MCR through install.ps1, which ships the CLI + // as part of the same archive and has no separate package, so this value is + // ignored on Windows hosts. + InstallCLI bool `yaml:"installCLI,omitempty"` + Metadata *MCRMetadata `yaml:"-"` } diff --git a/pkg/product/common/config/mcr_config_test.go b/pkg/product/common/config/mcr_config_test.go index 977aa3b4..c9e20818 100644 --- a/pkg/product/common/config/mcr_config_test.go +++ b/pkg/product/common/config/mcr_config_test.go @@ -39,3 +39,35 @@ func TestSwarmUpdateCommands(t *testing.T) { require.Equal(t, 1, slices.Index(cfg.SwarmUpdateCommands, "command2")) require.Equal(t, 2, slices.Index(cfg.SwarmUpdateCommands, "command3")) } + +func TestMCRConfig_InstallCLI(t *testing.T) { + // The yaml key is the user-facing contract: a mismatch here would silently + // ignore the setting and reintroduce PRODENG-3641 for anyone opting in. + for _, tc := range []struct { + name string + yaml string + expected bool + }{ + { + name: "absent defaults to false", + yaml: "channel: stable", + expected: false, + }, + { + name: "installCLI true is honoured", + yaml: "channel: stable\ninstallCLI: true", + expected: true, + }, + { + name: "installCLI false is honoured", + yaml: "channel: stable\ninstallCLI: false", + expected: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + cfg := commonconfig.MCRConfig{} + require.NoError(t, yaml.Unmarshal([]byte(tc.yaml), &cfg)) + require.Equal(t, tc.expected, cfg.InstallCLI) + }) + } +}