diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 755b5da..1f0e009 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,10 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} + - name: Setup Node.js + uses: actions/setup-node@v2.2.0 + with: + node-version: 16.0.0 - name: Install dependencies run: yarn install --check-files - name: build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d45682a..668d76a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,10 @@ jobs: run: |- git config user.name "github-actions" git config user.email "github-actions@github.com" + - name: Setup Node.js + uses: actions/setup-node@v2.2.0 + with: + node-version: 16.0.0 - name: Install dependencies run: yarn install --check-files --frozen-lockfile - name: release diff --git a/.github/workflows/upgrade-main.yml b/.github/workflows/upgrade-main.yml index 4202da9..70d5dff 100644 --- a/.github/workflows/upgrade-main.yml +++ b/.github/workflows/upgrade-main.yml @@ -18,6 +18,10 @@ jobs: uses: actions/checkout@v2 with: ref: main + - name: Setup Node.js + uses: actions/setup-node@v2.2.0 + with: + node-version: 16.0.0 - name: Install dependencies run: yarn install --check-files --frozen-lockfile - name: Upgrade dependencies diff --git a/.projen/deps.json b/.projen/deps.json index 94a02a7..b180c2e 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -10,7 +10,7 @@ }, { "name": "@types/node", - "version": "^12", + "version": "^16", "type": "build" }, { @@ -100,10 +100,51 @@ "name": "@aws-sdk/client-cloudcontrol", "type": "runtime" }, + { + "name": "@aws-sdk/client-eventbridge", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-iam", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-lambda", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-secrets-manager", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-sqs", + "type": "runtime" + }, { "name": "@aws-sdk/client-ssm", "type": "runtime" }, + { + "name": "@aws-sdk/client-sts", + "type": "runtime" + }, + { + "name": "aws-sdk", + "type": "runtime" + }, + { + "name": "cdk-assets", + "type": "runtime" + }, + { + "name": "chalk", + "version": "4", + "type": "runtime" + }, + { + "name": "commander", + "type": "runtime" + }, { "name": "fast-json-patch", "type": "runtime" @@ -111,6 +152,10 @@ { "name": "immutable", "type": "runtime" + }, + { + "name": "short-uuid", + "type": "runtime" } ], "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." diff --git a/.projen/tasks.json b/.projen/tasks.json index fa83c42..0c61d56 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -166,7 +166,7 @@ "description": "Run tests", "steps": [ { - "exec": "jest --passWithNoTests --all --updateSnapshot" + "exec": "jest --passWithNoTests --all --updateSnapshot --coverageProvider=v8" }, { "spawn": "eslint" @@ -232,7 +232,7 @@ "exec": "yarn install --check-files" }, { - "exec": "yarn upgrade @types/immutable @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-import-resolver-node eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-prettier eslint jest jest-junit json-schema npm-check-updates prettier standard-version ts-jest ts-node typedoc typescript @aws-sdk/client-cloudcontrol @aws-sdk/client-ssm fast-json-patch immutable" + "exec": "yarn upgrade @types/immutable @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-import-resolver-node eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-prettier eslint jest jest-junit json-schema npm-check-updates prettier standard-version ts-jest ts-node typedoc typescript @aws-sdk/client-cloudcontrol @aws-sdk/client-eventbridge @aws-sdk/client-iam @aws-sdk/client-lambda @aws-sdk/client-secrets-manager @aws-sdk/client-sqs @aws-sdk/client-ssm @aws-sdk/client-sts aws-sdk cdk-assets chalk commander fast-json-patch immutable short-uuid" }, { "exec": "npx projen" diff --git a/.projenrc.js b/.projenrc.js index 31675bf..b64ece5 100644 --- a/.projenrc.js +++ b/.projenrc.js @@ -8,16 +8,31 @@ const project = new typescript.TypeScriptProject({ deps: [ "@aws-sdk/client-cloudcontrol", "@aws-sdk/client-ssm", + "@aws-sdk/client-eventbridge", + "@aws-sdk/client-iam", + "@aws-sdk/client-sts", + "@aws-sdk/client-sqs", + "@aws-sdk/client-secrets-manager", + "@aws-sdk/client-lambda", + "chalk@4", + "short-uuid", + "commander", "fast-json-patch", "immutable", + "cdk-assets", + "aws-sdk", ], - devDeps: ["@types/immutable", "ts-node"], + devDeps: ["@types/immutable", "ts-node", "@types/node"], eslintOptions: { prettier: true, }, releaseToNpm: true, docgen: true, + minNodeVersion: "16.0.0", gitignore: [".DS_Store"], + tsconfig: { + compilerOptions: { target: "es2020", lib: ["es2020"] }, + }, }); project.synth(); diff --git a/.vscode/settings.json b/.vscode/settings.json index 6bb9209..6d531b8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,7 +18,11 @@ "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, - "editor.quickSuggestions": true, + "editor.quickSuggestions": { + "comments": "on", + "strings": "on", + "other": "on" + }, "editor.suggest.showReferences": true, "editor.wordWrap": "on" }, @@ -29,5 +33,6 @@ "editor.tabSize": 2, "editor.insertSpaces": true, "editor.formatOnSave": true, - "eslint.format.enable": false + "eslint.format.enable": false, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/README.md b/README.md index 90bea05..fb3c667 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # node-cfn -This is a toy-implementation of the AWS CloudFormation deployment engine in TypeScript and Node-JS. It was built as an experiment to better underestand why CloudFormation is so slow and to also experiment locally with new features. +This is a toy-implementation of the AWS CloudFormation deployment engine in TypeScript and Node-JS. It was built as an experiment to better understand why CloudFormation is so slow and to also experiment locally with new features. It is built on top of the [AWS Cloud Control API](https://aws.amazon.com/cloudcontrolapi/) and thus only supports the [resources that are supported by the Cloud Control API](https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html). -The most important missing feature right now is Rollbacks, so this tool should not be considered useful for any type of production service. The hope is that it can become a playground for CloudFormation enhancements and to stand as a performance benchmark for the official AWS CloudFormation sevice. I dream of the day when CloudFormation is as fast as Terraform and Pulumi's provisioning engines. +The most important missing feature right now is Rollbacks, so this tool should not be considered useful for any type of production service. The hope is that it can become a playground for CloudFormation enhancements and to stand as a performance benchmark for the official AWS CloudFormation service. I dream of the day when CloudFormation is as fast as Terraform and Pulumi's provisioning engines. ## Supported Features @@ -14,6 +14,7 @@ The most important missing feature right now is Rollbacks, so this tool should n - [x] Intrinsic Functions (`Ref`, `!Ref`, `Fn::GetAtt`, `Fn::Join`, `Fn::Split`, `Fn::Select`, `Fn::FindInMap`, `Fn::Sub`, etc.) - [ ] Rollbacks on failure - [ ] Outputs and cross-stack references +- [ ] Assets ## Usage diff --git a/bin/node-cfn.js b/bin/node-cfn.js new file mode 100755 index 0000000..c02e540 --- /dev/null +++ b/bin/node-cfn.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require("../lib/cli"); \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 37d00eb..fe9c399 100644 --- a/docs/index.html +++ b/docs/index.html @@ -61,9 +61,9 @@

node-cfn

node-cfn

-

This is a toy-implementation of the AWS CloudFormation deployment engine in TypeScript and Node-JS. It was built as an experiment to better underestand why CloudFormation is so slow and to also experiment locally with new features.

+

This is a toy-implementation of the AWS CloudFormation deployment engine in TypeScript and Node-JS. It was built as an experiment to better understand why CloudFormation is so slow and to also experiment locally with new features.

It is built on top of the AWS Cloud Control API and thus only supports the resources that are supported by the Cloud Control API.

-

The most important missing feature right now is Rollbacks, so this tool should not be considered useful for any type of production service. The hope is that it can become a playground for CloudFormation enhancements and to stand as a performance benchmark for the official AWS CloudFormation sevice. I dream of the day when CloudFormation is as fast as Terraform and Pulumi's provisioning engines.

+

The most important missing feature right now is Rollbacks, so this tool should not be considered useful for any type of production service. The hope is that it can become a playground for CloudFormation enhancements and to stand as a performance benchmark for the official AWS CloudFormation service. I dream of the day when CloudFormation is as fast as Terraform and Pulumi's provisioning engines.

Supported Features

@@ -74,6 +74,7 @@

Supported Features

  • Intrinsic Functions (Ref, !Ref, Fn::GetAtt, Fn::Join, Fn::Split, Fn::Select, Fn::FindInMap, Fn::Sub, etc.)
  • Rollbacks on failure
  • Outputs and cross-stack references
  • +
  • Assets
  • Usage

    diff --git a/package.json b/package.json index 6464d9a..df6aeb4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "node-cfn", "bin": { - "node-cfn": "lib/main.js" + "node-cfn": "lib/main.js", + "node-cfn.js": "bin/node-cfn.js" }, "scripts": { "build": "npx projen build", @@ -28,7 +29,7 @@ "devDependencies": { "@types/immutable": "^3.8.7", "@types/jest": "^27.4.0", - "@types/node": "^12", + "@types/node": "^16", "@typescript-eslint/eslint-plugin": "^5", "@typescript-eslint/parser": "^5", "eslint": "^8", @@ -51,9 +52,23 @@ }, "dependencies": { "@aws-sdk/client-cloudcontrol": "^3.48.0", + "@aws-sdk/client-eventbridge": "^3.179.0", + "@aws-sdk/client-iam": "^3.180.0", + "@aws-sdk/client-lambda": "^3.185.0", + "@aws-sdk/client-secrets-manager": "^3.185.0", + "@aws-sdk/client-sqs": "^3.180.0", "@aws-sdk/client-ssm": "^3.48.0", + "@aws-sdk/client-sts": "^3.180.0", + "aws-sdk": "^2.1225.0", + "cdk-assets": "^2.43.1", + "chalk": "4", + "commander": "*", "fast-json-patch": "^3.1.0", - "immutable": "^4.0.0" + "immutable": "^4.0.0", + "short-uuid": "^4.2.0" + }, + "engines": { + "node": ">= 16.0.0" }, "main": "lib/index.js", "license": "Apache-2.0", diff --git a/src/aws.ts b/src/aws.ts new file mode 100644 index 0000000..33f5020 --- /dev/null +++ b/src/aws.ts @@ -0,0 +1,109 @@ +import { Account, ClientOptions, IAws } from "cdk-assets"; +import s3_v2 from "aws-sdk/clients/s3"; +import secrets_manager_v2 from "aws-sdk/clients/secretsmanager"; +import ecr_v2 from "aws-sdk/clients/ecr"; +import sts from "@aws-sdk/client-sts"; +import * as os from "os"; +import { ChainableTemporaryCredentials, Credentials } from "aws-sdk"; + +export default class AwsClient implements IAws { + constructor( + private account: string, + private region: string, + private sdkConfig?: any + ) {} + + async discoverPartition(): Promise { + return "aws"; + } + async discoverDefaultRegion(): Promise { + return this.region; + } + async discoverCurrentAccount(): Promise { + return { + accountId: this.account, + partition: await this.discoverPartition(), + }; + } + async discoverTargetAccount(options: ClientOptions): Promise { + const stsClient = await this.stsClient(await this.awsOptions(options)); + const response = await stsClient.send(new sts.GetCallerIdentityCommand({})); + if (!response.Account || !response.Arn) { + throw new Error( + `Unrecognized response from STS: '${JSON.stringify(response)}'` + ); + } + return { + accountId: response.Account!, + partition: response.Arn!.split(":")[1], + }; + } + async s3Client(options: ClientOptions): Promise { + return new s3_v2(options); + } + async stsClient(options: ClientOptions): Promise { + return new sts.STSClient(options); + } + async ecrClient(options: ClientOptions): Promise { + return new ecr_v2(options); + } + async secretsManagerClient( + options: ClientOptions + ): Promise { + return new secrets_manager_v2(await this.awsOptions(options)); + } + async awsOptions(options: ClientOptions) { + let credentials; + if (options.assumeRoleArn) { + credentials = await this.assumeRole( + options.region, + options.assumeRoleArn, + options.assumeRoleExternalId + ); + } + return { + ...this.sdkConfig, + region: options.region, + customUserAgent: "node-cfn", + credentials, + }; + } + /** + * Explicit manual AssumeRole call + * + * Necessary since I can't seem to get the built-in support for ChainableTemporaryCredentials to work. + * + * It needs an explicit configuration of `masterCredentials`, we need to put + * a `DefaultCredentialProverChain()` in there but that is not possible. + */ + async assumeRole( + region: string | undefined, + roleArn: string, + externalId?: string + ): Promise { + return new ChainableTemporaryCredentials({ + params: { + RoleArn: roleArn, + ExternalId: externalId, + RoleSessionName: `node-cfn-${safeUsername()}`, + }, + stsConfig: { + region, + customUserAgent: "node-cfn", + }, + }); + } +} + +/** + * Return the username with characters invalid for a RoleSessionName removed + * + * @see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html#API_AssumeRole_RequestParameters + */ +function safeUsername() { + try { + return os.userInfo().username.replace(/[^\w+=,.@-]/g, "@"); + } catch (e) { + return "noname"; + } +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..cee8466 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,56 @@ +import { program } from "commander"; +import fs from "fs/promises"; +import { displayTopoOrder } from "./display"; +import { Stack } from "./stack"; +import { CloudFormationTemplate } from "./template"; +import * as sts from "@aws-sdk/client-sts"; + +const STS = new sts.STSClient({}); + +program + .command("show") + .argument("