diff --git a/install.sh b/install.sh new file mode 100755 index 0000000000000..e753da03d08e7 --- /dev/null +++ b/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -euo pipefail +exec npx yarn install --frozen-lockfile diff --git a/packages/@aws-cdk/aws-ssm/lib/parameter.ts b/packages/@aws-cdk/aws-ssm/lib/parameter.ts index 4cbbabacdcffd..a0994ef843d5a 100644 --- a/packages/@aws-cdk/aws-ssm/lib/parameter.ts +++ b/packages/@aws-cdk/aws-ssm/lib/parameter.ts @@ -2,10 +2,11 @@ import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import { CfnDynamicReference, CfnDynamicReferenceService, CfnParameter, - Construct, ContextProvider, Fn, IConstruct, IResource, Resource, Stack, Token + Construct, ContextProvider, Fn, IResource, Resource, Stack, Token } from '@aws-cdk/core'; import cxapi = require('@aws-cdk/cx-api'); import ssm = require('./ssm.generated'); +import { arnForParameterName, AUTOGEN_MARKER } from './util'; /** * An SSM Parameter reference. @@ -94,6 +95,22 @@ export interface ParameterOptions { * @default - a name will be generated by CloudFormation */ readonly parameterName?: string; + + /** + * Indicates of the parameter name is a simple name (i.e. does not include "/" + * separators). + * + * This is only required only if `parameterName` is a token, which means we + * are unable to detect if the name is simple or "path-like" for the purpose + * of rendering SSM parameter ARNs. + * + * If `parameterName` is not specified, `simpleName` must be `true` (or + * undefined) since the name generated by AWS CloudFormation is always a + * simple name. + * + * @default - auto-detect based on `parameterName` + */ + readonly simpleName?: boolean; } /** @@ -184,12 +201,36 @@ export enum ParameterType { AWS_EC2_IMAGE_ID = 'AWS::EC2::Image::Id', } -export interface StringParameterAttributes { +/** + * Common attributes for string parameters. + */ +export interface CommonStringParameterAttributes { /** - * The name of the parameter store value + * The name of the parameter store value. + * + * This value can be a token or a concrete string. If it is a concrete string + * and includes "/" it must also be prefixed with a "/" (fully-qualified). */ readonly parameterName: string; + /** + * Indicates of the parameter name is a simple name (i.e. does not include "/" + * separators). + * + * This is only required only if `parameterName` is a token, which means we + * are unable to detect if the name is simple or "path-like" for the purpose + * of rendering SSM parameter ARNs. + * + * If `parameterName` is not specified, `simpleName` must be `true` (or + * undefined) since the name generated by AWS CloudFormation is always a + * simple name. + * + * @default - auto-detect based on `parameterName` + */ + readonly simpleName?: boolean; +} + +export interface StringParameterAttributes extends CommonStringParameterAttributes { /** * The version number of the value you wish to retrieve. * @@ -205,12 +246,7 @@ export interface StringParameterAttributes { readonly type?: ParameterType; } -export interface SecureStringParameterAttributes { - /** - * The name of the parameter store value - */ - readonly parameterName: string; - +export interface SecureStringParameterAttributes extends CommonStringParameterAttributes { /** * The version number of the value you wish to retrieve. This is required for secure strings. */ @@ -253,7 +289,7 @@ export class StringParameter extends ParameterBase implements IStringParameter { class Import extends ParameterBase { public readonly parameterName = attrs.parameterName; - public readonly parameterArn = arnForParameterName(this, this.parameterName); + public readonly parameterArn = arnForParameterName(this, attrs.parameterName, { simpleName: attrs.simpleName }); public readonly parameterType = type; public readonly stringValue = stringValue; } @@ -269,7 +305,7 @@ export class StringParameter extends ParameterBase implements IStringParameter { class Import extends ParameterBase { public readonly parameterName = attrs.parameterName; - public readonly parameterArn = arnForParameterName(this, this.parameterName); + public readonly parameterArn = arnForParameterName(this, attrs.parameterName, { simpleName: attrs.simpleName }); public readonly parameterType = ParameterType.SECURE_STRING; public readonly stringValue = stringValue; public readonly encryptionKey = attrs.encryptionKey; @@ -360,7 +396,10 @@ export class StringParameter extends ParameterBase implements IStringParameter { }); this.parameterName = this.getResourceNameAttribute(resource.ref); - this.parameterArn = arnForParameterName(this, this.parameterName); + this.parameterArn = arnForParameterName(this, this.parameterName, { + physicalName: props.parameterName || AUTOGEN_MARKER, + simpleName: props.simpleName + }); this.parameterType = resource.attrType; this.stringValue = resource.attrValue; @@ -413,7 +452,10 @@ export class StringListParameter extends ParameterBase implements IStringListPar value: props.stringListValue.join(','), }); this.parameterName = this.getResourceNameAttribute(resource.ref); - this.parameterArn = arnForParameterName(this, this.parameterName); + this.parameterArn = arnForParameterName(this, this.parameterName, { + physicalName: props.parameterName || AUTOGEN_MARKER, + simpleName: props.simpleName + }); this.parameterType = resource.attrType; this.stringListValue = Fn.split(',', resource.attrValue); @@ -442,20 +484,3 @@ function _assertValidValue(value: string, allowedPattern: string): void { function makeIdentityForImportedValue(parameterName: string) { return `SsmParameterValue:${parameterName}:C96584B6-F00A-464E-AD19-53AFF4B05118`; } - -function arnForParameterName(scope: IConstruct, parameterName: string): string { - - // remove trailing "/" if we can resolve parameter name. - if (!Token.isUnresolved(parameterName)) { - if (parameterName.startsWith('/')) { - parameterName = parameterName.substr(1); - } - } - - return Stack.of(scope).formatArn({ - service: 'ssm', - resource: 'parameter', - sep: '/', // Sep is empty because this.parameterName starts with a / already! - resourceName: parameterName, - }); -} diff --git a/packages/@aws-cdk/aws-ssm/lib/util.ts b/packages/@aws-cdk/aws-ssm/lib/util.ts new file mode 100644 index 0000000000000..660179eedff78 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/lib/util.ts @@ -0,0 +1,63 @@ +import { IConstruct, Stack, Token } from "@aws-cdk/core"; + +export const AUTOGEN_MARKER = '$$autogen$$'; + +export interface ArnForParameterNameOptions { + readonly physicalName?: string; + readonly simpleName?: boolean; +} + +/** + * Renders an ARN for an SSM parameter given a parameter name. + * @param scope definition scope + * @param parameterName the parameter name to include in the ARN + * @param physicalName optional physical name specified by the user (to auto-detect separator) + */ +export function arnForParameterName(scope: IConstruct, parameterName: string, options: ArnForParameterNameOptions = { }): string { + const physicalName = options.physicalName; + const nameToValidate = physicalName || parameterName; + + if (!Token.isUnresolved(nameToValidate) && nameToValidate.includes('/') && !nameToValidate.startsWith('/')) { + throw new Error(`Parameter names must be fully qualified (if they include "/" they must also begin with a "/"): ${nameToValidate}`); + } + + return Stack.of(scope).formatArn({ + service: 'ssm', + resource: 'parameter', + sep: isSimpleName() ? '/' : '', + resourceName: parameterName, + }); + + /** + * Determines the ARN separator for this parameter: if we have a concrete + * parameter name (or explicitly defined physical name), we will parse them + * and decide whether a "/" is needed or not. Otherwise, users will have to + * explicitly specify `simpleName` when they import the ARN. + */ + function isSimpleName(): boolean { + // look for a concrete name as a hint for determining the separator + const concreteName = !Token.isUnresolved(parameterName) ? parameterName : physicalName; + if (!concreteName || Token.isUnresolved(concreteName)) { + + if (options.simpleName === undefined) { + 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`); + } + + return options.simpleName; + } + + const result = !concreteName.startsWith('/'); + + // if users explicitly specify the separator and it conflicts with the one we need, it's an error. + if (options.simpleName !== undefined && options.simpleName !== result) { + + if (concreteName === AUTOGEN_MARKER) { + throw new Error(`If "parameterName" is not explicitly defined, "simpleName" must be "true" or undefined since auto-generated parameter names always have simple names`); + } + + 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}`); + } + + return result; + } +} diff --git a/packages/@aws-cdk/aws-ssm/test/integ.parameter-arns.expected.json b/packages/@aws-cdk/aws-ssm/test/integ.parameter-arns.expected.json new file mode 100644 index 0000000000000..1c4b0348b7888 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/integ.parameter-arns.expected.json @@ -0,0 +1,287 @@ +{ + "Parameters": { + "ParameterNameParameter": { + "Type": "String", + "Default": "myParamName" + } + }, + "Resources": { + "StringAutogenE7E896E4": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "hello, world" + } + }, + "StringSimpleA681514D": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "hello, world", + "Name": "simple-name" + } + }, + "StringPathD8120137": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "hello, world", + "Name": "/path/name/foo/bar" + } + }, + "ListAutogenC5DA1CAE": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "StringList", + "Value": "hello,world" + } + }, + "ListSimple9DB641CB": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "StringList", + "Value": "hello,world", + "Name": "list-simple-name" + } + }, + "ListPath120D6FAB": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "StringList", + "Value": "hello,world", + "Name": "/list/path/name" + } + }, + "ParameterizedSimpleB6311859": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "hello, world", + "Name": { + "Ref": "ParameterNameParameter" + } + } + }, + "ParameterizedNonSimple23C44BF6": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "hello, world", + "Name": { + "Fn::Join": [ + "", + [ + "/", + { + "Ref": "ParameterNameParameter" + }, + "/non/simple" + ] + ] + } + } + } + }, + "Outputs": { + "StringAutogenArn": { + "Value": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "StringAutogenE7E896E4" + } + ] + ] + } + }, + "StringSimpleArn": { + "Value": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "StringSimpleA681514D" + } + ] + ] + } + }, + "StringPathArn": { + "Value": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "StringPathD8120137" + } + ] + ] + } + }, + "ListAutogenArn": { + "Value": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "ListAutogenC5DA1CAE" + } + ] + ] + } + }, + "ListSimpleArn": { + "Value": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "ListSimple9DB641CB" + } + ] + ] + } + }, + "ListPathArn": { + "Value": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "ListPath120D6FAB" + } + ] + ] + } + }, + "ParameterizedSimpleArn": { + "Value": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "ParameterizedSimpleB6311859" + } + ] + ] + } + }, + "ParameterizedNonSimpleArn": { + "Value": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "ParameterizedNonSimple23C44BF6" + } + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/integ.parameter-arns.ts b/packages/@aws-cdk/aws-ssm/test/integ.parameter-arns.ts new file mode 100644 index 0000000000000..b5c99e2395d40 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/integ.parameter-arns.ts @@ -0,0 +1,25 @@ +// tslint:disable: max-line-length +import { App, CfnOutput, CfnParameter, Stack } from "@aws-cdk/core"; +import ssm = require('../lib'); + +const app = new App(); +const stack = new Stack(app, 'integ-parameter-arns'); + +const input = new CfnParameter(stack, 'ParameterNameParameter', { type: 'String', default: 'myParamName' }); + +const params = [ + new ssm.StringParameter(stack, 'StringAutogen', { stringValue: 'hello, world' }), + new ssm.StringParameter(stack, 'StringSimple', { stringValue: 'hello, world', parameterName: 'simple-name' }), + new ssm.StringParameter(stack, 'StringPath', { stringValue: 'hello, world', parameterName: '/path/name/foo/bar' }), + new ssm.StringListParameter(stack, 'ListAutogen', { stringListValue: [ 'hello', 'world' ] }), + new ssm.StringListParameter(stack, 'ListSimple', { stringListValue: [ 'hello', 'world' ], parameterName: 'list-simple-name' }), + new ssm.StringListParameter(stack, 'ListPath', { stringListValue: [ 'hello', 'world' ], parameterName: '/list/path/name' }), + new ssm.StringParameter(stack, 'ParameterizedSimple', { stringValue: 'hello, world', parameterName: input.valueAsString, simpleName: true }), + new ssm.StringParameter(stack, 'ParameterizedNonSimple', { stringValue: 'hello, world', parameterName: `/${input.valueAsString}/non/simple`, simpleName: false }), +]; + +for (const p of params) { + new CfnOutput(stack, `${p.node.id}Arn`, { value: p.parameterArn }); +} + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts b/packages/@aws-cdk/aws-ssm/test/test.parameter.ts index 6889bcdd743f8..b59dcf9781f3d 100644 --- a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts +++ b/packages/@aws-cdk/aws-ssm/test/test.parameter.ts @@ -1,3 +1,5 @@ +// tslint:disable: max-line-length + import { expect, haveResource } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); @@ -36,7 +38,7 @@ export = { // THEN test.throws(() => new ssm.StringParameter(stack, 'Parameter', { allowedPattern: '^Bar$', stringValue: 'FooBar' }), - /does not match the specified allowedPattern/); + /does not match the specified allowedPattern/); test.done(); }, @@ -46,9 +48,9 @@ export = { // THEN test.doesNotThrow(() => { - new ssm.StringParameter(stack, 'Parameter', { - allowedPattern: '^Bar$', - stringValue: cdk.Lazy.stringValue({ produce: () => 'Foo!' }), + new ssm.StringParameter(stack, 'Parameter', { + allowedPattern: '^Bar$', + stringValue: cdk.Lazy.stringValue({ produce: () => 'Foo!' }), }); }); test.done(); @@ -83,7 +85,7 @@ export = { // THEN test.throws(() => new ssm.StringListParameter(stack, 'Parameter', { stringListValue: ['Foo,Bar'] }), - /cannot contain the ',' character/); + /cannot contain the ',' character/); test.done(); }, @@ -93,7 +95,7 @@ export = { // THEN test.throws(() => new ssm.StringListParameter(stack, 'Parameter', { allowedPattern: '^(Foo|Bar)$', stringListValue: ['Foo', 'FooBar'] }), - /does not match the specified allowedPattern/); + /does not match the specified allowedPattern/); test.done(); }, @@ -130,6 +132,24 @@ export = { test.done(); }, + 'parameterName that includes a "/" must be fully qualified (i.e. begin with "/") as well'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + test.throws(() => new ssm.StringParameter(stack, 'myParam', { + stringValue: 'myValue', + parameterName: 'path/to/parameter', + }), /Parameter names must be fully qualified/); + + test.throws(() => new ssm.StringListParameter(stack, 'myParam2', { + stringListValue: [ 'foo', 'bar' ], + parameterName: 'path/to/parameter2' + }), /Parameter names must be fully qualified \(if they include \"\/\" they must also begin with a \"\/\"\)\: path\/to\/parameter2/); + + test.done(); + }, + 'StringParameter.fromStringParameterName'(test: Test) { // GIVEN const stack = new Stack(); @@ -139,14 +159,14 @@ export = { // THEN test.deepEqual(stack.resolve(param.parameterArn), { - 'Fn::Join': [ '', [ + 'Fn::Join': ['', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, - ':parameter/MyParamName' ] ] + ':parameter/MyParamName']] }); test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); test.deepEqual(stack.resolve(param.parameterType), 'String'); @@ -174,14 +194,14 @@ export = { // THEN test.deepEqual(stack.resolve(param.parameterArn), { - 'Fn::Join': [ '', [ + 'Fn::Join': ['', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, - ':parameter/MyParamName' ] ] + ':parameter/MyParamName']] }); test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); test.deepEqual(stack.resolve(param.parameterType), 'String'); @@ -201,14 +221,14 @@ export = { // THEN test.deepEqual(stack.resolve(param.parameterArn), { - 'Fn::Join': [ '', [ + 'Fn::Join': ['', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, - ':parameter/MyParamName' ] ] + ':parameter/MyParamName']] }); test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); test.deepEqual(stack.resolve(param.parameterType), 'SecureString'); @@ -348,25 +368,25 @@ export = { // THEN test.deepEqual(stack.resolve(param.parameterArn), { - 'Fn::Join': [ '', [ + 'Fn::Join': ['', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, - ':parameter/MyParamName' ] ] + ':parameter/MyParamName']] }); test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); test.deepEqual(stack.resolve(param.parameterType), 'StringList'); - test.deepEqual(stack.resolve(param.stringListValue), { 'Fn::Split': [ ',', '{{resolve:ssm:MyParamName}}' ] }); + test.deepEqual(stack.resolve(param.stringListValue), { 'Fn::Split': [',', '{{resolve:ssm:MyParamName}}'] }); test.done(); }, 'fromLookup will use the SSM context provider to read value during synthesis'(test: Test) { // GIVEN const app = new App(); - const stack = new Stack(app, 'my-staq', { env: { region: 'us-east-1', account: '12344' }}); + const stack = new Stack(app, 'my-staq', { env: { region: 'us-east-1', account: '12344' } }); // WHEN const value = ssm.StringParameter.valueFromLookup(stack, 'my-param-name'); @@ -450,38 +470,104 @@ export = { 'rendering of parameter arns'(test: Test) { const stack = new Stack(); const param = new CfnParameter(stack, 'param'); - const expectedA = { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/bam' ] ] }; - const expectedB = { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'param' } ] ] }; + const expectedA = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/bam'] ] }; + const expectedB = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'param' } ] ] }; + const expectedC = { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'param' } ] ] }; let i = 0; // WHEN const case1 = ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, 'bam'); const case2 = ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, '/bam'); - const case3 = ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, param.valueAsString); const case4 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: 'bam' }); const case5 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: '/bam' }); - const case6 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString }); + const case6 = ssm.StringParameter.fromStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, simpleName: true }); const case7 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: 'bam', version: 10 }); const case8 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: '/bam', version: 10 }); - const case9 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, version: 10 }); + const case9 = ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, version: 10, simpleName: false }); + + // auto-generated name is always generated as a "simple name" (not/a/path) const case10 = new ssm.StringParameter(stack, `p${i++}`, { stringValue: 'value' }); + // explicitly named physical name gives us a hint on how to render the ARN + const case11 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: '/foo/bar', stringValue: 'hello' }); + const case12 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: 'simple-name', stringValue: 'hello' }); + + const case13 = new ssm.StringListParameter(stack, `p${i++}`, { stringListValue: [ 'hello', 'world' ] }); + const case14 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: '/not/simple', stringListValue: [ 'hello', 'world' ] }); + const case15 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: 'simple', stringListValue: [ 'hello', 'world' ] }); + // THEN test.deepEqual(stack.resolve(case1.parameterArn), expectedA); test.deepEqual(stack.resolve(case2.parameterArn), expectedA); - test.deepEqual(stack.resolve(case3.parameterArn), expectedB); test.deepEqual(stack.resolve(case4.parameterArn), expectedA); test.deepEqual(stack.resolve(case5.parameterArn), expectedA); test.deepEqual(stack.resolve(case6.parameterArn), expectedB); test.deepEqual(stack.resolve(case7.parameterArn), expectedA); test.deepEqual(stack.resolve(case8.parameterArn), expectedA); - test.deepEqual(stack.resolve(case9.parameterArn), expectedB); - test.deepEqual(stack.resolve(case10.parameterArn), { - 'Fn::Join': [ '', [ - 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p97A508212' } - ] - ] }); + test.deepEqual(stack.resolve(case9.parameterArn), expectedC); + + // new ssm.Parameters determine if "/" is needed based on the posture of `parameterName`. + test.deepEqual(stack.resolve(case10.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p81BB0F6FE' } ] ] }); + test.deepEqual(stack.resolve(case11.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p97A508212' } ] ] }); + test.deepEqual(stack.resolve(case12.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p107D6B8AB0' } ] ] }); + test.deepEqual(stack.resolve(case13.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p118A9CB02C' } ] ] }); + test.deepEqual(stack.resolve(case14.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p129BE4CE91' } ] ] }); + test.deepEqual(stack.resolve(case15.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p1326A2AEC4' } ] ] }); + + test.done(); + }, + + 'if parameterName is a token separator must be specified'(test: Test) { + // GIVEN + const stack = new Stack(); + const param = new CfnParameter(stack, 'param'); + let i = 0; + + // WHEN + const p1 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo', simpleName: true }); + const p2 = new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo', simpleName: false }); + const p3 = new ssm.StringListParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringListValue: [ 'foo' ], simpleName: false }); + + // THEN + test.deepEqual(stack.resolve(p1.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'p0B02A8F65' } ] ] }); + test.deepEqual(stack.resolve(p2.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p1E43AD5AC' } ] ] }); + test.deepEqual(stack.resolve(p3.parameterArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'p2C1903AEB' } ] ] }); + + test.done(); + }, + + 'fails if name is a token and no explicit separator'(test: Test) { + // GIVEN + const stack = new Stack(); + const param = new CfnParameter(stack, 'param'); + let i = 0; + // THEN + const expected = /Unable to determine ARN separator for SSM parameter since the parameter name is an unresolved token. Use "fromAttributes" and specify "simpleName" explicitly/; + test.throws(() => ssm.StringParameter.fromStringParameterName(stack, `p${i++}`, param.valueAsString), expected); + test.throws(() => ssm.StringParameter.fromSecureStringParameterAttributes(stack, `p${i++}`, { parameterName: param.valueAsString, version: 1 }), expected); + test.throws(() => new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo' }), expected); + test.throws(() => new ssm.StringParameter(stack, `p${i++}`, { parameterName: param.valueAsString, stringValue: 'foo' }), expected); + test.done(); + }, + + 'fails if simpleName is wrong based on a concrete physical name'(test: Test) { + // GIVEN + const stack = new Stack(); + let i = 0; + + // THEN + 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/); + 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/); + test.done(); + }, + + 'fails if parameterName is undefined and simpleName is "false"'(test: Test) { + // GIVEN + const stack = new Stack(); + + // THEN + 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/); test.done(); } }; diff --git a/packages/@aws-cdk/aws-ssm/test/test.util.ts b/packages/@aws-cdk/aws-ssm/test/test.util.ts new file mode 100644 index 0000000000000..2f11117384415 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/test.util.ts @@ -0,0 +1,73 @@ +// tslint:disable: max-line-length + +import { Stack, Token } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import { arnForParameterName } from '../lib/util'; + +export = { + arnForParameterName: { + + 'simple names': { + + 'concrete parameterName and no physical name (sep is "/")'(test: Test) { + const stack = new Stack(); + test.deepEqual(stack.resolve(arnForParameterName(stack, 'myParam', undefined)), { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/myParam']] + }); + test.done(); + }, + + 'token parameterName and concrete physical name (no additional "/")'(test: Test) { + const stack = new Stack(); + test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { physicalName: 'myParam' })), { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'Boom' }]] + }); + test.done(); + }, + + 'token parameterName, explicit "/" separator'(test: Test) { + const stack = new Stack(); + test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { simpleName: true })), { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/', { Ref: 'Boom' }]] + }); + test.done(); + } + + }, + + 'path names': { + + 'concrete parameterName and no physical name (sep is "/")'(test: Test) { + const stack = new Stack(); + test.deepEqual(stack.resolve(arnForParameterName(stack, '/foo/bar', undefined)), { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter/foo/bar']] + }); + test.done(); + }, + + 'token parameterName and concrete physical name (no sep)'(test: Test) { + const stack = new Stack(); + test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { physicalName: '/foo/bar' })), { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'Boom' }]] + }); + test.done(); + }, + + 'token parameterName, explicit "" separator'(test: Test) { + const stack = new Stack(); + test.deepEqual(stack.resolve(arnForParameterName(stack, Token.asString({ Ref: 'Boom' }), { simpleName: false })), { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':parameter', { Ref: 'Boom' }]] + }); + test.done(); + } + + }, + + 'fails if explicit separator is not defined and parameterName is a token'(test: Test) { + const stack = new Stack(); + 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/); + test.done(); + } + + } +}; \ No newline at end of file