├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ ├── bug_report_docker.md │ └── bug_report_local_install.md ├── CODEOWNERS ├── FUNDING.yml ├── renovate.json5 ├── dependabot.yml ├── workflows │ ├── release.yml │ ├── stale-actions.yaml │ ├── pr-title.yml │ ├── pre-commit.yaml │ ├── build-image-test.yaml │ └── build-image.yaml ├── PULL_REQUEST_TEMPLATE.md ├── .container-structure-test-config.yaml └── CONTRIBUTING.md ├── .gitignore ├── .dockerignore ├── hooks ├── __init__.py ├── tofu_fmt.sh ├── tofu_docs_replace.py ├── terrascan.sh ├── terragrunt_fmt.sh ├── terragrunt_validate.sh ├── tofu_trivy.sh ├── tofu_checkov.sh ├── tofu_tflint.sh ├── tofu_tfsec.sh ├── tfupdate.sh ├── tofu_providers_lock.sh ├── infracost_breakdown.sh ├── tofu_validate.sh ├── tofu_docs.sh ├── _common.sh └── tofu_wrapper_module_for_each.sh ├── .editorconfig ├── tests ├── Dockerfile └── hooks_performance_test.sh ├── .releaserc.json ├── setup.py ├── LICENSE ├── .pre-commit-config.yaml ├── tools └── entrypoint.sh ├── CHANGELOG.md ├── .pre-commit-hooks.yaml ├── Dockerfile ├── lib_getopt └── README.md /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/results/* 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Nmishin @anastasiiakozlova245 @kvendingoldo 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !.dockerignore 3 | !Dockerfile 4 | !tools/entrypoint.sh 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [kvendingoldo] 2 | custom: https://www.paypal.me/kvendingoldo 3 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: ["local>SpotOnInc/renovate-config"], 4 | } 5 | -------------------------------------------------------------------------------- /hooks/__init__.py: -------------------------------------------------------------------------------- 1 | print( 2 | '`tofu_docs_replace` hook is DEPRECATED.' 3 | 'TODO: For migration instructions see https://github.com/tofuutils/pre-commit-opentofu/issues/248#issuecomment-1290829226' 4 | ) 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: / 6 | schedule: 7 | interval: daily 8 | time: "11:00" 9 | commit-message: 10 | prefix: "gh-actions:" 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{*.{py,md},Dockerfile}] 11 | indent_size = 4 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pre-commit-opentofu:latest 2 | 3 | RUN apt update && \ 4 | apt install -y \ 5 | datamash \ 6 | time && \ 7 | # Cleanup 8 | rm -rf /var/lib/apt/lists/* 9 | 10 | WORKDIR /pct 11 | ENTRYPOINT [ "/pct/tests/hooks_performance_test.sh" ] 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: 5 | - feature 6 | --- 7 | 8 | 15 | 16 | ### What problem are you facing? 17 | 18 | 23 | 24 | 25 | ### How could pre-commit-opentofu help solve your problem? 26 | 27 | 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - '**/*.py' 10 | - '**/*.sh' 11 | - 'Dockerfile' 12 | - '.pre-commit-hooks.yaml' 13 | # Ignore paths 14 | - '!tests/**' 15 | jobs: 16 | release: 17 | name: Release 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 22 | with: 23 | persist-credentials: false 24 | fetch-depth: 0 25 | 26 | - name: Release 27 | uses: cycjimmy/semantic-release-action@b12c8f6015dc215fe37bc154d4ad456dd3833c90 # v6.0.0 28 | with: 29 | semantic_version: 18.0.0 30 | extra_plugins: | 31 | @semantic-release/changelog@6.0.0 32 | @semantic-release/git@10.0.0 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} 35 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | "master" 5 | ], 6 | "ci": false, 7 | "plugins": [ 8 | "@semantic-release/commit-analyzer", 9 | "@semantic-release/release-notes-generator", 10 | [ 11 | "@semantic-release/github", 12 | { 13 | "successComment": 14 | "This ${issue.pull_request ? 'PR is included' : 'issue has been resolved'} in version ${nextRelease.version} :tada:", 15 | "labels": false, 16 | "releasedLabels": false 17 | } 18 | ], 19 | [ 20 | "@semantic-release/changelog", 21 | { 22 | "changelogFile": "CHANGELOG.md", 23 | "changelogTitle": "# Changelog\n\nAll notable changes to this project will be documented in this file." 24 | } 25 | ], 26 | [ 27 | "@semantic-release/git", 28 | { 29 | "assets": [ 30 | "CHANGELOG.md" 31 | ], 32 | "message": "chore(release): version ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 33 | } 34 | ] 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | Put an `x` into the box if that apply: 6 | 7 | - [ ] This PR introduces breaking change. 8 | - [ ] This PR fixes a bug. 9 | - [ ] This PR adds new functionality. 10 | - [ ] This PR enhances existing functionality. 11 | 12 | ### Description of your changes 13 | 14 | 22 | 23 | 24 | 25 | ### How can we test changes 26 | 27 | 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages 2 | from setuptools import setup 3 | 4 | 5 | setup( 6 | name='pre-commit-opentofu', 7 | description='Pre-commit hooks for tofu_docs_replace', 8 | url='https://github.com/tofuutils/pre-commit-opentofu', 9 | version_format='{tag}+{gitsha}', 10 | 11 | author='Contributors', 12 | 13 | classifiers=[ 14 | 'License :: OSI Approved :: MIT License', 15 | 'Programming Language :: Python :: 2', 16 | 'Programming Language :: Python :: 2.7', 17 | 'Programming Language :: Python :: 3', 18 | 'Programming Language :: Python :: 3.6', 19 | 'Programming Language :: Python :: 3.7', 20 | 'Programming Language :: Python :: Implementation :: CPython', 21 | 'Programming Language :: Python :: Implementation :: PyPy', 22 | ], 23 | 24 | packages=find_packages(exclude=('tests*', 'testing*')), 25 | install_requires=[ 26 | 'setuptools-git-version', 27 | ], 28 | entry_points={ 29 | 'console_scripts': [ 30 | 'tofu_docs_replace = hooks.tofu_docs_replace:main', 31 | ], 32 | }, 33 | ) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Anton Babenko, https://github.com/antonbabenko/pre-commit-terraform 2 | Copyright (c) 2024 tofuutils authors 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/stale-actions.yaml: -------------------------------------------------------------------------------- 1 | name: "Mark or close stale issues and PRs" 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | # Staling issues and PR's 14 | days-before-stale: 30 15 | stale-issue-label: stale 16 | stale-pr-label: stale 17 | stale-issue-message: | 18 | This issue has been automatically marked as stale because it has been open 30 days 19 | with no activity. Remove stale label or comment or this issue will be closed in 10 days 20 | stale-pr-message: | 21 | This PR has been automatically marked as stale because it has been open 30 days 22 | with no activity. Remove stale label or comment or this PR will be closed in 10 days 23 | # Not stale if have this labels or part of milestone 24 | exempt-issue-labels: bug,wip,on-hold,auto-update 25 | exempt-pr-labels: bug,wip,on-hold 26 | exempt-all-milestones: true 27 | # Close issue operations 28 | # Label will be automatically removed if the issues are no longer closed nor locked. 29 | days-before-close: 10 30 | delete-branch: true 31 | close-issue-message: This issue was automatically closed because of stale in 10 days 32 | close-pr-message: This PR was automatically closed because of stale in 10 days 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docker bug report 3 | about: Create a bug report 4 | labels: 5 | - bug 6 | - area/docker 7 | --- 8 | 9 | 16 | 17 | ### Describe the bug 18 | 19 | 23 | 24 | 25 | ### How can we reproduce it? 26 | 27 | 41 | 42 | 43 | ### Environment information 44 | 45 | * OS: 46 | 47 | 53 | 54 | * `docker info`: 55 | 56 |
command output 57 | 58 | ```bash 59 | INSERT_OUTPUT_HERE 60 | ``` 61 | 62 |
63 | 64 | * Docker image tag/git commit: 65 | 66 | * Tools versions. Don't forget to specify right tag in command - 67 | `TAG=latest && docker run --entrypoint cat pre-commit:$TAG /usr/bin/tools_versions_info` 68 | 69 | ```bash 70 | INSERT_OUTPUT_HERE 71 | ``` 72 | 73 | * `.pre-commit-config.yaml`: 74 | 75 |
file content 76 | 77 | ```bash 78 | INSERT_FILE_CONTENT_HERE 79 | ``` 80 | 81 |
82 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | # Git style 6 | - id: check-added-large-files 7 | - id: check-merge-conflict 8 | - id: check-vcs-permalinks 9 | - id: forbid-new-submodules 10 | - id: no-commit-to-branch 11 | 12 | # Common errors 13 | - id: end-of-file-fixer 14 | - id: trailing-whitespace 15 | args: [--markdown-linebreak-ext=md] 16 | exclude: CHANGELOG.md 17 | - id: check-yaml 18 | - id: check-merge-conflict 19 | - id: check-executables-have-shebangs 20 | 21 | # Cross platform 22 | - id: check-case-conflict 23 | - id: mixed-line-ending 24 | args: [--fix=lf] 25 | 26 | # Security 27 | - id: detect-aws-credentials 28 | args: ['--allow-missing-credentials'] 29 | - id: detect-private-key 30 | 31 | 32 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 33 | rev: 3.0.0 34 | hooks: 35 | - id: shfmt 36 | args: ['-l', '-i', '2', '-ci', '-sr', '-w'] 37 | - id: shellcheck 38 | 39 | # Dockerfile linter 40 | - repo: https://github.com/hadolint/hadolint 41 | rev: v2.12.1-beta 42 | hooks: 43 | - id: hadolint 44 | args: [ 45 | '--ignore', 'DL3027', # Do not use apt 46 | '--ignore', 'DL3007', # Using latest 47 | '--ignore', 'DL4006', # Not related to alpine 48 | '--ignore', 'SC1091', # Useless check 49 | '--ignore', 'SC2015', # Useless check 50 | '--ignore', 'SC3037', # Not related to alpine 51 | '--ignore', 'DL3013', # Pin versions in pip 52 | ] 53 | 54 | # JSON5 Linter 55 | - repo: https://github.com/pre-commit/mirrors-prettier 56 | rev: v3.1.0 57 | hooks: 58 | - id: prettier 59 | # https://prettier.io/docs/en/options.html#parser 60 | files: '.json5$' 61 | -------------------------------------------------------------------------------- /hooks/tofu_fmt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | function main { 11 | common::initialize "$SCRIPT_DIR" 12 | common::parse_cmdline "$@" 13 | common::export_provided_env_vars "${ENV_VARS[@]}" 14 | common::parse_and_export_env_vars 15 | 16 | # Suppress tofu fmt color 17 | if [ "$PRE_COMMIT_COLOR" = "never" ]; then 18 | ARGS+=("-no-color") 19 | fi 20 | 21 | # shellcheck disable=SC2153 # False positive 22 | common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" 23 | } 24 | 25 | ####################################################################### 26 | # Unique part of `common::per_dir_hook`. The function is executed in loop 27 | # on each provided dir path. Run wrapped tool with specified arguments 28 | # Arguments: 29 | # dir_path (string) PATH to dir relative to git repo root. 30 | # Can be used in error logging 31 | # change_dir_in_unique_part (string/false) Modifier which creates 32 | # possibilities to use non-common chdir strategies. 33 | # Availability depends on hook. 34 | # args (array) arguments that configure wrapped tool behavior 35 | # Outputs: 36 | # If failed - print out hook checks status 37 | ####################################################################### 38 | function per_dir_hook_unique_part { 39 | # shellcheck disable=SC2034 # Unused var. 40 | local -r dir_path="$1" 41 | # shellcheck disable=SC2034 # Unused var. 42 | local -r change_dir_in_unique_part="$2" 43 | shift 2 44 | local -a -r args=("$@") 45 | 46 | # pass the arguments to hook 47 | tofu fmt "${args[@]}" 48 | 49 | # return exit code to common::per_dir_hook 50 | local exit_code=$? 51 | return $exit_code 52 | } 53 | 54 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 55 | -------------------------------------------------------------------------------- /hooks/tofu_docs_replace.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | 7 | def main(argv=None): 8 | parser = argparse.ArgumentParser( 9 | description="""Run terraform-docs on a set of files. Follows the standard convention of 10 | pulling the documentation from main.(tf|tofu) in order to replace the entire 11 | README.md file each time.""" 12 | ) 13 | parser.add_argument( 14 | "--dest", 15 | dest="dest", 16 | default="README.md", 17 | ) 18 | parser.add_argument( 19 | "--sort-inputs-by-required", 20 | dest="sort", 21 | action="store_true", 22 | help="[deprecated] use --sort-by-required instead", 23 | ) 24 | parser.add_argument( 25 | "--sort-by-required", 26 | dest="sort", 27 | action="store_true", 28 | ) 29 | parser.add_argument( 30 | "--with-aggregate-type-defaults", 31 | dest="aggregate", 32 | action="store_true", 33 | help="[deprecated]", 34 | ) 35 | parser.add_argument("filenames", nargs="*", help="Filenames to check.") 36 | args = parser.parse_args(argv) 37 | 38 | dirs = [] 39 | for filename in args.filenames: 40 | if os.path.realpath(filename) not in dirs and ( 41 | filename.endswith(".tf") 42 | or filename.endswith(".tofu") 43 | or filename.endswith(".tfvars") 44 | ): 45 | dirs.append(os.path.dirname(filename)) 46 | 47 | retval = 0 48 | 49 | for dir in dirs: 50 | try: 51 | procArgs = [] 52 | procArgs.append("terraform-docs") 53 | if args.sort: 54 | procArgs.append("--sort-by-required") 55 | procArgs.append("md") 56 | procArgs.append("./{dir}".format(dir=dir)) 57 | procArgs.append(">") 58 | procArgs.append("./{dir}/{dest}".format(dir=dir, dest=args.dest)) 59 | subprocess.check_call(" ".join(procArgs), shell=True) 60 | except subprocess.CalledProcessError as e: 61 | print(e) 62 | retval = 1 63 | return retval 64 | 65 | 66 | if __name__ == "__main__": 67 | sys.exit(main()) 68 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: "Validate PR title" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | # Please look up the latest version from 16 | # https://github.com/amannn/action-semantic-pull-request/releases 17 | - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | # Configure which types are allowed. 22 | # Default: https://github.com/commitizen/conventional-commit-types 23 | types: | 24 | fix 25 | feat 26 | docs 27 | ci 28 | chore 29 | # Configure that a scope must always be provided. 30 | requireScope: false 31 | # Configure additional validation for the subject based on a regex. 32 | # This example ensures the subject starts with an uppercase character. 33 | subjectPattern: ^[A-Z].+$ 34 | # If `subjectPattern` is configured, you can use this property to override 35 | # the default error message that is shown when the pattern doesn't match. 36 | # The variables `subject` and `title` can be used within the message. 37 | subjectPatternError: | 38 | The subject "{subject}" found in the pull request title "{title}" 39 | didn't match the configured pattern. Please ensure that the subject 40 | starts with an uppercase character. 41 | # For work-in-progress PRs you can typically use draft pull requests 42 | # from Github. However, private repositories on the free plan don't have 43 | # this option and therefore this action allows you to opt-in to using the 44 | # special "[WIP]" prefix to indicate this state. This will avoid the 45 | # validation of the PR title and the pull request checks remain pending. 46 | # Note that a second check will be reported if this is enabled. 47 | wip: true 48 | # When using "Squash and merge" on a PR with only one commit, GitHub 49 | # will suggest using that commit message instead of the PR title for the 50 | # merge commit, and it's easy to commit this by mistake. Enable this option 51 | # to also validate the commit message for one commit PRs. 52 | validateSingleCommit: false 53 | -------------------------------------------------------------------------------- /hooks/terrascan.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | function main { 11 | common::initialize "$SCRIPT_DIR" 12 | common::parse_cmdline "$@" 13 | common::export_provided_env_vars "${ENV_VARS[@]}" 14 | common::parse_and_export_env_vars 15 | # JFYI: terrascan color already suppressed via PRE_COMMIT_COLOR=never 16 | 17 | # shellcheck disable=SC2153 # False positive 18 | common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" 19 | } 20 | 21 | ####################################################################### 22 | # Unique part of `common::per_dir_hook`. The function is executed in loop 23 | # on each provided dir path. Run wrapped tool with specified arguments 24 | # Arguments: 25 | # dir_path (string) PATH to dir relative to git repo root. 26 | # Can be used in error logging 27 | # change_dir_in_unique_part (string/false) Modifier which creates 28 | # possibilities to use non-common chdir strategies. 29 | # Availability depends on hook. 30 | # args (array) arguments that configure wrapped tool behavior 31 | # Outputs: 32 | # If failed - print out hook checks status 33 | ####################################################################### 34 | function per_dir_hook_unique_part { 35 | # shellcheck disable=SC2034 # Unused var. 36 | local -r dir_path="$1" 37 | # shellcheck disable=SC2034 # Unused var. 38 | local -r change_dir_in_unique_part="$2" 39 | shift 2 40 | local -a -r args=("$@") 41 | 42 | # pass the arguments to hook 43 | terrascan scan -i tofu "${args[@]}" 44 | 45 | # return exit code to common::per_dir_hook 46 | local exit_code=$? 47 | return $exit_code 48 | } 49 | 50 | ####################################################################### 51 | # Unique part of `common::per_dir_hook`. The function is executed one time 52 | # in the root git repo 53 | # Arguments: 54 | # args (array) arguments that configure wrapped tool behavior 55 | ####################################################################### 56 | function run_hook_on_whole_repo { 57 | local -a -r args=("$@") 58 | 59 | # pass the arguments to hook 60 | terrascan scan -i tofu "${args[@]}" 61 | 62 | # return exit code to common::per_dir_hook 63 | local exit_code=$? 64 | return $exit_code 65 | } 66 | 67 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 68 | -------------------------------------------------------------------------------- /hooks/terragrunt_fmt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | function main { 11 | common::initialize "$SCRIPT_DIR" 12 | common::parse_cmdline "$@" 13 | common::export_provided_env_vars "${ENV_VARS[@]}" 14 | common::parse_and_export_env_vars 15 | # JFYI: terragrunt hcl format color already suppressed via PRE_COMMIT_COLOR=never 16 | 17 | # shellcheck disable=SC2153 # False positive 18 | common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" 19 | } 20 | 21 | ####################################################################### 22 | # Unique part of `common::per_dir_hook`. The function is executed in loop 23 | # on each provided dir path. Run wrapped tool with specified arguments 24 | # Arguments: 25 | # dir_path (string) PATH to dir relative to git repo root. 26 | # Can be used in error logging 27 | # change_dir_in_unique_part (string/false) Modifier which creates 28 | # possibilities to use non-common chdir strategies. 29 | # Availability depends on hook. 30 | # args (array) arguments that configure wrapped tool behavior 31 | # Outputs: 32 | # If failed - print out hook checks status 33 | ####################################################################### 34 | function per_dir_hook_unique_part { 35 | # shellcheck disable=SC2034 # Unused var. 36 | local -r dir_path="$1" 37 | # shellcheck disable=SC2034 # Unused var. 38 | local -r change_dir_in_unique_part="$2" 39 | shift 2 40 | local -a -r args=("$@") 41 | 42 | # pass the arguments to hook 43 | terragrunt hcl format "${args[@]}" 44 | 45 | # return exit code to common::per_dir_hook 46 | local exit_code=$? 47 | return $exit_code 48 | } 49 | 50 | ####################################################################### 51 | # Unique part of `common::per_dir_hook`. The function is executed one time 52 | # in the root git repo 53 | # Arguments: 54 | # args (array) arguments that configure wrapped tool behavior 55 | ####################################################################### 56 | function run_hook_on_whole_repo { 57 | local -a -r args=("$@") 58 | 59 | # pass the arguments to hook 60 | terragrunt hcl format "$(pwd)" "${args[@]}" 61 | 62 | # return exit code to common::per_dir_hook 63 | local exit_code=$? 64 | return $exit_code 65 | } 66 | 67 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 68 | -------------------------------------------------------------------------------- /hooks/terragrunt_validate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | function main { 11 | common::initialize "$SCRIPT_DIR" 12 | common::parse_cmdline "$@" 13 | common::export_provided_env_vars "${ENV_VARS[@]}" 14 | common::parse_and_export_env_vars 15 | # JFYI: terragrunt validate color already suppressed via PRE_COMMIT_COLOR=never 16 | 17 | # shellcheck disable=SC2153 # False positive 18 | common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" 19 | } 20 | 21 | ####################################################################### 22 | # Unique part of `common::per_dir_hook`. The function is executed in loop 23 | # on each provided dir path. Run wrapped tool with specified arguments 24 | # Arguments: 25 | # dir_path (string) PATH to dir relative to git repo root. 26 | # Can be used in error logging 27 | # change_dir_in_unique_part (string/false) Modifier which creates 28 | # possibilities to use non-common chdir strategies. 29 | # Availability depends on hook. 30 | # args (array) arguments that configure wrapped tool behavior 31 | # Outputs: 32 | # If failed - print out hook checks status 33 | ####################################################################### 34 | function per_dir_hook_unique_part { 35 | # shellcheck disable=SC2034 # Unused var. 36 | local -r dir_path="$1" 37 | # shellcheck disable=SC2034 # Unused var. 38 | local -r change_dir_in_unique_part="$2" 39 | shift 2 40 | local -a -r args=("$@") 41 | 42 | # pass the arguments to hook 43 | terragrunt validate "${args[@]}" 44 | 45 | # return exit code to common::per_dir_hook 46 | local exit_code=$? 47 | return $exit_code 48 | } 49 | 50 | ####################################################################### 51 | # Unique part of `common::per_dir_hook`. The function is executed one time 52 | # in the root git repo 53 | # Arguments: 54 | # args (array) arguments that configure wrapped tool behavior 55 | ####################################################################### 56 | function run_hook_on_whole_repo { 57 | local -a -r args=("$@") 58 | 59 | # pass the arguments to hook 60 | terragrunt run-all validate "${args[@]}" 61 | 62 | # return exit code to common::per_dir_hook 63 | local exit_code=$? 64 | return $exit_code 65 | } 66 | 67 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 68 | -------------------------------------------------------------------------------- /hooks/tofu_trivy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | function main { 11 | common::initialize "$SCRIPT_DIR" 12 | common::parse_cmdline "$@" 13 | common::export_provided_env_vars "${ENV_VARS[@]}" 14 | common::parse_and_export_env_vars 15 | # Support for setting PATH to repo root. 16 | for i in "${!ARGS[@]}"; do 17 | ARGS[i]=${ARGS[i]/__GIT_WORKING_DIR__/$(pwd)\/} 18 | done 19 | 20 | common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" 21 | } 22 | 23 | ####################################################################### 24 | # Unique part of `common::per_dir_hook`. The function is executed in loop 25 | # on each provided dir path. Run wrapped tool with specified arguments 26 | # Arguments: 27 | # dir_path (string) PATH to dir relative to git repo root. 28 | # Can be used in error logging 29 | # change_dir_in_unique_part (string/false) Modifier which creates 30 | # possibilities to use non-common chdir strategies. 31 | # Availability depends on hook. 32 | # args (array) arguments that configure wrapped tool behavior 33 | # Outputs: 34 | # If failed - print out hook checks status 35 | ####################################################################### 36 | function per_dir_hook_unique_part { 37 | # shellcheck disable=SC2034 # Unused var. 38 | local -r dir_path="$1" 39 | # shellcheck disable=SC2034 # Unused var. 40 | local -r change_dir_in_unique_part="$2" 41 | shift 2 42 | local -a -r args=("$@") 43 | 44 | # pass the arguments to hook 45 | trivy conf "$(pwd)" --exit-code=1 "${args[@]}" 46 | 47 | # return exit code to common::per_dir_hook 48 | local exit_code=$? 49 | return $exit_code 50 | } 51 | 52 | ####################################################################### 53 | # Unique part of `common::per_dir_hook`. The function is executed one time 54 | # in the root git repo 55 | # Arguments: 56 | # args (array) arguments that configure wrapped tool behavior 57 | ####################################################################### 58 | function run_hook_on_whole_repo { 59 | local -a -r args=("$@") 60 | 61 | # pass the arguments to hook 62 | trivy conf "$(pwd)" "${args[@]}" 63 | 64 | # return exit code to common::per_dir_hook 65 | local exit_code=$? 66 | return $exit_code 67 | } 68 | 69 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 70 | -------------------------------------------------------------------------------- /hooks/tofu_checkov.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | function main { 11 | common::initialize "$SCRIPT_DIR" 12 | common::parse_cmdline "$@" 13 | common::export_provided_env_vars "${ENV_VARS[@]}" 14 | common::parse_and_export_env_vars 15 | # Support for setting PATH to repo root. 16 | for i in "${!ARGS[@]}"; do 17 | ARGS[i]=${ARGS[i]/__GIT_WORKING_DIR__/$(pwd)\/} 18 | done 19 | 20 | # Suppress checkov color 21 | if [ "$PRE_COMMIT_COLOR" = "never" ]; then 22 | export ANSI_COLORS_DISABLED=true 23 | fi 24 | 25 | common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" 26 | } 27 | 28 | ####################################################################### 29 | # Unique part of `common::per_dir_hook`. The function is executed in loop 30 | # on each provided dir path. Run wrapped tool with specified arguments 31 | # Arguments: 32 | # dir_path (string) PATH to dir relative to git repo root. 33 | # Can be used in error logging 34 | # change_dir_in_unique_part (string/false) Modifier which creates 35 | # possibilities to use non-common chdir strategies. 36 | # Availability depends on hook. 37 | # args (array) arguments that configure wrapped tool behavior 38 | # Outputs: 39 | # If failed - print out hook checks status 40 | ####################################################################### 41 | function per_dir_hook_unique_part { 42 | # shellcheck disable=SC2034 # Unused var. 43 | local -r dir_path="$1" 44 | # shellcheck disable=SC2034 # Unused var. 45 | local -r change_dir_in_unique_part="$2" 46 | shift 2 47 | local -a -r args=("$@") 48 | 49 | checkov -d . "${args[@]}" 50 | 51 | # return exit code to common::per_dir_hook 52 | local exit_code=$? 53 | return $exit_code 54 | } 55 | 56 | ####################################################################### 57 | # Unique part of `common::per_dir_hook`. The function is executed one time 58 | # in the root git repo 59 | # Arguments: 60 | # args (array) arguments that configure wrapped tool behavior 61 | ####################################################################### 62 | function run_hook_on_whole_repo { 63 | local -a -r args=("$@") 64 | 65 | # pass the arguments to hook 66 | checkov -d "$(pwd)" "${args[@]}" 67 | 68 | # return exit code to common::per_dir_hook 69 | local exit_code=$? 70 | return $exit_code 71 | } 72 | 73 | [[ ${BASH_SOURCE[0]} != "$0" ]] || main "$@" 74 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: Common issues check 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | pre-commit: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 10 | - run: | 11 | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* 12 | 13 | - name: Get changed files 14 | id: file_changes 15 | run: | 16 | export DIFF=$(git diff --name-only origin/${{ github.base_ref }} ${{ github.sha }}) 17 | echo "Diff between ${{ github.base_ref }} and ${{ github.sha }}" 18 | echo "files=$( echo "$DIFF" | xargs echo )" >> $GITHUB_OUTPUT 19 | 20 | - name: Install shfmt 21 | run: | 22 | curl -L "$(curl -s https://api.github.com/repos/mvdan/sh/releases/latest | grep -o -E -m 1 "https://.+?linux_amd64")" > shfmt \ 23 | && chmod +x shfmt && sudo mv shfmt /usr/bin/ 24 | 25 | - name: Install shellcheck 26 | run: | 27 | sudo apt update && sudo apt install shellcheck 28 | 29 | - name: Install hadolint 30 | run: | 31 | curl -L "$(curl -s https://api.github.com/repos/hadolint/hadolint/releases/latest | grep -o -E -m 1 "https://.+?/hadolint-Linux-x86_64")" > hadolint \ 32 | && chmod +x hadolint && sudo mv hadolint /usr/bin/ 33 | # Need to success pre-commit fix push 34 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 35 | with: 36 | fetch-depth: 0 37 | ref: ${{ github.event.pull_request.head.sha }} 38 | # Skip tofu_tflint which interferes to commit pre-commit auto-fixes 39 | - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 40 | with: 41 | python-version: '3.9' 42 | - name: Execute pre-commit 43 | uses: pre-commit/action@576ff52938d158a24ac7e009dfa94b1455e7df99 44 | env: 45 | SKIP: no-commit-to-branch,hadolint 46 | with: 47 | token: ${{ secrets.GITHUB_TOKEN }} 48 | extra_args: --color=always --show-diff-on-failure --files ${{ steps.file_changes.outputs.files }} 49 | # Run only skipped checks 50 | - name: Execute pre-commit check that have no auto-fixes 51 | if: always() 52 | uses: pre-commit/action@576ff52938d158a24ac7e009dfa94b1455e7df99 53 | env: 54 | 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 55 | with: 56 | extra_args: --color=always --show-diff-on-failure --files ${{ steps.file_changes.outputs.files }} 57 | -------------------------------------------------------------------------------- /hooks/tofu_tflint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | # globals variables 6 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 7 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 8 | # shellcheck source=_common.sh 9 | . "$SCRIPT_DIR/_common.sh" 10 | 11 | function main { 12 | common::initialize "$SCRIPT_DIR" 13 | common::parse_cmdline "$@" 14 | common::export_provided_env_vars "${ENV_VARS[@]}" 15 | common::parse_and_export_env_vars 16 | # Support for setting PATH to repo root. 17 | for i in "${!ARGS[@]}"; do 18 | ARGS[i]=${ARGS[i]/__GIT_WORKING_DIR__/$(pwd)\/} 19 | done 20 | # JFYI: tflint color already suppressed via PRE_COMMIT_COLOR=never 21 | 22 | # Run `tflint --init` for check that plugins installed. 23 | # It should run once on whole repo. 24 | { 25 | TFLINT_INIT=$(tflint --init "${ARGS[@]}" 2>&1) 2> /dev/null && 26 | common::colorify "green" "Command 'tflint --init' successfully done:" && 27 | echo -e "${TFLINT_INIT}\n\n\n" 28 | } || { 29 | local exit_code=$? 30 | common::colorify "red" "Command 'tflint --init' failed:" 31 | echo -e "${TFLINT_INIT}" 32 | return ${exit_code} 33 | } 34 | 35 | common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" 36 | } 37 | 38 | ####################################################################### 39 | # Unique part of `common::per_dir_hook`. The function is executed in loop 40 | # on each provided dir path. Run wrapped tool with specified arguments 41 | # Arguments: 42 | # dir_path (string) PATH to dir relative to git repo root. 43 | # Can be used in error logging 44 | # change_dir_in_unique_part (string/false) Modifier which creates 45 | # possibilities to use non-common chdir strategies. 46 | # Availability depends on hook. 47 | # args (array) arguments that configure wrapped tool behavior 48 | # Outputs: 49 | # If failed - print out hook checks status 50 | ####################################################################### 51 | function per_dir_hook_unique_part { 52 | local -r dir_path="$1" 53 | local -r change_dir_in_unique_part="$2" 54 | shift 2 55 | local -a -r args=("$@") 56 | 57 | if [ "$change_dir_in_unique_part" == "delegate_chdir" ]; then 58 | local dir_args="--chdir=$dir_path" 59 | fi 60 | 61 | # shellcheck disable=SC2086 # we need to remove the arg if its unset 62 | TFLINT_OUTPUT=$(tflint ${dir_args:-} "${args[@]}" 2>&1) 63 | local exit_code=$? 64 | 65 | if [ $exit_code -ne 0 ]; then 66 | common::colorify "yellow" "TFLint in $dir_path/:" 67 | echo -e "$TFLINT_OUTPUT" 68 | fi 69 | 70 | # return exit code to common::per_dir_hook 71 | return $exit_code 72 | } 73 | 74 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 75 | -------------------------------------------------------------------------------- /.github/.container-structure-test-config.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' 2 | commandTests: 3 | - name: "git" 4 | command: "git" 5 | args: ["--version"] 6 | expectedOutput: ["^git version 2\\.[0-9]+\\.[0-9]+\\n$"] 7 | 8 | - name: "pre-commit" 9 | command: "pre-commit" 10 | args: ["-V"] 11 | expectedOutput: ["^pre-commit ([0-9]+\\.){2}[0-9]+\\n$"] 12 | 13 | - name: "tofu" 14 | command: "tofu" 15 | args: ["-version"] 16 | expectedOutput: ["^OpenTofu v([0-9]+\\.){2}[0-9]+\\non linux_amd64\\n$"] 17 | 18 | - name: "checkov" 19 | command: "checkov" 20 | args: ["--version"] 21 | expectedOutput: ["^([0-9]+\\.){2}[0-9]+\\n$"] 22 | 23 | - name: "infracost" 24 | command: "infracost" 25 | args: ["--version"] 26 | expectedOutput: ["^Infracost v([0-9]+\\.){2}[0-9]+\\n$"] 27 | 28 | - name: "terraform-docs" 29 | command: "terraform-docs" 30 | args: ["--version"] 31 | expectedOutput: ["^terraform-docs version v([0-9]+\\.){2}[0-9]+ [a-z0-9]+ linux/amd64\\n$"] 32 | 33 | - name: "terragrunt" 34 | command: "terragrunt" 35 | args: ["--version"] 36 | expectedOutput: ["^terragrunt version v([0-9]+\\.){2}[0-9]+\\n$"] 37 | 38 | - name: "terrascan" 39 | command: "terrascan" 40 | args: [ "version" ] 41 | expectedOutput: [ "^version: v([0-9]+\\.){2}[0-9]+\\n$" ] 42 | 43 | - name: "tflint" 44 | command: "tflint" 45 | args: [ "--version" ] 46 | expectedOutput: [ "TFLint version ([0-9]+\\.){2}[0-9]+\\n" ] 47 | 48 | - name: "tfsec" 49 | command: "tfsec" 50 | args: [ "--version" ] 51 | expectedOutput: [ "([0-9]+\\.){2}[0-9]+\\n$" ] 52 | 53 | - name: "trivy" 54 | command: "trivy" 55 | args: [ "--version" ] 56 | expectedOutput: [ "Version: ([0-9]+\\.){2}[0-9]+\\n" ] 57 | 58 | - name: "tfupdate" 59 | command: "tfupdate" 60 | args: [ "--version" ] 61 | expectedOutput: [ "([0-9]+\\.){2}[0-9]+\\n$" ] 62 | 63 | - name: "hcledit" 64 | command: "hcledit" 65 | args: [ "version" ] 66 | expectedOutput: [ "([0-9]+\\.){2}[0-9]+\\n$" ] 67 | 68 | - name: "entrypoint.sh" 69 | envVars: 70 | - key: "USERID" 71 | value: "1000:1000" 72 | command: "/entrypoint.sh" 73 | args: [ "-V" ] 74 | expectedError: ["^ERROR: uid:gid 1000:1000 lacks permissions to //\\n$"] 75 | exitCode: 1 76 | 77 | - name: "su-exec" 78 | command: "su-exec" 79 | expectedOutput: ["^Usage: su-exec user-spec command \\[args\\]\\n$"] 80 | 81 | - name: "ssh" 82 | command: "ssh" 83 | args: [ "-V" ] 84 | expectedError: ["^OpenSSH_9\\.[0-9]+"] 85 | 86 | fileExistenceTests: 87 | - name: 'terrascan init' 88 | path: '/root/.terrascan/pkg/policies/opa/rego/github/github_repository/privateRepoEnabled.rego' 89 | shouldExist: true 90 | uid: 0 91 | gid: 0 92 | -------------------------------------------------------------------------------- /hooks/tofu_tfsec.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | function main { 11 | common::initialize "$SCRIPT_DIR" 12 | common::parse_cmdline "$@" 13 | common::export_provided_env_vars "${ENV_VARS[@]}" 14 | common::parse_and_export_env_vars 15 | # Support for setting PATH to repo root. 16 | for i in "${!ARGS[@]}"; do 17 | ARGS[i]=${ARGS[i]/__GIT_WORKING_DIR__/$(pwd)\/} 18 | done 19 | 20 | # Suppress tfsec color 21 | if [ "$PRE_COMMIT_COLOR" = "never" ]; then 22 | ARGS+=("--no-color") 23 | fi 24 | 25 | common::colorify "yellow" "tfsec tool was deprecated, and replaced by trivy. You can check trivy hook here:" 26 | common::colorify "yellow" "https://github.com/tofuutils/pre-commit-opentofu/tree/master#tofu_trivy" 27 | 28 | common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" 29 | } 30 | 31 | ####################################################################### 32 | # Unique part of `common::per_dir_hook`. The function is executed in loop 33 | # on each provided dir path. Run wrapped tool with specified arguments 34 | # Arguments: 35 | # dir_path (string) PATH to dir relative to git repo root. 36 | # Can be used in error logging 37 | # change_dir_in_unique_part (string/false) Modifier which creates 38 | # possibilities to use non-common chdir strategies. 39 | # Availability depends on hook. 40 | # args (array) arguments that configure wrapped tool behavior 41 | # Outputs: 42 | # If failed - print out hook checks status 43 | ####################################################################### 44 | function per_dir_hook_unique_part { 45 | # shellcheck disable=SC2034 # Unused var. 46 | local -r dir_path="$1" 47 | # shellcheck disable=SC2034 # Unused var. 48 | local -r change_dir_in_unique_part="$2" 49 | shift 2 50 | local -a -r args=("$@") 51 | 52 | # pass the arguments to hook 53 | tfsec "${args[@]}" 54 | 55 | # return exit code to common::per_dir_hook 56 | local exit_code=$? 57 | return $exit_code 58 | } 59 | 60 | ####################################################################### 61 | # Unique part of `common::per_dir_hook`. The function is executed one time 62 | # in the root git repo 63 | # Arguments: 64 | # args (array) arguments that configure wrapped tool behavior 65 | ####################################################################### 66 | function run_hook_on_whole_repo { 67 | local -a -r args=("$@") 68 | 69 | # pass the arguments to hook 70 | tfsec "$(pwd)" "${args[@]}" 71 | 72 | # return exit code to common::per_dir_hook 73 | local exit_code=$? 74 | return $exit_code 75 | } 76 | 77 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 78 | -------------------------------------------------------------------------------- /hooks/tfupdate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | function main { 11 | common::initialize "$SCRIPT_DIR" 12 | common::parse_cmdline "$@" 13 | common::export_provided_env_vars "${ENV_VARS[@]}" 14 | common::parse_and_export_env_vars 15 | # JFYI: suppress color for `tfupdate` is N/A` 16 | 17 | # Prevent PASSED scenarios for things like: 18 | # - --args=--version '~> 4.2.0' 19 | # - --args=provider aws 20 | # shellcheck disable=SC2153 # False positive 21 | if ! [[ ${ARGS[0]} =~ ^[a-z] ]]; then 22 | common::colorify 'red' "Check the hook args order in .pre-commit.config.yaml." 23 | common::colorify 'red' "Current command looks like:" 24 | common::colorify 'red' "tfupdate ${ARGS[*]}" 25 | exit 1 26 | fi 27 | 28 | # shellcheck disable=SC2153 # False positive 29 | common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" 30 | } 31 | ####################################################################### 32 | # Unique part of `common::per_dir_hook`. The function is executed in loop 33 | # on each provided dir path. Run wrapped tool with specified arguments 34 | # Arguments: 35 | # dir_path (string) PATH to dir relative to git repo root. 36 | # Can be used in error logging 37 | # change_dir_in_unique_part (string/false) Modifier which creates 38 | # possibilities to use non-common chdir strategies. 39 | # Availability depends on hook. 40 | # args (array) arguments that configure wrapped tool behavior 41 | # Outputs: 42 | # If failed - print out hook checks status 43 | ####################################################################### 44 | function per_dir_hook_unique_part { 45 | # shellcheck disable=SC2034 # Unused var. 46 | local -r dir_path="$1" 47 | # shellcheck disable=SC2034 # Unused var. 48 | local -r change_dir_in_unique_part="$2" 49 | shift 2 50 | local -a -r args=("$@") 51 | 52 | # pass the arguments to hook 53 | tfupdate "${args[@]}" . 54 | 55 | # return exit code to common::per_dir_hook 56 | local exit_code=$? 57 | return $exit_code 58 | } 59 | 60 | ####################################################################### 61 | # Unique part of `common::per_dir_hook`. The function is executed one time 62 | # in the root git repo 63 | # Arguments: 64 | # args (array) arguments that configure wrapped tool behavior 65 | ####################################################################### 66 | function run_hook_on_whole_repo { 67 | local -a -r args=("$@") 68 | 69 | # pass the arguments to hook 70 | tfupdate "${args[@]}" --recursive . 71 | 72 | # return exit code to common::per_dir_hook 73 | local exit_code=$? 74 | return $exit_code 75 | } 76 | 77 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 78 | -------------------------------------------------------------------------------- /.github/workflows/build-image-test.yaml: -------------------------------------------------------------------------------- 1 | name: "Build Dockerfile if changed and run smoke tests" 2 | 3 | on: [pull_request] 4 | 5 | env: 6 | IMAGE_TAG: pr-test 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Get changed Dockerfile 17 | id: changed-files-specific 18 | uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0 19 | with: 20 | files: | 21 | Dockerfile 22 | .dockerignore 23 | tools/entrypoint.sh 24 | .github/workflows/build-image-test.yaml 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 28 | 29 | - name: Build if Dockerfile changed 30 | if: steps.changed-files-specific.outputs.any_changed == 'true' 31 | uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 32 | with: 33 | context: . 34 | build-args: | 35 | INSTALL_ALL=true 36 | platforms: linux/amd64 # Only one allowed here, see https://github.com/docker/buildx/issues/59#issuecomment-1433097926 37 | push: false 38 | load: true 39 | tags: | 40 | ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }} 41 | # Fix multi-platform: https://github.com/docker/buildx/issues/1533 42 | provenance: false 43 | secrets: | 44 | "github_token=${{ secrets.GITHUB_TOKEN }}" 45 | 46 | - name: Run structure tests 47 | if: steps.changed-files-specific.outputs.any_changed == 'true' 48 | uses: plexsystems/container-structure-test-action@c0a028aa96e8e82ae35be556040340cbb3e280ca # v0.3.0 49 | with: 50 | image: ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }} 51 | config: .github/.container-structure-test-config.yaml 52 | 53 | - name: Dive - check image for waste files 54 | if: steps.changed-files-specific.outputs.any_changed == 'true' 55 | uses: MaxymVlasov/dive-action@fafb796951b322cc4926b8a5eafda89ab9de8edf # v1.5.1 56 | with: 57 | image: ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }} 58 | config-file: ${{ github.workspace }}/.github/.dive-ci.yaml 59 | github-token: ${{ secrets.GITHUB_TOKEN }} 60 | 61 | # Can't build both platforms and use --load at the same time 62 | # https://github.com/docker/buildx/issues/59#issuecomment-1433097926 63 | - name: Build Multi-arch docker-image 64 | if: steps.changed-files-specific.outputs.any_changed == 'true' 65 | uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 66 | with: 67 | context: . 68 | build-args: | 69 | INSTALL_ALL=true 70 | platforms: linux/amd64,linux/arm64 71 | push: false 72 | tags: | 73 | ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }} 74 | # Fix multi-platform: https://github.com/docker/buildx/issues/1533 75 | provenance: false 76 | secrets: | 77 | "github_token=${{ secrets.GITHUB_TOKEN }}" 78 | -------------------------------------------------------------------------------- /tools/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #exit on error 3 | set -e 4 | 5 | readonly USERBASE="run" 6 | readonly BASHPATH="/bin/bash" 7 | readonly HOMEPATH="/home" 8 | 9 | function echo_error_and_exit { 10 | echo -e "ERROR: " "$@" >&2 11 | exit 1 12 | } 13 | 14 | # make sure entrypoint is running as root 15 | if [[ $(id -u) -ne 0 ]]; then 16 | echo_error_and_exit "Container must run as root. Use environment variable USERID to set user.\n" \ 17 | "Example: \"TAG=latest && " \ 18 | "docker run -e USERID=$(id -u):$(id -g) -v $(pwd):/lint -w /lint ghcr.io/tofuutils/pre-commit-opentofu:$TAG run -a\"" 19 | fi 20 | 21 | # make sure USERID makes sense as UID:GID 22 | # it looks like the alpine distro limits UID and GID to 256000, but 23 | # could be more, so we accept any valid integers 24 | USERID=${USERID:-"0:0"} 25 | if [[ ! $USERID =~ ^[0-9]+:[0-9]+$ ]]; then 26 | echo_error_and_exit "USERID environment variable invalid, format is userid:groupid. Received: \"$USERID\"" 27 | fi 28 | 29 | # separate uid and gid 30 | uid=${USERID%%:*} 31 | gid=${USERID##*:} 32 | 33 | # if requested UID:GID is root, go ahead and run without other processing 34 | [[ $USERID == "0:0" ]] && exec su-exec "$USERID" pre-commit "$@" 35 | 36 | # make sure workdir and some files are readable/writable by the provided UID/GID 37 | # combo, otherwise will have errors when processing hooks 38 | wdir="$(pwd)" 39 | if ! su-exec "$USERID" "$BASHPATH" -c "test -w $wdir && test -r $wdir"; then 40 | echo_error_and_exit "uid:gid $USERID lacks permissions to $wdir/" 41 | fi 42 | wdirgitindex="$wdir/.git/index" 43 | if ! su-exec "$USERID" "$BASHPATH" -c "test -w $wdirgitindex && test -r $wdirgitindex"; then 44 | echo_error_and_exit "uid:gid $USERID cannot write to $wdirgitindex" 45 | fi 46 | 47 | # check if group by this GID already exists, if so get the name since adduser 48 | # only accepts names 49 | if groupinfo="$(getent group "$gid")"; then 50 | groupname="${groupinfo%%:*}" 51 | else 52 | # create group in advance in case GID is different than UID 53 | groupname="$USERBASE$gid" 54 | if ! err="$(addgroup -g "$gid" "$groupname" 2>&1)"; then 55 | echo_error_and_exit "failed to create gid \"$gid\" with name \"$groupname\"\ncommand output: \"$err\"" 56 | fi 57 | fi 58 | 59 | # check if user by this UID already exists, if so get the name since id 60 | # only accepts names 61 | if userinfo="$(getent passwd "$uid")"; then 62 | username="${userinfo%%:*}" 63 | else 64 | username="$USERBASE$uid" 65 | if ! err="$(adduser -h "$HOMEPATH$username" -s "$BASHPATH" -G "$groupname" -D -u "$uid" -k "$HOME" "$username" 2>&1)"; then 66 | echo_error_and_exit "failed to create uid \"$uid\" with name \"$username\" and group \"$groupname\"\ncommand output: \"$err\"" 67 | fi 68 | fi 69 | 70 | # it's possible it was not in the group specified, add it 71 | if ! idgroupinfo="$(id -G "$username" 2>&1)"; then 72 | echo_error_and_exit "failed to get group list for username \"$username\"\ncommand output: \"$idgroupinfo\"" 73 | fi 74 | if [[ ! " $idgroupinfo " =~ [[:blank:]]${gid}[[:blank:]] ]]; then 75 | if ! err="$(addgroup "$username" "$groupname" 2>&1)"; then 76 | echo_error_and_exit "failed to add user \"$username\" to group \"$groupname\"\ncommand output: \"$err\"" 77 | fi 78 | fi 79 | 80 | # user and group of specified UID/GID should exist now, and user should be 81 | # a member of group, so execute pre-commit 82 | exec su-exec "$USERID" pre-commit "$@" 83 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_local_install.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Local installation bug report 3 | about: Create a bug report 4 | labels: 5 | - bug 6 | - area/local_installation 7 | --- 8 | 9 | 16 | 17 | ### Describe the bug 18 | 19 | 23 | 24 | 25 | ### How can we reproduce it? 26 | 27 | 41 | 42 | 43 | ### Environment information 44 | 45 | * OS: 46 | 52 | 53 | * `uname -a` and/or `systeminfo | Select-String "^OS"` output: 54 | 55 | ```bash 56 | INSERT_OUTPUT_HERE 57 | ``` 58 | 59 | 73 | 74 | * Tools availability and versions: 75 | 76 | 95 | 96 | ```bash 97 | INSERT_TOOLS_VERSIONS_HERE 98 | ``` 99 | 100 | 101 | * `.pre-commit-config.yaml`: 102 | 103 |
file content 104 | 105 | ```bash 106 | INSERT_FILE_CONTENT_HERE 107 | ``` 108 | 109 |
110 | -------------------------------------------------------------------------------- /.github/workflows/build-image.yaml: -------------------------------------------------------------------------------- 1 | name: Publish container image 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: 7 | - created 8 | schedule: 9 | - cron: '00 00 * * *' 10 | 11 | jobs: 12 | docker: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v5.0.1 17 | 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v3 20 | 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | 24 | - name: Login to ghcr.io 25 | uses: docker/login-action@v3 26 | with: 27 | registry: ghcr.io 28 | username: ${{ github.repository_owner }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Set tag for image 32 | run: | 33 | echo IMAGE_TAG=$([ ${{ github.ref_type }} == 'tag' ] && echo ${{ github.ref_name }} || echo 'latest') >> $GITHUB_ENV 34 | 35 | - name: Build and Push release to ghcr.io 36 | if: github.event_name != 'schedule' 37 | uses: docker/build-push-action@v5 38 | with: 39 | context: . 40 | build-args: | 41 | INSTALL_ALL=true 42 | platforms: linux/amd64,linux/arm64 43 | push: true 44 | tags: | 45 | ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }} 46 | ghcr.io/${{ github.repository }}:latest 47 | # Fix multi-platform: https://github.com/docker/buildx/issues/1533 48 | provenance: false 49 | secrets: | 50 | "github_token=${{ secrets.GITHUB_TOKEN }}" 51 | 52 | - name: Build and Push nightly to ghcr.io 53 | if: github.event_name == 'schedule' 54 | uses: docker/build-push-action@v5 55 | with: 56 | context: . 57 | build-args: | 58 | INSTALL_ALL=true 59 | platforms: linux/amd64,linux/arm64 60 | push: true 61 | tags: | 62 | ghcr.io/${{ github.repository }}:nightly 63 | # Fix multi-platform: https://github.com/docker/buildx/issues/1533 64 | provenance: false 65 | secrets: | 66 | "github_token=${{ secrets.GITHUB_TOKEN }}" 67 | 68 | - name: Login to DockerHub Container Registry 69 | uses: docker/login-action@v3 70 | with: 71 | registry: registry.hub.docker.com 72 | username: ${{ secrets.DOCKERHUB_USER }} 73 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 74 | 75 | - name: Build and Push release to DockerHub 76 | if: github.event_name != 'schedule' 77 | uses: docker/build-push-action@v5 78 | with: 79 | context: . 80 | build-args: | 81 | INSTALL_ALL=true 82 | platforms: linux/amd64,linux/arm64 83 | push: true 84 | tags: | 85 | registry.hub.docker.com/tofuutils/pre-commit-opentofu:${{ env.IMAGE_TAG }} 86 | registry.hub.docker.com/tofuutils/pre-commit-opentofu:latest 87 | provenance: false 88 | 89 | - name: Build and Push nightly to DockerHub 90 | if: github.event_name == 'schedule' 91 | uses: docker/build-push-action@v5 92 | with: 93 | context: . 94 | build-args: | 95 | INSTALL_ALL=true 96 | platforms: linux/amd64,linux/arm64 97 | push: true 98 | tags: | 99 | registry.hub.docker.com/tofuutils/pre-commit-opentofu:nightly 100 | provenance: false 101 | 102 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [2.2.2](https://github.com/tofuutils/pre-commit-opentofu/compare/v2.2.1...v2.2.2) (2025-10-22) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * replace deprecated hclfmt with hcl format command ([f1a589b](https://github.com/tofuutils/pre-commit-opentofu/commit/f1a589bd124b277cc02fcbf04ee05017fb8822c0)) 11 | 12 | ## [2.2.1](https://github.com/tofuutils/pre-commit-opentofu/compare/v2.2.0...v2.2.1) (2025-06-04) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * make infracost_breakdown.sh compatible with bash 3.2 (macOS) ([df886fa](https://github.com/tofuutils/pre-commit-opentofu/commit/df886fa772e7d1eedf5603327c0cf02968e7d779)) 18 | * Update pre-commit/action version ([#30](https://github.com/tofuutils/pre-commit-opentofu/issues/30)) ([44c7b5d](https://github.com/tofuutils/pre-commit-opentofu/commit/44c7b5dec9362d2fe7ed5e8786f4d95956791d3d)) 19 | 20 | # [2.2.0](https://github.com/tofuutils/pre-commit-opentofu/compare/v2.1.0...v2.2.0) (2025-03-29) 21 | 22 | 23 | ### Features 24 | 25 | * make release ([e625db1](https://github.com/tofuutils/pre-commit-opentofu/commit/e625db13ec285e132f43cdf6e5aa3f3272e45451)) 26 | 27 | # [2.1.0](https://github.com/tofuutils/pre-commit-opentofu/compare/v2.0.0...v2.1.0) (2024-10-16) 28 | 29 | 30 | ### Features 31 | 32 | * spport .tofu files ([#6](https://github.com/tofuutils/pre-commit-opentofu/issues/6)) ([e059c58](https://github.com/tofuutils/pre-commit-opentofu/commit/e059c5859bceddf1ca018f55851f6940ad51f1c2)) 33 | 34 | # [2.0.0](https://github.com/tofuutils/pre-commit-opentofu/compare/v1.0.4...v2.0.0) (2024-09-25) 35 | 36 | 37 | ### Features 38 | 39 | * **tofu:** add handling for missing tofu binary in Docker image This commit introduces logic to gracefully handle the case when the tofu binary is not found in the Docker image, improving the overall user experience. BREAKING CHANGE: The previous behavior of the application when the tofu binary was missing may have caused unexpected crashes. ([14fc63e](https://github.com/tofuutils/pre-commit-opentofu/commit/14fc63eb5b04e3ad1525d06e437b15935841775f)) 40 | 41 | 42 | ### BREAKING CHANGES 43 | 44 | * **tofu:** The previous behavior of the application when the tofu binary was missing may have caused unexpected crashes." 45 | 46 | ## [1.0.4](https://github.com/tofuutils/pre-commit-opentofu/compare/v1.0.3...v1.0.4) (2024-09-21) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * docker image reference in README.md ([7b04f0c](https://github.com/tofuutils/pre-commit-opentofu/commit/7b04f0c24940f1642c8f599bfd0794dd46b0b274)) 52 | * docker image reference in README.md ([f9b71fe](https://github.com/tofuutils/pre-commit-opentofu/commit/f9b71fe08fedd4ceb23ced6fe2171edf24add290)) 53 | * dockerhub ([0fac591](https://github.com/tofuutils/pre-commit-opentofu/commit/0fac59197f2f2cb4bc417917e5adb6ac92a20b7a)) 54 | * entry for tofu_docs_replace ([f146463](https://github.com/tofuutils/pre-commit-opentofu/commit/f146463ac8effcfa441f3f6b21e811095f0da73c)) 55 | 56 | ## [1.0.2](https://github.com/tofuutils/pre-commit-opentofu/compare/v1.0.1...v1.0.2) (2024-03-08) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * remove obsolete terraform checks and awk file hack ([97cba7a](https://github.com/tofuutils/pre-commit-opentofu/commit/97cba7a646996c7cae3719f1b6241d47da5882d9)) 62 | 63 | ## [1.0.1](https://github.com/tofuutils/pre-commit-opentofu/compare/v1.0.0...v1.0.1) (2024-03-07) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * dockerfile ([65b197c](https://github.com/tofuutils/pre-commit-opentofu/commit/65b197c841dc10aa772c7fc2594a213a9158d2f4)) 69 | 70 | # [1.0.0](https://github.com/tofuutils/pre-commit-opentofu/compare/v1.0.0) (2023-12-21) 71 | 72 | 73 | ### Features 74 | 75 | * TODO 76 | -------------------------------------------------------------------------------- /tests/hooks_performance_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TEST_NUM=$1 # 1000 4 | TEST_COMMAND=$2 # 'pre-commit try-repo -a /tmp/159/pre-commit-opentofu tofu_tfsec' 5 | TEST_DIR=$3 # '/tmp/infrastructure' 6 | TEST_DESCRIPTION="$TEST_NUM runs '$4'" # '`tofu_tfsec` PR #123:' 7 | RAW_TEST_RESULTS_FILE_NAME=$5 # tofu_tfsec_pr123 8 | 9 | function run_tests { 10 | local TEST_NUM=$1 11 | local TEST_DIR=$2 12 | local TEST_COMMAND 13 | IFS=" " read -r -a TEST_COMMAND <<< "$3" 14 | local FILE_NAME_TO_SAVE_TEST_RESULTS=$4 15 | 16 | local RESULTS_DIR 17 | RESULTS_DIR="$(pwd)/tests/results" 18 | 19 | cd "$TEST_DIR" || { echo "Specified TEST_DIR does not exist" && exit 1; } 20 | # Cleanup 21 | rm "$RESULTS_DIR/$FILE_NAME_TO_SAVE_TEST_RESULTS" 22 | 23 | for ((i = 1; i <= TEST_NUM; i++)); do 24 | { 25 | echo -e "\n\nTest run $i times\n\n" 26 | /usr/bin/time --quiet -f '%U user %S system %P cpu %e total' \ 27 | "${TEST_COMMAND[@]}" 28 | } 2>> "$RESULTS_DIR/$FILE_NAME_TO_SAVE_TEST_RESULTS" 29 | done 30 | # shellcheck disable=2164 # Always exist 31 | cd - > /dev/null 32 | } 33 | 34 | function generate_table { 35 | local FILE_PATH="tests/results/$1" 36 | 37 | local users_seconds system_seconds cpu total_time 38 | users_seconds=$(awk '{ print $1; }' "$FILE_PATH") 39 | system_seconds=$(awk '{ print $3; }' "$FILE_PATH") 40 | cpu=$(awk '{ gsub("%","",$5); print $5; }' "$FILE_PATH") 41 | total_time=$(awk '{ print $7; }' "$FILE_PATH") 42 | 43 | echo " 44 | | time command | max | min | mean | median | 45 | | -------------- | ------ | ------ | -------- | ------ | 46 | | users seconds | $( 47 | printf %"s\n" "$users_seconds" | datamash max 1 48 | ) | $( 49 | printf %"s\n" "$users_seconds" | datamash min 1 50 | ) | $( 51 | printf %"s\n" "$users_seconds" | datamash mean 1 52 | ) | $(printf %"s\n" "$users_seconds" | datamash median 1) | 53 | | system seconds | $( 54 | printf %"s\n" "$system_seconds" | datamash max 1 55 | ) | $( 56 | printf %"s\n" "$system_seconds" | datamash min 1 57 | ) | $( 58 | printf %"s\n" "$system_seconds" | datamash mean 1 59 | ) | $(printf %"s\n" "$system_seconds" | datamash median 1) | 60 | | CPU % | $( 61 | printf %"s\n" "$cpu" | datamash max 1 62 | ) | $( 63 | printf %"s\n" "$cpu" | datamash min 1 64 | ) | $( 65 | printf %"s\n" "$cpu" | datamash mean 1 66 | ) | $(printf %"s\n" "$cpu" | datamash median 1) | 67 | | Total time | $( 68 | printf %"s\n" "$total_time" | datamash max 1 69 | ) | $( 70 | printf %"s\n" "$total_time" | datamash min 1 71 | ) | $( 72 | printf %"s\n" "$total_time" | datamash mean 1 73 | ) | $(printf %"s\n" "$total_time" | datamash median 1) | 74 | " 75 | } 76 | 77 | function save_result { 78 | local DESCRIPTION=$1 79 | local TABLE=$2 80 | local TEST_RUN_START_TIME=$3 81 | local TEST_RUN_END_TIME=$4 82 | 83 | local FILE_NAME=${5:-"tests_result.md"} 84 | 85 | echo -e "\n$DESCRIPTION\n$TABLE" >> "tests/results/$FILE_NAME" 86 | # shellcheck disable=SC2016,SC2128 # Irrelevant 87 | echo -e ' 88 |
Run details 89 | 90 | * Test Start: '"$TEST_RUN_START_TIME"' 91 | * Test End: '"$TEST_RUN_END_TIME"' 92 | 93 | | Variable name | Value | 94 | | ---------------------------- | --- | 95 | | `TEST_NUM` | '"$TEST_NUM"' | 96 | | `TEST_COMMAND` | '"$TEST_COMMAND"' | 97 | | `TEST_DIR` | '"$TEST_DIR"' | 98 | | `TEST_DESCRIPTION` | '"$TEST_DESCRIPTION"' | 99 | | `RAW_TEST_RESULTS_FILE_NAME` | '"$RAW_TEST_RESULTS_FILE_NAME"' | 100 | 101 | Memory info (`head -n 6 /proc/meminfo`): 102 | 103 | ```bash 104 | '"$(head -n 6 /proc/meminfo)"' 105 | ``` 106 | 107 | CPU info: 108 | 109 | ```bash 110 | Real procs: '"$(grep ^cpu\\scores /proc/cpuinfo | uniq | awk '{print $4}')"' 111 | Virtual (hyper-threading) procs: '"$(grep -c ^processor /proc/cpuinfo)"' 112 | '"$(tail -n 28 /proc/cpuinfo)"' 113 | ``` 114 | 115 |
116 | ' >> "tests/results/$FILE_NAME" 117 | 118 | } 119 | 120 | mkdir -p tests/results 121 | TEST_RUN_START_TIME=$(date -u) 122 | # shellcheck disable=SC2128 # Irrelevant 123 | run_tests "$TEST_NUM" "$TEST_DIR" "$TEST_COMMAND" "$RAW_TEST_RESULTS_FILE_NAME" 124 | TEST_RUN_END_TIME=$(date -u) 125 | 126 | TABLE=$(generate_table "$RAW_TEST_RESULTS_FILE_NAME") 127 | save_result "$TEST_DESCRIPTION" "$TABLE" "$TEST_RUN_START_TIME" "$TEST_RUN_END_TIME" 128 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: infracost_breakdown 2 | name: Infracost breakdown 3 | description: Check OpenTofu infrastructure cost 4 | entry: hooks/infracost_breakdown.sh 5 | language: script 6 | require_serial: true 7 | files: \.((tf|tofu)(vars)?|hcl)$ 8 | exclude: \.terraform\/.*$ 9 | 10 | - id: tofu_fmt 11 | name: OpenTofu fmt 12 | description: Rewrites all OpenTofu configuration files to a canonical format. 13 | entry: hooks/tofu_fmt.sh 14 | language: script 15 | files: \.(tf|tofu)(vars)?$ 16 | exclude: \.terraform\/.*$ 17 | 18 | - id: tofu_docs 19 | name: OpenTofu docs 20 | description: 21 | Inserts input and output documentation into README.md (using 22 | terraform-docs). 23 | require_serial: true 24 | entry: hooks/tofu_docs.sh 25 | language: script 26 | files: (\.(tf|tofu)|\.terraform\.lock\.hcl)$ 27 | exclude: \.terraform\/.*$ 28 | 29 | - id: tofu_docs_without_aggregate_type_defaults 30 | name: OpenTofu docs (without aggregate type defaults) 31 | description: 32 | Inserts input and output documentation into README.md (using 33 | terraform-docs). Identical to terraform_docs. 34 | require_serial: true 35 | entry: hooks/tofu_docs.sh 36 | language: script 37 | files: \.(tf|tofu)$ 38 | exclude: \.terraform\/.*$ 39 | 40 | - id: tofu_docs_replace 41 | name: OpenTofu docs (overwrite README.md) 42 | description: Overwrite content of README.md with terraform-docs. 43 | require_serial: true 44 | entry: hooks/tofu_docs_replace.py 45 | language: python 46 | files: \.(tf|tofu)$ 47 | exclude: \.terraform\/.*$ 48 | 49 | - id: tofu_validate 50 | name: OpenTofu validate 51 | description: Validates all OpenTofu configuration files. 52 | require_serial: true 53 | entry: hooks/tofu_validate.sh 54 | language: script 55 | files: \.(tf|tofu)(vars)?$ 56 | exclude: \.terraform\/.*$ 57 | 58 | - id: tofu_providers_lock 59 | name: Lock OpenTofu provider versions 60 | description: Updates provider signatures in dependency lock files. 61 | require_serial: true 62 | entry: hooks/tofu_providers_lock.sh 63 | language: script 64 | files: (\.terraform\.lock\.hcl)$ 65 | exclude: \.terraform\/.*$ 66 | 67 | - id: tofu_tflint 68 | name: OpenTofu validate with tflint 69 | description: Validates all OpenTofu configuration files with TFLint. 70 | require_serial: true 71 | entry: hooks/tofu_tflint.sh 72 | language: script 73 | files: \.(tf|tofu)(vars)?$ 74 | exclude: \.terraform\/.*$ 75 | 76 | - id: terragrunt_fmt 77 | name: Terragrunt fmt 78 | description: 79 | Rewrites all Terragrunt configuration files to a canonical format. 80 | entry: hooks/terragrunt_fmt.sh 81 | language: script 82 | files: (\.hcl)$ 83 | exclude: \.terraform\/.*$ 84 | 85 | - id: terragrunt_validate 86 | name: Terragrunt validate 87 | description: Validates all Terragrunt configuration files. 88 | entry: hooks/terragrunt_validate.sh 89 | language: script 90 | files: (\.hcl)$ 91 | exclude: \.terraform\/.*$ 92 | 93 | - id: tofu_tfsec 94 | name: OpenTofu validate with tfsec (deprecated, use "tofu_trivy") 95 | description: 96 | Static analysis of OpenTofu templates to spot potential security issues. 97 | require_serial: true 98 | entry: hooks/tofu_tfsec.sh 99 | files: \.(tf|tofu)(vars)?$ 100 | language: script 101 | 102 | - id: tofu_trivy 103 | name: OpenTofu validate with trivy 104 | description: 105 | Static analysis of OpenTofu templates to spot potential security issues. 106 | require_serial: true 107 | entry: hooks/tofu_trivy.sh 108 | files: \.(tf|tofu)(vars)?$ 109 | language: script 110 | 111 | - id: checkov 112 | name: checkov (deprecated, use "tofu_checkov") 113 | description: Runs checkov on OpenTofu templates. 114 | entry: checkov -d . 115 | language: python 116 | pass_filenames: false 117 | always_run: false 118 | files: \.tf$ 119 | exclude: \.terraform\/.*$ 120 | require_serial: true 121 | 122 | - id: tofu_checkov 123 | name: Checkov 124 | description: Runs checkov on OpenTofu templates. 125 | entry: hooks/tofu_checkov.sh 126 | language: script 127 | always_run: false 128 | files: \.(tf|tofu)$ 129 | exclude: \.terraform\/.*$ 130 | require_serial: true 131 | 132 | - id: tofu_wrapper_module_for_each 133 | name: OpenTofu wrapper with for_each in module 134 | description: Generate OpenTofu wrappers with for_each in module. 135 | entry: hooks/tofu_wrapper_module_for_each.sh 136 | language: script 137 | pass_filenames: false 138 | always_run: false 139 | require_serial: true 140 | files: \.tf$ 141 | exclude: \.terraform\/.*$ 142 | 143 | - id: terrascan 144 | name: terrascan 145 | description: Runs terrascan on OpenTofu templates. 146 | language: script 147 | entry: hooks/terrascan.sh 148 | files: \.(tf|tofu)$ 149 | exclude: \.terraform\/.*$ 150 | require_serial: true 151 | 152 | - id: tfupdate 153 | name: tfupdate 154 | description: Runs tfupdate on OpenTofu templates. 155 | language: script 156 | entry: hooks/tfupdate.sh 157 | args: 158 | - --args=terraform 159 | files: \.(tf|tofu)$ 160 | require_serial: true 161 | -------------------------------------------------------------------------------- /hooks/tofu_providers_lock.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | # globals variables 6 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 7 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 8 | # shellcheck source=_common.sh 9 | . "$SCRIPT_DIR/_common.sh" 10 | 11 | function main { 12 | common::initialize "$SCRIPT_DIR" 13 | common::parse_cmdline "$@" 14 | common::export_provided_env_vars "${ENV_VARS[@]}" 15 | common::parse_and_export_env_vars 16 | # JFYI: suppress color for `tofu providers lock` is N/A` 17 | 18 | # shellcheck disable=SC2153 # False positive 19 | common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" 20 | } 21 | 22 | ####################################################################### 23 | # Check that all needed `h1` and `zh` SHAs are included in lockfile for 24 | # each provider. 25 | # Arguments: 26 | # platforms_count (number) How many `-platform` flags provided 27 | # Outputs: 28 | # Return 0 when lockfile has all needed SHAs 29 | # Return 1-99 when lockfile is invalid 30 | # Return 100+ when not all SHAs found 31 | ####################################################################### 32 | function lockfile_contains_all_needed_sha { 33 | local -r platforms_count="$1" 34 | 35 | local h1_counter="$platforms_count" 36 | local zh_counter=0 37 | 38 | # Reading each line 39 | while read -r line; do 40 | 41 | if grep -Eq '^"h1:' <<< "$line"; then 42 | h1_counter=$((h1_counter - 1)) 43 | continue 44 | fi 45 | 46 | if grep -Eq '^"zh:' <<< "$line"; then 47 | zh_counter=0 48 | continue 49 | fi 50 | 51 | if grep -Eq '^provider' <<< "$line"; then 52 | h1_counter="$platforms_count" 53 | zh_counter=$((zh_counter + 1)) 54 | continue 55 | fi 56 | # Not all SHA inside provider lock definition block found 57 | if grep -Eq '^}' <<< "$line"; then 58 | if [ "$h1_counter" -ge 1 ] || [ "$zh_counter" -ge 1 ]; then 59 | # h1_counter can be less than 0, in the case when lockfile 60 | # contains more platforms than you currently specify 61 | # That's why here extra +50 - for safety reasons, to be sure 62 | # that error goes exactly from this part of the function 63 | return $((150 + h1_counter + zh_counter)) 64 | fi 65 | fi 66 | 67 | # lockfile always exists, because the hook triggered only on 68 | # `files: (\.terraform\.lock\.hcl)$` 69 | done < ".terraform.lock.hcl" 70 | 71 | # When you specify `-platform``, but don't specify current platform - 72 | # platforms_count will be less than `h1:` headers` 73 | [ "$h1_counter" -lt 0 ] && h1_counter=0 74 | 75 | # 0 if all OK, 2+ when invalid lockfile 76 | return $((h1_counter + zh_counter)) 77 | } 78 | 79 | ####################################################################### 80 | # Unique part of `common::per_dir_hook`. The function is executed in loop 81 | # on each provided dir path. Run wrapped tool with specified arguments 82 | # Arguments: 83 | # dir_path (string) PATH to dir relative to git repo root. 84 | # Can be used in error logging 85 | # change_dir_in_unique_part (string/false) Modifier which creates 86 | # possibilities to use non-common chdir strategies. 87 | # Availability depends on hook. 88 | # args (array) arguments that configure wrapped tool behavior 89 | # Outputs: 90 | # If failed - print out hook checks status 91 | ####################################################################### 92 | function per_dir_hook_unique_part { 93 | local -r dir_path="$1" 94 | # shellcheck disable=SC2034 # Unused var. 95 | local -r change_dir_in_unique_part="$2" 96 | shift 2 97 | local -a -r args=("$@") 98 | 99 | local platforms_count=0 100 | for arg in "${args[@]}"; do 101 | if grep -Eq '^-platform=' <<< "$arg"; then 102 | platforms_count=$((platforms_count + 1)) 103 | fi 104 | done 105 | 106 | local exit_code 107 | # 108 | # Get hook settings 109 | # 110 | local mode 111 | 112 | IFS=";" read -r -a configs <<< "${HOOK_CONFIG[*]}" 113 | 114 | for c in "${configs[@]}"; do 115 | 116 | IFS="=" read -r -a config <<< "$c" 117 | key=${config[0]} 118 | value=${config[1]} 119 | 120 | case $key in 121 | --mode) 122 | if [ "$mode" ]; then 123 | common::colorify "yellow" 'Invalid hook config. Make sure that you specify not more than one "--mode" flag' 124 | exit 1 125 | fi 126 | mode=$value 127 | ;; 128 | esac 129 | done 130 | 131 | # Available options: 132 | # only-check-is-current-lockfile-cross-platform (will be default) 133 | # always-regenerate-lockfile 134 | # TODO: Remove in 2.0 135 | if [ ! "$mode" ]; then 136 | common::colorify "yellow" "DEPRECATION NOTICE: We introduced '--mode' flag for this hook. 137 | Check migration instructions at https://github.com/tofuutils/pre-commit-opentofu#tofu_providers_lock 138 | " 139 | common::tofu_init 'OpenTofu providers lock' "$dir_path" || { 140 | exit_code=$? 141 | return $exit_code 142 | } 143 | fi 144 | 145 | if [ "$mode" == "only-check-is-current-lockfile-cross-platform" ] && 146 | lockfile_contains_all_needed_sha "$platforms_count"; then 147 | 148 | exit 0 149 | fi 150 | 151 | #? Don't require `tf init` for providers, but required `tf init` for modules 152 | #? Mitigated by `function match_validate_errors` from tofu_validate hook 153 | # pass the arguments to hook 154 | tofu providers lock "${args[@]}" 155 | 156 | # return exit code to common::per_dir_hook 157 | exit_code=$? 158 | return $exit_code 159 | } 160 | 161 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 162 | -------------------------------------------------------------------------------- /hooks/infracost_breakdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | function main { 11 | common::initialize "$SCRIPT_DIR" 12 | common::parse_cmdline "$@" 13 | common::export_provided_env_vars "${ENV_VARS[@]}" 14 | common::parse_and_export_env_vars 15 | # shellcheck disable=SC2153 # False positive 16 | infracost_breakdown_ "${HOOK_CONFIG[*]}" "${ARGS[*]}" 17 | } 18 | 19 | ####################################################################### 20 | # Wrapper around `infracost breakdown` tool which checks and compares 21 | # infra cost based on provided hook_config 22 | # Environment variables: 23 | # PRE_COMMIT_COLOR (string) If set to `never` - do not colorize output 24 | # Arguments: 25 | # hook_config (string with array) arguments that configure hook behavior 26 | # args (string with array) arguments that configure wrapped tool behavior 27 | # Outputs: 28 | # Print out hook checks status (Passed/Failed), total monthly cost and 29 | # diff, summary about infracost check (non-supported resources etc.) 30 | ####################################################################### 31 | function infracost_breakdown_ { 32 | local -r hook_config="$1" 33 | local args 34 | read -r -a args <<< "$2" 35 | 36 | # Get hook settings 37 | IFS=";" read -r -a checks <<< "$hook_config" 38 | # Suppress infracost color 39 | if [ "$PRE_COMMIT_COLOR" = "never" ]; then 40 | args+=("--no-color") 41 | fi 42 | 43 | local RESULTS 44 | RESULTS="$(infracost breakdown "${args[@]}" --format json)" 45 | local API_VERSION 46 | API_VERSION="$(jq -r .version <<< "$RESULTS")" 47 | 48 | if [ "$API_VERSION" != "0.2" ]; then 49 | common::colorify "yellow" "WARNING: Hook supports Infracost API version \"0.2\", got \"$API_VERSION\"" 50 | common::colorify "yellow" " Some things may not work as expected" 51 | fi 52 | 53 | local dir 54 | dir="$(jq '.projects[].metadata.vcsSubPath' <<< "$RESULTS")" 55 | echo -e "\nRunning in $dir" 56 | 57 | local have_failed_checks=false 58 | 59 | for check in "${checks[@]}"; do 60 | # $hook_config receives string like '1 > 2; 3 == 4;' etc. 61 | # It gets split by `;` into array, which we're parsing here ('1 > 2' ' 3 == 4') 62 | # Next line removes leading spaces, just for fancy output reason. 63 | # shellcheck disable=SC2001 # Rule exception 64 | check=$(echo "$check" | sed 's/^[[:space:]]*//') 65 | 66 | # Drop quotes in hook args section. From: 67 | # -h ".totalHourlyCost > 0.1" 68 | # --hook-config='.currency == "USD"' 69 | # To: 70 | # -h .totalHourlyCost > 0.1 71 | # --hook-config=.currency == "USD" 72 | first_char=${check:0:1} 73 | last_char=${check:$((${#check} - 1)):1} 74 | if [ "$first_char" == "$last_char" ] && { 75 | [ "$first_char" == '"' ] || [ "$first_char" == "'" ] 76 | }; then 77 | check="${check:1:$((${#check} - 2))}" 78 | fi 79 | 80 | # Replace mapfile with while read loop for bash 3.2 compatibility 81 | operations=() 82 | while IFS= read -r line; do 83 | operations+=("$line") 84 | done < <(echo "$check" | grep -oE '[!<>=]{1,2}') 85 | 86 | # Get the very last operator, that is used in comparison inside `jq` query. 87 | # From the example below we need to pick the `>` which is in between `add` and `1000`, 88 | # but not the `!=`, which goes earlier in the `jq` expression 89 | # [.projects[].diff.totalMonthlyCost | select (.!=null) | tonumber] | add > 1000 90 | operation=${operations[$((${#operations[@]} - 1))]} 91 | 92 | IFS="$operation" read -r -a jq_check <<< "$check" 93 | real_value="$(jq "${jq_check[0]}" <<< "$RESULTS")" 94 | compare_value="${jq_check[1]}${jq_check[2]}" 95 | # Check types 96 | jq_check_type="$(jq -r "${jq_check[0]} | type" <<< "$RESULTS")" 97 | compare_value_type="$(jq -r "$compare_value | type" <<< "$RESULTS")" 98 | # Fail if comparing different types 99 | if [ "$jq_check_type" != "$compare_value_type" ]; then 100 | common::colorify "yellow" "Warning: Comparing values with different types may give incorrect result" 101 | common::colorify "yellow" " Expression: $check" 102 | common::colorify "yellow" " Types in the expression: [$jq_check_type] $operation [$compare_value_type]" 103 | common::colorify "yellow" " Use 'tonumber' filter when comparing costs (e.g. '.totalMonthlyCost|tonumber')" 104 | have_failed_checks=true 105 | continue 106 | fi 107 | # Fail if string is compared not with `==` or `!=` 108 | if [ "$jq_check_type" == "string" ] && { 109 | [ "$operation" != '==' ] && [ "$operation" != '!=' ] 110 | }; then 111 | common::colorify "yellow" "Warning: Wrong comparison operator is used in expression: $check" 112 | common::colorify "yellow" " Use 'tonumber' filter when comparing costs (e.g. '.totalMonthlyCost|tonumber')" 113 | common::colorify "yellow" " Use '==' or '!=' when comparing strings (e.g. '.currency == \"USD\"')." 114 | have_failed_checks=true 115 | continue 116 | fi 117 | 118 | # Compare values 119 | check_passed="$(echo "$RESULTS" | jq "$check")" 120 | 121 | status="Passed" 122 | color="green" 123 | if ! $check_passed; then 124 | status="Failed" 125 | color="red" 126 | have_failed_checks=true 127 | fi 128 | 129 | # Print check result 130 | common::colorify $color "$status: $check\t\t$real_value $operation $compare_value" 131 | done 132 | 133 | # Fancy informational output 134 | currency="$(jq -r '.currency' <<< "$RESULTS")" 135 | 136 | echo -e "\nSummary: $(jq -r '.summary' <<< "$RESULTS")" 137 | 138 | echo -e "\nTotal Monthly Cost: $(jq -r .totalMonthlyCost <<< "$RESULTS") $currency" 139 | echo "Total Monthly Cost (diff): $(jq -r .projects[].diff.totalMonthlyCost <<< "$RESULTS") $currency" 140 | 141 | if $have_failed_checks; then 142 | exit 1 143 | fi 144 | } 145 | 146 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 147 | -------------------------------------------------------------------------------- /hooks/tofu_validate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | # `tofu validate` requires this env variable to be set 11 | export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-us-east-1} 12 | 13 | function main { 14 | common::initialize "$SCRIPT_DIR" 15 | common::parse_cmdline "$@" 16 | common::export_provided_env_vars "${ENV_VARS[@]}" 17 | common::parse_and_export_env_vars 18 | 19 | # Suppress tofu validate color 20 | if [ "$PRE_COMMIT_COLOR" = "never" ]; then 21 | ARGS+=("-no-color") 22 | fi 23 | # shellcheck disable=SC2153 # False positive 24 | common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" 25 | } 26 | 27 | ####################################################################### 28 | # Run `tofu validate` and match errors. Requires `jq` 29 | # Arguments: 30 | # validate_output (string with json) output of `tofu validate` command 31 | # Outputs: 32 | # Returns integer: 33 | # - 0 (no errors) 34 | # - 1 (matched errors; retry) 35 | # - 2 (no matched errors; do not retry) 36 | ####################################################################### 37 | function match_validate_errors { 38 | local validate_output=$1 39 | 40 | local valid 41 | local summary 42 | 43 | valid=$(jq -rc '.valid' <<< "$validate_output") 44 | 45 | if [ "$valid" == "true" ]; then 46 | return 0 47 | fi 48 | 49 | # Parse error message for retry-able errors. 50 | while IFS= read -r error_message; do 51 | summary=$(jq -rc '.summary' <<< "$error_message") 52 | case $summary in 53 | "missing or corrupted provider plugins") return 1 ;; 54 | "Module source has changed") return 1 ;; 55 | "Module version requirements have changed") return 1 ;; 56 | "Module not installed") return 1 ;; 57 | "Could not load plugin") return 1 ;; 58 | "Missing required provider") return 1 ;; 59 | *"there is no package for"*"cached in .terraform/providers") return 1 ;; 60 | esac 61 | done < <(jq -rc '.diagnostics[]' <<< "$validate_output") 62 | 63 | return 2 # Some other error; don't retry 64 | } 65 | 66 | ####################################################################### 67 | # Unique part of `common::per_dir_hook`. The function is executed in loop 68 | # on each provided dir path. Run wrapped tool with specified arguments 69 | # 1. Check if `.terraform` dir exists and if not - run `tofu init` 70 | # 2. Run `tofu validate` 71 | # 3. If at least 1 check failed - change the exit code to non-zero 72 | # Arguments: 73 | # dir_path (string) PATH to dir relative to git repo root. 74 | # Can be used in error logging 75 | # change_dir_in_unique_part (string/false) Modifier which creates 76 | # possibilities to use non-common chdir strategies. 77 | # Availability depends on hook. 78 | # args (array) arguments that configure wrapped tool behavior 79 | # Outputs: 80 | # If failed - print out hook checks status 81 | ####################################################################### 82 | function per_dir_hook_unique_part { 83 | local -r dir_path="$1" 84 | # shellcheck disable=SC2034 # Unused var. 85 | local -r change_dir_in_unique_part="$2" 86 | shift 2 87 | local -a -r args=("$@") 88 | 89 | local exit_code 90 | # 91 | # Get hook settings 92 | # 93 | local retry_once_with_cleanup 94 | 95 | IFS=";" read -r -a configs <<< "${HOOK_CONFIG[*]}" 96 | 97 | for c in "${configs[@]}"; do 98 | 99 | IFS="=" read -r -a config <<< "$c" 100 | key=${config[0]} 101 | value=${config[1]} 102 | 103 | case $key in 104 | --retry-once-with-cleanup) 105 | if [ "$retry_once_with_cleanup" ]; then 106 | common::colorify "yellow" 'Invalid hook config. Make sure that you specify not more than one "--retry-once-with-cleanup" flag' 107 | exit 1 108 | fi 109 | retry_once_with_cleanup=$value 110 | ;; 111 | esac 112 | done 113 | 114 | # First try `tofu validate` with the hope that all deps are 115 | # pre-installed. That is needed for cases when `.terraform/modules` 116 | # or `.terraform/providers` missed AND that is expected. 117 | tofu validate "${args[@]}" &> /dev/null && { 118 | exit_code=$? 119 | return $exit_code 120 | } 121 | 122 | # In case `tofu validate` failed to execute 123 | # - check is simple `tofu init` will help 124 | common::tofu_init 'tofu validate' "$dir_path" || { 125 | exit_code=$? 126 | return $exit_code 127 | } 128 | 129 | if [ "$retry_once_with_cleanup" != "true" ]; then 130 | # tofu validate only 131 | validate_output=$(tofu validate "${args[@]}" 2>&1) 132 | exit_code=$? 133 | else 134 | # tofu validate, plus capture possible errors 135 | validate_output=$(tofu validate -json "${args[@]}" 2>&1) 136 | exit_code=$? 137 | 138 | # Match specific validation errors 139 | local -i validate_errors_matched 140 | match_validate_errors "$validate_output" 141 | validate_errors_matched=$? 142 | 143 | # Errors matched; Retry validation 144 | if [ "$validate_errors_matched" -eq 1 ]; then 145 | common::colorify "yellow" "Validation failed. Removing cached providers and modules from \"$dir_path/.terraform\" directory" 146 | # `.terraform` dir may comprise some extra files, like `environment` 147 | # which stores info about current TF workspace, so we can't just remove 148 | # `.terraform` dir completely. 149 | rm -rf .terraform/{modules,providers}/ 150 | 151 | common::colorify "yellow" "Re-validating: $dir_path" 152 | 153 | common::tofu_init 'tofu validate' "$dir_path" || { 154 | exit_code=$? 155 | return $exit_code 156 | } 157 | 158 | validate_output=$(tofu validate "${args[@]}" 2>&1) 159 | exit_code=$? 160 | fi 161 | fi 162 | 163 | if [ $exit_code -ne 0 ]; then 164 | common::colorify "red" "Validation failed: $dir_path" 165 | echo -e "$validate_output\n\n" 166 | fi 167 | 168 | # return exit code to common::per_dir_hook 169 | return $exit_code 170 | } 171 | 172 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 173 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Notes for contributors 2 | 3 | 1. Python hooks are supported now too. All you have to do is: 4 | 1. add a line to the `console_scripts` array in `entry_points` in `setup.py` 5 | 2. Put your python script in the `pre_commit_hooks` folder 6 | 7 | Enjoy the clean, valid, and documented code! 8 | 9 | * [Run and debug hooks locally](#run-and-debug-hooks-locally) 10 | * [Run hook performance test](#run-hook-performance-test) 11 | * [Run via BASH](#run-via-bash) 12 | * [Run via Docker](#run-via-docker) 13 | * [Check results](#check-results) 14 | * [Cleanup](#cleanup) 15 | * [Add new hook](#add-new-hook) 16 | * [Before write code](#before-write-code) 17 | * [Prepare basic documentation](#prepare-basic-documentation) 18 | * [Add code](#add-code) 19 | * [Finish with the documentation](#finish-with-the-documentation) 20 | 21 | ## Run and debug hooks locally 22 | 23 | ```bash 24 | pre-commit try-repo {-a} /path/to/local/pre-commit-opentofu/repo {hook_name} 25 | ``` 26 | 27 | I.e. 28 | 29 | ```bash 30 | pre-commit try-repo /mnt/c/Users/tf/pre-commit-opentofu tofu_fmt # Run only `tofu_fmt` check 31 | pre-commit try-repo -a ~/pre-commit-opentofu # run all existing checks from repo 32 | ``` 33 | 34 | Running `pre-commit` with `try-repo` ignores all arguments specified in `.pre-commit-config.yaml`. 35 | 36 | If you need to test hook with arguments, follow [pre-commit doc](https://pre-commit.com/#arguments-pattern-in-hooks) to test hooks. 37 | 38 | For example, to test that the [`tofu_fmt`](../README.md#tofu_fmt) hook works fine with arguments: 39 | 40 | ```bash 41 | /tmp/pre-commit-opentofu/tofu_fmt.sh --args=-diff --args=-write=false test-dir/main.tf test-dir/vars.tf 42 | ``` 43 | 44 | ## Run hook performance test 45 | 46 | To check is your improvement not violate performance, we have dummy execution time tests. 47 | 48 | Script accept next options: 49 | 50 | | # | Name | Example value | Description | 51 | | --- | ---------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------- | 52 | | 1 | `TEST_NUM` | `200` | How many times need repeat test | 53 | | 2 | `TEST_COMMAND` | `'pre-commit try-repo -a /tmp/159/pre-commit-opentofu tofu_tfsec'` | Valid pre-commit command | 54 | | 3 | `TEST_DIR` | `'/tmp/infrastructure'` | Dir on what you run tests. | 55 | | 4 | `TEST_DESCRIPTION` | ```'`tofu_tfsec` PR #123:'``` | Text that you'd like to see in result | 56 | | 5 | `RAW_TEST_`
`RESULTS_FILE_NAME` | `tofu_tfsec_pr123` | (Temporary) File where all test data will be stored. | 57 | 58 | 59 | > **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. 60 | > 61 | > 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. 62 | 63 | ### Run via BASH 64 | 65 | ```bash 66 | # Install deps 67 | sudo apt install -y datamash 68 | # Run tests 69 | ./hooks_performance_test.sh 200 'pre-commit try-repo -a /tmp/159/pre-commit-opentofu tofu_tfsec' '/tmp/infrastructure' '`tofu_tfsec` v1.51.0:' 'tofu_tfsec_pr159' 70 | ``` 71 | 72 | ### Run via Docker 73 | 74 | ```bash 75 | # Build `pre-commit-opentofu` image 76 | docker build -t pre-commit-opentofu --build-arg INSTALL_ALL=true . 77 | # Build test image 78 | docker build -t pre-commit-tests tests/ 79 | # Run 80 | TEST_NUM=1 81 | TEST_DIR='/tmp/infrastructure' 82 | PRE_COMMIT_DIR="$(pwd)" 83 | TEST_COMMAND='pre-commit try-repo -a /pct tofu_tfsec' 84 | TEST_DESCRIPTION='`tofu_tfsec` v1.51.0:' 85 | RAW_TEST_RESULTS_FILE_NAME='tofu_tfsec_pr159' 86 | 87 | docker run -v "$PRE_COMMIT_DIR:/pct:rw" -v "$TEST_DIR:/lint:ro" pre-commit-tests \ 88 | $TEST_NUM "$TEST_COMMAND" '/lint' "$RAW_TEST_RESULTS_FILE_NAME" "$RAW_TEST_RESULTS_FILE_NAME" 89 | ``` 90 | 91 | ### Check results 92 | 93 | Results will be located at `./test/results` dir. 94 | 95 | ### Cleanup 96 | 97 | ```bash 98 | sudo rm -rf tests/results 99 | ``` 100 | 101 | ## Add new hook 102 | 103 | You can use [this PR](https://github.com/tofuutils/pre-commit-opentofu/pull/1) as an example. 104 | 105 | ### Before write code 106 | 107 | 1. Try to figure out future hook usage. 108 | 2. Confirm the concept with one of the following people: [Alexander Sharov](https://github.com/kvendingoldo), [Nikolay Mishin](https://github.com/Nmishin), [Anastasiia Kozlova](https://github.com/anastasiiakozlova245). 109 | 110 | 111 | ### Prepare basic documentation 112 | 113 | 1. Identify and describe dependencies in [Install dependencies](../README.md#1-install-dependencies) and [Available Hooks](../README.md#available-hooks) sections 114 | 115 | ### Add code 116 | 117 | 1. Based on prev. block, add hook dependencies installation to [Dockerfile](../Dockerfile). 118 | Check that works: 119 | * `docker build -t pre-commit --build-arg INSTALL_ALL=true .` 120 | * `docker build -t pre-commit --build-arg _VERSION=latest .` 121 | * `docker build -t pre-commit --build-arg _VERSION=<1.2.3> .` 122 | 2. Add Docker structure tests to [`.github/.container-structure-test-config.yaml`](.container-structure-test-config.yaml) 123 | 3. Add new hook to [`.pre-commit-hooks.yaml`](../.pre-commit-hooks.yaml) 124 | 4. Create hook file. Don't forget to make it executable via `chmod +x /path/to/hook/file`. 125 | 5. Test hook. How to do it is described in [Run and debug hooks locally](#run-and-debug-hooks-locally) section. 126 | 6. Test hook one more time. 127 | 1. Push commit with hook file to GitHub 128 | 2. Grab SHA hash of the commit 129 | 3. Test hook using `.pre-commit-config.yaml`: 130 | 131 | ```yaml 132 | repos: 133 | - repo: https://github.com/tofuutils/pre-commit-opentofu # Your repo 134 | rev: 3d76da3885e6a33d59527eff3a57d246dfb66620 # Your commit SHA 135 | hooks: 136 | - id: terraform_docs # New hook name 137 | args: 138 | - --args=--config=.terraform-docs.yml # Some args that you'd like to test 139 | ``` 140 | 141 | ### Finish with the documentation 142 | 143 | 1. Add hook description to [Available Hooks](../README.md#available-hooks). 144 | 2. Create and populate a new hook section in [Hooks usage notes and examples](../README.md#hooks-usage-notes-and-examples). 145 | -------------------------------------------------------------------------------- /hooks/tofu_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | # set up default insertion markers. These will be changed to the markers used by 11 | # terraform-docs if the hook config contains `--use-standard-markers=true` 12 | insertion_marker_begin="" 13 | insertion_marker_end="" 14 | 15 | # these are the standard insertion markers used by terraform-docs 16 | readonly standard_insertion_marker_begin="" 17 | readonly standard_insertion_marker_end="" 18 | 19 | function main { 20 | common::initialize "$SCRIPT_DIR" 21 | common::parse_cmdline "$@" 22 | common::export_provided_env_vars "${ENV_VARS[@]}" 23 | common::parse_and_export_env_vars 24 | # Support for setting relative PATH to .terraform-docs.yml config. 25 | for i in "${!ARGS[@]}"; do 26 | ARGS[i]=${ARGS[i]/--config=/--config=$(pwd)\/} 27 | done 28 | # shellcheck disable=SC2153 # False positive 29 | tofu_check_ "${HOOK_CONFIG[*]}" "${ARGS[*]}" "${FILES[@]}" 30 | } 31 | 32 | ####################################################################### 33 | # TODO Function which checks `terraform-docs` exists 34 | # Arguments: 35 | # hook_config (string with array) arguments that configure hook behavior 36 | # args (string with array) arguments that configure wrapped tool behavior 37 | # files (array) filenames to check 38 | ####################################################################### 39 | function tofu_check_ { 40 | local -r hook_config="$1" 41 | local -r args="$2" 42 | shift 2 43 | local -a -r files=("$@") 44 | 45 | # Get hook settings 46 | IFS=";" read -r -a configs <<< "$hook_config" 47 | 48 | if [[ ! $(command -v terraform-docs) ]]; then 49 | echo "ERROR: terraform-docs is required by tofu_docs pre-commit hook but is not installed or in the system's PATH." 50 | exit 1 51 | fi 52 | 53 | tofu_docs "${configs[*]}" "${args[*]}" "${files[@]}" 54 | } 55 | 56 | ####################################################################### 57 | # Wrapper around `terraform-docs` tool that check and change/create 58 | # (depends on provided hook_config) OpenTofu documentation in 59 | # markdown format 60 | # Arguments: 61 | # hook_config (string with array) arguments that configure hook behavior 62 | # args (string with array) arguments that configure wrapped tool behavior 63 | # files (array) filenames to check 64 | ####################################################################### 65 | function tofu_docs { 66 | local -r hook_config="$1" 67 | local -r args="$2" 68 | shift 2 69 | local -a -r files=("$@") 70 | 71 | local -a paths 72 | 73 | local index=0 74 | local file_with_path 75 | for file_with_path in "${files[@]}"; do 76 | file_with_path="${file_with_path// /__REPLACED__SPACE__}" 77 | 78 | paths[index]=$(dirname "$file_with_path") 79 | 80 | ((index += 1)) 81 | done 82 | 83 | local -r tmp_file=$(mktemp) 84 | 85 | # 86 | # Get hook settings 87 | # 88 | local text_file="README.md" 89 | local add_to_existing=false 90 | local create_if_not_exist=false 91 | local use_standard_markers=false 92 | 93 | read -r -a configs <<< "$hook_config" 94 | 95 | for c in "${configs[@]}"; do 96 | 97 | IFS="=" read -r -a config <<< "$c" 98 | key=${config[0]} 99 | value=${config[1]} 100 | 101 | case $key in 102 | --path-to-file) 103 | text_file=$value 104 | ;; 105 | --add-to-existing-file) 106 | add_to_existing=$value 107 | ;; 108 | --create-file-if-not-exist) 109 | create_if_not_exist=$value 110 | ;; 111 | --use-standard-markers) 112 | use_standard_markers=$value 113 | ;; 114 | esac 115 | done 116 | 117 | if [ "$use_standard_markers" = true ]; then 118 | # update the insertion markers to those used by terraform-docs 119 | insertion_marker_begin="$standard_insertion_marker_begin" 120 | insertion_marker_end="$standard_insertion_marker_end" 121 | fi 122 | 123 | # Override formatter if no config file set 124 | if [[ "$args" != *"--config"* ]]; then 125 | local tf_docs_formatter="md" 126 | 127 | # Suppress terraform_docs color 128 | else 129 | 130 | local config_file=${args#*--config} 131 | config_file=${config_file#*=} 132 | config_file=${config_file% *} 133 | 134 | local config_file_no_color 135 | config_file_no_color="$config_file$(date +%s).yml" 136 | 137 | if [ "$PRE_COMMIT_COLOR" = "never" ] && 138 | [[ $(grep -e '^formatter:' "$config_file") == *"pretty"* ]] && 139 | [[ $(grep ' color: ' "$config_file") != *"false"* ]]; then 140 | 141 | cp "$config_file" "$config_file_no_color" 142 | echo -e "settings:\n color: false" >> "$config_file_no_color" 143 | args=${args/$config_file/$config_file_no_color} 144 | fi 145 | fi 146 | 147 | local dir_path 148 | for dir_path in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do 149 | dir_path="${dir_path//__REPLACED__SPACE__/ }" 150 | 151 | pushd "$dir_path" > /dev/null || continue 152 | 153 | # 154 | # Create file if it not exist and `--create-if-not-exist=true` provided 155 | # 156 | if $create_if_not_exist && [[ ! -f "$text_file" ]]; then 157 | dir_have_tf_files="$( 158 | find . -maxdepth 1 -type f | sed 's|.*\.||' | sort -u | grep -oE '^tofu|^tf$|^tfvars$' || 159 | exit 0 160 | )" 161 | 162 | # if no TF files - skip dir 163 | [ ! "$dir_have_tf_files" ] && popd > /dev/null && continue 164 | 165 | dir="$(dirname "$text_file")" 166 | 167 | mkdir -p "$dir" 168 | 169 | # Use of insertion markers, where there is no existing README file 170 | { 171 | echo -e "# ${PWD##*/}\n" 172 | echo "$insertion_marker_begin" 173 | echo "$insertion_marker_end" 174 | } >> "$text_file" 175 | fi 176 | 177 | # If file still not exist - skip dir 178 | [[ ! -f "$text_file" ]] && popd > /dev/null && continue 179 | 180 | # 181 | # If `--add-to-existing-file=true` set, check is in file exist "hook markers", 182 | # and if not - append "hook markers" to the end of file. 183 | # 184 | if $add_to_existing; then 185 | HAVE_MARKER=$(grep -o "$insertion_marker_begin" "$text_file" || exit 0) 186 | 187 | if [ ! "$HAVE_MARKER" ]; then 188 | # Use of insertion markers, where addToExisting=true, with no markers in the existing file 189 | echo "$insertion_marker_begin" >> "$text_file" 190 | echo "$insertion_marker_end" >> "$text_file" 191 | fi 192 | fi 193 | 194 | # shellcheck disable=SC2086 195 | terraform-docs $tf_docs_formatter $args ./ > "$tmp_file" 196 | 197 | # Use of insertion markers to insert the terraform-docs output between the markers 198 | # Replace content between markers with the placeholder - https://stackoverflow.com/questions/1212799/how-do-i-extract-lines-between-two-line-delimiters-in-perl#1212834 199 | perl_expression="if (/$insertion_marker_begin/../$insertion_marker_end/) { print \$_ if /$insertion_marker_begin/; print \"I_WANT_TO_BE_REPLACED\\n\$_\" if /$insertion_marker_end/;} else { print \$_ }" 200 | perl -i -ne "$perl_expression" "$text_file" 201 | 202 | # Replace placeholder with the content of the file 203 | perl -i -e 'open(F, "'"$tmp_file"'"); $f = join "", ; while(<>){if (/I_WANT_TO_BE_REPLACED/) {print $f} else {print $_};}' "$text_file" 204 | 205 | rm -f "$tmp_file" 206 | 207 | popd > /dev/null 208 | done 209 | 210 | # Cleanup 211 | rm -f "$config_file_no_color" 212 | } 213 | 214 | [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" 215 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG TAG=3.12.0-alpine3.17@sha256:fc34b07ec97a4f288bc17083d288374a803dd59800399c76b977016c9fe5b8f2 2 | FROM python:${TAG} as builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /bin_dir 7 | 8 | RUN apk add --no-cache \ 9 | # Builder deps 10 | curl=~8 && \ 11 | # Upgrade packages for be able get latest Checkov 12 | python3 -m pip install --no-cache-dir --upgrade \ 13 | pip \ 14 | setuptools 15 | 16 | ARG PRE_COMMIT_VERSION=${PRE_COMMIT_VERSION:-latest} 17 | ARG TOFU_VERSION=${TOFU_VERSION:-1.9.0} 18 | 19 | # Install pre-commit 20 | RUN [ ${PRE_COMMIT_VERSION} = "latest" ] && pip3 install --no-cache-dir pre-commit \ 21 | || pip3 install --no-cache-dir pre-commit==${PRE_COMMIT_VERSION} 22 | 23 | RUN curl -LO https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip \ 24 | && curl -LO https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_SHA256SUMS \ 25 | && [ $(sha256sum "tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip" | cut -f 1 -d ' ') = "$(grep "tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip" tofu_*_SHA256SUMS | cut -f 1 -d ' ')" ] \ 26 | && unzip tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip -d /usr/bin/ \ 27 | && rm "tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip" \ 28 | && rm "tofu_${TOFU_VERSION}_SHA256SUMS" 29 | 30 | # 31 | # Install tools 32 | # 33 | ARG CHECKOV_VERSION=${CHECKOV_VERSION:-false} 34 | ARG INFRACOST_VERSION=${INFRACOST_VERSION:-false} 35 | ARG TERRAFORM_DOCS_VERSION=${TERRAFORM_DOCS_VERSION:-false} 36 | ARG TERRAGRUNT_VERSION=${TERRAGRUNT_VERSION:-false} 37 | ARG TERRASCAN_VERSION=${TERRASCAN_VERSION:-false} 38 | ARG TFLINT_VERSION=${TFLINT_VERSION:-false} 39 | ARG TFSEC_VERSION=${TFSEC_VERSION:-false} 40 | ARG TRIVY_VERSION=${TRIVY_VERSION:-false} 41 | ARG TFUPDATE_VERSION=${TFUPDATE_VERSION:-false} 42 | ARG HCLEDIT_VERSION=${HCLEDIT_VERSION:-false} 43 | 44 | 45 | # Tricky thing to install all tools by set only one arg. 46 | # In RUN command below used `. /.env` <- this is sourcing vars that 47 | # specified in step below 48 | ARG INSTALL_ALL=${INSTALL_ALL:-false} 49 | RUN if [ "$INSTALL_ALL" != "false" ]; then \ 50 | echo "export CHECKOV_VERSION=latest" >> /.env && \ 51 | echo "export INFRACOST_VERSION=latest" >> /.env && \ 52 | echo "export TERRAFORM_DOCS_VERSION=latest" >> /.env && \ 53 | echo "export TERRAGRUNT_VERSION=latest" >> /.env && \ 54 | echo "export TERRASCAN_VERSION=latest" >> /.env && \ 55 | echo "export TFLINT_VERSION=latest" >> /.env && \ 56 | echo "export TFSEC_VERSION=latest" >> /.env && \ 57 | echo "export TRIVY_VERSION=latest" >> /.env && \ 58 | echo "export TFUPDATE_VERSION=latest" >> /.env && \ 59 | echo "export HCLEDIT_VERSION=latest" >> /.env \ 60 | ; else \ 61 | touch /.env \ 62 | ; fi 63 | 64 | 65 | # Checkov 66 | RUN . /.env && \ 67 | if [ "$CHECKOV_VERSION" != "false" ]; then \ 68 | ( \ 69 | apk add --no-cache gcc=~12 libffi-dev=~3 musl-dev=~1; \ 70 | [ "$CHECKOV_VERSION" = "latest" ] && pip3 install --no-cache-dir checkov \ 71 | || pip3 install --no-cache-dir checkov==${CHECKOV_VERSION}; \ 72 | apk del gcc libffi-dev musl-dev \ 73 | ) \ 74 | ; fi 75 | 76 | # infracost 77 | RUN . /.env && \ 78 | if [ "$INFRACOST_VERSION" != "false" ]; then \ 79 | ( \ 80 | INFRACOST_RELEASES="https://api.github.com/repos/infracost/infracost/releases" && \ 81 | [ "$INFRACOST_VERSION" = "latest" ] && curl -L "$(curl -s ${INFRACOST_RELEASES}/latest | grep -o -E -m 1 "https://.+?-${TARGETOS}-${TARGETARCH}.tar.gz")" > infracost.tgz \ 82 | || curl -L "$(curl -s ${INFRACOST_RELEASES} | grep -o -E "https://.+?v${INFRACOST_VERSION}/infracost-${TARGETOS}-${TARGETARCH}.tar.gz")" > infracost.tgz \ 83 | ) && tar -xzf infracost.tgz && rm infracost.tgz && mv infracost-${TARGETOS}-${TARGETARCH} infracost \ 84 | ; fi 85 | 86 | # Terraform docs 87 | RUN . /.env && \ 88 | if [ "$TERRAFORM_DOCS_VERSION" != "false" ]; then \ 89 | ( \ 90 | TERRAFORM_DOCS_RELEASES="https://api.github.com/repos/terraform-docs/terraform-docs/releases" && \ 91 | [ "$TERRAFORM_DOCS_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRAFORM_DOCS_RELEASES}/latest | grep -o -E -m 1 "https://.+?-${TARGETOS}-${TARGETARCH}.tar.gz")" > terraform-docs.tgz \ 92 | || curl -L "$(curl -s ${TERRAFORM_DOCS_RELEASES} | grep -o -E "https://.+?v${TERRAFORM_DOCS_VERSION}-${TARGETOS}-${TARGETARCH}.tar.gz")" > terraform-docs.tgz \ 93 | ) && tar -xzf terraform-docs.tgz terraform-docs && rm terraform-docs.tgz && chmod +x terraform-docs \ 94 | ; fi 95 | 96 | # Terragrunt 97 | RUN . /.env \ 98 | && if [ "$TERRAGRUNT_VERSION" != "false" ]; then \ 99 | ( \ 100 | TERRAGRUNT_RELEASES="https://api.github.com/repos/gruntwork-io/terragrunt/releases" && \ 101 | [ "$TERRAGRUNT_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRAGRUNT_RELEASES}/latest | grep -o -E -m 1 "https://.+?/terragrunt_${TARGETOS}_${TARGETARCH}")" > terragrunt \ 102 | || curl -L "$(curl -s ${TERRAGRUNT_RELEASES} | grep -o -E -m 1 "https://.+?v${TERRAGRUNT_VERSION}/terragrunt_${TARGETOS}_${TARGETARCH}")" > terragrunt \ 103 | ) && chmod +x terragrunt \ 104 | ; fi 105 | 106 | 107 | # Terrascan 108 | RUN . /.env && \ 109 | if [ "$TERRASCAN_VERSION" != "false" ]; then \ 110 | if [ "$TARGETARCH" != "amd64" ]; then ARCH="$TARGETARCH"; else ARCH="x86_64"; fi; \ 111 | # Convert the first letter to Uppercase 112 | OS="$(echo ${TARGETOS} | cut -c1 | tr '[:lower:]' '[:upper:]' | xargs echo -n; echo ${TARGETOS} | cut -c2-)"; \ 113 | ( \ 114 | TERRASCAN_RELEASES="https://api.github.com/repos/tenable/terrascan/releases" && \ 115 | [ "$TERRASCAN_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRASCAN_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${OS}_${ARCH}.tar.gz")" > terrascan.tar.gz \ 116 | || curl -L "$(curl -s ${TERRASCAN_RELEASES} | grep -o -E "https://.+?${TERRASCAN_VERSION}_${OS}_${ARCH}.tar.gz")" > terrascan.tar.gz \ 117 | ) && tar -xzf terrascan.tar.gz terrascan && rm terrascan.tar.gz && \ 118 | ./terrascan init \ 119 | ; fi 120 | 121 | # TFLint 122 | RUN . /.env && \ 123 | if [ "$TFLINT_VERSION" != "false" ]; then \ 124 | ( \ 125 | TFLINT_RELEASES="https://api.github.com/repos/terraform-linters/tflint/releases" && \ 126 | [ "$TFLINT_VERSION" = "latest" ] && curl -L "$(curl -s ${TFLINT_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${TARGETOS}_${TARGETARCH}.zip")" > tflint.zip \ 127 | || curl -L "$(curl -s ${TFLINT_RELEASES} | grep -o -E "https://.+?/v${TFLINT_VERSION}/tflint_${TARGETOS}_${TARGETARCH}.zip")" > tflint.zip \ 128 | ) && unzip tflint.zip && rm tflint.zip \ 129 | ; fi 130 | 131 | # TFSec 132 | RUN . /.env && \ 133 | if [ "$TFSEC_VERSION" != "false" ]; then \ 134 | ( \ 135 | TFSEC_RELEASES="https://api.github.com/repos/aquasecurity/tfsec/releases" && \ 136 | [ "$TFSEC_VERSION" = "latest" ] && curl -L "$(curl -s ${TFSEC_RELEASES}/latest | grep -o -E -m 1 "https://.+?/tfsec-${TARGETOS}-${TARGETARCH}")" > tfsec \ 137 | || curl -L "$(curl -s ${TFSEC_RELEASES} | grep -o -E -m 1 "https://.+?v${TFSEC_VERSION}/tfsec-${TARGETOS}-${TARGETARCH}")" > tfsec \ 138 | ) && chmod +x tfsec \ 139 | ; fi 140 | 141 | # Trivy 142 | RUN . /.env && \ 143 | if [ "$TRIVY_VERSION" != "false" ]; then \ 144 | if [ "$TARGETARCH" != "amd64" ]; then ARCH="$TARGETARCH"; else ARCH="64bit"; fi; \ 145 | ( \ 146 | TRIVY_RELEASES="https://api.github.com/repos/aquasecurity/trivy/releases" && \ 147 | [ "$TRIVY_VERSION" = "latest" ] && curl -L "$(curl -s ${TRIVY_RELEASES}/latest | grep -o -E -i -m 1 "https://.+?/trivy_.+?_${TARGETOS}-${ARCH}.tar.gz")" > trivy.tar.gz \ 148 | || curl -L "$(curl -s ${TRIVY_RELEASES} | grep -o -E -i -m 1 "https://.+?/v${TRIVY_VERSION}/trivy_.+?_${TARGETOS}-${ARCH}.tar.gz")" > trivy.tar.gz \ 149 | ) && tar -xzf trivy.tar.gz trivy && rm trivy.tar.gz \ 150 | ; fi 151 | 152 | # TFUpdate 153 | RUN . /.env && \ 154 | if [ "$TFUPDATE_VERSION" != "false" ]; then \ 155 | ( \ 156 | TFUPDATE_RELEASES="https://api.github.com/repos/minamijoyo/tfupdate/releases" && \ 157 | [ "$TFUPDATE_VERSION" = "latest" ] && curl -L "$(curl -s ${TFUPDATE_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${TARGETOS}_${TARGETARCH}.tar.gz")" > tfupdate.tgz \ 158 | || curl -L "$(curl -s ${TFUPDATE_RELEASES} | grep -o -E -m 1 "https://.+?${TFUPDATE_VERSION}_${TARGETOS}_${TARGETARCH}.tar.gz")" > tfupdate.tgz \ 159 | ) && tar -xzf tfupdate.tgz tfupdate && rm tfupdate.tgz \ 160 | ; fi 161 | 162 | # hcledit 163 | RUN . /.env && \ 164 | if [ "$HCLEDIT_VERSION" != "false" ]; then \ 165 | ( \ 166 | HCLEDIT_RELEASES="https://api.github.com/repos/minamijoyo/hcledit/releases" && \ 167 | [ "$HCLEDIT_VERSION" = "latest" ] && curl -L "$(curl -s ${HCLEDIT_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${TARGETOS}_${TARGETARCH}.tar.gz")" > hcledit.tgz \ 168 | || curl -L "$(curl -s ${HCLEDIT_RELEASES} | grep -o -E -m 1 "https://.+?${HCLEDIT_VERSION}_${TARGETOS}_${TARGETARCH}.tar.gz")" > hcledit.tgz \ 169 | ) && tar -xzf hcledit.tgz hcledit && rm hcledit.tgz \ 170 | ; fi 171 | 172 | # Checking binaries versions and write it to debug file 173 | RUN . /.env && \ 174 | F=tools_versions_info && \ 175 | pre-commit --version >> $F && \ 176 | ./terraform --version | head -n 1 >> $F && \ 177 | (if [ "$CHECKOV_VERSION" != "false" ]; then echo "checkov $(checkov --version)" >> $F; else echo "checkov SKIPPED" >> $F ; fi) && \ 178 | (if [ "$INFRACOST_VERSION" != "false" ]; then echo "$(./infracost --version)" >> $F; else echo "infracost SKIPPED" >> $F ; fi) && \ 179 | (if [ "$TERRAFORM_DOCS_VERSION" != "false" ]; then ./terraform-docs --version >> $F; else echo "terraform-docs SKIPPED" >> $F ; fi) && \ 180 | (if [ "$TERRAGRUNT_VERSION" != "false" ]; then ./terragrunt --version >> $F; else echo "terragrunt SKIPPED" >> $F ; fi) && \ 181 | (if [ "$TERRASCAN_VERSION" != "false" ]; then echo "terrascan $(./terrascan version)" >> $F; else echo "terrascan SKIPPED" >> $F ; fi) && \ 182 | (if [ "$TFLINT_VERSION" != "false" ]; then ./tflint --version >> $F; else echo "tflint SKIPPED" >> $F ; fi) && \ 183 | (if [ "$TFSEC_VERSION" != "false" ]; then echo "tfsec $(./tfsec --version)" >> $F; else echo "tfsec SKIPPED" >> $F ; fi) && \ 184 | (if [ "$TFUPDATE_VERSION" != "false" ]; then echo "tfupdate $(./tfupdate --version)" >> $F; else echo "tfupdate SKIPPED" >> $F ; fi) && \ 185 | (if [ "$HCLEDIT_VERSION" != "false" ]; then echo "hcledit $(./hcledit version)" >> $F; else echo "hcledit SKIPPED" >> $F ; fi) && \ 186 | echo -e "\n\n" && cat $F && echo -e "\n\n" 187 | 188 | 189 | 190 | FROM python:${TAG} 191 | 192 | RUN apk add --no-cache \ 193 | # pre-commit deps 194 | git=~2 \ 195 | # All hooks deps 196 | bash=~5 \ 197 | # pre-commit-hooks deps: https://github.com/pre-commit/pre-commit-hooks 198 | musl-dev=~1 \ 199 | gcc=~12 \ 200 | # entrypoint wrapper deps 201 | su-exec=~0.2 \ 202 | # ssh-client for external private module in ssh 203 | openssh-client=~9 204 | 205 | # Copy tools 206 | COPY --from=builder \ 207 | # Needed for all hooks 208 | /usr/local/bin/pre-commit \ 209 | # Hooks and terraform binaries 210 | /bin_dir/ \ 211 | /usr/bin/tofu \ 212 | /usr/local/bin/checkov* \ 213 | /usr/bin/ 214 | # Copy pre-commit packages 215 | COPY --from=builder /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/ 216 | # Copy terrascan policies 217 | COPY --from=builder /root/ /root/ 218 | 219 | # Install hooks extra deps 220 | RUN if [ "$(grep -o '^terraform-docs SKIPPED$' /usr/bin/tools_versions_info)" = "" ]; then \ 221 | apk add --no-cache perl=~5 \ 222 | ; fi && \ 223 | if [ "$(grep -o '^infracost SKIPPED$' /usr/bin/tools_versions_info)" = "" ]; then \ 224 | apk add --no-cache jq=~1 \ 225 | ; fi && \ 226 | # Fix git runtime fatal: 227 | # unsafe repository ('/lint' is owned by someone else) 228 | git config --global --add safe.directory /lint 229 | 230 | COPY tools/entrypoint.sh /entrypoint.sh 231 | 232 | ENV PRE_COMMIT_COLOR=${PRE_COMMIT_COLOR:-always} 233 | 234 | ENV INFRACOST_API_KEY=${INFRACOST_API_KEY:-} 235 | ENV INFRACOST_SKIP_UPDATE_CHECK=${INFRACOST_SKIP_UPDATE_CHECK:-false} 236 | 237 | ENTRYPOINT [ "/entrypoint.sh" ] 238 | 239 | -------------------------------------------------------------------------------- /hooks/_common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # Hook ID, based on hook filename. 5 | # Hook filename MUST BE same with `- id` in .pre-commit-hooks.yaml file 6 | # shellcheck disable=SC2034 # Unused var. 7 | HOOK_ID=${0##*/} 8 | readonly HOOK_ID=${HOOK_ID%%.*} 9 | 10 | ####################################################################### 11 | # Init arguments parser 12 | # Arguments: 13 | # script_dir - absolute path to hook dir location 14 | ####################################################################### 15 | function common::initialize { 16 | local -r script_dir=$1 17 | # source getopt function 18 | # shellcheck source=../lib_getopt 19 | . "$script_dir/../lib_getopt" 20 | } 21 | 22 | ####################################################################### 23 | # Parse args and filenames passed to script and populate respective 24 | # global variables with appropriate values 25 | # Globals (init and populate): 26 | # ARGS (array) arguments that configure wrapped tool behavior 27 | # HOOK_CONFIG (array) arguments that configure hook behavior 28 | # TF_INIT_ARGS (array) arguments for `tofu init` command 29 | # ENV_VARS (array) environment variables will be available 30 | # for all 3rd-party tools executed by a hook. 31 | # FILES (array) filenames to check 32 | # Arguments: 33 | # $@ (array) all specified in `hooks.[].args` in 34 | # `.pre-commit-config.yaml` and filenames. 35 | ####################################################################### 36 | function common::parse_cmdline { 37 | # common global arrays. 38 | # Populated via `common::parse_cmdline` and can be used inside hooks' functions 39 | ARGS=() HOOK_CONFIG=() FILES=() 40 | # Used inside `common::tofu_init` function 41 | TF_INIT_ARGS=() 42 | # Used inside `common::export_provided_env_vars` function 43 | ENV_VARS=() 44 | 45 | local argv 46 | # TODO: Planned breaking change: remove `init-args`, `envs` as not self-descriptive 47 | argv=$(getopt -o a:,h:,i:,e: --long args:,hook-config:,init-args:,tf-init-args:,envs:,env-vars: -- "$@") || return 48 | eval "set -- $argv" 49 | 50 | for argv; do 51 | case $argv in 52 | -a | --args) 53 | shift 54 | # `argv` is an string from array with content like: 55 | # ('provider aws' '--version "> 0.14"' '--ignore-path "some/path"') 56 | # where each element is the value of each `--args` from hook config. 57 | # `echo` prints contents of `argv` as an expanded string 58 | # `xargs` passes expanded string to `printf` 59 | # `printf` which splits it into NUL-separated elements, 60 | # NUL-separated elements read by `read` using empty separator 61 | # (`-d ''` or `-d $'\0'`) 62 | # into an `ARGS` array 63 | 64 | # This allows to "rebuild" initial `args` array of sort of grouped elements 65 | # into a proper array, where each element is a standalone array slice 66 | # with quoted elements being treated as a standalone slice of array as well. 67 | while read -r -d '' ARG; do 68 | ARGS+=("$ARG") 69 | done < <(echo "$1" | xargs printf '%s\0') 70 | shift 71 | ;; 72 | -h | --hook-config) 73 | shift 74 | HOOK_CONFIG+=("$1;") 75 | shift 76 | ;; 77 | # TODO: Planned breaking change: remove `--init-args` as not self-descriptive 78 | -i | --init-args | --tf-init-args) 79 | shift 80 | TF_INIT_ARGS+=("$1") 81 | shift 82 | ;; 83 | # TODO: Planned breaking change: remove `--envs` as not self-descriptive 84 | -e | --envs | --env-vars) 85 | shift 86 | ENV_VARS+=("$1") 87 | shift 88 | ;; 89 | --) 90 | shift 91 | # shellcheck disable=SC2034 # Variable is used 92 | FILES=("$@") 93 | break 94 | ;; 95 | esac 96 | done 97 | } 98 | 99 | ####################################################################### 100 | # Expand environment variables definition into their values in '--args'. 101 | # Support expansion only for ${ENV_VAR} vars, not $ENV_VAR. 102 | # Globals (modify): 103 | # ARGS (array) arguments that configure wrapped tool behavior 104 | ####################################################################### 105 | function common::parse_and_export_env_vars { 106 | local arg_idx 107 | 108 | for arg_idx in "${!ARGS[@]}"; do 109 | local arg="${ARGS[$arg_idx]}" 110 | 111 | # Repeat until all env vars will be expanded 112 | while true; do 113 | # Check if at least 1 env var exists in `$arg` 114 | # shellcheck disable=SC2016 # '${' should not be expanded 115 | if [[ "$arg" =~ .*'${'[A-Z_][A-Z0-9_]+?'}'.* ]]; then 116 | # Get `ENV_VAR` from `.*${ENV_VAR}.*` 117 | local env_var_name=${arg#*$\{} 118 | env_var_name=${env_var_name%%\}*} 119 | local env_var_value="${!env_var_name}" 120 | # shellcheck disable=SC2016 # '${' should not be expanded 121 | common::colorify "green" 'Found ${'"$env_var_name"'} in: '"'$arg'" 122 | # Replace env var name with its value. 123 | # `$arg` will be checked in `if` conditional, `$ARGS` will be used in the next functions. 124 | # shellcheck disable=SC2016 # '${' should not be expanded 125 | arg=${arg/'${'$env_var_name'}'/$env_var_value} 126 | ARGS[$arg_idx]=$arg 127 | # shellcheck disable=SC2016 # '${' should not be expanded 128 | common::colorify "green" 'After ${'"$env_var_name"'} expansion: '"'$arg'\n" 129 | continue 130 | fi 131 | break 132 | done 133 | done 134 | } 135 | 136 | ####################################################################### 137 | # This is a workaround to improve performance when all files are passed 138 | # See: https://github.com/tofuutils/pre-commit-opentofu/issues/309 139 | # Arguments: 140 | # hook_id (string) hook ID, see `- id` for details in .pre-commit-hooks.yaml file 141 | # files (array) filenames to check 142 | # Outputs: 143 | # Return 0 if `-a|--all` arg was passed to `pre-commit` 144 | ####################################################################### 145 | function common::is_hook_run_on_whole_repo { 146 | local -r hook_id="$1" 147 | shift 148 | local -a -r files=("$@") 149 | # get directory containing `.pre-commit-hooks.yaml` file 150 | local -r root_config_dir="$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)")" 151 | # get included and excluded files from .pre-commit-hooks.yaml file 152 | local -r hook_config_block=$(sed -n "/^- id: $hook_id$/,/^$/p" "$root_config_dir/.pre-commit-hooks.yaml") 153 | local -r included_files=$(awk '$1 == "files:" {print $2; exit}' <<< "$hook_config_block") 154 | local -r excluded_files=$(awk '$1 == "exclude:" {print $2; exit}' <<< "$hook_config_block") 155 | # sorted string with the files passed to the hook by pre-commit 156 | local -r files_to_check=$(printf '%s\n' "${files[@]}" | sort | tr '\n' ' ') 157 | # git ls-files sorted string 158 | local all_files_that_can_be_checked 159 | 160 | if [ -z "$excluded_files" ]; then 161 | all_files_that_can_be_checked=$(git ls-files | sort | grep -e "$included_files" | tr '\n' ' ') 162 | else 163 | all_files_that_can_be_checked=$(git ls-files | sort | grep -e "$included_files" | grep -v -e "$excluded_files" | tr '\n' ' ') 164 | fi 165 | 166 | if [ "$files_to_check" == "$all_files_that_can_be_checked" ]; then 167 | return 0 168 | else 169 | return 1 170 | fi 171 | } 172 | 173 | ####################################################################### 174 | # Hook execution boilerplate logic which is common to hooks, that run 175 | # on per dir basis. 176 | # 1. Because hook runs on whole dir, reduce file paths to uniq dir paths 177 | # 2. Run for each dir `per_dir_hook_unique_part`, on all paths 178 | # 2.1. If at least 1 check failed - change exit code to non-zero 179 | # 3. Complete hook execution and return exit code 180 | # Arguments: 181 | # hook_id (string) hook ID, see `- id` for details in .pre-commit-hooks.yaml file 182 | # args_array_length (integer) Count of arguments in args array. 183 | # args (array) arguments that configure wrapped tool behavior 184 | # files (array) filenames to check 185 | ####################################################################### 186 | function common::per_dir_hook { 187 | local -r hook_id="$1" 188 | local -i args_array_length=$2 189 | shift 2 190 | local -a args=() 191 | # Expand args to a true array. 192 | # Based on https://stackoverflow.com/a/10953834 193 | while ((args_array_length-- > 0)); do 194 | args+=("$1") 195 | shift 196 | done 197 | # assign rest of function's positional ARGS into `files` array, 198 | # despite there's only one positional ARG left 199 | local -a -r files=("$@") 200 | 201 | # check is (optional) function defined 202 | if [ "$(type -t run_hook_on_whole_repo)" == function ] && 203 | # check is hook run via `pre-commit run --all` 204 | common::is_hook_run_on_whole_repo "$hook_id" "${files[@]}"; then 205 | run_hook_on_whole_repo "${args[@]}" 206 | exit 0 207 | fi 208 | 209 | # consume modified files passed from pre-commit so that 210 | # hook runs against only those relevant directories 211 | local index=0 212 | for file_with_path in "${files[@]}"; do 213 | file_with_path="${file_with_path// /__REPLACED__SPACE__}" 214 | 215 | dir_paths[index]=$(dirname "$file_with_path") 216 | 217 | ((index += 1)) 218 | done 219 | 220 | # Lookup hook-config for modifiers that impact common behavior 221 | local change_dir_in_unique_part=false 222 | IFS=";" read -r -a configs <<< "${HOOK_CONFIG[*]}" 223 | for c in "${configs[@]}"; do 224 | IFS="=" read -r -a config <<< "$c" 225 | key=${config[0]} 226 | value=${config[1]} 227 | 228 | case $key in 229 | --delegate-chdir) 230 | # this flag will skip pushing and popping directories 231 | # delegating the responsibility to the hooked plugin/binary 232 | if [[ ! $value || $value == true ]]; then 233 | change_dir_in_unique_part="delegate_chdir" 234 | fi 235 | ;; 236 | esac 237 | done 238 | 239 | # preserve errexit status 240 | shopt -qo errexit && ERREXIT_IS_SET=true 241 | # allow hook to continue if exit_code is greater than 0 242 | set +e 243 | local final_exit_code=0 244 | 245 | # run hook for each path 246 | for dir_path in $(echo "${dir_paths[*]}" | tr ' ' '\n' | sort -u); do 247 | dir_path="${dir_path//__REPLACED__SPACE__/ }" 248 | 249 | if [[ $change_dir_in_unique_part == false ]]; then 250 | pushd "$dir_path" > /dev/null || continue 251 | fi 252 | 253 | per_dir_hook_unique_part "$dir_path" "$change_dir_in_unique_part" "${args[@]}" 254 | 255 | local exit_code=$? 256 | if [ $exit_code -ne 0 ]; then 257 | final_exit_code=$exit_code 258 | fi 259 | 260 | if [[ $change_dir_in_unique_part == false ]]; then 261 | popd > /dev/null 262 | fi 263 | 264 | done 265 | 266 | # restore errexit if it was set before the "for" loop 267 | [[ $ERREXIT_IS_SET ]] && set -e 268 | # return the hook final exit_code 269 | exit $final_exit_code 270 | } 271 | 272 | ####################################################################### 273 | # Colorize provided string and print it out to stdout 274 | # Environment variables: 275 | # PRE_COMMIT_COLOR (string) If set to `never` - do not colorize output 276 | # Arguments: 277 | # COLOR (string) Color name that will be used to colorize 278 | # TEXT (string) 279 | # Outputs: 280 | # Print out provided text to stdout 281 | ####################################################################### 282 | function common::colorify { 283 | # shellcheck disable=SC2034 284 | local -r red="\x1b[0m\x1b[31m" 285 | # shellcheck disable=SC2034 286 | local -r green="\x1b[0m\x1b[32m" 287 | # shellcheck disable=SC2034 288 | local -r yellow="\x1b[0m\x1b[33m" 289 | # Color reset 290 | local -r RESET="\x1b[0m" 291 | 292 | # Params start # 293 | local COLOR="${!1}" 294 | local -r TEXT=$2 295 | # Params end # 296 | 297 | if [ "$PRE_COMMIT_COLOR" = "never" ]; then 298 | COLOR=$RESET 299 | fi 300 | 301 | echo -e "${COLOR}${TEXT}${RESET}" 302 | } 303 | 304 | ####################################################################### 305 | # Run tofu init command 306 | # Arguments: 307 | # command_name (string) command that will tun after successful init 308 | # dir_path (string) PATH to dir relative to git repo root. 309 | # Can be used in error logging 310 | # Globals (init and populate): 311 | # TF_INIT_ARGS (array) arguments for `tofu init` command 312 | # Outputs: 313 | # If failed - print out tofu init output 314 | ####################################################################### 315 | # TODO: v2.0: Move it inside tofu_validate.sh 316 | function common::tofu_init { 317 | local -r command_name=$1 318 | local -r dir_path=$2 319 | 320 | local exit_code=0 321 | local init_output 322 | 323 | # Suppress tofu init color 324 | if [ "$PRE_COMMIT_COLOR" = "never" ]; then 325 | TF_INIT_ARGS+=("-no-color") 326 | fi 327 | 328 | if [ ! -d .terraform/modules ] || [ ! -d .terraform/providers ]; then 329 | init_output=$(tofu init -backend=false "${TF_INIT_ARGS[@]}" 2>&1) 330 | exit_code=$? 331 | 332 | if [ $exit_code -ne 0 ]; then 333 | common::colorify "red" "'tofu init' failed, '$command_name' skipped: $dir_path" 334 | echo -e "$init_output\n\n" 335 | else 336 | common::colorify "green" "Command 'tofu init' successfully done: $dir_path" 337 | fi 338 | fi 339 | 340 | return $exit_code 341 | } 342 | 343 | ####################################################################### 344 | # Export provided K/V as environment variables. 345 | # Arguments: 346 | # env_vars (array) environment variables will be available 347 | # for all 3rd-party tools executed by a hook. 348 | ####################################################################### 349 | function common::export_provided_env_vars { 350 | local -a -r env_vars=("$@") 351 | 352 | local var 353 | local var_name 354 | local var_value 355 | 356 | for var in "${env_vars[@]}"; do 357 | var_name="${var%%=*}" 358 | var_value="${var#*=}" 359 | # shellcheck disable=SC2086 360 | export $var_name="$var_value" 361 | done 362 | } 363 | -------------------------------------------------------------------------------- /hooks/tofu_wrapper_module_for_each.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # globals variables 5 | # shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines 6 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" 7 | # shellcheck source=_common.sh 8 | . "$SCRIPT_DIR/_common.sh" 9 | 10 | function main { 11 | common::initialize "$SCRIPT_DIR" 12 | common::parse_cmdline "$@" 13 | common::export_provided_env_vars "${ENV_VARS[@]}" 14 | common::parse_and_export_env_vars 15 | # JFYI: suppress color for `hcledit` is N/A` 16 | 17 | check_dependencies 18 | 19 | # shellcheck disable=SC2153 # False positive 20 | tofu_module_wrapper_ "${ARGS[*]}" 21 | } 22 | 23 | readonly CONTENT_MAIN_TF='module "wrapper" {}' 24 | readonly CONTENT_VARIABLES_TF='variable "defaults" { 25 | description = "Map of default values which will be used for each item." 26 | type = any 27 | default = {} 28 | } 29 | 30 | variable "items" { 31 | description = "Maps of items to create a wrapper from. Values are passed through to the module." 32 | type = any 33 | default = {} 34 | }' 35 | readonly CONTENT_OUTPUTS_TF='output "wrapper" { 36 | description = "Map of outputs of a wrapper." 37 | value = module.wrapper 38 | WRAPPER_OUTPUT_SENSITIVE 39 | }' 40 | readonly CONTENT_VERSIONS_TF='terraform { 41 | required_version = ">= 1.6.0" 42 | }' 43 | # shellcheck disable=SC2016 # False positive 44 | readonly CONTENT_README='# WRAPPER_TITLE 45 | 46 | The configuration in this directory contains an implementation of a single module wrapper pattern, which allows managing several copies of a module in places where using the native OpenTofu 1.6.0+ `for_each` feature is not feasible (e.g., with Terragrunt). 47 | 48 | You may want to use a single Terragrunt configuration file to manage multiple resources without duplicating `terragrunt.hcl` files for each copy of the same module. 49 | 50 | This wrapper does not implement any extra functionality. 51 | 52 | ## Usage with Terragrunt 53 | 54 | `terragrunt.hcl`: 55 | 56 | ```hcl 57 | terraform { 58 | source = "tfr:///MODULE_REPO_ORG/MODULE_REPO_SHORTNAME/MODULE_REPO_PROVIDER//WRAPPER_PATH" 59 | # Alternative source: 60 | # source = "git::git@github.com:MODULE_REPO_ORG/terraform-MODULE_REPO_PROVIDER-MODULE_REPO_SHORTNAME.git//WRAPPER_PATH?ref=master" 61 | } 62 | 63 | inputs = { 64 | defaults = { # Default values 65 | create = true 66 | tags = { 67 | OpenTofu = "true" 68 | Environment = "dev" 69 | } 70 | } 71 | 72 | items = { 73 | my-item = { 74 | # omitted... can be any argument supported by the module 75 | } 76 | my-second-item = { 77 | # omitted... can be any argument supported by the module 78 | } 79 | # omitted... 80 | } 81 | } 82 | ``` 83 | 84 | ## Usage with OpenTofu 85 | 86 | ```hcl 87 | module "wrapper" { 88 | source = "MODULE_REPO_ORG/MODULE_REPO_SHORTNAME/MODULE_REPO_PROVIDER//WRAPPER_PATH" 89 | 90 | defaults = { # Default values 91 | create = true 92 | tags = { 93 | OpenTofu = "true" 94 | Environment = "dev" 95 | } 96 | } 97 | 98 | items = { 99 | my-item = { 100 | # omitted... can be any argument supported by the module 101 | } 102 | my-second-item = { 103 | # omitted... can be any argument supported by the module 104 | } 105 | # omitted... 106 | } 107 | } 108 | ``` 109 | 110 | ## Example: Manage multiple S3 buckets in one Terragrunt layer 111 | 112 | `eu-west-1/s3-buckets/terragrunt.hcl`: 113 | 114 | ```hcl 115 | terraform { 116 | source = "tfr:///terraform-aws-modules/s3-bucket/aws//wrappers" 117 | # Alternative source: 118 | # source = "git::git@github.com:terraform-aws-modules/terraform-aws-s3-bucket.git//wrappers?ref=master" 119 | } 120 | 121 | inputs = { 122 | defaults = { 123 | force_destroy = true 124 | 125 | attach_elb_log_delivery_policy = true 126 | attach_lb_log_delivery_policy = true 127 | attach_deny_insecure_transport_policy = true 128 | attach_require_latest_tls_policy = true 129 | } 130 | 131 | items = { 132 | bucket1 = { 133 | bucket = "my-random-bucket-1" 134 | } 135 | bucket2 = { 136 | bucket = "my-random-bucket-2" 137 | tags = { 138 | Secure = "probably" 139 | } 140 | } 141 | } 142 | } 143 | ```' 144 | 145 | function tofu_module_wrapper_ { 146 | local args 147 | read -r -a args <<< "$1" 148 | 149 | local root_dir 150 | local module_dir="" # values: empty (default), "." (just root module), or a single module (e.g. "modules/iam-user") 151 | local wrapper_dir="wrappers" 152 | local wrapper_relative_source_path="../" # From "wrappers" to root_dir. 153 | local module_repo_org 154 | local module_repo_name 155 | local module_repo_shortname 156 | local module_repo_provider 157 | local dry_run="false" 158 | local verbose="false" 159 | 160 | root_dir=$(git rev-parse --show-toplevel 2> /dev/null || pwd) 161 | module_repo_org="terraform-aws-modules" 162 | module_repo_name=${root_dir##*/} 163 | module_repo_shortname="${module_repo_name#terraform-aws-}" 164 | module_repo_provider="aws" 165 | 166 | for argv in "${args[@]}"; do 167 | 168 | local key="${argv%%=*}" 169 | local value="${argv#*=}" 170 | 171 | case "$key" in 172 | --root-dir) 173 | root_dir="$value" 174 | ;; 175 | --module-dir) 176 | module_dir="$value" 177 | ;; 178 | --wrapper-dir) 179 | wrapper_dir="$value" 180 | ;; 181 | --module-repo-org) 182 | module_repo_org="$value" 183 | ;; 184 | --module-repo-shortname) 185 | module_repo_shortname="$value" 186 | ;; 187 | --module-repo-provider) 188 | module_repo_provider="$value" 189 | ;; 190 | --dry-run) 191 | dry_run="true" 192 | ;; 193 | --verbose) 194 | verbose="true" 195 | ;; 196 | *) 197 | cat << EOF 198 | ERROR: Unrecognized argument: $key 199 | Hook ID: $HOOK_ID. 200 | Generate OpenTofu module wrapper. Available arguments: 201 | --root-dir=... - Root dir of the repository (Optional) 202 | --module-dir=... - Single module directory. Options: "." (means just root module), 203 | "modules/iam-user" (a single module), or empty (means include all 204 | submodules found in "modules/*"). Default: "${module_dir}". (Optional) 205 | --wrapper-dir=... - Directory where 'wrappers' should be saved. Default: "${wrapper_dir}". (Optional) 206 | --module-repo-org=... - Module repository organization (e.g., 'terraform-aws-modules'). (Optional) 207 | --module-repo-shortname=... - Short name of the repository (e.g., for 'terraform-aws-s3-bucket' it should be 's3-bucket'). (Optional) 208 | --module-repo-provider=... - Name of the repository provider (e.g., for 'terraform-aws-s3-bucket' it should be 'aws'). (Optional) 209 | --dry-run - Whether to run in dry mode. If not specified, wrapper files will be overwritten. 210 | --verbose - Show verbose output. 211 | 212 | Example: 213 | --module-dir=modules/object - Generate wrapper for one specific submodule. 214 | --module-dir=. - Generate wrapper for the root module. 215 | --module-repo-org=terraform-google-modules --module-repo-shortname=network --module-repo-provider=google - Generate wrappers for repository available by name "terraform-google-modules/network/google" in the OpenTofu registry and it includes all modules (root and in "modules/*"). 216 | EOF 217 | exit 1 218 | ;; 219 | esac 220 | 221 | done 222 | 223 | if [[ ! $root_dir ]]; then 224 | echo "--root-dir can't be empty. Remove it to use default value." 225 | exit 1 226 | fi 227 | 228 | if [[ ! $wrapper_dir ]]; then 229 | echo "--wrapper-dir can't be empty. Remove it to use default value." 230 | exit 1 231 | fi 232 | 233 | if [[ ! $module_repo_org ]]; then 234 | echo "--module-repo-org can't be empty. Remove it to use default value." 235 | exit 1 236 | fi 237 | 238 | if [[ ! $module_repo_shortname ]]; then 239 | echo "--module-repo-shortname can't be empty. It should be part of full repo name (eg, s3-bucket)." 240 | exit 1 241 | fi 242 | 243 | if [[ ! $module_repo_provider ]]; then 244 | echo "--module-repo-provider can't be empty. It should be name of the provider used by the module (eg, aws)." 245 | exit 1 246 | fi 247 | 248 | if [[ ! -d "$root_dir" ]]; then 249 | echo "Root directory $root_dir does not exist!" 250 | exit 1 251 | fi 252 | 253 | OLD_IFS="$IFS" 254 | IFS=$'\n' 255 | 256 | all_module_dirs=("./") 257 | # Find all modules directories if nothing was provided via "--module-dir" argument 258 | if [[ ! $module_dir ]]; then 259 | # shellcheck disable=SC2207 260 | all_module_dirs+=($(cd "${root_dir}" && find . -maxdepth 2 -path '**/modules/*' -type d -print)) 261 | else 262 | all_module_dirs=("$module_dir") 263 | fi 264 | 265 | IFS="$OLD_IFS" 266 | 267 | for module_dir in "${all_module_dirs[@]}"; do 268 | 269 | # Remove "./" from the "./modules/iam-user" or "./" 270 | module_dir="${module_dir/.\//}" 271 | 272 | full_module_dir="${root_dir}/${module_dir}" 273 | # echo "FULL=${full_module_dir}" 274 | 275 | if [[ ! -d "$full_module_dir" ]]; then 276 | echo "Module directory \"$full_module_dir\" does not exist!" 277 | exit 1 278 | fi 279 | 280 | # Remove "modules/" from "modules/iam-user" 281 | # module_name="${module_dir//modules\//}" 282 | module_name="${module_dir#modules/}" 283 | if [[ ! $module_name ]]; then 284 | wrapper_title="Wrapper for the root module" 285 | wrapper_path="${wrapper_dir}" 286 | else 287 | wrapper_title="Wrapper for module: \`${module_dir}\`" 288 | wrapper_path="${wrapper_dir}/${module_name}" 289 | fi 290 | 291 | # Wrappers will be stored in "wrappers/{module_name}" 292 | output_dir="${root_dir}/${wrapper_dir}/${module_name}" 293 | 294 | # Calculate relative depth for module source by number of slashes 295 | module_depth="${module_dir//[^\/]/}" 296 | 297 | local relative_source_path=$wrapper_relative_source_path 298 | 299 | for ((c = 0; c < ${#module_depth}; c++)); do 300 | relative_source_path+="../" 301 | done 302 | 303 | create_tmp_file_tf 304 | 305 | if [[ "$verbose" == "true" ]]; then 306 | echo "Root directory: $root_dir" 307 | echo "Module directory: $module_dir" 308 | echo "Output directory: $output_dir" 309 | echo "Temp file: $tmp_file_tf" 310 | echo 311 | fi 312 | 313 | # Read content of all OpenTofu files 314 | # shellcheck disable=SC2207 315 | all_tf_content=$(find "${full_module_dir}" -regex '.*\.(tf|tofu)' -maxdepth 1 -type f -exec cat {} +) 316 | 317 | if [[ ! $all_tf_content ]]; then 318 | common::colorify "yellow" "Skipping ${full_module_dir} because there are no *.(tf|tofu) files." 319 | continue 320 | fi 321 | 322 | # Get names of module variables in all OpenTofu files 323 | # shellcheck disable=SC2207 324 | module_vars=($(echo "$all_tf_content" | hcledit block list | { grep "^variable\." | cut -d'.' -f 2 | sort || true; })) 325 | 326 | # Get names of module outputs in all OpenTofu files 327 | # shellcheck disable=SC2207 328 | module_outputs=($(echo "$all_tf_content" | hcledit block list | { grep "^output\." | cut -d'.' -f 2 || true; })) 329 | 330 | # Get names of module providers in all OpenTofu files 331 | module_providers=$(echo "$all_tf_content" | hcledit block list | { grep "^provider\." || true; }) 332 | 333 | if [[ $module_providers ]]; then 334 | common::colorify "yellow" "Skipping ${full_module_dir} because it is a legacy module which contains its own local provider configurations and so calls to it may not use the for_each argument." 335 | break 336 | fi 337 | 338 | # Looking for sensitive output 339 | local wrapper_output_sensitive="# sensitive = false # No sensitive module output found" 340 | for module_output in "${module_outputs[@]}"; do 341 | module_output_sensitive=$(echo "$all_tf_content" | hcledit attribute get "output.${module_output}.sensitive") 342 | 343 | # At least one output is sensitive - the wrapper's output should be sensitive, too 344 | if [[ "$module_output_sensitive" == "true" ]]; then 345 | wrapper_output_sensitive="sensitive = true # At least one sensitive module output (${module_output}) found (requires OpenTofu 1.6.0+)" 346 | break 347 | fi 348 | done 349 | 350 | # Create content of temporary main.tf file 351 | hcledit attribute append module.wrapper.source "\"${relative_source_path}${module_dir}\"" --newline -f "$tmp_file_tf" -u 352 | hcledit attribute append module.wrapper.for_each var.items --newline -f "$tmp_file_tf" -u 353 | 354 | # Add newline before the first variable in a loop 355 | local newline="--newline" 356 | 357 | for module_var in "${module_vars[@]}"; do 358 | # Get default value for the variable 359 | var_default=$(echo "$all_tf_content" | hcledit attribute get "variable.${module_var}.default") 360 | 361 | # Empty default means that the variable is required 362 | if [[ ! $var_default ]]; then 363 | var_value="try(each.value.${module_var}, var.defaults.${module_var})" 364 | elif [[ "$var_default" == "{" ]]; then 365 | # BUG in hcledit ( https://github.com/minamijoyo/hcledit/issues/31 ) which breaks on inline comments 366 | # https://github.com/terraform-aws-modules/terraform-aws-security-group/blob/0bd31aa88339194efff470d3b3f58705bd008db0/rules.tf#L8 367 | # As a result, wrappers in terraform-aws-security-group module are missing values of the rules variable and is not useful. :( 368 | var_value="try(each.value.${module_var}, var.defaults.${module_var}, {})" 369 | elif [[ $var_default == \<\<* ]]; then 370 | # Heredoc style default values produce HCL parsing error: 371 | # 'Unterminated template string; No closing marker was found for the string.' 372 | # Because closing marker must be alone on it's own line: 373 | # https://developer.hashicorp.com/terraform/language/expressions/strings#heredoc-strings 374 | var_value="try(each.value.${module_var}, var.defaults.${module_var}, $var_default 375 | )" 376 | else 377 | var_value="try(each.value.${module_var}, var.defaults.${module_var}, $var_default)" 378 | fi 379 | 380 | hcledit attribute append "module.wrapper.${module_var}" "${var_value}" $newline -f "$tmp_file_tf" -u 381 | 382 | newline="" 383 | done 384 | 385 | [[ "$verbose" == "true" ]] && cat "$tmp_file_tf" 386 | 387 | if [[ "$dry_run" == "false" ]]; then 388 | common::colorify "green" "Saving files into \"${output_dir}\"" 389 | 390 | # Create output dir 391 | [[ ! -d "$output_dir" ]] && mkdir -p "$output_dir" 392 | 393 | mv "$tmp_file_tf" "${output_dir}/main.tf" 394 | 395 | echo "$CONTENT_VARIABLES_TF" > "${output_dir}/variables.tf" 396 | echo "$CONTENT_VERSIONS_TF" > "${output_dir}/versions.tf" 397 | 398 | echo "$CONTENT_OUTPUTS_TF" > "${output_dir}/outputs.tf" 399 | sed -i.bak "s|WRAPPER_OUTPUT_SENSITIVE|${wrapper_output_sensitive}|g" "${output_dir}/outputs.tf" 400 | rm -rf "${output_dir}/outputs.tf.bak" 401 | 402 | echo "$CONTENT_README" > "${output_dir}/README.md" 403 | sed -i.bak -e " 404 | s#WRAPPER_TITLE#${wrapper_title}#g 405 | s#WRAPPER_PATH#${wrapper_path}#g 406 | s#MODULE_REPO_ORG#${module_repo_org}#g 407 | s#MODULE_REPO_SHORTNAME#${module_repo_shortname}#g 408 | s#MODULE_REPO_PROVIDER#${module_repo_provider}#g 409 | " "${output_dir}/README.md" 410 | rm -rf "${output_dir}/README.md.bak" 411 | else 412 | common::colorify "yellow" "There is nothing to save. Remove --dry-run flag to write files." 413 | fi 414 | 415 | done 416 | 417 | } 418 | 419 | function check_dependencies { 420 | if ! command -v hcledit > /dev/null; then 421 | echo "ERROR: The binary 'hcledit' is required by this hook but is not installed or is not in the system's PATH." 422 | echo "Check documentation: https://github.com/minamijoyo/hcledit" 423 | exit 1 424 | fi 425 | } 426 | 427 | function create_tmp_file_tf { 428 | # Can't append extension for mktemp, so renaming instead 429 | tmp_file=$(mktemp "${TMPDIR:-/tmp}/tfwrapper-XXXXXXXXXX") 430 | mv "$tmp_file" "$tmp_file.tf" 431 | tmp_file_tf="$tmp_file.tf" 432 | 433 | # mktemp creates with no group/other read permissions 434 | chmod a+r "$tmp_file_tf" 435 | 436 | echo "$CONTENT_MAIN_TF" > "$tmp_file_tf" 437 | } 438 | 439 | [[ "${BASH_SOURCE[0]}" != "$0" ]] || main "$@" 440 | -------------------------------------------------------------------------------- /lib_getopt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | getopt() { 4 | # pure-getopt, a drop-in replacement for GNU getopt in pure Bash. 5 | # version 1.4.4 6 | # 7 | # Copyright 2012-2020 Aron Griffis 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included 18 | # in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 21 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 23 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 25 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 26 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | _getopt_main() { 29 | # Returns one of the following statuses: 30 | # 0 success 31 | # 1 error parsing parameters 32 | # 2 error in getopt invocation 33 | # 3 internal error 34 | # 4 reserved for -T 35 | # 36 | # For statuses 0 and 1, generates normalized and shell-quoted 37 | # "options -- parameters" on stdout. 38 | 39 | declare parsed status 40 | declare short long='' name flags='' 41 | declare have_short=false 42 | 43 | # Synopsis from getopt man-page: 44 | # 45 | # getopt optstring parameters 46 | # getopt [options] [--] optstring parameters 47 | # getopt [options] -o|--options optstring [options] [--] parameters 48 | # 49 | # The first form can be normalized to the third form which 50 | # _getopt_parse() understands. The second form can be recognized after 51 | # first parse when $short hasn't been set. 52 | 53 | if [[ -n ${GETOPT_COMPATIBLE+isset} || $1 == [^-]* ]]; then 54 | # Enable compatibility mode 55 | flags=c$flags 56 | # Normalize first to third synopsis form 57 | set -- -o "$1" -- "${@:2}" 58 | fi 59 | 60 | # First parse always uses flags=p since getopt always parses its own 61 | # arguments effectively in this mode. 62 | parsed=$(_getopt_parse getopt ahl:n:o:qQs:TuV \ 63 | alternative,help,longoptions:,name:,options:,quiet,quiet-output,shell:,test,version \ 64 | p "$@") 65 | status=$? 66 | if [[ $status != 0 ]]; then 67 | if [[ $status == 1 ]]; then 68 | echo "Try \`getopt --help' for more information." >&2 69 | # Since this is the first parse, convert status 1 to 2 70 | status=2 71 | fi 72 | return $status 73 | fi 74 | eval "set -- $parsed" 75 | 76 | while [[ $# -gt 0 ]]; do 77 | case $1 in 78 | (-a|--alternative) 79 | flags=a$flags ;; 80 | 81 | (-h|--help) 82 | _getopt_help 83 | return 2 # as does GNU getopt 84 | ;; 85 | 86 | (-l|--longoptions) 87 | long="$long${long:+,}$2" 88 | shift ;; 89 | 90 | (-n|--name) 91 | name=$2 92 | shift ;; 93 | 94 | (-o|--options) 95 | short=$2 96 | have_short=true 97 | shift ;; 98 | 99 | (-q|--quiet) 100 | flags=q$flags ;; 101 | 102 | (-Q|--quiet-output) 103 | flags=Q$flags ;; 104 | 105 | (-s|--shell) 106 | case $2 in 107 | (sh|bash) 108 | flags=${flags//t/} ;; 109 | (csh|tcsh) 110 | flags=t$flags ;; 111 | (*) 112 | echo 'getopt: unknown shell after -s or --shell argument' >&2 113 | echo "Try \`getopt --help' for more information." >&2 114 | return 2 ;; 115 | esac 116 | shift ;; 117 | 118 | (-u|--unquoted) 119 | flags=u$flags ;; 120 | 121 | (-T|--test) 122 | return 4 ;; 123 | 124 | (-V|--version) 125 | echo "pure-getopt 1.4.4" 126 | return 0 ;; 127 | 128 | (--) 129 | shift 130 | break ;; 131 | esac 132 | 133 | shift 134 | done 135 | 136 | if ! $have_short; then 137 | # $short was declared but never set, not even to an empty string. 138 | # This implies the second form in the synopsis. 139 | if [[ $# == 0 ]]; then 140 | echo 'getopt: missing optstring argument' >&2 141 | echo "Try \`getopt --help' for more information." >&2 142 | return 2 143 | fi 144 | short=$1 145 | have_short=true 146 | shift 147 | fi 148 | 149 | if [[ $short == -* ]]; then 150 | # Leading dash means generate output in place rather than reordering, 151 | # unless we're already in compatibility mode. 152 | [[ $flags == *c* ]] || flags=i$flags 153 | short=${short#?} 154 | elif [[ $short == +* ]]; then 155 | # Leading plus means POSIXLY_CORRECT, unless we're already in 156 | # compatibility mode. 157 | [[ $flags == *c* ]] || flags=p$flags 158 | short=${short#?} 159 | fi 160 | 161 | # This should fire if POSIXLY_CORRECT is in the environment, even if 162 | # it's an empty string. That's the difference between :+ and + 163 | flags=${POSIXLY_CORRECT+p}$flags 164 | 165 | _getopt_parse "${name:-getopt}" "$short" "$long" "$flags" "$@" 166 | } 167 | 168 | _getopt_parse() { 169 | # Inner getopt parser, used for both first parse and second parse. 170 | # Returns 0 for success, 1 for error parsing, 3 for internal error. 171 | # In the case of status 1, still generates stdout with whatever could 172 | # be parsed. 173 | # 174 | # $flags is a string of characters with the following meanings: 175 | # a - alternative parsing mode 176 | # c - GETOPT_COMPATIBLE 177 | # i - generate output in place rather than reordering 178 | # p - POSIXLY_CORRECT 179 | # q - disable error reporting 180 | # Q - disable normal output 181 | # t - quote for csh/tcsh 182 | # u - unquoted output 183 | 184 | declare name="$1" short="$2" long="$3" flags="$4" 185 | shift 4 186 | 187 | # Split $long on commas, prepend double-dashes, strip colons; 188 | # for use with _getopt_resolve_abbrev 189 | declare -a longarr 190 | _getopt_split longarr "$long" 191 | longarr=( "${longarr[@]/#/--}" ) 192 | longarr=( "${longarr[@]%:}" ) 193 | longarr=( "${longarr[@]%:}" ) 194 | 195 | # Parse and collect options and parameters 196 | declare -a opts params 197 | declare o alt_recycled=false error=0 198 | 199 | while [[ $# -gt 0 ]]; do 200 | case $1 in 201 | (--) 202 | params=( "${params[@]}" "${@:2}" ) 203 | break ;; 204 | 205 | (--*=*) 206 | o=${1%%=*} 207 | if ! o=$(_getopt_resolve_abbrev "$o" "${longarr[@]}"); then 208 | error=1 209 | elif [[ ,"$long", == *,"${o#--}"::,* ]]; then 210 | opts=( "${opts[@]}" "$o" "${1#*=}" ) 211 | elif [[ ,"$long", == *,"${o#--}":,* ]]; then 212 | opts=( "${opts[@]}" "$o" "${1#*=}" ) 213 | elif [[ ,"$long", == *,"${o#--}",* ]]; then 214 | if $alt_recycled; then o=${o#-}; fi 215 | _getopt_err "$name: option '$o' doesn't allow an argument" 216 | error=1 217 | else 218 | echo "getopt: assertion failed (1)" >&2 219 | return 3 220 | fi 221 | alt_recycled=false 222 | ;; 223 | 224 | (--?*) 225 | o=$1 226 | if ! o=$(_getopt_resolve_abbrev "$o" "${longarr[@]}"); then 227 | error=1 228 | elif [[ ,"$long", == *,"${o#--}",* ]]; then 229 | opts=( "${opts[@]}" "$o" ) 230 | elif [[ ,"$long", == *,"${o#--}::",* ]]; then 231 | opts=( "${opts[@]}" "$o" '' ) 232 | elif [[ ,"$long", == *,"${o#--}:",* ]]; then 233 | if [[ $# -ge 2 ]]; then 234 | shift 235 | opts=( "${opts[@]}" "$o" "$1" ) 236 | else 237 | if $alt_recycled; then o=${o#-}; fi 238 | _getopt_err "$name: option '$o' requires an argument" 239 | error=1 240 | fi 241 | else 242 | echo "getopt: assertion failed (2)" >&2 243 | return 3 244 | fi 245 | alt_recycled=false 246 | ;; 247 | 248 | (-*) 249 | if [[ $flags == *a* ]]; then 250 | # Alternative parsing mode! 251 | # Try to handle as a long option if any of the following apply: 252 | # 1. There's an equals sign in the mix -x=3 or -xy=3 253 | # 2. There's 2+ letters and an abbreviated long match -xy 254 | # 3. There's a single letter and an exact long match 255 | # 4. There's a single letter and no short match 256 | o=${1::2} # temp for testing #4 257 | if [[ $1 == *=* || $1 == -?? || \ 258 | ,$long, == *,"${1#-}"[:,]* || \ 259 | ,$short, != *,"${o#-}"[:,]* ]]; then 260 | o=$(_getopt_resolve_abbrev "${1%%=*}" "${longarr[@]}" 2>/dev/null) 261 | case $? in 262 | (0) 263 | # Unambiguous match. Let the long options parser handle 264 | # it, with a flag to get the right error message. 265 | set -- "-$1" "${@:2}" 266 | alt_recycled=true 267 | continue ;; 268 | (1) 269 | # Ambiguous match, generate error and continue. 270 | _getopt_resolve_abbrev "${1%%=*}" "${longarr[@]}" >/dev/null 271 | error=1 272 | shift 273 | continue ;; 274 | (2) 275 | # No match, fall through to single-character check. 276 | true ;; 277 | (*) 278 | echo "getopt: assertion failed (3)" >&2 279 | return 3 ;; 280 | esac 281 | fi 282 | fi 283 | 284 | o=${1::2} 285 | if [[ "$short" == *"${o#-}"::* ]]; then 286 | if [[ ${#1} -gt 2 ]]; then 287 | opts=( "${opts[@]}" "$o" "${1:2}" ) 288 | else 289 | opts=( "${opts[@]}" "$o" '' ) 290 | fi 291 | elif [[ "$short" == *"${o#-}":* ]]; then 292 | if [[ ${#1} -gt 2 ]]; then 293 | opts=( "${opts[@]}" "$o" "${1:2}" ) 294 | elif [[ $# -ge 2 ]]; then 295 | shift 296 | opts=( "${opts[@]}" "$o" "$1" ) 297 | else 298 | _getopt_err "$name: option requires an argument -- '${o#-}'" 299 | error=1 300 | fi 301 | elif [[ "$short" == *"${o#-}"* ]]; then 302 | opts=( "${opts[@]}" "$o" ) 303 | if [[ ${#1} -gt 2 ]]; then 304 | set -- "$o" "-${1:2}" "${@:2}" 305 | fi 306 | else 307 | if [[ $flags == *a* ]]; then 308 | # Alternative parsing mode! Report on the entire failed 309 | # option. GNU includes =value but we omit it for sanity with 310 | # very long values. 311 | _getopt_err "$name: unrecognized option '${1%%=*}'" 312 | else 313 | _getopt_err "$name: invalid option -- '${o#-}'" 314 | if [[ ${#1} -gt 2 ]]; then 315 | set -- "$o" "-${1:2}" "${@:2}" 316 | fi 317 | fi 318 | error=1 319 | fi ;; 320 | 321 | (*) 322 | # GNU getopt in-place mode (leading dash on short options) 323 | # overrides POSIXLY_CORRECT 324 | if [[ $flags == *i* ]]; then 325 | opts=( "${opts[@]}" "$1" ) 326 | elif [[ $flags == *p* ]]; then 327 | params=( "${params[@]}" "$@" ) 328 | break 329 | else 330 | params=( "${params[@]}" "$1" ) 331 | fi 332 | esac 333 | 334 | shift 335 | done 336 | 337 | if [[ $flags == *Q* ]]; then 338 | true # generate no output 339 | else 340 | echo -n ' ' 341 | if [[ $flags == *[cu]* ]]; then 342 | printf '%s -- %s' "${opts[*]}" "${params[*]}" 343 | else 344 | if [[ $flags == *t* ]]; then 345 | _getopt_quote_csh "${opts[@]}" -- "${params[@]}" 346 | else 347 | _getopt_quote "${opts[@]}" -- "${params[@]}" 348 | fi 349 | fi 350 | echo 351 | fi 352 | 353 | return $error 354 | } 355 | 356 | _getopt_err() { 357 | if [[ $flags != *q* ]]; then 358 | printf '%s\n' "$1" >&2 359 | fi 360 | } 361 | 362 | _getopt_resolve_abbrev() { 363 | # Resolves an abbrevation from a list of possibilities. 364 | # If the abbreviation is unambiguous, echoes the expansion on stdout 365 | # and returns 0. If the abbreviation is ambiguous, prints a message on 366 | # stderr and returns 1. (For first parse this should convert to exit 367 | # status 2.) If there is no match at all, prints a message on stderr 368 | # and returns 2. 369 | declare a q="$1" 370 | declare -a matches=() 371 | shift 372 | for a; do 373 | if [[ $q == "$a" ]]; then 374 | # Exact match. Squash any other partial matches. 375 | matches=( "$a" ) 376 | break 377 | elif [[ $flags == *a* && $q == -[^-]* && $a == -"$q" ]]; then 378 | # Exact alternative match. Squash any other partial matches. 379 | matches=( "$a" ) 380 | break 381 | elif [[ $a == "$q"* ]]; then 382 | # Abbreviated match. 383 | matches=( "${matches[@]}" "$a" ) 384 | elif [[ $flags == *a* && $q == -[^-]* && $a == -"$q"* ]]; then 385 | # Abbreviated alternative match. 386 | matches=( "${matches[@]}" "${a#-}" ) 387 | fi 388 | done 389 | case ${#matches[@]} in 390 | (0) 391 | [[ $flags == *q* ]] || \ 392 | printf "$name: unrecognized option %s\\n" >&2 \ 393 | "$(_getopt_quote "$q")" 394 | return 2 ;; 395 | (1) 396 | printf '%s' "${matches[0]}"; return 0 ;; 397 | (*) 398 | [[ $flags == *q* ]] || \ 399 | printf "$name: option %s is ambiguous; possibilities: %s\\n" >&2 \ 400 | "$(_getopt_quote "$q")" "$(_getopt_quote "${matches[@]}")" 401 | return 1 ;; 402 | esac 403 | } 404 | 405 | _getopt_split() { 406 | # Splits $2 at commas to build array specified by $1 407 | declare IFS=, 408 | eval "$1=( \$2 )" 409 | } 410 | 411 | _getopt_quote() { 412 | # Quotes arguments with single quotes, escaping inner single quotes 413 | declare s space='' q=\' 414 | for s; do 415 | printf "$space'%s'" "${s//$q/$q\\$q$q}" 416 | space=' ' 417 | done 418 | } 419 | 420 | _getopt_quote_csh() { 421 | # Quotes arguments with single quotes, escaping inner single quotes, 422 | # bangs, backslashes and newlines 423 | declare s i c space 424 | for s; do 425 | echo -n "$space'" 426 | for ((i=0; i<${#s}; i++)); do 427 | c=${s:i:1} 428 | case $c in 429 | (\\|\'|!) 430 | echo -n "'\\$c'" ;; 431 | ($'\n') 432 | echo -n "\\$c" ;; 433 | (*) 434 | echo -n "$c" ;; 435 | esac 436 | done 437 | echo -n \' 438 | space=' ' 439 | done 440 | } 441 | 442 | _getopt_help() { 443 | cat <<-EOT >&2 444 | 445 | Usage: 446 | getopt 447 | getopt [options] [--] 448 | getopt [options] -o|--options [options] [--] 449 | 450 | Parse command options. 451 | 452 | Options: 453 | -a, --alternative allow long options starting with single - 454 | -l, --longoptions the long options to be recognized 455 | -n, --name the name under which errors are reported 456 | -o, --options the short options to be recognized 457 | -q, --quiet disable error reporting by getopt(3) 458 | -Q, --quiet-output no normal output 459 | -s, --shell set quoting conventions to those of 460 | -T, --test test for getopt(1) version 461 | -u, --unquoted do not quote the output 462 | 463 | -h, --help display this help and exit 464 | -V, --version output version information and exit 465 | 466 | For more details see getopt(1). 467 | EOT 468 | } 469 | 470 | _getopt_version_check() { 471 | if [[ -z $BASH_VERSION ]]; then 472 | echo "getopt: unknown version of bash might not be compatible" >&2 473 | return 1 474 | fi 475 | 476 | # This is a lexical comparison that should be sufficient forever. 477 | if [[ $BASH_VERSION < 2.05b ]]; then 478 | echo "getopt: bash $BASH_VERSION might not be compatible" >&2 479 | return 1 480 | fi 481 | 482 | return 0 483 | } 484 | 485 | _getopt_version_check 486 | _getopt_main "$@" 487 | declare status=$? 488 | unset -f _getopt_main _getopt_err _getopt_parse _getopt_quote \ 489 | _getopt_quote_csh _getopt_resolve_abbrev _getopt_split _getopt_help \ 490 | _getopt_version_check 491 | return $status 492 | } 493 | 494 | # vim:sw=2 495 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Collection of git hooks for OpenTofu to be used with [pre-commit framework](http://pre-commit.com/) 2 | 3 | [![Github tag](https://img.shields.io/github/tag/tofuutils/pre-commit-opentofu.svg)](https://github.com/tofuutils/pre-commit-opentofu/releases) ![maintenance status](https://img.shields.io/maintenance/yes/2024.svg) [![Help Contribute to Open Source](https://www.codetriage.com/tofuutils/pre-commit-opentofu/badges/users.svg)](https://www.codetriage.com/tofuutils/pre-commit-opentofu) 4 | 5 | Want to contribute? Check [open issues](https://github.com/tofuutils/pre-commit-opentofu/issues?q=label%3A%22good+first+issue%22+is%3Aopen+sort%3Aupdated-desc) and [contributing notes](/.github/CONTRIBUTING.md). 6 | 7 | ## Sponsors 8 | If you are using `pre-commit-opentofu` already or want to support its development and [many other open-source projects](https://github.com/tofuutils), please become a [GitHub Sponsor](https://github.com/sponsors/tofuutils)! 9 | 10 | 11 | ## Table of content 12 | 13 | * [Table of content](#table-of-content) 14 | * [How to install](#how-to-install) 15 | * [1. Install dependencies](#1-install-dependencies) 16 | * [2. Install the pre-commit hook globally](#2-install-the-pre-commit-hook-globally) 17 | * [3. Add configs and hooks](#3-add-configs-and-hooks) 18 | * [4. Run](#4-run) 19 | * [Available Hooks](#available-hooks) 20 | * [Hooks usage notes and examples](#hooks-usage-notes-and-examples) 21 | * [Known limitations](#known-limitations) 22 | * [All hooks: Usage of environment variables in `--args`](#all-hooks-usage-of-environment-variables-in---args) 23 | * [All hooks: Set env vars inside hook at runtime](#all-hooks-set-env-vars-inside-hook-at-runtime) 24 | * [All hooks: Disable color output](#all-hooks-disable-color-output) 25 | * [checkov (deprecated) and tofu\_checkov](#checkov-deprecated-and-tofu_checkov) 26 | * [infracost\_breakdown](#infracost_breakdown) 27 | * [tofu\_docs](#tofu_docs) 28 | * [tofu\_docs\_replace (deprecated)](#tofu_docs_replace-deprecated) 29 | * [tofu\_fmt](#tofu_fmt) 30 | * [tofu\_providers\_lock](#tofu_providers_lock) 31 | * [tofu\_tflint](#tofu_tflint) 32 | * [tofu\_tfsec (deprecated)](#tofu_tfsec-deprecated) 33 | * [tofu\_trivy](#tofu_trivy) 34 | * [tofu\_validate](#tofu_validate) 35 | * [tofu\_wrapper\_module\_for\_each](#tofu_wrapper_module_for_each) 36 | * [terrascan](#terrascan) 37 | * [tfupdate](#tfupdate) 38 | * [Docker Usage](#docker-usage) 39 | * [File Permissions](#file-permissions) 40 | * [Download OpenTofu modules from private GitHub repositories](#download-tofu-modules-from-private-github-repositories) 41 | * [Authors](#authors) 42 | * [License](#license) 43 | 44 | ## How to install 45 | 46 | ### 1. Install dependencies 47 | 48 | 49 | 50 | * [`pre-commit`](https://pre-commit.com/#install), 51 | [`opentofu`](https://opentofu.org/docs/intro/install/), 52 | [`git`](https://git-scm.com/downloads), 53 | POSIX compatible shell, 54 | Internet connection (on first run), 55 | x86_64 or arm64 compatible operation system, 56 | Some hardware where this OS will run, 57 | Electricity for hardware and internet connection, 58 | Some basic physical laws, 59 | Hope that it all will work. 60 |

