From 425fe5386b177485f5810f16572afaa0de07929e Mon Sep 17 00:00:00 2001 From: Yohan Belval Date: Thu, 5 Dec 2024 20:57:36 +0000 Subject: [PATCH] feat: add hook for provider version checking --- .gitignore | 2 + .pre-commit-hooks.yaml | 8 +++ hooks/provider-pinned-versions/check | 15 ++++++ .../fixtures/fail_when_all_constraints.tf | 14 +++++ .../fixtures/fail_when_mixed.tf | 14 +++++ .../fixtures/pass_when_all_pinned.tf | 22 ++++++++ .../fixtures/pass_when_inlined.tf | 8 +++ .../fixtures/pass_when_pre_release.tf | 14 +++++ .../fixtures/pass_when_version_is_first.tf | 14 +++++ .../required_providers.awk | 54 +++++++++++++++++++ hooks/provider-pinned-versions/test | 26 +++++++++ script/test | 1 + 12 files changed, 192 insertions(+) create mode 100755 hooks/provider-pinned-versions/check create mode 100644 hooks/provider-pinned-versions/fixtures/fail_when_all_constraints.tf create mode 100644 hooks/provider-pinned-versions/fixtures/fail_when_mixed.tf create mode 100644 hooks/provider-pinned-versions/fixtures/pass_when_all_pinned.tf create mode 100644 hooks/provider-pinned-versions/fixtures/pass_when_inlined.tf create mode 100644 hooks/provider-pinned-versions/fixtures/pass_when_pre_release.tf create mode 100644 hooks/provider-pinned-versions/fixtures/pass_when_version_is_first.tf create mode 100644 hooks/provider-pinned-versions/required_providers.awk create mode 100755 hooks/provider-pinned-versions/test diff --git a/.gitignore b/.gitignore index 2ecd388..d29c8af 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ # Crash log files crash.log + +.vscode/ diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 9f3dbc6..602e59e 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -13,3 +13,11 @@ language: script files: \.tf$ exclude: ^outputs\..*\.tf$ + +- id: provider-pinned-versions + name: "Ensures provider versions are pinned.`" + description: "Ensures Terraform providers are pinned and not in a constrained format." + entry: hooks/provider-pinned-versions/check + language: script + files: \.tf$ + exclude: ^(variables|outputs)\.{0,1}.*\.tf$ diff --git a/hooks/provider-pinned-versions/check b/hooks/provider-pinned-versions/check new file mode 100755 index 0000000..e196d8a --- /dev/null +++ b/hooks/provider-pinned-versions/check @@ -0,0 +1,15 @@ +#!/bin/bash -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +AWK_FILE="${SCRIPT_DIR}/required_providers.awk" + +check_files() { + has_error=0 + for file in "$@"; do + awk -f "$AWK_FILE" "$file" || has_error=1 + done + return $has_error +} + +check_files "$@" +exit $has_error diff --git a/hooks/provider-pinned-versions/fixtures/fail_when_all_constraints.tf b/hooks/provider-pinned-versions/fixtures/fail_when_all_constraints.tf new file mode 100644 index 0000000..8578865 --- /dev/null +++ b/hooks/provider-pinned-versions/fixtures/fail_when_all_constraints.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0, < 2.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~>5.78" + } + newrelic = { + source = "newrelic/newrelic" + version = ">=2, <3" + } + } +} diff --git a/hooks/provider-pinned-versions/fixtures/fail_when_mixed.tf b/hooks/provider-pinned-versions/fixtures/fail_when_mixed.tf new file mode 100644 index 0000000..aad37f5 --- /dev/null +++ b/hooks/provider-pinned-versions/fixtures/fail_when_mixed.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0, < 2.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.78.0" + } + newrelic = { + source = "newrelic/newrelic" + version = ">=2, <3" + } + } +} diff --git a/hooks/provider-pinned-versions/fixtures/pass_when_all_pinned.tf b/hooks/provider-pinned-versions/fixtures/pass_when_all_pinned.tf new file mode 100644 index 0000000..b41cfa4 --- /dev/null +++ b/hooks/provider-pinned-versions/fixtures/pass_when_all_pinned.tf @@ -0,0 +1,22 @@ +terraform { + required_version = ">= 1.0.0, < 2.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.78.0" + } + cyral = { + source = "cyralinc/cyral" + version = "4.14.1" + } + mysql = { + source = "petoju/mysql" + version = "3.0.67" + } + newrelic = { + source = "newrelic/newrelic" + version = "3.52.1" + } + } +} diff --git a/hooks/provider-pinned-versions/fixtures/pass_when_inlined.tf b/hooks/provider-pinned-versions/fixtures/pass_when_inlined.tf new file mode 100644 index 0000000..617dd4e --- /dev/null +++ b/hooks/provider-pinned-versions/fixtures/pass_when_inlined.tf @@ -0,0 +1,8 @@ +terraform { + required_version = ">= 1.0.0, < 2.0.0" + + required_providers { + aws = { source = "hashicorp/aws", version = "5.78.0" } + cyral = { source = "cyralinc/cyral", version = "4.14.1" } + } +} diff --git a/hooks/provider-pinned-versions/fixtures/pass_when_pre_release.tf b/hooks/provider-pinned-versions/fixtures/pass_when_pre_release.tf new file mode 100644 index 0000000..ff2892a --- /dev/null +++ b/hooks/provider-pinned-versions/fixtures/pass_when_pre_release.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0, < 2.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.78.0-beta-some-branch-name" + } + newrelic = { + source = "newrelic/newrelic" + version = "3.52.1" + } + } +} diff --git a/hooks/provider-pinned-versions/fixtures/pass_when_version_is_first.tf b/hooks/provider-pinned-versions/fixtures/pass_when_version_is_first.tf new file mode 100644 index 0000000..87e5a7b --- /dev/null +++ b/hooks/provider-pinned-versions/fixtures/pass_when_version_is_first.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0, < 2.0.0" + + required_providers { + aws = { + version = "5.78.0" + source = "hashicorp/aws" + } + cyral = { + source = "cyralinc/cyral" + version = "4.14.1" + } + } +} diff --git a/hooks/provider-pinned-versions/required_providers.awk b/hooks/provider-pinned-versions/required_providers.awk new file mode 100644 index 0000000..2bc326e --- /dev/null +++ b/hooks/provider-pinned-versions/required_providers.awk @@ -0,0 +1,54 @@ +#! /bin/awk + +BEGIN { + in_required_providers = 0; + brace_count = 0; + version_prefix_regex = ".*version[[:space:]]+=[[:space:]]+"; + provider_prefix_regex = "[a-z_-]+[[:space:]]+=[[:space:]]+\{"; + pinned_version_regex = "[0-9]+\.[0-9]+\.[0-9]+"; + version_constraints_regex = "[!~><]+"; +} + +{ + if ($0 ~ /required_providers/) { + in_required_providers = 1; + } + + # If inside "required_providers" block, count braces + if (in_required_providers) { + brace_count += gsub(/{/, "{"); + brace_count -= gsub(/}/, "}"); + + # If brace count returns to 0, exit the block + if (brace_count == 0) { + in_required_providers = 0; + next; + } + + if ($0 ~ provider_prefix_regex) { + provider = $1; + } + + # If the line doesn't contain a version, skip + if (!($0 ~ version_prefix_regex)) { + next; + } else { + # If the line contains a version, remove the prefix + gsub(version_prefix_regex, "", $0); + if ($0 ~ version_constraints_regex) { + # remove trailing curly brace if any (happens when version is inlined) + gsub(/}/, "", $0); + error[++i] = "ERROR: '" provider "' version not in pinned format: " $0 " in file " FILENAME; + } + } + } +} + +END { + if (length(error) > 0) { + for (j = 1; j <= i; j++) { + print error[j] + } + exit 1; + } +} diff --git a/hooks/provider-pinned-versions/test b/hooks/provider-pinned-versions/test new file mode 100755 index 0000000..0d44abb --- /dev/null +++ b/hooks/provider-pinned-versions/test @@ -0,0 +1,26 @@ +#!/bin/bash -e + +# get the directory of the script +script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +echo "testing: $script_directory" + +# shellcheck disable=SC2207 +files=($(find "$script_directory"/fixtures -type f -name "pass_*.tf")) +for file in "${files[@]}"; do + echo "testing: check $file" + "$script_directory/check" "$file" +done + +# shellcheck disable=SC2207 +files=($(find "$script_directory"/fixtures -type f -name "fail_*.tf")) +for file in "${files[@]}"; do + echo "testing: check $file" + echo " expecting error" + if "$script_directory/check" "$file"; then + echo "ERROR: should have failed" + exit 1 + fi +done + +echo "testing: PASS" diff --git a/script/test b/script/test index 01c8e87..d1384ea 100755 --- a/script/test +++ b/script/test @@ -4,3 +4,4 @@ REPO_DIR="$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pw "$REPO_DIR/hooks/outputs-in-outputs-files/test" "$REPO_DIR/hooks/vars-in-variables-files/test" +"$REPO_DIR/hooks/provider-pinned-versions/test"