From 394b5b35045a001ee2c66750a8382934c411d96d Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Wed, 1 Jul 2026 11:34:38 +0200 Subject: [PATCH 01/12] feat: install docker in bootstrapping on postgres vm --- cli/cmd/install_postgres.go | 245 ++++++++++++++++++ internal/bootstrap/gcp/gcp.go | 7 + internal/bootstrap/gcp/postgres.go | 14 + .../installer/docker/argocd_suite_test.go | 16 ++ internal/installer/docker/docker.go | 157 +++++++++++ internal/installer/docker/docker_test.go | 192 ++++++++++++++ 6 files changed, 631 insertions(+) create mode 100644 cli/cmd/install_postgres.go create mode 100644 internal/bootstrap/gcp/postgres.go create mode 100644 internal/installer/docker/argocd_suite_test.go create mode 100644 internal/installer/docker/docker.go create mode 100644 internal/installer/docker/docker_test.go diff --git a/cli/cmd/install_postgres.go b/cli/cmd/install_postgres.go new file mode 100644 index 00000000..0c52e54f --- /dev/null +++ b/cli/cmd/install_postgres.go @@ -0,0 +1,245 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + packageio "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/oms/internal/env" + "github.com/codesphere-cloud/oms/internal/util" + "github.com/spf13/cobra" +) + +// InstallPostgresCmd represents the postgres install command. +type InstallPostgresCmd struct { + cmd *cobra.Command + Opts InstallPostgresOpts + Env env.Env + FileWriter util.FileIO +} + +// InstallPostgresOpts holds all CLI flags for the postgres sub-command. +type InstallPostgresOpts struct { + *GlobalOptions + + // SSH / remote host + SSHKeyPath string + SSHUser string + SSHHost string + SSHPort int + + // Docker + DockerVersion string // empty → latest + + // Postgres container + PostgresVersion string // Docker image tag, e.g. "16" or "16.3-alpine" + ContainerName string + DataDir string // host-side bind-mount for pg data + PostgresPassword string + PostgresUser string + PostgresDB string + PublishPort string // host:container, e.g. "5432:5432" + RestartPolicy string // always | unless-stopped | on-failure | no + ForcePostgres bool // stop & recreate the container if it already exists +} + +func (c *InstallPostgresCmd) RunE(_ *cobra.Command, _ []string) error { + // // ssh := installer.NewSSHRunner( + // // c.Opts.SSHHost, + // // c.Opts.SSHPort, + // // c.Opts.SSHUser, + // // c.Opts.SSHKeyPath, + // // ) + // // docker := installer.NewDockerManager(ssh) + // // pg := installer.NewPostgresManager(ssh) + + // // return c.InstallPostgres(docker, pg) + return nil +} + +// AddInstallPostgresCmd registers the "install postgres" sub-command under +// the provided parent install command, following the same pattern as +// AddInstallK0sCmd. +func AddInstallPostgresCmd(install *cobra.Command, opts *GlobalOptions) { + pg := InstallPostgresCmd{ + cmd: &cobra.Command{ + Use: "postgres", + Short: "Install PostgreSQL inside Docker on a remote host", + Long: packageio.Long(`Install Docker (if not already present) and run a + PostgreSQL container on a remote host accessed via SSH. + + The command will: + - Connect to the remote host over SSH + - Detect whether Docker is installed; install it if not + - Pull the requested PostgreSQL Docker image + - Start (or recreate) a named Postgres container with the + specified credentials, data directory, and port mapping`), + Example: formatExamples("install postgres", []packageio.Example{ + {Cmd: "--ssh-host 10.0.0.5 --ssh-key-path ~/.ssh/id_rsa", Desc: "Minimal invocation using defaults"}, + {Cmd: "--postgres-version 16-alpine", Desc: "Use a specific Postgres image tag"}, + {Cmd: "--data-dir /mnt/pgdata --publish-port 15432:5432", Desc: "Custom data directory and host port"}, + {Cmd: "--force", Desc: "Recreate the container and reinstall Docker if present"}, + {Cmd: "--docker-version 26.1.4", Desc: "Pin the Docker engine version"}, + }), + }, + Opts: InstallPostgresOpts{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") + + // Docker flags + f.StringVar(&pg.Opts.DockerVersion, "docker-version", "", "Docker Engine version to install (empty = latest)") + // f.BoolVar(&pg.Opts.ForceDocker, "force-docker", false, "Reinstall Docker even if already present") + + // Postgres flags + f.StringVar(&pg.Opts.PostgresVersion, "postgres-version", "16", "PostgreSQL Docker image tag (e.g. 16, 16.3-alpine)") + f.StringVar(&pg.Opts.ContainerName, "container-name", "postgres", "Docker container name") + f.StringVar(&pg.Opts.DataDir, "data-dir", "/var/lib/postgresql/data", "Host path for Postgres data bind-mount") + f.StringVar(&pg.Opts.PostgresPassword, "password", "changeme", "POSTGRES_PASSWORD environment variable") + f.StringVar(&pg.Opts.PostgresUser, "user", "postgres", "POSTGRES_USER environment variable") + f.StringVar(&pg.Opts.PostgresDB, "db", "postgres", "POSTGRES_DB environment variable") + f.StringVar(&pg.Opts.PublishPort, "publish-port", "5432:5432", "Port mapping host:container") + f.StringVar(&pg.Opts.RestartPolicy, "restart", "unless-stopped", "Container restart policy") + f.BoolVarP(&pg.Opts.ForcePostgres, "force", "f", false, "Stop and recreate the Postgres container if it exists") + + _ = pg.cmd.MarkFlagRequired("ssh-host") + + AddCmd(install, pg.cmd) + pg.cmd.RunE = pg.RunE +} + +// // --------------------------------------------------------------------------- +// // Orchestration +// // --------------------------------------------------------------------------- + +// // InstallPostgres is the top-level orchestrator; it mirrors InstallK0s in +// // structure so it is easy to test with mock implementations. +// func (c *InstallPostgresCmd) InstallPostgres(docker installer.DockerManager, pg installer.PostgresManager) error { +// if err := c.ensureDocker(docker); err != nil { +// return err +// } + +// if err := c.pullPostgresImage(pg); err != nil { +// return err +// } + +// if err := c.startPostgresContainer(pg); err != nil { +// return err +// } + +// log.Printf("PostgreSQL is running in container %q on %s:%d", +// c.Opts.ContainerName, c.Opts.SSHHost, c.Opts.SSHPort) +// log.Printf("Connect with: psql -h %s -p %s -U %s -d %s", +// c.Opts.SSHHost, hostPort(c.Opts.PublishPort), c.Opts.PostgresUser, c.Opts.PostgresDB) + +// return nil +// } + +// // ensureDocker checks whether Docker is installed on the remote host and +// // installs it when it is absent (or when --force-docker is given). +// func (c *InstallPostgresCmd) ensureDocker(docker installer.DockerManager) error { +// installed, err := docker.IsInstalled() +// if err != nil { +// return fmt.Errorf("failed to check Docker installation: %w", err) +// } + +// if installed && !c.Opts.ForceDocker { +// ver, _ := docker.Version() +// log.Printf("Docker already installed on remote host (version: %s), skipping", ver) +// return nil +// } + +// log.Println("Installing Docker on remote host...") +// if err := docker.Install(c.Opts.DockerVersion); err != nil { +// return fmt.Errorf("failed to install Docker: %w", err) +// } + +// ver, _ := docker.Version() +// log.Printf("Docker installed successfully (version: %s)", ver) +// return nil +// } + +// // pullPostgresImage pulls the requested Postgres image on the remote host. +// func (c *InstallPostgresCmd) pullPostgresImage(pg installer.PostgresManager) error { +// image := postgresImage(c.Opts.PostgresVersion) +// log.Printf("Pulling Docker image %s on remote host...", image) + +// if err := pg.PullImage(image); err != nil { +// return fmt.Errorf("failed to pull Postgres image %s: %w", image, err) +// } + +// log.Printf("Image %s pulled successfully", image) +// return nil +// } + +// // startPostgresContainer stops any existing container (when --force is set) +// // and starts a fresh one with the configured options. +// func (c *InstallPostgresCmd) startPostgresContainer(pg installer.PostgresManager) error { +// exists, running, err := pg.ContainerStatus(c.Opts.ContainerName) +// if err != nil { +// return fmt.Errorf("failed to inspect container %q: %w", c.Opts.ContainerName, err) +// } + +// if exists { +// if !c.Opts.ForcePostgres { +// if running { +// log.Printf("Container %q is already running; use --force to recreate", c.Opts.ContainerName) +// return nil +// } +// log.Printf("Container %q exists but is stopped; starting it...", c.Opts.ContainerName) +// if err := pg.StartContainer(c.Opts.ContainerName); err != nil { +// return fmt.Errorf("failed to start existing container %q: %w", c.Opts.ContainerName, err) +// } +// return nil +// } + +// log.Printf("Removing existing container %q (--force)...", c.Opts.ContainerName) +// if err := pg.RemoveContainer(c.Opts.ContainerName); err != nil { +// return fmt.Errorf("failed to remove container %q: %w", c.Opts.ContainerName, err) +// } +// } + +// cfg := installer.PostgresContainerConfig{ +// Image: postgresImage(c.Opts.PostgresVersion), +// ContainerName: c.Opts.ContainerName, +// DataDir: c.Opts.DataDir, +// Password: c.Opts.PostgresPassword, +// User: c.Opts.PostgresUser, +// DB: c.Opts.PostgresDB, +// PublishPort: c.Opts.PublishPort, +// RestartPolicy: c.Opts.RestartPolicy, +// } + +// log.Printf("Starting Postgres container %q...", c.Opts.ContainerName) +// if err := pg.RunContainer(cfg); err != nil { +// return fmt.Errorf("failed to start Postgres container: %w", err) +// } + +// return nil +// } + +// // --------------------------------------------------------------------------- +// // Helpers +// // --------------------------------------------------------------------------- + +// func postgresImage(tag string) string { +// return fmt.Sprintf("postgres:%s", tag) +// } + +// // hostPort extracts the host-side port from a "host:container" mapping. +// func hostPort(publish string) string { +// for i, ch := range publish { +// if ch == ':' { +// return publish[:i] +// } +// } +// return publish +// } diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 99f46d43..be258472 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -333,6 +333,11 @@ func (b *GCPBootstrapper) Bootstrap() error { return fmt.Errorf("failed to generate k0s config script: %w", err) } + err = b.stlog.Step("Install Postgres", b.InstallPostgres) + if err != nil { + return fmt.Errorf("failed to install postgres: %w", err) + } + if b.Env.InstallVersion != "" || b.Env.InstallLocal != "" { err = b.stlog.Step("Install Codesphere", b.InstallCodesphere) if err != nil { @@ -390,6 +395,7 @@ func (b *GCPBootstrapper) createTestUser() error { testuser.LogAndPersistResult(result, b.Env.OmsWorkdir) return nil } + func (b *GCPBootstrapper) ValidateInput() error { err := b.validateInstallVersion() if err != nil { @@ -1034,6 +1040,7 @@ func (b *GCPBootstrapper) runInstallCommand(packageFilename string) error { b.stlog.Logf("Installing Codesphere...") installCmd := fmt.Sprintf("oms install codesphere -c /etc/codesphere/config.yaml -k %s/age_key.txt --vault %s -p %s%s", b.Env.SecretsDir, filepath.Join(b.Env.SecretsDir, "prod.vault.yaml"), packageFilename, b.generateSkipStepsArg()) + return b.Env.Jumpbox.RunSSHCommand("root", installCmd) } diff --git a/internal/bootstrap/gcp/postgres.go b/internal/bootstrap/gcp/postgres.go new file mode 100644 index 00000000..c8e42861 --- /dev/null +++ b/internal/bootstrap/gcp/postgres.go @@ -0,0 +1,14 @@ +package gcp + +import "github.com/codesphere-cloud/oms/internal/installer/docker" + +func (b *GCPBootstrapper) InstallPostgres() error { + dockerInstaller := docker.New("root", b.Env.PostgreSQLNode) + if !dockerInstaller.IsInstalled() { + dockerInstaller.Install() + } + + // b.Env.InstallSkipSteps = append(b.Env.InstallSkipSteps, "docker") + + return nil +} diff --git a/internal/installer/docker/argocd_suite_test.go b/internal/installer/docker/argocd_suite_test.go new file mode 100644 index 00000000..e75f448f --- /dev/null +++ b/internal/installer/docker/argocd_suite_test.go @@ -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") +} diff --git a/internal/installer/docker/docker.go b/internal/installer/docker/docker.go new file mode 100644 index 00000000..29e7c2bd --- /dev/null +++ b/internal/installer/docker/docker.go @@ -0,0 +1,157 @@ +// package docker installs docker on a remote host +package docker + +import ( + "fmt" + + "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 DockerInstaller 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. + Install() error +} + +type dockerInstaller struct { + remoteUser string + remoteNode *node.Node +} + +func New(user string, node *node.Node) DockerInstaller { + return &dockerInstaller{ + remoteUser: user, + remoteNode: node, + } +} + +// IsInstalled checks whether the docker binary is available on the remote host. +func (d *dockerInstaller) IsInstalled() bool { + err := d.remoteNode.RunSSHCommand(d.remoteUser, "command -v docker") + + return err == nil +} + +// Install 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 *dockerInstaller) Install() error { + 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 *dockerInstaller) 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 *dockerInstaller) installAptPrerequisites() error { + 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 *dockerInstaller) addDockerRepository() error { + 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 := installer.Install() + 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 := installer.Install() + 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 := installer.Install() + 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 := installer.Install() + 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 := installer.Install() + 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 := installer.Install() + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) From a70eef8f609724854e0683db40bf027f5b868407 Mon Sep 17 00:00:00 2001 From: joka134 <27293650+joka134@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:37:31 +0000 Subject: [PATCH 02/12] chore(docs): Auto-update docs and licenses Signed-off-by: joka134 <27293650+joka134@users.noreply.github.com> --- internal/bootstrap/gcp/postgres.go | 3 + internal/installer/docker/docker.go | 3 + internal/installer/docker/mocks.go | 124 ++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 internal/installer/docker/mocks.go diff --git a/internal/bootstrap/gcp/postgres.go b/internal/bootstrap/gcp/postgres.go index c8e42861..264e92d2 100644 --- a/internal/bootstrap/gcp/postgres.go +++ b/internal/bootstrap/gcp/postgres.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package gcp import "github.com/codesphere-cloud/oms/internal/installer/docker" diff --git a/internal/installer/docker/docker.go b/internal/installer/docker/docker.go index 29e7c2bd..9b991648 100644 --- a/internal/installer/docker/docker.go +++ b/internal/installer/docker/docker.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + // package docker installs docker on a remote host package docker 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 9e1edacb06c4d20fef767570d4b304fb4e806b9c Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Wed, 1 Jul 2026 19:23:48 +0200 Subject: [PATCH 03/12] feat: install docker using apt --- cli/cmd/install.go | 1 + cli/cmd/install_postgres.go | 245 ------------------ internal/bootstrap/gcp/gcp.go | 7 - internal/bootstrap/gcp/postgres.go | 17 -- internal/installer/docker/docker.go | 31 ++- ...ocd_suite_test.go => docker_suite_test.go} | 0 internal/installer/docker/docker_test.go | 24 +- 7 files changed, 31 insertions(+), 294 deletions(-) delete mode 100644 cli/cmd/install_postgres.go delete mode 100644 internal/bootstrap/gcp/postgres.go rename internal/installer/docker/{argocd_suite_test.go => docker_suite_test.go} (100%) diff --git a/cli/cmd/install.go b/cli/cmd/install.go index 7b5d946c..53cdc67a 100644 --- a/cli/cmd/install.go +++ b/cli/cmd/install.go @@ -26,4 +26,5 @@ func AddInstallCmd(rootCmd *cobra.Command, opts *GlobalOptions) { AddInstallCodesphereCmd(install.cmd, opts) AddInstallK0sCmd(install.cmd, opts) AddInstallOpenBaoCmd(install.cmd, opts) + AddInstallDockerCmd(install.cmd, opts) } diff --git a/cli/cmd/install_postgres.go b/cli/cmd/install_postgres.go deleted file mode 100644 index 0c52e54f..00000000 --- a/cli/cmd/install_postgres.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) Codesphere Inc. -// SPDX-License-Identifier: Apache-2.0 - -package cmd - -import ( - packageio "github.com/codesphere-cloud/cs-go/pkg/io" - "github.com/codesphere-cloud/oms/internal/env" - "github.com/codesphere-cloud/oms/internal/util" - "github.com/spf13/cobra" -) - -// InstallPostgresCmd represents the postgres install command. -type InstallPostgresCmd struct { - cmd *cobra.Command - Opts InstallPostgresOpts - Env env.Env - FileWriter util.FileIO -} - -// InstallPostgresOpts holds all CLI flags for the postgres sub-command. -type InstallPostgresOpts struct { - *GlobalOptions - - // SSH / remote host - SSHKeyPath string - SSHUser string - SSHHost string - SSHPort int - - // Docker - DockerVersion string // empty → latest - - // Postgres container - PostgresVersion string // Docker image tag, e.g. "16" or "16.3-alpine" - ContainerName string - DataDir string // host-side bind-mount for pg data - PostgresPassword string - PostgresUser string - PostgresDB string - PublishPort string // host:container, e.g. "5432:5432" - RestartPolicy string // always | unless-stopped | on-failure | no - ForcePostgres bool // stop & recreate the container if it already exists -} - -func (c *InstallPostgresCmd) RunE(_ *cobra.Command, _ []string) error { - // // ssh := installer.NewSSHRunner( - // // c.Opts.SSHHost, - // // c.Opts.SSHPort, - // // c.Opts.SSHUser, - // // c.Opts.SSHKeyPath, - // // ) - // // docker := installer.NewDockerManager(ssh) - // // pg := installer.NewPostgresManager(ssh) - - // // return c.InstallPostgres(docker, pg) - return nil -} - -// AddInstallPostgresCmd registers the "install postgres" sub-command under -// the provided parent install command, following the same pattern as -// AddInstallK0sCmd. -func AddInstallPostgresCmd(install *cobra.Command, opts *GlobalOptions) { - pg := InstallPostgresCmd{ - cmd: &cobra.Command{ - Use: "postgres", - Short: "Install PostgreSQL inside Docker on a remote host", - Long: packageio.Long(`Install Docker (if not already present) and run a - PostgreSQL container on a remote host accessed via SSH. - - The command will: - - Connect to the remote host over SSH - - Detect whether Docker is installed; install it if not - - Pull the requested PostgreSQL Docker image - - Start (or recreate) a named Postgres container with the - specified credentials, data directory, and port mapping`), - Example: formatExamples("install postgres", []packageio.Example{ - {Cmd: "--ssh-host 10.0.0.5 --ssh-key-path ~/.ssh/id_rsa", Desc: "Minimal invocation using defaults"}, - {Cmd: "--postgres-version 16-alpine", Desc: "Use a specific Postgres image tag"}, - {Cmd: "--data-dir /mnt/pgdata --publish-port 15432:5432", Desc: "Custom data directory and host port"}, - {Cmd: "--force", Desc: "Recreate the container and reinstall Docker if present"}, - {Cmd: "--docker-version 26.1.4", Desc: "Pin the Docker engine version"}, - }), - }, - Opts: InstallPostgresOpts{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") - - // Docker flags - f.StringVar(&pg.Opts.DockerVersion, "docker-version", "", "Docker Engine version to install (empty = latest)") - // f.BoolVar(&pg.Opts.ForceDocker, "force-docker", false, "Reinstall Docker even if already present") - - // Postgres flags - f.StringVar(&pg.Opts.PostgresVersion, "postgres-version", "16", "PostgreSQL Docker image tag (e.g. 16, 16.3-alpine)") - f.StringVar(&pg.Opts.ContainerName, "container-name", "postgres", "Docker container name") - f.StringVar(&pg.Opts.DataDir, "data-dir", "/var/lib/postgresql/data", "Host path for Postgres data bind-mount") - f.StringVar(&pg.Opts.PostgresPassword, "password", "changeme", "POSTGRES_PASSWORD environment variable") - f.StringVar(&pg.Opts.PostgresUser, "user", "postgres", "POSTGRES_USER environment variable") - f.StringVar(&pg.Opts.PostgresDB, "db", "postgres", "POSTGRES_DB environment variable") - f.StringVar(&pg.Opts.PublishPort, "publish-port", "5432:5432", "Port mapping host:container") - f.StringVar(&pg.Opts.RestartPolicy, "restart", "unless-stopped", "Container restart policy") - f.BoolVarP(&pg.Opts.ForcePostgres, "force", "f", false, "Stop and recreate the Postgres container if it exists") - - _ = pg.cmd.MarkFlagRequired("ssh-host") - - AddCmd(install, pg.cmd) - pg.cmd.RunE = pg.RunE -} - -// // --------------------------------------------------------------------------- -// // Orchestration -// // --------------------------------------------------------------------------- - -// // InstallPostgres is the top-level orchestrator; it mirrors InstallK0s in -// // structure so it is easy to test with mock implementations. -// func (c *InstallPostgresCmd) InstallPostgres(docker installer.DockerManager, pg installer.PostgresManager) error { -// if err := c.ensureDocker(docker); err != nil { -// return err -// } - -// if err := c.pullPostgresImage(pg); err != nil { -// return err -// } - -// if err := c.startPostgresContainer(pg); err != nil { -// return err -// } - -// log.Printf("PostgreSQL is running in container %q on %s:%d", -// c.Opts.ContainerName, c.Opts.SSHHost, c.Opts.SSHPort) -// log.Printf("Connect with: psql -h %s -p %s -U %s -d %s", -// c.Opts.SSHHost, hostPort(c.Opts.PublishPort), c.Opts.PostgresUser, c.Opts.PostgresDB) - -// return nil -// } - -// // ensureDocker checks whether Docker is installed on the remote host and -// // installs it when it is absent (or when --force-docker is given). -// func (c *InstallPostgresCmd) ensureDocker(docker installer.DockerManager) error { -// installed, err := docker.IsInstalled() -// if err != nil { -// return fmt.Errorf("failed to check Docker installation: %w", err) -// } - -// if installed && !c.Opts.ForceDocker { -// ver, _ := docker.Version() -// log.Printf("Docker already installed on remote host (version: %s), skipping", ver) -// return nil -// } - -// log.Println("Installing Docker on remote host...") -// if err := docker.Install(c.Opts.DockerVersion); err != nil { -// return fmt.Errorf("failed to install Docker: %w", err) -// } - -// ver, _ := docker.Version() -// log.Printf("Docker installed successfully (version: %s)", ver) -// return nil -// } - -// // pullPostgresImage pulls the requested Postgres image on the remote host. -// func (c *InstallPostgresCmd) pullPostgresImage(pg installer.PostgresManager) error { -// image := postgresImage(c.Opts.PostgresVersion) -// log.Printf("Pulling Docker image %s on remote host...", image) - -// if err := pg.PullImage(image); err != nil { -// return fmt.Errorf("failed to pull Postgres image %s: %w", image, err) -// } - -// log.Printf("Image %s pulled successfully", image) -// return nil -// } - -// // startPostgresContainer stops any existing container (when --force is set) -// // and starts a fresh one with the configured options. -// func (c *InstallPostgresCmd) startPostgresContainer(pg installer.PostgresManager) error { -// exists, running, err := pg.ContainerStatus(c.Opts.ContainerName) -// if err != nil { -// return fmt.Errorf("failed to inspect container %q: %w", c.Opts.ContainerName, err) -// } - -// if exists { -// if !c.Opts.ForcePostgres { -// if running { -// log.Printf("Container %q is already running; use --force to recreate", c.Opts.ContainerName) -// return nil -// } -// log.Printf("Container %q exists but is stopped; starting it...", c.Opts.ContainerName) -// if err := pg.StartContainer(c.Opts.ContainerName); err != nil { -// return fmt.Errorf("failed to start existing container %q: %w", c.Opts.ContainerName, err) -// } -// return nil -// } - -// log.Printf("Removing existing container %q (--force)...", c.Opts.ContainerName) -// if err := pg.RemoveContainer(c.Opts.ContainerName); err != nil { -// return fmt.Errorf("failed to remove container %q: %w", c.Opts.ContainerName, err) -// } -// } - -// cfg := installer.PostgresContainerConfig{ -// Image: postgresImage(c.Opts.PostgresVersion), -// ContainerName: c.Opts.ContainerName, -// DataDir: c.Opts.DataDir, -// Password: c.Opts.PostgresPassword, -// User: c.Opts.PostgresUser, -// DB: c.Opts.PostgresDB, -// PublishPort: c.Opts.PublishPort, -// RestartPolicy: c.Opts.RestartPolicy, -// } - -// log.Printf("Starting Postgres container %q...", c.Opts.ContainerName) -// if err := pg.RunContainer(cfg); err != nil { -// return fmt.Errorf("failed to start Postgres container: %w", err) -// } - -// return nil -// } - -// // --------------------------------------------------------------------------- -// // Helpers -// // --------------------------------------------------------------------------- - -// func postgresImage(tag string) string { -// return fmt.Sprintf("postgres:%s", tag) -// } - -// // hostPort extracts the host-side port from a "host:container" mapping. -// func hostPort(publish string) string { -// for i, ch := range publish { -// if ch == ':' { -// return publish[:i] -// } -// } -// return publish -// } diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index be258472..99f46d43 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -333,11 +333,6 @@ func (b *GCPBootstrapper) Bootstrap() error { return fmt.Errorf("failed to generate k0s config script: %w", err) } - err = b.stlog.Step("Install Postgres", b.InstallPostgres) - if err != nil { - return fmt.Errorf("failed to install postgres: %w", err) - } - if b.Env.InstallVersion != "" || b.Env.InstallLocal != "" { err = b.stlog.Step("Install Codesphere", b.InstallCodesphere) if err != nil { @@ -395,7 +390,6 @@ func (b *GCPBootstrapper) createTestUser() error { testuser.LogAndPersistResult(result, b.Env.OmsWorkdir) return nil } - func (b *GCPBootstrapper) ValidateInput() error { err := b.validateInstallVersion() if err != nil { @@ -1040,7 +1034,6 @@ func (b *GCPBootstrapper) runInstallCommand(packageFilename string) error { b.stlog.Logf("Installing Codesphere...") installCmd := fmt.Sprintf("oms install codesphere -c /etc/codesphere/config.yaml -k %s/age_key.txt --vault %s -p %s%s", b.Env.SecretsDir, filepath.Join(b.Env.SecretsDir, "prod.vault.yaml"), packageFilename, b.generateSkipStepsArg()) - return b.Env.Jumpbox.RunSSHCommand("root", installCmd) } diff --git a/internal/bootstrap/gcp/postgres.go b/internal/bootstrap/gcp/postgres.go deleted file mode 100644 index 264e92d2..00000000 --- a/internal/bootstrap/gcp/postgres.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Codesphere Inc. -// SPDX-License-Identifier: Apache-2.0 - -package gcp - -import "github.com/codesphere-cloud/oms/internal/installer/docker" - -func (b *GCPBootstrapper) InstallPostgres() error { - dockerInstaller := docker.New("root", b.Env.PostgreSQLNode) - if !dockerInstaller.IsInstalled() { - dockerInstaller.Install() - } - - // b.Env.InstallSkipSteps = append(b.Env.InstallSkipSteps, "docker") - - return nil -} diff --git a/internal/installer/docker/docker.go b/internal/installer/docker/docker.go index 9b991648..c066ccde 100644 --- a/internal/installer/docker/docker.go +++ b/internal/installer/docker/docker.go @@ -6,6 +6,7 @@ package docker import ( "fmt" + "log" "github.com/codesphere-cloud/oms/internal/installer/node" ) @@ -14,36 +15,38 @@ import ( // The interface makes the command easy to unit-test with mocks. // //mockery:generate: true -type DockerInstaller interface { +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. - Install() error + InstallWithApt() error } -type dockerInstaller struct { +type dockerManager struct { remoteUser string remoteNode *node.Node } -func New(user string, node *node.Node) DockerInstaller { - return &dockerInstaller{ +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 *dockerInstaller) IsInstalled() bool { +func (d *dockerManager) IsInstalled() bool { err := d.remoteNode.RunSSHCommand(d.remoteUser, "command -v docker") return err == nil } -// Install installs Docker Engine on the remote host using Docker's official apt repository +// 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 *dockerInstaller) Install() error { +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") } @@ -70,7 +73,7 @@ func (d *dockerInstaller) Install() error { // 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 *dockerInstaller) removeConflictingPackages() error { +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) @@ -81,7 +84,8 @@ func (d *dockerInstaller) removeConflictingPackages() error { // installAptPrerequisites ensures ca-certificates and curl are present; // these are required before the Docker GPG key and repo can be added. -func (d *dockerInstaller) installAptPrerequisites() error { +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", @@ -96,7 +100,8 @@ func (d *dockerInstaller) installAptPrerequisites() error { // addDockerRepository adds Docker's official GPG key and apt repository, // exactly as described in the official Ubuntu install docs. -func (d *dockerInstaller) addDockerRepository() error { +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 && " + @@ -128,7 +133,7 @@ func (d *dockerInstaller) addDockerRepository() error { } // installDockerPackages installs docker and related packages using apt-get. -func (d *dockerInstaller) installDockerPackages() error { +func (d *dockerManager) installDockerPackages() error { cmd := fmt.Sprintf( "apt-get install -y -qq " + "docker-ce " + @@ -147,7 +152,7 @@ func (d *dockerInstaller) installDockerPackages() error { // startDaemon starts and enables the Docker daemon via systemctl so it // survives reboots. -func (d *dockerInstaller) startDaemon() error { +func (d *dockerManager) startDaemon() error { for _, cmd := range []string{ "systemctl start docker", "systemctl enable docker", diff --git a/internal/installer/docker/argocd_suite_test.go b/internal/installer/docker/docker_suite_test.go similarity index 100% rename from internal/installer/docker/argocd_suite_test.go rename to internal/installer/docker/docker_suite_test.go diff --git a/internal/installer/docker/docker_test.go b/internal/installer/docker/docker_test.go index d725d85d..7e1d7ca2 100644 --- a/internal/installer/docker/docker_test.go +++ b/internal/installer/docker/docker_test.go @@ -33,7 +33,7 @@ func fakeNode(name string, commandRunner node.NodeClient) *node.Node { var _ = Describe("Docker", func() { var ( - installer docker.DockerInstaller + manager docker.DockerManager nodeClient *node.MockNodeClient remoteNode *node.Node ) @@ -42,12 +42,12 @@ var _ = Describe("Docker", func() { nodeClient = node.NewMockNodeClient(GinkgoT()) remoteNode = fakeNode("docker-host", nodeClient) - installer = docker.New("ubuntu", remoteNode) + manager = docker.New("ubuntu", remoteNode) }) Describe("New", func() { It("creates a new DockerInstaller", func() { - Expect(installer).ToNot(BeNil()) + Expect(manager).ToNot(BeNil()) }) }) @@ -55,23 +55,23 @@ var _ = Describe("Docker", func() { It("returns true when the docker binary is available", func() { nodeClient.EXPECT().RunCommand(remoteNode, "ubuntu", "command -v docker").Return(nil) - Expect(installer.IsInstalled()).To(BeTrue()) + Expect(manager.IsInstalled()).To(BeTrue()) }) It("returns false when the docker binary is not available", func() { nodeClient.EXPECT().RunCommand(remoteNode, "ubuntu", "command -v docker").Return(errors.New("not found")) - Expect(installer.IsInstalled()).To(BeFalse()) + Expect(manager.IsInstalled()).To(BeFalse()) }) }) - Describe("Install", func() { + Describe("InstallDocker", func() { It("fails when removing conflicting 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(errors.New("ssh error")) - err := installer.Install() + err := manager.InstallWithApt() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to remove conflicting docker packages")) }) @@ -84,7 +84,7 @@ var _ = Describe("Docker", func() { RunCommand(remoteNode, "ubuntu", "apt-get update -qq"). Return(errors.New("apt error")) - err := installer.Install() + err := manager.InstallWithApt() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to install docker apt prequisites")) }) @@ -104,7 +104,7 @@ var _ = Describe("Docker", func() { Return(errors.New("repo error")). Once() - err := installer.Install() + err := manager.InstallWithApt() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to add docker apt repository")) }) @@ -128,7 +128,7 @@ var _ = Describe("Docker", func() { 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 := installer.Install() + err := manager.InstallWithApt() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to install docker packages")) }) @@ -155,7 +155,7 @@ var _ = Describe("Docker", func() { RunCommand(remoteNode, "ubuntu", "systemctl start docker"). Return(errors.New("daemon error")) - err := installer.Install() + err := manager.InstallWithApt() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to start docker daemon")) }) @@ -185,7 +185,7 @@ var _ = Describe("Docker", func() { RunCommand(remoteNode, "ubuntu", "systemctl enable docker"). Return(nil) - err := installer.Install() + err := manager.InstallWithApt() Expect(err).ToNot(HaveOccurred()) }) }) From f02ec10cd3e092ae5f613e42efa57387b168ed20 Mon Sep 17 00:00:00 2001 From: Tim Schrodi Date: Tue, 30 Jun 2026 14:18:49 +0200 Subject: [PATCH 04/12] feat: remove unnecessary verbose extraction logs (#547) Logging every extracted file leads to a verbose output without value --- internal/util/tar.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/util/tar.go b/internal/util/tar.go index 11154f27..e60bae77 100644 --- a/internal/util/tar.go +++ b/internal/util/tar.go @@ -43,7 +43,6 @@ func openTar(filename string, fileIo FileIO) (*tar.Reader, error) { } bufferedFile := bufio.NewReader(file) - log.Println("Reading TAR archive contents...") tr := tar.NewReader(bufferedFile) return tr, nil } @@ -62,7 +61,6 @@ func openTarGz(filename string, fileIo FileIO) (*tar.Reader, error) { return nil, fmt.Errorf("failed to create gzip reader: %w", err) } - log.Println("Reading TAR archive contents...") tr := tar.NewReader(gzr) return tr, nil } @@ -71,13 +69,11 @@ func openTarGz(filename string, fileIo FileIO) (*tar.Reader, error) { func extractEntry(header *tar.Header, targetPath string, fileIo FileIO, tr *tar.Reader) error { switch header.Typeflag { case tar.TypeDir: - log.Printf("Creating directory: %s", targetPath) if err := fileIo.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil { return fmt.Errorf("failed to create directory %s: %w", targetPath, err) } case tar.TypeReg: - log.Printf("Extracting file: %s", targetPath) if err := fileIo.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { return fmt.Errorf("failed to create directory %s: %w", targetPath, err) } From 486ea878fc40ee11fa953b3fc1c694fe91c4ffea Mon Sep 17 00:00:00 2001 From: Tim Schrodi Date: Tue, 30 Jun 2026 14:19:03 +0200 Subject: [PATCH 05/12] fix(install): resolve vault from config secrets dir (#546) Allow dependency installation to load prod.vault.yaml from config.secrets.baseDir when --vault is not provided. --- cli/cmd/install_codesphere_dependencies.go | 21 +++++- .../install_codesphere_dependencies_test.go | 74 +++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 cli/cmd/install_codesphere_dependencies_test.go diff --git a/cli/cmd/install_codesphere_dependencies.go b/cli/cmd/install_codesphere_dependencies.go index 3ac87d52..b49f2a1b 100644 --- a/cli/cmd/install_codesphere_dependencies.go +++ b/cli/cmd/install_codesphere_dependencies.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "os" + "path/filepath" "runtime" "strings" @@ -118,10 +119,14 @@ type argoCDAndAppsInstall struct { } func (i *argoCDAndAppsInstall) loadVaultData() error { - var err error - i.vault, err = installer.LoadVaultData(i.opts.Vault, i.opts.PrivKey) + vaultPath, err := i.resolveVaultPath() + if err != nil { + return err + } + + i.vault, err = installer.LoadVaultData(vaultPath, i.opts.PrivKey) if err != nil { - return fmt.Errorf("failed to load vault: %w", err) + return fmt.Errorf("failed to load vault %s: %w", vaultPath, err) } if s := i.vault.GetSecret(files.SecretRegistryPassword); s != nil && s.Fields != nil { i.ociPassword = s.Fields.Password @@ -145,6 +150,16 @@ func (i *argoCDAndAppsInstall) loadVaultData() error { return nil } +func (i *argoCDAndAppsInstall) resolveVaultPath() (string, error) { + if strings.TrimSpace(i.opts.Vault) != "" { + return i.opts.Vault, nil + } + if strings.TrimSpace(i.config.Secrets.BaseDir) == "" { + return "", fmt.Errorf("vault path is not set and config.yaml secrets.baseDir is empty") + } + return filepath.Join(i.config.Secrets.BaseDir, "prod.vault.yaml"), nil +} + func (i *argoCDAndAppsInstall) installArgoCD() error { i.ociRegistryURL = i.opts.ArgoCDRegistryURL if i.ociRegistryURL == "" && i.config.Registry != nil { diff --git a/cli/cmd/install_codesphere_dependencies_test.go b/cli/cmd/install_codesphere_dependencies_test.go new file mode 100644 index 00000000..b0bf8e4d --- /dev/null +++ b/cli/cmd/install_codesphere_dependencies_test.go @@ -0,0 +1,74 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "os" + "path/filepath" + + "github.com/codesphere-cloud/oms/internal/installer/files" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("argoCDAndAppsInstall.loadVaultData", func() { + It("falls back to config secrets.baseDir when --vault is not set", func() { + tmpDir := GinkgoT().TempDir() + secretsDir := filepath.Join(tmpDir, "secrets") + Expect(os.MkdirAll(secretsDir, 0700)).To(Succeed()) + + vault := &files.InstallVault{ + Secrets: []files.SecretEntry{ + { + Name: files.SecretRegistryPassword, + Fields: &files.SecretFields{ + Password: "registry-password", + }, + }, + { + Name: files.SecretKubeConfig, + File: &files.SecretFile{ + Name: "kubeconfig", + Content: `apiVersion: v1 +kind: Config +clusters: +- name: test + cluster: + server: https://127.0.0.1:6443 +contexts: +- name: test + context: + cluster: test + user: test +current-context: test +users: +- name: test + user: + token: test-token +`, + }, + }, + }, + } + vaultYAML, err := vault.Marshal() + Expect(err).ToNot(HaveOccurred()) + Expect(os.WriteFile(filepath.Join(secretsDir, "prod.vault.yaml"), vaultYAML, 0600)).To(Succeed()) + + install := &argoCDAndAppsInstall{ + opts: &InstallCodesphereOpts{}, + config: files.RootConfig{ + Secrets: files.SecretsConfig{ + BaseDir: secretsDir, + }, + }, + } + + err = install.loadVaultData() + Expect(err).ToNot(HaveOccurred()) + Expect(install.vault).ToNot(BeNil()) + Expect(install.ociPassword).To(Equal("registry-password")) + Expect(install.kubeConfig).ToNot(BeNil()) + Expect(install.kubeClient).ToNot(BeNil()) + }) +}) From 975635eeff37b918d91a86ea80afd258b9d5909d Mon Sep 17 00:00:00 2001 From: Manuel Dewald Date: Tue, 30 Jun 2026 15:17:22 +0200 Subject: [PATCH 06/12] update(download): Set installer-lite.tar.gz as default (#548) * Also update help output to suggest lite packages --------- Signed-off-by: NautiluX <2600004+NautiluX@users.noreply.github.com> Co-authored-by: NautiluX <2600004+NautiluX@users.noreply.github.com> --- cli/cmd/download_package.go | 2 +- cli/cmd/install_codesphere.go | 2 +- cli/cmd/install_codesphere_dependencies.go | 6 +++--- cli/cmd/install_codesphere_infra.go | 2 +- cli/cmd/install_codesphere_platform.go | 2 +- cli/cmd/install_codesphere_test.go | 2 +- cli/cmd/install_k0s.go | 4 ++-- docs/oms_download_package.md | 2 +- docs/oms_install_codesphere.md | 2 +- docs/oms_install_codesphere_dependencies.md | 8 ++++---- docs/oms_install_codesphere_infra.md | 4 ++-- docs/oms_install_codesphere_platform.md | 4 ++-- docs/oms_install_k0s.md | 4 ++-- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/cli/cmd/download_package.go b/cli/cmd/download_package.go index 4d0ebf38..38c33695 100644 --- a/cli/cmd/download_package.go +++ b/cli/cmd/download_package.go @@ -87,7 +87,7 @@ func AddDownloadPackageCmd(download *cobra.Command, opts *GlobalOptions) { pkg.cmd.Flags().StringVarP(&pkg.Opts.Version, "version", "V", "", "Codesphere version to download") pkg.cmd.Flags().StringVarP(&pkg.Opts.Hash, "hash", "H", "", "Hash of the version to download if multiple builds exist for the same version") - pkg.cmd.Flags().StringVarP(&pkg.Opts.Filename, "file", "f", "installer.tar.gz", "Specify artifact to download") + pkg.cmd.Flags().StringVarP(&pkg.Opts.Filename, "file", "f", "installer-lite.tar.gz", "Specify artifact to download") pkg.cmd.Flags().BoolVarP(&pkg.Opts.Quiet, "quiet", "q", false, "Suppress progress output during download") AddCmd(download, pkg.cmd) diff --git a/cli/cmd/install_codesphere.go b/cli/cmd/install_codesphere.go index 14bcea07..0263586e 100644 --- a/cli/cmd/install_codesphere.go +++ b/cli/cmd/install_codesphere.go @@ -116,7 +116,7 @@ func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) { Opts: &InstallCodesphereOpts{GlobalOptions: opts}, Env: env.NewEnv(), } - codesphere.cmd.PersistentFlags().StringVarP(&codesphere.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load binaries, installer etc. from") + codesphere.cmd.PersistentFlags().StringVarP(&codesphere.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer-lite.tar.gz) to load binaries, installer etc. from") codesphere.cmd.PersistentFlags().BoolVarP(&codesphere.Opts.Force, "force", "f", false, "Enforce package extraction") codesphere.cmd.PersistentFlags().StringArrayVarP(&codesphere.Opts.Configs, "config", "c", nil, "Path to a Codesphere Private Cloud configuration file (yaml). Can be specified multiple times and merged in order") codesphere.cmd.PersistentFlags().StringVar(&codesphere.Opts.Vault, "vault", "", "Path to the SOPS-encrypted prod.vault.yaml file used for config templating") diff --git a/cli/cmd/install_codesphere_dependencies.go b/cli/cmd/install_codesphere_dependencies.go index b49f2a1b..bed99794 100644 --- a/cli/cmd/install_codesphere_dependencies.go +++ b/cli/cmd/install_codesphere_dependencies.go @@ -239,15 +239,15 @@ func AddInstallCodesphereDepenciesCmd(codesphere *cobra.Command, opts *InstallCo Pass --skip-steps argocd or add argocd to operations.skip to skip the ArgoCD pre-step.`), Example: formatExamples("install codesphere dependencies", []io.Example{ { - Cmd: "-p codesphere-v1.2.3-installer.tar.gz -k -c config.yaml", + Cmd: "-p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml", Desc: "Install cluster dependencies (including ArgoCD)", }, { - Cmd: "-p codesphere-v1.2.3-installer.tar.gz -k -c config.yaml -s argocd", + Cmd: "-p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml -s argocd", Desc: "Install cluster dependencies without the ArgoCD pre-step", }, { - Cmd: "-p codesphere-v1.2.3-installer.tar.gz -k -c config.yaml --pc-apps-values base.yaml --pc-apps-values dc-overlay.yaml", + Cmd: "-p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml --pc-apps-values base.yaml --pc-apps-values dc-overlay.yaml", Desc: "Install cluster dependencies with custom pc-apps values files", }, }), diff --git a/cli/cmd/install_codesphere_infra.go b/cli/cmd/install_codesphere_infra.go index 4b8c7f78..2fd6c83a 100644 --- a/cli/cmd/install_codesphere_infra.go +++ b/cli/cmd/install_codesphere_infra.go @@ -63,7 +63,7 @@ func AddInstallCodesphereInfraCmd(codesphere *cobra.Command, opts *InstallCodesp Runs steps: copy-dependencies, extract-dependencies, load-container-images, sops, docker, postgres, ceph, kubernetes.`), Example: formatExamples("install codesphere infra", []io.Example{ { - Cmd: "-p codesphere-v1.2.3-installer.tar.gz -k -c config.yaml", + Cmd: "-p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml", Desc: "Install infrastructure components only", }, { diff --git a/cli/cmd/install_codesphere_platform.go b/cli/cmd/install_codesphere_platform.go index 578bd805..7a259c71 100644 --- a/cli/cmd/install_codesphere_platform.go +++ b/cli/cmd/install_codesphere_platform.go @@ -65,7 +65,7 @@ func AddInstallCodespherePlatformCmd(codesphere *cobra.Command, opts *InstallCod Requires the infrastructure and dependencies phases to have completed successfully.`), Example: formatExamples("install codesphere platform", []io.Example{ { - Cmd: "-p codesphere-v1.2.3-installer.tar.gz -k -c config.yaml", + Cmd: "-p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml", Desc: "Install Codesphere platform only", }, }), diff --git a/cli/cmd/install_codesphere_test.go b/cli/cmd/install_codesphere_test.go index c3df5253..332b20b6 100644 --- a/cli/cmd/install_codesphere_test.go +++ b/cli/cmd/install_codesphere_test.go @@ -28,7 +28,7 @@ var _ = Describe("InstallCodesphereCmd", func() { globalOpts = &cmd.GlobalOptions{} opts = &cmd.InstallCodesphereOpts{ GlobalOptions: globalOpts, - Package: "codesphere-v1.66.0-installer.tar.gz", + Package: "codesphere-v1.66.0-installer-lite.tar.gz", Force: false, } c = cmd.InstallCodesphereCmd{ diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index c5f3f040..4302c896 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -65,7 +65,7 @@ func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { {Cmd: "--install-config ", Desc: "Path to Codesphere install-config file to generate k0s config from"}, {Cmd: "--version ", Desc: "Version of k0s to install (e.g., v1.30.0+k0s.0)"}, {Cmd: "--k0sctl-version ", Desc: "Version of k0sctl to use (e.g., v0.17.4)"}, - {Cmd: "--package ", Desc: "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load k0s from"}, + {Cmd: "--package ", Desc: "Package file (e.g. codesphere-v1.2.3-installer-lite.tar.gz) to load k0s from"}, {Cmd: "--ssh-key-path ", Desc: "SSH private key path for remote installation"}, {Cmd: "--force", Desc: "Force new download and installation"}, {Cmd: "--no-download", Desc: "Skip downloading k0s binary (expects it to be on remote nodes)"}, @@ -77,7 +77,7 @@ func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { } k0s.cmd.Flags().StringVarP(&k0s.Opts.Version, "version", "v", "", "Version of k0s to install") k0s.cmd.Flags().StringVar(&k0s.Opts.K0sctlVersion, "k0sctl-version", "", "Version of k0sctl to use") - k0s.cmd.Flags().StringVarP(&k0s.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load k0s from") + k0s.cmd.Flags().StringVarP(&k0s.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer-lite.tar.gz) to load k0s from") k0s.cmd.Flags().StringVar(&k0s.Opts.InstallConfig, "install-config", "", "Path to Codesphere install-config file (required)") k0s.cmd.Flags().StringVar(&k0s.Opts.SSHKeyPath, "ssh-key-path", "", "SSH private key path for remote installation") k0s.cmd.Flags().BoolVarP(&k0s.Opts.Force, "force", "f", false, "Force new download and installation") diff --git a/docs/oms_download_package.md b/docs/oms_download_package.md index 6a831ea4..a9d12498 100644 --- a/docs/oms_download_package.md +++ b/docs/oms_download_package.md @@ -28,7 +28,7 @@ $ oms download package --version codesphere-v1.55.0 --file installer-lite.tar.gz ### Options ``` - -f, --file string Specify artifact to download (default "installer.tar.gz") + -f, --file string Specify artifact to download (default "installer-lite.tar.gz") -H, --hash string Hash of the version to download if multiple builds exist for the same version -h, --help help for package -q, --quiet Suppress progress output during download diff --git a/docs/oms_install_codesphere.md b/docs/oms_install_codesphere.md index c52728b7..dcf6c214 100644 --- a/docs/oms_install_codesphere.md +++ b/docs/oms_install_codesphere.md @@ -36,7 +36,7 @@ $ oms install codesphere -p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml +$ oms install codesphere dependencies -p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml # Install cluster dependencies without the ArgoCD pre-step -$ oms install codesphere dependencies -p codesphere-v1.2.3-installer.tar.gz -k -c config.yaml -s argocd +$ oms install codesphere dependencies -p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml -s argocd # Install cluster dependencies with custom pc-apps values files -$ oms install codesphere dependencies -p codesphere-v1.2.3-installer.tar.gz -k -c config.yaml --pc-apps-values base.yaml --pc-apps-values dc-overlay.yaml +$ oms install codesphere dependencies -p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml --pc-apps-values base.yaml --pc-apps-values dc-overlay.yaml ``` @@ -45,7 +45,7 @@ $ oms install codesphere dependencies -p codesphere-v1.2.3-installer.tar.gz -k < -c, --config stringArray Path to a Codesphere Private Cloud configuration file (yaml). Can be specified multiple times and merged in order --direct-connection Use direct connection for installation, requires having access to the cluster nodes from your machine -f, --force Enforce package extraction - -p, --package string Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load binaries, installer etc. from + -p, --package string Package file (e.g. codesphere-v1.2.3-installer-lite.tar.gz) to load binaries, installer etc. from --pc-apps-values stringArray pc-apps values YAML file (can be specified multiple times) -k, --priv-key string Path to the private key to encrypt/decrypt secrets -s, --skip-steps strings Steps to be skipped. E.g. copy-dependencies, extract-dependencies, load-container-images, ceph, postgres, kubernetes, docker, argocd diff --git a/docs/oms_install_codesphere_infra.md b/docs/oms_install_codesphere_infra.md index 7bb69ef3..cd174c53 100644 --- a/docs/oms_install_codesphere_infra.md +++ b/docs/oms_install_codesphere_infra.md @@ -15,7 +15,7 @@ oms install codesphere infra [flags] ``` # Install infrastructure components only -$ oms install codesphere infra -p codesphere-v1.2.3-installer.tar.gz -k -c config.yaml +$ oms install codesphere infra -p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml # Skip loading container images when using a lite package $ oms install codesphere infra -p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml -s load-container-images @@ -40,7 +40,7 @@ $ oms install codesphere infra -p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml +$ oms install codesphere platform -p codesphere-v1.2.3-installer-lite.tar.gz -k -c config.yaml ``` @@ -38,7 +38,7 @@ $ oms install codesphere platform -p codesphere-v1.2.3-installer.tar.gz -k # Version of k0sctl to use (e.g., v0.17.4) $ oms install k0s --k0sctl-version -# Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load k0s from +# Package file (e.g. codesphere-v1.2.3-installer-lite.tar.gz) to load k0s from $ oms install k0s --package # SSH private key path for remote installation @@ -50,7 +50,7 @@ $ oms install k0s --no-download --install-config string Path to Codesphere install-config file (required) --k0sctl-version string Version of k0sctl to use --no-download Skip downloading k0s binary - -p, --package string Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load k0s from + -p, --package string Package file (e.g. codesphere-v1.2.3-installer-lite.tar.gz) to load k0s from --ssh-key-path string SSH private key path for remote installation --vault string Path to prod.vault.yaml to save the kubeconfig into (optional) --vault-priv-key string Path to the age private key to decrypt the vault (optional, for SOPS-encrypted vaults) From 5db08405f5d496c252389315fc6f7290aec8de30 Mon Sep 17 00:00:00 2001 From: Codesphere Bot <117686659+CodesphereBot@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:15:49 +0200 Subject: [PATCH 07/12] update(deps): update module google.golang.org/grpc to v1.82.0 (#550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [google.golang.org/grpc](https://redirect.github.com/grpc/grpc-go) | `v1.81.1` → `v1.82.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/google.golang.org%2fgrpc/v1.82.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/google.golang.org%2fgrpc/v1.81.1/v1.82.0?slim=true) | --- ### Release Notes
grpc/grpc-go (google.golang.org/grpc) ### [`v1.82.0`](https://redirect.github.com/grpc/grpc-go/releases/tag/v1.82.0): Release 1.82.0 [Compare Source](https://redirect.github.com/grpc/grpc-go/compare/v1.81.1...v1.82.0) ### Behavior Changes - server: Remove support for `GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING` environment varibale. Strict incoming RPC path validation (which has been the default since `v1.79.3`) can no longer be disabled. ([#​9112](https://redirect.github.com/grpc/grpc-go/issues/9112)) - transport: Add environment variable to change the default max header list size from `16MB` to `8KB`. This may be enabled by setting `GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE=true`. This will be enabled by default in a subsequent release. ([#​9019](https://redirect.github.com/grpc/grpc-go/issues/9019)) - balancer: Load Balancing policy registry is now case-sensitive. Set `GRPC_GO_EXPERIMENTAL_CASE_SENSITIVE_BALANCER_REGISTRIES=false` (and file an issue) to revert to case-insensitive behavior. ([#​9017](https://redirect.github.com/grpc/grpc-go/issues/9017)) ### New Features - experimental/stats: Expose a new API, `NewContextWithLabelCallback`, to register a callback that is invoked when telemetry labels are added. ([#​8877](https://redirect.github.com/grpc/grpc-go/issues/8877)) - Special Thanks: [@​seth-epps](https://redirect.github.com/seth-epps) - client: Return a portion of the response body in the error message, when the client receives an unexpected non-gRPC HTTP response, to make debugging easier. ([#​8929](https://redirect.github.com/grpc/grpc-go/issues/8929)) - Special Thanks: [@​chengxilo](https://redirect.github.com/chengxilo) - server: Add environment variable `GRPC_GO_SERVER_GOROUTINE_LABELS` that controls setting `runtime/pprof.Labels` on goroutines spawned by the server. Set `GRPC_GO_SERVER_GOROUTINE_LABELS=grpc.method=true` to add the `grpc.method` label on goroutines spawned to handle incoming requests. ([#​9082](https://redirect.github.com/grpc/grpc-go/issues/9082)) - Special Thanks: [@​dfinkel](https://redirect.github.com/dfinkel) ### Bug Fixes - xds/server: Fix a memory leak of HTTP filter instances occurring when route configurations are updated in-place during a Route Discovery Service (RDS) update. ([#​9138](https://redirect.github.com/grpc/grpc-go/issues/9138)) - grpc: In the deprecated `gzip` Compressor (used via the deprecated `WithCompressor` dial option), enforce the `MaxRecvMsgSize` limit on the decompressed message buffer, preventing excessive memory allocation from highly compressed payloads. ([#​9114](https://redirect.github.com/grpc/grpc-go/issues/9114)) - Special Thanks: [@​evilgensec](https://redirect.github.com/evilgensec) - stats/opentelemetry: Record retry attempts, `grpc.previous-rpc-attempts`, at the call level and not the attempt level. ([#​8923](https://redirect.github.com/grpc/grpc-go/issues/8923)) - encoding: Ensure `Close()` is always called on readers returned from `Compressor.Decompress` if possible. ([#​9135](https://redirect.github.com/grpc/grpc-go/issues/9135)) - channelz: Fix the `LastMessageSentTimestamp` and `LastMessageReceivedTimestamp` fields in `SocketMetrics` to ensure they contain correct timestamp values. ([#​9109](https://redirect.github.com/grpc/grpc-go/issues/9109))
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). --- NOTICE | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- internal/tmpl/NOTICE | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/NOTICE b/NOTICE index 5d7a920a..d2da94e2 100644 --- a/NOTICE +++ b/NOTICE @@ -1211,9 +1211,9 @@ License URL: https://github.com/googleapis/go-genproto/blob/b703f567277d/googlea ---------- Module: google.golang.org/grpc -Version: v1.81.1 +Version: v1.82.0 License: Apache-2.0 -License URL: https://github.com/grpc/grpc-go/blob/v1.81.1/LICENSE +License URL: https://github.com/grpc/grpc-go/blob/v1.82.0/LICENSE ---------- Module: google.golang.org/protobuf diff --git a/go.mod b/go.mod index 89c9e15e..ab97def0 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( golang.org/x/oauth2 v0.36.0 golang.org/x/term v0.44.0 google.golang.org/api v0.286.0 - google.golang.org/grpc v1.81.1 + google.golang.org/grpc v1.82.0 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v4 v4.2.2 diff --git a/go.sum b/go.sum index f1336cd9..dbd7bc00 100644 --- a/go.sum +++ b/go.sum @@ -6650,8 +6650,8 @@ google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= google.golang.org/grpc/examples v0.0.0-20201112215255-90f1b3ee835b/go.mod h1:IBqQ7wSUJ2Ep09a8rMWFsg4fmI2r38zwsq8a0GgxXpM= diff --git a/internal/tmpl/NOTICE b/internal/tmpl/NOTICE index 5d7a920a..d2da94e2 100644 --- a/internal/tmpl/NOTICE +++ b/internal/tmpl/NOTICE @@ -1211,9 +1211,9 @@ License URL: https://github.com/googleapis/go-genproto/blob/b703f567277d/googlea ---------- Module: google.golang.org/grpc -Version: v1.81.1 +Version: v1.82.0 License: Apache-2.0 -License URL: https://github.com/grpc/grpc-go/blob/v1.81.1/LICENSE +License URL: https://github.com/grpc/grpc-go/blob/v1.82.0/LICENSE ---------- Module: google.golang.org/protobuf From b178246b60b23644d4ca1afc0cfd9a5b4b2b1b4e Mon Sep 17 00:00:00 2001 From: Codesphere Bot <117686659+CodesphereBot@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:23:17 +0200 Subject: [PATCH 08/12] update(deps): update module google.golang.org/api to v0.287.0 (#551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [google.golang.org/api](https://redirect.github.com/googleapis/google-api-go-client) | `v0.286.0` → `v0.287.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/google.golang.org%2fapi/v0.287.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/google.golang.org%2fapi/v0.286.0/v0.287.0?slim=true) | --- ### Release Notes
googleapis/google-api-go-client (google.golang.org/api) ### [`v0.287.0`](https://redirect.github.com/googleapis/google-api-go-client/releases/tag/v0.287.0) [Compare Source](https://redirect.github.com/googleapis/google-api-go-client/compare/v0.286.0...v0.287.0) ##### Features - **all:** Auto-regenerate discovery clients ([#​3635](https://redirect.github.com/googleapis/google-api-go-client/issues/3635)) ([504873e](https://redirect.github.com/googleapis/google-api-go-client/commit/504873e45d4a0993065311ed3f6a0467f2c41ab1)) - **all:** Auto-regenerate discovery clients ([#​3637](https://redirect.github.com/googleapis/google-api-go-client/issues/3637)) ([5c975be](https://redirect.github.com/googleapis/google-api-go-client/commit/5c975bee9f05d3c570eb0d95be248f3cb418739f)) - **all:** Auto-regenerate discovery clients ([#​3639](https://redirect.github.com/googleapis/google-api-go-client/issues/3639)) ([9737c4b](https://redirect.github.com/googleapis/google-api-go-client/commit/9737c4bf678023b300958b55d1548a902ff36c5d))
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). --- NOTICE | 8 ++++---- go.mod | 2 +- go.sum | 4 ++-- internal/tmpl/NOTICE | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/NOTICE b/NOTICE index d2da94e2..2a1a9aee 100644 --- a/NOTICE +++ b/NOTICE @@ -1181,15 +1181,15 @@ License URL: https://github.com/gomodules/jsonpatch/blob/v2.5.0/v2/LICENSE ---------- Module: google.golang.org/api -Version: v0.286.0 +Version: v0.287.0 License: BSD-3-Clause -License URL: https://github.com/googleapis/google-api-go-client/blob/v0.286.0/LICENSE +License URL: https://github.com/googleapis/google-api-go-client/blob/v0.287.0/LICENSE ---------- Module: google.golang.org/api/internal/third_party/uritemplates -Version: v0.286.0 +Version: v0.287.0 License: BSD-3-Clause -License URL: https://github.com/googleapis/google-api-go-client/blob/v0.286.0/internal/third_party/uritemplates/LICENSE +License URL: https://github.com/googleapis/google-api-go-client/blob/v0.287.0/internal/third_party/uritemplates/LICENSE ---------- Module: google.golang.org/genproto/googleapis diff --git a/go.mod b/go.mod index ab97def0..349c684e 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( golang.org/x/mod v0.37.0 golang.org/x/oauth2 v0.36.0 golang.org/x/term v0.44.0 - google.golang.org/api v0.286.0 + google.golang.org/api v0.287.0 google.golang.org/grpc v1.82.0 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index dbd7bc00..05f24086 100644 --- a/go.sum +++ b/go.sum @@ -6133,8 +6133,8 @@ google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1Gk google.golang.org/api v0.222.0/go.mod h1:efZia3nXpWELrwMlN5vyQrD4GmJN1Vw0x68Et3r+a9c= google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4= -google.golang.org/api v0.286.0 h1:TdTXMvzYKnWV1/lPbCdbXRqBrkDqjPto22H2xeZZ8LI= -google.golang.org/api v0.286.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ= +google.golang.org/api v0.287.0 h1:CQDMqUiqZZ0U/Yge3zyjAhNQ0OSYEH0PaA7l4xtEen4= +google.golang.org/api v0.287.0/go.mod h1:pPW85yt3Iuc3unkpaMhFtMmOqnTdCwCqEOaUlnuxRlQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/internal/tmpl/NOTICE b/internal/tmpl/NOTICE index d2da94e2..2a1a9aee 100644 --- a/internal/tmpl/NOTICE +++ b/internal/tmpl/NOTICE @@ -1181,15 +1181,15 @@ License URL: https://github.com/gomodules/jsonpatch/blob/v2.5.0/v2/LICENSE ---------- Module: google.golang.org/api -Version: v0.286.0 +Version: v0.287.0 License: BSD-3-Clause -License URL: https://github.com/googleapis/google-api-go-client/blob/v0.286.0/LICENSE +License URL: https://github.com/googleapis/google-api-go-client/blob/v0.287.0/LICENSE ---------- Module: google.golang.org/api/internal/third_party/uritemplates -Version: v0.286.0 +Version: v0.287.0 License: BSD-3-Clause -License URL: https://github.com/googleapis/google-api-go-client/blob/v0.286.0/internal/third_party/uritemplates/LICENSE +License URL: https://github.com/googleapis/google-api-go-client/blob/v0.287.0/internal/third_party/uritemplates/LICENSE ---------- Module: google.golang.org/genproto/googleapis From 6d51db6deb3527c29ddaa4c126d75c760329e1ca Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:11:31 +0200 Subject: [PATCH 09/12] fix(vault): trim trailing newlines from kubeconfig before storing in vault add unwrapSOPSData function to handle SOPS data extraction (#539) This pull request introduces several improvements and fixes related to handling kubeconfig storage, SOPS-encrypted vault data, and associated tests. The main changes ensure that kubeconfig files are stored without unwanted trailing newlines, add robust handling for SOPS-encrypted YAML files by unwrapping top-level `data` wrappers, and enhance testing to validate these behaviors. **Kubeconfig storage improvements:** * Trailing newlines are now trimmed from the kubeconfig content before storing it in the vault, preventing YAML formatting issues such as the use of `|+` chomping. * Tests were updated and extended to verify that trailing newlines are properly removed and to ensure the vault file does not contain unwanted YAML formatting. **SOPS/YAML handling improvements:** * Added the `unwrapSOPSData` function to automatically strip a top-level `data` block scalar wrapper from SOPS-encrypted YAML files, ensuring the vault parser receives the intended document structure. * Integrated the unwrapping logic into the vault data parser and made the function available for testing. * Added comprehensive unit tests for `unwrapSOPSData` to verify correct behavior with various YAML inputs, including normal, wrapped, and invalid documents. [Clickup](https://app.clickup.com/t/24560134/869dwweda) --- cli/cmd/install_k0s.go | 2 + cli/cmd/install_k0s_test.go | 45 ++++++++++- internal/installer/vault_encryption.go | 29 ++++++- internal/installer/vault_encryption_test.go | 77 +++++++++++++++++++ .../vault_encryption_unexported_test.go | 47 +++++++++++ .../vault_templating_secret_store.go | 2 + 6 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 internal/installer/vault_encryption_unexported_test.go diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index 4302c896..52a8baaa 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -7,6 +7,7 @@ import ( "fmt" "log" "path/filepath" + "strings" packageio "github.com/codesphere-cloud/cs-go/pkg/io" "github.com/spf13/cobra" @@ -236,6 +237,7 @@ func (c *InstallK0sCmd) saveKubeconfigToVault(k0sctl installer.K0sctlManager, k0 if err != nil { return fmt.Errorf("failed to retrieve kubeconfig from k0sctl: %w", err) } + kubeconfigContent = strings.TrimRight(kubeconfigContent, "\n\r") vault, wasEncrypted, err := c.loadOrCreateVault() if err != nil { diff --git a/cli/cmd/install_k0s_test.go b/cli/cmd/install_k0s_test.go index aa21eec0..3c65f937 100644 --- a/cli/cmd/install_k0s_test.go +++ b/cli/cmd/install_k0s_test.go @@ -258,7 +258,9 @@ var _ = Describe("InstallK0sCmd", func() { setupCommonMocks() mockK0sctl.EXPECT().GetKubeconfig(mock.Anything, "/tmp/k0sctl").Return("apiVersion: v1\nkind: Config\n", nil) mockFileWriter.EXPECT().Exists(c.Opts.Vault).Return(false) - mockFileWriter.EXPECT().WriteFile(c.Opts.Vault, mock.Anything, os.FileMode(0600)).Return(nil) + mockFileWriter.EXPECT().WriteFile(c.Opts.Vault, mock.MatchedBy(func(data []byte) bool { + return !strings.Contains(string(data), "content: |+") + }), os.FileMode(0600)).Return(nil) err := c.InstallK0s(mockPM, mockK0s, mockK0sctl) Expect(err).NotTo(HaveOccurred()) @@ -290,7 +292,9 @@ var _ = Describe("InstallK0sCmd", func() { setupCommonMocks() mockK0sctl.EXPECT().GetKubeconfig(mock.Anything, "/tmp/k0sctl").Return("apiVersion: v1\nkind: Config\n", nil) mockFileWriter.EXPECT().Exists(c.Opts.Vault).Return(true) - mockFileWriter.EXPECT().WriteFile(c.Opts.Vault, mock.Anything, os.FileMode(0600)).Return(nil) + mockFileWriter.EXPECT().WriteFile(c.Opts.Vault, mock.MatchedBy(func(data []byte) bool { + return !strings.Contains(string(data), "content: |+") + }), os.FileMode(0600)).Return(nil) err = c.InstallK0s(mockPM, mockK0s, mockK0sctl) Expect(err).NotTo(HaveOccurred()) @@ -322,12 +326,47 @@ var _ = Describe("InstallK0sCmd", func() { setupCommonMocks() mockK0sctl.EXPECT().GetKubeconfig(mock.Anything, "/tmp/k0sctl").Return("apiVersion: v1\nkind: Config\nnew: true\n", nil) mockFileWriter.EXPECT().Exists(c.Opts.Vault).Return(true) - mockFileWriter.EXPECT().WriteFile(c.Opts.Vault, mock.Anything, os.FileMode(0600)).Return(nil) + mockFileWriter.EXPECT().WriteFile(c.Opts.Vault, mock.MatchedBy(func(data []byte) bool { + return !strings.Contains(string(data), "content: |+") + }), os.FileMode(0600)).Return(nil) err = c.InstallK0s(mockPM, mockK0s, mockK0sctl) Expect(err).NotTo(HaveOccurred()) }) + It("trims trailing newlines from kubeconfig before storing in vault", func() { + c.Opts.InstallConfig = writeTestConfig(createTestConfig(true)) + c.Opts.Package = "test-package.tar.gz" + c.Opts.Version = "v1.30.0+k0s.0" + c.Opts.Vault = filepath.Join(tempDir, "prod.vault.yaml") + + setupCommonMocks() + // kubeconfig with multiple trailing newlines — should be stripped + mockK0sctl.EXPECT().GetKubeconfig(mock.Anything, "/tmp/k0sctl"). + Return("apiVersion: v1\nkind: Config\n\n\n", nil) + mockFileWriter.EXPECT().Exists(c.Opts.Vault).Return(false) + mockFileWriter.EXPECT().WriteFile(c.Opts.Vault, mock.MatchedBy(func(data []byte) bool { + // Must not contain |+ chomping — trailing newlines should be stripped + if strings.Contains(string(data), "content: |+") { + return false + } + // Verify the stored kubeconfig has no trailing newlines + var vault files.InstallVault + if err := yaml.Unmarshal(data, &vault); err != nil { + return false + } + for _, s := range vault.Secrets { + if s.Name == "kubeConfig" && s.File != nil { + return s.File.Content == "apiVersion: v1\nkind: Config" + } + } + return false + }), os.FileMode(0600)).Return(nil) + + err := c.InstallK0s(mockPM, mockK0s, mockK0sctl) + Expect(err).NotTo(HaveOccurred()) + }) + It("fails when GetKubeconfig fails", func() { c.Opts.InstallConfig = writeTestConfig(createTestConfig(true)) c.Opts.Package = "test-package.tar.gz" diff --git a/internal/installer/vault_encryption.go b/internal/installer/vault_encryption.go index 91a108b3..321ddab7 100644 --- a/internal/installer/vault_encryption.go +++ b/internal/installer/vault_encryption.go @@ -14,6 +14,7 @@ import ( "filippo.io/age" sopsage "github.com/getsops/sops/v3/age" + "go.yaml.in/yaml/v3" ) var ( @@ -158,7 +159,7 @@ func generateAgeKey(keyPath string) (string, error) { // EncryptFileWithSOPS encrypts src with SOPS+age and writes ciphertext to target. func EncryptFileWithSOPS(src, target, recipient string) error { - cmd := exec.Command("sops", "--encrypt", "--age", recipient, "--output", target, src) + cmd := exec.Command("sops", "--encrypt", "--input-type", "yaml", "--age", recipient, "--output", target, src) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("sops encrypt failed: %w: %s", err, out) @@ -169,7 +170,7 @@ func EncryptFileWithSOPS(src, target, recipient string) error { // DecryptFileWithSOPS decrypts a SOPS-encrypted file and returns the plaintext bytes. // If keyPath is non-empty, SOPS_AGE_KEY_FILE is set for the sops process. func DecryptFileWithSOPS(src, keyPath string) ([]byte, error) { - cmd := exec.Command("sops", "--decrypt", src) + cmd := exec.Command("sops", "--decrypt", "--input-type", "yaml", src) if keyPath != "" { cmd.Env = append(os.Environ(), "SOPS_AGE_KEY_FILE="+keyPath) } @@ -182,3 +183,27 @@ func DecryptFileWithSOPS(src, keyPath string) ([]byte, error) { } return out, nil } + +// unwrapSOPSData strips a top-level "data" literal block scalar wrapper if +// present. When SOPS encrypts with --input-type yaml, it +// wraps the entire document under a data: | key. +func unwrapSOPSData(data []byte) []byte { + var doc yaml.Node + if err := yaml.Unmarshal(data, &doc); err != nil { + return data + } + if len(doc.Content) == 0 { + return data + } + root := doc.Content[0] + if root.Kind != yaml.MappingNode || len(root.Content) != 2 { + return data + } + keyNode := root.Content[0] + valNode := root.Content[1] + if keyNode.Value != "data" || valNode.Kind != yaml.ScalarNode { + return data + } + // The scalar value is the inner YAML content. + return []byte(valNode.Value) +} diff --git a/internal/installer/vault_encryption_test.go b/internal/installer/vault_encryption_test.go index 8b64ebbd..f40ea89a 100644 --- a/internal/installer/vault_encryption_test.go +++ b/internal/installer/vault_encryption_test.go @@ -207,6 +207,83 @@ var _ = Describe("VaultEncryption", func() { Expect(err).To(HaveOccurred()) }) }) + + Describe("LoadVaultData", func() { + var tmpDir string + + BeforeEach(func() { + var err error + tmpDir, err = os.MkdirTemp("", "load-vault-test-*") + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + Expect(os.RemoveAll(tmpDir)).To(Succeed()) + }) + + It("parses a plain vault file without data: wrapper", func() { + vaultPath := filepath.Join(tmpDir, "plain.vault.yaml") + plainYAML := "secrets:\n - name: test-secret\n fields:\n password: hunter2\n" + Expect(os.WriteFile(vaultPath, []byte(plainYAML), 0644)).To(Succeed()) + + vault, err := installer.LoadVaultData(vaultPath, "") + Expect(err).ToNot(HaveOccurred()) + Expect(vault.Secrets).To(HaveLen(1)) + Expect(vault.Secrets[0].Name).To(Equal("test-secret")) + Expect(vault.Secrets[0].Fields.Password).To(Equal("hunter2")) + }) + + It("unwraps a plain file with data: | wrapper (SOPS whole-file format edge case)", func() { + vaultPath := filepath.Join(tmpDir, "wrapped.vault.yaml") + wrappedYAML := "data: |\n secrets:\n - name: test-secret\n fields:\n password: hunter2\n" + Expect(os.WriteFile(vaultPath, []byte(wrappedYAML), 0644)).To(Succeed()) + + vault, err := installer.LoadVaultData(vaultPath, "") + Expect(err).ToNot(HaveOccurred()) + Expect(vault.Secrets).To(HaveLen(1)) + Expect(vault.Secrets[0].Name).To(Equal("test-secret")) + Expect(vault.Secrets[0].Fields.Password).To(Equal("hunter2")) + }) + + It("loads and decrypts a SOPS-encrypted vault end-to-end", func() { + if !sopsAndAgeAvailable() { + Skip("sops and age-keygen not available") + } + + // Generate an age keypair. + ageKeyPath := filepath.Join(tmpDir, "age_key.txt") + out, err := exec.Command("age-keygen", "-o", ageKeyPath).CombinedOutput() + Expect(err).ToNot(HaveOccurred(), string(out)) + + // Extract the public key (recipient). + recipient, _, err := installer.ResolveAgeKey(ageKeyPath, tmpDir) + Expect(err).ToNot(HaveOccurred()) + + // Write a plain vault file. + plainPath := filepath.Join(tmpDir, "plain.vault.yaml") + plainYAML := "secrets:\n - name: sops-secret\n fields:\n password: s3cr3t\n" + Expect(os.WriteFile(plainPath, []byte(plainYAML), 0644)).To(Succeed()) + + // Encrypt with SOPS using --input-type yaml (whole-file mode, + // which wraps content under data: |). + vaultPath := filepath.Join(tmpDir, "encrypted.vault.yaml") + encryptCmd := exec.Command("sops", "--encrypt", "--input-type", "yaml", "--age", recipient, "--output", vaultPath, plainPath) + encOut, err := encryptCmd.CombinedOutput() + Expect(err).ToNot(HaveOccurred(), string(encOut)) + + // LoadVaultData should detect SOPS, decrypt, unwrap data: |, and parse. + vault, err := installer.LoadVaultData(vaultPath, ageKeyPath) + Expect(err).ToNot(HaveOccurred()) + Expect(vault.Secrets).To(HaveLen(1)) + Expect(vault.Secrets[0].Name).To(Equal("sops-secret")) + Expect(vault.Secrets[0].Fields.Password).To(Equal("s3cr3t")) + }) + + It("returns an error for a non-existent file", func() { + _, err := installer.LoadVaultData(filepath.Join(tmpDir, "missing.yaml"), "") + Expect(err).To(HaveOccurred()) + }) + }) }) func splitLines(s string) []string { diff --git a/internal/installer/vault_encryption_unexported_test.go b/internal/installer/vault_encryption_unexported_test.go new file mode 100644 index 00000000..2697450f --- /dev/null +++ b/internal/installer/vault_encryption_unexported_test.go @@ -0,0 +1,47 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package installer + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("unwrapSOPSData", func() { + It("returns data unchanged when there is no data: wrapper", func() { + input := []byte("secrets:\n - name: foo\n fields:\n password: bar\n") + output := unwrapSOPSData(input) + Expect(string(output)).To(Equal(string(input))) + }) + + It("strips a top-level data: | wrapper and returns inner content", func() { + input := []byte("data: |\n secrets:\n - name: foo\n fields:\n password: bar\n") + output := unwrapSOPSData(input) + Expect(string(output)).To(Equal("secrets:\n - name: foo\n fields:\n password: bar\n")) + }) + + It("returns data unchanged for an empty document", func() { + input := []byte("") + output := unwrapSOPSData(input) + Expect(string(output)).To(Equal(string(input))) + }) + + It("returns data unchanged when root has multiple keys", func() { + input := []byte("data: some-value\nsops:\n key: val\n") + output := unwrapSOPSData(input) + Expect(string(output)).To(Equal(string(input))) + }) + + It("returns data unchanged for invalid YAML", func() { + input := []byte("not: valid: yaml: [[") + output := unwrapSOPSData(input) + Expect(string(output)).To(Equal(string(input))) + }) + + It("returns data unchanged when data is not a scalar", func() { + input := []byte("data:\n nested: value\n") + output := unwrapSOPSData(input) + Expect(string(output)).To(Equal(string(input))) + }) +}) diff --git a/internal/installer/vault_templating_secret_store.go b/internal/installer/vault_templating_secret_store.go index 42a7efcf..4bcbd500 100644 --- a/internal/installer/vault_templating_secret_store.go +++ b/internal/installer/vault_templating_secret_store.go @@ -179,6 +179,8 @@ func isSOPSEncryptedYAML(data []byte) (bool, error) { } func parseVaultData(data []byte) (*files.InstallVault, error) { + data = unwrapSOPSData(data) + vault := &files.InstallVault{} if err := vault.Unmarshal(data); err != nil { return nil, err From abc9e0f3955cadbd2282c4ff16e7323410fcf64a Mon Sep 17 00:00:00 2001 From: Nathanael Ruf <104262550+nathanael-ruf@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:35:03 +0200 Subject: [PATCH 10/12] refac(cs-flags): Split experiments into internal and preview (#544) Also enabled `workspace-ssh` preview flag by default (was handed over to SRE yesterday). --------- Signed-off-by: Nathanael Ruf --- cli/cmd/bootstrap_gcp.go | 21 +++-- cli/cmd/bootstrap_local.go | 26 ++++-- cli/cmd/init_install_config_test.go | 4 +- cli/cmd/update_install_config_test.go | 4 +- docs/oms_beta_bootstrap-gcp.md | 3 +- docs/oms_beta_bootstrap-local.md | 37 ++++---- internal/bootstrap/gcp/gce_test.go | 67 ++++++++------- internal/bootstrap/gcp/gcp.go | 84 +++++++++++-------- internal/bootstrap/gcp/gcp_test.go | 5 +- internal/bootstrap/gcp/iam_admin_test.go | 5 +- internal/bootstrap/gcp/install_config.go | 5 +- internal/bootstrap/gcp/install_config_test.go | 37 ++++++-- internal/bootstrap/local/local.go | 14 ++-- internal/installer/config_manager_profile.go | 10 ++- internal/installer/files/config_yaml.go | 3 +- internal/util/map.go | 14 ++++ internal/util/map_test.go | 34 ++++++++ 17 files changed, 247 insertions(+), 126 deletions(-) create mode 100644 internal/util/map_test.go diff --git a/cli/cmd/bootstrap_gcp.go b/cli/cmd/bootstrap_gcp.go index de02ab75..02368144 100644 --- a/cli/cmd/bootstrap_gcp.go +++ b/cli/cmd/bootstrap_gcp.go @@ -28,7 +28,9 @@ type BootstrapGcpCmd struct { CodesphereEnv *gcp.CodesphereEnvironment InputRegistryType string SSHQuiet bool - FeatureFlagList []string + // experiments backs the deprecated --experiments flag; its values + // are folded into the internal bucket for backwards compatibility. + experiments []string } func (c *BootstrapGcpCmd) RunE(_ *cobra.Command, args []string) error { @@ -53,7 +55,7 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *GlobalOptions) { }, Opts: opts, Env: env.NewEnv(), - CodesphereEnv: &gcp.CodesphereEnvironment{FeatureFlags: map[string]bool{}}, + CodesphereEnv: &gcp.CodesphereEnvironment{}, } bootstrapGcpCmd.cmd.RunE = bootstrapGcpCmd.RunE @@ -97,8 +99,11 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *GlobalOptions) { flags.StringArrayVarP(&bootstrapGcpCmd.CodesphereEnv.InstallSkipSteps, "install-skip-steps", "s", []string{}, "Installation steps to skip during Codesphere installation (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.RegistryUser, "registry-user", "", "Custom Registry username (only for GitHub registry type) (optional)") flags.StringVar(&bootstrapGcpCmd.InputRegistryType, "registry-type", "local-container", "Container registry type to use (options: local-container, artifact-registry) (default: local-container)") - flags.StringArrayVar(&bootstrapGcpCmd.CodesphereEnv.Experiments, "experiments", gcp.DefaultExperiments, "Experiments to enable in Codesphere installation (optional)") - flags.StringArrayVar(&bootstrapGcpCmd.FeatureFlagList, "feature-flags", []string{}, "Feature flags to enable in Codesphere installation (optional)") + flags.StringArrayVar(&bootstrapGcpCmd.CodesphereEnv.InternalFlags, "internal-flags", gcp.DefaultInternalFlags, "Internal flags to enable in Codesphere installation (optional)") + flags.StringArrayVar(&bootstrapGcpCmd.experiments, "experiments", []string{}, "Deprecated: use --internal-flags instead. Values are added to the internal flags.") + _ = flags.MarkDeprecated("experiments", "use --internal-flags instead") + flags.StringArrayVar(&bootstrapGcpCmd.CodesphereEnv.PreviewFlags, "preview-flags", gcp.DefaultPreviewFlags, "Preview flags to enable in Codesphere installation (optional)") + flags.StringArrayVar(&bootstrapGcpCmd.CodesphereEnv.FeatureFlags, "feature-flags", gcp.DefaultFeatureFlags, "Feature flags to enable in Codesphere installation (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.ExternalLokiEndpoint, "external-loki-endpoint", "", "External Loki endpoint for Grafana Alloy log forwarding (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.ExternalLokiSecret, "external-loki-secret", "", "External Loki password stored in the generated vault (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.ExternalLokiUser, "external-loki-user", "", "External Loki username for Grafana Alloy log forwarding (optional)") @@ -172,8 +177,12 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error { } } - for _, flag := range c.FeatureFlagList { - c.CodesphereEnv.FeatureFlags[flag] = true + if c.cmd.Flags().Changed("experiments") { + if c.cmd.Flags().Changed("internal-flags") { + log.Printf("Warning: both --experiments and --internal-flags were set; ignoring deprecated --experiments values %v", c.experiments) + } else { + c.CodesphereEnv.InternalFlags = c.experiments + } } err = bs.Bootstrap() diff --git a/cli/cmd/bootstrap_local.go b/cli/cmd/bootstrap_local.go index 06171bd1..d3c2add1 100644 --- a/cli/cmd/bootstrap_local.go +++ b/cli/cmd/bootstrap_local.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" stdio "io" + "log" "os" "os/exec" "path/filepath" @@ -35,10 +36,12 @@ import ( ) type BootstrapLocalCmd struct { - cmd *cobra.Command - CodesphereEnv *local.CodesphereEnvironment - Yes bool - FeatureFlagList []string + cmd *cobra.Command + CodesphereEnv *local.CodesphereEnvironment + Yes bool + // Experiments backs the deprecated --experiments flag; its values + // are folded into the internal bucket for backwards compatibility. + experiments []string } func (c *BootstrapLocalCmd) RunE(_ *cobra.Command, args []string) error { @@ -75,8 +78,11 @@ func AddBootstrapLocalCmd(parent *cobra.Command) { // Codesphere Environment flags.StringVar(&bootstrapLocalCmd.CodesphereEnv.BaseDomain, "base-domain", "cs.local", "Base domain for Codesphere") - flags.StringArrayVar(&bootstrapLocalCmd.CodesphereEnv.Experiments, "experiments", gcp.DefaultExperiments, "Experiments to enable in Codesphere installation (optional)") - flags.StringArrayVar(&bootstrapLocalCmd.FeatureFlagList, "feature-flags", []string{}, "Feature flags to enable in Codesphere installation (optional)") + flags.StringArrayVar(&bootstrapLocalCmd.CodesphereEnv.InternalFlags, "internal-flags", gcp.DefaultInternalFlags, "Internal flags to enable in Codesphere installation (optional)") + flags.StringArrayVar(&bootstrapLocalCmd.experiments, "experiments", []string{}, "Deprecated: use --internal-flags instead. Values are added to the internal flags.") + _ = flags.MarkDeprecated("experiments", "use --internal-flags instead") + flags.StringArrayVar(&bootstrapLocalCmd.CodesphereEnv.PreviewFlags, "preview-flags", gcp.DefaultPreviewFlags, "Preview flags to enable in Codesphere installation (optional)") + flags.StringArrayVar(&bootstrapLocalCmd.CodesphereEnv.FeatureFlags, "feature-flags", gcp.DefaultFeatureFlags, "Feature flags to enable in Codesphere installation (optional)") flags.StringVar(&bootstrapLocalCmd.CodesphereEnv.Profile, "profile", installer.PROFILE_DEV, "Profile to apply to the install config like resources (supported: dev, minimal, prod)") flags.BoolVar(&bootstrapLocalCmd.CodesphereEnv.K0s, "k0s", false, "Use k0s-specific configuration (required to deploy to k0s clusters)") @@ -124,8 +130,12 @@ func (c *BootstrapLocalCmd) BootstrapLocal() error { return err } - for _, flag := range c.FeatureFlagList { - c.CodesphereEnv.FeatureFlags[flag] = true + if c.cmd.Flags().Changed("experiments") { + if c.cmd.Flags().Changed("internal-flags") { + log.Printf("Warning: both --experiments and --internal-flags were set; ignoring deprecated --experiments values %v", c.experiments) + } else { + c.CodesphereEnv.InternalFlags = c.experiments + } } stlog := bootstrap.NewStepLogger(false) diff --git a/cli/cmd/init_install_config_test.go b/cli/cmd/init_install_config_test.go index 6a834fc2..a073f498 100644 --- a/cli/cmd/init_install_config_test.go +++ b/cli/cmd/init_install_config_test.go @@ -110,7 +110,9 @@ codesphere: cNameBaseDomain: custom.example.com dnsServers: - 8.8.8.8 - experiments: [] + internal: [] + preview: {} + features: {} deployConfig: images: ubuntu-24.04: diff --git a/cli/cmd/update_install_config_test.go b/cli/cmd/update_install_config_test.go index 41cf3222..3ead587d 100644 --- a/cli/cmd/update_install_config_test.go +++ b/cli/cmd/update_install_config_test.go @@ -126,7 +126,9 @@ codesphere: dnsServers: - 8.8.8.8 - 8.8.4.4 - experiments: [] + internal: [] + preview: {} + features: {} deployConfig: images: {} plans: diff --git a/docs/oms_beta_bootstrap-gcp.md b/docs/oms_beta_bootstrap-gcp.md index bfeac3a5..ce081e6c 100644 --- a/docs/oms_beta_bootstrap-gcp.md +++ b/docs/oms_beta_bootstrap-gcp.md @@ -33,7 +33,6 @@ oms beta bootstrap-gcp [flags] --datacenter-name string Datacenter name (default: dev) (default "dev") --dns-project-id string GCP Project ID for Cloud DNS (optional) --dns-zone-name string Cloud DNS Zone Name (optional) (default "oms-testing") - --experiments stringArray Experiments to enable in Codesphere installation (optional) (default [headless-services,vcluster,custom-service-image,ms-in-ls,secret-management,sub-path-mount]) --external-loki-endpoint string External Loki endpoint for Grafana Alloy log forwarding (optional) --external-loki-secret string External Loki password stored in the generated vault (optional) --external-loki-user string External Loki username for Grafana Alloy log forwarding (optional) @@ -54,6 +53,7 @@ oms beta bootstrap-gcp [flags] --install-local string Install Codesphere from local package (default: none) -s, --install-skip-steps stringArray Installation steps to skip during Codesphere installation (optional) --install-version string Codesphere version to install (default: none) + --internal-flags stringArray Internal flags to enable in Codesphere installation (optional) (default [headless-services,vcluster,custom-service-image,ms-in-ls]) --local-trace-endpoint string Endpoint for exporting traces to an in-cluster storage (optional) --oidc-client-id string OIDC OAuth provider Client ID (optional) --oidc-client-secret string OIDC OAuth provider Client Secret (optional) @@ -64,6 +64,7 @@ oms beta bootstrap-gcp [flags] --openbao-uri string URI for OpenBao (optional) --openbao-user string OpenBao username (optional) (default "admin") --preemptible Use preemptible VMs for Codesphere infrastructure. Mutually exclusive with --spot-vms (default: false) + --preview-flags stringArray Preview flags to enable in Codesphere installation (optional) (default [secret-management,sub-path-mount,workspace-ssh]) --project-name string Unique GCP Project Name (required) --project-ttl string Time to live for the GCP project. Cleanup workflows will remove it afterwards. (default: 2 hours) (default "2h") --prometheus-remote-write-password string Prometheus remote write password stored in the generated vault (optional) diff --git a/docs/oms_beta_bootstrap-local.md b/docs/oms_beta_bootstrap-local.md index 896e8697..ee43c3b2 100644 --- a/docs/oms_beta_bootstrap-local.md +++ b/docs/oms_beta_bootstrap-local.md @@ -16,24 +16,25 @@ oms beta bootstrap-local [flags] ### Options ``` - --argocd After infra setup: install ArgoCD, update the OCI pull secret, and install pc-apps from the BOM version - --base-domain string Base domain for Codesphere (default "cs.local") - --experiments stringArray Experiments to enable in Codesphere installation (optional) (default [headless-services,vcluster,custom-service-image,ms-in-ls,secret-management,sub-path-mount]) - --feature-flags stringArray Feature flags to enable in Codesphere installation (optional) - -h, --help help for bootstrap-local - --install-config string Path to install config file (default: /config.yaml) - --install-dir string Directory for config, secrets, and bundle files (default ".installer") - --install-hash string Codesphere package hash (required when install-version is set) - --install-local string Path to a local installer package (tar.gz or unpacked directory) - --install-version string Codesphere version to install (downloaded from the OMS portal) - --k0s Use k0s-specific configuration (required to deploy to k0s clusters) - --pod-cidr string Service CIDR of the Kubernetes cluster. If not specified, OMS will try to determine it. - --profile string Profile to apply to the install config like resources (supported: dev, minimal, prod) (default "dev") - --registry-url string OCI registry URL used for the ArgoCD helm pull secret (only relevant with --argocd) (default "oci://ghcr.io/codesphere-cloud/charts") - --registry-user string Custom Registry username (optional) - --secrets-file string Path to secrets file (default: /prod.vault.yaml) - --service-cidr string Service CIDR of the Kubernetes cluster. If not specified, OMS will try to determine it. - -y, --yes Auto-approve the local bootstrapping warning prompt + --argocd After infra setup: install ArgoCD, update the OCI pull secret, and install pc-apps from the BOM version + --base-domain string Base domain for Codesphere (default "cs.local") + --feature-flags stringArray Feature flags to enable in Codesphere installation (optional) + -h, --help help for bootstrap-local + --install-config string Path to install config file (default: /config.yaml) + --install-dir string Directory for config, secrets, and bundle files (default ".installer") + --install-hash string Codesphere package hash (required when install-version is set) + --install-local string Path to a local installer package (tar.gz or unpacked directory) + --install-version string Codesphere version to install (downloaded from the OMS portal) + --internal-flags stringArray Internal flags to enable in Codesphere installation (optional) (default [headless-services,vcluster,custom-service-image,ms-in-ls]) + --k0s Use k0s-specific configuration (required to deploy to k0s clusters) + --pod-cidr string Service CIDR of the Kubernetes cluster. If not specified, OMS will try to determine it. + --preview-flags stringArray Preview flags to enable in Codesphere installation (optional) (default [secret-management,sub-path-mount,workspace-ssh]) + --profile string Profile to apply to the install config like resources (supported: dev, minimal, prod) (default "dev") + --registry-url string OCI registry URL used for the ArgoCD helm pull secret (only relevant with --argocd) (default "oci://ghcr.io/codesphere-cloud/charts") + --registry-user string Custom Registry username (optional) + --secrets-file string Path to secrets file (default: /prod.vault.yaml) + --service-cidr string Service CIDR of the Kubernetes cluster. If not specified, OMS will try to determine it. + -y, --yes Auto-approve the local bootstrapping warning prompt ``` ### SEE ALSO diff --git a/internal/bootstrap/gcp/gce_test.go b/internal/bootstrap/gcp/gce_test.go index 0dc86348..4d6a1f5f 100644 --- a/internal/bootstrap/gcp/gce_test.go +++ b/internal/bootstrap/gcp/gce_test.go @@ -145,15 +145,16 @@ var _ = Describe("GCE", func() { BeforeEach(func() { csEnv = &gcp.CodesphereEnvironment{ - ProjectName: "test", - Region: "us-central1", - Zone: "us-central1-a", - BaseDomain: "example.com", - DNSProjectID: "dns-project", - DNSZoneName: "test-zone", - SecretsDir: "/etc/codesphere/secrets", - DatacenterID: 1, - Experiments: gcp.DefaultExperiments, + ProjectName: "test", + Region: "us-central1", + Zone: "us-central1-a", + BaseDomain: "example.com", + DNSProjectID: "dns-project", + DNSZoneName: "test-zone", + SecretsDir: "/etc/codesphere/secrets", + DatacenterID: 1, + InternalFlags: gcp.DefaultInternalFlags, + PreviewFlags: gcp.DefaultPreviewFlags, } gc := gcp.NewMockGCPClientManager(GinkgoT()) bs = newTestBootstrapper(csEnv, gc) @@ -202,7 +203,8 @@ var _ = Describe("GCE", func() { DNSZoneName: "test-zone", SecretsDir: "/etc/codesphere/secrets", DatacenterID: 1, - Experiments: gcp.DefaultExperiments, + InternalFlags: gcp.DefaultInternalFlags, + PreviewFlags: gcp.DefaultPreviewFlags, InstallConfigPath: "fake-config", SecretsFilePath: "fake-secrets", GitHubAppName: "fake-app", @@ -248,16 +250,17 @@ var _ = Describe("GCE", func() { BeforeEach(func() { gc = gcp.NewMockGCPClientManager(GinkgoT()) csEnv = &gcp.CodesphereEnvironment{ - ProjectName: "test", - ProjectID: "test-pid", - Region: "us-central1", - Zone: "us-central1-a", - BaseDomain: "example.com", - DNSProjectID: "dns-project", - DNSZoneName: "test-zone", - SecretsDir: "/etc/codesphere/secrets", - DatacenterID: 1, - Experiments: gcp.DefaultExperiments, + ProjectName: "test", + ProjectID: "test-pid", + Region: "us-central1", + Zone: "us-central1-a", + BaseDomain: "example.com", + DNSProjectID: "dns-project", + DNSZoneName: "test-zone", + SecretsDir: "/etc/codesphere/secrets", + DatacenterID: 1, + InternalFlags: gcp.DefaultInternalFlags, + PreviewFlags: gcp.DefaultPreviewFlags, } logCh = make(chan string, 10) bs = newTestBootstrapper(csEnv, gc) @@ -487,15 +490,16 @@ var _ = Describe("GCE", func() { gc := gcp.NewMockGCPClientManager(GinkgoT()) fw = util.NewMockFileIO(GinkgoT()) csEnv := &gcp.CodesphereEnvironment{ - ProjectName: "test", - Region: "us-central1", - Zone: "us-central1-a", - BaseDomain: "example.com", - DNSProjectID: "dns-project", - DNSZoneName: "test-zone", - SecretsDir: "/etc/codesphere/secrets", - DatacenterID: 1, - Experiments: gcp.DefaultExperiments, + ProjectName: "test", + Region: "us-central1", + Zone: "us-central1-a", + BaseDomain: "example.com", + DNSProjectID: "dns-project", + DNSZoneName: "test-zone", + SecretsDir: "/etc/codesphere/secrets", + DatacenterID: 1, + InternalFlags: gcp.DefaultInternalFlags, + PreviewFlags: gcp.DefaultPreviewFlags, } bs = newTestBootstrapperWithFileIO(csEnv, gc, fw) }) @@ -546,8 +550,9 @@ var _ = Describe("GCE", func() { SecretsDir: "/etc/codesphere/secrets", DatacenterID: 1, SSHPublicKeyPath: "key.pub", - Experiments: gcp.DefaultExperiments, - FeatureFlags: map[string]bool{}, + InternalFlags: gcp.DefaultInternalFlags, + PreviewFlags: gcp.DefaultPreviewFlags, + FeatureFlags: gcp.DefaultFeatureFlags, RootDiskSize: 50, } bs = newTestBootstrapperAll(csEnv, gc, fw, mockGitHubClient) diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 99f46d43..3a189512 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -62,15 +62,26 @@ func GetDNSRecordNames(baseDomain string) []struct { } } -var DefaultExperiments []string = []string{ +// This should ALWAYS be empty. Internal flags are for internal feature +// development and not intended for customer use. +// Atm. it's not empty as the internal flags below are likely preview or +// feature flags, but are still in the internal bucket for historical +// reasons (before we only had one "experiments" bucket). +var DefaultInternalFlags []string = []string{ "headless-services", "vcluster", "custom-service-image", "ms-in-ls", +} + +var DefaultPreviewFlags []string = []string{ "secret-management", "sub-path-mount", + "workspace-ssh", } +var DefaultFeatureFlags []string = []string{} + type GCPBootstrapper struct { ctx context.Context stlog *bootstrap.StepLogger @@ -87,41 +98,42 @@ type GCPBootstrapper struct { } type CodesphereEnvironment struct { - ProjectID string `json:"project_id"` - ProjectTTL string `json:"project_ttl"` - ProjectName string `json:"project_name"` - DNSProjectID string `json:"dns_project_id"` - Jumpbox *node.Node `json:"jumpbox"` - PostgreSQLNode *node.Node `json:"postgres_node"` - ControlPlaneNodes []*node.Node `json:"control_plane_nodes"` - CephNodes []*node.Node `json:"ceph_nodes"` - ContainerRegistryURL string `json:"-"` - ExistingConfigUsed bool `json:"-"` - InstallVersion string `json:"install_version"` - InstallLocal string `json:"install_local"` - InstallHash string `json:"install_hash"` - InstallSkipSteps []string `json:"install_skip_steps"` - Preemptible bool `json:"preemptible"` - SpotVMs bool `json:"spot_vms"` - WriteConfig bool `json:"-"` - RecoverConfig bool `json:"-"` - GatewayIP string `json:"gateway_ip"` - PublicGatewayIP string `json:"public_gateway_ip"` - SshProxyIP string `json:"ssh_proxy_ip"` - RegistryType RegistryType `json:"registry_type"` - GitHubPAT string `json:"-"` - GitHubAppName string `json:"-"` - GitHubTeamOrg string `json:"github_team_org"` - GitHubTeamSlug string `json:"github_team_slug"` - RegistryUser string `json:"-"` - Experiments []string `json:"experiments"` - FeatureFlags map[string]bool `json:"feature_flags"` - ExternalLokiEndpoint string `json:"external_loki_endpoint,omitempty"` - ExternalLokiSecret string `json:"-"` - ExternalLokiUser string `json:"external_loki_user,omitempty"` - PrometheusRemoteWriteUser string `json:"prometheus_remote_write_user,omitempty"` - PrometheusRemoteWritePassword string `json:"-"` - PrometheusRemoteWriteURL string `json:"prometheus_remote_write_url,omitempty"` + ProjectID string `json:"project_id"` + ProjectTTL string `json:"project_ttl"` + ProjectName string `json:"project_name"` + DNSProjectID string `json:"dns_project_id"` + Jumpbox *node.Node `json:"jumpbox"` + PostgreSQLNode *node.Node `json:"postgres_node"` + ControlPlaneNodes []*node.Node `json:"control_plane_nodes"` + CephNodes []*node.Node `json:"ceph_nodes"` + ContainerRegistryURL string `json:"-"` + ExistingConfigUsed bool `json:"-"` + InstallVersion string `json:"install_version"` + InstallLocal string `json:"install_local"` + InstallHash string `json:"install_hash"` + InstallSkipSteps []string `json:"install_skip_steps"` + Preemptible bool `json:"preemptible"` + SpotVMs bool `json:"spot_vms"` + WriteConfig bool `json:"-"` + RecoverConfig bool `json:"-"` + GatewayIP string `json:"gateway_ip"` + PublicGatewayIP string `json:"public_gateway_ip"` + SshProxyIP string `json:"ssh_proxy_ip"` + RegistryType RegistryType `json:"registry_type"` + GitHubPAT string `json:"-"` + GitHubAppName string `json:"-"` + GitHubTeamOrg string `json:"github_team_org"` + GitHubTeamSlug string `json:"github_team_slug"` + RegistryUser string `json:"-"` + InternalFlags []string `json:"internal"` + PreviewFlags []string `json:"preview"` + FeatureFlags []string `json:"feature_flags"` + ExternalLokiEndpoint string `json:"external_loki_endpoint,omitempty"` + ExternalLokiSecret string `json:"-"` + ExternalLokiUser string `json:"external_loki_user,omitempty"` + PrometheusRemoteWriteUser string `json:"prometheus_remote_write_user,omitempty"` + PrometheusRemoteWritePassword string `json:"-"` + PrometheusRemoteWriteURL string `json:"prometheus_remote_write_url,omitempty"` // ACME Issuer GoogleACMEIssuer bool `json:"google_acme_issuer,omitempty"` diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index a10c84fd..81a95782 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -95,8 +95,9 @@ var _ = Describe("GCP Bootstrapper", func() { DNSZoneName: "test-zone", SSHPublicKeyPath: "key.pub", ProjectID: "pid", - Experiments: gcp.DefaultExperiments, - FeatureFlags: map[string]bool{}, + InternalFlags: gcp.DefaultInternalFlags, + PreviewFlags: gcp.DefaultPreviewFlags, + FeatureFlags: gcp.DefaultFeatureFlags, RootDiskSize: 50, InstallConfig: &files.RootConfig{ Registry: &files.RegistryConfig{}, diff --git a/internal/bootstrap/gcp/iam_admin_test.go b/internal/bootstrap/gcp/iam_admin_test.go index 21aba720..ee37a846 100644 --- a/internal/bootstrap/gcp/iam_admin_test.go +++ b/internal/bootstrap/gcp/iam_admin_test.go @@ -88,8 +88,9 @@ var _ = Describe("IAM & Admin", func() { DNSZoneName: "test-zone", SSHPublicKeyPath: "key.pub", ProjectID: "pid", - Experiments: gcp.DefaultExperiments, - FeatureFlags: map[string]bool{}, + InternalFlags: gcp.DefaultInternalFlags, + PreviewFlags: gcp.DefaultPreviewFlags, + FeatureFlags: gcp.DefaultFeatureFlags, InstallConfig: &files.RootConfig{ Registry: &files.RegistryConfig{}, Postgres: files.PostgresConfig{ diff --git a/internal/bootstrap/gcp/install_config.go b/internal/bootstrap/gcp/install_config.go index 36ab16cb..0308f517 100644 --- a/internal/bootstrap/gcp/install_config.go +++ b/internal/bootstrap/gcp/install_config.go @@ -363,8 +363,9 @@ func (b *GCPBootstrapper) UpdateInstallConfig() error { } } - b.Env.InstallConfig.Codesphere.Experiments = b.Env.Experiments - b.Env.InstallConfig.Codesphere.Features = b.Env.FeatureFlags + b.Env.InstallConfig.Codesphere.Internal = b.Env.InternalFlags + b.Env.InstallConfig.Codesphere.Preview = util.StringSliceToBoolMap(b.Env.PreviewFlags) + b.Env.InstallConfig.Codesphere.Features = util.StringSliceToBoolMap(b.Env.FeatureFlags) b.applyExternalLokiConfig() b.applyPrometheusRemoteWriteConfig() diff --git a/internal/bootstrap/gcp/install_config_test.go b/internal/bootstrap/gcp/install_config_test.go index ddfd6953..7d8212e1 100644 --- a/internal/bootstrap/gcp/install_config_test.go +++ b/internal/bootstrap/gcp/install_config_test.go @@ -92,8 +92,9 @@ var _ = Describe("Installconfig & Secrets", func() { DNSZoneName: "test-zone", SSHPublicKeyPath: "key.pub", ProjectID: "pid", - Experiments: gcp.DefaultExperiments, - FeatureFlags: map[string]bool{}, + InternalFlags: gcp.DefaultInternalFlags, + PreviewFlags: gcp.DefaultPreviewFlags, + FeatureFlags: gcp.DefaultFeatureFlags, InstallConfig: &files.RootConfig{ Registry: &files.RegistryConfig{}, Postgres: files.PostgresConfig{ @@ -345,7 +346,8 @@ var _ = Describe("Installconfig & Secrets", func() { Expect(bs.Env.InstallConfig.Datacenter.Name).To(Equal("dev")) Expect(bs.Env.InstallConfig.Codesphere.Domain).To(Equal("cs.example.com")) Expect(bs.Env.InstallConfig.Codesphere.Features).To(Equal(map[string]bool{})) - Expect(bs.Env.InstallConfig.Codesphere.Experiments).To(Equal(gcp.DefaultExperiments)) + Expect(bs.Env.InstallConfig.Codesphere.Internal).To(Equal(gcp.DefaultInternalFlags)) + Expect(bs.Env.InstallConfig.Codesphere.Preview).To(Equal(util.StringSliceToBoolMap(gcp.DefaultPreviewFlags))) expectedInstallURI := "https://github.com/apps/" + bs.Env.GitHubAppName + "/installations/new" Expect(bs.Env.InstallConfig.Codesphere.GitProviders.GitHub.OAuth.InstallationURI).To(Equal(expectedInstallURI)) @@ -379,11 +381,11 @@ var _ = Describe("Installconfig & Secrets", func() { Expect(bs.Env.InstallConfig.Datacenter.Name).To(Equal("staging")) }) - Context("When Experiments are set in CodesphereEnvironment", func() { + Context("When internal flags are set in CodesphereEnvironment", func() { BeforeEach(func() { - csEnv.Experiments = []string{"fake-exp1", "fake-exp2"} + csEnv.InternalFlags = []string{"fake-exp1", "fake-exp2"} }) - It("uses those experiments instead of defaults", func() { + It("uses those internal flags instead of defaults", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) icg.EXPECT().WriteVault("fake-secret", true).Return(nil) @@ -393,12 +395,29 @@ var _ = Describe("Installconfig & Secrets", func() { err := bs.UpdateInstallConfig() Expect(err).NotTo(HaveOccurred()) - Expect(bs.Env.InstallConfig.Codesphere.Experiments).To(Equal(csEnv.Experiments)) + Expect(bs.Env.InstallConfig.Codesphere.Internal).To(Equal(csEnv.InternalFlags)) + }) + }) + Context("When preview flags are set in CodesphereEnvironment", func() { + BeforeEach(func() { + csEnv.PreviewFlags = []string{"fake-preview1", "fake-preview2"} + }) + It("uses those preview flags instead of defaults", func() { + icg.EXPECT().GenerateSecrets().Return(nil) + icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) + icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + + nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() + + err := bs.UpdateInstallConfig() + Expect(err).NotTo(HaveOccurred()) + + Expect(bs.Env.InstallConfig.Codesphere.Preview).To(Equal(util.StringSliceToBoolMap(csEnv.PreviewFlags))) }) }) Context("When feature flags are set in CodesphereEnvironment", func() { BeforeEach(func() { - csEnv.FeatureFlags = map[string]bool{"fake-flag1": true, "fake-flag2": true} + csEnv.FeatureFlags = []string{"fake-flag1", "fake-flag2"} }) It("uses those feature flags", func() { icg.EXPECT().GenerateSecrets().Return(nil) @@ -410,7 +429,7 @@ var _ = Describe("Installconfig & Secrets", func() { err := bs.UpdateInstallConfig() Expect(err).NotTo(HaveOccurred()) - Expect(bs.Env.InstallConfig.Codesphere.Features).To(Equal(csEnv.FeatureFlags)) + Expect(bs.Env.InstallConfig.Codesphere.Features).To(Equal(util.StringSliceToBoolMap(csEnv.FeatureFlags))) }) }) Context("When GitHub App name is not set ", func() { diff --git a/internal/bootstrap/local/local.go b/internal/bootstrap/local/local.go index f7e26aa7..8e0ac35b 100644 --- a/internal/bootstrap/local/local.go +++ b/internal/bootstrap/local/local.go @@ -69,10 +69,11 @@ type LocalBootstrapper struct { } type CodesphereEnvironment struct { - BaseDomain string `json:"base_domain"` - Experiments []string `json:"experiments"` - FeatureFlags map[string]bool `json:"feature_flags"` - Profile string `json:"profile"` + BaseDomain string `json:"base_domain"` + InternalFlags []string `json:"internal"` + PreviewFlags []string `json:"preview"` + FeatureFlags []string `json:"feature_flags"` + Profile string `json:"profile"` // Installer InstallVersion string `json:"install_version"` InstallHash string `json:"install_hash"` @@ -636,8 +637,9 @@ func (b *LocalBootstrapper) UpdateInstallConfig() (err error) { } b.Env.InstallConfig.Codesphere.Plans = bootstrap.DefaultCodespherePlans() - b.Env.InstallConfig.Codesphere.Experiments = b.Env.Experiments - b.Env.InstallConfig.Codesphere.Features = b.Env.FeatureFlags + b.Env.InstallConfig.Codesphere.Internal = b.Env.InternalFlags + b.Env.InstallConfig.Codesphere.Preview = util.StringSliceToBoolMap(b.Env.PreviewFlags) + b.Env.InstallConfig.Codesphere.Features = util.StringSliceToBoolMap(b.Env.FeatureFlags) if !b.Env.ExistingConfigUsed { err := b.icg.GenerateSecrets() diff --git a/internal/installer/config_manager_profile.go b/internal/installer/config_manager_profile.go index fa974ef9..ec4f0b25 100644 --- a/internal/installer/config_manager_profile.go +++ b/internal/installer/config_manager_profile.go @@ -115,8 +115,14 @@ func (g *InstallConfig) applyCommonProperties() { if g.Config.Codesphere.DNSServers == nil { g.Config.Codesphere.DNSServers = []string{"8.8.8.8", "1.1.1.1"} } - if g.Config.Codesphere.Experiments == nil { - g.Config.Codesphere.Experiments = []string{} + if g.Config.Codesphere.Internal == nil { + g.Config.Codesphere.Internal = []string{} + } + if g.Config.Codesphere.Preview == nil { + g.Config.Codesphere.Preview = map[string]bool{} + } + if g.Config.Codesphere.Features == nil { + g.Config.Codesphere.Features = map[string]bool{} } if g.Config.Codesphere.WorkspaceImages == nil { g.Config.Codesphere.WorkspaceImages = &files.WorkspaceImagesConfig{ diff --git a/internal/installer/files/config_yaml.go b/internal/installer/files/config_yaml.go index 4bf0071b..f617c29c 100644 --- a/internal/installer/files/config_yaml.go +++ b/internal/installer/files/config_yaml.go @@ -302,7 +302,8 @@ type CodesphereConfig struct { CertIssuer CertIssuerConfig `yaml:"certIssuer"` CustomDomains CustomDomainsConfig `yaml:"customDomains"` DNSServers []string `yaml:"dnsServers"` - Experiments []string `yaml:"experiments"` + Internal []string `yaml:"internal"` + Preview map[string]bool `yaml:"preview"` Features map[string]bool `yaml:"features"` ExtraCAPem string `yaml:"extraCaPem,omitempty"` ExtraWorkspaceEnvVars map[string]string `yaml:"extraWorkspaceEnvVars,omitempty"` diff --git a/internal/util/map.go b/internal/util/map.go index a06658d7..e8835d03 100644 --- a/internal/util/map.go +++ b/internal/util/map.go @@ -3,6 +3,20 @@ package util +// StringSliceToBoolMap converts a slice of strings into a set-style map where +// every element maps to true. A nil slice yields a nil map; a non-nil (possibly +// empty) slice yields a non-nil map. +func StringSliceToBoolMap(items []string) map[string]bool { + if items == nil { + return nil + } + m := make(map[string]bool, len(items)) + for _, item := range items { + m[item] = true + } + return m +} + // DeepMergeMaps recursively merges two maps of string to any, returning a new merged map. // Values from the src map will overwrite those in dst when there are conflicts, except when both values are maps themselves, // in which case they will be merged recursively. The original dst map is not modified; a new map is returned. diff --git a/internal/util/map_test.go b/internal/util/map_test.go new file mode 100644 index 00000000..3478b88b --- /dev/null +++ b/internal/util/map_test.go @@ -0,0 +1,34 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package util_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/codesphere-cloud/oms/internal/util" +) + +var _ = Describe("StringSliceToBoolMap", func() { + It("maps every element to true", func() { + Expect(util.StringSliceToBoolMap([]string{"a", "b"})).To(Equal(map[string]bool{ + "a": true, + "b": true, + })) + }) + + It("returns nil for a nil slice", func() { + Expect(util.StringSliceToBoolMap(nil)).To(BeNil()) + }) + + It("returns an empty map for an empty slice", func() { + result := util.StringSliceToBoolMap([]string{}) + Expect(result).NotTo(BeNil()) + Expect(result).To(BeEmpty()) + }) + + It("deduplicates repeated elements", func() { + Expect(util.StringSliceToBoolMap([]string{"a", "a"})).To(Equal(map[string]bool{"a": true})) + }) +}) From 957497552094b4a05caf1a9f3748c05ed1684d26 Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Wed, 1 Jul 2026 19:24:23 +0200 Subject: [PATCH 11/12] feat: install docker using apt --- cli/cmd/install_docker.go | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 cli/cmd/install_docker.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 +} From e9ab4206d529960cfd8e879850f58d209a495351 Mon Sep 17 00:00:00 2001 From: joka134 <27293650+joka134@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:27:08 +0000 Subject: [PATCH 12/12] chore(docs): Auto-update docs and licenses Signed-off-by: joka134 <27293650+joka134@users.noreply.github.com> --- docs/oms_install.md | 1 + docs/oms_install_docker.md | 25 +++++++++++++ internal/installer/docker/mocks.go | 58 +++++++++++++++--------------- 3 files changed, 55 insertions(+), 29 deletions(-) create mode 100644 docs/oms_install_docker.md diff --git a/docs/oms_install.md b/docs/oms_install.md index 21543f1d..c65b1ad0 100644 --- a/docs/oms_install.md +++ b/docs/oms_install.md @@ -16,6 +16,7 @@ Install Codesphere and other components like Ceph and PostgreSQL. * [oms](oms.md) - Codesphere Operations Management System (OMS) * [oms install codesphere](oms_install_codesphere.md) - Install a Codesphere instance +* [oms install docker](oms_install_docker.md) - Install Docker on a remote host * [oms install k0s](oms_install_k0s.md) - Install k0s Kubernetes distribution * [oms install openbao](oms_install_openbao.md) - Bootstrap OpenBao with Bank-Vaults Operator and DR backup diff --git a/docs/oms_install_docker.md b/docs/oms_install_docker.md new file mode 100644 index 00000000..7c42e535 --- /dev/null +++ b/docs/oms_install_docker.md @@ -0,0 +1,25 @@ +## oms install docker + +Install Docker on a remote host + +### Synopsis + +Install Docker (if not already present) on a remote host accessed via SSH. + +``` +oms install docker [flags] +``` + +### Options + +``` + -h, --help help for docker + --ssh-host string Remote host IP or hostname (required) + --ssh-port int SSH port on the remote host (default 22) + --ssh-user string SSH username (default "root") +``` + +### SEE ALSO + +* [oms install](oms_install.md) - Install Codesphere and other components + 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 }