From 66e634e11400a0a61e5e6def3b7b3a9da92608a8 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 1 Nov 2018 23:20:05 +0200 Subject: [PATCH 1/3] feat(toolkit): ui improvements 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 --- packages/aws-cdk/bin/cdk.ts | 26 ++++++++++----- packages/aws-cdk/lib/api/deploy-stack.ts | 3 +- .../cloudformation/stack-activity-monitor.ts | 32 +++++++++++-------- packages/aws-cdk/lib/assets.ts | 8 +++-- 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 72fda53711b99..898e42171b9ab 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -623,23 +623,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; } } diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 81511a9530355..a8a84e9c7c8fd 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -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'; @@ -61,6 +61,7 @@ export async function deployStack(options: DeployStackOptions): Promise 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 diff --git a/packages/aws-cdk/lib/assets.ts b/packages/aws-cdk/lib/assets.ts index a264b2a3eeee8..1610421a7ab36 100644 --- a/packages/aws-cdk/lib/assets.ts +++ b/packages/aws-cdk/lib/assets.ts @@ -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'); @@ -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 [ From b86d610f6f894b2e65a27ceb64b765aca79cdd6b Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 1 Nov 2018 23:49:02 +0200 Subject: [PATCH 2/3] Fix integ tests --- packages/aws-cdk/bin/cdk.ts | 4 ++-- packages/aws-cdk/integ-tests/app/app.js | 4 ++-- packages/aws-cdk/integ-tests/common.bash | 13 +++++++++++++ packages/aws-cdk/integ-tests/test-cdk-deploy-all.sh | 3 +++ packages/aws-cdk/integ-tests/test.sh | 10 ---------- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 898e42171b9ab..5919d989ef6bb 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -631,8 +631,8 @@ async function initCommandLine() { try { const result = await deployStack({ stack, sdk: aws, toolkitInfo, deployName, roleArn }); const message = result.noOp - ? ` ✅ %s (no changes)` - : ` ✅ %s`; + ? ` ✅ %s (no changes)` + : ` ✅ %s`; success('\n' + message, stack.name); diff --git a/packages/aws-cdk/integ-tests/app/app.js b/packages/aws-cdk/integ-tests/app/app.js index 19264e52f4eec..c63e71e9208e6 100644 --- a/packages/aws-cdk/integ-tests/app/app.js +++ b/packages/aws-cdk/integ-tests/app/app.js @@ -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(''); } } diff --git a/packages/aws-cdk/integ-tests/common.bash b/packages/aws-cdk/integ-tests/common.bash index 6f6a9f92bfdac..fb767a8b92649 100644 --- a/packages/aws-cdk/integ-tests/common.bash +++ b/packages/aws-cdk/integ-tests/common.bash @@ -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" @@ -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 } diff --git a/packages/aws-cdk/integ-tests/test-cdk-deploy-all.sh b/packages/aws-cdk/integ-tests/test-cdk-deploy-all.sh index 2621f03357e55..a91c2e1677cd6 100755 --- a/packages/aws-cdk/integ-tests/test-cdk-deploy-all.sh +++ b/packages/aws-cdk/integ-tests/test-cdk-deploy-all.sh @@ -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 diff --git a/packages/aws-cdk/integ-tests/test.sh b/packages/aws-cdk/integ-tests/test.sh index 56148989473ce..247844b40feab 100755 --- a/packages/aws-cdk/integ-tests/test.sh +++ b/packages/aws-cdk/integ-tests/test.sh @@ -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 "============================================================================================" From fa6fe1ce0b2dbe8fb02b72647022ae8f36769ab8 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sun, 4 Nov 2018 17:13:34 +0200 Subject: [PATCH 3/3] Improvements --- packages/aws-cdk/bin/cdk.ts | 10 ++-- packages/aws-cdk/lib/api/deploy-stack.ts | 6 +-- .../cloudformation/stack-activity-monitor.ts | 51 +++++++++++++++---- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 5919d989ef6bb..2286c780819b9 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -623,9 +623,9 @@ async function initCommandLine() { const deployName = renames.finalName(stack.name); if (deployName !== stack.name) { - print('%s: Deploying... (was %s)', colors.bold(deployName), colors.bold(stack.name)); + print('%s: deploying... (was %s)', colors.bold(deployName), colors.bold(stack.name)); } else { - print('%s: Deploying...', colors.bold(stack.name)); + print('%s: deploying...', colors.bold(stack.name)); } try { @@ -670,12 +670,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; } } diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index a8a84e9c7c8fd..44d48f7e3d87c 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -61,7 +61,7 @@ export async function deployStack(options: DeployStackOptions): Promise { throw e; }); const destroyedStack = await waitForStack(cfn, deployName, false); if (monitor) { await monitor.stop(); } diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts index 5d8b4ef8764fb..8615b01583395 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts @@ -65,9 +65,14 @@ export class StackActivityMonitor { */ private readPromise?: Promise; + /** + * 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) { @@ -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() { @@ -164,24 +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 stackTrace = ''; if (md && e.ResourceStatus && e.ResourceStatus.indexOf('FAILED') !== -1) { 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 - process.stderr.write(util.format(` %s | %s | %s | [%s] %s (%s) %s%s\n`, + // remove "/" prefix + if (resourceName.startsWith(this.stackName + '/')) { + resourceName = resourceName.substr(this.stackName.length + 1); + } + + const logicalId = resourceName !== e.LogicalResourceId ? `(${e.LogicalResourceId}) ` : ''; + + process.stderr.write(util.format(` %s | %s | %s | %s | %s %s%s%s\n`, this.progress(), new Date(e.Timestamp).toLocaleTimeString(), - color(padLeft(44, e.ResourceStatus || '')), - e.ResourceType || '', + color(padRight(20, (e.ResourceStatus || '').substr(0, 20))), // pad left and trim + padRight(this.resourceTypeColumnWidth, e.ResourceType || ''), color(colors.bold(resourceName)), - e.LogicalResourceId, - color(colors.bold(e.ResourceStatusReason ? e.ResourceStatusReason : '')), - color(stackTrace))); + logicalId, + reasonColor(colors.bold(e.ResourceStatusReason ? e.ResourceStatusReason : '')), + reasonColor(stackTrace))); this.lastPrintTime = Date.now(); } @@ -192,7 +208,7 @@ 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', @@ -221,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 }; } } @@ -290,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; +} \ No newline at end of file