token-injectable-docker-builder 1.13.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,35 @@ const {
6
6
  CloudWatchLogsClient,
7
7
  GetLogEventsCommand,
8
8
  } = require('@aws-sdk/client-cloudwatch-logs');
9
+ const {
10
+ ECRClient,
11
+ BatchGetImageCommand,
12
+ } = require('@aws-sdk/client-ecr');
13
+
14
+ async function checkReplicaAvailability(repositoryName, imageTag, replicaRegions) {
15
+ for (const region of replicaRegions) {
16
+ const ecr = new ECRClient({ region });
17
+ try {
18
+ const resp = await ecr.send(new BatchGetImageCommand({
19
+ repositoryName,
20
+ imageIds: [{ imageTag }],
21
+ }));
22
+ const images = resp.images || [];
23
+ if (images.length === 0) {
24
+ console.log(`Replica ${region}: repo exists but tag ${imageTag} not yet present.`);
25
+ return false;
26
+ }
27
+ console.log(`Replica ${region}: image present.`);
28
+ } catch (err) {
29
+ if (err.name === 'RepositoryNotFoundException') {
30
+ console.log(`Replica ${region}: destination repo not yet created by ECR replication.`);
31
+ return false;
32
+ }
33
+ throw err;
34
+ }
35
+ }
36
+ return true;
37
+ }
9
38
 
