diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c6ee553 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +--- +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: / + schedule: + interval: daily + time: "11:00" + commit-message: + prefix: "gh-actions:" diff --git a/.github/workflows/build-image-test.yaml b/.github/workflows/build-image-test.yaml index f83e11e..c7223f0 100644 --- a/.github/workflows/build-image-test.yaml +++ b/.github/workflows/build-image-test.yaml @@ -9,13 +9,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 - name: Get changed Dockerfile id: changed-files-specific - uses: tj-actions/changed-files@2c85495a7bb72f2734cb5181e29b2ee5e08e61f7 # v13.1 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | Dockerfile @@ -52,7 +52,7 @@ jobs: - name: Dive - check image for waste files if: steps.changed-files-specific.outputs.any_changed == 'true' - uses: MaxymVlasov/dive-action@0035999cae50d4ef657ac94be84f01812aa192a5 # v0.1.0 + uses: MaxymVlasov/dive-action@fafb796951b322cc4926b8a5eafda89ab9de8edf # v1.5.1 with: image: ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }} config-file: ${{ github.workspace }}/.github/.dive-ci.yaml diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 3f79fb2..a79e218 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -13,27 +13,28 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@v5.0.1 + - name: Set up QEMU - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - - name: Login to GitHub Container Registry - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + uses: docker/setup-buildx-action@v3 + + - name: Login to ghcr.io + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set tag for image run: | echo IMAGE_TAG=$([ ${{ github.ref_type }} == 'tag' ] && echo ${{ github.ref_name }} || echo 'latest') >> $GITHUB_ENV - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - - - name: Build and Push release + - name: Build and Push release to ghcr.io if: github.event_name != 'schedule' - uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 + uses: docker/build-push-action@v5 with: context: . build-args: | @@ -48,9 +49,9 @@ jobs: secrets: | "github_token=${{ secrets.GITHUB_TOKEN }}" - - name: Build and Push nightly + - name: Build and Push nightly to ghcr.io if: github.event_name == 'schedule' - uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 + uses: docker/build-push-action@v5 with: context: . build-args: | @@ -63,3 +64,37 @@ jobs: provenance: false secrets: | "github_token=${{ secrets.GITHUB_TOKEN }}" + + - name: Login to DockerHub Container Registry + uses: docker/login-action@v4 + with: + registry: registry.hub.docker.com + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build and Push release to DockerHub + if: github.event_name != 'schedule' + uses: docker/build-push-action@v5 + with: + context: . + build-args: | + INSTALL_ALL=true + platforms: linux/amd64,linux/arm64 + push: true + tags: | + registry.hub.docker.com/tofuutils/pre-commit-opentofu:${{ env.IMAGE_TAG }} + registry.hub.docker.com/tofuutils/pre-commit-opentofu:latest + provenance: false + + - name: Build and Push nightly to DockerHub + if: github.event_name == 'schedule' + uses: docker/build-push-action@v5 + with: + context: . + build-args: | + INSTALL_ALL=true + platforms: linux/amd64,linux/arm64 + push: true + tags: | + registry.hub.docker.com/tofuutils/pre-commit-opentofu:nightly + provenance: false diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 7a8b642..3d86d51 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -14,7 +14,7 @@ jobs: steps: # Please look up the latest version from # https://github.com/amannn/action-semantic-pull-request/releases - - uses: amannn/action-semantic-pull-request@e9fabac35e210fea40ca5b14c0da95a099eff26f # v5.4.0 + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 1085384..b0566cf 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -6,7 +6,7 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - run: | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* @@ -27,20 +27,24 @@ jobs: sudo apt update && sudo apt install shellcheck - name: Install hadolint + env: + GH_TOKEN: ${{ github.token }} run: | - curl -L "$(curl -s https://api.github.com/repos/hadolint/hadolint/releases/latest | grep -o -E -m 1 "https://.+?/hadolint-Linux-x86_64")" > hadolint \ - && chmod +x hadolint && sudo mv hadolint /usr/bin/ + gh release download v2.14.0 --repo hadolint/hadolint --pattern "hadolint-linux-x86_64" + mv hadolint-linux-x86_64 hadolint + chmod +x hadolint + sudo mv hadolint /usr/bin/ # Need to success pre-commit fix push - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} # Skip tofu_tflint which interferes to commit pre-commit auto-fixes - - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.9' - name: Execute pre-commit - uses: pre-commit/action@9b88afc9cd57fd75b655d5c71bd38146d07135fe # v2.0.3 + uses: pre-commit/action@576ff52938d158a24ac7e009dfa94b1455e7df99 env: SKIP: no-commit-to-branch,hadolint with: @@ -49,7 +53,7 @@ jobs: # Run only skipped checks - name: Execute pre-commit check that have no auto-fixes if: always() - uses: pre-commit/action@9b88afc9cd57fd75b655d5c71bd38146d07135fe # v2.0.3 + uses: pre-commit/action@576ff52938d158a24ac7e009dfa94b1455e7df99 env: SKIP: check-added-large-files,check-merge-conflict,check-vcs-permalinks,forbid-new-submodules,no-commit-to-branch,end-of-file-fixer,trailing-whitespace,check-yaml,check-merge-conflict,check-executables-have-shebangs,check-case-conflict,mixed-line-ending,detect-aws-credentials,detect-private-key,shfmt,shellcheck with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4aa0df1..35b8f62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,13 +18,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false fetch-depth: 0 - name: Release - uses: cycjimmy/semantic-release-action@61680d0e9b02ff86f5648ade99e01be17f0260a4 # v4.0.0 + uses: cycjimmy/semantic-release-action@b12c8f6015dc215fe37bc154d4ad456dd3833c90 # v6.0.0 with: semantic_version: 18.0.0 extra_plugins: | diff --git a/.github/workflows/stale-actions.yaml b/.github/workflows/stale-actions.yaml index 32f08e7..d797c08 100644 --- a/.github/workflows/stale-actions.yaml +++ b/.github/workflows/stale-actions.yaml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 + - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} # Staling issues and PR's diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 17fc5a6..e801df2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v6.0.0 hooks: # Git style - id: check-added-large-files diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index d92c532..d60a3e5 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,7 +4,7 @@ entry: hooks/infracost_breakdown.sh language: script require_serial: true - files: \.(tf(vars)?|hcl)$ + files: \.(tf|tofu|tfvars|hcl)$ exclude: \.terraform\/.*$ - id: tofu_fmt @@ -12,34 +12,38 @@ description: Rewrites all OpenTofu configuration files to a canonical format. entry: hooks/tofu_fmt.sh language: script - files: (\.tf|\.tfvars)$ + files: \.(tf|tofu|tfvars|(tftest|tofutest|tfmock|tfquery)\.hcl)$ exclude: \.terraform\/.*$ - id: tofu_docs name: OpenTofu docs - description: Inserts input and output documentation into README.md (using terraform-docs). + description: + Inserts input and output documentation into README.md (using + terraform-docs). require_serial: true entry: hooks/tofu_docs.sh language: script - files: (\.tf|\.terraform\.lock\.hcl)$ + files: \.(tf|tofu|terraform\.lock\.hcl)$ exclude: \.terraform\/.*$ - id: tofu_docs_without_aggregate_type_defaults name: OpenTofu docs (without aggregate type defaults) - description: Inserts input and output documentation into README.md (using terraform-docs). Identical to terraform_docs. + description: + Inserts input and output documentation into README.md (using + terraform-docs). Identical to terraform_docs. require_serial: true entry: hooks/tofu_docs.sh language: script - files: (\.tf)$ + files: \.(tf|tofu)$ exclude: \.terraform\/.*$ - id: tofu_docs_replace name: OpenTofu docs (overwrite README.md) description: Overwrite content of README.md with terraform-docs. require_serial: true - entry: terraform_docs_replace + entry: hooks/tofu_docs_replace.py language: python - files: (\.tf)$ + files: \.(tf|tofu)$ exclude: \.terraform\/.*$ - id: tofu_validate @@ -48,7 +52,7 @@ require_serial: true entry: hooks/tofu_validate.sh language: script - files: (\.tf|\.tfvars)$ + files: \.(tf|tofu|tfvars|terraform\.lock\.hcl)$ exclude: \.terraform\/.*$ - id: tofu_providers_lock @@ -66,12 +70,13 @@ require_serial: true entry: hooks/tofu_tflint.sh language: script - files: (\.tf|\.tfvars)$ + files: \.(tf|tofu|tfvars)$ exclude: \.terraform\/.*$ - id: terragrunt_fmt name: Terragrunt fmt - description: Rewrites all Terragrunt configuration files to a canonical format. + description: + Rewrites all Terragrunt configuration files to a canonical format. entry: hooks/terragrunt_fmt.sh language: script files: (\.hcl)$ @@ -85,20 +90,30 @@ files: (\.hcl)$ exclude: \.terraform\/.*$ +- id: terragrunt_validate_inputs + name: Terragrunt validate inputs + description: Validates Terragrunt unused and undefined inputs. + entry: hooks/terragrunt_validate_inputs.sh + language: script + files: (\.hcl)$ + exclude: \.terraform\/.*$ + - id: tofu_tfsec name: OpenTofu validate with tfsec (deprecated, use "tofu_trivy") - description: Static analysis of OpenTofu templates to spot potential security issues. + description: + Static analysis of OpenTofu templates to spot potential security issues. require_serial: true entry: hooks/tofu_tfsec.sh - files: \.tf(vars)?$ + files: \.(tf|tofu|tfvars)$ language: script - id: tofu_trivy name: OpenTofu validate with trivy - description: Static analysis of OpenTofu templates to spot potential security issues. + description: + Static analysis of OpenTofu templates to spot potential security issues. require_serial: true entry: hooks/tofu_trivy.sh - files: \.tf(vars)?$ + files: \.(tf|tofu|tfvars)$ language: script - id: checkov @@ -108,7 +123,7 @@ language: python pass_filenames: false always_run: false - files: \.tf$ + files: \.(tf|tofu)$ exclude: \.terraform\/.*$ require_serial: true @@ -118,7 +133,7 @@ entry: hooks/tofu_checkov.sh language: script always_run: false - files: \.tf$ + files: \.(tf|tofu)$ exclude: \.terraform\/.*$ require_serial: true @@ -130,7 +145,7 @@ pass_filenames: false always_run: false require_serial: true - files: \.tf$ + files: \.(tf|tofu)$ exclude: \.terraform\/.*$ - id: terrascan @@ -138,7 +153,7 @@ description: Runs terrascan on OpenTofu templates. language: script entry: hooks/terrascan.sh - files: \.tf$ + files: \.(tf|tofu)$ exclude: \.terraform\/.*$ require_serial: true @@ -149,5 +164,5 @@ entry: hooks/tfupdate.sh args: - --args=terraform - files: \.tf$ + files: \.(tf|tofu)$ require_serial: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 8febd9c..2112ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,83 @@ All notable changes to this project will be documented in this file. +# [2.4.0](https://github.com/tofuutils/pre-commit-opentofu/compare/v2.3.0...v2.4.0) (2026-05-25) + + +### Bug Fixes + +* expand file extension patterns for tofu hooks ([451aaa5](https://github.com/tofuutils/pre-commit-opentofu/commit/451aaa59b552eb3913629c835af8b6b568aab120)) + + +### Features + +* support .tofu file extension and update hooks/tools ([9624cc8](https://github.com/tofuutils/pre-commit-opentofu/commit/9624cc8f24177378449203194b4f0ee71bc6c1a0)) + +# [2.3.0](https://github.com/tofuutils/pre-commit-opentofu/compare/v2.2.2...v2.3.0) (2026-04-21) + + +### Features + +* add terragrunt_validate_inputs hook ([0ca9eca](https://github.com/tofuutils/pre-commit-opentofu/commit/0ca9eca3823420f31d09c62bf1672bea03ca3e07)) + +## [2.2.2](https://github.com/tofuutils/pre-commit-opentofu/compare/v2.2.1...v2.2.2) (2025-10-22) + + +### Bug Fixes + +* replace deprecated hclfmt with hcl format command ([f1a589b](https://github.com/tofuutils/pre-commit-opentofu/commit/f1a589bd124b277cc02fcbf04ee05017fb8822c0)) + +## [2.2.1](https://github.com/tofuutils/pre-commit-opentofu/compare/v2.2.0...v2.2.1) (2025-06-04) + + +### Bug Fixes + +* make infracost_breakdown.sh compatible with bash 3.2 (macOS) ([df886fa](https://github.com/tofuutils/pre-commit-opentofu/commit/df886fa772e7d1eedf5603327c0cf02968e7d779)) +* 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)) + +# [2.2.0](https://github.com/tofuutils/pre-commit-opentofu/compare/v2.1.0...v2.2.0) (2025-03-29) + + +### Features + +* make release ([e625db1](https://github.com/tofuutils/pre-commit-opentofu/commit/e625db13ec285e132f43cdf6e5aa3f3272e45451)) + +# [2.1.0](https://github.com/tofuutils/pre-commit-opentofu/compare/v2.0.0...v2.1.0) (2024-10-16) + + +### Features + +* support .tofu files ([#6](https://github.com/tofuutils/pre-commit-opentofu/issues/6)) ([e059c58](https://github.com/tofuutils/pre-commit-opentofu/commit/e059c5859bceddf1ca018f55851f6940ad51f1c2)) + +# [2.0.0](https://github.com/tofuutils/pre-commit-opentofu/compare/v1.0.4...v2.0.0) (2024-09-25) + + +### Features + +* **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)) + + +### BREAKING CHANGES + +* **tofu:** The previous behavior of the application when the tofu binary was missing may have caused unexpected crashes." + +## [1.0.4](https://github.com/tofuutils/pre-commit-opentofu/compare/v1.0.3...v1.0.4) (2024-09-21) + + +### Bug Fixes + +* docker image reference in README.md ([7b04f0c](https://github.com/tofuutils/pre-commit-opentofu/commit/7b04f0c24940f1642c8f599bfd0794dd46b0b274)) +* docker image reference in README.md ([f9b71fe](https://github.com/tofuutils/pre-commit-opentofu/commit/f9b71fe08fedd4ceb23ced6fe2171edf24add290)) +* dockerhub ([0fac591](https://github.com/tofuutils/pre-commit-opentofu/commit/0fac59197f2f2cb4bc417917e5adb6ac92a20b7a)) +* entry for tofu_docs_replace ([f146463](https://github.com/tofuutils/pre-commit-opentofu/commit/f146463ac8effcfa441f3f6b21e811095f0da73c)) + +## [1.0.2](https://github.com/tofuutils/pre-commit-opentofu/compare/v1.0.1...v1.0.2) (2024-03-08) + + +### Bug Fixes + +* remove obsolete terraform checks and awk file hack ([97cba7a](https://github.com/tofuutils/pre-commit-opentofu/commit/97cba7a646996c7cae3719f1b6241d47da5882d9)) + ## [1.0.1](https://github.com/tofuutils/pre-commit-opentofu/compare/v1.0.0...v1.0.1) (2024-03-07) diff --git a/Dockerfile b/Dockerfile index 0f7426d..6699cb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,22 +10,22 @@ RUN apk add --no-cache \ curl=~8 && \ # Upgrade packages for be able get latest Checkov python3 -m pip install --no-cache-dir --upgrade \ - pip \ - setuptools + pip \ + setuptools ARG PRE_COMMIT_VERSION=${PRE_COMMIT_VERSION:-latest} -ARG TOFU_VERSION=${TOFU_VERSION:-1.6.1} +ARG TOFU_VERSION=${TOFU_VERSION:-1.9.0} # Install pre-commit RUN [ ${PRE_COMMIT_VERSION} = "latest" ] && pip3 install --no-cache-dir pre-commit \ || pip3 install --no-cache-dir pre-commit==${PRE_COMMIT_VERSION} - RUN curl -LO https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip \ - && curl -LO https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_SHA256SUMS \ - && [ $(sha256sum "tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip" | cut -f 1 -d ' ') = "$(grep "tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip" tofu_*_SHA256SUMS | cut -f 1 -d ' ')" ] \ - && unzip tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip \ - && mv tofu /usr/bin/tofu + && curl -LO https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_SHA256SUMS \ + && [ "$(sha256sum "tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip" | cut -f 1 -d ' ')" = "$(grep "tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip" tofu_*_SHA256SUMS | cut -f 1 -d ' ')" ] \ + && unzip tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip -d /usr/bin/ \ + && rm "tofu_${TOFU_VERSION}_${TARGETOS}_${TARGETARCH}.zip" \ + && rm "tofu_${TOFU_VERSION}_SHA256SUMS" # # Install tools @@ -47,18 +47,18 @@ ARG HCLEDIT_VERSION=${HCLEDIT_VERSION:-false} # specified in step below ARG INSTALL_ALL=${INSTALL_ALL:-false} RUN if [ "$INSTALL_ALL" != "false" ]; then \ - echo "export CHECKOV_VERSION=latest" >> /.env && \ - echo "export INFRACOST_VERSION=latest" >> /.env && \ - echo "export TERRAFORM_DOCS_VERSION=latest" >> /.env && \ - echo "export TERRAGRUNT_VERSION=latest" >> /.env && \ - echo "export TERRASCAN_VERSION=latest" >> /.env && \ - echo "export TFLINT_VERSION=latest" >> /.env && \ - echo "export TFSEC_VERSION=latest" >> /.env && \ - echo "export TRIVY_VERSION=latest" >> /.env && \ - echo "export TFUPDATE_VERSION=latest" >> /.env && \ - echo "export HCLEDIT_VERSION=latest" >> /.env \ + echo "export CHECKOV_VERSION=latest" >> /.env && \ + echo "export INFRACOST_VERSION=latest" >> /.env && \ + echo "export TERRAFORM_DOCS_VERSION=latest" >> /.env && \ + echo "export TERRAGRUNT_VERSION=latest" >> /.env && \ + echo "export TERRASCAN_VERSION=latest" >> /.env && \ + echo "export TFLINT_VERSION=latest" >> /.env && \ + echo "export TFSEC_VERSION=latest" >> /.env && \ + echo "export TRIVY_VERSION=latest" >> /.env && \ + echo "export TFUPDATE_VERSION=latest" >> /.env && \ + echo "export HCLEDIT_VERSION=latest" >> /.env \ ; else \ - touch /.env \ + touch /.env \ ; fi @@ -66,10 +66,10 @@ RUN if [ "$INSTALL_ALL" != "false" ]; then \ RUN . /.env && \ if [ "$CHECKOV_VERSION" != "false" ]; then \ ( \ - apk add --no-cache gcc=~12 libffi-dev=~3 musl-dev=~1; \ - [ "$CHECKOV_VERSION" = "latest" ] && pip3 install --no-cache-dir checkov \ - || pip3 install --no-cache-dir checkov==${CHECKOV_VERSION}; \ - apk del gcc libffi-dev musl-dev \ + apk add --no-cache gcc=~12 libffi-dev=~3 musl-dev=~1; \ + [ "$CHECKOV_VERSION" = "latest" ] && pip3 install --no-cache-dir checkov \ + || pip3 install --no-cache-dir checkov==${CHECKOV_VERSION}; \ + apk del gcc libffi-dev musl-dev \ ) \ ; fi @@ -77,9 +77,9 @@ RUN . /.env && \ RUN . /.env && \ if [ "$INFRACOST_VERSION" != "false" ]; then \ ( \ - INFRACOST_RELEASES="https://api.github.com/repos/infracost/infracost/releases" && \ - [ "$INFRACOST_VERSION" = "latest" ] && curl -L "$(curl -s ${INFRACOST_RELEASES}/latest | grep -o -E -m 1 "https://.+?-${TARGETOS}-${TARGETARCH}.tar.gz")" > infracost.tgz \ - || curl -L "$(curl -s ${INFRACOST_RELEASES} | grep -o -E "https://.+?v${INFRACOST_VERSION}/infracost-${TARGETOS}-${TARGETARCH}.tar.gz")" > infracost.tgz \ + INFRACOST_RELEASES="https://api.github.com/repos/infracost/infracost/releases" && \ + [ "$INFRACOST_VERSION" = "latest" ] && curl -L "$(curl -s ${INFRACOST_RELEASES}/latest | grep -o -E -m 1 "https://.+?-${TARGETOS}-${TARGETARCH}.tar.gz")" > infracost.tgz \ + || curl -L "$(curl -s ${INFRACOST_RELEASES} | grep -o -E "https://.+?v${INFRACOST_VERSION}/infracost-${TARGETOS}-${TARGETARCH}.tar.gz")" > infracost.tgz \ ) && tar -xzf infracost.tgz && rm infracost.tgz && mv infracost-${TARGETOS}-${TARGETARCH} infracost \ ; fi @@ -87,9 +87,9 @@ RUN . /.env && \ RUN . /.env && \ if [ "$TERRAFORM_DOCS_VERSION" != "false" ]; then \ ( \ - TERRAFORM_DOCS_RELEASES="https://api.github.com/repos/terraform-docs/terraform-docs/releases" && \ - [ "$TERRAFORM_DOCS_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRAFORM_DOCS_RELEASES}/latest | grep -o -E -m 1 "https://.+?-${TARGETOS}-${TARGETARCH}.tar.gz")" > terraform-docs.tgz \ - || curl -L "$(curl -s ${TERRAFORM_DOCS_RELEASES} | grep -o -E "https://.+?v${TERRAFORM_DOCS_VERSION}-${TARGETOS}-${TARGETARCH}.tar.gz")" > terraform-docs.tgz \ + TERRAFORM_DOCS_RELEASES="https://api.github.com/repos/terraform-docs/terraform-docs/releases" && \ + [ "$TERRAFORM_DOCS_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRAFORM_DOCS_RELEASES}/latest | grep -o -E -m 1 "https://.+?-${TARGETOS}-${TARGETARCH}.tar.gz")" > terraform-docs.tgz \ + || curl -L "$(curl -s ${TERRAFORM_DOCS_RELEASES} | grep -o -E "https://.+?v${TERRAFORM_DOCS_VERSION}-${TARGETOS}-${TARGETARCH}.tar.gz")" > terraform-docs.tgz \ ) && tar -xzf terraform-docs.tgz terraform-docs && rm terraform-docs.tgz && chmod +x terraform-docs \ ; fi @@ -97,9 +97,9 @@ RUN . /.env && \ RUN . /.env \ && if [ "$TERRAGRUNT_VERSION" != "false" ]; then \ ( \ - TERRAGRUNT_RELEASES="https://api.github.com/repos/gruntwork-io/terragrunt/releases" && \ - [ "$TERRAGRUNT_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRAGRUNT_RELEASES}/latest | grep -o -E -m 1 "https://.+?/terragrunt_${TARGETOS}_${TARGETARCH}")" > terragrunt \ - || curl -L "$(curl -s ${TERRAGRUNT_RELEASES} | grep -o -E -m 1 "https://.+?v${TERRAGRUNT_VERSION}/terragrunt_${TARGETOS}_${TARGETARCH}")" > terragrunt \ + TERRAGRUNT_RELEASES="https://api.github.com/repos/gruntwork-io/terragrunt/releases" && \ + [ "$TERRAGRUNT_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRAGRUNT_RELEASES}/latest | grep -o -E -m 1 "https://.+?/terragrunt_${TARGETOS}_${TARGETARCH}")" > terragrunt \ + || curl -L "$(curl -s ${TERRAGRUNT_RELEASES} | grep -o -E -m 1 "https://.+?v${TERRAGRUNT_VERSION}/terragrunt_${TARGETOS}_${TARGETARCH}")" > terragrunt \ ) && chmod +x terragrunt \ ; fi @@ -111,9 +111,9 @@ RUN . /.env && \ # Convert the first letter to Uppercase OS="$(echo ${TARGETOS} | cut -c1 | tr '[:lower:]' '[:upper:]' | xargs echo -n; echo ${TARGETOS} | cut -c2-)"; \ ( \ - TERRASCAN_RELEASES="https://api.github.com/repos/tenable/terrascan/releases" && \ - [ "$TERRASCAN_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRASCAN_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${OS}_${ARCH}.tar.gz")" > terrascan.tar.gz \ - || curl -L "$(curl -s ${TERRASCAN_RELEASES} | grep -o -E "https://.+?${TERRASCAN_VERSION}_${OS}_${ARCH}.tar.gz")" > terrascan.tar.gz \ + TERRASCAN_RELEASES="https://api.github.com/repos/tenable/terrascan/releases" && \ + [ "$TERRASCAN_VERSION" = "latest" ] && curl -L "$(curl -s ${TERRASCAN_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${OS}_${ARCH}.tar.gz")" > terrascan.tar.gz \ + || curl -L "$(curl -s ${TERRASCAN_RELEASES} | grep -o -E "https://.+?${TERRASCAN_VERSION}_${OS}_${ARCH}.tar.gz")" > terrascan.tar.gz \ ) && tar -xzf terrascan.tar.gz terrascan && rm terrascan.tar.gz && \ ./terrascan init \ ; fi @@ -122,9 +122,9 @@ RUN . /.env && \ RUN . /.env && \ if [ "$TFLINT_VERSION" != "false" ]; then \ ( \ - TFLINT_RELEASES="https://api.github.com/repos/terraform-linters/tflint/releases" && \ - [ "$TFLINT_VERSION" = "latest" ] && curl -L "$(curl -s ${TFLINT_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${TARGETOS}_${TARGETARCH}.zip")" > tflint.zip \ - || curl -L "$(curl -s ${TFLINT_RELEASES} | grep -o -E "https://.+?/v${TFLINT_VERSION}/tflint_${TARGETOS}_${TARGETARCH}.zip")" > tflint.zip \ + TFLINT_RELEASES="https://api.github.com/repos/terraform-linters/tflint/releases" && \ + [ "$TFLINT_VERSION" = "latest" ] && curl -L "$(curl -s ${TFLINT_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${TARGETOS}_${TARGETARCH}.zip")" > tflint.zip \ + || curl -L "$(curl -s ${TFLINT_RELEASES} | grep -o -E "https://.+?/v${TFLINT_VERSION}/tflint_${TARGETOS}_${TARGETARCH}.zip")" > tflint.zip \ ) && unzip tflint.zip && rm tflint.zip \ ; fi @@ -132,9 +132,9 @@ RUN . /.env && \ RUN . /.env && \ if [ "$TFSEC_VERSION" != "false" ]; then \ ( \ - TFSEC_RELEASES="https://api.github.com/repos/aquasecurity/tfsec/releases" && \ - [ "$TFSEC_VERSION" = "latest" ] && curl -L "$(curl -s ${TFSEC_RELEASES}/latest | grep -o -E -m 1 "https://.+?/tfsec-${TARGETOS}-${TARGETARCH}")" > tfsec \ - || curl -L "$(curl -s ${TFSEC_RELEASES} | grep -o -E -m 1 "https://.+?v${TFSEC_VERSION}/tfsec-${TARGETOS}-${TARGETARCH}")" > tfsec \ + TFSEC_RELEASES="https://api.github.com/repos/aquasecurity/tfsec/releases" && \ + [ "$TFSEC_VERSION" = "latest" ] && curl -L "$(curl -s ${TFSEC_RELEASES}/latest | grep -o -E -m 1 "https://.+?/tfsec-${TARGETOS}-${TARGETARCH}")" > tfsec \ + || curl -L "$(curl -s ${TFSEC_RELEASES} | grep -o -E -m 1 "https://.+?v${TFSEC_VERSION}/tfsec-${TARGETOS}-${TARGETARCH}")" > tfsec \ ) && chmod +x tfsec \ ; fi @@ -143,9 +143,9 @@ RUN . /.env && \ if [ "$TRIVY_VERSION" != "false" ]; then \ if [ "$TARGETARCH" != "amd64" ]; then ARCH="$TARGETARCH"; else ARCH="64bit"; fi; \ ( \ - TRIVY_RELEASES="https://api.github.com/repos/aquasecurity/trivy/releases" && \ - [ "$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 \ - || curl -L "$(curl -s ${TRIVY_RELEASES} | grep -o -E -i -m 1 "https://.+?/v${TRIVY_VERSION}/trivy_.+?_${TARGETOS}-${ARCH}.tar.gz")" > trivy.tar.gz \ + TRIVY_RELEASES="https://api.github.com/repos/aquasecurity/trivy/releases" && \ + [ "$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 \ + || curl -L "$(curl -s ${TRIVY_RELEASES} | grep -o -E -i -m 1 "https://.+?/v${TRIVY_VERSION}/trivy_.+?_${TARGETOS}-${ARCH}.tar.gz")" > trivy.tar.gz \ ) && tar -xzf trivy.tar.gz trivy && rm trivy.tar.gz \ ; fi @@ -153,9 +153,9 @@ RUN . /.env && \ RUN . /.env && \ if [ "$TFUPDATE_VERSION" != "false" ]; then \ ( \ - TFUPDATE_RELEASES="https://api.github.com/repos/minamijoyo/tfupdate/releases" && \ - [ "$TFUPDATE_VERSION" = "latest" ] && curl -L "$(curl -s ${TFUPDATE_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${TARGETOS}_${TARGETARCH}.tar.gz")" > tfupdate.tgz \ - || curl -L "$(curl -s ${TFUPDATE_RELEASES} | grep -o -E -m 1 "https://.+?${TFUPDATE_VERSION}_${TARGETOS}_${TARGETARCH}.tar.gz")" > tfupdate.tgz \ + TFUPDATE_RELEASES="https://api.github.com/repos/minamijoyo/tfupdate/releases" && \ + [ "$TFUPDATE_VERSION" = "latest" ] && curl -L "$(curl -s ${TFUPDATE_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${TARGETOS}_${TARGETARCH}.tar.gz")" > tfupdate.tgz \ + || curl -L "$(curl -s ${TFUPDATE_RELEASES} | grep -o -E -m 1 "https://.+?${TFUPDATE_VERSION}_${TARGETOS}_${TARGETARCH}.tar.gz")" > tfupdate.tgz \ ) && tar -xzf tfupdate.tgz tfupdate && rm tfupdate.tgz \ ; fi @@ -163,9 +163,9 @@ RUN . /.env && \ RUN . /.env && \ if [ "$HCLEDIT_VERSION" != "false" ]; then \ ( \ - HCLEDIT_RELEASES="https://api.github.com/repos/minamijoyo/hcledit/releases" && \ - [ "$HCLEDIT_VERSION" = "latest" ] && curl -L "$(curl -s ${HCLEDIT_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${TARGETOS}_${TARGETARCH}.tar.gz")" > hcledit.tgz \ - || curl -L "$(curl -s ${HCLEDIT_RELEASES} | grep -o -E -m 1 "https://.+?${HCLEDIT_VERSION}_${TARGETOS}_${TARGETARCH}.tar.gz")" > hcledit.tgz \ + HCLEDIT_RELEASES="https://api.github.com/repos/minamijoyo/hcledit/releases" && \ + [ "$HCLEDIT_VERSION" = "latest" ] && curl -L "$(curl -s ${HCLEDIT_RELEASES}/latest | grep -o -E -m 1 "https://.+?_${TARGETOS}_${TARGETARCH}.tar.gz")" > hcledit.tgz \ + || curl -L "$(curl -s ${HCLEDIT_RELEASES} | grep -o -E -m 1 "https://.+?${HCLEDIT_VERSION}_${TARGETOS}_${TARGETARCH}.tar.gz")" > hcledit.tgz \ ) && tar -xzf hcledit.tgz hcledit && rm hcledit.tgz \ ; fi @@ -208,8 +208,9 @@ COPY --from=builder \ /usr/local/bin/pre-commit \ # Hooks and terraform binaries /bin_dir/ \ + /usr/bin/tofu \ /usr/local/bin/checkov* \ - /usr/bin/ + /usr/bin/ # Copy pre-commit packages COPY --from=builder /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/ # Copy terrascan policies @@ -217,10 +218,10 @@ COPY --from=builder /root/ /root/ # Install hooks extra deps RUN if [ "$(grep -o '^terraform-docs SKIPPED$' /usr/bin/tools_versions_info)" = "" ]; then \ - apk add --no-cache perl=~5 \ + apk add --no-cache perl=~5 \ ; fi && \ if [ "$(grep -o '^infracost SKIPPED$' /usr/bin/tools_versions_info)" = "" ]; then \ - apk add --no-cache jq=~1 \ + apk add --no-cache jq=~1 \ ; fi && \ # Fix git runtime fatal: # unsafe repository ('/lint' is owned by someone else) diff --git a/README.md b/README.md index 1cd4a21..c8434c9 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ If you are using `pre-commit-opentofu` already or want to support its developmen

