|
7 | 7 | RepositoryNotFoundException, |
8 | 8 | GetAuthorizationTokenCommand, |
9 | 9 | PutLifecyclePolicyCommand, |
| 10 | + PutImageTagMutabilityCommand, |
10 | 11 | } from "@aws-sdk/client-ecr"; |
11 | 12 | import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; |
12 | 13 | import { tryCatch } from "@trigger.dev/core"; |
@@ -196,6 +197,22 @@ export function parseRegistryTags(tags: string): Tag[] { |
196 | 197 | .filter((tag): tag is Tag => tag !== null); |
197 | 198 | } |
198 | 199 |
|
| 200 | +const untaggedImageExpirationPolicy = JSON.stringify({ |
| 201 | + rules: [ |
| 202 | + { |
| 203 | + rulePriority: 1, |
| 204 | + description: "Expire untagged images older than 3 days", |
| 205 | + selection: { |
| 206 | + tagStatus: "untagged", |
| 207 | + countType: "sinceImagePushed", |
| 208 | + countUnit: "days", |
| 209 | + countNumber: 3, |
| 210 | + }, |
| 211 | + action: { type: "expire" }, |
| 212 | + }, |
| 213 | + ], |
| 214 | +}); |
| 215 | + |
199 | 216 | async function createEcrRepository({ |
200 | 217 | repositoryName, |
201 | 218 | region, |
@@ -241,27 +258,62 @@ async function createEcrRepository({ |
241 | 258 | new PutLifecyclePolicyCommand({ |
242 | 259 | repositoryName: result.repository.repositoryName, |
243 | 260 | registryId: result.repository.registryId, |
244 | | - lifecyclePolicyText: JSON.stringify({ |
245 | | - rules: [ |
246 | | - { |
247 | | - rulePriority: 1, |
248 | | - description: "Expire untagged images older than 3 days", |
249 | | - selection: { |
250 | | - tagStatus: "untagged", |
251 | | - countType: "sinceImagePushed", |
252 | | - countUnit: "days", |
253 | | - countNumber: 3, |
254 | | - }, |
255 | | - action: { type: "expire" }, |
256 | | - }, |
257 | | - ], |
258 | | - }), |
| 261 | + lifecyclePolicyText: untaggedImageExpirationPolicy, |
259 | 262 | }) |
260 | 263 | ); |
261 | 264 |
|
262 | 265 | return result.repository; |
263 | 266 | } |
264 | 267 |
|
| 268 | +async function updateEcrRepositoryCacheSettings({ |
| 269 | + repositoryName, |
| 270 | + region, |
| 271 | + accountId, |
| 272 | + assumeRole, |
| 273 | +}: { |
| 274 | + repositoryName: string; |
| 275 | + region: string; |
| 276 | + accountId?: string; |
| 277 | + assumeRole?: AssumeRoleConfig; |
| 278 | +}): Promise<void> { |
| 279 | + logger.debug("Updating ECR repository tag mutability to IMMUTABLE_WITH_EXCLUSION", { |
| 280 | + repositoryName, |
| 281 | + region, |
| 282 | + }); |
| 283 | + |
| 284 | + const ecr = await createEcrClient({ region, assumeRole }); |
| 285 | + |
| 286 | + await ecr.send( |
| 287 | + new PutImageTagMutabilityCommand({ |
| 288 | + repositoryName, |
| 289 | + registryId: accountId, |
| 290 | + imageTagMutability: "IMMUTABLE_WITH_EXCLUSION", |
| 291 | + imageTagMutabilityExclusionFilters: [ |
| 292 | + { |
| 293 | + // only the `cache` tag will be mutable, all other tags will be immutable |
| 294 | + filter: "cache", |
| 295 | + filterType: "WILDCARD", |
| 296 | + }, |
| 297 | + ], |
| 298 | + }) |
| 299 | + ); |
| 300 | + |
| 301 | + // When the `cache` tag is mutated, the old cache images are untagged. |
| 302 | + // This policy matches those images and expires them to avoid bloating the repository. |
| 303 | + await ecr.send( |
| 304 | + new PutLifecyclePolicyCommand({ |
| 305 | + repositoryName, |
| 306 | + registryId: accountId, |
| 307 | + lifecyclePolicyText: untaggedImageExpirationPolicy, |
| 308 | + }) |
| 309 | + ); |
| 310 | + |
| 311 | + logger.debug("Successfully updated ECR repository to IMMUTABLE_WITH_EXCLUSION", { |
| 312 | + repositoryName, |
| 313 | + region, |
| 314 | + }); |
| 315 | +} |
| 316 | + |
265 | 317 | async function getEcrRepository({ |
266 | 318 | repositoryName, |
267 | 319 | region, |
@@ -350,6 +402,22 @@ async function ensureEcrRepositoryExists({ |
350 | 402 |
|
351 | 403 | if (existingRepo) { |
352 | 404 | logger.debug("ECR repository already exists", { repositoryName, region, existingRepo }); |
| 405 | + |
| 406 | + // check if the repository is missing the cache settings |
| 407 | + if (existingRepo.imageTagMutability === "IMMUTABLE") { |
| 408 | + const [updateError] = await tryCatch( |
| 409 | + updateEcrRepositoryCacheSettings({ repositoryName, region, accountId, assumeRole }) |
| 410 | + ); |
| 411 | + |
| 412 | + if (updateError) { |
| 413 | + logger.error("Failed to update ECR repository cache settings", { |
| 414 | + repositoryName, |
| 415 | + region, |
| 416 | + updateError, |
| 417 | + }); |
| 418 | + } |
| 419 | + } |
| 420 | + |
353 | 421 | return { |
354 | 422 | repo: existingRepo, |
355 | 423 | repoCreated: false, |
|
0 commit comments