Coverage for cli / storage.py: 100.00%

824 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +0000

1"""Discover GCO S3 buckets and safely sync data with local storage. 

2 

3The physical names of GCO buckets contain deployment-generated account and 

4region components. This module keeps those names out of the user interface by 

5resolving stable aliases from the SSM and CloudFormation contracts published by 

6the stacks. 

7""" 

8 

9from __future__ import annotations 

10 

11import base64 

12import hashlib 

13import os 

14import secrets 

15import stat 

16import sys 

17import unicodedata 

18from contextlib import suppress 

19from dataclasses import dataclass 

20from datetime import datetime 

21from pathlib import Path 

22from types import TracebackType 

23from typing import Any, Self 

24 

25import boto3 

26from botocore.exceptions import ClientError 

27 

28from .config import GCOConfig, get_config 

29 

30type _FileSignature = tuple[int, int, int, int, int] 

31 

32 

33class StorageBucketNotFoundError(RuntimeError): 

34 """Raised when a friendly alias has no deployed backing bucket.""" 

35 

36 

37@dataclass(frozen=True) 

38class _ConfinementContract: 

39 """Identity-bound MCP local-root contract propagated to the CLI.""" 

40 

41 root: Path 

42 device: int 

43 inode: int 

44 

45 

46class _PinnedRoot: 

47 """Descriptor-pinned root for race-resistant confined filesystem access.""" 

48 

49 def __init__(self, contract: _ConfinementContract): 

50 if os.name != "posix" or not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_DIRECTORY"): 

51 raise RuntimeError( 

52 "Confined storage sync requires descriptor-relative no-follow filesystem support" 

53 ) 

54 if not contract.root.is_absolute(): 

55 raise ValueError("The internal storage confinement root must be absolute") 

56 

57 self.root = contract.root 

58 flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0) 

59 try: 

60 self._fd = os.open(self.root, flags) 

61 except OSError as exc: 

62 raise ValueError( 

63 f"Cannot open the configured storage confinement root securely: {self.root}" 

64 ) from exc 

65 

66 root_stat = os.fstat(self._fd) 

67 if (root_stat.st_dev, root_stat.st_ino) != (contract.device, contract.inode): 

68 os.close(self._fd) 

69 self._fd = -1 

70 raise RuntimeError( 

71 "GCO_STORAGE_LOCAL_ROOT changed after the MCP request was validated; retry the call" 

72 ) 

73 

74 def __enter__(self) -> Self: 

75 return self 

76 

77 def __exit__( 

78 self, 

79 exc_type: type[BaseException] | None, 

80 exc_value: BaseException | None, 

81 traceback: TracebackType | None, 

82 ) -> None: 

83 if self._fd >= 0: 

84 os.close(self._fd) 

85 self._fd = -1 

86 

87 @staticmethod 

88 def _directory_flags() -> int: 

89 return os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0) 

90 

91 @staticmethod 

92 def _file_flags() -> int: 

93 # Avoid blocking if a raced source replacement is a FIFO; fstat below 

94 # still rejects every opened object that is not a regular file. 

95 return ( 

96 os.O_RDONLY 

97 | os.O_NOFOLLOW 

98 | getattr(os, "O_NONBLOCK", 0) 

99 | getattr(os, "O_BINARY", 0) 

100 | getattr(os, "O_CLOEXEC", 0) 

101 ) 

102 

103 def relative_parts(self, local_path: str) -> tuple[str, ...]: 

104 """Return a lexical root-relative path; traversal itself stays descriptor-relative.""" 

105 supplied = Path(local_path).expanduser() 

106 candidate = supplied if supplied.is_absolute() else self.root / supplied 

107 lexical = Path(os.path.abspath(candidate)) 

108 try: 

109 relative = lexical.relative_to(self.root) 

110 except ValueError as exc: 

111 raise ValueError( 

112 f"Local sync path must stay within GCO_STORAGE_LOCAL_ROOT: {local_path}" 

113 ) from exc 

114 

115 return tuple(relative.parts) 

116 

117 def display_path(self, parts: tuple[str, ...]) -> Path: 

118 """Return a human-readable path without using it for confined access.""" 

119 return self.root.joinpath(*parts) 

120 

121 def open_directory( 

122 self, 

123 parts: tuple[str, ...], 

124 *, 

125 create: bool = False, 

126 allow_missing: bool = False, 

127 ) -> int | None: 

128 """Open a directory by walking from the pinned root without following links.""" 

129 current_fd = os.dup(self._fd) 

130 try: 

131 walked: tuple[str, ...] = () 

132 for part in parts: 

133 walked += (part,) 

134 try: 

135 child_fd = os.open(part, self._directory_flags(), dir_fd=current_fd) 

136 except FileNotFoundError: 

137 if not create: 

138 if allow_missing: 

139 os.close(current_fd) 

140 return None 

141 raise FileNotFoundError( 

142 f"Confined local directory does not exist: {self.display_path(walked)}" 

143 ) from None 

144 with suppress(FileExistsError): 

145 os.mkdir(part, dir_fd=current_fd) 

146 try: 

147 child_fd = os.open(part, self._directory_flags(), dir_fd=current_fd) 

148 except OSError as exc: 

149 raise ValueError( 

150 "Confined local path changed while creating a directory: " 

151 f"{self.display_path(walked)}" 

152 ) from exc 

153 except OSError as exc: 

154 raise ValueError( 

155 "Confined local path contains a symbolic link or non-directory: " 

156 f"{self.display_path(walked)}" 

157 ) from exc 

158 os.close(current_fd) 

159 current_fd = child_fd 

160 return current_fd 

161 except BaseException: 

162 os.close(current_fd) 

163 raise 

164 

165 def inspect_directory( 

166 self, 

167 parts: tuple[str, ...], 

168 *, 

169 create: bool, 

170 ) -> bool: 

171 """Securely inspect or create a confined directory.""" 

172 directory_fd = self.open_directory(parts, create=create, allow_missing=not create) 

173 if directory_fd is None: 

174 return False 

175 os.close(directory_fd) 

176 return True 

177 

178 def lstat(self, parts: tuple[str, ...]) -> os.stat_result | None: 

179 """Stat a confined path without following its final component.""" 

180 if not parts: 

181 return os.fstat(self._fd) 

182 parent_fd = self.open_directory(parts[:-1], allow_missing=True) 

183 if parent_fd is None: 

184 return None 

185 try: 

186 try: 

187 return os.stat(parts[-1], dir_fd=parent_fd, follow_symlinks=False) 

188 except FileNotFoundError: 

189 return None 

190 finally: 

191 os.close(parent_fd) 

192 

193 def open_regular_file(self, parts: tuple[str, ...]) -> int: 

194 """Open a confined regular file without following any path component.""" 

195 if not parts: 

196 raise IsADirectoryError(f"Upload source is a directory: {self.root}") 

197 parent_fd = self.open_directory(parts[:-1]) 

198 if parent_fd is None: # pragma: no cover - allow_missing is false 

199 raise FileNotFoundError(self.display_path(parts)) 

200 try: 

201 try: 

202 file_fd = os.open(parts[-1], self._file_flags(), dir_fd=parent_fd) 

203 except OSError as exc: 

204 raise ValueError( 

205 "Confined upload source is missing, linked, or not a regular file: " 

206 f"{self.display_path(parts)}" 

207 ) from exc 

208 finally: 

209 os.close(parent_fd) 

210 

211 file_stat = os.fstat(file_fd) 

212 if not stat.S_ISREG(file_stat.st_mode): 

213 os.close(file_fd) 

214 raise ValueError(f"Upload source is not a regular file: {self.display_path(parts)}") 

215 return file_fd 

216 

217 def open_child_directory(self, parent_fd: int, name: str, display: Path) -> int: 

218 """Open an enumerated child directory without following a raced replacement.""" 

219 try: 

220 child_fd = os.open(name, self._directory_flags(), dir_fd=parent_fd) 

221 except OSError as exc: 

222 raise ValueError(f"Upload source directory changed or is linked: {display}") from exc 

223 return child_fd 

224 

225 def open_child_regular_file(self, parent_fd: int, name: str, display: Path) -> int: 

226 """Open an enumerated child file without following a raced replacement.""" 

227 try: 

228 file_fd = os.open(name, self._file_flags(), dir_fd=parent_fd) 

229 except OSError as exc: 

230 raise ValueError(f"Upload source file changed or is linked: {display}") from exc 

231 if not stat.S_ISREG(os.fstat(file_fd).st_mode): 

232 os.close(file_fd) 

233 raise ValueError(f"Upload source is not a regular file: {display}") 

234 return file_fd 

235 

236 def download_target_is_current( 

237 self, 

238 parts: tuple[str, ...], 

239 size: int, 

240 modified: datetime | None, 

241 *, 

242 evaluate_current: bool, 

243 ) -> bool: 

244 """Securely inspect a prospective destination and optionally test freshness.""" 

245 parent_fd = self.open_directory(parts[:-1], allow_missing=True) 

246 if parent_fd is None: 

247 return False 

248 try: 

249 try: 

250 target_stat = os.stat(parts[-1], dir_fd=parent_fd, follow_symlinks=False) 

251 except FileNotFoundError: 

252 return False 

253 finally: 

254 os.close(parent_fd) 

255 

