Coverage for gco / config / config_loader.py: 100.00%

786 statements  

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

1""" 

2Configuration loader for GCO (Global Capacity Orchestrator on AWS). 

3 

4This module loads and validates configuration from CDK context (cdk.json). 

5It provides type-safe access to all configuration values with sensible defaults 

6and comprehensive validation. 

7 

8Configuration Sections: 

9- project_name: Unique identifier for the deployment 

10- regions: List of AWS regions to deploy to 

11- kubernetes_version: EKS Kubernetes version 

12- resource_thresholds: CPU/memory/GPU utilization thresholds 

13- global_accelerator: Global Accelerator settings 

14- alb_config: Application Load Balancer health check settings 

15- inference_proxy: Shared inference TLS proxy CPU request and HPA target 

16- manifest_processor: Manifest validation and resource limits 

17- api_gateway: Throttling and logging configuration 

18- tracing: OpenTelemetry -> AWS X-Ray tracing for the API services 

19- tags: Common tags applied to all resources 

20 

21Usage: 

22 config = ConfigLoader(app) 

23 regions = config.get_regions() 

24 cluster_config = config.get_cluster_config("us-east-1") 

25""" 

26 

27from __future__ import annotations 

28 

29import copy 

30import json 

31import logging 

32import math 

33import re 

34from collections.abc import Mapping 

35from typing import Any, cast 

36 

37import boto3 

38from aws_cdk import App 

39 

40from gco.argocd_config import ArgoCdConfigError, validate_argocd_config 

41from gco.eks_capabilities_config import ( 

42 EKS_CAPABILITIES_CONTEXT_KEY, 

43 EKS_CAPABILITIES_OVERRIDES_CONTEXT_KEY, 

44 EksCapabilitiesConfigError, 

45 merge_eks_capabilities_overrides, 

46 parse_eks_capabilities_overrides, 

47 validate_eks_capabilities_config, 

48) 

49from gco.inference_proxy_config import ( 

50 INFERENCE_PROXY_MAX_REPLICAS_DEFAULT, 

51 INFERENCE_PROXY_MIN_REPLICAS_DEFAULT, 

52 INFERENCE_PROXY_TLS_CPU_REQUEST_MILLICORES_DEFAULT, 

53 INFERENCE_PROXY_TLS_CPU_TARGET_UTILIZATION_DEFAULT, 

54) 

55from gco.manifest_security_policy import validate_manifest_security_policy 

56from gco.models import ClusterConfig, ResourceThresholds 

57from gco.resource_governance import parse_k8s_quantity 

58from gco.slurm_config import SlurmConfigError, validate_slurm_config 

59from gco.stacks.constants import ( 

60 CONTROL_PLANE_ONLY_CONTEXT_KEY, 

61 DEFAULT_MAX_REQUEST_BODY_BYTES, 

62 control_plane_only_requested, 

63 known_cloudformation_regions, 

64 validated_deployment_partition, 

65 validated_regional_deployment_regions, 

66 validated_request_body_limit, 

67) 

68 

69logger = logging.getLogger(__name__) 

70 

71#: ``manifest_processor.autoscaling`` defaults. Off: the API tier is I/O-bound 

72#: (Kubernetes round trips), so CPU is a weak saturation signal; an operator who 

73#: has measured a CPU-bound API turns it on and the HPA owns the replica count 

74#: between ``manifest_processor.replicas`` and ``max_replicas``. 

75_MANIFEST_PROCESSOR_AUTOSCALING_DEFAULTS: dict[str, Any] = { 

76 "enabled": False, 

77 "max_replicas": 6, 

78 "cpu_target_utilization_percentage": 70, 

79} 

80 

81#: The ``manifest-processor`` container's fixed requests in 

82#: ``lambda/kubectl-applier-simple/manifests/31-manifest-processor.yaml``; 

83#: ``manifest_processor.resource_limits`` may not go below them. Pinned to the 

84#: manifest by ``tests/test_config_loader.py``. 

85_MANIFEST_PROCESSOR_CONTAINER_REQUESTS: dict[str, str] = { 

86 "cpu": "500m", 

87 "memory": "1Gi", 

88} 

89 

90#: Gateway VPC endpoints ``vpc_endpoints.gateway`` may name. Free, route-table 

91#: based, same-region only. 

92VPC_GATEWAY_ENDPOINT_SERVICES: tuple[str, ...] = ("s3", "dynamodb") 

93 

94#: Interface (PrivateLink) endpoints ``vpc_endpoints.interface`` may name, 

95#: keyed the way the regional stack maps them onto 

96#: ``ec2.InterfaceVpcEndpointAwsService``. Each costs per AZ-hour plus per GB, 

97#: so the list is opt-in; together they keep every AWS API call the platform 

98#: and its jobs make inside the VPC (the cross-region calls to the global 

99#: region's tables and buckets still leave through the NAT gateways). 

100VPC_INTERFACE_ENDPOINT_SERVICES: tuple[str, ...] = ( 

101 "sts", 

102 "ecr.api", 

103 "ecr.dkr", 

104 "logs", 

105 "monitoring", 

106 "sqs", 

107 "ssm", 

108 "secretsmanager", 

109 "kms", 

110 "eks", 

111 "elasticfilesystem", 

112 "bedrock-runtime", 

113 # OTLP span export from the traced API services (``tracing`` block). 

114 "xray", 

115) 

116 

117_VPC_ENDPOINTS_DEFAULTS: dict[str, list[str]] = { 

118 "gateway": ["s3", "dynamodb"], 

119 "interface": [], 

120} 

121 

122#: CDK context key that force-enables optional infrastructure features for one 

123#: deploy without touching cdk.json — the infrastructure sibling of the 

124#: ``helm_enabled_overrides`` context handled in ``gco/stacks/regional_stack.py``. 

125#: Used by validation harnesses (``gco examples validate``) whose preflight 

126#: requires a clean worktree. 

127FEATURE_OVERRIDE_CONTEXT_KEY = "feature_enabled_overrides" 

128 

129#: The cdk.json blocks whose ``enabled`` flag the override may force on. Kept 

130#: deliberately narrow: each of these is a self-contained regional feature the 

131#: examples exercise (Aurora pgvector, Valkey Serverless, FSx for Lustre). 

132FEATURE_OVERRIDE_KEYS = frozenset({"aurora_pgvector", "valkey", "fsx_lustre", "vector_store"}) 

133 

134 

135def parse_feature_enabled_overrides(raw: object) -> frozenset[str]: 

136 """Parse and validate the ``feature_enabled_overrides`` context value. 

137 

138 Accepts a comma-separated string (the only shape the CDK CLI can pass with 

139 ``--context``) or a list of strings (cdk.json-style). Unknown names raise 

140 at synth time with the valid list — identical semantics to 

141 ``_parse_helm_enabled_overrides``. 

142 """ 

143 if raw is None: 

144 return frozenset() 

145 if isinstance(raw, str): 

146 names = [part.strip() for part in raw.split(",") if part.strip()] 

147 elif isinstance(raw, list) and all(isinstance(part, str) for part in raw): 

148 names = [part.strip() for part in raw if part.strip()] 

149 else: 

150 raise ConfigValidationError( 

151 f"{FEATURE_OVERRIDE_CONTEXT_KEY} must be a comma-separated string or string list" 

152 ) 

153 unknown = sorted(set(names) - FEATURE_OVERRIDE_KEYS) 

154 if unknown: 

155 valid = ", ".join(sorted(FEATURE_OVERRIDE_KEYS)) 

156 raise ConfigValidationError( 

157 f"Unknown {FEATURE_OVERRIDE_CONTEXT_KEY} name(s): {', '.join(unknown)}. Valid: {valid}" 

158 ) 

159 return frozenset(names) 

160 

161 

162#: cdk.json context key for the OpenTelemetry -> AWS X-Ray tracing block. 

163TRACING_CONTEXT_KEY = "tracing" 

164 

165#: Run-scoped CDK context key carrying a JSON object deep-merged over the 

166#: ``tracing`` block for one deploy (``cdk deploy --context 

167#: tracing_overrides='{"sample_ratio": 1.0}'``) — the tracing sibling of 

168#: ``eks_capabilities_overrides``. The live release harness, whose preflight 

169#: requires a clean worktree, records every trace of its run this way. 

170TRACING_OVERRIDES_CONTEXT_KEY = "tracing_overrides" 

171 

172#: The ``tracing`` block with every knob at its default. Tracing is ON by 

173#: default, like cluster observability: an absent block means the shipped 

174#: posture, not "off". ``sample_ratio`` is the head-sampling probability of a 

175#: new trace; ``enable_transaction_search`` lets each regional stack switch on 

176#: CloudWatch Transaction Search, which the X-Ray OTLP endpoint requires, and 

177#: only matters while ``enabled`` is true. 

178TRACING_DEFAULTS: dict[str, Any] = { 

179 "enabled": True, 

180 "sample_ratio": 0.05, 

181 "enable_transaction_search": True, 

182} 

183 

184 

185def _deep_merge(base: Mapping[str, Any], overlay: Mapping[str, Any]) -> dict[str, Any]: 

186 """Return ``overlay`` recursively merged over a copy of ``base``.""" 

187 merged: dict[str, Any] = copy.deepcopy(dict(base)) 

188 for key, value in overlay.items(): 

189 if isinstance(value, Mapping) and isinstance(merged.get(key), Mapping): 

190 merged[key] = _deep_merge(merged[key], value) 

191 else: 

192 merged[key] = copy.deepcopy(value) 

193 return merged 

194 

195 

196def parse_tracing_overrides(raw: object) -> dict[str, Any]: 

197 """Parse the ``tracing_overrides`` context value into a mapping. 

198 

199 Accepts a JSON object string (the only shape ``cdk --context`` can carry) 

200 or a mapping (cdk.json-style); ``None`` and blank strings mean no 

201 overrides. Only the shape is checked here: the merged block is validated 

202 as a whole, so an override that produces an invalid block fails exactly 

203 like an invalid cdk.json does. 

204 """ 

205 if raw is None: 

206 return {} 

207 if isinstance(raw, Mapping): 

208 return copy.deepcopy(dict(raw)) 

209 if isinstance(raw, str): 

210 if not raw.strip(): 

211 return {} 

212 try: 

213 parsed = json.loads(raw) 

214 except json.JSONDecodeError as exc: 

215 raise ConfigValidationError( 

216 f"{TRACING_OVERRIDES_CONTEXT_KEY} must be a JSON object: {exc}" 

217 ) from exc 

218 if not isinstance(parsed, dict): 

219 raise ConfigValidationError( 

220 f"{TRACING_OVERRIDES_CONTEXT_KEY} must be a JSON object, got {type(parsed).__name__}" 

221 ) 

222 return parsed 

223 raise ConfigValidationError( 

224 f"{TRACING_OVERRIDES_CONTEXT_KEY} must be a JSON object string or an object, " 

225 f"got {type(raw).__name__}" 

226 ) 

227 

228 

229def resolve_tracing_config(raw: object, overrides: object = None) -> dict[str, Any]: 

230 """Return the validated ``tracing`` block with overrides and defaults applied. 

231 

232 Single source of truth shared by :meth:`ConfigLoader.get_tracing_config` 

233 and the regional stack, which falls back to it for config doubles that 

234 predate the knob. ``overrides`` is the raw ``tracing_overrides`` context 

235 value. Types are checked exactly: the flags are rendered verbatim into 

236 the ``GCO_TRACING_ENABLED`` env var and a quoted ``"false"`` in cdk.json 

237 must not read as enabled, and ``sample_ratio`` must be a real number in 

238 ``[0, 1]`` because the services treat it as a probability. 

239 

240 Raises: 

241 ConfigValidationError: If the block or the overrides are malformed. 

242 """ 

243 if raw is None: 

244 block: Mapping[str, Any] = {} 

245 elif isinstance(raw, Mapping): 

246 block = raw 

247 else: 

248 raise ConfigValidationError( 

249 f"{TRACING_CONTEXT_KEY} must be an object, got {type(raw).__name__}" 

250 ) 

251 parsed_overrides = parse_tracing_overrides(overrides) 

252 merged = _deep_merge(_deep_merge(TRACING_DEFAULTS, block), parsed_overrides) 

253 

254 unknown = sorted(str(key) for key in merged if key not in TRACING_DEFAULTS) 

255 if unknown: 

256 source = f" (after {TRACING_OVERRIDES_CONTEXT_KEY})" if parsed_overrides else "" 

257 raise ConfigValidationError( 

258 f"{TRACING_CONTEXT_KEY}{source} contains unknown key(s): {', '.join(unknown)}; " 

259 f"allowed keys: {', '.join(sorted(TRACING_DEFAULTS))}" 

260 ) 

261 for flag in ("enabled", "enable_transaction_search"): 

262 if type(merged[flag]) is not bool: 

263 raise ConfigValidationError( 

264 f"{TRACING_CONTEXT_KEY}.{flag} must be a boolean, got {merged[flag]!r}" 

265 ) 

266 ratio = merged["sample_ratio"] 

267 if ( 

268 isinstance(ratio, bool) 

269 or not isinstance(ratio, int | float) 

270 or not math.isfinite(ratio) 

271 or not 0 <= ratio <= 1 

272 ): 

273 raise ConfigValidationError( 

274 f"{TRACING_CONTEXT_KEY}.sample_ratio must be a number from 0 to 1 (inclusive), " 

275 f"got {ratio!r}" 

276 ) 

277 return { 

278 "enabled": merged["enabled"], 

279 "sample_ratio": float(ratio), 

280 "enable_transaction_search": merged["enable_transaction_search"], 

281 } 

282 

283 

284class ConfigValidationError(Exception): 

285 """Raised when configuration validation fails.""" 

286 

287 

288class ConfigLoader: 

289 """ 

290 Loads and validates configuration from CDK context (cdk.json) 

291 """ 

292 

293 # Keep the public class attribute for compatibility. Endpoint metadata 

294 # covers every CloudFormation Region known to the installed AWS SDK; this 

295 # is not a project-specific allowlist. 

296 VALID_REGIONS = known_cloudformation_regions() 

297 

298 def __init__(self, app: App): 

299 self.app = app 

300 self._validate_configuration() 

301 

302 def _validate_configuration(self) -> None: 

303 """Validate the entire configuration""" 

304 # Check if we have any context at all (might be running outside CDK) 

305 project_name = self.app.node.try_get_context("project_name") 

306 if project_name is None: 

307 # Running outside CDK context, skip validation 

308 return 

309 

310 # Validate required fields exist 

311 required_fields = [ 

312 "project_name", 

313 "kubernetes_version", 

314 "resource_thresholds", 

315 ] 

316 for field in required_fields: 

317 if not self.app.node.try_get_context(field): 

318 raise ConfigValidationError(f"Required configuration field '{field}' is missing") 

319 

320 # Validate project_name format before anything consumes it (#139). 

321 self._validate_project_name() 

322 

323 # Check for deployment_regions 

324 deployment_regions = self.app.node.try_get_context("deployment_regions") 

325 if not isinstance(deployment_regions, dict) or not deployment_regions: 

326 raise ConfigValidationError( 

327 "Required configuration field 'deployment_regions' must be a non-empty object" 

328 ) 

329 

330 # Validate regions 

331 self._validate_regions() 

332 

333 # Validate resource thresholds 

334 self._validate_resource_thresholds() 

335 

336 # Validate Global Accelerator config 

337 self._validate_global_accelerator_config() 

338 

339 # Validate deployment-local backend TLS rotation policy 

340 self._validate_backend_tls_config() 

341 

342 # Validate inference proxy TLS sidecar autoscaling settings 

343 self._validate_inference_proxy_config() 

344 

345 # Validate ALB config 

346 self._validate_alb_config() 

347 

348 # Validate manifest processor config 

349 self._validate_manifest_processor_config() 

350 

351 # Validate API Gateway config 

352 self._validate_api_gateway_config() 

353 

354 # Validate EKS cluster config 

355 self._validate_eks_cluster_config() 

356 

357 # Validate the opt-in EKS Capabilities block (ACK / kro) 

358 self._validate_eks_capabilities_config() 

