Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 22 additions & 23 deletions packages/@aws-cdk/aws-ssm/lib/parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } from './util';

/**
* An SSM Parameter reference.
Expand Down Expand Up @@ -186,10 +187,25 @@ export enum ParameterType {

export interface StringParameterAttributes {
/**
* 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;

/**
* Determines the separator used to render the ARN for the SSM parameter.
* Valid values are `"/"` or `""`.
*
* If `parameterName` is a path (i.e. begins with "/"), the separator must be
* `""`. Otherwise, it must be `"/"`.
*
* @default - automatically determined based on the value of `parameterName`
* unless it is a token, in which case this field is required.
*/
readonly parameterArnSeparator?: string;
Comment thread
eladb marked this conversation as resolved.
Outdated

/**
* The version number of the value you wish to retrieve.
*
Expand Down Expand Up @@ -253,7 +269,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, undefined);
public readonly parameterType = type;
public readonly stringValue = stringValue;
}
Expand All @@ -269,7 +285,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, undefined);
public readonly parameterType = ParameterType.SECURE_STRING;
public readonly stringValue = stringValue;
public readonly encryptionKey = attrs.encryptionKey;
Expand Down Expand Up @@ -360,7 +376,7 @@ 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, props.parameterName || 'autogen');

this.parameterType = resource.attrType;
this.stringValue = resource.attrValue;
Expand Down Expand Up @@ -413,7 +429,7 @@ 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, props.parameterName || 'autogen');

this.parameterType = resource.attrType;
this.stringListValue = Fn.split(',', resource.attrValue);
Expand Down Expand Up @@ -442,20 +458,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,
});
}
71 changes: 71 additions & 0 deletions packages/@aws-cdk/aws-ssm/lib/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { CfnCondition, Construct, Fn, IConstruct, Stack, Token } from "@aws-cdk/core";

/**
* 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, physicalName?: string): string {
const { sep, resourceName } = determineSepAndResourceName();

validateParameterName(physicalName || parameterName);

return Stack.of(scope).formatArn({
service: 'ssm',
resource: 'parameter',
sep,
resourceName,
});

function validateParameterName(concreteName: string) {
// can't validate tokens
if (Token.isUnresolved(concreteName)) {
return;
}

if (concreteName.includes('/') && !concreteName.startsWith('/')) {
throw new Error(`Parameter names must be fully qualified (if they include "/" they must also begin with a "/"): ${concreteName}`);
}
}

function determineSepAndResourceName() {
// if the parameter name is a token
if (Token.isUnresolved(parameterName)) {

// if we have a concrete physical name, we can use it to determine the separator
if (physicalName && !Token.isUnresolved(physicalName)) {
return {
sep: physicalName.startsWith('/') ? '' : '/',
resourceName: parameterName
};
}

// parameterName is a token and physical name is not helping us (either missing or a token itself)
// in this use case we will need to synthesize a CloudFormation condition that will be used to determine
// if the name has a "/" prefix or not.
const startsWithSlash = startsWithCondition(scope as Construct, parameterName, "/");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh shit. After thinking about it some more, I'm not sure this is going to work. At least, this won't work for { Refs }s, as Conditions need to evaluate before Resources are.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right:

 ❌  integ-parameter-arns failed: ValidationError: Template format error: Unresolved dependencies [StringDeployTime8EC8E402]. Cannot reference resources in the Conditions block of the template

return {
sep: '',
resourceName: Token.asString(Fn.conditionIf(startsWithSlash.logicalId, parameterName, `/${parameterName}`))
};
}

// parameterName is concrete, use it to determine the token
return {
sep: parameterName.startsWith('/') ? '' : '/',
resourceName: parameterName
};
}
}

/**
* Gets or creates a CloudFormation condition that evaluates to "TRUE" if `parameterName` (treated as an opaque token)
* starts with a "/".
*/
function startsWithCondition(scope: Construct, value: string, startsWith: string) {
const id = `AWS::CDK::StartsWith(${startsWith})`;
return scope.node.tryFindChild(id) as CfnCondition || new CfnCondition(scope, id, {
expression: Fn.conditionEquals(Fn.select(0, Fn.split(startsWith, value)), "")
});
}
202 changes: 202 additions & 0 deletions packages/@aws-cdk/aws-ssm/test/integ.parameter-arns.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
{
"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"
}
}
},
"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"
}
]
]
}
}
}
}
Loading