From eb9954572964d6031dcc52287ff9915573535008 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:25:04 +0100 Subject: [PATCH] fresh auth token for each deploy --- .../app/v3/getDeploymentImageRef.server.ts | 39 ++++++++++++++++++- .../services/finalizeDeploymentV2.server.ts | 38 ++++++++++++++---- .../webapp/test/getDeploymentImageRef.test.ts | 38 +++++++++++++++++- 3 files changed, 106 insertions(+), 9 deletions(-) diff --git a/apps/webapp/app/v3/getDeploymentImageRef.server.ts b/apps/webapp/app/v3/getDeploymentImageRef.server.ts index f6a11c43a..18a2fc808 100644 --- a/apps/webapp/app/v3/getDeploymentImageRef.server.ts +++ b/apps/webapp/app/v3/getDeploymentImageRef.server.ts @@ -5,6 +5,7 @@ import { type Repository, type Tag, RepositoryNotFoundException, + GetAuthorizationTokenCommand, } from "@aws-sdk/client-ecr"; import { tryCatch } from "@trigger.dev/core"; import { logger } from "~/services/logger.server"; @@ -58,7 +59,7 @@ export async function getDeploymentImageRef({ }; } -function isEcrRegistry(registryHost: string) { +export function isEcrRegistry(registryHost: string) { return registryHost.includes("amazonaws.com"); } @@ -196,3 +197,39 @@ async function ensureEcrRepositoryExists({ return newRepo; } + +export async function getEcrAuthToken({ + registryHost, + registryId, +}: { + registryHost: string; + registryId?: string; +}): Promise<{ username: string; password: string }> { + const region = getEcrRegion(registryHost); + if (!region) { + logger.error("Invalid ECR registry host", { registryHost }); + throw new Error("Invalid ECR registry host"); + } + + const ecr = new ECRClient({ region }); + const response = await ecr.send( + new GetAuthorizationTokenCommand({ + registryIds: registryId ? [registryId] : undefined, + }) + ); + + if (!response.authorizationData) { + throw new Error("Failed to get ECR authorization token"); + } + + const authData = response.authorizationData[0]; + + if (!authData.authorizationToken) { + throw new Error("No authorization token returned from ECR"); + } + + const authToken = Buffer.from(authData.authorizationToken, "base64").toString(); + const [username, password] = authToken.split(":"); + + return { username, password }; +} diff --git a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts index 810d05168..3edc90bf4 100644 --- a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts +++ b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts @@ -9,6 +9,8 @@ import { env } from "~/env.server"; import { depot as execDepot } from "@depot/cli"; import { FinalizeDeploymentService } from "./finalizeDeployment.server"; import { remoteBuildsEnabled } from "../remoteImageBuilder.server"; +import { getEcrAuthToken, isEcrRegistry } from "../getDeploymentImageRef.server"; +import { tryCatch } from "@trigger.dev/core"; export class FinalizeDeploymentV2Service extends BaseService { public async call( @@ -172,11 +174,27 @@ async function executePushToRegistry( { depot, registry, deployment }: ExecutePushToRegistryOptions, writer?: WritableStreamDefaultWriter ): Promise { - // Step 1: We need to "login" to the digital ocean registry - const configDir = await ensureLoggedIntoDockerRegistry(registry.host, { - username: registry.username, - password: registry.password, - }); + // Step 1: We need to "login" to the registry + const [loginError, configDir] = await tryCatch( + ensureLoggedIntoDockerRegistry(registry.host, { + username: registry.username, + password: registry.password, + }) + ); + + if (loginError) { + logger.error("Failed to login to registry", { + deployment, + registryHost: registry.host, + error: loginError.message, + }); + + return { + ok: false as const, + error: "Failed to login to registry", + logs: "", + }; + } const imageTag = deployment.imageReference; @@ -244,12 +262,18 @@ async function executePushToRegistry( async function ensureLoggedIntoDockerRegistry( registryHost: string, - auth: { username: string; password: string } + auth: { username: string; password: string } | undefined = undefined ) { const tmpDir = await createTempDir(); - // Read the current docker config const dockerConfigPath = join(tmpDir, "config.json"); + // If this is an ECR registry, get fresh credentials + if (isEcrRegistry(registryHost)) { + auth = await getEcrAuthToken({ registryHost, registryId: env.DEPLOY_REGISTRY_ID }); + } else if (!auth) { + throw new Error("Authentication required for non-ECR registry"); + } + await writeJSONFile(dockerConfigPath, { auths: { [registryHost]: { diff --git a/apps/webapp/test/getDeploymentImageRef.test.ts b/apps/webapp/test/getDeploymentImageRef.test.ts index 86f25ce46..0f45c36cf 100644 --- a/apps/webapp/test/getDeploymentImageRef.test.ts +++ b/apps/webapp/test/getDeploymentImageRef.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { getDeploymentImageRef, getEcrRegion } from "../app/v3/getDeploymentImageRef.server"; +import { + getDeploymentImageRef, + getEcrAuthToken, + getEcrRegion, +} from "../app/v3/getDeploymentImageRef.server"; import { ECRClient, DeleteRepositoryCommand } from "@aws-sdk/client-ecr"; describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", () => { @@ -101,3 +105,35 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", ).rejects.toThrow("Invalid ECR registry host: invalid.ecr.amazonaws.com"); }); }); + +describe.skipIf(process.env.RUN_REGISTRY_AUTH_TESTS !== "1")("getEcrAuthToken", () => { + const registryId = process.env.DEPLOY_REGISTRY_ID; + const testHost = "123456789012.dkr.ecr.us-east-1.amazonaws.com"; + + it("should return valid ECR credentials", async () => { + const auth = await getEcrAuthToken({ + registryHost: testHost, + registryId, + }); + + // Check the structure and basic validation of the returned credentials + expect(auth).toHaveProperty("username"); + expect(auth).toHaveProperty("password"); + expect(auth.username).toBe("AWS"); + expect(typeof auth.password).toBe("string"); + expect(auth.password.length).toBeGreaterThan(0); + + // Verify the token format (should be a base64-encoded string) + const base64Regex = /^[A-Za-z0-9+/=]+$/; + expect(base64Regex.test(auth.password)).toBe(true); + }); + + it("should throw error for invalid region", async () => { + await expect( + getEcrAuthToken({ + registryHost: "invalid.ecr.amazonaws.com", + registryId, + }) + ).rejects.toThrow(); + }); +});