|
1 | 1 | """Smoke tests for EKS cluster and AWS infrastructure.""" |
2 | 2 |
|
| 3 | +import subprocess |
| 4 | + |
3 | 5 | import pytest |
4 | 6 | import yaml |
5 | 7 | from helpers import get_unstable_node_names, pod_is_on_unstable_node, run_aws, run_kubectl |
@@ -158,3 +160,107 @@ def test_ecr_repos_exist(self, cluster_config, upstream_dir): |
158 | 160 |
|
159 | 161 | missing = [repo for repo in expected_repos if repo not in ecr_repo_names] |
160 | 162 | assert not missing, f"ECR repositories missing for bootstrap images: {missing}" |
| 163 | + |
| 164 | + |
| 165 | +# ============================================================================ |
| 166 | +# CoreDNS Topology Pinning |
| 167 | +# ============================================================================ |
| 168 | + |
| 169 | + |
| 170 | +class TestCoreDNSTopology: |
| 171 | + """Verify CoreDNS replicaCount, topology spread, autoscaling, and PDB. |
| 172 | +
|
| 173 | + Pinned via aws_eks_addon.coredns configuration_values in |
| 174 | + modules/eks/terraform/modules/eks/main.tf. Replica count is per-cluster |
| 175 | + via clusters.yaml (coredns.replicas — default 6, staging 2). |
| 176 | + """ |
| 177 | + |
| 178 | + def test_replica_count_matches_clusters_yaml(self, resolve_config): |
| 179 | + expected = resolve_config("coredns.replicas", 6) |
| 180 | + deploy = run_kubectl(["get", "deployment", "coredns"], namespace="kube-system") |
| 181 | + actual = deploy.get("spec", {}).get("replicas") |
| 182 | + assert actual == expected, ( |
| 183 | + f"CoreDNS Deployment.spec.replicas={actual} does not match clusters.yaml expected={expected}" |
| 184 | + ) |
| 185 | + |
| 186 | + def test_zone_topology_spread_is_hard(self, resolve_config): |
| 187 | + deploy = run_kubectl(["get", "deployment", "coredns"], namespace="kube-system") |
| 188 | + constraints = deploy.get("spec", {}).get("template", {}).get("spec", {}).get("topologySpreadConstraints", []) |
| 189 | + zone_rules = [ |
| 190 | + c |
| 191 | + for c in constraints |
| 192 | + if c.get("topologyKey") == "topology.kubernetes.io/zone" and c.get("whenUnsatisfiable") == "DoNotSchedule" |
| 193 | + ] |
| 194 | + assert zone_rules, ( |
| 195 | + f"CoreDNS Deployment missing hard zone spread (topology.kubernetes.io/zone, DoNotSchedule). " |
| 196 | + f"Found constraints: {constraints}" |
| 197 | + ) |
| 198 | + # maxSkew=2 tolerates AWS subnet placement drift (e.g. 4-2-0 across AZs after |
| 199 | + # AMI rolls) while still preventing catastrophic 6-0-0 single-AZ pinning. |
| 200 | + for rule in zone_rules: |
| 201 | + assert rule.get("maxSkew") == 2, ( |
| 202 | + f"CoreDNS zone spread maxSkew={rule.get('maxSkew')}, expected 2. Constraint: {rule}" |
| 203 | + ) |
| 204 | + assert rule.get("labelSelector", {}).get("matchLabels", {}).get("k8s-app") == "kube-dns", ( |
| 205 | + f"CoreDNS zone spread labelSelector mismatch. Constraint: {rule}" |
| 206 | + ) |
| 207 | + |
| 208 | + def test_hostname_topology_spread_is_soft(self, resolve_config): |
| 209 | + """Soft hostname spread (ScheduleAnyway) keeps replicas off the same node when possible.""" |
| 210 | + deploy = run_kubectl(["get", "deployment", "coredns"], namespace="kube-system") |
| 211 | + constraints = deploy.get("spec", {}).get("template", {}).get("spec", {}).get("topologySpreadConstraints", []) |
| 212 | + host_rules = [ |
| 213 | + c |
| 214 | + for c in constraints |
| 215 | + if c.get("topologyKey") == "kubernetes.io/hostname" and c.get("whenUnsatisfiable") == "ScheduleAnyway" |
| 216 | + ] |
| 217 | + assert host_rules, ( |
| 218 | + f"CoreDNS Deployment missing soft hostname spread (kubernetes.io/hostname, ScheduleAnyway). " |
| 219 | + f"Found constraints: {constraints}" |
| 220 | + ) |
| 221 | + for rule in host_rules: |
| 222 | + assert rule.get("maxSkew") == 1, ( |
| 223 | + f"CoreDNS hostname spread maxSkew={rule.get('maxSkew')}, expected 1. Constraint: {rule}" |
| 224 | + ) |
| 225 | + assert rule.get("labelSelector", {}).get("matchLabels", {}).get("k8s-app") == "kube-dns", ( |
| 226 | + f"CoreDNS hostname spread labelSelector mismatch. Constraint: {rule}" |
| 227 | + ) |
| 228 | + |
| 229 | + def test_no_cluster_proportional_autoscaler(self, cluster_config): |
| 230 | + """Addon-managed autoscaling must be off — no CPA Deployment should exist for coredns. |
| 231 | +
|
| 232 | + EKS CoreDNS uses the cluster-proportional-autoscaler (CPA), which runs as a |
| 233 | + Deployment named 'coredns-autoscaler' (newer addon versions) or 'eks-coredns-autoscaler' |
| 234 | + (older spelling). It is NOT a HorizontalPodAutoscaler. With autoScaling.enabled=false |
| 235 | + in the addon configuration_values, neither Deployment should be present. |
| 236 | + """ |
| 237 | + cluster_name = cluster_config["cluster"]["cluster_name"] |
| 238 | + for cpa_name in ("coredns-autoscaler", "eks-coredns-autoscaler"): |
| 239 | + try: |
| 240 | + deploy = run_kubectl(["get", "deployment", cpa_name], namespace="kube-system") |
| 241 | + except subprocess.CalledProcessError: |
| 242 | + # 'NotFound' surfaces as kubectl exit 1 — that's the desired state. |
| 243 | + continue |
| 244 | + # If we got JSON back, the CPA Deployment exists — that's a regression. |
| 245 | + pytest.fail( |
| 246 | + f"Unexpected cluster-proportional-autoscaler Deployment '{cpa_name}' in kube-system " |
| 247 | + f"on {cluster_name} (autoScaling.enabled=false should prevent this): {deploy}" |
| 248 | + ) |
| 249 | + |
| 250 | + def test_pdb_max_unavailable_is_one(self): |
| 251 | + pdbs = run_kubectl(["get", "poddisruptionbudgets"], namespace="kube-system") |
| 252 | + items = pdbs.get("items", []) |
| 253 | + coredns_pdbs = [ |
| 254 | + p |
| 255 | + for p in items |
| 256 | + if p.get("spec", {}).get("selector", {}).get("matchLabels", {}).get("k8s-app") == "kube-dns" |
| 257 | + ] |
| 258 | + assert coredns_pdbs, f"No PodDisruptionBudget found targeting CoreDNS (k8s-app=kube-dns). Items: {items}" |
| 259 | + # EKS addon picks one PDB resource name (typically 'coredns'); assert all |
| 260 | + # found PDBs have maxUnavailable=1 (defensive against multiple stale PDBs). |
| 261 | + for pdb in coredns_pdbs: |
| 262 | + spec = pdb.get("spec", {}) |
| 263 | + max_unavail = spec.get("maxUnavailable") |
| 264 | + assert max_unavail == 1, ( |
| 265 | + f"CoreDNS PDB {pdb['metadata']['name']} has maxUnavailable={max_unavail}, expected 1" |
| 266 | + ) |
0 commit comments