359 # Validate the self-managed Argo CD block (helm.argocd) 

360 self._validate_argocd_config() 

361 # Validate the Slurm cluster block (helm.slurm) 

362 self._validate_slurm_config() 

363 

364 # Validate analytics environment config (optional block) 

365 self._validate_analytics_environment_config() 

366 

367 # Validate cluster observability config (optional block) 

368 self._validate_cluster_observability_config() 

369 

370 # Validate cost monitoring config (optional block) 

371 self._validate_cost_monitoring_config() 

372 

373 # Validate the OpenTelemetry -> X-Ray tracing block and its run-scoped 

374 # overrides (optional; on by default) 

375 self._validate_tracing_config() 

376 

377 # Validate historical capacity surface config (optional block) 

378 self._validate_capacity_history_config() 

379 

380 # Validate mission-memory configuration (recall across mission sessions) 

381 self._validate_mission_memory_config() 

382 # Validate the vector-store configuration (global workload RAG corpus) 

383 self._validate_vector_store_config() 

384 

385 #: Allowed ``project_name`` format (#139). ``project_name`` is the 

386 #: deployment's unique prefix and flows into S3 bucket names, the Cognito 

387 #: hosted-UI domain prefix, SSM parameter paths, IAM role names, and 

388 #: CloudFormation export names. The tightest of those constraints is S3 / 

389 #: Cognito naming (lowercase letters, digits, hyphens; must start with a 

390 #: letter), so require: a leading lowercase letter followed by 1–30 

391 #: lowercase letters, digits, or hyphens (total length 2–31). 

392 PROJECT_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]{1,30}$") 

393 

394 def _validate_project_name(self) -> None: 

395 """Validate ``project_name`` format so misconfigurations fail at synth. 

396 

397 ``project_name`` is documented as the deployment's unique identifier 

398 and is used as the prefix for nearly every physical resource name. If 

399 it contains characters that are illegal in S3 bucket names or Cognito 

400 domain prefixes (uppercase, underscores, dots, leading digit, etc.), 

401 ``cdk synth`` still succeeds but the deploy fails late with an opaque 

402 AWS naming error. Catching it here turns that into an actionable 

403 message up front. 

404 """ 

405 project_name = self.app.node.try_get_context("project_name") 

406 if not isinstance(project_name, str) or not self.PROJECT_NAME_PATTERN.match(project_name): 

407 raise ConfigValidationError( 

408 f"Invalid project_name {project_name!r}. It must match " 

409 f"{self.PROJECT_NAME_PATTERN.pattern} (start with a lowercase letter, then " 

410 "2–31 total characters of lowercase letters, digits, or hyphens). " 

411 "project_name is the deployment prefix for S3 buckets, the Cognito " 

412 "domain, SSM paths, and CloudFormation exports, so it must be a valid " 

413 "lowercase DNS-style label." 

414 ) 

415 

416 def _validate_regions(self) -> None: 

417 """Validate region configuration against the shared app/CLI contract.""" 

418 deployment_regions = self.get_deployment_regions() 

419 try: 

420 for field in ("global", "api_gateway", "monitoring"): 

421 region = deployment_regions[field] 

422 if not isinstance(region, str) or region not in self.VALID_REGIONS: 

423 raise ValueError( 

424 f"Invalid {field} region {region!r}; expected an AWS region with a " 

425 "CloudFormation endpoint known to the installed SDK" 

426 ) 

427 regional = validated_regional_deployment_regions( 

428 deployment_regions["regional"], 

429 known_regions=self.VALID_REGIONS, 

430 ) 

431 validated_deployment_partition( 

432 ( 

433 deployment_regions["global"], 

434 deployment_regions["api_gateway"], 

435 deployment_regions["monitoring"], 

436 *regional, 

437 ) 

438 ) 

439 except (RuntimeError, ValueError) as exc: 

440 raise ConfigValidationError(str(exc)) from exc 

441 

442 def _validate_resource_thresholds(self) -> None: 

443 """Validate resource threshold configuration""" 

444 thresholds_config = self.app.node.try_get_context("resource_thresholds") 

445 

446 required_thresholds = ["cpu_threshold", "memory_threshold", "gpu_threshold"] 

447 for threshold in required_thresholds: 

448 if threshold not in thresholds_config: 

449 raise ConfigValidationError(f"Missing threshold configuration: {threshold}") 

450 

451 value = thresholds_config[threshold] 

452 if not isinstance(value, int) or (value != -1 and not 0 <= value <= 100): 

453 raise ConfigValidationError( 

454 f"{threshold} must be an integer between 0 and 100 (or -1 to disable), got {value}" 

455 ) 

456 

457 # Validate optional thresholds if present 

458 for opt_threshold in [ 

459 "pending_pods_threshold", 

460 "pending_requested_cpu_vcpus", 

461 "pending_requested_memory_gb", 

462 "pending_requested_gpus", 

463 ]: 

464 if opt_threshold in thresholds_config: 

465 value = thresholds_config[opt_threshold] 

466 if not isinstance(value, int) or (value != -1 and value < 0): 

467 raise ConfigValidationError( 

468 f"{opt_threshold} must be a non-negative integer (or -1 to disable), got {value}" 

469 ) 

470 

471 #: Health-check probe intervals Global Accelerator accepts. The API 

472 #: constrains HealthCheckIntervalSeconds to exactly 10 or 30 seconds, so 

473 #: any other value must fail at synth instead of at deploy. 

474 GLOBAL_ACCELERATOR_HEALTH_CHECK_INTERVALS = frozenset({10, 30}) 

475 

476 #: Traffic-dial controller modes. ``monitor`` computes and publishes 

477 #: per-region dial decisions without mutating Global Accelerator; 

478 #: ``enforce`` additionally applies them via UpdateEndpointGroup. 

479 TRAFFIC_DIAL_MODES = frozenset({"monitor", "enforce"}) 

480 

481 def _validate_global_accelerator_config(self) -> None: 

482 """Validate the ``global_accelerator`` block in cdk.json. 

483 

484 The block is optional; absence means the shipped defaults apply. 

485 Validation runs against the *merged* configuration so a partial block 

486 is checked together with every default it kept: 

487 

488 - ``health_check_interval``: 10 or 30 — the only probe intervals the 

489 Global Accelerator API accepts (UpdateEndpointGroup rejects others). 

490 - ``health_check_threshold``: integer 1-10 (the API ThresholdCount 

491 range). 

492 - ``health_check_path``: must start with ``/``. 

493 - ``client_affinity``: NONE or SOURCE_IP, case-insensitive. 

494 - ``traffic_dial``: see :meth:`_validate_traffic_dial_config`. 

495 

496 The legacy ``health_check_grace_period`` and ``health_check_timeout`` 

497 keys are tolerated and ignored: Global Accelerator endpoint groups 

498 have no such settings (the keys were validated but never consumed), 

499 so their presence in an existing cdk.json must not fail synthesis. 

500 

501 ``name`` is intentionally optional: when omitted it defaults to 

502 ``<project_name>-accelerator`` so a second deployment gets a 

503 project-scoped name from the single ``project_name`` knob (#139). 

504 """ 

505 ga_config = self.get_global_accelerator_config() 

506 

507 interval = ga_config["health_check_interval"] 

508 if ( 

509 not isinstance(interval, int) 

510 or isinstance(interval, bool) 

511 or interval not in self.GLOBAL_ACCELERATOR_HEALTH_CHECK_INTERVALS 

512 ): 

513 raise ConfigValidationError( 

514 "global_accelerator.health_check_interval must be one of " 

515 f"{sorted(self.GLOBAL_ACCELERATOR_HEALTH_CHECK_INTERVALS)} — the only probe " 

516 f"intervals the Global Accelerator API accepts — got {interval!r}" 

517 ) 

518 

519 threshold = ga_config["health_check_threshold"] 

520 if ( 

521 not isinstance(threshold, int) 

522 or isinstance(threshold, bool) 

523 or not 1 <= threshold <= 10 

524 ): 

525 raise ConfigValidationError( 

526 "global_accelerator.health_check_threshold must be an integer between " 

527 f"1 and 10, got {threshold!r}" 

528 ) 

529 

530 path = ga_config["health_check_path"] 

531 if not isinstance(path, str) or not path.startswith("/"): 

532 raise ConfigValidationError("health_check_path must start with '/'") 

533 

534 allowed_affinity = {"NONE", "SOURCE_IP"} 

535 affinity = ga_config["client_affinity"] 

536 if not isinstance(affinity, str) or affinity.upper() not in allowed_affinity: 

537 raise ConfigValidationError( 

538 f"client_affinity must be one of {sorted(allowed_affinity)}, got {affinity!r}" 

539 ) 

540 

541 self._validate_traffic_dial_config(ga_config["traffic_dial"]) 

542 

543 def _validate_traffic_dial_config(self, dial_config: Any) -> None: 

544 """Validate the merged ``global_accelerator.traffic_dial`` sub-block. 

545 

546 Ranges mirror the Global Accelerator API and the controller contract: 

547 

548 - ``enabled``: bool (default False — the controller is opt-in). 

549 - ``mode``: ``monitor`` or ``enforce``, case-insensitive. 

550 - ``interval_minutes`` / ``lookback_minutes``: 1-1440. 

551 - ``min_dial_percentage``: 0-100 (TrafficDialPercentage range); the 

552 floor a degraded region can be dialed down to. 

553 - ``max_step_percentage``: 1-100; the largest change one run applies. 

554 - ``full_health_percentage``: 1-100; the healthy fraction (percent) 

555 at or above which a region is restored toward 100. 

556 """ 

557 if not isinstance(dial_config, dict): 

558 raise ConfigValidationError( 

559 "global_accelerator.traffic_dial must be a mapping, got " 

560 f"{type(dial_config).__name__}: {dial_config!r}" 

561 ) 

562 

563 enabled = dial_config["enabled"] 

564 if not isinstance(enabled, bool): 

565 raise ConfigValidationError( 

566 "global_accelerator.traffic_dial.enabled must be a bool, got " 

567 f"{type(enabled).__name__}: {enabled!r}" 

568 ) 

569 

570 mode = dial_config["mode"] 

571 if not isinstance(mode, str) or mode.lower() not in self.TRAFFIC_DIAL_MODES: 

572 raise ConfigValidationError( 

573 "global_accelerator.traffic_dial.mode must be one of " 

574 f"{sorted(self.TRAFFIC_DIAL_MODES)}, got {mode!r}" 

575 ) 

576 

577 int_ranges = ( 

578 ("interval_minutes", 1, 1_440), 

579 ("lookback_minutes", 1, 1_440), 

580 ("min_dial_percentage", 0, 100), 

581 ("max_step_percentage", 1, 100), 

582 ("full_health_percentage", 1, 100), 

583 ) 

584 for field, minimum, maximum in int_ranges: 

585 value = dial_config[field] 

586 if ( 

587 not isinstance(value, int) 

588 or isinstance(value, bool) 

589 or not minimum <= value <= maximum 

590 ): 

591 raise ConfigValidationError( 

592 f"global_accelerator.traffic_dial.{field} must be an integer between " 

593 f"{minimum} and {maximum}, got {value!r}" 

594 ) 

595 

596 def _validate_backend_tls_config(self) -> None: 

597 """Validate private-root and leaf-certificate lifecycle settings.""" 

598 config = self.get_backend_tls_config() 

599 ranges = { 

600 "root_generation": (1, 1_000_000), 

601 "root_validity_days": (365, 36_500), 

602 "root_rotate_before_days": (30, 3_650), 

603 "root_activation_delay_hours": (1, 168), 

604 "root_overlap_days": (2, 365), 

605 "leaf_validity_days": (2, 397), 

606 "leaf_rotate_before_days": (1, 90), 

607 "rotation_schedule_hours": (1, 24), 

608 "trust_cache_ttl_seconds": (1, 3_600), 

609 "trust_cache_max_stale_seconds": (1, 86_400), 

610 } 

611 for field, (minimum, maximum) in ranges.items(): 

612 value = config.get(field) 

613 if type(value) is not int or not minimum <= value <= maximum: 

614 raise ConfigValidationError( 

615 f"backend_tls.{field} must be an integer between " 

616 f"{minimum} and {maximum}, got {value!r}" 

617 ) 

618 

619 if config["root_rotate_before_days"] >= config["root_validity_days"]: 

620 raise ConfigValidationError( 

621 "backend_tls.root_rotate_before_days must be less than root_validity_days" 

622 ) 

623 if config["leaf_rotate_before_days"] >= config["leaf_validity_days"]: 

624 raise ConfigValidationError( 

625 "backend_tls.leaf_rotate_before_days must be less than leaf_validity_days" 

626 ) 

627 if config["root_validity_days"] <= config["leaf_validity_days"]: 

628 raise ConfigValidationError( 

629 "backend_tls.root_validity_days must exceed leaf_validity_days" 

630 ) 

631 if config["root_overlap_days"] <= config["leaf_validity_days"]: 

632 raise ConfigValidationError( 

633 "backend_tls.root_overlap_days must exceed leaf_validity_days so old leaves " 

634 "remain trusted throughout root rollover" 

635 ) 

636 if config["trust_cache_max_stale_seconds"] < config["trust_cache_ttl_seconds"]: 

637 raise ConfigValidationError( 

638 "backend_tls.trust_cache_max_stale_seconds must be at least trust_cache_ttl_seconds" 

639 ) 

640 if config["root_activation_delay_hours"] * 3_600 <= config["trust_cache_max_stale_seconds"]: 

641 raise ConfigValidationError( 

642 "backend_tls.root_activation_delay_hours must exceed the maximum stale trust " 

643 "cache window so every proxy can observe a pending root before leaf rollover" 

644 ) 

645 

646 def _validate_inference_proxy_config(self) -> None: 

647 """Validate the inference proxy TLS CPU request, HPA target and replica bounds.""" 

648 config = self.get_inference_proxy_config() 

649 ranges = { 

650 "tls_proxy_cpu_request_millicores": (1, 250), 

651 "tls_proxy_cpu_target_utilization_percentage": (1, 100), 

652 "min_replicas": (1, 50), 

653 "max_replicas": (1, 100), 

654 } 

655 for field, (minimum, maximum) in ranges.items(): 

656 value = config[field] 

657 if type(value) is not int or not minimum <= value <= maximum: 

658 raise ConfigValidationError( 

659 f"inference_proxy.{field} must be an integer between " 

660 f"{minimum} and {maximum}, got {value!r}" 

661 ) 

662 if config["max_replicas"] < config["min_replicas"]: 

663 raise ConfigValidationError( 

664 "inference_proxy.max_replicas must be at least inference_proxy.min_replicas, " 

665 f"got {config['max_replicas']} < {config['min_replicas']}" 

666 ) 

667 

668 def _validate_alb_config(self) -> None: 

669 """Validate ALB configuration""" 

670 alb_config = self.app.node.try_get_context("alb_config") 

671 if not alb_config: 

672 raise ConfigValidationError("alb_config configuration is required") 

673 

674 required_fields = [ 

675 "health_check_interval", 

676 "health_check_timeout", 

677 "healthy_threshold", 

678 "unhealthy_threshold", 

679 ] 

680 for field in required_fields: 

681 if field not in alb_config: 

682 raise ConfigValidationError(f"Missing alb_config configuration: {field}") 

683 

684 value = alb_config[field] 

685 if not isinstance(value, int) or value <= 0: 

686 raise ConfigValidationError(f"{field} must be a positive integer, got {value}") 

687 

688 def _validate_manifest_processor_config(self) -> None: 

689 """Validate manifest processor configuration. 

690 

691 The manifest processor section in cdk.json holds service-specific 

692 settings only. The shared validation policy (allowed_namespaces, 

693 resource_quotas, trusted_registries, trusted_dockerhub_orgs, 

694 manifest_security_policy, allowed_kinds) lives under 

695 ``job_validation_policy`` because the queue_processor reads the 

696 same values. 

697 """ 

698 mp_config = self.app.node.try_get_context("manifest_processor") 

699 if not mp_config: 

700 raise ConfigValidationError("manifest_processor configuration is required") 

701 

702 required_fields = [ 

703 "image", 

704 "replicas", 

705 "resource_limits", 

706 ] 

