Coverage for lambda / kubectl-applier-simple / handler.py: 100.00%

1349 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-10-28 16:19 +0000

1""" 

2Lambda handler for applying Kubernetes manifests to EKS clusters. 

3 

4This Lambda function is triggered by CloudFormation Custom Resources during 

5stack deployment. It applies Kubernetes manifests (namespaces, deployments, 

6services, RBAC, Karpenter NodePools, etc.) to the EKS cluster. 

7 

8Key Features: 

9- Pure Python implementation (no Docker/kubectl binary required) 

10- Generates EKS authentication tokens using STS presigned URLs 

11- Supports create/update operations with idempotent behavior 

12- Handles placeholder replacement for dynamic values (image URIs, etc.) 

13- Two-pass deployment: main pass then post-Helm pass for CRD-dependent resources 

14 

15Manifest Naming Convention: 

16 NN-name.yaml Applied in the main pass (before Helm) 

17 post-helm-*.yaml Applied in the post-Helm pass (after Helm installs CRDs) 

18 

19 Files with unreplaced {{PLACEHOLDER}} values are automatically skipped, 

20 enabling optional features (FSx, Valkey, queue processor). 

21 

22Environment Variables: 

23 CLUSTER_NAME: Name of the EKS cluster 

24 REGION: AWS region where the cluster is deployed 

25 

26CloudFormation Properties: 

27 ClusterName: EKS cluster name 

28 Region: AWS region 

29 ImageReplacements: Dict of placeholder -> value mappings 

30 SkipDeletionOnStackDelete: If "true", don't delete resources on stack deletion 

31 PostHelm: "true" to apply only post-helm-* manifests (after Helm installs CRDs) 

32""" 

33 

34import base64 

35import copy 

36import json 

37import logging 

38import os 

39import re 

40import time 

41from datetime import UTC 

42from typing import Any 

43 

44import boto3 

45import urllib3 

46import yaml 

47from kubernetes import client, dynamic 

48from kubernetes.client.rest import ApiException 

49from kubernetes.dynamic.exceptions import NotFoundError, ResourceNotFoundError 

50 

51# <pyflowchart-code-diagram> BEGIN - auto-inserted, do not edit 

52# Generated at (UTC): 2026-10-28T14:57:12Z 

53# Generated from Git commit: fdfe31c154bbf84cdfeb034fee1256e0a838d1f1 

54# Flowchart(s) generated from this file: 

55# * ``lambda_handler`` -> ``diagrams/code_diagrams/lambda/kubectl-applier-simple/handler.lambda_handler.html`` 

56# (PNG: ``diagrams/code_diagrams/lambda/kubectl-applier-simple/handler.lambda_handler.png``) 

57# Regenerate with ``SOURCE_DATE_EPOCH=<unix-seconds> GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> python diagrams/generate.py --code-only``. 

58# <pyflowchart-code-diagram> END 

59 

60 

61# Configure logging for CloudWatch 

62# In Lambda, the root logger is already configured, so we need to set the level explicitly 

63logger = logging.getLogger() 

64logger.setLevel(logging.INFO) 

65 

66# Lazy-initialized AWS clients 

67_eks_client = None 

68 

69# CloudFormation response status constants 

70SUCCESS = "SUCCESS" 

71FAILED = "FAILED" 

72 

73# Feature-gate placeholders follow an UPPER_SNAKE token convention 

74# ({{CLUSTER_OBSERVABILITY_ENABLED}}, {{FSX_FILE_SYSTEM_ID}}, {{VALKEY_ENDPOINT}}, 

75# ...). A manifest that still contains one *after* substitution belongs to a 

76# feature that is turned off, so the file is skipped. The character class is 

77# deliberately restricted to A-Z/0-9/_ so the check never matches lower- or 

78# mixed-case double-brace tokens that are legitimate *content* in an applied 

79# manifest — e.g. Grafana dashboard legend fields ({{gpu}}, {{service}}, 

80# {{Hostname}}) in the observability dashboard ConfigMaps, which must survive 

81# substitution untouched and be applied verbatim. 

82_UNRESOLVED_PLACEHOLDER_RE = re.compile(r"\{\{[A-Z0-9_]+\}\}") 

83_HPA_REPLICA_OWNERSHIP_ANNOTATION = "gco.aws/hpa-controls-replicas" 

84_POST_HELM_PREFIX = "post-helm-" 

85_CLUSTER_SCOPE = "<cluster>" 

86_MAX_PLANNING_FAILURES = 20 

87_MAX_VALIDATION_FAILURES = 20 

88_GATEWAY_DELETE_WAIT_SECONDS = 270 

89_GATEWAY_DELETE_POLL_SECONDS = 5 

90 

91# Gateway API resources are installed after the pinned CRD bootstrap. Keep the 

92# exact group/version/plural/scope mapping in one place so apply and teardown 

93# cannot drift onto different objects. 

94_GATEWAY_CUSTOM_OBJECTS: dict[str, tuple[str, str, str, bool]] = { 

95 "GatewayClass": ("gateway.networking.k8s.io", "v1", "gatewayclasses", True), 

96 "Gateway": ("gateway.networking.k8s.io", "v1", "gateways", False), 

97 "HTTPRoute": ("gateway.networking.k8s.io", "v1", "httproutes", False), 

98 "LoadBalancerConfiguration": ( 

99 "gateway.k8s.aws", 

100 "v1", 

101 "loadbalancerconfigurations", 

102 False, 

103 ), 

104 "TargetGroupConfiguration": ( 

105 "gateway.k8s.aws", 

106 "v1", 

107 "targetgroupconfigurations", 

108 False, 

109 ), 

110} 

111 

112# Kueue queue-topology resources applied by post-helm-kueue-default-queues.yaml. 

113# Kept separate from _GATEWAY_CUSTOM_OBJECTS because gateway kinds get the 

114# ALB-finalizer teardown treatment while these are ordinary CRs; the same 

115# group/version/plural/scope discipline applies so apply and pruning cannot 

116# drift onto different objects. tests/test_kubectl_applier.py pins this map 

117# against the manifests directory exactly like the gateway map. 

118_QUEUEING_CUSTOM_OBJECTS: dict[str, tuple[str, str, str, bool]] = { 

119 "ResourceFlavor": ("kueue.x-k8s.io", "v1beta2", "resourceflavors", True), 

120 "ClusterQueue": ("kueue.x-k8s.io", "v1beta2", "clusterqueues", True), 

121 "LocalQueue": ("kueue.x-k8s.io", "v1beta2", "localqueues", False), 

122} 

123 

124# cert-manager resources behind GCO's internal PKI 

125# (post-helm-api-workload-certificates.yaml): the cluster-scoped bootstrap and 

126# CA ClusterIssuers, and the namespaced Certificates they sign (the CA itself 

127# and every in-cluster TLS leaf). cert-manager's chart installs the CRDs, so 

128# these are POST-HELM kinds; the file sorts before the Gateway resources. 

129# Issuer stays supported for namespaced issuers even though GCO ships none. 

130_CERT_MANAGER_CUSTOM_OBJECTS: dict[str, tuple[str, str, str, bool]] = { 

131 "ClusterIssuer": ("cert-manager.io", "v1", "clusterissuers", True), 

132 "Issuer": ("cert-manager.io", "v1", "issuers", False), 

133 "Certificate": ("cert-manager.io", "v1", "certificates", False), 

134} 

135 

136# Argo CD objects GCO owns for the self-managed Argo CD (helm.argocd): the 

137# fenced AppProject (post-helm-argocd-access.yaml) and the optional root 

138# Application (post-helm-argocd-gitops.yaml). The argo-cd chart installs their 

139# CRDs, so they belong to the POST-HELM pass. Ordinary namespaced CRs in the 

140# ``argocd`` namespace; readiness is exact existence only — a tenant 

141# repository that fails to sync must never fail a GCO deployment (``gco gitops 

142# status`` and the Argo CD UI surface sync/health instead). 

143_ARGOCD_CUSTOM_OBJECTS: dict[str, tuple[str, str, str, bool]] = { 

144 "AppProject": ("argoproj.io", "v1alpha1", "appprojects", False), 

145 "Application": ("argoproj.io", "v1alpha1", "applications", False), 

146} 

147# The Crossplane composition function GCO installs for the self-managed 

148# Crossplane (helm.crossplane; post-helm-crossplane.yaml). Cluster-scoped; the 

149# chart installs its CRD, so it is a POST-HELM kind. Readiness is exact 

150# existence only — the package pull is Crossplane's to report. 

151_CROSSPLANE_CUSTOM_OBJECTS: dict[str, tuple[str, str, str, bool]] = { 

152 "Function": ("pkg.crossplane.io", "v1", "functions", True), 

153} 

154# The trust-manager Bundle that publishes the internal CA certificate to 

155# MLflow's clients in gco-jobs (post-helm-mlflow-tls.yaml). Cluster-scoped; the 

156# trust-manager chart installs its CRD, so it is a POST-HELM kind. Readiness 

157# is a Synced condition for the current generation: trust-manager has written 

158# the target ConfigMap from the current spec and source. 

159_TRUST_MANAGER_CUSTOM_OBJECTS: dict[str, tuple[str, str, str, bool]] = { 

160 "Bundle": ("trust.cert-manager.io", "v1alpha1", "bundles", True), 

161} 

162 

163# Prometheus Operator kinds (monitoring.coreos.com) the applier creates or 

164# patches through the custom-objects API, with their resource plurals. 

165_MONITORING_CUSTOM_OBJECT_PLURALS: dict[str, str] = { 

166 "ServiceMonitor": "servicemonitors", 

167 "PodMonitor": "podmonitors", 

168 "PrometheusRule": "prometheusrules", 

169} 

170 

171# A Secret annotated with this marker names (comma-separated) the data keys 

172# the applier fills with a fresh random value when it CREATES the Secret. An 

173# existing Secret is never overwritten: the generated values are credentials 

174# other objects already hold (a database's password), so a later apply only 

175# adds keys the Secret is missing. The manifest itself carries no data for 

176# those keys, so no credential ever appears in the repository, the Step 

177# Functions input or the applier's logs. Used for the Slurm accounting 

178# database (29-slurm-accounting-db.yaml). 

179_GENERATE_SECRET_KEYS_ANNOTATION = "gco.io/generate-keys" # nosec B105 # annotation key 

180_GENERATED_SECRET_BYTES = 32 

181 

182# Services annotated with this marker are validated for exact existence only; 

183# a ready EndpointSlice endpoint is not required. Reserved for Services whose 

184# backends schedule exclusively onto accelerator nodes that a fresh cluster 

185# does not have yet (for example the DCGM exporter DaemonSet). 

186_ALLOW_EMPTY_ENDPOINTS_ANNOTATION = "gco.io/allow-empty-endpoints" 

187 

188# This is the authoritative set of kinds the applier knows how to create or 

189# patch. Planning rejects anything else before the first Kubernetes mutation, 

190# which prevents a newly added raw manifest from being silently ignored. 

191_SUPPORTED_MANIFEST_KINDS = frozenset( 

192 { 

193 "APIService", 

194 "AppProject", 

195 "Application", 

196 "Bundle", 

197 "Certificate", 

198 "ClusterIssuer", 

199 "ClusterRole", 

200 "ClusterRoleBinding", 

201 "ClusterTrainingRuntime", 

202 "ConfigMap", 

203 "CronJob", 

204 "CustomResourceDefinition", 

205 "DaemonSet", 

206 "Deployment", 

207 "DeviceClass", 

208 "EC2NodeClass", 

209 "Function", 

210 "Gateway", 

211 "GatewayClass", 

212 "HTTPRoute", 

213 "HorizontalPodAutoscaler", 

214 "Issuer", 

215 "Job", 

216 "Lease", 

217 "LimitRange", 

218 "LoadBalancerConfiguration", 

219 "Namespace", 

220 "NetworkPolicy", 

221 "NodePool", 

222 "PersistentVolume", 

223 "PersistentVolumeClaim", 

224 "Pod", 

225 "ClusterQueue", 

226 "LocalQueue", 

227 "PodDisruptionBudget", 

228 "PodMonitor", 

229 "PriorityClass", 

230 "PrometheusRule", 

231 "ResourceFlavor", 

232 "ResourceQuota", 

233 "Role", 

234 "RoleBinding", 

235 "ScaledJob", 

236 "ScaledObject", 

237 "Secret", 

238 "Service", 

239 "ServiceAccount", 

240 "ServiceMonitor", 

241 "StatefulSet", 

242 "StorageClass", 

243 "TargetGroupConfiguration", 

244 "Token", 

245 "ValidatingAdmissionPolicy", 

246 "ValidatingAdmissionPolicyBinding", 

247 } 

248) 

249_CLUSTER_SCOPED_KINDS = frozenset( 

250 { 

251 "APIService", 

252 "Bundle", 

253 "ClusterIssuer", 

254 "ClusterQueue", 

255 "ClusterRole", 

256 "ClusterRoleBinding", 

257 "ClusterTrainingRuntime", 

258 "CustomResourceDefinition", 

259 "DeviceClass", 

260 "EC2NodeClass", 

261 "Function", 

262 "GatewayClass", 

263 "Namespace", 

264 "NodePool", 

265 "PersistentVolume", 

266 "PriorityClass", 

267 "ResourceFlavor", 

268 "StorageClass", 

269 "ValidatingAdmissionPolicy", 

270 "ValidatingAdmissionPolicyBinding", 

271 } 

272) 

273_IDENTITY_FIELDS = ("apiVersion", "kind", "namespace", "name", "sourceFile", "phase") 

274 

275 

276def _public_manifest_identity(resource: dict[str, Any]) -> dict[str, str]: 

277 """Return the serializable, normalized identity for a planned resource.""" 

278 return {field: resource[field] for field in _IDENTITY_FIELDS} 

279 

280 

281def _planning_error_message(errors: list[str], failure_count: int) -> str: 

282 """Build a bounded planning failure with enough source context to act on.""" 

283 hidden = failure_count - len(errors) 

284 suffix = f"; ... {hidden} additional error(s)" if hidden else "" 

285 return "Manifest planning failed: " + "; ".join(errors) + suffix 

286 

287 

288# --------------------------------------------------------------------------- 

289# Cross-phase ServiceAccount/token-projection consistency guard. 

290# 

291# The 2026-08 SQS submission-path outage: hardening a ServiceAccount with 

292# ``automountServiceAccountToken: false`` (base phase) and projecting the 

293# compensating kubernetes-audience token onto its workload (post-Helm phase) 

294# are one logical change split across two apply invocations. A redeploy that 

295# ran only the base pass left the live queue-processor pod with no way to 

296# authenticate to the Kubernetes API, crash-looping before its first SQS 

297# receive. The two files cannot apply in one transaction (the ScaledJob needs 

298# KEDA CRDs that do not exist in the base phase), so the planner enforces the 

299# pairing instead — and because BOTH phases are always planned before either 

300# is applied, a violation fails the base pass too. 

301 

302# Standard in-cluster credential mount point the kubernetes client reads. 

303_KUBE_SERVICEACCOUNT_MOUNT = "/var/run/secrets/kubernetes.io/serviceaccount" 

304 

305# Where each workload kind keeps its pod spec. 

306_POD_SPEC_PATHS: dict[str, tuple[str, ...]] = { 

307 "Pod": ("spec",), 

308 "Deployment": ("spec", "template", "spec"), 

309 "StatefulSet": ("spec", "template", "spec"), 

310 "DaemonSet": ("spec", "template", "spec"), 

311 "Job": ("spec", "template", "spec"), 

312 "CronJob": ("spec", "jobTemplate", "spec", "template", "spec"), 

313 "ScaledJob": ("spec", "jobTargetRef", "template", "spec"), 

314} 

315 

316 

317def _planned_pod_spec(document: dict[str, Any]) -> dict[str, Any] | None: 

318 """Return the pod spec embedded in a planned workload document, if any.""" 

319 path = _POD_SPEC_PATHS.get(str(document.get("kind"))) 

320 if path is None: 

321 return None 

322 node: Any = document 

323 for key in path: 

324 if not isinstance(node, dict): 

325 return None 

326 node = node.get(key) 

327 return node if isinstance(node, dict) else None 

328 

329 

330def _pod_spec_projects_service_account_token(pod_spec: dict[str, Any]) -> bool: 

331 """True when a projected serviceAccountToken is mounted at the standard path.""" 

332 token_volumes: set[str] = set() 

333 for volume in pod_spec.get("volumes") or []: 

334 if not isinstance(volume, dict): 

335 continue 

336 projected = volume.get("projected") 

337 if not isinstance(projected, dict): 

338 continue 

339 sources = projected.get("sources") or [] 

340 has_token_source = any( 

341 isinstance(source, dict) and "serviceAccountToken" in source for source in sources 

342 ) 

343 if has_token_source and isinstance(volume.get("name"), str): 

344 token_volumes.add(volume["name"]) 

345 if not token_volumes: 

346 return False 

347 for containers_key in ("containers", "initContainers", "ephemeralContainers"): 

348 containers = pod_spec.get(containers_key) or [] 

349 if not isinstance(containers, list): 

350 continue 

351 for container in containers: 

352 if not isinstance(container, dict): 

353 continue 

354 for mount in container.get("volumeMounts") or []: 

355 if ( 

356 isinstance(mount, dict) 

357 and mount.get("mountPath") == _KUBE_SERVICEACCOUNT_MOUNT 

358 and mount.get("name") in token_volumes 

359 ): 

360 return True 

361 return False 

362 

363 

364def _automount_disabled_service_accounts( 

365 planned: list[dict[str, Any]], 

366) -> dict[tuple[str, str], str]: 

367 """Planned ServiceAccounts with automount disabled, keyed by (namespace, name).""" 

368 disabled: dict[tuple[str, str], str] = {} 

369 for item in planned: 

370 if item["kind"] != "ServiceAccount": 

371 continue 

372 if item["document"].get("automountServiceAccountToken") is False: 

373 disabled[(item["namespace"], item["name"])] = item["sourceFile"] 

374 return disabled 

375 

376 

377def _rbac_bound_service_accounts(planned: list[dict[str, Any]]) -> set[tuple[str, str]]: 

