From c7b10042d3bec67ac71aec03cf2ed49689ad4c1d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 16 Nov 2018 14:55:20 +0100 Subject: [PATCH] fix(aws-cdk): make bootstrapping not require --app (#1191) Improve the error message when needing to bootstrap while using assets. Also make it not necessary to have '--app' in there when bootstrapping if an environment name is given. This makes the toolkit easier to use in integration tests, where the cdk.json is generated and it's not clear what the argument to --app should be (nor should it be necessary if the environment identifier has enough information in it to reconstruct the Environment object). This also refactors the code somewhat so related code (for running the CX App and parsing its output) is together. Fixes #1188. --- packages/aws-cdk/bin/cdk.ts | 276 ++---------------- .../aws-cdk/lib/api/cxapp/environments.ts | 64 ++++ packages/aws-cdk/lib/{ => api/cxapp}/exec.ts | 6 +- packages/aws-cdk/lib/api/cxapp/stacks.ts | 194 ++++++++++++ packages/aws-cdk/lib/assets.ts | 3 +- packages/aws-cdk/lib/commands/context.ts | 16 +- packages/aws-cdk/lib/settings.ts | 56 +++- 7 files changed, 350 insertions(+), 265 deletions(-) create mode 100644 packages/aws-cdk/lib/api/cxapp/environments.ts rename packages/aws-cdk/lib/{ => api/cxapp}/exec.ts (98%) create mode 100644 packages/aws-cdk/lib/api/cxapp/stacks.ts diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 17b9b3f27e21d..5844934a47c74 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -4,22 +4,20 @@ import 'source-map-support/register'; import cxapi = require('@aws-cdk/cx-api'); import colors = require('colors/safe'); import fs = require('fs-extra'); -import minimatch = require('minimatch'); import util = require('util'); import yargs = require('yargs'); -import cdkUtil = require('../lib/util'); import { bootstrapEnvironment, deployStack, destroyStack, loadToolkitInfo, Mode, SDK } from '../lib'; -import contextproviders = require('../lib/context-providers/index'); +import { environmentsFromDescriptors, globEnvironmentsFromStacks } from '../lib/api/cxapp/environments'; +import { AppStacks, listStackNames } from '../lib/api/cxapp/stacks'; import { printStackDiff } from '../lib/diff'; -import { execProgram } from '../lib/exec'; import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; import { interactive } from '../lib/interactive'; import { data, debug, error, highlight, print, setVerbose, success, warning } from '../lib/logging'; import { PluginHost } from '../lib/plugin'; import { parseRenames } from '../lib/renames'; import { deserializeStructure, serializeStructure } from '../lib/serialize'; -import { DEFAULTS, loadProjectConfig, loadUserConfig, PER_USER_DEFAULTS, saveProjectConfig, Settings } from '../lib/settings'; +import { Configuration, Settings } from '../lib/settings'; import { VERSION } from '../lib/version'; // tslint:disable-next-line:no-var-requires @@ -27,12 +25,6 @@ const promptly = require('promptly'); const DEFAULT_TOOLKIT_STACK_NAME = 'CDKToolkit'; -/** - * Since app execution basically always synthesizes all the stacks, - * we can invoke it once and cache the response for subsequent calls. - */ -let cachedResponse: cxapi.SynthesizeResponse; - // tslint:disable:no-shadowed-variable max-line-length async function parseCommandLineArguments() { const initTemplateLanuages = await availableInitLanguages; @@ -138,18 +130,13 @@ async function initCommandLine() { ec2creds: argv.ec2creds, }); - const defaultConfig = new Settings({ versionReporting: true, pathMetadata: true }); - const userConfig = await loadUserConfig(); - const projectConfig = await loadProjectConfig(); - const commandLineArguments = argumentsToSettings(); - const renames = parseRenames(argv.rename); + const configuration = new Configuration(argumentsToSettings()); + await configuration.load(); + configuration.logDefaults(); - logDefaults(); // Ignores command-line arguments + const appStacks = new AppStacks(argv, configuration, aws); - /** Function to return the complete merged config */ - function completeConfig(): Settings { - return defaultConfig.merge(userConfig).merge(projectConfig).merge(commandLineArguments); - } + const renames = parseRenames(argv.rename); /** Function to load plug-ins, using configurations additively. */ function loadPlugins(...settings: Settings[]) { @@ -175,7 +162,7 @@ async function initCommandLine() { } } - loadPlugins(userConfig, projectConfig, commandLineArguments); + loadPlugins(configuration.combined); const cmd = argv._[0]; @@ -189,7 +176,7 @@ async function initCommandLine() { } async function main(command: string, args: any): Promise { - const toolkitStackName: string = completeConfig().get(['toolkitStackName']) || DEFAULT_TOOLKIT_STACK_NAME; + const toolkitStackName: string = configuration.combined.get(['toolkitStackName']) || DEFAULT_TOOLKIT_STACK_NAME; args.STACKS = args.STACKS || []; args.ENVIRONMENTS = args.ENVIRONMENTS || []; @@ -219,7 +206,7 @@ async function initCommandLine() { return await cliMetadata(await findStack(args.STACK)); case 'init': - const language = completeConfig().get(['language']); + const language = configuration.combined.get(['language']); if (args.list) { return await printAvailableTemplates(language); } else { @@ -232,47 +219,10 @@ async function initCommandLine() { } async function cliMetadata(stackName: string) { - const s = await synthesizeStack(stackName); + const s = await appStacks.synthesizeStack(stackName); return s.metadata; } - /** - * Extracts 'aws:cdk:warning|info|error' metadata entries from the stack synthesis - */ - function processMessages(stacks: cxapi.SynthesizeResponse): { errors: boolean, warnings: boolean } { - let warnings = false; - let errors = false; - for (const stack of stacks.stacks) { - for (const id of Object.keys(stack.metadata)) { - const metadata = stack.metadata[id]; - for (const entry of metadata) { - switch (entry.type) { - case cxapi.WARNING_METADATA_KEY: - warnings = true; - printMessage(warning, 'Warning', id, entry); - break; - case cxapi.ERROR_METADATA_KEY: - errors = true; - printMessage(error, 'Error', id, entry); - break; - case cxapi.INFO_METADATA_KEY: - printMessage(print, 'Info', id, entry); - break; - } - } - } - } - return { warnings, errors }; - } - - function printMessage(logFn: (s: string) => void, prefix: string, id: string, entry: cxapi.MetadataEntry) { - logFn(`[${prefix} at ${id}] ${entry.data}`); - - if (argv.trace || argv.verbose) { - logFn(` ${entry.trace.join('\n ')}`); - } - } - /** * Bootstrap the CDK Toolkit stack in the accounts used by the specified stack(s). * @@ -282,18 +232,15 @@ async function initCommandLine() { * @param toolkitStackName the name to be used for the CDK Toolkit stack. */ async function cliBootstrap(environmentGlobs: string[], toolkitStackName: string, roleArn: string | undefined): Promise { - if (environmentGlobs.length === 0) { - environmentGlobs = [ '**' ]; // default to ALL - } - const stacks = await selectStacks(); - const availableEnvironments = distinct(stacks.map(stack => stack.environment) - .filter(env => env !== undefined) as cxapi.Environment[]); - const environments = availableEnvironments.filter(env => environmentGlobs.find(glob => minimatch(env!.name, glob))); - if (environments.length === 0) { - const globs = JSON.stringify(environmentGlobs); - const envList = availableEnvironments.length > 0 ? availableEnvironments.map(env => env!.name).join(', ') : ''; - throw new Error(`No environments were found when selecting across ${globs} (available: ${envList})`); - } + // Two modes of operation. + // + // If there is an '--app' argument, we select the environments from the app. Otherwise we just take the user + // at their word that they know the name of the environment. + + const app = configuration.combined.get(['app']); + + const environments = app ? await globEnvironmentsFromStacks(appStacks, environmentGlobs) : environmentsFromDescriptors(environmentGlobs); + await Promise.all(environments.map(async (environment) => { success(' ⏳ Bootstrapping environment %s...', colors.blue(environment.name)); try { @@ -306,24 +253,6 @@ async function initCommandLine() { throw e; } })); - - /** - * De-duplicates a list of environments, such that a given account and region is only represented exactly once - * in the result. - * - * @param envs the possibly full-of-duplicates list of environments. - * - * @return a de-duplicated list of environments. - */ - function distinct(envs: cxapi.Environment[]): cxapi.Environment[] { - const unique: { [id: string]: cxapi.Environment } = {}; - for (const env of envs) { - const id = `${env.account || 'default'}/${env.region || 'default'}`; - if (id in unique) { continue; } - unique[id] = env; - } - return Object.values(unique); - } } /** @@ -339,14 +268,14 @@ async function initCommandLine() { doInteractive: boolean, outputDir: string|undefined, json: boolean): Promise { - const stacks = await selectStacks(...stackNames); + const stacks = await appStacks.selectStacks(...stackNames); renames.validateSelectedStacks(stacks); if (doInteractive) { if (stacks.length !== 1) { throw new Error(`When using interactive synthesis, must select exactly one stack. Got: ${listStackNames(stacks)}`); } - return await interactive(stacks[0], argv.verbose, (stack) => synthesizeStack(stack)); + return await interactive(stacks[0], argv.verbose, (stack) => appStacks.synthesizeStack(stack)); } if (stacks.length > 1 && outputDir == null) { @@ -370,130 +299,8 @@ async function initCommandLine() { return undefined; // Nothing to print } - /** - * Synthesize a single stack - */ - async function synthesizeStack(stackName: string): Promise { - const resp = await synthesizeStacks(); - const stack = resp.stacks.find(s => s.name === stackName); - if (!stack) { - throw new Error(`Stack ${stackName} not found`); - } - return stack; - } - - /** - * Synthesize a set of stacks - */ - async function synthesizeStacks(): Promise { - if (cachedResponse) { - return cachedResponse; - } - - let config = completeConfig(); - const trackVersions: boolean = completeConfig().get(['versionReporting']); - - // We may need to run the cloud executable multiple times in order to satisfy all missing context - while (true) { - const response: cxapi.SynthesizeResponse = await execProgram(aws, config); - const allMissing = cdkUtil.deepMerge(...response.stacks.map(s => s.missing)); - - if (!cdkUtil.isEmpty(allMissing)) { - debug(`Some context information is missing. Fetching...`); - - await contextproviders.provideContextValues(allMissing, projectConfig, aws); - - // Cache the new context to disk - await saveProjectConfig(projectConfig); - config = completeConfig(); - - continue; - } - - const { errors, warnings } = processMessages(response); - - if (errors && !argv.ignoreErrors) { - throw new Error('Found errors'); - } - - if (argv.strict && warnings) { - throw new Error('Found warnings (--strict mode)'); - } - - if (trackVersions && response.runtime) { - const modules = formatModules(response.runtime); - for (const stack of response.stacks) { - if (!stack.template.Resources) { - stack.template.Resources = {}; - } - if (!stack.template.Resources.CDKMetadata) { - stack.template.Resources.CDKMetadata = { - Type: 'AWS::CDK::Metadata', - Properties: { - Modules: modules - } - }; - } else { - warning(`The stack ${stack.name} already includes a CDKMetadata resource`); - } - } - } - - // All good, return - cachedResponse = response; - return response; - - function formatModules(runtime: cxapi.AppRuntime): string { - const modules = new Array(); - for (const key of Object.keys(runtime.libraries).sort()) { - modules.push(`${key}=${runtime.libraries[key]}`); - } - return modules.join(','); - } - } - } - - /** - * List all stacks in the CX and return the selected ones - * - * It's an error if there are no stacks to select, or if one of the requested parameters - * refers to a nonexistant stack. - */ - async function selectStacks(...selectors: string[]): Promise { - selectors = selectors.filter(s => s != null); // filter null/undefined - - const stacks: cxapi.SynthesizedStack[] = await listStacks(); - if (stacks.length === 0) { - throw new Error('This app contains no stacks'); - } - - if (selectors.length === 0) { - debug('Stack name not specified, so defaulting to all available stacks: ' + listStackNames(stacks)); - return stacks; - } - - // For every selector argument, pick stacks from the list. - const matched = new Set(); - for (const pattern of selectors) { - let found = false; - - for (const stack of stacks) { - if (minimatch(stack.name, pattern)) { - matched.add(stack.name); - found = true; - } - } - - if (!found) { - throw new Error(`No stack found matching '${pattern}'. Use "list" to print manifest`); - } - } - - return stacks.filter(s => matched.has(s.name)); - } - async function cliList(options: { long?: boolean } = { }) { - const stacks = await listStacks(); + const stacks = await appStacks.listStacks(); // if we are in "long" mode, emit the array as-is (JSON/YAML) if (options.long) { @@ -515,13 +322,8 @@ async function initCommandLine() { return 0; // exit-code } - async function listStacks(): Promise { - const response = await synthesizeStacks(); - return response.stacks; - } - async function cliDeploy(stackNames: string[], toolkitStackName: string, roleArn: string | undefined) { - const stacks = await selectStacks(...stackNames); + const stacks = await appStacks.selectStacks(...stackNames); renames.validateSelectedStacks(stacks); for (const stack of stacks) { @@ -567,7 +369,7 @@ async function initCommandLine() { } async function cliDestroy(stackNames: string[], force: boolean, roleArn: string | undefined) { - const stacks = await selectStacks(...stackNames); + const stacks = await appStacks.selectStacks(...stackNames); renames.validateSelectedStacks(stacks); if (!force) { @@ -593,7 +395,7 @@ async function initCommandLine() { } async function diffStack(stackName: string, templatePath?: string): Promise { - const stack = await synthesizeStack(stackName); + const stack = await appStacks.synthesizeStack(stackName); const currentTemplate = await readCurrentTemplate(stack, templatePath); if (printStackDiff(currentTemplate, stack) === 0) { return 0; @@ -634,7 +436,7 @@ async function initCommandLine() { * Match a single stack from the list of available stacks */ async function findStack(name: string): Promise { - const stacks = await selectStacks(name); + const stacks = await appStacks.selectStacks(name); // Could have been a glob so check that we evaluated to exactly one if (stacks.length > 1) { @@ -644,21 +446,6 @@ async function initCommandLine() { return stacks[0].name; } - function logDefaults() { - if (!userConfig.empty()) { - debug(PER_USER_DEFAULTS + ':', JSON.stringify(userConfig.settings, undefined, 2)); - } - - if (!projectConfig.empty()) { - debug(DEFAULTS + ':', JSON.stringify(projectConfig.settings, undefined, 2)); - } - - const combined = userConfig.merge(projectConfig); - if (!combined.empty()) { - debug('Defaults:', JSON.stringify(combined.settings, undefined, 2)); - } - } - /** Convert the command-line arguments into a Settings object */ function argumentsToSettings() { const context: any = {}; @@ -689,13 +476,6 @@ async function initCommandLine() { }); } - /** - * Combine the names of a set of stacks using a comma - */ - function listStackNames(stacks: cxapi.SynthesizedStack[]): string { - return stacks.map(s => s.name).join(', '); - } - function toJsonOrYaml(object: any): string { return serializeStructure(object, argv.json); } diff --git a/packages/aws-cdk/lib/api/cxapp/environments.ts b/packages/aws-cdk/lib/api/cxapp/environments.ts new file mode 100644 index 0000000000000..a744622f29cde --- /dev/null +++ b/packages/aws-cdk/lib/api/cxapp/environments.ts @@ -0,0 +1,64 @@ +import cxapi = require('@aws-cdk/cx-api'); +import minimatch = require('minimatch'); +import { AppStacks } from './stacks'; + +export async function globEnvironmentsFromStacks(appStacks: AppStacks, environmentGlobs: string[]): Promise { + if (environmentGlobs.length === 0) { + environmentGlobs = [ '**' ]; // default to ALL + } + const stacks = await appStacks.selectStacks(); + + const availableEnvironments = distinct(stacks.map(stack => stack.environment) + .filter(env => env !== undefined) as cxapi.Environment[]); + const environments = availableEnvironments.filter(env => environmentGlobs.find(glob => minimatch(env!.name, glob))); + if (environments.length === 0) { + const globs = JSON.stringify(environmentGlobs); + const envList = availableEnvironments.length > 0 ? availableEnvironments.map(env => env!.name).join(', ') : ''; + throw new Error(`No environments were found when selecting across ${globs} (available: ${envList})`); + } + + return environments; +} + +/** + * Given a set of "/" strings, construct environments for them + */ +export function environmentsFromDescriptors(envSpecs: string[]): cxapi.Environment[] { + if (envSpecs.length === 0) { + throw new Error(`Either specify an app with '--app', or specify an environment name like '123456789012/us-east-1'`); + } + + const ret = new Array(); + for (const spec of envSpecs) { + const parts = spec.split('/'); + if (parts.length !== 2) { + throw new Error(`Expected environment name in format '/', got: ${spec}`); + } + + ret.push({ + name: spec, + account: parts[0], + region: parts[1] + }); + } + + return ret; +} + +/** + * De-duplicates a list of environments, such that a given account and region is only represented exactly once + * in the result. + * + * @param envs the possibly full-of-duplicates list of environments. + * + * @return a de-duplicated list of environments. + */ +function distinct(envs: cxapi.Environment[]): cxapi.Environment[] { + const unique: { [id: string]: cxapi.Environment } = {}; + for (const env of envs) { + const id = `${env.account || 'default'}/${env.region || 'default'}`; + if (id in unique) { continue; } + unique[id] = env; + } + return Object.values(unique); +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/exec.ts b/packages/aws-cdk/lib/api/cxapp/exec.ts similarity index 98% rename from packages/aws-cdk/lib/exec.ts rename to packages/aws-cdk/lib/api/cxapp/exec.ts index 8a588d382a982..a597783573398 100644 --- a/packages/aws-cdk/lib/exec.ts +++ b/packages/aws-cdk/lib/api/cxapp/exec.ts @@ -4,9 +4,9 @@ import fs = require('fs-extra'); import os = require('os'); import path = require('path'); import semver = require('semver'); -import { DEFAULTS, PER_USER_DEFAULTS, Settings } from '../lib/settings'; -import { SDK } from './api'; -import { debug } from './logging'; +import { debug } from '../../logging'; +import { DEFAULTS, PER_USER_DEFAULTS, Settings } from '../../settings'; +import { SDK } from '../util/sdk'; /** Invokes the cloud executable and returns JSON output */ export async function execProgram(aws: SDK, config: Settings): Promise { diff --git a/packages/aws-cdk/lib/api/cxapp/stacks.ts b/packages/aws-cdk/lib/api/cxapp/stacks.ts new file mode 100644 index 0000000000000..79bcae2f8619f --- /dev/null +++ b/packages/aws-cdk/lib/api/cxapp/stacks.ts @@ -0,0 +1,194 @@ +import cxapi = require('@aws-cdk/cx-api'); +import minimatch = require('minimatch'); +import yargs = require('yargs'); +import contextproviders = require('../../context-providers'); +import { debug, error, print, warning } from '../../logging'; +import { Configuration } from '../../settings'; +import cdkUtil = require('../../util'); +import { SDK } from '../util/sdk'; +import { execProgram } from './exec'; + +/** + * Routines to get stacks from an app + * + * In a class because it shares some global state + */ +export class AppStacks { + /** + * Since app execution basically always synthesizes all the stacks, + * we can invoke it once and cache the response for subsequent calls. + */ + private cachedResponse?: cxapi.SynthesizeResponse; + + constructor(private readonly argv: yargs.Arguments, private readonly configuration: Configuration, private readonly aws: SDK) { + } + + /** + * List all stacks in the CX and return the selected ones + * + * It's an error if there are no stacks to select, or if one of the requested parameters + * refers to a nonexistant stack. + */ + public async selectStacks(...selectors: string[]): Promise { + selectors = selectors.filter(s => s != null); // filter null/undefined + + const stacks: cxapi.SynthesizedStack[] = await this.listStacks(); + if (stacks.length === 0) { + throw new Error('This app contains no stacks'); + } + + if (selectors.length === 0) { + debug('Stack name not specified, so defaulting to all available stacks: ' + listStackNames(stacks)); + return stacks; + } + + // For every selector argument, pick stacks from the list. + const matched = new Set(); + for (const pattern of selectors) { + let found = false; + + for (const stack of stacks) { + if (minimatch(stack.name, pattern)) { + matched.add(stack.name); + found = true; + } + } + + if (!found) { + throw new Error(`No stack found matching '${pattern}'. Use "list" to print manifest`); + } + } + + return stacks.filter(s => matched.has(s.name)); + } + + public async listStacks(): Promise { + const response = await this.synthesizeStacks(); + return response.stacks; + } + + /** + * Synthesize a single stack + */ + public async synthesizeStack(stackName: string): Promise { + const resp = await this.synthesizeStacks(); + const stack = resp.stacks.find(s => s.name === stackName); + if (!stack) { + throw new Error(`Stack ${stackName} not found`); + } + return stack; + } + + /** + * Synthesize a set of stacks + */ + public async synthesizeStacks(): Promise { + if (this.cachedResponse) { + return this.cachedResponse; + } + + const trackVersions: boolean = this.configuration.combined.get(['versionReporting']); + + // We may need to run the cloud executable multiple times in order to satisfy all missing context + while (true) { + const response: cxapi.SynthesizeResponse = await execProgram(this.aws, this.configuration.combined); + const allMissing = cdkUtil.deepMerge(...response.stacks.map(s => s.missing)); + + if (!cdkUtil.isEmpty(allMissing)) { + debug(`Some context information is missing. Fetching...`); + + await contextproviders.provideContextValues(allMissing, this.configuration.projectConfig, this.aws); + + // Cache the new context to disk + await this.configuration.saveProjectConfig(); + + continue; + } + + const { errors, warnings } = this.processMessages(response); + + if (errors && !this.argv.ignoreErrors) { + throw new Error('Found errors'); + } + + if (this.argv.strict && warnings) { + throw new Error('Found warnings (--strict mode)'); + } + + if (trackVersions && response.runtime) { + const modules = formatModules(response.runtime); + for (const stack of response.stacks) { + if (!stack.template.Resources) { + stack.template.Resources = {}; + } + if (!stack.template.Resources.CDKMetadata) { + stack.template.Resources.CDKMetadata = { + Type: 'AWS::CDK::Metadata', + Properties: { + Modules: modules + } + }; + } else { + warning(`The stack ${stack.name} already includes a CDKMetadata resource`); + } + } + } + + // All good, return + this.cachedResponse = response; + return response; + + function formatModules(runtime: cxapi.AppRuntime): string { + const modules = new Array(); + for (const key of Object.keys(runtime.libraries).sort()) { + modules.push(`${key}=${runtime.libraries[key]}`); + } + return modules.join(','); + } + } + } + + /** + * Extracts 'aws:cdk:warning|info|error' metadata entries from the stack synthesis + */ + private processMessages(stacks: cxapi.SynthesizeResponse): { errors: boolean, warnings: boolean } { + let warnings = false; + let errors = false; + for (const stack of stacks.stacks) { + for (const id of Object.keys(stack.metadata)) { + const metadata = stack.metadata[id]; + for (const entry of metadata) { + switch (entry.type) { + case cxapi.WARNING_METADATA_KEY: + warnings = true; + this.printMessage(warning, 'Warning', id, entry); + break; + case cxapi.ERROR_METADATA_KEY: + errors = true; + this.printMessage(error, 'Error', id, entry); + break; + case cxapi.INFO_METADATA_KEY: + this.printMessage(print, 'Info', id, entry); + break; + } + } + } + } + return { warnings, errors }; + } + + private printMessage(logFn: (s: string) => void, prefix: string, id: string, entry: cxapi.MetadataEntry) { + logFn(`[${prefix} at ${id}] ${entry.data}`); + + if (this.argv.trace || this.argv.verbose) { + logFn(` ${entry.trace.join('\n ')}`); + } + } +} + +/** + * Combine the names of a set of stacks using a comma + */ +export function listStackNames(stacks: cxapi.SynthesizedStack[]): string { + return stacks.map(s => s.name).join(', '); +} diff --git a/packages/aws-cdk/lib/assets.ts b/packages/aws-cdk/lib/assets.ts index 6bc5bc5eebfd1..13177fc4aa565 100644 --- a/packages/aws-cdk/lib/assets.ts +++ b/packages/aws-cdk/lib/assets.ts @@ -17,7 +17,8 @@ export async function prepareAssets(stack: SynthesizedStack, toolkitInfo?: Toolk } if (!toolkitInfo) { - throw new Error('Since this stack uses assets, the toolkit stack must be deployed to the environment ("cdk bootstrap")'); + // tslint:disable-next-line:max-line-length + throw new Error(`This stack uses assets, so the toolkit stack must be deployed to the environment (Run "${colors.blue("cdk bootstrap " + stack.environment!.name)}")`); } debug('Preparing assets'); diff --git a/packages/aws-cdk/lib/commands/context.ts b/packages/aws-cdk/lib/commands/context.ts index a2de226b5e9e0..e77d0cbf35864 100644 --- a/packages/aws-cdk/lib/commands/context.ts +++ b/packages/aws-cdk/lib/commands/context.ts @@ -2,7 +2,7 @@ import colors = require('colors/safe'); import table = require('table'); import yargs = require('yargs'); import { print } from '../../lib/logging'; -import { DEFAULTS, loadProjectConfig, saveProjectConfig } from '../settings'; +import { Configuration, DEFAULTS } from '../settings'; export const command = 'context'; export const describe = 'Manage cached context values'; @@ -20,17 +20,19 @@ export const builder = { }; export async function handler(args: yargs.Arguments): Promise { - const settings = await loadProjectConfig(); - const context = settings.get(['context']) || {}; + const configuration = new Configuration(); + await configuration.load(); + + const context = configuration.projectConfig.get(['context']) || {}; if (args.clear) { - settings.set(['context'], {}); - await saveProjectConfig(settings); + configuration.projectConfig.set(['context'], {}); + await configuration.saveProjectConfig(); print('All context values cleared.'); } else if (args.reset) { invalidateContext(context, args.reset); - settings.set(['context'], context); - await saveProjectConfig(settings); + configuration.projectConfig.set(['context'], context); + await configuration.saveProjectConfig(); } else { // List -- support '--json' flag if (args.json) { diff --git a/packages/aws-cdk/lib/settings.ts b/packages/aws-cdk/lib/settings.ts index 0cab968051981..07ff6716c0aaf 100644 --- a/packages/aws-cdk/lib/settings.ts +++ b/packages/aws-cdk/lib/settings.ts @@ -1,7 +1,7 @@ import fs = require('fs-extra'); import os = require('os'); import fs_path = require('path'); -import { warning } from './logging'; +import { debug, warning } from './logging'; import util = require('./util'); export type SettingsMap = {[key: string]: any}; @@ -9,18 +9,62 @@ 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); -} +/** + * All sources of settings combined + */ +export class Configuration { + public readonly commandLineArguments: Settings; + public readonly defaultConfig = new Settings({ versionReporting: true, pathMetadata: true }); + public readonly userConfig = new Settings(); + public readonly projectConfig = new Settings(); + + constructor(commandLineArguments?: Settings) { + this.commandLineArguments = commandLineArguments || new Settings(); + } + + /** + * Load all config + */ + public async load() { + await this.userConfig.load(PER_USER_DEFAULTS); + await this.projectConfig.load(DEFAULTS); + } + + /** + * Save the project config + */ + public async saveProjectConfig() { + await this.projectConfig.save(DEFAULTS); + } -export async function loadProjectConfig() { - return new Settings().load(DEFAULTS); + /** + * Log the loaded defaults + */ + public logDefaults() { + if (!this.userConfig.empty()) { + debug(PER_USER_DEFAULTS + ':', JSON.stringify(this.userConfig.settings, undefined, 2)); + } + + if (!this.projectConfig.empty()) { + debug(DEFAULTS + ':', JSON.stringify(this.projectConfig.settings, undefined, 2)); + } + } + + /** + * Return the combined config from all config sources + */ + public get combined(): Settings { + return this.defaultConfig.merge(this.userConfig).merge(this.projectConfig).merge(this.commandLineArguments); + } } export async function saveProjectConfig(settings: Settings) { return settings.save(DEFAULTS); } +/** + * A single set of settings + */ export class Settings { public static mergeAll(...settings: Settings[]): Settings { let ret = new Settings();