Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4bb5272
feat: k0s download flags for airgapped bundle
joka134 Sep 4, 2026
3c00db7
chore(docs): Auto-update docs and licenses
joka134 Sep 4, 2026
37cf9bd
fix: tests for k0s
joka134 Sep 4, 2026
0b6f30c
fix: tests for k0s
joka134 Sep 4, 2026
f7865ee
feat: Add support for airgapped installations of k0s
OliverTrautvetter Sep 17, 2026
223d6cb
chore(docs): Auto-update docs and licenses
joka134 Sep 17, 2026
13a0202
ref: k0sctl installation logic and configuration handling
OliverTrautvetter Sep 21, 2026
eb9a226
Merge remote-tracking branch 'origin/main' into feat/k0s-download-air…
OliverTrautvetter Sep 22, 2026
3fc99a5
fix(lint): document exported K0sctlFile type
OliverTrautvetter Sep 22, 2026
559e465
fix(download): remove partial downloads on failure and update tests
OliverTrautvetter Sep 23, 2026
09409a3
ref: streamline k0s and k0sctl download logic and improve airgap hand…
OliverTrautvetter Sep 23, 2026
a598875
ref: streamline airgap handling and improve download options
OliverTrautvetter Sep 25, 2026
4111694
fix: comment
OliverTrautvetter Sep 29, 2026
2f843e8
ref: cleanup
OliverTrautvetter Sep 29, 2026
b841fd5
feat: add airgap support for k0s installation and configuration gener…
OliverTrautvetter Oct 2, 2026
830dbf7
feat: implement SSH agent forwarding and key management for Node
OliverTrautvetter Oct 2, 2026
08f7c9b
Merge branch 'main' into feat/k0s-download-airgapped-bundle
OliverTrautvetter Oct 2, 2026
b41a88c
chore(docs): Auto-update docs and licenses
OliverTrautvetter Oct 2, 2026
895ac8b
chore(docs): Auto-update docs and licenses
OliverTrautvetter Oct 2, 2026
aeb4ca4
chore(docs): Auto-update docs and licenses
OliverTrautvetter Oct 2, 2026
9502e01
chore(docs): Auto-update docs and licenses
OliverTrautvetter Oct 2, 2026
bc6e20d
chore(docs): Auto-update docs and licenses
OliverTrautvetter Oct 2, 2026
af7f3d0
chore(docs): Auto-update docs and licenses
OliverTrautvetter Oct 2, 2026
194e06c
ref: cleanup
OliverTrautvetter Oct 2, 2026
c67df95
Merge branch 'feat/k0s-download-airgapped-bundle' of https://github.c…
OliverTrautvetter Oct 2, 2026
03977c8
ref: update airgap handling and configuration options in k0s installa…
OliverTrautvetter Oct 2, 2026
72f7d57
chore(docs): Auto-update docs and licenses
OliverTrautvetter Oct 2, 2026
300d961
fix: lint errors
OliverTrautvetter Oct 2, 2026
9983ab2
Merge branch 'feat/k0s-download-airgapped-bundle' of https://github.c…
OliverTrautvetter Oct 2, 2026
04a836c
chore(docs): Auto-update docs and licenses
OliverTrautvetter Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ License URL: https://github.com/beorn7/perks/blob/v1.0.1/LICENSE
Module: github.com/blang/semver/v4
Version: v4.0.0
License: MIT
License URL: https://github.com/blang/semver/blob/v4.0.0/v4/LICENSE
License URL: https://github.com/blang/semver/blob/v4.0.0/LICENSE

----------
Module: github.com/bmatcuk/doublestar/v4
Expand Down Expand Up @@ -397,7 +397,7 @@ License URL: https://github.com/emirpasic/gods/blob/v1.18.1/LICENSE
Module: github.com/evanphx/json-patch/v5
Version: v5.9.11
License: BSD-3-Clause
License URL: https://github.com/evanphx/json-patch/blob/v5.9.11/v5/LICENSE
License URL: https://github.com/evanphx/json-patch/blob/v5.9.11/LICENSE

----------
Module: github.com/exponent-io/jsonpath
Expand Down Expand Up @@ -721,7 +721,7 @@ License URL: https://github.com/googleapis/enterprise-certificate-proxy/blob/v0.
Module: github.com/googleapis/gax-go/v2
Version: v2.26.2
License: BSD-3-Clause
License URL: https://github.com/googleapis/gax-go/blob/v2.26.2/v2/LICENSE
License URL: https://github.com/googleapis/gax-go/blob/v2.26.2/LICENSE

