@aws-cdk/aws-codebuild
Version:
The CDK Construct Library for AWS::CodeBuild
1,102 lines • 215 kB
JavaScript
"use strict";
var _a, _b, _c;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProjectNotificationEvents = exports.BuildEnvironmentVariableType = exports.WindowsBuildImage = exports.WindowsImageType = exports.LinuxBuildImage = exports.ImagePullPrincipalType = exports.ComputeType = exports.Project = void 0;
const jsiiDeprecationWarnings = require("../.warnings.jsii.js");
const JSII_RTTI_SYMBOL_1 = Symbol.for("jsii.rtti");
const cloudwatch = require("@aws-cdk/aws-cloudwatch");
const notifications = require("@aws-cdk/aws-codestarnotifications");
const ec2 = require("@aws-cdk/aws-ec2");
const aws_ecr_assets_1 = require("@aws-cdk/aws-ecr-assets");
const events = require("@aws-cdk/aws-events");
const iam = require("@aws-cdk/aws-iam");
const kms = require("@aws-cdk/aws-kms");
const core_1 = require("@aws-cdk/core");
const build_spec_1 = require("./build-spec");
const cache_1 = require("./cache");
const codebuild_canned_metrics_generated_1 = require("./codebuild-canned-metrics.generated");
const codebuild_generated_1 = require("./codebuild.generated");
const codepipeline_artifacts_1 = require("./codepipeline-artifacts");
const no_artifacts_1 = require("./no-artifacts");
const no_source_1 = require("./no-source");
const run_script_linux_build_spec_1 = require("./private/run-script-linux-build-spec");
const report_group_utils_1 = require("./report-group-utils");
const source_types_1 = require("./source-types");
const VPC_POLICY_SYM = Symbol.for('@aws-cdk/aws-codebuild.roleVpcPolicy');
/**
* Represents a reference to a CodeBuild Project.
*
* If you're managing the Project alongside the rest of your CDK resources,
* use the {@link Project} class.
*
* If you want to reference an already existing Project
* (or one defined in a different CDK Stack),
* use the {@link import} method.
*/
class ProjectBase extends core_1.Resource {
/**
* Access the Connections object.
* Will fail if this Project does not have a VPC set.
*/
get connections() {
if (!this._connections) {
throw new Error('Only VPC-associated Projects have security groups to manage. Supply the "vpc" parameter when creating the Project');
}
return this._connections;
}
enableBatchBuilds() {
return undefined;
}
/**
* Add a permission only if there's a policy attached.
* @param statement The permissions statement to add
*/
addToRolePolicy(statement) {
if (this.role) {
this.role.addToPrincipalPolicy(statement);
}
}
/**
* Defines a CloudWatch event rule triggered when something happens with this project.
*
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html
*/
onEvent(id, options = {}) {
const rule = new events.Rule(this, id, options);
rule.addTarget(options.target);
rule.addEventPattern({
source: ['aws.codebuild'],
detail: {
'project-name': [this.projectName],
},
});
return rule;
}
/**
* Defines a CloudWatch event rule triggered when the build project state
* changes. You can filter specific build status events using an event
* pattern filter on the `build-status` detail field:
*
* const rule = project.onStateChange('OnBuildStarted', { target });
* rule.addEventPattern({
* detail: {
* 'build-status': [
* "IN_PROGRESS",
* "SUCCEEDED",
* "FAILED",
* "STOPPED"
* ]
* }
* });
*
* You can also use the methods `onBuildFailed` and `onBuildSucceeded` to define rules for
* these specific state changes.
*
* To access fields from the event in the event target input,
* use the static fields on the `StateChangeEvent` class.
*
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html
*/
onStateChange(id, options = {}) {
const rule = this.onEvent(id, options);
rule.addEventPattern({
detailType: ['CodeBuild Build State Change'],
});
return rule;
}
/**
* Defines a CloudWatch event rule that triggers upon phase change of this
* build project.
*
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html
*/
onPhaseChange(id, options = {}) {
const rule = this.onEvent(id, options);
rule.addEventPattern({
detailType: ['CodeBuild Build Phase Change'],
});
return rule;
}
/**
* Defines an event rule which triggers when a build starts.
*
* To access fields from the event in the event target input,
* use the static fields on the `StateChangeEvent` class.
*/
onBuildStarted(id, options = {}) {
const rule = this.onStateChange(id, options);
rule.addEventPattern({
detail: {
'build-status': ['IN_PROGRESS'],
},
});
return rule;
}
/**
* Defines an event rule which triggers when a build fails.
*
* To access fields from the event in the event target input,
* use the static fields on the `StateChangeEvent` class.
*/
onBuildFailed(id, options = {}) {
const rule = this.onStateChange(id, options);
rule.addEventPattern({
detail: {
'build-status': ['FAILED'],
},
});
return rule;
}
/**
* Defines an event rule which triggers when a build completes successfully.
*
* To access fields from the event in the event target input,
* use the static fields on the `StateChangeEvent` class.
*/
onBuildSucceeded(id, options = {}) {
const rule = this.onStateChange(id, options);
rule.addEventPattern({
detail: {
'build-status': ['SUCCEEDED'],
},
});
return rule;
}
/**
* @returns a CloudWatch metric associated with this build project.
* @param metricName The name of the metric
* @param props Customization properties
*/
metric(metricName, props) {
return new cloudwatch.Metric({
namespace: 'AWS/CodeBuild',
metricName,
dimensionsMap: { ProjectName: this.projectName },
...props,
}).attachTo(this);
}
/**
* Measures the number of builds triggered.
*
* Units: Count
*
* Valid CloudWatch statistics: Sum
*
* @default sum over 5 minutes
*/
metricBuilds(props) {
return this.cannedMetric(codebuild_canned_metrics_generated_1.CodeBuildMetrics.buildsSum, props);
}
/**
* Measures the duration of all builds over time.
*
* Units: Seconds
*
* Valid CloudWatch statistics: Average (recommended), Maximum, Minimum
*
* @default average over 5 minutes
*/
metricDuration(props) {
return this.cannedMetric(codebuild_canned_metrics_generated_1.CodeBuildMetrics.durationAverage, props);
}
/**
* Measures the number of successful builds.
*
* Units: Count
*
* Valid CloudWatch statistics: Sum
*
* @default sum over 5 minutes
*/
metricSucceededBuilds(props) {
return this.cannedMetric(codebuild_canned_metrics_generated_1.CodeBuildMetrics.succeededBuildsSum, props);
}
/**
* Measures the number of builds that failed because of client error or
* because of a timeout.
*
* Units: Count
*
* Valid CloudWatch statistics: Sum
*
* @default sum over 5 minutes
*/
metricFailedBuilds(props) {
return this.cannedMetric(codebuild_canned_metrics_generated_1.CodeBuildMetrics.failedBuildsSum, props);
}
notifyOn(id, target, options) {
return new notifications.NotificationRule(this, id, {
...options,
source: this,
targets: [target],
});
}
notifyOnBuildSucceeded(id, target, options) {
return this.notifyOn(id, target, {
...options,
events: [ProjectNotificationEvents.BUILD_SUCCEEDED],
});
}
notifyOnBuildFailed(id, target, options) {
return this.notifyOn(id, target, {
...options,
events: [ProjectNotificationEvents.BUILD_FAILED],
});
}
bindAsNotificationRuleSource(_scope) {
return {
sourceArn: this.projectArn,
};
}
cannedMetric(fn, props) {
return new cloudwatch.Metric({
...fn({ ProjectName: this.projectName }),
...props,
}).attachTo(this);
}
}
/**
* A representation of a CodeBuild Project.
*/
class Project extends ProjectBase {
constructor(scope, id, props) {
super(scope, id, {
physicalName: props.projectName,
});
try {
jsiiDeprecationWarnings._aws_cdk_aws_codebuild_ProjectProps(props);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, Project);
}
throw error;
}
this.role = props.role || new iam.Role(this, 'Role', {
roleName: core_1.PhysicalName.GENERATE_IF_NEEDED,
assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
});
this.grantPrincipal = this.role;
this.buildImage = (props.environment && props.environment.buildImage) || LinuxBuildImage.STANDARD_1_0;
// let source "bind" to the project. this usually involves granting permissions
// for the code build role to interact with the source.
this.source = props.source || new no_source_1.NoSource();
const sourceConfig = this.source.bind(this, this);
if (props.badge && !this.source.badgeSupported) {
throw new Error(`Badge is not supported for source type ${this.source.type}`);
}
const artifacts = props.artifacts
? props.artifacts
: (this.source.type === source_types_1.CODEPIPELINE_SOURCE_ARTIFACTS_TYPE
? new codepipeline_artifacts_1.CodePipelineArtifacts()
: new no_artifacts_1.NoArtifacts());
const artifactsConfig = artifacts.bind(this, this);
const cache = props.cache || cache_1.Cache.none();
// give the caching strategy the option to grant permissions to any required resources
cache._bind(this);
// Inject download commands for asset if requested
const environmentVariables = props.environmentVariables || {};
const buildSpec = props.buildSpec;
if (this.source.type === source_types_1.NO_SOURCE_TYPE && (buildSpec === undefined || !buildSpec.isImmediate)) {
throw new Error("If the Project's source is NoSource, you need to provide a concrete buildSpec");
}
this._secondarySources = [];
this._secondarySourceVersions = [];
this._fileSystemLocations = [];
for (const secondarySource of props.secondarySources || []) {
this.addSecondarySource(secondarySource);
}
this._secondaryArtifacts = [];
for (const secondaryArtifact of props.secondaryArtifacts || []) {
this.addSecondaryArtifact(secondaryArtifact);
}
this.validateCodePipelineSettings(artifacts);
for (const fileSystemLocation of props.fileSystemLocations || []) {
this.addFileSystemLocation(fileSystemLocation);
}
const resource = new codebuild_generated_1.CfnProject(this, 'Resource', {
description: props.description,
source: {
...sourceConfig.sourceProperty,
buildSpec: buildSpec && buildSpec.toBuildSpec(),
},
artifacts: artifactsConfig.artifactsProperty,
serviceRole: this.role.roleArn,
environment: this.renderEnvironment(props, environmentVariables),
fileSystemLocations: core_1.Lazy.any({ produce: () => this.renderFileSystemLocations() }),
// lazy, because we have a setter for it in setEncryptionKey
// The 'alias/aws/s3' default is necessary because leaving the `encryptionKey` field
// empty will not remove existing encryptionKeys during an update (ref. t/D17810523)
encryptionKey: core_1.Lazy.string({ produce: () => this._encryptionKey ? this._encryptionKey.keyArn : 'alias/aws/s3' }),
badgeEnabled: props.badge,
cache: cache._toCloudFormation(),
name: this.physicalName,
timeoutInMinutes: props.timeout && props.timeout.toMinutes(),
queuedTimeoutInMinutes: props.queuedTimeout && props.queuedTimeout.toMinutes(),
concurrentBuildLimit: props.concurrentBuildLimit,
secondarySources: core_1.Lazy.any({ produce: () => this.renderSecondarySources() }),
secondarySourceVersions: core_1.Lazy.any({ produce: () => this.renderSecondarySourceVersions() }),
secondaryArtifacts: core_1.Lazy.any({ produce: () => this.renderSecondaryArtifacts() }),
triggers: sourceConfig.buildTriggers,
sourceVersion: sourceConfig.sourceVersion,
vpcConfig: this.configureVpc(props),
logsConfig: this.renderLoggingConfiguration(props.logging),
buildBatchConfig: core_1.Lazy.any({
produce: () => {
const config = this._batchServiceRole ? {
serviceRole: this._batchServiceRole.roleArn,
} : undefined;
return config;
},
}),
});
this.addVpcRequiredPermissions(props, resource);
this.projectArn = this.getResourceArnAttribute(resource.attrArn, {
service: 'codebuild',
resource: 'project',
resourceName: this.physicalName,
});
this.projectName = this.getResourceNameAttribute(resource.ref);
this.addToRolePolicy(this.createLoggingPermission());
// add permissions to create and use test report groups
// with names starting with the project's name,
// unless the customer explicitly opts out of it
if (props.grantReportGroupPermissions !== false) {
this.addToRolePolicy(new iam.PolicyStatement({
actions: [
'codebuild:CreateReportGroup',
'codebuild:CreateReport',
'codebuild:UpdateReport',
'codebuild:BatchPutTestCases',
'codebuild:BatchPutCodeCoverages',
],
resources: [report_group_utils_1.renderReportGroupArn(this, `${this.projectName}-*`)],
}));
}
if (props.encryptionKey) {
this.encryptionKey = props.encryptionKey;
}
// bind
if (isBindableBuildImage(this.buildImage)) {
this.buildImage.bind(this, this, {});
}
}
static fromProjectArn(scope, id, projectArn) {
const parsedArn = core_1.Stack.of(scope).splitArn(projectArn, core_1.ArnFormat.SLASH_RESOURCE_NAME);
class Import extends ProjectBase {
constructor(s, i) {
super(s, i, {
account: parsedArn.account,
region: parsedArn.region,
});
this.projectArn = projectArn;
this.projectName = parsedArn.resourceName;
this.role = undefined;
this.grantPrincipal = new iam.UnknownPrincipal({ resource: this });
}
}
return new Import(scope, id);
}
/**
* Import a Project defined either outside the CDK,
* or in a different CDK Stack
* (and exported using the {@link export} method).
*
* @note if you're importing a CodeBuild Project for use
* in a CodePipeline, make sure the existing Project
* has permissions to access the S3 Bucket of that Pipeline -
* otherwise, builds in that Pipeline will always fail.
*
* @param scope the parent Construct for this Construct
* @param id the logical name of this Construct
* @param projectName the name of the project to import
* @returns a reference to the existing Project
*/
static fromProjectName(scope, id, projectName) {
class Import extends ProjectBase {
constructor(s, i) {
super(s, i);
this.role = undefined;
this.projectArn = core_1.Stack.of(this).formatArn({
service: 'codebuild',
resource: 'project',
resourceName: projectName,
});
this.grantPrincipal = new iam.UnknownPrincipal({ resource: this });
this.projectName = projectName;
}
}
return new Import(scope, id);
}
/**
* Convert the environment variables map of string to {@link BuildEnvironmentVariable},
* which is the customer-facing type, to a list of {@link CfnProject.EnvironmentVariableProperty},
* which is the representation of environment variables in CloudFormation.
*
* @param environmentVariables the map of string to environment variables
* @param validateNoPlainTextSecrets whether to throw an exception
* if any of the plain text environment variables contain secrets, defaults to 'false'
* @returns an array of {@link CfnProject.EnvironmentVariableProperty} instances
*/
static serializeEnvVariables(environmentVariables, validateNoPlainTextSecrets = false, principal) {
const ret = new Array();
const ssmIamResources = new Array();
const secretsManagerIamResources = new Set();
const kmsIamResources = new Set();
for (const [name, envVariable] of Object.entries(environmentVariables)) {
const envVariableValue = envVariable.value?.toString();
const cfnEnvVariable = {
name,
type: envVariable.type || BuildEnvironmentVariableType.PLAINTEXT,
value: envVariableValue,
};
ret.push(cfnEnvVariable);
// validate that the plain-text environment variables don't contain any secrets in them
if (validateNoPlainTextSecrets && cfnEnvVariable.type === BuildEnvironmentVariableType.PLAINTEXT) {
const fragments = core_1.Tokenization.reverseString(cfnEnvVariable.value);
for (const token of fragments.tokens) {
if (token instanceof core_1.SecretValue) {
throw new Error(`Plaintext environment variable '${name}' contains a secret value! ` +
'This means the value of this variable will be visible in plain text in the AWS Console. ' +
"Please consider using CodeBuild's SecretsManager environment variables feature instead. " +
"If you'd like to continue with having this secret in the plaintext environment variables, " +
'please set the checkSecretsInPlainTextEnvVariables property to false');
}
}
}
if (principal) {
const stack = core_1.Stack.of(principal);
// save the SSM env variables
if (envVariable.type === BuildEnvironmentVariableType.PARAMETER_STORE) {
ssmIamResources.push(stack.formatArn({
service: 'ssm',
resource: 'parameter',
// If the parameter name starts with / the resource name is not separated with a double '/'
// arn:aws:ssm:region:1111111111:parameter/PARAM_NAME
resourceName: envVariableValue.startsWith('/')
? envVariableValue.slice(1)
: envVariableValue,
}));
}
// save SecretsManager env variables
if (envVariable.type === BuildEnvironmentVariableType.SECRETS_MANAGER) {
// We have 3 basic cases here of what envVariableValue can be:
// 1. A string that starts with 'arn:' (and might contain Token fragments).
// 2. A Token.
// 3. A simple value, like 'secret-id'.
if (envVariableValue.startsWith('arn:')) {
const parsedArn = stack.splitArn(envVariableValue, core_1.ArnFormat.COLON_RESOURCE_NAME);
if (!parsedArn.resourceName) {
throw new Error('SecretManager ARN is missing the name of the secret: ' + envVariableValue);
}
// the value of the property can be a complex string, separated by ':';
// see https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec.env.secrets-manager
const secretName = parsedArn.resourceName.split(':')[0];
secretsManagerIamResources.add(stack.formatArn({
service: 'secretsmanager',
resource: 'secret',
// since we don't know whether the ARN was full, or partial
// (CodeBuild supports both),
// stick a "*" at the end, which makes it work for both
resourceName: `${secretName}*`,
arnFormat: core_1.ArnFormat.COLON_RESOURCE_NAME,
partition: parsedArn.partition,
account: parsedArn.account,
region: parsedArn.region,
}));
// if secret comes from another account, SecretsManager will need to access
// KMS on the other account as well to be able to get the secret
if (parsedArn.account && core_1.Token.compareStrings(parsedArn.account, stack.account) === core_1.TokenComparison.DIFFERENT) {
kmsIamResources.add(stack.formatArn({
service: 'kms',
resource: 'key',
// We do not know the ID of the key, but since this is a cross-account access,
// the key policies have to allow this access, so a wildcard is safe here
resourceName: '*',
arnFormat: core_1.ArnFormat.SLASH_RESOURCE_NAME,
partition: parsedArn.partition,
account: parsedArn.account,
region: parsedArn.region,
}));
}
}
else if (core_1.Token.isUnresolved(envVariableValue)) {
// the value of the property can be a complex string, separated by ':';
// see https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec.env.secrets-manager
let secretArn = envVariableValue.split(':')[0];
// parse the Token, and see if it represents a single resource
// (we will assume it's a Secret from SecretsManager)
const fragments = core_1.Tokenization.reverseString(envVariableValue);
if (fragments.tokens.length === 1) {
const resolvable = fragments.tokens[0];
if (core_1.Reference.isReference(resolvable)) {
// check the Stack the resource owning the reference belongs to
const resourceStack = core_1.Stack.of(resolvable.target);
if (core_1.Token.compareStrings(stack.account, resourceStack.account) === core_1.TokenComparison.DIFFERENT) {
// since this is a cross-account access,
// add the appropriate KMS permissions
kmsIamResources.add(stack.formatArn({
service: 'kms',
resource: 'key',
// We do not know the ID of the key, but since this is a cross-account access,
// the key policies have to allow this access, so a wildcard is safe here
resourceName: '*',
arnFormat: core_1.ArnFormat.SLASH_RESOURCE_NAME,
partition: resourceStack.partition,
account: resourceStack.account,
region: resourceStack.region,
}));
// Work around a bug in SecretsManager -
// when the access is cross-environment,
// Secret.secretArn returns a partial ARN!
// So add a "*" at the end, so that the permissions work
secretArn = `${secretArn}-??????`;
}
}
}
// if we are passed a Token, we should assume it's the ARN of the Secret
// (as the name would not work anyway, because it would be the full name, which CodeBuild does not support)
secretsManagerIamResources.add(secretArn);
}
else {
// the value of the property can be a complex string, separated by ':';
// see https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec.env.secrets-manager
const secretName = envVariableValue.split(':')[0];
secretsManagerIamResources.add(stack.formatArn({
service: 'secretsmanager',
resource: 'secret',
resourceName: `${secretName}-??????`,
arnFormat: core_1.ArnFormat.COLON_RESOURCE_NAME,
}));
}
}
}
}
if (ssmIamResources.length !== 0) {
principal?.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['ssm:GetParameters'],
resources: ssmIamResources,
}));
}
if (secretsManagerIamResources.size !== 0) {
principal?.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue'],
resources: Array.from(secretsManagerIamResources),
}));
}
if (kmsIamResources.size !== 0) {
principal?.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['kms:Decrypt'],
resources: Array.from(kmsIamResources),
}));
}
return ret;
}
enableBatchBuilds() {
if (!this._batchServiceRole) {
this._batchServiceRole = new iam.Role(this, 'BatchServiceRole', {
assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
});
this._batchServiceRole.addToPrincipalPolicy(new iam.PolicyStatement({
resources: [core_1.Lazy.string({
produce: () => this.projectArn,
})],
actions: [
'codebuild:StartBuild',
'codebuild:StopBuild',
'codebuild:RetryBuild',
],
}));
}
return {
role: this._batchServiceRole,
};
}
/**
* Adds a secondary source to the Project.
*
* @param secondarySource the source to add as a secondary source
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-multi-in-out.html
*/
addSecondarySource(secondarySource) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_codebuild_ISource(secondarySource);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.addSecondarySource);
}
throw error;
}
if (!secondarySource.identifier) {
throw new Error('The identifier attribute is mandatory for secondary sources');
}
const secondarySourceConfig = secondarySource.bind(this, this);
this._secondarySources.push(secondarySourceConfig.sourceProperty);
if (secondarySourceConfig.sourceVersion) {
this._secondarySourceVersions.push({
sourceIdentifier: secondarySource.identifier,
sourceVersion: secondarySourceConfig.sourceVersion,
});
}
}
/**
* Adds a fileSystemLocation to the Project.
*
* @param fileSystemLocation the fileSystemLocation to add
*/
addFileSystemLocation(fileSystemLocation) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_codebuild_IFileSystemLocation(fileSystemLocation);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.addFileSystemLocation);
}
throw error;
}
const fileSystemConfig = fileSystemLocation.bind(this, this);
this._fileSystemLocations.push(fileSystemConfig.location);
}
/**
* Adds a secondary artifact to the Project.
*
* @param secondaryArtifact the artifact to add as a secondary artifact
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-multi-in-out.html
*/
addSecondaryArtifact(secondaryArtifact) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_codebuild_IArtifacts(secondaryArtifact);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.addSecondaryArtifact);
}
throw error;
}
if (!secondaryArtifact.identifier) {
throw new Error('The identifier attribute is mandatory for secondary artifacts');
}
this._secondaryArtifacts.push(secondaryArtifact.bind(this, this).artifactsProperty);
}
/**
* A callback invoked when the given project is added to a CodePipeline.
*
* @param _scope the construct the binding is taking place in
* @param options additional options for the binding
*/
bindToCodePipeline(_scope, options) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_codebuild_BindToCodePipelineOptions(options);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.bindToCodePipeline);
}
throw error;
}
// work around a bug in CodeBuild: it ignores the KMS key set on the pipeline,
// and always uses its own, project-level key
if (options.artifactBucket.encryptionKey && !this._encryptionKey) {
// we cannot safely do this assignment if the key is of type kms.Key,
// and belongs to a stack in a different account or region than the project
// (that would cause an illegal reference, as KMS keys don't have physical names)
const keyStack = core_1.Stack.of(options.artifactBucket.encryptionKey);
const projectStack = core_1.Stack.of(this);
if (!(options.artifactBucket.encryptionKey instanceof kms.Key &&
(keyStack.account !== projectStack.account || keyStack.region !== projectStack.region))) {
this.encryptionKey = options.artifactBucket.encryptionKey;
}
}
}
/**
* @override
*/
validate() {
const ret = new Array();
if (this.source.type === source_types_1.CODEPIPELINE_SOURCE_ARTIFACTS_TYPE) {
if (this._secondarySources.length > 0) {
ret.push('A Project with a CodePipeline Source cannot have secondary sources. ' +
"Use the CodeBuild Pipeline Actions' `extraInputs` property instead");
}
if (this._secondaryArtifacts.length > 0) {
ret.push('A Project with a CodePipeline Source cannot have secondary artifacts. ' +
"Use the CodeBuild Pipeline Actions' `outputs` property instead");
}
}
return ret;
}
set encryptionKey(encryptionKey) {
this._encryptionKey = encryptionKey;
encryptionKey.grantEncryptDecrypt(this);
}
createLoggingPermission() {
const logGroupArn = core_1.Stack.of(this).formatArn({
service: 'logs',
resource: 'log-group',
arnFormat: core_1.ArnFormat.COLON_RESOURCE_NAME,
resourceName: `/aws/codebuild/${this.projectName}`,
});
const logGroupStarArn = `${logGroupArn}:*`;
return new iam.PolicyStatement({
resources: [logGroupArn, logGroupStarArn],
actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
});
}
renderEnvironment(props, projectVars = {}) {
const env = props.environment ?? {};
const vars = {};
const containerVars = env.environmentVariables || {};
// first apply environment variables from the container definition
for (const name of Object.keys(containerVars)) {
vars[name] = containerVars[name];
}
// now apply project-level vars
for (const name of Object.keys(projectVars)) {
vars[name] = projectVars[name];
}
const hasEnvironmentVars = Object.keys(vars).length > 0;
const errors = this.buildImage.validate(env);
if (errors.length > 0) {
throw new Error('Invalid CodeBuild environment: ' + errors.join('\n'));
}
const imagePullPrincipalType = this.buildImage.imagePullPrincipalType === ImagePullPrincipalType.CODEBUILD
? ImagePullPrincipalType.CODEBUILD
: ImagePullPrincipalType.SERVICE_ROLE;
if (this.buildImage.repository) {
if (imagePullPrincipalType === ImagePullPrincipalType.SERVICE_ROLE) {
this.buildImage.repository.grantPull(this);
}
else {
const statement = new iam.PolicyStatement({
principals: [new iam.ServicePrincipal('codebuild.amazonaws.com')],
actions: ['ecr:GetDownloadUrlForLayer', 'ecr:BatchGetImage', 'ecr:BatchCheckLayerAvailability'],
});
statement.sid = 'CodeBuild';
this.buildImage.repository.addToResourcePolicy(statement);
}
}
if (imagePullPrincipalType === ImagePullPrincipalType.SERVICE_ROLE) {
this.buildImage.secretsManagerCredentials?.grantRead(this);
}
const secret = this.buildImage.secretsManagerCredentials;
return {
type: this.buildImage.type,
image: this.buildImage.imageId,
imagePullCredentialsType: imagePullPrincipalType,
registryCredential: secret
? {
credentialProvider: 'SECRETS_MANAGER',
// Secrets must be referenced by either the full ARN (with SecretsManager suffix), or by name.
// "Partial" ARNs (without the suffix) will fail a validation regex at deploy-time.
credential: secret.secretFullArn ?? secret.secretName,
}
: undefined,
certificate: env.certificate?.bucket.arnForObjects(env.certificate.objectKey),
privilegedMode: env.privileged || false,
computeType: env.computeType || this.buildImage.defaultComputeType,
environmentVariables: hasEnvironmentVars
? Project.serializeEnvVariables(vars, props.checkSecretsInPlainTextEnvVariables ?? true, this)
: undefined,
};
}
renderFileSystemLocations() {
return this._fileSystemLocations.length === 0
? undefined
: this._fileSystemLocations;
}
renderSecondarySources() {
return this._secondarySources.length === 0
? undefined
: this._secondarySources;
}
renderSecondarySourceVersions() {
return this._secondarySourceVersions.length === 0
? undefined
: this._secondarySourceVersions;
}
renderSecondaryArtifacts() {
return this._secondaryArtifacts.length === 0
? undefined
: this._secondaryArtifacts;
}
/**
* If configured, set up the VPC-related properties
*
* Returns the VpcConfig that should be added to the
* codebuild creation properties.
*/
configureVpc(props) {
if ((props.securityGroups || props.allowAllOutbound !== undefined) && !props.vpc) {
throw new Error('Cannot configure \'securityGroup\' or \'allowAllOutbound\' without configuring a VPC');
}
if (!props.vpc) {
return undefined;
}
if ((props.securityGroups && props.securityGroups.length > 0) && props.allowAllOutbound !== undefined) {
throw new Error('Configure \'allowAllOutbound\' directly on the supplied SecurityGroup.');
}
let securityGroups;
if (props.securityGroups && props.securityGroups.length > 0) {
securityGroups = props.securityGroups;
}
else {
const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', {
vpc: props.vpc,
description: 'Automatic generated security group for CodeBuild ' + core_1.Names.uniqueId(this),
allowAllOutbound: props.allowAllOutbound,
});
securityGroups = [securityGroup];
}
this._connections = new ec2.Connections({ securityGroups });
return {
vpcId: props.vpc.vpcId,
subnets: props.vpc.selectSubnets(props.subnetSelection).subnetIds,
securityGroupIds: this.connections.securityGroups.map(s => s.securityGroupId),
};
}
renderLoggingConfiguration(props) {
if (props === undefined) {
return undefined;
}
let s3Config = undefined;
let cloudwatchConfig = undefined;
if (props.s3) {
const s3Logs = props.s3;
s3Config = {
status: (s3Logs.enabled ?? true) ? 'ENABLED' : 'DISABLED',
location: `${s3Logs.bucket.bucketName}` + (s3Logs.prefix ? `/${s3Logs.prefix}` : ''),
encryptionDisabled: s3Logs.encrypted,
};
s3Logs.bucket?.grantWrite(this);
}
if (props.cloudWatch) {
const cloudWatchLogs = props.cloudWatch;
const status = (cloudWatchLogs.enabled ?? true) ? 'ENABLED' : 'DISABLED';
if (status === 'ENABLED' && !(cloudWatchLogs.logGroup)) {
throw new Error('Specifying a LogGroup is required if CloudWatch logging for CodeBuild is enabled');
}
cloudWatchLogs.logGroup?.grantWrite(this);
cloudwatchConfig = {
status,
groupName: cloudWatchLogs.logGroup?.logGroupName,
streamName: cloudWatchLogs.prefix,
};
}
return {
s3Logs: s3Config,
cloudWatchLogs: cloudwatchConfig,
};
}
addVpcRequiredPermissions(props, project) {
if (!props.vpc || !this.role) {
return;
}
this.role.addToPrincipalPolicy(new iam.PolicyStatement({
resources: [`arn:${core_1.Aws.PARTITION}:ec2:${core_1.Aws.REGION}:${core_1.Aws.ACCOUNT_ID}:network-interface/*`],
actions: ['ec2:CreateNetworkInterfacePermission'],
conditions: {
StringEquals: {
'ec2:Subnet': props.vpc
.selectSubnets(props.subnetSelection).subnetIds
.map(si => `arn:${core_1.Aws.PARTITION}:ec2:${core_1.Aws.REGION}:${core_1.Aws.ACCOUNT_ID}:subnet/${si}`),
'ec2:AuthorizedService': 'codebuild.amazonaws.com',
},
},
}));
// If the same Role is used for multiple Projects, always creating a new `iam.Policy`
// will attach the same policy multiple times, probably exceeding the maximum size of the
// Role policy. Make sure we only do it once for the same role.
//
// This deduplication could be a feature of the Role itself, but that feels risky and
// is hard to implement (what with Tokens and all). Safer to fix it locally for now.
let policy = this.role[VPC_POLICY_SYM];
if (!policy) {
policy = new iam.Policy(this, 'PolicyDocument', {
statements: [
new iam.PolicyStatement({
resources: ['*'],
actions: [
'ec2:CreateNetworkInterface',
'ec2:DescribeNetworkInterfaces',
'ec2:DeleteNetworkInterface',
'ec2:DescribeSubnets',
'ec2:DescribeSecurityGroups',
'ec2:DescribeDhcpOptions',
'ec2:DescribeVpcs',
],
}),
],
});
this.role.attachInlinePolicy(policy);
this.role[VPC_POLICY_SYM] = policy;
}
// add an explicit dependency between the EC2 Policy and this Project -
// otherwise, creating the Project fails, as it requires these permissions
// to be already attached to the Project's Role
project.node.addDependency(policy);
}
validateCodePipelineSettings(artifacts) {
const sourceType = this.source.type;
const artifactsType = artifacts.type;
if ((sourceType === source_types_1.CODEPIPELINE_SOURCE_ARTIFACTS_TYPE ||
artifactsType === source_types_1.CODEPIPELINE_SOURCE_ARTIFACTS_TYPE) &&
(sourceType !== artifactsType)) {
throw new Error('Both source and artifacts must be set to CodePipeline');
}
}
}
exports.Project = Project;
_a = JSII_RTTI_SYMBOL_1;
Project[_a] = { fqn: "@aws-cdk/aws-codebuild.Project", version: "1.204.0" };
/**
* Build machine compute type.
*/
var ComputeType;
(function (ComputeType) {
ComputeType["SMALL"] = "BUILD_GENERAL1_SMALL";
ComputeType["MEDIUM"] = "BUILD_GENERAL1_MEDIUM";
ComputeType["LARGE"] = "BUILD_GENERAL1_LARGE";
ComputeType["X2_LARGE"] = "BUILD_GENERAL1_2XLARGE";
})(ComputeType = exports.ComputeType || (exports.ComputeType = {}));
/**
* The type of principal CodeBuild will use to pull your build Docker image.
*/
var ImagePullPrincipalType;
(function (ImagePullPrincipalType) {
/**
* CODEBUILD specifies that CodeBuild uses its own identity when pulling the image.
* This means the resource policy of the ECR repository that hosts the image will be modified to trust
* CodeBuild's service principal.
* This is the required principal type when using CodeBuild's pre-defined images.
*/
ImagePullPrincipalType["CODEBUILD"] = "CODEBUILD";
/**
* SERVICE_ROLE specifies that AWS CodeBuild uses the project's role when pulling the image.
* The role will be granted pull permissions on the ECR repository hosting the image.
*/
ImagePullPrincipalType["SERVICE_ROLE"] = "SERVICE_ROLE";
})(ImagePullPrincipalType = exports.ImagePullPrincipalType || (exports.ImagePullPrincipalType = {}));
// Keep around to resolve a circular dependency until removing deprecated ARM image constants from LinuxBuildImage
// eslint-disable-next-line no-duplicate-imports, import/order
const linux_arm_build_image_1 = require("./linux-arm-build-image");
/**
* A CodeBuild image running x86-64 Linux.
*
* This class has a bunch of public constants that represent the most popular images.
*
* You can also specify a custom image using one of the static methods:
*
* - LinuxBuildImage.fromDockerRegistry(image[, { secretsManagerCredentials }])
* - LinuxBuildImage.fromEcrRepository(repo[, tag])
* - LinuxBuildImage.fromAsset(parent, id, props)
*
*
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-available.html
*/
class LinuxBuildImage {
constructor(props) {
this.type = 'LINUX_CONTAINER';
this.defaultComputeType = ComputeType.SMALL;
this.imageId = props.imageId;
this.imagePullPrincipalType = props.imagePullPrincipalType;
this.secretsManagerCredentials = props.secretsManagerCredentials;
this.repository = props.repository;
}
/**
* @returns a x86-64 Linux build image from a Docker Hub image.
*/
static fromDockerRegistry(name, options = {}) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_codebuild_DockerImageOptions(options);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.fromDockerRegistry);
}
throw error;
}
return new LinuxBuildImage({
...options,
imageId: name,
imagePullPrincipalType: ImagePullPrincipalType.SERVICE_ROLE,
});
}
/**
* @returns A x86-64 Linux build image from an ECR repository.
*
* NOTE: if the repository is external (i.e. imported), then we won't be able to add
* a resource policy statement for it so CodeBuild can pull the image.
*
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-ecr.html
*
* @param repository The ECR repository
* @param tagOrDigest Image tag or digest (default "latest", digests must start with `sha256:`)
*/
static fromEcrRepository(repository, tagOrDigest = 'latest') {
return new LinuxBuildImage({
imageId: repository.repositoryUriForTagOrDigest(tagOrDigest),
imagePullPrincipalType: ImagePullPrincipalType.SERVICE_ROLE,
repository,
});
}
/**
* Uses an Docker image asset as a x86-64 Linux build image.
*/
static fromAsset(scope, id, props) {
const asset = new aws_ecr_assets_1.DockerImageAsset(scope, id, props);
return new LinuxBuildImage({
imageId: asset.imageUri,
imagePullPrincipalType: ImagePullPrincipalType.SERVICE_ROLE,
repository: asset.repository,
});
}
/**
* Uses a Docker image provided by CodeBuild.
*
* @returns A Docker image provided by CodeBuild.
*
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-available.html
*
* @param id The image identifier
* @example 'aws/codebuild/standard:4.0'
*/
static fromCodeBuildImageId(id) {
return LinuxBuildImage.codeBuildImage(id);
}
static codeBuildImage(name) {
return new LinuxBuildImage({
imageId: name,
imagePullPrincipalType: ImagePullPrincipalType.CODEBUILD,
});
}
validate(_) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_codebuild_BuildEnvironment(_);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.validate);
}
throw error;
}
return [];
}
runScriptBuildspec(entrypoint) {
return run_script_linux_build_spec_1.runScriptLinuxBuildSpec(entrypoint);
}
}
exports.LinuxBuildImage = LinuxBuildImage;
_b = JSII_RTTI_SYMBOL_1;
LinuxBuildImage[_b] = { fqn: "@aws-cdk/aws-codebuild.LinuxBuildImage", version: "1.204.0" };
LinuxBuildImage.STANDARD_1_0 = LinuxBuildImage.codeBuildImage('aws/codebuild/standard:1.0');
LinuxBuildImage.STANDARD_2_0 = LinuxBuildImage.codeBuildImage('aws/codebuild/standard:2.0');
LinuxBuildImage.STANDARD_3_0 = LinuxBuildImage.codeBuildImage('aws/codebuild/standard:3.0');
/** The `aws/codebuild/standard:4.0` build image. */
LinuxBuildImage.STANDARD_4_0 = LinuxBuildImage.codeBuildImage('aws/codebuild/standard:4.0');
/** The `aws/codebuild/standard:5.0` build image. */
LinuxBuildImage.STANDARD_5_0 = LinuxBuildImage.codeBuildImage('aws/codebuild/standard:5.0');
LinuxBuildImage.AMAZON_LINUX_2 = LinuxBuildImage.codeBuildImage('aws/codebuild/amazonlinux2-x86_64-standard:1.0');
LinuxBuildImage.AMAZON_LINUX_2_2 = LinuxBuildImage.codeBuildImage('aws/codebuild/amazonlinux2-x86_64-standard:2.0');
/** The Amazon Linux 2 x86_64 standard image, version `3.0`. */
LinuxBuildImage.AMAZON_LINUX_2_3 = LinuxBuildImage.code