Skip to content

Commit e5c4ebd

Browse files
committed
Add endpoint to generate registry credentials for a deployment
1 parent 5d7013b commit e5c4ebd

File tree

6 files changed

+223
-5
lines changed

6 files changed

+223
-5
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
2+
import {
3+
type GenerateRegistryCredentialsResponseBody,
4+
ProgressDeploymentRequestBody,
5+
tryCatch,
6+
} from "@trigger.dev/core/v3";
7+
import { z } from "zod";
8+
import { authenticateRequest } from "~/services/apiAuth.server";
9+
import { logger } from "~/services/logger.server";
10+
import { DeploymentService } from "~/v3/services/deployment.server";
11+
12+
const ParamsSchema = z.object({
13+
deploymentId: z.string(),
14+
});
15+
16+
export async function action({ request, params }: ActionFunctionArgs) {
17+
if (request.method.toUpperCase() !== "POST") {
18+
return json({ error: "Method Not Allowed" }, { status: 405 });
19+
}
20+
21+
const parsedParams = ParamsSchema.safeParse(params);
22+
23+
if (!parsedParams.success) {
24+
return json({ error: "Invalid params" }, { status: 400 });
25+
}
26+
27+
const authenticationResult = await authenticateRequest(request, {
28+
apiKey: true,
29+
organizationAccessToken: false,
30+
personalAccessToken: false,
31+
});
32+
33+
if (!authenticationResult || !authenticationResult.result.ok) {
34+
logger.info("Invalid or missing api key", { url: request.url });
35+
return json({ error: "Invalid or Missing API key" }, { status: 401 });
36+
}
37+
38+
const { environment: authenticatedEnv } = authenticationResult.result;
39+
const { deploymentId } = parsedParams.data;
40+
41+
const [, rawBody] = await tryCatch(request.json());
42+
const body = ProgressDeploymentRequestBody.safeParse(rawBody ?? {});
43+
44+
if (!body.success) {
45+
return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 });
46+
}
47+
48+
const deploymentService = new DeploymentService();
49+
50+
return await deploymentService.generateRegistryCredentials(authenticatedEnv, deploymentId).match(
51+
(result) => {
52+
return json(
53+
{
54+
username: result.username,
55+
password: result.password,
56+
expiresAt: result.expiresAt.toISOString(),
57+
repositoryUri: result.repositoryUri,
58+
} satisfies GenerateRegistryCredentialsResponseBody,
59+
{ status: 200 }
60+
);
61+
},
62+
(error) => {
63+
switch (error.type) {
64+
case "deployment_not_found":
65+
return json({ error: "Deployment not found" }, { status: 404 });
66+
case "deployment_has_no_image_reference":
67+
logger.error(
68+
"Failed to generate registry credentials: deployment_has_no_image_reference",
69+
{ deploymentId }
70+
);
71+
return json({ error: "Deployment has no image reference" }, { status: 409 });
72+
case "deployment_is_already_final":
73+
return json(
74+
{ error: "Failed to generate registry credentials: deployment_is_already_final" },
75+
{ status: 409 }
76+
);
77+
case "missing_registry_credentials":
78+
logger.error("Failed to generate registry credentials: missing_registry_credentials", {
79+
deploymentId,
80+
});
81+
return json({ error: "Missing registry credentials" }, { status: 409 });
82+
case "registry_not_supported":
83+
logger.error("Failed to generate registry credentials: registry_not_supported", {
84+
deploymentId,
85+
});
86+
return json({ error: "Registry not supported" }, { status: 409 });
87+
case "registry_region_not_supported":
88+
logger.error("Failed to generate registry credentials: registry_region_not_supported", {
89+
deploymentId,
90+
});
91+
return json({ error: "Registry region not supported" }, { status: 409 });
92+
case "other":
93+
default:
94+
error.type satisfies "other";
95+
logger.error("Failed to generate registry credentials", { error: error.cause });
96+
return json({ error: "Internal server error" }, { status: 500 });
97+
}
98+
}
99+
);
100+
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,24 @@ export async function setBillingAlert(
511511
return result;
512512
}
513513

