Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(toolkit): add 'cdk context' command #1169

Merged
merged 5 commits into from
Nov 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a --json flag that will output the context as JSON for machines?

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}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add --force and this will be no-op (similar to rm -f)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for a semi-interactive usage, where copying the key is a bother and you simply supply the number. Blindly deleting by key # is dangerous, given that the numbering is not unique.

But yeah, just deleting a nonexistant key will be a no-op.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right of course!

}

/**
* 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