378 """ServiceAccounts referenced as subjects by planned (Cluster)RoleBindings. 

379 

380 An RBAC binding is the planned inventory's own declaration that the 

381 ServiceAccount is expected to call the Kubernetes API. Accounts with 

382 automount disabled and NO binding (workload identities that only hold 

383 AWS credentials, e.g. the inference proxy) are deliberately exempt from 

384 the token-projection invariant. 

385 """ 

386 bound: set[tuple[str, str]] = set() 

387 for item in planned: 

388 if item["kind"] not in ("RoleBinding", "ClusterRoleBinding"): 

389 continue 

390 default_namespace = item["namespace"] if item["kind"] == "RoleBinding" else None 

391 for subject in item["document"].get("subjects") or []: 

392 if not isinstance(subject, dict) or subject.get("kind") != "ServiceAccount": 

393 continue 

394 name = subject.get("name") 

395 namespace = subject.get("namespace") or default_namespace 

396 if isinstance(name, str) and isinstance(namespace, str): 

397 bound.add((namespace, name)) 

398 return bound 

399 

400 

401def _service_account_token_projection_errors( 

402 phases: dict[str, list[dict[str, Any]]], 

403) -> list[str]: 

404 """Cross-phase invariant: hardened, API-bound SAs need a projected token. 

405 

406 For every planned ServiceAccount (either phase) that sets 

407 ``automountServiceAccountToken: false`` AND is bound to the Kubernetes 

408 API by a planned RoleBinding/ClusterRoleBinding, every planned workload 

409 whose pod spec runs as that account must mount a projected 

410 serviceAccountToken at ``/var/run/secrets/kubernetes.io/serviceaccount``. 

411 """ 

412 planned = phases["base"] + phases["post-helm"] 

413 disabled = _automount_disabled_service_accounts(planned) 

414 if not disabled: 

415 return [] 

416 bound = _rbac_bound_service_accounts(planned) 

417 guarded = {identity: source for identity, source in disabled.items() if identity in bound} 

418 if not guarded: 

419 return [] 

420 

421 errors: list[str] = [] 

422 for item in planned: 

423 pod_spec = _planned_pod_spec(item["document"]) 

424 if pod_spec is None: 

425 continue 

426 service_account = pod_spec.get("serviceAccountName") 

427 if not isinstance(service_account, str) or not service_account: 

428 continue 

429 source_file = guarded.get((item["namespace"], service_account)) 

430 if source_file is None: 

431 continue 

432 if _pod_spec_projects_service_account_token(pod_spec): 

433 continue 

434 errors.append( 

435 f"{item['sourceFile']}: {item['kind']}/{item['namespace']}/{item['name']} runs as " 

436 f"ServiceAccount {service_account!r} ({source_file}), which sets " 

437 "automountServiceAccountToken: false and is RBAC-bound to the Kubernetes API, " 

438 f"but mounts no projected serviceAccountToken at {_KUBE_SERVICEACCOUNT_MOUNT} - " 

439 "the pod would have no credentials. Project the token (see the " 

440 "kubernetes-api-token volume in post-helm-sqs-consumer.yaml) or ship both " 

441 "halves of the hardening in one release" 

442 ) 

443 return errors 

444 

445 

446def _log_service_account_automount_flip( 

447 v1: Any, 

448 document: dict[str, Any], 

449 namespace: str, 

450 name: str, 

451 plan: dict[str, Any], 

452) -> None: 

453 """Name the workloads affected when automount is being flipped to false. 

454 

455 Clusters created by an older release are supported: their live 

456 ServiceAccount may still automount tokens while the incoming manifest 

457 disables it. This apply is the exact moment previously-running pods 

458 lose ambient credentials, so log every planned workload that references 

459 the account and whether each already carries the compensating projected 

460 token — the diagnostic that would have named the queue processor on the 

461 redeploy that caused the SQS outage. Best-effort: any read failure 

462 skips the diagnostic, never the apply. 

463 """ 

464 if document.get("automountServiceAccountToken") is not False: 

465 return 

466 try: 

467 live = v1.read_namespaced_service_account(name, namespace) 

468 except ApiException as e: 

469 if e.status != 404: 

470 logger.debug( 

471 "Automount-flip check could not read live ServiceAccount %s/%s: %s", 

472 namespace, 

473 name, 

474 e, 

475 ) 

476 return 

477 except Exception as e: # pragma: no cover - defensive; diagnostics never block 

478 logger.debug("Automount-flip check failed for ServiceAccount %s/%s: %s", namespace, name, e) 

479 return 

480 if getattr(live, "automount_service_account_token", None) is False: 

481 return # Already hardened on the live cluster; nothing is flipping. 

482 

483 references: list[str] = [] 

484 for item in plan["phases"]["base"] + plan["phases"]["post-helm"]: 

485 pod_spec = _planned_pod_spec(item["document"]) 

486 if ( 

487 pod_spec is None 

488 or item["namespace"] != namespace 

489 or pod_spec.get("serviceAccountName") != name 

490 ): 

491 continue 

492 projected = _pod_spec_projects_service_account_token(pod_spec) 

493 references.append( 

494 f"{item['phase']}:{item['sourceFile']} {item['kind']}/{item['name']} " 

495 f"projected-token={'present' if projected else 'MISSING'}" 

496 ) 

497 logger.warning( 

498 "ServiceAccount %s/%s: automountServiceAccountToken is being flipped to false on a " 

499 "live cluster; planned workloads running as it: %s", 

500 namespace, 

501 name, 

502 "; ".join(references) or "<none>", 

503 ) 

504 

505 

506def plan_manifests( 

507 manifests_dir: str, 

508 replacements: object, 

509) -> dict[str, Any]: 

510 """Plan the complete raw-manifest inventory without mutating the cluster. 

511 

512 Files are scanned in lexical order. Replacements are literal string 

513 replacements, then an unresolved UPPER_SNAKE placeholder gates the entire 

514 file out. Every remaining nonempty YAML document must have an exact, 

515 supported identity and be unique across both apply phases. 

516 

517 ``replacements`` is the ``ImageReplacements`` value exactly as it 

518 arrived in the CloudFormation / Step Functions event, so it is typed 

519 ``object`` and validated here: anything but a string-to-string mapping 

520 is reported as a planning failure rather than trusted. 

521 """ 

522 phases: dict[str, list[dict[str, Any]]] = {"base": [], "post-helm": []} 

523 skipped: dict[str, list[str]] = {"base": [], "post-helm": []} 

524 feature_gates: dict[str, set[str]] = {"base": set(), "post-helm": set()} 

525 errors: list[str] = [] 

526 failure_count = 0 

527 seen: dict[tuple[str, str, str, str], str] = {} 

528 

529 def add_error(message: str) -> None: 

530 nonlocal failure_count 

531 failure_count += 1 

532 if len(errors) < _MAX_PLANNING_FAILURES: 

533 errors.append(message[:500]) 

534 

535 replacement_items: list[tuple[str, str]] = [] 

536 if not isinstance(replacements, dict): 

537 add_error("ImageReplacements must be a string-to-string mapping") 

538 else: 

539 for key, value in replacements.items(): 

540 if not isinstance(key, str) or not key: 

541 add_error("ImageReplacements contains an empty or non-string key") 

542 continue 

543 if not isinstance(value, str): 

544 add_error(f"ImageReplacements value for {key!r} is not a string") 

545 continue 

546 replacement_items.append((key, value)) 

547 

548 if failure_count: 

549 raise ValueError(_planning_error_message(errors, failure_count)) 

550 

551 for filename in sorted(os.listdir(manifests_dir)): 

552 if not filename.endswith((".yaml", ".yml")): 

553 continue 

554 

555 phase = "post-helm" if filename.startswith(_POST_HELM_PREFIX) else "base" 

556 if phase == "post-helm": 

557 skipped["base"].append(f"{filename}:deferred-to-post-helm") 

558 

559 filepath = os.path.join(manifests_dir, filename) 

560 try: 

561 with open(filepath, encoding="utf-8") as manifest_file: 

562 content = manifest_file.read() 

563 except OSError as exc: 

564 add_error(f"{filename}: unable to read manifest: {exc}") 

565 continue 

566 

567 for key, value in replacement_items: 

568 content = content.replace(key, value) 

569 

570 unresolved = sorted(set(_UNRESOLVED_PLACEHOLDER_RE.findall(content))) 

571 if unresolved: 

572 skipped[phase].append(f"{filename}:unreplaced-placeholders") 

573 feature_gates[phase].update(unresolved) 

574 logger.info( 

575 "Planning excludes %s - unresolved feature placeholder(s): %s", 

576 filename, 

577 ", ".join(unresolved), 

578 ) 

579 continue 

580 

581 try: 

582 documents = list(yaml.safe_load_all(content)) 

583 except yaml.YAMLError as exc: 

584 add_error(f"{filename}: invalid YAML: {exc}") 

585 continue 

586 

587 for document_index, document in enumerate(documents, start=1): 

588 if document is None: 

589 continue 

590 location = f"{filename} document {document_index}" 

591 if not isinstance(document, dict): 

592 add_error(f"{location}: document must be a mapping") 

593 continue 

594 

595 api_version = document.get("apiVersion") 

596 kind = document.get("kind") 

597 metadata = document.get("metadata") 

598 if not isinstance(api_version, str) or not api_version.strip(): 

599 add_error(f"{location}: apiVersion must be a nonempty string") 

600 continue 

601 if not isinstance(kind, str) or not kind.strip(): 

602 add_error(f"{location}: kind must be a nonempty string") 

603 continue 

604 api_version = api_version.strip() 

605 kind = kind.strip() 

606 if kind not in _SUPPORTED_MANIFEST_KINDS: 

607 add_error(f"{location}: unsupported kind {kind!r}") 

608 continue 

609 if not isinstance(metadata, dict): 

610 add_error(f"{location}: metadata must be a mapping") 

611 continue 

612 

613 name = metadata.get("name") 

614 if not isinstance(name, str) or not name.strip(): 

615 add_error(f"{location}: metadata.name must be a nonempty string") 

616 continue 

617 name = name.strip() 

618 

619 if kind in _CLUSTER_SCOPED_KINDS: 

620 namespace = _CLUSTER_SCOPE 

621 else: 

622 namespace_value = metadata.get("namespace", "default") 

623 if not isinstance(namespace_value, str) or not namespace_value.strip(): 

624 add_error( 

625 f"{location}: metadata.namespace must be a nonempty string when present" 

626 ) 

627 continue 

628 namespace = namespace_value.strip() 

629 

630 duplicate_key = (api_version, kind, namespace, name) 

631 previous = seen.get(duplicate_key) 

632 if previous is not None: 

633 add_error( 

634 f"{location}: duplicate {api_version}/{kind}/{namespace}/{name}; " 

635 f"first declared in {previous}" 

636 ) 

637 continue 

638 seen[duplicate_key] = location 

639 

640 phases[phase].append( 

641 { 

642 "apiVersion": api_version, 

643 "kind": kind, 

644 "namespace": namespace, 

645 "name": name, 

646 "sourceFile": filename, 

647 "phase": phase, 

648 "document": document, 

649 } 

650 ) 

651 

652 # Cross-phase consistency: a hardened ServiceAccount and the token 

653 # projection that compensates for it must ship together. Runs over BOTH 

654 # planned phases, so the base pass fails before its first Kubernetes 

655 # mutation even when the violation lives in a post-Helm file. 

656 for message in _service_account_token_projection_errors(phases): 

657 add_error(message) 

658 

659 if failure_count: 

660 raise ValueError(_planning_error_message(errors, failure_count)) 

661 

662 return { 

663 "phases": phases, 

664 "skipped": skipped, 

665 "featureGates": { 

666 phase: sorted(placeholders) for phase, placeholders in feature_gates.items() 

667 }, 

668 } 

669 

670 

671def _hpa_owns_replicas(document: dict[str, Any]) -> bool: 

672 """Whether a Deployment manifest hands its replica count to an HPA.""" 

673 metadata = document.get("metadata") 

674 annotations = metadata.get("annotations") if isinstance(metadata, dict) else None 

675 return ( 

676 isinstance(annotations, dict) 

677 and annotations.get(_HPA_REPLICA_OWNERSHIP_ANNOTATION) == "true" 

678 ) 

679 

680 

681def _generated_secret_keys(document: dict[str, Any]) -> list[str]: 

682 """The data keys a Secret manifest asks the applier to generate, if any. 

683 

684 Read from the ``gco.io/generate-keys`` annotation (comma-separated). A 

685 manifest that lists a key AND ships a value for it is contradictory and 

686 fails planning-time-equivalent here, before any Kubernetes call. 

687 """ 

688 metadata = document.get("metadata") 

689 annotations = metadata.get("annotations") if isinstance(metadata, dict) else None 

690 raw = ( 

691 annotations.get(_GENERATE_SECRET_KEYS_ANNOTATION) if isinstance(annotations, dict) else None 

692 ) 

693 if not isinstance(raw, str) or not raw.strip(): 

694 return [] 

695 keys = [part.strip() for part in raw.split(",") if part.strip()] 

696 shipped: set[str] = set() 

697 for field in ("data", "stringData"): 

698 values = document.get(field) 

699 if isinstance(values, dict): 

700 shipped.update(str(key) for key in values) 

701 conflicting = sorted(set(keys) & shipped) 

702 if conflicting: 

703 name = metadata.get("name") if isinstance(metadata, dict) else None 

704 raise ValueError( 

705 f"Secret {name!r} lists generated key(s) {conflicting} in " 

706 f"{_GENERATE_SECRET_KEYS_ANNOTATION} but also ships a value for them" 

707 ) 

708 return keys 

709 

710 

711def _apply_generated_secret( 

712 v1: Any, 

713 document: dict[str, Any], 

714 namespace: str, 

715 name: str, 

716 generated_keys: list[str], 

717) -> None: 

718 """Create a Secret with fresh random values for its generated keys. 

719 

720 When the Secret already exists only the generated keys it lacks are 

721 added, never replacing a value: the consumers (the Slurm accounting 

722 database and slurmdbd) hold the current one, and rotating it is an 

723 operator action, not a side effect of a redeploy. Values are URL-safe 

724 random strings of ``_GENERATED_SECRET_BYTES`` bytes of entropy and are 

725 never logged. 

726 """ 

727 import secrets 

728 

729 try: 

730 existing = v1.read_namespaced_secret(name, namespace) 

731 except ApiException as e: 

732 if e.status != 404: 

733 raise 

734 existing = None 

735 

736 # The three log lines below carry a targeted `nosemgrep`: the 

737 # logger-credential-disclosure rule matches the word "Secret" in the 

738 # message, but only the Secret's namespace/name and a key *count* are 

739 # logged. The generated values go straight into the API call and are 

740 # never bound to a name this function could log. 

741 if existing is None: 

742 body = copy.deepcopy(document) 

743 string_data = body.setdefault("stringData", {}) 

744 for key in generated_keys: 

745 string_data[key] = secrets.token_urlsafe(_GENERATED_SECRET_BYTES) 

746 v1.create_namespaced_secret(namespace, body=body) 

747 # nosemgrep: python-logger-credential-disclosure 

748 logger.info( 

749 "Generated %d key(s) for new Secret %s/%s", len(generated_keys), namespace, name 

750 ) 

751 return 

752 

753 present = set((getattr(existing, "data", None) or {}).keys()) 

754 missing = [key for key in generated_keys if key not in present] 

755 if not missing: 

756 # nosemgrep: python-logger-credential-disclosure 

757 logger.info( 

758 "Secret %s/%s already carries its generated keys; left unchanged", namespace, name 

759 ) 

760 return 

761 patch = {"stringData": {key: secrets.token_urlsafe(_GENERATED_SECRET_BYTES) for key in missing}} 

762 v1.patch_namespaced_secret(name, namespace, body=patch) 

763 # nosemgrep: python-logger-credential-disclosure 

764 logger.info("Added %d missing generated key(s) to Secret %s/%s", len(missing), namespace, name) 

765 

766 

767def _deployment_patch_body(document: dict[str, Any]) -> dict[str, Any]: 

768 """Copy a Deployment update without replicas when its HPA owns scale.""" 

769 patch_body = copy.deepcopy(document) 

770 if not _hpa_owns_replicas(patch_body): 

771 return patch_body 

772 

773 spec = patch_body.get("spec") 

774 if isinstance(spec, dict): 

775 spec.pop("replicas", None) 

776 return patch_body 

777 

778 

779def _api_error_detail(exc: ApiException) -> str: 

780 """Return the API server's own message for a failed call, as a log suffix. 

781 

782 ``ApiException.reason`` is only the HTTP phrase ("Unprocessable Entity"); 

783 the field-level cause ("spec.template.spec.containers[0].livenessProbe 

784 .tcpSocket: Forbidden: may not specify more than 1 handler type") is the 

785 ``message`` of the Status object in the response body. Without it a 

786 rejected manifest is a dead end in the convergence log. Returns "" when 

787 the body is absent or not a Status document. 

788 """ 

789 body = getattr(exc, "body", None) 

790 if not body: 

791 return "" 

792 try: 

793 status = json.loads(body) 

794 except TypeError, ValueError: 

795 return "" 

796 message = status.get("message") if isinstance(status, dict) else None 

797 return f": {message}" if message else "" 

798 

799 

800def _patch_or_replace( 

801 api: Any, 

802 resource: str, 

803 name: str, 

804 namespace: str, 

805 document: dict[str, Any], 

806 *, 

807 patch_body: dict[str, Any] | None = None, 

808 keep_live_replicas: bool = False, 

809) -> None: 

810 """Strategic-merge-patch an existing object, replacing it when the merge is invalid. 

811 

812 A strategic merge keeps every live field the manifest no longer sets. For 

813 fields that are mutually exclusive that produces an object the API server 

814 rejects with 422, so an older install can never be patched forward: 

815 

816 - a container probe that moved from ``httpGet`` to ``exec`` (or, for the 

817 Slurm accounting MariaDB StatefulSet, from ``exec`` to ``tcpSocket``) 

818 ends up with both handlers ("may not specify more than 1 handler type"); 

819 - a PodDisruptionBudget that moved from ``minAvailable`` to 

820 ``maxUnavailable`` ends up with both. 

821 

822 On 422 the manifest is the whole desired state, so this replaces the object 

823 with it at the live ``resourceVersion`` (an optimistic-concurrency update, 

824 not a delete). An HPA-owned Deployment keeps its live replica count. A 

825 StatefulSet replace is still bound by the API server's immutability rules 

826 (``selector``, ``serviceName``, ``volumeClaimTemplates`` ...): a manifest 

827 that changes one of those fails loudly here, as it should. 

828 

829 ``resource`` is the client method suffix, e.g. ``namespaced_deployment`` 

830 for ``patch_/read_/replace_namespaced_deployment``. 

831 """ 

