Skip to content

Commit 3c7200f

Browse files
committed
fix(webapp): fail closed on image verification + address review
1 parent b48e0d3 commit 3c7200f

3 files changed

Lines changed: 59 additions & 22 deletions

File tree

apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ export class FinalizeDeploymentV2Service extends BaseService {
169169

170170
const registryConfig = getRegistryConfig(deployment.type === "MANAGED");
171171

172+
// ECR-only: non-ECR (self-hosted) registries can't be checked this way, so skip.
173+
if (!isEcrRegistry(registryConfig.host)) {
174+
return;
175+
}
176+
172177
const result = await ecrImageExists({
173178
imageReference: deployment.imageReference,
174179
imageDigest: body.imageDigest,
@@ -181,9 +186,13 @@ export class FinalizeDeploymentV2Service extends BaseService {
181186
);
182187
}
183188

184-
// "unknown" (non-ECR registry, unparseable ref, or an API/permission error)
185-
// is logged inside ecrImageExists - proceed, since a verifier failure must
186-
// not become a deploy outage.
189+
// Fail closed: if we can't confirm the image is present, don't promote a version
190+
// that might not start. Set DEPLOY_IMAGE_VERIFICATION_ENABLED=0 for out-of-band pushes.
191+
if (result === "unknown") {
192+
throw new ServiceValidationError(
193+
"Could not verify the deployment image exists in the registry. Aborting the deploy."
194+
);
195+
}
187196
}
188197

189198
async #createTemplateIfNeeded(

apps/webapp/app/v3/services/verifyDeploymentImage.server.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { BatchGetImageCommand, type BatchGetImageCommandOutput } from "@aws-sdk/client-ecr";
1+
import {
2+
BatchGetImageCommand,
3+
type BatchGetImageCommandOutput,
4+
RepositoryNotFoundException,
5+
} from "@aws-sdk/client-ecr";
26
import { tryCatch } from "@trigger.dev/core";
37
import { logger } from "~/services/logger.server";
48
import {
@@ -81,28 +85,17 @@ const sendBatchGetImage: BatchGetImageSender = async ({
8185
imageIds,
8286
}) => {
8387
const ecr = await createEcrClient({ region, assumeRole });
84-
return ecr.send(
85-
new BatchGetImageCommand({
86-
repositoryName,
87-
registryId,
88-
imageIds,
89-
// We only care whether the manifest exists, not its contents.
90-
acceptedMediaTypes: [
91-
"application/vnd.docker.distribution.manifest.v2+json",
92-
"application/vnd.oci.image.manifest.v1+json",
93-
"application/vnd.oci.image.index.v1+json",
94-
"application/vnd.docker.distribution.manifest.list.v2+json",
95-
],
96-
})
97-
);
88+
// No acceptedMediaTypes: only the single-manifest types are valid enum values, and
89+
// we only care whether the image exists, not its manifest format.
90+
return ecr.send(new BatchGetImageCommand({ repositoryName, registryId, imageIds }));
9891
};
9992

10093
/**
10194
* Pre-promotion backstop: check the deployment image actually exists in ECR.
10295
*
103-
* Returns "unknown" for non-ECR registries or any error we can't read as a
104-
* definitive miss - callers treat "unknown" as "proceed", so a verifier failure
105-
* never becomes a deploy outage. `_send` is a test seam.
96+
* "found"/"missing" are definitive (a nonexistent repo counts as missing).
97+
* "unknown" means we couldn't determine it - non-ECR registry, unparseable ref, or
98+
* an API error; the caller decides what to do with each. `_send` is a test seam.
10699
*/
107100
export async function ecrImageExists(
108101
{
@@ -152,6 +145,11 @@ export async function ecrImageExists(
152145
);
153146

154147
if (error) {
148+
// A missing repo is a definitive miss, not an ambiguous error.
149+
if (error instanceof RepositoryNotFoundException) {
150+
return "missing";
151+
}
152+
155153
logger.error("Failed to verify deployment image in ECR", {
156154
imageReference,
157155
repositoryName: parsed.repositoryName,

apps/webapp/test/verifyDeploymentImage.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { RepositoryNotFoundException } from "@aws-sdk/client-ecr";
12
import { describe, expect, it } from "vitest";
23
import {
34
ecrImageExists,
@@ -77,6 +78,22 @@ describe("ecrImageExists", () => {
7778
expect(called).toBe(false);
7879
});
7980

81+
it("returns unknown for an unparseable ECR ref without calling the registry", async () => {
82+
let called = false;
83+
const result = await ecrImageExists(
84+
{
85+
imageReference: `${ECR_HOST}/deployments-test/proj_abc`,
86+
registryConfig: ecrConfig,
87+
},
88+
async () => {
89+
called = true;
90+
return {} as any;
91+
}
92+
);
93+
expect(result).toBe("unknown");
94+
expect(called).toBe(false);
95+
});
96+
8097
it("returns found when the image exists", async () => {
8198
const result = await ecrImageExists(
8299
{
@@ -99,7 +116,7 @@ describe("ecrImageExists", () => {
99116
expect(result).toBe("missing");
100117
});
101118

102-
it("returns unknown (fails open) when the registry call throws", async () => {
119+
it("returns unknown when the registry call throws an ambiguous error", async () => {
103120
const result = await ecrImageExists(
104121
{
105122
imageReference: `${ECR_HOST}/deployments-test/proj_abc:v1.prod.a1b2c3d4`,
@@ -112,6 +129,19 @@ describe("ecrImageExists", () => {
112129
expect(result).toBe("unknown");
113130
});
114131

132+
it("returns missing when the repository does not exist", async () => {
133+
const result = await ecrImageExists(
134+
{
135+
imageReference: `${ECR_HOST}/deployments-test/proj_abc:v1.prod.a1b2c3d4`,
136+
registryConfig: ecrConfig,
137+
},
138+
async () => {
139+
throw new RepositoryNotFoundException({ message: "not found", $metadata: {} });
140+
}
141+
);
142+
expect(result).toBe("missing");
143+
});
144+
115145
it("queries by digest when a valid digest is supplied", async () => {
116146
const digest = `sha256:${"b".repeat(64)}`;
117147
let seen: any;

0 commit comments

Comments
 (0)