Skip to content

Commit 43f276a

Browse files
Elad Ben-Israelmergify[bot]
Elad Ben-Israel
authored andcommittedNov 5, 2019
fix(ssm): malformed ARNs for parameters with physical names that use path notation (aws#4842)
* fix(ssm): malformed ARNs for parameters with physical names that use path notation SSM parameter names can have one of two forms: “simpleName” or “/path/name”. This makes it tricky to render an ARN for the parameter is the name is an unresolvable token (such as a “Ref”) because we can’t decide whether a “/“ separator is required in the ARN. The previous implementation assumed "Ref" always returns the name without a "/" prefix, and therefore did not use the "/" separator. This fix will use the physical name itself (if possible) to determine the separator (and also assume that generated names will not use the path notation). The only case where this is impossible is if the physical name is a token (either created or imported), in which case we should be able to synthesize a CloudFormation condition which will parse the token during deployment. This test also adds a validation that verifies that if a physical name is provided and uses path notation, it must begin with a "/". Misc: re-add `install.sh` to call `npx yarn install` * explicit parameterArnSeparator revert attempt to guess parameter name prefix if it's a token since we can't incorporate refs in conditions. Instead, if the parameter name if a token, we expect `parameterArnSeparator` to be explicitly defined and be one of "/" or "". * misc * fix test expectation * add public API doc * add --frozen-lockfile to install.sh * rename "parameterArnSeparator: string" to "simpleName: boolean"
1 parent df6fc58 commit 43f276a

File tree

7 files changed

+620
-58
lines changed

7 files changed

+620
-58
lines changed
 

‎install.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
exec npx yarn install --frozen-lockfile

‎packages/@aws-cdk/aws-ssm/lib/parameter.ts

+55-30
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import iam = require('@aws-cdk/aws-iam');
22
import kms = require('@aws-cdk/aws-kms');
33
import {
44
CfnDynamicReference, CfnDynamicReferenceService, CfnParameter,
5-
Construct, ContextProvider, Fn, IConstruct, IResource, Resource, Stack, Token
5+
Construct, ContextProvider, Fn, IResource, Resource, Stack, Token
66
} from '@aws-cdk/core';
77
import cxapi = require('@aws-cdk/cx-api');
88
import ssm = require('./ssm.generated');
9+
import { arnForParameterName, AUTOGEN_MARKER } from './util';
910

1011
/**
1112
* An SSM Parameter reference.
@@ -94,6 +95,22 @@ export interface ParameterOptions {
9495
* @default - a name will be generated by CloudFormation
9596
*/
9697
readonly parameterName?: string;
98+
99+
/**
100+
* Indicates of the parameter name is a simple name (i.e. does not include "/"
101+
* separators).
102+
*
103+
* This is only required only if `parameterName` is a token, which means we
104+
* are unable to detect if the name is simple or "path-like" for the purpose
105+
* of rendering SSM parameter ARNs.
106+
*
107+
* If `parameterName` is not specified, `simpleName` must be `true` (or
108+
* undefined) since the name generated by AWS CloudFormation is always a
109+
* simple name.
110+
*
111+
* @default - auto-detect based on `parameterName`
112+
*/
113+
readonly simpleName?: boolean;
97114
}
98115