707 for field in required_fields: 

708 if field not in mp_config: 

709 raise ConfigValidationError(f"Missing manifest_processor configuration: {field}") 

710 

711 # Validate replicas 

712 if not isinstance(mp_config["replicas"], int) or mp_config["replicas"] <= 0: 

713 raise ConfigValidationError("manifest_processor replicas must be a positive integer") 

714 

715 try: 

716 validated_request_body_limit( 

717 mp_config.get("max_request_body_bytes", DEFAULT_MAX_REQUEST_BODY_BYTES) 

718 ) 

719 except ValueError as exc: 

720 raise ConfigValidationError(f"manifest_processor.{exc}") from exc 

721 

722 validation_enabled = mp_config.get("validation_enabled", True) 

723 if type(validation_enabled) is not bool: 

724 raise ConfigValidationError("manifest_processor.validation_enabled must be a boolean") 

725 

726 # Validate the shared policy section separately so a misconfigured 

727 # policy block surfaces a clear error pointing at the right key. 

728 policy = self.app.node.try_get_context("job_validation_policy") 

729 if policy is None: 

730 raise ConfigValidationError( 

731 "job_validation_policy configuration is required (shared between " 

732 "manifest_processor and queue_processor)" 

733 ) 

734 if not isinstance(policy, dict): 

735 raise ConfigValidationError("job_validation_policy must be an object") 

736 for policy_field in ("allowed_namespaces", "resource_quotas"): 

737 if policy_field not in policy: 

738 raise ConfigValidationError( 

739 f"Missing job_validation_policy configuration: {policy_field}" 

740 ) 

741 

742 try: 

743 validate_manifest_security_policy(policy.get("manifest_security_policy", {})) 

744 except ValueError as exc: 

745 raise ConfigValidationError(f"job_validation_policy.{exc}") from exc 

746 

747 require_toleration = policy.get("require_accelerator_toleration", True) 

748 if type(require_toleration) is not bool: 

749 raise ConfigValidationError( 

750 "job_validation_policy.require_accelerator_toleration must be a boolean" 

751 ) 

752 

753 # Validate resource limits. They render verbatim into the Deployment's 

754 # container limits, so they must be non-empty Kubernetes quantities no 

755 # smaller than the requests fixed in 31-manifest-processor.yaml — 

756 # Kubernetes rejects limit < request, and catching that here fails the 

757 # synth instead of the applier halfway through a deploy. 

758 resource_limits = mp_config["resource_limits"] 

759 if "cpu" not in resource_limits or "memory" not in resource_limits: 

760 raise ConfigValidationError( 

761 "manifest_processor resource_limits must contain 'cpu' and 'memory'" 

762 ) 

763 for resource, request in _MANIFEST_PROCESSOR_CONTAINER_REQUESTS.items(): 

764 quantity = resource_limits[resource] 

765 try: 

766 if not isinstance(quantity, str): 

767 raise ValueError("must be a string") 

768 parsed = parse_k8s_quantity(quantity) 

769 except ValueError as exc: 

770 raise ConfigValidationError( 

771 f"manifest_processor.resource_limits.{resource} must be a Kubernetes " 

772 f"quantity string such as '1000m' or '2Gi', got {quantity!r} ({exc})" 

773 ) from exc 

774 if parsed < parse_k8s_quantity(request): 

775 raise ConfigValidationError( 

776 f"manifest_processor.resource_limits.{resource} must be at least the " 

777 f"container request of {request!r} " 

778 f"(31-manifest-processor.yaml), got {quantity!r}" 

779 ) 

780 

781 # Validate the optional CPU autoscaler. Off by default: the API tier is 

782 # I/O-bound, so CPU is a weak saturation signal; operators who have 

783 # measured otherwise turn it on here and the HPA takes over the count. 

784 autoscaling = mp_config.get("autoscaling") 

785 if autoscaling is None: 

786 autoscaling = {} 

787 if not isinstance(autoscaling, dict): 

788 raise ConfigValidationError("manifest_processor.autoscaling must be an object") 

789 unknown_autoscaling = sorted( 

790 str(key) for key in autoscaling if key not in _MANIFEST_PROCESSOR_AUTOSCALING_DEFAULTS 

791 ) 

792 if unknown_autoscaling: 

793 raise ConfigValidationError( 

794 "manifest_processor.autoscaling contains unknown key(s): " 

795 + ", ".join(unknown_autoscaling) 

796 + "; allowed keys: " 

797 + ", ".join(sorted(_MANIFEST_PROCESSOR_AUTOSCALING_DEFAULTS)) 

798 ) 

799 merged_autoscaling = {**_MANIFEST_PROCESSOR_AUTOSCALING_DEFAULTS, **autoscaling} 

800 if type(merged_autoscaling["enabled"]) is not bool: 

801 raise ConfigValidationError("manifest_processor.autoscaling.enabled must be a boolean") 

802 for key, minimum, maximum in ( 

803 ("max_replicas", 1, 100), 

804 ("cpu_target_utilization_percentage", 1, 100), 

805 ): 

806 value = merged_autoscaling[key] 

807 if type(value) is not int or not minimum <= value <= maximum: 

808 raise ConfigValidationError( 

809 f"manifest_processor.autoscaling.{key} must be an integer between " 

810 f"{minimum} and {maximum}, got {value!r}" 

811 ) 

812 if merged_autoscaling["max_replicas"] < mp_config["replicas"]: 

813 raise ConfigValidationError( 

814 "manifest_processor.autoscaling.max_replicas must be at least " 

815 f"manifest_processor.replicas, got {merged_autoscaling['max_replicas']} " 

816 f"< {mp_config['replicas']}" 

817 ) 

818 

819 # Validate allowed namespaces (lives under job_validation_policy). 

820 if not isinstance(policy["allowed_namespaces"], list): 

821 raise ConfigValidationError("job_validation_policy.allowed_namespaces must be a list") 

822 

823 def _validate_api_gateway_config(self) -> None: 

824 """Validate API Gateway configuration""" 

825 api_gw_config = self.app.node.try_get_context("api_gateway") 

826 if not api_gw_config: 

827 raise ConfigValidationError("api_gateway configuration is required") 

828 

829 required_fields = [ 

830 "throttle_rate_limit", 

831 "throttle_burst_limit", 

832 "log_level", 

833 "metrics_enabled", 

834 "tracing_enabled", 

835 ] 

836 for field in required_fields: 

837 if field not in api_gw_config: 

838 raise ConfigValidationError(f"Missing api_gateway configuration: {field}") 

839 

840 # Validate throttle limits 

841 throttle_rate = api_gw_config["throttle_rate_limit"] 

842 throttle_burst = api_gw_config["throttle_burst_limit"] 

843 

844 if not isinstance(throttle_rate, int) or throttle_rate <= 0: 

845 raise ConfigValidationError( 

846 f"throttle_rate_limit must be a positive integer, got {throttle_rate}" 

847 ) 

848 

849 if not isinstance(throttle_burst, int) or throttle_burst <= 0: 

850 raise ConfigValidationError( 

851 f"throttle_burst_limit must be a positive integer, got {throttle_burst}" 

852 ) 

853 

854 if throttle_burst < throttle_rate: 

855 raise ConfigValidationError( 

856 "throttle_burst_limit should be greater than or equal to throttle_rate_limit" 

857 ) 

858 

859 # Validate log level 

860 valid_log_levels = ["OFF", "ERROR", "INFO"] 

861 log_level = api_gw_config["log_level"] 

862 if log_level not in valid_log_levels: 

863 raise ConfigValidationError( 

864 f"log_level must be one of {valid_log_levels}, got {log_level}" 

865 ) 

866 

867 # Validate boolean flags 

868 if not isinstance(api_gw_config["metrics_enabled"], bool): 

869 raise ConfigValidationError("metrics_enabled must be a boolean") 

870 

871 if not isinstance(api_gw_config["tracing_enabled"], bool): 

872 raise ConfigValidationError("tracing_enabled must be a boolean") 

873 

874 if "regional_api_enabled" in api_gw_config and not isinstance( 

875 api_gw_config["regional_api_enabled"], bool 

876 ): 

877 raise ConfigValidationError("regional_api_enabled must be a boolean") 

878 

879 def _validate_eks_cluster_config(self) -> None: 

880 """Validate EKS cluster configuration""" 

881 eks_config = self.app.node.try_get_context("eks_cluster") or {} 

882 

883 # Validate endpoint_access if present 

884 if "endpoint_access" in eks_config: 

885 valid_access_modes = ["PRIVATE", "PUBLIC_AND_PRIVATE"] 

886 if eks_config["endpoint_access"] not in valid_access_modes: 

887 raise ConfigValidationError( 

888 f"endpoint_access must be one of {valid_access_modes}, " 

889 f"got {eks_config['endpoint_access']}" 

890 ) 

891 # The NetworkPolicy enforcement switch renders straight into a 

892 # kube-system ConfigMap value, so only a literal JSON boolean is 

893 # accepted — a string "false" would be applied verbatim and enable 

894 # nothing while reading as disabled. 

895 if ( 

896 "network_policy_enforcement" in eks_config 

897 and type(eks_config["network_policy_enforcement"]) is not bool 

898 ): 

899 raise ConfigValidationError( 

900 "eks_cluster.network_policy_enforcement must be a boolean, got " 

901 f"{eks_config['network_policy_enforcement']!r}" 

902 ) 

903 self._validate_vpc_endpoints_config() 

904 

905 def _validate_vpc_endpoints_config(self) -> None: 

906 """Validate the optional ``vpc_endpoints`` block. 

907 

908 ``gateway`` lists the free route-table endpoints (``s3``, 

909 ``dynamodb``); ``interface`` lists PrivateLink endpoints by the 

910 service key in :data:`VPC_INTERFACE_ENDPOINT_SERVICES`. Interface 

911 endpoints bill per AZ-hour, so the list is explicit and unknown names 

912 fail at synth instead of silently creating nothing. 

913 """ 

914 raw = self.app.node.try_get_context("vpc_endpoints") 

915 if raw is None: 

916 return 

917 if not isinstance(raw, dict): 

918 raise ConfigValidationError("vpc_endpoints must be an object") 

919 unknown = sorted(str(key) for key in raw if key not in _VPC_ENDPOINTS_DEFAULTS) 

920 if unknown: 

921 raise ConfigValidationError( 

922 "vpc_endpoints contains unknown key(s): " 

923 + ", ".join(unknown) 

924 + "; allowed keys: " 

925 + ", ".join(sorted(_VPC_ENDPOINTS_DEFAULTS)) 

926 ) 

927 for key, allowed in ( 

928 ("gateway", VPC_GATEWAY_ENDPOINT_SERVICES), 

929 ("interface", VPC_INTERFACE_ENDPOINT_SERVICES), 

930 ): 

931 if key not in raw: 

932 continue 

933 value = raw[key] 

934 if not isinstance(value, list) or not all(isinstance(item, str) for item in value): 

935 raise ConfigValidationError(f"vpc_endpoints.{key} must be a list of strings") 

936 unsupported = sorted(set(value) - set(allowed)) 

937 if unsupported: 

938 raise ConfigValidationError( 

939 f"vpc_endpoints.{key} contains unsupported service(s): " 

940 + ", ".join(unsupported) 

941 + "; supported: " 

942 + ", ".join(sorted(allowed)) 

943 ) 

944 if len(set(value)) != len(value): 

945 raise ConfigValidationError(f"vpc_endpoints.{key} lists a service twice") 

946 

947 def _validate_eks_capabilities_config(self) -> None: 

948 """Validate the optional ``eks_capabilities`` block. 

949 

950 Every capability type is off by default, so an absent block is valid. 

951 When present, the shape rules live in 

952 :mod:`gco.eks_capabilities_config`; per-type ``regions`` subsets must 

953 name regional deployment regions and every ACK role or policy entry 

954 must be an ARN of the right shape. The run-scoped 

955 ``eks_capabilities_overrides`` context (a JSON object) is merged in 

956 first, so an override that would produce an invalid block fails the 

957 same way an invalid cdk.json does. 

958 """ 

959 raw = self._raw_eks_capabilities_config() 

960 if raw is None: 

961 return 

962 try: 

963 validate_eks_capabilities_config(raw, self.get_regions()) 

964 except EksCapabilitiesConfigError as exc: 

965 raise ConfigValidationError(str(exc)) from exc 

966 

967 def _raw_eks_capabilities_config(self) -> object: 

968 """The cdk.json ``eks_capabilities`` block with run-scoped overrides merged in.""" 

969 raw = self.app.node.try_get_context(EKS_CAPABILITIES_CONTEXT_KEY) 

970 try: 

971 overrides = parse_eks_capabilities_overrides( 

972 self.app.node.try_get_context(EKS_CAPABILITIES_OVERRIDES_CONTEXT_KEY) 

973 ) 

974 except EksCapabilitiesConfigError as exc: 

975 raise ConfigValidationError(str(exc)) from exc 

976 return merge_eks_capabilities_overrides(raw, overrides) 

977 

978 def _raw_argocd_config(self) -> object: 

979 """The cdk.json ``helm.argocd`` block, or ``None`` when absent.""" 

980 helm = self.app.node.try_get_context("helm") 

981 return helm.get("argocd") if isinstance(helm, dict) else None 

982 

983 def _validate_argocd_config(self) -> None: 

984 """Validate the optional self-managed Argo CD block (``helm.argocd``). 

985 

986 Argo CD is off by default, so an absent block is valid. When present, 

987 the shape rules live in :mod:`gco.argocd_config`: unknown keys fail by 

988 name, the GitOps repository must be a Git URL admitted by 

989 ``source_repos``, and the sync policy is ``manual`` or ``automated``. 

990 """ 

991 try: 

992 validate_argocd_config(self._raw_argocd_config()) 

993 except ArgoCdConfigError as exc: 

994 raise ConfigValidationError(str(exc)) from exc 

995 

996 def get_argocd_config(self) -> dict[str, Any]: 

997 """Return the ``helm.argocd`` block with defaults merged in. 

998 

999 Keys: ``enabled``, ``source_repos``, ``gitops`` ``{repo_url, revision, 

1000 path, sync_policy}`` (see :data:`gco.argocd_config.ARGOCD_DEFAULTS`). 

1001 Whether the chart actually installs also honors the run-scoped 

1002 ``helm_enabled_overrides`` context, which the regional stack resolves. 

1003 """ 

1004 try: 

1005 return validate_argocd_config(self._raw_argocd_config()) 

1006 except ArgoCdConfigError as exc: 

1007 raise ConfigValidationError(str(exc)) from exc 

1008 

1009 def _raw_slurm_config(self) -> object: 

1010 """The cdk.json ``helm.slurm`` block, or ``None`` when absent.""" 

1011 helm = self.app.node.try_get_context("helm") 

1012 return helm.get("slurm") if isinstance(helm, dict) else None 

1013 

1014 def _slurm_fsx_available_everywhere(self) -> bool: 

1015 """True when every regional cluster gets an FSx for Lustre filesystem. 

1016 

1017 ``helm.slurm.storage.scratch: fsx`` asks for the FSx claim on every 

1018 Slurm worker and login pod, and FSx is a per-region toggle, so the 

1019 strict setting is only valid when each region has it. (``auto`` 

1020 resolves per region in the regional stack and never needs this.) 

1021 

1022 Consulted only when the block actually says ``scratch: fsx``: the FSx 

1023 lookup validates the run-scoped ``feature_enabled_overrides`` as a 

1024 side effect, and a Slurm block that never asked for FSx must not turn 

1025 an unrelated override typo into a construction-time Slurm error. 

1026 """ 

1027 raw = self._raw_slurm_config() 

1028 storage = raw.get("storage") if isinstance(raw, dict) else None 

1029 if not isinstance(storage, dict) or storage.get("scratch") != "fsx": 

1030 return False 

1031 regions = self.get_regions() 

1032 return bool(regions) and all( 

1033 bool(self.get_fsx_lustre_config(region)["enabled"]) for region in regions 

1034 ) 

1035 

1036 def _validate_slurm_config(self) -> None: 

