Skip to content
Draft
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
File renamed without changes.
File renamed without changes.
165 changes: 165 additions & 0 deletions .agents/references/stackit-backplane.md
Original file line number Diff line number Diff line change
@@ -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.

<!-- scorecard-checks: stackit_uses_service_account_key -->
## Implementation Pattern

```hcl
# backplane/main.tf — service account + key + role assignments

resource "stackit_service_account" "backplane" {
project_id = var.project_id
name = "mesh-<service-name>"
}

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 = "<required-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 = "<required-role>"
subject = stackit_service_account.backplane.email
}
```

<!-- scorecard-checks: stackit_service_account_key_output -->
## 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."
}
```

<!-- scorecard-checks: stackit_provider_uses_key -->
## 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/<service>/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`
211 changes: 211 additions & 0 deletions .agents/skills/e2e-test/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
---
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,
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/<cloud-provider>/<service-name>/
└── 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/
└── <test-name>.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.<name>.building_block_definition.version_ref` **directly** — do not unwrap it as
`{ uuid = module.<name>.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-<name>-${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 `<cloud>_<service>_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/<name>.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 <sha> 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/<provider>/<service>/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/<provider>/<service>/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
Loading
Loading