256 display = self.display_path(parts) 

257 if stat.S_ISLNK(target_stat.st_mode): 

258 raise ValueError(f"Download destination must not be a symbolic link: {display}") 

259 if stat.S_ISDIR(target_stat.st_mode): 

260 raise IsADirectoryError(f"S3 object maps to an existing directory: {display}") 

261 if not stat.S_ISREG(target_stat.st_mode): 

262 raise ValueError(f"Download destination is not a regular file: {display}") 

263 return bool( 

264 evaluate_current 

265 and modified is not None 

266 and target_stat.st_size == size 

267 and int(target_stat.st_mtime) >= int(modified.timestamp()) 

268 ) 

269 

270 def download_object(self, s3: Any, bucket: str, obj: _SyncObject) -> None: 

271 """Download to a secure sibling temporary file and atomically install it.""" 

272 if obj.destination_parts is None: # pragma: no cover - internal invariant 

273 raise RuntimeError("Missing confined destination components") 

274 parent_fd = self.open_directory(obj.destination_parts[:-1], create=True) 

275 if parent_fd is None: # pragma: no cover - create is true 

276 raise RuntimeError("Failed to create confined destination directory") 

277 

278 temporary_name = "" 

279 temporary_created = False 

280 try: 

281 temporary_flags = ( 

282 os.O_RDWR 

283 | os.O_CREAT 

284 | os.O_EXCL 

285 | os.O_NOFOLLOW 

286 | getattr(os, "O_BINARY", 0) 

287 | getattr(os, "O_CLOEXEC", 0) 

288 ) 

289 for _ in range(128): 

290 temporary_name = f".gco-sync-{os.getpid()}-{secrets.token_hex(12)}.tmp" 

291 try: 

292 temporary_fd = os.open( 

293 temporary_name, 

294 temporary_flags, 

295 0o600, 

296 dir_fd=parent_fd, 

297 ) 

298 temporary_created = True 

299 break 

300 except FileExistsError: 

301 continue 

302 else: # pragma: no cover - cryptographically improbable 

303 raise RuntimeError("Could not allocate a unique download temporary file") 

304 

305 with os.fdopen(temporary_fd, "w+b") as temporary_file: 

306 s3.download_fileobj(bucket, obj.key, temporary_file) 

307 temporary_file.flush() 

308 downloaded_stat = os.fstat(temporary_file.fileno()) 

309 if downloaded_stat.st_size != obj.size: 

310 raise RuntimeError( 

311 f"Downloaded size changed for s3://{bucket}/{obj.key}: " 

312 f"expected {obj.size}, received {downloaded_stat.st_size}" 

313 ) 

314 if obj.last_modified is not None: 

315 timestamp = obj.last_modified.timestamp() 

316 os.utime(temporary_file.fileno(), (timestamp, timestamp)) 

317 

318 os.replace( 

319 temporary_name, 

320 obj.destination_parts[-1], 

321 src_dir_fd=parent_fd, 

322 dst_dir_fd=parent_fd, 

323 ) 

324 temporary_created = False 

325 finally: 

326 if temporary_created: 

327 with suppress(FileNotFoundError): 

328 os.unlink(temporary_name, dir_fd=parent_fd) 

329 os.close(parent_fd) 

330 

331 

332@dataclass(frozen=True) 

333class _SyncObject: 

334 """One validated object in a bucket-to-local sync plan.""" 

335 

336 key: str 

337 destination: Path 

338 destination_parts: tuple[str, ...] | None 

339 size: int 

340 last_modified: datetime | None 

341 current: bool 

342 

343 

344@dataclass(frozen=True) 

345class _PreparedUpload: 

346 """One securely opened and hashed local upload source.""" 

347 

348 source: Path 

349 relative: str 

350 source_parts: tuple[str, ...] | None 

351 size: int 

352 sha256: str 

353 signature: _FileSignature 

354 

355 

356@dataclass(frozen=True) 

357class _UploadObject: 

358 """One validated local file in a local-to-bucket sync plan.""" 

359 

360 source: Path 

361 source_parts: tuple[str, ...] | None 

362 key: str 

363 size: int 

364 sha256: str 

365 signature: _FileSignature 

366 current: bool 

367 

368 

369class StorageManager: 

370 """Resolve friendly GCO bucket aliases and transfer their contents.""" 

371 

372 _UPLOAD_DIGEST_METADATA = "gco-sync-sha256" 

373 

374 _PURPOSES = { 

375 "cluster-shared": "Cross-region cluster job artifacts and shared data", 

376 "model-weights": "Central model weights used by inference endpoints", 

377 "regional-shared": "General-purpose data for workloads in one region", 

378 "analytics-studio": "SageMaker Studio private scratch data and outputs", 

379 } 

380 

381 def __init__(self, config: GCOConfig | None = None): 

382 self.config = config or get_config() 

383 # One CloudFormation sweep per (stack, region) serves every access-logs 

384 # entry in that stack, so a full inventory stays at one call per stack. 

385 self._stack_resource_cache: dict[tuple[str, str], dict[str, str]] = {} 

386 

387 def list_buckets(self, region: str | None = None) -> list[dict[str, str]]: 

388 """Return deployed user-facing buckets under their stable aliases. 

389 

390 Global and analytics buckets are always considered. ``region`` limits 

391 regional-bucket discovery to one region; otherwise every regional 

392 deployment configured in ``cdk.json`` is considered. A bucket whose 

393 stack is not deployed is omitted, while permission and transport 

394 errors still surface to the caller. 

395 """ 

396 buckets: list[dict[str, str]] = [] 

397 

398 for alias in ("cluster-shared", "model-weights"): 

399 with suppress(StorageBucketNotFoundError): 

400 buckets.append(self.resolve_bucket(alias)) 

401 

402 regional_regions = [region] if region else self._configured_regional_regions() 

403 for regional_region in regional_regions: 

404 with suppress(StorageBucketNotFoundError): 

405 buckets.append(self.resolve_bucket("regional-shared", region=regional_region)) 

406 

407 with suppress(StorageBucketNotFoundError): 

408 buckets.append(self.resolve_bucket("analytics-studio")) 

409 

410 return buckets 

411 

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

413 """Describe every S3 bucket this deployment creates, deployed or not. 

414 

415 Complements :meth:`list_buckets`, which deliberately reports only the 

416 four user-facing buckets addressable by ``storage sync``. This reports 

417 the full set — including the server-access-log sinks and the cost-report 

418 bucket — with the deployment-contract facts a caller actually needs: 

419 which stack owns each bucket, what it is for, whether job pods can reach 

420 it and how they discover it, what happens to it on teardown, and which 

421 object-key prefixes the platform has already reserved. 

422 

423 A bucket whose stack is not deployed is reported with 

424 ``status="not-deployed"`` rather than omitted, so the answer to "what 

425 buckets does this deployment have?" is complete even before a region is 

426 rolled out. Static facts come from :data:`BUCKET_DESCRIPTORS` and need no 

427 AWS call; only the physical name, ARN, and status are resolved live. 

428 

429 ``region`` limits the regional entries to one region. Global, 

430 monitoring, and analytics entries are always included because they exist 

431 once per deployment, not once per region. 

432 

433 Note this inventories *buckets and their deployment contract*. It is 

434 unrelated to the AWS "S3 Inventory" feature, which produces scheduled 

435 reports of the objects inside a single bucket. 

436 """ 

437 project = self.config.project_name 

438 regional_regions = [region] if region else self._configured_regional_regions() 

439 account = self._account_id() 

440 

441 # One CloudFormation sweep per stack, shared by every access-logs entry 

442 # in that stack — those buckets are CDK-auto-named, so their physical 

443 # names exist only as stack resources. 

444 log_buckets: dict[tuple[str, str], str | None] = {} 

445 stacks_to_sweep: list[tuple[str, str, str]] = [ 

446 ("global", self.config.global_stack_name, self.config.global_region), 

447 ("monitoring", f"{project}-monitoring", self.config.monitoring_region), 

448 ("analytics", f"{project}-analytics", self.config.api_gateway_region), 

449 ] 

450 stacks_to_sweep.extend( 

451 (f"regional:{item}", f"{self.config.regional_stack_prefix}-{item}", item) 

452 for item in regional_regions 

453 ) 

454 for scope_key, stack_name, stack_region in stacks_to_sweep: 

455 for logical_id, physical in self._stack_bucket_resources( 

456 stack_name, stack_region 

457 ).items(): 

458 log_buckets[(scope_key, logical_id)] = physical 

459 

460 records: list[dict[str, Any]] = [] 

461 for descriptor in BUCKET_DESCRIPTORS: 

462 if descriptor.scope == "regional": 

463 for regional_region in regional_regions: 

464 records.append( 

465 self._s3_inventory_record( 

466 descriptor, 

467 region=regional_region, 

468 stack_name=f"{self.config.regional_stack_prefix}-{regional_region}", 

469 account=account, 

470 log_buckets=log_buckets, 

471 scope_key=f"regional:{regional_region}", 

472 ) 

473 ) 

474 continue 

475 

476 scope_region, stack_name = { 

477 "global": (self.config.global_region, self.config.global_stack_name), 

478 "monitoring": (self.config.monitoring_region, f"{project}-monitoring"), 

479 "analytics": (self.config.api_gateway_region, f"{project}-analytics"), 

480 }[descriptor.scope] 

