Skip to content

Commit db92e55

Browse files
Brend-Smitsnpalmedersonbrilhante
authored
ci(runners): add terraform test for pool module type safety (#5157)
## Problem `terraform validate` cannot detect conditional type mismatches like the one introduced in #4875: ```hcl role = var.iam_overrides["override_runner_role"] ? { arn = var.iam_overrides["runner_role_arn"] } : aws_iam_role.runner[0] ``` This passes `terraform validate` but fails at plan time because the two branches produce objects with different attributes (1 vs 16). ## Solution Add a `terraform test` file (`modules/runners/tests/pool.tftest.hcl`) that uses mock providers to run a full plan of the runners module with pool enabled. This catches type errors that `validate` misses, without requiring AWS credentials. Also adds a `terraform_test` job to the existing CI workflow. The job uses Terraform `latest` (requires 1.7+ for `mock_provider` / `mock_data` support) and runs `terraform test` on modules that have test files. ### Verified locally With the buggy code (before #5156): ``` Error: Inconsistent conditional result types on pool.tf line 54, in module "pool": The true and false result expressions must have consistent types. ``` With the fix (#5156 applied): ``` Success! 1 passed, 0 failed. ``` ## Future work The `terraform_test` job matrix can be expanded as more modules add `.tftest.hcl` files. This is a starting point to catch the class of bugs that `validate` cannot. --------- Signed-off-by: Brend Smits <brend.smits@philips.com> Co-authored-by: Niek Palm <npalm@users.noreply.github.com> Co-authored-by: Ederson Brilhante <contato@edersonbrilhante.com.br>
1 parent 0e28137 commit db92e55

3 files changed

Lines changed: 159 additions & 0 deletions

File tree

.github/workflows/terraform.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,30 @@ jobs:
206206
run: |
207207
tflint --init -c ${GITHUB_WORKSPACE}/.tflint.hcl --chdir "examples/${EXAMPLE_NAME}"
208208
tflint -f compact -c ${GITHUB_WORKSPACE}/.tflint.hcl --var-file ${GITHUB_WORKSPACE}/.github/lint/tflint.tfvars --chdir "examples/${EXAMPLE_NAME}"
209+
210+
terraform_test:
211+
name: Terraform test
212+
strategy:
213+
fail-fast: false
214+
matrix:
215+
module:
216+
- modules/runners
217+
defaults:
218+
run:
219+
working-directory: ${{ matrix.module }}
220+
runs-on: ubuntu-latest
221+
container:
222+
image: hashicorp/terraform@sha256:1d10ec4073f4ddbdf34a28540a3b9250852ab500cb1c53f68c8bd17d82f474d8 # 1.14
223+
steps:
224+
- name: Harden the runner (Audit all outbound calls)
225+
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
226+
with:
227+
egress-policy: audit
228+
229+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
230+
with:
231+
persist-credentials: false
232+
- name: terraform init
233+
run: terraform init -backend=false -input=false
234+
- name: terraform test
235+
run: terraform test -test-directory=tests

modules/runners/tests/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Terraform Tests
2+
3+
This directory contains [Terraform test files](https://developer.hashicorp.com/terraform/language/tests) (`.tftest.hcl`) for the runners module.
4+
5+
## Why `terraform test` instead of `terraform validate`?
6+
7+
`terraform validate` only checks syntax and basic type correctness of the configuration. It **cannot** detect:
8+
9+
- Conditional expressions with inconsistent result types (e.g., one branch returns an object with 1 attribute, the other returns 16)
10+
- Runtime type mismatches that only surface during `plan`
11+
- Invalid cross-module references that depend on resource attribute shapes
12+
13+
`terraform test` with `mock_provider` runs a full plan without needing real cloud credentials, catching these classes of bugs in CI.
14+
15+
## Requirements
16+
17+
- Terraform >= 1.7 (for `mock_provider` and `mock_data` support)
18+
- No AWS credentials required — all providers are mocked
19+
20+
## Running locally
21+
22+
```bash
23+
cd modules/runners
24+
terraform test -test-directory=tests
25+
```
26+
27+
Expected output:
28+
29+
```
30+
tests/pool.tftest.hcl... in progress
31+
run "plan_with_pool_enabled"... pass
32+
tests/pool.tftest.hcl... pass
33+
34+
Success! 1 passed, 0 failed.
35+
```
36+
37+
## Writing new tests
38+
39+
1. Create a `.tftest.hcl` file in this directory
40+
2. Use `mock_provider "aws" {}` to avoid needing credentials
41+
3. Use `mock_data` blocks to provide realistic values for data sources that perform validation (e.g., `aws_iam_policy_document` validates JSON)
42+
4. Set all required variables in a `variables {}` block
43+
5. Use `run` blocks with `command = plan` and `assert` conditions
44+
45+
### Example template
46+
47+
```hcl
48+
mock_provider "aws" {
49+
mock_data "aws_iam_policy_document" {
50+
defaults = {
51+
json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
52+
}
53+
}
54+
}
55+
56+
variables {
57+
# ... required variables ...
58+
}
59+
60+
run "descriptive_test_name" {
61+
command = plan
62+
63+
assert {
64+
condition = <expression>
65+
error_message = "Explanation of what failed"
66+
}
67+
}
68+
```
69+
70+
## CI integration
71+
72+
These tests run automatically in the `terraform_test` job of `.github/workflows/terraform.yml` on every PR that touches `*.tf` or `*.hcl` files.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
mock_provider "aws" {
2+
mock_data "aws_iam_policy_document" {
3+
defaults = {
4+
json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
5+
}
6+
}
7+
}
8+
9+
variables {
10+
aws_region = "eu-west-1"
11+
vpc_id = "vpc-12345678"
12+
subnet_ids = ["subnet-12345678"]
13+
14+
instance_types = ["m5.large"]
15+
16+
s3_runner_binaries = {
17+
arn = "arn:aws:s3:::my-bucket"
18+
id = "my-bucket"
19+
key = "runners/linux/actions-runner.tar.gz"
20+
}
21+
22+
sqs_build_queue = {
23+
arn = "arn:aws:sqs:eu-west-1:123456789012:build-queue"
24+
url = "https://sqs.eu-west-1.amazonaws.com/123456789012/build-queue"
25+
}
26+
27+
enable_organization_runners = true
28+
enable_ssm_on_runners = true
29+
runner_labels = ["self-hosted", "linux", "x64"]
30+
31+
# Use S3 bucket to avoid filebase64sha256 needing local zip files
32+
lambda_s3_bucket = "my-lambda-bucket"
33+
runners_lambda_s3_key = "runners.zip"
34+
35+
github_app_parameters = {
36+
key_base64 = { name = "/github-runner/key-base64", arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/key-base64" }
37+
id = { name = "/github-runner/app-id", arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/app-id" }
38+
}
39+
40+
ssm_paths = {
41+
root = "/github-runner"
42+
tokens = "tokens"
43+
config = "config"
44+
}
45+
46+
# Enable pool to exercise the pool module and its role type
47+
pool_config = [{
48+
schedule_expression = "cron(0 8 * * ? *)"
49+
size = 1
50+
}]
51+
}
52+
53+
run "plan_with_pool_enabled" {
54+
command = plan
55+
56+
assert {
57+
condition = length(module.pool) == 1
58+
error_message = "Pool module should be enabled when pool_config is non-empty"
59+
}
60+
}

0 commit comments

Comments
 (0)