Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions pkg/configurer/enterpriselinux/el.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@ 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")
installCmd, cmdErr := configurer.MCRInstallCommand(configurer.Yum, engineConfig)
if cmdErr != nil {
return fmt.Errorf("could not build MCR install command: %w", cmdErr)
}
if err := h.Exec(installCmd, exec.Sudo(h)); err != nil {
return fmt.Errorf("package manager could not install docker-ee: %w", err)
}

if err := c.EnableMCR(h, engineConfig); err != nil {
Expand Down
73 changes: 73 additions & 0 deletions pkg/configurer/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,79 @@ const (
SbinPath = `PATH=/usr/local/sbin:/usr/sbin:/sbin:$PATH`
)

// mcrPackage is the Mirantis Container Runtime package name. It is identical
// across the rpm and deb repositories on repos.mirantis.com.
const mcrPackage = "docker-ee"

// PackageManager identifies the package manager a Linux family installs MCR with.
type PackageManager int

const (
// Yum is dnf/yum, on the enterprise linux family.
Yum PackageManager = iota
// AptGet is apt-get, on debian and ubuntu.
AptGet
// Zypper is zypper, on SLES.
Zypper
)

// ErrUnknownPackageManager is returned for a PackageManager this package does
// not know how to build a command for.
var ErrUnknownPackageManager = errors.New("unknown package manager")

// MCRInstallCommand returns the command that installs the MCR runtime package.
//
// These commands are issued directly rather than through rig's InstallPackage,
// which hardcodes `<manager> install -y <packages>` and offers no way to pass
// additional arguments. SLES already had to bypass it for --allow-vendor-change
// (PRODENG-3623); installRecommends needs the same freedom on every family, so
// all three are built here instead, in one place, rather than three.
//
// When engineConfig.InstallRecommends is set, the manager is instructed to
// install recommended packages regardless of the host's configured default:
//
// - yum/dnf: --setopt=install_weak_deps=True
// - apt-get: -o APT::Install-Recommends=true, the configuration item behind
// apt's --no-install-recommends flag
// - zypper: --recommends, "install also recommended packages in addition to
// the required ones"
//
// Which packages those are is decided by the repository metadata, not here. See
// PRODENG-3641.
func MCRInstallCommand(manager PackageManager, engineConfig commonconfig.MCRConfig) (string, error) {
switch manager {
case Yum:
args := []string{"yum", "install", "-y"}
if engineConfig.InstallRecommends {
args = append(args, "--setopt=install_weak_deps=True")
}

return strings.Join(append(args, mcrPackage), " "), nil
case AptGet:
// -q matches what rig's InstallPackage ran, so the default command is
// unchanged. rig also ran `apt-get update` here; InstallMCR already does
// that explicitly above, so it is not repeated.
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y", "-q"}
if engineConfig.InstallRecommends {
args = append(args, "-o", "APT::Install-Recommends=true")
}

return strings.Join(append(args, mcrPackage), " "), nil
case Zypper:
// --allow-vendor-change is unconditional on SLES: cloud images ship a
// SUSE-vendor containerd that the Mirantis packages must replace, and
// zypper otherwise cancels non-interactively. See PRODENG-3623.
args := []string{"zypper", "-n", "install", "-y", "--allow-vendor-change"}
if engineConfig.InstallRecommends {
args = append(args, "--recommends")
}

return strings.Join(append(args, mcrPackage), " "), nil
default:
return "", fmt.Errorf("%w: %d", ErrUnknownPackageManager, manager)
}
}

var ErrLinuxMCRInstall = errors.New("failed to install MCR on linux")

// LinuxConfigurer is a generic linux host configurer.
Expand Down
79 changes: 79 additions & 0 deletions pkg/configurer/mcr_install_command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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"
)

// TestMCRInstallCommand pins the install command for every Linux family, with
// and without installRecommends.
//
// docker-ee declares docker-ee-cli and cri-dockerd-ee as recommended packages
// rather than hard requirements. Package managers install recommended packages
// by default, so the unset case must stay exactly what launchpad ran before:
// turning recommends on for everyone would change what every existing cluster
// installs. The set case must pass each manager's own opt-in. See PRODENG-3641.
func TestMCRInstallCommand(t *testing.T) {
for _, tc := range []struct {
name string
manager configurer.PackageManager
recommen bool
expected string
}{
{
name: "yum default is unchanged from the previous behaviour",
manager: configurer.Yum,
expected: "yum install -y docker-ee",
},
{
name: "yum opts in with setopt",
manager: configurer.Yum,
recommen: true,
expected: "yum install -y --setopt=install_weak_deps=True docker-ee",
},
{
name: "apt-get default is unchanged from the previous behaviour",
manager: configurer.AptGet,
expected: "DEBIAN_FRONTEND=noninteractive apt-get install -y -q docker-ee",
},
{
name: "apt-get opts in with the Install-Recommends config item",
manager: configurer.AptGet,
recommen: true,
expected: "DEBIAN_FRONTEND=noninteractive apt-get install -y -q -o APT::Install-Recommends=true docker-ee",
},
{
// --allow-vendor-change must survive in both cases: without it zypper
// cancels non-interactively on SLES cloud images. See PRODENG-3623.
name: "zypper keeps allow-vendor-change when recommends is unset",
manager: configurer.Zypper,
expected: "zypper -n install -y --allow-vendor-change docker-ee",
},
{
name: "zypper opts in with --recommends alongside allow-vendor-change",
manager: configurer.Zypper,
recommen: true,
expected: "zypper -n install -y --allow-vendor-change --recommends docker-ee",
},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := configurer.MCRInstallCommand(tc.manager,
commonconfig.MCRConfig{InstallRecommends: tc.recommen})
require.NoError(t, err)
require.Equal(t, tc.expected, got)
})
}
}