1037 """Validate the optional Slurm cluster block (``helm.slurm``). 

1038 

1039 Slurm is off by default, so an absent block is valid. When present, 

1040 the shape rules live in :mod:`gco.slurm_config`: unknown keys fail by 

1041 name, every partition must target a known node pool with a coherent 

1042 accelerator request and fit the ``gco-jobs`` resource quota, exactly 

1043 one enabled partition is the default, autoscaled partitions need 

1044 ``cluster_observability`` (their metrics come through Prometheus), and 

1045 ``storage.scratch: fsx`` needs FSx in every region. 

1046 """ 

1047 try: 

1048 validate_slurm_config( 

1049 self._raw_slurm_config(), 

1050 resource_quota=self.app.node.try_get_context("resource_quota") or {}, 

1051 observability_enabled=self.get_cluster_observability_enabled(), 

1052 fsx_enabled=self._slurm_fsx_available_everywhere(), 

1053 ) 

1054 except SlurmConfigError as exc: 

1055 raise ConfigValidationError(str(exc)) from exc 

1056 

1057 def get_slurm_config(self) -> dict[str, Any]: 

1058 """Return the ``helm.slurm`` block with defaults merged in. 

1059 

1060 Keys: ``enabled``, ``cluster_name``, ``controller``, ``restapi``, 

1061 ``accounting``, ``metrics``, ``login``, ``autoscaling``, 

1062 ``partitions``, ``storage``, ``slurm_conf``, ``slurmdbd_conf`` (see 

1063 :data:`gco.slurm_config.SLURM_DEFAULTS`). Whether the charts actually 

1064 install also honors the run-scoped ``helm_enabled_overrides`` context, 

1065 which the regional stack resolves. 

1066 """ 

1067 try: 

1068 return validate_slurm_config( 

1069 self._raw_slurm_config(), 

1070 resource_quota=self.app.node.try_get_context("resource_quota") or {}, 

1071 observability_enabled=self.get_cluster_observability_enabled(), 

1072 fsx_enabled=self._slurm_fsx_available_everywhere(), 

1073 ) 

1074 except SlurmConfigError as exc: 

1075 raise ConfigValidationError(str(exc)) from exc 

1076 

1077 def get_eks_capabilities_config(self) -> dict[str, Any]: 

1078 """Return the ``eks_capabilities`` block with defaults merged in. 

1079 

1080 Keys: ``ack`` / ``kro``, each ``{enabled, regions, ...}`` 

1081 (see :data:`gco.eks_capabilities_config.EKS_CAPABILITIES_DEFAULTS`). 

1082 Both are disabled by default; ``regions: []`` means every 

1083 regional deployment region. Honors the run-scoped 

1084 ``eks_capabilities_overrides`` context like the validator does. 

1085 """ 

1086 try: 

1087 return validate_eks_capabilities_config(self._raw_eks_capabilities_config(), None) 

1088 except EksCapabilitiesConfigError as exc: 

1089 raise ConfigValidationError(str(exc)) from exc 

1090 

1091 def get_vpc_endpoints_config(self) -> dict[str, list[str]]: 

1092 """Return the VPC endpoint selection with defaults merged in. 

1093 

1094 Defaults: the two free gateway endpoints (S3 carries the platform's 

1095 largest data path — models, datasets, checkpoints, MLflow artifacts, 

1096 cost reports — off the NAT gateways' per-GB metering; DynamoDB serves 

1097 single-region topologies) and no interface endpoints. 

1098 """ 

1099 configured = self.app.node.try_get_context("vpc_endpoints") or {} 

1100 return { 

1101 "gateway": list(configured.get("gateway", _VPC_ENDPOINTS_DEFAULTS["gateway"])), 

1102 "interface": list(configured.get("interface", _VPC_ENDPOINTS_DEFAULTS["interface"])), 

1103 } 

1104 

1105 def _validate_analytics_environment_config(self) -> None: 

1106 """Validate the optional analytics_environment block in cdk.json. 

1107 

1108 The block is entirely optional; absence means the feature is disabled 

1109 and no validation is needed. When present, we validate: 

1110 

1111 - ``enabled``: must be a bool if present (defaults to False via merge). 

1112 - ``hyperpod.enabled``: must be a bool if present (defaults to False). 

1113 - ``cognito.removal_policy`` and ``efs.removal_policy``: must be the 

1114 literal strings ``"destroy"`` or ``"retain"`` (case sensitive — they 

1115 are passed verbatim to CDK's ``RemovalPolicy`` lookup by the 

1116 consumer). 

1117 """ 

1118 analytics_ctx = self.app.node.try_get_context("analytics_environment") 

1119 if not isinstance(analytics_ctx, dict): 

1120 # Block is absent or malformed — defaults apply, nothing to validate. 

1121 return 

1122 

1123 # Top-level `enabled` must be a bool if provided. 

1124 if "enabled" in analytics_ctx and not isinstance(analytics_ctx["enabled"], bool): 

1125 raise ConfigValidationError( 

1126 f"analytics_environment.enabled must be a bool, got " 

1127 f"{type(analytics_ctx['enabled']).__name__}: {analytics_ctx['enabled']!r}" 

1128 ) 

1129 

1130 # `hyperpod.enabled` must be a bool if the sub-block is a dict and 

1131 # carries the key. 

1132 hyperpod_ctx = analytics_ctx.get("hyperpod") 

1133 if ( 

1134 isinstance(hyperpod_ctx, dict) 

1135 and "enabled" in hyperpod_ctx 

1136 and not isinstance(hyperpod_ctx["enabled"], bool) 

1137 ): 

1138 raise ConfigValidationError( 

1139 f"analytics_environment.hyperpod.enabled must be a bool, got " 

1140 f"{type(hyperpod_ctx['enabled']).__name__}: {hyperpod_ctx['enabled']!r}" 

1141 ) 

1142 

1143 # `canvas.enabled` must be a bool if the sub-block is a dict and 

1144 # carries the key. Mirrors the hyperpod validation above. 

1145 canvas_ctx = analytics_ctx.get("canvas") 

1146 if ( 

1147 isinstance(canvas_ctx, dict) 

1148 and "enabled" in canvas_ctx 

1149 and not isinstance(canvas_ctx["enabled"], bool) 

1150 ): 

1151 raise ConfigValidationError( 

1152 f"analytics_environment.canvas.enabled must be a bool, got " 

1153 f"{type(canvas_ctx['enabled']).__name__}: {canvas_ctx['enabled']!r}" 

1154 ) 

1155 

1156 valid_removal_policies = {"destroy", "retain"} 

1157 

1158 for sub_block in ("cognito", "efs"): 

1159 sub_ctx = analytics_ctx.get(sub_block) 

1160 if not isinstance(sub_ctx, dict): 

1161 continue 

1162 if "removal_policy" not in sub_ctx: 

1163 continue 

1164 removal_policy = sub_ctx["removal_policy"] 

1165 if removal_policy not in valid_removal_policies: 

1166 raise ConfigValidationError( 

1167 f"analytics_environment.{sub_block}.removal_policy must be one of " 

1168 f"{sorted(valid_removal_policies)}, got {removal_policy!r}" 

1169 ) 

1170 

1171 def _validate_cluster_observability_config(self) -> None: 

1172 """Validate the optional cluster_observability block in cdk.json. 

1173 

1174 The block is entirely optional; absence means the on-by-default 

1175 defaults apply and nothing needs validating. When present, we check: 

1176 

1177 - ``enabled``: must be a bool if present (defaults to True via merge — 

1178 in-cluster observability is on unless explicitly disabled). 

1179 - ``grafana``/``prometheus``/``alertmanager`` sub-block ``persistence_size`` 

1180 and ``prometheus.retention``: must be non-empty strings if present 

1181 (they are passed verbatim to Helm chart values as Kubernetes 

1182 quantity / duration strings). 

1183 - ``alertmanager.enabled``: must be a bool if present. 

1184 """ 

1185 obs_ctx = self.app.node.try_get_context("cluster_observability") 

1186 if not isinstance(obs_ctx, dict): 

1187 # Block is absent or malformed — defaults apply, nothing to validate. 

1188 return 

1189 

1190 if "enabled" in obs_ctx and not isinstance(obs_ctx["enabled"], bool): 

1191 raise ConfigValidationError( 

1192 f"cluster_observability.enabled must be a bool, got " 

1193 f"{type(obs_ctx['enabled']).__name__}: {obs_ctx['enabled']!r}" 

1194 ) 

1195 

1196 # Non-empty-string checks for the size / retention knobs. 

1197 string_fields = ( 

1198 ("grafana", "persistence_size"), 

1199 ("prometheus", "persistence_size"), 

1200 ("prometheus", "retention"), 

1201 ("alertmanager", "persistence_size"), 

1202 ) 

1203 for sub_block, field in string_fields: 

1204 sub_ctx = obs_ctx.get(sub_block) 

1205 if not isinstance(sub_ctx, dict) or field not in sub_ctx: 

1206 continue 

1207 value = sub_ctx[field] 

1208 if not isinstance(value, str) or not value.strip(): 

1209 raise ConfigValidationError( 

1210 f"cluster_observability.{sub_block}.{field} must be a non-empty " 

1211 f"string, got {value!r}" 

1212 ) 

1213 

1214 # `grafana.admin_password_rotation_schedule`: the cron for the in-cluster 

1215 # CronJob that rotates the Grafana admin password. Validate the 5-field 

1216 # cron shape so a typo fails at synth rather than yielding an 

1217 # un-schedulable CronJob in every region. 

1218 grafana_ctx = obs_ctx.get("grafana") 

1219 if isinstance(grafana_ctx, dict) and "admin_password_rotation_schedule" in grafana_ctx: 

1220 schedule = grafana_ctx["admin_password_rotation_schedule"] 

1221 if not isinstance(schedule, str) or len(schedule.split()) != 5: 

1222 raise ConfigValidationError( 

1223 "cluster_observability.grafana.admin_password_rotation_schedule must be " 

1224 f"a 5-field cron expression string, got {schedule!r}" 

1225 ) 

1226 

1227 # `alertmanager.enabled` must be a bool if the sub-block carries it. 

1228 alertmanager_ctx = obs_ctx.get("alertmanager") 

1229 if ( 

1230 isinstance(alertmanager_ctx, dict) 

1231 and "enabled" in alertmanager_ctx 

1232 and not isinstance(alertmanager_ctx["enabled"], bool) 

1233 ): 

1234 raise ConfigValidationError( 

1235 f"cluster_observability.alertmanager.enabled must be a bool, got " 

1236 f"{type(alertmanager_ctx['enabled']).__name__}: " 

1237 f"{alertmanager_ctx['enabled']!r}" 

1238 ) 

1239 

1240 def _validate_cost_monitoring_config(self) -> None: 

1241 """Validate the optional ``cost_monitoring`` block in cdk.json. 

1242 

1243 The block is entirely optional; absence means the on-by-default 

1244 defaults apply and nothing needs validating. When present, we check: 

1245 

1246 - ``enabled``: must be a bool if present (defaults to True via merge). 

1247 - ``reports.interval_minutes``: positive int between 5 and 1440 if 

1248 present (the cost-monitor service's scheduled report cadence). 

1249 - ``reports.retention_days`` / 

1250 ``reports.transition_to_infrequent_access_days`` / 

1251 ``athena.query_results_retention_days``: positive ints if present. 

1252 - The IA transition must happen strictly before expiration, otherwise 

1253 the S3 lifecycle configuration is rejected at deploy time — fail at 

1254 synth instead. 

1255 

1256 There is deliberately no cross-toggle error against 

1257 ``cluster_observability``: cost monitoring's *effective* enablement is 

1258 the conjunction of both toggles (see 

1259 :meth:`get_cost_monitoring_enabled`), so disabling observability 

1260 simply switches the cost pipeline off with it — ``gco monitoring 

1261 disable`` must not break synthesis. 

1262 """ 

1263 cost_ctx = self.app.node.try_get_context("cost_monitoring") 

1264 if not isinstance(cost_ctx, dict): 

1265 # Block absent or malformed — defaults apply, nothing to validate. 

1266 return 

1267 

1268 if "enabled" in cost_ctx and not isinstance(cost_ctx["enabled"], bool): 

1269 raise ConfigValidationError( 

1270 f"cost_monitoring.enabled must be a bool, got " 

1271 f"{type(cost_ctx['enabled']).__name__}: {cost_ctx['enabled']!r}" 

1272 ) 

1273 

1274 int_fields = ( 

1275 ("reports", "interval_minutes", 5, 1_440), 

1276 ("reports", "retention_days", 1, 3_650), 

1277 ("reports", "transition_to_infrequent_access_days", 30, 3_650), 

1278 ("athena", "query_results_retention_days", 1, 3_650), 

1279 ) 

1280 for sub_block, field, minimum, maximum in int_fields: 

1281 sub_ctx = cost_ctx.get(sub_block) 

1282 if not isinstance(sub_ctx, dict) or field not in sub_ctx: 

1283 continue 

1284 value = sub_ctx[field] 

1285 if ( 

1286 not isinstance(value, int) 

1287 or isinstance(value, bool) 

1288 or not minimum <= value <= maximum 

1289 ): 

1290 raise ConfigValidationError( 

1291 f"cost_monitoring.{sub_block}.{field} must be an integer between " 

1292 f"{minimum} and {maximum}, got {value!r}" 

1293 ) 

1294 

1295 merged = self.get_cost_monitoring_config() 

1296 reports = merged["reports"] 

1297 if reports["transition_to_infrequent_access_days"] >= reports["retention_days"]: 

1298 raise ConfigValidationError( 

1299 "cost_monitoring.reports.transition_to_infrequent_access_days " 

1300 f"({reports['transition_to_infrequent_access_days']}) must be smaller than " 

1301 f"cost_monitoring.reports.retention_days ({reports['retention_days']}); " 

1302 "S3 rejects lifecycle rules that transition on or after expiration." 

1303 ) 

1304 

1305 def _validate_capacity_history_config(self) -> None: 

1306 """Validate the optional ``historical`` block in cdk.json. 

1307 

1308 The block is entirely optional; absence means the historical capacity 

1309 surface is disabled and no validation is needed. When present, types 

1310 are validated so a typo fails fast at synth time: 

1311 

1312 - ``enabled``: bool if present. 

1313 - ``retention_days`` / ``poll_interval_minutes``: positive ints if present. 

1314 - ``watch_instance_types`` / ``enabled_regions``: lists of strings if present. 

1315 - every region in ``enabled_regions`` must be a known AWS region. 

1316 - ``spot_score_target_capacities``: non-empty list of positive 

1317 integers (booleans rejected), every value a member of the supported 

1318 set exported by ``cli/capacity/history.py`` — metric fields are 

1319 statically named, so an unsupported capacity has nowhere to land. 

1320 """ 

1321 historical_ctx = self.app.node.try_get_context("historical") 

1322 if not isinstance(historical_ctx, dict): 

1323 return 

1324 

1325 if "enabled" in historical_ctx and not isinstance(historical_ctx["enabled"], bool): 

1326 raise ConfigValidationError( 

1327 f"historical.enabled must be a bool, got " 

1328 f"{type(historical_ctx['enabled']).__name__}: {historical_ctx['enabled']!r}" 

1329 ) 

1330 

1331 for int_field in ( 

1332 "retention_days", 

1333 "poll_interval_minutes", 

1334 "capacity_block_duration_hours", 

1335 ): 

1336 if int_field not in historical_ctx: 

1337 continue 

1338 value = historical_ctx[int_field] 

1339 if not isinstance(value, int) or isinstance(value, bool) or value <= 0: 

1340 raise ConfigValidationError( 

1341 f"historical.{int_field} must be a positive integer, got {value!r}" 

1342 ) 

1343 

1344 # The long-block probe duration may be 0 to disable the long probe 

1345 # entirely, so it is validated as non-negative rather than positive. 

1346 if "capacity_block_long_duration_hours" in historical_ctx: 

1347 long_value = historical_ctx["capacity_block_long_duration_hours"] 

1348 if not isinstance(long_value, int) or isinstance(long_value, bool) or long_value < 0: 

