From 7cd75c182137c1c75796311d59c413ef3ce0e0e0 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:35:29 +0100 Subject: [PATCH 01/32] feat: add create k0s setup from config.yaml --- Makefile | 3 + cli/cmd/install_k0s.go | 112 +++++- cli/cmd/install_k0s_integration_test.go | 449 ++++++++++++++++++++++++ cli/cmd/mocks.go | 17 +- go.mod | 2 + go.sum | 4 + hack/lima-oms.yaml | 42 ++- internal/installer/k0s.go | 101 ++++++ internal/installer/k0s_config.go | 118 +++++++ internal/installer/k0s_config_test.go | 148 ++++++++ internal/installer/mocks.go | 241 +++++-------- internal/installer/node/node.go | 444 +++++++++++++++++++++++ internal/portal/mocks.go | 225 ++---------- internal/system/mocks.go | 44 +-- internal/util/filewriter.go | 5 + internal/util/mocks.go | 268 ++++++-------- 16 files changed, 1644 insertions(+), 579 deletions(-) create mode 100644 cli/cmd/install_k0s_integration_test.go create mode 100644 internal/installer/k0s_config.go create mode 100644 internal/installer/k0s_config_test.go create mode 100644 internal/installer/node/node.go diff --git a/Makefile b/Makefile index aac4d16..d4ff508 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,9 @@ all: build-cli build-service build-cli: cd cli && go build -v && mv cli ../oms-cli +build-cli-linux: + GOOS=linux GOARCH=amd64 go build -C cli -o ../oms-cli + build-service: cd service && go build -v && mv service ../oms-service diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index a513933..aed5d79 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -5,12 +5,17 @@ package cmd import ( "fmt" + "log" + "os" + "path/filepath" packageio "github.com/codesphere-cloud/cs-go/pkg/io" "github.com/spf13/cobra" "github.com/codesphere-cloud/oms/internal/env" "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/installer/files" + "github.com/codesphere-cloud/oms/internal/installer/node" "github.com/codesphere-cloud/oms/internal/portal" "github.com/codesphere-cloud/oms/internal/util" ) @@ -25,10 +30,14 @@ type InstallK0sCmd struct { type InstallK0sOpts struct { *GlobalOptions - Version string - Package string - Config string - Force bool + Version string + Package string + Config string + InstallConfig string + SSHKeyPath string + RemoteHost string + RemoteUser string + Force bool } func (c *InstallK0sCmd) RunE(_ *cobra.Command, args []string) error { @@ -37,6 +46,10 @@ func (c *InstallK0sCmd) RunE(_ *cobra.Command, args []string) error { pm := installer.NewPackage(env.GetOmsWorkdir(), c.Opts.Package) k0s := installer.NewK0s(hw, env, c.FileWriter) + if c.Opts.InstallConfig != "" { + return c.InstallK0sFromInstallConfig(pm, k0s) + } + err := c.InstallK0s(pm, k0s) if err != nil { return fmt.Errorf("failed to install k0s: %w", err) @@ -54,12 +67,19 @@ func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { This will either download the k0s binary directly to the OMS workdir, if not already present, and install it or load the k0s binary from the provided package file and install it. If no version is specified, the latest version will be downloaded. - If no install config is provided, k0s will be installed with the '--single' flag.`), + If no install config is provided, k0s will be installed with the '--single' flag. + + You can also install k0s from a Codesphere install-config file, which will: + - Generate a k0s configuration from the install-config + - Optionally install k0s on remote nodes via SSH`), Example: formatExamplesWithBinary("install k0s", []packageio.Example{ {Cmd: "", Desc: "Install k0s using the Go-native implementation"}, {Cmd: "--version ", Desc: "Version of k0s to install"}, {Cmd: "--package ", Desc: "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load k0s from"}, {Cmd: "--k0s-config ", Desc: "Path to k0s configuration file, if not set k0s will be installed with the '--single' flag"}, + {Cmd: "--install-config ", Desc: "Path to Codesphere install-config file to generate k0s config from"}, + {Cmd: "--remote-host ", Desc: "Remote host IP to install k0s on (requires --ssh-key-path)"}, + {Cmd: "--ssh-key-path ", Desc: "SSH private key path for remote installation"}, {Cmd: "--force", Desc: "Force new download and installation even if k0s binary exists or is already installed"}, }, "oms-cli"), }, @@ -70,6 +90,10 @@ func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { k0s.cmd.Flags().StringVarP(&k0s.Opts.Version, "version", "v", "", "Version of k0s to install") 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().StringVar(&k0s.Opts.Config, "k0s-config", "", "Path to k0s configuration file") + k0s.cmd.Flags().StringVar(&k0s.Opts.InstallConfig, "install-config", "", "Path to Codesphere install-config file") + k0s.cmd.Flags().StringVar(&k0s.Opts.SSHKeyPath, "ssh-key-path", "", "SSH private key path for remote installation") + k0s.cmd.Flags().StringVar(&k0s.Opts.RemoteHost, "remote-host", "", "Remote host IP to install k0s on") + k0s.cmd.Flags().StringVar(&k0s.Opts.RemoteUser, "remote-user", "root", "Remote user for SSH connection") k0s.cmd.Flags().BoolVarP(&k0s.Opts.Force, "force", "f", false, "Force new download and installation") install.AddCommand(k0s.cmd) @@ -98,3 +122,81 @@ func (c *InstallK0sCmd) InstallK0s(pm installer.PackageManager, k0s installer.K0 return nil } + +func (c *InstallK0sCmd) InstallK0sFromInstallConfig(pm installer.PackageManager, k0s installer.K0sManager) error { + icg := installer.NewInstallConfigManager() + if err := icg.LoadInstallConfigFromFile(c.Opts.InstallConfig); err != nil { + return fmt.Errorf("failed to load install-config: %w", err) + } + + config := icg.GetInstallConfig() + + if !config.Kubernetes.ManagedByCodesphere { + return fmt.Errorf("install-config specifies external Kubernetes, k0s installation is only supported for Codesphere-managed Kubernetes") + } + + log.Println("Generating k0s configuration from install-config...") + k0sConfig, err := installer.GenerateK0sConfig(config) + if err != nil { + return fmt.Errorf("failed to generate k0s config: %w", err) + } + + k0sConfigData, err := k0sConfig.Marshal() + if err != nil { + return fmt.Errorf("failed to marshal k0s config: %w", err) + } + + tmpK0sConfigPath := filepath.Join(os.TempDir(), "k0s-config.yaml") + if err := os.WriteFile(tmpK0sConfigPath, k0sConfigData, 0644); err != nil { + return fmt.Errorf("failed to write k0s config: %w", err) + } + defer os.Remove(tmpK0sConfigPath) + + log.Printf("Generated k0s configuration at %s", tmpK0sConfigPath) + + k0sPath := pm.GetDependencyPath(defaultK0sPath) + if c.Opts.Package == "" { + k0sPath, err = k0s.Download(c.Opts.Version, c.Opts.Force, false) + if err != nil { + return fmt.Errorf("failed to download k0s: %w", err) + } + } + + if c.Opts.RemoteHost != "" { + return c.InstallK0sRemote(config, k0sPath, tmpK0sConfigPath) + } + + err = k0s.Install(tmpK0sConfigPath, k0sPath, c.Opts.Force) + if err != nil { + return fmt.Errorf("failed to install k0s: %w", err) + } + + log.Println("k0s installed successfully using configuration from install-config") + return nil +} + +func (c *InstallK0sCmd) InstallK0sRemote(config *files.RootConfig, k0sBinaryPath string, k0sConfigPath string) error { + if c.Opts.SSHKeyPath == "" { + return fmt.Errorf("--ssh-key-path is required for remote installation") + } + + log.Printf("Installing k0s on remote host %s", c.Opts.RemoteHost) + + nm := &node.NodeManager{ + FileIO: c.FileWriter, + KeyPath: c.Opts.SSHKeyPath, + } + + remoteNode := &node.Node{ + ExternalIP: c.Opts.RemoteHost, + InternalIP: c.Opts.RemoteHost, + Name: "k0s-node", + } + + if err := remoteNode.InstallK0s(nm, k0sBinaryPath, k0sConfigPath, c.Opts.Force); err != nil { + return fmt.Errorf("failed to install k0s on remote host: %w", err) + } + + log.Printf("k0s successfully installed on remote host %s", c.Opts.RemoteHost) + return nil +} diff --git a/cli/cmd/install_k0s_integration_test.go b/cli/cmd/install_k0s_integration_test.go new file mode 100644 index 0000000..966315a --- /dev/null +++ b/cli/cmd/install_k0s_integration_test.go @@ -0,0 +1,449 @@ +//go:build integration +// +build integration + +package cmd_test + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" + + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/installer/files" +) + +var _ = Describe("K0s Install-Config Integration", func() { + var ( + tempDir string + configPath string + k0sConfigOut string + ) + + BeforeEach(func() { + var err error + tempDir, err = os.MkdirTemp("", "k0s-integration-test-*") + Expect(err).NotTo(HaveOccurred()) + + configPath = filepath.Join(tempDir, "install-config.yaml") + k0sConfigOut = filepath.Join(tempDir, "k0s-config.yaml") + }) + + AfterEach(func() { + if tempDir != "" { + os.RemoveAll(tempDir) + } + }) + + Describe("Config Generation Workflow", func() { + It("should generate valid k0s config from install-config", func() { + // Create a minimal install-config using RootConfig + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "test-dc", + City: "Test City", + CountryCode: "US", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + ControlPlanes: []files.K8sNode{ + { + IPAddress: "192.168.1.100", + }, + }, + APIServerHost: "api.test.example.com", + }, + Codesphere: files.CodesphereConfig{ + Domain: "test.example.com", + PublicIP: "192.168.1.100", + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{}, + }, + Plans: files.PlansConfig{ + HostingPlans: map[int]files.HostingPlan{}, + WorkspacePlans: map[int]files.WorkspacePlan{}, + }, + }, + } + + // Write install-config to file + configData, err := yaml.Marshal(installConfig) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(configPath, configData, 0644) + Expect(err).NotTo(HaveOccurred()) + + // Load the config back using InstallConfigManager + icg := installer.NewInstallConfigManager() + err = icg.LoadInstallConfigFromFile(configPath) + Expect(err).NotTo(HaveOccurred()) + + loadedConfig := icg.GetInstallConfig() + Expect(loadedConfig).NotTo(BeNil()) + Expect(loadedConfig.Kubernetes.ManagedByCodesphere).To(BeTrue()) + + // Generate k0s config + k0sConfig, err := installer.GenerateK0sConfig(loadedConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(k0sConfig).NotTo(BeNil()) + + // Verify k0s config structure + Expect(k0sConfig.APIVersion).To(Equal("k0s.k0sproject.io/v1beta1")) + Expect(k0sConfig.Kind).To(Equal("ClusterConfig")) + Expect(k0sConfig.Metadata.Name).To(Equal("codesphere-test-dc")) + Expect(k0sConfig.Spec.API).NotTo(BeNil()) + Expect(k0sConfig.Spec.API.Address).To(Equal("192.168.1.100")) + Expect(k0sConfig.Spec.API.ExternalAddress).To(Equal("api.test.example.com")) + + // Write k0s config to file + k0sData, err := k0sConfig.Marshal() + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(k0sConfigOut, k0sData, 0644) + Expect(err).NotTo(HaveOccurred()) + + // Verify file was created and is valid YAML + Expect(k0sConfigOut).To(BeAnExistingFile()) + data, err := os.ReadFile(k0sConfigOut) + Expect(err).NotTo(HaveOccurred()) + Expect(len(data)).To(BeNumerically(">", 0)) + + // Verify we can unmarshal it back + var verifyConfig installer.K0sConfig + err = yaml.Unmarshal(data, &verifyConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(verifyConfig.APIVersion).To(Equal("k0s.k0sproject.io/v1beta1")) + }) + + It("should handle multi-control-plane configuration", func() { + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "multi-dc", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + ControlPlanes: []files.K8sNode{ + {IPAddress: "10.0.0.10"}, + {IPAddress: "10.0.0.11"}, + {IPAddress: "10.0.0.12"}, + }, + APIServerHost: "api.cluster.test", + }, + Codesphere: files.CodesphereConfig{ + Domain: "cluster.test", + PublicIP: "10.0.0.10", + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{}, + }, + Plans: files.PlansConfig{ + HostingPlans: map[int]files.HostingPlan{}, + WorkspacePlans: map[int]files.WorkspacePlan{}, + }, + }, + } + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).NotTo(HaveOccurred()) + + // Verify primary IP is used + Expect(k0sConfig.Spec.API.Address).To(Equal("10.0.0.10")) + // Verify all IPs are in SANs + Expect(k0sConfig.Spec.API.SANs).To(ContainElement("10.0.0.10")) + Expect(k0sConfig.Spec.API.SANs).To(ContainElement("10.0.0.11")) + Expect(k0sConfig.Spec.API.SANs).To(ContainElement("10.0.0.12")) + Expect(k0sConfig.Spec.API.SANs).To(ContainElement("api.cluster.test")) + }) + + It("should preserve network configuration", func() { + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "network-test", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + ControlPlanes: []files.K8sNode{ + {IPAddress: "192.168.1.100"}, + }, + PodCIDR: "10.244.0.0/16", + ServiceCIDR: "10.96.0.0/12", + }, + Codesphere: files.CodesphereConfig{ + Domain: "network.test", + PublicIP: "192.168.1.100", + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{}, + }, + Plans: files.PlansConfig{ + HostingPlans: map[int]files.HostingPlan{}, + WorkspacePlans: map[int]files.WorkspacePlan{}, + }, + }, + } + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).NotTo(HaveOccurred()) + + // Verify network settings + Expect(k0sConfig.Spec.Network).NotTo(BeNil()) + Expect(k0sConfig.Spec.Network.PodCIDR).To(Equal("10.244.0.0/16")) + Expect(k0sConfig.Spec.Network.ServiceCIDR).To(Equal("10.96.0.0/12")) + }) + + It("should handle storage configuration", func() { + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "storage-test", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + ControlPlanes: []files.K8sNode{ + {IPAddress: "192.168.1.100"}, + }, + }, + Codesphere: files.CodesphereConfig{ + Domain: "storage.test", + PublicIP: "192.168.1.100", + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{}, + }, + Plans: files.PlansConfig{ + HostingPlans: map[int]files.HostingPlan{}, + WorkspacePlans: map[int]files.WorkspacePlan{}, + }, + }, + } + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).NotTo(HaveOccurred()) + + // Verify storage/etcd settings + Expect(k0sConfig.Spec.Storage).NotTo(BeNil()) + Expect(k0sConfig.Spec.Storage.Type).To(Equal("etcd")) + Expect(k0sConfig.Spec.Storage.Etcd).NotTo(BeNil()) + Expect(k0sConfig.Spec.Storage.Etcd.PeerAddress).To(Equal("192.168.1.100")) + }) + }) + + Describe("Error Handling", func() { + It("should fail gracefully on missing install-config file", func() { + nonExistentPath := filepath.Join(tempDir, "does-not-exist.yaml") + icg := installer.NewInstallConfigManager() + err := icg.LoadInstallConfigFromFile(nonExistentPath) + Expect(err).To(HaveOccurred()) + }) + + It("should handle nil config gracefully", func() { + _, err := installer.GenerateK0sConfig(nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot be nil")) + }) + + It("should handle invalid YAML gracefully", func() { + invalidYAML := []byte("invalid: [unclosed bracket") + err := os.WriteFile(configPath, invalidYAML, 0644) + Expect(err).NotTo(HaveOccurred()) + + icg := installer.NewInstallConfigManager() + err = icg.LoadInstallConfigFromFile(configPath) + Expect(err).To(HaveOccurred()) + }) + + It("should fail for external Kubernetes", func() { + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "external-k8s", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: false, // External K8s + }, + Codesphere: files.CodesphereConfig{ + Domain: "external.test", + PublicIP: "10.0.0.1", + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{}, + }, + Plans: files.PlansConfig{ + HostingPlans: map[int]files.HostingPlan{}, + WorkspacePlans: map[int]files.WorkspacePlan{}, + }, + }, + } + + // GenerateK0sConfig should still work (doesn't validate ManagedByCodesphere) + // The validation happens in the CLI command + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(k0sConfig).NotTo(BeNil()) + }) + }) + + Describe("YAML Marshalling", func() { + It("should produce valid k0s YAML output", func() { + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "yaml-test", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + ControlPlanes: []files.K8sNode{ + {IPAddress: "10.20.30.40"}, + }, + }, + Codesphere: files.CodesphereConfig{ + Domain: "yaml.test", + PublicIP: "10.20.30.40", + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{}, + }, + Plans: files.PlansConfig{ + HostingPlans: map[int]files.HostingPlan{}, + WorkspacePlans: map[int]files.WorkspacePlan{}, + }, + }, + } + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).NotTo(HaveOccurred()) + + yamlData, err := k0sConfig.Marshal() + Expect(err).NotTo(HaveOccurred()) + Expect(string(yamlData)).To(ContainSubstring("k0s.k0sproject.io/v1beta1")) + Expect(string(yamlData)).To(ContainSubstring("ClusterConfig")) + Expect(string(yamlData)).To(ContainSubstring("10.20.30.40")) + }) + + It("should round-trip marshal and unmarshal correctly", func() { + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "roundtrip-test", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + ControlPlanes: []files.K8sNode{ + {IPAddress: "172.16.0.1"}, + }, + PodCIDR: "10.244.0.0/16", + ServiceCIDR: "10.96.0.0/12", + }, + Codesphere: files.CodesphereConfig{ + Domain: "roundtrip.test", + PublicIP: "172.16.0.1", + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{}, + }, + Plans: files.PlansConfig{ + HostingPlans: map[int]files.HostingPlan{}, + WorkspacePlans: map[int]files.WorkspacePlan{}, + }, + }, + } + + original, err := installer.GenerateK0sConfig(installConfig) + Expect(err).NotTo(HaveOccurred()) + + // Marshal to YAML + yamlData, err := original.Marshal() + Expect(err).NotTo(HaveOccurred()) + + // Unmarshal back + var restored installer.K0sConfig + err = yaml.Unmarshal(yamlData, &restored) + Expect(err).NotTo(HaveOccurred()) + + // Verify they match + Expect(restored.APIVersion).To(Equal(original.APIVersion)) + Expect(restored.Kind).To(Equal(original.Kind)) + Expect(restored.Metadata.Name).To(Equal(original.Metadata.Name)) + Expect(restored.Spec.API.Address).To(Equal(original.Spec.API.Address)) + Expect(restored.Spec.Network.PodCIDR).To(Equal(original.Spec.Network.PodCIDR)) + Expect(restored.Spec.Network.ServiceCIDR).To(Equal(original.Spec.Network.ServiceCIDR)) + }) + }) + + Describe("Full Workflow Integration", func() { + It("should complete full config generation workflow", func() { + // Step 1: Create install-config + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "integration-dc", + City: "Integration City", + CountryCode: "US", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + ControlPlanes: []files.K8sNode{ + {IPAddress: "203.0.113.10"}, + }, + APIServerHost: "api.integration.test", + PodCIDR: "10.244.0.0/16", + ServiceCIDR: "10.96.0.0/12", + }, + Codesphere: files.CodesphereConfig{ + Domain: "integration.test", + PublicIP: "203.0.113.10", + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{}, + }, + Plans: files.PlansConfig{ + HostingPlans: map[int]files.HostingPlan{}, + WorkspacePlans: map[int]files.WorkspacePlan{}, + }, + }, + } + + // Step 2: Write install-config + configData, err := yaml.Marshal(installConfig) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(configPath, configData, 0644) + Expect(err).NotTo(HaveOccurred()) + + // Step 3: Load install-config + icg := installer.NewInstallConfigManager() + err = icg.LoadInstallConfigFromFile(configPath) + Expect(err).NotTo(HaveOccurred()) + + // Step 4: Generate k0s config + loadedConfig := icg.GetInstallConfig() + k0sConfig, err := installer.GenerateK0sConfig(loadedConfig) + Expect(err).NotTo(HaveOccurred()) + + // Step 5: Marshal k0s config + k0sData, err := k0sConfig.Marshal() + Expect(err).NotTo(HaveOccurred()) + + // Step 6: Write k0s config + err = os.WriteFile(k0sConfigOut, k0sData, 0644) + Expect(err).NotTo(HaveOccurred()) + + // Step 7: Verify complete workflow + Expect(k0sConfigOut).To(BeAnExistingFile()) + + // Step 8: Load and verify k0s config + readData, err := os.ReadFile(k0sConfigOut) + Expect(err).NotTo(HaveOccurred()) + + var finalK0sConfig installer.K0sConfig + err = yaml.Unmarshal(readData, &finalK0sConfig) + Expect(err).NotTo(HaveOccurred()) + + // Step 9: Validate all fields + Expect(finalK0sConfig.APIVersion).To(Equal("k0s.k0sproject.io/v1beta1")) + Expect(finalK0sConfig.Kind).To(Equal("ClusterConfig")) + Expect(finalK0sConfig.Metadata.Name).To(Equal("codesphere-integration-dc")) + Expect(finalK0sConfig.Spec.API.Address).To(Equal("203.0.113.10")) + Expect(finalK0sConfig.Spec.API.ExternalAddress).To(Equal("api.integration.test")) + Expect(finalK0sConfig.Spec.Network.PodCIDR).To(Equal("10.244.0.0/16")) + Expect(finalK0sConfig.Spec.Network.ServiceCIDR).To(Equal("10.96.0.0/12")) + Expect(finalK0sConfig.Spec.Storage.Type).To(Equal("etcd")) + Expect(finalK0sConfig.Spec.Storage.Etcd.PeerAddress).To(Equal("203.0.113.10")) + }) + }) +}) diff --git a/cli/cmd/mocks.go b/cli/cmd/mocks.go index 2f86863..a1ac35e 100644 --- a/cli/cmd/mocks.go +++ b/cli/cmd/mocks.go @@ -74,26 +74,15 @@ type MockOMSUpdater_Update_Call struct { } // Update is a helper method to define mock.On call -// - v semver.Version -// - repo string +// - v +// - repo func (_e *MockOMSUpdater_Expecter) Update(v interface{}, repo interface{}) *MockOMSUpdater_Update_Call { return &MockOMSUpdater_Update_Call{Call: _e.mock.On("Update", v, repo)} } func (_c *MockOMSUpdater_Update_Call) Run(run func(v semver.Version, repo string)) *MockOMSUpdater_Update_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 semver.Version - if args[0] != nil { - arg0 = args[0].(semver.Version) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) + run(args[0].(semver.Version), args[1].(string)) }) return _c } diff --git a/go.mod b/go.mod index fc94122..2654bc5 100644 --- a/go.mod +++ b/go.mod @@ -329,6 +329,7 @@ require ( github.com/knadh/koanf/providers/posflag v0.1.0 // indirect github.com/knadh/koanf/providers/structs v0.1.0 // indirect github.com/knadh/koanf/v2 v2.3.0 // indirect + github.com/kr/fs v0.1.0 // indirect github.com/kulti/thelper v0.7.1 // indirect github.com/kunwardeep/paralleltest v1.0.15 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -390,6 +391,7 @@ require ( github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/sftp v1.13.10 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect diff --git a/go.sum b/go.sum index 1571e54..bea566d 100644 --- a/go.sum +++ b/go.sum @@ -853,6 +853,8 @@ github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSd github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -1024,6 +1026,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/hack/lima-oms.yaml b/hack/lima-oms.yaml index 8dabfd2..631b796 100644 --- a/hack/lima-oms.yaml +++ b/hack/lima-oms.yaml @@ -18,8 +18,8 @@ disk: "60GiB" # Mount your OMS project mounts: -- location: "." - mountPoint: "/home/user/oms" +- location: "~" + mountPoint: "/home/user/host-home" writable: true # Ports and SSH @@ -61,13 +61,12 @@ provision: set -eux -o pipefail # Install Docker in rootless mode - dockerd-rootless-setuptool.sh install + dockerd-rootless-setuptool.sh install || true - # Set up the OMS project - cd /home/user/oms - export PATH=$PATH:/usr/local/go/bin - go mod download - cd cli && go build -a -buildvcs=false && mv cli ../oms-cli + # Clone OMS repository locally for better build performance + if [ ! -d ~/oms ]; then + git clone https://github.com/codesphere-cloud/oms.git ~/oms + fi message: | Your OMS development environment is ready! @@ -75,13 +74,32 @@ message: | To access it: ------ limactl shell lima-oms - cd /home/user/oms + cd oms ./oms-cli --help ------ + + To build the CLI for Linux (from your Mac): + ------ + make build-cli-linux + ------ - To install Codesphere eg.: + To build the CLI inside lima: ------ - ./oms-cli install codesphere --package codesphere-v1.66.0-installer --config config.yaml --priv-key ./path-to-private-key + limactl shell lima-oms + cd oms + make build-cli ------ - Go 1.24 and Docker are installed and ready to use. + To install k0s (run inside lima): + ------ + limactl shell lima-oms + cd oms + ./oms-cli install k0s --k0s-config config.yaml --version v1.30.0+k0s.0 --force + ------ + + To install Codesphere (run inside lima): + ------ + limactl shell lima-oms + cd oms + ./oms-cli install codesphere --package codesphere-v1.66.0-installer --install-config config.yaml --priv-key ./path-to-private-key + ------ diff --git a/internal/installer/k0s.go b/internal/installer/k0s.go index 1c350b3..f497c1c 100644 --- a/internal/installer/k0s.go +++ b/internal/installer/k0s.go @@ -14,12 +14,14 @@ import ( "github.com/codesphere-cloud/oms/internal/env" "github.com/codesphere-cloud/oms/internal/portal" "github.com/codesphere-cloud/oms/internal/util" + "gopkg.in/yaml.v3" ) type K0sManager interface { GetLatestVersion() (string, error) Download(version string, force bool, quiet bool) (string, error) Install(configPath string, k0sPath string, force bool) error + Reset(k0sPath string) error } type K0s struct { @@ -62,6 +64,12 @@ func (k *K0s) Download(version string, force bool, quiet bool) (string, error) { // Check if k0s binary already exists and create destination file workdir := k.Env.GetOmsWorkdir() + + // Ensure workdir exists + if err := os.MkdirAll(workdir, 0755); err != nil { + return "", fmt.Errorf("failed to create workdir: %w", err) + } + k0sPath := filepath.Join(workdir, "k0s") if k.FileWriter.Exists(k0sPath) && !force { return "", fmt.Errorf("k0s binary already exists at %s. Use --force to overwrite", k0sPath) @@ -102,8 +110,23 @@ func (k *K0s) Install(configPath string, k0sPath string, force bool) error { return fmt.Errorf("k0s binary does not exist in '%s', please download first", k0sPath) } + if force { + if err := k.Reset(k0sPath); err != nil { + log.Printf("Warning: failed to reset k0s: %v", err) + } + } + args := []string{k0sPath, "install", "controller"} + + // If config path is provided, filter it to only include k0s-compatible fields if configPath != "" { + filteredConfigPath, err := k.filterConfigForK0s(configPath) + if err != nil { + log.Printf("Warning: failed to filter config, using original: %v", err) + } else { + configPath = filteredConfigPath + defer os.Remove(filteredConfigPath) // Clean up temp file after use + } args = append(args, "--config", configPath) } else { args = append(args, "--single") @@ -128,3 +151,81 @@ func (k *K0s) Install(configPath string, k0sPath string, force bool) error { return nil } + +func (k *K0s) filterConfigForK0s(configPath string) (string, error) { + data, err := os.ReadFile(configPath) + if err != nil { + return "", fmt.Errorf("failed to read config: %w", err) + } + + var config map[string]interface{} + if err := yaml.Unmarshal(data, &config); err != nil { + return "", fmt.Errorf("failed to parse config: %w", err) + } + + keysToKeep := map[string]bool{ + "apiVersion": true, + "kind": true, + "metadata": true, + "spec": true, + } + + for key := range config { + if !keysToKeep[key] { + delete(config, key) + } + } + + if spec, ok := config["spec"].(map[string]interface{}); ok { + specKeysToKeep := map[string]bool{ + "api": true, + "controllerManager": true, + "scheduler": true, + "extensions": true, + "network": true, + "storage": true, + "telemetry": true, + "images": true, + "konnectivity": true, + } + + for key := range spec { + if !specKeysToKeep[key] { + delete(spec, key) + } + } + config["spec"] = spec + } + + filteredData, err := yaml.Marshal(config) + if err != nil { + return "", fmt.Errorf("failed to marshal filtered config: %w", err) + } + + tmpFile, err := os.CreateTemp("", "k0s-config-*.yaml") + if err != nil { + return "", fmt.Errorf("failed to create temp config: %w", err) + } + defer tmpFile.Close() + + if _, err := tmpFile.Write(filteredData); err != nil { + return "", fmt.Errorf("failed to write temp config: %w", err) + } + + return tmpFile.Name(), nil +} + +func (k *K0s) Reset(k0sPath string) error { + if !k.FileWriter.Exists(k0sPath) { + return nil + } + + log.Println("Resetting existing k0s installation...") + err := util.RunCommand("sudo", []string{k0sPath, "reset"}, "") + if err != nil { + return fmt.Errorf("failed to reset k0s: %w", err) + } + + log.Println("k0s reset completed successfully") + return nil +} diff --git a/internal/installer/k0s_config.go b/internal/installer/k0s_config.go new file mode 100644 index 0000000..738e0c8 --- /dev/null +++ b/internal/installer/k0s_config.go @@ -0,0 +1,118 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package installer + +import ( + "fmt" + + "github.com/codesphere-cloud/oms/internal/installer/files" + "gopkg.in/yaml.v3" +) + +type K0sConfig struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata K0sMetadata `yaml:"metadata"` + Spec K0sSpec `yaml:"spec"` +} + +type K0sMetadata struct { + Name string `yaml:"name"` +} + +type K0sSpec struct { + API *K0sAPI `yaml:"api,omitempty"` + Network *K0sNetwork `yaml:"network,omitempty"` + Storage *K0sStorage `yaml:"storage,omitempty"` +} + +type K0sAPI struct { + Address string `yaml:"address,omitempty"` + ExternalAddress string `yaml:"externalAddress,omitempty"` + SANs []string `yaml:"sans,omitempty"` + Port int `yaml:"port,omitempty"` +} + +type K0sNetwork struct { + PodCIDR string `yaml:"podCIDR,omitempty"` + ServiceCIDR string `yaml:"serviceCIDR,omitempty"` + Provider string `yaml:"provider,omitempty"` +} + +type K0sStorage struct { + Type string `yaml:"type,omitempty"` + Etcd *K0sEtcd `yaml:"etcd,omitempty"` +} + +type K0sEtcd struct { + PeerAddress string `yaml:"peerAddress,omitempty"` +} + +func GenerateK0sConfig(installConfig *files.RootConfig) (*K0sConfig, error) { + if installConfig == nil { + return nil, fmt.Errorf("installConfig cannot be nil") + } + + k0sConfig := &K0sConfig{ + APIVersion: "k0s.k0sproject.io/v1beta1", + Kind: "ClusterConfig", + Metadata: K0sMetadata{ + Name: fmt.Sprintf("codesphere-%s", installConfig.Datacenter.Name), + }, + Spec: K0sSpec{}, + } + + if installConfig.Kubernetes.ManagedByCodesphere { + if len(installConfig.Kubernetes.ControlPlanes) > 0 { + firstControlPlane := installConfig.Kubernetes.ControlPlanes[0] + k0sConfig.Spec.API = &K0sAPI{ + Address: firstControlPlane.IPAddress, + Port: 6443, + } + + if installConfig.Kubernetes.APIServerHost != "" { + k0sConfig.Spec.API.ExternalAddress = installConfig.Kubernetes.APIServerHost + } + + sans := make([]string, 0, len(installConfig.Kubernetes.ControlPlanes)) + for _, cp := range installConfig.Kubernetes.ControlPlanes { + sans = append(sans, cp.IPAddress) + } + if installConfig.Kubernetes.APIServerHost != "" { + sans = append(sans, installConfig.Kubernetes.APIServerHost) + } + k0sConfig.Spec.API.SANs = sans + } + + k0sConfig.Spec.Network = &K0sNetwork{ + Provider: "kuberouter", + } + + if installConfig.Kubernetes.PodCIDR != "" { + k0sConfig.Spec.Network.PodCIDR = installConfig.Kubernetes.PodCIDR + } + if installConfig.Kubernetes.ServiceCIDR != "" { + k0sConfig.Spec.Network.ServiceCIDR = installConfig.Kubernetes.ServiceCIDR + } + + if len(installConfig.Kubernetes.ControlPlanes) > 0 { + k0sConfig.Spec.Storage = &K0sStorage{ + Type: "etcd", + Etcd: &K0sEtcd{ + PeerAddress: installConfig.Kubernetes.ControlPlanes[0].IPAddress, + }, + } + } + } + + return k0sConfig, nil +} + +func (c *K0sConfig) Marshal() ([]byte, error) { + return yaml.Marshal(c) +} + +func (c *K0sConfig) Unmarshal(data []byte) error { + return yaml.Unmarshal(data, c) +} diff --git a/internal/installer/k0s_config_test.go b/internal/installer/k0s_config_test.go new file mode 100644 index 0000000..673a886 --- /dev/null +++ b/internal/installer/k0s_config_test.go @@ -0,0 +1,148 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package installer_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" + + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/installer/files" +) + +var _ = Describe("K0sConfig", func() { + Describe("GenerateK0sConfig", func() { + Context("with valid install-config", func() { + It("should generate k0s config with control plane settings", func() { + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "test-dc", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + APIServerHost: "k8s.example.com", + ControlPlanes: []files.K8sNode{ + {IPAddress: "10.0.1.10"}, + {IPAddress: "10.0.1.11"}, + {IPAddress: "10.0.1.12"}, + }, + PodCIDR: "10.244.0.0/16", + ServiceCIDR: "10.96.0.0/12", + }, + } + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).ToNot(HaveOccurred()) + Expect(k0sConfig).ToNot(BeNil()) + + // Check basic structure + Expect(k0sConfig.APIVersion).To(Equal("k0s.k0sproject.io/v1beta1")) + Expect(k0sConfig.Kind).To(Equal("ClusterConfig")) + Expect(k0sConfig.Metadata.Name).To(Equal("codesphere-test-dc")) + + // Check API configuration + Expect(k0sConfig.Spec.API).ToNot(BeNil()) + Expect(k0sConfig.Spec.API.Address).To(Equal("10.0.1.10")) + Expect(k0sConfig.Spec.API.ExternalAddress).To(Equal("k8s.example.com")) + Expect(k0sConfig.Spec.API.Port).To(Equal(6443)) + Expect(k0sConfig.Spec.API.SANs).To(ContainElements("10.0.1.10", "10.0.1.11", "10.0.1.12", "k8s.example.com")) + + // Check Network configuration + Expect(k0sConfig.Spec.Network).ToNot(BeNil()) + Expect(k0sConfig.Spec.Network.PodCIDR).To(Equal("10.244.0.0/16")) + Expect(k0sConfig.Spec.Network.ServiceCIDR).To(Equal("10.96.0.0/12")) + Expect(k0sConfig.Spec.Network.Provider).To(Equal("kuberouter")) + + // Check Storage configuration + Expect(k0sConfig.Spec.Storage).ToNot(BeNil()) + Expect(k0sConfig.Spec.Storage.Type).To(Equal("etcd")) + Expect(k0sConfig.Spec.Storage.Etcd).ToNot(BeNil()) + Expect(k0sConfig.Spec.Storage.Etcd.PeerAddress).To(Equal("10.0.1.10")) + }) + + It("should handle minimal configuration", func() { + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "minimal", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + ControlPlanes: []files.K8sNode{ + {IPAddress: "192.168.1.100"}, + }, + }, + } + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).ToNot(HaveOccurred()) + Expect(k0sConfig).ToNot(BeNil()) + Expect(k0sConfig.Metadata.Name).To(Equal("codesphere-minimal")) + }) + + It("should generate valid YAML", func() { + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "test-dc", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + ControlPlanes: []files.K8sNode{ + {IPAddress: "10.0.1.10"}, + }, + PodCIDR: "10.244.0.0/16", + ServiceCIDR: "10.96.0.0/12", + }, + } + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).ToNot(HaveOccurred()) + + yamlData, err := k0sConfig.Marshal() + Expect(err).ToNot(HaveOccurred()) + Expect(yamlData).ToNot(BeEmpty()) + + // 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")) + }) + }) + + Context("with invalid input", func() { + It("should return error for nil install-config", func() { + k0sConfig, err := installer.GenerateK0sConfig(nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("installConfig cannot be nil")) + Expect(k0sConfig).To(BeNil()) + }) + }) + + Context("with non-managed Kubernetes", func() { + It("should not configure k0s for external kubernetes", func() { + installConfig := &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "external", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: false, + PodCIDR: "10.244.0.0/16", + ServiceCIDR: "10.96.0.0/12", + }, + } + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).ToNot(HaveOccurred()) + Expect(k0sConfig).ToNot(BeNil()) + // Should still have basic structure but no specific config + Expect(k0sConfig.Metadata.Name).To(Equal("codesphere-external")) + }) + }) + }) +}) diff --git a/internal/installer/mocks.go b/internal/installer/mocks.go index 56e8250..fdfaf36 100644 --- a/internal/installer/mocks.go +++ b/internal/installer/mocks.go @@ -69,20 +69,14 @@ type MockConfigManager_ParseConfigYaml_Call struct { } // ParseConfigYaml is a helper method to define mock.On call -// - configPath string +// - configPath func (_e *MockConfigManager_Expecter) ParseConfigYaml(configPath interface{}) *MockConfigManager_ParseConfigYaml_Call { return &MockConfigManager_ParseConfigYaml_Call{Call: _e.mock.On("ParseConfigYaml", configPath)} } func (_c *MockConfigManager_ParseConfigYaml_Call) Run(run func(configPath string)) *MockConfigManager_ParseConfigYaml_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -147,20 +141,14 @@ type MockInstallConfigManager_ApplyProfile_Call struct { } // ApplyProfile is a helper method to define mock.On call -// - profile string +// - profile func (_e *MockInstallConfigManager_Expecter) ApplyProfile(profile interface{}) *MockInstallConfigManager_ApplyProfile_Call { return &MockInstallConfigManager_ApplyProfile_Call{Call: _e.mock.On("ApplyProfile", profile)} } func (_c *MockInstallConfigManager_ApplyProfile_Call) Run(run func(profile string)) *MockInstallConfigManager_ApplyProfile_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -332,20 +320,14 @@ type MockInstallConfigManager_LoadInstallConfigFromFile_Call struct { } // LoadInstallConfigFromFile is a helper method to define mock.On call -// - configPath string +// - configPath func (_e *MockInstallConfigManager_Expecter) LoadInstallConfigFromFile(configPath interface{}) *MockInstallConfigManager_LoadInstallConfigFromFile_Call { return &MockInstallConfigManager_LoadInstallConfigFromFile_Call{Call: _e.mock.On("LoadInstallConfigFromFile", configPath)} } func (_c *MockInstallConfigManager_LoadInstallConfigFromFile_Call) Run(run func(configPath string)) *MockInstallConfigManager_LoadInstallConfigFromFile_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -383,20 +365,14 @@ type MockInstallConfigManager_LoadVaultFromFile_Call struct { } // LoadVaultFromFile is a helper method to define mock.On call -// - vaultPath string +// - vaultPath func (_e *MockInstallConfigManager_Expecter) LoadVaultFromFile(vaultPath interface{}) *MockInstallConfigManager_LoadVaultFromFile_Call { return &MockInstallConfigManager_LoadVaultFromFile_Call{Call: _e.mock.On("LoadVaultFromFile", vaultPath)} } func (_c *MockInstallConfigManager_LoadVaultFromFile_Call) Run(run func(vaultPath string)) *MockInstallConfigManager_LoadVaultFromFile_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -570,26 +546,15 @@ type MockInstallConfigManager_WriteInstallConfig_Call struct { } // WriteInstallConfig is a helper method to define mock.On call -// - configPath string -// - withComments bool +// - configPath +// - withComments func (_e *MockInstallConfigManager_Expecter) WriteInstallConfig(configPath interface{}, withComments interface{}) *MockInstallConfigManager_WriteInstallConfig_Call { return &MockInstallConfigManager_WriteInstallConfig_Call{Call: _e.mock.On("WriteInstallConfig", configPath, withComments)} } func (_c *MockInstallConfigManager_WriteInstallConfig_Call) Run(run func(configPath string, withComments bool)) *MockInstallConfigManager_WriteInstallConfig_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 bool - if args[1] != nil { - arg1 = args[1].(bool) - } - run( - arg0, - arg1, - ) + run(args[0].(string), args[1].(bool)) }) return _c } @@ -627,26 +592,15 @@ type MockInstallConfigManager_WriteVault_Call struct { } // WriteVault is a helper method to define mock.On call -// - vaultPath string -// - withComments bool +// - vaultPath +// - withComments func (_e *MockInstallConfigManager_Expecter) WriteVault(vaultPath interface{}, withComments interface{}) *MockInstallConfigManager_WriteVault_Call { return &MockInstallConfigManager_WriteVault_Call{Call: _e.mock.On("WriteVault", vaultPath, withComments)} } func (_c *MockInstallConfigManager_WriteVault_Call) Run(run func(vaultPath string, withComments bool)) *MockInstallConfigManager_WriteVault_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 bool - if args[1] != nil { - arg1 = args[1].(bool) - } - run( - arg0, - arg1, - ) + run(args[0].(string), args[1].(bool)) }) return _c } @@ -720,32 +674,16 @@ type MockK0sManager_Download_Call struct { } // Download is a helper method to define mock.On call -// - version string -// - force bool -// - quiet bool +// - version +// - force +// - quiet func (_e *MockK0sManager_Expecter) Download(version interface{}, force interface{}, quiet interface{}) *MockK0sManager_Download_Call { return &MockK0sManager_Download_Call{Call: _e.mock.On("Download", version, force, quiet)} } func (_c *MockK0sManager_Download_Call) Run(run func(version string, force bool, quiet bool)) *MockK0sManager_Download_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 bool - if args[1] != nil { - arg1 = args[1].(bool) - } - var arg2 bool - if args[2] != nil { - arg2 = args[2].(bool) - } - run( - arg0, - arg1, - arg2, - ) + run(args[0].(string), args[1].(bool), args[2].(bool)) }) return _c } @@ -836,32 +774,16 @@ type MockK0sManager_Install_Call struct { } // Install is a helper method to define mock.On call -// - configPath string -// - k0sPath string -// - force bool +// - configPath +// - k0sPath +// - force func (_e *MockK0sManager_Expecter) Install(configPath interface{}, k0sPath interface{}, force interface{}) *MockK0sManager_Install_Call { return &MockK0sManager_Install_Call{Call: _e.mock.On("Install", configPath, k0sPath, force)} } func (_c *MockK0sManager_Install_Call) Run(run func(configPath string, k0sPath string, force bool)) *MockK0sManager_Install_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 bool - if args[2] != nil { - arg2 = args[2].(bool) - } - run( - arg0, - arg1, - arg2, - ) + run(args[0].(string), args[1].(string), args[2].(bool)) }) return _c } @@ -876,6 +798,51 @@ func (_c *MockK0sManager_Install_Call) RunAndReturn(run func(configPath string, return _c } +// Reset provides a mock function for the type MockK0sManager +func (_mock *MockK0sManager) Reset(k0sPath string) error { + ret := _mock.Called(k0sPath) + + if len(ret) == 0 { + panic("no return value specified for Reset") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(k0sPath) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockK0sManager_Reset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reset' +type MockK0sManager_Reset_Call struct { + *mock.Call +} + +// Reset is a helper method to define mock.On call +// - k0sPath +func (_e *MockK0sManager_Expecter) Reset(k0sPath interface{}) *MockK0sManager_Reset_Call { + return &MockK0sManager_Reset_Call{Call: _e.mock.On("Reset", k0sPath)} +} + +func (_c *MockK0sManager_Reset_Call) Run(run func(k0sPath string)) *MockK0sManager_Reset_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockK0sManager_Reset_Call) Return(err error) *MockK0sManager_Reset_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockK0sManager_Reset_Call) RunAndReturn(run func(k0sPath string) error) *MockK0sManager_Reset_Call { + _c.Call.Return(run) + return _c +} + // NewMockPackageManager creates a new instance of MockPackageManager. 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 NewMockPackageManager(t interface { @@ -926,20 +893,14 @@ type MockPackageManager_Extract_Call struct { } // Extract is a helper method to define mock.On call -// - force bool +// - force func (_e *MockPackageManager_Expecter) Extract(force interface{}) *MockPackageManager_Extract_Call { return &MockPackageManager_Extract_Call{Call: _e.mock.On("Extract", force)} } func (_c *MockPackageManager_Extract_Call) Run(run func(force bool)) *MockPackageManager_Extract_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 bool - if args[0] != nil { - arg0 = args[0].(bool) - } - run( - arg0, - ) + run(args[0].(bool)) }) return _c } @@ -977,26 +938,15 @@ type MockPackageManager_ExtractDependency_Call struct { } // ExtractDependency is a helper method to define mock.On call -// - file string -// - force bool +// - file +// - force func (_e *MockPackageManager_Expecter) ExtractDependency(file interface{}, force interface{}) *MockPackageManager_ExtractDependency_Call { return &MockPackageManager_ExtractDependency_Call{Call: _e.mock.On("ExtractDependency", file, force)} } func (_c *MockPackageManager_ExtractDependency_Call) Run(run func(file string, force bool)) *MockPackageManager_ExtractDependency_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 bool - if args[1] != nil { - arg1 = args[1].(bool) - } - run( - arg0, - arg1, - ) + run(args[0].(string), args[1].(bool)) }) return _c } @@ -1043,20 +993,14 @@ type MockPackageManager_ExtractOciImageIndex_Call struct { } // ExtractOciImageIndex is a helper method to define mock.On call -// - imagefile string +// - imagefile func (_e *MockPackageManager_Expecter) ExtractOciImageIndex(imagefile interface{}) *MockPackageManager_ExtractOciImageIndex_Call { return &MockPackageManager_ExtractOciImageIndex_Call{Call: _e.mock.On("ExtractOciImageIndex", imagefile)} } func (_c *MockPackageManager_ExtractOciImageIndex_Call) Run(run func(imagefile string)) *MockPackageManager_ExtractOciImageIndex_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -1149,20 +1093,14 @@ type MockPackageManager_GetBaseimageName_Call struct { } // GetBaseimageName is a helper method to define mock.On call -// - baseimage string +// - baseimage func (_e *MockPackageManager_Expecter) GetBaseimageName(baseimage interface{}) *MockPackageManager_GetBaseimageName_Call { return &MockPackageManager_GetBaseimageName_Call{Call: _e.mock.On("GetBaseimageName", baseimage)} } func (_c *MockPackageManager_GetBaseimageName_Call) Run(run func(baseimage string)) *MockPackageManager_GetBaseimageName_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -1209,26 +1147,15 @@ type MockPackageManager_GetBaseimagePath_Call struct { } // GetBaseimagePath is a helper method to define mock.On call -// - baseimage string -// - force bool +// - baseimage +// - force func (_e *MockPackageManager_Expecter) GetBaseimagePath(baseimage interface{}, force interface{}) *MockPackageManager_GetBaseimagePath_Call { return &MockPackageManager_GetBaseimagePath_Call{Call: _e.mock.On("GetBaseimagePath", baseimage, force)} } func (_c *MockPackageManager_GetBaseimagePath_Call) Run(run func(baseimage string, force bool)) *MockPackageManager_GetBaseimagePath_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 bool - if args[1] != nil { - arg1 = args[1].(bool) - } - run( - arg0, - arg1, - ) + run(args[0].(string), args[1].(bool)) }) return _c } @@ -1319,20 +1246,14 @@ type MockPackageManager_GetDependencyPath_Call struct { } // GetDependencyPath is a helper method to define mock.On call -// - filename string +// - filename func (_e *MockPackageManager_Expecter) GetDependencyPath(filename interface{}) *MockPackageManager_GetDependencyPath_Call { return &MockPackageManager_GetDependencyPath_Call{Call: _e.mock.On("GetDependencyPath", filename)} } func (_c *MockPackageManager_GetDependencyPath_Call) Run(run func(filename string)) *MockPackageManager_GetDependencyPath_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go new file mode 100644 index 0000000..2012360 --- /dev/null +++ b/internal/installer/node/node.go @@ -0,0 +1,444 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package node + +import ( + "fmt" + "log" + "net" + "os" + "path/filepath" + "syscall" + "time" + + "github.com/codesphere-cloud/oms/internal/util" + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" + "golang.org/x/crypto/ssh/knownhosts" + "golang.org/x/term" +) + +type Node struct { + Name string `json:"name"` + ExternalIP string `json:"external_ip"` + InternalIP string `json:"internal_ip"` +} + +type NodeManager struct { + FileIO util.FileIO + KeyPath string +} + +func (n *NodeManager) getHostKeyCallback() (ssh.HostKeyCallback, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get user home directory: %w", err) + } + + knownHostsPath := filepath.Join(homeDir, ".ssh", "known_hosts") + hostKeyCallback, err := knownhosts.New(knownHostsPath) + if err != nil { + sshDir := filepath.Join(homeDir, ".ssh") + if err := os.MkdirAll(sshDir, 0700); err != nil { + return nil, fmt.Errorf("failed to create .ssh directory: %w", err) + } + if _, err := os.Create(knownHostsPath); err != nil { + return nil, fmt.Errorf("failed to create known_hosts file: %w", err) + } + hostKeyCallback, err = knownhosts.New(knownHostsPath) + if err != nil { + return nil, fmt.Errorf("failed to load known_hosts: %w", err) + } + } + + return hostKeyCallback, nil +} + +func (n *NodeManager) getAuthMethods() ([]ssh.AuthMethod, error) { + var authMethods []ssh.AuthMethod + + if authSocket := os.Getenv("SSH_AUTH_SOCK"); authSocket != "" { + conn, err := net.Dial("unix", authSocket) + if err == nil { + agentClient := agent.NewClient(conn) + authMethods = append(authMethods, ssh.PublicKeysCallback(agentClient.Signers)) + return authMethods, nil + } + fmt.Printf("Could not connect to SSH Agent (%s): %v\n", authSocket, err) + } + + if n.KeyPath != "" { + fmt.Println("Falling back to private key file authentication.") + + key, err := n.FileIO.ReadFile(n.KeyPath) + if err != nil { + return nil, fmt.Errorf("failed to read private key file %s: %v", n.KeyPath, err) + } + + signer, err := ssh.ParsePrivateKey(key) + if err == nil { + authMethods = append(authMethods, ssh.PublicKeys(signer)) + return authMethods, nil + } + if _, ok := err.(*ssh.PassphraseMissingError); ok { + fmt.Printf("Enter passphrase for key '%s': ", n.KeyPath) + passphraseBytes, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println() + + if err != nil { + return nil, fmt.Errorf("failed to read passphrase: %v", err) + } + + signer, err = ssh.ParsePrivateKeyWithPassphrase(key, passphraseBytes) + for i := range passphraseBytes { + passphraseBytes[i] = 0 + } + + if err != nil { + return nil, fmt.Errorf("failed to parse private key with passphrase: %v", err) + } + authMethods = append(authMethods, ssh.PublicKeys(signer)) + return authMethods, nil + } + return nil, fmt.Errorf("failed to parse private key: %v", err) + } + + if len(authMethods) == 0 { + return nil, fmt.Errorf("no valid authentication methods configured. Check SSH_AUTH_SOCK and private key path") + } + + return authMethods, nil +} + +func (n *NodeManager) connectToJumpbox(ip, username string) (*ssh.Client, error) { + authMethods, err := n.getAuthMethods() + if err != nil { + return nil, fmt.Errorf("jumpbox authentication setup failed: %v", err) + } + + hostKeyCallback, err := n.getHostKeyCallback() + if err != nil { + return nil, fmt.Errorf("failed to get host key callback: %w", err) + } + + config := &ssh.ClientConfig{ + User: username, + Auth: authMethods, + Timeout: 10 * time.Second, + HostKeyCallback: hostKeyCallback, + } + + addr := fmt.Sprintf("%s:22", ip) + jumpboxClient, err := ssh.Dial("tcp", addr, config) + if err != nil { + return nil, fmt.Errorf("failed to dial jumpbox %s: %v", addr, err) + } + + if err := n.forwardAgent(jumpboxClient, nil); err != nil { + fmt.Printf(" Warning: Agent forwarding setup failed on jumpbox: %v\n", err) + } + + return jumpboxClient, nil +} + +func (n *NodeManager) forwardAgent(client *ssh.Client, session *ssh.Session) error { + authSocket := os.Getenv("SSH_AUTH_SOCK") + if authSocket == "" { + log.Printf("SSH_AUTH_SOCK not set. Cannot perform agent forwarding") + } else { + conn, err := net.Dial("unix", authSocket) + if err != nil { + log.Printf("failed to dial SSH agent socket: %v", err) + } else { + ag := agent.NewClient(conn) + if err := agent.ForwardToAgent(client, ag); err != nil { + log.Printf("failed to forward agent to remote client: %v", err) + } + if session != nil { + if err := agent.RequestAgentForwarding(session); err != nil { + log.Printf("failed to request agent forwarding on session: %v", err) + } + } + } + + } + return nil +} + +func (n *NodeManager) RunSSHCommand(jumpboxIp string, ip string, username string, command string) error { + client, err := n.GetClient(jumpboxIp, ip, username) + if err != nil { + return fmt.Errorf("failed to get client: %w", err) + } + defer client.Close() + session, err := client.NewSession() + if err != nil { + return fmt.Errorf("failed to create session on jumpbox: %v", err) + } + defer session.Close() + + if err := n.forwardAgent(client, session); err != nil { + fmt.Printf(" Warning: Agent forwarding setup failed on session: %v\n", err) + } + + session.Stdout = os.Stdout + session.Stderr = os.Stderr + if err := session.Start(command); err != nil { + return fmt.Errorf("failed to start command: %v", err) + } + + if err := session.Wait(); err != nil { + return fmt.Errorf("command failed: %w", err) + } + + return nil +} + +func (n *NodeManager) GetClient(jumpboxIp string, ip string, username string) (*ssh.Client, error) { + + authMethods, err := n.getAuthMethods() + if err != nil { + return nil, fmt.Errorf("failed to get authentication methods: %w", err) + } + + hostKeyCallback, err := n.getHostKeyCallback() + if err != nil { + return nil, fmt.Errorf("failed to get host key callback: %w", err) + } + + if jumpboxIp != "" { + jbClient, err := n.connectToJumpbox(jumpboxIp, "ubuntu") + if err != nil { + return nil, fmt.Errorf("failed to connect to jumpbox: %v", err) + } + + finalTargetConfig := &ssh.ClientConfig{ + User: username, + Auth: authMethods, + Timeout: 10 * time.Second, + HostKeyCallback: hostKeyCallback, + } + + finalAddr := fmt.Sprintf("%s:22", ip) + jbConn, err := jbClient.Dial("tcp", finalAddr) + if err != nil { + return nil, fmt.Errorf("failed to create connection through jumpbox: %v", err) + } + finalClient, channels, requests, err := ssh.NewClientConn(jbConn, finalAddr, finalTargetConfig) + if err != nil { + return nil, fmt.Errorf("failed to perform SSH handshake through jumpbox: %v", err) + } + + return ssh.NewClient(finalClient, channels, requests), nil + } + + config := &ssh.ClientConfig{ + User: username, + Auth: authMethods, + Timeout: 10 * time.Second, + HostKeyCallback: hostKeyCallback, + } + + addr := fmt.Sprintf("%s:22", ip) + client, err := ssh.Dial("tcp", addr, config) + if err != nil { + return nil, fmt.Errorf("failed to dial: %v", err) + } + return client, nil +} + +func (n *NodeManager) GetSFTPClient(jumpboxIp string, ip string, username string) (*sftp.Client, error) { + client, err := n.GetClient(jumpboxIp, ip, username) + if err != nil { + return nil, fmt.Errorf("failed to get SSH client: %v", err) + } + sftpClient, err := sftp.NewClient(client) + if err != nil { + return nil, fmt.Errorf("failed to create SFTP client: %v", err) + } + return sftpClient, nil +} + +func (nm *NodeManager) EnsureDirectoryExists(ip string, username string, dir string) error { + cmd := fmt.Sprintf("mkdir -p '%s'", dir) + return nm.RunSSHCommand("", ip, username, cmd) +} + +func (n *NodeManager) CopyFile(jumpboxIp string, ip string, username string, src string, dst string) error { + client, err := n.GetSFTPClient(jumpboxIp, ip, username) + if err != nil { + return fmt.Errorf("failed to get SSH client: %v", err) + } + defer client.Close() + + srcFile, err := n.FileIO.Open(src) + if err != nil { + return fmt.Errorf("failed to open source file %s: %v", src, err) + } + defer srcFile.Close() + + dstFile, err := client.Create(dst) + if err != nil { + return fmt.Errorf("failed to create destination file %s: %v", dst, err) + } + defer dstFile.Close() + + _, err = dstFile.ReadFrom(srcFile) + if err != nil { + return fmt.Errorf("failed to copy data from %s to %s: %v", src, dst, err) + } + + return nil +} + +func (n *Node) HasCommand(nm *NodeManager, command string) bool { + checkCommand := fmt.Sprintf("command -v %s >/dev/null 2>&1", command) + err := nm.RunSSHCommand("", n.ExternalIP, "root", checkCommand) + if err != nil { + return false + } + return true +} + +func (n *Node) InstallOms(nm *NodeManager) error { + remoteCommands := []string{ + "wget -qO- 'https://api.github.com/repos/codesphere-cloud/oms/releases/latest' | jq -r '.assets[] | select(.name | match(\"oms-cli.*linux_amd64\")) | .browser_download_url' | xargs wget -O oms-cli", + "chmod +x oms-cli; sudo mv oms-cli /usr/local/bin/", + "curl -LO https://github.com/getsops/sops/releases/download/v3.11.0/sops-v3.11.0.linux.amd64; sudo mv sops-v3.11.0.linux.amd64 /usr/local/bin/sops; sudo chmod +x /usr/local/bin/sops", + "wget https://dl.filippo.io/age/latest?for=linux/amd64 -O age.tar.gz; tar -xvf age.tar.gz; sudo mv age/age* /usr/local/bin/", + } + for _, cmd := range remoteCommands { + err := nm.RunSSHCommand("", n.ExternalIP, "root", cmd) + if err != nil { + return fmt.Errorf("failed to run remote command '%s': %w", cmd, err) + } + } + return nil +} + +func (n *Node) CopyFile(nm *NodeManager, src string, dst string) error { + err := nm.EnsureDirectoryExists(n.ExternalIP, "root", filepath.Dir(dst)) + if err != nil { + return fmt.Errorf("failed to ensure directory exists: %w", err) + } + return nm.CopyFile("", n.ExternalIP, "root", src, dst) +} + +func (n *Node) HasAcceptEnvConfigured(jumpbox *Node, nm *NodeManager) bool { + checkCommand := "sudo grep -E '^AcceptEnv OMS_PORTAL_API_KEY' /etc/ssh/sshd_config >/dev/null 2>&1" + err := n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommand) + if err != nil { + return false + } + return true +} + +func (n *Node) ConfigureAcceptEnv(jumpbox *Node, nm *NodeManager) error { + cmds := []string{ + "sudo sed -i 's/^#\\?AcceptEnv.*/AcceptEnv OMS_PORTAL_API_KEY/' /etc/ssh/sshd_config", + "sudo systemctl restart sshd", + } + for _, cmd := range cmds { + err := n.RunSSHCommand(jumpbox, nm, "ubuntu", cmd) + if err != nil { + return fmt.Errorf("failed to run command '%s': %w", cmd, err) + } + } + return nil +} + +func (n *Node) HasRootLoginEnabled(jumpbox *Node, nm *NodeManager) bool { + checkCommandPermit := "sudo grep -E '^PermitRootLogin yes' /etc/ssh/sshd_config >/dev/null 2>&1" + err := n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommandPermit) + if err != nil { + return false + } + checkCommandAuthorizedKeys := "sudo grep -E '^no-port-forwarding' /root/.ssh/authorized_keys >/dev/null 2>&1" + err = n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommandAuthorizedKeys) + if err == nil { + return false + } + return true +} + +func (n *Node) HasFile(jumpbox *Node, nm *NodeManager, filePath string) bool { + checkCommand := fmt.Sprintf("test -f '%s'", filePath) + err := n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommand) + if err != nil { + return false + } + return true +} + +func (n *Node) RunSSHCommand(jumpbox *Node, nm *NodeManager, username string, command string) error { + if jumpbox == nil { + return nm.RunSSHCommand("", n.ExternalIP, username, command) + } + + return nm.RunSSHCommand(jumpbox.ExternalIP, n.InternalIP, "ubuntu", command) +} + +func (n *Node) EnableRootLogin(jumpbox *Node, nm *NodeManager) error { + cmds := []string{ + "sudo sed-i 's/^#\\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config", + "sudo sed -i 's/no-port-forwarding.*$//g' /root/.ssh/authorized_keys", + "sudo systemctl restart sshd", + } + for _, cmd := range cmds { + err := n.RunSSHCommand(jumpbox, nm, "ubuntu", cmd) + if err != nil { + return fmt.Errorf("failed to run command '%s': %w", cmd, err) + } + } + return nil +} + +func (n *Node) InstallK0s(nm *NodeManager, k0sBinaryPath string, k0sConfigPath string, force bool) error { + remoteK0sDir := "/usr/local/bin" + remoteK0sBinary := filepath.Join(remoteK0sDir, "k0s") + remoteConfigPath := "/etc/k0s/k0s.yaml" + + log.Printf("Copying k0s binary to %s:%s", n.ExternalIP, remoteK0sBinary) + if err := n.CopyFile(nm, k0sBinaryPath, remoteK0sBinary); err != nil { + return fmt.Errorf("failed to copy k0s binary: %w", err) + } + + log.Printf("Making k0s binary executable on %s", n.ExternalIP) + chmodCmd := fmt.Sprintf("chmod +x %s", remoteK0sBinary) + if err := nm.RunSSHCommand("", n.ExternalIP, "root", chmodCmd); err != nil { + return fmt.Errorf("failed to make k0s binary executable: %w", err) + } + + if k0sConfigPath != "" { + log.Printf("Copying k0s config to %s:%s", n.ExternalIP, remoteConfigPath) + if err := nm.EnsureDirectoryExists(n.ExternalIP, "root", "/etc/k0s"); err != nil { + return fmt.Errorf("failed to create /etc/k0s directory: %w", err) + } + if err := nm.CopyFile("", n.ExternalIP, "root", k0sConfigPath, remoteConfigPath); err != nil { + return fmt.Errorf("failed to copy k0s config: %w", err) + } + } + + installCmd := fmt.Sprintf("sudo %s install controller", remoteK0sBinary) + if k0sConfigPath != "" { + installCmd += fmt.Sprintf(" --config %s", remoteConfigPath) + } else { + installCmd += " --single" + } + if force { + installCmd += " --force" + } + + log.Printf("Installing k0s on %s", n.ExternalIP) + if err := nm.RunSSHCommand("", n.ExternalIP, "root", installCmd); err != nil { + return fmt.Errorf("failed to install k0s: %w", err) + } + + log.Printf("k0s successfully installed on %s", n.ExternalIP) + log.Printf("You can start it using: ssh root@%s 'sudo %s start'", n.ExternalIP, remoteK0sBinary) + log.Printf("You can check the status using: ssh root@%s 'sudo %s status'", n.ExternalIP, remoteK0sBinary) + + return nil +} diff --git a/internal/portal/mocks.go b/internal/portal/mocks.go index c162a3e..8337236 100644 --- a/internal/portal/mocks.go +++ b/internal/portal/mocks.go @@ -61,32 +61,16 @@ type MockHttp_Download_Call struct { } // Download is a helper method to define mock.On call -// - url string -// - file io.Writer -// - quiet bool +// - url +// - file +// - quiet func (_e *MockHttp_Expecter) Download(url interface{}, file interface{}, quiet interface{}) *MockHttp_Download_Call { return &MockHttp_Download_Call{Call: _e.mock.On("Download", url, file, quiet)} } func (_c *MockHttp_Download_Call) Run(run func(url string, file io.Writer, quiet bool)) *MockHttp_Download_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 io.Writer - if args[1] != nil { - arg1 = args[1].(io.Writer) - } - var arg2 bool - if args[2] != nil { - arg2 = args[2].(bool) - } - run( - arg0, - arg1, - arg2, - ) + run(args[0].(string), args[1].(io.Writer), args[2].(bool)) }) return _c } @@ -135,20 +119,14 @@ type MockHttp_Get_Call struct { } // Get is a helper method to define mock.On call -// - url string +// - url func (_e *MockHttp_Expecter) Get(url interface{}) *MockHttp_Get_Call { return &MockHttp_Get_Call{Call: _e.mock.On("Get", url)} } func (_c *MockHttp_Get_Call) Run(run func(url string)) *MockHttp_Get_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -197,32 +175,16 @@ type MockHttp_Request_Call struct { } // Request is a helper method to define mock.On call -// - url string -// - method string -// - body io.Reader +// - url +// - method +// - body func (_e *MockHttp_Expecter) Request(url interface{}, method interface{}, body interface{}) *MockHttp_Request_Call { return &MockHttp_Request_Call{Call: _e.mock.On("Request", url, method, body)} } func (_c *MockHttp_Request_Call) Run(run func(url string, method string, body io.Reader)) *MockHttp_Request_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 io.Reader - if args[2] != nil { - arg2 = args[2].(io.Reader) - } - run( - arg0, - arg1, - arg2, - ) + run(args[0].(string), args[1].(string), args[2].(io.Reader)) }) return _c } @@ -287,44 +249,18 @@ type MockPortal_DownloadBuildArtifact_Call struct { } // DownloadBuildArtifact is a helper method to define mock.On call -// - product Product -// - build Build -// - file io.Writer -// - startByte int -// - quiet bool +// - product +// - build +// - file +// - startByte +// - quiet func (_e *MockPortal_Expecter) DownloadBuildArtifact(product interface{}, build interface{}, file interface{}, startByte interface{}, quiet interface{}) *MockPortal_DownloadBuildArtifact_Call { return &MockPortal_DownloadBuildArtifact_Call{Call: _e.mock.On("DownloadBuildArtifact", product, build, file, startByte, quiet)} } func (_c *MockPortal_DownloadBuildArtifact_Call) Run(run func(product Product, build Build, file io.Writer, startByte int, quiet bool)) *MockPortal_DownloadBuildArtifact_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 Product - if args[0] != nil { - arg0 = args[0].(Product) - } - var arg1 Build - if args[1] != nil { - arg1 = args[1].(Build) - } - var arg2 io.Writer - if args[2] != nil { - arg2 = args[2].(io.Writer) - } - var arg3 int - if args[3] != nil { - arg3 = args[3].(int) - } - var arg4 bool - if args[4] != nil { - arg4 = args[4].(bool) - } - run( - arg0, - arg1, - arg2, - arg3, - arg4, - ) + run(args[0].(Product), args[1].(Build), args[2].(io.Writer), args[3].(int), args[4].(bool)) }) return _c } @@ -371,20 +307,14 @@ type MockPortal_GetApiKeyId_Call struct { } // GetApiKeyId is a helper method to define mock.On call -// - oldKey string +// - oldKey func (_e *MockPortal_Expecter) GetApiKeyId(oldKey interface{}) *MockPortal_GetApiKeyId_Call { return &MockPortal_GetApiKeyId_Call{Call: _e.mock.On("GetApiKeyId", oldKey)} } func (_c *MockPortal_GetApiKeyId_Call) Run(run func(oldKey string)) *MockPortal_GetApiKeyId_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -431,32 +361,16 @@ type MockPortal_GetBuild_Call struct { } // GetBuild is a helper method to define mock.On call -// - product Product -// - version string -// - hash string +// - product +// - version +// - hash func (_e *MockPortal_Expecter) GetBuild(product interface{}, version interface{}, hash interface{}) *MockPortal_GetBuild_Call { return &MockPortal_GetBuild_Call{Call: _e.mock.On("GetBuild", product, version, hash)} } func (_c *MockPortal_GetBuild_Call) Run(run func(product Product, version string, hash string)) *MockPortal_GetBuild_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 Product - if args[0] != nil { - arg0 = args[0].(Product) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) + run(args[0].(Product), args[1].(string), args[2].(string)) }) return _c } @@ -558,20 +472,14 @@ type MockPortal_ListBuilds_Call struct { } // ListBuilds is a helper method to define mock.On call -// - product Product +// - product func (_e *MockPortal_Expecter) ListBuilds(product interface{}) *MockPortal_ListBuilds_Call { return &MockPortal_ListBuilds_Call{Call: _e.mock.On("ListBuilds", product)} } func (_c *MockPortal_ListBuilds_Call) Run(run func(product Product)) *MockPortal_ListBuilds_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 Product - if args[0] != nil { - arg0 = args[0].(Product) - } - run( - arg0, - ) + run(args[0].(Product)) }) return _c } @@ -620,38 +528,17 @@ type MockPortal_RegisterAPIKey_Call struct { } // RegisterAPIKey is a helper method to define mock.On call -// - owner string -// - organization string -// - role string -// - expiresAt time.Time +// - owner +// - organization +// - role +// - expiresAt func (_e *MockPortal_Expecter) RegisterAPIKey(owner interface{}, organization interface{}, role interface{}, expiresAt interface{}) *MockPortal_RegisterAPIKey_Call { return &MockPortal_RegisterAPIKey_Call{Call: _e.mock.On("RegisterAPIKey", owner, organization, role, expiresAt)} } func (_c *MockPortal_RegisterAPIKey_Call) Run(run func(owner string, organization string, role string, expiresAt time.Time)) *MockPortal_RegisterAPIKey_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - var arg3 time.Time - if args[3] != nil { - arg3 = args[3].(time.Time) - } - run( - arg0, - arg1, - arg2, - arg3, - ) + run(args[0].(string), args[1].(string), args[2].(string), args[3].(time.Time)) }) return _c } @@ -689,20 +576,14 @@ type MockPortal_RevokeAPIKey_Call struct { } // RevokeAPIKey is a helper method to define mock.On call -// - key string +// - key func (_e *MockPortal_Expecter) RevokeAPIKey(key interface{}) *MockPortal_RevokeAPIKey_Call { return &MockPortal_RevokeAPIKey_Call{Call: _e.mock.On("RevokeAPIKey", key)} } func (_c *MockPortal_RevokeAPIKey_Call) Run(run func(key string)) *MockPortal_RevokeAPIKey_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -740,26 +621,15 @@ type MockPortal_UpdateAPIKey_Call struct { } // UpdateAPIKey is a helper method to define mock.On call -// - key string -// - expiresAt time.Time +// - key +// - expiresAt func (_e *MockPortal_Expecter) UpdateAPIKey(key interface{}, expiresAt interface{}) *MockPortal_UpdateAPIKey_Call { return &MockPortal_UpdateAPIKey_Call{Call: _e.mock.On("UpdateAPIKey", key, expiresAt)} } func (_c *MockPortal_UpdateAPIKey_Call) Run(run func(key string, expiresAt time.Time)) *MockPortal_UpdateAPIKey_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 time.Time - if args[1] != nil { - arg1 = args[1].(time.Time) - } - run( - arg0, - arg1, - ) + run(args[0].(string), args[1].(time.Time)) }) return _c } @@ -797,26 +667,15 @@ type MockPortal_VerifyBuildArtifactDownload_Call struct { } // VerifyBuildArtifactDownload is a helper method to define mock.On call -// - file io.Reader -// - download Build +// - file +// - download func (_e *MockPortal_Expecter) VerifyBuildArtifactDownload(file interface{}, download interface{}) *MockPortal_VerifyBuildArtifactDownload_Call { return &MockPortal_VerifyBuildArtifactDownload_Call{Call: _e.mock.On("VerifyBuildArtifactDownload", file, download)} } func (_c *MockPortal_VerifyBuildArtifactDownload_Call) Run(run func(file io.Reader, download Build)) *MockPortal_VerifyBuildArtifactDownload_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 io.Reader - if args[0] != nil { - arg0 = args[0].(io.Reader) - } - var arg1 Build - if args[1] != nil { - arg1 = args[1].(Build) - } - run( - arg0, - arg1, - ) + run(args[0].(io.Reader), args[1].(Build)) }) return _c } @@ -892,20 +751,14 @@ type MockHttpClient_Do_Call struct { } // Do is a helper method to define mock.On call -// - request *http.Request +// - request func (_e *MockHttpClient_Expecter) Do(request interface{}) *MockHttpClient_Do_Call { return &MockHttpClient_Do_Call{Call: _e.mock.On("Do", request)} } func (_c *MockHttpClient_Do_Call) Run(run func(request *http.Request)) *MockHttpClient_Do_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 *http.Request - if args[0] != nil { - arg0 = args[0].(*http.Request) - } - run( - arg0, - ) + run(args[0].(*http.Request)) }) return _c } diff --git a/internal/system/mocks.go b/internal/system/mocks.go index ee80bc6..cf8e929 100644 --- a/internal/system/mocks.go +++ b/internal/system/mocks.go @@ -58,32 +58,16 @@ type MockImageManager_BuildImage_Call struct { } // BuildImage is a helper method to define mock.On call -// - dockerfile string -// - tag string -// - buildContext string +// - dockerfile +// - tag +// - buildContext func (_e *MockImageManager_Expecter) BuildImage(dockerfile interface{}, tag interface{}, buildContext interface{}) *MockImageManager_BuildImage_Call { return &MockImageManager_BuildImage_Call{Call: _e.mock.On("BuildImage", dockerfile, tag, buildContext)} } func (_c *MockImageManager_BuildImage_Call) Run(run func(dockerfile string, tag string, buildContext string)) *MockImageManager_BuildImage_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) + run(args[0].(string), args[1].(string), args[2].(string)) }) return _c } @@ -121,20 +105,14 @@ type MockImageManager_LoadImage_Call struct { } // LoadImage is a helper method to define mock.On call -// - imageTarPath string +// - imageTarPath func (_e *MockImageManager_Expecter) LoadImage(imageTarPath interface{}) *MockImageManager_LoadImage_Call { return &MockImageManager_LoadImage_Call{Call: _e.mock.On("LoadImage", imageTarPath)} } func (_c *MockImageManager_LoadImage_Call) Run(run func(imageTarPath string)) *MockImageManager_LoadImage_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -172,20 +150,14 @@ type MockImageManager_PushImage_Call struct { } // PushImage is a helper method to define mock.On call -// - tag string +// - tag func (_e *MockImageManager_Expecter) PushImage(tag interface{}) *MockImageManager_PushImage_Call { return &MockImageManager_PushImage_Call{Call: _e.mock.On("PushImage", tag)} } func (_c *MockImageManager_PushImage_Call) Run(run func(tag string)) *MockImageManager_PushImage_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } diff --git a/internal/util/filewriter.go b/internal/util/filewriter.go index 48938df..8c2685f 100644 --- a/internal/util/filewriter.go +++ b/internal/util/filewriter.go @@ -17,6 +17,7 @@ type FileIO interface { MkdirAll(path string, perm os.FileMode) error OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) WriteFile(filename string, data []byte, perm os.FileMode) error + ReadFile(filename string) ([]byte, error) ReadDir(dirname string) ([]os.DirEntry, error) CreateAndWrite(filePath string, data []byte, fileType string) error } @@ -83,6 +84,10 @@ func (fs *FilesystemWriter) WriteFile(filename string, data []byte, perm os.File return os.WriteFile(filename, data, perm) } +func (fs *FilesystemWriter) ReadFile(filename string) ([]byte, error) { + return os.ReadFile(filename) +} + func (fs *FilesystemWriter) ReadDir(dirname string) ([]os.DirEntry, error) { return os.ReadDir(dirname) } diff --git a/internal/util/mocks.go b/internal/util/mocks.go index 24e1f4b..394f6b8 100644 --- a/internal/util/mocks.go +++ b/internal/util/mocks.go @@ -70,26 +70,15 @@ type MockDockerfileManager_UpdateFromStatement_Call struct { } // UpdateFromStatement is a helper method to define mock.On call -// - dockerfile io.Reader -// - baseImage string +// - dockerfile +// - baseImage func (_e *MockDockerfileManager_Expecter) UpdateFromStatement(dockerfile interface{}, baseImage interface{}) *MockDockerfileManager_UpdateFromStatement_Call { return &MockDockerfileManager_UpdateFromStatement_Call{Call: _e.mock.On("UpdateFromStatement", dockerfile, baseImage)} } func (_c *MockDockerfileManager_UpdateFromStatement_Call) Run(run func(dockerfile io.Reader, baseImage string)) *MockDockerfileManager_UpdateFromStatement_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 io.Reader - if args[0] != nil { - arg0 = args[0].(io.Reader) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) + run(args[0].(io.Reader), args[1].(string)) }) return _c } @@ -165,20 +154,14 @@ type MockFileIO_Create_Call struct { } // Create is a helper method to define mock.On call -// - filename string +// - filename func (_e *MockFileIO_Expecter) Create(filename interface{}) *MockFileIO_Create_Call { return &MockFileIO_Create_Call{Call: _e.mock.On("Create", filename)} } func (_c *MockFileIO_Create_Call) Run(run func(filename string)) *MockFileIO_Create_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -216,32 +199,16 @@ type MockFileIO_CreateAndWrite_Call struct { } // CreateAndWrite is a helper method to define mock.On call -// - filePath string -// - data []byte -// - fileType string +// - filePath +// - data +// - fileType func (_e *MockFileIO_Expecter) CreateAndWrite(filePath interface{}, data interface{}, fileType interface{}) *MockFileIO_CreateAndWrite_Call { return &MockFileIO_CreateAndWrite_Call{Call: _e.mock.On("CreateAndWrite", filePath, data, fileType)} } func (_c *MockFileIO_CreateAndWrite_Call) Run(run func(filePath string, data []byte, fileType string)) *MockFileIO_CreateAndWrite_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 []byte - if args[1] != nil { - arg1 = args[1].([]byte) - } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } - run( - arg0, - arg1, - arg2, - ) + run(args[0].(string), args[1].([]byte), args[2].(string)) }) return _c } @@ -279,20 +246,14 @@ type MockFileIO_Exists_Call struct { } // Exists is a helper method to define mock.On call -// - filename string +// - filename func (_e *MockFileIO_Expecter) Exists(filename interface{}) *MockFileIO_Exists_Call { return &MockFileIO_Exists_Call{Call: _e.mock.On("Exists", filename)} } func (_c *MockFileIO_Exists_Call) Run(run func(filename string)) *MockFileIO_Exists_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -339,20 +300,14 @@ type MockFileIO_IsDirectory_Call struct { } // IsDirectory is a helper method to define mock.On call -// - filename string +// - filename func (_e *MockFileIO_Expecter) IsDirectory(filename interface{}) *MockFileIO_IsDirectory_Call { return &MockFileIO_IsDirectory_Call{Call: _e.mock.On("IsDirectory", filename)} } func (_c *MockFileIO_IsDirectory_Call) Run(run func(filename string)) *MockFileIO_IsDirectory_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -390,26 +345,15 @@ type MockFileIO_MkdirAll_Call struct { } // MkdirAll is a helper method to define mock.On call -// - path string -// - perm os.FileMode +// - path +// - perm func (_e *MockFileIO_Expecter) MkdirAll(path interface{}, perm interface{}) *MockFileIO_MkdirAll_Call { return &MockFileIO_MkdirAll_Call{Call: _e.mock.On("MkdirAll", path, perm)} } func (_c *MockFileIO_MkdirAll_Call) Run(run func(path string, perm os.FileMode)) *MockFileIO_MkdirAll_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 os.FileMode - if args[1] != nil { - arg1 = args[1].(os.FileMode) - } - run( - arg0, - arg1, - ) + run(args[0].(string), args[1].(os.FileMode)) }) return _c } @@ -458,20 +402,14 @@ type MockFileIO_Open_Call struct { } // Open is a helper method to define mock.On call -// - filename string +// - filename func (_e *MockFileIO_Expecter) Open(filename interface{}) *MockFileIO_Open_Call { return &MockFileIO_Open_Call{Call: _e.mock.On("Open", filename)} } func (_c *MockFileIO_Open_Call) Run(run func(filename string)) *MockFileIO_Open_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -520,20 +458,14 @@ type MockFileIO_OpenAppend_Call struct { } // OpenAppend is a helper method to define mock.On call -// - filename string +// - filename func (_e *MockFileIO_Expecter) OpenAppend(filename interface{}) *MockFileIO_OpenAppend_Call { return &MockFileIO_OpenAppend_Call{Call: _e.mock.On("OpenAppend", filename)} } func (_c *MockFileIO_OpenAppend_Call) Run(run func(filename string)) *MockFileIO_OpenAppend_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -582,32 +514,16 @@ type MockFileIO_OpenFile_Call struct { } // OpenFile is a helper method to define mock.On call -// - name string -// - flag int -// - perm os.FileMode +// - name +// - flag +// - perm func (_e *MockFileIO_Expecter) OpenFile(name interface{}, flag interface{}, perm interface{}) *MockFileIO_OpenFile_Call { return &MockFileIO_OpenFile_Call{Call: _e.mock.On("OpenFile", name, flag, perm)} } func (_c *MockFileIO_OpenFile_Call) Run(run func(name string, flag int, perm os.FileMode)) *MockFileIO_OpenFile_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 int - if args[1] != nil { - arg1 = args[1].(int) - } - var arg2 os.FileMode - if args[2] != nil { - arg2 = args[2].(os.FileMode) - } - run( - arg0, - arg1, - arg2, - ) + run(args[0].(string), args[1].(int), args[2].(os.FileMode)) }) return _c } @@ -656,20 +572,14 @@ type MockFileIO_ReadDir_Call struct { } // ReadDir is a helper method to define mock.On call -// - dirname string +// - dirname func (_e *MockFileIO_Expecter) ReadDir(dirname interface{}) *MockFileIO_ReadDir_Call { return &MockFileIO_ReadDir_Call{Call: _e.mock.On("ReadDir", dirname)} } func (_c *MockFileIO_ReadDir_Call) Run(run func(dirname string)) *MockFileIO_ReadDir_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) + run(args[0].(string)) }) return _c } @@ -684,6 +594,62 @@ func (_c *MockFileIO_ReadDir_Call) RunAndReturn(run func(dirname string) ([]os.D return _c } +// ReadFile provides a mock function for the type MockFileIO +func (_mock *MockFileIO) ReadFile(filename string) ([]byte, error) { + ret := _mock.Called(filename) + + if len(ret) == 0 { + panic("no return value specified for ReadFile") + } + + var r0 []byte + var r1 error + if returnFunc, ok := ret.Get(0).(func(string) ([]byte, error)); ok { + return returnFunc(filename) + } + if returnFunc, ok := ret.Get(0).(func(string) []byte); ok { + r0 = returnFunc(filename) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + if returnFunc, ok := ret.Get(1).(func(string) error); ok { + r1 = returnFunc(filename) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockFileIO_ReadFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFile' +type MockFileIO_ReadFile_Call struct { + *mock.Call +} + +// ReadFile is a helper method to define mock.On call +// - filename +func (_e *MockFileIO_Expecter) ReadFile(filename interface{}) *MockFileIO_ReadFile_Call { + return &MockFileIO_ReadFile_Call{Call: _e.mock.On("ReadFile", filename)} +} + +func (_c *MockFileIO_ReadFile_Call) Run(run func(filename string)) *MockFileIO_ReadFile_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockFileIO_ReadFile_Call) Return(bytes []byte, err error) *MockFileIO_ReadFile_Call { + _c.Call.Return(bytes, err) + return _c +} + +func (_c *MockFileIO_ReadFile_Call) RunAndReturn(run func(filename string) ([]byte, error)) *MockFileIO_ReadFile_Call { + _c.Call.Return(run) + return _c +} + // WriteFile provides a mock function for the type MockFileIO func (_mock *MockFileIO) WriteFile(filename string, data []byte, perm os.FileMode) error { ret := _mock.Called(filename, data, perm) @@ -707,32 +673,16 @@ type MockFileIO_WriteFile_Call struct { } // WriteFile is a helper method to define mock.On call -// - filename string -// - data []byte -// - perm os.FileMode +// - filename +// - data +// - perm func (_e *MockFileIO_Expecter) WriteFile(filename interface{}, data interface{}, perm interface{}) *MockFileIO_WriteFile_Call { return &MockFileIO_WriteFile_Call{Call: _e.mock.On("WriteFile", filename, data, perm)} } func (_c *MockFileIO_WriteFile_Call) Run(run func(filename string, data []byte, perm os.FileMode)) *MockFileIO_WriteFile_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - var arg1 []byte - if args[1] != nil { - arg1 = args[1].([]byte) - } - var arg2 os.FileMode - if args[2] != nil { - arg2 = args[2].(os.FileMode) - } - run( - arg0, - arg1, - arg2, - ) + run(args[0].(string), args[1].([]byte), args[2].(os.FileMode)) }) return _c } @@ -862,8 +812,8 @@ type MockTableWriter_AppendHeader_Call struct { } // AppendHeader is a helper method to define mock.On call -// - row table.Row -// - configs ...table.RowConfig +// - row +// - configs func (_e *MockTableWriter_Expecter) AppendHeader(row interface{}, configs ...interface{}) *MockTableWriter_AppendHeader_Call { return &MockTableWriter_AppendHeader_Call{Call: _e.mock.On("AppendHeader", append([]interface{}{row}, configs...)...)} @@ -871,20 +821,13 @@ func (_e *MockTableWriter_Expecter) AppendHeader(row interface{}, configs ...int func (_c *MockTableWriter_AppendHeader_Call) Run(run func(row table.Row, configs ...table.RowConfig)) *MockTableWriter_AppendHeader_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 table.Row - if args[0] != nil { - arg0 = args[0].(table.Row) - } - var arg1 []table.RowConfig - var variadicArgs []table.RowConfig - if len(args) > 1 { - variadicArgs = args[1].([]table.RowConfig) + variadicArgs := make([]table.RowConfig, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(table.RowConfig) + } } - arg1 = variadicArgs - run( - arg0, - arg1..., - ) + run(args[0].(table.Row), variadicArgs...) }) return _c } @@ -916,8 +859,8 @@ type MockTableWriter_AppendRow_Call struct { } // AppendRow is a helper method to define mock.On call -// - row table.Row -// - configs ...table.RowConfig +// - row +// - configs func (_e *MockTableWriter_Expecter) AppendRow(row interface{}, configs ...interface{}) *MockTableWriter_AppendRow_Call { return &MockTableWriter_AppendRow_Call{Call: _e.mock.On("AppendRow", append([]interface{}{row}, configs...)...)} @@ -925,20 +868,13 @@ func (_e *MockTableWriter_Expecter) AppendRow(row interface{}, configs ...interf func (_c *MockTableWriter_AppendRow_Call) Run(run func(row table.Row, configs ...table.RowConfig)) *MockTableWriter_AppendRow_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 table.Row - if args[0] != nil { - arg0 = args[0].(table.Row) - } - var arg1 []table.RowConfig - var variadicArgs []table.RowConfig - if len(args) > 1 { - variadicArgs = args[1].([]table.RowConfig) + variadicArgs := make([]table.RowConfig, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(table.RowConfig) + } } - arg1 = variadicArgs - run( - arg0, - arg1..., - ) + run(args[0].(table.Row), variadicArgs...) }) return _c } From 5a907ab6f8bffd4d533777696dae37a598c3e309 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:55:08 +0000 Subject: [PATCH 02/32] chore(docs): Auto-update docs and licenses Signed-off-by: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> --- NOTICE | 40 ++++- cli/cmd/install_k0s_integration_test.go | 3 + cli/cmd/mocks.go | 17 +- docs/oms-cli_install_k0s.md | 27 ++- internal/installer/mocks.go | 206 ++++++++++++++++++---- internal/portal/mocks.go | 225 ++++++++++++++++++++---- internal/system/mocks.go | 44 ++++- internal/util/mocks.go | 222 ++++++++++++++++++----- 8 files changed, 635 insertions(+), 149 deletions(-) diff --git a/NOTICE b/NOTICE index d7e513d..ed0df75 100644 --- a/NOTICE +++ b/NOTICE @@ -23,9 +23,9 @@ License URL: https://github.com/clipperhouse/uax29/blob/v2.3.0/LICENSE ---------- Module: github.com/codesphere-cloud/cs-go/pkg/io -Version: v0.14.1 +Version: v0.15.0 License: Apache-2.0 -License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.14.1/LICENSE +License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.15.0/LICENSE ---------- Module: github.com/codesphere-cloud/oms/internal/tmpl @@ -77,9 +77,15 @@ License URL: https://github.com/inconshreveable/go-update/blob/8152e7eb6ccf/inte ---------- Module: github.com/jedib0t/go-pretty/v6 -Version: v6.7.5 +Version: v6.7.7 License: MIT -License URL: https://github.com/jedib0t/go-pretty/blob/v6.7.5/LICENSE +License URL: https://github.com/jedib0t/go-pretty/blob/v6.7.7/LICENSE + +---------- +Module: github.com/kr/fs +Version: v0.1.0 +License: BSD-3-Clause +License URL: https://github.com/kr/fs/blob/v0.1.0/LICENSE ---------- Module: github.com/mattn/go-runewidth @@ -87,6 +93,12 @@ Version: v0.0.19 License: MIT License URL: https://github.com/mattn/go-runewidth/blob/v0.0.19/LICENSE +---------- +Module: github.com/pkg/sftp +Version: v1.13.10 +License: BSD-2-Clause +License URL: https://github.com/pkg/sftp/blob/v1.13.10/LICENSE + ---------- Module: github.com/pmezard/go-difflib/difflib Version: v1.0.1-0.20181226105442-5d4384ee4fb2 @@ -155,9 +167,9 @@ License URL: https://github.com/yaml/go-yaml/blob/v3.0.4/LICENSE ---------- Module: golang.org/x/crypto -Version: v0.45.0 +Version: v0.46.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/crypto/+/v0.45.0:LICENSE +License URL: https://cs.opensource.google/go/x/crypto/+/v0.46.0:LICENSE ---------- Module: golang.org/x/oauth2 @@ -165,11 +177,23 @@ Version: v0.33.0 License: BSD-3-Clause License URL: https://cs.opensource.google/go/x/oauth2/+/v0.33.0:LICENSE +---------- +Module: golang.org/x/sys/unix +Version: v0.39.0 +License: BSD-3-Clause +License URL: https://cs.opensource.google/go/x/sys/+/v0.39.0:LICENSE + +---------- +Module: golang.org/x/term +Version: v0.38.0 +License: BSD-3-Clause +License URL: https://cs.opensource.google/go/x/term/+/v0.38.0:LICENSE + ---------- Module: golang.org/x/text -Version: v0.31.0 +Version: v0.32.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/text/+/v0.31.0:LICENSE +License URL: https://cs.opensource.google/go/x/text/+/v0.32.0:LICENSE ---------- Module: gopkg.in/yaml.v3 diff --git a/cli/cmd/install_k0s_integration_test.go b/cli/cmd/install_k0s_integration_test.go index 966315a..8823aa4 100644 --- a/cli/cmd/install_k0s_integration_test.go +++ b/cli/cmd/install_k0s_integration_test.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + //go:build integration // +build integration diff --git a/cli/cmd/mocks.go b/cli/cmd/mocks.go index a1ac35e..2f86863 100644 --- a/cli/cmd/mocks.go +++ b/cli/cmd/mocks.go @@ -74,15 +74,26 @@ type MockOMSUpdater_Update_Call struct { } // Update is a helper method to define mock.On call -// - v -// - repo +// - v semver.Version +// - repo string func (_e *MockOMSUpdater_Expecter) Update(v interface{}, repo interface{}) *MockOMSUpdater_Update_Call { return &MockOMSUpdater_Update_Call{Call: _e.mock.On("Update", v, repo)} } func (_c *MockOMSUpdater_Update_Call) Run(run func(v semver.Version, repo string)) *MockOMSUpdater_Update_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(semver.Version), args[1].(string)) + var arg0 semver.Version + if args[0] != nil { + arg0 = args[0].(semver.Version) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) }) return _c } diff --git a/docs/oms-cli_install_k0s.md b/docs/oms-cli_install_k0s.md index e9c7fa0..08b17f3 100644 --- a/docs/oms-cli_install_k0s.md +++ b/docs/oms-cli_install_k0s.md @@ -10,6 +10,10 @@ or load the k0s binary from the provided package file and install it. If no version is specified, the latest version will be downloaded. If no install config is provided, k0s will be installed with the '--single' flag. +You can also install k0s from a Codesphere install-config file, which will: +- Generate a k0s configuration from the install-config +- Optionally install k0s on remote nodes via SSH + ``` oms-cli install k0s [flags] ``` @@ -29,6 +33,15 @@ $ oms-cli install k0s --package # Path to k0s configuration file, if not set k0s will be installed with the '--single' flag $ oms-cli install k0s --k0s-config +# Path to Codesphere install-config file to generate k0s config from +$ oms-cli install k0s --install-config + +# Remote host IP to install k0s on (requires --ssh-key-path) +$ oms-cli install k0s --remote-host + +# SSH private key path for remote installation +$ oms-cli install k0s --ssh-key-path + # Force new download and installation even if k0s binary exists or is already installed $ oms-cli install k0s --force @@ -37,11 +50,15 @@ $ oms-cli install k0s --force ### Options ``` - -f, --force Force new download and installation - -h, --help help for k0s - --k0s-config string Path to k0s configuration file - -p, --package string Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load k0s from - -v, --version string Version of k0s to install + -f, --force Force new download and installation + -h, --help help for k0s + --install-config string Path to Codesphere install-config file + --k0s-config string Path to k0s configuration file + -p, --package string Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load k0s from + --remote-host string Remote host IP to install k0s on + --remote-user string Remote user for SSH connection (default "root") + --ssh-key-path string SSH private key path for remote installation + -v, --version string Version of k0s to install ``` ### SEE ALSO diff --git a/internal/installer/mocks.go b/internal/installer/mocks.go index fdfaf36..d22ab78 100644 --- a/internal/installer/mocks.go +++ b/internal/installer/mocks.go @@ -69,14 +69,20 @@ type MockConfigManager_ParseConfigYaml_Call struct { } // ParseConfigYaml is a helper method to define mock.On call -// - configPath +// - configPath string func (_e *MockConfigManager_Expecter) ParseConfigYaml(configPath interface{}) *MockConfigManager_ParseConfigYaml_Call { return &MockConfigManager_ParseConfigYaml_Call{Call: _e.mock.On("ParseConfigYaml", configPath)} } func (_c *MockConfigManager_ParseConfigYaml_Call) Run(run func(configPath string)) *MockConfigManager_ParseConfigYaml_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -141,14 +147,20 @@ type MockInstallConfigManager_ApplyProfile_Call struct { } // ApplyProfile is a helper method to define mock.On call -// - profile +// - profile string func (_e *MockInstallConfigManager_Expecter) ApplyProfile(profile interface{}) *MockInstallConfigManager_ApplyProfile_Call { return &MockInstallConfigManager_ApplyProfile_Call{Call: _e.mock.On("ApplyProfile", profile)} } func (_c *MockInstallConfigManager_ApplyProfile_Call) Run(run func(profile string)) *MockInstallConfigManager_ApplyProfile_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -320,14 +332,20 @@ type MockInstallConfigManager_LoadInstallConfigFromFile_Call struct { } // LoadInstallConfigFromFile is a helper method to define mock.On call -// - configPath +// - configPath string func (_e *MockInstallConfigManager_Expecter) LoadInstallConfigFromFile(configPath interface{}) *MockInstallConfigManager_LoadInstallConfigFromFile_Call { return &MockInstallConfigManager_LoadInstallConfigFromFile_Call{Call: _e.mock.On("LoadInstallConfigFromFile", configPath)} } func (_c *MockInstallConfigManager_LoadInstallConfigFromFile_Call) Run(run func(configPath string)) *MockInstallConfigManager_LoadInstallConfigFromFile_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -365,14 +383,20 @@ type MockInstallConfigManager_LoadVaultFromFile_Call struct { } // LoadVaultFromFile is a helper method to define mock.On call -// - vaultPath +// - vaultPath string func (_e *MockInstallConfigManager_Expecter) LoadVaultFromFile(vaultPath interface{}) *MockInstallConfigManager_LoadVaultFromFile_Call { return &MockInstallConfigManager_LoadVaultFromFile_Call{Call: _e.mock.On("LoadVaultFromFile", vaultPath)} } func (_c *MockInstallConfigManager_LoadVaultFromFile_Call) Run(run func(vaultPath string)) *MockInstallConfigManager_LoadVaultFromFile_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -546,15 +570,26 @@ type MockInstallConfigManager_WriteInstallConfig_Call struct { } // WriteInstallConfig is a helper method to define mock.On call -// - configPath -// - withComments +// - configPath string +// - withComments bool func (_e *MockInstallConfigManager_Expecter) WriteInstallConfig(configPath interface{}, withComments interface{}) *MockInstallConfigManager_WriteInstallConfig_Call { return &MockInstallConfigManager_WriteInstallConfig_Call{Call: _e.mock.On("WriteInstallConfig", configPath, withComments)} } func (_c *MockInstallConfigManager_WriteInstallConfig_Call) Run(run func(configPath string, withComments bool)) *MockInstallConfigManager_WriteInstallConfig_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(bool)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 bool + if args[1] != nil { + arg1 = args[1].(bool) + } + run( + arg0, + arg1, + ) }) return _c } @@ -592,15 +627,26 @@ type MockInstallConfigManager_WriteVault_Call struct { } // WriteVault is a helper method to define mock.On call -// - vaultPath -// - withComments +// - vaultPath string +// - withComments bool func (_e *MockInstallConfigManager_Expecter) WriteVault(vaultPath interface{}, withComments interface{}) *MockInstallConfigManager_WriteVault_Call { return &MockInstallConfigManager_WriteVault_Call{Call: _e.mock.On("WriteVault", vaultPath, withComments)} } func (_c *MockInstallConfigManager_WriteVault_Call) Run(run func(vaultPath string, withComments bool)) *MockInstallConfigManager_WriteVault_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(bool)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 bool + if args[1] != nil { + arg1 = args[1].(bool) + } + run( + arg0, + arg1, + ) }) return _c } @@ -674,16 +720,32 @@ type MockK0sManager_Download_Call struct { } // Download is a helper method to define mock.On call -// - version -// - force -// - quiet +// - version string +// - force bool +// - quiet bool func (_e *MockK0sManager_Expecter) Download(version interface{}, force interface{}, quiet interface{}) *MockK0sManager_Download_Call { return &MockK0sManager_Download_Call{Call: _e.mock.On("Download", version, force, quiet)} } func (_c *MockK0sManager_Download_Call) Run(run func(version string, force bool, quiet bool)) *MockK0sManager_Download_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(bool), args[2].(bool)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 bool + if args[1] != nil { + arg1 = args[1].(bool) + } + var arg2 bool + if args[2] != nil { + arg2 = args[2].(bool) + } + run( + arg0, + arg1, + arg2, + ) }) return _c } @@ -774,16 +836,32 @@ type MockK0sManager_Install_Call struct { } // Install is a helper method to define mock.On call -// - configPath -// - k0sPath -// - force +// - configPath string +// - k0sPath string +// - force bool func (_e *MockK0sManager_Expecter) Install(configPath interface{}, k0sPath interface{}, force interface{}) *MockK0sManager_Install_Call { return &MockK0sManager_Install_Call{Call: _e.mock.On("Install", configPath, k0sPath, force)} } func (_c *MockK0sManager_Install_Call) Run(run func(configPath string, k0sPath string, force bool)) *MockK0sManager_Install_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(string), args[2].(bool)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 bool + if args[2] != nil { + arg2 = args[2].(bool) + } + run( + arg0, + arg1, + arg2, + ) }) return _c } @@ -821,14 +899,20 @@ type MockK0sManager_Reset_Call struct { } // Reset is a helper method to define mock.On call -// - k0sPath +// - k0sPath string func (_e *MockK0sManager_Expecter) Reset(k0sPath interface{}) *MockK0sManager_Reset_Call { return &MockK0sManager_Reset_Call{Call: _e.mock.On("Reset", k0sPath)} } func (_c *MockK0sManager_Reset_Call) Run(run func(k0sPath string)) *MockK0sManager_Reset_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -893,14 +977,20 @@ type MockPackageManager_Extract_Call struct { } // Extract is a helper method to define mock.On call -// - force +// - force bool func (_e *MockPackageManager_Expecter) Extract(force interface{}) *MockPackageManager_Extract_Call { return &MockPackageManager_Extract_Call{Call: _e.mock.On("Extract", force)} } func (_c *MockPackageManager_Extract_Call) Run(run func(force bool)) *MockPackageManager_Extract_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(bool)) + var arg0 bool + if args[0] != nil { + arg0 = args[0].(bool) + } + run( + arg0, + ) }) return _c } @@ -938,15 +1028,26 @@ type MockPackageManager_ExtractDependency_Call struct { } // ExtractDependency is a helper method to define mock.On call -// - file -// - force +// - file string +// - force bool func (_e *MockPackageManager_Expecter) ExtractDependency(file interface{}, force interface{}) *MockPackageManager_ExtractDependency_Call { return &MockPackageManager_ExtractDependency_Call{Call: _e.mock.On("ExtractDependency", file, force)} } func (_c *MockPackageManager_ExtractDependency_Call) Run(run func(file string, force bool)) *MockPackageManager_ExtractDependency_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(bool)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 bool + if args[1] != nil { + arg1 = args[1].(bool) + } + run( + arg0, + arg1, + ) }) return _c } @@ -993,14 +1094,20 @@ type MockPackageManager_ExtractOciImageIndex_Call struct { } // ExtractOciImageIndex is a helper method to define mock.On call -// - imagefile +// - imagefile string func (_e *MockPackageManager_Expecter) ExtractOciImageIndex(imagefile interface{}) *MockPackageManager_ExtractOciImageIndex_Call { return &MockPackageManager_ExtractOciImageIndex_Call{Call: _e.mock.On("ExtractOciImageIndex", imagefile)} } func (_c *MockPackageManager_ExtractOciImageIndex_Call) Run(run func(imagefile string)) *MockPackageManager_ExtractOciImageIndex_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -1093,14 +1200,20 @@ type MockPackageManager_GetBaseimageName_Call struct { } // GetBaseimageName is a helper method to define mock.On call -// - baseimage +// - baseimage string func (_e *MockPackageManager_Expecter) GetBaseimageName(baseimage interface{}) *MockPackageManager_GetBaseimageName_Call { return &MockPackageManager_GetBaseimageName_Call{Call: _e.mock.On("GetBaseimageName", baseimage)} } func (_c *MockPackageManager_GetBaseimageName_Call) Run(run func(baseimage string)) *MockPackageManager_GetBaseimageName_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -1147,15 +1260,26 @@ type MockPackageManager_GetBaseimagePath_Call struct { } // GetBaseimagePath is a helper method to define mock.On call -// - baseimage -// - force +// - baseimage string +// - force bool func (_e *MockPackageManager_Expecter) GetBaseimagePath(baseimage interface{}, force interface{}) *MockPackageManager_GetBaseimagePath_Call { return &MockPackageManager_GetBaseimagePath_Call{Call: _e.mock.On("GetBaseimagePath", baseimage, force)} } func (_c *MockPackageManager_GetBaseimagePath_Call) Run(run func(baseimage string, force bool)) *MockPackageManager_GetBaseimagePath_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(bool)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 bool + if args[1] != nil { + arg1 = args[1].(bool) + } + run( + arg0, + arg1, + ) }) return _c } @@ -1246,14 +1370,20 @@ type MockPackageManager_GetDependencyPath_Call struct { } // GetDependencyPath is a helper method to define mock.On call -// - filename +// - filename string func (_e *MockPackageManager_Expecter) GetDependencyPath(filename interface{}) *MockPackageManager_GetDependencyPath_Call { return &MockPackageManager_GetDependencyPath_Call{Call: _e.mock.On("GetDependencyPath", filename)} } func (_c *MockPackageManager_GetDependencyPath_Call) Run(run func(filename string)) *MockPackageManager_GetDependencyPath_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } diff --git a/internal/portal/mocks.go b/internal/portal/mocks.go index 8337236..c162a3e 100644 --- a/internal/portal/mocks.go +++ b/internal/portal/mocks.go @@ -61,16 +61,32 @@ type MockHttp_Download_Call struct { } // Download is a helper method to define mock.On call -// - url -// - file -// - quiet +// - url string +// - file io.Writer +// - quiet bool func (_e *MockHttp_Expecter) Download(url interface{}, file interface{}, quiet interface{}) *MockHttp_Download_Call { return &MockHttp_Download_Call{Call: _e.mock.On("Download", url, file, quiet)} } func (_c *MockHttp_Download_Call) Run(run func(url string, file io.Writer, quiet bool)) *MockHttp_Download_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(io.Writer), args[2].(bool)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 io.Writer + if args[1] != nil { + arg1 = args[1].(io.Writer) + } + var arg2 bool + if args[2] != nil { + arg2 = args[2].(bool) + } + run( + arg0, + arg1, + arg2, + ) }) return _c } @@ -119,14 +135,20 @@ type MockHttp_Get_Call struct { } // Get is a helper method to define mock.On call -// - url +// - url string func (_e *MockHttp_Expecter) Get(url interface{}) *MockHttp_Get_Call { return &MockHttp_Get_Call{Call: _e.mock.On("Get", url)} } func (_c *MockHttp_Get_Call) Run(run func(url string)) *MockHttp_Get_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -175,16 +197,32 @@ type MockHttp_Request_Call struct { } // Request is a helper method to define mock.On call -// - url -// - method -// - body +// - url string +// - method string +// - body io.Reader func (_e *MockHttp_Expecter) Request(url interface{}, method interface{}, body interface{}) *MockHttp_Request_Call { return &MockHttp_Request_Call{Call: _e.mock.On("Request", url, method, body)} } func (_c *MockHttp_Request_Call) Run(run func(url string, method string, body io.Reader)) *MockHttp_Request_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(string), args[2].(io.Reader)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 io.Reader + if args[2] != nil { + arg2 = args[2].(io.Reader) + } + run( + arg0, + arg1, + arg2, + ) }) return _c } @@ -249,18 +287,44 @@ type MockPortal_DownloadBuildArtifact_Call struct { } // DownloadBuildArtifact is a helper method to define mock.On call -// - product -// - build -// - file -// - startByte -// - quiet +// - product Product +// - build Build +// - file io.Writer +// - startByte int +// - quiet bool func (_e *MockPortal_Expecter) DownloadBuildArtifact(product interface{}, build interface{}, file interface{}, startByte interface{}, quiet interface{}) *MockPortal_DownloadBuildArtifact_Call { return &MockPortal_DownloadBuildArtifact_Call{Call: _e.mock.On("DownloadBuildArtifact", product, build, file, startByte, quiet)} } func (_c *MockPortal_DownloadBuildArtifact_Call) Run(run func(product Product, build Build, file io.Writer, startByte int, quiet bool)) *MockPortal_DownloadBuildArtifact_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(Product), args[1].(Build), args[2].(io.Writer), args[3].(int), args[4].(bool)) + var arg0 Product + if args[0] != nil { + arg0 = args[0].(Product) + } + var arg1 Build + if args[1] != nil { + arg1 = args[1].(Build) + } + var arg2 io.Writer + if args[2] != nil { + arg2 = args[2].(io.Writer) + } + var arg3 int + if args[3] != nil { + arg3 = args[3].(int) + } + var arg4 bool + if args[4] != nil { + arg4 = args[4].(bool) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + ) }) return _c } @@ -307,14 +371,20 @@ type MockPortal_GetApiKeyId_Call struct { } // GetApiKeyId is a helper method to define mock.On call -// - oldKey +// - oldKey string func (_e *MockPortal_Expecter) GetApiKeyId(oldKey interface{}) *MockPortal_GetApiKeyId_Call { return &MockPortal_GetApiKeyId_Call{Call: _e.mock.On("GetApiKeyId", oldKey)} } func (_c *MockPortal_GetApiKeyId_Call) Run(run func(oldKey string)) *MockPortal_GetApiKeyId_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -361,16 +431,32 @@ type MockPortal_GetBuild_Call struct { } // GetBuild is a helper method to define mock.On call -// - product -// - version -// - hash +// - product Product +// - version string +// - hash string func (_e *MockPortal_Expecter) GetBuild(product interface{}, version interface{}, hash interface{}) *MockPortal_GetBuild_Call { return &MockPortal_GetBuild_Call{Call: _e.mock.On("GetBuild", product, version, hash)} } func (_c *MockPortal_GetBuild_Call) Run(run func(product Product, version string, hash string)) *MockPortal_GetBuild_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(Product), args[1].(string), args[2].(string)) + var arg0 Product + if args[0] != nil { + arg0 = args[0].(Product) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) }) return _c } @@ -472,14 +558,20 @@ type MockPortal_ListBuilds_Call struct { } // ListBuilds is a helper method to define mock.On call -// - product +// - product Product func (_e *MockPortal_Expecter) ListBuilds(product interface{}) *MockPortal_ListBuilds_Call { return &MockPortal_ListBuilds_Call{Call: _e.mock.On("ListBuilds", product)} } func (_c *MockPortal_ListBuilds_Call) Run(run func(product Product)) *MockPortal_ListBuilds_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(Product)) + var arg0 Product + if args[0] != nil { + arg0 = args[0].(Product) + } + run( + arg0, + ) }) return _c } @@ -528,17 +620,38 @@ type MockPortal_RegisterAPIKey_Call struct { } // RegisterAPIKey is a helper method to define mock.On call -// - owner -// - organization -// - role -// - expiresAt +// - owner string +// - organization string +// - role string +// - expiresAt time.Time func (_e *MockPortal_Expecter) RegisterAPIKey(owner interface{}, organization interface{}, role interface{}, expiresAt interface{}) *MockPortal_RegisterAPIKey_Call { return &MockPortal_RegisterAPIKey_Call{Call: _e.mock.On("RegisterAPIKey", owner, organization, role, expiresAt)} } func (_c *MockPortal_RegisterAPIKey_Call) Run(run func(owner string, organization string, role string, expiresAt time.Time)) *MockPortal_RegisterAPIKey_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(string), args[2].(string), args[3].(time.Time)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 time.Time + if args[3] != nil { + arg3 = args[3].(time.Time) + } + run( + arg0, + arg1, + arg2, + arg3, + ) }) return _c } @@ -576,14 +689,20 @@ type MockPortal_RevokeAPIKey_Call struct { } // RevokeAPIKey is a helper method to define mock.On call -// - key +// - key string func (_e *MockPortal_Expecter) RevokeAPIKey(key interface{}) *MockPortal_RevokeAPIKey_Call { return &MockPortal_RevokeAPIKey_Call{Call: _e.mock.On("RevokeAPIKey", key)} } func (_c *MockPortal_RevokeAPIKey_Call) Run(run func(key string)) *MockPortal_RevokeAPIKey_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -621,15 +740,26 @@ type MockPortal_UpdateAPIKey_Call struct { } // UpdateAPIKey is a helper method to define mock.On call -// - key -// - expiresAt +// - key string +// - expiresAt time.Time func (_e *MockPortal_Expecter) UpdateAPIKey(key interface{}, expiresAt interface{}) *MockPortal_UpdateAPIKey_Call { return &MockPortal_UpdateAPIKey_Call{Call: _e.mock.On("UpdateAPIKey", key, expiresAt)} } func (_c *MockPortal_UpdateAPIKey_Call) Run(run func(key string, expiresAt time.Time)) *MockPortal_UpdateAPIKey_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(time.Time)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 time.Time + if args[1] != nil { + arg1 = args[1].(time.Time) + } + run( + arg0, + arg1, + ) }) return _c } @@ -667,15 +797,26 @@ type MockPortal_VerifyBuildArtifactDownload_Call struct { } // VerifyBuildArtifactDownload is a helper method to define mock.On call -// - file -// - download +// - file io.Reader +// - download Build func (_e *MockPortal_Expecter) VerifyBuildArtifactDownload(file interface{}, download interface{}) *MockPortal_VerifyBuildArtifactDownload_Call { return &MockPortal_VerifyBuildArtifactDownload_Call{Call: _e.mock.On("VerifyBuildArtifactDownload", file, download)} } func (_c *MockPortal_VerifyBuildArtifactDownload_Call) Run(run func(file io.Reader, download Build)) *MockPortal_VerifyBuildArtifactDownload_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(io.Reader), args[1].(Build)) + var arg0 io.Reader + if args[0] != nil { + arg0 = args[0].(io.Reader) + } + var arg1 Build + if args[1] != nil { + arg1 = args[1].(Build) + } + run( + arg0, + arg1, + ) }) return _c } @@ -751,14 +892,20 @@ type MockHttpClient_Do_Call struct { } // Do is a helper method to define mock.On call -// - request +// - request *http.Request func (_e *MockHttpClient_Expecter) Do(request interface{}) *MockHttpClient_Do_Call { return &MockHttpClient_Do_Call{Call: _e.mock.On("Do", request)} } func (_c *MockHttpClient_Do_Call) Run(run func(request *http.Request)) *MockHttpClient_Do_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(*http.Request)) + var arg0 *http.Request + if args[0] != nil { + arg0 = args[0].(*http.Request) + } + run( + arg0, + ) }) return _c } diff --git a/internal/system/mocks.go b/internal/system/mocks.go index cf8e929..ee80bc6 100644 --- a/internal/system/mocks.go +++ b/internal/system/mocks.go @@ -58,16 +58,32 @@ type MockImageManager_BuildImage_Call struct { } // BuildImage is a helper method to define mock.On call -// - dockerfile -// - tag -// - buildContext +// - dockerfile string +// - tag string +// - buildContext string func (_e *MockImageManager_Expecter) BuildImage(dockerfile interface{}, tag interface{}, buildContext interface{}) *MockImageManager_BuildImage_Call { return &MockImageManager_BuildImage_Call{Call: _e.mock.On("BuildImage", dockerfile, tag, buildContext)} } func (_c *MockImageManager_BuildImage_Call) Run(run func(dockerfile string, tag string, buildContext string)) *MockImageManager_BuildImage_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(string), args[2].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) }) return _c } @@ -105,14 +121,20 @@ type MockImageManager_LoadImage_Call struct { } // LoadImage is a helper method to define mock.On call -// - imageTarPath +// - imageTarPath string func (_e *MockImageManager_Expecter) LoadImage(imageTarPath interface{}) *MockImageManager_LoadImage_Call { return &MockImageManager_LoadImage_Call{Call: _e.mock.On("LoadImage", imageTarPath)} } func (_c *MockImageManager_LoadImage_Call) Run(run func(imageTarPath string)) *MockImageManager_LoadImage_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -150,14 +172,20 @@ type MockImageManager_PushImage_Call struct { } // PushImage is a helper method to define mock.On call -// - tag +// - tag string func (_e *MockImageManager_Expecter) PushImage(tag interface{}) *MockImageManager_PushImage_Call { return &MockImageManager_PushImage_Call{Call: _e.mock.On("PushImage", tag)} } func (_c *MockImageManager_PushImage_Call) Run(run func(tag string)) *MockImageManager_PushImage_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } diff --git a/internal/util/mocks.go b/internal/util/mocks.go index 394f6b8..abfdd4f 100644 --- a/internal/util/mocks.go +++ b/internal/util/mocks.go @@ -70,15 +70,26 @@ type MockDockerfileManager_UpdateFromStatement_Call struct { } // UpdateFromStatement is a helper method to define mock.On call -// - dockerfile -// - baseImage +// - dockerfile io.Reader +// - baseImage string func (_e *MockDockerfileManager_Expecter) UpdateFromStatement(dockerfile interface{}, baseImage interface{}) *MockDockerfileManager_UpdateFromStatement_Call { return &MockDockerfileManager_UpdateFromStatement_Call{Call: _e.mock.On("UpdateFromStatement", dockerfile, baseImage)} } func (_c *MockDockerfileManager_UpdateFromStatement_Call) Run(run func(dockerfile io.Reader, baseImage string)) *MockDockerfileManager_UpdateFromStatement_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(io.Reader), args[1].(string)) + var arg0 io.Reader + if args[0] != nil { + arg0 = args[0].(io.Reader) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) }) return _c } @@ -154,14 +165,20 @@ type MockFileIO_Create_Call struct { } // Create is a helper method to define mock.On call -// - filename +// - filename string func (_e *MockFileIO_Expecter) Create(filename interface{}) *MockFileIO_Create_Call { return &MockFileIO_Create_Call{Call: _e.mock.On("Create", filename)} } func (_c *MockFileIO_Create_Call) Run(run func(filename string)) *MockFileIO_Create_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -199,16 +216,32 @@ type MockFileIO_CreateAndWrite_Call struct { } // CreateAndWrite is a helper method to define mock.On call -// - filePath -// - data -// - fileType +// - filePath string +// - data []byte +// - fileType string func (_e *MockFileIO_Expecter) CreateAndWrite(filePath interface{}, data interface{}, fileType interface{}) *MockFileIO_CreateAndWrite_Call { return &MockFileIO_CreateAndWrite_Call{Call: _e.mock.On("CreateAndWrite", filePath, data, fileType)} } func (_c *MockFileIO_CreateAndWrite_Call) Run(run func(filePath string, data []byte, fileType string)) *MockFileIO_CreateAndWrite_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].([]byte), args[2].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 []byte + if args[1] != nil { + arg1 = args[1].([]byte) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) }) return _c } @@ -246,14 +279,20 @@ type MockFileIO_Exists_Call struct { } // Exists is a helper method to define mock.On call -// - filename +// - filename string func (_e *MockFileIO_Expecter) Exists(filename interface{}) *MockFileIO_Exists_Call { return &MockFileIO_Exists_Call{Call: _e.mock.On("Exists", filename)} } func (_c *MockFileIO_Exists_Call) Run(run func(filename string)) *MockFileIO_Exists_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -300,14 +339,20 @@ type MockFileIO_IsDirectory_Call struct { } // IsDirectory is a helper method to define mock.On call -// - filename +// - filename string func (_e *MockFileIO_Expecter) IsDirectory(filename interface{}) *MockFileIO_IsDirectory_Call { return &MockFileIO_IsDirectory_Call{Call: _e.mock.On("IsDirectory", filename)} } func (_c *MockFileIO_IsDirectory_Call) Run(run func(filename string)) *MockFileIO_IsDirectory_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -345,15 +390,26 @@ type MockFileIO_MkdirAll_Call struct { } // MkdirAll is a helper method to define mock.On call -// - path -// - perm +// - path string +// - perm os.FileMode func (_e *MockFileIO_Expecter) MkdirAll(path interface{}, perm interface{}) *MockFileIO_MkdirAll_Call { return &MockFileIO_MkdirAll_Call{Call: _e.mock.On("MkdirAll", path, perm)} } func (_c *MockFileIO_MkdirAll_Call) Run(run func(path string, perm os.FileMode)) *MockFileIO_MkdirAll_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(os.FileMode)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 os.FileMode + if args[1] != nil { + arg1 = args[1].(os.FileMode) + } + run( + arg0, + arg1, + ) }) return _c } @@ -402,14 +458,20 @@ type MockFileIO_Open_Call struct { } // Open is a helper method to define mock.On call -// - filename +// - filename string func (_e *MockFileIO_Expecter) Open(filename interface{}) *MockFileIO_Open_Call { return &MockFileIO_Open_Call{Call: _e.mock.On("Open", filename)} } func (_c *MockFileIO_Open_Call) Run(run func(filename string)) *MockFileIO_Open_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -458,14 +520,20 @@ type MockFileIO_OpenAppend_Call struct { } // OpenAppend is a helper method to define mock.On call -// - filename +// - filename string func (_e *MockFileIO_Expecter) OpenAppend(filename interface{}) *MockFileIO_OpenAppend_Call { return &MockFileIO_OpenAppend_Call{Call: _e.mock.On("OpenAppend", filename)} } func (_c *MockFileIO_OpenAppend_Call) Run(run func(filename string)) *MockFileIO_OpenAppend_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -514,16 +582,32 @@ type MockFileIO_OpenFile_Call struct { } // OpenFile is a helper method to define mock.On call -// - name -// - flag -// - perm +// - name string +// - flag int +// - perm os.FileMode func (_e *MockFileIO_Expecter) OpenFile(name interface{}, flag interface{}, perm interface{}) *MockFileIO_OpenFile_Call { return &MockFileIO_OpenFile_Call{Call: _e.mock.On("OpenFile", name, flag, perm)} } func (_c *MockFileIO_OpenFile_Call) Run(run func(name string, flag int, perm os.FileMode)) *MockFileIO_OpenFile_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(int), args[2].(os.FileMode)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 int + if args[1] != nil { + arg1 = args[1].(int) + } + var arg2 os.FileMode + if args[2] != nil { + arg2 = args[2].(os.FileMode) + } + run( + arg0, + arg1, + arg2, + ) }) return _c } @@ -572,14 +656,20 @@ type MockFileIO_ReadDir_Call struct { } // ReadDir is a helper method to define mock.On call -// - dirname +// - dirname string func (_e *MockFileIO_Expecter) ReadDir(dirname interface{}) *MockFileIO_ReadDir_Call { return &MockFileIO_ReadDir_Call{Call: _e.mock.On("ReadDir", dirname)} } func (_c *MockFileIO_ReadDir_Call) Run(run func(dirname string)) *MockFileIO_ReadDir_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -628,14 +718,20 @@ type MockFileIO_ReadFile_Call struct { } // ReadFile is a helper method to define mock.On call -// - filename +// - filename string func (_e *MockFileIO_Expecter) ReadFile(filename interface{}) *MockFileIO_ReadFile_Call { return &MockFileIO_ReadFile_Call{Call: _e.mock.On("ReadFile", filename)} } func (_c *MockFileIO_ReadFile_Call) Run(run func(filename string)) *MockFileIO_ReadFile_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) }) return _c } @@ -673,16 +769,32 @@ type MockFileIO_WriteFile_Call struct { } // WriteFile is a helper method to define mock.On call -// - filename -// - data -// - perm +// - filename string +// - data []byte +// - perm os.FileMode func (_e *MockFileIO_Expecter) WriteFile(filename interface{}, data interface{}, perm interface{}) *MockFileIO_WriteFile_Call { return &MockFileIO_WriteFile_Call{Call: _e.mock.On("WriteFile", filename, data, perm)} } func (_c *MockFileIO_WriteFile_Call) Run(run func(filename string, data []byte, perm os.FileMode)) *MockFileIO_WriteFile_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].([]byte), args[2].(os.FileMode)) + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 []byte + if args[1] != nil { + arg1 = args[1].([]byte) + } + var arg2 os.FileMode + if args[2] != nil { + arg2 = args[2].(os.FileMode) + } + run( + arg0, + arg1, + arg2, + ) }) return _c } @@ -812,8 +924,8 @@ type MockTableWriter_AppendHeader_Call struct { } // AppendHeader is a helper method to define mock.On call -// - row -// - configs +// - row table.Row +// - configs ...table.RowConfig func (_e *MockTableWriter_Expecter) AppendHeader(row interface{}, configs ...interface{}) *MockTableWriter_AppendHeader_Call { return &MockTableWriter_AppendHeader_Call{Call: _e.mock.On("AppendHeader", append([]interface{}{row}, configs...)...)} @@ -821,13 +933,20 @@ func (_e *MockTableWriter_Expecter) AppendHeader(row interface{}, configs ...int func (_c *MockTableWriter_AppendHeader_Call) Run(run func(row table.Row, configs ...table.RowConfig)) *MockTableWriter_AppendHeader_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]table.RowConfig, len(args)-1) - for i, a := range args[1:] { - if a != nil { - variadicArgs[i] = a.(table.RowConfig) - } + var arg0 table.Row + if args[0] != nil { + arg0 = args[0].(table.Row) } - run(args[0].(table.Row), variadicArgs...) + var arg1 []table.RowConfig + var variadicArgs []table.RowConfig + if len(args) > 1 { + variadicArgs = args[1].([]table.RowConfig) + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) }) return _c } @@ -859,8 +978,8 @@ type MockTableWriter_AppendRow_Call struct { } // AppendRow is a helper method to define mock.On call -// - row -// - configs +// - row table.Row +// - configs ...table.RowConfig func (_e *MockTableWriter_Expecter) AppendRow(row interface{}, configs ...interface{}) *MockTableWriter_AppendRow_Call { return &MockTableWriter_AppendRow_Call{Call: _e.mock.On("AppendRow", append([]interface{}{row}, configs...)...)} @@ -868,13 +987,20 @@ func (_e *MockTableWriter_Expecter) AppendRow(row interface{}, configs ...interf func (_c *MockTableWriter_AppendRow_Call) Run(run func(row table.Row, configs ...table.RowConfig)) *MockTableWriter_AppendRow_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]table.RowConfig, len(args)-1) - for i, a := range args[1:] { - if a != nil { - variadicArgs[i] = a.(table.RowConfig) - } + var arg0 table.Row + if args[0] != nil { + arg0 = args[0].(table.Row) + } + var arg1 []table.RowConfig + var variadicArgs []table.RowConfig + if len(args) > 1 { + variadicArgs = args[1].([]table.RowConfig) } - run(args[0].(table.Row), variadicArgs...) + arg1 = variadicArgs + run( + arg0, + arg1..., + ) }) return _c } From 3035990d6a48bb052f12d45ec3f156c3fb2ce50a Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:59:37 +0100 Subject: [PATCH 03/32] ref: use deferred functions for cleanup in k0s and node installers --- cli/cmd/install_k0s.go | 2 +- internal/installer/k0s.go | 4 ++-- internal/installer/node/node.go | 30 +++++++++--------------------- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index aed5d79..220654e 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -150,7 +150,7 @@ func (c *InstallK0sCmd) InstallK0sFromInstallConfig(pm installer.PackageManager, if err := os.WriteFile(tmpK0sConfigPath, k0sConfigData, 0644); err != nil { return fmt.Errorf("failed to write k0s config: %w", err) } - defer os.Remove(tmpK0sConfigPath) + defer func() { _ = os.Remove(tmpK0sConfigPath) }() log.Printf("Generated k0s configuration at %s", tmpK0sConfigPath) diff --git a/internal/installer/k0s.go b/internal/installer/k0s.go index f497c1c..422268e 100644 --- a/internal/installer/k0s.go +++ b/internal/installer/k0s.go @@ -125,7 +125,7 @@ func (k *K0s) Install(configPath string, k0sPath string, force bool) error { log.Printf("Warning: failed to filter config, using original: %v", err) } else { configPath = filteredConfigPath - defer os.Remove(filteredConfigPath) // Clean up temp file after use + defer func() { _ = os.Remove(filteredConfigPath) }() // Clean up temp file after use } args = append(args, "--config", configPath) } else { @@ -206,7 +206,7 @@ func (k *K0s) filterConfigForK0s(configPath string) (string, error) { if err != nil { return "", fmt.Errorf("failed to create temp config: %w", err) } - defer tmpFile.Close() + defer func() { _ = tmpFile.Close() }() if _, err := tmpFile.Write(filteredData); err != nil { return "", fmt.Errorf("failed to write temp config: %w", err) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index 2012360..438d80b 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -172,12 +172,12 @@ func (n *NodeManager) RunSSHCommand(jumpboxIp string, ip string, username string if err != nil { return fmt.Errorf("failed to get client: %w", err) } - defer client.Close() + defer func() { _ = client.Close() }() session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session on jumpbox: %v", err) } - defer session.Close() + defer func() { _ = session.Close() }() if err := n.forwardAgent(client, session); err != nil { fmt.Printf(" Warning: Agent forwarding setup failed on session: %v\n", err) @@ -271,19 +271,19 @@ func (n *NodeManager) CopyFile(jumpboxIp string, ip string, username string, src if err != nil { return fmt.Errorf("failed to get SSH client: %v", err) } - defer client.Close() + defer func() { _ = client.Close() }() srcFile, err := n.FileIO.Open(src) if err != nil { return fmt.Errorf("failed to open source file %s: %v", src, err) } - defer srcFile.Close() + defer func() { _ = srcFile.Close() }() dstFile, err := client.Create(dst) if err != nil { return fmt.Errorf("failed to create destination file %s: %v", dst, err) } - defer dstFile.Close() + defer func() { _ = dstFile.Close() }() _, err = dstFile.ReadFrom(srcFile) if err != nil { @@ -296,10 +296,7 @@ func (n *NodeManager) CopyFile(jumpboxIp string, ip string, username string, src func (n *Node) HasCommand(nm *NodeManager, command string) bool { checkCommand := fmt.Sprintf("command -v %s >/dev/null 2>&1", command) err := nm.RunSSHCommand("", n.ExternalIP, "root", checkCommand) - if err != nil { - return false - } - return true + return err == nil } func (n *Node) InstallOms(nm *NodeManager) error { @@ -329,10 +326,7 @@ func (n *Node) CopyFile(nm *NodeManager, src string, dst string) error { func (n *Node) HasAcceptEnvConfigured(jumpbox *Node, nm *NodeManager) bool { checkCommand := "sudo grep -E '^AcceptEnv OMS_PORTAL_API_KEY' /etc/ssh/sshd_config >/dev/null 2>&1" err := n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommand) - if err != nil { - return false - } - return true + return err == nil } func (n *Node) ConfigureAcceptEnv(jumpbox *Node, nm *NodeManager) error { @@ -357,19 +351,13 @@ func (n *Node) HasRootLoginEnabled(jumpbox *Node, nm *NodeManager) bool { } checkCommandAuthorizedKeys := "sudo grep -E '^no-port-forwarding' /root/.ssh/authorized_keys >/dev/null 2>&1" err = n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommandAuthorizedKeys) - if err == nil { - return false - } - return true + return err != nil } func (n *Node) HasFile(jumpbox *Node, nm *NodeManager, filePath string) bool { checkCommand := fmt.Sprintf("test -f '%s'", filePath) err := n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommand) - if err != nil { - return false - } - return true + return err == nil } func (n *Node) RunSSHCommand(jumpbox *Node, nm *NodeManager, username string, command string) error { From ad86e8e0fdf8e1602849c0d1d67d905bbf1fa9b6 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:00:55 +0000 Subject: [PATCH 04/32] chore(docs): Auto-update docs and licenses Signed-off-by: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> --- internal/tmpl/NOTICE | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/internal/tmpl/NOTICE b/internal/tmpl/NOTICE index d7e513d..ed0df75 100644 --- a/internal/tmpl/NOTICE +++ b/internal/tmpl/NOTICE @@ -23,9 +23,9 @@ License URL: https://github.com/clipperhouse/uax29/blob/v2.3.0/LICENSE ---------- Module: github.com/codesphere-cloud/cs-go/pkg/io -Version: v0.14.1 +Version: v0.15.0 License: Apache-2.0 -License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.14.1/LICENSE +License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.15.0/LICENSE ---------- Module: github.com/codesphere-cloud/oms/internal/tmpl @@ -77,9 +77,15 @@ License URL: https://github.com/inconshreveable/go-update/blob/8152e7eb6ccf/inte ---------- Module: github.com/jedib0t/go-pretty/v6 -Version: v6.7.5 +Version: v6.7.7 License: MIT -License URL: https://github.com/jedib0t/go-pretty/blob/v6.7.5/LICENSE +License URL: https://github.com/jedib0t/go-pretty/blob/v6.7.7/LICENSE + +---------- +Module: github.com/kr/fs +Version: v0.1.0 +License: BSD-3-Clause +License URL: https://github.com/kr/fs/blob/v0.1.0/LICENSE ---------- Module: github.com/mattn/go-runewidth @@ -87,6 +93,12 @@ Version: v0.0.19 License: MIT License URL: https://github.com/mattn/go-runewidth/blob/v0.0.19/LICENSE +---------- +Module: github.com/pkg/sftp +Version: v1.13.10 +License: BSD-2-Clause +License URL: https://github.com/pkg/sftp/blob/v1.13.10/LICENSE + ---------- Module: github.com/pmezard/go-difflib/difflib Version: v1.0.1-0.20181226105442-5d4384ee4fb2 @@ -155,9 +167,9 @@ License URL: https://github.com/yaml/go-yaml/blob/v3.0.4/LICENSE ---------- Module: golang.org/x/crypto -Version: v0.45.0 +Version: v0.46.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/crypto/+/v0.45.0:LICENSE +License URL: https://cs.opensource.google/go/x/crypto/+/v0.46.0:LICENSE ---------- Module: golang.org/x/oauth2 @@ -165,11 +177,23 @@ Version: v0.33.0 License: BSD-3-Clause License URL: https://cs.opensource.google/go/x/oauth2/+/v0.33.0:LICENSE +---------- +Module: golang.org/x/sys/unix +Version: v0.39.0 +License: BSD-3-Clause +License URL: https://cs.opensource.google/go/x/sys/+/v0.39.0:LICENSE + +---------- +Module: golang.org/x/term +Version: v0.38.0 +License: BSD-3-Clause +License URL: https://cs.opensource.google/go/x/term/+/v0.38.0:LICENSE + ---------- Module: golang.org/x/text -Version: v0.31.0 +Version: v0.32.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/text/+/v0.31.0:LICENSE +License URL: https://cs.opensource.google/go/x/text/+/v0.32.0:LICENSE ---------- Module: gopkg.in/yaml.v3 From c79fc3dd57207430c45737994ec776072d3fbd76 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:09:09 +0100 Subject: [PATCH 05/32] ref: refactor install-config integration tests for improved clarity and structure --- cli/cmd/install_k0s_integration_test.go | 473 +++++++++++------------- 1 file changed, 215 insertions(+), 258 deletions(-) diff --git a/cli/cmd/install_k0s_integration_test.go b/cli/cmd/install_k0s_integration_test.go index 8823aa4..28a5523 100644 --- a/cli/cmd/install_k0s_integration_test.go +++ b/cli/cmd/install_k0s_integration_test.go @@ -7,6 +7,7 @@ package cmd_test import ( + "fmt" "os" "path/filepath" @@ -40,45 +41,45 @@ var _ = Describe("K0s Install-Config Integration", func() { } }) - Describe("Config Generation Workflow", func() { - It("should generate valid k0s config from install-config", func() { - // Create a minimal install-config using RootConfig - installConfig := &files.RootConfig{ - Datacenter: files.DatacenterConfig{ - ID: 1, - Name: "test-dc", - City: "Test City", - CountryCode: "US", + createBaseConfig := func(name string, ip string) *files.RootConfig { + return &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: name, + City: "Test City", + CountryCode: "US", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + ControlPlanes: []files.K8sNode{ + {IPAddress: ip}, }, - Kubernetes: files.KubernetesConfig{ - ManagedByCodesphere: true, - ControlPlanes: []files.K8sNode{ - { - IPAddress: "192.168.1.100", - }, - }, - APIServerHost: "api.test.example.com", + APIServerHost: "api.test.example.com", + }, + Codesphere: files.CodesphereConfig{ + Domain: "test.example.com", + PublicIP: ip, + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{}, }, - Codesphere: files.CodesphereConfig{ - Domain: "test.example.com", - PublicIP: "192.168.1.100", - DeployConfig: files.DeployConfig{ - Images: map[string]files.ImageConfig{}, - }, - Plans: files.PlansConfig{ - HostingPlans: map[int]files.HostingPlan{}, - WorkspacePlans: map[int]files.WorkspacePlan{}, - }, + Plans: files.PlansConfig{ + HostingPlans: map[int]files.HostingPlan{}, + WorkspacePlans: map[int]files.WorkspacePlan{}, }, - } + }, + } + } - // Write install-config to file + Describe("Complete Workflow", func() { + It("should generate valid k0s config from install-config file", func() { + installConfig := createBaseConfig("test-dc", "192.168.1.100") + + // Write and load install-config configData, err := yaml.Marshal(installConfig) Expect(err).NotTo(HaveOccurred()) err = os.WriteFile(configPath, configData, 0644) Expect(err).NotTo(HaveOccurred()) - // Load the config back using InstallConfigManager icg := installer.NewInstallConfigManager() err = icg.LoadInstallConfigFromFile(configPath) Expect(err).NotTo(HaveOccurred()) @@ -96,56 +97,36 @@ var _ = Describe("K0s Install-Config Integration", func() { Expect(k0sConfig.APIVersion).To(Equal("k0s.k0sproject.io/v1beta1")) Expect(k0sConfig.Kind).To(Equal("ClusterConfig")) Expect(k0sConfig.Metadata.Name).To(Equal("codesphere-test-dc")) - Expect(k0sConfig.Spec.API).NotTo(BeNil()) Expect(k0sConfig.Spec.API.Address).To(Equal("192.168.1.100")) Expect(k0sConfig.Spec.API.ExternalAddress).To(Equal("api.test.example.com")) - // Write k0s config to file + // Write k0s config to file and verify k0sData, err := k0sConfig.Marshal() Expect(err).NotTo(HaveOccurred()) err = os.WriteFile(k0sConfigOut, k0sData, 0644) Expect(err).NotTo(HaveOccurred()) - // Verify file was created and is valid YAML Expect(k0sConfigOut).To(BeAnExistingFile()) data, err := os.ReadFile(k0sConfigOut) Expect(err).NotTo(HaveOccurred()) - Expect(len(data)).To(BeNumerically(">", 0)) - // Verify we can unmarshal it back var verifyConfig installer.K0sConfig err = yaml.Unmarshal(data, &verifyConfig) Expect(err).NotTo(HaveOccurred()) Expect(verifyConfig.APIVersion).To(Equal("k0s.k0sproject.io/v1beta1")) + Expect(verifyConfig.Metadata.Name).To(Equal("codesphere-test-dc")) }) + }) + Describe("Configuration Features", func() { It("should handle multi-control-plane configuration", func() { - installConfig := &files.RootConfig{ - Datacenter: files.DatacenterConfig{ - ID: 1, - Name: "multi-dc", - }, - Kubernetes: files.KubernetesConfig{ - ManagedByCodesphere: true, - ControlPlanes: []files.K8sNode{ - {IPAddress: "10.0.0.10"}, - {IPAddress: "10.0.0.11"}, - {IPAddress: "10.0.0.12"}, - }, - APIServerHost: "api.cluster.test", - }, - Codesphere: files.CodesphereConfig{ - Domain: "cluster.test", - PublicIP: "10.0.0.10", - DeployConfig: files.DeployConfig{ - Images: map[string]files.ImageConfig{}, - }, - Plans: files.PlansConfig{ - HostingPlans: map[int]files.HostingPlan{}, - WorkspacePlans: map[int]files.WorkspacePlan{}, - }, - }, + installConfig := createBaseConfig("multi-dc", "10.0.0.10") + installConfig.Kubernetes.ControlPlanes = []files.K8sNode{ + {IPAddress: "10.0.0.10"}, + {IPAddress: "10.0.0.11"}, + {IPAddress: "10.0.0.12"}, } + installConfig.Kubernetes.APIServerHost = "api.cluster.test" k0sConfig, err := installer.GenerateK0sConfig(installConfig) Expect(err).NotTo(HaveOccurred()) @@ -159,93 +140,132 @@ var _ = Describe("K0s Install-Config Integration", func() { Expect(k0sConfig.Spec.API.SANs).To(ContainElement("api.cluster.test")) }) - It("should preserve network configuration", func() { - installConfig := &files.RootConfig{ - Datacenter: files.DatacenterConfig{ - ID: 1, - Name: "network-test", - }, - Kubernetes: files.KubernetesConfig{ - ManagedByCodesphere: true, - ControlPlanes: []files.K8sNode{ - {IPAddress: "192.168.1.100"}, - }, - PodCIDR: "10.244.0.0/16", - ServiceCIDR: "10.96.0.0/12", - }, - Codesphere: files.CodesphereConfig{ - Domain: "network.test", - PublicIP: "192.168.1.100", - DeployConfig: files.DeployConfig{ - Images: map[string]files.ImageConfig{}, - }, - Plans: files.PlansConfig{ - HostingPlans: map[int]files.HostingPlan{}, - WorkspacePlans: map[int]files.WorkspacePlan{}, - }, - }, - } + It("should preserve custom network configuration", func() { + installConfig := createBaseConfig("network-test", "192.168.1.100") + installConfig.Kubernetes.PodCIDR = "10.244.0.0/16" + installConfig.Kubernetes.ServiceCIDR = "10.96.0.0/12" k0sConfig, err := installer.GenerateK0sConfig(installConfig) Expect(err).NotTo(HaveOccurred()) - // Verify network settings Expect(k0sConfig.Spec.Network).NotTo(BeNil()) Expect(k0sConfig.Spec.Network.PodCIDR).To(Equal("10.244.0.0/16")) Expect(k0sConfig.Spec.Network.ServiceCIDR).To(Equal("10.96.0.0/12")) }) - It("should handle storage configuration", func() { - installConfig := &files.RootConfig{ - Datacenter: files.DatacenterConfig{ - ID: 1, - Name: "storage-test", - }, - Kubernetes: files.KubernetesConfig{ - ManagedByCodesphere: true, - ControlPlanes: []files.K8sNode{ - {IPAddress: "192.168.1.100"}, - }, - }, - Codesphere: files.CodesphereConfig{ - Domain: "storage.test", - PublicIP: "192.168.1.100", - DeployConfig: files.DeployConfig{ - Images: map[string]files.ImageConfig{}, - }, - Plans: files.PlansConfig{ - HostingPlans: map[int]files.HostingPlan{}, - WorkspacePlans: map[int]files.WorkspacePlan{}, - }, - }, - } + It("should configure etcd storage correctly", func() { + installConfig := createBaseConfig("storage-test", "192.168.1.100") k0sConfig, err := installer.GenerateK0sConfig(installConfig) Expect(err).NotTo(HaveOccurred()) - // Verify storage/etcd settings Expect(k0sConfig.Spec.Storage).NotTo(BeNil()) Expect(k0sConfig.Spec.Storage.Type).To(Equal("etcd")) Expect(k0sConfig.Spec.Storage.Etcd).NotTo(BeNil()) Expect(k0sConfig.Spec.Storage.Etcd.PeerAddress).To(Equal("192.168.1.100")) }) + + It("should generate correct cluster name from datacenter", func() { + installConfig := createBaseConfig("prod-us-east", "10.1.2.3") + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).NotTo(HaveOccurred()) + + Expect(k0sConfig.Metadata.Name).To(Equal("codesphere-prod-us-east")) + }) + + It("should handle empty control plane list", func() { + installConfig := createBaseConfig("empty-cp", "10.0.0.1") + installConfig.Kubernetes.ControlPlanes = []files.K8sNode{} + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + // Should either handle gracefully or error + if err == nil { + Expect(k0sConfig).NotTo(BeNil()) + } else { + Expect(err).To(HaveOccurred()) + } + }) + + It("should use default network values when not specified", func() { + installConfig := createBaseConfig("defaults-test", "10.0.0.1") + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).NotTo(HaveOccurred()) + + // Verify defaults are applied or fields are present + Expect(k0sConfig.Spec.Network).NotTo(BeNil()) + if k0sConfig.Spec.Network.PodCIDR != "" { + Expect(k0sConfig.Spec.Network.PodCIDR).To(MatchRegexp(`^\d+\.\d+\.\d+\.\d+/\d+$`)) + } + if k0sConfig.Spec.Network.ServiceCIDR != "" { + Expect(k0sConfig.Spec.Network.ServiceCIDR).To(MatchRegexp(`^\d+\.\d+\.\d+\.\d+/\d+$`)) + } + }) + + It("should handle special characters in datacenter names", func() { + testCases := []struct { + name string + expected string + }{ + {"test-dc-01", "codesphere-test-dc-01"}, + {"test_dc_02", "codesphere-test_dc_02"}, + {"TestDC03", "codesphere-TestDC03"}, + } + + for _, tc := range testCases { + installConfig := createBaseConfig(tc.name, "10.0.0.1") + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(k0sConfig.Metadata.Name).To(Equal(tc.expected)) + } + }) + + It("should handle large multi-control-plane setup", func() { + installConfig := createBaseConfig("large-cluster", "10.0.1.1") + controlPlanes := make([]files.K8sNode, 7) + for i := 0; i < 7; i++ { + controlPlanes[i] = files.K8sNode{ + IPAddress: fmt.Sprintf("10.0.1.%d", i+1), + } + } + installConfig.Kubernetes.ControlPlanes = controlPlanes + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(k0sConfig.Spec.API.Address).To(Equal("10.0.1.1")) + Expect(len(k0sConfig.Spec.API.SANs)).To(BeNumerically(">=", 7)) + }) + + It("should properly configure certificate SANs", func() { + installConfig := createBaseConfig("san-test", "192.168.100.50") + installConfig.Kubernetes.APIServerHost = "k8s.example.com" + installConfig.Codesphere.Domain = "app.example.com" + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + Expect(err).NotTo(HaveOccurred()) + + Expect(k0sConfig.Spec.API.SANs).To(ContainElement("192.168.100.50")) + Expect(k0sConfig.Spec.API.SANs).To(ContainElement("k8s.example.com")) + Expect(len(k0sConfig.Spec.API.SANs)).To(BeNumerically(">=", 2)) + }) }) Describe("Error Handling", func() { - It("should fail gracefully on missing install-config file", func() { + It("should fail when loading non-existent file", func() { nonExistentPath := filepath.Join(tempDir, "does-not-exist.yaml") icg := installer.NewInstallConfigManager() err := icg.LoadInstallConfigFromFile(nonExistentPath) Expect(err).To(HaveOccurred()) }) - It("should handle nil config gracefully", func() { + It("should fail when generating config from nil", func() { _, err := installer.GenerateK0sConfig(nil) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("cannot be nil")) }) - It("should handle invalid YAML gracefully", func() { + It("should fail when loading invalid YAML", func() { invalidYAML := []byte("invalid: [unclosed bracket") err := os.WriteFile(configPath, invalidYAML, 0644) Expect(err).NotTo(HaveOccurred()) @@ -255,112 +275,87 @@ var _ = Describe("K0s Install-Config Integration", func() { Expect(err).To(HaveOccurred()) }) - It("should fail for external Kubernetes", func() { - installConfig := &files.RootConfig{ - Datacenter: files.DatacenterConfig{ - ID: 1, - Name: "external-k8s", - }, - Kubernetes: files.KubernetesConfig{ - ManagedByCodesphere: false, // External K8s - }, - Codesphere: files.CodesphereConfig{ - Domain: "external.test", - PublicIP: "10.0.0.1", - DeployConfig: files.DeployConfig{ - Images: map[string]files.ImageConfig{}, - }, - Plans: files.PlansConfig{ - HostingPlans: map[int]files.HostingPlan{}, - WorkspacePlans: map[int]files.WorkspacePlan{}, - }, - }, - } + It("should handle empty file gracefully", func() { + err := os.WriteFile(configPath, []byte{}, 0644) + Expect(err).NotTo(HaveOccurred()) + + icg := installer.NewInstallConfigManager() + err = icg.LoadInstallConfigFromFile(configPath) + // Empty file loads successfully but returns empty config + Expect(err).NotTo(HaveOccurred()) + config := icg.GetInstallConfig() + Expect(config).NotTo(BeNil()) + }) + + It("should handle external Kubernetes cluster config", func() { + installConfig := createBaseConfig("external-k8s", "10.0.0.1") + installConfig.Kubernetes.ManagedByCodesphere = false - // GenerateK0sConfig should still work (doesn't validate ManagedByCodesphere) - // The validation happens in the CLI command k0sConfig, err := installer.GenerateK0sConfig(installConfig) Expect(err).NotTo(HaveOccurred()) Expect(k0sConfig).NotTo(BeNil()) }) - }) - Describe("YAML Marshalling", func() { - It("should produce valid k0s YAML output", func() { - installConfig := &files.RootConfig{ - Datacenter: files.DatacenterConfig{ - ID: 1, - Name: "yaml-test", - }, - Kubernetes: files.KubernetesConfig{ - ManagedByCodesphere: true, - ControlPlanes: []files.K8sNode{ - {IPAddress: "10.20.30.40"}, - }, - }, - Codesphere: files.CodesphereConfig{ - Domain: "yaml.test", - PublicIP: "10.20.30.40", - DeployConfig: files.DeployConfig{ - Images: map[string]files.ImageConfig{}, - }, - Plans: files.PlansConfig{ - HostingPlans: map[int]files.HostingPlan{}, - WorkspacePlans: map[int]files.WorkspacePlan{}, - }, - }, + It("should handle missing APIServerHost", func() { + installConfig := createBaseConfig("missing-host", "10.0.0.1") + installConfig.Kubernetes.APIServerHost = "" + + k0sConfig, err := installer.GenerateK0sConfig(installConfig) + if err == nil { + Expect(k0sConfig).NotTo(BeNil()) + Expect(k0sConfig.Spec.API.Address).To(Equal("10.0.0.1")) + } else { + Expect(err).To(HaveOccurred()) } + }) + + It("should handle missing datacenter name", func() { + installConfig := createBaseConfig("", "10.0.0.1") k0sConfig, err := installer.GenerateK0sConfig(installConfig) + if err == nil { + Expect(k0sConfig).NotTo(BeNil()) + } else { + Expect(err).To(HaveOccurred()) + } + }) + + It("should fail when writing to read-only directory", func() { + readOnlyDir := filepath.Join(tempDir, "readonly") + err := os.Mkdir(readOnlyDir, 0444) Expect(err).NotTo(HaveOccurred()) - yamlData, err := k0sConfig.Marshal() + readOnlyPath := filepath.Join(readOnlyDir, "config.yaml") + installConfig := createBaseConfig("test", "10.0.0.1") + configData, err := yaml.Marshal(installConfig) Expect(err).NotTo(HaveOccurred()) - Expect(string(yamlData)).To(ContainSubstring("k0s.k0sproject.io/v1beta1")) - Expect(string(yamlData)).To(ContainSubstring("ClusterConfig")) - Expect(string(yamlData)).To(ContainSubstring("10.20.30.40")) + + err = os.WriteFile(readOnlyPath, configData, 0644) + Expect(err).To(HaveOccurred()) + + os.Chmod(readOnlyDir, 0755) }) + }) - It("should round-trip marshal and unmarshal correctly", func() { - installConfig := &files.RootConfig{ - Datacenter: files.DatacenterConfig{ - ID: 1, - Name: "roundtrip-test", - }, - Kubernetes: files.KubernetesConfig{ - ManagedByCodesphere: true, - ControlPlanes: []files.K8sNode{ - {IPAddress: "172.16.0.1"}, - }, - PodCIDR: "10.244.0.0/16", - ServiceCIDR: "10.96.0.0/12", - }, - Codesphere: files.CodesphereConfig{ - Domain: "roundtrip.test", - PublicIP: "172.16.0.1", - DeployConfig: files.DeployConfig{ - Images: map[string]files.ImageConfig{}, - }, - Plans: files.PlansConfig{ - HostingPlans: map[int]files.HostingPlan{}, - WorkspacePlans: map[int]files.WorkspacePlan{}, - }, - }, - } + Describe("YAML Serialization", func() { + It("should marshal and unmarshal k0s config correctly", func() { + installConfig := createBaseConfig("roundtrip-test", "172.16.0.1") + installConfig.Kubernetes.PodCIDR = "10.244.0.0/16" + installConfig.Kubernetes.ServiceCIDR = "10.96.0.0/12" original, err := installer.GenerateK0sConfig(installConfig) Expect(err).NotTo(HaveOccurred()) - // Marshal to YAML yamlData, err := original.Marshal() Expect(err).NotTo(HaveOccurred()) + Expect(string(yamlData)).To(ContainSubstring("k0s.k0sproject.io/v1beta1")) + Expect(string(yamlData)).To(ContainSubstring("ClusterConfig")) - // Unmarshal back var restored installer.K0sConfig err = yaml.Unmarshal(yamlData, &restored) Expect(err).NotTo(HaveOccurred()) - // Verify they match + // Verify critical fields match Expect(restored.APIVersion).To(Equal(original.APIVersion)) Expect(restored.Kind).To(Equal(original.Kind)) Expect(restored.Metadata.Name).To(Equal(original.Metadata.Name)) @@ -370,83 +365,45 @@ var _ = Describe("K0s Install-Config Integration", func() { }) }) - Describe("Full Workflow Integration", func() { - It("should complete full config generation workflow", func() { - // Step 1: Create install-config - installConfig := &files.RootConfig{ - Datacenter: files.DatacenterConfig{ - ID: 1, - Name: "integration-dc", - City: "Integration City", - CountryCode: "US", - }, - Kubernetes: files.KubernetesConfig{ - ManagedByCodesphere: true, - ControlPlanes: []files.K8sNode{ - {IPAddress: "203.0.113.10"}, - }, - APIServerHost: "api.integration.test", - PodCIDR: "10.244.0.0/16", - ServiceCIDR: "10.96.0.0/12", - }, - Codesphere: files.CodesphereConfig{ - Domain: "integration.test", - PublicIP: "203.0.113.10", - DeployConfig: files.DeployConfig{ - Images: map[string]files.ImageConfig{}, - }, - Plans: files.PlansConfig{ - HostingPlans: map[int]files.HostingPlan{}, - WorkspacePlans: map[int]files.WorkspacePlan{}, - }, - }, - } + Describe("Config Persistence", func() { + It("should persist and reload config correctly", func() { + originalConfig := createBaseConfig("persist-test", "172.20.30.40") + originalConfig.Kubernetes.PodCIDR = "10.100.0.0/16" + originalConfig.Kubernetes.ServiceCIDR = "10.200.0.0/16" - // Step 2: Write install-config - configData, err := yaml.Marshal(installConfig) + // Save install-config + configData, err := yaml.Marshal(originalConfig) Expect(err).NotTo(HaveOccurred()) err = os.WriteFile(configPath, configData, 0644) Expect(err).NotTo(HaveOccurred()) - // Step 3: Load install-config - icg := installer.NewInstallConfigManager() - err = icg.LoadInstallConfigFromFile(configPath) + // Generate and save k0s config + k0sConfig, err := installer.GenerateK0sConfig(originalConfig) Expect(err).NotTo(HaveOccurred()) - - // Step 4: Generate k0s config - loadedConfig := icg.GetInstallConfig() - k0sConfig, err := installer.GenerateK0sConfig(loadedConfig) - Expect(err).NotTo(HaveOccurred()) - - // Step 5: Marshal k0s config k0sData, err := k0sConfig.Marshal() Expect(err).NotTo(HaveOccurred()) - - // Step 6: Write k0s config err = os.WriteFile(k0sConfigOut, k0sData, 0644) Expect(err).NotTo(HaveOccurred()) - // Step 7: Verify complete workflow - Expect(k0sConfigOut).To(BeAnExistingFile()) - - // Step 8: Load and verify k0s config - readData, err := os.ReadFile(k0sConfigOut) + // Reload install-config + icg := installer.NewInstallConfigManager() + err = icg.LoadInstallConfigFromFile(configPath) Expect(err).NotTo(HaveOccurred()) + reloadedInstallConfig := icg.GetInstallConfig() - var finalK0sConfig installer.K0sConfig - err = yaml.Unmarshal(readData, &finalK0sConfig) + // Reload k0s config + reloadedK0sData, err := os.ReadFile(k0sConfigOut) + Expect(err).NotTo(HaveOccurred()) + var reloadedK0sConfig installer.K0sConfig + err = yaml.Unmarshal(reloadedK0sData, &reloadedK0sConfig) Expect(err).NotTo(HaveOccurred()) - // Step 9: Validate all fields - Expect(finalK0sConfig.APIVersion).To(Equal("k0s.k0sproject.io/v1beta1")) - Expect(finalK0sConfig.Kind).To(Equal("ClusterConfig")) - Expect(finalK0sConfig.Metadata.Name).To(Equal("codesphere-integration-dc")) - Expect(finalK0sConfig.Spec.API.Address).To(Equal("203.0.113.10")) - Expect(finalK0sConfig.Spec.API.ExternalAddress).To(Equal("api.integration.test")) - Expect(finalK0sConfig.Spec.Network.PodCIDR).To(Equal("10.244.0.0/16")) - Expect(finalK0sConfig.Spec.Network.ServiceCIDR).To(Equal("10.96.0.0/12")) - Expect(finalK0sConfig.Spec.Storage.Type).To(Equal("etcd")) - Expect(finalK0sConfig.Spec.Storage.Etcd.PeerAddress).To(Equal("203.0.113.10")) + // Verify both configs match original + Expect(reloadedInstallConfig.Datacenter.Name).To(Equal(originalConfig.Datacenter.Name)) + Expect(reloadedInstallConfig.Kubernetes.PodCIDR).To(Equal(originalConfig.Kubernetes.PodCIDR)) + Expect(reloadedK0sConfig.Metadata.Name).To(Equal(k0sConfig.Metadata.Name)) + Expect(reloadedK0sConfig.Spec.API.Address).To(Equal(k0sConfig.Spec.API.Address)) + Expect(reloadedK0sConfig.Spec.Network.PodCIDR).To(Equal(k0sConfig.Spec.Network.PodCIDR)) }) }) }) From 071eea949dc50f69a0aebd731876d27276c04ea3 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:04:30 +0100 Subject: [PATCH 06/32] ref: cleanup --- cli/cmd/install_k0s.go | 42 ++++------------------- cli/cmd/install_k0s_test.go | 66 ++++--------------------------------- 2 files changed, 13 insertions(+), 95 deletions(-) diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index 220654e..b314347 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -32,7 +32,6 @@ type InstallK0sOpts struct { *GlobalOptions Version string Package string - Config string InstallConfig string SSHKeyPath string RemoteHost string @@ -46,16 +45,11 @@ func (c *InstallK0sCmd) RunE(_ *cobra.Command, args []string) error { pm := installer.NewPackage(env.GetOmsWorkdir(), c.Opts.Package) k0s := installer.NewK0s(hw, env, c.FileWriter) - if c.Opts.InstallConfig != "" { - return c.InstallK0sFromInstallConfig(pm, k0s) + if c.Opts.InstallConfig == "" { + return fmt.Errorf("--install-config is required") } - err := c.InstallK0s(pm, k0s) - if err != nil { - return fmt.Errorf("failed to install k0s: %w", err) - } - - return nil + return c.InstallK0sFromInstallConfig(pm, k0s) } func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { @@ -67,17 +61,14 @@ func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { This will either download the k0s binary directly to the OMS workdir, if not already present, and install it or load the k0s binary from the provided package file and install it. If no version is specified, the latest version will be downloaded. - If no install config is provided, k0s will be installed with the '--single' flag. - You can also install k0s from a Codesphere install-config file, which will: + You must provide a Codesphere install-config file, which will: - Generate a k0s configuration from the install-config - Optionally install k0s on remote nodes via SSH`), Example: formatExamplesWithBinary("install k0s", []packageio.Example{ - {Cmd: "", Desc: "Install k0s using the Go-native implementation"}, + {Cmd: "--install-config ", Desc: "Path to Codesphere install-config file to generate k0s config from"}, {Cmd: "--version ", Desc: "Version of k0s to install"}, {Cmd: "--package ", Desc: "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load k0s from"}, - {Cmd: "--k0s-config ", Desc: "Path to k0s configuration file, if not set k0s will be installed with the '--single' flag"}, - {Cmd: "--install-config ", Desc: "Path to Codesphere install-config file to generate k0s config from"}, {Cmd: "--remote-host ", Desc: "Remote host IP to install k0s on (requires --ssh-key-path)"}, {Cmd: "--ssh-key-path ", Desc: "SSH private key path for remote installation"}, {Cmd: "--force", Desc: "Force new download and installation even if k0s binary exists or is already installed"}, @@ -89,8 +80,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().StringVarP(&k0s.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load k0s from") - k0s.cmd.Flags().StringVar(&k0s.Opts.Config, "k0s-config", "", "Path to k0s configuration file") - k0s.cmd.Flags().StringVar(&k0s.Opts.InstallConfig, "install-config", "", "Path to Codesphere install-config file") + 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().StringVar(&k0s.Opts.RemoteHost, "remote-host", "", "Remote host IP to install k0s on") k0s.cmd.Flags().StringVar(&k0s.Opts.RemoteUser, "remote-user", "root", "Remote user for SSH connection") @@ -103,26 +93,6 @@ func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { const defaultK0sPath = "kubernetes/files/k0s" -func (c *InstallK0sCmd) InstallK0s(pm installer.PackageManager, k0s installer.K0sManager) error { - // Default dependency path for k0s binary within package - k0sPath := pm.GetDependencyPath(defaultK0sPath) - - var err error - if c.Opts.Package == "" { - k0sPath, err = k0s.Download(c.Opts.Version, c.Opts.Force, false) - if err != nil { - return fmt.Errorf("failed to download k0s: %w", err) - } - } - - err = k0s.Install(c.Opts.Config, k0sPath, c.Opts.Force) - if err != nil { - return fmt.Errorf("failed to install k0s: %w", err) - } - - return nil -} - func (c *InstallK0sCmd) InstallK0sFromInstallConfig(pm installer.PackageManager, k0s installer.K0sManager) error { icg := installer.NewInstallConfigManager() if err := icg.LoadInstallConfigFromFile(c.Opts.InstallConfig); err != nil { diff --git a/cli/cmd/install_k0s_test.go b/cli/cmd/install_k0s_test.go index 8c59b54..c6b3e20 100644 --- a/cli/cmd/install_k0s_test.go +++ b/cli/cmd/install_k0s_test.go @@ -4,14 +4,11 @@ package cmd_test import ( - "errors" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/codesphere-cloud/oms/cli/cmd" "github.com/codesphere-cloud/oms/internal/env" - "github.com/codesphere-cloud/oms/internal/installer" "github.com/codesphere-cloud/oms/internal/util" ) @@ -32,7 +29,7 @@ var _ = Describe("InstallK0sCmd", func() { GlobalOptions: globalOpts, Version: "", Package: "", - Config: "", + InstallConfig: "", Force: false, } c = cmd.InstallK0sCmd{ @@ -47,63 +44,14 @@ var _ = Describe("InstallK0sCmd", func() { mockFileWriter.AssertExpectations(GinkgoT()) }) - Context("InstallK0s method", func() { - It("fails when package is not specified and k0s download fails", func() { - mockPackageManager := installer.NewMockPackageManager(GinkgoT()) - mockK0sManager := installer.NewMockK0sManager(GinkgoT()) - - c.Opts.Package = "" // No package specified, should download - mockPackageManager.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/workdir/test-package/deps/kubernetes/files/k0s") - mockK0sManager.EXPECT().Download("", false, false).Return("", errors.New("download failed")) - - err := c.InstallK0s(mockPackageManager, mockK0sManager) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to download k0s")) - Expect(err.Error()).To(ContainSubstring("download failed")) - }) - - It("fails when k0s install fails", func() { - mockPackageManager := installer.NewMockPackageManager(GinkgoT()) - mockK0sManager := installer.NewMockK0sManager(GinkgoT()) + Context("RunE method", func() { + It("fails when install-config is not provided", func() { + c.Opts.InstallConfig = "" + mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir") - c.Opts.Package = "" // No package specified, should download - c.Opts.Config = "/path/to/config.yaml" - c.Opts.Force = true - mockPackageManager.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/workdir/test-package/deps/kubernetes/files/k0s") - mockK0sManager.EXPECT().Download("", true, false).Return("/test/workdir/k0s", nil) - mockK0sManager.EXPECT().Install("/path/to/config.yaml", "/test/workdir/k0s", true).Return(errors.New("install failed")) - - err := c.InstallK0s(mockPackageManager, mockK0sManager) + err := c.RunE(nil, nil) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to install k0s")) - Expect(err.Error()).To(ContainSubstring("install failed")) - }) - - It("succeeds when package is not specified and k0s download and install work", func() { - mockPackageManager := installer.NewMockPackageManager(GinkgoT()) - mockK0sManager := installer.NewMockK0sManager(GinkgoT()) - - c.Opts.Package = "" // No package specified, should download - c.Opts.Config = "" // No config, will use single mode - mockPackageManager.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/workdir/test-package/deps/kubernetes/files/k0s") - mockK0sManager.EXPECT().Download("", false, false).Return("/test/workdir/k0s", nil) - mockK0sManager.EXPECT().Install("", "/test/workdir/k0s", false).Return(nil) - - err := c.InstallK0s(mockPackageManager, mockK0sManager) - Expect(err).ToNot(HaveOccurred()) - }) - - It("succeeds when package is specified and k0s install works", func() { - mockPackageManager := installer.NewMockPackageManager(GinkgoT()) - mockK0sManager := installer.NewMockK0sManager(GinkgoT()) - - c.Opts.Package = "test-package.tar.gz" // Package specified, should use k0s from package - c.Opts.Config = "/path/to/config.yaml" - mockPackageManager.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/workdir/test-package/deps/kubernetes/files/k0s") - mockK0sManager.EXPECT().Install("/path/to/config.yaml", "/test/workdir/test-package/deps/kubernetes/files/k0s", false).Return(nil) - - err := c.InstallK0s(mockPackageManager, mockK0sManager) - Expect(err).ToNot(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("--install-config is required")) }) }) }) From 9de5965476fef6c1a1733208ecc053b3715060ad Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:05:25 +0000 Subject: [PATCH 07/32] chore(docs): Auto-update docs and licenses Signed-off-by: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> --- docs/oms-cli_install_k0s.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/docs/oms-cli_install_k0s.md b/docs/oms-cli_install_k0s.md index 08b17f3..e0084e7 100644 --- a/docs/oms-cli_install_k0s.md +++ b/docs/oms-cli_install_k0s.md @@ -8,9 +8,8 @@ Install k0s either from the package or by downloading it. This will either download the k0s binary directly to the OMS workdir, if not already present, and install it or load the k0s binary from the provided package file and install it. If no version is specified, the latest version will be downloaded. -If no install config is provided, k0s will be installed with the '--single' flag. -You can also install k0s from a Codesphere install-config file, which will: +You must provide a Codesphere install-config file, which will: - Generate a k0s configuration from the install-config - Optionally install k0s on remote nodes via SSH @@ -21,8 +20,8 @@ oms-cli install k0s [flags] ### Examples ``` -# Install k0s using the Go-native implementation -$ oms-cli install k0s +# Path to Codesphere install-config file to generate k0s config from +$ oms-cli install k0s --install-config # Version of k0s to install $ oms-cli install k0s --version @@ -30,12 +29,6 @@ $ oms-cli install k0s --version # Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load k0s from $ oms-cli install k0s --package -# Path to k0s configuration file, if not set k0s will be installed with the '--single' flag -$ oms-cli install k0s --k0s-config - -# Path to Codesphere install-config file to generate k0s config from -$ oms-cli install k0s --install-config - # Remote host IP to install k0s on (requires --ssh-key-path) $ oms-cli install k0s --remote-host @@ -52,8 +45,7 @@ $ oms-cli install k0s --force ``` -f, --force Force new download and installation -h, --help help for k0s - --install-config string Path to Codesphere install-config file - --k0s-config string Path to k0s configuration file + --install-config string Path to Codesphere install-config file (required) -p, --package string Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load k0s from --remote-host string Remote host IP to install k0s on --remote-user string Remote user for SSH connection (default "root") From 01d67504bee81e4bbd4b23be549e3d2bf9287ece Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:05:53 +0100 Subject: [PATCH 08/32] Update internal/installer/node/node.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/installer/node/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index 438d80b..ab437eb 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -175,7 +175,7 @@ func (n *NodeManager) RunSSHCommand(jumpboxIp string, ip string, username string defer func() { _ = client.Close() }() session, err := client.NewSession() if err != nil { - return fmt.Errorf("failed to create session on jumpbox: %v", err) + return fmt.Errorf("failed to create session on target node (%s): %v", ip, err) } defer func() { _ = session.Close() }() From b19ac3b7f79a9b5edaf31673e8dc076c4d134878 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:10:09 +0100 Subject: [PATCH 09/32] ref: enhance shell command safety --- internal/installer/node/node.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index 438d80b..558ab65 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -9,6 +9,7 @@ import ( "net" "os" "path/filepath" + "strings" "syscall" "time" @@ -31,6 +32,10 @@ type NodeManager struct { KeyPath string } +func shellEscape(s string) string { + return strings.ReplaceAll(s, "'", "'\\''") +} + func (n *NodeManager) getHostKeyCallback() (ssh.HostKeyCallback, error) { homeDir, err := os.UserHomeDir() if err != nil { @@ -262,7 +267,7 @@ func (n *NodeManager) GetSFTPClient(jumpboxIp string, ip string, username string } func (nm *NodeManager) EnsureDirectoryExists(ip string, username string, dir string) error { - cmd := fmt.Sprintf("mkdir -p '%s'", dir) + cmd := fmt.Sprintf("mkdir -p '%s'", shellEscape(dir)) return nm.RunSSHCommand("", ip, username, cmd) } @@ -294,7 +299,7 @@ func (n *NodeManager) CopyFile(jumpboxIp string, ip string, username string, src } func (n *Node) HasCommand(nm *NodeManager, command string) bool { - checkCommand := fmt.Sprintf("command -v %s >/dev/null 2>&1", command) + checkCommand := fmt.Sprintf("command -v '%s' >/dev/null 2>&1", shellEscape(command)) err := nm.RunSSHCommand("", n.ExternalIP, "root", checkCommand) return err == nil } @@ -355,7 +360,7 @@ func (n *Node) HasRootLoginEnabled(jumpbox *Node, nm *NodeManager) bool { } func (n *Node) HasFile(jumpbox *Node, nm *NodeManager, filePath string) bool { - checkCommand := fmt.Sprintf("test -f '%s'", filePath) + checkCommand := fmt.Sprintf("test -f '%s'", shellEscape(filePath)) err := n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommand) return err == nil } @@ -394,7 +399,7 @@ func (n *Node) InstallK0s(nm *NodeManager, k0sBinaryPath string, k0sConfigPath s } log.Printf("Making k0s binary executable on %s", n.ExternalIP) - chmodCmd := fmt.Sprintf("chmod +x %s", remoteK0sBinary) + chmodCmd := fmt.Sprintf("chmod +x '%s'", shellEscape(remoteK0sBinary)) if err := nm.RunSSHCommand("", n.ExternalIP, "root", chmodCmd); err != nil { return fmt.Errorf("failed to make k0s binary executable: %w", err) } @@ -409,9 +414,9 @@ func (n *Node) InstallK0s(nm *NodeManager, k0sBinaryPath string, k0sConfigPath s } } - installCmd := fmt.Sprintf("sudo %s install controller", remoteK0sBinary) + installCmd := fmt.Sprintf("sudo '%s' install controller", shellEscape(remoteK0sBinary)) if k0sConfigPath != "" { - installCmd += fmt.Sprintf(" --config %s", remoteConfigPath) + installCmd += fmt.Sprintf(" --config '%s'", shellEscape(remoteConfigPath)) } else { installCmd += " --single" } @@ -425,8 +430,8 @@ func (n *Node) InstallK0s(nm *NodeManager, k0sBinaryPath string, k0sConfigPath s } log.Printf("k0s successfully installed on %s", n.ExternalIP) - log.Printf("You can start it using: ssh root@%s 'sudo %s start'", n.ExternalIP, remoteK0sBinary) - log.Printf("You can check the status using: ssh root@%s 'sudo %s status'", n.ExternalIP, remoteK0sBinary) + log.Printf("You can start it using: ssh root@%s 'sudo %s start'", n.ExternalIP, shellEscape(remoteK0sBinary)) + log.Printf("You can check the status using: ssh root@%s 'sudo %s status'", n.ExternalIP, shellEscape(remoteK0sBinary)) return nil } From d4d1b5f164eb789ef77d8b6aefcfebb281693782 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:41:38 +0100 Subject: [PATCH 10/32] test: add tests for NodeManager and Node methods --- internal/installer/node/node_test.go | 294 +++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 internal/installer/node/node_test.go diff --git a/internal/installer/node/node_test.go b/internal/installer/node/node_test.go new file mode 100644 index 0000000..1db8f10 --- /dev/null +++ b/internal/installer/node/node_test.go @@ -0,0 +1,294 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package node_test + +import ( + "errors" + "io" + "os" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/codesphere-cloud/oms/internal/installer/node" + "github.com/codesphere-cloud/oms/internal/util" +) + +func TestNode(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Node Suite") +} + +var _ = Describe("Node", func() { + Describe("NodeManager", func() { + var ( + nm *node.NodeManager + mockFileWriter *util.MockFileIO + ) + + BeforeEach(func() { + mockFileWriter = util.NewMockFileIO(GinkgoT()) + nm = &node.NodeManager{ + FileIO: mockFileWriter, + KeyPath: "", + } + }) + + AfterEach(func() { + mockFileWriter.AssertExpectations(GinkgoT()) + }) + + Context("authentication methods", func() { + It("should return error when no authentication method is available", func() { + originalAuthSock := os.Getenv("SSH_AUTH_SOCK") + defer func() { + if originalAuthSock != "" { + os.Setenv("SSH_AUTH_SOCK", originalAuthSock) + } else { + os.Unsetenv("SSH_AUTH_SOCK") + } + }() + os.Unsetenv("SSH_AUTH_SOCK") + + nm.KeyPath = "" + + client, err := nm.GetClient("", "10.0.0.1", "root") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no valid authentication methods")) + Expect(client).To(BeNil()) + }) + + It("should return error when key file cannot be read", func() { + originalAuthSock := os.Getenv("SSH_AUTH_SOCK") + defer func() { + if originalAuthSock != "" { + os.Setenv("SSH_AUTH_SOCK", originalAuthSock) + } else { + os.Unsetenv("SSH_AUTH_SOCK") + } + }() + os.Unsetenv("SSH_AUTH_SOCK") + + nm.KeyPath = "/nonexistent/key" + mockFileWriter.EXPECT().ReadFile("/nonexistent/key").Return(nil, errors.New("file not found")) + + client, err := nm.GetClient("", "10.0.0.1", "root") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to read private key file")) + Expect(client).To(BeNil()) + }) + + It("should return error when key file is invalid", func() { + originalAuthSock := os.Getenv("SSH_AUTH_SOCK") + defer func() { + if originalAuthSock != "" { + os.Setenv("SSH_AUTH_SOCK", originalAuthSock) + } else { + os.Unsetenv("SSH_AUTH_SOCK") + } + }() + os.Unsetenv("SSH_AUTH_SOCK") + + invalidKey := []byte("not a valid ssh key") + nm.KeyPath = "/path/to/invalid/key" + mockFileWriter.EXPECT().ReadFile("/path/to/invalid/key").Return(invalidKey, nil) + + client, err := nm.GetClient("", "10.0.0.1", "root") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to parse private key")) + Expect(client).To(BeNil()) + }) + }) + + Context("SSH connection", func() { + It("should fail to connect to invalid host", func() { + privateKey := []byte(`-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDjKvZvwzXnCdFniXHDZdFPo4LFJ7KJJdBWrJjN1rO1ZQAAAJgNY3PmDWNz +5gAAAAtzc2gtZWQyNTUxOQAAACDjKvZvwzXnCdFniXHDZdFPo4LFJ7KJJdBWrJjN1rO1ZQ +AAAEDcZfnYLBVPEQT3qYDh6e5zMvKjN8x5k4l3n9qYLFJ7MOMq9m/DNecJ0WeJccNl0U+j +gsUnsokl0FasmM3Ws7VlAAAADnRlc3RAZXhhbXBsZS5jb20BAgMEBQ== +-----END OPENSSH PRIVATE KEY-----`) + + nm.KeyPath = "/path/to/key" + mockFileWriter.EXPECT().ReadFile("/path/to/key").Return(privateKey, nil).Maybe() + + client, err := nm.GetClient("", "192.0.2.1", "root") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to dial")) + Expect(client).To(BeNil()) + }) + + It("should fail to connect through invalid jumpbox", func() { + privateKey := []byte(`-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDjKvZvwzXnCdFniXHDZdFPo4LFJ7KJJdBWrJjN1rO1ZQAAAJgNY3PmDWNz +5gAAAAtzc2gtZWQyNTUxOQAAACDjKvZvwzXnCdFniXHDZdFPo4LFJ7KJJdBWrJjN1rO1ZQ +AAAEDcZfnYLBVPEQT3qYDh6e5zMvKjN8x5k4l3n9qYLFJ7MOMq9m/DNecJ0WeJccNl0U+j +gsUnsokl0FasmM3Ws7VlAAAADnRlc3RAZXhhbXBsZS5jb20BAgMEBQ== +-----END OPENSSH PRIVATE KEY-----`) + + nm.KeyPath = "/path/to/key" + mockFileWriter.EXPECT().ReadFile("/path/to/key").Return(privateKey, nil).Maybe() + + client, err := nm.GetClient("192.0.2.1", "192.0.2.2", "root") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to connect to jumpbox")) + Expect(client).To(BeNil()) + }) + }) + + Context("file operations", func() { + It("should handle directory creation errors", func() { + err := nm.EnsureDirectoryExists("192.0.2.1", "root", "/tmp/test") + Expect(err).To(HaveOccurred()) + }) + + It("should handle copy file errors when source doesn't exist", func() { + mockFileWriter.EXPECT().Open("/nonexistent/file").Return(nil, errors.New("file not found")).Maybe() + + err := nm.CopyFile("", "192.0.2.1", "root", "/nonexistent/file", "/tmp/dest") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get SSH client")) + }) + }) + }) + + Describe("Node methods", func() { + var ( + n *node.Node + nm *node.NodeManager + mockFileWriter *util.MockFileIO + ) + + BeforeEach(func() { + mockFileWriter = util.NewMockFileIO(GinkgoT()) + nm = &node.NodeManager{ + FileIO: mockFileWriter, + KeyPath: "", + } + n = &node.Node{ + Name: "test-node", + ExternalIP: "10.0.0.1", + InternalIP: "192.168.1.1", + } + }) + + AfterEach(func() { + mockFileWriter.AssertExpectations(GinkgoT()) + }) + + Context("HasCommand", func() { + It("should return false when SSH connection fails", func() { + result := n.HasCommand(nm, "kubectl") + Expect(result).To(BeFalse()) + }) + + It("should handle commands with special characters safely", func() { + result := n.HasCommand(nm, "kubectl'; rm -rf /; echo '") + Expect(result).To(BeFalse()) + }) + }) + + Context("HasFile", func() { + It("should return false when SSH connection fails", func() { + result := n.HasFile(nil, nm, "/etc/k0s/k0s.yaml") + Expect(result).To(BeFalse()) + }) + + It("should handle paths with special characters safely", func() { + result := n.HasFile(nil, nm, "/path'; rm -rf /; echo '/file.txt") + Expect(result).To(BeFalse()) + }) + + It("should support jumpbox connections", func() { + jumpbox := &node.Node{ + ExternalIP: "10.0.0.2", + InternalIP: "10.0.0.2", + } + result := n.HasFile(jumpbox, nm, "/etc/k0s/k0s.yaml") + Expect(result).To(BeFalse()) + }) + }) + + Context("CopyFile", func() { + It("should fail when directory creation fails", func() { + err := n.CopyFile(nm, "/some/file", "/remote/path/dest.txt") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to ensure directory exists")) + }) + }) + + Context("RunSSHCommand", func() { + It("should handle direct connection without jumpbox", func() { + err := n.RunSSHCommand(nil, nm, "root", "echo test") + Expect(err).To(HaveOccurred()) + }) + + It("should handle connection through jumpbox", func() { + jumpbox := &node.Node{ + ExternalIP: "10.0.0.2", + InternalIP: "10.0.0.2", + } + err := n.RunSSHCommand(jumpbox, nm, "ubuntu", "echo test") + Expect(err).To(HaveOccurred()) + }) + }) + + Context("InstallK0s", func() { + It("should handle binary copy failure", func() { + k0sBinaryPath := "/path/to/k0s" + mockFileWriter.EXPECT().Open(k0sBinaryPath).Return(nil, errors.New("file not found")).Maybe() + + err := n.InstallK0s(nm, k0sBinaryPath, "", false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to ensure directory exists")) + }) + + It("should handle paths with special characters safely", func() { + k0sBinaryPath := "/path/to/k0s'; echo 'injected" + + err := n.InstallK0s(nm, k0sBinaryPath, "", false) + Expect(err).To(HaveOccurred()) + }) + + It("should support force flag parameter", func() { + k0sBinaryPath := "/tmp/k0s" + + err := n.InstallK0s(nm, k0sBinaryPath, "", true) + Expect(err).To(HaveOccurred()) + // Will fail to connect, but tests that force flag is handled + }) + + It("should support config file parameter", func() { + k0sBinaryPath := "/tmp/k0s" + k0sConfigPath := "/tmp/k0s.yaml" + + err := n.InstallK0s(nm, k0sBinaryPath, k0sConfigPath, false) + Expect(err).To(HaveOccurred()) + // Will fail to connect, but tests that config path is handled + }) + }) + }) +}) + +// mockReadCloser implements io.ReadCloser for testing +type mockReadCloser struct { + content []byte + pos int +} + +func (m *mockReadCloser) Read(p []byte) (n int, err error) { + if m.pos >= len(m.content) { + return 0, io.EOF + } + n = copy(p, m.content[m.pos:]) + m.pos += n + return n, nil +} + +func (m *mockReadCloser) Close() error { + return nil +} From 97bb3b884e5f5f348f5564a1cc5b0e09ba846905 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:01:15 +0100 Subject: [PATCH 11/32] fix: improve passphrase handling in getAuthMethods --- internal/installer/node/node.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index a5d35ce..d2377e1 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -96,11 +96,13 @@ func (n *NodeManager) getAuthMethods() ([]ssh.AuthMethod, error) { return nil, fmt.Errorf("failed to read passphrase: %v", err) } - signer, err = ssh.ParsePrivateKeyWithPassphrase(key, passphraseBytes) - for i := range passphraseBytes { - passphraseBytes[i] = 0 - } + defer func() { + for i := range passphraseBytes { + passphraseBytes[i] = 0 + } + }() + signer, err = ssh.ParsePrivateKeyWithPassphrase(key, passphraseBytes) if err != nil { return nil, fmt.Errorf("failed to parse private key with passphrase: %v", err) } From b07c0a80647b3db8d2806a6c96fd30a8593fdd0a Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:10:37 +0100 Subject: [PATCH 12/32] fix: enforce required flags for remote installation and clean up test code --- cli/cmd/install_k0s.go | 6 ++--- internal/installer/node/node_test.go | 38 +++++++--------------------- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index b314347..b5b0993 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -86,6 +86,8 @@ func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { k0s.cmd.Flags().StringVar(&k0s.Opts.RemoteUser, "remote-user", "root", "Remote user for SSH connection") k0s.cmd.Flags().BoolVarP(&k0s.Opts.Force, "force", "f", false, "Force new download and installation") + k0s.cmd.MarkFlagsRequiredTogether("remote-host", "ssh-key-path") + install.AddCommand(k0s.cmd) k0s.cmd.RunE = k0s.RunE @@ -146,10 +148,6 @@ func (c *InstallK0sCmd) InstallK0sFromInstallConfig(pm installer.PackageManager, } func (c *InstallK0sCmd) InstallK0sRemote(config *files.RootConfig, k0sBinaryPath string, k0sConfigPath string) error { - if c.Opts.SSHKeyPath == "" { - return fmt.Errorf("--ssh-key-path is required for remote installation") - } - log.Printf("Installing k0s on remote host %s", c.Opts.RemoteHost) nm := &node.NodeManager{ diff --git a/internal/installer/node/node_test.go b/internal/installer/node/node_test.go index 1db8f10..44d3a6d 100644 --- a/internal/installer/node/node_test.go +++ b/internal/installer/node/node_test.go @@ -5,7 +5,6 @@ package node_test import ( "errors" - "io" "os" "testing" @@ -45,12 +44,12 @@ var _ = Describe("Node", func() { originalAuthSock := os.Getenv("SSH_AUTH_SOCK") defer func() { if originalAuthSock != "" { - os.Setenv("SSH_AUTH_SOCK", originalAuthSock) + _ = os.Setenv("SSH_AUTH_SOCK", originalAuthSock) } else { - os.Unsetenv("SSH_AUTH_SOCK") + _ = os.Unsetenv("SSH_AUTH_SOCK") } }() - os.Unsetenv("SSH_AUTH_SOCK") + _ = os.Unsetenv("SSH_AUTH_SOCK") nm.KeyPath = "" @@ -64,12 +63,12 @@ var _ = Describe("Node", func() { originalAuthSock := os.Getenv("SSH_AUTH_SOCK") defer func() { if originalAuthSock != "" { - os.Setenv("SSH_AUTH_SOCK", originalAuthSock) + _ = os.Setenv("SSH_AUTH_SOCK", originalAuthSock) } else { - os.Unsetenv("SSH_AUTH_SOCK") + _ = os.Unsetenv("SSH_AUTH_SOCK") } }() - os.Unsetenv("SSH_AUTH_SOCK") + _ = os.Unsetenv("SSH_AUTH_SOCK") nm.KeyPath = "/nonexistent/key" mockFileWriter.EXPECT().ReadFile("/nonexistent/key").Return(nil, errors.New("file not found")) @@ -84,12 +83,12 @@ var _ = Describe("Node", func() { originalAuthSock := os.Getenv("SSH_AUTH_SOCK") defer func() { if originalAuthSock != "" { - os.Setenv("SSH_AUTH_SOCK", originalAuthSock) + _ = os.Setenv("SSH_AUTH_SOCK", originalAuthSock) } else { - os.Unsetenv("SSH_AUTH_SOCK") + _ = os.Unsetenv("SSH_AUTH_SOCK") } }() - os.Unsetenv("SSH_AUTH_SOCK") + _ = os.Unsetenv("SSH_AUTH_SOCK") invalidKey := []byte("not a valid ssh key") nm.KeyPath = "/path/to/invalid/key" @@ -273,22 +272,3 @@ gsUnsokl0FasmM3Ws7VlAAAADnRlc3RAZXhhbXBsZS5jb20BAgMEBQ== }) }) }) - -// mockReadCloser implements io.ReadCloser for testing -type mockReadCloser struct { - content []byte - pos int -} - -func (m *mockReadCloser) Read(p []byte) (n int, err error) { - if m.pos >= len(m.content) { - return 0, io.EOF - } - n = copy(p, m.content[m.pos:]) - m.pos += n - return n, nil -} - -func (m *mockReadCloser) Close() error { - return nil -} From 8f5647630191f9ff29b77e236d30139ae6ccf6d2 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:22:02 +0100 Subject: [PATCH 13/32] fix: update EnsureDirectoryExists to accept jumpbox IP --- internal/installer/node/node.go | 8 ++++---- internal/installer/node/node_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index d2377e1..2a5a26b 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -268,9 +268,9 @@ func (n *NodeManager) GetSFTPClient(jumpboxIp string, ip string, username string return sftpClient, nil } -func (nm *NodeManager) EnsureDirectoryExists(ip string, username string, dir string) error { +func (nm *NodeManager) EnsureDirectoryExists(jumpboxIp string, ip string, username string, dir string) error { cmd := fmt.Sprintf("mkdir -p '%s'", shellEscape(dir)) - return nm.RunSSHCommand("", ip, username, cmd) + return nm.RunSSHCommand(jumpboxIp, ip, username, cmd) } func (n *NodeManager) CopyFile(jumpboxIp string, ip string, username string, src string, dst string) error { @@ -323,7 +323,7 @@ func (n *Node) InstallOms(nm *NodeManager) error { } func (n *Node) CopyFile(nm *NodeManager, src string, dst string) error { - err := nm.EnsureDirectoryExists(n.ExternalIP, "root", filepath.Dir(dst)) + err := nm.EnsureDirectoryExists("", n.ExternalIP, "root", filepath.Dir(dst)) if err != nil { return fmt.Errorf("failed to ensure directory exists: %w", err) } @@ -408,7 +408,7 @@ func (n *Node) InstallK0s(nm *NodeManager, k0sBinaryPath string, k0sConfigPath s if k0sConfigPath != "" { log.Printf("Copying k0s config to %s:%s", n.ExternalIP, remoteConfigPath) - if err := nm.EnsureDirectoryExists(n.ExternalIP, "root", "/etc/k0s"); err != nil { + if err := nm.EnsureDirectoryExists("", n.ExternalIP, "root", "/etc/k0s"); err != nil { return fmt.Errorf("failed to create /etc/k0s directory: %w", err) } if err := nm.CopyFile("", n.ExternalIP, "root", k0sConfigPath, remoteConfigPath); err != nil { diff --git a/internal/installer/node/node_test.go b/internal/installer/node/node_test.go index 44d3a6d..8c7126f 100644 --- a/internal/installer/node/node_test.go +++ b/internal/installer/node/node_test.go @@ -141,7 +141,7 @@ gsUnsokl0FasmM3Ws7VlAAAADnRlc3RAZXhhbXBsZS5jb20BAgMEBQ== Context("file operations", func() { It("should handle directory creation errors", func() { - err := nm.EnsureDirectoryExists("192.0.2.1", "root", "/tmp/test") + err := nm.EnsureDirectoryExists("", "192.0.2.1", "root", "/tmp/test") Expect(err).To(HaveOccurred()) }) From f674352ceb3ba6ceb14270cbfadcc862fffdb59f Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:38:19 +0100 Subject: [PATCH 14/32] test: add tests for InstallK0sCmd --- cli/cmd/install_k0s_test.go | 212 ++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) diff --git a/cli/cmd/install_k0s_test.go b/cli/cmd/install_k0s_test.go index c6b3e20..b1f791b 100644 --- a/cli/cmd/install_k0s_test.go +++ b/cli/cmd/install_k0s_test.go @@ -4,11 +4,18 @@ package cmd_test import ( + "os" + "path/filepath" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + "gopkg.in/yaml.v3" "github.com/codesphere-cloud/oms/cli/cmd" "github.com/codesphere-cloud/oms/internal/env" + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/installer/files" "github.com/codesphere-cloud/oms/internal/util" ) @@ -54,4 +61,209 @@ var _ = Describe("InstallK0sCmd", func() { Expect(err.Error()).To(ContainSubstring("--install-config is required")) }) }) + + Context("InstallK0sFromInstallConfig method", func() { + var ( + mockPM *installer.MockPackageManager + mockK0s *installer.MockK0sManager + tempDir string + ) + + BeforeEach(func() { + mockPM = installer.NewMockPackageManager(GinkgoT()) + mockK0s = installer.NewMockK0sManager(GinkgoT()) + var err error + tempDir, err = os.MkdirTemp("", "install-k0s-test-*") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + mockPM.AssertExpectations(GinkgoT()) + mockK0s.AssertExpectations(GinkgoT()) + if tempDir != "" { + _ = os.RemoveAll(tempDir) + } + }) + + createTestConfig := func(managedByCodesphere bool) *files.RootConfig { + return &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "test-dc", + City: "Test City", + CountryCode: "US", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: managedByCodesphere, + ControlPlanes: []files.K8sNode{ + {IPAddress: "192.168.1.100"}, + }, + APIServerHost: "api.test.example.com", + }, + Codesphere: files.CodesphereConfig{ + Domain: "test.example.com", + PublicIP: "192.168.1.100", + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{}, + }, + Plans: files.PlansConfig{ + HostingPlans: map[int]files.HostingPlan{}, + WorkspacePlans: map[int]files.WorkspacePlan{}, + }, + }, + } + } + + It("fails when install-config file does not exist", func() { + c.Opts.InstallConfig = "/nonexistent/install-config.yaml" + + err := c.InstallK0sFromInstallConfig(mockPM, mockK0s) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to load install-config")) + }) + + It("fails when install-config specifies external Kubernetes", func() { + config := createTestConfig(false) + configPath := filepath.Join(tempDir, "install-config.yaml") + configData, err := yaml.Marshal(config) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(configPath, configData, 0644) + Expect(err).NotTo(HaveOccurred()) + + c.Opts.InstallConfig = configPath + + err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("external Kubernetes")) + }) + + It("successfully installs k0s locally with valid config", func() { + config := createTestConfig(true) + configPath := filepath.Join(tempDir, "install-config.yaml") + configData, err := yaml.Marshal(config) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(configPath, configData, 0644) + Expect(err).NotTo(HaveOccurred()) + + c.Opts.InstallConfig = configPath + c.Opts.Package = "test-package.tar.gz" + c.Opts.Force = true + + mockPM.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/path/k0s") + mockK0s.EXPECT().Install(mock.Anything, "/test/path/k0s", true).Return(nil) + + err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + Expect(err).NotTo(HaveOccurred()) + }) + + It("downloads k0s when package is not specified", func() { + config := createTestConfig(true) + configPath := filepath.Join(tempDir, "install-config.yaml") + configData, err := yaml.Marshal(config) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(configPath, configData, 0644) + Expect(err).NotTo(HaveOccurred()) + + c.Opts.InstallConfig = configPath + c.Opts.Package = "" + c.Opts.Version = "v1.29.0+k0s.0" + + mockPM.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/path/k0s") + mockK0s.EXPECT().Download("v1.29.0+k0s.0", false, false).Return("/downloaded/k0s", nil) + mockK0s.EXPECT().Install(mock.Anything, "/downloaded/k0s", false).Return(nil) + + err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + Expect(err).NotTo(HaveOccurred()) + }) + + It("fails when k0s download fails", func() { + config := createTestConfig(true) + configPath := filepath.Join(tempDir, "install-config.yaml") + configData, err := yaml.Marshal(config) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(configPath, configData, 0644) + Expect(err).NotTo(HaveOccurred()) + + c.Opts.InstallConfig = configPath + c.Opts.Package = "" + + mockPM.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/path/k0s") + mockK0s.EXPECT().Download("", false, false).Return("", os.ErrNotExist) + + err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to download k0s")) + }) + + It("fails when k0s install fails", func() { + config := createTestConfig(true) + configPath := filepath.Join(tempDir, "install-config.yaml") + configData, err := yaml.Marshal(config) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(configPath, configData, 0644) + Expect(err).NotTo(HaveOccurred()) + + c.Opts.InstallConfig = configPath + c.Opts.Package = "test-package.tar.gz" + + mockPM.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/path/k0s") + mockK0s.EXPECT().Install(mock.Anything, "/test/path/k0s", false).Return(os.ErrPermission) + + err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s")) + }) + + It("handles remote installation when remote-host is specified", func() { + config := createTestConfig(true) + configPath := filepath.Join(tempDir, "install-config.yaml") + configData, err := yaml.Marshal(config) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(configPath, configData, 0644) + Expect(err).NotTo(HaveOccurred()) + + c.Opts.InstallConfig = configPath + c.Opts.Package = "test-package.tar.gz" + c.Opts.RemoteHost = "192.168.1.50" + c.Opts.SSHKeyPath = "/path/to/key" + + mockPM.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/path/k0s") + + // Remote installation will fail because we can't actually connect, + // but we're testing that it attempts remote installation + err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) + }) + }) + + Context("InstallK0sRemote method", func() { + var ( + config *files.RootConfig + ) + + BeforeEach(func() { + config = &files.RootConfig{ + Datacenter: files.DatacenterConfig{ + ID: 1, + Name: "test-dc", + }, + Kubernetes: files.KubernetesConfig{ + ManagedByCodesphere: true, + ControlPlanes: []files.K8sNode{ + {IPAddress: "192.168.1.100"}, + }, + }, + } + }) + + It("fails when SSH connection cannot be established", func() { + c.Opts.RemoteHost = "192.0.2.1" // TEST-NET-1, should fail to connect + c.Opts.SSHKeyPath = "/tmp/nonexistent-key" + + err := c.InstallK0sRemote(config, "/path/to/k0s", "/path/to/config") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) + }) + }) }) From 4e610923120c2c7cf49c72f6f4965e8c680eac9c Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:41:00 +0100 Subject: [PATCH 15/32] fix: correct sed command syntax for enabling root login --- internal/installer/node/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index 2a5a26b..355dc63 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -377,7 +377,7 @@ func (n *Node) RunSSHCommand(jumpbox *Node, nm *NodeManager, username string, co func (n *Node) EnableRootLogin(jumpbox *Node, nm *NodeManager) error { cmds := []string{ - "sudo sed-i 's/^#\\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config", + "sudo sed -i 's/^#\\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config", "sudo sed -i 's/no-port-forwarding.*$//g' /root/.ssh/authorized_keys", "sudo systemctl restart sshd", } From 7a407f0e3b3fc10ba938994696aae426424ba800 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:02:44 +0100 Subject: [PATCH 16/32] test: add mock expectations for SSH key file reading in InstallK0sCmd tests --- cli/cmd/install_k0s_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/cmd/install_k0s_test.go b/cli/cmd/install_k0s_test.go index b1f791b..e82d03c 100644 --- a/cli/cmd/install_k0s_test.go +++ b/cli/cmd/install_k0s_test.go @@ -228,6 +228,7 @@ var _ = Describe("InstallK0sCmd", func() { c.Opts.SSHKeyPath = "/path/to/key" mockPM.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/path/k0s") + mockFileWriter.EXPECT().ReadFile("/path/to/key").Return([]byte("invalid-key-data"), nil).Maybe() // Remote installation will fail because we can't actually connect, // but we're testing that it attempts remote installation @@ -261,6 +262,8 @@ var _ = Describe("InstallK0sCmd", func() { c.Opts.RemoteHost = "192.0.2.1" // TEST-NET-1, should fail to connect c.Opts.SSHKeyPath = "/tmp/nonexistent-key" + mockFileWriter.EXPECT().ReadFile("/tmp/nonexistent-key").Return([]byte("invalid-key-data"), nil).Maybe() + err := c.InstallK0sRemote(config, "/path/to/k0s", "/path/to/config") Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) From 52e86679d9e16ec282ddc2652479d6fd9a04a505 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:58:05 +0100 Subject: [PATCH 17/32] test: add tests for K0s configuration filtering --- internal/installer/k0s_internal_test.go | 339 ++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 internal/installer/k0s_internal_test.go diff --git a/internal/installer/k0s_internal_test.go b/internal/installer/k0s_internal_test.go new file mode 100644 index 0000000..83ecc53 --- /dev/null +++ b/internal/installer/k0s_internal_test.go @@ -0,0 +1,339 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package installer + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" +) + +var _ = Describe("K0s Internal Methods", func() { + var ( + k0s *K0s + tempConfigDir string + ) + + BeforeEach(func() { + k0s = &K0s{} + var err error + tempConfigDir, err = os.MkdirTemp("", "k0s-config-test-*") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + if tempConfigDir != "" { + _ = os.RemoveAll(tempConfigDir) + } + }) + + createConfigFile := func(content string) string { + configPath := filepath.Join(tempConfigDir, "test-config.yaml") + err := os.WriteFile(configPath, []byte(content), 0644) + Expect(err).NotTo(HaveOccurred()) + return configPath + } + + Describe("filterConfigForK0s", func() { + It("filters out non-k0s top-level fields", func() { + configContent := `apiVersion: k0s.k0sproject.io/v1beta1 +kind: ClusterConfig +metadata: + name: test-cluster +spec: + api: + address: 192.168.1.100 +extraField: should-be-removed +anotherExtra: also-removed +` + configPath := createConfigFile(configContent) + + filteredPath, err := k0s.filterConfigForK0s(configPath) + Expect(err).NotTo(HaveOccurred()) + Expect(filteredPath).NotTo(BeEmpty()) + defer func() { _ = os.Remove(filteredPath) }() + + // Read and verify filtered content + data, err := os.ReadFile(filteredPath) + Expect(err).NotTo(HaveOccurred()) + content := string(data) + + Expect(content).To(ContainSubstring("apiVersion")) + Expect(content).To(ContainSubstring("kind")) + Expect(content).To(ContainSubstring("metadata")) + Expect(content).To(ContainSubstring("spec")) + Expect(content).NotTo(ContainSubstring("extraField")) + Expect(content).NotTo(ContainSubstring("anotherExtra")) + }) + + It("preserves all expected k0s fields at top level", func() { + configContent := `apiVersion: k0s.k0sproject.io/v1beta1 +kind: ClusterConfig +metadata: + name: test-cluster +spec: + api: + address: 192.168.1.100 +` + configPath := createConfigFile(configContent) + + filteredPath, err := k0s.filterConfigForK0s(configPath) + Expect(err).NotTo(HaveOccurred()) + Expect(filteredPath).NotTo(BeEmpty()) + defer func() { _ = os.Remove(filteredPath) }() + + data, err := os.ReadFile(filteredPath) + Expect(err).NotTo(HaveOccurred()) + content := string(data) + + Expect(content).To(ContainSubstring("apiVersion: k0s.k0sproject.io/v1beta1")) + Expect(content).To(ContainSubstring("kind: ClusterConfig")) + Expect(content).To(ContainSubstring("metadata")) + Expect(content).To(ContainSubstring("spec")) + }) + + It("filters out non-k0s spec fields", func() { + configContent := `apiVersion: k0s.k0sproject.io/v1beta1 +kind: ClusterConfig +spec: + api: + address: 192.168.1.100 + network: + provider: calico + customField: should-be-removed + anotherCustom: also-removed +` + configPath := createConfigFile(configContent) + + filteredPath, err := k0s.filterConfigForK0s(configPath) + Expect(err).NotTo(HaveOccurred()) + Expect(filteredPath).NotTo(BeEmpty()) + defer func() { _ = os.Remove(filteredPath) }() + + data, err := os.ReadFile(filteredPath) + Expect(err).NotTo(HaveOccurred()) + content := string(data) + + Expect(content).To(ContainSubstring("api")) + Expect(content).To(ContainSubstring("network")) + Expect(content).NotTo(ContainSubstring("customField")) + Expect(content).NotTo(ContainSubstring("anotherCustom")) + }) + + It("preserves all expected k0s spec fields", func() { + configContent := `apiVersion: k0s.k0sproject.io/v1beta1 +kind: ClusterConfig +spec: + api: + address: 192.168.1.100 + controllerManager: + extraArgs: + - --cluster-cidr=10.244.0.0/16 + scheduler: + extraArgs: + - --bind-address=0.0.0.0 + extensions: + helm: + repositories: + - name: stable + network: + provider: calico + storage: + type: etcd + telemetry: + enabled: false + images: + default_pull_policy: IfNotPresent + konnectivity: + enabled: true +` + configPath := createConfigFile(configContent) + + filteredPath, err := k0s.filterConfigForK0s(configPath) + Expect(err).NotTo(HaveOccurred()) + Expect(filteredPath).NotTo(BeEmpty()) + defer func() { _ = os.Remove(filteredPath) }() + + data, err := os.ReadFile(filteredPath) + Expect(err).NotTo(HaveOccurred()) + content := string(data) + + // Verify all expected spec fields are preserved + Expect(content).To(ContainSubstring("api")) + Expect(content).To(ContainSubstring("controllerManager")) + Expect(content).To(ContainSubstring("scheduler")) + Expect(content).To(ContainSubstring("extensions")) + Expect(content).To(ContainSubstring("network")) + Expect(content).To(ContainSubstring("storage")) + Expect(content).To(ContainSubstring("telemetry")) + Expect(content).To(ContainSubstring("images")) + Expect(content).To(ContainSubstring("konnectivity")) + }) + + It("handles config with only required fields", func() { + configContent := `apiVersion: k0s.k0sproject.io/v1beta1 +kind: ClusterConfig +spec: + api: + address: 192.168.1.100 +` + configPath := createConfigFile(configContent) + + filteredPath, err := k0s.filterConfigForK0s(configPath) + Expect(err).NotTo(HaveOccurred()) + Expect(filteredPath).NotTo(BeEmpty()) + defer func() { _ = os.Remove(filteredPath) }() + + data, err := os.ReadFile(filteredPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(data)).NotTo(BeEmpty()) + }) + + It("creates a temporary file with .yaml extension", func() { + configContent := `apiVersion: k0s.k0sproject.io/v1beta1 +kind: ClusterConfig +spec: + api: + address: 192.168.1.100 +` + configPath := createConfigFile(configContent) + + filteredPath, err := k0s.filterConfigForK0s(configPath) + Expect(err).NotTo(HaveOccurred()) + Expect(filteredPath).NotTo(BeEmpty()) + defer func() { _ = os.Remove(filteredPath) }() + + Expect(filteredPath).To(HaveSuffix(".yaml")) + Expect(filteredPath).To(ContainSubstring("k0s-config-")) + + // Verify file exists and is readable + _, err = os.Stat(filteredPath) + Expect(err).NotTo(HaveOccurred()) + }) + + It("fails when config file does not exist", func() { + _, err := k0s.filterConfigForK0s("/nonexistent/config.yaml") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to read config")) + }) + + It("fails when config contains invalid YAML", func() { + configPath := createConfigFile("invalid: yaml: content: [") + + _, err := k0s.filterConfigForK0s(configPath) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to parse config")) + }) + + It("handles complex nested structures correctly", func() { + configContent := `apiVersion: k0s.k0sproject.io/v1beta1 +kind: ClusterConfig +metadata: + name: production-cluster +spec: + api: + address: 192.168.1.100 + port: 6443 + sans: + - api.example.com + - 192.168.1.100 + network: + provider: calico + podCIDR: 10.244.0.0/16 + serviceCIDR: 10.96.0.0/12 + extensions: + helm: + repositories: + - name: stable + url: https://charts.helm.sh/stable + charts: + - name: metrics-server + namespace: kube-system +customTopLevel: should-be-filtered +` + configPath := createConfigFile(configContent) + + filteredPath, err := k0s.filterConfigForK0s(configPath) + Expect(err).NotTo(HaveOccurred()) + Expect(filteredPath).NotTo(BeEmpty()) + defer func() { _ = os.Remove(filteredPath) }() + + data, err := os.ReadFile(filteredPath) + Expect(err).NotTo(HaveOccurred()) + content := string(data) + + // Verify nested structures are preserved + Expect(content).To(ContainSubstring("api.example.com")) + Expect(content).To(ContainSubstring("10.244.0.0/16")) + Expect(content).To(ContainSubstring("metrics-server")) + + // Verify custom fields are filtered out + Expect(content).NotTo(ContainSubstring("customTopLevel")) + }) + + It("filters custom fields within spec", func() { + configContent := `apiVersion: k0s.k0sproject.io/v1beta1 +kind: ClusterConfig +spec: + api: + address: 192.168.1.100 + network: + provider: calico + customInSpec: should-be-filtered + anotherCustom: also-filtered +` + configPath := createConfigFile(configContent) + + filteredPath, err := k0s.filterConfigForK0s(configPath) + Expect(err).NotTo(HaveOccurred()) + Expect(filteredPath).NotTo(BeEmpty()) + defer func() { _ = os.Remove(filteredPath) }() + + data, err := os.ReadFile(filteredPath) + Expect(err).NotTo(HaveOccurred()) + content := string(data) + + Expect(content).To(ContainSubstring("api")) + Expect(content).To(ContainSubstring("network")) + Expect(content).NotTo(ContainSubstring("customInSpec")) + Expect(content).NotTo(ContainSubstring("anotherCustom")) + }) + + It("returns valid YAML that can be parsed", func() { + configContent := `apiVersion: k0s.k0sproject.io/v1beta1 +kind: ClusterConfig +spec: + api: + address: 192.168.1.100 + network: + provider: calico +extraField: removed +` + configPath := createConfigFile(configContent) + + filteredPath, err := k0s.filterConfigForK0s(configPath) + Expect(err).NotTo(HaveOccurred()) + Expect(filteredPath).NotTo(BeEmpty()) + defer func() { _ = os.Remove(filteredPath) }() + + // Verify the output is valid YAML by parsing it + data, err := os.ReadFile(filteredPath) + Expect(err).NotTo(HaveOccurred()) + + var result map[string]interface{} + err = yaml.Unmarshal(data, &result) + Expect(err).NotTo(HaveOccurred()) + + // Verify expected structure + Expect(result).To(HaveKey("apiVersion")) + Expect(result).To(HaveKey("kind")) + Expect(result).To(HaveKey("spec")) + Expect(result).NotTo(HaveKey("extraField")) + }) + }) +}) From ef0f96cfe2101d4a9ff535942a5a599bd1f66c4f Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:10:54 +0100 Subject: [PATCH 18/32] test: add tests for Reset functionality in K0s --- internal/installer/k0s.go | 2 ++ internal/installer/k0s_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/internal/installer/k0s.go b/internal/installer/k0s.go index 422268e..f5fd3c3 100644 --- a/internal/installer/k0s.go +++ b/internal/installer/k0s.go @@ -215,6 +215,8 @@ func (k *K0s) filterConfigForK0s(configPath string) (string, error) { return tmpFile.Name(), nil } +// Reset tears down an existing k0s installation by executing `k0s reset`. +// This command removes all k0s-related resources func (k *K0s) Reset(k0sPath string) error { if !k.FileWriter.Exists(k0sPath) { return nil diff --git a/internal/installer/k0s_test.go b/internal/installer/k0s_test.go index 83b9109..ba863a5 100644 --- a/internal/installer/k0s_test.go +++ b/internal/installer/k0s_test.go @@ -321,4 +321,32 @@ var _ = Describe("K0s", func() { }) }) }) + + Describe("Reset", func() { + BeforeEach(func() { + k0sImpl.Goos = "linux" + k0sImpl.Goarch = "amd64" + }) + + Context("when k0s binary does not exist", func() { + It("should return nil without attempting reset", func() { + mockFileWriter.EXPECT().Exists(k0sPath).Return(false) + + err := k0s.Reset(k0sPath) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("platform validation", func() { + It("should work regardless of platform for reset", func() { + k0sImpl.Goos = "darwin" + k0sImpl.Goarch = "arm64" + + mockFileWriter.EXPECT().Exists(k0sPath).Return(false) + + err := k0s.Reset(k0sPath) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) }) From 4b49bd603f7b9d3aef38d5c439d84e1ac607319b Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:30:16 +0100 Subject: [PATCH 19/32] test: add tests for SSH key file handling in InstallK0sRemote method --- cli/cmd/install_k0s_test.go | 102 ++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/cli/cmd/install_k0s_test.go b/cli/cmd/install_k0s_test.go index e82d03c..cbb70c8 100644 --- a/cli/cmd/install_k0s_test.go +++ b/cli/cmd/install_k0s_test.go @@ -268,5 +268,107 @@ var _ = Describe("InstallK0sCmd", func() { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) }) + + It("fails when SSH key file does not exist", func() { + c.Opts.RemoteHost = "192.168.1.50" + c.Opts.SSHKeyPath = "/nonexistent/ssh/key" + + mockFileWriter.EXPECT().ReadFile("/nonexistent/ssh/key").Return(nil, os.ErrNotExist).Maybe() + + err := c.InstallK0sRemote(config, "/path/to/k0s", "/path/to/config") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) + }) + + It("fails when SSH key file is invalid", func() { + c.Opts.RemoteHost = "192.168.1.50" + c.Opts.SSHKeyPath = "/path/to/invalid/key" + + mockFileWriter.EXPECT().ReadFile("/path/to/invalid/key").Return([]byte("not-a-valid-key"), nil).Maybe() + + err := c.InstallK0sRemote(config, "/path/to/k0s", "/path/to/config") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) + }) + + It("uses correct remote host IP for node configuration", func() { + c.Opts.RemoteHost = "10.0.0.50" + c.Opts.SSHKeyPath = "/path/to/key" + + mockFileWriter.EXPECT().ReadFile("/path/to/key").Return([]byte("ssh-key-data"), nil).Maybe() + + err := c.InstallK0sRemote(config, "/path/to/k0s", "/path/to/config") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) + }) + + It("passes correct paths to InstallK0s", func() { + c.Opts.RemoteHost = "192.168.1.60" + c.Opts.SSHKeyPath = "/custom/ssh/key" + c.Opts.Force = true + + mockFileWriter.EXPECT().ReadFile("/custom/ssh/key").Return([]byte("ssh-key-data"), nil).Maybe() + + err := c.InstallK0sRemote(config, "/custom/k0s/path", "/custom/config/path") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) + }) + + It("respects the force flag", func() { + c.Opts.RemoteHost = "192.168.1.70" + c.Opts.SSHKeyPath = "/path/to/key" + c.Opts.Force = true + + mockFileWriter.EXPECT().ReadFile("/path/to/key").Return([]byte("ssh-key-data"), nil).Maybe() + + err := c.InstallK0sRemote(config, "/path/to/k0s", "/path/to/config") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) + }) + + It("uses remote user from options", func() { + c.Opts.RemoteHost = "192.168.1.80" + c.Opts.SSHKeyPath = "/path/to/key" + c.Opts.RemoteUser = "ubuntu" + + mockFileWriter.EXPECT().ReadFile("/path/to/key").Return([]byte("ssh-key-data"), nil).Maybe() + + err := c.InstallK0sRemote(config, "/path/to/k0s", "/path/to/config") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) + }) + + It("handles empty remote host", func() { + c.Opts.RemoteHost = "" + c.Opts.SSHKeyPath = "/path/to/key" + + mockFileWriter.EXPECT().ReadFile("/path/to/key").Return([]byte("ssh-key-data"), nil).Maybe() + + err := c.InstallK0sRemote(config, "/path/to/k0s", "/path/to/config") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) + }) + + It("handles timeout during SSH connection", func() { + c.Opts.RemoteHost = "192.0.2.1" // TEST-NET-1 address + c.Opts.SSHKeyPath = "/path/to/key" + + mockFileWriter.EXPECT().ReadFile("/path/to/key").Return([]byte("-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----"), nil).Maybe() + + err := c.InstallK0sRemote(config, "/path/to/k0s", "/path/to/config") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) + }) + + It("wraps errors from InstallK0s with context", func() { + c.Opts.RemoteHost = "10.0.0.100" + c.Opts.SSHKeyPath = "/path/to/key" + + mockFileWriter.EXPECT().ReadFile("/path/to/key").Return([]byte("ssh-key-data"), nil).Maybe() + + err := c.InstallK0sRemote(config, "/path/to/k0s", "/path/to/config") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) + }) }) }) From e3a9e188aad04f5210fa76d2d2b80ae2e3aefa31 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:06:24 +0100 Subject: [PATCH 20/32] fix: fix k0s installation path and config handling --- cli/cmd/install_k0s.go | 16 ++++++++++------ hack/lima-oms.yaml | 2 +- internal/installer/k0s.go | 11 +++++++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index b5b0993..fb50ea2 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -118,13 +118,17 @@ func (c *InstallK0sCmd) InstallK0sFromInstallConfig(pm installer.PackageManager, return fmt.Errorf("failed to marshal k0s config: %w", err) } - tmpK0sConfigPath := filepath.Join(os.TempDir(), "k0s-config.yaml") - if err := os.WriteFile(tmpK0sConfigPath, k0sConfigData, 0644); err != nil { + k0sConfigPath := "/etc/k0s/k0s.yaml" + + if err := os.MkdirAll(filepath.Dir(k0sConfigPath), 0755); err != nil { + return fmt.Errorf("failed to create k0s config directory: %w", err) + } + + if err := os.WriteFile(k0sConfigPath, k0sConfigData, 0644); err != nil { return fmt.Errorf("failed to write k0s config: %w", err) } - defer func() { _ = os.Remove(tmpK0sConfigPath) }() - log.Printf("Generated k0s configuration at %s", tmpK0sConfigPath) + log.Printf("Generated k0s configuration at %s", k0sConfigPath) k0sPath := pm.GetDependencyPath(defaultK0sPath) if c.Opts.Package == "" { @@ -135,10 +139,10 @@ func (c *InstallK0sCmd) InstallK0sFromInstallConfig(pm installer.PackageManager, } if c.Opts.RemoteHost != "" { - return c.InstallK0sRemote(config, k0sPath, tmpK0sConfigPath) + return c.InstallK0sRemote(config, k0sPath, k0sConfigPath) } - err = k0s.Install(tmpK0sConfigPath, k0sPath, c.Opts.Force) + err = k0s.Install(k0sConfigPath, k0sPath, c.Opts.Force) if err != nil { return fmt.Errorf("failed to install k0s: %w", err) } diff --git a/hack/lima-oms.yaml b/hack/lima-oms.yaml index 631b796..f744fd8 100644 --- a/hack/lima-oms.yaml +++ b/hack/lima-oms.yaml @@ -94,7 +94,7 @@ message: | ------ limactl shell lima-oms cd oms - ./oms-cli install k0s --k0s-config config.yaml --version v1.30.0+k0s.0 --force + ./oms-cli install k0s --install-config config.yaml --version v1.30.0+k0s.0 --force ------ To install Codesphere (run inside lima): diff --git a/internal/installer/k0s.go b/internal/installer/k0s.go index f5fd3c3..24e9003 100644 --- a/internal/installer/k0s.go +++ b/internal/installer/k0s.go @@ -124,8 +124,15 @@ func (k *K0s) Install(configPath string, k0sPath string, force bool) error { if err != nil { log.Printf("Warning: failed to filter config, using original: %v", err) } else { - configPath = filteredConfigPath - defer func() { _ = os.Remove(filteredConfigPath) }() // Clean up temp file after use + filteredData, err := os.ReadFile(filteredConfigPath) + if err != nil { + log.Printf("Warning: failed to read filtered config: %v", err) + } else { + if err := os.WriteFile(configPath, filteredData, 0644); err != nil { + log.Printf("Warning: failed to write filtered config back: %v", err) + } + } + _ = os.Remove(filteredConfigPath) } args = append(args, "--config", configPath) } else { From e2192fe74c9e570fef7b3a84138c6a7aa014245e Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:24:01 +0100 Subject: [PATCH 21/32] fix: k0s config handling to support temporary paths in tests --- cli/cmd/install_k0s.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index fb50ea2..8a6a329 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -118,18 +118,31 @@ func (c *InstallK0sCmd) InstallK0sFromInstallConfig(pm installer.PackageManager, return fmt.Errorf("failed to marshal k0s config: %w", err) } + // Use /etc/k0s/k0s.yaml for production, but allow using temp directory in tests k0sConfigPath := "/etc/k0s/k0s.yaml" + usedTempPath := false if err := os.MkdirAll(filepath.Dir(k0sConfigPath), 0755); err != nil { - return fmt.Errorf("failed to create k0s config directory: %w", err) + // If we can't write to /etc/k0s (e.g., in tests without root), use temp directory + tmpK0sConfigPath := filepath.Join(os.TempDir(), "k0s-config.yaml") + if err := os.WriteFile(tmpK0sConfigPath, k0sConfigData, 0644); err != nil { + return fmt.Errorf("failed to write k0s config: %w", err) + } + k0sConfigPath = tmpK0sConfigPath + usedTempPath = true + log.Printf("Generated k0s configuration at %s (using temp path due to permissions)", k0sConfigPath) + } else { + if err := os.WriteFile(k0sConfigPath, k0sConfigData, 0644); err != nil { + return fmt.Errorf("failed to write k0s config: %w", err) + } + log.Printf("Generated k0s configuration at %s", k0sConfigPath) } - if err := os.WriteFile(k0sConfigPath, k0sConfigData, 0644); err != nil { - return fmt.Errorf("failed to write k0s config: %w", err) + // Clean up temp file if used (only for testing scenarios) + if usedTempPath { + defer func() { _ = os.Remove(k0sConfigPath) }() } - log.Printf("Generated k0s configuration at %s", k0sConfigPath) - k0sPath := pm.GetDependencyPath(defaultK0sPath) if c.Opts.Package == "" { k0sPath, err = k0s.Download(c.Opts.Version, c.Opts.Force, false) From bb39c0e4b2a0321b85d57414b15857350f35078f Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:17:18 +0100 Subject: [PATCH 22/32] feat: enhance remote k0s installation with user support and SSH instructions --- cli/cmd/install_k0s.go | 1 + hack/lima-oms.yaml | 18 +++++++++ internal/installer/node/node.go | 69 ++++++++++++++++++++++++--------- 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index 8a6a329..f50a912 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -176,6 +176,7 @@ func (c *InstallK0sCmd) InstallK0sRemote(config *files.RootConfig, k0sBinaryPath ExternalIP: c.Opts.RemoteHost, InternalIP: c.Opts.RemoteHost, Name: "k0s-node", + User: c.Opts.RemoteUser, } if err := remoteNode.InstallK0s(nm, k0sBinaryPath, k0sConfigPath, c.Opts.Force); err != nil { diff --git a/hack/lima-oms.yaml b/hack/lima-oms.yaml index f744fd8..8297ad1 100644 --- a/hack/lima-oms.yaml +++ b/hack/lima-oms.yaml @@ -97,6 +97,24 @@ message: | ./oms-cli install k0s --install-config config.yaml --version v1.30.0+k0s.0 --force ------ + To test remote k0s installation (run inside lima): + ------ + limactl shell lima-oms + cd oms + # Setup SSH for testing + ssh-keygen -t rsa -b 4096 -f ~/.ssh/test_key -N "" + cat ~/.ssh/test_key.pub >> ~/.ssh/authorized_keys + chmod 600 ~/.ssh/authorized_keys + # Add host to known_hosts + VM_IP=$(hostname -I | awk '{print $1}') + ssh-keyscan $VM_IP >> ~/.ssh/known_hosts + # Test SSH connection + ssh -i ~/.ssh/test_key $VM_IP "echo 'SSH works'" + # Test remote installation (unit tests validate this feature) + ./oms-cli install k0s --install-config config.yaml --version v1.30.0+k0s.0 \ + --remote-host $VM_IP --remote-user $(whoami) --ssh-key-path ~/.ssh/test_key --force + ------ + To install Codesphere (run inside lima): ------ limactl shell lima-oms diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index 355dc63..051fe54 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -25,6 +25,7 @@ type Node struct { Name string `json:"name"` ExternalIP string `json:"external_ip"` InternalIP string `json:"internal_ip"` + User string `json:"user,omitempty"` } type NodeManager struct { @@ -37,6 +38,12 @@ func shellEscape(s string) string { } func (n *NodeManager) getHostKeyCallback() (ssh.HostKeyCallback, error) { + // Required for testing/development via environment variable + if os.Getenv("OMS_SSH_INSECURE") == "true" { + fmt.Println("Warning: Using insecure host key checking (OMS_SSH_INSECURE=true)") + return ssh.InsecureIgnoreHostKey(), nil + } + homeDir, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("failed to get user home directory: %w", err) @@ -75,18 +82,23 @@ func (n *NodeManager) getAuthMethods() ([]ssh.AuthMethod, error) { } if n.KeyPath != "" { - fmt.Println("Falling back to private key file authentication.") + fmt.Printf("Falling back to private key file authentication (key: %s).\n", n.KeyPath) key, err := n.FileIO.ReadFile(n.KeyPath) if err != nil { return nil, fmt.Errorf("failed to read private key file %s: %v", n.KeyPath, err) } + fmt.Printf("Successfully read %d bytes from key file\n", len(key)) + signer, err := ssh.ParsePrivateKey(key) if err == nil { + fmt.Printf("Successfully parsed private key (type: %s)\n", signer.PublicKey().Type()) authMethods = append(authMethods, ssh.PublicKeys(signer)) return authMethods, nil } + + fmt.Printf("Failed to parse private key: %v\n", err) if _, ok := err.(*ssh.PassphraseMissingError); ok { fmt.Printf("Enter passphrase for key '%s': ", n.KeyPath) passphraseBytes, err := term.ReadPassword(int(syscall.Stdin)) @@ -323,11 +335,16 @@ func (n *Node) InstallOms(nm *NodeManager) error { } func (n *Node) CopyFile(nm *NodeManager, src string, dst string) error { - err := nm.EnsureDirectoryExists("", n.ExternalIP, "root", filepath.Dir(dst)) + user := n.User + if user == "" { + user = "root" + } + + err := nm.EnsureDirectoryExists("", n.ExternalIP, user, filepath.Dir(dst)) if err != nil { return fmt.Errorf("failed to ensure directory exists: %w", err) } - return nm.CopyFile("", n.ExternalIP, "root", src, dst) + return nm.CopyFile("", n.ExternalIP, user, src, dst) } func (n *Node) HasAcceptEnvConfigured(jumpbox *Node, nm *NodeManager) bool { @@ -395,24 +412,40 @@ func (n *Node) InstallK0s(nm *NodeManager, k0sBinaryPath string, k0sConfigPath s remoteK0sBinary := filepath.Join(remoteK0sDir, "k0s") remoteConfigPath := "/etc/k0s/k0s.yaml" - log.Printf("Copying k0s binary to %s:%s", n.ExternalIP, remoteK0sBinary) - if err := n.CopyFile(nm, k0sBinaryPath, remoteK0sBinary); err != nil { - return fmt.Errorf("failed to copy k0s binary: %w", err) + user := n.User + if user == "" { + user = "root" + } + + // Copy k0s binary to temp location first, then move with sudo + tmpK0sBinary := "/tmp/k0s" + log.Printf("Copying k0s binary to %s:%s", n.ExternalIP, tmpK0sBinary) + if err := nm.CopyFile("", n.ExternalIP, user, k0sBinaryPath, tmpK0sBinary); err != nil { + return fmt.Errorf("failed to copy k0s binary to temp: %w", err) } - log.Printf("Making k0s binary executable on %s", n.ExternalIP) - chmodCmd := fmt.Sprintf("chmod +x '%s'", shellEscape(remoteK0sBinary)) - if err := nm.RunSSHCommand("", n.ExternalIP, "root", chmodCmd); err != nil { - return fmt.Errorf("failed to make k0s binary executable: %w", err) + // Move to final location and make executable with sudo + log.Printf("Moving k0s binary to %s", remoteK0sBinary) + moveCmd := fmt.Sprintf("sudo mv '%s' '%s' && sudo chmod +x '%s'", + shellEscape(tmpK0sBinary), shellEscape(remoteK0sBinary), shellEscape(remoteK0sBinary)) + if err := nm.RunSSHCommand("", n.ExternalIP, user, moveCmd); err != nil { + return fmt.Errorf("failed to move and chmod k0s binary: %w", err) } if k0sConfigPath != "" { - log.Printf("Copying k0s config to %s:%s", n.ExternalIP, remoteConfigPath) - if err := nm.EnsureDirectoryExists("", n.ExternalIP, "root", "/etc/k0s"); err != nil { - return fmt.Errorf("failed to create /etc/k0s directory: %w", err) + // Copy config to temp location first + tmpConfigPath := "/tmp/k0s-config.yaml" + log.Printf("Copying k0s config to %s", tmpConfigPath) + if err := nm.CopyFile("", n.ExternalIP, user, k0sConfigPath, tmpConfigPath); err != nil { + return fmt.Errorf("failed to copy k0s config to temp: %w", err) } - if err := nm.CopyFile("", n.ExternalIP, "root", k0sConfigPath, remoteConfigPath); err != nil { - return fmt.Errorf("failed to copy k0s config: %w", err) + + // Create /etc/k0s directory and move config with sudo + log.Printf("Moving k0s config to %s", remoteConfigPath) + setupConfigCmd := fmt.Sprintf("sudo mkdir -p /etc/k0s && sudo mv '%s' '%s' && sudo chmod 644 '%s'", + shellEscape(tmpConfigPath), shellEscape(remoteConfigPath), shellEscape(remoteConfigPath)) + if err := nm.RunSSHCommand("", n.ExternalIP, user, setupConfigCmd); err != nil { + return fmt.Errorf("failed to setup k0s config: %w", err) } } @@ -427,13 +460,13 @@ func (n *Node) InstallK0s(nm *NodeManager, k0sBinaryPath string, k0sConfigPath s } log.Printf("Installing k0s on %s", n.ExternalIP) - if err := nm.RunSSHCommand("", n.ExternalIP, "root", installCmd); err != nil { + if err := nm.RunSSHCommand("", n.ExternalIP, user, installCmd); err != nil { return fmt.Errorf("failed to install k0s: %w", err) } log.Printf("k0s successfully installed on %s", n.ExternalIP) - log.Printf("You can start it using: ssh root@%s 'sudo %s start'", n.ExternalIP, shellEscape(remoteK0sBinary)) - log.Printf("You can check the status using: ssh root@%s 'sudo %s status'", n.ExternalIP, shellEscape(remoteK0sBinary)) + log.Printf("You can start it using: ssh %s@%s 'sudo %s start'", user, n.ExternalIP, shellEscape(remoteK0sBinary)) + log.Printf("You can check the status using: ssh %s@%s 'sudo %s status'", user, n.ExternalIP, shellEscape(remoteK0sBinary)) return nil } From fdcb21444bc534cb3c6eca90dd79778663bc1fc8 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Wed, 17 Dec 2025 16:33:28 +0100 Subject: [PATCH 23/32] ref: minor --- cli/cmd/install_k0s.go | 14 +++++--------- cli/cmd/install_k0s_test.go | 14 +++++++------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index f50a912..2938eff 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -45,11 +45,7 @@ func (c *InstallK0sCmd) RunE(_ *cobra.Command, args []string) error { pm := installer.NewPackage(env.GetOmsWorkdir(), c.Opts.Package) k0s := installer.NewK0s(hw, env, c.FileWriter) - if c.Opts.InstallConfig == "" { - return fmt.Errorf("--install-config is required") - } - - return c.InstallK0sFromInstallConfig(pm, k0s) + return c.InstallK0s(pm, k0s) } func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { @@ -86,6 +82,7 @@ func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { k0s.cmd.Flags().StringVar(&k0s.Opts.RemoteUser, "remote-user", "root", "Remote user for SSH connection") k0s.cmd.Flags().BoolVarP(&k0s.Opts.Force, "force", "f", false, "Force new download and installation") + k0s.cmd.MarkFlagRequired("install-config") k0s.cmd.MarkFlagsRequiredTogether("remote-host", "ssh-key-path") install.AddCommand(k0s.cmd) @@ -95,7 +92,7 @@ func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { const defaultK0sPath = "kubernetes/files/k0s" -func (c *InstallK0sCmd) InstallK0sFromInstallConfig(pm installer.PackageManager, k0s installer.K0sManager) error { +func (c *InstallK0sCmd) InstallK0s(pm installer.PackageManager, k0s installer.K0sManager) error { icg := installer.NewInstallConfigManager() if err := icg.LoadInstallConfigFromFile(c.Opts.InstallConfig); err != nil { return fmt.Errorf("failed to load install-config: %w", err) @@ -118,12 +115,11 @@ func (c *InstallK0sCmd) InstallK0sFromInstallConfig(pm installer.PackageManager, return fmt.Errorf("failed to marshal k0s config: %w", err) } - // Use /etc/k0s/k0s.yaml for production, but allow using temp directory in tests + // allow temp directory in tests k0sConfigPath := "/etc/k0s/k0s.yaml" usedTempPath := false if err := os.MkdirAll(filepath.Dir(k0sConfigPath), 0755); err != nil { - // If we can't write to /etc/k0s (e.g., in tests without root), use temp directory tmpK0sConfigPath := filepath.Join(os.TempDir(), "k0s-config.yaml") if err := os.WriteFile(tmpK0sConfigPath, k0sConfigData, 0644); err != nil { return fmt.Errorf("failed to write k0s config: %w", err) @@ -138,7 +134,7 @@ func (c *InstallK0sCmd) InstallK0sFromInstallConfig(pm installer.PackageManager, log.Printf("Generated k0s configuration at %s", k0sConfigPath) } - // Clean up temp file if used (only for testing scenarios) + // Clean up temp file if used if usedTempPath { defer func() { _ = os.Remove(k0sConfigPath) }() } diff --git a/cli/cmd/install_k0s_test.go b/cli/cmd/install_k0s_test.go index cbb70c8..af9dffa 100644 --- a/cli/cmd/install_k0s_test.go +++ b/cli/cmd/install_k0s_test.go @@ -117,7 +117,7 @@ var _ = Describe("InstallK0sCmd", func() { It("fails when install-config file does not exist", func() { c.Opts.InstallConfig = "/nonexistent/install-config.yaml" - err := c.InstallK0sFromInstallConfig(mockPM, mockK0s) + err := c.InstallK0s(mockPM, mockK0s) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to load install-config")) }) @@ -132,7 +132,7 @@ var _ = Describe("InstallK0sCmd", func() { c.Opts.InstallConfig = configPath - err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + err = c.InstallK0s(mockPM, mockK0s) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("external Kubernetes")) }) @@ -152,7 +152,7 @@ var _ = Describe("InstallK0sCmd", func() { mockPM.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/path/k0s") mockK0s.EXPECT().Install(mock.Anything, "/test/path/k0s", true).Return(nil) - err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + err = c.InstallK0s(mockPM, mockK0s) Expect(err).NotTo(HaveOccurred()) }) @@ -172,7 +172,7 @@ var _ = Describe("InstallK0sCmd", func() { mockK0s.EXPECT().Download("v1.29.0+k0s.0", false, false).Return("/downloaded/k0s", nil) mockK0s.EXPECT().Install(mock.Anything, "/downloaded/k0s", false).Return(nil) - err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + err = c.InstallK0s(mockPM, mockK0s) Expect(err).NotTo(HaveOccurred()) }) @@ -190,7 +190,7 @@ var _ = Describe("InstallK0sCmd", func() { mockPM.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/path/k0s") mockK0s.EXPECT().Download("", false, false).Return("", os.ErrNotExist) - err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + err = c.InstallK0s(mockPM, mockK0s) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to download k0s")) }) @@ -209,7 +209,7 @@ var _ = Describe("InstallK0sCmd", func() { mockPM.EXPECT().GetDependencyPath("kubernetes/files/k0s").Return("/test/path/k0s") mockK0s.EXPECT().Install(mock.Anything, "/test/path/k0s", false).Return(os.ErrPermission) - err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + err = c.InstallK0s(mockPM, mockK0s) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to install k0s")) }) @@ -232,7 +232,7 @@ var _ = Describe("InstallK0sCmd", func() { // Remote installation will fail because we can't actually connect, // but we're testing that it attempts remote installation - err = c.InstallK0sFromInstallConfig(mockPM, mockK0s) + err = c.InstallK0s(mockPM, mockK0s) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to install k0s on remote host")) }) From 2f6fa295c718f21df584bfe353399d25224f39f2 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:58:15 +0100 Subject: [PATCH 24/32] fix: error message for missing install-config in RunE method --- cli/cmd/install_k0s_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cmd/install_k0s_test.go b/cli/cmd/install_k0s_test.go index af9dffa..43573d5 100644 --- a/cli/cmd/install_k0s_test.go +++ b/cli/cmd/install_k0s_test.go @@ -58,7 +58,7 @@ var _ = Describe("InstallK0sCmd", func() { err := c.RunE(nil, nil) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("--install-config is required")) + Expect(err.Error()).To(ContainSubstring("install-config")) }) }) From 2ad025147dd7f630142b0a9adc5300e45e7845e9 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:03:54 +0100 Subject: [PATCH 25/32] fix: appease make lint --- cli/cmd/install_k0s.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index 2938eff..4eb7043 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -82,7 +82,7 @@ func AddInstallK0sCmd(install *cobra.Command, opts *GlobalOptions) { k0s.cmd.Flags().StringVar(&k0s.Opts.RemoteUser, "remote-user", "root", "Remote user for SSH connection") k0s.cmd.Flags().BoolVarP(&k0s.Opts.Force, "force", "f", false, "Force new download and installation") - k0s.cmd.MarkFlagRequired("install-config") + _ = k0s.cmd.MarkFlagRequired("install-config") k0s.cmd.MarkFlagsRequiredTogether("remote-host", "ssh-key-path") install.AddCommand(k0s.cmd) From 54ae71c7270441d82d6dff39dbfad2b21ff6d399 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:43:43 +0100 Subject: [PATCH 26/32] Update internal/installer/node/node.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/installer/node/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index 051fe54..8f56eb5 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -228,7 +228,7 @@ func (n *NodeManager) GetClient(jumpboxIp string, ip string, username string) (* } if jumpboxIp != "" { - jbClient, err := n.connectToJumpbox(jumpboxIp, "ubuntu") + jbClient, err := n.connectToJumpbox(jumpboxIp, username) if err != nil { return nil, fmt.Errorf("failed to connect to jumpbox: %v", err) } From a5faa2c5bd89f0c48ab431329331d806373abf69 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:21:23 +0100 Subject: [PATCH 27/32] fix: improve regex for enabling root login in SSH configuration --- internal/installer/node/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index 8f56eb5..9d0887c 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -394,7 +394,7 @@ func (n *Node) RunSSHCommand(jumpbox *Node, nm *NodeManager, username string, co func (n *Node) EnableRootLogin(jumpbox *Node, nm *NodeManager) error { cmds := []string{ - "sudo sed -i 's/^#\\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config", + "sudo sed -i -E 's/^[[:space:]]*(#[[:space:]]*)?PermitRootLogin[[:space:]]+(yes|no|prohibit-password|without-password)[[:space:]]*$/PermitRootLogin yes/' /etc/ssh/sshd_config", "sudo sed -i 's/no-port-forwarding.*$//g' /root/.ssh/authorized_keys", "sudo systemctl restart sshd", } From d1204842fe1be37de36583f48610068d5286a090 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:22:16 +0100 Subject: [PATCH 28/32] Update internal/installer/node/node.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/installer/node/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index 9d0887c..5456968 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -389,7 +389,7 @@ func (n *Node) RunSSHCommand(jumpbox *Node, nm *NodeManager, username string, co return nm.RunSSHCommand("", n.ExternalIP, username, command) } - return nm.RunSSHCommand(jumpbox.ExternalIP, n.InternalIP, "ubuntu", command) + return nm.RunSSHCommand(jumpbox.ExternalIP, n.InternalIP, username, command) } func (n *Node) EnableRootLogin(jumpbox *Node, nm *NodeManager) error { From 8e92d3a4eb61998b143d8a037dba9637efced68a Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:00:24 +0100 Subject: [PATCH 29/32] fix: update SSH installation notes and remove insecure host key warning --- hack/lima-oms.yaml | 3 ++- internal/installer/node/node.go | 6 ------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/hack/lima-oms.yaml b/hack/lima-oms.yaml index 8297ad1..83c11db 100644 --- a/hack/lima-oms.yaml +++ b/hack/lima-oms.yaml @@ -110,7 +110,8 @@ message: | ssh-keyscan $VM_IP >> ~/.ssh/known_hosts # Test SSH connection ssh -i ~/.ssh/test_key $VM_IP "echo 'SSH works'" - # Test remote installation (unit tests validate this feature) + # Note: Remote installation requires proper SSH host key verification + # For testing, you can add the host to known_hosts (already done above with ssh-keyscan) ./oms-cli install k0s --install-config config.yaml --version v1.30.0+k0s.0 \ --remote-host $VM_IP --remote-user $(whoami) --ssh-key-path ~/.ssh/test_key --force ------ diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index 9d0887c..a8dac88 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -38,12 +38,6 @@ func shellEscape(s string) string { } func (n *NodeManager) getHostKeyCallback() (ssh.HostKeyCallback, error) { - // Required for testing/development via environment variable - if os.Getenv("OMS_SSH_INSECURE") == "true" { - fmt.Println("Warning: Using insecure host key checking (OMS_SSH_INSECURE=true)") - return ssh.InsecureIgnoreHostKey(), nil - } - homeDir, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("failed to get user home directory: %w", err) From 6e80a788e15b1ced0df3e73ac59d5554f55135a2 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:42:07 +0100 Subject: [PATCH 30/32] refactor: standardize receiver names in NodeManager methods for consistency --- cli/cmd/install_k0s.go | 9 +-- internal/installer/node/node.go | 103 +++++++++++++++++++------------- 2 files changed, 65 insertions(+), 47 deletions(-) diff --git a/cli/cmd/install_k0s.go b/cli/cmd/install_k0s.go index 4eb7043..c273530 100644 --- a/cli/cmd/install_k0s.go +++ b/cli/cmd/install_k0s.go @@ -117,7 +117,6 @@ func (c *InstallK0sCmd) InstallK0s(pm installer.PackageManager, k0s installer.K0 // allow temp directory in tests k0sConfigPath := "/etc/k0s/k0s.yaml" - usedTempPath := false if err := os.MkdirAll(filepath.Dir(k0sConfigPath), 0755); err != nil { tmpK0sConfigPath := filepath.Join(os.TempDir(), "k0s-config.yaml") @@ -125,7 +124,8 @@ func (c *InstallK0sCmd) InstallK0s(pm installer.PackageManager, k0s installer.K0 return fmt.Errorf("failed to write k0s config: %w", err) } k0sConfigPath = tmpK0sConfigPath - usedTempPath = true + // Clean up temp file on all exit paths + defer func() { _ = os.Remove(k0sConfigPath) }() log.Printf("Generated k0s configuration at %s (using temp path due to permissions)", k0sConfigPath) } else { if err := os.WriteFile(k0sConfigPath, k0sConfigData, 0644); err != nil { @@ -134,11 +134,6 @@ func (c *InstallK0sCmd) InstallK0s(pm installer.PackageManager, k0s installer.K0 log.Printf("Generated k0s configuration at %s", k0sConfigPath) } - // Clean up temp file if used - if usedTempPath { - defer func() { _ = os.Remove(k0sConfigPath) }() - } - k0sPath := pm.GetDependencyPath(defaultK0sPath) if c.Opts.Package == "" { k0sPath, err = k0s.Download(c.Opts.Version, c.Opts.Force, false) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index b382bd0..d5892b6 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -37,7 +37,7 @@ func shellEscape(s string) string { return strings.ReplaceAll(s, "'", "'\\''") } -func (n *NodeManager) getHostKeyCallback() (ssh.HostKeyCallback, error) { +func (nm *NodeManager) getHostKeyCallback() (ssh.HostKeyCallback, error) { homeDir, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("failed to get user home directory: %w", err) @@ -62,7 +62,7 @@ func (n *NodeManager) getHostKeyCallback() (ssh.HostKeyCallback, error) { return hostKeyCallback, nil } -func (n *NodeManager) getAuthMethods() ([]ssh.AuthMethod, error) { +func (nm *NodeManager) getAuthMethods() ([]ssh.AuthMethod, error) { var authMethods []ssh.AuthMethod if authSocket := os.Getenv("SSH_AUTH_SOCK"); authSocket != "" { @@ -75,45 +75,68 @@ func (n *NodeManager) getAuthMethods() ([]ssh.AuthMethod, error) { fmt.Printf("Could not connect to SSH Agent (%s): %v\n", authSocket, err) } - if n.KeyPath != "" { - fmt.Printf("Falling back to private key file authentication (key: %s).\n", n.KeyPath) + if nm.KeyPath != "" { + fmt.Printf("Falling back to private key file authentication (key: %s).\n", nm.KeyPath) - key, err := n.FileIO.ReadFile(n.KeyPath) + key, err := nm.FileIO.ReadFile(nm.KeyPath) if err != nil { - return nil, fmt.Errorf("failed to read private key file %s: %v", n.KeyPath, err) + return nil, fmt.Errorf("failed to read private key file %s: %v", nm.KeyPath, err) } - fmt.Printf("Successfully read %d bytes from key file\n", len(key)) + fmt.Printf("Successfully read %d bytes from key file\\n", len(key)) signer, err := ssh.ParsePrivateKey(key) if err == nil { - fmt.Printf("Successfully parsed private key (type: %s)\n", signer.PublicKey().Type()) + fmt.Printf("Successfully parsed private key (type: %s)\\n", signer.PublicKey().Type()) authMethods = append(authMethods, ssh.PublicKeys(signer)) return authMethods, nil } - fmt.Printf("Failed to parse private key: %v\n", err) + fmt.Printf("Failed to parse private key: %v\\n", err) if _, ok := err.(*ssh.PassphraseMissingError); ok { - fmt.Printf("Enter passphrase for key '%s': ", n.KeyPath) - passphraseBytes, err := term.ReadPassword(int(syscall.Stdin)) - fmt.Println() + // Check if we're in an interactive terminal + if !term.IsTerminal(int(syscall.Stdin)) { + return nil, fmt.Errorf("passphrase-protected key requires interactive terminal. Use ssh-agent or an unencrypted key for automated scenarios") + } + + fmt.Printf("Enter passphrase for key '%s': ", nm.KeyPath) - if err != nil { - return nil, fmt.Errorf("failed to read passphrase: %v", err) + // Read passphrase with a timeout using a channel + type result struct { + password []byte + err error } + resultChan := make(chan result, 1) + go func() { + passphraseBytes, err := term.ReadPassword(int(syscall.Stdin)) + resultChan <- result{password: passphraseBytes, err: err} + }() - defer func() { - for i := range passphraseBytes { - passphraseBytes[i] = 0 + // Wait for passphrase input with 30 second timeout + select { + case res := <-resultChan: + fmt.Println() + if res.err != nil { + return nil, fmt.Errorf("failed to read passphrase: %v", res.err) } - }() - signer, err = ssh.ParsePrivateKeyWithPassphrase(key, passphraseBytes) - if err != nil { - return nil, fmt.Errorf("failed to parse private key with passphrase: %v", err) + defer func() { + for i := range res.password { + res.password[i] = 0 + } + }() + + signer, err = ssh.ParsePrivateKeyWithPassphrase(key, res.password) + if err != nil { + return nil, fmt.Errorf("failed to parse private key with passphrase: %v", err) + } + authMethods = append(authMethods, ssh.PublicKeys(signer)) + return authMethods, nil + + case <-time.After(30 * time.Second): + fmt.Println() + return nil, fmt.Errorf("passphrase input timeout after 30 seconds") } - authMethods = append(authMethods, ssh.PublicKeys(signer)) - return authMethods, nil } return nil, fmt.Errorf("failed to parse private key: %v", err) } @@ -125,13 +148,13 @@ func (n *NodeManager) getAuthMethods() ([]ssh.AuthMethod, error) { return authMethods, nil } -func (n *NodeManager) connectToJumpbox(ip, username string) (*ssh.Client, error) { - authMethods, err := n.getAuthMethods() +func (nm *NodeManager) connectToJumpbox(ip, username string) (*ssh.Client, error) { + authMethods, err := nm.getAuthMethods() if err != nil { return nil, fmt.Errorf("jumpbox authentication setup failed: %v", err) } - hostKeyCallback, err := n.getHostKeyCallback() + hostKeyCallback, err := nm.getHostKeyCallback() if err != nil { return nil, fmt.Errorf("failed to get host key callback: %w", err) } @@ -149,14 +172,14 @@ func (n *NodeManager) connectToJumpbox(ip, username string) (*ssh.Client, error) return nil, fmt.Errorf("failed to dial jumpbox %s: %v", addr, err) } - if err := n.forwardAgent(jumpboxClient, nil); err != nil { + if err := nm.forwardAgent(jumpboxClient, nil); err != nil { fmt.Printf(" Warning: Agent forwarding setup failed on jumpbox: %v\n", err) } return jumpboxClient, nil } -func (n *NodeManager) forwardAgent(client *ssh.Client, session *ssh.Session) error { +func (nm *NodeManager) forwardAgent(client *ssh.Client, session *ssh.Session) error { authSocket := os.Getenv("SSH_AUTH_SOCK") if authSocket == "" { log.Printf("SSH_AUTH_SOCK not set. Cannot perform agent forwarding") @@ -180,8 +203,8 @@ func (n *NodeManager) forwardAgent(client *ssh.Client, session *ssh.Session) err return nil } -func (n *NodeManager) RunSSHCommand(jumpboxIp string, ip string, username string, command string) error { - client, err := n.GetClient(jumpboxIp, ip, username) +func (nm *NodeManager) RunSSHCommand(jumpboxIp string, ip string, username string, command string) error { + client, err := nm.GetClient(jumpboxIp, ip, username) if err != nil { return fmt.Errorf("failed to get client: %w", err) } @@ -192,7 +215,7 @@ func (n *NodeManager) RunSSHCommand(jumpboxIp string, ip string, username string } defer func() { _ = session.Close() }() - if err := n.forwardAgent(client, session); err != nil { + if err := nm.forwardAgent(client, session); err != nil { fmt.Printf(" Warning: Agent forwarding setup failed on session: %v\n", err) } @@ -209,20 +232,20 @@ func (n *NodeManager) RunSSHCommand(jumpboxIp string, ip string, username string return nil } -func (n *NodeManager) GetClient(jumpboxIp string, ip string, username string) (*ssh.Client, error) { +func (nm *NodeManager) GetClient(jumpboxIp string, ip string, username string) (*ssh.Client, error) { - authMethods, err := n.getAuthMethods() + authMethods, err := nm.getAuthMethods() if err != nil { return nil, fmt.Errorf("failed to get authentication methods: %w", err) } - hostKeyCallback, err := n.getHostKeyCallback() + hostKeyCallback, err := nm.getHostKeyCallback() if err != nil { return nil, fmt.Errorf("failed to get host key callback: %w", err) } if jumpboxIp != "" { - jbClient, err := n.connectToJumpbox(jumpboxIp, username) + jbClient, err := nm.connectToJumpbox(jumpboxIp, username) if err != nil { return nil, fmt.Errorf("failed to connect to jumpbox: %v", err) } @@ -262,8 +285,8 @@ func (n *NodeManager) GetClient(jumpboxIp string, ip string, username string) (* return client, nil } -func (n *NodeManager) GetSFTPClient(jumpboxIp string, ip string, username string) (*sftp.Client, error) { - client, err := n.GetClient(jumpboxIp, ip, username) +func (nm *NodeManager) GetSFTPClient(jumpboxIp string, ip string, username string) (*sftp.Client, error) { + client, err := nm.GetClient(jumpboxIp, ip, username) if err != nil { return nil, fmt.Errorf("failed to get SSH client: %v", err) } @@ -279,14 +302,14 @@ func (nm *NodeManager) EnsureDirectoryExists(jumpboxIp string, ip string, userna return nm.RunSSHCommand(jumpboxIp, ip, username, cmd) } -func (n *NodeManager) CopyFile(jumpboxIp string, ip string, username string, src string, dst string) error { - client, err := n.GetSFTPClient(jumpboxIp, ip, username) +func (nm *NodeManager) CopyFile(jumpboxIp string, ip string, username string, src string, dst string) error { + client, err := nm.GetSFTPClient(jumpboxIp, ip, username) if err != nil { return fmt.Errorf("failed to get SSH client: %v", err) } defer func() { _ = client.Close() }() - srcFile, err := n.FileIO.Open(src) + srcFile, err := nm.FileIO.Open(src) if err != nil { return fmt.Errorf("failed to open source file %s: %v", src, err) } From 921e47c745fd74f0dcdb9909afa9d5a079e59659 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:28:51 +0100 Subject: [PATCH 31/32] fix: enhance k0s reset process by stopping service before reset and update error message in tests --- internal/installer/k0s.go | 6 +++ internal/installer/node/node.go | 62 ---------------------------- internal/installer/node/node_test.go | 2 +- 3 files changed, 7 insertions(+), 63 deletions(-) diff --git a/internal/installer/k0s.go b/internal/installer/k0s.go index 24e9003..17b8792 100644 --- a/internal/installer/k0s.go +++ b/internal/installer/k0s.go @@ -230,6 +230,12 @@ func (k *K0s) Reset(k0sPath string) error { } log.Println("Resetting existing k0s installation...") + + log.Println("Stopping k0s service if running...") + if err := util.RunCommand("sudo", []string{k0sPath, "stop"}, ""); err != nil { + log.Printf("Note: k0s stop returned error, try to continue the reset: %v", err) + } + err := util.RunCommand("sudo", []string{k0sPath, "reset"}, "") if err != nil { return fmt.Errorf("failed to reset k0s: %w", err) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index d5892b6..b4b11d0 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -335,22 +335,6 @@ func (n *Node) HasCommand(nm *NodeManager, command string) bool { return err == nil } -func (n *Node) InstallOms(nm *NodeManager) error { - remoteCommands := []string{ - "wget -qO- 'https://api.github.com/repos/codesphere-cloud/oms/releases/latest' | jq -r '.assets[] | select(.name | match(\"oms-cli.*linux_amd64\")) | .browser_download_url' | xargs wget -O oms-cli", - "chmod +x oms-cli; sudo mv oms-cli /usr/local/bin/", - "curl -LO https://github.com/getsops/sops/releases/download/v3.11.0/sops-v3.11.0.linux.amd64; sudo mv sops-v3.11.0.linux.amd64 /usr/local/bin/sops; sudo chmod +x /usr/local/bin/sops", - "wget https://dl.filippo.io/age/latest?for=linux/amd64 -O age.tar.gz; tar -xvf age.tar.gz; sudo mv age/age* /usr/local/bin/", - } - for _, cmd := range remoteCommands { - err := nm.RunSSHCommand("", n.ExternalIP, "root", cmd) - if err != nil { - return fmt.Errorf("failed to run remote command '%s': %w", cmd, err) - } - } - return nil -} - func (n *Node) CopyFile(nm *NodeManager, src string, dst string) error { user := n.User if user == "" { @@ -364,37 +348,6 @@ func (n *Node) CopyFile(nm *NodeManager, src string, dst string) error { return nm.CopyFile("", n.ExternalIP, user, src, dst) } -func (n *Node) HasAcceptEnvConfigured(jumpbox *Node, nm *NodeManager) bool { - checkCommand := "sudo grep -E '^AcceptEnv OMS_PORTAL_API_KEY' /etc/ssh/sshd_config >/dev/null 2>&1" - err := n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommand) - return err == nil -} - -func (n *Node) ConfigureAcceptEnv(jumpbox *Node, nm *NodeManager) error { - cmds := []string{ - "sudo sed -i 's/^#\\?AcceptEnv.*/AcceptEnv OMS_PORTAL_API_KEY/' /etc/ssh/sshd_config", - "sudo systemctl restart sshd", - } - for _, cmd := range cmds { - err := n.RunSSHCommand(jumpbox, nm, "ubuntu", cmd) - if err != nil { - return fmt.Errorf("failed to run command '%s': %w", cmd, err) - } - } - return nil -} - -func (n *Node) HasRootLoginEnabled(jumpbox *Node, nm *NodeManager) bool { - checkCommandPermit := "sudo grep -E '^PermitRootLogin yes' /etc/ssh/sshd_config >/dev/null 2>&1" - err := n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommandPermit) - if err != nil { - return false - } - checkCommandAuthorizedKeys := "sudo grep -E '^no-port-forwarding' /root/.ssh/authorized_keys >/dev/null 2>&1" - err = n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommandAuthorizedKeys) - return err != nil -} - func (n *Node) HasFile(jumpbox *Node, nm *NodeManager, filePath string) bool { checkCommand := fmt.Sprintf("test -f '%s'", shellEscape(filePath)) err := n.RunSSHCommand(jumpbox, nm, "ubuntu", checkCommand) @@ -409,21 +362,6 @@ func (n *Node) RunSSHCommand(jumpbox *Node, nm *NodeManager, username string, co return nm.RunSSHCommand(jumpbox.ExternalIP, n.InternalIP, username, command) } -func (n *Node) EnableRootLogin(jumpbox *Node, nm *NodeManager) error { - cmds := []string{ - "sudo sed -i -E 's/^[[:space:]]*(#[[:space:]]*)?PermitRootLogin[[:space:]]+(yes|no|prohibit-password|without-password)[[:space:]]*$/PermitRootLogin yes/' /etc/ssh/sshd_config", - "sudo sed -i 's/no-port-forwarding.*$//g' /root/.ssh/authorized_keys", - "sudo systemctl restart sshd", - } - for _, cmd := range cmds { - err := n.RunSSHCommand(jumpbox, nm, "ubuntu", cmd) - if err != nil { - return fmt.Errorf("failed to run command '%s': %w", cmd, err) - } - } - return nil -} - func (n *Node) InstallK0s(nm *NodeManager, k0sBinaryPath string, k0sConfigPath string, force bool) error { remoteK0sDir := "/usr/local/bin" remoteK0sBinary := filepath.Join(remoteK0sDir, "k0s") diff --git a/internal/installer/node/node_test.go b/internal/installer/node/node_test.go index 8c7126f..e59530a 100644 --- a/internal/installer/node/node_test.go +++ b/internal/installer/node/node_test.go @@ -243,7 +243,7 @@ gsUnsokl0FasmM3Ws7VlAAAADnRlc3RAZXhhbXBsZS5jb20BAgMEBQ== err := n.InstallK0s(nm, k0sBinaryPath, "", false) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to ensure directory exists")) + Expect(err.Error()).To(ContainSubstring("failed to copy k0s binary to temp")) }) It("should handle paths with special characters safely", func() { From dfc9debf914907000a3757fd0906a6ae7c36af3c Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:26:26 +0100 Subject: [PATCH 32/32] ref: extract k0s ClusterConfig keys into constants for improved maintainability --- internal/installer/k0s.go | 46 ++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/internal/installer/k0s.go b/internal/installer/k0s.go index 17b8792..9b5627f 100644 --- a/internal/installer/k0s.go +++ b/internal/installer/k0s.go @@ -32,6 +32,31 @@ type K0s struct { Goarch string } +// valid top-level fields in a k0s ClusterConfig. +// Reference: https://docs.k0sproject.io/stable/configuration/ +var K0sConfigTopLevelKeys = []string{ + "apiVersion", + "kind", + "metadata", + "spec", +} + +// valid fields in the spec section of a k0s ClusterConfig. +// Reference: https://docs.k0sproject.io/stable/configuration/ +var K0sConfigSpecKeys = []string{ + "api", + "controllerManager", + "scheduler", + "extensions", + "network", + "storage", + "telemetry", + "images", + "konnectivity", + "installConfig", + "featureGates", +} + func NewK0s(hw portal.Http, env env.Env, fw util.FileIO) K0sManager { return &K0s{ Env: env, @@ -170,11 +195,9 @@ func (k *K0s) filterConfigForK0s(configPath string) (string, error) { return "", fmt.Errorf("failed to parse config: %w", err) } - keysToKeep := map[string]bool{ - "apiVersion": true, - "kind": true, - "metadata": true, - "spec": true, + keysToKeep := make(map[string]bool, len(K0sConfigTopLevelKeys)) + for _, key := range K0sConfigTopLevelKeys { + keysToKeep[key] = true } for key := range config { @@ -184,16 +207,9 @@ func (k *K0s) filterConfigForK0s(configPath string) (string, error) { } if spec, ok := config["spec"].(map[string]interface{}); ok { - specKeysToKeep := map[string]bool{ - "api": true, - "controllerManager": true, - "scheduler": true, - "extensions": true, - "network": true, - "storage": true, - "telemetry": true, - "images": true, - "konnectivity": true, + specKeysToKeep := make(map[string]bool, len(K0sConfigSpecKeys)) + for _, key := range K0sConfigSpecKeys { + specKeysToKeep[key] = true } for key := range spec {