Skip to content
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
25 changes: 23 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v6
- uses: hashicorp/setup-terraform@v4
with:
terraform_version: "1.5.0"
terraform_version: "1.10.0"
terraform_wrapper: false
- run: terraform fmt -check -recursive -diff

Expand All @@ -37,8 +37,29 @@ jobs:
- uses: actions/checkout@v6
- uses: hashicorp/setup-terraform@v4
with:
terraform_version: "1.5.0"
terraform_version: "1.10.0"
terraform_wrapper: false
# -backend=false: validate needs init for provider/module schemas but no backend or cloud credentials.
- run: terraform init -backend=false
- run: terraform validate -no-color

test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
dir:
- modules/vpc
- modules/quilt/tests/smoke
defaults:
run:
working-directory: ${{ matrix.dir }}
steps:
- uses: actions/checkout@v6
- uses: hashicorp/setup-terraform@v4
with:
terraform_version: "1.10.0"
terraform_wrapper: false
# The tests mock the AWS provider, so no backend or cloud credentials are needed.
- run: terraform init -backend=false
- run: terraform test -no-color
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
.terraform
.terraform.lock.hcl
tfplan
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,18 @@ terraform fmt
terraform validate
```

## Test

Module tests are plan-only and mock the AWS provider, so they need no AWS
credentials and create no infrastructure. Requires Terraform >= 1.7 (for
`mock_provider`). Run from a module or test-wrapper directory, e.g.
`modules/vpc` or `modules/quilt/tests/smoke`:

```
terraform init -backend=false
terraform test
```

## Plan
```
terraform plan -out tfplan
Expand Down
8 changes: 8 additions & 0 deletions modules/quilt/tests/smoke/fixtures/quilt.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Test fixture for terraform test smoke runs. Stands in for the real Quilt
CloudFormation template so plan-time references (template_file / filemd5)
resolve. Not a deployable Quilt stack.
Resources:
Placeholder:
Type: AWS::CloudFormation::WaitConditionHandle
96 changes: 96 additions & 0 deletions modules/quilt/tests/smoke/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Wrapper root for the `quilt` module smoke tests.
#
# The smoke tests run against this wrapper rather than modules/quilt directly
# because the quilt module's `stack` output embeds sensitive values (DB + admin
# passwords); as a root module under test that trips a sensitive-output error.
# The wrapper re-exposes only the non-sensitive stack name.
#
# The required_providers block below is also load-bearing for the tests:
# declaring the provider requirement at the root is what lets the test's
# mock_provider engage for the whole module tree. Without a direct provider
# reference at the root, the mock never attaches and the child modules fall
# back to real AWS credentials.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
# Match what the modules under test transitively require: the
# terraform-aws-modules/vpc ~> 6.0 module needs aws >= 6.28. Pinning keeps
# CI deterministic and off a future major. (Note: examples/main.tf and
# modules/cnames still pin ~> 5.0, which is incompatible with that floor.)
version = "~> 6.0"
}
}
Comment thread
sir-sigurd marked this conversation as resolved.
}

variable "create_new_vpc" {
type = bool
}

variable "internal" {
type = bool
default = false
}

variable "vpc_id" {
type = string
default = null
}
Comment thread
sir-sigurd marked this conversation as resolved.

variable "api_endpoint" {
type = string
default = null
}

variable "intra_subnets" {
type = list(string)
default = null
}

variable "private_subnets" {
type = list(string)
default = null
}

variable "public_subnets" {
type = list(string)
default = null
}

variable "user_security_group" {
type = string
default = null
}
Comment thread
sir-sigurd marked this conversation as resolved.

variable "user_subnets" {
type = list(string)
default = null
}

# New inputs added to the quilt module must be threaded through here, or the
# smoke coverage silently narrows (the new input is never exercised).
module "quilt" {
source = "../../"

name = "quilt-test"
parameters = {}
template_file = "${path.module}/fixtures/quilt.yaml"

create_new_vpc = var.create_new_vpc
internal = var.internal
vpc_id = var.vpc_id
api_endpoint = var.api_endpoint
intra_subnets = var.intra_subnets
private_subnets = var.private_subnets
public_subnets = var.public_subnets
user_security_group = var.user_security_group
user_subnets = var.user_subnets
}
Comment thread
sir-sigurd marked this conversation as resolved.

# Re-expose ONLY the non-sensitive stack name. Do not output module.quilt.stack
# (it embeds the DB URL + admin password) or any *_password value — a sensitive
# root output makes `terraform test` fail, which is the whole reason this
# wrapper exists.
output "stack_name" {
value = module.quilt.stack.name
}
84 changes: 84 additions & 0 deletions modules/quilt/tests/smoke/smoke.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Smoke tests for the public `quilt` module, run against the wrapper root in
# this directory (see main.tf).
#
# Plan-only with the AWS provider mocked: no credentials, no infrastructure.
# Exercises the full wiring (vpc + db + search + the CloudFormation stack), so
# a change that breaks the public boundary or the vpc pass-through fails in CI.
#
# Assertions reference known inputs (the stack name), not mocked computed
# attributes, whose generated values are intentionally arbitrary.

mock_provider "aws" {
# slice(..., 0, 2) and the cidrsubnet math in the vpc submodule need at
# least two AZ names; mocked collections are otherwise empty.
mock_data "aws_availability_zones" {
defaults = {
names = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
}
}

run "new_vpc_plans" {
command = plan
variables {
create_new_vpc = true
internal = false
}
assert {
condition = output.stack_name == "quilt-test"
error_message = "The CloudFormation stack must be named after var.name"
}
}

run "new_vpc_internal_plans" {
command = plan
variables {
create_new_vpc = true
internal = true
}
# internal = true exercises the most conditional wiring in quilt/main.tf:
# the PublicSubnets/UserSubnets null-coalescing and the internal-gated api
# endpoint.
assert {
condition = output.stack_name == "quilt-test"
error_message = "The CloudFormation stack must be named after var.name"
}
}

run "existing_vpc_plans" {
command = plan
variables {
create_new_vpc = false
internal = false
vpc_id = "vpc-00000000000000000"
intra_subnets = ["subnet-intra-a", "subnet-intra-b"]
private_subnets = ["subnet-priv-a", "subnet-priv-b"]
public_subnets = ["subnet-pub-a", "subnet-pub-b"]
user_security_group = "sg-00000000000000000"
}
assert {
condition = output.stack_name == "quilt-test"
error_message = "The CloudFormation stack must be named after var.name"
}
}

run "existing_vpc_internal_plans" {
command = plan
variables {
create_new_vpc = false
internal = true
vpc_id = "vpc-00000000000000000"
api_endpoint = "vpce-00000000000000000"
intra_subnets = ["subnet-intra-a", "subnet-intra-b"]
private_subnets = ["subnet-priv-a", "subnet-priv-b"]
public_subnets = null
user_security_group = "sg-00000000000000000"
user_subnets = ["subnet-user-a", "subnet-user-b"]
}
# internal = true on the existing-VPC path exercises quilt's pass-through of
# api_endpoint + user_subnets and the internal-gated CFN wiring.
assert {
condition = output.stack_name == "quilt-test"
error_message = "The CloudFormation stack must be named after var.name"
}
}
Comment thread
sir-sigurd marked this conversation as resolved.
Loading