packages/@aws-cdk/toolkit-lib/lib/api/hotswap/s3-bucket-deployments.ts (107 lines of code) (raw):
import type { HotswapChange } from './common';
import type { ResourceChange } from '../../payloads/hotswap';
import type { SDK } from '../aws-auth/private';
import type { EvaluateCloudFormationTemplate } from '../cloudformation';
/**
* This means that the value is required to exist by CloudFormation's Custom Resource API (or our S3 Bucket Deployment Lambda's API)
* but the actual value specified is irrelevant
*/
const REQUIRED_BY_CFN = 'required-to-be-present-by-cfn';
const CDK_BUCKET_DEPLOYMENT_CFN_TYPE = 'Custom::CDKBucketDeployment';
export async function isHotswappableS3BucketDeploymentChange(
logicalId: string,
change: ResourceChange,
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
): Promise<HotswapChange[]> {
// In old-style synthesis, the policy used by the lambda to copy assets Ref's the assets directly,
// meaning that the changes made to the Policy are artifacts that can be safely ignored
const ret: HotswapChange[] = [];
if (change.newValue.Type !== CDK_BUCKET_DEPLOYMENT_CFN_TYPE) {
return [];
}
// no classification to be done here; all the properties of this custom resource thing are hotswappable
const customResourceProperties = await evaluateCfnTemplate.evaluateCfnExpression({
...change.newValue.Properties,
ServiceToken: undefined,
});
// note that this gives the ARN of the lambda, not the name. This is fine though, the invoke() sdk call will take either
const functionName = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.ServiceToken);
if (!functionName) {
return ret;
}
ret.push({
change: {
cause: change,
resources: [{
logicalId,
physicalName: customResourceProperties.DestinationBucketName,
resourceType: CDK_BUCKET_DEPLOYMENT_CFN_TYPE,
description: `Contents of AWS::S3::Bucket '${customResourceProperties.DestinationBucketName}'`,
metadata: evaluateCfnTemplate.metadataFor(logicalId),
}],
},
hotswappable: true,
service: 'custom-s3-deployment',
apply: async (sdk: SDK) => {
await sdk.lambda().invokeCommand({
FunctionName: functionName,
// Lambda refuses to take a direct JSON object and requires it to be stringify()'d
Payload: JSON.stringify({
RequestType: 'Update',
ResponseURL: REQUIRED_BY_CFN,
PhysicalResourceId: REQUIRED_BY_CFN,
StackId: REQUIRED_BY_CFN,
RequestId: REQUIRED_BY_CFN,
LogicalResourceId: REQUIRED_BY_CFN,
ResourceProperties: stringifyObject(customResourceProperties), // JSON.stringify() doesn't turn the actual objects to strings, but the lambda expects strings
}),
});
},
});
return ret;
}
export async function skipChangeForS3DeployCustomResourcePolicy(
iamPolicyLogicalId: string,
change: ResourceChange,
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
): Promise<boolean> {
if (change.newValue.Type !== 'AWS::IAM::Policy') {
return false;
}
const roles: string[] = change.newValue.Properties?.Roles;
// If no roles are referenced, the policy is definitely not used for a S3Deployment
if (!roles || !roles.length) {
return false;
}
// Check if every role this policy is referenced by is only used for a S3Deployment
for (const role of roles) {
const roleArn = await evaluateCfnTemplate.evaluateCfnExpression(role);
const roleLogicalId = await evaluateCfnTemplate.findLogicalIdForPhysicalName(roleArn);
// We must assume this role is used for something else, because we can't check it
if (!roleLogicalId) {
return false;
}
// Find all interesting reference to the role
const roleRefs = evaluateCfnTemplate
.findReferencesTo(roleLogicalId)
// we are not interested in the reference from the original policy - it always exists
.filter((roleRef) => !(roleRef.Type == 'AWS::IAM::Policy' && roleRef.LogicalId === iamPolicyLogicalId));
// Check if the role is only used for S3Deployment
// We know this is the case, if S3Deployment -> Lambda -> Role is satisfied for every reference
// And we have at least one reference.
const isRoleOnlyForS3Deployment =
roleRefs.length >= 1 &&
roleRefs.every((roleRef) => {
if (roleRef.Type === 'AWS::Lambda::Function') {
const lambdaRefs = evaluateCfnTemplate.findReferencesTo(roleRef.LogicalId);
// Every reference must be to the custom resource and at least one reference must be present
return (
lambdaRefs.length >= 1 && lambdaRefs.every((lambdaRef) => lambdaRef.Type === 'Custom::CDKBucketDeployment')
);
}
return false;
});
// We have determined this role is used for something else, so we can't skip the change
if (!isRoleOnlyForS3Deployment) {
return false;
}
}
// We have checked that any use of this policy is only for S3Deployment and we can safely skip it
return true;
}
function stringifyObject(obj: any): any {
if (obj == null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(stringifyObject);
}
if (typeof obj !== 'object') {
return obj.toString();
}
const ret: { [k: string]: any } = {};
for (const [k, v] of Object.entries(obj)) {
ret[k] = stringifyObject(v);
}
return ret;
}