Skip to content

Commit 2db536e

Browse files
authored
feat(toolkit): add 'cdk context' command (#1169)
Add a command to view and manage cached context values. Fixes #311.
1 parent 29b611f commit 2db536e

File tree

9 files changed

+21310
-7
lines changed

9 files changed

+21310
-7
lines changed

.gitallowed

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ account: '000000000000'
55
account: '111111111111'
66
account: '333333333333'
77
# Account patterns used in the CHANGELOG
8-
account: '123456789012'
8+
account: '123456789012'
9+
123456789012

docs/src/context.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,58 @@ The |cdk| currently supports the following context providers.
6969
}
7070
});
7171
const vpc = VpcNetworkRef.import(this, 'VPC', provider.vpcProps);
72+
73+
74+
###########################
75+
Viewing and managing context
76+
###########################
77+
78+
Context is used to retrieve things like Availability Zones in your account, or
79+
AMI IDs used to start your instances. In order to avoid unexpected changes to
80+
your deployments-- let's say you were adding a ``Queue`` to your application but
81+
it happened that a new Amazon Linux AMI was released and all of a sudden your
82+
AutoScalingGroup will change-- we store the context values in ``cdk.json``, so
83+
after they've been retrieved once we can be sure we're using the same value on
84+
the next synthesis.
85+
86+
To have a look at the context values stored for your application, run ``cdk
87+
context``. You will see something like the following:
88+
89+
.. code::
90+
91+
$ cdk context
92+
93+
Context found in cdk.json:
94+
95+
┌───┬────────────────────────────────────────────────────┬────────────────────────────────────────────────────┐
96+
│ # │ Key │ Value │
97+
├───┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────┤
98+
│ 1 │ availability-zones:account=123456789012:region=us- │ [ "us-east-1a", "us-east-1b", "us-east-1c", │
99+
│ │ east-1 │ "us-east-1d", "us-east-1e", "us-east-1f" ] │
100+
├───┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────┤
101+
│ 2 │ ssm:account=123456789012:parameterName=/aws/ │ "ami-013be31976ca2c322" │
102+
│ │ service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_ │ │
103+
│ │ 64-gp2:region=us-east-1 │ │
104+
└───┴────────────────────────────────────────────────────┴────────────────────────────────────────────────────┘
105+
106+
Run cdk context --reset KEY_OR_NUMBER to remove a context key. It will be refreshed on the next CDK synthesis run.
107+
108+
At some point, we *do* want to update to the latest version of the Amazon Linux
109+
AMI. To do a controlled update of the context value, reset it and
110+
synthesize again:
111+
112+
.. code::
113+
114+
$ cdk context --reset 2
115+
Context value
116+
ssm:account=123456789012:parameterName=/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2:region=us-east-1
117+
reset. It will be refreshed on the next SDK synthesis run.
118+
119+
$ cdk synth
120+
...
121+
122+
To clear all context values, run:
123+
124+
.. code::
125+
126+
$ cdk context --clear

packages/aws-cdk/bin/cdk.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { data, debug, error, highlight, print, setVerbose, success, warning } fr
1919
import { PluginHost } from '../lib/plugin';
2020
import { parseRenames } from '../lib/renames';
2121
import { deserializeStructure, serializeStructure } from '../lib/serialize';
22-
import { DEFAULTS, PER_USER_DEFAULTS, Settings } from '../lib/settings';
22+
import { loadProjectConfig, loadUserConfig, PER_USER_DEFAULTS, saveProjectConfig, Settings } from '../lib/settings';
2323
import { VERSION } from '../lib/version';
2424

