Skip to content

Commit 56d8ae9

Browse files
Merge active ECR manifest capture fix (#117)
2 parents 7716768 + 1cf2471 commit 56d8ae9

2 files changed

Lines changed: 166 additions & 7 deletions

File tree

core/tests/test_deployment_workflow.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5775,6 +5775,141 @@ def test_active_image_proof_binds_full_sha_tag_to_exact_digest(self) -> None:
57755775
with self.assertRaisesMessage(ReleaseContractError, "source SHA tag"):
57765776
gateway.verify_image_digest_exists(identity)
57775777

5778+
def test_active_image_proof_uses_exact_sha_fallback_for_manifest_lookup(self) -> None:
5779+
gateway = self.gateway(FakeMigrationEcs({}, {}))
5780+
gateway.ecr = Mock()
5781+
source_sha = "a" * 40
5782+
image_digest = f"sha256:{'a' * 64}"
5783+
identity = ReleaseIdentity(
5784+
source_sha,
5785+
image_digest,
5786+
ECR_REPOSITORY_URI,
5787+
f"20260809-143205-{source_sha[:7]}",
5788+
)
5789+
gateway.ecr.describe_images.side_effect = [
5790+
{"imageDetails": [{"imageDigest": image_digest}]},
5791+
{"imageDetails": [{"imageDigest": image_digest}]},
5792+
{"imageDetails": [{"imageDigest": image_digest}]},
5793+
]
5794+
gateway.ecr.batch_get_image.side_effect = [
5795+
{
5796+
"failures": [{"failureCode": "ImageNotFound"}],
5797+
"images": [],
5798+
},
5799+
{
5800+
"failures": [],
5801+
"images": [
5802+
{
5803+
"imageId": {
5804+
"imageDigest": image_digest,
5805+
"imageTag": source_sha,
5806+
},
5807+
"imageManifest": "{}",
5808+
}
5809+
],
5810+
},
5811+
]
5812+
5813+
gateway.verify_image_digest_exists(identity)
5814+
5815+
self.assertEqual(
5816+
[call.kwargs["imageIds"] for call in gateway.ecr.batch_get_image.call_args_list],
5817+
[
5818+
[{"imageDigest": image_digest}],
5819+
[{"imageTag": source_sha}],
5820+
],
5821+
)
5822+
5823+
def test_active_image_manifest_fallback_remains_fail_closed(self) -> None:
5824+
source_sha = "a" * 40
5825+
image_digest = f"sha256:{'a' * 64}"
5826+
identity = ReleaseIdentity(
5827+
source_sha,
5828+
image_digest,
5829+
ECR_REPOSITORY_URI,
5830+
f"20260809-143205-{source_sha[:7]}",
5831+
)
5832+
malformed_failure_values: tuple[object, ...] = ({}, None, "", 0, False)
5833+
invalid_fallbacks = (
5834+
*(
5835+
{
5836+
"failures": malformed_failures,
5837+
"images": [
5838+
{
5839+
"imageId": {"imageDigest": image_digest},
5840+
"imageManifest": "{}",
5841+
}
5842+
],
5843+
}
5844+
for malformed_failures in malformed_failure_values
5845+
),
5846+
{
5847+
"failures": [],
5848+
"images": [
5849+
{
5850+
"imageId": {"imageDigest": f"sha256:{'b' * 64}"},
5851+
"imageManifest": "{}",
5852+
}
5853+
],
5854+
},
5855+
{
5856+
"failures": [{"failureCode": "ImageNotFound"}],
5857+
"images": [],
5858+
},
5859+
{
5860+
"failures": [],
5861+
"images": [{"imageId": {"imageDigest": image_digest}}],
5862+
},
5863+
{
5864+
"failures": [],
5865+
"images": [
5866+
{
5867+
"imageId": {"imageDigest": image_digest},
5868+
"imageManifest": " ",
5869+
}
5870+
],
5871+
},
5872+
{
5873+
"failures": [],
5874+
"images": [
5875+
{
5876+
"imageId": {"imageDigest": image_digest},
5877+
"imageManifest": "{}",
5878+
},
5879+
{
5880+
"imageId": {"imageDigest": image_digest},
5881+
"imageManifest": "{}",
5882+
},
5883+
],
5884+
},
5885+
)
5886+
unusable_digest_lookup = {
5887+
"failures": [{"failureCode": "ImageNotFound"}],
5888+
"images": [],
5889+
}
5890+
5891+
for fallback in invalid_fallbacks:
5892+
gateway = self.gateway(FakeMigrationEcs({}, {}))
5893+
gateway.ecr = Mock()
5894+
gateway.ecr.describe_images.side_effect = [
5895+
{"imageDetails": [{"imageDigest": image_digest}]},
5896+
{"imageDetails": [{"imageDigest": image_digest}]},
5897+
{"imageDetails": [{"imageDigest": image_digest}]},
5898+
]
5899+
gateway.ecr.batch_get_image.side_effect = [
5900+
unusable_digest_lookup,
5901+
fallback,
5902+
]
5903+
5904+
with (
5905+
self.subTest(fallback=fallback),
5906+
self.assertRaisesMessage(
5907+
ReleaseContractError,
5908+
"active image manifest is missing",
5909+
),
5910+
):
5911+
gateway.verify_image_digest_exists(identity)
5912+
57785913
def test_public_health_polls_until_exact_readiness_or_timeout(self) -> None:
57795914
gateway = self.gateway(FakeMigrationEcs({}, {}))
57805915
source_sha = "a" * 40

deploy/aws_gateway.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -500,15 +500,39 @@ def verify_image_digest_exists(self, identity: ReleaseIdentity) -> None:
500500
repositoryName=ECR_REPOSITORY_NAME,
501501
imageIds=[{"imageDigest": identity.image_digest}],
502502
)
503-
images = manifest.get("images", [])
504-
if (
505-
manifest.get("failures")
506-
or len(images) != 1
507-
or images[0].get("imageId", {}).get("imageDigest") != identity.image_digest
508-
or not images[0].get("imageManifest")
509-
):
503+
if not self._contains_exact_image_manifest(manifest, identity.image_digest):
504+
# ECR can describe an exact digest while declining that digest form in
505+
# BatchGetImage. The already-proven immutable full-SHA tag is an equivalent
506+
# lookup key only when the returned image still binds to the same digest.
507+
manifest = self.ecr.batch_get_image(
508+
repositoryName=ECR_REPOSITORY_NAME,
509+
imageIds=[{"imageTag": identity.source_sha}],
510+
)
511+
if not self._contains_exact_image_manifest(manifest, identity.image_digest):
510512
raise ReleaseContractError("active image manifest is missing from development ECR")
511513

514+
@staticmethod
515+
def _contains_exact_image_manifest(response: Any, expected_digest: str) -> bool:
516+
if not isinstance(response, dict):
517+
return False
518+
failures = response.get("failures")
519+
if not isinstance(failures, list) or failures:
520+
return False
521+
images = response.get("images")
522+
if not isinstance(images, list) or len(images) != 1:
523+
return False
524+
image = images[0]
525+
if not isinstance(image, dict):
526+
return False
527+
image_id = image.get("imageId")
528+
manifest = image.get("imageManifest")
529+
return bool(
530+
isinstance(image_id, dict)
531+
and image_id.get("imageDigest") == expected_digest
532+
and isinstance(manifest, str)
533+
and manifest.strip()
534+
)
535+
512536
def _stop_migration_and_prove_terminal(self, task_arn: str, reason: str) -> None:
513537
stop_error: Exception | None = None
514538
try:

0 commit comments

Comments
 (0)