832 try: 

833 getattr(api, f"patch_{resource}")( 

834 name, namespace, body=document if patch_body is None else patch_body 

835 ) 

836 return 

837 except ApiException as exc: 

838 if exc.status != 422: 

839 raise 

840 patch_error = exc 

841 

842 live = getattr(api, f"read_{resource}")(name, namespace) 

843 body = copy.deepcopy(document) 

844 metadata = body.setdefault("metadata", {}) 

845 metadata["resourceVersion"] = live.metadata.resource_version 

846 if keep_live_replicas: 

847 live_replicas = getattr(getattr(live, "spec", None), "replicas", None) 

848 spec = body.setdefault("spec", {}) 

849 if live_replicas is None: 

850 spec.pop("replicas", None) 

851 else: 

852 spec["replicas"] = live_replicas 

853 logger.warning( 

854 "Strategic merge of %s %s/%s was rejected (%s); replacing it with the manifest", 

855 resource, 

856 namespace, 

857 name, 

858 patch_error.reason, 

859 ) 

860 getattr(api, f"replace_{resource}")(name, namespace, body=body) 

861 

862 

863# Exact resources owned by optional features. Keys include the apply phase so 

864# disabling one feature cannot delete similarly named or unrelated resources. 

865# Tuples are (apiVersion, kind, namespace-or-None, name); order is deliberate 

866# for dependent resources such as FSx claims before volumes before the class. 

867# post-helm-slurm-observability.yaml carries both the Slurm and the 

868# observability gate, so it applies only when both are on and is pruned when 

869# either is turned off: the same identities appear under both inventory keys. 

870_SLURM_OBSERVABILITY_RESOURCES: tuple[tuple[str, str, str | None, str], ...] = ( 

871 ("v1", "ConfigMap", "monitoring", "gco-dashboard-slurm"), 

872 ("monitoring.coreos.com/v1", "PrometheusRule", "monitoring", "gco-slurm-alerts"), 

873) 

874 

875_FEATURE_RESOURCE_INVENTORY: dict[ 

876 tuple[str, bool], tuple[tuple[str, str, str | None, str], ...] 

877] = { 

878 ("{{FSX_FILE_SYSTEM_ID}}", False): ( 

879 ("v1", "PersistentVolumeClaim", "default", "gco-fsx-storage"), 

880 ("v1", "PersistentVolumeClaim", "gco-jobs", "gco-fsx-storage"), 

881 ("v1", "PersistentVolumeClaim", "gco-system", "gco-fsx-storage"), 

882 ("v1", "PersistentVolume", None, "gco-fsx-pv-default"), 

883 ("v1", "PersistentVolume", None, "gco-fsx-pv-jobs"), 

884 ("v1", "PersistentVolume", None, "gco-fsx-pv-system"), 

885 ("storage.k8s.io/v1", "StorageClass", None, "fsx-sc"), 

886 ), 

887 ("{{VALKEY_ENDPOINT}}", False): tuple( 

888 ("v1", "ConfigMap", namespace, "gco-valkey") 

889 for namespace in ("gco-system", "gco-jobs", "gco-inference") 

890 ), 

891 ("{{AURORA_PGVECTOR_ENDPOINT}}", False): tuple( 

892 ("v1", "ConfigMap", namespace, "gco-aurora-pgvector") 

893 for namespace in ("gco-system", "gco-jobs", "gco-inference") 

894 ), 

895 ("{{VECTOR_STORE_TABLE_NAME}}", False): tuple( 

896 ("v1", "ConfigMap", namespace, "gco-vector-store") 

897 for namespace in ("gco-system", "gco-jobs", "gco-inference") 

898 ), 

899 ("{{CLUSTER_OBSERVABILITY_ENABLED}}", False): ( 

900 ("apps/v1", "DaemonSet", "kube-system", "dcgm-exporter"), 

901 ("v1", "Service", "kube-system", "dcgm-exporter"), 

902 ("v1", "ConfigMap", "kube-system", "dcgm-device-counters"), 

903 ("storage.k8s.io/v1", "StorageClass", None, "gco-observability-gp3"), 

904 ), 

905 ("{{CLUSTER_OBSERVABILITY_ENABLED}}", True): ( 

906 ("batch/v1", "CronJob", "monitoring", "gco-grafana-admin-password-rotation"), 

907 ("v1", "ConfigMap", "monitoring", "gco-dashboard-gpu"), 

908 ("v1", "ConfigMap", "monitoring", "gco-dashboard-schedulers"), 

909 ("v1", "ConfigMap", "monitoring", "gco-dashboard-keda"), 

910 ("v1", "ConfigMap", "monitoring", "gco-dashboard-services"), 

911 ( 

912 "rbac.authorization.k8s.io/v1", 

913 "ClusterRoleBinding", 

914 None, 

915 "gco-prometheus-kueue-metrics", 

916 ), 

917 *tuple( 

918 ("monitoring.coreos.com/v1", "ServiceMonitor", "monitoring", name) 

919 for name in ( 

920 "gco-keda", 

921 "gco-volcano", 

922 "gco-kueue", 

923 "gco-kuberay", 

924 "gco-yunikorn", 

925 "gco-dcgm-exporter", 

926 "gco-slurm-controller", 

927 ) 

928 ), 

929 *_SLURM_OBSERVABILITY_RESOURCES, 

930 *tuple( 

931 ("monitoring.coreos.com/v1", "PodMonitor", "monitoring", name) 

932 for name in ( 

933 "gco-health-monitor", 

934 "gco-manifest-processor", 

935 "gco-inference-proxy", 

936 "gco-inference-monitor", 

937 ) 

938 ), 

939 # The monitoring half of the internal PKI 

940 # (post-helm-monitoring-tls.yaml). Each Certificate goes before its 

941 # Secret: cert-manager does not delete a Secret with its Certificate, 

942 # and a disabled feature must not leave key material behind; deleted 

943 # in this order, nothing re-issues it. 

944 ("v1", "Service", "monitoring", "grafana-tls"), 

945 ("cert-manager.io/v1", "Certificate", "monitoring", "grafana-tls"), 

946 ("v1", "Secret", "monitoring", "grafana-tls"), 

947 ("cert-manager.io/v1", "Certificate", "monitoring", "gco-monitoring-trust"), 

948 ("v1", "Secret", "monitoring", "gco-monitoring-trust"), 

949 ("rbac.authorization.k8s.io/v1", "RoleBinding", "monitoring", "gco-grafana-rotator"), 

950 ("rbac.authorization.k8s.io/v1", "Role", "monitoring", "gco-grafana-rotator"), 

951 ("v1", "ServiceAccount", "monitoring", "gco-grafana-rotator"), 

952 ), 

953 ("{{QUEUE_PROCESSOR_IMAGE}}", True): ( 

954 ("keda.sh/v1alpha1", "ScaledJob", "gco-system", "sqs-queue-processor"), 

955 ), 

956 # Optional manifest-processor CPU autoscaler (cdk.json 

957 # manifest_processor.autoscaling.enabled). Turning it off must also remove 

958 # the HPA, otherwise the last scale value would keep fighting the 

959 # Deployment's re-asserted replicas. 

960 ("{{MP_HPA_ENABLED}}", False): ( 

961 ("autoscaling/v2", "HorizontalPodAutoscaler", "gco-system", "manifest-processor-hpa"), 

962 ), 

963 ("{{KUEUE_ENABLED}}", True): ( 

964 # Deletion order matters: the LocalQueue references the ClusterQueue, 

965 # which references the ResourceFlavor. 

966 ("kueue.x-k8s.io/v1beta2", "LocalQueue", "gco-jobs", "gco-default"), 

967 ("kueue.x-k8s.io/v1beta2", "ClusterQueue", None, "gco-cluster-queue"), 

968 ("kueue.x-k8s.io/v1beta2", "ResourceFlavor", None, "gco-default-flavor"), 

969 ), 

970 # Slurm (cdk.json helm.slurm). The base pass owns the gp3 StorageClass the 

971 # controller's state volume and the accounting database bind to; the 

972 # post-Helm pass owns the NetworkPolicies, the GCO API's Slurm credential, 

973 # the metrics ServiceMonitor and the observability objects 

974 # (28-storage-slurm-gp3.yaml, post-helm-slurm-network.yaml, 

975 # post-helm-slurm-api-access.yaml, post-helm-slurm-observability.yaml). 

976 ("{{SLURM_ENABLED}}", False): (("storage.k8s.io/v1", "StorageClass", None, "gco-slurm-gp3"),), 

977 ("{{SLURM_ENABLED}}", True): ( 

978 # The API's credential first: the binding before the role it names, 

979 # then the Token. The Secret the operator minted from the Token is 

980 # owned by the chart's JWT key Secret rather than by the Token, so it 

981 # is listed here explicitly instead of relying on garbage collection. 

982 ( 

983 "rbac.authorization.k8s.io/v1", 

984 "RoleBinding", 

985 "gco-jobs", 

986 "gco-manifest-processor-slurm-token", 

987 ), 

988 ("rbac.authorization.k8s.io/v1", "Role", "gco-jobs", "gco-manifest-processor-slurm-token"), 

989 ("slinky.slurm.net/v1beta1", "Token", "gco-jobs", "slinky-slurm-token-gco-api"), 

990 ("v1", "Secret", "gco-jobs", "slinky-slurm-token-gco-api"), 

991 ("slinky.slurm.net/v1beta1", "Token", "gco-jobs", "slinky-slurm-token-client"), 

992 ("v1", "Secret", "gco-jobs", "slinky-slurm-client-token"), 

993 ("networking.k8s.io/v1", "NetworkPolicy", "gco-jobs", "allow-slurm-cluster-internal"), 

994 ("networking.k8s.io/v1", "NetworkPolicy", "gco-jobs", "allow-slurm-client-to-restapi"), 

995 ("networking.k8s.io/v1", "NetworkPolicy", "gco-jobs", "allow-slurm-client-egress"), 

996 ("networking.k8s.io/v1", "NetworkPolicy", "gco-jobs", "allow-slurm-operator-to-restapi"), 

997 ( 

998 "networking.k8s.io/v1", 

999 "NetworkPolicy", 

1000 "gco-jobs", 

1001 "allow-prometheus-to-slurm-controller", 

1002 ), 

1003 ( 

1004 "networking.k8s.io/v1", 

1005 "NetworkPolicy", 

1006 "gco-jobs", 

1007 "allow-manifest-processor-to-slurm-restapi", 

1008 ), 

1009 ( 

1010 "networking.k8s.io/v1", 

1011 "NetworkPolicy", 

1012 "gco-system", 

1013 "allow-manifest-processor-to-slurm-restapi", 

1014 ), 

1015 *_SLURM_OBSERVABILITY_RESOURCES, 

1016 ), 

1017 # The in-cluster accounting database (29-slurm-accounting-db.yaml, base 

1018 # pass so MariaDB is up before the Slinky chart starts slurmdbd and 

1019 # slurmctld, whose first boot refuses to run without the dbd) and its 

1020 # backup CronJob (post-helm-slurm-accounting-backup.yaml), each on its own 

1021 # gate so an external database or backups-off keeps the rest. The 

1022 # StatefulSet's persistentVolumeClaimRetentionPolicy deletes the data 

1023 # claim with it: the nightly dumps in the regional bucket are the record, 

1024 # exactly as the MLflow claim below is pruned while its artifacts stay in 

1025 # S3. The password Secret is generated in-cluster by the applier 

1026 # (gco.io/generate-keys) and goes with the database. 

1027 ("{{SLURM_ACCOUNTING_DB_ENABLED}}", False): ( 

1028 ("apps/v1", "StatefulSet", "gco-jobs", "slinky-slurm-mariadb"), 

1029 ("v1", "Service", "gco-jobs", "slinky-slurm-mariadb"), 

1030 ("v1", "Secret", "gco-jobs", "slinky-slurm-mariadb"), 

1031 ), 

1032 ("{{SLURM_ACCOUNTING_BACKUP_ENABLED}}", True): ( 

1033 ("batch/v1", "CronJob", "gco-jobs", "slinky-slurm-accounting-backup"), 

1034 ), 

1035 ("{{KUBEFLOW_TRAINER_ENABLED}}", True): ( 

1036 ("trainer.kubeflow.org/v1alpha1", "ClusterTrainingRuntime", None, "torch-distributed"), 

1037 ), 

1038 ("{{MLFLOW_ENABLED}}", True): ( 

1039 # The claim is created BY THE CHART (storage.enabled), not by a 

1040 # shipped manifest — it appears here because helm uninstall never 

1041 # deletes chart PVCs, so disabling the feature would otherwise leak 

1042 # the volume forever. Deliberately destructive on disable: the claim 

1043 # holds the tracking server's SQLite run METADATA. Run artifacts 

1044 # live in S3 (untouched). 

1045 ("v1", "PersistentVolumeClaim", "monitoring", "mlflow"), 

1046 ("networking.k8s.io/v1", "NetworkPolicy", "gco-jobs", "allow-mlflow-clients"), 

1047 # The server's own network posture; GCO owns it because the chart's 

1048 # policy drops kubelet probes (post-helm-mlflow-network.yaml). 

1049 ("networking.k8s.io/v1", "NetworkPolicy", "monitoring", "mlflow-server"), 

1050 # The HTTPS front door and its trust (post-helm-mlflow-tls.yaml). Each 

1051 # Certificate goes before its Secret, as for the observability leaves. 

1052 # The Bundle goes before the ConfigMap trust-manager writes from it, 

1053 # so nothing rewrites the ConfigMap once it is deleted; the Bundle 

1054 # owns it, but pruning it here does not wait on garbage collection. 

1055 # helm uninstall keeps the Bundle CRD, so the Bundle delete resolves. 

1056 ("v1", "Service", "monitoring", "mlflow-tls"), 

1057 ("cert-manager.io/v1", "Certificate", "monitoring", "mlflow-tls"), 

1058 ("v1", "Secret", "monitoring", "mlflow-tls"), 

1059 ("trust.cert-manager.io/v1alpha1", "Bundle", None, "gco-internal-ca"), 

1060 ("v1", "ConfigMap", "gco-jobs", "gco-internal-ca"), 

1061 ("cert-manager.io/v1", "Certificate", "trust-manager", "gco-internal-ca-source"), 

1062 ("v1", "Secret", "trust-manager", "gco-internal-ca-source"), 

1063 ), 

1064 ("{{COST_MONITORING_ENABLED}}", False): ( 

1065 ("apps/v1", "Deployment", "gco-system", "cost-monitor"), 

1066 ("v1", "Service", "gco-system", "cost-monitor"), 

1067 ("v1", "ServiceAccount", "gco-system", "gco-cost-monitor-sa"), 

1068 ( 

1069 "networking.k8s.io/v1", 

1070 "NetworkPolicy", 

1071 "gco-system", 

1072 "allow-manifest-processor-to-cost-monitor-ingress", 

1073 ), 

1074 ( 

1075 "networking.k8s.io/v1", 

1076 "NetworkPolicy", 

1077 "gco-system", 

1078 "allow-cost-monitor-to-opencost", 

1079 ), 

1080 ( 

1081 "networking.k8s.io/v1", 

1082 "NetworkPolicy", 

1083 "gco-system", 

1084 "allow-manifest-processor-to-cost-monitor-egress", 

1085 ), 

1086 ), 

1087 ("{{COST_MONITORING_ENABLED}}", True): ( 

1088 ("v1", "ConfigMap", "monitoring", "gco-dashboard-cost"), 

1089 # post-helm-cost-monitoring-tls.yaml; each Certificate before its 

1090 # Secret, as for the observability leaves above. 

1091 ("v1", "Service", "monitoring", "opencost-tls"), 

1092 ("cert-manager.io/v1", "Certificate", "monitoring", "opencost-tls"), 

1093 ("v1", "Secret", "monitoring", "opencost-tls"), 

1094 ("cert-manager.io/v1", "Certificate", "gco-system", "cost-monitor-tls"), 

1095 ("v1", "Secret", "gco-system", "cost-monitor-tls"), 

1096 ), 

1097 # Self-managed Argo CD (cdk.json helm.argocd; post-helm-argocd-access.yaml). 

1098 # Bindings before the roles they reference. The AppProject is normally 

1099 # gone already: the helm installer deletes every argoproj.io object before 

1100 # it uninstalls the chart (the CRDs stay, so this delete is a clean no-op). 

1101 ("{{ARGOCD_ENABLED}}", True): ( 

1102 ("rbac.authorization.k8s.io/v1", "RoleBinding", "gco-jobs", "gco-argocd-deploy"), 

1103 ("rbac.authorization.k8s.io/v1", "Role", "gco-jobs", "gco-argocd-deploy"), 

1104 ("rbac.authorization.k8s.io/v1", "RoleBinding", "gco-inference", "gco-argocd-deploy"), 

1105 ("rbac.authorization.k8s.io/v1", "Role", "gco-inference", "gco-argocd-deploy"), 

1106 ("rbac.authorization.k8s.io/v1", "RoleBinding", "gco-jobs", "gco-argocd-read"), 

1107 ("rbac.authorization.k8s.io/v1", "Role", "gco-jobs", "gco-argocd-read"), 

1108 ("rbac.authorization.k8s.io/v1", "RoleBinding", "gco-inference", "gco-argocd-read"), 

1109 ("rbac.authorization.k8s.io/v1", "Role", "gco-inference", "gco-argocd-read"), 

1110 ("argoproj.io/v1alpha1", "AppProject", "argocd", "gco-tenants"), 

1111 ), 

1112 # post-helm-argocd-gitops.yaml: the GitOps hand-off (helm.argocd.gitops). 

1113 # The Application carries no resources-finalizer, so deleting it detaches 

1114 # the tenant workloads from Git instead of deleting them. 

1115 ("{{ARGOCD_GITOPS_REPO_URL}}", True): ( 

1116 ("argoproj.io/v1alpha1", "Application", "argocd", "gco-gitops-root"), 

1117 ), 

1118 # Self-managed Crossplane (cdk.json helm.crossplane; post-helm-crossplane.yaml). 

1119 # Bindings before the roles they reference; the Function (and with it 

1120 # the function's Deployment) goes last. As with Argo CD, the helm 

1121 # installer has normally deleted every pkg.crossplane.io object already. 

1122 ("{{CROSSPLANE_ENABLED}}", True): ( 

1123 ("rbac.authorization.k8s.io/v1", "ClusterRoleBinding", None, "gco-crossview-read"), 

1124 ("rbac.authorization.k8s.io/v1", "ClusterRole", None, "gco-crossview-read"), 

1125 ( 

1126 "rbac.authorization.k8s.io/v1", 

1127 "ClusterRoleBinding", 

1128 None, 

1129 "gco-crossview-crossplane-view", 

1130 ), 

1131 ("rbac.authorization.k8s.io/v1", "RoleBinding", "gco-jobs", "gco-crossplane-compose"), 

1132 ("rbac.authorization.k8s.io/v1", "Role", "gco-jobs", "gco-crossplane-compose"), 

1133 ("rbac.authorization.k8s.io/v1", "RoleBinding", "gco-inference", "gco-crossplane-compose"), 

1134 ("rbac.authorization.k8s.io/v1", "Role", "gco-inference", "gco-crossplane-compose"), 

1135 ("rbac.authorization.k8s.io/v1", "ClusterRole", None, "gco-crossplane-read"), 

1136 ("pkg.crossplane.io/v1", "Function", None, "crossplane-contrib-function-go-templating"), 

1137 ), 

1138 # kro EKS Capability tenant RBAC (cdk.json eks_capabilities.kro; 

1139 # 07-kro-tenant-access.yaml). Bindings before the roles they reference. 

1140 ("{{KRO_CAPABILITY_USERNAME}}", False): ( 

1141 ("rbac.authorization.k8s.io/v1", "ClusterRoleBinding", None, "gco-kro-read"), 

1142 ("rbac.authorization.k8s.io/v1", "ClusterRole", None, "gco-kro-read"), 

1143 ("rbac.authorization.k8s.io/v1", "RoleBinding", "gco-jobs", "gco-kro-compose"), 

1144 ("rbac.authorization.k8s.io/v1", "Role", "gco-jobs", "gco-kro-compose"), 

1145 ("rbac.authorization.k8s.io/v1", "RoleBinding", "gco-inference", "gco-kro-compose"), 

1146 ("rbac.authorization.k8s.io/v1", "Role", "gco-inference", "gco-kro-compose"), 

1147 ), 

1148} 

