├── .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 | [](https://github.com/tofuutils/pre-commit-opentofu/releases)  [](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 |
1062 |
1063 |
1064 |
1065 |
1066 |
1067 | ## License
1068 |
1069 | MIT licensed. See [LICENSE](LICENSE) for full details.
1070 |
--------------------------------------------------------------------------------