From 96242d73c0ae853524a567aece86f8a8a514495c Mon Sep 17 00:00:00 2001 From: Rico Hermans Date: Tue, 27 Dec 2022 12:23:27 +0100 Subject: [PATCH] fix(codedeploy): referenced Applications are not environment-aware (#23405) Applications (and associated DeploymentGroups) that were referenced using `XxxApplication.fromXxxApplicationName` always assumed they lived in the "current" stack. Add `XxxApplication.fromXxxApplicationArn` for all types, which will take account and region from the ARN, allowing cross-environment references. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/base-deployment-config.ts | 2 +- .../aws-codedeploy/lib/ecs/application.ts | 26 +++- .../lib/ecs/deployment-config.ts | 2 +- .../lib/ecs/deployment-group.ts | 44 +++--- .../aws-codedeploy/lib/lambda/application.ts | 26 +++- .../lib/lambda/custom-deployment-config.ts | 2 +- .../lib/lambda/deployment-config.ts | 2 +- .../lib/lambda/deployment-group.ts | 45 +++--- .../lib/private/base-deployment-group.ts | 138 ++++++++++++++++++ .../aws-codedeploy/lib/{ => private}/utils.ts | 32 ++-- .../aws-codedeploy/lib/server/application.ts | 25 +++- .../lib/server/deployment-config.ts | 2 +- .../lib/server/deployment-group.ts | 73 +++------ .../test/ecs/deployment-group.test.ts | 22 +++ .../test/lambda/deployment-group.test.ts | 22 +++ .../test/server/deployment-group.test.ts | 21 +++ 16 files changed, 356 insertions(+), 128 deletions(-) create mode 100644 packages/@aws-cdk/aws-codedeploy/lib/private/base-deployment-group.ts rename packages/@aws-cdk/aws-codedeploy/lib/{ => private}/utils.ts (74%) diff --git a/packages/@aws-cdk/aws-codedeploy/lib/base-deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/base-deployment-config.ts index 35c0c453cc7c2..0df406088fdb1 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/base-deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/base-deployment-config.ts @@ -2,8 +2,8 @@ import { ArnFormat, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnDeploymentConfig } from './codedeploy.generated'; import { MinimumHealthyHosts } from './host-health-config'; +import { arnForDeploymentConfig, validateName } from './private/utils'; import { TrafficRouting } from './traffic-routing-config'; -import { arnForDeploymentConfig, validateName } from './utils'; /** * The base class for ServerDeploymentConfig, EcsDeploymentConfig, diff --git a/packages/@aws-cdk/aws-codedeploy/lib/ecs/application.ts b/packages/@aws-cdk/aws-codedeploy/lib/ecs/application.ts index 289a6660c8c4f..9b9173a7d17c0 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/ecs/application.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/ecs/application.ts @@ -1,7 +1,7 @@ -import { ArnFormat, IResource, Resource } from '@aws-cdk/core'; +import { ArnFormat, IResource, Resource, Stack, Arn } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApplication } from '../codedeploy.generated'; -import { arnForApplication, validateName } from '../utils'; +import { arnForApplication, validateName } from '../private/utils'; /** * Represents a reference to a CodeDeploy Application deploying to Amazon ECS. @@ -42,6 +42,9 @@ export class EcsApplication extends Resource implements IEcsApplication { /** * Import an Application defined either outside the CDK, or in a different CDK Stack. * + * The Application's account and region are assumed to be the same as the stack it is being imported + * into. If not, use `fromEcsApplicationArn`. + * * @param scope the parent Construct for this new Construct * @param id the logical ID of this new Construct * @param ecsApplicationName the name of the application to import @@ -49,13 +52,28 @@ export class EcsApplication extends Resource implements IEcsApplication { */ public static fromEcsApplicationName(scope: Construct, id: string, ecsApplicationName: string): IEcsApplication { class Import extends Resource implements IEcsApplication { - public applicationArn = arnForApplication(ecsApplicationName); + public applicationArn = arnForApplication(Stack.of(scope), ecsApplicationName); public applicationName = ecsApplicationName; } return new Import(scope, id); } + /** + * Import an Application defined either outside the CDK, or in a different CDK Stack, by ARN. + * + * @param scope the parent Construct for this new Construct + * @param id the logical ID of this new Construct + * @param ecsApplicationArn the ARN of the application to import + * @returns a Construct representing a reference to an existing Application + */ + public static fromEcsApplicationArn(scope: Construct, id: string, ecsApplicationArn: string): IEcsApplication { + return new class extends Resource implements IEcsApplication { + public applicationArn = ecsApplicationArn; + public applicationName = Arn.split(ecsApplicationArn, ArnFormat.COLON_RESOURCE_NAME).resourceName ?? ''; + } (scope, id, { environmentFromArn: ecsApplicationArn }); + } + public readonly applicationArn: string; public readonly applicationName: string; @@ -70,7 +88,7 @@ export class EcsApplication extends Resource implements IEcsApplication { }); this.applicationName = this.getResourceNameAttribute(resource.ref); - this.applicationArn = this.getResourceArnAttribute(arnForApplication(resource.ref), { + this.applicationArn = this.getResourceArnAttribute(arnForApplication(Stack.of(scope), resource.ref), { service: 'codedeploy', resource: 'application', resourceName: this.physicalName, diff --git a/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-config.ts index be6204b4b10ad..32a5552834e3a 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-config.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { BaseDeploymentConfig, BaseDeploymentConfigOptions, ComputePlatform, IBaseDeploymentConfig } from '../base-deployment-config'; +import { deploymentConfig } from '../private/utils'; import { TrafficRouting } from '../traffic-routing-config'; -import { deploymentConfig } from '../utils'; /** * The Deployment Configuration of an ECS Deployment Group. diff --git a/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-group.ts index 762f6210e51c3..02c7e809b4275 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-group.ts @@ -5,8 +5,9 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnDeploymentGroup } from '../codedeploy.generated'; +import { ImportedDeploymentGroupBase, DeploymentGroupBase } from '../private/base-deployment-group'; +import { renderAlarmConfiguration, renderAutoRollbackConfiguration } from '../private/utils'; import { AutoRollbackConfig } from '../rollback-config'; -import { arnForDeploymentGroup, renderAlarmConfiguration, renderAutoRollbackConfiguration, validateName } from '../utils'; import { IEcsApplication, EcsApplication } from './application'; import { EcsDeploymentConfig, IEcsDeploymentConfig } from './deployment-config'; @@ -182,9 +183,11 @@ export interface EcsDeploymentGroupProps { * A CodeDeploy deployment group that orchestrates ECS blue-green deployments. * @resource AWS::CodeDeploy::DeploymentGroup */ -export class EcsDeploymentGroup extends cdk.Resource implements IEcsDeploymentGroup { +export class EcsDeploymentGroup extends DeploymentGroupBase implements IEcsDeploymentGroup { /** - * Import an ECS Deployment Group defined outside the CDK app. + * Reference an ECS Deployment Group defined outside the CDK app. + * + * Account and region for the DeploymentGroup are taken from the application. * * @param scope the parent Construct for this new Construct * @param id the logical ID of this new Construct @@ -199,8 +202,6 @@ export class EcsDeploymentGroup extends cdk.Resource implements IEcsDeploymentGr } public readonly application: IEcsApplication; - public readonly deploymentGroupName: string; - public readonly deploymentGroupArn: string; public readonly deploymentConfig: IEcsDeploymentConfig; /** * The service Role of this Deployment Group. @@ -211,16 +212,15 @@ export class EcsDeploymentGroup extends cdk.Resource implements IEcsDeploymentGr constructor(scope: Construct, id: string, props: EcsDeploymentGroupProps) { super(scope, id, { - physicalName: props.deploymentGroupName, + deploymentGroupName: props.deploymentGroupName, + role: props.role, + roleConstructId: 'ServiceRole', }); + this.role = this._role; this.application = props.application || new EcsApplication(this, 'Application'); this.alarms = props.alarms || []; - this.role = props.role || new iam.Role(this, 'ServiceRole', { - assumedBy: new iam.ServicePrincipal('codedeploy.amazonaws.com'), - }); - this.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AWSCodeDeployRoleForECS')); this.deploymentConfig = props.deploymentConfig || EcsDeploymentConfig.ALL_AT_ONCE; @@ -261,21 +261,13 @@ export class EcsDeploymentGroup extends cdk.Resource implements IEcsDeploymentGr autoRollbackConfiguration: cdk.Lazy.any({ produce: () => renderAutoRollbackConfiguration(this.alarms, props.autoRollback) }), }); - this.deploymentGroupName = this.getResourceNameAttribute(resource.ref); - this.deploymentGroupArn = this.getResourceArnAttribute(arnForDeploymentGroup(this.application.applicationName, resource.ref), { - service: 'codedeploy', - resource: 'deploymentgroup', - resourceName: `${this.application.applicationName}/${this.physicalName}`, - arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME, - }); + this._setNameAndArn(resource, this.application); // If the deployment config is a construct, add a dependency to ensure the deployment config // is created before the deployment group is. if (Construct.isConstruct(this.deploymentConfig)) { this.node.addDependency(this.deploymentConfig); } - - this.node.addValidation({ validate: () => validateName('Deployment group', this.physicalName) }); } /** @@ -355,17 +347,17 @@ export interface EcsDeploymentGroupAttributes { readonly deploymentConfig?: IEcsDeploymentConfig; } -class ImportedEcsDeploymentGroup extends cdk.Resource implements IEcsDeploymentGroup { +class ImportedEcsDeploymentGroup extends ImportedDeploymentGroupBase implements IEcsDeploymentGroup { public readonly application: IEcsApplication; - public readonly deploymentGroupName: string; - public readonly deploymentGroupArn: string; public readonly deploymentConfig: IEcsDeploymentConfig; - constructor(scope:Construct, id: string, props: EcsDeploymentGroupAttributes) { - super(scope, id); + constructor(scope: Construct, id: string, props: EcsDeploymentGroupAttributes) { + super(scope, id, { + application: props.application, + deploymentGroupName: props.deploymentGroupName, + }); + this.application = props.application; - this.deploymentGroupName = props.deploymentGroupName; - this.deploymentGroupArn = arnForDeploymentGroup(props.application.applicationName, props.deploymentGroupName); this.deploymentConfig = props.deploymentConfig || EcsDeploymentConfig.ALL_AT_ONCE; } } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/lambda/application.ts b/packages/@aws-cdk/aws-codedeploy/lib/lambda/application.ts index e0d27f8dabc34..c3c20a5afb2d9 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/lambda/application.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/lambda/application.ts @@ -1,7 +1,7 @@ -import { ArnFormat, IResource, Resource } from '@aws-cdk/core'; +import { ArnFormat, IResource, Resource, Stack, Arn } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApplication } from '../codedeploy.generated'; -import { arnForApplication, validateName } from '../utils'; +import { arnForApplication, validateName } from '../private/utils'; /** * Represents a reference to a CodeDeploy Application deploying to AWS Lambda. @@ -42,6 +42,9 @@ export class LambdaApplication extends Resource implements ILambdaApplication { /** * Import an Application defined either outside the CDK, or in a different CDK Stack. * + * The Application's account and region are assumed to be the same as the stack it is being imported + * into. If not, use `fromLambdaApplicationArn`. + * * @param scope the parent Construct for this new Construct * @param id the logical ID of this new Construct * @param lambdaApplicationName the name of the application to import @@ -49,13 +52,28 @@ export class LambdaApplication extends Resource implements ILambdaApplication { */ public static fromLambdaApplicationName(scope: Construct, id: string, lambdaApplicationName: string): ILambdaApplication { class Import extends Resource implements ILambdaApplication { - public applicationArn = arnForApplication(lambdaApplicationName); + public applicationArn = arnForApplication(Stack.of(scope), lambdaApplicationName); public applicationName = lambdaApplicationName; } return new Import(scope, id); } + /** + * Import an Application defined either outside the CDK, or in a different CDK Stack, by ARN. + * + * @param scope the parent Construct for this new Construct + * @param id the logical ID of this new Construct + * @param lambdaApplicationArn the ARN of the application to import + * @returns a Construct representing a reference to an existing Application + */ + public static fromLambdaApplicationArn(scope: Construct, id: string, lambdaApplicationArn: string): ILambdaApplication { + return new class extends Resource implements ILambdaApplication { + public applicationArn = lambdaApplicationArn; + public applicationName = Arn.split(lambdaApplicationArn, ArnFormat.COLON_RESOURCE_NAME).resourceName ?? ''; + }(scope, id, { environmentFromArn: lambdaApplicationArn }); + } + public readonly applicationArn: string; public readonly applicationName: string; @@ -70,7 +88,7 @@ export class LambdaApplication extends Resource implements ILambdaApplication { }); this.applicationName = this.getResourceNameAttribute(resource.ref); - this.applicationArn = this.getResourceArnAttribute(arnForApplication(resource.ref), { + this.applicationArn = this.getResourceArnAttribute(arnForApplication(Stack.of(this), resource.ref), { service: 'codedeploy', resource: 'application', resourceName: this.physicalName, diff --git a/packages/@aws-cdk/aws-codedeploy/lib/lambda/custom-deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/lambda/custom-deployment-config.ts index fe4ceeb249af2..4a9a6d09e426d 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/lambda/custom-deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/lambda/custom-deployment-config.ts @@ -1,7 +1,7 @@ import { Duration, Names, Resource } from '@aws-cdk/core'; import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '@aws-cdk/custom-resources'; import { Construct } from 'constructs'; -import { arnForDeploymentConfig, validateName } from '../utils'; +import { arnForDeploymentConfig, validateName } from '../private/utils'; import { ILambdaDeploymentConfig } from './deployment-config'; /** diff --git a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-config.ts index a86da3222b5f1..e049ecbe08887 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-config.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { BaseDeploymentConfig, BaseDeploymentConfigOptions, ComputePlatform, IBaseDeploymentConfig } from '../base-deployment-config'; +import { deploymentConfig } from '../private/utils'; import { TrafficRouting } from '../traffic-routing-config'; -import { deploymentConfig } from '../utils'; /** * The Deployment Configuration of a Lambda Deployment Group. diff --git a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts index 5968402efdd1f..44f1cd01eded7 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts @@ -4,8 +4,9 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnDeploymentGroup } from '../codedeploy.generated'; +import { ImportedDeploymentGroupBase, DeploymentGroupBase } from '../private/base-deployment-group'; +import { renderAlarmConfiguration, renderAutoRollbackConfiguration } from '../private/utils'; import { AutoRollbackConfig } from '../rollback-config'; -import { arnForDeploymentGroup, renderAlarmConfiguration, renderAutoRollbackConfiguration, validateName } from '../utils'; import { ILambdaApplication, LambdaApplication } from './application'; import { ILambdaDeploymentConfig, LambdaDeploymentConfig } from './deployment-config'; @@ -120,10 +121,12 @@ export interface LambdaDeploymentGroupProps { /** * @resource AWS::CodeDeploy::DeploymentGroup */ -export class LambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploymentGroup { +export class LambdaDeploymentGroup extends DeploymentGroupBase implements ILambdaDeploymentGroup { /** * Import an Lambda Deployment Group defined either outside the CDK app, or in a different AWS region. * + * Account and region for the DeploymentGroup are taken from the application. + * * @param scope the parent Construct for this new Construct * @param id the logical ID of this new Construct * @param attrs the properties of the referenced Deployment Group @@ -137,9 +140,10 @@ export class LambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploy } public readonly application: ILambdaApplication; - public readonly deploymentGroupName: string; - public readonly deploymentGroupArn: string; public readonly deploymentConfig: ILambdaDeploymentConfig; + /** + * The service Role of this Deployment Group. + */ public readonly role: iam.IRole; private readonly alarms: cloudwatch.IAlarm[]; @@ -148,16 +152,15 @@ export class LambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploy constructor(scope: Construct, id: string, props: LambdaDeploymentGroupProps) { super(scope, id, { - physicalName: props.deploymentGroupName, + deploymentGroupName: props.deploymentGroupName, + role: props.role, + roleConstructId: 'ServiceRole', }); + this.role = this._role; this.application = props.application || new LambdaApplication(this, 'Application'); this.alarms = props.alarms || []; - this.role = props.role || new iam.Role(this, 'ServiceRole', { - assumedBy: new iam.ServicePrincipal('codedeploy.amazonaws.com'), - }); - this.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSCodeDeployRoleForLambdaLimited')); this.deploymentConfig = props.deploymentConfig || LambdaDeploymentConfig.CANARY_10PERCENT_5MINUTES; @@ -174,13 +177,7 @@ export class LambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploy autoRollbackConfiguration: cdk.Lazy.any({ produce: () => renderAutoRollbackConfiguration(this.alarms, props.autoRollback) }), }); - this.deploymentGroupName = this.getResourceNameAttribute(resource.ref); - this.deploymentGroupArn = this.getResourceArnAttribute(arnForDeploymentGroup(this.application.applicationName, resource.ref), { - service: 'codedeploy', - resource: 'deploymentgroup', - resourceName: `${this.application.applicationName}/${this.physicalName}`, - arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME, - }); + this._setNameAndArn(resource, this.application); if (props.preHook) { this.addPreHook(props.preHook); @@ -203,8 +200,6 @@ export class LambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploy if (this.deploymentConfig instanceof Construct) { this.node.addDependency(this.deploymentConfig); } - - this.node.addValidation({ validate: () => validateName('Deployment group', this.physicalName) }); } /** @@ -284,17 +279,17 @@ export interface LambdaDeploymentGroupAttributes { readonly deploymentConfig?: ILambdaDeploymentConfig; } -class ImportedLambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploymentGroup { +class ImportedLambdaDeploymentGroup extends ImportedDeploymentGroupBase implements ILambdaDeploymentGroup { public readonly application: ILambdaApplication; - public readonly deploymentGroupName: string; - public readonly deploymentGroupArn: string; public readonly deploymentConfig: ILambdaDeploymentConfig; - constructor(scope:Construct, id: string, props: LambdaDeploymentGroupAttributes) { - super(scope, id); + constructor(scope: Construct, id: string, props: LambdaDeploymentGroupAttributes) { + super(scope, id, { + application: props.application, + deploymentGroupName: props.deploymentGroupName, + }); + this.application = props.application; - this.deploymentGroupName = props.deploymentGroupName; - this.deploymentGroupArn = arnForDeploymentGroup(props.application.applicationName, props.deploymentGroupName); this.deploymentConfig = props.deploymentConfig || LambdaDeploymentConfig.CANARY_10PERCENT_5MINUTES; } } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/private/base-deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/private/base-deployment-group.ts new file mode 100644 index 0000000000000..3ea12aa50d702 --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/lib/private/base-deployment-group.ts @@ -0,0 +1,138 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { Resource, IResource, ArnFormat, Arn, Aws } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnDeploymentGroup } from '../codedeploy.generated'; +import { validateName } from './utils'; + +/** + * Structural typing, not jsii compatible but doesn't need to be + */ +interface IApplicationLike extends IResource { + readonly applicationArn: string; + readonly applicationName: string; +} + +/** + */ +export interface ImportedDeploymentGroupBaseProps { + /** + * The reference to the CodeDeploy Application that this Deployment Group belongs to. + */ + readonly application: IApplicationLike; + + /** + * The physical, human-readable name of the CodeDeploy Deployment Group + * that we are referencing. + * + * @default Either deploymentGroupName or deploymentGroupArn is required + */ + readonly deploymentGroupName: string; +} + +/** + * @internal + */ +export class ImportedDeploymentGroupBase extends Resource { + public readonly deploymentGroupName: string; + public readonly deploymentGroupArn: string; + + constructor(scope: Construct, id: string, props: ImportedDeploymentGroupBaseProps) { + const deploymentGroupName = props.deploymentGroupName; + const deploymentGroupArn = Arn.format({ + partition: Aws.PARTITION, + account: props.application.env.account, + region: props.application.env.region, + service: 'codedeploy', + resource: 'deploymentgroup', + resourceName: `${props.application.applicationName}/${deploymentGroupName}`, + arnFormat: ArnFormat.COLON_RESOURCE_NAME, + }); + + super(scope, id, { environmentFromArn: deploymentGroupArn }); + this.deploymentGroupName = deploymentGroupName; + this.deploymentGroupArn = deploymentGroupArn; + } +} + +export interface DeploymentGroupBaseProps { + /** + * The physical, human-readable name of the CodeDeploy Deployment Group. + * + * @default An auto-generated name will be used. + */ + readonly deploymentGroupName?: string; + + /** + * The service Role of this Deployment Group. + * + * @default A new Role will be created. + */ + readonly role?: iam.IRole; + + /** + * Id of the role construct, if created by this construct + * + * Exists because when we factored this out, there was a difference between the + * 3 deployment groups. + */ + readonly roleConstructId: string; +} + +/** + * @internal + */ +export class DeploymentGroupBase extends Resource { + /** + * The name of the Deployment Group. + */ + public readonly deploymentGroupName!: string; + + /** + * The ARN of the Deployment Group. + */ + public readonly deploymentGroupArn!: string; + + /** + * The service Role of this Deployment Group. + * + * (Can't make `role` properly public here, as it's typed as optional in one + * interface and typing it here as definitely set interferes with that.) + * + * @internal + */ + public readonly _role: iam.IRole; + + constructor(scope: Construct, id: string, props: DeploymentGroupBaseProps) { + super(scope, id, { + physicalName: props.deploymentGroupName, + }); + + this._role = props.role || new iam.Role(this, props.roleConstructId, { + assumedBy: new iam.ServicePrincipal('codedeploy.amazonaws.com'), + }); + + this.node.addValidation({ validate: () => validateName('Deployment group', this.physicalName) }); + } + + /** + * Set name and ARN properties. + * + * Must be called in the child constructor. + * + * @internal + */ + protected _setNameAndArn(resource: CfnDeploymentGroup, application: IApplicationLike) { + (this as any).deploymentGroupName = this.getResourceNameAttribute(resource.ref); + (this as any).deploymentGroupArn = this.getResourceArnAttribute(this.stack.formatArn({ + service: 'codedeploy', + resource: 'deploymentgroup', + resourceName: `${application.applicationName}/${resource.ref}`, + arnFormat: ArnFormat.COLON_RESOURCE_NAME, + }), { + service: 'codedeploy', + resource: 'deploymentgroup', + resourceName: `${application.applicationName}/${this.physicalName}`, + arnFormat: ArnFormat.COLON_RESOURCE_NAME, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codedeploy/lib/utils.ts b/packages/@aws-cdk/aws-codedeploy/lib/private/utils.ts similarity index 74% rename from packages/@aws-cdk/aws-codedeploy/lib/utils.ts rename to packages/@aws-cdk/aws-codedeploy/lib/private/utils.ts index 137998a23eb37..9dccb367d8578 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/utils.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/private/utils.ts @@ -1,19 +1,33 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { Aws, Token } from '@aws-cdk/core'; -import { IBaseDeploymentConfig } from './base-deployment-config'; -import { CfnDeploymentGroup } from './codedeploy.generated'; -import { AutoRollbackConfig } from './rollback-config'; +import { Token, Stack, ArnFormat, Arn, Fn, Aws } from '@aws-cdk/core'; +import { IBaseDeploymentConfig } from '../base-deployment-config'; +import { CfnDeploymentGroup } from '../codedeploy.generated'; +import { AutoRollbackConfig } from '../rollback-config'; -export function arnForApplication(applicationName: string): string { - return `arn:${Aws.PARTITION}:codedeploy:${Aws.REGION}:${Aws.ACCOUNT_ID}:application:${applicationName}`; +export function arnForApplication(stack: Stack, applicationName: string): string { + return stack.formatArn({ + service: 'codedeploy', + resource: 'application', + resourceName: applicationName, + arnFormat: ArnFormat.COLON_RESOURCE_NAME, + }); } -export function arnForDeploymentGroup(applicationName: string, deploymentGroupName: string): string { - return `arn:${Aws.PARTITION}:codedeploy:${Aws.REGION}:${Aws.ACCOUNT_ID}:deploymentgroup:${applicationName}/${deploymentGroupName}`; +export function nameFromDeploymentGroupArn(deploymentGroupArn: string): string { + const components = Arn.split(deploymentGroupArn, ArnFormat.COLON_RESOURCE_NAME); + return Fn.select(1, Fn.split('/', components.resourceName ?? '')); } export function arnForDeploymentConfig(name: string): string { - return `arn:${Aws.PARTITION}:codedeploy:${Aws.REGION}:${Aws.ACCOUNT_ID}:deploymentconfig:${name}`; + return Arn.format({ + partition: Aws.PARTITION, + account: Aws.ACCOUNT_ID, + region: Aws.REGION, + service: 'codedeploy', + resource: 'deploymentconfig', + resourceName: name, + arnFormat: ArnFormat.COLON_RESOURCE_NAME, + }); } export function renderAlarmConfiguration(alarms: cloudwatch.IAlarm[], ignorePollAlarmFailure?: boolean): diff --git a/packages/@aws-cdk/aws-codedeploy/lib/server/application.ts b/packages/@aws-cdk/aws-codedeploy/lib/server/application.ts index a04d789fce566..16addfd0537ae 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/server/application.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/server/application.ts @@ -1,7 +1,7 @@ -import { ArnFormat, IResource, Resource } from '@aws-cdk/core'; +import { ArnFormat, IResource, Resource, Stack, Arn } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApplication } from '../codedeploy.generated'; -import { arnForApplication, validateName } from '../utils'; +import { arnForApplication, validateName } from '../private/utils'; /** * Represents a reference to a CodeDeploy Application deploying to EC2/on-premise instances. @@ -42,6 +42,9 @@ export class ServerApplication extends Resource implements IServerApplication { /** * Import an Application defined either outside the CDK app, or in a different region. * + * The Application's account and region are assumed to be the same as the stack it is being imported + * into. If not, use `fromServerApplicationArn`. + * * @param scope the parent Construct for this new Construct * @param id the logical ID of this new Construct * @param serverApplicationName the name of the application to import @@ -49,12 +52,26 @@ export class ServerApplication extends Resource implements IServerApplication { */ public static fromServerApplicationName(scope: Construct, id: string, serverApplicationName: string): IServerApplication { class Import extends Resource implements IServerApplication { - public readonly applicationArn = arnForApplication(serverApplicationName); + public readonly applicationArn = arnForApplication(Stack.of(scope), serverApplicationName); public readonly applicationName = serverApplicationName; } return new Import(scope, id); + } + /** + * Import an Application defined either outside the CDK, or in a different CDK Stack, by ARN. + * + * @param scope the parent Construct for this new Construct + * @param id the logical ID of this new Construct + * @param serverApplicationArn the ARN of the application to import + * @returns a Construct representing a reference to an existing Application + */ + public static fromServerApplicationArn(scope: Construct, id: string, serverApplicationArn: string): IServerApplication { + return new class extends Resource implements IServerApplication { + public applicationArn = serverApplicationArn; + public applicationName = Arn.split(serverApplicationArn, ArnFormat.COLON_RESOURCE_NAME).resourceName ?? ''; + }(scope, id, { environmentFromArn: serverApplicationArn }); } public readonly applicationArn: string; @@ -71,7 +88,7 @@ export class ServerApplication extends Resource implements IServerApplication { }); this.applicationName = this.getResourceNameAttribute(resource.ref); - this.applicationArn = this.getResourceArnAttribute(arnForApplication(resource.ref), { + this.applicationArn = this.getResourceArnAttribute(arnForApplication(Stack.of(scope), resource.ref), { service: 'codedeploy', resource: 'application', resourceName: this.physicalName, diff --git a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-config.ts index c3ae4a75ffb59..d09af03cf2fa5 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-config.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; import { BaseDeploymentConfig, BaseDeploymentConfigOptions, IBaseDeploymentConfig } from '../base-deployment-config'; import { MinimumHealthyHosts } from '../host-health-config'; -import { deploymentConfig } from '../utils'; +import { deploymentConfig } from '../private/utils'; /** * The Deployment Configuration of an EC2/on-premise Deployment Group. diff --git a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts index afb6aee8545e0..448d4f9dde5e8 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts @@ -4,11 +4,11 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; -import { ArnFormat } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnDeploymentGroup } from '../codedeploy.generated'; +import { ImportedDeploymentGroupBase, DeploymentGroupBase } from '../private/base-deployment-group'; +import { renderAlarmConfiguration, renderAutoRollbackConfiguration } from '../private/utils'; import { AutoRollbackConfig } from '../rollback-config'; -import { arnForDeploymentGroup, renderAlarmConfiguration, renderAutoRollbackConfiguration, validateName } from '../utils'; import { IServerApplication, ServerApplication } from './application'; import { IServerDeploymentConfig, ServerDeploymentConfig } from './deployment-config'; import { LoadBalancer, LoadBalancerGeneration } from './load-balancer'; @@ -55,43 +55,20 @@ export interface ServerDeploymentGroupAttributes { readonly deploymentConfig?: IServerDeploymentConfig; } -/** - * Represents a reference to a CodeDeploy EC2/on-premise Deployment Group. - * - * If you're managing the Deployment Group alongside the rest of your CDK resources, - * use the {@link ServerDeploymentGroup} class. - * - * If you want to reference an already existing Deployment Group, - * or one defined in a different CDK Stack, - * use the {@link #import} method. - */ -abstract class ServerDeploymentGroupBase extends cdk.Resource implements IServerDeploymentGroup { - public abstract readonly application: IServerApplication; - public abstract readonly role?: iam.IRole; - public abstract readonly deploymentGroupName: string; - public abstract readonly deploymentGroupArn: string; - public readonly deploymentConfig: IServerDeploymentConfig; - public abstract readonly autoScalingGroups?: autoscaling.IAutoScalingGroup[]; - - constructor(scope: Construct, id: string, deploymentConfig?: IServerDeploymentConfig, props?: cdk.ResourceProps) { - super(scope, id, props); - this.deploymentConfig = deploymentConfig || ServerDeploymentConfig.ONE_AT_A_TIME; - } -} - -class ImportedServerDeploymentGroup extends ServerDeploymentGroupBase { +class ImportedServerDeploymentGroup extends ImportedDeploymentGroupBase implements IServerDeploymentGroup { public readonly application: IServerApplication; public readonly role?: iam.Role = undefined; - public readonly deploymentGroupName: string; - public readonly deploymentGroupArn: string; public readonly autoScalingGroups?: autoscaling.AutoScalingGroup[] = undefined; + public readonly deploymentConfig: IServerDeploymentConfig; constructor(scope: Construct, id: string, props: ServerDeploymentGroupAttributes) { - super(scope, id, props.deploymentConfig); + super(scope, id, { + application: props.application, + deploymentGroupName: props.deploymentGroupName, + }); this.application = props.application; - this.deploymentGroupName = props.deploymentGroupName; - this.deploymentGroupArn = arnForDeploymentGroup(props.application.applicationName, props.deploymentGroupName); + this.deploymentConfig = props.deploymentConfig || ServerDeploymentConfig.ONE_AT_A_TIME; } } @@ -238,7 +215,7 @@ export interface ServerDeploymentGroupProps { * A CodeDeploy Deployment Group that deploys to EC2/on-premise instances. * @resource AWS::CodeDeploy::DeploymentGroup */ -export class ServerDeploymentGroup extends ServerDeploymentGroupBase { +export class ServerDeploymentGroup extends DeploymentGroupBase implements IServerDeploymentGroup { /** * Import an EC2/on-premise Deployment Group defined either outside the CDK app, * or in a different region. @@ -256,9 +233,11 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { } public readonly application: IServerApplication; + public readonly deploymentConfig: IServerDeploymentConfig; + /** + * The service Role of this Deployment Group. + */ public readonly role?: iam.IRole; - public readonly deploymentGroupArn: string; - public readonly deploymentGroupName: string; private readonly _autoScalingGroups: autoscaling.IAutoScalingGroup[]; private readonly installAgent: boolean; @@ -266,19 +245,19 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { private readonly alarms: cloudwatch.IAlarm[]; constructor(scope: Construct, id: string, props: ServerDeploymentGroupProps = {}) { - super(scope, id, props.deploymentConfig, { - physicalName: props.deploymentGroupName, + super(scope, id, { + deploymentGroupName: props.deploymentGroupName, + role: props.role, + roleConstructId: 'Role', }); + this.role = this._role; this.application = props.application || new ServerApplication(this, 'Application', { applicationName: props.deploymentGroupName === cdk.PhysicalName.GENERATE_IF_NEEDED ? cdk.PhysicalName.GENERATE_IF_NEEDED : undefined, }); + this.deploymentConfig = props.deploymentConfig || ServerDeploymentConfig.ONE_AT_A_TIME; - this.role = props.role || new iam.Role(this, 'Role', { - assumedBy: new iam.ServicePrincipal('codedeploy.amazonaws.com'), - managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSCodeDeployRole')], - }); - + this.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSCodeDeployRole')); this._autoScalingGroups = props.autoScalingGroups || []; this.installAgent = props.installAgent ?? true; this.codeDeployBucket = s3.Bucket.fromBucketName(this, 'Bucket', `aws-codedeploy-${cdk.Stack.of(this).region}`); @@ -307,15 +286,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { autoRollbackConfiguration: cdk.Lazy.any({ produce: () => renderAutoRollbackConfiguration(this.alarms, props.autoRollback) }), }); - this.deploymentGroupName = this.getResourceNameAttribute(resource.ref); - this.deploymentGroupArn = this.getResourceArnAttribute(arnForDeploymentGroup(this.application.applicationName, resource.ref), { - service: 'codedeploy', - resource: 'deploymentgroup', - resourceName: `${this.application.applicationName}/${this.physicalName}`, - arnFormat: ArnFormat.COLON_RESOURCE_NAME, - }); - - this.node.addValidation({ validate: () => validateName('Deployment group', this.physicalName) }); + this._setNameAndArn(resource, this.application); } /** diff --git a/packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts index 62f2b4402c009..aed7b8655516e 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts @@ -848,4 +848,26 @@ describe('CodeDeploy ECS DeploymentGroup', () => { }, }); }); + + test('deploymentGroup from Arn knows its account and region', () => { + // GIVEN + const stack = new cdk.Stack(undefined, 'Stack', { env: { account: '111111111111', region: 'blabla-1' } }); + + // WHEN + const application = codedeploy.EcsApplication.fromEcsApplicationArn(stack, 'Application', 'arn:aws:codedeploy:theregion-1:222222222222:application:MyApplication'); + const group = codedeploy.EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(stack, 'Group', { + application, + deploymentGroupName: 'DeploymentGroup', + }); + + // THEN + expect(application.env).toEqual(expect.objectContaining({ + account: '222222222222', + region: 'theregion-1', + })); + expect(group.env).toEqual(expect.objectContaining({ + account: '222222222222', + region: 'theregion-1', + })); + }); }); diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts index 959c98bbce139..9eae61c7a4e4f 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts @@ -615,6 +615,28 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }); }); + + test('deploymentGroup from Arn knows its account and region', () => { + // GIVEN + const stack = new cdk.Stack(undefined, 'Stack', { env: { account: '111111111111', region: 'blabla-1' } }); + + // WHEN + const application = codedeploy.LambdaApplication.fromLambdaApplicationArn(stack, 'Application', 'arn:aws:codedeploy:theregion-1:222222222222:application:MyApplication'); + const group = codedeploy.LambdaDeploymentGroup.fromLambdaDeploymentGroupAttributes(stack, 'Group', { + application, + deploymentGroupName: 'DeploymentGroup', + }); + + // THEN + expect(application.env).toEqual(expect.objectContaining({ + account: '222222222222', + region: 'theregion-1', + })); + expect(group.env).toEqual(expect.objectContaining({ + account: '222222222222', + region: 'theregion-1', + })); + }); }); describe('imported with fromLambdaDeploymentGroupAttributes', () => { diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts index 6b22a90cbeedb..4f360aa9cb9c2 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts @@ -494,4 +494,25 @@ describe('CodeDeploy Server Deployment Group', () => { expect(() => app.synth()).toThrow('Deployment group name: "my name" can only contain letters (a-z, A-Z), numbers (0-9), periods (.), underscores (_), + (plus signs), = (equals signs), , (commas), @ (at signs), - (minus signs).'); }); + test('deploymentGroup from Arn knows its account and region', () => { + // GIVEN + const stack = new cdk.Stack(undefined, 'Stack', { env: { account: '111111111111', region: 'blabla-1' } }); + + // WHEN + const application = codedeploy.ServerApplication.fromServerApplicationArn(stack, 'Application', 'arn:aws:codedeploy:theregion-1:222222222222:application:MyApplication'); + const group = codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes(stack, 'Group', { + application, + deploymentGroupName: 'DeploymentGroup', + }); + + // THEN + expect(application.env).toEqual(expect.objectContaining({ + account: '222222222222', + region: 'theregion-1', + })); + expect(group.env).toEqual(expect.objectContaining({ + account: '222222222222', + region: 'theregion-1', + })); + }); });