1349 raise ConfigValidationError( 

1350 "historical.capacity_block_long_duration_hours must be a non-negative " 

1351 f"integer (0 disables the long probe), got {long_value!r}" 

1352 ) 

1353 

1354 for list_field in ("watch_instance_types", "enabled_regions"): 

1355 if list_field not in historical_ctx: 

1356 continue 

1357 value = historical_ctx[list_field] 

1358 if not isinstance(value, list) or not all(isinstance(v, str) for v in value): 

1359 raise ConfigValidationError( 

1360 f"historical.{list_field} must be a list of strings, got {value!r}" 

1361 ) 

1362 

1363 for region in historical_ctx.get("enabled_regions", []) or []: 

1364 if region not in self.VALID_REGIONS: 

1365 raise ConfigValidationError( 

1366 f"historical.enabled_regions contains invalid region '{region}'. " 

1367 f"Valid regions: {sorted(self.VALID_REGIONS)}" 

1368 ) 

1369 

1370 if "spot_score_target_capacities" in historical_ctx: 

1371 # Function-local import: cli/capacity/history.py owns the supported 

1372 # set and the capacity->field naming rule, and imports nothing from 

1373 # gco, so validation and storage cannot drift apart. 

1374 from cli.capacity.history import SUPPORTED_SPOT_SCORE_TARGET_CAPACITIES 

1375 

1376 capacities = historical_ctx["spot_score_target_capacities"] 

1377 if ( 

1378 not isinstance(capacities, list) 

1379 or not capacities 

1380 or not all( 

1381 isinstance(value, int) and not isinstance(value, bool) and value > 0 

1382 for value in capacities 

1383 ) 

1384 ): 

1385 raise ConfigValidationError( 

1386 "historical.spot_score_target_capacities must be a non-empty list " 

1387 f"of positive integers, got {capacities!r}" 

1388 ) 

1389 for value in capacities: 

1390 if value not in SUPPORTED_SPOT_SCORE_TARGET_CAPACITIES: 

1391 raise ConfigValidationError( 

1392 "historical.spot_score_target_capacities contains unsupported " 

1393 f"target capacity {value!r}. Supported target capacities: " 

1394 f"{list(SUPPORTED_SPOT_SCORE_TARGET_CAPACITIES)} (each needs a " 

1395 "statically declared metric field; see cli/capacity/history.py)" 

1396 ) 

1397 

1398 #: Distance functions the DynamoDB vector-index API accepts. The choice is 

1399 #: immutable after index creation, so a typo must fail at synth time. 

1400 MISSION_MEMORY_DISTANCE_FUNCTIONS = frozenset({"COSINE", "DOT_PRODUCT", "EUCLIDEAN"}) 

1401 

1402 #: DynamoDB vector-index maximum dimensionality (service quota). 

1403 MISSION_MEMORY_MAX_DIMENSIONS = 4096 

1404 

1405 def _validate_mission_memory_config(self) -> None: 

1406 """Validate the ``mission_memory`` block in cdk.json. 

1407 

1408 The block is optional; absence means the shipped defaults apply 

1409 (feature on). When present, types are validated so a typo fails fast 

1410 at synth time — especially the one-way-door fields (``dimensions``, 

1411 ``distance_function``) that cannot be corrected after the vector 

1412 index exists: 

1413 

1414 - ``enabled``: bool if present. 

1415 - ``retention_days`` / ``top_k``: positive ints if present. 

1416 - ``dimensions``: positive int <= 4096 if present. 

1417 - ``distance_function``: one of COSINE / DOT_PRODUCT / EUCLIDEAN. 

1418 """ 

1419 mission_memory_ctx = self.app.node.try_get_context("mission_memory") 

1420 if not isinstance(mission_memory_ctx, dict): 

1421 return 

1422 

1423 if "enabled" in mission_memory_ctx and not isinstance(mission_memory_ctx["enabled"], bool): 

1424 raise ConfigValidationError( 

1425 f"mission_memory.enabled must be a bool, got " 

1426 f"{type(mission_memory_ctx['enabled']).__name__}: " 

1427 f"{mission_memory_ctx['enabled']!r}" 

1428 ) 

1429 

1430 for int_field in ("retention_days", "top_k"): 

1431 if int_field not in mission_memory_ctx: 

1432 continue 

1433 value = mission_memory_ctx[int_field] 

1434 if not isinstance(value, int) or isinstance(value, bool) or value <= 0: 

1435 raise ConfigValidationError( 

1436 f"mission_memory.{int_field} must be a positive integer, got {value!r}" 

1437 ) 

1438 

1439 if "dimensions" in mission_memory_ctx: 

1440 dimensions = mission_memory_ctx["dimensions"] 

1441 if ( 

1442 not isinstance(dimensions, int) 

1443 or isinstance(dimensions, bool) 

1444 or dimensions <= 0 

1445 or dimensions > self.MISSION_MEMORY_MAX_DIMENSIONS 

1446 ): 

1447 raise ConfigValidationError( 

1448 "mission_memory.dimensions must be a positive integer <= " 

1449 f"{self.MISSION_MEMORY_MAX_DIMENSIONS} (DynamoDB vector-index " 

1450 f"limit), got {dimensions!r}. This is a one-way door: it is " 

1451 "immutable after index creation and must match the " 

1452 "bedrock.embedding_model_id output width." 

1453 ) 

1454 

1455 if "distance_function" in mission_memory_ctx: 

1456 distance = mission_memory_ctx["distance_function"] 

1457 if ( 

1458 not isinstance(distance, str) 

1459 or distance not in self.MISSION_MEMORY_DISTANCE_FUNCTIONS 

1460 ): 

1461 valid = ", ".join(sorted(self.MISSION_MEMORY_DISTANCE_FUNCTIONS)) 

1462 raise ConfigValidationError( 

1463 f"mission_memory.distance_function must be one of {valid}, got " 

1464 f"{distance!r}. This is immutable after index creation." 

1465 ) 

1466 

1467 def _validate_vector_store_config(self) -> None: 

1468 """Validate the optional ``vector_store`` block in cdk.json. 

1469 

1470 The block is optional; absence means the feature stays off. When 

1471 present, types are validated so a typo fails fast at synth time — 

1472 especially the one-way-door fields (``dimensions``, 

1473 ``distance_function``) that cannot be corrected after the vector 

1474 index exists. The distance-function set and dimension ceiling reuse 

1475 the mission-memory constants because both features target the same 

1476 DynamoDB vector-index API limits: 

1477 

1478 - ``enabled``: bool if present. 

1479 - ``dimensions``: positive int <= 4096 if present. 

1480 - ``distance_function``: one of COSINE / DOT_PRODUCT / EUCLIDEAN. 

1481 - ``embedding_model_id``: non-empty string. Deliberately independent 

1482 of ``bedrock.embedding_model_id`` (mission memory) — the two 

1483 corpora may use different models. 

1484 - ``replica_regions``: list of known regions, no duplicates, and 

1485 never the global region (that is the table's primary). 

1486 - ``corpus_prefix``: non-empty S3 key prefix ending in ``/``. 

1487 """ 

1488 vector_store_ctx = self.app.node.try_get_context("vector_store") 

1489 if not isinstance(vector_store_ctx, dict): 

1490 return 

1491 

1492 if "enabled" in vector_store_ctx and not isinstance(vector_store_ctx["enabled"], bool): 

1493 raise ConfigValidationError( 

1494 f"vector_store.enabled must be a bool, got " 

1495 f"{type(vector_store_ctx['enabled']).__name__}: " 

1496 f"{vector_store_ctx['enabled']!r}" 

1497 ) 

1498 if "dimensions" in vector_store_ctx: 

1499 dimensions = vector_store_ctx["dimensions"] 

1500 if ( 

1501 not isinstance(dimensions, int) 

1502 or isinstance(dimensions, bool) 

1503 or dimensions <= 0 

1504 or dimensions > self.MISSION_MEMORY_MAX_DIMENSIONS 

1505 ): 

1506 raise ConfigValidationError( 

1507 "vector_store.dimensions must be a positive integer <= " 

1508 f"{self.MISSION_MEMORY_MAX_DIMENSIONS} (DynamoDB vector-index " 

1509 f"limit), got {dimensions!r}. This is a one-way door: it is " 

1510 "immutable after index creation and must match the " 

1511 "vector_store.embedding_model_id output width." 

1512 ) 

1513 if "distance_function" in vector_store_ctx: 

1514 distance = vector_store_ctx["distance_function"] 

1515 if ( 

1516 not isinstance(distance, str) 

1517 or distance not in self.MISSION_MEMORY_DISTANCE_FUNCTIONS 

1518 ): 

1519 valid = ", ".join(sorted(self.MISSION_MEMORY_DISTANCE_FUNCTIONS)) 

1520 raise ConfigValidationError( 

1521 f"vector_store.distance_function must be one of {valid}, got " 

1522 f"{distance!r}. This is immutable after index creation." 

1523 ) 

1524 if "embedding_model_id" in vector_store_ctx: 

1525 model_id = vector_store_ctx["embedding_model_id"] 

1526 if not isinstance(model_id, str) or not model_id.strip(): 

1527 raise ConfigValidationError( 

1528 f"vector_store.embedding_model_id must be a non-empty string, got {model_id!r}" 

1529 ) 

1530 if "replica_regions" in vector_store_ctx: 

1531 replica_regions = vector_store_ctx["replica_regions"] 

1532 if not isinstance(replica_regions, list) or not all( 

1533 isinstance(region, str) for region in replica_regions 

1534 ): 

1535 raise ConfigValidationError( 

1536 f"vector_store.replica_regions must be a list of region strings, " 

1537 f"got {replica_regions!r}" 

1538 ) 

1539 if len(replica_regions) != len(set(replica_regions)): 

1540 raise ConfigValidationError( 

1541 f"vector_store.replica_regions contains duplicates: {replica_regions!r}" 

1542 ) 

1543 global_region = self.get_global_region() 

1544 for region in replica_regions: 

1545 if region not in self.VALID_REGIONS: 

1546 raise ConfigValidationError( 

1547 f"vector_store.replica_regions contains invalid region " 

1548 f"'{region}'. Valid regions: {sorted(self.VALID_REGIONS)}" 

1549 ) 

1550 if region == global_region: 

1551 raise ConfigValidationError( 

1552 f"vector_store.replica_regions must not include the global " 

1553 f"region '{global_region}': the primary table already lives " 

1554 "there and a global table cannot replicate into its own region." 

1555 ) 

1556 if "corpus_prefix" in vector_store_ctx: 

1557 corpus_prefix = vector_store_ctx["corpus_prefix"] 

1558 if ( 

1559 not isinstance(corpus_prefix, str) 

1560 or not corpus_prefix.strip() 

1561 or not corpus_prefix.endswith("/") 

1562 or corpus_prefix.startswith("/") 

1563 ): 

1564 raise ConfigValidationError( 

1565 "vector_store.corpus_prefix must be a non-empty S3 key prefix " 

1566 f"ending in '/' (and not starting with '/'), got {corpus_prefix!r}" 

1567 ) 

1568 

1569 def get_project_name(self) -> str: 

1570 """Get project name from configuration""" 

1571 return self.app.node.try_get_context("project_name") or "gco" 

1572 

1573 def get_deployment_regions(self) -> dict[str, Any]: 

1574 """Get deployment regions configuration. 

1575 

1576 Returns a dict with: 

1577 - global: Region for Global Accelerator and SSM parameters (default: us-east-2) 

1578 - api_gateway: Region for API Gateway stack (default: us-east-2) 

1579 - monitoring: Region for Monitoring stack (default: us-east-2) 

1580 - regional: List of regions for EKS clusters (default: ["us-east-1"]). 

1581 An explicitly empty list is valid and means a control-plane-only 

1582 deployment: only the global, API Gateway, and monitoring stacks are 

1583 synthesized until a workload Region is added back. 

1584 

1585 Note: Global Accelerator is a global service but requires a "home" region 

1586 for CloudFormation deployment. us-east-2 is used by default to keep 

1587 global infrastructure separate from workload regions. 

1588 """ 

1589 deployment_regions = self.app.node.try_get_context("deployment_regions") or {} 

1590 

1591 regional = deployment_regions.get("regional", ["us-east-1"]) 

1592 if self.is_control_plane_only(): 

1593 regional = [] 

1594 return { 

1595 "global": deployment_regions.get("global", "us-east-2"), 

1596 "api_gateway": deployment_regions.get("api_gateway", "us-east-2"), 

1597 "monitoring": deployment_regions.get("monitoring", "us-east-2"), 

1598 "regional": regional, 

1599 } 

1600 

1601 def is_control_plane_only(self) -> bool: 

1602 """Return whether this run synthesizes with zero workload Regions. 

1603 

1604 True when ``--context gco:control-plane-only=true`` was passed (see 

1605 :data:`gco.stacks.constants.CONTROL_PLANE_ONLY_CONTEXT_KEY`). The 

1606 configured ``deployment_regions.regional`` list in ``cdk.json`` is 

1607 left untouched; only this synthesis ignores it. 

1608 """ 

1609 return control_plane_only_requested( 

1610 self.app.node.try_get_context(CONTROL_PLANE_ONLY_CONTEXT_KEY) 

1611 ) 

1612 

1613 def get_deployment_partition(self) -> str: 

1614 """Return the one SDK partition shared by every configured Region.""" 

1615 deployment_regions = self.get_deployment_regions() 

1616 regional = validated_regional_deployment_regions( 

1617 deployment_regions["regional"], 

1618 known_regions=self.VALID_REGIONS, 

1619 ) 

1620 return validated_deployment_partition( 

1621 ( 

1622 deployment_regions["global"], 

1623 deployment_regions["api_gateway"], 

1624 deployment_regions["monitoring"], 

1625 *regional, 

1626 ) 

1627 ) 

1628 

1629 def supports_global_accelerator(self) -> bool: 

1630 """Return whether this partition exposes the Global Accelerator topology.""" 

1631 return self.get_deployment_partition() == "aws" 

1632 

1633 def get_global_region(self) -> str: 

1634 """Get the region for global resources and shared SSM parameters.""" 

1635 region = self.get_deployment_regions()["global"] 

1636 return str(region) 

1637 

1638 def get_api_gateway_region(self) -> str: 

1639 """Get the region for API Gateway stack.""" 

1640 region = self.get_deployment_regions()["api_gateway"] 

1641 return str(region) 

1642 

1643 def get_monitoring_region(self) -> str: 

1644 """Get the region for Monitoring stack.""" 

1645 region = self.get_deployment_regions()["monitoring"] 

1646 return str(region) 

1647 

1648 def get_regions(self) -> list[str]: 

1649 """Get list of regions for EKS cluster deployment (empty when scaled to zero).""" 

1650 deployment_regions = self.get_deployment_regions() 

1651 regional = deployment_regions["regional"] 

1652 return list(regional) if isinstance(regional, list) else [str(regional)] 

1653 

1654 def get_kubernetes_version(self) -> str: 

1655 """Get Kubernetes version from configuration""" 

1656 return self.app.node.try_get_context("kubernetes_version") or "1.37" 

1657 

1658 def get_resource_thresholds(self) -> ResourceThresholds: 

1659 """Get resource thresholds configuration""" 

1660 thresholds_config = self.app.node.try_get_context("resource_thresholds") or { 

1661 "cpu_threshold": 80, 

1662 "memory_threshold": 80, 

1663 "gpu_threshold": -1, 

1664 "pending_pods_threshold": 10, 

1665 "pending_requested_cpu_vcpus": 100, 

1666 "pending_requested_memory_gb": 200, 

1667 "pending_requested_gpus": -1, 

1668 } 

1669 return ResourceThresholds( 

1670 cpu_threshold=thresholds_config["cpu_threshold"], 

1671 memory_threshold=thresholds_config["memory_threshold"], 

1672 gpu_threshold=thresholds_config["gpu_threshold"], 

1673 pending_pods_threshold=thresholds_config.get("pending_pods_threshold", 10), 

1674 pending_requested_cpu_vcpus=thresholds_config.get("pending_requested_cpu_vcpus", 100), 

1675 pending_requested_memory_gb=thresholds_config.get("pending_requested_memory_gb", 200), 

1676 pending_requested_gpus=thresholds_config.get("pending_requested_gpus", 8), 

1677 ) 

