Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bootstrap Configuration #302

Merged
merged 8 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/k8s/api/v1/bootstrap_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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) }

// 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
}
neoaggelos marked this conversation as resolved.
Show resolved Hide resolved
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 := NewGomegaWithT(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 @@ -125,39 +143,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)
neoaggelos marked this conversation as resolved.
Show resolved Hide resolved
}

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 == ',' }) {
neoaggelos marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -213,18 +248,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
Loading