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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ jobs:
- single-vm/azure
- ha-hot-hot/azure
- unlimited-scale/azure
# Shared infrastructure — foundational modules consumed by every tier
- network/aws
- network/azure
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
1 change: 1 addition & 0 deletions modules/asm-aws-single/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module "this" {
allow_internet_ingress = var.allow_internet_ingress
enable_snapshots = var.enable_snapshots
marketplace_product_code = var.marketplace_product_code
enable_flow_logs = var.enable_flow_logs

# Patching and migration safety
create_backup_bucket = var.create_backup_bucket
Expand Down
6 changes: 6 additions & 0 deletions modules/asm-aws-single/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@ output "pre_patch_ssm_document_name" {
value = module.this.pre_patch_ssm_document_name
sensitive = false
}

output "flow_log_group_name" {
description = "CloudWatch log group name receiving VPC Flow Logs. Empty string when enable_flow_logs is false."
value = module.this.flow_log_group_name
sensitive = false
}
6 changes: 6 additions & 0 deletions modules/asm-aws-single/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ variable "backup_noncurrent_version_expiration_days" {
default = 365
}

variable "enable_flow_logs" {
description = "Enable VPC Flow Logs for the provided VPC, sending ALL traffic to a CloudWatch log group named /aws/vpc-flow-logs/<name_prefix>. Matches the default stated in SECURITY-DEFAULTS.md."
type = bool
default = true
}

variable "tags" {
description = "Additional tags applied to every resource."
type = map(string)
Expand Down
87 changes: 87 additions & 0 deletions modules/network/aws/tests/basic.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Minimal-input apply against a mocked AWS provider. No real credentials and no
# API calls — the mock provider stubs the provider entirely. This proves the
# module instantiates with only its required variables and that every
# operator-facing output is populated.
#
# The aws_availability_zones data source is overridden via mock_data so that
# slice(names, 0, az_count) evaluates to a known, non-empty list. Without this
# override the mock provider returns an empty list and the slice call fails.
#
# The apply command is used (rather than plan) because the private route tables
# reference aws_nat_gateway.main[*].id inside a for_each. With mock apply the
# provider generates deterministic mock IDs for all resources, so that reference
# is fully known; with plan only, it would be unknown and Terraform would refuse
# to evaluate the for_each.

mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = {
names = ["us-east-1a", "us-east-1b", "us-east-1c"]
ids = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
}

# Flow-log IAM role ARN must look like a real ARN so downstream policy
# resources that reference it pass provider-side validation.
mock_resource "aws_iam_role" {
defaults = {
arn = "arn:aws:iam::123456789012:role/mock-role"
}
}

# aws_flow_log.main validates that log_destination is a well-formed ARN, so the
# mocked CloudWatch log group must carry one (the random mock token isn't).
mock_resource "aws_cloudwatch_log_group" {
defaults = {
arn = "arn:aws:logs:us-east-1:123456789012:log-group:/aws/vpc-flow-logs/mock:*"
}
}
}

variables {
name_prefix = "hailbytes-test"
}

run "minimal_inputs_apply" {
command = apply

assert {
condition = output.vpc_id != ""
error_message = "vpc_id output must be non-empty"
}

assert {
condition = output.vpc_cidr == "10.0.0.0/16"
error_message = "vpc_cidr must match the default value"
}

assert {
condition = length(output.public_subnet_ids) == 2
error_message = "public_subnet_ids must contain one entry per AZ (default az_count = 2)"
}

assert {
condition = length(output.private_subnet_ids) == 2
error_message = "private_subnet_ids must contain one entry per AZ (default az_count = 2)"
}

assert {
condition = length(output.db_subnet_ids) == 2
error_message = "db_subnet_ids must contain one entry per AZ (default az_count = 2)"
}

assert {
condition = output.internet_gateway_id != ""
error_message = "internet_gateway_id output must be non-empty"
}

assert {
condition = length(output.nat_gateway_ids) == 2
error_message = "nat_gateway_ids must contain 2 entries when enable_nat_gateway = true (the default)"
}

assert {
condition = length(output.nat_gateway_public_ips) == 2
error_message = "nat_gateway_public_ips must contain 2 entries when enable_nat_gateway = true (the default)"
}
}
95 changes: 95 additions & 0 deletions modules/network/aws/tests/feature_flags.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Conditional resources must respect their feature flags. All runs use
# command = plan; no apply or credentials are needed.
#
# Every run explicitly sets enable_nat_gateway = false. When nat is enabled the
# private route table's for_each references aws_nat_gateway.main[*].id, which
# is unknown at plan time. Setting it false makes the for_each evaluate to []
# (always known) so each run can focus on the flag under test in isolation.

mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = {
names = ["us-east-1a", "us-east-1b", "us-east-1c"]
ids = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
}
}

variables {
name_prefix = "hailbytes-test"
enable_nat_gateway = false
}

run "nat_disabled_creates_no_gateways" {
command = plan

assert {
condition = length(aws_nat_gateway.main) == 0
error_message = "enable_nat_gateway = false must create zero NAT gateways."
}

assert {
condition = length(aws_eip.nat) == 0
error_message = "enable_nat_gateway = false must create zero Elastic IPs."
}
}

