constructor()

in packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts [303:514]


  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');
  }