Skip to content

Commit

Permalink
feat(toolkit): a programmatic toolkit for the AWS CDK
Browse files Browse the repository at this point in the history
  • Loading branch information
mrgrain committed Jan 14, 2025
1 parent c7d6fb6 commit 32d8494
Show file tree
Hide file tree
Showing 35 changed files with 1,589 additions and 202 deletions.
42 changes: 42 additions & 0 deletions packages/@aws-cdk/toolkit/lib/actions/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Deployments } from 'aws-cdk/lib/api/deployments';
import { WorkGraph } from 'aws-cdk/lib/util/work-graph';
import { StackSelector } from '../types';

export type DeploymentMethod = DirectDeploymentMethod | ChangeSetDeploymentMethod;
Expand Down Expand Up @@ -225,4 +227,44 @@ export interface DeployOptions extends BaseDeployOptions {
* @default AssetBuildTime.ALL_BEFORE_DEPLOY
*/
readonly assetBuildTime?: AssetBuildTime;

/**
* Change stack watcher output to CI mode.
*
* @deprecated Implement in IoHost instead
*/
readonly ci?: boolean;
}

export function buildParameterMap(parameters?: Map<string, string | undefined>): { [name: string]: { [name: string]: string | undefined } } {
const parameterMap: {
[name: string]: { [name: string]: string | undefined };
} = {};
parameterMap['*'] = {};

const entries = parameters?.entries() ?? [];
for (const [key, value] of entries) {
const [stack, parameter] = key.split(':', 2) as [string, string | undefined];
if (!parameter) {
parameterMap['*'][stack] = value;
} else {
if (!parameterMap[stack]) {
parameterMap[stack] = {};
}
parameterMap[stack][parameter] = value;
}
}

return parameterMap;
}

/**
* Remove the asset publishing and building from the work graph for assets that are already in place
*/
export async function removePublishedAssets(graph: WorkGraph, deployments: Deployments, options: DeployOptions) {
await graph.removeUnnecessaryAssets(assetNode => deployments.isSingleAssetPublished(assetNode.assetManifest, assetNode.asset, {
stack: assetNode.parentStack,
roleArn: options.roleArn,
stackName: assetNode.parentStack.stackName,
}));
}
7 changes: 7 additions & 0 deletions packages/@aws-cdk/toolkit/lib/actions/destroy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,11 @@ export interface DestroyOptions {
* The arn of the IAM role to use
*/
readonly roleArn?: string;

/**
* Change stack watcher output to CI mode.
*
* @deprecated Implement in IoHost instead
*/
readonly ci?: boolean;
}
89 changes: 89 additions & 0 deletions packages/@aws-cdk/toolkit/lib/actions/diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { StackSelector } from '../types';

export interface CloudFormationDiffOptions {
/**
* Whether to run the diff against the template after the CloudFormation Transforms inside it have been executed
* (as opposed to the original template, the default, which contains the unprocessed Transforms).
*
* @default false
*/
readonly compareAgainstProcessedTemplate?: boolean;
}

export interface ChangeSetDiffOptions extends CloudFormationDiffOptions {
/**
* Enable falling back to template-based diff in case creating the changeset is not possible or results in an error.
*
* Should be used for stacks containing nested stacks or when change set permissions aren't available.
*
* @default true
*/
readonly fallbackToTemplate?: boolean;

/**
* Additional parameters for CloudFormation when creating a diff change set
*
* @default {}
*/
readonly parameters?: { [name: string]: string | undefined };
}

export class DiffMode {
/**
* Use a changeset to compute the diff.
*
* This will create, analyze, and subsequently delete a changeset against the CloudFormation stack.
*/
public static ChangeSet(options: ChangeSetDiffOptions = {}) {}
public static TemplateOnly(options: CloudFormationDiffOptions = {}) {}
public static LocalTemplate(path: string) {}

private constructor(public readonly mode: string) {

}
}

export interface DiffOptions {
/**
* Select the stacks
*/
readonly stacks: StackSelector;

/**
* The mode to create a stack diff.
*
* Use changeset diff for the highest fidelity, including analyze resource replacements.
* In this mode, diff will use the deploy role instead of the lookup role.
*
* Use template-only diff for a faster, less accurate diff that doesn't require
* permissions to create a change-set.
*
* Use local-template diff for a fast, local-only diff that doesn't require
* any permissions or internet access.
*
* @default DiffMode.ChangeSet
*/
readonly mode: DiffMode;

/**
* Strict diff mode
* When enabled, this will not filter out AWS::CDK::Metadata resources, mangled non-ASCII characters, or the CheckBootstrapVersionRule.
*
* @default false
*/
readonly strict?: boolean;

/**
* How many lines of context to show in the diff
*
* @default 3
*/
readonly contextLines?: number;

/**
* Only include broadened security changes in the diff
*
* @default false
*/
readonly securityOnly?: boolean;
}
8 changes: 8 additions & 0 deletions packages/@aws-cdk/toolkit/lib/actions/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { StackSelector } from '../types';

export interface ListOptions {
/**
* Select the stacks
*/
readonly stacks: StackSelector;
}
18 changes: 18 additions & 0 deletions packages/@aws-cdk/toolkit/lib/actions/synth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,21 @@ export interface SynthOptions {
*/
readonly validateStacks?: boolean;
}

