diff --git a/src/k8s/api/v1/bootstrap_config.go b/src/k8s/api/v1/bootstrap_config.go new file mode 100644 index 000000000..7c5a18ffd --- /dev/null +++ b/src/k8s/api/v1/bootstrap_config.go @@ -0,0 +1,55 @@ +package v1 + +import ( + "encoding/json" + "fmt" +) + +// BootstrapConfig is used to seed cluster configuration when bootstrapping a new cluster. +type BootstrapConfig struct { + // ClusterConfig + ClusterConfig UserFacingClusterConfig `json:"cluster-config,omitempty" yaml:"cluster-config,omitempty"` + + // Seed configuration for the control plane (flat on purpose). Empty values are ignored + PodCIDR *string `json:"pod-cidr,omitempty" yaml:"pod-cidr,omitempty"` + ServiceCIDR *string `json:"service-cidr,omitempty" yaml:"service-cidr,omitempty"` + DisableRBAC *bool `json:"disable-rbac,omitempty" yaml:"disable-rbac,omitempty"` + SecurePort *int `json:"secure-port,omitempty" yaml:"secure-port,omitempty"` + CloudProvider *string `json:"cloud-provider,omitempty" yaml:"cloud-provider,omitempty"` + K8sDqlitePort *int `json:"k8s-dqlite-port,omitempty" yaml:"k8s-dqlite-port,omitempty"` + DatastoreType *string `json:"datastore-type,omitempty" yaml:"datastore-type,omitempty"` + DatastoreServers []string `json:"datastore-servers,omitempty" yaml:"datastore-servers,omitempty"` + DatastoreCACert *string `json:"datastore-ca-crt,omitempty" yaml:"datastore-ca-crt,omitempty"` + DatastoreClientCert *string `json:"datastore-client-crt,omitempty" yaml:"datastore-client-crt,omitempty"` + DatastoreClientKey *string `json:"datastore-client-key,omitempty" yaml:"datastore-client-key,omitempty"` + + // Seed configuration for certificates + ExtraSANs []string `json:"extra-sans,omitempty" yaml:"extra-sans,omitempty"` +} + +func (b *BootstrapConfig) GetDatastoreType() string { return getField(b.DatastoreType) } +func (b *BootstrapConfig) GetDatastoreCACert() string { return getField(b.DatastoreCACert) } +func (b *BootstrapConfig) GetDatastoreClientCert() string { return getField(b.DatastoreClientCert) } +func (b *BootstrapConfig) GetDatastoreClientKey() string { return getField(b.DatastoreClientKey) } +func (b *BootstrapConfig) GetK8sDqlitePort() int { return getField(b.K8sDqlitePort) } + +// ToMicrocluster converts a BootstrapConfig to a map[string]string for use in microcluster. +func (b *BootstrapConfig) ToMicrocluster() (map[string]string, error) { + config, err := json.Marshal(b) + if err != nil { + return nil, fmt.Errorf("failed to marshal bootstrap config: %w", err) + } + + return map[string]string{ + "bootstrapConfig": string(config), + }, nil +} + +// BootstrapConfigFromMicrocluster parses a microcluster map[string]string and retrieves the BootstrapConfig. +func BootstrapConfigFromMicrocluster(m map[string]string) (BootstrapConfig, error) { + config := BootstrapConfig{} + if err := json.Unmarshal([]byte(m["bootstrapConfig"]), &config); err != nil { + return BootstrapConfig{}, fmt.Errorf("failed to unmarshal bootstrap config: %w", err) + } + return config, nil +} diff --git a/src/k8s/api/v1/bootstrap_config_test.go b/src/k8s/api/v1/bootstrap_config_test.go new file mode 100644 index 000000000..5afbc2c8b --- /dev/null +++ b/src/k8s/api/v1/bootstrap_config_test.go @@ -0,0 +1,59 @@ +package v1_test + +import ( + "testing" + + apiv1 "github.com/canonical/k8s/api/v1" + "github.com/canonical/k8s/pkg/utils/vals" + . "github.com/onsi/gomega" +) + +func TestBootstrapConfigToMicrocluster(t *testing.T) { + g := NewWithT(t) + + cfg := apiv1.BootstrapConfig{ + ClusterConfig: apiv1.UserFacingClusterConfig{ + Network: apiv1.NetworkConfig{ + Enabled: vals.Pointer(true), + }, + DNS: apiv1.DNSConfig{ + Enabled: vals.Pointer(true), + ClusterDomain: vals.Pointer("cluster.local"), + }, + Ingress: apiv1.IngressConfig{ + Enabled: vals.Pointer(true), + }, + LoadBalancer: apiv1.LoadBalancerConfig{ + Enabled: vals.Pointer(true), + L2Mode: vals.Pointer(true), + CIDRs: vals.Pointer([]string{"10.0.0.0/24"}), + }, + LocalStorage: apiv1.LocalStorageConfig{ + Enabled: vals.Pointer(true), + LocalPath: vals.Pointer("/storage/path"), + SetDefault: vals.Pointer(false), + }, + Gateway: apiv1.GatewayConfig{ + Enabled: vals.Pointer(true), + }, + MetricsServer: apiv1.MetricsServerConfig{ + Enabled: vals.Pointer(true), + }, + }, + PodCIDR: vals.Pointer("10.100.0.0/16"), + ServiceCIDR: vals.Pointer("10.200.0.0/16"), + DisableRBAC: vals.Pointer(false), + SecurePort: vals.Pointer(6443), + CloudProvider: vals.Pointer("external"), + K8sDqlitePort: vals.Pointer(9090), + DatastoreType: vals.Pointer("k8s-dqlite"), + ExtraSANs: []string{"custom.kubernetes"}, + } + + microclusterConfig, err := cfg.ToMicrocluster() + g.Expect(err).To(BeNil()) + + fromMicrocluster, err := apiv1.BootstrapConfigFromMicrocluster(microclusterConfig) + g.Expect(err).To(BeNil()) + g.Expect(fromMicrocluster).To(Equal(cfg)) +} diff --git a/src/k8s/api/v1/types.go b/src/k8s/api/v1/types.go index c91cc985d..9eef29b9b 100644 --- a/src/k8s/api/v1/types.go +++ b/src/k8s/api/v1/types.go @@ -4,60 +4,9 @@ import ( "fmt" "strings" - "github.com/canonical/k8s/pkg/utils/vals" "gopkg.in/yaml.v2" ) -type BootstrapConfig struct { - // Components are the components that should be enabled on bootstrap. - Components []string `yaml:"components"` - // ClusterCIDR is the CIDR of the cluster. - ClusterCIDR string `yaml:"cluster-cidr"` - // ServiceCIDR is the CIDR of the cluster services. - ServiceCIDR string `yaml:"service-cidr"` - // EnableRBAC determines if RBAC will be enabled; *bool to know true/false/unset. - EnableRBAC *bool `yaml:"enable-rbac"` - K8sDqlitePort int `yaml:"k8s-dqlite-port"` - Datastore string `yaml:"datastore"` - DatastoreURL string `yaml:"datastore-url,omitempty"` - DatastoreCACert string `yaml:"datastore-ca-crt,omitempty"` - DatastoreClientCert string `yaml:"datastore-client-crt,omitempty"` - DatastoreClientKey string `yaml:"datastore-client-key,omitempty"` - ExtraSANs []string `yaml:"extrasans,omitempty"` -} - -// SetDefaults sets the fields to default values. -func (b *BootstrapConfig) SetDefaults() { - b.Components = []string{"dns", "metrics-server", "network", "gateway"} - b.ClusterCIDR = "10.1.0.0/16" - b.ServiceCIDR = "10.152.183.0/24" - b.EnableRBAC = vals.Pointer(true) - b.K8sDqlitePort = 9000 - b.Datastore = "k8s-dqlite" -} - -// ToMap marshals the BootstrapConfig into yaml and map it to "bootstrapConfig". -func (b *BootstrapConfig) ToMap() (map[string]string, error) { - config, err := yaml.Marshal(b) - if err != nil { - return nil, fmt.Errorf("failed to marshal config map: %w", err) - } - - return map[string]string{ - "bootstrapConfig": string(config), - }, nil -} - -// BootstrapConfigFromMap converts a string map to a BootstrapConfig struct. -func BootstrapConfigFromMap(m map[string]string) (*BootstrapConfig, error) { - config := &BootstrapConfig{} - err := yaml.Unmarshal([]byte(m["bootstrapConfig"]), config) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal bootstrap config: %w", err) - } - return config, nil -} - type ClusterRole string const ( diff --git a/src/k8s/api/v1/types_test.go b/src/k8s/api/v1/types_test.go index b26201821..adb262049 100644 --- a/src/k8s/api/v1/types_test.go +++ b/src/k8s/api/v1/types_test.go @@ -7,49 +7,6 @@ import ( . "github.com/onsi/gomega" ) -// This is expected to break if the default changes to make sure this is done intentionally. -func TestSetDefaults(t *testing.T) { - g := NewWithT(t) - - b := &BootstrapConfig{} - b.SetDefaults() - - expected := &BootstrapConfig{ - Components: []string{"dns", "metrics-server", "network", "gateway"}, - ClusterCIDR: "10.1.0.0/16", - ServiceCIDR: "10.152.183.0/24", - EnableRBAC: vals.Pointer(true), - K8sDqlitePort: 9000, - Datastore: "k8s-dqlite", - } - - g.Expect(b).To(Equal(expected)) -} - -func TestBootstrapConfigFromMap(t *testing.T) { - g := NewWithT(t) - // Create a new BootstrapConfig with default values - bc := &BootstrapConfig{ - ClusterCIDR: "10.1.0.0/16", - Components: []string{"dns", "network", "storage"}, - EnableRBAC: vals.Pointer(true), - K8sDqlitePort: 9000, - } - - // Convert the BootstrapConfig to a map - m, err := bc.ToMap() - g.Expect(err).To(BeNil()) - - // Unmarshal the YAML string from the map into a new BootstrapConfig instance - bcyaml, err := BootstrapConfigFromMap(m) - - // Check for errors - g.Expect(err).To(BeNil()) - // Compare the unmarshaled BootstrapConfig with the original one - g.Expect(bcyaml).To(Equal(bc)) // Note the *bc here to compare values, not pointers - -} - func TestHaClusterFormed(t *testing.T) { g := NewWithT(t) diff --git a/src/k8s/cmd/k8s/k8s_bootstrap.go b/src/k8s/cmd/k8s/k8s_bootstrap.go index 7a291533d..2957427b3 100644 --- a/src/k8s/cmd/k8s/k8s_bootstrap.go +++ b/src/k8s/cmd/k8s/k8s_bootstrap.go @@ -8,11 +8,13 @@ import ( "os" "slices" "strings" + "unicode" apiv1 "github.com/canonical/k8s/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" "github.com/canonical/k8s/pkg/config" "github.com/canonical/k8s/pkg/utils" + "github.com/canonical/k8s/pkg/utils/vals" "github.com/canonical/lxd/lxd/util" "github.com/spf13/cobra" "gopkg.in/yaml.v2" @@ -81,7 +83,7 @@ func newBootstrapCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } - bootstrapConfig := apiv1.BootstrapConfig{} + var bootstrapConfig apiv1.BootstrapConfig switch { case opts.interactive: bootstrapConfig = getConfigInteractively(env.Stdin, env.Stdout, env.Stderr) @@ -93,7 +95,23 @@ func newBootstrapCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } default: - bootstrapConfig.SetDefaults() + // Default bootstrap configuration + bootstrapConfig = apiv1.BootstrapConfig{ + ClusterConfig: apiv1.UserFacingClusterConfig{ + Network: apiv1.NetworkConfig{ + Enabled: vals.Pointer(true), + }, + DNS: apiv1.DNSConfig{ + Enabled: vals.Pointer(true), + }, + Gateway: apiv1.GatewayConfig{ + Enabled: vals.Pointer(true), + }, + MetricsServer: apiv1.MetricsServerConfig{ + Enabled: vals.Pointer(true), + }, + }, + } } cmd.PrintErrln("Bootstrapping the cluster. This may take a few seconds, please wait.") @@ -126,17 +144,14 @@ func newBootstrapCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { } func getConfigFromYaml(filePath string) (apiv1.BootstrapConfig, error) { - config := apiv1.BootstrapConfig{} - config.SetDefaults() - - yamlContent, err := os.ReadFile(filePath) + b, err := os.ReadFile(filePath) if err != nil { - return config, fmt.Errorf("failed to read YAML config file: %w", err) + return apiv1.BootstrapConfig{}, fmt.Errorf("failed to read file: %w", err) } - err = yaml.Unmarshal(yamlContent, &config) - if err != nil { - return config, fmt.Errorf("failed to parse YAML config file: %w", err) + var config apiv1.BootstrapConfig + if err := yaml.UnmarshalStrict(b, &config); err != nil { + return apiv1.BootstrapConfig{}, fmt.Errorf("failed to parse YAML config file: %w", err) } return config, nil @@ -144,21 +159,41 @@ func getConfigFromYaml(filePath string) (apiv1.BootstrapConfig, error) { func getConfigInteractively(stdin io.Reader, stdout io.Writer, stderr io.Writer) apiv1.BootstrapConfig { config := apiv1.BootstrapConfig{} - config.SetDefaults() components := askQuestion( stdin, stdout, stderr, "Which components would you like to enable?", componentList, - strings.Join(config.Components, ", "), + "network, dns, gateway, metrics-server", nil, ) - config.Components = strings.Split(components, ",") + for _, component := range strings.FieldsFunc(components, func(r rune) bool { return unicode.IsSpace(r) || r == ',' }) { + switch component { + case "network": + config.ClusterConfig.Network.Enabled = vals.Pointer(true) + case "dns": + config.ClusterConfig.DNS.Enabled = vals.Pointer(true) + case "ingress": + config.ClusterConfig.Ingress.Enabled = vals.Pointer(true) + case "load-balancer": + config.ClusterConfig.LoadBalancer.Enabled = vals.Pointer(true) + case "gateway": + config.ClusterConfig.Gateway.Enabled = vals.Pointer(true) + case "local-storage": + config.ClusterConfig.LocalStorage.Enabled = vals.Pointer(true) + case "metrics-server": + config.ClusterConfig.MetricsServer.Enabled = vals.Pointer(true) + } + } + + podCIDR := askQuestion(stdin, stdout, stderr, "Please set the Pod CIDR:", nil, "10.1.0.0/16", nil) + serviceCIDR := askQuestion(stdin, stdout, stderr, "Please set the Service CIDR:", nil, "10.152.183.0/24", nil) + + config.PodCIDR = vals.Pointer(podCIDR) + config.ServiceCIDR = vals.Pointer(serviceCIDR) + + // TODO: any other configs we care about in the interactive bootstrap? - config.ClusterCIDR = askQuestion(stdin, stdout, stderr, "Please set the Cluster CIDR:", nil, config.ClusterCIDR, nil) - config.ServiceCIDR = askQuestion(stdin, stdout, stderr, "Please set the Service CIDR:", nil, config.ServiceCIDR, nil) - rbac := askBool(stdin, stdout, stderr, "Enable Role Based Access Control (RBAC)?", []string{"yes", "no"}, "yes") - *config.EnableRBAC = rbac return config } @@ -214,18 +249,3 @@ func askQuestion(stdin io.Reader, stdout io.Writer, stderr io.Writer, question s return s } } - -// askBool asks a question and expect a yes/no answer. -func askBool(stdin io.Reader, stdout io.Writer, stderr io.Writer, question string, options []string, defaultVal string) bool { - for { - answer := askQuestion(stdin, stdout, stderr, question, options, defaultVal, nil) - - if utils.ValueInSlice(strings.ToLower(answer), []string{"yes", "y"}) { - return true - } else if utils.ValueInSlice(strings.ToLower(answer), []string{"no", "n"}) { - return false - } - - fmt.Fprintf(stderr, "Invalid input, try again.\n\n") - } -} diff --git a/src/k8s/cmd/k8s/k8s_bootstrap_test.go b/src/k8s/cmd/k8s/k8s_bootstrap_test.go index 3257c41a1..806c26d90 100644 --- a/src/k8s/cmd/k8s/k8s_bootstrap_test.go +++ b/src/k8s/cmd/k8s/k8s_bootstrap_test.go @@ -1,6 +1,7 @@ package k8s import ( + _ "embed" "os" "path/filepath" "testing" @@ -10,6 +11,15 @@ import ( . "github.com/onsi/gomega" ) +var ( + //go:embed testdata/bootstrap-config-full.yaml + bootstrapConfigFull string + //go:embed testdata/bootstrap-config-some.yaml + bootstrapConfigSome string + //go:embed testdata/bootstrap-config-invalid-keys.yaml + bootstrapConfigInvalidKeys string +) + type testCase struct { name string yamlConfig string @@ -19,45 +29,62 @@ type testCase struct { var testCases = []testCase{ { - name: "CompleteConfig", - yamlConfig: ` -components: - - network - - dns - - gateway - - ingress - - storage - - metrics-server -cluster-cidr: "10.244.0.0/16" -service-cidr: "10.152.100.0/24" -enable-rbac: true -k8s-dqlite-port: 12379`, + name: "FullConfig", + yamlConfig: bootstrapConfigFull, expectedConfig: apiv1.BootstrapConfig{ - Components: []string{"network", "dns", "gateway", "ingress", "storage", "metrics-server"}, - ClusterCIDR: "10.244.0.0/16", - ServiceCIDR: "10.152.100.0/24", - EnableRBAC: vals.Pointer(true), - K8sDqlitePort: 12379, - Datastore: "k8s-dqlite", + ClusterConfig: apiv1.UserFacingClusterConfig{ + Network: apiv1.NetworkConfig{ + Enabled: vals.Pointer(true), + }, + DNS: apiv1.DNSConfig{ + Enabled: vals.Pointer(true), + ClusterDomain: vals.Pointer("cluster.local"), + }, + Ingress: apiv1.IngressConfig{ + Enabled: vals.Pointer(true), + }, + LoadBalancer: apiv1.LoadBalancerConfig{ + Enabled: vals.Pointer(true), + L2Mode: vals.Pointer(true), + CIDRs: vals.Pointer([]string{"10.0.0.0/24"}), + }, + LocalStorage: apiv1.LocalStorageConfig{ + Enabled: vals.Pointer(true), + LocalPath: vals.Pointer("/storage/path"), + SetDefault: vals.Pointer(false), + }, + Gateway: apiv1.GatewayConfig{ + Enabled: vals.Pointer(true), + }, + MetricsServer: apiv1.MetricsServerConfig{ + Enabled: vals.Pointer(true), + }, + }, + PodCIDR: vals.Pointer("10.100.0.0/16"), + ServiceCIDR: vals.Pointer("10.200.0.0/16"), + DisableRBAC: vals.Pointer(false), + SecurePort: vals.Pointer(6443), + CloudProvider: vals.Pointer("external"), + K8sDqlitePort: vals.Pointer(9090), + DatastoreType: vals.Pointer("k8s-dqlite"), + ExtraSANs: []string{"custom.kubernetes"}, }, }, { - name: "IncompleteConfig", - yamlConfig: ` -cluster-cidr: "10.244.0.0/16" -enable-rbac: true -bananas: 5`, + name: "SomeConfig", + yamlConfig: bootstrapConfigSome, expectedConfig: apiv1.BootstrapConfig{ - Components: []string{"dns", "metrics-server", "network", "gateway"}, - ClusterCIDR: "10.244.0.0/16", - ServiceCIDR: "10.152.183.0/24", - EnableRBAC: vals.Pointer(true), - K8sDqlitePort: 9000, - Datastore: "k8s-dqlite", + PodCIDR: vals.Pointer("10.100.0.0/16"), + ServiceCIDR: vals.Pointer("10.152.200.0/24"), }, }, { - name: "InvalidYaml", + name: "InvalidKeys", + yamlConfig: bootstrapConfigInvalidKeys, + expectedError: "field cluster-cidr not found in type v1.BootstrapConfig", + }, + { + name: "InvalidYAML", yamlConfig: "this is not valid yaml", expectedError: "failed to parse YAML config file", }, diff --git a/src/k8s/cmd/k8s/testdata/bootstrap-config-full.yaml b/src/k8s/cmd/k8s/testdata/bootstrap-config-full.yaml new file mode 100644 index 000000000..dbbfdecd6 --- /dev/null +++ b/src/k8s/cmd/k8s/testdata/bootstrap-config-full.yaml @@ -0,0 +1,30 @@ +cluster-config: + network: + enabled: true + dns: + enabled: true + cluster-domain: cluster.local + ingress: + enabled: true + load-balancer: + enabled: true + cidrs: + - 10.0.0.0/24 + l2-mode: true + local-storage: + enabled: true + local-path: /storage/path + set-default: false + gateway: + enabled: true + metrics-server: + enabled: true +pod-cidr: 10.100.0.0/16 +service-cidr: 10.200.0.0/16 +disable-rbac: false +secure-port: 6443 +cloud-provider: external +k8s-dqlite-port: 9090 +datastore-type: k8s-dqlite +extra-sans: +- custom.kubernetes diff --git a/src/k8s/cmd/k8s/testdata/bootstrap-config-invalid-keys.yaml b/src/k8s/cmd/k8s/testdata/bootstrap-config-invalid-keys.yaml new file mode 100644 index 000000000..a98d86861 --- /dev/null +++ b/src/k8s/cmd/k8s/testdata/bootstrap-config-invalid-keys.yaml @@ -0,0 +1,2 @@ +cluster-cidr: "10.244.0.0/16" +disable-rbac: true diff --git a/src/k8s/cmd/k8s/testdata/bootstrap-config-some.yaml b/src/k8s/cmd/k8s/testdata/bootstrap-config-some.yaml new file mode 100644 index 000000000..e923f5084 --- /dev/null +++ b/src/k8s/cmd/k8s/testdata/bootstrap-config-some.yaml @@ -0,0 +1,2 @@ +pod-cidr: "10.100.0.0/16" +service-cidr: "10.152.200.0/24" diff --git a/src/k8s/pkg/k8sd/api/cluster_bootstrap.go b/src/k8s/pkg/k8sd/api/cluster_bootstrap.go index d45ebe898..fbb818c42 100644 --- a/src/k8s/pkg/k8sd/api/cluster_bootstrap.go +++ b/src/k8s/pkg/k8sd/api/cluster_bootstrap.go @@ -17,12 +17,10 @@ func (e *Endpoints) postClusterBootstrap(s *state.State, r *http.Request) respon return response.BadRequest(fmt.Errorf("failed to parse request: %w", err)) } - req.Config.SetDefaults() - //Convert Bootstrap config to map - config, err := req.Config.ToMap() + config, err := req.Config.ToMicrocluster() if err != nil { - return response.BadRequest(fmt.Errorf("failed to convert bootstrap config to map: %w", err)) + return response.BadRequest(fmt.Errorf("failed to prepare bootstrap config: %w", err)) } // Clean hostname diff --git a/src/k8s/pkg/k8sd/app/hooks_bootstrap.go b/src/k8s/pkg/k8sd/app/hooks_bootstrap.go index a8d229af5..58431b7f2 100644 --- a/src/k8s/pkg/k8sd/app/hooks_bootstrap.go +++ b/src/k8s/pkg/k8sd/app/hooks_bootstrap.go @@ -147,12 +147,15 @@ func (a *App) onBootstrapWorkerNode(s *state.State, encodedToken string) error { func (a *App) onBootstrapControlPlane(s *state.State, initConfig map[string]string) error { snap := a.Snap() - bootstrapConfig, err := apiv1.BootstrapConfigFromMap(initConfig) + bootstrapConfig, err := apiv1.BootstrapConfigFromMicrocluster(initConfig) if err != nil { return fmt.Errorf("failed to unmarshal bootstrap config: %w", err) } - cfg := types.ClusterConfigFromBootstrapConfig(bootstrapConfig) + cfg, err := types.ClusterConfigFromBootstrapConfig(bootstrapConfig) + if err != nil { + return fmt.Errorf("invalid bootstrap config: %w", err) + } cfg.SetDefaults() if err := cfg.Validate(); err != nil { return fmt.Errorf("invalid cluster configuration: %w", err) diff --git a/src/k8s/pkg/k8sd/types/cluster_config_convert.go b/src/k8s/pkg/k8sd/types/cluster_config_convert.go index dff8b03a9..90d064a56 100644 --- a/src/k8s/pkg/k8sd/types/cluster_config_convert.go +++ b/src/k8s/pkg/k8sd/types/cluster_config_convert.go @@ -1,63 +1,71 @@ package types import ( + "fmt" + "strings" + apiv1 "github.com/canonical/k8s/api/v1" "github.com/canonical/k8s/pkg/utils/vals" ) // ClusterConfigFromBootstrapConfig converts BootstrapConfig from public API into a ClusterConfig. -func ClusterConfigFromBootstrapConfig(b *apiv1.BootstrapConfig) ClusterConfig { - var config ClusterConfig +func ClusterConfigFromBootstrapConfig(b apiv1.BootstrapConfig) (ClusterConfig, error) { + config := ClusterConfigFromUserFacing(b.ClusterConfig) - authorizationMode := "Node,RBAC" - if !vals.OptionalBool(b.EnableRBAC, true) { - authorizationMode = "AlwaysAllow" + // APIServer + config.APIServer.SecurePort = b.SecurePort + if b.DisableRBAC != nil && *b.DisableRBAC { + config.APIServer.AuthorizationMode = vals.Pointer("AlwaysAllow") + } else { + config.APIServer.AuthorizationMode = vals.Pointer("Node,RBAC") } - config.APIServer.AuthorizationMode = vals.Pointer(authorizationMode) - switch b.Datastore { + // Datastore + switch b.GetDatastoreType() { case "", "k8s-dqlite": + if len(b.DatastoreServers) > 0 { + return ClusterConfig{}, fmt.Errorf("datastore-servers needs datastore-type to be external, not %q", b.GetDatastoreType()) + } + if b.GetDatastoreCACert() != "" { + return ClusterConfig{}, fmt.Errorf("datastore-ca-crt needs datastore-type to be external, not %q", b.GetDatastoreType()) + } + if b.GetDatastoreClientCert() != "" { + return ClusterConfig{}, fmt.Errorf("datastore-client-crt needs datastore-type to be external, not %q", b.GetDatastoreType()) + } + if b.GetDatastoreClientKey() != "" { + return ClusterConfig{}, fmt.Errorf("datastore-client-key needs datastore-type to be external, not %q", b.GetDatastoreType()) + } + config.Datastore = Datastore{ Type: vals.Pointer("k8s-dqlite"), - K8sDqlitePort: vals.Pointer(b.K8sDqlitePort), + K8sDqlitePort: b.K8sDqlitePort, } case "external": + if len(b.DatastoreServers) == 0 { + return ClusterConfig{}, fmt.Errorf("datastore type is external but no datastore servers were set") + } + if b.GetK8sDqlitePort() != 0 { + return ClusterConfig{}, fmt.Errorf("k8s-dqlite-port needs datastore-type to be k8s-dqlite") + } config.Datastore = Datastore{ Type: vals.Pointer("external"), - ExternalURL: vals.Pointer(b.DatastoreURL), - ExternalCACert: vals.Pointer(b.DatastoreCACert), - ExternalClientCert: vals.Pointer(b.DatastoreClientCert), - ExternalClientKey: vals.Pointer(b.DatastoreClientKey), + ExternalURL: vals.Pointer(strings.Join(b.DatastoreServers, ",")), + ExternalCACert: b.DatastoreCACert, + ExternalClientCert: b.DatastoreClientCert, + ExternalClientKey: b.DatastoreClientKey, } + default: + return ClusterConfig{}, fmt.Errorf("unknown datastore type specified in bootstrap config %q", b.GetDatastoreType()) } - if b.ClusterCIDR != "" { - config.Network.PodCIDR = vals.Pointer(b.ClusterCIDR) - } - if b.ServiceCIDR != "" { - config.Network.ServiceCIDR = vals.Pointer(b.ServiceCIDR) - } + // Network + config.Network.PodCIDR = b.PodCIDR + config.Network.ServiceCIDR = b.ServiceCIDR - for _, component := range b.Components { - switch component { - case "network": - config.Network.Enabled = vals.Pointer(true) - case "dns": - config.DNS.Enabled = vals.Pointer(true) - case "local-storage": - config.LocalStorage.Enabled = vals.Pointer(true) - case "ingress": - config.Ingress.Enabled = vals.Pointer(true) - case "gateway": - config.Gateway.Enabled = vals.Pointer(true) - case "metrics-server": - config.MetricsServer.Enabled = vals.Pointer(true) - case "load-balancer": - config.LoadBalancer.Enabled = vals.Pointer(true) - } - } + // Kubelet + config.Kubelet.CloudProvider = b.CloudProvider - return config + return config, nil } // ClusterConfigFromUserFacing converts UserFacingClusterConfig from public API into a ClusterConfig. diff --git a/src/k8s/pkg/k8sd/types/cluster_config_convert_test.go b/src/k8s/pkg/k8sd/types/cluster_config_convert_test.go index 6983612d5..976a9198c 100644 --- a/src/k8s/pkg/k8sd/types/cluster_config_convert_test.go +++ b/src/k8s/pkg/k8sd/types/cluster_config_convert_test.go @@ -10,54 +10,226 @@ import ( ) func TestClusterConfigFromBootstrapConfig(t *testing.T) { - t.Run("Default", func(t *testing.T) { - g := NewWithT(t) - - bootstrapConfig := &apiv1.BootstrapConfig{ - ClusterCIDR: "10.1.0.0/16", - ServiceCIDR: "10.152.183.0/24", - Components: []string{"dns", "network"}, - EnableRBAC: vals.Pointer(true), - K8sDqlitePort: 12345, - } - - expectedConfig := types.ClusterConfig{ - APIServer: types.APIServer{ - AuthorizationMode: vals.Pointer("Node,RBAC"), + for _, tc := range []struct { + name string + bootstrap apiv1.BootstrapConfig + expectConfig types.ClusterConfig + }{ + { + name: "Nil", + expectConfig: types.ClusterConfig{ + APIServer: types.APIServer{ + AuthorizationMode: vals.Pointer("Node,RBAC"), + }, + Datastore: types.Datastore{ + Type: vals.Pointer("k8s-dqlite"), + }, }, - Datastore: types.Datastore{ - Type: vals.Pointer("k8s-dqlite"), - K8sDqlitePort: vals.Pointer(12345), + }, + { + name: "DisableRBAC", + bootstrap: apiv1.BootstrapConfig{ + DisableRBAC: vals.Pointer(true), }, - Network: types.Network{ - Enabled: vals.Pointer(true), - PodCIDR: vals.Pointer("10.1.0.0/16"), - ServiceCIDR: vals.Pointer("10.152.183.0/24"), + expectConfig: types.ClusterConfig{ + APIServer: types.APIServer{ + AuthorizationMode: vals.Pointer("AlwaysAllow"), + }, + Datastore: types.Datastore{ + Type: vals.Pointer("k8s-dqlite"), + }, }, - DNS: types.DNS{ - Enabled: vals.Pointer(true), + }, + { + name: "K8sDqliteDefault", + bootstrap: apiv1.BootstrapConfig{ + DatastoreType: vals.Pointer(""), }, - } + expectConfig: types.ClusterConfig{ + APIServer: types.APIServer{ + AuthorizationMode: vals.Pointer("Node,RBAC"), + }, + Datastore: types.Datastore{ + Type: vals.Pointer("k8s-dqlite"), + }, + }, + }, + { + name: "ExternalDatastore", + bootstrap: apiv1.BootstrapConfig{ + DatastoreType: vals.Pointer("external"), + DatastoreServers: []string{"https://10.0.0.1:2379", "https://10.0.0.2:2379"}, + DatastoreCACert: vals.Pointer("CA DATA"), + DatastoreClientCert: vals.Pointer("CERT DATA"), + DatastoreClientKey: vals.Pointer("KEY DATA"), + }, + expectConfig: types.ClusterConfig{ + APIServer: types.APIServer{ + AuthorizationMode: vals.Pointer("Node,RBAC"), + }, + Datastore: types.Datastore{ + Type: vals.Pointer("external"), + ExternalURL: vals.Pointer("https://10.0.0.1:2379,https://10.0.0.2:2379"), + ExternalCACert: vals.Pointer("CA DATA"), + ExternalClientCert: vals.Pointer("CERT DATA"), + ExternalClientKey: vals.Pointer("KEY DATA"), + }, + }, + }, + { + name: "Full", + bootstrap: apiv1.BootstrapConfig{ + ClusterConfig: apiv1.UserFacingClusterConfig{ + Network: apiv1.NetworkConfig{ + Enabled: vals.Pointer(true), + }, + DNS: apiv1.DNSConfig{ + Enabled: vals.Pointer(true), + ClusterDomain: vals.Pointer("cluster.local"), + }, + Ingress: apiv1.IngressConfig{ + Enabled: vals.Pointer(true), + }, + LoadBalancer: apiv1.LoadBalancerConfig{ + Enabled: vals.Pointer(true), + L2Mode: vals.Pointer(true), + CIDRs: vals.Pointer([]string{"10.0.0.0/24"}), + }, + LocalStorage: apiv1.LocalStorageConfig{ + Enabled: vals.Pointer(true), + LocalPath: vals.Pointer("/storage/path"), + SetDefault: vals.Pointer(false), + }, + Gateway: apiv1.GatewayConfig{ + Enabled: vals.Pointer(true), + }, + MetricsServer: apiv1.MetricsServerConfig{ + Enabled: vals.Pointer(true), + }, + }, + PodCIDR: vals.Pointer("10.100.0.0/16"), + ServiceCIDR: vals.Pointer("10.200.0.0/16"), + DisableRBAC: vals.Pointer(false), + SecurePort: vals.Pointer(6443), + CloudProvider: vals.Pointer("external"), + K8sDqlitePort: vals.Pointer(9090), + DatastoreType: vals.Pointer("k8s-dqlite"), + ExtraSANs: []string{"custom.kubernetes"}, + }, + expectConfig: types.ClusterConfig{ + Datastore: types.Datastore{ + Type: vals.Pointer("k8s-dqlite"), + K8sDqlitePort: vals.Pointer(9090), + }, + APIServer: types.APIServer{ + SecurePort: vals.Pointer(6443), + AuthorizationMode: vals.Pointer("Node,RBAC"), + }, + Kubelet: types.Kubelet{ + ClusterDomain: vals.Pointer("cluster.local"), + CloudProvider: vals.Pointer("external"), + }, + Network: types.Network{ + Enabled: vals.Pointer(true), + PodCIDR: vals.Pointer("10.100.0.0/16"), + ServiceCIDR: vals.Pointer("10.200.0.0/16"), + }, + DNS: types.DNS{ + Enabled: vals.Pointer(true), + }, + Ingress: types.Ingress{ + Enabled: vals.Pointer(true), + }, + LoadBalancer: types.LoadBalancer{ + Enabled: vals.Pointer(true), + L2Mode: vals.Pointer(true), + CIDRs: vals.Pointer([]string{"10.0.0.0/24"}), + }, + LocalStorage: types.LocalStorage{ + Enabled: vals.Pointer(true), + LocalPath: vals.Pointer("/storage/path"), + SetDefault: vals.Pointer(false), + }, + Gateway: types.Gateway{ + Enabled: vals.Pointer(true), + }, + MetricsServer: types.MetricsServer{ + Enabled: vals.Pointer(true), + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) - g.Expect(types.ClusterConfigFromBootstrapConfig(bootstrapConfig)).To(Equal(expectedConfig)) - }) + config, err := types.ClusterConfigFromBootstrapConfig(tc.bootstrap) + g.Expect(err).To(BeNil()) + g.Expect(config).To(Equal(tc.expectConfig)) + }) + } - t.Run("RBAC", func(t *testing.T) { + t.Run("Invalid", func(t *testing.T) { for _, tc := range []struct { - name string - enableRBAC *bool - expectedAuthorizationMode *string + name string + bootstrap apiv1.BootstrapConfig }{ - {name: "EnableRBAC=true", enableRBAC: vals.Pointer(true), expectedAuthorizationMode: vals.Pointer("Node,RBAC")}, - {name: "EnableRBAC=false", enableRBAC: vals.Pointer(false), expectedAuthorizationMode: vals.Pointer("AlwaysAllow")}, - {name: "EnableRBAC=nil", enableRBAC: nil, expectedAuthorizationMode: vals.Pointer("Node,RBAC")}, + { + name: "K8sDqliteWithExternalServers", + bootstrap: apiv1.BootstrapConfig{ + DatastoreType: vals.Pointer(""), + DatastoreServers: []string{"http://10.0.0.1:2379"}, + }, + }, + { + name: "K8sDqliteWithExternalCA", + bootstrap: apiv1.BootstrapConfig{ + DatastoreType: vals.Pointer(""), + DatastoreCACert: vals.Pointer("CA DATA"), + }, + }, + { + name: "K8sDqliteWithExternalClientCert", + bootstrap: apiv1.BootstrapConfig{ + DatastoreType: vals.Pointer(""), + DatastoreClientCert: vals.Pointer("CERT DATA"), + }, + }, + { + name: "K8sDqliteWithExternalClientKey", + bootstrap: apiv1.BootstrapConfig{ + DatastoreType: vals.Pointer(""), + DatastoreClientKey: vals.Pointer("KEY DATA"), + }, + }, + { + name: "ExternalWithK8sDqlitePort", + bootstrap: apiv1.BootstrapConfig{ + DatastoreType: vals.Pointer("external"), + DatastoreServers: []string{"http://10.0.0.1:2379"}, + K8sDqlitePort: vals.Pointer(18080), + }, + }, + { + name: "ExternalWithoutServers", + bootstrap: apiv1.BootstrapConfig{ + DatastoreType: vals.Pointer("external"), + }, + }, + { + name: "UnsupportedDatastore", + bootstrap: apiv1.BootstrapConfig{ + DatastoreType: vals.Pointer("unknown"), + }, + }, } { - t.Run(tc.name, func(t *testing.T) { g := NewWithT(t) - c := types.ClusterConfigFromBootstrapConfig(&apiv1.BootstrapConfig{EnableRBAC: tc.enableRBAC}) - g.Expect(c.APIServer.AuthorizationMode).To(Equal(tc.expectedAuthorizationMode)) + + config, err := types.ClusterConfigFromBootstrapConfig(tc.bootstrap) + g.Expect(config).To(BeZero()) + g.Expect(err).To(HaveOccurred()) }) } + }) } diff --git a/src/k8s/pkg/utils/file.go b/src/k8s/pkg/utils/file.go index 998b1847b..58fa18bca 100644 --- a/src/k8s/pkg/utils/file.go +++ b/src/k8s/pkg/utils/file.go @@ -123,17 +123,6 @@ func FileExists(path ...string) (bool, error) { return true, nil } -// ValueInSlice returns true if key is in list. -func ValueInSlice[T comparable](key T, list []T) bool { - for _, entry := range list { - if entry == key { - return true - } - } - - return false -} - var ErrUnknownMount = errors.New("mount is unknown") // GetMountPath returns the first mountpath for a given filesystem type.