diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ef3bf57cb7..7e9c2d29bd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -912,7 +912,6 @@ jobs: - TestSingleNodeResumeDisasterRecovery - TestMultiNodeHADisasterRecovery - TestSingleNodeInstallationNoopUpgrade - - TestCustomCIDR - TestLocalArtifactMirror - TestMultiNodeReset - TestCollectSupportBundle diff --git a/.github/workflows/release-prod.yaml b/.github/workflows/release-prod.yaml index f58ff0426d..f219a76e84 100644 --- a/.github/workflows/release-prod.yaml +++ b/.github/workflows/release-prod.yaml @@ -527,7 +527,6 @@ jobs: - TestSingleNodeResumeDisasterRecovery - TestMultiNodeHADisasterRecovery - TestSingleNodeInstallationNoopUpgrade - - TestCustomCIDR - TestLocalArtifactMirror - TestMultiNodeReset - TestCollectSupportBundle diff --git a/api/integration/linux/install/infra_test.go b/api/integration/linux/install/infra_test.go index f33a5c20d7..50a02942db 100644 --- a/api/integration/linux/install/infra_test.go +++ b/api/integration/linux/install/infra_test.go @@ -171,7 +171,8 @@ func TestLinuxPostSetupInfra(t *testing.T) { } mock.InOrder( k0sMock.On("IsInstalled").Return(false, nil), - k0sMock.On("WriteK0sConfig", mock.Anything, "eth0", "", "10.244.0.0/16", "10.96.0.0/12", mock.Anything, mock.Anything).Return(k0sConfig, nil), + k0sMock.On("NewK0sConfig", "eth0", false, "10.244.0.0/16", "10.96.0.0/12", mock.Anything, mock.Anything).Return(k0sConfig, nil), + k0sMock.On("WriteK0sConfig", mock.Anything, k0sConfig).Return(nil), hostutilsMock.On("CreateSystemdUnitFiles", mock.Anything, mock.Anything, rc, hostname, false).Return(nil), k0sMock.On("Install", rc, hostname).Return(nil), k0sMock.On("WaitForK0s").Return(nil), @@ -704,7 +705,8 @@ func TestLinuxPostSetupInfra(t *testing.T) { k0sConfig := &k0sv1beta1.ClusterConfig{} mock.InOrder( k0sMock.On("IsInstalled").Return(false, nil), - k0sMock.On("WriteK0sConfig", mock.Anything, "eth0", "", "10.244.0.0/16", "10.96.0.0/12", mock.Anything, mock.Anything).Return(k0sConfig, nil), + k0sMock.On("NewK0sConfig", "eth0", false, "10.244.0.0/16", "10.96.0.0/12", mock.Anything, mock.Anything).Return(k0sConfig, nil), + k0sMock.On("WriteK0sConfig", mock.Anything, k0sConfig).Return(nil), hostutilsMock.On("CreateSystemdUnitFiles", mock.Anything, mock.Anything, rc, hostname, false).Return(nil), k0sMock.On("Install", rc, hostname).Return(errors.New("failed to install k0s")), ) diff --git a/api/internal/managers/linux/infra/install.go b/api/internal/managers/linux/infra/install.go index 3d8f34379c..5d7bd3aced 100644 --- a/api/internal/managers/linux/infra/install.go +++ b/api/internal/managers/linux/infra/install.go @@ -162,8 +162,11 @@ func (m *infraManager) installK0s(ctx context.Context, rc runtimeconfig.RuntimeC logFn := m.logFn("k0s") logFn("creating k0s configuration file") - k0sCfg, err = m.k0scli.WriteK0sConfig(ctx, rc.NetworkInterface(), m.airgapBundle, rc.PodCIDR(), rc.ServiceCIDR(), m.endUserConfig, nil) + k0sCfg, err = m.k0scli.NewK0sConfig(rc.NetworkInterface(), m.airgapBundle != "", rc.PodCIDR(), rc.ServiceCIDR(), m.endUserConfig, nil) if err != nil { + return nil, fmt.Errorf("new k0s config: %w", err) + } + if err := m.k0scli.WriteK0sConfig(ctx, k0sCfg); err != nil { return nil, fmt.Errorf("create config file: %w", err) } diff --git a/cmd/installer/cli/cidr_test.go b/cmd/installer/cli/cidr_test.go deleted file mode 100644 index 2cc36e67fa..0000000000 --- a/cmd/installer/cli/cidr_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package cli - -import ( - "testing" - - "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" - newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/stretchr/testify/require" - "k8s.io/utils/ptr" -) - -func Test_getCIDRConfig(t *testing.T) { - tests := []struct { - name string - setFlags func(flagSet *pflag.FlagSet) - expected *newconfig.CIDRConfig - }{ - { - name: "with pod and service flags", - expected: &newconfig.CIDRConfig{ - PodCIDR: "10.0.0.0/24", - ServiceCIDR: "10.1.0.0/24", - GlobalCIDR: nil, - }, - setFlags: func(flagSet *pflag.FlagSet) { - flagSet.Set("pod-cidr", "10.0.0.0/24") - flagSet.Set("service-cidr", "10.1.0.0/24") - }, - }, - { - name: "with pod flag", - expected: &newconfig.CIDRConfig{ - PodCIDR: "10.0.0.0/24", - ServiceCIDR: v1beta1.DefaultNetwork().ServiceCIDR, - GlobalCIDR: nil, - }, - setFlags: func(flagSet *pflag.FlagSet) { - flagSet.Set("pod-cidr", "10.0.0.0/24") - }, - }, - { - name: "with pod, service and cidr flags", - expected: &newconfig.CIDRConfig{ - PodCIDR: "10.0.0.0/24", - ServiceCIDR: "10.1.0.0/24", - GlobalCIDR: nil, - }, - setFlags: func(flagSet *pflag.FlagSet) { - flagSet.Set("pod-cidr", "10.0.0.0/24") - flagSet.Set("service-cidr", "10.1.0.0/24") - flagSet.Set("cidr", "10.2.0.0/24") - }, - }, - { - name: "with pod and cidr flags", - expected: &newconfig.CIDRConfig{ - PodCIDR: "10.0.0.0/24", - ServiceCIDR: v1beta1.DefaultNetwork().ServiceCIDR, - GlobalCIDR: nil, - }, - setFlags: func(flagSet *pflag.FlagSet) { - flagSet.Set("pod-cidr", "10.0.0.0/24") - flagSet.Set("cidr", "10.2.0.0/24") - }, - }, - { - name: "with cidr flag", - expected: &newconfig.CIDRConfig{ - PodCIDR: "10.2.0.0/25", - ServiceCIDR: "10.2.0.128/25", - GlobalCIDR: ptr.To("10.2.0.0/24"), - }, - setFlags: func(flagSet *pflag.FlagSet) { - flagSet.Set("cidr", "10.2.0.0/24") - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - req := require.New(t) - - cmd := &cobra.Command{} - mustAddCIDRFlags(cmd.Flags()) - - test.setFlags(cmd.Flags()) - - got, err := getCIDRConfig(cmd) - req.NoError(err) - req.Equal(test.expected, got) - }) - } -} diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 5f866144ea..e5da190219 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -8,16 +8,13 @@ import ( "io/fs" "os" "path/filepath" - "slices" "strings" "syscall" "time" "github.com/AlecAivazis/survey/v2/terminal" - "github.com/google/uuid" k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" apitypes "github.com/replicatedhq/embedded-cluster/api/types" - "github.com/replicatedhq/embedded-cluster/cmd/installer/goods" "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/cloudutils" @@ -28,12 +25,10 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg-new/kubernetesinstallation" ecmetadata "github.com/replicatedhq/embedded-cluster/pkg-new/metadata" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" - "github.com/replicatedhq/embedded-cluster/pkg-new/tlsutils" "github.com/replicatedhq/embedded-cluster/pkg/addons" "github.com/replicatedhq/embedded-cluster/pkg/addons/registry" addontypes "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/airgap" - "github.com/replicatedhq/embedded-cluster/pkg/configutils" "github.com/replicatedhq/embedded-cluster/pkg/extensions" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/helpers" @@ -59,13 +54,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -type InstallCmdFlags struct { +type installFlags struct { adminConsolePassword string adminConsolePort int airgapBundle string - airgapMetadata *airgap.AirgapMetadata - embeddedAssetsSize int64 - isAirgap bool licenseFile string assumeYes bool overrides string @@ -78,28 +70,33 @@ type InstallCmdFlags struct { ignoreHostPreflights bool ignoreAppPreflights bool networkInterface string + cidrConfig *newconfig.CIDRConfig + proxySpec *ecv1beta1.ProxySpec // kubernetes flags kubernetesEnvSettings *helmcli.EnvSettings // guided UI flags - enableManagerExperience bool - target string - managerPort int - tlsCertFile string - tlsKeyFile string - hostname string - - installConfig + target string + managerPort int + tlsCertFile string + tlsKeyFile string + hostname string } +// installConfig holds computed/derived values from install flags type installConfig struct { - clusterID string - license *kotsv1beta1.License - licenseBytes []byte - tlsCert tls.Certificate - tlsCertBytes []byte - tlsKeyBytes []byte + clusterID string + isAirgap bool + enableManagerExperience bool + licenseBytes []byte + license *kotsv1beta1.License + airgapMetadata *airgap.AirgapMetadata + embeddedAssetsSize int64 + endUserConfig *ecv1beta1.Config + tlsCert tls.Certificate + tlsCertBytes []byte + tlsKeyBytes []byte } // webAssetsFS is the filesystem to be used by the web component. Defaults to nil allowing the web server to use the default assets embedded in the binary. Useful for testing. @@ -107,7 +104,7 @@ var webAssetsFS fs.FS = nil // InstallCmd returns a cobra command for installing the embedded cluster. func InstallCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { - var flags InstallCmdFlags + var flags installFlags ctx, cancel := context.WithCancel(ctx) @@ -128,21 +125,22 @@ func InstallCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { cancel() // Cancel context when command completes }, RunE: func(cmd *cobra.Command, args []string) error { - if err := preRunInstall(cmd, &flags, rc, ki); err != nil { + installCfg, err := preRunInstall(cmd, &flags, rc, ki) + if err != nil { return err } - if err := verifyAndPrompt(ctx, cmd, appSlug, &flags, prompts.New()); err != nil { + if err := verifyAndPrompt(ctx, cmd, appSlug, &flags, installCfg, prompts.New()); err != nil { return err } metricsReporter := newInstallReporter( replicatedAppURL(), cmd.CalledAs(), flagsToStringSlice(cmd.Flags()), - flags.license.Spec.LicenseID, flags.clusterID, flags.license.Spec.AppSlug, + installCfg.license.Spec.LicenseID, installCfg.clusterID, installCfg.license.Spec.AppSlug, ) metricsReporter.ReportInstallationStarted(ctx) - if flags.enableManagerExperience { - return runManagerExperienceInstall(ctx, flags, rc, ki, metricsReporter.reporter, appTitle) + if installCfg.enableManagerExperience { + return runManagerExperienceInstall(ctx, flags, installCfg, rc, ki, metricsReporter.reporter, appTitle) } _ = rc.SetEnv() @@ -152,7 +150,7 @@ func InstallCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { metricsReporter.ReportSignalAborted(ctx, sig) }) - if err := runInstall(cmd.Context(), flags, rc, metricsReporter); err != nil { + if err := runInstall(cmd.Context(), flags, installCfg, rc, metricsReporter); err != nil { // Check if this is an interrupt error from the terminal if errors.Is(err, terminal.InterruptErr) { metricsReporter.ReportSignalAborted(ctx, syscall.SIGINT) @@ -212,7 +210,7 @@ func installCmdExample(appSlug string) string { return fmt.Sprintf(installCmdExampleText, appSlug, appSlug) } -func mustAddInstallFlags(cmd *cobra.Command, flags *InstallCmdFlags) { +func mustAddInstallFlags(cmd *cobra.Command, flags *installFlags) { enableV3 := isV3Enabled() normalizeFuncs := []func(f *pflag.FlagSet, name string) pflag.NormalizedName{} @@ -246,7 +244,7 @@ func mustAddInstallFlags(cmd *cobra.Command, flags *InstallCmdFlags) { }) } -func newCommonInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet { +func newCommonInstallFlags(flags *installFlags, enableV3 bool) *pflag.FlagSet { flagSet := pflag.NewFlagSet("common", pflag.ContinueOnError) flagSet.StringVar(&flags.target, "target", "", "The target platform to install to. Valid options are 'linux' or 'kubernetes'.") @@ -269,7 +267,7 @@ func newCommonInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet return flagSet } -func newLinuxInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet { +func newLinuxInstallFlags(flags *installFlags, enableV3 bool) *pflag.FlagSet { flagSet := pflag.NewFlagSet("linux", pflag.ContinueOnError) // Use the app slug as default data directory only when ENABLE_V3 is set @@ -302,7 +300,7 @@ func newLinuxInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet return flagSet } -func newKubernetesInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet { +func newKubernetesInstallFlags(flags *installFlags, enableV3 bool) *pflag.FlagSet { flagSet := pflag.NewFlagSet("kubernetes", pflag.ContinueOnError) addKubernetesCLIFlags(flagSet, flags) @@ -317,13 +315,13 @@ func newKubernetesInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.Fla return flagSet } -func addKubernetesCLIFlags(flagSet *pflag.FlagSet, flags *InstallCmdFlags) { +func addKubernetesCLIFlags(flagSet *pflag.FlagSet, flags *installFlags) { s := helmcli.New() helm.AddKubernetesCLIFlags(flagSet, s) flags.kubernetesEnvSettings = s } -func addInstallAdminConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { +func addInstallAdminConsoleFlags(cmd *cobra.Command, flags *installFlags) error { cmd.Flags().StringVar(&flags.adminConsolePassword, "admin-console-password", "", "Password for the Admin Console") cmd.Flags().IntVar(&flags.adminConsolePort, "admin-console-port", ecv1beta1.DefaultAdminConsolePort, "Port on which the Admin Console will be served") cmd.Flags().StringVarP(&flags.licenseFile, "license", "l", "", "Path to the license file") @@ -333,7 +331,7 @@ func addInstallAdminConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) err return nil } -func addTLSFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { +func addTLSFlags(cmd *cobra.Command, flags *installFlags) error { managerName := "Admin Console" if isV3Enabled() { managerName = "Manager" @@ -346,7 +344,7 @@ func addTLSFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { return nil } -func addManagementConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { +func addManagementConsoleFlags(cmd *cobra.Command, flags *installFlags) error { cmd.Flags().IntVar(&flags.managerPort, "manager-port", ecv1beta1.DefaultManagerPort, "Port on which the Manager will be served") // If the ENABLE_V3 environment variable is set, default to the new manager experience and do @@ -360,141 +358,44 @@ func addManagementConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) error return nil } -func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation) error { - if !isV3Enabled() { - flags.target = "linux" - } - - if !slices.Contains([]string{"linux", "kubernetes"}, flags.target) { - return fmt.Errorf(`invalid --target (must be one of: "linux", "kubernetes")`) - } - - flags.clusterID = uuid.New().String() - - if err := preRunInstallCommon(cmd, flags, rc, ki); err != nil { - return err - } - - switch flags.target { - case "linux": - return preRunInstallLinux(cmd, flags, rc) - case "kubernetes": - return preRunInstallKubernetes(cmd, flags, ki) - } - - return nil -} - -func preRunInstallCommon(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation) error { - flags.enableManagerExperience = isV3Enabled() - - // license file can be empty for restore - if flags.licenseFile != "" { - b, err := os.ReadFile(flags.licenseFile) - if err != nil { - return fmt.Errorf("failed to read license file: %w", err) - } - flags.licenseBytes = b - - // validate the the license is indeed a license file - l, err := helpers.ParseLicense(flags.licenseFile) - if err != nil { - if err == helpers.ErrNotALicenseFile { - return fmt.Errorf("license file is not a valid license file") - } - - return fmt.Errorf("failed to parse license file: %w", err) - } - flags.license = l - } - - if flags.configValues != "" { - err := configutils.ValidateKotsConfigValues(flags.configValues) - if err != nil { - return fmt.Errorf("config values file is not valid: %w", err) - } - } - - flags.isAirgap = flags.airgapBundle != "" - if flags.airgapBundle != "" { - metadata, err := airgap.AirgapMetadataFromPath(flags.airgapBundle) - if err != nil { - return fmt.Errorf("failed to get airgap info: %w", err) - } - flags.airgapMetadata = metadata - } - - var err error - flags.embeddedAssetsSize, err = goods.SizeOfEmbeddedAssets() - if err != nil { - return fmt.Errorf("failed to get size of embedded files: %w", err) - } - - if flags.managerPort != 0 && flags.adminConsolePort != 0 { - if flags.managerPort == flags.adminConsolePort { - return fmt.Errorf("manager port cannot be the same as admin console port") - } +func preRunInstall(cmd *cobra.Command, flags *installFlags, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation) (*installConfig, error) { + // Hydrate flags + if err := buildInstallFlags(cmd, flags); err != nil { + return nil, err } - proxy, err := proxyConfigFromCmd(cmd, flags.assumeYes) + // Build installCfg config + installCfg, err := buildInstallConfig(flags) if err != nil { - return err + return nil, err } + // Set runtime config values from flags rc.SetAdminConsolePort(flags.adminConsolePort) ki.SetAdminConsolePort(flags.adminConsolePort) rc.SetManagerPort(flags.managerPort) ki.SetManagerPort(flags.managerPort) - rc.SetProxySpec(proxy) - ki.SetProxySpec(proxy) - - // Process TLS certificate configuration if provided - if err := processTLSConfig(flags); err != nil { - return fmt.Errorf("process TLS configuration: %w", err) - } + rc.SetProxySpec(flags.proxySpec) + ki.SetProxySpec(flags.proxySpec) - return nil -} - -// processTLSConfig validates and processes TLS certificate configuration for both traditional and manager experience flows -func processTLSConfig(flags *InstallCmdFlags) error { - // If both cert and key are provided, validate and load them - if flags.tlsCertFile != "" && flags.tlsKeyFile != "" { - cert, err := tls.LoadX509KeyPair(flags.tlsCertFile, flags.tlsKeyFile) - if err != nil { - return fmt.Errorf("load tls certificate: %w", err) - } - certData, err := os.ReadFile(flags.tlsCertFile) - if err != nil { - return fmt.Errorf("failed to read tls cert file: %w", err) + // Target-specific configuration + switch flags.target { + case "linux": + if err := preRunInstallLinux(flags, installCfg, rc); err != nil { + return nil, err } - keyData, err := os.ReadFile(flags.tlsKeyFile) - if err != nil { - return fmt.Errorf("failed to read tls key file: %w", err) + case "kubernetes": + if err := preRunInstallKubernetes(flags, ki); err != nil { + return nil, err } - flags.tlsCert = cert - flags.tlsCertBytes = certData - flags.tlsKeyBytes = keyData - - return nil - } - - // If only one of cert or key is provided, return an error - if flags.tlsCertFile != "" || flags.tlsKeyFile != "" { - return fmt.Errorf("both --tls-cert and --tls-key must be provided together") } - // If neither is provided, no TLS configuration (will use default behavior) - return nil + return installCfg, nil } -func preRunInstallLinux(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { - if !cmd.Flags().Changed("skip-host-preflights") && (os.Getenv("SKIP_HOST_PREFLIGHTS") == "1" || os.Getenv("SKIP_HOST_PREFLIGHTS") == "true") { - flags.skipHostPreflights = true - } - +func preRunInstallLinux(flags *installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) error { if os.Getuid() != 0 { return fmt.Errorf("install command must be run as root") } @@ -509,38 +410,14 @@ func preRunInstallLinux(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeco } logrus.Debugf("using host CA bundle: %s", hostCABundlePath) - // if a network interface flag was not provided, attempt to discover it - if flags.networkInterface == "" { - autoInterface, err := newconfig.DetermineBestNetworkInterface() - if err == nil { - flags.networkInterface = autoInterface - } - } - - if flags.localArtifactMirrorPort != 0 && flags.adminConsolePort != 0 { - if flags.localArtifactMirrorPort == flags.adminConsolePort { - return fmt.Errorf("local artifact mirror port cannot be the same as admin console port") - } - } - - eucfg, err := helpers.ParseEndUserConfig(flags.overrides) - if err != nil { - return fmt.Errorf("process overrides file: %w", err) - } - - cidrCfg, err := cidrConfigFromCmd(cmd) - if err != nil { - return err - } - - k0sCfg, err := k0s.NewK0sConfig(flags.networkInterface, flags.isAirgap, cidrCfg.PodCIDR, cidrCfg.ServiceCIDR, eucfg, nil) + k0sCfg, err := k0sConfigFromFlags(flags, installCfg) if err != nil { return fmt.Errorf("failed to create k0s config: %w", err) } networkSpec := helpers.NetworkSpecFromK0sConfig(k0sCfg) networkSpec.NetworkInterface = flags.networkInterface - if cidrCfg.GlobalCIDR != nil { - networkSpec.GlobalCIDR = *cidrCfg.GlobalCIDR + if flags.cidrConfig.GlobalCIDR != nil { + networkSpec.GlobalCIDR = *flags.cidrConfig.GlobalCIDR } // TODO: validate that a single port isn't used for multiple services @@ -557,7 +434,7 @@ func preRunInstallLinux(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeco return nil } -func preRunInstallKubernetes(_ *cobra.Command, flags *InstallCmdFlags, ki kubernetesinstallation.Installation) error { +func preRunInstallKubernetes(flags *installFlags, ki kubernetesinstallation.Installation) error { // TODO: we only support amd64 clusters for target=kubernetes installs helpers.SetClusterArch("amd64") @@ -590,86 +467,15 @@ func preRunInstallKubernetes(_ *cobra.Command, flags *InstallCmdFlags, ki kubern return nil } -func proxyConfigFromCmd(cmd *cobra.Command, assumeYes bool) (*ecv1beta1.ProxySpec, error) { - proxy, err := parseProxyFlags(cmd) - if err != nil { - return nil, err - } - - if err := verifyProxyConfig(proxy, prompts.New(), assumeYes); err != nil { - return nil, err - } - - return proxy, nil -} - -func cidrConfigFromCmd(cmd *cobra.Command) (*newconfig.CIDRConfig, error) { - if err := validateCIDRFlags(cmd); err != nil { - return nil, err - } - - // parse the various cidr flags to make sure we have exactly what we want - cidrCfg, err := getCIDRConfig(cmd) - if err != nil { - return nil, fmt.Errorf("failed to determine pod and service CIDRs: %w", err) - } - - return cidrCfg, nil -} - func runManagerExperienceInstall( - ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation, + ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation, metricsReporter metrics.ReporterInterface, appTitle string, ) (finalErr error) { - kotsadmNamespace, err := runtimeconfig.KotsadmNamespace(ctx, nil) - if err != nil { - return fmt.Errorf("get kotsadm namespace: %w", err) - } - - // this is necessary because the api listens on all interfaces, - // and we only know the interface to use when the user selects it in the ui - ipAddresses, err := netutils.ListAllValidIPAddresses() - if err != nil { - return fmt.Errorf("failed to list all valid IP addresses: %w", err) - } - passwordHash, err := bcrypt.GenerateFromPassword([]byte(flags.adminConsolePassword), 10) if err != nil { return fmt.Errorf("failed to generate password hash: %w", err) } - // For manager experience, generate self-signed cert if none provided, with user confirmation - if flags.tlsCertFile == "" || flags.tlsKeyFile == "" { - logrus.Warn("\nNo certificate files provided. A self-signed certificate will be used, and your browser will show a security warning.") - logrus.Info("To use your own certificate, provide both --tls-key and --tls-cert flags.") - - if !flags.assumeYes { - logrus.Info("") // newline so the prompt is separated from the warning - confirmed, err := prompts.New().Confirm("Do you want to continue with a self-signed certificate?", false) - if err != nil { - return fmt.Errorf("failed to get confirmation: %w", err) - } - if !confirmed { - logrus.Infof("\nInstallation cancelled. Please run the command again with the --tls-key and --tls-cert flags.\n") - return nil - } - } - - // Generate self-signed certificate - cert, certData, keyData, err := tlsutils.GenerateCertificate(flags.hostname, ipAddresses, kotsadmNamespace) - if err != nil { - return fmt.Errorf("generate tls certificate: %w", err) - } - flags.tlsCert = cert - flags.tlsCertBytes = certData - flags.tlsKeyBytes = keyData - } - - eucfg, err := helpers.ParseEndUserConfig(flags.overrides) - if err != nil { - return fmt.Errorf("process overrides file: %w", err) - } - var configValues apitypes.AppConfigValues if flags.configValues != "" { kotsConfigValues, err := helpers.ParseConfigValues(flags.configValues) @@ -685,18 +491,18 @@ func runManagerExperienceInstall( Password: flags.adminConsolePassword, PasswordHash: passwordHash, TLSConfig: apitypes.TLSConfig{ - CertBytes: flags.tlsCertBytes, - KeyBytes: flags.tlsKeyBytes, + CertBytes: installCfg.tlsCertBytes, + KeyBytes: installCfg.tlsKeyBytes, Hostname: flags.hostname, }, - License: flags.licenseBytes, + License: installCfg.licenseBytes, AirgapBundle: flags.airgapBundle, - AirgapMetadata: flags.airgapMetadata, - EmbeddedAssetsSize: flags.embeddedAssetsSize, + AirgapMetadata: installCfg.airgapMetadata, + EmbeddedAssetsSize: installCfg.embeddedAssetsSize, ConfigValues: configValues, ReleaseData: release.GetReleaseData(), - EndUserConfig: eucfg, - ClusterID: flags.clusterID, + EndUserConfig: installCfg.endUserConfig, + ClusterID: installCfg.clusterID, Mode: apitypes.ModeInstall, RequiresInfraUpgrade: false, // Always false for install @@ -717,7 +523,7 @@ func runManagerExperienceInstall( ctx, cancel := context.WithCancel(ctx) defer cancel() - if err := startAPI(ctx, flags.tlsCert, apiConfig, cancel); err != nil { + if err := startAPI(ctx, installCfg.tlsCert, apiConfig, cancel); err != nil { return fmt.Errorf("failed to start api: %w", err) } @@ -729,25 +535,25 @@ func runManagerExperienceInstall( return nil } -func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, metricsReporter *installReporter) (finalErr error) { - if flags.enableManagerExperience { +func runInstall(ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, metricsReporter *installReporter) (finalErr error) { + if installCfg.enableManagerExperience { return nil } logrus.Debug("initializing install") - if err := initializeInstall(ctx, flags, rc); err != nil { + if err := initializeInstall(ctx, flags, installCfg, rc); err != nil { return fmt.Errorf("failed to initialize install: %w", err) } logrus.Debugf("running install preflights") - if err := runInstallPreflights(ctx, flags, rc, metricsReporter.reporter); err != nil { + if err := runInstallPreflights(ctx, flags, installCfg, rc, metricsReporter.reporter); err != nil { if errors.Is(err, preflights.ErrPreflightsHaveFail) { return NewErrorNothingElseToAdd(err) } return fmt.Errorf("failed to run install preflights: %w", err) } - if _, err := installAndStartCluster(ctx, flags, rc, nil); err != nil { + if _, err := installAndStartCluster(ctx, flags, installCfg, rc, nil); err != nil { return fmt.Errorf("failed to install cluster: %w", err) } @@ -764,7 +570,7 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run errCh := kubeutils.WaitForKubernetes(ctx, kcli) defer logKubernetesErrors(errCh) - in, err := recordInstallation(ctx, kcli, flags, rc) + in, err := recordInstallation(ctx, kcli, flags, installCfg, rc) if err != nil { return fmt.Errorf("failed to record installation: %w", err) } @@ -785,7 +591,7 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run } airgapChartsPath := "" - if flags.isAirgap { + if installCfg.isAirgap { airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } @@ -801,7 +607,7 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run defer hcli.Close() logrus.Debugf("installing addons") - if err := installAddons(ctx, kcli, mcli, hcli, flags, rc); err != nil { + if err := installAddons(ctx, kcli, mcli, hcli, flags, installCfg, rc); err != nil { return err } @@ -820,24 +626,40 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run isHeadlessInstall := flags.configValues != "" && flags.adminConsolePassword != "" - printSuccessMessage(flags.license, flags.hostname, flags.networkInterface, rc, isHeadlessInstall) + printSuccessMessage(installCfg.license, flags.hostname, flags.networkInterface, rc, isHeadlessInstall) return nil } -func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, loading **spinner.MessageWriter) (*addons.InstallOptions, error) { +func k0sConfigFromFlags(flags *installFlags, installCfg *installConfig) (*k0sv1beta1.ClusterConfig, error) { + return k0s.NewK0sConfig(flags.networkInterface, installCfg.isAirgap, flags.cidrConfig.PodCIDR, flags.cidrConfig.ServiceCIDR, installCfg.endUserConfig, nil) +} + +// kotsInstallOptionsFromFlags builds kotscli.InstallOptions from CLI flags and install config. +// This function is extracted to enable integration testing. +// Returns InstallOptions with ConfigValuesFile set to the file path (not parsed values). +func kotsInstallOptionsFromFlags(flags installFlags, installCfg *installConfig, kotsadmNamespace string) kotscli.InstallOptions { + return kotscli.InstallOptions{ + AppSlug: installCfg.license.Spec.AppSlug, + License: installCfg.licenseBytes, + Namespace: kotsadmNamespace, + ClusterID: installCfg.clusterID, + AirgapBundle: flags.airgapBundle, + ConfigValuesFile: flags.configValues, + ReplicatedAppEndpoint: replicatedAppURL(), + SkipPreflights: flags.ignoreAppPreflights, + } +} + +func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, loading **spinner.MessageWriter) (*addons.InstallOptions, error) { var embCfgSpec *ecv1beta1.ConfigSpec if embCfg := release.GetEmbeddedClusterConfig(); embCfg != nil { embCfgSpec = &embCfg.Spec } - euCfg, err := helpers.ParseEndUserConfig(flags.overrides) - if err != nil { - return nil, fmt.Errorf("failed to process overrides file: %w", err) - } var euCfgSpec *ecv1beta1.ConfigSpec - if euCfg != nil { - euCfgSpec = &euCfg.Spec + if installCfg.endUserConfig != nil { + euCfgSpec = &installCfg.endUserConfig.Spec } kotsadmNamespace, err := runtimeconfig.KotsadmNamespace(ctx, kcli) @@ -846,16 +668,16 @@ func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags InstallC } opts := &addons.InstallOptions{ - ClusterID: flags.clusterID, + ClusterID: installCfg.clusterID, AdminConsolePwd: flags.adminConsolePassword, AdminConsolePort: rc.AdminConsolePort(), - License: flags.license, + License: installCfg.license, IsAirgap: flags.airgapBundle != "", - TLSCertBytes: flags.tlsCertBytes, - TLSKeyBytes: flags.tlsKeyBytes, + TLSCertBytes: installCfg.tlsCertBytes, + TLSKeyBytes: installCfg.tlsKeyBytes, Hostname: flags.hostname, - DisasterRecoveryEnabled: flags.license.Spec.IsDisasterRecoverySupported, - IsMultiNodeEnabled: flags.license.Spec.IsEmbeddedClusterMultiNodeEnabled, + DisasterRecoveryEnabled: installCfg.license.Spec.IsDisasterRecoverySupported, + IsMultiNodeEnabled: installCfg.license.Spec.IsEmbeddedClusterMultiNodeEnabled, EmbeddedConfigSpec: embCfgSpec, EndUserConfigSpec: euCfgSpec, ProxySpec: rc.ProxySpec(), @@ -866,31 +688,22 @@ func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags InstallC OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), ServiceCIDR: rc.ServiceCIDR(), KotsInstaller: func() error { - opts := kotscli.InstallOptions{ - AppSlug: flags.license.Spec.AppSlug, - License: flags.licenseBytes, - Namespace: kotsadmNamespace, - ClusterID: flags.clusterID, - AirgapBundle: flags.airgapBundle, - ConfigValuesFile: flags.configValues, - ReplicatedAppEndpoint: replicatedAppURL(), - SkipPreflights: flags.ignoreAppPreflights, - Stdout: *loading, - } + opts := kotsInstallOptionsFromFlags(flags, installCfg, kotsadmNamespace) + opts.Stdout = *loading return kotscli.Install(opts) }, } return opts, nil } -func verifyAndPrompt(ctx context.Context, cmd *cobra.Command, appSlug string, flags *InstallCmdFlags, prompt prompts.Prompt) error { +func verifyAndPrompt(ctx context.Context, cmd *cobra.Command, appSlug string, flags *installFlags, installCfg *installConfig, prompt prompts.Prompt) error { logrus.Debugf("checking if k0s is already installed") err := verifyNoInstallation(appSlug, "reinstall") if err != nil { return err } - err = verifyChannelRelease("installation", flags.isAirgap, flags.assumeYes) + err = verifyChannelRelease("installation", installCfg.isAirgap, flags.assumeYes) if err != nil { return err } @@ -900,14 +713,14 @@ func verifyAndPrompt(ctx context.Context, cmd *cobra.Command, appSlug string, fl if err != nil { return err } - if flags.airgapMetadata != nil && flags.airgapMetadata.AirgapInfo != nil { + if installCfg.airgapMetadata != nil && installCfg.airgapMetadata.AirgapInfo != nil { logrus.Debugf("checking airgap bundle matches binary") - if err := checkAirgapMatches(flags.airgapMetadata.AirgapInfo); err != nil { + if err := checkAirgapMatches(installCfg.airgapMetadata.AirgapInfo); err != nil { return err // we want the user to see the error message without a prefix } } - if !flags.isAirgap { + if !installCfg.isAirgap { if err := maybePromptForAppUpdate(ctx, prompt, license, flags.assumeYes); err != nil { if errors.As(err, &ErrorNothingElseToAdd{}) { return err @@ -922,8 +735,14 @@ func verifyAndPrompt(ctx context.Context, cmd *cobra.Command, appSlug string, fl return err } + // TODO (@salah): figure out how we can move this to buildInstallFlags without changing product behavior + if err := verifyProxyConfig(flags.proxySpec, prompts.New(), flags.assumeYes); err != nil { + return err + } + // restore command doesn't have a password flag if cmd.Flags().Lookup("admin-console-password") != nil { + // TODO (@salah): figure out how we can move this to buildInstallFlags without changing product behavior if err := ensureAdminConsolePassword(flags); err != nil { return err } @@ -932,7 +751,7 @@ func verifyAndPrompt(ctx context.Context, cmd *cobra.Command, appSlug string, fl return nil } -func ensureAdminConsolePassword(flags *InstallCmdFlags) error { +func ensureAdminConsolePassword(flags *installFlags) error { if flags.adminConsolePassword == "" { // no password was provided if flags.assumeYes { @@ -1079,18 +898,13 @@ func verifyNoInstallation(appSlug string, cmdName string) error { return nil } -func initializeInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { +func initializeInstall(ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) error { logrus.Info("") spinner := spinner.Start() spinner.Infof("Initializing") - licenseBytes, err := os.ReadFile(flags.licenseFile) - if err != nil { - return fmt.Errorf("failed to read license file: %w", err) - } - if err := hostutils.ConfigureHost(ctx, rc, hostutils.InitForInstallOptions{ - License: licenseBytes, + License: installCfg.licenseBytes, AirgapBundle: flags.airgapBundle, }); err != nil { spinner.ErrorClosef("Initialization failed") @@ -1101,7 +915,7 @@ func initializeInstall(ctx context.Context, flags InstallCmdFlags, rc runtimecon return nil } -func installAndStartCluster(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { +func installAndStartCluster(ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { loading := spinner.Start() loading.Infof("Installing node") @@ -1114,12 +928,12 @@ func installAndStartCluster(ctx context.Context, flags InstallCmdFlags, rc runti logrus.Debugf("creating k0s configuration file") - eucfg, err := helpers.ParseEndUserConfig(flags.overrides) + cfg, err := k0sConfigFromFlags(&flags, installCfg) if err != nil { - return nil, fmt.Errorf("process overrides file: %w", err) + return nil, fmt.Errorf("unable to create k0s config: %w", err) } - cfg, err := k0s.WriteK0sConfig(ctx, flags.networkInterface, flags.airgapBundle, rc.PodCIDR(), rc.ServiceCIDR(), eucfg, mutate) + err = k0s.WriteK0sConfig(ctx, cfg) if err != nil { loading.ErrorClosef("Failed to install node") return nil, fmt.Errorf("create config file: %w", err) @@ -1154,7 +968,7 @@ func installAndStartCluster(ctx context.Context, flags InstallCmdFlags, rc runti return cfg, nil } -func installAddons(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { +func installAddons(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) error { progressChan := make(chan addontypes.AddOnProgress) defer close(progressChan) @@ -1182,7 +996,7 @@ func installAddons(ctx context.Context, kcli client.Client, mcli metadata.Interf addons.WithProgressChannel(progressChan), ) - opts, err := getAddonInstallOpts(ctx, kcli, flags, rc, &loading) + opts, err := getAddonInstallOpts(ctx, kcli, flags, installCfg, rc, &loading) if err != nil { return fmt.Errorf("get addon install opts: %w", err) } @@ -1375,7 +1189,7 @@ func waitForNode(ctx context.Context) error { } func recordInstallation( - ctx context.Context, kcli client.Client, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, + ctx context.Context, kcli client.Client, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, ) (*ecv1beta1.Installation, error) { // get the embedded cluster config cfg := release.GetEmbeddedClusterConfig() @@ -1384,27 +1198,21 @@ func recordInstallation( cfgspec = &cfg.Spec } - // parse the end user config - eucfg, err := helpers.ParseEndUserConfig(flags.overrides) - if err != nil { - return nil, fmt.Errorf("process overrides file: %w", err) - } - // extract airgap uncompressed size if airgap info is provided var airgapUncompressedSize int64 - if flags.airgapMetadata != nil && flags.airgapMetadata.AirgapInfo != nil { - airgapUncompressedSize = flags.airgapMetadata.AirgapInfo.Spec.UncompressedSize + if installCfg.airgapMetadata != nil && installCfg.airgapMetadata.AirgapInfo != nil { + airgapUncompressedSize = installCfg.airgapMetadata.AirgapInfo.Spec.UncompressedSize } // record the installation installation, err := kubeutils.RecordInstallation(ctx, kcli, kubeutils.RecordInstallationOptions{ - ClusterID: flags.clusterID, - IsAirgap: flags.isAirgap, - License: flags.license, + ClusterID: installCfg.clusterID, + IsAirgap: installCfg.isAirgap, + License: installCfg.license, ConfigSpec: cfgspec, MetricsBaseURL: replicatedAppURL(), RuntimeConfig: rc.Get(), - EndUserConfig: eucfg, + EndUserConfig: installCfg.endUserConfig, AirgapUncompressedSize: airgapUncompressedSize, }) if err != nil { diff --git a/cmd/installer/cli/install_config.go b/cmd/installer/cli/install_config.go new file mode 100644 index 0000000000..06ea71f244 --- /dev/null +++ b/cmd/installer/cli/install_config.go @@ -0,0 +1,222 @@ +package cli + +import ( + "context" + "crypto/tls" + "fmt" + "os" + + "github.com/google/uuid" + "github.com/replicatedhq/embedded-cluster/cmd/installer/goods" + newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" + "github.com/replicatedhq/embedded-cluster/pkg-new/tlsutils" + "github.com/replicatedhq/embedded-cluster/pkg/airgap" + "github.com/replicatedhq/embedded-cluster/pkg/configutils" + "github.com/replicatedhq/embedded-cluster/pkg/helpers" + "github.com/replicatedhq/embedded-cluster/pkg/netutils" + "github.com/replicatedhq/embedded-cluster/pkg/prompts" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// buildInstallFlags maps cobra command flags to install flags +func buildInstallFlags(cmd *cobra.Command, flags *installFlags) error { + // Target defaulting (if not V3) + if !isV3Enabled() { + flags.target = "linux" + } + + // Target validation + if flags.target != "linux" && flags.target != "kubernetes" { + return fmt.Errorf(`invalid --target (must be one of: "linux", "kubernetes")`) + } + + // If only one of cert or key is provided, return an error + if (flags.tlsCertFile != "" && flags.tlsKeyFile == "") || (flags.tlsCertFile == "" && flags.tlsKeyFile != "") { + return fmt.Errorf("both --tls-cert and --tls-key must be provided together") + } + + // Skip host preflights from env var (if flag not explicitly set) + if !cmd.Flags().Changed("skip-host-preflights") { + if os.Getenv("SKIP_HOST_PREFLIGHTS") == "1" || os.Getenv("SKIP_HOST_PREFLIGHTS") == "true" { + flags.skipHostPreflights = true + } + } + + // Network interface auto-detection (if not provided) + if flags.networkInterface == "" && flags.target == "linux" { + autoInterface, err := newconfig.DetermineBestNetworkInterface() + if err == nil { + flags.networkInterface = autoInterface + } + // If error, leave empty and validation will catch it later + } + + // Port conflict validations + if flags.managerPort != 0 && flags.adminConsolePort != 0 { + if flags.managerPort == flags.adminConsolePort { + return fmt.Errorf("manager port cannot be the same as admin console port") + } + } + + if flags.localArtifactMirrorPort != 0 && flags.adminConsolePort != 0 { + if flags.localArtifactMirrorPort == flags.adminConsolePort { + return fmt.Errorf("local artifact mirror port cannot be the same as admin console port") + } + } + + // CIDR configuration + cidrCfg, err := cidrConfigFromCmd(cmd) + if err != nil { + return err + } + flags.cidrConfig = cidrCfg + + // Proxy configuration + proxy, err := parseProxyFlags(cmd, flags.networkInterface, flags.cidrConfig) + if err != nil { + return err + } + flags.proxySpec = proxy + + return nil +} + +// buildInstallConfig builds the install config from install flags +func buildInstallConfig(flags *installFlags) (*installConfig, error) { + installCfg := &installConfig{ + clusterID: uuid.New().String(), + enableManagerExperience: isV3Enabled(), + } + + // License file reading + if flags.licenseFile != "" { + b, err := os.ReadFile(flags.licenseFile) + if err != nil { + return nil, fmt.Errorf("failed to read license file: %w", err) + } + installCfg.licenseBytes = b + + l, err := helpers.ParseLicense(flags.licenseFile) + if err != nil { + return nil, fmt.Errorf("failed to parse license file: %w", err) + } + installCfg.license = l + } + + // Config values validation + if flags.configValues != "" { + err := configutils.ValidateKotsConfigValues(flags.configValues) + if err != nil { + return nil, fmt.Errorf("config values file is not valid: %w", err) + } + } + + // Airgap detection and metadata + installCfg.isAirgap = flags.airgapBundle != "" + if flags.airgapBundle != "" { + metadata, err := airgap.AirgapMetadataFromPath(flags.airgapBundle) + if err != nil { + return nil, fmt.Errorf("failed to get airgap info: %w", err) + } + installCfg.airgapMetadata = metadata + } + + // Embedded assets size + size, err := goods.SizeOfEmbeddedAssets() + if err != nil { + return nil, fmt.Errorf("failed to get size of embedded files: %w", err) + } + installCfg.embeddedAssetsSize = size + + // End user config (overrides file) + eucfg, err := helpers.ParseEndUserConfig(flags.overrides) + if err != nil { + return nil, fmt.Errorf("process overrides file: %w", err) + } + installCfg.endUserConfig = eucfg + + // TLS Certificate Processing + if err := processTLSConfig(flags, installCfg); err != nil { + return nil, fmt.Errorf("process TLS config: %w", err) + } + + return installCfg, nil +} + +func cidrConfigFromCmd(cmd *cobra.Command) (*newconfig.CIDRConfig, error) { + if err := validateCIDRFlags(cmd); err != nil { + return nil, err + } + + // parse the various cidr flags to make sure we have exactly what we want + cidrCfg, err := getCIDRConfig(cmd) + if err != nil { + return nil, fmt.Errorf("failed to determine pod and service CIDRs: %w", err) + } + + return cidrCfg, nil +} + +func processTLSConfig(flags *installFlags, installCfg *installConfig) error { + // If both cert and key are provided, load them + if flags.tlsCertFile != "" && flags.tlsKeyFile != "" { + certBytes, err := os.ReadFile(flags.tlsCertFile) + if err != nil { + return fmt.Errorf("failed to read TLS certificate: %w", err) + } + keyBytes, err := os.ReadFile(flags.tlsKeyFile) + if err != nil { + return fmt.Errorf("failed to read TLS key: %w", err) + } + + cert, err := tls.X509KeyPair(certBytes, keyBytes) + if err != nil { + return fmt.Errorf("failed to parse TLS certificate: %w", err) + } + + installCfg.tlsCert = cert + installCfg.tlsCertBytes = certBytes + installCfg.tlsKeyBytes = keyBytes + } else if installCfg.enableManagerExperience { + // For manager experience, generate self-signed certificate if none provided + logrus.Warn("\nNo certificate files provided. A self-signed certificate will be used, and your browser will show a security warning.") + logrus.Info("To use your own certificate, provide both --tls-key and --tls-cert flags.") + + if !flags.assumeYes { + logrus.Info("") // newline so the prompt is separated from the warning + confirmed, err := prompts.New().Confirm("Do you want to continue with a self-signed certificate?", false) + if err != nil { + return fmt.Errorf("failed to get confirmation: %w", err) + } + if !confirmed { + logrus.Infof("\nInstallation cancelled. Please run the command again with the --tls-key and --tls-cert flags.\n") + return fmt.Errorf("installation cancelled by user") + } + } + + // Get all IP addresses for the certificate + ipAddresses, err := netutils.ListAllValidIPAddresses() + if err != nil { + return fmt.Errorf("failed to list all valid IP addresses: %w", err) + } + + // Determine the namespace for the certificate + kotsadmNamespace, err := runtimeconfig.KotsadmNamespace(context.Background(), nil) + if err != nil { + return fmt.Errorf("get kotsadm namespace: %w", err) + } + + // Generate self-signed certificate + cert, certData, keyData, err := tlsutils.GenerateCertificate(flags.hostname, ipAddresses, kotsadmNamespace) + if err != nil { + return fmt.Errorf("generate tls certificate: %w", err) + } + installCfg.tlsCert = cert + installCfg.tlsCertBytes = certData + installCfg.tlsKeyBytes = keyData + } + + return nil +} diff --git a/cmd/installer/cli/install_config_test.go b/cmd/installer/cli/install_config_test.go new file mode 100644 index 0000000000..631429d7d9 --- /dev/null +++ b/cmd/installer/cli/install_config_test.go @@ -0,0 +1,447 @@ +package cli + +import ( + "net" + "testing" + + "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "k8s.io/utils/ptr" +) + +// Mock network interface for testing +type mockNetworkLookup struct{} + +func (m *mockNetworkLookup) FirstValidIPNet(networkInterface string) (*net.IPNet, error) { + _, ipnet, _ := net.ParseCIDR("192.168.1.0/24") + return ipnet, nil +} + +// Helper function to create bool pointer +func boolPtr(b bool) *bool { + return &b +} + +func Test_buildInstallFlags_ProxyConfig(t *testing.T) { + tests := []struct { + name string + init func(t *testing.T, flagSet *pflag.FlagSet) + want *ecv1beta1.ProxySpec + }{ + { + name: "no flags set and no env vars should not set proxy", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + // No env vars, no flags + }, + want: nil, + }, + { + name: "lowercase env vars should be used when no flags set", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + t.Setenv("http_proxy", "http://lower-proxy") + t.Setenv("https_proxy", "https://lower-proxy") + t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://lower-proxy", + HTTPSProxy: "https://lower-proxy", + ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "uppercase env vars should be used when no flags set and no lowercase vars", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + t.Setenv("HTTP_PROXY", "http://upper-proxy") + t.Setenv("HTTPS_PROXY", "https://upper-proxy") + t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://upper-proxy", + HTTPSProxy: "https://upper-proxy", + ProvidedNoProxy: "upper-no-proxy-1,upper-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,upper-no-proxy-1,upper-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "lowercase should take precedence over uppercase", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + t.Setenv("http_proxy", "http://lower-proxy") + t.Setenv("https_proxy", "https://lower-proxy") + t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") + t.Setenv("HTTP_PROXY", "http://upper-proxy") + t.Setenv("HTTPS_PROXY", "https://upper-proxy") + t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://lower-proxy", + HTTPSProxy: "https://lower-proxy", + ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "proxy flags should override env vars", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + t.Setenv("http_proxy", "http://lower-proxy") + t.Setenv("https_proxy", "https://lower-proxy") + t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") + t.Setenv("HTTP_PROXY", "http://upper-proxy") + t.Setenv("HTTPS_PROXY", "https://upper-proxy") + t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") + + flagSet.Set("http-proxy", "http://flag-proxy") + flagSet.Set("https-proxy", "https://flag-proxy") + flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://flag-proxy", + HTTPSProxy: "https://flag-proxy", + ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "pod and service CIDR should override default no proxy", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("http-proxy", "http://flag-proxy") + flagSet.Set("https-proxy", "https://flag-proxy") + flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") + + flagSet.Set("pod-cidr", "1.1.1.1/24") + flagSet.Set("service-cidr", "2.2.2.2/24") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://flag-proxy", + HTTPSProxy: "https://flag-proxy", + ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,1.1.1.1/24,2.2.2.2/24,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "custom --cidr should be present in the no-proxy", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("http-proxy", "http://flag-proxy") + flagSet.Set("https-proxy", "https://flag-proxy") + flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") + + flagSet.Set("cidr", "10.0.0.0/16") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://flag-proxy", + HTTPSProxy: "https://flag-proxy", + ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.0.0.0/17,10.0.128.0/17,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "partial env vars with partial flag vars", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + t.Setenv("http_proxy", "http://lower-proxy") + // No https_proxy set + t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") + + // Only set https-proxy flag + flagSet.Set("https-proxy", "https://flag-proxy") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://lower-proxy", + HTTPSProxy: "https://flag-proxy", + ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup flags struct + flags := &installFlags{ + networkInterface: "eth0", // Skip network interface auto-detection + } + + // Setup cobra command with flags + cmd := &cobra.Command{} + mustAddCIDRFlags(cmd.Flags()) + mustAddProxyFlags(cmd.Flags()) + + flagSet := cmd.Flags() + if tt.init != nil { + tt.init(t, flagSet) + } + + // Override the network lookup with our mock + defaultNetworkLookupImpl = &mockNetworkLookup{} + + err := buildInstallFlags(cmd, flags) + assert.NoError(t, err, "unexpected error") + assert.Equal(t, tt.want, flags.proxySpec) + }) + } +} + +func Test_buildInstallFlags_SkipHostPreflightsEnvVar(t *testing.T) { + tests := []struct { + name string + envVarValue string + flagValue *bool // nil means not set, true/false means explicitly set + expectedSkipPreflights bool + }{ + { + name: "env var set to 1, no flag", + envVarValue: "1", + flagValue: nil, + expectedSkipPreflights: true, + }, + { + name: "env var set to true, no flag", + envVarValue: "true", + flagValue: nil, + expectedSkipPreflights: true, + }, + { + name: "env var set, flag explicitly false (flag takes precedence)", + envVarValue: "1", + flagValue: boolPtr(false), + expectedSkipPreflights: false, + }, + { + name: "env var set, flag explicitly true", + envVarValue: "1", + flagValue: boolPtr(true), + expectedSkipPreflights: true, + }, + { + name: "env var not set, no flag", + envVarValue: "", + flagValue: nil, + expectedSkipPreflights: false, + }, + { + name: "env var not set, flag explicitly false", + envVarValue: "", + flagValue: boolPtr(false), + expectedSkipPreflights: false, + }, + { + name: "env var not set, flag explicitly true", + envVarValue: "", + flagValue: boolPtr(true), + expectedSkipPreflights: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up environment variable + if tt.envVarValue != "" { + t.Setenv("SKIP_HOST_PREFLIGHTS", tt.envVarValue) + } + + // Create a mock cobra command to simulate flag behavior + cmd := &cobra.Command{} + flags := &installFlags{ + networkInterface: "eth0", // Skip network interface auto-detection + } + + // Add the flags + cmd.Flags().BoolVar(&flags.skipHostPreflights, "skip-host-preflights", false, "Skip host preflight checks") + mustAddCIDRFlags(cmd.Flags()) + mustAddProxyFlags(cmd.Flags()) + + // Set the flag if explicitly provided in test + if tt.flagValue != nil { + err := cmd.Flags().Set("skip-host-preflights", "true") + if *tt.flagValue { + assert.NoError(t, err) + } else { + // For false, we need to mark the flag as changed but set to false + cmd.Flags().Set("skip-host-preflights", "false") + } + } + + err := buildInstallFlags(cmd, flags) + assert.NoError(t, err) + + // Verify the flag was set correctly + assert.Equal(t, tt.expectedSkipPreflights, flags.skipHostPreflights) + }) + } +} + +func Test_buildInstallFlags_CIDRConfig(t *testing.T) { + // Compute expected split CIDR values for the default CIDR + defaultPodCIDR, defaultServiceCIDR, err := newconfig.SplitCIDR(ecv1beta1.DefaultNetworkCIDR) + assert.NoError(t, err, "failed to split default CIDR") + + tests := []struct { + name string + init func(t *testing.T, flagSet *pflag.FlagSet) + expected *newconfig.CIDRConfig + expectError bool + }{ + { + name: "with pod and service flags", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("pod-cidr", "10.0.0.0/24") + flagSet.Set("service-cidr", "10.1.0.0/24") + }, + expected: &newconfig.CIDRConfig{ + PodCIDR: "10.0.0.0/24", + ServiceCIDR: "10.1.0.0/24", + GlobalCIDR: nil, + }, + }, + { + name: "with pod flag", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("pod-cidr", "10.0.0.0/24") + }, + expected: &newconfig.CIDRConfig{ + PodCIDR: "10.0.0.0/24", + ServiceCIDR: v1beta1.DefaultNetwork().ServiceCIDR, + GlobalCIDR: nil, + }, + }, + { + name: "with pod, service and cidr flags - should error", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("pod-cidr", "10.0.0.0/24") + flagSet.Set("service-cidr", "10.1.0.0/24") + flagSet.Set("cidr", "10.2.0.0/24") + }, + expectError: true, + }, + { + name: "with pod and cidr flags - should error", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("pod-cidr", "10.0.0.0/24") + flagSet.Set("cidr", "10.2.0.0/24") + }, + expectError: true, + }, + { + name: "with service flag only", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("service-cidr", "10.1.0.0/24") + }, + expected: &newconfig.CIDRConfig{ + PodCIDR: v1beta1.DefaultNetwork().PodCIDR, + ServiceCIDR: "10.1.0.0/24", + GlobalCIDR: nil, + }, + }, + { + name: "with cidr flag", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("cidr", "10.2.0.0/16") + }, + expected: &newconfig.CIDRConfig{ + PodCIDR: "10.2.0.0/17", + ServiceCIDR: "10.2.128.0/17", + GlobalCIDR: ptr.To("10.2.0.0/16"), + }, + }, + { + name: "with no flags (defaults)", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + // No flags set, should use default cidr value and split it + }, + expected: &newconfig.CIDRConfig{ + PodCIDR: defaultPodCIDR, + ServiceCIDR: defaultServiceCIDR, + GlobalCIDR: ptr.To(ecv1beta1.DefaultNetworkCIDR), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup flags struct + flags := &installFlags{ + networkInterface: "eth0", // Skip network interface auto-detection + } + + // Setup cobra command with flags + cmd := &cobra.Command{} + mustAddCIDRFlags(cmd.Flags()) + mustAddProxyFlags(cmd.Flags()) + + flagSet := cmd.Flags() + if tt.init != nil { + tt.init(t, flagSet) + } + + err := buildInstallFlags(cmd, flags) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err, "unexpected error") + assert.Equal(t, tt.expected, flags.cidrConfig) + } + }) + } +} + +func Test_buildInstallFlags_TLSValidation(t *testing.T) { + tests := []struct { + name string + tlsCertFile string + tlsKeyFile string + wantErr string + }{ + { + name: "both cert and key provided", + tlsCertFile: "/path/to/cert.pem", + tlsKeyFile: "/path/to/key.pem", + wantErr: "", + }, + { + name: "neither cert nor key provided", + tlsCertFile: "", + tlsKeyFile: "", + wantErr: "", + }, + { + name: "only cert file provided", + tlsCertFile: "/path/to/cert.pem", + tlsKeyFile: "", + wantErr: "both --tls-cert and --tls-key must be provided together", + }, + { + name: "only key file provided", + tlsCertFile: "", + tlsKeyFile: "/path/to/key.pem", + wantErr: "both --tls-cert and --tls-key must be provided together", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup flags struct + flags := &installFlags{ + networkInterface: "eth0", // Skip network interface auto-detection + tlsCertFile: tt.tlsCertFile, + tlsKeyFile: tt.tlsKeyFile, + } + + // Setup cobra command with flags + cmd := &cobra.Command{} + mustAddCIDRFlags(cmd.Flags()) + mustAddProxyFlags(cmd.Flags()) + + err := buildInstallFlags(cmd, flags) + + if tt.wantErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/installer/cli/install_runpreflights.go b/cmd/installer/cli/install_runpreflights.go index 9d6ae68968..77a6e1a17d 100644 --- a/cmd/installer/cli/install_runpreflights.go +++ b/cmd/installer/cli/install_runpreflights.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "os" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg-new/kubernetesinstallation" @@ -23,7 +22,7 @@ import ( var ErrPreflightsHaveFail = metrics.NewErrorNoFail(fmt.Errorf("host preflight failures detected")) func InstallRunPreflightsCmd(ctx context.Context, appSlug string) *cobra.Command { - var flags InstallCmdFlags + var flags installFlags rc := runtimeconfig.New(nil) ki := kubernetesinstallation.New(nil) @@ -36,16 +35,17 @@ func InstallRunPreflightsCmd(ctx context.Context, appSlug string) *cobra.Command rc.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { - if err := preRunInstall(cmd, &flags, rc, ki); err != nil { + installCfg, err := preRunInstall(cmd, &flags, rc, ki) + if err != nil { return err } - if err := verifyAndPrompt(ctx, cmd, appSlug, &flags, prompts.New()); err != nil { + if err := verifyAndPrompt(ctx, cmd, appSlug, &flags, installCfg, prompts.New()); err != nil { return err } _ = rc.SetEnv() - if err := runInstallRunPreflights(cmd.Context(), flags, rc); err != nil { + if err := runInstallRunPreflights(cmd.Context(), flags, installCfg, rc); err != nil { return err } @@ -62,22 +62,17 @@ func InstallRunPreflightsCmd(ctx context.Context, appSlug string) *cobra.Command return cmd } -func runInstallRunPreflights(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { - licenseBytes, err := os.ReadFile(flags.licenseFile) - if err != nil { - return fmt.Errorf("unable to read license file: %w", err) - } - +func runInstallRunPreflights(ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) error { logrus.Debugf("configuring host") if err := hostutils.ConfigureHost(ctx, rc, hostutils.InitForInstallOptions{ - License: licenseBytes, + License: installCfg.licenseBytes, AirgapBundle: flags.airgapBundle, }); err != nil { return fmt.Errorf("configure host: %w", err) } logrus.Debugf("running install preflights") - if err := runInstallPreflights(ctx, flags, rc, nil); err != nil { + if err := runInstallPreflights(ctx, flags, installCfg, rc, nil); err != nil { if errors.Is(err, preflights.ErrPreflightsHaveFail) { return NewErrorNothingElseToAdd(err) } @@ -89,7 +84,7 @@ func runInstallRunPreflights(ctx context.Context, flags InstallCmdFlags, rc runt return nil } -func runInstallPreflights(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, metricsReporter metrics.ReporterInterface) error { +func runInstallPreflights(ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, metricsReporter metrics.ReporterInterface) error { replicatedAppURL := replicatedAppURL() proxyRegistryURL := proxyRegistryURL() @@ -100,11 +95,11 @@ func runInstallPreflights(ctx context.Context, flags InstallCmdFlags, rc runtime // Calculate airgap storage space requirement var controllerAirgapStorageSpace string - if flags.airgapMetadata != nil && flags.airgapMetadata.AirgapInfo != nil { + if installCfg.airgapMetadata != nil && installCfg.airgapMetadata.AirgapInfo != nil { controllerAirgapStorageSpace = preflights.CalculateAirgapStorageSpace(preflights.AirgapStorageSpaceCalcArgs{ - UncompressedSize: flags.airgapMetadata.AirgapInfo.Spec.UncompressedSize, - EmbeddedAssetsSize: flags.embeddedAssetsSize, - K0sImageSize: flags.airgapMetadata.K0sImageSize, + UncompressedSize: installCfg.airgapMetadata.AirgapInfo.Spec.UncompressedSize, + EmbeddedAssetsSize: installCfg.embeddedAssetsSize, + K0sImageSize: installCfg.airgapMetadata.K0sImageSize, IsController: true, }) } @@ -122,7 +117,7 @@ func runInstallPreflights(ctx context.Context, flags InstallCmdFlags, rc runtime PodCIDR: rc.PodCIDR(), ServiceCIDR: rc.ServiceCIDR(), NodeIP: nodeIP, - IsAirgap: flags.isAirgap, + IsAirgap: installCfg.isAirgap, ControllerAirgapStorageSpace: controllerAirgapStorageSpace, } if globalCIDR := rc.GlobalCIDR(); globalCIDR != "" { diff --git a/cmd/installer/cli/install_test.go b/cmd/installer/cli/install_test.go index a7e959b5cf..be4eb98557 100644 --- a/cmd/installer/cli/install_test.go +++ b/cmd/installer/cli/install_test.go @@ -11,10 +11,10 @@ import ( "testing" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/prompts/plain" "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" @@ -111,7 +111,7 @@ func Test_ensureAdminConsolePassword(t *testing.T) { t.Run(tt.name, func(t *testing.T) { req := require.New(t) - flags := &InstallCmdFlags{ + flags := &installFlags{ assumeYes: tt.noPrompt, adminConsolePassword: tt.userPassword, } @@ -610,100 +610,6 @@ func Test_verifyProxyConfig(t *testing.T) { } } -func Test_preRunInstall_SkipHostPreflightsEnvVar(t *testing.T) { - tests := []struct { - name string - envVarValue string - flagValue *bool // nil means not set, true/false means explicitly set - expectedSkipPreflights bool - }{ - { - name: "env var set to 1, no flag", - envVarValue: "1", - flagValue: nil, - expectedSkipPreflights: true, - }, - { - name: "env var set to true, no flag", - envVarValue: "true", - flagValue: nil, - expectedSkipPreflights: true, - }, - { - name: "env var set, flag explicitly false (flag takes precedence)", - envVarValue: "1", - flagValue: boolPtr(false), - expectedSkipPreflights: false, - }, - { - name: "env var set, flag explicitly true", - envVarValue: "1", - flagValue: boolPtr(true), - expectedSkipPreflights: true, - }, - { - name: "env var not set, no flag", - envVarValue: "", - flagValue: nil, - expectedSkipPreflights: false, - }, - { - name: "env var not set, flag explicitly false", - envVarValue: "", - flagValue: boolPtr(false), - expectedSkipPreflights: false, - }, - { - name: "env var not set, flag explicitly true", - envVarValue: "", - flagValue: boolPtr(true), - expectedSkipPreflights: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Set up environment variable - if tt.envVarValue != "" { - t.Setenv("SKIP_HOST_PREFLIGHTS", tt.envVarValue) - } - - // Create a mock cobra command to simulate flag behavior - cmd := &cobra.Command{} - flags := &InstallCmdFlags{} - - // Add the flag to the command (similar to addInstallFlags) - cmd.Flags().BoolVar(&flags.skipHostPreflights, "skip-host-preflights", false, "Skip host preflight checks") - - // Set the flag if explicitly provided in test - if tt.flagValue != nil { - err := cmd.Flags().Set("skip-host-preflights", fmt.Sprintf("%t", *tt.flagValue)) - require.NoError(t, err) - } - - // Create a minimal runtime config for the test - rc := runtimeconfig.New(nil) - - // Call preRunInstall (this would normally require root, but we're just testing the flag logic) - // We expect this to fail due to non-root execution, but we can check the flag value before it fails - err := preRunInstallLinux(cmd, flags, rc) - - // The function will fail due to non-root check, but we can verify the flag was set correctly - // by checking the flag value before the root check fails - assert.Equal(t, tt.expectedSkipPreflights, flags.skipHostPreflights) - - // We expect an error due to non-root execution - assert.Error(t, err) - assert.Contains(t, err.Error(), "install command must be run as root") - }) - } -} - -// Helper function to create bool pointer -func boolPtr(b bool) *bool { - return &b -} - func Test_ignoreAppPreflights_FlagVisibility(t *testing.T) { tests := []struct { name string @@ -732,7 +638,7 @@ func Test_ignoreAppPreflights_FlagVisibility(t *testing.T) { t.Setenv("ENABLE_V3", tt.enableV3EnvVar) } - flags := &InstallCmdFlags{} + flags := &installFlags{} enableV3 := isV3Enabled() flagSet := newLinuxInstallFlags(flags, enableV3) @@ -797,7 +703,7 @@ func Test_ignoreAppPreflights_FlagParsing(t *testing.T) { } // Create a flagset similar to how newLinuxInstallFlags works - flags := &InstallCmdFlags{} + flags := &installFlags{} flagSet := newLinuxInstallFlags(flags, tt.enableV3) // Create a command to test flag parsing @@ -820,7 +726,101 @@ func Test_ignoreAppPreflights_FlagParsing(t *testing.T) { } } -func Test_processTLSConfig(t *testing.T) { +func Test_k0sConfigFromFlags(t *testing.T) { + tests := []struct { + name string + podCIDR string + serviceCIDR string + globalCIDR *string + expectedPodCIDR string + expectedServiceCIDR string + wantErr bool + }{ + { + name: "pod and service CIDRs set", + podCIDR: "10.0.0.0/24", + serviceCIDR: "10.1.0.0/24", + globalCIDR: nil, + expectedPodCIDR: "10.0.0.0/24", + expectedServiceCIDR: "10.1.0.0/24", + wantErr: false, + }, + { + name: "custom pod and service CIDRs", + podCIDR: "192.168.0.0/16", + serviceCIDR: "10.96.0.0/12", + globalCIDR: nil, + expectedPodCIDR: "192.168.0.0/16", + expectedServiceCIDR: "10.96.0.0/12", + wantErr: false, + }, + { + name: "global CIDR should not affect k0s config", + podCIDR: "10.0.0.0/25", + serviceCIDR: "10.0.0.128/25", + globalCIDR: stringPtr("10.0.0.0/24"), + expectedPodCIDR: "10.0.0.0/25", + expectedServiceCIDR: "10.0.0.128/25", + wantErr: false, + }, + { + name: "IPv4 CIDRs with different masks", + podCIDR: "172.16.0.0/20", + serviceCIDR: "172.17.0.0/20", + globalCIDR: nil, + expectedPodCIDR: "172.16.0.0/20", + expectedServiceCIDR: "172.17.0.0/20", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := require.New(t) + + flags := &installFlags{ + cidrConfig: &newconfig.CIDRConfig{ + PodCIDR: tt.podCIDR, + ServiceCIDR: tt.serviceCIDR, + GlobalCIDR: tt.globalCIDR, + }, + networkInterface: "", + overrides: "", + } + installCfg := &installConfig{} + + cfg, err := k0sConfigFromFlags(flags, installCfg) + + if tt.wantErr { + req.Error(err) + return + } + + req.NoError(err) + req.NotNil(cfg) + req.NotNil(cfg.Spec) + req.NotNil(cfg.Spec.Network) + + // Verify pod CIDR is set correctly if expected + if tt.expectedPodCIDR != "" { + req.Equal(tt.expectedPodCIDR, cfg.Spec.Network.PodCIDR, + "Pod CIDR should be set correctly in k0s config") + } + + // Verify service CIDR is set correctly if expected + if tt.expectedServiceCIDR != "" { + req.Equal(tt.expectedServiceCIDR, cfg.Spec.Network.ServiceCIDR, + "Service CIDR should be set correctly in k0s config") + } + }) + } +} + +func stringPtr(s string) *string { + return &s +} + +func Test_buildInstallDerivedConfig_TLS(t *testing.T) { // Create a temporary directory for test certificates tmpdir := t.TempDir() @@ -899,32 +899,18 @@ oxhVqyhpk86rf0rT5DcD/sBw wantErr: "", expectTLS: false, }, - { - name: "only cert file provided", - tlsCertFile: certPath, - tlsKeyFile: "", - wantErr: "both --tls-cert and --tls-key must be provided together", - expectTLS: false, - }, - { - name: "only key file provided", - tlsCertFile: "", - tlsKeyFile: keyPath, - wantErr: "both --tls-cert and --tls-key must be provided together", - expectTLS: false, - }, { name: "cert file does not exist", tlsCertFile: filepath.Join(tmpdir, "nonexistent.pem"), tlsKeyFile: keyPath, - wantErr: "load tls certificate", + wantErr: "failed to read TLS certificate", expectTLS: false, }, { name: "key file does not exist", tlsCertFile: certPath, tlsKeyFile: filepath.Join(tmpdir, "nonexistent.key"), - wantErr: "load tls certificate", + wantErr: "failed to read TLS key", expectTLS: false, }, { @@ -935,7 +921,7 @@ oxhVqyhpk86rf0rT5DcD/sBw return invalidCertPath }(), tlsKeyFile: keyPath, - wantErr: "load tls certificate", + wantErr: "failed to parse TLS certificate", expectTLS: false, }, { @@ -949,26 +935,27 @@ oxhVqyhpk86rf0rT5DcD/sBw for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - flags := &InstallCmdFlags{ + flags := &installFlags{ tlsCertFile: tt.tlsCertFile, tlsKeyFile: tt.tlsKeyFile, } - err := processTLSConfig(flags) + installCfg, err := buildInstallConfig(flags) if tt.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr) } else { require.NoError(t, err) - } - if tt.expectTLS { - assert.NotEmpty(t, flags.tlsCertBytes, "TLS cert bytes should be populated") - assert.NotEmpty(t, flags.tlsKeyBytes, "TLS key bytes should be populated") - } else { - assert.Empty(t, flags.tlsCertBytes, "TLS cert bytes should be empty") - assert.Empty(t, flags.tlsKeyBytes, "TLS key bytes should be empty") + if tt.expectTLS { + assert.NotEmpty(t, installCfg.tlsCertBytes, "TLS cert bytes should be populated") + assert.NotEmpty(t, installCfg.tlsKeyBytes, "TLS key bytes should be populated") + assert.NotNil(t, installCfg.tlsCert.Certificate, "TLS cert should be loaded") + } else { + assert.Empty(t, installCfg.tlsCertBytes, "TLS cert bytes should be empty") + assert.Empty(t, installCfg.tlsKeyBytes, "TLS key bytes should be empty") + } } }) } diff --git a/cmd/installer/cli/kots_options_test.go b/cmd/installer/cli/kots_options_test.go new file mode 100644 index 0000000000..9c04065227 --- /dev/null +++ b/cmd/installer/cli/kots_options_test.go @@ -0,0 +1,101 @@ +package cli + +import ( + "testing" + + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/stretchr/testify/require" +) + +func Test_kotsInstallOptionsFromFlags(t *testing.T) { + tests := []struct { + name string + configValuesFile string + airgapBundle string + ignoreAppPreflights bool + expectedConfigFile string + expectedAirgapBundle string + expectedSkipPreflights bool + }{ + { + name: "with config values file", + configValuesFile: "/tmp/config-values.yaml", + airgapBundle: "", + ignoreAppPreflights: false, + expectedConfigFile: "/tmp/config-values.yaml", + expectedAirgapBundle: "", + expectedSkipPreflights: false, + }, + { + name: "without config values", + configValuesFile: "", + airgapBundle: "", + ignoreAppPreflights: false, + expectedConfigFile: "", + expectedAirgapBundle: "", + expectedSkipPreflights: false, + }, + { + name: "with config values and airgap bundle", + configValuesFile: "/tmp/config-values.yaml", + airgapBundle: "/tmp/airgap.tar.gz", + ignoreAppPreflights: false, + expectedConfigFile: "/tmp/config-values.yaml", + expectedAirgapBundle: "/tmp/airgap.tar.gz", + expectedSkipPreflights: false, + }, + { + name: "with all flags set", + configValuesFile: "/tmp/config-values.yaml", + airgapBundle: "/tmp/airgap.tar.gz", + ignoreAppPreflights: true, + expectedConfigFile: "/tmp/config-values.yaml", + expectedAirgapBundle: "/tmp/airgap.tar.gz", + expectedSkipPreflights: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := require.New(t) + + // Create mock flags and install config + flags := installFlags{ + configValues: tt.configValuesFile, + airgapBundle: tt.airgapBundle, + ignoreAppPreflights: tt.ignoreAppPreflights, + } + + installCfg := &installConfig{ + clusterID: "test-cluster-123", + licenseBytes: []byte("license-data"), + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app", + }, + }, + } + + // Call function + opts := kotsInstallOptionsFromFlags(flags, installCfg, "kotsadm") + + // Validate all fields + req.Equal(tt.expectedConfigFile, opts.ConfigValuesFile, + "ConfigValuesFile should match") + req.Equal(tt.expectedAirgapBundle, opts.AirgapBundle, + "AirgapBundle should match") + req.Equal(tt.expectedSkipPreflights, opts.SkipPreflights, + "SkipPreflights should match") + + // Validate fields from installConfig + req.Equal("test-app", opts.AppSlug, + "AppSlug should be set from license") + req.Equal("kotsadm", opts.Namespace, + "Namespace should be set from parameter") + req.Equal("test-cluster-123", opts.ClusterID, + "ClusterID should be set from installConfig") + req.Equal([]byte("license-data"), opts.License, + "License should be set from installConfig") + }) + } +} diff --git a/cmd/installer/cli/proxy.go b/cmd/installer/cli/proxy.go index 5573e9afce..87c87b6c33 100644 --- a/cmd/installer/cli/proxy.go +++ b/cmd/installer/cli/proxy.go @@ -30,8 +30,8 @@ func mustAddProxyFlags(flagSet *pflag.FlagSet) { flagSet.String("no-proxy", "", "Comma-separated list of hosts for which not to use a proxy (overrides no_proxy/NO_PROXY environment variables)") } -func parseProxyFlags(cmd *cobra.Command) (*ecv1beta1.ProxySpec, error) { - p, err := getProxySpec(cmd) +func parseProxyFlags(cmd *cobra.Command, networkInterface string, cidrCfg *newconfig.CIDRConfig) (*ecv1beta1.ProxySpec, error) { + p, err := getProxySpec(cmd, networkInterface, cidrCfg) if err != nil { return nil, fmt.Errorf("unable to get proxy spec from flags: %w", err) } @@ -40,7 +40,7 @@ func parseProxyFlags(cmd *cobra.Command) (*ecv1beta1.ProxySpec, error) { return p, nil } -func getProxySpec(cmd *cobra.Command) (*ecv1beta1.ProxySpec, error) { +func getProxySpec(cmd *cobra.Command, networkInterface string, cidrCfg *newconfig.CIDRConfig) (*ecv1beta1.ProxySpec, error) { // Command-line flags have the highest precedence httpProxy, err := cmd.Flags().GetString("http-proxy") if err != nil { @@ -54,14 +54,6 @@ func getProxySpec(cmd *cobra.Command) (*ecv1beta1.ProxySpec, error) { if err != nil { return nil, fmt.Errorf("unable to get no-proxy flag: %w", err) } - networkInterface, err := cmd.Flags().GetString("network-interface") - if err != nil { - return nil, fmt.Errorf("unable to get network-interface flag: %w", err) - } - cidrCfg, err := getCIDRConfig(cmd) - if err != nil { - return nil, fmt.Errorf("unable to determine pod and service CIDRs: %w", err) - } proxy, err := newconfig.GetProxySpec(httpProxy, httpsProxy, noProxy, cidrCfg.PodCIDR, cidrCfg.ServiceCIDR, networkInterface, defaultNetworkLookupImpl) if err != nil { return nil, fmt.Errorf("unable to get proxy spec: %w", err) diff --git a/cmd/installer/cli/proxy_test.go b/cmd/installer/cli/proxy_test.go deleted file mode 100644 index d6e320b88a..0000000000 --- a/cmd/installer/cli/proxy_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package cli - -import ( - "net" - "testing" - - ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/stretchr/testify/assert" -) - -// Mock network interface for testing -type mockNetworkLookup struct{} - -func (m *mockNetworkLookup) FirstValidIPNet(networkInterface string) (*net.IPNet, error) { - _, ipnet, _ := net.ParseCIDR("192.168.1.0/24") - return ipnet, nil -} - -func Test_getProxySpecFromFlags(t *testing.T) { - tests := []struct { - name string - init func(t *testing.T, flagSet *pflag.FlagSet) - want *ecv1beta1.ProxySpec - }{ - { - name: "no flags set and no env vars should not set proxy", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - // No env vars, no flags - }, - want: nil, - }, - { - name: "lowercase env vars should be used when no flags set", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - t.Setenv("http_proxy", "http://lower-proxy") - t.Setenv("https_proxy", "https://lower-proxy") - t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://lower-proxy", - HTTPSProxy: "https://lower-proxy", - ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "uppercase env vars should be used when no flags set and no lowercase vars", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - t.Setenv("HTTP_PROXY", "http://upper-proxy") - t.Setenv("HTTPS_PROXY", "https://upper-proxy") - t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://upper-proxy", - HTTPSProxy: "https://upper-proxy", - ProvidedNoProxy: "upper-no-proxy-1,upper-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,upper-no-proxy-1,upper-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "lowercase should take precedence over uppercase", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - t.Setenv("http_proxy", "http://lower-proxy") - t.Setenv("https_proxy", "https://lower-proxy") - t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") - t.Setenv("HTTP_PROXY", "http://upper-proxy") - t.Setenv("HTTPS_PROXY", "https://upper-proxy") - t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://lower-proxy", - HTTPSProxy: "https://lower-proxy", - ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "proxy flags should override env vars", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - t.Setenv("http_proxy", "http://lower-proxy") - t.Setenv("https_proxy", "https://lower-proxy") - t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") - t.Setenv("HTTP_PROXY", "http://upper-proxy") - t.Setenv("HTTPS_PROXY", "https://upper-proxy") - t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") - - flagSet.Set("http-proxy", "http://flag-proxy") - flagSet.Set("https-proxy", "https://flag-proxy") - flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://flag-proxy", - HTTPSProxy: "https://flag-proxy", - ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "pod and service CIDR should override default no proxy", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - flagSet.Set("http-proxy", "http://flag-proxy") - flagSet.Set("https-proxy", "https://flag-proxy") - flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") - - flagSet.Set("pod-cidr", "1.1.1.1/24") - flagSet.Set("service-cidr", "2.2.2.2/24") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://flag-proxy", - HTTPSProxy: "https://flag-proxy", - ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,1.1.1.1/24,2.2.2.2/24,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "custom --cidr should be present in the no-proxy", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - flagSet.Set("http-proxy", "http://flag-proxy") - flagSet.Set("https-proxy", "https://flag-proxy") - flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") - - flagSet.Set("cidr", "10.0.0.0/16") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://flag-proxy", - HTTPSProxy: "https://flag-proxy", - ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.0.0.0/17,10.0.128.0/17,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "partial env vars with partial flag vars", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - t.Setenv("http_proxy", "http://lower-proxy") - // No https_proxy set - t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") - - // Only set https-proxy flag - flagSet.Set("https-proxy", "https://flag-proxy") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://lower-proxy", - HTTPSProxy: "https://flag-proxy", - ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cmd := &cobra.Command{} - mustAddCIDRFlags(cmd.Flags()) - mustAddProxyFlags(cmd.Flags()) - cmd.Flags().String("network-interface", "", "The network interface to use for the cluster") - - flagSet := cmd.Flags() - if tt.init != nil { - tt.init(t, flagSet) - } - - // Override the network lookup with our mock - defaultNetworkLookupImpl = &mockNetworkLookup{} - - got, err := getProxySpec(cmd) - assert.NoError(t, err, "unexpected error received") - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/cmd/installer/cli/restore.go b/cmd/installer/cli/restore.go index bcc16c5c44..8fa9e8edae 100644 --- a/cmd/installer/cli/restore.go +++ b/cmd/installer/cli/restore.go @@ -87,7 +87,7 @@ const ( ) func RestoreCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { - var flags InstallCmdFlags + var flags installFlags var s3Store s3BackupStore var skipStoreValidation bool @@ -102,13 +102,14 @@ func RestoreCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { rc.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { - if err := preRunInstall(cmd, &flags, rc, ki); err != nil { + installCfg, err := preRunInstall(cmd, &flags, rc, ki) + if err != nil { return err } _ = rc.SetEnv() - if err := runRestore(cmd.Context(), appSlug, appTitle, flags, rc, s3Store, skipStoreValidation); err != nil { + if err := runRestore(cmd.Context(), appSlug, appTitle, flags, installCfg, rc, s3Store, skipStoreValidation); err != nil { return err } @@ -124,15 +125,15 @@ func RestoreCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { return cmd } -func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, s3Store s3BackupStore, skipStoreValidation bool) error { - err := verifyChannelRelease("restore", flags.isAirgap, flags.assumeYes) +func runRestore(ctx context.Context, appSlug, appTitle string, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, s3Store s3BackupStore, skipStoreValidation bool) error { + err := verifyChannelRelease("restore", installCfg.isAirgap, flags.assumeYes) if err != nil { return err } - if flags.airgapMetadata != nil && flags.airgapMetadata.AirgapInfo != nil { + if installCfg.airgapMetadata != nil && installCfg.airgapMetadata.AirgapInfo != nil { logrus.Debugf("checking airgap bundle matches binary") - if err := checkAirgapMatches(flags.airgapMetadata.AirgapInfo); err != nil { + if err := checkAirgapMatches(installCfg.airgapMetadata.AirgapInfo); err != nil { return err // we want the user to see the error message without a prefix } } @@ -157,7 +158,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF if state != ecRestoreStateNew { logrus.Debugf("getting backup from restore state") var err error - backupToRestore, err = getBackupFromRestoreState(ctx, flags.isAirgap, rc) + backupToRestore, err = getBackupFromRestoreState(ctx, installCfg.isAirgap, rc) if err != nil { return fmt.Errorf("unable to resume: %w", err) } @@ -190,7 +191,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF switch state { case ecRestoreStateNew: - err = runRestoreStepNew(ctx, appSlug, appTitle, flags, rc, &s3Store, skipStoreValidation) + err = runRestoreStepNew(ctx, appSlug, appTitle, flags, installCfg, rc, &s3Store, skipStoreValidation) if err != nil { return err } @@ -204,7 +205,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return fmt.Errorf("unable to set restore state: %w", err) } - backup, ok, err := runRestoreStepConfirmBackup(ctx, flags, rc) + backup, ok, err := runRestoreStepConfirmBackup(ctx, installCfg, rc) if err != nil { return err } else if !ok { @@ -263,7 +264,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreSeaweedFS(ctx, flags, backupToRestore) + err = runRestoreSeaweedFS(ctx, installCfg, backupToRestore) if err != nil { return err } @@ -277,7 +278,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreRegistry(ctx, flags, backupToRestore) + err = runRestoreRegistry(ctx, installCfg, backupToRestore) if err != nil { return err } @@ -291,7 +292,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreEnableAdminConsoleHA(ctx, flags, rc, backupToRestore) + err = runRestoreEnableAdminConsoleHA(ctx, installCfg, rc, backupToRestore) if err != nil { return err } @@ -319,7 +320,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreExtensions(ctx, flags, rc) + err = runRestoreExtensions(ctx, installCfg, rc) if err != nil { return err } @@ -345,7 +346,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return nil } -func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, s3Store *s3BackupStore, skipStoreValidation bool) error { +func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, s3Store *s3BackupStore, skipStoreValidation bool) error { logrus.Debugf("checking if k0s is already installed") err := verifyNoInstallation(appSlug, "restore") if err != nil { @@ -377,14 +378,14 @@ func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags Inst } logrus.Debugf("running install preflights") - if err := runInstallPreflights(ctx, flags, rc, nil); err != nil { + if err := runInstallPreflights(ctx, flags, installCfg, rc, nil); err != nil { if errors.Is(err, preflights.ErrPreflightsHaveFail) { return NewErrorNothingElseToAdd(err) } return fmt.Errorf("unable to run install preflights: %w", err) } - _, err = installAndStartCluster(ctx, flags, rc, nil) + _, err = installAndStartCluster(ctx, flags, installCfg, rc, nil) if err != nil { return err } @@ -400,7 +401,7 @@ func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags Inst } airgapChartsPath := "" - if flags.isAirgap { + if installCfg.isAirgap { airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } @@ -421,7 +422,7 @@ func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags Inst // TODO (@salah): update installation status to reflect what's happening logrus.Debugf("installing addons") - if err := installAddonsForRestore(ctx, kcli, mcli, hcli, rc, flags); err != nil { + if err := installAddonsForRestore(ctx, kcli, mcli, hcli, rc); err != nil { return err } @@ -451,7 +452,7 @@ func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags Inst return nil } -func installAddonsForRestore(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, flags InstallCmdFlags) error { +func installAddonsForRestore(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig) error { embCfg := release.GetEmbeddedClusterConfig() var embCfgSpec *ecv1beta1.ConfigSpec if embCfg != nil { @@ -500,7 +501,7 @@ func installAddonsForRestore(ctx context.Context, kcli client.Client, mcli metad return nil } -func runRestoreStepConfirmBackup(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) (*disasterrecovery.ReplicatedBackup, bool, error) { +func runRestoreStepConfirmBackup(ctx context.Context, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) (*disasterrecovery.ReplicatedBackup, bool, error) { kcli, err := kubeutils.KubeClient() if err != nil { return nil, false, fmt.Errorf("unable to create kube client: %w", err) @@ -512,7 +513,7 @@ func runRestoreStepConfirmBackup(ctx context.Context, flags InstallCmdFlags, rc } logrus.Debugf("waiting for backups to become available") - backups, err := waitForBackups(ctx, os.Stdout, kcli, k0sCfg, rc, flags.isAirgap) + backups, err := waitForBackups(ctx, os.Stdout, kcli, k0sCfg, rc, installCfg.isAirgap) if err != nil { return nil, false, err } @@ -564,7 +565,7 @@ func runRestoreAdminConsole(ctx context.Context, backupToRestore *disasterrecove return nil } -func runRestoreWaitForNodes(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { +func runRestoreWaitForNodes(ctx context.Context, flags installFlags, rc runtimeconfig.RuntimeConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { logrus.Debugf("checking if backup is high availability") highAvailability, err := isHighAvailabilityReplicatedBackup(*backupToRestore) if err != nil { @@ -580,7 +581,7 @@ func runRestoreWaitForNodes(ctx context.Context, flags InstallCmdFlags, rc runti return nil } -func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { +func runRestoreEnableAdminConsoleHA(ctx context.Context, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { highAvailability, err := isHighAvailabilityReplicatedBackup(*backupToRestore) if err != nil { return err @@ -609,17 +610,13 @@ func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, } airgapChartsPath := "" - if flags.isAirgap { + if installCfg.isAirgap { airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } - euCfg, err := helpers.ParseEndUserConfig(flags.overrides) - if err != nil { - return fmt.Errorf("parse end user config: %w", err) - } var euCfgSpec *ecv1beta1.ConfigSpec - if euCfg != nil { - euCfgSpec = &euCfg.Spec + if installCfg.endUserConfig != nil { + euCfgSpec = &installCfg.endUserConfig.Spec } hcli, err := helm.NewClient(helm.HelmOptions{ @@ -672,11 +669,11 @@ func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, return nil } -func runRestoreSeaweedFS(ctx context.Context, flags InstallCmdFlags, backupToRestore *disasterrecovery.ReplicatedBackup) error { +func runRestoreSeaweedFS(ctx context.Context, installCfg *installConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { highAvailability, err := isHighAvailabilityReplicatedBackup(*backupToRestore) if err != nil { return err - } else if !flags.isAirgap || !highAvailability { + } else if !installCfg.isAirgap || !highAvailability { // only restore seaweedfs in case of high availability and airgap return nil } @@ -689,9 +686,9 @@ func runRestoreSeaweedFS(ctx context.Context, flags InstallCmdFlags, backupToRes return nil } -func runRestoreRegistry(ctx context.Context, flags InstallCmdFlags, backupToRestore *disasterrecovery.ReplicatedBackup) error { +func runRestoreRegistry(ctx context.Context, installCfg *installConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { // only restore registry in case of airgap - if !flags.isAirgap { + if !installCfg.isAirgap { return nil } @@ -721,9 +718,9 @@ func runRestoreECO(ctx context.Context, backupToRestore *disasterrecovery.Replic return nil } -func runRestoreExtensions(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { +func runRestoreExtensions(ctx context.Context, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) error { airgapChartsPath := "" - if flags.isAirgap { + if installCfg.isAirgap { airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } diff --git a/cmd/installer/kotscli/kotscli.go b/cmd/installer/kotscli/kotscli.go index 057ef81b62..cd8418ebb0 100644 --- a/cmd/installer/kotscli/kotscli.go +++ b/cmd/installer/kotscli/kotscli.go @@ -35,6 +35,40 @@ type InstallOptions struct { Stdout io.Writer } +// buildInstallArgs constructs the command-line arguments for kubectl-kots install. +// This function is extracted to enable integration testing of argument construction. +func buildInstallArgs(opts InstallOptions, upstreamURI string, appVersionLabel string, licenseFile string) []string { + installArgs := []string{ + "install", + upstreamURI, + "--license-file", + licenseFile, + "--namespace", + opts.Namespace, + "--app-version-label", + appVersionLabel, + "--exclude-admin-console", + } + + if opts.DisableImagePush { + installArgs = append(installArgs, "--disable-image-push") + } + + if opts.AirgapBundle != "" { + installArgs = append(installArgs, "--airgap-bundle", opts.AirgapBundle) + } + + if opts.ConfigValuesFile != "" { + installArgs = append(installArgs, "--config-values", opts.ConfigValuesFile) + } + + if opts.SkipPreflights { + installArgs = append(installArgs, "--skip-preflights") + } + + return installArgs +} + func Install(opts InstallOptions) error { kotsBinPath, err := goods.InternalBinary("kubectl-kots") if err != nil { @@ -61,32 +95,11 @@ func Install(opts InstallOptions) error { defer os.Remove(licenseFile) maskfn := MaskKotsOutputForOnline() - installArgs := []string{ - "install", - upstreamURI, - "--license-file", - licenseFile, - "--namespace", - opts.Namespace, - "--app-version-label", - appVersionLabel, - "--exclude-admin-console", - } - if opts.DisableImagePush { - installArgs = append(installArgs, "--disable-image-push") - } if opts.AirgapBundle != "" { - installArgs = append(installArgs, "--airgap-bundle", opts.AirgapBundle) maskfn = MaskKotsOutputForAirgap() } - if opts.ConfigValuesFile != "" { - installArgs = append(installArgs, "--config-values", opts.ConfigValuesFile) - } - - if opts.SkipPreflights { - installArgs = append(installArgs, "--skip-preflights") - } + installArgs := buildInstallArgs(opts, upstreamURI, appVersionLabel, licenseFile) if msg, ok := opts.Stdout.(*spinner.MessageWriter); ok && msg != nil { msg.SetMask(maskfn) diff --git a/cmd/installer/kotscli/kotscli_test.go b/cmd/installer/kotscli/kotscli_test.go new file mode 100644 index 0000000000..6c08f477ff --- /dev/null +++ b/cmd/installer/kotscli/kotscli_test.go @@ -0,0 +1,132 @@ +package kotscli + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_buildInstallArgs(t *testing.T) { + tests := []struct { + name string + opts InstallOptions + upstreamURI string + appVersionLabel string + licenseFile string + shouldContain []string + shouldNotContain []string + }{ + { + name: "with config values file", + opts: InstallOptions{ + Namespace: "kotsadm", + ConfigValuesFile: "/tmp/config-values.yaml", + }, + upstreamURI: "test-app", + appVersionLabel: "v1.0.0", + licenseFile: "/tmp/license", + shouldContain: []string{ + "install", + "test-app", + "--namespace", "kotsadm", + "--config-values", "/tmp/config-values.yaml", + "--exclude-admin-console", + }, + shouldNotContain: []string{}, + }, + { + name: "without config values file", + opts: InstallOptions{ + Namespace: "kotsadm", + ConfigValuesFile: "", + }, + upstreamURI: "test-app", + appVersionLabel: "v1.0.0", + licenseFile: "/tmp/license", + shouldContain: []string{ + "install", + "test-app", + "--namespace", "kotsadm", + }, + shouldNotContain: []string{ + "--config-values", + }, + }, + { + name: "with config values and airgap bundle", + opts: InstallOptions{ + Namespace: "kotsadm", + ConfigValuesFile: "/tmp/config-values.yaml", + AirgapBundle: "/tmp/airgap.tar.gz", + }, + upstreamURI: "test-app", + appVersionLabel: "v1.0.0", + licenseFile: "/tmp/license", + shouldContain: []string{ + "install", + "--config-values", "/tmp/config-values.yaml", + "--airgap-bundle", "/tmp/airgap.tar.gz", + }, + shouldNotContain: []string{}, + }, + { + name: "with all optional flags", + opts: InstallOptions{ + Namespace: "kotsadm", + ConfigValuesFile: "/tmp/config-values.yaml", + AirgapBundle: "/tmp/airgap.tar.gz", + SkipPreflights: true, + DisableImagePush: true, + }, + upstreamURI: "test-app", + appVersionLabel: "v1.0.0", + licenseFile: "/tmp/license", + shouldContain: []string{ + "--config-values", "/tmp/config-values.yaml", + "--airgap-bundle", "/tmp/airgap.tar.gz", + "--skip-preflights", + "--disable-image-push", + }, + shouldNotContain: []string{}, + }, + { + name: "config values file with special path characters", + opts: InstallOptions{ + Namespace: "kotsadm", + ConfigValuesFile: "/path/with-special_chars.123/config-values.yaml", + }, + upstreamURI: "test-app", + appVersionLabel: "v1.0.0", + licenseFile: "/tmp/license", + shouldContain: []string{ + "--config-values", + "/path/with-special_chars.123/config-values.yaml", + }, + shouldNotContain: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := require.New(t) + + args := buildInstallArgs(tt.opts, tt.upstreamURI, tt.appVersionLabel, tt.licenseFile) + + req.NotNil(args, "args should not be nil") + req.NotEmpty(args, "args should not be empty") + + argsString := strings.Join(args, " ") + + for _, expected := range tt.shouldContain { + req.Contains(argsString, expected, + "args should contain %q, got: %s", expected, argsString) + } + + for _, notExpected := range tt.shouldNotContain { + req.NotContains(argsString, notExpected, + "args should not contain %q, got: %s", notExpected, argsString) + } + }) + } +} diff --git a/e2e/install_test.go b/e2e/install_test.go index 6a72a883e0..4edaf71d19 100644 --- a/e2e/install_test.go +++ b/e2e/install_test.go @@ -1667,64 +1667,6 @@ func TestInstallSnapshotFromReplicatedApp(t *testing.T) { t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) } -// TestCustomCIDR tests the installation with an alternate CIDR range -func TestCustomCIDR(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 4, - Distro: "debian-bookworm", - LicensePath: "licenses/license.yaml", - ECBinaryPath: "../output/bin/embedded-cluster", - }) - defer tc.Cleanup() - t.Log("non-proxied infrastructure created") - - installSingleNodeWithOptions(t, tc, installOptions{ - podCidr: "10.128.0.0/20", - serviceCidr: "10.129.0.0/20", - }) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - // join a controller node - joinControllerNode(t, tc, 1) - - // XXX If we are too aggressive joining nodes we can see the following error being - // thrown by kotsadm on its log (and we get a 500 back): - // " - // failed to get controller role name: failed to get cluster config: failed to get - // current installation: failed to list installations: etcdserver: leader changed - // " - t.Logf("node 1 joined, sleeping...") - time.Sleep(30 * time.Second) - - // join another controller node - joinControllerNode(t, tc, 2) - - // join a worker node - joinWorkerNode(t, tc, 3) - - // wait for the nodes to report as ready. - waitForNodes(t, tc, 4, nil) - - checkInstallationState(t, tc) - - // ensure that the cluster is using the right IP ranges. - t.Logf("%s: checking service and pod IP addresses", time.Now().Format(time.RFC3339)) - stdout, stderr, err := tc.RunCommandOnNode(0, []string{"check-cidr-ranges.sh", "^10.128.[0-9]*.[0-9]", "^10.129.[0-9]*.[0-9]"}) - if err != nil { - t.Fatalf("fail to check addresses on node 0: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - func TestSingleNodeInstallationNoopUpgrade(t *testing.T) { t.Parallel() diff --git a/pkg-new/k0s/interface.go b/pkg-new/k0s/interface.go index ac5f145801..e0c9971021 100644 --- a/pkg-new/k0s/interface.go +++ b/pkg-new/k0s/interface.go @@ -37,7 +37,8 @@ type K0sInterface interface { GetStatus(ctx context.Context) (*K0sStatus, error) Install(rc runtimeconfig.RuntimeConfig, hostname string) error IsInstalled() (bool, error) - WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle string, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) + NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) + WriteK0sConfig(ctx context.Context, cfg *k0sv1beta1.ClusterConfig) error PatchK0sConfig(path string, patch string) error WaitForK0s() error } @@ -54,8 +55,12 @@ func IsInstalled() (bool, error) { return _k0s.IsInstalled() } -func WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle string, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { - return _k0s.WriteK0sConfig(ctx, networkInterface, airgapBundle, podCIDR, serviceCIDR, eucfg, mutate) +func NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { + return _k0s.NewK0sConfig(networkInterface, isAirgap, podCIDR, serviceCIDR, eucfg, mutate) +} + +func WriteK0sConfig(ctx context.Context, cfg *k0sv1beta1.ClusterConfig) error { + return _k0s.WriteK0sConfig(ctx, cfg) } func PatchK0sConfig(path string, patch string) error { diff --git a/pkg-new/k0s/k0s.go b/pkg-new/k0s/k0s.go index b88d686ca4..3c980ee269 100644 --- a/pkg-new/k0s/k0s.go +++ b/pkg-new/k0s/k0s.go @@ -95,7 +95,7 @@ func (k *K0s) IsInstalled() (bool, error) { } // NewK0sConfig creates a new k0sv1beta1.ClusterConfig object from the input parameters. -func NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { +func (k *K0s) NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { var embCfgSpec *ecv1beta1.ConfigSpec if embCfg := release.GetEmbeddedClusterConfig(); embCfg != nil { embCfgSpec = &embCfg.Spec @@ -136,35 +136,30 @@ func NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, servic // WriteK0sConfig creates a new k0s.yaml configuration file. The file is saved in the // global location (as returned by runtimeconfig.K0sConfigPath). If a file already sits // there, this function returns an error. -func (k *K0s) WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle string, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { - cfg, err := NewK0sConfig(networkInterface, airgapBundle != "", podCIDR, serviceCIDR, eucfg, mutate) - if err != nil { - return nil, fmt.Errorf("unable to create k0s config: %w", err) - } - +func (k *K0s) WriteK0sConfig(ctx context.Context, cfg *k0sv1beta1.ClusterConfig) error { cfgpath := runtimeconfig.K0sConfigPath if _, err := os.Stat(cfgpath); err == nil { - return nil, fmt.Errorf("configuration file already exists") + return fmt.Errorf("configuration file already exists") } if err := os.MkdirAll(filepath.Dir(cfgpath), 0755); err != nil { - return nil, fmt.Errorf("unable to create directory: %w", err) + return fmt.Errorf("unable to create directory: %w", err) } // This is necessary to install the previous version of k0s in e2e tests // TODO: remove this once the previous version is > 1.29 unstructured, err := helpers.K0sClusterConfigTo129Compat(cfg) if err != nil { - return nil, fmt.Errorf("unable to convert cluster config to 1.29 compat: %w", err) + return fmt.Errorf("unable to convert cluster config to 1.29 compat: %w", err) } data, err := k8syaml.Marshal(unstructured) if err != nil { - return nil, fmt.Errorf("unable to marshal config: %w", err) + return fmt.Errorf("unable to marshal config: %w", err) } if err := os.WriteFile(cfgpath, data, 0600); err != nil { - return nil, fmt.Errorf("unable to write config file: %w", err) + return fmt.Errorf("unable to write config file: %w", err) } - return cfg, nil + return nil } // applyUnsupportedOverrides applies overrides to the k0s configuration. Applies the diff --git a/pkg-new/k0s/mock.go b/pkg-new/k0s/mock.go index 1ab36da6d7..d64616c6d1 100644 --- a/pkg-new/k0s/mock.go +++ b/pkg-new/k0s/mock.go @@ -37,12 +37,21 @@ func (m *MockK0s) IsInstalled() (bool, error) { return args.Bool(0), args.Error(1) } -// WriteK0sConfig mocks the WriteK0sConfig method -func (m *MockK0s) WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle string, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { - args := m.Called(ctx, networkInterface, airgapBundle, podCIDR, serviceCIDR, eucfg, mutate) +// NewK0sConfig mocks the NewK0sConfig method +func (m *MockK0s) NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { + args := m.Called(networkInterface, isAirgap, podCIDR, serviceCIDR, eucfg, mutate) + if args.Get(0) == nil { + return nil, args.Error(1) + } return args.Get(0).(*k0sv1beta1.ClusterConfig), args.Error(1) } +// WriteK0sConfig mocks the WriteK0sConfig method +func (m *MockK0s) WriteK0sConfig(ctx context.Context, cfg *k0sv1beta1.ClusterConfig) error { + args := m.Called(ctx, cfg) + return args.Error(0) +} + // PatchK0sConfig mocks the PatchK0sConfig method func (m *MockK0s) PatchK0sConfig(path string, patch string) error { args := m.Called(path, patch) diff --git a/pkg/dryrun/k0s.go b/pkg/dryrun/k0s.go index 0a8c84f7f6..ee7a55ed65 100644 --- a/pkg/dryrun/k0s.go +++ b/pkg/dryrun/k0s.go @@ -27,8 +27,12 @@ func (c *K0s) IsInstalled() (bool, error) { return c.Status != nil, nil } -func (c *K0s) WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle string, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { - return k0s.New().WriteK0sConfig(ctx, networkInterface, airgapBundle, podCIDR, serviceCIDR, eucfg, mutate) // actual implementation accounts for dryrun +func (c *K0s) NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { + return k0s.New().NewK0sConfig(networkInterface, isAirgap, podCIDR, serviceCIDR, eucfg, mutate) // actual implementation accounts for dryrun +} + +func (c *K0s) WriteK0sConfig(ctx context.Context, cfg *k0sv1beta1.ClusterConfig) error { + return k0s.New().WriteK0sConfig(ctx, cfg) // actual implementation accounts for dryrun } func (c *K0s) PatchK0sConfig(path string, patch string) error {