1149 

1150 

1151# Resources GCO shipped in earlier releases that no longer appear in the 

1152# manifest set. The base apply pass deletes them exactly (missing = no-op) so 

1153# upgraded clusters do not keep orphaned objects running forever. 

1154# 

1155# nvidia-device-plugin-daemonset: GCO runs exclusively on EKS Auto Mode, which 

1156# ships its own NVIDIA device plugin built into the node ("runs automatically 

1157# and isn't visible as a daemon set" — the EKS auto-accelerated guide). The 

1158# community plugin GCO used to ship can never start on Auto Mode GPU nodes: 

1159# the runtime only injects the NVIDIA driver libraries for containers that 

1160# request them, so the plugin crash-loops with NVML ERROR_LIBRARY_NOT_FOUND 

1161# and permanently fails DaemonSet convergence (observed live the moment the 

1162# Slurm NodeSet provisioned the first GPU nodes). The built-in plugin 

1163# advertises nvidia.com/gpu on its own. 

1164# 

1165# allow-vpc-endpoint-egress / allow-ray-cluster-internal (gco-jobs): the 

1166# pre-v7.7 job-namespace model — HTTPS only to the VPC's own CIDR (there were 

1167# never VPC endpoints for that traffic to reach, so enforced it cut jobs off 

1168# from every AWS API) and a Ray-only peer rule. 03-network-policies.yaml now 

1169# ships allow-https-egress + allow-vpc-egress + allow-same-namespace, which 

1170# strictly contain both. NetworkPolicies union, so the leftovers would allow 

1171# nothing new — they are swept so the live policy set stays exactly the 

1172# shipped, documented one. 

1173# 

1174# gco-api-selfsigned (gco-system): the namespaced selfSigned Issuer that signed 

1175# each API workload leaf as its own root before the internal CA existed. The 

1176# leaves now name the gco-internal-ca ClusterIssuer 

1177# (post-helm-api-workload-certificates.yaml). Deleting the Issuer in the base 

1178# pass, before the post-Helm pass re-points them, is safe: a Certificate whose 

1179# issuer is briefly missing keeps its Secret, stays Ready while that Secret is 

1180# valid, and is re-issued by the new issuer as soon as its issuerRef changes. 

1181# On a fresh cluster the cert-manager CRDs do not exist yet in the base pass, 

1182# which the sweep treats as already absent. 

1183_LEGACY_REMOVED_RESOURCES: tuple[tuple[str, str, str | None, str], ...] = ( 

1184 ("apps/v1", "DaemonSet", "kube-system", "nvidia-device-plugin-daemonset"), 

1185 ("networking.k8s.io/v1", "NetworkPolicy", "gco-jobs", "allow-vpc-endpoint-egress"), 

1186 ("networking.k8s.io/v1", "NetworkPolicy", "gco-jobs", "allow-ray-cluster-internal"), 

1187 ("cert-manager.io/v1", "Issuer", "gco-system", "gco-api-selfsigned"), 

1188) 

1189 

1190 

1191def _delete_exact_resources( 

1192 targets: tuple[tuple[str, str, str | None, str], ...], 

1193 context: str, 

1194) -> dict[str, list[str]]: 

1195 """Delete an exact list of (apiVersion, kind, namespace, name) resources. 

1196 

1197 Missing resources and missing CRDs/API resource types are successful no-ops. 

1198 Every other error is returned so convergence fails instead of silently 

1199 leaving stale resources running. 

1200 """ 

1201 result: dict[str, list[str]] = {"pruned": [], "failed": []} 

1202 if not targets: 

1203 return result 

1204 

1205 dynamic_client = dynamic.DynamicClient(client.ApiClient()) 

1206 delete_options = client.V1DeleteOptions(propagation_policy="Background") 

1207 for api_version, kind, namespace, name in targets: 

1208 identifier = f"{api_version}/{kind}/{namespace or '<cluster>'}/{name}" 

1209 try: 

1210 resource = dynamic_client.resources.get(api_version=api_version, kind=kind) 

1211 kwargs: dict[str, Any] = {"name": name, "body": delete_options} 

1212 if namespace is not None: 

1213 kwargs["namespace"] = namespace 

1214 resource.delete(**kwargs) 

1215 result["pruned"].append(identifier) 

1216 logger.info("Pruned %s resource %s", context, identifier) 

1217 except ResourceNotFoundError, NotFoundError: 

1218 logger.info("%s resource already absent: %s", context, identifier) 

1219 except ApiException as exc: 

1220 if exc.status == 404: 

1221 logger.info("%s resource already absent: %s", context, identifier) 

1222 else: 

1223 failure = f"{identifier}:{exc.status}:{exc.reason}" 

1224 result["failed"].append(failure) 

1225 logger.error("Failed pruning %s resource %s", context, failure) 

1226 except Exception as exc: 

1227 if getattr(exc, "status", None) == 404: 

1228 logger.info("%s resource already absent: %s", context, identifier) 

1229 else: 

1230 failure = f"{identifier}:{exc}" 

1231 result["failed"].append(failure) 

1232 logger.error("Failed pruning %s resource %s", context, failure) 

1233 

1234 return result 

1235 

1236 

1237def _prune_disabled_feature(placeholder: str, post_helm: bool) -> dict[str, list[str]]: 

1238 """Delete only the exact resources managed by a disabled optional feature.""" 

1239 return _delete_exact_resources( 

1240 _FEATURE_RESOURCE_INVENTORY.get((placeholder, post_helm), ()), 

1241 "disabled-feature", 

1242 ) 

1243 

1244 

1245def _prune_legacy_removed_resources() -> dict[str, list[str]]: 

1246 """Delete resources shipped by earlier GCO releases and since removed.""" 

1247 return _delete_exact_resources(_LEGACY_REMOVED_RESOURCES, "legacy-removed") 

1248 

1249 

1250# --------------------------------------------------------------------------- 

1251# Tunables 

1252# --------------------------------------------------------------------------- 

1253 

1254# Maximum time to wait for a PersistentVolume or PersistentVolumeClaim to 

1255# disappear after we issue a delete. Needed when we're recreating a PV 

1256# whose ``volumeHandle`` changed (FSx/EFS ID rotated) or reconciling a 

1257# Lost PVC whose backing PV was just recreated. If you see this wait 

1258# consistently timing out, check for stuck finalizers (the handler 

1259# already clears the standard ``pv-protection`` / ``pvc-protection`` 

1260# finalizers) or for an AWS control-plane issue on the underlying 

1261# volume. Raising the value is safe — it only blocks the re-create path, 

1262# not steady-state applies. 

1263PV_PVC_DELETE_WAIT_SECONDS = 30 

1264 

1265# Interval between PV/PVC existence polls during the delete wait. 

1266PV_PVC_DELETE_POLL_INTERVAL_SECONDS = 1 

1267 

1268 

1269def get_eks_client() -> Any: 

1270 """Get EKS client with lazy initialization.""" 

1271 global _eks_client 

1272 if _eks_client is None: 

1273 _eks_client = boto3.client("eks") 

1274 return _eks_client 

1275 

1276 

1277def send_response( 

1278 event: dict[str, Any], 

1279 context: Any, 

1280 response_status: str, 

1281 response_data: dict[str, Any], 

1282 physical_resource_id: str, 

1283 reason: str | None = None, 

1284) -> None: 

1285 """Send response to CloudFormation.""" 

1286 response_body = { 

1287 "Status": response_status, 

1288 "Reason": reason or f"See CloudWatch Log Stream: {context.log_stream_name}", 

1289 "PhysicalResourceId": physical_resource_id, 

1290 "StackId": event["StackId"], 

1291 "RequestId": event["RequestId"], 

1292 "LogicalResourceId": event["LogicalResourceId"], 

1293 "Data": response_data, 

1294 } 

1295 

1296 logger.info(f"Sending response: {json.dumps(response_data)}") 

1297 

1298 # Timeout is for the CFN response callback (HTTP PUT to S3 presigned URL), 

1299 # not for manifest application. K8s API calls have their own timeouts. 

1300 http = urllib3.PoolManager() 

1301 try: 

1302 http.request( 

1303 "PUT", 

1304 event["ResponseURL"], 

1305 body=json.dumps(response_body).encode("utf-8"), 

1306 headers={"Content-Type": "application/json"}, 

1307 timeout=10.0, 

1308 ) 

1309 except Exception as e: 

1310 logger.error(f"Failed to send response: {e}") 

1311 

1312 

1313def get_eks_token(cluster_name: str, region: str) -> str: 

1314 """Generate EKS authentication token using STS presigned URL.""" 

1315 from botocore.signers import RequestSigner 

1316 

1317 # Create STS client 

1318 session = boto3.Session() 

1319 sts_client = session.client("sts", region_name=region) 

1320 service_id = sts_client.meta.service_model.service_id 

1321 

1322 # Create request signer 

1323 signer = RequestSigner( 

1324 service_id, region, "sts", "v4", session.get_credentials(), session.events 

1325 ) 

1326 

1327 # Build the presigned URL for GetCallerIdentity 

1328 params = { 

1329 "method": "GET", 

1330 "url": f"https://sts.{region}.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15", 

1331 "body": {}, 

1332 "headers": {"x-k8s-aws-id": cluster_name}, 

1333 "context": {}, 

1334 } 

1335 

1336 # Generate presigned URL (valid for 60 seconds) 

1337 url = signer.generate_presigned_url( 

1338 params, region_name=region, expires_in=60, operation_name="" 

1339 ) 

1340 

1341 # Encode as base64 and create the k8s-aws-v1 token 

1342 token_b64 = base64.urlsafe_b64encode(url.encode("utf-8")).decode("utf-8").rstrip("=") 

1343 return f"k8s-aws-v1.{token_b64}" 

1344 

1345 

1346def configure_k8s_client(cluster_name: str, region: str) -> None: 

1347 """Configure Kubernetes client for EKS cluster.""" 

1348 eks = get_eks_client() 

1349 

1350 # Get cluster info 

1351 cluster_info = eks.describe_cluster(name=cluster_name) 

1352 cluster = cluster_info["cluster"] 

1353 

1354 # Configure Kubernetes client 

1355 configuration = client.Configuration() 

1356 configuration.host = cluster["endpoint"] 

1357 configuration.verify_ssl = True 

1358 

1359 # Set connection timeouts (important for Lambda!) 

1360 configuration.connection_pool_maxsize = 1 

1361 configuration.retries = 3 

1362 # Set socket timeout to 30 seconds 

1363 import socket 

1364 

1365 socket.setdefaulttimeout(30) 

1366 

1367 # Decode and write CA certificate to temp file using secure method 

1368 ca_cert = base64.b64decode(cluster["certificateAuthority"]["data"]) 

1369 import tempfile 

1370 

1371 fd, ca_cert_path = tempfile.mkstemp(suffix=".crt") 

1372 try: 

1373 with os.fdopen(fd, "wb") as ca_file: 

1374 ca_file.write(ca_cert) 

1375 ca_file.flush() 

1376 configuration.ssl_ca_cert = ca_cert_path 

1377 except Exception: 

1378 os.close(fd) 

1379 raise 

1380 

1381 # Generate EKS authentication token 

1382 eks_token = get_eks_token(cluster_name, region) 

1383 

1384 # Set the bearer token 

1385 configuration.api_key = {"authorization": f"Bearer {eks_token}"} 

1386 

1387 logger.info( 

1388 f"✓ Configured Kubernetes client for cluster {cluster_name} at {cluster['endpoint']}" 

1389 ) 

1390 

1391 client.Configuration.set_default(configuration) 

1392 

1393 

1394def restart_deployments(namespace: str, deployment_names: list[str]) -> dict[str, Any]: 

1395 """ 

1396 Restart deployments by patching their spec with a restart annotation. 

1397 This forces Kubernetes to roll out new pods with the latest image. 

1398 """ 

1399 from datetime import datetime 

1400 

1401 apps_v1 = client.AppsV1Api() 

1402 restarted = [] 

1403 failed = [] 

1404 

1405 restart_time = datetime.now(UTC).isoformat() 

1406 

1407 for name in deployment_names: 

1408 try: 

1409 # Patch the deployment with a restart annotation 

1410 # This is equivalent to `kubectl rollout restart deployment` 

1411 patch = { 

1412 "spec": { 

1413 "template": { 

1414 "metadata": { 

1415 "annotations": {"kubectl.kubernetes.io/restartedAt": restart_time} 

1416 } 

1417 } 

1418 } 

1419 } 

1420 apps_v1.patch_namespaced_deployment(name, namespace, body=patch) 

1421 restarted.append(name) 

1422 logger.info(f"✓ Restarted deployment {name} in namespace {namespace}") 

1423 except ApiException as e: 

1424 # 404 means the deployment isn't installed on this cluster 

1425 # (e.g. fsx-csi when FSx is disabled) — that's not a failure. 

1426 if e.status == 404: 

1427 logger.info(f"Deployment {namespace}/{name} not found — skipping restart") 

1428 else: 

1429 logger.error(f"Failed to restart deployment {name}: {e.status} - {e.reason}") 

1430 failed.append(name) 

1431 

1432 return {"restarted": restarted, "failed": failed} 

1433 

1434 

1435def restart_daemonsets(namespace: str, daemonset_names: list[str]) -> dict[str, Any]: 

1436 """ 

1437 Restart daemonsets by patching their pod template with a restart annotation. 

1438 This forces Kubernetes to roll out new pods with the latest image or latest 

1439 service-account annotation set. 

1440 

1441 Used for IRSA-annotated addons whose pods still need to be re-mutated by 

1442 the EKS Pod Identity webhook after the addon's service account had its 

1443 role ARN annotation patched post-install. Without this, DaemonSet pods 

1444 like efs-csi-node, fsx-csi-node, and cloudwatch-agent keep their 

1445 original (credential-less) pod spec and silently fail with IMDS 401s. 

1446 """ 

1447 from datetime import datetime 

1448 

1449 apps_v1 = client.AppsV1Api() 

1450 restarted = [] 

1451 failed = [] 

1452 

1453 restart_time = datetime.now(UTC).isoformat() 

1454 

1455 for name in daemonset_names: 

1456 try: 

1457 patch = { 

1458 "spec": { 

1459 "template": { 

1460 "metadata": { 

1461 "annotations": {"kubectl.kubernetes.io/restartedAt": restart_time} 

1462 } 

1463 } 

1464 } 

1465 } 

1466 apps_v1.patch_namespaced_daemon_set(name, namespace, body=patch) 

1467 restarted.append(name) 

1468 logger.info(f"✓ Restarted daemonset {name} in namespace {namespace}") 

1469 except ApiException as e: 

1470 # 404 means the daemonset isn't installed on this cluster 

1471 # (e.g. fsx-csi-node when FSx is disabled) — that's expected, 

1472 # not a failure. 

1473 if e.status == 404: 

1474 logger.info(f"DaemonSet {namespace}/{name} not found — skipping restart") 

1475 else: 

1476 logger.error(f"Failed to restart daemonset {name}: {e.status} - {e.reason}") 

1477 failed.append(name) 

1478 

1479 return {"restarted": restarted, "failed": failed} 

1480 

1481 

1482# Every platform Deployment GCO ships in gco-system, with the dedicated 

1483# ServiceAccount it must run as: (namespace, deployment, service account). 

1484# tests/test_platform_workload_contract.py pins this to the manifests, so a new 

1485# service (or a renamed account) fails a unit test instead of silently escaping 

1486# the post-apply credential verification below. 

1487PLATFORM_DEPLOYMENTS: tuple[tuple[str, str, str], ...] = ( 

1488 ("gco-system", "health-monitor", "gco-health-monitor-sa"), 

1489 ("gco-system", "manifest-processor", "gco-manifest-processor-sa"), 

1490 ("gco-system", "inference-monitor", "gco-inference-monitor-sa"), 

1491 ("gco-system", "inference-proxy", "gco-inference-proxy-sa"), 

1492 # Feature-gated (cdk.json cost_monitoring); verified only when planned. 

1493 ("gco-system", "cost-monitor", "gco-cost-monitor-sa"), 

1494) 

