Skip to content

Commit

Permalink
feat(toolkit): add 'cdk context' command
Browse files Browse the repository at this point in the history
Add a command to view and manage cached context values.

Fixes #311.
  • Loading branch information
rix0rrr committed Nov 14, 2018
1 parent 89d5266 commit bf50850
Show file tree
Hide file tree
Showing 9 changed files with 21,289 additions and 23 deletions.
3 changes: 2 additions & 1 deletion .gitallowed
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ account: '000000000000'
account: '111111111111'
account: '333333333333'
# Account patterns used in the CHANGELOG
account: '123456789012'
account: '123456789012'
123456789012
49 changes: 49 additions & 0 deletions docs/src/context.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,52 @@ The |cdk| currently supports the following context providers.
}
});
const vpc = VpcNetworkRef.import(this, 'VPC', provider.vpcProps);
###########################
Viewing and managing context
###########################

Context is used to retrieve things like Availability Zones available to you, or
AMI IDs used to start your instances. In order to avoid unexpected changes to
your deployments-- let's say you were adding a ``Queue`` to your application but
it happened that a new Amazon Linux AMI was released and all of a sudden your
AutoScalingGroup will change-- we store the context values in ``cdk.json``, so
after they've been retrieved once we can be sure we're using the same value on
the next synthesis.

To have a look at the context values stored for your application, run ``cdk
context``. You will see something like the following:

.. code::
$ cdk context
Context found in cdk.json:
┌───┬────────────────────────────────────────────────────┬────────────────────────────────────────────────────┐
│ # │ Key │ Value │
├───┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────┤
│ 1 │ availability-zones:account=123456789012:region=us- │ [ "us-east-1a", "us-east-1b", "us-east-1c", │
│ │ east-1 │ "us-east-1d", "us-east-1e", "us-east-1f" ] │
├───┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────┤
│ 2 │ ssm:account=123456789012:parameterName=/aws/ │ "ami-013be31976ca2c322" │
│ │ service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_ │ │
│ │ 64-gp2:region=us-east-1 │ │
└───┴────────────────────────────────────────────────────┴────────────────────────────────────────────────────┘
Run cdk context --invalidate KEY_OR_NUMBER to invalidate a context key. It will be refreshed on the next CDK synthesis run.
At some point, we *do* want to update to the latest version of the Amazon Linux
AMI. To do a controlled update of the context value, invalidate it and
synthesize again:

.. code::
$ cdk context --invalidate 2
Context value
ssm:account=123456789012:parameterName=/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2:region=us-east-1
invalidated. It will be refreshed on the next SDK synthesis run.
$ cdk synth
...
38 changes: 30 additions & 8 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import yargs = require('yargs');
import cdkUtil = require('../lib/util');

import { bootstrapEnvironment, deployStack, destroyStack, loadToolkitInfo, Mode, SDK } from '../lib';
import contextplugins = require('../lib/contextplugins');
import contextproviders = require('../lib/context-providers/index');
import { printStackDiff } from '../lib/diff';
import { execProgram } from '../lib/exec';
import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init';
Expand All @@ -19,7 +19,7 @@ import { data, debug, error, highlight, print, setVerbose, success, warning } fr
import { PluginHost } from '../lib/plugin';
import { parseRenames } from '../lib/renames';
import { deserializeStructure, serializeStructure } from '../lib/serialize';
import { DEFAULTS, PER_USER_DEFAULTS, Settings } from '../lib/settings';
import { loadProjectConfig, loadUserConfig, PER_USER_DEFAULTS, saveProjectConfig, Settings } from '../lib/settings';
import { VERSION } from '../lib/version';

// tslint:disable-next-line:no-var-requires
Expand Down Expand Up @@ -85,6 +85,13 @@ async function parseCommandLineArguments() {
* Decorates commands discovered by ``yargs.commandDir`` in order to apply global
* options as appropriate.
*
* Command handlers are supposed to be (args) => void, but ours are actually
* (args) => Promise<number>, so we deal with the asyncness by copying the actual
* handler object to `args.commandHandler` which will be 'await'ed later on
* (instead of awaiting 'main').
*
* Also adds exception handling so individual command handlers don't all have to do it.
*
* @param commandObject is the command to be decorated.
* @returns a decorated ``CommandModule``.
*/
Expand All @@ -95,7 +102,22 @@ function decorateCommand(commandObject: yargs.CommandModule): yargs.CommandModul
if (args.verbose) {
setVerbose();
}
return args.result = commandObject.handler(args);
args.commandHandler = wrapExceptionHandler(args.verbose, commandObject.handler as any)(args);
}
};
}

