Skip to content
Open
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
90 changes: 90 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,96 @@ getting-started steps, and shared responsibility matrix.

---

## End-to-End Testing

Modules that can be smoke-tested against a live meshStack instance should include an `e2e/` directory alongside the module root.

### 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
```

### `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 = { BBEnvironment = ["dev"] }
}
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-<name>-${var.test_context.name_suffix}"
target_ref = {
kind = "meshWorkspace"
identifier = var.test_context.workspace
}
inputs = { ... }
}
}
```

Pass `module.<name>.building_block_definition.version_ref` **directly** — do not unwrap it as `{ uuid = module.<name>.building_block_definition.version_ref.uuid }`.

### `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.

### 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

---

## Checklist for New Modules

- [ ] `backplane/` (optional) and `buildingblock/` with all required files
Expand Down
2 changes: 2 additions & 0 deletions modules/meshstack/noop/buildingblock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ output "some_file_yaml" {
| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.0 |
| <a name="requirement_external"></a> [external](#requirement\_external) | ~> 2.3.0 |

## Modules

Expand All @@ -59,6 +60,7 @@ No modules.
| Name | Type |
|------|------|
| [terraform_data.noop](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource |
| [external_external.aws_version](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source |

## Inputs

Expand Down
12 changes: 11 additions & 1 deletion modules/meshstack/noop/buildingblock/main.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
resource "terraform_data" "noop" {
# This resource does nothing and is always up-to-date.
}
}

data "external" "aws_version" {
# Demonstrate that we can call aws cli installed in the prerun script.
# Validates the installed version is v2 and surfaces it as a Terraform output.
program = ["bash", "-c", <<-EOT
version=$(aws --version 2>&1)
echo "{\"version\": \"$version\"}"
EOT
]
}
2 changes: 1 addition & 1 deletion modules/meshstack/noop/buildingblock/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ output "num" {
}

output "text" {
value = var.text
value = "${var.text} ${data.external.aws_version.result.version}"
}

output "sensitive_text" {
Expand Down
5 changes: 3 additions & 2 deletions modules/meshstack/noop/buildingblock/prerun.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ ls -lah
echo ""

echo "--- Tool Installation ---"
echo "Currently not supported via apk add, but coming soon, see https://feedback.meshcloud.io/feature-requests/p/building-block-should-support-aws-cli-and-other"
# sudo apk add aws-cli
echo "Install additional packages safely via nix"
nix profile add nixpkgs#awscli2
aws --version
echo ""

echo "--- Terraform State Manipulation ---"
Expand Down
9 changes: 7 additions & 2 deletions modules/meshstack/noop/buildingblock/versions.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
terraform {
required_version = ">= 1.0"
# No provider required - this building block manages no cloud resources.
# It only processes and echoes back the inputs provided by meshStack.
required_providers {
# external is used to capture the AWS CLI version at apply time.
external = {
source = "hashicorp/external"
version = "~> 2.3.0"
}
}
}
46 changes: 46 additions & 0 deletions modules/meshstack/noop/e2e/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
variable "test_context" {
type = object({
hub_git_ref = string
workspace = string
project = string
name_suffix = string
})
nullable = false
}

module "noop" {
source = "../"
meshstack = {
owning_workspace_identifier = var.test_context.workspace
tags = {
BBEnvironment = ["dev"]
}
}
hub = {
git_ref = var.test_context.hub_git_ref
bbd_draft = true
}
}

resource "meshstack_building_block_v2" "this" {
wait_for_completion = true
spec = {
building_block_definition_version_ref = module.noop.building_block_definition.version_ref

display_name = "smoke-test-noop-hub-${var.test_context.name_suffix}"
target_ref = {
kind = "meshWorkspace"
identifier = var.test_context.workspace
}

inputs = {
flag = { value_bool = true }
num = { value_int = 1 }
text = { value_string = "Hello, World!" }
sensitive_text = { value_string = "Hidden value" }
single_select = { value_single_select = "single1" }
multi_select = { value_multi_select = ["multi1", "multi2"] }
multi_select_json = { value_multi_select = ["multi2", "multi1"] }
}
}
}
9 changes: 9 additions & 0 deletions modules/meshstack/noop/e2e/terraform.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.0"

required_providers {
meshstack = {
source = "meshcloud/meshstack"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"sensitive-file.yaml":"some: input\nother: value\n",
"some-file.yaml":"some: input\nother: value\n"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"flag": true,
"multi_select": [
"multi1",
"multi2"
],
"multi_select_json": "[\"multi2\",\"multi1\"]",
"num": 1,
"sensitive_text":"Hidden value",
"sensitive_yaml":"some: yaml\nother: value\n",
"single_select":"single1",
"static":"A static value",
"static_code": {
"some":"code"
},
"text":"Hello, World!"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"email": "partner@meshcloud.io",
"euid": "partner@meshcloud.io",
"firstName": "Meshcloud",
"lastName": "Partner",
"meshIdentifier": "bebeb91e-3bf8-11ec-8fcd-0242ac110002",
"roles": ["Workspace Manager"],
"username": "partner@meshcloud.io"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# NoOp Building Block — Deployment Summary

This building block was successfully deployed. It is a **reference implementation**
demonstrating meshStack's complete Terraform interface — it provisions _no real
infrastructure_.

The `SUMMARY` output assignment type allows you to provide a rich markdown summary for application teams.
This summary is rendered like a README for this building block in meshPanel.

## Example: Tables

| Input | Value |
|----------------|------------------------------|
| Text | `Hello` |
| Number | `123` |

## Example: Code blocks

You can use fenced code blocks and `inline code` for formatting.
We support syntax highlighting for common languages.

```yaml
some: input
other: value
```

## Example: Callout blocks

> **Note**: Use quote blocks to create callouts for important information.
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
run "building_block_noop_hub" {
assert {
condition = meshstack_building_block_v2.this.status.status == "SUCCEEDED"
error_message = "noop hub building block expected SUCCEEDED, got ${meshstack_building_block_v2.this.status.status}"
}

assert {
condition = meshstack_building_block_v2.this.status.outputs["num"].value_int == 1
error_message = "noop hub building block expected output num to be 1, got ${meshstack_building_block_v2.this.status.outputs["num"].value_int}"
}

assert {
condition = startswith(meshstack_building_block_v2.this.status.outputs["text"].value_string, "Hello, World! aws-cli/2")
error_message = "noop hub building block expected output text to start with 'Hello, World! aws-cli/2', got ${meshstack_building_block_v2.this.status.outputs["text"].value_string}"
}

assert {
condition = meshstack_building_block_v2.this.status.outputs["flag"].value_bool == true
error_message = "noop hub building block expected output flag to be true, got ${meshstack_building_block_v2.this.status.outputs["flag"].value_bool}"
}

assert {
condition = meshstack_building_block_v2.this.status.outputs["resource_url"].value_string == "https://hub.meshcloud.io/modules/meshstack/noop"
error_message = "noop hub building block expected output resource_url to be 'https://hub.meshcloud.io/modules/meshstack/noop', got ${meshstack_building_block_v2.this.status.outputs["resource_url"].value_string}"
}

assert {
condition = (
meshstack_building_block_v2.this.status.outputs["summary"].value_string
==
file("${path.root}/tests/building_block_noop_hub.summary.expected.md")
)
error_message = "noop hub building block expected output summary to match expected, got ${meshstack_building_block_v2.this.status.outputs["summary"].value_string}"
}

assert {
# we have to exclude the user permissions inputs because several meshis hold role assignments on this workspace
# and permissions may change, so we assert those separately below
condition = (
{
for k, v in jsondecode(meshstack_building_block_v2.this.status.outputs["debug_input_variables_json"].value_code) :
k => v
if k != "user_permissions_json" && k != "user_permissions"
}
==
jsondecode(file("${path.root}/tests/building_block_noop_hub.debug_input_variables_json.expected.json"))
)
error_message = "noop hub building block expected output debug_input_variables_json to match expected (excluding user_permissions_json and user_permissions)"
}

assert {
condition = contains(
jsondecode(meshstack_building_block_v2.this.status.outputs["debug_input_variables_json"].value_code)["user_permissions"],
jsondecode(file("${path.root}/tests/building_block_noop_hub.debug_input_variables_json_binding.expected.json"))
)
error_message = "could not find expected user permission"
}

assert {
condition = contains(
# double decoding is required when user_permissions_json is passed as json
jsondecode(jsondecode(meshstack_building_block_v2.this.status.outputs["debug_input_variables_json"].value_code)["user_permissions_json"]),
jsondecode(file("${path.root}/tests/building_block_noop_hub.debug_input_variables_json_binding.expected.json"))
)
error_message = "could not find expected user permission"
}

assert {
condition = (
jsondecode(meshstack_building_block_v2.this.status.outputs["debug_input_files_json"].value_code)
==
jsondecode(file("${path.root}/tests/building_block_noop_hub.debug_input_files_json.expected.json"))
)
error_message = "noop hub building block expected output debug_input_files_json to match expected, got ${meshstack_building_block_v2.this.status.outputs["debug_input_files_json"].value_code}"
}
}

1 change: 1 addition & 0 deletions modules/meshstack/noop/meshstack_integration.tf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ output "building_block_definition" {
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
git_ref = var.hub.git_ref
}
}

Expand Down
Loading
Loading