token-injectable-docker-builder 0.1.2 → 0.1.3

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.
package/lib/index.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { DockerImageCode } from 'aws-cdk-lib/aws-lambda';
2
+ import { Construct } from 'constructs';
3
+ import { ContainerImage } from 'aws-cdk-lib/aws-ecs';
4
+ export interface TokenInjectableDockerBuilderProps {
5
+ path: string;
6
+ buildArgs?: {
7
+ [key: string]: string;
8
+ };
9
+ }
10
+ export declare class TokenInjectableDockerBuilder extends Construct {
11
+ private readonly ecrRepository;
12
+ private readonly buildTriggerResource;
13
+ constructor(scope: Construct, id: string, props: TokenInjectableDockerBuilderProps);
14
+ getContainerImage(): ContainerImage;
15
+ getDockerImageCode(): DockerImageCode;
16
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,171 @@
1
+ import * as path from 'path';
2
+ import { Duration, CustomResource, Stack } from 'aws-cdk-lib';
3
+ import { Project, Source, LinuxBuildImage, BuildSpec } from 'aws-cdk-lib/aws-codebuild';
4
+ import { Repository } from 'aws-cdk-lib/aws-ecr';
5
+ import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
6
+ import { Code, DockerImageCode, Runtime } from 'aws-cdk-lib/aws-lambda';
7
+ import { Asset } from 'aws-cdk-lib/aws-s3-assets';
8
+ import { Provider } from 'aws-cdk-lib/custom-resources';
9
+ import { Construct } from 'constructs';
10
+ import { ContainerImage } from 'aws-cdk-lib/aws-ecs';
11
+ import * as crypto from 'crypto';
12
+ import { Function } from 'aws-cdk-lib/aws-lambda';
13
+
14
+ export interface TokenInjectableDockerBuilderProps {
15
+ path: string;
16
+ buildArgs?: { [key: string]: string };
17
+ }
18
+
19
+ export class TokenInjectableDockerBuilder extends Construct {
20
+ private readonly ecrRepository: Repository;
21
+ private readonly buildTriggerResource: CustomResource;
22
+
23
+ constructor(scope: Construct, id: string, props: TokenInjectableDockerBuilderProps) {
24
+ super(scope, id);
25
+
26
+ const { path: sourcePath, buildArgs } = props; // Default to linux/amd64
27
+
28
+ // Define absolute paths for Lambda handlers
29
+ const onEventHandlerPath = path.resolve(__dirname, '../src/onEventHandler');
30
+ const isCompleteHandlerPath = path.resolve(__dirname, '../src/isCompleteHandler');
31
+
32
+ // Create an ECR repository
33
+ this.ecrRepository = new Repository(this, 'ECRRepository');
34
+
35
+ // Package the source code as an asset
36
+ const sourceAsset = new Asset(this, 'SourceAsset', {
37
+ path: sourcePath, // Path to the Dockerfile or source code
38
+ });
39
+
40
+ // Transform buildArgs into a string of --build-arg KEY=VALUE
41
+ const buildArgsString = buildArgs
42
+ ? Object.entries(buildArgs)
43
+ .map(([key, value]) => `--build-arg ${key}=${value}`)
44
+ .join(' ')
45
+ : '';
46
+
47
+ // Pass the buildArgsString and platform as environment variables
48
+ const environmentVariables: { [name: string]: { value: string } } = {
49
+ ECR_REPO_URI: { value: this.ecrRepository.repositoryUri },
50
+ BUILD_ARGS: { value: buildArgsString },
51
+ };
52
+
53
+ // Create a CodeBuild project
54
+ const codeBuildProject = new Project(this, 'UICodeBuildProject', {
55
+ source: Source.s3({
56
+ bucket: sourceAsset.bucket,
57
+ path: sourceAsset.s3ObjectKey,
58
+ }),
59
+ environment: {
60
+ buildImage: LinuxBuildImage.STANDARD_7_0,
61
+ privileged: true, // Required for Docker builds
62
+ },
63
+ environmentVariables: environmentVariables,
64
+ buildSpec: BuildSpec.fromObject({
65
+ version: '0.2',
66
+ phases: {
67
+ pre_build: {
68
+ commands: [
69
+ 'echo "Retrieving AWS Account ID..."',
70
+ 'export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)',
71
+ 'echo "Logging in to Amazon ECR..."',
72
+ 'aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com',
73
+ ],
74
+ },
75
+ build: {
76
+ commands: [
77
+ 'echo Build phase: Building the Docker image...',
78
+ 'docker build $BUILD_ARGS -t $ECR_REPO_URI:latest $CODEBUILD_SRC_DIR',
79
+ ],
80
+ },
81
+ post_build: {
82
+ commands: [
83
+ 'echo Post-build phase: Pushing the Docker image...',
84
+ 'docker push $ECR_REPO_URI:latest',
85
+ ],
86
+ },
87
+ },
88
+ }),
89
+ });
90
+
91
+ // Grant permissions to interact with ECR
92
+ this.ecrRepository.grantPullPush(codeBuildProject);
93
+
94
+ codeBuildProject.role!.addToPrincipalPolicy(
95
+ new PolicyStatement({
96
+ actions: ['ecr:GetAuthorizationToken'],
97
+ resources: ['*'],
98
+ })
99
+ );
100
+
101
+ // Grant permissions to CodeBuild for CloudWatch Logs
102
+ codeBuildProject.role!.addToPrincipalPolicy(
103
+ new PolicyStatement({
104
+ actions: ['logs:PutLogEvents', 'logs:CreateLogGroup', 'logs:CreateLogStream'],
105
+ resources: [`arn:aws:logs:${Stack.of(this).region}:${Stack.of(this).account}:*`],
106
+ })
107
+ );
108
+
109
+ // Create Node.js Lambda function for onEvent
110
+ const onEventHandlerFunction = new Function(this, 'OnEventHandlerFunction', {
111
+ runtime: Runtime.NODEJS_18_X, // Use Node.js runtime
112
+ code: Code.fromAsset(onEventHandlerPath), // Path to handler code
113
+ handler: 'index.handler', // Entry point (adjust as needed)
114
+ timeout: Duration.minutes(15),
115
+ });
116
+
117
+ onEventHandlerFunction.addToRolePolicy(
118
+ new PolicyStatement({
119
+ actions: ['codebuild:StartBuild'],
120
+ resources: [codeBuildProject.projectArn], // Restrict to specific project
121
+ })
122
+ );
123
+
124
+ // Create Node.js Lambda function for isComplete
125
+ const isCompleteHandlerFunction = new Function(this, 'IsCompleteHandlerFunction', {
126
+ runtime: Runtime.NODEJS_18_X,
127
+ code: Code.fromAsset(isCompleteHandlerPath),
128
+ handler: 'index.handler',
129
+ timeout: Duration.minutes(15),
130
+ });
131
+
132
+ isCompleteHandlerFunction.addToRolePolicy(
133
+ new PolicyStatement({
134
+ actions: [
135
+ 'codebuild:BatchGetBuilds',
136
+ 'codebuild:ListBuildsForProject',
137
+ 'logs:GetLogEvents',
138
+ 'logs:DescribeLogStreams',
139
+ 'logs:DescribeLogGroups'
140
+ ],
141
+ resources: ['*'],
142
+ })
143
+ );
144
+
145
+ // Create a custom resource provider
146
+ const provider = new Provider(this, 'CustomResourceProvider', {
147
+ onEventHandler: onEventHandlerFunction,
148
+ isCompleteHandler: isCompleteHandlerFunction,
149
+ queryInterval: Duration.minutes(1),
150
+ });
151
+
152
+ // Define the custom resource
153
+ this.buildTriggerResource = new CustomResource(this, 'BuildTriggerResource', {
154
+ serviceToken: provider.serviceToken,
155
+ properties: {
156
+ ProjectName: codeBuildProject.projectName,
157
+ Trigger: crypto.randomUUID(),
158
+ },
159
+ });
160
+
161
+ this.buildTriggerResource.node.addDependency(codeBuildProject);
162
+ }
163
+
164
+ public getContainerImage(): ContainerImage {
165
+ return ContainerImage.fromEcrRepository(this.ecrRepository, 'latest');
166
+ }
167
+
168
+ public getDockerImageCode(): DockerImageCode {
169
+ return DockerImageCode.fromEcr(this.ecrRepository);
170
+ }
171
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-injectable-docker-builder",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
@@ -21,5 +21,30 @@
21
21
  "peerDependencies": {
22
22
  "aws-cdk-lib": "2.166.0",
23
23
  "constructs": "^10.0.0"
24
- }
24
+ },
25
+ "keywords": [
26
+ "aws",
27
+ "cdk",
28
+ "aws-cdk",
29
+ "docker",
30
+ "ecr",
31
+ "lambda",
32
+ "custom-resource",
33
+ "docker-build",
34
+ "codebuild",
35
+ "token-injection",
36
+ "docker-image",
37
+ "aws-codebuild",
38
+ "aws-ecr",
39
+ "docker-builder",
40
+ "cdk-construct",
41
+ "lambda-custom-resource",
42
+ "container-image",
43
+ "aws-lambda",
44
+ "aws-cdk-lib",
45
+ "cloud-development-kit",
46
+ "ci-cd",
47
+ "aws-ci-cd",
48
+ "infrastructure-as-code"
49
+ ]
25
50
  }
