diff --git a/packages/@aws-cdk/cx-api/lib/environment.ts b/packages/@aws-cdk/cx-api/lib/environment.ts index c23e75c7829e3..c5551f673c8e9 100644 --- a/packages/@aws-cdk/cx-api/lib/environment.ts +++ b/packages/@aws-cdk/cx-api/lib/environment.ts @@ -39,6 +39,16 @@ export class EnvironmentUtils { return { account, region, name: environment }; } + /** + * Build an environment object from an account and region + */ + public static make(account: string, region: string): Environment { + return { account, region, name: this.format(account, region) }; + } + + /** + * Format an environment string from an account and region + */ public static format(account: string, region: string): string { return `aws://${account}/${region}`; } diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 8834dbfa08a42..41e8336ced9c0 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -56,7 +56,8 @@ async function parseCommandLineArguments() { .option('tags', { type: 'array', alias: 't', desc: 'Tags to add for the stack (KEY=VALUE)', nargs: 1, requiresArg: true, default: [] }) .option('execute', {type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true}) .option('trust', { type: 'array', desc: 'The (space-separated) list of AWS account IDs that should be trusted to perform deployments into this environment', default: [], hidden: true }) - .option('cloudformation-execution-policies', { type: 'array', desc: 'The (space-separated) list of Managed Policy ARNs that should be attached to the role performing deployments into this environment. Required if --trust was passed', default: [], hidden: true }), + .option('cloudformation-execution-policies', { type: 'array', desc: 'The (space-separated) list of Managed Policy ARNs that should be attached to the role performing deployments into this environment. Required if --trust was passed', default: [], hidden: true }) + .option('force', { alias: 'f', type: 'boolean', desc: 'Always bootstrap even if it would downgrade template version', default: false }), ) .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', yargs => yargs .option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times.', default: [] }) @@ -209,14 +210,18 @@ async function initCommandLine() { }); case 'bootstrap': - return await cli.bootstrap(args.ENVIRONMENTS, toolkitStackName, args.roleArn, !!process.env.CDK_NEW_BOOTSTRAP, { - bucketName: configuration.settings.get(['toolkitBucket', 'bucketName']), - kmsKeyId: configuration.settings.get(['toolkitBucket', 'kmsKeyId']), - tags: configuration.settings.get(['tags']), - execute: args.execute, - trustedAccounts: args.trust, - cloudFormationExecutionPolicies: args.cloudformationExecutionPolicies, - }); + return await cli.bootstrap(args.ENVIRONMENTS, toolkitStackName, + args.roleArn, + !!process.env.CDK_NEW_BOOTSTRAP, + argv.force, + { + bucketName: configuration.settings.get(['toolkitBucket', 'bucketName']), + kmsKeyId: configuration.settings.get(['toolkitBucket', 'kmsKeyId']), + tags: configuration.settings.get(['tags']), + execute: args.execute, + trustedAccounts: args.trust, + cloudFormationExecutionPolicies: args.cloudformationExecutionPolicies, + }); case 'deploy': const parameterMap: { [name: string]: string | undefined } = {}; diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts index 22dc175235aeb..98c2e03ac1a62 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts @@ -110,11 +110,10 @@ export class SdkProvider { /** * Return an SDK which can do operations in the given environment * - * The `region` and `accountId` parameters are interpreted as in `resolveEnvironment()` (which is to - * say, `undefined` doesn't do what you expect). + * The `environment` parameter is resolved first (see `resolveEnvironment()`). */ - public async forEnvironment(accountId: string | undefined, region: string | undefined, mode: Mode): Promise { - const env = await this.resolveEnvironment(accountId, region); + public async forEnvironment(environment: cxapi.Environment, mode: Mode): Promise { + const env = await this.resolveEnvironment(environment); const creds = await this.obtainCredentials(env.account, mode); return new SDK(creds, env.region, this.sdkOptions); } @@ -150,30 +149,26 @@ export class SdkProvider { /** * Resolve the environment for a stack * - * `undefined` actually means `undefined`, and is NOT changed to default values! Only the magic values UNKNOWN_REGION - * and UNKNOWN_ACCOUNT will be replaced with looked-up values! + * Replaces the magic values `UNKNOWN_REGION` and `UNKNOWN_ACCOUNT` + * with the defaults for the current SDK configuration (`~/.aws/config` or + * otherwise). + * + * It is an error if `UNKNOWN_ACCOUNT` is used but the user hasn't configured + * any SDK credentials. */ - public async resolveEnvironment(accountId: string | undefined, region: string | undefined) { - region = region !== cxapi.UNKNOWN_REGION ? region : this.defaultRegion; - accountId = accountId !== cxapi.UNKNOWN_ACCOUNT ? accountId : (await this.defaultAccount())?.accountId; - - if (!region) { - throw new Error('AWS region must be configured either when you configure your CDK stack or through the environment'); - } + public async resolveEnvironment(env: cxapi.Environment): Promise { + const region = env.region !== cxapi.UNKNOWN_REGION ? env.region : this.defaultRegion; + const account = env.account !== cxapi.UNKNOWN_ACCOUNT ? env.account : (await this.defaultAccount())?.accountId; - if (!accountId) { + if (!account) { throw new Error('Unable to resolve AWS account to use. It must be either configured when you define your CDK or through the environment'); } - const environment: cxapi.Environment = { - region, account: accountId, name: cxapi.EnvironmentUtils.format(accountId, region), + return { + region, + account, + name: cxapi.EnvironmentUtils.format(account, region), }; - - return environment; - } - - public async resolveEnvironmentObject(env: cxapi.Environment) { - return this.resolveEnvironment(env.account, env.region); } /** diff --git a/packages/aws-cdk/lib/api/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap-environment.ts deleted file mode 100644 index 30a7c3b6cf1ff..0000000000000 --- a/packages/aws-cdk/lib/api/bootstrap-environment.ts +++ /dev/null @@ -1,161 +0,0 @@ -import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import * as cxapi from '@aws-cdk/cx-api'; -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import { Tag } from '../cdk-toolkit'; -import { Mode, SdkProvider } from './aws-auth'; -import { deployStack, DeployStackResult } from './deploy-stack'; - -// tslint:disable:max-line-length - -/** @experimental */ -export const BUCKET_NAME_OUTPUT = 'BucketName'; -/** @experimental */ -export const REPOSITORY_NAME_OUTPUT = 'RepositoryName'; -/** @experimental */ -export const BUCKET_DOMAIN_NAME_OUTPUT = 'BucketDomainName'; - -export interface BootstrapEnvironmentProps { - /** - * The name to be given to the CDK Bootstrap bucket. - * - * @default - a name is generated by CloudFormation. - */ - readonly bucketName?: string; - - /** - * The ID of an existing KMS key to be used for encrypting items in the bucket. - * - * @default - the default KMS key for S3 will be used. - */ - readonly kmsKeyId?: string; - /** - * Tags for cdktoolkit stack. - * - * @default - None. - */ - readonly tags?: Tag[]; - /** - * Whether to execute the changeset or only create it and leave it in review. - * @default true - */ - readonly execute?: boolean; - - /** - * The list of AWS account IDs that are trusted to deploy into the environment being bootstrapped. - * - * @default - only the bootstrapped account can deploy into this environment - */ - readonly trustedAccounts?: string[]; - - /** - * The ARNs of the IAM managed policies that should be attached to the role performing CloudFormation deployments. - * In most cases, this will be the AdministratorAccess policy. - * At least one policy is required if {@link trustedAccounts} were passed. - * - * @default - the role will have no policies attached - */ - readonly cloudFormationExecutionPolicies?: string[]; -} - -/** @experimental */ -export async function bootstrapEnvironment(environment: cxapi.Environment, sdkProvider: SdkProvider, toolkitStackName: string, roleArn: string | undefined, props: BootstrapEnvironmentProps = {}): Promise { - if (props.trustedAccounts?.length) { - throw new Error('--trust can only be passed for the new bootstrap experience!'); - } - if (props.cloudFormationExecutionPolicies?.length) { - throw new Error('--cloudformation-execution-policies can only be passed for the new bootstrap experience!'); - } - - const template = { - Description: 'The CDK Toolkit Stack. It was created by `cdk bootstrap` and manages resources necessary for managing your Cloud Applications with AWS CDK.', - Resources: { - StagingBucket: { - Type: 'AWS::S3::Bucket', - Properties: { - BucketName: props.bucketName, - AccessControl: 'Private', - BucketEncryption: { - ServerSideEncryptionConfiguration: [{ - ServerSideEncryptionByDefault: { - SSEAlgorithm: 'aws:kms', - KMSMasterKeyID: props.kmsKeyId, - }, - }], - }, - PublicAccessBlockConfiguration: { - BlockPublicAcls: true, - BlockPublicPolicy: true, - IgnorePublicAcls: true, - RestrictPublicBuckets: true, - }, - }, - }, - StagingBucketPolicy: { - Type: 'AWS::S3::BucketPolicy', - Properties: { - Bucket: { Ref: 'StagingBucket' }, - PolicyDocument: { - Id: 'AccessControl', - Version: '2012-10-17', - Statement: [ - { - Sid: 'AllowSSLRequestsOnly', - Action: 's3:*', - Effect: 'Deny', - Resource: [ - { 'Fn::Sub': '${StagingBucket.Arn}' }, - { 'Fn::Sub': '${StagingBucket.Arn}/*' }, - ], - Condition: { - Bool: { 'aws:SecureTransport': 'false' }, - }, - Principal: '*', - }, - ], - }, - }, - - }, - }, - Outputs: { - [BUCKET_NAME_OUTPUT]: { - Description: 'The name of the S3 bucket owned by the CDK toolkit stack', - Value: { Ref: 'StagingBucket' }, - }, - [BUCKET_DOMAIN_NAME_OUTPUT]: { - Description: 'The domain name of the S3 bucket owned by the CDK toolkit stack', - Value: { 'Fn::GetAtt': ['StagingBucket', 'RegionalDomainName'] }, - }, - }, - }; - - const outdir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-bootstrap')); - const builder = new cxapi.CloudAssemblyBuilder(outdir); - const templateFile = `${toolkitStackName}.template.json`; - - await fs.writeJson(path.join(builder.outdir, templateFile), template, { spaces: 2 }); - - builder.addArtifact(toolkitStackName, { - type: cxschema.ArtifactType.AWS_CLOUDFORMATION_STACK, - environment: cxapi.EnvironmentUtils.format(environment.account, environment.region), - properties: { - templateFile, - }, - }); - - const assembly = builder.buildAssembly(); - - const resolvedEnvironment = await sdkProvider.resolveEnvironment(environment.account, environment.region); - - return await deployStack({ - stack: assembly.getStackByName(toolkitStackName), - resolvedEnvironment, - sdk: await sdkProvider.forEnvironment(environment.account, environment.region, Mode.ForWriting), - sdkProvider, - roleArn, - tags: props.tags, - execute: props.execute, - }); -} diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts new file mode 100644 index 0000000000000..2bf3e4ede46dd --- /dev/null +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts @@ -0,0 +1,65 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import * as path from 'path'; +import { loadStructuredFile } from '../../serialize'; +import { SdkProvider } from '../aws-auth'; +import { DeployStackResult } from '../deploy-stack'; +import { BootstrapEnvironmentOptions } from './bootstrap-props'; +import { deployBootstrapStack } from './deploy-bootstrap'; +import { legacyBootstrapTemplate } from './legacy-template'; + +// tslint:disable:max-line-length + +/** + * Deploy legacy bootstrap stack + * + * @experimental + */ +export async function bootstrapEnvironment(environment: cxapi.Environment, sdkProvider: SdkProvider, options: BootstrapEnvironmentOptions): Promise { + const params = options.parameters ?? {}; + + if (params.trustedAccounts?.length) { + throw new Error('--trust can only be passed for the new bootstrap experience.'); + } + if (params.cloudFormationExecutionPolicies?.length) { + throw new Error('--cloudformation-execution-policies can only be passed for the new bootstrap experience.'); + } + + return deployBootstrapStack( + legacyBootstrapTemplate(params), + {}, + environment, + sdkProvider, + options); +} + +/** + * Deploy CI/CD-ready bootstrap stack from template + * + * @experimental + */ +export async function bootstrapEnvironment2( + environment: cxapi.Environment, + sdkProvider: SdkProvider, + options: BootstrapEnvironmentOptions): Promise { + + const params = options.parameters ?? {}; + + if (params.trustedAccounts?.length && !params.cloudFormationExecutionPolicies?.length) { + throw new Error('--cloudformation-execution-policies are required if --trust has been passed!'); + } + + const bootstrapTemplatePath = path.join(__dirname, 'bootstrap-template.yaml'); + const bootstrapTemplate = await loadStructuredFile(bootstrapTemplatePath); + + return deployBootstrapStack( + bootstrapTemplate, + { + FileAssetsBucketName: params.bucketName, + FileAssetsBucketKmsKeyId: params.kmsKeyId, + TrustedAccounts: params.trustedAccounts?.join(','), + CloudFormationExecutionPolicies: params.cloudFormationExecutionPolicies?.join(','), + }, + environment, + sdkProvider, + options); +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment2.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment2.ts deleted file mode 100644 index ec77521b223c6..0000000000000 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment2.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import * as cxapi from '@aws-cdk/cx-api'; -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import { BootstrapEnvironmentProps, deployStack, DeployStackResult } from '..'; -import { loadStructuredFile } from '../../serialize'; -import { Mode, SdkProvider } from '../aws-auth'; - -export async function bootstrapEnvironment2( - environment: cxapi.Environment, sdkProvider: SdkProvider, - toolkitStackName: string, roleArn: string | undefined, - props: BootstrapEnvironmentProps = {}): Promise { - if (props.trustedAccounts?.length && !props.cloudFormationExecutionPolicies?.length) { - throw new Error('--cloudformation-execution-policies are required if --trust has been passed!'); - } - - const outdir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-bootstrap-new')); - const builder = new cxapi.CloudAssemblyBuilder(outdir); - - // convert from YAML to JSON (which the Cloud Assembly uses) - const templateFile = `${toolkitStackName}.template.json`; - const bootstrapTemplatePath = path.join(__dirname, 'bootstrap-template.yaml'); - const bootstrapTemplateObject = await loadStructuredFile(bootstrapTemplatePath); - await fs.writeJson( - path.join(builder.outdir, templateFile), - bootstrapTemplateObject); - - builder.addArtifact(toolkitStackName, { - type: cxschema.ArtifactType.AWS_CLOUDFORMATION_STACK, - environment: cxapi.EnvironmentUtils.format(environment.account, environment.region), - properties: { - templateFile, - }, - }); - - const resolvedEnvironment = await sdkProvider.resolveEnvironment(environment.account, environment.region); - - const assembly = builder.buildAssembly(); - return await deployStack({ - stack: assembly.getStackByName(toolkitStackName), - resolvedEnvironment, - sdk: await sdkProvider.forEnvironment(environment.account, environment.region, Mode.ForWriting), - sdkProvider, - roleArn, - tags: props.tags, - execute: props.execute, - parameters: { - FileAssetsBucketName: props.bucketName, - FileAssetsBucketKmsKeyId: props.kmsKeyId, - TrustedAccounts: props.trustedAccounts?.join(','), - CloudFormationExecutionPolicies: props.cloudFormationExecutionPolicies?.join(','), - }, - }); -} diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts new file mode 100644 index 0000000000000..2859daba5d308 --- /dev/null +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts @@ -0,0 +1,66 @@ +import { Tag } from '../../cdk-toolkit'; + +/** @experimental */ +export const BUCKET_NAME_OUTPUT = 'BucketName'; +/** @experimental */ +export const REPOSITORY_NAME_OUTPUT = 'RepositoryName'; +/** @experimental */ +export const BUCKET_DOMAIN_NAME_OUTPUT = 'BucketDomainName'; +/** @experimental */ +export const BOOTSTRAP_VERSION_OUTPUT = 'BootstrapVersion'; + +/** + * Options for the bootstrapEnvironment operation(s) + */ +export interface BootstrapEnvironmentOptions { + readonly toolkitStackName?: string; + readonly roleArn?: string; + readonly parameters?: BootstrappingParameters; + readonly force?: boolean; +} + +/** + * Parameters for the bootstrapping template + */ +export interface BootstrappingParameters { + /** + * The name to be given to the CDK Bootstrap bucket. + * + * @default - a name is generated by CloudFormation. + */ + readonly bucketName?: string; + + /** + * The ID of an existing KMS key to be used for encrypting items in the bucket. + * + * @default - the default KMS key for S3 will be used. + */ + readonly kmsKeyId?: string; + /** + * Tags for cdktoolkit stack. + * + * @default - None. + */ + readonly tags?: Tag[]; + /** + * Whether to execute the changeset or only create it and leave it in review. + * @default true + */ + readonly execute?: boolean; + + /** + * The list of AWS account IDs that are trusted to deploy into the environment being bootstrapped. + * + * @default - only the bootstrapped account can deploy into this environment + */ + readonly trustedAccounts?: string[]; + + /** + * The ARNs of the IAM managed policies that should be attached to the role performing CloudFormation deployments. + * In most cases, this will be the AdministratorAccess policy. + * At least one policy is required if {@link trustedAccounts} were passed. + * + * @default - the role will have no policies attached + */ + readonly cloudFormationExecutionPolicies?: string[]; +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts new file mode 100644 index 0000000000000..8f54b1b665a68 --- /dev/null +++ b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts @@ -0,0 +1,61 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { Mode, SdkProvider } from '../aws-auth'; +import { deployStack, DeployStackResult } from '../deploy-stack'; +import { DEFAULT_TOOLKIT_STACK_NAME, ToolkitInfo } from '../toolkit-info'; +import { BOOTSTRAP_VERSION_OUTPUT, BootstrapEnvironmentOptions } from './bootstrap-props'; + +/** + * Perform the actual deployment of a bootstrap stack, given a template and some parameters + */ +export async function deployBootstrapStack( + template: any, + parameters: Record, + environment: cxapi.Environment, + sdkProvider: SdkProvider, + options: BootstrapEnvironmentOptions): Promise { + + const toolkitStackName = options.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME; + + const resolvedEnvironment = await sdkProvider.resolveEnvironment(environment); + const sdk = await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForWriting); + + const newVersion = bootstrapVersionFromTemplate(template); + const currentBootstrapStack = await ToolkitInfo.lookup(resolvedEnvironment, sdk, toolkitStackName); + if (currentBootstrapStack && newVersion < currentBootstrapStack.version && !options.force) { + throw new Error(`Not downgrading existing bootstrap stack from version '${currentBootstrapStack.version}' to version '${newVersion}'. Use --force to force.`); + } + + const outdir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-bootstrap')); + const builder = new cxapi.CloudAssemblyBuilder(outdir); + const templateFile = `${toolkitStackName}.template.json`; + await fs.writeJson(path.join(builder.outdir, templateFile), template, { spaces: 2 }); + + builder.addArtifact(toolkitStackName, { + type: cxschema.ArtifactType.AWS_CLOUDFORMATION_STACK, + environment: cxapi.EnvironmentUtils.format(environment.account, environment.region), + properties: { + templateFile, + }, + }); + + const assembly = builder.buildAssembly(); + + return await deployStack({ + stack: assembly.getStackByName(toolkitStackName), + resolvedEnvironment, + sdk: await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForWriting), + sdkProvider, + roleArn: options.roleArn, + tags: options.parameters?.tags, + execute: options?.parameters?.execute, + parameters, + }); +} + +function bootstrapVersionFromTemplate(template: any): number { + return parseInt(template.Outputs?.[BOOTSTRAP_VERSION_OUTPUT]?.Value ?? '0', 10); +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/bootstrap/index.ts b/packages/aws-cdk/lib/api/bootstrap/index.ts new file mode 100644 index 0000000000000..8ece35e085f2c --- /dev/null +++ b/packages/aws-cdk/lib/api/bootstrap/index.ts @@ -0,0 +1,2 @@ +export * from './bootstrap-environment'; +export * from './bootstrap-props'; \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/bootstrap/legacy-template.ts b/packages/aws-cdk/lib/api/bootstrap/legacy-template.ts new file mode 100644 index 0000000000000..9bd99a8792836 --- /dev/null +++ b/packages/aws-cdk/lib/api/bootstrap/legacy-template.ts @@ -0,0 +1,65 @@ +import { BootstrappingParameters, BUCKET_DOMAIN_NAME_OUTPUT, BUCKET_NAME_OUTPUT } from './bootstrap-props'; + +export function legacyBootstrapTemplate(params: BootstrappingParameters): any { + return { + Description: 'The CDK Toolkit Stack. It was created by `cdk bootstrap` and manages resources necessary for managing your Cloud Applications with AWS CDK.', + Resources: { + StagingBucket: { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: params.bucketName, + AccessControl: 'Private', + BucketEncryption: { + ServerSideEncryptionConfiguration: [{ + ServerSideEncryptionByDefault: { + SSEAlgorithm: 'aws:kms', + KMSMasterKeyID: params.kmsKeyId, + }, + }], + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + }, + }, + StagingBucketPolicy: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { Ref: 'StagingBucket' }, + PolicyDocument: { + Id: 'AccessControl', + Version: '2012-10-17', + Statement: [ + { + Sid: 'AllowSSLRequestsOnly', + Action: 's3:*', + Effect: 'Deny', + Resource: [ + { 'Fn::Sub': '${StagingBucket.Arn}' }, + { 'Fn::Sub': '${StagingBucket.Arn}/*' }, + ], + Condition: { + Bool: { 'aws:SecureTransport': 'false' }, + }, + Principal: '*', + }, + ], + }, + }, + }, + }, + Outputs: { + [BUCKET_NAME_OUTPUT]: { + Description: 'The name of the S3 bucket owned by the CDK toolkit stack', + Value: { Ref: 'StagingBucket' }, + }, + [BUCKET_DOMAIN_NAME_OUTPUT]: { + Description: 'The domain name of the S3 bucket owned by the CDK toolkit stack', + Value: { 'Fn::GetAtt': ['StagingBucket', 'RegionalDomainName'] }, + }, + }, + }; +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index dbacbecff42c1..65dc93616b4d6 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -172,9 +172,9 @@ export class CloudFormationDeployments { if (!stack.environment) { throw new Error(`The stack ${stack.displayName} does not have an environment`); } - const resolvedEnvironment = await this.sdkProvider.resolveEnvironment(stack.environment.account, stack.environment.region); + const resolvedEnvironment = await this.sdkProvider.resolveEnvironment(stack.environment); - const stackSdk = await this.sdkProvider.forEnvironment(stack.environment.account, stack.environment.region, mode); + const stackSdk = await this.sdkProvider.forEnvironment(stack.environment, mode); return { stackSdk, diff --git a/packages/aws-cdk/lib/api/cxapp/environments.ts b/packages/aws-cdk/lib/api/cxapp/environments.ts index 0dc3ad65e611f..d95d44b3ea38e 100644 --- a/packages/aws-cdk/lib/api/cxapp/environments.ts +++ b/packages/aws-cdk/lib/api/cxapp/environments.ts @@ -3,15 +3,17 @@ import * as minimatch from 'minimatch'; import { SdkProvider } from '../aws-auth'; import { StackCollection } from './cloud-assembly'; +export function looksLikeGlob(environment: string) { + return environment.indexOf('*') > -1; +} + // tslint:disable-next-line:max-line-length export async function globEnvironmentsFromStacks(stacks: StackCollection, environmentGlobs: string[], sdk: SdkProvider): Promise { - if (environmentGlobs.length === 0) { - environmentGlobs = [ '**' ]; // default to ALL - } + if (environmentGlobs.length === 0) { return []; } const availableEnvironments = new Array(); for (const stack of stacks.stackArtifacts) { - const actual = await sdk.resolveEnvironment(stack.environment.account, stack.environment.region); + const actual = await sdk.resolveEnvironment(stack.environment); availableEnvironments.push(actual); } @@ -29,10 +31,6 @@ export async function globEnvironmentsFromStacks(stacks: StackCollection, enviro * Given a set of "/" strings, construct environments for them */ export function environmentsFromDescriptors(envSpecs: string[]): cxapi.Environment[] { - if (envSpecs.length === 0) { - throw new Error('Either specify an app with \'--app\', or specify an environment name like \'aws://123456789012/us-east-1\''); - } - const ret = new Array(); for (const spec of envSpecs) { diff --git a/packages/aws-cdk/lib/api/index.ts b/packages/aws-cdk/lib/api/index.ts index 702bfc5a1a721..5747b4c8d570f 100644 --- a/packages/aws-cdk/lib/api/index.ts +++ b/packages/aws-cdk/lib/api/index.ts @@ -1,7 +1,7 @@ import 'source-map-support/register'; export * from './aws-auth/credentials'; -export * from './bootstrap-environment'; +export * from './bootstrap'; export * from './deploy-stack'; export * from './toolkit-info'; export * from './aws-auth'; diff --git a/packages/aws-cdk/lib/api/toolkit-info.ts b/packages/aws-cdk/lib/api/toolkit-info.ts index e0276465d6214..d695547a571e0 100644 --- a/packages/aws-cdk/lib/api/toolkit-info.ts +++ b/packages/aws-cdk/lib/api/toolkit-info.ts @@ -2,7 +2,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as colors from 'colors/safe'; import { debug } from '../logging'; import { ISDK } from './aws-auth'; -import { BUCKET_DOMAIN_NAME_OUTPUT, BUCKET_NAME_OUTPUT } from './bootstrap-environment'; +import { BOOTSTRAP_VERSION_OUTPUT, BUCKET_DOMAIN_NAME_OUTPUT, BUCKET_NAME_OUTPUT } from './bootstrap'; import { waitForStack } from './util/cloudformation'; export const DEFAULT_TOOLKIT_STACK_NAME = 'CDKToolkit'; @@ -29,6 +29,7 @@ export class ToolkitInfo { sdk, environment, bucketName: requireOutput(BUCKET_NAME_OUTPUT), bucketEndpoint: requireOutput(BUCKET_DOMAIN_NAME_OUTPUT), + version: parseInt(outputs[BOOTSTRAP_VERSION_OUTPUT] ?? '0', 10), }); function requireOutput(output: string): string { @@ -45,7 +46,8 @@ export class ToolkitInfo { readonly sdk: ISDK, bucketName: string, bucketEndpoint: string, - environment: cxapi.Environment + environment: cxapi.Environment, + version: number, }) { this.sdk = props.sdk; } @@ -58,6 +60,10 @@ export class ToolkitInfo { return this.props.bucketName; } + public get version() { + return this.props.version; + } + /** * Prepare an ECR repository for uploading to using Docker * diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 2ef3591a089d3..6028c2b6fea28 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -5,10 +5,10 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import * as promptly from 'promptly'; import { format } from 'util'; -import { bootstrapEnvironment, BootstrapEnvironmentProps } from '../lib'; -import { environmentsFromDescriptors, globEnvironmentsFromStacks } from '../lib/api/cxapp/environments'; +import { environmentsFromDescriptors, globEnvironmentsFromStacks, looksLikeGlob } from '../lib/api/cxapp/environments'; +import { bootstrapEnvironment } from './api'; import { SdkProvider } from './api/aws-auth'; -import { bootstrapEnvironment2 } from './api/bootstrap/bootstrap-environment2'; +import { bootstrapEnvironment2, BootstrappingParameters } from './api/bootstrap'; import { CloudFormationDeployments } from './api/cloudformation-deployments'; import { CloudAssembly, DefaultSelection, ExtendedStackSelection, StackCollection } from './api/cxapp/cloud-assembly'; import { CloudExecutable } from './api/cxapp/cloud-executable'; @@ -16,6 +16,7 @@ import { printSecurityDiff, printStackDiff, RequireApproval } from './diff'; import { data, error, highlight, print, success, warning } from './logging'; import { deserializeStructure } from './serialize'; import { Configuration } from './settings'; +import { partition } from './util'; export interface CdkToolkitProps { @@ -322,32 +323,40 @@ export class CdkToolkit { /** * Bootstrap the CDK Toolkit stack in the accounts used by the specified stack(s). * - * @param environmentGlobs environment names that need to have toolkit support + * @param environmentSpecs environment names that need to have toolkit support * provisioned, as a glob filter. If none is provided, * all stacks are implicitly selected. * @param toolkitStackName the name to be used for the CDK Toolkit stack. */ public async bootstrap( - environmentGlobs: string[], toolkitStackName: string, roleArn: string | undefined, - useNewBootstrapping: boolean, props: BootstrapEnvironmentProps): Promise { - // Two modes of operation. - // - // If there is an '--app' argument, we select the environments from the app. Otherwise we just take the user - // at their word that they know the name of the environment. - let environments: cxapi.Environment[]; - if (this.props.cloudExecutable.hasApp) { - const stacks = await this.selectStacksForList([]); - environments = await globEnvironmentsFromStacks(stacks, environmentGlobs, this.props.sdkProvider); - } else { - environments = environmentsFromDescriptors(environmentGlobs); + environmentSpecs: string[], toolkitStackName: string | undefined, roleArn: string | undefined, + useNewBootstrapping: boolean, force: boolean | undefined, props: BootstrappingParameters): Promise { + // If there is an '--app' argument and an environment looks like a glob, we + // select the environments from the app. Otherwise use what the user said. + + // By default glob for everything + environmentSpecs = environmentSpecs.length > 0 ? environmentSpecs : ['**']; + + // Partition into globs and non-globs (this will mutate environmentSpecs). + const globSpecs = partition(environmentSpecs, looksLikeGlob); + if (globSpecs.length > 0 && !this.props.cloudExecutable.hasApp) { + throw new Error(`'${globSpecs}' is not an environment name. Run in app directory to glob or specify an environment name like \'aws://123456789012/us-east-1\'.`); } + const environments: cxapi.Environment[] = [ + ...environmentsFromDescriptors(environmentSpecs), + ...await globEnvironmentsFromStacks(await this.selectStacksForList([]), globSpecs, this.props.sdkProvider), + ]; + await Promise.all(environments.map(async (environment) => { success(' ⏳ Bootstrapping environment %s...', colors.blue(environment.name)); try { - const result = useNewBootstrapping - ? await bootstrapEnvironment2(environment, this.props.sdkProvider, toolkitStackName, roleArn, props) - : await bootstrapEnvironment(environment, this.props.sdkProvider, toolkitStackName, roleArn, props); + const result = await (useNewBootstrapping ? bootstrapEnvironment2 : bootstrapEnvironment)(environment, this.props.sdkProvider, { + toolkitStackName, + roleArn, + force, + parameters: props, + }); const message = result.noOp ? ' ✅ Environment %s bootstrapped (no changes).' : ' ✅ Environment %s bootstrapped.'; diff --git a/packages/aws-cdk/lib/context-providers/ami.ts b/packages/aws-cdk/lib/context-providers/ami.ts index 5b930b8985553..0cd461df4df43 100644 --- a/packages/aws-cdk/lib/context-providers/ami.ts +++ b/packages/aws-cdk/lib/context-providers/ami.ts @@ -19,7 +19,7 @@ export class AmiContextProviderPlugin implements ContextProviderPlugin { print(`Searching for AMI in ${account}:${region}`); debug(`AMI search parameters: ${JSON.stringify(args)}`); - const ec2 = (await this.aws.forEnvironment(account, region, Mode.ForReading)).ec2(); + const ec2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).ec2(); const response = await ec2.describeImages({ Owners: args.owners, Filters: Object.entries(args.filters).map(([key, values]) => ({ diff --git a/packages/aws-cdk/lib/context-providers/availability-zones.ts b/packages/aws-cdk/lib/context-providers/availability-zones.ts index c7482c6977ffb..b5b4cd6296fb4 100644 --- a/packages/aws-cdk/lib/context-providers/availability-zones.ts +++ b/packages/aws-cdk/lib/context-providers/availability-zones.ts @@ -1,3 +1,4 @@ +import * as cxapi from '@aws-cdk/cx-api'; import { Mode, SdkProvider } from '../api'; import { debug } from '../logging'; import { ContextProviderPlugin } from './provider'; @@ -13,7 +14,7 @@ export class AZContextProviderPlugin implements ContextProviderPlugin { const region = args.region; const account = args.account; debug(`Reading AZs for ${account}:${region}`); - const ec2 = (await this.aws.forEnvironment(account, region, Mode.ForReading)).ec2(); + const ec2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).ec2(); const response = await ec2.describeAvailabilityZones().promise(); if (!response.AvailabilityZones) { return []; } const azs = response.AvailabilityZones.filter(zone => zone.State === 'available').map(zone => zone.ZoneName); diff --git a/packages/aws-cdk/lib/context-providers/hosted-zones.ts b/packages/aws-cdk/lib/context-providers/hosted-zones.ts index 3d77e56a4de88..bb61c181c2263 100644 --- a/packages/aws-cdk/lib/context-providers/hosted-zones.ts +++ b/packages/aws-cdk/lib/context-providers/hosted-zones.ts @@ -16,7 +16,7 @@ export class HostedZoneContextProviderPlugin implements ContextProviderPlugin { } const domainName = args.domainName; debug(`Reading hosted zone ${account}:${region}:${domainName}`); - const r53 = (await this.aws.forEnvironment(account, region, Mode.ForReading)).route53(); + const r53 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).route53(); const response = await r53.listHostedZonesByName({ DNSName: domainName }).promise(); if (!response.HostedZones) { throw new Error(`Hosted Zone not found in account ${account}, region ${region}: ${domainName}`); diff --git a/packages/aws-cdk/lib/context-providers/ssm-parameters.ts b/packages/aws-cdk/lib/context-providers/ssm-parameters.ts index d92be3101b9c2..25fb21215e1fb 100644 --- a/packages/aws-cdk/lib/context-providers/ssm-parameters.ts +++ b/packages/aws-cdk/lib/context-providers/ssm-parameters.ts @@ -1,3 +1,4 @@ +import * as cxapi from '@aws-cdk/cx-api'; import * as AWS from 'aws-sdk'; import { Mode, SdkProvider } from '../api'; import { debug } from '../logging'; @@ -37,7 +38,7 @@ export class SSMContextProviderPlugin implements ContextProviderPlugin { * @throws Error if a service error (other than ``ParameterNotFound``) occurs. */ private async getSsmParameterValue(account: string, region: string, parameterName: string): Promise { - const ssm = (await this.aws.forEnvironment(account, region, Mode.ForReading)).ssm(); + const ssm = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).ssm(); try { return await ssm.getParameter({ Name: parameterName }).promise(); } catch (e) { diff --git a/packages/aws-cdk/lib/context-providers/vpcs.ts b/packages/aws-cdk/lib/context-providers/vpcs.ts index aca46c668278f..058e81b6c8b18 100644 --- a/packages/aws-cdk/lib/context-providers/vpcs.ts +++ b/packages/aws-cdk/lib/context-providers/vpcs.ts @@ -13,7 +13,7 @@ export class VpcNetworkContextProviderPlugin implements ContextProviderPlugin { const account: string = args.account!; const region: string = args.region!; - const ec2 = (await this.aws.forEnvironment(account, region, Mode.ForReading)).ec2(); + const ec2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).ec2(); const vpcId = await this.findVpc(ec2, args); diff --git a/packages/aws-cdk/lib/util/asset-publishing.ts b/packages/aws-cdk/lib/util/asset-publishing.ts index 41a0026aa0cf5..9bfa8dea5b694 100644 --- a/packages/aws-cdk/lib/util/asset-publishing.ts +++ b/packages/aws-cdk/lib/util/asset-publishing.ts @@ -59,11 +59,14 @@ class PublishingAws implements cdk_assets.IAws { * Get an SDK appropriate for the given client options */ private sdk(options: cdk_assets.ClientOptions): Promise { - const region = options.region ?? this.targetEnv.region; // Default: same region as the stack + const env = { + ...this.targetEnv, + region: options.region ?? this.targetEnv.region, // Default: same region as the stack + }; return options.assumeRoleArn - ? this.aws.withAssumedRole(options.assumeRoleArn, options.assumeRoleExternalId, region) - : this.aws.forEnvironment(this.targetEnv.account, region, Mode.ForWriting); + ? this.aws.withAssumedRole(options.assumeRoleArn, options.assumeRoleExternalId, env.region) + : this.aws.forEnvironment(env, Mode.ForWriting); } } diff --git a/packages/aws-cdk/test/api/bootstrap.test.ts b/packages/aws-cdk/test/api/bootstrap.test.ts index bc4ef75f49f99..560552236ec71 100644 --- a/packages/aws-cdk/test/api/bootstrap.test.ts +++ b/packages/aws-cdk/test/api/bootstrap.test.ts @@ -19,7 +19,8 @@ beforeEach(() => { cfnMocks = { describeStacks: jest.fn() - // First call, no stacks exist + // First two calls, no stacks exist (first is for version checking, second is in deploy-stack.ts) + .mockImplementationOnce(() => ({ Stacks: [] })) .mockImplementationOnce(() => ({ Stacks: [] })) // Second call, stack has been created .mockImplementationOnce(() => ({ Stacks: [ @@ -46,7 +47,7 @@ beforeEach(() => { test('do bootstrap', async () => { // WHEN - const ret = await bootstrapEnvironment(env, sdk, 'mockStack', undefined); + const ret = await bootstrapEnvironment(env, sdk, { toolkitStackName: 'mockStack' }); // THEN const bucketProperties = changeSetTemplate.Resources.StagingBucket.Properties; @@ -59,8 +60,11 @@ test('do bootstrap', async () => { test('do bootstrap using custom bucket name', async () => { // WHEN - const ret = await bootstrapEnvironment(env, sdk, 'mockStack', undefined, { - bucketName: 'foobar', + const ret = await bootstrapEnvironment(env, sdk, { + toolkitStackName: 'mockStack', + parameters: { + bucketName: 'foobar', + }, }); // THEN @@ -74,8 +78,11 @@ test('do bootstrap using custom bucket name', async () => { test('do bootstrap using KMS CMK', async () => { // WHEN - const ret = await bootstrapEnvironment(env, sdk, 'mockStack', undefined, { - kmsKeyId: 'myKmsKey', + const ret = await bootstrapEnvironment(env, sdk, { + toolkitStackName: 'mockStack', + parameters: { + kmsKeyId: 'myKmsKey', + }, }); // THEN @@ -89,8 +96,11 @@ test('do bootstrap using KMS CMK', async () => { test('do bootstrap with custom tags for toolkit stack', async () => { // WHEN - const ret = await bootstrapEnvironment(env, sdk, 'mockStack', undefined, { - tags: [{ Key: 'Foo', Value: 'Bar' }], + const ret = await bootstrapEnvironment(env, sdk, { + toolkitStackName: 'mockStack', + parameters: { + tags: [{ Key: 'Foo', Value: 'Bar' }], + }, }); // THEN @@ -103,17 +113,23 @@ test('do bootstrap with custom tags for toolkit stack', async () => { }); test('passing trusted accounts to the old bootstrapping results in an error', async () => { - await expect(bootstrapEnvironment(env, sdk, 'mockStack', undefined, { - trustedAccounts: ['0123456789012'], + await expect(bootstrapEnvironment(env, sdk, { + toolkitStackName: 'mockStack', + parameters: { + trustedAccounts: ['0123456789012'], + }, })) .rejects - .toThrow('--trust can only be passed for the new bootstrap experience!'); + .toThrow('--trust can only be passed for the new bootstrap experience.'); }); test('passing CFN execution policies to the old bootstrapping results in an error', async () => { - await expect(bootstrapEnvironment(env, sdk, 'mockStack', undefined, { - cloudFormationExecutionPolicies: ['arn:aws:iam::aws:policy/AdministratorAccess'], + await expect(bootstrapEnvironment(env, sdk, { + toolkitStackName: 'mockStack', + parameters: { + cloudFormationExecutionPolicies: ['arn:aws:iam::aws:policy/AdministratorAccess'], + }, })) .rejects - .toThrow('--cloudformation-execution-policies can only be passed for the new bootstrap experience!'); + .toThrow('--cloudformation-execution-policies can only be passed for the new bootstrap experience.'); }); diff --git a/packages/aws-cdk/test/api/bootstrap2.test.ts b/packages/aws-cdk/test/api/bootstrap2.test.ts index 84d068a23f413..f90db81806d24 100644 --- a/packages/aws-cdk/test/api/bootstrap2.test.ts +++ b/packages/aws-cdk/test/api/bootstrap2.test.ts @@ -4,7 +4,17 @@ jest.mock('../../lib/api/deploy-stack', () => ({ deployStack: mockDeployStack, })); -import { bootstrapEnvironment2 } from '../../lib/api/bootstrap/bootstrap-environment2'; +let mockToolkitInfo: any; + +jest.mock('../../lib/api/toolkit-info', () => ({ + // Pretend there's no toolkit deployed yet + DEFAULT_TOOLKIT_STACK_NAME: 'CDKToolkit', + ToolkitInfo: { + lookup: () => mockToolkitInfo, + }, +})); + +import { bootstrapEnvironment2 } from '../../lib/api/bootstrap'; import { MockSdkProvider } from '../util/mock-sdk'; describe('Bootstrapping v2', () => { @@ -14,10 +24,13 @@ describe('Bootstrapping v2', () => { name: 'mock', }; const sdk = new MockSdkProvider(); + mockToolkitInfo = undefined; test('passes the bucket name as a CFN parameter', async () => { - await bootstrapEnvironment2(env, sdk, 'mockStack', undefined, { - bucketName: 'my-bucket-name', + await bootstrapEnvironment2(env, sdk, { + parameters: { + bucketName: 'my-bucket-name', + }, }); expect(mockDeployStack).toHaveBeenCalledWith(expect.objectContaining({ @@ -28,8 +41,10 @@ describe('Bootstrapping v2', () => { }); test('passes the KMS key ID as a CFN parameter', async () => { - await bootstrapEnvironment2(env, sdk, 'mockStack', undefined, { - kmsKeyId: 'my-kms-key-id', + await bootstrapEnvironment2(env, sdk, { + parameters: { + kmsKeyId: 'my-kms-key-id', + }, }); expect(mockDeployStack).toHaveBeenCalledWith(expect.objectContaining({ @@ -40,14 +55,26 @@ describe('Bootstrapping v2', () => { }); test('passing trusted accounts without CFN managed policies results in an error', async () => { - await expect(bootstrapEnvironment2(env, sdk, 'mockStack', undefined, { - trustedAccounts: ['123456789012'], + await expect(bootstrapEnvironment2(env, sdk, { + parameters: { + trustedAccounts: ['123456789012'], + }, })) .rejects .toThrow('--cloudformation-execution-policies are required if --trust has been passed!'); }); + test('Do not allow downgrading bootstrap stack version', async () => { + // GIVEN + mockToolkitInfo = { + version: 999, + }; + + await expect(bootstrapEnvironment2(env, sdk, {})) + .rejects.toThrow('Not downgrading existing bootstrap stack'); + }); + afterEach(() => { mockDeployStack.mockClear(); }); -}); +}); \ No newline at end of file diff --git a/packages/aws-cdk/test/api/sdk-provider.test.ts b/packages/aws-cdk/test/api/sdk-provider.test.ts index c1daa73312aae..21bb61dfbb9ed 100644 --- a/packages/aws-cdk/test/api/sdk-provider.test.ts +++ b/packages/aws-cdk/test/api/sdk-provider.test.ts @@ -21,6 +21,7 @@ const defaultCredOptions = { // Account cache buster let uid: string; let pluginQueried = false; +let defaultEnv: cxapi.Environment; beforeEach(() => { uid = `(${uuid.v4()})`; @@ -83,6 +84,8 @@ beforeEach(() => { name: 'test plugin', }); + defaultEnv = cxapi.EnvironmentUtils.make(`${uid}the_account_#`, 'def'); + // Set environment variables that we want process.env.AWS_CONFIG_FILE = bockfs.path('/home/me/.bxt/config'); process.env.AWS_SHARED_CREDENTIALS_FILE = bockfs.path('/home/me/.bxt/credentials'); @@ -107,7 +110,7 @@ describe('CLI compatible credentials loading', () => { // THEN expect(provider.defaultRegion).toEqual('eu-bla-5'); await expect(provider.defaultAccount()).resolves.toEqual({ accountId: `${uid}the_account_#`, partition: 'aws-here' }); - const sdk = await provider.forEnvironment(`${uid}the_account_#`, 'rgn', Mode.ForReading); + const sdk = await provider.forEnvironment({ ...defaultEnv, region: 'rgn' }, Mode.ForReading); expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}access`); expect(sdkConfig(sdk).region).toEqual('rgn'); }); @@ -117,7 +120,7 @@ describe('CLI compatible credentials loading', () => { const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions }); // THEN - const sdk = await provider.forEnvironment(cxapi.UNKNOWN_ACCOUNT, cxapi.UNKNOWN_REGION, Mode.ForReading); + const sdk = await provider.forEnvironment(cxapi.EnvironmentUtils.make(cxapi.UNKNOWN_ACCOUNT, cxapi.UNKNOWN_REGION), Mode.ForReading); expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}access`); expect(sdkConfig(sdk).region).toEqual('eu-bla-5'); }); @@ -129,7 +132,7 @@ describe('CLI compatible credentials loading', () => { // THEN expect(provider.defaultRegion).toEqual('eu-west-1'); await expect(provider.defaultAccount()).resolves.toEqual({ accountId: `${uid}the_account_#`, partition: 'aws-here' }); - const sdk = await provider.forEnvironment(`${uid}the_account_#`, 'def', Mode.ForReading); + const sdk = await provider.forEnvironment(defaultEnv, Mode.ForReading); expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}fooccess`); }); @@ -140,14 +143,14 @@ describe('CLI compatible credentials loading', () => { // THEN expect(provider.defaultRegion).toEqual('eu-bla-5'); // Fall back to default config await expect(provider.defaultAccount()).resolves.toEqual({ accountId: `${uid}the_account_#`, partition: 'aws-here' }); - const sdk = await provider.forEnvironment(`${uid}the_account_#`, 'def', Mode.ForReading); + const sdk = await provider.forEnvironment(defaultEnv, Mode.ForReading); expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}booccess`); }); test('different account throws', async () => { const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'boo' }); - await expect(provider.forEnvironment(`${uid}some_account_#`, 'def', Mode.ForReading)).rejects.toThrow('Need to perform AWS calls'); + await expect(provider.forEnvironment({...defaultEnv, account: `${uid}some_account_#` }, Mode.ForReading)).rejects.toThrow('Need to perform AWS calls'); }); test('even when using a profile to assume another profile, STS calls goes through the proxy', async () => { @@ -191,13 +194,13 @@ describe('CLI compatible credentials loading', () => { describe('Plugins', () => { test('does not use plugins if current credentials are for expected account', async () => { const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions }); - await provider.forEnvironment(`${uid}the_account_#`, 'def', Mode.ForReading); + await provider.forEnvironment(defaultEnv, Mode.ForReading); expect(pluginQueried).toEqual(false); }); test('uses plugin for other account', async () => { const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions }); - await provider.forEnvironment(`${uid}plugin_account_#`, 'def', Mode.ForReading); + await provider.forEnvironment({...defaultEnv, account: `${uid}plugin_account_#`}, Mode.ForReading); expect(pluginQueried).toEqual(true); }); }); diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index c97a24ac7ea1c..239745c7483a1 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -1,3 +1,10 @@ +const mockBootstrapEnvironment = jest.fn(); +jest.mock('../lib/api/bootstrap', () => { + return { + bootstrapEnvironment: mockBootstrapEnvironment, + }; +}); + import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import { CloudFormationDeployments, DeployStackOptions } from '../lib/api/cloudformation-deployments'; @@ -14,21 +21,27 @@ beforeEach(() => { MockStack.MOCK_STACK_B, ], }); + + mockBootstrapEnvironment.mockReset().mockResolvedValue({ noOp: false, outputs: {} }); }); +function defaultToolkitSetup() { + return new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + cloudFormation: new FakeCloudFormation({ + 'Test-Stack-A': { Foo: 'Bar' }, + 'Test-Stack-B': { Baz: 'Zinga!' }, + }), + }); +} + describe('deploy', () => { describe('makes correct CloudFormation calls', () => { test('without options', async () => { // GIVEN - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - cloudFormation: new FakeCloudFormation({ - 'Test-Stack-A': { Foo: 'Bar' }, - 'Test-Stack-B': { Baz: 'Zinga!' }, - }), - }); + const toolkit = defaultToolkitSetup(); // WHEN await toolkit.deploy({ stackNames: ['Test-Stack-A', 'Test-Stack-B'] }); @@ -53,6 +66,39 @@ describe('deploy', () => { notificationArns, }); }); + + test('globless bootstrap uses environment without question', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); + + // WHEN + await toolkit.bootstrap(['aws://56789/south-pole'], undefined, undefined, false, false, {}); + + // THEN + expect(mockBootstrapEnvironment).toHaveBeenCalledWith({ + account: '56789', + region: 'south-pole', + name: 'aws://56789/south-pole', + }, expect.anything(), expect.anything()); + expect(mockBootstrapEnvironment).toHaveBeenCalledTimes(1); + }); + + test('globby bootstrap uses whats in the stacks', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); + cloudExecutable.configuration.settings.set(['app'], 'something'); + + // WHEN + await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], undefined, undefined, false, false, {}); + + // THEN + expect(mockBootstrapEnvironment).toHaveBeenCalledWith({ + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, expect.anything(), expect.anything()); + expect(mockBootstrapEnvironment).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/packages/aws-cdk/test/integ/bootstrap/bootstrap.integ-test.ts b/packages/aws-cdk/test/integ/bootstrap/bootstrap.integ-test.ts index 654ff48eb0f9e..33b74253a9292 100644 --- a/packages/aws-cdk/test/integ/bootstrap/bootstrap.integ-test.ts +++ b/packages/aws-cdk/test/integ/bootstrap/bootstrap.integ-test.ts @@ -4,7 +4,7 @@ import * as AWS from 'aws-sdk'; import * as fs from 'fs-extra'; import * as path from 'path'; import { bootstrapEnvironment, deployStack, destroyStack, ISDK, Mode, SdkProvider, ToolkitInfo } from '../../../lib/api'; -import { bootstrapEnvironment2 } from '../../../lib/api/bootstrap/bootstrap-environment2'; +import { bootstrapEnvironment2 } from '../../../lib/api/bootstrap'; import { ExampleAsset, MyTestCdkStack } from './example-cdk-app/my-test-cdk-stack'; jest.setTimeout(600_000); @@ -30,7 +30,7 @@ describe('Bootstrapping', () => { userAgent: 'aws-cdk-bootstrap-integ-test', }, }); - sdk = await sdkProvider.forEnvironment(env.account, env.region, Mode.ForWriting); + sdk = await sdkProvider.forEnvironment(env, Mode.ForWriting); s3 = sdk.s3(); }); @@ -45,8 +45,11 @@ describe('Bootstrapping', () => { beforeAll(async () => { // bootstrap the "old" way - const bootstrapResults = await bootstrapEnvironment(env, sdkProvider, bootstrapStackName, undefined, { - bucketName: legacyBootstrapBucketName, + const bootstrapResults = await bootstrapEnvironment(env, sdkProvider, { + toolkitStackName: bootstrapStackName, + parameters: { + bucketName: legacyBootstrapBucketName, + }, }); bootstrapStack = bootstrapResults.stackArtifact; }); @@ -63,13 +66,16 @@ describe('Bootstrapping', () => { describe('and then updates the bootstrap stack with the new resources', () => { beforeAll(async () => { // bootstrap the "new" way - const bootstrapResults = await bootstrapEnvironment2(env, sdkProvider, bootstrapStackName, undefined, { - bucketName: newBootstrapBucketName, - trustedAccounts: ['790124522186', '593667001225'], - cloudFormationExecutionPolicies: [ - 'arn:aws:iam::aws:policy/AdministratorAccess', - 'arn:aws:iam::aws:policy/AmazonS3FullAccess', - ], + const bootstrapResults = await bootstrapEnvironment2(env, sdkProvider, { + toolkitStackName: bootstrapStackName, + parameters: { + bucketName: newBootstrapBucketName, + trustedAccounts: ['790124522186', '593667001225'], + cloudFormationExecutionPolicies: [ + 'arn:aws:iam::aws:policy/AdministratorAccess', + 'arn:aws:iam::aws:policy/AmazonS3FullAccess', + ], + }, }); bootstrapStack = bootstrapResults.stackArtifact; }); diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index c773d3de0d5b8..74a7a76786025 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -170,6 +170,7 @@ export function mockToolkitInfo() { bucketName: 'BUCKET_NAME', bucketEndpoint: 'BUCKET_ENDPOINT', environment: { name: 'env', account: '1234', region: 'abc' }, + version: 1, }); } diff --git a/packages/cdk-assets/bin/cdk-assets.ts b/packages/cdk-assets/bin/cdk-assets.ts index ff837fd6dabe5..902c4d65ecc41 100644 --- a/packages/cdk-assets/bin/cdk-assets.ts +++ b/packages/cdk-assets/bin/cdk-assets.ts @@ -64,4 +64,4 @@ main().catch(e => { // tslint:disable-next-line:no-console console.error(e.stack); process.exitCode = 1; -}); \ No newline at end of file +}); diff --git a/scripts/buildup b/scripts/buildup index 3fea629af9976..59aeff93dcf50 100755 --- a/scripts/buildup +++ b/scripts/buildup @@ -19,7 +19,7 @@ else fi fi -${scriptdir}/foreach.sh --up yarn build +${scriptdir}/foreach.sh --up yarn build+test ${scriptdir}/foreach.sh --reset echo "************************************************************"