481 records.append( 

482 self._s3_inventory_record( 

483 descriptor, 

484 region=scope_region, 

485 stack_name=stack_name, 

486 account=account, 

487 log_buckets=log_buckets, 

488 scope_key=descriptor.scope, 

489 ) 

490 ) 

491 

492 deployed = sum(1 for item in records if item["status"] == "deployed") 

493 return { 

494 "project_name": project, 

495 "account": account, 

496 "regions": { 

497 "global": self.config.global_region, 

498 "monitoring": self.config.monitoring_region, 

499 "analytics": self.config.api_gateway_region, 

500 "regional": regional_regions, 

501 }, 

502 "buckets": records, 

503 "summary": { 

504 "total": len(records), 

505 "deployed": deployed, 

506 "not_deployed": len(records) - deployed, 

507 "pod_writable": sorted( 

508 item["bucket"] 

509 for item in records 

510 if item["pod_access"] == "read-write" and item["bucket"] 

511 ), 

512 }, 

513 } 

514 

515 def _s3_inventory_record( 

516 self, 

517 descriptor: BucketDescriptor, 

518 *, 

519 region: str, 

520 stack_name: str, 

521 account: str | None, 

522 log_buckets: dict[tuple[str, str], str | None], 

523 scope_key: str, 

524 ) -> dict[str, Any]: 

525 """Merge one descriptor's static facts with its live name and status.""" 

526 name: str | None = None 

527 arn: str | None = None 

528 detail = "" 

529 

530 try: 

531 if descriptor.role == "access-logs": 

532 name = log_buckets.get((scope_key, descriptor.logical_id_prefix)) 

533 else: 

534 name, arn = self._resolve_primary_bucket(descriptor, region, account) 

535 except Exception as exc: # noqa: BLE001 - an unresolvable entry is reported, not fatal 

536 detail = f"could not resolve: {exc}" 

537 

538 if name and not arn: 

539 arn = f"arn:{self._partition_for(region)}:s3:::{name}" 

540 if not name and not detail: 

541 detail = ( 

542 f"{stack_name} is not deployed" 

543 if descriptor.opt_in is None 

544 else f"{stack_name} is not deployed (opt-in: {descriptor.opt_in})" 

545 ) 

546 

547 return { 

548 "id": descriptor.id if descriptor.scope != "regional" else f"{descriptor.id}:{region}", 

549 "role": descriptor.role, 

550 "scope": descriptor.scope, 

551 "region": region, 

552 "owning_stack": stack_name, 

553 "bucket": name, 

554 "arn": arn, 

555 "s3_uri": f"s3://{name}/" if name else None, 

556 "status": "deployed" if name else "not-deployed", 

557 "detail": detail, 

558 "purpose": descriptor.purpose, 

559 "pod_access": descriptor.pod_access, 

560 "discovery": descriptor.discovery, 

561 "removal_policy": _effective_removal_policy(descriptor), 

562 "reserved_prefixes": list(descriptor.reserved_prefixes), 

563 "sync_alias": ( 

564 f"{descriptor.sync_alias}:{region}" 

565 if descriptor.sync_alias and descriptor.scope == "regional" 

566 else descriptor.sync_alias 

567 ), 

568 "opt_in": descriptor.opt_in, 

569 } 

570 

571 def _resolve_primary_bucket( 

572 self, descriptor: BucketDescriptor, region: str, account: str | None 

573 ) -> tuple[str | None, str | None]: 

574 """Resolve a primary bucket's physical name and ARN, or ``(None, None)``. 

575 

576 Each family publishes its identity differently, so this routes to the 

577 contract that family actually uses rather than reconstructing names 

578 (every primary bucket is CloudFormation-named): the two shared buckets 

579 and the cost bucket publish name+ARN to SSM in their home region, the 

580 model bucket publishes its name, and the Studio bucket is only 

581 knowable from its stack's resources. 

582 """ 

583 from gco.services.aws_ssm import get_ssm_parameter_optional 

584 from gco.stacks.constants import ( 

585 cluster_shared_ssm_parameter_prefix, 

586 cost_report_ssm_parameter_prefix, 

587 regional_shared_ssm_parameter_prefix, 

588 ) 

589 

590 project = self.config.project_name 

591 

592 if descriptor.id == "cluster-shared": 

593 prefix = cluster_shared_ssm_parameter_prefix(project) 

594 return ( 

595 get_ssm_parameter_optional(f"{prefix}/name", region=region), 

596 get_ssm_parameter_optional(f"{prefix}/arn", region=region), 

597 ) 

598 if descriptor.id == "regional-shared": 

599 prefix = regional_shared_ssm_parameter_prefix(project) 

600 return ( 

601 get_ssm_parameter_optional(f"{prefix}/name", region=region), 

602 get_ssm_parameter_optional(f"{prefix}/arn", region=region), 

603 ) 

604 if descriptor.id == "model-weights": 

605 return get_ssm_parameter_optional(f"/{project}/model-bucket-name", region=region), None 

606 if descriptor.id == "cost-reports": 

607 prefix = cost_report_ssm_parameter_prefix(project) 

608 return ( 

609 get_ssm_parameter_optional(f"{prefix}/name", region=region), 

610 get_ssm_parameter_optional(f"{prefix}/arn", region=region), 

611 ) 

612 if descriptor.id == "analytics-studio": 

613 resources = self._stack_bucket_resources(f"{project}-analytics", region) 

614 return resources.get(descriptor.logical_id_prefix), None 

615 return None, None 

616 

617 def _stack_bucket_resources(self, stack_name: str, region: str) -> dict[str, str]: 

618 """Map ``logical-id-prefix -> physical bucket name`` for one stack. 

619 

620 CDK appends a hash to logical IDs, so entries are keyed by the stable 

621 construct-id prefix the descriptors declare. Returns ``{}`` when the 

622 stack is absent — an undeployed stack is an expected state here, not an 

623 error — while permission and transport failures propagate so they are 

624 never silently reported as "not deployed". 

625 """ 

626 cache_key = (stack_name, region) 

627 if cache_key in self._stack_resource_cache: 

628 return self._stack_resource_cache[cache_key] 

629 

630 prefixes = {item.logical_id_prefix for item in BUCKET_DESCRIPTORS} 

631 found: dict[str, str] = {} 

632 cfn = boto3.client("cloudformation", region_name=region) 

633 token: str | None = None 

634 try: 

635 while True: 

636 kwargs: dict[str, str] = {"StackName": stack_name} 

637 if token: 

638 kwargs["NextToken"] = token 

639 response = cfn.list_stack_resources(**kwargs) 

640 for resource in response.get("StackResourceSummaries", []): 

641 if resource.get("ResourceType") != "AWS::S3::Bucket": 

642 continue 

643 logical_id = str(resource.get("LogicalResourceId", "")) 

644 physical = resource.get("PhysicalResourceId") 

645 if not isinstance(physical, str) or not physical: 

646 continue 

647 for prefix in prefixes: 

648 if logical_id.startswith(prefix): 

649 found[prefix] = physical 

650 break 

651 token_value = response.get("NextToken") 

652 token = token_value if isinstance(token_value, str) else None 

653 if not token: 

654 break 

655 except ClientError as exc: 

656 error = exc.response.get("Error", {}) 

657 if error.get("Code") == "ValidationError" and "does not exist" in str( 

658 error.get("Message", "") 

659 ): 

660 found = {} 

661 else: 

662 raise 

663 

664 self._stack_resource_cache[cache_key] = found 

665 return found 

666 

667 def _account_id(self) -> str | None: 

668 """The caller's account id, or ``None`` when it cannot be determined. 

669 

670 ``sts:GetCallerIdentity`` needs no IAM permission, so this normally 

671 succeeds wherever credentials exist at all. 

672 """ 

673 try: 

674 identity = boto3.client("sts").get_caller_identity() 

675 value = identity.get("Account") 

676 return str(value) if value else None 

677 except Exception: # noqa: BLE001 - the inventory degrades without it 

678 return None 

679 

680 def _partition_for(self, region: str) -> str: 

681 """ARN partition for a region (aws, aws-cn, aws-us-gov).""" 

682 try: 

683 return str(boto3.Session().get_partition_for_region(region)) 

684 except Exception: # noqa: BLE001 - commercial is the right default 

685 return "aws" 

686 

687 def resolve_bucket(self, alias: str, region: str | None = None) -> dict[str, str]: 

688 """Resolve a stable alias to a physical bucket and home region. 

689 

690 Supported aliases are ``cluster-shared``, ``model-weights``, 

691 ``analytics-studio``, and either ``regional-shared:<region>`` or 

692 ``regional-shared`` with ``region`` supplied. The unqualified regional 

693 alias is inferred only when exactly one deployment region is configured. 

694 """ 

695 normalized = alias.strip().lower() 

696 embedded_region: str | None = None 

697 

698 if normalized.startswith("regional-shared:"): 

699 normalized, embedded_region = normalized.split(":", 1) 

700 embedded_region = embedded_region.strip() 

701 if not embedded_region: 

702 raise ValueError("Regional bucket alias must include a region after ':'") 

703 if region and region != embedded_region: 

704 raise ValueError( 

705 f"Alias region '{embedded_region}' conflicts with --region '{region}'" 

706 ) 