99116
/**
@@ -184,12 +201,36 @@ export enum ParameterType {
184201
AWS_EC2_IMAGE_ID = 'AWS::EC2::Image::Id',
185202
}
186203

187-
export interface StringParameterAttributes {
204+
/**
205+
* Common attributes for string parameters.
206+
*/
207+
export interface CommonStringParameterAttributes {
188208
/**
189-
* The name of the parameter store value
209+
* The name of the parameter store value.
210+
*
211+
* This value can be a token or a concrete string. If it is a concrete string
212+
* and includes "/" it must also be prefixed with a "/" (fully-qualified).
190213
*/
191214
readonly parameterName: string;
192215

216+
/**
217+
* Indicates of the parameter name is a simple name (i.e. does not include "/"
218+
* separators).
219+
*
220+
* This is only required only if `parameterName` is a token, which means we
221+
* are unable to detect if the name is simple or "path-like" for the purpose
222+
* of rendering SSM parameter ARNs.
223+
*
224+
* If `parameterName` is not specified, `simpleName` must be `true` (or
225+
* undefined) since the name generated by AWS CloudFormation is always a
226+
* simple name.
227+
*
228+
* @default - auto-detect based on `parameterName`
229+
*/
230+
readonly simpleName?: boolean;
231+
}
232+
233+
export interface StringParameterAttributes extends CommonStringParameterAttributes {
193234
/**
194235
* The version number of the value you wish to retrieve.
195236
*
@@ -205,12 +246,7 @@ export interface StringParameterAttributes {
205246
readonly type?: ParameterType;
206247
}
207248

208-
export interface SecureStringParameterAttributes {
209-
/**
210-
* The name of the parameter store value
211-
*/
212-
readonly parameterName: string;
213-
249+
export interface SecureStringParameterAttributes extends CommonStringParameterAttributes {
214250
/**
215251
* The version number of the value you wish to retrieve. This is required for secure strings.
216252
*/
@@ -253,7 +289,7 @@ export class StringParameter extends ParameterBase implements IStringParameter {
253289

254290
class Import extends ParameterBase {
255291
public readonly parameterName = attrs.parameterName;
256-
public readonly parameterArn = arnForParameterName(this, this.parameterName);
292+
public readonly parameterArn = arnForParameterName(this, attrs.parameterName, { simpleName: attrs.simpleName });
257293
public readonly parameterType = type;
258294
public readonly stringValue = stringValue;
259295
}
@@ -269,7 +305,7 @@ export class StringParameter extends ParameterBase implements IStringParameter {
269305

270306
class Import extends ParameterBase {
271307
public readonly parameterName = attrs.parameterName;
272-
public readonly parameterArn = arnForParameterName(this, this.parameterName);
308+
public readonly parameterArn = arnForParameterName(this, attrs.parameterName, { simpleName: attrs.simpleName });
273309
public readonly parameterType = ParameterType.SECURE_STRING;
274310
public readonly stringValue = stringValue;
275311
public readonly encryptionKey = attrs.encryptionKey;
@@ -360,7 +396,10 @@ export class StringParameter extends ParameterBase implements IStringParameter {
360396
});
361397

362398
this.parameterName = this.getResourceNameAttribute(resource.ref);
363-
this.parameterArn = arnForParameterName(this, this.parameterName);
399+
this.parameterArn = arnForParameterName(this, this.parameterName, {
400+
physicalName: props.parameterName || AUTOGEN_MARKER,
401+
simpleName: props.simpleName
402+
});
364403

365404
this.parameterType = resource.attrType;
366405
this.stringValue = resource.attrValue;
@@ -413,7 +452,10 @@ export class StringListParameter extends ParameterBase implements IStringListPar
413452
value: props.stringListValue.join(','),
414453
});
415454
this.parameterName = this.getResourceNameAttribute(resource.ref);
416-
this.parameterArn = arnForParameterName(this, this.parameterName);
455+
this.parameterArn = arnForParameterName(this, this.parameterName, {
456+
physicalName: props.parameterName || AUTOGEN_MARKER,
457+
simpleName: props.simpleName
458+
});
417459

418460
this.parameterType = resource.attrType;
419461
this.stringListValue = Fn.split(',', resource.attrValue);
@@ -442,20 +484,3 @@ function _assertValidValue(value: string, allowedPattern: string): void {
442484
function makeIdentityForImportedValue(parameterName: string) {
443485
return `SsmParameterValue:${parameterName}:C96584B6-F00A-464E-AD19-53AFF4B05118`;
444486
}
445-
446-
function arnForParameterName(scope: IConstruct, parameterName: string): string {
447-
448-
// remove trailing "/" if we can resolve parameter name.
449-
if (!Token.isUnresolved(parameterName)) {
450-
if (parameterName.startsWith('/')) {
451-
parameterName = parameterName.substr(1);
452-
}
453-
}
454-
455-
return Stack.of(scope).formatArn({
456-
service: 'ssm',
457-
resource: 'parameter',
458-
sep: '/', // Sep is empty because this.parameterName starts with a / already!
459-
resourceName: parameterName,
460-
});
461-
}

‎packages/@aws-cdk/aws-ssm/lib/util.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { IConstruct, Stack, Token } from "@aws-cdk/core";
2+
3+
export const AUTOGEN_MARKER = '$$autogen$$';
4+
5+
export interface ArnForParameterNameOptions {
6+
readonly physicalName?: string;
7+
readonly simpleName?: boolean;
8+
}
9+
10+
/**
11+
* Renders an ARN for an SSM parameter given a parameter name.
12+
* @param scope definition scope
13+
* @param parameterName the parameter name to include in the ARN
14+
* @param physicalName optional physical name specified by the user (to auto-detect separator)
15+
*/
16+
export function arnForParameterName(scope: IConstruct, parameterName: string, options: ArnForParameterNameOptions = { }): string {
17+
const physicalName = options.physicalName;
18+
const nameToValidate = physicalName || parameterName;
19+
20+
if (!Token.isUnresolved(nameToValidate) && nameToValidate.includes('/') && !nameToValidate.startsWith('/')) {
21+
throw new Error(`Parameter names must be fully qualified (if they include "/" they must also begin with a "/"): ${nameToValidate}`);
22+
}
23+
24+
return Stack.of(scope).formatArn({
25+
service: 'ssm',
26+
resource: 'parameter',
27+
sep: isSimpleName() ? '/' : '',
28+
resourceName: parameterName,
29+
});
30+
31+
/**
32+
* Determines the ARN separator for this parameter: if we have a concrete
33+
* parameter name (or explicitly defined physical name), we will parse them
34+
* and decide whether a "/" is needed or not. Otherwise, users will have to
35+
* explicitly specify `simpleName` when they import the ARN.
36+
*/
37+
function isSimpleName(): boolean {
38+
// look for a concrete name as a hint for determining the separator
39+
const concreteName = !Token.isUnresolved(parameterName) ? parameterName : physicalName;
40+
if (!concreteName || Token.isUnresolved(concreteName)) {
41+
42+
if (options.simpleName === undefined) {
43+
throw new Error(`Unable to determine ARN separator for SSM parameter since the parameter name is an unresolved token. Use "fromAttributes" and specify "simpleName" explicitly`);
44+
}
45+
46+
return options.simpleName;
47+
}
48+
49+
const result = !concreteName.startsWith('/');
50+
51+
// if users explicitly specify the separator and it conflicts with the one we need, it's an error.
52+
if (options.simpleName !== undefined && options.simpleName !== result) {
53+
54+
if (concreteName === AUTOGEN_MARKER) {
55+
throw new Error(`If "parameterName" is not explicitly defined, "simpleName" must be "true" or undefined since auto-generated parameter names always have simple names`);
56+
}
57+
58+
throw new Error(`Parameter name "${concreteName}" is ${result ? 'a simple name' : 'not a simple name'}, but "simpleName" was explicitly set to ${options.simpleName}. Either omit it or set it to ${result}`);
59+
}
60+
61+
return result;
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
{
2+
"Parameters": {
3+
"ParameterNameParameter": {
4+
"Type": "String",
5+
"Default": "myParamName"
6+
}
7+
},
8+
"Resources": {
9+
"StringAutogenE7E896E4": {
10+
"Type": "AWS::SSM::Parameter",
11+
"Properties": {
12+
"Type": "String",
13+
"Value": "hello, world"
14+
}
15+
},
16+
"StringSimpleA681514D": {
17+
"Type": "AWS::SSM::Parameter",
18+
"Properties": {
19+
"Type": "String",
20+
"Value": "hello, world",
21+
"Name": "simple-name"
22+
}
23+
},
24+
"StringPathD8120137": {
25+
"Type": "AWS::SSM::Parameter",
26+
"Properties": {
27+
"Type": "String",
28+
"Value": "hello, world",
29+
"Name": "/path/name/foo/bar"
30+
}
31+
},
32+
"ListAutogenC5DA1CAE": {
33+
"Type": "AWS::SSM::Parameter",
34+
"Properties": {
35+
"Type": "StringList",
36+
"Value": "hello,world"
37+
}
38+
},
39+
"ListSimple9DB641CB": {
40+
"Type": "AWS::SSM::Parameter",
41+
"Properties": {
42+
"Type": "StringList",
43+
"Value": "hello,world",
44+
"Name": "list-simple-name"
45+
}
46+
},
47+
"ListPath120D6FAB": {
48+
"Type": "AWS::SSM::Parameter",
49+
"Properties": {
50+
"Type": "StringList",
51+
"Value": "hello,world",
52+
"Name": "/list/path/name"
53+
}
54+
},
55+
"ParameterizedSimpleB6311859": {
56+
"Type": "AWS::SSM::Parameter",
57+
"Properties": {
58+
"Type": "String",
59+
"Value": "hello, world",
60+
"Name": {
61+
"Ref": "ParameterNameParameter"
62+
}
63+
}
64+
},
65+
"ParameterizedNonSimple23C44BF6": {
66+
"Type": "AWS::SSM::Parameter",
67+
"Properties": {
68+
"Type": "String",
69+
"Value": "hello, world",
70+
"Name": {
71+
"Fn::Join": [
72+
"",
73+
[
74+
"/",
75+
{
76+
"Ref": "ParameterNameParameter"
77+
},
78+
"/non/simple"
79+
]
80+
]
81+
}
82+
}
83+
}
84+
},
85+
"Outputs": {
86+
"StringAutogenArn": {
87+
"Value": {
88+
"Fn::Join": [
89+
"",
90+
[
91+
"arn:",
92+
{
93+
"Ref": "AWS::Partition"
94+
},
95+
":ssm:",
96+
{
97+
"Ref": "AWS::Region"
98+
},
99+
":",
100+
{
101+
"Ref": "AWS::AccountId"
102+
},
103+
":parameter/",
104+
{
105+
"Ref": "StringAutogenE7E896E4"
106+
}
107+
]
108+
]
109+
}
110+
},
111+
"StringSimpleArn": {
112+
"Value": {
113+
"Fn::Join": [
114+
"",
115+
[
116+
"arn:",
117+
{
118+
"Ref": "AWS::Partition"
119+
},
120+
":ssm:",
121+
{
122+
"Ref": "AWS::Region"
123+
},
124+
":",
125+
{
126+
"Ref": "AWS::AccountId"
127+
},
128+
":parameter/",
129+
{
130+
"Ref": "StringSimpleA681514D"
131+
}
132+
]
133+
]
134+
}
135+
},
136+
"StringPathArn": {
137+
"Value": {
138+
"Fn::Join": [
139+
"",
140+
[
141+
"arn:",
142+
{
143+
"Ref": "AWS::Partition"
144+
},
145+
":ssm:",
146+
{
147+
"Ref": "AWS::Region"
148+
},
149+
":",
150+
{
151+
"Ref": "AWS::AccountId"
152+
},
153+
":parameter",
154+
{
155+
"Ref": "StringPathD8120137"
156+
}
157+
]
158+
]
159+
}
160+
},
161+
"ListAutogenArn": {
162+
"Value": {
163+
"Fn::Join": [
164+
"",
165+
[
166+
"arn:",
167+
{
168+
"Ref": "AWS::Partition"
169+
},
170+
":ssm:",
171+
{
172+
"Ref": "AWS::Region"
173+
},
174+
":",
175+
{
176+
"Ref": "AWS::AccountId"
177+
},
178+
":parameter/",
179+
{
180+
"Ref": "ListAutogenC5DA1CAE"
181+
}
182+
]
183+
]
184+
}
185+
},
186+
"ListSimpleArn": {
187+
"Value": {
188+
"Fn::Join": [
189+
"",
190+
[
191+
"arn:",
192+
{
193+
"Ref": "AWS::Partition"
194+
},
195+
":ssm:",
196+
{
197+
"Ref": "AWS::Region"
198+
},
199+
":",
200+
{
201+
"Ref": "AWS::AccountId"
202+
},
203+
":parameter/",
204+
{
205+
"Ref": "ListSimple9DB641CB"
206+
}
207+
]
208+
]
209+
}
210+
},
211+
"ListPathArn": {
212+
"Value": {
213+
"Fn::Join": [
214+
"",
215+
[
216+
"arn:",
217+
{
218+
"Ref": "AWS::Partition"
219+
},
220+
":ssm:",
221+
{
222+
"Ref": "AWS::Region"
223+
},
224+
":",
225+
{
226+
"Ref": "AWS::AccountId"
227+
},
228+
":parameter",
229+
{
230+
"Ref": "ListPath120D6FAB"
231+
}
232+
]
233+
]
234+
}
235+
},
236+
"ParameterizedSimpleArn": {
237+
"Value": {
238+
"Fn::Join": [
239+
"",
240+
[
241+
"arn:",
242+
{
243+
"Ref": "AWS::Partition"
244+
},
245+
":ssm:",
246+
{
247+
"Ref": "AWS::Region"
248+
},
249+
":",
250+
{
251+
"Ref": "AWS::AccountId"
252+
},
253+
":parameter/",
254+
{
255+
"Ref": "ParameterizedSimpleB6311859"
256+
}
257+
]
258+
]
259+
}
260+
},
261+
"ParameterizedNonSimpleArn": {
262+
"Value": {
263+
"Fn::Join": [
264+
"",
265+
[
266+
"arn:",
267+
{
268+
"Ref": "AWS::Partition"
269+
},
270+
":ssm:",
271+
{
272+
"Ref": "AWS::Region"
273+
},
274+
":",
275+
{
276+
"Ref": "AWS::AccountId"
277+
},
278+
":parameter",
279+
{
280+
"Ref": "ParameterizedNonSimple23C44BF6"
281+
}
282+
]
283+
]
284+
}
285+
}
286+
}
287+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// tslint:disable: max-line-length
2+
import { App, CfnOutput, CfnParameter, Stack } from "@aws-cdk/core";
3+
import ssm = require('../lib');
4+
5+
const app = new App();
6+
const stack = new Stack(app, 'integ-parameter-arns');
7+
8+
const input = new CfnParameter(stack, 'ParameterNameParameter', { type: 'String', default: 'myParamName' });
9+
10+
const params = [
11+
new ssm.StringParameter(stack, 'StringAutogen', { stringValue: 'hello, world' }),
12+
new ssm.StringParameter(stack, 'StringSimple', { stringValue: 'hello, world', parameterName: 'simple-name' }),
13+
new ssm.StringParameter(stack, 'StringPath', { stringValue: 'hello, world', parameterName: '/path/name/foo/bar' }),
14+
new ssm.StringListParameter(stack, 'ListAutogen', { stringListValue: [ 'hello', 'world' ] }),
15+
new ssm.StringListParameter(stack, 'ListSimple', { stringListValue: [ 'hello', 'world' ], parameterName: 'list-simple-name' }),
16+
new ssm.StringListParameter(stack, 'ListPath', { stringListValue: [ 'hello', 'world' ], parameterName: '/list/path/name' }),
17+
new ssm.StringParameter(stack, 'ParameterizedSimple', { stringValue: 'hello, world', parameterName: input.valueAsString, simpleName: true }),
18+
new ssm.StringParameter(stack, 'ParameterizedNonSimple', { stringValue: 'hello, world', parameterName: `/${input.valueAsString}/non/simple`, simpleName: false }),
19+
];
20+
21+
for (const p of params) {
22+
new CfnOutput(stack, `${p.node.id}Arn`, { value: p.parameterArn });
23+
}
24+
25+
app.synth();

‎packages/@aws-cdk/aws-ssm/test/test.parameter.ts

+114-28
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// tslint:disable: max-line-length
2+
13
import { expect, haveResource } from '@aws-cdk/assert';
24
import iam = require('@aws-cdk/aws-iam');
35
import kms = require('@aws-cdk/aws-kms');
@@ -36,7 +38,7 @@ export = {
3638

3739
// THEN
3840
test.throws(() => new ssm.StringParameter(stack, 'Parameter', { allowedPattern: '^Bar$', stringValue: 'FooBar' }),
39-
/does not match the specified allowedPattern/);
41+
/does not match the specified allowedPattern/);
4042
test.done();
4143
},
4244

@@ -46,9 +48,9 @@ export = {
4648

4749
// THEN
4850
test.doesNotThrow(() => {
49-
new ssm.StringParameter(stack, 'Parameter', {
50-
allowedPattern: '^Bar$',
51-
stringValue: cdk.Lazy.stringValue({ produce: () => 'Foo!' }),
51+
new ssm.StringParameter(stack, 'Parameter', {
52+
allowedPattern: '^Bar$',
53+
stringValue: cdk.Lazy.stringValue({ produce: () => 'Foo!' }),
5254
});
5355
});
5456
test.done();
@@ -83,7 +85,7 @@ export = {
8385

8486
// THEN
8587
test.throws(() => new ssm.StringListParameter(stack, 'Parameter', { stringListValue: ['Foo,Bar'] }),
86-
/cannot contain the ',' character/);
88+
/cannot contain the ',' character/);
8789
test.done();
8890
},
8991

@@ -93,7 +95,7 @@ export = {
9395

9496
// THEN
9597
test.throws(() => new ssm.StringListParameter(stack, 'Parameter', { allowedPattern: '^(Foo|Bar)$', stringListValue: ['Foo', 'FooBar'] }),
96-
/does not match the specified allowedPattern/);
98+
/does not match the specified allowedPattern/);
9799
test.done();
98100
},
99101

@@ -130,6 +132,24 @@ export = {
130132
test.done();
131133
},
132134

135+
'parameterName that includes a "/" must be fully qualified (i.e. begin with "/") as well'(test: Test) {
136+
// GIVEN
137+
const stack = new cdk.Stack();
138+
139+
// THEN
140+
test.throws(() => new ssm.StringParameter(stack, 'myParam', {
141+
stringValue: 'myValue',
142+
parameterName: 'path/to/parameter',
143+
}), /Parameter names must be fully qualified/);
144+
145+
test.throws(() => new ssm.StringListParameter(stack, 'myParam2', {
146+
stringListValue: [ 'foo', 'bar' ],
147+
parameterName: 'path/to/parameter2'
148+
}), /Parameter names must be fully qualified \(if they include \"\/\" they must also begin with a \"\/\"\)\: path\/to\/parameter2/);
149+
150+
test.done();
151+
},
152+
133153
'StringParameter.fromStringParameterName'(test: Test) {
134154
// GIVEN
135155
const stack = new Stack();
@@ -139,14 +159,14 @@ export = {
139159

140160
// THEN
141161
test.deepEqual(stack.resolve(param.parameterArn), {
142-
'Fn::Join': [ '', [
162+
'Fn::Join': ['', [
143163
'arn:',
144164
{ Ref: 'AWS::Partition' },
145165
':ssm:',
146166
{ Ref: 'AWS::Region' },
147167
':',
148168
{ Ref: 'AWS::AccountId' },
149-
':parameter/MyParamName' ] ]
169+
':parameter/MyParamName']]
150170
});
151171
test.deepEqual(stack.resolve(param.parameterName), 'MyParamName');
152172
test.deepEqual(stack.resolve(param.parameterType), 'String');
@@ -174,14 +194,14 @@ export = {
174194

175195
// THEN
176196
test.deepEqual(stack.resolve(param.parameterArn), {
177-
'Fn::Join': [ '', [
197+
'Fn::Join': ['', [
178198
'arn:',
179199
{ Ref: 'AWS::Partition' },
180200
':ssm:',
181201
{ Ref: 'AWS::Region' },
182202
':',
183203
{ Ref: 'AWS::AccountId' },
184-
':parameter/MyParamName' ] ]
204+
':parameter/MyParamName']]
185205
});
186206
test.deepEqual(stack.resolve(param.parameterName), 'MyParamName');
187207
test.deepEqual(stack.resolve(param.parameterType), 'String');
@@ -201,14 +221,14 @@ export = {
201221

202222
// THEN
203223
test.deepEqual(stack.resolve(param.parameterArn), {
204-
'Fn::Join': [ '', [
224+
'Fn::Join': ['', [
205225
'arn:',
206226
{ Ref: 'AWS::Partition' },
207227
':ssm:',
208228
{ Ref: 'AWS::Region' },
209229
':',
210230
{ Ref: 'AWS::AccountId' },
211-
':parameter/MyParamName' ] ]
231+
':parameter/MyParamName']]
212232
});
213233
test.deepEqual(stack.resolve(param.parameterName), 'MyParamName');
214234
test.deepEqual(stack.resolve(param.parameterType), 'SecureString');
@@ -348,25 +368,25 @@ export = {
348368

349369
// THEN
350370
test.deepEqual(stack.resolve(param.parameterArn), {
351-
'Fn::Join': [ '', [
371+
'Fn::Join': ['', [
352372
'arn:',
353373
{ Ref: 'AWS::Partition' },
354374
':ssm:',
355375
{ Ref: 'AWS::Region' },
356376
':',
357377
{ Ref: 'AWS::AccountId' },
358-
':parameter/MyParamName' ] ]
378+
':parameter/MyParamName']]
359379
});
360380
test.deepEqual(stack.resolve(param.parameterName), 'MyParamName');
361381
test.deepEqual(stack.resolve(param.parameterType), 'StringList');
362-
test.deepEqual(stack.resolve(param.stringListValue), { 'Fn::Split': [ ',', '{{resolve:ssm:MyParamName}}' ] });
382+
test.deepEqual(stack.resolve(param.stringListValue), { 'Fn::Split': [',', '{{resolve:ssm:MyParamName}}'] });
363383
test.done();
364384
},
365385

366386
'fromLookup will use the SSM context provider to read value during synthesis'(test: Test) {
367387
// GIVEN
368388
const app = new App();
369-
const stack = new Stack(app, 'my-staq', { env: { region: 'us-east-1', account: '12344' }});
389+
const stack = new Stack(app, 'my-staq', { env: { region: 'us-east-1', account: '12344' } });
370390

371391
// WHEN
372392
const value = ssm.StringParameter.valueFromLookup(stack, 'my-param-name');
@@ -450,38 +470,104 @@ export = {
450470
'rendering of parameter arns'(test: Test) {
451471
const stack = new Stack();
452472
const param = new CfnParameter(stack, 'param');
453-
const expectedA = { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/bam' ] ] };
454-
const expectedB = { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'param' } ] ] };
473+
const expectedA = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/bam'] ] };
474+
const expectedB = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'param' } ] ] };
475+
const expectedC = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'param' } ] ] };
455476
let i = 0;
456477

457478
// WHEN
458479
const case1 = ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, 'bam');
459480
const case2 = ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, '/bam');
460-
const case3 = ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, param.valueAsString);
461481
const case4 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: 'bam' });
462482
const case5 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: '/bam' });
463-
const case6 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString });
483+
const case6 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, simpleName: true });
464484
const case7 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: 'bam', version: 10 });
465485
const case8 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: '/bam', version: 10 });
466-
const case9 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, version: 10 });
486+
const case9 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, version: 10, simpleName: false });
487+
488+
// auto-generated name is always generated as a "simple name" (not/a/path)
467489
const case10 = new ssm.StringParameter(stack, `p${i++}`, { stringValue: 'value' });
468490

