Skip to content

Commit 12ff023

Browse files
authored
fix(cdk): Add AppSync:Api_Key as hot swappable and fix a bug with AppSync.function (#27559)
Add AppSync:Api_Key as hots wappable and fix the bug where AppSync.function doesn't allow setting version and runtime 1. Allow `expires` property of resource `AWS::AppSync::Api_Key` to be hot-swappable 2. Read the Api_Key_id from the physical ARN if not available from resource properties. (It's optional in CFN but mandatory in SDK) 3. UpdateFunction doesn't [allow](https://docs.aws.amazon.com/appsync/latest/APIReference/API_UpdateFunction.html) setting both `functionVersion` and `runtime` in the SDK (allowed in CFN). Update to remove one based on if `code` is provided or `mappingTemplates` 4. Fix a bug where the file returned from S3 was not being decoded from buffer. 5. Increase the timeout and number of retries for concurrent modification of AppSync.Functions. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 7121c7e commit 12ff023

File tree

3 files changed

+258
-7
lines changed

3 files changed

+258
-7
lines changed

packages/aws-cdk/lib/api/hotswap-deployments.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const RESOURCE_DETECTORS: { [key: string]: HotswapDetector } = {
2929
'AWS::AppSync::Resolver': isHotswappableAppSyncChange,
3030
'AWS::AppSync::FunctionConfiguration': isHotswappableAppSyncChange,
3131
'AWS::AppSync::GraphQLSchema': isHotswappableAppSyncChange,
32+
'AWS::AppSync::ApiKey': isHotswappableAppSyncChange,
3233

3334
'AWS::ECS::TaskDefinition': isHotswappableEcsServiceChange,
3435
'AWS::CodeBuild::Project': isHotswappableCodeBuildProjectChange,

packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts

+27-7
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export async function isHotswappableAppSyncChange(
1010
const isResolver = change.newValue.Type === 'AWS::AppSync::Resolver';
1111
const isFunction = change.newValue.Type === 'AWS::AppSync::FunctionConfiguration';
1212
const isGraphQLSchema = change.newValue.Type === 'AWS::AppSync::GraphQLSchema';
13-
14-
if (!isResolver && !isFunction && !isGraphQLSchema) {
13+
const isAPIKey = change.newValue.Type === 'AWS::AppSync::ApiKey';
14+
if (!isResolver && !isFunction && !isGraphQLSchema && !isAPIKey) {
1515
return [];
1616
}
1717

@@ -26,6 +26,7 @@ export async function isHotswappableAppSyncChange(
2626
'CodeS3Location',
2727
'Definition',
2828
'DefinitionS3Location',
29+
'Expires',
2930
]);
3031
classifiedChanges.reportNonHotswappablePropertyChanges(ret);
3132

@@ -60,6 +61,7 @@ export async function isHotswappableAppSyncChange(
6061
responseMappingTemplateS3Location: change.newValue.Properties?.ResponseMappingTemplateS3Location,
6162
code: change.newValue.Properties?.Code,
6263
codeS3Location: change.newValue.Properties?.CodeS3Location,
64+
expires: change.newValue.Properties?.Expires,
6365
};
6466
const evaluatedResourceProperties = await evaluateCfnTemplate.evaluateCfnExpression(sdkProperties);
6567
const sdkRequestObject = transformObjectKeys(evaluatedResourceProperties, lowerCaseFirstCharacter);
@@ -74,25 +76,34 @@ export async function isHotswappableAppSyncChange(
7476
delete sdkRequestObject.responseMappingTemplateS3Location;
7577
}
7678
if (sdkRequestObject.definitionS3Location) {
77-
sdkRequestObject.definition = await fetchFileFromS3(sdkRequestObject.definitionS3Location, sdk);
79+
sdkRequestObject.definition = (await fetchFileFromS3(sdkRequestObject.definitionS3Location, sdk))?.toString('utf8');
7880
delete sdkRequestObject.definitionS3Location;
7981
}
8082
if (sdkRequestObject.codeS3Location) {
81-
sdkRequestObject.code = await fetchFileFromS3(sdkRequestObject.codeS3Location, sdk);
83+
sdkRequestObject.code = (await fetchFileFromS3(sdkRequestObject.codeS3Location, sdk))?.toString('utf8');
8284
delete sdkRequestObject.codeS3Location;
8385
}
8486

8587
if (isResolver) {
8688
await sdk.appsync().updateResolver(sdkRequestObject).promise();
8789
} else if (isFunction) {
8890

91+
// Function version is only applicable when using VTL and mapping templates
92+
// Runtime only applicable when using code (JS mapping templates)
93+
if (sdkRequestObject.code) {
94+
delete sdkRequestObject.functionVersion;
95+
} else {
96+
delete sdkRequestObject.runtime;
97+
}
98+
8999
const { functions } = await sdk.appsync().listFunctions({ apiId: sdkRequestObject.apiId }).promise();
90100
const { functionId } = functions?.find(fn => fn.name === physicalName) ?? {};
101+
// Updating multiple functions at the same time or along with graphql schema results in `ConcurrentModificationException`
91102
await simpleRetry(
92103
() => sdk.appsync().updateFunction({ ...sdkRequestObject, functionId: functionId! }).promise(),
93-
3,
104+
5,
94105
'ConcurrentModificationException');
95-
} else {
106+
} else if (isGraphQLSchema) {
96107
let schemaCreationResponse: GetSchemaCreationStatusResponse = await sdk.appsync().startSchemaCreation(sdkRequestObject).promise();
97108
while (schemaCreationResponse.status && ['PROCESSING', 'DELETING'].some(status => status === schemaCreationResponse.status)) {
98109
await sleep(1000); // poll every second
@@ -104,6 +115,15 @@ export async function isHotswappableAppSyncChange(
104115
if (schemaCreationResponse.status === 'FAILED') {
105116
throw new Error(schemaCreationResponse.details);
106117
}
118+
} else { //isApiKey
119+
if (!sdkRequestObject.id) {
120+
// ApiKeyId is optional in CFN but required in SDK. Grab the KeyId from physicalArn if not available as part of CFN template
121+
const arnParts = physicalName?.split('/');
122+
if (arnParts && arnParts.length === 4) {
123+
sdkRequestObject.id = arnParts[3];
124+
}
125+
}
126+
await sdk.appsync().updateApiKey(sdkRequestObject).promise();
107127
}
108128
},
109129
});
@@ -124,7 +144,7 @@ async function simpleRetry(fn: () => Promise<any>, numOfRetries: number, errorCo
124144
await fn();
125145
} catch (error: any) {
126146
if (error && error.code === errorCodeToRetry && numOfRetries > 0) {
127-
await sleep(500); // wait half a second
147+
await sleep(1000); // wait a whole second
128148
await simpleRetry(fn, numOfRetries - 1, errorCodeToRetry);
129149
} else {
130150
throw error;

packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts

+230
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ import { HotswapMode } from '../../../lib/api/hotswap/common';
66
let hotswapMockSdkProvider: setup.HotswapMockSdkProvider;
77
let mockUpdateResolver: (params: AppSync.UpdateResolverRequest) => AppSync.UpdateResolverResponse;
88
let mockUpdateFunction: (params: AppSync.UpdateFunctionRequest) => AppSync.UpdateFunctionResponse;
9+
let mockUpdateApiKey: (params: AppSync.UpdateApiKeyRequest) => AppSync.UpdateApiKeyResponse;
910
let mockStartSchemaCreation: (params: AppSync.StartSchemaCreationRequest) => AppSync.StartSchemaCreationResponse;
1011
let mockS3GetObject: (params: S3.GetObjectRequest) => S3.GetObjectOutput;
1112

1213
beforeEach(() => {
1314
hotswapMockSdkProvider = setup.setupHotswapTests();
1415
mockUpdateResolver = jest.fn();
1516
mockUpdateFunction = jest.fn();
17+
mockUpdateApiKey = jest.fn();
1618
mockStartSchemaCreation = jest.fn();
1719
hotswapMockSdkProvider.stubAppSync({
1820
updateResolver: mockUpdateResolver,
1921
updateFunction: mockUpdateFunction,
22+
updateApiKey: mockUpdateApiKey,
2023
startSchemaCreation: mockStartSchemaCreation,
2124
});
2225

@@ -568,6 +571,127 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot
568571
});
569572
});
570573

574+
test('calls the updateFunction() API with function version when it receives both function version and runtime with a mapping template in a Function', async () => {
575+
// GIVEN
576+
const mockListFunctions = jest.fn().mockReturnValue({ functions: [{ name: 'my-function', functionId: 'functionId' }] });
577+
hotswapMockSdkProvider.stubAppSync({ listFunctions: mockListFunctions, updateFunction: mockUpdateFunction });
578+
579+
setup.setCurrentCfnStackTemplate({
580+
Resources: {
581+
AppSyncFunction: {
582+
Type: 'AWS::AppSync::FunctionConfiguration',
583+
Properties: {
584+
Name: 'my-function',
585+
ApiId: 'apiId',
586+
DataSourceName: 'my-datasource',
587+
FunctionVersion: '2018-05-29',
588+
Runtime: 'APPSYNC_JS',
589+
RequestMappingTemplate: '## original request template',
590+
ResponseMappingTemplate: '## original response template',
591+
},
592+
Metadata: {
593+
'aws:asset:path': 'old-path',
594+
},
595+
},
596+
},
597+
});
598+
const cdkStackArtifact = setup.cdkStackArtifactOf({
599+
template: {
600+
Resources: {
601+
AppSyncFunction: {
602+
Type: 'AWS::AppSync::FunctionConfiguration',
603+
Properties: {
604+
Name: 'my-function',
605+
ApiId: 'apiId',
606+
DataSourceName: 'my-datasource',
607+
FunctionVersion: '2018-05-29',
608+
Runtime: 'APPSYNC_JS',
609+
RequestMappingTemplate: '## original request template',
610+
ResponseMappingTemplate: '## new response template',
611+
},
612+
Metadata: {
613+
'aws:asset:path': 'new-path',
614+
},
615+
},
616+
},
617+
},
618+
});
619+
620+
// WHEN
621+
const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact);
622+
623+
// THEN
624+
expect(deployStackResult).not.toBeUndefined();
625+
expect(mockUpdateFunction).toHaveBeenCalledWith({
626+
apiId: 'apiId',
627+
dataSourceName: 'my-datasource',
628+
functionId: 'functionId',
629+
functionVersion: '2018-05-29',
630+
name: 'my-function',
631+
requestMappingTemplate: '## original request template',
632+
responseMappingTemplate: '## new response template',
633+
});
634+
});
635+
636+
test('calls the updateFunction() API with runtime when it receives both function version and runtime with code in a Function', async () => {
637+
// GIVEN
638+
const mockListFunctions = jest.fn().mockReturnValue({ functions: [{ name: 'my-function', functionId: 'functionId' }] });
639+
hotswapMockSdkProvider.stubAppSync({ listFunctions: mockListFunctions, updateFunction: mockUpdateFunction });
640+
641+
setup.setCurrentCfnStackTemplate({
642+
Resources: {
643+
AppSyncFunction: {
644+
Type: 'AWS::AppSync::FunctionConfiguration',
645+
Properties: {
646+
Name: 'my-function',
647+
ApiId: 'apiId',
648+
DataSourceName: 'my-datasource',
649+
FunctionVersion: '2018-05-29',
650+
Runtime: 'APPSYNC_JS',
651+
Code: 'old test code',
652+
},
653+
Metadata: {
654+
'aws:asset:path': 'old-path',
655+
},
656+
},
657+
},
658+
});
659+
const cdkStackArtifact = setup.cdkStackArtifactOf({
660+
template: {
661+
Resources: {
662+
AppSyncFunction: {
663+
Type: 'AWS::AppSync::FunctionConfiguration',
664+
Properties: {
665+
Name: 'my-function',
666+
ApiId: 'apiId',
667+
DataSourceName: 'my-datasource',
668+
FunctionVersion: '2018-05-29',
669+
Runtime: 'APPSYNC_JS',
670+
Code: 'new test code',
671+
},
672+
Metadata: {
673+
'aws:asset:path': 'new-path',
674+
},
675+
},
676+
},
677+
},
678+
});
679+
680+
// WHEN
681+
const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact);
682+
683+
// THEN
684+
expect(deployStackResult).not.toBeUndefined();
685+
expect(mockUpdateFunction).toHaveBeenCalledWith({
686+
apiId: 'apiId',
687+
dataSourceName: 'my-datasource',
688+
functionId: 'functionId',
689+
runtime: 'APPSYNC_JS',
690+
name: 'my-function',
691+
code: 'new test code',
692+
});
693+
});
694+
571695
test('calls the updateFunction() API when it receives only a mapping template s3 location difference in a Function', async () => {
572696
// GIVEN
573697
mockS3GetObject = jest.fn().mockImplementation(async () => {
@@ -1032,4 +1156,110 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot
10321156
apiId: 'apiId',
10331157
});
10341158
});
1159+
1160+
test('calls the updateApiKey() API when it receives only a expires property difference in an AppSync ApiKey', async () => {
1161+
// GIVEN
1162+
setup.setCurrentCfnStackTemplate({
1163+
Resources: {
1164+
AppSyncApiKey: {
1165+
Type: 'AWS::AppSync::ApiKey',
1166+
Properties: {
1167+
ApiId: 'apiId',
1168+
Expires: 1000,
1169+
Id: 'key-id',
1170+
},
1171+
Metadata: {
1172+
'aws:asset:path': 'old-path',
1173+
},
1174+
},
1175+
},
1176+
});
1177+
setup.pushStackResourceSummaries(
1178+
setup.stackSummaryOf(
1179+
'AppSyncApiKey',
1180+
'AWS::AppSync::ApiKey',
1181+
'arn:aws:appsync:us-east-1:111111111111:apis/apiId/apikeys/api-key-id',
1182+
),
1183+
);
1184+
const cdkStackArtifact = setup.cdkStackArtifactOf({
1185+
template: {
1186+
Resources: {
1187+
AppSyncApiKey: {
1188+
Type: 'AWS::AppSync::ApiKey',
1189+
Properties: {
1190+
ApiId: 'apiId',
1191+
Expires: 1001,
1192+
Id: 'key-id',
1193+
},
1194+
Metadata: {
1195+
'aws:asset:path': 'new-path',
1196+
},
1197+
},
1198+
},
1199+
},
1200+
});
1201+
1202+
// WHEN
1203+
const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact);
1204+
1205+
// THEN
1206+
expect(deployStackResult).not.toBeUndefined();
1207+
expect(mockUpdateApiKey).toHaveBeenCalledWith({
1208+
apiId: 'apiId',
1209+
expires: 1001,
1210+
id: 'key-id',
1211+
});
1212+
});
1213+
1214+
test('calls the updateApiKey() API when it receives only a expires property difference and no api-key-id in an AppSync ApiKey', async () => {
1215+
// GIVEN
1216+
setup.setCurrentCfnStackTemplate({
1217+
Resources: {
1218+
AppSyncApiKey: {
1219+
Type: 'AWS::AppSync::ApiKey',
1220+
Properties: {
1221+
ApiId: 'apiId',
1222+
Expires: 1000,
1223+
},
1224+
Metadata: {
1225+
'aws:asset:path': 'old-path',
1226+
},
1227+
},
1228+
},
1229+
});
1230+
setup.pushStackResourceSummaries(
1231+
setup.stackSummaryOf(
1232+
'AppSyncApiKey',
1233+
'AWS::AppSync::ApiKey',
1234+
'arn:aws:appsync:us-east-1:111111111111:apis/apiId/apikeys/api-key-id',
1235+
),
1236+
);
1237+
const cdkStackArtifact = setup.cdkStackArtifactOf({
1238+
template: {
1239+
Resources: {
1240+
AppSyncApiKey: {
1241+
Type: 'AWS::AppSync::ApiKey',
1242+
Properties: {
1243+
ApiId: 'apiId',
1244+
Expires: 1001,
1245+
},
1246+
Metadata: {
1247+
'aws:asset:path': 'new-path',
1248+
},
1249+
},
1250+
},
1251+
},
1252+
});
1253+
1254+
// WHEN
1255+
const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact);
1256+
1257+
// THEN
1258+
expect(deployStackResult).not.toBeUndefined();
1259+
expect(mockUpdateApiKey).toHaveBeenCalledWith({
1260+
apiId: 'apiId',
1261+
expires: 1001,
1262+
id: 'api-key-id',
1263+
});
1264+
});
10351265
});

0 commit comments

Comments
 (0)