packages/aws-cdk/lib/commands/context.ts (138 lines of code) (raw):
import * as chalk from 'chalk';
import { minimatch } from 'minimatch';
import { ToolkitError } from '../../../@aws-cdk/toolkit-lib/lib/api';
import type { Context } from '../api/context';
import { renderTable } from '../cli/tables';
import { PROJECT_CONFIG, PROJECT_CONTEXT, USER_DEFAULTS } from '../cli/user-configuration';
import * as version from '../cli/version';
import { error, warning, info, result } from '../logging';
/**
* Options for the context command
*/
export interface ContextOptions {
/**
* The context object sourced from all context locations
*/
context: Context;
/**
* The context key (or its index) to reset
*
* @default undefined
*/
reset?: string;
/**
* Ignore missing key error
*
* @default false
*/
force?: boolean;
/**
* Clear all context
*
* @default false
*/
clear?: boolean;
/**
* Use JSON output instead of YAML when templates are printed to STDOUT
*
* @default false
*/
json?: boolean;
}
export async function contextHandler(options: ContextOptions): Promise<number> {
if (options.clear) {
options.context.clear();
await options.context.save(PROJECT_CONTEXT);
info('All context values cleared.');
} else if (options.reset) {
invalidateContext(options.context, options.reset, options.force ?? false);
await options.context.save(PROJECT_CONTEXT);
} else {
// List -- support '--json' flag
if (options.json) {
/* c8 ignore start */
const contextValues = options.context.all;
result(JSON.stringify(contextValues, undefined, 2));
/* c8 ignore stop */
} else {
listContext(options.context);
}
}
await version.displayVersionMessage();
return 0;
}
function listContext(context: Context) {
const keys = contextKeys(context);
if (keys.length === 0) {
info('This CDK application does not have any saved context values yet.');
info('');
info('Context will automatically be saved when you synthesize CDK apps');
info('that use environment context information like AZ information, VPCs,');
info('SSM parameters, and so on.');
return;
}
// Print config by default
const data_out: any[] = [[chalk.green('#'), chalk.green('Key'), chalk.green('Value')]];
for (const [i, key] of keys) {
const jsonWithoutNewlines = JSON.stringify(context.all[key], undefined, 2).replace(/\s+/g, ' ');
data_out.push([i, key, jsonWithoutNewlines]);
}
info('Context found in %s:', chalk.blue(PROJECT_CONFIG));
info('');
info(renderTable(data_out, process.stdout.columns));
// eslint-disable-next-line max-len
info(`Run ${chalk.blue('cdk context --reset KEY_OR_NUMBER')} to remove a context key. It will be refreshed on the next CDK synthesis run.`);
}
function invalidateContext(context: Context, key: string, force: boolean) {
const i = parseInt(key, 10);
if (`${i}` === key) {
// was a number and we fully parsed it.
key = keyByNumber(context, i);
}
// Unset!
if (context.has(key)) {
context.unset(key);
// check if the value was actually unset.
if (!context.has(key)) {
info('Context value %s reset. It will be refreshed on next synthesis', chalk.blue(key));
return;
}
// Value must be in readonly bag
error('Only context values specified in %s can be reset through the CLI', chalk.blue(PROJECT_CONTEXT));
if (!force) {
throw new ToolkitError(`Cannot reset readonly context value with key: ${key}`);
}
}
// check if value is expression matching keys
const matches = keysByExpression(context, key);
if (matches.length > 0) {
matches.forEach((match) => {
context.unset(match);
});
const { unset, readonly } = getUnsetAndReadonly(context, matches);
// output the reset values
printUnset(unset);
// warn about values not reset
printReadonly(readonly);
// throw when none of the matches were reset
if (!force && unset.length === 0) {
throw new ToolkitError('None of the matched context values could be reset');
}
return;
}
if (!force) {
throw new ToolkitError(`No context value matching key: ${key}`);
}
}
function printUnset(unset: string[]) {
if (unset.length === 0) return;
info('The following matched context values reset. They will be refreshed on next synthesis');
unset.forEach((match) => {
info(' %s', match);
});
}
function printReadonly(readonly: string[]) {
if (readonly.length === 0) return;
warning('The following matched context values could not be reset through the CLI');
readonly.forEach((match) => {
info(' %s', match);
});
info('');
info('This usually means they are configured in %s or %s', chalk.blue(PROJECT_CONFIG), chalk.blue(USER_DEFAULTS));
}
function keysByExpression(context: Context, expression: string) {
return context.keys.filter(minimatch.filter(expression));
}
function getUnsetAndReadonly(context: Context, matches: string[]) {
return matches.reduce<{ unset: string[]; readonly: string[] }>((acc, match) => {
if (context.has(match)) {
acc.readonly.push(match);
} else {
acc.unset.push(match);
}
return acc;
}, { unset: [], readonly: [] });
}
function keyByNumber(context: Context, n: number) {
for (const [i, key] of contextKeys(context)) {
if (n === i) {
return key;
}
}
throw new ToolkitError(`No context key with number: ${n}`);
}
/**
* Return enumerated keys in a definitive order
*/
function contextKeys(context: Context): [number, string][] {
const keys = context.keys;
keys.sort();
return enumerate1(keys);
}
function enumerate1<T>(xs: T[]): Array<[number, T]> {
const ret = new Array<[number, T]>();
let i = 1;
for (const x of xs) {
ret.push([i, x]);
i += 1;
}
return ret;
}