. hack/e2e-apps/remediation-guard.sh run_kubernetes_test() { local version_expr="$1" local test_name="$2" local port="$3" local k8s_version=$(yq "$version_expr" packages/apps/kubernetes/files/versions.yaml) # Clean up stale resources from a previous failed retry kubectl -n tenant-test delete kuberneteses.apps.cozystack.io "${test_name}" --ignore-not-found --wait=false 2>/dev/null || true kubectl -n tenant-test wait kuberneteses.apps.cozystack.io "${test_name}" --for=delete --timeout=2m 2>/dev/null || true kubectl apply -f - < "tenantkubeconfig-${test_name}" # Update the kubeconfig to use localhost for the API server yq -i ".clusters[0].cluster.server = \"https://localhost:${port}\"" "tenantkubeconfig-${test_name}" # Kill any stale port-forward on this port from a previous retry pkill -f "port-forward.*${port}:" 2>/dev/null || true sleep 1 # Set up port forwarding to the Kubernetes API server # No timeout — process is killed at end of test or by job-level timeout-minutes kubectl port-forward service/kubernetes-"${test_name}" -n tenant-test "${port}":6443 > /dev/null 2>&1 & # Wait for port-forward to be ready before using it timeout 15 sh -ec 'until curl -sk https://localhost:'"${port}"' >/dev/null 2>&1; do sleep 1; done' # Verify the Kubernetes version matches what we expect (retry for up to 20 seconds) timeout 20 sh -ec 'until kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' version 2>/dev/null | grep -Fq "Server Version: ${k8s_version}"; do sleep 1; done' # Wait for at least 2 nodes to join (timeout after 8 minutes) timeout 8m bash -c ' until [ "$(kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' get nodes -o jsonpath="{.items[*].metadata.name}" | wc -w)" -ge 2 ]; do sleep 2 done ' # Verify the nodes are ready if ! kubectl --kubeconfig "tenantkubeconfig-${test_name}" wait node --all --timeout=3m --for=condition=Ready; then # Dump debug info and fail fast — no point running LB/NFS tests without Ready nodes kubectl --kubeconfig "tenantkubeconfig-${test_name}" describe nodes kubectl -n tenant-test get hr exit 1 fi kubectl --kubeconfig "tenantkubeconfig-${test_name}" get nodes -o wide # Verify the kubelet version matches what we expect versions=$(kubectl --kubeconfig "tenantkubeconfig-${test_name}" \ get nodes -o jsonpath='{.items[*].status.nodeInfo.kubeletVersion}') node_ok=true for v in $versions; do case "$v" in "${k8s_version}" | "${k8s_version}".* | "${k8s_version}"-*) # acceptable ;; *) node_ok=false break ;; esac done if [ "$node_ok" != true ]; then echo "Kubelet versions did not match expected ${k8s_version}" >&2 exit 1 fi kubectl --kubeconfig "tenantkubeconfig-${test_name}" apply -f - <&2 exit 1 fi # TODO(e2e-replace-fixed-timeouts): genuine retry loop. This validates an # external HTTP path (MetalLB-advertised LB IP -> in-tenant ingress -> # backend pod) which is not visible to the Kubernetes API as a single # condition, so kubectl wait cannot replace it. The 20x3s = 60s budget is # capped with `lb_ok=false` then asserted below. lb_ok=false for i in $(seq 1 20); do echo "Attempt $i" if curl --silent --fail "http://${LB_ADDR}"; then lb_ok=true break fi sleep 3 done if [ "$lb_ok" != true ]; then echo "LoadBalancer not reachable" >&2 exit 1 fi # Cleanup kubectl delete deployment --kubeconfig "tenantkubeconfig-${test_name}" "${test_name}-backend" -n tenant-test kubectl delete service --kubeconfig "tenantkubeconfig-${test_name}" "${test_name}-backend" -n tenant-test # Clean up NFS test resources from any previous failed attempt kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pod nfs-test-pod \ -n tenant-test --ignore-not-found --timeout=60s || true kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pvc nfs-test-pvc \ -n tenant-test --ignore-not-found --timeout=60s || true # Test RWX NFS mount in tenant cluster (uses kubevirt CSI driver with RWX support) kubectl --kubeconfig "tenantkubeconfig-${test_name}" apply -f - < /data/test.txt && cat /data/test.txt"] volumeMounts: - name: nfs-vol mountPath: /data volumes: - name: nfs-vol persistentVolumeClaim: claimName: nfs-test-pvc restartPolicy: Never EOF # Wait for Pod to complete successfully if ! kubectl --kubeconfig "tenantkubeconfig-${test_name}" wait pod nfs-test-pod -n tenant-test --timeout=5m --for=jsonpath='{.status.phase}'=Succeeded; then echo "=== NFS test pod did not complete ===" >&2 kubectl --kubeconfig "tenantkubeconfig-${test_name}" describe pod nfs-test-pod -n tenant-test >&2 || true kubectl --kubeconfig "tenantkubeconfig-${test_name}" get events -n tenant-test --sort-by='.lastTimestamp' >&2 || true exit 1 fi # Verify NFS data integrity nfs_result=$(kubectl --kubeconfig "tenantkubeconfig-${test_name}" logs nfs-test-pod -n tenant-test) if [ "$nfs_result" != "nfs-mount-ok" ]; then echo "NFS mount test failed: expected 'nfs-mount-ok', got '$nfs_result'" >&2 kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pod nfs-test-pod -n tenant-test --wait=false 2>/dev/null || true kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pvc nfs-test-pvc -n tenant-test --wait=false 2>/dev/null || true exit 1 fi # Cleanup NFS test resources in tenant cluster kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pod nfs-test-pod -n tenant-test --wait kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pvc nfs-test-pvc -n tenant-test # Wait for all machine deployment replicas to be ready (timeout after 10 minutes) kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=10m --for=jsonpath='{.status.v1beta2.readyReplicas}'=2 for component in cilium coredns csi vsnap-crd; do kubectl wait hr kubernetes-${test_name}-${component} -n tenant-test --timeout=1m --for=condition=ready done kubectl wait hr kubernetes-${test_name}-ingress-nginx -n tenant-test --timeout=5m --for=condition=ready # Guard: parent HelmRelease must not have entered an install/upgrade remediation cycle. # A non-zero installFailures/upgradeFailures indicates the helm-wait budget expired while # admin-kubeconfig was still being provisioned, which would trigger uninstall remediation # and churn the Cluster CR. # Flux helm-controller v2 retains per-revision release Snapshots in # .status.history; each Snapshot's .status reflects the Helm release # state (deployed/superseded/failed/uninstalled). A remediation cycle # leaves a "failed" or "uninstalled" entry behind that survives a later # successful reinstall, unlike the installFailures/upgradeFailures # counters (which ClearFailures zeroes on every successful reconcile). # The shape is pinned by hack/remediation-guard.bats; the upstream # types are github.com/fluxcd/helm-controller/api v2 Snapshot. history_statuses=$(kubectl get hr -n tenant-test "kubernetes-${test_name}" \ -ojsonpath='{range .status.history[*]}{.status}{"\n"}{end}') # Always emit the raw value so a silent future-Flux field rename shows # up as "empty history on a Ready HR" in CI logs rather than vanishing. echo "Parent HelmRelease history statuses:" printf '%s\n' "${history_statuses:-}" if [ -z "${history_statuses}" ]; then echo "Unexpected empty .status.history on a Ready HelmRelease - Flux API shape may have changed." >&2 kubectl -n tenant-test describe hr "kubernetes-${test_name}" >&2 exit 1 fi if helmrelease_has_remediation_cycle "${history_statuses}"; then echo "Parent HelmRelease entered remediation cycle." >&2 kubectl -n tenant-test describe hr "kubernetes-${test_name}" >&2 exit 1 fi # Clean up pkill -f "port-forward.*${port}:" 2>/dev/null || true rm -f "tenantkubeconfig-${test_name}" kubectl -n tenant-test delete kuberneteses.apps.cozystack.io "${test_name}" --ignore-not-found --wait=false 2>/dev/null || true }