diff --git a/.agents/references/meshstack-integration.md b/.agents/references/meshstack-integration.md
index 5d9a9154..dcedf8a3 100644
--- a/.agents/references/meshstack-integration.md
+++ b/.agents/references/meshstack-integration.md
@@ -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.
-
-## Input Types and 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`.
## Runner identity
diff --git a/modules/ske/ske-starterkit/buildingblock/README.md b/modules/ske/ske-starterkit/buildingblock/README.md
index 9542d8c6..2c643b3d 100644
--- a/modules/ske/ske-starterkit/buildingblock/README.md
+++ b/modules/ske/ske-starterkit/buildingblock/README.md
@@ -44,9 +44,10 @@ No modules.
| [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`). |
map(object({
uuid = string
})) | n/a | yes |
| [creator](#input\_creator) | Information about the creator of the resources who will be assigned Project Admin role | object({
type = string
identifier = string
displayName = string
username = optional(string)
email = optional(string)
euid = optional(string)
}) | n/a | yes |
| [dns\_zone\_name](#input\_dns\_zone\_name) | DNS zone name used for application ingress hostnames. | `string` | n/a | yes |
-| [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 |
+| [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 |
| [name](#input\_name) | This name will be used for the created projects. | `string` | n/a | yes |
-| [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. | object({
uuid = string
kind = optional(string, "meshPlatform")
}) | n/a | yes |
+| [payment\_method](#input\_payment\_method) | Payment method of the created meshProjects. | object({
kind = string
name = string
}) | n/a | yes |
+| [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. | object({
uuid = string
}) | n/a | yes |
| [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. | object({
stages = map(map(list(string)))
owner_tag_key = optional(string, null)
}) | n/a | yes |
| [repo\_clone\_addr](#input\_repo\_clone\_addr) | URL to clone into the starterkit git repository. | `string` | n/a | yes |
| [workspace\_identifier](#input\_workspace\_identifier) | n/a | `string` | n/a | yes |
diff --git a/modules/ske/ske-starterkit/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf
index e8b64e6c..584d8a9c 100644
--- a/modules/ske/ske-starterkit/buildingblock/main.tf
+++ b/modules/ske/ske-starterkit/buildingblock/main.tf
@@ -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 ? {} : {
diff --git a/modules/ske/ske-starterkit/buildingblock/variables.tf b/modules/ske/ske-starterkit/buildingblock/variables.tf
index ac253ba9..f92f3685 100644
--- a/modules/ske/ske-starterkit/buildingblock/variables.tf
+++ b/modules/ske/ske-starterkit/buildingblock/variables.tf
@@ -15,6 +15,15 @@ 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
}
@@ -22,13 +31,12 @@ variable "workspace_identifier" {
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)."
}
diff --git a/modules/ske/ske-starterkit/e2e/main.tf b/modules/ske/ske-starterkit/e2e/main.tf
index d9c620df..e70e8c3e 100644
--- a/modules/ske/ske-starterkit/e2e/main.tf
+++ b/modules/ske/ske-starterkit/e2e/main.tf
@@ -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 {
@@ -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]
@@ -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) }
}
}
}
diff --git a/modules/ske/ske-starterkit/meshstack_integration.tf b/modules/ske/ske-starterkit/meshstack_integration.tf
index 8d72be54..66926786 100644
--- a/modules/ske/ske-starterkit/meshstack_integration.tf
+++ b/modules/ske/ske-starterkit/meshstack_integration.tf
@@ -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."
}
@@ -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"
diff --git a/modules/stackit/stackit-project-starterkit/buildingblock/README.md b/modules/stackit/stackit-project-starterkit/buildingblock/README.md
index 4d345250..c515b741 100644
--- a/modules/stackit/stackit-project-starterkit/buildingblock/README.md
+++ b/modules/stackit/stackit-project-starterkit/buildingblock/README.md
@@ -102,13 +102,14 @@ mandatory block's output. By the time the network block is ordered, the project
| [add\_random\_name\_suffix](#input\_add\_random\_name\_suffix) | n/a | `bool` | n/a | yes |
| [creator](#input\_creator) | n/a | object({
type = string
identifier = string
displayName = string
username = optional(string)
email = optional(string)
euid = optional(string)
}) | n/a | yes |
| [landing\_zone](#input\_landing\_zone) | n/a | `string` | n/a | yes |
-| [landing\_zone\_refs](#input\_landing\_zone\_refs) | n/a | map(object({
name = string
kind = string
})) | n/a | yes |
+| [landing\_zone\_refs](#input\_landing\_zone\_refs) | n/a | map(object({
name = string
})) | n/a | yes |
| [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 |
| [name](#input\_name) | n/a | `string` | n/a | yes |
| [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. | object({
prefix_length = number
ipv4_nameservers = list(string)
}) | `null` | no |
| [network\_static](#input\_network\_static) | n/a | object({
matching_landing_zones = set(string)
bbd_version_ref = object({ uuid = string })
}) | n/a | yes |
-| [platform\_ref](#input\_platform\_ref) | n/a | object({
uuid = string
kind = string
}) | n/a | yes |
-| [tags](#input\_tags) | n/a | object({
project = map(list(string))
project_owner_tag_key = string
}) | n/a | yes |
+| [payment\_method](#input\_payment\_method) | n/a | object({
kind = string
name = string
}) | n/a | yes |
+| [platform\_ref](#input\_platform\_ref) | n/a | object({
uuid = string
}) | n/a | yes |
+| [tags](#input\_tags) | n/a | object({
project = map(list(string))
project_owner_tag_key = optional(string)
}) | n/a | yes |
| [workspace\_identifier](#input\_workspace\_identifier) | n/a | `string` | n/a | yes |
## Outputs
diff --git a/modules/stackit/stackit-project-starterkit/buildingblock/main.tf b/modules/stackit/stackit-project-starterkit/buildingblock/main.tf
index 2dad9064..24fa07f3 100644
--- a/modules/stackit/stackit-project-starterkit/buildingblock/main.tf
+++ b/modules/stackit/stackit-project-starterkit/buildingblock/main.tf
@@ -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
diff --git a/modules/stackit/stackit-project-starterkit/buildingblock/this/main.tf b/modules/stackit/stackit-project-starterkit/buildingblock/this/main.tf
index a3141618..cbb17da0 100644
--- a/modules/stackit/stackit-project-starterkit/buildingblock/this/main.tf
+++ b/modules/stackit/stackit-project-starterkit/buildingblock/this/main.tf
@@ -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.
@@ -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)
}
}
diff --git a/modules/stackit/stackit-project-starterkit/buildingblock/this/variables.tf b/modules/stackit/stackit-project-starterkit/buildingblock/this/variables.tf
index 1f26970f..c7b79bf4 100644
--- a/modules/stackit/stackit-project-starterkit/buildingblock/this/variables.tf
+++ b/modules/stackit/stackit-project-starterkit/buildingblock/this/variables.tf
@@ -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."
@@ -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."
@@ -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" {
diff --git a/modules/stackit/stackit-project-starterkit/buildingblock/variables.tf b/modules/stackit/stackit-project-starterkit/buildingblock/variables.tf
index 81c50b86..c0756e3b 100644
--- a/modules/stackit/stackit-project-starterkit/buildingblock/variables.tf
+++ b/modules/stackit/stackit-project-starterkit/buildingblock/variables.tf
@@ -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
@@ -34,7 +42,6 @@ variable "workspace_identifier" {
variable "platform_ref" {
type = object({
uuid = string
- kind = string
})
nullable = false
}
@@ -42,7 +49,6 @@ variable "platform_ref" {
variable "landing_zone_refs" {
type = map(object({
name = string
- kind = string
}))
nullable = false
}
@@ -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
}
diff --git a/modules/stackit/stackit-project-starterkit/meshstack_integration.tf b/modules/stackit/stackit-project-starterkit/meshstack_integration.tf
index 95d50964..a9151740 100644
--- a/modules/stackit/stackit-project-starterkit/meshstack_integration.tf
+++ b/modules/stackit/stackit-project-starterkit/meshstack_integration.tf
@@ -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."
@@ -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."
@@ -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
@@ -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 = {
diff --git a/reference-architectures/otc-landingzone/meshstack_integration.tf b/reference-architectures/otc-landingzone/meshstack_integration.tf
index 028201b6..e8e704a8 100644
--- a/reference-architectures/otc-landingzone/meshstack_integration.tf
+++ b/reference-architectures/otc-landingzone/meshstack_integration.tf
@@ -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."
diff --git a/reference-architectures/stackit-kubernetes/IMPLEMENTATION_DETAILS.md b/reference-architectures/stackit-kubernetes/IMPLEMENTATION_DETAILS.md
index 8573fcd8..9c1ea2ee 100644
--- a/reference-architectures/stackit-kubernetes/IMPLEMENTATION_DETAILS.md
+++ b/reference-architectures/stackit-kubernetes/IMPLEMENTATION_DETAILS.md
@@ -21,7 +21,8 @@ input, so updating the outer building block is the only way to change the platfo
- **Automation Identity Federation**, a child of the Automation Identity, trusts the six
definitions whose runs act as the account: Secrets Manager, SKE cluster, Git, container
- registry, AI Model Serving and DNS. Those six building blocks are its children.
+ registry, AI Model Serving and DNS. Those six building blocks are its children. Without an AI
+ model there is no AI Model Serving block, and the federation trusts the other five.
- **Starter Kit Identity Federation** (phase 2) trusts the Git repository definition, whose runs
list the Git instance's users through the STACKIT Git API to match workspace members by email.
The Forgejo token's technical user is restricted and cannot see them.
@@ -62,7 +63,7 @@ An ephemeral value has no version meshStack could compare, so the nested run pas
`secret_hash` on as the version of the write-only value it fills: the `kubeconfig.yaml` input of
the `kubernetes` and ingress blocks, and the platform's tokens. A rotated secret reaches them in
the same nested run. The Forgejo connector gets one hash of the pull robot and the
-AI secret as `secrets_revision`, so its runs rewrite the Kubernetes secrets when either changes.
+AI secret, if any, as `secrets_revision`, so its runs rewrite the Kubernetes secrets when either changes.
The Forgejo token has no human owner: the Git block creates a technical user through the STACKIT
Git API and exchanges its password for a personal access token.
diff --git a/reference-architectures/stackit-kubernetes/README.md b/reference-architectures/stackit-kubernetes/README.md
index 9d80d785..8f65ad34 100644
--- a/reference-architectures/stackit-kubernetes/README.md
+++ b/reference-architectures/stackit-kubernetes/README.md
@@ -91,6 +91,7 @@ Per stage:
- a Kubernetes service account with RBAC on the namespace and read access to ClusterIssuers,
- a Harbor image pull secret on the namespace's `default` service account,
- a `stackit-ai` secret with `STACKIT_AI_BASE_URL`, `STACKIT_AI_API_KEY` and `STACKIT_AI_MODEL`,
+ unless **Starter Kit Demo** names no `ai_model`,
- the Actions secret `KUBECONFIG_` and the Actions variables `K8S_NAMESPACE_` and
`APP_HOSTNAME_`.
@@ -108,11 +109,11 @@ Once set up, it triggers the repository's workflow and waits for it to finish.
### Install
-1. Register the definition from `meshstack_integration.tf` in your foundation repository. Set
- `playground_mode = false` for a platform that is used: in playground mode the platform
- identifier gets a random suffix and the STACKIT project is not protected against deletion.
+1. Register the definition from `meshstack_integration.tf` in your foundation repository.
2. Order **STACKIT Kubernetes Platform** in the workspace that will own the platform, and paste
- the landing zone's code block into **STACKIT Landing Zone**.
+ the landing zone's code block into **STACKIT Landing Zone**. Turn **Playground Mode** off for a
+ platform that is used: in playground mode the platform identifier gets a random suffix and the
+ STACKIT project is not protected against deletion. It cannot be changed after the order.
3. Finish phase 2.
The main inputs:
@@ -122,11 +123,15 @@ The main inputs:
| STACKIT Landing Zone | Platform, landing zones and the service account and federation definitions of the landing zone |
| Landing Zone Variant | `default` or `networked` (needs hub-and-spoke networking on the landing zone) |
| Platform Identifier | Name of the meshStack platform; the project, cluster, Git instance, registry and DNS zone are named after it |
-| Stages | One landing zone per key, with stage-specific tags and optional quotas; `dev` and `prod` by default |
-| Tags | Tags for landing zones, the definitions platform engineers order (`building_block`) and those application teams order (`starterkit_building_block`), the platform's project and the starter kit's projects |
-| DNS Parent Domain | Domain the DNS zone is created under, `stackit.run` by default |
-| AI Model | Model applications default to, `openai/gpt-oss-120b` by default |
-| Starter Kit Image Name, Template Repository | What the starter kit builds, `ai-summarizer` from the likvid-bank template by default |
+| Use Global Location | Put the platform into the global meshStack location, on by default; off creates a location for it |
+| Playground Mode | Throwaway deployment, on by default; turn it off for a platform that is used; fixed at the order. It hides Starterkit Random Name Suffix (always on), DNS Domain (always `.stackit.run`) and both approval policies (no gates) |
+| Payment Method | One of the workspace's payment methods, for the platform's own project |
+| Starter Kit Demo | What the starter kit builds: image `name`, `template_repo` and `ai_model`, `ai-summarizer` from the likvid-bank template on `openai/gpt-oss-120b` by default. Without `ai_model` the platform sets up no Model Serving token and no `stackit-ai` secret |
+| DNS Domain | The platform's DNS zone under a free STACKIT domain (`stackit.run`, `stackit.zone`, `stackit.rocks`, `stackit.gg`, `runs.onstackit.cloud`): a bare one gives `.stackit.run`, one label more such as `likvid.stackit.run` is the zone itself; `stackit.run` by default |
+| Stages | One landing zone per named stage, with stage-specific tags and optional quotas; `dev` and `prod` by default |
+| Quota Definitions | The quotas the meshPlatform offers; a stage's `quotas` entry names one by its `quota_key` |
+| Tags | Tags as `{key, values}` entries for landing zones, the definitions platform engineers order (`building_block`) and those application teams order (`starterkit_building_block`), the platform's project and the starter kit's projects |
+| Approval Policies, Starterkit Approval Policies | See Approval gates |
| Phase 2 Completed | See below |
| Imports, Import Secrets | See Adopting Existing Resources |
@@ -134,8 +139,9 @@ A tag that a tag policy pairs between a landing zone and a project belongs on bo
`tags.landingzone` and `tags.starterkit_project` or in a stage's `landingzone` and `project`;
otherwise no tenant can be created on the landing zone.
-Optional overrides: Cluster Name, ClusterIssuer Email, DNS Subdomain, Project Identifier, Payment
-Method Identifier, Use Global Location, Quota Definitions (the quotas the meshPlatform offers). The building block outputs a link to the STACKIT project
+**Overrides** replaces generated values, each optional: `cluster_name`, `cluster_issuer_email` (the
+platform's service account by default) and `project_identifier` (`-ske` by
+default). The building block outputs a link to the STACKIT project
and the summary of Platform Services.
### Phase 2: link the Harbor robot
@@ -149,18 +155,18 @@ robot is linked.
### Approval gates
-`approval_policies` sets which run triggers need an operator's approval, for this definition and
-the platform definitions it registers. `starterkit_approval_policies` does the same for the Git
-repository, Forgejo connector and starter kit definitions. Both default to no gate. A gate switched
-on in meshPanel is reset on the next apply unless it is set here.
+The **Approval Policies** input sets which run triggers need an operator's approval on the
+platform definitions this architecture registers, **Starterkit Approval Policies** the same for the
+Git repository, Forgejo connector and starter kit definitions. The `approval_policies` variable of
+`meshstack_integration.tf` sets them for this definition. All default to no gate. A gate switched on
+in meshPanel is reset on the next run unless it is set in the input or variable.
### Platform engineer access
The owners and managers of the ordering workspace become Project Admins of the platform's project.
-`platform_engineer_role_mapping` maps the project's meshStack roles, by service, to what members
-get: by default Project Admins join the Forgejo `Owners` team (once they signed in to the Git
-instance), become registry admins and get `secrets-manager.reader`, with which they read secrets
-using their STACKIT access token:
+Project Admins join the Forgejo `Owners` team (once they signed in to the Git instance), become
+registry admins and get `secrets-manager.reader`, with which they read secrets using their STACKIT
+access token:
```sh
export VAULT_ADDR=https://prod.sm.eu01.stackit.cloud
@@ -175,19 +181,18 @@ built before this architecture:
| Key | Takes over |
|---|---|
-| `project = {display_name, admin_bindings}` | the meshProject **Project Identifier** and its tenant on the landing zone platform; `display_name` keeps the project's display name, `admin_bindings` maps a username to the name of its existing Project Admin binding |
-| `ske = {}` | the SKE cluster **Cluster Name** |
+| `project = {display_name, admin_bindings}` | the meshProject `overrides.project_identifier` and its tenant on the landing zone platform; `display_name` keeps the project's display name, `admin_bindings` maps a username to the name of its existing Project Admin binding |
+| `adopt_ske = true` | the SKE cluster `overrides.cluster_name` |
| `git = {instance_id, instance_name, forgejo_organization}` | the STACKIT Git instance and its Forgejo organization |
-| `dns = {zone_id, record_set_id}` | the DNS zone **DNS Subdomain**.**DNS Parent Domain** and its wildcard record set |
-| `ingress = {}` | the namespaces `cert-manager` and `haproxy-ingress`, their Helm releases `cert-manager` and `haproxy`, and the ClusterIssuer `letsencrypt-prod` |
+| `dns = {zone_id, record_set_id}` | the DNS zone **DNS Domain** names and its wildcard record set |
+| `adopt_ingress = true` | the namespaces `cert-manager` and `haproxy-ingress`, their Helm releases `cert-manager` and `haproxy`, and the ClusterIssuer `letsencrypt-prod` |
| `secrets_manager = {instance_id}` | the Secrets Manager instance, renamed to the platform identifier |
| `container_registry = {artifactory_id, registry_name}` | the container registry of that full name |
| `kubernetes_platform = {platform_name, location_name, landingzone_names}` | the namespace `meshcloud` and the identities `meshfed-service` and `meshfed-metering` of an earlier meshStack platform registration; `platform_name` also takes over the KUBERNETES meshPlatform of that name, and `landingzone_names` maps a stage to the name of its existing landing zone on it. `location_name` references an existing location instead of creating one; `platform_name` needs it or **Use Global Location**. All three are optional; `{}` takes over only what is in the cluster |
-
| `starterkit_definitions = {starterkit, git_repository, forgejo_connector}` | the existing definitions application teams order, by uuid, each optional; the platform registers its definition as a new version of each, so their building blocks upgrade to it |
-Every key but `starterkit_definitions` needs `project`, because the resources live in its STACKIT project. `ingress` and
-`kubernetes_platform` also need `ske`. Everything else is created new. Names the imports leave out
+Every key but `starterkit_definitions` needs `project`, because the resources live in its STACKIT project. `adopt_ingress` and
+`kubernetes_platform` also need `adopt_ske`. Everything else is created new. Names the imports leave out
derive from **Platform Identifier**, so a stage added later gets `-namespace-`.
`git` also needs a Forgejo API token of an owner of the organization, in the sensitive **Import
@@ -203,16 +208,17 @@ The token only serves to make the Git building block's technical user an owner.
first run succeeded, set **Import Secrets** to `{}`; later runs act as that user. Leaving the input
out keeps the stored token, because meshStack keeps an input an update does not send.
-In playground mode, deleting the platform leaves what it took over in place. Otherwise it deletes
-it. A meshPlatform and definitions can only be taken over with playground mode off.
+The order form shows **Imports** and **Import Secrets** only with playground mode off. Through the
+API, a playground that takes something over leaves it in place on deletion; otherwise deletion
+deletes it. A meshPlatform and definitions can only be taken over with playground mode off.
A definition is taken over in phase 2, when the platform registers it. Its latest version decides
what that run does: a draft version is overwritten, and a released one gets a new draft version
only if `hub.bbd_draft` is true. With `hub.bbd_draft = false` the run fails on a released version, so
take the definitions over with `hub.bbd_draft = true` and release afterwards. Existing building
-blocks stay on their version until they are upgraded. Set `starterkit_add_random_name_suffix` to
-what the adopted starterkit used: a starterkit upgrade that changes it renames, and so replaces,
-every project and tenant the starterkit created. meshStack checks the Forgejo connector's
+blocks stay on their version until they are upgraded. Order with **Starterkit Random Name Suffix**
+set to what the adopted starterkit used: a starterkit upgrade that changes it renames, and so
+replaces, every project and tenant the starterkit created, so it cannot be changed after the order. meshStack checks the Forgejo connector's
`repository_name` input against the latest released Git repository version, so release the Git
repository's new draft by hand before the connector can be saved.
@@ -221,7 +227,7 @@ definition and the platform's own definitions are always drafts, so they stay in
workspace.
The first run may change what it takes over to match this architecture's settings. To review it,
-switch on the `building_block_creation` and `any_input_changes` gates in `approval_policies` for the
+switch on the `building_block_creation` and `any_input_changes` gates in **Approval Policies** for the
adoption and off again afterwards.
## Shared Responsibilities
diff --git a/reference-architectures/stackit-kubernetes/buildingblock/imports.tf b/reference-architectures/stackit-kubernetes/buildingblock/imports.tf
index 0f9c2d84..417cf247 100644
--- a/reference-architectures/stackit-kubernetes/buildingblock/imports.tf
+++ b/reference-architectures/stackit-kubernetes/buildingblock/imports.tf
@@ -1,18 +1,18 @@
locals {
imports = var.imports != null ? var.imports : {
project = null
- ske = null
+ adopt_ske = false
git = null
dns = null
- ingress = null
+ adopt_ingress = false
secrets_manager = null
container_registry = null
kubernetes_platform = null
starterkit_definitions = null
}
- # Null keys are left out, so adding a key to `imports` leaves the nested input unchanged.
- platform_imports = { for key, adopted in local.imports : key => adopted if key != "project" && adopted != null }
+ # Unset keys are left out, so adding a key to `imports` leaves the nested input unchanged.
+ platform_imports = { for key, adopted in local.imports : key => adopted if key != "project" && adopted != null && adopted != false }
project_import_ids = local.imports.project == null ? toset([]) : toset(["${var.workspace}.${local.project_identifier}"])
diff --git a/reference-architectures/stackit-kubernetes/buildingblock/main.tf b/reference-architectures/stackit-kubernetes/buildingblock/main.tf
index c616a64c..57d6891a 100644
--- a/reference-architectures/stackit-kubernetes/buildingblock/main.tf
+++ b/reference-architectures/stackit-kubernetes/buildingblock/main.tf
@@ -1,4 +1,20 @@
locals {
+ overrides = coalesce(var.overrides, { cluster_name = null, cluster_issuer_email = null, project_identifier = null })
+
+ # The order form hides these inputs in playground mode, which fixes their values.
+ starterkit_add_random_name_suffix = var.playground_mode ? true : var.starterkit_add_random_name_suffix
+ dns_domain = var.playground_mode ? "stackit.run" : var.dns_domain
+ approval_policies = var.playground_mode ? local.no_approval : var.approval_policies
+ starterkit_approval_policies = var.playground_mode ? local.no_approval : var.starterkit_approval_policies
+
+ no_approval = {
+ building_block_creation = false
+ user_input_changes = false
+ any_input_changes = false
+ manual_triggers = false
+ version_upgrade = false
+ }
+
# The identifier is unique across the whole meshStack instance and lands in the platform name, the
# location and the STACKIT project, so a playground deployment suffixes it instead of occupying the
# plain name.
@@ -14,7 +30,7 @@ locals {
substr(sha256(local.platform_identifier), 0, 4)
)
- project_identifier = coalesce(var.project_identifier, "${local.platform_identifier}-ske")
+ project_identifier = coalesce(local.overrides.project_identifier, "${local.platform_identifier}-ske")
project_display_name = coalesce(
local.imports.project == null ? null : local.imports.project.display_name,
@@ -24,7 +40,20 @@ locals {
# A production platform deletes what it adopted, like anything it created.
release_imports_on_destroy = var.playground_mode && local.imports.project != null
- owner_tags = var.tags.project_owner_tag_key == "" ? {} : { (var.tags.project_owner_tag_key) = [var.creator.displayName] }
+ tag_maps = {
+ for kind in ["landingzone", "building_block", "starterkit_building_block", "project", "starterkit_project"] :
+ kind => { for tag in var.tags[kind] : tag.key => tag.values }
+ }
+
+ stages = {
+ for stage in var.stages : stage.name => {
+ landingzone = { for tag in stage.landingzone : tag.key => tag.values }
+ project = { for tag in stage.project : tag.key => tag.values }
+ quotas = stage.quotas
+ }
+ }
+
+ owner_tags = var.tags.project_owner_tag_key == null ? {} : { (var.tags.project_owner_tag_key) = [var.creator.displayName] }
platform_admins = toset([
for member in var.workspace_members : member.username
@@ -44,6 +73,34 @@ locals {
stackit_project_id = module.tenant.tenant.spec.platform_tenant_id
}
+# Flipping the flag on an existing platform would suffix or rename everything and change what may be destroyed.
+resource "terraform_data" "playground_mode" {
+ input = var.playground_mode
+
+ lifecycle {
+ ignore_changes = [input]
+
+ postcondition {
+ condition = self.output == var.playground_mode
+ error_message = "playground_mode cannot change after the order."
+ }
+ }
+}
+
+# An upgrade with another value renames, and so replaces, every project and tenant the starterkit created.
+resource "terraform_data" "starterkit_add_random_name_suffix" {
+ input = local.starterkit_add_random_name_suffix
+
+ lifecycle {
+ ignore_changes = [input]
+
+ postcondition {
+ condition = self.output == local.starterkit_add_random_name_suffix
+ error_message = "starterkit_add_random_name_suffix cannot change after the order."
+ }
+ }
+}
+
resource "random_string" "playground_suffix" {
lifecycle {
enabled = var.playground_mode
@@ -66,8 +123,8 @@ module "project" {
spec = {
display_name = local.project_display_name
- payment_method_identifier = var.payment_method_identifier
- tags = merge(var.tags.project, local.owner_tags)
+ payment_method_identifier = var.payment_method.name
+ tags = merge(local.tag_maps.project, local.owner_tags)
}
}
diff --git a/reference-architectures/stackit-kubernetes/buildingblock/modules/tenant/variables.tf b/reference-architectures/stackit-kubernetes/buildingblock/modules/tenant/variables.tf
index 1c6ded95..9f4c2f22 100644
--- a/reference-architectures/stackit-kubernetes/buildingblock/modules/tenant/variables.tf
+++ b/reference-architectures/stackit-kubernetes/buildingblock/modules/tenant/variables.tf
@@ -25,8 +25,8 @@ variable "metadata" {
variable "spec" {
type = object({
- platform_ref = object({ uuid = string, kind = string })
- landing_zone_ref = object({ name = string, kind = string })
+ platform_ref = object({ uuid = string })
+ landing_zone_ref = object({ name = string })
})
nullable = false
}
diff --git a/reference-architectures/stackit-kubernetes/buildingblock/platform.tf b/reference-architectures/stackit-kubernetes/buildingblock/platform.tf
index 7ce95d28..48fec5e9 100644
--- a/reference-architectures/stackit-kubernetes/buildingblock/platform.tf
+++ b/reference-architectures/stackit-kubernetes/buildingblock/platform.tf
@@ -1,14 +1,16 @@
locals {
- cluster_name = coalesce(var.cluster_name, format(
+ cluster_name = coalesce(local.overrides.cluster_name, format(
"%s-%s",
replace(substr(local.platform_identifier, 0, 6), "/-+$/", ""),
substr(sha256(local.platform_identifier), 0, 4)
))
# It's fine that by default, Let's Encrypt Expiry notifications go nowhere.
- cluster_issuer_email = coalesce(var.cluster_issuer_email, local.platform_service_account_email)
+ cluster_issuer_email = coalesce(local.overrides.cluster_issuer_email, local.platform_service_account_email)
- dns_subdomain = trimspace(var.dns_subdomain == null ? "" : var.dns_subdomain) != "" ? lower(var.dns_subdomain) : lower(local.platform_identifier)
+ # STACKIT allows a single label under its free domains.
+ dns_domain_labels = regex("^(?:([a-z0-9-]+)\\.)?(stackit\\.(?:run|zone|rocks|gg)|runs\\.onstackit\\.cloud)$", local.dns_domain)
+ dns_subdomain = lower(coalesce(local.dns_domain_labels[0], local.platform_identifier))
}
# The stackit provider rejects a service account email that is unknown while planning, so this run
@@ -17,14 +19,12 @@ locals {
module "platform_integration" {
source = "github.com/meshcloud/meshstack-hub//reference-architectures/stackit-kubernetes/platform?ref=${var.hub.git_ref}"
- approval_policies = var.approval_policies
- starterkit_approval_policies = var.starterkit_approval_policies
+ approval_policies = local.approval_policies
+ starterkit_approval_policies = local.starterkit_approval_policies
- starterkit_add_random_name_suffix = var.starterkit_add_random_name_suffix
+ starterkit_add_random_name_suffix = local.starterkit_add_random_name_suffix
- platform_engineer_role_mapping = var.platform_engineer_role_mapping
-
- meshstack = { owning_workspace_identifier = var.workspace, tags = var.tags.building_block }
+ meshstack = { owning_workspace_identifier = var.workspace, tags = local.tag_maps.building_block }
hub = var.hub
}
@@ -48,7 +48,7 @@ resource "meshstack_building_block" "platform" {
display_name = "Platform Services"
target_ref = module.tenant.tenant.ref
- inputs = merge({
+ inputs = {
STACKIT_SERVICE_ACCOUNT_EMAIL = { value = jsonencode(local.platform_service_account_email) }
automation_identity = { value = jsonencode(jsonencode({
building_block_ref = meshstack_building_block.platform_service_account.ref
@@ -63,25 +63,23 @@ resource "meshstack_building_block" "platform" {
cluster_name = { value = jsonencode(local.cluster_name) }
cluster_issuer_email = { value = jsonencode(local.cluster_issuer_email) }
dns_subdomain = { value = jsonencode(local.dns_subdomain) }
- dns_parent_domain = { value = jsonencode(var.dns_parent_domain) }
- ai_model = { value = jsonencode(var.ai_model) }
+ dns_parent_domain = { value = jsonencode(local.dns_domain_labels[1]) }
phase2_completed = { value = jsonencode(var.phase2_completed) }
- starterkit_app_name = { value = jsonencode(var.starterkit_app_name) }
- starterkit_repo_clone_addr = { value = jsonencode(var.starterkit_repo_clone_addr) }
+ starterkit_app_name = { value = jsonencode(var.starterkit_demo.name) }
+ starterkit_repo_clone_addr = { value = jsonencode(var.starterkit_demo.template_repo) }
+ ai_model = { value = jsonencode(var.starterkit_demo.ai_model) }
tags = { value = jsonencode(jsonencode({
- landingzone = var.tags.landingzone
- building_block = var.tags.building_block
- starterkit_building_block = var.tags.starterkit_building_block
- starterkit_project = var.tags.starterkit_project
+ landingzone = local.tag_maps.landingzone
+ building_block = local.tag_maps.building_block
+ starterkit_building_block = local.tag_maps.starterkit_building_block
+ starterkit_project = local.tag_maps.starterkit_project
project_owner_tag_key = var.tags.project_owner_tag_key
})) }
- stages = { value = jsonencode(jsonencode(var.stages)) }
+ stages = { value = jsonencode(jsonencode(local.stages)) }
quota_definitions = { value = jsonencode(jsonencode(var.quota_definitions)) }
- }, length(local.platform_imports) == 0 ? {} : {
- imports = { value = jsonencode(jsonencode(local.platform_imports)) }
- }, {
+ imports = { value = jsonencode(jsonencode(length(local.platform_imports) == 0 ? null : local.platform_imports)) }
# Sent even when empty: meshStack keeps the stored value of an input an update leaves out.
import_secrets = {
sensitive = {
@@ -89,6 +87,6 @@ resource "meshstack_building_block" "platform" {
secret_version = nonsensitive(sha256(jsonencode(var.import_secrets)))
}
}
- })
+ }
}
}
diff --git a/reference-architectures/stackit-kubernetes/buildingblock/variables.tf b/reference-architectures/stackit-kubernetes/buildingblock/variables.tf
index fba70fb1..d4d8c170 100644
--- a/reference-architectures/stackit-kubernetes/buildingblock/variables.tf
+++ b/reference-architectures/stackit-kubernetes/buildingblock/variables.tf
@@ -1,7 +1,45 @@
+variable "platform_identifier" {
+ type = string
+ nullable = false
+ description = "Identifier of the Kubernetes platform created in meshStack (letters, digits and dashes only). In playground mode a random suffix is appended to it."
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9-]+$", var.platform_identifier))
+ error_message = "platform_identifier must only contain letters, digits, and dashes."
+ }
+}
+
+variable "phase2_completed" {
+ type = bool
+ nullable = false
+ description = "Set to true once a Harbor robot is linked to the platform's service account, as the summary describes; the run fails while none is."
+}
+
+variable "use_global_location" {
+ type = bool
+ nullable = false
+ description = "Use the global meshStack location instead of creating a dedicated one for this platform."
+}
+
+variable "payment_method" {
+ type = object({
+ kind = string
+ name = string
+ })
+ nullable = false
+ description = "Payment method of the platform's own meshProject."
+}
+
+variable "playground_mode" {
+ type = bool
+ nullable = false
+ description = "Deploy a throwaway platform that gets a random identifier suffix and stays destroyable. Cannot change after the first run."
+}
+
variable "landingzone" {
type = object({
- platform_ref = object({ uuid = string, kind = string })
- landingzone_refs = map(object({ name = string, kind = string }))
+ platform_ref = object({ uuid = string })
+ landingzone_refs = map(object({ name = string }))
service_account_bbd_version_ref = object({ uuid = string })
service_account_federation_bbd_version_ref = object({ uuid = string })
})
@@ -20,65 +58,65 @@ variable "landingzone_variant" {
}
}
-variable "workspace" {
- type = string
- nullable = false
- description = "Identifier of the meshStack workspace that owns the platform, location, landing zones, STACKIT project and the building block definitions this architecture registers."
-}
-
-variable "platform_identifier" {
- type = string
- nullable = false
- description = "Identifier of the Kubernetes platform created in meshStack (letters, digits and dashes only). In playground mode a random suffix is appended to it."
+variable "starterkit_add_random_name_suffix" {
+ type = bool
+ default = null
+ description = "Append a random suffix to the projects the starterkit creates. Cannot change after the first run: it renames, and so replaces, every project and tenant the starterkit created. Ignored in playground mode, which always appends one."
validation {
- condition = can(regex("^[a-zA-Z0-9-]+$", var.platform_identifier))
- error_message = "platform_identifier must only contain letters, digits, and dashes."
+ condition = var.playground_mode || var.starterkit_add_random_name_suffix != null
+ error_message = "starterkit_add_random_name_suffix is required unless playground_mode is set."
}
}
-variable "use_global_location" {
- type = bool
+variable "starterkit_demo" {
+ type = object({
+ name = string
+ template_repo = string
+ ai_model = optional(string)
+ })
nullable = false
- description = "Use the global meshStack location instead of creating a dedicated one for this platform."
+ description = <<-EOT
+ Demo application the starter kit deploys.
+ `name`: image name every application ordered from the starter kit builds under, set as APP_NAME.
+ `template_repo`: template repository the starter kit initialises an application repository from.
+ `ai_model`: model applications default to, one STACKIT Model Serving's `/v1/models` endpoint serves; null sets up no Model Serving token and no `stackit-ai` secret.
+ EOT
}
-variable "payment_method_identifier" {
+variable "dns_domain" {
type = string
default = null
- description = "Payment method identifier assigned to the platform's meshProject. Null assigns none."
-}
+ description = "DNS zone of the platform under a free STACKIT domain: a bare one such as `stackit.run` gives `.stackit.run`, one label more such as `likvid.stackit.run` is the zone itself. Ignored in playground mode, which uses `stackit.run`."
-variable "project_identifier" {
- type = string
- default = null
- description = "Identifier of the platform's meshProject; null uses `-ske`."
-}
+ validation {
+ condition = var.playground_mode || var.dns_domain != null
+ error_message = "dns_domain is required unless playground_mode is set."
+ }
-variable "tags" {
- type = object({
- landingzone = map(list(string))
- building_block = map(list(string))
- starterkit_building_block = optional(map(list(string)), {})
- project = map(list(string))
- starterkit_project = map(list(string))
- project_owner_tag_key = optional(string, "")
- })
- nullable = false
- description = "Tags shared by every stage: `landingzone` on the SKE landing zones, `building_block` on the definitions platform engineers order, `starterkit_building_block` on those application teams order (Git repository, Forgejo connector, SKE starterkit), `project` on the platform's meshProject and `starterkit_project` on the starter kit's meshProjects. `project_owner_tag_key` names the tag on those meshProjects that gets the creator's display name, e.g. `projectOwner`."
+ validation {
+ condition = var.dns_domain == null || can(regex("^([a-z0-9]([a-z0-9-]{0,18}[a-z0-9])?\\.)?(stackit\\.(run|zone|rocks|gg)|runs\\.onstackit\\.cloud)$", var.dns_domain))
+ error_message = "dns_domain must be stackit.run, stackit.zone, stackit.rocks, stackit.gg or runs.onstackit.cloud, optionally with one label of at most 20 lowercase letters, digits or dashes in front."
+ }
}
variable "stages" {
- type = map(object({
- landingzone = optional(map(list(string)), {})
- project = optional(map(list(string)), {})
+ type = list(object({
+ name = string
+ landingzone = list(object({ key = string, values = list(string) }))
+ project = list(object({ key = string, values = list(string) }))
quotas = optional(list(object({
key = string
value = number
})))
}))
nullable = false
- description = "Stages by name, each with one landing zone and one starter kit meshProject and namespace. Their `landingzone` and `project` tags are merged over `tags.landingzone` and `tags.starterkit_project`. `quotas` sets the landing zone's quotas; null keeps the defaults of the `kubernetes` hub module, the larger set for `prod`."
+ description = "Stages, each with one landing zone and one starter kit meshProject and namespace. Their `landingzone` and `project` tags are merged over `tags.landingzone` and `tags.starterkit_project`. `quotas` sets the landing zone's quotas; null keeps the defaults of the `kubernetes` hub module, the larger set for `prod`."
+
+ validation {
+ condition = length(distinct([for stage in var.stages : stage.name])) == length(var.stages)
+ error_message = "Every stage needs a name of its own."
+ }
}
variable "quota_definitions" {
@@ -95,31 +133,43 @@ variable "quota_definitions" {
description = "Quotas the meshPlatform offers, with their limits; null keeps the defaults of the `kubernetes` hub module. CPU in millicores (m), memory in mebibytes (Mi)."
}
-variable "creator" {
+variable "tags" {
type = object({
- type = string
- identifier = string
- displayName = string
- username = optional(string)
- email = optional(string)
- euid = optional(string)
+ landingzone = list(object({ key = string, values = list(string) }))
+ building_block = list(object({ key = string, values = list(string) }))
+ starterkit_building_block = list(object({ key = string, values = list(string) }))
+ project = list(object({ key = string, values = list(string) }))
+ starterkit_project = list(object({ key = string, values = list(string) }))
+ project_owner_tag_key = optional(string)
})
nullable = false
- description = "Creator of the platform, injected by meshStack; its display name fills the `tags.project_owner_tag_key` tag."
+ description = <<-EOT
+ Tags shared by every stage, each map built in the meshPanel form as a list of {key, values} entries.
+ `landingzone` goes on the SKE landing zones, `building_block` on the definitions platform engineers order,
+ `starterkit_building_block` on those application teams order (Git repository, Forgejo connector, SKE starterkit),
+ `project` on the platform's meshProject and `starterkit_project` on the starter kit's meshProjects.
+ `project_owner_tag_key` names the tag on those meshProjects that gets the creator's display name; null sets none.
+ EOT
}
-variable "workspace_members" {
- type = list(object({
- meshIdentifier = string
- username = string
- firstName = string
- lastName = string
- email = string
- euid = string
- roles = list(string)
- }))
- nullable = false
- description = "Members of the owning workspace, injected by meshStack. Owners and managers become Project Admin of the platform's project."
+variable "overrides" {
+ type = object({
+ cluster_name = optional(string)
+ cluster_issuer_email = optional(string)
+ project_identifier = optional(string)
+ })
+ default = null
+ description = <<-EOT
+ Names and settings that replace the generated ones; null or a field left out keeps the default.
+ `cluster_name`: SKE cluster name, 2-11 chars, lowercase alphanumeric or dashes; the default derives from the platform identifier.
+ `cluster_issuer_email`: Let's Encrypt contact email of the ACME ClusterIssuer; the default is the platform's service account.
+ `project_identifier`: identifier of the platform's meshProject; the default is `-ske`.
+ EOT
+
+ validation {
+ condition = (var.overrides == null ? null : var.overrides.cluster_name) == null ? true : can(regex("^[a-z0-9][a-z0-9-]{0,9}[a-z0-9]$", var.overrides.cluster_name))
+ error_message = "overrides.cluster_name must be 2-11 characters, lowercase alphanumeric or dashes, and not start or end with a dash."
+ }
}
variable "approval_policies" {
@@ -130,8 +180,13 @@ variable "approval_policies" {
manual_triggers = bool
version_upgrade = bool
})
- nullable = false
- description = "Run triggers that need an operator's approval on the platform definitions this architecture registers."
+ default = null
+ description = "Run triggers that need an operator's approval on the platform definitions this architecture registers. Ignored in playground mode, which gates nothing."
+
+ validation {
+ condition = var.playground_mode || var.approval_policies != null
+ error_message = "approval_policies is required unless playground_mode is set."
+ }
}
variable "starterkit_approval_policies" {
@@ -142,111 +197,22 @@ variable "starterkit_approval_policies" {
manual_triggers = bool
version_upgrade = bool
})
- nullable = false
- description = "Run triggers that need an operator's approval on the application team definitions (Git repository, Forgejo connector, SKE starterkit) this architecture registers."
-}
-
-variable "starterkit_add_random_name_suffix" {
- type = bool
- nullable = false
- description = "Append a random suffix to the projects the starterkit creates. Keep it as the adopted starterkit had it: an upgrade that changes it renames, and so replaces, every project and tenant it created."
-}
-
-variable "platform_engineer_role_mapping" {
- type = object({
- git = optional(map(list(string)))
- container_registry = optional(map(list(string)))
- secrets_manager = optional(map(list(string)))
- })
- nullable = false
- default = {}
- description = "Maps the meshStack project roles on the platform's project to roles on the Git instance (Forgejo `owner`, `writer`, `reader`), the container registry and the Secrets Manager (STACKIT project roles). A service left out keeps its module's default: Project Admins become Forgejo owners, registry admins and Secrets Manager readers."
-}
-
-variable "playground_mode" {
- type = bool
- nullable = false
- description = "Deploy a throwaway platform that gets a random identifier suffix and stays destroyable."
-}
-
-variable "cluster_name" {
- type = string
- nullable = true
default = null
- description = "Overrides the generated SKE cluster name. 2-11 chars, lowercase alphanumeric or dashes."
+ description = "Run triggers that need an operator's approval on the application team definitions (Git repository, Forgejo connector, SKE starterkit) this architecture registers. Ignored in playground mode, which gates nothing."
validation {
- condition = var.cluster_name == null || var.cluster_name == "" || can(regex("^[a-z0-9][a-z0-9-]{0,9}[a-z0-9]$", var.cluster_name))
- error_message = "cluster_name must be 2-11 characters, lowercase alphanumeric or dashes, and not start or end with a dash."
+ condition = var.playground_mode || var.starterkit_approval_policies != null
+ error_message = "starterkit_approval_policies is required unless playground_mode is set."
}
}
-variable "cluster_issuer_email" {
- type = string
- nullable = true
- default = null
- description = "Overrides the Let's Encrypt contact email registered for the ACME ClusterIssuer."
-}
-
-variable "dns_subdomain" {
- type = string
- nullable = true
- default = null
- description = "Label the platform's DNS zone occupies under `dns_parent_domain`. Empty uses the platform identifier. Set it to adopt a zone that already carries a different name."
-}
-
-variable "dns_parent_domain" {
- type = string
- nullable = false
- description = "Domain the platform's DNS zone is created under."
-}
-
-variable "phase2_completed" {
- type = bool
- nullable = false
- default = false
- description = "Set to true once a Harbor robot is linked to the platform's service account, as the summary describes; the run fails while none is."
-}
-
-variable "ai_model" {
- type = string
- nullable = false
- description = "Model applications default to. Must be one STACKIT Model Serving's `/v1/models` endpoint serves."
-}
-
-variable "starterkit_app_name" {
- type = string
- nullable = false
- description = "Image name every application ordered from the starter kit builds under, set as APP_NAME."
-}
-
-variable "starterkit_repo_clone_addr" {
- type = string
- nullable = false
- description = "Template repository the starter kit initialises an application repository from."
-}
-
-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`: meshstack-hub reference the nested integrations are sourced from.
- `bbd_draft`: Draft state of the application team definitions; the platform's own are always drafts.
- EOT
-}
-
variable "imports" {
type = object({
project = optional(object({
display_name = optional(string)
admin_bindings = optional(map(string), {})
}))
- ske = optional(object({}))
+ adopt_ske = optional(bool, false)
git = optional(object({
instance_id = string
instance_name = string
@@ -256,7 +222,7 @@ variable "imports" {
zone_id = string
record_set_id = string
}))
- ingress = optional(object({}))
+ adopt_ingress = optional(bool, false)
secrets_manager = optional(object({
instance_id = string
}))
@@ -279,13 +245,13 @@ variable "imports" {
description = "Existing resources and definitions to take over, by key; see Adopting Existing Resources in the README. `git` needs `import_secrets.git` on its first run; null creates everything."
validation {
- condition = var.imports == null || var.imports.project != null || alltrue([for key, adopted in var.imports : adopted == null if key != "starterkit_definitions"])
+ condition = var.imports == null || var.imports.project != null || alltrue([for key, adopted in var.imports : adopted == null || adopted == false if key != "starterkit_definitions"])
error_message = "Every key of imports but starterkit_definitions needs imports.project, because what it names lives in that project."
}
validation {
- condition = var.imports == null || (var.imports.ingress == null && var.imports.kubernetes_platform == null) || var.imports.ske != null
- error_message = "imports.ingress and imports.kubernetes_platform need imports.ske, because they live in that cluster."
+ condition = var.imports == null || (!var.imports.adopt_ingress && var.imports.kubernetes_platform == null) || var.imports.adopt_ske
+ error_message = "imports.adopt_ingress and imports.kubernetes_platform need imports.adopt_ske, because they live in that cluster."
}
validation {
@@ -316,8 +282,54 @@ variable "import_secrets" {
forgejo_api_token = string
}))
})
- nullable = false
- default = {}
sensitive = true
+ default = null
description = "Secrets of the resources in `imports`: `git.forgejo_api_token`, a token of an owner of the adopted Forgejo organization, needed until the organization is adopted."
}
+
+variable "workspace" {
+ type = string
+ nullable = false
+ description = "Identifier of the meshStack workspace that owns the platform, location, landing zones, STACKIT project and the building block definitions this architecture registers."
+}
+
+variable "creator" {
+ type = object({
+ type = string
+ identifier = string
+ displayName = string
+ username = optional(string)
+ email = optional(string)
+ euid = optional(string)
+ })
+ nullable = false
+ description = "Creator of the platform, injected by meshStack; its display name fills the `tags.project_owner_tag_key` tag."
+}
+
+variable "workspace_members" {
+ type = list(object({
+ meshIdentifier = string
+ username = string
+ firstName = string
+ lastName = string
+ email = string
+ euid = string
+ roles = list(string)
+ }))
+ nullable = false
+ description = "Members of the owning workspace, injected by meshStack. Owners and managers become Project Admin of the platform's project."
+}
+
+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`: meshstack-hub reference the nested integrations are sourced from.
+ `bbd_draft`: Draft state of the application team definitions; the platform's own are always drafts.
+ EOT
+}
diff --git a/reference-architectures/stackit-kubernetes/e2e/main.tf b/reference-architectures/stackit-kubernetes/e2e/main.tf
index 4a296932..eebfc769 100644
--- a/reference-architectures/stackit-kubernetes/e2e/main.tf
+++ b/reference-architectures/stackit-kubernetes/e2e/main.tf
@@ -9,8 +9,9 @@ variable "test_context" {
can(var.test_context.hub_git_ref),
can(var.test_context.meshstack.project_identifier_suffix),
can(var.test_context.meshstack.tag_schema.mandatory.project),
+ can(var.test_context.meshstack.tag_schema.mandatory.payment_method),
])
- error_message = "test_context must provide workspace, run_id, hub_git_ref, meshstack.project_identifier_suffix and meshstack.tag_schema.mandatory.project."
+ error_message = "test_context must provide workspace, run_id, hub_git_ref, meshstack.project_identifier_suffix and meshstack.tag_schema.mandatory.project and .payment_method."
}
validation {
@@ -44,12 +45,8 @@ locals {
}
# A landing zone has to offer every value a project assigned to it may carry.
- starterkit_project_tags = {
- confidentiality = ["Public"]
- }
- landingzone_tags = {
- confidentiality = ["Public", "Internal", "Confidential"]
- }
+ starterkit_project_tags = [{ key = "confidentiality", values = ["Public"] }]
+ landingzone_tags = [{ key = "confidentiality", values = ["Public", "Internal", "Confidential"] }]
}
provider "stackit" {
@@ -91,13 +88,22 @@ module "service_account_federation" {
stackit_service_account_name = "${var.test_context.run_id}-safb"
}
+resource "meshstack_payment_method" "platform" {
+ metadata = {
+ name = "${var.test_context.run_id}-ske-pm"
+ owned_by_workspace = var.test_context.workspace
+ }
+ spec = {
+ display_name = "${var.test_context.run_id} STACKIT Kubernetes Platform"
+ tags = var.test_context.meshstack.tag_schema.mandatory.payment_method
+ }
+}
+
module "stackit_kubernetes" {
source = "../"
meshstack = local.meshstack
hub = local.hub
-
- playground_mode = true
}
resource "meshstack_building_block" "this" {
@@ -124,34 +130,39 @@ resource "meshstack_building_block" "this" {
# service account.
inputs = {
landingzone = { value = jsonencode(jsonencode({
- platform_ref = { uuid = local.stackit.platform_uuid, kind = "meshPlatform" }
+ platform_ref = { uuid = local.stackit.platform_uuid }
landingzone_refs = {
- default = { name = local.stackit.landing_zone_name, kind = "meshLandingZone" }
+ default = { name = local.stackit.landing_zone_name }
}
service_account_bbd_version_ref = { uuid = module.service_account.building_block_definition.version_ref.uuid }
service_account_federation_bbd_version_ref = { uuid = module.service_account_federation.building_block_definition.version_ref.uuid }
})) }
landingzone_variant = { value = jsonencode("default") }
platform_identifier = { value = jsonencode(var.test_context.run_id) }
- project_identifier = { value = jsonencode("${var.test_context.run_id}-ske${var.test_context.meshstack.project_identifier_suffix}") }
use_global_location = { value = jsonencode(false) }
- dns_parent_domain = { value = jsonencode("stackit.run") }
- ai_model = { value = jsonencode("openai/gpt-oss-120b") }
+ payment_method = { value = jsonencode(meshstack_payment_method.platform.ref) }
+ playground_mode = { value = jsonencode(true) }
+ starterkit_demo = { value = jsonencode(jsonencode({
+ name = "ai-summarizer"
+ template_repo = "https://github.com/likvid-bank/starterkit-template-stackit-ai-summarizer.git"
+ ai_model = "openai/gpt-oss-120b"
+ })) }
+ phase2_completed = { value = jsonencode(false) }
- starterkit_app_name = { value = jsonencode("ai-summarizer") }
- starterkit_repo_clone_addr = { value = jsonencode("https://github.com/likvid-bank/starterkit-template-stackit-ai-summarizer.git") }
+ overrides = { value = jsonencode(jsonencode({
+ project_identifier = "${var.test_context.run_id}-ske${var.test_context.meshstack.project_identifier_suffix}"
+ })) }
tags = { value = jsonencode(jsonencode({
- project = var.test_context.meshstack.tag_schema.mandatory.project
+ project = [for key, values in var.test_context.meshstack.tag_schema.mandatory.project : { key = key, values = values }]
starterkit_project = local.starterkit_project_tags
landingzone = local.landingzone_tags
- building_block = {}
- starterkit_building_block = {}
- project_owner_tag_key = ""
- })) }
- stages = { value = jsonencode(jsonencode({
- dev = { landingzone = { environment = ["dev"] }, project = { environment = ["dev"] } }
+ building_block = []
+ starterkit_building_block = []
})) }
+ stages = { value = jsonencode(jsonencode([
+ { name = "dev", landingzone = [{ key = "environment", values = ["dev"] }], project = [{ key = "environment", values = ["dev"] }] },
+ ])) }
}
}
}
diff --git a/reference-architectures/stackit-kubernetes/meshstack_integration.tf b/reference-architectures/stackit-kubernetes/meshstack_integration.tf
index 98515225..178f4b11 100644
--- a/reference-architectures/stackit-kubernetes/meshstack_integration.tf
+++ b/reference-architectures/stackit-kubernetes/meshstack_integration.tf
@@ -1,10 +1,3 @@
-variable "playground_mode" {
- type = bool
- nullable = false
- default = true
- description = "Deploy a throwaway platform that gets a random identifier suffix and stays destroyable."
-}
-
variable "approval_policies" {
type = object({
building_block_creation = optional(bool, false)
@@ -13,40 +6,15 @@ variable "approval_policies" {
manual_triggers = optional(bool, false)
version_upgrade = optional(bool, false)
})
- nullable = false
- default = {}
- description = "Run triggers that need an operator's approval on this architecture and on the platform definitions it registers."
-}
-
-variable "starterkit_approval_policies" {
- type = object({
- building_block_creation = optional(bool, false)
- user_input_changes = optional(bool, false)
- any_input_changes = optional(bool, false)
- manual_triggers = optional(bool, false)
- version_upgrade = optional(bool, false)
- })
- nullable = false
- default = {}
- description = "Run triggers that need an operator's approval on the application team definitions (Git repository, Forgejo connector, SKE starterkit) this architecture registers."
-}
-
-variable "starterkit_add_random_name_suffix" {
- type = bool
- nullable = false
- default = true
- description = "Append a random suffix to the projects the starterkit creates. Keep it as the adopted starterkit had it: an upgrade that changes it renames, and so replaces, every project and tenant it created."
-}
-
-variable "platform_engineer_role_mapping" {
- type = object({
- git = optional(map(list(string)))
- container_registry = optional(map(list(string)))
- secrets_manager = optional(map(list(string)))
- })
- nullable = false
- default = {}
- description = "Maps the meshStack project roles on the platform's project to roles on the Git instance (Forgejo `owner`, `writer`, `reader`), the container registry and the Secrets Manager (STACKIT project roles). A service left out keeps its module's default: Project Admins become Forgejo owners, registry admins and Secrets Manager readers."
+ nullable = false
+ default = {
+ building_block_creation = false
+ user_input_changes = false
+ any_input_changes = false
+ manual_triggers = false
+ version_upgrade = false
+ }
+ description = "Run triggers that need an operator's approval on this architecture's own runs. The platform definitions it registers take theirs from the Approval Policies input."
}
variable "meshstack" {
@@ -88,6 +56,53 @@ output "building_block_definition" {
}
}
+# meshPanel has no form widget for a map of free-form keys, so tag maps and stages are growable lists;
+# buildingblock/ folds them back into maps.
+locals {
+ tag_list_schema = {
+ type = "array"
+ items = {
+ type = "object"
+ required = ["key", "values"]
+ properties = {
+ key = { type = "string", title = "Tag Key" }
+ values = { type = "array", title = "Values", minItems = 1, items = { type = "string" } }
+ }
+ }
+ }
+
+ approval_gates = ["building_block_creation", "user_input_changes", "any_input_changes", "manual_triggers", "version_upgrade"]
+ approval_policies_default = { for gate in local.approval_gates : gate => false }
+
+ approval_policies_schema = {
+ type = "object"
+ additionalProperties = false
+ required = local.approval_gates
+ properties = { for gate, enabled in local.approval_policies_default : gate => { type = "boolean", title = gate, default = enabled } }
+ }
+
+ tags_default = {
+ landingzone = []
+ building_block = []
+ starterkit_building_block = []
+ project = []
+ starterkit_project = []
+ }
+
+ starterkit_demo_default = {
+ name = "ai-summarizer"
+ template_repo = "https://github.com/likvid-bank/starterkit-template-stackit-ai-summarizer.git"
+ ai_model = "openai/gpt-oss-120b"
+ }
+
+ stages_default = [
+ { name = "dev", landingzone = [], project = [] },
+ { name = "prod", landingzone = [], project = [] },
+ ]
+
+ uuid_pattern = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
+}
+
resource "meshstack_building_block_definition" "this" {
metadata = {
owned_by_workspace = var.meshstack.owning_workspace_identifier
@@ -120,9 +135,10 @@ resource "meshstack_building_block_definition" "this" {
- **SKE cluster** – a managed STACKIT Kubernetes Engine cluster with an admin kubeconfig.
- **In-cluster services** – HAProxy ingress, cert-manager with a Let's Encrypt ClusterIssuer, and
the meshStack replication/metering service accounts.
- - **DNS zone** – a zone under the DNS Parent Domain with a wildcard record pointing at the
- ingress load balancer.
- - **AI Model Serving** – a STACKIT Model Serving token every application namespace receives.
+ - **DNS zone** – the zone DNS Domain names, with a wildcard record pointing at the ingress load
+ balancer.
+ - **AI Model Serving** – a STACKIT Model Serving token every application namespace receives,
+ unless Starter Kit Demo names no AI model.
- **STACKIT Git instance** – a managed Forgejo with its organization and a STACKIT-hosted shared
runner.
- **Container registry** – a Harbor project for the platform's application images, with the
@@ -203,50 +219,14 @@ resource "meshstack_building_block_definition" "this" {
}
inputs = {
- landingzone = {
- display_name = "STACKIT Landing Zone"
- description = "HCL object of refs this platform is built on. Copy it from the summary of the STACKIT Landing Zone building block."
- type = "CODE"
- assignment_type = "USER_INPUT"
- }
-
- landingzone_variant = {
- display_name = "Landing Zone Variant"
- description = "Landing zone the hosting project is created in. `networked` requires hub-and-spoke networking enabled on the STACKIT Landing Zone."
- type = "SINGLE_SELECT"
- assignment_type = "USER_INPUT"
- # TODO only include 'networked' if enabled in stackit lz ref arch (add 'network_enabled' variable here, default false?)
- selectable_values = ["default", "networked"]
- default_value = jsonencode("default")
- }
-
- cluster_name = {
- display_name = "Cluster Name"
- description = "Overrides the generated SKE cluster name. 2-11 chars, lowercase alphanumeric or dashes."
- type = "STRING"
- assignment_type = "USER_INPUT"
- is_optional = true
- value_validation_regex = "^$|^[a-z0-9][a-z0-9-]{0,9}[a-z0-9]$"
- validation_regex_error_message = "Cluster name must be 2-11 characters, lowercase alphanumeric or dashes, and not start or end with a dash."
- }
-
- cluster_issuer_email = {
- display_name = "ClusterIssuer Email"
- description = "Overrides the Let's Encrypt contact email registered for the ACME ClusterIssuer."
- type = "STRING"
- assignment_type = "USER_INPUT"
- is_optional = true
- }
-
- dns_subdomain = {
- display_name = "DNS Subdomain"
- description = "Label the platform's DNS zone occupies under the parent domain. Leave empty to use the platform identifier."
+ platform_identifier = {
+ display_name = "Platform Identifier"
+ description = "Identifier of the Kubernetes platform created in meshStack (letters, digits and dashes only). In playground mode a random suffix is appended to it."
type = "STRING"
assignment_type = "USER_INPUT"
- is_optional = true
- updateable_by_consumer = true
- value_validation_regex = "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
- validation_regex_error_message = "DNS subdomain must be a DNS label: lowercase letters, digits and dashes, not starting or ending with a dash."
+ value_validation_regex = "^[a-zA-Z0-9-]+$"
+ validation_regex_error_message = "platform_identifier must only contain letters, digits, and dashes."
+ display_order = 10
}
phase2_completed = {
@@ -256,199 +236,417 @@ resource "meshstack_building_block_definition" "this" {
assignment_type = "USER_INPUT"
default_value = jsonencode(false)
updateable_by_consumer = true
+ display_order = 12
}
- ai_model = {
- display_name = "AI Model"
- description = "Model applications on this platform default to. Must be one STACKIT Model Serving's `/v1/models` endpoint serves."
- type = "STRING"
- assignment_type = "USER_INPUT"
- default_value = jsonencode("openai/gpt-oss-120b")
- updateable_by_consumer = true
- }
-
- dns_parent_domain = {
- display_name = "DNS Parent Domain"
- description = "Domain the platform's DNS zone is created under. The default is the one STACKIT delegates to its customers, so the zone resolves without owning a domain."
- type = "STRING"
- assignment_type = "USER_INPUT"
- default_value = jsonencode("stackit.run")
- updateable_by_consumer = true
+ use_global_location = {
+ display_name = "Use Global Location"
+ description = "Use the existing global meshStack location. Turn it off to create a location of its own for this platform."
+ type = "BOOLEAN"
+ assignment_type = "USER_INPUT"
+ default_value = jsonencode(true)
+ display_order = 15
}
- quota_definitions = {
- display_name = "Quota Definitions"
- description = "HCL list of the quotas the meshPlatform offers (`quota_key`, `label`, `description`, `unit`, `min_value`, `max_value`, `auto_approval_threshold`). Empty keeps the defaults."
+ payment_method = {
+ display_name = "Payment Method"
+ description = "Payment method of the platform's own meshProject, one of this workspace's payment methods."
type = "CODE"
- assignment_type = "USER_INPUT"
- is_optional = true
+ assignment_type = "PAYMENT_METHOD"
updateable_by_consumer = true
+ display_order = 20
}
- hub = {
- display_name = "Hub"
- description = "HCL object with `git_ref` (meshstack-hub reference to source nested modules from) and `bbd_draft` (draft state of the application team definitions)."
- type = "CODE"
- assignment_type = "STATIC"
- argument = jsonencode(jsonencode(var.hub))
- }
-
- workspace = {
- display_name = "Workspace Identifier"
- description = "Workspace that will own the created platform, location, landing zones and hosting project."
- type = "STRING"
- assignment_type = "WORKSPACE_IDENTIFIER"
- }
-
- creator = {
- display_name = "Creator"
- description = "Creator of the platform, injected by meshStack."
- type = "CODE"
- assignment_type = "AUTHOR"
+ playground_mode = {
+ display_name = "Playground Mode"
+ description = "Throwaway deployment: the identifier gets a random suffix and nothing is protected against deletion. Do not publish such a platform or its definitions to other workspaces. Cannot be changed after the order."
+ type = "BOOLEAN"
+ assignment_type = "USER_INPUT"
+ default_value = jsonencode(true)
+ display_order = 25
}
- workspace_members = {
- display_name = "Workspace Members"
- description = "Members of the owning workspace. Owners and managers become Project Admin of the platform's project."
+ landingzone = {
+ display_name = "STACKIT Landing Zone"
+ description = "HCL object of refs this platform is built on. Copy it from the summary of the STACKIT Landing Zone building block."
type = "CODE"
- assignment_type = "USER_PERMISSIONS"
+ assignment_type = "USER_INPUT"
+ display_order = 27
}
- payment_method_identifier = {
- display_name = "Payment Method Identifier"
- description = "Payment method assigned to the hosting meshProject. Leave empty to assign none."
- type = "STRING"
+ landingzone_variant = {
+ display_name = "Landing Zone Variant"
+ description = "Landing zone the hosting project is created in. `networked` requires hub-and-spoke networking enabled on the STACKIT Landing Zone."
+ type = "SINGLE_SELECT"
assignment_type = "USER_INPUT"
- is_optional = true
+ # TODO only include 'networked' if enabled in stackit lz ref arch (add 'network_enabled' variable here, default false?)
+ selectable_values = ["default", "networked"]
+ default_value = jsonencode("default")
+ display_order = 28
}
- project_identifier = {
- display_name = "Project Identifier"
- description = "Identifier of the hosting meshProject; empty uses `-ske`."
- type = "STRING"
+ starterkit_add_random_name_suffix = {
+ display_name = "Starterkit Random Name Suffix"
+ description = "Append a random suffix to the projects the starterkit creates. Keep it as the adopted starterkit had it: an upgrade that changes it renames, and so replaces, every project and tenant it created. Cannot be changed after the order."
+ type = "BOOLEAN"
assignment_type = "USER_INPUT"
- is_optional = true
+ default_value = jsonencode(true)
+ condition = "!input.playground_mode"
+ display_order = 30
}
- tags = {
- display_name = "Tags"
- description = "Tag maps shared by every stage: `landingzone`, `building_block` (platform definitions), `starterkit_building_block` (app team definitions), `project`, `starterkit_project` (the starter kit's projects), `project_owner_tag_key` (e.g. `projectOwner`)."
- type = "CODE"
+ starterkit_demo = {
+ display_name = "Starter Kit Demo"
+ description = "Demo application the starter kit deploys into every application team's namespaces."
+ type = "JSON"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
- default_value = jsonencode(jsonencode({
- landingzone = {}
- building_block = {}
- starterkit_building_block = {}
- project = {}
- starterkit_project = {}
- project_owner_tag_key = ""
- }))
+ default_value = jsonencode(jsonencode(local.starterkit_demo_default))
+ display_order = 35
+
+ json_schema = jsonencode({
+ type = "object"
+ additionalProperties = false
+ required = ["name", "template_repo"]
+ properties = {
+ name = {
+ type = "string"
+ title = "Image Name"
+ default = local.starterkit_demo_default.name
+ description = "Image name every application ordered from the starter kit builds under, set as APP_NAME."
+ pattern = "^[a-z0-9]+([._-][a-z0-9]+)*$"
+ }
+ template_repo = {
+ type = "string"
+ title = "Template Repository"
+ default = local.starterkit_demo_default.template_repo
+ description = "Git URL the starter kit initialises every application repository from."
+ pattern = "^https?://[^\\s]+$"
+ }
+ ai_model = {
+ type = "string"
+ title = "AI Model"
+ default = local.starterkit_demo_default.ai_model
+ description = "Model applications default to; must be one STACKIT Model Serving's `/v1/models` endpoint serves. Leave it out to set up no Model Serving token and no `stackit-ai` secret in the namespaces."
+ }
+ }
+ })
+ }
+
+ dns_domain = {
+ display_name = "DNS Domain"
+ description = "DNS zone of the platform, under one of the free domains STACKIT delegates to its customers. A bare free domain such as `stackit.run` gives `.stackit.run`; one label more, such as `likvid.stackit.run`, is the zone itself."
+ type = "STRING"
+ assignment_type = "USER_INPUT"
+ default_value = jsonencode("stackit.run")
+ updateable_by_consumer = true
+ value_validation_regex = "^([a-z0-9]([a-z0-9-]{0,18}[a-z0-9])?\\.)?(stackit\\.(run|zone|rocks|gg)|runs\\.onstackit\\.cloud)$"
+ validation_regex_error_message = "DNS domain must be stackit.run, stackit.zone, stackit.rocks, stackit.gg or runs.onstackit.cloud, optionally with one label of at most 20 lowercase letters, digits or dashes in front, such as likvid.stackit.run."
+ condition = "!input.playground_mode"
+ display_order = 50
}
stages = {
display_name = "Stages"
- description = "Stages, one landing zone per key, each with its own tags: `landingzone` for its landing zone, `project` for the starter kit's meshProjects (a tag a policy pairs goes on both), and optional landing zone `quotas` (`[{key, value}]`)."
- type = "CODE"
+ description = "One landing zone per stage with its own tags: `landingzone` for the landing zone, `project` for the starter kit's meshProjects (a tag a policy pairs goes on both), and optional `quotas`, each `key` a `quota_key` from Quota Definitions."
+ type = "JSON"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
- default_value = jsonencode(jsonencode({
- dev = { landingzone = {}, project = {} }
- prod = { landingzone = {}, project = {} }
- }))
+ default_value = jsonencode(jsonencode(local.stages_default))
+ display_order = 80
+
+ json_schema = jsonencode({
+ "$schema" = "http://json-schema.org/draft-07/schema#"
+ type = "array"
+ minItems = 1
+ default = local.stages_default
+ items = {
+ type = "object"
+ additionalProperties = false
+ required = ["name", "landingzone", "project"]
+ properties = {
+ name = { type = "string", title = "name", pattern = "^[a-z0-9-]+$" }
+ landingzone = merge(local.tag_list_schema, { title = "landingzone" })
+ project = merge(local.tag_list_schema, { title = "project" })
+ quotas = {
+ type = "array"
+ title = "quotas"
+ items = {
+ type = "object"
+ additionalProperties = false
+ required = ["key", "value"]
+ properties = {
+ key = { type = "string", title = "key" }
+ value = { type = "number", title = "value" }
+ }
+ }
+ }
+ }
+ }
+ })
}
- platform_identifier = {
- display_name = "Platform Identifier"
- description = "Identifier of the Kubernetes platform created in meshStack (letters, digits and dashes only). In playground mode a random suffix is appended to it."
- type = "STRING"
- assignment_type = "USER_INPUT"
- value_validation_regex = "^[a-zA-Z0-9-]+$"
- validation_regex_error_message = "platform_identifier must only contain letters, digits, and dashes."
- }
-
- use_global_location = {
- display_name = "Use Global Location"
- description = "If true, use the existing global meshStack location instead of creating a dedicated location for this platform."
- type = "BOOLEAN"
- assignment_type = "USER_INPUT"
- default_value = jsonencode(false)
+ quota_definitions = {
+ display_name = "Quota Definitions"
+ description = "Quotas the meshPlatform offers, with their limits; a stage's `quotas` entries name one by its `quota_key`. CPU in millicores (m), memory in mebibytes (Mi). Leave out to keep the defaults."
+ type = "JSON"
+ assignment_type = "USER_INPUT"
+ is_optional = true
+ updateable_by_consumer = true
+ display_order = 85
+
+ json_schema = jsonencode({
+ "$schema" = "http://json-schema.org/draft-07/schema#"
+ type = "array"
+ items = {
+ type = "object"
+ additionalProperties = false
+ required = ["quota_key", "label", "description", "unit", "min_value", "max_value", "auto_approval_threshold"]
+ properties = {
+ quota_key = { type = "string", title = "quota_key" }
+ label = { type = "string", title = "label" }
+ description = { type = "string", title = "description" }
+ unit = { type = "string", title = "unit" }
+ min_value = { type = "number", title = "min_value" }
+ max_value = { type = "number", title = "max_value" }
+ auto_approval_threshold = { type = "number", title = "auto_approval_threshold" }
+ }
+ }
+ })
}
- starterkit_app_name = {
- display_name = "Starter Kit Image Name"
- description = "Image name every application ordered from the starter kit builds under, set on its repository as APP_NAME."
- type = "STRING"
+ tags = {
+ display_name = "Tags"
+ description = "Tags as {key, values} entries for landing zones, platform and app team building blocks, the platform project and the starterkit projects, plus the project owner tag key."
+ type = "JSON"
assignment_type = "USER_INPUT"
- default_value = jsonencode("ai-summarizer")
updateable_by_consumer = true
+ default_value = jsonencode(jsonencode(local.tags_default))
+ display_order = 90
+
+ json_schema = jsonencode({
+ "$schema" = "http://json-schema.org/draft-07/schema#"
+ type = "object"
+ required = ["landingzone", "building_block", "starterkit_building_block", "project", "starterkit_project"]
+ properties = {
+ landingzone = merge(local.tag_list_schema, { title = "Landing Zone Tags", default = local.tags_default.landingzone })
+ building_block = merge(local.tag_list_schema, { title = "Platform Building Block Tags", default = local.tags_default.building_block })
+ starterkit_building_block = merge(local.tag_list_schema, { title = "App Team Building Block Tags", default = local.tags_default.starterkit_building_block })
+ project = merge(local.tag_list_schema, { title = "Platform Project Tags", default = local.tags_default.project })
+ starterkit_project = merge(local.tag_list_schema, { title = "Starterkit Project Tags", default = local.tags_default.starterkit_project })
+ project_owner_tag_key = {
+ type = "string"
+ title = "Project Owner Tag Key"
+ description = "Tag on the platform project and the starterkit projects that gets the creator's display name. Leave out to set none."
+ }
+ }
+ })
}
- starterkit_repo_clone_addr = {
- display_name = "Starter Kit Template Repository"
- description = "Git URL the starter kit initialises every application repository from."
- type = "STRING"
+ overrides = {
+ display_name = "Overrides"
+ description = "Names and settings that replace the generated ones. Leave a field out to keep its default."
+ type = "JSON"
assignment_type = "USER_INPUT"
- default_value = jsonencode("https://github.com/likvid-bank/starterkit-template-stackit-ai-summarizer.git")
+ is_optional = true
updateable_by_consumer = true
+ display_order = 95
+
+ json_schema = jsonencode({
+ "$schema" = "http://json-schema.org/draft-07/schema#"
+ type = "object"
+ additionalProperties = false
+ properties = {
+ cluster_name = {
+ type = "string"
+ title = "Cluster Name"
+ description = "SKE cluster name, 2-11 lowercase letters, digits or dashes. Left out, one is generated from the platform identifier."
+ pattern = "^[a-z0-9][a-z0-9-]{0,9}[a-z0-9]$"
+ }
+ cluster_issuer_email = {
+ type = "string"
+ title = "ClusterIssuer Email"
+ description = "Let's Encrypt contact email of the ACME ClusterIssuer. Left out, the platform's service account is used."
+ pattern = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"
+ }
+ project_identifier = {
+ type = "string"
+ title = "Project Identifier"
+ description = "Identifier of the hosting meshProject. Left out, `-ske` is used."
+ pattern = "^([a-z0-9]+-)*[a-z0-9]+$"
+ }
+ }
+ })
}
approval_policies = {
- display_name = "Approval Policies"
- description = "HCL object of approval gates on the platform definitions this registers."
- type = "CODE"
- assignment_type = "STATIC"
- argument = jsonencode(jsonencode(var.approval_policies))
+ display_name = "Approval Policies"
+ description = "Run triggers that need an operator's approval before a run of a platform definition this architecture registers is applied. Adopting a platform needs them."
+ type = "JSON"
+ assignment_type = "USER_INPUT"
+ updateable_by_consumer = true
+ default_value = jsonencode(jsonencode(local.approval_policies_default))
+ condition = "!input.playground_mode"
+ display_order = 100
+ json_schema = jsonencode(local.approval_policies_schema)
}
starterkit_approval_policies = {
- display_name = "Starterkit Approval Policies"
- description = "HCL object of approval gates on the application team definitions this registers."
- type = "CODE"
- assignment_type = "STATIC"
- argument = jsonencode(jsonencode(var.starterkit_approval_policies))
- }
-
- starterkit_add_random_name_suffix = {
- display_name = "Starterkit Random Name Suffix"
- description = "Whether the starterkit appends a random suffix to the projects it creates."
- type = "BOOLEAN"
- assignment_type = "STATIC"
- argument = jsonencode(var.starterkit_add_random_name_suffix)
- }
-
- platform_engineer_role_mapping = {
- display_name = "Platform Engineer Role Mapping"
- description = "HCL object of role mappings by service (`git`, `container_registry`, `secrets_manager`) for the members of the platform's project."
- type = "CODE"
- assignment_type = "STATIC"
- argument = jsonencode(jsonencode(var.platform_engineer_role_mapping))
+ display_name = "Starterkit Approval Policies"
+ description = "Run triggers that need an operator's approval before a run of an application team building block (Git repository, Forgejo connector, starterkit) is applied."
+ type = "JSON"
+ assignment_type = "USER_INPUT"
+ updateable_by_consumer = true
+ default_value = jsonencode(jsonencode(local.approval_policies_default))
+ condition = "!input.playground_mode"
+ display_order = 110
+ json_schema = jsonencode(local.approval_policies_schema)
}
imports = {
display_name = "Imports"
- description = "HCL object of what to take over, by key: `project`, `ske`, `git`, `dns`, `ingress`, `secrets_manager`, `container_registry`, `kubernetes_platform`, `starterkit_definitions`; see Adopting Existing Resources in the README."
- type = "CODE"
+ description = "Existing resources and definitions to take over when adopting a platform built before this architecture; see Adopting Existing Resources in the README. Leave out to create everything."
+ type = "JSON"
assignment_type = "USER_INPUT"
is_optional = true
+ condition = "!input.playground_mode"
+ display_order = 210
+
+ json_schema = jsonencode({
+ "$schema" = "http://json-schema.org/draft-07/schema#"
+ type = "object"
+ additionalProperties = false
+ properties = {
+ project = {
+ type = "object"
+ title = "project"
+ description = "Adopt the hosting project. Every other import except starterkit_definitions needs it."
+ additionalProperties = false
+ properties = {
+ display_name = { type = "string", title = "display_name" }
+ admin_bindings = { type = "object", title = "admin_bindings", description = "Existing Project Admin binding names by username.", additionalProperties = { type = "string" } }
+ }
+ }
+ adopt_ske = {
+ type = "boolean"
+ title = "adopt_ske"
+ description = "Adopt the existing SKE cluster named by `overrides.cluster_name`."
+ }
+ git = {
+ type = "object"
+ title = "git"
+ additionalProperties = false
+ required = ["instance_id", "instance_name", "forgejo_organization"]
+ properties = {
+ instance_id = { type = "string", title = "instance_id" }
+ instance_name = { type = "string", title = "instance_name" }
+ forgejo_organization = { type = "string", title = "forgejo_organization" }
+ }
+ }
+ dns = {
+ type = "object"
+ title = "dns"
+ additionalProperties = false
+ required = ["zone_id", "record_set_id"]
+ properties = {
+ zone_id = { type = "string", title = "zone_id" }
+ record_set_id = { type = "string", title = "record_set_id" }
+ }
+ }
+ adopt_ingress = {
+ type = "boolean"
+ title = "adopt_ingress"
+ description = "Adopt haproxy, cert-manager and the ClusterIssuer. Needs adopt_ske."
+ }
+ secrets_manager = {
+ type = "object"
+ title = "secrets_manager"
+ additionalProperties = false
+ required = ["instance_id"]
+ properties = {
+ instance_id = { type = "string", title = "instance_id" }
+ }
+ }
+ container_registry = {
+ type = "object"
+ title = "container_registry"
+ additionalProperties = false
+ required = ["artifactory_id", "registry_name"]
+ properties = {
+ artifactory_id = { type = "string", title = "artifactory_id" }
+ registry_name = { type = "string", title = "registry_name" }
+ }
+ }
+ kubernetes_platform = {
+ type = "object"
+ title = "kubernetes_platform"
+ description = "Adopt the meshPlatform and its landing zones by name. Needs adopt_ske."
+ additionalProperties = false
+ properties = {
+ platform_name = { type = "string", title = "platform_name" }
+ location_name = { type = "string", title = "location_name" }
+ landingzone_names = { type = "object", title = "landingzone_names", description = "Existing landing zone names by stage.", additionalProperties = { type = "string" } }
+ }
+ }
+ starterkit_definitions = {
+ type = "object"
+ title = "starterkit_definitions"
+ description = "Adopt the app team definitions by uuid."
+ additionalProperties = false
+ properties = {
+ starterkit = { type = "string", title = "starterkit", pattern = local.uuid_pattern }
+ git_repository = { type = "string", title = "git_repository", pattern = local.uuid_pattern }
+ forgejo_connector = { type = "string", title = "forgejo_connector", pattern = local.uuid_pattern }
+ }
+ }
+ }
+ })
}
+ # A sensitive input cannot be JSON.
import_secrets = {
display_name = "Import Secrets"
- description = "HCL object `{git = {forgejo_api_token}}`: the token of an owner of the Forgejo organization that `imports.git` adopts. Leave empty when nothing is adopted or once the organization is adopted."
+ description = "HCL object `{git = {forgejo_api_token}}`: the token of an owner of the Forgejo organization that `imports.git` adopts. Set it to `{}` once the organization is adopted."
type = "CODE"
assignment_type = "USER_INPUT"
sensitive = {}
is_optional = true
updateable_by_consumer = true
+ condition = "!input.playground_mode && input.imports.git.instance_id != \"\""
+ display_order = 220
}
- playground_mode = {
- display_name = "Playground Mode"
- description = "Throwaway deployment: the identifier gets a random suffix and nothing is protected against deletion. Do not publish such a platform or its definitions to other workspaces. Set false for real use."
- type = "BOOLEAN"
+ workspace = {
+ display_name = "Workspace Identifier"
+ description = "Workspace that will own the created platform, location, landing zones and hosting project."
+ type = "STRING"
+ assignment_type = "WORKSPACE_IDENTIFIER"
+ display_order = 900
+ }
+
+ creator = {
+ display_name = "Creator"
+ description = "Creator of the platform, injected by meshStack."
+ type = "CODE"
+ assignment_type = "AUTHOR"
+ display_order = 910
+ }
+
+ workspace_members = {
+ display_name = "Workspace Members"
+ description = "Members of the owning workspace. Owners and managers become Project Admin of the platform's project."
+ type = "CODE"
+ assignment_type = "USER_PERMISSIONS"
+ display_order = 920
+ }
+
+ hub = {
+ display_name = "Hub"
+ description = "HCL object with `git_ref` (meshstack-hub reference to source nested modules from) and `bbd_draft` (draft state of the application team definitions)."
+ type = "CODE"
assignment_type = "STATIC"
- argument = jsonencode(var.playground_mode)
+ argument = jsonencode(jsonencode(var.hub))
+ display_order = 1000
}
}
diff --git a/reference-architectures/stackit-kubernetes/platform/buildingblock/README.md b/reference-architectures/stackit-kubernetes/platform/buildingblock/README.md
index e1e79c46..3a8933e6 100644
--- a/reference-architectures/stackit-kubernetes/platform/buildingblock/README.md
+++ b/reference-architectures/stackit-kubernetes/platform/buildingblock/README.md
@@ -8,12 +8,12 @@ federation.
It creates the platform's Secrets Manager instance with a writer and a reader user, an SKE cluster
with a platform admin service account, in-cluster ingress (cert-manager, HAProxy and a Let's Encrypt
ClusterIssuer), the meshStack replicator and metering identities, a DNS zone, a STACKIT Model
-Serving token, a STACKIT Git instance, a container registry, and the meshStack SKE platform with one
-landing zone per stage.
+Serving token when `ai_model` is set, a STACKIT Git instance, a container registry, and the
+meshStack SKE platform with one landing zone per stage.
-With a key of `imports` set (`ske`, `git`, `dns`, `ingress`, `secrets_manager`, `container_registry`
-or `kubernetes_platform`), the building block of that name takes over what already exists instead
-of creating it. In playground mode it gets `release_on_destroy`, so deleting it leaves what it took
+With a key of `imports` set (`adopt_ske`, `git`, `dns`, `adopt_ingress`, `secrets_manager`,
+`container_registry` or `kubernetes_platform`), the building block of that name takes over what
+already exists instead of creating it. In playground mode it gets `release_on_destroy`, so deleting it leaves what it took
over in place. The Git building block also takes over the Forgejo organization with the token in
`import_secrets.git`, which this run writes to `/imports/stackit/git` in the
Secrets Manager with the writer user before it orders the Git building block. Once the Git building
diff --git a/reference-architectures/stackit-kubernetes/platform/buildingblock/SUMMARY.md.tftpl b/reference-architectures/stackit-kubernetes/platform/buildingblock/SUMMARY.md.tftpl
index bdbc53d9..bd69eb97 100644
--- a/reference-architectures/stackit-kubernetes/platform/buildingblock/SUMMARY.md.tftpl
+++ b/reference-architectures/stackit-kubernetes/platform/buildingblock/SUMMARY.md.tftpl
@@ -20,7 +20,9 @@
| **Forgejo Organization** | [`${forgejo_organization}`](${forgejo_instance_url}/${forgejo_organization}) |
| **Container Registry** | [${registry_host}/${registry_name}](${registry_url}) — @buildingblock[${container_registry_bb_uuid}] |
| **DNS Zone** | `${dns_zone_name}` — @buildingblock[${dns_bb_uuid}] |
+%{ if ai_model != null ~}
| **AI Model Serving** | `${ai_model}` — @buildingblock[${ai_bb_uuid}] |
+%{ endif ~}
| **meshStack Platform** | `${platform_identifier}` |
%{ if !phase2_completed ~}
diff --git a/reference-architectures/stackit-kubernetes/platform/buildingblock/cluster.tf b/reference-architectures/stackit-kubernetes/platform/buildingblock/cluster.tf
index bb4ee2cb..d0ce23b0 100644
--- a/reference-architectures/stackit-kubernetes/platform/buildingblock/cluster.tf
+++ b/reference-architectures/stackit-kubernetes/platform/buildingblock/cluster.tf
@@ -40,11 +40,9 @@ resource "meshstack_building_block" "cluster" {
}
}
admin_token_generation = { value = jsonencode(local.token_generation) }
- release_on_destroy = { value = jsonencode(var.imports.ske != null && var.playground_mode) }
- },
- var.imports.ske == null ? {} : {
- imports = { value = jsonencode(jsonencode(var.imports.ske)) }
+ release_on_destroy = { value = jsonencode(local.imports.adopt_ske && var.playground_mode) }
},
+ local.imports.adopt_ske ? { imports = { value = jsonencode(jsonencode({})) } } : {},
)
}
}
@@ -147,9 +145,9 @@ resource "meshstack_building_block" "kubernetes_platform" {
secret_version = nonsensitive(sha256(local.output_to_vault))
}
}
- release_on_destroy = { value = jsonencode(var.imports.kubernetes_platform != null && var.playground_mode) }
+ release_on_destroy = { value = jsonencode(local.imports.kubernetes_platform != null && var.playground_mode) }
},
- var.imports.kubernetes_platform == null ? {} : {
+ local.imports.kubernetes_platform == null ? {} : {
# The meshPlatform and its landing zones are this run's own; the child adopts what is in the cluster.
imports = { value = jsonencode(jsonencode({})) }
},
@@ -200,9 +198,9 @@ resource "meshstack_building_block" "ingress" {
inputs = merge(
{
"kubeconfig.yaml" = local.cluster_kubeconfig_input
- release_on_destroy = { value = jsonencode(var.imports.ingress != null && var.playground_mode) }
+ release_on_destroy = { value = jsonencode(local.imports.adopt_ingress && var.playground_mode) }
},
- var.imports.ingress == null ? {} : { imports = { value = jsonencode(jsonencode(var.imports.ingress)) } },
+ local.imports.adopt_ingress ? { imports = { value = jsonencode(jsonencode({})) } } : {},
)
}
}
diff --git a/reference-architectures/stackit-kubernetes/platform/buildingblock/imports.tf b/reference-architectures/stackit-kubernetes/platform/buildingblock/imports.tf
index 5a0daed4..834a18b5 100644
--- a/reference-architectures/stackit-kubernetes/platform/buildingblock/imports.tf
+++ b/reference-architectures/stackit-kubernetes/platform/buildingblock/imports.tf
@@ -57,8 +57,8 @@ import {
locals {
# The definitions are registered only in phase 2.
- adopted_starterkit_definitions = var.phase2_completed && var.imports.starterkit_definitions != null ? {
- for key, uuid in var.imports.starterkit_definitions : key => uuid if uuid != null
+ adopted_starterkit_definitions = var.phase2_completed && local.imports.starterkit_definitions != null ? {
+ for key, uuid in local.imports.starterkit_definitions : key => uuid if uuid != null
} : {}
starterkit_definition_target_types = {
diff --git a/reference-architectures/stackit-kubernetes/platform/buildingblock/main.tf b/reference-architectures/stackit-kubernetes/platform/buildingblock/main.tf
index 73d51366..54659026 100644
--- a/reference-architectures/stackit-kubernetes/platform/buildingblock/main.tf
+++ b/reference-architectures/stackit-kubernetes/platform/buildingblock/main.tf
@@ -6,7 +6,18 @@ locals {
service_account_email = var.automation_identity.service_account_email
- kubernetes_platform_imports = var.imports.kubernetes_platform != null ? var.imports.kubernetes_platform : {
+ imports = var.imports != null ? var.imports : {
+ adopt_ske = false
+ git = null
+ dns = null
+ adopt_ingress = false
+ secrets_manager = null
+ container_registry = null
+ kubernetes_platform = null
+ starterkit_definitions = null
+ }
+
+ kubernetes_platform_imports = local.imports.kubernetes_platform != null ? local.imports.kubernetes_platform : {
platform_name = null
location_name = null
landingzone_names = {}
@@ -95,14 +106,18 @@ resource "meshstack_building_block" "platform_federation" {
inputs = {
service_account_email = { value = jsonencode(local.service_account_email) }
federated_building_block_definitions = {
- value = jsonencode(jsonencode(concat([
- module.cluster_integration.building_block_definition.uuid,
- module.git_integration.building_block_definition.uuid,
- module.container_registry_integration.building_block_definition.uuid,
- module.ai_llm_integration.building_block_definition.uuid,
- module.secrets_manager_integration.building_block_definition.uuid,
- module.dns_integration.building_block_definition.uuid,
- ])))
+ value = jsonencode(jsonencode(concat(
+ [
+ module.cluster_integration.building_block_definition.uuid,
+ module.git_integration.building_block_definition.uuid,
+ module.container_registry_integration.building_block_definition.uuid,
+ ],
+ var.ai_model == null ? [] : [module.ai_llm_integration.building_block_definition.uuid],
+ [
+ module.secrets_manager_integration.building_block_definition.uuid,
+ module.dns_integration.building_block_definition.uuid,
+ ],
+ )))
}
}
}
diff --git a/reference-architectures/stackit-kubernetes/platform/buildingblock/outputs.tf b/reference-architectures/stackit-kubernetes/platform/buildingblock/outputs.tf
index 3c445d08..c041bb13 100644
--- a/reference-architectures/stackit-kubernetes/platform/buildingblock/outputs.tf
+++ b/reference-architectures/stackit-kubernetes/platform/buildingblock/outputs.tf
@@ -25,7 +25,7 @@ output "summary" {
stage_names = join(", ", [for stage in sort(keys(var.stages)) : "**${stage}**"])
dns_zone_name = local.dns_zone_name
dns_bb_uuid = meshstack_building_block.dns.metadata.uuid
- ai_bb_uuid = meshstack_building_block.ai_llm.metadata.uuid
+ ai_bb_uuid = var.ai_model == null ? null : meshstack_building_block.ai_llm.metadata.uuid
ai_model = var.ai_model
phase2_completed = var.phase2_completed
platform_service_account_email = local.service_account_email
diff --git a/reference-architectures/stackit-kubernetes/platform/buildingblock/secrets_manager.tf b/reference-architectures/stackit-kubernetes/platform/buildingblock/secrets_manager.tf
index 6ceb5c69..3892235d 100644
--- a/reference-architectures/stackit-kubernetes/platform/buildingblock/secrets_manager.tf
+++ b/reference-architectures/stackit-kubernetes/platform/buildingblock/secrets_manager.tf
@@ -1,7 +1,6 @@
module "secrets_manager_integration" {
source = "github.com/meshcloud/meshstack-hub//modules/stackit/secrets-manager?ref=${var.hub.git_ref}"
- role_mapping = var.platform_engineer_role_mapping.secrets_manager
approval_policies = var.approval_policies
meshstack = { owning_workspace_identifier = var.workspace, tags = var.tags.building_block }
@@ -28,10 +27,10 @@ resource "meshstack_building_block" "secrets_manager" {
{
instance_name = { value = jsonencode(var.platform_identifier) }
STACKIT_SERVICE_ACCOUNT_EMAIL = { value = jsonencode(local.service_account_email) }
- release_on_destroy = { value = jsonencode(var.imports.secrets_manager != null && var.playground_mode) }
+ release_on_destroy = { value = jsonencode(local.imports.secrets_manager != null && var.playground_mode) }
},
- var.imports.secrets_manager == null ? {} : {
- imports = { value = jsonencode(jsonencode(var.imports.secrets_manager)) }
+ local.imports.secrets_manager == null ? {} : {
+ imports = { value = jsonencode(jsonencode(local.imports.secrets_manager)) }
},
)
}
@@ -63,8 +62,9 @@ resource "stackit_secretsmanager_user" "reader" {
}
locals {
- import_git = var.imports.git != null && nonsensitive(var.import_secrets.git != null)
- import_git_data_json = var.import_secrets.git == null ? null : jsonencode({ "forgejo-api-token" = var.import_secrets.git.forgejo_api_token })
+ import_secrets_git = var.import_secrets == null ? null : var.import_secrets.git
+ import_git = local.imports.git != null && nonsensitive(local.import_secrets_git != null)
+ import_git_data_json = local.import_secrets_git == null ? null : jsonencode({ "forgejo-api-token" = local.import_secrets_git.forgejo_api_token })
}
resource "vault_kv_secret_v2" "import_git" {
diff --git a/reference-architectures/stackit-kubernetes/platform/buildingblock/services.tf b/reference-architectures/stackit-kubernetes/platform/buildingblock/services.tf
index 64c2bdde..af93aaa7 100644
--- a/reference-architectures/stackit-kubernetes/platform/buildingblock/services.tf
+++ b/reference-architectures/stackit-kubernetes/platform/buildingblock/services.tf
@@ -31,16 +31,20 @@ resource "meshstack_building_block" "dns" {
wildcard_target_ip = { value = jsonencode(local.ingress_load_balancer_ip) }
contact_email = { value = jsonencode(var.cluster_issuer_email) }
STACKIT_SERVICE_ACCOUNT_EMAIL = { value = jsonencode(local.service_account_email) }
- release_on_destroy = { value = jsonencode(var.imports.dns != null && var.playground_mode) }
+ release_on_destroy = { value = jsonencode(local.imports.dns != null && var.playground_mode) }
},
- var.imports.dns == null ? {} : {
- imports = { value = jsonencode(jsonencode(var.imports.dns)) }
+ local.imports.dns == null ? {} : {
+ imports = { value = jsonencode(jsonencode(local.imports.dns)) }
},
)
}
}
module "ai_llm_integration" {
+ lifecycle {
+ enabled = var.ai_model != null
+ }
+
source = "github.com/meshcloud/meshstack-hub//modules/stackit/ai-llm?ref=${var.hub.git_ref}"
model = var.ai_model
@@ -55,6 +59,8 @@ resource "meshstack_building_block" "ai_llm" {
wait_for_completion = true
lifecycle {
+ enabled = var.ai_model != null
+
postcondition {
condition = self.status.succeeded
error_message = "Building block ${self.metadata.uuid} did not succeed (health status ${self.status.health_status}, action ${self.status.action}). See its run in meshPanel."
@@ -83,7 +89,6 @@ resource "meshstack_building_block" "ai_llm" {
module "git_integration" {
source = "github.com/meshcloud/meshstack-hub//modules/stackit/git?ref=${var.hub.git_ref}"
- role_mapping = var.platform_engineer_role_mapping.git
approval_policies = var.approval_policies
meshstack = { owning_workspace_identifier = var.workspace, tags = var.tags.building_block }
@@ -111,8 +116,8 @@ resource "meshstack_building_block" "git" {
inputs = merge(
{
- instance_name = { value = jsonencode(var.imports.git != null ? var.imports.git.instance_name : var.platform_identifier) }
- forgejo_organization = { value = jsonencode(var.imports.git != null ? var.imports.git.forgejo_organization : var.platform_identifier) }
+ instance_name = { value = jsonencode(local.imports.git != null ? local.imports.git.instance_name : var.platform_identifier) }
+ forgejo_organization = { value = jsonencode(local.imports.git != null ? local.imports.git.forgejo_organization : var.platform_identifier) }
shared_runner_labels = { value = jsonencode(jsonencode(["stackit-ubuntu-22"])) }
STACKIT_SERVICE_ACCOUNT_EMAIL = { value = jsonencode(local.service_account_email) }
output_to_vault = {
@@ -121,11 +126,11 @@ resource "meshstack_building_block" "git" {
secret_version = nonsensitive(sha256(local.output_to_vault))
}
}
- release_on_destroy = { value = jsonencode(var.imports.git != null && var.playground_mode) }
+ release_on_destroy = { value = jsonencode(local.imports.git != null && var.playground_mode) }
},
- var.imports.git == null ? {} : {
+ local.imports.git == null ? {} : {
imports = { value = jsonencode(jsonencode(merge(
- { instance_id = var.imports.git.instance_id },
+ { instance_id = local.imports.git.instance_id },
local.import_git ? { existing_forgejo_api_token_path = vault_kv_secret_v2.import_git.name } : {},
))) }
},
@@ -136,7 +141,6 @@ resource "meshstack_building_block" "git" {
module "container_registry_integration" {
source = "github.com/meshcloud/meshstack-hub//modules/stackit/container-registry?ref=${var.hub.git_ref}"
- role_mapping = var.platform_engineer_role_mapping.container_registry
approval_policies = var.approval_policies
meshstack = { owning_workspace_identifier = var.workspace, tags = var.tags.building_block }
@@ -162,7 +166,7 @@ resource "meshstack_building_block" "container_registry" {
inputs = merge(
{
# A Harbor project name is lowercase only, while the platform identifier also allows capitals.
- registry_name = { value = jsonencode(var.imports.container_registry != null ? var.imports.container_registry.registry_name : lower(var.platform_identifier)) }
+ registry_name = { value = jsonencode(local.imports.container_registry != null ? local.imports.container_registry.registry_name : lower(var.platform_identifier)) }
STACKIT_SERVICE_ACCOUNT_EMAIL = { value = jsonencode(local.service_account_email) }
require_robot_link = { value = jsonencode(var.phase2_completed) }
@@ -173,10 +177,10 @@ resource "meshstack_building_block" "container_registry" {
secret_version = nonsensitive(sha256(local.output_to_vault))
}
}
- release_on_destroy = { value = jsonencode(var.imports.container_registry != null && var.playground_mode) }
+ release_on_destroy = { value = jsonencode(local.imports.container_registry != null && var.playground_mode) }
},
- var.imports.container_registry == null ? {} : {
- imports = { value = jsonencode(jsonencode({ artifactory_id = var.imports.container_registry.artifactory_id })) }
+ local.imports.container_registry == null ? {} : {
+ imports = { value = jsonencode(jsonencode({ artifactory_id = local.imports.container_registry.artifactory_id })) }
},
)
}
diff --git a/reference-architectures/stackit-kubernetes/platform/buildingblock/starterkit.tf b/reference-architectures/stackit-kubernetes/platform/buildingblock/starterkit.tf
index 50ba5134..48f34300 100644
--- a/reference-architectures/stackit-kubernetes/platform/buildingblock/starterkit.tf
+++ b/reference-architectures/stackit-kubernetes/platform/buildingblock/starterkit.tf
@@ -7,19 +7,20 @@ locals {
forgejo_instance_id = jsondecode(meshstack_building_block.git.status.outputs["instance_id"].value)
forgejo_organization = jsondecode(meshstack_building_block.git.status.outputs["forgejo_organization"].value)
- dns_zone_name = jsondecode(meshstack_building_block.dns.status.outputs["zone_name"].value)
+ # Built like the DNS block builds it, so the starterkit does not wait for the DNS block.
+ dns_zone_name = "${var.dns_subdomain}.${var.dns_parent_domain}"
git_vault_secret = jsondecode(jsondecode(meshstack_building_block.git.status.outputs["vault_secret"].value))
- ai_llm_vault_secret = jsondecode(jsondecode(meshstack_building_block.ai_llm.status.outputs["vault_secret"].value))
+ ai_llm_vault_secret = var.ai_model == null ? null : jsondecode(jsondecode(meshstack_building_block.ai_llm.status.outputs["vault_secret"].value))
container_registry_vault_secret = jsondecode(jsondecode(meshstack_building_block.container_registry.status.outputs["vault_secret"].value))
# The registry reports its secret only once the robots exist, and a disabled module still evaluates
# its arguments.
container_registry_secret_path = var.phase2_completed ? local.container_registry_vault_secret.path : ""
- connector_secrets_revision = var.phase2_completed ? parseint(substr(sha256(jsonencode([
- local.container_registry_vault_secret.secret_hash,
- local.ai_llm_vault_secret.secret_hash,
- ])), 0, 12), 16) : 1
+ connector_secrets_revision = var.phase2_completed ? parseint(substr(sha256(jsonencode(concat(
+ [local.container_registry_vault_secret.secret_hash],
+ var.ai_model == null ? [] : [local.ai_llm_vault_secret.secret_hash],
+ ))), 0, 12), 16) : 1
}
module "git_repository_integration" {
@@ -99,7 +100,7 @@ module "forgejo_connector_integration" {
registry_secret_path = local.container_registry_secret_path
# The starter kit's demo application reads its inference configuration from `stackit-ai`.
- additional_kubernetes_secrets = {
+ additional_kubernetes_secrets = var.ai_model == null ? {} : {
"stackit-ai" = local.ai_llm_vault_secret.path
}
secrets_revision = local.connector_secrets_revision
@@ -136,7 +137,7 @@ module "ske_starterkit_integration" {
project_tags = {
stages = local.stage_project_tags
- owner_tag_key = var.tags.project_owner_tag_key == "" ? null : var.tags.project_owner_tag_key
+ owner_tag_key = var.tags.project_owner_tag_key
}
approval_policies = var.starterkit_approval_policies
diff --git a/reference-architectures/stackit-kubernetes/platform/buildingblock/variables.tf b/reference-architectures/stackit-kubernetes/platform/buildingblock/variables.tf
index f9a19bfd..fb988b69 100644
--- a/reference-architectures/stackit-kubernetes/platform/buildingblock/variables.tf
+++ b/reference-architectures/stackit-kubernetes/platform/buildingblock/variables.tf
@@ -1,6 +1,6 @@
variable "automation_identity" {
type = object({
- building_block_ref = object({ kind = string, uuid = string })
+ building_block_ref = object({ uuid = string })
service_account_email = string
service_account_id = string
})
@@ -14,24 +14,6 @@ variable "service_account_federation_bbd_version_ref" {
description = "STACKIT Service Account Federation definition version the landing zone offers."
}
-variable "stackit_project_id" {
- type = string
- nullable = false
- description = "STACKIT project the platform runs in."
-}
-
-variable "tenant_uuid" {
- type = string
- nullable = false
- description = "meshStack tenant of the STACKIT project, which every building block of this run is ordered for."
-}
-
-variable "workspace" {
- type = string
- nullable = false
- description = "meshStack workspace that owns the platform and the definitions this run registers."
-}
-
variable "platform_identifier" {
type = string
nullable = false
@@ -44,95 +26,89 @@ variable "playground_mode" {
description = "Whether the ordering STACKIT Kubernetes Platform is a throwaway deployment."
}
-variable "imports" {
- type = object({
- ske = optional(object({}))
- git = optional(object({
- instance_id = string
- instance_name = string
- forgejo_organization = string
- }))
- dns = optional(object({
- zone_id = string
- record_set_id = string
- }))
- ingress = optional(object({}))
- secrets_manager = optional(object({
- instance_id = string
- }))
- container_registry = optional(object({
- artifactory_id = string
- registry_name = string
- }))
- kubernetes_platform = optional(object({
- platform_name = optional(string)
- location_name = optional(string)
- landingzone_names = optional(map(string), {})
- }))
- starterkit_definitions = optional(object({
- starterkit = optional(string)
- git_repository = optional(string)
- forgejo_connector = optional(string)
- }))
- })
+variable "dns_parent_domain" {
+ type = string
nullable = false
- default = {}
- description = "Existing resources the child building blocks of the same key take over, as in the STACKIT Kubernetes Platform's `imports`."
+ description = "Domain the platform's DNS zone is created under."
}
-variable "import_secrets" {
- type = object({
- git = optional(object({
- forgejo_api_token = string
- }))
- })
+variable "dns_subdomain" {
+ type = string
nullable = false
- default = {}
- sensitive = true
- description = "Secrets of the resources in `imports`, written to the platform's Secrets Manager: `git.forgejo_api_token`, a token of an owner of the adopted Forgejo organization, needed until the Git building block's technical user is an owner."
+ description = "Label the platform's DNS zone occupies under `dns_parent_domain`."
}
-variable "use_global_location" {
+variable "cluster_issuer_email" {
+ type = string
+ nullable = false
+ description = "Let's Encrypt contact email registered for the ACME ClusterIssuer and the DNS zone."
+}
+
+variable "phase2_completed" {
type = bool
nullable = false
- description = "Use the global meshStack location; false creates one for the platform."
+ description = "Whether a Harbor robot is linked to the platform's service account; the run fails while none is."
}
-variable "cluster_name" {
- type = string
+variable "stages" {
+ type = map(object({
+ landingzone = optional(map(list(string)), {})
+ project = optional(map(list(string)), {})
+ quotas = optional(list(object({
+ key = string
+ value = number
+ })))
+ }))
nullable = false
- description = "Name of the SKE cluster."
+ description = "Stages of the STACKIT Kubernetes Platform by name, with their tag maps and quotas."
}
-variable "cluster_issuer_email" {
- type = string
+variable "tags" {
+ type = object({
+ landingzone = map(list(string))
+ building_block = map(list(string))
+ starterkit_building_block = map(list(string))
+ starterkit_project = map(list(string))
+ project_owner_tag_key = optional(string)
+ })
nullable = false
- description = "Let's Encrypt contact email registered for the ACME ClusterIssuer and the DNS zone."
+ description = "Tag maps of the STACKIT Kubernetes Platform, see its `tags` input. A null `project_owner_tag_key` sets no owner tag."
}
-variable "dns_subdomain" {
- type = string
+variable "approval_policies" {
+ type = object({
+ building_block_creation = bool
+ user_input_changes = bool
+ any_input_changes = bool
+ manual_triggers = bool
+ version_upgrade = bool
+ })
nullable = false
- description = "Label the platform's DNS zone occupies under `dns_parent_domain`."
+ description = "Run triggers that need an operator's approval on the platform definitions this run registers."
}
-variable "dns_parent_domain" {
- type = string
+variable "starterkit_approval_policies" {
+ type = object({
+ building_block_creation = bool
+ user_input_changes = bool
+ any_input_changes = bool
+ manual_triggers = bool
+ version_upgrade = bool
+ })
nullable = false
- description = "Domain the platform's DNS zone is created under."
+ description = "Run triggers that need an operator's approval on the application team definitions (Git repository, Forgejo connector, SKE starterkit) this run registers."
}
-variable "phase2_completed" {
+variable "use_global_location" {
type = bool
nullable = false
- default = false
- description = "Whether a Harbor robot is linked to the platform's service account; the run fails while none is."
+ description = "Use the global meshStack location; false creates one for the platform."
}
variable "ai_model" {
type = string
- nullable = false
- description = "Model applications default to. Must be one STACKIT Model Serving's `/v1/models` endpoint serves."
+ default = null
+ description = "Model applications default to. Must be one STACKIT Model Serving's `/v1/models` endpoint serves. Null sets up no Model Serving token and no `stackit-ai` secret."
}
variable "starterkit_app_name" {
@@ -147,29 +123,10 @@ variable "starterkit_repo_clone_addr" {
description = "Template repository the starter kit initialises an application repository from."
}
-variable "tags" {
- type = object({
- landingzone = map(list(string))
- building_block = map(list(string))
- starterkit_building_block = optional(map(list(string)), {})
- starterkit_project = map(list(string))
- project_owner_tag_key = optional(string, "")
- })
- nullable = false
- description = "Tags of the STACKIT Kubernetes Platform, see its `tags` input."
-}
-
-variable "stages" {
- type = map(object({
- landingzone = optional(map(list(string)), {})
- project = optional(map(list(string)), {})
- quotas = optional(list(object({
- key = string
- value = number
- })))
- }))
+variable "cluster_name" {
+ type = string
nullable = false
- description = "Stages of the STACKIT Kubernetes Platform, see its `stages` input."
+ description = "Name of the SKE cluster."
}
variable "quota_definitions" {
@@ -186,28 +143,50 @@ variable "quota_definitions" {
description = "Quotas the meshPlatform offers, with their limits; null keeps the defaults of the `kubernetes` hub module. CPU in millicores (m), memory in mebibytes (Mi)."
}
-variable "approval_policies" {
+variable "imports" {
type = object({
- building_block_creation = bool
- user_input_changes = bool
- any_input_changes = bool
- manual_triggers = bool
- version_upgrade = bool
+ adopt_ske = optional(bool, false)
+ git = optional(object({
+ instance_id = string
+ instance_name = string
+ forgejo_organization = string
+ }))
+ dns = optional(object({
+ zone_id = string
+ record_set_id = string
+ }))
+ adopt_ingress = optional(bool, false)
+ secrets_manager = optional(object({
+ instance_id = string
+ }))
+ container_registry = optional(object({
+ artifactory_id = string
+ registry_name = string
+ }))
+ kubernetes_platform = optional(object({
+ platform_name = optional(string)
+ location_name = optional(string)
+ landingzone_names = optional(map(string), {})
+ }))
+ starterkit_definitions = optional(object({
+ starterkit = optional(string)
+ git_repository = optional(string)
+ forgejo_connector = optional(string)
+ }))
})
- nullable = false
- description = "Run triggers that need an operator's approval on the platform definitions this run registers."
+ default = null
+ description = "Existing resources the child building blocks of the same key take over, as in the STACKIT Kubernetes Platform's `imports`. Null takes over nothing."
}
-variable "starterkit_approval_policies" {
+variable "import_secrets" {
type = object({
- building_block_creation = bool
- user_input_changes = bool
- any_input_changes = bool
- manual_triggers = bool
- version_upgrade = bool
+ git = optional(object({
+ forgejo_api_token = string
+ }))
})
- nullable = false
- description = "Run triggers that need an operator's approval on the application team definitions (Git repository, Forgejo connector, SKE starterkit) this run registers."
+ sensitive = true
+ default = null
+ description = "Secrets of the resources in `imports`, written to the platform's Secrets Manager: `git.forgejo_api_token`, a token of an owner of the adopted Forgejo organization, needed until the Git building block's technical user is an owner."
}
variable "starterkit_add_random_name_suffix" {
@@ -216,15 +195,22 @@ variable "starterkit_add_random_name_suffix" {
description = "Append a random suffix to the projects the starterkit creates. Keep it as the adopted starterkit had it: an upgrade that changes it renames, and so replaces, every project and tenant it created."
}
-variable "platform_engineer_role_mapping" {
- type = object({
- git = optional(map(list(string)))
- container_registry = optional(map(list(string)))
- secrets_manager = optional(map(list(string)))
- })
+variable "stackit_project_id" {
+ type = string
nullable = false
- default = {}
- description = "Maps the meshStack project roles on the platform's project to roles on the Git instance (Forgejo `owner`, `writer`, `reader`), the container registry and the Secrets Manager (STACKIT project roles). A service left out keeps its module's default: Project Admins become Forgejo owners, registry admins and Secrets Manager readers."
+ description = "STACKIT project the platform runs in."
+}
+
+variable "tenant_uuid" {
+ type = string
+ nullable = false
+ description = "meshStack tenant of the STACKIT project, which every building block of this run is ordered for."
+}
+
+variable "workspace" {
+ type = string
+ nullable = false
+ description = "meshStack workspace that owns the platform and the definitions this run registers."
}
variable "hub" {
diff --git a/reference-architectures/stackit-kubernetes/platform/meshstack_integration.tf b/reference-architectures/stackit-kubernetes/platform/meshstack_integration.tf
index 3dd770d8..eddf4fc7 100644
--- a/reference-architectures/stackit-kubernetes/platform/meshstack_integration.tf
+++ b/reference-architectures/stackit-kubernetes/platform/meshstack_integration.tf
@@ -18,17 +18,6 @@ variable "starterkit_add_random_name_suffix" {
description = "Append a random suffix to the projects the starterkit creates. Keep it as the adopted starterkit had it: an upgrade that changes it renames, and so replaces, every project and tenant it created."
}
-variable "platform_engineer_role_mapping" {
- type = object({
- git = optional(map(list(string)))
- container_registry = optional(map(list(string)))
- secrets_manager = optional(map(list(string)))
- })
- nullable = false
- default = {}
- description = "Maps the meshStack project roles on the platform's project to roles on the Git instance (Forgejo `owner`, `writer`, `reader`), the container registry and the Secrets Manager (STACKIT project roles). A service left out keeps its module's default: Project Admins become Forgejo owners, registry admins and Secrets Manager readers."
-}
-
variable "starterkit_approval_policies" {
type = object({
building_block_creation = optional(bool, false)
@@ -182,6 +171,7 @@ resource "meshstack_building_block_definition" "this" {
assignment_type = "USER_INPUT"
is_environment = true
updateable_by_consumer = true
+ display_order = 10
}
STACKIT_USE_OIDC = {
@@ -191,6 +181,7 @@ resource "meshstack_building_block_definition" "this" {
assignment_type = "STATIC"
is_environment = true
argument = jsonencode("1")
+ display_order = 20
}
STACKIT_FEDERATED_TOKEN_FILE = {
@@ -200,6 +191,7 @@ resource "meshstack_building_block_definition" "this" {
assignment_type = "STATIC"
is_environment = true
argument = jsonencode("/var/run/secrets/workload-identity/azure/token")
+ display_order = 30
}
automation_identity = {
@@ -208,6 +200,7 @@ resource "meshstack_building_block_definition" "this" {
type = "CODE"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
+ display_order = 40
}
service_account_federation_bbd_version_ref = {
@@ -216,27 +209,7 @@ resource "meshstack_building_block_definition" "this" {
type = "CODE"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
- }
-
- stackit_project_id = {
- display_name = "STACKIT Project ID"
- description = "STACKIT project the platform runs in."
- type = "STRING"
- assignment_type = "PLATFORM_TENANT_ID"
- }
-
- tenant_uuid = {
- display_name = "Tenant UUID"
- description = "meshStack tenant of the STACKIT project, which the building blocks of this run are ordered for."
- type = "STRING"
- assignment_type = "MESHSTACK_TENANT_UUID"
- }
-
- workspace = {
- display_name = "Workspace Identifier"
- description = "Workspace that owns the platform and the definitions this run registers."
- type = "STRING"
- assignment_type = "WORKSPACE_IDENTIFIER"
+ display_order = 50
}
platform_identifier = {
@@ -245,6 +218,7 @@ resource "meshstack_building_block_definition" "this" {
type = "STRING"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
+ display_order = 60
}
playground_mode = {
@@ -253,40 +227,25 @@ resource "meshstack_building_block_definition" "this" {
type = "BOOLEAN"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
+ display_order = 70
}
- imports = {
- display_name = "Imports"
- description = "HCL object of what the building blocks take over, by key: `ske`, `git`, `dns`, `ingress`, `secrets_manager`, `container_registry`, `kubernetes_platform`, `starterkit_definitions`. All optional."
- type = "CODE"
- assignment_type = "USER_INPUT"
- is_optional = true
- }
-
- import_secrets = {
- display_name = "Import Secrets"
- description = "HCL object `{git = {forgejo_api_token}}` this run writes to the platform's Secrets Manager for the adopted Forgejo organization. Empty once the organization is adopted."
- type = "CODE"
- assignment_type = "USER_INPUT"
- sensitive = {}
- is_optional = true
- updateable_by_consumer = true
- }
-
- use_global_location = {
- display_name = "Use Global Location"
- description = "Use the global meshStack location; false creates one for the platform."
- type = "BOOLEAN"
+ dns_parent_domain = {
+ display_name = "DNS Parent Domain"
+ description = "Domain the platform's DNS zone is created under."
+ type = "STRING"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
+ display_order = 80
}
- cluster_name = {
- display_name = "Cluster Name"
- description = "Name of the SKE cluster."
+ dns_subdomain = {
+ display_name = "DNS Subdomain"
+ description = "Label the platform's DNS zone occupies under the parent domain."
type = "STRING"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
+ display_order = 90
}
cluster_issuer_email = {
@@ -295,39 +254,72 @@ resource "meshstack_building_block_definition" "this" {
type = "STRING"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
+ display_order = 100
}
- dns_subdomain = {
- display_name = "DNS Subdomain"
- description = "Label the platform's DNS zone occupies under the parent domain."
- type = "STRING"
+ phase2_completed = {
+ display_name = "Phase 2 Completed"
+ description = "Whether a Harbor robot is linked to the platform's service account; the run fails while none is."
+ type = "BOOLEAN"
assignment_type = "USER_INPUT"
+ default_value = jsonencode(false)
updateable_by_consumer = true
+ display_order = 110
}
- dns_parent_domain = {
- display_name = "DNS Parent Domain"
- description = "Domain the platform's DNS zone is created under."
- type = "STRING"
+ stages = {
+ display_name = "Stages"
+ description = "Stages the platform offers, as the STACKIT Kubernetes Platform takes them."
+ type = "CODE"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
+ display_order = 120
}
- phase2_completed = {
- display_name = "Phase 2 Completed"
- description = "Whether a Harbor robot is linked to the platform's service account; the run fails while none is."
+ tags = {
+ display_name = "Tags"
+ description = "Tag maps `landingzone`, `building_block`, `starterkit_building_block` and `starterkit_project`, and `project_owner_tag_key`, as the STACKIT Kubernetes Platform takes them."
+ type = "CODE"
+ assignment_type = "USER_INPUT"
+ updateable_by_consumer = true
+ display_order = 130
+ }
+
+ approval_policies = {
+ display_name = "Approval Policies"
+ description = "HCL object of approval gates on the platform definitions this registers."
+ type = "CODE"
+ assignment_type = "STATIC"
+ argument = jsonencode(jsonencode(var.approval_policies))
+ display_order = 140
+ }
+
+ starterkit_approval_policies = {
+ display_name = "Starterkit Approval Policies"
+ description = "HCL object of approval gates on the application team definitions this registers."
+ type = "CODE"
+ assignment_type = "STATIC"
+ argument = jsonencode(jsonencode(var.starterkit_approval_policies))
+ display_order = 150
+ }
+
+ use_global_location = {
+ display_name = "Use Global Location"
+ description = "Use the global meshStack location; false creates one for the platform."
type = "BOOLEAN"
assignment_type = "USER_INPUT"
- default_value = jsonencode(false)
updateable_by_consumer = true
+ display_order = 160
}
ai_model = {
display_name = "AI Model"
- description = "Model applications on the platform default to."
+ description = "Model applications on the platform default to. Null sets up no Model Serving token."
type = "STRING"
assignment_type = "USER_INPUT"
+ is_optional = true
updateable_by_consumer = true
+ display_order = 170
}
starterkit_app_name = {
@@ -336,6 +328,7 @@ resource "meshstack_building_block_definition" "this" {
type = "STRING"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
+ display_order = 180
}
starterkit_repo_clone_addr = {
@@ -344,22 +337,16 @@ resource "meshstack_building_block_definition" "this" {
type = "STRING"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
+ display_order = 190
}
- tags = {
- display_name = "Tags"
- description = "Tag maps `landingzone`, `building_block`, `starterkit_building_block` and `starterkit_project`, and `project_owner_tag_key`, as the STACKIT Kubernetes Platform takes them."
- type = "CODE"
- assignment_type = "USER_INPUT"
- updateable_by_consumer = true
- }
-
- stages = {
- display_name = "Stages"
- description = "Stages the platform offers, as the STACKIT Kubernetes Platform takes them."
- type = "CODE"
+ cluster_name = {
+ display_name = "Cluster Name"
+ description = "Name of the SKE cluster."
+ type = "STRING"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
+ display_order = 200
}
quota_definitions = {
@@ -369,30 +356,28 @@ resource "meshstack_building_block_definition" "this" {
assignment_type = "USER_INPUT"
is_optional = true
updateable_by_consumer = true
+ display_order = 210
}
- hub = {
- display_name = "Hub"
- description = "HCL object with `git_ref` (meshstack-hub reference to source nested modules from) and `bbd_draft` (draft state of the application team definitions)."
- type = "CODE"
- assignment_type = "STATIC"
- argument = jsonencode(jsonencode(var.hub))
- }
-
- approval_policies = {
- display_name = "Approval Policies"
- description = "HCL object of approval gates on the platform definitions this registers."
- type = "CODE"
- assignment_type = "STATIC"
- argument = jsonencode(jsonencode(var.approval_policies))
+ imports = {
+ display_name = "Imports"
+ description = "HCL object of what the building blocks take over, by key: `adopt_ske`, `git`, `dns`, `adopt_ingress`, `secrets_manager`, `container_registry`, `kubernetes_platform`, `starterkit_definitions`. All optional."
+ type = "CODE"
+ assignment_type = "USER_INPUT"
+ is_optional = true
+ updateable_by_consumer = true
+ display_order = 220
}
- starterkit_approval_policies = {
- display_name = "Starterkit Approval Policies"
- description = "HCL object of approval gates on the application team definitions this registers."
- type = "CODE"
- assignment_type = "STATIC"
- argument = jsonencode(jsonencode(var.starterkit_approval_policies))
+ import_secrets = {
+ display_name = "Import Secrets"
+ description = "HCL object `{git = {forgejo_api_token}}` this run writes to the platform's Secrets Manager for the adopted Forgejo organization. Empty once the organization is adopted."
+ type = "CODE"
+ assignment_type = "USER_INPUT"
+ sensitive = {}
+ is_optional = true
+ updateable_by_consumer = true
+ display_order = 230
}
starterkit_add_random_name_suffix = {
@@ -401,14 +386,40 @@ resource "meshstack_building_block_definition" "this" {
type = "BOOLEAN"
assignment_type = "STATIC"
argument = jsonencode(var.starterkit_add_random_name_suffix)
+ display_order = 240
+ }
+
+ stackit_project_id = {
+ display_name = "STACKIT Project ID"
+ description = "STACKIT project the platform runs in."
+ type = "STRING"
+ assignment_type = "PLATFORM_TENANT_ID"
+ display_order = 900
+ }
+
+ tenant_uuid = {
+ display_name = "Tenant UUID"
+ description = "meshStack tenant of the STACKIT project, which the building blocks of this run are ordered for."
+ type = "STRING"
+ assignment_type = "MESHSTACK_TENANT_UUID"
+ display_order = 910
}
- platform_engineer_role_mapping = {
- display_name = "Platform Engineer Role Mapping"
- description = "HCL object of role mappings by service (`git`, `container_registry`, `secrets_manager`) for the members of the platform's project."
+ workspace = {
+ display_name = "Workspace Identifier"
+ description = "Workspace that owns the platform and the definitions this run registers."
+ type = "STRING"
+ assignment_type = "WORKSPACE_IDENTIFIER"
+ display_order = 920
+ }
+
+ hub = {
+ display_name = "Hub"
+ description = "HCL object with `git_ref` (meshstack-hub reference to source nested modules from) and `bbd_draft` (draft state of the application team definitions)."
type = "CODE"
assignment_type = "STATIC"
- argument = jsonencode(jsonencode(var.platform_engineer_role_mapping))
+ argument = jsonencode(jsonencode(var.hub))
+ display_order = 1000
}
}
diff --git a/reference-architectures/stackit-landingzone/README.md b/reference-architectures/stackit-landingzone/README.md
index 830b2e7b..96b51ca8 100644
--- a/reference-architectures/stackit-landingzone/README.md
+++ b/reference-architectures/stackit-landingzone/README.md
@@ -93,11 +93,11 @@ When a **network** configuration is provided, it additionally:
## Service Accounts
-You supply one account, as `stackit_service_account_key`, and the architecture creates one. The
-account you supply creates the folder, the foundation project and the meshStack objects, and its key
-is reused on every run rather than only the first. The account it creates lives in the foundation
-project and is what creates tenant projects; it authenticates through workload identity federation,
-so no key for it is ever stored.
+You supply the **deployment service account**, as `stackit_service_account_key`, and the
+architecture creates the **project-creation service account**. The deployment service account
+creates the folder, the foundation project and the meshStack objects, and its key is reused on every
+run. The project-creation service account lives in the foundation project and creates tenant
+projects; it authenticates through workload identity federation, so no key for it is ever stored.
### How composing architectures get an identity
@@ -113,8 +113,9 @@ landing zone refs, and the version refs of two definitions:
Both are the latest versions. Another workspace may only order a released version that is
published to the marketplace, so deploy the landing zone with `hub.bbd_draft = false` and publish
-both definitions in meshPanel. A new version gets a new uuid, so copy the block again after the
-definitions change.
+both definitions in meshPanel. `hub.bbd_draft` releases only the definitions this landing zone
+registers; its own definition is always a draft, so it stays in the owning workspace. A new version
+gets a new uuid, so copy the block again after the definitions change.
A composing architecture orders the service account on a tenant it just created, with the roles it
needs. It then orders the federation as a child of the service account, with the uuids of its own
@@ -136,14 +137,15 @@ not a new credential.
`stackit_owner_email` owns the folder, the foundation project and every tenant project the platform
creates. STACKIT applies it at creation only, so changing it later means recreating what it owns.
-The key you supply is expected to be an organization owner, which may name any address — a team
-mailbox, typically. A key holding only `resource-manager.admin` may create a project but not act
-inside one it does not own, so with such a key the owner has to be that account's own address, or
-the run fails with `POST /v2/projects//service-accounts -> 403`.
+The deployment service account is expected to be an organization owner, which may name any
+address — a team mailbox, typically. A deployment service account holding only
+`resource-manager.admin` may create a project but not act inside one it does not own, so the owner
+then has to be the deployment service account's own email, or the run fails with
+`POST /v2/projects//service-accounts -> 403`.
-Tenant projects are unaffected either way: the tenant-project account works through its
+Tenant projects are unaffected either way: the project-creation service account works through its
organization-scoped roles, not through ownership. One of those roles is conditional — with
-`stackit_organization_onboarding_enabled` off it lacks `iam.member-admin` and cannot assign project
+`role_mapping.organization_onboarding` off it lacks `iam.member-admin` and cannot assign project
roles to users.
## Getting Started
@@ -152,7 +154,7 @@ roles to users.
| Requirement | Description |
|----------------------|-----------------------------------------------------------------------------------|
-| STACKIT organization | With an organization-owner service account key. `resource-manager.admin` alone also works, but then constrains which `stackit_owner_email` values are valid — see [Service Accounts](#service-accounts). |
+| STACKIT organization | A key of the deployment service account, an organization owner. `resource-manager.admin` alone also works, but then constrains which `stackit_owner_email` values are valid — see [Service Accounts](#service-accounts). |
| CIDR plan | *(Only when enabling networking)* A non-overlapping IPv4 address plan chosen up front for the hub network ranges and transfer network. |
### Deployment Order
@@ -189,14 +191,15 @@ the tenant projects stay.
### Playground Mode
-`playground_mode` defaults to `true`, so an unconfigured deployment is a throwaway one. Two things
-change with it:
+**Playground Mode** deploys a throwaway platform. It is off by default, and four things change
+with it:
| | `playground_mode = true` | `playground_mode = false` |
|---|---|---|
| Platform identifier | random suffix appended | taken exactly as given |
| Root folder | always, `playground-` unless named | only when named |
| Folders and foundation project | destroyable | guarded with `prevent_destroy` |
+| Starterkit Approval Policies | hidden, no gates | as set |
The suffix exists because a platform identifier is unique across the whole meshStack instance, and
`modules/stackit` builds every landing zone name from it as `-`. A
@@ -206,15 +209,14 @@ playground deployment that took the plain name would occupy it for good, across
definitions it registers should be published to other workspaces: the platform can disappear at any
time, and its name is not the one a real deployment will use.
-Set `playground_mode = false` for a platform that is actually used, as the `stackit-platform`
+Keep `playground_mode = false` for a platform that is actually used, as the `stackit-platform`
workspace in likvid-cloudfoundation does. The guard is then a plan-time rule, so it also catches a
teardown caused by a change that forces the folder to be replaced. It cannot catch a deletion that
happens outside the Terraform run, such as someone removing the folder in the STACKIT portal.
-The flag belongs to whoever deploys the definition, not to whoever orders it. It reaches the
-building block as a `STATIC` input, so it does not appear as a choice in the order form and a
-consumer cannot turn a real platform into a playground one, or the reverse. Change it by setting
-`playground_mode` on the reference architecture module and deploying a new definition version.
+The flag is chosen in the order form and cannot be changed afterwards: a run that sees another
+value fails at plan time, so a real platform never turns into a playground one, or the reverse.
+Deleting the building block still works.
### Adopting an Existing Platform
@@ -226,7 +228,7 @@ false`, because nothing taken over is released on destroy.
|---|---|
| `folder = {container_id}` | the landing-zone folder, renamed to the platform identifier |
| `foundation_project = {container_id}` | the foundation project, renamed to `-foundation` |
-| `platform = {}` | the location (unless **Use Global Location**), the STACKIT platform `` in it and its landing zones `-default` and, with `hub&spoke`, `-networked` |
+| `adopt_platform = true` | the location (unless **Use Global Location**), the STACKIT platform `` in it and its landing zones `-default` and, with `hub&spoke`, `-networked` |
| `project_definition = {uuid, service_account_name, service_account_email, federated_identity_provider_ids}` | the STACKIT Project definition and its backplane: the project-creation service account, its federated identity providers in the order of the subjects they trust, and its organization roles |
| `service_account_definition = {uuid, service_account_email, federated_identity_provider_ids}` | the STACKIT Service Account definition and its backplane |
| `service_account_federation_definition = {uuid, service_account_email, federated_identity_provider_ids}` | the STACKIT Service Account Federation definition and its backplane |
@@ -235,11 +237,15 @@ The three definition keys need `foundation_project`, where their service account
definition gets a new version and its building blocks stay on theirs until upgraded. Turn on the
approval gates for the adoption and approve only a plan that replaces nothing.
+The first run keeps the project-creation service account's name in its state, the adopted
+`service_account_name` or else one derived from the platform identifier. Once the adoption has
+succeeded, **Imports** can be set back to null without renaming or replacing anything.
+
### Approval Gates
-`approval_policies` sets which run triggers need an operator's approval before a run of this
-architecture is applied; `starterkit_approval_policies` does the same for the project starterkit
-definition it registers. Both default to no gate at all.
+The module variable `approval_policies` sets which run triggers need an operator's approval before
+a run of this architecture is applied. The order form input **Starterkit Approval Policies** does
+the same for the project starterkit definition it registers. Both default to no gate at all.
## Shared Responsibilities
diff --git a/reference-architectures/stackit-landingzone/buildingblock/README.md b/reference-architectures/stackit-landingzone/buildingblock/README.md
index 386a4679..775625ae 100644
--- a/reference-architectures/stackit-landingzone/buildingblock/README.md
+++ b/reference-architectures/stackit-landingzone/buildingblock/README.md
@@ -27,10 +27,11 @@ The summary shows the platform ref, the landing zone refs and the version refs o
account definitions as a code block. You paste it into a composing architecture such as the STACKIT
Kubernetes Platform, so that architecture can build on this one without reading this building block.
-It authenticates to STACKIT with a service account key you paste as a secret input. You also
-provide the STACKIT organization UUID, owner email, nested integration tags and default role mapping
-as user inputs. The service account needs `resource-manager.admin` on the organization. The nested
-integrations are pinned to the same `git_ref` as this building block's implementation.
+It authenticates to STACKIT as the deployment service account, whose key you paste as a secret
+input. You also provide the STACKIT organization UUID, owner email, nested integration tags and
+default role mapping as user inputs. The deployment service account needs `resource-manager.admin`
+on the organization. The nested integrations are pinned to the same `git_ref` as this building
+block's implementation.
The user-facing readme is maintained inline in the `readme` field of the
`meshstack_building_block_definition` in
diff --git a/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl b/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl
index d524d69c..7113595b 100644
--- a/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl
+++ b/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl
@@ -50,10 +50,10 @@ value from this landing zone. Paste this block into its **STACKIT Landing Zone**
%{ endif ~}
```hcl
{
- platform_ref = { uuid = "${platform_ref.uuid}", kind = "${platform_ref.kind}" }
+ platform_ref = { uuid = "${platform_ref.uuid}" }
landingzone_refs = {
%{ for key, ref in landingzone_refs ~}
- ${key} = { name = "${ref.name}", kind = "${ref.kind}" }
+ ${key} = { name = "${ref.name}" }
%{ endfor ~}
}
service_account_bbd_version_ref = { uuid = "${service_account_bbd_version_uuid}" }
diff --git a/reference-architectures/stackit-landingzone/buildingblock/imports.tf b/reference-architectures/stackit-landingzone/buildingblock/imports.tf
index 1e14b1a0..facdd5e1 100644
--- a/reference-architectures/stackit-landingzone/buildingblock/imports.tf
+++ b/reference-architectures/stackit-landingzone/buildingblock/imports.tf
@@ -2,7 +2,7 @@ locals {
imports = var.imports != null ? var.imports : {
folder = null
foundation_project = null
- platform = null
+ adopt_platform = false
project_definition = null
service_account_definition = null
service_account_federation_definition = null
@@ -49,7 +49,7 @@ data "stackit_resourcemanager_project" "adopted" {
}
import {
- for_each = local.imports.platform == null || var.use_global_location ? toset([]) : toset([local.platform_identifier])
+ for_each = local.imports.adopt_platform && !var.use_global_location ? toset([local.platform_identifier]) : toset([])
to = meshstack_location.this
id = each.value
}
@@ -58,11 +58,11 @@ import {
# lookup fails unless both match.
data "meshstack_platforms" "adopted" {
lifecycle {
- enabled = local.imports.platform != null
+ enabled = local.imports.adopt_platform
postcondition {
condition = length(self.platforms) == 1
- error_message = "imports.platform: workspace ${var.workspace} owns no STACKIT platform ${local.platform_identifier}.${local.adopted_location_name}."
+ error_message = "imports.adopt_platform: workspace ${var.workspace} owns no STACKIT platform ${local.platform_identifier}.${local.adopted_location_name}."
}
}
@@ -72,13 +72,13 @@ data "meshstack_platforms" "adopted" {
}
import {
- for_each = local.imports.platform == null ? toset([]) : toset([local.platform_identifier])
+ for_each = local.imports.adopt_platform ? toset([local.platform_identifier]) : toset([])
to = module.stackit_integration.meshstack_platform.this
id = one(data.meshstack_platforms.adopted.platforms).metadata.uuid
}
import {
- for_each = local.imports.platform == null ? toset([]) : toset(local.adopted_landingzone_variants)
+ for_each = local.imports.adopt_platform ? toset(local.adopted_landingzone_variants) : toset([])
to = module.stackit_integration.meshstack_landingzone.this[each.key]
id = "${local.platform_identifier}-${each.key}"
}
@@ -123,7 +123,7 @@ import {
}
import {
- for_each = { for key, adopted in local.adopted_definitions : key => adopted.service_account_email if key == "project_definition" && var.stackit_organization_onboarding_enabled }
+ for_each = { for key, adopted in local.adopted_definitions : key => adopted.service_account_email if key == "project_definition" && var.role_mapping.organization_onboarding }
to = module.stackit_integration.module.backplane.stackit_authorization_organization_role_assignment.member_admin
id = "${var.stackit_org},iam.member-admin,${each.value}"
}
diff --git a/reference-architectures/stackit-landingzone/buildingblock/main.tf b/reference-architectures/stackit-landingzone/buildingblock/main.tf
index ec7ff689..ba355161 100644
--- a/reference-architectures/stackit-landingzone/buildingblock/main.tf
+++ b/reference-architectures/stackit-landingzone/buildingblock/main.tf
@@ -3,6 +3,15 @@ locals {
# name, so a playground deployment suffixes it instead of occupying the plain name.
platform_identifier = var.playground_mode ? "${var.platform_identifier}-${random_string.playground_suffix.result}" : var.platform_identifier
+ # The order form hides the policies in playground mode, which gates nothing.
+ starterkit_approval_policies = var.playground_mode ? {
+ building_block_creation = false
+ user_input_changes = false
+ any_input_changes = false
+ manual_triggers = false
+ version_upgrade = false
+ } : var.starterkit_approval_policies
+
# STACKIT caps a service account name at 20 characters and rejects one ending in a dash, so cutting
# the identifier to length can produce an invalid name. Cut shorter instead, drop whatever dashes
# the cut exposed, and end with a hash of the full identifier so two names sharing a prefix stay
@@ -38,6 +47,30 @@ locals {
}
}
+# Flipping the flag on an existing platform would suffix or rename everything and change what may be destroyed.
+resource "terraform_data" "playground_mode" {
+ input = var.playground_mode
+
+ lifecycle {
+ ignore_changes = [input]
+
+ postcondition {
+ condition = self.output == var.playground_mode
+ error_message = "playground_mode cannot change after the order."
+ }
+ }
+}
+
+# Pinned at the first run, so a later run without imports keeps the service account every tenant
+# project names as its owner. Read `input`: `output` is unknown at the first plan and would replace it.
+resource "terraform_data" "project_service_account_name" {
+ input = local.imports.project_definition == null ? local.service_account_name : local.imports.project_definition.service_account_name
+
+ lifecycle {
+ ignore_changes = [input]
+ }
+}
+
resource "random_string" "playground_suffix" {
lifecycle {
enabled = var.playground_mode
@@ -113,9 +146,9 @@ module "stackit_integration" {
stackit_project_owner_email = var.stackit_owner_email
stackit_parent_container_id = stackit_resourcemanager_folder.this.container_id
stackit_project_id = stackit_resourcemanager_project.foundation.project_id
- stackit_service_account_name = local.imports.project_definition == null ? local.service_account_name : local.imports.project_definition.service_account_name
- role_mapping = var.role_mapping
- stackit_organization_onboarding_enabled = var.stackit_organization_onboarding_enabled
+ stackit_service_account_name = terraform_data.project_service_account_name.input
+ role_mapping = { admin = var.role_mapping.admin, user = var.role_mapping.user, reader = var.role_mapping.reader }
+ stackit_organization_onboarding_enabled = var.role_mapping.organization_onboarding
# The networked project definition places its projects in the hub network area via a static
# `networkArea` label, so no landing zone tag (and no tag definition) is involved.
@@ -144,7 +177,7 @@ module "stackit_project_starterkit" {
)
default_landing_zone = contains(var.topology, "sandbox") ? "sandbox" : "hub&spoke"
- approval_policies = var.starterkit_approval_policies
+ approval_policies = local.starterkit_approval_policies
# `hub&spoke` is the only landing zone attached to a network area, so it is the only one where the
# starterkit creates a spoke network. Null — networking disabled — drops the `Network` input from the
diff --git a/reference-architectures/stackit-landingzone/buildingblock/variables.tf b/reference-architectures/stackit-landingzone/buildingblock/variables.tf
index c3bbb21a..8702b5a0 100644
--- a/reference-architectures/stackit-landingzone/buildingblock/variables.tf
+++ b/reference-architectures/stackit-landingzone/buildingblock/variables.tf
@@ -1,7 +1,18 @@
-variable "workspace" {
+variable "platform_identifier" {
type = string
nullable = false
- description = "Identifier of the meshStack workspace that will own the created platform, location, landing zones, and (when networking is enabled) the hub network-area instance."
+ description = "Identifier for the STACKIT sandbox platform created in meshStack (letters, digits and dashes only)."
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9-]+$", var.platform_identifier))
+ error_message = "platform_identifier must only contain letters, digits, and dashes."
+ }
+}
+
+variable "playground_mode" {
+ type = bool
+ nullable = false
+ description = "Deploy a throwaway platform: the platform identifier gets a random suffix so it does not occupy a name for good, and the landing-zone folder and foundation project are left destroyable. Set to false for a platform that is actually used. Cannot change after the first run. A playground platform and the building block definitions it registers are not meant to be published to other workspaces."
}
variable "use_global_location" {
@@ -21,20 +32,6 @@ variable "stackit_org" {
}
}
-variable "stackit_owner_email" {
- type = string
- nullable = false
- description = "Owner email assigned to the STACKIT resourcemanager folder, the foundation project, and every tenant project the platform creates."
-}
-
-variable "stackit_service_account_key" {
- type = string
- sensitive = true
- ephemeral = true
- nullable = false
- description = "STACKIT service account key JSON with `resource-manager.admin` on the organization. Used to create the landing-zone folder and foundation project."
-}
-
variable "root_folder_name" {
type = string
default = null
@@ -46,44 +43,18 @@ variable "root_folder_name" {
}
}
-variable "platform_identifier" {
+variable "stackit_service_account_key" {
type = string
+ sensitive = true
+ ephemeral = true
nullable = false
- description = "Identifier for the STACKIT sandbox platform created in meshStack (letters, digits and dashes only)."
-
- validation {
- condition = can(regex("^[a-zA-Z0-9-]+$", var.platform_identifier))
- error_message = "platform_identifier must only contain letters, digits, and dashes."
- }
-}
-
-variable "tags" {
- type = object({
- landingzone = list(object({ key = string, values = list(string) }))
- building_block = list(object({ key = string, values = list(string) }))
- project = list(object({ key = string, values = list(string) }))
- project_owner_tag_key = string
- })
- nullable = false
- description = <<-EOT
- Tags forwarded to the nested STACKIT integrations, each map built in the meshPanel form as a list of {key, values} entries.
- `landingzone` tags are applied to the created landing zones.
- `building_block` tags are applied to the nested building block definitions.
- `project` tags are applied to the meshProjects the starterkit creates.
- `project_owner_tag_key` names the tag that receives the creator's display name.
- EOT
-}
-
-variable "role_mapping" {
- type = map(list(string))
- nullable = false
- description = "Default mapping from meshStack roles to STACKIT project roles for the nested STACKIT Project integration. Values can be built-in STACKIT roles or custom STACKIT role names."
+ description = "Key JSON of the deployment service account, which needs `resource-manager.admin` on the organization. Used to create the landing-zone folder and foundation project."
}
-variable "stackit_organization_onboarding_enabled" {
- type = bool
+variable "stackit_owner_email" {
+ type = string
nullable = false
- description = "Whether the nested STACKIT Project integration adds meshStack project users to the STACKIT organization before applying project-level role assignments. Disable if organization membership is managed outside this landing zone."
+ description = "Owner email assigned to the STACKIT resourcemanager folder, the foundation project, and every tenant project the platform creates. Must be the deployment service account's own email unless the deployment service account is an organization owner."
}
variable "topology" {
@@ -110,39 +81,55 @@ variable "network" {
tenant_network_max_prefix_length = optional(number, 28)
})
default = null
- description = "Hub-and-spoke address plan, read only when `topology` includes `hub&spoke`. The input is hidden, and so not sent, otherwise."
+ description = "Hub-and-spoke address plan, read only when `topology` includes `hub&spoke`. Otherwise the input is hidden and meshStack sends no value."
}
-variable "starterkit_approval_policies" {
+variable "role_mapping" {
type = object({
- building_block_creation = bool
- user_input_changes = bool
- any_input_changes = bool
- manual_triggers = bool
- version_upgrade = bool
+ admin = list(string)
+ user = list(string)
+ reader = list(string)
+ organization_onboarding = optional(bool, true)
})
nullable = false
- description = "Run triggers that need an operator's approval before a run of the project starterkit is applied. The defaults are the provider's own, and the provider asserts them whenever the definition sets no policies — so a gate switched on in meshPanel is turned off again by the next run unless it is set here."
+ description = <<-EOT
+ Default mapping from meshStack roles to STACKIT project roles for the nested STACKIT Project integration. Values can be built-in STACKIT roles or custom STACKIT role names.
+ `organization_onboarding` adds meshStack project users to the STACKIT organization before the project roles are granted. Turn it off if organization membership is managed outside this landing zone.
+ EOT
}
-variable "playground_mode" {
- type = bool
+variable "tags" {
+ type = object({
+ landingzone = list(object({ key = string, values = list(string) }))
+ building_block = list(object({ key = string, values = list(string) }))
+ project = list(object({ key = string, values = list(string) }))
+ project_owner_tag_key = optional(string)
+ })
nullable = false
- description = "Deploy a throwaway platform: the platform identifier gets a random suffix so it does not occupy a name for good, and the landing-zone folder and foundation project are left destroyable. Set to false for a platform that is actually used. A playground platform and the building block definitions it registers are not meant to be published to other workspaces."
+ description = <<-EOT
+ Tags forwarded to the nested STACKIT integrations, each map built in the meshPanel form as a list of {key, values} entries.
+ `landingzone` tags are applied to the created landing zones.
+ `building_block` tags are applied to the nested building block definitions.
+ `project` tags are applied to the meshProjects the starterkit creates.
+ `project_owner_tag_key` names the tag that receives the creator's display name; null or empty sets none.
+ EOT
}
-variable "hub" {
+variable "starterkit_approval_policies" {
type = object({
- git_ref = optional(string, "main")
- bbd_draft = optional(bool, true)
+ building_block_creation = bool
+ user_input_changes = bool
+ any_input_changes = bool
+ manual_triggers = bool
+ version_upgrade = bool
})
- const = true
- default = { git_ref = "main", bbd_draft = true }
+ default = null
+ description = "Run triggers that need an operator's approval before a run of the project starterkit is applied. The defaults are the provider's own, and the provider asserts them whenever the definition sets no policies — so a gate switched on in meshPanel is turned off again by the next run unless it is set here. Ignored in playground mode, which gates nothing."
- description = <<-EOT
- `git_ref`: meshstack-hub reference used to source the nested foundation, network-area, and network integration modules. `const` so it can be interpolated into the module source at init time.
- `bbd_draft`: Forwarded as-is to those nested integrations' own `hub.bbd_draft`, so their building block definition draft state tracks this building block's own release state.
- EOT
+ validation {
+ condition = var.playground_mode || var.starterkit_approval_policies != null
+ error_message = "starterkit_approval_policies is required unless playground_mode is set."
+ }
}
variable "imports" {
@@ -153,7 +140,7 @@ variable "imports" {
foundation_project = optional(object({
container_id = string
}))
- platform = optional(object({}))
+ adopt_platform = optional(bool, false)
project_definition = optional(object({
uuid = string
service_account_name = string
@@ -180,7 +167,27 @@ variable "imports" {
}
validation {
- condition = var.imports == null ? true : alltrue([for key, adopted in var.imports : adopted == null || key == "folder" || key == "platform" || var.imports.foundation_project != null])
+ condition = var.imports == null ? true : alltrue([for key, adopted in var.imports : adopted == null || contains(["folder", "adopt_platform"], key) || var.imports.foundation_project != null])
error_message = "imports.project_definition, service_account_definition and service_account_federation_definition need imports.foundation_project, because their service accounts live in it."
}
}
+
+variable "workspace" {
+ type = string
+ nullable = false
+ description = "Identifier of the meshStack workspace that will own the created platform, location, landing zones, and (when networking is enabled) the hub network-area instance."
+}
+
+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`: meshstack-hub reference used to source the nested foundation, network-area, and network integration modules. `const` so it can be interpolated into the module source at init time.
+ `bbd_draft`: Draft state of the definitions this landing zone registers; the landing zone's own definition is always a draft.
+ EOT
+}
diff --git a/reference-architectures/stackit-landingzone/meshstack_integration.tf b/reference-architectures/stackit-landingzone/meshstack_integration.tf
index e6493a1f..dba68106 100644
--- a/reference-architectures/stackit-landingzone/meshstack_integration.tf
+++ b/reference-architectures/stackit-landingzone/meshstack_integration.tf
@@ -42,7 +42,9 @@ variable "hub" {
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.
+ `bbd_draft`: If true, the definitions this landing zone registers (project starter kit, service account,
+ federation, networks) are kept in draft mode. This definition is always a draft, so it stays in the owning
+ workspace.
EOT
}
@@ -65,59 +67,6 @@ variable "approval_policies" {
description = "Run triggers that need an operator's approval before a run of this architecture is applied. The defaults are the provider's own, and the provider asserts them whenever the definition sets no policies — so a gate switched on in meshPanel is turned off again by the next apply unless it is set here."
}
-variable "starterkit_approval_policies" {
- type = object({
- building_block_creation = optional(bool, false)
- user_input_changes = optional(bool, false)
- any_input_changes = optional(bool, false)
- manual_triggers = optional(bool, false)
- version_upgrade = optional(bool, false)
- })
- nullable = false
- default = {
- building_block_creation = false
- user_input_changes = false
- any_input_changes = false
- manual_triggers = false
- version_upgrade = false
- }
- description = "The same, for the project starterkit definition this architecture registers. Set `building_block_creation` to have an operator approve every project an application team orders."
-}
-
-variable "playground_mode" {
- type = bool
- nullable = false
- default = true
-
- description = "Deploy a throwaway platform: the platform identifier gets a random suffix so it does not occupy a name for good, and the landing-zone folder and foundation project are left destroyable. Set to false for a platform that is actually used. Passed to the building block as a STATIC input, so whoever orders the architecture cannot choose. A playground platform and the building block definitions it registers are not meant to be published to other workspaces."
-}
-
-variable "default_tags" {
- type = object({
- landingzone = optional(map(list(string)), {})
- building_block = optional(map(list(string)), {})
- project = optional(map(list(string)), {})
- project_owner_tag_key = optional(string, "")
- })
- nullable = false
-
- # Spelled out rather than left to the `optional()` defaults: this feeds an input's `json_schema`
- # default, and a consumer that does not evaluate object-attribute defaulting would see unset
- # fields. See .agents/references/meshstack-integration.md.
- default = {
- landingzone = {
- LandingZoneFamily = ["sandbox"]
- confidentiality = ["internal", "public"]
- environment = ["dev"]
- }
- building_block = {}
- project = {}
- project_owner_tag_key = "projectOwner"
- }
-
- description = "Starter values pre-filling the Tags form, as maps of tag key to values. Ships with an example set; override per foundation to match the instance's own tag schema. The operator can still edit, extend or clear them when ordering."
-}
-
output "building_block_definition" {
description = "BBD is consumed in building block compositions."
value = {
@@ -126,11 +75,8 @@ output "building_block_definition" {
}
}
-# The Tags input is a meshPanel form the operator fills from scratch: for each of the three tag maps
-# they add as many {key, values} entries as they need — nothing is read from the instance's tag
-# schema. A map of free-form keys has no form widget (meshPanel renders only declared properties), so
-# each map is modelled as a growable array of entries; buildingblock/ folds them back into the
-# map(list(string)) the nested integrations take.
+# meshPanel has no form widget for a map of free-form keys, so each tag map is a growable list of
+# {key, values} entries; buildingblock/ folds them back into maps.
locals {
tag_list_schema = {
type = "array"
@@ -144,21 +90,13 @@ locals {
}
}
- # The form's starter entries come from var.default_tags, so each foundation seeds keys matching its
- # own tag schema instead of anything hard-coded here. Each map is turned into the {key, values}
- # entry list the form (and the buildingblock variable) use.
- tags_default_entries = {
- for section, m in {
- landingzone = var.default_tags.landingzone
- building_block = var.default_tags.building_block
- project = var.default_tags.project
- } : section => [for tk, tv in m : { key = tk, values = tv }]
- }
+ approval_gates = ["building_block_creation", "user_input_changes", "any_input_changes", "manual_triggers", "version_upgrade"]
+ approval_policies_default = { for gate in local.approval_gates : gate => false }
imports_definition_properties = {
- uuid = { type = "string", title = "Definition UUID" }
- service_account_email = { type = "string", title = "Backplane Service Account Email" }
- federated_identity_provider_ids = { type = "array", title = "Federated Identity Provider IDs", items = { type = "string" } }
+ uuid = { type = "string", title = "uuid" }
+ service_account_email = { type = "string", title = "service_account_email" }
+ federated_identity_provider_ids = { type = "array", title = "federated_identity_provider_ids", items = { type = "string" } }
}
imports_definition_schema = {
@@ -167,15 +105,25 @@ locals {
properties = local.imports_definition_properties
}
+ imports_container_schema = {
+ type = "object"
+ required = ["container_id"]
+ properties = { container_id = { type = "string", title = "container_id" } }
+ }
+
tags_json_schema = {
"$schema" = "http://json-schema.org/draft-07/schema#"
type = "object"
- required = ["landingzone", "building_block", "project", "project_owner_tag_key"]
+ required = ["landingzone", "building_block", "project"]
properties = {
- landingzone = merge(local.tag_list_schema, { title = "Landing Zone Tags", default = local.tags_default_entries.landingzone })
- building_block = merge(local.tag_list_schema, { title = "Building Block Tags", default = local.tags_default_entries.building_block })
- project = merge(local.tag_list_schema, { title = "Project Tags", default = local.tags_default_entries.project })
- project_owner_tag_key = { type = "string", title = "Project Owner Tag Key", default = var.default_tags.project_owner_tag_key }
+ landingzone = merge(local.tag_list_schema, { title = "Landing Zone Tags" })
+ building_block = merge(local.tag_list_schema, { title = "Building Block Tags" })
+ project = merge(local.tag_list_schema, { title = "Project Tags" })
+ project_owner_tag_key = {
+ type = "string"
+ title = "Project Owner Tag Key"
+ description = "Tag on the created meshProjects that gets the creator's display name. Leave out to set none."
+ }
}
}
}
@@ -268,9 +216,8 @@ resource "meshstack_building_block_definition" "this" {
## 🧪 Playground mode
- **Playground Mode** is fixed by whoever deployed this definition and cannot be chosen when
- ordering. It defaults to `true`, which deploys a throwaway platform: the platform identifier gets
- a random suffix, everything goes into a root folder named `playground-`,
+ **Playground Mode** is chosen when ordering and cannot be changed afterwards. Turned on, it
+ deploys a throwaway platform: the platform identifier gets a random suffix, everything goes into a root folder named `playground-`,
and none of the folders nor the foundation project is protected against deletion, so the whole
thing can be deleted again in one step.
@@ -279,20 +226,19 @@ resource "meshstack_building_block_definition" "this" {
occupy the plain name for good. Such a platform and the building block definitions it registers
are meant for the deploying workspace only — do not publish them to other workspaces.
- A platform that is actually used is deployed from a definition with **Playground Mode** set to
- `false`. The identifier is then taken as given, and the folder and foundation project are guarded
+ A platform that is actually used keeps **Playground Mode** off, the default. The identifier is then taken as given, and the folder and foundation project are guarded
with `prevent_destroy`, so a deletion run fails rather than taking every tenant project with it.
## 🔑 Authentication
- You provide the STACKIT organization UUID, owner email, tags, default role mapping and a service account key as inputs.
- The building block authenticates to STACKIT with the service account key, which needs `resource-manager.admin` on the organization.
+ You provide the STACKIT organization UUID, owner email, tags, default role mapping and the key of the deployment service account as inputs.
+ The building block authenticates to STACKIT as the deployment service account, which needs `resource-manager.admin` on the organization.
## 📊 Shared responsibility
| Responsibility | Platform Team | Application Team |
|---|:---:|:---:|
- | Provide the STACKIT service account key, organization details, tags and role mapping | ✅ | ❌ |
+ | Provide the deployment service account key, organization details, tags and role mapping | ✅ | ❌ |
| Provision the location, folder and STACKIT Project platform | ✅ | ❌ |
| Register the self-service `STACKIT Service Account` building block | ✅ | ❌ |
| (Optional) Provide the network CIDR plan and provision the hub network area | ✅ | ❌ |
@@ -306,7 +252,7 @@ resource "meshstack_building_block_definition" "this" {
}
version_spec = {
- draft = var.hub.bbd_draft
+ draft = true
runner_ref = var.meshstack.runner_ref
deletion_mode = "DELETE"
@@ -337,23 +283,32 @@ resource "meshstack_building_block_definition" "this" {
}
inputs = {
- stackit_service_account_key = {
- display_name = "STACKIT Service Account Key"
- description = "Full key JSON of the deployment service account, reused on every run. Needs `resource-manager.admin` on the organization, or organization owner to allow a different `stackit_owner_email`."
- type = "CODE"
- assignment_type = "USER_INPUT"
- updateable_by_consumer = true
- sensitive = {}
- display_order = 60
+ platform_identifier = {
+ display_name = "Platform Identifier"
+ description = "Identifier for the STACKIT sandbox platform created in meshStack (letters, digits and dashes only)."
+ type = "STRING"
+ assignment_type = "USER_INPUT"
+ value_validation_regex = "^[a-zA-Z0-9-]+$"
+ validation_regex_error_message = "platform_identifier must only contain letters, digits, and dashes."
+ display_order = 10
}
- hub = {
- display_name = "Hub"
- description = "HCL object with `git_ref` (meshstack-hub reference used to source the nested STACKIT integration modules) and `bbd_draft` (forwarded to those nested integrations' own building block definition draft state)."
- type = "CODE"
- assignment_type = "STATIC"
- argument = jsonencode(jsonencode(var.hub))
- display_order = 120
+ playground_mode = {
+ display_name = "Playground Mode"
+ description = "Throwaway deployment: the identifier gets a random suffix and nothing is protected against deletion. Do not publish such a platform or its definitions to other workspaces. Cannot be changed after the order."
+ type = "BOOLEAN"
+ assignment_type = "USER_INPUT"
+ default_value = jsonencode(false)
+ display_order = 15
+ }
+
+ use_global_location = {
+ display_name = "Use Global Location"
+ description = "If true, use the existing global meshStack location instead of creating a dedicated location for this platform."
+ type = "BOOLEAN"
+ assignment_type = "USER_INPUT"
+ default_value = jsonencode(true)
+ display_order = 20
}
stackit_org = {
@@ -363,63 +318,35 @@ resource "meshstack_building_block_definition" "this" {
assignment_type = "USER_INPUT"
value_validation_regex = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
validation_regex_error_message = "STACKIT Organization UUID must be a valid UUID."
- display_order = 40
+ display_order = 30
}
- stackit_owner_email = {
- display_name = "STACKIT Owner Email"
- description = "Owner of the STACKIT folder, foundation project, and every tenant project. Applied at creation only. Must be the deployment account's own address unless that account is an organization owner."
- type = "STRING"
- assignment_type = "USER_INPUT"
- display_order = 50
- }
-
- # Keep this description under roughly 200 characters. meshStack answers a longer one with
- # `500 InternalError` on the version update, not a 400 — the longest description any live
- # definition here carries is 206, and 387 fails.
- tags = {
- display_name = "Tags"
- description = "Tags forwarded to the nested integrations. Build them in the form: add {key, values} entries for landing zones, building blocks, and the created meshProjects, plus the project owner tag key."
- type = "JSON"
+ root_folder_name = {
+ display_name = "Root Folder Name"
+ description = "Folder at the organization root for the landing-zone folder and the foundation project, when the organization serves more than one meshStack. Left out: no root folder."
+ type = "STRING"
assignment_type = "USER_INPUT"
+ is_optional = true
updateable_by_consumer = true
- display_order = 80
-
- # meshStack decodes a JSON input into the buildingblock variable's declared object type (see
- # commit 991343f0), so `var.tags` stays the typed object and needs no jsondecode.
- json_schema = jsonencode(local.tags_json_schema)
+ display_order = 35
}
- role_mapping = {
- display_name = "STACKIT Project Role Mapping"
- description = "Maps each meshStack project role to the STACKIT project roles it grants. Values can be built-in STACKIT roles or custom STACKIT role names."
- type = "JSON"
+ stackit_service_account_key = {
+ display_name = "STACKIT Service Account Key"
+ description = "Full key JSON of the deployment service account, reused on every run. Needs `resource-manager.admin` on the organization, or organization owner to allow a different `stackit_owner_email`."
+ type = "CODE"
assignment_type = "USER_INPUT"
updateable_by_consumer = true
- display_order = 70
-
- # A JSON input decodes into var.role_mapping's map(list(string)); the three meshStack roles
- # are fixed keys, so the form is one field per role holding its STACKIT role list.
- json_schema = jsonencode({
- "$schema" = "http://json-schema.org/draft-07/schema#"
- type = "object"
- required = ["admin", "user", "reader"]
- properties = {
- admin = { type = "array", title = "admin", items = { type = "string" }, default = ["owner"] }
- user = { type = "array", title = "user", items = { type = "string" }, default = ["editor"] }
- reader = { type = "array", title = "reader", items = { type = "string" }, default = ["reader"] }
- }
- })
+ sensitive = {}
+ display_order = 40
}
- stackit_organization_onboarding_enabled = {
- display_name = "STACKIT Organization Onboarding Enabled"
- description = "If true, the nested STACKIT Project integration adds meshStack project users to the STACKIT organization before applying project-level role assignments."
- type = "BOOLEAN"
- assignment_type = "USER_INPUT"
- updateable_by_consumer = true
- default_value = jsonencode(true)
- display_order = 90
+ stackit_owner_email = {
+ display_name = "STACKIT Owner Email"
+ description = "Owner of the STACKIT folder, foundation project, and every tenant project. Applied at creation only. Must be the deployment service account's own email unless the deployment service account is an organization owner."
+ type = "STRING"
+ assignment_type = "USER_INPUT"
+ display_order = 50
}
topology = {
@@ -429,7 +356,7 @@ resource "meshstack_building_block_definition" "this" {
assignment_type = "USER_INPUT"
selectable_values = ["sandbox", "hub&spoke"]
default_value = jsonencode(["sandbox"])
- display_order = 20
+ display_order = 60
}
network = {
@@ -439,10 +366,9 @@ resource "meshstack_building_block_definition" "this" {
assignment_type = "USER_INPUT"
updateable_by_consumer = true
condition = "\"hub&spoke\" in input.topology"
- display_order = 30
+ display_order = 70
- # Decodes into var.network's object; hidden for the sandbox topology, so var.network falls
- # back to its null default and the buildingblock leaves networking off.
+ # Hidden for the sandbox topology, so var.network falls back to null and networking stays off.
json_schema = jsonencode({
"$schema" = "http://json-schema.org/draft-07/schema#"
type = "object"
@@ -460,102 +386,110 @@ resource "meshstack_building_block_definition" "this" {
})
}
- workspace = {
- display_name = "Workspace Identifier"
- description = "Workspace that will own the created platform, location and landing zones."
- type = "STRING"
- assignment_type = "WORKSPACE_IDENTIFIER"
- display_order = 110
- }
+ role_mapping = {
+ display_name = "STACKIT Project Role Mapping"
+ description = "Maps each meshStack project role to the STACKIT project roles it grants. Values can be built-in STACKIT roles or custom STACKIT role names. organization_onboarding adds the users to the STACKIT organization first."
+ type = "JSON"
+ assignment_type = "USER_INPUT"
+ updateable_by_consumer = true
+ display_order = 80
- platform_identifier = {
- display_name = "Platform Identifier"
- description = "Identifier for the STACKIT sandbox platform created in meshStack (letters, digits and dashes only)."
- type = "STRING"
- assignment_type = "USER_INPUT"
- value_validation_regex = "^[a-zA-Z0-9-]+$"
- validation_regex_error_message = "platform_identifier must only contain letters, digits, and dashes."
- display_order = 10
+ json_schema = jsonencode({
+ "$schema" = "http://json-schema.org/draft-07/schema#"
+ type = "object"
+ required = ["admin", "user", "reader"]
+ properties = {
+ admin = { type = "array", title = "admin", items = { type = "string" }, default = ["owner"] }
+ user = { type = "array", title = "user", items = { type = "string" }, default = ["editor"] }
+ reader = { type = "array", title = "reader", items = { type = "string" }, default = ["reader"] }
+ organization_onboarding = {
+ type = "boolean"
+ title = "organization_onboarding"
+ default = true
+ description = "Add meshStack project users to the STACKIT organization before the project roles are granted. Turn off if organization membership is managed outside this landing zone."
+ }
+ }
+ })
}
- root_folder_name = {
- display_name = "Root Folder Name"
- description = "Folder at the organization root for the landing-zone folder and the foundation project, when the organization serves more than one meshStack. Empty: no root folder."
- type = "STRING"
+ tags = {
+ display_name = "Tags"
+ description = "Tags forwarded to the nested integrations. Build them in the form: add {key, values} entries for landing zones, building blocks, and the created meshProjects, plus the project owner tag key."
+ type = "JSON"
assignment_type = "USER_INPUT"
- is_optional = true
updateable_by_consumer = true
- display_order = 45
- }
+ display_order = 100
- use_global_location = {
- display_name = "Use Global Location"
- description = "If true, use the existing global meshStack location instead of creating a dedicated location for this platform."
- type = "BOOLEAN"
- assignment_type = "USER_INPUT"
- default_value = jsonencode(false)
- display_order = 100
+ json_schema = jsonencode(local.tags_json_schema)
}
starterkit_approval_policies = {
- display_name = "Starterkit Approval Policies"
- description = "HCL object of approval gates applied to the project starterkit definition this registers. Fixed by whoever deployed this definition."
- type = "CODE"
- assignment_type = "STATIC"
- argument = jsonencode(jsonencode(var.starterkit_approval_policies))
- display_order = 130
+ display_name = "Starterkit Approval Policies"
+ description = "Run triggers that need an operator's approval before a run of a project starterkit building block is applied."
+ type = "JSON"
+ assignment_type = "USER_INPUT"
+ updateable_by_consumer = true
+ default_value = jsonencode(jsonencode(local.approval_policies_default))
+ condition = "!input.playground_mode"
+ display_order = 105
+
+ json_schema = jsonencode({
+ type = "object"
+ additionalProperties = false
+ required = local.approval_gates
+ properties = { for gate, enabled in local.approval_policies_default : gate => { type = "boolean", title = gate, default = enabled } }
+ })
}
imports = {
display_name = "Imports"
- description = "Existing resources to take over when adopting a platform built before this architecture; see Adopting an Existing Platform in the README. Leave empty to create everything."
+ description = "Existing resources to take over when adopting a platform built before this architecture; see Adopting an Existing Platform in the README. Leave out to create everything."
type = "JSON"
assignment_type = "USER_INPUT"
is_optional = true
- display_order = 150
+ display_order = 110
json_schema = jsonencode({
"$schema" = "http://json-schema.org/draft-07/schema#"
type = "object"
additionalProperties = false
properties = {
- folder = {
- type = "object"
- title = "Landing-Zone Folder"
- required = ["container_id"]
- properties = {
- container_id = { type = "string", title = "Container ID" }
- }
- }
- foundation_project = {
- type = "object"
- title = "Foundation Project"
- required = ["container_id"]
- properties = {
- container_id = { type = "string", title = "Container ID" }
- }
+ folder = merge(local.imports_container_schema, { title = "folder" })
+ foundation_project = merge(local.imports_container_schema, { title = "foundation_project" })
+ adopt_platform = {
+ type = "boolean"
+ title = "adopt_platform"
+ description = "Adopt the existing meshPlatform, location and landing zones named by Platform Identifier."
}
- platform = { type = "object", title = "Location, Platform and Landing Zones", properties = {} }
project_definition = {
type = "object"
+ title = "project_definition"
required = concat(keys(local.imports_definition_properties), ["service_account_name"])
properties = merge(local.imports_definition_properties, {
- service_account_name = { type = "string", title = "Backplane Service Account Name" }
+ service_account_name = { type = "string", title = "service_account_name" }
})
}
- service_account_definition = local.imports_definition_schema
- service_account_federation_definition = local.imports_definition_schema
+ service_account_definition = merge(local.imports_definition_schema, { title = "service_account_definition" })
+ service_account_federation_definition = merge(local.imports_definition_schema, { title = "service_account_federation_definition" })
}
})
}
- playground_mode = {
- display_name = "Playground Mode"
- description = "Throwaway deployment: the identifier gets a random suffix and nothing is protected against deletion. Do not publish such a platform or its definitions to other workspaces. Set false for real use."
- type = "BOOLEAN"
+ workspace = {
+ display_name = "Workspace Identifier"
+ description = "Workspace that will own the created platform, location and landing zones."
+ type = "STRING"
+ assignment_type = "WORKSPACE_IDENTIFIER"
+ display_order = 900
+ }
+
+ hub = {
+ display_name = "Hub"
+ description = "HCL object with `git_ref` (meshstack-hub reference used to source the nested STACKIT integration modules) and `bbd_draft` (draft state of the definitions this landing zone registers)."
+ type = "CODE"
assignment_type = "STATIC"
- argument = jsonencode(var.playground_mode)
- display_order = 140
+ argument = jsonencode(jsonencode(var.hub))
+ display_order = 1000
}
}
diff --git a/tools/scorecard/scorecard.mjs b/tools/scorecard/scorecard.mjs
index f6e81216..1113f2bf 100644
--- a/tools/scorecard/scorecard.mjs
+++ b/tools/scorecard/scorecard.mjs
@@ -1554,9 +1554,37 @@ const detectors = [
},
},
{
- id: "bbd_optional_over_default",
+ id: "bbd_input_order",
category: "shared",
- name: "BBD inputs do not repeat the variable's default in default_value",
+ name: "BBD inputs and their variables are declared in display order",
+ emoji: "🔢",
+ targets: ["module", "refArch"],
+ needsCode: true,
+ fn: (target) => {
+ const offenders = [];
+ let checked = 0;
+ for (const { where, bbd, inputs, variables } of bbdsWithImplementation(target)) {
+ const orders = new Map(inputs.map(([key, entry]) => [key, displayOrder(entry)]).filter(([, n]) => !Number.isNaN(n)));
+ if (orders.size === 0) continue;
+ checked++;
+ if (!isAscending([...orders.values()])) offenders.push(`${where}: ${bbd} inputs`);
+ const tail = inputs
+ .filter(([key]) => orders.has(key))
+ .toSorted(([a], [b]) => orders.get(a) - orders.get(b))
+ .map(([key, entry]) => (key === "hub" ? 2 : /^(USER_INPUT|PAYMENT_METHOD|PLATFORM_OPERATOR_MANUAL_INPUT|STATIC)$/.test(inputAttribute(entry, "assignment_type")) ? 0 : 1));
+ if (!isAscending(tail)) offenders.push(`${where}: ${bbd} generated inputs and hub are not last`);
+ if (!isAscending([...variables.keys()].filter((key) => orders.has(key)).map((key) => orders.get(key)))) {
+ offenders.push(`${where}: ${bbd} buildingblock variables`);
+ }
+ }
+ if (checked === 0) return { pass: null, detail: "no definition inputs with a display_order" };
+ return listOffenders(offenders, "declare the inputs by ascending display_order, generated inputs and then hub last, and the variables they feed in the same order");
+ },
+ },
+ {
+ id: "bbd_input_defaults",
+ category: "shared",
+ name: "The definition owns input defaults: variables fed by required inputs have no default and are not nullable, no \"\" as unset",
emoji: "🪞",
targets: ["module", "refArch"],
needsCode: true,
@@ -1564,16 +1592,20 @@ const detectors = [
const offenders = [];
let checked = 0;
for (const { where, bbd, key, entry, variable } of bbdInputsWithVariables(target)) {
- const inputDefault = entry.match(/^\s*default_value\s*=\s*jsonencode\((.*)\)\s*$/m)?.[1];
- if (inputDefault === undefined || inputAttribute(entry, "type") === "BOOLEAN") continue;
checked++;
- const variableDefault = variable.match(/^\s*default\s*=\s*(.+?)\s*$/m)?.[1];
- if (isPlainLiteral(inputDefault) && normalizeLiteral(inputDefault) === normalizeLiteral(variableDefault ?? "")) {
- offenders.push(`${where}: ${bbd} input "${key}"`);
+ if (/^\s*(is_optional\s*=\s*true\b|condition\s*=)/m.test(entry)) continue;
+ if (/^\s*const\s*=\s*true\b/m.test(variable)) continue;
+ if (/^\s*default\s*=/m.test(variable)) offenders.push(`${where}: ${bbd} input "${key}" (required, but the variable has a default)`);
+ if (!/^\s*nullable\s*=\s*false\b/m.test(variable)) offenders.push(`${where}: ${bbd} input "${key}" (required, but the variable is nullable)`);
+ }
+ for (const { where, bbd, inputs, code } of bbdsWithImplementation(target)) {
+ for (const [key] of inputs) {
+ const emptyString = new RegExp(String.raw`coalesce\(\s*var\.${key}\s*,\s*""|var\.${key}\s*[!=]=\s*""|compact\(\s*\[\s*var\.${key}\b`);
+ if (emptyString.test(code)) offenders.push(`${where}: ${bbd} input "${key}" (handles "" as unset)`);
}
}
- if (checked === 0) return { pass: null, detail: "no non-BOOLEAN inputs with a default_value backed by a variable" };
- return listOffenders(offenders, "drop default_value and set is_optional = true, so the variable's default stays the only one");
+ if (checked === 0) return { pass: null, detail: "no definition inputs backed by a variable" };
+ return listOffenders(offenders, "put a sane default into default_value and give the variable of a required input no default and nullable = false, or make the input optional and check only for null");
},
},
@@ -2318,6 +2350,14 @@ function bbdEntries(bbdBlock, attribute) {
// meshStack passes each input to the Terraform variable of the same name in the definition's
// repository_path, which every definition in this repo sets as a literal.
function bbdInputsWithVariables(target) {
+ return bbdsWithImplementation(target).flatMap(({ where, bbd, inputs, variables }) =>
+ inputs.filter(([key]) => variables.has(key)).map(([key, entry]) => ({ where, bbd, key, entry, variable: variables.get(key) }))
+ );
+}
+
+// Every definition whose repository_path is in this repo, with its inputs in source order and the
+// code of its implementation directory.
+function bbdsWithImplementation(target) {
const found = [];
for (const { file, content } of readCodeFiles(target)) {
for (const [bbd, body] of extractResourceBlocks(content, "meshstack_building_block_definition")) {
@@ -2325,23 +2365,18 @@ function bbdInputsWithVariables(target) {
if (!path) continue;
const implementation = join(ROOT, path);
if (!existsSync(implementation)) continue;
- const variables = extractVariableBlocks(
- readdirSync(implementation)
- .filter((f) => f.endsWith(".tf"))
- .map((f) => readFileSync(join(implementation, f), "utf-8"))
- .join("\n")
- );
- for (const [key, entry] of bbdEntries(body, "inputs")) {
- const variable = variables.get(key);
- if (variable) found.push({ where: relative(target.path, file), bbd, key, entry, variable });
- }
+ const code = readdirSync(implementation)
+ .filter((f) => f.endsWith(".tf"))
+ .map((f) => readFileSync(join(implementation, f), "utf-8"))
+ .join("\n");
+ found.push({ where: relative(target.path, file), bbd, inputs: bbdEntries(body, "inputs"), variables: extractVariableBlocks(code), code });
}
}
return found;
}
-const isPlainLiteral = (text) => /^("([^"\\$]|\\.)*"|-?\d+(\.\d+)?|true|false|\[\s*\]|\{\s*\})$/.test(text.trim());
-const normalizeLiteral = (text) => text.replace(/\s+/g, "");
+const displayOrder = (entry) => Number(entry.match(/^\s*display_order\s*=\s*(\d+)/m)?.[1] ?? NaN);
+const isAscending = (numbers) => numbers.every((n, i) => i === 0 || numbers[i - 1] <= n);
function moduleSourceDirs(file, content) {
const dirs = [];