----------
Module: github.com/gorilla/websocket
Expand Down Expand Up @@ -1507,7 +1507,7 @@ License URL: https://cs.opensource.google/go/x/time/+/v0.16.0:LICENSE
Module: gomodules.xyz/jsonpatch/v2
Version: v2.5.0
License: Apache-2.0
License URL: https://github.com/gomodules/jsonpatch/blob/v2.5.0/v2/LICENSE
License URL: https://github.com/gomodules/jsonpatch/blob/v2.5.0/LICENSE

----------
Module: google.golang.org/api
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/bootstrap_gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *util.GlobalOptions) {
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.DNSZoneName, "dns-zone-name", "oms-testing", "Cloud DNS Zone Name (optional)")
flags.BoolVar(&bootstrapGcpCmd.CodesphereEnv.Preemptible, "preemptible", false, "Use preemptible VMs for Codesphere infrastructure. Mutually exclusive with --spot-vms (default: false)")
flags.BoolVar(&bootstrapGcpCmd.CodesphereEnv.SpotVMs, "spot-vms", false, "Use Spot VMs for Codesphere infrastructure. Falls back to standard VMs if spot capacity unavailable. Mutually exclusive with --preemptible (default: false)")
flags.BoolVar(&bootstrapGcpCmd.CodesphereEnv.Airgapped, "airgapped", false, "Install k0s from the k0s airgap image bundle, so the cluster nodes pull no k0s images from the internet (default: false)")
flags.Int64Var(&bootstrapGcpCmd.CodesphereEnv.RootDiskSize, "root-disk-size", 50, "Instance root disk size in GB (default: 50)")

flags.BoolVar(&bootstrapGcpCmd.CodesphereEnv.WriteConfig, "write-config", true, "Write generated install config to file (default: true)")
Expand Down
35 changes: 20 additions & 15 deletions cli/cmd/k0s/download_k0s.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,15 @@ type DownloadK0sOpts struct {
*util.GlobalOptions
Version string
Force bool
Airgap bool
}

func (c *DownloadK0sCmd) RunE(_ *cobra.Command, args []string) error {
hw := portal.NewHttpWrapper()
env := c.Env
k0s := installer.NewK0s(hw, env, c.FileWriter)

err := c.DownloadK0s(k0s)
if err != nil {
return fmt.Errorf("failed to download k0s: %w", err)
}

return nil
return c.DownloadK0s(k0s)
}

func AddDownloadCmd(download *cobra.Command, opts *util.GlobalOptions) {
Expand All @@ -55,6 +51,7 @@ func AddDownloadCmd(download *cobra.Command, opts *util.GlobalOptions) {
{Cmd: "", Desc: "Download k0s using the Go-native implementation"},
{Cmd: "--version 1.22.0", Desc: "Download a specific version of k0s"},
{Cmd: "--force", Desc: "Force download even if k0s binary exists"},
{Cmd: "--airgapped", Desc: "Also download the airgap image bundle for that version"},
}),
},
Opts: DownloadK0sOpts{GlobalOptions: opts},
Expand All @@ -63,28 +60,36 @@ 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.Airgap, "airgapped", "a", false, "Downloads the airgapped bundle for that version")

util.AddCmd(download, k0s.cmd)

k0s.cmd.RunE = k0s.RunE
}

