Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add hook for provider version checking #51

Merged
merged 2 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@

# Crash log files
crash.log

.vscode/
8 changes: 8 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@
language: script
files: \.tf$
exclude: ^outputs\..*\.tf$

- id: provider-pinned-versions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add to the readme this hook and how to use it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we don't have any docs about hooks

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$
77 changes: 77 additions & 0 deletions hooks/provider-pinned-versions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Provider Pinned Versions Hook

This hook validates that all providers under the `required_providers` block of a Terraform configuration file have a pinned version.

## Usage

### cli

```bash
pre-commit run provider-pinned-versions --all-files
```

### pre-commit config

```yaml
repos:
- repo: https://github.com/open-turo/standards-terraform
hooks:
- id: provider-pinned-versions
files: ^terraform.tf$
```

## Examples of supported configurations

### Standard pinned version

```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "3.40.0"
}
}
}
```

### Pre-release version

```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "3.41.0-beta1"
}
}
}
```

## Examples of unsupported configurations

### Version constraints

If the version contains any of the following constraint characters, `!~><`, the hook will fail.

```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 3.40.0"
}
}
}
```

```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.40.0"
}
}
}
```
15 changes: 15 additions & 0 deletions hooks/provider-pinned-versions/check
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
14 changes: 14 additions & 0 deletions hooks/provider-pinned-versions/fixtures/fail_when_mixed.tf
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
22 changes: 22 additions & 0 deletions hooks/provider-pinned-versions/fixtures/pass_when_all_pinned.tf
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
@@ -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" }
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
terraform {
required_version = ">= 1.0.0, < 2.0.0"

required_providers {
aws = {
version = "5.78.0"

This comment was marked as resolved.

This comment was marked as resolved.

source = "hashicorp/aws"
}
cyral = {
source = "cyralinc/cyral"
version = "4.14.1"
}
}
}
53 changes: 53 additions & 0 deletions hooks/provider-pinned-versions/required_providers.awk
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#! /bin/awk
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AWK is fine, but I wonder if it would be easier if we used python (since it's available already as pre-commit uses python)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of it as well but I thought we wouldn't have access to it since we're removing the .python-version files.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can leave that in... and/or just assume that python is present.


BEGIN {
in_required_providers = 0;
brace_count = 0;
version_prefix_regex = ".*version[[:space:]]+=[[:space:]]+";
provider_prefix_regex = "[a-z_-]+[[:space:]]+=[[:space:]]+\{";
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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have used match with capture groups but it wasn't working on macos. I think it's a gawk specific function and depends on the os.

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;
}
}
26 changes: 26 additions & 0 deletions hooks/provider-pinned-versions/test
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions script/test
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading