From 4bb5272aefe1ec85f675cd7125008b090df8d70b Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Fri, 4 Sep 2026 15:40:28 +0200 Subject: [PATCH 1/4] feat: k0s download flags for airgapped bundle --- cli/cmd/k0s/download_k0s.go | 13 +++--- cli/cmd/k0s/download_k0s_test.go | 4 ++ cli/cmd/k0s/install_k0s.go | 24 ++++++++++- cli/cmd/k0s/install_k0s_test.go | 14 +++++++ internal/installer/download.go | 17 ++++++-- internal/installer/k0s.go | 69 +++++++++++++++++++++++++++----- 6 files changed, 120 insertions(+), 21 deletions(-) diff --git a/cli/cmd/k0s/download_k0s.go b/cli/cmd/k0s/download_k0s.go index cff80a7fd..038c97cf6 100644 --- a/cli/cmd/k0s/download_k0s.go +++ b/cli/cmd/k0s/download_k0s.go @@ -27,9 +27,10 @@ type DownloadK0sCmd struct { type DownloadK0sOpts struct { *util.GlobalOptions - Version string - Force bool - Quiet bool + Version string + Force bool + Quiet bool + AirGapped bool } func (c *DownloadK0sCmd) RunE(_ *cobra.Command, args []string) error { @@ -66,6 +67,7 @@ func AddDownloadCmd(download *cobra.Command, opts *util.GlobalOptions) { k0s.cmd.Flags().StringVarP(&k0s.Opts.Version, "version", "v", "", "Version of k0s to download") k0s.cmd.Flags().BoolVarP(&k0s.Opts.Force, "force", "f", false, "Force download even if k0s binary exists") k0s.cmd.Flags().BoolVarP(&k0s.Opts.Quiet, "quiet", "q", false, "Suppress progress output during download") + k0s.cmd.Flags().BoolVarP(&k0s.Opts.AirGapped, "airgapped", "a", false, "Downloads the airgapped bundle for that version") util.AddCmd(download, k0s.cmd) @@ -74,6 +76,7 @@ func AddDownloadCmd(download *cobra.Command, opts *util.GlobalOptions) { func (c *DownloadK0sCmd) DownloadK0s(k0s installer.K0sManager) error { version := c.Opts.Version + var err error if version == "" { version, err = k0s.GetLatestVersion() @@ -82,12 +85,12 @@ func (c *DownloadK0sCmd) DownloadK0s(k0s installer.K0sManager) error { } } - k0sPath, err := k0s.Download(version, c.Opts.Force, c.Opts.Quiet) + k0sPath, err := k0s.Download(version, c.Opts.Force, c.Opts.Quiet, c.Opts.AirGapped) if err != nil { return fmt.Errorf("failed to download k0s: %w", err) } - log.Printf("k0s binary downloaded successfully at '%s'", k0sPath) + log.Printf("k0s binary downloaded successfully to '%s'", k0sPath) return nil } diff --git a/cli/cmd/k0s/download_k0s_test.go b/cli/cmd/k0s/download_k0s_test.go index bdb3fc4a0..8a213c063 100644 --- a/cli/cmd/k0s/download_k0s_test.go +++ b/cli/cmd/k0s/download_k0s_test.go @@ -52,6 +52,7 @@ var _ = Describe("DownloadK0sk0s", func() { mockK0sManager := installer.NewMockK0sManager(GinkgoT()) c.Opts.Version = "" // Test auto-version detection + mockK0sManager.EXPECT().GetLatestVersion().Return("", errors.New("network error")) err := c.DownloadK0s(mockK0sManager) @@ -64,6 +65,7 @@ var _ = Describe("DownloadK0sk0s", func() { mockK0sManager := installer.NewMockK0sManager(GinkgoT()) c.Opts.Version = "v1.29.1+k0s.0" + mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", false, false).Return("", errors.New("download failed")) err := c.DownloadK0s(mockK0sManager) @@ -76,6 +78,7 @@ var _ = Describe("DownloadK0sk0s", func() { mockK0sManager := installer.NewMockK0sManager(GinkgoT()) c.Opts.Version = "v1.29.1+k0s.0" + mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", false, false).Return("/test/workdir/k0s", nil) err := c.DownloadK0s(mockK0sManager) @@ -88,6 +91,7 @@ var _ = Describe("DownloadK0sk0s", func() { c.Opts.Version = "" // Test auto-version detection c.Opts.Force = true c.Opts.Quiet = true + mockK0sManager.EXPECT().GetLatestVersion().Return("v1.29.1+k0s.0", nil) mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", true, true).Return("/test/workdir/k0s", nil) diff --git a/cli/cmd/k0s/install_k0s.go b/cli/cmd/k0s/install_k0s.go index f67257fcd..87c0fa255 100644 --- a/cli/cmd/k0s/install_k0s.go +++ b/cli/cmd/k0s/install_k0s.go @@ -21,7 +21,7 @@ import ( intutil "github.com/codesphere-cloud/oms/internal/util" ) -// InstallK0sCmd represents the k0s download command +// InstallK0sCmd represents the k0s command type InstallK0sCmd struct { cmd *cobra.Command Opts InstallK0sOpts @@ -29,6 +29,7 @@ type InstallK0sCmd struct { FileWriter intutil.FileIO } +// InstallK0sOpts holds the flags of the k0s install command type InstallK0sOpts struct { *util.GlobalOptions Version string @@ -43,6 +44,7 @@ type InstallK0sOpts struct { VaultType string } +// RunE is the starting point for the k0s install command that executes the bootstrap logic. func (c *InstallK0sCmd) RunE(_ *cobra.Command, args []string) error { hw := portal.NewHttpWrapper() env := c.Env @@ -53,6 +55,7 @@ func (c *InstallK0sCmd) RunE(_ *cobra.Command, args []string) error { return c.InstallK0s(pm, k0s, k0sctl) } +// AddInstallCmd registers the k0s install command in the parent command func AddInstallCmd(install *cobra.Command, opts *util.GlobalOptions) { k0s := InstallK0sCmd{ cmd: &cobra.Command{ @@ -79,6 +82,7 @@ func AddInstallCmd(install *cobra.Command, opts *util.GlobalOptions) { Env: env.NewEnv(), FileWriter: intutil.NewFilesystemWriter(), } + k0s.cmd.Flags().StringVarP(&k0s.Opts.Version, "version", "v", installer.DefaultK0sVersion, "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-lite.tar.gz) to load k0s from") @@ -103,6 +107,8 @@ const ( vaultSecretNameKubeconfig = "kubeConfig" ) +// InstallK0s generates a k0sctl config based on the input parameters and installs k0s +// Returns an error if installation fails func (c *InstallK0sCmd) InstallK0s(pm installer.PackageManager, k0s installer.K0sManager, k0sctl installer.K0sctlManager) error { if err := c.FileWriter.MkdirAll(c.Env.GetOmsWorkdir(), 0755); err != nil { return fmt.Errorf("failed to create oms workdir: %w", err) @@ -163,12 +169,15 @@ func (c *InstallK0sCmd) determineK0sVersion(k0s installer.K0sManager) (string, e k0sVersion := c.Opts.Version if k0sVersion == "" { var err error + k0sVersion, err = k0s.GetLatestVersion() if err != nil { return "", fmt.Errorf("failed to get latest k0s version: %w", err) } + log.Printf("Using latest k0s version: %s", k0sVersion) } + return k0sVersion, nil } @@ -181,27 +190,32 @@ func (c *InstallK0sCmd) getK0sBinaryPath(pm installer.PackageManager, k0s instal if err := pm.ExtractDependency(defaultK0sPath, c.Opts.Force); err != nil { return "", fmt.Errorf("failed to extract k0s from package: %w", err) } + return pm.GetDependencyPath(defaultK0sPath), nil } - k0sBinaryPath, err := k0s.Download(k0sVersion, c.Opts.Force, false) + k0sBinaryPath, err := k0s.Download(k0sVersion, c.Opts.Force, false, false) if err != nil { return "", fmt.Errorf("failed to download k0s: %w", err) } + return k0sBinaryPath, nil } func (c *InstallK0sCmd) downloadK0sctl(k0sctl installer.K0sctlManager) (string, error) { log.Println("Downloading k0sctl...") + k0sctlPath, err := k0sctl.Download(c.Opts.K0sctlVersion, c.Opts.Force, false) if err != nil { return "", fmt.Errorf("failed to download k0sctl: %w", err) } + return k0sctlPath, nil } func (c *InstallK0sCmd) generateK0sctlConfig(config *files.RootConfig, k0sVersion string, k0sBinaryPath string) (string, error) { log.Println("Generating k0sctl configuration from install-config...") + k0sctlConfig, err := installer.GenerateK0sctlConfig(config, k0sVersion, c.Opts.SSHKeyPath, k0sBinaryPath) if err != nil { return "", fmt.Errorf("failed to generate k0sctl config: %w", err) @@ -218,11 +232,13 @@ func (c *InstallK0sCmd) generateK0sctlConfig(config *files.RootConfig, k0sVersio } log.Printf("Generated k0sctl configuration at %s", k0sctlConfigPath) + return k0sctlConfigPath, nil } func (c *InstallK0sCmd) deployK0sCluster(k0sctl installer.K0sctlManager, k0sctlPath string, k0sctlConfigPath string) error { log.Println("Applying k0sctl configuration to deploy k0s cluster...") + if err := k0sctl.Apply(k0sctlConfigPath, k0sctlPath, c.Opts.Force); err != nil { return fmt.Errorf("failed to apply k0sctl config: %w", err) } @@ -235,10 +251,12 @@ func (c *InstallK0sCmd) deployK0sCluster(k0sctl installer.K0sctlManager, k0sctlP func (c *InstallK0sCmd) saveKubeconfigToVault(k0sctl installer.K0sctlManager, k0sctlConfigPath, k0sctlPath string) error { log.Println("Retrieving kubeconfig from k0sctl for vault...") + kubeconfigContent, err := k0sctl.GetKubeconfig(k0sctlConfigPath, k0sctlPath) if err != nil { return fmt.Errorf("failed to retrieve kubeconfig from k0sctl: %w", err) } + kubeconfigContent = strings.TrimRight(kubeconfigContent, "\n\r") vault, err := c.loadOrCreateVault() @@ -271,6 +289,7 @@ func (c *InstallK0sCmd) saveKubeconfigToVault(k0sctl installer.K0sctlManager, k0 } log.Printf("Saved kubeconfig to %s", c.Opts.Vault) + return nil } @@ -293,5 +312,6 @@ func (c *InstallK0sCmd) vaultStore() (vault.Vault, error) { if err != nil { return nil, fmt.Errorf("failed to load vault: %w", err) } + return vault, nil } diff --git a/cli/cmd/k0s/install_k0s_test.go b/cli/cmd/k0s/install_k0s_test.go index 7a4bf2c03..76b5655c0 100644 --- a/cli/cmd/k0s/install_k0s_test.go +++ b/cli/cmd/k0s/install_k0s_test.go @@ -64,6 +64,7 @@ var _ = Describe("InstallK0sCmd", func() { Context("RunE method", func() { It("fails when install-config is not provided", func() { c.Opts.InstallConfig = "" + mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir").Times(2) mockFileWriter.EXPECT().MkdirAll("/test/workdir", os.FileMode(0755)).Return(nil) @@ -85,7 +86,9 @@ var _ = Describe("InstallK0sCmd", func() { mockPM = installer.NewMockPackageManager(GinkgoT()) mockK0s = installer.NewMockK0sManager(GinkgoT()) mockK0sctl = installer.NewMockK0sctlManager(GinkgoT()) + var err error + tempDir, err = os.MkdirTemp("", "install-k0s-test-*") Expect(err).NotTo(HaveOccurred()) }) @@ -94,6 +97,7 @@ var _ = Describe("InstallK0sCmd", func() { mockPM.AssertExpectations(GinkgoT()) mockK0s.AssertExpectations(GinkgoT()) mockK0sctl.AssertExpectations(GinkgoT()) + if tempDir != "" { _ = os.RemoveAll(tempDir) } @@ -134,6 +138,7 @@ var _ = Describe("InstallK0sCmd", func() { Expect(err).NotTo(HaveOccurred()) err = os.WriteFile(configPath, configData, 0644) Expect(err).NotTo(HaveOccurred()) + return configPath } @@ -254,6 +259,7 @@ var _ = Describe("InstallK0sCmd", func() { if !testutil.SopsAndAgeAvailable() { Skip("sops and age-keygen not available") } + c.FileWriter = intutil.NewFilesystemWriter() }) @@ -278,6 +284,7 @@ var _ = Describe("InstallK0sCmd", func() { Expect(err).NotTo(HaveOccurred()) loaded, err := backend.Load() Expect(err).NotTo(HaveOccurred()) + secret := loaded.GetSecret(files.SecretKubeConfig) Expect(secret).NotTo(BeNil()) Expect(secret.File.Content).To(Equal("apiVersion: v1\nkind: Config")) @@ -313,6 +320,7 @@ var _ = Describe("InstallK0sCmd", func() { vaultYAML, err := existingVault.Marshal() Expect(err).NotTo(HaveOccurred()) + plainPath := c.Opts.Vault + ".plain" err = os.WriteFile(plainPath, vaultYAML, 0600) Expect(err).NotTo(HaveOccurred()) @@ -365,6 +373,7 @@ var _ = Describe("InstallK0sCmd", func() { } vaultYAML, err := existingVault.Marshal() Expect(err).NotTo(HaveOccurred()) + plainPath := c.Opts.Vault + ".plain" err = os.WriteFile(plainPath, vaultYAML, 0600) Expect(err).NotTo(HaveOccurred()) @@ -382,6 +391,7 @@ var _ = Describe("InstallK0sCmd", func() { Expect(err).NotTo(HaveOccurred()) loaded, err := backend.Load() Expect(err).NotTo(HaveOccurred()) + secret := loaded.GetSecret(files.SecretKubeConfig) Expect(secret).NotTo(BeNil()) Expect(secret.File.Content).To(Equal("apiVersion: v1\nkind: Config\nnew: true")) @@ -411,6 +421,7 @@ var _ = Describe("InstallK0sCmd", func() { Expect(err).NotTo(HaveOccurred()) loaded, err := backend.Load() Expect(err).NotTo(HaveOccurred()) + secret := loaded.GetSecret(files.SecretKubeConfig) Expect(secret).NotTo(BeNil()) Expect(secret.File.Content).To(Equal("apiVersion: v1\nkind: Config")) @@ -459,6 +470,7 @@ var _ = Describe("InstallK0sCmd", func() { } vaultYAML, err := existingVault.Marshal() Expect(err).NotTo(HaveOccurred()) + plainPath := vaultPath + ".plain" err = os.WriteFile(plainPath, vaultYAML, 0600) Expect(err).NotTo(HaveOccurred()) @@ -513,6 +525,7 @@ var _ = Describe("InstallK0sCmd", func() { } vaultYAML, err := existingVault.Marshal() Expect(err).NotTo(HaveOccurred()) + plainPath := vaultPath + ".plain" err = os.WriteFile(plainPath, vaultYAML, 0600) Expect(err).NotTo(HaveOccurred()) @@ -577,6 +590,7 @@ var _ = Describe("InstallK0sCmd", func() { } vaultYAML, err := existingVault.Marshal() Expect(err).NotTo(HaveOccurred()) + plainPath := vaultPath + ".plain" err = os.WriteFile(plainPath, vaultYAML, 0600) Expect(err).NotTo(HaveOccurred()) diff --git a/internal/installer/download.go b/internal/installer/download.go index bb46b3d92..94c12e58a 100644 --- a/internal/installer/download.go +++ b/internal/installer/download.go @@ -11,15 +11,24 @@ import ( "github.com/codesphere-cloud/oms/internal/util" ) -func downloadBinaryToPath(fw util.FileIO, http portal.Http, binaryPath, binaryName, downloadURL string, quiet bool) (string, error) { - dstFile, err := fw.Create(binaryPath) +func downloadToPath(fw util.FileIO, http portal.Http, path, downloadURL string, quiet bool) error { + dstFile, err := fw.Create(path) if err != nil { - return "", fmt.Errorf("failed to create %s binary file: %w", binaryName, err) + return fmt.Errorf("failed to create file: %s: %w", path, err) } defer util.CloseFileIgnoreError(dstFile) if err := http.Download(downloadURL, dstFile, quiet); err != nil { - return "", fmt.Errorf("failed to download %s binary: %w", binaryName, err) + return fmt.Errorf("failed to download %s: %w", path, err) + } + + return nil +} + +func downloadBinaryToPath(fw util.FileIO, http portal.Http, binaryPath, binaryName, downloadURL string, quiet bool) (string, error) { + err := downloadToPath(fw, http, binaryPath, downloadURL, quiet) + if err != nil { + return "", fmt.Errorf("failed to download: %w", err) } if err := fw.Chmod(binaryPath, 0755); err != nil { diff --git a/internal/installer/k0s.go b/internal/installer/k0s.go index ed052267b..aa8cf24cf 100644 --- a/internal/installer/k0s.go +++ b/internal/installer/k0s.go @@ -16,14 +16,21 @@ import ( "github.com/codesphere-cloud/oms/internal/util" ) -// DefaultK0sVersion is the currently verified k0s version -// Use of newer versions should work in most cases but can't be guaranteed -const DefaultK0sVersion = "v1.31.14+k0s.0" +const ( + // DefaultK0sVersion is the currently verified k0s version + // Use of newer versions should work in most cases but can't be guaranteed + DefaultK0sVersion = "v1.31.14+k0s.0" + + GitHubReleaseURL = "https://github.com/k0sproject/k0s/releases/download" + + BinaryName = "k0s" + AirgapBundleName = "k0s-airgap-bundle" +) //mockery:generate: true type K0sManager interface { GetLatestVersion() (string, error) - Download(version string, force bool, quiet bool) (string, error) + Download(version string, force bool, quiet bool, airgapped bool) (string, error) } type K0s struct { @@ -59,7 +66,7 @@ func (k *K0s) GetLatestVersion() (string, error) { } // Download downloads the k0s binary for the specified version and saves it to the OMS cache dir. -func (k *K0s) Download(version string, force bool, quiet bool) (string, error) { +func (k *K0s) Download(version string, force, quiet, airgapped bool) (string, error) { if k.Goos != "linux" || k.Goarch != "amd64" { return "", fmt.Errorf("codesphere installation is only supported on Linux amd64. Current platform: %s/%s", k.Goos, k.Goarch) } @@ -75,7 +82,29 @@ func (k *K0s) Download(version string, force bool, quiet bool) (string, error) { return "", fmt.Errorf("failed to create workdir: %w", err) } - cachePath := filepath.Join(cacheDir, "k0s") + path, err := k.downloadBinary(version, cacheDir, force, quiet) + if err != nil { + return "", fmt.Errorf("failed to download k0s binary: %w", err) + } + + if airgapped { + err = k.downloadAirgappedBundle(version, cacheDir, force, quiet) + if err != nil { + return "", fmt.Errorf("failed to download k0s-airgapped bundle: %w", err) + } + } + + log.Printf("k0s binary downloaded and made executable at '%s'", path) + + return path, nil +} + +// downloadBinary fetches the k0s binary for the given version from the k0s GitHub +// releases and stores it as "k0s" in cacheDir, returning the path to it. +// If a binary is already cached and force is false, the cached binary is reused as +// long as its version matches; otherwise it is replaced by a fresh download. +func (k *K0s) downloadBinary(version, cacheDir string, force, quiet bool) (string, error) { + cachePath := filepath.Join(cacheDir, BinaryName) if k.FileWriter.Exists(cachePath) && !force { cachedVersion, versionErr := localBinaryVersion(cachePath) if versionErr == nil && cachedVersion == version { @@ -91,14 +120,34 @@ func (k *K0s) Download(version string, force bool, quiet bool) (string, error) { io.Verbosef(!quiet, "Replacing existing k0s binary: %s", replaceReason) } - downloadURL := fmt.Sprintf("https://github.com/k0sproject/k0s/releases/download/%s/k0s-%s-%s", version, version, k.Goarch) + downloadURL := fmt.Sprintf("%s/%s/k0s-%s-%s", GitHubReleaseURL, version, version, k.Goarch) - path, err := downloadBinaryToPath(k.FileWriter, k.Http, cachePath, "k0s", downloadURL, quiet) + path, err := downloadBinaryToPath(k.FileWriter, k.Http, cachePath, BinaryName, downloadURL, quiet) if err != nil { return "", err } - log.Printf("k0s binary downloaded and made executable at '%s'", path) + return path, err +} - return path, nil +// downloadAirgappedBundle fetches the k0s airgap image bundle for the given version +// from the k0s GitHub releases and stores it as tar in cacheDir +// If a bundle for that version is already cached and force is false, the cached bundle +// is reused; otherwise it is replaced by a fresh download. +func (k *K0s) downloadAirgappedBundle(version, cacheDir string, force, quiet bool) error { + bundleName := fmt.Sprintf("%s-%s-%s-%s.tar", AirgapBundleName, version, k.Goos, k.Goarch) + cachePath := filepath.Join(cacheDir, bundleName) + if k.FileWriter.Exists(cachePath) && !force { + io.Verbosef(!quiet, "Using cached %s at %s", bundleName, cachePath) + return nil + } + + downloadURL := fmt.Sprintf("%s/%s/%s", GitHubReleaseURL, version, bundleName) + + err := downloadToPath(k.FileWriter, k.Http, cachePath, downloadURL, quiet) + if err != nil { + return err + } + + return err } From 3c00db74f1c32c1b26cccfc88a0f2f0054fa632e Mon Sep 17 00:00:00 2001 From: joka134 <27293650+joka134@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:43:06 +0000 Subject: [PATCH 2/4] chore(docs): Auto-update docs and licenses Signed-off-by: joka134 <27293650+joka134@users.noreply.github.com> --- docs/oms_download_k0s.md | 1 + internal/installer/mocks.go | 30 ++++++++++++++++++------------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/oms_download_k0s.md b/docs/oms_download_k0s.md index b3cbdfc9e..29e480c36 100644 --- a/docs/oms_download_k0s.md +++ b/docs/oms_download_k0s.md @@ -31,6 +31,7 @@ $ oms download k0s --force ### Options ``` + -a, --airgapped Downloads the airgapped bundle for that version -f, --force Force download even if k0s binary exists -h, --help help for k0s -q, --quiet Suppress progress output during download diff --git a/internal/installer/mocks.go b/internal/installer/mocks.go index 6f36a2239..8c6ed992f 100644 --- a/internal/installer/mocks.go +++ b/internal/installer/mocks.go @@ -1208,8 +1208,8 @@ func (_m *MockK0sManager) EXPECT() *MockK0sManager_Expecter { } // Download provides a mock function for the type MockK0sManager -func (_mock *MockK0sManager) Download(version string, force bool, quiet bool) (string, error) { - ret := _mock.Called(version, force, quiet) +func (_mock *MockK0sManager) Download(version string, force bool, quiet bool, airgapped bool) (string, error) { + ret := _mock.Called(version, force, quiet, airgapped) if len(ret) == 0 { panic("no return value specified for Download") @@ -1217,16 +1217,16 @@ func (_mock *MockK0sManager) Download(version string, force bool, quiet bool) (s var r0 string var r1 error - if returnFunc, ok := ret.Get(0).(func(string, bool, bool) (string, error)); ok { - return returnFunc(version, force, quiet) + if returnFunc, ok := ret.Get(0).(func(string, bool, bool, bool) (string, error)); ok { + return returnFunc(version, force, quiet, airgapped) } - if returnFunc, ok := ret.Get(0).(func(string, bool, bool) string); ok { - r0 = returnFunc(version, force, quiet) + if returnFunc, ok := ret.Get(0).(func(string, bool, bool, bool) string); ok { + r0 = returnFunc(version, force, quiet, airgapped) } else { r0 = ret.Get(0).(string) } - if returnFunc, ok := ret.Get(1).(func(string, bool, bool) error); ok { - r1 = returnFunc(version, force, quiet) + if returnFunc, ok := ret.Get(1).(func(string, bool, bool, bool) error); ok { + r1 = returnFunc(version, force, quiet, airgapped) } else { r1 = ret.Error(1) } @@ -1242,11 +1242,12 @@ type MockK0sManager_Download_Call struct { // - version string // - force bool // - quiet bool -func (_e *MockK0sManager_Expecter) Download(version any, force any, quiet any) *MockK0sManager_Download_Call { - return &MockK0sManager_Download_Call{Call: _e.mock.On("Download", version, force, quiet)} +// - airgapped bool +func (_e *MockK0sManager_Expecter) Download(version any, force any, quiet any, airgapped any) *MockK0sManager_Download_Call { + return &MockK0sManager_Download_Call{Call: _e.mock.On("Download", version, force, quiet, airgapped)} } -func (_c *MockK0sManager_Download_Call) Run(run func(version string, force bool, quiet bool)) *MockK0sManager_Download_Call { +func (_c *MockK0sManager_Download_Call) Run(run func(version string, force bool, quiet bool, airgapped bool)) *MockK0sManager_Download_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -1260,10 +1261,15 @@ func (_c *MockK0sManager_Download_Call) Run(run func(version string, force bool, if args[2] != nil { arg2 = args[2].(bool) } + var arg3 bool + if args[3] != nil { + arg3 = args[3].(bool) + } run( arg0, arg1, arg2, + arg3, ) }) return _c @@ -1274,7 +1280,7 @@ func (_c *MockK0sManager_Download_Call) Return(s string, err error) *MockK0sMana return _c } -func (_c *MockK0sManager_Download_Call) RunAndReturn(run func(version string, force bool, quiet bool) (string, error)) *MockK0sManager_Download_Call { +func (_c *MockK0sManager_Download_Call) RunAndReturn(run func(version string, force bool, quiet bool, airgapped bool) (string, error)) *MockK0sManager_Download_Call { _c.Call.Return(run) return _c } From 37cf9bdd4a51ab3bc082a7c2da073db44d8ce445 Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Fri, 4 Sep 2026 16:12:01 +0200 Subject: [PATCH 3/4] fix: tests for k0s --- cli/cmd/k0s/download_k0s_test.go | 6 ++--- cli/cmd/k0s/install_k0s_test.go | 4 +-- internal/installer/k0s.go | 1 + internal/installer/k0s_config.go | 2 ++ internal/installer/k0s_config_test.go | 1 + internal/installer/k0s_test.go | 32 +++++++++++++++--------- internal/installer/k0sctl.go | 9 +++++++ internal/installer/k0sctl_config.go | 1 + internal/installer/k0sctl_config_test.go | 1 + 9 files changed, 40 insertions(+), 17 deletions(-) diff --git a/cli/cmd/k0s/download_k0s_test.go b/cli/cmd/k0s/download_k0s_test.go index 8a213c063..81456d4e0 100644 --- a/cli/cmd/k0s/download_k0s_test.go +++ b/cli/cmd/k0s/download_k0s_test.go @@ -66,7 +66,7 @@ var _ = Describe("DownloadK0sk0s", func() { c.Opts.Version = "v1.29.1+k0s.0" - mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", false, false).Return("", errors.New("download failed")) + mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", false, false, false).Return("", errors.New("download failed")) err := c.DownloadK0s(mockK0sManager) Expect(err).To(HaveOccurred()) @@ -79,7 +79,7 @@ var _ = Describe("DownloadK0sk0s", func() { c.Opts.Version = "v1.29.1+k0s.0" - mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", false, false).Return("/test/workdir/k0s", nil) + mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", false, false, false).Return("/test/workdir/k0s", nil) err := c.DownloadK0s(mockK0sManager) Expect(err).ToNot(HaveOccurred()) @@ -93,7 +93,7 @@ var _ = Describe("DownloadK0sk0s", func() { c.Opts.Quiet = true mockK0sManager.EXPECT().GetLatestVersion().Return("v1.29.1+k0s.0", nil) - mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", true, true).Return("/test/workdir/k0s", nil) + mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", true, true, false).Return("/test/workdir/k0s", nil) err := c.DownloadK0s(mockK0sManager) Expect(err).ToNot(HaveOccurred()) diff --git a/cli/cmd/k0s/install_k0s_test.go b/cli/cmd/k0s/install_k0s_test.go index 76b5655c0..ce3737fe0 100644 --- a/cli/cmd/k0s/install_k0s_test.go +++ b/cli/cmd/k0s/install_k0s_test.go @@ -189,7 +189,7 @@ var _ = Describe("InstallK0sCmd", func() { mockEnv.EXPECT().GetOmsWorkdir().Return(tempDir) mockFileWriter.EXPECT().MkdirAll(tempDir, os.FileMode(0755)).Return(nil) - mockK0s.EXPECT().Download("v1.29.0+k0s.0", false, false).Return("/downloaded/k0s", nil) + mockK0s.EXPECT().Download("v1.29.0+k0s.0", false, false, false).Return("/downloaded/k0s", nil) mockK0sctl.EXPECT().Download("", false, false).Return("/tmp/k0sctl", nil) mockFileWriter.EXPECT().WriteFile(mock.Anything, mock.Anything, mock.Anything).Return(nil) mockK0sctl.EXPECT().Apply(mock.Anything, "/tmp/k0sctl", false).Return(nil) @@ -205,7 +205,7 @@ var _ = Describe("InstallK0sCmd", func() { mockEnv.EXPECT().GetOmsWorkdir().Return(tempDir) mockFileWriter.EXPECT().MkdirAll(tempDir, os.FileMode(0755)).Return(nil) mockK0s.EXPECT().GetLatestVersion().Return("v1.30.0+k0s.0", nil) - mockK0s.EXPECT().Download("v1.30.0+k0s.0", false, false).Return("", os.ErrNotExist) + mockK0s.EXPECT().Download("v1.30.0+k0s.0", false, false, false).Return("", os.ErrNotExist) err := c.InstallK0s(mockPM, mockK0s, mockK0sctl) Expect(err).To(HaveOccurred()) diff --git a/internal/installer/k0s.go b/internal/installer/k0s.go index aa8cf24cf..0f3648bbd 100644 --- a/internal/installer/k0s.go +++ b/internal/installer/k0s.go @@ -136,6 +136,7 @@ func (k *K0s) downloadBinary(version, cacheDir string, force, quiet bool) (strin // is reused; otherwise it is replaced by a fresh download. func (k *K0s) downloadAirgappedBundle(version, cacheDir string, force, quiet bool) error { bundleName := fmt.Sprintf("%s-%s-%s-%s.tar", AirgapBundleName, version, k.Goos, k.Goarch) + cachePath := filepath.Join(cacheDir, bundleName) if k.FileWriter.Exists(cachePath) && !force { io.Verbosef(!quiet, "Using cached %s at %s", bundleName, cachePath) diff --git a/internal/installer/k0s_config.go b/internal/installer/k0s_config.go index 99d6cc592..0213221b4 100644 --- a/internal/installer/k0s_config.go +++ b/internal/installer/k0s_config.go @@ -88,6 +88,7 @@ func GenerateK0sConfig(installConfig *files.RootConfig) (*K0sConfig, error) { for _, cp := range installConfig.Kubernetes.ControlPlanes { sans = append(sans, cp.IPAddress) } + if installConfig.Kubernetes.APIServerHost != "" { sans = append(sans, installConfig.Kubernetes.APIServerHost) } @@ -135,6 +136,7 @@ func defaultIfEmpty(value, defaultValue string) string { if value != "" { return value } + return defaultValue } diff --git a/internal/installer/k0s_config_test.go b/internal/installer/k0s_config_test.go index 41ee8b462..1e2df3d0d 100644 --- a/internal/installer/k0s_config_test.go +++ b/internal/installer/k0s_config_test.go @@ -79,6 +79,7 @@ var _ = Describe("K0sConfig", func() { // Verify it can be unmarshalled back var parsedConfig installer.K0sConfig + err = yaml.Unmarshal(yamlData, &parsedConfig) Expect(err).ToNot(HaveOccurred()) Expect(parsedConfig.Metadata.Name).To(Equal("codesphere-test-dc")) diff --git a/internal/installer/k0s_test.go b/internal/installer/k0s_test.go index f59be7419..b4b20b84b 100644 --- a/internal/installer/k0s_test.go +++ b/internal/installer/k0s_test.go @@ -57,7 +57,7 @@ var _ = Describe("K0s", func() { }) It("implements K0sManager interface", func() { - var manager = installer.NewK0s(mockHttp, mockEnv, mockFileWriter) + manager := installer.NewK0s(mockHttp, mockEnv, mockFileWriter) Expect(manager).ToNot(BeNil()) }) }) @@ -113,7 +113,7 @@ var _ = Describe("K0s", func() { k0sImpl.Goos = "windows" k0sImpl.Goarch = "amd64" - _, err := k0s.Download("v1.29.1+k0s.0", false, false) + _, err := k0s.Download("v1.29.1+k0s.0", false, false, false) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("codesphere installation is only supported on Linux amd64")) Expect(err.Error()).To(ContainSubstring("windows/amd64")) @@ -123,7 +123,7 @@ var _ = Describe("K0s", func() { k0sImpl.Goos = "linux" k0sImpl.Goarch = "arm64" - _, err := k0s.Download("v1.29.1+k0s.0", false, false) + _, err := k0s.Download("v1.29.1+k0s.0", false, false, false) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("codesphere installation is only supported on Linux amd64")) Expect(err.Error()).To(ContainSubstring("linux/arm64")) @@ -148,13 +148,14 @@ var _ = Describe("K0s", func() { // Create a real file for the test realFile, err := os.Create(k0sPath) Expect(err).ToNot(HaveOccurred()) + defer util.CloseFileIgnoreError(realFile) mockFileWriter.EXPECT().Create(k0sPath).Return(realFile, nil) mockHttp.EXPECT().Download("https://github.com/k0sproject/k0s/releases/download/v1.29.1+k0s.0/k0s-v1.29.1+k0s.0-amd64", realFile, false).Return(nil) mockFileWriter.EXPECT().Chmod(k0sPath, os.FileMode(0755)).Return(nil) - path, err := k0s.Download("v1.29.1+k0s.0", false, false) + path, err := k0s.Download("v1.29.1+k0s.0", false, false, false) Expect(err).ToNot(HaveOccurred()) Expect(path).To(Equal(k0sPath)) }) @@ -164,6 +165,7 @@ var _ = Describe("K0s", func() { BeforeEach(func() { k0sImpl.Goos = "linux" k0sImpl.Goarch = "amd64" + mockEnv.EXPECT().GetOmsCacheDir().Return(workDir, nil) mockFileWriter.EXPECT().MkdirAll(workDir, os.FileMode(0755)).Return(nil) }) @@ -175,7 +177,7 @@ var _ = Describe("K0s", func() { Expect(err).ToNot(HaveOccurred()) mockFileWriter.EXPECT().Exists(k0sPath).Return(true) - path, err := k0s.Download("v1.29.1+k0s.0", false, false) + path, err := k0s.Download("v1.29.1+k0s.0", false, false, false) Expect(err).ToNot(HaveOccurred()) Expect(path).To(Equal(k0sPath)) }) @@ -196,7 +198,7 @@ var _ = Describe("K0s", func() { mockHttp.EXPECT().Download("https://github.com/k0sproject/k0s/releases/download/v1.29.1+k0s.0/k0s-v1.29.1+k0s.0-amd64", realFile, false).Return(nil) mockFileWriter.EXPECT().Chmod(k0sPath, os.FileMode(0755)).Return(nil) - path, err := k0s.Download("v1.29.1+k0s.0", false, false) + path, err := k0s.Download("v1.29.1+k0s.0", false, false, false) Expect(err).ToNot(HaveOccurred()) Expect(path).To(Equal(k0sPath)) }) @@ -211,13 +213,14 @@ var _ = Describe("K0s", func() { // Create a real file for the test realFile, err := os.Create(k0sPath) Expect(err).ToNot(HaveOccurred()) + defer util.CloseFileIgnoreError(realFile) mockFileWriter.EXPECT().Create(k0sPath).Return(realFile, nil) mockHttp.EXPECT().Download("https://github.com/k0sproject/k0s/releases/download/v1.29.1+k0s.0/k0s-v1.29.1+k0s.0-amd64", realFile, false).Return(nil) mockFileWriter.EXPECT().Chmod(k0sPath, os.FileMode(0755)).Return(nil) - path, err := k0s.Download("v1.29.1+k0s.0", true, false) + path, err := k0s.Download("v1.29.1+k0s.0", true, false, false) Expect(err).ToNot(HaveOccurred()) Expect(path).To(Equal(k0sPath)) }) @@ -227,6 +230,7 @@ var _ = Describe("K0s", func() { BeforeEach(func() { k0sImpl.Goos = "linux" k0sImpl.Goarch = "amd64" + mockEnv.EXPECT().GetOmsCacheDir().Return(workDir, nil) mockFileWriter.EXPECT().MkdirAll(workDir, os.FileMode(0755)).Return(nil) mockFileWriter.EXPECT().Exists(k0sPath).Return(false) @@ -235,9 +239,9 @@ var _ = Describe("K0s", func() { It("should fail when file creation fails", func() { mockFileWriter.EXPECT().Create(k0sPath).Return(nil, errors.New("permission denied")) - _, err := k0s.Download("v1.29.1+k0s.0", false, false) + _, err := k0s.Download("v1.29.1+k0s.0", false, false, false) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to create k0s binary file")) + Expect(err.Error()).To(ContainSubstring("failed to download k0s binary")) Expect(err.Error()).To(ContainSubstring("permission denied")) }) @@ -245,6 +249,7 @@ var _ = Describe("K0s", func() { // Create a mock file for the test mockFile, err := os.CreateTemp("", "k0s-test") Expect(err).ToNot(HaveOccurred()) + defer func() { _ = os.Remove(mockFile.Name()) }() @@ -253,7 +258,7 @@ var _ = Describe("K0s", func() { mockFileWriter.EXPECT().Create(k0sPath).Return(mockFile, nil) mockHttp.EXPECT().Download("https://github.com/k0sproject/k0s/releases/download/v1.29.1+k0s.0/k0s-v1.29.1+k0s.0-amd64", mockFile, false).Return(errors.New("download failed")) - _, err = k0s.Download("v1.29.1+k0s.0", false, false) + _, err = k0s.Download("v1.29.1+k0s.0", false, false, false) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to download k0s binary")) Expect(err.Error()).To(ContainSubstring("download failed")) @@ -266,13 +271,14 @@ var _ = Describe("K0s", func() { realFile, err := os.Create(k0sPath) Expect(err).ToNot(HaveOccurred()) + defer util.CloseFileIgnoreError(realFile) mockFileWriter.EXPECT().Create(k0sPath).Return(realFile, nil) mockHttp.EXPECT().Download("https://github.com/k0sproject/k0s/releases/download/v1.29.1+k0s.0/k0s-v1.29.1+k0s.0-amd64", realFile, false).Return(nil) mockFileWriter.EXPECT().Chmod(k0sPath, os.FileMode(0755)).Return(nil) - path, err := k0s.Download("v1.29.1+k0s.0", false, false) + path, err := k0s.Download("v1.29.1+k0s.0", false, false, false) Expect(err).ToNot(HaveOccurred()) Expect(path).To(Equal(k0sPath)) }) @@ -281,6 +287,7 @@ var _ = Describe("K0s", func() { Context("URL construction", func() { BeforeEach(func() { k0sImpl.Goos = "linux" + mockEnv.EXPECT().GetOmsCacheDir().Return(workDir, nil) mockFileWriter.EXPECT().Exists(k0sPath).Return(false) }) @@ -297,13 +304,14 @@ var _ = Describe("K0s", func() { // Create a real file for the test realFile, err := os.Create(k0sPath) Expect(err).ToNot(HaveOccurred()) + defer util.CloseFileIgnoreError(realFile) mockFileWriter.EXPECT().Create(k0sPath).Return(realFile, nil) mockHttp.EXPECT().Download("https://github.com/k0sproject/k0s/releases/download/v1.29.1+k0s.0/k0s-v1.29.1+k0s.0-amd64", realFile, false).Return(nil) mockFileWriter.EXPECT().Chmod(k0sPath, os.FileMode(0755)).Return(nil) - path, err := k0s.Download("v1.29.1+k0s.0", false, false) + path, err := k0s.Download("v1.29.1+k0s.0", false, false, false) Expect(err).ToNot(HaveOccurred()) Expect(path).To(Equal(k0sPath)) }) diff --git a/internal/installer/k0sctl.go b/internal/installer/k0sctl.go index 7ff1e677e..a49ccd352 100644 --- a/internal/installer/k0sctl.go +++ b/internal/installer/k0sctl.go @@ -50,6 +50,7 @@ type githubRelease struct { func (k *K0sctl) GetLatestVersion() (string, error) { releaseURL := "https://api.github.com/repos/k0sproject/k0sctl/releases/latest" + responseBody, err := k.Http.Get(releaseURL) if err != nil { return "", fmt.Errorf("failed to fetch latest k0sctl release: %w", err) @@ -79,10 +80,12 @@ func (k *K0sctl) Download(version string, force bool, quiet bool) (string, error if version == "" { var err error + version, err = k.GetLatestVersion() if err != nil { return "", fmt.Errorf("failed to get latest version: %w", err) } + io.Verbosef(!quiet, "Using latest k0sctl version: %s", version) } @@ -127,9 +130,11 @@ func (k *K0sctl) requireBinaryAndConfig(configPath, k0sctlPath string) error { if !k.FileWriter.Exists(k0sctlPath) { return fmt.Errorf("k0sctl binary does not exist at '%s', please download first", k0sctlPath) } + if !k.FileWriter.Exists(configPath) { return fmt.Errorf("k0sctl config does not exist at '%s'", configPath) } + return nil } @@ -155,6 +160,7 @@ func (k *K0sctl) Apply(configPath string, k0sctlPath string, force bool) error { } log.Println("k0sctl apply completed successfully") + return nil } @@ -162,6 +168,7 @@ func (k *K0sctl) Reset(configPath string, k0sctlPath string) error { if !k.FileWriter.Exists(k0sctlPath) { return nil } + if err := k.requireBinaryAndConfig(configPath, k0sctlPath); err != nil { return err } @@ -176,6 +183,7 @@ func (k *K0sctl) Reset(configPath string, k0sctlPath string) error { } log.Println("k0sctl reset completed successfully") + return nil } @@ -187,6 +195,7 @@ func (k *K0sctl) GetKubeconfig(configPath string, k0sctlPath string) (string, er args := []string{"kubeconfig", "--config", configPath} log.Println("Retrieving kubeconfig from k0sctl...") + output, err := util.RunCommandWithOutput(k0sctlPath, args, "") if err != nil { return "", fmt.Errorf("k0sctl kubeconfig failed: %w", err) diff --git a/internal/installer/k0sctl_config.go b/internal/installer/k0sctl_config.go index 30791b0d6..69cb2c14a 100644 --- a/internal/installer/k0sctl_config.go +++ b/internal/installer/k0sctl_config.go @@ -75,6 +75,7 @@ func (k *K0sctlSpec) addUniqueK0sctlHost(node files.K8sNode, role string, instal return } } + host := K0sctlHost{ Role: role, SSH: K0sctlSSH{ diff --git a/internal/installer/k0sctl_config_test.go b/internal/installer/k0sctl_config_test.go index 55ca856a3..97c099a2f 100644 --- a/internal/installer/k0sctl_config_test.go +++ b/internal/installer/k0sctl_config_test.go @@ -150,6 +150,7 @@ var _ = Describe("K0sctlConfig", func() { // Verify it can be unmarshalled back var parsedConfig installer.K0sctlConfig + err = yaml.Unmarshal(yamlData, &parsedConfig) Expect(err).ToNot(HaveOccurred()) Expect(parsedConfig.Metadata.Name).To(Equal("codesphere-test-dc")) From 0b6f30c5a1703a0cff5d953008298e8efb89d30f Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Fri, 4 Sep 2026 16:17:44 +0200 Subject: [PATCH 4/4] fix: tests for k0s --- internal/installer/k0s.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/installer/k0s.go b/internal/installer/k0s.go index 0f3648bbd..b20712e9c 100644 --- a/internal/installer/k0s.go +++ b/internal/installer/k0s.go @@ -21,9 +21,13 @@ const ( // Use of newer versions should work in most cases but can't be guaranteed DefaultK0sVersion = "v1.31.14+k0s.0" + // GitHubReleaseURL is the github release page for k0s GitHubReleaseURL = "https://github.com/k0sproject/k0s/releases/download" - BinaryName = "k0s" + // BinaryName is the name of target binary for oms to download to + BinaryName = "k0s" + + // AirgapBundleName is the name of target airgap-bundle for oms to download to AirgapBundleName = "k0s-airgap-bundle" )