1495 

1496# User-workload accounts the manifests declare outside gco-system. 

1497WORKLOAD_SERVICE_ACCOUNTS: tuple[tuple[str, str], ...] = ( 

1498 ("gco-jobs", "gco-service-account"), 

1499 ("gco-inference", "gco-service-account"), 

1500) 

1501 

1502 

1503def _verify_workload_credentials( 

1504 apps_v1: Any, 

1505 planned: list[dict[str, Any]] | None = None, 

1506) -> list[str]: 

1507 """Verify that key GCO deployments have working IAM credential configuration. 

1508 

1509 Checks that: 

1510 1. Deployments use their dedicated service account (with IRSA annotation) 

1511 2. The projected service-account token volume is mounted 

1512 3. AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE env vars are set 

1513 

1514 ``planned`` is the base-phase resource list from ``plan_manifests``. When 

1515 given, a platform Deployment (and its ServiceAccount) is checked only if 

1516 this apply planned it, so a feature-gated service that is switched off 

1517 (cost-monitor) does not surface as "deployment not found". 

1518 

1519 Returns a list of warning strings (empty = all good). 

1520 """ 

1521 warnings: list[str] = [] 

1522 planned_identities: set[tuple[str, str, str]] | None = None 

1523 if planned is not None: 

1524 planned_identities = { 

1525 (str(item["kind"]), str(item["namespace"]), str(item["name"])) for item in planned 

1526 } 

1527 

1528 def _is_planned(kind: str, namespace: str, name: str) -> bool: 

1529 return planned_identities is None or (kind, namespace, name) in planned_identities 

1530 

1531 expected_deployments = [ 

1532 (namespace, name, service_account) 

1533 for namespace, name, service_account in PLATFORM_DEPLOYMENTS 

1534 if _is_planned("Deployment", namespace, name) 

1535 ] 

1536 

1537 for namespace, name, expected_sa in expected_deployments: 

1538 try: 

1539 dep = apps_v1.read_namespaced_deployment(name, namespace) 

1540 spec = dep.spec.template.spec 

1541 

1542 # Check service account 

1543 if spec.service_account_name != expected_sa: 

1544 warnings.append( 

1545 f"{namespace}/{name}: uses SA '{spec.service_account_name}' instead of {expected_sa}" 

1546 ) 

1547 

1548 # Check for projected token volume 

1549 has_token_volume = False 

1550 if spec.volumes: 

1551 for vol in spec.volumes: 

1552 if vol.projected and vol.projected.sources: 

1553 for src in vol.projected.sources: 

1554 if ( 

1555 src.service_account_token 

1556 and src.service_account_token.audience == "sts.amazonaws.com" 

1557 ): 

1558 has_token_volume = True 

1559 break 

1560 

1561 if not has_token_volume: 

1562 warnings.append( 

1563 f"{namespace}/{name}: missing projected service-account token volume for IRSA" 

1564 ) 

1565 

1566 # Check env vars on first container 

1567 container = spec.containers[0] if spec.containers else None 

1568 if container and container.env: 

1569 env_names = {e.name for e in container.env} 

1570 if "AWS_ROLE_ARN" not in env_names: 

1571 warnings.append(f"{namespace}/{name}: missing AWS_ROLE_ARN env var") 

1572 if "AWS_WEB_IDENTITY_TOKEN_FILE" not in env_names: 

1573 warnings.append( 

1574 f"{namespace}/{name}: missing AWS_WEB_IDENTITY_TOKEN_FILE env var" 

1575 ) 

1576 

1577 except ApiException as e: 

1578 if e.status == 404: 

1579 warnings.append(f"{namespace}/{name}: deployment not found") 

1580 else: 

1581 warnings.append(f"{namespace}/{name}: failed to read ({e.status})") 

1582 except Exception as e: 

1583 warnings.append(f"{namespace}/{name}: verification error ({e})") 

1584 

1585 # Check that service accounts exist in all required namespaces: the 

1586 # dedicated account of every planned platform Deployment, then the user 

1587 # workload accounts. 

1588 v1 = client.CoreV1Api() 

1589 platform_sas = [(namespace, sa_name) for namespace, _name, sa_name in expected_deployments] 

1590 workload_sas = [ 

1591 (namespace, sa_name) 

1592 for namespace, sa_name in WORKLOAD_SERVICE_ACCOUNTS 

1593 if _is_planned("ServiceAccount", namespace, sa_name) 

1594 ] 

1595 for namespace, sa_name in platform_sas + workload_sas: 

1596 try: 

1597 sa = v1.read_namespaced_service_account(sa_name, namespace) 

1598 annotations = sa.metadata.annotations or {} 

1599 if "eks.amazonaws.com/role-arn" not in annotations: 

1600 warnings.append( 

1601 f"{namespace}/{sa_name}: missing eks.amazonaws.com/role-arn annotation" 

1602 ) 

1603 except ApiException as e: 

1604 if e.status == 404: 

1605 warnings.append(f"{namespace}/{sa_name}: ServiceAccount not found") 

1606 else: 

1607 warnings.append(f"{namespace}/{sa_name}: failed to read ({e.status})") 

1608 

1609 if warnings: 

1610 for w in warnings: 

1611 logger.warning(f"⚠ Credential check: {w}") 

1612 else: 

1613 logger.info("✓ All workload IAM credential configurations verified") 

1614 

1615 return warnings 

1616 

1617 

1618def apply_manifests( 

1619 cluster_name: str, 

1620 region: str, 

1621 manifests_dir: str, 

1622 replacements: dict[str, str], 

1623 post_helm: bool = False, 

1624) -> dict[str, Any]: 

1625 """Apply Kubernetes manifests. 

1626 

1627 Args: 

1628 cluster_name: EKS cluster name 

1629 region: AWS region 

1630 manifests_dir: Directory containing manifest YAML files 

1631 replacements: Template variable substitutions 

1632 post_helm: If True, apply only post-helm-* files (run after Helm installs CRDs). 

1633 If False (default), apply all other files and skip post-helm-* ones. 

1634 """ 

1635 plan = plan_manifests(manifests_dir, replacements) 

1636 phase_name = "post-helm" if post_helm else "base" 

1637 planned_resources = plan["phases"][phase_name] 

1638 expected_resources = [_public_manifest_identity(item) for item in planned_resources] 

1639 expected_count = len(planned_resources) 

1640 

1641 configure_k8s_client(cluster_name, region) 

1642 v1 = client.CoreV1Api() 

1643 apps_v1 = client.AppsV1Api() 

1644 autoscaling_v2 = client.AutoscalingV2Api() 

1645 rbac_v1 = client.RbacAuthorizationV1Api() 

1646 networking_v1 = client.NetworkingV1Api() 

1647 custom_api = client.CustomObjectsApi() 

1648 

1649 applied_count = 0 

1650 failed: list[str] = [] 

1651 skipped: list[str] = list(plan["skipped"][phase_name]) 

1652 pruned: list[str] = [] 

1653 prune_failures: list[str] = [] 

1654 

1655 # A gated-out file represents an optional feature that is disabled. Preserve 

1656 # the existing exact-resource pruning behavior, once per gate and phase. 

1657 for placeholder in plan["featureGates"][phase_name]: 

1658 if (placeholder, post_helm) not in _FEATURE_RESOURCE_INVENTORY: 

1659 continue 

1660 prune_result = _prune_disabled_feature(placeholder, post_helm) 

1661 pruned.extend(prune_result["pruned"]) 

1662 prune_failures.extend(prune_result["failed"]) 

1663 failed.extend(f"prune:{failure}" for failure in prune_result["failed"]) 

1664 

1665 # Objects GCO used to ship that no longer exist in any manifest — delete 

1666 # them exactly on upgraded clusters (fresh clusters: no-op). Base pass 

1667 # only, so the sweep runs once per convergence. 

1668 if not post_helm: 

1669 legacy_result = _prune_legacy_removed_resources() 

1670 pruned.extend(legacy_result["pruned"]) 

1671 prune_failures.extend(legacy_result["failed"]) 

1672 failed.extend(f"prune:{failure}" for failure in legacy_result["failed"]) 

1673 

1674 for planned_resource in planned_resources: 

1675 filename = planned_resource["sourceFile"] 

1676 try: 

1677 for doc in (planned_resource["document"],): 

1678 kind = planned_resource["kind"] 

1679 api_version = planned_resource["apiVersion"] 

1680 namespace = doc.get("metadata", {}).get("namespace", "default") 

1681 name = planned_resource["name"] 

1682 

1683 logger.info(f"Applying {kind}/{name} in namespace {namespace}") 

1684 try: 

1685 # Apply based on kind 

1686 if kind == "Namespace": 

1687 try: 

1688 v1.create_namespace(body=doc) 

1689 except ApiException as e: 

1690 if e.status == 409: # Already exists 

1691 v1.patch_namespace(name, body=doc) 

1692 else: 

1693 raise 

1694 

1695 elif kind == "ServiceAccount": 

1696 _log_service_account_automount_flip(v1, doc, namespace, name, plan) 

1697 try: 

1698 v1.create_namespaced_service_account(namespace, body=doc) 

1699 except ApiException as e: 

1700 if e.status == 409: 

1701 v1.patch_namespaced_service_account(name, namespace, body=doc) 

1702 else: 

1703 raise 

1704 

1705 elif kind == "ClusterRole": 

1706 try: 

1707 rbac_v1.create_cluster_role(body=doc) 

1708 except ApiException as e: 

1709 if e.status == 409: 

1710 rbac_v1.patch_cluster_role(name, body=doc) 

1711 else: 

1712 raise 

1713 

1714 elif kind == "ClusterRoleBinding": 

1715 try: 

1716 rbac_v1.create_cluster_role_binding(body=doc) 

1717 except ApiException as e: 

1718 if e.status == 409: 

1719 rbac_v1.patch_cluster_role_binding(name, body=doc) 

1720 else: 

1721 raise 

1722 

1723 elif kind == "Role": 

1724 try: 

1725 rbac_v1.create_namespaced_role(namespace, body=doc) 

1726 except ApiException as e: 

1727 if e.status == 409: 

1728 rbac_v1.patch_namespaced_role(name, namespace, body=doc) 

1729 else: 

1730 raise 

1731 

1732 elif kind == "RoleBinding": 

1733 try: 

1734 rbac_v1.create_namespaced_role_binding(namespace, body=doc) 

1735 except ApiException as e: 

1736 if e.status == 409: 

1737 rbac_v1.patch_namespaced_role_binding(name, namespace, body=doc) 

1738 else: 

1739 raise 

1740 

1741 elif kind == "Lease": 

1742 coordination_v1 = client.CoordinationV1Api() 

1743 try: 

1744 coordination_v1.create_namespaced_lease(namespace, body=doc) 

1745 except ApiException as e: 

1746 if e.status == 409: 

1747 coordination_v1.patch_namespaced_lease(name, namespace, body=doc) 

1748 else: 

1749 raise 

1750 

1751 elif kind == "Deployment": 

1752 try: 

1753 apps_v1.create_namespaced_deployment(namespace, body=doc) 

1754 except ApiException as e: 

1755 if e.status == 409: 

1756 _patch_or_replace( 

1757 apps_v1, 

1758 "namespaced_deployment", 

1759 name, 

1760 namespace, 

1761 doc, 

1762 patch_body=_deployment_patch_body(doc), 

1763 keep_live_replicas=_hpa_owns_replicas(doc), 

1764 ) 

1765 else: 

1766 raise 

1767 

1768 elif kind == "StatefulSet": 

1769 try: 

1770 apps_v1.create_namespaced_stateful_set(namespace, body=doc) 

1771 except ApiException as e: 

1772 if e.status == 409: 

1773 _patch_or_replace( 

1774 apps_v1, "namespaced_stateful_set", name, namespace, doc 

1775 ) 

1776 else: 

1777 raise 

1778 

1779 elif kind == "DaemonSet": 

1780 try: 

1781 apps_v1.create_namespaced_daemon_set(namespace, body=doc) 

1782 except ApiException as e: 

1783 if e.status == 409: 

1784 _patch_or_replace( 

1785 apps_v1, "namespaced_daemon_set", name, namespace, doc 

1786 ) 

1787 else: 

1788 raise 

1789 

1790 elif kind == "Job": 

1791 batch_v1 = client.BatchV1Api() 

1792 try: 

1793 batch_v1.create_namespaced_job(namespace, body=doc) 

1794 except ApiException as e: 

1795 if e.status == 409: 

1796 batch_v1.patch_namespaced_job(name, namespace, body=doc) 

1797 else: 

1798 raise 

1799 

1800 elif kind == "CronJob": 

1801 # batch/v1 CronJob (e.g. the Grafana admin-password 

1802 # rotation job in the observability post-Helm pass). 

1803 batch_v1 = client.BatchV1Api() 

1804 try: 

1805 batch_v1.create_namespaced_cron_job(namespace, body=doc) 

1806 except ApiException as e: 

1807 if e.status == 409: 

1808 batch_v1.patch_namespaced_cron_job(name, namespace, body=doc) 

1809 else: 

1810 raise 

1811 

1812 elif kind == "HorizontalPodAutoscaler": 

1813 try: 

1814 autoscaling_v2.create_namespaced_horizontal_pod_autoscaler( 

1815 namespace, body=doc 

1816 ) 

1817 except ApiException as e: 

1818 if e.status == 409: 

1819 autoscaling_v2.patch_namespaced_horizontal_pod_autoscaler( 

1820 name, namespace, body=doc 

1821 ) 

1822 else: 

1823 raise 

1824 

1825 elif kind == "PodDisruptionBudget": 

1826 policy_v1 = client.PolicyV1Api() 

1827 try: 

1828 policy_v1.create_namespaced_pod_disruption_budget(namespace, body=doc) 

1829 except ApiException as e: 

1830 if e.status == 409: 

1831 _patch_or_replace( 

1832 policy_v1, 

1833 "namespaced_pod_disruption_budget", 

1834 name, 

1835 namespace, 

1836 doc, 

1837 ) 

1838 else: 

1839 raise 

1840 

1841 elif kind == "Service": 

1842 try: 

1843 v1.create_namespaced_service(namespace, body=doc) 

1844 except ApiException as e: 

1845 if e.status == 409: 

1846 v1.patch_namespaced_service(name, namespace, body=doc) 

1847 else: 

1848 raise 

1849 

1850 elif kind == "Pod": 

1851 try: 

1852 v1.create_namespaced_pod(namespace, body=doc) 

1853 except ApiException as e: 

1854 if e.status == 409: 

1855 v1.patch_namespaced_pod(name, namespace, body=doc) 

1856 else: 

1857 raise 

1858 

1859 elif kind == "ConfigMap": 

1860 try: 

1861 v1.create_namespaced_config_map(namespace, body=doc) 

1862 except ApiException as e: 

1863 if e.status == 409: 

1864 v1.patch_namespaced_config_map(name, namespace, body=doc) 

1865 else: 

1866 raise 

1867 

1868 elif kind == "Secret": 

1869 generated_keys = _generated_secret_keys(doc) 

1870 if generated_keys: 

1871 _apply_generated_secret(v1, doc, namespace, name, generated_keys) 

1872 else: 

1873 try: 

1874 v1.create_namespaced_secret(namespace, body=doc) 

1875 except ApiException as e: 

1876 if e.status == 409: 

1877 v1.patch_namespaced_secret(name, namespace, body=doc) 

1878 else: 

1879 raise 

1880 

1881 elif ( 

1882 kind in _GATEWAY_CUSTOM_OBJECTS 

1883 or kind in _QUEUEING_CUSTOM_OBJECTS 

1884 or kind in _CERT_MANAGER_CUSTOM_OBJECTS 

1885 or kind in _ARGOCD_CUSTOM_OBJECTS 

1886 or kind in _CROSSPLANE_CUSTOM_OBJECTS 

1887 or kind in _TRUST_MANAGER_CUSTOM_OBJECTS 

1888 ): 

1889 group, version, plural, cluster_scoped = ( 

1890 _GATEWAY_CUSTOM_OBJECTS.get(kind) 

1891 or _QUEUEING_CUSTOM_OBJECTS.get(kind) 

1892 or _CERT_MANAGER_CUSTOM_OBJECTS.get(kind) 

1893 or _ARGOCD_CUSTOM_OBJECTS.get(kind) 

1894 or _CROSSPLANE_CUSTOM_OBJECTS.get(kind) 

1895 or _TRUST_MANAGER_CUSTOM_OBJECTS[kind] 

1896 ) 

1897 try: 

1898 if cluster_scoped: 

1899 custom_api.create_cluster_custom_object( 

1900 group, version, plural, body=doc 

1901 ) 

1902 else: 

1903 custom_api.create_namespaced_custom_object( 

1904 group, version, namespace, plural, body=doc 

1905 ) 

1906 except ApiException as e: 

1907 if e.status != 409: 

1908 raise 

1909 if cluster_scoped: 

1910 custom_api.patch_cluster_custom_object( 

1911 group, version, plural, name, body=doc 

1912 ) 

1913 else: 

1914 custom_api.patch_namespaced_custom_object( 

1915 group, version, namespace, plural, name, body=doc 

1916 ) 

1917 

1918 elif kind == "StorageClass": 

1919 storage_v1 = client.StorageV1Api() 

1920 try: 

1921 storage_v1.create_storage_class(body=doc) 

1922 except ApiException as e: 

1923 if e.status == 409: 

1924 # StorageClass already exists - skip patching as most fields are immutable 

1925 logger.info(f"StorageClass {name} already exists, skipping update") 

1926 else: 

1927 raise 

1928 elif kind == "PriorityClass": 

1929 scheduling_v1 = client.SchedulingV1Api() 

1930 try: 

1931 scheduling_v1.create_priority_class(body=doc) 

1932 except ApiException as e: 

1933 if e.status == 409: 

1934 # Converge the mutable fields (description, 

1935 # labels, globalDefault). ``value`` and 

1936 # ``preemptionPolicy`` are immutable: patching 

1937 # them with an unchanged value is a no-op, 

1938 # while a genuine change fails loudly (422) 

1939 # instead of silently keeping the old 

1940 # priority — delete the class and redeploy to 

