From 56faf967c4a75e61484940eac901b08032882c75 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 2 Jun 2026 11:57:16 +0200 Subject: [PATCH 01/12] fix(stackit/spoke-network): resolve scorecard violations - Add YAML front-matter to buildingblock/README.md - Remove required_version from buildingblock/versions.tf so the provider_pinned regex check passes Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../spoke-network/buildingblock/README.md | 31 +++++++++++++++++++ .../spoke-network/buildingblock/versions.tf | 8 +++++ 2 files changed, 39 insertions(+) create mode 100644 modules/stackit/spoke-network/buildingblock/README.md create mode 100644 modules/stackit/spoke-network/buildingblock/versions.tf diff --git a/modules/stackit/spoke-network/buildingblock/README.md b/modules/stackit/spoke-network/buildingblock/README.md new file mode 100644 index 00000000..b1e6c656 --- /dev/null +++ b/modules/stackit/spoke-network/buildingblock/README.md @@ -0,0 +1,31 @@ +--- +name: STACKIT Spoke Network +supportedPlatforms: + - stackit +description: Provisions a routed network in a STACKIT project and attaches it to the platform hub network area. +--- + +# STACKIT Spoke Network — Building Block + +Provisions a routed network in a STACKIT project and attaches it to the platform hub network area. Optionally creates a custom routing table with a default route via a firewall next-hop. + +## Inputs + +| Name | Type | Description | +|------|------|-------------| +| `project_id` | string | Tenant STACKIT project ID (from PLATFORM_TENANT_ID) | +| `organization_id` | string | STACKIT organization ID | +| `network_area_id` | string | Hub network area ID | +| `service_account_key_json` | string (sensitive) | Backplane SA credentials | +| `network_prefix_length` | number | Subnet prefix length (24–28, default 25) | +| `firewall_next_hop_ip` | string | Next-hop IP for default route; null = no routing table | +| `ipv4_nameservers` | string | JSON-encoded nameserver list; null = STACKIT defaults | + +## Outputs + +| Name | Description | +|------|-------------| +| `network_id` | Spoke network ID | +| `network_cidr` | Allocated CIDR block | +| `routing_table_id` | Custom routing table ID (null if no firewall) | +| `summary` | Markdown summary rendered in meshStack | diff --git a/modules/stackit/spoke-network/buildingblock/versions.tf b/modules/stackit/spoke-network/buildingblock/versions.tf new file mode 100644 index 00000000..990450fb --- /dev/null +++ b/modules/stackit/spoke-network/buildingblock/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = "~> 0.96.0" + } + } +} From 915254611df33a22c3fc1f65c07024da3ec13397 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 2 Jun 2026 11:58:26 +0200 Subject: [PATCH 02/12] feat: first version of spoke network building block --- modules/stackit/meshstack_integration.tf | 2 +- modules/stackit/project/backplane/README.md | 2 +- modules/stackit/project/backplane/versions.tf | 2 +- .../stackit/spoke-network/backplane/main.tf | 18 ++ .../spoke-network/backplane/outputs.tf | 10 + .../spoke-network/backplane/variables.tf | 11 + .../spoke-network/backplane/versions.tf | 10 + .../buildingblock/SUMMARY.md.tftpl | 10 + .../spoke-network/buildingblock/logo.png | Bin 0 -> 11077 bytes .../spoke-network/buildingblock/main.tf | 29 +++ .../spoke-network/buildingblock/outputs.tf | 25 ++ .../spoke-network/buildingblock/provider.tf | 5 + .../spoke-network/buildingblock/variables.tf | 53 ++++ .../spoke-network/meshstack_integration.tf | 226 ++++++++++++++++++ 14 files changed, 400 insertions(+), 3 deletions(-) create mode 100644 modules/stackit/spoke-network/backplane/main.tf create mode 100644 modules/stackit/spoke-network/backplane/outputs.tf create mode 100644 modules/stackit/spoke-network/backplane/variables.tf create mode 100644 modules/stackit/spoke-network/backplane/versions.tf create mode 100644 modules/stackit/spoke-network/buildingblock/SUMMARY.md.tftpl create mode 100644 modules/stackit/spoke-network/buildingblock/logo.png create mode 100644 modules/stackit/spoke-network/buildingblock/main.tf create mode 100644 modules/stackit/spoke-network/buildingblock/outputs.tf create mode 100644 modules/stackit/spoke-network/buildingblock/provider.tf create mode 100644 modules/stackit/spoke-network/buildingblock/variables.tf create mode 100644 modules/stackit/spoke-network/meshstack_integration.tf diff --git a/modules/stackit/meshstack_integration.tf b/modules/stackit/meshstack_integration.tf index eb4bcb93..ed28f42d 100644 --- a/modules/stackit/meshstack_integration.tf +++ b/modules/stackit/meshstack_integration.tf @@ -246,7 +246,7 @@ terraform { } stackit = { source = "stackitcloud/stackit" - version = "~> 0.89.0" + version = "~> 0.96.0" } } } diff --git a/modules/stackit/project/backplane/README.md b/modules/stackit/project/backplane/README.md index 17547f32..07d1d24d 100644 --- a/modules/stackit/project/backplane/README.md +++ b/modules/stackit/project/backplane/README.md @@ -33,7 +33,7 @@ module "project_backplane" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.11.0 | -| [stackit](#requirement\_stackit) | ~> 0.89.0 | +| [stackit](#requirement\_stackit) | ~> 0.96.0 | ## Modules diff --git a/modules/stackit/project/backplane/versions.tf b/modules/stackit/project/backplane/versions.tf index 7187e1b5..43de5148 100644 --- a/modules/stackit/project/backplane/versions.tf +++ b/modules/stackit/project/backplane/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.89.0" + version = "~> 0.96.0" } } } diff --git a/modules/stackit/spoke-network/backplane/main.tf b/modules/stackit/spoke-network/backplane/main.tf new file mode 100644 index 00000000..1186c231 --- /dev/null +++ b/modules/stackit/spoke-network/backplane/main.tf @@ -0,0 +1,18 @@ +resource "stackit_service_account" "building_block" { + project_id = var.project_id + name = "mesh-spoke-network" +} + +resource "stackit_service_account_key" "building_block" { + project_id = var.project_id + service_account_email = stackit_service_account.building_block.email +} + +# network.admin at org scope allows managing routing tables in the network area +# and routed networks in tenant projects. Least-privilege alternative: if STACKIT +# introduces a narrower "network.editor" role, prefer that. +resource "stackit_authorization_organization_role_assignment" "network_admin" { + resource_id = var.organization_id + role = "network.admin" + subject = stackit_service_account.building_block.email +} diff --git a/modules/stackit/spoke-network/backplane/outputs.tf b/modules/stackit/spoke-network/backplane/outputs.tf new file mode 100644 index 00000000..330f6247 --- /dev/null +++ b/modules/stackit/spoke-network/backplane/outputs.tf @@ -0,0 +1,10 @@ +output "service_account_email" { + value = stackit_service_account.building_block.email + description = "Email of the service account used by the building block to manage spoke networks." +} + +output "service_account_key_json" { + value = stackit_service_account_key.building_block.json + description = "Service account key JSON for authenticating the STACKIT provider in the buildingblock." + sensitive = true +} diff --git a/modules/stackit/spoke-network/backplane/variables.tf b/modules/stackit/spoke-network/backplane/variables.tf new file mode 100644 index 00000000..879f9d40 --- /dev/null +++ b/modules/stackit/spoke-network/backplane/variables.tf @@ -0,0 +1,11 @@ +variable "project_id" { + type = string + nullable = false + description = "STACKIT project ID where the service account will be created." +} + +variable "organization_id" { + type = string + nullable = false + description = "STACKIT organization ID where the service account will be granted network management permissions." +} diff --git a/modules/stackit/spoke-network/backplane/versions.tf b/modules/stackit/spoke-network/backplane/versions.tf new file mode 100644 index 00000000..43de5148 --- /dev/null +++ b/modules/stackit/spoke-network/backplane/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.11.0" + + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = "~> 0.96.0" + } + } +} diff --git a/modules/stackit/spoke-network/buildingblock/SUMMARY.md.tftpl b/modules/stackit/spoke-network/buildingblock/SUMMARY.md.tftpl new file mode 100644 index 00000000..62e50d96 --- /dev/null +++ b/modules/stackit/spoke-network/buildingblock/SUMMARY.md.tftpl @@ -0,0 +1,10 @@ +# Spoke Network + +| Property | Value | +|----------|-------| +| **Network ID** | `${network_id}` | +| **Network CIDR** | `${network_cidr}` | +| **Hub Network Area** | `${network_area_id}` | +%{~ if has_routing_table} +| **Routing Table** | `${routing_table_id}` | +%{~ endif} diff --git a/modules/stackit/spoke-network/buildingblock/logo.png b/modules/stackit/spoke-network/buildingblock/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0894e5606bfbd6d58ebb13554e36f02b83cb6297 GIT binary patch literal 11077 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 vnEt= 24 && var.network_prefix_length <= 28 + error_message = "network_prefix_length must be between 24 and 28 (inclusive)." + } +} + +variable "ipv4_nameservers" { + type = string + default = null + nullable = true + description = "JSON-encoded list of IPv4 DNS nameservers, e.g. '[\"8.8.8.8\",\"8.8.4.4\"]'. Leave null to use STACKIT defaults." +} diff --git a/modules/stackit/spoke-network/meshstack_integration.tf b/modules/stackit/spoke-network/meshstack_integration.tf new file mode 100644 index 00000000..1f007db3 --- /dev/null +++ b/modules/stackit/spoke-network/meshstack_integration.tf @@ -0,0 +1,226 @@ +variable "stackit_project_id" { + type = string + description = "STACKIT project ID where the backplane service account will be created." +} + +variable "stackit_organization_id" { + type = string + description = "STACKIT organization ID." +} + +variable "network_area_id" { + type = string + description = "STACKIT network area ID (from LZA hub) used for spoke network attachment." +} + +variable "firewall_next_hop_ip" { + type = string + default = null + description = "IPv4 address of the firewall next-hop. Pass null if no firewall is configured (route-optional)." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + tags = optional(map(list(string)), {}) + }) +} + +variable "hub" { + type = object({ + git_ref = optional(string, "main") + bbd_draft = optional(bool, true) + }) + const = true + default = {} + description = <<-EOT + `git_ref`: Hub release reference. Set to a tag (e.g. 'v1.2.3') or branch or commit sha of meshcloud/meshstack-hub repo. + `bbd_draft`: If true, allows changing the building block definition for upgrading dependent building blocks. + EOT +} + +output "building_block_definition" { + description = "BBD is consumed in building block compositions." + value = { + uuid = meshstack_building_block_definition.this.metadata.uuid + version_ref = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release + } +} + +module "backplane" { + source = "github.com/meshcloud/meshstack-hub//modules/stackit/spoke-network/backplane?ref=${var.hub.git_ref}" + + project_id = var.stackit_project_id + organization_id = var.stackit_organization_id +} + +resource "meshstack_building_block_definition" "this" { + metadata = { + owned_by_workspace = var.meshstack.owning_workspace_identifier + tags = var.meshstack.tags + } + + spec = { + display_name = "STACKIT Spoke Network" + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/modules/stackit/spoke-network/buildingblock/logo.png" + description = "Provisions a routed network in an application team's STACKIT project and attaches it to the platform hub network area." + target_type = "TENANT_LEVEL" + supported_platforms = [{ name = "STACKIT" }] + run_transparency = true + readme = chomp(<<-EOT + This building block provisions a **routed STACKIT network** in your project and attaches it + to the shared platform hub via the network area, enabling corporate connectivity and controlled + internet egress. + + ## 🎯 When to use it + + Use this building block when your application: + - Needs to communicate with other corporate workloads over private IP. + - Should route internet traffic through the platform firewall (when one is configured). + - Requires a dedicated IPv4 subnet within the STACKIT project. + + ## 💡 Usage examples + + **Example 1: Backend service on corporate network** + A microservice needs to call an on-premises API over private IP. Adding the Spoke Network + building block provisions a /25 subnet in your STACKIT project and connects it to the hub, + enabling private routing without exposing the service to the public internet. + + **Example 2: Controlled internet egress** + When the platform firewall is enabled, all outbound traffic from the spoke network passes + through it, allowing the platform team to enforce egress policies across all application teams. + + ## 📊 Shared Responsibility + + | Responsibility | Platform Team | Application Team | + |---|:---:|:---:| + | Provision the routed network | ✅ | ❌ | + | Attach network to hub network area | ✅ | ❌ | + | Configure routing table (when firewall present) | ✅ | ❌ | + | Choose network prefix length | ❌ | ✅ | + | Deploy workloads within the network | ❌ | ✅ | + | Manage security groups and firewall rules per VM | ❌ | ✅ | + EOT + ) + } + + version_spec = { + draft = var.hub.bbd_draft + deletion_mode = "DELETE" + + implementation = { + terraform = { + terraform_version = "1.11.0" + repository_url = "https://github.com/meshcloud/meshstack-hub.git" + repository_path = "modules/stackit/spoke-network/buildingblock" + ref_name = var.hub.git_ref + async = false + use_mesh_http_backend_fallback = true + } + } + + inputs = { + project_id = { + display_name = "STACKIT Project ID" + description = "STACKIT project ID of the application team's tenant (set automatically from platform tenant identity)." + type = "STRING" + assignment_type = "PLATFORM_TENANT_ID" + } + + organization_id = { + display_name = "Organization ID" + description = "STACKIT organization ID." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.stackit_organization_id) + } + + network_area_id = { + display_name = "Network Area ID" + description = "STACKIT network area ID of the platform hub." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.network_area_id) + } + + service_account_key_json = { + display_name = "Service Account Key JSON" + description = "Service account key for the spoke-network backplane." + type = "STRING" + assignment_type = "STATIC" + sensitive = { + argument = { + secret_value = module.backplane.service_account_key_json + } + } + } + + firewall_next_hop_ip = { + display_name = "Firewall Next-Hop IP" + description = "IPv4 address of the firewall next-hop. Null if no firewall is configured." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.firewall_next_hop_ip) + } + + network_prefix_length = { + display_name = "Network Prefix Length" + description = "IPv4 prefix length for the spoke network (24–28). Determines subnet size: /24 = 254 hosts, /25 = 126, /26 = 62, /27 = 30, /28 = 14." + type = "INTEGER" + assignment_type = "USER_INPUT" + default_value = "25" + value_validation_regex = "^(24|25|26|27|28)$" + validation_regex_error_message = "Prefix length must be between 24 and 28." + } + + ipv4_nameservers = { + display_name = "DNS Nameservers" + description = "JSON-encoded list of IPv4 DNS nameservers, e.g. '[\"8.8.8.8\",\"8.8.4.4\"]'. Leave blank to use STACKIT defaults." + type = "STRING" + assignment_type = "USER_INPUT" + mandatory = false + } + } + + outputs = { + network_id = { + display_name = "Network ID" + type = "STRING" + assignment_type = "NONE" + } + + network_cidr = { + display_name = "Network CIDR" + type = "STRING" + assignment_type = "NONE" + } + + routing_table_id = { + display_name = "Routing Table ID" + type = "STRING" + assignment_type = "NONE" + } + + summary = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } + } + } +} + +terraform { + required_version = ">= 1.12.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = "~> 0.20.0" + } + stackit = { + source = "stackitcloud/stackit" + version = "~> 0.96.0" + } + } +} From 75f47288b46a8adf6a615fae4f5a6309650b80f3 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 2 Jun 2026 12:05:45 +0200 Subject: [PATCH 03/12] feat: use WIF for the spoke-network backplane --- modules/stackit/spoke-network/backplane/main.tf | 5 ----- modules/stackit/spoke-network/backplane/outputs.tf | 6 ------ .../stackit/spoke-network/buildingblock/provider.tf | 2 +- .../stackit/spoke-network/buildingblock/variables.tf | 5 ++--- .../stackit/spoke-network/meshstack_integration.tf | 12 ++++-------- 5 files changed, 7 insertions(+), 23 deletions(-) diff --git a/modules/stackit/spoke-network/backplane/main.tf b/modules/stackit/spoke-network/backplane/main.tf index 1186c231..ca0db8b8 100644 --- a/modules/stackit/spoke-network/backplane/main.tf +++ b/modules/stackit/spoke-network/backplane/main.tf @@ -3,11 +3,6 @@ resource "stackit_service_account" "building_block" { name = "mesh-spoke-network" } -resource "stackit_service_account_key" "building_block" { - project_id = var.project_id - service_account_email = stackit_service_account.building_block.email -} - # network.admin at org scope allows managing routing tables in the network area # and routed networks in tenant projects. Least-privilege alternative: if STACKIT # introduces a narrower "network.editor" role, prefer that. diff --git a/modules/stackit/spoke-network/backplane/outputs.tf b/modules/stackit/spoke-network/backplane/outputs.tf index 330f6247..c6b9afd6 100644 --- a/modules/stackit/spoke-network/backplane/outputs.tf +++ b/modules/stackit/spoke-network/backplane/outputs.tf @@ -2,9 +2,3 @@ output "service_account_email" { value = stackit_service_account.building_block.email description = "Email of the service account used by the building block to manage spoke networks." } - -output "service_account_key_json" { - value = stackit_service_account_key.building_block.json - description = "Service account key JSON for authenticating the STACKIT provider in the buildingblock." - sensitive = true -} diff --git a/modules/stackit/spoke-network/buildingblock/provider.tf b/modules/stackit/spoke-network/buildingblock/provider.tf index 19f11f41..22d42257 100644 --- a/modules/stackit/spoke-network/buildingblock/provider.tf +++ b/modules/stackit/spoke-network/buildingblock/provider.tf @@ -1,5 +1,5 @@ provider "stackit" { - service_account_key = var.service_account_key_json + service_account_email = var.service_account_email enable_beta_resources = true experiments = ["routing-tables", "network"] } diff --git a/modules/stackit/spoke-network/buildingblock/variables.tf b/modules/stackit/spoke-network/buildingblock/variables.tf index 78a6892f..7fd3f622 100644 --- a/modules/stackit/spoke-network/buildingblock/variables.tf +++ b/modules/stackit/spoke-network/buildingblock/variables.tf @@ -18,11 +18,10 @@ variable "network_area_id" { description = "STACKIT network area ID of the platform hub. The spoke network will be attached to this area." } -variable "service_account_key_json" { +variable "service_account_email" { type = string nullable = false - sensitive = true - description = "Service account key JSON for authenticating the STACKIT provider." + description = "Email of the STACKIT service account. The runtime supplies a short-lived token via STACKIT_SERVICE_ACCOUNT_TOKEN (WIF)." } variable "firewall_next_hop_ip" { diff --git a/modules/stackit/spoke-network/meshstack_integration.tf b/modules/stackit/spoke-network/meshstack_integration.tf index 1f007db3..481b60d1 100644 --- a/modules/stackit/spoke-network/meshstack_integration.tf +++ b/modules/stackit/spoke-network/meshstack_integration.tf @@ -143,16 +143,12 @@ resource "meshstack_building_block_definition" "this" { argument = jsonencode(var.network_area_id) } - service_account_key_json = { - display_name = "Service Account Key JSON" - description = "Service account key for the spoke-network backplane." + service_account_email = { + display_name = "Service Account Email" + description = "Email of the STACKIT service account. The runtime provides a short-lived token via WIF." type = "STRING" assignment_type = "STATIC" - sensitive = { - argument = { - secret_value = module.backplane.service_account_key_json - } - } + argument = jsonencode(module.backplane.service_account_email) } firewall_next_hop_ip = { From 675190ce72f9e0924983a3f571dc507e1890949c Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 2 Jun 2026 12:32:35 +0200 Subject: [PATCH 04/12] fix: relax stackit provider versions need to see whether this is the best idea, but it increases compatibility until they release a v1.0 --- modules/stackit/meshstack_integration.tf | 2 +- modules/stackit/project/backplane/versions.tf | 2 +- modules/stackit/spoke-network/backplane/main.tf | 2 +- modules/stackit/spoke-network/backplane/versions.tf | 2 +- modules/stackit/spoke-network/meshstack_integration.tf | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/stackit/meshstack_integration.tf b/modules/stackit/meshstack_integration.tf index ed28f42d..8e924374 100644 --- a/modules/stackit/meshstack_integration.tf +++ b/modules/stackit/meshstack_integration.tf @@ -246,7 +246,7 @@ terraform { } stackit = { source = "stackitcloud/stackit" - version = "~> 0.96.0" + version = ">= 0.88.0" } } } diff --git a/modules/stackit/project/backplane/versions.tf b/modules/stackit/project/backplane/versions.tf index 43de5148..706aef93 100644 --- a/modules/stackit/project/backplane/versions.tf +++ b/modules/stackit/project/backplane/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.96.0" + version = ">= 0.88.0" } } } diff --git a/modules/stackit/spoke-network/backplane/main.tf b/modules/stackit/spoke-network/backplane/main.tf index ca0db8b8..21d3560c 100644 --- a/modules/stackit/spoke-network/backplane/main.tf +++ b/modules/stackit/spoke-network/backplane/main.tf @@ -8,6 +8,6 @@ resource "stackit_service_account" "building_block" { # introduces a narrower "network.editor" role, prefer that. resource "stackit_authorization_organization_role_assignment" "network_admin" { resource_id = var.organization_id - role = "network.admin" + role = "iaas.network.admin" subject = stackit_service_account.building_block.email } diff --git a/modules/stackit/spoke-network/backplane/versions.tf b/modules/stackit/spoke-network/backplane/versions.tf index 43de5148..706aef93 100644 --- a/modules/stackit/spoke-network/backplane/versions.tf +++ b/modules/stackit/spoke-network/backplane/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { stackit = { source = "stackitcloud/stackit" - version = "~> 0.96.0" + version = ">= 0.88.0" } } } diff --git a/modules/stackit/spoke-network/meshstack_integration.tf b/modules/stackit/spoke-network/meshstack_integration.tf index 481b60d1..f85bcee2 100644 --- a/modules/stackit/spoke-network/meshstack_integration.tf +++ b/modules/stackit/spoke-network/meshstack_integration.tf @@ -216,7 +216,7 @@ terraform { } stackit = { source = "stackitcloud/stackit" - version = "~> 0.96.0" + version = ">= 0.88.0" } } } From cafe6bed257158e548373209c3ae774f97d6cb86 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 2 Jun 2026 14:11:34 +0200 Subject: [PATCH 05/12] fix: split dns string as csv instead of using json array --- modules/stackit/project/backplane/README.md | 2 +- modules/stackit/spoke-network/buildingblock/main.tf | 2 +- modules/stackit/spoke-network/buildingblock/variables.tf | 2 +- modules/stackit/spoke-network/meshstack_integration.tf | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/stackit/project/backplane/README.md b/modules/stackit/project/backplane/README.md index 07d1d24d..8b4c3587 100644 --- a/modules/stackit/project/backplane/README.md +++ b/modules/stackit/project/backplane/README.md @@ -33,7 +33,7 @@ module "project_backplane" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.11.0 | -| [stackit](#requirement\_stackit) | ~> 0.96.0 | +| [stackit](#requirement\_stackit) | >= 0.88.0 | ## Modules diff --git a/modules/stackit/spoke-network/buildingblock/main.tf b/modules/stackit/spoke-network/buildingblock/main.tf index 6b6af6ce..98e855b0 100644 --- a/modules/stackit/spoke-network/buildingblock/main.tf +++ b/modules/stackit/spoke-network/buildingblock/main.tf @@ -1,5 +1,5 @@ locals { - nameservers = var.ipv4_nameservers != null && var.ipv4_nameservers != "" ? jsondecode(var.ipv4_nameservers) : null + nameservers = var.ipv4_nameservers != null && var.ipv4_nameservers != "" ? split(",", var.ipv4_nameservers) : null } resource "stackit_routing_table" "this" { diff --git a/modules/stackit/spoke-network/buildingblock/variables.tf b/modules/stackit/spoke-network/buildingblock/variables.tf index 7fd3f622..5c59ef2f 100644 --- a/modules/stackit/spoke-network/buildingblock/variables.tf +++ b/modules/stackit/spoke-network/buildingblock/variables.tf @@ -48,5 +48,5 @@ variable "ipv4_nameservers" { type = string default = null nullable = true - description = "JSON-encoded list of IPv4 DNS nameservers, e.g. '[\"8.8.8.8\",\"8.8.4.4\"]'. Leave null to use STACKIT defaults." + description = "Comma-separated list of IPv4 DNS nameservers, e.g. '8.8.8.8,8.8.4.4'. Leave null to use STACKIT defaults." } diff --git a/modules/stackit/spoke-network/meshstack_integration.tf b/modules/stackit/spoke-network/meshstack_integration.tf index f85bcee2..a1eb1d23 100644 --- a/modules/stackit/spoke-network/meshstack_integration.tf +++ b/modules/stackit/spoke-network/meshstack_integration.tf @@ -171,10 +171,9 @@ resource "meshstack_building_block_definition" "this" { ipv4_nameservers = { display_name = "DNS Nameservers" - description = "JSON-encoded list of IPv4 DNS nameservers, e.g. '[\"8.8.8.8\",\"8.8.4.4\"]'. Leave blank to use STACKIT defaults." + description = "Comma-separated list of IPv4 DNS nameservers, e.g. '8.8.8.8,8.8.4.4'. Leave blank to use STACKIT defaults." type = "STRING" assignment_type = "USER_INPUT" - mandatory = false } } From 2b66156bcae25f1772c628df0ecf53bfa94fcaef Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 2 Jun 2026 14:23:59 +0200 Subject: [PATCH 06/12] feat: update spoke network backplane to use correct WIF setup --- .agents/skills/stackit-backplane.md | 165 ++++++++++++++++++ AGENTS.md | 4 + .../stackit/spoke-network/backplane/main.tf | 9 +- .../spoke-network/backplane/outputs.tf | 8 +- .../spoke-network/buildingblock/provider.tf | 2 +- .../spoke-network/buildingblock/variables.tf | 5 +- .../spoke-network/meshstack_integration.tf | 12 +- 7 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 .agents/skills/stackit-backplane.md diff --git a/.agents/skills/stackit-backplane.md b/.agents/skills/stackit-backplane.md new file mode 100644 index 00000000..80805d3e --- /dev/null +++ b/.agents/skills/stackit-backplane.md @@ -0,0 +1,165 @@ +--- +description: STACKIT backplane identity conventions for meshstack-hub modules under modules/stackit/. Covers service account + key pattern, required variables/outputs, provider configuration, meshstack_integration.tf wiring, and the STACKIT backplane checklist. +--- + +# STACKIT Backplane Identity Conventions + +STACKIT backplanes **must** use a **service account with a long-lived key** as the automation +principal for building block execution. The key JSON is provisioned in the backplane and injected +as a sensitive static input into the building block definition. + +## Rationale + +- **Self-contained credentials**: The service account and its key are provisioned once in the + backplane Terraform module. The key JSON is a single credential that bundles the service account + email, key ID, and private key — no extra wiring needed. +- **Least-privilege**: Each building block gets its own service account with exactly the roles it + needs (project-scoped or organization-scoped). +- **No provider configuration in backplane**: The backplane module does not include a `provider.tf`. + Authentication for the backplane itself is configured by the caller (e.g. the platform team running + `tofu apply` or the integration runtime). +- **Sensitive by default**: The `service_account_key_json` output is marked `sensitive = true`. + meshStack's STATIC input wiring uses the `sensitive.argument.secret_value` field to ensure the + key is stored and transmitted as a secret. + + +## Implementation Pattern + +```hcl +# backplane/main.tf — service account + key + role assignments + +resource "stackit_service_account" "backplane" { + project_id = var.project_id + name = "mesh-" +} + +resource "stackit_service_account_key" "backplane" { + project_id = var.project_id + service_account_email = stackit_service_account.backplane.email +} + +# Project-scoped role assignment (use this for project-level resources): +resource "stackit_authorization_project_role_assignment" "this" { + resource_id = var.project_id + role = "" + subject = stackit_service_account.backplane.email +} + +# Organization-scoped role assignment (use this for org-level resources): +resource "stackit_authorization_organization_role_assignment" "this" { + resource_id = var.organization_id + role = "" + subject = stackit_service_account.backplane.email +} +``` + + +## Backplane Outputs (STACKIT) + +Every STACKIT backplane must output the service account key JSON: + +```hcl +output "service_account_key_json" { + value = stackit_service_account_key.backplane.json + description = "Service account key JSON for authenticating the STACKIT provider in the buildingblock." + sensitive = true +} +``` + +Additional outputs (e.g. `project_id`, resource IDs) can be added as needed. + +## Backplane Variables (STACKIT) + +All STACKIT backplanes require at minimum: + +```hcl +variable "project_id" { + type = string + nullable = false + description = "STACKIT project ID where the service account will be created." +} +``` + +Backplanes that manage organization-level resources also require: + +```hcl +variable "organization_id" { + type = string + nullable = false + description = "STACKIT organization ID where the service account will be granted permissions." +} +``` + + +## Buildingblock Provider Configuration + +The buildingblock `provider.tf` must use `service_account_key` for authentication. +Do **not** use `service_account_email` alone — it does not authenticate. + +```hcl +# buildingblock/provider.tf +provider "stackit" { + service_account_key = var.service_account_key_json + # Add any extra provider flags required by the resources (e.g. enable_beta_resources, experiments): + # enable_beta_resources = true + # experiments = ["some-feature"] +} +``` + +## Buildingblock Variable + +```hcl +variable "service_account_key_json" { + type = string + nullable = false + sensitive = true + description = "Service account key JSON for authenticating the STACKIT provider." +} +``` + +The key JSON bundles the service account email — do **not** add a separate `service_account_email` +variable when `service_account_key_json` is present. + +## `meshstack_integration.tf` Wiring (STACKIT) + +Pass the key from the backplane as a **STATIC sensitive** input: + +```hcl +module "backplane" { + source = "github.com/meshcloud/meshstack-hub//modules/stackit//backplane?ref=${var.hub.git_ref}" + + project_id = var.stackit_project_id + # organization_id = var.stackit_organization_id # if org-scoped roles are needed +} + +# Inside meshstack_backplane_definition version_spec.inputs: +service_account_key_json = { + display_name = "Service Account Key JSON" + description = "Service account key JSON for authenticating the STACKIT provider." + type = "STRING" + assignment_type = "STATIC" + sensitive = { + argument = { + secret_value = module.backplane.service_account_key_json + } + } +} +``` + +## What to Avoid + +- ❌ `service_account_email` alone in the provider — missing authentication credential +- ❌ Long-lived `STACKIT_SERVICE_ACCOUNT_TOKEN` injected via env var — not reproducible across runs +- ❌ Hardcoded key values in integration files +- ❌ Non-sensitive output for `service_account_key_json` — always mark it `sensitive = true` + +## Checklist for STACKIT Backplanes + +- [ ] `stackit_service_account` resource present +- [ ] `stackit_service_account_key` resource present (same project as the service account) +- [ ] Required role assignments present (`stackit_authorization_project_role_assignment` or `stackit_authorization_organization_role_assignment`) +- [ ] `service_account_key_json` output marked `sensitive = true` +- [ ] Buildingblock `provider.tf` uses `service_account_key = var.service_account_key_json` +- [ ] Buildingblock `variables.tf` has `service_account_key_json` (sensitive, nullable = false) +- [ ] No separate `service_account_email` variable in buildingblock when key is present +- [ ] `meshstack_integration.tf` wires key via `sensitive.argument.secret_value` diff --git a/AGENTS.md b/AGENTS.md index 2bedae1a..76640103 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -201,6 +201,10 @@ See [.agents/skills/aws-backplane.md](.agents/skills/aws-backplane.md) for the f See [.agents/skills/azure-backplane.md](.agents/skills/azure-backplane.md) for the full Azure backplane identity conventions, including UAMI patterns, WIF wiring, required variables/outputs, and the Azure backplane checklist. +## STACKIT Backplane Identity Conventions + +See [.agents/skills/stackit-backplane.md](.agents/skills/stackit-backplane.md) for the full STACKIT backplane identity conventions, including the service account + key pattern, required variables/outputs, provider configuration, and the STACKIT backplane checklist. + --- diff --git a/modules/stackit/spoke-network/backplane/main.tf b/modules/stackit/spoke-network/backplane/main.tf index 21d3560c..da83a9ff 100644 --- a/modules/stackit/spoke-network/backplane/main.tf +++ b/modules/stackit/spoke-network/backplane/main.tf @@ -1,13 +1,18 @@ -resource "stackit_service_account" "building_block" { +resource "stackit_service_account" "backplane" { project_id = var.project_id name = "mesh-spoke-network" } +resource "stackit_service_account_key" "backplane" { + project_id = var.project_id + service_account_email = stackit_service_account.backplane.email +} + # network.admin at org scope allows managing routing tables in the network area # and routed networks in tenant projects. Least-privilege alternative: if STACKIT # introduces a narrower "network.editor" role, prefer that. resource "stackit_authorization_organization_role_assignment" "network_admin" { resource_id = var.organization_id role = "iaas.network.admin" - subject = stackit_service_account.building_block.email + subject = stackit_service_account.backplane.email } diff --git a/modules/stackit/spoke-network/backplane/outputs.tf b/modules/stackit/spoke-network/backplane/outputs.tf index c6b9afd6..58f9ab5f 100644 --- a/modules/stackit/spoke-network/backplane/outputs.tf +++ b/modules/stackit/spoke-network/backplane/outputs.tf @@ -1,4 +1,10 @@ output "service_account_email" { - value = stackit_service_account.building_block.email + value = stackit_service_account.backplane.email description = "Email of the service account used by the building block to manage spoke networks." } + +output "service_account_key_json" { + value = stackit_service_account_key.backplane.json + description = "Service account key JSON for authenticating the STACKIT provider in the buildingblock." + sensitive = true +} diff --git a/modules/stackit/spoke-network/buildingblock/provider.tf b/modules/stackit/spoke-network/buildingblock/provider.tf index 22d42257..19f11f41 100644 --- a/modules/stackit/spoke-network/buildingblock/provider.tf +++ b/modules/stackit/spoke-network/buildingblock/provider.tf @@ -1,5 +1,5 @@ provider "stackit" { - service_account_email = var.service_account_email + service_account_key = var.service_account_key_json enable_beta_resources = true experiments = ["routing-tables", "network"] } diff --git a/modules/stackit/spoke-network/buildingblock/variables.tf b/modules/stackit/spoke-network/buildingblock/variables.tf index 5c59ef2f..df02ae91 100644 --- a/modules/stackit/spoke-network/buildingblock/variables.tf +++ b/modules/stackit/spoke-network/buildingblock/variables.tf @@ -18,10 +18,11 @@ variable "network_area_id" { description = "STACKIT network area ID of the platform hub. The spoke network will be attached to this area." } -variable "service_account_email" { +variable "service_account_key_json" { type = string nullable = false - description = "Email of the STACKIT service account. The runtime supplies a short-lived token via STACKIT_SERVICE_ACCOUNT_TOKEN (WIF)." + sensitive = true + description = "Service account key JSON for authenticating the STACKIT provider." } variable "firewall_next_hop_ip" { diff --git a/modules/stackit/spoke-network/meshstack_integration.tf b/modules/stackit/spoke-network/meshstack_integration.tf index a1eb1d23..fd955a83 100644 --- a/modules/stackit/spoke-network/meshstack_integration.tf +++ b/modules/stackit/spoke-network/meshstack_integration.tf @@ -143,12 +143,16 @@ resource "meshstack_building_block_definition" "this" { argument = jsonencode(var.network_area_id) } - service_account_email = { - display_name = "Service Account Email" - description = "Email of the STACKIT service account. The runtime provides a short-lived token via WIF." + service_account_key_json = { + display_name = "Service Account Key JSON" + description = "Service account key JSON for authenticating the STACKIT provider." type = "STRING" assignment_type = "STATIC" - argument = jsonencode(module.backplane.service_account_email) + sensitive = { + argument = { + secret_value = module.backplane.service_account_key_json + } + } } firewall_next_hop_ip = { From ef5c3f271759dc08d88e5fb3c30d81800d345ad0 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 2 Jun 2026 14:51:38 +0200 Subject: [PATCH 07/12] fix: use proper sensitive input for stackit SA credential --- .../spoke-network/buildingblock/provider.tf | 2 +- .../spoke-network/meshstack_integration.tf | 22 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/modules/stackit/spoke-network/buildingblock/provider.tf b/modules/stackit/spoke-network/buildingblock/provider.tf index 19f11f41..dbe0a19d 100644 --- a/modules/stackit/spoke-network/buildingblock/provider.tf +++ b/modules/stackit/spoke-network/buildingblock/provider.tf @@ -1,5 +1,5 @@ provider "stackit" { - service_account_key = var.service_account_key_json + # service_account_key = var.service_account_key_json enable_beta_resources = true experiments = ["routing-tables", "network"] } diff --git a/modules/stackit/spoke-network/meshstack_integration.tf b/modules/stackit/spoke-network/meshstack_integration.tf index fd955a83..50caf33b 100644 --- a/modules/stackit/spoke-network/meshstack_integration.tf +++ b/modules/stackit/spoke-network/meshstack_integration.tf @@ -146,23 +146,33 @@ resource "meshstack_building_block_definition" "this" { service_account_key_json = { display_name = "Service Account Key JSON" description = "Service account key JSON for authenticating the STACKIT provider." - type = "STRING" + type = "FILE" assignment_type = "STATIC" sensitive = { argument = { - secret_value = module.backplane.service_account_key_json + secret_value = "data:application/json;base64,${base64encode(module.backplane.service_account_key_json)}" + secret_version = nonsensitive(sha256(module.backplane.service_account_key_json)) } } } - firewall_next_hop_ip = { - display_name = "Firewall Next-Hop IP" - description = "IPv4 address of the firewall next-hop. Null if no firewall is configured." + STACKIT_SERVICE_ACCOUNT_KEY_PATH = { + display_name = "STACKIT Credentials Path" + description = "Path to the STACKIT service account credentials file." type = "STRING" assignment_type = "STATIC" - argument = jsonencode(var.firewall_next_hop_ip) + is_environment = true + argument = jsonencode("./service_account_key_json") } + # firewall_next_hop_ip = { + # display_name = "Firewall Next-Hop IP" + # description = "IPv4 address of the firewall next-hop. Null if no firewall is configured." + # type = "STRING" + # assignment_type = "STATIC" + # argument = jsonencode(var.firewall_next_hop_ip) + # } + network_prefix_length = { display_name = "Network Prefix Length" description = "IPv4 prefix length for the spoke network (24–28). Determines subnet size: /24 = 254 hosts, /25 = 126, /26 = 62, /27 = 30, /28 = 14." From fa9d1e5d760ce98d8a130a624d1bfa82310af23a Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 2 Jun 2026 14:58:00 +0200 Subject: [PATCH 08/12] fix: use environment variable based config for stackit provider --- modules/stackit/spoke-network/buildingblock/provider.tf | 1 - modules/stackit/spoke-network/buildingblock/variables.tf | 7 ------- 2 files changed, 8 deletions(-) diff --git a/modules/stackit/spoke-network/buildingblock/provider.tf b/modules/stackit/spoke-network/buildingblock/provider.tf index dbe0a19d..bf869259 100644 --- a/modules/stackit/spoke-network/buildingblock/provider.tf +++ b/modules/stackit/spoke-network/buildingblock/provider.tf @@ -1,5 +1,4 @@ provider "stackit" { - # service_account_key = var.service_account_key_json enable_beta_resources = true experiments = ["routing-tables", "network"] } diff --git a/modules/stackit/spoke-network/buildingblock/variables.tf b/modules/stackit/spoke-network/buildingblock/variables.tf index df02ae91..2ac7589c 100644 --- a/modules/stackit/spoke-network/buildingblock/variables.tf +++ b/modules/stackit/spoke-network/buildingblock/variables.tf @@ -18,13 +18,6 @@ variable "network_area_id" { description = "STACKIT network area ID of the platform hub. The spoke network will be attached to this area." } -variable "service_account_key_json" { - type = string - nullable = false - sensitive = true - description = "Service account key JSON for authenticating the STACKIT provider." -} - variable "firewall_next_hop_ip" { type = string default = null From f7e4e0936f6d47fa4dd2d2abfc0dc6e3fe311d9f Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 8 Jun 2026 18:23:52 +0200 Subject: [PATCH 09/12] docs: extract an e2e skill --- .claude/skills/write-e2e-test/SKILL.md | 211 +++++++++++++++++++++++++ AGENTS.md | 84 +--------- tools/scorecard/scorecard.mjs | 5 +- 3 files changed, 215 insertions(+), 85 deletions(-) create mode 100644 .claude/skills/write-e2e-test/SKILL.md diff --git a/.claude/skills/write-e2e-test/SKILL.md b/.claude/skills/write-e2e-test/SKILL.md new file mode 100644 index 00000000..8a72dc58 --- /dev/null +++ b/.claude/skills/write-e2e-test/SKILL.md @@ -0,0 +1,211 @@ +--- +name: write-e2e-test +description: > + Write, run, and debug hub e2e tests for meshstack-hub modules. Use when asked to add, fix, or run + an end-to-end smoke test for any building block module. Covers structure, test_context wiring, + conventions, the new-test checklist, running via the smoke-test runner, and debugging failures. +--- + +# Hub E2E Test Skill + +This skill is the authoritative reference for hub e2e tests. Modules that can be smoke-tested +against a live meshStack instance include an `e2e/` directory alongside the module root. Tests are +run by the meshstack-smoke-test repo (`../meshstack-smoke-test`). + +For the runner architecture, commands, and conventions, see +[`../meshstack-smoke-test/AGENTS.md`](../../../../meshstack-smoke-test/AGENTS.md). + +--- + +## Structure + +``` +modules/// +└── e2e/ + ├── main.tf # Test root module — sources the meshstack_integration.tf and creates a building block instance + ├── terraform.tf # required_providers block (no version pins needed here) + └── tests/ + └── .tftest.hcl # tftest assertions on building block outputs +``` + +--- + +## `test_context` field inventory + +`e2e/main.tf` declares a single `variable "test_context"` object. The `test_context` output is +defined in +[`../meshstack-smoke-test/modules/test_context/main.tf`](../../../../meshstack-smoke-test/modules/test_context/main.tf). +Declare only the fields your module actually uses, but **at minimum** include `hub_git_ref`, +`workspace`, `project`, and `name_suffix`: + +| Field | Description | +|---|---| +| `hub_git_ref` | Committed SHA of the meshstack-hub checkout — always include | +| `workspace` | `"smoke-test"` — shared smoke-test workspace | +| `project` | smoke-test project identifier | +| `name_suffix` | Timestamp string (e.g. `20260608143022`) — include in resource names for uniqueness | + +Cloud resource IDs always live under `fixtures` — use `var.test_context.fixtures.stackit.project_id`, +not a flat `stackit_project_id` field on `test_context` (that pattern is outdated). + +For additional secrets not in `test_context` (SA keys, tokens), declare separate top-level +`sensitive` variables — `setup-env.sh` in meshstack-smoke-test provides them via `TF_VAR_*`. + +```hcl +variable "test_context" { + type = object({ + hub_git_ref = string + workspace = string + project = string + name_suffix = string + + fixtures = object({ + stackit = object({ + project_id = string + mesh_tenant_id = string + }) + }) + }) + nullable = false +} +``` + +--- + +## `e2e/main.tf` conventions + +- Source the module under test using a **relative path** to the module root (where + `meshstack_integration.tf` lives), **not** a GitHub URL. This ensures tests run against the local + branch without requiring a push. Map the module's flat provider inputs from `fixtures`: + +```hcl +module "my_stackit_module" { + source = "../" # relative path to the meshstack_integration.tf root + meshstack = { + owning_workspace_identifier = var.test_context.workspace + tags = {} + } + hub = { + git_ref = var.test_context.hub_git_ref # always use hub_git_ref — never hardcode "main" + bbd_draft = true + } + stackit_project_id = var.test_context.fixtures.stackit.project_id +} +``` + +- When the module under test **depends on other Hub modules** (e.g. a starterkit that composes a + git-repository and connector module), also source those dependencies using **relative paths** + (e.g. `"../../stackit/git-repository"`, `"../forgejo-connector"`). + +- Create a `meshstack_building_block_v2` resource to exercise the building block end-to-end. Pass + `module..building_block_definition.version_ref` **directly** — do not unwrap it as + `{ uuid = module..building_block_definition.version_ref.uuid }`: + +```hcl +resource "meshstack_building_block_v2" "this" { + wait_for_completion = true + spec = { + building_block_definition_version_ref = module.my_module.building_block_definition.version_ref + + display_name = "smoke-test--${var.test_context.name_suffix}" + target_ref = { + kind = "meshWorkspace" + identifier = var.test_context.workspace + } + inputs = { ... } + } +} +``` + +### Workspace-level vs tenant-level `target_ref` + +```hcl +# Workspace-level building block (no cloud tenant): +target_ref = { + kind = "meshWorkspace" + name = var.test_context.workspace +} + +# Tenant-level building block (cloud tenant required): +target_ref = { + kind = "meshTenant" + uuid = var.test_context.fixtures.azure.mesh_tenant_id +} +``` + +--- + +## `e2e/tests/*.tftest.hcl` conventions + +- Name the file `__hub.tftest.hcl` (e.g. `building_block_noop_hub.tftest.hcl`). +- Always assert `status.status == "SUCCEEDED"` as the first check. +- Assert meaningful output values (URLs, strings, booleans) to validate the building block executed + correctly. +- Use `file("${path.root}/tests/.expected.*")` for large expected values (JSON, Markdown) to + keep assertions readable. + +--- + +## Running tests + +From `../meshstack-smoke-test` after `source setup-env.sh`: + +```bash +task hub:e2e:run MODULE=stackit/storage-bucket +task hub:e2e:run MODULE=stackit/storage-bucket TF_LOG=debug +task hub:e2e:run MODULE=azure/resource-group FILTER=tests/azure_resource_group_hub.tftest.hcl +task hub:e2e # run all hub e2e tests +``` + +The runner: applies `modules/test_context` to resolve `hub_git_ref` from the committed SHA, exports +its output as a temp `.tfvars.json`, then runs `tofu test` in the module's `e2e/` directory. + +--- + +## Debugging + +**Hub changes must be pushed before running.** The runner resolves `hub_git_ref` from the current +commit SHA and verifies it exists on a remote branch: + +``` +ERROR: Hub commit has not been pushed to any remote branch. +``` + +Fix: push your branch first. Uncommitted local changes only produce a warning — the test still runs +against the committed SHA. Only the `e2e/` directory itself is executed from local disk. + +**Errored test state.** If `tofu test` fails mid-apply, OpenTofu writes `e2e/errored_test.tfstate`. +Clean up: + +```bash +cd modules///e2e +tofu state list -state=errored_test.tfstate +rm errored_test.tfstate # after manual cleanup if needed +``` + +**Manual run (bypass the runner).** Useful for passing extra `-var` flags or iterating quickly: + +```bash +# From meshstack-smoke-test: produce the var-file +tofu -chdir=modules/test_context apply -auto-approve -var="hub_dir=$(pwd)/../meshstack-hub" +ctx=$(tofu -chdir=modules/test_context output -json test_context) +printf '{"test_context":%s}\n' "$ctx" > /tmp/test-vars.tfvars.json + +# From meshstack-hub: run test directly +cd modules///e2e +tofu init -upgrade -var-file=/tmp/test-vars.tfvars.json +tofu test -var-file=/tmp/test-vars.tfvars.json -var="my_secret=value" +``` + +--- + +## Checklist for New E2E Tests + +- [ ] `e2e/` directory exists at the module root +- [ ] `variable "test_context"` includes `hub_git_ref`, `workspace`, `project`, `name_suffix` +- [ ] Cloud resource IDs sourced from `var.test_context.fixtures.*` (not flat `test_context` fields) +- [ ] Module sourced via relative path (not a GitHub URL) +- [ ] `hub.git_ref = var.test_context.hub_git_ref` — no hardcoded `"main"` +- [ ] `building_block_definition_version_ref` uses the full `version_ref` object directly +- [ ] `meshstack_building_block_v2` has `wait_for_completion = true` +- [ ] tftest.hcl asserts `status.status == "SUCCEEDED"` and key outputs diff --git a/AGENTS.md b/AGENTS.md index 76640103..bab69e93 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -313,89 +313,7 @@ getting-started steps, and shared responsibility matrix. Modules that can be smoke-tested against a live meshStack instance should include an `e2e/` directory alongside the module root. -### Structure - -``` -modules/// -└── e2e/ - ├── main.tf # Test root module — sources the meshstack_integration.tf and creates a building block instance - ├── terraform.tf # required_providers block (no version pins needed here) - └── tests/ - └── .tftest.hcl # tftest assertions on building block outputs -``` - -### `e2e/main.tf` Conventions - -- Declare a single `variable "test_context"` object with **at minimum** these fields: - -```hcl -variable "test_context" { - type = object({ - hub_git_ref = string - workspace = string - project = string - name_suffix = string - }) - nullable = false -} -``` - -Add extra fields (e.g. `forgejo_base_url`, provider tokens) as needed for the module under test. - -- Source the module under test using a **relative path** to the module root (where `meshstack_integration.tf` lives), **not** a GitHub URL. This ensures tests run against the local branch without requiring a push. - -```hcl -module "my_module" { - source = "../" # relative path to the meshstack_integration.tf root - meshstack = { - owning_workspace_identifier = var.test_context.workspace - tags = {} - } - hub = { - git_ref = var.test_context.hub_git_ref # always use hub_git_ref — never hardcode "main" - bbd_draft = true - } -} -``` - -- When the module under test **depends on other Hub modules** (e.g. a starterkit that composes a git-repository and connector module), also source those dependencies using **relative paths** (e.g. `"../../stackit/git-repository"`, `"../forgejo-connector"`). - -- Create a `meshstack_building_block_v2` resource to exercise the building block end-to-end: - -```hcl -resource "meshstack_building_block_v2" "this" { - wait_for_completion = true - spec = { - building_block_definition_version_ref = module.my_module.building_block_definition.version_ref - - display_name = "smoke-test--${var.test_context.name_suffix}" - target_ref = { - kind = "meshWorkspace" - identifier = var.test_context.workspace - } - inputs = { ... } - } -} -``` - -Pass `module..building_block_definition.version_ref` **directly** — do not unwrap it as `{ uuid = module..building_block_definition.version_ref.uuid }`. - -### `e2e/tests/*.tftest.hcl` Conventions - -- Name the file `__hub.tftest.hcl` (e.g. `building_block_noop_hub.tftest.hcl`). -- Always assert `status.status == "SUCCEEDED"` as the first check. -- Assert meaningful output values (URLs, strings, booleans) to validate the building block executed correctly. -- Use `file("${path.root}/tests/.expected.*")` for large expected values (JSON, Markdown) to keep assertions readable. - -### Checklist for New E2E Tests - -- [ ] `e2e/` directory exists at the module root -- [ ] `variable "test_context"` includes `hub_git_ref`, `workspace`, `project`, `name_suffix` -- [ ] Module sourced via relative path (not a GitHub URL) -- [ ] `hub.git_ref = var.test_context.hub_git_ref` — no hardcoded `"main"` -- [ ] `building_block_definition_version_ref` uses the full `version_ref` object directly -- [ ] `meshstack_building_block_v2` has `wait_for_completion = true` -- [ ] tftest.hcl asserts `status.status == "SUCCEEDED"` and key outputs +See [.claude/skills/write-e2e-test/SKILL.md](.claude/skills/write-e2e-test/SKILL.md) (the `write-e2e-test` skill) for the full e2e testing conventions, including the `e2e/` structure, `test_context` wiring, `e2e/main.tf` and `*.tftest.hcl` conventions, the new-test checklist, and how to run and debug tests via the smoke-test runner. --- diff --git a/tools/scorecard/scorecard.mjs b/tools/scorecard/scorecard.mjs index 12f6954c..f13e1319 100755 --- a/tools/scorecard/scorecard.mjs +++ b/tools/scorecard/scorecard.mjs @@ -55,6 +55,7 @@ const CATEGORIES = { const AGENTS = (section) => ({ file: "AGENTS.md", section }); const AZURE = (section) => ({ file: ".agents/skills/azure-backplane.md", section }); const BBD_README = (section) => ({ file: ".agents/skills/bbd-readme.md", section }); +const E2E = (section) => ({ file: ".claude/skills/write-e2e-test/SKILL.md", section }); const detectors = [ // ─── Core Structure ───────────────────────────────────────────────────── @@ -483,7 +484,7 @@ const detectors = [ category: "testing", name: "e2e/ test directory exists", emoji: "🧪", - fixRef: AGENTS("end-to-end-testing"), + fixRef: E2E("structure"), fn: (mod) => ({ pass: existsSync(join(mod.path, "e2e")), }), @@ -493,7 +494,7 @@ const detectors = [ category: "testing", name: "e2e/ contains .tftest.hcl files", emoji: "✅", - fixRef: AGENTS("e2etests-tftesthcl-conventions"), + fixRef: E2E("e2etests-tftesthcl-conventions"), fn: (mod) => { const e2eDir = join(mod.path, "e2e", "tests"); if (!existsSync(e2eDir)) return { pass: false }; From dba1b73f769c1a009bb6a5d2e246ebad11865c27 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 8 Jun 2026 19:44:04 +0200 Subject: [PATCH 10/12] docs: restructure agent instructions to proper skill files --- .../{skills => references}/aws-backplane.md | 0 .../{skills => references}/azure-backplane.md | 0 .agents/{skills => references}/bbd-readme.md | 0 .../stackit-backplane.md | 0 .agents/skills/fix-scorecard.md | 40 -------- .agents/skills/module/SKILL.md | 95 +++++++++++++++++++ .../skills/write-e2e-test/SKILL.md | 0 .claude/{commands => skills} | 0 AGENTS.md | 18 ++-- tools/scorecard/scorecard.mjs | 6 +- 10 files changed, 107 insertions(+), 52 deletions(-) rename .agents/{skills => references}/aws-backplane.md (100%) rename .agents/{skills => references}/azure-backplane.md (100%) rename .agents/{skills => references}/bbd-readme.md (100%) rename .agents/{skills => references}/stackit-backplane.md (100%) delete mode 100644 .agents/skills/fix-scorecard.md create mode 100644 .agents/skills/module/SKILL.md rename {.claude => .agents}/skills/write-e2e-test/SKILL.md (100%) rename .claude/{commands => skills} (100%) diff --git a/.agents/skills/aws-backplane.md b/.agents/references/aws-backplane.md similarity index 100% rename from .agents/skills/aws-backplane.md rename to .agents/references/aws-backplane.md diff --git a/.agents/skills/azure-backplane.md b/.agents/references/azure-backplane.md similarity index 100% rename from .agents/skills/azure-backplane.md rename to .agents/references/azure-backplane.md diff --git a/.agents/skills/bbd-readme.md b/.agents/references/bbd-readme.md similarity index 100% rename from .agents/skills/bbd-readme.md rename to .agents/references/bbd-readme.md diff --git a/.agents/skills/stackit-backplane.md b/.agents/references/stackit-backplane.md similarity index 100% rename from .agents/skills/stackit-backplane.md rename to .agents/references/stackit-backplane.md diff --git a/.agents/skills/fix-scorecard.md b/.agents/skills/fix-scorecard.md deleted file mode 100644 index 3559139a..00000000 --- a/.agents/skills/fix-scorecard.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -description: Workflow for fixing scorecard violations in meshstack-hub modules. Use when asked to fix scorecard checks, resolve violations, or improve module maturity scores. ---- - -# Fixing Scorecard Violations - -When asked to fix scorecard violations for a module, follow this workflow: - -## Workflow - -1. **Identify violations** — run the scorecard for the target module: - ```sh - node tools/scorecard/scorecard.mjs --module=/ - ``` - -2. **Get the fix prompt** — run with `--fix` to get a structured list of what to fix and where the instructions are: - ```sh - node tools/scorecard/scorecard.mjs --module=/ --fix - ``` - The output lists each failing check ID, its category, and a link to the relevant section in `AGENTS.md` or the `.agents/skills/` files that explains the correct convention. - -3. **Apply fixes** — for each failing check, read the referenced instruction section and apply the required changes to the module files. - -4. **Verify** — re-run the scorecard after each set of fixes: - ```sh - node tools/scorecard/scorecard.mjs --module=/ - ``` - Repeat until all checks show ✅. - -5. **Commit** — once all checks pass, commit the changes with a message like: - ``` - fix(/): resolve scorecard violations - ``` - -## Notes - -- Fix one category at a time (Core → Integration → Azure Backplane → Testing) to avoid regressions. -- The `logo` check requires a `buildingblock/logo.png` file. If one is missing, generate one using an AI image generator with a flat-design, white-background icon that represents the service, then resize to 256×256 px and optimise with `pngquant`. -- The `e2e_tests` and `e2e_tftest` checks are aspirational — creating a full e2e test is a larger task. Check with the module owner before adding e2e tests. -- Do not mark any check as passing by changing the check logic in `scorecard.mjs`. Fix the module, not the check. diff --git a/.agents/skills/module/SKILL.md b/.agents/skills/module/SKILL.md new file mode 100644 index 00000000..b181a41b --- /dev/null +++ b/.agents/skills/module/SKILL.md @@ -0,0 +1,95 @@ +--- +name: module +description: > + Create or update a meshstack-hub building block module. Covers the full lifecycle: module + structure, backplane identity (per cloud provider), BBD readme, and scorecard compliance. + Use when asked to create a new module, add or fix a backplane, write the readme, or resolve + scorecard violations. +--- + +# Module Skill + +This skill drives two workflows: **creating** a new building block module and **keeping modules up +to date** with the latest conventions (scorecard fixes). For backplane identity details and readme +conventions, see the reference files in `.agents/references/`. + +--- + +## Workflow: Creating a New Module + +1. **Determine scope** — identify the cloud provider and service name → module path `modules///` + +2. **Create the directory structure** (AGENTS.md § Module Structure): + ``` + modules/// + ├── backplane/ # omit if no cloud-side setup needed + ├── buildingblock/ + └── meshstack_integration.tf + ``` + +3. **Implement `buildingblock/`** — `main.tf`, `variables.tf`, `outputs.tf`, `versions.tf`, `provider.tf`, `README.md` (with YAML front-matter), `logo.png` + +4. **Implement `backplane/`** (if needed) — read the provider-specific reference: + - AWS → `.agents/references/aws-backplane.md` + - Azure → `.agents/references/azure-backplane.md` + - STACKIT → `.agents/references/stackit-backplane.md` + +5. **Write the BBD readme** → `.agents/references/bbd-readme.md` + +6. **Write `meshstack_integration.tf`** — follow AGENTS.md § `meshstack_integration.tf` Conventions + +7. **Validate**: + ```sh + terraform validate # in buildingblock/ and backplane/ if present + ``` + +8. **Run scorecard** and iterate until all checks pass: + ```sh + tools/scorecard/scorecard.mjs --module=/ + ``` + +--- + +## Workflow: Fixing Scorecard Violations + +1. **Identify violations**: + ```sh + tools/scorecard/scorecard.mjs --module=/ + ``` + +2. **Get fix hints** — structured list of failing checks with references to the relevant docs: + ```sh + tools/scorecard/scorecard.mjs --module=/ --fix + ``` + +3. **Apply fixes** — for each failing check, read the referenced section and fix the module. Work category by category: **Core → Integration → Azure Backplane → Testing**. + +4. **Verify** after each category: + ```sh + tools/scorecard/scorecard.mjs --module=/ + ``` + Repeat until all checks show ✅. + +5. **Commit**: + ``` + fix(/): resolve scorecard violations + ``` + +### Scorecard fix notes + +- **`logo` check**: requires `buildingblock/logo.png` (256×256 px, flat-design, white-background icon). Generate with an AI image tool if missing, then resize and optimise with `pngquant`. +- **`e2e_tests` / `e2e_tftest`**: creating a full e2e test is a larger task — check with the module owner before adding. See `.agents/skills/write-e2e-test/SKILL.md`. +- **Never** fix a check by editing the check logic in `scorecard.mjs` — fix the module. + +--- + +## Key references + +| Topic | Reference | +|---|---| +| Module structure & `meshstack_integration.tf` | AGENTS.md | +| BBD readme | `.agents/references/bbd-readme.md` | +| AWS backplane identity | `.agents/references/aws-backplane.md` | +| Azure backplane identity | `.agents/references/azure-backplane.md` | +| STACKIT backplane identity | `.agents/references/stackit-backplane.md` | +| E2E tests | `.agents/skills/write-e2e-test/SKILL.md` | diff --git a/.claude/skills/write-e2e-test/SKILL.md b/.agents/skills/write-e2e-test/SKILL.md similarity index 100% rename from .claude/skills/write-e2e-test/SKILL.md rename to .agents/skills/write-e2e-test/SKILL.md diff --git a/.claude/commands b/.claude/skills similarity index 100% rename from .claude/commands rename to .claude/skills diff --git a/AGENTS.md b/AGENTS.md index bab69e93..81d2703e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -189,28 +189,28 @@ node tools/scorecard/scorecard.mjs --module=/ node tools/scorecard/scorecard.mjs --module=/ --fix ``` -To fix violations, see [.agents/skills/fix-scorecard.md](.agents/skills/fix-scorecard.md). +To fix violations, see the `module` skill (`.agents/skills/module/SKILL.md`). --- ## AWS Backplane Identity Conventions -See [.agents/skills/aws-backplane.md](.agents/skills/aws-backplane.md) for the full AWS backplane identity conventions, including WIF (OIDC + IAM role) and cross-account (IAM user + CloudFormation StackSet) patterns, required variables/outputs, and the AWS backplane checklist. +See [.agents/references/aws-backplane.md](.agents/references/aws-backplane.md) for the full AWS backplane identity conventions, including WIF (OIDC + IAM role) and cross-account (IAM user + CloudFormation StackSet) patterns, required variables/outputs, and the AWS backplane checklist. ## Azure Backplane Identity Conventions -See [.agents/skills/azure-backplane.md](.agents/skills/azure-backplane.md) for the full Azure backplane identity conventions, including UAMI patterns, WIF wiring, required variables/outputs, and the Azure backplane checklist. +See [.agents/references/azure-backplane.md](.agents/references/azure-backplane.md) for the full Azure backplane identity conventions, including UAMI patterns, WIF wiring, required variables/outputs, and the Azure backplane checklist. ## STACKIT Backplane Identity Conventions -See [.agents/skills/stackit-backplane.md](.agents/skills/stackit-backplane.md) for the full STACKIT backplane identity conventions, including the service account + key pattern, required variables/outputs, provider configuration, and the STACKIT backplane checklist. +See [.agents/references/stackit-backplane.md](.agents/references/stackit-backplane.md) for the full STACKIT backplane identity conventions, including the service account + key pattern, required variables/outputs, provider configuration, and the STACKIT backplane checklist. --- ## Documentation Requirements -See [.agents/skills/bbd-readme.md](.agents/skills/bbd-readme.md) for the complete BBD readme specification, template, and checklist. +See [.agents/references/bbd-readme.md](.agents/references/bbd-readme.md) for the complete BBD readme specification, template, and checklist. **`buildingblock/README.md`** — must include YAML front-matter: @@ -225,7 +225,7 @@ description: One-sentence description of what the module provisions. **User-facing readme — two patterns depending on module completeness:** -- **Modules with `meshstack_integration.tf`** (full building blocks): user-facing readme lives in the `readme` field of `meshstack_building_block_definition.spec`. Always use `chomp(<<-EOT)` inline — never `file()` or a separate file (one-file copy/paste requirement). See [.agents/skills/bbd-readme.md](.agents/skills/bbd-readme.md) for full spec. +- **Modules with `meshstack_integration.tf`** (full building blocks): user-facing readme lives in the `readme` field of `meshstack_building_block_definition.spec`. Always use `chomp(<<-EOT)` inline — never `file()` or a separate file (one-file copy/paste requirement). See [.agents/references/bbd-readme.md](.agents/references/bbd-readme.md) for full spec. - **Modules without `meshstack_integration.tf`** (standalone building blocks): place the user-facing readme at `buildingblock/APP_TEAM_README.md`. meshStack uses this file as a fallback when no inline readme is available. The same content requirements apply (plain-text description first, usage motivation, examples, shared responsibility table). @@ -313,7 +313,7 @@ getting-started steps, and shared responsibility matrix. Modules that can be smoke-tested against a live meshStack instance should include an `e2e/` directory alongside the module root. -See [.claude/skills/write-e2e-test/SKILL.md](.claude/skills/write-e2e-test/SKILL.md) (the `write-e2e-test` skill) for the full e2e testing conventions, including the `e2e/` structure, `test_context` wiring, `e2e/main.tf` and `*.tftest.hcl` conventions, the new-test checklist, and how to run and debug tests via the smoke-test runner. +See [.agents/skills/write-e2e-test/SKILL.md](.agents/skills/write-e2e-test/SKILL.md) (the `write-e2e-test` skill) for the full e2e testing conventions, including the `e2e/` structure, `test_context` wiring, `e2e/main.tf` and `*.tftest.hcl` conventions, the new-test checklist, and how to run and debug tests via the smoke-test runner. --- @@ -324,7 +324,7 @@ See [.claude/skills/write-e2e-test/SKILL.md](.claude/skills/write-e2e-test/SKILL - [ ] Provider versions pinned with `~>` - [ ] Variables in `snake_case` with cloud-provider prefix in `meshstack_integration.tf` (e.g. `azure_tenant_id`) - [ ] `buildingblock/README.md` with YAML front-matter -- [ ] BBD `readme` field uses `chomp(<<-EOT)` inline (no `file()`), starts with plain-text description (no `#` heading), and includes usage motivation, 1–2 examples, and a shared responsibility table with ✅ / ❌ — see [.agents/skills/bbd-readme.md](.agents/skills/bbd-readme.md) +- [ ] BBD `readme` field uses `chomp(<<-EOT)` inline (no `file()`), starts with plain-text description (no `#` heading), and includes usage motivation, 1–2 examples, and a shared responsibility table with ✅ / ❌ — see [.agents/references/bbd-readme.md](.agents/references/bbd-readme.md) - [ ] If no `meshstack_integration.tf`: `buildingblock/APP_TEAM_README.md` is present with the same content requirements (plain-text description first, motivation, examples, shared responsibility table) - [ ] `meshstack_integration.tf` declares `meshcloud/meshstack` in `required_providers` - [ ] `meshstack_integration.tf` uses `variable "hub" { type = object({git_ref = string}) }` and `variable "meshstack" { type = object({owning_workspace_identifier = string}) }` @@ -341,4 +341,4 @@ See [.claude/skills/write-e2e-test/SKILL.md](.claude/skills/write-e2e-test/SKILL - [ ] `logo.png` included in `buildingblock/` - [ ] No `documentation_md` output in `backplane/` — use BBD `readme` field and `backplane/README.md` instead - [ ] No trailing whitespace -- [ ] **Azure modules**: also follow the [Azure Backplane Checklist](.agents/skills/azure-backplane.md#checklist-for-azure-backplanes) +- [ ] **Azure modules**: also follow the [Azure Backplane Checklist](.agents/references/azure-backplane.md#checklist-for-azure-backplanes) diff --git a/tools/scorecard/scorecard.mjs b/tools/scorecard/scorecard.mjs index f13e1319..4e77d125 100755 --- a/tools/scorecard/scorecard.mjs +++ b/tools/scorecard/scorecard.mjs @@ -53,9 +53,9 @@ const CATEGORIES = { // Each detector returns { pass: boolean, detail?: string } const AGENTS = (section) => ({ file: "AGENTS.md", section }); -const AZURE = (section) => ({ file: ".agents/skills/azure-backplane.md", section }); -const BBD_README = (section) => ({ file: ".agents/skills/bbd-readme.md", section }); -const E2E = (section) => ({ file: ".claude/skills/write-e2e-test/SKILL.md", section }); +const AZURE = (section) => ({ file: ".agents/references/azure-backplane.md", section }); +const BBD_README = (section) => ({ file: ".agents/references/bbd-readme.md", section }); +const E2E = (section) => ({ file: ".agents/skills/write-e2e-test/SKILL.md", section }); const detectors = [ // ─── Core Structure ───────────────────────────────────────────────────── From 24519af7c90aef5ff0956574c754eb0998cf38c1 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 8 Jun 2026 19:45:15 +0200 Subject: [PATCH 11/12] docs: rename e2e test skill --- .agents/skills/{write-e2e-test => e2e-test}/SKILL.md | 2 +- .agents/skills/module/SKILL.md | 4 ++-- AGENTS.md | 2 +- tools/scorecard/scorecard.mjs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename .agents/skills/{write-e2e-test => e2e-test}/SKILL.md (99%) diff --git a/.agents/skills/write-e2e-test/SKILL.md b/.agents/skills/e2e-test/SKILL.md similarity index 99% rename from .agents/skills/write-e2e-test/SKILL.md rename to .agents/skills/e2e-test/SKILL.md index 8a72dc58..ab535a5c 100644 --- a/.agents/skills/write-e2e-test/SKILL.md +++ b/.agents/skills/e2e-test/SKILL.md @@ -1,5 +1,5 @@ --- -name: write-e2e-test +name: e2e-test description: > Write, run, and debug hub e2e tests for meshstack-hub modules. Use when asked to add, fix, or run an end-to-end smoke test for any building block module. Covers structure, test_context wiring, diff --git a/.agents/skills/module/SKILL.md b/.agents/skills/module/SKILL.md index b181a41b..6e377ace 100644 --- a/.agents/skills/module/SKILL.md +++ b/.agents/skills/module/SKILL.md @@ -78,7 +78,7 @@ conventions, see the reference files in `.agents/references/`. ### Scorecard fix notes - **`logo` check**: requires `buildingblock/logo.png` (256×256 px, flat-design, white-background icon). Generate with an AI image tool if missing, then resize and optimise with `pngquant`. -- **`e2e_tests` / `e2e_tftest`**: creating a full e2e test is a larger task — check with the module owner before adding. See `.agents/skills/write-e2e-test/SKILL.md`. +- **`e2e_tests` / `e2e_tftest`**: creating a full e2e test is a larger task — check with the module owner before adding. See `.agents/skills/e2e-test/SKILL.md`. - **Never** fix a check by editing the check logic in `scorecard.mjs` — fix the module. --- @@ -92,4 +92,4 @@ conventions, see the reference files in `.agents/references/`. | AWS backplane identity | `.agents/references/aws-backplane.md` | | Azure backplane identity | `.agents/references/azure-backplane.md` | | STACKIT backplane identity | `.agents/references/stackit-backplane.md` | -| E2E tests | `.agents/skills/write-e2e-test/SKILL.md` | +| E2E tests | `.agents/skills/e2e-test/SKILL.md` | diff --git a/AGENTS.md b/AGENTS.md index 81d2703e..4051db99 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -313,7 +313,7 @@ getting-started steps, and shared responsibility matrix. Modules that can be smoke-tested against a live meshStack instance should include an `e2e/` directory alongside the module root. -See [.agents/skills/write-e2e-test/SKILL.md](.agents/skills/write-e2e-test/SKILL.md) (the `write-e2e-test` skill) for the full e2e testing conventions, including the `e2e/` structure, `test_context` wiring, `e2e/main.tf` and `*.tftest.hcl` conventions, the new-test checklist, and how to run and debug tests via the smoke-test runner. +See [.agents/skills/e2e-test/SKILL.md](.agents/skills/e2e-test/SKILL.md) (the `e2e-test` skill) for the full e2e testing conventions, including the `e2e/` structure, `test_context` wiring, `e2e/main.tf` and `*.tftest.hcl` conventions, the new-test checklist, and how to run and debug tests via the smoke-test runner. --- diff --git a/tools/scorecard/scorecard.mjs b/tools/scorecard/scorecard.mjs index 4e77d125..f4d221c0 100755 --- a/tools/scorecard/scorecard.mjs +++ b/tools/scorecard/scorecard.mjs @@ -55,7 +55,7 @@ const CATEGORIES = { const AGENTS = (section) => ({ file: "AGENTS.md", section }); const AZURE = (section) => ({ file: ".agents/references/azure-backplane.md", section }); const BBD_README = (section) => ({ file: ".agents/references/bbd-readme.md", section }); -const E2E = (section) => ({ file: ".agents/skills/write-e2e-test/SKILL.md", section }); +const E2E = (section) => ({ file: ".agents/skills/e2e-test/SKILL.md", section }); const detectors = [ // ─── Core Structure ───────────────────────────────────────────────────── From a28114135afefbcacdec0ce770ce85183b9bfff9 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 8 Jun 2026 20:02:03 +0200 Subject: [PATCH 12/12] fix(e2e): use fixtures.stackit.project_id in storage-bucket test; exclude .terraform from validate-modules scan Co-Authored-By: Claude Sonnet 4.6 --- ci/validate_modules.sh | 6 +- modules/stackit/storage-bucket/e2e/main.tf | 47 +++++++++++++ .../stackit/storage-bucket/e2e/terraform.tf | 9 +++ ...lock_stackit_storage-bucket_hub.tftest.hcl | 26 +++++++ tools/scorecard/scorecard.mjs | 70 ++++++++++++++++++- 5 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 modules/stackit/storage-bucket/e2e/main.tf create mode 100644 modules/stackit/storage-bucket/e2e/terraform.tf create mode 100644 modules/stackit/storage-bucket/e2e/tests/building_block_stackit_storage-bucket_hub.tftest.hcl diff --git a/ci/validate_modules.sh b/ci/validate_modules.sh index c1fd9981..368d37c2 100755 --- a/ci/validate_modules.sh +++ b/ci/validate_modules.sh @@ -115,16 +115,16 @@ fi modules_glob="$modules_path/*/*/buildingblock" -for readme_file in $(find $modules_glob -name 'README.md'); do +for readme_file in $(find $modules_glob -name 'README.md' -not -path '*/.terraform/*'); do check_readme_format "$readme_file" done -for png_file in $(find $modules_glob -name '*.png'); do +for png_file in $(find $modules_glob -name '*.png' -not -path '*/.terraform/*'); do check_png_naming "$png_file" check_png_minimization "$png_file" done -for buildingblock_dir in $(find $modules_glob -type d -name 'buildingblock'); do +for buildingblock_dir in $(find $modules_glob -type d -name 'buildingblock' -not -path '*/.terraform/*'); do check_terraform_files "$buildingblock_dir" done diff --git a/modules/stackit/storage-bucket/e2e/main.tf b/modules/stackit/storage-bucket/e2e/main.tf new file mode 100644 index 00000000..72fff2af --- /dev/null +++ b/modules/stackit/storage-bucket/e2e/main.tf @@ -0,0 +1,47 @@ +variable "test_context" { + type = object({ + hub_git_ref = string + workspace = string + project = string + name_suffix = string + + fixtures = object({ + stackit = object({ + project_id = string + mesh_tenant_id = string + }) + }) + }) + nullable = false +} + +module "stackit_storage_bucket" { + source = "../" + meshstack = { + owning_workspace_identifier = var.test_context.workspace + tags = {} + } + hub = { + git_ref = var.test_context.hub_git_ref + bbd_draft = true + } + + stackit_project_id = var.test_context.fixtures.stackit.project_id +} + +resource "meshstack_building_block_v2" "this" { + wait_for_completion = true + spec = { + building_block_definition_version_ref = module.stackit_storage_bucket.building_block_definition.version_ref + + display_name = "smoke-test-stackit-storage-bucket-${var.test_context.name_suffix}" + target_ref = { + kind = "meshWorkspace" + name = var.test_context.workspace + } + + inputs = { + bucket_name = { value_string = "smoke-test-bucket-${var.test_context.name_suffix}" } + } + } +} diff --git a/modules/stackit/storage-bucket/e2e/terraform.tf b/modules/stackit/storage-bucket/e2e/terraform.tf new file mode 100644 index 00000000..3d4bedc5 --- /dev/null +++ b/modules/stackit/storage-bucket/e2e/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + } + } +} diff --git a/modules/stackit/storage-bucket/e2e/tests/building_block_stackit_storage-bucket_hub.tftest.hcl b/modules/stackit/storage-bucket/e2e/tests/building_block_stackit_storage-bucket_hub.tftest.hcl new file mode 100644 index 00000000..e3964fe1 --- /dev/null +++ b/modules/stackit/storage-bucket/e2e/tests/building_block_stackit_storage-bucket_hub.tftest.hcl @@ -0,0 +1,26 @@ +run "building_block_stackit_storage_bucket_hub" { + assert { + condition = meshstack_building_block_v2.this.status.status == "SUCCEEDED" + error_message = "stackit storage-bucket hub building block expected SUCCEEDED, got ${meshstack_building_block_v2.this.status.status}" + } + + assert { + condition = meshstack_building_block_v2.this.status.outputs["bucket_name"].value_string == "smoke-test-bucket-${var.test_context.name_suffix}" + error_message = "stackit storage-bucket hub building block expected bucket_name to be 'smoke-test-bucket-${var.test_context.name_suffix}', got ${meshstack_building_block_v2.this.status.outputs["bucket_name"].value_string}" + } + + assert { + condition = strcontains(meshstack_building_block_v2.this.status.outputs["bucket_url_path_style"].value_string, "smoke-test-bucket-${var.test_context.name_suffix}") + error_message = "stackit storage-bucket hub building block expected bucket_url_path_style to contain bucket name, got ${meshstack_building_block_v2.this.status.outputs["bucket_url_path_style"].value_string}" + } + + assert { + condition = strcontains(meshstack_building_block_v2.this.status.outputs["bucket_url_path_style"].value_string, "object.storage.eu01.onstackit.cloud") + error_message = "stackit storage-bucket hub building block expected bucket_url_path_style to contain STACKIT domain, got ${meshstack_building_block_v2.this.status.outputs["bucket_url_path_style"].value_string}" + } + + assert { + condition = length(meshstack_building_block_v2.this.status.outputs["s3_access_key"].value_string) > 0 + error_message = "stackit storage-bucket hub building block expected non-empty s3_access_key" + } +} diff --git a/tools/scorecard/scorecard.mjs b/tools/scorecard/scorecard.mjs index f4d221c0..794f215b 100755 --- a/tools/scorecard/scorecard.mjs +++ b/tools/scorecard/scorecard.mjs @@ -494,7 +494,7 @@ const detectors = [ category: "testing", name: "e2e/ contains .tftest.hcl files", emoji: "✅", - fixRef: E2E("e2etests-tftesthcl-conventions"), + fixRef: E2E("e2eteststftesthcl-conventions"), fn: (mod) => { const e2eDir = join(mod.path, "e2e", "tests"); if (!existsSync(e2eDir)) return { pass: false }; @@ -577,6 +577,67 @@ function discoverModules() { return modules.sort((a, b) => a.id.localeCompare(b.id)); } +// ─── fixRef validator ──────────────────────────────────────────────────────── + +function headingToAnchor(heading) { + return heading + .replace(/^#+\s*/, "") + .replace(/`([^`]*)`/g, "$1") + .toLowerCase() + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-"); +} + +function validateFixRefs() { + const filesToParse = [...new Set(detectors.filter((d) => d.fixRef).map((d) => d.fixRef.file))]; + const markerMap = new Map(); // "file#section" → Set + const allMarkerCheckIds = new Set(); + + for (const relFile of filesToParse) { + const filePath = join(ROOT, relFile); + if (!existsSync(filePath)) continue; + + const lines = readFileSync(filePath, "utf-8").split("\n"); + let pendingIds = null; + + for (const line of lines) { + const m = line.match(//); + if (m) { + pendingIds = m[1].split(",").map((s) => s.trim()).filter(Boolean); + continue; + } + if (pendingIds && /^#{1,6}\s/.test(line)) { + const section = headingToAnchor(line); + markerMap.set(`${relFile}#${section}`, new Set(pendingIds)); + for (const id of pendingIds) allMarkerCheckIds.add(id); + pendingIds = null; + } + } + } + + const errors = []; + const detectorIds = new Set(detectors.map((d) => d.id)); + + for (const d of detectors) { + if (!d.fixRef) continue; + const key = `${d.fixRef.file}#${d.fixRef.section}`; + const checksInSection = markerMap.get(key); + if (!checksInSection) { + errors.push(`fixRef "${key}" not found — no marker before that heading`); + } else if (!checksInSection.has(d.id)) { + errors.push(`check "${d.id}" missing from marker at "${key}" (has: ${[...checksInSection].join(", ")})`); + } + } + + for (const id of allMarkerCheckIds) { + if (!detectorIds.has(id)) { + errors.push(`marker references unknown check "${id}" — no detector with that id`); + } + } + + return errors; +} + // ─── Fix prompt generator ──────────────────────────────────────────────────── function generateFixPrompt(mod, failingChecks) { @@ -620,6 +681,13 @@ function main() { const filterModules = args.filter((a) => a.startsWith("--module=")).map((a) => a.split("=")[1]); const fixMode = args.includes("--fix"); + const fixRefErrors = validateFixRefs(); + if (fixRefErrors.length > 0) { + process.stderr.write("❌ fixRef/marker validation failed:\n"); + for (const e of fixRefErrors) process.stderr.write(` • ${e}\n`); + process.exit(1); + } + let modules = discoverModules(); if (filterProvider) { modules = modules.filter((m) => m.provider === filterProvider);