491+
// explicitly named physical name gives us a hint on how to render the ARN
492+
const case11 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: '/foo/bar', stringValue: 'hello' });
493+
const case12 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: 'simple-name', stringValue: 'hello' });
494+
495+
const case13 = new ssm.StringListParameter(stack, `p${i++}`, { stringListValue: [ 'hello', 'world' ] });
496+
const case14 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: '/not/simple', stringListValue: [ 'hello', 'world' ] });
497+
const case15 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: 'simple', stringListValue: [ 'hello', 'world' ] });
498+
469499
// THEN
470500
test.deepEqual(stack.resolve(case1.parameterArn), expectedA);
471501
test.deepEqual(stack.resolve(case2.parameterArn), expectedA);
472-
test.deepEqual(stack.resolve(case3.parameterArn), expectedB);
473502
test.deepEqual(stack.resolve(case4.parameterArn), expectedA);
474503
test.deepEqual(stack.resolve(case5.parameterArn), expectedA);
475504
test.deepEqual(stack.resolve(case6.parameterArn), expectedB);
476505
test.deepEqual(stack.resolve(case7.parameterArn), expectedA);
477506
test.deepEqual(stack.resolve(case8.parameterArn), expectedA);
478-
test.deepEqual(stack.resolve(case9.parameterArn), expectedB);
479-
test.deepEqual(stack.resolve(case10.parameterArn), {
480-
'Fn::Join': [ '', [
481-
'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p97A508212' }
482-
]
483-
] });
507+
test.deepEqual(stack.resolve(case9.parameterArn), expectedC);
508+
509+
// new ssm.Parameters determine if "/" is needed based on the posture of `parameterName`.
510+
test.deepEqual(stack.resolve(case10.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p81BB0F6FE' } ] ] });
511+
test.deepEqual(stack.resolve(case11.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p97A508212' } ] ] });
512+
test.deepEqual(stack.resolve(case12.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p107D6B8AB0' } ] ] });
513+
test.deepEqual(stack.resolve(case13.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p118A9CB02C' } ] ] });
514+
test.deepEqual(stack.resolve(case14.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p129BE4CE91' } ] ] });
515+
test.deepEqual(stack.resolve(case15.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p1326A2AEC4' } ] ] });
516+
517+
test.done();
518+
},
519+
520+
'if parameterName is a token separator must be specified'(test: Test) {
521+
// GIVEN
522+
const stack = new Stack();
523+
const param = new CfnParameter(stack, 'param');
524+
let i = 0;
525+
526+
// WHEN
527+
const p1 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo', simpleName: true });
528+
const p2 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo', simpleName: false });
529+
const p3 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringListValue: [ 'foo' ], simpleName: false });
530+
531+
// THEN
532+
test.deepEqual(stack.resolve(p1.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p0B02A8F65' } ] ] });
533+
test.deepEqual(stack.resolve(p2.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p1E43AD5AC' } ] ] });
534+
test.deepEqual(stack.resolve(p3.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p2C1903AEB' } ] ] });
535+
536+
test.done();
537+
},
538+
539+
'fails if name is a token and no explicit separator'(test: Test) {
540+
// GIVEN
541+
const stack = new Stack();
542+
const param = new CfnParameter(stack, 'param');
543+
let i = 0;
484544

545+
// THEN
546+
const expected = /Unable to determine ARN separator for SSM parameter since the parameter name is an unresolved token. Use "fromAttributes" and specify "simpleName" explicitly/;
547+
test.throws(() => ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, param.valueAsString), expected);
548+
test.throws(() => ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, version: 1 }), expected);
549+
test.throws(() => new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo' }), expected);
550+
test.throws(() => new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo' }), expected);
551+
test.done();
552+
},
553+
554+
'fails if simpleName is wrong based on a concrete physical name'(test: Test) {
555+
// GIVEN
556+
const stack = new Stack();
557+
let i = 0;
558+
559+
// THEN
560+
test.throws(() => ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: 'simple', simpleName: false }), /Parameter name "simple" is a simple name, but "simpleName" was explicitly set to false. Either omit it or set it to true/);
561+
test.throws(() => ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: '/foo/bar', simpleName: true }), /Parameter name "\/foo\/bar" is not a simple name, but "simpleName" was explicitly set to true. Either omit it or set it to false/);
562+
test.done();
563+
},
564+
565+
'fails if parameterName is undefined and simpleName is "false"'(test: Test) {
566+
// GIVEN
567+
const stack = new Stack();
568+
569+
// THEN
570+
test.throws(() => new ssm.StringParameter(stack, 'p', { simpleName: false, stringValue: 'foo' }), /If "parameterName" is not explicitly defined, "simpleName" must be "true" or undefined since auto-generated parameter names always have simple names/);
485571
test.done();
486572
}
487573
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// tslint:disable: max-line-length
2+
3+
import { Stack, Token } from '@aws-cdk/core';
4+
import { Test } from 'nodeunit';
5+
import { arnForParameterName } from '../lib/util';
6+
7+
export = {
8+
arnForParameterName: {
9+
10+
'simple names': {
11+
12+
'concrete parameterName and no physical name (sep is "/")'(test: Test) {
13+
const stack = new Stack();
14+
test.deepEqual(stack.resolve(arnForParameterName(stack, 'myParam', undefined)), {
15+
'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/myParam']]
16+
});
17+
test.done();
18+
},
19+
20+
'token parameterName and concrete physical name (no additional "/")'(test: Test) {
21+
const stack = new Stack();
22+
test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { physicalName: 'myParam' })), {
23+
'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'Boom' }]]
24+
});
25+
test.done();
26+
},
27+
28+
'token parameterName, explicit "/" separator'(test: Test) {
29+
const stack = new Stack();
30+
test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { simpleName: true })), {
31+
'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'Boom' }]]
32+
});
33+
test.done();
34+
}
35+
36+
},
37+
38+
'path names': {
39+
40+
'concrete parameterName and no physical name (sep is "/")'(test: Test) {
41+
const stack = new Stack();
42+
test.deepEqual(stack.resolve(arnForParameterName(stack, '/foo/bar', undefined)), {
43+
'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/foo/bar']]
44+
});
45+
test.done();
46+
},
47+
48+
'token parameterName and concrete physical name (no sep)'(test: Test) {
49+
const stack = new Stack();
50+
test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { physicalName: '/foo/bar' })), {
51+
'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'Boom' }]]
52+
});
53+
test.done();
54+
},
55+
56+
'token parameterName, explicit "" separator'(test: Test) {
57+
const stack = new Stack();
58+
test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { simpleName: false })), {
59+
'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'Boom' }]]
60+
});
61+
test.done();
62+
}
63+
64+
},
65+
66+
'fails if explicit separator is not defined and parameterName is a token'(test: Test) {
67+
const stack = new Stack();
68+
test.throws(() => arnForParameterName(stack, Token.asString({ Ref: 'Boom' })), /Unable to determine ARN separator for SSM parameter since the parameter name is an unresolved token. Use "fromAttributes" and specify "simpleName" explicitly/);
69+
test.done();
70+
}
71+
72+
}
73+
};

0 commit comments

Comments
 (0)
Please sign in to comment.