skills/ionfury/homelab/opentofu-modules

opentofu-modules

SKILL.md

OpenTofu Modules & Testing

Write OpenTofu modules and tests for the homelab infrastructure. Modules live in infrastructure/modules/, tests in infrastructure/modules/<name>/tests/.

Quick Reference

# Run tests for a module
task tg:test-<module>          # e.g., task tg:test-config

# Format all HCL
task tg:fmt

# Version pinned in .opentofu-version (currently 1.11.2)

Module Structure

Every module MUST have:

infrastructure/modules/<name>/
├── variables.tf    # Input definitions with descriptions and validations
├── main.tf         # Primary resources and locals
├── outputs.tf      # Output definitions
├── versions.tf     # Provider and OpenTofu version constraints
└── tests/          # Test directory
    └── *.tftest.hcl

Test File Structure

Use .tftest.hcl extension. Define top-level variables for defaults inherited by all run blocks.

# Top-level variables set defaults for ALL run blocks
variables {
  name     = "test-cluster"
  features = ["gateway-api", "longhorn"]

  networking = {
    id           = 1
    internal_tld = "internal.test.local"
    # ... other required fields
  }

  # Default machine - inherited unless overridden
  machines = {
    node1 = {
      cluster = "test-cluster"
      type    = "controlplane"
      install = { selector = "disk.model = *" }
      interfaces = [{
        id           = "eth0"
        hardwareAddr = "aa:bb:cc:dd:ee:01"
        addresses    = [{ ip = "192.168.10.101" }]
      }]
    }
  }
}

run "descriptive_test_name" {
  command = plan  # Use plan mode - no real resources created

  variables {
    features = ["prometheus"]  # Only override what differs
  }

  assert {
    condition     = output.some_value == "expected"
    error_message = "Descriptive failure message"
  }
}

Key Patterns

Use command = plan

Always use plan mode for tests. This validates configuration without creating resources.

Variable Inheritance

Only include variables in run blocks when they differ from defaults. Minimizes duplication.

# CORRECT: Override only what changes
run "feature_enabled" {
  command = plan
  variables {
    features = ["prometheus"]
  }
  assert { ... }
}

# AVOID: Repeating all variables
run "feature_enabled" {
  command = plan
  variables {
    name     = "test-cluster"      # Unnecessary - inherited
    features = ["prometheus"]
    machines = { ... }             # Unnecessary - inherited
  }
}

Assert Against Outputs

Reference module outputs in assertions, not internal resources.

assert {
  condition     = length(output.machines) == 2
  error_message = "Expected 2 machines"
}

assert {
  condition     = output.talos.kubernetes_version == "1.32.0"
  error_message = "Version mismatch"
}

Test Feature Flags

Test both enabled and disabled states:

run "feature_enabled" {
  command = plan
  variables { features = ["longhorn"] }

  assert {
    condition = alltrue([
      for m in output.talos.talos_machines :
      contains(m.install.extensions, "iscsi-tools")
    ])
    error_message = "Extension should be added when feature enabled"
  }
}

run "feature_disabled" {
  command = plan
  variables { features = [] }

  assert {
    condition = alltrue([
      for m in output.talos.talos_machines :
      !contains(m.install.extensions, "iscsi-tools")
    ])
    error_message = "Extension should not be present without feature"
  }
}

Test Validations

Use expect_failures to verify variable validation rules:

run "invalid_version_rejected" {
  command = plan
  variables {
    versions = {
      talos = "1.9.0"  # Missing v prefix - should fail
      # ...
    }
  }
  expect_failures = [var.versions]
}

Common Assertions

# Check length
condition = length(output.items) == 3

# Check key exists
condition = contains(keys(output.map), "expected_key")

# Check value in list
condition = contains(output.list, "expected_value")

# Check string contains
condition = strcontains(output.config, "expected_substring")

# Check all items match
condition = alltrue([for item in output.list : item.enabled == true])

# Check any item matches
condition = anytrue([for item in output.list : item.name == "target"])

# Nested check with labels/annotations
condition = anytrue([
  for label in output.machines["node1"].labels :
  label.key == "expected-label" && label.value == "expected-value"
])

Test Organization

Organize tests by concern:

  • plan.tftest.hcl - Basic structure and output validation
  • validation.tftest.hcl - Input validation rules
  • feature_<name>.tftest.hcl - Feature flag behavior
  • edge_cases.tftest.hcl - Boundary conditions

Detailed Reference

For OpenTofu testing syntax, mock providers, and advanced patterns, see: references/opentofu-testing.md

Weekly Installs
55
Repository
ionfury/homelab
GitHub Stars
22
First Seen
Jan 25, 2026
Installed on
gemini-cli55
github-copilot55
codex55
opencode55
amp54
kimi-cli54