1941 # change a value. 

1942 scheduling_v1.patch_priority_class(name, body=doc) 

1943 else: 

1944 raise 

1945 

1946 elif kind == "PersistentVolume": 

1947 try: 

1948 v1.create_persistent_volume(body=doc) 

1949 except ApiException as e: 

1950 if e.status == 409: 

1951 # PVs have immutable spec fields. Check if the existing PV 

1952 # matches — if so, skip. If the volumeHandle changed (new 

1953 # FSx file system), force-remove the old PV and recreate. 

1954 existing = v1.read_persistent_volume(name) 

1955 existing_handle = ( 

1956 existing.spec.csi.volume_handle if existing.spec.csi else None 

1957 ) 

1958 new_handle = doc.get("spec", {}).get("csi", {}).get("volumeHandle") 

1959 

1960 if existing_handle == new_handle: 

1961 logger.info(f"PersistentVolume {name} unchanged, skipping") 

1962 else: 

1963 logger.info( 

1964 f"PersistentVolume {name} volumeHandle changed " 

1965 f"({existing_handle} → {new_handle}), recreating" 

1966 ) 

1967 # Remove the protection finalizer so the PV can be deleted 

1968 # even while bound to a PVC 

1969 v1.patch_persistent_volume( 

1970 name, 

1971 body={"metadata": {"finalizers": None}}, 

1972 ) 

1973 v1.delete_persistent_volume(name) 

1974 # Wait for the PV to actually disappear 

1975 import time as _time 

1976 

1977 _pv_iterations = max( 

1978 1, 

1979 PV_PVC_DELETE_WAIT_SECONDS 

1980 // PV_PVC_DELETE_POLL_INTERVAL_SECONDS, 

1981 ) 

1982 for _wait in range(_pv_iterations): 

1983 try: 

1984 v1.read_persistent_volume(name) 

1985 _time.sleep(PV_PVC_DELETE_POLL_INTERVAL_SECONDS) 

1986 except ApiException as read_e: 

1987 if read_e.status == 404: 

1988 break 

1989 raise 

1990 v1.create_persistent_volume(body=doc) 

1991 else: 

1992 raise 

1993 

1994 elif kind == "PersistentVolumeClaim": 

1995 try: 

1996 v1.create_namespaced_persistent_volume_claim(namespace, body=doc) 

1997 except ApiException as e: 

1998 if e.status == 409: 

1999 # Check if the PVC is in Lost state (bound PV was recreated). 

2000 # A Lost PVC can't be patched back to health — it must be 

2001 # deleted and recreated so it binds to the new PV. 

2002 existing_pvc = v1.read_namespaced_persistent_volume_claim( 

2003 name, namespace 

2004 ) 

2005 if existing_pvc.status.phase == "Lost": 

2006 logger.info( 

2007 f"PVC {namespace}/{name} is Lost (bound PV was " 

2008 f"recreated), deleting and recreating" 

2009 ) 

2010 v1.delete_namespaced_persistent_volume_claim(name, namespace) 

2011 import time as _time 

2012 

2013 _pvc_iterations = max( 

2014 1, 

2015 PV_PVC_DELETE_WAIT_SECONDS 

2016 // PV_PVC_DELETE_POLL_INTERVAL_SECONDS, 

2017 ) 

2018 for _wait in range(_pvc_iterations): 

2019 try: 

2020 v1.read_namespaced_persistent_volume_claim( 

2021 name, namespace 

2022 ) 

2023 _time.sleep(PV_PVC_DELETE_POLL_INTERVAL_SECONDS) 

2024 except ApiException as read_e: 

2025 if read_e.status == 404: 

2026 break 

2027 raise 

2028 v1.create_namespaced_persistent_volume_claim( 

2029 namespace, body=doc 

2030 ) 

2031 else: 

2032 v1.patch_namespaced_persistent_volume_claim( 

2033 name, namespace, body=doc 

2034 ) 

2035 else: 

2036 raise 

2037 

2038 elif kind == "NodePool": 

2039 # Karpenter NodePool CRD 

2040 group = "karpenter.sh" 

2041 version = api_version.split("/")[-1] if "/" in api_version else "v1" 

2042 plural = "nodepools" 

2043 try: 

2044 custom_api.create_cluster_custom_object( 

2045 group, version, plural, body=doc 

2046 ) 

2047 except ApiException as e: 

2048 if e.status == 409: 

2049 custom_api.patch_cluster_custom_object( 

2050 group, version, plural, name, body=doc 

2051 ) 

2052 else: 

2053 raise 

2054 

2055 elif kind == "EC2NodeClass": 

2056 # Karpenter EC2NodeClass CRD 

2057 group = "karpenter.k8s.aws" 

2058 version = api_version.split("/")[-1] if "/" in api_version else "v1" 

2059 plural = "ec2nodeclasses" 

2060 try: 

2061 custom_api.create_cluster_custom_object( 

2062 group, version, plural, body=doc 

2063 ) 

2064 except ApiException as e: 

2065 if e.status == 409: 

2066 custom_api.patch_cluster_custom_object( 

2067 group, version, plural, name, body=doc 

2068 ) 

2069 else: 

2070 raise 

2071 

2072 elif kind == "APIService": 

2073 # Kubernetes API aggregation layer 

2074 api_reg_v1 = client.ApiregistrationV1Api() 

2075 try: 

2076 api_reg_v1.create_api_service(body=doc) 

2077 except ApiException as e: 

2078 if e.status == 409: 

2079 api_reg_v1.patch_api_service(name, body=doc) 

2080 else: 

2081 raise 

2082 

2083 elif kind == "ValidatingAdmissionPolicy": 

2084 # Built-in and cluster-scoped: the fence on who may 

2085 # obtain a certificate from GCO's internal CA 

2086 # (08-internal-ca-issuance.yaml) and the write fence on 

2087 # GCO's objects in the tenant namespaces 

2088 # (09-tenant-write-fence.yaml). Both are base-pass 

2089 # objects, so they are enforced before the post-Helm 

2090 # pass creates the CA and its leaves. 

2091 admission_v1 = client.AdmissionregistrationV1Api() 

2092 try: 

2093 admission_v1.create_validating_admission_policy(body=doc) 

2094 except ApiException as e: 

2095 if e.status == 409: 

2096 admission_v1.patch_validating_admission_policy(name, body=doc) 

2097 else: 

2098 raise 

2099 

2100 elif kind == "ValidatingAdmissionPolicyBinding": 

2101 admission_v1 = client.AdmissionregistrationV1Api() 

2102 try: 

2103 admission_v1.create_validating_admission_policy_binding(body=doc) 

2104 except ApiException as e: 

2105 if e.status == 409: 

2106 admission_v1.patch_validating_admission_policy_binding( 

2107 name, body=doc 

2108 ) 

2109 else: 

2110 raise 

2111 

2112 elif kind == "CustomResourceDefinition": 

2113 api_extensions_v1 = client.ApiextensionsV1Api() 

2114 try: 

2115 api_extensions_v1.create_custom_resource_definition(body=doc) 

2116 except ApiException as e: 

2117 if e.status == 409: 

2118 api_extensions_v1.patch_custom_resource_definition(name, body=doc) 

2119 else: 

2120 raise 

2121 

2122 elif kind == "DeviceClass": 

2123 # Kubernetes DRA DeviceClass (resource.k8s.io) 

2124 group = "resource.k8s.io" 

2125 version = api_version.split("/")[-1] if "/" in api_version else "v1" 

2126 plural = "deviceclasses" 

2127 try: 

2128 custom_api.create_cluster_custom_object( 

2129 group, version, plural, body=doc 

2130 ) 

2131 except ApiException as e: 

2132 if e.status == 409: 

2133 custom_api.patch_cluster_custom_object( 

2134 group, version, plural, name, body=doc 

2135 ) 

2136 else: 

2137 raise 

2138 elif kind == "Token": 

2139 # slurm-operator Token: the operator mints a Slurm 

2140 # auth/jwt token into a Secret and refreshes it 

2141 # (post-helm-slurm-api-access.yaml). The CRD is 

2142 # registered by the slinky-slurm-operator chart, so 

2143 # Tokens land in the post-Helm pass. 

2144 group = "slinky.slurm.net" 

2145 version = api_version.split("/")[-1] if "/" in api_version else "v1beta1" 

2146 plural = "tokens" 

2147 try: 

2148 custom_api.create_namespaced_custom_object( 

2149 group, version, namespace, plural, body=doc 

2150 ) 

2151 except ApiException as e: 

2152 if e.status == 409: 

2153 custom_api.patch_namespaced_custom_object( 

2154 group, version, namespace, plural, name, body=doc 

2155 ) 

2156 else: 

2157 raise 

2158 elif kind == "ClusterTrainingRuntime": 

2159 # Kubeflow Trainer v2 runtime blueprint (cluster-scoped; 

2160 # the CRD is registered by the kubeflow-trainer chart, so 

2161 # the shipped torch-distributed runtime lands in the 

2162 # post-Helm pass). 

2163 group = "trainer.kubeflow.org" 

2164 version = api_version.split("/")[-1] if "/" in api_version else "v1alpha1" 

2165 plural = "clustertrainingruntimes" 

2166 try: 

2167 custom_api.create_cluster_custom_object( 

2168 group, version, plural, body=doc 

2169 ) 

2170 except ApiException as e: 

2171 if e.status == 409: 

2172 custom_api.patch_cluster_custom_object( 

2173 group, version, plural, name, body=doc 

2174 ) 

2175 else: 

2176 raise 

2177 

2178 elif kind == "NetworkPolicy": 

2179 try: 

2180 networking_v1.create_namespaced_network_policy(namespace, body=doc) 

2181 except ApiException as e: 

2182 if e.status == 409: 

2183 networking_v1.patch_namespaced_network_policy( 

2184 name, namespace, body=doc 

2185 ) 

2186 else: 

2187 raise 

2188 

2189 elif kind == "ResourceQuota": 

2190 try: 

2191 v1.create_namespaced_resource_quota(namespace, body=doc) 

2192 except ApiException as e: 

2193 if e.status == 409: 

2194 v1.patch_namespaced_resource_quota(name, namespace, body=doc) 

2195 else: 

2196 raise 

2197 

2198 elif kind == "LimitRange": 

2199 try: 

2200 v1.create_namespaced_limit_range(namespace, body=doc) 

2201 except ApiException as e: 

2202 if e.status == 409: 

2203 v1.patch_namespaced_limit_range(name, namespace, body=doc) 

2204 else: 

2205 raise 

2206 

2207 elif kind == "ScaledJob": 

2208 # KEDA ScaledJob CRD 

2209 group = "keda.sh" 

2210 version = api_version.split("/")[-1] if "/" in api_version else "v1alpha1" 

2211 plural = "scaledjobs" 

2212 try: 

2213 custom_api.create_namespaced_custom_object( 

2214 group, version, namespace, plural, body=doc 

2215 ) 

2216 except ApiException as e: 

2217 if e.status == 409: 

2218 custom_api.patch_namespaced_custom_object( 

2219 group, version, namespace, plural, name, body=doc 

2220 ) 

2221 else: 

2222 raise 

2223 

2224 elif kind == "ScaledObject": 

2225 # KEDA ScaledObject CRD 

2226 group = "keda.sh" 

2227 version = api_version.split("/")[-1] if "/" in api_version else "v1alpha1" 

2228 plural = "scaledobjects" 

2229 try: 

2230 custom_api.create_namespaced_custom_object( 

2231 group, version, namespace, plural, body=doc 

2232 ) 

2233 except ApiException as e: 

2234 if e.status == 409: 

2235 custom_api.patch_namespaced_custom_object( 

2236 group, version, namespace, plural, name, body=doc 

2237 ) 

2238 else: 

2239 raise 

2240 

2241 elif kind in _MONITORING_CUSTOM_OBJECT_PLURALS: 

2242 # Prometheus Operator CRDs registered by the 

2243 # kube-prometheus-stack chart, so these land in the 

2244 # post-Helm pass. GCO uses ServiceMonitors for 

2245 # components fronted by a Service (schedulers, DCGM, 

2246 # slurmctld), PodMonitors for its own multi-replica 

2247 # services, and PrometheusRules for alerting rules. 

2248 group = "monitoring.coreos.com" 

2249 version = api_version.split("/")[-1] if "/" in api_version else "v1" 

2250 plural = _MONITORING_CUSTOM_OBJECT_PLURALS[kind] 

2251 try: 

2252 custom_api.create_namespaced_custom_object( 

2253 group, version, namespace, plural, body=doc 

2254 ) 

2255 except ApiException as e: 

2256 if e.status == 409: 

2257 custom_api.patch_namespaced_custom_object( 

2258 group, version, namespace, plural, name, body=doc 

2259 ) 

2260 else: 

2261 raise 

2262 

2263 else: 

2264 # Defensive only: plan_manifests rejects unsupported kinds. 

2265 raise ValueError(f"Planner admitted unsupported kind: {kind}") 

2266 

2267 applied_count += 1 

2268 logger.info(f"✓ Applied {kind}/{name}") 

2269 

2270 except ApiException as e: 

2271 logger.error( 

2272 f"API error applying {kind}/{name}: {e.status} - {e.reason}" 

2273 f"{_api_error_detail(e)}" 

2274 ) 

2275 failed.append(f"{filename}:{kind}/{name}") 

2276 

2277 except Exception as e: 

2278 logger.error(f"Failed to apply {filename}: {e}") 

2279 failed.append(filename) 

2280 

2281 # Invariant, not a reachable path: every iteration above either increments 

2282 # applied_count or appends to failed, so a clean pass always applies exactly 

2283 # expected_count resources. Kept as a tripwire against future edits to the 

2284 # loop that break that accounting. 

2285 if not failed and applied_count != expected_count: # pragma: no cover - loop invariant 

2286 raise RuntimeError( 

2287 f"Manifest apply count mismatch: expected={expected_count} applied={applied_count}" 

2288 ) 

2289 

2290 # Restart deployments and verify credentials only on the main (full) pass, 

2291 # not on the post-Helm pass 

2292 if post_helm: 

2293 return { 

2294 "AppliedCount": applied_count, 

2295 "ExpectedCount": expected_count, 

2296 "ExpectedResources": expected_resources, 

2297 "FailedCount": len(failed), 

2298 "SkippedCount": len(skipped), 

2299 "Failed": ",".join(failed) if failed else "None", 

2300 "Skipped": ",".join(skipped) if skipped else "None", 

2301 "PrunedCount": len(pruned), 

2302 "Pruned": ",".join(pruned) if pruned else "None", 

2303 "PruneFailures": ",".join(prune_failures) if prune_failures else "None", 

2304 } 

2305 

2306 # GCO Deployments already roll exactly once through their 

2307 # gco.aws/deployment-timestamp pod-template annotation (and any image patch). 

2308 # Do not immediately add kubectl.kubernetes.io/restartedAt: a second 

2309 # back-to-back revision can pin maxUnavailable=0/maxSurge=1 Deployments at 

2310 # their surge ceiling while the first revision is still converging. 

2311 

2312 # Restart the EFS CSI controller so it picks up the IRSA role-ARN 

2313 # annotation that the EKS addon update patched onto its service account. 

2314 # 

2315 # Background: the managed aws-efs-csi-driver addon creates the 

2316 # efs-csi-controller-sa ServiceAccount and the controller Deployment in 

2317 # parallel. Our stack later calls UpdateAddon with a 

2318 # serviceAccountRoleArn, which patches the SA's eks.amazonaws.com/role-arn 

2319 # annotation — but EKS does NOT restart the controller pods. The existing 

2320 # pods keep their original (un-mutated) pod spec: no AWS_ROLE_ARN, 

2321 # no AWS_WEB_IDENTITY_TOKEN_FILE. They then fall back to IMDS for 

2322 # credentials, which EKS Auto Mode blocks at the pod network level 

2323 # (hop-limit / security policy), causing every EFS CreateAccessPoint 

2324 # call to fail with HTTP 401. The visible symptom is a PVC that stays 

2325 # Pending forever with "no EC2 IMDS role found". 

2326 # 

2327 # Restarting the deployment forces a new pod template to go through the 

2328 # EKS Pod Identity / IRSA mutating webhook, which sees the annotation 

2329 # this time and injects the projected-token volume and env vars. 

2330 # 

2331 # The same pattern applies to: 

2332 # - aws-fsx-csi-driver: fsx-csi-controller Deployment + 

2333 # fsx-csi-node DaemonSet 

2334 # - amazon-cloudwatch-observability: cloudwatch-agent DaemonSet 

2335 # - aws-efs-csi-driver: efs-csi-node DaemonSet (the controller already 

2336 # above) 

2337 # 

2338 # Deployments/DaemonSets that don't exist on this cluster (e.g. FSx when 

2339 # fsx_lustre.enabled=false) return 404 and are skipped gracefully — see 

2340 # the 404 branch in restart_deployments/restart_daemonsets. 

2341 kube_system_deployments = ["efs-csi-controller", "fsx-csi-controller"] 

2342 logger.info(f"Restarting deployments in kube-system: {kube_system_deployments}") 

2343 ks_deploy_restart = restart_deployments("kube-system", kube_system_deployments) 

2344 

2345 kube_system_daemonsets = ["efs-csi-node", "fsx-csi-node"] 

2346 logger.info(f"Restarting daemonsets in kube-system: {kube_system_daemonsets}") 

2347 ks_ds_restart = restart_daemonsets("kube-system", kube_system_daemonsets) 

2348 

2349 # CloudWatch agent daemonset runs in its own namespace when the 

2350 # amazon-cloudwatch-observability addon is installed. 

2351 cw_daemonsets = ["cloudwatch-agent"] 

2352 logger.info(f"Restarting daemonsets in amazon-cloudwatch: {cw_daemonsets}") 

2353 cw_ds_restart = restart_daemonsets("amazon-cloudwatch", cw_daemonsets) 

2354 

2355 # Verify IAM credentials are available for workloads 

2356 # Check that the projected service-account token volume is configured 

2357 # on key deployments — if missing, IRSA won't work. Scoped to the 

2358 # Deployments this pass planned so a gated-off service is not reported. 

2359 credential_warnings = _verify_workload_credentials(apps_v1, planned_resources) 

2360 

2361 # Combine the restart results for the return payload. 

