From 8f7ee2ba58b38f3f6d9eb8bebd96c208c3d7d2ce Mon Sep 17 00:00:00 2001 From: Cory Hall <43035978+corymhall@users.noreply.github.com> Date: Thu, 15 Sep 2022 09:23:48 -0400 Subject: [PATCH] feat(ssm): reference existing SSM list parameters (#21880) This PR fixes some issues with how SSM parameter types are implemented. Currently this module models a single type of parameter (`ParameterType` enum) and that type is used to represent _both_ [CloudFormation SSM Parameter types](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-ssm-parameter-types) For example, ```ts new cdk.CfnParameter(this, 'Param', { type: 'AWS::SSM::Parameter::Value', // type }); ``` _and_ the [AWS::SSM::Parameter.type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-parameter.html#cfn-ssm-parameter-type) For example, ```ts new ssm.CfnParameter(this, 'Param', { type: 'String', }); ``` This overloading caused the issue in the referenced issue as well as making it more confusing for the user. For example, You can specify a type when creating a `StringParameter`, but you shouldn't need to since the only valid values are `String | StringList` and these are modeled as two separate classes `StringParameter & StringListParameter`. To address this, the PR introduces a new enum `ParameterValueType` to model the CloudFormation SSM Parameter Types. This enum is only used in the `valueForXXX` and `fromXXX` methods since those return a CFN parameter. - Deprecated `ssm.StringParameter.valueForTypedStringParameter` since it uses the old overloaded `ParameterType`. - Introduce a new `ssm.StringParameter.valueForTypedStringParameterV2` that uses the new `ParameterValueType` enum - Add `ssm.StringListParameter.valueForTypedListParameter` - Add `ssm.StringListParameter.fromListParameterAttributes` - Deprecated `StringParameterProps.type` since the value should only be `String`. fix #12477, #14364 ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-ec2/lib/machine-image.ts | 4 +- packages/@aws-cdk/aws-ecs/lib/amis.ts | 2 +- packages/@aws-cdk/aws-ssm/README.md | 13 + packages/@aws-cdk/aws-ssm/lib/parameter.ts | 204 +++++- packages/@aws-cdk/aws-ssm/package.json | 1 + .../aws-ssm/test/integ.list-parameter.ts | 85 +++ .../index.js | 611 ++++++++++++++++++ .../base.assets.json | 19 + .../base.template.json | 59 ++ .../list-parameter.integ.snapshot/cdk.out | 1 + .../list-parameter.integ.snapshot/integ.json | 12 + .../list-param.assets.json | 19 + .../list-param.template.json | 129 ++++ .../manifest.json | 244 +++++++ ...efaultTestDeployAssert9C612E37.assets.json | 32 + ...aultTestDeployAssert9C612E37.template.json | 160 +++++ .../@aws-cdk/aws-ssm/test/parameter.test.ts | 164 ++++- .../@aws-cdk/core/lib/private/intrinsic.ts | 11 + 18 files changed, 1748 insertions(+), 22 deletions(-) create mode 100644 packages/@aws-cdk/aws-ssm/test/integ.list-parameter.ts create mode 100644 packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/asset.2a53dc40a7dae81c8850e125ab49e5f55d80b7b8ceac86976f2a4119393cab72.bundle/index.js create mode 100644 packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/base.assets.json create mode 100644 packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/base.template.json create mode 100644 packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/cdk.out create mode 100644 packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/integ.json create mode 100644 packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/list-param.assets.json create mode 100644 packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/list-param.template.json create mode 100644 packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/manifest.json create mode 100644 packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/ssmstringparamDefaultTestDeployAssert9C612E37.assets.json create mode 100644 packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/ssmstringparamDefaultTestDeployAssert9C612E37.template.json diff --git a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts index 9dd3d9d665455..4e1b87503004b 100644 --- a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts +++ b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts @@ -165,7 +165,7 @@ export class GenericSSMParameterImage implements IMachineImage { * Return the image to use in the given context */ public getImage(scope: Construct): MachineImageConfig { - const ami = ssm.StringParameter.valueForTypedStringParameter(scope, this.parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); + const ami = ssm.StringParameter.valueForTypedStringParameterV2(scope, this.parameterName, ssm.ParameterValueType.AWS_EC2_IMAGE_ID); return { imageId: ami, osType: this.os, @@ -732,5 +732,5 @@ export interface LookupMachineImageProps { function lookupImage(scope: Construct, cachedInContext: boolean | undefined, parameterName: string) { return cachedInContext ? ssm.StringParameter.valueFromLookup(scope, parameterName) - : ssm.StringParameter.valueForTypedStringParameter(scope, parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); + : ssm.StringParameter.valueForTypedStringParameterV2(scope, parameterName, ssm.ParameterValueType.AWS_EC2_IMAGE_ID); } diff --git a/packages/@aws-cdk/aws-ecs/lib/amis.ts b/packages/@aws-cdk/aws-ecs/lib/amis.ts index cbe95cd8fd2c1..7ce0a447c8bf8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/amis.ts +++ b/packages/@aws-cdk/aws-ecs/lib/amis.ts @@ -369,5 +369,5 @@ export class BottleRocketImage implements ec2.IMachineImage { function lookupImage(scope: Construct, cachedInContext: boolean | undefined, parameterName: string) { return cachedInContext ? ssm.StringParameter.valueFromLookup(scope, parameterName) - : ssm.StringParameter.valueForTypedStringParameter(scope, parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); + : ssm.StringParameter.valueForTypedStringParameterV2(scope, parameterName, ssm.ParameterValueType.AWS_EC2_IMAGE_ID); } diff --git a/packages/@aws-cdk/aws-ssm/README.md b/packages/@aws-cdk/aws-ssm/README.md index 406bab8b9f804..a96bf50d7d752 100644 --- a/packages/@aws-cdk/aws-ssm/README.md +++ b/packages/@aws-cdk/aws-ssm/README.md @@ -20,6 +20,19 @@ your CDK app by using `ssm.StringParameter.fromStringParameterAttributes`: [using SSM parameter](test/integ.parameter-store-string.lit.ts) +You can also reference an existing SSM Parameter Store value that matches an +[AWS specific parameter type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-specific-parameter-types): + +```ts +ssm.StringParameter.valueForTypedStringParameterV2(stack, '/My/Public/Parameter', ssm.ParameterValueType.AWS_EC2_IMAGE_ID); +``` + +To do the same for a SSM Parameter Store value that is stored as a list: + +```ts +ssm.StringListParameter.valueForTypedListParameter(stack, '/My/Public/Parameter', ssm.ParameterValueType.AWS_EC2_IMAGE_ID); +``` + ### Lookup existing parameters You can also use an existing parameter by looking up the parameter from the AWS environment. diff --git a/packages/@aws-cdk/aws-ssm/lib/parameter.ts b/packages/@aws-cdk/aws-ssm/lib/parameter.ts index 9d4edf34401b8..f24e4d0375108 100644 --- a/packages/@aws-cdk/aws-ssm/lib/parameter.ts +++ b/packages/@aws-cdk/aws-ssm/lib/parameter.ts @@ -135,6 +135,7 @@ export interface StringParameterProps extends ParameterOptions { * The type of the string parameter * * @default ParameterType.STRING + * @deprecated - type will always be 'String' */ readonly type?: ParameterType; @@ -199,8 +200,75 @@ abstract class ParameterBase extends Resource implements IParameter { } } +/** + * The type of CFN SSM Parameter + * + * Using specific types can be helpful in catching invalid values + * at the start of creating or updating a stack. CloudFormation validates + * the values against existing values in the account. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-ssm-parameter-types + */ +export enum ParameterValueType { + /** + * String + */ + STRING = 'String', + + /** + * An Availability Zone, such as us-west-2a. + */ + AWS_EC2_AVAILABILITYZONE_NAME = 'AWS::EC2::AvailabilityZone::Name', + + /** + * An Amazon EC2 image ID, such as ami-0ff8a91507f77f867. + */ + AWS_EC2_IMAGE_ID = 'AWS::EC2::Image::Id', + + /** + * An Amazon EC2 instance ID, such as i-1e731a32. + */ + AWS_EC2_INSTANCE_ID = 'AWS::EC2::Instance::Id', + + /** + * An Amazon EC2 key pair name. + */ + AWS_EC2_KEYPAIR_KEYNAME = 'AWS::EC2::KeyPair::KeyName', + + /** + * An EC2-Classic or default VPC security group name, such as my-sg-abc. + */ + AWS_EC2_SECURITYGROUP_GROUPNAME = 'AWS::EC2::SecurityGroup::GroupName', + + /** + * A security group ID, such as sg-a123fd85. + */ + AWS_EC2_SECURITYGROUP_ID = 'AWS::EC2::SecurityGroup::Id', + + /** + * A subnet ID, such as subnet-123a351e. + */ + AWS_EC2_SUBNET_ID = 'AWS::EC2::Subnet::Id', + + /** + * An Amazon EBS volume ID, such as vol-3cdd3f56. + */ + AWS_EC2_VOLUME_ID = 'AWS::EC2::Volume::Id', + + /** + * A VPC ID, such as vpc-a123baa3. + */ + AWS_EC2_VPC_ID = 'AWS::EC2::VPC::Id', + + /** + * An Amazon Route 53 hosted zone ID, such as Z23YXV4OVPL04A. + */ + AWS_ROUTE53_HOSTEDZONE_ID = 'AWS::Route53::HostedZone::Id', +} + /** * SSM parameter type + * @deprecated these types are no longer used */ export enum ParameterType { /** @@ -302,8 +370,55 @@ export interface StringParameterAttributes extends CommonStringParameterAttribut * The type of the string parameter * * @default ParameterType.STRING + * @deprecated - use valueType instead */ readonly type?: ParameterType; + + /** + * The type of the string parameter value + * + * Using specific types can be helpful in catching invalid values + * at the start of creating or updating a stack. CloudFormation validates + * the values against existing values in the account. + * + * Note - if you want to allow values from different AWS accounts, use + * ParameterValueType.STRING + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-ssm-parameter-types + * + * @default ParameterValueType.STRING + */ + readonly valueType?: ParameterValueType; +} + +/** + * Attributes for parameters of string list type. + * + * @see ParameterType + */ +export interface ListParameterAttributes extends CommonStringParameterAttributes { + /** + * The version number of the value you wish to retrieve. + * + * @default The latest version will be retrieved. + */ + readonly version?: number; + + /** + * The type of the string list parameter value. + * + * Using specific types can be helpful in catching invalid values + * at the start of creating or updating a stack. CloudFormation validates + * the values against existing values in the account. + * + * Note - if you want to allow values from different AWS accounts, use + * ParameterValueType.STRING + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-ssm-parameter-types + * + * @default ParameterValueType.STRING + */ + readonly elementType?: ParameterValueType; } /** @@ -331,25 +446,10 @@ export interface SecureStringParameterAttributes extends CommonStringParameterAt * @resource AWS::SSM::Parameter * * @example - * * const ssmParameter = new ssm.StringParameter(this, 'mySsmParameter', { * parameterName: 'mySsmParameter', * stringValue: 'mySsmParameterValue', - * type: ssm.ParameterType.STRING, * }); - * - * const secureParameter = new ssm.StringParameter(this, 'mySecretParameter', { - * parameterName: 'mySecretParameter', - * stringValue: 'mySecretParameterValue', - * type: ssm.ParameterType.SECURE_STRING, - * }); - * - * const listParameter = new ssm.StringParameter(this, 'myListParameter', { - * parameterName: 'myListParameter', - * stringValue: ["myListParameterValue1", "myListParameterValue2"], - * type: ssm.ParameterType.STRING_LIST, - * }); - * */ export class StringParameter extends ParameterBase implements IStringParameter { @@ -367,8 +467,11 @@ export class StringParameter extends ParameterBase implements IStringParameter { if (!attrs.parameterName) { throw new Error('parameterName cannot be an empty string'); } + if (attrs.type && ![ParameterType.STRING, ParameterType.AWS_EC2_IMAGE_ID].includes(attrs.type)) { + throw new Error(`fromStringParameterAttributes does not support ${attrs.type}. Please use ParameterType.STRING or ParameterType.AWS_EC2_IMAGE_ID`); + } - const type = attrs.type || ParameterType.STRING; + const type = attrs.type ?? attrs.valueType ?? ParameterValueType.STRING; const stringValue = attrs.version ? new CfnDynamicReference(CfnDynamicReferenceService.SSM, `${attrs.parameterName}:${Tokenization.stringifyNumber(attrs.version)}`).toString() @@ -377,7 +480,7 @@ export class StringParameter extends ParameterBase implements IStringParameter { class Import extends ParameterBase { public readonly parameterName = attrs.parameterName; public readonly parameterArn = arnForParameterName(this, attrs.parameterName, { simpleName: attrs.simpleName }); - public readonly parameterType = type; + 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; } @@ -426,7 +529,24 @@ export class StringParameter extends ParameterBase implements IStringParameter { * @param version The parameter version (recommended in order to ensure that the value won't change during deployment) */ public static valueForStringParameter(scope: Construct, parameterName: string, version?: number): string { - return StringParameter.valueForTypedStringParameter(scope, parameterName, ParameterType.STRING, version); + return StringParameter.valueForTypedStringParameterV2(scope, parameterName, ParameterValueType.STRING, version); + } + + /** + * Returns a token that will resolve (during deployment) to the string value of an SSM string parameter. + * @param scope Some scope within a stack + * @param parameterName The name of the SSM parameter. + * @param type The type of the SSM parameter. + * @param version The parameter version (recommended in order to ensure that the value won't change during deployment) + */ + public static valueForTypedStringParameterV2(scope: Construct, parameterName: string, type = ParameterValueType.STRING, version?: number): string { + const stack = Stack.of(scope); + const id = makeIdentityForImportedValue(parameterName); + const exists = stack.node.tryFindChild(id) as IStringParameter; + + if (exists) { return exists.stringValue; } + + return this.fromStringParameterAttributes(stack, id, { parameterName, version, valueType: type }).stringValue; } /** @@ -435,8 +555,13 @@ export class StringParameter extends ParameterBase implements IStringParameter { * @param parameterName The name of the SSM parameter. * @param type The type of the SSM parameter. * @param version The parameter version (recommended in order to ensure that the value won't change during deployment) + * @deprecated - use valueForTypedStringParameterV2 instead */ public static valueForTypedStringParameter(scope: Construct, parameterName: string, type = ParameterType.STRING, version?: number): string { + if (type === ParameterType.STRING_LIST) { + throw new Error('valueForTypedStringParameter does not support STRING_LIST, ' + +'use valueForTypedListParameter instead'); + } const stack = Stack.of(scope); const id = makeIdentityForImportedValue(parameterName); const exists = stack.node.tryFindChild(id) as IStringParameter; @@ -528,6 +653,49 @@ export class StringListParameter extends ParameterBase implements IStringListPar return new Import(scope, id); } + /** + * Imports an external string list parameter with name and optional version. + */ + public static fromListParameterAttributes(scope: Construct, id: string, attrs: ListParameterAttributes): IStringListParameter { + if (!attrs.parameterName) { + throw new Error('parameterName cannot be an empty string'); + } + + 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; + } + + return new Import(scope, id); + } + + /** + * Returns a token that will resolve (during deployment) to the list value of an SSM StringList parameter. + * @param scope Some scope within a stack + * @param parameterName The name of the SSM parameter. + * @param type the type of the SSM list parameter + * @param version The parameter version (recommended in order to ensure that the value won't change during deployment) + */ + public static valueForTypedListParameter(scope: Construct, parameterName: string, type = ParameterValueType.STRING, version?: number): string[] { + const stack = Stack.of(scope); + const id = makeIdentityForImportedValue(parameterName); + const exists = stack.node.tryFindChild(id) as IStringListParameter; + + if (exists) { return exists.stringListValue; } + + return this.fromListParameterAttributes(stack, id, { parameterName, elementType: type, version }).stringListValue; + } + + public readonly parameterArn: string; public readonly parameterName: string; public readonly parameterType: string; diff --git a/packages/@aws-cdk/aws-ssm/package.json b/packages/@aws-cdk/aws-ssm/package.json index 6cdf2f7b437a5..a39df20f35990 100644 --- a/packages/@aws-cdk/aws-ssm/package.json +++ b/packages/@aws-cdk/aws-ssm/package.json @@ -83,6 +83,7 @@ "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^27.5.2", diff --git a/packages/@aws-cdk/aws-ssm/test/integ.list-parameter.ts b/packages/@aws-cdk/aws-ssm/test/integ.list-parameter.ts new file mode 100644 index 0000000000000..81d438fa96f9c --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/integ.list-parameter.ts @@ -0,0 +1,85 @@ +import * as cdk from '@aws-cdk/core'; +import { IntegTest, ExpectedResult, Match } from '@aws-cdk/integ-tests'; +import { Construct } from 'constructs'; +import * as ssm from '../lib'; +const paramName = 'integ-list-param'; +const paramValue = ['value1', 'value2']; + +class TestCaseBase extends cdk.Stack { + public readonly listParam: ssm.IStringListParameter; + constructor(scope: Construct, id: string) { + super(scope, id); + + this.listParam = new ssm.StringListParameter(this, 'ListParam', { + parameterName: paramName, + stringListValue: paramValue, + }); + } +} + + +const app = new cdk.App({ + treeMetadata: false, +}); +app.node.setContext('@aws-cdk/core:newStyleStackSynthesis', true); +const base = new TestCaseBase(app, 'base'); +const testCase = new cdk.Stack(app, 'list-param'); + +// creates the dependency between stacks +new cdk.CfnOutput(testCase, 'Output', { + value: cdk.Fn.join(',', base.listParam.stringListValue), +}); + + +/** + * get the value from the `base` stack and then write it to a new parameter + * We will then assert that the value that is written is the correct value + * This validates that the `fromXXX` and `valueForXXX` imports the value correctly + */ + +const fromAttrs = ssm.StringListParameter.fromListParameterAttributes(testCase, 'FromAttrs', { + parameterName: paramName, + elementType: ssm.ParameterValueType.STRING, +}); +const ssmAttrsValue = new ssm.CfnParameter(testCase, 'attrs-test', { + type: 'StringList', + value: cdk.Fn.join(',', fromAttrs.stringListValue), +}); + +const value = ssm.StringListParameter.valueForTypedListParameter(testCase, paramName, ssm.ParameterValueType.STRING); +const ssmValue = new ssm.CfnParameter(testCase, 'value-test', { + type: 'StringList', + value: cdk.Fn.join(',', value), +}); + +const versionValue = ssm.StringListParameter.valueForTypedListParameter(testCase, paramName, ssm.ParameterValueType.STRING, 1); +const ssmVersionValue = new ssm.CfnParameter(testCase, 'version-value-test', { + type: 'StringList', + value: cdk.Fn.join(',', versionValue), +}); + + +const integ = new IntegTest(app, 'ssm-string-param', { + testCases: [ + testCase, + ], +}); + +// list the parameters +const actualAttrs = integ.assertions.awsApiCall('SSM', 'getParameters', { + Names: [ssmVersionValue.ref, ssmValue.ref, ssmAttrsValue.ref], +}); + +actualAttrs.expect(ExpectedResult.objectLike({ + Parameters: Match.arrayWith([ + Match.objectLike({ + Value: paramValue.join(','), + }), + Match.objectLike({ + Value: paramValue.join(','), + }), + Match.objectLike({ + Value: paramValue.join(','), + }), + ]), +})); diff --git a/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/asset.2a53dc40a7dae81c8850e125ab49e5f55d80b7b8ceac86976f2a4119393cab72.bundle/index.js b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/asset.2a53dc40a7dae81c8850e125ab49e5f55d80b7b8ceac86976f2a4119393cab72.bundle/index.js new file mode 100644 index 0000000000000..09ec17c1ae178 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/asset.2a53dc40a7dae81c8850e125ab49e5f55d80b7b8ceac86976f2a4119393cab72.bundle/index.js @@ -0,0 +1,611 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// lib/assertions/providers/lambda-handler/index.ts +var lambda_handler_exports = {}; +__export(lambda_handler_exports, { + handler: () => handler +}); +module.exports = __toCommonJS(lambda_handler_exports); + +// ../assertions/lib/matcher.ts +var Matcher = class { + static isMatcher(x) { + return x && x instanceof Matcher; + } +}; +var MatchResult = class { + constructor(target) { + this.failures = []; + this.captures = /* @__PURE__ */ new Map(); + this.finalized = false; + this.target = target; + } + push(matcher, path, message) { + return this.recordFailure({ matcher, path, message }); + } + recordFailure(failure) { + this.failures.push(failure); + return this; + } + hasFailed() { + return this.failures.length !== 0; + } + get failCount() { + return this.failures.length; + } + compose(id, inner) { + const innerF = inner.failures; + this.failures.push(...innerF.map((f) => { + return { path: [id, ...f.path], message: f.message, matcher: f.matcher }; + })); + inner.captures.forEach((vals, capture) => { + vals.forEach((value) => this.recordCapture({ capture, value })); + }); + return this; + } + finished() { + if (this.finalized) { + return this; + } + if (this.failCount === 0) { + this.captures.forEach((vals, cap) => cap._captured.push(...vals)); + } + this.finalized = true; + return this; + } + toHumanStrings() { + return this.failures.map((r) => { + const loc = r.path.length === 0 ? "" : ` at ${r.path.join("")}`; + return "" + r.message + loc + ` (using ${r.matcher.name} matcher)`; + }); + } + recordCapture(options) { + let values = this.captures.get(options.capture); + if (values === void 0) { + values = []; + } + values.push(options.value); + this.captures.set(options.capture, values); + } +}; + +// ../assertions/lib/private/matchers/absent.ts +var AbsentMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual !== void 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Received ${actual}, but key should be absent` + }); + } + return result; + } +}; + +// ../assertions/lib/private/type.ts +function getType(obj) { + return Array.isArray(obj) ? "array" : typeof obj; +} + +// ../assertions/lib/match.ts +var Match = class { + static absent() { + return new AbsentMatch("absent"); + } + static arrayWith(pattern) { + return new ArrayMatch("arrayWith", pattern); + } + static arrayEquals(pattern) { + return new ArrayMatch("arrayEquals", pattern, { subsequence: false }); + } + static exact(pattern) { + return new LiteralMatch("exact", pattern, { partialObjects: false }); + } + static objectLike(pattern) { + return new ObjectMatch("objectLike", pattern); + } + static objectEquals(pattern) { + return new ObjectMatch("objectEquals", pattern, { partial: false }); + } + static not(pattern) { + return new NotMatch("not", pattern); + } + static serializedJson(pattern) { + return new SerializedJson("serializedJson", pattern); + } + static anyValue() { + return new AnyMatch("anyValue"); + } + static stringLikeRegexp(pattern) { + return new StringLikeRegexpMatch("stringLikeRegexp", pattern); + } +}; +var LiteralMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partialObjects = options.partialObjects ?? false; + if (Matcher.isMatcher(this.pattern)) { + throw new Error("LiteralMatch cannot directly contain another matcher. Remove the top-level matcher or nest it more deeply."); + } + } + test(actual) { + if (Array.isArray(this.pattern)) { + return new ArrayMatch(this.name, this.pattern, { subsequence: false, partialObjects: this.partialObjects }).test(actual); + } + if (typeof this.pattern === "object") { + return new ObjectMatch(this.name, this.pattern, { partial: this.partialObjects }).test(actual); + } + const result = new MatchResult(actual); + if (typeof this.pattern !== typeof actual) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected type ${typeof this.pattern} but received ${getType(actual)}` + }); + return result; + } + if (actual !== this.pattern) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected ${this.pattern} but received ${actual}` + }); + } + return result; + } +}; +var ArrayMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.subsequence = options.subsequence ?? true; + this.partialObjects = options.partialObjects ?? false; + } + test(actual) { + if (!Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type array but received ${getType(actual)}` + }); + } + if (!this.subsequence && this.pattern.length !== actual.length) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected array of length ${this.pattern.length} but received ${actual.length}` + }); + } + let patternIdx = 0; + let actualIdx = 0; + const result = new MatchResult(actual); + while (patternIdx < this.pattern.length && actualIdx < actual.length) { + const patternElement = this.pattern[patternIdx]; + const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects }); + const matcherName = matcher.name; + if (this.subsequence && (matcherName == "absent" || matcherName == "anyValue")) { + throw new Error(`The Matcher ${matcherName}() cannot be nested within arrayWith()`); + } + const innerResult = matcher.test(actual[actualIdx]); + if (!this.subsequence || !innerResult.hasFailed()) { + result.compose(`[${actualIdx}]`, innerResult); + patternIdx++; + actualIdx++; + } else { + actualIdx++; + } + } + for (; patternIdx < this.pattern.length; patternIdx++) { + const pattern = this.pattern[patternIdx]; + const element = Matcher.isMatcher(pattern) || typeof pattern === "object" ? " " : ` [${pattern}] `; + result.recordFailure({ + matcher: this, + path: [], + message: `Missing element${element}at pattern index ${patternIdx}` + }); + } + return result; + } +}; +var ObjectMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partial = options.partial ?? true; + } + test(actual) { + if (typeof actual !== "object" || Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type object but received ${getType(actual)}` + }); + } + const result = new MatchResult(actual); + if (!this.partial) { + for (const a of Object.keys(actual)) { + if (!(a in this.pattern)) { + result.recordFailure({ + matcher: this, + path: [`/${a}`], + message: "Unexpected key" + }); + } + } + } + for (const [patternKey, patternVal] of Object.entries(this.pattern)) { + if (!(patternKey in actual) && !(patternVal instanceof AbsentMatch)) { + result.recordFailure({ + matcher: this, + path: [`/${patternKey}`], + message: `Missing key '${patternKey}' among {${Object.keys(actual).join(",")}}` + }); + continue; + } + const matcher = Matcher.isMatcher(patternVal) ? patternVal : new LiteralMatch(this.name, patternVal, { partialObjects: this.partial }); + const inner = matcher.test(actual[patternKey]); + result.compose(`/${patternKey}`, inner); + } + return result; + } +}; +var SerializedJson = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + if (getType(actual) !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected JSON as a string but found ${getType(actual)}` + }); + return result; + } + let parsed; + try { + parsed = JSON.parse(actual); + } catch (err) { + if (err instanceof SyntaxError) { + result.recordFailure({ + matcher: this, + path: [], + message: `Invalid JSON string: ${actual}` + }); + return result; + } else { + throw err; + } + } + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(parsed); + result.compose(`(${this.name})`, innerResult); + return result; + } +}; +var NotMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(actual); + const result = new MatchResult(actual); + if (innerResult.failCount === 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Found unexpected match: ${JSON.stringify(actual, void 0, 2)}` + }); + } + return result; + } +}; +var AnyMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual == null) { + result.recordFailure({ + matcher: this, + path: [], + message: "Expected a value but found none" + }); + } + return result; + } +}; +var StringLikeRegexpMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + const regex = new RegExp(this.pattern, "gm"); + if (typeof actual !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected a string, but got '${typeof actual}'` + }); + } + if (!regex.test(actual)) { + result.recordFailure({ + matcher: this, + path: [], + message: `String '${actual}' did not match pattern '${this.pattern}'` + }); + } + return result; + } +}; + +// lib/assertions/providers/lambda-handler/base.ts +var https = __toESM(require("https")); +var url = __toESM(require("url")); +var CustomResourceHandler = class { + constructor(event, context) { + this.event = event; + this.context = context; + this.timedOut = false; + this.timeout = setTimeout(async () => { + await this.respond({ + status: "FAILED", + reason: "Lambda Function Timeout", + data: this.context.logStreamName + }); + this.timedOut = true; + }, context.getRemainingTimeInMillis() - 1200); + this.event = event; + this.physicalResourceId = extractPhysicalResourceId(event); + } + async handle() { + try { + console.log(`Event: ${JSON.stringify({ ...this.event, ResponseURL: "..." })}`); + const response = await this.processEvent(this.event.ResourceProperties); + console.log(`Event output : ${JSON.stringify(response)}`); + await this.respond({ + status: "SUCCESS", + reason: "OK", + data: response + }); + } catch (e) { + console.log(e); + await this.respond({ + status: "FAILED", + reason: e.message ?? "Internal Error" + }); + } finally { + clearTimeout(this.timeout); + } + } + respond(response) { + if (this.timedOut) { + return; + } + const cfResponse = { + Status: response.status, + Reason: response.reason, + PhysicalResourceId: this.physicalResourceId, + StackId: this.event.StackId, + RequestId: this.event.RequestId, + LogicalResourceId: this.event.LogicalResourceId, + NoEcho: false, + Data: response.data + }; + const responseBody = JSON.stringify(cfResponse); + console.log("Responding to CloudFormation", responseBody); + const parsedUrl = url.parse(this.event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: "PUT", + headers: { "content-type": "", "content-length": responseBody.length } + }; + return new Promise((resolve, reject) => { + try { + const request2 = https.request(requestOptions, resolve); + request2.on("error", reject); + request2.write(responseBody); + request2.end(); + } catch (e) { + reject(e); + } + }); + } +}; +function extractPhysicalResourceId(event) { + switch (event.RequestType) { + case "Create": + return event.LogicalResourceId; + case "Update": + case "Delete": + return event.PhysicalResourceId; + } +} + +// lib/assertions/providers/lambda-handler/assertion.ts +var AssertionHandler = class extends CustomResourceHandler { + async processEvent(request2) { + let actual = decodeCall(request2.actual); + const expected = decodeCall(request2.expected); + let result; + const matcher = new MatchCreator(expected).getMatcher(); + console.log(`Testing equality between ${JSON.stringify(request2.actual)} and ${JSON.stringify(request2.expected)}`); + const matchResult = matcher.test(actual); + matchResult.finished(); + if (matchResult.hasFailed()) { + result = { + data: JSON.stringify({ + status: "fail", + message: [ + ...matchResult.toHumanStrings(), + JSON.stringify(matchResult.target, void 0, 2) + ].join("\n") + }) + }; + if (request2.failDeployment) { + throw new Error(result.data); + } + } else { + result = { + data: JSON.stringify({ + status: "success" + }) + }; + } + return result; + } +}; +var MatchCreator = class { + constructor(obj) { + this.parsedObj = { + matcher: obj + }; + } + getMatcher() { + try { + const final = JSON.parse(JSON.stringify(this.parsedObj), function(_k, v) { + const nested = Object.keys(v)[0]; + switch (nested) { + case "$ArrayWith": + return Match.arrayWith(v[nested]); + case "$ObjectLike": + return Match.objectLike(v[nested]); + case "$StringLike": + return Match.stringLikeRegexp(v[nested]); + default: + return v; + } + }); + if (Matcher.isMatcher(final.matcher)) { + return final.matcher; + } + return Match.exact(final.matcher); + } catch { + return Match.exact(this.parsedObj.matcher); + } + } +}; +function decodeCall(call) { + if (!call) { + return void 0; + } + try { + const parsed = JSON.parse(call); + return parsed; + } catch (e) { + return call; + } +} + +// lib/assertions/providers/lambda-handler/utils.ts +function decode(object) { + return JSON.parse(JSON.stringify(object), (_k, v) => { + switch (v) { + case "TRUE:BOOLEAN": + return true; + case "FALSE:BOOLEAN": + return false; + default: + return v; + } + }); +} + +// lib/assertions/providers/lambda-handler/sdk.ts +function flatten(object) { + return Object.assign( + {}, + ...function _flatten(child, path = []) { + return [].concat(...Object.keys(child).map((key) => { + const childKey = Buffer.isBuffer(child[key]) ? child[key].toString("utf8") : child[key]; + return typeof childKey === "object" && childKey !== null ? _flatten(childKey, path.concat([key])) : { [path.concat([key]).join(".")]: childKey }; + })); + }(object) + ); +} +var AwsApiCallHandler = class extends CustomResourceHandler { + async processEvent(request2) { + const AWS = require("aws-sdk"); + console.log(`AWS SDK VERSION: ${AWS.VERSION}`); + const service = new AWS[request2.service](); + const response = await service[request2.api](request2.parameters && decode(request2.parameters)).promise(); + console.log(`SDK response received ${JSON.stringify(response)}`); + delete response.ResponseMetadata; + const respond = { + apiCallResponse: response + }; + const flatData = { + ...flatten(respond) + }; + return request2.flattenResponse === "true" ? flatData : respond; + } +}; + +// lib/assertions/providers/lambda-handler/types.ts +var ASSERT_RESOURCE_TYPE = "Custom::DeployAssert@AssertEquals"; +var SDK_RESOURCE_TYPE_PREFIX = "Custom::DeployAssert@SdkCall"; + +// lib/assertions/providers/lambda-handler/index.ts +async function handler(event, context) { + const provider = createResourceHandler(event, context); + await provider.handle(); +} +function createResourceHandler(event, context) { + if (event.ResourceType.startsWith(SDK_RESOURCE_TYPE_PREFIX)) { + return new AwsApiCallHandler(event, context); + } + switch (event.ResourceType) { + case ASSERT_RESOURCE_TYPE: + return new AssertionHandler(event, context); + default: + throw new Error(`Unsupported resource type "${event.ResourceType}`); + } +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + handler +}); diff --git a/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/base.assets.json b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/base.assets.json new file mode 100644 index 0000000000000..9f0c6525d7f91 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/base.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "1caf5ea1b3cc1aedc4ec46feb2680836eae5804fa1ae1d8a572944636e88531b": { + "source": { + "path": "base.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "1caf5ea1b3cc1aedc4ec46feb2680836eae5804fa1ae1d8a572944636e88531b.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/base.template.json b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/base.template.json new file mode 100644 index 0000000000000..c2bdff818119f --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/base.template.json @@ -0,0 +1,59 @@ +{ + "Resources": { + "ListParam66ABDC3F": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "StringList", + "Value": "value1,value2", + "Name": "integ-list-param" + } + } + }, + "Outputs": { + "ExportsOutputFnGetAttListParam66ABDC3FValue8C623E22": { + "Value": { + "Fn::GetAtt": [ + "ListParam66ABDC3F", + "Value" + ] + }, + "Export": { + "Name": "base:ExportsOutputFnGetAttListParam66ABDC3FValue8C623E22" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/integ.json b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/integ.json new file mode 100644 index 0000000000000..1e9ee364aca3e --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "21.0.0", + "testCases": { + "ssm-string-param/DefaultTest": { + "stacks": [ + "list-param" + ], + "assertionStack": "ssm-string-param/DefaultTest/DeployAssert", + "assertionStackName": "ssmstringparamDefaultTestDeployAssert9C612E37" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/list-param.assets.json b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/list-param.assets.json new file mode 100644 index 0000000000000..47303559bc7be --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/list-param.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "f21f04e61fc048db023578e8c9bdab9b7f45992bd3d533bcf7fb9eb87991bc95": { + "source": { + "path": "list-param.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "f21f04e61fc048db023578e8c9bdab9b7f45992bd3d533bcf7fb9eb87991bc95.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/list-param.template.json b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/list-param.template.json new file mode 100644 index 0000000000000..304d3b6c0aa15 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/list-param.template.json @@ -0,0 +1,129 @@ +{ + "Outputs": { + "Output": { + "Value": { + "Fn::Join": [ + ",", + { + "Fn::Split": [ + ",", + { + "Fn::ImportValue": "base:ExportsOutputFnGetAttListParam66ABDC3FValue8C623E22" + } + ] + } + ] + } + }, + "ExportsOutputRefversionvaluetestB139B4AA": { + "Value": { + "Ref": "versionvaluetest" + }, + "Export": { + "Name": "list-param:ExportsOutputRefversionvaluetestB139B4AA" + } + }, + "ExportsOutputRefvaluetest4DD5FF9D": { + "Value": { + "Ref": "valuetest" + }, + "Export": { + "Name": "list-param:ExportsOutputRefvaluetest4DD5FF9D" + } + }, + "ExportsOutputRefattrstestCCA64863": { + "Value": { + "Ref": "attrstest" + }, + "Export": { + "Name": "list-param:ExportsOutputRefattrstestCCA64863" + } + } + }, + "Parameters": { + "FromAttrsParameter": { + "Type": "AWS::SSM::Parameter::Value>", + "Default": "integ-list-param" + }, + "SsmParameterValueinteglistparamC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value>", + "Default": "integ-list-param" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Resources": { + "attrstest": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "StringList", + "Value": { + "Fn::Join": [ + ",", + { + "Ref": "FromAttrsParameter" + } + ] + } + } + }, + "valuetest": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "StringList", + "Value": { + "Fn::Join": [ + ",", + { + "Ref": "SsmParameterValueinteglistparamC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + } + }, + "versionvaluetest": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "StringList", + "Value": { + "Fn::Join": [ + ",", + { + "Ref": "SsmParameterValueinteglistparamC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ] + } + } + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..aa707e00f12e8 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/manifest.json @@ -0,0 +1,244 @@ +{ + "version": "21.0.0", + "artifacts": { + "base.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "base.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "base": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "base.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/1caf5ea1b3cc1aedc4ec46feb2680836eae5804fa1ae1d8a572944636e88531b.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "base.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "base.assets" + ], + "metadata": { + "/base/ListParam/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ListParam66ABDC3F" + } + ], + "/base/Exports/Output{\"Fn::GetAtt\":[\"ListParam66ABDC3F\",\"Value\"]}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputFnGetAttListParam66ABDC3FValue8C623E22" + } + ], + "/base/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/base/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "base" + }, + "list-param.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "list-param.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "list-param": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "list-param.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/f21f04e61fc048db023578e8c9bdab9b7f45992bd3d533bcf7fb9eb87991bc95.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "list-param.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "base", + "list-param.assets" + ], + "metadata": { + "/list-param/Output": [ + { + "type": "aws:cdk:logicalId", + "data": "Output" + } + ], + "/list-param/FromAttrs.Parameter": [ + { + "type": "aws:cdk:logicalId", + "data": "FromAttrsParameter" + } + ], + "/list-param/attrs-test": [ + { + "type": "aws:cdk:logicalId", + "data": "attrstest" + } + ], + "/list-param/SsmParameterValue:integ-list-param:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter": [ + { + "type": "aws:cdk:logicalId", + "data": "SsmParameterValueinteglistparamC96584B6F00A464EAD1953AFF4B05118Parameter" + } + ], + "/list-param/value-test": [ + { + "type": "aws:cdk:logicalId", + "data": "valuetest" + } + ], + "/list-param/version-value-test": [ + { + "type": "aws:cdk:logicalId", + "data": "versionvaluetest" + } + ], + "/list-param/Exports/Output{\"Ref\":\"versionvaluetest\"}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputRefversionvaluetestB139B4AA" + } + ], + "/list-param/Exports/Output{\"Ref\":\"valuetest\"}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputRefvaluetest4DD5FF9D" + } + ], + "/list-param/Exports/Output{\"Ref\":\"attrstest\"}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputRefattrstestCCA64863" + } + ], + "/list-param/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/list-param/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "list-param" + }, + "ssmstringparamDefaultTestDeployAssert9C612E37.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "ssmstringparamDefaultTestDeployAssert9C612E37.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "ssmstringparamDefaultTestDeployAssert9C612E37": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "ssmstringparamDefaultTestDeployAssert9C612E37.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/ff1de129c8ab3914f9a0758b9422b50fdbd43f6a61767dc5391e7b08231cdc6f.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "ssmstringparamDefaultTestDeployAssert9C612E37.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "list-param", + "ssmstringparamDefaultTestDeployAssert9C612E37.assets" + ], + "metadata": { + "/ssm-string-param/DefaultTest/DeployAssert/AwsApiCallSSMgetParameters/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallSSMgetParameters" + } + ], + "/ssm-string-param/DefaultTest/DeployAssert/AwsApiCallSSMgetParameters/AssertEqualsSSMgetParameters/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallSSMgetParametersAssertEqualsSSMgetParametersE460052D" + } + ], + "/ssm-string-param/DefaultTest/DeployAssert/AwsApiCallSSMgetParameters/AssertEqualsSSMgetParameters/AssertionResults": [ + { + "type": "aws:cdk:logicalId", + "data": "AssertionResultsAssertEqualsSSMgetParameters" + } + ], + "/ssm-string-param/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73" + } + ], + "/ssm-string-param/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F" + } + ], + "/ssm-string-param/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/ssm-string-param/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "ssm-string-param/DefaultTest/DeployAssert" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/ssmstringparamDefaultTestDeployAssert9C612E37.assets.json b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/ssmstringparamDefaultTestDeployAssert9C612E37.assets.json new file mode 100644 index 0000000000000..78772a19bf4c0 --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/ssmstringparamDefaultTestDeployAssert9C612E37.assets.json @@ -0,0 +1,32 @@ +{ + "version": "21.0.0", + "files": { + "2a53dc40a7dae81c8850e125ab49e5f55d80b7b8ceac86976f2a4119393cab72": { + "source": { + "path": "asset.2a53dc40a7dae81c8850e125ab49e5f55d80b7b8ceac86976f2a4119393cab72.bundle", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "2a53dc40a7dae81c8850e125ab49e5f55d80b7b8ceac86976f2a4119393cab72.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "ff1de129c8ab3914f9a0758b9422b50fdbd43f6a61767dc5391e7b08231cdc6f": { + "source": { + "path": "ssmstringparamDefaultTestDeployAssert9C612E37.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "ff1de129c8ab3914f9a0758b9422b50fdbd43f6a61767dc5391e7b08231cdc6f.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/ssmstringparamDefaultTestDeployAssert9C612E37.template.json b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/ssmstringparamDefaultTestDeployAssert9C612E37.template.json new file mode 100644 index 0000000000000..e5af2645c54fc --- /dev/null +++ b/packages/@aws-cdk/aws-ssm/test/list-parameter.integ.snapshot/ssmstringparamDefaultTestDeployAssert9C612E37.template.json @@ -0,0 +1,160 @@ +{ + "Resources": { + "AwsApiCallSSMgetParameters": { + "Type": "Custom::DeployAssert@SdkCallSSMgetParameters", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "SSM", + "api": "getParameters", + "parameters": { + "Names": [ + { + "Fn::ImportValue": "list-param:ExportsOutputRefversionvaluetestB139B4AA" + }, + { + "Fn::ImportValue": "list-param:ExportsOutputRefvaluetest4DD5FF9D" + }, + { + "Fn::ImportValue": "list-param:ExportsOutputRefattrstestCCA64863" + } + ] + }, + "flattenResponse": "false", + "salt": "1661970076677" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AwsApiCallSSMgetParametersAssertEqualsSSMgetParametersE460052D": { + "Type": "Custom::DeployAssert@AssertEquals", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "actual": { + "Fn::GetAtt": [ + "AwsApiCallSSMgetParameters", + "apiCallResponse" + ] + }, + "expected": "{\"$ObjectLike\":{\"Parameters\":{\"$ArrayWith\":[{\"$ObjectLike\":{\"Value\":\"value1,value2\"}},{\"$ObjectLike\":{\"Value\":\"value1,value2\"}},{\"$ObjectLike\":{\"Value\":\"value1,value2\"}}]}}}", + "salt": "1661970076678" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "ssm:GetParameters" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + } + ] + } + } + ] + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "2a53dc40a7dae81c8850e125ab49e5f55d80b7b8ceac86976f2a4119393cab72.zip" + }, + "Timeout": 120, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73", + "Arn" + ] + } + } + } + }, + "Outputs": { + "AssertionResultsAssertEqualsSSMgetParameters": { + "Value": { + "Fn::GetAtt": [ + "AwsApiCallSSMgetParametersAssertEqualsSSMgetParametersE460052D", + "data" + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/test/parameter.test.ts b/packages/@aws-cdk/aws-ssm/test/parameter.test.ts index 7953006b3cfa6..817eb4d264239 100644 --- a/packages/@aws-cdk/aws-ssm/test/parameter.test.ts +++ b/packages/@aws-cdk/aws-ssm/test/parameter.test.ts @@ -3,9 +3,11 @@ import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; +import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import * as ssm from '../lib'; +import { ParameterType, ParameterValueType } from '../lib'; test('creating a String SSM Parameter', () => { // GIVEN @@ -29,7 +31,7 @@ test('creating a String SSM Parameter', () => { }); }); -test('type cannot be specified as AWS_EC2_IMAGE_ID', () => { +testDeprecated('type cannot be specified as AWS_EC2_IMAGE_ID', () => { // GIVEN const stack = new cdk.Stack(); @@ -650,6 +652,166 @@ test('fromLookup will use the SSM context provider to read value during synthesi ]); }); +describe('from string list parameter', () => { + testDeprecated('valueForTypedStringParameter list type throws error', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + ssm.StringParameter.valueForTypedStringParameter(stack, 'my-param-name', ParameterType.STRING_LIST); + }).toThrow(/use valueForTypedListParameter instead/); + }); + + testDeprecated('fromStringParameterAttributes list type throws error', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + ssm.StringParameter.fromStringParameterAttributes(stack, 'my-param-name', { + parameterName: 'my-param-name', + type: ParameterType.STRING_LIST, + }); + }).toThrow(/fromStringParameterAttributes does not support StringList/); + }); + + testDeprecated('fromStringParameterAttributes returns correct value', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + ssm.StringParameter.fromStringParameterAttributes(stack, 'my-param-name', { + parameterName: 'my-param-name', + type: ParameterType.STRING, + }); + + // THEN + Template.fromStack(stack).templateMatches({ + Parameters: { + myparamnameParameter: { + Type: 'AWS::SSM::Parameter::Value', + Default: 'my-param-name', + }, + }, + }); + }); + + test('fromStringParameterAttributes returns correct value with valueType', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + ssm.StringParameter.fromStringParameterAttributes(stack, 'my-param-name', { + parameterName: 'my-param-name', + valueType: ParameterValueType.STRING, + }); + + // THEN + Template.fromStack(stack).templateMatches({ + Parameters: { + myparamnameParameter: { + Type: 'AWS::SSM::Parameter::Value', + Default: 'my-param-name', + }, + }, + }); + }); + + test('valueForTypedListParameter returns correct value', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + ssm.StringListParameter.valueForTypedListParameter(stack, 'my-param-name'); + + // THEN + Template.fromStack(stack).templateMatches({ + Parameters: { + SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: 'AWS::SSM::Parameter::Value>', + Default: 'my-param-name', + }, + }, + }); + }); + + test('valueForTypedListParameter returns correct value with type', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + ssm.StringListParameter.valueForTypedListParameter(stack, 'my-param-name', ParameterValueType.AWS_EC2_INSTANCE_ID); + + // THEN + Template.fromStack(stack).templateMatches({ + Parameters: { + SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: 'AWS::SSM::Parameter::Value>', + Default: 'my-param-name', + }, + }, + }); + }); + + test('fromStringListParameterAttributes returns correct value', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + ssm.StringListParameter.fromListParameterAttributes(stack, 'my-param-name', { + parameterName: 'my-param-name', + }); + + // THEN + Template.fromStack(stack).templateMatches({ + Parameters: { + myparamnameParameter: { + Type: 'AWS::SSM::Parameter::Value>', + Default: 'my-param-name', + }, + }, + }); + }); + + testDeprecated('string type returns correct value', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + ssm.StringParameter.valueForTypedStringParameter(stack, 'my-param-name', ParameterType.STRING); + + // THEN + Template.fromStack(stack).templateMatches({ + Parameters: { + SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: 'AWS::SSM::Parameter::Value', + Default: 'my-param-name', + }, + }, + }); + }); + + test('string valueType returns correct value', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + ssm.StringParameter.valueForTypedStringParameterV2(stack, 'my-param-name', ParameterValueType.AWS_EC2_IMAGE_ID); + + // THEN + Template.fromStack(stack).templateMatches({ + Parameters: { + SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: 'AWS::SSM::Parameter::Value', + Default: 'my-param-name', + }, + }, + }); + }); + +}); + describe('valueForStringParameter', () => { test('returns a token that represents the SSM parameter value', () => { // GIVEN diff --git a/packages/@aws-cdk/core/lib/private/intrinsic.ts b/packages/@aws-cdk/core/lib/private/intrinsic.ts index cb814a6363594..8a15e38806134 100644 --- a/packages/@aws-cdk/core/lib/private/intrinsic.ts +++ b/packages/@aws-cdk/core/lib/private/intrinsic.ts @@ -56,6 +56,17 @@ export class Intrinsic implements IResolvable { return Token.asString(this); } + /** + * Convert an instance of this Token to a string list + * + * This method will be called implicitly by language runtimes if the object + * is embedded into a list. We treat it the same as an explicit + * stringification. + */ + public toStringList(): string[] { + return Token.asList(this); + } + /** * Turn this Token into JSON *