707 region = embedded_region 

708 

709 if normalized == "regional-shared": 

710 target_region = region or self._infer_single_regional_region() 

711 return self._resolve_regional_shared(target_region) 

712 

713 if region: 

714 raise ValueError("--region is only valid with the 'regional-shared' alias") 

715 

716 if normalized == "cluster-shared": 

717 return self._resolve_cluster_shared() 

718 if normalized == "model-weights": 

719 return self._resolve_model_weights() 

720 if normalized == "analytics-studio": 

721 return self._resolve_analytics_studio() 

722 

723 raise ValueError( 

724 f"Unknown bucket alias '{alias}'. Use one of: cluster-shared, " 

725 "model-weights, regional-shared:<region>, analytics-studio" 

726 ) 

727 

728 def sync( 

729 self, 

730 alias: str, 

731 local_dir: str, 

732 *, 

733 region: str | None = None, 

734 prefix: str = "", 

735 direction: str = "download", 

736 dry_run: bool = False, 

737 force: bool = False, 

738 confinement_root: str | None = None, 

739 confinement_device: int | None = None, 

740 confinement_inode: int | None = None, 

741 ) -> dict[str, Any]: 

742 """Incrementally transfer files in one explicit direction. 

743 

744 ``download`` preserves the original S3-to-local behavior. ``upload`` 

745 transfers a local file or directory into S3. Neither direction deletes 

746 destination-only files or objects. The confinement values form an 

747 internal MCP-to-CLI contract and are not public CLI options. 

748 """ 

749 normalized_direction = direction.strip().lower() 

750 if normalized_direction not in {"download", "upload"}: 

751 raise ValueError("Sync direction must be either 'download' or 'upload'") 

752 

753 contract = self._make_confinement_contract( 

754 confinement_root, 

755 confinement_device, 

756 confinement_inode, 

757 ) 

758 bucket = self.resolve_bucket(alias, region=region) 

759 normalized_prefix = self._normalize_prefix(prefix) 

760 s3 = boto3.client("s3", region_name=bucket["region"]) 

761 

762 if contract is not None: 

763 with _PinnedRoot(contract) as confinement: 

764 local_parts = confinement.relative_parts(local_dir) 

765 local_path = confinement.display_path(local_parts) 

766 if normalized_direction == "upload": 

767 return self._sync_upload( 

768 bucket, 

769 s3, 

770 local_path, 

771 normalized_prefix, 

772 dry_run=dry_run, 

773 force=force, 

774 confinement=confinement, 

775 source_parts=local_parts, 

776 ) 

777 return self._sync_download( 

778 bucket, 

779 s3, 

780 local_path, 

781 normalized_prefix, 

782 dry_run=dry_run, 

783 force=force, 

784 confinement=confinement, 

785 destination_parts=local_parts, 

786 ) 

787 

788 local_path = Path(local_dir).expanduser() 

789 if normalized_direction == "upload": 

790 return self._sync_upload( 

791 bucket, 

792 s3, 

793 local_path, 

794 normalized_prefix, 

795 dry_run=dry_run, 

796 force=force, 

797 ) 

798 return self._sync_download( 

799 bucket, 

800 s3, 

801 local_path, 

802 normalized_prefix, 

803 dry_run=dry_run, 

804 force=force, 

805 ) 

806 

807 @staticmethod 

808 def _make_confinement_contract( 

809 root: str | None, 

810 device: int | None, 

811 inode: int | None, 

812 ) -> _ConfinementContract | None: 

813 if root is None and device is None and inode is None: 

814 return None 

815 if not root or device is None or inode is None: 

816 raise ValueError("The internal storage confinement contract is incomplete") 

817 if device < 0 or inode < 0: 

818 raise ValueError("The internal storage confinement identity is invalid") 

819 root_path = Path(root).expanduser() 

820 if not root_path.is_absolute() or Path(os.path.abspath(root_path)) != root_path: 

821 raise ValueError( 

822 "The internal storage confinement root must be normalized and absolute" 

823 ) 

824 return _ConfinementContract(root=root_path, device=device, inode=inode) 

825 

826 def _sync_download( 

827 self, 

828 bucket: dict[str, str], 

829 s3: Any, 

830 destination: Path, 

831 prefix: str, 

832 *, 

833 dry_run: bool, 

834 force: bool, 

835 confinement: _PinnedRoot | None = None, 

836 destination_parts: tuple[str, ...] = (), 

837 ) -> dict[str, Any]: 

838 """Download the selected bucket prefix into a local directory.""" 

839 if confinement is None: 

840 if destination.exists() and not destination.is_dir(): 

841 raise NotADirectoryError(f"Sync destination is not a directory: {destination}") 

842 if not dry_run: 

843 destination.mkdir(parents=True, exist_ok=True) 

844 destination = destination.resolve() 

845 else: 

846 confinement.inspect_directory(destination_parts, create=not dry_run) 

847 

848 objects, directory_markers = self._build_sync_plan( 

849 s3, 

850 bucket["bucket"], 

851 prefix, 

852 destination, 

853 force=force, 

854 confinement=confinement, 

855 destination_parts=destination_parts, 

856 ) 

857 pending = [obj for obj in objects if not obj.current] 

858 current = [obj for obj in objects if obj.current] 

859 skipped = len(current) 

860 bytes_planned = sum(obj.size for obj in pending) 

861 downloaded = 0 

862 bytes_downloaded = 0 

863 

864 if not dry_run: 

865 for obj in pending: 

866 try: 

867 if confinement is not None: 

868 confinement.download_object(s3, bucket["bucket"], obj) 

869 else: 

870 obj.destination.parent.mkdir(parents=True, exist_ok=True) 

871 s3.download_file(bucket["bucket"], obj.key, str(obj.destination)) 

872 if obj.last_modified is not None: 

873 timestamp = obj.last_modified.timestamp() 

874 os.utime(obj.destination, (timestamp, timestamp)) 

875 except Exception as exc: 

876 raise RuntimeError( 

877 "Sync did not complete: failed to download " 

878 f"'s3://{bucket['bucket']}/{obj.key}' to " 

879 f"'{obj.destination}': {exc}" 

880 ) from exc 

881 downloaded += 1 

882 bytes_downloaded += obj.size 

883 

884 for obj in current: 

885 if confinement is not None: 

886 if obj.destination_parts is None: # pragma: no cover - internal invariant 

887 raise RuntimeError("Missing confined destination components") 

888 still_current = confinement.download_target_is_current( 

889 obj.destination_parts, 

890 obj.size, 

891 obj.last_modified, 

892 evaluate_current=True, 

893 ) 

894 else: 

895 still_current = self._is_current( 

896 obj.destination, 

897 obj.size, 

898 obj.last_modified, 

899 ) 

900 if not still_current: 

901 raise RuntimeError( 

902 "Sync did not complete: a skipped local file changed after planning: " 

903 f"{obj.destination}" 

904 ) 

905 

906 source = f"s3://{bucket['bucket']}/{prefix}" 

907 return { 

908 "alias": bucket["alias"], 

909 "bucket": bucket["bucket"], 

910 "region": bucket["region"], 

911 "direction": "download", 

912 "source": source, 

913 "destination": str(destination), 

914 "prefix": prefix, 

915 "dry_run": dry_run, 

916 "force": force, 

917 "objects_scanned": len(objects) + directory_markers, 

918 "directory_markers": directory_markers, 

919 "files_planned": len(pending), 

920 "files_downloaded": downloaded, 

921 "files_skipped": skipped, 

922 "bytes_planned": bytes_planned, 

923 "bytes_downloaded": bytes_downloaded, 

924 } 

925 

926 def _sync_upload( 

927 self, 

928 bucket: dict[str, str], 

929 s3: Any, 

930 source: Path, 

931 prefix: str, 

932 *, 

933 dry_run: bool, 

934 force: bool, 

935 confinement: _PinnedRoot | None = None, 

936 source_parts: tuple[str, ...] = (), 

937 ) -> dict[str, Any]: 

938 """Upload a local file or directory into the selected bucket prefix.""" 

939 if confinement is None: 

940 if source.is_symlink(): 

941 raise ValueError(f"Upload source must not be a symbolic link: {source}") 

942 if not source.exists(): 

943 raise FileNotFoundError(f"Upload source not found: {source}") 

944 if not source.is_file() and not source.is_dir(): 

945 raise ValueError(f"Upload source must be a regular file or directory: {source}") 

946 source = source.resolve() 

947 

948 objects, remote_objects_probed = self._build_upload_plan( 

949 s3, 

950 bucket["bucket"], 

951 prefix, 

952 source, 

953 force=force, 

954 confinement=confinement, 

955 source_parts=source_parts, 

956 ) 

957 pending = [obj for obj in objects if not obj.current] 

958 current = [obj for obj in objects if obj.current] 

959 skipped = len(current) 

960 bytes_planned = sum(obj.size for obj in pending) 

961 uploaded = 0 

962 bytes_uploaded = 0 

963 

964 if not dry_run: 

965 for obj in pending: 

966 try: 

967 source_fd = self._open_upload_source(obj, confinement) 

968 try: 

969 if self._stat_signature(os.fstat(source_fd)) != obj.signature: 

970 raise RuntimeError(f"Local file changed after planning: {obj.source}") 