2362 all_restarted = ( 

2363 ks_deploy_restart["restarted"] + ks_ds_restart["restarted"] + cw_ds_restart["restarted"] 

2364 ) 

2365 

2366 return { 

2367 "AppliedCount": applied_count, 

2368 "ExpectedCount": expected_count, 

2369 "ExpectedResources": expected_resources, 

2370 "FailedCount": len(failed), 

2371 "SkippedCount": len(skipped), 

2372 "Failed": ",".join(failed) if failed else "None", 

2373 "Skipped": ",".join(skipped) if skipped else "None", 

2374 "RestartedDeployments": (",".join(all_restarted) if all_restarted else "None"), 

2375 "CredentialWarnings": ",".join(credential_warnings) if credential_warnings else "None", 

2376 "PrunedCount": len(pruned), 

2377 "Pruned": ",".join(pruned) if pruned else "None", 

2378 "PruneFailures": ",".join(prune_failures) if prune_failures else "None", 

2379 } 

2380 

2381 

2382def _delete_gateway_resources(cluster_name: str, region: str) -> dict[str, Any]: 

2383 """Delete Gateway objects while the LBC controller is still running. 

2384 

2385 The Gateway is deleted only after its routes, and every object is polled to 

2386 exact absence. Waiting for the Gateway's controller finalizer is what proves 

2387 the ALB has been removed before the LBC Helm release is uninstalled. 

2388 """ 

2389 configure_k8s_client(cluster_name, region) 

2390 custom_api = client.CustomObjectsApi() 

2391 namespace = "gco-system" 

2392 delete_options = client.V1DeleteOptions(propagation_policy="Foreground") 

2393 resources = ( 

2394 ("HTTPRoute", "gco-routes"), 

2395 ("Gateway", "gco-gateway"), 

2396 ("LoadBalancerConfiguration", "gco-gateway-load-balancer"), 

2397 ("TargetGroupConfiguration", "gco-health-monitor-target-group"), 

2398 ("TargetGroupConfiguration", "gco-manifest-processor-target-group"), 

2399 ("TargetGroupConfiguration", "gco-inference-proxy-target-group"), 

2400 ("TargetGroupConfiguration", "gco-default-target-group"), 

2401 ("GatewayClass", "gco-aws-alb"), 

2402 ) 

2403 deleted: list[str] = [] 

2404 deadline = time.monotonic() + _GATEWAY_DELETE_WAIT_SECONDS 

2405 

2406 for kind, name in resources: 

2407 group, version, plural, cluster_scoped = _GATEWAY_CUSTOM_OBJECTS[kind] 

2408 label = f"{kind}/{name}" if cluster_scoped else f"{kind}/{namespace}/{name}" 

2409 try: 

2410 if cluster_scoped: 

2411 custom_api.delete_cluster_custom_object( 

2412 group, version, plural, name, body=delete_options 

2413 ) 

2414 else: 

2415 custom_api.delete_namespaced_custom_object( 

2416 group, version, namespace, plural, name, body=delete_options 

2417 ) 

2418 except ApiException as exc: 

2419 if exc.status != 404: 

2420 raise RuntimeError( 

2421 f"failed to delete {label}: Kubernetes API {exc.status} ({exc.reason})" 

2422 ) from exc 

2423 deleted.append(f"{label}:already-absent") 

2424 continue 

2425 

2426 while True: 

2427 try: 

2428 if cluster_scoped: 

2429 custom_api.get_cluster_custom_object(group, version, plural, name) 

2430 else: 

2431 custom_api.get_namespaced_custom_object(group, version, namespace, plural, name) 

2432 except ApiException as exc: 

2433 if exc.status == 404: 

2434 deleted.append(label) 

2435 break 

2436 raise RuntimeError( 

2437 f"failed while waiting for {label} deletion: " 

2438 f"Kubernetes API {exc.status} ({exc.reason})" 

2439 ) from exc 

2440 if time.monotonic() >= deadline: 

2441 raise RuntimeError( 

2442 "timed out waiting for the complete Gateway resource set to delete " 

2443 f"within {_GATEWAY_DELETE_WAIT_SECONDS}s; last resource was {label}" 

2444 ) 

2445 time.sleep(_GATEWAY_DELETE_POLL_SECONDS) 

2446 

2447 return {"status": "deleted", "DeletedCount": len(deleted), "Deleted": deleted} 

2448 

2449 

2450def _as_plain_dict(value: Any) -> dict[str, Any]: 

2451 """Convert a DynamicClient response to a plain mapping.""" 

2452 if isinstance(value, dict): 

2453 return value 

2454 converter = getattr(value, "to_dict", None) 

2455 if callable(converter): 

2456 converted = converter() 

2457 if isinstance(converted, dict): 

2458 return converted 

2459 try: 

2460 converted = dict(value) 

2461 except (TypeError, ValueError) as exc: 

2462 raise TypeError(f"Kubernetes response is not a mapping: {type(value).__name__}") from exc 

2463 return converted 

2464 

2465 

2466def _condition_matches(condition: dict[str, Any], expected_status: str) -> bool: 

2467 value = condition.get("status") 

2468 if isinstance(value, bool): 

2469 value = "True" if value else "False" 

2470 return str(value).lower() == expected_status.lower() 

2471 

2472 

2473def _conditions(status: dict[str, Any]) -> list[dict[str, Any]]: 

2474 value = status.get("conditions", []) 

2475 if not isinstance(value, list): 

2476 return [] 

2477 return [condition for condition in value if isinstance(condition, dict)] 

2478 

2479 

2480def _required_condition_failure( 

2481 status: dict[str, Any], 

2482 condition_type: str, 

2483) -> str | None: 

2484 matching = [item for item in _conditions(status) if item.get("type") == condition_type] 

2485 if any(_condition_matches(item, "True") for item in matching): 

2486 return None 

2487 if matching: 

2488 details = matching[-1].get("message") or matching[-1].get("reason") or "status is not True" 

2489 return f"condition {condition_type} is not True ({details})" 

2490 return f"condition {condition_type} is missing" 

2491 

2492 

2493def _generation_failure(resource: dict[str, Any]) -> str | None: 

2494 metadata_value = resource.get("metadata") 

2495 status_value = resource.get("status") 

2496 metadata: dict[str, Any] = metadata_value if isinstance(metadata_value, dict) else {} 

2497 status: dict[str, Any] = status_value if isinstance(status_value, dict) else {} 

2498 generation = metadata.get("generation") 

2499 observed = status.get("observedGeneration") 

2500 if ( 

2501 not isinstance(generation, int) 

2502 or isinstance(generation, bool) 

2503 or not isinstance(observed, int) 

2504 or isinstance(observed, bool) 

2505 or observed != generation 

2506 ): 

2507 return f"generation not observed (generation={generation}, observedGeneration={observed})" 

2508 return None 

2509 

2510 

2511def _replica_failure( 

2512 status: dict[str, Any], 

2513 desired: int, 

2514 fields: tuple[str, ...], 

2515) -> str | None: 

2516 for field in fields: 

2517 # Kubernetes omits optional zero-valued counters. Treat omission as 

2518 # zero while still rejecting malformed non-integer values. 

2519 value = status.get(field, 0) 

2520 if not isinstance(value, int) or isinstance(value, bool) or value != desired: 

2521 return f"replicas not converged ({field}={value}, desired={desired})" 

2522 unavailable = status.get("unavailableReplicas", status.get("numberUnavailable", 0)) 

2523 if not isinstance(unavailable, int) or isinstance(unavailable, bool) or unavailable != 0: 

2524 return f"replicas unavailable ({unavailable})" 

2525 return None 

2526 

2527 

2528def _current_condition_failure( 

2529 conditions: list[dict[str, Any]], 

2530 condition_type: str, 

2531 generation: Any, 

2532 context: str, 

2533) -> str | None: 

2534 """Require a True condition that observed the object's current generation.""" 

2535 current = [ 

2536 condition 

2537 for condition in conditions 

2538 if condition.get("type") == condition_type 

2539 and condition.get("observedGeneration") == generation 

2540 ] 

2541 if any(_condition_matches(condition, "True") for condition in current): 

2542 return None 

2543 matching = [condition for condition in conditions if condition.get("type") == condition_type] 

2544 if current: 

2545 detail = current[-1].get("message") or current[-1].get("reason") or "status is not True" 

2546 return f"{context} condition {condition_type} is not True ({detail})" 

2547 if matching: 

2548 observed = matching[-1].get("observedGeneration") 

2549 return ( 

2550 f"{context} condition {condition_type} is stale " 

2551 f"(generation={generation}, observedGeneration={observed})" 

2552 ) 

2553 return f"{context} condition {condition_type} is missing" 

2554 

2555 

2556def _gateway_api_readiness_failure(kind: str, resource: dict[str, Any]) -> str | None: 

2557 """Return strict, generation-aware Gateway API readiness evidence.""" 

2558 metadata_value = resource.get("metadata") 

2559 spec_value = resource.get("spec") 

2560 status_value = resource.get("status") 

2561 metadata: dict[str, Any] = metadata_value if isinstance(metadata_value, dict) else {} 

2562 spec: dict[str, Any] = spec_value if isinstance(spec_value, dict) else {} 

2563 status: dict[str, Any] = status_value if isinstance(status_value, dict) else {} 

2564 generation = metadata.get("generation") 

2565 if not isinstance(generation, int) or isinstance(generation, bool): 

2566 return f"invalid metadata.generation ({generation})" 

2567 

2568 if kind == "GatewayClass": 

2569 return _current_condition_failure( 

2570 _conditions(status), "Accepted", generation, "GatewayClass" 

2571 ) 

2572 

2573 if kind == "Gateway": 

2574 for condition_type in ("Accepted", "Programmed"): 

2575 failure = _current_condition_failure( 

2576 _conditions(status), condition_type, generation, "Gateway" 

2577 ) 

2578 if failure: 

2579 return failure 

2580 addresses = status.get("addresses", []) 

2581 if not isinstance(addresses, list) or not any( 

2582 isinstance(address, dict) and str(address.get("value", "")).strip() 

2583 for address in addresses 

2584 ): 

2585 return "Gateway has no address" 

2586 

2587 intended_listeners: set[str] = { 

2588 listener["name"] 

2589 for listener in spec.get("listeners", []) 

2590 if isinstance(listener, dict) and isinstance(listener.get("name"), str) 

2591 } 

2592 listener_statuses = status.get("listeners", []) 

2593 if not isinstance(listener_statuses, list): 

2594 return "Gateway listener status is missing" 

2595 by_name = { 

2596 listener.get("name"): listener 

2597 for listener in listener_statuses 

2598 if isinstance(listener, dict) and isinstance(listener.get("name"), str) 

2599 } 

2600 for listener_name in sorted(intended_listeners): 

2601 listener = by_name.get(listener_name) 

2602 if not isinstance(listener, dict): 

2603 return f"Gateway listener {listener_name!r} status is missing" 

2604 listener_conditions = listener.get("conditions", []) 

2605 if not isinstance(listener_conditions, list): 

2606 return f"Gateway listener {listener_name!r} conditions are missing" 

2607 plain_conditions = [item for item in listener_conditions if isinstance(item, dict)] 

2608 for condition_type in ("Accepted", "ResolvedRefs", "Programmed"): 

2609 failure = _current_condition_failure( 

2610 plain_conditions, 

2611 condition_type, 

2612 generation, 

2613 f"Gateway listener {listener_name!r}", 

2614 ) 

2615 if failure: 

2616 return failure 

2617 return None 

2618 

2619 if kind == "HTTPRoute": 

2620 namespace = str(metadata.get("namespace", "default")) 

2621 

2622 def parent_key(reference: dict[str, Any]) -> tuple[str, str, str, str, str]: 

2623 return ( 

2624 str(reference.get("group", "gateway.networking.k8s.io")), 

2625 str(reference.get("kind", "Gateway")), 

2626 str(reference.get("namespace", namespace)), 

2627 str(reference.get("name", "")), 

2628 str(reference.get("sectionName", "")), 

2629 ) 

2630 

2631 intended: set[tuple[str, str, str, str, str]] = { 

2632 parent_key(parent) for parent in spec.get("parentRefs", []) if isinstance(parent, dict) 

2633 } 

2634 parents = status.get("parents", []) 

2635 if not isinstance(parents, list): 

2636 return "HTTPRoute parent status is missing" 

2637 live_by_ref = { 

2638 parent_key(parent["parentRef"]): parent 

2639 for parent in parents 

2640 if isinstance(parent, dict) and isinstance(parent.get("parentRef"), dict) 

2641 } 

2642 for reference in sorted(intended): 

2643 parent = live_by_ref.get(reference) 

2644 if not isinstance(parent, dict): 

2645 return f"HTTPRoute intended parent {reference} status is missing" 

2646 parent_conditions = parent.get("conditions", []) 

2647 if not isinstance(parent_conditions, list): 

2648 return f"HTTPRoute intended parent {reference} conditions are missing" 

2649 plain_conditions = [item for item in parent_conditions if isinstance(item, dict)] 

2650 for condition_type in ("Accepted", "ResolvedRefs"): 

2651 failure = _current_condition_failure( 

2652 plain_conditions, 

2653 condition_type, 

2654 generation, 

2655 f"HTTPRoute parent {reference}", 

2656 ) 

2657 if failure: 

2658 return failure 

2659 return None 

2660 

2661 return None 

2662 

2663 

2664def _resource_readiness_failure(kind: str, resource: dict[str, Any]) -> str | None: 

2665 """Return an actionable readiness reason, or None when the object is ready.""" 

2666 metadata_value = resource.get("metadata") 

2667 spec_value = resource.get("spec") 

2668 status_value = resource.get("status") 

2669 metadata: dict[str, Any] = metadata_value if isinstance(metadata_value, dict) else {} 

2670 spec: dict[str, Any] = spec_value if isinstance(spec_value, dict) else {} 

2671 status: dict[str, Any] = status_value if isinstance(status_value, dict) else {} 

2672 

2673 if metadata.get("deletionTimestamp"): 

2674 return f"object is terminating since {metadata['deletionTimestamp']}" 

2675 

2676 for condition in _conditions(status): 

2677 if condition.get("type") in {"Ready", "Available"} and _condition_matches( 

2678 condition, "False" 

2679 ): 

2680 detail = condition.get("message") or condition.get("reason") or "no detail" 

2681 return f"condition {condition.get('type')} is False ({detail})" 

2682 

2683 if kind in {"GatewayClass", "Gateway", "HTTPRoute"}: 

2684 return _gateway_api_readiness_failure(kind, resource) 

2685 

2686 if kind in {"Deployment", "StatefulSet"}: 

2687 generation_failure = _generation_failure(resource) 

2688 if generation_failure: 

2689 return generation_failure 

2690 desired_value = spec.get("replicas", 1) 

2691 if not isinstance(desired_value, int) or isinstance(desired_value, bool): 

2692 return f"invalid desired replica count ({desired_value})" 

2693 desired = desired_value 

2694 fields = ( 

2695 ("replicas", "updatedReplicas", "readyReplicas", "availableReplicas") 

2696 if kind == "Deployment" 

2697 else ("currentReplicas", "updatedReplicas", "readyReplicas") 

2698 ) 

2699 return _replica_failure(status, desired, fields) 

2700 

2701 if kind == "DaemonSet": 

2702 generation_failure = _generation_failure(resource) 

2703 if generation_failure: 

2704 return generation_failure 

2705 desired_value = status.get("desiredNumberScheduled") 

2706 if not isinstance(desired_value, int) or isinstance(desired_value, bool): 

2707 return f"invalid desiredNumberScheduled ({desired_value})" 

2708 desired = desired_value 

2709 replica_failure = _replica_failure( 

2710 status, 

2711 desired, 

2712 ( 

2713 "currentNumberScheduled", 

2714 "updatedNumberScheduled", 

2715 "numberReady", 

2716 "numberAvailable", 

2717 ), 

2718 ) 

2719 if replica_failure: 

2720 return replica_failure 

2721 misscheduled = status.get("numberMisscheduled", 0) 

2722 if not isinstance(misscheduled, int) or isinstance(misscheduled, bool) or misscheduled != 0: 

2723 return f"pods are misscheduled (numberMisscheduled={misscheduled})" 

2724 return None 

2725 

2726 if kind == "Job": 

2727 failed = _required_condition_failure(status, "Failed") 

2728 if failed is None: 

2729 return "condition Failed is True" 

2730 return _required_condition_failure(status, "Complete") 

2731 

2732 if kind == "Pod": 

2733 return _required_condition_failure(status, "Ready") 

2734 

2735 if kind == "PersistentVolumeClaim": 

2736 phase = status.get("phase") 

2737 return None if phase == "Bound" else f"PVC phase is {phase!r}, expected 'Bound'" 

2738 

2739 if kind == "PersistentVolume": 

2740 phase = status.get("phase") 

2741 return ( 

2742 None 

2743 if phase in {"Bound", "Available"} 

2744 else f"PV phase is {phase!r}, expected 'Bound' or 'Available'" 

2745 ) 

2746 

2747 if kind == "CustomResourceDefinition": 

2748 return _required_condition_failure(status, "Established") 

2749 

2750 if kind == "APIService": 

2751 return _required_condition_failure(status, "Available") 

2752 

2753 if kind == "HorizontalPodAutoscaler": 

2754 for condition_type in ("AbleToScale", "ScalingActive"): 

2755 failure = _required_condition_failure(status, condition_type) 

2756 if failure: 

2757 return failure 

2758 return None 

2759 

2760 if kind == "PodDisruptionBudget": 

2761 generation_failure = _generation_failure(resource) 

2762 if generation_failure: 

2763 return generation_failure 

2764 current_healthy = status.get("currentHealthy") 

2765 desired_healthy = status.get("desiredHealthy") 

2766 if ( 

2767 not isinstance(current_healthy, int) 

2768 or isinstance(current_healthy, bool) 

2769 or not isinstance(desired_healthy, int) 

2770 or isinstance(desired_healthy, bool) 

2771 ): 

2772 return ( 

2773 "invalid PDB health " 

2774 f"(currentHealthy={current_healthy}, desiredHealthy={desired_healthy})" 

2775 ) 

2776 if current_healthy < desired_healthy: 

2777 return ( 

2778 "PDB health below target " 

2779 f"(currentHealthy={current_healthy}, desiredHealthy={desired_healthy})" 

2780 ) 

