Skip to content

Commit

Permalink
feat(toolkit): deployment ui improvements (#1067)
Browse files Browse the repository at this point in the history
A few UI improvements to the toolkit:

- Remove a few emojis (sorry @RomainMuller) to
  reduce clutter.
- Make output more concise.
- Add "Creating changeset..." (because it takes ages)
- Improve log view, align columns
- Print resource path in log view
- Clean up asset logs
- Tone down colors a little
  • Loading branch information
Elad Ben-Israel authored Nov 5, 2018
1 parent 28b28a0 commit c832eaf
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 46 deletions.
32 changes: 21 additions & 11 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,23 +517,33 @@ async function initCommandLine() {
const deployName = renames.finalName(stack.name);
if (deployName !== stack.name) {
success(' ⏳ Starting deployment of stack %s as %s...', colors.blue(stack.name), colors.blue(deployName));
print('%s: deploying... (was %s)', colors.bold(deployName), colors.bold(stack.name));
} else {
success(' ⏳ Starting deployment of stack %s...', colors.blue(stack.name));
print('%s: deploying...', colors.bold(stack.name));
}
try {
const result = await deployStack({ stack, sdk: aws, toolkitInfo, deployName, roleArn });
const message = result.noOp ? ` Stack was already up-to-date, it has ARN ${colors.blue(result.stackArn)}`
: ` Deployment of stack %s completed successfully, it has ARN ${colors.blue(result.stackArn)}`;
data(result.stackArn);
success(message, colors.blue(stack.name));
const message = result.noOp
? ` %s (no changes)`
: ` %s`;
success('\n' + message, stack.name);
if (Object.keys(result.outputs).length > 0) {
print('\nOutputs:');
}
for (const name of Object.keys(result.outputs)) {
const value = result.outputs[name];
print('%s.%s = %s', colors.blue(deployName), colors.blue(name), colors.green(value));
print('%s.%s = %s', colors.cyan(deployName), colors.cyan(name), colors.underline(colors.cyan(value)));
}
print('\nStack ARN:');
data(result.stackArn);
} catch (e) {
error(' ❌ Deployment of stack %s failed: %s', colors.blue(stack.name), e);
error('\n ❌ %s failed: %s', colors.bold(stack.name), e);
throw e;
}
}
Expand All @@ -554,12 +564,12 @@ async function initCommandLine() {
for (const stack of stacks) {
const deployName = renames.finalName(stack.name);
success(' ⏳ Starting destruction of stack %s...', colors.blue(deployName));
success('%s: destroying...', colors.blue(deployName));
try {
await destroyStack({ stack, sdk: aws, deployName, roleArn });
success(' ✅ Stack %s successfully destroyed.', colors.blue(deployName));
success('\n%s: destroyed', colors.blue(deployName));
} catch (e) {
error(' ❌ Destruction failed: %s', colors.blue(deployName), e);
error('\n%s: destroy failed', colors.blue(deployName), e);
throw e;
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/aws-cdk/integ-tests/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ class MyStack extends cdk.Stack {
super(parent, id);
new sns.Topic(this, 'topic');

console.log(new cdk.AvailabilityZoneProvider(this).availabilityZones);
console.log(new cdk.SSMParameterProvider(this, { parameterName: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' }).parameterValue(''));
new cdk.AvailabilityZoneProvider(this).availabilityZones;
new cdk.SSMParameterProvider(this, { parameterName: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' }).parameterValue('');
}
}

Expand Down
13 changes: 13 additions & 0 deletions packages/aws-cdk/integ-tests/common.bash
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
scriptdir=$(cd $(dirname $0) && pwd)

toolkit_bin="${scriptdir}/../bin"

if [ ! -x ${toolkit_bin}/cdk ]; then
echo "Unable to find 'cdk' under ${toolkit_bin}"
exit 1
fi

# make sure "this" toolkit is in the path
export PATH=${toolkit_bin}:$PATH

function cleanup_stack() {
local stack_arn=$1
echo "| ensuring ${stack_arn} is cleaned up"
Expand Down Expand Up @@ -71,6 +83,7 @@ function assert_lines() {

local lines="$(echo "${data}" | wc -l)"
if [ "${lines}" -ne "${expected}" ]; then
echo "${data}"
fail "response has ${lines} lines and we expected ${expected} lines to be returned"
fi
}
3 changes: 3 additions & 0 deletions packages/aws-cdk/integ-tests/test-cdk-deploy-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ echo "Stack deployed successfully"
# verify that we only deployed a single stack (there's a single ARN in the output)
lines="$(echo "${stack_arns}" | wc -l)"
if [ "${lines}" -ne 2 ]; then
echo "-- output -----------"
echo "${stack_arns}"
echo "---------------------"
fail "cdk deploy returned ${lines} arns and we expected 2"
fi

Expand Down
10 changes: 0 additions & 10 deletions packages/aws-cdk/integ-tests/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,6 @@
set -euo pipefail
scriptdir=$(cd $(dirname $0) && pwd)

toolkit_bin="${scriptdir}/../bin"

if [ ! -x ${toolkit_bin}/cdk ]; then
echo "Unable to find 'cdk' under ${toolkit_bin}"
exit 1
fi

# make sure "this" toolkit is in the path
export PATH=${toolkit_bin}:$PATH

cd ${scriptdir}
for test in test-*.sh; do
echo "============================================================================================"
Expand Down
7 changes: 4 additions & 3 deletions packages/aws-cdk/lib/api/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import colors = require('colors/safe');
import YAML = require('js-yaml');
import uuid = require('uuid');
import { prepareAssets } from '../assets';
import { debug, error } from '../logging';
import { debug, error, print } from '../logging';
import { Mode } from './aws-auth/credentials';
import { ToolkitInfo } from './toolkit-info';
import { describeStack, stackExists, stackFailedCreating, waitForChangeSet, waitForStack } from './util/cloudformation';
Expand Down Expand Up @@ -61,6 +61,7 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt

const changeSetName = `CDK-${executionId}`;
debug(`Attempting to create ChangeSet ${changeSetName} to ${update ? 'update' : 'create'} stack ${deployName}`);
print(`%s: creating CloudFormation changeset...`, colors.bold(deployName));
const changeSet = await cfn.createChangeSet({
StackName: deployName,
ChangeSetName: changeSetName,
Expand All @@ -83,7 +84,7 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
debug('Initiating execution of changeset %s on stack %s', changeSetName, deployName);
await cfn.executeChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise();
// tslint:disable-next-line:max-line-length
const monitor = options.quiet ? undefined : new StackActivityMonitor(cfn, deployName, options.stack.metadata, changeSetDescription.Changes.length).start();
const monitor = options.quiet ? undefined : new StackActivityMonitor(cfn, deployName, options.stack, changeSetDescription.Changes.length).start();
debug('Execution of changeset %s on stack %s has started; waiting for the update to complete...', changeSetName, deployName);
await waitForStack(cfn, deployName);
if (monitor) { await monitor.stop(); }
Expand Down Expand Up @@ -153,7 +154,7 @@ export async function destroyStack(options: DestroyStackOptions) {
if (!await stackExists(cfn, deployName)) {
return;
}
const monitor = options.quiet ? undefined : new StackActivityMonitor(cfn, deployName).start();
const monitor = options.quiet ? undefined : new StackActivityMonitor(cfn, deployName, options.stack).start();
await cfn.deleteStack({ StackName: deployName, RoleARN: options.roleArn }).promise().catch(e => { throw e; });
const destroyedStack = await waitForStack(cfn, deployName, false);
if (monitor) { await monitor.stop(); }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,14 @@ export class StackActivityMonitor {
*/
private readPromise?: Promise<AWS.CloudFormation.StackEvent[]>;

/**
* The with of the "resource type" column.
*/
private readonly resourceTypeColumnWidth: number;

constructor(private readonly cfn: aws.CloudFormation,
private readonly stackName: string,
private readonly metadata?: cxapi.StackMetadata,
private readonly stack: cxapi.SynthesizedStack,
private readonly resourcesTotal?: number) {

if (this.resourcesTotal != null) {
Expand All @@ -78,6 +83,8 @@ export class StackActivityMonitor {
// How many digits does this number take to represent?
this.resourceDigits = Math.ceil(Math.log10(this.resourcesTotal));
}

this.resourceTypeColumnWidth = calcMaxResourceTypeLength(this.stack.template);
}

public start() {
Expand Down Expand Up @@ -164,20 +171,33 @@ export class StackActivityMonitor {
const e = activity.event;
const color = this.colorFromStatus(e.ResourceStatus);
const md = this.findMetadataFor(e.LogicalResourceId);
let reasonColor = colors.cyan;

let suffix = '';
let stackTrace = '';
if (md && e.ResourceStatus && e.ResourceStatus.indexOf('FAILED') !== -1) {
suffix = `\n${md.entry.data} was created at: ${md.path}\n\t${md.entry.trace.join('\n\t\\_ ')}`;
stackTrace = `\n\t${md.entry.trace.join('\n\t\\_ ')}`;
reasonColor = colors.red;
}

let resourceName = md ? md.path.replace(/\/Resource$/, '') : (e.LogicalResourceId || '');
resourceName = resourceName.replace(/^\//, ''); // remove "/" prefix

// remove "<stack-name>/" prefix
if (resourceName.startsWith(this.stackName + '/')) {
resourceName = resourceName.substr(this.stackName.length + 1);
}

process.stderr.write(util.format(color(`%s %s %s [%s] %s %s%s\n`),
const logicalId = resourceName !== e.LogicalResourceId ? `(${e.LogicalResourceId}) ` : '';

process.stderr.write(util.format(` %s | %s | %s | %s | %s %s%s%s\n`,
this.progress(),
e.Timestamp,
padRight(18, "" + e.ResourceStatus),
e.ResourceType,
e.LogicalResourceId,
e.ResourceStatusReason ? e.ResourceStatusReason : '',
suffix));
new Date(e.Timestamp).toLocaleTimeString(),
color(padRight(20, (e.ResourceStatus || '').substr(0, 20))), // pad left and trim
padRight(this.resourceTypeColumnWidth, e.ResourceType || ''),
color(colors.bold(resourceName)),
logicalId,
reasonColor(colors.bold(e.ResourceStatusReason ? e.ResourceStatusReason : '')),
reasonColor(stackTrace)));

this.lastPrintTime = Date.now();
}
Expand All @@ -188,10 +208,10 @@ export class StackActivityMonitor {
private progress(): string {
if (this.resourcesTotal == null) {
// Don't have total, show simple count and hope the human knows
return util.format('[%s]', this.resourcesDone);
return padLeft(3, util.format('%s', this.resourcesDone)); // max 200 resources
}

return util.format('[%s/%s]',
return util.format('%s/%s',
padLeft(this.resourceDigits, this.resourcesDone.toString()),
padLeft(this.resourceDigits, this.resourcesTotal != null ? this.resourcesTotal.toString() : '?'));
}
Expand All @@ -204,9 +224,11 @@ export class StackActivityMonitor {
return;
}

process.stderr.write(util.format(colors.blue('%s Currently in progress: %s\n'),
this.progress(),
Array.from(this.resourcesInProgress).join(', ')));
if (this.resourcesInProgress.size > 0) {
process.stderr.write(util.format('%s Currently in progress: %s\n',
this.progress(),
colors.bold(Array.from(this.resourcesInProgress).join(', '))));
}

// We cheat a bit here. To prevent printInProgress() from repeatedly triggering,
// we set the timestamp into the future. It will be reset whenever a regular print
Expand All @@ -215,9 +237,10 @@ export class StackActivityMonitor {
}

private findMetadataFor(logicalId: string | undefined): { entry: cxapi.MetadataEntry, path: string } | undefined {
if (!logicalId || !this.metadata) { return undefined; }
for (const path of Object.keys(this.metadata)) {
const entry = this.metadata[path].filter(e => e.type === 'aws:cdk:logicalId')
const metadata = this.stack.metadata;
if (!logicalId || !metadata) { return undefined; }
for (const path of Object.keys(metadata)) {
const entry = metadata[path].filter(e => e.type === 'aws:cdk:logicalId')
.find(e => e.data === logicalId);
if (entry) { return { entry, path }; }
}
Expand Down Expand Up @@ -284,3 +307,15 @@ function padRight(n: number, x: string): string {
function padLeft(n: number, x: string): string {
return ' '.repeat(Math.max(0, n - x.length)) + x;
}

function calcMaxResourceTypeLength(template: any) {
const resources = (template && template.Resources) || {};
let maxWidth = 0;
for (const id of Object.keys(resources)) {
const type = resources[id].Type || '';
if (type.length > maxWidth) {
maxWidth = type.length;
}
}
return maxWidth;
}
8 changes: 6 additions & 2 deletions packages/aws-cdk/lib/assets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ASSET_METADATA, ASSET_PREFIX_SEPARATOR, AssetMetadataEntry, StackMetadata, SynthesizedStack } from '@aws-cdk/cx-api';
import { CloudFormation } from 'aws-sdk';
import colors = require('colors');
import fs = require('fs-extra');
import os = require('os');
import path = require('path');
Expand Down Expand Up @@ -78,11 +79,14 @@ async function prepareFileAsset(
contentType
});

const relativePath = path.relative(process.cwd(), asset.path);

const s3url = `s3://${toolkitInfo.bucketName}/${key}`;
debug(`S3 url for ${relativePath}: ${s3url}`);
if (changed) {
success(` 👑 Asset ${asset.path} (${asset.packaging}) uploaded: ${s3url}`);
success(`Updated: ${colors.bold(relativePath)} (${asset.packaging})`);
} else {
debug(` 👑 Asset ${asset.path} (${asset.packaging}) is up-to-date: ${s3url}`);
debug(`Up-to-date: ${colors.bold(relativePath)} (${asset.packaging})`);
}

return [
Expand Down

0 comments on commit c832eaf

Please sign in to comment.