diff --git a/modules/meshstack/composition/buildingblock/README.md b/modules/meshstack/composition/buildingblock/README.md new file mode 100644 index 00000000..6c67ff62 --- /dev/null +++ b/modules/meshstack/composition/buildingblock/README.md @@ -0,0 +1,96 @@ +--- +name: Composition Demo +supportedPlatforms: + - meshstack +description: | + Reference building block demonstrating the composition pattern: it uses the run's + ephemeral API key to create a building block definition and a building block from it. +# Creates only meshObjects through the meshStack API, so there is nothing to set up cloud-side. +requiresBackplane: false +--- +# Composition Demo + +This building block is a reference implementation of the **composition** pattern: a building block +that provisions other meshObjects through the meshStack API instead of cloud resources. It creates a +`meshBuildingBlockDefinition` in the consuming workspace and a `meshBuildingBlock` from that +definition. + +The created definition runs the hub's [`link`](../../link) building block, which provisions nothing +but a `terraform_data` and needs neither a cloud provider nor an operator. Reusing it keeps the whole +chain automatic and avoids inventing a throwaway implementation just to have something to create. +Both created meshObjects are named after `link_name` (default `Link`) to keep them distinguishable +from the `Composition Demo` block that created them. + +## How it works + +The building block definition declares `permissions`, so meshStack issues an **ephemeral API key** +scoped to the consuming workspace for the duration of each run and injects it as `MESHSTACK_ENDPOINT` +/ `MESHSTACK_API_TOKEN`. The `meshstack` provider picks those up with no explicit configuration, so +`provider "meshstack" {}` is all this module needs. + +Because both meshObjects are created with that key, meshStack records it as their creation author and +surfaces **"created by building block"** provenance on the definition and the building block, linking +back to the composition that created them. That makes this module a convenient end-to-end check of +that provenance without any cloud platform involved. + +## Permissions + +| Permission | Why | +|---|---| +| `BUILDINGBLOCKDEFINITION_LIST` / `_SAVE` / `_DELETE` | Manage the created building block definition | +| `BUILDINGBLOCK_LIST` / `_SAVE` / `_DELETE` | Manage the created building block | + +The created definition itself declares no permissions, so it stays within meshStack's privilege +escalation guard (a nested definition may only request a subset of its parent's permissions). + +## Notes + +- This module does not wait for the created building block's run. That run needs a terraform runner, + and a stack with a single one cannot start it before this run returns. Provenance is recorded when + the block is created, so nothing here depends on the result. +- For the same reason the created definition uses `deletion_mode = "PURGE"`. `DELETE` would schedule + a deprovisioning run on teardown that cannot start until the composition's own destroy run returns. + Purging leaks nothing, since `link` provisions no infrastructure. +- `hub_git_ref` is wired in as a static input from the composition's own `var.hub.git_ref`, so the + created definition clones the `link` module from the same hub revision. +- The created definition's version stays a **draft**. Releasing needs admin approval, which the run's + ephemeral key — a plain workspace key — cannot obtain, so `draft = false` would leave the version + `DRAFT` regardless, warn on every run, and leave `version_latest_release` null. The created + building block therefore references `version_latest`, which a draft permits because the definition + and the building block's target are the same workspace. + + +## Requirements + +| Name | Version | +|------|---------| +| [meshstack](#requirement\_meshstack) | >= 0.24.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [meshstack_building_block.created](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block) | resource | +| [meshstack_building_block_definition.created](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [hub\_git\_ref](#input\_hub\_git\_ref) | Hub reference the created building block definition clones its implementation from. Wired in as a static input from the composition's own `var.hub.git_ref`, so both definitions stay on the same hub revision. | `string` | n/a | yes | +| [link\_name](#input\_link\_name) | Name given to the building block definition and building block this composition creates. | `string` | `"Link"` | no | +| [link\_url](#input\_link\_url) | Target of the link the created building block publishes. | `string` | n/a | yes | +| [workspace\_identifier](#input\_workspace\_identifier) | Workspace the created building block definition is owned by and the created building block is attached to. Wired in as a WORKSPACE\_IDENTIFIER input, so it is always the consuming workspace — the same one the run's ephemeral API key is scoped to. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [created\_building\_block\_definition\_uuid](#output\_created\_building\_block\_definition\_uuid) | UUID of the building block definition this composition created. | +| [created\_building\_block\_uuid](#output\_created\_building\_block\_uuid) | UUID of the building block this composition created. | +| [summary](#output\_summary) | Markdown summary shown on the building block's detail page. | + diff --git a/modules/meshstack/composition/buildingblock/logo.png b/modules/meshstack/composition/buildingblock/logo.png new file mode 100644 index 00000000..634d5dcb Binary files /dev/null and b/modules/meshstack/composition/buildingblock/logo.png differ diff --git a/modules/meshstack/composition/buildingblock/main.tf b/modules/meshstack/composition/buildingblock/main.tf new file mode 100644 index 00000000..6a4a4da3 --- /dev/null +++ b/modules/meshstack/composition/buildingblock/main.tf @@ -0,0 +1,118 @@ +# Both resources below are created through the meshStack API with the run's ephemeral API key, which +# meshStack records as their creation author. That is what makes them show "created by building +# block" provenance pointing back at the building block this module runs for. + +# Runs the hub's `link` building block rather than an implementation of its own: `link` provisions +# nothing but a terraform_data, needs no cloud provider and no operator, which makes it the cheapest +# real implementation to hand a created definition. See modules/meshstack/link. +resource "meshstack_building_block_definition" "created" { + metadata = { + owned_by_workspace = var.workspace_identifier + } + + spec = { + display_name = var.link_name + description = "Link building block definition created by the Composition Demo building block." + target_type = "WORKSPACE_LEVEL" + run_transparency = true + + readme = chomp(<<-EOT + A link building block definition created by a composition rather than by a platform engineer. + + Ordering it publishes a link in your workspace and provisions no infrastructure. + EOT + ) + } + + version_spec = { + # Stays a draft: releasing a version needs admin approval, which the run's ephemeral key — a + # plain workspace key — cannot obtain. Setting draft = false would leave the version DRAFT + # anyway, warn on every run, and leave version_latest_release null. The building block below can + # still be created from a draft because the definition and the target are the same workspace, + # which satisfies BuildingBlockCreationValidator.requireAccess's `selfOwning` branch. + draft = true + + # PURGE, not DELETE: DELETE would schedule a deprovisioning run for the building block below when + # this composition is torn down, and that run cannot start until the composition's own destroy run + # returns — a deadlock wherever the terraform runner pool has a single worker. Nothing is leaked + # by purging, because the `link` implementation provisions nothing but a terraform_data. + deletion_mode = "PURGE" + + implementation = { + terraform = { + repository_url = "https://github.com/meshcloud/meshstack-hub.git" + repository_path = "modules/meshstack/link/buildingblock" + ref_name = var.hub_git_ref + terraform_version = "1.11.0" + async = false + use_mesh_http_backend_fallback = true + } + } + + # All static, so the created building block below has no inputs left to supply. Mirrors the + # wiring in modules/meshstack/link/meshstack_integration.tf. + inputs = { + url = { + display_name = "URL" + description = "Target of the link." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.link_url) + } + title = { + display_name = "Title" + description = "Human-readable name of the linked resource." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.link_name) + } + # Empty falls back to the summary the link module generates from title and url. + summary = { + display_name = "Summary" + description = "Markdown rendered for the application team after deployment." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode("") + } + } + + outputs = { + url = { + display_name = var.link_name + type = "STRING" + assignment_type = "RESOURCE_URL" + } + summary = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } + } + } +} + +resource "meshstack_building_block" "created" { + spec = { + # version_latest, not version_latest_release: the definition above stays a draft, so + # version_latest_release is null. + building_block_definition_version_ref = { + uuid = meshstack_building_block_definition.created.version_latest.uuid + content_hash = meshstack_building_block_definition.created.version_latest.content_hash + } + + display_name = var.link_name + + target_ref = { + kind = "meshWorkspace" + name = var.workspace_identifier + } + + # Every input of the definition above is static, so there is nothing for this block to set. + inputs = {} + } + + # Deliberately not waiting: this run would be waiting on a run of the same implementation type, + # which a stack with a single terraform runner cannot start until this one returns. Nothing below + # depends on the result either — provenance is recorded when the block is created. + wait_for_completion = false +} diff --git a/modules/meshstack/composition/buildingblock/outputs.tf b/modules/meshstack/composition/buildingblock/outputs.tf new file mode 100644 index 00000000..e35a573b --- /dev/null +++ b/modules/meshstack/composition/buildingblock/outputs.tf @@ -0,0 +1,23 @@ +output "created_building_block_definition_uuid" { + value = meshstack_building_block_definition.created.metadata.uuid + description = "UUID of the building block definition this composition created." +} + +output "created_building_block_uuid" { + value = meshstack_building_block.created.metadata.uuid + description = "UUID of the building block this composition created." +} + +output "summary" { + value = chomp(<<-EOT + Created a Link building block definition and a building block from it. Both show this building + block as their creator. + + | meshObject | Name | UUID | + |---|---|---| + | Building Block Definition | ${var.link_name} | `${meshstack_building_block_definition.created.metadata.uuid}` | + | Building Block | ${var.link_name} | `${meshstack_building_block.created.metadata.uuid}` | + EOT + ) + description = "Markdown summary shown on the building block's detail page." +} diff --git a/modules/meshstack/composition/buildingblock/provider.tf b/modules/meshstack/composition/buildingblock/provider.tf new file mode 100644 index 00000000..e03d77b1 --- /dev/null +++ b/modules/meshstack/composition/buildingblock/provider.tf @@ -0,0 +1,4 @@ +# Authenticates from the MESHSTACK_ENDPOINT / MESHSTACK_API_TOKEN environment variables the runner +# injects for the run's ephemeral API key. +provider "meshstack" { +} diff --git a/modules/meshstack/composition/buildingblock/variables.tf b/modules/meshstack/composition/buildingblock/variables.tf new file mode 100644 index 00000000..885df83e --- /dev/null +++ b/modules/meshstack/composition/buildingblock/variables.tf @@ -0,0 +1,23 @@ +# Defaulted, unlike the other inputs, so that a run can still destroy a building block whose stored +# inputs predate a rename of this one. A destroy only has to delete the created meshObjects, and their +# display names have no bearing on that. +variable "link_name" { + type = string + default = "Link" + description = "Name given to the building block definition and building block this composition creates." +} + +variable "link_url" { + type = string + description = "Target of the link the created building block publishes." +} + +variable "workspace_identifier" { + type = string + description = "Workspace the created building block definition is owned by and the created building block is attached to. Wired in as a WORKSPACE_IDENTIFIER input, so it is always the consuming workspace — the same one the run's ephemeral API key is scoped to." +} + +variable "hub_git_ref" { + type = string + description = "Hub reference the created building block definition clones its implementation from. Wired in as a static input from the composition's own `var.hub.git_ref`, so both definitions stay on the same hub revision." +} diff --git a/modules/meshstack/composition/buildingblock/versions.tf b/modules/meshstack/composition/buildingblock/versions.tf new file mode 100644 index 00000000..da8747cc --- /dev/null +++ b/modules/meshstack/composition/buildingblock/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.24.0" + } + } +} diff --git a/modules/meshstack/composition/meshstack_integration.tf b/modules/meshstack/composition/meshstack_integration.tf new file mode 100644 index 00000000..e14c33f9 --- /dev/null +++ b/modules/meshstack/composition/meshstack_integration.tf @@ -0,0 +1,183 @@ +variable "link_name" { + type = string + default = "Link" + description = "Name given to the building block definition and building block this composition creates. Both are the hub's `link` building block, hence the default." +} + +variable "link_url" { + type = string + default = "https://docs.meshcloud.io" + description = "Target of the link the created building block publishes. The created definition runs the hub's `link` building block, which provisions nothing but this address." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + tags = optional(map(list(string)), {}) + }) + description = "Shared meshStack context. Tags are optional and propagated to building block definition metadata." +} + +variable "hub" { + type = object({ + git_ref = optional(string, "main") + bbd_draft = optional(bool, true) + }) + const = true + default = { + git_ref = "main" + bbd_draft = true + } + description = <<-EOT + `git_ref`: Hub release reference. Set to a tag (e.g. 'v1.2.3') or branch or commit sha of the meshstack-hub repo. + `bbd_draft`: If true, the building block definition version is kept in draft mode. + 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 + git_ref = var.hub.git_ref + } +} + +resource "meshstack_building_block_definition" "this" { + metadata = { + owned_by_workspace = var.meshstack.owning_workspace_identifier + tags = var.meshstack.tags + } + + spec = { + display_name = "Composition Demo" + description = "Reference building block demonstrating the composition pattern: creates a Link building block definition and a building block from it using the run's ephemeral API key." + target_type = "WORKSPACE_LEVEL" + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/modules/meshstack/composition/buildingblock/logo.png" + + # Lets consuming workspace users read the run logs, which is where the interesting part happens. + run_transparency = true + + readme = chomp(<<-EOT + Creates a link building block definition in your workspace and a building block from it. Both + carry a "created by building block" reference back to this building block, so you can follow + where they came from. Nothing is provisioned outside meshStack, and no step needs an operator. + + ## 🎯 When to use it + + Use this building block when you: + - Want to see the composition pattern end to end — a building block provisioning meshObjects + through the meshStack API rather than cloud resources. + - Need a self-service way to hand a workspace its own building block definition without a + platform engineer creating one by hand. + + ## 💡 Usage examples + + **Example 1: Exploring compositions** + A developer adds this building block to a sandbox workspace and inspects the definition and + building block that appear, following the creator link on each back to this building block. + + **Example 2: Offering a workspace its own marketplace link** + A team wants a documentation link published in their own workspace's marketplace and gets both + the definition and a ready-made building block by adding this building block. + + ## 📊 Shared Responsibility + + | Responsibility | Platform Team | Application Team | + |---|:---:|:---:| + | Maintain this composition and its permissions | ✅ | ❌ | + | Operate whatever the published link points at | ❌ | ❌ | + | Choose the name and link target | ❌ | ✅ | + | Decide when to add or remove this building block | ❌ | ✅ | + EOT + ) + } + + version_spec = { + draft = var.hub.bbd_draft + + # DELETE so removing the building block runs `tofu destroy` and cleans up what it created. + deletion_mode = "DELETE" + + implementation = { + terraform = { + repository_url = "https://github.com/meshcloud/meshstack-hub.git" + repository_path = "modules/meshstack/composition/buildingblock" + ref_name = var.hub.git_ref + terraform_version = "1.11.0" + async = false + use_mesh_http_backend_fallback = true + } + } + + inputs = { + link_name = { + assignment_type = "USER_INPUT" + type = "STRING" + display_name = "Link Name" + description = "Name given to the building block definition and building block this composition creates." + default_value = jsonencode(var.link_name) + } + link_url = { + assignment_type = "USER_INPUT" + type = "STRING" + display_name = "Link URL" + description = "Target of the link the created building block publishes." + default_value = jsonencode(var.link_url) + } + workspace_identifier = { + assignment_type = "WORKSPACE_IDENTIFIER" + type = "STRING" + display_name = "Workspace Identifier" + description = "Workspace the created meshObjects belong to. Always the consuming workspace, which is the one the run's ephemeral API key is scoped to." + } + # Pins the created definition's implementation to the same hub revision as this one. + hub_git_ref = { + assignment_type = "STATIC" + type = "STRING" + display_name = "Hub Git Ref" + argument = jsonencode(var.hub.git_ref) + } + } + + outputs = { + created_building_block_definition_uuid = { + assignment_type = "NONE" + type = "STRING" + display_name = "Created Building Block Definition" + } + created_building_block_uuid = { + assignment_type = "NONE" + type = "STRING" + display_name = "Created Building Block" + } + summary = { + assignment_type = "SUMMARY" + type = "STRING" + display_name = "Summary" + } + } + + # Grants each run an ephemeral API key with exactly these workspace permissions. Creating the + # meshObjects with that key is what records this building block as their creator. + permissions = [ + "BUILDINGBLOCKDEFINITION_LIST", + "BUILDINGBLOCKDEFINITION_SAVE", + "BUILDINGBLOCKDEFINITION_DELETE", + "BUILDINGBLOCK_LIST", + "BUILDINGBLOCK_SAVE", + "BUILDINGBLOCK_DELETE", + ] + } +} + +terraform { + required_version = ">= 1.12.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.24.0" + } + } +}