Skip to content

Commit

Permalink
Add extra SANs to microcluster certificate (#629)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Berkay Tekin Öz <berkay.tekinoz@canonical.com>
  • Loading branch information
bschimke95 and berkayoz authored Aug 28, 2024
1 parent 6d8b29d commit 991e48e
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 8 deletions.
4 changes: 4 additions & 0 deletions src/k8s/cmd/k8s/k8s_get_join_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ func newGetJoinTokenCmd(env cmdutil.ExecutionEnvironment) *cobra.Command {
PreRun: chainPreRunHooks(hookRequireRoot(env)),
Args: cmdutil.MaximumNArgs(env, 1),
Run: func(cmd *cobra.Command, args []string) {
if !opts.worker && len(args) == 0 {
cmd.PrintErrln("Error: A node name is required for control-plane nodes.")
}

var name string
if len(args) == 1 {
name = args[0]
Expand Down
4 changes: 4 additions & 0 deletions src/k8s/pkg/k8sd/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,16 @@ func New(cfg Config) (*App, error) {
func (a *App) Run(ctx context.Context, customHooks *state.Hooks) error {
// TODO: consider improving API for overriding hooks.
hooks := &state.Hooks{
PreInit: a.onPreInit,
PostBootstrap: a.onBootstrap,
PostJoin: a.onPostJoin,
PreRemove: a.onPreRemove,
OnStart: a.onStart,
}
if customHooks != nil {
if customHooks.PreInit != nil {
hooks.PreInit = customHooks.PreInit
}
if customHooks.PostBootstrap != nil {
hooks.PostBootstrap = customHooks.PostBootstrap
}
Expand Down
56 changes: 56 additions & 0 deletions src/k8s/pkg/k8sd/app/hooks_preinit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package app

import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/canonical/k8s/pkg/utils"
"github.com/canonical/lxd/shared"
microclusterTypes "github.com/canonical/microcluster/v3/rest/types"
"github.com/canonical/microcluster/v3/state"
)

// onPreInit is called before we bootstrap or join a node.
func (a *App) onPreInit(ctx context.Context, s state.State, bootstrap bool, initConfig map[string]string) error {
if bootstrap {
return nil
}

controlPlaneJoinConfig, err := utils.MicroclusterControlPlaneJoinConfigFromMap(initConfig)
if err != nil {
return fmt.Errorf("failed to get control plane join config, boostrap %v: %w", bootstrap, err)
}
extraSANs := controlPlaneJoinConfig.ExtraSANS

if err := os.Remove(filepath.Join(s.FileSystem().StateDir, "server.crt")); err != nil {
return fmt.Errorf("failed to remove server.crt: %w", err)
}

if err := os.Remove(filepath.Join(s.FileSystem().StateDir, "server.key")); err != nil {
return fmt.Errorf("failed to remove server.key: %w", err)
}

cert, err := shared.KeyPairAndCA(
s.FileSystem().StateDir,
string(microclusterTypes.ServerCertificateName),
shared.CertServer,
shared.CertOptions{
AddHosts: true,
CommonName: s.Name(),
SubjectAlternativeNames: extraSANs,
})
if err != nil {
return err
}

if err := a.client.UpdateCertificate(ctx, microclusterTypes.ServerCertificateName, microclusterTypes.KeyPair{
Cert: string(cert.PublicKey()),
Key: string(cert.PrivateKey()),
}); err != nil {
return fmt.Errorf("failed to update certificate %s: %w", microclusterTypes.ServerCertificateName, err)
}

return nil
}
8 changes: 5 additions & 3 deletions src/k8s/pkg/k8sd/app/hooks_remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ func (a *App) onPreRemove(ctx context.Context, s state.State, force bool) (rerr
log.Error(err, "Failed to create Kubernetes client", err)
}

log.Info("Deleting node from Kubernetes cluster")
if err := c.DeleteNode(ctx, s.Name()); err != nil {
log.Error(err, "Failed to remove Kubernetes node")
if c != nil {
log.Info("Deleting node from Kubernetes cluster")
if err := c.DeleteNode(ctx, s.Name()); err != nil {
log.Error(err, "Failed to remove Kubernetes node")
}
}
}

Expand Down
59 changes: 57 additions & 2 deletions tests/integration/tests/test_clustering.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,64 @@ def test_no_remove(instances: List[harness.Instance]):
assert "control-plane" in util.get_local_node_status(joining_cp)
assert "worker" in util.get_local_node_status(joining_worker)

nodes = util.ready_nodes(cluster_node)

cluster_node.exec(["k8s", "remove-node", joining_cp.id])
nodes = util.ready_nodes(cluster_node)
assert len(nodes) == 3, "cp node should not have been removed from cluster"
cluster_node.exec(["k8s", "remove-node", joining_worker.id])
nodes = util.ready_nodes(cluster_node)
assert len(nodes) == 3, "worker node should not have been removed from cluster"


@pytest.mark.node_count(3)
def test_join_with_custom_token_name(instances: List[harness.Instance]):
cluster_node = instances[0]
joining_cp = instances[1]
joining_cp_with_hostname = instances[2]

out = cluster_node.exec(
["k8s", "get-join-token", "my-token"],
capture_output=True,
text=True,
)
join_token = out.stdout.strip()

join_config = """
extra-sans:
- my-token
"""
joining_cp.exec(
["k8s", "join-cluster", join_token, "--name", "my-node", "--file", "-"],
input=join_config,
text=True,
)

out = cluster_node.exec(
["k8s", "get-join-token", "my-token-2"],
capture_output=True,
text=True,
)
join_token_2 = out.stdout.strip()

join_config_2 = """
extra-sans:
- my-token-2
"""
joining_cp_with_hostname.exec(
["k8s", "join-cluster", join_token_2, "--file", "-"],
input=join_config_2,
text=True,
)

util.wait_until_k8s_ready(
cluster_node, instances, node_names={joining_cp.id: "my-node"}
)
nodes = util.ready_nodes(cluster_node)
assert len(nodes) == 3, "nodes should have joined cluster"

cluster_node.exec(["k8s", "remove-node", "my-node"])
nodes = util.ready_nodes(cluster_node)
assert len(nodes) == 2, "cp node should be removed from the cluster"

cluster_node.exec(["k8s", "remove-node", joining_cp_with_hostname.id])
nodes = util.ready_nodes(cluster_node)
assert len(nodes) == 1, "cp node with hostname should be removed from the cluster"
15 changes: 12 additions & 3 deletions tests/integration/tests/test_util/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import subprocess
from functools import partial
from pathlib import Path
from typing import Any, Callable, List, Optional, Union
from typing import Any, Callable, List, Mapping, Optional, Union

from tenacity import (
RetryCallState,
Expand Down Expand Up @@ -141,17 +141,26 @@ def wait_until_k8s_ready(
instances: List[harness.Instance],
retries: int = 30,
delay_s: int = 5,
node_names: Mapping[str, str] = {},
):
"""
Validates that the K8s node is in Ready state.
By default, the hostname of the instances is used as the node name.
If the instance name is different from the hostname, the instance name should be passed to the
node_names dictionary, e.g. {"instance_id": "node_name"}.
"""
for instance in instances:
host = hostname(instance)
if instance.id in node_names:
node_name = node_names[instance.id]
else:
node_name = hostname(instance)

result = (
stubbornly(retries=retries, delay_s=delay_s)
.on(control_node)
.until(lambda p: " Ready" in p.stdout.decode())
.exec(["k8s", "kubectl", "get", "node", host, "--no-headers"])
.exec(["k8s", "kubectl", "get", "node", node_name, "--no-headers"])
)
LOG.info("Kubelet registered successfully!")
LOG.info("%s", result.stdout.decode())
Expand Down

0 comments on commit 991e48e

Please sign in to comment.