Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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