* [`checkov`](https://github.com/bridgecrewio/checkov) required for `tofu_checkov` hook. * [`terraform-docs`](https://github.com/terraform-docs/terraform-docs) required for `tofu_docs` hook. -* [`terragrunt`](https://terragrunt.gruntwork.io/docs/getting-started/install/) required for `terragrunt_validate` hook. +* [`terragrunt`](https://terragrunt.gruntwork.io/docs/getting-started/install/) required for `terragrunt_validate` and `terragrunt_validate_inputs` hooks. * [`terrascan`](https://github.com/tenable/terrascan) required for `terrascan` hook. * [`TFLint`](https://github.com/terraform-linters/tflint) required for `tofu_tflint` hook. * [`TFSec`](https://github.com/liamg/tfsec) required for `tofu_tfsec` hook. @@ -76,7 +76,7 @@ If you are using `pre-commit-opentofu` already or want to support its developmen ```bash TAG=latest -docker pull ghcr.io/tofuutils/pre-commit-opentofu:$TAG +docker pull tofuutils/pre-commit-opentofu:$TAG ``` All available tags [here](https://github.com/tofuutils/pre-commit-opentofu/pkgs/container/pre-commit-opentofu/versions). @@ -256,19 +256,45 @@ Or, using Docker ([available tags](https://github.com/tofuutils/pre-commit-opent ```bash TAG=latest -docker run -e "USERID=$(id -u):$(id -g)" -v $(pwd):/lint -w /lint ghcr.io/tofuutils/pre-commit-opentofu:$TAG run -a +docker run -e "USERID=$(id -u):$(id -g)" -v $(pwd):/lint -w /lint tofuutils/pre-commit-opentofu:$TAG run -a ``` Execute this command to list the versions of the tools in Docker: ```bash TAG=latest -docker run --rm --entrypoint cat ghcr.io/tofuutils/pre-commit-opentofu:$TAG /usr/bin/tools_versions_info +docker run --rm --entrypoint cat tofuutils/pre-commit-opentofu:$TAG /usr/bin/tools_versions_info ``` +### Example: Terragrunt Input Validation + +Use `terragrunt_validate_inputs` to check that Terragrunt inputs line up with the module variables they are passed into: + +```yaml +repos: +- repo: https://github.com/tofuutils/pre-commit-opentofu + rev: # Get the latest from: https://github.com/tofuutils/pre-commit-opentofu/releases + hooks: + - id: terragrunt_fmt + - id: terragrunt_validate_inputs + args: + - --args=--terragrunt-strict-validate +``` + +> **Note**: This hook automatically uses `terragrunt validate-inputs` for older Terragrunt releases and `terragrunt hcl validate --inputs` for newer releases. +> +> If Terragrunt reports intermittent `.terragrunt-cache` download or `file exists` errors in your repository, run this hook serially in your consumer configuration: +> +> ```yaml +> - id: terragrunt_validate_inputs +> require_serial: true +> args: +> - --args=--terragrunt-strict-validate +> ``` + ## Available Hooks -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: +There are several [pre-commit](https://pre-commit.com/) hooks to keep OpenTofu configurations (`*.tf`, `*.tofu`, and `*.tfvars`) and Terragrunt configurations (`*.hcl`) in a good shape: | Hook name | Description | Dependencies
[Install instructions here](#1-install-dependencies) | @@ -286,6 +312,7 @@ There are several [pre-commit](https://pre-commit.com/) hooks to keep OpenTofu c | `tofu_validate` | Validates all Terraform configuration files. [Hook notes](#tofu_validate) | `jq`, only for `--retry-once-with-cleanup` flag | | `terragrunt_fmt` | Reformat all [Terragrunt](https://github.com/gruntwork-io/terragrunt) configuration files (`*.hcl`) to a canonical format. | `terragrunt` | | `terragrunt_validate` | Validates all [Terragrunt](https://github.com/gruntwork-io/terragrunt) configuration files (`*.hcl`) | `terragrunt` | +| `terragrunt_validate_inputs` | Validates Terragrunt unused and undefined inputs. | `terragrunt` | | `tofu_wrapper_module_for_each` | Generates OpenTofu wrappers with `for_each` in module. [Hook notes](#terraform_wrapper_module_for_each) | `hcledit` | | `terrascan` | [terrascan](https://github.com/tenable/terrascan) Detect compliance and security violations. [Hook notes](#terrascan) | `terrascan` | | `tfupdate` | [tfupdate](https://github.com/minamijoyo/tfupdate) Update version constraints of OpenTofu core, providers, and modules. [Hook notes](#tfupdate) | `tfupdate` | @@ -299,6 +326,8 @@ Check the [source file](https://github.com/tofuutils/pre-commit-opentofu/blob/ma 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). +Hooks match `*.tofu` files where OpenTofu configuration files are supported, but some wrapped third-party tools may lag behind OpenTofu's native `*.tofu` parsing. If a hook runs `terraform-docs`, `tflint`, `tfsec`, `trivy`, `checkov`, `infracost`, or `tfupdate`, make sure the installed tool version supports the file extensions used in your repository. + ### All hooks: Usage of environment variables in `--args` > All, except deprecated hooks: `checkov`, `tofu_docs_replace` @@ -552,7 +581,7 @@ To replicate functionality in `tofu_docs` hook: - --args=--config=.terraform-docs.yml ``` -### terraftofu_fmtorm_fmt +### tofu_fmt 1. `tofu_fmt` supports custom arguments so you can pass [supported flags](https://www.terraform.io/docs/cli/commands/fmt.html#usage). Eg: @@ -689,7 +718,7 @@ To replicate functionality in `tofu_docs` hook: - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl ``` -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: +3. By default, pre-commit-opentofu performs directory switching into the OpenTofu modules for you. If you want to delegate 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: ```yaml - id: tofu_tflint @@ -903,7 +932,7 @@ To replicate functionality in `tofu_docs` hook: require_serial: true entry: .generate-providers.sh language: script - files: \.tf(vars)?$ + files: \.(tf|tofu|tfvars)$ pass_filenames: false - repo: https://github.com/pre-commit/pre-commit-hooks @@ -997,7 +1026,7 @@ The [recommended command](#4-run) to run the Docker container is: ```bash TAG=latest -docker run -e "USERID=$(id -u):$(id -g)" -v $(pwd):/lint -w /lint ghcr.io/tofuutils/pre-commit-opentofu:$TAG run -a +docker run -e "USERID=$(id -u):$(id -g)" -v $(pwd):/lint -w /lint tofuutils/pre-commit-opentofu:$TAG run -a ``` 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. @@ -1041,7 +1070,7 @@ Finally, you can execute `docker run` with an additional volume mount so that th # run pre-commit-opentofu with docker # adding volume for .netrc file # .netrc needs to be in /root/ dir -docker run --rm -e "USERID=$(id -u):$(id -g)" -v ~/.netrc:/root/.netrc -v $(pwd):/lint -w /lint ghcr.io/tofuutils/pre-commit-opentofu:latest run -a +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 ``` ## Authors diff --git a/hooks/_common.sh b/hooks/_common.sh index 133e457..5808c19 100644 --- a/hooks/_common.sh +++ b/hooks/_common.sh @@ -112,7 +112,7 @@ function common::parse_and_export_env_vars { while true; do # Check if at least 1 env var exists in `$arg` # shellcheck disable=SC2016 # '${' should not be expanded - if [[ "$arg" =~ .*'${'[A-Z_][A-Z0-9_]+?'}'.* ]]; then + if [[ "$arg" =~ .*'${'[A-Z_][A-Z0-9_]*'}'.* ]]; then # Get `ENV_VAR` from `.*${ENV_VAR}.*` local env_var_name=${arg#*$\{} env_var_name=${env_var_name%%\}*} @@ -123,7 +123,7 @@ function common::parse_and_export_env_vars { # `$arg` will be checked in `if` conditional, `$ARGS` will be used in the next functions. # shellcheck disable=SC2016 # '${' should not be expanded arg=${arg/'${'$env_var_name'}'/$env_var_value} - ARGS[$arg_idx]=$arg + ARGS[arg_idx]=$arg # shellcheck disable=SC2016 # '${' should not be expanded common::colorify "green" 'After ${'"$env_var_name"'} expansion: '"'$arg'\n" continue diff --git a/hooks/infracost_breakdown.sh b/hooks/infracost_breakdown.sh index 5515791..121f49b 100755 --- a/hooks/infracost_breakdown.sh +++ b/hooks/infracost_breakdown.sh @@ -70,19 +70,24 @@ function infracost_breakdown_ { # -h .totalHourlyCost > 0.1 # --hook-config=.currency == "USD" first_char=${check:0:1} - last_char=${check: -1} + last_char=${check:$((${#check} - 1)):1} if [ "$first_char" == "$last_char" ] && { [ "$first_char" == '"' ] || [ "$first_char" == "'" ] }; then - check="${check:1:-1}" + check="${check:1:$((${#check} - 2))}" fi - mapfile -t operations < <(echo "$check" | grep -oE '[!<>=]{1,2}') + # Replace mapfile with while read loop for bash 3.2 compatibility + operations=() + while IFS= read -r line; do + operations+=("$line") + done < <(echo "$check" | grep -oE '[!<>=]{1,2}') + # Get the very last operator, that is used in comparison inside `jq` query. # From the example below we need to pick the `>` which is in between `add` and `1000`, # but not the `!=`, which goes earlier in the `jq` expression # [.projects[].diff.totalMonthlyCost | select (.!=null) | tonumber] | add > 1000 - operation=${operations[-1]} + operation=${operations[$((${#operations[@]} - 1))]} IFS="$operation" read -r -a jq_check <<< "$check" real_value="$(jq "${jq_check[0]}" <<< "$RESULTS")" diff --git a/hooks/terragrunt_fmt.sh b/hooks/terragrunt_fmt.sh index 7c78b92..0b4165a 100755 --- a/hooks/terragrunt_fmt.sh +++ b/hooks/terragrunt_fmt.sh @@ -12,7 +12,7 @@ function main { common::parse_cmdline "$@" common::export_provided_env_vars "${ENV_VARS[@]}" common::parse_and_export_env_vars - # JFYI: terragrunt hclfmt color already suppressed via PRE_COMMIT_COLOR=never + # JFYI: terragrunt hcl format color already suppressed via PRE_COMMIT_COLOR=never # shellcheck disable=SC2153 # False positive common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" @@ -40,7 +40,7 @@ function per_dir_hook_unique_part { local -a -r args=("$@") # pass the arguments to hook - terragrunt hclfmt "${args[@]}" + terragrunt hcl format "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? @@ -57,7 +57,7 @@ function run_hook_on_whole_repo { local -a -r args=("$@") # pass the arguments to hook - terragrunt hclfmt "$(pwd)" "${args[@]}" + terragrunt hcl format "$(pwd)" "${args[@]}" # return exit code to common::per_dir_hook local exit_code=$? diff --git a/hooks/terragrunt_validate_inputs.sh b/hooks/terragrunt_validate_inputs.sh new file mode 100755 index 0000000..a406324 --- /dev/null +++ b/hooks/terragrunt_validate_inputs.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +set -eo pipefail + +# globals variables +# shellcheck disable=SC2155 # No way to assign to readonly variable in separate lines +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +# shellcheck source=_common.sh +. "$SCRIPT_DIR/_common.sh" + +function main { + common::initialize "$SCRIPT_DIR" + common::parse_cmdline "$@" + common::export_provided_env_vars "${ENV_VARS[@]}" + common::parse_and_export_env_vars + # JFYI: terragrunt validate color already suppressed via PRE_COMMIT_COLOR=never + + if terragrunt_version_ge_0_78; then + normalize_validate_args_for_modern_terragrunt + readonly SUBCOMMAND=("hcl" "validate" "--inputs") + readonly RUN_ALL_SUBCOMMAND=("run" "--all" "hcl" "validate" "--inputs") + + # shellcheck disable=SC2153 # False positive + common::per_dir_hook "$HOOK_ID" "${#ARGS[@]}" "${ARGS[@]}" "${FILES[@]}" + return + fi + + run_legacy_validate_inputs +} + +function normalize_validate_args_for_modern_terragrunt { + local arg_idx + + for arg_idx in "${!ARGS[@]}"; do + case "${ARGS[$arg_idx]}" in + --terragrunt-strict-validate | --strict-validate) + ARGS[arg_idx]="--strict" + ;; + esac + done +} + +function terragrunt_version_ge_0_78 { + local version_raw + local version + local major + local minor + + version_raw=$(terragrunt --version 2> /dev/null || true) + version=$(echo "$version_raw" | sed -E 's/.*v?([0-9]+)\.([0-9]+)\.([0-9]+).*/\1.\2.\3/') + + if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + return 1 + fi + + IFS=. read -r major minor _ <<< "$version" + + if ((major > 0)); then + return 0 + fi + + if ((minor >= 78)); then + return 0 + fi + + return 1 +} + +function run_legacy_validate_inputs { + local -a unit_dirs=() + local final_exit_code=0 + local dir_path + + while read -r dir_path; do + if [[ -n $dir_path ]]; then + unit_dirs+=("$dir_path") + fi + done < <(legacy_unit_dirs_from_files) + + if [[ ${#unit_dirs[@]} -eq 0 ]]; then + return 0 + fi + + # preserve errexit status + shopt -qo errexit && ERREXIT_IS_SET=true + set +e + + for dir_path in "${unit_dirs[@]}"; do + pushd "$dir_path" > /dev/null || continue + terragrunt validate-inputs "${ARGS[@]}" + + local exit_code=$? + if [ $exit_code -ne 0 ]; then + final_exit_code=$exit_code + fi + + popd > /dev/null + done + + [[ $ERREXIT_IS_SET ]] && set -e + exit $final_exit_code +} + +function legacy_unit_dirs_from_files { + local -a unit_files=() + local file_with_path + local file_dir + local file_name + + if common::is_hook_run_on_whole_repo "$HOOK_ID" "${FILES[@]}"; then + find . -type f -name terragrunt.hcl \ + -not -path '*/.terragrunt-cache/*' \ + -not -path '*/.terraform/*' | + sort -u | while read -r unit_file; do + dirname "$unit_file" + done + return + fi + + for file_with_path in "${FILES[@]}"; do + file_dir=$(dirname "$file_with_path") + file_name=$(basename "$file_with_path") + + if [[ $file_name == terragrunt.hcl ]]; then + unit_files+=("$file_with_path") + continue + fi + + while read -r unit_file; do + if [[ -n $unit_file ]]; then + unit_files+=("$unit_file") + fi + done < <(find "$file_dir" -type f -name terragrunt.hcl \ + -not -path '*/.terragrunt-cache/*' \ + -not -path '*/.terraform/*' | sort -u) + done + + if [[ ${#unit_files[@]} -eq 0 ]]; then + find . -type f -name terragrunt.hcl \ + -not -path '*/.terragrunt-cache/*' \ + -not -path '*/.terraform/*' | + sort -u | while read -r unit_file; do + dirname "$unit_file" + done + return + fi + + printf '%s\n' "${unit_files[@]}" | sort -u | while read -r unit_file; do + dirname "$unit_file" + done +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed in loop +# on each provided dir path. Run wrapped tool with specified arguments +# Arguments: +# dir_path (string) PATH to dir relative to git repo root. +# Can be used in error logging +# change_dir_in_unique_part (string/false) Modifier which creates +# possibilities to use non-common chdir strategies. +# Availability depends on hook. +# args (array) arguments that configure wrapped tool behavior +# Outputs: +# If failed - print out hook checks status +####################################################################### +function per_dir_hook_unique_part { + # shellcheck disable=SC2034 # Unused var. + local -r dir_path="$1" + # shellcheck disable=SC2034 # Unused var. + local -r change_dir_in_unique_part="$2" + shift 2 + local -a -r args=("$@") + + # pass the arguments to hook + terragrunt "${SUBCOMMAND[@]}" "${args[@]}" + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +####################################################################### +# Unique part of `common::per_dir_hook`. The function is executed one time +# in the root git repo +# Arguments: +# args (array) arguments that configure wrapped tool behavior +####################################################################### +function run_hook_on_whole_repo { + local -a -r args=("$@") + + # pass the arguments to hook + terragrunt "${RUN_ALL_SUBCOMMAND[@]}" "${args[@]}" + + # return exit code to common::per_dir_hook + local exit_code=$? + return $exit_code +} + +[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/hooks/tofu_docs.sh b/hooks/tofu_docs.sh index 308b26f..8b8d136 100755 --- a/hooks/tofu_docs.sh +++ b/hooks/tofu_docs.sh @@ -26,18 +26,17 @@ function main { ARGS[i]=${ARGS[i]/--config=/--config=$(pwd)\/} done # shellcheck disable=SC2153 # False positive - terraform_docs_ "${HOOK_CONFIG[*]}" "${ARGS[*]}" "${FILES[@]}" + tofu_check_ "${HOOK_CONFIG[*]}" "${ARGS[*]}" "${FILES[@]}" } ####################################################################### -# TODO Function which prepares hacks for old versions of `terraform` and -# `terraform-docs` that them call `terraform_docs` +# TODO Function which checks `terraform-docs` exists # Arguments: # hook_config (string with array) arguments that configure hook behavior # args (string with array) arguments that configure wrapped tool behavior # files (array) filenames to check ####################################################################### -function tofu_docs_ { +function tofu_check_ { local -r hook_config="$1" local -r args="$2" shift 2 @@ -46,40 +45,12 @@ function tofu_docs_ { # Get hook settings IFS=";" read -r -a configs <<< "$hook_config" - local hack_tofu_docs - hack_terraform_docs=$(tofu version | sed -n 1p | grep -c 0.12) || true - if [[ ! $(command -v terraform-docs) ]]; then - echo "ERROR: terraform-docs is required by terraform_docs pre-commit hook but is not installed or in the system's PATH." + echo "ERROR: terraform-docs is required by tofu_docs pre-commit hook but is not installed or in the system's PATH." exit 1 fi - local is_old_terraform_docs - is_old_terraform_docs=$(terraform-docs version | grep -o "v0.[1-7]\." | tail -1) || true - - if [[ -z "$is_old_terraform_docs" ]]; then # Using terraform-docs 0.8+ (preferred) - - terraform_docs "0" "${configs[*]}" "$args" "${files[@]}" - - elif [[ "$hack_terraform_docs" == "1" ]]; then # Using awk script because terraform-docs is older than 0.8 and terraform 0.12 is used - - if [[ ! $(command -v awk) ]]; then - # TODO: pls check it - echo "ERROR: awk is required for terraform-docs hack to work with Terraform 0.12." - exit 1 - fi - - local tmp_file_awk - tmp_file_awk=$(mktemp "${TMPDIR:-/tmp}/tofu-docs-XXXXXXXXXX") - tofu_docs_awk "$tmp_file_awk" - tofu_docs "$tmp_file_awk" "${configs[*]}" "$args" "${files[@]}" - rm -f "$tmp_file_awk" - - else # Using terraform 0.11 and no awk script is needed for that - # TODO: should be deleted for OpenTofu. - tofu_docs "0" "${configs[*]}" "$args" "${files[@]}" - - fi + tofu_docs "${configs[*]}" "${args[*]}" "${files[@]}" } ####################################################################### @@ -87,18 +58,14 @@ function tofu_docs_ { # (depends on provided hook_config) OpenTofu documentation in # markdown format # Arguments: -# terraform_docs_awk_file (string) filename where awk hack for old -# `terraform-docs` was written. Needed for TF 0.12+. -# Hack skipped when `tofu_docs_awk_file == "0"` # hook_config (string with array) arguments that configure hook behavior # args (string with array) arguments that configure wrapped tool behavior # files (array) filenames to check ####################################################################### function tofu_docs { - local -r tofu_docs_awk_file="$1" - local -r hook_config="$2" - local args="$3" - shift 3 + local -r hook_config="$1" + local -r args="$2" + shift 2 local -a -r files=("$@") local -a paths @@ -188,7 +155,7 @@ function tofu_docs { # if $create_if_not_exist && [[ ! -f "$text_file" ]]; then dir_have_tf_files="$( - find . -maxdepth 1 -type f | sed 's|.*\.||' | sort -u | grep -oE '^tf$|^tfvars$' || + find . -maxdepth 1 -type f | sed 's|.*\.||' | sort -u | grep -oE '^tofu|^tf$|^tfvars$' || exit 0 )" @@ -224,22 +191,8 @@ function tofu_docs { fi fi - if [[ "$terraform_docs_awk_file" == "0" ]]; then - # shellcheck disable=SC2086 - terraform-docs $tf_docs_formatter $args ./ > "$tmp_file" - else - # Can't append extension for mktemp, so renaming instead - local tmp_file_docs - tmp_file_docs=$(mktemp "${TMPDIR:-/tmp}/tofu-docs-XXXXXXXXXX") - mv "$tmp_file_docs" "$tmp_file_docs.tf" - local tmp_file_docs_tf - tmp_file_docs_tf="$tmp_file_docs.tf" - - awk -f "$terraform_docs_awk_file" ./*.tf > "$tmp_file_docs_tf" - # shellcheck disable=SC2086 - terraform-docs $tf_docs_formatter $args "$tmp_file_docs_tf" > "$tmp_file" - rm -f "$tmp_file_docs_tf" - fi + # shellcheck disable=SC2086 + terraform-docs $tf_docs_formatter $args ./ > "$tmp_file" # Use of insertion markers to insert the terraform-docs output between the markers # Replace content between markers with the placeholder - https://stackoverflow.com/questions/1212799/how-do-i-extract-lines-between-two-line-delimiters-in-perl#1212834 @@ -258,169 +211,4 @@ function tofu_docs { rm -f "$config_file_no_color" } -####################################################################### -# Function which creates file with `awk` hacks for old versions of -# `terraform-docs` -# Arguments: -# output_file (string) filename where hack will be written to -####################################################################### -function tofu_docs_awk { - local -r output_file=$1 - - cat << "EOF" > "$output_file" -# This script converts Terraform 0.12 variables/outputs to something suitable for `terraform-docs` -# As of terraform-docs v0.6.0, HCL2 is not supported. This script is a *dirty hack* to get around it. -# https://github.com/terraform-docs/terraform-docs/ -# https://github.com/terraform-docs/terraform-docs/issues/62 -# Script was originally found here: https://github.com/cloudposse/build-harness/blob/master/bin/terraform-docs.awk -{ - if ( $0 ~ /\{/ ) { - braceCnt++ - } - if ( $0 ~ /\}/ ) { - braceCnt-- - } - # ---------------------------------------------------------------------------------------------- - # variable|output "..." { - # ---------------------------------------------------------------------------------------------- - # [END] variable/output block - if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) { - if (braceCnt == 0 && blockCnt > 0) { - blockCnt-- - print $0 - } - } - # [START] variable or output block started - if ($0 ~ /^[[:space:]]*(variable|output)[[:space:]][[:space:]]*"(.*?)"/) { - # Normalize the braceCnt and block (should be 1 now) - braceCnt = 1 - blockCnt = 1 - # [CLOSE] "default" and "type" block - blockDefaultCnt = 0 - blockTypeCnt = 0 - # Print variable|output line - print $0 - } - # ---------------------------------------------------------------------------------------------- - # default = ... - # ---------------------------------------------------------------------------------------------- - # [END] multiline "default" continues/ends - if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt > 0) { - print $0 - # Count opening blocks - blockDefaultCnt += gsub(/\(/, "") - blockDefaultCnt += gsub(/\[/, "") - blockDefaultCnt += gsub(/\{/, "") - # Count closing blocks - blockDefaultCnt -= gsub(/\)/, "") - blockDefaultCnt -= gsub(/\]/, "") - blockDefaultCnt -= gsub(/\}/, "") - } - # [START] multiline "default" statement started - if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) { - if ($0 ~ /^[[:space:]][[:space:]]*(default)[[:space:]][[:space:]]*=/) { - if ($3 ~ "null") { - print " default = \"null\"" - } else { - print $0 - # Count opening blocks - blockDefaultCnt += gsub(/\(/, "") - blockDefaultCnt += gsub(/\[/, "") - blockDefaultCnt += gsub(/\{/, "") - # Count closing blocks - blockDefaultCnt -= gsub(/\)/, "") - blockDefaultCnt -= gsub(/\]/, "") - blockDefaultCnt -= gsub(/\}/, "") - } - } - } - # ---------------------------------------------------------------------------------------------- - # type = ... - # ---------------------------------------------------------------------------------------------- - # [END] multiline "type" continues/ends - if (blockCnt > 0 && blockTypeCnt > 0 && blockDefaultCnt == 0) { - # The following 'print $0' would print multiline type definitions - #print $0 - # Count opening blocks - blockTypeCnt += gsub(/\(/, "") - blockTypeCnt += gsub(/\[/, "") - blockTypeCnt += gsub(/\{/, "") - # Count closing blocks - blockTypeCnt -= gsub(/\)/, "") - blockTypeCnt -= gsub(/\]/, "") - blockTypeCnt -= gsub(/\}/, "") - } - # [START] multiline "type" statement started - if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) { - if ($0 ~ /^[[:space:]][[:space:]]*(type)[[:space:]][[:space:]]*=/ ) { - if ($3 ~ "object") { - print " type = \"object\"" - } else { - # Convert multiline stuff into single line - if ($3 ~ /^[[:space:]]*list[[:space:]]*\([[:space:]]*$/) { - type = "list" - } else if ($3 ~ /^[[:space:]]*string[[:space:]]*\([[:space:]]*$/) { - type = "string" - } else if ($3 ~ /^[[:space:]]*map[[:space:]]*\([[:space:]]*$/) { - type = "map" - } else { - type = $3 - } - # legacy quoted types: "string", "list", and "map" - if (type ~ /^[[:space:]]*"(.*?)"[[:space:]]*$/) { - print " type = " type - } else { - print " type = \"" type "\"" - } - } - # Count opening blocks - blockTypeCnt += gsub(/\(/, "") - blockTypeCnt += gsub(/\[/, "") - blockTypeCnt += gsub(/\{/, "") - # Count closing blocks - blockTypeCnt -= gsub(/\)/, "") - blockTypeCnt -= gsub(/\]/, "") - blockTypeCnt -= gsub(/\}/, "") - } - } - # ---------------------------------------------------------------------------------------------- - # description = ... - # ---------------------------------------------------------------------------------------------- - # [PRINT] single line "description" - if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) { - if ($0 ~ /^[[:space:]][[:space:]]*description[[:space:]][[:space:]]*=/) { - print $0 - } - } - # ---------------------------------------------------------------------------------------------- - # value = ... - # ---------------------------------------------------------------------------------------------- - ## [PRINT] single line "value" - #if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) { - # if ($0 ~ /^[[:space:]][[:space:]]*value[[:space:]][[:space:]]*=/) { - # print $0 - # } - #} - # ---------------------------------------------------------------------------------------------- - # Newlines, comments, everything else - # ---------------------------------------------------------------------------------------------- - #if (blockTypeCnt == 0 && blockDefaultCnt == 0) { - # Comments with '#' - if ($0 ~ /^[[:space:]]*#/) { - print $0 - } - # Comments with '//' - if ($0 ~ /^[[:space:]]*\/\//) { - print $0 - } - # Newlines - if ($0 ~ /^[[:space:]]*$/) { - print $0 - } - #} -} -EOF - -} - [ "${BASH_SOURCE[0]}" != "$0" ] || main "$@" diff --git a/hooks/tofu_docs_replace.py b/hooks/tofu_docs_replace.py index a9cf6c9..26f1d06 100644 --- a/hooks/tofu_docs_replace.py +++ b/hooks/tofu_docs_replace.py @@ -7,30 +7,41 @@ import sys def main(argv=None): parser = argparse.ArgumentParser( description="""Run terraform-docs on a set of files. Follows the standard convention of - pulling the documentation from main.tf in order to replace the entire + pulling the documentation from main.(tf|tofu) in order to replace the entire README.md file each time.""" ) parser.add_argument( - '--dest', dest='dest', default='README.md', + "--dest", + dest="dest", + default="README.md", ) parser.add_argument( - '--sort-inputs-by-required', dest='sort', action='store_true', - help='[deprecated] use --sort-by-required instead', + "--sort-inputs-by-required", + dest="sort", + action="store_true", + help="[deprecated] use --sort-by-required instead", ) parser.add_argument( - '--sort-by-required', dest='sort', action='store_true', + "--sort-by-required", + dest="sort", + action="store_true", ) parser.add_argument( - '--with-aggregate-type-defaults', dest='aggregate', action='store_true', - help='[deprecated]', + "--with-aggregate-type-defaults", + dest="aggregate", + action="store_true", + help="[deprecated]", ) - parser.add_argument('filenames', nargs='*', help='Filenames to check.') + parser.add_argument("filenames", nargs="*", help="Filenames to check.") args = parser.parse_args(argv) dirs = [] for filename in args.filenames: - if (os.path.realpath(filename) not in dirs and - (filename.endswith(".tf") or filename.endswith(".tfvars"))): + if os.path.realpath(filename) not in dirs and ( + filename.endswith(".tf") + or filename.endswith(".tofu") + or filename.endswith(".tfvars") + ): dirs.append(os.path.dirname(filename)) retval = 0 @@ -38,12 +49,12 @@ def main(argv=None): for dir in dirs: try: procArgs = [] - procArgs.append('terraform-docs') + procArgs.append("terraform-docs") if args.sort: - procArgs.append('--sort-by-required') - procArgs.append('md') + procArgs.append("--sort-by-required") + procArgs.append("md") procArgs.append("./{dir}".format(dir=dir)) - procArgs.append('>') + procArgs.append(">") procArgs.append("./{dir}/{dest}".format(dir=dir, dest=args.dest)) subprocess.check_call(" ".join(procArgs), shell=True) except subprocess.CalledProcessError as e: @@ -52,5 +63,5 @@ def main(argv=None): return retval -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main()) diff --git a/hooks/tofu_wrapper_module_for_each.sh b/hooks/tofu_wrapper_module_for_each.sh index f1a70ef..4572b58 100755 --- a/hooks/tofu_wrapper_module_for_each.sh +++ b/hooks/tofu_wrapper_module_for_each.sh @@ -312,10 +312,10 @@ EOF # Read content of all OpenTofu files # shellcheck disable=SC2207 - all_tf_content=$(find "${full_module_dir}" -name '*.tf' -maxdepth 1 -type f -exec cat {} +) + all_tf_content=$(find "${full_module_dir}" -regex '.*\.(tf|tofu)' -maxdepth 1 -type f -exec cat {} +) if [[ ! $all_tf_content ]]; then - common::colorify "yellow" "Skipping ${full_module_dir} because there are no *.tf files." + common::colorify "yellow" "Skipping ${full_module_dir} because there are no .tf or .tofu files." continue fi diff --git a/lib_getopt b/lib_getopt index c4b21fa..74e72f1 100644 --- a/lib_getopt +++ b/lib_getopt @@ -360,7 +360,7 @@ getopt() { } _getopt_resolve_abbrev() { - # Resolves an abbrevation from a list of possibilities. + # Resolves an abbreviation from a list of possibilities. # If the abbreviation is unambiguous, echoes the expansion on stdout # and returns 0. If the abbreviation is ambiguous, prints a message on # stderr and returns 1. (For first parse this should convert to exit