Skip to content

Commit 8161b59

Browse files
rix0rrrhollanddd
authored andcommitted
fix(core): toJsonString() cannot handle list intrinsics (aws#13544)
The previous attempt at a fix missed one important case: the types of the values involved in the `{ Fn::Join }` expression didn't actually match up. They all needed to be strings, but the previous implentation just dropped list-typed values in there. Unfortunately, there is no way to do it correctly with just string manipulation in CloudFormation (`{ Fn::Join }` etc), so we'll have to resort to using a Custom Resource if we encounter list values. Actually fixes aws#13465. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 6ab4f1f commit 8161b59

File tree

5 files changed

+88
-8
lines changed

5 files changed

+88
-8
lines changed

packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts

+33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Construct } from '../construct-compat';
2+
import { CustomResource } from '../custom-resource';
23
import { CustomResourceProvider, CustomResourceProviderRuntime } from '../custom-resource-provider';
4+
import { CfnUtilsResourceType } from './cfn-utils-provider/consts';
35

46
/**
57
* A custom resource provider for CFN utilities such as `CfnJson`.
@@ -11,4 +13,35 @@ export class CfnUtilsProvider extends Construct {
1113
codeDirectory: `${__dirname}/cfn-utils-provider`,
1214
});
1315
}
16+
}
17+
18+
/**
19+
* Utility functions provided by the CfnUtilsProvider
20+
*/
21+
export abstract class CfnUtils {
22+
/**
23+
* Encode a structure to JSON at CloudFormation deployment time
24+
*
25+
* This would have been suitable for the JSON-encoding of abitrary structures, however:
26+
*
27+
* - It uses a custom resource to do the encoding, and we'd rather not use a custom
28+
* resource if we can avoid it.
29+
* - It cannot be used to encode objects where the keys of the objects can contain
30+
* tokens--because those cannot be represented in the JSON encoding that CloudFormation
31+
* templates use.
32+
*
33+
* This helper is used by `CloudFormationLang.toJSON()` if and only if it encounters
34+
* objects that cannot be stringified any other way.
35+
*/
36+
public static stringify(scope: Construct, id: string, value: any): string {
37+
const resource = new CustomResource(scope, id, {
38+
serviceToken: CfnUtilsProvider.getOrCreate(scope),
39+
resourceType: CfnUtilsResourceType.CFN_JSON_STRINGIFY,
40+
properties: {
41+
Value: value,
42+
},
43+
});
44+
45+
return resource.getAttString('Value');
46+
}
1447
}

packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,10 @@ export const enum CfnUtilsResourceType {
55
/**
66
* CfnJson
77
*/
8-
CFN_JSON = 'Custom::AWSCDKCfnJson'
8+
CFN_JSON = 'Custom::AWSCDKCfnJson',
9+
10+
/**
11+
* CfnJsonStringify
12+
*/
13+
CFN_JSON_STRINGIFY = 'Custom::AWSCDKCfnJsonStringify',
914
}

packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
99
if (event.ResourceType === CfnUtilsResourceType.CFN_JSON) {
1010
return cfnJsonHandler(event);
1111
}
12+
if (event.ResourceType === CfnUtilsResourceType.CFN_JSON_STRINGIFY) {
13+
return cfnJsonStringifyHandler(event);
14+
}
1215

1316
throw new Error(`unexpected resource type "${event.ResourceType}`);
1417
}
@@ -20,3 +23,11 @@ function cfnJsonHandler(event: AWSLambda.CloudFormationCustomResourceEvent) {
2023
},
2124
};
2225
}
26+
27+
function cfnJsonStringifyHandler(event: AWSLambda.CloudFormationCustomResourceEvent) {
28+
return {
29+
Data: {
30+
Value: JSON.stringify(event.ResourceProperties.Value),
31+
},
32+
};
33+
}

packages/@aws-cdk/core/lib/private/cloudformation-lang.ts

+33-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Lazy } from '../lazy';
22
import { DefaultTokenResolver, IFragmentConcatenator, IResolveContext } from '../resolvable';
3+
import { Stack } from '../stack';
34
import { Token } from '../token';
5+
import { CfnUtils } from './cfn-utils-provider';
46
import { INTRINSIC_KEY_PREFIX, ResolutionTypeHint, resolvedTypeHint } from './resolve';
57

68
/**
@@ -170,7 +172,8 @@ function tokenAwareStringify(root: any, space: number, ctx: IResolveContext) {
170172
// AND it's the result of a token resolution. Otherwise, we just treat this
171173
// value as a regular old JSON object (that happens to look a lot like an intrinsic).
172174
if (isIntrinsic(obj) && resolvedTypeHint(obj)) {
173-
return renderIntrinsic(obj);
175+
renderIntrinsic(obj);
176+
return;
174177
}
175178

176179
return renderCollection('{', '}', definedEntries(obj), ([key, value]) => {
@@ -211,12 +214,34 @@ function tokenAwareStringify(root: any, space: number, ctx: IResolveContext) {
211214
pushLiteral('"');
212215
pushIntrinsic(deepQuoteStringLiterals(intrinsic));
213216
pushLiteral('"');
214-
break;
215-
216-
default:
217+
return;
218+
219+
case ResolutionTypeHint.LIST:
220+
// We need this to look like:
221+
//
222+
// '{"listValue":' ++ STRINGIFY(CFN_EVAL({ Ref: MyList })) ++ '}'
223+
//
224+
// However, STRINGIFY would need to execute at CloudFormation deployment time, and that doesn't exist.
225+
//
226+
// We could *ALMOST* use:
227+
//
228+
// '{"listValue":["' ++ JOIN('","', { Ref: MyList }) ++ '"]}'
229+
//
230+
// But that has the unfortunate side effect that if `CFN_EVAL({ Ref: MyList }) == []`, then it would
231+
// evaluate to `[""]`, which is a different value. Since CloudFormation does not have arbitrary
232+
// conditionals there's no way to deal with this case properly.
233+
//
234+
// Therefore, if we encounter lists we need to defer to a custom resource to handle
235+
// them properly at deploy time.
236+
pushIntrinsic(CfnUtils.stringify(Stack.of(ctx.scope), `CdkJsonStringify${stringifyCounter++}`, intrinsic));
237+
return;
238+
239+
case ResolutionTypeHint.NUMBER:
217240
pushIntrinsic(intrinsic);
218-
break;
241+
return;
219242
}
243+
244+
throw new Error(`Unexpected type hint: ${resolvedTypeHint(intrinsic)}`);
220245
}
221246

222247
/**
@@ -391,4 +416,6 @@ function deepQuoteStringLiterals(x: any): any {
391416
function quoteString(s: string) {
392417
s = JSON.stringify(s);
393418
return s.substring(1, s.length - 1);
394-
}
419+
}
420+
421+
let stringifyCounter = 1;

packages/@aws-cdk/core/test/cloudformation-json.test.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,11 @@ describe('tokens that return literals', () => {
103103

104104
// WHEN
105105
expect(stack.resolve(stack.toJsonString({ someList }))).toEqual({
106-
'Fn::Join': ['', ['{"someList":', { Ref: 'Thing' }, '}']],
106+
'Fn::Join': ['', [
107+
'{"someList":',
108+
{ 'Fn::GetAtt': [expect.stringContaining('CdkJsonStringify'), 'Value'] },
109+
'}',
110+
]],
107111
});
108112
});
109113

0 commit comments

Comments
 (0)