|
8 | 8 | GetAuthorizationTokenCommand, |
9 | 9 | PutLifecyclePolicyCommand, |
10 | 10 | PutImageTagMutabilityCommand, |
| 11 | + SetRepositoryPolicyCommand, |
11 | 12 | } from "@aws-sdk/client-ecr"; |
12 | 13 | import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; |
13 | 14 | import { tryCatch } from "@trigger.dev/core"; |
@@ -138,6 +139,7 @@ export async function getDeploymentImageRef({ |
138 | 139 | roleArn: registry.ecrAssumeRoleArn, |
139 | 140 | externalId: registry.ecrAssumeRoleExternalId, |
140 | 141 | }, |
| 142 | + defaultRepositoryPolicy: registry.ecrDefaultRepositoryPolicy, |
141 | 143 | }) |
142 | 144 | ); |
143 | 145 |
|
@@ -219,12 +221,14 @@ async function createEcrRepository({ |
219 | 221 | accountId, |
220 | 222 | registryTags, |
221 | 223 | assumeRole, |
| 224 | + defaultRepositoryPolicy, |
222 | 225 | }: { |
223 | 226 | repositoryName: string; |
224 | 227 | region: string; |
225 | 228 | accountId?: string; |
226 | 229 | registryTags?: string; |
227 | 230 | assumeRole?: AssumeRoleConfig; |
| 231 | + defaultRepositoryPolicy?: string; |
228 | 232 | }): Promise<Repository> { |
229 | 233 | const ecr = await createEcrClient({ region, assumeRole }); |
230 | 234 |
|
@@ -262,9 +266,50 @@ async function createEcrRepository({ |
262 | 266 | }) |
263 | 267 | ); |
264 | 268 |
|
| 269 | + // Apply an operator-provided IAM policy to the new repository. Useful for |
| 270 | + // self-hosters whose ECR account is separate from the account running the |
| 271 | + // EKS workers — without this the workers get 403 Forbidden when pulling the |
| 272 | + // task image (default ECR policy only grants access to the registry owner). |
| 273 | + // The existing-repo branch of `ensureEcrRepositoryExists` reconciles this |
| 274 | + // same policy on every call, so a partial-create that fails here is |
| 275 | + // self-healing on the next deploy. |
| 276 | + if (defaultRepositoryPolicy) { |
| 277 | + await applyEcrRepositoryPolicy({ |
| 278 | + repositoryName: result.repository.repositoryName!, |
| 279 | + region, |
| 280 | + accountId: result.repository.registryId ?? accountId, |
| 281 | + assumeRole, |
| 282 | + defaultRepositoryPolicy, |
| 283 | + }); |
| 284 | + } |
| 285 | + |
265 | 286 | return result.repository; |
266 | 287 | } |
267 | 288 |
|
| 289 | +async function applyEcrRepositoryPolicy({ |
| 290 | + repositoryName, |
| 291 | + region, |
| 292 | + accountId, |
| 293 | + assumeRole, |
| 294 | + defaultRepositoryPolicy, |
| 295 | +}: { |
| 296 | + repositoryName: string; |
| 297 | + region: string; |
| 298 | + accountId?: string; |
| 299 | + assumeRole?: AssumeRoleConfig; |
| 300 | + defaultRepositoryPolicy: string; |
| 301 | +}): Promise<void> { |
| 302 | + const ecr = await createEcrClient({ region, assumeRole }); |
| 303 | + |
| 304 | + await ecr.send( |
| 305 | + new SetRepositoryPolicyCommand({ |
| 306 | + repositoryName, |
| 307 | + registryId: accountId, |
| 308 | + policyText: defaultRepositoryPolicy, |
| 309 | + }) |
| 310 | + ); |
| 311 | +} |
| 312 | + |
268 | 313 | async function updateEcrRepositoryCacheSettings({ |
269 | 314 | repositoryName, |
270 | 315 | region, |
@@ -386,11 +431,13 @@ async function ensureEcrRepositoryExists({ |
386 | 431 | registryHost, |
387 | 432 | registryTags, |
388 | 433 | assumeRole, |
| 434 | + defaultRepositoryPolicy, |
389 | 435 | }: { |
390 | 436 | repositoryName: string; |
391 | 437 | registryHost: string; |
392 | 438 | registryTags?: string; |
393 | 439 | assumeRole?: AssumeRoleConfig; |
| 440 | + defaultRepositoryPolicy?: string; |
394 | 441 | }): Promise<{ repo: Repository; repoCreated: boolean }> { |
395 | 442 | const { region, accountId } = parseEcrRegistryDomain(registryHost); |
396 | 443 |
|
@@ -421,14 +468,46 @@ async function ensureEcrRepositoryExists({ |
421 | 468 | } |
422 | 469 | } |
423 | 470 |
|
| 471 | + // Reconcile the default repository policy on every call. Idempotent, and |
| 472 | + // covers two recovery cases: (1) a previous create succeeded but the |
| 473 | + // SetRepositoryPolicy call failed mid-flight, leaving the repo without a |
| 474 | + // policy; (2) the operator updated DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY |
| 475 | + // and existing repos need to pick up the new value. |
| 476 | + if (defaultRepositoryPolicy) { |
| 477 | + const [policyError] = await tryCatch( |
| 478 | + applyEcrRepositoryPolicy({ |
| 479 | + repositoryName, |
| 480 | + region, |
| 481 | + accountId, |
| 482 | + assumeRole, |
| 483 | + defaultRepositoryPolicy, |
| 484 | + }) |
| 485 | + ); |
| 486 | + |
| 487 | + if (policyError) { |
| 488 | + logger.error("Failed to reconcile ECR repository policy on existing repo", { |
| 489 | + repositoryName, |
| 490 | + region, |
| 491 | + policyError, |
| 492 | + }); |
| 493 | + } |
| 494 | + } |
| 495 | + |
424 | 496 | return { |
425 | 497 | repo: existingRepo, |
426 | 498 | repoCreated: false, |
427 | 499 | }; |
428 | 500 | } |
429 | 501 |
|
430 | 502 | const [createRepoError, newRepo] = await tryCatch( |
431 | | - createEcrRepository({ repositoryName, region, accountId, registryTags, assumeRole }) |
| 503 | + createEcrRepository({ |
| 504 | + repositoryName, |
| 505 | + region, |
| 506 | + accountId, |
| 507 | + registryTags, |
| 508 | + assumeRole, |
| 509 | + defaultRepositoryPolicy, |
| 510 | + }) |
432 | 511 | ); |
433 | 512 |
|
434 | 513 | if (createRepoError) { |
|
0 commit comments