Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
37 changes: 37 additions & 0 deletions packages/aws-cdk-lib/aws-ecs/test/container-definition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2018,6 +2018,43 @@ describe('container definition', () => {
});
});

test('ecs.Secret.fromSsmParameter does not leave an unreferenced SSM Parameter in the template (regression for #38396)', () => {
// GIVEN
const stack = new cdk.Stack();
const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef');

// Only the ARN is consumed below — stringValue is never read.
const parameter = ssm.StringParameter.fromStringParameterName(stack, 'Param', '/path-to/mongodb-uri');

taskDefinition.addContainer('web', {
image: ecs.ContainerImage.fromRegistry('nginx'),
secrets: {
MONGO_URI: ecs.Secret.fromSsmParameter(parameter),
},
});

// THEN — no Parameters section entry was synthesized for the imported SSM parameter,
// yet the secret still resolves to its ARN as expected.
const template = Template.fromStack(stack);
template.templateMatches(Match.not(Match.objectLike({
Parameters: Match.objectLike({
ParamParameter: Match.anyValue(),
}),
})));
template.hasResourceProperties('AWS::ECS::TaskDefinition', {
ContainerDefinitions: [
Match.objectLike({
Secrets: [
Match.objectLike({
Name: 'MONGO_URI',
ValueFrom: stack.resolve(parameter.parameterArn),
}),
],
}),
],
});
});

