Skip to content
Merged
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
21 changes: 16 additions & 5 deletions .agents/references/meshstack-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,28 @@ the definition gains a required input, even one with a default. So a new input i
without that flag, setting it on an existing block fails with 403 "You tried to edit input ...".
A BOOLEAN input cannot be optional, so it always takes the second way.

<!-- scorecard-checks: bbd_structured_inputs, bbd_optional_over_default -->
## Input Types and Defaults
<!-- scorecard-checks: bbd_structured_inputs, bbd_input_order, bbd_input_defaults -->
## Input Types, Order and Defaults

meshStack passes each input to the Terraform variable of the same name.

- An input whose variable is an `object` or a `map` is `type = "JSON"` with a `json_schema`: meshPanel
renders a form from the schema, and meshStack rejects a value the schema does not allow. `CODE`
stays right for free text such as YAML, HCL or a script.
- An input whose default only repeats the variable's `default` is `is_optional = true` with no
`default_value`. meshStack then sends no value and the variable's default applies, so the default
lives in one place. A BOOLEAN input cannot be optional and keeps its `default_value`.
- Declare the inputs by ascending `display_order`, and the `buildingblock/` variables they feed in
the same order, so the code reads like the order form. Inputs meshStack fills in, such as the
workspace identifier, come after those a person sets, and the STATIC `hub` comes last.
- The definition owns every value, so a `buildingblock/` variable fed by a required input has no
`default` and is `nullable = false`. A `const = true` variable keeps a default, because `tofu init`
and `validate` outside a run need it.
- An input with a sane default is required and carries it in `default_value`. A JSON object input
also puts each field's default into its schema as `default`, because the order form shows only
those; derive both from one local.
- `is_optional = true` is for a value that is really absent, with no sane default. The code checks
only for null. Handling `""` as unset hides that contract. The check finds `coalesce(var.x, "")`,
`var.x == ""`, `var.x != ""` and `compact([var.x])`, and misses the same handling through a local.
- meshStack currently sends no value for an input whose value is null, whether it is optional or
hidden by a `condition`. So the variable of an optional or conditional input has `default = null`.