2525
// tslint:disable-next-line:no-var-requires
@@ -85,6 +85,13 @@ async function parseCommandLineArguments() {
8585
* Decorates commands discovered by ``yargs.commandDir`` in order to apply global
8686
* options as appropriate.
8787
*
88+
* Command handlers are supposed to be (args) => void, but ours are actually
89+
* (args) => Promise<number>, so we deal with the asyncness by copying the actual
90+
* handler object to `args.commandHandler` which will be 'await'ed later on
91+
* (instead of awaiting 'main').
92+
*
93+
* Also adds exception handling so individual command handlers don't all have to do it.
94+
*
8895
* @param commandObject is the command to be decorated.
8996
* @returns a decorated ``CommandModule``.
9097
*/
@@ -95,7 +102,22 @@ function decorateCommand(commandObject: yargs.CommandModule): yargs.CommandModul
95102
if (args.verbose) {
96103
setVerbose();
97104
}
98-
return args.result = commandObject.handler(args);
105+
args.commandHandler = wrapExceptionHandler(args.verbose, commandObject.handler as any)(args);
106+
}
107+
};
108+
}
109+
110+
function wrapExceptionHandler(verbose: boolean, fn: (args: any) => Promise<number>) {
111+
return async (a: any) => {
112+
try {
113+
return await fn(a);
114+
} catch (e) {
115+
if (verbose) {
116+
error(e);
117+
} else {
118+
error(e.message);
119+
}
120+
return 1;
99121
}
100122
};
101123
}
@@ -116,8 +138,8 @@ async function initCommandLine() {
116138
});
117139

118140
const defaultConfig = new Settings({ versionReporting: true });
119-
const userConfig = await new Settings().load(PER_USER_DEFAULTS);
120-
const projectConfig = await new Settings().load(DEFAULTS);
141+
const userConfig = await loadUserConfig();
142+
const projectConfig = await loadProjectConfig();
121143
const commandLineArguments = argumentsToSettings();
122144
const renames = parseRenames(argv.rename);
123145

@@ -156,7 +178,7 @@ async function initCommandLine() {
156178

157179
const cmd = argv._[0];
158180

159-
const returnValue = await (argv.result || main(cmd, argv));
181+
const returnValue = await (argv.commandHandler || main(cmd, argv));
160182
if (typeof returnValue === 'object') {
161183
return toJsonOrYaml(returnValue);
162184
} else if (typeof returnValue === 'string') {
@@ -381,7 +403,7 @@ async function initCommandLine() {
381403
await contextproviders.provideContextValues(allMissing, projectConfig, aws);
382404
383405
// Cache the new context to disk
384-
await projectConfig.save(DEFAULTS);
406+
await saveProjectConfig(projectConfig);
385407
config = completeConfig();
386408
387409
continue;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
scriptdir=$(cd $(dirname $0) && pwd)
4+
source ${scriptdir}/common.bash
5+
# ----------------------------------------------------------
6+
7+
rm -rf /tmp/cdk-integ-test
8+
mkdir -p /tmp/cdk-integ-test
9+
cd /tmp/cdk-integ-test
10+
11+
cat > cdk.json <<HERE
12+
{
13+
"context": {
14+
"contextkey": "this is the context value"
15+
}
16+
}
17+
HERE
18+
19+
20+
echo "Testing for the context value"
21+
cdk context 2>&1 | grep "this is the context value" > /dev/null
22+
23+
# Test that deleting the contextkey works
24+
cdk context --reset contextkey
25+
cdk context 2>&1 | grep "this is the context value" > /dev/null && { echo "Should not contain key"; exit 1; } || true
26+
27+
# Test that forced delete of the context key does not error
28+
cdk context -f --reset contextkey
29+
30+
echo "✅ success"

packages/aws-cdk/integ-tests/test-cdk-deploy-with-role.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ aws iam put-role-policy \
3838
}]
3939
}')
4040

41+
echo "Sleeping a bit to improve chances of the role having propagated"
42+
sleep 5
43+
4144
setup
4245

