Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/ecr-default-repository-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

Optional `DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY` env var to apply a default repository policy when the webapp creates new ECR repos
5 changes: 5 additions & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ const EnvironmentSchema = z
DEPLOY_REGISTRY_ECR_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2"
DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z.string().optional(),
DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z.string().optional(),
DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY: z.string().optional(), // raw IAM policy JSON applied to every repo created by the webapp

// Deployment registry (v4) - falls back to v3 registry if not specified
V4_DEPLOY_REGISTRY_HOST: z
Expand Down Expand Up @@ -332,6 +333,10 @@ const EnvironmentSchema = z
.string()
.optional()
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID),
V4_DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY: z
.string()
.optional()
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY),

// Compute gateway (template creation during deploy finalize)
COMPUTE_GATEWAY_URL: z.string().optional(),
Expand Down
81 changes: 80 additions & 1 deletion apps/webapp/app/v3/getDeploymentImageRef.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
GetAuthorizationTokenCommand,
PutLifecyclePolicyCommand,
PutImageTagMutabilityCommand,
SetRepositoryPolicyCommand,
} from "@aws-sdk/client-ecr";
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
import { tryCatch } from "@trigger.dev/core";
Expand Down Expand Up @@ -138,6 +139,7 @@ export async function getDeploymentImageRef({
roleArn: registry.ecrAssumeRoleArn,
externalId: registry.ecrAssumeRoleExternalId,
},
defaultRepositoryPolicy: registry.ecrDefaultRepositoryPolicy,
})
);

Expand Down Expand Up @@ -219,12 +221,14 @@ async function createEcrRepository({
accountId,
registryTags,
assumeRole,
defaultRepositoryPolicy,
}: {
repositoryName: string;
region: string;
accountId?: string;
registryTags?: string;
assumeRole?: AssumeRoleConfig;
defaultRepositoryPolicy?: string;
}): Promise<Repository> {
const ecr = await createEcrClient({ region, assumeRole });

Expand Down Expand Up @@ -262,9 +266,50 @@ async function createEcrRepository({
})
);

// Apply an operator-provided IAM policy to the new repository. Useful for
// self-hosters whose ECR account is separate from the account running the
// EKS workers — without this the workers get 403 Forbidden when pulling the
// task image (default ECR policy only grants access to the registry owner).
// The existing-repo branch of `ensureEcrRepositoryExists` reconciles this
// same policy on every call, so a partial-create that fails here is
// self-healing on the next deploy.
if (defaultRepositoryPolicy) {
await applyEcrRepositoryPolicy({
repositoryName: result.repository.repositoryName!,
region,
accountId: result.repository.registryId ?? accountId,
assumeRole,
defaultRepositoryPolicy,
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return result.repository;
}

async function applyEcrRepositoryPolicy({
repositoryName,
region,
accountId,
assumeRole,
defaultRepositoryPolicy,
}: {
repositoryName: string;
region: string;
accountId?: string;
assumeRole?: AssumeRoleConfig;
defaultRepositoryPolicy: string;
}): Promise<void> {
const ecr = await createEcrClient({ region, assumeRole });

await ecr.send(
new SetRepositoryPolicyCommand({
repositoryName,
registryId: accountId,
policyText: defaultRepositoryPolicy,
})
);
}

async function updateEcrRepositoryCacheSettings({
repositoryName,
region,
Expand Down Expand Up @@ -386,11 +431,13 @@ async function ensureEcrRepositoryExists({
registryHost,
registryTags,
assumeRole,
defaultRepositoryPolicy,
}: {
repositoryName: string;
registryHost: string;
registryTags?: string;
assumeRole?: AssumeRoleConfig;
defaultRepositoryPolicy?: string;
}): Promise<{ repo: Repository; repoCreated: boolean }> {
const { region, accountId } = parseEcrRegistryDomain(registryHost);

Expand Down Expand Up @@ -421,14 +468,46 @@ async function ensureEcrRepositoryExists({
}
}

// Reconcile the default repository policy on every call. Idempotent, and
// covers two recovery cases: (1) a previous create succeeded but the
// SetRepositoryPolicy call failed mid-flight, leaving the repo without a
// policy; (2) the operator updated DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY
// and existing repos need to pick up the new value.
if (defaultRepositoryPolicy) {
const [policyError] = await tryCatch(
applyEcrRepositoryPolicy({
repositoryName,
region,
accountId,
assumeRole,
defaultRepositoryPolicy,
})
);

if (policyError) {
logger.error("Failed to reconcile ECR repository policy on existing repo", {
repositoryName,
region,
policyError,
});
}
}

return {
repo: existingRepo,
repoCreated: false,
};
}

const [createRepoError, newRepo] = await tryCatch(
createEcrRepository({ repositoryName, region, accountId, registryTags, assumeRole })
createEcrRepository({
repositoryName,
region,
accountId,
registryTags,
assumeRole,
defaultRepositoryPolicy,
})
);

if (createRepoError) {
Expand Down
3 changes: 3 additions & 0 deletions apps/webapp/app/v3/registryConfig.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type RegistryConfig = {
ecrTags?: string;
ecrAssumeRoleArn?: string;
ecrAssumeRoleExternalId?: string;
ecrDefaultRepositoryPolicy?: string;
};

export function getRegistryConfig(isV4Deployment: boolean): RegistryConfig {
Expand All @@ -20,6 +21,7 @@ export function getRegistryConfig(isV4Deployment: boolean): RegistryConfig {
ecrTags: env.V4_DEPLOY_REGISTRY_ECR_TAGS,
ecrAssumeRoleArn: env.V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN,
ecrAssumeRoleExternalId: env.V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID,
ecrDefaultRepositoryPolicy: env.V4_DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY,
};
}

Expand All @@ -31,5 +33,6 @@ export function getRegistryConfig(isV4Deployment: boolean): RegistryConfig {
ecrTags: env.DEPLOY_REGISTRY_ECR_TAGS,
ecrAssumeRoleArn: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN,
ecrAssumeRoleExternalId: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID,
ecrDefaultRepositoryPolicy: env.DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY,
};
}
1 change: 1 addition & 0 deletions docs/self-hosting/env/webapp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ mode: "wide"
| `DEPLOY_REGISTRY_USERNAME` | No | — | Deploy registry username. |
| `DEPLOY_REGISTRY_PASSWORD` | No | — | Deploy registry password. |
| `DEPLOY_REGISTRY_NAMESPACE` | No | trigger | Deploy registry namespace. |
| `DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY` | No | — | Raw IAM policy JSON applied via SetRepositoryPolicy to every ECR repo created by the webapp. Use to grant cross-account pull access to EKS workers when the ECR account is separate from the cluster account. |
| `DEPLOY_IMAGE_PLATFORM` | No | linux/amd64 | Deploy image platform, same values as docker `--platform` flag. |
| `DEPLOY_TIMEOUT_MS` | No | 480000 (8m) | Deploy timeout (ms). |
| **Object store (S3)** | | | |
Expand Down
Loading