From ae22cc9a36f95c266480f495a634b1c51274289a Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Wed, 1 Jul 2026 19:27:41 +0200 Subject: [PATCH 1/3] feat: install docker using apt --- cli/cmd/install_docker.go | 87 ++++++++ internal/installer/docker/docker.go | 165 +++++++++++++++ .../installer/docker/docker_suite_test.go | 16 ++ internal/installer/docker/docker_test.go | 192 ++++++++++++++++++ internal/installer/docker/mocks.go | 124 +++++++++++ 5 files changed, 584 insertions(+) create mode 100644 cli/cmd/install_docker.go create mode 100644 internal/installer/docker/docker.go create mode 100644 internal/installer/docker/docker_suite_test.go create mode 100644 internal/installer/docker/docker_test.go create mode 100644 internal/installer/docker/mocks.go diff --git a/cli/cmd/install_docker.go b/cli/cmd/install_docker.go new file mode 100644 index 00000000..5b2982b2 --- /dev/null +++ b/cli/cmd/install_docker.go @@ -0,0 +1,87 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "fmt" + + packageio "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/oms/internal/env" + "github.com/codesphere-cloud/oms/internal/installer/docker" + "github.com/codesphere-cloud/oms/internal/installer/node" + "github.com/codesphere-cloud/oms/internal/util" + "github.com/spf13/cobra" +) + +// InstallDockerCmd represents the docker install command. +type InstallDockerCmd struct { + cmd *cobra.Command + Opts InstallDockerOpts + Env env.Env + FileWriter util.FileIO +} + +// InstallDockerOpts holds all CLI flags for the docker sub-command. +type InstallDockerOpts struct { + *GlobalOptions + + // SSH / remote host + // SSHKeyPath string + SSHUser string + SSHHost string + SSHPort int +} + +func (c *InstallDockerCmd) RunE(_ *cobra.Command, args []string) error { + return c.InstallDocker() +} + +// AddInstallDockerCmd registers the "install docker" sub-command under +// the provided parent install command, following the same pattern as +// AddInstallK0sCmd. +func AddInstallDockerCmd(install *cobra.Command, opts *GlobalOptions) { + pg := InstallDockerCmd{ + cmd: &cobra.Command{ + Use: "docker", + Short: "Install Docker on a remote host", + Long: packageio.Long(`Install Docker (if not already present) on a remote host accessed via SSH.`), + Example: formatExamples("install docker", []packageio.Example{}), + }, + Opts: InstallDockerOpts{GlobalOptions: opts}, + Env: env.NewEnv(), + FileWriter: util.NewFilesystemWriter(), + } + + f := pg.cmd.Flags() + + // SSH flags + f.StringVar(&pg.Opts.SSHHost, "ssh-host", "", "Remote host IP or hostname (required)") + f.IntVar(&pg.Opts.SSHPort, "ssh-port", 22, "SSH port on the remote host") + f.StringVar(&pg.Opts.SSHUser, "ssh-user", "root", "SSH username") + // f.StringVar(&pg.Opts.SSHKeyPath, "ssh-key-path", "", "Path to SSH private key") + + _ = pg.cmd.MarkFlagRequired("ssh-host") + + AddCmd(install, pg.cmd) + pg.cmd.RunE = pg.RunE +} + +func (c *InstallDockerCmd) InstallDocker() error { + node := &node.Node{ + Name: "node", + ExternalIP: c.Opts.SSHHost, + + NodeClient: node.NewSSHNodeClient(true), + } + + dockerClient := docker.New("root", node) + if !dockerClient.IsInstalled() { + err := dockerClient.InstallWithApt() + if err != nil { + return fmt.Errorf("failed to install Docker: %w", err) + } + } + + return nil +} diff --git a/internal/installer/docker/docker.go b/internal/installer/docker/docker.go new file mode 100644 index 00000000..c066ccde --- /dev/null +++ b/internal/installer/docker/docker.go @@ -0,0 +1,165 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +// package docker installs docker on a remote host +package docker + +import ( + "fmt" + "log" + + "github.com/codesphere-cloud/oms/internal/installer/node" +) + +// DockerManager abstracts Docker operations on a remote host. +// The interface makes the command easy to unit-test with mocks. +// +//mockery:generate: true +type DockerManager interface { + // IsInstalled checks whether the docker binary is available on the remote host. + IsInstalled() bool + + // Install installs Docker Engine on the remote host using Docker's official apt repository. + InstallWithApt() error +} + +type dockerManager struct { + remoteUser string + remoteNode *node.Node +} + +func New(user string, node *node.Node) DockerManager { + return &dockerManager{ + remoteUser: user, + remoteNode: node, + } +} + +// IsInstalled checks whether the docker binary is available on the remote host. +func (d *dockerManager) IsInstalled() bool { + err := d.remoteNode.RunSSHCommand(d.remoteUser, "command -v docker") + + return err == nil +} + +// InstallWithApt installs Docker Engine on the remote host using Docker's official apt repository +// see https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository +func (d *dockerManager) InstallWithApt() error { + log.Println("Installing Docker on remote host via apt...") + + if err := d.removeConflictingPackages(); err != nil { + return fmt.Errorf("failed to remove conflicting docker packages") + } + + if err := d.installAptPrerequisites(); err != nil { + return fmt.Errorf("failed to install docker apt prequisites") + } + + if err := d.addDockerRepository(); err != nil { + return fmt.Errorf("failed to add docker apt repository") + } + + if err := d.installDockerPackages(); err != nil { + return fmt.Errorf("failed to install docker packages") + } + + if err := d.startDaemon(); err != nil { + return fmt.Errorf("failed to start docker daemon") + } + + return nil +} + +// removeConflictingPackages removes any unofficial Docker packages that may +// conflict with the official Docker Engine packages. The list matches what the +// official docs specify. +func (d *dockerManager) removeConflictingPackages() error { + cmd := "apt-get remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc 2>/dev/null || true" + if err := d.remoteNode.RunSSHCommand(d.remoteUser, cmd); err != nil { + return fmt.Errorf("failed to remove conflicting packages: %w", err) + } + + return nil +} + +// installAptPrerequisites ensures ca-certificates and curl are present; +// these are required before the Docker GPG key and repo can be added. +func (d *dockerManager) installAptPrerequisites() error { + log.Println("Installing Docker apt prerequisites...") + for _, cmd := range []string{ + "apt-get update -qq", + "apt-get install -y -qq ca-certificates curl", + } { + if err := d.remoteNode.RunSSHCommand(d.remoteUser, cmd); err != nil { + return fmt.Errorf("failed to install apt prerequisites (%q): %w", cmd, err) + } + } + + return nil +} + +// addDockerRepository adds Docker's official GPG key and apt repository, +// exactly as described in the official Ubuntu install docs. +func (d *dockerManager) addDockerRepository() error { + log.Println("Adding Docker apt repository...") + dockerAddRepoCmd := fmt.Sprintf( + "sudo install -m 0755 -d /etc/apt/keyrings && " + + "sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc && " + + "sudo chmod a+r /etc/apt/keyrings/docker.asc && " + + "SUITE=$(. /etc/os-release && echo \"${UBUNTU_CODENAME:-$VERSION_CODENAME}\") && " + + "ARCH=$(dpkg --print-architecture) && " + + "sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null </dev/null || true"). + Return(errors.New("ssh error")) + + err := manager.InstallWithApt() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to remove conflicting docker packages")) + }) + + It("fails when installing apt prerequisites fails", func() { + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc 2>/dev/null || true"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get update -qq"). + Return(errors.New("apt error")) + + err := manager.InstallWithApt() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install docker apt prequisites")) + }) + + It("fails when adding the docker repository fails", func() { + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc 2>/dev/null || true"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get update -qq"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get install -y -qq ca-certificates curl"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", mock.MatchedBy(isAddRepoCommand)). + Return(errors.New("repo error")). + Once() + + err := manager.InstallWithApt() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to add docker apt repository")) + }) + + It("fails when installing docker packages fails", func() { + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc 2>/dev/null || true"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get update -qq"). + Return(nil). + Twice() + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get install -y -qq ca-certificates curl"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", mock.MatchedBy(isAddRepoCommand)). + Return(nil). + Once() + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin"). + Return(errors.New("install error")) + + err := manager.InstallWithApt() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install docker packages")) + }) + + It("fails when starting the docker daemon fails", func() { + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc 2>/dev/null || true"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get update -qq"). + Return(nil). + Twice() + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get install -y -qq ca-certificates curl"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", mock.MatchedBy(isAddRepoCommand)). + Return(nil). + Once() + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "systemctl start docker"). + Return(errors.New("daemon error")) + + err := manager.InstallWithApt() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to start docker daemon")) + }) + + It("succeeds when all steps succeed", func() { + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc 2>/dev/null || true"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get update -qq"). + Return(nil). + Twice() + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get install -y -qq ca-certificates curl"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", mock.MatchedBy(isAddRepoCommand)). + Return(nil). + Once() + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "systemctl start docker"). + Return(nil) + nodeClient.EXPECT(). + RunCommand(remoteNode, "ubuntu", "systemctl enable docker"). + Return(nil) + + err := manager.InstallWithApt() + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) diff --git a/internal/installer/docker/mocks.go b/internal/installer/docker/mocks.go new file mode 100644 index 00000000..34c8bed1 --- /dev/null +++ b/internal/installer/docker/mocks.go @@ -0,0 +1,124 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package docker + +import ( + mock "github.com/stretchr/testify/mock" +) + +// NewMockDockerInstaller creates a new instance of MockDockerInstaller. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockDockerInstaller(t interface { + mock.TestingT + Cleanup(func()) +}) *MockDockerInstaller { + mock := &MockDockerInstaller{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockDockerInstaller is an autogenerated mock type for the DockerInstaller type +type MockDockerInstaller struct { + mock.Mock +} + +type MockDockerInstaller_Expecter struct { + mock *mock.Mock +} + +func (_m *MockDockerInstaller) EXPECT() *MockDockerInstaller_Expecter { + return &MockDockerInstaller_Expecter{mock: &_m.Mock} +} + +// Install provides a mock function for the type MockDockerInstaller +func (_mock *MockDockerInstaller) Install() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Install") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockDockerInstaller_Install_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Install' +type MockDockerInstaller_Install_Call struct { + *mock.Call +} + +// Install is a helper method to define mock.On call +func (_e *MockDockerInstaller_Expecter) Install() *MockDockerInstaller_Install_Call { + return &MockDockerInstaller_Install_Call{Call: _e.mock.On("Install")} +} + +func (_c *MockDockerInstaller_Install_Call) Run(run func()) *MockDockerInstaller_Install_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockDockerInstaller_Install_Call) Return(err error) *MockDockerInstaller_Install_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockDockerInstaller_Install_Call) RunAndReturn(run func() error) *MockDockerInstaller_Install_Call { + _c.Call.Return(run) + return _c +} + +// IsInstalled provides a mock function for the type MockDockerInstaller +func (_mock *MockDockerInstaller) IsInstalled() bool { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for IsInstalled") + } + + var r0 bool + if returnFunc, ok := ret.Get(0).(func() bool); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(bool) + } + return r0 +} + +// MockDockerInstaller_IsInstalled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsInstalled' +type MockDockerInstaller_IsInstalled_Call struct { + *mock.Call +} + +// IsInstalled is a helper method to define mock.On call +func (_e *MockDockerInstaller_Expecter) IsInstalled() *MockDockerInstaller_IsInstalled_Call { + return &MockDockerInstaller_IsInstalled_Call{Call: _e.mock.On("IsInstalled")} +} + +func (_c *MockDockerInstaller_IsInstalled_Call) Run(run func()) *MockDockerInstaller_IsInstalled_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockDockerInstaller_IsInstalled_Call) Return(b bool) *MockDockerInstaller_IsInstalled_Call { + _c.Call.Return(b) + return _c +} + +func (_c *MockDockerInstaller_IsInstalled_Call) RunAndReturn(run func() bool) *MockDockerInstaller_IsInstalled_Call { + _c.Call.Return(run) + return _c +} From db6dd43d1b04740aed8bf40c72e5702c6c0f0e17 Mon Sep 17 00:00:00 2001 From: joka134 <27293650+joka134@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:30:45 +0000 Subject: [PATCH 2/3] chore(docs): Auto-update docs and licenses Signed-off-by: joka134 <27293650+joka134@users.noreply.github.com> --- internal/installer/docker/mocks.go | 58 +++++++++++++++--------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/internal/installer/docker/mocks.go b/internal/installer/docker/mocks.go index 34c8bed1..5a4ec8f6 100644 --- a/internal/installer/docker/mocks.go +++ b/internal/installer/docker/mocks.go @@ -8,13 +8,13 @@ import ( mock "github.com/stretchr/testify/mock" ) -// NewMockDockerInstaller creates a new instance of MockDockerInstaller. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockDockerManager creates a new instance of MockDockerManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewMockDockerInstaller(t interface { +func NewMockDockerManager(t interface { mock.TestingT Cleanup(func()) -}) *MockDockerInstaller { - mock := &MockDockerInstaller{} +}) *MockDockerManager { + mock := &MockDockerManager{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) @@ -22,25 +22,25 @@ func NewMockDockerInstaller(t interface { return mock } -// MockDockerInstaller is an autogenerated mock type for the DockerInstaller type -type MockDockerInstaller struct { +// MockDockerManager is an autogenerated mock type for the DockerManager type +type MockDockerManager struct { mock.Mock } -type MockDockerInstaller_Expecter struct { +type MockDockerManager_Expecter struct { mock *mock.Mock } -func (_m *MockDockerInstaller) EXPECT() *MockDockerInstaller_Expecter { - return &MockDockerInstaller_Expecter{mock: &_m.Mock} +func (_m *MockDockerManager) EXPECT() *MockDockerManager_Expecter { + return &MockDockerManager_Expecter{mock: &_m.Mock} } -// Install provides a mock function for the type MockDockerInstaller -func (_mock *MockDockerInstaller) Install() error { +// InstallWithApt provides a mock function for the type MockDockerManager +func (_mock *MockDockerManager) InstallWithApt() error { ret := _mock.Called() if len(ret) == 0 { - panic("no return value specified for Install") + panic("no return value specified for InstallWithApt") } var r0 error @@ -52,35 +52,35 @@ func (_mock *MockDockerInstaller) Install() error { return r0 } -// MockDockerInstaller_Install_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Install' -type MockDockerInstaller_Install_Call struct { +// MockDockerManager_InstallWithApt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InstallWithApt' +type MockDockerManager_InstallWithApt_Call struct { *mock.Call } -// Install is a helper method to define mock.On call -func (_e *MockDockerInstaller_Expecter) Install() *MockDockerInstaller_Install_Call { - return &MockDockerInstaller_Install_Call{Call: _e.mock.On("Install")} +// InstallWithApt is a helper method to define mock.On call +func (_e *MockDockerManager_Expecter) InstallWithApt() *MockDockerManager_InstallWithApt_Call { + return &MockDockerManager_InstallWithApt_Call{Call: _e.mock.On("InstallWithApt")} } -func (_c *MockDockerInstaller_Install_Call) Run(run func()) *MockDockerInstaller_Install_Call { +func (_c *MockDockerManager_InstallWithApt_Call) Run(run func()) *MockDockerManager_InstallWithApt_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockDockerInstaller_Install_Call) Return(err error) *MockDockerInstaller_Install_Call { +func (_c *MockDockerManager_InstallWithApt_Call) Return(err error) *MockDockerManager_InstallWithApt_Call { _c.Call.Return(err) return _c } -func (_c *MockDockerInstaller_Install_Call) RunAndReturn(run func() error) *MockDockerInstaller_Install_Call { +func (_c *MockDockerManager_InstallWithApt_Call) RunAndReturn(run func() error) *MockDockerManager_InstallWithApt_Call { _c.Call.Return(run) return _c } -// IsInstalled provides a mock function for the type MockDockerInstaller -func (_mock *MockDockerInstaller) IsInstalled() bool { +// IsInstalled provides a mock function for the type MockDockerManager +func (_mock *MockDockerManager) IsInstalled() bool { ret := _mock.Called() if len(ret) == 0 { @@ -96,29 +96,29 @@ func (_mock *MockDockerInstaller) IsInstalled() bool { return r0 } -// MockDockerInstaller_IsInstalled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsInstalled' -type MockDockerInstaller_IsInstalled_Call struct { +// MockDockerManager_IsInstalled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsInstalled' +type MockDockerManager_IsInstalled_Call struct { *mock.Call } // IsInstalled is a helper method to define mock.On call -func (_e *MockDockerInstaller_Expecter) IsInstalled() *MockDockerInstaller_IsInstalled_Call { - return &MockDockerInstaller_IsInstalled_Call{Call: _e.mock.On("IsInstalled")} +func (_e *MockDockerManager_Expecter) IsInstalled() *MockDockerManager_IsInstalled_Call { + return &MockDockerManager_IsInstalled_Call{Call: _e.mock.On("IsInstalled")} } -func (_c *MockDockerInstaller_IsInstalled_Call) Run(run func()) *MockDockerInstaller_IsInstalled_Call { +func (_c *MockDockerManager_IsInstalled_Call) Run(run func()) *MockDockerManager_IsInstalled_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockDockerInstaller_IsInstalled_Call) Return(b bool) *MockDockerInstaller_IsInstalled_Call { +func (_c *MockDockerManager_IsInstalled_Call) Return(b bool) *MockDockerManager_IsInstalled_Call { _c.Call.Return(b) return _c } -func (_c *MockDockerInstaller_IsInstalled_Call) RunAndReturn(run func() bool) *MockDockerInstaller_IsInstalled_Call { +func (_c *MockDockerManager_IsInstalled_Call) RunAndReturn(run func() bool) *MockDockerManager_IsInstalled_Call { _c.Call.Return(run) return _c } From 70437701ece2b837cd687d09cc821a2bd6cb7bed Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Wed, 1 Jul 2026 19:34:31 +0200 Subject: [PATCH 3/3] chore: update mocks --- cli/cmd/install_docker.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cli/cmd/install_docker.go b/cli/cmd/install_docker.go index 5b2982b2..e93daec0 100644 --- a/cli/cmd/install_docker.go +++ b/cli/cmd/install_docker.go @@ -37,9 +37,6 @@ func (c *InstallDockerCmd) RunE(_ *cobra.Command, args []string) error { return c.InstallDocker() } -// AddInstallDockerCmd registers the "install docker" sub-command under -// the provided parent install command, following the same pattern as -// AddInstallK0sCmd. func AddInstallDockerCmd(install *cobra.Command, opts *GlobalOptions) { pg := InstallDockerCmd{ cmd: &cobra.Command{