From dba2c09f4c7bb9735e250eccd050c2ee3dd1e238 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:56:57 -0500 Subject: [PATCH 01/41] bootstrap action --- .../lib/actions/bootstrap/index.ts | 138 +++++++++++++ .../lib/actions/bootstrap/private/helpers.ts | 13 ++ .../lib/actions/bootstrap/private/index.ts | 1 + .../@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts | 2 + .../toolkit-lib/lib/api/io/private/codes.ts | 2 + .../toolkit-lib/lib/api/io/private/timer.ts | 5 +- .../toolkit-lib/lib/toolkit/toolkit.ts | 49 ++++- .../test/actions/bootstrap.test.ts | 184 ++++++++++++++++++ .../@aws-cdk/toolkit-lib/test/util/aws-cdk.ts | 11 +- packages/aws-cdk/test/util/mock-sdk.ts | 1 - 10 files changed, 401 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts create mode 100644 packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts create mode 100644 packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts create mode 100644 packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts new file mode 100644 index 000000000..8dec90c1b --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts @@ -0,0 +1,138 @@ +import { Tag } from './private'; +import { StringWithoutPlaceholders } from '../../api/aws-cdk'; + +/** + * Options for the bootstrapEnvironment operation(s) + */ +export interface BootstrapEnvironmentOptions { + readonly toolkitStackName?: string; + readonly roleArn?: StringWithoutPlaceholders; + 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 + */ + readonly execute?: boolean; + + /** + * Tags for cdktoolkit stack. + * + * @default - None. + */ + readonly tags?: Tag[]; + + /** + * Whether the stacks created by the bootstrap process should be protected from termination. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-protect-stacks.html + * @default true + */ + readonly terminationProtection?: boolean; + + /** + * Use previous values for unspecified parameters + * + * If not set, all parameters must be specified for every deployment. + * + * @default true + */ + usePreviousParameters?: 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 - use the default KMS key or create a custom one + */ + readonly kmsKeyId?: string; + + /** + * Whether or not to create a new customer master key (CMK) + * + * Only applies to modern bootstrapping. Legacy bootstrapping will never create + * a CMK, only use the default S3 key. + * + * @default false + */ + readonly createCustomerMasterKey?: 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 list of AWS account IDs that are trusted to look up values in the environment being bootstrapped. + * + * @default - only the bootstrapped account can look up values in this environment + */ + readonly trustedAccountsForLookup?: string[]; + + /** + * The list of AWS account IDs that should not be trusted by the bootstrapped environment. + * If these accounts are already trusted, they will be removed on bootstrapping. + * + * @default - no account will be untrusted. + */ + readonly untrustedAccounts?: 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 `trustedAccounts` were passed. + * + * @default - the role will have no policies attached + */ + readonly cloudFormationExecutionPolicies?: string[]; + + /** + * Identifier to distinguish multiple bootstrapped environments + * + * @default - Default qualifier + */ + readonly qualifier?: string; + + /** + * Whether or not to enable S3 Staging Bucket Public Access Block Configuration + * + * @default true + */ + readonly publicAccessBlockConfiguration?: boolean; + + /** + * Flag for using the default permissions boundary for bootstrapping + * + * @default - No value, optional argument + */ + readonly examplePermissionsBoundary?: boolean; + + /** + * Name for the customer's custom permissions boundary for bootstrapping + * + * @default - No value, optional argument + */ + readonly customPermissionsBoundary?: string; +} + +export type BootstrapSource = { source: 'legacy' } | { source: 'default' } | { source: 'custom'; templateFile: string }; \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts new file mode 100644 index 000000000..76d00f045 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts @@ -0,0 +1,13 @@ +import type * as cxapi from '@aws-cdk/cx-api'; + +/** + * @returns an array with the tags available in the stack metadata. + */ +export function tagsForStack(stack: cxapi.CloudFormationStackArtifact): Tag[] { + return Object.entries(stack.tags).map(([Key, Value]) => ({ Key, Value })); +} + +export interface Tag { + readonly Key: string; + readonly Value: string; +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts new file mode 100644 index 000000000..c0410d337 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts @@ -0,0 +1 @@ +export * from './helpers'; \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts index 85c4b2d2d..5d8c9eb72 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts @@ -1,5 +1,7 @@ /* eslint-disable import/no-restricted-paths */ +export { Bootstrapper } from '../../../../aws-cdk/lib/api'; + // APIs export { formatSdkLoggerContent, SdkProvider } from '../../../../aws-cdk/lib/api/aws-auth'; export { Context, PROJECT_CONTEXT } from '../../../../aws-cdk/lib/api/context'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts index df36c116e..106e7f34d 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts @@ -45,6 +45,8 @@ export const CODES = { CDK_TOOLKIT_E7900: 'Stack deletion failed', // 9: Bootstrap + CDK_TOOLKIT_I9000: 'Provides bootstrap times', + CDK_TOOLKIT_E9900: 'Bootstrap failed', // Assembly codes CDK_ASSEMBLY_I0042: 'Writing updated context', diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts index 2bfdafff5..a13905d36 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts @@ -37,7 +37,7 @@ export class Timer { * Ends the current timer as a specified timing and notifies the IoHost. * @returns the elapsed time */ - public async endAs(ioHost: ActionAwareIoHost, type: 'synth' | 'deploy' | 'rollback' | 'destroy') { + public async endAs(ioHost: ActionAwareIoHost, type: 'synth' | 'deploy' | 'rollback' | 'destroy' | 'bootstrap') { const duration = this.end(); const { code, text } = timerMessageProps(type); @@ -49,7 +49,7 @@ export class Timer { } } -function timerMessageProps(type: 'synth' | 'deploy' | 'rollback'| 'destroy'): { +function timerMessageProps(type: 'synth' | 'deploy' | 'rollback'| 'destroy' | 'bootstrap'): { code: VALID_CODE; text: string; } { @@ -58,5 +58,6 @@ function timerMessageProps(type: 'synth' | 'deploy' | 'rollback'| 'destroy'): { case 'deploy': return { code: 'CDK_TOOLKIT_I5000', text: 'Deployment' }; case 'rollback': return { code: 'CDK_TOOLKIT_I6000', text: 'Rollback' }; case 'destroy': return { code: 'CDK_TOOLKIT_I7000', text: 'Destroy' }; + case 'bootstrap': return { code: 'CDK_TOOLKIT_I9000', text: 'Bootstrap' }; } } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 6a162525c..67cde5afb 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -4,6 +4,7 @@ import * as chalk from 'chalk'; import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; import { ToolkitServices } from './private'; +import { BootstrapEnvironmentOptions } from '../actions/bootstrap'; import { AssetBuildTime, type DeployOptions, RequireApproval } from '../actions/deploy'; import { type ExtendedDeployOptions, buildParameterMap, createHotswapPropertyOverrides, removePublishedAssets } from '../actions/deploy/private'; import { type DestroyOptions } from '../actions/destroy'; @@ -14,13 +15,17 @@ import { type RollbackOptions } from '../actions/rollback'; import { type SynthOptions } from '../actions/synth'; import { patternsArrayForWatch, WatchOptions } from '../actions/watch'; import { type SdkOptions } from '../api/aws-auth'; -import { DEFAULT_TOOLKIT_STACK_NAME, SdkProvider, SuccessfulDeployStackResult, StackCollection, Deployments, HotswapMode, StackActivityProgress, ResourceMigrator, obscureTemplate, serializeStructure, tagsForStack, CliIoHost, validateSnsTopicArn, Concurrency, WorkGraphBuilder, AssetBuildNode, AssetPublishNode, StackNode, formatErrorMessage, CloudWatchLogEventMonitor, findCloudWatchLogGroups, formatTime, StackDetails } from '../api/aws-cdk'; +import { DEFAULT_TOOLKIT_STACK_NAME, Bootstrapper, SdkProvider, SuccessfulDeployStackResult, StackCollection, Deployments, HotswapMode, StackActivityProgress, ResourceMigrator, obscureTemplate, serializeStructure, tagsForStack, CliIoHost, validateSnsTopicArn, Concurrency, WorkGraphBuilder, AssetBuildNode, AssetPublishNode, StackNode, formatErrorMessage, CloudWatchLogEventMonitor, findCloudWatchLogGroups, formatTime, StackDetails } from '../api/aws-cdk'; import { CachedCloudAssemblySource, IdentityCloudAssemblySource, StackAssembly, ICloudAssemblySource, StackSelectionStrategy } from '../api/cloud-assembly'; import { ALL_STACKS, CloudAssemblySourceBuilder } from '../api/cloud-assembly/private'; import { ToolkitError } from '../api/errors'; import { IIoHost, IoMessageCode, IoMessageLevel } from '../api/io'; import { asSdkLogger, withAction, Timer, confirm, error, info, success, warn, ActionAwareIoHost, debug, result, withoutEmojis, withoutColor, withTrimmedWhitespace } from '../api/io/private'; +// Must use a require() otherwise esbuild complains about calling a namespace +// eslint-disable-next-line @typescript-eslint/no-require-imports +const pLimit: typeof import('p-limit') = require('p-limit'); + /** * The current action being performed by the CLI. 'none' represents the absence of an action. */ @@ -153,6 +158,48 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab }; } + /** + * Bootstrap Action + */ + public async bootstrap(cx: ICloudAssemblySource, options: BootstrapEnvironmentOptions = {}): Promise { + const ioHost = withAction(this.ioHost, 'bootstrap'); + const assembly = await this.assemblyFromSource(cx); + const stackCollection = assembly.selectStacksV2({ + patterns: ['**'], + strategy: StackSelectionStrategy.ALL_STACKS, + }); + + const bootstrapper = new Bootstrapper(options.source, { ioHost: withAction(this.ioHost, 'bootstrap'), action: 'bootstrap' }); + + const environments = stackCollection.stackArtifacts.map(stack => stack.environment); + const sdkProvider = await this.sdkProvider('bootstrap'); + const limit = pLimit(20); + + // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism + await Promise.all(environments.map((environment: cxapi.Environment) => limit(async () => { + await ioHost.notify(info(`${chalk.bold(environment.name)}: bootstrapping...`)); + const bootstrapTimer = Timer.start(); + + try { + const bootstrapResult = await bootstrapper.bootstrapEnvironment(environment, sdkProvider, options); + const message = bootstrapResult.noOp + ? ` ✅ ${environment.name} (no changes)` + : ` ✅ ${environment.name}`; + + await ioHost.notify(success('\n' + message)); + await bootstrapTimer.endAs(ioHost, 'bootstrap'); + } catch (e) { + await ioHost.notify({ + time: new Date(), + level: 'error', + code: 'CDK_TOOLKIT_E9900', + message: `\n ❌ ${chalk.bold(environment.name)} failed: ${formatErrorMessage(e)}`, + }); + throw e; + } + }))); + } + /** * Synth Action */ diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts new file mode 100644 index 000000000..18eab38e0 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -0,0 +1,184 @@ +import { + CreateChangeSetCommand, + DeleteChangeSetCommand, + DescribeChangeSetCommand, + DescribeStacksCommand, + ExecuteChangeSetCommand, + Stack, +} from '@aws-sdk/client-cloudformation'; +import { Toolkit } from '../../lib/toolkit'; +import { TestIoHost, builderFixture } from '../_helpers'; +import { + mockCloudFormationClient, + restoreSdkMocksToDefault, + setDefaultSTSMocks, +} from '../util/aws-cdk'; + +const ioHost = new TestIoHost(); +const toolkit = new Toolkit({ ioHost }); + +beforeEach(() => { + restoreSdkMocksToDefault(); + setDefaultSTSMocks(); + ioHost.notifySpy.mockClear(); +}); + +afterEach(() => { + jest.resetAllMocks(); +}); + +describe('bootstrap', () => { + test('bootstrap creates a new stack if it does not exist', async () => { + // GIVEN + const mockStack = { + StackId: 'mock-stack-id', + StackName: 'CDKToolkit', + CreationTime: new Date(), + LastUpdatedTime: new Date(), + Outputs: [ + { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME' }, + { OutputKey: 'BucketDomainName', OutputValue: 'BUCKET_ENDPOINT' }, + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, + ], + } as Stack; + + mockCloudFormationClient + .on(DescribeStacksCommand) + .resolves({ Stacks: [] }) // First call - stack doesn't exist + .on(CreateChangeSetCommand) + .resolves({ Id: 'CHANGESET_ID' }) + .on(DescribeChangeSetCommand) + .resolves({ + Status: 'CREATE_COMPLETE', + Changes: [{ ResourceChange: { Action: 'Add' } }], + ExecutionStatus: 'AVAILABLE', + }) + .on(ExecuteChangeSetCommand) + .resolves({}) + .on(DescribeStacksCommand) + .resolves({ // Stack is in progress + Stacks: [{ + ...mockStack, + StackStatus: 'CREATE_IN_PROGRESS', + }], + }) + .on(DescribeStacksCommand) + .resolves({ // Final state - stack is complete + Stacks: [{ + ...mockStack, + StackStatus: 'CREATE_COMPLETE', + }], + }); + + // WHEN + const cx = await builderFixture(toolkit, 'stack-with-asset'); + await toolkit.bootstrap(cx); + + // THEN + expect(mockCloudFormationClient.calls().length).toBeGreaterThan(0); + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('bootstrapping...'), + })); + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('✅'), + })); + }); + + test('bootstrap handles no-op scenarios', async () => { + // GIVEN + const mockExistingStack = { + StackId: 'mock-stack-id', + StackName: 'CDKToolkit', + StackStatus: 'CREATE_COMPLETE', + CreationTime: new Date(), + LastUpdatedTime: new Date(), + Outputs: [ + { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME' }, + { OutputKey: 'BucketDomainName', OutputValue: 'BUCKET_ENDPOINT' }, + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, + ], + } as Stack; + + // First describe call to check if stack exists + mockCloudFormationClient + .on(DescribeStacksCommand) + .resolves({ Stacks: [mockExistingStack] }); + + // Create changeset call + mockCloudFormationClient + .on(CreateChangeSetCommand) + .resolves({ Id: 'CHANGESET_ID', StackId: mockExistingStack.StackId }); + + // Describe changeset call - indicate no changes + mockCloudFormationClient + .on(DescribeChangeSetCommand) + .resolves({ + Status: 'FAILED', + StatusReason: 'No updates are to be performed.', + Changes: [], + ExecutionStatus: 'UNAVAILABLE', + StackId: mockExistingStack.StackId, + ChangeSetId: 'CHANGESET_ID', + }); + + // Delete changeset call after no changes detected + mockCloudFormationClient + .on(DeleteChangeSetCommand) + .resolves({}); + + // Final describe call to get outputs + mockCloudFormationClient + .on(DescribeStacksCommand) + .resolves({ Stacks: [mockExistingStack] }); + + // WHEN + const cx = await builderFixture(toolkit, 'stack-with-asset'); + await toolkit.bootstrap(cx); + + // THEN + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('✅'), + })); + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('(no changes)'), + })); + }); + + describe('error handling', () => { + test('handles generic bootstrap errors', async () => { + // GIVEN + mockCloudFormationClient.onAnyCommand().rejects(new Error('Bootstrap failed')); + + // WHEN + const cx = await builderFixture(toolkit, 'stack-with-asset'); + await expect(toolkit.bootstrap(cx)).rejects.toThrow('Bootstrap failed'); + + // THEN + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + level: 'error', + message: expect.stringContaining('❌'), + })); + }); + + test('handles permission errors', async () => { + // GIVEN + const permissionError = new Error('Access Denied'); + permissionError.name = 'AccessDeniedException'; + mockCloudFormationClient.onAnyCommand().rejects(permissionError); + + // WHEN + const cx = await builderFixture(toolkit, 'stack-with-asset'); + await expect(toolkit.bootstrap(cx)).rejects.toThrow('Access Denied'); + + // THEN + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + level: 'error', + message: expect.stringContaining('❌'), + })); + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + level: 'error', + message: expect.stringContaining('Access Denied'), + })); + }); + }); +}); diff --git a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts index fa54a188f..9d8c5452f 100644 --- a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts @@ -1,3 +1,12 @@ /* eslint-disable import/no-restricted-paths */ -export { MockSdk } from '../../../../aws-cdk/test/util/mock-sdk'; +import * as path from 'node:path'; + +export { + MockSdk, MockSdkProvider, mockCloudFormationClient, mockS3Client, mockSTSClient, setDefaultSTSMocks, restoreSdkMocksToDefault, +} from '../../../../aws-cdk/test/util/mock-sdk'; +export * as os from 'node:os'; +export { path }; +export * as cxapi from '@aws-cdk/cx-api'; +export * as fs from 'fs-extra'; +export { SdkProvider, Bootstrapper, type StringWithoutPlaceholders } from '../../lib/api/aws-cdk'; \ No newline at end of file diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index 04dca429f..c027bb976 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -78,7 +78,6 @@ export const restoreSdkMocksToDefault = () => { mockS3Client.onAnyCommand().resolves({}); mockSecretsManagerClient.onAnyCommand().resolves({}); mockSSMClient.onAnyCommand().resolves({}); - mockSSMClient.onAnyCommand().resolves({}); }; /** From 6e3e58a7e22a277426971e069a3c113d9cc93d72 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Tue, 18 Feb 2025 22:27:31 -0500 Subject: [PATCH 02/41] import ordering --- .../@aws-cdk/toolkit-lib/test/util/aws-cdk.ts | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts index 9d8c5452f..6a565a9d2 100644 --- a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts @@ -1,12 +1,46 @@ /* eslint-disable import/no-restricted-paths */ +// Node.js built-in modules import * as path from 'node:path'; +import * as os from 'node:os'; -export { - MockSdk, MockSdkProvider, mockCloudFormationClient, mockS3Client, mockSTSClient, setDefaultSTSMocks, restoreSdkMocksToDefault, +// Third-party modules +import * as cxapi from '@aws-cdk/cx-api'; +import * as fs from 'fs-extra'; + +// Local modules +import { + MockSdk, + MockSdkProvider, + mockCloudFormationClient, + mockS3Client, + mockSTSClient, + setDefaultSTSMocks, + restoreSdkMocksToDefault, } from '../../../../aws-cdk/test/util/mock-sdk'; -export * as os from 'node:os'; + +import { + SdkProvider, + Bootstrapper, + type StringWithoutPlaceholders +} from '../../lib/api/aws-cdk'; + +// Re-exports export { path }; -export * as cxapi from '@aws-cdk/cx-api'; -export * as fs from 'fs-extra'; -export { SdkProvider, Bootstrapper, type StringWithoutPlaceholders } from '../../lib/api/aws-cdk'; \ No newline at end of file +export { os }; +export { cxapi }; +export { fs }; +export { + MockSdk, + MockSdkProvider, + mockCloudFormationClient, + mockS3Client, + mockSTSClient, + setDefaultSTSMocks, + restoreSdkMocksToDefault, +}; +export { + SdkProvider, + Bootstrapper, + type StringWithoutPlaceholders +}; \ No newline at end of file From 29973efa3a691983c2d0d2865551c960c62111a7 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 19 Feb 2025 00:09:20 -0500 Subject: [PATCH 03/41] bootstrap template YAML --- .../lib/api/bootstrap/bootstrap-template.yaml | 692 ++++++++++++++++++ 1 file changed, 692 insertions(+) create mode 100644 packages/@aws-cdk/toolkit-lib/lib/api/bootstrap/bootstrap-template.yaml diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/bootstrap/bootstrap-template.yaml b/packages/@aws-cdk/toolkit-lib/lib/api/bootstrap/bootstrap-template.yaml new file mode 100644 index 000000000..15acb72c4 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/api/bootstrap/bootstrap-template.yaml @@ -0,0 +1,692 @@ +Description: This stack includes resources needed to deploy AWS CDK apps into this + environment +Parameters: + TrustedAccounts: + Description: List of AWS accounts that are trusted to publish assets and deploy + stacks to this environment + Default: '' + Type: CommaDelimitedList + TrustedAccountsForLookup: + Description: List of AWS accounts that are trusted to look up values in this + environment + Default: '' + Type: CommaDelimitedList + CloudFormationExecutionPolicies: + Description: List of the ManagedPolicy ARN(s) to attach to the CloudFormation + deployment role + Default: '' + Type: CommaDelimitedList + FileAssetsBucketName: + Description: The name of the S3 bucket used for file assets + Default: '' + Type: String + FileAssetsBucketKmsKeyId: + Description: Empty to create a new key (default), 'AWS_MANAGED_KEY' to use a managed + S3 key, or the ID/ARN of an existing key. + Default: '' + Type: String + ContainerAssetsRepositoryName: + Description: A user-provided custom name to use for the container assets ECR repository + Default: '' + Type: String + Qualifier: + Description: An identifier to distinguish multiple bootstrap stacks in the same environment + Default: hnb659fds + Type: String + # "cdk-(qualifier)-image-publishing-role-(account)-(region)" needs to be <= 64 chars + # account = 12, region <= 14, 10 chars for qualifier and 28 for rest of role name + AllowedPattern: "[A-Za-z0-9_-]{1,10}" + ConstraintDescription: Qualifier must be an alphanumeric identifier of at most 10 characters + PublicAccessBlockConfiguration: + Description: Whether or not to enable S3 Staging Bucket Public Access Block Configuration + Default: 'true' + Type: 'String' + AllowedValues: ['true', 'false'] + InputPermissionsBoundary: + Description: Whether or not to use either the CDK supplied or custom permissions boundary + Default: '' + Type: 'String' + UseExamplePermissionsBoundary: + Default: 'false' + AllowedValues: [ 'true', 'false' ] + Type: String + BootstrapVariant: + Type: String + Default: 'AWS CDK: Default Resources' + Description: Describe the provenance of the resources in this bootstrap + stack. Change this when you customize the template. To prevent accidents, + the CDK CLI will not overwrite bootstrap stacks with a different variant. +Conditions: + HasTrustedAccounts: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: TrustedAccounts + HasTrustedAccountsForLookup: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: TrustedAccountsForLookup + HasCloudFormationExecutionPolicies: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: CloudFormationExecutionPolicies + HasCustomFileAssetsBucketName: + Fn::Not: + - Fn::Equals: + - '' + - Ref: FileAssetsBucketName + CreateNewKey: + Fn::Equals: + - '' + - Ref: FileAssetsBucketKmsKeyId + UseAwsManagedKey: + Fn::Equals: + - 'AWS_MANAGED_KEY' + - Ref: FileAssetsBucketKmsKeyId + ShouldCreatePermissionsBoundary: + Fn::Equals: + - 'true' + - Ref: UseExamplePermissionsBoundary + PermissionsBoundarySet: + Fn::Not: + - Fn::Equals: + - '' + - Ref: InputPermissionsBoundary + HasCustomContainerAssetsRepositoryName: + Fn::Not: + - Fn::Equals: + - '' + - Ref: ContainerAssetsRepositoryName + UsePublicAccessBlockConfiguration: + Fn::Equals: + - 'true' + - Ref: PublicAccessBlockConfiguration +Resources: + FileAssetsBucketEncryptionKey: + Type: AWS::KMS::Key + Properties: + KeyPolicy: + Statement: + - Action: + - kms:Create* + - kms:Describe* + - kms:Enable* + - kms:List* + - kms:Put* + - kms:Update* + - kms:Revoke* + - kms:Disable* + - kms:Get* + - kms:Delete* + - kms:ScheduleKeyDeletion + - kms:CancelKeyDeletion + - kms:GenerateDataKey + - kms:TagResource + - kms:UntagResource + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + Resource: "*" + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Principal: + # Not actually everyone -- see below for Conditions + AWS: "*" + Resource: "*" + Condition: + StringEquals: + # See https://docs.aws.amazon.com/kms/latest/developerguide/policy-conditions.html#conditions-kms-caller-account + kms:CallerAccount: + Ref: AWS::AccountId + kms:ViaService: + - Fn::Sub: s3.${AWS::Region}.amazonaws.com + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Principal: + AWS: + Fn::Sub: "${FilePublishingRole.Arn}" + Resource: "*" + Condition: CreateNewKey + FileAssetsBucketEncryptionKeyAlias: + Condition: CreateNewKey + Type: AWS::KMS::Alias + Properties: + AliasName: + Fn::Sub: "alias/cdk-${Qualifier}-assets-key" + TargetKeyId: + Ref: FileAssetsBucketEncryptionKey + StagingBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: + Fn::If: + - HasCustomFileAssetsBucketName + - Fn::Sub: "${FileAssetsBucketName}" + - Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region} + AccessControl: Private + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: aws:kms + KMSMasterKeyID: + Fn::If: + - CreateNewKey + - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" + - Fn::If: + - UseAwsManagedKey + - Ref: AWS::NoValue + - Fn::Sub: "${FileAssetsBucketKmsKeyId}" + PublicAccessBlockConfiguration: + Fn::If: + - UsePublicAccessBlockConfiguration + - BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + - Ref: AWS::NoValue + VersioningConfiguration: + Status: Enabled + LifecycleConfiguration: + Rules: + # Objects will only be noncurrent if they are deleted via garbage collection. + - Id: CleanupOldVersions + Status: Enabled + NoncurrentVersionExpiration: + NoncurrentDays: 30 + - Id: AbortIncompleteMultipartUploads + Status: Enabled + AbortIncompleteMultipartUpload: + DaysAfterInitiation: 1 + UpdateReplacePolicy: Retain + DeletionPolicy: Retain + 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: '*' + ContainerAssetsRepository: + Type: AWS::ECR::Repository + Properties: + ImageTagMutability: IMMUTABLE + # Untagged images should never exist but Security Hub wants this rule to exist + LifecyclePolicy: + LifecyclePolicyText: | + { + "rules": [ + { + "rulePriority": 1, + "description": "Untagged images should not exist, but expire any older than one year", + "selection": { + "tagStatus": "untagged", + "countType": "sinceImagePushed", + "countUnit": "days", + "countNumber": 365 + }, + "action": { "type": "expire" } + } + ] + } + RepositoryName: + Fn::If: + - HasCustomContainerAssetsRepositoryName + - Fn::Sub: "${ContainerAssetsRepositoryName}" + - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} + RepositoryPolicyText: + Version: "2012-10-17" + Statement: + # Necessary for Lambda container images + # https://docs.aws.amazon.com/lambda/latest/dg/configuration-images.html#configuration-images-permissions + - Sid: LambdaECRImageRetrievalPolicy + Effect: Allow + Principal: { Service: "lambda.amazonaws.com" } + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Condition: + StringLike: + "aws:sourceArn": { "Fn::Sub": "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*" } + FilePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + # allows this role to be assumed with session tags. + # see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_permissions-required + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: file-publishing + ImagePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + # allows this role to be assumed with session tags. + # see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_permissions-required + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: image-publishing + LookupRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + # allows this role to be assumed with session tags. + # see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_permissions-required + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccountsForLookup + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccountsForLookup + - Ref: AWS::NoValue + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region} + ManagedPolicyArns: + - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess" + Policies: + - PolicyDocument: + Statement: + - Sid: DontReadSecrets + Effect: Deny + Action: + - kms:Decrypt + Resource: "*" + Version: '2012-10-17' + PolicyName: LookupRolePolicy + Tags: + - Key: aws-cdk:bootstrap-role + Value: lookup + FilePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - s3:GetObject* + - s3:GetBucket* + - s3:GetEncryptionConfiguration + - s3:List* + - s3:DeleteObject* + - s3:PutObject* + - s3:Abort* + Resource: + - Fn::Sub: "${StagingBucket.Arn}" + - Fn::Sub: "${StagingBucket.Arn}/*" + Condition: + StringEquals: + aws:ResourceAccount: + - Fn::Sub: ${AWS::AccountId} + Effect: Allow + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Resource: + Fn::If: + - CreateNewKey + - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" + - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId} + Version: '2012-10-17' + Roles: + - Ref: FilePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + ImagePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + - ecr:BatchCheckLayerAvailability + - ecr:DescribeRepositories + - ecr:DescribeImages + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Resource: + Fn::Sub: "${ContainerAssetsRepository.Arn}" + Effect: Allow + - Action: + - ecr:GetAuthorizationToken + Resource: "*" + Effect: Allow + Version: '2012-10-17' + Roles: + - Ref: ImagePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + DeploymentActionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + # allows this role to be assumed with session tags. + # see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_permissions-required + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + Policies: + - PolicyDocument: + Statement: + - Sid: CloudFormationPermissions + Effect: Allow + Action: + - cloudformation:CreateChangeSet + - cloudformation:DeleteChangeSet + - cloudformation:DescribeChangeSet + - cloudformation:DescribeStacks + - cloudformation:ExecuteChangeSet + - cloudformation:CreateStack + - cloudformation:UpdateStack + - cloudformation:RollbackStack + - cloudformation:ContinueUpdateRollback + Resource: "*" + - Sid: PipelineCrossAccountArtifactsBucket + # Read/write buckets in different accounts. Permissions to buckets in + # same account are granted by bucket policies. + # + # Write permissions necessary to write outputs to the cross-region artifact replication bucket + # https://aws.amazon.com/premiumsupport/knowledge-center/codepipeline-deploy-cloudformation/. + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + - s3:Abort* + - s3:DeleteObject* + - s3:PutObject* + Resource: "*" + Condition: + StringNotEquals: + s3:ResourceAccount: + Ref: 'AWS::AccountId' + - Sid: PipelineCrossAccountArtifactsKey + # Use keys only for the purposes of reading encrypted files from S3. + Effect: Allow + Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Resource: "*" + Condition: + StringEquals: + kms:ViaService: + Fn::Sub: s3.${AWS::Region}.amazonaws.com + - Action: iam:PassRole + Resource: + Fn::Sub: "${CloudFormationExecutionRole.Arn}" + Effect: Allow + - Sid: CliPermissions + Action: + # Permissions needed by the CLI when doing `cdk deploy`. + # Our CI/CD does not need DeleteStack, + # but we also want to use this role from the CLI, + # and there you can call `cdk destroy` + - cloudformation:DescribeStackEvents + - cloudformation:GetTemplate + - cloudformation:DeleteStack + - cloudformation:UpdateTerminationProtection + - sts:GetCallerIdentity + # `cdk import` + - cloudformation:GetTemplateSummary + Resource: "*" + Effect: Allow + - Sid: CliStagingBucket + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + Resource: + - Fn::Sub: ${StagingBucket.Arn} + - Fn::Sub: ${StagingBucket.Arn}/* + - Sid: ReadVersion + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters # CreateChangeSet uses this to evaluate any SSM parameters (like `CdkBootstrapVersion`) + Resource: + - Fn::Sub: "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CdkBootstrapVersion}" + Version: '2012-10-17' + PolicyName: default + RoleName: + Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: deploy + CloudFormationExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: cloudformation.amazonaws.com + Version: '2012-10-17' + ManagedPolicyArns: + Fn::If: + - HasCloudFormationExecutionPolicies + - Ref: CloudFormationExecutionPolicies + - Fn::If: + - HasTrustedAccounts + # The CLI will prevent this case from occurring + - Ref: AWS::NoValue + # The CLI will advertise that we picked this implicitly + - - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" + RoleName: + Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} + PermissionsBoundary: + Fn::If: + - PermissionsBoundarySet + - Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}' + - Ref: AWS::NoValue + CdkBoostrapPermissionsBoundaryPolicy: + # Edit the template prior to boostrap in order to have this example policy created + Condition: ShouldCreatePermissionsBoundary + Type: AWS::IAM::ManagedPolicy + Properties: + PolicyDocument: + Statement: + # If permission boundaries do not have an explicit `allow`, then the effect is `deny` + - Sid: ExplicitAllowAll + Action: + - "*" + Effect: Allow + Resource: "*" + # Default permissions to prevent privilege escalation + - Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied + Action: + - iam:CreateUser + - iam:CreateRole + - iam:PutRolePermissionsBoundary + - iam:PutUserPermissionsBoundary + Condition: + StringNotEquals: + iam:PermissionsBoundary: + Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + Effect: Deny + Resource: "*" + # Forbid the policy itself being edited + - Sid: DenyPermBoundaryIAMPolicyAlteration + Action: + - iam:CreatePolicyVersion + - iam:DeletePolicy + - iam:DeletePolicyVersion + - iam:SetDefaultPolicyVersion + Effect: Deny + Resource: + Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + # Forbid removing the permissions boundary from any user or role that has it associated + - Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole + Action: + - iam:DeleteUserPermissionsBoundary + - iam:DeleteRolePermissionsBoundary + Effect: Deny + Resource: "*" + # Add your specific organizational security policy here + # Uncomment the example to deny access to AWS Config + #- Sid: OrganizationalSecurityPolicy + # Action: + # - "config:*" + # Effect: Deny + # Resource: "*" + Version: "2012-10-17" + Description: "Bootstrap Permission Boundary" + ManagedPolicyName: + Fn::Sub: cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + Path: / + # The SSM parameter is used in pipeline-deployed templates to verify the version + # of the bootstrap resources. + CdkBootstrapVersion: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: + Fn::Sub: '/cdk-bootstrap/${Qualifier}/version' + Value: '25' +Outputs: + BucketName: + Description: The name of the S3 bucket owned by the CDK toolkit stack + Value: + Fn::Sub: "${StagingBucket}" + BucketDomainName: + Description: The domain name of the S3 bucket owned by the CDK toolkit stack + Value: + Fn::Sub: "${StagingBucket.RegionalDomainName}" + # @deprecated - This Export can be removed at some future point in time. + # We can't do it today because if there are stacks that use it, the bootstrap + # stack cannot be updated. Not used anymore by apps >= 1.60.0 + FileAssetKeyArn: + Description: The ARN of the KMS key used to encrypt the asset bucket (deprecated) + Value: + Fn::If: + - CreateNewKey + - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" + - Fn::Sub: "${FileAssetsBucketKmsKeyId}" + Export: + Name: + Fn::Sub: CdkBootstrap-${Qualifier}-FileAssetKeyArn + ImageRepositoryName: + Description: The name of the ECR repository which hosts docker image assets + Value: + Fn::Sub: "${ContainerAssetsRepository}" + # The Output is used by the CLI to verify the version of the bootstrap resources. + BootstrapVersion: + Description: The version of the bootstrap resources that are currently mastered + in this stack + Value: + Fn::GetAtt: [CdkBootstrapVersion, Value] From 73d8aee19a6b3c79e1a775002cc196af5c06e755 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:11:39 -0500 Subject: [PATCH 04/41] fix the sdk provider mock --- .../@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts | 6 ++++++ packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index 18eab38e0..79f563545 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -9,6 +9,8 @@ import { import { Toolkit } from '../../lib/toolkit'; import { TestIoHost, builderFixture } from '../_helpers'; import { + MockSdkProvider, + SdkProvider, mockCloudFormationClient, restoreSdkMocksToDefault, setDefaultSTSMocks, @@ -16,6 +18,10 @@ import { const ioHost = new TestIoHost(); const toolkit = new Toolkit({ ioHost }); +const mockSdkProvider = new MockSdkProvider(); + +// we don't need to use AWS CLI compatible defaults here, since everything is mocked anyway +jest.spyOn(SdkProvider, 'withAwsCliCompatibleDefaults').mockResolvedValue(mockSdkProvider); beforeEach(() => { restoreSdkMocksToDefault(); diff --git a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts index 6a565a9d2..932ca20fd 100644 --- a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts @@ -1,8 +1,8 @@ /* eslint-disable import/no-restricted-paths */ // Node.js built-in modules -import * as path from 'node:path'; import * as os from 'node:os'; +import * as path from 'node:path'; // Third-party modules import * as cxapi from '@aws-cdk/cx-api'; @@ -22,7 +22,7 @@ import { import { SdkProvider, Bootstrapper, - type StringWithoutPlaceholders + type StringWithoutPlaceholders, } from '../../lib/api/aws-cdk'; // Re-exports @@ -42,5 +42,5 @@ export { export { SdkProvider, Bootstrapper, - type StringWithoutPlaceholders + type StringWithoutPlaceholders, }; \ No newline at end of file From f2769f57b40a398e1a0b2bfdaed69556770f3727 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:40:40 -0500 Subject: [PATCH 05/41] modified .projenrc.ts to add a post-compile task that copies the bootstrap template from cli-lib-alpha to toolkit-lib during build time --- .projenrc.ts | 2 ++ packages/@aws-cdk/toolkit-lib/.projen/tasks.json | 3 +++ packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts | 2 +- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 4 ++-- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.projenrc.ts b/.projenrc.ts index 757afeff2..e2306f2b2 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1139,6 +1139,8 @@ toolkitLib.package.addField('exports', { }); toolkitLib.postCompileTask.exec('node build-tools/bundle.mjs'); +// Copy bootstrap template from cli-lib-alpha +toolkitLib.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../cli-lib-alpha/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/'); // Smoke test built JS files toolkitLib.postCompileTask.exec('node ./lib/index.js >/dev/null 2>/dev/null /dev/null 2>/dev/null /dev/null 2>/dev/null { + public async bootstrap(cx: ICloudAssemblySource, options: BootstrapOptions = {}): Promise { const ioHost = withAction(this.ioHost, 'bootstrap'); const assembly = await this.assemblyFromSource(cx); const stackCollection = assembly.selectStacksV2({ From e972af1c363a20231d41bb2b6e1cd4f4c6de3bde Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:42:56 -0500 Subject: [PATCH 06/41] delete copied bootstrap-template.yaml file --- .../lib/api/bootstrap/bootstrap-template.yaml | 692 ------------------ 1 file changed, 692 deletions(-) delete mode 100644 packages/@aws-cdk/toolkit-lib/lib/api/bootstrap/bootstrap-template.yaml diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/bootstrap/bootstrap-template.yaml b/packages/@aws-cdk/toolkit-lib/lib/api/bootstrap/bootstrap-template.yaml deleted file mode 100644 index 15acb72c4..000000000 --- a/packages/@aws-cdk/toolkit-lib/lib/api/bootstrap/bootstrap-template.yaml +++ /dev/null @@ -1,692 +0,0 @@ -Description: This stack includes resources needed to deploy AWS CDK apps into this - environment -Parameters: - TrustedAccounts: - Description: List of AWS accounts that are trusted to publish assets and deploy - stacks to this environment - Default: '' - Type: CommaDelimitedList - TrustedAccountsForLookup: - Description: List of AWS accounts that are trusted to look up values in this - environment - Default: '' - Type: CommaDelimitedList - CloudFormationExecutionPolicies: - Description: List of the ManagedPolicy ARN(s) to attach to the CloudFormation - deployment role - Default: '' - Type: CommaDelimitedList - FileAssetsBucketName: - Description: The name of the S3 bucket used for file assets - Default: '' - Type: String - FileAssetsBucketKmsKeyId: - Description: Empty to create a new key (default), 'AWS_MANAGED_KEY' to use a managed - S3 key, or the ID/ARN of an existing key. - Default: '' - Type: String - ContainerAssetsRepositoryName: - Description: A user-provided custom name to use for the container assets ECR repository - Default: '' - Type: String - Qualifier: - Description: An identifier to distinguish multiple bootstrap stacks in the same environment - Default: hnb659fds - Type: String - # "cdk-(qualifier)-image-publishing-role-(account)-(region)" needs to be <= 64 chars - # account = 12, region <= 14, 10 chars for qualifier and 28 for rest of role name - AllowedPattern: "[A-Za-z0-9_-]{1,10}" - ConstraintDescription: Qualifier must be an alphanumeric identifier of at most 10 characters - PublicAccessBlockConfiguration: - Description: Whether or not to enable S3 Staging Bucket Public Access Block Configuration - Default: 'true' - Type: 'String' - AllowedValues: ['true', 'false'] - InputPermissionsBoundary: - Description: Whether or not to use either the CDK supplied or custom permissions boundary - Default: '' - Type: 'String' - UseExamplePermissionsBoundary: - Default: 'false' - AllowedValues: [ 'true', 'false' ] - Type: String - BootstrapVariant: - Type: String - Default: 'AWS CDK: Default Resources' - Description: Describe the provenance of the resources in this bootstrap - stack. Change this when you customize the template. To prevent accidents, - the CDK CLI will not overwrite bootstrap stacks with a different variant. -Conditions: - HasTrustedAccounts: - Fn::Not: - - Fn::Equals: - - '' - - Fn::Join: - - '' - - Ref: TrustedAccounts - HasTrustedAccountsForLookup: - Fn::Not: - - Fn::Equals: - - '' - - Fn::Join: - - '' - - Ref: TrustedAccountsForLookup - HasCloudFormationExecutionPolicies: - Fn::Not: - - Fn::Equals: - - '' - - Fn::Join: - - '' - - Ref: CloudFormationExecutionPolicies - HasCustomFileAssetsBucketName: - Fn::Not: - - Fn::Equals: - - '' - - Ref: FileAssetsBucketName - CreateNewKey: - Fn::Equals: - - '' - - Ref: FileAssetsBucketKmsKeyId - UseAwsManagedKey: - Fn::Equals: - - 'AWS_MANAGED_KEY' - - Ref: FileAssetsBucketKmsKeyId - ShouldCreatePermissionsBoundary: - Fn::Equals: - - 'true' - - Ref: UseExamplePermissionsBoundary - PermissionsBoundarySet: - Fn::Not: - - Fn::Equals: - - '' - - Ref: InputPermissionsBoundary - HasCustomContainerAssetsRepositoryName: - Fn::Not: - - Fn::Equals: - - '' - - Ref: ContainerAssetsRepositoryName - UsePublicAccessBlockConfiguration: - Fn::Equals: - - 'true' - - Ref: PublicAccessBlockConfiguration -Resources: - FileAssetsBucketEncryptionKey: - Type: AWS::KMS::Key - Properties: - KeyPolicy: - Statement: - - Action: - - kms:Create* - - kms:Describe* - - kms:Enable* - - kms:List* - - kms:Put* - - kms:Update* - - kms:Revoke* - - kms:Disable* - - kms:Get* - - kms:Delete* - - kms:ScheduleKeyDeletion - - kms:CancelKeyDeletion - - kms:GenerateDataKey - - kms:TagResource - - kms:UntagResource - Effect: Allow - Principal: - AWS: - Ref: AWS::AccountId - Resource: "*" - - Action: - - kms:Decrypt - - kms:DescribeKey - - kms:Encrypt - - kms:ReEncrypt* - - kms:GenerateDataKey* - Effect: Allow - Principal: - # Not actually everyone -- see below for Conditions - AWS: "*" - Resource: "*" - Condition: - StringEquals: - # See https://docs.aws.amazon.com/kms/latest/developerguide/policy-conditions.html#conditions-kms-caller-account - kms:CallerAccount: - Ref: AWS::AccountId - kms:ViaService: - - Fn::Sub: s3.${AWS::Region}.amazonaws.com - - Action: - - kms:Decrypt - - kms:DescribeKey - - kms:Encrypt - - kms:ReEncrypt* - - kms:GenerateDataKey* - Effect: Allow - Principal: - AWS: - Fn::Sub: "${FilePublishingRole.Arn}" - Resource: "*" - Condition: CreateNewKey - FileAssetsBucketEncryptionKeyAlias: - Condition: CreateNewKey - Type: AWS::KMS::Alias - Properties: - AliasName: - Fn::Sub: "alias/cdk-${Qualifier}-assets-key" - TargetKeyId: - Ref: FileAssetsBucketEncryptionKey - StagingBucket: - Type: AWS::S3::Bucket - Properties: - BucketName: - Fn::If: - - HasCustomFileAssetsBucketName - - Fn::Sub: "${FileAssetsBucketName}" - - Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region} - AccessControl: Private - BucketEncryption: - ServerSideEncryptionConfiguration: - - ServerSideEncryptionByDefault: - SSEAlgorithm: aws:kms - KMSMasterKeyID: - Fn::If: - - CreateNewKey - - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" - - Fn::If: - - UseAwsManagedKey - - Ref: AWS::NoValue - - Fn::Sub: "${FileAssetsBucketKmsKeyId}" - PublicAccessBlockConfiguration: - Fn::If: - - UsePublicAccessBlockConfiguration - - BlockPublicAcls: true - BlockPublicPolicy: true - IgnorePublicAcls: true - RestrictPublicBuckets: true - - Ref: AWS::NoValue - VersioningConfiguration: - Status: Enabled - LifecycleConfiguration: - Rules: - # Objects will only be noncurrent if they are deleted via garbage collection. - - Id: CleanupOldVersions - Status: Enabled - NoncurrentVersionExpiration: - NoncurrentDays: 30 - - Id: AbortIncompleteMultipartUploads - Status: Enabled - AbortIncompleteMultipartUpload: - DaysAfterInitiation: 1 - UpdateReplacePolicy: Retain - DeletionPolicy: Retain - 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: '*' - ContainerAssetsRepository: - Type: AWS::ECR::Repository - Properties: - ImageTagMutability: IMMUTABLE - # Untagged images should never exist but Security Hub wants this rule to exist - LifecyclePolicy: - LifecyclePolicyText: | - { - "rules": [ - { - "rulePriority": 1, - "description": "Untagged images should not exist, but expire any older than one year", - "selection": { - "tagStatus": "untagged", - "countType": "sinceImagePushed", - "countUnit": "days", - "countNumber": 365 - }, - "action": { "type": "expire" } - } - ] - } - RepositoryName: - Fn::If: - - HasCustomContainerAssetsRepositoryName - - Fn::Sub: "${ContainerAssetsRepositoryName}" - - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} - RepositoryPolicyText: - Version: "2012-10-17" - Statement: - # Necessary for Lambda container images - # https://docs.aws.amazon.com/lambda/latest/dg/configuration-images.html#configuration-images-permissions - - Sid: LambdaECRImageRetrievalPolicy - Effect: Allow - Principal: { Service: "lambda.amazonaws.com" } - Action: - - ecr:BatchGetImage - - ecr:GetDownloadUrlForLayer - Condition: - StringLike: - "aws:sourceArn": { "Fn::Sub": "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*" } - FilePublishingRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - # allows this role to be assumed with session tags. - # see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_permissions-required - - Action: sts:TagSession - Effect: Allow - Principal: - AWS: - Ref: AWS::AccountId - - Action: sts:AssumeRole - Effect: Allow - Principal: - AWS: - Ref: AWS::AccountId - - Fn::If: - - HasTrustedAccounts - - Action: sts:AssumeRole - Effect: Allow - Principal: - AWS: - Ref: TrustedAccounts - - Ref: AWS::NoValue - RoleName: - Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region} - Tags: - - Key: aws-cdk:bootstrap-role - Value: file-publishing - ImagePublishingRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - # allows this role to be assumed with session tags. - # see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_permissions-required - - Action: sts:TagSession - Effect: Allow - Principal: - AWS: - Ref: AWS::AccountId - - Action: sts:AssumeRole - Effect: Allow - Principal: - AWS: - Ref: AWS::AccountId - - Fn::If: - - HasTrustedAccounts - - Action: sts:AssumeRole - Effect: Allow - Principal: - AWS: - Ref: TrustedAccounts - - Ref: AWS::NoValue - RoleName: - Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region} - Tags: - - Key: aws-cdk:bootstrap-role - Value: image-publishing - LookupRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - # allows this role to be assumed with session tags. - # see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_permissions-required - - Action: sts:TagSession - Effect: Allow - Principal: - AWS: - Ref: AWS::AccountId - - Action: sts:AssumeRole - Effect: Allow - Principal: - AWS: - Ref: AWS::AccountId - - Fn::If: - - HasTrustedAccountsForLookup - - Action: sts:AssumeRole - Effect: Allow - Principal: - AWS: - Ref: TrustedAccountsForLookup - - Ref: AWS::NoValue - - Fn::If: - - HasTrustedAccounts - - Action: sts:AssumeRole - Effect: Allow - Principal: - AWS: - Ref: TrustedAccounts - - Ref: AWS::NoValue - RoleName: - Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region} - ManagedPolicyArns: - - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess" - Policies: - - PolicyDocument: - Statement: - - Sid: DontReadSecrets - Effect: Deny - Action: - - kms:Decrypt - Resource: "*" - Version: '2012-10-17' - PolicyName: LookupRolePolicy - Tags: - - Key: aws-cdk:bootstrap-role - Value: lookup - FilePublishingRoleDefaultPolicy: - Type: AWS::IAM::Policy - Properties: - PolicyDocument: - Statement: - - Action: - - s3:GetObject* - - s3:GetBucket* - - s3:GetEncryptionConfiguration - - s3:List* - - s3:DeleteObject* - - s3:PutObject* - - s3:Abort* - Resource: - - Fn::Sub: "${StagingBucket.Arn}" - - Fn::Sub: "${StagingBucket.Arn}/*" - Condition: - StringEquals: - aws:ResourceAccount: - - Fn::Sub: ${AWS::AccountId} - Effect: Allow - - Action: - - kms:Decrypt - - kms:DescribeKey - - kms:Encrypt - - kms:ReEncrypt* - - kms:GenerateDataKey* - Effect: Allow - Resource: - Fn::If: - - CreateNewKey - - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" - - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId} - Version: '2012-10-17' - Roles: - - Ref: FilePublishingRole - PolicyName: - Fn::Sub: cdk-${Qualifier}-file-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} - ImagePublishingRoleDefaultPolicy: - Type: AWS::IAM::Policy - Properties: - PolicyDocument: - Statement: - - Action: - - ecr:PutImage - - ecr:InitiateLayerUpload - - ecr:UploadLayerPart - - ecr:CompleteLayerUpload - - ecr:BatchCheckLayerAvailability - - ecr:DescribeRepositories - - ecr:DescribeImages - - ecr:BatchGetImage - - ecr:GetDownloadUrlForLayer - Resource: - Fn::Sub: "${ContainerAssetsRepository.Arn}" - Effect: Allow - - Action: - - ecr:GetAuthorizationToken - Resource: "*" - Effect: Allow - Version: '2012-10-17' - Roles: - - Ref: ImagePublishingRole - PolicyName: - Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} - DeploymentActionRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - # allows this role to be assumed with session tags. - # see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_permissions-required - - Action: sts:TagSession - Effect: Allow - Principal: - AWS: - Ref: AWS::AccountId - - Action: sts:AssumeRole - Effect: Allow - Principal: - AWS: - Ref: AWS::AccountId - - Fn::If: - - HasTrustedAccounts - - Action: sts:AssumeRole - Effect: Allow - Principal: - AWS: - Ref: TrustedAccounts - - Ref: AWS::NoValue - Policies: - - PolicyDocument: - Statement: - - Sid: CloudFormationPermissions - Effect: Allow - Action: - - cloudformation:CreateChangeSet - - cloudformation:DeleteChangeSet - - cloudformation:DescribeChangeSet - - cloudformation:DescribeStacks - - cloudformation:ExecuteChangeSet - - cloudformation:CreateStack - - cloudformation:UpdateStack - - cloudformation:RollbackStack - - cloudformation:ContinueUpdateRollback - Resource: "*" - - Sid: PipelineCrossAccountArtifactsBucket - # Read/write buckets in different accounts. Permissions to buckets in - # same account are granted by bucket policies. - # - # Write permissions necessary to write outputs to the cross-region artifact replication bucket - # https://aws.amazon.com/premiumsupport/knowledge-center/codepipeline-deploy-cloudformation/. - Effect: Allow - Action: - - s3:GetObject* - - s3:GetBucket* - - s3:List* - - s3:Abort* - - s3:DeleteObject* - - s3:PutObject* - Resource: "*" - Condition: - StringNotEquals: - s3:ResourceAccount: - Ref: 'AWS::AccountId' - - Sid: PipelineCrossAccountArtifactsKey - # Use keys only for the purposes of reading encrypted files from S3. - Effect: Allow - Action: - - kms:Decrypt - - kms:DescribeKey - - kms:Encrypt - - kms:ReEncrypt* - - kms:GenerateDataKey* - Resource: "*" - Condition: - StringEquals: - kms:ViaService: - Fn::Sub: s3.${AWS::Region}.amazonaws.com - - Action: iam:PassRole - Resource: - Fn::Sub: "${CloudFormationExecutionRole.Arn}" - Effect: Allow - - Sid: CliPermissions - Action: - # Permissions needed by the CLI when doing `cdk deploy`. - # Our CI/CD does not need DeleteStack, - # but we also want to use this role from the CLI, - # and there you can call `cdk destroy` - - cloudformation:DescribeStackEvents - - cloudformation:GetTemplate - - cloudformation:DeleteStack - - cloudformation:UpdateTerminationProtection - - sts:GetCallerIdentity - # `cdk import` - - cloudformation:GetTemplateSummary - Resource: "*" - Effect: Allow - - Sid: CliStagingBucket - Effect: Allow - Action: - - s3:GetObject* - - s3:GetBucket* - - s3:List* - Resource: - - Fn::Sub: ${StagingBucket.Arn} - - Fn::Sub: ${StagingBucket.Arn}/* - - Sid: ReadVersion - Effect: Allow - Action: - - ssm:GetParameter - - ssm:GetParameters # CreateChangeSet uses this to evaluate any SSM parameters (like `CdkBootstrapVersion`) - Resource: - - Fn::Sub: "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CdkBootstrapVersion}" - Version: '2012-10-17' - PolicyName: default - RoleName: - Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region} - Tags: - - Key: aws-cdk:bootstrap-role - Value: deploy - CloudFormationExecutionRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: cloudformation.amazonaws.com - Version: '2012-10-17' - ManagedPolicyArns: - Fn::If: - - HasCloudFormationExecutionPolicies - - Ref: CloudFormationExecutionPolicies - - Fn::If: - - HasTrustedAccounts - # The CLI will prevent this case from occurring - - Ref: AWS::NoValue - # The CLI will advertise that we picked this implicitly - - - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" - RoleName: - Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} - PermissionsBoundary: - Fn::If: - - PermissionsBoundarySet - - Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}' - - Ref: AWS::NoValue - CdkBoostrapPermissionsBoundaryPolicy: - # Edit the template prior to boostrap in order to have this example policy created - Condition: ShouldCreatePermissionsBoundary - Type: AWS::IAM::ManagedPolicy - Properties: - PolicyDocument: - Statement: - # If permission boundaries do not have an explicit `allow`, then the effect is `deny` - - Sid: ExplicitAllowAll - Action: - - "*" - Effect: Allow - Resource: "*" - # Default permissions to prevent privilege escalation - - Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied - Action: - - iam:CreateUser - - iam:CreateRole - - iam:PutRolePermissionsBoundary - - iam:PutUserPermissionsBoundary - Condition: - StringNotEquals: - iam:PermissionsBoundary: - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} - Effect: Deny - Resource: "*" - # Forbid the policy itself being edited - - Sid: DenyPermBoundaryIAMPolicyAlteration - Action: - - iam:CreatePolicyVersion - - iam:DeletePolicy - - iam:DeletePolicyVersion - - iam:SetDefaultPolicyVersion - Effect: Deny - Resource: - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} - # Forbid removing the permissions boundary from any user or role that has it associated - - Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole - Action: - - iam:DeleteUserPermissionsBoundary - - iam:DeleteRolePermissionsBoundary - Effect: Deny - Resource: "*" - # Add your specific organizational security policy here - # Uncomment the example to deny access to AWS Config - #- Sid: OrganizationalSecurityPolicy - # Action: - # - "config:*" - # Effect: Deny - # Resource: "*" - Version: "2012-10-17" - Description: "Bootstrap Permission Boundary" - ManagedPolicyName: - Fn::Sub: cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} - Path: / - # The SSM parameter is used in pipeline-deployed templates to verify the version - # of the bootstrap resources. - CdkBootstrapVersion: - Type: AWS::SSM::Parameter - Properties: - Type: String - Name: - Fn::Sub: '/cdk-bootstrap/${Qualifier}/version' - Value: '25' -Outputs: - BucketName: - Description: The name of the S3 bucket owned by the CDK toolkit stack - Value: - Fn::Sub: "${StagingBucket}" - BucketDomainName: - Description: The domain name of the S3 bucket owned by the CDK toolkit stack - Value: - Fn::Sub: "${StagingBucket.RegionalDomainName}" - # @deprecated - This Export can be removed at some future point in time. - # We can't do it today because if there are stacks that use it, the bootstrap - # stack cannot be updated. Not used anymore by apps >= 1.60.0 - FileAssetKeyArn: - Description: The ARN of the KMS key used to encrypt the asset bucket (deprecated) - Value: - Fn::If: - - CreateNewKey - - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" - - Fn::Sub: "${FileAssetsBucketKmsKeyId}" - Export: - Name: - Fn::Sub: CdkBootstrap-${Qualifier}-FileAssetKeyArn - ImageRepositoryName: - Description: The name of the ECR repository which hosts docker image assets - Value: - Fn::Sub: "${ContainerAssetsRepository}" - # The Output is used by the CLI to verify the version of the bootstrap resources. - BootstrapVersion: - Description: The version of the bootstrap resources that are currently mastered - in this stack - Value: - Fn::GetAtt: [CdkBootstrapVersion, Value] From 91b9ae84fc660c00b7c1ce168325553d6d446c3f Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 19 Feb 2025 11:00:38 -0500 Subject: [PATCH 07/41] some import optimizations --- .../toolkit-lib/lib/actions/bootstrap/index.ts | 3 +-- .../lib/actions/bootstrap/private/helpers.ts | 13 ------------- .../lib/actions/bootstrap/private/index.ts | 1 - packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts | 5 ++--- .../@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 5 +---- .../@aws-cdk/toolkit-lib/lib/util/concurrency.ts | 7 +++++++ 6 files changed, 11 insertions(+), 23 deletions(-) delete mode 100644 packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts delete mode 100644 packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts create mode 100644 packages/@aws-cdk/toolkit-lib/lib/util/concurrency.ts diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts index 8bae309ac..89d307950 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts @@ -1,5 +1,4 @@ -import { Tag } from './private'; -import { StringWithoutPlaceholders } from '../../api/aws-cdk'; +import { Tag, StringWithoutPlaceholders } from '../../api/aws-cdk'; /** * Options for the bootstrapEnvironment operation(s) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts deleted file mode 100644 index 76d00f045..000000000 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type * as cxapi from '@aws-cdk/cx-api'; - -/** - * @returns an array with the tags available in the stack metadata. - */ -export function tagsForStack(stack: cxapi.CloudFormationStackArtifact): Tag[] { - return Object.entries(stack.tags).map(([Key, Value]) => ({ Key, Value })); -} - -export interface Tag { - readonly Key: string; - readonly Value: string; -} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts deleted file mode 100644 index c0410d337..000000000 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './helpers'; \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts index 5d8c9eb72..4eb23e4ce 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts @@ -1,18 +1,17 @@ /* eslint-disable import/no-restricted-paths */ -export { Bootstrapper } from '../../../../aws-cdk/lib/api'; - // APIs export { formatSdkLoggerContent, SdkProvider } from '../../../../aws-cdk/lib/api/aws-auth'; export { Context, PROJECT_CONTEXT } from '../../../../aws-cdk/lib/api/context'; export { Deployments, type SuccessfulDeployStackResult } from '../../../../aws-cdk/lib/api/deployments'; export { Settings } from '../../../../aws-cdk/lib/api/settings'; -export { tagsForStack } from '../../../../aws-cdk/lib/api/tags'; +export { Tag, tagsForStack } from '../../../../aws-cdk/lib/api/tags'; export { DEFAULT_TOOLKIT_STACK_NAME } from '../../../../aws-cdk/lib/api/toolkit-info'; export { ResourceMigrator } from '../../../../aws-cdk/lib/api/resource-import'; export { StackActivityProgress } from '../../../../aws-cdk/lib/api/stack-events'; export { CloudWatchLogEventMonitor, findCloudWatchLogGroups } from '../../../../aws-cdk/lib/api/logs'; export { WorkGraph, WorkGraphBuilder, AssetBuildNode, AssetPublishNode, StackNode, Concurrency } from '../../../../aws-cdk/lib/api/work-graph'; +export { Bootstrapper } from '../../../../aws-cdk/lib/api/bootstrap'; // Context Providers export * as contextproviders from '../../../../aws-cdk/lib/context-providers'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 7f2100c01..a67da1709 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -21,10 +21,7 @@ import { ALL_STACKS, CloudAssemblySourceBuilder } from '../api/cloud-assembly/pr import { ToolkitError } from '../api/errors'; import { IIoHost, IoMessageCode, IoMessageLevel } from '../api/io'; import { asSdkLogger, withAction, Timer, confirm, error, info, success, warn, ActionAwareIoHost, debug, result, withoutEmojis, withoutColor, withTrimmedWhitespace } from '../api/io/private'; - -// Must use a require() otherwise esbuild complains about calling a namespace -// eslint-disable-next-line @typescript-eslint/no-require-imports -const pLimit: typeof import('p-limit') = require('p-limit'); +import { pLimit } from '../util/concurrency'; /** * The current action being performed by the CLI. 'none' represents the absence of an action. diff --git a/packages/@aws-cdk/toolkit-lib/lib/util/concurrency.ts b/packages/@aws-cdk/toolkit-lib/lib/util/concurrency.ts new file mode 100644 index 000000000..e59afc99a --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/util/concurrency.ts @@ -0,0 +1,7 @@ +/** + * Re-export p-limit for concurrency control + */ +// Must use a require() otherwise esbuild complains about calling a namespace +// eslint-disable-next-line @typescript-eslint/no-require-imports +const pLimit: typeof import('p-limit') = require('p-limit'); +export { pLimit }; From 35a789bb5a02e9e143c31924b96391a0dd65ff2a Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:02:11 -0500 Subject: [PATCH 08/41] use toolkit's toolkitStackName --- packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts | 3 +-- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts index 89d307950..ebb8583f9 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts @@ -4,7 +4,6 @@ import { Tag, StringWithoutPlaceholders } from '../../api/aws-cdk'; * Options for the bootstrapEnvironment operation(s) */ export interface BootstrapOptions { - readonly toolkitStackName?: string; readonly roleArn?: StringWithoutPlaceholders; readonly parameters?: BootstrappingParameters; readonly force?: boolean; @@ -134,4 +133,4 @@ export interface BootstrappingParameters { readonly customPermissionsBoundary?: string; } -export type BootstrapSource = { source: 'legacy' } | { source: 'default' } | { source: 'custom'; templateFile: string }; \ No newline at end of file +export type BootstrapSource = { source: 'legacy' } | { source: 'default' } | { source: 'custom'; templateFile: string }; diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index a67da1709..47fa854ed 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -178,7 +178,8 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const bootstrapTimer = Timer.start(); try { - const bootstrapResult = await bootstrapper.bootstrapEnvironment(environment, sdkProvider, options); + const { toolkitStackName } = this; + const bootstrapResult = await bootstrapper.bootstrapEnvironment(environment, sdkProvider, { ...options, toolkitStackName }); const message = bootstrapResult.noOp ? ` ✅ ${environment.name} (no changes)` : ` ✅ ${environment.name}`; From f8d8f4e0ac277f24a2eef3f79dbaf34f10b18383 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:16:19 -0500 Subject: [PATCH 09/41] new api and tests for custom bootstrap template --- .projenrc.ts | 5 +- packages/@aws-cdk/toolkit-lib/.npmignore | 1 + .../@aws-cdk/toolkit-lib/.projen/tasks.json | 2 +- .../toolkit-lib/build-tools/bundle.mjs | 2 +- .../lib/actions/bootstrap/index.ts | 26 ++- .../@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts | 1 + .../test/actions/bootstrap.test.ts | 193 +++++++++++++----- .../@aws-cdk/toolkit-lib/test/util/aws-cdk.ts | 4 +- .../bootstrap/custom-bootstrap-template.yaml | 99 +++++++++ 9 files changed, 266 insertions(+), 67 deletions(-) create mode 100644 packages/aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml diff --git a/.projenrc.ts b/.projenrc.ts index e2306f2b2..3982339fd 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -982,7 +982,7 @@ new JsiiBuild(cliLib, { // clilib needs to bundle some resources, same as the CLI cliLib.postCompileTask.exec('node-bundle validate --external=fsevents:optional --entrypoint=lib/index.js --fix --dont-attribute "^@aws-cdk/|^cdk-assets$|^cdk-cli-wrapper$|^aws-cdk$"'); -cliLib.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/'); +cliLib.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml ./lib/api/bootstrap/'); for (const resourceCommand of includeCliResourcesCommands) { cliLib.postCompileTask.exec(resourceCommand); } @@ -1140,7 +1140,7 @@ toolkitLib.package.addField('exports', { toolkitLib.postCompileTask.exec('node build-tools/bundle.mjs'); // Copy bootstrap template from cli-lib-alpha -toolkitLib.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../cli-lib-alpha/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/'); +toolkitLib.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../cli-lib-alpha/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/ && cp ../cli-lib-alpha/lib/api/bootstrap/custom-bootstrap-template.yaml ./lib/api/bootstrap/'); // Smoke test built JS files toolkitLib.postCompileTask.exec('node ./lib/index.js >/dev/null 2>/dev/null /dev/null 2>/dev/null /dev/null 2>/dev/null { jest.resetAllMocks(); }); +function setupMockCloudFormationClient(mockStack: Stack) { + mockCloudFormationClient + .on(DescribeStacksCommand) + .resolves({ Stacks: [] }) // First call - stack doesn't exist + .on(CreateChangeSetCommand) + .resolves({ Id: 'CHANGESET_ID' }) + .on(DescribeChangeSetCommand) + .resolves({ + Status: 'CREATE_COMPLETE', + Changes: [{ ResourceChange: { Action: 'Add' } }], + ExecutionStatus: 'AVAILABLE', + }) + .on(ExecuteChangeSetCommand) + .resolves({}) + .on(DescribeStacksCommand) + .resolves({ // Stack is in progress + Stacks: [{ + ...mockStack, + StackStatus: 'CREATE_IN_PROGRESS', + }], + }) + .on(DescribeStacksCommand) + .resolves({ // Final state - stack is complete + Stacks: [{ + ...mockStack, + StackStatus: 'CREATE_COMPLETE', + }], + }); +} + +function createMockStack(outputs: { OutputKey: string; OutputValue: string }[]): Stack { + return { + StackId: 'mock-stack-id', + StackName: 'CDKToolkit', + CreationTime: new Date(), + LastUpdatedTime: new Date(), + Outputs: outputs, + } as Stack; +} + +async function runBootstrap(options?: { source?: BootstrapSource }) { + const cx = await builderFixture(toolkit, 'stack-with-asset'); + return toolkit.bootstrap(cx, options); +} + +function expectSuccessfulBootstrap() { + expect(mockCloudFormationClient.calls().length).toBeGreaterThan(0); + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('bootstrapping...'), + })); + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('✅'), + })); +} + describe('bootstrap', () => { - test('bootstrap creates a new stack if it does not exist', async () => { - // GIVEN - const mockStack = { - StackId: 'mock-stack-id', - StackName: 'CDKToolkit', - CreationTime: new Date(), - LastUpdatedTime: new Date(), - Outputs: [ + describe('template sources', () => { + test('uses default template when no source is specified', async () => { + // GIVEN + const mockStack = createMockStack([ { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME' }, { OutputKey: 'BucketDomainName', OutputValue: 'BUCKET_ENDPOINT' }, { OutputKey: 'BootstrapVersion', OutputValue: '1' }, - ], - } as Stack; + ]); + setupMockCloudFormationClient(mockStack); - mockCloudFormationClient - .on(DescribeStacksCommand) - .resolves({ Stacks: [] }) // First call - stack doesn't exist - .on(CreateChangeSetCommand) - .resolves({ Id: 'CHANGESET_ID' }) - .on(DescribeChangeSetCommand) - .resolves({ - Status: 'CREATE_COMPLETE', - Changes: [{ ResourceChange: { Action: 'Add' } }], - ExecutionStatus: 'AVAILABLE', - }) - .on(ExecuteChangeSetCommand) - .resolves({}) - .on(DescribeStacksCommand) - .resolves({ // Stack is in progress - Stacks: [{ - ...mockStack, - StackStatus: 'CREATE_IN_PROGRESS', - }], - }) - .on(DescribeStacksCommand) - .resolves({ // Final state - stack is complete - Stacks: [{ - ...mockStack, - StackStatus: 'CREATE_COMPLETE', - }], + // WHEN + await runBootstrap(); + + // THEN + expectSuccessfulBootstrap(); + }); + + test('uses custom template when specified (old api)', async () => { + // GIVEN + const mockStack = createMockStack([ + { OutputKey: 'BucketName', OutputValue: 'CUSTOM_BUCKET_NAME' }, + { OutputKey: 'BucketDomainName', OutputValue: 'CUSTOM_BUCKET_ENDPOINT' }, + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, + ]); + setupMockCloudFormationClient(mockStack); + + // WHEN + + await runBootstrap({ + source: { + source: 'custom', + templateFile: path.join(rootDir(), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml'), + }, }); - // WHEN - const cx = await builderFixture(toolkit, 'stack-with-asset'); - await toolkit.bootstrap(cx); + // THEN + const createChangeSetCalls = mockCloudFormationClient.calls().filter(call => call.args[0] instanceof CreateChangeSetCommand); + expect(createChangeSetCalls.length).toBeGreaterThan(0); + expectSuccessfulBootstrap(); + }); - // THEN - expect(mockCloudFormationClient.calls().length).toBeGreaterThan(0); - expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - message: expect.stringContaining('bootstrapping...'), - })); - expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - message: expect.stringContaining('✅'), - })); + test('uses custom template when specified (new api)', async () => { + // GIVEN + const mockStack = createMockStack([ + { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME' }, + { OutputKey: 'BucketDomainName', OutputValue: 'BUCKET_ENDPOINT' }, + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, + ]); + setupMockCloudFormationClient(mockStack); + + // WHEN + await runBootstrap({ + source: BootstrapSource.customTemplate(path.join(rootDir(), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml')), + }); + + // THEN + const createChangeSetCalls = mockCloudFormationClient.calls().filter(call => call.args[0] instanceof CreateChangeSetCommand); + expect(createChangeSetCalls.length).toBeGreaterThan(0); + expectSuccessfulBootstrap(); + }); + + test('handles errors with custom template', async () => { + // GIVEN + const templateError = new Error('Invalid template file'); + mockCloudFormationClient + .on(DescribeStacksCommand) + .rejects(templateError); + + // WHEN + await expect(runBootstrap({ + source: { + source: 'custom', + templateFile: path.join(rootDir(), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml'), + }, + })).rejects.toThrow('Invalid template file'); + + // THEN + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + level: 'error', + message: expect.stringContaining('❌'), + })); + }); }); test('bootstrap handles no-op scenarios', async () => { @@ -138,13 +224,10 @@ describe('bootstrap', () => { .resolves({ Stacks: [mockExistingStack] }); // WHEN - const cx = await builderFixture(toolkit, 'stack-with-asset'); - await toolkit.bootstrap(cx); + await runBootstrap(); // THEN - expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - message: expect.stringContaining('✅'), - })); + expectSuccessfulBootstrap(); expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining('(no changes)'), })); @@ -156,8 +239,7 @@ describe('bootstrap', () => { mockCloudFormationClient.onAnyCommand().rejects(new Error('Bootstrap failed')); // WHEN - const cx = await builderFixture(toolkit, 'stack-with-asset'); - await expect(toolkit.bootstrap(cx)).rejects.toThrow('Bootstrap failed'); + await expect(runBootstrap()).rejects.toThrow('Bootstrap failed'); // THEN expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ @@ -173,8 +255,7 @@ describe('bootstrap', () => { mockCloudFormationClient.onAnyCommand().rejects(permissionError); // WHEN - const cx = await builderFixture(toolkit, 'stack-with-asset'); - await expect(toolkit.bootstrap(cx)).rejects.toThrow('Access Denied'); + await expect(runBootstrap()).rejects.toThrow('Access Denied'); // THEN expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ diff --git a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts index 932ca20fd..03ed33471 100644 --- a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts @@ -22,7 +22,7 @@ import { import { SdkProvider, Bootstrapper, - type StringWithoutPlaceholders, + rootDir, } from '../../lib/api/aws-cdk'; // Re-exports @@ -42,5 +42,5 @@ export { export { SdkProvider, Bootstrapper, - type StringWithoutPlaceholders, + rootDir, }; \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml b/packages/aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml new file mode 100644 index 000000000..c5f5dff12 --- /dev/null +++ b/packages/aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml @@ -0,0 +1,99 @@ +Description: Custom CDK Bootstrap Template + +Parameters: + Qualifier: + Type: String + Description: Qualifier for the bootstrap resources + Default: custom + + CustomTagKey: + Type: String + Description: Key for a custom tag to apply to all resources + Default: Environment + + CustomTagValue: + Type: String + Description: Value for the custom tag + Default: Development + +Resources: + StagingBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: + Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region} + VersioningConfiguration: + Status: Enabled + LifecycleConfiguration: + Rules: + - Id: DeleteOldVersions + Status: Enabled + NoncurrentVersionExpiration: + NoncurrentDays: 90 + Tags: + - Key: + Ref: CustomTagKey + Value: + Ref: CustomTagValue + + ContainerAssetsRepository: + Type: AWS::ECR::Repository + Properties: + RepositoryName: + Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} + ImageScanningConfiguration: + ScanOnPush: true + Tags: + - Key: + Ref: CustomTagKey + Value: + Ref: CustomTagValue + + CloudFormationExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: cloudformation.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess + RoleName: + Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: + Ref: CustomTagKey + Value: + Ref: CustomTagValue + + CdkBootstrapVersion: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: + Fn::Sub: /cdk-bootstrap/${Qualifier}/version + Value: '1' + +Outputs: + BucketName: + Description: The name of the S3 bucket for CDK assets + Value: + Ref: StagingBucket + + RepositoryName: + Description: The name of the ECR repository for container assets + Value: + Ref: ContainerAssetsRepository + + CloudFormationExecutionRoleArn: + Description: The ARN of the CloudFormation execution role + Value: + Fn::GetAtt: [CloudFormationExecutionRole, Arn] + + BootstrapVersion: + Description: The version of this bootstrap stack + Value: + Fn::GetAtt: [CdkBootstrapVersion, Value] \ No newline at end of file From 8ff41567450e53cc459c73f8ade5875cfd96b031 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 19 Feb 2025 19:28:51 +0000 Subject: [PATCH 10/41] chore: self mutation Signed-off-by: github-actions --- .../@aws-cdk/cli-lib-alpha/.projen/tasks.json | 2 +- .../bootstrap/custom-bootstrap-template.yaml | 99 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/cli-lib-alpha/lib/api/bootstrap/custom-bootstrap-template.yaml diff --git a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json index 2ad251abc..fbd365e1b 100644 --- a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json +++ b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json @@ -241,7 +241,7 @@ "exec": "node-bundle validate --external=fsevents:optional --entrypoint=lib/index.js --fix --dont-attribute \"^@aws-cdk/|^cdk-assets$|^cdk-cli-wrapper$|^aws-cdk$\"" }, { - "exec": "mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/" + "exec": "mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml ./lib/api/bootstrap/" }, { "exec": "cp $(node -p 'require.resolve(\"cdk-from-cfn/index_bg.wasm\")') ./lib/" diff --git a/packages/@aws-cdk/cli-lib-alpha/lib/api/bootstrap/custom-bootstrap-template.yaml b/packages/@aws-cdk/cli-lib-alpha/lib/api/bootstrap/custom-bootstrap-template.yaml new file mode 100644 index 000000000..c5f5dff12 --- /dev/null +++ b/packages/@aws-cdk/cli-lib-alpha/lib/api/bootstrap/custom-bootstrap-template.yaml @@ -0,0 +1,99 @@ +Description: Custom CDK Bootstrap Template + +Parameters: + Qualifier: + Type: String + Description: Qualifier for the bootstrap resources + Default: custom + + CustomTagKey: + Type: String + Description: Key for a custom tag to apply to all resources + Default: Environment + + CustomTagValue: + Type: String + Description: Value for the custom tag + Default: Development + +Resources: + StagingBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: + Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region} + VersioningConfiguration: + Status: Enabled + LifecycleConfiguration: + Rules: + - Id: DeleteOldVersions + Status: Enabled + NoncurrentVersionExpiration: + NoncurrentDays: 90 + Tags: + - Key: + Ref: CustomTagKey + Value: + Ref: CustomTagValue + + ContainerAssetsRepository: + Type: AWS::ECR::Repository + Properties: + RepositoryName: + Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} + ImageScanningConfiguration: + ScanOnPush: true + Tags: + - Key: + Ref: CustomTagKey + Value: + Ref: CustomTagValue + + CloudFormationExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: cloudformation.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess + RoleName: + Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: + Ref: CustomTagKey + Value: + Ref: CustomTagValue + + CdkBootstrapVersion: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: + Fn::Sub: /cdk-bootstrap/${Qualifier}/version + Value: '1' + +Outputs: + BucketName: + Description: The name of the S3 bucket for CDK assets + Value: + Ref: StagingBucket + + RepositoryName: + Description: The name of the ECR repository for container assets + Value: + Ref: ContainerAssetsRepository + + CloudFormationExecutionRoleArn: + Description: The ARN of the CloudFormation execution role + Value: + Fn::GetAtt: [CloudFormationExecutionRole, Arn] + + BootstrapVersion: + Description: The version of this bootstrap stack + Value: + Fn::GetAtt: [CdkBootstrapVersion, Value] \ No newline at end of file From 4e14148f823eca39092def89c705e3d11cfbff4e Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:28:22 -0500 Subject: [PATCH 11/41] bootstrap scoped to environment level --- .../lib/actions/bootstrap/index.ts | 128 +++++++++++++----- .../toolkit-lib/lib/toolkit/toolkit.ts | 39 ++++-- .../toolkit-lib/lib/util/environments.ts | 24 ++++ .../test/actions/bootstrap.test.ts | 112 +++++++++++---- 4 files changed, 232 insertions(+), 71 deletions(-) create mode 100644 packages/@aws-cdk/toolkit-lib/lib/util/environments.ts diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts index 7d4064859..d63ef458f 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts @@ -1,32 +1,33 @@ import { Tag } from '../../api/aws-cdk'; /** - * Options for the bootstrapEnvironment operation(s) + * Options for Bootstrap */ export interface BootstrapOptions { + readonly parameters?: BootstrappingParameters; /** * The source of the bootstrap stack * - * @default - modern v2-style bootstrapping + * @default BootstrapSource.default() */ readonly source?: BootstrapSource; /** - * Whether to execute the changeset or only create it and leave it in review. + * Whether to execute the changeset or only create it and leave it in review * @default true */ readonly execute?: boolean; /** - * Tags for cdktoolkit stack. + * Tags for cdktoolkit stack * - * @default - None. + * @default [] */ readonly tags?: Tag[]; /** - * Whether the stacks created by the bootstrap process should be protected from termination. + * Whether the stacks created by the bootstrap process should be protected from termination * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-protect-stacks.html * @default true */ @@ -35,7 +36,7 @@ export interface BootstrapOptions { /** * Use previous values for unspecified parameters * - * If not set, all parameters must be specified for every deployment. + * If not set, all parameters must be specified for every deployment * * @default true */ @@ -43,68 +44,71 @@ export interface BootstrapOptions { } /** - * Parameters for the bootstrapping template + * Parameter values for the bootstrapping template */ -export interface BootstrappingParameters { +export interface BootstrappingParameterValues { /** - * The name to be given to the CDK Bootstrap bucket. + * The name to be given to the CDK Bootstrap bucket + * By default, a name is generated by CloudFormation * - * @default - a name is generated by CloudFormation. + * @default - No value, optional argument */ readonly bucketName?: string; /** - * The ID of an existing KMS key to be used for encrypting items in the bucket. + * The ID of an existing KMS key to be used for encrypting items in the bucket + * By default, the default KMS key is used * - * @default - use the default KMS key or create a custom one + * @default - No value, optional argument */ readonly kmsKeyId?: string; /** * Whether or not to create a new customer master key (CMK) * - * Only applies to modern bootstrapping. Legacy bootstrapping will never create - * a CMK, only use the default S3 key. + * Only applies to modern bootstrapping + * Legacy bootstrapping will never create a CMK, only use the default S3 key * * @default false */ readonly createCustomerMasterKey?: boolean; /** - * The list of AWS account IDs that are trusted to deploy into the environment being bootstrapped. + * 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 + * @default [] */ readonly trustedAccounts?: string[]; /** - * The list of AWS account IDs that are trusted to look up values in the environment being bootstrapped. + * The list of AWS account IDs that are trusted to look up values in the environment being bootstrapped * - * @default - only the bootstrapped account can look up values in this environment + * @default [] */ readonly trustedAccountsForLookup?: string[]; /** - * The list of AWS account IDs that should not be trusted by the bootstrapped environment. - * If these accounts are already trusted, they will be removed on bootstrapping. + * The list of AWS account IDs that should not be trusted by the bootstrapped environment + * If these accounts are already trusted, they will be removed on bootstrapping * - * @default - no account will be untrusted. + * @default [] */ readonly untrustedAccounts?: 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 `trustedAccounts` were passed. + * 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 `trustedAccounts` were passed * - * @default - the role will have no policies attached + * @default [] */ readonly cloudFormationExecutionPolicies?: string[]; /** * Identifier to distinguish multiple bootstrapped environments - * - * @default - Default qualifier + * The default qualifier is an arbitrary but unique string + * + * @default - 'hnb659fds' */ readonly qualifier?: string; @@ -130,23 +134,73 @@ export interface BootstrappingParameters { readonly customPermissionsBoundary?: string; } +/** + * Parameters for the bootstrapping template with flexible configuration options + */ +export class BootstrappingParameters { + /** + * Use default values for all parameters + */ + static default() { + return new BootstrappingParameters(); + } + + /** + * Use custom parameters and fall back to default values + */ + static custom(params: BootstrappingParameterValues) { + return new BootstrappingParameters(params); + } + + /** + * The parameters as a Map for easy access and manipulation + */ + private readonly parameters?: BootstrappingParameterValues; + + private constructor(params?: BootstrappingParameterValues) { + this.parameters = params; + } + + /** + * Render the parameters as a BootstrappingParameterValues object + * @returns A BootstrappingParameterValues object + */ + public render(): BootstrappingParameterValues { + return this.parameters ?? {}; + } +} + /** * Source configuration for bootstrap operations */ -export type BootstrapSource = { source: 'default' } | { source: 'custom'; templateFile: string }; +// export type BootstrapSource = { source: 'default' } | { source: 'custom'; templateFile: string }; -export const BootstrapSource = { +export class BootstrapSource { /** * Use the default bootstrap template */ - default(): BootstrapSource { - return { source: 'default' }; - }, + static default(): BootstrapSource { + return new BootstrapSource('default'); + } /** * Use a custom bootstrap template */ - customTemplate(templateFile: string): BootstrapSource { - return { source: 'custom', templateFile }; - }, -} as const; + static customTemplate(templateFile: string): BootstrapSource { + return new BootstrapSource('custom', templateFile); + } + + private readonly source: 'default' | 'custom'; + private readonly templateFile?: string; + private constructor(source: 'default' | 'custom', templateFile?: string) { + this.source = source; + this.templateFile = templateFile; + } + + public render() { + return { + source: this.source, + ...(this.templateFile ? { templateFile: this.templateFile } : {}), + } as { source: 'default' } | { source: 'custom'; templateFile: string }; + } +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 47fa854ed..ddcc223c0 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -4,7 +4,7 @@ import * as chalk from 'chalk'; import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; import { ToolkitServices } from './private'; -import { BootstrapOptions } from '../actions/bootstrap'; +import { BootstrapOptions, BootstrappingParameters, BootstrapSource } from '../actions/bootstrap'; import { AssetBuildTime, type DeployOptions, RequireApproval } from '../actions/deploy'; import { type ExtendedDeployOptions, buildParameterMap, createHotswapPropertyOverrides, removePublishedAssets } from '../actions/deploy/private'; import { type DestroyOptions } from '../actions/destroy'; @@ -22,6 +22,7 @@ import { ToolkitError } from '../api/errors'; import { IIoHost, IoMessageCode, IoMessageLevel } from '../api/io'; import { asSdkLogger, withAction, Timer, confirm, error, info, success, warn, ActionAwareIoHost, debug, result, withoutEmojis, withoutColor, withTrimmedWhitespace } from '../api/io/private'; import { pLimit } from '../util/concurrency'; +import { environmentsFromDescriptors } from '../util/environments'; /** * The current action being performed by the CLI. 'none' represents the absence of an action. @@ -158,17 +159,28 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab /** * Bootstrap Action */ - public async bootstrap(cx: ICloudAssemblySource, options: BootstrapOptions = {}): Promise { + public async bootstrap(cx: ICloudAssemblySource, userEnvironmentSpecs: string[], options: BootstrapOptions = {}): Promise { const ioHost = withAction(this.ioHost, 'bootstrap'); const assembly = await this.assemblyFromSource(cx); - const stackCollection = assembly.selectStacksV2({ - patterns: ['**'], - strategy: StackSelectionStrategy.ALL_STACKS, - }); - const bootstrapper = new Bootstrapper(options.source, { ioHost: withAction(this.ioHost, 'bootstrap'), action: 'bootstrap' }); + let environments; + if (userEnvironmentSpecs.length) { + environments = environmentsFromDescriptors(userEnvironmentSpecs); + } else { + // select all environments we can find, from stacks being bootstrapped + const stackCollection = assembly.selectStacksV2({ + patterns: ['**'], + strategy: StackSelectionStrategy.ALL_STACKS, + }); + environments = stackCollection.stackArtifacts.map(stack => stack.environment); + } + + const source = options.source ?? BootstrapSource.default(); + + const parameters = options.parameters ?? BootstrappingParameters.default(); + + const bootstrapper = new Bootstrapper(source.render(), { ioHost: withAction(this.ioHost, 'bootstrap'), action: 'bootstrap' }); - const environments = stackCollection.stackArtifacts.map(stack => stack.environment); const sdkProvider = await this.sdkProvider('bootstrap'); const limit = pLimit(20); @@ -179,7 +191,16 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab try { const { toolkitStackName } = this; - const bootstrapResult = await bootstrapper.bootstrapEnvironment(environment, sdkProvider, { ...options, toolkitStackName }); + const bootstrapResult = await bootstrapper.bootstrapEnvironment( + environment, + sdkProvider, + { + ...options, + toolkitStackName, + source: source.render(), + parameters: parameters.render(), + }, + ); const message = bootstrapResult.noOp ? ` ✅ ${environment.name} (no changes)` : ` ✅ ${environment.name}`; diff --git a/packages/@aws-cdk/toolkit-lib/lib/util/environments.ts b/packages/@aws-cdk/toolkit-lib/lib/util/environments.ts new file mode 100644 index 000000000..5138c26b2 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/util/environments.ts @@ -0,0 +1,24 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import { ToolkitError } from '../api/errors'; + +/** + * Given a set of "/" strings, construct environments for them + */ +export function environmentsFromDescriptors(envSpecs: string[]): cxapi.Environment[] { + const ret = new Array(); + + for (const spec of envSpecs) { + const parts = spec.replace(/^aws:\/\//, '').split('/'); + if (parts.length !== 2) { + throw new ToolkitError(`Expected environment name in format 'aws:///', got: ${spec}`); + } + + ret.push({ + name: spec, + account: parts[0], + region: parts[1], + }); + } + + return ret; +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index 24b046219..6b7a779a7 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -6,11 +6,13 @@ import { ExecuteChangeSetCommand, Stack, } from '@aws-sdk/client-cloudformation'; +import { bold } from 'chalk'; import { BootstrapSource } from '../../lib/actions/bootstrap'; import { Toolkit } from '../../lib/toolkit'; import { TestIoHost, builderFixture } from '../_helpers'; import { MockSdkProvider, + MockSdk, SdkProvider, mockCloudFormationClient, path, @@ -76,9 +78,9 @@ function createMockStack(outputs: { OutputKey: string; OutputValue: string }[]): } as Stack; } -async function runBootstrap(options?: { source?: BootstrapSource }) { +async function runBootstrap(options?: { environments?: string[]; source?: BootstrapSource }) { const cx = await builderFixture(toolkit, 'stack-with-asset'); - return toolkit.bootstrap(cx, options); + return toolkit.bootstrap(cx, options?.environments ?? [], { source: options?.source }); } function expectSuccessfulBootstrap() { @@ -92,8 +94,51 @@ function expectSuccessfulBootstrap() { } describe('bootstrap', () => { - describe('template sources', () => { - test('uses default template when no source is specified', async () => { + describe('with user-specified environments', () => { + let originalSdk = mockSdkProvider.forEnvironment; + beforeEach(() => { + const mockForEnvironment = jest.fn().mockImplementation(() => { + return { sdk: new MockSdk() }; + }); + mockSdkProvider.forEnvironment = mockForEnvironment; + }); + + afterAll(() => { + mockSdkProvider.forEnvironment = originalSdk; + }); + + test('bootstraps specified environments', async () => { + // GIVEN + const mockStack1 = createMockStack([ + { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME_1' }, + { OutputKey: 'BucketDomainName', OutputValue: 'BUCKET_ENDPOINT_1' }, + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, + ]); + const mockStack2 = createMockStack([ + { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME_2' }, + { OutputKey: 'BucketDomainName', OutputValue: 'BUCKET_ENDPOINT_2' }, + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, + ]); + setupMockCloudFormationClient(mockStack1); + setupMockCloudFormationClient(mockStack2); + + // WHEN + await runBootstrap({ environments: ['aws://123456789012/us-east-1', 'aws://210987654321/eu-west-1'] }); + + // THEN + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining(`${bold('aws://123456789012/us-east-1')}: bootstrapping...`), + })); + + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining(`${bold('aws://210987654321/eu-west-1')}: bootstrapping...`), + })); + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('✅'), + })); + }); + + test('handles errors in user-specified environments', async () => { // GIVEN const mockStack = createMockStack([ { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME' }, @@ -102,38 +147,58 @@ describe('bootstrap', () => { ]); setupMockCloudFormationClient(mockStack); - // WHEN - await runBootstrap(); + // Mock an access denied error + const accessDeniedError = new Error('Access Denied'); + accessDeniedError.name = 'AccessDeniedException'; + mockCloudFormationClient + .on(CreateChangeSetCommand) + .rejects(accessDeniedError); - // THEN - expectSuccessfulBootstrap(); + // WHEN/THEN + await expect(runBootstrap({ environments: ['aws://123456789012/us-east-1'] })) + .rejects.toThrow('Access Denied'); + + // Get all error notifications + const errorCalls = ioHost.notifySpy.mock.calls + .filter(call => call[0].level === 'error') + .map(call => call[0]); + + // Verify error notifications + expect(errorCalls).toContainEqual(expect.objectContaining({ + level: 'error', + message: expect.stringContaining('❌'), + })); + expect(errorCalls).toContainEqual(expect.objectContaining({ + level: 'error', + message: expect.stringContaining(`${bold('aws://123456789012/us-east-1')} failed: Access Denied`), + })); }); - test('uses custom template when specified (old api)', async () => { + test('throws error for invalid environment format', async () => { + // WHEN/THEN + await expect(runBootstrap({ environments: ['invalid-format'] })) + .rejects.toThrow('Expected environment name in format \'aws:///\', got: invalid-format'); + }); + }); + + describe('template sources', () => { + test('uses default template when no source is specified', async () => { // GIVEN const mockStack = createMockStack([ - { OutputKey: 'BucketName', OutputValue: 'CUSTOM_BUCKET_NAME' }, - { OutputKey: 'BucketDomainName', OutputValue: 'CUSTOM_BUCKET_ENDPOINT' }, + { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME' }, + { OutputKey: 'BucketDomainName', OutputValue: 'BUCKET_ENDPOINT' }, { OutputKey: 'BootstrapVersion', OutputValue: '1' }, ]); setupMockCloudFormationClient(mockStack); // WHEN - - await runBootstrap({ - source: { - source: 'custom', - templateFile: path.join(rootDir(), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml'), - }, - }); + await runBootstrap(); // THEN - const createChangeSetCalls = mockCloudFormationClient.calls().filter(call => call.args[0] instanceof CreateChangeSetCommand); - expect(createChangeSetCalls.length).toBeGreaterThan(0); expectSuccessfulBootstrap(); }); - test('uses custom template when specified (new api)', async () => { + test('uses custom template when specified', async () => { // GIVEN const mockStack = createMockStack([ { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME' }, @@ -162,10 +227,7 @@ describe('bootstrap', () => { // WHEN await expect(runBootstrap({ - source: { - source: 'custom', - templateFile: path.join(rootDir(), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml'), - }, + source: BootstrapSource.customTemplate(path.join(rootDir(), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml')), })).rejects.toThrow('Invalid template file'); // THEN From 52c91542814b0918b790e36aad3e1c7fd554cb2b Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:49:55 -0500 Subject: [PATCH 12/41] remove extra comment --- packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts index d63ef458f..41d6fb9a3 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts @@ -107,7 +107,7 @@ export interface BootstrappingParameterValues { /** * Identifier to distinguish multiple bootstrapped environments * The default qualifier is an arbitrary but unique string - * + * * @default - 'hnb659fds' */ readonly qualifier?: string; @@ -173,8 +173,6 @@ export class BootstrappingParameters { /** * Source configuration for bootstrap operations */ -// export type BootstrapSource = { source: 'default' } | { source: 'custom'; templateFile: string }; - export class BootstrapSource { /** * Use the default bootstrap template From e48ae89a8d449523c11ba497aef640da8ecd31b1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 20 Feb 2025 17:27:58 +0000 Subject: [PATCH 13/41] chore: self mutation Signed-off-by: github-actions --- packages/@aws-cdk/toolkit-lib/lib/util/environments.ts | 2 +- packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/util/environments.ts b/packages/@aws-cdk/toolkit-lib/lib/util/environments.ts index 5138c26b2..9cd269180 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/util/environments.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/util/environments.ts @@ -21,4 +21,4 @@ export function environmentsFromDescriptors(envSpecs: string[]): cxapi.Environme } return ret; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts index 03ed33471..c2d6835af 100644 --- a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts @@ -43,4 +43,4 @@ export { SdkProvider, Bootstrapper, rootDir, -}; \ No newline at end of file +}; From 71e8b71d66301c564e4fe623d08acc7e6181d329 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:54:32 -0500 Subject: [PATCH 14/41] Simple refactoring, removed usage of deprecated .success() method, used .result() instead --- .projenrc.ts | 4 +-- .../@aws-cdk/toolkit-lib/.projen/tasks.json | 2 +- .../bootstrap/private/helpers.ts} | 2 +- .../lib/actions/bootstrap/private/index.ts | 1 + .../@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts | 2 +- .../toolkit-lib/lib/api/io/private/codes.ts | 1 + .../toolkit-lib/lib/toolkit/toolkit.ts | 33 +++++++---------- .../test/actions/bootstrap.test.ts | 24 +++++++++++-- .../@aws-cdk/toolkit-lib/test/util/aws-cdk.ts | 36 +------------------ 9 files changed, 42 insertions(+), 63 deletions(-) rename packages/@aws-cdk/toolkit-lib/lib/{util/environments.ts => actions/bootstrap/private/helpers.ts} (92%) create mode 100644 packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts diff --git a/.projenrc.ts b/.projenrc.ts index 3982339fd..c02e09d39 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1139,8 +1139,8 @@ toolkitLib.package.addField('exports', { }); toolkitLib.postCompileTask.exec('node build-tools/bundle.mjs'); -// Copy bootstrap template from cli-lib-alpha -toolkitLib.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../cli-lib-alpha/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/ && cp ../cli-lib-alpha/lib/api/bootstrap/custom-bootstrap-template.yaml ./lib/api/bootstrap/'); +// Copy bootstrap template from aws-cdk +toolkitLib.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml ./lib/api/bootstrap/'); // Smoke test built JS files toolkitLib.postCompileTask.exec('node ./lib/index.js >/dev/null 2>/dev/null /dev/null 2>/dev/null /dev/null 2>/dev/null /" strings, construct environments for them diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts new file mode 100644 index 000000000..c5f595cf9 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/index.ts @@ -0,0 +1 @@ +export * from './helpers'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts index fd9ba3727..71ceb941a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts @@ -5,7 +5,7 @@ export { formatSdkLoggerContent, SdkProvider } from '../../../../aws-cdk/lib/api export { Context, PROJECT_CONTEXT } from '../../../../aws-cdk/lib/api/context'; export { Deployments, type SuccessfulDeployStackResult } from '../../../../aws-cdk/lib/api/deployments'; export { Settings } from '../../../../aws-cdk/lib/api/settings'; -export { Tag, tagsForStack } from '../../../../aws-cdk/lib/api/tags'; +export { type Tag, tagsForStack } from '../../../../aws-cdk/lib/api/tags'; export { DEFAULT_TOOLKIT_STACK_NAME } from '../../../../aws-cdk/lib/api/toolkit-info'; export { ResourceMigrator } from '../../../../aws-cdk/lib/api/resource-import'; export { StackActivityProgress } from '../../../../aws-cdk/lib/api/stack-events'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts index 106e7f34d..b4c26bf69 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts @@ -47,6 +47,7 @@ export const CODES = { // 9: Bootstrap CDK_TOOLKIT_I9000: 'Provides bootstrap times', CDK_TOOLKIT_E9900: 'Bootstrap failed', + CDK_TOOLKIT_I9900: 'Bootstrap results on success', // Assembly codes CDK_ASSEMBLY_I0042: 'Writing updated context', diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index ddcc223c0..7f1308794 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -7,6 +7,7 @@ import { ToolkitServices } from './private'; import { BootstrapOptions, BootstrappingParameters, BootstrapSource } from '../actions/bootstrap'; import { AssetBuildTime, type DeployOptions, RequireApproval } from '../actions/deploy'; import { type ExtendedDeployOptions, buildParameterMap, createHotswapPropertyOverrides, removePublishedAssets } from '../actions/deploy/private'; +import { environmentsFromDescriptors } from '../actions/bootstrap/private'; import { type DestroyOptions } from '../actions/destroy'; import { type DiffOptions } from '../actions/diff'; import { diffRequiresApproval } from '../actions/diff/private'; @@ -22,7 +23,6 @@ import { ToolkitError } from '../api/errors'; import { IIoHost, IoMessageCode, IoMessageLevel } from '../api/io'; import { asSdkLogger, withAction, Timer, confirm, error, info, success, warn, ActionAwareIoHost, debug, result, withoutEmojis, withoutColor, withTrimmedWhitespace } from '../api/io/private'; import { pLimit } from '../util/concurrency'; -import { environmentsFromDescriptors } from '../util/environments'; /** * The current action being performed by the CLI. 'none' represents the absence of an action. @@ -159,33 +159,30 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab /** * Bootstrap Action */ - public async bootstrap(cx: ICloudAssemblySource, userEnvironmentSpecs: string[], options: BootstrapOptions = {}): Promise { + public async bootstrap(cx: ICloudAssemblySource, environments: string[], options: BootstrapOptions = {}): Promise { const ioHost = withAction(this.ioHost, 'bootstrap'); - const assembly = await this.assemblyFromSource(cx); - let environments; - if (userEnvironmentSpecs.length) { - environments = environmentsFromDescriptors(userEnvironmentSpecs); + let bootstrapEnvironments; + if (environments.length) { + bootstrapEnvironments = environmentsFromDescriptors(environments); } else { // select all environments we can find, from stacks being bootstrapped - const stackCollection = assembly.selectStacksV2({ - patterns: ['**'], - strategy: StackSelectionStrategy.ALL_STACKS, - }); - environments = stackCollection.stackArtifacts.map(stack => stack.environment); + const assembly = await this.assemblyFromSource(cx); + const stackCollection = assembly.selectStacksV2(ALL_STACKS); + bootstrapEnvironments = stackCollection.stackArtifacts.map(stack => stack.environment); } const source = options.source ?? BootstrapSource.default(); const parameters = options.parameters ?? BootstrappingParameters.default(); - const bootstrapper = new Bootstrapper(source.render(), { ioHost: withAction(this.ioHost, 'bootstrap'), action: 'bootstrap' }); + const bootstrapper = new Bootstrapper(source.render(), { ioHost, action: 'bootstrap' }); const sdkProvider = await this.sdkProvider('bootstrap'); const limit = pLimit(20); // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism - await Promise.all(environments.map((environment: cxapi.Environment) => limit(async () => { + await Promise.all(bootstrapEnvironments.map((environment: cxapi.Environment) => limit(async () => { await ioHost.notify(info(`${chalk.bold(environment.name)}: bootstrapping...`)); const bootstrapTimer = Timer.start(); @@ -205,15 +202,11 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab ? ` ✅ ${environment.name} (no changes)` : ` ✅ ${environment.name}`; - await ioHost.notify(success('\n' + message)); + console.log(result(chalk.green('\n' + message), 'CDK_TOOLKIT_I9900', { environment })); + await ioHost.notify(result(chalk.green('\n' + message), 'CDK_TOOLKIT_I9900', { environment })); await bootstrapTimer.endAs(ioHost, 'bootstrap'); } catch (e) { - await ioHost.notify({ - time: new Date(), - level: 'error', - code: 'CDK_TOOLKIT_E9900', - message: `\n ❌ ${chalk.bold(environment.name)} failed: ${formatErrorMessage(e)}`, - }); + await ioHost.notify(error(`\n ❌ ${chalk.bold(environment.name)} failed: ${formatErrorMessage(e)}`, 'CDK_TOOLKIT_E9900')); throw e; } }))); diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index 6b7a779a7..c4fb69a9e 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -1,3 +1,4 @@ +import * as path from 'node:path'; import { CreateChangeSetCommand, DeleteChangeSetCommand, @@ -8,17 +9,15 @@ import { } from '@aws-sdk/client-cloudformation'; import { bold } from 'chalk'; import { BootstrapSource } from '../../lib/actions/bootstrap'; +import { SdkProvider, rootDir } from '../../lib/api/aws-cdk'; import { Toolkit } from '../../lib/toolkit'; import { TestIoHost, builderFixture } from '../_helpers'; import { MockSdkProvider, MockSdk, - SdkProvider, mockCloudFormationClient, - path, restoreSdkMocksToDefault, setDefaultSTSMocks, - rootDir, } from '../util/aws-cdk'; const ioHost = new TestIoHost(); @@ -134,7 +133,26 @@ describe('bootstrap', () => { message: expect.stringContaining(`${bold('aws://210987654321/eu-west-1')}: bootstrapping...`), })); expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + code: 'CDK_TOOLKIT_I9900', message: expect.stringContaining('✅'), + data: expect.objectContaining({ + environment: { + name: 'aws://123456789012/us-east-1', + account: '123456789012', + region: 'us-east-1' + } + }), + })); + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + code: 'CDK_TOOLKIT_I9900', + message: expect.stringContaining('✅'), + data: expect.objectContaining({ + environment: { + name: 'aws://210987654321/eu-west-1', + account: '210987654321', + region: 'eu-west-1' + } + }), })); }); diff --git a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts index c2d6835af..d7020bad6 100644 --- a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts @@ -1,35 +1,6 @@ /* eslint-disable import/no-restricted-paths */ -// Node.js built-in modules -import * as os from 'node:os'; -import * as path from 'node:path'; - -// Third-party modules -import * as cxapi from '@aws-cdk/cx-api'; -import * as fs from 'fs-extra'; - // Local modules -import { - MockSdk, - MockSdkProvider, - mockCloudFormationClient, - mockS3Client, - mockSTSClient, - setDefaultSTSMocks, - restoreSdkMocksToDefault, -} from '../../../../aws-cdk/test/util/mock-sdk'; - -import { - SdkProvider, - Bootstrapper, - rootDir, -} from '../../lib/api/aws-cdk'; - -// Re-exports -export { path }; -export { os }; -export { cxapi }; -export { fs }; export { MockSdk, MockSdkProvider, @@ -38,9 +9,4 @@ export { mockSTSClient, setDefaultSTSMocks, restoreSdkMocksToDefault, -}; -export { - SdkProvider, - Bootstrapper, - rootDir, -}; +} from '../../../../aws-cdk/test/util/mock-sdk'; From 9e43a8f4636b402e8f5de87adbecf9b04ac03af0 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:04:13 -0500 Subject: [PATCH 15/41] remove unnecessary files from cli-lib-alpha --- .../@aws-cdk/cli-lib-alpha/.projen/tasks.json | 2 +- .../bootstrap/custom-bootstrap-template.yaml | 99 ------------------- 2 files changed, 1 insertion(+), 100 deletions(-) delete mode 100644 packages/@aws-cdk/cli-lib-alpha/lib/api/bootstrap/custom-bootstrap-template.yaml diff --git a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json index fbd365e1b..2ad251abc 100644 --- a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json +++ b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json @@ -241,7 +241,7 @@ "exec": "node-bundle validate --external=fsevents:optional --entrypoint=lib/index.js --fix --dont-attribute \"^@aws-cdk/|^cdk-assets$|^cdk-cli-wrapper$|^aws-cdk$\"" }, { - "exec": "mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml ./lib/api/bootstrap/" + "exec": "mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/" }, { "exec": "cp $(node -p 'require.resolve(\"cdk-from-cfn/index_bg.wasm\")') ./lib/" diff --git a/packages/@aws-cdk/cli-lib-alpha/lib/api/bootstrap/custom-bootstrap-template.yaml b/packages/@aws-cdk/cli-lib-alpha/lib/api/bootstrap/custom-bootstrap-template.yaml deleted file mode 100644 index c5f5dff12..000000000 --- a/packages/@aws-cdk/cli-lib-alpha/lib/api/bootstrap/custom-bootstrap-template.yaml +++ /dev/null @@ -1,99 +0,0 @@ -Description: Custom CDK Bootstrap Template - -Parameters: - Qualifier: - Type: String - Description: Qualifier for the bootstrap resources - Default: custom - - CustomTagKey: - Type: String - Description: Key for a custom tag to apply to all resources - Default: Environment - - CustomTagValue: - Type: String - Description: Value for the custom tag - Default: Development - -Resources: - StagingBucket: - Type: AWS::S3::Bucket - Properties: - BucketName: - Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region} - VersioningConfiguration: - Status: Enabled - LifecycleConfiguration: - Rules: - - Id: DeleteOldVersions - Status: Enabled - NoncurrentVersionExpiration: - NoncurrentDays: 90 - Tags: - - Key: - Ref: CustomTagKey - Value: - Ref: CustomTagValue - - ContainerAssetsRepository: - Type: AWS::ECR::Repository - Properties: - RepositoryName: - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} - ImageScanningConfiguration: - ScanOnPush: true - Tags: - - Key: - Ref: CustomTagKey - Value: - Ref: CustomTagValue - - CloudFormationExecutionRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: cloudformation.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess - RoleName: - Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} - Tags: - - Key: - Ref: CustomTagKey - Value: - Ref: CustomTagValue - - CdkBootstrapVersion: - Type: AWS::SSM::Parameter - Properties: - Type: String - Name: - Fn::Sub: /cdk-bootstrap/${Qualifier}/version - Value: '1' - -Outputs: - BucketName: - Description: The name of the S3 bucket for CDK assets - Value: - Ref: StagingBucket - - RepositoryName: - Description: The name of the ECR repository for container assets - Value: - Ref: ContainerAssetsRepository - - CloudFormationExecutionRoleArn: - Description: The ARN of the CloudFormation execution role - Value: - Fn::GetAtt: [CloudFormationExecutionRole, Arn] - - BootstrapVersion: - Description: The version of this bootstrap stack - Value: - Fn::GetAtt: [CdkBootstrapVersion, Value] \ No newline at end of file From b7f757f3f9be0834633328acf18c47c334fa22b8 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:17:01 -0500 Subject: [PATCH 16/41] copy bootstrap templates at build time instead of as a post-compile step --- .projenrc.ts | 3 --- packages/@aws-cdk/toolkit-lib/.projen/tasks.json | 3 --- packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs | 8 +++++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.projenrc.ts b/.projenrc.ts index c02e09d39..eb3181605 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -982,7 +982,6 @@ new JsiiBuild(cliLib, { // clilib needs to bundle some resources, same as the CLI cliLib.postCompileTask.exec('node-bundle validate --external=fsevents:optional --entrypoint=lib/index.js --fix --dont-attribute "^@aws-cdk/|^cdk-assets$|^cdk-cli-wrapper$|^aws-cdk$"'); -cliLib.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml ./lib/api/bootstrap/'); for (const resourceCommand of includeCliResourcesCommands) { cliLib.postCompileTask.exec(resourceCommand); } @@ -1139,8 +1138,6 @@ toolkitLib.package.addField('exports', { }); toolkitLib.postCompileTask.exec('node build-tools/bundle.mjs'); -// Copy bootstrap template from aws-cdk -toolkitLib.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml ./lib/api/bootstrap/'); // Smoke test built JS files toolkitLib.postCompileTask.exec('node ./lib/index.js >/dev/null 2>/dev/null /dev/null 2>/dev/null /dev/null 2>/dev/null Date: Fri, 21 Feb 2025 14:20:14 -0500 Subject: [PATCH 17/41] BootstrapEnvironment class enables running bootstrap with either cx or environment strings --- .../@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts | 1 - .../toolkit-lib/lib/toolkit/toolkit.ts | 107 ++++++++++++------ .../test/actions/bootstrap.test.ts | 9 +- .../@aws-cdk/toolkit-lib/test/util/aws-cdk.ts | 2 + 4 files changed, 79 insertions(+), 40 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts index 71ceb941a..cabb30bb3 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts @@ -30,7 +30,6 @@ export { RWLock, type ILock } from '../../../../aws-cdk/lib/api/util/rwlock'; // @todo Not yet API probably should be export { loadTree, some } from '../../../../aws-cdk/lib/tree'; -export { rootDir } from '../../../../aws-cdk/lib/util/directories'; // @todo Cloud Assembly and Executable - this is a messy API right now export { CloudAssembly, sanitizePatterns, type StackDetails, StackCollection, ExtendedStackSelection } from '../../../../aws-cdk/lib/api/cxapp/cloud-assembly'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 7f1308794..592a8d21e 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -87,6 +87,56 @@ export interface ToolkitOptions { assemblyFailureAt?: 'error' | 'warn' | 'none'; } +/** + * Creates a Toolkit internal CloudAssembly from a CloudAssemblySource. + * @param assemblySource the source for the cloud assembly + * @param cache if the assembly should be cached, default: `true` + * @returns the CloudAssembly object + */ +async function assemblyFromSource(assemblySource: ICloudAssemblySource, cache: boolean = true): Promise { + if (assemblySource instanceof StackAssembly) { + return assemblySource; + } + + if (cache) { + return new StackAssembly(await new CachedCloudAssemblySource(assemblySource).produce()); + } + + return new StackAssembly(await assemblySource.produce()); +} + +/** + * Helper class to manage bootstrap environments + */ +class BootstrapEnvironments { + /** + * Create from a list of environment descriptors + */ + static fromList(environments: string[]): BootstrapEnvironments { + return new BootstrapEnvironments(environmentsFromDescriptors(environments)); + } + + /** + * Create from a cloud assembly source + */ + static fromCloudAssemblySource(cx: ICloudAssemblySource): BootstrapEnvironments { + return new BootstrapEnvironments(async () => { + const assembly = await assemblyFromSource(cx); + const stackCollection = assembly.selectStacksV2(ALL_STACKS); + return stackCollection.stackArtifacts.map(stack => stack.environment); + }); + } + + private constructor(private readonly envProvider: cxapi.Environment[] | (() => Promise)) {} + + async getEnvironments(): Promise { + if (Array.isArray(this.envProvider)) { + return this.envProvider; + } + return this.envProvider(); + } +} + /** * The AWS CDK Programmatic Toolkit */ @@ -159,25 +209,26 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab /** * Bootstrap Action */ - public async bootstrap(cx: ICloudAssemblySource, environments: string[], options: BootstrapOptions = {}): Promise { + public async bootstrap(options: BootstrapOptions & { + environments?: string[]; + cloudAssembly?: ICloudAssemblySource; + }): Promise { const ioHost = withAction(this.ioHost, 'bootstrap'); - let bootstrapEnvironments; - if (environments.length) { - bootstrapEnvironments = environmentsFromDescriptors(environments); - } else { - // select all environments we can find, from stacks being bootstrapped - const assembly = await this.assemblyFromSource(cx); - const stackCollection = assembly.selectStacksV2(ALL_STACKS); - bootstrapEnvironments = stackCollection.stackArtifacts.map(stack => stack.environment); + const bootstrapEnvs = options.environments + ? BootstrapEnvironments.fromList(options.environments) + : options.cloudAssembly + ? BootstrapEnvironments.fromCloudAssemblySource(options.cloudAssembly) + : undefined; + + if (!bootstrapEnvs) { + throw new ToolkitError('Either environments or cloudAssembly must be provided'); } + const bootstrapEnvironments = await bootstrapEnvs.getEnvironments(); const source = options.source ?? BootstrapSource.default(); - const parameters = options.parameters ?? BootstrappingParameters.default(); - const bootstrapper = new Bootstrapper(source.render(), { ioHost, action: 'bootstrap' }); - const sdkProvider = await this.sdkProvider('bootstrap'); const limit = pLimit(20); @@ -218,7 +269,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab public async synth(cx: ICloudAssemblySource, options: SynthOptions = {}): Promise { const ioHost = withAction(this.ioHost, 'synth'); const synthTimer = Timer.start(); - const assembly = await this.assemblyFromSource(cx); + const assembly = await assemblyFromSource(cx); const stacks = assembly.selectStacksV2(options.stacks ?? ALL_STACKS); const autoValidateStacks = options.validateStacks ? [assembly.selectStacksForValidation()] : []; await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), ioHost); @@ -263,7 +314,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab public async list(cx: ICloudAssemblySource, options: ListOptions = {}): Promise { const ioHost = withAction(this.ioHost, 'list'); const synthTimer = Timer.start(); - const assembly = await this.assemblyFromSource(cx); + const assembly = await assemblyFromSource(cx); const stackCollection = await assembly.selectStacksV2(options.stacks ?? ALL_STACKS); await synthTimer.endAs(ioHost, 'synth'); @@ -281,7 +332,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab */ public async diff(cx: ICloudAssemblySource, options: DiffOptions): Promise { const ioHost = withAction(this.ioHost, 'diff'); - const assembly = await this.assemblyFromSource(cx); + const assembly = await assemblyFromSource(cx); const stacks = await assembly.selectStacksV2(options.stacks); await this.validateStacksMetadata(stacks, ioHost); // temporary @@ -295,7 +346,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab * Deploys the selected stacks into an AWS account */ public async deploy(cx: ICloudAssemblySource, options: DeployOptions = {}): Promise { - const assembly = await this.assemblyFromSource(cx); + const assembly = await assemblyFromSource(cx); return this._deploy(assembly, 'deploy', options); } @@ -604,7 +655,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab * Implies hotswap deployments. */ public async watch(cx: ICloudAssemblySource, options: WatchOptions): Promise { - const assembly = await this.assemblyFromSource(cx, false); + const assembly = await assemblyFromSource(cx, false); const ioHost = withAction(this.ioHost, 'watch'); const rootDir = options.watchDir ?? process.cwd(); await ioHost.notify(debug(`root directory used for 'watch' is: ${rootDir}`)); @@ -706,7 +757,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab * Rolls back the selected stacks. */ public async rollback(cx: ICloudAssemblySource, options: RollbackOptions): Promise { - const assembly = await this.assemblyFromSource(cx); + const assembly = await assemblyFromSource(cx); return this._rollback(assembly, 'rollback', options); } @@ -760,7 +811,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab * Destroys the selected Stacks. */ public async destroy(cx: ICloudAssemblySource, options: DestroyOptions): Promise { - const assembly = await this.assemblyFromSource(cx); + const assembly = await assemblyFromSource(cx); return this._destroy(assembly, 'destroy', options); } @@ -825,24 +876,6 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab })); } - /** - * Creates a Toolkit internal CloudAssembly from a CloudAssemblySource. - * @param assemblySource the source for the cloud assembly - * @param cache if the assembly should be cached, default: `true` - * @returns the CloudAssembly object - */ - private async assemblyFromSource(assemblySource: ICloudAssemblySource, cache: boolean = true): Promise { - if (assemblySource instanceof StackAssembly) { - return assemblySource; - } - - if (cache) { - return new StackAssembly(await new CachedCloudAssemblySource(assemblySource).produce()); - } - - return new StackAssembly(await assemblySource.produce()); - } - /** * Create a deployments class */ diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index c4fb69a9e..5fdedf0c3 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -9,7 +9,7 @@ import { } from '@aws-sdk/client-cloudformation'; import { bold } from 'chalk'; import { BootstrapSource } from '../../lib/actions/bootstrap'; -import { SdkProvider, rootDir } from '../../lib/api/aws-cdk'; +import { SdkProvider } from '../../lib/api/aws-cdk'; import { Toolkit } from '../../lib/toolkit'; import { TestIoHost, builderFixture } from '../_helpers'; import { @@ -18,6 +18,7 @@ import { mockCloudFormationClient, restoreSdkMocksToDefault, setDefaultSTSMocks, + rootDir, } from '../util/aws-cdk'; const ioHost = new TestIoHost(); @@ -79,7 +80,11 @@ function createMockStack(outputs: { OutputKey: string; OutputValue: string }[]): async function runBootstrap(options?: { environments?: string[]; source?: BootstrapSource }) { const cx = await builderFixture(toolkit, 'stack-with-asset'); - return toolkit.bootstrap(cx, options?.environments ?? [], { source: options?.source }); + return toolkit.bootstrap({ + // if environment strings are not provided, get the environments from the cloud assembly + ...(options?.environments?.length ? { environments: options.environments } : { cloudAssembly: cx }), + ...(options?.source && { source: options.source } ) + }); } function expectSuccessfulBootstrap() { diff --git a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts index d7020bad6..a2a475d7e 100644 --- a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts @@ -10,3 +10,5 @@ export { setDefaultSTSMocks, restoreSdkMocksToDefault, } from '../../../../aws-cdk/test/util/mock-sdk'; + +export { rootDir } from '../../../../aws-cdk/lib/util/directories'; \ No newline at end of file From 2fcf944442b133e1d0e74673b6fbe7cc4239f011 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:28:55 -0500 Subject: [PATCH 18/41] some renaming --- .../toolkit-lib/lib/actions/bootstrap/index.ts | 18 +++++++++--------- .../toolkit-lib/lib/toolkit/toolkit.ts | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts index 41d6fb9a3..b15a4de95 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts @@ -4,7 +4,7 @@ import { Tag } from '../../api/aws-cdk'; * Options for Bootstrap */ export interface BootstrapOptions { - readonly parameters?: BootstrappingParameters; + readonly parameters?: BootstrapEnvironmentParameters; /** * The source of the bootstrap stack @@ -46,7 +46,7 @@ export interface BootstrapOptions { /** * Parameter values for the bootstrapping template */ -export interface BootstrappingParameterValues { +export interface BootstrapParameterValues { /** * The name to be given to the CDK Bootstrap bucket * By default, a name is generated by CloudFormation @@ -137,27 +137,27 @@ export interface BootstrappingParameterValues { /** * Parameters for the bootstrapping template with flexible configuration options */ -export class BootstrappingParameters { +export class BootstrapEnvironmentParameters { /** * Use default values for all parameters */ static default() { - return new BootstrappingParameters(); + return new BootstrapEnvironmentParameters(); } /** * Use custom parameters and fall back to default values */ - static custom(params: BootstrappingParameterValues) { - return new BootstrappingParameters(params); + static custom(params: BootstrapParameterValues) { + return new BootstrapEnvironmentParameters(params); } /** * The parameters as a Map for easy access and manipulation */ - private readonly parameters?: BootstrappingParameterValues; + private readonly parameters?: BootstrapParameterValues; - private constructor(params?: BootstrappingParameterValues) { + private constructor(params?: BootstrapParameterValues) { this.parameters = params; } @@ -165,7 +165,7 @@ export class BootstrappingParameters { * Render the parameters as a BootstrappingParameterValues object * @returns A BootstrappingParameterValues object */ - public render(): BootstrappingParameterValues { + public render(): BootstrapParameterValues { return this.parameters ?? {}; } } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index a157a874c..f7e12bf11 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -4,7 +4,7 @@ import * as chalk from 'chalk'; import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; import { ToolkitServices } from './private'; -import { BootstrapOptions, BootstrappingParameters, BootstrapSource } from '../actions/bootstrap'; +import { BootstrapOptions, BootstrapEnvironmentParameters, BootstrapSource } from '../actions/bootstrap'; import { AssetBuildTime, type DeployOptions, RequireApproval } from '../actions/deploy'; import { type ExtendedDeployOptions, buildParameterMap, createHotswapPropertyOverrides, removePublishedAssets } from '../actions/deploy/private'; import { environmentsFromDescriptors } from '../actions/bootstrap/private'; @@ -227,7 +227,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const bootstrapEnvironments = await bootstrapEnvs.getEnvironments(); const source = options.source ?? BootstrapSource.default(); - const parameters = options.parameters ?? BootstrappingParameters.default(); + const parameters = options.parameters ?? BootstrapEnvironmentParameters.default(); const bootstrapper = new Bootstrapper(source.render(), { ioHost, action: 'bootstrap' }); const sdkProvider = await this.sdkProvider('bootstrap'); const limit = pLimit(20); From e454a863a45f13c46658c68f12d45a3873c86835 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:49:38 -0500 Subject: [PATCH 19/41] remove console log --- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index f7e12bf11..938771e2d 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -253,7 +253,6 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab ? ` ✅ ${environment.name} (no changes)` : ` ✅ ${environment.name}`; - console.log(result(chalk.green('\n' + message), 'CDK_TOOLKIT_I9900', { environment })); await ioHost.notify(result(chalk.green('\n' + message), 'CDK_TOOLKIT_I9900', { environment })); await bootstrapTimer.endAs(ioHost, 'bootstrap'); } catch (e) { From 792f145fb9e86dd0add7bf842d0037390273a91c Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 21 Feb 2025 20:02:45 +0000 Subject: [PATCH 20/41] chore: self mutation Signed-off-by: github-actions --- packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json | 3 --- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 9 +++++---- .../toolkit-lib/test/actions/bootstrap.test.ts | 10 +++++----- packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts | 2 +- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json index 2ad251abc..1e7224b1e 100644 --- a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json +++ b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json @@ -240,9 +240,6 @@ { "exec": "node-bundle validate --external=fsevents:optional --entrypoint=lib/index.js --fix --dont-attribute \"^@aws-cdk/|^cdk-assets$|^cdk-cli-wrapper$|^aws-cdk$\"" }, - { - "exec": "mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/" - }, { "exec": "cp $(node -p 'require.resolve(\"cdk-from-cfn/index_bg.wasm\")') ./lib/" }, diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 938771e2d..654406cac 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -5,9 +5,9 @@ import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; import { ToolkitServices } from './private'; import { BootstrapOptions, BootstrapEnvironmentParameters, BootstrapSource } from '../actions/bootstrap'; +import { environmentsFromDescriptors } from '../actions/bootstrap/private'; import { AssetBuildTime, type DeployOptions, RequireApproval } from '../actions/deploy'; import { type ExtendedDeployOptions, buildParameterMap, createHotswapPropertyOverrides, removePublishedAssets } from '../actions/deploy/private'; -import { environmentsFromDescriptors } from '../actions/bootstrap/private'; import { type DestroyOptions } from '../actions/destroy'; import { type DiffOptions } from '../actions/diff'; import { diffRequiresApproval } from '../actions/diff/private'; @@ -127,7 +127,8 @@ class BootstrapEnvironments { }); } - private constructor(private readonly envProvider: cxapi.Environment[] | (() => Promise)) {} + private constructor(private readonly envProvider: cxapi.Environment[] | (() => Promise)) { + } async getEnvironments(): Promise { if (Array.isArray(this.envProvider)) { @@ -215,9 +216,9 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab }): Promise { const ioHost = withAction(this.ioHost, 'bootstrap'); - const bootstrapEnvs = options.environments + const bootstrapEnvs = options.environments ? BootstrapEnvironments.fromList(options.environments) - : options.cloudAssembly + : options.cloudAssembly ? BootstrapEnvironments.fromCloudAssemblySource(options.cloudAssembly) : undefined; diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index 5fdedf0c3..e14748b59 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -83,7 +83,7 @@ async function runBootstrap(options?: { environments?: string[]; source?: Bootst return toolkit.bootstrap({ // if environment strings are not provided, get the environments from the cloud assembly ...(options?.environments?.length ? { environments: options.environments } : { cloudAssembly: cx }), - ...(options?.source && { source: options.source } ) + ...(options?.source && { source: options.source } ), }); } @@ -144,8 +144,8 @@ describe('bootstrap', () => { environment: { name: 'aws://123456789012/us-east-1', account: '123456789012', - region: 'us-east-1' - } + region: 'us-east-1', + }, }), })); expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ @@ -155,8 +155,8 @@ describe('bootstrap', () => { environment: { name: 'aws://210987654321/eu-west-1', account: '210987654321', - region: 'eu-west-1' - } + region: 'eu-west-1', + }, }), })); }); diff --git a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts index a2a475d7e..4adb638a3 100644 --- a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts @@ -11,4 +11,4 @@ export { restoreSdkMocksToDefault, } from '../../../../aws-cdk/test/util/mock-sdk'; -export { rootDir } from '../../../../aws-cdk/lib/util/directories'; \ No newline at end of file +export { rootDir } from '../../../../aws-cdk/lib/util/directories'; From f6497a2be798b7b76c2679fc0ccd86220b698e0c Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:38:18 -0500 Subject: [PATCH 21/41] bootstrap parameter changes --- .../lib/actions/bootstrap/index.ts | 49 +++++++++++-------- .../toolkit-lib/lib/toolkit/toolkit.ts | 28 +++-------- .../test/actions/bootstrap.test.ts | 9 ++-- 3 files changed, 39 insertions(+), 47 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts index b15a4de95..852eeb5e9 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts @@ -4,10 +4,15 @@ import { Tag } from '../../api/aws-cdk'; * Options for Bootstrap */ export interface BootstrapOptions { + + /** + * Bootstrap environment parameters for CloudFormation used when deploying the bootstrap stack + * @default BootstrapEnvironmentParameters.onlyExisting() + */ readonly parameters?: BootstrapEnvironmentParameters; /** - * The source of the bootstrap stack + * The template source of the bootstrap stack * * @default BootstrapSource.default() */ @@ -139,34 +144,36 @@ export interface BootstrapParameterValues { */ export class BootstrapEnvironmentParameters { /** - * Use default values for all parameters + * Use only existing parameters on the stack. */ - static default() { - return new BootstrapEnvironmentParameters(); - } - + public static onlyExisting() { + return new BootstrapEnvironmentParameters({}, true); + } + /** - * Use custom parameters and fall back to default values + * Use exactly these parameters and remove any other existing parameters from the stack. */ - static custom(params: BootstrapParameterValues) { - return new BootstrapEnvironmentParameters(params); - } - + public static exactly(params: BootstrapParameterValues) { + return new BootstrapEnvironmentParameters(params, false); + } + + /** + * Define additional parameters for the stack, while keeping existing parameters for unspecified values. + */ + public static withExisting(params: BootstrapParameterValues) { + return new BootstrapEnvironmentParameters(params, true); + } + /** * The parameters as a Map for easy access and manipulation */ - private readonly parameters?: BootstrapParameterValues; + public readonly parameters?: BootstrapParameterValues; + public readonly keepExistingParameters: boolean; - private constructor(params?: BootstrapParameterValues) { - this.parameters = params; - } - /** - * Render the parameters as a BootstrappingParameterValues object - * @returns A BootstrappingParameterValues object - */ - public render(): BootstrapParameterValues { - return this.parameters ?? {}; + private constructor(params?: BootstrapParameterValues, usePreviousParameters = true) { + this.keepExistingParameters = usePreviousParameters; + this.parameters = params; } } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 654406cac..3c7bcea49 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -4,8 +4,8 @@ import * as chalk from 'chalk'; import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; import { ToolkitServices } from './private'; -import { BootstrapOptions, BootstrapEnvironmentParameters, BootstrapSource } from '../actions/bootstrap'; import { environmentsFromDescriptors } from '../actions/bootstrap/private'; +import { BootstrapOptions, BootstrapSource } from '../actions/bootstrap'; import { AssetBuildTime, type DeployOptions, RequireApproval } from '../actions/deploy'; import { type ExtendedDeployOptions, buildParameterMap, createHotswapPropertyOverrides, removePublishedAssets } from '../actions/deploy/private'; import { type DestroyOptions } from '../actions/destroy'; @@ -108,9 +108,10 @@ async function assemblyFromSource(assemblySource: ICloudAssemblySource, cache: b /** * Helper class to manage bootstrap environments */ -class BootstrapEnvironments { +export class BootstrapEnvironments { /** * Create from a list of environment descriptors + * List of strings like `['aws://012345678912/us-east-1', 'aws://234567890123/eu-west-1']` */ static fromList(environments: string[]): BootstrapEnvironments { return new BootstrapEnvironments(environmentsFromDescriptors(environments)); @@ -210,25 +211,11 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab /** * Bootstrap Action */ - public async bootstrap(options: BootstrapOptions & { - environments?: string[]; - cloudAssembly?: ICloudAssemblySource; - }): Promise { + public async bootstrap(environments: BootstrapEnvironments, options: BootstrapOptions): Promise { const ioHost = withAction(this.ioHost, 'bootstrap'); - - const bootstrapEnvs = options.environments - ? BootstrapEnvironments.fromList(options.environments) - : options.cloudAssembly - ? BootstrapEnvironments.fromCloudAssemblySource(options.cloudAssembly) - : undefined; - - if (!bootstrapEnvs) { - throw new ToolkitError('Either environments or cloudAssembly must be provided'); - } - - const bootstrapEnvironments = await bootstrapEnvs.getEnvironments(); + const bootstrapEnvironments = await environments.getEnvironments(); const source = options.source ?? BootstrapSource.default(); - const parameters = options.parameters ?? BootstrapEnvironmentParameters.default(); + const parameters = options.parameters; const bootstrapper = new Bootstrapper(source.render(), { ioHost, action: 'bootstrap' }); const sdkProvider = await this.sdkProvider('bootstrap'); const limit = pLimit(20); @@ -247,7 +234,8 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab ...options, toolkitStackName, source: source.render(), - parameters: parameters.render(), + parameters: parameters?.parameters, + usePreviousParameters: parameters?.keepExistingParameters }, ); const message = bootstrapResult.noOp diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index e14748b59..1804b9377 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -10,7 +10,7 @@ import { import { bold } from 'chalk'; import { BootstrapSource } from '../../lib/actions/bootstrap'; import { SdkProvider } from '../../lib/api/aws-cdk'; -import { Toolkit } from '../../lib/toolkit'; +import { BootstrapEnvironments, Toolkit } from '../../lib/toolkit'; import { TestIoHost, builderFixture } from '../_helpers'; import { MockSdkProvider, @@ -80,11 +80,8 @@ function createMockStack(outputs: { OutputKey: string; OutputValue: string }[]): async function runBootstrap(options?: { environments?: string[]; source?: BootstrapSource }) { const cx = await builderFixture(toolkit, 'stack-with-asset'); - return toolkit.bootstrap({ - // if environment strings are not provided, get the environments from the cloud assembly - ...(options?.environments?.length ? { environments: options.environments } : { cloudAssembly: cx }), - ...(options?.source && { source: options.source } ), - }); + const bootstrapEnvs = options?.environments?.length ? BootstrapEnvironments.fromList(options.environments) : BootstrapEnvironments.fromCloudAssemblySource(cx); + return toolkit.bootstrap(bootstrapEnvs, { source: options?.source }); } function expectSuccessfulBootstrap() { From f0dd0656297efe87d2ed4523899b161f439200c1 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:12:42 -0500 Subject: [PATCH 22/41] line too long --- packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index 1804b9377..4bfbfe52a 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -80,7 +80,8 @@ function createMockStack(outputs: { OutputKey: string; OutputValue: string }[]): async function runBootstrap(options?: { environments?: string[]; source?: BootstrapSource }) { const cx = await builderFixture(toolkit, 'stack-with-asset'); - const bootstrapEnvs = options?.environments?.length ? BootstrapEnvironments.fromList(options.environments) : BootstrapEnvironments.fromCloudAssemblySource(cx); + const bootstrapEnvs = options?.environments?.length ? + BootstrapEnvironments.fromList(options.environments) : BootstrapEnvironments.fromCloudAssemblySource(cx); return toolkit.bootstrap(bootstrapEnvs, { source: options?.source }); } From 28ecc6aa134f777c00a6e84b97c3c8c5baea78cc Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Feb 2025 21:24:32 +0000 Subject: [PATCH 23/41] chore: self mutation Signed-off-by: github-actions --- .../lib/actions/bootstrap/index.ts | 33 +++++++++---------- .../toolkit-lib/lib/toolkit/toolkit.ts | 4 +-- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts index 852eeb5e9..82ae40534 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts @@ -4,7 +4,7 @@ import { Tag } from '../../api/aws-cdk'; * Options for Bootstrap */ export interface BootstrapOptions { - + /** * Bootstrap environment parameters for CloudFormation used when deploying the bootstrap stack * @default BootstrapEnvironmentParameters.onlyExisting() @@ -146,31 +146,30 @@ export class BootstrapEnvironmentParameters { /** * Use only existing parameters on the stack. */ - public static onlyExisting() { - return new BootstrapEnvironmentParameters({}, true); - } - + public static onlyExisting() { + return new BootstrapEnvironmentParameters({}, true); + } + /** * Use exactly these parameters and remove any other existing parameters from the stack. */ - public static exactly(params: BootstrapParameterValues) { - return new BootstrapEnvironmentParameters(params, false); - } - - /** - * Define additional parameters for the stack, while keeping existing parameters for unspecified values. - */ - public static withExisting(params: BootstrapParameterValues) { - return new BootstrapEnvironmentParameters(params, true); - } - + public static exactly(params: BootstrapParameterValues) { + return new BootstrapEnvironmentParameters(params, false); + } + + /** + * Define additional parameters for the stack, while keeping existing parameters for unspecified values. + */ + public static withExisting(params: BootstrapParameterValues) { + return new BootstrapEnvironmentParameters(params, true); + } + /** * The parameters as a Map for easy access and manipulation */ public readonly parameters?: BootstrapParameterValues; public readonly keepExistingParameters: boolean; - private constructor(params?: BootstrapParameterValues, usePreviousParameters = true) { this.keepExistingParameters = usePreviousParameters; this.parameters = params; diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 3c7bcea49..97cc96826 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -4,8 +4,8 @@ import * as chalk from 'chalk'; import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; import { ToolkitServices } from './private'; -import { environmentsFromDescriptors } from '../actions/bootstrap/private'; import { BootstrapOptions, BootstrapSource } from '../actions/bootstrap'; +import { environmentsFromDescriptors } from '../actions/bootstrap/private'; import { AssetBuildTime, type DeployOptions, RequireApproval } from '../actions/deploy'; import { type ExtendedDeployOptions, buildParameterMap, createHotswapPropertyOverrides, removePublishedAssets } from '../actions/deploy/private'; import { type DestroyOptions } from '../actions/destroy'; @@ -235,7 +235,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab toolkitStackName, source: source.render(), parameters: parameters?.parameters, - usePreviousParameters: parameters?.keepExistingParameters + usePreviousParameters: parameters?.keepExistingParameters, }, ); const message = bootstrapResult.noOp From 64880df32fc48df445536b59917fe1cae68b3efb Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:17:44 -0500 Subject: [PATCH 24/41] merge changes --- .projenrc.ts | 1 - packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.projenrc.ts b/.projenrc.ts index d541ae414..f23763476 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1148,7 +1148,6 @@ toolkitLib.npmignore?.addPatterns( 'build-tools', 'docs', 'typedoc.json', - '*.d.ts', '*.d.ts.map', // Explicitly allow all required files '!build-info.json', diff --git a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json index 3e7e4d40e..89b7f84ba 100644 --- a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json +++ b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json @@ -236,6 +236,9 @@ { "exec": "node-bundle validate --external=fsevents:optional --entrypoint=lib/index.js --fix --dont-attribute \"^@aws-cdk/|^cdk-assets$|^cdk-cli-wrapper$|^aws-cdk$\"" }, + { + "exec": "mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/" + }, { "exec": "cp $(node -p 'require.resolve(\"cdk-from-cfn/index_bg.wasm\")') ./lib/" }, From 103f7cdac89ea8affd990b15f1dbf26c78acfa7e Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 24 Feb 2025 15:26:22 +0000 Subject: [PATCH 25/41] chore: self mutation Signed-off-by: github-actions --- packages/@aws-cdk/toolkit-lib/.npmignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/.npmignore b/packages/@aws-cdk/toolkit-lib/.npmignore index f7b76008a..b99667ed4 100644 --- a/packages/@aws-cdk/toolkit-lib/.npmignore +++ b/packages/@aws-cdk/toolkit-lib/.npmignore @@ -19,10 +19,10 @@ tsconfig.tsbuildinfo /.eslintrc.json .eslintrc.js *.ts -!*.d.ts build-tools docs typedoc.json +*.d.ts *.d.ts.map !build-info.json !db.json.gz From fa8e75ba2e5527cc12be9b295bdbcbe70a595109 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 24 Feb 2025 15:29:49 +0000 Subject: [PATCH 26/41] chore: self mutation Signed-off-by: github-actions --- packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json index 89b7f84ba..3e7e4d40e 100644 --- a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json +++ b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json @@ -236,9 +236,6 @@ { "exec": "node-bundle validate --external=fsevents:optional --entrypoint=lib/index.js --fix --dont-attribute \"^@aws-cdk/|^cdk-assets$|^cdk-cli-wrapper$|^aws-cdk$\"" }, - { - "exec": "mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/" - }, { "exec": "cp $(node -p 'require.resolve(\"cdk-from-cfn/index_bg.wasm\")') ./lib/" }, From 945050238b4150fdcb3f9f5effb96da4c52e39bd Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 24 Feb 2025 15:42:07 +0000 Subject: [PATCH 27/41] chore: self mutation Signed-off-by: github-actions --- packages/@aws-cdk/toolkit-lib/.npmignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/.npmignore b/packages/@aws-cdk/toolkit-lib/.npmignore index b99667ed4..f7b76008a 100644 --- a/packages/@aws-cdk/toolkit-lib/.npmignore +++ b/packages/@aws-cdk/toolkit-lib/.npmignore @@ -19,10 +19,10 @@ tsconfig.tsbuildinfo /.eslintrc.json .eslintrc.js *.ts +!*.d.ts build-tools docs typedoc.json -*.d.ts *.d.ts.map !build-info.json !db.json.gz From dbcb73e1d5c75c87955ea8a5d27aee225f54bd4f Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:14:15 -0500 Subject: [PATCH 28/41] modifed wrong projen file --- .projenrc.ts | 1 + packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.projenrc.ts b/.projenrc.ts index f23763476..e4d7ba0b3 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -983,6 +983,7 @@ new JsiiBuild(cliLib, { // clilib needs to bundle some resources, same as the CLI cliLib.postCompileTask.exec('node-bundle validate --external=fsevents:optional --entrypoint=lib/index.js --fix --dont-attribute "^@aws-cdk/|^cdk-assets$|^cdk-cli-wrapper$|^aws-cdk$"'); +cliLib.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/'); for (const resourceCommand of includeCliResourcesCommands) { cliLib.postCompileTask.exec(resourceCommand); } diff --git a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json index 3e7e4d40e..89b7f84ba 100644 --- a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json +++ b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json @@ -236,6 +236,9 @@ { "exec": "node-bundle validate --external=fsevents:optional --entrypoint=lib/index.js --fix --dont-attribute \"^@aws-cdk/|^cdk-assets$|^cdk-cli-wrapper$|^aws-cdk$\"" }, + { + "exec": "mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/" + }, { "exec": "cp $(node -p 'require.resolve(\"cdk-from-cfn/index_bg.wasm\")') ./lib/" }, From 849f7f913b882d98557367624bc845b7c8ab3593 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:42:37 -0500 Subject: [PATCH 29/41] tests for parameters --- .../test/actions/bootstrap.test.ts | 181 +++++++++++++++++- 1 file changed, 178 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index 4bfbfe52a..59dbaa406 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -8,7 +8,7 @@ import { Stack, } from '@aws-sdk/client-cloudformation'; import { bold } from 'chalk'; -import { BootstrapSource } from '../../lib/actions/bootstrap'; +import { BootstrapEnvironmentParameters, BootstrapSource } from '../../lib/actions/bootstrap'; import { SdkProvider } from '../../lib/api/aws-cdk'; import { BootstrapEnvironments, Toolkit } from '../../lib/toolkit'; import { TestIoHost, builderFixture } from '../_helpers'; @@ -78,11 +78,18 @@ function createMockStack(outputs: { OutputKey: string; OutputValue: string }[]): } as Stack; } -async function runBootstrap(options?: { environments?: string[]; source?: BootstrapSource }) { +async function runBootstrap(options?: { + environments?: string[], + source?: BootstrapSource, + parameters?: BootstrapEnvironmentParameters +}) { const cx = await builderFixture(toolkit, 'stack-with-asset'); const bootstrapEnvs = options?.environments?.length ? BootstrapEnvironments.fromList(options.environments) : BootstrapEnvironments.fromCloudAssemblySource(cx); - return toolkit.bootstrap(bootstrapEnvs, { source: options?.source }); + return toolkit.bootstrap(bootstrapEnvs, { + source: options?.source, + parameters: options?.parameters, + }); } function expectSuccessfulBootstrap() { @@ -202,6 +209,174 @@ describe('bootstrap', () => { }); }); + describe('bootstrap parameters', () => { + test('bootstrap with default parameters', async () => { + // GIVEN + const mockStack = createMockStack([ + { OutputKey: 'BucketName', OutputValue: 'BUCKET_NAME' }, + { OutputKey: 'BucketDomainName', OutputValue: 'BUCKET_ENDPOINT' }, + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, + ]); + setupMockCloudFormationClient(mockStack); + + // WHEN + await runBootstrap(); + + // THEN + const createChangeSetCalls = mockCloudFormationClient.calls().filter(call => call.args[0] instanceof CreateChangeSetCommand); + expect(createChangeSetCalls.length).toBeGreaterThan(0); + const parameters = (createChangeSetCalls[0].args[0].input as any).Parameters; + // Default parameters should include standard bootstrap parameters + expect(new Set(parameters)).toEqual(new Set([ + { + ParameterKey: 'TrustedAccounts', + ParameterValue: '', + }, + { + ParameterKey: 'TrustedAccountsForLookup', + ParameterValue: '', + }, + { + ParameterKey: 'CloudFormationExecutionPolicies', + ParameterValue: '', + }, + { + ParameterKey: 'FileAssetsBucketKmsKeyId', + ParameterValue: 'AWS_MANAGED_KEY', + }, + { + ParameterKey: 'PublicAccessBlockConfiguration', + ParameterValue: 'true', + }, + ])); + expectSuccessfulBootstrap(); + }); + + test('bootstrap with exact parameters', async () => { + // GIVEN + const mockStack = createMockStack([ + { OutputKey: 'BucketName', OutputValue: 'CUSTOM_BUCKET' }, + { OutputKey: 'BucketDomainName', OutputValue: 'CUSTOM_ENDPOINT' }, + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, + ]); + setupMockCloudFormationClient(mockStack); + + const customParams = { + bucketName: 'custom-bucket', + qualifier: 'test', + publicAccessBlockConfiguration: false, + }; + + // WHEN + await runBootstrap({ + parameters: BootstrapEnvironmentParameters.exactly(customParams), + }); + + // THEN + const createChangeSetCalls = mockCloudFormationClient.calls().filter(call => call.args[0] instanceof CreateChangeSetCommand); + expect(createChangeSetCalls.length).toBeGreaterThan(0); + const parameters = (createChangeSetCalls[0].args[0].input as any).Parameters; + // For exact parameters, we should see our custom values + expect(parameters).toContainEqual({ + ParameterKey: 'FileAssetsBucketName', + ParameterValue: 'custom-bucket', + }); + expect(parameters).toContainEqual({ + ParameterKey: 'Qualifier', + ParameterValue: 'test', + }); + expect(parameters).toContainEqual({ + ParameterKey: 'PublicAccessBlockConfiguration', + ParameterValue: 'false', + }); + expectSuccessfulBootstrap(); + }); + + test('bootstrap with additional parameters', async () => { + // GIVEN + const mockStack = createMockStack([ + { OutputKey: 'BucketName', OutputValue: 'EXISTING_BUCKET' }, + { OutputKey: 'BucketDomainName', OutputValue: 'EXISTING_ENDPOINT' }, + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, + ]); + setupMockCloudFormationClient(mockStack); + + const additionalParams = { + qualifier: 'additional', + trustedAccounts: ['123456789012'], + cloudFormationExecutionPolicies: ['arn:aws:iam::aws:policy/AdministratorAccess'], + }; + + // WHEN + await runBootstrap({ + parameters: BootstrapEnvironmentParameters.withExisting(additionalParams), + }); + + // THEN + const createChangeSetCalls = mockCloudFormationClient.calls().filter(call => call.args[0] instanceof CreateChangeSetCommand); + expect(createChangeSetCalls.length).toBeGreaterThan(0); + const parameters = (createChangeSetCalls[0].args[0].input as any).Parameters; + // For additional parameters, we should see our new values merged with defaults + expect(parameters).toContainEqual({ + ParameterKey: 'Qualifier', + ParameterValue: 'additional', + }); + expect(parameters).toContainEqual({ + ParameterKey: 'TrustedAccounts', + ParameterValue: '123456789012', + }); + expect(parameters).toContainEqual({ + ParameterKey: 'CloudFormationExecutionPolicies', + ParameterValue: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + expectSuccessfulBootstrap(); + }); + + test('bootstrap with only existing parameters', async () => { + // GIVEN + const mockStack = createMockStack([ + { OutputKey: 'BucketName', OutputValue: 'EXISTING_BUCKET' }, + { OutputKey: 'BucketDomainName', OutputValue: 'EXISTING_ENDPOINT' }, + { OutputKey: 'BootstrapVersion', OutputValue: '1' }, + ]); + setupMockCloudFormationClient(mockStack); + + // WHEN + await runBootstrap({ + parameters: BootstrapEnvironmentParameters.onlyExisting(), + }); + + // THEN + const createChangeSetCalls = mockCloudFormationClient.calls().filter(call => call.args[0] instanceof CreateChangeSetCommand); + expect(createChangeSetCalls.length).toBeGreaterThan(0); + const parameters = (createChangeSetCalls[0].args[0].input as any).Parameters; + // When using only existing parameters, we should get the default set + expect(new Set(parameters)).toEqual(new Set([ + { + ParameterKey: 'TrustedAccounts', + ParameterValue: '', + }, + { + ParameterKey: 'TrustedAccountsForLookup', + ParameterValue: '', + }, + { + ParameterKey: 'CloudFormationExecutionPolicies', + ParameterValue: '', + }, + { + ParameterKey: 'FileAssetsBucketKmsKeyId', + ParameterValue: 'AWS_MANAGED_KEY', + }, + { + ParameterKey: 'PublicAccessBlockConfiguration', + ParameterValue: 'true', + }, + ])); + expectSuccessfulBootstrap(); + }); + }); + describe('template sources', () => { test('uses default template when no source is specified', async () => { // GIVEN From bacd2609ad5a7d06ccc1dfc935af3bf7a6e68c1b Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 24 Feb 2025 18:57:33 +0000 Subject: [PATCH 30/41] chore: self mutation Signed-off-by: github-actions --- .../@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index 59dbaa406..c95a83d01 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -79,14 +79,14 @@ function createMockStack(outputs: { OutputKey: string; OutputValue: string }[]): } async function runBootstrap(options?: { - environments?: string[], - source?: BootstrapSource, - parameters?: BootstrapEnvironmentParameters + environments?: string[]; + source?: BootstrapSource; + parameters?: BootstrapEnvironmentParameters; }) { const cx = await builderFixture(toolkit, 'stack-with-asset'); const bootstrapEnvs = options?.environments?.length ? BootstrapEnvironments.fromList(options.environments) : BootstrapEnvironments.fromCloudAssemblySource(cx); - return toolkit.bootstrap(bootstrapEnvs, { + return toolkit.bootstrap(bootstrapEnvs, { source: options?.source, parameters: options?.parameters, }); From 1b3100dd1fe71356eec3a9fbf0058c5e54634063 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:33:45 -0500 Subject: [PATCH 31/41] merge conflicts --- .../toolkit-lib/lib/api/io/private/codes.ts | 15 ++++++++++++--- .../toolkit-lib/lib/api/io/private/timer.ts | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts index cb89e676c..e3a972545 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts @@ -164,9 +164,18 @@ export const CODES = { }), // 9: Bootstrap - CDK_TOOLKIT_I9000: 'Provides bootstrap times', - CDK_TOOLKIT_E9900: 'Bootstrap failed', - CDK_TOOLKIT_I9900: 'Bootstrap results on success', + CDK_TOOLKIT_I9000: codeInfo({ code: 'CDK_TOOLKIT_I9000', + description: 'Provides bootstrap times', + level: 'info', + }), + CDK_TOOLKIT_I9900: codeInfo({ code: 'CDK_TOOLKIT_I9900', + description: 'Bootstrap results on success', + level: 'info', + }), + CDK_TOOLKIT_E9900: codeInfo({ code: 'CDK_TOOLKIT_E9900', + description: 'Bootstrap failed', + level: 'error', + }), // Assembly codes CDK_ASSEMBLY_I0042: codeInfo({ diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts index bfc291133..cb76a232c 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts @@ -49,7 +49,7 @@ export class Timer { } } -function timerMessageProps(type: 'synth' | 'deploy' | 'rollback'| 'destroy'): { +function timerMessageProps(type: 'synth' | 'deploy' | 'rollback'| 'destroy' | 'bootstrap'): { code: CodeInfo; text: string; } { From 89acec047d8f91fa399fc0f4da5a7ef54cafd153 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:41:01 -0500 Subject: [PATCH 32/41] use CodeInfo code instead of string codes for bootstrap action and tests --- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 2 +- packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index d2e9cfbcf..8ba031717 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -243,7 +243,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab ? ` ✅ ${environment.name} (no changes)` : ` ✅ ${environment.name}`; - await ioHost.notify(result(chalk.green('\n' + message), 'CDK_TOOLKIT_I9900', { environment })); + await ioHost.notify(result(chalk.green('\n' + message), CODES.CDK_TOOLKIT_I9900, { environment })); await bootstrapTimer.endAs(ioHost, 'bootstrap'); } catch (e) { await ioHost.notify(error(`\n ❌ ${chalk.bold(environment.name)} failed: ${formatErrorMessage(e)}`, 'CDK_TOOLKIT_E9900')); diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index c95a83d01..87029d0e3 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -20,6 +20,7 @@ import { setDefaultSTSMocks, rootDir, } from '../util/aws-cdk'; +import { CODES } from '../../lib/api/io/private'; const ioHost = new TestIoHost(); const toolkit = new Toolkit({ ioHost }); @@ -143,7 +144,7 @@ describe('bootstrap', () => { message: expect.stringContaining(`${bold('aws://210987654321/eu-west-1')}: bootstrapping...`), })); expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - code: 'CDK_TOOLKIT_I9900', + code: CODES.CDK_TOOLKIT_I9900, message: expect.stringContaining('✅'), data: expect.objectContaining({ environment: { @@ -154,7 +155,7 @@ describe('bootstrap', () => { }), })); expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - code: 'CDK_TOOLKIT_I9900', + code: CODES.CDK_TOOLKIT_I9900, message: expect.stringContaining('✅'), data: expect.objectContaining({ environment: { From d86a8b795657fa3ad18591adba2e37e11c95391e Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:49:42 -0500 Subject: [PATCH 33/41] missed a status code --- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 8ba031717..3fec4d7ac 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -246,7 +246,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab await ioHost.notify(result(chalk.green('\n' + message), CODES.CDK_TOOLKIT_I9900, { environment })); await bootstrapTimer.endAs(ioHost, 'bootstrap'); } catch (e) { - await ioHost.notify(error(`\n ❌ ${chalk.bold(environment.name)} failed: ${formatErrorMessage(e)}`, 'CDK_TOOLKIT_E9900')); + await ioHost.notify(error(`\n ❌ ${chalk.bold(environment.name)} failed: ${formatErrorMessage(e)}`, CODES.CDK_TOOLKIT_E9900)); throw e; } }))); From d2fd48be482377092d98e4ca759e28f4a7e1bf20 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:03:26 -0500 Subject: [PATCH 34/41] code registry update and used string codes in tests --- packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md | 3 +++ packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md b/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md index c5c65d2cb..9a63d6cf5 100644 --- a/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md +++ b/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md @@ -22,6 +22,9 @@ | CDK_TOOLKIT_I7010 | Confirm destroy stacks | info | n/a | | CDK_TOOLKIT_E7010 | Action was aborted due to negative confirmation of request | error | n/a | | CDK_TOOLKIT_E7900 | Stack deletion failed | error | n/a | +| CDK_TOOLKIT_I9000 | Provides bootstrap times | info | n/a | +| CDK_TOOLKIT_I9900 | Bootstrap results on success | info | n/a | +| CDK_TOOLKIT_E9900 | Bootstrap failed | error | n/a | | CDK_ASSEMBLY_I0042 | Writing updated context | debug | n/a | | CDK_ASSEMBLY_I0241 | Fetching missing context | debug | n/a | | CDK_ASSEMBLY_I1000 | Cloud assembly output starts | debug | n/a | diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index 87029d0e3..c95a83d01 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -20,7 +20,6 @@ import { setDefaultSTSMocks, rootDir, } from '../util/aws-cdk'; -import { CODES } from '../../lib/api/io/private'; const ioHost = new TestIoHost(); const toolkit = new Toolkit({ ioHost }); @@ -144,7 +143,7 @@ describe('bootstrap', () => { message: expect.stringContaining(`${bold('aws://210987654321/eu-west-1')}: bootstrapping...`), })); expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - code: CODES.CDK_TOOLKIT_I9900, + code: 'CDK_TOOLKIT_I9900', message: expect.stringContaining('✅'), data: expect.objectContaining({ environment: { @@ -155,7 +154,7 @@ describe('bootstrap', () => { }), })); expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - code: CODES.CDK_TOOLKIT_I9900, + code: 'CDK_TOOLKIT_I9900', message: expect.stringContaining('✅'), data: expect.objectContaining({ environment: { From 70e7082de9d57bd6153346b0d9ae2fb07d529738 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:06:44 -0500 Subject: [PATCH 35/41] linting --- .../@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts | 9 ++++++--- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts index e3a972545..d2d2f8323 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts @@ -164,15 +164,18 @@ export const CODES = { }), // 9: Bootstrap - CDK_TOOLKIT_I9000: codeInfo({ code: 'CDK_TOOLKIT_I9000', + CDK_TOOLKIT_I9000: codeInfo({ + code: 'CDK_TOOLKIT_I9000', description: 'Provides bootstrap times', level: 'info', }), - CDK_TOOLKIT_I9900: codeInfo({ code: 'CDK_TOOLKIT_I9900', + CDK_TOOLKIT_I9900: codeInfo({ + code: 'CDK_TOOLKIT_I9900', description: 'Bootstrap results on success', level: 'info', }), - CDK_TOOLKIT_E9900: codeInfo({ code: 'CDK_TOOLKIT_E9900', + CDK_TOOLKIT_E9900: codeInfo({ + code: 'CDK_TOOLKIT_E9900', description: 'Bootstrap failed', level: 'error', }), diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 3fec4d7ac..dcf3db2d4 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -4,9 +4,9 @@ import * as chalk from 'chalk'; import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; import { ToolkitServices } from './private'; +import { AssemblyData, StackAndAssemblyData } from './types'; import { BootstrapOptions, BootstrapSource } from '../actions/bootstrap'; import { environmentsFromDescriptors } from '../actions/bootstrap/private'; -import { AssemblyData, StackAndAssemblyData } from './types'; import { AssetBuildTime, type DeployOptions, RequireApproval } from '../actions/deploy'; import { type ExtendedDeployOptions, buildParameterMap, createHotswapPropertyOverrides, removePublishedAssets } from '../actions/deploy/private'; import { type DestroyOptions } from '../actions/destroy'; From 4e941da5af20d58288057535e23430afb53b1cab Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:00:39 -0500 Subject: [PATCH 36/41] move bootstrap example custom template to toolkit-lib test directory --- .projenrc.ts | 1 - packages/@aws-cdk/toolkit-lib/.npmignore | 1 - packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs | 1 - packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts | 2 +- .../test}/api/bootstrap/custom-bootstrap-template.yaml | 0 5 files changed, 1 insertion(+), 4 deletions(-) rename packages/{aws-cdk/lib => @aws-cdk/toolkit-lib/test}/api/bootstrap/custom-bootstrap-template.yaml (100%) diff --git a/.projenrc.ts b/.projenrc.ts index f6789141e..31f054668 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1191,7 +1191,6 @@ toolkitLib.npmignore?.addPatterns( '!db.json.gz', '!lib/init-templates/**/*.ts', '!lib/api/bootstrap/bootstrap-template.yaml', - '!lib/api/bootstrap/custom-bootstrap-template.yaml', '!lib/*.js', '!lib/*.d.ts', '!LICENSE', diff --git a/packages/@aws-cdk/toolkit-lib/.npmignore b/packages/@aws-cdk/toolkit-lib/.npmignore index 5f2241c6e..ccb176b46 100644 --- a/packages/@aws-cdk/toolkit-lib/.npmignore +++ b/packages/@aws-cdk/toolkit-lib/.npmignore @@ -31,7 +31,6 @@ typedoc.json !db.json.gz !lib/init-templates/**/*.ts !lib/api/bootstrap/bootstrap-template.yaml -!lib/api/bootstrap/custom-bootstrap-template.yaml !lib/*.js !lib/*.d.ts !LICENSE diff --git a/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs b/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs index ef81ee99b..ede8d86ca 100644 --- a/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs +++ b/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs @@ -21,7 +21,6 @@ await Promise.all([ await fs.ensureDir('lib/api/bootstrap'); await Promise.all([ copyFromCli(['lib', 'api', 'bootstrap', 'bootstrap-template.yaml'], ['lib', 'api', 'bootstrap', 'bootstrap-template.yaml']), - copyFromCli(['lib', 'api', 'bootstrap', 'custom-bootstrap-template.yaml'], ['lib', 'api', 'bootstrap', 'custom-bootstrap-template.yaml']) ]); await esbuild.build({ diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index c95a83d01..c2de2d8fc 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -405,7 +405,7 @@ describe('bootstrap', () => { // WHEN await runBootstrap({ - source: BootstrapSource.customTemplate(path.join(rootDir(), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml')), + source: BootstrapSource.customTemplate(path.join('test', 'api', 'bootstrap', 'custom-bootstrap-template.yaml')), }); // THEN diff --git a/packages/aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml b/packages/@aws-cdk/toolkit-lib/test/api/bootstrap/custom-bootstrap-template.yaml similarity index 100% rename from packages/aws-cdk/lib/api/bootstrap/custom-bootstrap-template.yaml rename to packages/@aws-cdk/toolkit-lib/test/api/bootstrap/custom-bootstrap-template.yaml From 5021e6084f8885d49985cbe44e0f1087b552627f Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 28 Feb 2025 09:25:46 -0500 Subject: [PATCH 37/41] Revert bundle.mjs --- packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs b/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs index ede8d86ca..1a5ffd058 100644 --- a/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs +++ b/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs @@ -18,10 +18,9 @@ await Promise.all([ copyFromCli(['lib', 'index_bg.wasm']), ]); -await fs.ensureDir('lib/api/bootstrap'); -await Promise.all([ - copyFromCli(['lib', 'api', 'bootstrap', 'bootstrap-template.yaml'], ['lib', 'api', 'bootstrap', 'bootstrap-template.yaml']), -]); +// # Copy all resources that aws_cdk/generate.sh produced, and some othersCall the generator for the +// cp -R $aws_cdk/lib/init-templates ./lib/ +// mkdir -p ./lib/api/bootstrap/ && cp $aws_cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/ await esbuild.build({ entryPoints: ['lib/api/aws-cdk.ts'], From a77119a08f8c02470c92c0c857d1a92eba4e9ab6 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 28 Feb 2025 09:42:35 -0500 Subject: [PATCH 38/41] fix unbound method --- packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index c2de2d8fc..736bc6131 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -104,7 +104,7 @@ function expectSuccessfulBootstrap() { describe('bootstrap', () => { describe('with user-specified environments', () => { - let originalSdk = mockSdkProvider.forEnvironment; + const originalSdk = mockSdkProvider.forEnvironment.bind(mockSdkProvider); beforeEach(() => { const mockForEnvironment = jest.fn().mockImplementation(() => { return { sdk: new MockSdk() }; From 7fab3aeefbbc8fdd810a55245f8ad8df9906edeb Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 28 Feb 2025 10:02:17 -0500 Subject: [PATCH 39/41] use allowed example account number --- .../@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index 736bc6131..136b70a4c 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -132,7 +132,7 @@ describe('bootstrap', () => { setupMockCloudFormationClient(mockStack2); // WHEN - await runBootstrap({ environments: ['aws://123456789012/us-east-1', 'aws://210987654321/eu-west-1'] }); + await runBootstrap({ environments: ['aws://123456789012/us-east-1', 'aws://234567890123/eu-west-1'] }); // THEN expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ @@ -140,7 +140,7 @@ describe('bootstrap', () => { })); expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - message: expect.stringContaining(`${bold('aws://210987654321/eu-west-1')}: bootstrapping...`), + message: expect.stringContaining(`${bold('aws://234567890123/eu-west-1')}: bootstrapping...`), })); expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ code: 'CDK_TOOLKIT_I9900', @@ -158,8 +158,8 @@ describe('bootstrap', () => { message: expect.stringContaining('✅'), data: expect.objectContaining({ environment: { - name: 'aws://210987654321/eu-west-1', - account: '210987654321', + name: 'aws://234567890123/eu-west-1', + account: '234567890123', region: 'eu-west-1', }, }), From 17fd77e5e9536ce1d675d16dea9192ccf14e3e95 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Mon, 3 Mar 2025 17:14:33 +0000 Subject: [PATCH 40/41] interface cleanup --- .../src/util/directories.ts | 28 ++++++ .../toolkit-lib/build-tools/bundle.mjs | 2 +- .../lib/actions/bootstrap/index.ts | 97 +++++++++++-------- .../lib/actions/bootstrap/private/helpers.ts | 2 +- .../toolkit-lib/lib/toolkit/private/index.ts | 20 ++++ .../toolkit-lib/lib/toolkit/toolkit.ts | 68 ++----------- .../_fixtures}/custom-bootstrap-template.yaml | 14 +-- .../_fixtures/invalid-bootstrap-template.yaml | 1 + .../test/actions/bootstrap.test.ts | 19 ++-- .../@aws-cdk/toolkit-lib/test/util/aws-cdk.ts | 2 - .../aws-cdk/lib/api/aws-auth/user-agent.ts | 5 +- .../api/bootstrap/bootstrap-environment.ts | 5 +- 12 files changed, 136 insertions(+), 127 deletions(-) rename packages/@aws-cdk/toolkit-lib/test/{api/bootstrap => actions/_fixtures}/custom-bootstrap-template.yaml (94%) create mode 100644 packages/@aws-cdk/toolkit-lib/test/actions/_fixtures/invalid-bootstrap-template.yaml diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/util/directories.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/util/directories.ts index 501322867..7ae5b2b9b 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/util/directories.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/util/directories.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { ToolkitError } from '../api/toolkit-error'; /** * Return a location that will be used as the CDK home directory. @@ -34,3 +35,30 @@ export function cdkHomeDir() { export function cdkCacheDir() { return path.join(cdkHomeDir(), 'cache'); } + +/** + * From the start location, find the directory that contains the bundled package's package.json + * + * You must assume the caller of this function will be bundled and the package root dir + * is not going to be the same as the package the caller currently lives in. + */ +export function bundledPackageRootDir(start: string): string; +export function bundledPackageRootDir(start: string, fail: true): string; +export function bundledPackageRootDir(start: string, fail: false): string | undefined; +export function bundledPackageRootDir(start: string, fail?: boolean) { + function _rootDir(dirname: string): string | undefined { + const manifestPath = path.join(dirname, 'package.json'); + if (fs.existsSync(manifestPath)) { + return dirname; + } + if (path.dirname(dirname) === dirname) { + if (fail ?? true) { + throw new ToolkitError('Unable to find package manifest'); + } + return undefined; + } + return _rootDir(path.dirname(dirname)); + } + + return _rootDir(start); +} diff --git a/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs b/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs index 6c4e0b050..745ef8602 100644 --- a/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs +++ b/packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs @@ -16,11 +16,11 @@ await Promise.all([ copyFromCli(['build-info.json']), copyFromCli(['/db.json.gz']), copyFromCli(['lib', 'index_bg.wasm']), + copyFromCli(['lib', 'api', 'bootstrap', 'bootstrap-template.yaml']), ]); // # Copy all resources that aws_cdk/generate.sh produced, and some othersCall the generator for the // cp -R $aws_cdk/lib/init-templates ./lib/ -// mkdir -p ./lib/api/bootstrap/ && cp $aws_cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/ await esbuild.build({ outdir: 'lib', diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts index 82ae40534..6c4915128 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts @@ -1,4 +1,43 @@ -import { Tag } from '../../api/aws-cdk'; +import * as cxapi from '@aws-cdk/cx-api'; +import { environmentsFromDescriptors } from './private'; +import type { Tag } from '../../api/aws-cdk'; +import type { ICloudAssemblySource } from '../../api/cloud-assembly'; +import { ALL_STACKS } from '../../api/cloud-assembly/private'; +import { assemblyFromSource } from '../../toolkit/private'; + +/** + * Create manage bootstrap environments + */ +export class BootstrapEnvironments { + /** + * Create from a list of environment descriptors + * List of strings like `['aws://012345678912/us-east-1', 'aws://234567890123/eu-west-1']` + */ + static fromList(environments: string[]): BootstrapEnvironments { + return new BootstrapEnvironments(environmentsFromDescriptors(environments)); + } + + /** + * Create from a cloud assembly source + */ + static fromCloudAssemblySource(cx: ICloudAssemblySource): BootstrapEnvironments { + return new BootstrapEnvironments(async () => { + const assembly = await assemblyFromSource(cx); + const stackCollection = assembly.selectStacksV2(ALL_STACKS); + return stackCollection.stackArtifacts.map(stack => stack.environment); + }); + } + + private constructor(private readonly envProvider: cxapi.Environment[] | (() => Promise)) { + } + + async getEnvironments(): Promise { + if (Array.isArray(this.envProvider)) { + return this.envProvider; + } + return this.envProvider(); + } +} /** * Options for Bootstrap @@ -9,14 +48,14 @@ export interface BootstrapOptions { * Bootstrap environment parameters for CloudFormation used when deploying the bootstrap stack * @default BootstrapEnvironmentParameters.onlyExisting() */ - readonly parameters?: BootstrapEnvironmentParameters; + readonly parameters?: BootstrapStackParameters; /** * The template source of the bootstrap stack * * @default BootstrapSource.default() */ - readonly source?: BootstrapSource; + readonly source?: { source: 'default' } | { source: 'custom'; templateFile: string }; /** * Whether to execute the changeset or only create it and leave it in review @@ -37,21 +76,12 @@ export interface BootstrapOptions { * @default true */ readonly terminationProtection?: boolean; - - /** - * Use previous values for unspecified parameters - * - * If not set, all parameters must be specified for every deployment - * - * @default true - */ - usePreviousParameters?: boolean; } /** * Parameter values for the bootstrapping template */ -export interface BootstrapParameterValues { +export interface BootstrapParameters { /** * The name to be given to the CDK Bootstrap bucket * By default, a name is generated by CloudFormation @@ -140,37 +170,37 @@ export interface BootstrapParameterValues { } /** - * Parameters for the bootstrapping template with flexible configuration options + * Parameters of the bootstrapping template with flexible configuration options */ -export class BootstrapEnvironmentParameters { +export class BootstrapStackParameters { /** * Use only existing parameters on the stack. */ public static onlyExisting() { - return new BootstrapEnvironmentParameters({}, true); + return new BootstrapStackParameters({}, true); } /** * Use exactly these parameters and remove any other existing parameters from the stack. */ - public static exactly(params: BootstrapParameterValues) { - return new BootstrapEnvironmentParameters(params, false); + public static exactly(params: BootstrapParameters) { + return new BootstrapStackParameters(params, false); } /** * Define additional parameters for the stack, while keeping existing parameters for unspecified values. */ - public static withExisting(params: BootstrapParameterValues) { - return new BootstrapEnvironmentParameters(params, true); + public static withExisting(params: BootstrapParameters) { + return new BootstrapStackParameters(params, true); } /** * The parameters as a Map for easy access and manipulation */ - public readonly parameters?: BootstrapParameterValues; + public readonly parameters?: BootstrapParameters; public readonly keepExistingParameters: boolean; - private constructor(params?: BootstrapParameterValues, usePreviousParameters = true) { + private constructor(params?: BootstrapParameters, usePreviousParameters = true) { this.keepExistingParameters = usePreviousParameters; this.parameters = params; } @@ -183,28 +213,17 @@ export class BootstrapSource { /** * Use the default bootstrap template */ - static default(): BootstrapSource { - return new BootstrapSource('default'); + static default(): BootstrapOptions['source'] { + return { source: 'default' }; } /** * Use a custom bootstrap template */ - static customTemplate(templateFile: string): BootstrapSource { - return new BootstrapSource('custom', templateFile); - } - - private readonly source: 'default' | 'custom'; - private readonly templateFile?: string; - private constructor(source: 'default' | 'custom', templateFile?: string) { - this.source = source; - this.templateFile = templateFile; - } - - public render() { + static customTemplate(templateFile: string): BootstrapOptions['source'] { return { - source: this.source, - ...(this.templateFile ? { templateFile: this.templateFile } : {}), - } as { source: 'default' } | { source: 'custom'; templateFile: string }; + source: 'custom', + templateFile, + }; } } diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts index 634191db1..6c965319b 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/private/helpers.ts @@ -1,5 +1,5 @@ import * as cxapi from '@aws-cdk/cx-api'; -import { ToolkitError } from '../../../api/errors'; +import { ToolkitError } from '../../../api/shared-public'; /** * Given a set of "/" strings, construct environments for them diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts index af7409265..012e05a6c 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts @@ -1,5 +1,7 @@ import { SdkProvider } from '../../api/aws-cdk'; +import { ICloudAssemblySource } from '../../api/cloud-assembly'; +import { CachedCloudAssemblySource, StackAssembly } from '../../api/cloud-assembly/private'; import { ActionAwareIoHost } from '../../api/io/private'; /** @@ -9,3 +11,21 @@ export interface ToolkitServices { sdkProvider: SdkProvider; ioHost: ActionAwareIoHost; } + +/** + * Creates a Toolkit internal CloudAssembly from a CloudAssemblySource. + * @param assemblySource the source for the cloud assembly + * @param cache if the assembly should be cached, default: `true` + * @returns the CloudAssembly object + */ +export async function assemblyFromSource(assemblySource: ICloudAssemblySource, cache: boolean = true): Promise { + if (assemblySource instanceof StackAssembly) { + return assemblySource; + } + + if (cache) { + return new StackAssembly(await new CachedCloudAssemblySource(assemblySource).produce()); + } + + return new StackAssembly(await assemblySource.produce()); +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 847485103..dac285dda 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -3,10 +3,9 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as chalk from 'chalk'; import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; -import { ToolkitServices } from './private'; +import { assemblyFromSource, ToolkitServices } from './private'; import { AssemblyData, StackAndAssemblyData } from './types'; -import { BootstrapOptions, BootstrapSource } from '../actions/bootstrap'; -import { environmentsFromDescriptors } from '../actions/bootstrap/private'; +import { BootstrapEnvironments, BootstrapOptions, BootstrapSource } from '../actions/bootstrap'; import { AssetBuildTime, type DeployOptions, RequireApproval } from '../actions/deploy'; import { type ExtendedDeployOptions, buildParameterMap, createHotswapPropertyOverrides, removePublishedAssets } from '../actions/deploy/private'; import { type DestroyOptions } from '../actions/destroy'; @@ -19,12 +18,12 @@ import { patternsArrayForWatch } from '../actions/watch/private'; import { type SdkConfig } from '../api/aws-auth'; import { DEFAULT_TOOLKIT_STACK_NAME, Bootstrapper, SdkProvider, SuccessfulDeployStackResult, StackCollection, Deployments, HotswapMode, ResourceMigrator, tagsForStack, CliIoHost, Concurrency, WorkGraphBuilder, AssetBuildNode, AssetPublishNode, StackNode, CloudWatchLogEventMonitor, findCloudWatchLogGroups, StackDetails } from '../api/aws-cdk'; import { ICloudAssemblySource, StackSelectionStrategy } from '../api/cloud-assembly'; -import { ALL_STACKS, CachedCloudAssemblySource, CloudAssemblySourceBuilder, IdentityCloudAssemblySource, StackAssembly } from '../api/cloud-assembly/private'; +import { ALL_STACKS, CloudAssemblySourceBuilder, IdentityCloudAssemblySource, StackAssembly } from '../api/cloud-assembly/private'; import { IIoHost, IoMessageCode, IoMessageLevel } from '../api/io'; import { asSdkLogger, withAction, Timer, confirm, error, info, success, warn, ActionAwareIoHost, debug, result, withoutEmojis, withoutColor, withTrimmedWhitespace, CODES } from '../api/io/private'; -import { pLimit } from '../util/concurrency'; import { ToolkitError } from '../api/shared-public'; import { obscureTemplate, serializeStructure, validateSnsTopicArn, formatTime, formatErrorMessage } from '../private/util'; +import { pLimit } from '../util/concurrency'; /** * The current action being performed by the CLI. 'none' represents the absence of an action. @@ -89,58 +88,6 @@ export interface ToolkitOptions { assemblyFailureAt?: 'error' | 'warn' | 'none'; } -/** - * Creates a Toolkit internal CloudAssembly from a CloudAssemblySource. - * @param assemblySource the source for the cloud assembly - * @param cache if the assembly should be cached, default: `true` - * @returns the CloudAssembly object - */ -async function assemblyFromSource(assemblySource: ICloudAssemblySource, cache: boolean = true): Promise { - if (assemblySource instanceof StackAssembly) { - return assemblySource; - } - - if (cache) { - return new StackAssembly(await new CachedCloudAssemblySource(assemblySource).produce()); - } - - return new StackAssembly(await assemblySource.produce()); -} - -/** - * Helper class to manage bootstrap environments - */ -export class BootstrapEnvironments { - /** - * Create from a list of environment descriptors - * List of strings like `['aws://012345678912/us-east-1', 'aws://234567890123/eu-west-1']` - */ - static fromList(environments: string[]): BootstrapEnvironments { - return new BootstrapEnvironments(environmentsFromDescriptors(environments)); - } - - /** - * Create from a cloud assembly source - */ - static fromCloudAssemblySource(cx: ICloudAssemblySource): BootstrapEnvironments { - return new BootstrapEnvironments(async () => { - const assembly = await assemblyFromSource(cx); - const stackCollection = assembly.selectStacksV2(ALL_STACKS); - return stackCollection.stackArtifacts.map(stack => stack.environment); - }); - } - - private constructor(private readonly envProvider: cxapi.Environment[] | (() => Promise)) { - } - - async getEnvironments(): Promise { - if (Array.isArray(this.envProvider)) { - return this.envProvider; - } - return this.envProvider(); - } -} - /** * The AWS CDK Programmatic Toolkit */ @@ -218,7 +165,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const bootstrapEnvironments = await environments.getEnvironments(); const source = options.source ?? BootstrapSource.default(); const parameters = options.parameters; - const bootstrapper = new Bootstrapper(source.render(), { ioHost, action: 'bootstrap' }); + const bootstrapper = new Bootstrapper(source, { ioHost, action: 'bootstrap' }); const sdkProvider = await this.sdkProvider('bootstrap'); const limit = pLimit(20); @@ -228,14 +175,13 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const bootstrapTimer = Timer.start(); try { - const { toolkitStackName } = this; const bootstrapResult = await bootstrapper.bootstrapEnvironment( environment, sdkProvider, { ...options, - toolkitStackName, - source: source.render(), + toolkitStackName: this.toolkitStackName, + source, parameters: parameters?.parameters, usePreviousParameters: parameters?.keepExistingParameters, }, diff --git a/packages/@aws-cdk/toolkit-lib/test/api/bootstrap/custom-bootstrap-template.yaml b/packages/@aws-cdk/toolkit-lib/test/actions/_fixtures/custom-bootstrap-template.yaml similarity index 94% rename from packages/@aws-cdk/toolkit-lib/test/api/bootstrap/custom-bootstrap-template.yaml rename to packages/@aws-cdk/toolkit-lib/test/actions/_fixtures/custom-bootstrap-template.yaml index c5f5dff12..b2c2949c9 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/bootstrap/custom-bootstrap-template.yaml +++ b/packages/@aws-cdk/toolkit-lib/test/actions/_fixtures/custom-bootstrap-template.yaml @@ -31,9 +31,9 @@ Resources: NoncurrentVersionExpiration: NoncurrentDays: 90 Tags: - - Key: + - Key: Ref: CustomTagKey - Value: + Value: Ref: CustomTagValue ContainerAssetsRepository: @@ -44,9 +44,9 @@ Resources: ImageScanningConfiguration: ScanOnPush: true Tags: - - Key: + - Key: Ref: CustomTagKey - Value: + Value: Ref: CustomTagValue CloudFormationExecutionRole: @@ -64,9 +64,9 @@ Resources: RoleName: Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} Tags: - - Key: + - Key: Ref: CustomTagKey - Value: + Value: Ref: CustomTagValue CdkBootstrapVersion: @@ -96,4 +96,4 @@ Outputs: BootstrapVersion: Description: The version of this bootstrap stack Value: - Fn::GetAtt: [CdkBootstrapVersion, Value] \ No newline at end of file + Fn::GetAtt: [CdkBootstrapVersion, Value] diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/_fixtures/invalid-bootstrap-template.yaml b/packages/@aws-cdk/toolkit-lib/test/actions/_fixtures/invalid-bootstrap-template.yaml new file mode 100644 index 000000000..4715d89e2 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/actions/_fixtures/invalid-bootstrap-template.yaml @@ -0,0 +1 @@ +not a valid template diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index 136b70a4c..4e32790ca 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -8,9 +8,9 @@ import { Stack, } from '@aws-sdk/client-cloudformation'; import { bold } from 'chalk'; -import { BootstrapEnvironmentParameters, BootstrapSource } from '../../lib/actions/bootstrap'; +import { BootstrapEnvironments, BootstrapOptions, BootstrapSource, BootstrapStackParameters } from '../../lib/actions/bootstrap'; import { SdkProvider } from '../../lib/api/aws-cdk'; -import { BootstrapEnvironments, Toolkit } from '../../lib/toolkit'; +import { Toolkit } from '../../lib/toolkit'; import { TestIoHost, builderFixture } from '../_helpers'; import { MockSdkProvider, @@ -18,7 +18,6 @@ import { mockCloudFormationClient, restoreSdkMocksToDefault, setDefaultSTSMocks, - rootDir, } from '../util/aws-cdk'; const ioHost = new TestIoHost(); @@ -80,8 +79,8 @@ function createMockStack(outputs: { OutputKey: string; OutputValue: string }[]): async function runBootstrap(options?: { environments?: string[]; - source?: BootstrapSource; - parameters?: BootstrapEnvironmentParameters; + source?: BootstrapOptions['source']; + parameters?: BootstrapStackParameters; }) { const cx = await builderFixture(toolkit, 'stack-with-asset'); const bootstrapEnvs = options?.environments?.length ? @@ -269,7 +268,7 @@ describe('bootstrap', () => { // WHEN await runBootstrap({ - parameters: BootstrapEnvironmentParameters.exactly(customParams), + parameters: BootstrapStackParameters.exactly(customParams), }); // THEN @@ -309,7 +308,7 @@ describe('bootstrap', () => { // WHEN await runBootstrap({ - parameters: BootstrapEnvironmentParameters.withExisting(additionalParams), + parameters: BootstrapStackParameters.withExisting(additionalParams), }); // THEN @@ -343,7 +342,7 @@ describe('bootstrap', () => { // WHEN await runBootstrap({ - parameters: BootstrapEnvironmentParameters.onlyExisting(), + parameters: BootstrapStackParameters.onlyExisting(), }); // THEN @@ -405,7 +404,7 @@ describe('bootstrap', () => { // WHEN await runBootstrap({ - source: BootstrapSource.customTemplate(path.join('test', 'api', 'bootstrap', 'custom-bootstrap-template.yaml')), + source: BootstrapSource.customTemplate(path.join(__dirname, '_fixtures', 'custom-bootstrap-template.yaml')), }); // THEN @@ -423,7 +422,7 @@ describe('bootstrap', () => { // WHEN await expect(runBootstrap({ - source: BootstrapSource.customTemplate(path.join(rootDir(), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml')), + source: BootstrapSource.customTemplate(path.join(__dirname, '_fixtures', 'invalid-bootstrap-template.yaml')), })).rejects.toThrow('Invalid template file'); // THEN diff --git a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts index 4adb638a3..d7020bad6 100644 --- a/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts +++ b/packages/@aws-cdk/toolkit-lib/test/util/aws-cdk.ts @@ -10,5 +10,3 @@ export { setDefaultSTSMocks, restoreSdkMocksToDefault, } from '../../../../aws-cdk/test/util/mock-sdk'; - -export { rootDir } from '../../../../aws-cdk/lib/util/directories'; diff --git a/packages/aws-cdk/lib/api/aws-auth/user-agent.ts b/packages/aws-cdk/lib/api/aws-auth/user-agent.ts index a63601234..8b9385657 100644 --- a/packages/aws-cdk/lib/api/aws-auth/user-agent.ts +++ b/packages/aws-cdk/lib/api/aws-auth/user-agent.ts @@ -1,7 +1,6 @@ import * as path from 'path'; import { readIfPossible } from './util'; -import { cliRootDir } from '../../cli/root-dir'; - +import { bundledPackageRootDir } from '../../util'; /** * Find the package.json from the main toolkit. * @@ -9,7 +8,7 @@ import { cliRootDir } from '../../cli/root-dir'; * Fall back to argv[1], or a standard string if that is undefined for some reason. */ export function defaultCliUserAgent() { - const root = cliRootDir(false); + const root = bundledPackageRootDir(__dirname, false); const pkg = JSON.parse((root ? readIfPossible(path.join(root, 'package.json')) : undefined) ?? '{}'); const name = pkg.name ?? path.basename(process.argv[1] ?? 'cdk-cli'); const version = pkg.version ?? ''; diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts index ec04e15d0..d62071184 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts @@ -5,10 +5,9 @@ import type { BootstrapEnvironmentOptions, BootstrappingParameters } from './boo import { BootstrapStack, bootstrapVersionFromTemplate } from './deploy-bootstrap'; import { legacyBootstrapTemplate } from './legacy-template'; import { warn } from '../../cli/messages'; -import { cliRootDir } from '../../cli/root-dir'; import { IoMessaging } from '../../toolkit/cli-io-host'; import { ToolkitError } from '../../toolkit/error'; -import { loadStructuredFile, serializeStructure } from '../../util'; +import { bundledPackageRootDir, loadStructuredFile, serializeStructure } from '../../util'; import type { SDK, SdkProvider } from '../aws-auth'; import type { SuccessfulDeployStackResult } from '../deployments'; import { Mode } from '../plugin/mode'; @@ -375,7 +374,7 @@ export class Bootstrapper { case 'custom': return loadStructuredFile(this.source.templateFile); case 'default': - return loadStructuredFile(path.join(cliRootDir(), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml')); + return loadStructuredFile(path.join(bundledPackageRootDir(__dirname), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml')); case 'legacy': return legacyBootstrapTemplate(params); } From afdbef88c48ef742fb34cff30c6477468ee41952 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Mon, 3 Mar 2025 17:49:01 +0000 Subject: [PATCH 41/41] public bootstrap types --- packages/@aws-cdk/toolkit-lib/lib/actions/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts index c40d4f6ed..add78a1be 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts @@ -1,3 +1,4 @@ +export * from './bootstrap'; export * from './deploy'; export * from './destroy'; export * from './list';