diff --git a/src/k8s/pkg/k8sd/setup/containerd.go b/src/k8s/pkg/k8sd/setup/containerd.go index e2476c186..4b61a9fa4 100644 --- a/src/k8s/pkg/k8sd/setup/containerd.go +++ b/src/k8s/pkg/k8sd/setup/containerd.go @@ -139,7 +139,7 @@ func Containerd(snap snap.Snap, registries []types.ContainerdRegistry, extraArgs } // Apply extra arguments after the defaults, so they can override them. - updateArgs, deleteArgs := snaputil.ServiceArgsFromMap(extraArgs) + updateArgs, deleteArgs := utils.ServiceArgsFromMap(extraArgs) if _, err := snaputil.UpdateServiceArguments(snap, "containerd", updateArgs, deleteArgs); err != nil { return fmt.Errorf("failed to write arguments file: %w", err) } diff --git a/src/k8s/pkg/k8sd/setup/k8s_apiserver_proxy.go b/src/k8s/pkg/k8sd/setup/k8s_apiserver_proxy.go index eccb1c540..f5683a6a7 100644 --- a/src/k8s/pkg/k8sd/setup/k8s_apiserver_proxy.go +++ b/src/k8s/pkg/k8sd/setup/k8s_apiserver_proxy.go @@ -7,6 +7,7 @@ import ( "github.com/canonical/k8s/pkg/proxy" "github.com/canonical/k8s/pkg/snap" snaputil "github.com/canonical/k8s/pkg/snap/util" + "github.com/canonical/k8s/pkg/utils" ) // K8sAPIServerProxy prepares configuration for k8s-apiserver-proxy. @@ -25,7 +26,7 @@ func K8sAPIServerProxy(snap snap.Snap, servers []string, extraArgs map[string]*s } // Apply extra arguments after the defaults, so they can override them. - updateArgs, deleteArgs := snaputil.ServiceArgsFromMap(extraArgs) + updateArgs, deleteArgs := utils.ServiceArgsFromMap(extraArgs) if _, err := snaputil.UpdateServiceArguments(snap, "k8s-apiserver-proxy", updateArgs, deleteArgs); err != nil { return fmt.Errorf("failed to write arguments file: %w", err) } diff --git a/src/k8s/pkg/k8sd/setup/k8s_dqlite.go b/src/k8s/pkg/k8sd/setup/k8s_dqlite.go index 6ed7620bd..f9a95d6e6 100644 --- a/src/k8s/pkg/k8sd/setup/k8s_dqlite.go +++ b/src/k8s/pkg/k8sd/setup/k8s_dqlite.go @@ -7,6 +7,7 @@ import ( "github.com/canonical/k8s/pkg/snap" snaputil "github.com/canonical/k8s/pkg/snap/util" + "github.com/canonical/k8s/pkg/utils" "gopkg.in/yaml.v2" ) @@ -33,7 +34,7 @@ func K8sDqlite(snap snap.Snap, address string, cluster []string, extraArgs map[s } // Apply extra arguments after the defaults, so they can override them. - updateArgs, deleteArgs := snaputil.ServiceArgsFromMap(extraArgs) + updateArgs, deleteArgs := utils.ServiceArgsFromMap(extraArgs) if _, err := snaputil.UpdateServiceArguments(snap, "k8s-dqlite", updateArgs, deleteArgs); err != nil { return fmt.Errorf("failed to write arguments file: %w", err) } diff --git a/src/k8s/pkg/k8sd/setup/kube_apiserver.go b/src/k8s/pkg/k8sd/setup/kube_apiserver.go index b932311f5..636b5f156 100644 --- a/src/k8s/pkg/k8sd/setup/kube_apiserver.go +++ b/src/k8s/pkg/k8sd/setup/kube_apiserver.go @@ -9,6 +9,7 @@ import ( "github.com/canonical/k8s/pkg/k8sd/types" "github.com/canonical/k8s/pkg/snap" snaputil "github.com/canonical/k8s/pkg/snap/util" + "github.com/canonical/k8s/pkg/utils" ) type apiserverAuthTokenWebhookTemplateConfig struct { @@ -107,7 +108,7 @@ func KubeAPIServer(snap snap.Snap, serviceCIDR string, authWebhookURL string, en } // Apply extra arguments after the defaults, so they can override them. - updateArgs, deleteArgs := snaputil.ServiceArgsFromMap(extraArgs) + updateArgs, deleteArgs := utils.ServiceArgsFromMap(extraArgs) if _, err := snaputil.UpdateServiceArguments(snap, "kube-apiserver", updateArgs, deleteArgs); err != nil { return fmt.Errorf("failed to write arguments file: %w", err) } diff --git a/src/k8s/pkg/k8sd/setup/kube_controller_manager.go b/src/k8s/pkg/k8sd/setup/kube_controller_manager.go index 5b3bba22c..91f68fcd9 100644 --- a/src/k8s/pkg/k8sd/setup/kube_controller_manager.go +++ b/src/k8s/pkg/k8sd/setup/kube_controller_manager.go @@ -7,6 +7,7 @@ import ( "github.com/canonical/k8s/pkg/snap" snaputil "github.com/canonical/k8s/pkg/snap/util" + "github.com/canonical/k8s/pkg/utils" ) // KubeControllerManager configures kube-controller-manager on the local node. @@ -31,7 +32,7 @@ func KubeControllerManager(snap snap.Snap, extraArgs map[string]*string) error { return fmt.Errorf("failed to render arguments file: %w", err) } // Apply extra arguments after the defaults, so they can override them. - updateArgs, deleteArgs := snaputil.ServiceArgsFromMap(extraArgs) + updateArgs, deleteArgs := utils.ServiceArgsFromMap(extraArgs) if _, err := snaputil.UpdateServiceArguments(snap, "kube-controller-manager", updateArgs, deleteArgs); err != nil { return fmt.Errorf("failed to write arguments file: %w", err) } diff --git a/src/k8s/pkg/k8sd/setup/kube_proxy.go b/src/k8s/pkg/k8sd/setup/kube_proxy.go index 6731b2925..64da185d6 100644 --- a/src/k8s/pkg/k8sd/setup/kube_proxy.go +++ b/src/k8s/pkg/k8sd/setup/kube_proxy.go @@ -8,6 +8,7 @@ import ( "github.com/canonical/k8s/pkg/snap" snaputil "github.com/canonical/k8s/pkg/snap/util" + "github.com/canonical/k8s/pkg/utils" ) // KubeProxy configures kube-proxy on the local node. @@ -35,7 +36,7 @@ func KubeProxy(ctx context.Context, snap snap.Snap, hostname string, podCIDR str } // Apply extra arguments after the defaults, so they can override them. - updateArgs, deleteArgs := snaputil.ServiceArgsFromMap(extraArgs) + updateArgs, deleteArgs := utils.ServiceArgsFromMap(extraArgs) if _, err := snaputil.UpdateServiceArguments(snap, "kube-proxy", updateArgs, deleteArgs); err != nil { return fmt.Errorf("failed to write arguments file: %w", err) } diff --git a/src/k8s/pkg/k8sd/setup/kube_scheduler.go b/src/k8s/pkg/k8sd/setup/kube_scheduler.go index da151a0bc..bc687af31 100644 --- a/src/k8s/pkg/k8sd/setup/kube_scheduler.go +++ b/src/k8s/pkg/k8sd/setup/kube_scheduler.go @@ -6,6 +6,7 @@ import ( "github.com/canonical/k8s/pkg/snap" snaputil "github.com/canonical/k8s/pkg/snap/util" + "github.com/canonical/k8s/pkg/utils" ) // KubeScheduler configures kube-scheduler on the local node. @@ -21,7 +22,7 @@ func KubeScheduler(snap snap.Snap, extraArgs map[string]*string) error { return fmt.Errorf("failed to render arguments file: %w", err) } // Apply extra arguments after the defaults, so they can override them. - updateArgs, deleteArgs := snaputil.ServiceArgsFromMap(extraArgs) + updateArgs, deleteArgs := utils.ServiceArgsFromMap(extraArgs) if _, err := snaputil.UpdateServiceArguments(snap, "kube-scheduler", updateArgs, deleteArgs); err != nil { return fmt.Errorf("failed to write arguments file: %w", err) } diff --git a/src/k8s/pkg/k8sd/setup/kubelet.go b/src/k8s/pkg/k8sd/setup/kubelet.go index bac5da7d2..4563cb224 100644 --- a/src/k8s/pkg/k8sd/setup/kubelet.go +++ b/src/k8s/pkg/k8sd/setup/kubelet.go @@ -8,6 +8,7 @@ import ( "github.com/canonical/k8s/pkg/snap" snaputil "github.com/canonical/k8s/pkg/snap/util" + "github.com/canonical/k8s/pkg/utils" ) var kubeletTLSCipherSuites = []string{ @@ -76,7 +77,7 @@ func kubelet(snap snap.Snap, hostname string, nodeIP net.IP, clusterDNS string, } // Apply extra arguments after the defaults, so they can override them. - updateArgs, deleteArgs := snaputil.ServiceArgsFromMap(extraArgs) + updateArgs, deleteArgs := utils.ServiceArgsFromMap(extraArgs) if _, err := snaputil.UpdateServiceArguments(snap, "kubelet", updateArgs, deleteArgs); err != nil { return fmt.Errorf("failed to write arguments file: %w", err) } diff --git a/src/k8s/pkg/k8sd/setup/util_extra_files.go b/src/k8s/pkg/k8sd/setup/util_extra_files.go index 5fb038655..e6fef0162 100644 --- a/src/k8s/pkg/k8sd/setup/util_extra_files.go +++ b/src/k8s/pkg/k8sd/setup/util_extra_files.go @@ -19,16 +19,8 @@ func ExtraNodeConfigFiles(snap snap.Snap, files map[string]string) error { } filePath := path.Join(snap.ServiceExtraConfigDir(), filename) - // Create or truncate the file - file, err := os.Create(filePath) - if err != nil { - return fmt.Errorf("failed to create file %s: %w", filePath, err) - } - defer file.Close() - // Write the content to the file - _, err = file.WriteString(content) - if err != nil { + if err := os.WriteFile(filePath, []byte(content), 0400); err != nil { return fmt.Errorf("failed to write to file %s: %w", filePath, err) } @@ -36,10 +28,6 @@ func ExtraNodeConfigFiles(snap snap.Snap, files map[string]string) error { if err := os.Chown(filePath, snap.UID(), snap.GID()); err != nil { return fmt.Errorf("failed to change owner of file %s: %w", filePath, err) } - - if err := os.Chmod(filePath, 0400); err != nil { - return fmt.Errorf("failed to change mode of file %s: %w", filePath, err) - } } return nil } diff --git a/src/k8s/pkg/utils/services.go b/src/k8s/pkg/utils/services.go new file mode 100644 index 000000000..596ee5425 --- /dev/null +++ b/src/k8s/pkg/utils/services.go @@ -0,0 +1,18 @@ +package utils + +// ServiceArgsFromMap processes a map of string pointers and categorizes them into update and delete lists. +// - If the value pointer is nil, it adds the argument name to the delete list. +// - If the value pointer is not nil, it adds the argument and its value to the update map. +func ServiceArgsFromMap(args map[string]*string) (map[string]string, []string) { + updateArgs := make(map[string]string) + deleteArgs := make([]string, 0) + + for arg, val := range args { + if val == nil { + deleteArgs = append(deleteArgs, arg) + } else { + updateArgs[arg] = *val + } + } + return updateArgs, deleteArgs +} diff --git a/src/k8s/pkg/utils/services_test.go b/src/k8s/pkg/utils/services_test.go new file mode 100644 index 000000000..8a18cbfc0 --- /dev/null +++ b/src/k8s/pkg/utils/services_test.go @@ -0,0 +1,77 @@ +package utils + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestServiceArgsFromMap(t *testing.T) { + tests := []struct { + name string + input map[string]*string + expected struct { + updateArgs map[string]string + deleteArgs []string + } + }{ + { + name: "NilValue", + input: map[string]*string{"arg1": nil}, + expected: struct { + updateArgs map[string]string + deleteArgs []string + }{ + updateArgs: map[string]string{}, + deleteArgs: []string{"arg1"}, + }, + }, + { + name: "EmptyString", // Should be threated as normal string + input: map[string]*string{"arg1": Pointer("")}, + expected: struct { + updateArgs map[string]string + deleteArgs []string + }{ + updateArgs: map[string]string{"arg1": ""}, + deleteArgs: []string{}, + }, + }, + { + name: "NonEmptyString", + input: map[string]*string{"arg1": Pointer("value1")}, + expected: struct { + updateArgs map[string]string + deleteArgs []string + }{ + updateArgs: map[string]string{"arg1": "value1"}, + deleteArgs: []string{}, + }, + }, + { + name: "MixedValues", + input: map[string]*string{ + "arg1": Pointer("value1"), + "arg2": Pointer(""), + "arg3": nil, + }, + expected: struct { + updateArgs map[string]string + deleteArgs []string + }{ + updateArgs: map[string]string{"arg1": "value1", "arg2": ""}, + deleteArgs: []string{"arg3"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + updateArgs, deleteArgs := ServiceArgsFromMap(tt.input) + g.Expect(updateArgs).To(Equal(tt.expected.updateArgs)) + g.Expect(deleteArgs).To(Equal(tt.expected.deleteArgs)) + }) + } +} diff --git a/tests/integration/templates/bootstrap-session.yaml b/tests/integration/templates/bootstrap-session.yaml index ca569664f..bdeebaece 100644 --- a/tests/integration/templates/bootstrap-session.yaml +++ b/tests/integration/templates/bootstrap-session.yaml @@ -15,19 +15,3 @@ cluster-config: enabled: true metrics-server: enabled: true -extra-node-config-files: - bootstrap-extra-file.yaml: extra-args-test-file-content -extra-node-kube-apiserver-args: - --request-timeout: 2m -extra-node-kube-controller-manager-args: - --leader-elect-retry-period: 3s -extra-node-kube-scheduler-args: - --authorization-webhook-cache-authorized-ttl: 11s -extra-node-kube-proxy-args: - --config-sync-period: 14m -extra-node-kubelet-args: - --authentication-token-webhook-cache-ttl: 3m -extra-node-containerd-args: - --log-level: debug -extra-node-k8s-dqlite-args: - --watch-storage-available-size-interval: 6s diff --git a/tests/integration/templates/bootstrap-smoke.yaml b/tests/integration/templates/bootstrap-smoke.yaml new file mode 100644 index 000000000..220a43c49 --- /dev/null +++ b/tests/integration/templates/bootstrap-smoke.yaml @@ -0,0 +1,32 @@ +# Contains the bootstrap configuration for the smoke test. +cluster-config: + network: + enabled: true + dns: + enabled: true + ingress: + enabled: true + load-balancer: + enabled: true + local-storage: + enabled: true + gateway: + enabled: true + metrics-server: + enabled: true +extra-node-config-files: + bootstrap-extra-file.yaml: extra-args-test-file-content +extra-node-kube-apiserver-args: + --request-timeout: 2m +extra-node-kube-controller-manager-args: + --leader-elect-retry-period: 3s +extra-node-kube-scheduler-args: + --authorization-webhook-cache-authorized-ttl: 11s +extra-node-kube-proxy-args: + --config-sync-period: 14m +extra-node-kubelet-args: + --authentication-token-webhook-cache-ttl: 3m +extra-node-containerd-args: + --log-level: debug +extra-node-k8s-dqlite-args: + --watch-storage-available-size-interval: 6s diff --git a/tests/integration/tests/test_smoke.py b/tests/integration/tests/test_smoke.py index 9bc9b4c85..7ff72158c 100644 --- a/tests/integration/tests/test_smoke.py +++ b/tests/integration/tests/test_smoke.py @@ -2,24 +2,39 @@ # Copyright 2024 Canonical, Ltd. # import logging +from typing import List -from test_util import harness +import pytest +from test_util import config, harness, util LOG = logging.getLogger(__name__) -def test_smoke(session_instance: harness.Instance): +@pytest.mark.node_count(1) +@pytest.mark.disable_k8s_bootstrapping() +def test_smoke(instances: List[harness.Instance]): + instance = instances[0] + + bootstrap_smoke_config_path = "/home/ubuntu/bootstrap-smoke.yaml" + instance.send_file( + (config.MANIFESTS_DIR / "bootstrap-smoke.yaml").as_posix(), + bootstrap_smoke_config_path, + ) + + instance.exec(["k8s", "bootstrap", "--file", bootstrap_smoke_config_path]) + util.wait_until_k8s_ready(instance, [instance]) + # Verify the functionality of the k8s config command during the smoke test. # It would be excessive to deploy a cluster solely for this purpose. - result = session_instance.exec( + result = instance.exec( "k8s config --server 192.168.210.41".split(), capture_output=True ) - config = result.stdout.decode() - assert len(config) > 0 - assert "server: https://192.168.210.41" in config + kubeconfig = result.stdout.decode() + assert len(kubeconfig) > 0 + assert "server: https://192.168.210.41" in kubeconfig # Verify extra node configs - content = session_instance.exec( + content = instance.exec( ["cat", "/var/snap/k8s/common/args/conf.d/bootstrap-extra-file.yaml"], capture_output=True, ) @@ -35,7 +50,7 @@ def test_smoke(session_instance: harness.Instance): "containerd": "--log-level=debug", "k8s-dqlite": "--watch-storage-available-size-interval=6s", }.items(): - args = session_instance.exec( + args = instance.exec( ["cat", f"/var/snap/k8s/common/args/{service}"], capture_output=True ) assert value in args.stdout.decode() diff --git a/tests/integration/tests/test_util/util.py b/tests/integration/tests/test_util/util.py index 599390e20..c9a62b452 100644 --- a/tests/integration/tests/test_util/util.py +++ b/tests/integration/tests/test_util/util.py @@ -223,8 +223,8 @@ def get_join_token( # Join an existing cluster. -def join_cluster(instance: harness.Instance, join_token: str, *args: str): - instance.exec(["k8s", "join-cluster", join_token, *args]) +def join_cluster(instance: harness.Instance, join_token: str): + instance.exec(["k8s", "join-cluster", join_token]) def get_default_cidr(instance: harness.Instance, instance_default_ip: str):