971 # Managed single-part uploads close their input stream. Keep the 

972 # securely opened descriptor alive for the post-transfer signature 

973 # check while allowing s3transfer to close its stream normally. 

974 with open(source_fd, "rb", closefd=False) as source_file: 

975 s3.upload_fileobj( 

976 source_file, 

977 bucket["bucket"], 

978 obj.key, 

979 ExtraArgs={ 

980 "Metadata": {self._UPLOAD_DIGEST_METADATA: obj.sha256}, 

981 "ChecksumSHA256": base64.b64encode( 

982 bytes.fromhex(obj.sha256) 

983 ).decode("ascii"), 

984 }, 

985 ) 

986 if self._stat_signature(os.fstat(source_fd)) != obj.signature: 

987 raise RuntimeError(f"Local file changed during upload: {obj.source}") 

988 finally: 

989 os.close(source_fd) 

990 # A descriptor remains valid if its pathname is renamed away. Reopen 

991 # after upload so a raced path replacement cannot be reported current. 

992 self._verify_upload_source_signature( 

993 obj, 

994 confinement, 

995 skipped=False, 

996 ) 

997 except Exception as exc: 

998 raise RuntimeError( 

999 "Sync did not complete: failed to upload " 

1000 f"'{obj.source}' to 's3://{bucket['bucket']}/{obj.key}': {exc}" 

1001 ) from exc 

1002 uploaded += 1 

1003 bytes_uploaded += obj.size 

1004 

1005 for obj in current: 

1006 source_fd = self._open_upload_source(obj, confinement) 

1007 try: 

1008 if self._stat_signature(os.fstat(source_fd)) != obj.signature: 

1009 raise RuntimeError( 

1010 f"A skipped local file changed after planning: {obj.source}" 

1011 ) 

1012 remote_objects_probed += 1 

1013 remote_current = self._remote_digest_matches( 

1014 s3, 

1015 bucket["bucket"], 

1016 obj.key, 

1017 obj.size, 

1018 obj.sha256, 

1019 ) 

1020 if self._stat_signature(os.fstat(source_fd)) != obj.signature: 

1021 raise RuntimeError( 

1022 f"A skipped local file changed during revalidation: {obj.source}" 

1023 ) 

1024 finally: 

1025 os.close(source_fd) 

1026 

1027 # Reopen after the remote probe so a renamed source path cannot 

1028 # be reported current merely because its old descriptor is stable. 

1029 self._verify_upload_source_signature( 

1030 obj, 

1031 confinement, 

1032 skipped=True, 

1033 ) 

1034 if not remote_current: 

1035 raise RuntimeError( 

1036 "Sync did not complete: a skipped S3 object changed after planning: " 

1037 f"s3://{bucket['bucket']}/{obj.key}" 

1038 ) 

1039 

1040 destination = f"s3://{bucket['bucket']}/{prefix}" 

1041 return { 

1042 "alias": bucket["alias"], 

1043 "bucket": bucket["bucket"], 

1044 "region": bucket["region"], 

1045 "direction": "upload", 

1046 "source": str(source), 

1047 "destination": destination, 

1048 "prefix": prefix, 

1049 "dry_run": dry_run, 

1050 "force": force, 

1051 "files_scanned": len(objects), 

1052 "objects_scanned": len(objects), 

1053 "objects_probed": remote_objects_probed, 

1054 "files_planned": len(pending), 

1055 "files_uploaded": uploaded, 

1056 "files_skipped": skipped, 

1057 "bytes_planned": bytes_planned, 

1058 "bytes_uploaded": bytes_uploaded, 

1059 } 

1060 

1061 def _resolve_cluster_shared(self) -> dict[str, str]: 

1062 from gco.services.aws_ssm import get_ssm_parameter_optional 

1063 from gco.stacks.constants import cluster_shared_ssm_parameter_prefix 

1064 

1065 prefix = cluster_shared_ssm_parameter_prefix(self.config.project_name) 

1066 name = get_ssm_parameter_optional( 

1067 f"{prefix}/name", 

1068 region=self.config.global_region, 

1069 ) 

1070 if not name: 

1071 raise StorageBucketNotFoundError( 

1072 "Cluster shared bucket not found. Deploy the global stack first." 

1073 ) 

1074 bucket_region = get_ssm_parameter_optional( 

1075 f"{prefix}/region", 

1076 region=self.config.global_region, 

1077 ) 

1078 return self._bucket_record( 

1079 alias="cluster-shared", 

1080 name=name, 

1081 region=bucket_region or self.config.global_region, 

1082 scope="global", 

1083 ) 

1084 

1085 def _resolve_model_weights(self) -> dict[str, str]: 

1086 from gco.services.aws_ssm import get_ssm_parameter_optional 

1087 

1088 name = get_ssm_parameter_optional( 

1089 f"/{self.config.project_name}/model-bucket-name", 

1090 region=self.config.global_region, 

1091 ) 

1092 if not name: 

1093 raise StorageBucketNotFoundError( 

1094 "Model weights bucket not found. Deploy the global stack first." 

1095 ) 

1096 return self._bucket_record( 

1097 alias="model-weights", 

1098 name=name, 

1099 region=self.config.global_region, 

1100 scope="global", 

1101 ) 

1102 

1103 def _resolve_regional_shared(self, region: str) -> dict[str, str]: 

1104 from gco.services.aws_ssm import get_ssm_parameter_optional 

1105 from gco.stacks.constants import regional_shared_ssm_parameter_prefix 

1106 

1107 prefix = regional_shared_ssm_parameter_prefix(self.config.project_name) 

1108 name = get_ssm_parameter_optional(f"{prefix}/name", region=region) 

1109 if not name: 

1110 raise StorageBucketNotFoundError( 

1111 f"Regional shared bucket not found in region '{region}'. " 

1112 "Deploy that region's stack first." 

1113 ) 

1114 bucket_region = get_ssm_parameter_optional(f"{prefix}/region", region=region) 

1115 return self._bucket_record( 

1116 alias=f"regional-shared:{region}", 

1117 name=name, 

1118 region=bucket_region or region, 

1119 scope="regional", 

1120 ) 

1121 

1122 def _resolve_analytics_studio(self) -> dict[str, str]: 

1123 region = self.config.api_gateway_region 

1124 stack_name = f"{self.config.project_name}-analytics" 

1125 cfn = boto3.client("cloudformation", region_name=region) 

1126 token: str | None = None 

1127 

1128 try: 

1129 while True: 

1130 kwargs: dict[str, str] = {"StackName": stack_name} 

1131 if token: 

1132 kwargs["NextToken"] = token 

1133 response = cfn.list_stack_resources(**kwargs) 

1134 for resource in response.get("StackResourceSummaries", []): 

1135 logical_id = str(resource.get("LogicalResourceId", "")) 

1136 if resource.get("ResourceType") == "AWS::S3::Bucket" and logical_id.startswith( 

1137 "StudioOnlyBucket" 

1138 ): 

1139 name = resource.get("PhysicalResourceId") 

1140 if isinstance(name, str) and name: 

1141 return self._bucket_record( 

1142 alias="analytics-studio", 

1143 name=name, 

1144 region=region, 

1145 scope="analytics", 

1146 ) 

1147 token_value = response.get("NextToken") 

1148 token = token_value if isinstance(token_value, str) else None 

1149 if not token: 

1150 break 

1151 except ClientError as exc: 

1152 error = exc.response.get("Error", {}) 

1153 if error.get("Code") == "ValidationError" and "does not exist" in str( 

1154 error.get("Message", "") 

1155 ): 

1156 raise StorageBucketNotFoundError( 

1157 "Analytics Studio bucket not found. Deploy the analytics stack first." 

1158 ) from exc 

1159 raise 

1160 

1161 raise StorageBucketNotFoundError( 

1162 "Analytics Studio bucket not found in the deployed analytics stack." 

1163 ) 

1164 

1165 def _configured_regional_regions(self) -> list[str]: 

1166 from .config import _load_cdk_json 

1167 

1168 configured = _load_cdk_json().get("regional", []) 

1169 candidates = configured if isinstance(configured, list) else [] 

1170 regions: list[str] = [] 

1171 seen: set[str] = set() 

1172 for value in candidates: 

1173 if isinstance(value, str) and value and value not in seen: 

1174 regions.append(value) 

1175 seen.add(value) 

1176 return regions or [self.config.default_region] 

1177 

1178 def _infer_single_regional_region(self) -> str: 

1179 regions = self._configured_regional_regions() 

1180 if len(regions) == 1: 

1181 return regions[0] 

1182 choices = ", ".join(f"regional-shared:{item}" for item in regions) 

1183 raise ValueError( 

1184 "The 'regional-shared' alias is ambiguous across configured regions. " 

1185 f"Use --region or one of: {choices}" 

1186 ) 

1187 

1188 def _bucket_record(self, *, alias: str, name: str, region: str, scope: str) -> dict[str, str]: 

1189 purpose_key = "regional-shared" if alias.startswith("regional-shared:") else alias 

1190 return { 

1191 "alias": alias, 

1192 "bucket": name, 

1193 "region": region, 

1194 "scope": scope, 

1195 "purpose": self._PURPOSES[purpose_key], 

1196 "s3_uri": f"s3://{name}/", 

1197 } 