@@ -0,0 +1,97 @@
1
+ const { CodeBuildClient, ListBuildsForProjectCommand, BatchGetBuildsCommand } = require('@aws-sdk/client-codebuild');
2
+ const { CloudWatchLogsClient, GetLogEventsCommand } = require('@aws-sdk/client-cloudwatch-logs');
3
+
4
+ exports.handler = async (event, context) => {
5
+ console.log('isCompleteHandler Event:', JSON.stringify(event, null, 2));
6
+
7
+ // Initialize AWS SDK v3 clients
8
+ const codebuildClient = new CodeBuildClient({ region: process.env.AWS_REGION });
9
+ const cloudwatchlogsClient = new CloudWatchLogsClient({ region: process.env.AWS_REGION });
10
+
11
+ try {
12
+ const projectName = event.ResourceProperties.ProjectName;
13
+
14
+ if (!projectName) {
15
+ throw new Error('ProjectName is required in ResourceProperties');
16
+ }
17
+
18
+ console.log(`Checking status for CodeBuild project: ${projectName}`);
19
+
20
+ // Retrieve the latest build for the given project
21
+ const listBuildsCommand = new ListBuildsForProjectCommand({
22
+ projectName: projectName,
23
+ sortOrder: 'DESCENDING',
24
+ maxResults: 1,
25
+ });
26
+
27
+ const listBuildsResp = await codebuildClient.send(listBuildsCommand);
28
+ const buildIds = listBuildsResp.ids;
29
+
30
+ if (!buildIds || buildIds.length === 0) {
31
+ throw new Error(`No builds found for project: ${projectName}`);
32
+ }
33
+
34
+ const buildId = buildIds[0];
35
+ console.log(`Latest Build ID: ${buildId}`);
36
+
37
+ // Get build details
38
+ const batchGetBuildsCommand = new BatchGetBuildsCommand({
39
+ ids: [buildId],
40
+ });
41
+
42
+ const buildDetailsResp = await codebuildClient.send(batchGetBuildsCommand);
43
+ const build = buildDetailsResp.builds[0];
44
+
45
+ if (!build) {
46
+ throw new Error(`Build details not found for Build ID: ${buildId}`);
47
+ }
48
+
49
+ const buildStatus = build.buildStatus;
50
+ console.log(`Build Status: ${buildStatus}`);
51
+
52
+ if (buildStatus === 'IN_PROGRESS') {
53
+ // Build is still in progress
54
+ console.log('Build is still in progress.');
55
+ return { IsComplete: false };
56
+ } else if (buildStatus === 'SUCCEEDED') {
57
+ // Build succeeded
58
+ console.log('Build succeeded.');
59
+ return { IsComplete: true };
60
+ } else if (['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT'].includes(buildStatus)) {
61
+ // Build failed; retrieve last 5 log lines
62
+ const logsInfo = build.logs;
63
+ if (logsInfo && logsInfo.groupName && logsInfo.streamName) {
64
+ console.log(`Retrieving logs from CloudWatch Logs Group: ${logsInfo.groupName}, Stream: ${logsInfo.streamName}`);
65
+
66
+ const getLogEventsCommand = new GetLogEventsCommand({
67
+ logGroupName: logsInfo.groupName,
68
+ logStreamName: logsInfo.streamName,
69
+ startFromHead: false, // Start from the end to get latest logs
70
+ limit: 5,
71
+ });
72
+
73
+ const logEventsResp = await cloudwatchlogsClient.send(getLogEventsCommand);
74
+ const logEvents = logEventsResp.events;
75
+ const lastFiveMessages = logEvents.map((event) => event.message).reverse().join('\n');
76
+
77
+ const errorMessage = `Build failed with status: ${buildStatus}\nLast 5 build logs:\n${lastFiveMessages}`;
78
+ console.error(errorMessage);
79
+
80
+ // Throw an error to indicate failure to the CDK provider
81
+ throw new Error(errorMessage);
82
+ } else {
83
+ const errorMessage = `Build failed with status: ${buildStatus}, but logs are not available.`;
84
+ console.error(errorMessage);
85
+ throw new Error(errorMessage);
86
+ }
87
+ } else {
88
+ const errorMessage = `Unknown build status: ${buildStatus}`;
89
+ console.error(errorMessage);
90
+ throw new Error(errorMessage);
91
+ }
92
+ } catch (error) {
93
+ console.error('Error in isCompleteHandler:', error);
94
+ // Rethrow the error to inform the CDK provider of the failure
95
+ throw error;
96
+ }
97
+ };
@@ -0,0 +1,39 @@
1
+ const { CodeBuildClient, StartBuildCommand } = require('@aws-sdk/client-codebuild');
2
+
3
+ exports.handler = async (event, context) => {
4
+ console.log('Event:', JSON.stringify(event, null, 2));
5
+
6
+ // Initialize the AWS SDK v3 CodeBuild client
7
+ const codebuildClient = new CodeBuildClient({ region: process.env.AWS_REGION });
8
+
9
+ // Set the PhysicalResourceId
10
+ let physicalResourceId = event.PhysicalResourceId || event.LogicalResourceId;
11
+
12
+ if (event.RequestType === 'Create' || event.RequestType === 'Update') {
13
+ const params = {
14
+ projectName: event.ResourceProperties.ProjectName,
15
+ };
16
+
17
+ try {
18
+ const command = new StartBuildCommand(params); // Create the command
19
+ const build = await codebuildClient.send(command); // Send the command
20
+ console.log('Started build:', JSON.stringify(build, null, 2));
21
+ } catch (error) {
22
+ console.error('Error starting build:', error);
23
+
24
+ return {
25
+ PhysicalResourceId: physicalResourceId,
26
+ Data: {},
27
+ Reason: error.message,
28
+ };
29
+ }
30
+ } else if (event.RequestType === 'Delete') {
31
+ // No action needed for delete, but ensure PhysicalResourceId remains the same
32
+ console.log('Delete request received. No action required.');
33
+ }
34
+
35
+ return {
36
+ PhysicalResourceId: physicalResourceId,
37
+ Data: {},
38
+ };
39
+ };