4346
stack_arn=$(cdk --role-arn $role_arn deploy cdk-toolkit-integration-test-2)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import colors = require('colors/safe');
2+
import table = require('table');
3+
import yargs = require('yargs');
4+
import { print } from '../../lib/logging';
5+
import { DEFAULTS, loadProjectConfig, saveProjectConfig } from '../settings';
6+
7+
export const command = 'context';
8+
export const describe = 'Manage cached context values';
9+
export const builder = {
10+
reset: {
11+
alias: 'e',
12+
desc: 'The context key (or its index) to reset',
13+
type: 'string',
14+
requiresArg: 'KEY'
15+
},
16+
clear: {
17+
desc: 'Clear all context',
18+
type: 'boolean',
19+
},
20+
};
21+
22+
export async function handler(args: yargs.Arguments): Promise<number> {
23+
const settings = await loadProjectConfig();
24+
const context = settings.get(['context']) || {};
25+
26+
if (args.clear) {
27+
settings.set(['context'], {});
28+
await saveProjectConfig(settings);
29+
print('All context values cleared.');
30+
} else if (args.reset) {
31+
invalidateContext(context, args.reset);
32+
settings.set(['context'], context);
33+
await saveProjectConfig(settings);
34+
} else {
35+
// List -- support '--json' flag
36+
if (args.json) {
37+
process.stdout.write(JSON.stringify(context, undefined, 2));
38+
} else {
39+
listContext(context);
40+
}
41+
}
42+
43+
return 0;
44+
}
45+
46+
function listContext(context: any) {
47+
const keys = contextKeys(context);
48+
49+
// Print config by default
50+
const data: any[] = [[colors.green('#'), colors.green('Key'), colors.green('Value')]];
51+
for (const [i, key] of keys) {
52+
const jsonWithoutNewlines = JSON.stringify(context[key], undefined, 2).replace(/\s+/g, ' ');
53+
data.push([i, key, jsonWithoutNewlines]);
54+
}
55+
56+
print(`Context found in ${colors.blue(DEFAULTS)}:\n`);
57+
58+
print(table.table(data, {
59+
border: table.getBorderCharacters('norc'),
60+
columns: {
61+
1: { width: 50, wrapWord: true } as any,
62+
2: { width: 50, wrapWord: true } as any
63+
}
64+
}));
65+
66+
// tslint:disable-next-line:max-line-length
67+
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.`);
68+
}
69+
70+
function invalidateContext(context: any, key: string) {
71+
const i = parseInt(key, 10);
72+
if (`${i}` === key) {
73+
// Twas a number and we fully parsed it.
74+
key = keyByNumber(context, i);
75+
}
76+
77+
// Unset!
78+
if (key in context) {
79+
delete context[key];
80+
print(`Context value ${colors.blue(key)} reset. It will be refreshed on the next SDK synthesis run.`);
81+
} else {
82+
print(`No context value with key ${colors.blue(key)}`);
83+
}
84+
}
85+
86+
function keyByNumber(context: any, n: number) {
87+
for (const [i, key] of contextKeys(context)) {
88+
if (n === i) {
89+
return key;
90+
}
91+
}
92+
throw new Error(`No context key with number: ${n}`);
93+
}
94+
95+
/**
96+
* Return enumerated keys in a definitive order
97+
*/
98+
function contextKeys(context: any) {
99+
const keys = Object.keys(context);
100+
keys.sort();
101+
return enumerate1(keys);
102+
}
103+
104+
function enumerate1<T>(xs: T[]): Array<[number, T]> {
105+
const ret = new Array<[number, T]>();
106+
let i = 1;
107+
for (const x of xs) {
108+
ret.push([i, x]);
109+
i += 1;
110+
}
111+
return ret;
112+
}

packages/aws-cdk/lib/settings.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ export type SettingsMap = {[key: string]: any};
99
export const DEFAULTS = 'cdk.json';
1010
export const PER_USER_DEFAULTS = '~/.cdk.json';
1111

12+
export async function loadUserConfig() {
13+
return new Settings().load(PER_USER_DEFAULTS);
14+
}
15+
16+
export async function loadProjectConfig() {
17+
return new Settings().load(DEFAULTS);
18+
}
19+
20+
export async function saveProjectConfig(settings: Settings) {
21+
return settings.save(DEFAULTS);
22+
}
23+
1224
export class Settings {
1325
public static mergeAll(...settings: Settings[]): Settings {
1426
let ret = new Settings();

0 commit comments

Comments
 (0)