From 6f2b8c25c820b2369d822c2544cc0082bc992da3 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Wed, 30 Mar 2022 13:42:39 +0200 Subject: [PATCH] wip --- .../cdktf-cli/lib/models/terraform-cloud.ts | 10 +- test/package.json | 7 +- test/test-helper.ts | 64 +++++--- .../diff-deploy-output-destroy/test.ts | 10 +- test/typescript/multiple-stacks/test.ts | 10 +- test/typescript/providers/test.ts | 2 +- test/typescript/terraform-cloud/cdktf.json | 2 +- test/typescript/terraform-cloud/main.ts | 25 ++- test/typescript/terraform-cloud/test.ts | 148 ++++++++++++------ test/yarn.lock | 34 +--- 10 files changed, 179 insertions(+), 133 deletions(-) diff --git a/packages/cdktf-cli/lib/models/terraform-cloud.ts b/packages/cdktf-cli/lib/models/terraform-cloud.ts index 921d3eb81b..28d03e9887 100644 --- a/packages/cdktf-cli/lib/models/terraform-cloud.ts +++ b/packages/cdktf-cli/lib/models/terraform-cloud.ts @@ -155,10 +155,14 @@ export class TerraformCloud implements Terraform { this.removeRun("cancel"); }); - const httpsProxy = process.env.https_proxy || process.env.HTTPS_PROXY; - if (httpsProxy) { + const httpProxy = process.env.http_proxy || process.env.HTTP_PROXY; + if (httpProxy) { + const url = new URL(httpProxy); + logger.debug( + `setting tunnel agent via hostname=${url.hostname} port=${url.port}` + ); this.client.client.defaults.httpsAgent = agent.httpsOverHttp({ - proxy: new URL(httpsProxy), + proxy: { host: url.hostname, port: url.port }, }); } } diff --git a/test/package.json b/test/package.json index 73a12bf458..2b389e74e3 100644 --- a/test/package.json +++ b/test/package.json @@ -12,14 +12,12 @@ "license": "MPL-2.0", "devDependencies": { "@skorfmann/terraform-cloud": "^1.9.1", + "@types/express": "^4.17.13", "@types/fs-extra": "^8.1.0", "@types/jest": "^27.0.1", - "@types/express": "^4.17.13", - "@types/express-http-proxy": "^1.6.3", "archiver": "^5.3.0", "execa": "^5.1.1", "express": "^4.17.2", - "express-http-proxy": "^1.6.3", "fs-extra": "^8.1.0", "jest": "^27.2.1", "jsii-rosetta": "^1.53.0", @@ -27,5 +25,6 @@ "strip-ansi": "^6.0.0", "ts-jest": "^27.0.7", "typescript": "^3.9.7" - } + }, + "dependencies": {} } diff --git a/test/test-helper.ts b/test/test-helper.ts index 8416afc369..974fb0d2a6 100644 --- a/test/test-helper.ts +++ b/test/test-helper.ts @@ -1,14 +1,24 @@ import { TemplateServer } from "./template-server"; -import { spawn } from "child_process"; +import { spawn, execSync } from "child_process"; +import * as execa from "execa"; import { spawn as ptySpawn } from "node-pty"; -const { execSync } = require("child_process"); const os = require("os"); const path = require("path"); const fs = require("fs"); const fse = require("fs-extra"); const stripAnsi = require("strip-ansi"); +function execSyncLogErrors(...args: Parameters) { + try { + return execSync(...args); + } catch (e) { + console.log(e.stdout?.toString()); + console.error(e.stderr?.toString()); + throw e; + } +} + export class QueryableStack { private readonly stack: Record; constructor(stackInput: string) { @@ -169,7 +179,7 @@ export class TestDriver { list = (flags?: string) => { return stripAnsi( - execSync(`cdktf list ${flags ? flags : ""}`, { + execSyncLogErrors(`cdktf list ${flags ? flags : ""}`, { env: this.env, }).toString() ); @@ -177,30 +187,39 @@ export class TestDriver { diff = (stackName?: string) => { return stripAnsi( - execSync(`cdktf diff ${stackName ? stackName : ""}`, { + execSyncLogErrors(`cdktf diff ${stackName ? stackName : ""}`, { env: this.env, }).toString() ); }; - deploy = (stackNames?: string[], outputsFilePath?: string) => { - return stripAnsi( - execSync( - `cdktf deploy ${ - stackNames ? stackNames.join(" ") : "" - } --auto-approve ${ - outputsFilePath ? `--outputs-file=${outputsFilePath}` : "" - }`, - { env: this.env } - ).toString() + deploy = async (stackNames?: string[], outputsFilePath?: string) => { + const result = await execa( + "cdktf", + [ + "deploy", + ...stackNames, + "--auto-approve", + ...(outputsFilePath ? [`--outputs-file=${outputsFilePath}`] : []), + ], + { env: { ...process.env, ...this.env } } // make sure env is up to date ); + return stripAnsi(result.stdout); }; - output = (stackName?: string, outputsFilePath?: string) => { + output = ( + stackName?: string, + outputsFilePath?: string, + includeSensitiveOutputs?: boolean + ) => { return stripAnsi( - execSync( + execSyncLogErrors( `cdktf output ${stackName ? stackName : ""} ${ outputsFilePath ? `--outputs-file=${outputsFilePath}` : "" + } ${ + includeSensitiveOutputs + ? `--outputs-file-include-sensitive-outputs=true` + : "" }`, { env: this.env } ).toString() @@ -209,7 +228,7 @@ export class TestDriver { destroy = (stackNames?: string[]) => { return stripAnsi( - execSync( + execSyncLogErrors( `cdktf destroy ${ stackNames ? stackNames.join(" ") : "" } --auto-approve`, @@ -250,10 +269,13 @@ export class TestDriver { await this.init("csharp"); this.copyFiles("Main.cs", "cdktf.json"); await this.get(); - execSync("dotnet add reference .gen/Providers.Null/Providers.Null.csproj", { - stdio: "inherit", - env: this.env, - }); + execSyncLogErrors( + "dotnet add reference .gen/Providers.Null/Providers.Null.csproj", + { + stdio: "inherit", + env: this.env, + } + ); }; setupJavaProject = async () => { diff --git a/test/typescript/diff-deploy-output-destroy/test.ts b/test/typescript/diff-deploy-output-destroy/test.ts index 47f7b84cab..65686467ca 100644 --- a/test/typescript/diff-deploy-output-destroy/test.ts +++ b/test/typescript/diff-deploy-output-destroy/test.ts @@ -14,8 +14,8 @@ describe("full integration test", () => { expect(driver.diff()).toContain(`1 to add, 0 to change, 0 to destroy.`); }); - test("deploy", () => { - const output = driver.deploy(); + test("deploy", async () => { + const output = await driver.deploy(); expect(output).toContain(`null_resource.test (test) will be created`); expect(output).not.toContain(`"world"`); expect(output).toContain(`output = "hello"`); @@ -29,7 +29,7 @@ describe("full integration test", () => { expect(output).toContain(`output2 = `); }); - it("deploy and output write the same outputs file", () => { + it("deploy and output write the same outputs file", async () => { const deployOutputsPath = path.resolve( driver.workingDirectory, "deploy.outputs.json" @@ -39,9 +39,9 @@ describe("full integration test", () => { "output.outputs.json" ); - driver.deploy(undefined, deployOutputsPath); + await driver.deploy(undefined, deployOutputsPath); const deployOutput = JSON.parse(fs.readFileSync(deployOutputsPath, "utf8")); - driver.output(undefined, outputOutputsPath); + await driver.output(undefined, outputOutputsPath); const outputOutput = JSON.parse(fs.readFileSync(outputOutputsPath, "utf8")); expect(deployOutput).toMatchInlineSnapshot(` diff --git a/test/typescript/multiple-stacks/test.ts b/test/typescript/multiple-stacks/test.ts index ab267b5473..ec03608063 100644 --- a/test/typescript/multiple-stacks/test.ts +++ b/test/typescript/multiple-stacks/test.ts @@ -64,11 +64,13 @@ describe("multiple stacks", () => { expect(stderr).toEqual(""); }); - test("deploy", () => { - expect(driver.deploy(["first"])).toContain(`Apply complete!`); - expect(driver.deploy(["first", "second"])).toContain(`Apply complete!`); + test("deploy", async () => { + expect(await driver.deploy(["first"])).toContain(`Apply complete!`); + expect(await driver.deploy(["first", "second"])).toContain( + `Apply complete!` + ); - expect(() => driver.deploy()).toThrowError( + expect(driver.deploy()).toThrowError( "Found more than one stack, please specify a target stack. Run cdktf deploy with one of these stacks: first, second" ); }); diff --git a/test/typescript/providers/test.ts b/test/typescript/providers/test.ts index af5ce6e588..b008ed3b44 100755 --- a/test/typescript/providers/test.ts +++ b/test/typescript/providers/test.ts @@ -7,7 +7,7 @@ describe("full integration test", () => { // This is a workaround for that, by having a new driver every run we don't have this bug. const deployDriver = new TestDriver(__dirname); await deployDriver.setupTypescriptProject(); - const deployLog = deployDriver.deploy(["using-all-providers"]); + const deployLog = await deployDriver.deploy(["using-all-providers"]); expect(deployLog).toContain("null_resource.test (test) will be created"); expect(deployLog).toContain("1 to add"); expect(deployLog).toContain("Apply complete!"); diff --git a/test/typescript/terraform-cloud/cdktf.json b/test/typescript/terraform-cloud/cdktf.json index 5f302f8365..0fc3a028be 100644 --- a/test/typescript/terraform-cloud/cdktf.json +++ b/test/typescript/terraform-cloud/cdktf.json @@ -1,6 +1,6 @@ { "language": "typescript", - "app": "npm run --silent compile && node main.js", + "app": "npx ts-node main.ts", "terraformProviders": [ "random@= 3.1.0", "local@= 2.1.0", diff --git a/test/typescript/terraform-cloud/main.ts b/test/typescript/terraform-cloud/main.ts index b82d38ed86..48ac6c4aa2 100644 --- a/test/typescript/terraform-cloud/main.ts +++ b/test/typescript/terraform-cloud/main.ts @@ -5,6 +5,8 @@ import { Testing, TerraformAsset, TerraformOutput, + RemoteBackend, + NamedRemoteWorkspace, } from "cdktf"; import * as NullProvider from "./.gen/providers/null"; import * as local from "./.gen/providers/local"; @@ -29,11 +31,6 @@ export class SourceStack extends TerraformStack { length: 32, }); - new local.File(this, "file", { - filename: "../../../origin-file.txt", - content: this.password.result, - }); - const nullResouce = new NullProvider.Resource(this, "test", {}); nullResouce.addOverride("provisioner", [ @@ -45,14 +42,10 @@ export class SourceStack extends TerraformStack { ]); if (!localExecution) { - this.addOverride("terraform.backend", { - remote: { - organization, - workspaces: { - name, - }, - token, - }, + new RemoteBackend(this, { + organization: organization!, + workspaces: new NamedRemoteWorkspace(name!), + token, }); } @@ -60,6 +53,12 @@ export class SourceStack extends TerraformStack { value: "constant value", }); + new TerraformOutput(this, "password_output", { + value: this.password.result, + staticId: true, + sensitive: true, + }); + const asset = new TerraformAsset(this, "asset-a", { path: path.resolve(__dirname, "fixtures/a.txt"), }); diff --git a/test/typescript/terraform-cloud/test.ts b/test/typescript/terraform-cloud/test.ts index d5e3eee34a..6402f25682 100755 --- a/test/typescript/terraform-cloud/test.ts +++ b/test/typescript/terraform-cloud/test.ts @@ -1,8 +1,9 @@ import { TestDriver, onPosix } from "../../test-helper"; import { TerraformCloud } from "@skorfmann/terraform-cloud"; import * as crypto from "crypto"; -import express from "express"; -import proxy from "express-http-proxy"; +import * as http from "http"; +import * as net from "net"; +import { readFileSync } from "fs-extra"; const { TERRAFORM_CLOUD_TOKEN, GITHUB_RUN_NUMBER, TERRAFORM_VERSION } = process.env; @@ -12,29 +13,62 @@ if (!TERRAFORM_CLOUD_TOKEN) { console.log("TERRAFORM_CLOUD_TOKEN is undefined, skipping authed tests"); } -function startHttpProxy( - target: string, - mock: jest.Mock -): Promise<{ +process.on("uncaughtException", (err) => { + console.dir(err); + console.log("UNHANDLED"); + process.exit(1); +}); + +process.on("unhandledRejection", (err) => { + console.dir(err); + console.log("UNHANDLED"); + throw err; +}); + +function startHttpProxy(mock: jest.Mock): Promise<{ address: string; close: () => void; }> { return new Promise((resolve) => { - const app = express(); - app.use( - "/", - proxy(target, { - filter: (req) => { - // We use this as it's called for every request - mock(req); - return true; - }, - }) - ); - const listener = app.listen(() => { + // Inspired by: https://nodejs.org/api/http.html#http_event_connect + const proxyServer = http.createServer((req, res) => { + res.writeHead(200, { + "Content-Type": "text/plain", + }); + res.end("OK"); + }); + + proxyServer.on("connect", (req, clientSocket, head) => { + mock(req); + const { port, hostname } = new URL(`http://${req.url}`); + var serverSocket = net.connect(Number(port), hostname, () => { + clientSocket.write( + "HTTP/1.1 200 Connection Established\r\n" + + "Proxy-agent: Node.js-Proxy\r\n" + + "\r\n" + ); + + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + serverSocket.on("error", () => { + // we ignore any errors + }); + clientSocket.on("error", () => { + // we ignore any errors + }); + }); + }); + + proxyServer.listen(0, "127.0.0.1", () => { + const addr = proxyServer.address(); resolve({ - address: listener.address().toString(), - close: () => listener.close(), + address: + typeof addr === "string" ? addr : `${addr.address}:${addr.port}`, + close: () => { + console.log("close"); + proxyServer.close(); + }, }); }); }); @@ -72,7 +106,7 @@ describe("full integration test", () => { }, }); - expect(driver.deploy(["source-stack"])).toContain("Apply complete!"); + expect(await driver.deploy(["source-stack"])).toContain("Apply complete!"); await client.Workspaces.deleteByName(orgName, workspaceName); }); @@ -91,9 +125,9 @@ describe("full integration test", () => { }); process.env.TF_EXECUTE_LOCAL = "true"; - driver.deploy(["source-stack"]); + await driver.deploy(["source-stack"]); process.env.TF_EXECUTE_LOCAL = undefined; - driver.deploy(["source-stack"]); + await driver.deploy(["source-stack"]); await client.Workspaces.deleteByName(orgName, workspaceName); }); @@ -115,42 +149,56 @@ describe("full integration test", () => { }, }); - driver.deploy(["source-stack", "consumer-stack"]); + await driver.deploy(["source-stack", "consumer-stack"]); + driver.output("source-stack", "outputs.tmp.json", true); + const outputs = JSON.parse(readFileSync("outputs.tmp.json").toString()); await client.Workspaces.deleteByName(orgName, workspaceName); - expect(driver.readLocalFile("origin-file.txt")).toEqual( - driver.readLocalFile("consumer-file.txt") + expect(driver.readLocalFile("consumer-file.txt")).toEqual( + outputs["source-stack"].password_output ); } ); - withAuth("deploy through HTTP_PROXY in Terraform Cloud", async () => { - const client = new TerraformCloud(TERRAFORM_CLOUD_TOKEN); + describe("with proxy", () => { + let proxyCalls: jest.Mock; + let proxyAddress: string | undefined; + let closeProxy: () => void | undefined; - await client.Workspaces.create(orgName, { - data: { - attributes: { - name: workspaceName, - executionMode: "remote", - terraformVersion: TERRAFORM_VERSION, - }, - type: "workspaces", - }, + beforeEach(async () => { + proxyCalls = jest.fn(); + const { close, address } = await startHttpProxy(proxyCalls); + proxyAddress = address; + closeProxy = close; }); - const proxyCalls = jest.fn(); - const { close, address } = await startHttpProxy( - "https://app.terraform.io/api/v2", - proxyCalls - ); - - process.env.HTTP_PROXY = address; - // Run deploy command - driver.deploy(["source-stack"]); - process.env.HTTP_PROXY = undefined; - close(); - await client.Workspaces.deleteByName(orgName, workspaceName); - expect(proxyCalls).toHaveBeenCalled(); + afterEach(async () => { + closeProxy(); + proxyAddress = undefined; + }); + + withAuth("deploy through HTTP_PROXY in Terraform Cloud", async () => { + const client = new TerraformCloud(TERRAFORM_CLOUD_TOKEN); + + await client.Workspaces.create(orgName, { + data: { + attributes: { + name: workspaceName, + executionMode: "remote", + terraformVersion: TERRAFORM_VERSION, + }, + type: "workspaces", + }, + }); + + process.env.HTTP_PROXY = `http://${proxyAddress}`; + // Run deploy command + console.log(await driver.deploy(["source-stack"])); + process.env.HTTP_PROXY = undefined; + + await client.Workspaces.deleteByName(orgName, workspaceName); + expect(proxyCalls).toHaveBeenCalled(); + }); }); }); diff --git a/test/yarn.lock b/test/yarn.lock index 23f6bb8470..e0bcececcc 100644 --- a/test/yarn.lock +++ b/test/yarn.lock @@ -598,13 +598,6 @@ dependencies: "@types/node" "*" -"@types/express-http-proxy@^1.6.3": - version "1.6.3" - resolved "https://registry.yarnpkg.com/@types/express-http-proxy/-/express-http-proxy-1.6.3.tgz#35fc0fb32e7741bc50619869de381ef759621fd0" - integrity sha512-dX3+Cb0HNPtqhC5JUWzzuODHRlgJRZx7KvwKVVwkOvm+8vOtpsh3qy8+qLv5X1hs4vdVHWKyXf86DwJot5H8pg== - dependencies: - "@types/express" "*" - "@types/express-serve-static-core@^4.17.18": version "4.17.28" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" @@ -614,7 +607,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@^4.17.13": +"@types/express@^4.17.13": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -1287,13 +1280,6 @@ debug@4, debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" -debug@^3.0.1: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - debug@^4.3.3: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" @@ -1477,11 +1463,6 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es6-promise@^4.1.1: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -1571,15 +1552,6 @@ expect@^27.3.1: jest-message-util "^27.3.1" jest-regex-util "^27.0.6" -express-http-proxy@^1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/express-http-proxy/-/express-http-proxy-1.6.3.tgz#f3ef139ffd49a7962e7af0462bbcca557c913175" - integrity sha512-/l77JHcOUrDUX8V67E287VEUQT0lbm71gdGVoodnlWBziarYKgMcpqT7xvh/HM8Jv52phw8Bd8tY+a7QjOr7Yg== - dependencies: - debug "^3.0.1" - es6-promise "^4.1.1" - raw-body "^2.3.0" - express@^4.17.2: version "4.17.2" resolved "https://registry.yarnpkg.com/express/-/express-4.17.2.tgz#c18369f265297319beed4e5558753cc8c1364cb3" @@ -2904,7 +2876,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -3173,7 +3145,7 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.4.2, raw-body@^2.3.0: +raw-body@2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32" integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==