<!-- scorecard-checks: wif_no_replicator -->
## Runner identity
Expand Down
5 changes: 3 additions & 2 deletions modules/ske/ske-starterkit/buildingblock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ No modules.
| <a name="input_building_block_definition_version_refs"></a> [building\_block\_definition\_version\_refs](#input\_building\_block\_definition\_version\_refs) | Building block definition versions this starter kit creates its child building blocks from, keyed by definition name (`git-repository` and `forgejo-connector`). | <pre>map(object({<br/> uuid = string<br/> }))</pre> | n/a | yes |
| <a name="input_creator"></a> [creator](#input\_creator) | Information about the creator of the resources who will be assigned Project Admin role | <pre>object({<br/> type = string<br/> identifier = string<br/> displayName = string<br/> username = optional(string)<br/> email = optional(string)<br/> euid = optional(string)<br/> })</pre> | n/a | yes |
| <a name="input_dns_zone_name"></a> [dns\_zone\_name](#input\_dns\_zone\_name) | DNS zone name used for application ingress hostnames. | `string` | n/a | yes |
| <a name="input_landing_zone_refs"></a> [landing\_zone\_refs](#input\_landing\_zone\_refs) | Landing zone references keyed by stage. The keys decide which stages this starter kit creates. Wired in as a static building block input from the platform/backplane that owns the meshLandingZones (their `.ref` outputs). | `map(object({ name = string, kind = optional(string, "meshLandingZone") }))` | n/a | yes |
| <a name="input_landing_zone_refs"></a> [landing\_zone\_refs](#input\_landing\_zone\_refs) | Landing zone references keyed by stage. The keys decide which stages this starter kit creates. Wired in as a static building block input from the platform/backplane that owns the meshLandingZones (their `.ref` outputs). | `map(object({ name = string }))` | n/a | yes |
| <a name="input_name"></a> [name](#input\_name) | This name will be used for the created projects. | `string` | n/a | yes |
| <a name="input_platform_ref"></a> [platform\_ref](#input\_platform\_ref) | Reference (by uuid) to the meshPlatform the tenants are created on. Wired in as a static building block input from the platform/backplane that owns the meshPlatform (its `.ref` output). Required because the meshTenant v4 API references platforms by ref. | <pre>object({<br/> uuid = string<br/> kind = optional(string, "meshPlatform")<br/> })</pre> | n/a | yes |
| <a name="input_payment_method"></a> [payment\_method](#input\_payment\_method) | Payment method of the created meshProjects. | <pre>object({<br/> kind = string<br/> name = string<br/> })</pre> | n/a | yes |
| <a name="input_platform_ref"></a> [platform\_ref](#input\_platform\_ref) | Reference (by uuid) to the meshPlatform the tenants are created on. Wired in as a static building block input from the platform/backplane that owns the meshPlatform (its `.ref` output). Required because the meshTenant v4 API references platforms by ref. | <pre>object({<br/> uuid = string<br/> })</pre> | n/a | yes |
| <a name="input_project_tags"></a> [project\_tags](#input\_project\_tags) | Tags for the created meshProjects. `stages` is keyed as `landing_zone_refs`; `owner_tag_key` names the tag that receives the creator's display name. | <pre>object({<br/> stages = map(map(list(string)))<br/> owner_tag_key = optional(string, null)<br/> })</pre> | n/a | yes |
| <a name="input_repo_clone_addr"></a> [repo\_clone\_addr](#input\_repo\_clone\_addr) | URL to clone into the starterkit git repository. | `string` | n/a | yes |
| <a name="input_workspace_identifier"></a> [workspace\_identifier](#input\_workspace\_identifier) | n/a | `string` | n/a | yes |
Expand Down
3 changes: 2 additions & 1 deletion modules/ske/ske-starterkit/buildingblock/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ resource "meshstack_project" "this" {
owned_by_workspace = var.workspace_identifier
}
spec = {
display_name = "${var.name} ${title(each.key)}"
display_name = "${var.name} ${title(each.key)}"
payment_method_identifier = var.payment_method.name
tags = merge(
lookup(var.project_tags.stages, each.key, {}),
var.project_tags.owner_tag_key == null ? {} : {
Expand Down
12 changes: 10 additions & 2 deletions modules/ske/ske-starterkit/buildingblock/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,28 @@ variable "name" {
description = "This name will be used for the created projects."
}

variable "payment_method" {
type = object({
kind = string
name = string
})
nullable = false
description = "Payment method of the created meshProjects."
}

variable "workspace_identifier" {
type = string
}

variable "platform_ref" {
type = object({
uuid = string
kind = optional(string, "meshPlatform")
})
description = "Reference (by uuid) to the meshPlatform the tenants are created on. Wired in as a static building block input from the platform/backplane that owns the meshPlatform (its `.ref` output). Required because the meshTenant v4 API references platforms by ref."
}

variable "landing_zone_refs" {
type = map(object({ name = string, kind = optional(string, "meshLandingZone") }))
type = map(object({ name = string }))
description = "Landing zone references keyed by stage. The keys decide which stages this starter kit creates. Wired in as a static building block input from the platform/backplane that owns the meshLandingZones (their `.ref` outputs)."
}

Expand Down
18 changes: 15 additions & 3 deletions modules/ske/ske-starterkit/e2e/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ variable "test_context" {
nullable = false

validation {
condition = can(var.test_context.workspace) && can(var.test_context.run_id)
error_message = "test_context must provide workspace and run_id."
condition = can(var.test_context.workspace) && can(var.test_context.run_id) && can(var.test_context.meshstack.tag_schema.mandatory.payment_method)
error_message = "test_context must provide workspace, run_id and meshstack.tag_schema.mandatory.payment_method."
}

validation {
Expand All @@ -28,6 +28,17 @@ module "definition" {
test_context = var.test_context
}

resource "meshstack_payment_method" "this" {
metadata = {
name = "${var.test_context.run_id}-ske-starterkit-pm"
owned_by_workspace = var.test_context.workspace
}
spec = {
display_name = "${var.test_context.run_id} SKE Starterkit"
tags = var.test_context.meshstack.tag_schema.mandatory.payment_method
}
}

resource "meshstack_building_block" "this" {
depends_on = [module.definition]

Expand All @@ -42,7 +53,8 @@ resource "meshstack_building_block" "this" {
}

inputs = {
name = { value = jsonencode(var.test_context.run_id) }
name = { value = jsonencode(var.test_context.run_id) }
payment_method = { value = jsonencode(meshstack_payment_method.this.ref) }
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions modules/ske/ske-starterkit/meshstack_integration.tf
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
variable "platform_ref" {
type = object({
uuid = string
kind = string
})
description = "The `.ref` of the meshPlatform the tenants are created on."
}

variable "landing_zone_refs" {
type = map(object({ name = string, kind = string }))
type = map(object({ name = string }))
description = "Landing zone references keyed by stage. The keys decide which stages the starter kit creates, and the definition declares one app link output per key."
}

Expand Down Expand Up @@ -215,6 +214,13 @@ resource "meshstack_building_block_definition" "this" {
value_validation_regex = local.name_regex
validation_regex_error_message = "Does not match ${local.name_regex} (no underscore/dots allowed). A maximum length of 25 characters is allowed."
}
"payment_method" = {
assignment_type = "PAYMENT_METHOD"
type = "CODE"
display_name = "Payment Method"
description = "Payment method of the created meshProjects, one of your workspace's payment methods."
updateable_by_consumer = true
}
"workspace_identifier" = {
assignment_type = "WORKSPACE_IDENTIFIER"
type = "STRING"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,14 @@ mandatory block's output. By the time the network block is ordered, the project
| <a name="input_add_random_name_suffix"></a> [add\_random\_name\_suffix](#input\_add\_random\_name\_suffix) | n/a | `bool` | n/a | yes |
| <a name="input_creator"></a> [creator](#input\_creator) | n/a | <pre>object({<br/> type = string<br/> identifier = string<br/> displayName = string<br/> username = optional(string)<br/> email = optional(string)<br/> euid = optional(string)<br/> })</pre> | n/a | yes |
| <a name="input_landing_zone"></a> [landing\_zone](#input\_landing\_zone) | n/a | `string` | n/a | yes |
| <a name="input_landing_zone_refs"></a> [landing\_zone\_refs](#input\_landing\_zone\_refs) | n/a | <pre>map(object({<br/> name = string<br/> kind = string<br/> }))</pre> | n/a | yes |
| <a name="input_landing_zone_refs"></a> [landing\_zone\_refs](#input\_landing\_zone\_refs) | n/a | <pre>map(object({<br/> name = string<br/> }))</pre> | n/a | yes |
| <a name="input_meshstack_building_block_id"></a> [meshstack\_building\_block\_id](#input\_meshstack\_building\_block\_id) | Written by the runner, not by the definition's inputs: it is the uuid of the building block this run belongs to. `terraform_data.self_purge` uses it to delete that block once the starterkit is done. | `string` | n/a | yes |
| <a name="input_name"></a> [name](#input\_name) | n/a | `string` | n/a | yes |
| <a name="input_network"></a> [network](#input\_network) | The only variable here that keeps a default. The definition hides this input unless the selected landing zone gets a spoke network, and a hidden input is not sent. | <pre>object({<br/> prefix_length = number<br/> ipv4_nameservers = list(string)<br/> })</pre> | `null` | no |
| <a name="input_network_static"></a> [network\_static](#input\_network\_static) | n/a | <pre>object({<br/> matching_landing_zones = set(string)<br/> bbd_version_ref = object({ uuid = string })<br/> })</pre> | n/a | yes |
| <a name="input_platform_ref"></a> [platform\_ref](#input\_platform\_ref) | n/a | <pre>object({<br/> uuid = string<br/> kind = string<br/> })</pre> | n/a | yes |
| <a name="input_tags"></a> [tags](#input\_tags) | n/a | <pre>object({<br/> project = map(list(string))<br/> project_owner_tag_key = string<br/> })</pre> | n/a | yes |
| <a name="input_payment_method"></a> [payment\_method](#input\_payment\_method) | n/a | <pre>object({<br/> kind = string<br/> name = string<br/> })</pre> | n/a | yes |
| <a name="input_platform_ref"></a> [platform\_ref](#input\_platform\_ref) | n/a | <pre>object({<br/> uuid = string<br/> })</pre> | n/a | yes |
| <a name="input_tags"></a> [tags](#input\_tags) | n/a | <pre>object({<br/> project = map(list(string))<br/> project_owner_tag_key = optional(string)<br/> })</pre> | n/a | yes |
| <a name="input_workspace_identifier"></a> [workspace\_identifier](#input\_workspace\_identifier) | n/a | `string` | n/a | yes |

## Outputs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module "this" {

creator = var.creator
workspace_identifier = var.workspace_identifier
payment_method = var.payment_method
tags = var.tags

name = var.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ locals {
# A service account or an API key has no username to bind a role to.
creator_is_user = var.creator.type == "User" && var.creator.username != null

owner_tags = var.tags.project_owner_tag_key == "" ? {} : { (var.tags.project_owner_tag_key) = [var.creator.displayName] }
owner_tags = { for key in compact([var.tags.project_owner_tag_key]) : key => [var.creator.displayName] }

# The landing zone decides whether an order gets a network at all; `network` only shapes it, and is
# null wherever the definition did not offer the input.
Expand All @@ -32,8 +32,9 @@ resource "meshstack_project" "this" {
}

spec = {
display_name = var.name
tags = merge(var.tags.project, local.owner_tags)
display_name = var.name
payment_method_identifier = var.payment_method.name
tags = merge(var.tags.project, local.owner_tags)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,24 @@ variable "landing_zone" {
}
}

variable "payment_method" {
type = object({
kind = string
name = string
})
nullable = false
description = "Payment method of the created meshProject."
}

variable "workspace_identifier" {
type = string
nullable = false
description = "Identifier of the meshStack workspace the created project belongs to."
}

# `kind` is required rather than optional on this variable and on `landing_zone_refs` so that neither
# type contains `optional()`. Both are genuinely required inputs, and giving them a default purely to
# satisfy a convention would hide a misconfiguration behind a value nobody chose.
variable "platform_ref" {
type = object({
uuid = string
kind = string
})
nullable = false
description = "Reference to the meshPlatform the tenant is created on. Required because the meshTenant v4 API references platforms by ref."
Expand All @@ -52,7 +57,6 @@ variable "platform_ref" {
variable "landing_zone_refs" {
type = map(object({
name = string
kind = string
}))
nullable = false
description = "Landing zones the application team can choose from, keyed by the label shown in the select."
Expand Down Expand Up @@ -101,10 +105,10 @@ variable "network_static" {
variable "tags" {
type = object({
project = map(list(string))
project_owner_tag_key = string
project_owner_tag_key = optional(string)
})
nullable = false
description = "`project` tags are applied to the created meshProject; `project_owner_tag_key` names the tag that receives the creator's display name, empty to set none."
description = "`project` tags are applied to the created meshProject; `project_owner_tag_key` names the tag that receives the creator's display name, null or empty to set none."
}

variable "add_random_name_suffix" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ variable "landing_zone" {
nullable = false
}

variable "payment_method" {
type = object({
kind = string
name = string
})
nullable = false
}

variable "workspace_identifier" {
type = string
nullable = false
Expand All @@ -34,15 +42,13 @@ variable "workspace_identifier" {
variable "platform_ref" {
type = object({
uuid = string
kind = string
})
nullable = false
}

variable "landing_zone_refs" {
type = map(object({
name = string
kind = string
}))
nullable = false
}
Expand All @@ -68,7 +74,7 @@ variable "network_static" {
variable "tags" {
type = object({
project = map(list(string))
project_owner_tag_key = string
project_owner_tag_key = optional(string)
})
nullable = false
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
variable "platform_ref" {
type = object({
uuid = string
kind = string
})
nullable = false
description = "Reference (by uuid) to the meshPlatform tenants are created on — e.g. the `platform_ref` output of `modules/stackit`. Required since the meshTenant v4 API references platforms by ref."
Expand All @@ -10,7 +9,6 @@ variable "platform_ref" {
variable "landing_zone_refs" {
type = map(object({
name = string
kind = string
}))
nullable = false
description = "Landing zones the application team can choose from, keyed by the label shown in the select — e.g. the `landingzone_refs` output of `modules/stackit`, relabelled. The definition builds its select from these keys and passes the whole map to the building block, which resolves the selected label against it."
Expand Down Expand Up @@ -110,7 +108,7 @@ variable "meshstack" {
tags = object({
building_block = map(list(string))
project = map(list(string))
project_owner_tag_key = string
project_owner_tag_key = optional(string)
})
runner_ref = optional(object({
kind = string
Expand Down Expand Up @@ -288,6 +286,14 @@ resource "meshstack_building_block_definition" "this" {
default_value = jsonencode(var.default_landing_zone)
}

payment_method = {
assignment_type = "PAYMENT_METHOD"
type = "CODE"
display_name = "Payment Method"
description = "Payment method of the created meshProject, one of your workspace's payment methods."
updateable_by_consumer = true
}

# ── Set by the platform team ──

platform_ref = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,6 @@ resource "meshstack_building_block_definition" "this" {
display_order = 75
}

# Keep this description under roughly 200 characters. meshStack answers a longer one with
# `500 InternalError` on the version update, not a 400.
tags = {
display_name = "Tags"
description = "Tags forwarded to the nested integrations and the management meshProject. Build them in the form: add {key, values} entries, plus the project owner tag key."
Expand Down
Loading
Loading