From 0a38e5ab5fc25c469f09f794780d63dc95d5e7d7 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Thu, 7 Sep 2023 17:36:00 +0200 Subject: [PATCH 1/9] fix nested stack hotswap --- .../api/evaluate-cloudformation-template.ts | 78 +++++++++++++++++-- .../aws-cdk/lib/api/hotswap-deployments.ts | 19 +++-- .../lib/api/logs/find-cloudwatch-logs.ts | 3 +- 3 files changed, 82 insertions(+), 18 deletions(-) diff --git a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts index f4a2576cee55e..aa43b5f93f972 100644 --- a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts +++ b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts @@ -1,5 +1,6 @@ import * as AWS from 'aws-sdk'; import { ISDK } from './aws-auth'; +import { NestedStackNames } from './nested-stack-helpers'; export interface ListStackResources { listStackResources(): Promise; @@ -42,27 +43,33 @@ export interface ResourceDefinition { } export interface EvaluateCloudFormationTemplateProps { + readonly stackName: string; readonly template: Template; readonly parameters: { [parameterName: string]: string }; readonly account: string; readonly region: string; readonly partition: string; readonly urlSuffix: (region: string) => string; - readonly listStackResources: ListStackResources; + readonly sdk: ISDK; + readonly nestedStackNames?: { [nestedStackLogicalId: string]: NestedStackNames }; } export class EvaluateCloudFormationTemplate { - private readonly stackResources: ListStackResources; + private readonly stackName: string; private readonly template: Template; private readonly context: { [k: string]: any }; private readonly account: string; private readonly region: string; private readonly partition: string; private readonly urlSuffix: (region: string) => string; + private readonly sdk: ISDK; + private readonly nestedStackNames: { [nestedStackLogicalId: string]: NestedStackNames }; + private readonly stackResources: LazyListStackResources; + private cachedUrlSuffix: string | undefined; constructor(props: EvaluateCloudFormationTemplateProps) { - this.stackResources = props.listStackResources; + this.stackName = props.stackName; this.template = props.template; this.context = { 'AWS::AccountId': props.account, @@ -74,22 +81,34 @@ export class EvaluateCloudFormationTemplate { this.region = props.region; this.partition = props.partition; this.urlSuffix = props.urlSuffix; + this.sdk = props.sdk; + + // We need names of nested stack so we can evaluate cross stack references + this.nestedStackNames = props.nestedStackNames ?? {}; + + // The current resources of the Stack. + // We need them to figure out the physical name of a resource in case it wasn't specified by the user. + // We fetch it lazily, to save a service call, in case all hotswapped resources have their physical names set. + this.stackResources = new LazyListStackResources(this.sdk, this.stackName); } // clones current EvaluateCloudFormationTemplate object, but updates the stack name - public createNestedEvaluateCloudFormationTemplate( - listNestedStackResources: ListStackResources, + public async createNestedEvaluateCloudFormationTemplate( + stackName: string, nestedTemplate: Template, nestedStackParameters: { [parameterName: string]: any }, ) { + const evaluatedParams = await this.evaluateCfnExpression(nestedStackParameters); return new EvaluateCloudFormationTemplate({ + stackName, template: nestedTemplate, - parameters: nestedStackParameters, + parameters: evaluatedParams, account: this.account, region: this.region, partition: this.partition, urlSuffix: this.urlSuffix, - listStackResources: listNestedStackResources, + sdk: this.sdk, + nestedStackNames: this.nestedStackNames, }); } @@ -262,20 +281,52 @@ export class EvaluateCloudFormationTemplate { return this.cachedUrlSuffix; } + // Try finding the ref in the passed in parameters const parameterTarget = this.context[logicalId]; if (parameterTarget) { return parameterTarget; } + + // If not in the passed in parameters, see if there is a default value in the template parameter that was not passed in + const defaultParameterValue = this.template.Parameters?.[logicalId]?.Default; + if (defaultParameterValue) { + return defaultParameterValue; + } + // if it's not a Parameter, we need to search in the current Stack resources return this.findGetAttTarget(logicalId); } private async findGetAttTarget(logicalId: string, attribute?: string): Promise { + + // Handle case where the attribute is referencing a stack output (used in nested stacks to share parameters) + // See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-cloudformation.html#w2ab1c17c23c19b5 + if (logicalId === 'Outputs' && attribute) { + return this.evaluateCfnExpression(this.template.Outputs[attribute]?.Value); + } + const stackResources = await this.stackResources.listStackResources(); const foundResource = stackResources.find(sr => sr.LogicalResourceId === logicalId); if (!foundResource) { return undefined; } + + if (foundResource.ResourceType == 'AWS::CloudFormation::Stack' && attribute?.startsWith('Outputs.')) { + // need to resolve attributes from another stack's Output section + const dependantStackName = this.nestedStackNames[logicalId]?.nestedStackPhysicalName; + if (!dependantStackName) { + //this is a newly created nested stack and cannot be hotswapped + return undefined; + } + const dependantStackTemplate = this.template.Resources[logicalId]; + const evaluateCfnTemplate = await this.createNestedEvaluateCloudFormationTemplate( + dependantStackName, + dependantStackTemplate?.Properties?.NestedTemplate, + dependantStackTemplate.newValue?.Properties?.Parameters); + + // Split Outputs. into 'Outputs' and '' and recursively call evaluate + return evaluateCfnTemplate.evaluateCfnExpression({ 'Fn::GetAtt': attribute.split(/\.(.*)/s) }); + } // now, we need to format the appropriate identifier depending on the resource type, // and the requested attribute name return this.formatResourceAttribute(foundResource, attribute); @@ -362,6 +413,9 @@ const RESOURCE_TYPE_ATTRIBUTES_FORMATS: { [type: string]: { [attribute: string]: }, 'AWS::DynamoDB::Table': { Arn: stdSlashResourceArnFmt }, 'AWS::AppSync::GraphQLApi': { ApiId: appsyncGraphQlApiApiIdFmt }, + 'AWS::AppSync::FunctionConfiguration': { FunctionId: appsyncGraphQlFunctionIDFmt }, + 'AWS::AppSync::DataSource': { Name: appsyncGraphQlDataSourceNameFmt }, + }; function iamArnFmt(parts: ArnParts): string { @@ -389,6 +443,16 @@ function appsyncGraphQlApiApiIdFmt(parts: ArnParts): string { return parts.resourceName.split('/')[1]; } +function appsyncGraphQlFunctionIDFmt(parts: ArnParts): string { + // arn:aws:appsync:us-east-1:111111111111:apis//functions/ + return parts.resourceName.split('/')[3]; +} + +function appsyncGraphQlDataSourceNameFmt(parts: ArnParts): string { + // arn:aws:appsync:us-east-1:111111111111:apis//datasources/ + return parts.resourceName.split('/')[3]; +} + interface Intrinsic { readonly name: string; readonly args: any; diff --git a/packages/aws-cdk/lib/api/hotswap-deployments.ts b/packages/aws-cdk/lib/api/hotswap-deployments.ts index eb03bdba5a8fb..7f304dbe7a90b 100644 --- a/packages/aws-cdk/lib/api/hotswap-deployments.ts +++ b/packages/aws-cdk/lib/api/hotswap-deployments.ts @@ -3,7 +3,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as chalk from 'chalk'; import { ISDK, Mode, SdkProvider } from './aws-auth'; import { DeployStackResult } from './deploy-stack'; -import { EvaluateCloudFormationTemplate, LazyListStackResources } from './evaluate-cloudformation-template'; +import { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-template'; import { isHotswappableAppSyncChange } from './hotswap/appsync-mapping-templates'; import { isHotswappableCodeBuildProjectChange } from './hotswap/code-build-projects'; import { ICON, ChangeHotswapResult, HotswapMode, HotswappableChange, NonHotswappableChange, HotswappableChangeCandidate, ClassifiedResourceChanges, reportNonHotswappableChange } from './hotswap/common'; @@ -54,21 +54,21 @@ export async function tryHotswapDeployment( // create a new SDK using the CLI credentials, because the default one will not work for new-style synthesis - // it assumes the bootstrap deploy Role, which doesn't have permissions to update Lambda functions const sdk = (await sdkProvider.forEnvironment(resolvedEnv, Mode.ForWriting)).sdk; - // The current resources of the Stack. - // We need them to figure out the physical name of a resource in case it wasn't specified by the user. - // We fetch it lazily, to save a service call, in case all hotswapped resources have their physical names set. - const listStackResources = new LazyListStackResources(sdk, stackArtifact.stackName); + + const currentTemplate = await loadCurrentTemplateWithNestedStacks(stackArtifact, sdk); + const evaluateCfnTemplate = new EvaluateCloudFormationTemplate({ + stackName: stackArtifact.stackName, template: stackArtifact.template, parameters: assetParams, account: resolvedEnv.account, region: resolvedEnv.region, partition: (await sdk.currentAccount()).partition, urlSuffix: (region) => sdk.getEndpointSuffix(region), - listStackResources, + sdk, + nestedStackNames: currentTemplate.nestedStackNames, }); - const currentTemplate = await loadCurrentTemplateWithNestedStacks(stackArtifact, sdk); const stackChanges = cfn_diff.diffTemplate(currentTemplate.deployedTemplate, stackArtifact.template); const { hotswappableChanges, nonHotswappableChanges } = await classifyResourceChanges( stackChanges, evaluateCfnTemplate, sdk, currentTemplate.nestedStackNames, @@ -231,9 +231,8 @@ async function findNestedHotswappableChanges( }; } - const nestedStackParameters = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue?.Properties?.Parameters); - const evaluateNestedCfnTemplate = evaluateCfnTemplate.createNestedEvaluateCloudFormationTemplate( - new LazyListStackResources(sdk, nestedStackName), change.newValue?.Properties?.NestedTemplate, nestedStackParameters, + const evaluateNestedCfnTemplate = await evaluateCfnTemplate.createNestedEvaluateCloudFormationTemplate( + nestedStackName, change.newValue?.Properties?.NestedTemplate, change.newValue?.Properties?.Parameters, ); const nestedDiff = cfn_diff.diffTemplate( diff --git a/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts b/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts index a54daabc9a0ae..bcee908dcc746 100644 --- a/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts +++ b/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts @@ -56,13 +56,14 @@ export async function findCloudWatchLogGroups( const listStackResources = new LazyListStackResources(sdk, stackArtifact.stackName); const evaluateCfnTemplate = new EvaluateCloudFormationTemplate({ + stackName: stackArtifact.stackName, template: stackArtifact.template, parameters: {}, account: resolvedEnv.account, region: resolvedEnv.region, partition: (await sdk.currentAccount()).partition, urlSuffix: (region) => sdk.getEndpointSuffix(region), - listStackResources, + sdk, }); const stackResources = await listStackResources.listStackResources(); From e454d9e090ed5b8ae06cc9bad3d4c133d5487565 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Thu, 7 Sep 2023 17:38:31 +0200 Subject: [PATCH 2/9] Add AppSync graphqlschema and pipeline resolvers as hotswappable --- .../api/hotswap/appsync-mapping-templates.ts | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts index 2ed050c1ed406..189559cc15a5e 100644 --- a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts +++ b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts @@ -1,5 +1,7 @@ -import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, lowerCaseFirstCharacter, reportNonHotswappableChange, transformObjectKeys } from './common'; +import { GetSchemaCreationStatusRequest, GetSchemaCreationStatusResponse } from 'aws-sdk/clients/appsync'; +import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys } from './common'; import { ISDK } from '../aws-auth'; + import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; export async function isHotswappableAppSyncChange( @@ -7,23 +9,19 @@ export async function isHotswappableAppSyncChange( ): Promise { const isResolver = change.newValue.Type === 'AWS::AppSync::Resolver'; const isFunction = change.newValue.Type === 'AWS::AppSync::FunctionConfiguration'; + const isGraphQLSchema = change.newValue.Type === 'AWS::AppSync::GraphQLSchema'; - if (!isResolver && !isFunction) { + if (!isResolver && !isFunction && !isGraphQLSchema) { return []; } const ret: ChangeHotswapResult = []; - if (isResolver && change.newValue.Properties?.Kind === 'PIPELINE') { - reportNonHotswappableChange( - ret, - change, - undefined, - 'Pipeline resolvers cannot be hotswapped since they reference the FunctionId of the underlying functions, which cannot be resolved', - ); - return ret; - } - const classifiedChanges = classifyChanges(change, ['RequestMappingTemplate', 'ResponseMappingTemplate']); + const classifiedChanges = classifyChanges(change, [ + 'RequestMappingTemplate', + 'ResponseMappingTemplate', + 'Definition', + ]); classifiedChanges.reportNonHotswappablePropertyChanges(ret); const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps); @@ -49,6 +47,7 @@ export async function isHotswappableAppSyncChange( const sdkProperties: { [name: string]: any } = { ...change.oldValue.Properties, + Definition: change.newValue.Properties?.Definition, requestMappingTemplate: change.newValue.Properties?.RequestMappingTemplate, responseMappingTemplate: change.newValue.Properties?.ResponseMappingTemplate, }; @@ -57,13 +56,25 @@ export async function isHotswappableAppSyncChange( if (isResolver) { await sdk.appsync().updateResolver(sdkRequestObject).promise(); - } else { + } else if (isFunction) { const { functions } = await sdk.appsync().listFunctions({ apiId: sdkRequestObject.apiId }).promise(); const { functionId } = functions?.find(fn => fn.name === physicalName) ?? {}; await sdk.appsync().updateFunction({ ...sdkRequestObject, functionId: functionId!, }).promise(); + } else { + let schemaCreationResponse: GetSchemaCreationStatusResponse = await sdk.appsync().startSchemaCreation(sdkRequestObject).promise(); + while (schemaCreationResponse.status && ['PROCESSING', 'DELETING'].some(status => status === schemaCreationResponse.status)) { + await new Promise(resolve => setTimeout(resolve, 1000)); // poll every second + const getSchemaCreationStatusRequest: GetSchemaCreationStatusRequest = { + apiId: sdkRequestObject.apiId, + }; + schemaCreationResponse = await sdk.appsync().getSchemaCreationStatus(getSchemaCreationStatusRequest).promise(); + } + if (schemaCreationResponse.status === 'FAILED') { + throw new Error(schemaCreationResponse.details); + } } }, }); From 7b741ecf8a89b0d0cde439cf3d6bcb0be889d6ac Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Fri, 8 Sep 2023 18:44:26 +0200 Subject: [PATCH 3/9] Build retry for updating AppSync functions when they fail due to concurrent modifications --- .../aws-cdk/lib/api/hotswap-deployments.ts | 2 + .../api/hotswap/appsync-mapping-templates.ts | 47 +++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/aws-cdk/lib/api/hotswap-deployments.ts b/packages/aws-cdk/lib/api/hotswap-deployments.ts index 7f304dbe7a90b..3b7737aa72e1e 100644 --- a/packages/aws-cdk/lib/api/hotswap-deployments.ts +++ b/packages/aws-cdk/lib/api/hotswap-deployments.ts @@ -24,9 +24,11 @@ const RESOURCE_DETECTORS: { [key:string]: HotswapDetector } = { 'AWS::Lambda::Function': isHotswappableLambdaFunctionChange, 'AWS::Lambda::Version': isHotswappableLambdaFunctionChange, 'AWS::Lambda::Alias': isHotswappableLambdaFunctionChange, + // AppSync 'AWS::AppSync::Resolver': isHotswappableAppSyncChange, 'AWS::AppSync::FunctionConfiguration': isHotswappableAppSyncChange, + 'AWS::AppSync::GraphQLSchema': isHotswappableAppSyncChange, 'AWS::ECS::TaskDefinition': isHotswappableEcsServiceChange, 'AWS::CodeBuild::Project': isHotswappableCodeBuildProjectChange, diff --git a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts index 189559cc15a5e..8f41188fd7542 100644 --- a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts +++ b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts @@ -19,8 +19,11 @@ export async function isHotswappableAppSyncChange( const classifiedChanges = classifyChanges(change, [ 'RequestMappingTemplate', + 'RequestMappingTemplateS3Location', 'ResponseMappingTemplate', + 'ResponseMappingTemplateS3Location', 'Definition', + 'DefinitionS3Location', ]); classifiedChanges.reportNonHotswappablePropertyChanges(ret); @@ -48,6 +51,7 @@ export async function isHotswappableAppSyncChange( const sdkProperties: { [name: string]: any } = { ...change.oldValue.Properties, Definition: change.newValue.Properties?.Definition, + DefinitionS3Location: change.newValue.Properties?.DefinitionS3Location, requestMappingTemplate: change.newValue.Properties?.RequestMappingTemplate, responseMappingTemplate: change.newValue.Properties?.ResponseMappingTemplate, }; @@ -57,13 +61,28 @@ export async function isHotswappableAppSyncChange( if (isResolver) { await sdk.appsync().updateResolver(sdkRequestObject).promise(); } else if (isFunction) { + if (sdkRequestObject.requestMappingTemplateS3Location) { + //code is in an S3 file but AppSync expects inline code + sdkRequestObject.requestMappingTemplate = (await fetchFileFromS3(sdkRequestObject.requestMappingTemplateS3Location, sdk))?.toString('utf8'); + delete sdkRequestObject.requestMappingTemplateS3Location; + } + if (sdkRequestObject.responseMappingTemplateS3Location) { + //code is in an S3 file but AppSync expects inline code + sdkRequestObject.responseMappingTemplate = (await fetchFileFromS3(sdkRequestObject.responseMappingTemplateS3Location, sdk))?.toString('utf8'); + delete sdkRequestObject.responseMappingTemplateS3Location; + } const { functions } = await sdk.appsync().listFunctions({ apiId: sdkRequestObject.apiId }).promise(); const { functionId } = functions?.find(fn => fn.name === physicalName) ?? {}; - await sdk.appsync().updateFunction({ - ...sdkRequestObject, - functionId: functionId!, - }).promise(); + await simpleRetry( + () => sdk.appsync().updateFunction({ ...sdkRequestObject, functionId: functionId! }).promise(), + 3, + 'ConcurrentModificationException'); } else { + if (sdkRequestObject.definitionS3Location) { + // code is in an S3 file but AppSync expects inline code + sdkRequestObject.definition = await fetchFileFromS3(sdkRequestObject.definitionS3Location, sdk); + delete sdkRequestObject.definitionS3Location; + } let schemaCreationResponse: GetSchemaCreationStatusResponse = await sdk.appsync().startSchemaCreation(sdkRequestObject).promise(); while (schemaCreationResponse.status && ['PROCESSING', 'DELETING'].some(status => status === schemaCreationResponse.status)) { await new Promise(resolve => setTimeout(resolve, 1000)); // poll every second @@ -82,3 +101,23 @@ export async function isHotswappableAppSyncChange( return ret; } + +async function fetchFileFromS3(s3Url: string, sdk: ISDK) { + const s3PathParts = s3Url.split('/'); + const s3Bucket = s3PathParts[2]; // first two are "s3:"" and "" due to two // + const s3Key = s3PathParts.splice(3).join('/'); // after removing first three we reconstruct the key + return (await sdk.s3().getObject({ Bucket: s3Bucket, Key: s3Key }).promise()).Body; +} + +async function simpleRetry(fn: () => Promise, numOfRetries: number, errorCodeToRetry: string) { + try { + await fn(); + } catch (error: any) { + if (error && error.code === errorCodeToRetry && numOfRetries > 0) { + await new Promise((resolve) => setTimeout(resolve, 500)); // wait half a second + await simpleRetry(fn, numOfRetries - 1, errorCodeToRetry); + } else { + throw error; + } + } +} From a93289c1b58618e164fc38a412d99eac3c02bd36 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Wed, 13 Sep 2023 21:54:04 +0200 Subject: [PATCH 4/9] update tests for appsync resolver and functions hotswap --- .../api/hotswap/appsync-mapping-templates.ts | 34 ++-- ...ping-templates-hotswap-deployments.test.ts | 185 ++++++++++++++++-- .../test/api/hotswap/hotswap-test-setup.ts | 4 + 3 files changed, 188 insertions(+), 35 deletions(-) diff --git a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts index 8f41188fd7542..908cbec6b1dbe 100644 --- a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts +++ b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts @@ -53,24 +53,31 @@ export async function isHotswappableAppSyncChange( Definition: change.newValue.Properties?.Definition, DefinitionS3Location: change.newValue.Properties?.DefinitionS3Location, requestMappingTemplate: change.newValue.Properties?.RequestMappingTemplate, + requestMappingTemplateS3Location: change.newValue.Properties?.RequestMappingTemplateS3Location, responseMappingTemplate: change.newValue.Properties?.ResponseMappingTemplate, + responseMappingTemplateS3Location: change.newValue.Properties?.ResponseMappingTemplateS3Location, }; const evaluatedResourceProperties = await evaluateCfnTemplate.evaluateCfnExpression(sdkProperties); const sdkRequestObject = transformObjectKeys(evaluatedResourceProperties, lowerCaseFirstCharacter); + // resolve s3 location files as SDK doesn't take in s3 location but inline code + if (sdkRequestObject.requestMappingTemplateS3Location) { + sdkRequestObject.requestMappingTemplate = (await fetchFileFromS3(sdkRequestObject.requestMappingTemplateS3Location, sdk))?.toString('utf8'); + delete sdkRequestObject.requestMappingTemplateS3Location; + } + if (sdkRequestObject.responseMappingTemplateS3Location) { + sdkRequestObject.responseMappingTemplate = (await fetchFileFromS3(sdkRequestObject.responseMappingTemplateS3Location, sdk))?.toString('utf8'); + delete sdkRequestObject.responseMappingTemplateS3Location; + } + if (sdkRequestObject.definitionS3Location) { + sdkRequestObject.definition = await fetchFileFromS3(sdkRequestObject.definitionS3Location, sdk); + delete sdkRequestObject.definitionS3Location; + } + if (isResolver) { await sdk.appsync().updateResolver(sdkRequestObject).promise(); } else if (isFunction) { - if (sdkRequestObject.requestMappingTemplateS3Location) { - //code is in an S3 file but AppSync expects inline code - sdkRequestObject.requestMappingTemplate = (await fetchFileFromS3(sdkRequestObject.requestMappingTemplateS3Location, sdk))?.toString('utf8'); - delete sdkRequestObject.requestMappingTemplateS3Location; - } - if (sdkRequestObject.responseMappingTemplateS3Location) { - //code is in an S3 file but AppSync expects inline code - sdkRequestObject.responseMappingTemplate = (await fetchFileFromS3(sdkRequestObject.responseMappingTemplateS3Location, sdk))?.toString('utf8'); - delete sdkRequestObject.responseMappingTemplateS3Location; - } + const { functions } = await sdk.appsync().listFunctions({ apiId: sdkRequestObject.apiId }).promise(); const { functionId } = functions?.find(fn => fn.name === physicalName) ?? {}; await simpleRetry( @@ -78,11 +85,6 @@ export async function isHotswappableAppSyncChange( 3, 'ConcurrentModificationException'); } else { - if (sdkRequestObject.definitionS3Location) { - // code is in an S3 file but AppSync expects inline code - sdkRequestObject.definition = await fetchFileFromS3(sdkRequestObject.definitionS3Location, sdk); - delete sdkRequestObject.definitionS3Location; - } let schemaCreationResponse: GetSchemaCreationStatusResponse = await sdk.appsync().startSchemaCreation(sdkRequestObject).promise(); while (schemaCreationResponse.status && ['PROCESSING', 'DELETING'].some(status => status === schemaCreationResponse.status)) { await new Promise(resolve => setTimeout(resolve, 1000)); // poll every second @@ -104,7 +106,7 @@ export async function isHotswappableAppSyncChange( async function fetchFileFromS3(s3Url: string, sdk: ISDK) { const s3PathParts = s3Url.split('/'); - const s3Bucket = s3PathParts[2]; // first two are "s3:"" and "" due to two // + const s3Bucket = s3PathParts[2]; // first two are "s3:" and "" due to s3:// const s3Key = s3PathParts.splice(3).join('/'); // after removing first three we reconstruct the key return (await sdk.s3().getObject({ Bucket: s3Bucket, Key: s3Key }).promise()).Body; } diff --git a/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts index f7780b873806b..04b530a6e79e8 100644 --- a/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts @@ -1,17 +1,19 @@ /* eslint-disable import/order */ -import { AppSync } from 'aws-sdk'; +import { AppSync, S3 } from 'aws-sdk'; import * as setup from './hotswap-test-setup'; import { HotswapMode } from '../../../lib/api/hotswap/common'; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; let mockUpdateResolver: (params: AppSync.UpdateResolverRequest) => AppSync.UpdateResolverResponse; let mockUpdateFunction: (params: AppSync.UpdateFunctionRequest) => AppSync.UpdateFunctionResponse; +let mockS3GetObject: (params: S3.GetObjectRequest) => S3.GetObjectOutput; beforeEach(() => { hotswapMockSdkProvider = setup.setupHotswapTests(); mockUpdateResolver = jest.fn(); mockUpdateFunction = jest.fn(); hotswapMockSdkProvider.stubAppSync({ updateResolver: mockUpdateResolver, updateFunction: mockUpdateFunction }); + }); describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { @@ -115,7 +117,81 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }); }); - test('does not call the updateResolver() API when it receives only a mapping template difference in a Pipeline Resolver', async () => { + test('calls the updateResolver() API when it receives only a mapping template difference s3 location in a Unit Resolver', async () => { + // GIVEN + mockS3GetObject = jest.fn().mockImplementation(async () => { + return { Body: 'template defined in s3' }; + }); + hotswapMockSdkProvider.stubS3({ getObject: mockS3GetObject }); + setup.setCurrentCfnStackTemplate({ + Resources: { + AppSyncResolver: { + Type: 'AWS::AppSync::Resolver', + Properties: { + ApiId: 'apiId', + FieldName: 'myField', + TypeName: 'Query', + DataSourceName: 'my-datasource', + Kind: 'UNIT', + RequestMappingTemplateS3Location: 's3://test-bucket/old_location', + ResponseMappingTemplate: '## original response template', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf( + 'AppSyncResolver', + 'AWS::AppSync::Resolver', + 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/types/Query/resolvers/myField', + ), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncResolver: { + Type: 'AWS::AppSync::Resolver', + Properties: { + ApiId: 'apiId', + FieldName: 'myField', + TypeName: 'Query', + DataSourceName: 'my-datasource', + Kind: 'UNIT', + RequestMappingTemplateS3Location: 's3://test-bucket/path/to/key', + ResponseMappingTemplate: '## original response template', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateResolver).toHaveBeenCalledWith({ + apiId: 'apiId', + dataSourceName: 'my-datasource', + typeName: 'Query', + fieldName: 'myField', + kind: 'UNIT', + requestMappingTemplate: 'template defined in s3', + responseMappingTemplate: '## original response template', + }); + expect(mockS3GetObject).toHaveBeenCalledWith({ + Bucket: 'test-bucket', + Key: 'path/to/key', + }); + }); + + test('calls the updateResolver() API when it receives only a mapping template difference in a Pipeline Resolver', async () => { // GIVEN setup.setCurrentCfnStackTemplate({ Resources: { @@ -137,6 +213,13 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }, }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf( + 'AppSyncResolver', + 'AWS::AppSync::Resolver', + 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/types/Query/resolvers/myField', + ), + ); const cdkStackArtifact = setup.cdkStackArtifactOf({ template: { Resources: { @@ -160,24 +243,20 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - if (hotswapMode === HotswapMode.FALL_BACK) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateFunction).not.toHaveBeenCalled(); - expect(mockUpdateResolver).not.toHaveBeenCalled(); - } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(deployStackResult?.noOp).toEqual(true); - expect(mockUpdateFunction).not.toHaveBeenCalled(); - expect(mockUpdateResolver).not.toHaveBeenCalled(); - } + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateResolver).toHaveBeenCalledWith({ + apiId: 'apiId', + dataSourceName: 'my-datasource', + typeName: 'Query', + fieldName: 'myField', + kind: 'PIPELINE', + pipelineConfig: ['function1'], + requestMappingTemplate: '## new request template', + responseMappingTemplate: '## original response template', + }); }); test(`when it receives a change that is not a mapping template difference in a Resolver, it does not call the updateResolver() API in CLASSIC mode @@ -360,6 +439,74 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }); }); + test('calls the updateFunction() API when it receives only a mapping template s3 location difference in a Function', async () => { + // GIVEN + mockS3GetObject = jest.fn().mockImplementation(async () => { + return { Body: 'template defined in s3' }; + }); + hotswapMockSdkProvider.stubS3({ getObject: mockS3GetObject }); + const mockListFunctions = jest.fn().mockReturnValue({ functions: [{ name: 'my-function', functionId: 'functionId' }] }); + hotswapMockSdkProvider.stubAppSync({ listFunctions: mockListFunctions, updateFunction: mockUpdateFunction }); + + setup.setCurrentCfnStackTemplate({ + Resources: { + AppSyncFunction: { + Type: 'AWS::AppSync::FunctionConfiguration', + Properties: { + Name: 'my-function', + ApiId: 'apiId', + DataSourceName: 'my-datasource', + FunctionVersion: '2018-05-29', + RequestMappingTemplate: '## original request template', + ResponseMappingTemplateS3Location: 's3://test-bucket/old_location', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncFunction: { + Type: 'AWS::AppSync::FunctionConfiguration', + Properties: { + Name: 'my-function', + ApiId: 'apiId', + DataSourceName: 'my-datasource', + FunctionVersion: '2018-05-29', + RequestMappingTemplate: '## original request template', + ResponseMappingTemplateS3Location: 's3://test-bucket/path/to/key', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateFunction).toHaveBeenCalledWith({ + apiId: 'apiId', + dataSourceName: 'my-datasource', + functionId: 'functionId', + functionVersion: '2018-05-29', + name: 'my-function', + requestMappingTemplate: '## original request template', + responseMappingTemplate: 'template defined in s3', + }); + expect(mockS3GetObject).toHaveBeenCalledWith({ + Bucket: 'test-bucket', + Key: 'path/to/key', + }); + }); + test(`when it receives a change that is not a mapping template difference in a Function, it does not call the updateFunction() API in CLASSIC mode but does in HOTSWAP_ONLY mode`, async () => { diff --git a/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts b/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts index 3483ae67118dc..63bded36370f1 100644 --- a/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts +++ b/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts @@ -172,6 +172,10 @@ export class HotswapMockSdkProvider { this.mockSdkProvider.stubGetEndpointSuffix(stub); } + public stubS3(stubs: SyncHandlerSubsetOf) { + this.mockSdkProvider.stubS3(stubs); + } + public tryHotswapDeployment( hotswapMode: HotswapMode, stackArtifact: cxapi.CloudFormationStackArtifact, From bd845dd2b9bb673af51df935ce049e2e5808eb40 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Thu, 14 Sep 2023 14:50:29 +0200 Subject: [PATCH 5/9] update tests for appsync graphql schema --- ...ping-templates-hotswap-deployments.test.ts | 354 ++++++++++++++++-- 1 file changed, 313 insertions(+), 41 deletions(-) diff --git a/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts index 04b530a6e79e8..5264b2662751f 100644 --- a/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts @@ -6,13 +6,19 @@ import { HotswapMode } from '../../../lib/api/hotswap/common'; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; let mockUpdateResolver: (params: AppSync.UpdateResolverRequest) => AppSync.UpdateResolverResponse; let mockUpdateFunction: (params: AppSync.UpdateFunctionRequest) => AppSync.UpdateFunctionResponse; +let mockStartSchemaCreation: (params: AppSync.StartSchemaCreationRequest) => AppSync.StartSchemaCreationResponse; let mockS3GetObject: (params: S3.GetObjectRequest) => S3.GetObjectOutput; beforeEach(() => { hotswapMockSdkProvider = setup.setupHotswapTests(); mockUpdateResolver = jest.fn(); mockUpdateFunction = jest.fn(); - hotswapMockSdkProvider.stubAppSync({ updateResolver: mockUpdateResolver, updateFunction: mockUpdateFunction }); + mockStartSchemaCreation = jest.fn(); + hotswapMockSdkProvider.stubAppSync({ + updateResolver: mockUpdateResolver, + updateFunction: mockUpdateFunction, + startSchemaCreation: mockStartSchemaCreation, + }); }); @@ -31,19 +37,15 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - if (hotswapMode === HotswapMode.FALL_BACK) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN + // THEN + if (hotswapMode === HotswapMode.FALL_BACK) { expect(deployStackResult).toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN expect(deployStackResult).not.toBeUndefined(); expect(deployStackResult?.noOp).toEqual(true); expect(mockUpdateFunction).not.toHaveBeenCalled(); @@ -304,19 +306,15 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - if (hotswapMode === HotswapMode.FALL_BACK) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN + // THEN + if (hotswapMode === HotswapMode.FALL_BACK) { expect(deployStackResult).toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).toHaveBeenCalledWith({ @@ -359,19 +357,15 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - if (hotswapMode === HotswapMode.FALL_BACK) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN + // THEN + if (hotswapMode === HotswapMode.FALL_BACK) { expect(deployStackResult).toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN expect(deployStackResult).not.toBeUndefined(); expect(deployStackResult?.noOp).toEqual(true); expect(mockUpdateFunction).not.toHaveBeenCalled(); @@ -548,19 +542,15 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - if (hotswapMode === HotswapMode.FALL_BACK) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN + // THEN + if (hotswapMode === HotswapMode.FALL_BACK) { expect(deployStackResult).toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateFunction).toHaveBeenCalledWith({ apiId: 'apiId', @@ -606,23 +596,305 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - if (hotswapMode === HotswapMode.FALL_BACK) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN + // THEN + if (hotswapMode === HotswapMode.FALL_BACK) { expect(deployStackResult).toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN expect(deployStackResult).not.toBeUndefined(); expect(deployStackResult?.noOp).toEqual(true); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } }); + + test('calls the startSchemaCreation() API when it receives only a definition difference in a graphql schema', async () => { + // GIVEN + mockStartSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'SUCCESS' }); + hotswapMockSdkProvider.stubAppSync({ startSchemaCreation: mockStartSchemaCreation }); + + setup.setCurrentCfnStackTemplate({ + Resources: { + AppSyncGraphQLSchema: { + Type: 'AWS::AppSync::GraphQLSchema', + Properties: { + ApiId: 'apiId', + Definition: 'original graphqlSchema', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf( + 'AppSyncGraphQLSchema', + 'AWS::AppSync::GraphQLSchema', + 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/schema/my-schema', + ), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncGraphQLSchema: { + Type: 'AWS::AppSync::GraphQLSchema', + Properties: { + ApiId: 'apiId', + Definition: 'new graphqlSchema', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockStartSchemaCreation).toHaveBeenCalledWith({ + apiId: 'apiId', + definition: 'new graphqlSchema', + }); + }); + + test('calls the startSchemaCreation() API when it receives only a definition s3 location difference in a graphql schema', async () => { + // GIVEN + mockS3GetObject = jest.fn().mockImplementation(async () => { + return { Body: 'schema defined in s3' }; + }); + hotswapMockSdkProvider.stubS3({ getObject: mockS3GetObject }); + mockStartSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'SUCCESS' }); + hotswapMockSdkProvider.stubAppSync({ startSchemaCreation: mockStartSchemaCreation }); + + setup.setCurrentCfnStackTemplate({ + Resources: { + AppSyncGraphQLSchema: { + Type: 'AWS::AppSync::GraphQLSchema', + Properties: { + ApiId: 'apiId', + DefinitionS3Location: 's3://test-bucket/old_location', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf( + 'AppSyncGraphQLSchema', + 'AWS::AppSync::GraphQLSchema', + 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/schema/my-schema', + ), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncGraphQLSchema: { + Type: 'AWS::AppSync::GraphQLSchema', + Properties: { + ApiId: 'apiId', + DefinitionS3Location: 's3://test-bucket/path/to/key', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockStartSchemaCreation).toHaveBeenCalledWith({ + apiId: 'apiId', + definition: 'schema defined in s3', + }); + + expect(mockS3GetObject).toHaveBeenCalledWith({ + Bucket: 'test-bucket', + Key: 'path/to/key', + }); + }); + + test('does not call startSchemaCreation() API when a resource with type that is not AWS::AppSync::GraphQLSchema but has the same properties is change', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Resources: { + AppSyncGraphQLSchema: { + Type: 'AWS::AppSync::NotGraphQLSchema', + Properties: { + ApiId: 'apiId', + Definition: 'original graphqlSchema', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf( + 'AppSyncGraphQLSchema', + 'AWS::AppSync::GraphQLSchema', + 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/schema/my-schema', + ), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncGraphQLSchema: { + Type: 'AWS::AppSync::NotGraphQLSchema', + Properties: { + ApiId: 'apiId', + Definition: 'new graphqlSchema', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + if (hotswapMode === HotswapMode.FALL_BACK) { + expect(deployStackResult).toBeUndefined(); + expect(mockStartSchemaCreation).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockStartSchemaCreation).not.toHaveBeenCalled(); + } + }); + + test('calls the startSchemaCreation() and waits for schema creation to stabilize before finishing', async () => { + // GIVEN + mockStartSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'PROCESSING' }); + const mockGetSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'SUCCESS' }); + hotswapMockSdkProvider.stubAppSync({ startSchemaCreation: mockStartSchemaCreation, getSchemaCreationStatus: mockGetSchemaCreation }); + + setup.setCurrentCfnStackTemplate({ + Resources: { + AppSyncGraphQLSchema: { + Type: 'AWS::AppSync::GraphQLSchema', + Properties: { + ApiId: 'apiId', + Definition: 'original graphqlSchema', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf( + 'AppSyncGraphQLSchema', + 'AWS::AppSync::GraphQLSchema', + 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/schema/my-schema', + ), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncGraphQLSchema: { + Type: 'AWS::AppSync::GraphQLSchema', + Properties: { + ApiId: 'apiId', + Definition: 'new graphqlSchema', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockStartSchemaCreation).toHaveBeenCalledWith({ + apiId: 'apiId', + definition: 'new graphqlSchema', + }); + expect(mockGetSchemaCreation).toHaveBeenCalledWith({ + apiId: 'apiId', + }); + }); + + test('calls the startSchemaCreation() and throws if schema creation fails', async () => { + // GIVEN + mockStartSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'PROCESSING' }); + const mockGetSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'FAILED', details: 'invalid schema' }); + hotswapMockSdkProvider.stubAppSync({ startSchemaCreation: mockStartSchemaCreation, getSchemaCreationStatus: mockGetSchemaCreation }); + + setup.setCurrentCfnStackTemplate({ + Resources: { + AppSyncGraphQLSchema: { + Type: 'AWS::AppSync::GraphQLSchema', + Properties: { + ApiId: 'apiId', + Definition: 'original graphqlSchema', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf( + 'AppSyncGraphQLSchema', + 'AWS::AppSync::GraphQLSchema', + 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/schema/my-schema', + ), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncGraphQLSchema: { + Type: 'AWS::AppSync::GraphQLSchema', + Properties: { + ApiId: 'apiId', + Definition: 'new graphqlSchema', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + // WHEN + await expect(() => hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact)).rejects.toThrow('invalid schema'); + + // THEN + expect(mockStartSchemaCreation).toHaveBeenCalledWith({ + apiId: 'apiId', + definition: 'new graphqlSchema', + }); + expect(mockGetSchemaCreation).toHaveBeenCalledWith({ + apiId: 'apiId', + }); + }); }); From dc6547ef63816ab809c697ce6cbcd8fd15140cf4 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Fri, 15 Sep 2023 17:25:37 +0200 Subject: [PATCH 6/9] update tests for nested stacks hotswap updates --- .../api/hotswap/nested-stacks-hotswap.test.ts | 210 ++++++++++++++++++ ...-sibling-stack-output.nested.template.json | 30 +++ .../one-output-stack.nested.template.json | 5 + 3 files changed, 245 insertions(+) create mode 100644 packages/aws-cdk/test/nested-stack-templates/one-lambda-stack-with-dependency-on-sibling-stack-output.nested.template.json create mode 100644 packages/aws-cdk/test/nested-stack-templates/one-output-stack.nested.template.json diff --git a/packages/aws-cdk/test/api/hotswap/nested-stacks-hotswap.test.ts b/packages/aws-cdk/test/api/hotswap/nested-stacks-hotswap.test.ts index 747a34fd12c8a..77d159074c8da 100644 --- a/packages/aws-cdk/test/api/hotswap/nested-stacks-hotswap.test.ts +++ b/packages/aws-cdk/test/api/hotswap/nested-stacks-hotswap.test.ts @@ -826,6 +826,216 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }); }); + test('can hotswap a lambda function in a 1-level nested stack with dependency on a output of sibling stack', async () => { + // GIVEN: RootStack has two child stacks `NestedLambdaStack` and `NestedSiblingStack`. `NestedLambdaStack` + // takes two parameters s3Key and s3Bucket and use them for a Lambda function. + // RootStack resolves s3Bucket from a root template parameter and s3Key through output of `NestedSiblingStack` + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('RootStack'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const rootStack = testStack({ + stackName: 'RootStack', + template: { + Resources: { + NestedLambdaStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + Parameters: { + referenceToS3BucketParam: { + Ref: 'S3BucketParam', + }, + referenceToS3StackKeyOutput: { + 'Fn::GetAtt': [ + 'NestedSiblingStack', + 'Outputs.NestedOutput', + ], + }, + }, + }, + Metadata: { + 'aws:asset:path': 'one-lambda-stack-with-dependency-on-sibling-stack-output.nested.template.json', + }, + }, + NestedSiblingStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-output-stack.nested.template.json', + }, + }, + Parameters: { + S3BucketParam: { + Type: 'String', + Description: 'S3 bucket for asset', + }, + }, + }, + }, + }); + + const nestedLambdaStack = testStack({ + stackName: 'NestedLambdaStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + }, + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }); + + const nestedSiblingStack = testStack({ + stackName: 'NestedSiblingStack', + template: { + Outputs: { + NestedOutput: { Value: 's3-key-value-from-output' }, + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }); + + setup.addTemplateToCloudFormationLookupMock(rootStack); + setup.addTemplateToCloudFormationLookupMock(nestedLambdaStack); + setup.addTemplateToCloudFormationLookupMock(nestedSiblingStack); + + setup.pushNestedStackResourceSummaries('RootStack', + setup.stackSummaryOf('NestedLambdaStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedLambdaStack/abcd', + ), + setup.stackSummaryOf('NestedSiblingStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedSiblingStack/abcd', + ), + ); + setup.pushNestedStackResourceSummaries('NestedLambdaStack', + setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'nested-lambda-function'), + ); + setup.pushNestedStackResourceSummaries('NestedSiblingStack'); + + const cdkStackArtifact = testStack({ stackName: 'RootStack', template: rootStack.template }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact, { + S3BucketParam: 'new-bucket', + }); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'new-bucket', + S3Key: 's3-key-value-from-output', + }); + }); + + test('can hotswap a lambda function in a 1-level nested stack and read default parameters value if not provided', async () => { + // GIVEN: RootStack has one child stack `NestedStack`. `NestedStack` takes two + // parameters s3Key and s3Bucket and use them for a Lambda function. + // RootStack resolves both parameters from root template parameters. Current/old change + // has hardcoded resolved values and the new change doesn't provide parameters through + // root stack forcing the evaluation of default parameter values. + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const rootStack = testStack({ + stackName: 'LambdaRoot', + template: { + Resources: { + NestedStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + Parameters: { + referencetoS3BucketParam: { + Ref: 'S3BucketParam', + }, + referencetoS3KeyParam: { + Ref: 'S3KeyParam', + }, + }, + }, + Metadata: { + 'aws:asset:path': 'one-lambda-stack-with-asset-parameters.nested.template.json', + }, + }, + }, + Parameters: { + S3BucketParam: { + Type: 'String', + Description: 'S3 bucket for asset', + Default: 'default-s3-bucket', + }, + S3KeyParam: { + Type: 'String', + Description: 'S3 bucket for asset', + Default: 'default-s3-key', + }, + }, + }, + }); + + setup.addTemplateToCloudFormationLookupMock(rootStack); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'NestedStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }, + })); + + setup.pushNestedStackResourceSummaries('LambdaRoot', + setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', + ), + ); + + const cdkStackArtifact = testStack({ stackName: 'LambdaRoot', template: rootStack.template }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'default-s3-bucket', + S3Key: 'default-s3-key', + }); + }); + test('can hotswap a lambda function in a 2-level nested stack with asset parameters', async () => { // GIVEN hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); diff --git a/packages/aws-cdk/test/nested-stack-templates/one-lambda-stack-with-dependency-on-sibling-stack-output.nested.template.json b/packages/aws-cdk/test/nested-stack-templates/one-lambda-stack-with-dependency-on-sibling-stack-output.nested.template.json new file mode 100644 index 0000000000000..68f18403ab37d --- /dev/null +++ b/packages/aws-cdk/test/nested-stack-templates/one-lambda-stack-with-dependency-on-sibling-stack-output.nested.template.json @@ -0,0 +1,30 @@ +{ + "Type": "AWS::CloudFormation::Stack", + "Resources": { + "Func": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "referenceToS3BucketParam" + }, + "S3Key": { + "Ref": "referenceToS3StackKeyOutput" + } + }, + "FunctionName": "my-function" + }, + "Metadata": { + "aws:asset:path": "old-path" + } + } + }, + "Parameters": { + "referenceToS3BucketParam": { + "Type": "String" + }, + "referenceToS3StackKeyOutput": { + "Type": "String" + } + } +} diff --git a/packages/aws-cdk/test/nested-stack-templates/one-output-stack.nested.template.json b/packages/aws-cdk/test/nested-stack-templates/one-output-stack.nested.template.json new file mode 100644 index 0000000000000..345987597d6cb --- /dev/null +++ b/packages/aws-cdk/test/nested-stack-templates/one-output-stack.nested.template.json @@ -0,0 +1,5 @@ +{ + "Outputs": { + "NestedOutput": { "Value": "s3-key-value-from-output" } + } +} From 208a36bd1df491f75a1ca89e30a400622fbd8aa1 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Tue, 19 Sep 2023 16:01:36 +0200 Subject: [PATCH 7/9] revert appsync changes --- .../api/evaluate-cloudformation-template.ts | 13 - .../api/hotswap/appsync-mapping-templates.ts | 86 +-- ...ping-templates-hotswap-deployments.test.ts | 539 ++---------------- 3 files changed, 77 insertions(+), 561 deletions(-) diff --git a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts index aa43b5f93f972..03daa7b7b8daf 100644 --- a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts +++ b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts @@ -413,9 +413,6 @@ const RESOURCE_TYPE_ATTRIBUTES_FORMATS: { [type: string]: { [attribute: string]: }, 'AWS::DynamoDB::Table': { Arn: stdSlashResourceArnFmt }, 'AWS::AppSync::GraphQLApi': { ApiId: appsyncGraphQlApiApiIdFmt }, - 'AWS::AppSync::FunctionConfiguration': { FunctionId: appsyncGraphQlFunctionIDFmt }, - 'AWS::AppSync::DataSource': { Name: appsyncGraphQlDataSourceNameFmt }, - }; function iamArnFmt(parts: ArnParts): string { @@ -443,16 +440,6 @@ function appsyncGraphQlApiApiIdFmt(parts: ArnParts): string { return parts.resourceName.split('/')[1]; } -function appsyncGraphQlFunctionIDFmt(parts: ArnParts): string { - // arn:aws:appsync:us-east-1:111111111111:apis//functions/ - return parts.resourceName.split('/')[3]; -} - -function appsyncGraphQlDataSourceNameFmt(parts: ArnParts): string { - // arn:aws:appsync:us-east-1:111111111111:apis//datasources/ - return parts.resourceName.split('/')[3]; -} - interface Intrinsic { readonly name: string; readonly args: any; diff --git a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts index 908cbec6b1dbe..2ed050c1ed406 100644 --- a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts +++ b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts @@ -1,7 +1,5 @@ -import { GetSchemaCreationStatusRequest, GetSchemaCreationStatusResponse } from 'aws-sdk/clients/appsync'; -import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys } from './common'; +import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, lowerCaseFirstCharacter, reportNonHotswappableChange, transformObjectKeys } from './common'; import { ISDK } from '../aws-auth'; - import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; export async function isHotswappableAppSyncChange( @@ -9,22 +7,23 @@ export async function isHotswappableAppSyncChange( ): Promise { const isResolver = change.newValue.Type === 'AWS::AppSync::Resolver'; const isFunction = change.newValue.Type === 'AWS::AppSync::FunctionConfiguration'; - const isGraphQLSchema = change.newValue.Type === 'AWS::AppSync::GraphQLSchema'; - if (!isResolver && !isFunction && !isGraphQLSchema) { + if (!isResolver && !isFunction) { return []; } const ret: ChangeHotswapResult = []; + if (isResolver && change.newValue.Properties?.Kind === 'PIPELINE') { + reportNonHotswappableChange( + ret, + change, + undefined, + 'Pipeline resolvers cannot be hotswapped since they reference the FunctionId of the underlying functions, which cannot be resolved', + ); + return ret; + } - const classifiedChanges = classifyChanges(change, [ - 'RequestMappingTemplate', - 'RequestMappingTemplateS3Location', - 'ResponseMappingTemplate', - 'ResponseMappingTemplateS3Location', - 'Definition', - 'DefinitionS3Location', - ]); + const classifiedChanges = classifyChanges(change, ['RequestMappingTemplate', 'ResponseMappingTemplate']); classifiedChanges.reportNonHotswappablePropertyChanges(ret); const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps); @@ -50,52 +49,21 @@ export async function isHotswappableAppSyncChange( const sdkProperties: { [name: string]: any } = { ...change.oldValue.Properties, - Definition: change.newValue.Properties?.Definition, - DefinitionS3Location: change.newValue.Properties?.DefinitionS3Location, requestMappingTemplate: change.newValue.Properties?.RequestMappingTemplate, - requestMappingTemplateS3Location: change.newValue.Properties?.RequestMappingTemplateS3Location, responseMappingTemplate: change.newValue.Properties?.ResponseMappingTemplate, - responseMappingTemplateS3Location: change.newValue.Properties?.ResponseMappingTemplateS3Location, }; const evaluatedResourceProperties = await evaluateCfnTemplate.evaluateCfnExpression(sdkProperties); const sdkRequestObject = transformObjectKeys(evaluatedResourceProperties, lowerCaseFirstCharacter); - // resolve s3 location files as SDK doesn't take in s3 location but inline code - if (sdkRequestObject.requestMappingTemplateS3Location) { - sdkRequestObject.requestMappingTemplate = (await fetchFileFromS3(sdkRequestObject.requestMappingTemplateS3Location, sdk))?.toString('utf8'); - delete sdkRequestObject.requestMappingTemplateS3Location; - } - if (sdkRequestObject.responseMappingTemplateS3Location) { - sdkRequestObject.responseMappingTemplate = (await fetchFileFromS3(sdkRequestObject.responseMappingTemplateS3Location, sdk))?.toString('utf8'); - delete sdkRequestObject.responseMappingTemplateS3Location; - } - if (sdkRequestObject.definitionS3Location) { - sdkRequestObject.definition = await fetchFileFromS3(sdkRequestObject.definitionS3Location, sdk); - delete sdkRequestObject.definitionS3Location; - } - if (isResolver) { await sdk.appsync().updateResolver(sdkRequestObject).promise(); - } else if (isFunction) { - + } else { const { functions } = await sdk.appsync().listFunctions({ apiId: sdkRequestObject.apiId }).promise(); const { functionId } = functions?.find(fn => fn.name === physicalName) ?? {}; - await simpleRetry( - () => sdk.appsync().updateFunction({ ...sdkRequestObject, functionId: functionId! }).promise(), - 3, - 'ConcurrentModificationException'); - } else { - let schemaCreationResponse: GetSchemaCreationStatusResponse = await sdk.appsync().startSchemaCreation(sdkRequestObject).promise(); - while (schemaCreationResponse.status && ['PROCESSING', 'DELETING'].some(status => status === schemaCreationResponse.status)) { - await new Promise(resolve => setTimeout(resolve, 1000)); // poll every second - const getSchemaCreationStatusRequest: GetSchemaCreationStatusRequest = { - apiId: sdkRequestObject.apiId, - }; - schemaCreationResponse = await sdk.appsync().getSchemaCreationStatus(getSchemaCreationStatusRequest).promise(); - } - if (schemaCreationResponse.status === 'FAILED') { - throw new Error(schemaCreationResponse.details); - } + await sdk.appsync().updateFunction({ + ...sdkRequestObject, + functionId: functionId!, + }).promise(); } }, }); @@ -103,23 +71,3 @@ export async function isHotswappableAppSyncChange( return ret; } - -async function fetchFileFromS3(s3Url: string, sdk: ISDK) { - const s3PathParts = s3Url.split('/'); - const s3Bucket = s3PathParts[2]; // first two are "s3:" and "" due to s3:// - const s3Key = s3PathParts.splice(3).join('/'); // after removing first three we reconstruct the key - return (await sdk.s3().getObject({ Bucket: s3Bucket, Key: s3Key }).promise()).Body; -} - -async function simpleRetry(fn: () => Promise, numOfRetries: number, errorCodeToRetry: string) { - try { - await fn(); - } catch (error: any) { - if (error && error.code === errorCodeToRetry && numOfRetries > 0) { - await new Promise((resolve) => setTimeout(resolve, 500)); // wait half a second - await simpleRetry(fn, numOfRetries - 1, errorCodeToRetry); - } else { - throw error; - } - } -} diff --git a/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts index 5264b2662751f..f7780b873806b 100644 --- a/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts @@ -1,25 +1,17 @@ /* eslint-disable import/order */ -import { AppSync, S3 } from 'aws-sdk'; +import { AppSync } from 'aws-sdk'; import * as setup from './hotswap-test-setup'; import { HotswapMode } from '../../../lib/api/hotswap/common'; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; let mockUpdateResolver: (params: AppSync.UpdateResolverRequest) => AppSync.UpdateResolverResponse; let mockUpdateFunction: (params: AppSync.UpdateFunctionRequest) => AppSync.UpdateFunctionResponse; -let mockStartSchemaCreation: (params: AppSync.StartSchemaCreationRequest) => AppSync.StartSchemaCreationResponse; -let mockS3GetObject: (params: S3.GetObjectRequest) => S3.GetObjectOutput; beforeEach(() => { hotswapMockSdkProvider = setup.setupHotswapTests(); mockUpdateResolver = jest.fn(); mockUpdateFunction = jest.fn(); - mockStartSchemaCreation = jest.fn(); - hotswapMockSdkProvider.stubAppSync({ - updateResolver: mockUpdateResolver, - updateFunction: mockUpdateFunction, - startSchemaCreation: mockStartSchemaCreation, - }); - + hotswapMockSdkProvider.stubAppSync({ updateResolver: mockUpdateResolver, updateFunction: mockUpdateFunction }); }); describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { @@ -37,15 +29,19 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN expect(deployStackResult).toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN expect(deployStackResult).not.toBeUndefined(); expect(deployStackResult?.noOp).toEqual(true); expect(mockUpdateFunction).not.toHaveBeenCalled(); @@ -119,81 +115,7 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }); }); - test('calls the updateResolver() API when it receives only a mapping template difference s3 location in a Unit Resolver', async () => { - // GIVEN - mockS3GetObject = jest.fn().mockImplementation(async () => { - return { Body: 'template defined in s3' }; - }); - hotswapMockSdkProvider.stubS3({ getObject: mockS3GetObject }); - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncResolver: { - Type: 'AWS::AppSync::Resolver', - Properties: { - ApiId: 'apiId', - FieldName: 'myField', - TypeName: 'Query', - DataSourceName: 'my-datasource', - Kind: 'UNIT', - RequestMappingTemplateS3Location: 's3://test-bucket/old_location', - ResponseMappingTemplate: '## original response template', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, - }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf( - 'AppSyncResolver', - 'AWS::AppSync::Resolver', - 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/types/Query/resolvers/myField', - ), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - AppSyncResolver: { - Type: 'AWS::AppSync::Resolver', - Properties: { - ApiId: 'apiId', - FieldName: 'myField', - TypeName: 'Query', - DataSourceName: 'my-datasource', - Kind: 'UNIT', - RequestMappingTemplateS3Location: 's3://test-bucket/path/to/key', - ResponseMappingTemplate: '## original response template', - }, - Metadata: { - 'aws:asset:path': 'new-path', - }, - }, - }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateResolver).toHaveBeenCalledWith({ - apiId: 'apiId', - dataSourceName: 'my-datasource', - typeName: 'Query', - fieldName: 'myField', - kind: 'UNIT', - requestMappingTemplate: 'template defined in s3', - responseMappingTemplate: '## original response template', - }); - expect(mockS3GetObject).toHaveBeenCalledWith({ - Bucket: 'test-bucket', - Key: 'path/to/key', - }); - }); - - test('calls the updateResolver() API when it receives only a mapping template difference in a Pipeline Resolver', async () => { + test('does not call the updateResolver() API when it receives only a mapping template difference in a Pipeline Resolver', async () => { // GIVEN setup.setCurrentCfnStackTemplate({ Resources: { @@ -215,13 +137,6 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }, }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf( - 'AppSyncResolver', - 'AWS::AppSync::Resolver', - 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/types/Query/resolvers/myField', - ), - ); const cdkStackArtifact = setup.cdkStackArtifactOf({ template: { Resources: { @@ -245,20 +160,24 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateResolver).toHaveBeenCalledWith({ - apiId: 'apiId', - dataSourceName: 'my-datasource', - typeName: 'Query', - fieldName: 'myField', - kind: 'PIPELINE', - pipelineConfig: ['function1'], - requestMappingTemplate: '## new request template', - responseMappingTemplate: '## original response template', - }); + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } }); test(`when it receives a change that is not a mapping template difference in a Resolver, it does not call the updateResolver() API in CLASSIC mode @@ -306,15 +225,19 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN expect(deployStackResult).toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).toHaveBeenCalledWith({ @@ -357,15 +280,19 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN expect(deployStackResult).toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN expect(deployStackResult).not.toBeUndefined(); expect(deployStackResult?.noOp).toEqual(true); expect(mockUpdateFunction).not.toHaveBeenCalled(); @@ -433,74 +360,6 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }); }); - test('calls the updateFunction() API when it receives only a mapping template s3 location difference in a Function', async () => { - // GIVEN - mockS3GetObject = jest.fn().mockImplementation(async () => { - return { Body: 'template defined in s3' }; - }); - hotswapMockSdkProvider.stubS3({ getObject: mockS3GetObject }); - const mockListFunctions = jest.fn().mockReturnValue({ functions: [{ name: 'my-function', functionId: 'functionId' }] }); - hotswapMockSdkProvider.stubAppSync({ listFunctions: mockListFunctions, updateFunction: mockUpdateFunction }); - - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncFunction: { - Type: 'AWS::AppSync::FunctionConfiguration', - Properties: { - Name: 'my-function', - ApiId: 'apiId', - DataSourceName: 'my-datasource', - FunctionVersion: '2018-05-29', - RequestMappingTemplate: '## original request template', - ResponseMappingTemplateS3Location: 's3://test-bucket/old_location', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - AppSyncFunction: { - Type: 'AWS::AppSync::FunctionConfiguration', - Properties: { - Name: 'my-function', - ApiId: 'apiId', - DataSourceName: 'my-datasource', - FunctionVersion: '2018-05-29', - RequestMappingTemplate: '## original request template', - ResponseMappingTemplateS3Location: 's3://test-bucket/path/to/key', - }, - Metadata: { - 'aws:asset:path': 'new-path', - }, - }, - }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateFunction).toHaveBeenCalledWith({ - apiId: 'apiId', - dataSourceName: 'my-datasource', - functionId: 'functionId', - functionVersion: '2018-05-29', - name: 'my-function', - requestMappingTemplate: '## original request template', - responseMappingTemplate: 'template defined in s3', - }); - expect(mockS3GetObject).toHaveBeenCalledWith({ - Bucket: 'test-bucket', - Key: 'path/to/key', - }); - }); - test(`when it receives a change that is not a mapping template difference in a Function, it does not call the updateFunction() API in CLASSIC mode but does in HOTSWAP_ONLY mode`, async () => { @@ -542,15 +401,19 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN expect(deployStackResult).toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateFunction).toHaveBeenCalledWith({ apiId: 'apiId', @@ -596,305 +459,23 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot }, }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN expect(deployStackResult).toBeUndefined(); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN expect(deployStackResult).not.toBeUndefined(); expect(deployStackResult?.noOp).toEqual(true); expect(mockUpdateFunction).not.toHaveBeenCalled(); expect(mockUpdateResolver).not.toHaveBeenCalled(); } }); - - test('calls the startSchemaCreation() API when it receives only a definition difference in a graphql schema', async () => { - // GIVEN - mockStartSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'SUCCESS' }); - hotswapMockSdkProvider.stubAppSync({ startSchemaCreation: mockStartSchemaCreation }); - - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncGraphQLSchema: { - Type: 'AWS::AppSync::GraphQLSchema', - Properties: { - ApiId: 'apiId', - Definition: 'original graphqlSchema', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, - }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf( - 'AppSyncGraphQLSchema', - 'AWS::AppSync::GraphQLSchema', - 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/schema/my-schema', - ), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - AppSyncGraphQLSchema: { - Type: 'AWS::AppSync::GraphQLSchema', - Properties: { - ApiId: 'apiId', - Definition: 'new graphqlSchema', - }, - Metadata: { - 'aws:asset:path': 'new-path', - }, - }, - }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockStartSchemaCreation).toHaveBeenCalledWith({ - apiId: 'apiId', - definition: 'new graphqlSchema', - }); - }); - - test('calls the startSchemaCreation() API when it receives only a definition s3 location difference in a graphql schema', async () => { - // GIVEN - mockS3GetObject = jest.fn().mockImplementation(async () => { - return { Body: 'schema defined in s3' }; - }); - hotswapMockSdkProvider.stubS3({ getObject: mockS3GetObject }); - mockStartSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'SUCCESS' }); - hotswapMockSdkProvider.stubAppSync({ startSchemaCreation: mockStartSchemaCreation }); - - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncGraphQLSchema: { - Type: 'AWS::AppSync::GraphQLSchema', - Properties: { - ApiId: 'apiId', - DefinitionS3Location: 's3://test-bucket/old_location', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, - }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf( - 'AppSyncGraphQLSchema', - 'AWS::AppSync::GraphQLSchema', - 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/schema/my-schema', - ), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - AppSyncGraphQLSchema: { - Type: 'AWS::AppSync::GraphQLSchema', - Properties: { - ApiId: 'apiId', - DefinitionS3Location: 's3://test-bucket/path/to/key', - }, - Metadata: { - 'aws:asset:path': 'new-path', - }, - }, - }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockStartSchemaCreation).toHaveBeenCalledWith({ - apiId: 'apiId', - definition: 'schema defined in s3', - }); - - expect(mockS3GetObject).toHaveBeenCalledWith({ - Bucket: 'test-bucket', - Key: 'path/to/key', - }); - }); - - test('does not call startSchemaCreation() API when a resource with type that is not AWS::AppSync::GraphQLSchema but has the same properties is change', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncGraphQLSchema: { - Type: 'AWS::AppSync::NotGraphQLSchema', - Properties: { - ApiId: 'apiId', - Definition: 'original graphqlSchema', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, - }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf( - 'AppSyncGraphQLSchema', - 'AWS::AppSync::GraphQLSchema', - 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/schema/my-schema', - ), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - AppSyncGraphQLSchema: { - Type: 'AWS::AppSync::NotGraphQLSchema', - Properties: { - ApiId: 'apiId', - Definition: 'new graphqlSchema', - }, - Metadata: { - 'aws:asset:path': 'new-path', - }, - }, - }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN - if (hotswapMode === HotswapMode.FALL_BACK) { - expect(deployStackResult).toBeUndefined(); - expect(mockStartSchemaCreation).not.toHaveBeenCalled(); - } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { - expect(deployStackResult).not.toBeUndefined(); - expect(deployStackResult?.noOp).toEqual(true); - expect(mockStartSchemaCreation).not.toHaveBeenCalled(); - } - }); - - test('calls the startSchemaCreation() and waits for schema creation to stabilize before finishing', async () => { - // GIVEN - mockStartSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'PROCESSING' }); - const mockGetSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'SUCCESS' }); - hotswapMockSdkProvider.stubAppSync({ startSchemaCreation: mockStartSchemaCreation, getSchemaCreationStatus: mockGetSchemaCreation }); - - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncGraphQLSchema: { - Type: 'AWS::AppSync::GraphQLSchema', - Properties: { - ApiId: 'apiId', - Definition: 'original graphqlSchema', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, - }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf( - 'AppSyncGraphQLSchema', - 'AWS::AppSync::GraphQLSchema', - 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/schema/my-schema', - ), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - AppSyncGraphQLSchema: { - Type: 'AWS::AppSync::GraphQLSchema', - Properties: { - ApiId: 'apiId', - Definition: 'new graphqlSchema', - }, - Metadata: { - 'aws:asset:path': 'new-path', - }, - }, - }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockStartSchemaCreation).toHaveBeenCalledWith({ - apiId: 'apiId', - definition: 'new graphqlSchema', - }); - expect(mockGetSchemaCreation).toHaveBeenCalledWith({ - apiId: 'apiId', - }); - }); - - test('calls the startSchemaCreation() and throws if schema creation fails', async () => { - // GIVEN - mockStartSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'PROCESSING' }); - const mockGetSchemaCreation = jest.fn().mockReturnValueOnce({ status: 'FAILED', details: 'invalid schema' }); - hotswapMockSdkProvider.stubAppSync({ startSchemaCreation: mockStartSchemaCreation, getSchemaCreationStatus: mockGetSchemaCreation }); - - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncGraphQLSchema: { - Type: 'AWS::AppSync::GraphQLSchema', - Properties: { - ApiId: 'apiId', - Definition: 'original graphqlSchema', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, - }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf( - 'AppSyncGraphQLSchema', - 'AWS::AppSync::GraphQLSchema', - 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/schema/my-schema', - ), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - AppSyncGraphQLSchema: { - Type: 'AWS::AppSync::GraphQLSchema', - Properties: { - ApiId: 'apiId', - Definition: 'new graphqlSchema', - }, - Metadata: { - 'aws:asset:path': 'new-path', - }, - }, - }, - }, - }); - - // WHEN - await expect(() => hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact)).rejects.toThrow('invalid schema'); - - // THEN - expect(mockStartSchemaCreation).toHaveBeenCalledWith({ - apiId: 'apiId', - definition: 'new graphqlSchema', - }); - expect(mockGetSchemaCreation).toHaveBeenCalledWith({ - apiId: 'apiId', - }); - }); }); From 5f77d886b9386c387584550d16aabfdeb912e298 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Tue, 19 Sep 2023 16:03:10 +0200 Subject: [PATCH 8/9] revert appsync changes --- packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts b/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts index 63bded36370f1..3483ae67118dc 100644 --- a/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts +++ b/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts @@ -172,10 +172,6 @@ export class HotswapMockSdkProvider { this.mockSdkProvider.stubGetEndpointSuffix(stub); } - public stubS3(stubs: SyncHandlerSubsetOf) { - this.mockSdkProvider.stubS3(stubs); - } - public tryHotswapDeployment( hotswapMode: HotswapMode, stackArtifact: cxapi.CloudFormationStackArtifact, From 42611630c86c77a87d6b2dd609dc4e22dcc4a04c Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Tue, 19 Sep 2023 16:06:50 +0200 Subject: [PATCH 9/9] revert appsync changes --- packages/aws-cdk/lib/api/hotswap-deployments.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/aws-cdk/lib/api/hotswap-deployments.ts b/packages/aws-cdk/lib/api/hotswap-deployments.ts index 3b7737aa72e1e..5006a39963605 100644 --- a/packages/aws-cdk/lib/api/hotswap-deployments.ts +++ b/packages/aws-cdk/lib/api/hotswap-deployments.ts @@ -28,7 +28,6 @@ const RESOURCE_DETECTORS: { [key:string]: HotswapDetector } = { // AppSync 'AWS::AppSync::Resolver': isHotswappableAppSyncChange, 'AWS::AppSync::FunctionConfiguration': isHotswappableAppSyncChange, - 'AWS::AppSync::GraphQLSchema': isHotswappableAppSyncChange, 'AWS::ECS::TaskDefinition': isHotswappableEcsServiceChange, 'AWS::CodeBuild::Project': isHotswappableCodeBuildProjectChange,