514+
export async function generateRegistryCredentials(
515+
projectId: string,
516+
region: "us-east-1" | "eu-central-1"
517+
) {
518+
if (!client) return undefined;
519+
const result = await client.generateRegistryCredentials(projectId, region);
520+
if (!result.success) {
521+
logger.error("Error generating registry credentials", {
522+
error: result.error,
523+
projectId,
524+
region,
525+
});
526+
throw new Error("Failed to generate registry credentials");
527+
}
528+
529+
return result;
530+
}
531+
514532
function isCloud(): boolean {
515533
const acceptableHosts = [
516534
"https://cloud.trigger.dev",

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { TimeoutDeploymentService } from "./timeoutDeployment.server";
77
import { env } from "~/env.server";
88
import { createRemoteImageBuild } from "../remoteImageBuilder.server";
99
import { FINAL_DEPLOYMENT_STATUSES } from "./failDeployment.server";
10+
import { generateRegistryCredentials } from "~/services/platform.v3.server";
1011

1112
export class DeploymentService extends BaseService {
1213
/**
@@ -231,4 +232,92 @@ export class DeploymentService extends BaseService {
231232
.andThen(deleteTimeout)
232233
.map(() => undefined);
233234
}
235+
236+
/**
237+
* Generates registry credentials for a deployment. Returns an error if the deployment is in a final state.
238+
*
239+
* Uses the `platform` package, only available in cloud.
240+
*
241+
* @param authenticatedEnv The environment which the deployment belongs to.
242+
* @param friendlyId The friendly deployment ID.
243+
*/
244+
public generateRegistryCredentials(
245+
authenticatedEnv: Pick<AuthenticatedEnvironment, "projectId">,
246+
friendlyId: string
247+
) {
248+
const validateDeployment = (
249+
deployment: Pick<WorkerDeployment, "id" | "status" | "imageReference">
250+
) => {
251+
if (FINAL_DEPLOYMENT_STATUSES.includes(deployment.status)) {
252+
return errAsync({ type: "deployment_is_already_final" as const });
253+
}
254+
return okAsync(deployment);
255+
};
256+
257+
const getDeploymentRegion = (deployment: Pick<WorkerDeployment, "imageReference">) => {
258+
if (!deployment.imageReference) {
259+
return errAsync({ type: "deployment_has_no_image_reference" as const });
260+
}
261+
if (!deployment.imageReference.includes("amazonaws.com")) {
262+
return errAsync({ type: "registry_not_supported" as const });
263+
}
264+
265+
// we should connect the deployment to a region more explicitly in the future
266+
// for now we just use the image reference to determine the region
267+
if (deployment.imageReference.includes("us-east-1")) {
268+
return okAsync({ region: "us-east-1" as const });
269+
}
270+
if (deployment.imageReference.includes("eu-central-1")) {
271+
return okAsync({ region: "eu-central-1" as const });
272+
}
273+
274+
return errAsync({ type: "registry_region_not_supported" as const });
275+
};
276+
277+
const generateCredentials = ({ region }: { region: "us-east-1" | "eu-central-1" }) =>
278+
fromPromise(generateRegistryCredentials(authenticatedEnv.projectId, region), (error) => ({
279+
type: "other" as const,
280+
cause: error,
281+
})).andThen((result) => {
282+
if (!result || !result.success) {
283+
return errAsync({ type: "missing_registry_credentials" as const });
284+
}
285+
return okAsync({
286+
username: result.username,
287+
password: result.password,
288+
expiresAt: new Date(result.expiresAt),
289+
repositoryUri: result.repositoryUri,
290+
});
291+
});
292+
293+
return this.getDeployment(authenticatedEnv.projectId, friendlyId)
294+
.andThen(validateDeployment)
295+
.andThen(getDeploymentRegion)
296+
.andThen(generateCredentials);
297+
}
298+
299+
private getDeployment(projectId: string, friendlyId: string) {
300+
return fromPromise(
301+
this._prisma.workerDeployment.findFirst({
302+
where: {
303+
friendlyId,
304+
projectId,
305+
},
306+
select: {
307+
status: true,
308+
id: true,
309+
imageReference: true,
310+
},
311+
}),
312+
(error) => ({
313+
type: "other" as const,
314+
cause: error,
315+
})
316+
).andThen((deployment) => {
317+
if (!deployment) {
318+
return errAsync({ type: "deployment_not_found" as const });
319+
}
320+
return okAsync(deployment);
321+
});
322+
}
234323
}

apps/webapp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
"@trigger.dev/core": "workspace:*",
118118
"@trigger.dev/database": "workspace:*",
119119
"@trigger.dev/otlp-importer": "workspace:*",
120-
"@trigger.dev/platform": "1.0.18",
120+
"@trigger.dev/platform": "0.0.0-prerelease-ecr-20251021203336",
121121
"@trigger.dev/redis-worker": "workspace:*",
122122
"@trigger.dev/sdk": "workspace:*",
123123
"@types/pg": "8.6.6",

packages/core/src/v3/schemas/api.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,17 @@ export const InitializeDeploymentRequestBody = z.object({
439439

440440
export type InitializeDeploymentRequestBody = z.infer<typeof InitializeDeploymentRequestBody>;
441441

442+
export const GenerateRegistryCredentialsResponseBody = z.object({
443+
username: z.string(),
444+
password: z.string(),
445+
expiresAt: z.string(),
446+
repositoryUri: z.string(),
447+
});
448+
449+
export type GenerateRegistryCredentialsResponseBody = z.infer<
450+
typeof GenerateRegistryCredentialsResponseBody
451+
>;
452+
442453
export const DeploymentErrorData = z.object({
443454
name: z.string(),
444455
message: z.string(),

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)