func (c *DownloadK0sCmd) DownloadK0s(k0s installer.K0sManager) error {
version := c.Opts.Version
var err error
if version == "" {
version, err = k0s.GetLatestVersion()
if err != nil {
return fmt.Errorf("failed to get latest k0s version: %w", err)
}
version, err := resolveK0sVersion(k0s, c.Opts.Version)
if err != nil {
return err
}

k0sPath, err := k0s.Download(version, c.Opts.Force, !c.Opts.Verbose)
opts := installer.DownloadOptions{Force: c.Opts.Force, Quiet: !c.Opts.Verbose}

k0sPath, err := k0s.Download(version, opts)
if err != nil {
return fmt.Errorf("failed to download k0s: %w", err)
}

log.Printf("k0s binary downloaded successfully at '%s'", k0sPath)
if c.Opts.Airgap {
bundlePath, err := k0s.EnsureAirgapBundle(version, opts)
if err != nil {
return fmt.Errorf("failed to download k0s airgap bundle: %w", err)
}

log.Printf("k0s airgap bundle downloaded to '%s'", bundlePath)
}

log.Printf("k0s binary downloaded successfully to '%s'", k0sPath)

return nil
}
58 changes: 48 additions & 10 deletions cli/cmd/k0s/download_k0s_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/spf13/cobra"

"github.com/codesphere-cloud/oms/cli/cmd/k0s"
"github.com/codesphere-cloud/oms/cli/cmd/util"
Expand All @@ -23,11 +24,13 @@ var _ = Describe("DownloadK0sk0s", func() {
globalOpts *util.GlobalOptions
mockEnv *env.MockEnv
mockFileWriter *intutil.MockFileIO
mockK0sManager *installer.MockK0sManager
)

BeforeEach(func() {
mockEnv = env.NewMockEnv(GinkgoT())
mockFileWriter = intutil.NewMockFileIO(GinkgoT())
mockK0sManager = installer.NewMockK0sManager(GinkgoT())
globalOpts = &util.GlobalOptions{}
opts = &k0s.DownloadK0sOpts{
GlobalOptions: globalOpts,
Expand All @@ -48,9 +51,8 @@ var _ = Describe("DownloadK0sk0s", func() {

Context("DownloadK0s method", func() {
It("fails when k0s manager fails to get latest version", func() {
mockK0sManager := installer.NewMockK0sManager(GinkgoT())

c.Opts.Version = "" // Test auto-version detection

mockK0sManager.EXPECT().GetLatestVersion().Return("", errors.New("network error"))

err := c.DownloadK0s(mockK0sManager)
Expand All @@ -60,11 +62,9 @@ var _ = Describe("DownloadK0sk0s", func() {
})

It("fails when k0s manager fails to download", func() {
mockK0sManager := installer.NewMockK0sManager(GinkgoT())

c.Opts.Version = "v1.29.1+k0s.0"

mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", false, true).Return("", errors.New("download failed"))
mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", installer.DownloadOptions{Quiet: true}).Return("", errors.New("download failed"))

err := c.DownloadK0s(mockK0sManager)
Expect(err).To(HaveOccurred())
Expand All @@ -73,27 +73,65 @@ var _ = Describe("DownloadK0sk0s", func() {
})

It("succeeds when version is specified and download works", func() {
mockK0sManager := installer.NewMockK0sManager(GinkgoT())
c.Opts.Version = "v1.29.1+k0s.0"

mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", installer.DownloadOptions{Quiet: true}).Return("/test/workdir/k0s", nil)

err := c.DownloadK0s(mockK0sManager)
Expect(err).ToNot(HaveOccurred())
})

It("requests the airgap bundle when --airgapped is set", func() {
c.Opts.Version = "v1.29.1+k0s.0"
c.Opts.Airgap = true

mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", false, true).Return("/test/workdir/k0s", nil)
opts := installer.DownloadOptions{Quiet: true}
mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", opts).Return("/test/workdir/k0s", nil)
mockK0sManager.EXPECT().EnsureAirgapBundle("v1.29.1+k0s.0", opts).Return("/cache/k0s-airgap-bundle-amd64", nil)

err := c.DownloadK0s(mockK0sManager)
Expect(err).ToNot(HaveOccurred())
})

It("succeeds when version is auto-detected and download works", func() {
mockK0sManager := installer.NewMockK0sManager(GinkgoT())
It("reports airgap bundle download failures", func() {
c.Opts.Version = "v1.29.1+k0s.0"
c.Opts.Airgap = true

opts := installer.DownloadOptions{Quiet: true}
mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", opts).Return("/test/workdir/k0s", nil)
mockK0sManager.EXPECT().EnsureAirgapBundle("v1.29.1+k0s.0", opts).Return("", errors.New("network error"))

err := c.DownloadK0s(mockK0sManager)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to download k0s airgap bundle"))
Expect(err.Error()).To(ContainSubstring("network error"))
})

It("succeeds when version is auto-detected and download works", func() {
c.Opts.Version = "" // Test auto-version detection
c.Opts.Force = true
c.Opts.Verbose = true

mockK0sManager.EXPECT().GetLatestVersion().Return("v1.29.1+k0s.0", nil)
mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", true, false).Return("/test/workdir/k0s", nil)
mockK0sManager.EXPECT().Download("v1.29.1+k0s.0", installer.DownloadOptions{Force: true, Quiet: false}).Return("/test/workdir/k0s", nil)

err := c.DownloadK0s(mockK0sManager)
Expect(err).ToNot(HaveOccurred())
})
})

Context("AddDownloadCmd", func() {
It("registers --airgapped with the -a shorthand and default false", func() {
download := &cobra.Command{Use: "download"}

k0s.AddDownloadCmd(download, &util.GlobalOptions{})

Expect(download.Commands()).To(HaveLen(1))

flag := download.Commands()[0].Flags().Lookup("airgapped")
Expect(flag).ToNot(BeNil())
Expect(flag.Shorthand).To(Equal("a"))
Expect(flag.DefValue).To(Equal("false"))
})
})
})
Loading
Loading