Files
home-ops/.github/template-tests/e2e/cluster.sh
T
grachevkoandGitHub e502e0ff76
Label Sync / Label Sync (push) Failing after 1m7s
E2E / reject-invalid (bad-bgp-asn) (push) Skipped
E2E / reject-invalid (bad-mac-address) (push) Skipped
E2E / reject-invalid (bad-repo-url) (push) Skipped
E2E / reject-invalid (bad-vlan-tag) (push) Skipped
E2E / reject-invalid (duplicate-gateway-addrs) (push) Skipped
E2E / reject-invalid (duplicate-node-names) (push) Skipped
E2E / reject-invalid (gateway-node-collision) (push) Skipped
E2E / reject-invalid (missing-dns-token) (push) Skipped
E2E / reject-invalid (nested-cidr-overlap) (push) Skipped
E2E / reject-invalid (node-addr-outside-cidr) (push) Skipped
E2E / reject-invalid (tunnel-without-dns) (push) Skipped
E2E / accept-valid (selfhosted) (push) Skipped
E2E / reject-invalid (missing-known-hosts) (push) Skipped
E2E / reject-invalid (missing-schematic) (push) Skipped
E2E / reject-invalid (partial-bgp) (push) Skipped
E2E / accept-valid (internal) (push) Skipped
E2E / reject-invalid (missing-external-gateway) (push) Skipped
E2E / reject-invalid (node-uses-gateway-addr) (push) Skipped
E2E / reject-invalid (non-canonical-cidr) (push) Skipped
E2E / accept-valid (private) (push) Skipped
E2E / accept-valid (single-node) (push) Skipped
E2E / reject-invalid (overlapping-cidrs) (push) Skipped
E2E / reject-invalid (reserved-node-name) (push) Skipped
E2E / reject-invalid (tiny-svc-cidr) (push) Skipped
E2E / validator-tests (push) Skipped
E2E / accept-valid (direct) (push) Skipped
E2E / accept-valid (multi-controller) (push) Skipped
E2E / accept-valid (no-webhook) (push) Skipped
E2E / accept-valid (public) (push) Skipped
Initial commit
2026-09-15 22:21:43 +03:00