10
39
  exports.handler = async (event) => {
11
40
  console.log('--- isComplete Handler Invoked ---');
@@ -86,9 +115,24 @@ exports.handler = async (event) => {
86
115
  return { IsComplete: false };
87
116
  }
88
117
 
89
- // If build succeeded, return the image tag from the custom resource properties
118
+ // If build succeeded, optionally wait for cross-region replicas to land
119
+ // before signalling complete. ECR replication is async (most images
120
+ // <30 min, rare cases longer) — without this, a consumer stack in
121
+ // another region may try to pull the image before it's been
122
+ // replicated and fail with "Source image does not exist".
90
123
  if (buildStatus === 'SUCCEEDED') {
91
124
  const imageTag = event.ResourceProperties?.ImageTag || process.env.IMAGE_TAG;
125
+ const repositoryName = event.ResourceProperties?.RepositoryName;
126
+ const replicaRegions = JSON.parse(event.ResourceProperties?.ReplicaRegions || '[]');
127
+
128
+ if (replicaRegions.length > 0 && repositoryName) {
129
+ const ready = await checkReplicaAvailability(repositoryName, imageTag, replicaRegions);
130
+ if (!ready) {
131
+ console.log('Build succeeded; waiting for replicas to catch up.');
132
+ return { IsComplete: false };
133
+ }
134
+ }
135
+
92
136
  return {
93
137
  IsComplete: true,
94
138
  Data: {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Parse a .dockerignore file at `sourcePath` (if present) and combine it with
3
+ * a user-supplied exclude list. The Dockerfile itself is never excluded —
4
+ * `.dockerignore` patterns matching `dockerFile` are filtered out so the
5
+ * S3 asset always contains the Dockerfile.
6
+ */
7
+ export declare function resolveExcludes(sourcePath: string, dockerFile: string, exclude: string[] | undefined): string[] | undefined;
8
+ export interface BuildSpecOptions {
9
+ readonly imageTag: string;
10
+ readonly dockerFile?: string;
11
+ readonly buildArgs?: {
12
+ [key: string]: string;
13
+ };
14
+ readonly dockerLoginSecretArn?: string;
15
+ readonly installCommands?: string[];
16
+ readonly preBuildCommands?: string[];
17
+ readonly cacheDisabled: boolean;
18
+ readonly platform: 'linux/amd64' | 'linux/arm64';
19
+ }
20
+ /**
21
+ * Assemble the plain buildspec object that the construct passes to
22
+ * `BuildSpec.fromObject`. Pure function — easy to unit test.
23
+ */
24
+ export declare function buildBuildSpec(opts: BuildSpecOptions): Record<string, unknown>;
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveExcludes = resolveExcludes;
4
+ exports.buildBuildSpec = buildBuildSpec;
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ /**
8
+ * Parse a .dockerignore file at `sourcePath` (if present) and combine it with
9
+ * a user-supplied exclude list. The Dockerfile itself is never excluded —
10
+ * `.dockerignore` patterns matching `dockerFile` are filtered out so the
11
+ * S3 asset always contains the Dockerfile.
12
+ */
13
+ function resolveExcludes(sourcePath, dockerFile, exclude) {
14
+ let effective = exclude;
15
+ if (!effective) {
16
+ const dockerignorePath = path.join(sourcePath, '.dockerignore');
17
+ if (fs.existsSync(dockerignorePath)) {
18
+ const fileContent = fs.readFileSync(dockerignorePath, 'utf8');
19
+ effective = fileContent
20
+ .split('\n')
21
+ .map((line) => line.trim())
22
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
23
+ }
24
+ }
25
+ if (!effective)
26
+ return undefined;
27
+ return effective.filter((pattern) => {
28
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
29
+ const regex = new RegExp(`^${escaped.replace(/\*/g, '.*').replace(/\?/g, '.')}$`, 'i');
30
+ return !regex.test(dockerFile);
31
+ });
32
+ }
33
+ /**
34
+ * Assemble the plain buildspec object that the construct passes to
35
+ * `BuildSpec.fromObject`. Pure function — easy to unit test.
36
+ */
37
+ function buildBuildSpec(opts) {
38
+ const { imageTag, dockerFile, buildArgs, dockerLoginSecretArn, installCommands, preBuildCommands, cacheDisabled, platform, } = opts;
39
+ const buildArgsString = buildArgs
40
+ ? Object.entries(buildArgs)
41
+ .map(([k, v]) => `--build-arg ${k}=${v}`)
42
+ .join(' ')
43
+ : '';
44
+ const dockerFileFlag = dockerFile ? `-f $CODEBUILD_SRC_DIR/${dockerFile}` : '';
45
+ const dockerLoginCommands = dockerLoginSecretArn
46
+ ? [
47
+ 'echo "Retrieving Docker credentials..."',
48
+ 'apt-get update -y && apt-get install -y jq',
49
+ `DOCKER_USERNAME=$(aws secretsmanager get-secret-value --secret-id ${dockerLoginSecretArn} --query SecretString --output text | jq -r .username)`,
50
+ `DOCKER_PASSWORD=$(aws secretsmanager get-secret-value --secret-id ${dockerLoginSecretArn} --query SecretString --output text | jq -r .password)`,
51
+ 'echo "Logging in to Docker Hub..."',
52
+ 'echo $DOCKER_PASSWORD | docker login --username $DOCKER_USERNAME --password-stdin',
53
+ ]
54
+ : ['echo "No Docker credentials. Skipping Docker Hub login."'];
55
+ const buildxInstallCommands = cacheDisabled
56
+ ? []
57
+ : [
58
+ 'echo "Setting up Docker buildx for ECR layer cache..."',
59
+ 'docker buildx create --driver docker-container --name ecr-cache-builder --use 2>/dev/null || docker buildx use ecr-cache-builder',
60
+ ];
61
+ const platformFlag = `--platform ${platform}`;
62
+ // --provenance=false --sbom=false: Docker Buildx v0.10+ adds attestations by default,
63
+ // producing OCI image indexes that AWS Lambda does not support.
64
+ const buildCommand = cacheDisabled
65
+ ? `docker build ${platformFlag} ${dockerFileFlag} ${buildArgsString} -t $ECR_REPO_URI:${imageTag} $CODEBUILD_SRC_DIR`
66
+ : `docker buildx build --push ${platformFlag} --provenance=false --sbom=false --cache-from type=registry,ref=$ECR_REPO_URI:cache --cache-to type=registry,ref=$ECR_REPO_URI:cache,mode=max,image-manifest=true ${dockerFileFlag} ${buildArgsString} -t $ECR_REPO_URI:${imageTag} $CODEBUILD_SRC_DIR`;
67
+ return {
68
+ version: '0.2',
69
+ phases: {
70
+ install: {
71
+ commands: [
72
+ 'echo "Beginning install phase..."',
73
+ ...(installCommands ?? []),
74
+ ...buildxInstallCommands,
75
+ ],
76
+ },
77
+ pre_build: {
78
+ commands: [
79
+ ...(preBuildCommands ?? []),
80
+ ...dockerLoginCommands,
81
+ 'echo "Retrieving AWS Account ID..."',
82
+ 'export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)',
83
+ 'echo "Logging into Amazon ECR..."',
84
+ 'aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com',
85
+ ],
86
+ },
87
+ build: {
88
+ commands: [
89
+ `echo "Building Docker image with tag ${imageTag}..."`,
90
+ buildCommand,
91
+ ],
92
+ },
93
+ ...(cacheDisabled && {
94
+ post_build: {
95
+ commands: [
96
+ `echo "Pushing Docker image with tag ${imageTag}..."`,
97
+ `docker push $ECR_REPO_URI:${imageTag}`,
98
+ ],
99
+ },
100
+ }),
101
+ },
102
+ };
103
+ }
104
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVpbGQtc3BlYy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9idWlsZC1zcGVjLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBU0EsMENBd0JDO0FBaUJELHdDQWtGQztBQXBJRCx5QkFBeUI7QUFDekIsNkJBQTZCO0FBRTdCOzs7OztHQUtHO0FBQ0gsU0FBZ0IsZUFBZSxDQUM3QixVQUFrQixFQUNsQixVQUFrQixFQUNsQixPQUE2QjtJQUU3QixJQUFJLFNBQVMsR0FBRyxPQUFPLENBQUM7SUFDeEIsSUFBSSxDQUFDLFNBQVMsRUFBRSxDQUFDO1FBQ2YsTUFBTSxnQkFBZ0IsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsRUFBRSxlQUFlLENBQUMsQ0FBQztRQUNoRSxJQUFJLEVBQUUsQ0FBQyxVQUFVLENBQUMsZ0JBQWdCLENBQUMsRUFBRSxDQUFDO1lBQ3BDLE1BQU0sV0FBVyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsZ0JBQWdCLEVBQUUsTUFBTSxDQUFDLENBQUM7WUFDOUQsU0FBUyxHQUFHLFdBQVc7aUJBQ3BCLEtBQUssQ0FBQyxJQUFJLENBQUM7aUJBQ1gsR0FBRyxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7aUJBQzFCLE1BQU0sQ0FBQyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsSUFBSSxDQUFDLE1BQU0sR0FBRyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7UUFDaEUsQ0FBQztJQUNILENBQUM7SUFFRCxJQUFJLENBQUMsU0FBUztRQUFFLE9BQU8sU0FBUyxDQUFDO0lBRWpDLE9BQU8sU0FBUyxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFO1FBQ2xDLE1BQU0sT0FBTyxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsbUJBQW1CLEVBQUUsTUFBTSxDQUFDLENBQUM7UUFDN0QsTUFB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ZnJvbSAncGF0aCc7XG5cbi8qKlxuICogUGFyc2UgYSAuZG9ja2VyaWdub3JlIGZpbGUgYXQgYHNvdXJjZVBhdGhgIChpZiBwcmVzZW50KSBhbmQgY29tYmluZSBpdCB3aXRoXG4gKiBhIHVzZXItc3VwcGxpZWQgZXhjbHVkZSBsaXN0LiBUaGUgRG9ja2VyZmlsZSBpdHNlbGYgaXMgbmV2ZXIgZXhjbHVkZWQg4oCUXG4gKiBgLmRvY2tlcmlnbm9yZWAgcGF0dGVybnMgbWF0Y2hpbmcgYGRvY2tlckZpbGVgIGFyZSBmaWx0ZXJlZCBvdXQgc28gdGhlXG4gKiBTMyBhc3NldCBhbHdheXMgY29udGFpbnMgdGhlIERvY2tlcmZpbGUuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZXNvbHZlRXhjbHVkZXMoXG4gIHNvdXJjZVBhdGg6IHN0cmluZyxcbiAgZG9ja2VyRmlsZTogc3RyaW5nLFxuICBleGNsdWRlOiBzdHJpbmdbXSB8IHVuZGVmaW5lZCxcbik6IHN0cmluZ1tdIHwgdW5kZWZpbmVkIHtcbiAgbGV0IGVmZmVjdGl2ZSA9IGV4Y2x1ZGU7XG4gIGlmICghZWZmZWN0aXZlKSB7XG4gICAgY29uc3QgZG9ja2VyaWdub3JlUGF0aCA9IHBhdGguam9pbihzb3VyY2VQYXRoLCAnLmRvY2tlcmlnbm9yZScpO1xuICAgIGlmIChmcy5leGlzdHNTeW5jKGRvY2tlcmlnbm9yZVBhdGgpKSB7XG4gICAgICBjb25zdCBmaWxlQ29udGVudCA9IGZzLnJlYWRGaWxlU3luYyhkb2NrZXJpZ25vcmVQYXRoLCAndXRmOCcpO1xuICAgICAgZWZmZWN0aXZlID0gZmlsZUNvbnRlbnRcbiAgICAgICAgLnNwbGl0KCdcXG4nKVxuICAgICAgICAubWFwKChsaW5lKSA9PiBsaW5lLnRyaW0oKSlcbiAgICAgICAgLmZpbHRlcigobGluZSkgPT4gbGluZS5sZW5ndGggPiAwICYmICFsaW5lLnN0YXJ0c1dpdGgoJyMnKSk7XG4gICAgfVxuICB9XG5cbiAgaWYgKCFlZmZlY3RpdmUpIHJldHVybiB1bmRlZmluZWQ7XG5cbiAgcmV0dXJuIGVmZmVjdGl2ZS5maWx0ZXIoKHBhdHRlcm4pID0+IHtcbiAgICBjb25zdCBlc2NhcGVkID0gcGF0dGVybi5yZXBsYWNlKC9bLiteJHt9KCl8W1xcXVxcXFxdL2csICdcXFxcJCYnKTtcbiAgICBjb25zdCByZWdleCA9IG5ldyBSZWdFeHAoYF4ke2VzY2FwZWQucmVwbGFjZSgvXFwqL2csICcuKicpLnJlcGxhY2UoL1xcPy9nLCAnLicpfSRgLCAnaScpO1xuICAgIHJldHVybiAhcmVnZXgudGVzdChkb2NrZXJGaWxlKTtcbiAgfSk7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgQnVpbGRTcGVjT3B0aW9ucyB7XG4gIHJlYWRvbmx5IGltYWdlVGFnOiBzdHJpbmc7XG4gIHJlYWRvbmx5IGRvY2tlckZpbGU/OiBzdHJpbmc7XG4gIHJlYWRvbmx5IGJ1aWxkQXJncz86IHsgW2tleTogc3RyaW5nXTogc3RyaW5nIH07XG4gIHJlYWRvbmx5IGRvY2tlckxvZ2luU2VjcmV0QXJuPzogc3RyaW5nO1xuICByZWFkb25seSBpbnN0YWxsQ29tbWFuZHM/OiBzdHJpbmdbXTtcbiAgcmVhZG9ubHkgcHJlQnVpbGRDb21tYW5kcz86IHN0cmluZ1tdO1xuICByZWFkb25seSBjYWNoZURpc2FibGVkOiBib29sZWFuO1xuICByZWFkb25seSBwbGF0Zm9ybTogJ2xpbnV4L2FtZDY0JyB8ICdsaW51eC9hcm02NCc7XG59XG5cbi8qKlxuICogQXNzZW1ibGUgdGhlIHBsYWluIGJ1aWxkc3BlYyBvYmplY3QgdGhhdCB0aGUgY29uc3RydWN0IHBhc3NlcyB0b1xuICogYEJ1aWxkU3BlYy5mcm9tT2JqZWN0YC4gUHVyZSBmdW5jdGlvbiDigJQgZWFzeSB0byB1bml0IHRlc3QuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBidWlsZEJ1aWxkU3BlYyhvcHRzOiBCdWlsZFNwZWNPcHRpb25zKTogUmVjb3JkPHN0cmluZywgdW5rbm93bj4ge1xuICBjb25zdCB7XG4gICAgaW1hZ2VUYWcsXG4gICAgZG9ja2VyRmlsZSxcbiAgICBidWlsZEFyZ3MsXG4gICAgZG9ja2VyTG9naW5TZWNyZXRBcm4sXG4gICAgaW5zdGFsbENvbW1hbmRzLFxuICAgIHByZUJ1aWxkQ29tbWFuZHMsXG4gICAgY2FjaGVEaXNhYmxlZCxcbiAgICBwbGF0Zm9ybSxcbiAgfSA9IG9wdHM7XG5cbiAgY29uc3QgYnVpbGRBcmdzU3RyaW5nID0gYnVpbGRBcmdzXG4gICAgPyBPYmplY3QuZW50cmllcyhidWlsZEFyZ3MpXG4gICAgICAubWFwKChbaywgdl0pID0+IGAtLWJ1aWxkLWFyZyAke2t9PSR7dn1gKVxuICAgICAgLmpvaW4oJyAnKVxuICAgIDogJyc7XG5cbiAgY29uc3QgZG9ja2VyRmlsZUZsYWcgPSBkb2NrZXJGaWxlID8gYC1mICRDT0RFQlVJTERfU1JDX0RJUi8ke2RvY2tlckZpbGV9YCA6ICcnO1xuXG4gIGNvbnN0IGRvY2tlckxvZ2luQ29tbWFuZHMgPSBkb2NrZXJMb2dpblNlY3JldEFyblxuICAgID8gW1xuICAgICAgJ2VjaG8gXCJSZXRyaWV2aW5nIERvY2tlciBjcmVkZW50aWFscy4uLlwiJyxcbiAgICAgICdhcHQtZ2V0IHVwZGF0ZSAteSAmJiBhcHQtZ2V0IGluc3RhbGwgLXkganEnLFxuICAgICAgYERPQ0tFUl9VU0VSTkFNRT0kKGF3cyBzZWNyZXRzbWFuYWdlciBnZXQtc2VjcmV0LXZhbHVlIC0tc2VjcmV0LWlkICR7ZG9ja2VyTG9naW5TZWNyZXRBcm59IC0tcXVlcnkgU2VjcmV0U3RyaW5nIC0tb3V0cHV0IHRleHQgfCBqcSAtciAudXNlcm5hbWUpYCxcbiAgICAgIGBET0NLRVJfUEFTU1dPUkQ9JChhd3Mgc2VjcmV0c21hbmFnZXIgZ2V0LXNlY3JldC12YWx1ZSAtLXNlY3JldC1pZCAke2RvY2tlckxvZ2luU2VjcmV0QXJufSAtLXF1ZXJ5IFNlY3JldFN0cmluZyAtLW91dHB1dCB0ZXh0IHwganEgLXIgLnBhc3N3b3JkKWAsXG4gICAgICAnZWNobyBcIkxvZ2dpbmcgaW4gdG8gRG9ja2VyIEh1Yi4uLlwiJyxcbiAgICAgICdlY2hvICRET0NLRVJfUEFTU1dPUkQgfCBkb2NrZXIgbG9naW4gLS11c2VybmFtZSAkRE9DS0VSX1VTRVJOQU1FIC0tcGFzc3dvcmQtc3RkaW4nLFxuICAgIF1cbiAgICA6IFsnZWNobyBcIk5vIERvY2tlciBjcmVkZW50aWFscy4gU2tpcHBpbmcgRG9ja2VyIEh1YiBsb2dpbi5cIiddO1xuXG4gIGNvbnN0IGJ1aWxkeEluc3RhbGxDb21tYW5kcyA9IGNhY2hlRGlzYWJsZWRcbiAgICA/IFtdXG4gICAgOiBbXG4gICAgICAnZWNobyBcIlNldHRpbmcgdXAgRG9ja2VyIGJ1aWxkeCBmb3IgRUNSIGxheWVyIGNhY2hlLi4uXCInLFxuICAgICAgJ2RvY2tlciBidWlsZHggY3JlYXRlIC0tZHJpdmVyIGRvY2tlci1jb250YWluZXIgLS1uYW1lIGVjci1jYWNoZS1idWlsZGVyIC0tdXNlIDI+L2Rldi9udWxsIHx8IGRvY2tlciBidWlsZHggdXNlIGVjci1jYWNoZS1idWlsZGVyJyxcbiAgICBdO1xuXG4gIGNvbnN0IHBsYXRmb3JtRmxhZyA9IGAtLXBsYXRmb3JtICR7cGxhdGZvcm19YDtcblxuICAvLyAtLXByb3ZlbmFuY2U9ZmFsc2UgLS1zYm9tPWZhbHNlOiBEb2NrZXIgQnVpbGR4IHYwLjEwKyBhZGRzIGF0dGVzdGF0aW9ucyBieSBkZWZhdWx0LFxuICAvLyBwcm9kdWNpbmcgT0NJIGltYWdlIGluZGV4ZXMgdGhhdCBBV1MgTGFtYmRhIGRvZXMgbm90IHN1cHBvcnQuXG4gIGNvbnN0IGJ1aWxkQ29tbWFuZCA9IGNhY2hlRGlzYWJsZWRcbiAgICA/IGBkb2NrZXIgYnVpbGQgJHtwbGF0Zm9ybUZsYWd9ICR7ZG9ja2VyRmlsZUZsYWd9ICR7YnVpbGRBcmdzU3RyaW5nfSAtdCAkRUNSX1JFUE9fVVJJOiR7aW1hZ2VUYWd9ICRDT0RFQlVJTERfU1JDX0RJUmBcbiAgICA6IGBkb2NrZXIgYnVpbGR4IGJ1aWxkIC0tcHVzaCAke3BsYXRmb3JtRmxhZ30gLS1wcm92ZW5hbmNlPWZhbHNlIC0tc2JvbT1mYWxzZSAtLWNhY2hlLWZyb20gdHlwZT1yZWdpc3RyeSxyZWY9JEVDUl9SRVBPX1VSSTpjYWNoZSAtLWNhY2hlLXRvIHR5cGU9cmVnaXN0cnkscmVmPSRFQ1JfUkVQT19VUkk6Y2FjaGUsbW9kZT1tYXgsaW1hZ2UtbWFuaWZlc3Q9dHJ1ZSAke2RvY2tlckZpbGVGbGFnfSAke2J1aWxkQXJnc1N0cmluZ30gLXQgJEVDUl9SRVBPX1VSSToke2ltYWdlVGFnfSAkQ09ERUJVSUxEX1NSQ19ESVJgO1xuXG4gIHJldHVybiB7XG4gICAgdmVyc2lvbjogJzAuMicsXG4gICAgcGhhc2VzOiB7XG4gICAgICBpbnN0YWxsOiB7XG4gICAgICAgIGNvbW1hbmRzOiBbXG4gICAgICAgICAgJ2VjaG8gXCJCZWdpbm5pbmcgaW5zdGFsbCBwaGFzZS4uLlwiJyxcbiAgICAgICAgICAuLi4oaW5zdGFsbENvbW1hbmRzID8/IFtdKSxcbiAgICAgICAgICAuLi5idWlsZHhJbnN0YWxsQ29tbWFuZHMsXG4gICAgICAgIF0sXG4gICAgICB9LFxuICAgICAgcHJlX2J1aWxkOiB7XG4gICAgICAgIGNvbW1hbmRzOiBbXG4gICAgICAgICAgLi4uKHByZUJ1aWxkQ29tbWFuZHMgPz8gW10pLFxuICAgICAgICAgIC4uLmRvY2tlckxvZ2luQ29tbWFuZHMsXG4gICAgICAgICAgJ2VjaG8gXCJSZXRyaWV2aW5nIEFXUyBBY2NvdW50IElELi4uXCInLFxuICAgICAgICAgICdleHBvcnQgQUNDT1VOVF9JRD0kKGF3cyBzdHMgZ2V0LWNhbGxlci1pZGVudGl0eSAtLXF1ZXJ5IEFjY291bnQgLS1vdXRwdXQgdGV4dCknLFxuICAgICAgICAgICdlY2hvIFwiTG9nZ2luZyBpbnRvIEFtYXpvbiBFQ1IuLi5cIicsXG4gICAgICAgICAgJ2F3cyBlY3IgZ2V0LWxvZ2luLXBhc3N3b3JkIC0tcmVnaW9uICRBV1NfREVGQVVMVF9SRUdJT04gfCBkb2NrZXIgbG9naW4gLS11c2VybmFtZSBBV1MgLS1wYXNzd29yZC1zdGRpbiAkQUNDT1VOVF9JRC5ka3IuZWNyLiRBV1NfREVGQVVMVF9SRUdJT04uYW1hem9uYXdzLmNvbScsXG4gICAgICAgIF0sXG4gICAgICB9LFxuICAgICAgYnVpbGQ6IHtcbiAgICAgICAgY29tbWFuZHM6IFtcbiAgICAgICAgICBgZWNobyBcIkJ1aWxkaW5nIERvY2tlciBpbWFnZSB3aXRoIHRhZyAke2ltYWdlVGFnfS4uLlwiYCxcbiAgICAgICAgICBidWlsZENvbW1hbmQsXG4gICAgICAgIF0sXG4gICAgICB9LFxuICAgICAgLi4uKGNhY2hlRGlzYWJsZWQgJiYge1xuICAgICAgICBwb3N0X2J1aWxkOiB7XG4gICAgICAgICAgY29tbWFuZHM6IFtcbiAgICAgICAgICAgIGBlY2hvIFwiUHVzaGluZyBEb2NrZXIgaW1hZ2Ugd2l0aCB0YWcgJHtpbWFnZVRhZ30uLi5cImAsXG4gICAgICAgICAgICBgZG9ja2VyIHB1c2ggJEVDUl9SRVBPX1VSSToke2ltYWdlVGFnfWAsXG4gICAgICAgICAgXSxcbiAgICAgICAgfSxcbiAgICAgIH0pLFxuICAgIH0sXG4gIH07XG59XG4iXX0=
@@ -0,0 +1,206 @@
1
+ import { IVpc, ISecurityGroup, SubnetSelection } from 'aws-cdk-lib/aws-ec2';
2
+ import { ContainerImage } from 'aws-cdk-lib/aws-ecs';
3
+ import { DockerImageCode } from 'aws-cdk-lib/aws-lambda';
4
+ import { ILogGroup } from 'aws-cdk-lib/aws-logs';
5
+ import { Construct } from 'constructs';
6
+ import { TokenInjectableDockerBuilderProvider } from './provider';
7
+ /**
8
+ * Properties for the `TokenInjectableDockerBuilder` construct.
9
+ */
10
+ export interface TokenInjectableDockerBuilderProps {
11
+ /**
12
+ * The path to the directory containing the Dockerfile or source code.
13
+ */
14
+ readonly path: string;
15
+ /**
16
+ * Build arguments to pass to the Docker build process.
17
+ * These are transformed into `--build-arg KEY=VALUE` flags.
18
+ * @example
19
+ * {
20
+ * TOKEN: 'my-secret-token',
21
+ * ENV: 'production'
22
+ * }
23
+ */
24
+ readonly buildArgs?: {
25
+ [key: string]: string;
26
+ };
27
+ /**
28
+ * The ARN of the AWS Secrets Manager secret containing Docker login credentials.
29
+ * The secret must store a JSON object: `{"username":"...","password":"..."}`.
30
+ * Must be in the same region as the stack.
31
+ *
32
+ * @default - No Docker Hub login.
33
+ */
34
+ readonly dockerLoginSecretArn?: string;
35
+ /**
36
+ * The VPC in which the CodeBuild project will be deployed.
37
+ *
38
+ * @default - CodeBuild uses public internet.
39
+ */
40
+ readonly vpc?: IVpc;
41
+ /**
42
+ * Security groups attached to the CodeBuild project.
43
+ *
44
+ * @default - No security groups attached.
45
+ */
46
+ readonly securityGroups?: ISecurityGroup[];
47
+ /**
48
+ * Subnet selection within the VPC.
49
+ *
50
+ * @default - All subnets in the VPC.
51
+ */
52
+ readonly subnetSelection?: SubnetSelection;
53
+ /**
54
+ * Custom commands to run during the install phase of CodeBuild.
55
+ *
56
+ * @default - No additional install commands.
57
+ */
58
+ readonly installCommands?: string[];
59
+ /**
60
+ * Custom commands to run during the pre_build phase of CodeBuild.
61
+ *
62
+ * @default - No additional pre-build commands.
63
+ */
64
+ readonly preBuildCommands?: string[];
65
+ /**
66
+ * Whether to enable KMS encryption for the ECR repository.
67
+ *
68
+ * @default false
69
+ */
70
+ readonly kmsEncryption?: boolean;
71
+ /**
72
+ * File paths in the Docker directory to exclude from the build asset.
73
+ * Falls back to `.dockerignore` if present.
74
+ *
75
+ * @default - No file path exclusions.
76
+ */
77
+ readonly exclude?: string[];
78
+ /**
79
+ * Name of the Dockerfile (passed as `-f`).
80
+ *
81
+ * @example 'Dockerfile.production'
82
+ * @default 'Dockerfile'
83
+ */
84
+ readonly file?: string;
85
+ /**
86
+ * When `true`, disables Docker layer caching.
87
+ *
88
+ * @default false
89
+ */
90
+ readonly cacheDisabled?: boolean;
91
+ /**
92
+ * CloudWatch log group for CodeBuild build logs.
93
+ *
94
+ * @default - CodeBuild default logging.
95
+ */
96
+ readonly buildLogGroup?: ILogGroup;
97
+ /**
98
+ * Target platform for the Docker image.
99
+ *
100
+ * @default 'linux/amd64'
101
+ */
102
+ readonly platform?: 'linux/amd64' | 'linux/arm64';
103
+ /**
104
+ * Shared provider for the custom resource Lambdas.
105
+ *
106
+ * Pass `TokenInjectableDockerBuilderProvider.getOrCreate(this, { queryInterval })`
107
+ * if you need a non-default query interval. Otherwise, the construct will
108
+ * call `getOrCreate(this)` itself and reuse the per-stack singleton.
109
+ *
110
+ * @default - Per-stack singleton provider, created on first use.
111
+ */
112
+ readonly provider?: TokenInjectableDockerBuilderProvider;
113
+ /**
114
+ * ECR pull-through cache repository prefixes to grant pull access to.
115
+ *
116
+ * @example ['docker-hub', 'ghcr']
117
+ * @default - No pull-through cache access.
118
+ */
119
+ readonly ecrPullThroughCachePrefixes?: string[];
120
+ /**
121
+ * When `true`, creates a CloudWatch log group outside of CloudFormation
122
+ * (`/docker-builder/<projectName>`) and directs CodeBuild output there.
123
+ * Survives stack rollbacks for debugging. 7-day retention.
124
+ *
125
+ * @default false
126
+ */
127
+ readonly retainBuildLogs?: boolean;
128
+ /**
129
+ * Additional AWS regions to replicate the built image to via ECR's
130
+ * native registry replication. The image is pushed to the primary
131
+ * region's ECR as usual; ECR asynchronously replicates the same
132
+ * `repositoryName` + `imageTag` to each region listed here.
133
+ *
134
+ * Consumers in another region (a Lambda in `us-west-2` referencing an
135
+ * image built in `us-east-1`) can use `dockerImageCodeFor(region)` or
136
+ * `containerImageFor(region)` to import the replicated image.
137
+ *
138
+ * The custom resource waits for replication to complete before
139
+ * signalling deploy-complete, so downstream stacks can safely deploy
140
+ * immediately after.
141
+ *
142
+ * **Caveats:**
143
+ * - Cross-region replication is not supported between AWS partitions.
144
+ * - Replicas do **not** inherit the primary's encryption (defaults to
145
+ * AES-256), lifecycle policies, or repository policies.
146
+ * - Replicated repositories persist on stack deletion — AWS does not
147
+ * auto-delete them. Clean up manually via the ECR console / CLI if
148
+ * needed.
149
+ * - Both the builder stack and any consumer stack in another region
150
+ * must set `crossRegionReferences: true` for the image tag to flow.
151
+ * - Stacks must have a concrete region (`env: { account, region }`),
152
+ * not the env-agnostic default.
153
+ *
154
+ * @example ['us-west-2', 'eu-west-1']
155
+ * @default [] - no replication
156
+ */
157
+ readonly replicaRegions?: string[];
158
+ }
159
+ /**
160
+ * A CDK construct to build and push Docker images to an ECR repository using
161
+ * CodeBuild and Lambda custom resources, **then** retrieve the final image tag
162
+ * so that ECS/Lambda references use the exact built image.
163
+ */
164
+ export declare class TokenInjectableDockerBuilder extends Construct {
165
+ /** The ECR repository that stores the resulting Docker image. */
166
+ private readonly ecrRepository;
167
+ /** ECS-compatible container image reference (primary region). */
168
+ readonly containerImage: ContainerImage;
169
+ /** Lambda-compatible DockerImageCode reference (primary region). */
170
+ readonly dockerImageCode: DockerImageCode;
171
+ /** The ECR repository name — preserved across replica regions. */
172
+ readonly repositoryName: string;
173
+ /** The resolved image tag (CFN token; available at deploy time). */
174
+ readonly imageTag: string;
175
+ private readonly primaryRegion;
176
+ private readonly accountId;
177
+ private readonly replicaRegions;
178
+ constructor(scope: Construct, id: string, props: TokenInjectableDockerBuilderProps);
179
+ /**
180
+ * Format the ECR repository URI for a given region. The region must
181
+ * be either the primary region or one of `replicaRegions`.
182
+ */
183
+ repositoryUriFor(region: string): string;
184
+ /**
185
+ * Import the replicated repository as an ECS-compatible
186
+ * `ContainerImage` in a consumer scope (typically a stack in `region`).
187
+ *
188
+ * The consumer's stack must have `crossRegionReferences: true` when
189
+ * `region` differs from the builder's region.
190
+ */
191
+ containerImageFor(scope: Construct, region: string): ContainerImage;
192
+ /**
193
+ * Import the replicated repository as a Lambda-compatible
194
+ * `DockerImageCode` in a consumer scope (typically a stack in `region`).
195
+ *
196
+ * The consumer's stack must have `crossRegionReferences: true` when
197
+ * `region` differs from the builder's region.
198
+ */
199
+ dockerImageCodeFor(scope: Construct, region: string): DockerImageCode;
200
+ private importRepoFor;
201
+ private assertRegionIsKnown;
202
+ private validateReplicaRegions;
203
+ private grantEcrAccess;
204
+ private grantPullThroughCacheAccess;
205
+ private grantBuildLogsAccess;
206
+ }