From 259077b31a1e24b5023aa289faf84d35e5d16aad Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 14 Apr 2021 16:40:31 +0200 Subject: [PATCH 01/26] Synthesize multiple stacks --- examples/typescript/aws/main.ts | 1 + .../cdktf-cli/bin/cmds/helper/synth-stack.ts | 37 ++++++++++--------- packages/cdktf-cli/bin/cmds/synth.ts | 6 ++- packages/cdktf-cli/bin/cmds/ui/synth.tsx | 5 ++- .../bin/cmds/ui/terraform-context.tsx | 5 ++- packages/cdktf/lib/app.ts | 30 ++++++++++++--- packages/cdktf/lib/terraform-stack.ts | 14 ++++--- packages/cdktf/test/app.test.ts | 12 ++++++ 8 files changed, 76 insertions(+), 34 deletions(-) diff --git a/examples/typescript/aws/main.ts b/examples/typescript/aws/main.ts index 8157e6ba59..5a9c014616 100644 --- a/examples/typescript/aws/main.ts +++ b/examples/typescript/aws/main.ts @@ -48,4 +48,5 @@ export class HelloTerra extends TerraformStack { const app = new App(); new HelloTerra(app, 'hello-terra'); +new HelloTerra(app, 'hello-terra-production'); app.synth(); \ No newline at end of file diff --git a/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts b/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts index 7fe5ff391f..b20b159489 100644 --- a/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts +++ b/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts @@ -21,7 +21,7 @@ interface SynthesizedStack { } export class SynthStack { - public static async synth(command: string, outdir: string): Promise { + public static async synth(command: string, outdir: string, targetStack: string): Promise { // start performance timer const startTime = performance.now(); try { @@ -29,7 +29,8 @@ export class SynthStack { shell: true, env: { ...process.env, - CDKTF_OUTDIR: outdir + CDKTF_OUTDIR: outdir, + CDKTF_TARGET_STACK_ID: targetStack } }); } catch (e) { @@ -58,18 +59,22 @@ Command output on stderr: const stacks: SynthesizedStack[] = []; - for (const file of await fs.readdir(outdir)) { - if (file.endsWith('.tf.json')) { - const filePath = path.join(outdir, file); - const jsonContent: SynthesizedStackMetadata = JSON.parse(fs.readFileSync(filePath).toString()); - const name = jsonContent['//']?.metadata.stackName; - if (name !== undefined) { - stacks.push({ - file: path.join(outdir, file), - name, - content: JSON.stringify(jsonContent, null, 2) - }) - } + const isDirectory = (source: string) => fs.lstatSync(source).isDirectory() + const getDirectories = (source: string) => + fs.readdirSync(source).map(name => path.join(source, name)).filter(isDirectory) + + const directories = getDirectories(path.join(outdir, 'stacks')) + + for (const directory of directories) { + const filePath = path.join(directory, 'cdk.tf.json'); + const jsonContent: SynthesizedStackMetadata = JSON.parse(fs.readFileSync(filePath).toString()); + const name = jsonContent['//']?.metadata.stackName; + if (name !== undefined) { + stacks.push({ + file: path.join(outdir, filePath), + name, + content: JSON.stringify(jsonContent, null, 2) + }) } } @@ -77,10 +82,6 @@ Command output on stderr: console.error('ERROR: No Terraform code synthesized.'); } - if (stacks.length > 1) { - console.error('ERROR: Found more than one stack. Multiple stacks are not supported at the moment and might lead to unpredictable behaviour.'); - } - return stacks } diff --git a/packages/cdktf-cli/bin/cmds/synth.ts b/packages/cdktf-cli/bin/cmds/synth.ts index b652463ab0..949973ed14 100644 --- a/packages/cdktf-cli/bin/cmds/synth.ts +++ b/packages/cdktf-cli/bin/cmds/synth.ts @@ -9,11 +9,12 @@ import { displayVersionMessage } from './version-check' const config = readConfigSync(); class Command implements yargs.CommandModule { - public readonly command = 'synth [OPTIONS]'; + public readonly command = 'synth [stack] [OPTIONS]'; public readonly describe = 'Synthesizes Terraform code for the given app in a directory.'; public readonly aliases = [ 'synthesize' ]; public readonly builder = (args: yargs.Argv) => args + .positional('stack', { desc: 'Synthesize stack which matches the given stack id only', type: 'string' }) .option('app', { default: config.app, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, desc: 'Output directory', alias: 'o' }) .option('json', { type: 'boolean', desc: 'Provide JSON output for the generated Terraform configuration.', default: false }) @@ -24,13 +25,14 @@ class Command implements yargs.CommandModule { const command = argv.app; const outdir = argv.output; const jsonOutput = argv.json; + const stack = argv.stack; if (config.checkCodeMakerOutput && !await fs.pathExists(config.codeMakerOutput)) { console.error(`ERROR: synthesis failed, run "cdktf get" to generate providers in ${config.codeMakerOutput}`); process.exit(1); } - await renderInk(React.createElement(Synth, { targetDir: outdir, synthCommand: command, jsonOutput: jsonOutput })) + await renderInk(React.createElement(Synth, { targetDir: outdir, targetStack: stack, synthCommand: command, jsonOutput: jsonOutput })) } } diff --git a/packages/cdktf-cli/bin/cmds/ui/synth.tsx b/packages/cdktf-cli/bin/cmds/ui/synth.tsx index 702aa5bf17..622cd760b0 100644 --- a/packages/cdktf-cli/bin/cmds/ui/synth.tsx +++ b/packages/cdktf-cli/bin/cmds/ui/synth.tsx @@ -5,6 +5,7 @@ import { useTerraform, Status, useTerraformState } from './terraform-context' interface CommonSynthConfig { targetDir: string; + targetStack: string; jsonOutput: boolean; } @@ -23,8 +24,8 @@ const SynthOutput = ({ targetDir, jsonOutput }: SynthOutputConfig): React.ReactE ) } -export const Synth = ({ targetDir, synthCommand, jsonOutput }: SynthConfig): React.ReactElement => { - const { synth } = useTerraform({targetDir, synthCommand}) +export const Synth = ({ targetDir, targetStack, synthCommand, jsonOutput }: SynthConfig): React.ReactElement => { + const { synth } = useTerraform({targetDir, targetStack, synthCommand}) const { status, stackName, errors } = synth() const isSynthesizing: boolean = status != Status.SYNTHESIZED diff --git a/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx b/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx index a97cb058ba..69f8288d03 100644 --- a/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx +++ b/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx @@ -193,6 +193,7 @@ export const TerraformProvider: React.FunctionComponent } interface UseTerraformInput { targetDir: string; + targetStack: string; synthCommand: string; isSpeculative?: boolean; autoApprove?: boolean; @@ -208,7 +209,7 @@ export const useTerraformState = () => { return state } -export const useTerraform = ({ targetDir, synthCommand, isSpeculative = false, autoApprove = false }: UseTerraformInput) => { +export const useTerraform = ({ targetDir, targetStack, synthCommand, isSpeculative = false, autoApprove = false }: UseTerraformInput) => { const dispatch = React.useContext(TerraformContextDispatch) const state = useTerraformState() const [terraform, setTerraform] = React.useState() @@ -251,7 +252,7 @@ export const useTerraform = ({ targetDir, synthCommand, isSpeculative = false, a const execTerraformSynth = async (loadExecutor = true) => { try { dispatch({ type: 'SYNTH' }) - const stacks = await SynthStack.synth(synthCommand, targetDir); + const stacks = await SynthStack.synth(synthCommand, targetDir, targetStack); if (loadExecutor) { await executorForStack(stacks[0].content) } diff --git a/packages/cdktf/lib/app.ts b/packages/cdktf/lib/app.ts index 8ca17adb51..e140308591 100644 --- a/packages/cdktf/lib/app.ts +++ b/packages/cdktf/lib/app.ts @@ -1,5 +1,6 @@ import { Construct, Node, ConstructMetadata } from 'constructs'; import * as fs from 'fs'; +import { exit } from 'process'; import { version } from '../package.json'; export const CONTEXT_ENV = 'CDKTF_CONTEXT_JSON'; @@ -34,6 +35,11 @@ export class App extends Construct { */ public readonly outdir: string; + /** + * The stack which will be synthesized. If not set, all stacks will be synthesized. + */ + public readonly targetStackId: string | undefined + /** * Defines an app * @param options configuration options @@ -41,6 +47,7 @@ export class App extends Construct { constructor(options: AppOptions = {}) { super(undefined as any, ''); this.outdir = process.env.CDKTF_OUTDIR ?? options.outdir ?? 'cdktf.out'; + this.targetStackId = process.env.CDKTF_TARGET_STACK_ID this.loadContext(options.context); @@ -50,19 +57,32 @@ export class App extends Construct { } node.setContext('cdktfVersion', version) + node.setContext('appOutdir', this.outdir) } /** * Synthesizes all resources to the output directory */ public synth(): void { - if (!fs.existsSync(this.outdir)) { - fs.mkdirSync(this.outdir); - } + if (!fs.existsSync(this.outdir)) { + fs.mkdirSync(this.outdir); + } - Node.of(this).synthesize({ + if (this.targetStackId) { + try { + const stackNode = Node.of(Node.of(this).findChild(this.targetStackId)) + stackNode.synthesize({ outdir: this.outdir + }); + } catch (e) { + console.error(`Couldn't synth stack ${this.targetStackId} - ${e}`) + exit(1) + } + } else { + Node.of(this).synthesize({ + outdir: this.outdir }); + } } private loadContext(defaults: { [key: string]: string } = { }) { @@ -78,7 +98,7 @@ export class App extends Construct { const contextFromEnvironment = contextJson ? JSON.parse(contextJson) : { }; - + for (const [k, v] of Object.entries(contextFromEnvironment)) { node.setContext(k, v); } diff --git a/packages/cdktf/lib/terraform-stack.ts b/packages/cdktf/lib/terraform-stack.ts index edc12c6fac..bc93b1b5e0 100644 --- a/packages/cdktf/lib/terraform-stack.ts +++ b/packages/cdktf/lib/terraform-stack.ts @@ -1,6 +1,6 @@ import { Construct, IConstruct, ISynthesisSession, Node } from 'constructs'; import { resolve } from './_tokens' -import * as fs from 'fs'; +import * as fs from 'fs-extra'; import * as path from 'path'; import { TerraformElement } from './terraform-element'; import { deepMerge } from './util'; @@ -83,7 +83,7 @@ export class TerraformStack extends Construct { * Returns the naming scheme used to allocate logical IDs. By default, uses * the `HashedAddressingScheme` but this method can be overridden to customize * this behavior. - * + * * @param tfElement The element for which the logical ID is allocated. */ protected allocateLogicalId(tfElement: TerraformElement): string { @@ -96,7 +96,7 @@ export class TerraformStack extends Construct { else { stackIndex = 0; } - + const components = node.scopes.slice(stackIndex + 1).map(c => Node.of(c).id); return components.length > 0 ? makeUniqueId(components, node.tryGetContext(ALLOW_SEP_CHARS_IN_LOGICAL_IDS)) : ''; } @@ -142,8 +142,12 @@ export class TerraformStack extends Construct { } protected onSynthesize(session: ISynthesisSession) { - const resourceOutput = path.join(session.outdir, this.artifactFile); - fs.writeFileSync(resourceOutput, JSON.stringify(this.toTerraform(), undefined, 2)); + const stackName = Node.of(this).id + + const workingDirectory = path.join(session.outdir, 'stacks', stackName) + if (!fs.existsSync(workingDirectory)) fs.ensureDirSync(workingDirectory); + + fs.writeFileSync(path.join(workingDirectory, this.artifactFile), JSON.stringify(this.toTerraform(), undefined, 2)); } } diff --git a/packages/cdktf/test/app.test.ts b/packages/cdktf/test/app.test.ts index 0606d25004..2b78b5f791 100644 --- a/packages/cdktf/test/app.test.ts +++ b/packages/cdktf/test/app.test.ts @@ -26,4 +26,16 @@ test('context can be passed through CDKTF_CONTEXT', () => { const node = Node.of(prog); expect(node.tryGetContext('key1')).toEqual('val1'); expect(node.tryGetContext('key2')).toEqual('val2'); +}); + +test('appOutdir is accessible in context', () => { + const prog = new App(); + const node = Node.of(prog); + expect(node.tryGetContext('appOutdir')).toEqual('cdktf.out'); +}); + +test('ckdtfVersion is accessible in context', () => { + const prog = new App(); + const node = Node.of(prog); + expect(node.tryGetContext('cdktfVersion')).toEqual('0.0.0'); }); \ No newline at end of file From 968faf2cacb0902a2879a7fd7f0aebfc997801b1 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Fri, 16 Apr 2021 16:15:52 +0200 Subject: [PATCH 02/26] Deploy / destroy / diff multiple stacks --- .gitignore | 2 + packages/cdktf-cli/bin/cmds/deploy.ts | 6 +- packages/cdktf-cli/bin/cmds/destroy.ts | 6 +- packages/cdktf-cli/bin/cmds/diff.ts | 6 +- .../cdktf-cli/bin/cmds/helper/synth-stack.ts | 60 ++++++++------- packages/cdktf-cli/bin/cmds/synth.ts | 6 +- .../cdktf-cli/bin/cmds/terraform-check.ts | 17 ++++- packages/cdktf-cli/bin/cmds/ui/deploy.tsx | 19 ++--- packages/cdktf-cli/bin/cmds/ui/destroy.tsx | 19 ++--- packages/cdktf-cli/bin/cmds/ui/diff.tsx | 16 ++-- .../bin/cmds/ui/models/terraform-cli.ts | 13 ++-- .../bin/cmds/ui/models/terraform-cloud.ts | 7 +- packages/cdktf-cli/bin/cmds/ui/synth.tsx | 22 +++--- .../bin/cmds/ui/terraform-context.tsx | 62 ++++++++++------ .../module-generator.test.ts.snap | 44 +++++++++++ packages/cdktf-cli/test/ui/deploy.test.tsx | 21 +++++- packages/cdktf-cli/test/ui/destroy.test.tsx | 21 +++++- packages/cdktf-cli/test/ui/diff.test.tsx | 41 ++++++++++- packages/cdktf/lib/app.ts | 73 ++++++++++++++----- packages/cdktf/lib/terraform-stack.ts | 14 ++-- packages/cdktf/test/app.test.ts | 6 -- 21 files changed, 342 insertions(+), 139 deletions(-) diff --git a/.gitignore b/.gitignore index 72d8844259..4b874c4958 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ tsconfig.json **/coverage **/dist **/.terraform +**/*.tfstate +**/*.tfstate.backup .vscode bootstrap.json terraform-cdk.github-issues diff --git a/packages/cdktf-cli/bin/cmds/deploy.ts b/packages/cdktf-cli/bin/cmds/deploy.ts index 18c6b9c084..2f69c4cde5 100644 --- a/packages/cdktf-cli/bin/cmds/deploy.ts +++ b/packages/cdktf-cli/bin/cmds/deploy.ts @@ -8,10 +8,11 @@ import { displayVersionMessage } from './version-check' const config = readConfigSync(); class Command implements yargs.CommandModule { - public readonly command = 'deploy [OPTIONS]'; + public readonly command = 'deploy [stack] [OPTIONS]'; public readonly describe = 'Deploy the given stack'; public readonly builder = (args: yargs.Argv) => args + .positional('stack', { desc: 'Deploy stack which matches the given id only. Required when more than stack is present in the app', type: 'string' }) .option('app', { default: config.app, required: true, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, required: true, desc: 'Output directory', alias: 'o' }) .option('auto-approve', { type: 'boolean', default: false, required: false, desc: 'Auto approve' }) @@ -22,8 +23,9 @@ class Command implements yargs.CommandModule { const command = argv.app; const outdir = argv.output; const autoApprove = argv.autoApprove; + const stack = argv.stack; - await renderInk(React.createElement(Deploy, { targetDir: outdir, synthCommand: command, autoApprove })) + await renderInk(React.createElement(Deploy, { targetDir: outdir, targetStack: stack, synthCommand: command, autoApprove })) } } diff --git a/packages/cdktf-cli/bin/cmds/destroy.ts b/packages/cdktf-cli/bin/cmds/destroy.ts index 205e9d27c9..11aa8f36d4 100644 --- a/packages/cdktf-cli/bin/cmds/destroy.ts +++ b/packages/cdktf-cli/bin/cmds/destroy.ts @@ -8,10 +8,11 @@ import { displayVersionMessage } from './version-check' const config = readConfigSync(); class Command implements yargs.CommandModule { - public readonly command = 'destroy [OPTIONS]'; + public readonly command = 'destroy [stack] [OPTIONS]'; public readonly describe = 'Destroy the given stack'; public readonly builder = (args: yargs.Argv) => args + .positional('stack', { desc: 'Destroy stack which matches the given id only. Required when more than stack is present in the app', type: 'string' }) .option('app', { default: config.app, required: true, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, required: true, desc: 'Output directory', alias: 'o' }) .option('auto-approve', { type: 'boolean', default: false, required: false, desc: 'Auto approve' }) @@ -22,8 +23,9 @@ class Command implements yargs.CommandModule { const command = argv.app; const outdir = argv.output; const autoApprove = argv.autoApprove; + const stack = argv.stack; - await renderInk(React.createElement(Destroy, { targetDir: outdir, synthCommand: command, autoApprove })) + await renderInk(React.createElement(Destroy, { targetDir: outdir, targetStack: stack, synthCommand: command, autoApprove })) } } diff --git a/packages/cdktf-cli/bin/cmds/diff.ts b/packages/cdktf-cli/bin/cmds/diff.ts index f560351544..420b41d596 100644 --- a/packages/cdktf-cli/bin/cmds/diff.ts +++ b/packages/cdktf-cli/bin/cmds/diff.ts @@ -8,10 +8,11 @@ import { displayVersionMessage } from './version-check' const config = readConfigSync(); class Command implements yargs.CommandModule { - public readonly command = 'diff [OPTIONS]'; + public readonly command = 'diff [stack] [OPTIONS]'; public readonly describe = 'Perform a diff (terraform plan) for the given stack'; public readonly builder = (args: yargs.Argv) => args + .positional('stack', { desc: 'Diff stack which matches the given id only. Required when more than stack is present in the app', type: 'string' }) .option('app', { default: config.app, required: true, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, required: true, desc: 'Output directory', alias: 'o' }) .showHelpOnFail(true) @@ -20,8 +21,9 @@ class Command implements yargs.CommandModule { await displayVersionMessage() const command = argv.app; const outdir = argv.output; + const stack = argv.stack; - await renderInk(React.createElement(Diff, { targetDir: outdir, synthCommand: command })) + await renderInk(React.createElement(Diff, { targetDir: outdir, targetStack: stack, synthCommand: command })) } } diff --git a/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts b/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts index b20b159489..1e11f12e4c 100644 --- a/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts +++ b/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts @@ -3,7 +3,7 @@ import * as fs from 'fs-extra'; import * as path from 'path' import * as chalk from 'chalk'; import indentString from 'indent-string'; -import { TerraformStackMetadata } from 'cdktf' +import { Manifest, StackManifest, TerraformStackMetadata } from 'cdktf' import { ReportRequest, ReportParams } from '../../../lib/checkpoint' import { performance } from 'perf_hooks'; import { versionNumber } from '../version-check'; @@ -14,23 +14,34 @@ interface SynthesizedStackMetadata { "//"?: {[key: string]: TerraformStackMetadata }; } -interface SynthesizedStack { - file: string; - name: string; +export interface SynthesizedStack extends StackManifest { content: string; } +interface ManifestJson { + version: string; + stacks: StackManifest[]; +} + export class SynthStack { - public static async synth(command: string, outdir: string, targetStack: string): Promise { + public static async synth(command: string, outdir: string): Promise { // start performance timer const startTime = performance.now(); + + const isDirectory = (source: string) => fs.lstatSync(source).isDirectory() + const getDirectories = (source: string) => { + if (!fs.existsSync(source)) return []; + return fs.readdirSync(source).map(name => path.join(source, name)).filter(isDirectory) + } + + const existingDirectories = getDirectories(path.join(outdir, Manifest.stacksFolder)) + try { await shell(command, [], { shell: true, env: { ...process.env, - CDKTF_OUTDIR: outdir, - CDKTF_TARGET_STACK_ID: targetStack + CDKTF_OUTDIR: outdir } }); } catch (e) { @@ -48,8 +59,8 @@ Command output on stderr: process.exit(1); } - if (!await fs.pathExists(outdir)) { - console.error(`ERROR: synthesis failed, app expected to create "${outdir}"`); + if (!await fs.pathExists(path.join(outdir, Manifest.fileName))) { + console.error(`ERROR: synthesis failed, app expected to create "${outdir}/${Manifest.fileName}"`); process.exit(1); } @@ -58,30 +69,29 @@ Command output on stderr: await this.synthTelemetry(command, (endTime - startTime)); const stacks: SynthesizedStack[] = []; + const manifest = JSON.parse(fs.readFileSync(path.join(outdir, Manifest.fileName)).toString()) as ManifestJson - const isDirectory = (source: string) => fs.lstatSync(source).isDirectory() - const getDirectories = (source: string) => - fs.readdirSync(source).map(name => path.join(source, name)).filter(isDirectory) - - const directories = getDirectories(path.join(outdir, 'stacks')) - - for (const directory of directories) { - const filePath = path.join(directory, 'cdk.tf.json'); + for (const stackName in manifest.stacks) { + const stack = manifest.stacks[stackName]; + const filePath = path.join(outdir, stack.synthesizedStackPath); const jsonContent: SynthesizedStackMetadata = JSON.parse(fs.readFileSync(filePath).toString()); - const name = jsonContent['//']?.metadata.stackName; - if (name !== undefined) { - stacks.push({ - file: path.join(outdir, filePath), - name, - content: JSON.stringify(jsonContent, null, 2) - }) - } + stacks.push({ + ...stack, + workingDirectory: path.join(outdir, stack.workingDirectory), + content: JSON.stringify(jsonContent, null, 2) + }) } if (stacks.length === 0) { console.error('ERROR: No Terraform code synthesized.'); } + const stackNames = stacks.map(s => s.name) + const orphanedDirectories = existingDirectories.filter(e => !stackNames.includes(path.basename(e))) + + console.log({orphanedDirectories}) + + return stacks } diff --git a/packages/cdktf-cli/bin/cmds/synth.ts b/packages/cdktf-cli/bin/cmds/synth.ts index 949973ed14..b652463ab0 100644 --- a/packages/cdktf-cli/bin/cmds/synth.ts +++ b/packages/cdktf-cli/bin/cmds/synth.ts @@ -9,12 +9,11 @@ import { displayVersionMessage } from './version-check' const config = readConfigSync(); class Command implements yargs.CommandModule { - public readonly command = 'synth [stack] [OPTIONS]'; + public readonly command = 'synth [OPTIONS]'; public readonly describe = 'Synthesizes Terraform code for the given app in a directory.'; public readonly aliases = [ 'synthesize' ]; public readonly builder = (args: yargs.Argv) => args - .positional('stack', { desc: 'Synthesize stack which matches the given stack id only', type: 'string' }) .option('app', { default: config.app, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, desc: 'Output directory', alias: 'o' }) .option('json', { type: 'boolean', desc: 'Provide JSON output for the generated Terraform configuration.', default: false }) @@ -25,14 +24,13 @@ class Command implements yargs.CommandModule { const command = argv.app; const outdir = argv.output; const jsonOutput = argv.json; - const stack = argv.stack; if (config.checkCodeMakerOutput && !await fs.pathExists(config.codeMakerOutput)) { console.error(`ERROR: synthesis failed, run "cdktf get" to generate providers in ${config.codeMakerOutput}`); process.exit(1); } - await renderInk(React.createElement(Synth, { targetDir: outdir, targetStack: stack, synthCommand: command, jsonOutput: jsonOutput })) + await renderInk(React.createElement(Synth, { targetDir: outdir, synthCommand: command, jsonOutput: jsonOutput })) } } diff --git a/packages/cdktf-cli/bin/cmds/terraform-check.ts b/packages/cdktf-cli/bin/cmds/terraform-check.ts index 37963a83d4..c853dcc581 100644 --- a/packages/cdktf-cli/bin/cmds/terraform-check.ts +++ b/packages/cdktf-cli/bin/cmds/terraform-check.ts @@ -1,12 +1,27 @@ import { TerraformCli } from './ui/models/terraform-cli' import * as semver from 'semver'; +import { SynthesizedStack } from './helper/synth-stack'; +import { existsSync } from 'fs-extra'; +import * as path from 'path'; const MIN_SUPPORTED_VERSION = '0.13.0' const VERSION_REGEXP = /Terraform v\d+.\d+.\d+/ export const terraformCheck = async (): Promise => { try { - const terraform = new TerraformCli('./') + if (existsSync(path.join(process.cwd(), 'terraform.tfstate'))) { + throw new Error(`Found 'terraform.tfstate' Terraform state file. Please rename it to match the stack name. Learn more https://cdk.tf/multiple-stacks`) + } + + const fakeStack: SynthesizedStack = { + name: '', + workingDirectory: './', + constructPath: '', + content: '', + synthesizedStackPath: '' + } + + const terraform = new TerraformCli(fakeStack) const terraformVersion = await terraform.version() const terraformVersionMatches = terraformVersion.match(VERSION_REGEXP) diff --git a/packages/cdktf-cli/bin/cmds/ui/deploy.tsx b/packages/cdktf-cli/bin/cmds/ui/deploy.tsx index f9c33780ae..b5ade01706 100644 --- a/packages/cdktf-cli/bin/cmds/ui/deploy.tsx +++ b/packages/cdktf-cli/bin/cmds/ui/deploy.tsx @@ -79,21 +79,21 @@ const Confirm = ({ callback }: ConfirmConfig): React.ReactElement => { } export const Apply = (): React.ReactElement => { - const { resources, status, stackName, output } = useTerraformState() + const { resources, status, currentStack, output } = useTerraformState() const applyActions = [PlannedResourceAction.UPDATE, PlannedResourceAction.CREATE, PlannedResourceAction.DELETE, PlannedResourceAction.READ]; const applyableResources = resources.filter(resource => (applyActions.includes(resource.action))); return ( - {Status.DEPLOYING == status ? (<>Deploying Stack: {stackName}) : ( - <>Deploying Stack: {stackName} + {Status.DEPLOYING == status ? (<>Deploying Stack: {currentStack.name}) : ( + <>Deploying Stack: {currentStack.name} )} Resources {applyableResources.map((resource: any) => ( - + ))} @@ -113,20 +113,21 @@ export const Apply = (): React.ReactElement => { interface DeployConfig { targetDir: string; + targetStack?: string; synthCommand: string; autoApprove: boolean; } -export const Deploy = ({ targetDir, synthCommand, autoApprove }: DeployConfig): React.ReactElement => { - const { deploy } = useTerraform({ targetDir, synthCommand, autoApprove }) - const { state: { status, stackName, errors, plan }, confirmation, isConfirmed } = deploy() +export const Deploy = ({ targetDir, targetStack, synthCommand, autoApprove }: DeployConfig): React.ReactElement => { + const { deploy } = useTerraform({ targetDir, targetStack, synthCommand, autoApprove }) + const { state: { status, currentStack, errors, plan }, confirmation, isConfirmed } = deploy() const planStages = [Status.INITIALIZING, Status.PLANNING, Status.SYNTHESIZING, Status.SYNTHESIZED, Status.STARTING] const isPlanning = planStages.includes(status) - const statusText = (stackName === '') ? {status}... : {status} {stackName}... + const statusText = (currentStack.name === '') ? {status}... : {status} {currentStack.name}... if (errors) return ({errors.map((e: any) => e.message)}); - if (plan && !plan.needsApply) return (<>No changes for Stack: {stackName}); + if (plan && !plan.needsApply) return (<>No changes for Stack: {currentStack.name}); return ( diff --git a/packages/cdktf-cli/bin/cmds/ui/destroy.tsx b/packages/cdktf-cli/bin/cmds/ui/destroy.tsx index a9a94145e7..1196e822c1 100644 --- a/packages/cdktf-cli/bin/cmds/ui/destroy.tsx +++ b/packages/cdktf-cli/bin/cmds/ui/destroy.tsx @@ -60,21 +60,21 @@ const Confirm = ({ callback }: ConfirmConfig): React.ReactElement => { } export const DestroyComponent = (): React.ReactElement => { - const { resources, status, stackName } = useTerraformState() + const { resources, status, currentStack } = useTerraformState() const applyActions = [PlannedResourceAction.UPDATE, PlannedResourceAction.CREATE, PlannedResourceAction.DELETE, PlannedResourceAction.READ]; const applyableResources = resources.filter(resource => (applyActions.includes(resource.action))); return ( - {Status.DESTROYING == status ? (<>Destroying Stack: {stackName}) : ( - <>Destroying Stack: {stackName} + {Status.DESTROYING == status ? (<>Destroying Stack: {currentStack.name}) : ( + <>Destroying Stack: {currentStack.name} )} Resources {applyableResources.map((resource: any) => ( - + ))} @@ -88,20 +88,21 @@ export const DestroyComponent = (): React.ReactElement => { interface DestroyConfig { targetDir: string; + targetStack?: string; synthCommand: string; autoApprove: boolean; } -export const Destroy = ({ targetDir, synthCommand, autoApprove }: DestroyConfig): React.ReactElement => { - const { destroy } = useTerraform({ targetDir, synthCommand, autoApprove }) - const { state: { status, stackName, errors, plan }, confirmation, isConfirmed } = destroy() +export const Destroy = ({ targetDir, targetStack, synthCommand, autoApprove }: DestroyConfig): React.ReactElement => { + const { destroy } = useTerraform({ targetDir, targetStack, synthCommand, autoApprove }) + const { state: { status, currentStack, errors, plan }, confirmation, isConfirmed } = destroy() const planStages = [Status.INITIALIZING, Status.PLANNING, Status.SYNTHESIZING, Status.SYNTHESIZED, Status.STARTING] const isPlanning = planStages.includes(status) - const statusText = (stackName === '') ? {status}... : {status} {stackName}... + const statusText = (currentStack.name === '') ? {status}... : {status} {currentStack.name}... if (errors) return ({errors}); - if (plan && !plan.needsApply) return (<>No changes for Stack: {stackName}); + if (plan && !plan.needsApply) return (<>No changes for Stack: {currentStack.name}); return ( diff --git a/packages/cdktf-cli/bin/cmds/ui/diff.tsx b/packages/cdktf-cli/bin/cmds/ui/diff.tsx index d415c4d170..553b0abb61 100644 --- a/packages/cdktf-cli/bin/cmds/ui/diff.tsx +++ b/packages/cdktf-cli/bin/cmds/ui/diff.tsx @@ -7,6 +7,7 @@ import { useTerraform, Status, useTerraformState } from './terraform-context' interface DiffConfig { targetDir: string; + targetStack?: string; synthCommand: string; } @@ -48,17 +49,17 @@ export const CloudRunInfo = (): React.ReactElement => { } export const Plan = (): React.ReactElement => { - const { plan, stackName } = useTerraformState() + const { plan, currentStack } = useTerraformState() return ( - Stack: {stackName} + Stack: {currentStack.name} {plan?.needsApply ? (Resources) : (<>)} - {plan?.applyableResources.map(resource => ())} + {plan?.applyableResources.map(resource => ())} Diff: . @@ -68,13 +69,14 @@ export const Plan = (): React.ReactElement => { ) } -export const Diff = ({ targetDir, synthCommand }: DiffConfig): React.ReactElement => { - const { plan } = useTerraform({ targetDir, synthCommand, isSpeculative: true }) +export const Diff = ({ targetDir, targetStack, synthCommand }: DiffConfig): React.ReactElement => { + const { plan } = useTerraform({ targetDir, targetStack, synthCommand, isSpeculative: true }) + + const { status, currentStack, errors } = plan() - const { status, stackName, errors } = plan() const isPlanning: boolean = status != Status.PLANNED - const statusText = (stackName === '') ? `${status}...` : {status} {stackName}... + const statusText = (currentStack.name === '') ? `${status}...` : {status} {currentStack.name}... if (errors) return ({errors}); diff --git a/packages/cdktf-cli/bin/cmds/ui/models/terraform-cli.ts b/packages/cdktf-cli/bin/cmds/ui/models/terraform-cli.ts index 9bc9fcab03..cffec697a8 100644 --- a/packages/cdktf-cli/bin/cmds/ui/models/terraform-cli.ts +++ b/packages/cdktf-cli/bin/cmds/ui/models/terraform-cli.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import { exec, readCDKTFVersion } from 'cdktf-cli/lib/util' import { Terraform, TerraformPlan, TerraformOutput, PlannedResourceAction, PlannedResource, ResourceChanges } from './terraform' +import { SynthesizedStack } from '../../helper/synth-stack'; const terraformBinaryName = process.env.TERRAFORM_BINARY_NAME || 'terraform' @@ -29,7 +30,10 @@ export class TerraformCliPlan implements TerraformPlan { } export class TerraformCli implements Terraform { - constructor(public readonly workdir: string) { + public readonly workdir: string; + + constructor(public readonly stack: SynthesizedStack) { + this.workdir = stack.workingDirectory } public async init(): Promise { @@ -38,7 +42,7 @@ export class TerraformCli implements Terraform { } public async plan(destroy = false): Promise { - const planFile = path.join(this.workdir, 'plan') + const planFile = 'plan' const options = ['plan', '-input=false', '-out', planFile, ...this.stateFileOption] if (destroy) { options.push('-destroy') @@ -50,9 +54,8 @@ export class TerraformCli implements Terraform { } public async deploy(planFile: string, stdout: (chunk: Buffer) => any): Promise { - const relativePlanFile = path.relative(this.workdir, planFile); await this.setUserAgent() - await exec(terraformBinaryName, ['apply', '-auto-approve', '-input=false', ...this.stateFileOption, relativePlanFile], { cwd: this.workdir, env: process.env }, stdout); + await exec(terraformBinaryName, ['apply', '-auto-approve', '-input=false', ...this.stateFileOption, planFile], { cwd: this.workdir, env: process.env }, stdout); } public async destroy(stdout: (chunk: Buffer) => any): Promise { @@ -74,7 +77,7 @@ export class TerraformCli implements Terraform { } private get stateFileOption() { - return ['-state', path.join(process.cwd(), 'terraform.tfstate')] + return ['-state', path.join(process.cwd(), `terraform.${this.stack.name}.tfstate`)] } public async setUserAgent(): Promise { diff --git a/packages/cdktf-cli/bin/cmds/ui/models/terraform-cloud.ts b/packages/cdktf-cli/bin/cmds/ui/models/terraform-cloud.ts index 958aa22783..5ff142908b 100644 --- a/packages/cdktf-cli/bin/cmds/ui/models/terraform-cloud.ts +++ b/packages/cdktf-cli/bin/cmds/ui/models/terraform-cloud.ts @@ -7,6 +7,7 @@ import { TerraformJsonConfigBackendRemote } from '../terraform-json' import * as TerraformCloudClient from '@skorfmann/terraform-cloud' import archiver from 'archiver'; import { WritableStreamBuffer } from 'stream-buffers'; +import { StackManifest } from 'cdktf'; export class TerraformCloudPlan implements TerraformPlan { constructor(public readonly planFile: string, public readonly plan: { [key: string]: any }, public readonly url: string) { } @@ -96,11 +97,13 @@ export class TerraformCloud implements Terraform { private readonly client: TerraformCloudClient.TerraformCloud; private readonly isSpeculative: boolean; private configurationVersionId?: string; + public readonly workDir: string; public run?: TerraformCloudClient.Run; - constructor(public readonly workdir: string, public readonly config: TerraformJsonConfigBackendRemote, isSpeculative = false) { + constructor(public readonly stack: StackManifest, public readonly config: TerraformJsonConfigBackendRemote, isSpeculative = false) { if (!config.workspaces.name) throw new Error("Please provide a workspace name for Terraform Cloud"); if (!config.organization) throw new Error("Please provide an organization for Terraform Cloud"); + this.workDir = stack.workingDirectory this.hostname = config.hostname || 'app.terraform.io' this.workspaceName = config.workspaces.name @@ -141,7 +144,7 @@ export class TerraformCloud implements Terraform { this.configurationVersionId = version.id - const zipBuffer = await zipDirectory(this.workdir) + const zipBuffer = await zipDirectory(this.workDir) if (!zipBuffer) throw new Error("Couldn't upload directory to Terraform Cloud"); await this.client.ConfigurationVersion.upload(version.attributes.uploadUrl, zipBuffer) diff --git a/packages/cdktf-cli/bin/cmds/ui/synth.tsx b/packages/cdktf-cli/bin/cmds/ui/synth.tsx index 622cd760b0..a693f51323 100644 --- a/packages/cdktf-cli/bin/cmds/ui/synth.tsx +++ b/packages/cdktf-cli/bin/cmds/ui/synth.tsx @@ -5,31 +5,33 @@ import { useTerraform, Status, useTerraformState } from './terraform-context' interface CommonSynthConfig { targetDir: string; - targetStack: string; jsonOutput: boolean; } -type SynthOutputConfig = CommonSynthConfig +type SynthOutputConfig = { + jsonOutput: boolean; +} interface SynthConfig extends CommonSynthConfig { synthCommand: string; } -const SynthOutput = ({ targetDir, jsonOutput }: SynthOutputConfig): React.ReactElement => { - const { stackJSON } = useTerraformState() +const SynthOutput = ({ jsonOutput }: SynthOutputConfig): React.ReactElement => { + const { currentStack } = useTerraformState() + return( <> - { jsonOutput ? ({stackJSON}) : (Generated Terraform code in the output directory: {targetDir}) } + { jsonOutput ? ({currentStack.content}) : (Generated Terraform code in the output directory: {currentStack.workingDirectory}) } ) } -export const Synth = ({ targetDir, targetStack, synthCommand, jsonOutput }: SynthConfig): React.ReactElement => { - const { synth } = useTerraform({targetDir, targetStack, synthCommand}) - const { status, stackName, errors } = synth() +export const Synth = ({ targetDir, synthCommand, jsonOutput }: SynthConfig): React.ReactElement => { + const { synth } = useTerraform({targetDir, synthCommand}) + const { status, currentStack, errors } = synth() const isSynthesizing: boolean = status != Status.SYNTHESIZED - const statusText = (stackName === '') ? `${status}...` : {status} {stackName}... + const statusText = (currentStack.name === '') ? `${status}...` : {status} {currentStack.name}... if (errors) return({ errors }); @@ -47,7 +49,7 @@ export const Synth = ({ targetDir, targetStack, synthCommand, jsonOutput }: Synt ) : ( - + )} diff --git a/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx b/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx index 69f8288d03..993dd08b65 100644 --- a/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx +++ b/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx @@ -1,6 +1,5 @@ /* eslint-disable no-control-regex */ import React from 'react' -import * as path from 'path' import { TerraformCli } from "./models/terraform-cli" import { TerraformCloud, TerraformCloudPlan } from "./models/terraform-cloud" import { Terraform, DeployingResource, DeployingResourceApplyState, PlannedResourceAction, PlannedResource, TerraformPlan, TerraformOutput } from './models/terraform' @@ -8,6 +7,7 @@ import { SynthStack } from '../helper/synth-stack' import { TerraformJson } from './terraform-json' import { useApp } from 'ink' import stripAnsi from 'strip-ansi' +import { SynthesizedStack } from '../helper/synth-stack' type DefaultValue = undefined; type ContextValue = DefaultValue | DeployState; @@ -89,15 +89,16 @@ export type DeployState = { resources: DeployingResource[]; plan?: TerraformPlan; url?: string; - stackName?: string; - stackJSON?: string; + currentStack: SynthesizedStack; + stacks?: SynthesizedStack[]; errors?: string[]; output?: { [key: string]: TerraformOutput }; } type Action = | { type: 'SYNTH' } - | { type: 'NEW_STACK'; stackName: string; stackJSON: string } + | { type: 'SYNTHESIZED'; stacks: SynthesizedStack[] } + | { type: 'CURRENT_STACK'; currentStack: SynthesizedStack } | { type: 'INIT' } | { type: 'PLAN' } | { type: 'PLANNED'; plan: TerraformPlan } @@ -124,12 +125,13 @@ function deployReducer(state: DeployState, action: Action): DeployState { case 'SYNTH': { return { ...state, status: Status.SYNTHESIZING } } - case 'NEW_STACK': { + case 'SYNTHESIZED': { + return { ...state, status: Status.SYNTHESIZED, stacks: action.stacks } + } + case 'CURRENT_STACK': { return { ...state, - status: Status.SYNTHESIZED, - stackName: action.stackName, - stackJSON: action.stackJSON + currentStack: action.currentStack } } case 'INIT': { @@ -181,7 +183,15 @@ interface TerraformProviderConfig { // eslint-disable-next-line react/prop-types export const TerraformProvider: React.FunctionComponent = ({ children, initialState }): React.ReactElement => { - const [state, dispatch] = React.useReducer(deployReducer, initialState || { status: Status.STARTING, resources: [] }) + const initialCurrentStack: SynthesizedStack = { + constructPath: '', + content: '', + name: '', + synthesizedStackPath: '', + workingDirectory: '' + } + + const [state, dispatch] = React.useReducer(deployReducer, initialState || { status: Status.STARTING, resources: [], currentStack: initialCurrentStack }) return ( @@ -193,7 +203,7 @@ export const TerraformProvider: React.FunctionComponent } interface UseTerraformInput { targetDir: string; - targetStack: string; + targetStack?: string; synthCommand: string; isSpeculative?: boolean; autoApprove?: boolean; @@ -220,7 +230,6 @@ export const useTerraform = ({ targetDir, targetStack, synthCommand, isSpeculati throw new Error('useTerraform must be used within a TerraformContextDispatch.Provider') } - const confirmationCallback = React.useCallback(submitValue => { if (submitValue === false) { exit() @@ -231,32 +240,39 @@ export const useTerraform = ({ targetDir, targetStack, synthCommand, isSpeculati }, []); - const executorForStack = async (stackJSON: string): Promise => { - if (stackJSON === undefined) throw new Error('no synthesized stack found'); - const cwd = process.cwd(); - const outdir = path.join(cwd, targetDir); - const stack = JSON.parse(stackJSON) as TerraformJson + const executorForStack = async (stack: SynthesizedStack): Promise => { + const parsedStack = JSON.parse(stack.content) as TerraformJson - if (stack.terraform?.backend?.remote) { - const tfClient = new TerraformCloud(outdir, stack.terraform?.backend?.remote, isSpeculative) + if (parsedStack.terraform?.backend?.remote) { + const tfClient = new TerraformCloud(stack, parsedStack.terraform?.backend?.remote, isSpeculative) if (await tfClient.isRemoteWorkspace()) { setTerraform(tfClient) } else { - setTerraform(new TerraformCli(outdir)) + setTerraform(new TerraformCli(stack)) } } else { - setTerraform(new TerraformCli(outdir)) + setTerraform(new TerraformCli(stack)) } } const execTerraformSynth = async (loadExecutor = true) => { try { dispatch({ type: 'SYNTH' }) - const stacks = await SynthStack.synth(synthCommand, targetDir, targetStack); + const stacks = await SynthStack.synth(synthCommand, targetDir); + if (loadExecutor) { - await executorForStack(stacks[0].content) + if (stacks.length > 1 && !targetStack) { + throw new Error(`Found more than one stack, please specify a target stack ${stacks.map(s => s.name).join(', ')}`); + } + const stack = targetStack ? stacks.find(s => s.name === targetStack) : stacks[0] + if (!stack) throw new Error(`Can't find given stack ${targetStack} - Found the following stacks ${stacks.map(s => s.name).join(', ')}`); + + dispatch({ type: 'CURRENT_STACK', currentStack: stack }) + + await executorForStack(stack) } - dispatch({ type: 'NEW_STACK', stackName: stacks[0].name, stackJSON: stacks[0].content }) + + dispatch({ type: 'SYNTHESIZED', stacks }) } catch (e) { dispatch({ type: 'ERROR', error: e }) } diff --git a/packages/cdktf-cli/test/get/generator/__snapshots__/module-generator.test.ts.snap b/packages/cdktf-cli/test/get/generator/__snapshots__/module-generator.test.ts.snap index 9fbb38e536..e824534a3a 100644 --- a/packages/cdktf-cli/test/get/generator/__snapshots__/module-generator.test.ts.snap +++ b/packages/cdktf-cli/test/get/generator/__snapshots__/module-generator.test.ts.snap @@ -753,3 +753,47 @@ export class Module extends TerraformModule { } " `; + +exports[`typeless variables 2`] = ` +"// generated by cdktf get +// ./module +import { TerraformModule } from 'cdktf'; +import { Construct } from 'constructs'; +export interface ModuleOptions { + /** + * My example var without type set, but with default + * @default 1 + */ + readonly myDefaultTypevar?: number; + /** + * My example var without type set + */ + readonly myTypelessVar: any; +} +export class Module extends TerraformModule { + private readonly inputs: { [name: string]: any } = { } + public constructor(scope: Construct, id: string, options: ModuleOptions) { + super(scope, id, { + source: './module', + }); + this.myDefaultTypevar = options.myDefaultTypevar; + this.myTypelessVar = options.myTypelessVar; + } + public get myDefaultTypevar(): number | undefined { + return this.inputs['my_default_typevar'] as number | undefined; + } + public set myDefaultTypevar(value: number | undefined) { + this.inputs['my_default_typevar'] = value; + } + public get myTypelessVar(): any { + return this.inputs['my_typeless_var'] as any; + } + public set myTypelessVar(value: any) { + this.inputs['my_typeless_var'] = value; + } + protected synthesizeAttributes() { + return this.inputs; + } +} +" +`; diff --git a/packages/cdktf-cli/test/ui/deploy.test.tsx b/packages/cdktf-cli/test/ui/deploy.test.tsx index 9514fc21f6..3de4a8c0cc 100644 --- a/packages/cdktf-cli/test/ui/deploy.test.tsx +++ b/packages/cdktf-cli/test/ui/deploy.test.tsx @@ -14,6 +14,7 @@ import { DeployState, Status } from "../../bin/cmds/ui/terraform-context"; +import { SynthesizedStack } from "../../bin/cmds/helper/synth-stack"; test("DeploySummary", async () => { const resource: DeployingResource = { @@ -50,10 +51,18 @@ test("Apply", async () => { applyState: DeployingResourceApplyState.CREATING }; + const currentStack: SynthesizedStack = { + constructPath: '', + content: '', + name: 'testing', + synthesizedStackPath: './foo/stacks/bar', + workingDirectory: './foo' + } + const initialState: DeployState = { status: Status.STARTING, resources: [resource], - stackName: "testing" + currentStack }; const { lastFrame } = render( @@ -83,10 +92,18 @@ test("Apply Multiple Resources", async () => { applyState: DeployingResourceApplyState.CREATED }; + const currentStack: SynthesizedStack = { + constructPath: '', + content: '', + name: 'hellodiff', + synthesizedStackPath: './foo/stacks/bar', + workingDirectory: './foo' + } + const initialState: DeployState = { status: Status.STARTING, resources: [resource, otherResource], - stackName: "hellodiff" + currentStack }; const { lastFrame } = render( diff --git a/packages/cdktf-cli/test/ui/destroy.test.tsx b/packages/cdktf-cli/test/ui/destroy.test.tsx index 1180884532..784897b6bc 100644 --- a/packages/cdktf-cli/test/ui/destroy.test.tsx +++ b/packages/cdktf-cli/test/ui/destroy.test.tsx @@ -12,6 +12,7 @@ import { DeployState, Status } from "../../bin/cmds/ui/terraform-context"; +import { SynthesizedStack } from "../../bin/cmds/helper/synth-stack"; test("Destroy", async () => { const resource: DeployingResource = { @@ -20,10 +21,18 @@ test("Destroy", async () => { applyState: DeployingResourceApplyState.DESTROYING }; + const currentStack: SynthesizedStack = { + constructPath: '', + content: '', + name: 'testing', + synthesizedStackPath: './foo/stacks/bar', + workingDirectory: './foo' + } + const initialState: DeployState = { status: Status.STARTING, resources: [resource], - stackName: "testing" + currentStack, }; const { lastFrame } = render( @@ -53,10 +62,18 @@ test("Apply Multiple Resources", async () => { applyState: DeployingResourceApplyState.DESTROYED }; + const currentStack: SynthesizedStack = { + constructPath: '', + content: '', + name: 'hellodiff', + synthesizedStackPath: './foo/stacks/bar', + workingDirectory: './foo' + } + const initialState: DeployState = { status: Status.STARTING, resources: [resource, otherResource], - stackName: "hellodiff" + currentStack }; const { lastFrame } = render( diff --git a/packages/cdktf-cli/test/ui/diff.test.tsx b/packages/cdktf-cli/test/ui/diff.test.tsx index 0a7b1a42f0..7eb8393ebd 100644 --- a/packages/cdktf-cli/test/ui/diff.test.tsx +++ b/packages/cdktf-cli/test/ui/diff.test.tsx @@ -11,6 +11,7 @@ import { DeployState, Status } from "../../bin/cmds/ui/terraform-context"; +import { SynthesizedStack } from "../../bin/cmds/helper/synth-stack"; test("Diff", async () => { const resource: PlannedResource = { @@ -18,9 +19,17 @@ test("Diff", async () => { action: PlannedResourceAction.CREATE }; + const currentStack: SynthesizedStack = { + constructPath: '', + content: '', + name: 'testing', + synthesizedStackPath: './foo/stacks/bar', + workingDirectory: './foo' + } + const initialState: DeployState = { status: Status.STARTING, - stackName: "testing", + currentStack, resources: [], plan: { needsApply: true, @@ -45,9 +54,17 @@ test("Diff", async () => { }); test("Diff no Changes", async () => { + const currentStack: SynthesizedStack = { + constructPath: '', + content: '', + name: 'testing', + synthesizedStackPath: './foo/stacks/bar', + workingDirectory: './foo' + } + const initialState: DeployState = { status: Status.STARTING, - stackName: "testing", + currentStack, resources: [], plan: { needsApply: false, @@ -70,9 +87,17 @@ test("Diff no Changes", async () => { }); test("Diff with Cloud URL", async () => { + const currentStack: SynthesizedStack = { + constructPath: '', + content: '', + name: 'testing', + synthesizedStackPath: './foo/stacks/bar', + workingDirectory: './foo' + } + const initialState: DeployState = { status: Status.STARTING, - stackName: "testing", + currentStack, resources: [], url: "https://app.terraform.io/foo/bar", plan: { @@ -108,9 +133,17 @@ test("Diff Multiple Resources", async () => { action: PlannedResourceAction.CREATE }; + const currentStack: SynthesizedStack = { + constructPath: '', + content: '', + name: 'testing', + synthesizedStackPath: './foo/stacks/bar', + workingDirectory: './foo' + } + const initialState: DeployState = { status: Status.STARTING, - stackName: "testing", + currentStack, resources: [], plan: { needsApply: true, diff --git a/packages/cdktf/lib/app.ts b/packages/cdktf/lib/app.ts index e140308591..6f3cb6029f 100644 --- a/packages/cdktf/lib/app.ts +++ b/packages/cdktf/lib/app.ts @@ -1,9 +1,54 @@ import { Construct, Node, ConstructMetadata } from 'constructs'; import * as fs from 'fs'; -import { exit } from 'process'; +import { TerraformStack } from './terraform-stack'; import { version } from '../package.json'; - +import * as path from 'path'; export const CONTEXT_ENV = 'CDKTF_CONTEXT_JSON'; +export interface StackManifest { + name: string; + constructPath: string; + synthesizedStackPath: string; + workingDirectory: string; +} + +export class Manifest { + public static readonly fileName = 'manifest.json'; + public static readonly stacksFolder = 'stacks'; + public static readonly stackFileName = 'cdk.tf.json'; + + public readonly stacks: StackManifest[] = []; + + constructor(public readonly version: string, public readonly outdir: string) { + fs.mkdirSync(this.outdir, Manifest.stacksFolder) + } + + public forStack(stack: TerraformStack): StackManifest { + const node = Node.of(stack) + const manifest = { + name: node.id, + constructPath: node.path, + workingDirectory: path.join(Manifest.stacksFolder, node.id), + synthesizedStackPath: path.join(Manifest.stacksFolder, node.id, Manifest.stackFileName) + } + this.stacks.push(manifest) + + return manifest; + } + + public build() { + return { + version: this.version, + stacks: this.stacks.reduce((newObject: Record, stack: StackManifest) => { + newObject[stack.name] = stack; + return newObject + }, {}) + } + } + + public writeToFile() { + fs.writeFileSync(path.join(this.outdir, Manifest.fileName), JSON.stringify(this.build(), undefined, 2)); + } +} export interface AppOptions { /** @@ -57,7 +102,6 @@ export class App extends Construct { } node.setContext('cdktfVersion', version) - node.setContext('appOutdir', this.outdir) } /** @@ -68,21 +112,16 @@ export class App extends Construct { fs.mkdirSync(this.outdir); } - if (this.targetStackId) { - try { - const stackNode = Node.of(Node.of(this).findChild(this.targetStackId)) - stackNode.synthesize({ - outdir: this.outdir - }); - } catch (e) { - console.error(`Couldn't synth stack ${this.targetStackId} - ${e}`) - exit(1) + const manifest = new Manifest(version, this.outdir) + + Node.of(this).synthesize({ + outdir: this.outdir, + sessionContext: { + manifest } - } else { - Node.of(this).synthesize({ - outdir: this.outdir - }); - } + }); + + manifest.writeToFile(); } private loadContext(defaults: { [key: string]: string } = { }) { diff --git a/packages/cdktf/lib/terraform-stack.ts b/packages/cdktf/lib/terraform-stack.ts index bc93b1b5e0..a61bd1d3a5 100644 --- a/packages/cdktf/lib/terraform-stack.ts +++ b/packages/cdktf/lib/terraform-stack.ts @@ -1,12 +1,13 @@ import { Construct, IConstruct, ISynthesisSession, Node } from 'constructs'; import { resolve } from './_tokens' -import * as fs from 'fs-extra'; +import * as fs from 'fs'; import * as path from 'path'; import { TerraformElement } from './terraform-element'; import { deepMerge } from './util'; import { TerraformProvider } from './terraform-provider'; import { EXCLUDE_STACK_ID_FROM_LOGICAL_IDS, ALLOW_SEP_CHARS_IN_LOGICAL_IDS } from './features'; import { makeUniqueId } from './private/unique'; +import { Manifest } from './app' const STACK_SYMBOL = Symbol.for('ckdtf/TerraformStack'); @@ -16,14 +17,12 @@ export interface TerraformStackMetadata { } export class TerraformStack extends Construct { - public readonly artifactFile: string; private readonly rawOverrides: any = {} private readonly cdktfVersion: string; constructor(scope: Construct, id: string) { super(scope, id); - this.artifactFile = `cdk.tf.json`; this.cdktfVersion = Node.of(this).tryGetContext('cdktfVersion') Object.defineProperty(this, STACK_SYMBOL, { value: true }); @@ -142,12 +141,13 @@ export class TerraformStack extends Construct { } protected onSynthesize(session: ISynthesisSession) { - const stackName = Node.of(this).id + const manifest = session.manifest as Manifest + const stackManifest = manifest.forStack(this) - const workingDirectory = path.join(session.outdir, 'stacks', stackName) - if (!fs.existsSync(workingDirectory)) fs.ensureDirSync(workingDirectory); + const workingDirectory = path.join(session.outdir, stackManifest.workingDirectory) + if (!fs.existsSync(workingDirectory)) fs.mkdirSync(workingDirectory); - fs.writeFileSync(path.join(workingDirectory, this.artifactFile), JSON.stringify(this.toTerraform(), undefined, 2)); + fs.writeFileSync(path.join(session.outdir, stackManifest.synthesizedStackPath), JSON.stringify(this.toTerraform(), undefined, 2)); } } diff --git a/packages/cdktf/test/app.test.ts b/packages/cdktf/test/app.test.ts index 2b78b5f791..23c58f8d40 100644 --- a/packages/cdktf/test/app.test.ts +++ b/packages/cdktf/test/app.test.ts @@ -28,12 +28,6 @@ test('context can be passed through CDKTF_CONTEXT', () => { expect(node.tryGetContext('key2')).toEqual('val2'); }); -test('appOutdir is accessible in context', () => { - const prog = new App(); - const node = Node.of(prog); - expect(node.tryGetContext('appOutdir')).toEqual('cdktf.out'); -}); - test('ckdtfVersion is accessible in context', () => { const prog = new App(); const node = Node.of(prog); From 7b209dd481feb66a3e53c79bec307ae8b2106fe5 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Fri, 16 Apr 2021 16:36:37 +0200 Subject: [PATCH 03/26] Adapt to jsii --- packages/cdktf/lib/app.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cdktf/lib/app.ts b/packages/cdktf/lib/app.ts index 6f3cb6029f..19b771a5d6 100644 --- a/packages/cdktf/lib/app.ts +++ b/packages/cdktf/lib/app.ts @@ -5,10 +5,10 @@ import { version } from '../package.json'; import * as path from 'path'; export const CONTEXT_ENV = 'CDKTF_CONTEXT_JSON'; export interface StackManifest { - name: string; - constructPath: string; - synthesizedStackPath: string; - workingDirectory: string; + readonly name: string; + readonly constructPath: string; + readonly synthesizedStackPath: string; + readonly workingDirectory: string; } export class Manifest { @@ -35,7 +35,7 @@ export class Manifest { return manifest; } - public build() { + public buildManifest(): any { return { version: this.version, stacks: this.stacks.reduce((newObject: Record, stack: StackManifest) => { @@ -46,7 +46,7 @@ export class Manifest { } public writeToFile() { - fs.writeFileSync(path.join(this.outdir, Manifest.fileName), JSON.stringify(this.build(), undefined, 2)); + fs.writeFileSync(path.join(this.outdir, Manifest.fileName), JSON.stringify(this.buildManifest(), undefined, 2)); } } From 7103eb29d66f2b94b1dad34e7ceb8f5f6f27b9d4 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Fri, 16 Apr 2021 16:46:01 +0200 Subject: [PATCH 04/26] Extract manifest in dedicated file --- .../bin/cmds/ui/models/terraform-cloud.ts | 4 +- packages/cdktf/lib/app.ts | 50 +------------------ packages/cdktf/lib/index.ts | 3 +- packages/cdktf/lib/manifest.ts | 50 +++++++++++++++++++ packages/cdktf/lib/terraform-stack.ts | 2 +- 5 files changed, 57 insertions(+), 52 deletions(-) create mode 100644 packages/cdktf/lib/manifest.ts diff --git a/packages/cdktf-cli/bin/cmds/ui/models/terraform-cloud.ts b/packages/cdktf-cli/bin/cmds/ui/models/terraform-cloud.ts index 5ff142908b..4f20ed1a63 100644 --- a/packages/cdktf-cli/bin/cmds/ui/models/terraform-cloud.ts +++ b/packages/cdktf-cli/bin/cmds/ui/models/terraform-cloud.ts @@ -7,7 +7,7 @@ import { TerraformJsonConfigBackendRemote } from '../terraform-json' import * as TerraformCloudClient from '@skorfmann/terraform-cloud' import archiver from 'archiver'; import { WritableStreamBuffer } from 'stream-buffers'; -import { StackManifest } from 'cdktf'; +import { SynthesizedStack } from '../../helper/synth-stack'; export class TerraformCloudPlan implements TerraformPlan { constructor(public readonly planFile: string, public readonly plan: { [key: string]: any }, public readonly url: string) { } @@ -100,7 +100,7 @@ export class TerraformCloud implements Terraform { public readonly workDir: string; public run?: TerraformCloudClient.Run; - constructor(public readonly stack: StackManifest, public readonly config: TerraformJsonConfigBackendRemote, isSpeculative = false) { + constructor(public readonly stack: SynthesizedStack, public readonly config: TerraformJsonConfigBackendRemote, isSpeculative = false) { if (!config.workspaces.name) throw new Error("Please provide a workspace name for Terraform Cloud"); if (!config.organization) throw new Error("Please provide an organization for Terraform Cloud"); this.workDir = stack.workingDirectory diff --git a/packages/cdktf/lib/app.ts b/packages/cdktf/lib/app.ts index 19b771a5d6..8a3665a967 100644 --- a/packages/cdktf/lib/app.ts +++ b/packages/cdktf/lib/app.ts @@ -1,55 +1,9 @@ import { Construct, Node, ConstructMetadata } from 'constructs'; import * as fs from 'fs'; -import { TerraformStack } from './terraform-stack'; import { version } from '../package.json'; -import * as path from 'path'; -export const CONTEXT_ENV = 'CDKTF_CONTEXT_JSON'; -export interface StackManifest { - readonly name: string; - readonly constructPath: string; - readonly synthesizedStackPath: string; - readonly workingDirectory: string; -} - -export class Manifest { - public static readonly fileName = 'manifest.json'; - public static readonly stacksFolder = 'stacks'; - public static readonly stackFileName = 'cdk.tf.json'; - - public readonly stacks: StackManifest[] = []; - - constructor(public readonly version: string, public readonly outdir: string) { - fs.mkdirSync(this.outdir, Manifest.stacksFolder) - } - - public forStack(stack: TerraformStack): StackManifest { - const node = Node.of(stack) - const manifest = { - name: node.id, - constructPath: node.path, - workingDirectory: path.join(Manifest.stacksFolder, node.id), - synthesizedStackPath: path.join(Manifest.stacksFolder, node.id, Manifest.stackFileName) - } - this.stacks.push(manifest) - - return manifest; - } - - public buildManifest(): any { - return { - version: this.version, - stacks: this.stacks.reduce((newObject: Record, stack: StackManifest) => { - newObject[stack.name] = stack; - return newObject - }, {}) - } - } - - public writeToFile() { - fs.writeFileSync(path.join(this.outdir, Manifest.fileName), JSON.stringify(this.buildManifest(), undefined, 2)); - } -} +import { Manifest } from './manifest' +export const CONTEXT_ENV = 'CDKTF_CONTEXT_JSON'; export interface AppOptions { /** * The directory to output Terraform resources. diff --git a/packages/cdktf/lib/index.ts b/packages/cdktf/lib/index.ts index c110a60e15..c096d8a1d5 100644 --- a/packages/cdktf/lib/index.ts +++ b/packages/cdktf/lib/index.ts @@ -17,4 +17,5 @@ export * from './terraform-local'; export * from './terraform-variable'; export * from './terraform-hcl-module'; export * from './runtime'; -export * from './terraform-dependable'; \ No newline at end of file +export * from './terraform-dependable'; +export * from './manifest'; \ No newline at end of file diff --git a/packages/cdktf/lib/manifest.ts b/packages/cdktf/lib/manifest.ts new file mode 100644 index 0000000000..636b885e48 --- /dev/null +++ b/packages/cdktf/lib/manifest.ts @@ -0,0 +1,50 @@ +import { Node } from 'constructs'; +import * as path from 'path'; +import * as fs from 'fs'; +import { TerraformStack } from './terraform-stack'; + +export interface StackManifest { + readonly name: string; + readonly constructPath: string; + readonly synthesizedStackPath: string; + readonly workingDirectory: string; +} + +export class Manifest { + public static readonly fileName = 'manifest.json'; + public static readonly stacksFolder = 'stacks'; + public static readonly stackFileName = 'cdk.tf.json'; + + public readonly stacks: StackManifest[] = []; + + constructor(public readonly version: string, public readonly outdir: string) { + fs.mkdirSync(this.outdir, Manifest.stacksFolder) + } + + public forStack(stack: TerraformStack): StackManifest { + const node = Node.of(stack) + const manifest = { + name: node.id, + constructPath: node.path, + workingDirectory: path.join(Manifest.stacksFolder, node.id), + synthesizedStackPath: path.join(Manifest.stacksFolder, node.id, Manifest.stackFileName) + } + this.stacks.push(manifest) + + return manifest; + } + + public buildManifest(): any { + return { + version: this.version, + stacks: this.stacks.reduce((newObject: Record, stack: StackManifest) => { + newObject[stack.name] = stack; + return newObject + }, {}) + } + } + + public writeToFile() { + fs.writeFileSync(path.join(this.outdir, Manifest.fileName), JSON.stringify(this.buildManifest(), undefined, 2)); + } +} diff --git a/packages/cdktf/lib/terraform-stack.ts b/packages/cdktf/lib/terraform-stack.ts index a61bd1d3a5..272a889eeb 100644 --- a/packages/cdktf/lib/terraform-stack.ts +++ b/packages/cdktf/lib/terraform-stack.ts @@ -7,7 +7,7 @@ import { deepMerge } from './util'; import { TerraformProvider } from './terraform-provider'; import { EXCLUDE_STACK_ID_FROM_LOGICAL_IDS, ALLOW_SEP_CHARS_IN_LOGICAL_IDS } from './features'; import { makeUniqueId } from './private/unique'; -import { Manifest } from './app' +import { Manifest } from './manifest' const STACK_SYMBOL = Symbol.for('ckdtf/TerraformStack'); From 25948e0512889b0de2a843e09c576e7d75a80033 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Fri, 16 Apr 2021 16:47:20 +0200 Subject: [PATCH 05/26] Dynamic version --- packages/cdktf/test/app.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cdktf/test/app.test.ts b/packages/cdktf/test/app.test.ts index 23c58f8d40..825cd52154 100644 --- a/packages/cdktf/test/app.test.ts +++ b/packages/cdktf/test/app.test.ts @@ -1,5 +1,6 @@ import { CONTEXT_ENV, App } from "../lib"; import { Node } from "constructs"; +import { version } from '../package.json'; test('context can be passed through CDKTF_CONTEXT', () => { process.env[CONTEXT_ENV] = JSON.stringify({ @@ -31,5 +32,5 @@ test('context can be passed through CDKTF_CONTEXT', () => { test('ckdtfVersion is accessible in context', () => { const prog = new App(); const node = Node.of(prog); - expect(node.tryGetContext('cdktfVersion')).toEqual('0.0.0'); + expect(node.tryGetContext('cdktfVersion')).toEqual(version); }); \ No newline at end of file From f51ff9e90b061b2dc4a942eeb980d5c768f913bc Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Mon, 19 Apr 2021 14:25:37 +0200 Subject: [PATCH 06/26] Drop orhpaned directories --- packages/cdktf-cli/bin/cmds/helper/synth-stack.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts b/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts index 1e11f12e4c..7a0af8ba7b 100644 --- a/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts +++ b/packages/cdktf-cli/bin/cmds/helper/synth-stack.ts @@ -89,8 +89,9 @@ Command output on stderr: const stackNames = stacks.map(s => s.name) const orphanedDirectories = existingDirectories.filter(e => !stackNames.includes(path.basename(e))) - console.log({orphanedDirectories}) - + for (const orphanedDirectory of orphanedDirectories) { + fs.rmdirSync(orphanedDirectory, { recursive: true }) + } return stacks } From 1efa4ebf41bc8162414eeb3f6341c541fb446a9c Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Mon, 19 Apr 2021 15:27:24 +0200 Subject: [PATCH 07/26] Adjust existing integration tests to changes --- packages/cdktf/lib/manifest.ts | 3 ++- test/csharp/synth-app/test.ts | 2 +- test/java/synth-app/test.ts | 2 +- test/python/synth-app/test.ts | 2 +- test/python/third-party-provider/test.ts | 2 +- test/test-helper.ts | 4 ++-- test/typescript/feature-flags/test.ts | 10 +++++----- test/typescript/modules/test.ts | 4 ++-- test/typescript/synth-app/test.ts | 2 +- 9 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/cdktf/lib/manifest.ts b/packages/cdktf/lib/manifest.ts index 636b885e48..429ef4eb75 100644 --- a/packages/cdktf/lib/manifest.ts +++ b/packages/cdktf/lib/manifest.ts @@ -18,7 +18,8 @@ export class Manifest { public readonly stacks: StackManifest[] = []; constructor(public readonly version: string, public readonly outdir: string) { - fs.mkdirSync(this.outdir, Manifest.stacksFolder) + const stacksPath = path.join(this.outdir, Manifest.stacksFolder) + if (!fs.existsSync(stacksPath)) fs.mkdirSync(stacksPath); } public forStack(stack: TerraformStack): StackManifest { diff --git a/test/csharp/synth-app/test.ts b/test/csharp/synth-app/test.ts index 6c96192463..74b14468d8 100644 --- a/test/csharp/synth-app/test.ts +++ b/test/csharp/synth-app/test.ts @@ -15,6 +15,6 @@ describe("csharp full integration test synth", () => { test("synth generates JSON", async () => { driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('csharp-simple')).toMatchSnapshot() }) }) \ No newline at end of file diff --git a/test/java/synth-app/test.ts b/test/java/synth-app/test.ts index 6cd701a0e5..4da87acdea 100644 --- a/test/java/synth-app/test.ts +++ b/test/java/synth-app/test.ts @@ -15,6 +15,6 @@ describe("java full integration", () => { test("synth generates JSON", async () => { driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('java-simple')).toMatchSnapshot() }) }) \ No newline at end of file diff --git a/test/python/synth-app/test.ts b/test/python/synth-app/test.ts index 36520666b2..392d7ae33e 100644 --- a/test/python/synth-app/test.ts +++ b/test/python/synth-app/test.ts @@ -15,6 +15,6 @@ describe("python full integration test synth", () => { test("synth generates JSON", async () => { driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('python-simple')).toMatchSnapshot() }) }) \ No newline at end of file diff --git a/test/python/third-party-provider/test.ts b/test/python/third-party-provider/test.ts index fb12a10124..f26fc9185e 100644 --- a/test/python/third-party-provider/test.ts +++ b/test/python/third-party-provider/test.ts @@ -15,6 +15,6 @@ describe("python full integration 3rd party", () => { test("synth generates JSON", async () => { driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('python-third-party-provider')).toMatchSnapshot() }) }) \ No newline at end of file diff --git a/test/test-helper.ts b/test/test-helper.ts index 751a752906..99f4cafdcb 100644 --- a/test/test-helper.ts +++ b/test/test-helper.ts @@ -37,8 +37,8 @@ export class TestDriver { fs.copyFileSync(path.join(this.rootDir, source), dest); } - synthesizedStack = () => { - return fs.readFileSync(path.join(this.workingDirectory, 'cdktf.out', 'cdk.tf.json'), 'utf-8') + synthesizedStack = (stackName: string) => { + return fs.readFileSync(path.join(this.workingDirectory, 'cdktf.out', 'stacks', stackName, 'cdk.tf.json'), 'utf-8') } init = (template) => { diff --git a/test/typescript/feature-flags/test.ts b/test/typescript/feature-flags/test.ts index 981ce32a6c..70755d0598 100644 --- a/test/typescript/feature-flags/test.ts +++ b/test/typescript/feature-flags/test.ts @@ -30,27 +30,27 @@ describe("full integration test", () => { test("with excludeStackIdFromLogicalIds feature", () => { writeConfig(driver.workingDirectory, jsonWithContext({ excludeStackIdFromLogicalIds: "true" })) driver.synth() - expect(loadStackJson(driver.workingDirectory)).toMatchSnapshot(); + expect(loadStackJson(driver.workingDirectory, 'hello-deploy')).toMatchSnapshot(); }); test("with allowSepCharsInLogicalIds feature", () => { writeConfig(driver.workingDirectory, jsonWithContext({ allowSepCharsInLogicalIds: "true" })) driver.synth() - expect(loadStackJson(driver.workingDirectory)).toMatchSnapshot(); + expect(loadStackJson(driver.workingDirectory, 'hello-deploy')).toMatchSnapshot(); }); test("without features", () => { writeConfig(driver.workingDirectory, cdktfJSON) driver.synth() - expect(loadStackJson(driver.workingDirectory)).toMatchSnapshot(); + expect(loadStackJson(driver.workingDirectory, 'hello-deploy')).toMatchSnapshot(); }); const jsonWithContext = (context) => { return Object.assign({}, cdktfJSON, { context }) } - const loadStackJson = (workingDir) => { - return fs.readFileSync(path.join(workingDir, 'cdktf.out', 'cdk.tf.json'), 'utf-8') + const loadStackJson = (workingDir, stackName) => { + return fs.readFileSync(path.join(workingDir, 'cdktf.out', 'stacks', stackName, 'cdk.tf.json'), 'utf-8') } const writeConfig = (workingDir, json) => { diff --git a/test/typescript/modules/test.ts b/test/typescript/modules/test.ts index 2950f6ff32..e7d6f7d3a5 100755 --- a/test/typescript/modules/test.ts +++ b/test/typescript/modules/test.ts @@ -25,11 +25,11 @@ describe("full integration test", () => { onPosix("build modules posix", () => { driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('hello-modules')).toMatchSnapshot() }) onWindows("build modules windows", () => { driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('hello-modules')).toMatchSnapshot() }) }) \ No newline at end of file diff --git a/test/typescript/synth-app/test.ts b/test/typescript/synth-app/test.ts index 30947a5e09..26568b02ad 100644 --- a/test/typescript/synth-app/test.ts +++ b/test/typescript/synth-app/test.ts @@ -16,6 +16,6 @@ describe("full integration test synth", () => { test("synth generates JSON", async () => { driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('hello-terra')).toMatchSnapshot() }) }) \ No newline at end of file From 469bd964121d7260e08f4e34edbdf48993e730c3 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Mon, 19 Apr 2021 15:53:22 +0200 Subject: [PATCH 08/26] Remove obsolete snapshot --- .../module-generator.test.ts.snap | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/packages/cdktf-cli/test/get/generator/__snapshots__/module-generator.test.ts.snap b/packages/cdktf-cli/test/get/generator/__snapshots__/module-generator.test.ts.snap index feae8a92c9..ebb0927283 100644 --- a/packages/cdktf-cli/test/get/generator/__snapshots__/module-generator.test.ts.snap +++ b/packages/cdktf-cli/test/get/generator/__snapshots__/module-generator.test.ts.snap @@ -8030,47 +8030,3 @@ export class Module extends TerraformModule { } " `; - -exports[`typeless variables 2`] = ` -"// generated by cdktf get -// ./module -import { TerraformModule } from 'cdktf'; -import { Construct } from 'constructs'; -export interface ModuleOptions { - /** - * My example var without type set, but with default - * @default 1 - */ - readonly myDefaultTypevar?: number; - /** - * My example var without type set - */ - readonly myTypelessVar: any; -} -export class Module extends TerraformModule { - private readonly inputs: { [name: string]: any } = { } - public constructor(scope: Construct, id: string, options: ModuleOptions) { - super(scope, id, { - source: './module', - }); - this.myDefaultTypevar = options.myDefaultTypevar; - this.myTypelessVar = options.myTypelessVar; - } - public get myDefaultTypevar(): number | undefined { - return this.inputs['my_default_typevar'] as number | undefined; - } - public set myDefaultTypevar(value: number | undefined) { - this.inputs['my_default_typevar'] = value; - } - public get myTypelessVar(): any { - return this.inputs['my_typeless_var'] as any; - } - public set myTypelessVar(value: any) { - this.inputs['my_typeless_var'] = value; - } - protected synthesizeAttributes() { - return this.inputs; - } -} -" -`; From ef653152c9cf8238d2192926f3012565646797a6 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Mon, 19 Apr 2021 16:15:58 +0200 Subject: [PATCH 09/26] Trying to prevent random 422 errors in TF Cloud --- test/typescript/terraform-cloud/test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/typescript/terraform-cloud/test.ts b/test/typescript/terraform-cloud/test.ts index e9b35bc880..cf1fd9d7c4 100755 --- a/test/typescript/terraform-cloud/test.ts +++ b/test/typescript/terraform-cloud/test.ts @@ -15,6 +15,10 @@ if (withAuth == it.skip) { console.log('TERRAFORM_CLOUD_TOKEN is undefined, skipping authed tests') } +const delay = (ms: number) => { + return new Promise(resolve => setTimeout(resolve, ms)); +} + describe("full integration test", () => { let driver: TestDriver; let workspaceName: string; @@ -48,6 +52,9 @@ describe("full integration test", () => { } }) + // Trying to prevent random 422 errrors + await delay(1000) + expect(driver.deploy()).toMatchSnapshot() await client.Workspaces.deleteByName(orgName, workspaceName) }) From a18898c5a217d50962196f17c0ad4b1395ef328d Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Tue, 20 Apr 2021 10:14:13 +0200 Subject: [PATCH 10/26] Integegration test for multiple stacks --- packages/cdktf-cli/bin/cmds/synth.ts | 6 +- packages/cdktf-cli/bin/cmds/ui/synth.tsx | 15 ++- .../bin/cmds/ui/terraform-context.tsx | 5 + test/test-helper.ts | 18 +-- .../__snapshots__/test.ts.snap | 92 +++++++++++++++ test/typescript/multiple-stacks/cdktf.json | 10 ++ test/typescript/multiple-stacks/main.ts | 22 ++++ test/typescript/multiple-stacks/test.ts | 106 ++++++++++++++++++ 8 files changed, 258 insertions(+), 16 deletions(-) create mode 100644 test/typescript/multiple-stacks/__snapshots__/test.ts.snap create mode 100644 test/typescript/multiple-stacks/cdktf.json create mode 100644 test/typescript/multiple-stacks/main.ts create mode 100644 test/typescript/multiple-stacks/test.ts diff --git a/packages/cdktf-cli/bin/cmds/synth.ts b/packages/cdktf-cli/bin/cmds/synth.ts index b652463ab0..a5be7455f0 100644 --- a/packages/cdktf-cli/bin/cmds/synth.ts +++ b/packages/cdktf-cli/bin/cmds/synth.ts @@ -9,11 +9,12 @@ import { displayVersionMessage } from './version-check' const config = readConfigSync(); class Command implements yargs.CommandModule { - public readonly command = 'synth [OPTIONS]'; + public readonly command = 'synth [stack] [OPTIONS]'; public readonly describe = 'Synthesizes Terraform code for the given app in a directory.'; public readonly aliases = [ 'synthesize' ]; public readonly builder = (args: yargs.Argv) => args + .positional('stack', { desc: 'Stack to output when using --json flag', type: 'string' }) .option('app', { default: config.app, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, desc: 'Output directory', alias: 'o' }) .option('json', { type: 'boolean', desc: 'Provide JSON output for the generated Terraform configuration.', default: false }) @@ -24,13 +25,14 @@ class Command implements yargs.CommandModule { const command = argv.app; const outdir = argv.output; const jsonOutput = argv.json; + const stack = argv.stack; if (config.checkCodeMakerOutput && !await fs.pathExists(config.codeMakerOutput)) { console.error(`ERROR: synthesis failed, run "cdktf get" to generate providers in ${config.codeMakerOutput}`); process.exit(1); } - await renderInk(React.createElement(Synth, { targetDir: outdir, synthCommand: command, jsonOutput: jsonOutput })) + await renderInk(React.createElement(Synth, { targetDir: outdir, targetStack: stack, synthCommand: command, jsonOutput: jsonOutput })) } } diff --git a/packages/cdktf-cli/bin/cmds/ui/synth.tsx b/packages/cdktf-cli/bin/cmds/ui/synth.tsx index a693f51323..f42a56cadd 100644 --- a/packages/cdktf-cli/bin/cmds/ui/synth.tsx +++ b/packages/cdktf-cli/bin/cmds/ui/synth.tsx @@ -5,6 +5,7 @@ import { useTerraform, Status, useTerraformState } from './terraform-context' interface CommonSynthConfig { targetDir: string; + targetStack: string; jsonOutput: boolean; } @@ -17,17 +18,21 @@ interface SynthConfig extends CommonSynthConfig { } const SynthOutput = ({ jsonOutput }: SynthOutputConfig): React.ReactElement => { - const { currentStack } = useTerraformState() + const { currentStack, stacks } = useTerraformState() return( <> - { jsonOutput ? ({currentStack.content}) : (Generated Terraform code in the output directory: {currentStack.workingDirectory}) } + { jsonOutput ? ( + {currentStack.content} + ) : ( + Generated Terraform code for the stacks: {stacks?.map(s => s.name).join(', ')} + ) } ) } -export const Synth = ({ targetDir, synthCommand, jsonOutput }: SynthConfig): React.ReactElement => { - const { synth } = useTerraform({targetDir, synthCommand}) +export const Synth = ({ targetDir, targetStack, synthCommand, jsonOutput }: SynthConfig): React.ReactElement => { + const { synth } = useTerraform({targetDir, targetStack, synthCommand}) const { status, currentStack, errors } = synth() const isSynthesizing: boolean = status != Status.SYNTHESIZED @@ -49,7 +54,7 @@ export const Synth = ({ targetDir, synthCommand, jsonOutput }: SynthConfig): Rea ) : ( - + )} diff --git a/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx b/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx index 993dd08b65..4c581b3037 100644 --- a/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx +++ b/packages/cdktf-cli/bin/cmds/ui/terraform-context.tsx @@ -270,6 +270,11 @@ export const useTerraform = ({ targetDir, targetStack, synthCommand, isSpeculati dispatch({ type: 'CURRENT_STACK', currentStack: stack }) await executorForStack(stack) + } else { // synth + const stack = targetStack ? stacks.find(s => s.name === targetStack) : stacks[0] + if (stack) { + dispatch({ type: 'CURRENT_STACK', currentStack: stack }) + } } dispatch({ type: 'SYNTHESIZED', stacks }) diff --git a/test/test-helper.ts b/test/test-helper.ts index 99f4cafdcb..d2007940c1 100644 --- a/test/test-helper.ts +++ b/test/test-helper.ts @@ -13,7 +13,7 @@ export class TestDriver { private execSync(command: string) { try { - execSync(command, { stdio: "pipe", env: this.env }); + return execSync(command, { stdio: "pipe", env: this.env }); } catch(e) { console.log(e.stdout.toString()) console.error(e.stderr.toString()) @@ -52,20 +52,20 @@ export class TestDriver { execSync(`cdktf get`, { stdio: "inherit", env: this.env }); } - synth = () => { - this.execSync(`cdktf synth`); + synth = (flags?: string) => { + return this.execSync(`cdktf synth ${flags ? flags : ''}`).toString(); } - diff = () => { - return execSync(`cdktf diff`, { env: this.env }).toString(); + diff = (stackName?: string) => { + return execSync(`cdktf diff ${stackName ? stackName : ''}`, { env: this.env }).toString(); } - deploy = () => { - return execSync(`cdktf deploy --auto-approve`, { env: this.env }).toString(); + deploy = (stackName?: string) => { + return execSync(`cdktf deploy ${stackName ? stackName : ''} --auto-approve`, { env: this.env }).toString(); } - destroy = () => { - return execSync(`cdktf destroy --auto-approve`, { env: this.env }).toString(); + destroy = (stackName?: string) => { + return execSync(`cdktf destroy ${stackName ? stackName : ''} --auto-approve`, { env: this.env }).toString(); } setupTypescriptProject = () => { diff --git a/test/typescript/multiple-stacks/__snapshots__/test.ts.snap b/test/typescript/multiple-stacks/__snapshots__/test.ts.snap new file mode 100644 index 0000000000..2d70726ffa --- /dev/null +++ b/test/typescript/multiple-stacks/__snapshots__/test.ts.snap @@ -0,0 +1,92 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`full integration test synth 1`] = ` +"{ + \\"//\\": { + \\"metadata\\": { + \\"version\\": \\"stubbed\\", + \\"stackName\\": \\"first\\" + } + }, + \\"resource\\": { + \\"null_resource\\": { + \\"test\\": { + \\"provisioner\\": [ + { + \\"local-exec\\": { + \\"command\\": \\"echo \\\\\\"hello deploy\\\\\\"\\" + } + } + ], + \\"//\\": { + \\"metadata\\": { + \\"path\\": \\"first/test\\", + \\"uniqueId\\": \\"test\\" + } + } + } + } + } +}" +`; + +exports[`full integration test synth 2`] = ` +"{ + \\"//\\": { + \\"metadata\\": { + \\"version\\": \\"stubbed\\", + \\"stackName\\": \\"second\\" + } + }, + \\"resource\\": { + \\"null_resource\\": { + \\"test\\": { + \\"provisioner\\": [ + { + \\"local-exec\\": { + \\"command\\": \\"echo \\\\\\"hello deploy\\\\\\"\\" + } + } + ], + \\"//\\": { + \\"metadata\\": { + \\"path\\": \\"second/test\\", + \\"uniqueId\\": \\"test\\" + } + } + } + } + } +}" +`; + +exports[`full integration test synth with json output 1`] = ` +"{ + \\"//\\": { + \\"metadata\\": { + \\"version\\": \\"stubbed\\", + \\"stackName\\": \\"first\\" + } + }, + \\"resource\\": { + \\"null_resource\\": { + \\"test\\": { + \\"provisioner\\": [ + { + \\"local-exec\\": { + \\"command\\": \\"echo \\\\\\"hello deploy\\\\\\"\\" + } + } + ], + \\"//\\": { + \\"metadata\\": { + \\"path\\": \\"first/test\\", + \\"uniqueId\\": \\"test\\" + } + } + } + } + } +} +" +`; diff --git a/test/typescript/multiple-stacks/cdktf.json b/test/typescript/multiple-stacks/cdktf.json new file mode 100644 index 0000000000..827ed077b2 --- /dev/null +++ b/test/typescript/multiple-stacks/cdktf.json @@ -0,0 +1,10 @@ +{ + "language": "typescript", + "app": "npm run --silent compile && node main.js", + "terraformProviders": [ + "null" + ], + "context": { + "excludeStackIdFromLogicalIds": "true" + } +} \ No newline at end of file diff --git a/test/typescript/multiple-stacks/main.ts b/test/typescript/multiple-stacks/main.ts new file mode 100644 index 0000000000..5ac4484f42 --- /dev/null +++ b/test/typescript/multiple-stacks/main.ts @@ -0,0 +1,22 @@ +import { Construct } from 'constructs'; +import { App, TerraformStack, Testing } from 'cdktf'; +import * as NullProvider from './.gen/providers/null'; + +export class HelloTerra extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const nullResouce = new NullProvider.Resource(this, 'test', {}) + + nullResouce.addOverride('provisioner', [{ + 'local-exec': { + command: `echo "hello deploy"` + } + }]); + } +} + +const app = Testing.stubVersion(new App({stackTraces: false})); +new HelloTerra(app, 'first'); +new HelloTerra(app, 'second'); +app.synth(); \ No newline at end of file diff --git a/test/typescript/multiple-stacks/test.ts b/test/typescript/multiple-stacks/test.ts new file mode 100644 index 0000000000..211a61e648 --- /dev/null +++ b/test/typescript/multiple-stacks/test.ts @@ -0,0 +1,106 @@ +/** + * Testing a full cycle of diff, deploy and destroy + * + * @group typescript + */ + +import { TestDriver } from "../../test-helper"; + +describe("full integration test", () => { + let driver: TestDriver; + + beforeAll(() => { + driver = new TestDriver(__dirname) + driver.setupTypescriptProject() + }); + + test("synth", () => { + driver.synth() + expect(driver.synthesizedStack('first')).toMatchSnapshot() + expect(driver.synthesizedStack('second')).toMatchSnapshot() + }); + + test("synth with json output", () => { + expect(driver.synth('--json')).toMatchSnapshot() + }); + + test("diff", () => { + expect(driver.diff('first')).toMatchInlineSnapshot(` + "Stack: first + Resources + + NULL_RESOURCE test null_resource.test + + + Diff: 1 to create, 0 to update, 0 to delete. + " + `); + + + expect(driver.diff('second')).toMatchInlineSnapshot(` + "Stack: second + Resources + + NULL_RESOURCE test null_resource.test + + + Diff: 1 to create, 0 to update, 0 to delete. + " + `); + + + let error; + try { driver.diff() } catch(e) { error = e.message } + expect(error).toMatch('Found more than one stack, please specify a target stack first, second'); + }); + + test("deploy", () => { + expect(driver.deploy('first')).toMatchInlineSnapshot(` + "Deploying Stack: first + Resources + ✔ NULL_RESOURCE test null_resource.test + + + Summary: 1 created, 0 updated, 0 destroyed. + " + `); + + expect(driver.deploy('second')).toMatchInlineSnapshot(` + "Deploying Stack: second + Resources + ✔ NULL_RESOURCE test null_resource.test + + + Summary: 1 created, 0 updated, 0 destroyed. + " + `); + + let error; + try { driver.deploy() } catch(e) { error = e.message } + expect(error).toMatch('Found more than one stack, please specify a target stack first, second'); + }); + + test("destroy", () => { + expect(driver.destroy('first')).toMatchInlineSnapshot(` + "Destroying Stack: first + Resources + ✔ NULL_RESOURCE test null_resource.test + + + Summary: 1 destroyed. + " + `); + + expect(driver.destroy('second')).toMatchInlineSnapshot(` + "Destroying Stack: second + Resources + ✔ NULL_RESOURCE test null_resource.test + + + Summary: 1 destroyed. + " + `); + + let error; + try { driver.destroy() } catch(e) { error = e.message } + expect(error).toMatch('Found more than one stack, please specify a target stack first, second'); + }); +}); From c5105b3379ea004ddb16d7af220068126c2fdeba Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Tue, 20 Apr 2021 10:14:48 +0200 Subject: [PATCH 11/26] List stacks command --- packages/cdktf-cli/bin/cmds/list.ts | 28 ++++++++++++ packages/cdktf-cli/bin/cmds/ui/list.tsx | 57 +++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 packages/cdktf-cli/bin/cmds/list.ts create mode 100644 packages/cdktf-cli/bin/cmds/ui/list.tsx diff --git a/packages/cdktf-cli/bin/cmds/list.ts b/packages/cdktf-cli/bin/cmds/list.ts new file mode 100644 index 0000000000..8ca82ec49c --- /dev/null +++ b/packages/cdktf-cli/bin/cmds/list.ts @@ -0,0 +1,28 @@ +import yargs from 'yargs' +import React from 'react'; +import { List } from './ui/list' +import { readConfigSync } from '../../lib/config'; +import { renderInk } from './render-ink' +import { displayVersionMessage } from './version-check' + +const config = readConfigSync(); + +class Command implements yargs.CommandModule { + public readonly command = 'list [OPTIONS]'; + public readonly describe = 'List stacks in app.'; + + public readonly builder = (args: yargs.Argv) => args + .option('app', { default: config.app, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) + .option('output', { default: config.output, desc: 'Output directory', alias: 'o' }) + .showHelpOnFail(true); + + public async handler(argv: any) { + await displayVersionMessage() + const command = argv.app; + const outdir = argv.output; + + await renderInk(React.createElement(List, { targetDir: outdir, synthCommand: command })) + } +} + +module.exports = new Command(); diff --git a/packages/cdktf-cli/bin/cmds/ui/list.tsx b/packages/cdktf-cli/bin/cmds/ui/list.tsx new file mode 100644 index 0000000000..d2ff0e24fe --- /dev/null +++ b/packages/cdktf-cli/bin/cmds/ui/list.tsx @@ -0,0 +1,57 @@ +import React, { Fragment } from "react"; +import { Text, Box } from "ink"; +import Spinner from "ink-spinner"; +import { useTerraform, Status } from './terraform-context' + +interface ListConfig { + targetDir: string; + synthCommand: string; +} + +export const List = ({ targetDir, synthCommand }: ListConfig): React.ReactElement => { + const { synth } = useTerraform({targetDir, synthCommand}) + const { status, errors, stacks } = synth() + + const isSynthesizing: boolean = status != Status.SYNTHESIZED + const statusText = `${status}...` + + if (errors) return({ errors }); + + return ( + + {isSynthesizing ? ( + + + + + + {statusText} + + + ) : ( + + + + + Stack name + + + Path + + + {stacks?.map(stack => ( + + + {stack.name} + + + {stack.workingDirectory} + + + ))} + + + )} + + ); +}; From 75eaaf0f93389f5b7b3e5273dbe16e844314ae11 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Tue, 20 Apr 2021 14:23:12 +0200 Subject: [PATCH 12/26] Perhaps it needs even more time? --- test/typescript/terraform-cloud/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/typescript/terraform-cloud/test.ts b/test/typescript/terraform-cloud/test.ts index cf1fd9d7c4..856afa686f 100755 --- a/test/typescript/terraform-cloud/test.ts +++ b/test/typescript/terraform-cloud/test.ts @@ -53,7 +53,7 @@ describe("full integration test", () => { }) // Trying to prevent random 422 errrors - await delay(1000) + await delay(3000) expect(driver.deploy()).toMatchSnapshot() await client.Workspaces.deleteByName(orgName, workspaceName) From 6322a7ef05f00ce1d0ba5bf22659953259723d25 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 09:57:25 +0200 Subject: [PATCH 13/26] Add docs for multiple stacks --- .../app-stacks-concept.md | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 docs/working-with-cdk-for-terraform/app-stacks-concept.md diff --git a/docs/working-with-cdk-for-terraform/app-stacks-concept.md b/docs/working-with-cdk-for-terraform/app-stacks-concept.md new file mode 100644 index 0000000000..7f7fb644b5 --- /dev/null +++ b/docs/working-with-cdk-for-terraform/app-stacks-concept.md @@ -0,0 +1,144 @@ +# App / Stacks Concept + +In CDK for Terraform we have the concept of an Application which consists of one or more Stacks. + +## Constructs + +CDK for Terraform apps are structured as a tree of [constructs](https://github.com/aws/constructs). The classes `App`, `TerraformStack`, `TerraformResource` and `Resource` are all deriving from `Construct` and are therefore represented as a node in the application tree, where the `App` node is the root. + +## Application + +The app is host of stacks and the root node in the constructs tree. It can be used to provide global configuration to each stack and underlying constructs. + +### App Context + +One option to provide global configuration is the app `context`, which can be accessed in any construct within the app. + +```typescript +import { Construct } from "constructs"; +import { App, TerraformStack } from "cdktf"; +import { AwsProvider, Instance } from "./.gen/providers/aws"; + +class MyStack extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + new AwsProvider(this, "aws", { + region: "us-east-1", + }); + + new Instance(this, "Hello", { + ami: "ami-2757f631", + instanceType: "t2.micro", + tags: { + myConfig: this.constructNode.getContext('myConfig') + } + }); + } +} + +const app = new App({context: {myConfig: 'config'}}); +new MyStack(app, "hello-cdktf"); +app.synth(); +``` + +## Stack + +A stack represents a collection of infrastructure which will be synthesized as a dedicated Terraform configuration. In comparision to the Terraform CLI, a Stack is equal to a dedicated working directory. Stacks are useful to separate the state management within an application. + +## A single Stack + +The following example will synthesize a single Terraform configuration in the configured output folder. When running `cdktf synth` we'll find the synthesized Terraform configuration in the folder `cdktf.out/stacks/a-single-stack` + +```typescript +import { Construct } from "constructs"; +import { App, TerraformStack } from "cdktf"; +import { AwsProvider, Instance } from "./.gen/providers/aws"; + +class MyStack extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + new AwsProvider(this, "aws", { + region: "us-east-1", + }); + + new Instance(this, "Hello", { + ami: "ami-2757f631", + instanceType: "t2.micro" + }); + } +} + +const app = new App(); +new MyStack(app, "a-single-stack"); +app.synth(); +``` + +## Multiple Stacks + +The following example will synthesize multiple Terraform configurations in the configured output folder. + +```typescript +import { Construct } from "constructs"; +import { App, TerraformStack } from "cdktf"; +import { AwsProvider, Instance } from "./.gen/providers/aws"; + +interface MyStackConfig { + environment: string, + region?: string; +} + +class MyStack extends TerraformStack { + constructor(scope: Construct, id: string, config: MyStackConfig) { + super(scope, id); + + const { region = 'us-east-1' } = config + + new AwsProvider(this, "aws", { + region, + }); + + new Instance(this, "Hello", { + ami: "ami-2757f631", + instanceType: "t2.micro", + tags: { + environment: config.environment + } + }); + } +} + +const app = new App(); +new MyStack(app, "multiple-stacks-dev", { environment: 'dev' }); +new MyStack(app, "multiple-stacks-staging", { environment: 'staging' }); +new MyStack(app, "multiple-stacks-production-us", { environment: 'production', region: 'us-east-1' }); +new MyStack(app, "multiple-stacks-production-eu", { environment: 'production', region: 'eu-central-1' }); +app.synth(); +``` + +After running `cdktf synth` we can see the following synthesized stacks: + +``` +$ cdktf list + +Stack name Path +multiple-stacks-dev cdktf.out/stacks/multiple-stacks-dev +multiple-stacks-staging cdktf.out/stacks/multiple-stacks-staging +multiple-stacks-production-us cdktf.out/stacks/multiple-stacks-production-us +multiple-stacks-production-eu cdktf.out/stacks/multiple-stacks-production-eu +``` + +### Limitations + +At the moment all Terraform operations are limited to a single stack. In order to run `diff`, `deploy` or `destroy`, a target stack has to be specified. A deploy command like `cdktf deploy multiple-stacks-dev` will work and all Terraform operations will run in the folder `cdktf.out/stacks/multiple-stacks-dev`. + +Omitting the target stack by running a plain `cdktf deploy` will result in error. This will change in future versions, where support for targeting all or a subset of stacks will be added. + +### Cross Stack References + +Referencing resources from another stack is not yet supported automatically. It can be achieved manually by using Outputs and the Remote State data source. Please track this [issue](https://github.com/hashicorp/terraform-cdk/issues/651) when you're interested in this feature. + +### Migration from `<= 0.2` + +Up until CDK for Terraform version `0.2` only a single stack was supported. For local state handling, a `terraform.tfstate` in the project root folder was used. With version `>= 0.3` the local state file reflects the stack name it belongs to in its file name. When a `terraform.tfstate` file is still present in the project root folder, it has to be renamed to match the schema `terraform..tfstate` manually. From 67acd3498e86b99ddb811b0741157a498e40fb27 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 10:01:41 +0200 Subject: [PATCH 14/26] Update docs --- .../app-stacks-concept.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/working-with-cdk-for-terraform/app-stacks-concept.md b/docs/working-with-cdk-for-terraform/app-stacks-concept.md index 7f7fb644b5..98175dbea2 100644 --- a/docs/working-with-cdk-for-terraform/app-stacks-concept.md +++ b/docs/working-with-cdk-for-terraform/app-stacks-concept.md @@ -129,15 +129,21 @@ multiple-stacks-production-us cdktf.out/stacks/multiple-stacks-production-us multiple-stacks-production-eu cdktf.out/stacks/multiple-stacks-production-eu ``` -### Limitations +### Current Limitations + +#### Deployments At the moment all Terraform operations are limited to a single stack. In order to run `diff`, `deploy` or `destroy`, a target stack has to be specified. A deploy command like `cdktf deploy multiple-stacks-dev` will work and all Terraform operations will run in the folder `cdktf.out/stacks/multiple-stacks-dev`. Omitting the target stack by running a plain `cdktf deploy` will result in error. This will change in future versions, where support for targeting all or a subset of stacks will be added. -### Cross Stack References +Please track this [issue](https://github.com/hashicorp/terraform-cdk/issues/650) when you're interested in this feature. + +#### Cross Stack References + +Referencing resources from another stack is not yet supported automatically. It can be achieved manually by using Outputs and the Remote State data source. -Referencing resources from another stack is not yet supported automatically. It can be achieved manually by using Outputs and the Remote State data source. Please track this [issue](https://github.com/hashicorp/terraform-cdk/issues/651) when you're interested in this feature. +Please track this [issue](https://github.com/hashicorp/terraform-cdk/issues/651) when you're interested in this feature. ### Migration from `<= 0.2` From 037c96d47d60c70c90385bf13db819dd149c8026 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 10:13:21 +0200 Subject: [PATCH 15/26] Integration test for cdktf list --- test/test-helper.ts | 4 ++++ test/typescript/multiple-stacks/test.ts | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/test/test-helper.ts b/test/test-helper.ts index d2007940c1..55cf4b6c19 100644 --- a/test/test-helper.ts +++ b/test/test-helper.ts @@ -56,6 +56,10 @@ export class TestDriver { return this.execSync(`cdktf synth ${flags ? flags : ''}`).toString(); } + list = (flags?: string) => { + return this.execSync(`cdktf list ${flags ? flags : ''}`).toString(); + } + diff = (stackName?: string) => { return execSync(`cdktf diff ${stackName ? stackName : ''}`, { env: this.env }).toString(); } diff --git a/test/typescript/multiple-stacks/test.ts b/test/typescript/multiple-stacks/test.ts index 211a61e648..3b7236222b 100644 --- a/test/typescript/multiple-stacks/test.ts +++ b/test/typescript/multiple-stacks/test.ts @@ -52,6 +52,15 @@ describe("full integration test", () => { expect(error).toMatch('Found more than one stack, please specify a target stack first, second'); }); + test("list", () => { + expect(driver.list()).toMatchInlineSnapshot(` + "Stack name Path + first cdktf.out/stacks/first + second cdktf.out/stacks/second + " + `); + }); + test("deploy", () => { expect(driver.deploy('first')).toMatchInlineSnapshot(` "Deploying Stack: first From 4023a455a501de8883d7c240131399ba2083e0a6 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 11:01:21 +0200 Subject: [PATCH 16/26] Test manifest --- packages/cdktf/test/manifest.test.ts | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 packages/cdktf/test/manifest.test.ts diff --git a/packages/cdktf/test/manifest.test.ts b/packages/cdktf/test/manifest.test.ts new file mode 100644 index 0000000000..9521756a35 --- /dev/null +++ b/packages/cdktf/test/manifest.test.ts @@ -0,0 +1,66 @@ +import { TerraformStack, Manifest, App } from "../lib"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; + +test("filename", () => { + expect(Manifest.fileName).toEqual("manifest.json"); +}); + +test("stacksFolder", () => { + expect(Manifest.stacksFolder).toEqual("stacks"); +}); + +test("stacksFilename", () => { + expect(Manifest.stackFileName).toEqual("cdk.tf.json"); +}); + +test("create stacks folder", () => { + const outdir = fs.mkdtempSync(path.join(os.tmpdir(), "cdktf.outdir.")); + new Manifest("0.0.0", outdir); + expect(fs.existsSync(path.join(outdir, Manifest.stacksFolder))).toBeTruthy(); +}); + +test("get stack manifest", () => { + const outdir = fs.mkdtempSync(path.join(os.tmpdir(), "cdktf.outdir.")); + const manifest = new Manifest("0.0.0", outdir); + + const app = new App(); + const stackManifest = manifest.forStack( + new TerraformStack(app, "this-is-a-stack") + ); + + expect(stackManifest).toMatchInlineSnapshot(` + Object { + "constructPath": "this-is-a-stack", + "name": "this-is-a-stack", + "synthesizedStackPath": "stacks/this-is-a-stack/cdk.tf.json", + "workingDirectory": "stacks/this-is-a-stack", + } + `); +}); + +test("write manifest", () => { + const outdir = fs.mkdtempSync(path.join(os.tmpdir(), "cdktf.outdir.")); + const manifest = new Manifest("0.0.0", outdir); + + const app = new App(); + manifest.forStack(new TerraformStack(app, "this-is-a-stack")); + + manifest.writeToFile(); + + expect(fs.readFileSync(path.join(outdir, Manifest.fileName)).toString()) + .toMatchInlineSnapshot(` + "{ + \\"version\\": \\"0.0.0\\", + \\"stacks\\": { + \\"this-is-a-stack\\": { + \\"name\\": \\"this-is-a-stack\\", + \\"constructPath\\": \\"this-is-a-stack\\", + \\"workingDirectory\\": \\"stacks/this-is-a-stack\\", + \\"synthesizedStackPath\\": \\"stacks/this-is-a-stack/cdk.tf.json\\" + } + } + }" + `); +}); From 3159db83adaeea9aa31de42d16208c8b644bd52d Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 11:06:39 +0200 Subject: [PATCH 17/26] Explicit multiple stacks example --- .../{aws => aws-multiple-stacks}/.gitignore | 0 .../{aws => aws-multiple-stacks}/README.md | 0 .../{aws => aws-multiple-stacks}/cdktf.json | 0 .../typescript/aws-multiple-stacks/main.ts | 35 +++++++++++++ .../{aws => aws-multiple-stacks}/package.json | 0 .../tsconfig.json | 0 examples/typescript/aws/main.ts | 52 ------------------- 7 files changed, 35 insertions(+), 52 deletions(-) rename examples/typescript/{aws => aws-multiple-stacks}/.gitignore (100%) rename examples/typescript/{aws => aws-multiple-stacks}/README.md (100%) rename examples/typescript/{aws => aws-multiple-stacks}/cdktf.json (100%) create mode 100644 examples/typescript/aws-multiple-stacks/main.ts rename examples/typescript/{aws => aws-multiple-stacks}/package.json (100%) rename examples/typescript/{aws => aws-multiple-stacks}/tsconfig.json (100%) delete mode 100644 examples/typescript/aws/main.ts diff --git a/examples/typescript/aws/.gitignore b/examples/typescript/aws-multiple-stacks/.gitignore similarity index 100% rename from examples/typescript/aws/.gitignore rename to examples/typescript/aws-multiple-stacks/.gitignore diff --git a/examples/typescript/aws/README.md b/examples/typescript/aws-multiple-stacks/README.md similarity index 100% rename from examples/typescript/aws/README.md rename to examples/typescript/aws-multiple-stacks/README.md diff --git a/examples/typescript/aws/cdktf.json b/examples/typescript/aws-multiple-stacks/cdktf.json similarity index 100% rename from examples/typescript/aws/cdktf.json rename to examples/typescript/aws-multiple-stacks/cdktf.json diff --git a/examples/typescript/aws-multiple-stacks/main.ts b/examples/typescript/aws-multiple-stacks/main.ts new file mode 100644 index 0000000000..760b09c3ba --- /dev/null +++ b/examples/typescript/aws-multiple-stacks/main.ts @@ -0,0 +1,35 @@ +import { Construct } from "constructs"; +import { App, TerraformStack } from "cdktf"; +import { AwsProvider, Instance } from "./.gen/providers/aws"; + +interface MyStackConfig { + environment: string, + region?: string; +} + +class MyStack extends TerraformStack { + constructor(scope: Construct, id: string, config: MyStackConfig) { + super(scope, id); + + const { region = 'us-east-1' } = config + + new AwsProvider(this, "aws", { + region, + }); + + new Instance(this, "Hello", { + ami: "ami-2757f631", + instanceType: "t2.micro", + tags: { + environment: config.environment + } + }); + } +} + +const app = new App(); +new MyStack(app, "multiple-stacks-dev", { environment: 'dev' }); +new MyStack(app, "multiple-stacks-staging", { environment: 'staging' }); +new MyStack(app, "multiple-stacks-production-us", { environment: 'production', region: 'us-east-1' }); +new MyStack(app, "multiple-stacks-production-eu", { environment: 'production', region: 'eu-central-1' }); +app.synth(); \ No newline at end of file diff --git a/examples/typescript/aws/package.json b/examples/typescript/aws-multiple-stacks/package.json similarity index 100% rename from examples/typescript/aws/package.json rename to examples/typescript/aws-multiple-stacks/package.json diff --git a/examples/typescript/aws/tsconfig.json b/examples/typescript/aws-multiple-stacks/tsconfig.json similarity index 100% rename from examples/typescript/aws/tsconfig.json rename to examples/typescript/aws-multiple-stacks/tsconfig.json diff --git a/examples/typescript/aws/main.ts b/examples/typescript/aws/main.ts deleted file mode 100644 index 5a9c014616..0000000000 --- a/examples/typescript/aws/main.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Construct } from 'constructs'; -import { App, TerraformStack, TerraformOutput } from 'cdktf'; -import { DynamodbTable } from './.gen/providers/aws/dynamodb-table'; -import { SnsTopic } from './.gen/providers/aws/sns-topic'; -import { DataAwsRegion, AwsProvider } from './.gen/providers/aws' - -export class HelloTerra extends TerraformStack { - constructor(scope: Construct, id: string) { - super(scope, id); - - new AwsProvider(this, 'aws', { - region: 'eu-central-1' - }) - - const region = new DataAwsRegion(this, 'region') - - const table = new DynamodbTable(this, 'Hello', { - name: `my-first-table-${region.name}`, - hashKey: 'temp', - attribute: [ - { name: 'id', type: 'S' }, - ], - billingMode: "PAY_PER_REQUEST" - }); - - table.addOverride('hash_key', 'id') - // table.addOverride('hash_key', 'foo') - table.addOverride('lifecycle', { create_before_destroy: true }) - - const topicCount = 1 - const topics = [...Array(topicCount).keys()].map((i) => { - return new SnsTopic(this, `Topic${i}`, { - displayName: `my-first-sns-topic${i}` - }); - }) - - new TerraformOutput(this, 'table_name', { - value: table.name - }) - - topics.forEach((topic, i) => { - new TerraformOutput(this, `sns_topic${i}`, { - value: topic.name - }) - }) - } -} - -const app = new App(); -new HelloTerra(app, 'hello-terra'); -new HelloTerra(app, 'hello-terra-production'); -app.synth(); \ No newline at end of file From ca8d63e2ed72e4c0f64aa74583746a4353ddbdae Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 14:59:01 +0200 Subject: [PATCH 18/26] Add Python integration test --- .../__snapshots__/test.ts.snap | 95 +++++++++++++++++++ test/python/multiple-stacks/cdktf.json | 10 ++ test/python/multiple-stacks/main.py | 27 ++++++ test/python/multiple-stacks/test.ts | 21 ++++ 4 files changed, 153 insertions(+) create mode 100644 test/python/multiple-stacks/__snapshots__/test.ts.snap create mode 100644 test/python/multiple-stacks/cdktf.json create mode 100755 test/python/multiple-stacks/main.py create mode 100644 test/python/multiple-stacks/test.ts diff --git a/test/python/multiple-stacks/__snapshots__/test.ts.snap b/test/python/multiple-stacks/__snapshots__/test.ts.snap new file mode 100644 index 0000000000..82fdd66741 --- /dev/null +++ b/test/python/multiple-stacks/__snapshots__/test.ts.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`python full integration test synth synth generates JSON for both stacks 1`] = ` +"{ + \\"//\\": { + \\"metadata\\": { + \\"version\\": \\"stubbed\\", + \\"stackName\\": \\"python-simple-one\\" + } + }, + \\"terraform\\": { + \\"required_providers\\": { + \\"aws\\": { + \\"version\\": \\"~> 2.0\\", + \\"source\\": \\"aws\\" + } + }, + \\"backend\\": { + \\"remote\\": { + \\"organization\\": \\"test\\", + \\"workspaces\\": { + \\"name\\": \\"test\\" + } + } + } + }, + \\"provider\\": { + \\"aws\\": [ + { + \\"region\\": \\"eu-central-1\\" + } + ] + }, + \\"resource\\": { + \\"aws_sns_topic\\": { + \\"Topic\\": { + \\"display_name\\": \\"my-first-sns-topic\\", + \\"//\\": { + \\"metadata\\": { + \\"path\\": \\"python-simple-one/Topic\\", + \\"uniqueId\\": \\"Topic\\" + } + } + } + } + } +}" +`; + +exports[`python full integration test synth synth generates JSON for both stacks 2`] = ` +"{ + \\"//\\": { + \\"metadata\\": { + \\"version\\": \\"stubbed\\", + \\"stackName\\": \\"python-simple-two\\" + } + }, + \\"terraform\\": { + \\"required_providers\\": { + \\"aws\\": { + \\"version\\": \\"~> 2.0\\", + \\"source\\": \\"aws\\" + } + }, + \\"backend\\": { + \\"remote\\": { + \\"organization\\": \\"test\\", + \\"workspaces\\": { + \\"name\\": \\"test\\" + } + } + } + }, + \\"provider\\": { + \\"aws\\": [ + { + \\"region\\": \\"eu-central-1\\" + } + ] + }, + \\"resource\\": { + \\"aws_sns_topic\\": { + \\"Topic\\": { + \\"display_name\\": \\"my-first-sns-topic\\", + \\"//\\": { + \\"metadata\\": { + \\"path\\": \\"python-simple-two/Topic\\", + \\"uniqueId\\": \\"Topic\\" + } + } + } + } + } +}" +`; diff --git a/test/python/multiple-stacks/cdktf.json b/test/python/multiple-stacks/cdktf.json new file mode 100644 index 0000000000..f2275a2944 --- /dev/null +++ b/test/python/multiple-stacks/cdktf.json @@ -0,0 +1,10 @@ +{ + "language": "python", + "app": "pipenv run python main.py", + "terraformProviders": ["aws@~> 2.0"], + "terraformModules": ["terraform-aws-modules/vpc/aws@2.77.0"], + "codeMakerOutput": "imports", + "context": { + "excludeStackIdFromLogicalIds": "true" + } +} \ No newline at end of file diff --git a/test/python/multiple-stacks/main.py b/test/python/multiple-stacks/main.py new file mode 100755 index 0000000000..76772bbb96 --- /dev/null +++ b/test/python/multiple-stacks/main.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +from constructs import Construct +from cdktf import App, TerraformStack, Testing +from imports.aws import SnsTopic, AwsProvider + +class MyStack(TerraformStack): + def __init__(self, scope: Construct, ns: str): + super().__init__(scope, ns) + + AwsProvider(self, 'Aws', region='eu-central-1') + topic = SnsTopic(self, 'Topic', display_name='overwritten') + topic.add_override('display_name', 'my-first-sns-topic') + + self.add_override('terraform.backend', { + 'remote': { + 'organization': 'test', + 'workspaces': { + 'name': 'test' + } + } + }) + +app = Testing.stub_version(App(stack_traces=False)) +MyStack(app, "python-simple-one") +MyStack(app, "python-simple-two") + +app.synth() \ No newline at end of file diff --git a/test/python/multiple-stacks/test.ts b/test/python/multiple-stacks/test.ts new file mode 100644 index 0000000000..3935974ec6 --- /dev/null +++ b/test/python/multiple-stacks/test.ts @@ -0,0 +1,21 @@ +/** + * + * @group python + */ + +import { TestDriver } from "../../test-helper"; + +describe("python full integration test synth", () => { + let driver: TestDriver; + + beforeAll(() => { + driver = new TestDriver(__dirname) + driver.setupPythonProject() + }); + + test("synth generates JSON for both stacks", async () => { + driver.synth() + expect(driver.synthesizedStack('python-simple-one')).toMatchSnapshot() + expect(driver.synthesizedStack('python-simple-two')).toMatchSnapshot() + }) +}) \ No newline at end of file From 5447f63f844eff894a3a80d84120e13c93df4d46 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 14:59:22 +0200 Subject: [PATCH 19/26] Typo --- packages/cdktf-cli/bin/cmds/deploy.ts | 2 +- packages/cdktf-cli/bin/cmds/destroy.ts | 2 +- packages/cdktf-cli/bin/cmds/diff.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cdktf-cli/bin/cmds/deploy.ts b/packages/cdktf-cli/bin/cmds/deploy.ts index 2f69c4cde5..af7dce6933 100644 --- a/packages/cdktf-cli/bin/cmds/deploy.ts +++ b/packages/cdktf-cli/bin/cmds/deploy.ts @@ -12,7 +12,7 @@ class Command implements yargs.CommandModule { public readonly describe = 'Deploy the given stack'; public readonly builder = (args: yargs.Argv) => args - .positional('stack', { desc: 'Deploy stack which matches the given id only. Required when more than stack is present in the app', type: 'string' }) + .positional('stack', { desc: 'Deploy stack which matches the given id only. Required when more than one stack is present in the app', type: 'string' }) .option('app', { default: config.app, required: true, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, required: true, desc: 'Output directory', alias: 'o' }) .option('auto-approve', { type: 'boolean', default: false, required: false, desc: 'Auto approve' }) diff --git a/packages/cdktf-cli/bin/cmds/destroy.ts b/packages/cdktf-cli/bin/cmds/destroy.ts index 11aa8f36d4..54051a5001 100644 --- a/packages/cdktf-cli/bin/cmds/destroy.ts +++ b/packages/cdktf-cli/bin/cmds/destroy.ts @@ -12,7 +12,7 @@ class Command implements yargs.CommandModule { public readonly describe = 'Destroy the given stack'; public readonly builder = (args: yargs.Argv) => args - .positional('stack', { desc: 'Destroy stack which matches the given id only. Required when more than stack is present in the app', type: 'string' }) + .positional('stack', { desc: 'Destroy stack which matches the given id only. Required when more than one stack is present in the app', type: 'string' }) .option('app', { default: config.app, required: true, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, required: true, desc: 'Output directory', alias: 'o' }) .option('auto-approve', { type: 'boolean', default: false, required: false, desc: 'Auto approve' }) diff --git a/packages/cdktf-cli/bin/cmds/diff.ts b/packages/cdktf-cli/bin/cmds/diff.ts index 420b41d596..e361efba08 100644 --- a/packages/cdktf-cli/bin/cmds/diff.ts +++ b/packages/cdktf-cli/bin/cmds/diff.ts @@ -12,7 +12,7 @@ class Command implements yargs.CommandModule { public readonly describe = 'Perform a diff (terraform plan) for the given stack'; public readonly builder = (args: yargs.Argv) => args - .positional('stack', { desc: 'Diff stack which matches the given id only. Required when more than stack is present in the app', type: 'string' }) + .positional('stack', { desc: 'Diff stack which matches the given id only. Required when more than one stack is present in the app', type: 'string' }) .option('app', { default: config.app, required: true, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, required: true, desc: 'Output directory', alias: 'o' }) .showHelpOnFail(true) From daefa91334243c301891c8c64141ba6af11d0134 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 14:59:35 +0200 Subject: [PATCH 20/26] A more uplifting error message --- packages/cdktf-cli/bin/cmds/terraform-check.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cdktf-cli/bin/cmds/terraform-check.ts b/packages/cdktf-cli/bin/cmds/terraform-check.ts index c853dcc581..f7aa3c2ed8 100644 --- a/packages/cdktf-cli/bin/cmds/terraform-check.ts +++ b/packages/cdktf-cli/bin/cmds/terraform-check.ts @@ -10,7 +10,10 @@ const VERSION_REGEXP = /Terraform v\d+.\d+.\d+/ export const terraformCheck = async (): Promise => { try { if (existsSync(path.join(process.cwd(), 'terraform.tfstate'))) { - throw new Error(`Found 'terraform.tfstate' Terraform state file. Please rename it to match the stack name. Learn more https://cdk.tf/multiple-stacks`) + throw new Error(` + CDK for Terraform now supports multiple stacks! + Found 'terraform.tfstate' Terraform state file. Please rename it to match the stack name. Learn more https://cdk.tf/multiple-stacks + `) } const fakeStack: SynthesizedStack = { From 03685e36a09db8142d13d74340b008a64907527e Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 14:59:51 +0200 Subject: [PATCH 21/26] Provide context for ugly workaround --- packages/cdktf-cli/bin/cmds/terraform-check.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cdktf-cli/bin/cmds/terraform-check.ts b/packages/cdktf-cli/bin/cmds/terraform-check.ts index f7aa3c2ed8..e3d5b1a8a2 100644 --- a/packages/cdktf-cli/bin/cmds/terraform-check.ts +++ b/packages/cdktf-cli/bin/cmds/terraform-check.ts @@ -16,6 +16,9 @@ export const terraformCheck = async (): Promise => { `) } + // We're abusing the TerraformCli class here, + // hence we need to construct this object. + // Only the `workingDirectory` is releveant here. const fakeStack: SynthesizedStack = { name: '', workingDirectory: './', From a4ce96a3006b8242c058b75e1cdbbad35c94f288 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 15:00:10 +0200 Subject: [PATCH 22/26] Adjust help texts for cli templates --- packages/cdktf-cli/templates/csharp/help | 8 +++---- packages/cdktf-cli/templates/java/help | 8 +++---- packages/cdktf-cli/templates/python-pip/help | 8 +++---- packages/cdktf-cli/templates/python/help | 8 +++---- packages/cdktf-cli/templates/typescript/help | 22 ++++++++++---------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/cdktf-cli/templates/csharp/help b/packages/cdktf-cli/templates/csharp/help index 1cdd2dc3f5..388e184e15 100644 --- a/packages/cdktf-cli/templates/csharp/help +++ b/packages/cdktf-cli/templates/csharp/help @@ -8,16 +8,16 @@ dotnet build Builds your dotnet packages Synthesize: - cdktf synth Synthesize Terraform resources to cdktf.out/ + cdktf synth [stack] Synthesize Terraform resources to cdktf.out/ Diff: - cdktf diff Perform a diff (terraform plan) for the given stack + cdktf diff [stack] Perform a diff (terraform plan) for the given stack Deploy: - cdktf deploy Deploy the given stack + cdktf deploy [stack] Deploy the given stack Destroy: - cdktf destroy Destroy the given stack + cdktf destroy [stack] Destroy the given stack Learn more about using modules and providers https://cdk.tf/modules-and-providers diff --git a/packages/cdktf-cli/templates/java/help b/packages/cdktf-cli/templates/java/help index 01168bb690..c84567834c 100644 --- a/packages/cdktf-cli/templates/java/help +++ b/packages/cdktf-cli/templates/java/help @@ -8,16 +8,16 @@ mvn compile Compiles your Java packages Synthesize: - cdktf synth Synthesize Terraform resources to cdktf.out/ + cdktf synth [stack] Synthesize Terraform resources to cdktf.out/ Diff: - cdktf diff Perform a diff (terraform plan) for the given stack + cdktf diff [stack] Perform a diff (terraform plan) for the given stack Deploy: - cdktf deploy Deploy the given stack + cdktf deploy [stack] Deploy the given stack Destroy: - cdktf destroy Destroy the given stack + cdktf destroy [stack] Destroy the given stack Learn more about using modules and providers https://cdk.tf/modules-and-providers diff --git a/packages/cdktf-cli/templates/python-pip/help b/packages/cdktf-cli/templates/python-pip/help index 22bb1e7013..2a10f1e956 100644 --- a/packages/cdktf-cli/templates/python-pip/help +++ b/packages/cdktf-cli/templates/python-pip/help @@ -8,16 +8,16 @@ python3 ./main.py Compile and run the python code. Synthesize: - cdktf synth Synthesize Terraform resources to cdktf.out/ + cdktf synth [stack] Synthesize Terraform resources to cdktf.out/ Diff: - cdktf diff Perform a diff (terraform plan) for the given stack + cdktf diff [stack] Perform a diff (terraform plan) for the given stack Deploy: - cdktf deploy Deploy the given stack + cdktf deploy [stack] Deploy the given stack Destroy: - cdktf destroy Destroy the given stack + cdktf destroy [stack] Destroy the given stack Learn more about using modules and providers https://cdk.tf/modules-and-providers diff --git a/packages/cdktf-cli/templates/python/help b/packages/cdktf-cli/templates/python/help index 0d2390c0ce..824f9b63d7 100644 --- a/packages/cdktf-cli/templates/python/help +++ b/packages/cdktf-cli/templates/python/help @@ -8,16 +8,16 @@ pipenv run ./main.py Compile and run the python code. Synthesize: - cdktf synth Synthesize Terraform resources to cdktf.out/ + cdktf synth [stack] Synthesize Terraform resources to cdktf.out/ Diff: - cdktf diff Perform a diff (terraform plan) for the given stack + cdktf diff [stack] Perform a diff (terraform plan) for the given stack Deploy: - cdktf deploy Deploy the given stack + cdktf deploy [stack] Deploy the given stack Destroy: - cdktf destroy Destroy the given stack + cdktf destroy [stack] Destroy the given stack Learn more about using modules and providers https://cdk.tf/modules-and-providers diff --git a/packages/cdktf-cli/templates/typescript/help b/packages/cdktf-cli/templates/typescript/help index 877c6657ae..3b2a4ec94a 100644 --- a/packages/cdktf-cli/templates/typescript/help +++ b/packages/cdktf-cli/templates/typescript/help @@ -2,30 +2,30 @@ Your cdktf typescript project is ready! - cat help Print this message + cat help Print this message Compile: - npm run get Import/update Terraform providers and modules (you should check-in this directory) - npm run compile Compile typescript code to javascript (or "npm run watch") - npm run watch Watch for changes and compile typescript in the background - npm run build Compile typescript + npm run get Import/update Terraform providers and modules (you should check-in this directory) + npm run compile Compile typescript code to javascript (or "npm run watch") + npm run watch Watch for changes and compile typescript in the background + npm run build Compile typescript Synthesize: - cdktf synth Synthesize Terraform resources from stacks to cdktf.out/ (ready for 'terraform apply') + cdktf synth [stack] Synthesize Terraform resources from stacks to cdktf.out/ (ready for 'terraform apply') Diff: - cdktf diff Perform a diff (terraform plan) for the given stack + cdktf diff [stack] Perform a diff (terraform plan) for the given stack Deploy: - cdktf deploy Deploy the given stack + cdktf deploy [stack] Deploy the given stack Destroy: - cdktf destroy Destroy the stack + cdktf destroy [stack] Destroy the stack Upgrades: - npm run upgrade Upgrade cdktf modules to latest version - npm run upgrade:next Upgrade cdktf modules to latest "@next" version (last commit) + npm run upgrade Upgrade cdktf modules to latest version + npm run upgrade:next Upgrade cdktf modules to latest "@next" version (last commit) Use Prebuilt Providers: From b160bbc8d8d28f7b768a8409115c0a1c0f407e95 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 15:00:45 +0200 Subject: [PATCH 23/26] Simplify test --- test/typescript/multiple-stacks/test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/typescript/multiple-stacks/test.ts b/test/typescript/multiple-stacks/test.ts index 3b7236222b..7934100b75 100644 --- a/test/typescript/multiple-stacks/test.ts +++ b/test/typescript/multiple-stacks/test.ts @@ -46,10 +46,7 @@ describe("full integration test", () => { " `); - - let error; - try { driver.diff() } catch(e) { error = e.message } - expect(error).toMatch('Found more than one stack, please specify a target stack first, second'); + expect(() => driver.diff()).toThrowError('Found more than one stack') }); test("list", () => { From a2af2ecc99eb9214c9827863ba5826c402702162 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 15:01:02 +0200 Subject: [PATCH 24/26] This doesnt work - Will be fixed with https://github.com/hashicorp/terraform-cdk/issues/647 --- test/typescript/terraform-cloud/test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/typescript/terraform-cloud/test.ts b/test/typescript/terraform-cloud/test.ts index 856afa686f..bb5a96e219 100755 --- a/test/typescript/terraform-cloud/test.ts +++ b/test/typescript/terraform-cloud/test.ts @@ -52,9 +52,6 @@ describe("full integration test", () => { } }) - // Trying to prevent random 422 errrors - await delay(3000) - expect(driver.deploy()).toMatchSnapshot() await client.Workspaces.deleteByName(orgName, workspaceName) }) From f44287f2dee3feb40c7c17ebc7adaf07543fbbf6 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 16:40:39 +0200 Subject: [PATCH 25/26] Explicit timeout --- test/python/multiple-stacks/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/multiple-stacks/test.ts b/test/python/multiple-stacks/test.ts index 5af8bacd8d..9ea91c18bc 100644 --- a/test/python/multiple-stacks/test.ts +++ b/test/python/multiple-stacks/test.ts @@ -17,5 +17,5 @@ describe("python full integration test synth", () => { await driver.synth() expect(driver.synthesizedStack('python-simple-one')).toMatchSnapshot() expect(driver.synthesizedStack('python-simple-two')).toMatchSnapshot() - }) + }, 240_000) }) \ No newline at end of file From 3432f695b4bdaf83dfdc75d89cc42f50ac613a89 Mon Sep 17 00:00:00 2001 From: Sebastian Korfmann Date: Wed, 21 Apr 2021 20:43:35 +0200 Subject: [PATCH 26/26] Typo --- packages/cdktf-cli/bin/cmds/deploy.ts | 2 +- packages/cdktf-cli/bin/cmds/destroy.ts | 2 +- packages/cdktf-cli/bin/cmds/diff.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cdktf-cli/bin/cmds/deploy.ts b/packages/cdktf-cli/bin/cmds/deploy.ts index af7dce6933..3699967ed9 100644 --- a/packages/cdktf-cli/bin/cmds/deploy.ts +++ b/packages/cdktf-cli/bin/cmds/deploy.ts @@ -12,7 +12,7 @@ class Command implements yargs.CommandModule { public readonly describe = 'Deploy the given stack'; public readonly builder = (args: yargs.Argv) => args - .positional('stack', { desc: 'Deploy stack which matches the given id only. Required when more than one stack is present in the app', type: 'string' }) + .positional('stack', { desc: 'Deploy stack which matches the given id only. Required when more than one stack is present in the app', type: 'string' }) .option('app', { default: config.app, required: true, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, required: true, desc: 'Output directory', alias: 'o' }) .option('auto-approve', { type: 'boolean', default: false, required: false, desc: 'Auto approve' }) diff --git a/packages/cdktf-cli/bin/cmds/destroy.ts b/packages/cdktf-cli/bin/cmds/destroy.ts index 54051a5001..7c82e3ea2c 100644 --- a/packages/cdktf-cli/bin/cmds/destroy.ts +++ b/packages/cdktf-cli/bin/cmds/destroy.ts @@ -12,7 +12,7 @@ class Command implements yargs.CommandModule { public readonly describe = 'Destroy the given stack'; public readonly builder = (args: yargs.Argv) => args - .positional('stack', { desc: 'Destroy stack which matches the given id only. Required when more than one stack is present in the app', type: 'string' }) + .positional('stack', { desc: 'Destroy stack which matches the given id only. Required when more than one stack is present in the app', type: 'string' }) .option('app', { default: config.app, required: true, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, required: true, desc: 'Output directory', alias: 'o' }) .option('auto-approve', { type: 'boolean', default: false, required: false, desc: 'Auto approve' }) diff --git a/packages/cdktf-cli/bin/cmds/diff.ts b/packages/cdktf-cli/bin/cmds/diff.ts index e361efba08..4f69700baa 100644 --- a/packages/cdktf-cli/bin/cmds/diff.ts +++ b/packages/cdktf-cli/bin/cmds/diff.ts @@ -12,7 +12,7 @@ class Command implements yargs.CommandModule { public readonly describe = 'Perform a diff (terraform plan) for the given stack'; public readonly builder = (args: yargs.Argv) => args - .positional('stack', { desc: 'Diff stack which matches the given id only. Required when more than one stack is present in the app', type: 'string' }) + .positional('stack', { desc: 'Diff stack which matches the given id only. Required when more than one stack is present in the app', type: 'string' }) .option('app', { default: config.app, required: true, desc: 'Command to use in order to execute cdktf app', alias: 'a' }) .option('output', { default: config.output, required: true, desc: 'Output directory', alias: 'o' }) .showHelpOnFail(true)