1678 

1679 def get_cluster_config(self, region: str) -> ClusterConfig: 

1680 """Get complete cluster configuration for a region""" 

1681 return ClusterConfig( 

1682 region=region, 

1683 cluster_name=f"{self.get_project_name()}-{region}", 

1684 kubernetes_version=self.get_kubernetes_version(), 

1685 addons=["metrics-server"], 

1686 resource_thresholds=self.get_resource_thresholds(), 

1687 ) 

1688 

1689 def get_global_accelerator_config(self) -> dict[str, Any]: 

1690 """Get the merged Global Accelerator configuration. 

1691 

1692 Returns the ``global_accelerator`` block from cdk.json layered on top 

1693 of the defaults below. This is a real merge, not the historical 

1694 all-or-nothing fallback: a partial block keeps every unspecified 

1695 default instead of silently dropping it. The ``traffic_dial`` 

1696 sub-block is deep-merged so overriding a single dial knob does not 

1697 wipe the sub-block's other defaults, mirroring 

1698 ``get_cost_monitoring_config``. 

1699 

1700 Keys: 

1701 - name: accelerator name (default ``<project_name>-accelerator``) 

1702 - health_check_interval: seconds between endpoint-group probes; 

1703 the Global Accelerator API accepts only 10 or 30 (default 30) 

1704 - health_check_threshold: consecutive probes before an endpoint 

1705 flips healthy/unhealthy, 1-10 (default 3) 

1706 - health_check_path: HTTPS path probed on each regional ALB 

1707 (default ``/api/v1/health``) 

1708 - client_affinity: ``NONE`` or ``SOURCE_IP`` (default ``NONE``) 

1709 - traffic_dial: capacity-driven traffic-dial controller 

1710 sub-block (default disabled, ``monitor`` mode; knob reference 

1711 in :meth:`_validate_traffic_dial_config`) 

1712 """ 

1713 default_config: dict[str, Any] = { 

1714 "name": f"{self.get_project_name()}-accelerator", 

1715 "health_check_interval": 30, 

1716 "health_check_threshold": 3, 

1717 "health_check_path": "/api/v1/health", 

1718 "client_affinity": "NONE", 

1719 "traffic_dial": { 

1720 "enabled": False, 

1721 "mode": "monitor", 

1722 "interval_minutes": 5, 

1723 "lookback_minutes": 15, 

1724 "min_dial_percentage": 10, 

1725 "max_step_percentage": 20, 

1726 "full_health_percentage": 95, 

1727 }, 

1728 } 

1729 configured = self.app.node.try_get_context("global_accelerator") or {} 

1730 if not isinstance(configured, dict): 

1731 raise ConfigValidationError("global_accelerator must be a mapping") 

1732 merged: dict[str, Any] = {**default_config, **configured} 

1733 

1734 # Deep-merge the nested sub-block so a partial override does not drop 

1735 # the other defaults in the same sub-block. A non-mapping override is 

1736 # deliberately left in place for the validator to reject with a 

1737 # precise message. 

1738 override = configured.get("traffic_dial") 

1739 if isinstance(override, dict): 

1740 default_sub = cast(dict[str, Any], default_config["traffic_dial"]) 

1741 merged["traffic_dial"] = {**default_sub, **override} 

1742 

1743 return merged 

1744 

1745 def get_backend_tls_config(self) -> dict[str, Any]: 

1746 """Return the mandatory deployment-local backend TLS lifecycle policy.""" 

1747 defaults = { 

1748 "root_generation": 1, 

1749 "root_validity_days": 3_650, 

1750 "root_rotate_before_days": 180, 

1751 "root_activation_delay_hours": 24, 

1752 "root_overlap_days": 45, 

1753 "leaf_validity_days": 30, 

1754 "leaf_rotate_before_days": 10, 

1755 "rotation_schedule_hours": 12, 

1756 "trust_cache_ttl_seconds": 300, 

1757 "trust_cache_max_stale_seconds": 3_600, 

1758 } 

1759 configured = self.app.node.try_get_context("backend_tls") or {} 

1760 if not isinstance(configured, dict): 

1761 raise ConfigValidationError("backend_tls must be a mapping") 

1762 return {**defaults, **configured} 

1763 

1764 def get_inference_proxy_config(self) -> dict[str, int]: 

1765 """Return merged inference TLS proxy autoscaling settings. 

1766 

1767 Omission is backward compatible and returns the shipped defaults. AWS 

1768 CDK normalizes a top-level JSON ``null`` context value to omission, so 

1769 ``None`` follows the same default-preserving contract. 

1770 """ 

1771 defaults = { 

1772 "tls_proxy_cpu_request_millicores": ( 

1773 INFERENCE_PROXY_TLS_CPU_REQUEST_MILLICORES_DEFAULT 

1774 ), 

1775 "tls_proxy_cpu_target_utilization_percentage": ( 

1776 INFERENCE_PROXY_TLS_CPU_TARGET_UTILIZATION_DEFAULT 

1777 ), 

1778 "min_replicas": INFERENCE_PROXY_MIN_REPLICAS_DEFAULT, 

1779 "max_replicas": INFERENCE_PROXY_MAX_REPLICAS_DEFAULT, 

1780 } 

1781 configured = self.app.node.try_get_context("inference_proxy") 

1782 if configured is None: 

1783 return dict(defaults) 

1784 if not isinstance(configured, dict): 

1785 raise ConfigValidationError( 

1786 "inference_proxy must be an object, got " 

1787 f"{type(configured).__name__}: {configured!r}" 

1788 ) 

1789 

1790 unknown = sorted(str(key) for key in configured if key not in defaults) 

1791 if unknown: 

1792 unknown_paths = ", ".join(f"inference_proxy.{key}" for key in unknown) 

1793 allowed_paths = ", ".join(f"inference_proxy.{key}" for key in sorted(defaults)) 

1794 raise ConfigValidationError( 

1795 f"inference_proxy contains unknown key(s): {unknown_paths}; " 

1796 f"allowed keys: {allowed_paths}" 

1797 ) 

1798 return {**defaults, **configured} 

1799 

1800 def get_alb_config(self) -> dict[str, Any]: 

1801 """Get ALB configuration""" 

1802 return self.app.node.try_get_context("alb_config") or { 

1803 "health_check_interval": 30, 

1804 "health_check_timeout": 5, 

1805 "healthy_threshold": 2, 

1806 "unhealthy_threshold": 2, 

1807 } 

1808 

1809 def get_manifest_processor_config(self) -> dict[str, Any]: 

1810 """Get manifest processor configuration. 

1811 

1812 Merges three cdk.json sections into a single runtime config: 

1813 

1814 - ``manifest_processor``: service-specific settings (replicas, image, 

1815 resource_limits, allowed_namespaces, validation_enabled, 

1816 max_request_body_bytes, yaml_max_depth) 

1817 - ``job_validation_policy``: shared validation policy (resource_quotas, 

1818 trusted_registries, trusted_dockerhub_orgs, manifest_security_policy, 

1819 allowed_kinds). Pulled in verbatim so the REST path reads the same 

1820 policy the SQS queue processor enforces. 

1821 

1822 Note: The 'image' field is a placeholder default. In practice, the actual 

1823 image is built from dockerfiles/Dockerfile.manifest-processor and pushed 

1824 to ECR during CDK deployment. The {{MANIFEST_PROCESSOR_IMAGE}} placeholder 

1825 in manifests is replaced with the ECR image URI. 

1826 """ 

1827 default_config = { 

1828 "image": "gco/manifest-processor:latest", # Placeholder, replaced by ECR image 

1829 "replicas": 3, 

1830 "resource_limits": {"cpu": "1000m", "memory": "2Gi"}, 

1831 "autoscaling": dict(_MANIFEST_PROCESSOR_AUTOSCALING_DEFAULTS), 

1832 "validation_enabled": True, 

1833 "max_request_body_bytes": DEFAULT_MAX_REQUEST_BODY_BYTES, 

1834 "central_queue_worker_enabled": True, 

1835 "central_queue_poll_interval_seconds": 10, 

1836 "central_queue_batch_size": 5, 

1837 "central_queue_reconcile_limit": 100, 

1838 "central_queue_lease_seconds": 300, 

1839 "central_queue_lease_renewal_seconds": 60, 

1840 # allowed_namespaces, resource_quotas, trusted_registries, 

1841 # trusted_dockerhub_orgs, manifest_security_policy, and 

1842 # allowed_kinds are merged in below from job_validation_policy. 

1843 "allowed_namespaces": ["gco-jobs"], 

1844 "resource_quotas": { 

1845 "max_cpu_per_manifest": "10", 

1846 "max_memory_per_manifest": "32Gi", 

1847 "max_gpu_per_manifest": 4, 

1848 }, 

1849 "trusted_registries": [ 

1850 "docker.io", 

1851 "gcr.io", 

1852 "quay.io", 

1853 "registry.k8s.io", 

1854 "k8s.gcr.io", 

1855 "public.ecr.aws", 

1856 "nvcr.io", 

1857 "gco", 

1858 ], 

1859 "trusted_dockerhub_orgs": [ 

1860 "nvidia", 

1861 "pytorch", 

1862 "rayproject", 

1863 "tensorflow", 

1864 "huggingface", 

1865 "amazon", 

1866 "bitnami", 

1867 ], 

1868 } 

1869 context_config = self.app.node.try_get_context("manifest_processor") or {} 

1870 

1871 # Merge in the shared job_validation_policy section. These keys apply 

1872 # to BOTH the manifest processor and the queue processor; they live 

1873 # in their own top-level cdk.json section so neither service "owns" 

1874 # them. We flatten them into the manifest processor's runtime config 

1875 # so service code keeps its existing attribute layout. 

1876 shared_policy = self.app.node.try_get_context("job_validation_policy") or {} 

1877 merged = {**default_config, **context_config, **shared_policy} 

1878 # Nested block: a partial ``autoscaling`` object (or JSON null) keeps 

1879 # the unspecified defaults instead of replacing the whole mapping. 

1880 # ``_validate_config`` has already rejected anything that is not a 

1881 # mapping or null. 

1882 merged["autoscaling"] = { 

1883 **_MANIFEST_PROCESSOR_AUTOSCALING_DEFAULTS, 

1884 **(context_config.get("autoscaling") or {}), 

1885 } 

1886 

1887 enabled = merged.get("central_queue_worker_enabled") 

1888 if not isinstance(enabled, bool): 

1889 raise ConfigValidationError( 

1890 "manifest_processor.central_queue_worker_enabled must be a boolean" 

1891 ) 

1892 for key, minimum, maximum in ( 

1893 ("central_queue_poll_interval_seconds", 1, 300), 

1894 ("central_queue_batch_size", 1, 20), 

1895 ("central_queue_reconcile_limit", 1, 500), 

1896 ("central_queue_lease_seconds", 30, 3600), 

1897 ("central_queue_lease_renewal_seconds", 1, 300), 

1898 ): 

1899 value = merged.get(key) 

1900 if type(value) is not int or not minimum <= value <= maximum: 

1901 raise ConfigValidationError( 

1902 f"manifest_processor.{key} must be an integer between {minimum} and {maximum}" 

1903 ) 

1904 if ( 

1905 merged["central_queue_lease_renewal_seconds"] * 2 

1906 > merged["central_queue_lease_seconds"] 

1907 ): 

1908 raise ConfigValidationError( 

1909 "manifest_processor.central_queue_lease_renewal_seconds must be no more than " 

1910 "half of central_queue_lease_seconds" 

1911 ) 

1912 return merged 

1913 

1914 def get_api_gateway_config(self) -> dict[str, Any]: 

1915 """Get API Gateway configuration. 

1916 

1917 Returns: 

1918 API Gateway configuration dictionary with the following keys: 

1919 - throttle_rate_limit: Requests per second limit 

1920 - throttle_burst_limit: Burst capacity 

1921 - log_level: CloudWatch logging level (OFF, ERROR, INFO) 

1922 - metrics_enabled: Enable CloudWatch metrics 

1923 - tracing_enabled: Enable X-Ray tracing 

1924 - regional_api_enabled: In the commercial ``aws`` partition, 

1925 permit direct same-account callers to use the always-deployed 

1926 regional API bridges. Other partitions force this access on 

1927 because the bridges are the supported workload ingress without 

1928 Global Accelerator. Centralized aggregation always uses them. 

1929 """ 

1930 default_config = { 

1931 "throttle_rate_limit": 1000, 

1932 "throttle_burst_limit": 2000, 

1933 "log_level": "INFO", 

1934 "metrics_enabled": True, 

1935 "tracing_enabled": True, 

1936 "regional_api_enabled": False, 

1937 } 

1938 return {**default_config, **(self.app.node.try_get_context("api_gateway") or {})} 

1939 

1940 def get_eks_cluster_config(self) -> dict[str, Any]: 

1941 """Get EKS cluster configuration. 

1942 

1943 Returns: 

1944 EKS cluster configuration dictionary with the following keys: 

1945 - endpoint_access: EKS API endpoint access mode 

1946 - "PRIVATE": API server only accessible from within VPC (default, most secure) 

1947 - "PUBLIC_AND_PRIVATE": API server accessible from internet and VPC 

1948 - public_access_cidrs: CIDR allowlist for the public endpoint when 

1949 endpoint_access is PUBLIC_AND_PRIVATE. Empty (the default) means 

1950 0.0.0.0/0, which synthesis calls out with a loud warning. 

1951 - developer_access: list of EKS access entries to synthesize for 

1952 human principals, each ``{principal_arn, scope, namespaces}``. 

1953 scope defaults to "namespace" and namespaces to ["gco-jobs"]; 

1954 scope "cluster" grants AmazonEKSClusterAdminPolicy instead. 

1955 Empty (the default) synthesizes exactly today's entries. 

1956 - network_policy_enforcement: whether the Auto Mode network policy 

1957 controller is switched on (default True). False keeps the 

1958 NetworkPolicy objects but stops enforcing them. 

1959 

1960 Note: 

1961 PRIVATE endpoint is recommended for production. Job submission still works 

1962 via API Gateway → Lambda (in VPC) or SQS queues. For kubectl access with 

1963 PRIVATE endpoint, use `gco cluster tunnel` (SSM), a bastion host, or a VPN — 

1964 and an access entry for your principal either way (`gco stacks access`). 

1965 """ 

1966 default_config: dict[str, Any] = { 

1967 "endpoint_access": "PRIVATE", 

1968 "public_access_cidrs": [], 

1969 "developer_access": [], 

1970 # Switches on the Auto Mode network policy controller 

1971 # (06-network-policy-controller.yaml) so 03-network-policies.yaml 

1972 # is enforced rather than merely stored. 

1973 "network_policy_enforcement": True, 

1974 } 

1975 return {**default_config, **(self.app.node.try_get_context("eks_cluster") or {})} 

1976 

1977 def get_fsx_lustre_config(self, region: str | None = None) -> dict[str, Any]: 

1978 """Get FSx for Lustre configuration. 

1979 

1980 Args: 

1981 region: Optional region to get config for. If provided, checks for 

1982 region-specific overrides first. 

1983 

1984 Returns: 

1985 FSx configuration dictionary with the following keys: 

1986 - enabled: Whether FSx is enabled 

1987 - storage_capacity_gib: Storage capacity in GiB (min 1200) 

1988 - deployment_type: SCRATCH_1, SCRATCH_2, PERSISTENT_1, PERSISTENT_2 

1989 - file_system_type_version: Lustre version (2.12 or 2.15, default: 2.15) 

1990 IMPORTANT: Use 2.15 for kernel 6.x compatibility (AL2023, Bottlerocket) 

1991 - per_unit_storage_throughput: Throughput for PERSISTENT types 

1992 - data_compression_type: LZ4 or NONE 

1993 - import_path: S3 path for data import 

1994 - export_path: S3 path for data export 

1995 - auto_import_policy: NEW, NEW_CHANGED, NEW_CHANGED_DELETED 

1996 - node_group: Node group configuration for FSx workloads 

1997 - instance_types: List of instance types 

1998 - min_size: Minimum nodes (default: 0) 

1999 - max_size: Maximum nodes (default: 10) 

2000 - desired_size: Desired nodes (default: 0, scales from zero) 

2001 - ami_type: AMI type - one of: 

2002 AL2023_X86_64_STANDARD (default), AL2023_ARM_64_STANDARD, 

2003 AL2023_X86_64_NVIDIA, AL2023_ARM_64_NVIDIA, AL2023_X86_64_NEURON 

2004 - capacity_type: ON_DEMAND (default) or SPOT 

2005 - disk_size: Root disk size in GB (default: 100) 

2006 - labels: Additional node labels (dict) 

2007 """ 

