packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts (356 lines of code) (raw):
import * as fs from 'fs';
import { kebab as toKebabCase } from 'case';
import { Construct } from 'constructs';
import { ISource, SourceConfig, Source, MarkersConfig } from './source';
import * as cloudfront from '../../aws-cloudfront';
import * as ec2 from '../../aws-ec2';
import * as efs from '../../aws-efs';
import * as iam from '../../aws-iam';
import * as lambda from '../../aws-lambda';
import * as logs from '../../aws-logs';
import * as s3 from '../../aws-s3';
import * as cdk from '../../core';
import { ValidationError } from '../../core/lib/errors';
import { BucketDeploymentSingletonFunction } from '../../custom-resource-handlers/dist/aws-s3-deployment/bucket-deployment-provider.generated';
import { AwsCliLayer } from '../../lambda-layer-awscli';
// tag key has a limit of 128 characters
const CUSTOM_RESOURCE_OWNER_TAG = 'aws-cdk:cr-owned';
/**
* Properties for `BucketDeployment`.
*/
export interface BucketDeploymentProps {
/**
* The sources from which to deploy the contents of this bucket.
*/
readonly sources: ISource[];
/**
* The S3 bucket to sync the contents of the zip file to.
*/
readonly destinationBucket: s3.IBucket;
/**
* Key prefix in the destination bucket. Must be <=104 characters
*
* If it's set with prune: true, it will only prune files with the prefix.
*
* We recommend to always configure the `destinationKeyPrefix` property. This will prevent the deployment
* from accidentally deleting data that wasn't uploaded by it.
*
* @default "/" (unzip to root of the destination bucket)
*/
readonly destinationKeyPrefix?: string;
/**
* If this is set, the zip file will be synced to the destination S3 bucket and extracted.
* If false, the file will remain zipped in the destination bucket.
* @default true
*/
readonly extract?: boolean;
/**
* If this is set, matching files or objects will be excluded from the deployment's sync
* command. This can be used to exclude a file from being pruned in the destination bucket.
*
* If you want to just exclude files from the deployment package (which excludes these files
* evaluated when invalidating the asset), you should leverage the `exclude` property of
* `AssetOptions` when defining your source.
*
* @default - No exclude filters are used
* @see https://docs.aws.amazon.com/cli/latest/reference/s3/index.html#use-of-exclude-and-include-filters
*/
readonly exclude?: string[];
/**
* If this is set, matching files or objects will be included with the deployment's sync
* command. Since all files from the deployment package are included by default, this property
* is usually leveraged alongside an `exclude` filter.
*
* @default - No include filters are used and all files are included with the sync command
* @see https://docs.aws.amazon.com/cli/latest/reference/s3/index.html#use-of-exclude-and-include-filters
*/
readonly include?: string[];
/**
* By default, files in the destination bucket that don't exist in the source will be deleted
* when the BucketDeployment resource is created or updated.
*
* If this is set to false, files in the destination bucket that
* do not exist in the asset, will NOT be deleted during deployment (create/update).
*
* @see https://docs.aws.amazon.com/cli/latest/reference/s3/sync.html
*
* @default true
*/
readonly prune?: boolean;
/**
* If this is set to "false", the destination files will be deleted when the
* resource is deleted or the destination is updated.
*
* NOTICE: Configuring this to "false" might have operational implications. Please
* visit to the package documentation referred below to make sure you fully understand those implications.
*
* @see https://github.com/aws/aws-cdk/tree/main/packages/aws-cdk-lib/aws-s3-deployment#retain-on-delete
* @default true - when resource is deleted/updated, files are retained
*/
readonly retainOnDelete?: boolean;
/**
* The CloudFront distribution using the destination bucket as an origin.
* Files in the distribution's edge caches will be invalidated after
* files are uploaded to the destination bucket.
*
* @default - No invalidation occurs
*/
readonly distribution?: cloudfront.IDistribution;
/**
* The file paths to invalidate in the CloudFront distribution.
*
* @default - All files under the destination bucket key prefix will be invalidated.
*/
readonly distributionPaths?: string[];
/**
* The number of days that the lambda function's log events are kept in CloudWatch Logs.
*
* This is a legacy API and we strongly recommend you migrate to `logGroup` if you can.
* `logGroup` allows you to create a fully customizable log group and instruct the Lambda function to send logs to it.
*
* @default logs.RetentionDays.INFINITE
*/
readonly logRetention?: logs.RetentionDays;
/**
* The Log Group used for logging of events emitted by the custom resource's lambda function.
*
* Providing a user-controlled log group was rolled out to commercial regions on 2023-11-16.
* If you are deploying to another type of region, please check regional availability first.
*
* @default - a default log group created by AWS Lambda
*/
readonly logGroup?: logs.ILogGroup;
/**
* The amount of memory (in MiB) to allocate to the AWS Lambda function which
* replicates the files from the CDK bucket to the destination bucket.
*
* If you are deploying large files, you will need to increase this number
* accordingly.
*
* @default 128
*/
readonly memoryLimit?: number;
/**
* The size of the AWS Lambda function’s /tmp directory in MiB.
*
* @default 512 MiB
*/
readonly ephemeralStorageSize?: cdk.Size;
/**
* Mount an EFS file system. Enable this if your assets are large and you encounter disk space errors.
* Enabling this option will require a VPC to be specified.
*
* @default - No EFS. Lambda has access only to 512MB of disk space.
*/
readonly useEfs?: boolean;
/**
* Execution role associated with this function
*
* @default - A role is automatically created
*/
readonly role?: iam.IRole;
/**
* User-defined object metadata to be set on all objects in the deployment
* @default - No user metadata is set
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#UserMetadata
*/
readonly metadata?: { [key: string]: string };
/**
* System-defined cache-control metadata to be set on all objects in the deployment.
* @default - Not set.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
readonly cacheControl?: CacheControl[];
/**
* System-defined cache-disposition metadata to be set on all objects in the deployment.
* @default - Not set.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
readonly contentDisposition?: string;
/**
* System-defined content-encoding metadata to be set on all objects in the deployment.
* @default - Not set.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
readonly contentEncoding?: string;
/**
* System-defined content-language metadata to be set on all objects in the deployment.
* @default - Not set.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
readonly contentLanguage?: string;
/**
* System-defined content-type metadata to be set on all objects in the deployment.
* @default - Not set.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
readonly contentType?: string;
/**
* System-defined expires metadata to be set on all objects in the deployment.
* @default - The objects in the distribution will not expire.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
readonly expires?: cdk.Expiration;
/**
* System-defined x-amz-server-side-encryption metadata to be set on all objects in the deployment.
* @default - Server side encryption is not used.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
readonly serverSideEncryption?: ServerSideEncryption;
/**
* System-defined x-amz-storage-class metadata to be set on all objects in the deployment.
* @default - Default storage-class for the bucket is used.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
readonly storageClass?: StorageClass;
/**
* System-defined x-amz-website-redirect-location metadata to be set on all objects in the deployment.
* @default - No website redirection.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
readonly websiteRedirectLocation?: string;
/**
* System-defined x-amz-server-side-encryption-aws-kms-key-id metadata to be set on all objects in the deployment.
* @default - Not set.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
readonly serverSideEncryptionAwsKmsKeyId?: string;
/**
* System-defined x-amz-server-side-encryption-customer-algorithm metadata to be set on all objects in the deployment.
* Warning: This is not a useful parameter until this bug is fixed: https://github.com/aws/aws-cdk/issues/6080
* @default - Not set.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html#sse-c-how-to-programmatically-intro
*/
readonly serverSideEncryptionCustomerAlgorithm?: string;
/**
* System-defined x-amz-acl metadata to be set on all objects in the deployment.
* @default - Not set.
* @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
*/
readonly accessControl?: s3.BucketAccessControl;
/**
* The VPC network to place the deployment lambda handler in.
* This is required if `useEfs` is set.
*
* @default None
*/
readonly vpc?: ec2.IVpc;
/**
* Where in the VPC to place the deployment lambda handler.
* Only used if 'vpc' is supplied.
*
* @default - the Vpc default strategy if not specified
*/
readonly vpcSubnets?: ec2.SubnetSelection;
/**
* If set to true, uploads will precompute the value of `x-amz-content-sha256`
* and include it in the signed S3 request headers.
*
* @default - `x-amz-content-sha256` will not be computed
*/
readonly signContent?: boolean;
/**
* If set to false, the custom resource will not send back the SourceObjectKeys.
* This is useful when you are facing the error `Response object is too long`
*
* See https://github.com/aws/aws-cdk/issues/28579
*
* @default true
*/
readonly outputObjectKeys?: boolean;
}
/**
* `BucketDeployment` populates an S3 bucket with the contents of .zip files from
* other S3 buckets or from local disk
*/
export class BucketDeployment extends Construct {
private readonly cr: cdk.CustomResource;
private _deployedBucket?: s3.IBucket;
private requestDestinationArn: boolean = false;
private readonly destinationBucket: s3.IBucket;
private readonly sources: SourceConfig[];
/**
* Execution role of the Lambda function behind the custom CloudFormation resource of type `Custom::CDKBucketDeployment`.
*/
public readonly handlerRole: iam.IRole;
constructor(scope: Construct, id: string, props: BucketDeploymentProps) {
super(scope, id);
if (props.distributionPaths) {
if (!props.distribution) {
throw new ValidationError('Distribution must be specified if distribution paths are specified', this);
}
if (!cdk.Token.isUnresolved(props.distributionPaths)) {
if (!props.distributionPaths.every(distributionPath => cdk.Token.isUnresolved(distributionPath) || distributionPath.startsWith('/'))) {
throw new ValidationError('Distribution paths must start with "/"', this);
}
}
}
if (props.useEfs && !props.vpc) {
throw new ValidationError('Vpc must be specified if useEfs is set', this);
}
this.destinationBucket = props.destinationBucket;
const accessPointPath = '/lambda';
let accessPoint;
if (props.useEfs && props.vpc) {
const accessMode = '0777';
const fileSystem = this.getOrCreateEfsFileSystem(scope, {
vpc: props.vpc,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
accessPoint = fileSystem.addAccessPoint('AccessPoint', {
path: accessPointPath,
createAcl: {
ownerUid: '1001',
ownerGid: '1001',
permissions: accessMode,
},
posixUser: {
uid: '1001',
gid: '1001',
},
});
accessPoint.node.addDependency(fileSystem.mountTargetsAvailable);
}
// Making VPC dependent on BucketDeployment so that CFN stack deletion is smooth.
// Refer comments on https://github.com/aws/aws-cdk/pull/15220 for more details.
if (props.vpc) {
this.node.addDependency(props.vpc);
}
const mountPath = `/mnt${accessPointPath}`;
const handler = new BucketDeploymentSingletonFunction(this, 'CustomResourceHandler', {
uuid: this.renderSingletonUuid(props.memoryLimit, props.ephemeralStorageSize, props.vpc),
layers: [new AwsCliLayer(this, 'AwsCliLayer')],
environment: {
...props.useEfs ? { MOUNT_PATH: mountPath } : undefined,
// Override the built-in CA bundle from the AWS CLI with the Lambda-curated one
// This is necessary to make the CLI work in ADC regions.
AWS_CA_BUNDLE: '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
},
lambdaPurpose: 'Custom::CDKBucketDeployment',
timeout: cdk.Duration.minutes(15),
role: props.role,
memorySize: props.memoryLimit,
ephemeralStorageSize: props.ephemeralStorageSize,
vpc: props.vpc,
vpcSubnets: props.vpcSubnets,
filesystem: accessPoint ? lambda.FileSystem.fromEfsAccessPoint(
accessPoint,
mountPath,
) : undefined,
// props.logRetention is deprecated, make sure we only set it if it is actually provided
// otherwise jsii will print warnings even for users that don't use this directly
...(props.logRetention ? { logRetention: props.logRetention } : {}),
logGroup: props.logGroup,
});
const handlerRole = handler.role;
if (!handlerRole) { throw new ValidationError('lambda.SingletonFunction should have created a Role', this); }
this.handlerRole = handlerRole;
this.sources = props.sources.map((source: ISource) => source.bind(this, { handlerRole: this.handlerRole }));
this.destinationBucket.grantReadWrite(handler);
if (props.accessControl) {
this.destinationBucket.grantPutAcl(handler);
}
if (props.distribution) {
handler.addToRolePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['cloudfront:GetInvalidation', 'cloudfront:CreateInvalidation'],
resources: ['*'],
}));
}
// Markers are not replaced if zip sources are not extracted, so throw an error
// if extraction is not wanted and sources have markers.
const _this = this;
this.node.addValidation({
validate(): string[] {
if (_this.sources.some(source => source.markers) && props.extract == false) {
return ['Some sources are incompatible with extract=false; sources with deploy-time values (such as \'snsTopic.topicArn\') must be extracted.'];
}
return [];
},
});
const crUniqueId = `CustomResource${this.renderUniqueId(props.memoryLimit, props.ephemeralStorageSize, props.vpc)}`;
this.cr = new cdk.CustomResource(this, crUniqueId, {
serviceToken: handler.functionArn,
resourceType: 'Custom::CDKBucketDeployment',
properties: {
SourceBucketNames: cdk.Lazy.uncachedList({ produce: () => this.sources.map(source => source.bucket.bucketName) }),
SourceObjectKeys: cdk.Lazy.uncachedList({ produce: () => this.sources.map(source => source.zipObjectKey) }),
SourceMarkers: cdk.Lazy.uncachedAny({
produce: () => {
return this.sources.reduce((acc, source) => {
if (source.markers) {
acc.push(source.markers);
// if there are more than 1 source, then all sources
// require markers (custom resource will throw an error otherwise)
} else if (this.sources.length > 1) {
acc.push({});
}
return acc;
}, [] as Array<Record<string, any>>);
},
}, { omitEmptyArray: true }),
SourceMarkersConfig: cdk.Lazy.uncachedAny({
produce: () => {
return this.sources.reduce((acc, source) => {
if (source.markersConfig) {
acc.push(source.markersConfig);
} else if (this.sources.length > 1) {
acc.push({});
}
return acc;
}, [] as Array<MarkersConfig>);
},
}, { omitEmptyArray: true }),
DestinationBucketName: this.destinationBucket.bucketName,
DestinationBucketKeyPrefix: props.destinationKeyPrefix,
RetainOnDelete: props.retainOnDelete,
Extract: props.extract,
Prune: props.prune ?? true,
Exclude: props.exclude,
Include: props.include,
UserMetadata: props.metadata ? mapUserMetadata(props.metadata) : undefined,
SystemMetadata: mapSystemMetadata(props),
DistributionId: props.distribution?.distributionId,
DistributionPaths: props.distributionPaths,
SignContent: props.signContent,
OutputObjectKeys: props.outputObjectKeys ?? true,
// Passing through the ARN sequences dependency on the deployment
DestinationBucketArn: cdk.Lazy.string({ produce: () => this.requestDestinationArn ? this.destinationBucket.bucketArn : undefined }),
},
});
let prefix: string = props.destinationKeyPrefix ?
`:${props.destinationKeyPrefix}` :
'';
prefix += `:${this.cr.node.addr.slice(-8)}`;
const tagKey = CUSTOM_RESOURCE_OWNER_TAG + prefix;
// destinationKeyPrefix can be 104 characters before we hit
// the tag key limit of 128
// '/this/is/a/random/key/prefix/that/is/a/lot/of/characters/do/we/think/that/it/will/ever/be/this/long?????'
// better to throw an error here than wait for CloudFormation to fail
if (!cdk.Token.isUnresolved(tagKey) && tagKey.length > 128) {
throw new ValidationError('The BucketDeployment construct requires that the "destinationKeyPrefix" be <=104 characters.', this);
}
/*
* This will add a tag to the deployment bucket in the format of
* `aws-cdk:cr-owned:{keyPrefix}:{uniqueHash}`
*
* For example:
* {
* Key: 'aws-cdk:cr-owned:deploy/here/:240D17B3',
* Value: 'true',
* }
*
* This will allow for scenarios where there is a single S3 Bucket that has multiple
* BucketDeployment resources deploying to it. Each bucket + keyPrefix can be "owned" by
* 1 or more BucketDeployment resources. Since there are some scenarios where multiple BucketDeployment
* resources can deploy to the same bucket and key prefix (e.g. using include/exclude) we
* also append part of the id to make the key unique.
*
* As long as a bucket + keyPrefix is "owned" by a BucketDeployment resource, another CR
* cannot delete data. There are a couple of scenarios where this comes into play.
*
* 1. If the LogicalResourceId of the CustomResource changes (e.g. the crUniqueId changes)
* CloudFormation will first issue a 'Create' to create the new CustomResource and will
* update the Tag on the bucket. CloudFormation will then issue a 'Delete' on the old CustomResource
* and since the new CR "owns" the Bucket+keyPrefix it will not delete the contents of the bucket
*
* 2. If the BucketDeployment resource is deleted _and_ it is the only CR for that bucket+keyPrefix
* then CloudFormation will first remove the tag from the bucket and then issue a "Delete" to the
* CR. Since there are no tags indicating that this bucket+keyPrefix is "owned" then it will delete
* the contents.
*
* 3. If the BucketDeployment resource is deleted _and_ it is *not* the only CR for that bucket:keyPrefix
* then CloudFormation will first remove the tag from the bucket and then issue a "Delete" to the CR.
* Since there are other CRs that also "own" that bucket+keyPrefix there will still be a tag on the bucket
* and the contents will not be removed.
*
* 4. If the BucketDeployment resource _and_ the S3 Bucket are both removed, then CloudFormation will first
* issue a "Delete" to the CR and since there is a tag on the bucket the contents will not be removed. If you
* want the contents of the bucket to be removed on bucket deletion, then `autoDeleteObjects` property should
* be set to true on the Bucket.
*/
cdk.Tags.of(this.destinationBucket).add(tagKey, 'true');
}
/**
* The bucket after the deployment
*
* If you want to reference the destination bucket in another construct and make sure the
* bucket deployment has happened before the next operation is started, pass the other construct
* a reference to `deployment.deployedBucket`.
*
* Note that this only returns an immutable reference to the destination bucket.
* If sequenced access to the original destination bucket is required, you may add a dependency
* on the bucket deployment instead: `otherResource.node.addDependency(deployment)`
*/
public get deployedBucket(): s3.IBucket {
this.requestDestinationArn = true;
this._deployedBucket = this._deployedBucket ?? s3.Bucket.fromBucketAttributes(this, 'DestinationBucket', {
bucketArn: cdk.Token.asString(this.cr.getAtt('DestinationBucketArn')),
region: this.destinationBucket.env.region,
account: this.destinationBucket.env.account,
isWebsite: this.destinationBucket.isWebsite,
});
return this._deployedBucket;
}
/**
* The object keys for the sources deployed to the S3 bucket.
*
* This returns a list of tokenized object keys for source files that are deployed to the bucket.
*
* This can be useful when using `BucketDeployment` with `extract` set to `false` and you need to reference
* the object key that resides in the bucket for that zip source file somewhere else in your CDK
* application, such as in a CFN output.
*
* For example, use `Fn.select(0, myBucketDeployment.objectKeys)` to reference the object key of the
* first source file in your bucket deployment.
*/
public get objectKeys(): string[] {
const objectKeys = cdk.Token.asList(this.cr.getAtt('SourceObjectKeys'));
return objectKeys;
}
/**
* Add an additional source to the bucket deployment
*
* @example
* declare const websiteBucket: s3.IBucket;
* const deployment = new s3deploy.BucketDeployment(this, 'Deployment', {
* sources: [s3deploy.Source.asset('./website-dist')],
* destinationBucket: websiteBucket,
* });
*
* deployment.addSource(s3deploy.Source.asset('./another-asset'));
*/
public addSource(source: ISource): void {
const config = source.bind(this, { handlerRole: this.handlerRole });
if (!this.sources.some((c) => sourceConfigEqual(cdk.Stack.of(this), c, config))) {
this.sources.push(config);
}
}
private renderUniqueId(memoryLimit?: number, ephemeralStorageSize?: cdk.Size, vpc?: ec2.IVpc) {
let uuid = '';
// if the user specifes a custom memory limit, we define another singleton handler
// with this configuration. otherwise, it won't be possible to use multiple
// configurations since we have a singleton.
if (memoryLimit) {
if (cdk.Token.isUnresolved(memoryLimit)) {
throw new ValidationError("Can't use tokens when specifying 'memoryLimit' since we use it to identify the singleton custom resource handler.", this);
}
uuid += `-${memoryLimit.toString()}MiB`;
}
// if the user specifies a custom ephemeral storage size, we define another singleton handler
// with this configuration. otherwise, it won't be possible to use multiple
// configurations since we have a singleton.
if (ephemeralStorageSize) {
if (ephemeralStorageSize.isUnresolved()) {
throw new ValidationError("Can't use tokens when specifying 'ephemeralStorageSize' since we use it to identify the singleton custom resource handler.", this);
}
uuid += `-${ephemeralStorageSize.toMebibytes().toString()}MiB`;
}
// if the user specifies a VPC, we define another singleton handler
// with this configuration. otherwise, it won't be possible to use multiple
// configurations since we have a singleton.
// A VPC is a must if EFS storage is used and that's why we are only using VPC in uuid.
if (vpc) {
uuid += `-${vpc.node.addr}`;
}
return uuid;
}
private renderSingletonUuid(memoryLimit?: number, ephemeralStorageSize?: cdk.Size, vpc?: ec2.IVpc) {
let uuid = '8693BB64-9689-44B6-9AAF-B0CC9EB8756C';
uuid += this.renderUniqueId(memoryLimit, ephemeralStorageSize, vpc);
return uuid;
}
/**
* Function to get/create a stack singleton instance of EFS FileSystem per vpc.
*
* @param scope Construct
* @param fileSystemProps EFS FileSystemProps
*/
private getOrCreateEfsFileSystem(scope: Construct, fileSystemProps: efs.FileSystemProps): efs.FileSystem {
const stack = cdk.Stack.of(scope);
const uuid = `BucketDeploymentEFS-VPC-${fileSystemProps.vpc.node.addr}`;
return stack.node.tryFindChild(uuid) as efs.FileSystem ?? new efs.FileSystem(scope, uuid, fileSystemProps);
}
}
export interface DeployTimeSubstitutedFileProps {
/**
* Path to the user's local file.
*/
readonly source: string;
/**
* The object key in the destination bucket where the processed
* file would be written to.
* @default - Fingerprint of the file content would be used as object key
*/
readonly destinationKey?: string;
/**
* The S3 bucket to sync the contents of the zip file to.
*/
readonly destinationBucket: s3.IBucket;
/**
* User-defined substitutions to make in the file.
* Placeholders in the user's local file must be specified with double curly
* brackets and spaces. For example, if you use the key 'xxxx' in the file,
* it must be written as: {{ xxxx }} to be recognized by the construct as a
* substitution.
*/
readonly substitutions: { [key: string]: string };
/**
* Execution role associated with this function
*
* @default - A role is automatically created
*/
readonly role?: iam.IRole;
}
/**
* `DeployTimeSubstitutedFile` is an extension of `BucketDeployment` that allows users to
* upload individual files and specify to make substitutions in the file.
*/
export class DeployTimeSubstitutedFile extends BucketDeployment {
public readonly objectKey: string;
constructor(scope: Construct, id: string, props: DeployTimeSubstitutedFileProps) {
if (!fs.existsSync(props.source)) {
throw new ValidationError(`No file found at 'source' path ${props.source}`, scope);
}
// Makes substitutions on the file
let fileData = fs.readFileSync(props.source, 'utf-8');
fileData = fileData.replace(/{{\s*(\w+)\s*}}/g, function (match, expr) {
return props.substitutions[expr] ?? match;
});
const objectKey = props.destinationKey ?? cdk.FileSystem.fingerprint(props.source);
const fileSource = Source.data(objectKey, fileData);
const fullBucketDeploymentProps: BucketDeploymentProps = {
prune: false,
extract: true,
...props,
sources: [fileSource],
role: props.role,
};
super(scope, id, fullBucketDeploymentProps);
// sets the object key
this.objectKey = objectKey;
}
public get bucket(): s3.IBucket {
return this.deployedBucket;
}
}
/**
* Metadata.
*
* The `x-amz-meta-` prefix will automatically be added to keys.
*
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#UserMetadata
*/
function mapUserMetadata(metadata: { [key: string]: string }) {
const mapKey = (key: string) => key.toLowerCase();
return Object.keys(metadata).reduce((o, key) => ({ ...o, [mapKey(key)]: metadata[key] }), {});
}
function mapSystemMetadata(metadata: BucketDeploymentProps) {
const res: { [key: string]: string } = {};
if (metadata.cacheControl) { res['cache-control'] = metadata.cacheControl.map(c => c.value).join(', '); }
if (metadata.expires) { res.expires = metadata.expires.date.toUTCString(); }
if (metadata.contentDisposition) { res['content-disposition'] = metadata.contentDisposition; }
if (metadata.contentEncoding) { res['content-encoding'] = metadata.contentEncoding; }
if (metadata.contentLanguage) { res['content-language'] = metadata.contentLanguage; }
if (metadata.contentType) { res['content-type'] = metadata.contentType; }
if (metadata.serverSideEncryption) { res.sse = metadata.serverSideEncryption; }
if (metadata.storageClass) { res['storage-class'] = metadata.storageClass; }
if (metadata.websiteRedirectLocation) { res['website-redirect'] = metadata.websiteRedirectLocation; }
if (metadata.serverSideEncryptionAwsKmsKeyId) { res['sse-kms-key-id'] = metadata.serverSideEncryptionAwsKmsKeyId; }
if (metadata.serverSideEncryptionCustomerAlgorithm) { res['sse-c-copy-source'] = metadata.serverSideEncryptionCustomerAlgorithm; }
if (metadata.accessControl) { res.acl = toKebabCase(metadata.accessControl.toString()); }
return Object.keys(res).length === 0 ? undefined : res;
}
/**
* Used for HTTP cache-control header, which influences downstream caches.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
export class CacheControl {
/**
* Sets 'must-revalidate'.
*/
public static mustRevalidate() { return new CacheControl('must-revalidate'); }
/**
* Sets 'no-cache'.
*/
public static noCache() { return new CacheControl('no-cache'); }
/**
* Sets 'no-transform'.
*/
public static noTransform() { return new CacheControl('no-transform'); }
/**
* Sets 'no-store'.
*/
public static noStore() { return new CacheControl('no-store'); }
/**
* Sets 'must-understand'.
*/
public static mustUnderstand() { return new CacheControl('must-understand'); }
/**
* Sets 'public'.
*/
public static setPublic() { return new CacheControl('public'); }
/**
* Sets 'private'.
*/
public static setPrivate() { return new CacheControl('private'); }
/**
* Sets 'immutable'.
*/
public static immutable() { return new CacheControl('immutable'); }
/**
* Sets 'proxy-revalidate'.
*/
public static proxyRevalidate() { return new CacheControl('proxy-revalidate'); }
/**
* Sets 'max-age=<duration-in-seconds>'.
*/
public static maxAge(t: cdk.Duration) { return new CacheControl(`max-age=${t.toSeconds()}`); }
/**
* Sets 's-maxage=<duration-in-seconds>'.
*/
public static sMaxAge(t: cdk.Duration) { return new CacheControl(`s-maxage=${t.toSeconds()}`); }
/**
* Sets 'stale-while-revalidate=<duration-in-seconds>'.
*/
public static staleWhileRevalidate(t: cdk.Duration) { return new CacheControl(`stale-while-revalidate=${t.toSeconds()}`); }
/**
* Sets 'stale-if-error=<duration-in-seconds>'.
*/
public static staleIfError(t: cdk.Duration) { return new CacheControl(`stale-if-error=${t.toSeconds()}`); }
/**
* Constructs a custom cache control key from the literal value.
*/
public static fromString(s: string) { return new CacheControl(s); }
private constructor(
/**
* The raw cache control setting.
*/
public readonly value: any,
) { }
}
/**
* Indicates whether server-side encryption is enabled for the object, and whether that encryption is
* from the AWS Key Management Service (AWS KMS) or from Amazon S3 managed encryption (SSE-S3).
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
export enum ServerSideEncryption {
/**
* 'AES256'
*/
AES_256 = 'AES256',
/**
* 'aws:kms'
*/
AWS_KMS = 'aws:kms',
/**
* 'aws:kms:dsse'
*/
AWS_KMS_DSSE = 'aws:kms:dsse',
}
/**
* Storage class used for storing the object.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*/
export enum StorageClass {
/**
* 'STANDARD'
*/
STANDARD = 'STANDARD',
/**
* 'REDUCED_REDUNDANCY'
*/
REDUCED_REDUNDANCY = 'REDUCED_REDUNDANCY',
/**
* 'STANDARD_IA'
*/
STANDARD_IA = 'STANDARD_IA',
/**
* 'ONEZONE_IA'
*/
ONEZONE_IA = 'ONEZONE_IA',
/**
* 'INTELLIGENT_TIERING'
*/
INTELLIGENT_TIERING = 'INTELLIGENT_TIERING',
/**
* 'GLACIER'
*/
GLACIER = 'GLACIER',
/**
* 'DEEP_ARCHIVE'
*/
DEEP_ARCHIVE = 'DEEP_ARCHIVE',
}
/**
* Used for HTTP expires header, which influences downstream caches. Does NOT influence deletion of the object.
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata
*
* @deprecated use core.Expiration
*/
export class Expires {
/**
* Expire at the specified date
* @param d date to expire at
*/
public static atDate(d: Date) { return new Expires(d.toUTCString()); }
/**
* Expire at the specified timestamp
* @param t timestamp in unix milliseconds
*/
public static atTimestamp(t: number) { return Expires.atDate(new Date(t)); }
/**
* Expire once the specified duration has passed since deployment time
* @param t the duration to wait before expiring
*/
public static after(t: cdk.Duration) { return Expires.atDate(new Date(Date.now() + t.toMilliseconds())); }
/**
* Create an expiration date from a raw date string.
*/
public static fromString(s: string) { return new Expires(s); }
private constructor(
/**
* The raw expiration date expression.
*/
public readonly value: any,
) { }
}
/**
* Custom user defined metadata.
*
* @deprecated Use raw property bags instead (object literals, `Map<String,Object>`, etc... )
*/
export interface UserDefinedObjectMetadata {
/**
* Arbitrary metadata key-values
* The `x-amz-meta-` prefix will automatically be added to keys.
*
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#UserMetadata
*
* This index signature is not usable in non-TypeScript/JavaScript languages.
*
* @jsii ignore
*/
readonly [key: string]: string;
}
function sourceConfigEqual(stack: cdk.Stack, a: SourceConfig, b: SourceConfig) {
return (
JSON.stringify(stack.resolve(a.bucket.bucketName)) === JSON.stringify(stack.resolve(b.bucket.bucketName))
&& a.zipObjectKey === b.zipObjectKey
&& a.markers === undefined && b.markers === undefined);
}