Skip to content

Commit

Permalink
feat(toolkit): add 'cdk context' command (#1169)
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 authored Nov 15, 2018
1 parent 29b611f commit 2db536e
Show file tree
Hide file tree
Showing 9 changed files with 21,310 additions and 7 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
55 changes: 55 additions & 0 deletions docs/src/context.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,58 @@ 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 in your account, 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 --reset KEY_OR_NUMBER to remove 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, reset it and
synthesize again:

.. code::
$ cdk context --reset 2
Context value
ssm:account=123456789012:parameterName=/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2:region=us-east-1
reset. It will be refreshed on the next SDK synthesis run.
$ cdk synth
...
To clear all context values, run:

.. code::
$ cdk context --clear
34 changes: 28 additions & 6 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -381,7 +403,7 @@ async function initCommandLine() {
await contextproviders.provideContextValues(allMissing, projectConfig, aws);
// Cache the new context to disk
await projectConfig.save(DEFAULTS);
await saveProjectConfig(projectConfig);
config = completeConfig();
continue;
Expand Down
30 changes: 30 additions & 0 deletions packages/aws-cdk/integ-tests/test-cdk-context.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail
scriptdir=$(cd $(dirname $0) && pwd)
source ${scriptdir}/common.bash
# ----------------------------------------------------------

rm -rf /tmp/cdk-integ-test
mkdir -p /tmp/cdk-integ-test
cd /tmp/cdk-integ-test

cat > cdk.json <<HERE
{
"context": {
"contextkey": "this is the context value"
}
}
HERE


echo "Testing for the context value"
cdk context 2>&1 | grep "this is the context value" > /dev/null

# Test that deleting the contextkey works
cdk context --reset contextkey
cdk context 2>&1 | grep "this is the context value" > /dev/null && { echo "Should not contain key"; exit 1; } || true

# Test that forced delete of the context key does not error
cdk context -f --reset contextkey

echo "✅ success"
3 changes: 3 additions & 0 deletions packages/aws-cdk/integ-tests/test-cdk-deploy-with-role.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ aws iam put-role-policy \
}]
}')

echo "Sleeping a bit to improve chances of the role having propagated"
sleep 5

setup

stack_arn=$(cdk --role-arn $role_arn deploy cdk-toolkit-integration-test-2)
Expand Down
112 changes: 112 additions & 0 deletions packages/aws-cdk/lib/commands/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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 = {
reset: {
alias: 'e',
desc: 'The context key (or its index) to reset',
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.reset) {
invalidateContext(context, args.reset);
settings.set(['context'], context);
await saveProjectConfig(settings);
} else {
// List -- support '--json' flag
if (args.json) {
process.stdout.write(JSON.stringify(context, undefined, 2));
} 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 --reset KEY_OR_NUMBER')} to remove 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) {
delete context[key];
print(`Context value ${colors.blue(key)} reset. It will be refreshed on the next SDK synthesis run.`);
} else {
print(`No context value with key ${colors.blue(key)}`);
}
}

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;
}
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 2db536e

Please sign in to comment.