constructor()

in packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts [242:401]


  constructor(scope: Construct, id: string, props: BucketDeploymentProps) {
    super(scope, id);

    if (props.distributionPaths) {
      if (!props.distribution) {
        throw new Error('Distribution must be specified if distribution paths are specified');
      }
      if (!cdk.Token.isUnresolved(props.distributionPaths)) {
        if (!props.distributionPaths.every(distributionPath => cdk.Token.isUnresolved(distributionPath) || distributionPath.startsWith('/'))) {
          throw new Error('Distribution paths must start with "/"');
        }
      }
    }

    if (props.useEfs && !props.vpc) {
      throw new Error('Vpc must be specified if useEfs is set');
    }

    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 lambda.SingletonFunction(this, 'CustomResourceHandler', {
      uuid: this.renderSingletonUuid(props.memoryLimit, props.vpc),
      code: lambda.Code.fromAsset(path.join(__dirname, 'lambda')),
      layers: [new AwsCliLayer(this, 'AwsCliLayer')],
      runtime: lambda.Runtime.PYTHON_3_7,
      environment: props.useEfs ? {
        MOUNT_PATH: mountPath,
      } : undefined,
      handler: 'index.handler',
      lambdaPurpose: 'Custom::CDKBucketDeployment',
      timeout: cdk.Duration.minutes(15),
      role: props.role,
      memorySize: props.memoryLimit,
      vpc: props.vpc,
      vpcSubnets: props.vpcSubnets,
      filesystem: accessPoint ? lambda.FileSystem.fromEfsAccessPoint(
        accessPoint,
        mountPath,
      ) : undefined,
      logRetention: props.logRetention,
    });

    const handlerRole = handler.role;
    if (!handlerRole) { throw new Error('lambda.SingletonFunction should have created a Role'); }

    const sources: SourceConfig[] = props.sources.map((source: ISource) => source.bind(this, { handlerRole }));

    props.destinationBucket.grantReadWrite(handler);
    if (props.distribution) {
      handler.addToRolePolicy(new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ['cloudfront:GetInvalidation', 'cloudfront:CreateInvalidation'],
        resources: ['*'],
      }));
    }

    const crUniqueId = `CustomResource${this.renderUniqueId(props.memoryLimit, props.vpc)}`;
    const cr = new cdk.CustomResource(this, crUniqueId, {
      serviceToken: handler.functionArn,
      resourceType: 'Custom::CDKBucketDeployment',
      properties: {
        SourceBucketNames: sources.map(source => source.bucket.bucketName),
        SourceObjectKeys: sources.map(source => source.zipObjectKey),
        DestinationBucketName: props.destinationBucket.bucketName,
        DestinationBucketKeyPrefix: props.destinationKeyPrefix,
        RetainOnDelete: props.retainOnDelete,
        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,
      },
    });

    let prefix: string = props.destinationKeyPrefix ?
      `:${props.destinationKeyPrefix}` :
      '';
    prefix += `:${cr.node.addr.substr(-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 (tagKey.length > 128) {
      throw new Error('The BucketDeployment construct requires that the "destinationKeyPrefix" be <=104 characters');
    }

    /*
     * 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(props.destinationBucket).add(tagKey, 'true');

  }