diff --git a/.gitignore b/.gitignore index 8483ea7bcf..d3e486ccc9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ tsconfig.json **/coverage **/dist **/.terraform +**/*.tfstate +**/*.tfstate.backup .vscode bootstrap.json terraform-cdk.github-issues 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..98175dbea2 --- /dev/null +++ b/docs/working-with-cdk-for-terraform/app-stacks-concept.md @@ -0,0 +1,150 @@ +# 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 +``` + +### 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. + +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. + +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. 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 8157e6ba59..0000000000 --- a/examples/typescript/aws/main.ts +++ /dev/null @@ -1,51 +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'); -app.synth(); \ No newline at end of file diff --git a/packages/cdktf-cli/bin/cmds/deploy.ts b/packages/cdktf-cli/bin/cmds/deploy.ts index 18c6b9c084..3699967ed9 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 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' }) @@ -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..7c82e3ea2c 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 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' }) @@ -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..4f69700baa 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 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) @@ -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 7fe5ff391f..7a0af8ba7b 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,16 +14,28 @@ 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): 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, @@ -47,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); } @@ -57,28 +69,28 @@ Command output on stderr: await this.synthTelemetry(command, (endTime - startTime)); 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 manifest = JSON.parse(fs.readFileSync(path.join(outdir, Manifest.fileName)).toString()) as ManifestJson + + 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()); + 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.'); } - 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.'); + const stackNames = stacks.map(s => s.name) + const orphanedDirectories = existingDirectories.filter(e => !stackNames.includes(path.basename(e))) + + for (const orphanedDirectory of orphanedDirectories) { + fs.rmdirSync(orphanedDirectory, { recursive: true }) } return stacks 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/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/terraform-check.ts b/packages/cdktf-cli/bin/cmds/terraform-check.ts index 37963a83d4..e3d5b1a8a2 100644 --- a/packages/cdktf-cli/bin/cmds/terraform-check.ts +++ b/packages/cdktf-cli/bin/cmds/terraform-check.ts @@ -1,12 +1,33 @@ 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(` + 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 + `) + } + + // 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: './', + 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/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} + + + ))} + + + )} + + ); +}; 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 6f795f6e93..e977016e98 100644 --- a/packages/cdktf-cli/bin/cmds/ui/models/terraform-cloud.ts +++ b/packages/cdktf-cli/bin/cmds/ui/models/terraform-cloud.ts @@ -7,8 +7,8 @@ import { TerraformJsonConfigBackendRemote } from '../terraform-json' import * as TerraformCloudClient from '@skorfmann/terraform-cloud' import archiver from 'archiver'; import { WritableStreamBuffer } from 'stream-buffers'; +import { SynthesizedStack } from '../../helper/synth-stack'; import { logger } from '../../../../lib/logging'; - export class TerraformCloudPlan implements TerraformPlan { constructor(public readonly planFile: string, public readonly plan: { [key: string]: any }, public readonly url: string) { } @@ -97,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: 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 this.hostname = config.hostname || 'app.terraform.io' this.workspaceName = config.workspaces.name @@ -142,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 702aa5bf17..f42a56cadd 100644 --- a/packages/cdktf-cli/bin/cmds/ui/synth.tsx +++ b/packages/cdktf-cli/bin/cmds/ui/synth.tsx @@ -5,30 +5,38 @@ 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, stacks } = useTerraformState() + return( <> - { jsonOutput ? ({stackJSON}) : (Generated Terraform code in the output directory: {targetDir}) } + { 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}) - const { status, stackName, errors } = synth() +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 - const statusText = (stackName === '') ? `${status}...` : {status} {stackName}... + const statusText = (currentStack.name === '') ? `${status}...` : {status} {currentStack.name}... if (errors) return({ errors }); @@ -46,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 a97cb058ba..4c581b3037 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,6 +203,7 @@ export const TerraformProvider: React.FunctionComponent } interface UseTerraformInput { targetDir: string; + targetStack?: string; synthCommand: string; isSpeculative?: boolean; autoApprove?: boolean; @@ -208,7 +219,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() @@ -219,7 +230,6 @@ export const useTerraform = ({ targetDir, synthCommand, isSpeculative = false, a throw new Error('useTerraform must be used within a TerraformContextDispatch.Provider') } - const confirmationCallback = React.useCallback(submitValue => { if (submitValue === false) { exit() @@ -230,21 +240,18 @@ export const useTerraform = ({ targetDir, synthCommand, isSpeculative = false, a }, []); - 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)) } } @@ -252,10 +259,25 @@ export const useTerraform = ({ targetDir, synthCommand, isSpeculative = false, a try { dispatch({ type: 'SYNTH' }) 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) + } else { // synth + const stack = targetStack ? stacks.find(s => s.name === targetStack) : stacks[0] + if (stack) { + dispatch({ type: 'CURRENT_STACK', currentStack: 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/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: 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 8ca17adb51..8a3665a967 100644 --- a/packages/cdktf/lib/app.ts +++ b/packages/cdktf/lib/app.ts @@ -1,9 +1,9 @@ import { Construct, Node, ConstructMetadata } from 'constructs'; import * as fs from 'fs'; import { version } from '../package.json'; +import { Manifest } from './manifest' export const CONTEXT_ENV = 'CDKTF_CONTEXT_JSON'; - export interface AppOptions { /** * The directory to output Terraform resources. @@ -34,6 +34,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 +46,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); @@ -56,13 +62,20 @@ export class App extends Construct { * 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); + } + + const manifest = new Manifest(version, this.outdir) + + Node.of(this).synthesize({ + outdir: this.outdir, + sessionContext: { + manifest } + }); - Node.of(this).synthesize({ - outdir: this.outdir - }); + manifest.writeToFile(); } private loadContext(defaults: { [key: string]: string } = { }) { @@ -78,7 +91,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/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..429ef4eb75 --- /dev/null +++ b/packages/cdktf/lib/manifest.ts @@ -0,0 +1,51 @@ +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) { + const stacksPath = path.join(this.outdir, Manifest.stacksFolder) + if (!fs.existsSync(stacksPath)) fs.mkdirSync(stacksPath); + } + + 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 edc12c6fac..272a889eeb 100644 --- a/packages/cdktf/lib/terraform-stack.ts +++ b/packages/cdktf/lib/terraform-stack.ts @@ -7,6 +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 './manifest' 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 }); @@ -83,7 +82,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 +95,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 +141,13 @@ 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 manifest = session.manifest as Manifest + const stackManifest = manifest.forStack(this) + + const workingDirectory = path.join(session.outdir, stackManifest.workingDirectory) + if (!fs.existsSync(workingDirectory)) fs.mkdirSync(workingDirectory); + + 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 0606d25004..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({ @@ -26,4 +27,10 @@ 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('ckdtfVersion is accessible in context', () => { + const prog = new App(); + const node = Node.of(prog); + expect(node.tryGetContext('cdktfVersion')).toEqual(version); }); \ No newline at end of file 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\\" + } + } + }" + `); +}); diff --git a/test/csharp/synth-app/test.ts b/test/csharp/synth-app/test.ts index 81b166a217..9da262c163 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 () => { await driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('csharp-simple')).toMatchSnapshot() }, 180_000); }) \ No newline at end of file diff --git a/test/java/synth-app/test.ts b/test/java/synth-app/test.ts index 81a4fb70d2..f6287e46f9 100644 --- a/test/java/synth-app/test.ts +++ b/test/java/synth-app/test.ts @@ -16,6 +16,6 @@ describe("java full integration", () => { test("synth generates JSON", async () => { await driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('java-simple')).toMatchSnapshot() }) }) \ No newline at end of file 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..9ea91c18bc --- /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(async () => { + driver = new TestDriver(__dirname) + await driver.setupPythonProject() + }); + + test("synth generates JSON for both stacks", async () => { + 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 diff --git a/test/python/synth-app/test.ts b/test/python/synth-app/test.ts index 5d726af581..eed6a97388 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 () => { await driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('python-simple')).toMatchSnapshot() }, 240_000); }) \ 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 32861bbabb..d5dfeacf76 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 () => { await driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('python-third-party-provider')).toMatchSnapshot() }, 30_000) }) \ No newline at end of file diff --git a/test/test-helper.ts b/test/test-helper.ts index 1c1d5d961d..37226a4a40 100644 --- a/test/test-helper.ts +++ b/test/test-helper.ts @@ -14,9 +14,9 @@ export class TestDriver { this.env = Object.assign({ CI: 1 }, process.env, addToEnv); } - private async exec(command: string, args: string[] = []) { + private async exec(command: string, args: string[] = []): Promise<{stdout: string, stderr: string}> { try { - await new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const stdout = [], stderr = []; const process = spawn(command, args, { shell: true, stdio: 'pipe', env: this.env },); process.stdout.on('data', (data) => { @@ -63,8 +63,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 = async (template: string) => { @@ -77,20 +77,24 @@ export class TestDriver { await this.exec(`cdktf get`); } - synth = async () => { - await this.exec(`cdktf synth`); + synth = async (flags?: string) => { + return await this.exec(`cdktf synth ${flags ? flags : ''}`); } - diff = () => { - return execSync(`cdktf diff`, { env: this.env }).toString(); + list = (flags?: string) => { + return execSync(`cdktf list ${flags ? flags : ''}`, { env: this.env}).toString(); } - deploy = () => { - return execSync(`cdktf deploy --auto-approve`, { env: this.env }).toString(); + diff = (stackName?: string) => { + return execSync(`cdktf diff ${stackName ? stackName : ''}`, { env: this.env }).toString(); } - destroy = () => { - return execSync(`cdktf destroy --auto-approve`, { env: this.env }).toString(); + deploy = (stackName?: string) => { + return execSync(`cdktf deploy ${stackName ? stackName : ''} --auto-approve`, { env: this.env }).toString(); + } + + destroy = (stackName?: string) => { + return execSync(`cdktf destroy ${stackName ? stackName : ''} --auto-approve`, { env: this.env }).toString(); } setupTypescriptProject = async () => { diff --git a/test/typescript/feature-flags/test.ts b/test/typescript/feature-flags/test.ts index 6784ece267..81dc87ec6e 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", async () => { writeConfig(driver.workingDirectory, jsonWithContext({ excludeStackIdFromLogicalIds: "true" })) await driver.synth() - expect(loadStackJson(driver.workingDirectory)).toMatchSnapshot(); + expect(loadStackJson(driver.workingDirectory, 'hello-deploy')).toMatchSnapshot(); }, 60_000); test("with allowSepCharsInLogicalIds feature", async () => { writeConfig(driver.workingDirectory, jsonWithContext({ allowSepCharsInLogicalIds: "true" })) await driver.synth() - expect(loadStackJson(driver.workingDirectory)).toMatchSnapshot(); + expect(loadStackJson(driver.workingDirectory, 'hello-deploy')).toMatchSnapshot(); }, 60_000); test("without features", async () => { writeConfig(driver.workingDirectory, cdktfJSON) await driver.synth() - expect(loadStackJson(driver.workingDirectory)).toMatchSnapshot(); + expect(loadStackJson(driver.workingDirectory, 'hello-deploy')).toMatchSnapshot(); }, 60_000); 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 c915891729..7ba1fd10d4 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", async () => { await driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('hello-modules')).toMatchSnapshot() }, 120_000) onWindows("build modules windows", async () => { await driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('hello-modules')).toMatchSnapshot() }, 120_000) }) \ No newline at end of file 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..3690a1955b --- /dev/null +++ b/test/typescript/multiple-stacks/test.ts @@ -0,0 +1,110 @@ +// +// Testing a full cycle of diff, deploy and destroy +// +// @group typescript +// +import { TestDriver } from "../../test-helper"; + +describe("full integration test", () => { + let driver: TestDriver; + + beforeAll(async () => { + driver = new TestDriver(__dirname); + await driver.setupTypescriptProject(); + }); + + test("synth", async () => { + await driver.synth(); + expect(driver.synthesizedStack("first")).toMatchSnapshot(); + expect(driver.synthesizedStack("second")).toMatchSnapshot(); + }); + + test("synth with json output", async () => { + expect((await driver.synth("--json")).stdout).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. + " + `); + + expect(() => driver.diff()).toThrowError("Found more than one stack"); + }); + + 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 + 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. + " + `); + + expect(() => driver.deploy()).toThrowError( + "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. + " + `); + + expect(() => driver.destroy()).toThrowError( + "Found more than one stack, please specify a target stack first, second" + ); + }); +}); diff --git a/test/typescript/synth-app/test.ts b/test/typescript/synth-app/test.ts index d027750f61..d18dda0ebc 100644 --- a/test/typescript/synth-app/test.ts +++ b/test/typescript/synth-app/test.ts @@ -17,6 +17,6 @@ describe("full integration test synth", () => { test("synth generates JSON", async () => { await driver.synth() - expect(driver.synthesizedStack()).toMatchSnapshot() + expect(driver.synthesizedStack('hello-terra')).toMatchSnapshot() }) }) \ No newline at end of file diff --git a/test/typescript/terraform-cloud/test.ts b/test/typescript/terraform-cloud/test.ts index 10e223ca80..f75c658983 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;