diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts index 779a056e3e666..6c28af3c019b7 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts @@ -14,7 +14,7 @@ import { Mode } from '../plugin'; export type BootstrapSource = { source: 'legacy' } | { source: 'default' } | { source: 'custom'; templateFile: string }; export class Bootstrapper { - constructor(private readonly source: BootstrapSource) {} + constructor(private readonly source: BootstrapSource = { source: 'default' }) {} public bootstrapEnvironment( environment: cxapi.Environment, diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts index 3f01a6ebbbe42..079a0322b73e5 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts @@ -1,3 +1,4 @@ +import { BootstrapSource } from './bootstrap-environment'; import { Tag } from '../../cdk-toolkit'; import { StringWithoutPlaceholders } from '../util/placeholders'; @@ -22,6 +23,13 @@ export interface BootstrapEnvironmentOptions { readonly parameters?: BootstrappingParameters; readonly force?: boolean; + /** + * The source of the bootstrap stack + * + * @default - modern v2-style bootstrapping + */ + readonly source?: BootstrapSource; + /** * Whether to execute the changeset or only create it and leave it in review. * @default true diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 9eb2b24c473ef..34c766efdd519 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -918,14 +918,13 @@ export class CdkToolkit { * * @param userEnvironmentSpecs environment names that need to have toolkit support * provisioned, as a glob filter. If none is provided, all stacks are implicitly selected. - * @param bootstrapper Legacy or modern. * @param options The name, role ARN, bootstrapping parameters, etc. to be used for the CDK Toolkit stack. */ public async bootstrap( userEnvironmentSpecs: string[], - bootstrapper: Bootstrapper, options: BootstrapEnvironmentOptions, ): Promise { + const bootstrapper = new Bootstrapper(options.source); // 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. diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index 363d0889c4ec2..2af03e05eb728 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -242,15 +242,15 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise { + jest.clearAllMocks(); +}); + +describe('cdk bootstrap', () => { + const bootstrapEnvironmentMock = jest.spyOn(Bootstrapper.prototype, 'bootstrapEnvironment'); + + test('will bootstrap the a provided environment', async () => { + bootstrapEnvironmentMock.mockResolvedValueOnce({ + noOp: false, + outputs: {}, + type: 'did-deploy-stack', + stackArn: 'fake-arn', + }); + + await exec(['bootstrap', 'aws://123456789012/us-east-1']); + expect(bootstrapEnvironmentMock).toHaveBeenCalledTimes(1); + expect(bootstrapEnvironmentMock).toHaveBeenCalledWith({ + name: 'aws://123456789012/us-east-1', + account: '123456789012', + region: 'us-east-1', + }, expect.anything(), expect.anything()); + }); +}); + +describe('cdk bootstrap --show-template', () => { + const stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { return true; }); + + test('prints the default bootstrap template', async () => { + await exec(['bootstrap', '--show-template']); + expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('BootstrapVersion')); + }); +}); diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index d2d58af3b1293..cba685fe15528 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -76,7 +76,7 @@ import { mockSSMClient, restoreSdkMocksToDefault, } from './util/mock-sdk'; -import { Bootstrapper } from '../lib/api/bootstrap'; +import { Bootstrapper, type BootstrapSource } from '../lib/api/bootstrap'; import { DeployStackResult, SuccessfulDeployStackResult } from '../lib/api/deploy-stack'; import { Deployments, @@ -97,8 +97,9 @@ markTesting(); process.env.CXAPI_DISABLE_SELECT_BY_ID = '1'; +const defaultBootstrapSource: BootstrapSource = { source: 'default' }; +const bootstrapEnvironmentMock = jest.spyOn(Bootstrapper.prototype, 'bootstrapEnvironment'); let cloudExecutable: MockCloudExecutable; -let bootstrapper: jest.Mocked; let stderrMock: jest.SpyInstance; beforeEach(() => { jest.resetAllMocks(); @@ -108,11 +109,12 @@ beforeEach(() => { // on() in chokidar's Watcher returns 'this' mockChokidarWatcherOn.mockReturnValue(fakeChokidarWatcher); - bootstrapper = instanceMockFrom(Bootstrapper); - bootstrapper.bootstrapEnvironment.mockResolvedValue({ + bootstrapEnvironmentMock.mockResolvedValue({ noOp: false, outputs: {}, - } as any); + type: 'did-deploy-stack', + stackArn: 'fake-arn', + }); cloudExecutable = new MockCloudExecutable({ stacks: [MockStack.MOCK_STACK_A, MockStack.MOCK_STACK_B], @@ -539,17 +541,19 @@ describe('bootstrap', () => { configuration.context.set('@aws-cdk/core:bootstrapQualifier', 'abcde'); // WHEN - await toolkit.bootstrap(['aws://56789/south-pole'], bootstrapper, { + await toolkit.bootstrap(['aws://56789/south-pole'], { + source: defaultBootstrapSource, parameters: { qualifier: configuration.context.get('@aws-cdk/core:bootstrapQualifier'), }, }); // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith(expect.anything(), expect.anything(), { + expect(bootstrapEnvironmentMock).toHaveBeenCalledWith(expect.anything(), expect.anything(), { parameters: { qualifier: 'abcde', }, + source: defaultBootstrapSource, }); }); }); @@ -868,10 +872,12 @@ describe('deploy', () => { const toolkit = defaultToolkitSetup(); // WHEN - await toolkit.bootstrap(['aws://56789/south-pole'], bootstrapper, {}); + await toolkit.bootstrap(['aws://56789/south-pole'], { + source: defaultBootstrapSource, + }); // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith( + expect(bootstrapEnvironmentMock).toHaveBeenCalledWith( { account: '56789', region: 'south-pole', @@ -880,7 +886,7 @@ describe('deploy', () => { expect.anything(), expect.anything(), ); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + expect(bootstrapEnvironmentMock).toHaveBeenCalledTimes(1); }); test('globby bootstrap uses whats in the stacks', async () => { @@ -889,10 +895,12 @@ describe('deploy', () => { cloudExecutable.configuration.settings.set(['app'], 'something'); // WHEN - await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], bootstrapper, {}); + await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], { + source: defaultBootstrapSource, + }); // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith( + expect(bootstrapEnvironmentMock).toHaveBeenCalledWith( { account: '123456789012', region: 'bermuda-triangle-1', @@ -901,7 +909,7 @@ describe('deploy', () => { expect.anything(), expect.anything(), ); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + expect(bootstrapEnvironmentMock).toHaveBeenCalledTimes(1); }); test('bootstrap can be invoked without the --app argument', async () => { @@ -913,10 +921,12 @@ describe('deploy', () => { const toolkit = defaultToolkitSetup(); // WHEN - await toolkit.bootstrap(['aws://123456789012/west-pole'], bootstrapper, {}); + await toolkit.bootstrap(['aws://123456789012/west-pole'], { + source: defaultBootstrapSource, + }); // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith( + expect(bootstrapEnvironmentMock).toHaveBeenCalledWith( { account: '123456789012', region: 'west-pole', @@ -925,7 +935,7 @@ describe('deploy', () => { expect.anything(), expect.anything(), ); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + expect(bootstrapEnvironmentMock).toHaveBeenCalledTimes(1); expect(cloudExecutable.hasApp).toEqual(false); expect(mockSynthesize).not.toHaveBeenCalled();