From 0f62258980cefd2471b6f817a6096248d2c8e131 Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Wed, 10 Jun 2026 14:07:52 +0200 Subject: [PATCH 1/2] feat: Added backplane to noop that sets up runner in Google Cloud Run --- modules/meshstack/noop/backplane/README.md | 39 +++- modules/meshstack/noop/backplane/main.tf | 220 ++++++++++++++++++ modules/meshstack/noop/backplane/outputs.tf | 9 + modules/meshstack/noop/backplane/provider.tf | 8 + .../noop/backplane/runner-config.yml | 28 +++ modules/meshstack/noop/backplane/variables.tf | 38 +++ modules/meshstack/noop/backplane/versions.tf | 18 ++ 7 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 modules/meshstack/noop/backplane/main.tf create mode 100644 modules/meshstack/noop/backplane/outputs.tf create mode 100644 modules/meshstack/noop/backplane/provider.tf create mode 100644 modules/meshstack/noop/backplane/runner-config.yml create mode 100644 modules/meshstack/noop/backplane/variables.tf create mode 100644 modules/meshstack/noop/backplane/versions.tf diff --git a/modules/meshstack/noop/backplane/README.md b/modules/meshstack/noop/backplane/README.md index 0c5367d6..cb8d0e92 100644 --- a/modules/meshstack/noop/backplane/README.md +++ b/modules/meshstack/noop/backplane/README.md @@ -1,3 +1,38 @@ -# No-Op Backplane +# NoOp Backplane — Self-Hosted Cloud Run Runner -This building block requires no backplane infrastructure. All inputs are supplied directly to the building block at run time — there is no cloud-side setup for platform teams to deploy. +This backplane provisions a self-hosted meshStack building block runner on Google Cloud Run. + +## What it provisions + +| Resource | Purpose | +|------------------------------------|-----------------------------------------------------------------------------------------------------| +| `tls_private_key` (RSA 4096) | Runner identity key pair — public key registered in meshStack, private key stored in Secret Manager | +| `meshstack_api_key` | meshStack credentials the runner uses to poll and update building block runs | +| `google_secret_manager_secret` × 3 | Stores the RSA private key, runner config YAML, and meshStack client secret | +| `google_cloud_run_v2_service` | Runs the meshStack runner container | +| `meshstack_building_block_runner` | Registers the runner in meshStack with `TERRAFORM` implementation type | + +## Runner container mounts + +The Cloud Run service mounts the following secrets into the container: + +| Mount path | Content | +|------------------------------------|--------------------------------------------------------------------------------------------------------------------| +| `/app/runner-config.yml` | Rendered from `runner-config.yml` with `RUNNER_UUID`, `RUNNER_API_URL`, and `RUNNER_API_KEY_CLIENT_ID` substituted | +| `/app/runner-private.pem` | RSA 4096 private key (PEM) | +| `$MESHSTACK_CLIENT_SECRET` env var | meshStack API client secret | + +Adjust the mount paths in `main.tf` if your runner image expects a different layout. + +## Prerequisites + +- The `cloudrun.googleapis.com` and `secretmanager.googleapis.com` APIs must be enabled in `gcp_project_id`. +- The service account identified by `gcp_cloud_run_service_account_email` must exist before applying. The backplane grants it `roles/secretmanager.secretAccessor` on the created secrets. +- The meshStack provider is configured via the `meshstack_endpoint` variable. Supply admin credentials via `MESHSTACK_CLIENT_ID` and `MESHSTACK_CLIENT_SECRET` environment variables (or `TF_VAR_*` equivalents). + +## Outputs + +| Output | Description | +|-------------------------|-------------------------------------------------------------------------------------------------------| +| `runner_ref` | Wire into `meshstack_building_block_definition.version_spec.runner_ref` in `meshstack_integration.tf` | +| `cloud_run_service_url` | URL of the deployed Cloud Run service | diff --git a/modules/meshstack/noop/backplane/main.tf b/modules/meshstack/noop/backplane/main.tf new file mode 100644 index 00000000..0a756ba8 --- /dev/null +++ b/modules/meshstack/noop/backplane/main.tf @@ -0,0 +1,220 @@ +data "google_project" "this" { + project_id = var.gcp_project_id +} + +locals { + resource_prefix = var.gcp_resource_name_prefix + cloud_run_service_account = "${data.google_project.this.number}-compute@developer.gserviceaccount.com" +} + +resource "tls_private_key" "runner" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "meshstack_api_key" "runner" { + metadata = { + owned_by_workspace = var.meshstack_workspace_identifier + } + spec = { + display_name = var.runner_display_name + permissions = [ + "MANAGED_BUILDINGBLOCKRUNSOURCE_SAVE", + "MANAGED_BUILDINGBLOCKRUN_LIST", + "MANAGED_BUILDINGBLOCKRUN_SAVE" + ] + expires_at = "2026-08-31" # TODO: this should be a variable? generated somehow + } +} + +resource "google_secret_manager_secret" "runner_private_key" { + project = var.gcp_project_id + secret_id = "${local.resource_prefix}-private-key" + + replication { + user_managed { + replicas { + location = var.gcp_region + } + } + } +} + +resource "google_secret_manager_secret_version" "runner_private_key" { + secret = google_secret_manager_secret.runner_private_key.id + secret_data = tls_private_key.runner.private_key_pem +} + +resource "google_secret_manager_secret" "runner_config" { + project = var.gcp_project_id + secret_id = "${local.resource_prefix}-config" + replication { + user_managed { + replicas { + location = var.gcp_region + } + } + } +} + +resource "google_secret_manager_secret_version" "runner_config" { + secret = google_secret_manager_secret.runner_config.id + secret_data = templatefile("${path.module}/runner-config.yml", { + RUNNER_UUID = meshstack_building_block_runner.this.metadata.uuid + RUNNER_API_URL = var.meshstack_endpoint + RUNNER_API_KEY_CLIENT_ID = meshstack_api_key.runner.status.client_id + }) +} + +resource "google_secret_manager_secret" "client_secret" { + project = var.gcp_project_id + secret_id = "${local.resource_prefix}-client-secret" + replication { + user_managed { + replicas { + location = var.gcp_region + } + } + } +} + +resource "google_secret_manager_secret_version" "client_secret" { + secret = google_secret_manager_secret.client_secret.id + secret_data = meshstack_api_key.runner.status.client_secret +} + +resource "google_secret_manager_secret_iam_member" "runner_private_key_accessor" { + project = var.gcp_project_id + secret_id = google_secret_manager_secret.runner_private_key.secret_id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${local.cloud_run_service_account}" +} + +resource "google_secret_manager_secret_iam_member" "runner_config_accessor" { + project = var.gcp_project_id + secret_id = google_secret_manager_secret.runner_config.secret_id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${local.cloud_run_service_account}" +} + +resource "google_secret_manager_secret_iam_member" "client_secret_accessor" { + project = var.gcp_project_id + secret_id = google_secret_manager_secret.client_secret.secret_id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${local.cloud_run_service_account}" +} + +resource "google_cloud_run_v2_service" "runner" { + project = var.gcp_project_id + name = local.resource_prefix + location = var.gcp_region + deletion_protection = false + + template { + service_account = local.cloud_run_service_account + + containers { + image = var.gcp_runner_image + + env { + name = "RUNNER_CONFIG_FILE" + value = "/config/runner-config.yml" + } + + env { + name = "RUNNER_PRIVATE_KEY_FILE" + value = "/keys/runner-private.pem" + } + + env { + name = "RUNNER_API_CLIENT_SECRET" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.client_secret.secret_id + version = "latest" + } + } + } + + volume_mounts { + name = "runner-config" + mount_path = "/config" + } + + volume_mounts { + name = "runner-private-key" + mount_path = "/keys" + } + + startup_probe { + http_get { + path = "/healthz" + port = 8080 + } + initial_delay_seconds = 5 + period_seconds = 5 + failure_threshold = 3 + } + + liveness_probe { + http_get { + path = "/healthz" + port = 8080 + } + period_seconds = 30 + failure_threshold = 3 + } + } + + volumes { + name = "runner-config" + secret { + secret = google_secret_manager_secret.runner_config.secret_id + items { + version = "latest" + path = "runner-config.yml" + } + } + } + + volumes { + name = "runner-private-key" + secret { + secret = google_secret_manager_secret.runner_private_key.secret_id + items { + version = "latest" + path = "runner-private.pem" + } + } + } + + } + + depends_on = [ + google_secret_manager_secret_iam_member.runner_private_key_accessor, + google_secret_manager_secret_iam_member.runner_config_accessor, + google_secret_manager_secret_iam_member.client_secret_accessor, + google_secret_manager_secret_version.runner_private_key, + google_secret_manager_secret_version.runner_config, + google_secret_manager_secret_version.client_secret, + ] +} + +resource "meshstack_building_block_runner" "this" { + metadata = { + owned_by_workspace = var.meshstack_workspace_identifier + } + spec = { + display_name = var.runner_display_name + implementation_type = "TERRAFORM" + public_key = tls_private_key.runner.public_key_pem + restriction = "PRIVATE" + } +} + +/** +provider "meshstack" { + endpoint = "https://federation.dev.meshcloud.io" + apikey = "761ca118-5801-424b-b839-1ea3b8866f57" + apisecret = "nyZG2oduo58aUWzvFtDYksuVGrZV25xK" +**/ diff --git a/modules/meshstack/noop/backplane/outputs.tf b/modules/meshstack/noop/backplane/outputs.tf new file mode 100644 index 00000000..7ed71d83 --- /dev/null +++ b/modules/meshstack/noop/backplane/outputs.tf @@ -0,0 +1,9 @@ +output "runner_ref" { + description = "meshStack building block runner reference. Wire into meshstack_building_block_definition.version_spec.runner_ref." + value = meshstack_building_block_runner.this.ref +} + +output "cloud_run_service_url" { + description = "URL of the deployed Cloud Run runner service." + value = google_cloud_run_v2_service.runner.uri +} diff --git a/modules/meshstack/noop/backplane/provider.tf b/modules/meshstack/noop/backplane/provider.tf new file mode 100644 index 00000000..d3f879b2 --- /dev/null +++ b/modules/meshstack/noop/backplane/provider.tf @@ -0,0 +1,8 @@ +provider "google" { + project = var.gcp_project_id + region = var.gcp_region +} + +provider "meshstack" { + endpoint = var.meshstack_endpoint +} diff --git a/modules/meshstack/noop/backplane/runner-config.yml b/modules/meshstack/noop/backplane/runner-config.yml new file mode 100644 index 00000000..626a1d6a --- /dev/null +++ b/modules/meshstack/noop/backplane/runner-config.yml @@ -0,0 +1,28 @@ +# Unique identifier of this runner, as registered in meshStack. +runnerUuid: ${RUNNER_UUID} + +# Maximum total runtime (in minutes) for a single Building Block run before it is aborted. +timeoutMins: 60 + +# Maximum time (in minutes) to wait for a workspace operation (e.g. checkout, sync) to complete. +wsTimeoutMins: 5 + +# Maximum time (in minutes) allowed for the runner to initialize before a run starts. +initTimeoutMins: 3 + +# Directory inside the container where Terraform working directories are created. +workingDir: /tmp/runner/wd + +# Directory inside the container where Terraform binaries are downloaded and cached. +tfInstallDir: /tmp/runner/tfbin + +# Connection settings for the meshStack API this runner polls for work. +api: + # Base URL of the meshStack federation API. + url: ${RUNNER_API_URL} + # OAuth2 client ID used by the runner to authenticate against meshStack. + clientId: ${RUNNER_API_KEY_CLIENT_ID} + # OAuth2 client secret is injected via the RUNNER_API_CLIENT_SECRET environment variable (see docker run command). + +# When true, SSH host key verification is skipped for Git operations. Keep false in production. +insecureHostKeys: false diff --git a/modules/meshstack/noop/backplane/variables.tf b/modules/meshstack/noop/backplane/variables.tf new file mode 100644 index 00000000..bfd1d6ac --- /dev/null +++ b/modules/meshstack/noop/backplane/variables.tf @@ -0,0 +1,38 @@ +variable "meshstack_workspace_identifier" { + type = string + description = "Identifier of the meshStack workspace that will own the runner and API key." +} + +variable "meshstack_endpoint" { + type = string + description = "Base URL of the meshStack API (e.g. https://federation.example.meshcloud.io). Used by both the Terraform provider and the runner config." +} + +variable "runner_display_name" { + type = string + default = "meshstack-noop-tf-runner" + description = "Display name for the meshStack building block runner and API key." +} + +variable "gcp_project_id" { + type = string + description = "GCP project ID where Cloud Run and Secret Manager resources are deployed." +} + +variable "gcp_region" { + type = string + default = "europe-west1" + description = "GCP region for the Cloud Run service." +} + +variable "gcp_runner_image" { + type = string + description = "Container image URI for the meshStack runner (e.g. ghcr.io/meshcloud/tf-block-runner:latest)." + default = "ghcr.io/meshcloud/tf-block-runner:latest" +} + +variable "gcp_resource_name_prefix" { + type = string + default = "meshstack-runner" + description = "Prefix for GCP resource names (Cloud Run service, Secret Manager secrets). Must be lowercase letters, numbers, and hyphens only." +} diff --git a/modules/meshstack/noop/backplane/versions.tf b/modules/meshstack/noop/backplane/versions.tf new file mode 100644 index 00000000..8d81fcf0 --- /dev/null +++ b/modules/meshstack/noop/backplane/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.12.0" + + required_providers { + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + google = { + source = "hashicorp/google" + version = "~> 7.0" + } + meshstack = { + source = "meshcloud/meshstack" + version = "~> 0.21.0" + } + } +} From 85066053c5aa92cc8da0d595d9d9c7943318edcb Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Wed, 10 Jun 2026 15:01:10 +0200 Subject: [PATCH 2/2] feat: Create e2e test for runner setup --- modules/meshstack/noop/backplane/main.tf | 16 ++++++- modules/meshstack/noop/backplane/variables.tf | 4 +- modules/meshstack/noop/backplane/versions.tf | 4 ++ modules/meshstack/noop/e2e/runner/main.tf | 46 +++++++++++++++++++ modules/meshstack/noop/e2e/runner/provider.tf | 4 ++ .../meshstack/noop/e2e/runner/terraform.tf | 17 +++++++ .../meshstack/noop/e2e/runner/variables.tf | 25 ++++++++++ .../building_block_noop_runner_hub.tftest.hcl | 25 ++++++++++ .../meshstack/noop/meshstack_integration.tf | 10 ++++ 9 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 modules/meshstack/noop/e2e/runner/main.tf create mode 100644 modules/meshstack/noop/e2e/runner/provider.tf create mode 100644 modules/meshstack/noop/e2e/runner/terraform.tf create mode 100644 modules/meshstack/noop/e2e/runner/variables.tf create mode 100644 modules/meshstack/noop/e2e/tests/building_block_noop_runner_hub.tftest.hcl diff --git a/modules/meshstack/noop/backplane/main.tf b/modules/meshstack/noop/backplane/main.tf index 0a756ba8..dd2c93ff 100644 --- a/modules/meshstack/noop/backplane/main.tf +++ b/modules/meshstack/noop/backplane/main.tf @@ -7,6 +7,8 @@ locals { cloud_run_service_account = "${data.google_project.this.number}-compute@developer.gserviceaccount.com" } +resource "time_static" "runner_key_expiry" {} + resource "tls_private_key" "runner" { algorithm = "RSA" rsa_bits = 4096 @@ -23,7 +25,7 @@ resource "meshstack_api_key" "runner" { "MANAGED_BUILDINGBLOCKRUN_LIST", "MANAGED_BUILDINGBLOCKRUN_SAVE" ] - expires_at = "2026-08-31" # TODO: this should be a variable? generated somehow + expires_at = formatdate("YYYY-MM-DD", timeadd(time_static.runner_key_expiry.rfc3339, "168h")) } } @@ -42,7 +44,7 @@ resource "google_secret_manager_secret" "runner_private_key" { resource "google_secret_manager_secret_version" "runner_private_key" { secret = google_secret_manager_secret.runner_private_key.id - secret_data = tls_private_key.runner.private_key_pem + secret_data = tls_private_key.runner.private_key_pem_pkcs8 } resource "google_secret_manager_secret" "runner_config" { @@ -116,6 +118,16 @@ resource "google_cloud_run_v2_service" "runner" { containers { image = var.gcp_runner_image + resources { + limits = { + cpu = "2" + # 512MiB did crash for the noop BB at pre-run script level, so did 1024: + # GCP Error message: Memory limit of 1024 MiB exceeded with 1074 MiB used. Consider increasing the memory limit, + # 2048 seems to work fine so far. + memory = "2048Mi" + } + } + env { name = "RUNNER_CONFIG_FILE" value = "/config/runner-config.yml" diff --git a/modules/meshstack/noop/backplane/variables.tf b/modules/meshstack/noop/backplane/variables.tf index bfd1d6ac..2239a5fe 100644 --- a/modules/meshstack/noop/backplane/variables.tf +++ b/modules/meshstack/noop/backplane/variables.tf @@ -27,8 +27,8 @@ variable "gcp_region" { variable "gcp_runner_image" { type = string - description = "Container image URI for the meshStack runner (e.g. ghcr.io/meshcloud/tf-block-runner:latest)." - default = "ghcr.io/meshcloud/tf-block-runner:latest" + description = "Container image URI for the meshStack runner." + default = "docker.io/meshcloud/tf-block-runner:main" } variable "gcp_resource_name_prefix" { diff --git a/modules/meshstack/noop/backplane/versions.tf b/modules/meshstack/noop/backplane/versions.tf index 8d81fcf0..e1f33515 100644 --- a/modules/meshstack/noop/backplane/versions.tf +++ b/modules/meshstack/noop/backplane/versions.tf @@ -14,5 +14,9 @@ terraform { source = "meshcloud/meshstack" version = "~> 0.21.0" } + time = { + source = "hashicorp/time" + version = "~> 0.12" + } } } diff --git a/modules/meshstack/noop/e2e/runner/main.tf b/modules/meshstack/noop/e2e/runner/main.tf new file mode 100644 index 00000000..b1b6433f --- /dev/null +++ b/modules/meshstack/noop/e2e/runner/main.tf @@ -0,0 +1,46 @@ +module "backplane" { + source = "../../backplane" + + meshstack_workspace_identifier = var.test_context.workspace + meshstack_endpoint = var.meshstack_endpoint + gcp_project_id = var.gcp_project_id + gcp_region = var.gcp_region + gcp_resource_name_prefix = "noop-runner-${var.test_context.name_suffix}" + runner_display_name = "smoke-test-noop-runner-${var.test_context.name_suffix}" +} + +module "noop" { + source = "../../" + meshstack = { + owning_workspace_identifier = var.test_context.workspace + tags = {} + } + hub = { + git_ref = var.test_context.hub_git_ref + bbd_draft = true + } + runner_ref = module.backplane.runner_ref +} + +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-runner-${var.test_context.name_suffix}" + target_ref = { + kind = "meshWorkspace" + name = var.test_context.workspace + } + + inputs = { + flag = { value_bool = true } + num = { value_int = 1 } + text = { value_string = "Hello, World!" } + sensitive_text = { value_string_sensitive = "Hidden value" } + single_select = { value_single_select = "single1" } + multi_select = { value_multi_select = ["multi1", "multi2"] } + multi_select_json = { value_multi_select = ["multi2", "multi1"] } + } + } +} diff --git a/modules/meshstack/noop/e2e/runner/provider.tf b/modules/meshstack/noop/e2e/runner/provider.tf new file mode 100644 index 00000000..7e8eebd1 --- /dev/null +++ b/modules/meshstack/noop/e2e/runner/provider.tf @@ -0,0 +1,4 @@ +provider "google" { + project = var.gcp_project_id + region = var.gcp_region +} diff --git a/modules/meshstack/noop/e2e/runner/terraform.tf b/modules/meshstack/noop/e2e/runner/terraform.tf new file mode 100644 index 00000000..a204b6e2 --- /dev/null +++ b/modules/meshstack/noop/e2e/runner/terraform.tf @@ -0,0 +1,17 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + } + google = { + source = "hashicorp/google" + version = "~> 7.0" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + } +} diff --git a/modules/meshstack/noop/e2e/runner/variables.tf b/modules/meshstack/noop/e2e/runner/variables.tf new file mode 100644 index 00000000..6b9240f5 --- /dev/null +++ b/modules/meshstack/noop/e2e/runner/variables.tf @@ -0,0 +1,25 @@ +variable "test_context" { + type = object({ + hub_git_ref = string + workspace = string + project = string + name_suffix = string + }) + nullable = false +} + +variable "gcp_project_id" { + type = string + description = "GCP project ID for the runner Cloud Run service and Secret Manager secrets." +} + +variable "gcp_region" { + type = string + default = "europe-west1" + description = "GCP region for the Cloud Run service and Secret Manager replicas." +} + +variable "meshstack_endpoint" { + type = string + description = "Base URL of the meshStack API. Written into the runner config for API polling." +} diff --git a/modules/meshstack/noop/e2e/tests/building_block_noop_runner_hub.tftest.hcl b/modules/meshstack/noop/e2e/tests/building_block_noop_runner_hub.tftest.hcl new file mode 100644 index 00000000..844a52fc --- /dev/null +++ b/modules/meshstack/noop/e2e/tests/building_block_noop_runner_hub.tftest.hcl @@ -0,0 +1,25 @@ +run "building_block_noop_runner_hub" { + module { + source = "./runner" + } + + assert { + condition = meshstack_building_block_v2.this.status.status == "SUCCEEDED" + error_message = "noop runner 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 runner 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 runner 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 runner building block expected output flag to be true, got ${meshstack_building_block_v2.this.status.outputs["flag"].value_bool}" + } +} diff --git a/modules/meshstack/noop/meshstack_integration.tf b/modules/meshstack/noop/meshstack_integration.tf index b4849218..642fd5a6 100644 --- a/modules/meshstack/noop/meshstack_integration.tf +++ b/modules/meshstack/noop/meshstack_integration.tf @@ -1,3 +1,12 @@ +variable "runner_ref" { + type = object({ + kind = string + uuid = string + }) + default = null + description = "Optional reference to a meshStack building block runner. When set, building block runs are dispatched to this custom runner. Obtain the value from the backplane module's `runner_ref` output." +} + variable "meshstack" { type = object({ owning_workspace_identifier = string @@ -74,6 +83,7 @@ resource "meshstack_building_block_definition" "this" { version_spec = { draft = var.hub.bbd_draft deletion_mode = "PURGE" + runner_ref = var.runner_ref implementation = { terraform = { ref_name = var.hub.git_ref