1198 

1199 @staticmethod 

1200 def _normalize_prefix(prefix: str) -> str: 

1201 normalized = prefix.lstrip("/") 

1202 if normalized and not normalized.endswith("/"): 

1203 normalized += "/" 

1204 return normalized 

1205 

1206 @staticmethod 

1207 def _stat_signature(value: os.stat_result) -> _FileSignature: 

1208 return ( 

1209 value.st_dev, 

1210 value.st_ino, 

1211 value.st_size, 

1212 value.st_mtime_ns, 

1213 value.st_ctime_ns, 

1214 ) 

1215 

1216 @classmethod 

1217 def _hash_upload_fd(cls, file_fd: int, path: Path) -> tuple[str, _FileSignature]: 

1218 with os.fdopen(file_fd, "rb") as source_file: 

1219 before = os.fstat(source_file.fileno()) 

1220 if not stat.S_ISREG(before.st_mode): 

1221 raise ValueError(f"Upload source is not a regular file: {path}") 

1222 before_signature = cls._stat_signature(before) 

1223 digest = hashlib.sha256() 

1224 while chunk := source_file.read(8 * 1024 * 1024): 

1225 digest.update(chunk) 

1226 after_signature = cls._stat_signature(os.fstat(source_file.fileno())) 

1227 if before_signature != after_signature: 

1228 raise RuntimeError(f"Local file changed while planning upload: {path}") 

1229 return digest.hexdigest(), before_signature 

1230 

1231 @classmethod 

1232 def _hash_upload_file(cls, path: Path) -> tuple[str, _FileSignature]: 

1233 flags = ( 

1234 os.O_RDONLY 

1235 | getattr(os, "O_BINARY", 0) 

1236 | getattr(os, "O_NOFOLLOW", 0) 

1237 | getattr(os, "O_CLOEXEC", 0) 

1238 ) 

1239 return cls._hash_upload_fd(os.open(path, flags), path) 

1240 

1241 @staticmethod 

1242 def _validate_upload_relative_path(relative: str, source: Path) -> None: 

1243 parts = relative.split("/") 

1244 if ( 

1245 not relative 

1246 or "\x00" in relative 

1247 or "\\" in relative 

1248 or any(part in ("", ".", "..") for part in parts) 

1249 ): 

1250 raise ValueError(f"Local path cannot be represented safely as an S3 key: {source}") 

1251 

1252 @classmethod 

1253 def _collect_upload_files(cls, source: Path) -> list[_PreparedUpload]: 

1254 paths: list[tuple[Path, str]] = [] 

1255 if source.is_file(): 

1256 relative = source.name 

1257 cls._validate_upload_relative_path(relative, source) 

1258 paths.append((source, relative)) 

1259 else: 

1260 

1261 def raise_walk_error(error: OSError) -> None: 

1262 raise error 

1263 

1264 for root, directories, names in os.walk( 

1265 source, 

1266 topdown=True, 

1267 onerror=raise_walk_error, 

1268 followlinks=False, 

1269 ): 

1270 root_path = Path(root) 

1271 for directory_name in directories: 

1272 directory = root_path / directory_name 

1273 if directory.is_symlink(): 

1274 raise ValueError(f"Upload source contains a symbolic link: {directory}") 

1275 for name in names: 

1276 path = root_path / name 

1277 if path.is_symlink(): 

1278 raise ValueError(f"Upload source contains a symbolic link: {path}") 

1279 if not path.is_file(): 

1280 raise ValueError(f"Upload source contains a non-regular file: {path}") 

1281 relative = path.relative_to(source).as_posix() 

1282 cls._validate_upload_relative_path(relative, path) 

1283 paths.append((path, relative)) 

1284 

1285 prepared: list[_PreparedUpload] = [] 

1286 for path, relative in sorted(paths, key=lambda item: item[1]): 

1287 digest, signature = cls._hash_upload_file(path) 

1288 prepared.append( 

1289 _PreparedUpload( 

1290 source=path, 

1291 relative=relative, 

1292 source_parts=None, 

1293 size=signature[2], 

1294 sha256=digest, 

1295 signature=signature, 

1296 ) 

1297 ) 

1298 return prepared 

1299 

1300 @classmethod 

1301 def _collect_confined_upload_files( 

1302 cls, 

1303 confinement: _PinnedRoot, 

1304 source_parts: tuple[str, ...], 

1305 ) -> list[_PreparedUpload]: 

1306 source = confinement.display_path(source_parts) 

1307 source_stat = confinement.lstat(source_parts) 

1308 if source_stat is None: 

1309 raise FileNotFoundError(f"Upload source not found: {source}") 

1310 if stat.S_ISLNK(source_stat.st_mode): 

1311 raise ValueError(f"Upload source must not be a symbolic link: {source}") 

1312 if stat.S_ISREG(source_stat.st_mode): 

1313 relative = source.name 

1314 cls._validate_upload_relative_path(relative, source) 

1315 digest, signature = cls._hash_upload_fd( 

1316 confinement.open_regular_file(source_parts), 

1317 source, 

1318 ) 

1319 return [ 

1320 _PreparedUpload( 

1321 source=source, 

1322 relative=relative, 

1323 source_parts=source_parts, 

1324 size=signature[2], 

1325 sha256=digest, 

1326 signature=signature, 

1327 ) 

1328 ] 

1329 if not stat.S_ISDIR(source_stat.st_mode): 

1330 raise ValueError(f"Upload source must be a regular file or directory: {source}") 

1331 

1332 source_fd = confinement.open_directory(source_parts) 

1333 if source_fd is None: # pragma: no cover - allow_missing is false 

1334 raise FileNotFoundError(source) 

1335 prepared: list[_PreparedUpload] = [] 

1336 try: 

1337 cls._walk_confined_upload_directory( 

1338 confinement, 

1339 source_fd, 

1340 source_parts, 

1341 (), 

1342 prepared, 

1343 ) 

1344 finally: 

1345 os.close(source_fd) 

1346 return sorted(prepared, key=lambda item: item.relative) 

1347 

1348 @classmethod 

1349 def _walk_confined_upload_directory( 

1350 cls, 

1351 confinement: _PinnedRoot, 

1352 directory_fd: int, 

1353 directory_parts: tuple[str, ...], 

1354 relative_parts: tuple[str, ...], 

1355 prepared: list[_PreparedUpload], 

1356 ) -> None: 

1357 """Enumerate and open one source directory through already-pinned descriptors.""" 

1358 for name in sorted(os.listdir(directory_fd)): 

1359 child_parts = directory_parts + (name,) 

1360 child_relative_parts = relative_parts + (name,) 

1361 child = confinement.display_path(child_parts) 

1362 relative = "/".join(child_relative_parts) 

1363 cls._validate_upload_relative_path(relative, child) 

1364 try: 

1365 child_stat = os.stat(name, dir_fd=directory_fd, follow_symlinks=False) 

1366 except FileNotFoundError as exc: 

1367 raise RuntimeError(f"Upload source changed during enumeration: {child}") from exc 

1368 

1369 if stat.S_ISLNK(child_stat.st_mode): 

1370 raise ValueError(f"Upload source contains a symbolic link: {child}") 

1371 if stat.S_ISDIR(child_stat.st_mode): 

1372 child_fd = confinement.open_child_directory(directory_fd, name, child) 

1373 try: 

1374 cls._walk_confined_upload_directory( 

1375 confinement, 

1376 child_fd, 

1377 child_parts, 

1378 child_relative_parts, 

1379 prepared, 

1380 ) 

1381 finally: 

1382 os.close(child_fd) 

1383 continue 

1384 if not stat.S_ISREG(child_stat.st_mode): 

1385 raise ValueError(f"Upload source contains a non-regular file: {child}") 

1386 

1387 digest, signature = cls._hash_upload_fd( 

1388 confinement.open_child_regular_file(directory_fd, name, child), 

1389 child, 

1390 ) 

1391 prepared.append( 

1392 _PreparedUpload( 

1393 source=child, 

1394 relative=relative, 

1395 source_parts=child_parts, 

1396 size=signature[2], 

1397 sha256=digest, 

1398 signature=signature, 

1399 ) 

1400 ) 

1401 

1402 @staticmethod 

1403 def _open_upload_source(obj: _UploadObject, confinement: _PinnedRoot | None) -> int: 

1404 if confinement is not None: 

1405 if obj.source_parts is None: # pragma: no cover - internal invariant 

1406 raise RuntimeError("Missing confined upload source components") 

1407 return confinement.open_regular_file(obj.source_parts) 

1408 flags = ( 

1409 os.O_RDONLY 

1410 | getattr(os, "O_BINARY", 0) 

1411 | getattr(os, "O_NOFOLLOW", 0) 

1412 | getattr(os, "O_CLOEXEC", 0) 

1413 ) 

1414 file_fd = os.open(obj.source, flags) 

1415 if not stat.S_ISREG(os.fstat(file_fd).st_mode): 

1416 os.close(file_fd) 

1417 raise ValueError(f"Upload source is not a regular file: {obj.source}") 

1418 return file_fd 

1419 

1420 @classmethod 

1421 def _verify_upload_source_signature( 

1422 cls, 

1423 obj: _UploadObject, 

1424 confinement: _PinnedRoot | None, 

1425 *, 

1426 skipped: bool, 

1427 ) -> None: 

