├── .eslintrc ├── .eslintrc.test.json ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── dependency-check.js │ ├── pr_qc.yml │ └── publish.yml ├── .gitignore ├── .husky └── pre-commit ├── .version-change-type ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEVELOPING_PLUGINS.md ├── LICENSE ├── PLUGINS.md ├── README.md ├── RELEASENOTES.md ├── example.gif ├── examples └── cdk │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── bin │ └── cdk.ts │ ├── cdk.json │ ├── index.ts │ ├── jest.config.js │ ├── lib │ └── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── abstracts │ ├── aws-cdk-parser.ts │ ├── index.ts │ ├── parser.ts │ ├── resource-checks.ts │ ├── template-checks.ts │ └── terraform-parser.ts ├── commands │ ├── check │ │ ├── checks │ │ │ ├── aws │ │ │ │ ├── index.ts │ │ │ │ └── resources.ts │ │ │ └── index.ts │ │ ├── detect-iac-format.ts │ │ ├── get-config.ts │ │ ├── index.ts │ │ ├── parser │ │ │ ├── aws-cdk │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── terraform │ │ │ │ └── index.ts │ │ └── prepare.ts │ ├── index.ts │ └── init │ │ └── index.ts ├── constants │ └── index.ts ├── errors │ ├── cli-error.ts │ ├── conflict-error.ts │ ├── index.ts │ └── quota-error.ts ├── exported-types.ts ├── hooks │ ├── cleanup-tmp-directory │ │ └── index.ts │ └── index.ts ├── index.ts ├── logger │ └── index.ts ├── types │ └── index.ts └── utils │ ├── dont-return-empty.ts │ ├── index.ts │ └── os │ └── index.ts ├── test ├── commands │ └── check │ │ ├── checks │ │ └── aws │ │ │ ├── MockResourceChecks.ts │ │ │ ├── MockTemplateChecks.ts │ │ │ └── index.test.ts │ │ ├── detect-iac-format.test.ts │ │ ├── get-config.test.ts │ │ ├── index.test.ts │ │ ├── parser │ │ ├── aws-cdk │ │ │ ├── MockParser.ts │ │ │ └── index.test.ts │ │ └── terraform │ │ │ ├── MockParser.ts │ │ │ └── index.test.ts │ │ ├── prepare.test.ts │ │ └── test-data │ │ ├── simple-sqs-stack │ │ ├── MockCdkDiff.txt │ │ ├── MockCdkTemplate.json │ │ ├── MockTfPlan.json │ │ └── manifest.json │ │ ├── tf-module-stack │ │ ├── main.tf │ │ ├── plan.json │ │ └── tf-json.json │ │ └── vpc-stack │ │ ├── cdk │ │ ├── no-nat │ │ │ ├── aws-cdk-diff.json │ │ │ └── diff.txt │ │ └── with-nat │ │ │ ├── aws-cdk-diff.json │ │ │ ├── diff.txt │ │ │ └── template.json │ │ └── tf │ │ ├── no-nat │ │ ├── main.tf │ │ ├── plan.json │ │ ├── tf-diff.json │ │ └── tf-json.json │ │ └── with-nat │ │ ├── main.tf │ │ ├── plan.json │ │ ├── tf-diff.json │ │ └── tf-json.json ├── hooks │ └── cleanup-tmp-directory │ │ └── index.test.ts ├── logger │ └── index.test.ts └── utils │ ├── dont-return-empty.test.ts │ └── os │ └── index.test.ts ├── tsconfig.json └── tsconfig.test.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaVersion": 13, 12 | "project": ["tsconfig.json"] 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint", 16 | "eslint-plugin-tsdoc", 17 | "unused-imports", 18 | "import" 19 | ], 20 | "rules": { 21 | "comma-dangle": [ 22 | "error", 23 | "never" 24 | ], 25 | "eol-last": [ 26 | "error", 27 | "never" 28 | ], 29 | "func-style": [ 30 | "error", 31 | "declaration" 32 | ], 33 | "indent": [ 34 | "error", 35 | 2, 36 | { "SwitchCase": 1 } 37 | ], 38 | "object-curly-spacing": [ 39 | "error", 40 | "always" 41 | ], 42 | "object-property-newline": [ 43 | "error", 44 | { 45 | "allowAllPropertiesOnSameLine": true 46 | } 47 | ], 48 | "prefer-arrow-callback": [ 49 | "error" 50 | ], 51 | "quotes": [ 52 | "error", 53 | "single" 54 | ], 55 | "semi": [ 56 | "error", 57 | "always" 58 | ], 59 | "space-before-function-paren": [ 60 | "error", 61 | "always" 62 | ], 63 | "arrow-parens": [ 64 | "error", 65 | "as-needed", 66 | { 67 | "requireForBlockBody": true 68 | } 69 | ], 70 | "tsdoc/syntax": "warn", 71 | "@typescript-eslint/no-explicit-any": "off", 72 | "@typescript-eslint/no-floating-promises": "error", 73 | "@typescript-eslint/no-unused-vars": "off", 74 | "unused-imports/no-unused-imports": "error", 75 | "unused-imports/no-unused-vars": [ 76 | "error", 77 | { 78 | "vars": "all", 79 | "varsIgnorePattern": "^_", 80 | "args": "after-used", 81 | "argsIgnorePattern": "^_" 82 | } 83 | ], 84 | "import/no-cycle": "error", 85 | "no-shadow": "error" 86 | } 87 | } -------------------------------------------------------------------------------- /.eslintrc.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaVersion": 13, 12 | "project": ["tsconfig.test.json"] 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint", 16 | "eslint-plugin-tsdoc", 17 | "unused-imports", 18 | "import" 19 | ], 20 | "rules": { 21 | "comma-dangle": [ 22 | "error", 23 | "never" 24 | ], 25 | "eol-last": [ 26 | "error", 27 | "never" 28 | ], 29 | "func-style": [ 30 | "error", 31 | "declaration" 32 | ], 33 | "indent": [ 34 | "error", 35 | 2 36 | ], 37 | "object-curly-spacing": [ 38 | "error", 39 | "always" 40 | ], 41 | "object-property-newline": [ 42 | "error", 43 | { 44 | "allowAllPropertiesOnSameLine": true 45 | } 46 | ], 47 | "prefer-arrow-callback": [ 48 | "error" 49 | ], 50 | "quotes": [ 51 | "error", 52 | "single" 53 | ], 54 | "semi": [ 55 | "error", 56 | "always" 57 | ], 58 | "space-before-function-paren": [ 59 | "error", 60 | "always" 61 | ], 62 | "arrow-parens": [ 63 | "error", 64 | "as-needed", 65 | { 66 | "requireForBlockBody": true 67 | } 68 | ], 69 | "tsdoc/syntax": "warn", 70 | "@typescript-eslint/no-explicit-any": "off", 71 | "@typescript-eslint/no-floating-promises": "error", 72 | "@typescript-eslint/no-unused-vars": "off", 73 | "@typescript-eslint/no-var-requires": "off", 74 | "@typescript-eslint/no-this-alias": "off", 75 | "unused-imports/no-unused-imports": "error", 76 | "unused-imports/no-unused-vars": [ 77 | "error", 78 | { 79 | "vars": "all", 80 | "args": "after-used", 81 | "argsIgnorePattern": "^_" 82 | } 83 | ], 84 | "import/no-cycle": "error" 85 | } 86 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Pull Request Type 2 | - [ ] Feature 3 | - [ ] Bug Fix 4 | 5 | ## Link to Notion Task or Github Issue 6 | 7 | 8 | 9 | 10 | ## Summary of Feature(s) 11 | 12 | 18 | 19 | ## Summary of Bug Fix(es) 20 | ### Previous Behaviour 21 | _Description of the bug and it's impact_ 22 | 23 | ### New Behaviour 24 | _Description of the bug fix and it's impact_ 25 | 26 | ## Other details 27 | 28 | 29 | ## Dependencies 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/dependency-check.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const depcheck = require('depcheck'); 3 | const isEmpty = require('lodash.isempty'); 4 | 5 | async function checkForMissingDependencies () { 6 | const projectRootDir = path.resolve('./'); 7 | const { missing } = await depcheck(projectRootDir, {}); 8 | if (!isEmpty(missing)) { 9 | throw new Error(`One or more dependencies are not installed! ${JSON.stringify(missing, null, 2)}`); 10 | } 11 | } 12 | checkForMissingDependencies() -------------------------------------------------------------------------------- /.github/workflows/pr_qc.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Pull Request Quality Checks 4 | 5 | # Controls when the workflow will run 6 | on: 7 | pull_request: 8 | branches: [ main ] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | name: PR checks 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v2 18 | with: 19 | node-version: 16 20 | cache: 'npm' 21 | - name: Quality Checks 22 | run: | 23 | npm ci; 24 | npm run qa; 25 | npm run build; 26 | node .github/workflows/dependency-check.js; 27 | changeType=$(<.version-change-type) 28 | if [ -z "$changeType" ]; 29 | then 30 | echo "missing file .version-change-type!" 31 | exit 1 32 | fi 33 | echo "Checking for release notes..." 34 | git fetch origin main ${{ github.event.pull_request.base.sha }}; 35 | diff=$(git diff -U0 ${{ github.event.pull_request.base.sha }} ${{ github.sha }} RELEASENOTES.md); 36 | if [ -z "$diff" ]; then echo "Missing release notes! exiting..."; exit 1; fi 37 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Flow 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | - 'push-action/**' 7 | workflow_dispatch: 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | # Just in case we ever need to push directly to 14 | - name: Release with tags 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: 16 18 | cache: 'npm' 19 | registry-url: 'https://registry.npmjs.org' 20 | token: ${{ secrets.NPM_DEPLOY_KEY }} 21 | - run: | 22 | npm ci; 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_DEPLOY_KEY }} 25 | - name: Submit PR for version change 26 | run: | 27 | git config --global user.email "zaydsimjee@gmail.com" 28 | git config --global user.name "Zayd Simjee" 29 | changeType=$(<.version-change-type) 30 | versionChange=$(npm version $changeType --no-git-tag-version) 31 | echo "version updated to" $versionChange 32 | git checkout -b "$versionChange" 33 | previousChangeLog=$(cat CHANGELOG.md) 34 | echo "$versionChange" > CHANGELOG.md 35 | echo "---" >> CHANGELOG.md 36 | echo "$(cat RELEASENOTES.md)" >> CHANGELOG.md 37 | echo " " >> CHANGELOG.md 38 | echo "$previousChangeLog" >> CHANGELOG.md 39 | echo "" > RELEASENOTES.md 40 | git add CHANGELOG.md 41 | git add RELEASENOTES.md 42 | git add package*.json 43 | git commit -m "version $versionChange [skip ci]" 44 | git push --set-upstream origin "$versionChange" 45 | git push origin --tags 46 | gh pr create --title "$versionChange" --body "update version to $versionChange" 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_DEPLOY_KEY }} 49 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | - name: Approve version change PR and merge 51 | run: | 52 | git config --global user.email "caleb.courier@gmail.com" 53 | git config --global user.name "Caleb Courier" 54 | gh pr review "$versionChange" --approve 55 | gh pr merge "$versionChange" --admin --squash 56 | sleep 2s 57 | env: 58 | NODE_AUTH_TOKEN: ${{ secrets.NPM_DEPLOY_KEY }} 59 | GH_TOKEN: ${{ secrets.SAFEER_APPROVER_BOT }} 60 | - name: Checkout main and publish module 61 | run: | 62 | git config --global user.email "zaydsimjee@gmail.com" 63 | git config --global user.name "Zayd Simjee" 64 | git checkout main 65 | git pull 66 | npm publish 67 | env: 68 | NODE_AUTH_TOKEN: ${{ secrets.NPM_DEPLOY_KEY }} 69 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint; 5 | -------------------------------------------------------------------------------- /.version-change-type: -------------------------------------------------------------------------------- 1 | patch -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v1.0.16 2 | --- 3 | 4 | 5 | v1.0.15 6 | --- 7 | 1. Update AWS CDK diff parser to account for different Stack name and construct id. 8 | 9 | v1.0.14 10 | --- 11 | 1. Add self review section to README 12 | 13 | v1.0.13 14 | --- 15 | 1. change all links in readme to explicitly reference github 16 | 17 | v1.0.12 18 | --- 19 | 1. Fix links in DEVELOPING_PLUGINS.md 20 | 1. Move non-production dependencies to devDependencies 21 | 1. Fix contributing pre-commit checklist 22 | 23 | v1.0.11 24 | --- 25 | 1. Add BSD 3-Clause license 26 | 1. Add description to package.json 27 | 28 | v1.0.10 29 | --- 30 | 1. Update gif to a more palatable resolution 31 | 1. Update title to match PH launch title 32 | 33 | v1.0.9 34 | --- 35 | 1. Add gif and readme ref to gif 36 | 1. Add use cases to readme 37 | 1. Add discord to readme 38 | 39 | v1.0.8 40 | --- 41 | 1. Add a sample cdk repo to test `precloud check` with 42 | 1. Add TOC to readme 43 | 1. Add how it works and contributing guides to TOC 44 | 1. Add example usage to readme 45 | 46 | v1.0.7 47 | --- 48 | Fix publish flow 49 | 50 | v1.0.6 51 | --- 52 | Add github workflows 53 | Safe guard against invalid parser responses 54 | Fix documentation for `requirePrivateSubnet` config option 55 | 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | This guide covers contributing to the core functionality of this module. For information on developing a third patry plugin see [Developing Plugins](PLUGINS.md) 4 | 5 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 6 | 7 | ## New contributor guide 8 | 9 | To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with contributions: 10 | 11 | - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git) 12 | - [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) 13 | - [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) 14 | 15 | 16 | ## Getting started 17 | 18 | ### Issues 19 | 20 | #### Create a new issue 21 | 22 | If you spot a problem with the modules or sample configs in this repository, [search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). If a related issue doesn't exist, you can open a new [issue](https://github.com/tinystacks/precloud/issues). 23 | 24 | #### Solve an issue 25 | 26 | Scan through our [existing issues](https://github.com/tinystacks/precloud/issues) to find one that interests you. You can narrow down the search using `labels` as filters. As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix. 27 | 28 | ### Make Changes 29 | 30 | #### Make changes locally 31 | 32 | 1. [Install Git](https://docs.github.com/en/get-started/quickstart/set-up-git). 33 | 34 | 2. If you are an external collaborator, fork the repository. 35 | 36 | 3. Install or update to **Terraform 1.x**. 37 | 38 | 4. Create a working branch and start with your changes! 39 | 40 | ### Commit your update 41 | 42 | Commit the changes once you are happy with them. Don't forget to [self-review](#self-review) to speed up the review process:zap:. 43 | 44 | #### Self Review 45 | You should always review your own PR first. 46 | 47 | For documentation changes, make sure that you: 48 | - [ ] Review the content for technical accuracy. 49 | - [ ] Review the entire pull request using the [translations guide for writers](https://github.com/github/docs/blob/main/contributing/translations/for-writers.md). 50 | 51 | If there are any failing checks in your PR, troubleshoot them until they're all passing. The following checklist is run automatically on pull requests, but you can also run them locally to check in advance. 52 | - [ ] run the linter 53 | - [ ] run tests 54 | - [ ] check test coverage 55 | - [ ] ensure the code builds 56 | - [ ] check for missing or unused dependencies with [depcheck](https://www.npmjs.com/package/depcheck) 57 | - [ ] add releasenotes 58 | - [ ] update .version-change-type (when applicable) 59 | 60 | ### Pull Request 61 | 62 | When you're finished with the changes, create a pull request, also known as a PR. 63 | - Fill the "Ready for review" template so that we can review your PR. This template helps reviewers understand your changes as well as the purpose of your pull request. 64 | - Don't forget to link the PR to the appropriate issue: 65 | - Github issue for external collaborators 66 | - If you are an external collaborator working from a fork, enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. 67 | Once you submit your PR, a team member will review your proposal. We may ask questions or request additional information. 68 | - We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch. 69 | - As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations). 70 | - If you run into any merge issues, checkout this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues. 71 | - As you commit changes to your branch, you should notice checks running and leaving a green checkmark (checks passed) or a red "x" (checks failed). Make sure all checks have passed. 72 | - Add details about your changes to RELEASENOTES.md. These are automatically prepended to the CHANGELOG when your PR is merged. 73 | - Update ./.version-change-type to reflect the type of changes included in your PR. Valid values are `major`, `minor`, and `patch` For a refresher on semver see: https://semver.org/ 74 | 75 | ### Your PR is merged! 76 | 77 | Congratulations :tada::tada: The team thanks you :sparkles:. 78 | 79 | Once your PR is merged, your contributions will be publicly visible. -------------------------------------------------------------------------------- /DEVELOPING_PLUGINS.md: -------------------------------------------------------------------------------- 1 | # Developing Plugins 2 | 3 | You can contribute to the ecosystem of this module by developing plugins. Plugins can be additional parsers, template checks, or resource checks. To develop a plugin, start by installing this module as a peer dependency. Each specific type of plugin is essentially an implementation of an abstract class we publish along with our types for this module. With that in mind, we typically recommend you develop plugins in [Typescript](https://www.typescriptlang.org/), but you can use any language that's capable of transpiling to Javascript since that is how you will distribute your plugin (via npm). See below for further details on developing the different types of plugins we support. 4 | 5 | ## Types Of Plugins And Their Behaviors 6 | ### Parsers 7 | 8 | Parsers are functions that given information about a resource in an IaC template return JSON that represents that resources base API definition. Since the inputs are different based on the IaC format, we currently publish two different abstract classes to guide development for parser plugins for AWS CDK and Terraform. 9 | 10 | #### AWS CDK Parser 11 | 12 | This type of parser uses information derived from the output of `cdk diff` and the synthesized Cloudformation template to extract the base API definition for a given cdk resource. 13 | 14 | A plugin implementing this type of parser must export a class that extends our `AwsCdkParser` abstract class. This primarily includes a named method `parseResource` with a specific method signature `(diff: CdkDiff, cloudformationTemplate: Json) => Promise`. 15 | 16 | See our default parser [@tinystacks/aws-cdk-parser](https://github.com/tinystacks/aws-cdk-parser) for an in depth example. 17 | 18 | #### Terraform Parser 19 | 20 | This type of parser uses information derived from the `terraform plan` command to extract the base API definition for a given terraform resource. 21 | 22 | A plugin implementing this type of parser must export a class that extends our `TerraformParser` abstract class. This primarily includes a named method `parseResource` with a specific method signature `(diff: TfDiff, tfPlan: Json) => Promise`. 23 | 24 | See our default parser [@tinystacks/terraform-resource-parser](https://github.com/tinystacks/terraform-resource-parser) for an in depth example. 25 | 26 | Note that you can also write a plugin that only attempts to parse resources from a specific Terraform module. This can be helpful in reducing the scope of your parser plugin since you will only have to crawl through known patterns in the tfplan (think references). 27 | 28 | For an example of a module specific parser, see our [@tinystacks/terraform-module-parser](https://github.com/tinystacks/terraform-module-parser); 29 | 30 | #### Expected Parser Behavior 31 | 32 | Besides correctly implementing the proper abstract class, a parser plugin should behave as follows: 33 | * Parsers should never throw. 34 | - Any thown errors will be ignored and the result from that parser will be considered undefined. 35 | * Parsers should be stateless and deterministic. 36 | - Given the same input, a parser should always yield the same output. 37 | * If you can't parse a resource, just return `undefined`. 38 | - Returning undefined allows other configured parsers to try to parse the resource. 39 | 40 | ### Checks 41 | #### Template Checks 42 | 43 | A template check plugin, as it's name implies, uses information about the proposed resources from the IaC template and runs verifications that span the template as a whole. This could include checking service quotas, validating required tags, etc. 44 | 45 | A template check plugin must export a class that extends our `TemplateChecks` abstract class. This primarily includes a named method `checkTemplate` with a specific method signature `(resources: ResourceDiffRecord[], config: CheckOptions): Promise`. 46 | 47 | See our default template checks [@tinystacks/aws-template-checks](https://github.com/tinystacks/aws-template-checks) for an in depth example. 48 | 49 | ##### Expected Template Check Behavior 50 | 51 | Besides correctly implementing the `TemplateChecks` abstract class, a template check plugin should behave as follows: 52 | * Don't short circuit your own checks 53 | - If your `checkTemplate` implementation can result in multiple error paths (i.e. multiple different checks), handle these internally and report them through the logger (see export `logger.cliError`). 54 | * Recoverable errors should be handled internally and retries should be implemented where reasonable. 55 | * If an error is potentially the result of bad configuration, consider throwing a `CliError` with helpful `hints`. 56 | * Template checks should be read only. 57 | - The scope of permissions is set by the end user via whatever credentials they allow to come through the [Node Provider Chain](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_credential_providers.html#fromnodeproviderchain). 58 | - If your template checks encounter 401's or other auth related errors, consider throwing a `CliError` explaining why the check failed. 59 | * If your plugin does not support a specific resource type, _do not throw an error_, just ignore it. 60 | 61 | #### Resource Checks 62 | 63 | A resource check plugin, as it's name implies, uses information about the proposed resources from the IaC template and performs some form of validation to ensure that the resource can be successfully deployed or is configured correctly. 64 | 65 | A resource check plugin must export a class that extends our `ResourceChecks` abstract class. This primarily includes a named method `checkResource` with a specific method signature `(resource: ResourceDiffRecord, allResources: ResourceDiffRecord[], config: CheckOptions): Promise`. 66 | 67 | See our default resource checks [@tinystacks/aws-resource-checks](https://github.com/tinystacks/aws-resource-checks) for an in depth example. 68 | 69 | ##### Expected Resource Check Behavior 70 | 71 | Besides correctly implementing the `ResourceChecks` abstract class, a resource check plugin should behave as follows: 72 | * Resource checks should throw a `CliError` if the deployment of the IaC template would encounter a runtime error. 73 | * Any other recoverable errors should be handled internally and retries should be implemented where reasonable. 74 | * If an error is potentially the result of bad configuration, consider throwing a `CliError` with helpful `hints`. 75 | * Resource checks should be read only. 76 | - The scope of permissions is set by the end user via whatever credentials they allow to come through the [Node Provider Chain](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_credential_providers.html#fromnodeproviderchain). 77 | - If your resource checks encounter 401's or other auth related errors, consider throwing a `CliError` explaining why the check failed. 78 | * If your plugin does not support a specific resource type, _do not throw an error_, just ignore it. 79 | 80 | #### Extending The Configuration Object 81 | You can extend the configuration object to add any configuration properties you need for your checks plugin by extending our interface `CheckOptions` and subbing in your extended interface for our base interface in the method signature. 82 | 83 | Note that we namespaced our additional configuration option; we _strongly_ encourage you to do the same. We used our scope as the namespace because we don't plan to use the same configuration option in multiple plugins published by us. You can make your namespace even more unique if necessary. For example, instead of only using our scope as the namespace `@tinystacks/strictBucketNaming`, we could namespace with the entire package name `@tinystacks/example-ts-resource-check/strictBucketNaming`. 84 | 85 | Example: 86 | ```js 87 | import { CliError, ResourceDiffRecord, ResourceChecks, CloudformationTypes, TerraformTypes, CheckOptions, getStandardResourceType } from "@tinystacks/precloud"; 88 | 89 | interface ExampleResourceChecksConfig extends CheckOptions { 90 | '@tinystacks/strictBucketNaming'?: boolean; 91 | } 92 | 93 | class ExampleResourceChecks extends ResourceChecks { 94 | constructor () { super(); } 95 | 96 | async checkResource(resource: ResourceDiffRecord, allResources: ResourceDiffRecord[], config: ExampleResourceChecksConfig): Promise { 97 | if ( 98 | ( 99 | resource.resourceType === CloudformationTypes.CFN_S3_BUCKET || 100 | resource.resourceType === TerraformTypes.TF_S3_BUCKET 101 | ) && 102 | resource.properties?.Name && 103 | config['@tinystacks/strictBucketNaming'] // custom config property! 104 | ) { 105 | const format = new RegExp(/[^a-zA-Z0-9-]+/); 106 | const nameIsInvalid = format.test(resource.properties?.Name); 107 | if (nameIsInvalid) { 108 | throw new CliError('Invalid S3 bucket name!', 'Name must only contain alphanumeric characters and hyphens.', 'Rename your bucket to meet these requirements or set "strictBucketNaming" to false if this requirement is unnecessary.') 109 | } 110 | } 111 | } 112 | 113 | } 114 | 115 | export default ExampleResourceChecks; 116 | ``` 117 | 118 | ## Using Your Plugin 119 | To use your plugin it must be resolvable in the directory of the IaC repository you plan to run the cli in. If you are publishing your plugin as an npm module, this means installing it either in or upstream of the IaC repository's root directory. Alternatively, if you only need to use your plugin for yourself or don't wish to publish it, you can provide a relative path to your plugin in the config file. 120 | 121 | ```json 122 | { 123 | "awsCdkParsers": [ 124 | "@my-scope/my-published-parser" 125 | ], 126 | "resourceChecks": [ 127 | "./my-local-resource-checks" 128 | ] 129 | } 130 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, TinyStacks 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /PLUGINS.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | This module runs on plugins. Even our default parsers and checks are composed of plugins published as separate npm modules. This allows our core cli code to stay light and extensible. To learn how to develop your own plugins see [DEVELOPING_PLUGINS.md]. To learn how to use existing plugins keep reading. 4 | 5 | ## Using Plugins 6 | In order to use a plugin, you simply need to install it in, or upstream of, the directory that contains your IaC code and then add it to your config file. Note that there are different fields for the different types of plugins that we support. Each are named respective to their purpose. So if you were to add all of our default plugins to your config (which is completely unnecessary but a good demonstration), that process would look something like this: 7 | 8 | `npm i -D @tinystacks/aws-cdk-parser @tinystacks/aws-template-checks @tinystacks/aws-resource-checks @tinystacks/terraform-module-parser @tinystacks/terraform-resource-parser` 9 | 10 | ```json 11 | { 12 | "awsCdkParsers": [ 13 | "@tinystacks/aws-cdk-parser" 14 | ], 15 | "terraformParsers": [ 16 | "@tinystacks/terraform-module-parser", 17 | "@tinystacks/terraform-resource-parser" 18 | ], 19 | "resourceChecks": [ 20 | "@tinystacks/aws-resource-checks" 21 | ], 22 | "templateChecks": [ 23 | "@tinystacks/aws-template-checks" 24 | ] 25 | } 26 | ``` 27 | 28 | If a plugin that you use defines additional configuration properties, you can add those properties directly into your config file. Check the plugin documentation for the correct property name. For example, our default resource checks `@tinystacks/aws-resource-checks` defines a config property called `requirePrivateSubnet`. To set this property, we would simply add to the config above like so: 29 | 30 | ```json 31 | { 32 | "awsCdkParsers": [ 33 | "@tinystacks/aws-cdk-parser" 34 | ], 35 | "terraformParsers": [ 36 | "@tinystacks/terraform-module-parser", 37 | "@tinystacks/terraform-resource-parser" 38 | ], 39 | "resourceChecks": [ 40 | "@tinystacks/aws-resource-checks" 41 | ], 42 | "templateChecks": [ 43 | "@tinystacks/aws-template-checks" 44 | ], 45 | "requirePrivateSubnet": true 46 | } 47 | ``` 48 | 49 | ## Default Plugins 50 | 51 | ### Parsers 52 | 53 | #### AWS CDK 54 | @tinystacks/aws-cdk-parser 55 | 56 | #### Terraform HCL 57 | @tinystacks/terraform-resource-parser 58 | @tinystacks/terraform-module-parser 59 | 60 | ### Checks 61 | #### AWS 62 | @tinystacks/aws-resource-checks 63 | @tinystacks/aws-template-checks 64 | 65 | #### GCP 66 | COMING SOON 67 | 68 | #### AZURE 69 | COMING SOON -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # precloud - Dynamic tests for infrastructure-as-code 2 | 3 | 1. [Introduction](https://github.com/tinystacks/precloud/blob/main/README.md#introduction) 4 | 1. [Use cases](https://github.com/tinystacks/precloud/blob/main/README.md#use-cases) 5 | 1. [How it works](https://github.com/tinystacks/precloud/blob/main/README.md#how-it-works) 6 | 1. [Contributing](https://github.com/tinystacks/precloud/blob/main/README.md#contributing) 7 | 1. [Installation](https://github.com/tinystacks/precloud/blob/main/README.md#installation) 8 | 1. [Install from the Global NPM registry](https://github.com/tinystacks/precloud/blob/main/README.md#install-from-the-global-npm-registry) 9 | 1. [Try it out](https://github.com/tinystacks/precloud/blob/main/README.md#try-it-out) 10 | 1. [Local Installation](https://github.com/tinystacks/precloud/blob/main/README.md#local-installation) 11 | 1. [Usage](https://github.com/tinystacks/precloud/blob/main/README.md#usage) 12 | 1. [precloud](https://github.com/tinystacks/precloud/blob/main/README.md#precloud) 13 | 1. [precloud --version](https://github.com/tinystacks/precloud/blob/main/README.md#precloud---version) 14 | 1. [precloud --help](https://github.com/tinystacks/precloud/blob/main/README.md#precloud---help) 15 | 1. [Available Commands](https://github.com/tinystacks/precloud/blob/main/README.md#Available-Commands) 16 | 1. [precloud help](https://github.com/tinystacks/precloud/blob/main/README.md#precloud-help) 17 | 1. [precloud check](https://github.com/tinystacks/precloud/blob/main/README.md#precloud-check) 18 | 1. [Options](https://github.com/tinystacks/precloud/blob/main/README.md#Options) 19 | 1. [Config File](https://github.com/tinystacks/precloud/blob/main/README.md#Config-File) 20 | 1. [Example Config File](https://github.com/tinystacks/precloud/blob/main/README.md#Example-Config-File) 21 | 1. [Check Behaviour](https://github.com/tinystacks/precloud/blob/main/README.md#Check-Behaviour) 22 | 1. [Authentication](https://github.com/tinystacks/precloud/blob/main/README.md#Authentication) 23 | 1. [AWS](https://github.com/tinystacks/precloud/blob/main/README.md#AWS) 24 | 1. [GCP](https://github.com/tinystacks/precloud/blob/main/README.md#GCP) 25 | 1. [Microsoft Azure](https://github.com/tinystacks/precloud/blob/main/README.md#Microsoft-Azure) 26 | 1. [Community](https://github.com/tinystacks/precloud/blob/main/README.md#community) 27 | 28 | ## Introduction 29 | 30 | example-gif 33 | 34 | Infrastructure code deployments often fail due to mismatched constraints over resource fields between the infrastructure code, the deployment engine, and the target cloud. For example, you may be able to pass any arbitrary string as a resource name to terraform or AWS CDK, and `plan` or `synth` go through fine, but the deployment may fail because that string failed a naming constraint on the target cloud. 35 | 36 | This package is an open source command line interface that is run before deploying to the cloud. It contains rules that check for names, quotas, and resource-specific constraints to make sure that your infrastructure code can be deployed successfully. 37 | 38 | ### Use cases 39 | 1. Harden your deployments. Ensure that you haven't defined resources that already exist so that you don't have to fail during deployments. 40 | 1. Enforce organizational resource patterns. Use resource checks to ensure resources are named and tagged correctly. 41 | 1. Maintain security standards. Use template check plugins to make sure that you're not launching things outside of VPCs, leaving public IPs open, or allowing global access to S3 buckets. 42 | 43 | ## How it works 44 | 45 | This package compairs resources in CDK diffs and Terraform Plans against the state of your cloud account. The rules and validations come from default and custom defined "plugins", which are composed of parsers and checkers. See [DEVELOPING_PLUGINS.md](DEVELOPING_PLUGINS.md) for more information. 46 | 47 | ## Contributing 48 | 49 | You may want to check for other attributes before deploying. This package is built using a plugin-model. You can find existing plugins at [PLUGINS.md](PLUGINS.md) and use them easily by adding the plugin to your config file. See the [example config file below](https://github.com/tinystacks/precloud/blob/main/README.md#-example-config-file). 50 | 51 | It is easy to create additional tests as plugins, please see [DEVELOPING_PLUGINS.md](https://github.com/tinystacks/precloud/blob/main/README.md#DEVELOPING_PLUGINS.md). Make sure to issue a PR to add your plugin to this package! 52 | 53 | ## Installation 54 | 55 | ### Install from the Global NPM registry 56 | ```bash 57 | # Install the CLI globally 58 | # Using the -g option installs the precloud cli to your shell scope instead of the package scope. 59 | # It adds the CLI command to bin, allowing you to call precloud from anywhere 60 | npm i -g @tinystacks/precloud; 61 | 62 | # Use the CLI, refer to the usage guide below 63 | precloud --version; 64 | 65 | ``` 66 | 67 | #### Try it out 68 | ```bash 69 | # After installing the CLI, you can try it out on a cdk or terraform package 70 | # An example cdk package is included in this package 71 | git clone https://github.com/tinystacks/precloud.git; 72 | 73 | # navigate to the examples directory 74 | cd precloud/examples/cdk; 75 | 76 | # install dependencies 77 | npm i; 78 | 79 | # (Optional) initalize precloud 80 | precloud init; 81 | 82 | # run precloud check 83 | precloud check; 84 | 85 | # To see a precloud check fail, uncomment the commented out lines in examples/cdk/index.ts 86 | precloud check; 87 | ``` 88 | 89 | ### Local Installation 90 | ```bash 91 | # Clone this package 92 | git clone https://github.com/tinystacks/precloud.git; 93 | 94 | # Install dependencies and build 95 | npm i; npm run build; 96 | 97 | # Install the CLI globally 98 | # Using the -g option installs the precloud cli to your shell scope instead of the package scope. 99 | # It adds the CLI command to bin, allowing you to call precloud from anywhere 100 | npm i -g; 101 | 102 | # Use the CLI, refer to the usage guide below 103 | precloud --version; 104 | ``` 105 | 106 | ## Usage 107 | ### precloud 108 | Shows usage and help information. 109 | 110 | ### precloud --version 111 | _Alias_: -V 112 | Shows the current installed version number. 113 | 114 | ### precloud --help 115 | _Alias_: -h 116 | Shows usage and help information. 117 | 118 | 119 | ## Available Commands 120 | 121 | ### precloud help 122 | Shows usage and help information. 123 | 124 | ### precloud check 125 | Performs a check on an AWS CDK app or a Terraform configuration to validate the planned resources can be launched or updated. 126 | 127 | #### Options 128 | |Flag|Arguments|Description| 129 | |----|---------|-----------| 130 | |-f, --format|\| Specifies the iac format. Can also be set via "format" in the config file. (choices: "tf", "aws-cdk")| 131 | |-c, --config-file|\| Specifies a config file. Options specified via the command line will always take precedence over options specified in a config file. Looks for precloud.config.json by default.| 132 | |-h, --help|| display help for this command 133 | 134 | #### Config File 135 | Alternatively, instead of specifying options via command line flags, you can set them in a configuration file. This file must be valid JSON and named either precloud.config.json or the `--config-file` flag specified. 136 | Valid config properties: 137 | |Property name|Type|Description| 138 | |-------------|----|-----------| 139 | |format|String|Specifies the iac format. (valid values: "tf", "aws-cdk")| 140 | |awsCdkParsers|Array\|A list of npm module names to parse AWS CDK resources. By default, the internal TinyStacks AWS CDK Parser will be used. Any parsers besides defaults must be installed in the target cdk repository.| 141 | |terraformParsers|Array\|A list of npm module names to parse Terraform resources or modules. By default, the internal TinyStacks Terraform Resource Parser and TinyStacks Terraform Module Parser will be used. Any parsers besides defaults must be installed in the target terraform repository.| 142 | |resourceChecks|Array\|A list of npm module names to run resource checks. By default, the [@tinystacks/aws-resource-checks](https://github.com/tinystacks/aws-resource-checks) package will be used. Any resource checks besides this must be installed within or upstream of the IaC repository.| 143 | |templateChecks|Array\|A list of npm module names to run template checks. By default, the [@tinystacks/aws-template-checks](https://github.com/tinystacks/aws-template-checks) package will be used. Any template checks besides this must be installed within or upstream of the IaC repository.| 144 | |requirePrivateSubnet|Boolean|Option for default plugin `@tinystacks/aws-resource-checks`. When set to true, requires VPCs to have a subnet with egress to the internet, but no ingress. Defaults to `false`.| 145 | 146 | #### Example Config File 147 | ```json 148 | { 149 | "awsCdkParsers": [ 150 | "@tinystacks/aws-cdk-parser" 151 | ], 152 | "terraformParsers": [ 153 | "@tinystacks/terraform-resource-parser", 154 | "@tinystacks/terraform-module-parser" 155 | ], 156 | "templateChecks": [ 157 | "@tinystacks/aws-template-checks" 158 | ], 159 | "resourceChecks": [ 160 | "@tinystacks/aws-resource-checks" 161 | ] 162 | } 163 | ``` 164 | 165 | #### Check Behaviour 166 | When the `check` command is run, it will first perform a diffing operation to determine the changes that deploying the stack would make. For AWS CDK this is `cdk diff`, for Terraform `terraform plan`. 167 | 168 | The diff from this operation is then used to identify resources that would change. These resources are then tested first by running template checks which validate across the resources in the IaC configuration, and then at an individual resource level to determine if any runtime errors might occur during a deployment. 169 | 170 | This cli includes some of our plugins for parsing and running template and resource checks by default. 171 | The default plugins will check the following: 172 | 1. Any SQS queue names are unique. 173 | 1. Any S3 bucket names are unique. 174 | 1. The current stack will not surpass the S3 service quota. 175 | 1. The current stack will not surpass the Elastic IP Address service quota. 176 | 1. The current stack will not surpass the VPC service quota. 177 | 1. (Optional) Verifies that the VPC has private subnets (egress-only subnets via a NAT Gateway or Nat Instance(s)). 178 | 179 | #### Authentication 180 | This command requires authentication to the Cloud Provider the CDK app or Terraform config will use. The following authentication methods are supported. 181 | 182 | ##### AWS 183 | - Environment Variables (preferred) 184 | - Any other authetication method supported by the [Node Provider Chain](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_credential_providers.html#fromnodeproviderchain). 185 | 186 | ##### GCP 187 | Not supported. 188 | 189 | ##### Microsoft Azure 190 | Not supported. 191 | 192 | 193 | # Community 194 | Join our [discord](https://discord.gg/AZZzdGVCNW) to have a chat! -------------------------------------------------------------------------------- /RELEASENOTES.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinystacks/precloud/b97b902d3395536bbae38fe8b20cfce0db2a5642/example.gif -------------------------------------------------------------------------------- /examples/cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /examples/cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /examples/cdk/README.md: -------------------------------------------------------------------------------- 1 | This is an example CDK package that you can use to test out `precloud check`. Here are steps to get started 2 | 3 | ```bash 4 | # Navigate to the correct dir 5 | cd examples/cdk; 6 | 7 | # install dependencies 8 | npm i; 9 | 10 | # (Optional) initalize precloud 11 | precloud init; 12 | 13 | # run precloud check 14 | precloud check; 15 | 16 | # To see a precloud check fail, uncomment the commented out lines in examples/cdk/index.ts 17 | precloud check; 18 | ``` -------------------------------------------------------------------------------- /examples/cdk/bin/cdk.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Example } from '../lib/index'; 3 | 4 | const app = new cdk.App(); 5 | new Example(app, 'example'); -------------------------------------------------------------------------------- /examples/cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | ] 9 | }, 10 | "context": { 11 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 12 | "@aws-cdk/core:stackRelativeExports": true, 13 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 14 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 15 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 16 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 17 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 18 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/aws-iam:minimizePolicies": true, 21 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 22 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 23 | "@aws-cdk/core:target-partitions": [ 24 | "aws", 25 | "aws-cn" 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /examples/cdk/index.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Stack } from 'aws-cdk-lib'; 3 | import { Construct } from 'constructs'; 4 | 5 | export class Example extends Stack { 6 | 7 | constructor(scope: Construct, id: string) { 8 | super(scope, id); 9 | 10 | new cdk.aws_s3.Bucket(this, 'uniqueBucketConst', { 11 | bucketName: "a-unique-demo-bucket-name" 12 | }); 13 | 14 | // new cdk.aws_s3.Bucket(this, 'notSoUniqueBucketConst', { 15 | // bucketName: "a-unique-demo-bucket-name" 16 | // }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /examples/cdk/lib/index.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Stack } from 'aws-cdk-lib'; 3 | import { Construct } from 'constructs'; 4 | 5 | export class Example extends Stack { 6 | 7 | constructor(scope: Construct, id: string) { 8 | super(scope, id); 9 | 10 | new cdk.aws_s3.Bucket(this, 'uniqueBucketConst', { 11 | bucketName: "a-unique-demo-bucket-name" 12 | }); 13 | 14 | // new cdk.aws_s3.Bucket(this, 'notSoUniqueBucketConst', { 15 | // bucketName: "a-unique-demo-bucket-name" 16 | // }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "main": "lib/index.js", 5 | "types": "lib/index.d.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "watch": "tsc -w", 9 | "test": "jest" 10 | }, 11 | "devDependencies": { 12 | "@types/jest": "^29.2.3", 13 | "@types/node": "18.7.2", 14 | "aws-cdk-lib": "2.54.0", 15 | "constructs": "^10.0.0", 16 | "jest": "^29.3.1", 17 | "ts-jest": "^29.0.3", 18 | "typescript": "4.7.4" 19 | }, 20 | "peerDependencies": { 21 | "aws-cdk-lib": "2.54.0", 22 | "constructs": "^10.0.0" 23 | }, 24 | "dependencies": { 25 | "ts-node": "^10.9.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2022" 7 | ], 8 | "esModuleInterop": true, 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ], 26 | "outDir": "dist" 27 | }, 28 | "include": [ 29 | "src" 30 | ] 31 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testPathIgnorePatterns: ['.d.ts', '.js'], 6 | coveragePathIgnorePatterns: [ 7 | 'src/errors' 8 | ], 9 | verbose: true, 10 | coverageThreshold: { 11 | global: { 12 | branches: 70, 13 | functions: 80, 14 | lines: 90, 15 | statements: 90 16 | } 17 | } 18 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinystacks/precloud", 3 | "version": "1.0.16", 4 | "description": "An open source command line interface that runs checks on infrastructure as code to catch potential deployment issues before deploying.", 5 | "main": "dist/exported-types.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "bin": { 10 | "precloud": "dist/index.js" 11 | }, 12 | "scripts": { 13 | "build": "tsc", 14 | "cleanup": "rm -rf dist || true && rm *.tgz || true", 15 | "clean-build": "npm ci && npm run cleanup && npm run build", 16 | "dependency-check": "./node_modules/.bin/depcheck", 17 | "install-remote": "npm i --@tinystacks:registry=https://registry.npmjs.org", 18 | "install-local": "npm i --@tinystacks:registry=http:////local-npm-registry:4873", 19 | "lint": "./node_modules/.bin/eslint ./src", 20 | "lint-fix": "./node_modules/.bin/eslint --fix ./src", 21 | "lint-tests": "./node_modules/.bin/eslint --config .eslintrc.test.json ./test", 22 | "lint-fix-tests": "./node_modules/.bin/eslint --config .eslintrc.test.json --fix ./test", 23 | "major": "npm version major --no-git-tag-version", 24 | "minor": "npm version minor --no-git-tag-version", 25 | "patch": "npm version patch --no-git-tag-version", 26 | "prepack": "npm run clean-build", 27 | "prerelease": "npm version prerelease --preid=local --no-git-tag-version", 28 | "publish-local": "npm run prerelease; npm publish --tag local --@tinystacks:registry=http://local-npm-registry:4873", 29 | "qa": "npm run lint && npm run test-cov", 30 | "test": "jest", 31 | "test-cov": "jest --coverage", 32 | "test-file": "jest ./test/commands/check/parser/aws-cdk/index.test.ts", 33 | "test-file-cov": "jest ./test/cli/commands/check/checks/aws/resource-tests/vpc-checks.test.ts --coverage", 34 | "view-test-cov": "jest --coverage && open coverage/lcov-report/index.html", 35 | "prepare": "husky install" 36 | }, 37 | "author": "", 38 | "license": "BSD-3-Clause", 39 | "dependencies": { 40 | "colors": "^1.4.0", 41 | "commander": "^10.0.0", 42 | "lodash.isnil": "^4.0.0", 43 | "lodash.isplainobject": "^4.0.6" 44 | }, 45 | "devDependencies": { 46 | "@types/jest": "^29.2.6", 47 | "@types/lodash.isnil": "^4.0.7", 48 | "@types/lodash.isplainobject": "^4.0.7", 49 | "@typescript-eslint/eslint-plugin": "^5.48.2", 50 | "@typescript-eslint/parser": "^5.48.2", 51 | "depcheck": "^1.4.3", 52 | "eslint": "^8.32.0", 53 | "eslint-plugin-import": "^2.27.5", 54 | "eslint-plugin-tsdoc": "^0.2.17", 55 | "eslint-plugin-unused-imports": "^2.0.0", 56 | "husky": "^8.0.3", 57 | "jest": "^29.3.1", 58 | "lodash.isempty": "^4.4.0", 59 | "ts-jest": "^29.0.5" 60 | }, 61 | "peerDependencies": { 62 | "@tinystacks/aws-cdk-parser": "0.x", 63 | "@tinystacks/aws-resource-checks": "0.x", 64 | "@tinystacks/aws-template-checks": "0.x", 65 | "@tinystacks/terraform-module-parser": "0.x", 66 | "@tinystacks/terraform-resource-parser": "0.x" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/abstracts/aws-cdk-parser.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { CdkDiff, Json } from '../types'; 3 | import Parser from './parser'; 4 | 5 | abstract class AwsCdkParser implements Parser { 6 | constructor () {} 7 | abstract parseResource (diff: CdkDiff, cloudformationTemplate: Json): Promise 8 | } 9 | 10 | export { 11 | AwsCdkParser 12 | }; 13 | export default AwsCdkParser; -------------------------------------------------------------------------------- /src/abstracts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aws-cdk-parser'; 2 | export * from './parser'; 3 | export * from './template-checks'; 4 | export * from './resource-checks'; 5 | export * from './terraform-parser'; -------------------------------------------------------------------------------- /src/abstracts/parser.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { CdkDiff, Json, TfDiff } from '../types'; 3 | 4 | interface Parser { 5 | parseResource (diff: TfDiff | CdkDiff, planOrTemplate: Json): Promise 6 | } 7 | 8 | export { 9 | Parser 10 | }; 11 | 12 | export default Parser; -------------------------------------------------------------------------------- /src/abstracts/resource-checks.ts: -------------------------------------------------------------------------------- 1 | import { ResourceDiffRecord, CheckOptions } from '../types'; 2 | 3 | abstract class ResourceChecks { 4 | abstract checkResource (resource: ResourceDiffRecord, allResources: ResourceDiffRecord[], config: CheckOptions): Promise; 5 | } 6 | 7 | export { 8 | ResourceChecks 9 | }; 10 | 11 | export default ResourceChecks; -------------------------------------------------------------------------------- /src/abstracts/template-checks.ts: -------------------------------------------------------------------------------- 1 | import { ResourceDiffRecord, CheckOptions } from '../types'; 2 | 3 | abstract class TemplateChecks { 4 | abstract checkTemplate (resources: ResourceDiffRecord[], config: CheckOptions): Promise; 5 | } 6 | 7 | export { 8 | TemplateChecks 9 | }; 10 | 11 | export default TemplateChecks; -------------------------------------------------------------------------------- /src/abstracts/terraform-parser.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { TfDiff, Json } from '../types'; 3 | import Parser from './parser'; 4 | 5 | abstract class TerraformParser implements Parser { 6 | constructor () {} 7 | abstract parseResource (diff: TfDiff, tfPlan: Json): Promise 8 | } 9 | 10 | export { 11 | TerraformParser 12 | }; 13 | export default TerraformParser; -------------------------------------------------------------------------------- /src/commands/check/checks/aws/index.ts: -------------------------------------------------------------------------------- 1 | import TemplateChecks from '../../../../abstracts/template-checks'; 2 | import { TINYSTACKS_AWS_TEMPLATE_CHECKS, TINYSTACKS_AWS_RESOURCE_CHECKS } from '../../../../constants'; 3 | import { ResourceDiffRecord, CheckOptions } from '../../../../types'; 4 | import logger from '../../../../logger'; 5 | import ResourceChecks from '../../../../abstracts/resource-checks'; 6 | 7 | const resourceChecksCache: { 8 | [name: string]: ResourceChecks 9 | } = {}; 10 | 11 | async function tryToUseResourceChecks (resource: ResourceDiffRecord, allResources: ResourceDiffRecord[], config: CheckOptions, resourceChecksName: string): Promise { 12 | let resourceChecksInstance = resourceChecksCache[resourceChecksName]; 13 | try { 14 | if (!resourceChecksInstance) { 15 | const modulePath = resourceChecksName === TINYSTACKS_AWS_RESOURCE_CHECKS ? 16 | resourceChecksName : 17 | require.resolve(resourceChecksName, { paths: [process.cwd()] }); 18 | // eslint-disable-next-line @typescript-eslint/no-var-requires 19 | const resourceChecks = require(modulePath); 20 | const mainExport = resourceChecks?.default ? resourceChecks.default : resourceChecks; 21 | if (mainExport) { 22 | resourceChecksInstance = new mainExport(); 23 | const isInstance = resourceChecksInstance instanceof ResourceChecks; 24 | const hasCheckResource = resourceChecksInstance.checkResource && typeof resourceChecksInstance.checkResource === 'function'; 25 | if (isInstance || hasCheckResource) { 26 | resourceChecksCache[resourceChecksName] = resourceChecksInstance; 27 | } else { 28 | logger.warn(`Invalid resource tester: ${resourceChecksName}.`); 29 | logger.warn(`The main export from ${resourceChecksName} does not properly implement ResourceChecks.`); 30 | } 31 | } 32 | } 33 | } 34 | catch (error) { 35 | logger.warn(`Invalid resource tester: ${resourceChecksName}.`); 36 | logger.warn(`The main export from ${resourceChecksName} could not be instantiated.`); 37 | logger.verbose(error); 38 | } 39 | if (resourceChecksInstance) { 40 | await resourceChecksInstance.checkResource(resource, allResources, config); 41 | } 42 | } 43 | 44 | async function testResource (resource: ResourceDiffRecord, allResources: ResourceDiffRecord[], config: CheckOptions) { 45 | const { 46 | resourceChecks = [] 47 | } = config; 48 | if (!resourceChecks.includes(TINYSTACKS_AWS_RESOURCE_CHECKS)) resourceChecks.push(TINYSTACKS_AWS_RESOURCE_CHECKS); 49 | for (const resourceCheck of resourceChecks) { 50 | await tryToUseResourceChecks(resource, allResources, config, resourceCheck); 51 | } 52 | } 53 | 54 | const templateChecksCache: { 55 | [name: string]: TemplateChecks 56 | } = {}; 57 | 58 | async function tryToUseTemplateChecks (resources: ResourceDiffRecord[], config: CheckOptions, templateChecksName: string): Promise { 59 | let templateChecksInstance = templateChecksCache[templateChecksName]; 60 | try { 61 | if (!templateChecksInstance) { 62 | const modulePath = templateChecksName === TINYSTACKS_AWS_TEMPLATE_CHECKS ? 63 | templateChecksName : 64 | require.resolve(templateChecksName, { paths: [process.cwd()] }); 65 | // eslint-disable-next-line @typescript-eslint/no-var-requires 66 | const templateChecks = require(modulePath); 67 | const mainExport = templateChecks?.default ? templateChecks.default : templateChecks; 68 | if (mainExport) { 69 | templateChecksInstance = new mainExport(); 70 | const isInstance = templateChecksInstance instanceof TemplateChecks; 71 | const hasCheckTemplate = templateChecksInstance.checkTemplate && typeof templateChecksInstance.checkTemplate === 'function'; 72 | if (isInstance || hasCheckTemplate) { 73 | templateChecksCache[templateChecksName] = templateChecksInstance; 74 | } else { 75 | logger.warn(`Invalid template check module: ${templateChecksName}.`); 76 | logger.warn(`The main export from ${templateChecksName} does not properly implement TemplateChecks.`); 77 | } 78 | } 79 | } 80 | } 81 | catch (error) { 82 | logger.warn(`Invalid template check module: ${templateChecksName}.`); 83 | logger.warn(`The main export from ${templateChecksName} could not be instantiated.`); 84 | logger.verbose(error); 85 | } 86 | if (templateChecksInstance) { 87 | await templateChecksInstance.checkTemplate(resources, config); 88 | } 89 | } 90 | 91 | async function checkTemplates (resources: ResourceDiffRecord[], config: CheckOptions) { 92 | const { 93 | templateChecks = [] 94 | } = config; 95 | if (!templateChecks.includes(TINYSTACKS_AWS_TEMPLATE_CHECKS)) templateChecks.push(TINYSTACKS_AWS_TEMPLATE_CHECKS); 96 | const errors: Error[] = []; 97 | for (const templateCheck of templateChecks) { 98 | await tryToUseTemplateChecks(resources, config, templateCheck) 99 | .catch(error => errors.push(error)); 100 | } 101 | errors.forEach(logger.cliError, logger); 102 | } 103 | 104 | export { 105 | testResource, 106 | checkTemplates 107 | }; -------------------------------------------------------------------------------- /src/commands/check/checks/aws/resources.ts: -------------------------------------------------------------------------------- 1 | import { Json } from '../../../../types'; 2 | 3 | // Standard Types 4 | const SQS_QUEUE = 'SQS_QUEUE'; 5 | const S3_BUCKET = 'S3_BUCKET'; 6 | const VPC = 'VPC'; 7 | const NAT_GATEWAY = 'NAT_GATEWAY'; 8 | const EIP = 'EIP'; 9 | const SUBNET = 'SUBNET'; 10 | const ROUTE_TABLE_ASSOCIATION = 'ROUTE_TABLE_ASSOCIATION'; 11 | const ROUTE = 'ROUTE'; 12 | const ROUTE_TABLE = 'ROUTE_TABLE'; 13 | const INTERNET_GATEWAY = 'INTERNET_GATEWAY'; 14 | 15 | // Cloudformation Types 16 | const CFN_SQS_QUEUE = 'AWS::SQS::Queue'; 17 | const CFN_S3_BUCKET = 'AWS::S3::Bucket'; 18 | const CFN_VPC = 'AWS::EC2::VPC'; 19 | const CFN_NAT_GATEWAY = 'AWS::EC2::NatGateway'; 20 | const CFN_EIP = 'AWS::EC2::EIP'; 21 | const CFN_SUBNET = 'AWS::EC2::Subnet'; 22 | const CFN_ROUTE_TABLE_ASSOCIATION = 'AWS::EC2::SubnetRouteTableAssociation'; 23 | const CFN_ROUTE = 'AWS::EC2::Route'; 24 | const CFN_ROUTE_TABLE = 'AWS::EC2::RouteTable'; 25 | 26 | const CloudformationTypes = { 27 | CFN_SQS_QUEUE, 28 | CFN_S3_BUCKET, 29 | CFN_VPC, 30 | CFN_NAT_GATEWAY, 31 | CFN_EIP, 32 | CFN_SUBNET, 33 | CFN_ROUTE_TABLE_ASSOCIATION, 34 | CFN_ROUTE, 35 | CFN_ROUTE_TABLE 36 | }; 37 | 38 | // Terraform Types 39 | const TF_SQS_QUEUE = 'aws_sqs_queue'; 40 | const TF_S3_BUCKET = 'aws_s3_bucket'; 41 | const TF_VPC = 'aws_vpc'; 42 | const TF_NAT_GATEWAY = 'aws_nat_gateway'; 43 | const TF_EIP = 'aws_eip'; 44 | const TF_SUBNET = 'aws_subnet'; 45 | const TF_ROUTE_TABLE_ASSOCIATION = 'aws_route_table_association'; 46 | const TF_ROUTE = 'aws_route'; 47 | const TF_ROUTE_TABLE = 'aws_route_table'; 48 | const TF_INTERNET_GATEWAY = 'aws_internet_gateway'; 49 | 50 | const TerraformTypes = { 51 | TF_SQS_QUEUE, 52 | TF_S3_BUCKET, 53 | TF_VPC, 54 | TF_NAT_GATEWAY, 55 | TF_EIP, 56 | TF_SUBNET, 57 | TF_ROUTE_TABLE_ASSOCIATION, 58 | TF_ROUTE, 59 | TF_ROUTE_TABLE, 60 | TF_INTERNET_GATEWAY 61 | }; 62 | 63 | const resourceTypeMap: Json = { 64 | SQS_QUEUE, 65 | [CFN_SQS_QUEUE]: SQS_QUEUE, 66 | [TF_SQS_QUEUE]: SQS_QUEUE, 67 | S3_BUCKET, 68 | [CFN_S3_BUCKET]: S3_BUCKET, 69 | [TF_S3_BUCKET]: S3_BUCKET, 70 | VPC, 71 | [CFN_VPC]: VPC, 72 | [TF_VPC]: VPC, 73 | NAT_GATEWAY, 74 | [CFN_NAT_GATEWAY]: NAT_GATEWAY, 75 | [TF_NAT_GATEWAY]: NAT_GATEWAY, 76 | EIP, 77 | [CFN_EIP]: EIP, 78 | [TF_EIP]: EIP, 79 | SUBNET, 80 | [CFN_SUBNET]: SUBNET, 81 | [TF_SUBNET]: SUBNET, 82 | ROUTE_TABLE_ASSOCIATION, 83 | [CFN_ROUTE_TABLE_ASSOCIATION]: ROUTE_TABLE_ASSOCIATION, 84 | [TF_ROUTE_TABLE_ASSOCIATION]: ROUTE_TABLE_ASSOCIATION, 85 | ROUTE, 86 | [CFN_ROUTE]: ROUTE, 87 | [TF_ROUTE]: ROUTE, 88 | ROUTE_TABLE, 89 | [CFN_ROUTE_TABLE]: ROUTE_TABLE, 90 | [TF_ROUTE_TABLE]: ROUTE_TABLE, 91 | INTERNET_GATEWAY, 92 | [TF_INTERNET_GATEWAY]: INTERNET_GATEWAY 93 | }; 94 | 95 | function getStandardResourceType (type: string): string { 96 | return resourceTypeMap[type]; 97 | } 98 | 99 | export { 100 | SQS_QUEUE, 101 | S3_BUCKET, 102 | VPC, 103 | NAT_GATEWAY, 104 | EIP, 105 | SUBNET, 106 | ROUTE_TABLE_ASSOCIATION, 107 | ROUTE, 108 | ROUTE_TABLE, 109 | CloudformationTypes, 110 | TerraformTypes, 111 | getStandardResourceType 112 | }; -------------------------------------------------------------------------------- /src/commands/check/checks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aws'; -------------------------------------------------------------------------------- /src/commands/check/detect-iac-format.ts: -------------------------------------------------------------------------------- 1 | import { resolve as resolvePath } from 'path'; 2 | import { readdirSync } from 'fs'; 3 | import { IacFormat } from '../../types'; 4 | import { CliError } from '../../errors'; 5 | 6 | function detectIacFormat (): IacFormat { 7 | const files = readdirSync(resolvePath('./')); 8 | const cdkJson = 'cdk.json'; 9 | const isCdkProject = files.includes(cdkJson); 10 | 11 | const tfFileExtension = '.tf'; 12 | const isTfProject = files.some(fileName => fileName.endsWith(tfFileExtension)); 13 | 14 | if (isCdkProject && isTfProject) { 15 | throw new CliError( 16 | 'Cannot determine IaC format!', 17 | 'Both AWS cdk and terraform files exist in this repository.', 18 | 'You can specify which format to use via the "--format" flag' 19 | ); 20 | } 21 | 22 | if (isCdkProject) return IacFormat.awsCdk; 23 | if (isTfProject) return IacFormat.tf; 24 | 25 | throw new CliError( 26 | 'Cannot determine IaC format!', 27 | 'Neither AWS cdk nor terraform files exist in this repository.', 28 | 'Are you running this command in the correct directory?', 29 | 'You can specify which format to use via the "--format" flag' 30 | ); 31 | } 32 | 33 | export { 34 | detectIacFormat 35 | }; -------------------------------------------------------------------------------- /src/commands/check/get-config.ts: -------------------------------------------------------------------------------- 1 | import logger from '../../logger'; 2 | import isNil from 'lodash.isnil'; 3 | import { resolve as resolvePath } from 'path'; 4 | import { readFileSync } from 'fs'; 5 | import { CheckOptions } from '../../types'; 6 | 7 | function tryReadFile (filePath: string): string | undefined { 8 | try { 9 | const fileContents = readFileSync(resolvePath(filePath)); 10 | return fileContents.toString(); 11 | } catch (error) { 12 | return undefined; 13 | } 14 | } 15 | 16 | function tryParseConfig (configString: string, fileName: string) { 17 | try { 18 | const configJson = JSON.parse(configString); 19 | return configJson; 20 | } catch (error) { 21 | logger.error(`Invalid config file! The contents of ${fileName} could not be parsed as JSON. Correct any syntax issues and try again.`); 22 | return {}; 23 | } 24 | } 25 | 26 | function getConfig (options: CheckOptions): CheckOptions { 27 | const { 28 | configFile = 'precloud.config.json' 29 | } = options; 30 | const config = tryParseConfig(tryReadFile(configFile) || '{}', configFile); 31 | 32 | const verbose = !isNil(options.verbose) ? options.verbose : config.verbose; 33 | if (!isNil(verbose)) { 34 | process.env.VERBOSE = verbose.toString(); 35 | } 36 | 37 | return { 38 | ...config, 39 | ...options 40 | }; 41 | } 42 | 43 | export { 44 | getConfig 45 | }; -------------------------------------------------------------------------------- /src/commands/check/index.ts: -------------------------------------------------------------------------------- 1 | import logger from '../../logger'; 2 | import { CheckOptions } from '../../types'; 3 | import { detectIacFormat } from './detect-iac-format'; 4 | import { getConfig } from './get-config'; 5 | import { prepareForCheck } from './prepare'; 6 | import { checkTemplates, testResource } from './checks'; 7 | 8 | async function check (options: CheckOptions) { 9 | const config = getConfig(options); 10 | let { format } = config; 11 | if (!format) { 12 | format = detectIacFormat(); 13 | logger.info(`No IaC format specified. Using detected format: ${format}`); 14 | config.format = format; 15 | } 16 | 17 | const resourceDiffRecords = await prepareForCheck(config); 18 | const errors: Error[] = []; 19 | await checkTemplates(resourceDiffRecords, config) 20 | .catch(error => errors.push(error)); 21 | for (const resource of resourceDiffRecords) { 22 | await testResource(resource, resourceDiffRecords, config) 23 | .catch(error => errors.push(error)); 24 | } 25 | if (errors.length === 0 ) { 26 | logger.success('PreCloud Check passed!'); 27 | return; 28 | } 29 | errors.forEach(logger.cliError, logger); 30 | } 31 | 32 | export { 33 | check 34 | }; -------------------------------------------------------------------------------- /src/commands/check/parser/aws-cdk/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { resolve as resolvePath } from 'path'; 3 | import { 4 | CDK_DIFF_CREATE_SYMBOL, 5 | CDK_DIFF_DELETE_SYMBOL, 6 | CDK_DIFF_UPDATE_SYMBOL, 7 | TINYSTACKS_AWS_CDK_PARSER 8 | } from '../../../../constants'; 9 | import { 10 | CdkDiff, 11 | ChangeType, 12 | IacFormat, 13 | Json, 14 | ResourceDiffRecord, 15 | DiffSection, 16 | CheckOptions 17 | } from '../../../../types'; 18 | import { AwsCdkParser } from '../../../../exported-types'; 19 | import logger from '../../../../logger'; 20 | import { dontReturnEmpty } from '../../../../utils'; 21 | 22 | function partitionDiff (diff: string[], diffHeaders: string[]): DiffSection[] { 23 | const headerIndices: { [key: string]: number } = diffHeaders.reduce<{ [key: string]: number }>((acc, header) => { 24 | const headerIndex = diff.findIndex(line => line.trim() === header); 25 | acc[header] = headerIndex; 26 | return acc; 27 | }, {}); 28 | 29 | const allHeaderIndices: number[] = Object.values(headerIndices).sort(); 30 | return Object.entries(headerIndices).reduce((acc, [ header, headerIndex ]) => { 31 | const sectionName = header.trim().replace('Stack ', ''); 32 | const nextStackHeaderIndex = allHeaderIndices[allHeaderIndices.indexOf(headerIndex) + 1]; 33 | 34 | const stackDiffLines: string[] = diff.slice(headerIndex + 1, nextStackHeaderIndex); 35 | 36 | acc.push({ 37 | sectionName, 38 | diffLines: stackDiffLines 39 | }); 40 | return acc; 41 | }, []); 42 | } 43 | 44 | function separateStacks (diff: string[]): DiffSection[] { 45 | const stackHeaders = diff.filter(line => line.trim().startsWith('Stack ')); 46 | return partitionDiff(diff, stackHeaders); 47 | } 48 | 49 | function getChangeTypeForCdkDiff (changeTypeSymbol: string): ChangeType { 50 | switch (changeTypeSymbol) { 51 | case CDK_DIFF_CREATE_SYMBOL: 52 | return ChangeType.CREATE; 53 | case CDK_DIFF_UPDATE_SYMBOL: 54 | return ChangeType.UPDATE; 55 | case CDK_DIFF_DELETE_SYMBOL: 56 | return ChangeType.DELETE; 57 | default: 58 | return ChangeType.UNKNOWN; 59 | } 60 | } 61 | 62 | function parseDiffLine (diff: string): CdkDiff { 63 | const [ changeTypeSymbol, resourceType, cdkPath, logicalId ] = diff.trim().replace(/\t/g, '').split(' ').filter(elem => elem.trim().length !== 0); 64 | return { 65 | changeTypeSymbol, 66 | resourceType: resourceType?.indexOf('::') > 0 ? resourceType : undefined, 67 | cdkPath, 68 | logicalId 69 | }; 70 | } 71 | 72 | function resolveTemplateName (stackName: string): string { 73 | const manifest: Json = JSON.parse(readFileSync(resolvePath('./cdk.out/manifest.json')).toString() || '{}'); 74 | const { artifacts = {} } = manifest; 75 | const templateArtifact: Json = Object.values(artifacts).find((artifact: Json) => artifact.displayName === stackName); 76 | return templateArtifact?.properties?.templateFile; 77 | } 78 | 79 | const parsers: { 80 | [parserName: string]: AwsCdkParser 81 | } = {}; 82 | 83 | async function tryToUseParser (diff: CdkDiff, cloudformationTemplate: Json, parserName: string): Promise { 84 | try { 85 | let parserInstance = parsers[parserName]; 86 | if (!parserInstance) { 87 | const modulePath = parserName === TINYSTACKS_AWS_CDK_PARSER ? 88 | parserName : 89 | require.resolve(parserName, { paths: [process.cwd()] }); 90 | // eslint-disable-next-line @typescript-eslint/no-var-requires 91 | const parser = require(modulePath); 92 | const mainExport = parser?.default ? parser.default : parser; 93 | if (mainExport) { 94 | parserInstance = new mainExport(); 95 | const isInstance = parserInstance instanceof AwsCdkParser; 96 | const hasParseResource = parserInstance.parseResource && typeof parserInstance.parseResource === 'function'; 97 | if (isInstance || hasParseResource) { 98 | parsers[parserName] = parserInstance; 99 | } else { 100 | logger.warn(`Invalid parser: ${parserName}.`); 101 | logger.warn(`The main export from ${parserName} does not properly implement AwsCdkParser.`); 102 | logger.verbose(parser); 103 | logger.verbose(mainExport); 104 | } 105 | } 106 | } 107 | if (parserInstance) { 108 | const parsedResource = await parserInstance.parseResource(diff, cloudformationTemplate); 109 | return dontReturnEmpty(parsedResource); 110 | } 111 | return undefined; 112 | } 113 | catch (error) { 114 | logger.warn(`Invalid parser: ${parserName}.`); 115 | logger.warn(`The main export from ${parserName} could not be instantiated or it threw an error while parsing the resource.`); 116 | logger.verbose(error); 117 | return undefined; 118 | } 119 | } 120 | 121 | async function parseCdkResource (diff: CdkDiff, cloudformationTemplate: Json, config: CheckOptions): Promise { 122 | const { 123 | awsCdkParsers = [] 124 | } = config; 125 | if (!awsCdkParsers.includes(TINYSTACKS_AWS_CDK_PARSER)) awsCdkParsers.push(TINYSTACKS_AWS_CDK_PARSER); 126 | let properties = {}; 127 | for (const parser of awsCdkParsers) { 128 | const response = await tryToUseParser(diff, cloudformationTemplate, parser); 129 | if (response) { 130 | properties = response; 131 | break; 132 | } 133 | } 134 | return properties; 135 | } 136 | 137 | async function composeCdkResourceDiffRecords (stackName: string, diffs: string[] = [], config: CheckOptions = {}): Promise { 138 | const templateName = resolveTemplateName(stackName); 139 | const templateJson: Json = JSON.parse(readFileSync(resolvePath(`./cdk.out/${templateName}`)).toString() || '{}'); 140 | const resources: ResourceDiffRecord[] = []; 141 | for (const diff of diffs) { 142 | const cdkDiff: CdkDiff = parseDiffLine(diff); 143 | const { 144 | changeTypeSymbol, 145 | resourceType, 146 | cdkPath, 147 | logicalId 148 | } = cdkDiff; 149 | const changeType = getChangeTypeForCdkDiff(changeTypeSymbol); 150 | if (changeType === ChangeType.UNKNOWN || !resourceType || !cdkPath || !logicalId) continue; 151 | const [ _logicalId, cfnEntry = {} ] = Object.entries(templateJson.Resources).find(([key]) => key === logicalId) || []; 152 | const resourceDiffRecord: ResourceDiffRecord = { 153 | stackName, 154 | format: IacFormat.awsCdk, 155 | changeType, 156 | resourceType: cfnEntry.Type || resourceType, 157 | address: cdkPath, 158 | logicalId, 159 | properties: await parseCdkResource(cdkDiff, templateJson, config) 160 | }; 161 | resources.push(resourceDiffRecord); 162 | } 163 | return resources; 164 | } 165 | 166 | async function parseStackDiff (stackDiffLines: DiffSection, config: CheckOptions): Promise { 167 | const { 168 | sectionName, 169 | diffLines 170 | } = stackDiffLines; 171 | const diffHeaders = ['IAM Statement Changes', 'IAM Policy Changes', 'Parameters', 'Resources', 'Outputs', 'Other Changes']; 172 | // Control for when stackName is set in cdk.StackProps 173 | // If this is set, and differs from the Stack's construct id, 174 | // the section header is "Stack StackConstructId (StackName)" instead of "Stack StackName" 175 | const stackName = sectionName.includes(' ') ? sectionName.split(' ').at(0) : sectionName; 176 | 177 | const diffSections = partitionDiff(diffLines, diffHeaders); 178 | const resourceDiffs = diffSections.find(diffSection => diffSection.sectionName === 'Resources'); 179 | return composeCdkResourceDiffRecords(stackName, resourceDiffs?.diffLines, config); 180 | } 181 | 182 | async function parseCdkDiff (diffTxt: string, config: CheckOptions): Promise { 183 | const diff = diffTxt.split('\n').filter(line => line.trim().length !== 0); 184 | const stackDiffLines = separateStacks(diff); 185 | const diffRecords: ResourceDiffRecord[] = []; 186 | for (const stackDiff of stackDiffLines) { 187 | const stackResources = await parseStackDiff(stackDiff, config); 188 | diffRecords.push(...stackResources); 189 | } 190 | return diffRecords; 191 | } 192 | 193 | export { 194 | parseCdkDiff 195 | }; -------------------------------------------------------------------------------- /src/commands/check/parser/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aws-cdk'; 2 | export * from './terraform'; -------------------------------------------------------------------------------- /src/commands/check/parser/terraform/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { 3 | TF_DIFF_CREATE_ACTION, 4 | TF_DIFF_DELETE_ACTION, 5 | TF_DIFF_NO_OP_ACTION, 6 | TF_DIFF_UPDATE_ACTION, 7 | TINYSTACKS_TF_MODULE_PARSER, 8 | TINYSTACKS_TF_RESOURCE_PARSER 9 | } from '../../../../constants'; 10 | import { 11 | ChangeType, 12 | IacFormat, 13 | Json, 14 | ResourceDiffRecord, 15 | CheckOptions, 16 | TfDiff 17 | } from '../../../../types'; 18 | import logger from '../../../../logger'; 19 | import TerraformParser from '../../../../abstracts/terraform-parser'; 20 | import { dontReturnEmpty } from '../../../../utils'; 21 | 22 | function getChangeTypeForTerraformDiff (tfChangeType: string): ChangeType { 23 | switch (tfChangeType) { 24 | case TF_DIFF_CREATE_ACTION: 25 | return ChangeType.CREATE; 26 | case TF_DIFF_UPDATE_ACTION: 27 | return ChangeType.UPDATE; 28 | case TF_DIFF_DELETE_ACTION: 29 | return ChangeType.DELETE; 30 | case TF_DIFF_NO_OP_ACTION: 31 | return ChangeType.NO_CHANGES; 32 | default: 33 | return ChangeType.UNKNOWN; 34 | } 35 | } 36 | 37 | const parsers: { 38 | [parserName: string]: TerraformParser 39 | } = {}; 40 | 41 | async function tryToUseParser (diff: TfDiff, tfPlan: Json, parserName: string): Promise { 42 | try { 43 | let parserInstance = parsers[parserName]; 44 | if (!parserInstance) { 45 | const defaultParsers = [TINYSTACKS_TF_RESOURCE_PARSER, TINYSTACKS_TF_MODULE_PARSER]; 46 | const modulePath = defaultParsers.includes(parserName) ? 47 | parserName : 48 | require.resolve(parserName, { paths: [process.cwd()] }); 49 | // eslint-disable-next-line @typescript-eslint/no-var-requires 50 | const parser = require(modulePath); 51 | const mainExport = parser?.default ? parser.default : parser; 52 | if (mainExport) { 53 | parserInstance = new mainExport(); 54 | const isInstance = parserInstance instanceof TerraformParser; 55 | const hasParseResource = parserInstance.parseResource && typeof parserInstance.parseResource === 'function'; 56 | if (isInstance || hasParseResource) { 57 | parsers[parserName] = parserInstance; 58 | } else { 59 | logger.warn(`Invalid parser: ${parserName}.`); 60 | logger.warn(`The main export from ${parserName} does not properly implement TerraformParser.`); 61 | } 62 | } 63 | } 64 | if (parserInstance) { 65 | const parsedResource = await parserInstance.parseResource(diff, tfPlan); 66 | return dontReturnEmpty(parsedResource); 67 | } 68 | return undefined; 69 | } 70 | catch (error) { 71 | logger.warn(`Invalid parser: ${parserName}.`); 72 | logger.warn(`The main export from ${parserName} could not be instantiated or it threw an error while parsing the resource.`); 73 | logger.verbose(error); 74 | return undefined; 75 | } 76 | } 77 | 78 | async function parseTfResource (diff: TfDiff, tfPlan: Json, config: CheckOptions): Promise { 79 | const { 80 | terraformParsers = [] 81 | } = config; 82 | if (!terraformParsers.includes(TINYSTACKS_TF_RESOURCE_PARSER)) terraformParsers.push(TINYSTACKS_TF_RESOURCE_PARSER); 83 | if (!terraformParsers.includes(TINYSTACKS_TF_MODULE_PARSER)) terraformParsers.push(TINYSTACKS_TF_MODULE_PARSER); 84 | let properties; 85 | for (const parser of terraformParsers) { 86 | const response = await tryToUseParser(diff, tfPlan, parser); 87 | if (response) { 88 | properties = response; 89 | break; 90 | } 91 | } 92 | const { address, logicalId, resourceType } = diff; 93 | if (!properties) logger.warn(`None of the configured parsers could parse resource ${address || `${resourceType}.${logicalId}`}`); 94 | return properties; 95 | } 96 | 97 | async function parseTerraformDiff (planFile: string, config: CheckOptions): Promise { 98 | const planJson: Json = JSON.parse(readFileSync(planFile)?.toString() || '{}'); 99 | const { 100 | resource_changes = [] as Json[] 101 | } = planJson; 102 | 103 | const resources: ResourceDiffRecord[] = []; 104 | for (const resourceChange of resource_changes) { 105 | const { 106 | address, 107 | index, 108 | name: logicalId, 109 | change: { 110 | // before = {}, 111 | // after = {}, 112 | actions: [ 113 | beforeAction, 114 | afterAction 115 | ] = [] 116 | } = {}, 117 | type, 118 | provider_name: providerName 119 | } = resourceChange || {}; 120 | 121 | if (afterAction) { 122 | const beforeDiff: TfDiff = { 123 | address, 124 | logicalId, 125 | action: beforeAction, 126 | index, 127 | resourceType: type 128 | }; 129 | resources.push({ 130 | format: IacFormat.tf, 131 | resourceType: type, 132 | changeType: getChangeTypeForTerraformDiff(beforeAction), 133 | address, 134 | index, 135 | logicalId, 136 | providerName, 137 | properties: await parseTfResource(beforeDiff, planJson, config) 138 | }); 139 | } 140 | const changeType = getChangeTypeForTerraformDiff(afterAction || beforeAction); 141 | if (changeType !== ChangeType.NO_CHANGES) { 142 | const afterDiff: TfDiff = { 143 | address, 144 | logicalId, 145 | action: afterAction, 146 | index, 147 | resourceType: type 148 | }; 149 | resources.push({ 150 | format: IacFormat.tf, 151 | resourceType: type, 152 | changeType, 153 | address, 154 | index, 155 | logicalId, 156 | providerName, 157 | properties: await parseTfResource(afterDiff, planJson, config) 158 | }); 159 | } 160 | } 161 | return resources; 162 | } 163 | 164 | export { 165 | parseTerraformDiff 166 | }; -------------------------------------------------------------------------------- /src/commands/check/prepare.ts: -------------------------------------------------------------------------------- 1 | import { 2 | existsSync, 3 | mkdirSync, 4 | writeFileSync 5 | } from 'fs'; 6 | import { TMP_DIRECTORY } from '../../constants'; 7 | import { CliError } from '../../errors'; 8 | import { 9 | IacFormat, 10 | OsOutput, 11 | ResourceDiffRecord, 12 | CheckOptions 13 | } from '../../types'; 14 | import { runCommand } from '../../utils'; 15 | import { 16 | parseCdkDiff, 17 | parseTerraformDiff 18 | } from './parser'; 19 | 20 | function createTmpDirectory () { 21 | if (!existsSync(TMP_DIRECTORY)){ 22 | mkdirSync(TMP_DIRECTORY, { recursive: true }); 23 | } 24 | } 25 | 26 | function handleNonZeroExitCode (output: OsOutput, process: string) { 27 | if (output?.exitCode !== 0) { 28 | throw new CliError(`${process} failed with exit code ${output?.exitCode}`); 29 | } 30 | } 31 | 32 | async function prepareCdk (config: CheckOptions): Promise { 33 | const output: OsOutput = await runCommand('cdk diff'); 34 | handleNonZeroExitCode(output, 'cdk diff'); 35 | const diffFileName = `${TMP_DIRECTORY}/diff.txt`; 36 | writeFileSync(diffFileName, output.stderr); 37 | const parsedDiff = await parseCdkDiff(output.stderr, config); 38 | writeFileSync(`${TMP_DIRECTORY}/aws-cdk-diff.json`, JSON.stringify(parsedDiff, null, 2)); 39 | return parsedDiff; 40 | } 41 | 42 | async function prepareTf (config: CheckOptions): Promise { 43 | const initOutput: OsOutput = await runCommand('terraform init'); 44 | handleNonZeroExitCode(initOutput, 'terraform init'); 45 | const planOutput: OsOutput = await runCommand(`terraform plan -out=${TMP_DIRECTORY}/tfplan`); 46 | handleNonZeroExitCode(planOutput, 'terraform plan'); 47 | const planFileName = `${TMP_DIRECTORY}/plan.json`; 48 | const showOutput: OsOutput = await runCommand(`terraform show -no-color -json ${TMP_DIRECTORY}/tfplan > ${planFileName}`); 49 | handleNonZeroExitCode(showOutput, 'terraform show'); 50 | const parsedDiff = await parseTerraformDiff(planFileName, config); 51 | writeFileSync(`${TMP_DIRECTORY}/tf-diff.json`, JSON.stringify(parsedDiff, null, 2)); 52 | return parsedDiff; 53 | 54 | } 55 | 56 | async function prepareForCheck (config: CheckOptions): Promise { 57 | createTmpDirectory(); 58 | const { format } = config; 59 | switch (format) { 60 | case IacFormat.awsCdk: 61 | return prepareCdk(config); 62 | case IacFormat.tf: 63 | return prepareTf(config); 64 | default: 65 | return []; 66 | } 67 | } 68 | 69 | export { 70 | prepareForCheck 71 | }; -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './check'; 2 | export * from './init'; -------------------------------------------------------------------------------- /src/commands/init/index.ts: -------------------------------------------------------------------------------- 1 | import logger from '../../logger'; 2 | import { writeFileSync, existsSync } from 'fs'; 3 | 4 | async function init () { 5 | const configData = ` 6 | { 7 | "awsCdkParsers": [ 8 | "@tinystacks/aws-cdk-parser" 9 | ], 10 | "terraformParsers": [ 11 | "@tinystacks/terraform-resource-parser", 12 | "@tinystacks/terraform-module-parser" 13 | ], 14 | "templateChecks": [ 15 | "@tinystacks/aws-template-checks" 16 | ], 17 | "resourceChecks": [ 18 | "@tinystacks/aws-resource-checks" 19 | ] 20 | } 21 | `; 22 | const dirname = './precloud.config.json'; 23 | if (existsSync(dirname)){ 24 | logger.info('Configuration file already exists, not creating a default one'); 25 | return; 26 | } 27 | try { 28 | writeFileSync(dirname, configData); 29 | logger.success('Configuration file successfully created!'); 30 | } catch(e) { 31 | logger.error(`Error creating configuration file: ${e}`); 32 | } 33 | } 34 | 35 | export { 36 | init 37 | }; -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | const TMP_DIRECTORY = '/tmp/precloud/tmp'; 2 | 3 | const CDK_DIFF_CREATE_SYMBOL = '[+]'; 4 | const CDK_DIFF_UPDATE_SYMBOL = '[~]'; 5 | const CDK_DIFF_DELETE_SYMBOL = '[-]'; 6 | 7 | const TF_DIFF_CREATE_ACTION = 'create'; 8 | const TF_DIFF_UPDATE_ACTION = 'update'; 9 | const TF_DIFF_DELETE_ACTION = 'delete'; 10 | const TF_DIFF_NO_OP_ACTION = 'no-op'; 11 | 12 | const AWS_TF_PROVIDER_NAME = 'registry.terraform.io/hashicorp/aws'; 13 | 14 | const TINYSTACKS_AWS_CDK_PARSER = '@tinystacks/aws-cdk-parser'; 15 | const TINYSTACKS_TF_RESOURCE_PARSER = '@tinystacks/terraform-resource-parser'; 16 | const TINYSTACKS_TF_MODULE_PARSER = '@tinystacks/terraform-module-parser'; 17 | const TINYSTACKS_AWS_RESOURCE_CHECKS = '@tinystacks/aws-resource-checks'; 18 | const TINYSTACKS_AWS_TEMPLATE_CHECKS = '@tinystacks/aws-template-checks'; 19 | 20 | export { 21 | TMP_DIRECTORY, 22 | CDK_DIFF_CREATE_SYMBOL, 23 | CDK_DIFF_UPDATE_SYMBOL, 24 | CDK_DIFF_DELETE_SYMBOL, 25 | TF_DIFF_CREATE_ACTION, 26 | TF_DIFF_UPDATE_ACTION, 27 | TF_DIFF_DELETE_ACTION, 28 | TF_DIFF_NO_OP_ACTION, 29 | AWS_TF_PROVIDER_NAME, 30 | TINYSTACKS_AWS_CDK_PARSER, 31 | TINYSTACKS_TF_RESOURCE_PARSER, 32 | TINYSTACKS_TF_MODULE_PARSER, 33 | TINYSTACKS_AWS_RESOURCE_CHECKS, 34 | TINYSTACKS_AWS_TEMPLATE_CHECKS 35 | }; -------------------------------------------------------------------------------- /src/errors/cli-error.ts: -------------------------------------------------------------------------------- 1 | class CliError extends Error { 2 | name = 'CliError'; 3 | reason: string; 4 | hints: string[]; 5 | constructor (message: string, reason?: string, ...hints: string[]) { 6 | super(message); 7 | this.name = CliError.name; 8 | this.message = message; 9 | this.reason = reason; 10 | this.hints = hints || []; 11 | } 12 | } 13 | 14 | export { 15 | CliError 16 | }; -------------------------------------------------------------------------------- /src/errors/conflict-error.ts: -------------------------------------------------------------------------------- 1 | import { CliError } from './cli-error'; 2 | 3 | const CONFLICT_MESSAGE = 'Conflict!'; 4 | 5 | class ConflictError extends CliError { 6 | constructor (reason: string, ...hints: string[]) { 7 | super(CONFLICT_MESSAGE, reason, ...hints); 8 | } 9 | } 10 | 11 | export { 12 | ConflictError 13 | }; -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './conflict-error'; 2 | export * from './cli-error'; 3 | export * from './quota-error'; -------------------------------------------------------------------------------- /src/errors/quota-error.ts: -------------------------------------------------------------------------------- 1 | import { CliError } from './cli-error'; 2 | 3 | const QUOTA_EXCEEDED_MESSAGE = 'Quota Limit Reached!'; 4 | 5 | class QuotaError extends CliError { 6 | constructor (reason: string, ...hints: string[]) { 7 | super(QUOTA_EXCEEDED_MESSAGE, reason, ...hints); 8 | } 9 | } 10 | 11 | export { 12 | QuotaError 13 | }; -------------------------------------------------------------------------------- /src/exported-types.ts: -------------------------------------------------------------------------------- 1 | export * from './abstracts'; 2 | export * from './commands/check/checks/aws/resources'; 3 | export * from './types'; 4 | export * from './errors'; 5 | export { 6 | CDK_DIFF_CREATE_SYMBOL, 7 | CDK_DIFF_UPDATE_SYMBOL, 8 | CDK_DIFF_DELETE_SYMBOL, 9 | TF_DIFF_CREATE_ACTION, 10 | TF_DIFF_UPDATE_ACTION, 11 | TF_DIFF_DELETE_ACTION, 12 | TF_DIFF_NO_OP_ACTION, 13 | AWS_TF_PROVIDER_NAME 14 | } from './constants'; 15 | import logger from './logger'; 16 | export { logger }; -------------------------------------------------------------------------------- /src/hooks/cleanup-tmp-directory/index.ts: -------------------------------------------------------------------------------- 1 | import { rmSync } from 'fs'; 2 | import { TMP_DIRECTORY } from '../../constants'; 3 | 4 | function cleanupTmpDirectory () { 5 | rmSync(TMP_DIRECTORY, { recursive: true, force: true }); 6 | } 7 | 8 | export { 9 | cleanupTmpDirectory 10 | }; -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cleanup-tmp-directory'; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Command, Option } from 'commander'; 4 | import * as colors from 'colors'; 5 | import { check, init } from './commands'; 6 | import logger from './logger'; 7 | import { cleanupTmpDirectory } from './hooks'; 8 | const program = new Command(); 9 | // eslint-disable-next-line 10 | const { version } = require('../package.json'); 11 | 12 | function handleError (error: Error) { 13 | logger.cliError(error); 14 | process.exit(1); 15 | } 16 | 17 | try { 18 | colors.enable(); 19 | 20 | program 21 | .name('precloud') 22 | .description('TinyStacks precloud command line interface') 23 | .version(version); 24 | 25 | program.command('check') 26 | .description('Performs a check on an AWS cdk app or a Terraform configuration to validate the planned resources can be launched or updated.') 27 | .addOption(new Option('-f, --format ', 'Specifies the iac format. Can also be set via "format" in the config file.').choices(['tf', 'aws-cdk'])) 28 | .option('-c, --config-file ', 'Specifies a config file. Options specified via the command line will always take precedence over options specified in a config file. Looks for precloud.config.json by default.') 29 | .option('-v, --verbose', 'Log additional details when available (plugin errors, etc.)') 30 | .action(check) 31 | .hook('postAction', cleanupTmpDirectory); 32 | 33 | program.command('init') 34 | .description('Creates a configuration file from the example shown in the README') 35 | .action(init); 36 | 37 | program.parseAsync() 38 | .catch(handleError); 39 | } catch (error) { 40 | handleError(error as Error); 41 | } -------------------------------------------------------------------------------- /src/logger/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | red, 3 | magenta, 4 | yellow, 5 | blue, 6 | gray, 7 | green 8 | } from 'colors'; 9 | import { CliError } from '../errors'; 10 | 11 | const logger = { 12 | error (message: string) { 13 | console.error(red(`Error: ${message}`)); 14 | }, 15 | 16 | debug (message: string | any) { 17 | console.debug(yellow(`Debug: ${message}`)); 18 | }, 19 | 20 | warn (message: string) { 21 | console.warn(yellow(`Warning: ${message}`)); 22 | }, 23 | 24 | info (message: string) { 25 | console.info(blue(`Info: ${message}`)); 26 | }, 27 | 28 | log (message: string) { 29 | console.log(gray(message)); 30 | }, 31 | 32 | hint (message: string) { 33 | console.log(magenta(`Hint: ${message}`)); 34 | }, 35 | 36 | success (message: string) { 37 | console.log(green(`Success: ${message}`)); 38 | }, 39 | 40 | verbose (message: string | Error | any) { 41 | if (process.env.VERBOSE === 'true') { 42 | console.log(gray(message)); 43 | } 44 | }, 45 | 46 | cliError (e: Error | unknown) { 47 | const error = e as Error; 48 | if (error.name === CliError.name) { 49 | const customError = e as CliError; 50 | this.error(`${customError.message}${customError.reason ? `\n\t${customError.reason}` : ''}`); 51 | if (customError.hints) { 52 | customError.hints.forEach(hintString => this.hint(`\t${hintString}`)); 53 | } 54 | } else { 55 | this.error('An unexpected error occurred!'); 56 | console.error(error); 57 | } 58 | } 59 | }; 60 | 61 | 62 | export default logger; -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-shadow 2 | enum IacFormat { 3 | tf = 'tf', 4 | awsCdk = 'aws-cdk' 5 | } 6 | 7 | // eslint-disable-next-line no-shadow 8 | enum ChangeType { 9 | CREATE = 'CREATE', 10 | UPDATE = 'UPDATE', 11 | DELETE = 'DELETE', 12 | NO_CHANGES = 'NO_CHANGES', 13 | UNKNOWN = 'UNKNOWN' 14 | } 15 | 16 | interface OsOutput { 17 | stdout: string; 18 | stderr: string; 19 | exitCode: number; 20 | } 21 | 22 | interface CheckOptions { 23 | format?: IacFormat; 24 | requirePrivateSubnet?: boolean; 25 | configFile?: string; 26 | awsCdkParsers?: string[]; 27 | terraformParsers?: string[]; 28 | resourceChecks?: string[]; 29 | templateChecks?: string[]; 30 | verbose?: boolean; 31 | } 32 | 33 | interface Json { 34 | [key: string]: any 35 | } 36 | 37 | interface ResourceDiffRecord { 38 | stackName?: string; 39 | format: IacFormat; 40 | changeType: ChangeType; 41 | resourceType: string; 42 | logicalId: string; 43 | address: string; 44 | index?: string; 45 | providerName?: string; 46 | properties: Json; 47 | } 48 | 49 | interface CdkDiff { 50 | changeTypeSymbol?: string; 51 | resourceType?: string; 52 | cdkPath: string; 53 | logicalId: string; 54 | } 55 | 56 | interface TfDiff { 57 | action?: string; 58 | resourceType?: string; 59 | address: string; 60 | index?: string; 61 | logicalId: string; 62 | } 63 | 64 | interface DiffSection { 65 | sectionName: string, 66 | diffLines: string[] 67 | } 68 | 69 | interface TxtFile { 70 | name: string; 71 | contents: string; 72 | } 73 | interface JsonFile { 74 | name: string; 75 | contents: Json; 76 | } 77 | 78 | export { 79 | IacFormat, 80 | OsOutput, 81 | CheckOptions, 82 | ChangeType, 83 | Json, 84 | ResourceDiffRecord, 85 | CdkDiff, 86 | TfDiff, 87 | DiffSection, 88 | TxtFile, 89 | JsonFile 90 | }; -------------------------------------------------------------------------------- /src/utils/dont-return-empty.ts: -------------------------------------------------------------------------------- 1 | import isNil from 'lodash.isnil'; 2 | import isPlainObject from 'lodash.isplainobject'; 3 | import { Json } from '../types'; 4 | 5 | export function dontReturnEmpty (properties: Json): Json | undefined { 6 | if (isPlainObject(properties)) { 7 | const values = Object.values(properties); 8 | const objectIsEmpty = values.every((value) => { 9 | if(isPlainObject(value)) { 10 | return isNil(dontReturnEmpty(value)); 11 | } else if (Array.isArray(value)) { 12 | return value.map(dontReturnEmpty).every(isNil); 13 | } 14 | return isNil(value); 15 | }); 16 | return objectIsEmpty ? undefined : properties; 17 | } 18 | return undefined; 19 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './os'; 2 | export * from './dont-return-empty'; -------------------------------------------------------------------------------- /src/utils/os/index.ts: -------------------------------------------------------------------------------- 1 | import logger from '../../logger'; 2 | import { exec, ExecOptions } from 'child_process'; 3 | import { OsOutput } from '../../types'; 4 | 5 | async function runCommand (command: string, opts?: ExecOptions): Promise { 6 | return new Promise((resolve, reject) => { 7 | try { 8 | // we "return await" here so that errors can be handled within this function to execute retry logic 9 | if (opts) { 10 | opts.env = { ...process.env, ...(opts.env || {}) }; 11 | } 12 | const standardOut: string[] = []; 13 | const standardError: string[] = []; 14 | let exitCode: number; 15 | 16 | logger.log(command); 17 | const childProcess = exec(command, opts); 18 | 19 | childProcess.stdout.on('data', (data) => { 20 | console.log(data); 21 | standardOut.push(data); 22 | }); 23 | 24 | childProcess.stderr.on('data', (data) => { 25 | console.error(data); 26 | standardError.push(data); 27 | }); 28 | 29 | process.stdin.pipe(childProcess.stdin); 30 | 31 | childProcess.on('error', (error: Error) => { 32 | logger.error(`Failed to execute command "${command}"`); 33 | reject(error); 34 | }); 35 | 36 | childProcess.on('exit', (code: number, signal: string) => { 37 | if (signal) logger.info(`Exited due to signal: ${signal}`); 38 | exitCode = code; 39 | resolve({ 40 | stdout: standardOut.join('\n'), 41 | stderr: standardError.join('\n'), 42 | exitCode 43 | }); 44 | }); 45 | } catch (error) { 46 | logger.error(`Failed to execute command "${command}"`); 47 | reject(error); 48 | } 49 | }); 50 | } 51 | 52 | export { 53 | runCommand 54 | }; -------------------------------------------------------------------------------- /test/commands/check/checks/aws/MockResourceChecks.ts: -------------------------------------------------------------------------------- 1 | import { ResourceChecks } from '../../../../../src/abstracts'; 2 | 3 | const mockCheckResource = jest.fn(); 4 | class MockResourceChecks extends ResourceChecks { 5 | checkResource = mockCheckResource; 6 | } 7 | 8 | export { 9 | mockCheckResource, 10 | MockResourceChecks 11 | }; 12 | export default MockResourceChecks; -------------------------------------------------------------------------------- /test/commands/check/checks/aws/MockTemplateChecks.ts: -------------------------------------------------------------------------------- 1 | import { TemplateChecks } from '../../../../../src/abstracts'; 2 | 3 | const mockCheckTemplate = jest.fn(); 4 | class MockTemplateChecks extends TemplateChecks { 5 | checkTemplate = mockCheckTemplate; 6 | } 7 | 8 | export { 9 | mockCheckTemplate, 10 | MockTemplateChecks 11 | }; 12 | export default MockTemplateChecks; -------------------------------------------------------------------------------- /test/commands/check/checks/aws/index.test.ts: -------------------------------------------------------------------------------- 1 | import MockResourceChecks, { mockCheckResource } from './MockResourceChecks'; 2 | import MockTemplateChecks, { mockCheckTemplate } from './MockTemplateChecks'; 3 | 4 | jest.mock('@tinystacks/aws-resource-checks', () => MockResourceChecks); 5 | jest.mock('@tinystacks/aws-template-checks', () => MockTemplateChecks); 6 | 7 | import { 8 | testResource, 9 | checkTemplates 10 | } from '../../../../../src/commands/check/checks/aws'; 11 | import { ResourceDiffRecord } from '../../../../../src/types'; 12 | 13 | describe('aws schecks', () => { 14 | beforeEach(() => { 15 | process.env.VERBOSE = 'true'; 16 | }); 17 | afterEach(() => { 18 | delete process.env.VERBOSE; 19 | }); 20 | it('testAwsResource', async () => { 21 | const mockResource = { 22 | resourceType: 'AWS::SQS::Queue' 23 | } as ResourceDiffRecord; 24 | 25 | await testResource(mockResource, [mockResource], {}); 26 | 27 | expect(mockCheckResource).toBeCalled(); 28 | }); 29 | 30 | it('checkTemplates', async () => { 31 | const mockResource = { 32 | resourceType: 'AWS::S3::Bucket' 33 | } as ResourceDiffRecord; 34 | 35 | await checkTemplates([mockResource], {}); 36 | 37 | expect(mockCheckTemplate).toBeCalled(); 38 | }); 39 | }); -------------------------------------------------------------------------------- /test/commands/check/detect-iac-format.test.ts: -------------------------------------------------------------------------------- 1 | const mockResolve = jest.fn(); 2 | const mockReadDirSync = jest.fn(); 3 | 4 | jest.mock('path', () => ({ 5 | resolve: mockResolve 6 | })); 7 | 8 | jest.mock('fs', () => ({ 9 | readdirSync: mockReadDirSync 10 | })); 11 | 12 | import { detectIacFormat } from '../../../src/commands/check/detect-iac-format'; 13 | import { IacFormat } from '../../../src/types'; 14 | 15 | describe('detectIacFormat', () => { 16 | afterEach(() => { 17 | // for mocks 18 | jest.resetAllMocks(); 19 | // for spies 20 | jest.restoreAllMocks(); 21 | }); 22 | 23 | it('returns aws-cdk if cdk.json is detected', () => { 24 | mockReadDirSync.mockReturnValue(['cdk.json']); 25 | 26 | const format = detectIacFormat(); 27 | 28 | expect(format).toEqual(IacFormat.awsCdk); 29 | }); 30 | it('returns tf if terraform files are detected', () => { 31 | mockReadDirSync.mockReturnValue(['main.tf']); 32 | 33 | const format = detectIacFormat(); 34 | 35 | expect(format).toEqual(IacFormat.tf); 36 | }); 37 | it('throws if both cdk and terraform files are present', () => { 38 | mockReadDirSync.mockReturnValue(['main.tf', 'cdk.json']); 39 | 40 | let thrownError; 41 | try { 42 | detectIacFormat(); 43 | } catch (error) { 44 | thrownError = error; 45 | } finally { 46 | expect(thrownError).toBeDefined(); 47 | expect(thrownError).toHaveProperty('name', 'CliError'); 48 | expect(thrownError).toHaveProperty('message', 'Cannot determine IaC format!'); 49 | expect(thrownError).toHaveProperty('reason', 'Both AWS cdk and terraform files exist in this repository.'); 50 | expect(thrownError).toHaveProperty('hints', ['You can specify which format to use via the "--format" flag']); 51 | } 52 | }); 53 | it('throws if neither cdk and terraform files are present', () => { 54 | mockReadDirSync.mockReturnValue([]); 55 | 56 | let thrownError; 57 | try { 58 | detectIacFormat(); 59 | } catch (error) { 60 | thrownError = error; 61 | } finally { 62 | expect(thrownError).toBeDefined(); 63 | expect(thrownError).toHaveProperty('name', 'CliError'); 64 | expect(thrownError).toHaveProperty('message', 'Cannot determine IaC format!'); 65 | expect(thrownError).toHaveProperty('reason', 'Neither AWS cdk nor terraform files exist in this repository.'); 66 | expect(thrownError).toHaveProperty('hints', [ 67 | 'Are you running this command in the correct directory?', 68 | 'You can specify which format to use via the "--format" flag' 69 | ]); 70 | } 71 | }); 72 | }); -------------------------------------------------------------------------------- /test/commands/check/get-config.test.ts: -------------------------------------------------------------------------------- 1 | const mockReadFileSync = jest.fn(); 2 | const mockResolve = jest.fn(); 3 | const mockLogError = jest.fn(); 4 | jest.mock('fs', () => { 5 | return { 6 | readFileSync: mockReadFileSync 7 | }; 8 | }); 9 | jest.mock('path', () => { 10 | return { 11 | resolve: mockResolve 12 | }; 13 | }); 14 | jest.mock('../../../src/logger', () => { 15 | return { 16 | error: mockLogError 17 | }; 18 | }); 19 | import * as fs from 'fs'; 20 | import * as path from 'path'; 21 | import { 22 | getConfig 23 | } from '../../../src/commands/check/get-config'; 24 | import { IacFormat } from '../../../src/types'; 25 | 26 | describe('get-config tests', () => { 27 | beforeEach(() => { 28 | mockResolve.mockImplementation(fileName => fileName); 29 | }); 30 | afterEach(() => { 31 | // for mocks 32 | jest.resetAllMocks(); 33 | // for spies 34 | jest.restoreAllMocks(); 35 | }); 36 | 37 | it('defaults config file name if not specified', () => { 38 | getConfig({}); 39 | 40 | expect(mockResolve).toBeCalled(); 41 | expect(mockResolve).toBeCalledWith('precloud.config.json'); 42 | expect(mockReadFileSync).toBeCalled(); 43 | expect(mockReadFileSync).toBeCalledWith('precloud.config.json'); 44 | expect(mockLogError).not.toBeCalled(); 45 | }); 46 | it('defaults to empty object if the file cannot be read', () => { 47 | const config = getConfig({}); 48 | 49 | expect(mockResolve).toBeCalled(); 50 | expect(mockResolve).toBeCalledWith('precloud.config.json'); 51 | expect(mockReadFileSync).toBeCalled(); 52 | expect(mockReadFileSync).toBeCalledWith('precloud.config.json'); 53 | expect(mockLogError).not.toBeCalled(); 54 | 55 | expect(config).toEqual({}); 56 | }); 57 | it('logs error and defaults to empty object if the config file cannot be parsed as JSON', () => { 58 | mockReadFileSync.mockReturnValueOnce('not json'); 59 | 60 | const config = getConfig({}); 61 | 62 | expect(mockResolve).toBeCalled(); 63 | expect(mockResolve).toBeCalledWith('precloud.config.json'); 64 | expect(mockReadFileSync).toBeCalled(); 65 | expect(mockReadFileSync).toBeCalledWith('precloud.config.json'); 66 | 67 | expect(mockLogError).toBeCalled(); 68 | expect(mockLogError).toBeCalledWith('Invalid config file! The contents of precloud.config.json could not be parsed as JSON. Correct any syntax issues and try again.'); 69 | expect(config).toEqual({}); 70 | }); 71 | it('sets verbose if specified via the cli', () => { 72 | delete process.env.VERBOSE; 73 | mockReadFileSync.mockReturnValueOnce('{ "format": "aws-cdk" }'); 74 | 75 | const config = getConfig({ verbose: true }); 76 | 77 | expect(mockResolve).toBeCalled(); 78 | expect(mockResolve).toBeCalledWith('precloud.config.json'); 79 | expect(mockReadFileSync).toBeCalled(); 80 | expect(mockReadFileSync).toBeCalledWith('precloud.config.json'); 81 | expect(mockLogError).not.toBeCalled(); 82 | 83 | expect(config).toEqual({ 84 | format: 'aws-cdk', 85 | verbose: true 86 | }); 87 | expect(process.env.VERBOSE).toEqual('true'); 88 | }); 89 | it('sets verbose if specified via the config file', () => { 90 | delete process.env.VERBOSE; 91 | mockReadFileSync.mockReturnValueOnce('{ "format": "aws-cdk", "verbose": true }'); 92 | 93 | const config = getConfig({ configFile: 'mock.config.json' }); 94 | 95 | expect(mockResolve).toBeCalled(); 96 | expect(mockResolve).toBeCalledWith('mock.config.json'); 97 | expect(mockReadFileSync).toBeCalled(); 98 | expect(mockReadFileSync).toBeCalledWith('mock.config.json'); 99 | expect(mockLogError).not.toBeCalled(); 100 | 101 | expect(config).toEqual({ 102 | format: 'aws-cdk', 103 | verbose: true, 104 | configFile: 'mock.config.json' 105 | }); 106 | expect(process.env.VERBOSE).toEqual('true'); 107 | }); 108 | it('prioritized cli options over config values', () => { 109 | delete process.env.VERBOSE; 110 | mockReadFileSync.mockReturnValueOnce('{ "format": "aws-cdk", "verbose": true }'); 111 | 112 | const config = getConfig({ format: IacFormat.tf, verbose: false }); 113 | 114 | expect(mockResolve).toBeCalled(); 115 | expect(mockResolve).toBeCalledWith('precloud.config.json'); 116 | expect(mockReadFileSync).toBeCalled(); 117 | expect(mockReadFileSync).toBeCalledWith('precloud.config.json'); 118 | expect(mockLogError).not.toBeCalled(); 119 | 120 | expect(config).toEqual({ 121 | format: 'tf', 122 | verbose: false 123 | }); 124 | expect(process.env.VERBOSE).toEqual('false'); 125 | }); 126 | }); -------------------------------------------------------------------------------- /test/commands/check/index.test.ts: -------------------------------------------------------------------------------- 1 | const mockLoggerInfo = jest.fn(); 2 | const mockLoggerSuccess = jest.fn(); 3 | const mockDetectIacFormat = jest.fn(); 4 | const mockPrepareForCheck = jest.fn(); 5 | const mockTestResource = jest.fn(); 6 | const mockCheckTemplates = jest.fn(); 7 | 8 | jest.mock('../../../src/logger', () => ({ 9 | info: mockLoggerInfo, 10 | success: mockLoggerSuccess 11 | })); 12 | 13 | jest.mock('../../../src/commands/check/detect-iac-format.ts', () => ({ 14 | detectIacFormat: mockDetectIacFormat 15 | })); 16 | 17 | jest.mock('../../../src/commands/check/prepare.ts', () => ({ 18 | prepareForCheck: mockPrepareForCheck 19 | })); 20 | 21 | jest.mock('../../../src/commands/check/checks', () => ({ 22 | testResource: mockTestResource, 23 | checkTemplates: mockCheckTemplates 24 | })); 25 | 26 | import { check } from '../../../src/commands/check'; 27 | import { ChangeType, IacFormat } from '../../../src/types'; 28 | 29 | describe('check', () => { 30 | beforeEach(() => { 31 | mockCheckTemplates.mockResolvedValue(undefined); 32 | mockTestResource.mockResolvedValue(undefined); 33 | }); 34 | afterEach(() => { 35 | // for mocks 36 | jest.resetAllMocks(); 37 | // for spies 38 | jest.restoreAllMocks(); 39 | }); 40 | 41 | it('detects IaC format if not passed', async () => { 42 | mockDetectIacFormat.mockReturnValueOnce('mock-format'); 43 | mockPrepareForCheck.mockResolvedValueOnce([]); // Because we don't care about the calls after this point 44 | 45 | await check({}); 46 | 47 | expect(mockDetectIacFormat).toBeCalled(); 48 | expect(mockLoggerInfo).toBeCalledWith('No IaC format specified. Using detected format: mock-format'); 49 | expect(mockPrepareForCheck).toBeCalledWith({ format: 'mock-format' }); 50 | }); 51 | it('runs check on each resource returned', async () => { 52 | const mockSqs = { 53 | stackName: 'mock-stack', 54 | format: IacFormat.awsCdk, 55 | resourceType: 'AWS::SQS::Queue', 56 | changeType: ChangeType.CREATE, 57 | resourceRecord: {} 58 | }; 59 | const mockVpc = { 60 | stackName: 'mock-stack', 61 | format: IacFormat.awsCdk, 62 | resourceType: 'AWS::EC2::VPC', 63 | changeType: ChangeType.CREATE, 64 | resourceRecord: {} 65 | }; 66 | mockDetectIacFormat.mockReturnValueOnce('mock-format'); 67 | mockPrepareForCheck.mockResolvedValueOnce([ 68 | mockSqs, 69 | mockVpc 70 | ]); 71 | 72 | const mockConfig = { format: IacFormat.awsCdk }; 73 | await check(mockConfig); 74 | 75 | expect(mockDetectIacFormat).not.toBeCalled(); 76 | expect(mockLoggerInfo).not.toBeCalled(); 77 | expect(mockPrepareForCheck).toBeCalledWith(mockConfig); 78 | expect(mockTestResource).toBeCalledTimes(2); 79 | expect(mockTestResource).toBeCalledWith(mockSqs, [mockSqs, mockVpc], mockConfig); 80 | expect(mockTestResource).toBeCalledWith(mockVpc, [mockSqs, mockVpc], mockConfig); 81 | expect(mockCheckTemplates).toBeCalledTimes(1); 82 | expect(mockCheckTemplates).toBeCalledWith([mockSqs, mockVpc], mockConfig); 83 | expect(mockLoggerSuccess).toBeCalledWith('PreCloud Check passed!'); 84 | }); 85 | }); -------------------------------------------------------------------------------- /test/commands/check/parser/aws-cdk/MockParser.ts: -------------------------------------------------------------------------------- 1 | import { AwsCdkParser } from '../../../../../src/abstracts'; 2 | 3 | const mockParseResource = jest.fn(); 4 | 5 | class MockParser extends AwsCdkParser { 6 | constructor () { super(); } 7 | parseResource = mockParseResource; 8 | } 9 | 10 | export { 11 | MockParser 12 | }; 13 | export default MockParser; -------------------------------------------------------------------------------- /test/commands/check/parser/aws-cdk/index.test.ts: -------------------------------------------------------------------------------- 1 | import { MockParser } from './MockParser'; 2 | const mockResolve = jest.fn(); 3 | const mockReadFileSync = jest.fn(); 4 | 5 | jest.mock('path', () => { 6 | const original = jest.requireActual('path'); 7 | return { 8 | resolve: mockResolve, 9 | realResolve: original.resolve 10 | }; 11 | }); 12 | 13 | jest.mock('fs', () => { 14 | const original = jest.requireActual('fs'); 15 | return { 16 | readFileSync: mockReadFileSync, 17 | realRFS: original.readFileSync 18 | }; 19 | }); 20 | 21 | jest.mock('@tinystacks/aws-cdk-parser', () => (MockParser)); 22 | 23 | import { parseCdkDiff } from '../../../../../src/commands/check/parser/aws-cdk'; 24 | import { ChangeType, IacFormat } from '../../../../../src/types'; 25 | 26 | const fs = require('fs'); 27 | const path = require('path'); 28 | const mockCdkDiff = fs.realRFS(path.realResolve(__dirname, '../../test-data/simple-sqs-stack/MockCdkDiff.txt')).toString(); 29 | const mockManifest = fs.realRFS(path.realResolve(__dirname, '../../test-data/simple-sqs-stack/MockCdkTemplate.json')); 30 | const mockCdkTemplate = fs.realRFS(path.realResolve(__dirname, '../../test-data/simple-sqs-stack/MockCdkTemplate.json')); 31 | 32 | describe('aws-cdk parser', () => { 33 | afterEach(() => { 34 | // for mocks 35 | jest.resetAllMocks(); 36 | // for spies 37 | jest.restoreAllMocks(); 38 | }); 39 | 40 | it('parseCdkDiff', async () => { 41 | mockReadFileSync.mockReturnValueOnce(mockManifest); 42 | mockReadFileSync.mockReturnValueOnce(mockCdkTemplate); 43 | 44 | const result = await parseCdkDiff(mockCdkDiff, {}); 45 | 46 | expect(Array.isArray(result)).toEqual(true); 47 | expect(result.length).toEqual(3); 48 | 49 | expect(result[0]).toHaveProperty('stackName', 'TestStack'); 50 | expect(result[0]).toHaveProperty('format', IacFormat.awsCdk); 51 | expect(result[0]).toHaveProperty('resourceType', 'AWS::SQS::Queue'); 52 | expect(result[0]).toHaveProperty('changeType', ChangeType.DELETE); 53 | 54 | expect(result[1]).toHaveProperty('stackName', 'TestStack'); 55 | expect(result[1]).toHaveProperty('format', IacFormat.awsCdk); 56 | expect(result[1]).toHaveProperty('resourceType', 'AWS::SQS::Queue'); 57 | expect(result[1]).toHaveProperty('changeType', ChangeType.CREATE); 58 | 59 | expect(result[2]).toHaveProperty('stackName', 'TestStack'); 60 | expect(result[2]).toHaveProperty('format', IacFormat.awsCdk); 61 | expect(result[2]).toHaveProperty('resourceType', 'AWS::SQS::Queue'); 62 | expect(result[2]).toHaveProperty('changeType', ChangeType.UPDATE); 63 | }); 64 | 65 | it('parseCdkDiff with specified stack name', async () => { 66 | mockReadFileSync.mockReturnValueOnce(mockManifest); 67 | mockReadFileSync.mockReturnValueOnce(mockCdkTemplate); 68 | 69 | const diff = mockCdkDiff.split('\n'); 70 | const firstLine = diff.at(0); 71 | const [stackHeader, stackName] = firstLine.split(' '); 72 | const alteredFirstLine = [stackHeader, stackName, `(${stackName}Name)`].join(' '); 73 | diff[0] = alteredFirstLine; 74 | 75 | const result = await parseCdkDiff(diff.join('\n'), {}); 76 | 77 | expect(Array.isArray(result)).toEqual(true); 78 | expect(result.length).toEqual(3); 79 | 80 | expect(result[0]).toHaveProperty('stackName', 'TestStack'); 81 | expect(result[0]).toHaveProperty('format', IacFormat.awsCdk); 82 | expect(result[0]).toHaveProperty('resourceType', 'AWS::SQS::Queue'); 83 | expect(result[0]).toHaveProperty('changeType', ChangeType.DELETE); 84 | 85 | expect(result[1]).toHaveProperty('stackName', 'TestStack'); 86 | expect(result[1]).toHaveProperty('format', IacFormat.awsCdk); 87 | expect(result[1]).toHaveProperty('resourceType', 'AWS::SQS::Queue'); 88 | expect(result[1]).toHaveProperty('changeType', ChangeType.CREATE); 89 | 90 | expect(result[2]).toHaveProperty('stackName', 'TestStack'); 91 | expect(result[2]).toHaveProperty('format', IacFormat.awsCdk); 92 | expect(result[2]).toHaveProperty('resourceType', 'AWS::SQS::Queue'); 93 | expect(result[2]).toHaveProperty('changeType', ChangeType.UPDATE); 94 | }); 95 | }); -------------------------------------------------------------------------------- /test/commands/check/parser/terraform/MockParser.ts: -------------------------------------------------------------------------------- 1 | import { TerraformParser } from '../../../../../src/abstracts'; 2 | 3 | const mockParseResource = jest.fn(); 4 | mockParseResource.mockResolvedValue({}); 5 | 6 | class MockParser extends TerraformParser { 7 | constructor () { super(); } 8 | parseResource = mockParseResource; 9 | } 10 | 11 | export { 12 | MockParser 13 | }; 14 | export default MockParser; -------------------------------------------------------------------------------- /test/commands/check/parser/terraform/index.test.ts: -------------------------------------------------------------------------------- 1 | import { MockParser } from './MockParser'; 2 | const mockResolve = jest.fn(); 3 | const mockReadFileSync = jest.fn(); 4 | 5 | jest.mock('path', () => { 6 | const original = jest.requireActual('path'); 7 | return { 8 | resolve: mockResolve, 9 | realResolve: original.resolve 10 | }; 11 | }); 12 | 13 | jest.mock('fs', () => { 14 | const original = jest.requireActual('fs'); 15 | return { 16 | readFileSync: mockReadFileSync, 17 | realRFS: original.readFileSync 18 | }; 19 | }); 20 | 21 | jest.mock('@tinystacks/terraform-resource-parser', () => (MockParser)); 22 | jest.mock('@tinystacks/terraform-module-parser', () => (MockParser)); 23 | 24 | import { 25 | parseTerraformDiff 26 | } from '../../../../../src/commands/check/parser/terraform'; 27 | import { ChangeType, IacFormat } from '../../../../../src/types'; 28 | 29 | const fs = require('fs'); 30 | const path = require('path'); 31 | 32 | // TODO: update test data with a tf plan that performs a replacement to test "afterAction" branch 33 | const mockSimpleTfPlan = fs.realRFS(path.realResolve(__dirname, '../../test-data/simple-sqs-stack/MockTfPlan.json')); 34 | const mockComplexTfPlan = fs.realRFS(path.realResolve(__dirname, '../../test-data/tf-module-stack/plan.json')); 35 | 36 | describe('aws-cdk parser', () => { 37 | beforeEach(() => { 38 | // because we're not returning mock values from mockParseResource 39 | jest.spyOn(global.console, 'warn').mockReturnValue(); 40 | }); 41 | afterEach(() => { 42 | mockReadFileSync.mockReset(); 43 | }); 44 | 45 | describe('parseTerraformDiff', () => { 46 | it ('capture resource type and change type correctly', async () => { 47 | mockReadFileSync.mockReturnValueOnce(mockSimpleTfPlan); 48 | 49 | const result = await parseTerraformDiff('mock-file', {}); 50 | 51 | expect(Array.isArray(result)).toEqual(true); 52 | expect(result.length).toEqual(3); 53 | 54 | expect(result[0]).toHaveProperty('format', IacFormat.tf); 55 | expect(result[0]).toHaveProperty('resourceType', 'aws_sqs_queue'); 56 | expect(result[0]).toHaveProperty('changeType', ChangeType.UPDATE); 57 | expect(result[0]).toHaveProperty('properties'); 58 | 59 | expect(result[1]).toHaveProperty('format', IacFormat.tf); 60 | expect(result[1]).toHaveProperty('resourceType', 'aws_sqs_queue'); 61 | expect(result[1]).toHaveProperty('changeType', ChangeType.DELETE); 62 | expect(result[1]).toHaveProperty('properties'); 63 | 64 | expect(result[2]).toHaveProperty('format', IacFormat.tf); 65 | expect(result[2]).toHaveProperty('resourceType', 'aws_sqs_queue'); 66 | expect(result[2]).toHaveProperty('changeType', ChangeType.CREATE); 67 | expect(result[2]).toHaveProperty('properties'); 68 | }); 69 | it('captures references and parses modules', async () => { 70 | mockReadFileSync.mockReturnValueOnce(mockComplexTfPlan); 71 | 72 | const result = await parseTerraformDiff('mock-file', {}); 73 | 74 | expect(Array.isArray(result)).toEqual(true); 75 | expect(result.length).toEqual(25); 76 | 77 | expect(result[0]).toHaveProperty('address'); 78 | expect(result[0].address.startsWith('module.')).toBe(true); 79 | }); 80 | }); 81 | }); -------------------------------------------------------------------------------- /test/commands/check/prepare.test.ts: -------------------------------------------------------------------------------- 1 | const mockExistsSync = jest.fn(); 2 | const mockMkdirSync = jest.fn(); 3 | const mockWriteFileSync = jest.fn(); 4 | const mockRunCommand = jest.fn(); 5 | const mockParseCdkDiff = jest.fn(); 6 | const mockParseTerraformDiff = jest.fn(); 7 | 8 | jest.mock('fs', () => ({ 9 | existsSync: mockExistsSync, 10 | mkdirSync: mockMkdirSync, 11 | writeFileSync: mockWriteFileSync 12 | })); 13 | 14 | jest.mock('../../../src/utils/os', () => ({ 15 | runCommand: mockRunCommand 16 | })); 17 | 18 | jest.mock('../../../src/commands/check/parser', () => ({ 19 | parseCdkDiff: mockParseCdkDiff, 20 | parseTerraformDiff: mockParseTerraformDiff 21 | })); 22 | 23 | import { 24 | prepareForCheck 25 | } from '../../../src/commands/check/prepare'; 26 | import { IacFormat, OsOutput } from '../../../src/types'; 27 | 28 | describe('prepare', () => { 29 | afterEach(() => { 30 | // for mocks 31 | jest.resetAllMocks(); 32 | // for spies 33 | jest.restoreAllMocks(); 34 | }); 35 | 36 | describe('prepareForCheck', () => { 37 | it('creates tmp directory if it does not exist', async () => { 38 | mockExistsSync.mockReturnValueOnce(false); 39 | const result = await prepareForCheck({ format: 'mock-format' as IacFormat }); 40 | 41 | expect(mockExistsSync).toBeCalled(); 42 | expect(mockMkdirSync).toBeCalled(); 43 | expect(mockMkdirSync).toBeCalledWith('/tmp/precloud/tmp', { recursive: true }); 44 | expect(result).toEqual([]); 45 | }); 46 | it('runs cdk diff and calls parse for cdk format', async () => { 47 | const mockCdkDiffOutput: OsOutput = { 48 | exitCode: 0, 49 | stderr: 'mock cdk diff', 50 | stdout: '' 51 | }; 52 | mockExistsSync.mockReturnValueOnce(true); 53 | mockRunCommand.mockResolvedValueOnce(mockCdkDiffOutput); 54 | 55 | await prepareForCheck({ format: IacFormat.awsCdk }); 56 | 57 | expect(mockExistsSync).toBeCalled(); 58 | expect(mockMkdirSync).not.toBeCalled(); 59 | 60 | expect(mockRunCommand).toBeCalled(); 61 | expect(mockRunCommand).toBeCalledWith('cdk diff'); 62 | 63 | expect(mockWriteFileSync).toBeCalled(); 64 | expect(mockWriteFileSync).toBeCalledWith('/tmp/precloud/tmp/diff.txt', 'mock cdk diff'); 65 | 66 | expect(mockParseCdkDiff).toBeCalled(); 67 | expect(mockParseCdkDiff).toBeCalledWith('mock cdk diff', { format: 'aws-cdk' }); 68 | }); 69 | it('throws on non-zero exit code', async () => { 70 | const mockCdkDiffOutput: OsOutput = { 71 | exitCode: 1, 72 | stderr: 'mock cdk diff', 73 | stdout: '' 74 | }; 75 | mockExistsSync.mockReturnValueOnce(true); 76 | mockRunCommand.mockResolvedValueOnce(mockCdkDiffOutput); 77 | 78 | let thrownError; 79 | try { 80 | await prepareForCheck({ format: IacFormat.awsCdk }); 81 | } catch (error) { 82 | thrownError = error; 83 | } finally { 84 | expect(mockExistsSync).toBeCalled(); 85 | expect(mockMkdirSync).not.toBeCalled(); 86 | 87 | expect(mockRunCommand).toBeCalled(); 88 | expect(mockRunCommand).toBeCalledWith('cdk diff'); 89 | 90 | expect(thrownError).toHaveProperty('name', 'CliError'); 91 | expect(thrownError).toHaveProperty('message', 'cdk diff failed with exit code 1'); 92 | 93 | expect(mockWriteFileSync).not.toBeCalled(); 94 | expect(mockParseCdkDiff).not.toBeCalled(); 95 | } 96 | 97 | }); 98 | it('runs terraform init, plan, and show and calls parse for terraform format', async () => { 99 | const mockOsOutput: OsOutput = { 100 | exitCode: 0, 101 | stderr: '', 102 | stdout: '' 103 | }; 104 | mockExistsSync.mockReturnValueOnce(true); 105 | mockRunCommand.mockResolvedValueOnce(mockOsOutput); 106 | mockRunCommand.mockResolvedValueOnce(mockOsOutput); 107 | mockRunCommand.mockResolvedValueOnce(mockOsOutput); 108 | 109 | await prepareForCheck({ format: IacFormat.tf }); 110 | 111 | expect(mockExistsSync).toBeCalled(); 112 | expect(mockMkdirSync).not.toBeCalled(); 113 | 114 | expect(mockRunCommand).toBeCalled(); 115 | expect(mockRunCommand).toBeCalledTimes(3); 116 | expect(mockRunCommand).toBeCalledWith('terraform init'); 117 | expect(mockRunCommand).toBeCalledWith('terraform plan -out=/tmp/precloud/tmp/tfplan'); 118 | expect(mockRunCommand).toBeCalledWith('terraform show -no-color -json /tmp/precloud/tmp/tfplan > /tmp/precloud/tmp/plan.json'); 119 | 120 | expect(mockParseTerraformDiff).toBeCalled(); 121 | expect(mockParseTerraformDiff).toBeCalledWith('/tmp/precloud/tmp/plan.json', { format: 'tf' }); 122 | }); 123 | }); 124 | }); -------------------------------------------------------------------------------- /test/commands/check/test-data/simple-sqs-stack/MockCdkDiff.txt: -------------------------------------------------------------------------------- 1 | Stack TestStack 2 | Resources 3 | [-] AWS::SQS::Queue SecondQueueD5D7042B destroy 4 | [+] AWS::SQS::Queue ThirdQueue ThirdQueue074C5B0A 5 | [~] AWS::SQS::Queue FirstQueue FirstQueue19075403 6 | └─ [~] VisibilityTimeout 7 | ├─ [-] 30 8 | └─ [+] 45 -------------------------------------------------------------------------------- /test/commands/check/test-data/simple-sqs-stack/MockCdkTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "FirstQueue19075403": { 4 | "Type": "AWS::SQS::Queue", 5 | "Properties": { 6 | "QueueName": "first-queue", 7 | "VisibilityTimeout": 45 8 | }, 9 | "UpdateReplacePolicy": "Delete", 10 | "DeletionPolicy": "Delete", 11 | "Metadata": { 12 | "aws:cdk:path": "TestStack/FirstQueue/Resource" 13 | } 14 | }, 15 | "ThirdQueue074C5B0A": { 16 | "Type": "AWS::SQS::Queue", 17 | "Properties": { 18 | "QueueName": "third-queue", 19 | "VisibilityTimeout": 30 20 | }, 21 | "UpdateReplacePolicy": "Delete", 22 | "DeletionPolicy": "Delete", 23 | "Metadata": { 24 | "aws:cdk:path": "TestStack/ThirdQueue/Resource" 25 | } 26 | }, 27 | "CDKMetadata": { 28 | "Type": "AWS::CDK::Metadata", 29 | "Properties": { 30 | "Analytics": "v2:deflate64:H4sIAAAAAAAA/zPSMzbXM1BMLC/WTU7J1s3JTNKrDi5JTM7WAQrFFxcW61UHlqaWpuo4p+WBGbUgVlBqcX5pUTJY1Dk/LyWzJDM/r1YnLz8lVS+rWL/M0EzPEGRsVnFmpm5RaV5JZm6qXhCEBgDXDwyzcgAAAA==" 31 | }, 32 | "Metadata": { 33 | "aws:cdk:path": "TestStack/CDKMetadata/Default" 34 | }, 35 | "Condition": "CDKMetadataAvailable" 36 | } 37 | }, 38 | "Parameters": { 39 | "BootstrapVersion": { 40 | "Type": "AWS::SSM::Parameter::Value", 41 | "Default": "/cdk-bootstrap/hnb659fds/version", 42 | "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /test/commands/check/test-data/simple-sqs-stack/MockTfPlan.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.1", 3 | "terraform_version": "1.2.3", 4 | "variables": { 5 | "queue_name": { 6 | "value": "check-sqs" 7 | }, 8 | "visibility_timeout": { 9 | "value": 45 10 | } 11 | }, 12 | "planned_values": { 13 | "outputs": { 14 | "test_tf_sqs_arn": { 15 | "sensitive": false, 16 | "type": "string", 17 | "value": "arn:aws:sqs:us-east-1:222140259348:check-sqs" 18 | }, 19 | "test_tf_sqs_url": { 20 | "sensitive": false, 21 | "type": "string", 22 | "value": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs" 23 | } 24 | }, 25 | "root_module": { 26 | "resources": [ 27 | { 28 | "address": "aws_sqs_queue.test_tf_sqs", 29 | "mode": "managed", 30 | "type": "aws_sqs_queue", 31 | "name": "test_tf_sqs", 32 | "provider_name": "registry.terraform.io/hashicorp/aws", 33 | "schema_version": 0, 34 | "values": { 35 | "arn": "arn:aws:sqs:us-east-1:222140259348:check-sqs", 36 | "content_based_deduplication": false, 37 | "deduplication_scope": "", 38 | "delay_seconds": 0, 39 | "fifo_queue": false, 40 | "fifo_throughput_limit": "", 41 | "id": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs", 42 | "kms_data_key_reuse_period_seconds": 300, 43 | "kms_master_key_id": "", 44 | "max_message_size": 262144, 45 | "message_retention_seconds": 345600, 46 | "name": "check-sqs", 47 | "name_prefix": "", 48 | "policy": "", 49 | "receive_wait_time_seconds": 0, 50 | "redrive_allow_policy": "", 51 | "redrive_policy": "", 52 | "sqs_managed_sse_enabled": true, 53 | "tags": {}, 54 | "tags_all": {}, 55 | "url": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs", 56 | "visibility_timeout_seconds": 30 57 | }, 58 | "sensitive_values": { 59 | "tags": {}, 60 | "tags_all": {} 61 | } 62 | }, 63 | { 64 | "address": "aws_sqs_queue.test_tf_sqs_3", 65 | "mode": "managed", 66 | "type": "aws_sqs_queue", 67 | "name": "test_tf_sqs_3", 68 | "provider_name": "registry.terraform.io/hashicorp/aws", 69 | "schema_version": 0, 70 | "values": { 71 | "content_based_deduplication": false, 72 | "delay_seconds": 0, 73 | "fifo_queue": false, 74 | "kms_master_key_id": null, 75 | "max_message_size": 262144, 76 | "message_retention_seconds": 345600, 77 | "name": "check-sqs-3", 78 | "receive_wait_time_seconds": 0, 79 | "redrive_allow_policy": null, 80 | "redrive_policy": null, 81 | "tags": null, 82 | "visibility_timeout_seconds": 45 83 | }, 84 | "sensitive_values": { 85 | "tags_all": {} 86 | } 87 | } 88 | ] 89 | } 90 | }, 91 | "resource_drift": [ 92 | { 93 | "address": "aws_sqs_queue.test_tf_sqs_2", 94 | "mode": "managed", 95 | "type": "aws_sqs_queue", 96 | "name": "test_tf_sqs_2", 97 | "provider_name": "registry.terraform.io/hashicorp/aws", 98 | "change": { 99 | "actions": [ 100 | "update" 101 | ], 102 | "before": { 103 | "arn": "arn:aws:sqs:us-east-1:222140259348:check-sqs-2", 104 | "content_based_deduplication": false, 105 | "deduplication_scope": "", 106 | "delay_seconds": 0, 107 | "fifo_queue": false, 108 | "fifo_throughput_limit": "", 109 | "id": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs-2", 110 | "kms_data_key_reuse_period_seconds": 300, 111 | "kms_master_key_id": "", 112 | "max_message_size": 262144, 113 | "message_retention_seconds": 345600, 114 | "name": "check-sqs-2", 115 | "name_prefix": "", 116 | "policy": "", 117 | "receive_wait_time_seconds": 0, 118 | "redrive_allow_policy": "", 119 | "redrive_policy": "", 120 | "sqs_managed_sse_enabled": true, 121 | "tags": null, 122 | "tags_all": {}, 123 | "url": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs-2", 124 | "visibility_timeout_seconds": 45 125 | }, 126 | "after": { 127 | "arn": "arn:aws:sqs:us-east-1:222140259348:check-sqs-2", 128 | "content_based_deduplication": false, 129 | "deduplication_scope": "", 130 | "delay_seconds": 0, 131 | "fifo_queue": false, 132 | "fifo_throughput_limit": "", 133 | "id": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs-2", 134 | "kms_data_key_reuse_period_seconds": 300, 135 | "kms_master_key_id": "", 136 | "max_message_size": 262144, 137 | "message_retention_seconds": 345600, 138 | "name": "check-sqs-2", 139 | "name_prefix": "", 140 | "policy": "", 141 | "receive_wait_time_seconds": 0, 142 | "redrive_allow_policy": "", 143 | "redrive_policy": "", 144 | "sqs_managed_sse_enabled": true, 145 | "tags": {}, 146 | "tags_all": {}, 147 | "url": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs-2", 148 | "visibility_timeout_seconds": 45 149 | }, 150 | "after_unknown": {}, 151 | "before_sensitive": { 152 | "tags_all": {} 153 | }, 154 | "after_sensitive": { 155 | "tags": {}, 156 | "tags_all": {} 157 | } 158 | } 159 | } 160 | ], 161 | "resource_changes": [ 162 | { 163 | "address": "aws_sqs_queue.test_tf_sqs", 164 | "mode": "managed", 165 | "type": "aws_sqs_queue", 166 | "name": "test_tf_sqs", 167 | "provider_name": "registry.terraform.io/hashicorp/aws", 168 | "change": { 169 | "actions": [ 170 | "update" 171 | ], 172 | "before": { 173 | "arn": "arn:aws:sqs:us-east-1:222140259348:check-sqs", 174 | "content_based_deduplication": false, 175 | "deduplication_scope": "", 176 | "delay_seconds": 0, 177 | "fifo_queue": false, 178 | "fifo_throughput_limit": "", 179 | "id": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs", 180 | "kms_data_key_reuse_period_seconds": 300, 181 | "kms_master_key_id": "", 182 | "max_message_size": 262144, 183 | "message_retention_seconds": 345600, 184 | "name": "check-sqs", 185 | "name_prefix": "", 186 | "policy": "", 187 | "receive_wait_time_seconds": 0, 188 | "redrive_allow_policy": "", 189 | "redrive_policy": "", 190 | "sqs_managed_sse_enabled": true, 191 | "tags": {}, 192 | "tags_all": {}, 193 | "url": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs", 194 | "visibility_timeout_seconds": 45 195 | }, 196 | "after": { 197 | "arn": "arn:aws:sqs:us-east-1:222140259348:check-sqs", 198 | "content_based_deduplication": false, 199 | "deduplication_scope": "", 200 | "delay_seconds": 0, 201 | "fifo_queue": false, 202 | "fifo_throughput_limit": "", 203 | "id": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs", 204 | "kms_data_key_reuse_period_seconds": 300, 205 | "kms_master_key_id": "", 206 | "max_message_size": 262144, 207 | "message_retention_seconds": 345600, 208 | "name": "check-sqs", 209 | "name_prefix": "", 210 | "policy": "", 211 | "receive_wait_time_seconds": 0, 212 | "redrive_allow_policy": "", 213 | "redrive_policy": "", 214 | "sqs_managed_sse_enabled": true, 215 | "tags": {}, 216 | "tags_all": {}, 217 | "url": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs", 218 | "visibility_timeout_seconds": 30 219 | }, 220 | "after_unknown": {}, 221 | "before_sensitive": { 222 | "tags": {}, 223 | "tags_all": {} 224 | }, 225 | "after_sensitive": { 226 | "tags": {}, 227 | "tags_all": {} 228 | } 229 | } 230 | }, 231 | { 232 | "address": "aws_sqs_queue.test_tf_sqs_2", 233 | "mode": "managed", 234 | "type": "aws_sqs_queue", 235 | "name": "test_tf_sqs_2", 236 | "provider_name": "registry.terraform.io/hashicorp/aws", 237 | "change": { 238 | "actions": [ 239 | "delete" 240 | ], 241 | "before": { 242 | "arn": "arn:aws:sqs:us-east-1:222140259348:check-sqs-2", 243 | "content_based_deduplication": false, 244 | "deduplication_scope": "", 245 | "delay_seconds": 0, 246 | "fifo_queue": false, 247 | "fifo_throughput_limit": "", 248 | "id": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs-2", 249 | "kms_data_key_reuse_period_seconds": 300, 250 | "kms_master_key_id": "", 251 | "max_message_size": 262144, 252 | "message_retention_seconds": 345600, 253 | "name": "check-sqs-2", 254 | "name_prefix": "", 255 | "policy": "", 256 | "receive_wait_time_seconds": 0, 257 | "redrive_allow_policy": "", 258 | "redrive_policy": "", 259 | "sqs_managed_sse_enabled": true, 260 | "tags": {}, 261 | "tags_all": {}, 262 | "url": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs-2", 263 | "visibility_timeout_seconds": 45 264 | }, 265 | "after": null, 266 | "after_unknown": {}, 267 | "before_sensitive": { 268 | "tags": {}, 269 | "tags_all": {} 270 | }, 271 | "after_sensitive": false 272 | }, 273 | "action_reason": "delete_because_no_resource_config" 274 | }, 275 | { 276 | "address": "aws_sqs_queue.test_tf_sqs_3", 277 | "mode": "managed", 278 | "type": "aws_sqs_queue", 279 | "name": "test_tf_sqs_3", 280 | "provider_name": "registry.terraform.io/hashicorp/aws", 281 | "change": { 282 | "actions": [ 283 | "create" 284 | ], 285 | "before": null, 286 | "after": { 287 | "content_based_deduplication": false, 288 | "delay_seconds": 0, 289 | "fifo_queue": false, 290 | "kms_master_key_id": null, 291 | "max_message_size": 262144, 292 | "message_retention_seconds": 345600, 293 | "name": "check-sqs-3", 294 | "receive_wait_time_seconds": 0, 295 | "redrive_allow_policy": null, 296 | "redrive_policy": null, 297 | "tags": null, 298 | "visibility_timeout_seconds": 45 299 | }, 300 | "after_unknown": { 301 | "arn": true, 302 | "deduplication_scope": true, 303 | "fifo_throughput_limit": true, 304 | "id": true, 305 | "kms_data_key_reuse_period_seconds": true, 306 | "name_prefix": true, 307 | "policy": true, 308 | "sqs_managed_sse_enabled": true, 309 | "tags_all": true, 310 | "url": true 311 | }, 312 | "before_sensitive": false, 313 | "after_sensitive": { 314 | "tags_all": {} 315 | } 316 | } 317 | } 318 | ], 319 | "output_changes": { 320 | "test_tf_sqs_arn": { 321 | "actions": [ 322 | "no-op" 323 | ], 324 | "before": "arn:aws:sqs:us-east-1:222140259348:check-sqs", 325 | "after": "arn:aws:sqs:us-east-1:222140259348:check-sqs", 326 | "after_unknown": false, 327 | "before_sensitive": false, 328 | "after_sensitive": false 329 | }, 330 | "test_tf_sqs_url": { 331 | "actions": [ 332 | "no-op" 333 | ], 334 | "before": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs", 335 | "after": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs", 336 | "after_unknown": false, 337 | "before_sensitive": false, 338 | "after_sensitive": false 339 | } 340 | }, 341 | "prior_state": { 342 | "format_version": "1.0", 343 | "terraform_version": "1.2.3", 344 | "values": { 345 | "outputs": { 346 | "test_tf_sqs_arn": { 347 | "sensitive": false, 348 | "value": "arn:aws:sqs:us-east-1:222140259348:check-sqs", 349 | "type": "string" 350 | }, 351 | "test_tf_sqs_url": { 352 | "sensitive": false, 353 | "value": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs", 354 | "type": "string" 355 | } 356 | }, 357 | "root_module": { 358 | "resources": [ 359 | { 360 | "address": "aws_sqs_queue.test_tf_sqs", 361 | "mode": "managed", 362 | "type": "aws_sqs_queue", 363 | "name": "test_tf_sqs", 364 | "provider_name": "registry.terraform.io/hashicorp/aws", 365 | "schema_version": 0, 366 | "values": { 367 | "arn": "arn:aws:sqs:us-east-1:222140259348:check-sqs", 368 | "content_based_deduplication": false, 369 | "deduplication_scope": "", 370 | "delay_seconds": 0, 371 | "fifo_queue": false, 372 | "fifo_throughput_limit": "", 373 | "id": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs", 374 | "kms_data_key_reuse_period_seconds": 300, 375 | "kms_master_key_id": "", 376 | "max_message_size": 262144, 377 | "message_retention_seconds": 345600, 378 | "name": "check-sqs", 379 | "name_prefix": "", 380 | "policy": "", 381 | "receive_wait_time_seconds": 0, 382 | "redrive_allow_policy": "", 383 | "redrive_policy": "", 384 | "sqs_managed_sse_enabled": true, 385 | "tags": {}, 386 | "tags_all": {}, 387 | "url": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs", 388 | "visibility_timeout_seconds": 45 389 | }, 390 | "sensitive_values": { 391 | "tags": {}, 392 | "tags_all": {} 393 | } 394 | }, 395 | { 396 | "address": "aws_sqs_queue.test_tf_sqs_2", 397 | "mode": "managed", 398 | "type": "aws_sqs_queue", 399 | "name": "test_tf_sqs_2", 400 | "provider_name": "registry.terraform.io/hashicorp/aws", 401 | "schema_version": 0, 402 | "values": { 403 | "arn": "arn:aws:sqs:us-east-1:222140259348:check-sqs-2", 404 | "content_based_deduplication": false, 405 | "deduplication_scope": "", 406 | "delay_seconds": 0, 407 | "fifo_queue": false, 408 | "fifo_throughput_limit": "", 409 | "id": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs-2", 410 | "kms_data_key_reuse_period_seconds": 300, 411 | "kms_master_key_id": "", 412 | "max_message_size": 262144, 413 | "message_retention_seconds": 345600, 414 | "name": "check-sqs-2", 415 | "name_prefix": "", 416 | "policy": "", 417 | "receive_wait_time_seconds": 0, 418 | "redrive_allow_policy": "", 419 | "redrive_policy": "", 420 | "sqs_managed_sse_enabled": true, 421 | "tags": {}, 422 | "tags_all": {}, 423 | "url": "https://sqs.us-east-1.amazonaws.com/222140259348/check-sqs-2", 424 | "visibility_timeout_seconds": 45 425 | }, 426 | "sensitive_values": { 427 | "tags": {}, 428 | "tags_all": {} 429 | } 430 | } 431 | ] 432 | } 433 | } 434 | }, 435 | "configuration": { 436 | "provider_config": { 437 | "aws": { 438 | "name": "aws", 439 | "full_name": "registry.terraform.io/hashicorp/aws", 440 | "version_constraint": "~\u003e 3.0" 441 | } 442 | }, 443 | "root_module": { 444 | "outputs": { 445 | "test_tf_sqs_arn": { 446 | "expression": { 447 | "references": [ 448 | "aws_sqs_queue.test_tf_sqs.arn", 449 | "aws_sqs_queue.test_tf_sqs" 450 | ] 451 | } 452 | }, 453 | "test_tf_sqs_url": { 454 | "expression": { 455 | "references": [ 456 | "aws_sqs_queue.test_tf_sqs.url", 457 | "aws_sqs_queue.test_tf_sqs" 458 | ] 459 | } 460 | } 461 | }, 462 | "resources": [ 463 | { 464 | "address": "aws_sqs_queue.test_tf_sqs", 465 | "mode": "managed", 466 | "type": "aws_sqs_queue", 467 | "name": "test_tf_sqs", 468 | "provider_config_key": "aws", 469 | "expressions": { 470 | "name": { 471 | "references": [ 472 | "var.queue_name" 473 | ] 474 | }, 475 | "visibility_timeout_seconds": { 476 | "constant_value": 30 477 | } 478 | }, 479 | "schema_version": 0 480 | }, 481 | { 482 | "address": "aws_sqs_queue.test_tf_sqs_3", 483 | "mode": "managed", 484 | "type": "aws_sqs_queue", 485 | "name": "test_tf_sqs_3", 486 | "provider_config_key": "aws", 487 | "expressions": { 488 | "name": { 489 | "references": [ 490 | "var.queue_name" 491 | ] 492 | }, 493 | "visibility_timeout_seconds": { 494 | "references": [ 495 | "var.visibility_timeout" 496 | ] 497 | } 498 | }, 499 | "schema_version": 0 500 | } 501 | ], 502 | "variables": { 503 | "queue_name": { 504 | "description": "The name for the queue that will be created in this stack" 505 | }, 506 | "visibility_timeout": { 507 | "default": 30, 508 | "description": "The visibility timeout for the queue in seconds" 509 | } 510 | } 511 | } 512 | } 513 | } -------------------------------------------------------------------------------- /test/commands/check/test-data/simple-sqs-stack/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "20.0.0", 3 | "artifacts": { 4 | "Tree": { 5 | "type": "cdk:tree", 6 | "properties": { 7 | "file": "tree.json" 8 | } 9 | }, 10 | "TestStack.assets": { 11 | "type": "cdk:asset-manifest", 12 | "properties": { 13 | "file": "TestStack.assets.json", 14 | "requiresBootstrapStackVersion": 6, 15 | "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" 16 | } 17 | }, 18 | "TestStack": { 19 | "type": "aws:cloudformation:stack", 20 | "environment": "aws://unknown-account/unknown-region", 21 | "properties": { 22 | "templateFile": "TestStack.template.json", 23 | "validateOnSynth": false, 24 | "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", 25 | "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", 26 | "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/6561fc934770d0fab62c927a7695f8dae4337b99f0718104325f9562ba392858.json", 27 | "requiresBootstrapStackVersion": 6, 28 | "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", 29 | "additionalDependencies": [ 30 | "TestStack.assets" 31 | ], 32 | "lookupRole": { 33 | "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", 34 | "requiresBootstrapStackVersion": 8, 35 | "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" 36 | } 37 | }, 38 | "dependencies": [ 39 | "TestStack.assets" 40 | ], 41 | "metadata": { 42 | "/TestStack/CheckQueue/Resource": [ 43 | { 44 | "type": "aws:cdk:logicalId", 45 | "data": "CheckQueueB0847F6A" 46 | } 47 | ], 48 | "/TestStack/CDKMetadata/Default": [ 49 | { 50 | "type": "aws:cdk:logicalId", 51 | "data": "CDKMetadata" 52 | } 53 | ], 54 | "/TestStack/CDKMetadata/Condition": [ 55 | { 56 | "type": "aws:cdk:logicalId", 57 | "data": "CDKMetadataAvailable" 58 | } 59 | ], 60 | "/TestStack/BootstrapVersion": [ 61 | { 62 | "type": "aws:cdk:logicalId", 63 | "data": "BootstrapVersion" 64 | } 65 | ], 66 | "/TestStack/CheckBootstrapVersion": [ 67 | { 68 | "type": "aws:cdk:logicalId", 69 | "data": "CheckBootstrapVersion" 70 | } 71 | ] 72 | }, 73 | "displayName": "TestStack" 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /test/commands/check/test-data/tf-module-stack/main.tf: -------------------------------------------------------------------------------- 1 | module "my_vpc" { 2 | source = "git::https://github.com/tinystacks/tinystacks-terraform-modules.git//aws/modules/vpc" 3 | 4 | ts_aws_vpc_cidr_block = "10.0.0.0/16" 5 | ts_aws_vpc_cidr_newbits = 4 6 | 7 | ts_vpc_slice_azs = true 8 | ts_vpc_slice_azs_start_index = 0 9 | ts_vpc_slice_azs_end_index = 2 10 | } -------------------------------------------------------------------------------- /test/commands/check/test-data/tf-module-stack/tf-json.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "module-main.tf", 4 | "contents": { 5 | "module": { 6 | "my_vpc": [ 7 | { 8 | "source": "git::https://github.com/tinystacks/tinystacks-terraform-modules.git//aws/modules/vpc", 9 | "ts_aws_vpc_cidr_block": "10.0.0.0/16", 10 | "ts_aws_vpc_cidr_newbits": 4, 11 | "ts_vpc_slice_azs": true, 12 | "ts_vpc_slice_azs_end_index": 2, 13 | "ts_vpc_slice_azs_start_index": 0 14 | } 15 | ], 16 | "other": [ 17 | { 18 | "source": "git::https://github.com/other/other-terraform-modules.git//aws/modules/other" 19 | } 20 | ] 21 | } 22 | } 23 | } 24 | ] -------------------------------------------------------------------------------- /test/commands/check/test-data/vpc-stack/cdk/no-nat/aws-cdk-diff.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "stackName": "CheckApp", 4 | "format": "aws-cdk", 5 | "changeType": "CREATE", 6 | "resourceType": "AWS::EC2::VPC", 7 | "address": "Vpc/Vpc", 8 | "logicalId": "VpcC3027511", 9 | "properties": { 10 | "cidrBlock": "10.40.0.0/16", 11 | "instanceTenancy": "default", 12 | "tagSet": [ 13 | { 14 | "Key": "Name", 15 | "Value": "CheckApp/Vpc/Vpc" 16 | } 17 | ] 18 | } 19 | }, 20 | { 21 | "stackName": "CheckApp", 22 | "format": "aws-cdk", 23 | "changeType": "CREATE", 24 | "resourceType": "AWS::EC2::Subnet", 25 | "address": "Vpc/Vpc/PublicSubnetSubnet1/Subnet", 26 | "logicalId": "VpcPublicSubnetSubnet1SubnetCE01ADA6", 27 | "properties": { 28 | "availabilityZone": { 29 | "Fn::Select": [ 30 | 0, 31 | { 32 | "Fn::GetAZs": "" 33 | } 34 | ] 35 | }, 36 | "cidrBlock": "10.40.0.0/19", 37 | "mapPublicIpOnLaunch": true, 38 | "vpcId": { 39 | "Ref": "VpcC3027511" 40 | }, 41 | "tagSet": [ 42 | { 43 | "Key": "aws-cdk:subnet-name", 44 | "Value": "PublicSubnet" 45 | }, 46 | { 47 | "Key": "aws-cdk:subnet-type", 48 | "Value": "Public" 49 | }, 50 | { 51 | "Key": "Name", 52 | "Value": "CheckApp/Vpc/Vpc/PublicSubnetSubnet1" 53 | } 54 | ] 55 | } 56 | }, 57 | { 58 | "stackName": "CheckApp", 59 | "format": "aws-cdk", 60 | "changeType": "CREATE", 61 | "resourceType": "AWS::EC2::RouteTable", 62 | "address": "Vpc/Vpc/PublicSubnetSubnet1/RouteTable", 63 | "logicalId": "VpcPublicSubnetSubnet1RouteTableD16E6674", 64 | "properties": { 65 | "associationSet": [ 66 | { 67 | "routeTableId": { 68 | "Ref": "VpcPublicSubnetSubnet1RouteTableD16E6674" 69 | }, 70 | "subnetId": { 71 | "Ref": "VpcPublicSubnetSubnet1SubnetCE01ADA6" 72 | } 73 | } 74 | ], 75 | "routeSet": [ 76 | { 77 | "destinationCidrBlock": "0.0.0.0/0", 78 | "gatewayId": { 79 | "Ref": "VpcIGW488B0FEB" 80 | } 81 | } 82 | ], 83 | "tagSet": [ 84 | { 85 | "Key": "Name", 86 | "Value": "CheckApp/Vpc/Vpc/PublicSubnetSubnet1" 87 | } 88 | ], 89 | "vpcId": { 90 | "Ref": "VpcC3027511" 91 | } 92 | } 93 | }, 94 | { 95 | "stackName": "CheckApp", 96 | "format": "aws-cdk", 97 | "changeType": "CREATE", 98 | "resourceType": "AWS::EC2::SubnetRouteTableAssociation", 99 | "address": "Vpc/Vpc/PublicSubnetSubnet1/RouteTableAssociation", 100 | "logicalId": "VpcPublicSubnetSubnet1RouteTableAssociationAF3B2E14", 101 | "properties": { 102 | "routeTableId": { 103 | "Ref": "VpcPublicSubnetSubnet1RouteTableD16E6674" 104 | }, 105 | "subnetId": { 106 | "Ref": "VpcPublicSubnetSubnet1SubnetCE01ADA6" 107 | } 108 | } 109 | }, 110 | { 111 | "stackName": "CheckApp", 112 | "format": "aws-cdk", 113 | "changeType": "CREATE", 114 | "resourceType": "AWS::EC2::Route", 115 | "address": "Vpc/Vpc/PublicSubnetSubnet1/DefaultRoute", 116 | "logicalId": "VpcPublicSubnetSubnet1DefaultRoute0E1DFC20", 117 | "properties": { 118 | "destinationCidrBlock": "0.0.0.0/0", 119 | "gatewayId": { 120 | "Ref": "VpcIGW488B0FEB" 121 | } 122 | } 123 | }, 124 | { 125 | "stackName": "CheckApp", 126 | "format": "aws-cdk", 127 | "changeType": "CREATE", 128 | "resourceType": "AWS::EC2::Subnet", 129 | "address": "Vpc/Vpc/PublicSubnetSubnet2/Subnet", 130 | "logicalId": "VpcPublicSubnetSubnet2Subnet1C0D5211", 131 | "properties": { 132 | "availabilityZone": { 133 | "Fn::Select": [ 134 | 1, 135 | { 136 | "Fn::GetAZs": "" 137 | } 138 | ] 139 | }, 140 | "cidrBlock": "10.40.32.0/19", 141 | "mapPublicIpOnLaunch": true, 142 | "vpcId": { 143 | "Ref": "VpcC3027511" 144 | }, 145 | "tagSet": [ 146 | { 147 | "Key": "aws-cdk:subnet-name", 148 | "Value": "PublicSubnet" 149 | }, 150 | { 151 | "Key": "aws-cdk:subnet-type", 152 | "Value": "Public" 153 | }, 154 | { 155 | "Key": "Name", 156 | "Value": "CheckApp/Vpc/Vpc/PublicSubnetSubnet2" 157 | } 158 | ] 159 | } 160 | }, 161 | { 162 | "stackName": "CheckApp", 163 | "format": "aws-cdk", 164 | "changeType": "CREATE", 165 | "resourceType": "AWS::EC2::RouteTable", 166 | "address": "Vpc/Vpc/PublicSubnetSubnet2/RouteTable", 167 | "logicalId": "VpcPublicSubnetSubnet2RouteTableD12B2355", 168 | "properties": { 169 | "associationSet": [ 170 | { 171 | "routeTableId": { 172 | "Ref": "VpcPublicSubnetSubnet2RouteTableD12B2355" 173 | }, 174 | "subnetId": { 175 | "Ref": "VpcPublicSubnetSubnet2Subnet1C0D5211" 176 | } 177 | } 178 | ], 179 | "routeSet": [ 180 | { 181 | "destinationCidrBlock": "0.0.0.0/0", 182 | "gatewayId": { 183 | "Ref": "VpcIGW488B0FEB" 184 | } 185 | } 186 | ], 187 | "tagSet": [ 188 | { 189 | "Key": "Name", 190 | "Value": "CheckApp/Vpc/Vpc/PublicSubnetSubnet2" 191 | } 192 | ], 193 | "vpcId": { 194 | "Ref": "VpcC3027511" 195 | } 196 | } 197 | }, 198 | { 199 | "stackName": "CheckApp", 200 | "format": "aws-cdk", 201 | "changeType": "CREATE", 202 | "resourceType": "AWS::EC2::SubnetRouteTableAssociation", 203 | "address": "Vpc/Vpc/PublicSubnetSubnet2/RouteTableAssociation", 204 | "logicalId": "VpcPublicSubnetSubnet2RouteTableAssociation31AD1654", 205 | "properties": { 206 | "routeTableId": { 207 | "Ref": "VpcPublicSubnetSubnet2RouteTableD12B2355" 208 | }, 209 | "subnetId": { 210 | "Ref": "VpcPublicSubnetSubnet2Subnet1C0D5211" 211 | } 212 | } 213 | }, 214 | { 215 | "stackName": "CheckApp", 216 | "format": "aws-cdk", 217 | "changeType": "CREATE", 218 | "resourceType": "AWS::EC2::Route", 219 | "address": "Vpc/Vpc/PublicSubnetSubnet2/DefaultRoute", 220 | "logicalId": "VpcPublicSubnetSubnet2DefaultRoute902DF4E4", 221 | "properties": { 222 | "destinationCidrBlock": "0.0.0.0/0", 223 | "gatewayId": { 224 | "Ref": "VpcIGW488B0FEB" 225 | } 226 | } 227 | }, 228 | { 229 | "stackName": "CheckApp", 230 | "format": "aws-cdk", 231 | "changeType": "CREATE", 232 | "resourceType": "AWS::EC2::Subnet", 233 | "address": "Vpc/Vpc/IsolatedSubnetSubnet1/Subnet", 234 | "logicalId": "VpcIsolatedSubnetSubnet1Subnet9413A915", 235 | "properties": { 236 | "availabilityZone": { 237 | "Fn::Select": [ 238 | 0, 239 | { 240 | "Fn::GetAZs": "" 241 | } 242 | ] 243 | }, 244 | "cidrBlock": "10.40.64.0/19", 245 | "mapPublicIpOnLaunch": false, 246 | "vpcId": { 247 | "Ref": "VpcC3027511" 248 | }, 249 | "tagSet": [ 250 | { 251 | "Key": "aws-cdk:subnet-name", 252 | "Value": "IsolatedSubnet" 253 | }, 254 | { 255 | "Key": "aws-cdk:subnet-type", 256 | "Value": "Isolated" 257 | }, 258 | { 259 | "Key": "Name", 260 | "Value": "CheckApp/Vpc/Vpc/IsolatedSubnetSubnet1" 261 | } 262 | ] 263 | } 264 | }, 265 | { 266 | "stackName": "CheckApp", 267 | "format": "aws-cdk", 268 | "changeType": "CREATE", 269 | "resourceType": "AWS::EC2::RouteTable", 270 | "address": "Vpc/Vpc/IsolatedSubnetSubnet1/RouteTable", 271 | "logicalId": "VpcIsolatedSubnetSubnet1RouteTableE8228AAB", 272 | "properties": { 273 | "associationSet": [ 274 | { 275 | "routeTableId": { 276 | "Ref": "VpcIsolatedSubnetSubnet1RouteTableE8228AAB" 277 | }, 278 | "subnetId": { 279 | "Ref": "VpcIsolatedSubnetSubnet1Subnet9413A915" 280 | } 281 | } 282 | ], 283 | "routeSet": [], 284 | "tagSet": [ 285 | { 286 | "Key": "Name", 287 | "Value": "CheckApp/Vpc/Vpc/IsolatedSubnetSubnet1" 288 | } 289 | ], 290 | "vpcId": { 291 | "Ref": "VpcC3027511" 292 | } 293 | } 294 | }, 295 | { 296 | "stackName": "CheckApp", 297 | "format": "aws-cdk", 298 | "changeType": "CREATE", 299 | "resourceType": "AWS::EC2::SubnetRouteTableAssociation", 300 | "address": "Vpc/Vpc/IsolatedSubnetSubnet1/RouteTableAssociation", 301 | "logicalId": "VpcIsolatedSubnetSubnet1RouteTableAssociationB0977E01", 302 | "properties": { 303 | "routeTableId": { 304 | "Ref": "VpcIsolatedSubnetSubnet1RouteTableE8228AAB" 305 | }, 306 | "subnetId": { 307 | "Ref": "VpcIsolatedSubnetSubnet1Subnet9413A915" 308 | } 309 | } 310 | }, 311 | { 312 | "stackName": "CheckApp", 313 | "format": "aws-cdk", 314 | "changeType": "CREATE", 315 | "resourceType": "AWS::EC2::Subnet", 316 | "address": "Vpc/Vpc/IsolatedSubnetSubnet2/Subnet", 317 | "logicalId": "VpcIsolatedSubnetSubnet2Subnet68825D18", 318 | "properties": { 319 | "availabilityZone": { 320 | "Fn::Select": [ 321 | 1, 322 | { 323 | "Fn::GetAZs": "" 324 | } 325 | ] 326 | }, 327 | "cidrBlock": "10.40.96.0/19", 328 | "mapPublicIpOnLaunch": false, 329 | "vpcId": { 330 | "Ref": "VpcC3027511" 331 | }, 332 | "tagSet": [ 333 | { 334 | "Key": "aws-cdk:subnet-name", 335 | "Value": "IsolatedSubnet" 336 | }, 337 | { 338 | "Key": "aws-cdk:subnet-type", 339 | "Value": "Isolated" 340 | }, 341 | { 342 | "Key": "Name", 343 | "Value": "CheckApp/Vpc/Vpc/IsolatedSubnetSubnet2" 344 | } 345 | ] 346 | } 347 | }, 348 | { 349 | "stackName": "CheckApp", 350 | "format": "aws-cdk", 351 | "changeType": "CREATE", 352 | "resourceType": "AWS::EC2::RouteTable", 353 | "address": "Vpc/Vpc/IsolatedSubnetSubnet2/RouteTable", 354 | "logicalId": "VpcIsolatedSubnetSubnet2RouteTable4C59DEE4", 355 | "properties": { 356 | "associationSet": [ 357 | { 358 | "routeTableId": { 359 | "Ref": "VpcIsolatedSubnetSubnet2RouteTable4C59DEE4" 360 | }, 361 | "subnetId": { 362 | "Ref": "VpcIsolatedSubnetSubnet2Subnet68825D18" 363 | } 364 | } 365 | ], 366 | "routeSet": [], 367 | "tagSet": [ 368 | { 369 | "Key": "Name", 370 | "Value": "CheckApp/Vpc/Vpc/IsolatedSubnetSubnet2" 371 | } 372 | ], 373 | "vpcId": { 374 | "Ref": "VpcC3027511" 375 | } 376 | } 377 | }, 378 | { 379 | "stackName": "CheckApp", 380 | "format": "aws-cdk", 381 | "changeType": "CREATE", 382 | "resourceType": "AWS::EC2::SubnetRouteTableAssociation", 383 | "address": "Vpc/Vpc/IsolatedSubnetSubnet2/RouteTableAssociation", 384 | "logicalId": "VpcIsolatedSubnetSubnet2RouteTableAssociationF1972EDB", 385 | "properties": { 386 | "routeTableId": { 387 | "Ref": "VpcIsolatedSubnetSubnet2RouteTable4C59DEE4" 388 | }, 389 | "subnetId": { 390 | "Ref": "VpcIsolatedSubnetSubnet2Subnet68825D18" 391 | } 392 | } 393 | }, 394 | { 395 | "stackName": "CheckApp", 396 | "format": "aws-cdk", 397 | "changeType": "CREATE", 398 | "resourceType": "AWS::EC2::InternetGateway", 399 | "address": "Vpc/Vpc/IGW", 400 | "logicalId": "VpcIGW488B0FEB", 401 | "properties": {} 402 | }, 403 | { 404 | "stackName": "CheckApp", 405 | "format": "aws-cdk", 406 | "changeType": "CREATE", 407 | "resourceType": "AWS::EC2::VPCGatewayAttachment", 408 | "address": "Vpc/Vpc/VPCGW", 409 | "logicalId": "VpcVPCGW42EC8516", 410 | "properties": {} 411 | } 412 | ] -------------------------------------------------------------------------------- /test/commands/check/test-data/vpc-stack/cdk/no-nat/diff.txt: -------------------------------------------------------------------------------- 1 | Stack CheckApp 2 | 3 | Parameters 4 | 5 | [+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value","Default":"/cdk-bootstrap/hnb659fds/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"} 6 | 7 | Conditions 8 | [+] Condition CDKMetadata/Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"af-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-northwest-1"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"sa-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-2"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-2"]}]}]} 9 | 10 | Resources 11 | 12 | [+] AWS::EC2::VPC Vpc/Vpc VpcC3027511 13 | 14 | [+] AWS::EC2::Subnet Vpc/Vpc/PublicSubnetSubnet1/Subnet VpcPublicSubnetSubnet1SubnetCE01ADA6 15 | [+] AWS::EC2::RouteTable Vpc/Vpc/PublicSubnetSubnet1/RouteTable VpcPublicSubnetSubnet1RouteTableD16E6674 16 | 17 | [+] AWS::EC2::SubnetRouteTableAssociation Vpc/Vpc/PublicSubnetSubnet1/RouteTableAssociation VpcPublicSubnetSubnet1RouteTableAssociationAF3B2E14 18 | 19 | [+] AWS::EC2::Route Vpc/Vpc/PublicSubnetSubnet1/DefaultRoute VpcPublicSubnetSubnet1DefaultRoute0E1DFC20 20 | [+] AWS::EC2::Subnet Vpc/Vpc/PublicSubnetSubnet2/Subnet VpcPublicSubnetSubnet2Subnet1C0D5211 21 | [+] AWS::EC2::RouteTable Vpc/Vpc/PublicSubnetSubnet2/RouteTable VpcPublicSubnetSubnet2RouteTableD12B2355 22 | [+] AWS::EC2::SubnetRouteTableAssociation Vpc/Vpc/PublicSubnetSubnet2/RouteTableAssociation VpcPublicSubnetSubnet2RouteTableAssociation31AD1654 23 | 24 | [+] AWS::EC2::Route Vpc/Vpc/PublicSubnetSubnet2/DefaultRoute VpcPublicSubnetSubnet2DefaultRoute902DF4E4 25 | [+] AWS::EC2::Subnet Vpc/Vpc/IsolatedSubnetSubnet1/Subnet VpcIsolatedSubnetSubnet1Subnet9413A915 26 | [+] AWS::EC2::RouteTable Vpc/Vpc/IsolatedSubnetSubnet1/RouteTable VpcIsolatedSubnetSubnet1RouteTableE8228AAB 27 | 28 | [+] AWS::EC2::SubnetRouteTableAssociation Vpc/Vpc/IsolatedSubnetSubnet1/RouteTableAssociation VpcIsolatedSubnetSubnet1RouteTableAssociationB0977E01 29 | 30 | [+] AWS::EC2::Subnet Vpc/Vpc/IsolatedSubnetSubnet2/Subnet VpcIsolatedSubnetSubnet2Subnet68825D18 31 | [+] AWS::EC2::RouteTable Vpc/Vpc/IsolatedSubnetSubnet2/RouteTable VpcIsolatedSubnetSubnet2RouteTable4C59DEE4 32 | [+] AWS::EC2::SubnetRouteTableAssociation Vpc/Vpc/IsolatedSubnetSubnet2/RouteTableAssociation VpcIsolatedSubnetSubnet2RouteTableAssociationF1972EDB 33 | [+] AWS::EC2::InternetGateway Vpc/Vpc/IGW VpcIGW488B0FEB 34 | [+] AWS::EC2::VPCGatewayAttachment Vpc/Vpc/VPCGW VpcVPCGW42EC8516 35 | 36 | Outputs 37 | [+] Output Vpc/VpcId VpcVpcId87CD4CDE: {"Description":"Vpc-vpc-id","Value":{"Ref":"VpcC3027511"}} 38 | 39 | Other Changes 40 | [+] Unknown Rules: {"CheckBootstrapVersion":{"Assertions":[{"Assert":{"Fn::Not":[{"Fn::Contains":[["1","2","3","4","5"],{"Ref":"BootstrapVersion"}]}]},"AssertDescription":"CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."}]}} 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/commands/check/test-data/vpc-stack/cdk/with-nat/diff.txt: -------------------------------------------------------------------------------- 1 | Stack CheckApp 2 | 3 | Parameters 4 | 5 | [+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value","Default":"/cdk-bootstrap/hnb659fds/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"} 6 | 7 | Conditions 8 | [+] Condition CDKMetadata/Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"af-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-northwest-1"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"sa-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-2"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-2"]}]}]} 9 | 10 | Resources 11 | 12 | [+] AWS::EC2::VPC Vpc/Vpc VpcC3027511 13 | 14 | [+] AWS::EC2::Subnet Vpc/Vpc/PrivateSubnetSubnet1/Subnet VpcPrivateSubnetSubnet1Subnet3C211B2A 15 | [+] AWS::EC2::RouteTable Vpc/Vpc/PrivateSubnetSubnet1/RouteTable VpcPrivateSubnetSubnet1RouteTableED4DF1E5 16 | [+] AWS::EC2::SubnetRouteTableAssociation Vpc/Vpc/PrivateSubnetSubnet1/RouteTableAssociation VpcPrivateSubnetSubnet1RouteTableAssociation85CAFFAD 17 | [+] AWS::EC2::Route Vpc/Vpc/PrivateSubnetSubnet1/DefaultRoute VpcPrivateSubnetSubnet1DefaultRouteF299C1D4 18 | [+] AWS::EC2::Subnet Vpc/Vpc/PrivateSubnetSubnet2/Subnet VpcPrivateSubnetSubnet2SubnetE5B22E3E 19 | [+] AWS::EC2::RouteTable Vpc/Vpc/PrivateSubnetSubnet2/RouteTable VpcPrivateSubnetSubnet2RouteTable07B1EDF0 20 | 21 | [+] AWS::EC2::SubnetRouteTableAssociation Vpc/Vpc/PrivateSubnetSubnet2/RouteTableAssociation VpcPrivateSubnetSubnet2RouteTableAssociationCAC14BDA 22 | 23 | [+] AWS::EC2::Route Vpc/Vpc/PrivateSubnetSubnet2/DefaultRoute VpcPrivateSubnetSubnet2DefaultRoute405F833F 24 | 25 | [+] AWS::EC2::Subnet Vpc/Vpc/PublicSubnetSubnet1/Subnet VpcPublicSubnetSubnet1SubnetCE01ADA6 26 | [+] AWS::EC2::RouteTable Vpc/Vpc/PublicSubnetSubnet1/RouteTable VpcPublicSubnetSubnet1RouteTableD16E6674 27 | [+] AWS::EC2::SubnetRouteTableAssociation Vpc/Vpc/PublicSubnetSubnet1/RouteTableAssociation VpcPublicSubnetSubnet1RouteTableAssociationAF3B2E14 28 | [+] AWS::EC2::Route Vpc/Vpc/PublicSubnetSubnet1/DefaultRoute VpcPublicSubnetSubnet1DefaultRoute0E1DFC20 29 | [+] AWS::EC2::EIP Vpc/Vpc/PublicSubnetSubnet1/EIP VpcPublicSubnetSubnet1EIP4F45FFE5 30 | [+] AWS::EC2::NatGateway Vpc/Vpc/PublicSubnetSubnet1/NATGateway VpcPublicSubnetSubnet1NATGatewayEDDB9575 31 | [+] AWS::EC2::Subnet Vpc/Vpc/PublicSubnetSubnet2/Subnet VpcPublicSubnetSubnet2Subnet1C0D5211 32 | [+] AWS::EC2::RouteTable Vpc/Vpc/PublicSubnetSubnet2/RouteTable VpcPublicSubnetSubnet2RouteTableD12B2355 33 | [+] AWS::EC2::SubnetRouteTableAssociation Vpc/Vpc/PublicSubnetSubnet2/RouteTableAssociation VpcPublicSubnetSubnet2RouteTableAssociation31AD1654 34 | [+] AWS::EC2::Route Vpc/Vpc/PublicSubnetSubnet2/DefaultRoute VpcPublicSubnetSubnet2DefaultRoute902DF4E4 35 | [+] AWS::EC2::EIP Vpc/Vpc/PublicSubnetSubnet2/EIP VpcPublicSubnetSubnet2EIPC2798F3C 36 | [+] AWS::EC2::NatGateway Vpc/Vpc/PublicSubnetSubnet2/NATGateway VpcPublicSubnetSubnet2NATGatewayE3416159 37 | [+] AWS::EC2::Subnet Vpc/Vpc/IsolatedSubnetSubnet1/Subnet VpcIsolatedSubnetSubnet1Subnet9413A915 38 | [+] AWS::EC2::RouteTable Vpc/Vpc/IsolatedSubnetSubnet1/RouteTable VpcIsolatedSubnetSubnet1RouteTableE8228AAB 39 | [+] AWS::EC2::SubnetRouteTableAssociation Vpc/Vpc/IsolatedSubnetSubnet1/RouteTableAssociation VpcIsolatedSubnetSubnet1RouteTableAssociationB0977E01 40 | [+] AWS::EC2::Subnet Vpc/Vpc/IsolatedSubnetSubnet2/Subnet VpcIsolatedSubnetSubnet2Subnet68825D18 41 | [+] AWS::EC2::RouteTable Vpc/Vpc/IsolatedSubnetSubnet2/RouteTable VpcIsolatedSubnetSubnet2RouteTable4C59DEE4 42 | [+] AWS::EC2::SubnetRouteTableAssociation Vpc/Vpc/IsolatedSubnetSubnet2/RouteTableAssociation VpcIsolatedSubnetSubnet2RouteTableAssociationF1972EDB 43 | [+] AWS::EC2::InternetGateway Vpc/Vpc/IGW VpcIGW488B0FEB 44 | 45 | [+] AWS::EC2::VPCGatewayAttachment Vpc/Vpc/VPCGW VpcVPCGW42EC8516 46 | 47 | Outputs 48 | 49 | [+] Output Vpc/VpcId VpcVpcId87CD4CDE: {"Description":"Vpc-vpc-id","Value":{"Ref":"VpcC3027511"}} 50 | 51 | Other Changes 52 | [+] Unknown Rules: {"CheckBootstrapVersion":{"Assertions":[{"Assert":{"Fn::Not":[{"Fn::Contains":[["1","2","3","4","5"],{"Ref":"BootstrapVersion"}]}]},"AssertDescription":"CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."}]}} 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /test/commands/check/test-data/vpc-stack/tf/no-nat/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_vpc" "ts_aws_vpc" { 2 | cidr_block = var.ts_aws_vpc_cidr_block 3 | instance_tenancy = "default" 4 | enable_dns_hostnames = true 5 | } 6 | 7 | /* */ 8 | 9 | resource "aws_subnet" "ts_aws_subnet_public_igw" { 10 | 11 | vpc_id = aws_vpc.ts_aws_vpc.id 12 | cidr_block = cidrsubnet(aws_vpc.ts_aws_vpc.cidr_block, var.ts_aws_vpc_cidr_newbits, 1) 13 | 14 | map_public_ip_on_launch = true 15 | 16 | } 17 | 18 | resource "aws_route_table" "ts_aws_route_table_public_igw" { 19 | 20 | vpc_id = aws_vpc.ts_aws_vpc.id 21 | 22 | } 23 | 24 | resource "aws_route_table_association" "ts_aws_route_table_association_public_igw" { 25 | 26 | subnet_id = aws_subnet.ts_aws_subnet_public_igw.id 27 | route_table_id = aws_route_table.ts_aws_route_table_public_igw.id 28 | 29 | } 30 | 31 | resource "aws_internet_gateway" "ts_aws_internet_gateway" { 32 | 33 | vpc_id = aws_vpc.ts_aws_vpc.id 34 | 35 | } 36 | 37 | resource "aws_route" "ts_aws_route_public_igw" { 38 | 39 | route_table_id = aws_route_table.ts_aws_route_table_public_igw.id 40 | destination_cidr_block = "0.0.0.0/0" 41 | gateway_id = aws_internet_gateway.ts_aws_internet_gateway.id 42 | 43 | } 44 | 45 | 46 | /* */ 47 | 48 | resource "aws_subnet" "ts_aws_subnet_private_isolated" { 49 | 50 | vpc_id = aws_vpc.ts_aws_vpc.id 51 | cidr_block = cidrsubnet(aws_vpc.ts_aws_vpc.cidr_block, var.ts_aws_vpc_cidr_newbits, 3) 52 | 53 | } 54 | 55 | resource "aws_route_table" "ts_aws_route_table_private_isolated" { 56 | 57 | vpc_id = aws_vpc.ts_aws_vpc.id 58 | 59 | } 60 | 61 | resource "aws_route_table_association" "ts_aws_route_table_association_private_isolated" { 62 | 63 | subnet_id = aws_subnet.ts_aws_subnet_private_isolated.id 64 | route_table_id = aws_route_table.ts_aws_route_table_private_isolated.id 65 | 66 | } -------------------------------------------------------------------------------- /test/commands/check/test-data/vpc-stack/tf/no-nat/plan.json: -------------------------------------------------------------------------------- 1 | {"format_version":"1.1","terraform_version":"1.2.3","variables":{"other_aws_vpc_cidr_block":{"value":"10.0.0.0/16"},"other_aws_vpc_cidr_newbits":{"value":4},"ts_aws_vpc_cidr_block":{"value":"10.0.0.0/16"},"ts_aws_vpc_cidr_newbits":{"value":4}},"planned_values":{"root_module":{"resources":[{"address":"aws_internet_gateway.ts_aws_internet_gateway","mode":"managed","type":"aws_internet_gateway","name":"ts_aws_internet_gateway","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":0,"values":{"tags":null,"timeouts":null},"sensitive_values":{"tags_all":{}}},{"address":"aws_route.ts_aws_route_public_igw","mode":"managed","type":"aws_route","name":"ts_aws_route_public_igw","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":0,"values":{"carrier_gateway_id":null,"core_network_arn":null,"destination_cidr_block":"0.0.0.0/0","destination_ipv6_cidr_block":null,"destination_prefix_list_id":null,"egress_only_gateway_id":null,"local_gateway_id":null,"nat_gateway_id":null,"timeouts":null,"transit_gateway_id":null,"vpc_endpoint_id":null,"vpc_peering_connection_id":null},"sensitive_values":{}},{"address":"aws_route_table.ts_aws_route_table_private_isolated","mode":"managed","type":"aws_route_table","name":"ts_aws_route_table_private_isolated","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":0,"values":{"tags":null,"timeouts":null},"sensitive_values":{"propagating_vgws":[],"route":[],"tags_all":{}}},{"address":"aws_route_table.ts_aws_route_table_public_igw","mode":"managed","type":"aws_route_table","name":"ts_aws_route_table_public_igw","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":0,"values":{"tags":null,"timeouts":null},"sensitive_values":{"propagating_vgws":[],"route":[],"tags_all":{}}},{"address":"aws_route_table_association.ts_aws_route_table_association_private_isolated","mode":"managed","type":"aws_route_table_association","name":"ts_aws_route_table_association_private_isolated","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":0,"values":{"gateway_id":null},"sensitive_values":{}},{"address":"aws_route_table_association.ts_aws_route_table_association_public_igw","mode":"managed","type":"aws_route_table_association","name":"ts_aws_route_table_association_public_igw","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":0,"values":{"gateway_id":null},"sensitive_values":{}},{"address":"aws_subnet.ts_aws_subnet_private_isolated","mode":"managed","type":"aws_subnet","name":"ts_aws_subnet_private_isolated","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":1,"values":{"assign_ipv6_address_on_creation":false,"cidr_block":"10.0.48.0/20","customer_owned_ipv4_pool":null,"enable_dns64":false,"enable_resource_name_dns_a_record_on_launch":false,"enable_resource_name_dns_aaaa_record_on_launch":false,"ipv6_cidr_block":null,"ipv6_native":false,"map_customer_owned_ip_on_launch":null,"map_public_ip_on_launch":false,"outpost_arn":null,"tags":null,"timeouts":null},"sensitive_values":{"tags_all":{}}},{"address":"aws_subnet.ts_aws_subnet_public_igw","mode":"managed","type":"aws_subnet","name":"ts_aws_subnet_public_igw","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":1,"values":{"assign_ipv6_address_on_creation":false,"cidr_block":"10.0.16.0/20","customer_owned_ipv4_pool":null,"enable_dns64":false,"enable_resource_name_dns_a_record_on_launch":false,"enable_resource_name_dns_aaaa_record_on_launch":false,"ipv6_cidr_block":null,"ipv6_native":false,"map_customer_owned_ip_on_launch":null,"map_public_ip_on_launch":true,"outpost_arn":null,"tags":null,"timeouts":null},"sensitive_values":{"tags_all":{}}},{"address":"aws_vpc.ts_aws_vpc","mode":"managed","type":"aws_vpc","name":"ts_aws_vpc","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":1,"values":{"assign_generated_ipv6_cidr_block":null,"cidr_block":"10.0.0.0/16","enable_dns_hostnames":true,"enable_dns_support":true,"instance_tenancy":"default","ipv4_ipam_pool_id":null,"ipv4_netmask_length":null,"ipv6_ipam_pool_id":null,"ipv6_netmask_length":null,"tags":null},"sensitive_values":{"tags_all":{}}}]}},"resource_changes":[{"address":"aws_internet_gateway.ts_aws_internet_gateway","mode":"managed","type":"aws_internet_gateway","name":"ts_aws_internet_gateway","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"tags":null,"timeouts":null},"after_unknown":{"arn":true,"id":true,"owner_id":true,"tags_all":true,"vpc_id":true},"before_sensitive":false,"after_sensitive":{"tags_all":{}}}},{"address":"aws_route.ts_aws_route_public_igw","mode":"managed","type":"aws_route","name":"ts_aws_route_public_igw","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"carrier_gateway_id":null,"core_network_arn":null,"destination_cidr_block":"0.0.0.0/0","destination_ipv6_cidr_block":null,"destination_prefix_list_id":null,"egress_only_gateway_id":null,"local_gateway_id":null,"nat_gateway_id":null,"timeouts":null,"transit_gateway_id":null,"vpc_endpoint_id":null,"vpc_peering_connection_id":null},"after_unknown":{"gateway_id":true,"id":true,"instance_id":true,"instance_owner_id":true,"network_interface_id":true,"origin":true,"route_table_id":true,"state":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"aws_route_table.ts_aws_route_table_private_isolated","mode":"managed","type":"aws_route_table","name":"ts_aws_route_table_private_isolated","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"tags":null,"timeouts":null},"after_unknown":{"arn":true,"id":true,"owner_id":true,"propagating_vgws":true,"route":true,"tags_all":true,"vpc_id":true},"before_sensitive":false,"after_sensitive":{"propagating_vgws":[],"route":[],"tags_all":{}}}},{"address":"aws_route_table.ts_aws_route_table_public_igw","mode":"managed","type":"aws_route_table","name":"ts_aws_route_table_public_igw","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"tags":null,"timeouts":null},"after_unknown":{"arn":true,"id":true,"owner_id":true,"propagating_vgws":true,"route":true,"tags_all":true,"vpc_id":true},"before_sensitive":false,"after_sensitive":{"propagating_vgws":[],"route":[],"tags_all":{}}}},{"address":"aws_route_table_association.ts_aws_route_table_association_private_isolated","mode":"managed","type":"aws_route_table_association","name":"ts_aws_route_table_association_private_isolated","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"gateway_id":null},"after_unknown":{"id":true,"route_table_id":true,"subnet_id":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"aws_route_table_association.ts_aws_route_table_association_public_igw","mode":"managed","type":"aws_route_table_association","name":"ts_aws_route_table_association_public_igw","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"gateway_id":null},"after_unknown":{"id":true,"route_table_id":true,"subnet_id":true},"before_sensitive":false,"after_sensitive":{}}},{"address":"aws_subnet.ts_aws_subnet_private_isolated","mode":"managed","type":"aws_subnet","name":"ts_aws_subnet_private_isolated","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"assign_ipv6_address_on_creation":false,"cidr_block":"10.0.48.0/20","customer_owned_ipv4_pool":null,"enable_dns64":false,"enable_resource_name_dns_a_record_on_launch":false,"enable_resource_name_dns_aaaa_record_on_launch":false,"ipv6_cidr_block":null,"ipv6_native":false,"map_customer_owned_ip_on_launch":null,"map_public_ip_on_launch":false,"outpost_arn":null,"tags":null,"timeouts":null},"after_unknown":{"arn":true,"availability_zone":true,"availability_zone_id":true,"id":true,"ipv6_cidr_block_association_id":true,"owner_id":true,"private_dns_hostname_type_on_launch":true,"tags_all":true,"vpc_id":true},"before_sensitive":false,"after_sensitive":{"tags_all":{}}}},{"address":"aws_subnet.ts_aws_subnet_public_igw","mode":"managed","type":"aws_subnet","name":"ts_aws_subnet_public_igw","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"assign_ipv6_address_on_creation":false,"cidr_block":"10.0.16.0/20","customer_owned_ipv4_pool":null,"enable_dns64":false,"enable_resource_name_dns_a_record_on_launch":false,"enable_resource_name_dns_aaaa_record_on_launch":false,"ipv6_cidr_block":null,"ipv6_native":false,"map_customer_owned_ip_on_launch":null,"map_public_ip_on_launch":true,"outpost_arn":null,"tags":null,"timeouts":null},"after_unknown":{"arn":true,"availability_zone":true,"availability_zone_id":true,"id":true,"ipv6_cidr_block_association_id":true,"owner_id":true,"private_dns_hostname_type_on_launch":true,"tags_all":true,"vpc_id":true},"before_sensitive":false,"after_sensitive":{"tags_all":{}}}},{"address":"aws_vpc.ts_aws_vpc","mode":"managed","type":"aws_vpc","name":"ts_aws_vpc","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"assign_generated_ipv6_cidr_block":null,"cidr_block":"10.0.0.0/16","enable_dns_hostnames":true,"enable_dns_support":true,"instance_tenancy":"default","ipv4_ipam_pool_id":null,"ipv4_netmask_length":null,"ipv6_ipam_pool_id":null,"ipv6_netmask_length":null,"tags":null},"after_unknown":{"arn":true,"default_network_acl_id":true,"default_route_table_id":true,"default_security_group_id":true,"dhcp_options_id":true,"enable_classiclink":true,"enable_classiclink_dns_support":true,"enable_network_address_usage_metrics":true,"id":true,"ipv6_association_id":true,"ipv6_cidr_block":true,"ipv6_cidr_block_network_border_group":true,"main_route_table_id":true,"owner_id":true,"tags_all":true},"before_sensitive":false,"after_sensitive":{"tags_all":{}}}}],"configuration":{"provider_config":{"aws":{"name":"aws","full_name":"registry.terraform.io/hashicorp/aws"}},"root_module":{"resources":[{"address":"aws_internet_gateway.ts_aws_internet_gateway","mode":"managed","type":"aws_internet_gateway","name":"ts_aws_internet_gateway","provider_config_key":"aws","expressions":{"vpc_id":{"references":["aws_vpc.ts_aws_vpc.id","aws_vpc.ts_aws_vpc"]}},"schema_version":0},{"address":"aws_route.ts_aws_route_public_igw","mode":"managed","type":"aws_route","name":"ts_aws_route_public_igw","provider_config_key":"aws","expressions":{"destination_cidr_block":{"constant_value":"0.0.0.0/0"},"gateway_id":{"references":["aws_internet_gateway.ts_aws_internet_gateway.id","aws_internet_gateway.ts_aws_internet_gateway"]},"route_table_id":{"references":["aws_route_table.ts_aws_route_table_public_igw.id","aws_route_table.ts_aws_route_table_public_igw"]}},"schema_version":0},{"address":"aws_route_table.ts_aws_route_table_private_isolated","mode":"managed","type":"aws_route_table","name":"ts_aws_route_table_private_isolated","provider_config_key":"aws","expressions":{"vpc_id":{"references":["aws_vpc.ts_aws_vpc.id","aws_vpc.ts_aws_vpc"]}},"schema_version":0},{"address":"aws_route_table.ts_aws_route_table_public_igw","mode":"managed","type":"aws_route_table","name":"ts_aws_route_table_public_igw","provider_config_key":"aws","expressions":{"vpc_id":{"references":["aws_vpc.ts_aws_vpc.id","aws_vpc.ts_aws_vpc"]}},"schema_version":0},{"address":"aws_route_table_association.ts_aws_route_table_association_private_isolated","mode":"managed","type":"aws_route_table_association","name":"ts_aws_route_table_association_private_isolated","provider_config_key":"aws","expressions":{"route_table_id":{"references":["aws_route_table.ts_aws_route_table_private_isolated.id","aws_route_table.ts_aws_route_table_private_isolated"]},"subnet_id":{"references":["aws_subnet.ts_aws_subnet_private_isolated.id","aws_subnet.ts_aws_subnet_private_isolated"]}},"schema_version":0},{"address":"aws_route_table_association.ts_aws_route_table_association_public_igw","mode":"managed","type":"aws_route_table_association","name":"ts_aws_route_table_association_public_igw","provider_config_key":"aws","expressions":{"route_table_id":{"references":["aws_route_table.ts_aws_route_table_public_igw.id","aws_route_table.ts_aws_route_table_public_igw"]},"subnet_id":{"references":["aws_subnet.ts_aws_subnet_public_igw.id","aws_subnet.ts_aws_subnet_public_igw"]}},"schema_version":0},{"address":"aws_subnet.ts_aws_subnet_private_isolated","mode":"managed","type":"aws_subnet","name":"ts_aws_subnet_private_isolated","provider_config_key":"aws","expressions":{"cidr_block":{"references":["aws_vpc.ts_aws_vpc.cidr_block","aws_vpc.ts_aws_vpc","var.ts_aws_vpc_cidr_newbits"]},"vpc_id":{"references":["aws_vpc.ts_aws_vpc.id","aws_vpc.ts_aws_vpc"]}},"schema_version":1},{"address":"aws_subnet.ts_aws_subnet_public_igw","mode":"managed","type":"aws_subnet","name":"ts_aws_subnet_public_igw","provider_config_key":"aws","expressions":{"cidr_block":{"references":["aws_vpc.ts_aws_vpc.cidr_block","aws_vpc.ts_aws_vpc","var.ts_aws_vpc_cidr_newbits"]},"map_public_ip_on_launch":{"constant_value":true},"vpc_id":{"references":["aws_vpc.ts_aws_vpc.id","aws_vpc.ts_aws_vpc"]}},"schema_version":1},{"address":"aws_vpc.ts_aws_vpc","mode":"managed","type":"aws_vpc","name":"ts_aws_vpc","provider_config_key":"aws","expressions":{"cidr_block":{"references":["var.ts_aws_vpc_cidr_block"]},"enable_dns_hostnames":{"constant_value":true},"instance_tenancy":{"constant_value":"default"}},"schema_version":1}],"variables":{"other_aws_vpc_cidr_block":{"default":"10.0.0.0/16","description":"TinyStacks AWS VPC CIDR block"},"other_aws_vpc_cidr_newbits":{"default":4,"description":"TinyStacks AWS VPC CIDR new bits"},"ts_aws_vpc_cidr_block":{"default":"10.0.0.0/16","description":"TinyStacks AWS VPC CIDR block"},"ts_aws_vpc_cidr_newbits":{"default":4,"description":"TinyStacks AWS VPC CIDR new bits"}}}},"relevant_attributes":[{"resource":"aws_internet_gateway.ts_aws_internet_gateway","attribute":["id"]},{"resource":"aws_vpc.ts_aws_vpc","attribute":["id"]},{"resource":"aws_vpc.ts_aws_vpc","attribute":["cidr_block"]},{"resource":"aws_route_table.ts_aws_route_table_private_isolated","attribute":["id"]},{"resource":"aws_subnet.ts_aws_subnet_private_isolated","attribute":["id"]},{"resource":"aws_route_table.ts_aws_route_table_public_igw","attribute":["id"]},{"resource":"aws_subnet.ts_aws_subnet_public_igw","attribute":["id"]}]} 2 | -------------------------------------------------------------------------------- /test/commands/check/test-data/vpc-stack/tf/no-nat/tf-diff.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "format": "tf", 4 | "resourceType": "aws_internet_gateway", 5 | "changeType": "CREATE", 6 | "address": "aws_internet_gateway.ts_aws_internet_gateway", 7 | "logicalId": "ts_aws_internet_gateway", 8 | "providerName": "registry.terraform.io/hashicorp/aws" 9 | }, 10 | { 11 | "format": "tf", 12 | "resourceType": "aws_route", 13 | "changeType": "CREATE", 14 | "address": "aws_route.ts_aws_route_public_igw", 15 | "logicalId": "ts_aws_route_public_igw", 16 | "providerName": "registry.terraform.io/hashicorp/aws", 17 | "properties": { 18 | "destinationCidrBlock": "0.0.0.0/0", 19 | "gatewayId": "${aws_internet_gateway.ts_aws_internet_gateway.id}" 20 | } 21 | }, 22 | { 23 | "format": "tf", 24 | "resourceType": "aws_route_table", 25 | "changeType": "CREATE", 26 | "address": "aws_route_table.ts_aws_route_table_private_isolated", 27 | "logicalId": "ts_aws_route_table_private_isolated", 28 | "providerName": "registry.terraform.io/hashicorp/aws", 29 | "properties": { 30 | "associationSet": [ 31 | { 32 | "routeTableId": "${aws_route_table.ts_aws_route_table_private_isolated.id}", 33 | "subnetId": "${aws_subnet.ts_aws_subnet_private_isolated.id}" 34 | } 35 | ], 36 | "routeSet": [], 37 | "vpcId": "${aws_vpc.ts_aws_vpc.id}" 38 | } 39 | }, 40 | { 41 | "format": "tf", 42 | "resourceType": "aws_route_table", 43 | "changeType": "CREATE", 44 | "address": "aws_route_table.ts_aws_route_table_public_igw", 45 | "logicalId": "ts_aws_route_table_public_igw", 46 | "providerName": "registry.terraform.io/hashicorp/aws", 47 | "properties": { 48 | "associationSet": [ 49 | { 50 | "routeTableId": "${aws_route_table.ts_aws_route_table_public_igw.id}", 51 | "subnetId": "${aws_subnet.ts_aws_subnet_public_igw.id}" 52 | } 53 | ], 54 | "routeSet": [ 55 | { 56 | "destinationCidrBlock": "0.0.0.0/0", 57 | "gatewayId": "${aws_internet_gateway.ts_aws_internet_gateway.id}" 58 | } 59 | ], 60 | "vpcId": "${aws_vpc.ts_aws_vpc.id}" 61 | } 62 | }, 63 | { 64 | "format": "tf", 65 | "resourceType": "aws_route_table_association", 66 | "changeType": "CREATE", 67 | "address": "aws_route_table_association.ts_aws_route_table_association_private_isolated", 68 | "logicalId": "ts_aws_route_table_association_private_isolated", 69 | "providerName": "registry.terraform.io/hashicorp/aws", 70 | "properties": { 71 | "routeTableId": "${aws_route_table.ts_aws_route_table_private_isolated.id}", 72 | "subnetId": "${aws_subnet.ts_aws_subnet_private_isolated.id}" 73 | } 74 | }, 75 | { 76 | "format": "tf", 77 | "resourceType": "aws_route_table_association", 78 | "changeType": "CREATE", 79 | "address": "aws_route_table_association.ts_aws_route_table_association_public_igw", 80 | "logicalId": "ts_aws_route_table_association_public_igw", 81 | "providerName": "registry.terraform.io/hashicorp/aws", 82 | "properties": { 83 | "routeTableId": "${aws_route_table.ts_aws_route_table_public_igw.id}", 84 | "subnetId": "${aws_subnet.ts_aws_subnet_public_igw.id}" 85 | } 86 | }, 87 | { 88 | "format": "tf", 89 | "resourceType": "aws_subnet", 90 | "changeType": "CREATE", 91 | "address": "aws_subnet.ts_aws_subnet_private_isolated", 92 | "logicalId": "ts_aws_subnet_private_isolated", 93 | "providerName": "registry.terraform.io/hashicorp/aws", 94 | "properties": { 95 | "assignIpv6AddressOnCreation": false, 96 | "cidrBlock": "10.0.48.0/20", 97 | "enableDns64": false, 98 | "ipv6Native": false, 99 | "mapPublicIpOnLaunch": false, 100 | "vpcId": "${aws_vpc.ts_aws_vpc.id}" 101 | } 102 | }, 103 | { 104 | "format": "tf", 105 | "resourceType": "aws_subnet", 106 | "changeType": "CREATE", 107 | "address": "aws_subnet.ts_aws_subnet_public_igw", 108 | "logicalId": "ts_aws_subnet_public_igw", 109 | "providerName": "registry.terraform.io/hashicorp/aws", 110 | "properties": { 111 | "assignIpv6AddressOnCreation": false, 112 | "cidrBlock": "10.0.16.0/20", 113 | "enableDns64": false, 114 | "ipv6Native": false, 115 | "mapPublicIpOnLaunch": true, 116 | "vpcId": "${aws_vpc.ts_aws_vpc.id}" 117 | } 118 | }, 119 | { 120 | "format": "tf", 121 | "resourceType": "aws_vpc", 122 | "changeType": "CREATE", 123 | "address": "aws_vpc.ts_aws_vpc", 124 | "logicalId": "ts_aws_vpc", 125 | "providerName": "registry.terraform.io/hashicorp/aws", 126 | "properties": { 127 | "cidrBlock": "10.0.0.0/16", 128 | "instanceTenancy": "default" 129 | } 130 | } 131 | ] -------------------------------------------------------------------------------- /test/commands/check/test-data/vpc-stack/tf/no-nat/tf-json.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "direct-main.tf", 4 | "contents": { 5 | "resource": { 6 | "aws_internet_gateway": { 7 | "ts_aws_internet_gateway": [ 8 | { 9 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 10 | } 11 | ] 12 | }, 13 | "aws_route": { 14 | "ts_aws_route_public_igw": [ 15 | { 16 | "destination_cidr_block": "0.0.0.0/0", 17 | "gateway_id": "${aws_internet_gateway.ts_aws_internet_gateway.id}", 18 | "route_table_id": "${aws_route_table.ts_aws_route_table_public_igw.id}" 19 | } 20 | ] 21 | }, 22 | "aws_route_table": { 23 | "ts_aws_route_table_private_isolated": [ 24 | { 25 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 26 | } 27 | ], 28 | "ts_aws_route_table_public_igw": [ 29 | { 30 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 31 | } 32 | ] 33 | }, 34 | "aws_route_table_association": { 35 | "ts_aws_route_table_association_private_isolated": [ 36 | { 37 | "route_table_id": "${aws_route_table.ts_aws_route_table_private_isolated.id}", 38 | "subnet_id": "${aws_subnet.ts_aws_subnet_private_isolated.id}" 39 | } 40 | ], 41 | "ts_aws_route_table_association_public_igw": [ 42 | { 43 | "route_table_id": "${aws_route_table.ts_aws_route_table_public_igw.id}", 44 | "subnet_id": "${aws_subnet.ts_aws_subnet_public_igw.id}" 45 | } 46 | ] 47 | }, 48 | "aws_subnet": { 49 | "ts_aws_subnet_private_isolated": [ 50 | { 51 | "cidr_block": "${cidrsubnet(aws_vpc.ts_aws_vpc.cidr_block, var.ts_aws_vpc_cidr_newbits, 3)}", 52 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 53 | } 54 | ], 55 | "ts_aws_subnet_public_igw": [ 56 | { 57 | "cidr_block": "${cidrsubnet(aws_vpc.ts_aws_vpc.cidr_block, var.ts_aws_vpc_cidr_newbits, 1)}", 58 | "map_public_ip_on_launch": true, 59 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 60 | } 61 | ] 62 | }, 63 | "aws_vpc": { 64 | "ts_aws_vpc": [ 65 | { 66 | "cidr_block": "${var.ts_aws_vpc_cidr_block}", 67 | "enable_dns_hostnames": true, 68 | "instance_tenancy": "default" 69 | } 70 | ] 71 | } 72 | } 73 | } 74 | } 75 | ] -------------------------------------------------------------------------------- /test/commands/check/test-data/vpc-stack/tf/with-nat/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_vpc" "ts_aws_vpc" { 2 | cidr_block = var.ts_aws_vpc_cidr_block 3 | instance_tenancy = "default" 4 | enable_dns_hostnames = true 5 | } 6 | 7 | /* */ 8 | 9 | resource "aws_subnet" "ts_aws_subnet_public_igw" { 10 | 11 | vpc_id = aws_vpc.ts_aws_vpc.id 12 | cidr_block = cidrsubnet(aws_vpc.ts_aws_vpc.cidr_block, var.ts_aws_vpc_cidr_newbits, 1) 13 | 14 | map_public_ip_on_launch = true 15 | 16 | } 17 | 18 | resource "aws_route_table" "ts_aws_route_table_public_igw" { 19 | 20 | vpc_id = aws_vpc.ts_aws_vpc.id 21 | 22 | } 23 | 24 | resource "aws_route_table_association" "ts_aws_route_table_association_public_igw" { 25 | 26 | subnet_id = aws_subnet.ts_aws_subnet_public_igw.id 27 | route_table_id = aws_route_table.ts_aws_route_table_public_igw.id 28 | 29 | } 30 | 31 | resource "aws_internet_gateway" "ts_aws_internet_gateway" { 32 | 33 | vpc_id = aws_vpc.ts_aws_vpc.id 34 | 35 | } 36 | 37 | resource "aws_route" "ts_aws_route_public_igw" { 38 | 39 | route_table_id = aws_route_table.ts_aws_route_table_public_igw.id 40 | destination_cidr_block = "0.0.0.0/0" 41 | gateway_id = aws_internet_gateway.ts_aws_internet_gateway.id 42 | 43 | } 44 | 45 | /* */ 46 | 47 | resource "aws_subnet" "ts_aws_subnet_private_ngw" { 48 | 49 | vpc_id = aws_vpc.ts_aws_vpc.id 50 | cidr_block = cidrsubnet(aws_vpc.ts_aws_vpc.cidr_block, var.ts_aws_vpc_cidr_newbits, 2) 51 | 52 | } 53 | 54 | resource "aws_route_table" "ts_aws_route_table_private_ngw" { 55 | 56 | vpc_id = aws_vpc.ts_aws_vpc.id 57 | 58 | } 59 | 60 | resource "aws_route_table_association" "ts_aws_route_table_association_private_ngw" { 61 | 62 | subnet_id = aws_subnet.ts_aws_subnet_private_ngw.id 63 | route_table_id = aws_route_table.ts_aws_route_table_private_ngw.id 64 | 65 | } 66 | 67 | resource "aws_eip" "ts_aws_eip_nat" { 68 | 69 | vpc = true 70 | 71 | } 72 | 73 | resource "aws_nat_gateway" "ts_aws_nat_gateway" { 74 | 75 | subnet_id = aws_subnet.ts_aws_subnet_public_igw.id 76 | allocation_id = aws_eip.ts_aws_eip_nat.id 77 | 78 | depends_on = [aws_internet_gateway.ts_aws_internet_gateway] 79 | 80 | } 81 | 82 | resource "aws_route" "ts_aws_route_private_ngw" { 83 | 84 | route_table_id = aws_route_table.ts_aws_route_table_private_ngw.id 85 | destination_cidr_block = "0.0.0.0/0" 86 | nat_gateway_id = aws_nat_gateway.ts_aws_nat_gateway.id 87 | 88 | } 89 | 90 | /* */ 91 | 92 | resource "aws_subnet" "ts_aws_subnet_private_isolated" { 93 | 94 | vpc_id = aws_vpc.ts_aws_vpc.id 95 | cidr_block = cidrsubnet(aws_vpc.ts_aws_vpc.cidr_block, var.ts_aws_vpc_cidr_newbits, 3) 96 | 97 | } 98 | 99 | resource "aws_route_table" "ts_aws_route_table_private_isolated" { 100 | 101 | vpc_id = aws_vpc.ts_aws_vpc.id 102 | 103 | } 104 | 105 | resource "aws_route_table_association" "ts_aws_route_table_association_private_isolated" { 106 | 107 | subnet_id = aws_subnet.ts_aws_subnet_private_isolated.id 108 | route_table_id = aws_route_table.ts_aws_route_table_private_isolated.id 109 | 110 | } 111 | 112 | resource "aws_s3_bucket" "ts_bucket" { 113 | 114 | bucket = "check-bucket" 115 | 116 | } 117 | 118 | resource "aws_sqs_queue" "ts_queue" { 119 | name = "check-queue" 120 | visibility_timeout = 45 121 | } -------------------------------------------------------------------------------- /test/commands/check/test-data/vpc-stack/tf/with-nat/tf-diff.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "format": "tf", 4 | "resourceType": "aws_eip", 5 | "changeType": "CREATE", 6 | "address": "aws_eip.ts_aws_eip_nat", 7 | "logicalId": "ts_aws_eip_nat", 8 | "providerName": "registry.terraform.io/hashicorp/aws", 9 | "properties": { 10 | "domain": "vpc" 11 | } 12 | }, 13 | { 14 | "format": "tf", 15 | "resourceType": "aws_internet_gateway", 16 | "changeType": "CREATE", 17 | "address": "aws_internet_gateway.ts_aws_internet_gateway", 18 | "logicalId": "ts_aws_internet_gateway", 19 | "providerName": "registry.terraform.io/hashicorp/aws" 20 | }, 21 | { 22 | "format": "tf", 23 | "resourceType": "aws_nat_gateway", 24 | "changeType": "CREATE", 25 | "address": "aws_nat_gateway.ts_aws_nat_gateway", 26 | "logicalId": "ts_aws_nat_gateway", 27 | "providerName": "registry.terraform.io/hashicorp/aws", 28 | "properties": { 29 | "connectivityType": "public", 30 | "subnetId": "${aws_subnet.ts_aws_subnet_public_igw.id}" 31 | } 32 | }, 33 | { 34 | "format": "tf", 35 | "resourceType": "aws_route", 36 | "changeType": "CREATE", 37 | "address": "aws_route.ts_aws_route_private_ngw", 38 | "logicalId": "ts_aws_route_private_ngw", 39 | "providerName": "registry.terraform.io/hashicorp/aws", 40 | "properties": { 41 | "destinationCidrBlock": "0.0.0.0/0", 42 | "natGatewayId": "${aws_nat_gateway.ts_aws_nat_gateway.id}" 43 | } 44 | }, 45 | { 46 | "format": "tf", 47 | "resourceType": "aws_route", 48 | "changeType": "CREATE", 49 | "address": "aws_route.ts_aws_route_public_igw", 50 | "logicalId": "ts_aws_route_public_igw", 51 | "providerName": "registry.terraform.io/hashicorp/aws", 52 | "properties": { 53 | "destinationCidrBlock": "0.0.0.0/0", 54 | "gatewayId": "${aws_internet_gateway.ts_aws_internet_gateway.id}" 55 | } 56 | }, 57 | { 58 | "format": "tf", 59 | "resourceType": "aws_route_table", 60 | "changeType": "CREATE", 61 | "address": "aws_route_table.ts_aws_route_table_private_isolated", 62 | "logicalId": "ts_aws_route_table_private_isolated", 63 | "providerName": "registry.terraform.io/hashicorp/aws", 64 | "properties": { 65 | "associationSet": [ 66 | { 67 | "routeTableId": "${aws_route_table.ts_aws_route_table_private_isolated.id}", 68 | "subnetId": "${aws_subnet.ts_aws_subnet_private_isolated.id}" 69 | } 70 | ], 71 | "routeSet": [], 72 | "vpcId": "${aws_vpc.ts_aws_vpc.id}" 73 | } 74 | }, 75 | { 76 | "format": "tf", 77 | "resourceType": "aws_route_table", 78 | "changeType": "CREATE", 79 | "address": "aws_route_table.ts_aws_route_table_private_ngw", 80 | "logicalId": "ts_aws_route_table_private_ngw", 81 | "providerName": "registry.terraform.io/hashicorp/aws", 82 | "properties": { 83 | "associationSet": [ 84 | { 85 | "routeTableId": "${aws_route_table.ts_aws_route_table_private_ngw.id}", 86 | "subnetId": "${aws_subnet.ts_aws_subnet_private_ngw.id}" 87 | } 88 | ], 89 | "routeSet": [ 90 | { 91 | "destinationCidrBlock": "0.0.0.0/0", 92 | "natGatewayId": "${aws_nat_gateway.ts_aws_nat_gateway.id}" 93 | } 94 | ], 95 | "vpcId": "${aws_vpc.ts_aws_vpc.id}" 96 | } 97 | }, 98 | { 99 | "format": "tf", 100 | "resourceType": "aws_route_table", 101 | "changeType": "CREATE", 102 | "address": "aws_route_table.ts_aws_route_table_public_igw", 103 | "logicalId": "ts_aws_route_table_public_igw", 104 | "providerName": "registry.terraform.io/hashicorp/aws", 105 | "properties": { 106 | "associationSet": [ 107 | { 108 | "routeTableId": "${aws_route_table.ts_aws_route_table_public_igw.id}", 109 | "subnetId": "${aws_subnet.ts_aws_subnet_public_igw.id}" 110 | } 111 | ], 112 | "routeSet": [ 113 | { 114 | "destinationCidrBlock": "0.0.0.0/0", 115 | "gatewayId": "${aws_internet_gateway.ts_aws_internet_gateway.id}" 116 | } 117 | ], 118 | "vpcId": "${aws_vpc.ts_aws_vpc.id}" 119 | } 120 | }, 121 | { 122 | "format": "tf", 123 | "resourceType": "aws_route_table_association", 124 | "changeType": "CREATE", 125 | "address": "aws_route_table_association.ts_aws_route_table_association_private_isolated", 126 | "logicalId": "ts_aws_route_table_association_private_isolated", 127 | "providerName": "registry.terraform.io/hashicorp/aws", 128 | "properties": { 129 | "routeTableId": "${aws_route_table.ts_aws_route_table_private_isolated.id}", 130 | "subnetId": "${aws_subnet.ts_aws_subnet_private_isolated.id}" 131 | } 132 | }, 133 | { 134 | "format": "tf", 135 | "resourceType": "aws_route_table_association", 136 | "changeType": "CREATE", 137 | "address": "aws_route_table_association.ts_aws_route_table_association_private_ngw", 138 | "logicalId": "ts_aws_route_table_association_private_ngw", 139 | "providerName": "registry.terraform.io/hashicorp/aws", 140 | "properties": { 141 | "routeTableId": "${aws_route_table.ts_aws_route_table_private_ngw.id}", 142 | "subnetId": "${aws_subnet.ts_aws_subnet_private_ngw.id}" 143 | } 144 | }, 145 | { 146 | "format": "tf", 147 | "resourceType": "aws_route_table_association", 148 | "changeType": "CREATE", 149 | "address": "aws_route_table_association.ts_aws_route_table_association_public_igw", 150 | "logicalId": "ts_aws_route_table_association_public_igw", 151 | "providerName": "registry.terraform.io/hashicorp/aws", 152 | "properties": { 153 | "routeTableId": "${aws_route_table.ts_aws_route_table_public_igw.id}", 154 | "subnetId": "${aws_subnet.ts_aws_subnet_public_igw.id}" 155 | } 156 | }, 157 | { 158 | "format": "tf", 159 | "resourceType": "aws_subnet", 160 | "changeType": "CREATE", 161 | "address": "aws_subnet.ts_aws_subnet_private_isolated", 162 | "logicalId": "ts_aws_subnet_private_isolated", 163 | "providerName": "registry.terraform.io/hashicorp/aws", 164 | "properties": { 165 | "assignIpv6AddressOnCreation": false, 166 | "cidrBlock": "10.0.48.0/20", 167 | "enableDns64": false, 168 | "ipv6Native": false, 169 | "mapPublicIpOnLaunch": false, 170 | "vpcId": "${aws_vpc.ts_aws_vpc.id}" 171 | } 172 | }, 173 | { 174 | "format": "tf", 175 | "resourceType": "aws_subnet", 176 | "changeType": "CREATE", 177 | "address": "aws_subnet.ts_aws_subnet_private_ngw", 178 | "logicalId": "ts_aws_subnet_private_ngw", 179 | "providerName": "registry.terraform.io/hashicorp/aws", 180 | "properties": { 181 | "assignIpv6AddressOnCreation": false, 182 | "cidrBlock": "10.0.32.0/20", 183 | "enableDns64": false, 184 | "ipv6Native": false, 185 | "mapPublicIpOnLaunch": false, 186 | "vpcId": "${aws_vpc.ts_aws_vpc.id}" 187 | } 188 | }, 189 | { 190 | "format": "tf", 191 | "resourceType": "aws_subnet", 192 | "changeType": "CREATE", 193 | "address": "aws_subnet.ts_aws_subnet_public_igw", 194 | "logicalId": "ts_aws_subnet_public_igw", 195 | "providerName": "registry.terraform.io/hashicorp/aws", 196 | "properties": { 197 | "assignIpv6AddressOnCreation": false, 198 | "cidrBlock": "10.0.16.0/20", 199 | "enableDns64": false, 200 | "ipv6Native": false, 201 | "mapPublicIpOnLaunch": true, 202 | "vpcId": "${aws_vpc.ts_aws_vpc.id}" 203 | } 204 | }, 205 | { 206 | "format": "tf", 207 | "resourceType": "aws_vpc", 208 | "changeType": "CREATE", 209 | "address": "aws_vpc.ts_aws_vpc", 210 | "logicalId": "ts_aws_vpc", 211 | "providerName": "registry.terraform.io/hashicorp/aws", 212 | "properties": { 213 | "cidrBlock": "10.0.0.0/16", 214 | "instanceTenancy": "default" 215 | } 216 | } 217 | ] -------------------------------------------------------------------------------- /test/commands/check/test-data/vpc-stack/tf/with-nat/tf-json.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "main.tf", 4 | "contents": { 5 | "resource": { 6 | "aws_eip": { 7 | "ts_aws_eip_nat": [ 8 | { 9 | "vpc": true 10 | } 11 | ] 12 | }, 13 | "aws_internet_gateway": { 14 | "ts_aws_internet_gateway": [ 15 | { 16 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 17 | } 18 | ] 19 | }, 20 | "aws_nat_gateway": { 21 | "ts_aws_nat_gateway": [ 22 | { 23 | "allocation_id": "${aws_eip.ts_aws_eip_nat.id}", 24 | "depends_on": [ 25 | "${aws_internet_gateway.ts_aws_internet_gateway}" 26 | ], 27 | "subnet_id": "${aws_subnet.ts_aws_subnet_public_igw.id}" 28 | } 29 | ] 30 | }, 31 | "aws_route": { 32 | "ts_aws_route_private_ngw": [ 33 | { 34 | "destination_cidr_block": "0.0.0.0/0", 35 | "nat_gateway_id": "${aws_nat_gateway.ts_aws_nat_gateway.id}", 36 | "route_table_id": "${aws_route_table.ts_aws_route_table_private_ngw.id}" 37 | } 38 | ], 39 | "ts_aws_route_public_igw": [ 40 | { 41 | "destination_cidr_block": "0.0.0.0/0", 42 | "gateway_id": "${aws_internet_gateway.ts_aws_internet_gateway.id}", 43 | "route_table_id": "${aws_route_table.ts_aws_route_table_public_igw.id}" 44 | } 45 | ] 46 | }, 47 | "aws_route_table": { 48 | "ts_aws_route_table_private_isolated": [ 49 | { 50 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 51 | } 52 | ], 53 | "ts_aws_route_table_private_ngw": [ 54 | { 55 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 56 | } 57 | ], 58 | "ts_aws_route_table_public_igw": [ 59 | { 60 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 61 | } 62 | ] 63 | }, 64 | "aws_route_table_association": { 65 | "ts_aws_route_table_association_private_isolated": [ 66 | { 67 | "route_table_id": "${aws_route_table.ts_aws_route_table_private_isolated.id}", 68 | "subnet_id": "${aws_subnet.ts_aws_subnet_private_isolated.id}" 69 | } 70 | ], 71 | "ts_aws_route_table_association_private_ngw": [ 72 | { 73 | "route_table_id": "${aws_route_table.ts_aws_route_table_private_ngw.id}", 74 | "subnet_id": "${aws_subnet.ts_aws_subnet_private_ngw.id}" 75 | } 76 | ], 77 | "ts_aws_route_table_association_public_igw": [ 78 | { 79 | "route_table_id": "${aws_route_table.ts_aws_route_table_public_igw.id}", 80 | "subnet_id": "${aws_subnet.ts_aws_subnet_public_igw.id}" 81 | } 82 | ] 83 | }, 84 | "aws_subnet": { 85 | "ts_aws_subnet_private_isolated": [ 86 | { 87 | "cidr_block": "${cidrsubnet(aws_vpc.ts_aws_vpc.cidr_block, var.ts_aws_vpc_cidr_newbits, 3)}", 88 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 89 | } 90 | ], 91 | "ts_aws_subnet_private_ngw": [ 92 | { 93 | "cidr_block": "${cidrsubnet(aws_vpc.ts_aws_vpc.cidr_block, var.ts_aws_vpc_cidr_newbits, 2)}", 94 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 95 | } 96 | ], 97 | "ts_aws_subnet_public_igw": [ 98 | { 99 | "cidr_block": "${cidrsubnet(aws_vpc.ts_aws_vpc.cidr_block, var.ts_aws_vpc_cidr_newbits, 1)}", 100 | "map_public_ip_on_launch": true, 101 | "vpc_id": "${aws_vpc.ts_aws_vpc.id}" 102 | } 103 | ] 104 | }, 105 | "aws_vpc": { 106 | "ts_aws_vpc": [ 107 | { 108 | "cidr_block": "${var.ts_aws_vpc_cidr_block}", 109 | "enable_dns_hostnames": true, 110 | "instance_tenancy": "default" 111 | } 112 | ] 113 | }, 114 | "aws_s3_bucket": { 115 | "ts_bucket": [ 116 | { 117 | "bucket": "check-bucket" 118 | } 119 | ] 120 | }, 121 | "aws_sqs_queue": { 122 | "ts_queue": [ 123 | { 124 | "name": "check-queue", 125 | "visibility_timeout": 45 126 | } 127 | ] 128 | } 129 | } 130 | } 131 | } 132 | ] -------------------------------------------------------------------------------- /test/hooks/cleanup-tmp-directory/index.test.ts: -------------------------------------------------------------------------------- 1 | const mockRmSync = jest.fn(); 2 | 3 | jest.mock('fs', () => ({ 4 | rmSync: mockRmSync 5 | })); 6 | 7 | import { 8 | cleanupTmpDirectory 9 | } from '../../../src/hooks/cleanup-tmp-directory'; 10 | 11 | test('cleanupTmpDirectory', () => { 12 | cleanupTmpDirectory(); 13 | 14 | expect(mockRmSync).toBeCalled(); 15 | expect(mockRmSync).toBeCalledWith('/tmp/precloud/tmp', { recursive: true, force: true }); 16 | }); -------------------------------------------------------------------------------- /test/logger/index.test.ts: -------------------------------------------------------------------------------- 1 | const mockRed = jest.fn(); 2 | const mockMagenta = jest.fn(); 3 | const mockYellow = jest.fn(); 4 | const mockBlue = jest.fn(); 5 | const mockGray = jest.fn(); 6 | const mockGreen = jest.fn(); 7 | const mockConsole = jest.fn(); 8 | 9 | jest.mock('colors', () => ({ 10 | red: mockRed, 11 | magenta: mockMagenta, 12 | yellow: mockYellow, 13 | blue: mockBlue, 14 | gray: mockGray, 15 | green: mockGreen 16 | })); 17 | 18 | import { CliError } from '../../src/errors'; 19 | import logger from '../../src/logger'; 20 | 21 | describe('logger', () => { 22 | beforeEach(() => { 23 | mockRed.mockImplementation(message => message); 24 | mockMagenta.mockImplementation(message => message); 25 | mockYellow.mockImplementation(message => message); 26 | mockBlue.mockImplementation(message => message); 27 | mockGray.mockImplementation(message => message); 28 | mockGreen.mockImplementation(message => message); 29 | 30 | jest.spyOn(global.console, 'error').mockImplementation(mockConsole); 31 | jest.spyOn(global.console, 'debug').mockImplementation(mockConsole); 32 | jest.spyOn(global.console, 'warn').mockImplementation(mockConsole); 33 | jest.spyOn(global.console, 'info').mockImplementation(mockConsole); 34 | jest.spyOn(global.console, 'log').mockImplementation(mockConsole); 35 | }); 36 | 37 | afterEach(() => { 38 | // for mocks 39 | jest.resetAllMocks(); 40 | // for spies 41 | jest.restoreAllMocks(); 42 | }); 43 | 44 | it('error', () => { 45 | logger.error('Error!'); 46 | 47 | expect(console.error).toBeCalled(); 48 | expect(console.error).toBeCalledWith('Error: Error!'); 49 | expect(mockRed).toBeCalled(); 50 | expect(mockRed).toBeCalledWith('Error: Error!'); 51 | }); 52 | 53 | it('debug', () => { 54 | logger.debug('Debug'); 55 | 56 | expect(console.debug).toBeCalled(); 57 | expect(console.debug).toBeCalledWith('Debug: Debug'); 58 | expect(mockYellow).toBeCalled(); 59 | expect(mockYellow).toBeCalledWith('Debug: Debug'); 60 | }); 61 | 62 | it('warn', () => { 63 | logger.warn('Warn'); 64 | 65 | expect(console.warn).toBeCalled(); 66 | expect(console.warn).toBeCalledWith('Warning: Warn'); 67 | expect(mockYellow).toBeCalled(); 68 | expect(mockYellow).toBeCalledWith('Warning: Warn'); 69 | }); 70 | 71 | it('info', () => { 72 | logger.info('Info'); 73 | 74 | expect(console.info).toBeCalled(); 75 | expect(console.info).toBeCalledWith('Info: Info'); 76 | expect(mockBlue).toBeCalled(); 77 | expect(mockBlue).toBeCalledWith('Info: Info'); 78 | }); 79 | 80 | it('log', () => { 81 | logger.log('Log'); 82 | 83 | expect(console.log).toBeCalled(); 84 | expect(console.log).toBeCalledWith('Log'); 85 | expect(mockGray).toBeCalled(); 86 | expect(mockGray).toBeCalledWith('Log'); 87 | }); 88 | 89 | it('hint', () => { 90 | logger.hint('Hint'); 91 | 92 | expect(console.log).toBeCalled(); 93 | expect(console.log).toBeCalledWith('Hint: Hint'); 94 | expect(mockMagenta).toBeCalled(); 95 | expect(mockMagenta).toBeCalledWith('Hint: Hint'); 96 | }); 97 | 98 | it('success', () => { 99 | logger.success('Success'); 100 | 101 | expect(console.log).toBeCalled(); 102 | expect(console.log).toBeCalledWith('Success: Success'); 103 | expect(mockGreen).toBeCalled(); 104 | expect(mockGreen).toBeCalledWith('Success: Success'); 105 | }); 106 | 107 | describe('verbose', () => { 108 | it('logs if VERBOSE env var is true', () => { 109 | process.env.VERBOSE = 'true'; 110 | logger.verbose('Verbose'); 111 | expect(console.log).toBeCalled(); 112 | expect(console.log).toBeCalledWith('Verbose'); 113 | expect(mockGray).toBeCalled(); 114 | expect(mockGray).toBeCalledWith('Verbose'); 115 | }); 116 | it('does not log if VERBOSE env var is false', () => { 117 | process.env.VERBOSE = 'false'; 118 | logger.verbose('Verbose'); 119 | expect(console.log).not.toBeCalled(); 120 | expect(mockGray).not.toBeCalled(); 121 | }); 122 | it('does not log if VERBOSE env var is undefined', () => { 123 | process.env.VERBOSE = undefined; 124 | logger.verbose('Verbose'); 125 | expect(console.log).not.toBeCalled(); 126 | expect(mockGray).not.toBeCalled(); 127 | }); 128 | }); 129 | 130 | describe('cliError', () => { 131 | beforeEach(() => { 132 | jest.spyOn(logger, 'error').mockImplementation(jest.fn()); 133 | jest.spyOn(logger, 'hint').mockImplementation(jest.fn()); 134 | jest.spyOn(global.console, 'error').mockImplementation(jest.fn()); 135 | }); 136 | it ('logs with special format if the error is a CliError', () => { 137 | const error = new CliError('Error!', 'Test error.', 'Hint 1', 'hint 2'); 138 | 139 | logger.cliError(error); 140 | 141 | expect(logger.error).toBeCalled(); 142 | expect(logger.error).toBeCalledWith('Error!\n\tTest error.'); 143 | expect(logger.hint).toBeCalledTimes(2); 144 | expect(logger.hint).toBeCalledWith('\tHint 1'); 145 | expect(logger.hint).toBeCalledWith('\thint 2'); 146 | expect(global.console.error).not.toBeCalled(); 147 | }); 148 | it ('logs unexpected error if the error is not a CliError', () => { 149 | const error = new Error('Error!'); 150 | 151 | logger.cliError(error); 152 | 153 | expect(logger.error).toBeCalled(); 154 | expect(logger.error).toBeCalledWith('An unexpected error occurred!'); 155 | expect(logger.hint).not.toBeCalled(); 156 | expect(global.console.error).toBeCalled(); 157 | expect(global.console.error).toBeCalledWith(error); 158 | }); 159 | }); 160 | }); -------------------------------------------------------------------------------- /test/utils/dont-return-empty.test.ts: -------------------------------------------------------------------------------- 1 | import { Json } from '../../src/types'; 2 | import { dontReturnEmpty } from '../../src/utils/dont-return-empty'; 3 | 4 | describe('dontReturnEmpty', () => { 5 | it('returns undefined if properties is not an object', () => { 6 | const undefInput = undefined as unknown as Json; 7 | const undefResult = dontReturnEmpty(undefInput); 8 | expect(undefResult).toEqual(undefined); 9 | 10 | const primitiveInput = 'a' as unknown as Json; 11 | const primitiveResult = dontReturnEmpty(primitiveInput); 12 | expect(primitiveResult).toEqual(undefined); 13 | }); 14 | it('returns properties if any are defined', () => { 15 | const input: any = { 16 | a: 1, 17 | b: undefined, 18 | c: null, 19 | d: {}, 20 | e: [] 21 | }; 22 | 23 | const output = dontReturnEmpty(input); 24 | 25 | expect(output).toEqual(input); 26 | }); 27 | it('returns undefined if all are empty', () => { 28 | const input: any = { 29 | a: undefined, 30 | b: undefined, 31 | c: null, 32 | d: {}, 33 | e: [] 34 | }; 35 | 36 | const output = dontReturnEmpty(input); 37 | 38 | expect(output).toEqual(undefined); 39 | }); 40 | it('works on nested objects', () => { 41 | const input: any = { 42 | a: undefined, 43 | b: undefined, 44 | c: null, 45 | d: {}, 46 | e: [], 47 | f: { 48 | g: [], 49 | h: {}, 50 | i: [{}] 51 | }, 52 | j: [{ k: [] }, []] 53 | }; 54 | 55 | const output = dontReturnEmpty(input); 56 | 57 | expect(output).toEqual(undefined); 58 | }); 59 | }); -------------------------------------------------------------------------------- /test/utils/os/index.test.ts: -------------------------------------------------------------------------------- 1 | const mockExec = jest.fn(); 2 | const mockPipe = jest.fn(); 3 | const mockLoggerLog = jest.fn(); 4 | const mockLoggerError = jest.fn(); 5 | const mockLoggerInfo = jest.fn(); 6 | 7 | jest.mock('child_process', () => { 8 | const { 9 | ExecOptions, 10 | ExecException 11 | } = jest.requireActual('child_process'); 12 | return { 13 | exec: mockExec, 14 | ExecOptions, 15 | ExecException 16 | }; 17 | }); 18 | 19 | jest.mock('../../../src/logger', () => ({ 20 | log: mockLoggerLog, 21 | error: mockLoggerError, 22 | info: mockLoggerInfo 23 | })); 24 | 25 | import { 26 | ExecOptions 27 | } from 'child_process'; 28 | import { 29 | runCommand 30 | } from '../../../src/utils/os'; 31 | 32 | class ChildProcessStub { 33 | stdoutCb: (data: string) => void; 34 | stderrCb: (data: string) => void; 35 | childProcessErrorCb: (error: Error) => void; 36 | childProcessExitCb: (code: number, signal?: string) => void; 37 | stdout: { 38 | on: (_event: string, callback: (data: string) => void) => void 39 | }; 40 | stderr: { 41 | on: (_event: string, callback: (data: string) => void) => void 42 | }; 43 | 44 | constructor () { 45 | const self = this; 46 | this.stdout = { 47 | on (_event: string, callback: (data: string) => void) { 48 | self.stdoutCb = callback; 49 | } 50 | }; 51 | this.stderr = { 52 | on (_event: string, callback: (data: string) => void) { 53 | self.stderrCb = callback; 54 | } 55 | }; 56 | } 57 | on (event: string, callback: (...args: any) => void) { 58 | if (event === 'error') { 59 | this.childProcessErrorCb = callback; 60 | } else if (event === 'exit') { 61 | this.childProcessExitCb = callback; 62 | } 63 | } 64 | } 65 | 66 | let childProcessStub: ChildProcessStub; 67 | function execStub (_command: string, _opts: ExecOptions) { 68 | childProcessStub = new ChildProcessStub(); 69 | return childProcessStub; 70 | } 71 | 72 | describe('os utils', () => { 73 | afterEach(() => { 74 | // for mocks 75 | jest.resetAllMocks(); 76 | // for spies 77 | jest.restoreAllMocks(); 78 | }); 79 | 80 | describe('runCommand', () => { 81 | beforeEach(() => { 82 | process.env.MOCK_VAR = 'mock-var'; 83 | jest.spyOn(global.process.stdin, 'pipe').mockImplementation(mockPipe); 84 | jest.spyOn(global.process.stdin, 'pipe').mockImplementation(mockPipe); 85 | mockExec.mockImplementation(execStub); 86 | }); 87 | 88 | it('combines env vars from options with env vars from process', async () => { 89 | const resultPromise = runCommand('mock command', { 90 | env: { 91 | TEST_VAR: 'test-var' 92 | } 93 | }); 94 | const childProcess = childProcessStub; 95 | childProcess.stdoutCb('data'); 96 | childProcess.stderrCb('warning'); 97 | childProcess.childProcessExitCb(0); 98 | const response = await resultPromise; 99 | 100 | expect(mockLoggerLog).toBeCalled(); 101 | expect(mockLoggerLog).toBeCalledTimes(1); 102 | expect(mockLoggerLog).toBeCalledWith('mock command'); 103 | 104 | expect(mockExec).toBeCalled(); 105 | expect(mockExec).toBeCalledTimes(1); 106 | expect(mockExec).toBeCalledWith( 107 | 'mock command', 108 | expect.any(Object) 109 | ); 110 | expect(mockExec.mock.calls[0][1].env).toHaveProperty('MOCK_VAR', 'mock-var'); 111 | expect(mockExec.mock.calls[0][1].env).toHaveProperty('TEST_VAR', 'test-var'); 112 | 113 | expect(response).toEqual({ 114 | stdout: 'data', 115 | stderr: 'warning', 116 | exitCode: 0 117 | }); 118 | }); 119 | it('rejects on error from child process', async () => { 120 | const mockError = { code: 1, name: 'ExecException', message: '' }; 121 | 122 | let thrownError; 123 | try { 124 | const resultPromise = runCommand('mock command'); 125 | const childProcess = childProcessStub; 126 | childProcess.childProcessErrorCb(mockError); 127 | childProcess.childProcessExitCb(0); 128 | await resultPromise; 129 | } catch (error) { 130 | thrownError = error; 131 | } finally { 132 | expect(mockLoggerLog).toBeCalled(); 133 | expect(mockLoggerLog).toBeCalledTimes(1); 134 | expect(mockLoggerLog).toBeCalledWith('mock command'); 135 | 136 | expect(mockExec).toBeCalled(); 137 | expect(mockExec).toBeCalledTimes(1); 138 | expect(mockExec).toBeCalledWith('mock command', undefined); 139 | 140 | expect(mockLoggerError).toBeCalled(); 141 | expect(mockLoggerError).toBeCalledWith('Failed to execute command "mock command"'); 142 | 143 | expect(thrownError).toBeDefined(); 144 | expect(thrownError).toEqual(mockError); 145 | } 146 | }); 147 | it('notifies of signal during exit', async () => { 148 | const resultPromise = runCommand('mock command'); 149 | const childProcess = childProcessStub; 150 | childProcess.childProcessExitCb(130, 'SIGINT'); 151 | const response = await resultPromise; 152 | 153 | expect(mockLoggerLog).toBeCalled(); 154 | expect(mockLoggerLog).toBeCalledTimes(1); 155 | expect(mockLoggerLog).toBeCalledWith('mock command'); 156 | 157 | expect(mockExec).toBeCalled(); 158 | expect(mockExec).toBeCalledTimes(1); 159 | expect(mockExec).toBeCalledWith('mock command', undefined); 160 | 161 | expect(mockLoggerInfo).toBeCalled(); 162 | expect(mockLoggerInfo).toBeCalledTimes(1); 163 | expect(mockLoggerInfo).toBeCalledWith('Exited due to signal: SIGINT'); 164 | 165 | expect(response).toEqual({ 166 | stdout: '', 167 | stderr: '', 168 | exitCode: 130 169 | }); 170 | }); 171 | it('rejects on error from main process', async () => { 172 | const mockError = new Error('Error!'); 173 | mockLoggerLog.mockImplementationOnce(() => { throw mockError; } ); 174 | 175 | let thrownError; 176 | try { 177 | await runCommand('mock command'); 178 | } catch (error) { 179 | thrownError = error; 180 | } finally { 181 | expect(mockLoggerLog).toBeCalled(); 182 | expect(mockLoggerLog).toBeCalledTimes(1); 183 | expect(mockLoggerLog).toBeCalledWith('mock command'); 184 | 185 | expect(mockExec).not.toBeCalled(); 186 | 187 | expect(mockLoggerError).toBeCalled(); 188 | expect(mockLoggerError).toBeCalledWith('Failed to execute command "mock command"'); 189 | 190 | expect(thrownError).toBeDefined(); 191 | expect(thrownError).toEqual(mockError); 192 | } 193 | }); 194 | }); 195 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "strictNullChecks": false, 9 | "noImplicitThis": true, 10 | "alwaysStrict": true, 11 | "noUnusedLocals": false, 12 | "noUnusedParameters": false, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": false, 15 | "inlineSourceMap": true, 16 | "inlineSources": true, 17 | "experimentalDecorators": true, 18 | "strictPropertyInitialization": false, 19 | "skipLibCheck": true, 20 | "esModuleInterop": true, 21 | "typeRoots": ["./node_modules/@types"], 22 | "outDir": "./dist" 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "strictNullChecks": false, 9 | "noImplicitThis": true, 10 | "alwaysStrict": true, 11 | "noUnusedLocals": false, 12 | "noUnusedParameters": false, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": false, 15 | "inlineSourceMap": true, 16 | "inlineSources": true, 17 | "experimentalDecorators": true, 18 | "strictPropertyInitialization": false, 19 | "skipLibCheck": true, 20 | "esModuleInterop": true, 21 | "typeRoots": ["./node_modules/@types"], 22 | "outDir": "./dist", 23 | }, 24 | "include": ["test"] 25 | } 26 | --------------------------------------------------------------------------------