diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..50c8ea340 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +* +!.dockerignore +!Dockerfile diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..b692705e2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{*.{py,md},Dockerfile}] +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..e8de3562f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @maxymvlasov @yermulnik diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..e606c0973 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,142 @@ +# Notes for contributors + +1. Python hooks are supported now too. All you have to do is: + 1. add a line to the `console_scripts` array in `entry_points` in `setup.py` + 2. Put your python script in the `pre_commit_hooks` folder + +Enjoy the clean, valid, and documented code! + +* [Run and debug hooks locally](#run-and-debug-hooks-locally) +* [Run hook performance test](#run-hook-performance-test) + * [Run via BASH](#run-via-bash) + * [Run via Docker](#run-via-docker) + * [Check results](#check-results) + * [Cleanup](#cleanup) +* [Add new hook](#add-new-hook) + * [Before write code](#before-write-code) + * [Prepare basic documentation](#prepare-basic-documentation) + * [Add code](#add-code) + * [Finish with the documentation](#finish-with-the-documentation) + +## Run and debug hooks locally + +```bash +pre-commit try-repo {-a} /path/to/local/pre-commit-terraform/repo {hook_name} +``` + +I.e. + +```bash +pre-commit try-repo /mnt/c/Users/tf/pre-commit-terraform terraform_fmt # Run only `terraform_fmt` check +pre-commit try-repo -a ~/pre-commit-terraform # run all existing checks from repo +``` + +Running `pre-commit` with `try-repo` ignores all arguments specified in `.pre-commit-config.yaml`. + +If you need to test hook with arguments, follow [pre-commit doc](https://pre-commit.com/#arguments-pattern-in-hooks) to test hooks. + +For example, to test that the [`terraform_fmt`](../README.md#terraform_fmt) hook works fine with arguments: + +```bash +/tmp/pre-commit-terraform/terraform_fmt.sh --args=-diff --args=-write=false test-dir/main.tf test-dir/vars.tf +``` + +## Run hook performance test + +To check is your improvement not violate performance, we have dummy execution time tests. + +Script accept next options: + +| # | Name | Example value | Description | +| --- | ---------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------- | +| 1 | `TEST_NUM` | `200` | How many times need repeat test | +| 2 | `TEST_COMMAND` | `'pre-commit try-repo -a /tmp/159/pre-commit-terraform terraform_tfsec'` | Valid pre-commit command | +| 3 | `TEST_DIR` | `'/tmp/infrastructure'` | Dir on what you run tests. | +| 4 | `TEST_DESCRIPTION` | ```'`terraform_tfsec` PR #123:'``` | Text that you'd like to see in result | +| 5 | `RAW_TEST_`
`RESULTS_FILE_NAME` | `terraform_tfsec_pr123` | (Temporary) File where all test data will be stored. | + + +> **Note:** To make test results repeatable and comparable, be sure that on the test machine nothing generates an unstable workload. During tests good to stop any other apps and do not interact with the test machine. +> +> Otherwise, for eg, when you watch Youtube videos during one test and not during other, test results can differ up to 30% for the same test. + +### Run via BASH + +```bash +# Install deps +sudo apt install -y datamash +# Run tests +./hooks_performance_test.sh 200 'pre-commit try-repo -a /tmp/159/pre-commit-terraform terraform_tfsec' '/tmp/infrastructure' '`terraform_tfsec` v1.51.0:' 'terraform_tfsec_pr159' +``` + +### Run via Docker + +```bash +# Build `pre-commit-terraform` image +docker build -t pre-commit-terraform --build-arg INSTALL_ALL=true . +# Build test image +docker build -t pre-commit-tests tests/ +# Run +TEST_NUM=1 +TEST_DIR='/tmp/infrastructure' +PRE_COMMIT_DIR="$(pwd)" +TEST_COMMAND='pre-commit try-repo -a /pct terraform_tfsec' +TEST_DESCRIPTION='`terraform_tfsec` v1.51.0:' +RAW_TEST_RESULTS_FILE_NAME='terraform_tfsec_pr159' + +docker run -v "$PRE_COMMIT_DIR:/pct:rw" -v "$TEST_DIR:/lint:ro" pre-commit-tests \ + $TEST_NUM "$TEST_COMMAND" '/lint' "$RAW_TEST_RESULTS_FILE_NAME" "$RAW_TEST_RESULTS_FILE_NAME" +``` + +### Check results + +Results will be located at `./test/results` dir. + +### Cleanup + +```bash +sudo rm -rf tests/results +``` + +## Add new hook + +You can use [this PR](https://github.com/antonbabenko/pre-commit-terraform/pull/252) as an example. + +### Before write code + +1. Try to figure out future hook usage. +2. Confirm the concept with [Anton Babenko](https://github.com/antonbabenko). + +### Prepare basic documentation + +1. Identify and describe dependencies in [Install dependencies](../README.md#1-install-dependencies) and [Available Hooks](../README.md#available-hooks) sections + +### Add code + +1. Based on prev. block, add hook dependencies installation to [Dockerfile](../Dockerfile). + Check that works: + * `docker build -t pre-commit --build-arg INSTALL_ALL=true .` + * `docker build -t pre-commit --build-arg _VERSION=latest .` + * `docker build -t pre-commit --build-arg _VERSION=<1.2.3> .` +2. Add new hook to [`.pre-commit-hooks.yaml`](../.pre-commit-hooks.yaml) +3. Create hook file. Don't forget to make it executable via `chmod +x /path/to/hook/file`. +4. Test hook. How to do it is described in [Run and debug hooks locally](#run-and-debug-hooks-locally) section. +5. Test hook one more time. + 1. Push commit with hook file to GitHub + 2. Grab SHA hash of the commit + 3. Test hook using `.pre-commit-config.yaml`: + + ```yaml + repos: + - repo: https://github.com/antonbabenko/pre-commit-terraform # Your repo + rev: 3d76da3885e6a33d59527eff3a57d246dfb66620 # Your commit SHA + hooks: + - id: terraform_docs # New hook name + args: + - --args=--config=.terraform-docs.yml # Some args that you'd like to test + ``` + +### Finish with the documentation + +1. Add hook description to [Available Hooks](../README.md#available-hooks). +2. Create and populate a new hook section in [Hooks usage notes and examples](../README.md#hooks-usage-notes-and-examples). diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..9fdcfce62 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [antonbabenko] +custom: https://www.paypal.me/antonbabenko diff --git a/.github/ISSUE_TEMPLATE/bug_report_docker.md b/.github/ISSUE_TEMPLATE/bug_report_docker.md new file mode 100644 index 000000000..a47e30657 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_docker.md @@ -0,0 +1,81 @@ +--- +name: Docker bug report +about: Create a bug report +labels: +- bug +- area/docker +--- + + + +### Describe the bug + + + + +### How can we reproduce it? + + + + +### Environment information + +* OS: + + + +* `docker info`: + +
command output + +```bash +INSERT_OUTPUT_HERE +``` + +
+ +* Docker image tag/git commit: + +* Tools versions. Don't forget to specify right tag in command - + `TAG=latest && docker run --entrypoint cat pre-commit:$TAG /usr/bin/tools_versions_info` + +```bash +INSERT_OUTPUT_HERE +``` + +* `.pre-commit-config.yaml`: + +
file content + +```bash +INSERT_FILE_CONTENT_HERE +``` + +
diff --git a/.github/ISSUE_TEMPLATE/bug_report_local_install.md b/.github/ISSUE_TEMPLATE/bug_report_local_install.md new file mode 100644 index 000000000..329a3ae88 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_local_install.md @@ -0,0 +1,106 @@ +--- +name: Local installation bug report +about: Create a bug report +labels: +- bug +- area/local_installation +--- + + + +### Describe the bug + + + + +### How can we reproduce it? + + + + +### Environment information + +* OS: + + +* `uname -a` and/or `systeminfo | Select-String "^OS"` output: + +```bash +INSERT_OUTPUT_HERE +``` + + + +* Tools availability and versions: + + + +```bash +INSERT_TOOLS_VERSIONS_HERE +``` + + +* `.pre-commit-config.yaml`: + +
file content + +```bash +INSERT_FILE_CONTENT_HERE +``` + +
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..d1b4b6424 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,29 @@ +--- +name: Feature request +about: Suggest an idea for this project +labels: +- feature +--- + + + +### What problem are you facing? + + + + +### How could pre-commit-terraform help solve your problem? + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..bd5e5c69f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ + + +Put an `x` into the box if that apply: + +- [ ] This PR introduces breaking change. +- [ ] This PR fixes a bug. +- [ ] This PR adds new functionality. +- [ ] This PR enhances existing functionality. + +### Description of your changes + + + + + +### How can we test changes + + diff --git a/.github/workflows/build-image-test.yaml b/.github/workflows/build-image-test.yaml new file mode 100644 index 000000000..082d2b091 --- /dev/null +++ b/.github/workflows/build-image-test.yaml @@ -0,0 +1,31 @@ +name: "Build Dockerfile if changed and run smoke tests" + +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + name: Test changed-files + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Get changed Dockerfile + id: changed-files-specific + uses: tj-actions/changed-files@v13.1 + with: + files: | + Dockerfile + + - name: Build if Dockerfile changed + if: steps.changed-files-specific.outputs.any_changed == 'true' + uses: docker/build-push-action@v2 + with: + context: . + build-args: | + INSTALL_ALL=true + platforms: linux/amd64 + push: false + tags: | + ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }} diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml new file mode 100644 index 000000000..8fe0b0054 --- /dev/null +++ b/.github/workflows/build-image.yaml @@ -0,0 +1,52 @@ +name: Publish container image + +on: + workflow_dispatch: + release: + types: + - created + schedule: + - cron: '00 00 * * *' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set tag for image + run: | + echo IMAGE_TAG=$([ ${{ github.ref_type }} == 'tag' ] && echo ${{ github.ref_name }} || echo 'latest') >> $GITHUB_ENV + - name: Build and Push release + if: github.event_name != 'schedule' + uses: docker/build-push-action@v2 + with: + context: . + build-args: | + INSTALL_ALL=true + platforms: linux/amd64 + push: true + tags: | + ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }} + ghcr.io/${{ github.repository }}:latest + - name: Build and Push nightly + if: github.event_name == 'schedule' + uses: docker/build-push-action@v2 + with: + context: . + build-args: | + INSTALL_ALL=true + platforms: linux/amd64 + push: true + tags: | + ghcr.io/${{ github.repository }}:nightly diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 000000000..b89795d72 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,52 @@ +name: "Validate PR title" + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + # Please look up the latest version from + # https://github.com/amannn/action-semantic-pull-request/releases + - uses: amannn/action-semantic-pull-request@v3.4.6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Configure which types are allowed. + # Default: https://github.com/commitizen/conventional-commit-types + types: | + fix + feat + docs + ci + chore + # Configure that a scope must always be provided. + requireScope: false + # Configure additional validation for the subject based on a regex. + # This example ensures the subject starts with an uppercase character. + subjectPattern: ^[A-Z].+$ + # If `subjectPattern` is configured, you can use this property to override + # the default error message that is shown when the pattern doesn't match. + # The variables `subject` and `title` can be used within the message. + subjectPatternError: | + The subject "{subject}" found in the pull request title "{title}" + didn't match the configured pattern. Please ensure that the subject + starts with an uppercase character. + # For work-in-progress PRs you can typically use draft pull requests + # from Github. However, private repositories on the free plan don't have + # this option and therefore this action allows you to opt-in to using the + # special "[WIP]" prefix to indicate this state. This will avoid the + # validation of the PR title and the pull request checks remain pending. + # Note that a second check will be reported if this is enabled. + wip: true + # When using "Squash and merge" on a PR with only one commit, GitHub + # will suggest using that commit message instead of the PR title for the + # merge commit, and it's easy to commit this by mistake. Enable this option + # to also validate the commit message for one commit PRs. + validateSingleCommit: false diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 000000000..6250935b0 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,56 @@ +name: Common issues check + +on: [pull_request] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + + - name: Get changed files + id: file_changes + run: | + export DIFF=$(git diff --name-only origin/${{ github.base_ref }} ${{ github.sha }}) + echo "Diff between ${{ github.base_ref }} and ${{ github.sha }}" + echo "::set-output name=files::$( echo "$DIFF" | xargs echo )" + + - name: Install shfmt + run: | + curl -L "$(curl -s https://api.github.com/repos/mvdan/sh/releases/latest | grep -o -E -m 1 "https://.+?linux_amd64")" > shfmt \ + && chmod +x shfmt && sudo mv shfmt /usr/bin/ + + - name: Install shellcheck + run: | + sudo apt update && sudo apt install shellcheck + + - name: Install hadolint + run: | + curl -L "$(curl -s https://api.github.com/repos/hadolint/hadolint/releases/latest | grep -o -E -m 1 "https://.+?/hadolint-Linux-x86_64")" > hadolint \ + && chmod +x hadolint && sudo mv hadolint /usr/bin/ + # Need to success pre-commit fix push + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + # Skip terraform_tflint which interferes to commit pre-commit auto-fixes + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Execute pre-commit + uses: pre-commit/action@v2.0.0 + env: + SKIP: no-commit-to-branch,hadolint + with: + token: ${{ secrets.GITHUB_TOKEN }} + extra_args: --color=always --show-diff-on-failure --files ${{ steps.file_changes.outputs.files }} + # Run only skipped checks + - name: Execute pre-commit check that have no auto-fixes + if: always() + uses: pre-commit/action@v2.0.0 + env: + SKIP: check-added-large-files,check-merge-conflict,check-vcs-permalinks,forbid-new-submodules,no-commit-to-branch,end-of-file-fixer,trailing-whitespace,check-yaml,check-merge-conflict,check-executables-have-shebangs,check-case-conflict,mixed-line-ending,detect-aws-credentials,detect-private-key,shfmt,shellcheck + with: + extra_args: --color=always --show-diff-on-failure --files ${{ steps.file_changes.outputs.files }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..3018771ab --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: Release + +on: + workflow_dispatch: + push: + branches: + - master + paths: + - '**/*.py' + - '**/*.sh' + - 'Dockerfile' + - '.pre-commit-hooks.yaml' + # Ignore paths + - '!tests/**' +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Release + uses: cycjimmy/semantic-release-action@v2 + with: + semantic_version: 18.0.0 + extra_plugins: | + @semantic-release/changelog@6.0.0 + @semantic-release/git@10.0.0 + env: + GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} diff --git a/.github/workflows/stale-actions.yaml b/.github/workflows/stale-actions.yaml new file mode 100644 index 000000000..d2a5f6b96 --- /dev/null +++ b/.github/workflows/stale-actions.yaml @@ -0,0 +1,32 @@ +name: "Mark or close stale issues and PRs" +on: + schedule: + - cron: "0 0 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v4 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # Staling issues and PR's + days-before-stale: 30 + stale-issue-label: stale + stale-pr-label: stale + stale-issue-message: | + This issue has been automatically marked as stale because it has been open 30 days + with no activity. Remove stale label or comment or this issue will be closed in 10 days + stale-pr-message: | + This PR has been automatically marked as stale because it has been open 30 days + with no activity. Remove stale label or comment or this PR will be closed in 10 days + # Not stale if have this labels or part of milestone + exempt-issue-labels: bug,wip,on-hold + exempt-pr-labels: bug,wip,on-hold + exempt-all-milestones: true + # Close issue operations + # Label will be automatically removed if the issues are no longer closed nor locked. + days-before-close: 10 + delete-branch: true + close-issue-message: This issue was automatically closed because of stale in 10 days + close-pr-message: This PR was automatically closed because of stale in 10 days diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..0bbeada90 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tests/results/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70a21ad98..5ba5639d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,51 @@ repos: -- repo: git://github.com/pre-commit/pre-commit-hooks - rev: v1.2.3 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 hooks: - - id: check-yaml + # Git style + - id: check-added-large-files + - id: check-merge-conflict + - id: check-vcs-permalinks + - id: forbid-new-submodules + - id: no-commit-to-branch + + # Common errors - id: end-of-file-fixer - id: trailing-whitespace - - id: check-case-conflict + args: [--markdown-linebreak-ext=md] + - id: check-yaml - id: check-merge-conflict - id: check-executables-have-shebangs + + # Cross platform + - id: check-case-conflict + - id: mixed-line-ending + args: [--fix=lf] + + # Security + - id: detect-aws-credentials + args: ['--allow-missing-credentials'] + - id: detect-private-key + + +- repo: https://github.com/jumanjihouse/pre-commit-hooks + rev: 2.1.5 + hooks: + - id: shfmt + args: ['-l', '-i', '2', '-ci', '-sr', '-w'] + - id: shellcheck + +# Dockerfile linter +- repo: https://github.com/hadolint/hadolint + rev: v2.8.0 + hooks: + - id: hadolint + args: [ + '--ignore', 'DL3027', # Do not use apt + '--ignore', 'DL3007', # Using latest + '--ignore', 'DL4006', # Not related to alpine + '--ignore', 'SC1091', # Useless check + '--ignore', 'SC2015', # Useless check + '--ignore', 'SC3037', # Not related to alpine + '--ignore', 'DL3013', # Pin versions in pip + ] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 2e15aab48..763906d52 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,42 +1,122 @@ +- id: infracost_breakdown + name: Infracost breakdown + description: Check terraform infrastructure cost + entry: hooks/infracost_breakdown.sh + language: script + require_serial: true + files: \.(tf(vars)?|hcl)$ + exclude: \.terraform\/.*$ + - id: terraform_fmt name: Terraform fmt description: Rewrites all Terraform configuration files to a canonical format. - entry: terraform_fmt.sh + entry: hooks/terraform_fmt.sh language: script files: (\.tf|\.tfvars)$ exclude: \.terraform\/.*$ - require_serial: true - id: terraform_docs - name: Terraform Docs - description: Creates readme for terraform modules. - entry: terraform_docs.sh + name: Terraform docs + description: Inserts input and output documentation into README.md (using terraform-docs). + require_serial: true + entry: hooks/terraform_docs.sh language: script - files: (\.tf)$ + files: (\.tf|\.terraform\.lock\.hcl)$ exclude: \.terraform\/.*$ -- id: terraform_readme - name: Terraform Readme - description: Creates a README for Terraform modules using `terraform_config_inspect`. - entry: terraform_readme.sh +- id: terraform_docs_without_aggregate_type_defaults + name: Terraform docs (without aggregate type defaults) + description: Inserts input and output documentation into README.md (using terraform-docs). Identical to terraform_docs. + require_serial: true + entry: hooks/terraform_docs.sh language: script files: (\.tf)$ exclude: \.terraform\/.*$ - require_serial: true +- id: terraform_docs_replace + name: Terraform docs (overwrite README.md) + description: Overwrite content of README.md with terraform-docs. + require_serial: true + entry: terraform_docs_replace + language: python + files: (\.tf)$ + exclude: \.terraform\/.*$ -- id: terraform_validate_no_variables - name: Terraform validate without variables - description: Validates all Terraform configuration files without checking whether all required variables were set (basic check). - entry: terraform_validate_no_variables.sh +- id: terraform_validate + name: Terraform validate + description: Validates all Terraform configuration files. + require_serial: true + entry: hooks/terraform_validate.sh language: script files: (\.tf|\.tfvars)$ exclude: \.terraform\/.*$ -- id: terraform_validate_with_variables - name: Terraform validate with variables - description: Validates all Terraform configuration files and checks whether all required variables were specified. - entry: terraform_validate_with_variables.sh +- id: terraform_providers_lock + name: Lock terraform provider versions + description: Updates provider signatures in dependency lock files. + require_serial: true + entry: hooks/terraform_providers_lock.sh + language: script + files: (\.terraform\.lock\.hcl)$ + exclude: \.terraform\/.*$ + +- id: terraform_tflint + name: Terraform validate with tflint + description: Validates all Terraform configuration files with TFLint. + require_serial: true + entry: hooks/terraform_tflint.sh language: script files: (\.tf|\.tfvars)$ exclude: \.terraform\/.*$ + +- id: terragrunt_fmt + name: Terragrunt fmt + description: Rewrites all Terragrunt configuration files to a canonical format. + entry: hooks/terragrunt_fmt.sh + language: script + files: (\.hcl)$ + exclude: \.terraform\/.*$ + +- id: terragrunt_validate + name: Terragrunt validate + description: Validates all Terragrunt configuration files. + entry: hooks/terragrunt_validate.sh + language: script + files: (\.hcl)$ + exclude: \.terraform\/.*$ + +- id: terraform_tfsec + name: Terraform validate with tfsec + description: Static analysis of Terraform templates to spot potential security issues. + require_serial: true + entry: hooks/terraform_tfsec.sh + language: script + +- id: checkov + name: Checkov + description: Runs checkov on Terraform templates. + entry: checkov -d . + language: python + pass_filenames: false + always_run: false + files: \.tf$ + exclude: \.terraform\/.*$ + require_serial: true + +- id: terrascan + name: terrascan + description: Runs terrascan on Terraform templates. + language: script + entry: hooks/terrascan.sh + files: \.tf$ + exclude: \.terraform\/.*$ + require_serial: true + +- id: terraform_readme + name: Terraform Readme + description: Creates a README for Terraform modules using `terraform_config_inspect`. + entry: terraform_readme.sh + language: script + files: (\.tf)$ + exclude: \.terraform\/.*$ + require_serial: true diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 000000000..6e39031cf --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,36 @@ +{ + "branches": [ + "main", + "master" + ], + "ci": false, + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/github", + { + "successComment": + "This ${issue.pull_request ? 'PR is included' : 'issue has been resolved'} in version ${nextRelease.version} :tada:", + "labels": false, + "releasedLabels": false + } + ], + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md", + "changelogTitle": "# Changelog\n\nAll notable changes to this project will be documented in this file." + } + ], + [ + "@semantic-release/git", + { + "assets": [ + "CHANGELOG.md" + ], + "message": "chore(release): version ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + } + ] + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..0766df9c1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,637 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +# [1.64.0](https://github.com/antonbabenko/pre-commit-terraform/compare/v1.63.0...v1.64.0) (2022-02-10) + + +### Features + +* Improved speed of `pre-commit run -a` for multiple hooks ([#338](https://github.com/antonbabenko/pre-commit-terraform/issues/338)) ([579dc45](https://github.com/antonbabenko/pre-commit-terraform/commit/579dc45fb40bc64c6742d42a9da78eddb0b70e1d)) + +# [1.63.0](https://github.com/antonbabenko/pre-commit-terraform/compare/v1.62.3...v1.63.0) (2022-02-10) + + +### Features + +* Improve performance during `pre-commit --all (-a)` run ([#327](https://github.com/antonbabenko/pre-commit-terraform/issues/327)) ([7e7c916](https://github.com/antonbabenko/pre-commit-terraform/commit/7e7c91643e8f213168b95d0583f787f914b04ce4)) + +## [1.62.3](https://github.com/antonbabenko/pre-commit-terraform/compare/v1.62.2...v1.62.3) (2021-12-22) + + +### Bug Fixes + +* Check all directories with changes and pass all args in terrascan hook ([#305](https://github.com/antonbabenko/pre-commit-terraform/issues/305)) ([66401d9](https://github.com/antonbabenko/pre-commit-terraform/commit/66401d93f485164fb2272af297df835b932c61c3)) + +## [1.62.2](https://github.com/antonbabenko/pre-commit-terraform/compare/v1.62.1...v1.62.2) (2021-12-21) + + +### Bug Fixes + +* Properly exclude .terraform directory with checkov hook ([#306](https://github.com/antonbabenko/pre-commit-terraform/issues/306)) ([b431a43](https://github.com/antonbabenko/pre-commit-terraform/commit/b431a43ffa6cd13156485ef853c967856e9572ef)) +* Speedup `terrascan` hook up to x3 times in big repos ([#307](https://github.com/antonbabenko/pre-commit-terraform/issues/307)) ([2e8dcf9](https://github.com/antonbabenko/pre-commit-terraform/commit/2e8dcf9298733a256cc7f8c6f05b3ef9a1047a36)) + +## [1.62.1](https://github.com/antonbabenko/pre-commit-terraform/compare/v1.62.0...v1.62.1) (2021-12-18) + + +### Bug Fixes + +* **terraform_tflint:** Restore current working directory behavior ([#302](https://github.com/antonbabenko/pre-commit-terraform/issues/302)) ([93029dc](https://github.com/antonbabenko/pre-commit-terraform/commit/93029dcfcf6b9b121c24573f3e647d9fde255486)) + +# [1.62.0](https://github.com/antonbabenko/pre-commit-terraform/compare/v1.61.0...v1.62.0) (2021-12-12) + + +### Features + +* Added semantic release ([#296](https://github.com/antonbabenko/pre-commit-terraform/issues/296)) ([1bcca44](https://github.com/antonbabenko/pre-commit-terraform/commit/1bcca44d1677128c23d505be644f1d16c320eb4c)) + +# Change Log + +All notable changes to this project will be documented in this file. + + +## [Unreleased] + + + + +## [v1.61.0] - 2021-12-11 + +- feat: Pass custom arguments to terraform init in `terraform_validate` hook ([#293](https://github.com/antonbabenko/pre-commit-terraform/issues/293)) +- fix: analyse all folders with tflint and don't stop on first execution ([#289](https://github.com/antonbabenko/pre-commit-terraform/issues/289)) + + + +## [v1.60.0] - 2021-12-08 + +- fix: pre-build docker image ([#292](https://github.com/antonbabenko/pre-commit-terraform/issues/292)) + + + +## [v1.59.0] - 2021-12-06 + +- fix: Fixed docker build ([#288](https://github.com/antonbabenko/pre-commit-terraform/issues/288)) + + + +## [v1.58.0] - 2021-11-20 + +- chore: Publish container image on release ([#285](https://github.com/antonbabenko/pre-commit-terraform/issues/285)) +- chore: Fix master merge to working branch on pre-commit autofixes ([#286](https://github.com/antonbabenko/pre-commit-terraform/issues/286)) + + + +## [v1.57.0] - 2021-11-17 + +- fix: typo in arg name for terraform-docs ([#283](https://github.com/antonbabenko/pre-commit-terraform/issues/283)) +- chore: Add deprecation notice to `terraform_docs_replace` ([#280](https://github.com/antonbabenko/pre-commit-terraform/issues/280)) + + + +## [v1.56.0] - 2021-11-08 + +- feat: Updated Docker image from Ubuntu to Alpine ([#278](https://github.com/antonbabenko/pre-commit-terraform/issues/278)) +- chore: Updated messages shown in terraform_tflint hook ([#274](https://github.com/antonbabenko/pre-commit-terraform/issues/274)) + + + +## [v1.55.0] - 2021-10-27 + +- fix: Fixed 1.54.0 where `terraform_docs` was broken ([#272](https://github.com/antonbabenko/pre-commit-terraform/issues/272)) + + + +## [v1.54.0] - 2021-10-27 + +- feat: Add support for quoted values in `infracost_breakdown` `--hook-config` ([#269](https://github.com/antonbabenko/pre-commit-terraform/issues/269)) +- docs: Added notes about sponsors ([#268](https://github.com/antonbabenko/pre-commit-terraform/issues/268)) +- fix: Fixed args expand in terraform_docs ([#260](https://github.com/antonbabenko/pre-commit-terraform/issues/260)) + + + +## [v1.53.0] - 2021-10-26 + +- docs: Pre-release 1.53 ([#267](https://github.com/antonbabenko/pre-commit-terraform/issues/267)) +- docs: Clarify docs for terraform_tfsec hook ([#266](https://github.com/antonbabenko/pre-commit-terraform/issues/266)) +- feat: Add infracost_breakdown hook ([#252](https://github.com/antonbabenko/pre-commit-terraform/issues/252)) +- feat: Set up PR reviewers automatically ([#258](https://github.com/antonbabenko/pre-commit-terraform/issues/258)) +- docs: fix protocol to prevent MITM ([#257](https://github.com/antonbabenko/pre-commit-terraform/issues/257)) +- feat: add __GIT_WORKING_DIR__ to tfsec ([#255](https://github.com/antonbabenko/pre-commit-terraform/issues/255)) +- docs: Add missing space in terrascan install cmd ([#253](https://github.com/antonbabenko/pre-commit-terraform/issues/253)) +- fix: command not found ([#251](https://github.com/antonbabenko/pre-commit-terraform/issues/251)) +- fix: execute tflint once in no errors ([#250](https://github.com/antonbabenko/pre-commit-terraform/issues/250)) +- docs: fix deps ([#249](https://github.com/antonbabenko/pre-commit-terraform/issues/249)) +- feat: Add `terraform_docs` hook settings ([#245](https://github.com/antonbabenko/pre-commit-terraform/issues/245)) +- fix: terrafrom_tflint ERROR output for files located in repo root ([#243](https://github.com/antonbabenko/pre-commit-terraform/issues/243)) +- feat: Add support for specify terraform-docs config file ([#244](https://github.com/antonbabenko/pre-commit-terraform/issues/244)) +- docs: Document hooks dependencies ([#247](https://github.com/antonbabenko/pre-commit-terraform/issues/247)) +- feat: Allow passing of args to terraform_fmt ([#147](https://github.com/antonbabenko/pre-commit-terraform/issues/147)) +- docs: Add terraform_fmt usage instructions and how-to debug script with args ([#242](https://github.com/antonbabenko/pre-commit-terraform/issues/242)) +- fix: TFSec outputs the same results multiple times ([#237](https://github.com/antonbabenko/pre-commit-terraform/issues/237)) +- chore: Do not mark issues and PR's in milestone as stale ([#241](https://github.com/antonbabenko/pre-commit-terraform/issues/241)) + + + +## [v1.52.0] - 2021-10-04 + +- feat: Add new hook for `terraform providers lock` operation ([#173](https://github.com/antonbabenko/pre-commit-terraform/issues/173)) +- docs: Document terraform_tfsec args usage ([#238](https://github.com/antonbabenko/pre-commit-terraform/issues/238)) +- docs: Make contributors more visible ([#236](https://github.com/antonbabenko/pre-commit-terraform/issues/236)) +- docs: Add contributing guide and docs about performance tests ([#235](https://github.com/antonbabenko/pre-commit-terraform/issues/235)) +- fix: terraform_tflint hook executes in a serial way to run less often ([#211](https://github.com/antonbabenko/pre-commit-terraform/issues/211)) +- feat: Add PATH outputs when TFLint found any problem ([#234](https://github.com/antonbabenko/pre-commit-terraform/issues/234)) +- fix: Dockerfile if INSTALL_ALL is not defined ([#233](https://github.com/antonbabenko/pre-commit-terraform/issues/233)) +- docs: Describe hooks usage and improve examples ([#232](https://github.com/antonbabenko/pre-commit-terraform/issues/232)) +- chore: Add shfmt to workflow ([#231](https://github.com/antonbabenko/pre-commit-terraform/issues/231)) +- fix: remove dead code from terraform-docs script ([#229](https://github.com/antonbabenko/pre-commit-terraform/issues/229)) + + + +## [v1.51.0] - 2021-09-17 + +- fix: trigger terraform-docs on changes in lock files ([#228](https://github.com/antonbabenko/pre-commit-terraform/issues/228)) +- fix: label auto-adding after label rename ([#226](https://github.com/antonbabenko/pre-commit-terraform/issues/226)) +- chore: Updated GH stale action config ([#223](https://github.com/antonbabenko/pre-commit-terraform/issues/223)) +- feat: Add GH checks and templates ([#222](https://github.com/antonbabenko/pre-commit-terraform/issues/222)) +- feat: Add mixed line ending check to prevent possible errors ([#221](https://github.com/antonbabenko/pre-commit-terraform/issues/221)) +- fix: Dockerized pre-commit-terraform ([#219](https://github.com/antonbabenko/pre-commit-terraform/issues/219)) +- docs: Initial docs improvement ([#218](https://github.com/antonbabenko/pre-commit-terraform/issues/218)) +- chore: Update Ubuntu install method ([#198](https://github.com/antonbabenko/pre-commit-terraform/issues/198)) + + + +## [v1.50.0] - 2021-04-22 + +- feat: Adds support for Terrascan ([#195](https://github.com/antonbabenko/pre-commit-terraform/issues/195)) + + + +## [v1.49.0] - 2021-04-20 + +- fix: Fix and pin versions in Dockerfile ([#193](https://github.com/antonbabenko/pre-commit-terraform/issues/193)) +- chore: Fix mistake on command ([#185](https://github.com/antonbabenko/pre-commit-terraform/issues/185)) +- Update README.md + + + +## [v1.48.0] - 2021-03-12 + +- chore: add dockerfile ([#183](https://github.com/antonbabenko/pre-commit-terraform/issues/183)) +- docs: Added checkov install ([#182](https://github.com/antonbabenko/pre-commit-terraform/issues/182)) + + + +## [v1.47.0] - 2021-02-25 + +- fix: remove sed postprocessing from the terraform_docs_replace hook to fix compatibility with terraform-docs 0.11.0+ ([#176](https://github.com/antonbabenko/pre-commit-terraform/issues/176)) +- docs: updates installs for macOS and ubuntu ([#175](https://github.com/antonbabenko/pre-commit-terraform/issues/175)) + + + +## [v1.46.0] - 2021-02-20 + +- fix: Terraform validate for submodules ([#172](https://github.com/antonbabenko/pre-commit-terraform/issues/172)) + + + +## [v1.45.0] - 2020-11-12 + +- fix: Correct deprecated parameter to terraform-docs ([#156](https://github.com/antonbabenko/pre-commit-terraform/issues/156)) + + + +## [v1.44.0] - 2020-11-02 + + + + +## [v1.43.1] - 2020-11-02 + +- feat: Make terraform_validate to run init if necessary ([#158](https://github.com/antonbabenko/pre-commit-terraform/issues/158)) + + + +## [v1.43.0] - 2020-09-24 + +- fix: Fix regex considering terraform-docs v0.10.0 old ([#151](https://github.com/antonbabenko/pre-commit-terraform/issues/151)) + + + +## [v1.42.0] - 2020-09-24 + +- fix: make terraform_docs Windows compatible ([#129](https://github.com/antonbabenko/pre-commit-terraform/issues/129)) + + + +## [v1.41.0] - 2020-09-23 + +- fix: terraform-docs version 0.10 removed with-aggregate-type-defaults ([#150](https://github.com/antonbabenko/pre-commit-terraform/issues/150)) + + + +## [v1.40.0] - 2020-09-22 + +- feat: Add possibility to share tflint config file for subdirs ([#149](https://github.com/antonbabenko/pre-commit-terraform/issues/149)) + + + +## [v1.39.0] - 2020-09-08 + +- feat: Add checkov support ([#143](https://github.com/antonbabenko/pre-commit-terraform/issues/143)) + + + +## [v1.38.0] - 2020-09-07 + +- fix: Correctly handle arrays in terraform_docs.sh ([#141](https://github.com/antonbabenko/pre-commit-terraform/issues/141)) + + + +## [v1.37.0] - 2020-09-01 + +- fix: make terraform_tfsec.sh executable ([#140](https://github.com/antonbabenko/pre-commit-terraform/issues/140)) + + + +## [v1.36.0] - 2020-09-01 + +- feat: have option for terraform_tfsec hook to only run in relevant modified directories ([#135](https://github.com/antonbabenko/pre-commit-terraform/issues/135)) + + + +## [v1.35.0] - 2020-08-28 + +- fix: Squash terraform_docs bug ([#138](https://github.com/antonbabenko/pre-commit-terraform/issues/138)) + + + +## [v1.34.0] - 2020-08-27 + +- chore: Use lib_getopt for all hooks and some style tweaks ([#137](https://github.com/antonbabenko/pre-commit-terraform/issues/137)) + + + +## [v1.33.0] - 2020-08-27 + +- fix: Pass args and env vars to terraform validate ([#125](https://github.com/antonbabenko/pre-commit-terraform/issues/125)) +- docs: Update terraform-docs link pointing to new organization ([#130](https://github.com/antonbabenko/pre-commit-terraform/issues/130)) + + + +## [v1.32.0] - 2020-08-19 + +- feat: add terragrunt validate hook ([#134](https://github.com/antonbabenko/pre-commit-terraform/issues/134)) + + + +## [v1.31.0] - 2020-05-27 + +- fix: Updated formatting in README (closes [#113](https://github.com/antonbabenko/pre-commit-terraform/issues/113)) +- docs: Fixed the docs to use the latest config syntax([#106](https://github.com/antonbabenko/pre-commit-terraform/issues/106)) +- docs: Added coreutils as requirements in README.md ([#105](https://github.com/antonbabenko/pre-commit-terraform/issues/105)) + + + +## [v1.30.0] - 2020-04-23 + +- Updated pre-commit deps +- feat: Support for TFSec ([#103](https://github.com/antonbabenko/pre-commit-terraform/issues/103)) + + + +## [v1.29.0] - 2020-04-04 + +- fix: Change terraform_validate hook functionality for subdirectories with terraform files ([#100](https://github.com/antonbabenko/pre-commit-terraform/issues/100)) + +### + +configuration for the appropriate working directory. + +* Neglected to change the terraform validate call to use the default of the +current directory. + +* Several changes to improve functionality: +- Switch to checking the path for '*.tf' instead of always checking the current + +directory. +- Try to find a '.terraform' directory (which indicates a `terraform init`) and + +change to that directory before running `terraform validate`. + +* Fix the description for the terraform_validate hook to reflect changes that were +made in: +https://github.com/antonbabenko/pre-commit-terraform/commit/35e0356188b64a4c5af9a4e7200d936e514cba71 + +* - Clean up comments. +- Adjust variable names to better reflect what they are holding. + + + +## [v1.28.0] - 2020-04-04 + +- Allow passing multiple args to terraform-docs ([#98](https://github.com/antonbabenko/pre-commit-terraform/issues/98)) +- Update installation instructions ([#79](https://github.com/antonbabenko/pre-commit-terraform/issues/79)) + + + +## [v1.27.0] - 2020-03-02 + +- corrected tflint documentation ([#95](https://github.com/antonbabenko/pre-commit-terraform/issues/95)) + + + +## [v1.26.0] - 2020-02-21 + +- Updated pre-commit-hooks +- Fixed exit code for terraform 0.11 branch in terraform_docs ([#94](https://github.com/antonbabenko/pre-commit-terraform/issues/94)) + + + +## [v1.25.0] - 2020-01-30 + +- Fixed tflint hook to iterate over files ([#77](https://github.com/antonbabenko/pre-commit-terraform/issues/77)) + + + +## [v1.24.0] - 2020-01-21 + +- Added shfmt to autoformat shell scripts ([#86](https://github.com/antonbabenko/pre-commit-terraform/issues/86)) + + + +## [v1.23.0] - 2020-01-21 + +- Added support for terraform-docs 0.8.0 with proper support for Terraform 0.12 syntax (bye-bye awk) ([#85](https://github.com/antonbabenko/pre-commit-terraform/issues/85)) + + + +## [v1.22.0] - 2020-01-13 + +- move terraform-docs args after markdown command ([#83](https://github.com/antonbabenko/pre-commit-terraform/issues/83)) + + + +## [v1.21.0] - 2019-11-16 + +- use getopt for args in the tflint hook, following the approach in terraform-docs ([#75](https://github.com/antonbabenko/pre-commit-terraform/issues/75)) + + + +## [v1.20.0] - 2019-11-02 + +- Fixes [#65](https://github.com/antonbabenko/pre-commit-terraform/issues/65): terraform-docs should not fail if complex types contain 'description' keyword ([#73](https://github.com/antonbabenko/pre-commit-terraform/issues/73)) +- Added FUNDING.yml +- Improve installation instructions and make README more readable ([#72](https://github.com/antonbabenko/pre-commit-terraform/issues/72)) +- Update rev in README.md ([#70](https://github.com/antonbabenko/pre-commit-terraform/issues/70)) + + + +## [v1.19.0] - 2019-08-20 + +- Updated README with terraform_tflint hook +- Added support for TFLint with --deep parameter ([#53](https://github.com/antonbabenko/pre-commit-terraform/issues/53)) + + + +## [v1.18.0] - 2019-08-20 + +- Updated README with terragrunt_fmt hook +- Formatter for Terragrunt HCL files ([#60](https://github.com/antonbabenko/pre-commit-terraform/issues/60)) + + + +## [v1.17.0] - 2019-06-25 + +- Fixed enquoted types in terraform_docs (fixed [#52](https://github.com/antonbabenko/pre-commit-terraform/issues/52)) +- Fix typo in README ([#51](https://github.com/antonbabenko/pre-commit-terraform/issues/51)) + + + +## [v1.16.0] - 2019-06-18 + +- Add slash to mktemp dir (fixed [#50](https://github.com/antonbabenko/pre-commit-terraform/issues/50)) + + + +## [v1.15.0] - 2019-06-18 + +- Fixed awk script for terraform-docs (kudos [@cytopia](https://github.com/cytopia)) and mktemp on Mac (closes [#47](https://github.com/antonbabenko/pre-commit-terraform/issues/47), [#48](https://github.com/antonbabenko/pre-commit-terraform/issues/48), [#49](https://github.com/antonbabenko/pre-commit-terraform/issues/49)) +- Fix version in README.md ([#46](https://github.com/antonbabenko/pre-commit-terraform/issues/46)) + + + +## [v1.14.0] - 2019-06-17 + +- Upgraded to work with Terraform >= 0.12 ([#44](https://github.com/antonbabenko/pre-commit-terraform/issues/44)) + + + +## [v1.13.0] - 2019-06-17 + +- Added support for terraform_docs for Terraform 0.12 ([#45](https://github.com/antonbabenko/pre-commit-terraform/issues/45)) + + + +## [v1.12.0] - 2019-05-27 + +- Added note about incompatibility of terraform-docs with Terraform 0.12 ([#41](https://github.com/antonbabenko/pre-commit-terraform/issues/41)) +- Fixed broken "maintained badge" +- Update README.md ([#36](https://github.com/antonbabenko/pre-commit-terraform/issues/36)) + + + +## [v1.11.0] - 2019-03-01 + +- Updated changelog +- fix check for errors at the end ([#35](https://github.com/antonbabenko/pre-commit-terraform/issues/35)) + + + +## [v1.10.0] - 2019-02-21 + +- Bump new version +- Add exit code for 'terraform validate' so pre-commit check fails ([#34](https://github.com/antonbabenko/pre-commit-terraform/issues/34)) + + + +## [v1.9.0] - 2019-02-18 + +- Added chglog (hi [@robinbowes](https://github.com/robinbowes) :)) +- Require terraform-docs runs in serial to avoid pre-commit doing parallel operations on similar file paths + + + +## [v1.8.1] - 2018-12-15 + +- Fix bug not letting terraform_docs_replace work in the root directory of a repo + + + +## [v1.8.0] - 2018-12-14 + +- fix typo +- Address requested changes +- Add `--dest` argument +- Address requested changes +- Add new hook for running terraform-docs with replacing README.md from doc in main.tf + + + +## [v1.7.4] - 2018-12-11 + +- Merge remote-tracking branch 'origin/master' into pr25 +- Added followup after [#25](https://github.com/antonbabenko/pre-commit-terraform/issues/25) +- Add feature to pass options to terraform-docs. +- Added license file (fixed [#21](https://github.com/antonbabenko/pre-commit-terraform/issues/21)) + + + +## [v1.7.3] - 2018-05-24 + +- Updated README +- Only run validate if .tf files exist in the directory. ([#20](https://github.com/antonbabenko/pre-commit-terraform/issues/20)) + + + +## [v1.7.2] - 2018-05-20 + +- Replace terraform_docs use of GNU sed with perl ([#15](https://github.com/antonbabenko/pre-commit-terraform/issues/15)) +- Fixes use of md5 for tempfile name ([#16](https://github.com/antonbabenko/pre-commit-terraform/issues/16)) + + + +## [v1.7.1] - 2018-05-16 + +- Run terraform_docs only if README.md is present +- Run terraform_docs only if README.md is present + + + +## [v1.7.0] - 2018-05-16 + +- Added terraform-docs integration ([#13](https://github.com/antonbabenko/pre-commit-terraform/issues/13)) + + + +## [v1.6.0] - 2018-04-21 + +- Allow to have spaces in directories ([#11](https://github.com/antonbabenko/pre-commit-terraform/issues/11)) + + + +## [v1.5.0] - 2018-03-06 + +- Bump new version +- Format tfvars files explicitely, because terraform fmt ignores them ([#9](https://github.com/antonbabenko/pre-commit-terraform/issues/9)) + + + +## [v1.4.0] - 2018-01-24 + +- Updated readme +- Show failed path +- Show failed path +- Show failed path +- Updated scripts +- Added scripts to validate terraform files + + + +## [v1.3.0] - 2018-01-15 + +- Added badges +- Added formatting for tfvars (fixes [#4](https://github.com/antonbabenko/pre-commit-terraform/issues/4)) ([#6](https://github.com/antonbabenko/pre-commit-terraform/issues/6)) + + + +## [v1.2.0] - 2017-06-08 + +- Renamed shell script file to the correct one +- Updated .pre-commit-hooks.yaml +- Updated sha in README +- Exclude .terraform even on subfolders + + + +## [v1.1.0] - 2017-02-04 + +- Copied to .pre-commit-hooks.yaml for compatibility (closes [#1](https://github.com/antonbabenko/pre-commit-terraform/issues/1)) + + + +## v1.0.0 - 2016-09-27 + +- Updated README +- Ready, probably :) +- Initial commit +- Initial commit + + +[Unreleased]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.61.0...HEAD +[v1.61.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.60.0...v1.61.0 +[v1.60.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.59.0...v1.60.0 +[v1.59.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.58.0...v1.59.0 +[v1.58.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.57.0...v1.58.0 +[v1.57.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.56.0...v1.57.0 +[v1.56.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.55.0...v1.56.0 +[v1.55.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.54.0...v1.55.0 +[v1.54.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.53.0...v1.54.0 +[v1.53.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.52.0...v1.53.0 +[v1.52.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.51.0...v1.52.0 +[v1.51.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.50.0...v1.51.0 +[v1.50.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.49.0...v1.50.0 +[v1.49.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.48.0...v1.49.0 +[v1.48.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.47.0...v1.48.0 +[v1.47.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.46.0...v1.47.0 +[v1.46.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.45.0...v1.46.0 +[v1.45.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.44.0...v1.45.0 +[v1.44.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.43.1...v1.44.0 +[v1.43.1]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.43.0...v1.43.1 +[v1.43.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.42.0...v1.43.0 +[v1.42.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.41.0...v1.42.0 +[v1.41.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.40.0...v1.41.0 +[v1.40.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.39.0...v1.40.0 +[v1.39.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.38.0...v1.39.0 +[v1.38.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.37.0...v1.38.0 +[v1.37.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.36.0...v1.37.0 +[v1.36.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.35.0...v1.36.0 +[v1.35.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.34.0...v1.35.0 +[v1.34.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.33.0...v1.34.0 +[v1.33.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.32.0...v1.33.0 +[v1.32.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.31.0...v1.32.0 +[v1.31.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.30.0...v1.31.0 +[v1.30.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.29.0...v1.30.0 +[v1.29.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.28.0...v1.29.0 +[v1.28.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.27.0...v1.28.0 +[v1.27.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.26.0...v1.27.0 +[v1.26.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.25.0...v1.26.0 +[v1.25.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.24.0...v1.25.0 +[v1.24.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.23.0...v1.24.0 +[v1.23.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.22.0...v1.23.0 +[v1.22.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.21.0...v1.22.0 +[v1.21.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.20.0...v1.21.0 +[v1.20.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.19.0...v1.20.0 +[v1.19.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.18.0...v1.19.0 +[v1.18.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.17.0...v1.18.0 +[v1.17.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.16.0...v1.17.0 +[v1.16.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.15.0...v1.16.0 +[v1.15.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.14.0...v1.15.0 +[v1.14.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.13.0...v1.14.0 +[v1.13.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.12.0...v1.13.0 +[v1.12.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.11.0...v1.12.0 +[v1.11.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.10.0...v1.11.0 +[v1.10.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.9.0...v1.10.0 +[v1.9.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.8.1...v1.9.0 +[v1.8.1]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.8.0...v1.8.1 +[v1.8.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.7.4...v1.8.0 +[v1.7.4]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.7.3...v1.7.4 +[v1.7.3]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.7.2...v1.7.3 +[v1.7.2]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.7.1...v1.7.2 +[v1.7.1]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.7.0...v1.7.1 +[v1.7.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.6.0...v1.7.0 +[v1.6.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.5.0...v1.6.0 +[v1.5.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.4.0...v1.5.0 +[v1.4.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.3.0...v1.4.0 +[v1.3.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.2.0...v1.3.0 +[v1.2.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.1.0...v1.2.0 +[v1.1.0]: https://github.com/antonbabenko/pre-commit-terraform/compare/v1.0.0...v1.1.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..184846008 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,179 @@ +ARG TAG=3.10.1-alpine3.15 +FROM python:${TAG} as builder + +WORKDIR /bin_dir + +RUN apk add --no-cache \ + # Builder deps + curl=~7 \ + unzip=~6 && \ + # Upgrade pip for be able get latest Checkov + python3 -m pip install --no-cache-dir --upgrade pip + +ARG PRE_COMMIT_VERSION=${PRE_COMMIT_VERSION:-latest} +ARG TERRAFORM_VERSION=${TERRAFORM_VERSION:-latest} + +# Install pre-commit +RUN [ ${PRE_COMMIT_VERSION} = "latest" ] && pip3 install --no-cache-dir pre-commit \ + || pip3 install --no-cache-dir pre-commit==${PRE_COMMIT_VERSION} + +# Install terraform because pre-commit needs it +RUN if [ "${TERRAFORM_VERSION}" = "latest" ]; then \ + TERRAFORM_VERSION="$(curl -s https://api.github.com/repos/hashicorp/terraform/releases/latest | grep tag_name | grep -o -E -m 1 "[0-9.]+")" \ + ; fi && \ + curl -L "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" > terraform.zip && \ + unzip terraform.zip terraform && rm terraform.zip + +# +# Install tools +# +ARG CHECKOV_VERSION=${CHECKOV_VERSION:-false} +ARG INFRACOST_VERSION=${INFRACOST_VERSION:-false} +ARG TERRAFORM_DOCS_VERSION=${TERRAFORM_DOCS_VERSION:-false} +ARG TERRAGRUNT_VERSION=${TERRAGRUNT_VERSION:-false} +ARG TERRASCAN_VERSION=${TERRASCAN_VERSION:-false} +ARG TFLINT_VERSION=${TFLINT_VERSION:-false} +ARG TFSEC_VERSION=${TFSEC_VERSION:-false} + + +# Tricky thing to install all tools by set only one arg. +# In RUN command below used `. /.env` <- this is sourcing vars that +# specified in step below +ARG INSTALL_ALL=${INSTALL_ALL:-false} +RUN if [ "$INSTALL_ALL" != "false" ]; then \ + echo "export CHECKOV_VERSION=latest" >> /.env && \ + echo "export INFRACOST_VERSION=latest" >> /.env && \ + echo "export TERRAFORM_DOCS_VERSION=latest" >> /.env && \ + echo "export TERRAGRUNT_VERSION=latest" >> /.env && \ + echo "export TERRASCAN_VERSION=latest" >> /.env && \ + echo "export TFLINT_VERSION=latest" >> /.env && \ + echo "export TFSEC_VERSION=latest" >> /.env \ + ; else \ + touch /.env \ + ; fi + + +# Checkov +RUN . /.env && \ + if [ "$CHECKOV_VERSION" != "false" ]; then \ + ( \ + apk add --no-cache gcc=~10 libffi-dev=~3 musl-dev=~1; \ + [ "$CHECKOV_VERSION" = "latest" ] && pip3 install --no-cache-dir checkov \ + || pip3 install --no-cache-dir checkov==${CHECKOV_VERSION}; \ + apk del gcc libffi-dev musl-dev \ + ) \ + ; fi + +# infracost +RUN . /.env && \ + if [ "$INFRACOST_VERSION" != "false" ]; then \ + ( \ + INFRACOST_RELEASES="https://api.github.com/repos/infracost/infracost/releases" && \ + [ "$INFRACOST_VERSION" = "latest" ] && curl -L "$(curl -s ${INFRACOST_RELEASES}/latest | grep -o -E -m 1 "https://.+?-linux-amd64.tar.gz")" > infracost.tgz \ + || curl -L "$(curl -s ${INFRACOST_RELEASES} | grep -o -E "https://.+?v${INFRACOST_VERSION}/infracost-linux-amd64.tar.gz")" > infracost.tgz \ + ) && tar -xzf infracost.tgz && rm infracost.tgz && mv infracost-linux-amd64 infracost \ + ; fi + +# Terraform docs +RUN . /.env && \ + if [ "$TERRAFORM_DOCS_VERSION" != "false" ]; then \ + ( \ + TERRAFORM_DOCS_RELEASES="https://api.github.com/repos/terraform-docs/terraform-docs/releases" && \ + [ "$TERRAFORM_DOCS_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRAFORM_DOCS_RELEASES}/latest | grep -o -E -m 1 "https://.+?-linux-amd64.tar.gz")" > terraform-docs.tgz \ + || curl -L "$(curl -s ${TERRAFORM_DOCS_RELEASES} | grep -o -E "https://.+?v${TERRAFORM_DOCS_VERSION}-linux-amd64.tar.gz")" > terraform-docs.tgz \ + ) && tar -xzf terraform-docs.tgz terraform-docs && rm terraform-docs.tgz && chmod +x terraform-docs \ + ; fi + +# Terragrunt +RUN . /.env \ + && if [ "$TERRAGRUNT_VERSION" != "false" ]; then \ + ( \ + TERRAGRUNT_RELEASES="https://api.github.com/repos/gruntwork-io/terragrunt/releases" && \ + [ "$TERRAGRUNT_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRAGRUNT_RELEASES}/latest | grep -o -E -m 1 "https://.+?/terragrunt_linux_amd64")" > terragrunt \ + || curl -L "$(curl -s ${TERRAGRUNT_RELEASES} | grep -o -E -m 1 "https://.+?v${TERRAGRUNT_VERSION}/terragrunt_linux_amd64")" > terragrunt \ + ) && chmod +x terragrunt \ + ; fi + + +# Terrascan +RUN . /.env && \ + if [ "$TERRASCAN_VERSION" != "false" ]; then \ + ( \ + TERRASCAN_RELEASES="https://api.github.com/repos/accurics/terrascan/releases" && \ + [ "$TERRASCAN_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRASCAN_RELEASES}/latest | grep -o -E -m 1 "https://.+?_Linux_x86_64.tar.gz")" > terrascan.tar.gz \ + || curl -L "$(curl -s ${TERRASCAN_RELEASES} | grep -o -E "https://.+?${TERRASCAN_VERSION}_Linux_x86_64.tar.gz")" > terrascan.tar.gz \ + ) && tar -xzf terrascan.tar.gz terrascan && rm terrascan.tar.gz && \ + ./terrascan init \ + ; fi + +# TFLint +RUN . /.env && \ + if [ "$TFLINT_VERSION" != "false" ]; then \ + ( \ + TFLINT_RELEASES="https://api.github.com/repos/terraform-linters/tflint/releases" && \ + [ "$TFLINT_VERSION" = "latest" ] && curl -L "$(curl -s ${TFLINT_RELEASES}/latest | grep -o -E -m 1 "https://.+?_linux_amd64.zip")" > tflint.zip \ + || curl -L "$(curl -s ${TFLINT_RELEASES} | grep -o -E "https://.+?/v${TFLINT_VERSION}/tflint_linux_amd64.zip")" > tflint.zip \ + ) && unzip tflint.zip && rm tflint.zip \ + ; fi + +# TFSec +RUN . /.env && \ + if [ "$TFSEC_VERSION" != "false" ]; then \ + ( \ + TFSEC_RELEASES="https://api.github.com/repos/aquasecurity/tfsec/releases" && \ + [ "$TFSEC_VERSION" = "latest" ] && curl -L "$(curl -s ${TFSEC_RELEASES}/latest | grep -o -E -m 1 "https://.+?/tfsec-linux-amd64")" > tfsec \ + || curl -L "$(curl -s ${TFSEC_RELEASES} | grep -o -E -m 1 "https://.+?v${TFSEC_VERSION}/tfsec-linux-amd64")" > tfsec \ + ) && chmod +x tfsec \ + ; fi + +# Checking binaries versions and write it to debug file +RUN . /.env && \ + F=tools_versions_info && \ + pre-commit --version >> $F && \ + ./terraform --version | head -n 1 >> $F && \ + (if [ "$CHECKOV_VERSION" != "false" ]; then echo "checkov $(checkov --version)" >> $F; else echo "checkov SKIPPED" >> $F ; fi) && \ + (if [ "$INFRACOST_VERSION" != "false" ]; then echo "$(./infracost --version)" >> $F; else echo "infracost SKIPPED" >> $F ; fi) && \ + (if [ "$TERRAFORM_DOCS_VERSION" != "false" ]; then ./terraform-docs --version >> $F; else echo "terraform-docs SKIPPED" >> $F ; fi) && \ + (if [ "$TERRAGRUNT_VERSION" != "false" ]; then ./terragrunt --version >> $F; else echo "terragrunt SKIPPED" >> $F ; fi) && \ + (if [ "$TERRASCAN_VERSION" != "false" ]; then echo "terrascan $(./terrascan version)" >> $F; else echo "terrascan SKIPPED" >> $F ; fi) && \ + (if [ "$TFLINT_VERSION" != "false" ]; then ./tflint --version >> $F; else echo "tflint SKIPPED" >> $F ; fi) && \ + (if [ "$TFSEC_VERSION" != "false" ]; then echo "tfsec $(./tfsec --version)" >> $F; else echo "tfsec SKIPPED" >> $F ; fi) && \ + echo -e "\n\n" && cat $F && echo -e "\n\n" + + + +FROM python:${TAG} + +RUN apk add --no-cache \ + # pre-commit deps + git=~2 \ + # All hooks deps + bash=~5 + +# Copy tools +COPY --from=builder \ + # Needed for all hooks + /usr/local/bin/pre-commit \ + # Hooks and terraform binaries + /bin_dir/ \ + /usr/local/bin/checkov* \ + /usr/bin/ +# Copy pre-commit packages +COPY --from=builder /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python3.10/site-packages/ +# Copy terrascan policies +COPY --from=builder /root/ /root/ + +# Install hooks extra deps +RUN if [ "$(grep -o '^terraform-docs SKIPPED$' /usr/bin/tools_versions_info)" = "" ]; then \ + apk add --no-cache perl=~5 \ + ; fi && \ + if [ "$(grep -o '^infracost SKIPPED$' /usr/bin/tools_versions_info)" = "" ]; then \ + apk add --no-cache jq=~1 \ + ; fi + +ENV PRE_COMMIT_COLOR=${PRE_COMMIT_COLOR:-always} + +ENV INFRACOST_API_KEY=${INFRACOST_API_KEY:-} +ENV INFRACOST_SKIP_UPDATE_CHECK=${INFRACOST_SKIP_UPDATE_CHECK:-false} + +ENTRYPOINT [ "pre-commit" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..2755ae9bc --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017 Anton Babenko + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 8a3fa373f..87a038494 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,632 @@ -# pre-commit-terraform hook +# Collection of git hooks for Terraform to be used with [pre-commit framework](http://pre-commit.com/) -[![Github tag](https://img.shields.io/github/tag/antonbabenko/pre-commit-terraform.svg)](https://github.com/antonbabenko/pre-commit-terraform/releases) ![](https://img.shields.io/maintenance/yes/2018.svg) [![Help Contribute to Open Source](https://www.codetriage.com/antonbabenko/pre-commit-terraform/badges/users.svg)](https://www.codetriage.com/antonbabenko/pre-commit-terraform) +[![Github tag](https://img.shields.io/github/tag/antonbabenko/pre-commit-terraform.svg)](https://github.com/antonbabenko/pre-commit-terraform/releases) ![maintenance status](https://img.shields.io/maintenance/yes/2021.svg) [![Help Contribute to Open Source](https://www.codetriage.com/antonbabenko/pre-commit-terraform/badges/users.svg)](https://www.codetriage.com/antonbabenko/pre-commit-terraform) -Several [pre-commit](http://pre-commit.com/) hooks to keep Terraform configurations (both `*.tf` and `*.tfvars`) in a good shape: -* `terraform_fmt` - Rewrites all Terraform configuration files to a canonical format. -* `terraform_validate_no_variables` - Validates all Terraform configuration files without checking whether all required variables were set. -* `terraform_validate_with_variables` - Validates all Terraform configuration files and checks whether all required variables were specified. +Want to contribute? Check [open issues](https://github.com/antonbabenko/pre-commit-terraform/issues?q=label%3A%22good+first+issue%22+is%3Aopen+sort%3Aupdated-desc) and [contributing notes](/.github/CONTRIBUTING.md). -Note that `terraform_validate_no_variables` and `terraform_validate_with_variables` will not work if variables are being set dynamically (eg, when using [Terragrunt](https://github.com/gruntwork-io/terragrunt)). Use `terragrunt validate` command instead. +## Sponsors -An example `.pre-commit-config.yaml`: + -```yaml -- repo: git://github.com/antonbabenko/pre-commit-terraform - sha: v1.6.0 +
+env0 + +Automated provisioning of Terraform workflows and Infrastructure as Code. + +
+infracost + + + +Cloud cost estimates for Terraform. + +If you are using `pre-commit-terraform` already or want to support its development and [many other open-source projects](https://github.com/antonbabenko/terraform-aws-devops), please become a [GitHub Sponsor](https://github.com/sponsors/antonbabenko)! + + +## Table of content + +* [Sponsors](#sponsors) +* [Table of content](#table-of-content) +* [How to install](#how-to-install) + * [1. Install dependencies](#1-install-dependencies) + * [2. Install the pre-commit hook globally](#2-install-the-pre-commit-hook-globally) + * [3. Add configs and hooks](#3-add-configs-and-hooks) + * [4. Run](#4-run) +* [Available Hooks](#available-hooks) +* [Hooks usage notes and examples](#hooks-usage-notes-and-examples) + * [checkov](#checkov) + * [infracost_breakdown](#infracost_breakdown) + * [terraform_docs](#terraform_docs) + * [terraform_docs_replace (deprecated)](#terraform_docs_replace-deprecated) + * [terraform_fmt](#terraform_fmt) + * [terraform_providers_lock](#terraform_providers_lock) + * [terraform_tflint](#terraform_tflint) + * [terraform_tfsec](#terraform_tfsec) + * [terraform_validate](#terraform_validate) + * [terrascan](#terrascan) +* [Authors](#authors) +* [License](#license) + +## How to install + +### 1. Install dependencies + + + +* [`pre-commit`](https://pre-commit.com/#install) +* [`checkov`](https://github.com/bridgecrewio/checkov) required for `checkov` hook. +* [`terraform-docs`](https://github.com/terraform-docs/terraform-docs) required for `terraform_docs` hook. +* [`terragrunt`](https://terragrunt.gruntwork.io/docs/getting-started/install/) required for `terragrunt_validate` hook. +* [`terrascan`](https://github.com/accurics/terrascan) required for `terrascan` hook. +* [`TFLint`](https://github.com/terraform-linters/tflint) required for `terraform_tflint` hook. +* [`TFSec`](https://github.com/liamg/tfsec) required for `terraform_tfsec` hook. +* [`infracost`](https://github.com/infracost/infracost) required for `infracost_breakdown` hook. +* [`jq`](https://github.com/stedolan/jq) required for `infracost_breakdown` hook. + +
Docker
+ +**Pull docker image with all hooks**: + +```bash +TAG=latest +docker pull ghcr.io/antonbabenko/pre-commit-terraform:$TAG +``` + +All available tags [here](https://github.com/antonbabenko/pre-commit-terraform/pkgs/container/pre-commit-terraform/versions). + +**Build from scratch**: + +When `--build-arg` is not specified, the latest version of `pre-commit` and `terraform` will be only installed. + +```bash +git clone git@github.com:antonbabenko/pre-commit-terraform.git +cd pre-commit-terraform +# Install the latest versions of all the tools +docker build -t pre-commit-terraform --build-arg INSTALL_ALL=true . +``` + +To install a specific version of individual tools, define it using `--build-arg` arguments or set it to `latest`: + +```bash +docker build -t pre-commit-terraform \ + --build-arg PRE_COMMIT_VERSION=latest \ + --build-arg TERRAFORM_VERSION=latest \ + --build-arg CHECKOV_VERSION=2.0.405 \ + --build-arg INFRACOST_VERSION=latest \ + --build-arg TERRAFORM_DOCS_VERSION=0.15.0 \ + --build-arg TERRAGRUNT_VERSION=latest \ + --build-arg TERRASCAN_VERSION=1.10.0 \ + --build-arg TFLINT_VERSION=0.31.0 \ + --build-arg TFSEC_VERSION=latest \ + . +``` + +Set `-e PRE_COMMIT_COLOR=never` to disable the color output in `pre-commit`. + +
+ + +
MacOS
+ +[`coreutils`](https://formulae.brew.sh/formula/coreutils) is required for `terraform_validate` hook on MacOS (due to use of `realpath`). + +```bash +brew install pre-commit terraform-docs tflint tfsec coreutils checkov terrascan infracost jq +``` + +
+ +
Ubuntu 18.04
+ +```bash +sudo apt update +sudo apt install -y unzip software-properties-common +sudo add-apt-repository ppa:deadsnakes/ppa +sudo apt install -y python3.7 python3-pip +python3 -m pip install --upgrade pip +pip3 install --no-cache-dir pre-commit +python3.7 -m pip install -U checkov +curl -L "$(curl -s https://api.github.com/repos/terraform-docs/terraform-docs/releases/latest | grep -o -E -m 1 "https://.+?-linux-amd64.tar.gz")" > terraform-docs.tgz && tar -xzf terraform-docs.tgz && rm terraform-docs.tgz && chmod +x terraform-docs && sudo mv terraform-docs /usr/bin/ +curl -L "$(curl -s https://api.github.com/repos/terraform-linters/tflint/releases/latest | grep -o -E -m 1 "https://.+?_linux_amd64.zip")" > tflint.zip && unzip tflint.zip && rm tflint.zip && sudo mv tflint /usr/bin/ +curl -L "$(curl -s https://api.github.com/repos/aquasecurity/tfsec/releases/latest | grep -o -E -m 1 "https://.+?tfsec-linux-amd64")" > tfsec && chmod +x tfsec && sudo mv tfsec /usr/bin/ +curl -L "$(curl -s https://api.github.com/repos/accurics/terrascan/releases/latest | grep -o -E -m 1 "https://.+?_Linux_x86_64.tar.gz")" > terrascan.tar.gz && tar -xzf terrascan.tar.gz terrascan && rm terrascan.tar.gz && sudo mv terrascan /usr/bin/ && terrascan init +sudo apt install -y jq && \ +curl -L "$(curl -s https://api.github.com/repos/infracost/infracost/releases/latest | grep -o -E -m 1 "https://.+?-linux-amd64.tar.gz")" > infracost.tgz && tar -xzf infracost.tgz && rm infracost.tgz && sudo mv infracost-linux-amd64 /usr/bin/infracost && infracost register +``` + +
+ + +
Ubuntu 20.04
+ +```bash +sudo apt update +sudo apt install -y unzip software-properties-common python3 python3-pip +python3 -m pip install --upgrade pip +pip3 install --no-cache-dir pre-commit +pip3 install --no-cache-dir checkov +curl -L "$(curl -s https://api.github.com/repos/terraform-docs/terraform-docs/releases/latest | grep -o -E -m 1 "https://.+?-linux-amd64.tar.gz")" > terraform-docs.tgz && tar -xzf terraform-docs.tgz terraform-docs && rm terraform-docs.tgz && chmod +x terraform-docs && sudo mv terraform-docs /usr/bin/ +curl -L "$(curl -s https://api.github.com/repos/accurics/terrascan/releases/latest | grep -o -E -m 1 "https://.+?_Linux_x86_64.tar.gz")" > terrascan.tar.gz && tar -xzf terrascan.tar.gz terrascan && rm terrascan.tar.gz && sudo mv terrascan /usr/bin/ && terrascan init +curl -L "$(curl -s https://api.github.com/repos/terraform-linters/tflint/releases/latest | grep -o -E -m 1 "https://.+?_linux_amd64.zip")" > tflint.zip && unzip tflint.zip && rm tflint.zip && sudo mv tflint /usr/bin/ +curl -L "$(curl -s https://api.github.com/repos/aquasecurity/tfsec/releases/latest | grep -o -E -m 1 "https://.+?tfsec-linux-amd64")" > tfsec && chmod +x tfsec && sudo mv tfsec /usr/bin/ +sudo apt install -y jq && \ +curl -L "$(curl -s https://api.github.com/repos/infracost/infracost/releases/latest | grep -o -E -m 1 "https://.+?-linux-amd64.tar.gz")" > infracost.tgz && tar -xzf infracost.tgz && rm infracost.tgz && sudo mv infracost-linux-amd64 /usr/bin/infracost && infracost register +``` + +
+ + + +### 2. Install the pre-commit hook globally + +> Note: not needed if you use the Docker image + +```bash +DIR=~/.git-template +git config --global init.templateDir ${DIR} +pre-commit init-templatedir -t pre-commit ${DIR} +``` + +### 3. Add configs and hooks + +Step into the repository you want to have the pre-commit hooks installed and run: + +```bash +git init +cat < .pre-commit-config.yaml +repos: +- repo: https://github.com/antonbabenko/pre-commit-terraform + rev: # Get the latest from: https://github.com/antonbabenko/pre-commit-terraform/releases hooks: - id: terraform_fmt + - id: terraform_docs +EOF +``` + +### 4. Run + +Execute this command to run `pre-commit` on all files in the repository (not only changed files): + +```bash +pre-commit run -a +``` + +Or, using Docker ([available tags](https://github.com/antonbabenko/pre-commit-terraform/pkgs/container/pre-commit-terraform/versions)): + +```bash +TAG=latest +docker run -v $(pwd):/lint -w /lint ghcr.io/antonbabenko/pre-commit-terraform:$TAG run -a +``` + +Execute this command to list the versions of the tools in Docker: + +```bash +TAG=latest +docker run --entrypoint cat ghcr.io/antonbabenko/pre-commit-terraform:$TAG /usr/bin/tools_versions_info +``` + +## Available Hooks + +There are several [pre-commit](https://pre-commit.com/) hooks to keep Terraform configurations (both `*.tf` and `*.tfvars`) and Terragrunt configurations (`*.hcl`) in a good shape: + + +| Hook name | Description | Dependencies
[Install instructions here](#1-install-dependencies) | +| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `checkov` | [checkov](https://github.com/bridgecrewio/checkov) static analysis of terraform templates to spot potential security issues. [Hook notes](#checkov) | `checkov`
Ubuntu deps: `python3`, `python3-pip` | +| `infracost_breakdown` | Check how much your infra costs with [infracost](https://github.com/infracost/infracost). [Hook notes](#infracost_breakdown) | `infracost`, `jq`, [Infracost API key](https://www.infracost.io/docs/#2-get-api-key) | +| `terraform_docs_replace` | Runs `terraform-docs` and pipes the output directly to README.md. **DEPRECATED**, see [#248](https://github.com/antonbabenko/pre-commit-terraform/issues/248). [Hook notes](#terraform_docs_replace-deprecated) | `python3`, `terraform-docs` | +| `terraform_docs_without_`
`aggregate_type_defaults` | Inserts input and output documentation into `README.md` without aggregate type defaults. Hook notes same as for [terraform_docs](#terraform_docs) | `terraform-docs` | +| `terraform_docs` | Inserts input and output documentation into `README.md`. Recommended. [Hook notes](#terraform_docs) | `terraform-docs` | +| `terraform_fmt` | Reformat all Terraform configuration files to a canonical format. [Hook notes](#terraform_fmt) | - | +| `terraform_providers_lock` | Updates provider signatures in [dependency lock files](https://www.terraform.io/docs/cli/commands/providers/lock.html). [Hook notes](#terraform_providers_lock) | - | +| `terraform_tflint` | Validates all Terraform configuration files with [TFLint](https://github.com/terraform-linters/tflint). [Available TFLint rules](https://github.com/terraform-linters/tflint/tree/master/docs/rules#rules). [Hook notes](#terraform_tflint). | `tflint` | +| `terraform_tfsec` | [TFSec](https://github.com/aquasecurity/tfsec) static analysis of terraform templates to spot potential security issues. [Hook notes](#terraform_tfsec) | `tfsec` | +| `terraform_validate` | Validates all Terraform configuration files. [Hook notes](#terraform_validate) | - | +| `terragrunt_fmt` | Reformat all [Terragrunt](https://github.com/gruntwork-io/terragrunt) configuration files (`*.hcl`) to a canonical format. | `terragrunt` | +| `terragrunt_validate` | Validates all [Terragrunt](https://github.com/gruntwork-io/terragrunt) configuration files (`*.hcl`) | `terragrunt` | +| `terrascan` | [terrascan](https://github.com/accurics/terrascan) Detect compliance and security violations. [Hook notes](#terrascan) | `terrascan` | + + +Check the [source file](https://github.com/antonbabenko/pre-commit-terraform/blob/master/.pre-commit-hooks.yaml) to know arguments used for each hook. + +## Hooks usage notes and examples + +### checkov + +For [checkov](https://github.com/bridgecrewio/checkov) you need to specify each argument separately: + +```yaml +- id: checkov + args: [ + "-d", ".", + "--skip-check", "CKV2_AWS_8", + ] +``` + +### infracost_breakdown + +`infracost_breakdown` executes `infracost breakdown` command and compare the estimated costs with those specified in the hook-config. `infracost breakdown` normally runs `terraform init`, `terraform plan`, and calls Infracost Cloud Pricing API (remote version or [self-hosted version](https://www.infracost.io/docs/cloud_pricing_api/self_hosted)). + +Unlike most other hooks, this hook triggers once if there are any changed files in the repository. + +1. `infracost_breakdown` supports [all `infracost breakdown` arguments](https://www.infracost.io/docs/#useful-options). The following example only shows costs: + + ```yaml + - id: infracost_breakdown + args: + - --args=--path=./env/dev + verbose: true # Always show costs + ``` + +
Output + + ```bash + Running in "env/dev" + + Summary: { + "unsupportedResourceCounts": { + "aws_sns_topic_subscription": 1 + } + } + + Total Monthly Cost: 86.83 USD + Total Monthly Cost (diff): 86.83 USD + ``` + +
+ +2. (Optionally) Define `cost constrains` the hook should evaluate successfully in order to pass: + + ```yaml + - id: infracost_breakdown + args: + - --args=--path=./env/dev + - --hook-config='.totalHourlyCost|tonumber > 0.1' + - --hook-config='.totalHourlyCost|tonumber > 1' + - --hook-config='.projects[].diff.totalMonthlyCost|tonumber != 10000' + - --hook-config='.currency == "USD"' + ``` + +
Output + + ```bash + Running in "env/dev" + Passed: .totalHourlyCost|tonumber > 0.1 0.11894520547945205 > 0.1 + Failed: .totalHourlyCost|tonumber > 1 0.11894520547945205 > 1 + Passed: .projects[].diff.totalMonthlyCost|tonumber !=10000 86.83 != 10000 + Passed: .currency == "USD" "USD" == "USD" + + Summary: { + "unsupportedResourceCounts": { + "aws_sns_topic_subscription": 1 + } + } + + Total Monthly Cost: 86.83 USD + Total Monthly Cost (diff): 86.83 USD + ``` + +
+ + * Only one path per one hook (`- id: infracost_breakdown`) is allowed. + * Set `verbose: true` to see cost even when the checks are passed. + * Hook uses `jq` to process the cost estimation report returned by `infracost breakdown` command + * Expressions defined as `--hook-config` argument should be in a jq-compatible format (e.g. `.totalHourlyCost`, `.totalMonthlyCost`) + To study json output produced by `infracost`, run the command `infracost breakdown -p PATH_TO_TF_DIR --format json`, and explore it on [jqplay.org](https://jqplay.org/). + * Supported comparison operators: `<`, `<=`, `==`, `!=`, `>=`, `>`. + * Most useful paths and checks: + * `.totalHourlyCost` (same as `.projects[].breakdown.totalHourlyCost`) - show total hourly infra cost + * `.totalMonthlyCost` (same as `.projects[].breakdown.totalMonthlyCost`) - show total monthly infra cost + * `.projects[].diff.totalHourlyCost` - show the difference in hourly cost for the existing infra and tf plan + * `.projects[].diff.totalMonthlyCost` - show the difference in monthly cost for the existing infra and tf plan + * `.diffTotalHourlyCost` (for Infracost version 0.9.12 or newer) or `[.projects[].diff.totalMonthlyCost | select (.!=null) | tonumber] | add` (for Infracost older than 0.9.12) + * To disable hook color output, set `PRE_COMMIT_COLOR=never` env var. + +3. **Docker usage**. In `docker build` or `docker run` command: + * You need to provide [Infracost API key](https://www.infracost.io/docs/integrations/environment_variables/#infracost_api_key) via `-e INFRACOST_API_KEY=`. By default, it is saved in `~/.config/infracost/credentials.yml` + * Set `-e INFRACOST_SKIP_UPDATE_CHECK=true` to [skip the Infracost update check](https://www.infracost.io/docs/integrations/environment_variables/#infracost_skip_update_check) if you use this hook as part of your CI/CD pipeline. + +### terraform_docs + +1. `terraform_docs` and `terraform_docs_without_aggregate_type_defaults` will insert/update documentation generated by [terraform-docs](https://github.com/terraform-docs/terraform-docs) framed by markers: + + ```txt + + + + ``` + + if they are present in `README.md`. + +2. It is possible to pass additional arguments to shell scripts when using `terraform_docs` and `terraform_docs_without_aggregate_type_defaults`. + +3. It is possible to automatically: + * create a documentation file + * extend existing documentation file by appending markers to the end of the file (see item 1 above) + * use different filename for the documentation (default is `README.md`) + + ```yaml + - id: terraform_docs + args: + - --hook-config=--path-to-file=README.md # Valid UNIX path. I.e. ../TFDOC.md or docs/README.md etc. + - --hook-config=--add-to-existing-file=true # Boolean. true or false + - --hook-config=--create-file-if-not-exist=true # Boolean. true or false + ``` + +4. You can provide [any configuration available in `terraform-docs`](https://terraform-docs.io/user-guide/configuration/) as an argument to `terraform_doc` hook, for example: + + ```yaml + - id: terraform_docs + args: + - --args=--config=.terraform-docs.yml + +5. If you need some exotic settings, it can be done too. I.e. this one generates HCL files: + + ```yaml + - id: terraform_docs + args: + - tfvars hcl --output-file terraform.tfvars.model . + ``` + +### terraform_docs_replace (deprecated) + +**DEPRECATED**. Will be merged in [`terraform_docs`](#terraform_docs). See [#248](https://github.com/antonbabenko/pre-commit-terraform/issues/248) for details. + +`terraform_docs_replace` replaces the entire README.md rather than doing string replacement between markers. Put your additional documentation at the top of your `main.tf` for it to be pulled in. The optional `--dest` argument lets you change the filename that gets created/modified. + +Example: + +```yaml +- id: terraform_docs_replace + args: + - --sort-by-required + - --dest=TEST.md ``` -Enjoy the clean code! +### terraform_fmt + +1. `terraform_fmt` supports custom arguments so you can pass [supported flags](https://www.terraform.io/docs/cli/commands/fmt.html#usage). Eg: + + ```yaml + - id: terraform_fmt + args: + - --args=-no-color + - --args=-diff + - --args=-write=false + ``` + +### terraform_providers_lock + +1. The hook requires Terraform 0.14 or later. +2. The hook invokes two operations that can be really slow: + * `terraform init` (in case `.terraform` directory is not initialised) + * `terraform providers lock` + + Both operations require downloading data from remote Terraform registries, and not all of that downloaded data or meta-data is currently being cached by Terraform. + +3. `terraform_providers_lock` supports custom arguments: + + ```yaml + - id: terraform_providers_lock + args: + - --args=-platform=windows_amd64 + - --args=-platform=darwin_amd64 + ``` + +4. It may happen that Terraform working directory (`.terraform`) already exists but not in the best condition (eg, not initialized modules, wrong version of Terraform, etc.). To solve this problem, you can find and delete all `.terraform` directories in your repository: + + ```bash + echo " + function rm_terraform { + find . -name ".terraform*" -print0 | xargs -0 rm -r + } + " >>~/.bashrc + + # Reload shell and use `rm_terraform` command in the repo root + ``` + + `terraform_providers_lock` hook will try to reinitialize directories before running the `terraform providers lock` command. + +### terraform_tflint + +1. `terraform_tflint` supports custom arguments so you can enable module inspection, deep check mode, etc. + + Example: + + ```yaml + - id: terraform_tflint + args: + - --args=--deep + - --args=--enable-rule=terraform_documented_variables + ``` + +2. When you have multiple directories and want to run `tflint` in all of them and share a single config file, it is impractical to hard-code the path to the `.tflint.hcl` file. The solution is to use the `__GIT_WORKING_DIR__` placeholder which will be replaced by `terraform_tflint` hooks with Git working directory (repo root) at run time. For example: + + ```yaml + - id: terraform_tflint + args: + - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl + ``` + + +### terraform_tfsec + +1. `terraform_tfsec` will consume modified files that pre-commit + passes to it, so you can perform whitelisting of directories + or files to run against via [files](https://pre-commit.com/#config-files) + pre-commit flag + + Example: + + ```yaml + - id: terraform_tfsec + files: ^prd-infra/ + ``` + + The above will tell pre-commit to pass down files from the `prd-infra/` folder + only such that the underlying `tfsec` tool can run against changed files in this + directory, ignoring any other folders at the root level + +2. To ignore specific warnings, follow the convention from the +[documentation](https://github.com/aquasecurity/tfsec#ignoring-warnings). + + Example: + + ```hcl + resource "aws_security_group_rule" "my-rule" { + type = "ingress" + cidr_blocks = ["0.0.0.0/0"] #tfsec:ignore:AWS006 + } + ``` + +3. `terraform_tfsec` supports custom arguments, so you can pass supported `--no-color` or `--format` (output), `-e` (exclude checks) flags: + + ```yaml + - id: terraform_tfsec + args: + - > + --args=--format json + --no-color + -e aws-s3-enable-bucket-logging,aws-s3-specify-public-access-block + ``` + +4. When you have multiple directories and want to run `tfsec` in all of them and share a single config file - use the `__GIT_WORKING_DIR__` placeholder. It will be replaced by `terraform_tfsec` hooks with Git working directory (repo root) at run time. For example: + + ```yaml + - id: terraform_tfsec + args: + - --args=--config-file=__GIT_WORKING_DIR__/.tfsec.json + ``` + + Otherwise, will be used files that located in sub-folders: + + ```yaml + - id: terraform_tfsec + args: + - --args=--config-file=.tfsec.json + ``` + +### terraform_validate + +1. `terraform_validate` supports custom arguments so you can pass supported `-no-color` or `-json` flags: + + ```yaml + - id: terraform_validate + args: + - --args=-json + - --args=-no-color + ``` + +2. `terraform_validate` also supports custom environment variables passed to the pre-commit runtime: + + ```yaml + - id: terraform_validate + args: + - --envs=AWS_DEFAULT_REGION="us-west-2" + - --envs=AWS_ACCESS_KEY_ID="anaccesskey" + - --envs=AWS_SECRET_ACCESS_KEY="asecretkey" + ``` + +3. `terraform_validate` also supports passing custom arguments to its `terraform init`: + + ```yaml + - id: terraform_validate + args: + - --init-args=-lockfile=readonly + ``` + +4. It may happen that Terraform working directory (`.terraform`) already exists but not in the best condition (eg, not initialized modules, wrong version of Terraform, etc.). To solve this problem, you can find and delete all `.terraform` directories in your repository: + + ```bash + echo " + function rm_terraform { + find . -name ".terraform*" -print0 | xargs -0 rm -r + } + " >>~/.bashrc + + # Reload shell and use `rm_terraform` command in the repo root + ``` + + `terraform_validate` hook will try to reinitialize them before running the `terraform validate` command. + + **Warning:** If you use Terraform workspaces, DO NOT use this workaround ([details](https://github.com/antonbabenko/pre-commit-terraform/issues/203#issuecomment-918791847)). Wait to [`force-init`](https://github.com/antonbabenko/pre-commit-terraform/issues/224) option implementation. + +5. `terraform_validate` in a repo with Terraform module, written using Terraform 0.15+ and which uses provider `configuration_aliases` ([Provider Aliases Within Modules](https://www.terraform.io/language/modules/develop/providers#provider-aliases-within-modules)), errors out. + + When running the hook against Terraform code where you have provider `configuration_aliases` defined in a `required_providers` configuration block, terraform will throw an error like: + > + > + > Error: Provider configuration not present + > To work with its original provider configuration at provider["registry.terraform.io/hashicorp/aws"]. is required, but it has been removed. This occurs when a provider configuration is removed while + > objects created by that provider still exist in the state. Re-add the provider configuration to destroy , after which you can remove the provider configuration again. + + This is a [known issue](https://github.com/hashicorp/terraform/issues/28490) with Terraform and how providers are initialized in Terraform 0.15 and later. To work around this you can add an `exclude` parameter to the configuration of `terraform_validate` hook like this: + ```yaml + - id: terraform_validate + exclude: [^/]+$ + ``` + This will exclude the root directory from being processed by this hook. Then add a subdirectory like "examples" or "tests" and put an example implementation in place that defines the providers with the proper aliases, and this will give you validation of your module through the example. If instead you are using this with multiple modules in one repository you'll want to set the path prefix in the regular expression, such as `exclude: modules/offendingmodule/[^/]+$`. + + Alternately, you can use [terraform-config-inspect](https://github.com/hashicorp/terraform-config-inspect) and use a variant of [this script](https://github.com/bendrucker/terraform-configuration-aliases-action/blob/main/providers.sh) to generate a providers file at runtime: + + ```bash + terraform-config-inspect --json . | jq -r ' + [.required_providers[].aliases] + | flatten + | del(.[] | select(. == null)) + | reduce .[] as $entry ( + {}; + .provider[$entry.name] //= [] | .provider[$entry.name] += [{"alias": $entry.alias}] + ) + ' | tee aliased-providers.tf.json + ``` + + Save it as `.generate-providers.sh` in the root of your repository and add a `pre-commit` hook to run it before all other hooks, like so: + ```yaml + - repos: + - repo: local + hooks: + - id: generate-terraform-providers + name: generate-terraform-providers + require_serial: true + entry: .generate-providers.sh + language: script + files: \.tf(vars)?$ + pass_filenames: false + + - repo: https://github.com/pre-commit/pre-commit-hooks + [...] + ``` + + **Note:** The latter method will leave an "aliased-providers.tf.json" file in your repo. You will either want to automate a way to clean this up or add it to your `.gitignore` or both. + +### terrascan + +1. `terrascan` supports custom arguments so you can pass supported flags like `--non-recursive` and `--policy-type` to disable recursive inspection and set the policy type respectively: + + ```yaml + - id: terrascan + args: + - --args=--non-recursive # avoids scan errors on subdirectories without Terraform config files + - --args=--policy-type=azure + ``` + + See the `terrascan run -h` command line help for available options. + +2. Use the `--args=--verbose` parameter to see the rule ID in the scaning output. Usuful to skip validations. +3. Use `--skip-rules="ruleID1,ruleID2"` parameter to skip one or more rules globally while scanning (e.g.: `--args=--skip-rules="ruleID1,ruleID2"`). +4. Use the syntax `#ts:skip=RuleID optional_comment` inside a resource to skip the rule for that resource. + +## Authors + +This repository is managed by [Anton Babenko](https://github.com/antonbabenko) with help from these awesome contributors: + + + + + + + +## License + +MIT licensed. See [LICENSE](LICENSE) for full details. diff --git a/assets/env0.png b/assets/env0.png new file mode 100644 index 000000000..da3eec1e3 Binary files /dev/null and b/assets/env0.png differ diff --git a/assets/infracost.png b/assets/infracost.png new file mode 100644 index 000000000..bacbd0470 Binary files /dev/null and b/assets/infracost.png differ diff --git a/hooks.yaml b/hooks.yaml deleted file mode 100644 index 051e92d2b..000000000 --- a/hooks.yaml +++ /dev/null @@ -1,39 +0,0 @@ -- id: terraform_fmt - name: Terraform fmt - description: Rewrites all Terraform configuration files to a canonical format. - entry: terraform_fmt.sh - language: script - files: (\.tf|\.tfvars)$ - exclude: \.terraform\/.*$ - -- id: terraform_docs - name: Terraform Docs - description: Creates readme for terraform modules. - entry: terraform_docs.sh - language: script - files: (\.tf)$ - exclude: \.terraform\/.*$ - -- id: terraform_readme - name: Terraform Readme - description: Creates a README for Terraform modules using `terraform_config_inspect`. - entry: terraform_readme.sh - language: script - files: (\.tf)$ - exclude: \.terraform\/.*$ - -- id: terraform_validate_no_variables - name: Terraform validate without variables - description: Validates all Terraform configuration files without checking whether all required variables were set (basic check). - entry: terraform_validate_no_variables.sh - language: script - files: (\.tf|\.tfvars)$ - exclude: \.terraform\/.*$ - -- id: terraform_validate_with_variables - name: Terraform validate with variables - description: Validates all Terraform configuration files and checks whether all required variables were specified. - entry: terraform_validate_with_variables.sh - language: script - files: (\.tf|\.tfvars)$ - exclude: \.terraform\/.*$ diff --git a/hooks/__init__.py b/hooks/__init__.py new file mode 100644 index 000000000..ff01067a9 --- /dev/null +++ b/hooks/__init__.py @@ -0,0 +1,4 @@ +print( + '`terraform_docs_replace` hook is DEPRECATED.' + 'For details, see https://github.com/antonbabenko/pre-commit-terraform/issues/248' +) diff --git a/hooks/_common.sh b/hooks/_common.sh new file mode 100644 index 000000000..8011e45e1 --- /dev/null +++ b/hooks/_common.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +set -eo pipefail + +####################################################################### +# Init arguments parser +# Arguments: +# script_dir - absolute path to hook dir location +####################################################################### +function common::initialize { + local -r script_dir=$1 + # source getopt function + # shellcheck source=../lib_getopt + . "$script_dir/../lib_getopt" +} + +####################################################################### +# Parse args and filenames passed to script and populate respective +# global variables with appropriate values +# Globals (init and populate): +# ARGS (array) arguments that configure wrapped tool behavior +# HOOK_CONFIG (array) arguments that configure hook behavior +# FILES (array) filenames to check +# Arguments: +# $@ (array) all specified in `hooks.[].args` in +# `.pre-commit-config.yaml` and filenames. +####################################################################### +function common::parse_cmdline { + # common global arrays. + # Populated via `common::parse_cmdline` and can be used inside hooks' functions + declare -g -a ARGS=() HOOK_CONFIG=() FILES=() + + local argv + argv=$(getopt -o a:,h: --long args:,hook-config: -- "$@") || return + eval "set -- $argv" + + for argv; do + case $argv in + -a | --args) + shift + ARGS+=("$1") + shift + ;; + -h | --hook-config) + shift + HOOK_CONFIG+=("$1;") + shift + ;; + --) + shift + # shellcheck disable=SC2034 # Variable is used + FILES=("$@") + break + ;; + esac + done +} + +####################################################################### +# This is a workaround to improve performance when all files are passed +# See: https://github.com/antonbabenko/pre-commit-terraform/issues/309 +# Arguments: +# hook_id (string) hook ID, see `- id` for details in .pre-commit-hooks.yaml file +# files (array) filenames to check +# Outputs: +# Return 0 if `-a|--all` arg was passed to `pre-commit` +####################################################################### +function common::is_hook_run_on_whole_repo { + local -r hook_id="$1" + shift 1 + local -a -r files=("$@") + # get directory containing `.pre-commit-hooks.yaml` file + local -r root_config_dir="$(dirname "$(dirname "$(realpath "${BASH_SOURCE[0]}")")")" + # get included and excluded files from .pre-commit-hooks.yaml file + local -r hook_config_block=$(sed -n "/^- id: $hook_id$/,/^$/p" "$root_config_dir/.pre-commit-hooks.yaml") + local -r included_files=$(awk '$1 == "files:" {print $2; exit}' <<< "$hook_config_block") + local -r excluded_files=$(awk '$1 == "exclude:" {print $2; exit}' <<< "$hook_config_block") + # sorted string with the files passed to the hook by pre-commit + local -r files_to_check=$(printf '%s\n' "${files[@]}" | sort | tr '\n' ' ') + # git ls-files sorted string + local all_files_that_can_be_checked + + if [ -z "$excluded_files" ]; then + all_files_that_can_be_checked=$(git ls-files | sort | grep -e "$included_files" | tr '\n' ' ') + else + all_files_that_can_be_checked=$(git ls-files | sort | grep -e "$included_files" | grep -v -e "$excluded_files" | tr '\n' ' ') + fi + + if [ "$files_to_check" == "$all_files_that_can_be_checked" ]; then + return 0 + else + return 1 + fi +} + +####################################################################### +# Hook execution boilerplate logic which is common to hooks, that run +# on per dir basis. +# 1. Because hook runs on whole dir, reduce file paths to uniq dir paths +# 2. Run for each dir `per_dir_hook_unique_part`, on all paths +# 2.1. If at least 1 check failed - change exit code to non-zero +# 3. Complete hook execution and return exit code +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +# hook_id (string) hook ID, see `- id` for details in .pre-commit-hooks.yaml file +# files (array) filenames to check +####################################################################### +function common::per_dir_hook { + local -r args="$1" + local -r hook_id="$2" + shift 2 + local -a -r files=("$@") + + # check is (optional) function defined + if [ "$(type -t run_hook_on_whole_repo)" == function ] && + # check is hook run via `pre-commit run --all` + common::is_hook_run_on_whole_repo "$hook_id" "${files[@]}"; then + run_hook_on_whole_repo "$args" + exit 0 + fi + + # consume modified files passed from pre-commit so that + # hook runs against only those relevant directories + local index=0 + for file_with_path in "${files[@]}"; do + file_with_path="${file_with_path// /__REPLACED__SPACE__}" + + dir_paths[index]=$(dirname "$file_with_path") + + ((index += 1)) + done + + # preserve errexit status + shopt -qo errexit && ERREXIT_IS_SET=true + # allow hook to continue if exit_code is greater than 0 + set +e + local final_exit_code=0 + + # run hook for each path + for dir_path in $(echo "${dir_paths[*]}" | tr ' ' '\n' | sort -u); do + dir_path="${dir_path//__REPLACED__SPACE__/ }" + pushd "$dir_path" > /dev/null || continue + + per_dir_hook_unique_part "$args" "$dir_path" + + local exit_code=$? + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code + fi + + popd > /dev/null + done + + # restore errexit if it was set before the "for" loop + [[ $ERREXIT_IS_SET ]] && set -e + # return the hook final exit_code + exit $final_exit_code +} + +####################################################################### +# Colorize provided string and print it out to stdout +# Environment variables: +# PRE_COMMIT_COLOR (string) If set to `never` - do not colorize output +# Arguments: +# COLOR (string) Color name that will be used to colorize +# TEXT (string) +# Outputs: +# Print out provided text to stdout +####################################################################### +function common::colorify { + # shellcheck disable=SC2034 + local -r red="\e[0m\e[31m" + # shellcheck disable=SC2034 + local -r green="\e[0m\e[32m" + # shellcheck disable=SC2034 + local -r yellow="\e[0m\e[33m" + # Color reset + local -r RESET="\e[0m" + + # Params start # + local COLOR="${!1}" + local -r TEXT=$2 + # Params end # + + if [ "$PRE_COMMIT_COLOR" = "never" ]; then + COLOR=$RESET + fi + + echo -e "${COLOR}${TEXT}${RESET}" +} diff --git a/hooks/infracost_breakdown.sh b/hooks/infracost_breakdown.sh new file mode 100755 index 000000000..86fa86499 --- /dev/null +++ b/hooks/infracost_breakdown.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +set -eo pipefail + +# globals variables +# hook ID, see `- id` for details in .pre-commit-hooks.yaml file +# shellcheck disable=SC2034 # Unused var. +readonly HOOK_ID='infracost_breakdown' +# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines +readonly SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +function main { + common::initialize "$SCRIPT_DIR" + common::parse_cmdline "$@" + # shellcheck disable=SC2153 # False positive + infracost_breakdown_ "${HOOK_CONFIG[*]}" "${ARGS[*]}" +} + +####################################################################### +# Wrapper around `infracost breakdown` tool which checks and compares +# infra cost based on provided hook_config +# Environment variables: +# PRE_COMMIT_COLOR (string) If set to `never` - do not colorize output +# Arguments: +# hook_config (string with array) arguments that configure hook behavior +# args (string with array) arguments that configure wrapped tool behavior +# Outputs: +# Print out hook checks status (Passed/Failed), total monthly cost and +# diff, summary about infracost check (non-supported resources etc.) +####################################################################### +function infracost_breakdown_ { + local -r hook_config="$1" + local args + read -r -a args <<< "$2" + + # Get hook settings + IFS=";" read -r -a checks <<< "$hook_config" + + if [ "$PRE_COMMIT_COLOR" = "never" ]; then + args+=("--no-color") + fi + + local RESULTS + RESULTS="$(infracost breakdown "${args[@]}" --format json)" + local API_VERSION + API_VERSION="$(jq -r .version <<< "$RESULTS")" + + if [ "$API_VERSION" != "0.2" ]; then + common::colorify "yellow" "WARNING: Hook supports Infracost API version \"0.2\", got \"$API_VERSION\"" + common::colorify "yellow" " Some things may not work as expected" + fi + + local dir + dir="$(jq '.projects[].metadata.vcsSubPath' <<< "$RESULTS")" + echo -e "\nRunning in $dir" + + local have_failed_checks=false + + for check in "${checks[@]}"; do + # $hook_config receives string like '1 > 2; 3 == 4;' etc. + # It gets split by `;` into array, which we're parsing here ('1 > 2' ' 3 == 4') + # Next line removes leading spaces, just for fancy output reason. + # shellcheck disable=SC2001 # Rule exception + check=$(echo "$check" | sed 's/^[[:space:]]*//') + + # Drop quotes in hook args section. From: + # -h ".totalHourlyCost > 0.1" + # --hook-config='.currency == "USD"' + # To: + # -h .totalHourlyCost > 0.1 + # --hook-config=.currency == "USD" + first_char=${check:0:1} + last_char=${check: -1} + if [ "$first_char" == "$last_char" ] && { + [ "$first_char" == '"' ] || [ "$first_char" == "'" ] + }; then + check="${check:1:-1}" + fi + + mapfile -t operations < <(echo "$check" | grep -oE '[!<>=]{1,2}') + # Get the very last operator, that is used in comparison inside `jq` query. + # From the example below we need to pick the `>` which is in between `add` and `1000`, + # but not the `!=`, which goes earlier in the `jq` expression + # [.projects[].diff.totalMonthlyCost | select (.!=null) | tonumber] | add > 1000 + operation=${operations[-1]} + + IFS="$operation" read -r -a jq_check <<< "$check" + real_value="$(jq "${jq_check[0]}" <<< "$RESULTS")" + compare_value="${jq_check[1]}${jq_check[2]}" + # Check types + jq_check_type="$(jq -r "${jq_check[0]} | type" <<< "$RESULTS")" + compare_value_type="$(jq -r "$compare_value | type" <<< "$RESULTS")" + # Fail if comparing different types + if [ "$jq_check_type" != "$compare_value_type" ]; then + common::colorify "yellow" "Warning: Comparing values with different types may give incorrect result" + common::colorify "yellow" " Expression: $check" + common::colorify "yellow" " Types in the expression: [$jq_check_type] $operation [$compare_value_type]" + common::colorify "yellow" " Use 'tonumber' filter when comparing costs (e.g. '.totalMonthlyCost|tonumber')" + have_failed_checks=true + continue + fi + # Fail if string is compared not with `==` or `!=` + if [ "$jq_check_type" == "string" ] && { + [ "$operation" != '==' ] && [ "$operation" != '!=' ] + }; then + common::colorify "yellow" "Warning: Wrong comparison operator is used in expression: $check" + common::colorify "yellow" " Use 'tonumber' filter when comparing costs (e.g. '.totalMonthlyCost|tonumber')" + common::colorify "yellow" " Use '==' or '!=' when comparing strings (e.g. '.currency == \"USD\"')." + have_failed_checks=true + continue + fi + + # Compare values + check_passed="$(echo "$RESULTS" | jq "$check")" + + status="Passed" + color="green" + if ! $check_passed; then + status="Failed" + color="red" + have_failed_checks=true + fi + + # Print check result + common::colorify $color "$status: $check\t\t$real_value $operation $compare_value" + done + + # Fancy informational output + currency="$(jq -r '.currency' <<< "$RESULTS")" + + echo -e "\nSummary: $(jq -r '.summary' <<< "$RESULTS")" + + echo -e "\nTotal Monthly Cost: $(jq -r .totalMonthlyCost <<< "$RESULTS") $currency" + echo "Total Monthly Cost (diff): $(jq -r .projects[].diff.totalMonthlyCost <<< "$RESULTS") $currency" + + if $have_failed_checks; then + exit 1 + fi +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/hooks/terraform_docs.sh b/hooks/terraform_docs.sh new file mode 100755 index 000000000..9bfc94003 --- /dev/null +++ b/hooks/terraform_docs.sh @@ -0,0 +1,375 @@ +#!/usr/bin/env bash +set -eo pipefail + +# globals variables +# hook ID, see `- id` for details in .pre-commit-hooks.yaml file +# shellcheck disable=SC2034 # Unused var. +readonly HOOK_ID='terraform_docs' +# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines +readonly SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +function main { + common::initialize "$SCRIPT_DIR" + common::parse_cmdline "$@" + # Support for setting relative PATH to .terraform-docs.yml config. + # shellcheck disable=SC2178 # It's the simplest syntax for that case + ARGS=${ARGS[*]/--config=/--config=$(pwd)\/} + # shellcheck disable=SC2128 # It's the simplest syntax for that case + # shellcheck disable=SC2153 # False positive + terraform_docs_ "${HOOK_CONFIG[*]}" "$ARGS" "${FILES[@]}" +} + +####################################################################### +# Function which prepares hacks for old versions of `terraform` and +# `terraform-docs` that them call `terraform_docs` +# Arguments: +# hook_config (string with array) arguments that configure hook behavior +# args (string with array) arguments that configure wrapped tool behavior +# files (array) filenames to check +####################################################################### +function terraform_docs_ { + local -r hook_config="$1" + local -r args="$2" + shift 2 + local -a -r files=("$@") + + # Get hook settings + IFS=";" read -r -a configs <<< "$hook_config" + + local hack_terraform_docs + hack_terraform_docs=$(terraform version | sed -n 1p | grep -c 0.12) || true + + if [[ ! $(command -v terraform-docs) ]]; then + echo "ERROR: terraform-docs is required by terraform_docs pre-commit hook but is not installed or in the system's PATH." + exit 1 + fi + + local is_old_terraform_docs + is_old_terraform_docs=$(terraform-docs version | grep -o "v0.[1-7]\." | tail -1) || true + + if [[ -z "$is_old_terraform_docs" ]]; then # Using terraform-docs 0.8+ (preferred) + + terraform_docs "0" "${configs[*]}" "$args" "${files[@]}" + + elif [[ "$hack_terraform_docs" == "1" ]]; then # Using awk script because terraform-docs is older than 0.8 and terraform 0.12 is used + + if [[ ! $(command -v awk) ]]; then + echo "ERROR: awk is required for terraform-docs hack to work with Terraform 0.12." + exit 1 + fi + + local tmp_file_awk + tmp_file_awk=$(mktemp "${TMPDIR:-/tmp}/terraform-docs-XXXXXXXXXX") + terraform_docs_awk "$tmp_file_awk" + terraform_docs "$tmp_file_awk" "${configs[*]}" "$args" "${files[@]}" + rm -f "$tmp_file_awk" + + else # Using terraform 0.11 and no awk script is needed for that + + terraform_docs "0" "${configs[*]}" "$args" "${files[@]}" + + fi +} + +####################################################################### +# Wrapper around `terraform-docs` tool that check and change/create +# (depends on provided hook_config) terraform documentation in +# markdown format +# Arguments: +# terraform_docs_awk_file (string) filename where awk hack for old +# `terraform-docs` was written. Needed for TF 0.12+. +# Hack skipped when `terraform_docs_awk_file == "0"` +# hook_config (string with array) arguments that configure hook behavior +# args (string with array) arguments that configure wrapped tool behavior +# files (array) filenames to check +####################################################################### +function terraform_docs { + local -r terraform_docs_awk_file="$1" + local -r hook_config="$2" + local -r args="$3" + shift 3 + local -a -r files=("$@") + + declare -a paths + + local index=0 + local file_with_path + for file_with_path in "${files[@]}"; do + file_with_path="${file_with_path// /__REPLACED__SPACE__}" + + paths[index]=$(dirname "$file_with_path") + + ((index += 1)) + done + + local -r tmp_file=$(mktemp) + + # + # Get hook settings + # + local text_file="README.md" + local add_to_existing=false + local create_if_not_exist=false + + read -r -a configs <<< "$hook_config" + + for c in "${configs[@]}"; do + + IFS="=" read -r -a config <<< "$c" + key=${config[0]} + value=${config[1]} + + case $key in + --path-to-file) + text_file=$value + ;; + --add-to-existing-file) + add_to_existing=$value + ;; + --create-file-if-not-exist) + create_if_not_exist=$value + ;; + esac + done + + local dir_path + for dir_path in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do + dir_path="${dir_path//__REPLACED__SPACE__/ }" + + pushd "$dir_path" > /dev/null || continue + + # + # Create file if it not exist and `--create-if-not-exist=true` provided + # + if $create_if_not_exist && [[ ! -f "$text_file" ]]; then + dir_have_tf_files="$( + find . -maxdepth 1 -type f | sed 's|.*\.||' | sort -u | grep -oE '^tf$|^tfvars$' || + exit 0 + )" + + # if no TF files - skip dir + [ ! "$dir_have_tf_files" ] && popd > /dev/null && continue + + dir="$(dirname "$text_file")" + + mkdir -p "$dir" + { + echo -e "# ${PWD##*/}\n" + echo "" + echo "" + } >> "$text_file" + fi + + # If file still not exist - skip dir + [[ ! -f "$text_file" ]] && popd > /dev/null && continue + + # + # If `--add-to-existing-file=true` set, check is in file exist "hook markers", + # and if not - append "hook markers" to the end of file. + # + if $add_to_existing; then + HAVE_MARKER=$(grep -o '' "$text_file" || exit 0) + + if [ ! "$HAVE_MARKER" ]; then + echo "" >> "$text_file" + echo "" >> "$text_file" + fi + fi + + if [[ "$terraform_docs_awk_file" == "0" ]]; then + # shellcheck disable=SC2086 + terraform-docs md $args ./ > "$tmp_file" + else + # Can't append extension for mktemp, so renaming instead + local tmp_file_docs + tmp_file_docs=$(mktemp "${TMPDIR:-/tmp}/terraform-docs-XXXXXXXXXX") + mv "$tmp_file_docs" "$tmp_file_docs.tf" + local tmp_file_docs_tf + tmp_file_docs_tf="$tmp_file_docs.tf" + + awk -f "$terraform_docs_awk_file" ./*.tf > "$tmp_file_docs_tf" + # shellcheck disable=SC2086 + terraform-docs md $args "$tmp_file_docs_tf" > "$tmp_file" + rm -f "$tmp_file_docs_tf" + fi + + # Replace content between markers with the placeholder - https://stackoverflow.com/questions/1212799/how-do-i-extract-lines-between-two-line-delimiters-in-perl#1212834 + perl -i -ne 'if (/BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK/../END OF PRE-COMMIT-TERRAFORM DOCS HOOK/) { print $_ if /BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK/; print "I_WANT_TO_BE_REPLACED\n$_" if /END OF PRE-COMMIT-TERRAFORM DOCS HOOK/;} else { print $_ }' "$text_file" + + # Replace placeholder with the content of the file + perl -i -e 'open(F, "'"$tmp_file"'"); $f = join "", ; while(<>){if (/I_WANT_TO_BE_REPLACED/) {print $f} else {print $_};}' "$text_file" + + rm -f "$tmp_file" + + popd > /dev/null + done +} + +####################################################################### +# Function which creates file with `awk` hacks for old versions of +# `terraform-docs` +# Arguments: +# output_file (string) filename where hack will be written to +####################################################################### +function terraform_docs_awk { + local -r output_file=$1 + + cat << "EOF" > "$output_file" +# This script converts Terraform 0.12 variables/outputs to something suitable for `terraform-docs` +# As of terraform-docs v0.6.0, HCL2 is not supported. This script is a *dirty hack* to get around it. +# https://github.com/terraform-docs/terraform-docs/ +# https://github.com/terraform-docs/terraform-docs/issues/62 +# Script was originally found here: https://github.com/cloudposse/build-harness/blob/master/bin/terraform-docs.awk +{ + if ( $0 ~ /\{/ ) { + braceCnt++ + } + if ( $0 ~ /\}/ ) { + braceCnt-- + } + # ---------------------------------------------------------------------------------------------- + # variable|output "..." { + # ---------------------------------------------------------------------------------------------- + # [END] variable/output block + if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) { + if (braceCnt == 0 && blockCnt > 0) { + blockCnt-- + print $0 + } + } + # [START] variable or output block started + if ($0 ~ /^[[:space:]]*(variable|output)[[:space:]][[:space:]]*"(.*?)"/) { + # Normalize the braceCnt and block (should be 1 now) + braceCnt = 1 + blockCnt = 1 + # [CLOSE] "default" and "type" block + blockDefaultCnt = 0 + blockTypeCnt = 0 + # Print variable|output line + print $0 + } + # ---------------------------------------------------------------------------------------------- + # default = ... + # ---------------------------------------------------------------------------------------------- + # [END] multiline "default" continues/ends + if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt > 0) { + print $0 + # Count opening blocks + blockDefaultCnt += gsub(/\(/, "") + blockDefaultCnt += gsub(/\[/, "") + blockDefaultCnt += gsub(/\{/, "") + # Count closing blocks + blockDefaultCnt -= gsub(/\)/, "") + blockDefaultCnt -= gsub(/\]/, "") + blockDefaultCnt -= gsub(/\}/, "") + } + # [START] multiline "default" statement started + if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) { + if ($0 ~ /^[[:space:]][[:space:]]*(default)[[:space:]][[:space:]]*=/) { + if ($3 ~ "null") { + print " default = \"null\"" + } else { + print $0 + # Count opening blocks + blockDefaultCnt += gsub(/\(/, "") + blockDefaultCnt += gsub(/\[/, "") + blockDefaultCnt += gsub(/\{/, "") + # Count closing blocks + blockDefaultCnt -= gsub(/\)/, "") + blockDefaultCnt -= gsub(/\]/, "") + blockDefaultCnt -= gsub(/\}/, "") + } + } + } + # ---------------------------------------------------------------------------------------------- + # type = ... + # ---------------------------------------------------------------------------------------------- + # [END] multiline "type" continues/ends + if (blockCnt > 0 && blockTypeCnt > 0 && blockDefaultCnt == 0) { + # The following 'print $0' would print multiline type definitions + #print $0 + # Count opening blocks + blockTypeCnt += gsub(/\(/, "") + blockTypeCnt += gsub(/\[/, "") + blockTypeCnt += gsub(/\{/, "") + # Count closing blocks + blockTypeCnt -= gsub(/\)/, "") + blockTypeCnt -= gsub(/\]/, "") + blockTypeCnt -= gsub(/\}/, "") + } + # [START] multiline "type" statement started + if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) { + if ($0 ~ /^[[:space:]][[:space:]]*(type)[[:space:]][[:space:]]*=/ ) { + if ($3 ~ "object") { + print " type = \"object\"" + } else { + # Convert multiline stuff into single line + if ($3 ~ /^[[:space:]]*list[[:space:]]*\([[:space:]]*$/) { + type = "list" + } else if ($3 ~ /^[[:space:]]*string[[:space:]]*\([[:space:]]*$/) { + type = "string" + } else if ($3 ~ /^[[:space:]]*map[[:space:]]*\([[:space:]]*$/) { + type = "map" + } else { + type = $3 + } + # legacy quoted types: "string", "list", and "map" + if (type ~ /^[[:space:]]*"(.*?)"[[:space:]]*$/) { + print " type = " type + } else { + print " type = \"" type "\"" + } + } + # Count opening blocks + blockTypeCnt += gsub(/\(/, "") + blockTypeCnt += gsub(/\[/, "") + blockTypeCnt += gsub(/\{/, "") + # Count closing blocks + blockTypeCnt -= gsub(/\)/, "") + blockTypeCnt -= gsub(/\]/, "") + blockTypeCnt -= gsub(/\}/, "") + } + } + # ---------------------------------------------------------------------------------------------- + # description = ... + # ---------------------------------------------------------------------------------------------- + # [PRINT] single line "description" + if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) { + if ($0 ~ /^[[:space:]][[:space:]]*description[[:space:]][[:space:]]*=/) { + print $0 + } + } + # ---------------------------------------------------------------------------------------------- + # value = ... + # ---------------------------------------------------------------------------------------------- + ## [PRINT] single line "value" + #if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) { + # if ($0 ~ /^[[:space:]][[:space:]]*value[[:space:]][[:space:]]*=/) { + # print $0 + # } + #} + # ---------------------------------------------------------------------------------------------- + # Newlines, comments, everything else + # ---------------------------------------------------------------------------------------------- + #if (blockTypeCnt == 0 && blockDefaultCnt == 0) { + # Comments with '#' + if ($0 ~ /^[[:space:]]*#/) { + print $0 + } + # Comments with '//' + if ($0 ~ /^[[:space:]]*\/\//) { + print $0 + } + # Newlines + if ($0 ~ /^[[:space:]]*$/) { + print $0 + } + #} +} +EOF + +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/hooks/terraform_docs_replace.py b/hooks/terraform_docs_replace.py new file mode 100644 index 000000000..a9cf6c9bc --- /dev/null +++ b/hooks/terraform_docs_replace.py @@ -0,0 +1,56 @@ +import argparse +import os +import subprocess +import sys + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="""Run terraform-docs on a set of files. Follows the standard convention of + pulling the documentation from main.tf in order to replace the entire + README.md file each time.""" + ) + parser.add_argument( + '--dest', dest='dest', default='README.md', + ) + parser.add_argument( + '--sort-inputs-by-required', dest='sort', action='store_true', + help='[deprecated] use --sort-by-required instead', + ) + parser.add_argument( + '--sort-by-required', dest='sort', action='store_true', + ) + parser.add_argument( + '--with-aggregate-type-defaults', dest='aggregate', action='store_true', + help='[deprecated]', + ) + parser.add_argument('filenames', nargs='*', help='Filenames to check.') + args = parser.parse_args(argv) + + dirs = [] + for filename in args.filenames: + if (os.path.realpath(filename) not in dirs and + (filename.endswith(".tf") or filename.endswith(".tfvars"))): + dirs.append(os.path.dirname(filename)) + + retval = 0 + + for dir in dirs: + try: + procArgs = [] + procArgs.append('terraform-docs') + if args.sort: + procArgs.append('--sort-by-required') + procArgs.append('md') + procArgs.append("./{dir}".format(dir=dir)) + procArgs.append('>') + procArgs.append("./{dir}/{dest}".format(dir=dir, dest=args.dest)) + subprocess.check_call(" ".join(procArgs), shell=True) + except subprocess.CalledProcessError as e: + print(e) + retval = 1 + return retval + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/hooks/terraform_fmt.sh b/hooks/terraform_fmt.sh new file mode 100755 index 000000000..62e70810e --- /dev/null +++ b/hooks/terraform_fmt.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -eo pipefail + +# globals variables +# hook ID, see `- id` for details in .pre-commit-hooks.yaml file +# shellcheck disable=SC2034 # Unused var. +readonly HOOK_ID='terraform_fmt' +# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines +readonly SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +function main { + common::initialize "$SCRIPT_DIR" + common::parse_cmdline "$@" + # shellcheck disable=SC2153 # False positive + terraform_fmt_ "${ARGS[*]}" "${FILES[@]}" +} + +####################################################################### +# Hook execution boilerplate logic which is common to hooks, that run +# on per dir basis. Little bit extended than `common::per_dir_hook` +# 1. Because hook runs on whole dir, reduce file paths to uniq dir paths +# (unique) 1.1. Collect paths to *.tfvars files in a separate variable +# 2. Run for each dir `per_dir_hook_unique_part`, on all paths +# (unique) 2.1. Run `terraform fmt` on each *.tfvars file +# 2.2. If at least 1 check failed - change exit code to non-zero +# 3. Complete hook execution and return exit code +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +# files (array) filenames to check +####################################################################### +function terraform_fmt_ { + local -r args="$1" + shift 1 + local -a -r files=("$@") + # consume modified files passed from pre-commit so that + # hook runs against only those relevant directories + local index=0 + for file_with_path in "${files[@]}"; do + file_with_path="${file_with_path// /__REPLACED__SPACE__}" + + dir_paths[index]=$(dirname "$file_with_path") + # TODO Unique part + if [[ "$file_with_path" == *".tfvars" ]]; then + tfvars_files+=("$file_with_path") + fi + #? End for unique part + ((index += 1)) + done + + # preserve errexit status + shopt -qo errexit && ERREXIT_IS_SET=true + # allow hook to continue if exit_code is greater than 0 + set +e + local final_exit_code=0 + + # run hook for each path + for dir_path in $(echo "${dir_paths[*]}" | tr ' ' '\n' | sort -u); do + dir_path="${dir_path//__REPLACED__SPACE__/ }" + pushd "$dir_path" > /dev/null || continue + + per_dir_hook_unique_part "$args" "$dir_path" + + local exit_code=$? + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code + fi + + popd > /dev/null + done + + # TODO: Unique part + # terraform.tfvars are excluded by `terraform fmt` + for tfvars_file in "${tfvars_files[@]}"; do + tfvars_file="${tfvars_file//__REPLACED__SPACE__/ }" + + terraform fmt "${ARGS[@]}" "$tfvars_file" + local exit_code=$? + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code + fi + done + #? End for unique part + # restore errexit if it was set before the "for" loop + [[ $ERREXIT_IS_SET ]] && set -e + # return the hook final exit_code + exit $final_exit_code + +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed in loop +# on each provided dir path. Run wrapped tool with specified arguments +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +# dir_path (string) PATH to dir relative to git repo root. +# Can be used in error logging +# Outputs: +# If failed - print out hook checks status +####################################################################### +function per_dir_hook_unique_part { + local -r args="$1" + local -r dir_path="$2" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terraform fmt ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/hooks/terraform_providers_lock.sh b/hooks/terraform_providers_lock.sh new file mode 100755 index 000000000..011ee45eb --- /dev/null +++ b/hooks/terraform_providers_lock.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -eo pipefail + +# globals variables +# hook ID, see `- id` for details in .pre-commit-hooks.yaml file +readonly HOOK_ID='terraform_providers_lock' +# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines +readonly SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +function main { + common::initialize "$SCRIPT_DIR" + common::parse_cmdline "$@" + # shellcheck disable=SC2153 # False positive + common::per_dir_hook "${ARGS[*]}" "$HOOK_ID" "${FILES[@]}" +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed in loop +# on each provided dir path. Run wrapped tool with specified arguments +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +# dir_path (string) PATH to dir relative to git repo root. +# Can be used in error logging +# Outputs: +# If failed - print out hook checks status +####################################################################### +function per_dir_hook_unique_part { + local -r args="$1" + local -r dir_path="$2" + + if [ ! -d ".terraform" ]; then + init_output=$(terraform init -backend=false 2>&1) + init_code=$? + + if [ $init_code -ne 0 ]; then + common::colorify "red" "Init before validation failed: $dir_path" + common::colorify "red" "$init_output" + exit $init_code + fi + fi + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terraform providers lock ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/hooks/terraform_tflint.sh b/hooks/terraform_tflint.sh new file mode 100755 index 000000000..f89ab58bf --- /dev/null +++ b/hooks/terraform_tflint.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -eo pipefail + +# globals variables +# hook ID, see `- id` for details in .pre-commit-hooks.yaml file +readonly HOOK_ID='terraform_tflint' +# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines +readonly SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +function main { + common::initialize "$SCRIPT_DIR" + common::parse_cmdline "$@" + # Support for setting PATH to repo root. + # shellcheck disable=SC2178 # It's the simplest syntax for that case + ARGS=${ARGS[*]/__GIT_WORKING_DIR__/$(pwd)\/} + # shellcheck disable=SC2128 # It's the simplest syntax for that case + common::per_dir_hook "$ARGS" "$HOOK_ID" "${FILES[@]}" +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed in loop +# on each provided dir path. Run wrapped tool with specified arguments +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +# dir_path (string) PATH to dir relative to git repo root. +# Can be used in error logging +# Outputs: +# If failed - print out hook checks status +####################################################################### +function per_dir_hook_unique_part { + local -r args="$1" + local -r dir_path="$2" + + # Print checked PATH **only** if TFLint have any messages + # shellcheck disable=SC2091,SC2068 # Suppress error output + $(tflint ${args[@]} 2>&1) 2> /dev/null || { + common::colorify "yellow" "TFLint in $dir_path/:" + + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + tflint ${args[@]} + } + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/hooks/terraform_tfsec.sh b/hooks/terraform_tfsec.sh new file mode 100755 index 000000000..b9273827a --- /dev/null +++ b/hooks/terraform_tfsec.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -eo pipefail + +# globals variables +# hook ID, see `- id` for details in .pre-commit-hooks.yaml file +readonly HOOK_ID='terraform_tfsec' +# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines +readonly SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +function main { + common::initialize "$SCRIPT_DIR" + common::parse_cmdline "$@" + # Support for setting PATH to repo root. + # shellcheck disable=SC2178 # It's the simplest syntax for that case + ARGS=${ARGS[*]/__GIT_WORKING_DIR__/$(pwd)\/} + # shellcheck disable=SC2128 # It's the simplest syntax for that case + common::per_dir_hook "$ARGS" "$HOOK_ID" "${FILES[@]}" +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed in loop +# on each provided dir path. Run wrapped tool with specified arguments +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +# dir_path (string) PATH to dir relative to git repo root. +# Can be used in error logging +# Outputs: +# If failed - print out hook checks status +####################################################################### +function per_dir_hook_unique_part { + local -r args="$1" + # shellcheck disable=SC2034 # Unused var. + local -r dir_path="$2" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + tfsec ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed one time +# in the root git repo +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +####################################################################### +function run_hook_on_whole_repo { + local -r args="$1" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + tfsec "$(pwd)" ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/hooks/terraform_validate.sh b/hooks/terraform_validate.sh new file mode 100755 index 000000000..13db4b060 --- /dev/null +++ b/hooks/terraform_validate.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +set -eo pipefail + +# globals variables +# hook ID, see `- id` for details in .pre-commit-hooks.yaml file +# shellcheck disable=SC2034 # Unused var. +readonly HOOK_ID='terraform_validate' +# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines +readonly SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +# `terraform validate` requires this env variable to be set +export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-us-east-1} + +function main { + common::initialize "$SCRIPT_DIR" + parse_cmdline_ "$@" + terraform_validate_ +} + +####################################################################### +# Parse args and filenames passed to script and populate respective +# global variables with appropriate values +# Globals (init and populate): +# ARGS (array) arguments that configure wrapped tool behavior +# INIT_ARGS (array) arguments to `terraform init` command +# ENVS (array) environment variables that will be used with +# `terraform` commands +# FILES (array) filenames to check +# Arguments: +# $@ (array) all specified in `hooks.[].args` in +# `.pre-commit-config.yaml` and filenames. +####################################################################### +function parse_cmdline_ { + declare argv + argv=$(getopt -o e:i:a: --long envs:,init-args:,args: -- "$@") || return + eval "set -- $argv" + + for argv; do + case $argv in + -a | --args) + shift + ARGS+=("$1") + shift + ;; + -i | --init-args) + shift + INIT_ARGS+=("$1") + shift + ;; + -e | --envs) + shift + ENVS+=("$1") + shift + ;; + --) + shift + FILES=("$@") + break + ;; + esac + done +} + +####################################################################### +# Wrapper around `terraform validate` tool that checks if code is valid +# 1. Export provided env var K/V pairs to environment +# 2. Because hook runs on whole dir, reduce file paths to uniq dir paths +# 3. In each dir that have *.tf files: +# 3.1. Check if `.terraform` dir exists and if not - run `terraform init` +# 3.2. Run `terraform validate` +# 3.3. If at least 1 check failed - change exit code to non-zero +# 4. Complete hook execution and return exit code +# Globals: +# ARGS (array) arguments that configure wrapped tool behavior +# INIT_ARGS (array) arguments for `terraform init` command` +# ENVS (array) environment variables that will be used with +# `terraform` commands +# FILES (array) filenames to check +# Outputs: +# If failed - print out hook checks status +####################################################################### +function terraform_validate_ { + + # Setup environment variables + local var var_name var_value + for var in "${ENVS[@]}"; do + var_name="${var%%=*}" + var_value="${var#*=}" + # shellcheck disable=SC2086 + export $var_name="$var_value" + done + + declare -a paths + local index=0 + local error=0 + + local file_with_path + for file_with_path in "${FILES[@]}"; do + file_with_path="${file_with_path// /__REPLACED__SPACE__}" + + paths[index]=$(dirname "$file_with_path") + ((index += 1)) + done + + local dir_path + for dir_path in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do + dir_path="${dir_path//__REPLACED__SPACE__/ }" + + if [[ -n "$(find "$dir_path" -maxdepth 1 -name '*.tf' -print -quit)" ]]; then + + pushd "$(realpath "$dir_path")" > /dev/null + + if [ ! -d .terraform ]; then + set +e + init_output=$(terraform init -backend=false "${INIT_ARGS[@]}" 2>&1) + init_code=$? + set -e + + if [ $init_code -ne 0 ]; then + error=1 + echo "Init before validation failed: $dir_path" + echo "$init_output" + popd > /dev/null + continue + fi + fi + + set +e + validate_output=$(terraform validate "${ARGS[@]}" 2>&1) + validate_code=$? + set -e + + if [ $validate_code -ne 0 ]; then + error=1 + echo "Validation failed: $dir_path" + echo "$validate_output" + echo + fi + + popd > /dev/null + fi + done + + if [ $error -ne 0 ]; then + exit 1 + fi +} + +# global arrays +declare -a INIT_ARGS +declare -a ENVS + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/hooks/terragrunt_fmt.sh b/hooks/terragrunt_fmt.sh new file mode 100755 index 000000000..b2a5b0684 --- /dev/null +++ b/hooks/terragrunt_fmt.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -eo pipefail + +# globals variables +# hook ID, see `- id` for details in .pre-commit-hooks.yaml file +readonly HOOK_ID='terragrunt_fmt' +# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines +readonly SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +function main { + common::initialize "$SCRIPT_DIR" + common::parse_cmdline "$@" + # shellcheck disable=SC2153 # False positive + common::per_dir_hook "${ARGS[*]}" "$HOOK_ID" "${FILES[@]}" +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed in loop +# on each provided dir path. Run wrapped tool with specified arguments +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +# dir_path (string) PATH to dir relative to git repo root. +# Can be used in error logging +# Outputs: +# If failed - print out hook checks status +####################################################################### +function per_dir_hook_unique_part { + local -r args="$1" + # shellcheck disable=SC2034 # Unused var. + local -r dir_path="$2" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terragrunt hclfmt ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed one time +# in the root git repo +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +####################################################################### +function run_hook_on_whole_repo { + local -r args="$1" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terragrunt hclfmt "$(pwd)" ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/hooks/terragrunt_validate.sh b/hooks/terragrunt_validate.sh new file mode 100755 index 000000000..589b823c8 --- /dev/null +++ b/hooks/terragrunt_validate.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -eo pipefail + +# globals variables +# hook ID, see `- id` for details in .pre-commit-hooks.yaml file +readonly HOOK_ID='terragrunt_validate' +# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines +readonly SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +function main { + common::initialize "$SCRIPT_DIR" + common::parse_cmdline "$@" + # shellcheck disable=SC2153 # False positive + common::per_dir_hook "${ARGS[*]}" "$HOOK_ID" "${FILES[@]}" +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed in loop +# on each provided dir path. Run wrapped tool with specified arguments +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +# dir_path (string) PATH to dir relative to git repo root. +# Can be used in error logging +# Outputs: +# If failed - print out hook checks status +####################################################################### +function per_dir_hook_unique_part { + local -r args="$1" + # shellcheck disable=SC2034 # Unused var. + local -r dir_path="$2" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terragrunt validate ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed one time +# in the root git repo +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +####################################################################### +function run_hook_on_whole_repo { + local -r args="$1" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terragrunt run-all validate ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/hooks/terrascan.sh b/hooks/terrascan.sh new file mode 100755 index 000000000..5c6415456 --- /dev/null +++ b/hooks/terrascan.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -eo pipefail + +# globals variables +# hook ID, see `- id` for details in .pre-commit-hooks.yaml file +readonly HOOK_ID='terrascan' +# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines +readonly SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +function main { + common::initialize "$SCRIPT_DIR" + common::parse_cmdline "$@" + # shellcheck disable=SC2153 # False positive + common::per_dir_hook "${ARGS[*]}" "$HOOK_ID" "${FILES[@]}" +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed in loop +# on each provided dir path. Run wrapped tool with specified arguments +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +# dir_path (string) PATH to dir relative to git repo root. +# Can be used in error logging +# Outputs: +# If failed - print out hook checks status +####################################################################### +function per_dir_hook_unique_part { + local -r args="$1" + # shellcheck disable=SC2034 # Unused var. + local -r dir_path="$2" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terrascan scan -i terraform ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed one time +# in the root git repo +# Arguments: +# args (string with array) arguments that configure wrapped tool behavior +####################################################################### +function run_hook_on_whole_repo { + local -r args="$1" + + # pass the arguments to hook + # shellcheck disable=SC2068 # hook fails when quoting is used ("$arg[@]") + terrascan scan -i terraform ${args[@]} + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/lib_getopt b/lib_getopt new file mode 100644 index 000000000..c4b21fa80 --- /dev/null +++ b/lib_getopt @@ -0,0 +1,494 @@ +#!/bin/bash + +getopt() { + # pure-getopt, a drop-in replacement for GNU getopt in pure Bash. + # version 1.4.4 + # + # Copyright 2012-2020 Aron Griffis + # + # Permission is hereby granted, free of charge, to any person obtaining + # a copy of this software and associated documentation files (the + # "Software"), to deal in the Software without restriction, including + # without limitation the rights to use, copy, modify, merge, publish, + # distribute, sublicense, and/or sell copies of the Software, and to + # permit persons to whom the Software is furnished to do so, subject to + # the following conditions: + # + # The above copyright notice and this permission notice shall be included + # in all copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + _getopt_main() { + # Returns one of the following statuses: + # 0 success + # 1 error parsing parameters + # 2 error in getopt invocation + # 3 internal error + # 4 reserved for -T + # + # For statuses 0 and 1, generates normalized and shell-quoted + # "options -- parameters" on stdout. + + declare parsed status + declare short long='' name flags='' + declare have_short=false + + # Synopsis from getopt man-page: + # + # getopt optstring parameters + # getopt [options] [--] optstring parameters + # getopt [options] -o|--options optstring [options] [--] parameters + # + # The first form can be normalized to the third form which + # _getopt_parse() understands. The second form can be recognized after + # first parse when $short hasn't been set. + + if [[ -n ${GETOPT_COMPATIBLE+isset} || $1 == [^-]* ]]; then + # Enable compatibility mode + flags=c$flags + # Normalize first to third synopsis form + set -- -o "$1" -- "${@:2}" + fi + + # First parse always uses flags=p since getopt always parses its own + # arguments effectively in this mode. + parsed=$(_getopt_parse getopt ahl:n:o:qQs:TuV \ + alternative,help,longoptions:,name:,options:,quiet,quiet-output,shell:,test,version \ + p "$@") + status=$? + if [[ $status != 0 ]]; then + if [[ $status == 1 ]]; then + echo "Try \`getopt --help' for more information." >&2 + # Since this is the first parse, convert status 1 to 2 + status=2 + fi + return $status + fi + eval "set -- $parsed" + + while [[ $# -gt 0 ]]; do + case $1 in + (-a|--alternative) + flags=a$flags ;; + + (-h|--help) + _getopt_help + return 2 # as does GNU getopt + ;; + + (-l|--longoptions) + long="$long${long:+,}$2" + shift ;; + + (-n|--name) + name=$2 + shift ;; + + (-o|--options) + short=$2 + have_short=true + shift ;; + + (-q|--quiet) + flags=q$flags ;; + + (-Q|--quiet-output) + flags=Q$flags ;; + + (-s|--shell) + case $2 in + (sh|bash) + flags=${flags//t/} ;; + (csh|tcsh) + flags=t$flags ;; + (*) + echo 'getopt: unknown shell after -s or --shell argument' >&2 + echo "Try \`getopt --help' for more information." >&2 + return 2 ;; + esac + shift ;; + + (-u|--unquoted) + flags=u$flags ;; + + (-T|--test) + return 4 ;; + + (-V|--version) + echo "pure-getopt 1.4.4" + return 0 ;; + + (--) + shift + break ;; + esac + + shift + done + + if ! $have_short; then + # $short was declared but never set, not even to an empty string. + # This implies the second form in the synopsis. + if [[ $# == 0 ]]; then + echo 'getopt: missing optstring argument' >&2 + echo "Try \`getopt --help' for more information." >&2 + return 2 + fi + short=$1 + have_short=true + shift + fi + + if [[ $short == -* ]]; then + # Leading dash means generate output in place rather than reordering, + # unless we're already in compatibility mode. + [[ $flags == *c* ]] || flags=i$flags + short=${short#?} + elif [[ $short == +* ]]; then + # Leading plus means POSIXLY_CORRECT, unless we're already in + # compatibility mode. + [[ $flags == *c* ]] || flags=p$flags + short=${short#?} + fi + + # This should fire if POSIXLY_CORRECT is in the environment, even if + # it's an empty string. That's the difference between :+ and + + flags=${POSIXLY_CORRECT+p}$flags + + _getopt_parse "${name:-getopt}" "$short" "$long" "$flags" "$@" + } + + _getopt_parse() { + # Inner getopt parser, used for both first parse and second parse. + # Returns 0 for success, 1 for error parsing, 3 for internal error. + # In the case of status 1, still generates stdout with whatever could + # be parsed. + # + # $flags is a string of characters with the following meanings: + # a - alternative parsing mode + # c - GETOPT_COMPATIBLE + # i - generate output in place rather than reordering + # p - POSIXLY_CORRECT + # q - disable error reporting + # Q - disable normal output + # t - quote for csh/tcsh + # u - unquoted output + + declare name="$1" short="$2" long="$3" flags="$4" + shift 4 + + # Split $long on commas, prepend double-dashes, strip colons; + # for use with _getopt_resolve_abbrev + declare -a longarr + _getopt_split longarr "$long" + longarr=( "${longarr[@]/#/--}" ) + longarr=( "${longarr[@]%:}" ) + longarr=( "${longarr[@]%:}" ) + + # Parse and collect options and parameters + declare -a opts params + declare o alt_recycled=false error=0 + + while [[ $# -gt 0 ]]; do + case $1 in + (--) + params=( "${params[@]}" "${@:2}" ) + break ;; + + (--*=*) + o=${1%%=*} + if ! o=$(_getopt_resolve_abbrev "$o" "${longarr[@]}"); then + error=1 + elif [[ ,"$long", == *,"${o#--}"::,* ]]; then + opts=( "${opts[@]}" "$o" "${1#*=}" ) + elif [[ ,"$long", == *,"${o#--}":,* ]]; then + opts=( "${opts[@]}" "$o" "${1#*=}" ) + elif [[ ,"$long", == *,"${o#--}",* ]]; then + if $alt_recycled; then o=${o#-}; fi + _getopt_err "$name: option '$o' doesn't allow an argument" + error=1 + else + echo "getopt: assertion failed (1)" >&2 + return 3 + fi + alt_recycled=false + ;; + + (--?*) + o=$1 + if ! o=$(_getopt_resolve_abbrev "$o" "${longarr[@]}"); then + error=1 + elif [[ ,"$long", == *,"${o#--}",* ]]; then + opts=( "${opts[@]}" "$o" ) + elif [[ ,"$long", == *,"${o#--}::",* ]]; then + opts=( "${opts[@]}" "$o" '' ) + elif [[ ,"$long", == *,"${o#--}:",* ]]; then + if [[ $# -ge 2 ]]; then + shift + opts=( "${opts[@]}" "$o" "$1" ) + else + if $alt_recycled; then o=${o#-}; fi + _getopt_err "$name: option '$o' requires an argument" + error=1 + fi + else + echo "getopt: assertion failed (2)" >&2 + return 3 + fi + alt_recycled=false + ;; + + (-*) + if [[ $flags == *a* ]]; then + # Alternative parsing mode! + # Try to handle as a long option if any of the following apply: + # 1. There's an equals sign in the mix -x=3 or -xy=3 + # 2. There's 2+ letters and an abbreviated long match -xy + # 3. There's a single letter and an exact long match + # 4. There's a single letter and no short match + o=${1::2} # temp for testing #4 + if [[ $1 == *=* || $1 == -?? || \ + ,$long, == *,"${1#-}"[:,]* || \ + ,$short, != *,"${o#-}"[:,]* ]]; then + o=$(_getopt_resolve_abbrev "${1%%=*}" "${longarr[@]}" 2>/dev/null) + case $? in + (0) + # Unambiguous match. Let the long options parser handle + # it, with a flag to get the right error message. + set -- "-$1" "${@:2}" + alt_recycled=true + continue ;; + (1) + # Ambiguous match, generate error and continue. + _getopt_resolve_abbrev "${1%%=*}" "${longarr[@]}" >/dev/null + error=1 + shift + continue ;; + (2) + # No match, fall through to single-character check. + true ;; + (*) + echo "getopt: assertion failed (3)" >&2 + return 3 ;; + esac + fi + fi + + o=${1::2} + if [[ "$short" == *"${o#-}"::* ]]; then + if [[ ${#1} -gt 2 ]]; then + opts=( "${opts[@]}" "$o" "${1:2}" ) + else + opts=( "${opts[@]}" "$o" '' ) + fi + elif [[ "$short" == *"${o#-}":* ]]; then + if [[ ${#1} -gt 2 ]]; then + opts=( "${opts[@]}" "$o" "${1:2}" ) + elif [[ $# -ge 2 ]]; then + shift + opts=( "${opts[@]}" "$o" "$1" ) + else + _getopt_err "$name: option requires an argument -- '${o#-}'" + error=1 + fi + elif [[ "$short" == *"${o#-}"* ]]; then + opts=( "${opts[@]}" "$o" ) + if [[ ${#1} -gt 2 ]]; then + set -- "$o" "-${1:2}" "${@:2}" + fi + else + if [[ $flags == *a* ]]; then + # Alternative parsing mode! Report on the entire failed + # option. GNU includes =value but we omit it for sanity with + # very long values. + _getopt_err "$name: unrecognized option '${1%%=*}'" + else + _getopt_err "$name: invalid option -- '${o#-}'" + if [[ ${#1} -gt 2 ]]; then + set -- "$o" "-${1:2}" "${@:2}" + fi + fi + error=1 + fi ;; + + (*) + # GNU getopt in-place mode (leading dash on short options) + # overrides POSIXLY_CORRECT + if [[ $flags == *i* ]]; then + opts=( "${opts[@]}" "$1" ) + elif [[ $flags == *p* ]]; then + params=( "${params[@]}" "$@" ) + break + else + params=( "${params[@]}" "$1" ) + fi + esac + + shift + done + + if [[ $flags == *Q* ]]; then + true # generate no output + else + echo -n ' ' + if [[ $flags == *[cu]* ]]; then + printf '%s -- %s' "${opts[*]}" "${params[*]}" + else + if [[ $flags == *t* ]]; then + _getopt_quote_csh "${opts[@]}" -- "${params[@]}" + else + _getopt_quote "${opts[@]}" -- "${params[@]}" + fi + fi + echo + fi + + return $error + } + + _getopt_err() { + if [[ $flags != *q* ]]; then + printf '%s\n' "$1" >&2 + fi + } + + _getopt_resolve_abbrev() { + # Resolves an abbrevation from a list of possibilities. + # If the abbreviation is unambiguous, echoes the expansion on stdout + # and returns 0. If the abbreviation is ambiguous, prints a message on + # stderr and returns 1. (For first parse this should convert to exit + # status 2.) If there is no match at all, prints a message on stderr + # and returns 2. + declare a q="$1" + declare -a matches=() + shift + for a; do + if [[ $q == "$a" ]]; then + # Exact match. Squash any other partial matches. + matches=( "$a" ) + break + elif [[ $flags == *a* && $q == -[^-]* && $a == -"$q" ]]; then + # Exact alternative match. Squash any other partial matches. + matches=( "$a" ) + break + elif [[ $a == "$q"* ]]; then + # Abbreviated match. + matches=( "${matches[@]}" "$a" ) + elif [[ $flags == *a* && $q == -[^-]* && $a == -"$q"* ]]; then + # Abbreviated alternative match. + matches=( "${matches[@]}" "${a#-}" ) + fi + done + case ${#matches[@]} in + (0) + [[ $flags == *q* ]] || \ + printf "$name: unrecognized option %s\\n" >&2 \ + "$(_getopt_quote "$q")" + return 2 ;; + (1) + printf '%s' "${matches[0]}"; return 0 ;; + (*) + [[ $flags == *q* ]] || \ + printf "$name: option %s is ambiguous; possibilities: %s\\n" >&2 \ + "$(_getopt_quote "$q")" "$(_getopt_quote "${matches[@]}")" + return 1 ;; + esac + } + + _getopt_split() { + # Splits $2 at commas to build array specified by $1 + declare IFS=, + eval "$1=( \$2 )" + } + + _getopt_quote() { + # Quotes arguments with single quotes, escaping inner single quotes + declare s space='' q=\' + for s; do + printf "$space'%s'" "${s//$q/$q\\$q$q}" + space=' ' + done + } + + _getopt_quote_csh() { + # Quotes arguments with single quotes, escaping inner single quotes, + # bangs, backslashes and newlines + declare s i c space + for s; do + echo -n "$space'" + for ((i=0; i<${#s}; i++)); do + c=${s:i:1} + case $c in + (\\|\'|!) + echo -n "'\\$c'" ;; + ($'\n') + echo -n "\\$c" ;; + (*) + echo -n "$c" ;; + esac + done + echo -n \' + space=' ' + done + } + + _getopt_help() { + cat <<-EOT >&2 + + Usage: + getopt + getopt [options] [--] + getopt [options] -o|--options [options] [--] + + Parse command options. + + Options: + -a, --alternative allow long options starting with single - + -l, --longoptions the long options to be recognized + -n, --name the name under which errors are reported + -o, --options the short options to be recognized + -q, --quiet disable error reporting by getopt(3) + -Q, --quiet-output no normal output + -s, --shell set quoting conventions to those of + -T, --test test for getopt(1) version + -u, --unquoted do not quote the output + + -h, --help display this help and exit + -V, --version output version information and exit + + For more details see getopt(1). + EOT + } + + _getopt_version_check() { + if [[ -z $BASH_VERSION ]]; then + echo "getopt: unknown version of bash might not be compatible" >&2 + return 1 + fi + + # This is a lexical comparison that should be sufficient forever. + if [[ $BASH_VERSION < 2.05b ]]; then + echo "getopt: bash $BASH_VERSION might not be compatible" >&2 + return 1 + fi + + return 0 + } + + _getopt_version_check + _getopt_main "$@" + declare status=$? + unset -f _getopt_main _getopt_err _getopt_parse _getopt_quote \ + _getopt_quote_csh _getopt_resolve_abbrev _getopt_split _getopt_help \ + _getopt_version_check + return $status +} + +# vim:sw=2 diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..2d88425b9 --- /dev/null +++ b/setup.py @@ -0,0 +1,33 @@ +from setuptools import find_packages +from setuptools import setup + + +setup( + name='pre-commit-terraform', + description='Pre-commit hooks for terraform_docs_replace', + url='https://github.com/antonbabenko/pre-commit-terraform', + version_format='{tag}+{gitsha}', + + author='Contributors', + + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ], + + packages=find_packages(exclude=('tests*', 'testing*')), + install_requires=[ + 'setuptools-git-version', + ], + entry_points={ + 'console_scripts': [ + 'terraform_docs_replace = hooks.terraform_docs_replace:main', + ], + }, +) diff --git a/terraform_docs.sh b/terraform_docs.sh deleted file mode 100755 index 2e0eeca5c..000000000 --- a/terraform_docs.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -set -e - -declare -a paths -declare -a tfvars_files - -index=0 - -for file_with_path in "$@"; do - file_with_path="${file_with_path// /__REPLACED__SPACE__}" - - paths[index]=$(dirname "$file_with_path") - - if [[ "$file_with_path" == *".tfvars" ]]; then - tfvars_files+=("$file_with_path") - fi - - let "index+=1" -done - -for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" - - pushd "$path_uniq" > /dev/null - if [[ -f README.md ]]; then - if grep -q "BEGINNING OF TERRAFORM-DOCS HOOK" README.md; then - DOCS=$(terraform-docs md ./) - perl -i -s0pe 's/().*()/\1\n$replacement\n\2/s' -- -replacement="$DOCS" README.md - else - # Assume there is content in the file we want to keep and append auto documentation - echo "" >> README.md - # Sed is used to remove the final newline - terraform-docs md ./ | sed -e '$ d' >> README.md - echo "" >> README.md - echo "Updating README.md, please git add." - exit 1 - fi - else - echo "" > README.md - # Sed is used to remove the final newline - terraform-docs md ./ | sed -e '$ d' >> README.md - echo "" >> README.md - echo "Creating README.md, please git add." - exit 1 - fi - popd > /dev/null -done diff --git a/terraform_fmt.sh b/terraform_fmt.sh deleted file mode 100755 index e1383f230..000000000 --- a/terraform_fmt.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -set -e - -declare -a paths -declare -a tfvars_files - -TERRAFORM=${TERRAFORM:=terraform} - -index=0 - -for file_with_path in "$@"; do - file_with_path="${file_with_path// /__REPLACED__SPACE__}" - - paths[index]=$(dirname "$file_with_path") - - if [[ "$file_with_path" == *".tfvars" ]]; then - tfvars_files+=("$file_with_path") - fi - - let "index+=1" -done - -for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" - - pushd "$path_uniq" > /dev/null - ${TERRAFORM} fmt - popd > /dev/null -done - -# terraform.tfvars are excluded by `terraform fmt` -for tfvars_file in "${tfvars_files[@]}"; do - tfvars_file="${tfvars_file//__REPLACED__SPACE__/ }" - - ${TERRAFORM} fmt "$tfvars_file" -done diff --git a/terraform_tfsec_test.sh b/terraform_tfsec_test.sh new file mode 100755 index 000000000..8b16044f0 --- /dev/null +++ b/terraform_tfsec_test.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -eo pipefail + +main() { + initialize_ + parse_cmdline_ "$@" + + # propagate $FILES to custom function + tfsec_ "$ARGS" "$FILES" +} + +tfsec_() { + # consume modified files passed from pre-commit so that + # tfsec runs against only those relevant directories + for file_with_path in $FILES; do + file_with_path="${file_with_path// /__REPLACED__SPACE__}" + paths[index]=$(dirname "$file_with_path") + + let "index+=1" + done + + for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do + path_uniq="${path_uniq//__REPLACED__SPACE__/ }" + pushd "$path_uniq" > /dev/null + tfsec $ARGS || true + popd > /dev/null + done +} + +initialize_() { + # get directory containing this script + local dir + local source + source="${BASH_SOURCE[0]}" + while [[ -L $source ]]; do # resolve $source until the file is no longer a symlink + dir="$(cd -P "$(dirname "$source")" > /dev/null && pwd)" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the symlink file was located + [[ $source != /* ]] && source="$dir/$source" + done + _SCRIPT_DIR="$(dirname "$source")" + + # source getopt function + # shellcheck source=lib_getopt + . "$_SCRIPT_DIR/lib_getopt" +} + +parse_cmdline_() { + declare argv + argv=$(getopt -o a: --long args: -- "$@") || return + eval "set -- $argv" + + for argv; do + case $argv in + -a | --args) + shift + ARGS+=("$1") + shift + ;; + --) + shift + FILES+=("$@") + break + ;; + esac + done +} + +# global arrays +declare -a ARGS=() +declare -a FILES=() + +[[ ${BASH_SOURCE[0]} != "$0" ]] || main "$@" diff --git a/terraform_validate_no_variables.sh b/terraform_validate_no_variables.sh deleted file mode 100755 index 21975ca7b..000000000 --- a/terraform_validate_no_variables.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -e - -declare -a paths -index=0 - -for file_with_path in "$@"; do - file_with_path="${file_with_path// /__REPLACED__SPACE__}" - - paths[index]=$(dirname "$file_with_path") - let "index+=1" -done - -for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" - - pushd "$path_uniq" > /dev/null - - set +e - terraform validate -check-variables=false - - if [[ "$?" -ne 0 ]]; then - >&2 echo - >&2 echo "Failed path: $path_uniq" - >&2 echo "================================" - exit 1 - fi - - set -e - popd > /dev/null -done diff --git a/terraform_validate_with_variables.sh b/terraform_validate_with_variables.sh deleted file mode 100755 index 42f1b5452..000000000 --- a/terraform_validate_with_variables.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -e - -declare -a paths -index=0 - -for file_with_path in "$@"; do - file_with_path="${file_with_path// /__REPLACED__SPACE__}" - - paths[index]=$(dirname "$file_with_path") - let "index+=1" -done - -for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do - path_uniq="${path_uniq//__REPLACED__SPACE__/ }" - - pushd "$path_uniq" > /dev/null - terraform validate -check-variables=true - - if [[ "$?" -ne 0 ]]; then - echo - echo "Failed path: $path_uniq" - echo "================================" - fi - popd > /dev/null -done diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100644 index 000000000..ec77d18af --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,11 @@ +FROM pre-commit-terraform:latest + +RUN apt update && \ + apt install -y \ + datamash \ + time && \ + # Cleanup + rm -rf /var/lib/apt/lists/* + +WORKDIR /pct +ENTRYPOINT [ "/pct/tests/hooks_performance_test.sh" ] diff --git a/tests/hooks_performance_test.sh b/tests/hooks_performance_test.sh new file mode 100755 index 000000000..4f35fce23 --- /dev/null +++ b/tests/hooks_performance_test.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash + +TEST_NUM=$1 # 1000 +TEST_COMMAND=$2 # 'pre-commit try-repo -a /tmp/159/pre-commit-terraform terraform_tfsec' +TEST_DIR=$3 # '/tmp/infrastructure' +TEST_DESCRIPTION="$TEST_NUM runs '$4'" # '`terraform_tfsec` PR #123:' +RAW_TEST_RESULTS_FILE_NAME=$5 # terraform_tfsec_pr123 + +function run_tests { + local TEST_NUM=$1 + local TEST_DIR=$2 + local TEST_COMMAND + IFS=" " read -r -a TEST_COMMAND <<< "$3" + local FILE_NAME_TO_SAVE_TEST_RESULTS=$4 + + local RESULTS_DIR + RESULTS_DIR="$(pwd)/tests/results" + + cd "$TEST_DIR" || { echo "Specified TEST_DIR does not exist" && exit 1; } + # Cleanup + rm "$RESULTS_DIR/$FILE_NAME_TO_SAVE_TEST_RESULTS" + + for ((i = 1; i <= TEST_NUM; i++)); do + { + echo -e "\n\nTest run $i times\n\n" + /usr/bin/time --quiet -f '%U user %S system %P cpu %e total' \ + "${TEST_COMMAND[@]}" + } 2>> "$RESULTS_DIR/$FILE_NAME_TO_SAVE_TEST_RESULTS" + done + # shellcheck disable=2164 # Always exist + cd - > /dev/null +} + +function generate_table { + local FILE_PATH="tests/results/$1" + + local users_seconds system_seconds cpu total_time + users_seconds=$(awk '{ print $1; }' "$FILE_PATH") + system_seconds=$(awk '{ print $3; }' "$FILE_PATH") + cpu=$(awk '{ gsub("%","",$5); print $5; }' "$FILE_PATH") + total_time=$(awk '{ print $7; }' "$FILE_PATH") + + echo " +| time command | max | min | mean | median | +| -------------- | ------ | ------ | -------- | ------ | +| users seconds | $( + printf %"s\n" "$users_seconds" | datamash max 1 + ) | $( + printf %"s\n" "$users_seconds" | datamash min 1 + ) | $( + printf %"s\n" "$users_seconds" | datamash mean 1 + ) | $(printf %"s\n" "$users_seconds" | datamash median 1) | +| system seconds | $( + printf %"s\n" "$system_seconds" | datamash max 1 + ) | $( + printf %"s\n" "$system_seconds" | datamash min 1 + ) | $( + printf %"s\n" "$system_seconds" | datamash mean 1 + ) | $(printf %"s\n" "$system_seconds" | datamash median 1) | +| CPU % | $( + printf %"s\n" "$cpu" | datamash max 1 + ) | $( + printf %"s\n" "$cpu" | datamash min 1 + ) | $( + printf %"s\n" "$cpu" | datamash mean 1 + ) | $(printf %"s\n" "$cpu" | datamash median 1) | +| Total time | $( + printf %"s\n" "$total_time" | datamash max 1 + ) | $( + printf %"s\n" "$total_time" | datamash min 1 + ) | $( + printf %"s\n" "$total_time" | datamash mean 1 + ) | $(printf %"s\n" "$total_time" | datamash median 1) | +" +} + +function save_result { + local DESCRIPTION=$1 + local TABLE=$2 + local TEST_RUN_START_TIME=$3 + local TEST_RUN_END_TIME=$4 + + local FILE_NAME=${5:-"tests_result.md"} + + echo -e "\n$DESCRIPTION\n$TABLE" >> "tests/results/$FILE_NAME" + # shellcheck disable=SC2016,SC2128 # Irrelevant + echo -e ' +
Run details + +* Test Start: '"$TEST_RUN_START_TIME"' +* Test End: '"$TEST_RUN_END_TIME"' + +| Variable name | Value | +| ---------------------------- | --- | +| `TEST_NUM` | '"$TEST_NUM"' | +| `TEST_COMMAND` | '"$TEST_COMMAND"' | +| `TEST_DIR` | '"$TEST_DIR"' | +| `TEST_DESCRIPTION` | '"$TEST_DESCRIPTION"' | +| `RAW_TEST_RESULTS_FILE_NAME` | '"$RAW_TEST_RESULTS_FILE_NAME"' | + +Memory info (`head -n 6 /proc/meminfo`): + +```bash +'"$(head -n 6 /proc/meminfo)"' +``` + +CPU info: + +```bash +Real procs: '"$(grep ^cpu\\scores /proc/cpuinfo | uniq | awk '{print $4}')"' +Virtual (hyper-threading) procs: '"$(grep -c ^processor /proc/cpuinfo)"' +'"$(tail -n 28 /proc/cpuinfo)"' +``` + +
+' >> "tests/results/$FILE_NAME" + +} + +mkdir -p tests/results +TEST_RUN_START_TIME=$(date -u) +# shellcheck disable=SC2128 # Irrelevant +run_tests "$TEST_NUM" "$TEST_DIR" "$TEST_COMMAND" "$RAW_TEST_RESULTS_FILE_NAME" +TEST_RUN_END_TIME=$(date -u) + +TABLE=$(generate_table "$RAW_TEST_RESULTS_FILE_NAME") +save_result "$TEST_DESCRIPTION" "$TABLE" "$TEST_RUN_START_TIME" "$TEST_RUN_END_TIME"