1428 source_fd = cls._open_upload_source(obj, confinement) 

1429 try: 

1430 if cls._stat_signature(os.fstat(source_fd)) != obj.signature: 

1431 description = "A skipped local file" if skipped else "Local file" 

1432 raise RuntimeError(f"{description} changed after planning: {obj.source}") 

1433 finally: 

1434 os.close(source_fd) 

1435 

1436 def _remote_digest_matches( 

1437 self, 

1438 s3: Any, 

1439 bucket: str, 

1440 key: str, 

1441 size: int, 

1442 digest: str, 

1443 ) -> bool: 

1444 try: 

1445 response = s3.head_object(Bucket=bucket, Key=key) 

1446 except ClientError as exc: 

1447 error = exc.response.get("Error", {}) 

1448 status = exc.response.get("ResponseMetadata", {}).get("HTTPStatusCode") 

1449 if error.get("Code") in { 

1450 "403", 

1451 "404", 

1452 "AccessDenied", 

1453 "NoSuchKey", 

1454 "NotFound", 

1455 } or status in {403, 404}: 

1456 # S3 returns 403 rather than 404 for a missing key when the 

1457 # caller intentionally lacks ListBucket. Treat it as not 

1458 # current and let PutObject enforce write authorization. 

1459 return False 

1460 raise 

1461 

1462 if int(response.get("ContentLength", -1)) != size: 

1463 return False 

1464 metadata = response.get("Metadata", {}) 

1465 if not isinstance(metadata, dict): 

1466 return False 

1467 remote_digest = next( 

1468 ( 

1469 str(value) 

1470 for name, value in metadata.items() 

1471 if str(name).lower() == self._UPLOAD_DIGEST_METADATA 

1472 ), 

1473 "", 

1474 ) 

1475 return remote_digest.strip().lower() == digest 

1476 

1477 def _build_upload_plan( 

1478 self, 

1479 s3: Any, 

1480 bucket: str, 

1481 prefix: str, 

1482 source: Path, 

1483 *, 

1484 force: bool, 

1485 confinement: _PinnedRoot | None, 

1486 source_parts: tuple[str, ...], 

1487 ) -> tuple[list[_UploadObject], int]: 

1488 if confinement is None: 

1489 files = self._collect_upload_files(source) 

1490 else: 

1491 files = self._collect_confined_upload_files(confinement, source_parts) 

1492 

1493 objects: list[_UploadObject] = [] 

1494 remote_objects_probed = 0 

1495 for prepared in files: 

1496 key = f"{prefix}{prepared.relative}" 

1497 current = False 

1498 if not force: 

1499 remote_objects_probed += 1 

1500 current = self._remote_digest_matches( 

1501 s3, 

1502 bucket, 

1503 key, 

1504 prepared.size, 

1505 prepared.sha256, 

1506 ) 

1507 objects.append( 

1508 _UploadObject( 

1509 source=prepared.source, 

1510 source_parts=prepared.source_parts, 

1511 key=key, 

1512 size=prepared.size, 

1513 sha256=prepared.sha256, 

1514 signature=prepared.signature, 

1515 current=current, 

1516 ) 

1517 ) 

1518 

1519 return objects, remote_objects_probed 

1520 

1521 def _build_sync_plan( 

1522 self, 

1523 s3: Any, 

1524 bucket: str, 

1525 prefix: str, 

1526 destination: Path, 

1527 *, 

1528 force: bool, 

1529 confinement: _PinnedRoot | None, 

1530 destination_parts: tuple[str, ...], 

1531 ) -> tuple[list[_SyncObject], int]: 

1532 objects: list[_SyncObject] = [] 

1533 directory_markers = 0 

1534 paginator = s3.get_paginator("list_objects_v2") 

1535 

1536 for page in paginator.paginate(Bucket=bucket, Prefix=prefix): 

1537 for item in page.get("Contents", []): 

1538 key = str(item.get("Key", "")) 

1539 if not key: 

1540 raise ValueError("S3 returned an object with an empty key") 

1541 size = int(item.get("Size", 0)) 

1542 if key.endswith("/"): 

1543 if size != 0: 

1544 raise ValueError( 

1545 "S3 object keys ending in '/' cannot be represented as local files: " 

1546 f"{key!r}" 

1547 ) 

1548 directory_markers += 1 

1549 continue 

1550 

1551 relative_key = key[len(prefix) :] if prefix else key 

1552 key_parts = self._download_relative_parts(relative_key, key) 

1553 modified_value = item.get("LastModified") 

1554 modified = modified_value if isinstance(modified_value, datetime) else None 

1555 if confinement is None: 

1556 local_path = self._safe_local_path(destination, relative_key, key) 

1557 local_parts: tuple[str, ...] | None = None 

1558 current = False if force else self._is_current(local_path, size, modified) 

1559 else: 

1560 local_parts = destination_parts + key_parts 

1561 local_path = confinement.display_path(local_parts) 

1562 current = confinement.download_target_is_current( 

1563 local_parts, 

1564 size, 

1565 modified, 

1566 evaluate_current=not force, 

1567 ) 

1568 objects.append( 

1569 _SyncObject( 

1570 key=key, 

1571 destination=local_path, 

1572 destination_parts=local_parts, 

1573 size=size, 

1574 last_modified=modified, 

1575 current=current, 

1576 ) 

1577 ) 

1578 

1579 self._validate_sync_plan(objects) 

1580 return objects, directory_markers 

1581 

1582 @staticmethod 

1583 def _validate_windows_download_part(part: str, source_key: str) -> None: 

1584 if not sys.platform.startswith("win"): 

1585 return 

1586 if part.endswith((".", " ")): 

1587 raise ValueError(f"Unsafe Windows S3 object key cannot be synced: {source_key!r}") 

1588 if any( 

1589 unicodedata.category(character) == "Cc" or character in '<>:"|?*' for character in part 

1590 ): 

1591 raise ValueError(f"Unsafe Windows S3 object key cannot be synced: {source_key!r}") 

1592 

1593 stem = part.split(".", 1)[0].rstrip(" .").casefold() 

1594 reserved = {"con", "prn", "aux", "nul", "clock$", "conin$", "conout$"} 

1595 numbered_suffixes = "123456789¹²³" 

1596 if stem in reserved or ( 

1597 len(stem) == 4 and stem[:3] in {"com", "lpt"} and stem[3] in numbered_suffixes 

1598 ): 

1599 raise ValueError(f"Reserved Windows path cannot be synced: {source_key!r}") 

1600 

1601 @classmethod 

1602 def _download_relative_parts(cls, relative_key: str, source_key: str) -> tuple[str, ...]: 

1603 if "\x00" in relative_key or "\\" in relative_key: 

1604 raise ValueError(f"Unsafe S3 object key cannot be synced: {source_key!r}") 

1605 parts = tuple(relative_key.split("/")) 

1606 if not relative_key or any(part in ("", ".", "..") for part in parts): 

1607 raise ValueError(f"Unsafe S3 object key cannot be synced: {source_key!r}") 

1608 for part in parts: 

1609 cls._validate_windows_download_part(part, source_key) 

1610 return parts 

1611 

1612 @staticmethod 

1613 def _local_collision_parts(path: Path) -> tuple[str, ...]: 

1614 """Return path components normalized conservatively for the host filesystem.""" 

1615 if sys.platform.startswith("win"): 

1616 return tuple(unicodedata.normalize("NFC", part).casefold() for part in path.parts) 

1617 parts = tuple(os.path.normcase(part) for part in path.parts) 

1618 if sys.platform == "darwin": 

1619 # Default macOS volumes compare names case-insensitively and apply 

1620 # Unicode normalization even though os.path.normcase is a no-op. 

1621 return tuple(unicodedata.normalize("NFD", part).casefold() for part in parts) 

1622 return parts 

1623 

1624 @classmethod 

1625 def _validate_sync_plan(cls, objects: list[_SyncObject]) -> None: 

1626 """Reject object sets that cannot be represented without collisions.""" 

1627 seen: dict[tuple[str, ...], _SyncObject] = {} 

1628 ordered = sorted(objects, key=lambda obj: len(obj.destination.parts)) 

1629 for obj in ordered: 

1630 collision_key = cls._local_collision_parts(obj.destination) 

1631 conflicting = seen.get(collision_key) 

1632 if conflicting is not None: 

1633 raise ValueError( 

1634 "S3 object keys map to the same local path: " 

1635 f"{conflicting.key!r} and {obj.key!r}" 

1636 ) 

1637 for length in range(1, len(collision_key)): 

1638 ancestor = seen.get(collision_key[:length]) 

1639 if ancestor is not None: 

1640 raise ValueError( 

1641 "S3 object keys have a local file/directory collision: " 

1642 f"{ancestor.key!r} and {obj.key!r}" 

1643 ) 

1644 seen[collision_key] = obj 

1645 

1646 @classmethod 

1647 def _safe_local_path(cls, destination: Path, relative_key: str, source_key: str) -> Path: 

1648 parts = cls._download_relative_parts(relative_key, source_key) 

1649 candidate = destination.joinpath(*parts).resolve() 

1650 if candidate == destination or not candidate.is_relative_to(destination): 

1651 raise ValueError(f"S3 object key escapes the sync destination: {source_key!r}") 