61 | * [`checkov`](https://github.com/bridgecrewio/checkov) required for `tofu_checkov` hook. 62 | * [`terraform-docs`](https://github.com/terraform-docs/terraform-docs) required for `tofu_docs` hook. 63 | * [`terragrunt`](https://terragrunt.gruntwork.io/docs/getting-started/install/) required for `terragrunt_validate` hook. 64 | * [`terrascan`](https://github.com/tenable/terrascan) required for `terrascan` hook. 65 | * [`TFLint`](https://github.com/terraform-linters/tflint) required for `tofu_tflint` hook. 66 | * [`TFSec`](https://github.com/liamg/tfsec) required for `tofu_tfsec` hook. 67 | * [`Trivy`](https://github.com/aquasecurity/trivy) required for `tofu_trivy` hook. 68 | * [`infracost`](https://github.com/infracost/infracost) required for `infracost_breakdown` hook. 69 | * [`jq`](https://github.com/stedolan/jq) required for `tofu_validate` with `--retry-once-with-cleanup` flag, and for `infracost_breakdown` hook. 70 | * [`tfupdate`](https://github.com/minamijoyo/tfupdate) required for `tfupdate` hook. 71 | * [`hcledit`](https://github.com/minamijoyo/hcledit) required for `tofu_wrapper_module_for_each` hook. 72 | 73 |
Docker
74 | 75 | **Pull docker image with all hooks**: 76 | 77 | ```bash 78 | TAG=latest 79 | docker pull tofuutils/pre-commit-opentofu:$TAG 80 | ``` 81 | 82 | All available tags [here](https://github.com/tofuutils/pre-commit-opentofu/pkgs/container/pre-commit-opentofu/versions). 83 | 84 | **Build from scratch**: 85 | 86 | > **Note**: To build image you need to have [`docker buildx`](https://docs.docker.com/build/install-buildx/) enabled as default builder. 87 | > Otherwise - provide `TARGETOS` and `TARGETARCH` as additional `--build-arg`'s to `docker build`. 88 | 89 | When hooks-related `--build-arg`s are not specified, only the latest version of `pre-commit` and `opentofu` will be installed. 90 | 91 | ```bash 92 | git clone git@github.com:tofuutils/pre-commit-opentofu.git 93 | cd pre-commit-opentofu 94 | # Install the latest versions of all the tools 95 | docker build -t pre-commit-opentofu --build-arg INSTALL_ALL=true . 96 | ``` 97 | 98 | To install a specific version of individual tools, define it using `--build-arg` arguments or set it to `latest`: 99 | 100 | ```bash 101 | docker build -t pre-commit-opentofu \ 102 | --build-arg PRE_COMMIT_VERSION=latest \ 103 | --build-arg TOFU_VERSION=latest \ 104 | --build-arg CHECKOV_VERSION=2.0.405 \ 105 | --build-arg INFRACOST_VERSION=latest \ 106 | --build-arg TERRAFORM_DOCS_VERSION=0.15.0 \ 107 | --build-arg TERRAGRUNT_VERSION=latest \ 108 | --build-arg TERRASCAN_VERSION=1.10.0 \ 109 | --build-arg TFLINT_VERSION=0.31.0 \ 110 | --build-arg TFSEC_VERSION=latest \ 111 | --build-arg TRIVY_VERSION=latest \ 112 | --build-arg TFUPDATE_VERSION=latest \ 113 | --build-arg HCLEDIT_VERSION=latest \ 114 | . 115 | ``` 116 | 117 | Set `-e PRE_COMMIT_COLOR=never` to disable the color output in `pre-commit`. 118 | 119 |
120 | 121 | 122 |
MacOS
123 | 124 | ```bash 125 | brew install pre-commit terraform-docs tflint tfsec trivy checkov terrascan infracost tfupdate minamijoyo/hcledit/hcledit jq 126 | ``` 127 | 128 |
129 | 130 |
Ubuntu 18.04
131 | 132 | ```bash 133 | sudo apt update 134 | sudo apt install -y unzip software-properties-common 135 | sudo add-apt-repository ppa:deadsnakes/ppa 136 | sudo apt install -y python3.7 python3-pip 137 | python3 -m pip install --upgrade pip 138 | pip3 install --no-cache-dir pre-commit 139 | python3.7 -m pip install -U checkov 140 | 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/ 141 | 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/ 142 | 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/ 143 | curl -L "$(curl -s https://api.github.com/repos/aquasecurity/trivy/releases/latest | grep -o -E -i -m 1 "https://.+?/trivy_.+?_Linux-64bit.tar.gz")" > trivy.tar.gz && tar -xzf trivy.tar.gz trivy && rm trivy.tar.gz && sudo mv trivy /usr/bin 144 | curl -L "$(curl -s https://api.github.com/repos/tenable/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 145 | sudo apt install -y jq && \ 146 | 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 147 | curl -L "$(curl -s https://api.github.com/repos/minamijoyo/tfupdate/releases/latest | grep -o -E -m 1 "https://.+?_linux_amd64.tar.gz")" > tfupdate.tar.gz && tar -xzf tfupdate.tar.gz tfupdate && rm tfupdate.tar.gz && sudo mv tfupdate /usr/bin/ 148 | curl -L "$(curl -s https://api.github.com/repos/minamijoyo/hcledit/releases/latest | grep -o -E -m 1 "https://.+?_linux_amd64.tar.gz")" > hcledit.tar.gz && tar -xzf hcledit.tar.gz hcledit && rm hcledit.tar.gz && sudo mv hcledit /usr/bin/ 149 | ``` 150 | 151 |
152 | 153 | 154 |
Ubuntu 20.04
155 | 156 | ```bash 157 | sudo apt update 158 | sudo apt install -y unzip software-properties-common python3 python3-pip 159 | python3 -m pip install --upgrade pip 160 | pip3 install --no-cache-dir pre-commit 161 | pip3 install --no-cache-dir checkov 162 | 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/ 163 | curl -L "$(curl -s https://api.github.com/repos/tenable/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 164 | 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/ 165 | 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/ 166 | curl -L "$(curl -s https://api.github.com/repos/aquasecurity/trivy/releases/latest | grep -o -E -i -m 1 "https://.+?/trivy_.+?_Linux-64bit.tar.gz")" > trivy.tar.gz && tar -xzf trivy.tar.gz trivy && rm trivy.tar.gz && sudo mv trivy /usr/bin 167 | sudo apt install -y jq && \ 168 | 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 169 | curl -L "$(curl -s https://api.github.com/repos/minamijoyo/tfupdate/releases/latest | grep -o -E -m 1 "https://.+?_linux_amd64.tar.gz")" > tfupdate.tar.gz && tar -xzf tfupdate.tar.gz tfupdate && rm tfupdate.tar.gz && sudo mv tfupdate /usr/bin/ 170 | curl -L "$(curl -s https://api.github.com/repos/minamijoyo/hcledit/releases/latest | grep -o -E -m 1 "https://.+?_linux_amd64.tar.gz")" > hcledit.tar.gz && tar -xzf hcledit.tar.gz hcledit && rm hcledit.tar.gz && sudo mv hcledit /usr/bin/ 171 | ``` 172 | 173 |
174 | 175 |
Ubuntu 22.04
176 | 177 | ```bash 178 | sudo apt update 179 | sudo apt install -y unzip software-properties-common python3 python3-pip 180 | python3 -m pip install --upgrade pip 181 | pip3 install --no-cache-dir pre-commit 182 | pip3 install --no-cache-dir checkov 183 | 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/ 184 | curl -L "$(curl -s https://api.github.com/repos/tenable/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 185 | 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/ 186 | 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/ 187 | curl -L "$(curl -s https://api.github.com/repos/aquasecurity/trivy/releases/latest | grep -o -E -i -m 1 "https://.+?/trivy_.+?_Linux-64bit.tar.gz")" > trivy.tar.gz && tar -xzf trivy.tar.gz trivy && rm trivy.tar.gz && sudo mv trivy /usr/bin 188 | sudo apt install -y jq && \ 189 | 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 190 | curl -L "$(curl -s https://api.github.com/repos/minamijoyo/tfupdate/releases/latest | grep -o -E -m 1 "https://.+?_linux_amd64.tar.gz")" > tfupdate.tar.gz && tar -xzf tfupdate.tar.gz tfupdate && rm tfupdate.tar.gz && sudo mv tfupdate /usr/bin/ 191 | curl -L "$(curl -s https://api.github.com/repos/minamijoyo/hcledit/releases/latest | grep -o -E -m 1 "https://.+?_linux_amd64.tar.gz")" > hcledit.tar.gz && tar -xzf hcledit.tar.gz hcledit && rm hcledit.tar.gz && sudo mv hcledit /usr/bin/ 192 | ``` 193 | 194 |
195 | 196 | 197 |
Windows 10/11 198 | 199 | We highly recommend using [WSL/WSL2](https://docs.microsoft.com/en-us/windows/wsl/install) with Ubuntu and following the Ubuntu installation guide. Or use Docker. 200 | 201 | > **Note**: We won't be able to help with issues that can't be reproduced in Linux/Mac. 202 | > So, try to find a working solution and send PR before open an issue. 203 | 204 | Otherwise, you can follow [this gist](https://gist.github.com/etiennejeanneaurevolve/1ed387dc73c5d4cb53ab313049587d09): 205 | 206 | 1. Install [`git`](https://git-scm.com/downloads) and [`gitbash`](https://gitforwindows.org/) 207 | 2. Install [Python 3](https://www.python.org/downloads/) 208 | 3. Install all prerequisites needed (see above) 209 | 210 | Ensure your PATH environment variable looks for `bash.exe` in `C:\Program Files\Git\bin` (the one present in `C:\Windows\System32\bash.exe` does not work with `pre-commit.exe`) 211 | 212 | For `checkov`, you may need to also set your `PYTHONPATH` environment variable with the path to your Python modules. 213 | E.g. `C:\Users\USERNAME\AppData\Local\Programs\Python\Python39\Lib\site-packages` 214 | 215 |
216 | 217 | 218 | 219 | ### 2. Install the pre-commit hook globally 220 | 221 | > **Note**: not needed if you use the Docker image 222 | 223 | ```bash 224 | DIR=~/.git-template 225 | git config --global init.templateDir ${DIR} 226 | pre-commit init-templatedir -t pre-commit ${DIR} 227 | ``` 228 | 229 | ### 3. Add configs and hooks 230 | 231 | Step into the repository you want to have the pre-commit hooks installed and run: 232 | 233 | ```bash 234 | git init 235 | cat < .pre-commit-config.yaml 236 | repos: 237 | - repo: https://github.com/tofuutils/pre-commit-opentofu 238 | rev: # Get the latest from: https://github.com/tofuutils/pre-commit-opentofu/releases 239 | hooks: 240 | - id: tofu_fmt 241 | - id: tofu_docs 242 | EOF 243 | ``` 244 | 245 | ### 4. Run 246 | 247 | Execute this command to run `pre-commit` on all files in the repository (not only changed files): 248 | 249 | ```bash 250 | pre-commit run -a 251 | ``` 252 | 253 | Or, using Docker ([available tags](https://github.com/tofuutils/pre-commit-opentofu/pkgs/container/pre-commit-opentofu/versions)): 254 | 255 | > **Note**: This command uses your user id and group id for the docker container to use to access the local files. If the files are owned by another user, update the `USERID` environment variable. See [File Permissions section](#file-permissions) for more information. 256 | 257 | ```bash 258 | TAG=latest 259 | docker run -e "USERID=$(id -u):$(id -g)" -v $(pwd):/lint -w /lint tofuutils/pre-commit-opentofu:$TAG run -a 260 | ``` 261 | 262 | Execute this command to list the versions of the tools in Docker: 263 | 264 | ```bash 265 | TAG=latest 266 | docker run --rm --entrypoint cat tofuutils/pre-commit-opentofu:$TAG /usr/bin/tools_versions_info 267 | ``` 268 | 269 | ## Available Hooks 270 | 271 | There are several [pre-commit](https://pre-commit.com/) hooks to keep OpenTofu configurations (both `*.tf` and `*.tfvars`) and Terragrunt configurations (`*.hcl`) in a good shape: 272 | 273 | 274 | | Hook name | Description | Dependencies
[Install instructions here](#1-install-dependencies) | 275 | | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | 276 | | `checkov` and `tofu_checkov` | [checkov](https://github.com/bridgecrewio/checkov) static analysis of OpenTofu templates to spot potential security issues. [Hook notes](#checkov-deprecated-and-tofu_checkov) | `checkov`
Ubuntu deps: `python3`, `python3-pip` | 277 | | `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) | 278 | | `tofu_docs` | Inserts input and output documentation into `README.md`. Recommended. [Hook notes](#terraform_docs) | `terraform-docs` | 279 | | `tofu_docs_replace` | Runs `terraform-docs` and pipes the output directly to README.md. **DEPRECATED**. [Hook notes](#terraform_docs_replace-deprecated) | `python3`, `terraform-docs` | 280 | | `tofu_docs_without_`
`aggregate_type_defaults` | Inserts input and output documentation into `README.md` without aggregate type defaults. Hook notes same as for [tofu_docs](#terraform_docs) | `tofu-docs` | 281 | | `tofu_fmt` | Reformat all OpenTofu configuration files to a canonical format. [Hook notes](#terraform_fmt) | - | 282 | | `tofu_providers_lock` | Updates provider signatures in [dependency lock files](https://www.terraform.io/docs/cli/commands/providers/lock.html). [Hook notes](#terraform_providers_lock) | - | 283 | | `tofu_tflint` | Validates all OpenTofu 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` | 284 | | `tofu_tfsec` | [TFSec](https://github.com/aquasecurity/tfsec) static analysis of terraform templates to spot potential security issues. **DEPRECATED**, use `tofu_trivy`. [Hook notes](#terraform_tfsec-deprecated) | `tfsec` | 285 | | `tofu_trivy` | [Trivy](https://github.com/aquasecurity/trivy) static analysis of terraform templates to spot potential security issues. [Hook notes](#terraform_trivy) | `trivy` | 286 | | `tofu_validate` | Validates all Terraform configuration files. [Hook notes](#tofu_validate) | `jq`, only for `--retry-once-with-cleanup` flag | 287 | | `terragrunt_fmt` | Reformat all [Terragrunt](https://github.com/gruntwork-io/terragrunt) configuration files (`*.hcl`) to a canonical format. | `terragrunt` | 288 | | `terragrunt_validate` | Validates all [Terragrunt](https://github.com/gruntwork-io/terragrunt) configuration files (`*.hcl`) | `terragrunt` | 289 | | `tofu_wrapper_module_for_each` | Generates OpenTofu wrappers with `for_each` in module. [Hook notes](#terraform_wrapper_module_for_each) | `hcledit` | 290 | | `terrascan` | [terrascan](https://github.com/tenable/terrascan) Detect compliance and security violations. [Hook notes](#terrascan) | `terrascan` | 291 | | `tfupdate` | [tfupdate](https://github.com/minamijoyo/tfupdate) Update version constraints of OpenTofu core, providers, and modules. [Hook notes](#tfupdate) | `tfupdate` | 292 | 293 | 294 | Check the [source file](https://github.com/tofuutils/pre-commit-opentofu/blob/master/.pre-commit-hooks.yaml) to know arguments used for each hook. 295 | 296 | ## Hooks usage notes and examples 297 | 298 | ### Known limitations 299 | 300 | OpenTofu operates on a per-dir basis, while `pre-commit` framework only supports files and files that exist. This means if you only remove the TF-related file without any other changes in the same dir, checks will be skipped. Example and details [here](https://github.com/pre-commit/pre-commit/issues/3048). 301 | 302 | ### All hooks: Usage of environment variables in `--args` 303 | 304 | > All, except deprecated hooks: `checkov`, `tofu_docs_replace` 305 | 306 | You can use environment variables for the `--args` section. 307 | 308 | > **Warning**: You _must_ use the `${ENV_VAR}` definition, `$ENV_VAR` will not expand. 309 | 310 | Config example: 311 | 312 | ```yaml 313 | - id: tofu_tflint 314 | args: 315 | - --args=--config=${CONFIG_NAME}.${CONFIG_EXT} 316 | - --args=--module 317 | ``` 318 | 319 | If for config above set up `export CONFIG_NAME=.tflint; export CONFIG_EXT=hcl` before `pre-commit run`, args will be expanded to `--config=.tflint.hcl --module`. 320 | 321 | ### All hooks: Set env vars inside hook at runtime 322 | 323 | > All, except deprecated hooks: `checkov`, `tofu_docs_replace` 324 | 325 | You can specify environment variables that will be passed to the hook at runtime. 326 | 327 | Config example: 328 | 329 | ```yaml 330 | - id: tofu_validate 331 | args: 332 | - --env-vars=AWS_DEFAULT_REGION="us-west-2" 333 | - --env-vars=AWS_ACCESS_KEY_ID="anaccesskey" 334 | - --env-vars=AWS_SECRET_ACCESS_KEY="asecretkey" 335 | ``` 336 | 337 | ### All hooks: Disable color output 338 | 339 | > All, except deprecated hooks: `checkov`, `tofu_docs_replace` 340 | 341 | To disable color output for all hooks, set `PRE_COMMIT_COLOR=never` var. Eg: 342 | 343 | ```bash 344 | PRE_COMMIT_COLOR=never pre-commit run 345 | ``` 346 | 347 | ### checkov (deprecated) and tofu_checkov 348 | 349 | > `checkov` hook is deprecated, please use `tofu_checkov`. 350 | 351 | Note that `tofu_checkov` runs recursively during `-d .` usage. That means, for example, if you change `.tf` file in repo root, all existing `.tf` files in the repo will be checked. 352 | 353 | 1. You can specify custom arguments. E.g.: 354 | 355 | ```yaml 356 | - id: tofu_checkov 357 | args: 358 | - --args=--quiet 359 | - --args=--skip-check CKV2_AWS_8 360 | ``` 361 | 362 | Check all available arguments [here](https://www.checkov.io/2.Basics/CLI%20Command%20Reference.html). 363 | 364 | For deprecated hook you need to specify each argument separately: 365 | 366 | ```yaml 367 | - id: checkov 368 | args: [ 369 | "-d", ".", 370 | "--skip-check", "CKV2_AWS_8", 371 | ] 372 | ``` 373 | 374 | 2. When you have multiple directories and want to run `tofu_checkov` in all of them and share a single config file - use the `__GIT_WORKING_DIR__` placeholder. It will be replaced by `tofu_checkov` hooks with the Git working directory (repo root) at run time. For example: 375 | 376 | ```yaml 377 | - id: tofu_checkov 378 | args: 379 | - --args=--config-file __GIT_WORKING_DIR__/.checkov.yml 380 | ``` 381 | 382 | ### infracost_breakdown 383 | 384 | `infracost_breakdown` executes `infracost breakdown` command and compare the estimated costs with those specified in the hook-config. `infracost breakdown` parses OpenTofu HCL code, and calls Infracost Cloud Pricing API (remote version or [self-hosted version](https://www.infracost.io/docs/cloud_pricing_api/self_hosted)). 385 | 386 | Unlike most other hooks, this hook triggers once if there are any changed files in the repository. 387 | 388 | 1. `infracost_breakdown` supports all `infracost breakdown` arguments (run `infracost breakdown --help` to see them). The following example only shows costs: 389 | 390 | ```yaml 391 | - id: infracost_breakdown 392 | args: 393 | - --args=--path=./env/dev 394 | verbose: true # Always show costs 395 | ``` 396 | 397 |
Output 398 | 399 | ```bash 400 | Running in "env/dev" 401 | 402 | Summary: { 403 | "unsupportedResourceCounts": { 404 | "aws_sns_topic_subscription": 1 405 | } 406 | } 407 | 408 | Total Monthly Cost: 86.83 USD 409 | Total Monthly Cost (diff): 86.83 USD 410 | ``` 411 | 412 |
413 | 414 | 2. Note that spaces are not allowed in `--args`, so you need to split it, like this: 415 | 416 | ```yaml 417 | - id: infracost_breakdown 418 | args: 419 | - --args=--path=./env/dev 420 | - --args=--terraform-var-file="terraform.tfvars" 421 | - --args=--terraform-var-file="../terraform.tfvars" 422 | ``` 423 | 424 | 3. (Optionally) Define `cost constraints` the hook should evaluate successfully in order to pass: 425 | 426 | ```yaml 427 | - id: infracost_breakdown 428 | args: 429 | - --args=--path=./env/dev 430 | - --hook-config='.totalHourlyCost|tonumber > 0.1' 431 | - --hook-config='.totalHourlyCost|tonumber > 1' 432 | - --hook-config='.projects[].diff.totalMonthlyCost|tonumber != 10000' 433 | - --hook-config='.currency == "USD"' 434 | ``` 435 | 436 |
Output 437 | 438 | ```bash 439 | Running in "env/dev" 440 | Passed: .totalHourlyCost|tonumber > 0.1 0.11894520547945205 > 0.1 441 | Failed: .totalHourlyCost|tonumber > 1 0.11894520547945205 > 1 442 | Passed: .projects[].diff.totalMonthlyCost|tonumber !=10000 86.83 != 10000 443 | Passed: .currency == "USD" "USD" == "USD" 444 | 445 | Summary: { 446 | "unsupportedResourceCounts": { 447 | "aws_sns_topic_subscription": 1 448 | } 449 | } 450 | 451 | Total Monthly Cost: 86.83 USD 452 | Total Monthly Cost (diff): 86.83 USD 453 | ``` 454 | 455 |
456 | 457 | * Only one path per one hook (`- id: infracost_breakdown`) is allowed. 458 | * Set `verbose: true` to see cost even when the checks are passed. 459 | * Hook uses `jq` to process the cost estimation report returned by `infracost breakdown` command 460 | * Expressions defined as `--hook-config` argument should be in a jq-compatible format (e.g. `.totalHourlyCost`, `.totalMonthlyCost`) 461 | 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/). 462 | * Supported comparison operators: `<`, `<=`, `==`, `!=`, `>=`, `>`. 463 | * Most useful paths and checks: 464 | * `.totalHourlyCost` (same as `.projects[].breakdown.totalHourlyCost`) - show total hourly infra cost 465 | * `.totalMonthlyCost` (same as `.projects[].breakdown.totalMonthlyCost`) - show total monthly infra cost 466 | * `.projects[].diff.totalHourlyCost` - show the difference in hourly cost for the existing infra and tf plan 467 | * `.projects[].diff.totalMonthlyCost` - show the difference in monthly cost for the existing infra and tf plan 468 | * `.diffTotalHourlyCost` (for Infracost version 0.9.12 or newer) or `[.projects[].diff.totalMonthlyCost | select (.!=null) | tonumber] | add` (for Infracost older than 0.9.12) 469 | 470 | 4. **Docker usage**. In `docker build` or `docker run` command: 471 | * 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` 472 | * 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. 473 | 474 | ### tofu_docs 475 | 476 | 1. `tofu_docs` and `tofu_docs_without_aggregate_type_defaults` will insert/update documentation generated by [terraform-docs](https://github.com/terraform-docs/terraform-docs) framed by markers: 477 | 478 | ```txt 479 | 480 | 481 | 482 | ``` 483 | 484 | if they are present in `README.md`. 485 | 486 | 2. It is possible to pass additional arguments to shell scripts when using `tofu_docs` and `tofu_docs_without_aggregate_type_defaults`. 487 | 488 | 3. It is possible to automatically: 489 | * create a documentation file 490 | * extend existing documentation file by appending markers to the end of the file (see item 1 above) 491 | * use different filename for the documentation (default is `README.md`) 492 | * use the same insertion markers as `terraform-docs` by default. It will be default in `v2.0`. 493 | To migrate to `terraform-docs` insertion markers, run in repo root: 494 | 495 | ```bash 496 | grep -rl 'BEGINNING OF PRE-COMMIT-OPENTOFU DOCS HOOK' . | xargs sed -i 's/BEGINNING OF PRE-COMMIT-OPENTOFU DOCS HOOK/BEGIN_TF_DOCS/g' 497 | grep -rl 'END OF PRE-COMMIT-OPENTOFU DOCS HOOK' . | xargs sed -i 's/END OF PRE-COMMIT-OPENTOFU DOCS HOOK/END_TF_DOCS/g' 498 | ``` 499 | 500 | ```yaml 501 | - id: tofu_docs 502 | args: 503 | - --hook-config=--path-to-file=README.md # Valid UNIX path. I.e. ../TFDOC.md or docs/README.md etc. 504 | - --hook-config=--add-to-existing-file=true # Boolean. true or false 505 | - --hook-config=--create-file-if-not-exist=true # Boolean. true or false 506 | - --hook-config=--use-standard-markers=true # Boolean. Defaults in v1.x to false. Set to true for compatibility with terraform-docs 507 | ``` 508 | 509 | 4. You can provide [any configuration available in `tofu-docs`](https://terraform-docs.io/user-guide/configuration/) as an argument to `tofu_doc` hook, for example: 510 | 511 | ```yaml 512 | - id: tofu_docs 513 | args: 514 | - --args=--config=.terraform-docs.yml 515 | ``` 516 | 517 | > **Warning**: Avoid use `recursive.enabled: true` in config file, that can cause unexpected behavior. 518 | 519 | 5. If you need some exotic settings, it can be done too. I.e. this one generates HCL files: 520 | 521 | ```yaml 522 | - id: tofu_docs 523 | args: 524 | - tfvars hcl --output-file terraform.tfvars.model . 525 | ``` 526 | 527 | ### tofu_docs_replace (deprecated) 528 | 529 | **DEPRECATED**. Will be merged in [`tofu_docs`](#tofu_docs). 530 | 531 | `tofu_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. 532 | 533 | To replicate functionality in `tofu_docs` hook: 534 | 535 | 1. Create `.terraform-docs.yml` in the repo root with the following content: 536 | 537 | ```yaml 538 | formatter: "markdown" 539 | 540 | output: 541 | file: "README.md" 542 | mode: replace 543 | template: |- 544 | {{/** End of file fixer */}} 545 | ``` 546 | 547 | 2. Replace `tofu_docs_replace` hook config in `.pre-commit-config.yaml` with: 548 | 549 | ```yaml 550 | - id: tofu_docs 551 | args: 552 | - --args=--config=.terraform-docs.yml 553 | ``` 554 | 555 | ### tofu_fmt 556 | 557 | 1. `tofu_fmt` supports custom arguments so you can pass [supported flags](https://www.terraform.io/docs/cli/commands/fmt.html#usage). Eg: 558 | 559 | ```yaml 560 | - id: tofu_fmt 561 | args: 562 | - --args=-no-color 563 | - --args=-diff 564 | - --args=-write=false 565 | ``` 566 | 567 | ### tofu_providers_lock 568 | 569 | > **Note**: The hook requires OpenTofu 1.6.0 or later. 570 | 571 | > **Note**: The hook can invoke `tofu providers lock` that can be really slow and requires fetching metadata from remote OpenTofu registries - not all of that metadata is currently being cached by OpenTofu. 572 | 573 | >
Note: Read this if you used this hook before v1.80.0 | Planned breaking changes in v2.0 574 | > We introduced '--mode' flag for this hook. If you'd like to continue using this hook as before, please: 575 | > 576 | > * Specify `--hook-config=--mode=always-regenerate-lockfile` in `args:` 577 | > * Before `tofu_providers_lock`, add `tofu_validate` hook with `--hook-config=--retry-once-with-cleanup=true` 578 | > * Move `--tf-init-args=` to `tofu_validate` hook 579 | > 580 | > In the end, you should get config like this: 581 | > 582 | > ```yaml 583 | > - id: tofu_validate 584 | > args: 585 | > - --hook-config=--retry-once-with-cleanup=true 586 | > # - --tf-init-args=-upgrade 587 | > 588 | > - id: tofu_providers_lock 589 | > args: 590 | > - --hook-config=--mode=always-regenerate-lockfile 591 | > ``` 592 | > 593 | > Why? When v2.x will be introduced - the default mode will be changed, probably, to `only-check-is-current-lockfile-cross-platform`. 594 | > 595 | > You can check available modes for hook below. 596 | >
597 | 598 | 599 | 1. The hook can work in a few different modes: `only-check-is-current-lockfile-cross-platform` with and without [tofu_validate hook](#tofu_validate) and `always-regenerate-lockfile` - only with tofu_validate hook. 600 | 601 | * `only-check-is-current-lockfile-cross-platform` without tofu_validate - only checks that lockfile has all required SHAs for all providers already added to lockfile. 602 | 603 | ```yaml 604 | - id: tofu_providers_lock 605 | args: 606 | - --hook-config=--mode=only-check-is-current-lockfile-cross-platform 607 | ``` 608 | 609 | * `only-check-is-current-lockfile-cross-platform` with [tofu_validate hook](#tofu_validate) - make up-to-date lockfile by adding/removing providers and only then check that lockfile has all required SHAs. 610 | 611 | > **Note**: Next `tofu_validate` flag requires additional dependency to be installed: `jq`. Also, it could run another slow and time consuming command - `tofu init` 612 | 613 | ```yaml 614 | - id: tofu_validate 615 | args: 616 | - --hook-config=--retry-once-with-cleanup=true 617 | 618 | - id: tofu_providers_lock 619 | args: 620 | - --hook-config=--mode=only-check-is-current-lockfile-cross-platform 621 | ``` 622 | 623 | * `always-regenerate-lockfile` only with [tofu_validate hook](#tofu_validate) - regenerate lockfile from scratch. Can be useful for upgrading providers in lockfile to latest versions 624 | 625 | ```yaml 626 | - id: tofu_validate 627 | args: 628 | - --hook-config=--retry-once-with-cleanup=true 629 | - --tf-init-args=-upgrade 630 | 631 | - id: tofu_providers_lock 632 | args: 633 | - --hook-config=--mode=always-regenerate-lockfile 634 | ``` 635 | 636 | 637 | 3. `tofu_providers_lock` supports custom arguments: 638 | 639 | ```yaml 640 | - id: tofu_providers_lock 641 | args: 642 | - --args=-platform=windows_amd64 643 | - --args=-platform=darwin_amd64 644 | ``` 645 | 646 | 4. It may happen that OpenTofu working directory (`.terraform`) already exists but not in the best condition (eg, not initialized modules, wrong version of OpenTofu, etc.). To solve this problem, you can find and delete all `.terraform` directories in your repository: 647 | 648 | ```bash 649 | echo " 650 | function rm_tofu { 651 | find . \( -iname ".terraform*" ! -iname ".terraform-docs*" \) -print0 | xargs -0 rm -r 652 | } 653 | " >>~/.bashrc 654 | 655 | # Reload shell and use `rm_tofu` command in the repo root 656 | ``` 657 | 658 | `tofu_providers_lock` hook will try to reinitialize directories before running the `tofu providers lock` command. 659 | 660 | 5. `tofu_providers_lock` support passing custom arguments to its `tofu init`: 661 | 662 | > **Warning** - DEPRECATION NOTICE: This is available only in `no-mode` mode, which will be removed in v2.0. Please provide this keys to [`tofu_validate`](#tofu_validate) hook, which, to take effect, should be called before `tofu_providers_lock` 663 | 664 | ```yaml 665 | - id: tofu_providers_lock 666 | args: 667 | - --tf-init-args=-upgrade 668 | ``` 669 | 670 | 671 | ### tofu_tflint 672 | 673 | 1. `tofu_tflint` supports custom arguments so you can enable module inspection, enable / disable rules, etc. 674 | 675 | Example: 676 | 677 | ```yaml 678 | - id: tofu_tflint 679 | args: 680 | - --args=--module 681 | - --args=--enable-rule=terraform_documented_variables 682 | ``` 683 | 684 | 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 `tofu_tflint` hooks with the Git working directory (repo root) at run time. For example: 685 | 686 | ```yaml 687 | - id: tofu_tflint 688 | args: 689 | - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl 690 | ``` 691 | 692 | 3. By default, pre-commit-opentofu performs directory switching into the OpenTofu modules for you. If you want to delgate the directory changing to the binary - this will allow tflint to determine the full paths for error/warning messages, rather than just module relative paths. *Note: this requires `tflint>=0.44.0`.* For example: 693 | 694 | ```yaml 695 | - id: tofu_tflint 696 | args: 697 | - --hook-config=--delegate-chdir 698 | ``` 699 | 700 | 701 | ### tofu_tfsec (deprecated) 702 | 703 | **DEPRECATED**. [tfsec was replaced by trivy](https://github.com/aquasecurity/tfsec/discussions/1994), so please use [`tofu_trivy`](#tofu_trivy). 704 | 705 | 1. `tofu_tfsec` will consume modified files that pre-commit 706 | passes to it, so you can perform whitelisting of directories 707 | or files to run against via [files](https://pre-commit.com/#config-files) 708 | pre-commit flag 709 | 710 | Example: 711 | 712 | ```yaml 713 | - id: tofu_tfsec 714 | files: ^prd-infra/ 715 | ``` 716 | 717 | The above will tell pre-commit to pass down files from the `prd-infra/` folder 718 | only such that the underlying `tfsec` tool can run against changed files in this 719 | directory, ignoring any other folders at the root level 720 | 721 | 2. To ignore specific warnings, follow the convention from the 722 | [documentation](https://github.com/aquasecurity/tfsec#ignoring-warnings). 723 | 724 | Example: 725 | 726 | ```hcl 727 | resource "aws_security_group_rule" "my-rule" { 728 | type = "ingress" 729 | cidr_blocks = ["0.0.0.0/0"] #tfsec:ignore:AWS006 730 | } 731 | ``` 732 | 733 | 3. `tofu_tfsec` supports custom arguments, so you can pass supported `--no-color` or `--format` (output), `-e` (exclude checks) flags: 734 | 735 | ```yaml 736 | - id: tofu_tfsec 737 | args: 738 | - > 739 | --args=--format json 740 | --no-color 741 | -e aws-s3-enable-bucket-logging,aws-s3-specify-public-access-block 742 | ``` 743 | 744 | 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 `tofu_tfsec` hooks with Git working directory (repo root) at run time. For example: 745 | 746 | ```yaml 747 | - id: tofu_tfsec 748 | args: 749 | - --args=--config-file=__GIT_WORKING_DIR__/.tfsec.json 750 | ``` 751 | 752 | Otherwise, will be used files that located in sub-folders: 753 | 754 | ```yaml 755 | - id: tofu_tfsec 756 | args: 757 | - --args=--config-file=.tfsec.json 758 | ``` 759 | 760 | ### tofu_trivy 761 | 762 | 1. `tofu_trivy` will consume modified files that pre-commit 763 | passes to it, so you can perform whitelisting of directories 764 | or files to run against via [files](https://pre-commit.com/#config-files) 765 | pre-commit flag 766 | 767 | Example: 768 | 769 | ```yaml 770 | - id: tofu_trivy 771 | files: ^prd-infra/ 772 | ``` 773 | 774 | The above will tell pre-commit to pass down files from the `prd-infra/` folder 775 | only such that the underlying `trivy` tool can run against changed files in this 776 | directory, ignoring any other folders at the root level 777 | 778 | 2. To ignore specific warnings, follow the convention from the 779 | [documentation](https://aquasecurity.github.io/trivy/latest/docs/configuration/filtering/). 780 | 781 | Example: 782 | 783 | ```hcl 784 | #trivy:ignore:AVD-AWS-0107 785 | #trivy:ignore:AVD-AWS-0124 786 | resource "aws_security_group_rule" "my-rule" { 787 | type = "ingress" 788 | cidr_blocks = ["0.0.0.0/0"] 789 | } 790 | ``` 791 | 792 | 3. `tofu_trivy` supports custom arguments, so you can pass supported `--format` (output), `--skip-dirs` (exclude directories) and other flags: 793 | 794 | ```yaml 795 | - id: tofu_trivy 796 | args: 797 | - > 798 | --args=--format json 799 | --skip-dirs="**/.terragrunt-cache" 800 | ``` 801 | 802 | ### tofu_validate 803 | 804 | 1. `tofu_validate` supports custom arguments so you can pass supported `-no-color` or `-json` flags: 805 | 806 | ```yaml 807 | - id: tofu_validate 808 | args: 809 | - --args=-json 810 | - --args=-no-color 811 | ``` 812 | 813 | 2. `tofu_validate` also supports passing custom arguments to its `tofu init`: 814 | 815 | ```yaml 816 | - id: tofu_validate 817 | args: 818 | - --tf-init-args=-upgrade 819 | - --tf-init-args=-lockfile=readonly 820 | ``` 821 | 822 | 3. It may happen that OpenTofu working directory (`.terraform`) already exists but not in the best condition (eg, not initialized modules, wrong version of OpenTofu, etc.). To solve this problem, you can delete broken `.terraform` directories in your repository: 823 | 824 | **Option 1** 825 | 826 | ```yaml 827 | - id: tofu_validate 828 | args: 829 | - --hook-config=--retry-once-with-cleanup=true # Boolean. true or false 830 | ``` 831 | 832 | > **Note**: The flag requires additional dependency to be installed: `jq`. 833 | 834 | > **Note**: Reinit can be very slow and require downloading data from remote OpenTofu registries, and not all of that downloaded data or meta-data is currently being cached by OpenTofu. 835 | 836 | When `--retry-once-with-cleanup=true`, in each failed directory the cached modules and providers from the `.terraform` directory will be deleted, before retrying once more. To avoid unnecessary deletion of this directory, the cleanup and retry will only happen if OpenTofu produces any of the following error messages: 837 | 838 | * "Missing or corrupted provider plugins" 839 | * "Module source has changed" 840 | * "Module version requirements have changed" 841 | * "Module not installed" 842 | * "Could not load plugin" 843 | 844 | **Warning**: When using `--retry-once-with-cleanup=true`, problematic `.terraform/modules/` and `.terraform/providers/` directories will be recursively deleted without prompting for consent. Other files and directories will not be affected, such as the `.terraform/environment` file. 845 | 846 | **Option 2** 847 | 848 | An alternative solution is to find and delete all `.terraform` directories in your repository: 849 | 850 | ```bash 851 | echo " 852 | function rm_tofu { 853 | find . \( -iname ".terraform*" ! -iname ".terraform-docs*" \) -print0 | xargs -0 rm -r 854 | } 855 | " >>~/.bashrc 856 | 857 | # Reload shell and use `rm_tofu` command in the repo root 858 | ``` 859 | 860 | `tofu_validate` hook will try to reinitialize them before running the `tofu validate` command. 861 | 862 | **Warning**: If you use OpenTofu workspaces, DO NOT use this option ([details](https://github.com/tofuutils/pre-commit-opentofu/issues/203#issuecomment-918791847)). Consider the first option, or wait for [`force-init`](https://github.com/tofuutils/pre-commit-opentofu/issues/224) option implementation. 863 | 864 | 4. `tofu_validate` in a repo with OpenTofu module, written using OpenTofu 1.6.0+ and which uses provider `configuration_aliases` ([Provider Aliases Within Modules](https://www.terraform.io/language/modules/develop/providers#provider-aliases-within-modules)), errors out. 865 | 866 | When running the hook against OpenTofu code where you have provider `configuration_aliases` defined in a `required_providers` configuration block, OpenTofu will throw an error like: 867 | 868 | > Error: Provider configuration not present 869 | > 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 870 | > 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. 871 | 872 | This is a [known issue](https://github.com/hashicorp/terraform/issues/28490) with OpenTofu and how providers are initialized in OpenTofu 1.6.0 and later. To work around this you can add an `exclude` parameter to the configuration of `tofu_validate` hook like this: 873 | 874 | ```yaml 875 | - id: tofu_validate 876 | exclude: '^[^/]+$' 877 | ``` 878 | 879 | 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/[^/]+$`. 880 | 881 | 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: 882 | 883 | ```bash 884 | terraform-config-inspect --json . | jq -r ' 885 | [.required_providers[].aliases] 886 | | flatten 887 | | del(.[] | select(. == null)) 888 | | reduce .[] as $entry ( 889 | {}; 890 | .provider[$entry.name] //= [] | .provider[$entry.name] += [{"alias": $entry.alias}] 891 | ) 892 | ' | tee aliased-providers.tf.json 893 | ``` 894 | 895 | 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: 896 | 897 | ```yaml 898 | - repos: 899 | - repo: local 900 | hooks: 901 | - id: generate-tofu-providers 902 | name: generate-tofu-providers 903 | require_serial: true 904 | entry: .generate-providers.sh 905 | language: script 906 | files: \.tf(vars)?$ 907 | pass_filenames: false 908 | 909 | - repo: https://github.com/pre-commit/pre-commit-hooks 910 | [...] 911 | ``` 912 | 913 | > 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. 914 | 915 | ### tofu_wrapper_module_for_each 916 | 917 | `tofu_wrapper_module_for_each` generates module wrappers for OpenTofu modules (useful for Terragrunt where `for_each` is not supported). When using this hook without arguments it will create wrappers for the root module and all modules available in "modules" directory. 918 | 919 | You may want to customize some of the options: 920 | 921 | 1. `--module-dir=...` - Specify a single directory to process. Values: "." (means just root module), "modules/iam-user" (a single module), or empty (means include all submodules found in "modules/*"). 922 | 2. `--module-repo-org=...` - Module repository organization (e.g. "terraform-aws-modules"). 923 | 3. `--module-repo-shortname=...` - Short name of the repository (e.g. "s3-bucket"). 924 | 4. `--module-repo-provider=...` - Name of the repository provider (e.g. "aws" or "google"). 925 | 926 | Sample configuration: 927 | 928 | ```yaml 929 | - id: tofu_wrapper_module_for_each 930 | args: 931 | - --args=--module-dir=. # Process only root module 932 | - --args=--dry-run # No files will be created/updated 933 | - --args=--verbose # Verbose output 934 | ``` 935 | 936 | **If you use hook inside Docker:** 937 | The `tofu_wrapper_module_for_each` hook attempts to determine the module's short name to be inserted into the generated `README.md` files for the `source` URLs. Since the container uses a bind mount at a static location, it can cause this short name to be incorrect. 938 | If the generated name is incorrect, set them by providing the `module-repo-shortname` option to the hook: 939 | 940 | ```yaml 941 | - id: tofu_wrapper_module_for_each 942 | args: 943 | - '--args=--module-repo-shortname=ec2-instance' 944 | ``` 945 | 946 | ### terrascan 947 | 948 | 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: 949 | 950 | ```yaml 951 | - id: terrascan 952 | args: 953 | - --args=--non-recursive # avoids scan errors on subdirectories without OpenTofu config files 954 | - --args=--policy-type=azure 955 | ``` 956 | 957 | See the `terrascan run -h` command line help for available options. 958 | 959 | 2. Use the `--args=--verbose` parameter to see the rule ID in the scanning output. Useful to skip validations. 960 | 3. Use `--skip-rules="ruleID1,ruleID2"` parameter to skip one or more rules globally while scanning (e.g.: `--args=--skip-rules="ruleID1,ruleID2"`). 961 | 4. Use the syntax `#ts:skip=RuleID optional_comment` inside a resource to skip the rule for that resource. 962 | 963 | ### tfupdate 964 | 965 | 1. Out of the box `tfupdate` will pin the OpenTofu version: 966 | 967 | ```yaml 968 | - id: tfupdate 969 | name: Autoupdate OpenTofu versions 970 | ``` 971 | 972 | 2. If you'd like to pin providers, etc., use custom arguments, i.e `provider=PROVIDER_NAME`: 973 | 974 | ```yaml 975 | - id: tfupdate 976 | name: Autoupdate AWS provider versions 977 | args: 978 | - --args=provider aws # Will be pined to latest version 979 | 980 | - id: tfupdate 981 | name: Autoupdate Helm provider versions 982 | args: 983 | - --args=provider helm 984 | - --args=--version 2.5.0 # Will be pined to specified version 985 | ``` 986 | 987 | Check [`tfupdate` usage instructions](https://github.com/minamijoyo/tfupdate#usage) for other available options and usage examples. 988 | No need to pass `--recursive .` as it is added automatically. 989 | 990 | ## Docker Usage 991 | 992 | ### File Permissions 993 | 994 | A mismatch between the Docker container's user and the local repository file ownership can cause permission issues in the repository where `pre-commit` is run. The container runs as the `root` user by default, and uses a `tools/entrypoint.sh` script to assume a user ID and group ID if specified by the environment variable `USERID`. 995 | 996 | The [recommended command](#4-run) to run the Docker container is: 997 | 998 | ```bash 999 | TAG=latest 1000 | docker run -e "USERID=$(id -u):$(id -g)" -v $(pwd):/lint -w /lint tofuutils/pre-commit-opentofu:$TAG run -a 1001 | ``` 1002 | 1003 | which uses your current session's user ID and group ID to set the variable in the run command. Without this setting, you may find files and directories owned by `root` in your local repository. 1004 | 1005 | If the local repository is using a different user or group for permissions, you can modify the `USERID` to the user ID and group ID needed. **Do not use the username or groupname in the environment variable, as it has no meaning in the container.** You can get the current directory's owner user ID and group ID from the 3rd (user) and 4th (group) columns in `ls` output: 1006 | 1007 | ```bash 1008 | $ ls -aldn . 1009 | drwxr-xr-x 9 1000 1000 4096 Sep 1 16:23 . 1010 | ``` 1011 | 1012 | ### Download OpenTofu modules from private GitHub repositories 1013 | 1014 | If you use a private Git repository as your OpenTofu module source, you are required to authenticate to GitHub using a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). 1015 | 1016 | When running pre-commit on Docker, both locally or on CI, you need to configure the [~/.netrc](https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) file, which contains login and initialization information used by the auto-login process. 1017 | 1018 | This can be achieved by firstly creating the `~/.netrc` file including your `GITHUB_PAT` and `GITHUB_SERVER_HOSTNAME` 1019 | 1020 | ```bash 1021 | # set GH values (replace with your own values) 1022 | GITHUB_PAT=ghp_bl481aBlabl481aBla 1023 | GITHUB_SERVER_HOSTNAME=github.com 1024 | 1025 | # create .netrc file 1026 | echo -e "machine $GITHUB_SERVER_HOSTNAME\n\tlogin $GITHUB_PAT" >> ~/.netrc 1027 | ``` 1028 | 1029 | The `~/.netrc` file will look similar to the following: 1030 | 1031 | ``` 1032 | machine github.com 1033 | login ghp_bl481aBlabl481aBla 1034 | ``` 1035 | 1036 | > **Note**: The value of `GITHUB_SERVER_HOSTNAME` can also refer to a GitHub Enterprise server (i.e. `github.my-enterprise.com`). 1037 | 1038 | Finally, you can execute `docker run` with an additional volume mount so that the `~/.netrc` is accessible within the container 1039 | 1040 | ```bash 1041 | # run pre-commit-opentofu with docker 1042 | # adding volume for .netrc file 1043 | # .netrc needs to be in /root/ dir 1044 | docker run --rm -e "USERID=$(id -u):$(id -g)" -v ~/.netrc:/root/.netrc -v $(pwd):/lint -w /lint tofuutils/pre-commit-opentofu:latest run -a 1045 | ``` 1046 | 1047 | ## Authors 1048 | 1049 | This repository is managed by [Alexander Sharov](https://github.com/kvendingoldo), [Nikolay Mishin](https://github.com/Nmishin), and [Anastasiia Kozlova](https://github.com/anastasiiakozlova245) with help from these awesome contributors: 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | Star History Chart 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | ## License 1068 | 1069 | MIT licensed. See [LICENSE](LICENSE) for full details. 1070 | --------------------------------------------------------------------------------