test('use a specific secret JSON key as environment variable', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down
68 changes: 46 additions & 22 deletions packages/aws-cdk-lib/aws-ssm/lib/parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,13 +542,23 @@ export class StringParameter extends ParameterBase implements IStringParameter {

const parameterType = ParameterValueType.STRING;

let stringValue: string;
stringValue = new CfnParameter(scope, `${id}.Parameter`, { type: `AWS::SSM::Parameter::Value<${parameterType}>`, default: stringParameterArn }).valueAsString;
class Import extends ParameterBase {
public readonly parameterName = stringParameterArn.split('/').pop()?.replace(/parameter\/$/, '') ?? '';
public readonly parameterArn = stringParameterArn;
public readonly parameterType = parameterType;
public readonly stringValue = stringValue;
private _stringValue?: string;

// Lazy — see fromStringParameterAttributes() below for why this is done this way,
// including why it's safe if first read happens inside a Lazy producer.
public get stringValue(): string {
if (this._stringValue === undefined) {
this._stringValue = new CfnParameter(scope, `${id}.Parameter`, {
type: `AWS::SSM::Parameter::Value<${parameterType}>`,
default: stringParameterArn,
}).valueAsString;
}
return this._stringValue;
}
}

return new Import(scope, id);
Expand All @@ -568,23 +578,30 @@ export class StringParameter extends ParameterBase implements IStringParameter {
const type = attrs.type ?? attrs.valueType ?? ParameterValueType.STRING;
const forceDynamicReference = attrs.forceDynamicReference ?? false;

let stringValue: string;
if (attrs.version) {
stringValue = new CfnDynamicReference(CfnDynamicReferenceService.SSM, `${attrs.parameterName}:${Tokenization.stringifyNumber(attrs.version)}`).toString();
} else if (forceDynamicReference) {
stringValue = new CfnDynamicReference(CfnDynamicReferenceService.SSM, attrs.parameterName).toString();
} else if (Token.isUnresolved(attrs.parameterName) && Fn._isFnBase(Tokenization.reverseString(attrs.parameterName).firstToken)) {
// the default value of a CfnParameter can only contain strings, so we cannot use it when a parameter name contains tokens.
stringValue = new CfnDynamicReference(CfnDynamicReferenceService.SSM, attrs.parameterName).toString();
} else {
stringValue = new CfnParameter(scope, `${id}.Parameter`, { type: `AWS::SSM::Parameter::Value<${type}>`, default: attrs.parameterName }).valueAsString;
}

class Import extends ParameterBase {
public readonly parameterName = attrs.parameterName;
public readonly parameterArn = arnForParameterName(this, attrs.parameterName, { simpleName: attrs.simpleName });
public readonly parameterType = ParameterType.STRING; // this is the type returned by CFN @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-parameter.html#aws-resource-ssm-parameter-return-values
public readonly stringValue = stringValue;
public readonly parameterType = ParameterValueType.STRING; // this is the type returned by CFN @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-parameter.html#aws-resource-ssm-parameter-return-values
private _stringValue?: string;

// Created lazily so ARN-only consumers don't leave an unreferenced Parameter behind.
// Safe even from a Lazy producer — CDK resolves all CfnElement props, Lazy included,
// before rendering the template.
public get stringValue(): string {
if (this._stringValue === undefined) {
if (attrs.version) {
this._stringValue = new CfnDynamicReference(CfnDynamicReferenceService.SSM, `${attrs.parameterName}:${Tokenization.stringifyNumber(attrs.version)}`).toString();
} else if (forceDynamicReference) {
this._stringValue = new CfnDynamicReference(CfnDynamicReferenceService.SSM, attrs.parameterName).toString();
} else if (Token.isUnresolved(attrs.parameterName) && Fn._isFnBase(Tokenization.reverseString(attrs.parameterName).firstToken)) {
// the default value of a CfnParameter can only contain strings, so we cannot use it when a parameter name contains tokens.
this._stringValue = new CfnDynamicReference(CfnDynamicReferenceService.SSM, attrs.parameterName).toString();
} else {
this._stringValue = new CfnParameter(scope, `${id}.Parameter`, { type: `AWS::SSM::Parameter::Value<${type}>`, default: attrs.parameterName }).valueAsString;
}
}
return this._stringValue;
}
}

return new Import(scope, id);
Expand Down Expand Up @@ -786,15 +803,22 @@ export class StringListParameter extends ParameterBase implements IStringListPar
const type = attrs.elementType ?? ParameterValueType.STRING;
const valueType = `List<${type}>`;

const stringValue = attrs.version
? new CfnDynamicReference(CfnDynamicReferenceService.SSM, `${attrs.parameterName}:${Tokenization.stringifyNumber(attrs.version)}`).toStringList()
: new CfnParameter(scope, `${id}.Parameter`, { type: `AWS::SSM::Parameter::Value<${valueType}>`, default: attrs.parameterName }).valueAsList;

class Import extends ParameterBase {
public readonly parameterName = attrs.parameterName;
public readonly parameterArn = arnForParameterName(this, attrs.parameterName, { simpleName: attrs.simpleName });
public readonly parameterType = valueType; // it doesn't really matter what this is since a CfnParameter can only be `String | StringList`
public readonly stringListValue = stringValue;
private _stringListValue?: string[];

// Lazy — see StringParameter.fromStringParameterAttributes() for why this is done this
// way, including why it's safe if first read happens inside a Lazy producer.
public get stringListValue(): string[] {
if (this._stringListValue === undefined) {
this._stringListValue = attrs.version
? new CfnDynamicReference(CfnDynamicReferenceService.SSM, `${attrs.parameterName}:${Tokenization.stringifyNumber(attrs.version)}`).toStringList()
: new CfnParameter(scope, `${id}.Parameter`, { type: `AWS::SSM::Parameter::Value<${valueType}>`, default: attrs.parameterName }).valueAsList;
}
return this._stringListValue;
}
}

return new Import(scope, id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ test('can reference SSMPS string - latest version', () => {
});

// THEN
expect(stack.resolve(ref.stringValue)).toEqual({ Ref: 'RefParameter' });
Template.fromStack(stack).templateMatches({
Parameters: {
RefParameter: {
Expand All @@ -34,8 +35,6 @@ test('can reference SSMPS string - latest version', () => {
},
},
});

expect(stack.resolve(ref.stringValue)).toEqual({ Ref: 'RefParameter' });
});

test('can reference SSMPS secure string', () => {
Expand Down
83 changes: 80 additions & 3 deletions packages/aws-cdk-lib/aws-ssm/test/parameter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,54 @@ test('StringParameter.fromStringParameterName', () => {
});
});

test('StringParameter.fromStringParameterName still synthesizes correctly when stringValue is first read inside a Lazy callback', () => {
// GIVEN
const stack = new cdk.Stack();
const param = ssm.StringParameter.fromStringParameterName(stack, 'MyParamName', 'MyParamName');

// WHEN
// First read of stringValue happens inside a Lazy producer instead of application code.
new cdk.CfnOutput(stack, 'Out', {
value: cdk.Lazy.string({ produce: () => param.stringValue }),
});

// THEN
const template = Template.fromStack(stack);
template.hasParameter('MyParamNameParameter', {
Type: 'AWS::SSM::Parameter::Value<String>',
Default: 'MyParamName',
});
template.hasOutput('Out', {
Value: { Ref: 'MyParamNameParameter' },
});
});

test('StringParameter.fromStringParameterName does not create a Parameter when only the ARN is used', () => {
// GIVEN
const stack = new cdk.Stack();

// WHEN
// Only parameterArn is used, `stringValue` is never accessed (e.g. ecs.Secret.fromSsmParameter)
const param = ssm.StringParameter.fromStringParameterName(stack, 'MyParamName', 'MyParamName');
expect(stack.resolve(param.parameterArn)).toBeDefined();

// THEN
Template.fromStack(stack).templateMatches({});
});

test('StringParameter.fromStringParameterArn does not create a Parameter when only the ARN is used', () => {
// GIVEN
const stack = new cdk.Stack();
const sharingParameterArn = 'arn:aws:ssm:us-east-1:123456789012:parameter/dummyName';

// WHEN
const param = ssm.StringParameter.fromStringParameterArn(stack, 'MyParamName', sharingParameterArn);
expect(stack.resolve(param.parameterArn)).toEqual(sharingParameterArn);

// THEN
Template.fromStack(stack).templateMatches({});
});

test('fromStringParameterArn StringParameter.fromStringParameterArn', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down Expand Up @@ -814,10 +862,11 @@ describe('from string list parameter', () => {
const stack = new cdk.Stack();

// WHEN
ssm.StringParameter.fromStringParameterAttributes(stack, 'my-param-name', {
const param = ssm.StringParameter.fromStringParameterAttributes(stack, 'my-param-name', {
parameterName: 'my-param-name',
type: ParameterType.STRING,
});
param.stringValue;

// THEN
Template.fromStack(stack).templateMatches({
Expand All @@ -835,10 +884,11 @@ describe('from string list parameter', () => {
const stack = new cdk.Stack();

// WHEN
ssm.StringParameter.fromStringParameterAttributes(stack, 'my-param-name', {
const param = ssm.StringParameter.fromStringParameterAttributes(stack, 'my-param-name', {
parameterName: 'my-param-name',
valueType: ParameterValueType.STRING,
});
param.stringValue;

// THEN
Template.fromStack(stack).templateMatches({
Expand All @@ -851,6 +901,19 @@ describe('from string list parameter', () => {
});
});

test('fromStringParameterAttributes does not create a Parameter when stringValue is never accessed', () => {
// GIVEN
const stack = new cdk.Stack();

// WHEN
ssm.StringParameter.fromStringParameterAttributes(stack, 'my-param-name', {
parameterName: 'my-param-name',
});

// THEN
Template.fromStack(stack).templateMatches({});
});

test('valueForTypedListParameter returns correct value', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down Expand Up @@ -892,9 +955,10 @@ describe('from string list parameter', () => {
const stack = new cdk.Stack();

// WHEN
ssm.StringListParameter.fromListParameterAttributes(stack, 'my-param-name', {
const param = ssm.StringListParameter.fromListParameterAttributes(stack, 'my-param-name', {
parameterName: 'my-param-name',
});
param.stringListValue;

// THEN
Template.fromStack(stack).templateMatches({
Expand All @@ -907,6 +971,19 @@ describe('from string list parameter', () => {
});
});

test('fromListParameterAttributes does not create a Parameter when stringListValue is never accessed', () => {
// GIVEN
const stack = new cdk.Stack();

// WHEN
ssm.StringListParameter.fromListParameterAttributes(stack, 'my-param-name', {
parameterName: 'my-param-name',
});

// THEN
Template.fromStack(stack).templateMatches({});
});

testDeprecated('string type returns correct value', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down
Loading