Skip to content

Commit 512d682

Browse files
committed
assume role fix and env var changes
1 parent 372c7ce commit 512d682

File tree

5 files changed

+142
-97
lines changed

5 files changed

+142
-97
lines changed

apps/webapp/app/env.server.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,17 +228,21 @@ const EnvironmentSchema = z.object({
228228
DEPOT_TOKEN: z.string().optional(),
229229
DEPOT_ORG_ID: z.string().optional(),
230230
DEPOT_REGION: z.string().default("us-east-1"),
231+
232+
// Deployment registry
231233
DEPLOY_REGISTRY_HOST: z.string().min(1),
232234
DEPLOY_REGISTRY_USERNAME: z.string().optional(),
233235
DEPLOY_REGISTRY_PASSWORD: z.string().optional(),
234236
DEPLOY_REGISTRY_NAMESPACE: z.string().min(1).default("trigger"),
235-
DEPLOY_REGISTRY_ID: z.string().optional(),
236-
DEPLOY_REGISTRY_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2"
237+
DEPLOY_REGISTRY_ECR_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2"
238+
DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z.string().optional(),
239+
DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z.string().optional(),
237240
DEPLOY_IMAGE_PLATFORM: z.string().default("linux/amd64"),
238241
DEPLOY_TIMEOUT_MS: z.coerce
239242
.number()
240243
.int()
241244
.default(60 * 1000 * 8), // 8 minutes
245+
242246
OBJECT_STORE_BASE_URL: z.string().optional(),
243247
OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(),
244248
OBJECT_STORE_SECRET_ACCESS_KEY: z.string().optional(),

apps/webapp/app/v3/getDeploymentImageRef.server.ts

Lines changed: 72 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,23 @@ import { tryCatch } from "@trigger.dev/core";
1212
import { logger } from "~/services/logger.server";
1313

1414
// Optional configuration for cross-account access
15-
export type CrossAccountConfig = {
16-
assumeRole: boolean;
17-
roleName: string;
15+
export type AssumeRoleConfig = {
16+
roleArn?: string;
17+
externalId?: string;
1818
};
1919

20-
const DEFAULT_CROSS_ACCOUNT_CONFIG: CrossAccountConfig = {
21-
assumeRole: false,
22-
roleName: "OrganizationAccountAccessRole",
23-
};
24-
25-
async function getAssumedRoleCredentials(
26-
region: string,
27-
accountId: string,
28-
config: CrossAccountConfig
29-
): Promise<{
20+
async function getAssumedRoleCredentials({
21+
region,
22+
assumeRole,
23+
}: {
24+
region: string;
25+
assumeRole?: AssumeRoleConfig;
26+
}): Promise<{
3027
accessKeyId: string;
3128
secretAccessKey: string;
3229
sessionToken: string;
3330
}> {
3431
const sts = new STSClient({ region });
35-
const roleArn = `arn:aws:iam::${accountId}:role/${config.roleName}`;
3632

3733
// Generate a unique session name using timestamp and random string
3834
// This helps with debugging but doesn't affect concurrent sessions
@@ -43,11 +39,12 @@ async function getAssumedRoleCredentials(
4339
try {
4440
const response = await sts.send(
4541
new AssumeRoleCommand({
46-
RoleArn: roleArn,
42+
RoleArn: assumeRole?.roleArn,
4743
RoleSessionName: sessionName,
4844
// Sessions automatically expire after 1 hour
4945
// AWS allows 5000 concurrent sessions by default
5046
DurationSeconds: 3600,
47+
ExternalId: assumeRole?.externalId,
5148
})
5249
);
5350

@@ -69,23 +66,28 @@ async function getAssumedRoleCredentials(
6966
sessionToken: response.Credentials.SessionToken,
7067
};
7168
} catch (error) {
72-
logger.error("Failed to assume role", { roleArn, sessionName, error });
69+
logger.error("Failed to assume role", {
70+
assumeRole,
71+
sessionName,
72+
error,
73+
});
7374
throw error;
7475
}
7576
}
7677

77-
async function createEcrClient(
78-
region: string,
79-
registryId?: string,
80-
crossAccountConfig: CrossAccountConfig = DEFAULT_CROSS_ACCOUNT_CONFIG
81-
) {
82-
// If no registryId or role assumption is disabled, use default credentials
83-
if (!registryId || !crossAccountConfig.assumeRole) {
78+
export async function createEcrClient({
79+
region,
80+
assumeRole,
81+
}: {
82+
region: string;
83+
assumeRole?: AssumeRoleConfig;
84+
}) {
85+
if (!assumeRole) {
8486
return new ECRClient({ region });
8587
}
8688

8789
// Get credentials for cross-account access
88-
const credentials = await getAssumedRoleCredentials(region, registryId, crossAccountConfig);
90+
const credentials = await getAssumedRoleCredentials({ region, assumeRole });
8991
return new ECRClient({
9092
region,
9193
credentials,
@@ -98,18 +100,16 @@ export async function getDeploymentImageRef({
98100
projectRef,
99101
nextVersion,
100102
environmentSlug,
101-
registryId,
102103
registryTags,
103-
crossAccountConfig,
104+
assumeRole,
104105
}: {
105106
host: string;
106107
namespace: string;
107108
projectRef: string;
108109
nextVersion: string;
109110
environmentSlug: string;
110-
registryId?: string;
111111
registryTags?: string;
112-
crossAccountConfig?: CrossAccountConfig;
112+
assumeRole?: AssumeRoleConfig;
113113
}): Promise<{
114114
imageRef: string;
115115
isEcr: boolean;
@@ -128,9 +128,8 @@ export async function getDeploymentImageRef({
128128
ensureEcrRepositoryExists({
129129
repositoryName,
130130
registryHost: host,
131-
registryId,
132131
registryTags,
133-
crossAccountConfig,
132+
assumeRole,
134133
})
135134
);
136135

@@ -163,17 +162,17 @@ function parseRegistryTags(tags: string): Tag[] {
163162
async function createEcrRepository({
164163
repositoryName,
165164
region,
166-
registryId,
165+
accountId,
167166
registryTags,
168-
crossAccountConfig,
167+
assumeRole,
169168
}: {
170169
repositoryName: string;
171170
region: string;
172-
registryId?: string;
171+
accountId?: string;
173172
registryTags?: string;
174-
crossAccountConfig?: CrossAccountConfig;
173+
assumeRole?: AssumeRoleConfig;
175174
}): Promise<Repository> {
176-
const ecr = await createEcrClient(region, registryId, crossAccountConfig);
175+
const ecr = await createEcrClient({ region, assumeRole });
177176

178177
const result = await ecr.send(
179178
new CreateRepositoryCommand({
@@ -182,7 +181,7 @@ async function createEcrRepository({
182181
encryptionConfiguration: {
183182
encryptionType: "AES256",
184183
},
185-
registryId,
184+
registryId: accountId,
186185
tags: registryTags ? parseRegistryTags(registryTags) : undefined,
187186
})
188187
);
@@ -198,21 +197,21 @@ async function createEcrRepository({
198197
async function getEcrRepository({
199198
repositoryName,
200199
region,
201-
registryId,
202-
crossAccountConfig,
200+
accountId,
201+
assumeRole,
203202
}: {
204203
repositoryName: string;
205204
region: string;
206-
registryId?: string;
207-
crossAccountConfig?: CrossAccountConfig;
205+
accountId?: string;
206+
assumeRole?: AssumeRoleConfig;
208207
}): Promise<Repository | undefined> {
209-
const ecr = await createEcrClient(region, registryId, crossAccountConfig);
208+
const ecr = await createEcrClient({ region, assumeRole });
210209

211210
try {
212211
const result = await ecr.send(
213212
new DescribeRepositoriesCommand({
214213
repositoryNames: [repositoryName],
215-
registryId,
214+
registryId: accountId,
216215
})
217216
);
218217

@@ -234,35 +233,46 @@ async function getEcrRepository({
234233
}
235234
}
236235

237-
export function getEcrRegion(registryHost: string): string | undefined {
236+
export type EcrRegistryComponents = {
237+
accountId: string;
238+
region: string;
239+
};
240+
241+
export function parseEcrRegistryDomain(registryHost: string): EcrRegistryComponents {
238242
const parts = registryHost.split(".");
239-
if (parts.length !== 6 || parts[1] !== "dkr" || parts[2] !== "ecr") {
240-
return undefined;
243+
244+
const isValid =
245+
parts.length === 6 &&
246+
parts[1] === "dkr" &&
247+
parts[2] === "ecr" &&
248+
parts[4] === "amazonaws" &&
249+
parts[5] === "com";
250+
251+
if (!isValid) {
252+
throw new Error(`Invalid ECR registry host: ${registryHost}`);
241253
}
242-
return parts[3];
254+
255+
return {
256+
accountId: parts[0],
257+
region: parts[3],
258+
};
243259
}
244260

245261
async function ensureEcrRepositoryExists({
246262
repositoryName,
247263
registryHost,
248-
registryId,
249264
registryTags,
250-
crossAccountConfig,
265+
assumeRole,
251266
}: {
252267
repositoryName: string;
253268
registryHost: string;
254-
registryId?: string;
255269
registryTags?: string;
256-
crossAccountConfig?: CrossAccountConfig;
270+
assumeRole?: AssumeRoleConfig;
257271
}): Promise<Repository> {
258-
const region = getEcrRegion(registryHost);
259-
260-
if (!region) {
261-
throw new Error(`Invalid ECR registry host: ${registryHost}`);
262-
}
272+
const { region, accountId } = parseEcrRegistryDomain(registryHost);
263273

264274
const [getRepoError, existingRepo] = await tryCatch(
265-
getEcrRepository({ repositoryName, region, registryId, crossAccountConfig })
275+
getEcrRepository({ repositoryName, region, accountId, assumeRole })
266276
);
267277

268278
if (getRepoError) {
@@ -276,7 +286,7 @@ async function ensureEcrRepositoryExists({
276286
}
277287

278288
const [createRepoError, newRepo] = await tryCatch(
279-
createEcrRepository({ repositoryName, region, registryId, registryTags, crossAccountConfig })
289+
createEcrRepository({ repositoryName, region, accountId, registryTags, assumeRole })
280290
);
281291

282292
if (createRepoError) {
@@ -296,23 +306,21 @@ async function ensureEcrRepositoryExists({
296306

297307
export async function getEcrAuthToken({
298308
registryHost,
299-
registryId,
300-
crossAccountConfig,
309+
assumeRole,
301310
}: {
302311
registryHost: string;
303-
registryId?: string;
304-
crossAccountConfig?: CrossAccountConfig;
312+
assumeRole?: AssumeRoleConfig;
305313
}): Promise<{ username: string; password: string }> {
306-
const region = getEcrRegion(registryHost);
314+
const { region, accountId } = parseEcrRegistryDomain(registryHost);
307315
if (!region) {
308316
logger.error("Invalid ECR registry host", { registryHost });
309317
throw new Error("Invalid ECR registry host");
310318
}
311319

312-
const ecr = await createEcrClient(region, registryId, crossAccountConfig);
320+
const ecr = await createEcrClient({ region, assumeRole });
313321
const response = await ecr.send(
314322
new GetAuthorizationTokenCommand({
315-
registryIds: registryId ? [registryId] : undefined,
323+
registryIds: accountId ? [accountId] : undefined,
316324
})
317325
);
318326

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,13 @@ async function ensureLoggedIntoDockerRegistry(
269269

270270
// If this is an ECR registry, get fresh credentials
271271
if (isEcrRegistry(registryHost)) {
272-
auth = await getEcrAuthToken({ registryHost, registryId: env.DEPLOY_REGISTRY_ID });
272+
auth = await getEcrAuthToken({
273+
registryHost,
274+
assumeRole: {
275+
roleArn: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN,
276+
externalId: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID,
277+
},
278+
});
273279
} else if (!auth) {
274280
throw new Error("Authentication required for non-ECR registry");
275281
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,11 @@ export class InitializeDeploymentService extends BaseService {
7676
projectRef: environment.project.externalRef,
7777
nextVersion,
7878
environmentSlug: environment.slug,
79-
registryId: env.DEPLOY_REGISTRY_ID,
80-
registryTags: env.DEPLOY_REGISTRY_TAGS,
79+
registryTags: env.DEPLOY_REGISTRY_ECR_TAGS,
80+
assumeRole: {
81+
roleArn: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN,
82+
externalId: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID,
83+
},
8184
})
8285
);
8386

0 commit comments

Comments
 (0)