Skip to content
Draft
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
84 changes: 84 additions & 0 deletions cli/cmd/install_docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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()
}

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
}
165 changes: 165 additions & 0 deletions internal/installer/docker/docker.go
Original file line number Diff line number Diff line change
@@ -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 <<EOF\n" +
"Types: deb\n" +
"URIs: https://download.docker.com/linux/ubuntu\n" +
"Suites: $SUITE\n" +
"Components: stable\n" +
"Architectures: $ARCH\n" +
"Signed-By: /etc/apt/keyrings/docker.asc\n" +
"EOF\n",
)

cmds := []string{
dockerAddRepoCmd,
"apt-get update -qq",
}

for _, cmd := range cmds {
if err := d.remoteNode.RunSSHCommand(d.remoteUser, cmd); err != nil {
return fmt.Errorf("failed to add Docker apt repository (%q): %w", cmd, err)
}
}

return nil
}

// installDockerPackages installs docker and related packages using apt-get.
func (d *dockerManager) installDockerPackages() error {
cmd := fmt.Sprintf(
"apt-get install -y -qq " +
"docker-ce " +
"docker-ce-cli " +
"containerd.io " +
"docker-buildx-plugin " +
"docker-compose-plugin",
)

if err := d.remoteNode.RunSSHCommand(d.remoteUser, cmd); err != nil {
return fmt.Errorf("failed to install Docker packages: %w", err)
}

return nil
}

// startDaemon starts and enables the Docker daemon via systemctl so it
// survives reboots.
func (d *dockerManager) startDaemon() error {
for _, cmd := range []string{
"systemctl start docker",
"systemctl enable docker",
} {
if err := d.remoteNode.RunSSHCommand(d.remoteUser, cmd); err != nil {
return fmt.Errorf("failed to run %q: %w", cmd, err)
}
}
return nil
}
16 changes: 16 additions & 0 deletions internal/installer/docker/docker_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package docker_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestDocker(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Docker Suite")
}
Loading
Loading