1652 parent = candidate.parent 

1653 while parent != destination: 

1654 if parent.exists() and not parent.is_dir(): 

1655 raise NotADirectoryError( 

1656 f"S3 object '{source_key}' has a local parent that is not a directory: {parent}" 

1657 ) 

1658 parent = parent.parent 

1659 if candidate.exists() and candidate.is_dir(): 

1660 raise IsADirectoryError( 

1661 f"S3 object '{source_key}' maps to an existing directory: {candidate}" 

1662 ) 

1663 return candidate 

1664 

1665 @staticmethod 

1666 def _is_current(path: Path, size: int, modified: datetime | None) -> bool: 

1667 if not path.is_file() or modified is None: 

1668 return False 

1669 path_stat = path.stat() 

1670 return path_stat.st_size == size and int(path_stat.st_mtime) >= int(modified.timestamp()) 

1671 

1672 

1673def _regional_shared_removal_policy() -> str: 

1674 """The configured regional-shared teardown policy, read tolerantly. 

1675 

1676 Mirrors the synth-time read of ``cdk.json::regional_shared_bucket. 

1677 removal_policy`` in ``gco/stacks/regional_stack.py`` so ``gco storage 

1678 s3-inventory`` reports the policy the next deploy will apply. The 

1679 inventory is a read-only report, so unlike synthesis (which fails 

1680 loudly on an invalid value) this degrades to the shipped default 

1681 rather than crashing on a hand-edited or missing cdk.json. 

1682 """ 

1683 import json 

1684 

1685 try: 

1686 from .stacks import _find_cdk_json 

1687 

1688 cdk_json_path = _find_cdk_json() 

1689 if cdk_json_path is None: 

1690 return "destroy" 

1691 with open(cdk_json_path, encoding="utf-8") as config_file: 

1692 cdk_config = json.load(config_file) 

1693 block = cdk_config.get("context", {}).get("regional_shared_bucket") or {} 

1694 value = str(block.get("removal_policy", "destroy")).strip().lower() 

1695 except Exception: 

1696 return "destroy" 

1697 return value if value in ("destroy", "retain") else "destroy" 

1698 

1699 

1700def _effective_removal_policy(descriptor: BucketDescriptor) -> str: 

1701 """A descriptor's teardown policy after applying cdk.json configuration. 

1702 

1703 The regional-shared bucket family is the one whose removal policy is 

1704 deploy-time configurable; every other bucket's policy is a fixed 

1705 property of the design. 

1706 """ 

1707 if descriptor.id in ("regional-shared", "regional-shared-access-logs"): 

1708 return _regional_shared_removal_policy() 

1709 return descriptor.removal_policy 

1710 

1711 

1712@dataclass(frozen=True) 

1713class BucketDescriptor: 

1714 """The deployment-contract facts about one bucket the stacks create. 

1715 

1716 Everything here is a property of the *design* — which stack owns the 

1717 bucket, what it is for, whether job pods can reach it, and what happens to 

1718 it on teardown — so it is knowable without an AWS call. The physical name, 

1719 ARN, and deployed/absent status are resolved separately at inventory time. 

1720 

1721 Keeping the two apart is deliberate: an operator asking "what buckets does 

1722 this deployment have and which can my pods write to?" gets a complete 

1723 answer even for a region that has not been deployed yet, with each entry 

1724 marked ``not-deployed`` rather than silently missing. 

1725 """ 

1726 

1727 id: str 

1728 role: str 

1729 scope: str 

1730 purpose: str 

1731 pod_access: str 

1732 discovery: str 

1733 removal_policy: str 

1734 logical_id_prefix: str 

1735 sync_alias: str | None = None 

1736 reserved_prefixes: tuple[str, ...] = () 

1737 opt_in: str | None = None 

1738 

1739 

1740#: Every bucket the GCO stacks create, in reporting order. ``scope`` decides 

1741#: which stack and region an entry resolves against; ``role`` separates the 

1742#: buckets workloads use from the server-access-log sinks that exist only to 

1743#: satisfy the "every bucket must log" control. 

1744BUCKET_DESCRIPTORS: tuple[BucketDescriptor, ...] = ( 

1745 BucketDescriptor( 

1746 id="cluster-shared", 

1747 role="primary", 

1748 scope="global", 

1749 purpose="Always-on central bucket every regional cluster can read and write", 

1750 pod_access="read-write", 

1751 discovery="gco-cluster-shared-bucket ConfigMap (sharedBucketName) in gco-jobs/gco-system/gco-inference", 

1752 removal_policy="destroy", 

1753 logical_id_prefix="ClusterSharedBucket", 

1754 sync_alias="cluster-shared", 

1755 reserved_prefixes=("mlflow-artifacts/", "analytics-data/", "vector-corpus/"), 

1756 ), 

1757 BucketDescriptor( 

1758 id="cluster-shared-access-logs", 

1759 role="access-logs", 

1760 scope="global", 

1761 purpose="Server access logs for the cluster-shared bucket", 

1762 pod_access="none", 

1763 discovery="CloudFormation resource of the global stack (CDK-generated name)", 

1764 removal_policy="destroy", 

1765 logical_id_prefix="ClusterSharedAccessLogsBucket", 

1766 ), 

1767 BucketDescriptor( 

1768 id="model-weights", 

1769 role="primary", 

1770 scope="global", 

1771 purpose="Central model weights pulled by inference init containers", 

1772 pod_access="read-only", 

1773 discovery="SSM /<project>/model-bucket-name in the global region", 

1774 removal_policy="destroy", 

1775 logical_id_prefix="ModelWeightsBucket", 

1776 sync_alias="model-weights", 

1777 ), 

1778 BucketDescriptor( 

1779 id="model-weights-access-logs", 

1780 role="access-logs", 

1781 scope="global", 

1782 purpose="Server access logs for the model weights bucket", 

1783 pod_access="none", 

1784 discovery="CloudFormation resource of the global stack (CDK-generated name)", 

1785 removal_policy="destroy", 

1786 logical_id_prefix="ModelWeightsAccessLogsBucket", 

1787 ), 

1788 BucketDescriptor( 

1789 id="regional-shared", 

1790 role="primary", 

1791 scope="regional", 

1792 purpose="Always-on general-purpose in-region bucket; no cross-region egress", 

1793 pod_access="read-write", 

1794 discovery="gco-regional-shared-bucket ConfigMap (regionalBucketName) in gco-jobs/gco-system/gco-inference", 

1795 removal_policy="destroy", 

1796 logical_id_prefix="RegionalSharedBucket", 

1797 sync_alias="regional-shared", 

1798 reserved_prefixes=("mooncake-kv/",), 

1799 ), 

1800 BucketDescriptor( 

1801 id="regional-shared-access-logs", 

1802 role="access-logs", 

1803 scope="regional", 

1804 purpose="Server access logs for that region's regional-shared bucket", 

1805 pod_access="none", 

1806 discovery="CloudFormation resource of the regional stack (CDK-generated name)", 

1807 removal_policy="destroy", 

1808 logical_id_prefix="RegionalSharedAccessLogsBucket", 

1809 ), 

1810 BucketDescriptor( 

1811 id="cost-reports", 

1812 role="primary", 

1813 scope="monitoring", 

1814 purpose="Hive-partitioned Parquet cost reports queried through Athena", 

1815 pod_access="none", 

1816 discovery="SSM /<project>/cost-report-bucket/{name,arn} in the monitoring region", 

1817 removal_policy="destroy", 

1818 logical_id_prefix="CostReportBucket", 

1819 reserved_prefixes=("reports/", "adhoc/", "athena-results/"), 

1820 ), 

1821 BucketDescriptor( 

1822 id="cost-reports-access-logs", 

1823 role="access-logs", 

1824 scope="monitoring", 

1825 purpose="Server access logs for the cost report bucket", 

1826 pod_access="none", 

1827 discovery="CloudFormation resource of the monitoring stack (CDK-generated name)", 

1828 removal_policy="destroy", 

1829 logical_id_prefix="CostReportAccessLogsBucket", 

1830 ), 

1831 BucketDescriptor( 

1832 id="analytics-studio", 

1833 role="primary", 

1834 scope="analytics", 

1835 purpose="SageMaker Studio private scratch data and notebook outputs", 

1836 pod_access="none", 

1837 discovery="CloudFormation resource of the analytics stack (CDK-generated name)", 

1838 removal_policy="destroy", 

1839 logical_id_prefix="StudioOnlyBucket", 

1840 sync_alias="analytics-studio", 

1841 opt_in="analytics_environment.enabled", 

1842 ), 

1843 BucketDescriptor( 

1844 id="analytics-studio-access-logs", 

1845 role="access-logs", 

1846 scope="analytics", 

1847 purpose="Server access logs for the analytics Studio bucket", 

1848 pod_access="none", 

1849 discovery="CloudFormation resource of the analytics stack (CDK-generated name)", 

1850 removal_policy="destroy", 

1851 logical_id_prefix="AnalyticsAccessLogsBucket", 

1852 opt_in="analytics_environment.enabled", 

1853 ), 

1854) 

1855 

1856 

1857def get_storage_manager(config: GCOConfig | None = None) -> StorageManager: 

1858 """Return a storage manager using the merged CLI configuration.""" 

1859 return StorageManager(config)