From 3dd834d66a9345eaab372a9dd59e673e52c86d7e Mon Sep 17 00:00:00 2001 From: M-Hawkins Date: Fri, 19 Apr 2024 14:58:01 -0700 Subject: [PATCH] feat(appconfig): constrain environments to a single deployment at a time (#29500) ### Issue # (if applicable) Closes #29345. ### Reason for this change The current L2 AppConfig constructs do not have any guardrails that prevent simultaneous Deployments to a single Environment. This is not allowed, and will result in Cfn deploy-time conflicts. ### Description of changes This commit adds a pair of new public methods to IEnvironment that enable the addition of a new Deployment for a given IConfiguration. It then updates the creation of new Deployments in ConfigurationBase to utilize these new methods instead of the current resource creation. These new methods interact with an internal queue. This queue creates a chain of Cfn dependencies between Deployments in order to enforce that only a single Deployment can be in progress for the Environment at any given time. ### Description of how you validated changes Added new unit and integ test coverage. Deployed the new integ test without these changes and confirmed that Cfn failed at deployment time. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- ...efaultTestDeployAssert1621E45D.assets.json | 19 + ...aultTestDeployAssert1621E45D.template.json | 36 ++ ...aws-appconfig-multi-config-env.assets.json | 19 + ...s-appconfig-multi-config-env.template.json | 156 ++++++++ .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 161 +++++++++ .../tree.json | 334 ++++++++++++++++++ .../test/integ.multi-config-env.ts | 44 +++ packages/aws-cdk-lib/aws-appconfig/README.md | 92 ++++- .../aws-appconfig/lib/configuration.ts | 18 +- .../aws-appconfig/lib/environment.ts | 53 ++- .../aws-appconfig/test/environment.test.ts | 295 +++++++++++++++- 13 files changed, 1224 insertions(+), 16 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/aws-appconfig-multi-config-env.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/aws-appconfig-multi-config-env.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.assets.json new file mode 100644 index 0000000000000..bcfd14bc7d20a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.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-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.template.json @@ -0,0 +1,36 @@ +{ + "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-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/aws-appconfig-multi-config-env.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/aws-appconfig-multi-config-env.assets.json new file mode 100644 index 0000000000000..6b8622f47adc9 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/aws-appconfig-multi-config-env.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "ebc42c894086c85ac3a2ea694b62aa368f6bc0556f3fe3f0a2edb2b93d5d8d74": { + "source": { + "path": "aws-appconfig-multi-config-env.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "ebc42c894086c85ac3a2ea694b62aa368f6bc0556f3fe3f0a2edb2b93d5d8d74.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-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/aws-appconfig-multi-config-env.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/aws-appconfig-multi-config-env.template.json new file mode 100644 index 0000000000000..410942d6012ec --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/aws-appconfig-multi-config-env.template.json @@ -0,0 +1,156 @@ +{ + "Resources": { + "MyApplicationForEnv1F597ED9": { + "Type": "AWS::AppConfig::Application", + "Properties": { + "Name": "awsappconfigmulticonfigenv-MyApplicationForEnv-1EE3EA95" + } + }, + "MultiConfigEnvironment5F41B747": { + "Type": "AWS::AppConfig::Environment", + "Properties": { + "ApplicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "Name": "awsappconfigmulticonfigenv-MultiConfigEnvironment-59525230" + } + }, + "QuickDeploymentStrategy980252EC": { + "Type": "AWS::AppConfig::DeploymentStrategy", + "Properties": { + "DeploymentDurationInMinutes": 1, + "GrowthFactor": 50, + "GrowthType": "LINEAR", + "Name": "awsappconfigmulticonfigenv-QuickDeploymentStrategy-CAB72574", + "ReplicateTo": "NONE" + } + }, + "MyFirstConfigConfigurationProfileAB11F87A": { + "Type": "AWS::AppConfig::ConfigurationProfile", + "Properties": { + "ApplicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "LocationUri": "hosted", + "Name": "awsappconfigmulticonfigenv-MyFirstConfig-2FF7CAAB" + } + }, + "MyFirstConfig117AFBAC": { + "Type": "AWS::AppConfig::HostedConfigurationVersion", + "Properties": { + "ApplicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "ConfigurationProfileId": { + "Ref": "MyFirstConfigConfigurationProfileAB11F87A" + }, + "Content": "first config content", + "ContentType": "application/octet-stream" + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "MyFirstConfigDeployment5AEBAD52B8EE4": { + "Type": "AWS::AppConfig::Deployment", + "Properties": { + "ApplicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "ConfigurationProfileId": { + "Ref": "MyFirstConfigConfigurationProfileAB11F87A" + }, + "ConfigurationVersion": { + "Ref": "MyFirstConfig117AFBAC" + }, + "DeploymentStrategyId": { + "Ref": "QuickDeploymentStrategy980252EC" + }, + "EnvironmentId": { + "Ref": "MultiConfigEnvironment5F41B747" + } + } + }, + "MySecondConfigConfigurationProfileD0CC1BAA": { + "Type": "AWS::AppConfig::ConfigurationProfile", + "Properties": { + "ApplicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "LocationUri": "hosted", + "Name": "awsappconfigmulticonfigenv-MySecondConfig-4F837809" + } + }, + "MySecondConfig28DEBAC4": { + "Type": "AWS::AppConfig::HostedConfigurationVersion", + "Properties": { + "ApplicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "ConfigurationProfileId": { + "Ref": "MySecondConfigConfigurationProfileD0CC1BAA" + }, + "Content": "second config content", + "ContentType": "application/octet-stream" + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "MySecondConfigDeployment5AEBAD1470BE4": { + "Type": "AWS::AppConfig::Deployment", + "Properties": { + "ApplicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "ConfigurationProfileId": { + "Ref": "MySecondConfigConfigurationProfileD0CC1BAA" + }, + "ConfigurationVersion": { + "Ref": "MySecondConfig28DEBAC4" + }, + "DeploymentStrategyId": { + "Ref": "QuickDeploymentStrategy980252EC" + }, + "EnvironmentId": { + "Ref": "MultiConfigEnvironment5F41B747" + } + }, + "DependsOn": [ + "MyFirstConfigDeployment5AEBAD52B8EE4" + ] + } + }, + "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-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/integ.json new file mode 100644 index 0000000000000..aa013d97752d6 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.0", + "testCases": { + "appconfig-multi-config-env/DefaultTest": { + "stacks": [ + "aws-appconfig-multi-config-env" + ], + "assertionStack": "appconfig-multi-config-env/DefaultTest/DeployAssert", + "assertionStackName": "appconfigmulticonfigenvDefaultTestDeployAssert1621E45D" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/manifest.json new file mode 100644 index 0000000000000..22a3fbb12bb74 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/manifest.json @@ -0,0 +1,161 @@ +{ + "version": "36.0.0", + "artifacts": { + "aws-appconfig-multi-config-env.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-appconfig-multi-config-env.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-appconfig-multi-config-env": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-appconfig-multi-config-env.template.json", + "terminationProtection": false, + "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}/ebc42c894086c85ac3a2ea694b62aa368f6bc0556f3fe3f0a2edb2b93d5d8d74.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-appconfig-multi-config-env.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": [ + "aws-appconfig-multi-config-env.assets" + ], + "metadata": { + "/aws-appconfig-multi-config-env/MyApplicationForEnv/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyApplicationForEnv1F597ED9" + } + ], + "/aws-appconfig-multi-config-env/MultiConfigEnvironment/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MultiConfigEnvironment5F41B747" + } + ], + "/aws-appconfig-multi-config-env/QuickDeploymentStrategy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "QuickDeploymentStrategy980252EC" + } + ], + "/aws-appconfig-multi-config-env/MyFirstConfig/ConfigurationProfile": [ + { + "type": "aws:cdk:logicalId", + "data": "MyFirstConfigConfigurationProfileAB11F87A" + } + ], + "/aws-appconfig-multi-config-env/MyFirstConfig/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyFirstConfig117AFBAC" + } + ], + "/aws-appconfig-multi-config-env/MyFirstConfig/Deployment5AEBA": [ + { + "type": "aws:cdk:logicalId", + "data": "MyFirstConfigDeployment5AEBAD52B8EE4" + } + ], + "/aws-appconfig-multi-config-env/MySecondConfig/ConfigurationProfile": [ + { + "type": "aws:cdk:logicalId", + "data": "MySecondConfigConfigurationProfileD0CC1BAA" + } + ], + "/aws-appconfig-multi-config-env/MySecondConfig/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MySecondConfig28DEBAC4" + } + ], + "/aws-appconfig-multi-config-env/MySecondConfig/Deployment5AEBA": [ + { + "type": "aws:cdk:logicalId", + "data": "MySecondConfigDeployment5AEBAD1470BE4" + } + ], + "/aws-appconfig-multi-config-env/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-appconfig-multi-config-env/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-appconfig-multi-config-env" + }, + "appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "appconfigmulticonfigenvDefaultTestDeployAssert1621E45D": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.template.json", + "terminationProtection": false, + "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}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.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": [ + "appconfigmulticonfigenvDefaultTestDeployAssert1621E45D.assets" + ], + "metadata": { + "/appconfig-multi-config-env/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/appconfig-multi-config-env/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "appconfig-multi-config-env/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/tree.json new file mode 100644 index 0000000000000..93e0497afe1a4 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.js.snapshot/tree.json @@ -0,0 +1,334 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-appconfig-multi-config-env": { + "id": "aws-appconfig-multi-config-env", + "path": "aws-appconfig-multi-config-env", + "children": { + "MyApplicationForEnv": { + "id": "MyApplicationForEnv", + "path": "aws-appconfig-multi-config-env/MyApplicationForEnv", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-appconfig-multi-config-env/MyApplicationForEnv/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppConfig::Application", + "aws:cdk:cloudformation:props": { + "name": "awsappconfigmulticonfigenv-MyApplicationForEnv-1EE3EA95" + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "MultiConfigEnvironment": { + "id": "MultiConfigEnvironment", + "path": "aws-appconfig-multi-config-env/MultiConfigEnvironment", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-appconfig-multi-config-env/MultiConfigEnvironment/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppConfig::Environment", + "aws:cdk:cloudformation:props": { + "applicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "name": "awsappconfigmulticonfigenv-MultiConfigEnvironment-59525230" + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "QuickDeploymentStrategy": { + "id": "QuickDeploymentStrategy", + "path": "aws-appconfig-multi-config-env/QuickDeploymentStrategy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-appconfig-multi-config-env/QuickDeploymentStrategy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppConfig::DeploymentStrategy", + "aws:cdk:cloudformation:props": { + "deploymentDurationInMinutes": 1, + "growthFactor": 50, + "growthType": "LINEAR", + "name": "awsappconfigmulticonfigenv-QuickDeploymentStrategy-CAB72574", + "replicateTo": "NONE" + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "MyFirstConfig": { + "id": "MyFirstConfig", + "path": "aws-appconfig-multi-config-env/MyFirstConfig", + "children": { + "ConfigurationProfile": { + "id": "ConfigurationProfile", + "path": "aws-appconfig-multi-config-env/MyFirstConfig/ConfigurationProfile", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppConfig::ConfigurationProfile", + "aws:cdk:cloudformation:props": { + "applicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "locationUri": "hosted", + "name": "awsappconfigmulticonfigenv-MyFirstConfig-2FF7CAAB" + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-appconfig-multi-config-env/MyFirstConfig/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppConfig::HostedConfigurationVersion", + "aws:cdk:cloudformation:props": { + "applicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "configurationProfileId": { + "Ref": "MyFirstConfigConfigurationProfileAB11F87A" + }, + "content": "first config content", + "contentType": "application/octet-stream" + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Deployment5AEBA": { + "id": "Deployment5AEBA", + "path": "aws-appconfig-multi-config-env/MyFirstConfig/Deployment5AEBA", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppConfig::Deployment", + "aws:cdk:cloudformation:props": { + "applicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "configurationProfileId": { + "Ref": "MyFirstConfigConfigurationProfileAB11F87A" + }, + "configurationVersion": { + "Ref": "MyFirstConfig117AFBAC" + }, + "deploymentStrategyId": { + "Ref": "QuickDeploymentStrategy980252EC" + }, + "environmentId": { + "Ref": "MultiConfigEnvironment5F41B747" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "MySecondConfig": { + "id": "MySecondConfig", + "path": "aws-appconfig-multi-config-env/MySecondConfig", + "children": { + "ConfigurationProfile": { + "id": "ConfigurationProfile", + "path": "aws-appconfig-multi-config-env/MySecondConfig/ConfigurationProfile", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppConfig::ConfigurationProfile", + "aws:cdk:cloudformation:props": { + "applicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "locationUri": "hosted", + "name": "awsappconfigmulticonfigenv-MySecondConfig-4F837809" + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-appconfig-multi-config-env/MySecondConfig/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppConfig::HostedConfigurationVersion", + "aws:cdk:cloudformation:props": { + "applicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "configurationProfileId": { + "Ref": "MySecondConfigConfigurationProfileD0CC1BAA" + }, + "content": "second config content", + "contentType": "application/octet-stream" + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Deployment5AEBA": { + "id": "Deployment5AEBA", + "path": "aws-appconfig-multi-config-env/MySecondConfig/Deployment5AEBA", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppConfig::Deployment", + "aws:cdk:cloudformation:props": { + "applicationId": { + "Ref": "MyApplicationForEnv1F597ED9" + }, + "configurationProfileId": { + "Ref": "MySecondConfigConfigurationProfileD0CC1BAA" + }, + "configurationVersion": { + "Ref": "MySecondConfig28DEBAC4" + }, + "deploymentStrategyId": { + "Ref": "QuickDeploymentStrategy980252EC" + }, + "environmentId": { + "Ref": "MultiConfigEnvironment5F41B747" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-appconfig-multi-config-env/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-appconfig-multi-config-env/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "appconfig-multi-config-env": { + "id": "appconfig-multi-config-env", + "path": "appconfig-multi-config-env", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "appconfig-multi-config-env/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "appconfig-multi-config-env/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "appconfig-multi-config-env/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "appconfig-multi-config-env/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "appconfig-multi-config-env/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.ts new file mode 100644 index 0000000000000..bcacbd18d326e --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appconfig/test/integ.multi-config-env.ts @@ -0,0 +1,44 @@ +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import { App, Duration, Stack } from 'aws-cdk-lib'; +import { Application, ConfigurationContent, DeploymentStrategy, Environment, HostedConfiguration, RolloutStrategy } from 'aws-cdk-lib/aws-appconfig'; + +/** + * Test case: + * - Single Environment + * - Two Configurations + * - Both have non-zero deployment duration + * + * If this was done via the L1 constructs alone, Cfn would fail due to + * violating AppConfig's simultaneous deployment rule. + */ + +const app = new App(); +const stack = new Stack(app, 'aws-appconfig-multi-config-env'); +const application = new Application(stack, 'MyApplicationForEnv'); +const env = new Environment(stack, 'MultiConfigEnvironment', { + application, +}); +const deploymentStrategy = new DeploymentStrategy(stack, 'QuickDeploymentStrategy', + { + rolloutStrategy: RolloutStrategy.linear({ + deploymentDuration: Duration.minutes(1), + growthFactor: 50, + }), + }); + +new HostedConfiguration(stack, 'MyFirstConfig', { + application, + content: ConfigurationContent.fromInline('first config content'), + deploymentStrategy, + deployTo: [env], +}); +new HostedConfiguration(stack, 'MySecondConfig', { + application, + content: ConfigurationContent.fromInline('second config content'), + deploymentStrategy, + deployTo: [env], +}); + +new IntegTest(app, 'appconfig-multi-config-env', { + testCases: [stack], +}); diff --git a/packages/aws-cdk-lib/aws-appconfig/README.md b/packages/aws-cdk-lib/aws-appconfig/README.md index 40aa40bf39035..ee5f4902b7902 100644 --- a/packages/aws-cdk-lib/aws-appconfig/README.md +++ b/packages/aws-cdk-lib/aws-appconfig/README.md @@ -252,8 +252,7 @@ new appconfig.HostedConfiguration(this, 'MyHostedConfiguration', { }); ``` -The `deployTo` parameter is used to specify which environments to deploy the configuration to. If this parameter is not -specified, there will not be a deployment. +The `deployTo` parameter is used to specify which environments to deploy the configuration to. A hosted configuration with `deployTo`: @@ -268,6 +267,95 @@ new appconfig.HostedConfiguration(this, 'MyHostedConfiguration', { }); ``` +When more than one configuration is set to deploy to the same environment, the +deployments will occur one at a time. This is done to satisfy +[AppConfig's constraint](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-deploying.html): +> [!NOTE] +> You can only deploy one configuration at a time to an environment. +> However, you can deploy one configuration each to different environments at the same time. + +The deployment order matches the order in which the configurations are declared. + +```ts +const app = new appconfig.Application(this, 'MyApp'); +const env = new appconfig.Environment(this, 'MyEnv', { + application: app, +}); + +new appconfig.HostedConfiguration(this, 'MyFirstHostedConfig', { + application: app, + deployTo: [env], + content: appconfig.ConfigurationContent.fromInlineText('This is my first configuration content.'), +}); + +new appconfig.HostedConfiguration(this, 'MySecondHostedConfig', { + application: app, + deployTo: [env], + content: appconfig.ConfigurationContent.fromInlineText('This is my second configuration content.'), +}); +``` + +If an application would benefit from a deployment order that differs from the +declared order, you can defer the decision by using `IEnvironment.addDeployment` +rather than the `deployTo` property. +In this example, `firstConfig` will be deployed before `secondConfig`. + +```ts +const app = new appconfig.Application(this, 'MyApp'); +const env = new appconfig.Environment(this, 'MyEnv', { + application: app, +}); + +const secondConfig = new appconfig.HostedConfiguration(this, 'MySecondHostedConfig', { + application: app, + content: appconfig.ConfigurationContent.fromInlineText('This is my second configuration content.'), +}); + +const firstConfig = new appconfig.HostedConfiguration(this, 'MyFirstHostedConfig', { + application: app, + deployTo: [env], + content: appconfig.ConfigurationContent.fromInlineText('This is my first configuration content.'), +}); + +env.addDeployment(secondConfig); +``` + +Alternatively, you can defer multiple deployments in favor of +`IEnvironment.addDeployments`, which allows you to declare multiple +configurations in the order they will be deployed. +In this example the deployment order will be +`firstConfig`, then `secondConfig`, and finally `thirdConfig`. + +```ts +const app = new appconfig.Application(this, 'MyApp'); +const env = new appconfig.Environment(this, 'MyEnv', { + application: app, +}); + +const secondConfig = new appconfig.HostedConfiguration(this, 'MySecondHostedConfig', { + application: app, + content: appconfig.ConfigurationContent.fromInlineText('This is my second configuration content.'), +}); + +const thirdConfig = new appconfig.HostedConfiguration(this, 'MyThirdHostedConfig', { + application: app, + content: appconfig.ConfigurationContent.fromInlineText('This is my third configuration content.'), +}); + +const firstConfig = new appconfig.HostedConfiguration(this, 'MyFirstHostedConfig', { + application: app, + content: appconfig.ConfigurationContent.fromInlineText('This is my first configuration content.'), +}); + +env.addDeployments(firstConfig, secondConfig, thirdConfig); +``` + +Any mix of `deployTo`, `addDeployment`, and `addDeployments` is permitted. +The declaration order will be respected regardless of the approach used. + +> [!IMPORTANT] +> If none of these options are utilized, there will not be any deployments. + ### SourcedConfiguration A sourced configuration represents configuration stored in any of the following: diff --git a/packages/aws-cdk-lib/aws-appconfig/lib/configuration.ts b/packages/aws-cdk-lib/aws-appconfig/lib/configuration.ts index a974aae0c4203..1e2197a411a56 100644 --- a/packages/aws-cdk-lib/aws-appconfig/lib/configuration.ts +++ b/packages/aws-cdk-lib/aws-appconfig/lib/configuration.ts @@ -2,12 +2,11 @@ import * as fs from 'fs'; import * as path from 'path'; import { Construct, IConstruct } from 'constructs'; -import { CfnConfigurationProfile, CfnDeployment, CfnHostedConfigurationVersion } from './appconfig.generated'; +import { CfnConfigurationProfile, CfnHostedConfigurationVersion } from './appconfig.generated'; import { IApplication } from './application'; import { DeploymentStrategy, IDeploymentStrategy, RolloutStrategy } from './deployment-strategy'; import { IEnvironment } from './environment'; import { ActionPoint, IEventDestination, ExtensionOptions, IExtension, IExtensible, ExtensibleBase } from './extension'; -import { getHash } from './private/hash'; import * as cp from '../../aws-codepipeline'; import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; @@ -62,7 +61,10 @@ export interface ConfigurationOptions { * The list of environments to deploy the configuration to. * * If this parameter is not specified, then there will be no - * deployment. + * deployment created alongside this configuration. + * + * Deployments can be added later using the `IEnvironment.addDeployment` or + * `IEnvironment.addDeployments` methods. * * @default - None. */ @@ -319,15 +321,7 @@ abstract class ConfigurationBase extends Construct implements IConfiguration, IE if ((this.deployTo && !this.deployTo.includes(environment))) { return; } - new CfnDeployment(this, `Deployment${getHash(environment.name!)}`, { - applicationId: this.application.applicationId, - configurationProfileId: this.configurationProfileId, - deploymentStrategyId: this.deploymentStrategy!.deploymentStrategyId, - environmentId: environment.environmentId, - configurationVersion: this.versionNumber!, - description: this.description, - kmsKeyIdentifier: this.deploymentKey?.keyArn, - }); + environment.addDeployment(this); }); } } diff --git a/packages/aws-cdk-lib/aws-appconfig/lib/environment.ts b/packages/aws-cdk-lib/aws-appconfig/lib/environment.ts index 437f267c1b366..be0965edcbe42 100644 --- a/packages/aws-cdk-lib/aws-appconfig/lib/environment.ts +++ b/packages/aws-cdk-lib/aws-appconfig/lib/environment.ts @@ -1,6 +1,7 @@ import { Construct } from 'constructs'; -import { CfnEnvironment } from './appconfig.generated'; +import { CfnDeployment, CfnEnvironment } from './appconfig.generated'; import { IApplication } from './application'; +import { IConfiguration } from './configuration'; import { ActionPoint, IEventDestination, ExtensionOptions, IExtension, IExtensible, ExtensibleBase } from './extension'; import { getHash } from './private/hash'; import * as cloudwatch from '../../aws-cloudwatch'; @@ -47,7 +48,38 @@ abstract class EnvironmentBase extends Resource implements IEnvironment, IExtens public abstract applicationId: string; public abstract environmentId: string; public abstract environmentArn: string; + public abstract name?: string; protected extensible!: ExtensibleBase; + protected deploymentQueue: Array = []; + + public addDeployment(configuration: IConfiguration): void { + if (this.name === undefined) { + throw new Error('Environment name must be known to add a Deployment'); + } + + const queueSize = this.deploymentQueue.push( + new CfnDeployment(configuration, `Deployment${getHash(this.name)}`, { + applicationId: configuration.application.applicationId, + configurationProfileId: configuration.configurationProfileId, + deploymentStrategyId: configuration.deploymentStrategy!.deploymentStrategyId, + environmentId: this.environmentId, + configurationVersion: configuration.versionNumber!, + description: configuration.description, + kmsKeyIdentifier: configuration.deploymentKey?.keyArn, + }), + ); + + // This internal member is used to keep track of configuration deployments + // as they are requested. Each element in this queue will depend on its + // predecessor, ensuring that the deployments occur sequentially in Cfn. + if (queueSize > 1) { + this.deploymentQueue[queueSize - 1].addDependency(this.deploymentQueue[queueSize - 2]); + } + } + + public addDeployments(...configurations: IConfiguration[]): void { + configurations.forEach((config) => this.addDeployment(config)); + } public on(actionPoint: ActionPoint, eventDestination: IEventDestination, options?: ExtensionOptions) { this.extensible.on(actionPoint, eventDestination, options); @@ -154,6 +186,7 @@ export class Environment extends EnvironmentBase { public readonly applicationId = applicationId; public readonly environmentId = environmentId; public readonly environmentArn = environmentArn; + public readonly name?: string; } return new Import(scope, id, { @@ -413,6 +446,24 @@ export interface IEnvironment extends IResource { */ readonly environmentArn: string; + /** + * Creates a deployment of the supplied configuration to this environment. + * Note that you can only deploy one configuration at a time to an environment. + * However, you can deploy one configuration each to different environments at the same time. + * If more than one deployment is requested for this environment, they will occur in the same order they were provided. + * + * @param configuration The configuration that will be deployed to this environment. + */ + addDeployment(configuration: IConfiguration): void; + + /** + * Creates a deployment for each of the supplied configurations to this environment. + * These configurations will be deployed in the same order as the input array. + * + * @param configurations The configurations that will be deployed to this environment. + */ + addDeployments(...configurations: Array): void; + /** * Adds an extension defined by the action point and event destination and also * creates an extension association to the environment. diff --git a/packages/aws-cdk-lib/aws-appconfig/test/environment.test.ts b/packages/aws-cdk-lib/aws-appconfig/test/environment.test.ts index dd2e5e278482f..eb59aa6742333 100644 --- a/packages/aws-cdk-lib/aws-appconfig/test/environment.test.ts +++ b/packages/aws-cdk-lib/aws-appconfig/test/environment.test.ts @@ -2,7 +2,7 @@ import { Template } from '../../assertions'; import { Alarm, CompositeAlarm, Metric } from '../../aws-cloudwatch'; import * as iam from '../../aws-iam'; import * as cdk from '../../core'; -import { Application, Environment, Monitor } from '../lib'; +import { Application, ConfigurationContent, Environment, HostedConfiguration, Monitor } from '../lib'; describe('environment', () => { test('default environment', () => { @@ -54,6 +54,216 @@ describe('environment', () => { }); }); + test('environment with single deployment', () => { + const stack = new cdk.Stack(); + const application = new Application(stack, 'MyAppConfig'); + const env = new Environment(stack, 'MyEnvironment', { + application, + }); + + const firstConfig = new HostedConfiguration(stack, 'FirstConfig', { + application, + content: ConfigurationContent.fromInlineText('This is my content 1'), + }); + env.addDeployment(firstConfig); + + const actual = Template.fromStack(stack); + + actual.hasResourceProperties('AWS::AppConfig::Environment', { + Name: 'MyEnvironment', + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + }); + + actual.hasResourceProperties('AWS::AppConfig::ConfigurationProfile', { + Name: 'FirstConfig', + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + LocationUri: 'hosted', + }); + actual.hasResourceProperties('AWS::AppConfig::HostedConfigurationVersion', { + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + ConfigurationProfileId: { + Ref: 'FirstConfigConfigurationProfileDEF37C63', + }, + Content: 'This is my content 1', + ContentType: 'text/plain', + }); + actual.hasResource('AWS::AppConfig::Deployment', { + Properties: { + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + EnvironmentId: { + Ref: 'MyEnvironment465E4DEA', + }, + ConfigurationVersion: { + Ref: 'FirstConfigC35E996C', + }, + ConfigurationProfileId: { + Ref: 'FirstConfigConfigurationProfileDEF37C63', + }, + DeploymentStrategyId: { + Ref: 'FirstConfigDeploymentStrategy863BBA9A', + }, + }, + }); + + actual.resourceCountIs('AWS::AppConfig::Deployment', 1); + }); + + test('environment with multiple deployments', () => { + const stack = new cdk.Stack(); + const application = new Application(stack, 'MyAppConfig'); + const env = new Environment(stack, 'MyEnvironment', { + application, + }); + + const firstConfig = new HostedConfiguration(stack, 'FirstConfig', { + application, + content: ConfigurationContent.fromInlineText('This is my content 1'), + }); + const secondConfig = new HostedConfiguration(stack, 'SecondConfig', { + application, + content: ConfigurationContent.fromInlineText('This is my content 2'), + }); + const thirdConfig = new HostedConfiguration(stack, 'ThirdConfig', { + application, + content: ConfigurationContent.fromInlineText('This is my content 3'), + }); + + env.addDeployments(firstConfig, secondConfig); + env.addDeployment(thirdConfig); + + const actual = Template.fromStack(stack); + + actual.hasResourceProperties('AWS::AppConfig::Environment', { + Name: 'MyEnvironment', + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + }); + + actual.hasResourceProperties('AWS::AppConfig::ConfigurationProfile', { + Name: 'FirstConfig', + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + LocationUri: 'hosted', + }); + actual.hasResourceProperties('AWS::AppConfig::HostedConfigurationVersion', { + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + ConfigurationProfileId: { + Ref: 'FirstConfigConfigurationProfileDEF37C63', + }, + Content: 'This is my content 1', + ContentType: 'text/plain', + }); + actual.hasResource('AWS::AppConfig::Deployment', { + Properties: { + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + EnvironmentId: { + Ref: 'MyEnvironment465E4DEA', + }, + ConfigurationVersion: { + Ref: 'FirstConfigC35E996C', + }, + ConfigurationProfileId: { + Ref: 'FirstConfigConfigurationProfileDEF37C63', + }, + DeploymentStrategyId: { + Ref: 'FirstConfigDeploymentStrategy863BBA9A', + }, + }, + }); + + actual.hasResourceProperties('AWS::AppConfig::ConfigurationProfile', { + Name: 'SecondConfig', + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + LocationUri: 'hosted', + }); + actual.hasResourceProperties('AWS::AppConfig::HostedConfigurationVersion', { + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + ConfigurationProfileId: { + Ref: 'SecondConfigConfigurationProfileE64FE7B4', + }, + Content: 'This is my content 2', + ContentType: 'text/plain', + }); + actual.hasResource('AWS::AppConfig::Deployment', { + Properties: { + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + EnvironmentId: { + Ref: 'MyEnvironment465E4DEA', + }, + ConfigurationVersion: { + Ref: 'SecondConfig22E40AAE', + }, + ConfigurationProfileId: { + Ref: 'SecondConfigConfigurationProfileE64FE7B4', + }, + DeploymentStrategyId: { + Ref: 'SecondConfigDeploymentStrategy9929738B', + }, + }, + DependsOn: ['FirstConfigDeployment52928BE68587B'], + }); + + actual.hasResourceProperties('AWS::AppConfig::ConfigurationProfile', { + Name: 'ThirdConfig', + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + LocationUri: 'hosted', + }); + actual.hasResourceProperties('AWS::AppConfig::HostedConfigurationVersion', { + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + ConfigurationProfileId: { + Ref: 'ThirdConfigConfigurationProfile4945C970', + }, + Content: 'This is my content 3', + ContentType: 'text/plain', + }); + actual.hasResource('AWS::AppConfig::Deployment', { + Properties: { + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + EnvironmentId: { + Ref: 'MyEnvironment465E4DEA', + }, + ConfigurationVersion: { + Ref: 'ThirdConfig498595D6', + }, + ConfigurationProfileId: { + Ref: 'ThirdConfigConfigurationProfile4945C970', + }, + DeploymentStrategyId: { + Ref: 'ThirdConfigDeploymentStrategy246FBD1A', + }, + }, + DependsOn: ['SecondConfigDeployment5292843F35B55'], + }); + + actual.resourceCountIs('AWS::AppConfig::Deployment', 3); + }); + test('environment with monitors with alarm and alarmRole', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig'); @@ -485,6 +695,20 @@ describe('environment', () => { expect(env.env.region).toEqual('us-west-2'); }); + test('from environment arn; cannot add new deployment', () => { + const stack = new cdk.Stack(); + const application = new Application(stack, 'MyAppConfig'); + const env = Environment.fromEnvironmentArn(stack, 'MyEnvironment', + 'arn:aws:appconfig:us-west-2:123456789012:application/abc123/environment/def456'); + + expect(() => { + env.addDeployment(new HostedConfiguration(stack, 'FirstConfig', { + application, + content: ConfigurationContent.fromInlineText('This is my content 1'), + })); + }).toThrow('Environment name must be known to add a Deployment'); + }); + test('from environment arn with no resource name', () => { const stack = new cdk.Stack(); expect(() => { @@ -536,4 +760,73 @@ describe('environment', () => { expect(env.env.account).toEqual('123456789012'); expect(env.env.region).toEqual('us-west-2'); }); + + test('from environment attributes; cannot add new deployment without name', () => { + const stack = new cdk.Stack(); + const application = new Application(stack, 'MyAppConfig'); + const env = Environment.fromEnvironmentAttributes(stack, 'MyEnvironment', { + application, + environmentId: 'def456', + }); + + expect(() => { + env.addDeployment(new HostedConfiguration(stack, 'FirstConfig', { + application, + content: ConfigurationContent.fromInlineText('This is my content 1'), + })); + }).toThrow('Environment name must be known to add a Deployment'); + }); + + test('from environment attributes with name; can add new deployment', () => { + const stack = new cdk.Stack(); + const application = new Application(stack, 'MyAppConfig'); + const env = Environment.fromEnvironmentAttributes(stack, 'MyEnvironment', { + application, + environmentId: 'def456', + name: 'NamedEnv', + }); + env.addDeployment(new HostedConfiguration(stack, 'FirstConfig', { + application, + content: ConfigurationContent.fromInlineText('This is my content 1'), + })); + + const actual = Template.fromStack(stack); + + actual.hasResourceProperties('AWS::AppConfig::ConfigurationProfile', { + Name: 'FirstConfig', + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + LocationUri: 'hosted', + }); + actual.hasResourceProperties('AWS::AppConfig::HostedConfigurationVersion', { + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + ConfigurationProfileId: { + Ref: 'FirstConfigConfigurationProfileDEF37C63', + }, + Content: 'This is my content 1', + ContentType: 'text/plain', + }); + actual.hasResource('AWS::AppConfig::Deployment', { + Properties: { + ApplicationId: { + Ref: 'MyAppConfigB4B63E75', + }, + EnvironmentId: 'def456', + ConfigurationVersion: { + Ref: 'FirstConfigC35E996C', + }, + ConfigurationProfileId: { + Ref: 'FirstConfigConfigurationProfileDEF37C63', + }, + DeploymentStrategyId: { + Ref: 'FirstConfigDeploymentStrategy863BBA9A', + }, + }, + }); + + actual.resourceCountIs('AWS::AppConfig::Deployment', 1); + }); });