From 226111d22c98ae8f583ef2ab228c8189883fdaff Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 29 Jun 2020 18:17:33 -0700 Subject: [PATCH] Introduce IEnvComponent, and use it in IAM. --- .../aws-certificatemanager/test/test.util.ts | 4 +- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 19 ++-- .../integ.load-balanced-fargate-service.ts | 10 +- .../test.load-balanced-fargate-service.ts | 19 +--- .../@aws-cdk/aws-iam/lib/account-utils.ts | 23 ----- packages/@aws-cdk/aws-iam/lib/grant.ts | 16 +++- packages/@aws-cdk/aws-iam/lib/group.ts | 2 +- packages/@aws-cdk/aws-iam/lib/lazy-role.ts | 2 +- .../aws-iam/lib/private/immutable-role.ts | 4 +- packages/@aws-cdk/aws-iam/lib/role.ts | 19 ++-- packages/@aws-cdk/aws-iam/lib/user.ts | 2 +- .../@aws-cdk/aws-route53/test/test.util.ts | 37 ++------ packages/@aws-cdk/aws-s3/lib/bucket.ts | 4 +- .../core/lib/environment-component.ts | 92 +++++++++++++++++++ packages/@aws-cdk/core/lib/index.ts | 1 + packages/@aws-cdk/core/lib/resource.ts | 49 ++++++++-- 16 files changed, 189 insertions(+), 114 deletions(-) delete mode 100644 packages/@aws-cdk/aws-iam/lib/account-utils.ts create mode 100644 packages/@aws-cdk/core/lib/environment-component.ts diff --git a/packages/@aws-cdk/aws-certificatemanager/test/test.util.ts b/packages/@aws-cdk/aws-certificatemanager/test/test.util.ts index c6f06819acfbd..bc37f3493dba8 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/test.util.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/test.util.ts @@ -1,5 +1,5 @@ import { PublicHostedZone } from '@aws-cdk/aws-route53'; -import { App, Stack } from '@aws-cdk/core'; +import { App, Stack, Token } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import { Certificate, DnsValidatedCertificate } from '../lib'; import { apexDomain, getCertificateRegion, isDnsValidatedCertificate } from '../lib/util'; @@ -101,7 +101,7 @@ export = { domainName: 'www.example.com', }); - test.equals(getCertificateRegion(certificate), '${Token[AWS.Region.4]}'); + test.ok(Token.isUnresolved(getCertificateRegion(certificate))); test.done(); }, }, diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index d4100c39c69dc..ecce72480e50f 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -3,8 +3,8 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import { - App, BootstraplessSynthesizer, Construct, DefaultStackSynthesizer, - IStackSynthesizer, Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token, + App, BootstraplessSynthesizer, Construct, DefaultStackSynthesizer, IStackSynthesizer, + Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token, } from '@aws-cdk/core'; import { ActionCategory, IAction, IPipeline, IStage } from './action'; import { CfnPipeline } from './codepipeline.generated'; @@ -400,13 +400,13 @@ export class Pipeline extends PipelineBase { const actionResource = action.actionProperties.resource; if (actionResource) { - const actionResourceStack = Stack.of(actionResource); - if (this.region !== actionResource.region) { - actionRegion = actionResource.region; + if (this.region.compare(actionResource.region).unEqual()) { + actionRegion = actionResource.region.toString(); + const actionResourceStack = Stack.of(actionResource); // if the resource is from a different stack in another region but the same account, // use that stack as home for the cross-region support resources if (pipelineStack.account === actionResourceStack.account && - actionResource.region === actionResourceStack.region) { + actionResource.region.compareToString(actionResourceStack.region).equalOrBothUnresolved()) { otherStack = actionResourceStack; } } @@ -851,10 +851,11 @@ export class Pipeline extends PipelineBase { } private requireRegion(): string { - if (Token.isUnresolved(this.region)) { + const region = this.region.toString(); + if (Token.isUnresolved(region)) { throw new Error('Pipeline stack which uses cross-environment actions must have an explicitly set region'); } - return this.region; + return region; } private requireApp(): App { @@ -932,4 +933,4 @@ class PipelineLocation { // runOrders are 1-based, so make the stageIndex also 1-based otherwise it's going to be confusing. return `Stage ${this.stageIndex + 1} Action ${this.action.runOrder} ('${this.stageName}'/'${this.actionName}')`; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.ts index 6b7c7ace3a775..322d5f684c6dc 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.ts @@ -1,6 +1,7 @@ import { Vpc } from '@aws-cdk/aws-ec2'; import { Cluster, ContainerImage } from '@aws-cdk/aws-ecs'; import { ApplicationProtocol } from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as route53 from '@aws-cdk/aws-route53'; import { App, Stack } from '@aws-cdk/core'; import { ApplicationLoadBalancedFargateService } from '../../lib'; @@ -20,15 +21,10 @@ new ApplicationLoadBalancedFargateService(stack, 'myService', { protocol: ApplicationProtocol.HTTPS, enableECSManagedTags: true, domainName: 'test.example.com', - domainZone: { + domainZone: route53.HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { hostedZoneId: 'fakeId', zoneName: 'example.com.', - hostedZoneArn: 'arn:aws:route53:::hostedzone/fakeId', - stack, - node: stack.node, - account: stack.account, - region: stack.region, - }, + }), }); app.synth(); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts index f4312798fae07..beabf7ddc3cd6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts @@ -3,6 +3,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import { ApplicationLoadBalancer, ApplicationProtocol, NetworkLoadBalancer } from '@aws-cdk/aws-elasticloadbalancingv2'; import * as iam from '@aws-cdk/aws-iam'; +import * as route53 from '@aws-cdk/aws-route53'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as ecsPatterns from '../../lib'; @@ -370,15 +371,10 @@ export = { cluster, protocol: ApplicationProtocol.HTTPS, domainName: 'domain.com', - domainZone: { + domainZone: route53.HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { hostedZoneId: 'fakeId', zoneName: 'domain.com', - hostedZoneArn: 'arn:aws:route53:::hostedzone/fakeId', - stack, - node: stack.node, - account: stack.account, - region: stack.region, - }, + }), taskImageOptions: { containerPort: 2015, image: ecs.ContainerImage.fromRegistry('abiosoft/caddy'), @@ -410,15 +406,10 @@ export = { cluster, protocol: ApplicationProtocol.HTTPS, domainName: 'test.domain.com', - domainZone: { + domainZone: route53.HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { hostedZoneId: 'fakeId', zoneName: 'domain.com.', - hostedZoneArn: 'arn:aws:route53:::hostedzone/fakeId', - stack, - node: stack.node, - account: stack.account, - region: stack.region, - }, + }), taskImageOptions: { containerPort: 2015, image: ecs.ContainerImage.fromRegistry('abiosoft/caddy'), diff --git a/packages/@aws-cdk/aws-iam/lib/account-utils.ts b/packages/@aws-cdk/aws-iam/lib/account-utils.ts deleted file mode 100644 index b60eaf8294c70..0000000000000 --- a/packages/@aws-cdk/aws-iam/lib/account-utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as cdk from '@aws-cdk/core'; - -export enum AccountCompare { - SAME, - DIFFERENT, - UNKNOWN, -} - -export function accountsAreSameOrUnresolved(account1: string | undefined, account2: string | undefined): AccountCompare { - const firstIsUnresolved = cdk.Token.isUnresolved(account1); - const secondIsUnresolved = cdk.Token.isUnresolved(account2); - - if (firstIsUnresolved && secondIsUnresolved) { - return AccountCompare.SAME; - } - if (firstIsUnresolved || secondIsUnresolved) { - return AccountCompare.UNKNOWN; - } - - return account1 === account2 - ? AccountCompare.SAME - : AccountCompare.DIFFERENT; -} diff --git a/packages/@aws-cdk/aws-iam/lib/grant.ts b/packages/@aws-cdk/aws-iam/lib/grant.ts index 83b28f3bdc756..b79c1cd230149 100644 --- a/packages/@aws-cdk/aws-iam/lib/grant.ts +++ b/packages/@aws-cdk/aws-iam/lib/grant.ts @@ -1,5 +1,4 @@ import * as cdk from '@aws-cdk/core'; -import { AccountCompare, accountsAreSameOrUnresolved } from './account-utils'; import { PolicyStatement } from './policy-statement'; import { IGrantable, IPrincipal } from './principals'; @@ -122,10 +121,17 @@ export class Grant implements cdk.IDependable { scope: options.resource, }); - const definitelySameAccount = accountsAreSameOrUnresolved( - options.grantee.grantPrincipal.principalAccount, - options.resource.account) === AccountCompare.SAME; - if (result.success && definitelySameAccount) { + const compareResult = options.grantee.grantPrincipal.principalAccount + ? options.resource.account.compareToString(options.grantee.grantPrincipal.principalAccount) + : undefined; + // if both accounts are tokens, we assume here they are the same + const sameAccount: boolean = compareResult?.equalOrBothUnresolved() ?? + // if the principal doesn't have an account (for example, a service principal), + // we assume the accounts are the same + true; + // If we added to the principal AND we're in the same account, then we're done. + // If not, it's a different account and we must also add a trust policy on the resource. + if (result.success && sameAccount) { return result; } diff --git a/packages/@aws-cdk/aws-iam/lib/group.ts b/packages/@aws-cdk/aws-iam/lib/group.ts index 5c3f4ecb236a1..b90503d271c63 100644 --- a/packages/@aws-cdk/aws-iam/lib/group.ts +++ b/packages/@aws-cdk/aws-iam/lib/group.ts @@ -72,7 +72,7 @@ abstract class GroupBase extends Resource implements IGroup { public abstract readonly groupArn: string; public readonly grantPrincipal: IPrincipal = this; - public readonly principalAccount: string | undefined = this.account; + public readonly principalAccount: string | undefined = this.account.toString(); public readonly assumeRoleAction: string = 'sts:AssumeRole'; private readonly attachedPolicies = new AttachedPolicies(); diff --git a/packages/@aws-cdk/aws-iam/lib/lazy-role.ts b/packages/@aws-cdk/aws-iam/lib/lazy-role.ts index b4ab57dad2096..b4ab6b8819b35 100644 --- a/packages/@aws-cdk/aws-iam/lib/lazy-role.ts +++ b/packages/@aws-cdk/aws-iam/lib/lazy-role.ts @@ -27,7 +27,7 @@ export interface LazyRoleProps extends RoleProps { */ export class LazyRole extends cdk.Resource implements IRole { public readonly grantPrincipal: IPrincipal = this; - public readonly principalAccount: string | undefined = this.account; + public readonly principalAccount: string | undefined = this.account.toString(); public readonly assumeRoleAction: string = 'sts:AssumeRole'; private role?: Role; diff --git a/packages/@aws-cdk/aws-iam/lib/private/immutable-role.ts b/packages/@aws-cdk/aws-iam/lib/private/immutable-role.ts index 2e7d3426db3b3..26b30350eb2a9 100644 --- a/packages/@aws-cdk/aws-iam/lib/private/immutable-role.ts +++ b/packages/@aws-cdk/aws-iam/lib/private/immutable-role.ts @@ -30,8 +30,8 @@ export class ImmutableRole extends Resource implements IRole { constructor(scope: Construct, id: string, private readonly role: IRole) { super(scope, id, { - account: role.account, - region: role.region, + account: role.account.toString(), + region: role.region.toString(), }); // implement IDependable privately diff --git a/packages/@aws-cdk/aws-iam/lib/role.ts b/packages/@aws-cdk/aws-iam/lib/role.ts index 4352c693d5fac..3fe7d8586c7a8 100644 --- a/packages/@aws-cdk/aws-iam/lib/role.ts +++ b/packages/@aws-cdk/aws-iam/lib/role.ts @@ -1,4 +1,4 @@ -import { Construct, Duration, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; +import { Construct, Duration, Lazy, Resource, Stack } from '@aws-cdk/core'; import { Grant } from './grant'; import { CfnRole } from './iam.generated'; import { IIdentity } from './identity-base'; @@ -213,9 +213,7 @@ export class Role extends Resource implements IRole { } public attachInlinePolicy(policy: Policy): void { - const policyAccount = Stack.of(policy).account; - - if (accountsAreEqualOrOneIsUnresolved(policyAccount, roleAccount)) { + if (this.account.compare(policy.account).equalOrAnyUnresolved()) { this.attachedPolicies.attach(policy); policy.attachToRole(this); } @@ -246,20 +244,15 @@ export class Role extends Resource implements IRole { } const importedRole = new Import(scope, id); - return options.mutable !== false && accountsAreEqualOrOneIsUnresolved(scopeStack.account, importedRole.account) + return options.mutable !== false && + // we only return an immutable Role if both accounts were explicitly provided, and different + importedRole.account.compareToString(scopeStack.account).equalOrAnyUnresolved() ? importedRole : new ImmutableRole(scope, `ImmutableRole${id}`, importedRole); - - function accountsAreEqualOrOneIsUnresolved( - account1: string | undefined, - account2: string | undefined): boolean { - return Token.isUnresolved(account1) || Token.isUnresolved(account2) || - account1 === account2; - } } public readonly grantPrincipal: IPrincipal = this; - public readonly principalAccount: string | undefined = this.account; + public readonly principalAccount: string | undefined = this.account.toString(); public readonly assumeRoleAction: string = 'sts:AssumeRole'; diff --git a/packages/@aws-cdk/aws-iam/lib/user.ts b/packages/@aws-cdk/aws-iam/lib/user.ts index 0be8ac94a5465..87a69c6312a09 100644 --- a/packages/@aws-cdk/aws-iam/lib/user.ts +++ b/packages/@aws-cdk/aws-iam/lib/user.ts @@ -176,7 +176,7 @@ export class User extends Resource implements IIdentity, IUser { } public readonly grantPrincipal: IPrincipal = this; - public readonly principalAccount: string | undefined = this.account; + public readonly principalAccount: string | undefined = this.account.toString(); public readonly assumeRoleAction: string = 'sts:AssumeRole'; /** diff --git a/packages/@aws-cdk/aws-route53/test/test.util.ts b/packages/@aws-cdk/aws-route53/test/test.util.ts index 6c240724a9017..d589b058e40cc 100644 --- a/packages/@aws-cdk/aws-route53/test/test.util.ts +++ b/packages/@aws-cdk/aws-route53/test/test.util.ts @@ -1,5 +1,6 @@ import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; +import { HostedZone } from '../lib'; import * as util from '../lib/util'; export = { @@ -20,15 +21,10 @@ export = { // WHEN const providedName = 'test.domain.com.'; - const qualified = util.determineFullyQualifiedDomainName(providedName, { + const qualified = util.determineFullyQualifiedDomainName(providedName, HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { hostedZoneId: 'fakeId', zoneName: 'ignored', - hostedZoneArn: 'arn:aws:route53:::hostedzone/fakeId', - stack, - node: stack.node, - account: stack.account, - region: stack.region, - }); + })); // THEN test.equal(qualified, 'test.domain.com.'); @@ -41,15 +37,10 @@ export = { // WHEN const providedName = 'test.domain.com'; - const qualified = util.determineFullyQualifiedDomainName(providedName, { + const qualified = util.determineFullyQualifiedDomainName(providedName, HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { hostedZoneId: 'fakeId', zoneName: 'test.domain.com.', - hostedZoneArn: 'arn:aws:route53:::hostedzone/fakeId', - stack, - node: stack.node, - account: stack.account, - region: stack.region, - }); + })); // THEN test.equal(qualified, 'test.domain.com.'); @@ -62,15 +53,10 @@ export = { // WHEN const providedName = 'test.domain.com'; - const qualified = util.determineFullyQualifiedDomainName(providedName, { + const qualified = util.determineFullyQualifiedDomainName(providedName, HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { hostedZoneId: 'fakeId', zoneName: 'domain.com.', - hostedZoneArn: 'arn:aws:route53:::hostedzone/fakeId', - stack, - node: stack.node, - account: stack.account, - region: stack.region, - }); + })); // THEN test.equal(qualified, 'test.domain.com.'); @@ -83,15 +69,10 @@ export = { // WHEN const providedName = 'test'; - const qualified = util.determineFullyQualifiedDomainName(providedName, { + const qualified = util.determineFullyQualifiedDomainName(providedName, HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { hostedZoneId: 'fakeId', zoneName: 'domain.com.', - hostedZoneArn: 'arn:aws:route53:::hostedzone/fakeId', - stack, - node: stack.node, - account: stack.account, - region: stack.region, - }); + })); // THEN test.equal(qualified, 'test.domain.com.'); diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 8e8bf60472190..61fb0a2ae1fa1 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -995,8 +995,8 @@ export class Bucket extends BucketBase { public readonly bucketDualStackDomainName = attrs.bucketDualStackDomainName || `${bucketName}.s3.dualstack.${region}.${urlSuffix}`; public readonly bucketWebsiteNewUrlFormat = newUrlFormat; public readonly encryptionKey = attrs.encryptionKey; - public readonly account = attrs.account ?? stack.account; - public readonly region = attrs.region ?? stack.region; + public readonly account = Resource.makeEnvComponent(attrs.account ?? stack.account); + public readonly region = Resource.makeEnvComponent(attrs.region ?? stack.region); public policy?: BucketPolicy = undefined; protected autoCreatePolicy = false; protected disallowPublicAccess = false; diff --git a/packages/@aws-cdk/core/lib/environment-component.ts b/packages/@aws-cdk/core/lib/environment-component.ts new file mode 100644 index 0000000000000..8f73d43a76f12 --- /dev/null +++ b/packages/@aws-cdk/core/lib/environment-component.ts @@ -0,0 +1,92 @@ +/** + * An enum-like class that represents the result of comparing either two {@link IEnvComponent}s, + * or a {@link IEnvComponent}, and a string. + */ +export class EnvComponentComparison { + /** + * This means we're certain the two components are NOT + * Tokens, and identical. + */ + public static readonly SAME = new EnvComponentComparison(); + + /** + * This means we're certain the two components are NOT + * Tokens, and different. + */ + public static readonly DIFFERENT = new EnvComponentComparison(); + + /** This means exactly one of the components is a Token. */ + public static readonly ONE_UNRESOLVED = new EnvComponentComparison(); + + /** This means both components are Tokens. */ + public static readonly BOTH_UNRESOLVED = new EnvComponentComparison(); + + private constructor() { + } + + /** + * Returns true if: + * - Neither component was a Token, and they were equal to each other. + * Or: + * - Both component were Tokens. + * + * @returns true if `this` is either SAME or BOTH_UNRESOLVED, + * false if `this` is either DIFFERENT or ONE_UNRESOLVED + */ + public equalOrBothUnresolved(): boolean { + return this === EnvComponentComparison.SAME || + this === EnvComponentComparison.BOTH_UNRESOLVED; + } + + /** + * Returns true if: + * - Neither component was a Token, and they were equal to each other. + * Or: + * - Either component (or both!) was a Token. + * + * @returns true if `this` is either SAME or ONE_UNRESOLVED or BOTH_UNRESOLVED, + * false if `this` is DIFFERENT + */ + public equalOrAnyUnresolved(): boolean { + return this !== EnvComponentComparison.DIFFERENT; + } + + /** + * Returns true if: + * - Neither component was a Token, and they were not equal to each other. + * Or: + * - Exactly one of the components was a Token. + * + * @returns true if `this` is either DIFFERENT or ONE_UNRESOLVED, + * false if `this` is either SAME or BOTH_UNRESOLVED + */ + public unEqual(): boolean { + return !this.equalOrBothUnresolved(); + } +} + +/** + * Represents a component of an Environment, + * like an AWS account ID, + * or the code name of an AWS region. + * Since they can be Tokens representing deploy-time values like AWS::AccountId and AWS::Region, + * their comparison logic is slightly more complicated than just comparing strings for equality. + */ +export interface IEnvComponent { + /** + * Compare two components with each other. + */ + compare(envComponent: IEnvComponent): EnvComponentComparison; + + /** + * Compare this component to a string representation. + */ + compareToString(envComponent: string): EnvComponentComparison; + + /** + * Returns a string representation of this component. + * Useful when passing it to places that expect strings, + * like Environment. + */ + toString(): string; +} diff --git a/packages/@aws-cdk/core/lib/index.ts b/packages/@aws-cdk/core/lib/index.ts index 6c54a222901d6..558a2dff110ee 100644 --- a/packages/@aws-cdk/core/lib/index.ts +++ b/packages/@aws-cdk/core/lib/index.ts @@ -37,6 +37,7 @@ export * from './stack-trace'; export * from './app'; export * from './context-provider'; export * from './environment'; +export * from './environment-component'; export * from './runtime'; export * from './secret-value'; diff --git a/packages/@aws-cdk/core/lib/resource.ts b/packages/@aws-cdk/core/lib/resource.ts index 2e0dbbc3c4fbc..d45ff0f95e59e 100644 --- a/packages/@aws-cdk/core/lib/resource.ts +++ b/packages/@aws-cdk/core/lib/resource.ts @@ -1,5 +1,6 @@ import { ArnComponents } from './arn'; import { Construct, IConstruct } from './construct-compat'; +import { EnvComponentComparison, IEnvComponent } from './environment-component'; import { Lazy } from './lazy'; import { generatePhysicalName, isGeneratedWhenNeededMarker } from './private/physical-name-generator'; import { IResolveContext } from './resolvable'; @@ -24,7 +25,7 @@ export interface IResource extends IConstruct { * (those obtained from static methods like fromRoleArn, fromBucketName, etc.) * that might be different than the stack they were imported into. */ - readonly account: string; + readonly account: IEnvComponent; /** * The AWS region that this resource belongs to. @@ -35,7 +36,7 @@ export interface IResource extends IConstruct { * (those obtained from static methods like fromRoleArn, fromBucketName, etc.) * that might be different than the stack they were imported into. */ - readonly region: string; + readonly region: IEnvComponent; } /** @@ -74,9 +75,13 @@ export interface ResourceProps { * A construct which represents an AWS resource. */ export abstract class Resource extends Construct implements IResource { + protected static makeEnvComponent(component: string): IEnvComponent { + return new EnvComponent(component); + } + public readonly stack: Stack; - public readonly account: string; - public readonly region: string; + public readonly account: IEnvComponent; + public readonly region: IEnvComponent; /** * Returns a string-encoded token that resolves to the physical name that @@ -99,8 +104,8 @@ export abstract class Resource extends Construct implements IResource { super(scope, id); this.stack = Stack.of(this); - this.account = props.account ?? this.stack.account; - this.region = props.region ?? this.stack.region; + this.account = Resource.makeEnvComponent(props.account ?? this.stack.account); + this.region = Resource.makeEnvComponent(props.region ?? this.stack.region); let physicalName = props.physicalName; @@ -209,3 +214,35 @@ export abstract class Resource extends Construct implements IResource { }); } } + +class EnvComponent implements IEnvComponent { + private readonly component: string; + + constructor(component: string) { + this.component = component; + } + + public compare(envComponent: IEnvComponent): EnvComponentComparison { + return this.compareToString(envComponent.toString()); + } + + public compareToString(thatComponent: string): EnvComponentComparison { + const firstIsUnresolved = Token.isUnresolved(this.component); + const secondIsUnresolved = Token.isUnresolved(thatComponent); + + if (firstIsUnresolved && secondIsUnresolved) { + return EnvComponentComparison.BOTH_UNRESOLVED; + } + if (firstIsUnresolved || secondIsUnresolved) { + return EnvComponentComparison.ONE_UNRESOLVED; + } + + return this.component === thatComponent + ? EnvComponentComparison.SAME + : EnvComponentComparison.DIFFERENT; + } + + public toString(): string { + return this.component; + } +}