/**
* Remove any template elements that we don't want to show users.
*/
export function obscureTemplate(template: any = {}) {
if (template.Rules) {
// see https://github.com/aws/aws-cdk/issues/17942
if (template.Rules.CheckBootstrapVersion) {
if (Object.keys(template.Rules).length > 1) {
delete template.Rules.CheckBootstrapVersion;
} else {
delete template.Rules;
}
}
}

return template;
}
43 changes: 43 additions & 0 deletions packages/@aws-cdk/toolkit/lib/api/aws-auth/sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

/**
* Options for the default SDK provider
*/
export interface SdkOptions {
/**
* Profile to read from ~/.aws
*
* @default - No profile
*/
readonly profile?: string;

/**
* Proxy address to use
*
* @default No proxy
*/
readonly region?: string;

/**
* HTTP options for SDK
*/
readonly httpOptions?: SdkHttpOptions;
}

/**
* Options for individual SDKs
*/
export interface SdkHttpOptions {
/**
* Proxy address to use
*
* @default No proxy
*/
readonly proxyAddress?: string;

/**
* A path to a certificate bundle that contains a cert to be trusted.
*
* @default No certificate bundle
*/
readonly caBundlePath?: string;
}
25 changes: 25 additions & 0 deletions packages/@aws-cdk/toolkit/lib/api/cloud-assembly/cached-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CloudAssembly } from '@aws-cdk/cx-api';
import { ICloudAssemblySource } from './types';

/**
* 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<CloudAssembly> {
if (!this.cloudAssembly) {
this.cloudAssembly = await this.source.produce();
}
return this.cloudAssembly;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { MissingContext } from '@aws-cdk/cloud-assembly-schema';
import * as cxapi from '@aws-cdk/cx-api';
import { SdkProvider } from 'aws-cdk/lib/api/aws-auth';
import * as contextproviders from 'aws-cdk/lib/context-providers';
import { debug } from 'aws-cdk/lib/logging';
import { Context, PROJECT_CONTEXT } from 'aws-cdk/lib/settings';
import { ICloudAssemblySource } from './types';
import { ToolkitError } from '../errors';

export interface CloudExecutableProps {
/**
* AWS object (used by contextprovider)
*/
readonly sdkProvider: SdkProvider;

/**
* Application context
*/
readonly context: Context;

/**
* The file used to store application context in (relative to cwd).
*
* @default "cdk.context.json"
*/
readonly contextFile?: string;

/**
* Enable context lookups.
*
* Producing a `cxapi.CloudAssembly` will fail if this is disabled and context lookups need to be performed.
*
* @default true
*/
readonly lookups?: boolean;
}

/**
* Represent the Cloud Executable and the synthesis we can do on it
*/
export class ContextAwareCloudAssembly implements ICloudAssemblySource {
private canLookup: boolean;
private context: Context;
private contextFile: string;

constructor(private readonly source: ICloudAssemblySource, private readonly props: CloudExecutableProps) {
this.canLookup = props.lookups ?? true;
this.context = props.context;
this.contextFile = props.contextFile ?? PROJECT_CONTEXT; // @todo new feature not needed right now
}

/**
* Produce a Cloud Assembly, i.e. a set of stacks
*/
public async produce(): Promise<cxapi.CloudAssembly> {
// We may need to run the cloud executable multiple times in order to satisfy all missing context
// (When the executable runs, it will tell us about context it wants to use
// but it missing. We'll then look up the context and run the executable again, and
// again, until it doesn't complain anymore or we've stopped making progress).
let previouslyMissingKeys: Set<string> | undefined;
while (true) {
const assembly = await this.source.produce();

if (assembly.manifest.missing && assembly.manifest.missing.length > 0) {
const missingKeys = missingContextKeys(assembly.manifest.missing);

if (!this.canLookup) {
throw new ToolkitError(
'Context lookups have been disabled. '
+ 'Make sure all necessary context is already in \'cdk.context.json\' by running \'cdk synth\' on a machine with sufficient AWS credentials and committing the result. '
+ `Missing context keys: '${Array.from(missingKeys).join(', ')}'`);
}

let tryLookup = true;
if (previouslyMissingKeys && equalSets(missingKeys, previouslyMissingKeys)) {
debug('Not making progress trying to resolve environmental context. Giving up.');
tryLookup = false;
}

previouslyMissingKeys = missingKeys;

if (tryLookup) {
debug('Some context information is missing. Fetching...');

await contextproviders.provideContextValues(
assembly.manifest.missing,
this.context,
this.props.sdkProvider,
);

// Cache the new context to disk
await this.context.save(this.contextFile);

// Execute again
continue;
}
}

return assembly;
}
}

}

/**
* Return all keys of missing context items
*/
function missingContextKeys(missing?: MissingContext[]): Set<string> {
return new Set((missing || []).map(m => m.key));
}

/**
* Are two sets equal to each other
*/
function equalSets<A>(a: Set<A>, b: Set<A>) {
if (a.size !== b.size) { return false; }
for (const x of a) {
if (!b.has(x)) { return false; }
}
return true;
}
Loading

0 comments on commit 32d8494

Please sign in to comment.