// TestMCRInstallCommandUnknownManager covers the error path, so an unhandled
// family fails loudly rather than returning an empty command that would be
// executed as a no-op.
func TestMCRInstallCommandUnknownManager(t *testing.T) {
got, err := configurer.MCRInstallCommand(configurer.PackageManager(99), commonconfig.MCRConfig{})
require.ErrorIs(t, err, configurer.ErrUnknownPackageManager)
require.Empty(t, got)
}
6 changes: 5 additions & 1 deletion pkg/configurer/sles/sles.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ 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 {
installCmd, cmdErr := configurer.MCRInstallCommand(configurer.Zypper, engineConfig)
if cmdErr != nil {
return fmt.Errorf("could not build MCR install command: %w", cmdErr)
}
if err := h.Exec(installCmd, exec.Sudo(h)); err != nil {
return fmt.Errorf("package manager could not install docker-ee: %w", err)
}

Expand Down
8 changes: 6 additions & 2 deletions pkg/configurer/ubuntu/ubuntu.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,12 @@ 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")
installCmd, cmdErr := configurer.MCRInstallCommand(configurer.AptGet, engineConfig)
if cmdErr != nil {
return fmt.Errorf("could not build MCR install command: %w", cmdErr)
}
if err := h.Exec(installCmd, exec.Sudo(h)); err != nil {
return fmt.Errorf("package manager could not install docker-ee: %w", err)
}

if err := c.EnableMCR(h, engineConfig); err != nil {
Expand Down
23 changes: 23 additions & 0 deletions pkg/product/common/config/mcr_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,29 @@ type MCRConfig struct {
SwarmInstallFlags Flags `yaml:"swarmInstallFlags,omitempty,flow"`
SwarmUpdateCommands []string `yaml:"swarmUpdateCommands,omitempty,flow"`

// InstallRecommends tells the package manager to install the runtime's
// recommended packages even when the host is configured not to.
//
// docker-ee declares docker-ee-cli and cri-dockerd-ee as recommended
// packages (rpm Recommends / deb Recommends), not as hard requirements.
// Package managers install recommended packages by default, so this is
// normally unnecessary. Hardening baselines commonly turn that off --
// install_weak_deps=false for dnf/yum, APT::Install-Recommends "false" for
// apt, solver.onlyRequires for zypper -- and the runtime then installs
// without its CLI, leaving later docker commands to fail on a host that
// otherwise looks correctly installed. See PRODENG-3641.
//
// Note the set of recommended packages is defined by the repository
// metadata, not by launchpad, and differs by platform. On rpm hosts it is
// docker-ee-cli and cri-dockerd-ee. On deb hosts it additionally includes
// ca-certificates, docker-ee-rootless-extras, git, a kernel package,
// libltdl7, pigz, procps and xz-utils.
//
// Linux only. Windows installs MCR through install.ps1 from a single archive
// that already contains the CLI; there is no package manager and no
// equivalent concept, so this value is ignored on Windows hosts.
InstallRecommends bool `yaml:"installRecommends,omitempty"`

Metadata *MCRMetadata `yaml:"-"`
}

Expand Down
33 changes: 33 additions & 0 deletions pkg/product/common/config/mcr_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,36 @@ 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_InstallRecommends(t *testing.T) {
// The yaml key is the user-facing contract: a mismatch here would silently
// ignore the setting and leave the runtime installed without its CLI on a
// host that disables weak dependencies. See PRODENG-3641.
for _, tc := range []struct {
name string
yaml string
expected bool
}{
{
name: "absent defaults to false",
yaml: "channel: stable",
expected: false,
},
{
name: "installRecommends true is honoured",
yaml: "channel: stable\ninstallRecommends: true",
expected: true,
},
{
name: "installRecommends false is honoured",
yaml: "channel: stable\ninstallRecommends: 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.InstallRecommends)
})
}
}