292 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# Full-fidelity bootstrap e2e: takes maintenance-mode Talos VMs, discovers
# their hardware the same way the README instructs users to, writes a
# cluster.toml from the discovered facts, and runs the template's real
# bootstrap flow against them.
#
# Two provisioning paths share this test body:
# - CI: talosctl-cluster-action boots the nodes (talos-cluster.yaml) and
# passes E2E_CONTROLPLANE_IPS / E2E_WORKER_IPS / E2E_CIDR; the action's
# post step destroys them.
# - Local: run with no env set; the script boots and destroys the cluster
# itself. Requires Docker, /dev/kvm, passwordless sudo, qemu-system-x86,
# and the repo's mise toolchain on PATH.
#
# Renders into the working tree like any configure run.
set -euo pipefail
NAME="${E2E_NAME:-template-e2e}"
MODE="${1:-all}"
E2E_DIR=".github/template-tests/e2e"
CIDR="${E2E_CIDR:-10.9.0.0/24}"
PREFIX="${CIDR%/*}"
PREFIX="${PREFIX%.*}"
TALOSCTL="$(command -v talosctl)"
# The Image Factory vanilla schematic, matching the ISO the nodes boot from.
SCHEMATIC="376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba"
if [ -n "${E2E_CONTROLPLANE_IPS:-}" ]; then
PROVISIONED=true
IFS=',' read -r -a CONTROLPLANES <<< "$E2E_CONTROLPLANE_IPS"
IFS=',' read -r -a WORKERS <<< "${E2E_WORKER_IPS:-}"
else
PROVISIONED=false
CONTROLPLANES=("$PREFIX.2")
WORKERS=("$PREFIX.3")
fi
NODES=("${CONTROLPLANES[@]}" "${WORKERS[@]}")
# The VMs reach the host at the gateway address; the rendered workspace is
# served from there over git smart HTTP so Flux can sync it.
GIT_HOST="${E2E_GATEWAY:-$PREFIX.1}"
GIT_PORT=8418
GIT_SERVER_CONTAINER=""
# The provisioner runs under sudo and writes state relative to its cwd and
# TALOSCONFIG, so both are pointed at a scratch dir to keep root-owned files
# out of the repo.
if [ -n "${E2E_STATE:-}" ]; then
STATE="$E2E_STATE"
STATE_OWNED=false
else
STATE="$(mktemp -d)"
STATE_OWNED=true
fi
mkdir -p "$STATE"
GIT_PUSH_URL="http://127.0.0.1:$GIT_PORT/repo.git"
cleanup() {
rc=$?
if [ "$rc" -ne 0 ]; then
echo "==> e2e failed (rc=$rc), collecting diagnostics"
kubectl get pods --all-namespaces 2>/dev/null || true
kubectl get gitrepositories,kustomizations,helmreleases --all-namespaces 2>/dev/null || true
kubectl get events --all-namespaces --sort-by=.lastTimestamp 2>/dev/null | tail -30 || true
[ -n "$GIT_SERVER_CONTAINER" ] && docker logs --tail 5 "$GIT_SERVER_CONTAINER" 2>/dev/null || true
for ip in "${NODES[@]}"; do
talosctl -n "$ip" dmesg 2>/dev/null | tail -20 || true
done
fi
if [ "$MODE" = all ]; then
[ -n "$GIT_SERVER_CONTAINER" ] && docker stop "$GIT_SERVER_CONTAINER" >/dev/null 2>&1 || true
fi
if [ "$MODE" = all ] && [ "$PROVISIONED" = false ]; then
(cd "$STATE" && sudo -E env TALOSCONFIG="$STATE/talosconfig" \
"$TALOSCTL" cluster destroy --name "$NAME" --provisioner qemu >/dev/null 2>&1) || true
fi
if [ "$MODE" = all ] && [ "$STATE_OWNED" = true ]; then
sudo rm -rf "$STATE" || true
fi
exit "$rc"
}
trap cleanup EXIT
start_local_git_server() {
GIT_SERVER_CONTAINER="$NAME-git"
docker run --detach --rm --name "$GIT_SERVER_CONTAINER" \
--publish "$GIT_PORT:23232" \
--env SOFT_SERVE_GIT_ENABLED=false \
--env SOFT_SERVE_LFS_ENABLED=false \
--env SOFT_SERVE_SSH_LISTEN_ADDR=127.0.0.1:23231 \
--env SOFT_SERVE_STATS_ENABLED=false \
--entrypoint /bin/sh \
ghcr.io/charmbracelet/soft-serve:v0.11.6 \
-c 'set -eu; ssh-keygen -q -t ed25519 -N "" -f /tmp/admin; export SOFT_SERVE_INITIAL_ADMIN_KEYS="$(cat /tmp/admin.pub)"; /usr/local/bin/soft serve & pid=$!; until ssh -q -i /tmp/admin -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -p 23231 localhost settings anon-access read-write; do sleep 1; done; ssh -q -i /tmp/admin -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -p 23231 localhost repo create repo; wait "$pid"' \
>/dev/null
deadline=$((SECONDS + 60))
until git ls-remote "$GIT_PUSH_URL" >/dev/null 2>&1; do
if (( SECONDS >= deadline )); then
just log fatal "Soft Serve is not reachable"
fi
sleep 1
done
}
prepare() {
# In CI the action itself waits for every node's maintenance API before
# returning, so the poll here covers only the local path, where cluster
# create returns as soon as the VMs launch.
if [ "$PROVISIONED" = false ]; then
echo "==> booting maintenance-mode nodes"
(cd "$STATE" && sudo -E env TALOSCONFIG="$STATE/talosconfig" \
"$TALOSCTL" cluster create qemu --name "$NAME" --presets iso,maintenance \
--controlplanes 1 --workers 1 --cidr "$CIDR" \
--memory-controlplanes 4GiB --memory-workers 3GiB)
echo "==> waiting for the maintenance API"
for ip in "${NODES[@]}"; do
until talosctl -n "$ip" get links --insecure >/dev/null 2>&1; do sleep 5; done
done
fi
echo "==> discovering node hardware"
declare -A MACS DISKS
for ip in "${NODES[@]}"; do
MACS[$ip]="$(talosctl -n "$ip" get links --insecure -o json \
| jq -r 'select(.spec.type == "ether" and .spec.operationalState == "up" and (.metadata.id | startswith("bond") | not)) | .spec.hardwareAddr' | head -1)"
DISKS[$ip]="/dev/$(talosctl -n "$ip" get disks --insecure -o json \
| jq -r 'select(.spec.readonly == false and (.metadata.id | startswith("loop") | not)) | .metadata.id' | head -1)"
echo " $ip mac=${MACS[$ip]} disk=${DISKS[$ip]}"
done
echo "==> generating cluster.toml"
export E2E_CIDR="$CIDR"
export E2E_GATEWAY="${E2E_GATEWAY:-$PREFIX.1}"
export E2E_GIT_HOST="$GIT_HOST"
export E2E_GIT_PORT="$GIT_PORT"
export E2E_PREFIX="$PREFIX"
export E2E_SCHEMATIC="$SCHEMATIC"
envsubst '${E2E_CIDR} ${E2E_GATEWAY} ${E2E_GIT_HOST} ${E2E_GIT_PORT} ${E2E_PREFIX} ${E2E_SCHEMATIC}' \
< "$E2E_DIR/cluster.toml.tmpl" > cluster.toml
index=0
for ip in "${NODES[@]}"; do
controller=false
for cp in "${CONTROLPLANES[@]}"; do [ "$ip" = "$cp" ] && controller=true; done
export E2E_NODE_NAME="e2e-$index"
export E2E_NODE_ADDRESS="$ip"
export E2E_NODE_CONTROLLER="$controller"
export E2E_NODE_DISK="${DISKS[$ip]}"
export E2E_NODE_MAC="${MACS[$ip]}"
envsubst '${E2E_NODE_ADDRESS} ${E2E_NODE_CONTROLLER} ${E2E_NODE_DISK} ${E2E_NODE_MAC} ${E2E_NODE_NAME}' \
< "$E2E_DIR/node.toml.tmpl" >> cluster.toml
index=$((index + 1))
done
echo "==> configure"
just init
just configure
# Flux's FluxInstance only reports Ready once its Git sync succeeds, so the
# rendered kubernetes/ tree is committed to a bare repo and served to the
# cluster — the same push-then-bootstrap flow the README walks users through.
echo "==> publishing rendered repo"
mkdir -p "$STATE/gitwork"
cp -r kubernetes "$STATE/gitwork/"
git -C "$STATE/gitwork" init --quiet --initial-branch main
git -C "$STATE/gitwork" add --all
git -C "$STATE/gitwork" -c user.name=e2e -c user.email=e2e@cluster.local \
commit --quiet --message "rendered workspace"
git -C "$STATE/gitwork" push --quiet "$GIT_PUSH_URL" main
}
assert_cluster_health() {
echo "==> asserting cluster health"
kubectl wait nodes --all --for=condition=Ready --timeout=10m
for ns in kube-system cert-manager flux-system; do
kubectl wait pods --namespace "$ns" --all --for=condition=Ready --timeout=10m
done
echo "==> asserting flux reconciliation"
kubectl wait fluxinstance/flux --namespace flux-system --for=condition=Ready --timeout=10m
kubectl wait gitrepositories --all --all-namespaces --for=condition=Ready --timeout=5m
kubectl wait kustomizations --all --all-namespaces --for=condition=Ready --timeout=10m
kubectl wait helmreleases --all --all-namespaces --for=condition=Ready --timeout=10m
}
foundation() {
deadline=$((SECONDS + 60))
until git ls-remote "http://$GIT_HOST:$GIT_PORT/repo.git" >/dev/null 2>&1; do
if (( SECONDS >= deadline )); then
just log fatal "Rendered repository server is not reachable"
fi
sleep 1
done
echo "==> bootstrap talos"
just bootstrap talos
echo "==> bootstrap apps"
just bootstrap apps
assert_cluster_health
echo "==> asserting bootstrap idempotency"
just configure
just bootstrap talos
just bootstrap apps
assert_cluster_health
}
flux_sops() {
echo "==> asserting Flux SOPS decryption"
SOPS_SECRET="$STATE/gitwork/kubernetes/apps/default/e2e-sops.sops.yaml"
export E2E_SOPS_VALUE=flux-decrypted
envsubst '${E2E_SOPS_VALUE}' < "$E2E_DIR/sops-secret.yaml.tmpl" > "$SOPS_SECRET"
sops encrypt --filename-override kubernetes/apps/default/e2e-sops.sops.yaml \
--in-place "$SOPS_SECRET"
yq --inplace '.resources += ["./e2e-sops.sops.yaml"]' \
"$STATE/gitwork/kubernetes/apps/default/kustomization.yaml"
git -C "$STATE/gitwork" add --all
git -C "$STATE/gitwork" -c user.name=e2e -c user.email=e2e@cluster.local \
commit --quiet --message "test Flux SOPS decryption"
git -C "$STATE/gitwork" push --quiet "$GIT_PUSH_URL" main
flux reconcile kustomization cluster-apps --with-source --timeout=10m
test "$(kubectl get secret e2e-sops --namespace default \
--output jsonpath='{.data.value}' | base64 --decode)" = "flux-decrypted"
}
networking() {
echo "==> asserting pod networking and DNS"
export E2E_CONTROLPLANE_NODE="$(kubectl get nodes \
--selector=node-role.kubernetes.io/control-plane \
--output jsonpath='{.items[0].metadata.name}')"
export E2E_WORKER_NODE="$(kubectl get nodes \
--selector='!node-role.kubernetes.io/control-plane' \
--output jsonpath='{.items[0].metadata.name}')"
NETWORK_CONFIG="$STATE/network.yaml"
envsubst '${E2E_CONTROLPLANE_NODE} ${E2E_WORKER_NODE}' \
< "$E2E_DIR/network.yaml.tmpl" > "$NETWORK_CONFIG"
kubectl apply --filename "$NETWORK_CONFIG"
kubectl wait pods/e2e-network-server pods/e2e-network-client \
--namespace default --for=condition=Ready --timeout=5m
SERVER_IP="$(kubectl get pod e2e-network-server --namespace default \
--output jsonpath='{.status.podIP}')"
kubectl exec --namespace default e2e-network-client -- \
/agnhost connect --timeout=10s "$SERVER_IP:8080"
kubectl exec --namespace default e2e-network-client -- \
/agnhost connect --timeout=10s e2e-network-server.default.svc.cluster.local:8080
kubectl exec --namespace default e2e-network-client -- \
/agnhost connect --timeout=10s github.com:443
kubectl apply --filename "$E2E_DIR/cilium-network-policy.yaml"
deadline=$((SECONDS + 60))
while kubectl exec --namespace default e2e-network-client -- \
/agnhost connect --timeout=2s "$SERVER_IP:8080" &>/dev/null; do
if (( SECONDS >= deadline )); then
just log fatal "CiliumNetworkPolicy did not block pod traffic"
fi
sleep 2
done
kubectl delete ciliumnetworkpolicy e2e-deny-server --namespace default
deadline=$((SECONDS + 60))
until kubectl exec --namespace default e2e-network-client -- \
/agnhost connect --timeout=2s "$SERVER_IP:8080" &>/dev/null; do
if (( SECONDS >= deadline )); then
just log fatal "Pod traffic did not recover after removing CiliumNetworkPolicy"
fi
sleep 2
done
}
summary() {
kubectl get nodes --output wide
kubectl get kustomizations,helmreleases --all-namespaces
echo "==> e2e bootstrap succeeded"
}
case "$MODE" in
prepare) prepare ;;
foundation) foundation ;;
flux-sops) flux_sops ;;
networking) networking ;;
summary) summary ;;
all)
start_local_git_server
prepare
foundation
flux_sops
networking
summary
;;
*)
echo "usage: $0 {prepare|foundation|flux-sops|networking|summary|all}" >&2
exit 2
;;
esac