function wrapExceptionHandler(verbose: boolean, fn: (args: any) => Promise<number>) {
return async (a: any) => {
try {
return await fn(a);
} catch (e) {
if (verbose) {
error(e);
} else {
error(e.message);
}
return 1;
}
};
}
Expand All @@ -116,8 +138,8 @@ async function initCommandLine() {
});

const defaultConfig = new Settings({ versionReporting: true });
const userConfig = await new Settings().load(PER_USER_DEFAULTS);
const projectConfig = await new Settings().load(DEFAULTS);
const userConfig = await loadUserConfig();
const projectConfig = await loadProjectConfig();
const commandLineArguments = argumentsToSettings();
const renames = parseRenames(argv.rename);

Expand Down Expand Up @@ -156,7 +178,7 @@ async function initCommandLine() {

const cmd = argv._[0];

const returnValue = await (argv.result || main(cmd, argv));
const returnValue = await (argv.commandHandler || main(cmd, argv));
if (typeof returnValue === 'object') {
return toJsonOrYaml(returnValue);
} else if (typeof returnValue === 'string') {
Expand Down Expand Up @@ -378,10 +400,10 @@ async function initCommandLine() {
if (!cdkUtil.isEmpty(allMissing)) {
debug(`Some context information is missing. Fetching...`);

await contextplugins.provideContextValues(allMissing, projectConfig, aws);
await contextproviders.provideContextValues(allMissing, projectConfig, aws);

// Cache the new context to disk
await projectConfig.save(DEFAULTS);
await saveProjectConfig(projectConfig);
config = completeConfig();

continue;
Expand Down
108 changes: 108 additions & 0 deletions packages/aws-cdk/lib/commands/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import colors = require('colors/safe');
import table = require('table');
import yargs = require('yargs');
import { print } from '../../lib/logging';
import { DEFAULTS, loadProjectConfig, saveProjectConfig } from '../settings';

export const command = 'context';
export const describe = 'Manage cached context values';
export const builder = {
invalidate: {
alias: 'd',
desc: 'The context key (or its index) to invalidate',
type: 'string',
requiresArg: 'KEY'
},
clear: {
desc: 'Clear all context',
type: 'boolean',
}
};

export async function handler(args: yargs.Arguments): Promise<number> {
const settings = await loadProjectConfig();
const context = settings.get(['context']) || {};

if (args.clear) {
settings.set(['context'], {});
await saveProjectConfig(settings);
print('All context values cleared.');
} else if (args.invalidate) {
invalidateContext(context, args.invalidate);
settings.set(['context'], context);
await saveProjectConfig(settings);
} else {
listContext(context);
}

return 0;
}

function listContext(context: any) {
const keys = contextKeys(context);

// Print config by default
const data: any[] = [[colors.green('#'), colors.green('Key'), colors.green('Value')]];
for (const [i, key] of keys) {
const jsonWithoutNewlines = JSON.stringify(context[key], undefined, 2).replace(/\s+/g, ' ');
data.push([i, key, jsonWithoutNewlines]);
}

print(`Context found in ${colors.blue(DEFAULTS)}:\n`);

print(table.table(data, {
border: table.getBorderCharacters('norc'),
columns: {
1: { width: 50, wrapWord: true } as any,
2: { width: 50, wrapWord: true } as any
}
}));

// tslint:disable-next-line:max-line-length
print(`Run ${colors.blue('cdk context --invalidate KEY_OR_NUMBER')} to invalidate a context key. It will be refreshed on the next CDK synthesis run.`);
}

function invalidateContext(context: any, key: string) {
const i = parseInt(key, 10);
if (`${i}` === key) {
// Twas a number and we fully parsed it.
key = keyByNumber(context, i);
}

// Unset!
if (!(key in context)) {
throw new Error(`No context value with key: ${key}`);
}

delete context[key];

print(`Context value ${colors.blue(key)} invalidated. It will be refreshed on the next SDK synthesis run.`);
}

function keyByNumber(context: any, n: number) {
for (const [i, key] of contextKeys(context)) {
if (n === i) {
return key;
}
}
throw new Error(`No context key with number: ${n}`);
}

/**
* Return enumerated keys in a definitive order
*/
function contextKeys(context: any) {
const keys = Object.keys(context);
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;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import cxapi = require('@aws-cdk/cx-api');
import { SDK } from './api/util/sdk';
import { AZContextProviderPlugin } from './context-providers/availability-zones';
import { HostedZoneContextProviderPlugin } from './context-providers/hosted-zones';
import { ContextProviderPlugin } from './context-providers/provider';
import { SSMContextProviderPlugin } from './context-providers/ssm-parameters';
import { VpcNetworkContextProviderPlugin } from './context-providers/vpcs';
import { debug } from './logging';
import { Settings } from './settings';
import { SDK } from '../api/util/sdk';
import { debug } from '../logging';
import { Settings } from '../settings';
import { AZContextProviderPlugin } from './availability-zones';
import { HostedZoneContextProviderPlugin } from './hosted-zones';
import { ContextProviderPlugin } from './provider';
import { SSMContextProviderPlugin } from './ssm-parameters';
import { VpcNetworkContextProviderPlugin } from './vpcs';

type ProviderConstructor = (new (sdk: SDK) => ContextProviderPlugin);
export type ProviderMap = {[name: string]: ProviderConstructor};
Expand Down
18 changes: 12 additions & 6 deletions packages/aws-cdk/lib/context-providers/vpcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ export class VpcNetworkContextProviderPlugin implements ContextProviderPlugin {
return {
vpcId,
availabilityZones: grouped.azs,
isolatedSubnetIds: flatMap(findGroups(SubnetType.Isolated, grouped), group => group.subnets.map(s => s.subnetId)),
isolatedSubnetNames: flatMap(findGroups(SubnetType.Isolated, grouped), group => group.name ? [group.name] : []),
privateSubnetIds: flatMap(findGroups(SubnetType.Private, grouped), group => group.subnets.map(s => s.subnetId)),
privateSubnetNames: flatMap(findGroups(SubnetType.Private, grouped), group => group.name ? [group.name] : []),
publicSubnetIds: flatMap(findGroups(SubnetType.Public, grouped), group => group.subnets.map(s => s.subnetId)),
publicSubnetNames: flatMap(findGroups(SubnetType.Public, grouped), group => group.name ? [group.name] : []),
isolatedSubnetIds: collapse(flatMap(findGroups(SubnetType.Isolated, grouped), group => group.subnets.map(s => s.subnetId))),
isolatedSubnetNames: collapse(flatMap(findGroups(SubnetType.Isolated, grouped), group => group.name ? [group.name] : [])),
privateSubnetIds: collapse(flatMap(findGroups(SubnetType.Private, grouped), group => group.subnets.map(s => s.subnetId))),
privateSubnetNames: collapse(flatMap(findGroups(SubnetType.Private, grouped), group => group.name ? [group.name] : [])),
publicSubnetIds: collapse(flatMap(findGroups(SubnetType.Public, grouped), group => group.subnets.map(s => s.subnetId))),
publicSubnetNames: collapse(flatMap(findGroups(SubnetType.Public, grouped), group => group.name ? [group.name] : [])),
};
}
}
Expand Down Expand Up @@ -184,4 +184,10 @@ function flatMap<T, U>(xs: T[], fn: (x: T) => U[]): U[] {
ret.push(...fn(x));
}
return ret;
}

function collapse<T>(xs: T[]): T[] | undefined {
if (xs.length > 0) { return xs; }
return undefined;

}
12 changes: 12 additions & 0 deletions packages/aws-cdk/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ export type SettingsMap = {[key: string]: any};
export const DEFAULTS = 'cdk.json';
export const PER_USER_DEFAULTS = '~/.cdk.json';

export async function loadUserConfig() {
return new Settings().load(PER_USER_DEFAULTS);
}

export async function loadProjectConfig() {
return new Settings().load(DEFAULTS);
}

export async function saveProjectConfig(settings: Settings) {
return settings.save(DEFAULTS);
}

export class Settings {
public static mergeAll(...settings: Settings[]): Settings {
let ret = new Settings();
Expand Down
Loading

0 comments on commit bf50850

Please sign in to comment.