Files
Robert Rose ec5b233eb9 nixos/rancher: wait for all nodes to be ready in multi node test
Without waiting, the rollout of the test daemonset may succeed
pre-maturely because only nodes that are ready are considered for
rollout.
2026-05-06 10:19:57 +02:00

251 lines
7.0 KiB
Nix

# A test that runs a multi-node rancher cluster and verifies pod networking works across nodes
{
pkgs,
lib,
rancherDistro,
rancherPackage,
serviceName,
disabledComponents,
coreImages,
vmResources,
...
}:
let
imageEnv = pkgs.buildEnv {
name = "${rancherDistro}-pause-image-env";
paths = with pkgs; [
tini
bashInteractive
coreutils
socat
];
};
pauseImage = pkgs.dockerTools.buildImage {
name = "test.local/pause";
tag = "local";
copyToRoot = imageEnv;
config.Entrypoint = [
"/bin/tini"
"--"
"/bin/sleep"
"inf"
];
};
# A daemonset that responds 'server' on port 8000
networkTestDaemonset = pkgs.writeText "test.yml" ''
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: test
labels:
name: test
spec:
selector:
matchLabels:
name: test
template:
metadata:
labels:
name: test
spec:
containers:
- name: test
image: test.local/pause:local
imagePullPolicy: Never
resources:
limits:
memory: 20Mi
command: ["socat", "TCP4-LISTEN:8000,fork", "EXEC:echo server"]
'';
tokenFile = pkgs.writeText "token" "p@s$w0rd";
supervisorPort =
{
k3s = "6443";
rke2 = "9345";
}
.${rancherDistro};
in
{
name = "${rancherPackage.name}-multi-node";
nodes = {
server =
{
nodes,
pkgs,
config,
...
}:
{
environment.systemPackages = with pkgs; [
kubectl
gzip
jq
];
environment.sessionVariables.KUBECONFIG = "/etc/rancher/${rancherDistro}/${rancherDistro}.yaml";
virtualisation = vmResources;
services.${rancherDistro} = lib.mkMerge [
{
inherit tokenFile;
enable = true;
role = "server";
package = rancherPackage;
images = coreImages ++ [ pauseImage ];
nodeIP = config.networking.primaryIPAddress;
disable = disabledComponents;
extraFlags = [
"--pause-image test.local/pause:local"
];
}
{
k3s = {
clusterInit = true;
extraFlags = [ "--flannel-iface eth1" ]; # see canalConfig definition
};
# The interface selection logic of flannel & canal would normally use eth0, as
# the nixos testing driver sets a default route via dev eth0. However, in test
# setups we have to use eth1 for inter-node communication.
# For K3s this can be handled via --flannel-iface, but RKE2's canal has to be
# configured with this manifest.
rke2.manifests.canal-config.content = {
apiVersion = "helm.cattle.io/v1";
kind = "HelmChartConfig";
metadata = {
name = "rke2-canal";
namespace = "kube-system";
};
# spec.valuesContent needs to a string, either json or yaml
spec.valuesContent = builtins.toJSON {
flannel.iface = "eth1";
};
};
}
.${rancherDistro}
];
networking.firewall.enable = false;
networking.firewall.allowedTCPPorts = [
2379
2380
6443
]
++ lib.optionals (rancherDistro == "rke2") [
9099
9345
];
networking.firewall.allowedUDPPorts = [ 8472 ];
};
server2 =
{
nodes,
pkgs,
config,
...
}:
{
virtualisation = vmResources;
services.${rancherDistro} = {
inherit tokenFile;
enable = true;
role = "server";
package = rancherPackage;
images = coreImages ++ [ pauseImage ];
serverAddr = "https://${nodes.server.networking.primaryIPAddress}:${supervisorPort}";
nodeIP = config.networking.primaryIPAddress;
disable = disabledComponents;
extraFlags = [
"--pause-image test.local/pause:local"
]
++ lib.optional (rancherDistro == "k3s") "--flannel-iface eth1";
};
networking.firewall.enable = false;
networking.firewall.allowedTCPPorts = [
2379
2380
6443
]
++ lib.optionals (rancherDistro == "rke2") [
9099
9345
];
networking.firewall.allowedUDPPorts = [ 8472 ];
};
agent =
{
nodes,
pkgs,
config,
...
}:
{
virtualisation = vmResources;
services.${rancherDistro} = {
inherit tokenFile;
enable = true;
role = "agent";
package = rancherPackage;
images = coreImages ++ [ pauseImage ];
serverAddr = "https://${nodes.server2.networking.primaryIPAddress}:${supervisorPort}";
nodeIP = config.networking.primaryIPAddress;
extraFlags = [
"--pause-image test.local/pause:local"
]
++ lib.optional (rancherDistro == "k3s") "--flannel-iface eth1";
};
networking.firewall.allowedTCPPorts = lib.optional (rancherDistro == "rke2") 9099;
networking.firewall.allowedUDPPorts = [ 8472 ];
};
};
testScript = # python
''
start_all()
servers = [server, server2]
for m in servers:
m.wait_for_unit("${serviceName}")
# wait for the agent to show up
server.wait_until_succeeds("kubectl get node agent")
server.succeed("kubectl cluster-info")
# Also wait for our service account to show up; it takes a sec
server.wait_until_succeeds("kubectl get serviceaccount default")
# Wait for all nodes to be ready
server.succeed("kubectl wait --timeout=-1s --for=condition=Ready node/server")
server.succeed("kubectl wait --timeout=-1s --for=condition=Ready node/server2")
server.succeed("kubectl wait --timeout=-1s --for=condition=Ready node/agent")
# Now create a pod on each node via a daemonset and verify they can talk to each other.
server.succeed("kubectl apply -f ${networkTestDaemonset}")
server.wait_until_succeeds("kubectl rollout status daemonset test")
# Get pod IPs
pods = server.succeed("kubectl get po -o json | jq '.items[].metadata.name' -r").splitlines()
pod_ips = [server.succeed(f"kubectl get po {name} -o json | jq '.status.podIP' -cr").strip() for name in pods]
# Verify each server can ping each pod ip
for pod_ip in pod_ips:
server.succeed(f"ping -c 1 {pod_ip}")
server2.succeed(f"ping -c 1 {pod_ip}")
agent.succeed(f"ping -c 1 {pod_ip}")
# Verify the pods can talk to each other
for pod in pods:
resp = server.succeed(f"kubectl exec {pod} -- socat TCP:{pod_ip}:8000 -")
t.assertEqual(resp.strip(), "server")
'';
meta.maintainers = lib.teams.k3s.members ++ pkgs.rke2.meta.maintainers;
}