2781 return None 

2782 

2783 if kind in {"Certificate", "ClusterIssuer", "Issuer", "Bundle"}: 

2784 # cert-manager stamps observedGeneration on these Ready conditions, 

2785 # and trust-manager on a Bundle's Synced condition, so a condition 

2786 # left over from a previous spec (a re-pointed issuerRef, a new 

2787 # dnsName, another bundle source) is stale rather than ready. 

2788 generation = metadata.get("generation") 

2789 if not isinstance(generation, int) or isinstance(generation, bool): 

2790 return f"invalid metadata.generation ({generation})" 

2791 return _current_condition_failure( 

2792 _conditions(status), 

2793 "Synced" if kind == "Bundle" else "Ready", 

2794 generation, 

2795 kind, 

2796 ) 

2797 

2798 if kind in {"NodePool", "EC2NodeClass", "ScaledJob", "ScaledObject"}: 

2799 return _required_condition_failure(status, "Ready") 

2800 

2801 # Static configuration and RBAC resources have no rollout contract. Their 

2802 # exact-object existence is sufficient unless a generic Ready/Available 

2803 # condition explicitly reported False above. That includes the 

2804 # ValidatingAdmissionPolicy and its binding: the API server rejects a 

2805 # policy whose CEL does not compile, and enforces a stored one without 

2806 # waiting for the status controller (whose type check is advisory). 

2807 return None 

2808 

2809 

2810def _dynamic_resource( 

2811 dynamic_client: Any, 

2812 cache: dict[tuple[str, str], Any], 

2813 api_version: str, 

2814 kind: str, 

2815) -> Any: 

2816 key = (api_version, kind) 

2817 if key not in cache: 

2818 cache[key] = dynamic_client.resources.get(api_version=api_version, kind=kind) 

2819 return cache[key] 

2820 

2821 

2822def _service_endpoint_failure( 

2823 dynamic_client: Any, 

2824 cache: dict[tuple[str, str], Any], 

2825 planned_resource: dict[str, Any], 

2826) -> str | None: 

2827 document_value = planned_resource["document"] 

2828 document: dict[str, Any] = document_value if isinstance(document_value, dict) else {} 

2829 metadata_value = document.get("metadata") 

2830 document_metadata: dict[str, Any] = metadata_value if isinstance(metadata_value, dict) else {} 

2831 annotations_value = document_metadata.get("annotations") 

2832 annotations: dict[str, Any] = annotations_value if isinstance(annotations_value, dict) else {} 

2833 if str(annotations.get(_ALLOW_EMPTY_ENDPOINTS_ANNOTATION)).lower() == "true": 

2834 # Services backing accelerator-scheduled DaemonSets (for example the 

2835 # DCGM exporter) legitimately have zero endpoints until the first GPU 

2836 # node is provisioned; existence is their readiness contract. 

2837 return None 

2838 spec_value = document.get("spec") 

2839 spec: dict[str, Any] = spec_value if isinstance(spec_value, dict) else {} 

2840 selector = spec.get("selector") 

2841 if not isinstance(selector, dict) or not selector: 

2842 return None 

2843 

2844 endpoint_slices = _dynamic_resource( 

2845 dynamic_client, 

2846 cache, 

2847 "discovery.k8s.io/v1", 

2848 "EndpointSlice", 

2849 ) 

2850 response = endpoint_slices.get( 

2851 namespace=planned_resource["namespace"], 

2852 label_selector=f"kubernetes.io/service-name={planned_resource['name']}", 

2853 ) 

2854 items = _as_plain_dict(response).get("items", []) 

2855 if not isinstance(items, list): 

2856 return "EndpointSlice response does not contain an items list" 

2857 for item in items: 

2858 item_mapping = _as_plain_dict(item) 

2859 metadata_value = item_mapping.get("metadata") 

2860 metadata: dict[str, Any] = metadata_value if isinstance(metadata_value, dict) else {} 

2861 if metadata.get("deletionTimestamp"): 

2862 continue 

2863 endpoints = item_mapping.get("endpoints", []) 

2864 if not isinstance(endpoints, list): 

2865 continue 

2866 for endpoint in endpoints: 

2867 if not isinstance(endpoint, dict): 

2868 continue 

2869 conditions = endpoint.get("conditions", {}) 

2870 if not isinstance(conditions, dict): 

2871 continue 

2872 if conditions.get("ready") is True and conditions.get("terminating") is not True: 

2873 return None 

2874 return "selector-backed Service has no ready, nonterminating EndpointSlice endpoint" 

2875 

2876 

2877def _certificate_secret_failure( 

2878 dynamic_client: Any, 

2879 cache: dict[tuple[str, str], Any], 

2880 planned_resource: dict[str, Any], 

2881 live_resource: dict[str, Any], 

2882) -> str | None: 

2883 """Require cert-manager's referenced Secret to contain a nonempty TLS keypair.""" 

2884 spec_value = live_resource.get("spec") 

2885 spec: dict[str, Any] = spec_value if isinstance(spec_value, dict) else {} 

2886 secret_name = spec.get("secretName") 

2887 if not isinstance(secret_name, str) or not secret_name: 

2888 return "Certificate spec.secretName is missing" 

2889 secret_api = _dynamic_resource(dynamic_client, cache, "v1", "Secret") 

2890 response = secret_api.get( 

2891 namespace=planned_resource["namespace"], 

2892 name=secret_name, 

2893 ) 

2894 secret = _as_plain_dict(response) 

2895 data_value = secret.get("data") 

2896 data: dict[str, Any] = data_value if isinstance(data_value, dict) else {} 

2897 missing = [ 

2898 key for key in ("tls.crt", "tls.key") if not isinstance(data.get(key), str) or not data[key] 

2899 ] 

2900 if missing: 

2901 return f"Certificate Secret {secret_name!r} has no nonempty {', '.join(missing)}" 

2902 return None 

2903 

2904 

2905def _manifest_identity_label(resource: dict[str, Any]) -> str: 

2906 return ( 

2907 f"{resource['apiVersion']}/{resource['kind']}/" 

2908 f"{resource['namespace']}/{resource['name']}" 

2909 f" [{resource['phase']}:{resource['sourceFile']}]" 

2910 ) 

2911 

2912 

2913def validate_manifests( 

2914 cluster_name: str, 

2915 region: str, 

2916 manifests_dir: str, 

2917 replacements: dict[str, str], 

2918 deployment_token: Any = None, 

2919) -> dict[str, Any]: 

2920 """Validate exact existence and readiness for the complete planned inventory.""" 

2921 plan = plan_manifests(manifests_dir, replacements) 

2922 expected = plan["phases"]["base"] + plan["phases"]["post-helm"] 

2923 expected_resources = [_public_manifest_identity(item) for item in expected] 

2924 phase_counts = { 

2925 phase: {"ExpectedCount": len(plan["phases"][phase]), "ValidatedCount": 0} 

2926 for phase in ("base", "post-helm") 

2927 } 

2928 

2929 configure_k8s_client(cluster_name, region) 

2930 dynamic_client = dynamic.DynamicClient(client.ApiClient()) 

2931 resource_cache: dict[tuple[str, str], Any] = {} 

2932 validated_resources: list[dict[str, str]] = [] 

2933 failures: list[str] = [] 

2934 failure_count = 0 

2935 

2936 def add_failure(message: str) -> None: 

2937 nonlocal failure_count 

2938 failure_count += 1 

2939 if len(failures) < _MAX_VALIDATION_FAILURES: 

2940 failures.append(message[:500]) 

2941 

2942 for planned_resource in expected: 

2943 label = _manifest_identity_label(planned_resource) 

2944 try: 

2945 resource_api = _dynamic_resource( 

2946 dynamic_client, 

2947 resource_cache, 

2948 planned_resource["apiVersion"], 

2949 planned_resource["kind"], 

2950 ) 

2951 get_kwargs: dict[str, Any] = {"name": planned_resource["name"]} 

2952 if planned_resource["namespace"] != _CLUSTER_SCOPE: 

2953 get_kwargs["namespace"] = planned_resource["namespace"] 

2954 live_resource = _as_plain_dict(resource_api.get(**get_kwargs)) 

2955 failure = _resource_readiness_failure(planned_resource["kind"], live_resource) 

2956 if failure is None and planned_resource["kind"] == "Service": 

2957 failure = _service_endpoint_failure( 

2958 dynamic_client, 

2959 resource_cache, 

2960 planned_resource, 

2961 ) 

2962 if failure is None and planned_resource["kind"] == "Certificate": 

2963 failure = _certificate_secret_failure( 

2964 dynamic_client, 

2965 resource_cache, 

2966 planned_resource, 

2967 live_resource, 

2968 ) 

2969 if failure: 

2970 add_failure(f"{label}: {failure}") 

2971 continue 

2972 except (NotFoundError, ResourceNotFoundError) as exc: 

2973 add_failure(f"{label}: object or API resource not found ({exc})") 

2974 continue 

2975 except ApiException as exc: 

2976 detail = exc.reason or str(exc) 

2977 add_failure(f"{label}: Kubernetes API error {exc.status} ({detail})") 

2978 continue 

2979 except Exception as exc: 

2980 add_failure(f"{label}: validation error ({exc})") 

2981 continue 

2982 

2983 identity = _public_manifest_identity(planned_resource) 

2984 validated_resources.append(identity) 

2985 phase_counts[planned_resource["phase"]]["ValidatedCount"] += 1 

2986 

2987 if failure_count: 

2988 hidden = failure_count - len(failures) 

2989 suffix = f"; ... {hidden} additional failure(s)" if hidden else "" 

2990 raise RuntimeError( 

2991 f"Manifest validation failed: validated={len(validated_resources)} " 

2992 f"expected={len(expected)}; " + "; ".join(failures) + suffix 

2993 ) 

2994 

2995 return { 

2996 "status": "validated", 

2997 "DeploymentToken": deployment_token, 

2998 "ExpectedCount": len(expected), 

2999 "ValidatedCount": len(validated_resources), 

3000 "BaseExpectedCount": phase_counts["base"]["ExpectedCount"], 

3001 "BaseValidatedCount": phase_counts["base"]["ValidatedCount"], 

3002 "PostHelmExpectedCount": phase_counts["post-helm"]["ExpectedCount"], 

3003 "PostHelmValidatedCount": phase_counts["post-helm"]["ValidatedCount"], 

3004 "PhaseCounts": phase_counts, 

3005 "ExpectedResources": expected_resources, 

3006 "ValidatedResources": validated_resources, 

3007 } 

3008 

3009 

3010def _record_phase_status(phase: str, status: str, message: str) -> None: 

3011 """Record a convergence phase's outcome to SSM (best-effort). 

3012 

3013 Mirrors the helm worker's per-chart status (``_record_addon_status``) so 

3014 ``gco stacks addons status`` surfaces the base and post-Helm apply passes 

3015 alongside the charts. Writes ``/<project>/addons/<region>/<phase>`` as a 

3016 small JSON blob. Failures are swallowed — status reporting must never turn a 

3017 successful apply into a failure (or vice versa). Reads PROJECT_NAME / REGION 

3018 from the Lambda environment; a no-op if either is unset. 

3019 """ 

3020 project = os.environ.get("PROJECT_NAME") 

3021 region = os.environ.get("REGION") 

3022 if not project or not region: 

3023 return 

3024 import contextlib 

3025 import time as _time 

3026 

3027 with contextlib.suppress(Exception): 

3028 boto3.client("ssm").put_parameter( 

3029 Name=f"/{project}/addons/{region}/{phase}", 

3030 Value=json.dumps( 

3031 { 

3032 "phase": phase, 

3033 "status": status, 

3034 "message": message[:1024], 

3035 "updated_at": int(_time.time()), 

3036 } 

3037 ), 

3038 Type="String", 

3039 Overwrite=True, 

3040 ) 

3041 

3042 

3043def handle_task(event: dict[str, Any]) -> dict[str, Any]: 

3044 """Run an apply or exhaustive manifest-validation Step Functions task.""" 

3045 cluster_name = event["ClusterName"] 

3046 region = event["Region"] 

3047 replacements = event.get("ImageReplacements", {}) 

3048 action = event.get("Action", "apply_manifests") 

3049 manifests_dir = os.path.join(os.path.dirname(__file__), "manifests") 

3050 

3051 if action == "delete_gateway_resources": 

3052 phase = "gateway-teardown" 

3053 try: 

3054 result = _delete_gateway_resources(cluster_name, region) 

3055 except Exception as exc: 

3056 _record_phase_status(phase, "failed", str(exc)) 

3057 raise 

3058 _record_phase_status( 

3059 phase, 

3060 "deleted", 

3061 f"deleted={result['DeletedCount']}", 

3062 ) 

3063 return result 

3064 

3065 if action == "validate_manifests": 

3066 phase = "manifest-validation" 

3067 deployment_token = event.get("DeploymentToken") 

3068 try: 

3069 result = validate_manifests( 

3070 cluster_name, 

3071 region, 

3072 manifests_dir, 

3073 replacements, 

3074 deployment_token, 

3075 ) 

3076 except Exception as exc: 

3077 _record_phase_status(phase, "failed", f"token={deployment_token} {exc}") 

3078 raise 

3079 _record_phase_status( 

3080 phase, 

3081 "validated", 

3082 f"token={deployment_token} validated={result['ValidatedCount']} " 

3083 f"expected={result['ExpectedCount']}", 

3084 ) 

3085 return result 

3086 

3087 if action != "apply_manifests": 

3088 raise ValueError(f"Unsupported task action: {action}") 

3089 

3090 post_helm = str(event.get("PostHelm", "false")).lower() == "true" 

3091 phase = "post-helm-manifests" if post_helm else "base-manifests" 

3092 try: 

3093 result = apply_manifests(cluster_name, region, manifests_dir, replacements, post_helm) 

3094 except Exception as exc: 

3095 _record_phase_status(phase, "failed", str(exc)) 

3096 raise 

3097 if result.get("FailedCount", 0): 

3098 _record_phase_status(phase, "failed", str(result.get("Failed"))) 

3099 raise RuntimeError( 

3100 f"kubectl apply failed (post_helm={post_helm}, " 

3101 f"failed={result.get('FailedCount')}): {result.get('Failed')}" 

3102 ) 

3103 if result.get("ExpectedCount") is not None and result.get("AppliedCount") != result.get( 

3104 "ExpectedCount" 

3105 ): 

3106 message = ( 

3107 f"apply count mismatch: applied={result.get('AppliedCount')} " 

3108 f"expected={result.get('ExpectedCount')}" 

3109 ) 

3110 _record_phase_status(phase, "failed", message) 

3111 raise RuntimeError(message) 

3112 _record_phase_status( 

3113 phase, 

3114 "applied", 

3115 f"applied={result.get('AppliedCount')} expected={result.get('ExpectedCount')} " 

3116 f"skipped={result.get('SkippedCount')}", 

3117 ) 

3118 return result 

3119 

3120 

3121def lambda_handler(event: dict[str, Any], context: Any) -> Any: 

3122 """Main Lambda handler. 

3123 

3124 Two entrypoints share this function: 

3125 

3126 - **Step Functions task** (the convergence pipeline): the event carries an 

3127 ``Action`` key and is dispatched to :func:`handle_task`, which applies the 

3128 manifests and raises on any failure. 

3129 - **CloudFormation custom resource** (legacy/fallback): the event carries a 

3130 ``RequestType`` and the result is POSTed back to CloudFormation. 

3131 """ 

3132 if event.get("Action"): 

3133 logger.info(f"Task event: {json.dumps(event)}") 

3134 return handle_task(event) 

3135 

3136 print(f"[HANDLER] Received event type: {event.get('RequestType')}") 

3137 logger.info(f"Received event: {json.dumps(event)}") 

3138 

3139 request_type = event["RequestType"] 

3140 physical_resource_id = event.get("PhysicalResourceId", f"kubectl-{event['LogicalResourceId']}") 

3141 

3142 try: 

3143 properties = event["ResourceProperties"] 

3144 cluster_name = properties["ClusterName"] 

3145 region = properties["Region"] 

3146 

3147 if request_type == "Create" or request_type == "Update": 

3148 manifests_dir = os.path.join(os.path.dirname(__file__), "manifests") 

3149 replacements = properties.get("ImageReplacements", {}) 

3150 # PostHelm: "true" means this is the post-Helm pass — apply only post-helm-* files 

3151 post_helm = properties.get("PostHelm", "false").lower() == "true" 

3152 response_data = apply_manifests( 

3153 cluster_name, region, manifests_dir, replacements, post_helm 

3154 ) 

3155 failed_count = int(response_data.get("FailedCount", 0)) 

3156 prune_failures = response_data.get("PruneFailures") 

3157 if failed_count > 0 or prune_failures not in (None, "", "None"): 

3158 raise RuntimeError( 

3159 "Manifest application reported failures: " 

3160 f"failed_count={failed_count}, failed={response_data.get('Failed', 'unknown')}, " 

3161 f"prune_failures={prune_failures or 'None'}" 

3162 ) 

3163 send_response(event, context, SUCCESS, response_data, physical_resource_id) 

3164 

3165 elif request_type == "Delete": 

3166 # Always succeed on delete to prevent stack from getting stuck 

3167 skip_deletion = properties.get("SkipDeletionOnStackDelete", "false").lower() == "true" 

3168 if skip_deletion: 

3169 logger.info("Skipping deletion (SkipDeletionOnStackDelete=true)") 

3170 response_data = {"Status": "Deleted"} 

3171 send_response(event, context, SUCCESS, response_data, physical_resource_id) 

3172 

3173 else: 

3174 # CloudFormation only ever sends Create/Update/Delete. Anything else 

3175 # must still get a FAILED callback: silently returning would leave 

3176 # the stack waiting on this resource until its timeout. 

3177 raise ValueError(f"Unsupported RequestType: {request_type}") 

3178 

3179 except Exception as e: 

3180 logger.error(f"Error: {e}", exc_info=True) 

3181 # On delete, always return success to prevent stack from getting stuck 

3182 if request_type == "Delete": 

3183 send_response( 

3184 event, 

3185 context, 

3186 SUCCESS, 

3187 {"Status": "Forced success on delete"}, 

3188 physical_resource_id, 

3189 ) 

3190 else: 

3191 send_response(event, context, FAILED, {}, physical_resource_id, str(e))