2008 default_config = { 

2009 "enabled": False, 

2010 "storage_capacity_gib": 1200, 

2011 "deployment_type": "SCRATCH_2", 

2012 "file_system_type_version": "2.15", # Use 2.15 for kernel 6.x compatibility 

2013 "per_unit_storage_throughput": 200, 

2014 "data_compression_type": "LZ4", 

2015 "import_path": None, 

2016 "export_path": None, 

2017 "auto_import_policy": "NEW_CHANGED_DELETED", 

2018 "node_group": { 

2019 "instance_types": ["m5.large", "m5.xlarge", "m6i.large", "m6i.xlarge"], 

2020 "min_size": 0, 

2021 "max_size": 10, 

2022 "desired_size": 1, 

2023 "ami_type": "AL2023_X86_64_STANDARD", 

2024 "capacity_type": "ON_DEMAND", 

2025 "disk_size": 100, 

2026 "labels": {}, 

2027 }, 

2028 } 

2029 

2030 # Get global FSx config 

2031 global_ctx = self.app.node.try_get_context("fsx_lustre") 

2032 global_config: dict[str, Any] = global_ctx if isinstance(global_ctx, dict) else {} 

2033 merged_config: dict[str, Any] = {**default_config, **global_config} 

2034 

2035 # Ensure node_group has all required fields with defaults 

2036 if "node_group" in global_config: 

2037 global_node_group = global_config["node_group"] 

2038 if isinstance(global_node_group, dict): 

2039 default_node_group = cast(dict[str, Any], default_config["node_group"]) 

2040 merged_config["node_group"] = { 

2041 **default_node_group, 

2042 **global_node_group, 

2043 } 

2044 

2045 # Check for region-specific override 

2046 if region: 

2047 region_overrides_ctx = self.app.node.try_get_context("fsx_lustre_regions") 

2048 region_overrides: dict[str, Any] = ( 

2049 region_overrides_ctx if isinstance(region_overrides_ctx, dict) else {} 

2050 ) 

2051 if region in region_overrides: 

2052 region_config = region_overrides[region] 

2053 if isinstance(region_config, dict): 

2054 # Preserve the fully merged default/global node group before 

2055 # the top-level regional overlay replaces that nested value. 

2056 # A regional node_group is a patch, not a wholesale reset. 

2057 existing_node_group = merged_config.get("node_group") 

2058 merged_config = {**merged_config, **region_config} 

2059 # Handle nested node_group override 

2060 if "node_group" in region_config: 

2061 region_node_group = region_config["node_group"] 

2062 if isinstance(region_node_group, dict): 

2063 if isinstance(existing_node_group, dict): 

2064 base_node_group = existing_node_group 

2065 else: 

2066 base_node_group = cast(dict[str, Any], default_config["node_group"]) 

2067 merged_config["node_group"] = { 

2068 **base_node_group, 

2069 **region_node_group, 

2070 } 

2071 

2072 if self._feature_override_enabled("fsx_lustre"): 

2073 merged_config["enabled"] = True 

2074 return merged_config 

2075 

2076 def _feature_override_enabled(self, feature_key: str) -> bool: 

2077 """True when ``feature_enabled_overrides`` context forces this feature on.""" 

2078 overrides = parse_feature_enabled_overrides( 

2079 self.app.node.try_get_context(FEATURE_OVERRIDE_CONTEXT_KEY) 

2080 ) 

2081 return feature_key in overrides 

2082 

2083 def get_valkey_config(self) -> dict[str, Any]: 

2084 """Get Valkey Serverless cache configuration. 

2085 

2086 Returns: 

2087 Valkey configuration dictionary with the following keys: 

2088 - enabled: Whether Valkey cache is enabled (default: False) 

2089 - max_data_storage_gb: Maximum data storage in GB (default: 5) 

2090 - max_ecpu_per_second: Maximum ECPUs per second (default: 5000) 

2091 - snapshot_retention_limit: Daily snapshots to retain (default: 1) 

2092 """ 

2093 default_config: dict[str, Any] = { 

2094 "enabled": False, 

2095 "max_data_storage_gb": 5, 

2096 "max_ecpu_per_second": 5000, 

2097 "snapshot_retention_limit": 1, 

2098 } 

2099 valkey_ctx = self.app.node.try_get_context("valkey") 

2100 valkey_config: dict[str, Any] = valkey_ctx if isinstance(valkey_ctx, dict) else {} 

2101 merged = {**default_config, **valkey_config} 

2102 if self._feature_override_enabled("valkey"): 

2103 merged["enabled"] = True 

2104 return merged 

2105 

2106 def get_aurora_pgvector_config(self) -> dict[str, Any]: 

2107 """Get Aurora Serverless v2 + pgvector vector database configuration. 

2108 

2109 Returns: 

2110 Aurora pgvector configuration dictionary with the following keys: 

2111 - enabled: Whether Aurora pgvector is enabled (default: False) 

2112 - min_acu: Minimum Aurora Capacity Units (default: 0, scales to zero) 

2113 - max_acu: Maximum Aurora Capacity Units (default: 16) 

2114 - backup_retention_days: Number of days to retain automated backups (default: 7) 

2115 - deletion_protection: Whether deletion protection is enabled (default: False) 

2116 """ 

2117 default_config: dict[str, Any] = { 

2118 "enabled": False, 

2119 "min_acu": 0, 

2120 "max_acu": 16, 

2121 "backup_retention_days": 7, 

2122 "deletion_protection": False, 

2123 } 

2124 aurora_ctx = self.app.node.try_get_context("aurora_pgvector") 

2125 aurora_config: dict[str, Any] = aurora_ctx if isinstance(aurora_ctx, dict) else {} 

2126 merged = {**default_config, **aurora_config} 

2127 if self._feature_override_enabled("aurora_pgvector"): 

2128 merged["enabled"] = True 

2129 return merged 

2130 

2131 def get_analytics_config(self) -> dict[str, Any]: 

2132 """Get optional analytics environment configuration. 

2133 

2134 Returns the fully-merged analytics_environment block from cdk.json 

2135 layered on top of the defaults below. Sub-blocks (``hyperpod``, 

2136 ``cognito``, ``efs``, ``studio``) are deep-merged so a user who 

2137 overrides a single nested key (e.g. ``cognito.domain_prefix``) does 

2138 not inadvertently wipe the sub-block's other defaults — mirroring the 

2139 nested-merge pattern used by ``get_fsx_lustre_config`` for its 

2140 ``node_group`` sub-block. 

2141 

2142 Returns: 

2143 Analytics configuration dictionary with the following keys: 

2144 - enabled: Whether the analytics environment stack is deployed 

2145 (default: False — the feature is off unless explicitly opted in) 

2146 - hyperpod: SageMaker HyperPod integration sub-block 

2147 - enabled: Whether to add the HyperPod IAM grants to 

2148 SageMaker_Execution_Role (default: False) 

2149 - canvas: SageMaker Canvas integration sub-block 

2150 - enabled: Whether to enable the SageMaker Canvas app on 

2151 the Studio domain and attach ``AmazonSageMakerCanvasFullAccess`` 

2152 to the SageMaker_Execution_Role (default: False) 

2153 - cognito: Cognito user-pool sub-block 

2154 - domain_prefix: UserPoolDomain prefix, or None to let the 

2155 analytics stack derive one (default: None) 

2156 - removal_policy: "destroy" (default) or "retain" — controls 

2157 the Cognito pool's CloudFormation DeletionPolicy 

2158 - efs: Studio_EFS sub-block 

2159 - removal_policy: "destroy" (default) or "retain" — controls 

2160 the Studio EFS file system's CloudFormation DeletionPolicy 

2161 - studio: SageMaker Studio sub-block 

2162 - user_profile_name_prefix: Optional prefix for per-user 

2163 profile names, or None to use the Cognito username verbatim 

2164 (default: None) 

2165 """ 

2166 default_config: dict[str, Any] = { 

2167 "enabled": False, 

2168 "hyperpod": {"enabled": False}, 

2169 "canvas": {"enabled": False}, 

2170 "cognito": {"domain_prefix": None, "removal_policy": "destroy"}, 

2171 "efs": {"removal_policy": "destroy"}, 

2172 "studio": {"user_profile_name_prefix": None}, 

2173 } 

2174 analytics_ctx = self.app.node.try_get_context("analytics_environment") 

2175 analytics_config: dict[str, Any] = analytics_ctx if isinstance(analytics_ctx, dict) else {} 

2176 merged_config: dict[str, Any] = {**default_config, **analytics_config} 

2177 

2178 # Deep-merge each nested sub-block so a partial override does not 

2179 # drop the other defaults in the same sub-block. 

2180 for sub_block in ("hyperpod", "canvas", "cognito", "efs", "studio"): 

2181 override = analytics_config.get(sub_block) 

2182 if isinstance(override, dict): 

2183 default_sub = cast(dict[str, Any], default_config[sub_block]) 

2184 merged_config[sub_block] = {**default_sub, **override} 

2185 

2186 return merged_config 

2187 

2188 def get_analytics_enabled(self) -> bool: 

2189 """Return whether the analytics environment stack is enabled. 

2190 

2191 Thin wrapper around ``get_analytics_config()["enabled"]`` to mirror 

2192 the existing ``get_valkey_config`` / ``get_aurora_pgvector_config`` 

2193 access pattern without forcing every call site to index into the 

2194 merged dict. 

2195 """ 

2196 return bool(self.get_analytics_config()["enabled"]) 

2197 

2198 def get_cluster_observability_config(self) -> dict[str, Any]: 

2199 """Get the in-cluster observability configuration. 

2200 

2201 Returns the fully-merged cluster_observability block from cdk.json 

2202 layered on top of the defaults below. Sub-blocks (``grafana``, 

2203 ``prometheus``, ``alertmanager``) are deep-merged so a user who 

2204 overrides a single nested key (e.g. ``prometheus.retention``) does not 

2205 inadvertently wipe the sub-block's other defaults — mirroring the 

2206 nested-merge pattern used by ``get_analytics_config``. 

2207 

2208 Unlike most optional features, this one is **on by default**: a stock 

2209 deployment installs kube-prometheus-stack on every regional cluster. 

2210 Operators opt out by setting ``cluster_observability.enabled = false``. 

2211 

2212 Returns: 

2213 Cluster observability configuration dictionary with the keys: 

2214 - enabled: Whether kube-prometheus-stack is installed per region 

2215 (default: True) 

2216 - grafana: Grafana sub-block 

2217 - persistence_size: EBS PVC size for Grafana's user database 

2218 and dashboards (default: "10Gi") 

2219 - admin_user: Grafana admin username; the password is 

2220 chart-generated in the <release>-grafana Secret, never 

2221 authored here (default: "admin") 

2222 - admin_password_rotation_schedule: 5-field cron for the 

2223 in-cluster CronJob that rotates the chart-generated admin 

2224 password (default: "0 4 1 * *", monthly) 

2225 - prometheus: Prometheus sub-block 

2226 - persistence_size: EBS PVC size for the Prometheus TSDB 

2227 (default: "50Gi") 

2228 - retention: Prometheus retention window (default: "15d") 

2229 - alertmanager: Alertmanager sub-block 

2230 - enabled: Whether Alertmanager is deployed (default: True) 

2231 - persistence_size: EBS PVC size for Alertmanager (default: "5Gi") 

2232 - mlflow: MLflow experiment-tracking sub-block 

2233 - enabled: Whether the MLflow tracking server is installed per 

2234 region (default: True). Effective only while observability 

2235 itself is enabled — see ``get_mlflow_enabled``. 

2236 - persistence_size: EBS PVC size for the tracking server's 

2237 SQLite run-metadata store (default: "10Gi"); artifacts go 

2238 to S3, not this volume 

2239 """ 

2240 default_config: dict[str, Any] = { 

2241 "enabled": True, 

2242 "grafana": { 

2243 "persistence_size": "10Gi", 

2244 "admin_user": "admin", 

2245 # Monthly (04:00 on the 1st) rotation of the chart-generated 

2246 # Grafana admin password, run by an in-cluster CronJob. 

2247 "admin_password_rotation_schedule": "0 4 1 * *", # nosec B105 # cron schedule, not a password 

2248 }, 

2249 "prometheus": {"persistence_size": "50Gi", "retention": "15d"}, 

2250 "alertmanager": {"enabled": True, "persistence_size": "5Gi"}, 

2251 "mlflow": {"enabled": True, "persistence_size": "10Gi"}, 

2252 } 

2253 obs_ctx = self.app.node.try_get_context("cluster_observability") 

2254 obs_config: dict[str, Any] = obs_ctx if isinstance(obs_ctx, dict) else {} 

2255 merged_config: dict[str, Any] = {**default_config, **obs_config} 

2256 

2257 # Deep-merge each nested sub-block so a partial override does not 

2258 # drop the other defaults in the same sub-block. 

2259 for sub_block in ("grafana", "prometheus", "alertmanager", "mlflow"): 

2260 override = obs_config.get(sub_block) 

2261 if isinstance(override, dict): 

2262 default_sub = cast(dict[str, Any], default_config[sub_block]) 

2263 merged_config[sub_block] = {**default_sub, **override} 

2264 

2265 return merged_config 

2266 

2267 def get_cluster_observability_enabled(self) -> bool: 

2268 """Return whether in-cluster observability is enabled (default True). 

2269 

2270 Thin wrapper around ``get_cluster_observability_config()["enabled"]`` 

2271 so call sites (the regional stack's chart-enable and value-override 

2272 methods, the CLI) do not have to index into the merged dict. 

2273 """ 

2274 return bool(self.get_cluster_observability_config()["enabled"]) 

2275 

2276 def get_mlflow_enabled(self) -> bool: 

2277 """Return whether the MLflow tracking server is effectively enabled. 

2278 

2279 The conjunction of ``cluster_observability.mlflow.enabled`` (default 

2280 True) and ``cluster_observability.enabled`` (default True): MLflow 

2281 installs into the ``monitoring`` namespace kube-prometheus-stack 

2282 creates, stores run metadata on the observability gp3 StorageClass, 

2283 and is reached through the same tunnel commands, so disabling 

2284 observability switches the tracking server off with it rather than 

2285 deploying it against missing storage — the same conjunction shape 

2286 ``get_cost_monitoring_enabled`` uses for OpenCost. 

2287 """ 

2288 obs = self.get_cluster_observability_config() 

2289 return bool(obs["mlflow"]["enabled"]) and bool(obs["enabled"]) 

2290 

2291 def get_cost_monitoring_config(self) -> dict[str, Any]: 

2292 """Get the cost monitoring configuration. 

2293 

2294 Returns the fully-merged ``cost_monitoring`` block from cdk.json 

2295 layered on top of the defaults below. Sub-blocks (``reports``, 

2296 ``athena``) are deep-merged so a user who overrides a single nested 

2297 key does not inadvertently wipe the sub-block's other defaults — 

2298 mirroring the nested-merge pattern used by 

2299 ``get_cluster_observability_config``. 

2300 

2301 Like cluster observability, cost monitoring is **on by default**: a 

2302 stock deployment installs OpenCost per region, provisions the cost 

2303 report bucket + Athena analytics in the monitoring stack, and runs 

2304 the cost-monitor service on every regional cluster. Operators opt out 

2305 by setting ``cost_monitoring.enabled = false``. 

2306 

2307 Returns: 

2308 Cost monitoring configuration dictionary with the keys: 

2309 - enabled: Whether the cost monitoring pipeline is deployed 

2310 (default: True). Requires ``cluster_observability.enabled``. 

2311 - reports: Cost report sub-block 

2312 - interval_minutes: cadence of the cost-monitor service's 

2313 scheduled Parquet reports (default: 60) 

2314 - retention_days: S3 lifecycle expiration for report objects 

2315 (default: 365) 

2316 - transition_to_infrequent_access_days: S3 lifecycle transition 

2317 to STANDARD_IA (default: 90; must be < retention_days) 

2318 - athena: Athena analytics sub-block 

2319 - query_results_retention_days: S3 lifecycle expiration for 

2320 Athena query results written under ``athena-results/`` 

2321 (default: 30) 

2322 """ 

