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();