run "flow_logs_disabled_creates_no_resources" {
command = plan

variables {
enable_flow_logs = false
}

assert {
condition = length(aws_flow_log.main) == 0
error_message = "enable_flow_logs = false must create zero VPC Flow Log resources."
}

assert {
condition = length(aws_cloudwatch_log_group.flow_logs) == 0
error_message = "enable_flow_logs = false must create zero CloudWatch log groups."
}

assert {
condition = length(aws_iam_role.flow_logs) == 0
error_message = "enable_flow_logs = false must create zero IAM roles for flow logs."
}
}

run "flow_logs_enabled_creates_one_set" {
command = plan

assert {
condition = length(aws_flow_log.main) == 1
error_message = "enable_flow_logs = true (the default) must create exactly one flow log."
}

assert {
condition = length(aws_cloudwatch_log_group.flow_logs) == 1
error_message = "enable_flow_logs = true must create exactly one CloudWatch log group."
}
}

run "three_az_span_creates_three_subnets_each" {
command = plan

variables {
az_count = 3
}

assert {
condition = length(aws_subnet.public) == 3
error_message = "az_count = 3 must create three public subnets."
}

assert {
condition = length(aws_subnet.private) == 3
error_message = "az_count = 3 must create three private subnets."
}

assert {
condition = length(aws_subnet.db) == 3
error_message = "az_count = 3 must create three database subnets."
}
}
83 changes: 83 additions & 0 deletions modules/network/azure/tests/basic.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Minimal-input apply against a mocked azurerm provider. No real credentials and
# no API calls. Proves the module instantiates with only its required variables
# and that every operator-facing output is populated.
#
# The Azure network module creates a vnet, three subnets (lb, workload, and a
# Postgres-delegated db subnet), a baseline NSG per subnet, a private DNS zone
# for Postgres Flexible Server, and a DNS zone virtual-network link. All
# resources are unconditional, so a single apply run covers the full surface.
#
# azurerm_network_security_group is mocked with a well-formed ID because the
# azurerm_subnet_network_security_group_association resource validates the NSG
# ID format; the mock provider's random token would otherwise fail that check.

mock_provider "azurerm" {
mock_resource "azurerm_virtual_network" {
defaults = {
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hailbytes-test/providers/Microsoft.Network/virtualNetworks/mock-vnet"
}
}

mock_resource "azurerm_subnet" {
defaults = {
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hailbytes-test/providers/Microsoft.Network/virtualNetworks/mock-vnet/subnets/mock-snet"
}
}

mock_resource "azurerm_network_security_group" {
defaults = {
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hailbytes-test/providers/Microsoft.Network/networkSecurityGroups/mock-nsg"
}
}

mock_resource "azurerm_private_dns_zone" {
defaults = {
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hailbytes-test/providers/Microsoft.Network/privateDnsZones/privatelink.postgres.database.azure.com"
}
}
}

variables {
name_prefix = "hailbytes-test"
resource_group_name = "rg-hailbytes-test"
location = "eastus"
}

run "minimal_inputs_apply" {
command = apply

assert {
condition = output.vnet_id != ""
error_message = "vnet_id output must be non-empty"
}

assert {
condition = output.vnet_name != ""
error_message = "vnet_name output must be non-empty"
}

assert {
condition = output.workload_subnet_id != ""
error_message = "workload_subnet_id output must be non-empty (pass to var.subnet_id / var.vm_subnet_id on workload modules)"
}

assert {
condition = output.lb_subnet_id != ""
error_message = "lb_subnet_id output must be non-empty"
}

assert {
condition = output.db_delegated_subnet_id != ""
error_message = "db_delegated_subnet_id output must be non-empty (pass to var.db_delegated_subnet_id on ha-hot-hot / unlimited-scale)"
}

assert {
condition = output.private_dns_zone_id != ""
error_message = "private_dns_zone_id output must be non-empty (pass to var.private_dns_zone_id on ha-hot-hot / unlimited-scale)"
}

assert {
condition = output.private_dns_zone_name == "privatelink.postgres.database.azure.com"
error_message = "private_dns_zone_name must be the well-known Postgres Flexible Server FQDN"
}
}
1 change: 1 addition & 0 deletions modules/sat-aws-single/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module "this" {
allow_internet_ingress = var.allow_internet_ingress
enable_snapshots = var.enable_snapshots
marketplace_product_code = var.marketplace_product_code
enable_flow_logs = var.enable_flow_logs

# Patching and migration safety
create_backup_bucket = var.create_backup_bucket
Expand Down
6 changes: 6 additions & 0 deletions modules/sat-aws-single/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@ output "pre_patch_ssm_document_name" {
value = module.this.pre_patch_ssm_document_name
sensitive = false
}

output "flow_log_group_name" {
description = "CloudWatch log group name receiving VPC Flow Logs. Empty string when enable_flow_logs is false."
value = module.this.flow_log_group_name
sensitive = false
}
6 changes: 6 additions & 0 deletions modules/sat-aws-single/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ variable "backup_noncurrent_version_expiration_days" {
default = 365
}

variable "enable_flow_logs" {
description = "Enable VPC Flow Logs for the provided VPC, sending ALL traffic to a CloudWatch log group named /aws/vpc-flow-logs/<name_prefix>. Matches the default stated in SECURITY-DEFAULTS.md."
type = bool
default = true
}

variable "tags" {
description = "Additional tags applied to every resource."
type = map(string)
Expand Down
Loading
Loading