From dcf1d74e12674f19306ddf93e36a04c2295406ac Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Tue, 24 Dec 2024 02:56:21 +0100 Subject: [PATCH] chore(toolkit): add proposed initial interfaces (#32643) ### Issue Close #32567 ### Reason for this change Add proposed interfaces and stub classes to the `@aws-cdk/toolkit` package according to RFC 300, so integrators can start designing their own solution. ### Describe any new or updated permissions being added n/a ### Description of how you validated changes n/a unused stub interfaces ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/toolkit/lib/actions/deploy.ts | 228 ++++++++++++++++++ .../@aws-cdk/toolkit/lib/actions/destroy.ts | 13 + .../@aws-cdk/toolkit/lib/actions/import.ts | 24 ++ .../@aws-cdk/toolkit/lib/actions/synth.ts | 14 ++ .../@aws-cdk/toolkit/lib/actions/watch.ts | 11 + .../toolkit/lib/cloud-assembly-source.ts | 72 ++++++ packages/@aws-cdk/toolkit/lib/index.ts | 11 +- packages/@aws-cdk/toolkit/lib/io-host.ts | 30 +++ packages/@aws-cdk/toolkit/lib/toolkit.ts | 32 ++- packages/@aws-cdk/toolkit/lib/types.ts | 78 ++++++ packages/@aws-cdk/toolkit/package.json | 3 + packages/@aws-cdk/toolkit/tsconfig.json | 11 +- 12 files changed, 520 insertions(+), 7 deletions(-) create mode 100644 packages/@aws-cdk/toolkit/lib/actions/deploy.ts create mode 100644 packages/@aws-cdk/toolkit/lib/actions/destroy.ts create mode 100644 packages/@aws-cdk/toolkit/lib/actions/import.ts create mode 100644 packages/@aws-cdk/toolkit/lib/actions/synth.ts create mode 100644 packages/@aws-cdk/toolkit/lib/actions/watch.ts create mode 100644 packages/@aws-cdk/toolkit/lib/cloud-assembly-source.ts create mode 100644 packages/@aws-cdk/toolkit/lib/io-host.ts create mode 100644 packages/@aws-cdk/toolkit/lib/types.ts diff --git a/packages/@aws-cdk/toolkit/lib/actions/deploy.ts b/packages/@aws-cdk/toolkit/lib/actions/deploy.ts new file mode 100644 index 0000000000000..6018ae37a6fe4 --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/actions/deploy.ts @@ -0,0 +1,228 @@ +import { StackSelector } from '../types'; + +export type DeploymentMethod = DirectDeploymentMethod | ChangeSetDeploymentMethod; + +export interface DirectDeploymentMethod { + /** + * Use stack APIs to the deploy stack changes + */ + readonly method: 'direct'; +} + +export interface ChangeSetDeploymentMethod { + /** + * Use change-set APIS to deploy a stack changes + */ + readonly method: 'change-set'; + + /** + * Whether to execute the changeset or leave it in review. + * + * @default true + */ + readonly execute?: boolean; + + /** + * Optional name to use for the CloudFormation change set. + * If not provided, a name will be generated automatically. + */ + readonly changeSetName?: string; +} + +/** + * When to build assets + */ +export enum AssetBuildTime { + /** + * Build all assets before deploying the first stack + * + * This is intended for expensive Docker image builds; so that if the Docker image build + * fails, no stacks are unnecessarily deployed (with the attendant wait time). + */ + ALL_BEFORE_DEPLOY = 'all-before-deploy', + + /** + * Build assets just-in-time, before publishing + */ + JUST_IN_TIME = 'just-in-time', +} + +export interface Tag { + readonly Key: string; + readonly Value: string; +} + +export enum RequireApproval { + NEVER = 'never', + ANY_CHANGE = 'any-change', + BROADENING = 'broadening', +} + +export enum HotswapMode { + /** + * Will fall back to CloudFormation when a non-hotswappable change is detected + */ + FALL_BACK = 'fall-back', + + /** + * Will not fall back to CloudFormation when a non-hotswappable change is detected + */ + HOTSWAP_ONLY = 'hotswap-only', + + /** + * Will not attempt to hotswap anything and instead go straight to CloudFormation + */ + FULL_DEPLOYMENT = 'full-deployment', +} + +export class StackParameters { + /** + * Use only existing parameters on the stack. + */ + public static onlyExisting() { + return new StackParameters({}, true); + }; + + /** + * Use exactly these parameters and remove any other existing parameters from the stack. + */ + public static exactly(params: { [name: string]: string | undefined }) { + return new StackParameters(params, false); + }; + + /** + * Define additional parameters for the stack, while keeping existing parameters for unspecified values. + */ + public static withExisting(params: { [name: string]: string | undefined }) { + return new StackParameters(params, true); + }; + + public readonly parameters: Map; + public readonly keepExistingParameters: boolean; + + private constructor(params: { [name: string]: string | undefined }, usePreviousParameters = true) { + this.keepExistingParameters = usePreviousParameters; + this.parameters = new Map(Object.entries(params)); + } +} + +export interface BaseDeployOptions { + /** + * Criteria for selecting stacks to deploy + */ + readonly stacks: StackSelector; + + /** + * Name of the toolkit stack to use/deploy + * + * @default CDKToolkit + */ + readonly toolkitStackName?: string; + + /** + * Role to pass to CloudFormation for deployment + */ + readonly roleArn?: string; + + /** + * @TODO can this be part of `DeploymentMethod` + * + * Always deploy, even if templates are identical. + * + * @default false + */ + readonly force?: boolean; + + /** + * Deployment method + */ + readonly deploymentMethod?: DeploymentMethod; + + /** + * @TODO can this be part of `DeploymentMethod` + * + * Whether to perform a 'hotswap' deployment. + * A 'hotswap' deployment will attempt to short-circuit CloudFormation + * and update the affected resources like Lambda functions directly. + * + * @default - `HotswapMode.FALL_BACK` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments + */ + readonly hotswap: HotswapMode; + + /** + * Rollback failed deployments + * + * @default true + */ + readonly rollback?: boolean; + + /** + * Reuse the assets with the given asset IDs + */ + readonly reuseAssets?: string[]; + + /** + * Maximum number of simultaneous deployments (dependency permitting) to execute. + * The default is '1', which executes all deployments serially. + * + * @default 1 + */ + readonly concurrency?: number; +} + +export interface DeployOptions extends BaseDeployOptions { + /** + * ARNs of SNS topics that CloudFormation will notify with stack related events + */ + readonly notificationArns?: string[]; + + /** + * What kind of security changes require approval + * + * @default RequireApproval.Broadening + */ + readonly requireApproval?: RequireApproval; + + /** + * Tags to pass to CloudFormation for deployment + */ + readonly tags?: Tag[]; + + /** + * Stack parameters for CloudFormation used at deploy time + * @default StackParameters.onlyExisting() + */ + readonly parameters?: StackParameters; + + /** + * Path to file where stack outputs will be written after a successful deploy as JSON + * @default - Outputs are not written to any file + */ + readonly outputsFile?: string; + + /** + * Whether to show logs from all CloudWatch log groups in the template + * locally in the users terminal + * + * @default - false + */ + readonly traceLogs?: boolean; + + /** + * Build/publish assets for a single stack in parallel + * + * Independent of whether stacks are being done in parallel or no. + * + * @default true + */ + readonly assetParallelism?: boolean; + + /** + * When to build assets + * + * The default is the Docker-friendly default. + * + * @default AssetBuildTime.ALL_BEFORE_DEPLOY + */ + readonly assetBuildTime?: AssetBuildTime; +} diff --git a/packages/@aws-cdk/toolkit/lib/actions/destroy.ts b/packages/@aws-cdk/toolkit/lib/actions/destroy.ts new file mode 100644 index 0000000000000..c2f566cdc8236 --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/actions/destroy.ts @@ -0,0 +1,13 @@ +import { StackSelector } from '../types'; + +export interface DestroyOptions { + /** + * Criteria for selecting stacks to deploy + */ + readonly stacks: StackSelector; + + /** + * The arn of the IAM role to use + */ + readonly roleArn?: string; +} diff --git a/packages/@aws-cdk/toolkit/lib/actions/import.ts b/packages/@aws-cdk/toolkit/lib/actions/import.ts new file mode 100644 index 0000000000000..dcd1e1facb7bb --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/actions/import.ts @@ -0,0 +1,24 @@ +import { BaseDeployOptions } from './deploy'; + +export interface ImportOptions extends Omit { + /** + * Build a physical resource mapping and write it to the given file, without performing the actual import operation + * + * @default - No file + */ + readonly recordResourceMapping?: string; + + /** + * Path to a file with the physical resource mapping to CDK constructs in JSON format + * + * @default - No mapping file + */ + readonly resourceMappingFile?: string; + + /** + * Allow non-addition changes to the template + * + * @default false + */ + readonly force?: boolean; +} diff --git a/packages/@aws-cdk/toolkit/lib/actions/synth.ts b/packages/@aws-cdk/toolkit/lib/actions/synth.ts new file mode 100644 index 0000000000000..0fca66440b1f3 --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/actions/synth.ts @@ -0,0 +1,14 @@ +import { StackSelector } from '../types'; + +export interface SynthOptions { + /** + * Select the stacks + */ + readonly stacks: StackSelector; + + /** + * After synthesis, validate stacks with the "validateOnSynth" attribute set (can also be controlled with CDK_VALIDATION) + * @default true + */ + readonly validateStacks?: boolean; +} diff --git a/packages/@aws-cdk/toolkit/lib/actions/watch.ts b/packages/@aws-cdk/toolkit/lib/actions/watch.ts new file mode 100644 index 0000000000000..1298e993c9fe3 --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/actions/watch.ts @@ -0,0 +1,11 @@ +import { BaseDeployOptions } from './deploy'; + +export interface WatchOptions extends BaseDeployOptions { + /** + * Whether to show CloudWatch logs for hotswapped resources + * locally in the users terminal + * + * @default - false + */ + readonly traceLogs?: boolean; +} diff --git a/packages/@aws-cdk/toolkit/lib/cloud-assembly-source.ts b/packages/@aws-cdk/toolkit/lib/cloud-assembly-source.ts new file mode 100644 index 0000000000000..5973d0cc144e1 --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/cloud-assembly-source.ts @@ -0,0 +1,72 @@ +import { CloudAssembly } from '@aws-cdk/cx-api'; + +export interface ICloudAssemblySource { + /** + * produce + */ + produce(): Promise; +} + +/** + * Configuration for creating a CLI from an AWS CDK App directory + */ +export interface CdkAppDirectoryProps { + /** + * Command-line for executing your app or a cloud assembly directory + * e.g. "node bin/my-app.js" + * or + * "cdk.out" + * + * @default - read from cdk.json + */ + readonly app?: string; + + /** + * Emits the synthesized cloud assembly into a directory + * + * @default cdk.out + */ + readonly output?: string; +} + +export class CloudAssemblySource implements ICloudAssemblySource { + /** + * Use a directory containing an AWS CDK app as source. + * @param directory the directory of the AWS CDK app. Defaults to the current working directory. + * @param props additional configuration properties + * @returns an instance of `AwsCdkCli` + */ + public static fromCdkAppDirectory(_directory?: string, _props: CdkAppDirectoryProps = {}) {} + + /** + * Create the CLI from a Cloud Assembly builder function. + */ + public static fromAssemblyBuilder(_builder: (context: Record) => Promise) {} + + public produce(): Promise { + throw new Error('Method not implemented.'); + } +} + +/** + * A CloudAssemblySource that is caching its result once produced. + * + * Most Toolkit interactions should use a cached source. + * Not caching is relevant when the source changes frequently + * and it is to expensive to predict if the source has changed. + */ +export class CachedCloudAssemblySource implements ICloudAssemblySource { + private source: ICloudAssemblySource; + private cloudAssembly: CloudAssembly | undefined; + + public constructor(source: ICloudAssemblySource) { + this.source = source; + } + + public async produce(): Promise { + if (!this.cloudAssembly) { + this.cloudAssembly = await this.source.produce(); + } + return this.cloudAssembly; + } +} diff --git a/packages/@aws-cdk/toolkit/lib/index.ts b/packages/@aws-cdk/toolkit/lib/index.ts index 8cfba82f758f0..567c3c4ae0e7e 100644 --- a/packages/@aws-cdk/toolkit/lib/index.ts +++ b/packages/@aws-cdk/toolkit/lib/index.ts @@ -1 +1,10 @@ -export class Toolkit {} +export * from './toolkit'; +export * from './cloud-assembly-source'; +export * from './actions/deploy'; +export * from './actions/destroy'; +export * from './actions/import'; +export * from './actions/synth'; +export * from './actions/watch'; + +export * from './io-host'; +export * from './types'; diff --git a/packages/@aws-cdk/toolkit/lib/io-host.ts b/packages/@aws-cdk/toolkit/lib/io-host.ts new file mode 100644 index 0000000000000..7038bf89138a2 --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/io-host.ts @@ -0,0 +1,30 @@ +import { MessageLevel, ToolkitAction } from './types'; + +export interface IoMessage { + time: string; + level: MessageLevel; + action: ToolkitAction; + code: string; + message: string; + data?: T; +} + +export interface IoRequest extends IoMessage { + defaultResponse: U; +} + +export interface IIoHost { + /** + * Notifies the host of a message. + * The caller waits until the notification completes. + */ + notify(msg: IoMessage): Promise; + + /** + * Notifies the host of a message that requires a response. + * + * If the host does not return a response the suggested + * default response from the input message will be used. + */ + requestResponse(msg: IoRequest): Promise; +} diff --git a/packages/@aws-cdk/toolkit/lib/toolkit.ts b/packages/@aws-cdk/toolkit/lib/toolkit.ts index 8cfba82f758f0..e442cc28a7bf9 100644 --- a/packages/@aws-cdk/toolkit/lib/toolkit.ts +++ b/packages/@aws-cdk/toolkit/lib/toolkit.ts @@ -1 +1,31 @@ -export class Toolkit {} + +import { DeployOptions } from './actions/deploy'; +import { DestroyOptions } from './actions/destroy'; +import { SynthOptions } from './actions/synth'; +import { WatchOptions } from './actions/watch'; +import { ICloudAssemblySource } from './cloud-assembly-source'; +import { IIoHost } from './io-host'; + +export interface ToolkitOptions { + ioHost: IIoHost; +} + +export class Toolkit { + public constructor(_options: ToolkitOptions) {} + + public async synth(_cx: ICloudAssemblySource, _options: SynthOptions): Promise { + throw new Error('Not implemented yet'); + } + + public async deploy(_cx: ICloudAssemblySource, _options: DeployOptions): Promise { + throw new Error('Not implemented yet'); + } + + public async watch(_cx: ICloudAssemblySource, _options: WatchOptions): Promise { + throw new Error('Not implemented yet'); + } + + public async destroy(_cx: ICloudAssemblySource, _options: DestroyOptions): Promise { + throw new Error('Not implemented yet'); + } +} diff --git a/packages/@aws-cdk/toolkit/lib/types.ts b/packages/@aws-cdk/toolkit/lib/types.ts new file mode 100644 index 0000000000000..459c3b9a04f22 --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/types.ts @@ -0,0 +1,78 @@ +export type ToolkitAction = + | 'bootstrap' + | 'synth' + | 'list' + | 'deploy' + | 'destroy'; + +export type MessageLevel = 'error' | 'warn' | 'info' | 'debug'; + +export enum StackSelectionStrategy { + /** + * Returns an empty selection in case there are no stacks. + */ + NONE = 'none', + + /** + * If the app includes a single stack, returns it. Otherwise throws an exception. + * This behavior is used by "deploy". + */ + ONLY_SINGLE = 'single', + + /** + * Throws an exception if the app doesn't contain at least one stack. + */ + AT_LEAST_ONE = 'at-least-one', + + /** + * Returns all stacks in the main (top level) assembly only. + */ + MAIN_ASSEMBLY = 'main', + + /** + * If no selectors are provided, returns all stacks in the app, + * including stacks inside nested assemblies. + */ + ALL_STACKS = 'all', +} + +/** + * When selecting stacks, what other stacks to include because of dependencies + */ +export enum ExtendedStackSelection { + /** + * Don't select any extra stacks + */ + NONE = 'none', + + /** + * Include stacks that this stack depends on + */ + UPSTREAM = 'upstream', + + /** + * Include stacks that depend on this stack + */ + DOWNSTREAM = 'downstream', +} + +/** + * A specification of which stacks should be selected + */ +export interface StackSelector { + /** + * A list of patterns to match the stack hierarchical ids + */ + patterns: string[]; + + /** + * Extend the selection to upstream/downstream stacks + * @default ExtendedStackSelection.None only select the specified stacks. + */ + extend?: ExtendedStackSelection; + + /** + * The behavior if if no selectors are provided. + */ + strategy: StackSelectionStrategy; +} diff --git a/packages/@aws-cdk/toolkit/package.json b/packages/@aws-cdk/toolkit/package.json index 79556306607ef..360f1ce16b743 100644 --- a/packages/@aws-cdk/toolkit/package.json +++ b/packages/@aws-cdk/toolkit/package.json @@ -33,6 +33,9 @@ "jest": "^29.7.0", "typescript": "~5.6.3" }, + "dependencies": { + "@aws-cdk/cx-api": "0.0.0" + }, "repository": { "url": "https://github.com/aws/aws-cdk.git", "type": "git", diff --git a/packages/@aws-cdk/toolkit/tsconfig.json b/packages/@aws-cdk/toolkit/tsconfig.json index 03f5bccb17a54..fc285ab52c7c5 100644 --- a/packages/@aws-cdk/toolkit/tsconfig.json +++ b/packages/@aws-cdk/toolkit/tsconfig.json @@ -11,13 +11,14 @@ "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "module": "NodeNext", - "outDir": "dist", "declaration": true, - "composite": true, "declarationMap": true, - "sourceMap": true, - "lib": ["es2022"] + "inlineSourceMap": true, + "lib": ["es2022"], + "composite": true, + "tsBuildInfoFile": "tsconfig.tsbuildinfo" }, "include": ["**/*.ts"], - "exclude": ["node_modules", "**/*.d.ts", "dist"] + "exclude": ["node_modules", "**/*.d.ts", "dist"], + "references": [{ "path": "../cx-api" }] }