2323 default_config: dict[str, Any] = { 

2324 "enabled": True, 

2325 "reports": { 

2326 "interval_minutes": 60, 

2327 "retention_days": 365, 

2328 "transition_to_infrequent_access_days": 90, 

2329 }, 

2330 "athena": { 

2331 "query_results_retention_days": 30, 

2332 }, 

2333 } 

2334 cost_ctx = self.app.node.try_get_context("cost_monitoring") 

2335 cost_config: dict[str, Any] = cost_ctx if isinstance(cost_ctx, dict) else {} 

2336 merged_config: dict[str, Any] = {**default_config, **cost_config} 

2337 

2338 # Deep-merge each nested sub-block so a partial override does not 

2339 # drop the other defaults in the same sub-block. 

2340 for sub_block in ("reports", "athena"): 

2341 override = cost_config.get(sub_block) 

2342 if isinstance(override, dict): 

2343 default_sub = cast(dict[str, Any], default_config[sub_block]) 

2344 merged_config[sub_block] = {**default_sub, **override} 

2345 

2346 return merged_config 

2347 

2348 def get_cost_monitoring_enabled(self) -> bool: 

2349 """Return whether the cost monitoring pipeline is effectively enabled. 

2350 

2351 The conjunction of ``cost_monitoring.enabled`` (default True) and 

2352 ``cluster_observability.enabled`` (default True): OpenCost reads its 

2353 usage data from the in-cluster Prometheus, so disabling observability 

2354 switches the whole cost pipeline off with it rather than deploying a 

2355 pipeline with no data source (or failing synthesis for a user who 

2356 only ran ``gco monitoring disable``). Call sites — the regional 

2357 stack's chart-enable and image-build methods, the monitoring stack, 

2358 the CLI — all gate on this one conjunction. 

2359 """ 

2360 return bool(self.get_cost_monitoring_config()["enabled"]) and bool( 

2361 self.get_cluster_observability_config()["enabled"] 

2362 ) 

2363 

2364 def _validate_tracing_config(self) -> None: 

2365 """Validate the optional ``tracing`` block and ``tracing_overrides``. 

2366 

2367 Resolving the block is the validation: unknown keys, non-boolean 

2368 flags, a ``sample_ratio`` outside ``[0, 1]`` and malformed overrides 

2369 all raise :class:`ConfigValidationError` at synth instead of shipping 

2370 a value the services would silently replace with their defaults. 

2371 """ 

2372 self.get_tracing_config() 

2373 

2374 def get_tracing_config(self) -> dict[str, Any]: 

2375 """Return the validated ``tracing`` block with defaults merged in. 

2376 

2377 Keys (see :data:`TRACING_DEFAULTS`): 

2378 - enabled: render ``GCO_TRACING_ENABLED=true`` into the 

2379 health-monitor, manifest-processor, inference-proxy and 

2380 cost-monitor Deployments and grant their roles the X-Ray span 

2381 export actions (default True) 

2382 - sample_ratio: head-sampling probability of a new trace, a float 

2383 in ``[0, 1]`` (default 0.05) 

2384 - enable_transaction_search: let each regional stack switch on 

2385 CloudWatch Transaction Search in its Region (default True; 

2386 effective only while ``enabled`` — see 

2387 :meth:`get_transaction_search_enabled`) 

2388 

2389 Honors the run-scoped ``tracing_overrides`` context (a JSON object 

2390 deep-merged over the block), which the live release harness uses to 

2391 sample every trace of its run. 

2392 """ 

2393 return resolve_tracing_config( 

2394 self.app.node.try_get_context(TRACING_CONTEXT_KEY), 

2395 self.app.node.try_get_context(TRACING_OVERRIDES_CONTEXT_KEY), 

2396 ) 

2397 

2398 def get_tracing_enabled(self) -> bool: 

2399 """Return whether the API services export traces (default True).""" 

2400 return bool(self.get_tracing_config()["enabled"]) 

2401 

2402 def get_transaction_search_enabled(self) -> bool: 

2403 """Return whether the regional stacks enable CloudWatch Transaction Search. 

2404 

2405 The conjunction of ``tracing.enabled`` and 

2406 ``tracing.enable_transaction_search``: Transaction Search only exists 

2407 to receive GCO's spans, so a deployment with tracing off never touches 

2408 the account-level setting. 

2409 """ 

2410 config = self.get_tracing_config() 

2411 return bool(config["enabled"]) and bool(config["enable_transaction_search"]) 

2412 

2413 def get_capacity_history_config(self) -> dict[str, Any]: 

2414 """Get the optional historical capacity surface configuration. 

2415 

2416 Returns the merged ``historical`` block from cdk.json layered on top of 

2417 the defaults below. The feature is off unless ``historical.enabled`` is 

2418 explicitly true, mirroring the analytics-environment opt-in pattern. 

2419 

2420 Keys: 

2421 - enabled: deploy the capacity poller stack + history table (default False) 

2422 - retention_days: DynamoDB TTL window for snapshots (default 90) 

2423 - poll_interval_minutes: EventBridge schedule cadence (default 15) 

2424 - capacity_block_duration_hours: short Capacity Block probe duration 

2425 the poller snapshots (default 24 = 1 day) 

2426 - capacity_block_long_duration_hours: long Capacity Block probe 

2427 duration in hours (default 1512 = 63 days); 0 disables the long 

2428 probe and its ``capacity_blocks_long_*`` metrics 

2429 - spot_score_target_capacities: Spot Placement Score target 

2430 capacities the poller snapshots per instance pool (default 

2431 [1, 10, 50]); a subset selector over the supported set exported 

2432 by ``cli/capacity/history.py``, where capacity 1 keeps the 

2433 original ``spot_score`` field and N > 1 writes ``spot_score_at_N`` 

2434 - watch_instance_types: instance types the poller snapshots 

2435 - enabled_regions: regions to poll; empty means all deployed regions 

2436 """ 

2437 default_config: dict[str, Any] = { 

2438 "enabled": False, 

2439 "retention_days": 90, 

2440 "poll_interval_minutes": 15, 

2441 "capacity_block_duration_hours": 24, 

2442 "capacity_block_long_duration_hours": 63 * 24, 

2443 "spot_score_target_capacities": [1, 10, 50], 

2444 "watch_instance_types": [ 

2445 "g4dn.12xlarge", 

2446 "g4dn.16xlarge", 

2447 "g4dn.2xlarge", 

2448 "g4dn.4xlarge", 

2449 "g4dn.8xlarge", 

2450 "g4dn.metal", 

2451 "g4dn.xlarge", 

2452 "g5.12xlarge", 

2453 "g5.16xlarge", 

2454 "g5.24xlarge", 

2455 "g5.2xlarge", 

2456 "g5.48xlarge", 

2457 "g5.4xlarge", 

2458 "g5.8xlarge", 

2459 "g5.xlarge", 

2460 "g5g.16xlarge", 

2461 "g5g.2xlarge", 

2462 "g5g.4xlarge", 

2463 "g5g.8xlarge", 

2464 "g5g.metal", 

2465 "g5g.xlarge", 

2466 "g6.12xlarge", 

2467 "g6.16xlarge", 

2468 "g6.24xlarge", 

2469 "g6.2xlarge", 

2470 "g6.48xlarge", 

2471 "g6.4xlarge", 

2472 "g6.8xlarge", 

2473 "g6.xlarge", 

2474 "g6e.12xlarge", 

2475 "g6e.16xlarge", 

2476 "g6e.24xlarge", 

2477 "g6e.2xlarge", 

2478 "g6e.48xlarge", 

2479 "g6e.4xlarge", 

2480 "g6e.8xlarge", 

2481 "g6e.xlarge", 

2482 "g6f.2xlarge", 

2483 "g6f.4xlarge", 

2484 "g6f.large", 

2485 "g6f.xlarge", 

2486 "g7.12xlarge", 

2487 "g7.24xlarge", 

2488 "g7.2xlarge", 

2489 "g7.48xlarge", 

2490 "g7.4xlarge", 

2491 "g7.8xlarge", 

2492 "g7e.12xlarge", 

2493 "g7e.24xlarge", 

2494 "g7e.2xlarge", 

2495 "g7e.48xlarge", 

2496 "g7e.4xlarge", 

2497 "g7e.8xlarge", 

2498 "gr6.4xlarge", 

2499 "gr6.8xlarge", 

2500 "gr6f.4xlarge", 

2501 "inf1.24xlarge", 

2502 "inf1.2xlarge", 

2503 "inf1.6xlarge", 

2504 "inf1.xlarge", 

2505 "inf2.24xlarge", 

2506 "inf2.48xlarge", 

2507 "inf2.8xlarge", 

2508 "inf2.xlarge", 

2509 "p3dn.24xlarge", 

2510 "p4d.24xlarge", 

2511 "p4de.24xlarge", 

2512 "p5.48xlarge", 

2513 "p5.4xlarge", 

2514 "p5e.48xlarge", 

2515 "p5en.48xlarge", 

2516 "p6-b200.48xlarge", 

2517 "p6-b300.48xlarge", 

2518 "trn1.2xlarge", 

2519 "trn1.32xlarge", 

2520 "trn1n.32xlarge", 

2521 "trn2.3xlarge", 

2522 "trn2.48xlarge", 

2523 ], 

2524 "enabled_regions": [], 

2525 } 

2526 historical_ctx = self.app.node.try_get_context("historical") 

2527 historical_config = historical_ctx if isinstance(historical_ctx, dict) else {} 

2528 return {**default_config, **historical_config} 

2529 

2530 def get_capacity_history_enabled(self) -> bool: 

2531 """Return whether the historical capacity surface is enabled.""" 

2532 return bool(self.get_capacity_history_config()["enabled"]) 

2533 

2534 def get_mission_memory_config(self) -> dict[str, Any]: 

2535 """Get the mission-memory configuration (recall across mission sessions). 

2536 

2537 Returns the merged ``mission_memory`` block from cdk.json layered on 

2538 top of the defaults below. The feature is ON by default — memory is 

2539 cheap (one small PAY_PER_REQUEST item plus one embedding call per 

2540 completed mission) and silently missing recall is the worse failure 

2541 mode; set ``mission_memory.enabled: false`` to opt out. 

2542 

2543 Keys: 

2544 - enabled: provision the mission-memory table + vector index and 

2545 activate best-effort write/retrieval in the engine (default True) 

2546 - retention_days: DynamoDB TTL window for memory items (default 365) 

2547 - dimensions: embedding vector width (default 1024). ONE-WAY DOOR: 

2548 immutable after index creation and must match the configured 

2549 ``bedrock.embedding_model_id`` output width. 

2550 - distance_function: vector distance metric (default COSINE); 

2551 immutable after index creation. 

2552 - top_k: similar past missions retrieved into the sampling prompt 

2553 (default 3) 

2554 """ 

2555 default_config: dict[str, Any] = { 

2556 "enabled": True, 

2557 "retention_days": 365, 

2558 "dimensions": 1024, 

2559 "distance_function": "COSINE", 

2560 "top_k": 3, 

2561 } 

2562 mission_memory_ctx = self.app.node.try_get_context("mission_memory") 

2563 mission_memory_config = mission_memory_ctx if isinstance(mission_memory_ctx, dict) else {} 

2564 return {**default_config, **mission_memory_config} 

2565 

2566 def get_mission_memory_enabled(self) -> bool: 

2567 """Return whether mission memory is enabled.""" 

2568 return bool(self.get_mission_memory_config()["enabled"]) 

2569 

2570 def get_vector_store_config(self) -> dict[str, Any]: 

2571 """Get the vector-store configuration (global workload RAG corpus). 

2572 

2573 Returns the merged ``vector_store`` block from cdk.json layered on 

2574 top of the defaults below. The feature is OFF by default — a 

2575 replicated vector store carries real per-region storage and write 

2576 cost, so it is an explicit opt-in like ``aurora_pgvector``; set 

2577 ``vector_store.enabled: true`` to provision it. 

2578 

2579 Keys: 

2580 - enabled: provision the global vector-store table + index, 

2581 the S3-triggered ingest pipeline, and the regional read wiring 

2582 (default False) 

2583 - dimensions: embedding vector width (default 1024). ONE-WAY 

2584 DOOR: immutable after index creation and must match the 

2585 configured ``embedding_model_id`` output width. 

2586 - distance_function: vector distance metric (default COSINE); 

2587 immutable after index creation. 

2588 - embedding_model_id: Bedrock text-embedding model used by the 

2589 ingest pipeline and query paths (default 

2590 amazon.titan-embed-text-v2:0). Independent of 

2591 ``bedrock.embedding_model_id`` on purpose. Changing it means 

2592 re-ingesting the corpus: vectors from different models are 

2593 not comparable. 

2594 - replica_regions: regions to replicate the table into. Empty 

2595 (the default) means "follow deployment_regions.regional", 

2596 excluding the global region (the primary). 

2597 - corpus_prefix: S3 key prefix on the Cluster_Shared_Bucket 

2598 watched by the ingest pipeline (default vector-corpus/). 

2599 """ 

2600 default_config: dict[str, Any] = { 

2601 "enabled": False, 

2602 "dimensions": 1024, 

2603 "distance_function": "COSINE", 

2604 "embedding_model_id": "amazon.titan-embed-text-v2:0", 

2605 "replica_regions": [], 

2606 "corpus_prefix": "vector-corpus/", 

2607 } 

2608 vector_store_ctx = self.app.node.try_get_context("vector_store") 

2609 vector_store_config = vector_store_ctx if isinstance(vector_store_ctx, dict) else {} 

2610 merged = {**default_config, **vector_store_config} 

2611 if self._feature_override_enabled("vector_store"): 

2612 merged["enabled"] = True 

2613 return merged 

2614 

2615 def get_vector_store_enabled(self) -> bool: 

2616 """Return whether the vector store is enabled (default False).""" 

2617 return bool(self.get_vector_store_config()["enabled"]) 

2618 

2619 def get_vector_store_replica_regions(self) -> list[str]: 

2620 """Return the effective replica region list for the vector store. 

2621 

2622 The configured ``replica_regions`` when non-empty, otherwise the 

2623 regional deployment list — in both cases with the global region 

2624 removed, because the primary table lives there and a global table 

2625 cannot replicate into its own region. May legitimately be empty 

2626 (single-region deployments get a single-region global table). 

2627 """ 

2628 config = self.get_vector_store_config() 

2629 configured = [str(region) for region in config["replica_regions"]] 

2630 candidates = configured or self.get_regions() 

2631 global_region = self.get_global_region() 

2632 return [region for region in candidates if region != global_region] 

2633 

2634 def get_tags(self) -> dict[str, str]: 

2635 """Get common tags from configuration""" 

2636 return self.app.node.try_get_context("tags") or {} 

2637 

2638 def validate_region_availability(self, region: str) -> bool: 

2639 """Validate that a region is available in the current AWS account""" 

2640 try: 

2641 ec2 = boto3.client("ec2", region_name=region) 

2642 ec2.describe_regions(RegionNames=[region]) 

2643 return True 

2644 except Exception as e: 

2645 logger.debug("Region %s not available: %s", region, e) 

2646 return False 

2647 

2648 def get_available_regions(self) -> list[str]: 

2649 """Get list of available AWS regions for the current account""" 

2650 try: 

2651 ec2 = boto3.client("ec2") 

2652 response = ec2.describe_regions() 

2653 return [region["RegionName"] for region in response["Regions"]] 

2654 except Exception as e: 

2655 logger.debug("Failed to list regions, using defaults: %s", e) 

2656 return list(self.VALID_REGIONS)