Skip to content

Commit

Permalink
Bootstrap Configuration (#302)
Browse files Browse the repository at this point in the history
* Create a proper apiv1.BootstrapConfig type

* Update types.ClusterConfig <-> apiv1.BootstrapConfig

* Adjust k8s bootstrap process for new bootstrap config

* remove unused code

* preserve default behaviour in k8s bootstrap

* DatastoreServers set as []string on API

* make sure to return zero value in case of error

* stricter validation for datastore config
  • Loading branch information
neoaggelos authored Apr 8, 2024
1 parent 3866fe4 commit 6178860
Show file tree
Hide file tree
Showing 14 changed files with 518 additions and 247 deletions.
55 changes: 55 additions & 0 deletions src/k8s/api/v1/bootstrap_config.go
Original file line number Diff line number Diff line change
@@ -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
}
59 changes: 59 additions & 0 deletions src/k8s/api/v1/bootstrap_config_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
51 changes: 0 additions & 51 deletions src/k8s/api/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
43 changes: 0 additions & 43 deletions src/k8s/api/v1/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
84 changes: 52 additions & 32 deletions src/k8s/cmd/k8s/k8s_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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.")
Expand Down Expand Up @@ -126,39 +144,56 @@ 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
}

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
}

Expand Down Expand Up @@ -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")
}
}
Loading

0 comments on commit 6178860

Please sign in to comment.