From c7f6d9a0f74a17c366556ad36cb5e83f389e9306 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 11:57:16 +0100 Subject: [PATCH 01/28] feat(ref-arch/stackit-kubernetes): add a summary to the cluster, Git, ingress, connector and service account blocks The SKE Cluster, STACKIT Git Instance, Kubernetes Ingress, SKE Forgejo Connector and Kubernetes Service Account definitions declare a `summary` output. Each shows names, links into the STACKIT portal or Forgejo, and the Vault paths of the secrets it writes or reads. Co-Authored-By: Claude Opus 5.5 --- .../ingress/buildingblock/README.md | 1 + .../ingress/buildingblock/outputs.tf | 46 +++++++++++++++++++ .../ingress/meshstack_integration.tf | 6 +++ .../service-account/buildingblock/README.md | 1 + .../buildingblock/output_to_vault.tf | 1 + .../service-account/buildingblock/outputs.tf | 16 +++++++ .../service-account/meshstack_integration.tf | 6 +++ modules/ske/cluster/buildingblock/README.md | 1 + .../cluster/buildingblock/output_to_vault.tf | 2 + modules/ske/cluster/buildingblock/outputs.tf | 39 +++++++++++++++- modules/ske/cluster/meshstack_integration.tf | 6 +++ .../forgejo-connector/buildingblock/README.md | 1 + .../buildingblock/outputs.tf | 38 +++++++++++++++ .../meshstack_integration.tf | 6 +++ modules/stackit/git/buildingblock/README.md | 1 + .../git/buildingblock/SUMMARY.md.tftpl | 28 +++++++++++ .../git/buildingblock/output_to_vault.tf | 1 + modules/stackit/git/buildingblock/outputs.tf | 27 +++++++++-- modules/stackit/git/meshstack_integration.tf | 6 +++ 19 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 modules/stackit/git/buildingblock/SUMMARY.md.tftpl diff --git a/modules/kubernetes/ingress/buildingblock/README.md b/modules/kubernetes/ingress/buildingblock/README.md index 584a0f030..933cf9c4f 100644 --- a/modules/kubernetes/ingress/buildingblock/README.md +++ b/modules/kubernetes/ingress/buildingblock/README.md @@ -141,6 +141,7 @@ puts Helm's ownership label and annotations on the ClusterIssuer, so the release | [haproxy\_lb\_ip](#output\_haproxy\_lb\_ip) | External IP of the HAProxy LoadBalancer service. Point application DNS A records here before TLS provisioning can complete. | | [haproxy\_namespace](#output\_haproxy\_namespace) | Namespace of the HAProxy ingress controller and of the wildcard certificate secret. | | [ingress\_class\_name](#output\_ingress\_class\_name) | Name of the IngressClass an application puts on its Ingress to be served by this controller. | +| [summary](#output\_summary) | Markdown summary shown on the building block. | | [wildcard\_certificate\_domain](#output\_wildcard\_certificate\_domain) | Domain the wildcard certificate `*.` covers; null without `dns01`. | | [wildcard\_certificate\_secret\_name](#output\_wildcard\_certificate\_secret\_name) | Secret in `haproxy_namespace` holding the wildcard certificate; null without `dns01`. | diff --git a/modules/kubernetes/ingress/buildingblock/outputs.tf b/modules/kubernetes/ingress/buildingblock/outputs.tf index fe859943c..1dffd2f6f 100644 --- a/modules/kubernetes/ingress/buildingblock/outputs.tf +++ b/modules/kubernetes/ingress/buildingblock/outputs.tf @@ -35,3 +35,49 @@ output "wildcard_certificate_secret_name" { depends_on = [module.issuer] } + +output "summary" { + description = "Markdown summary shown on the building block." + value = <<-EOT + # Kubernetes Ingress + + | Property | Value | + |----------|-------| + | **Load Balancer IP** | `${data.kubernetes_service_v1.haproxy_controller.status[0].load_balancer[0].ingress[0].ip}` | + | **IngressClass** | `${var.ingress_class_name}` | + | **ClusterIssuer** | `${var.cluster_issuer_name}`, registered at `${var.acme_server}` | + | **Certificates** | ${local.dns01_enabled ? "wildcard `*.${local.dns01_certificate_domain}` in `${module.haproxy_namespace.name}/${var.wildcard_certificate_name}`, served by default" : "one per hostname, over HTTP-01"} | + | **HAProxy** | chart `${var.haproxy_version}`, ${var.haproxy_replica_count} replicas in `${module.haproxy_namespace.name}` | + | **cert-manager** | chart `${var.cert_manager_version}` in `${module.cert_manager_namespace.name}` | + + Point the DNS records of your hostnames at the load balancer IP. An Ingress served by this + controller, with a certificate from Let's Encrypt: + + ```yaml + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: my-app + annotations: + cert-manager.io/cluster-issuer: ${var.cluster_issuer_name} + spec: + ingressClassName: ${var.ingress_class_name} + tls: + - hosts: [my-app.example.com] + secretName: my-app-tls + rules: + - host: my-app.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: my-app + port: + number: 80 + ``` + EOT + + depends_on = [module.haproxy, module.issuer] +} diff --git a/modules/kubernetes/ingress/meshstack_integration.tf b/modules/kubernetes/ingress/meshstack_integration.tf index a2ace23e9..049177e26 100644 --- a/modules/kubernetes/ingress/meshstack_integration.tf +++ b/modules/kubernetes/ingress/meshstack_integration.tf @@ -309,6 +309,12 @@ resource "meshstack_building_block_definition" "this" { type = "STRING" assignment_type = "NONE" } + + summary = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } } permissions = [] diff --git a/modules/kubernetes/service-account/buildingblock/README.md b/modules/kubernetes/service-account/buildingblock/README.md index 4ff56b8e3..267dd1ff6 100644 --- a/modules/kubernetes/service-account/buildingblock/README.md +++ b/modules/kubernetes/service-account/buildingblock/README.md @@ -72,5 +72,6 @@ No modules. |------|-------------| | [instructions](#output\_instructions) | Instructions for using the kubeconfig | | [kubeconfig](#output\_kubeconfig) | Kubeconfig file content for authenticating with the Kubernetes cluster. Empty when `output_to_vault` is set. | +| [summary](#output\_summary) | Markdown summary shown on the building block. | | [vault\_secret](#output\_vault\_secret) | `{path, secret_hash}` of the secret in Vault, where `secret_hash` changes with its content; `{}` without `output_to_vault`. | diff --git a/modules/kubernetes/service-account/buildingblock/output_to_vault.tf b/modules/kubernetes/service-account/buildingblock/output_to_vault.tf index 2420ac671..602499fac 100644 --- a/modules/kubernetes/service-account/buildingblock/output_to_vault.tf +++ b/modules/kubernetes/service-account/buildingblock/output_to_vault.tf @@ -1,6 +1,7 @@ locals { write_to_vault = nonsensitive(var.output_to_vault != null) vault_secret_path = local.write_to_vault ? "${nonsensitive(var.output_to_vault.path_prefix)}kubernetes/service-account" : null + vault_mount = local.write_to_vault ? nonsensitive(var.output_to_vault.mount) : null vault_data_json = jsonencode({ kubeconfig = yamlencode(local.kubeconfig) }) diff --git a/modules/kubernetes/service-account/buildingblock/outputs.tf b/modules/kubernetes/service-account/buildingblock/outputs.tf index 795221a49..d61533346 100644 --- a/modules/kubernetes/service-account/buildingblock/outputs.tf +++ b/modules/kubernetes/service-account/buildingblock/outputs.tf @@ -52,3 +52,19 @@ output "vault_secret" { } : key => value if local.write_to_vault } } + +output "summary" { + description = "Markdown summary shown on the building block." + value = <<-EOT + # Kubernetes Service Account: **${kubernetes_service_account.this.metadata[0].name}** + + | Property | Value | + |----------|-------| + | **Service Account** | `${var.namespace}/${kubernetes_service_account.this.metadata[0].name}` | + | **Role** | `${var.cluster_role}`, ${var.bind_cluster_wide ? "in every namespace" : "in `${var.namespace}`"} | + | **Cluster** | `${var.cluster_name}` at `${local.kubeconfig_cluster.server}` | + | **Kubeconfig** | ${local.write_to_vault ? "key `kubeconfig` of the Vault secret `${local.vault_secret_path}` on mount `${local.vault_mount}`" : "the **Kubeconfig** output of this building block"} | + + Save the kubeconfig to a file and use it with `kubectl --kubeconfig kubeconfig get pods`. + EOT +} diff --git a/modules/kubernetes/service-account/meshstack_integration.tf b/modules/kubernetes/service-account/meshstack_integration.tf index c3771a1a0..3eebfaa64 100644 --- a/modules/kubernetes/service-account/meshstack_integration.tf +++ b/modules/kubernetes/service-account/meshstack_integration.tf @@ -279,6 +279,12 @@ resource "meshstack_building_block_definition" "this" { type = "STRING" assignment_type = "NONE" } + + summary = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } } permissions = [] diff --git a/modules/ske/cluster/buildingblock/README.md b/modules/ske/cluster/buildingblock/README.md index 36cb5c147..243efb413 100644 --- a/modules/ske/cluster/buildingblock/README.md +++ b/modules/ske/cluster/buildingblock/README.md @@ -72,5 +72,6 @@ place. `release_on_destroy` cannot change once the cluster is managed. | [cluster\_name](#output\_cluster\_name) | Name of the SKE cluster. | | [cluster\_url](#output\_cluster\_url) | Deep link to the STACKIT project the cluster lives in, in the STACKIT portal. | | [kube\_host](#output\_kube\_host) | Kubernetes API server URL of the cluster. | +| [summary](#output\_summary) | Markdown summary shown on the building block. | | [vault\_secret](#output\_vault\_secret) | `{path, secret_hash}` of the secret in Vault, where `secret_hash` changes with its content. | diff --git a/modules/ske/cluster/buildingblock/output_to_vault.tf b/modules/ske/cluster/buildingblock/output_to_vault.tf index 4371406f0..6559bb2eb 100644 --- a/modules/ske/cluster/buildingblock/output_to_vault.tf +++ b/modules/ske/cluster/buildingblock/output_to_vault.tf @@ -1,4 +1,6 @@ locals { + vault_address = nonsensitive(var.output_to_vault.address) + vault_mount = nonsensitive(var.output_to_vault.mount) vault_secret_path = "${nonsensitive(var.output_to_vault.path_prefix)}ske/cluster" vault_data_json = jsonencode({ kubeconfig = local.admin_kubeconfig }) diff --git a/modules/ske/cluster/buildingblock/outputs.tf b/modules/ske/cluster/buildingblock/outputs.tf index e12371a9f..4b517e4f8 100644 --- a/modules/ske/cluster/buildingblock/outputs.tf +++ b/modules/ske/cluster/buildingblock/outputs.tf @@ -3,9 +3,13 @@ output "cluster_name" { value = module.cluster.cluster.name } +locals { + cluster_url = "https://portal.stackit.cloud/projects/${var.stackit_project_id}/ske/clusters" +} + output "cluster_url" { description = "Deep link to the STACKIT project the cluster lives in, in the STACKIT portal." - value = "https://portal.stackit.cloud/projects/${var.stackit_project_id}/ske/clusters" + value = local.cluster_url } output "kube_host" { @@ -20,3 +24,36 @@ output "vault_secret" { secret_hash = local.vault_secret_hash } } + +output "summary" { + description = "Markdown summary shown on the building block." + value = <<-EOT + # SKE Cluster: **${module.cluster.cluster.name}** + + | Property | Value | + |----------|-------| + | **Cluster** | [Open in STACKIT Portal](${local.cluster_url}) | + | **Kubernetes** | `${module.cluster.cluster.kubernetes_version_used}` | + | **API Server** | `${terraform_data.kube_host.output}` | + | **Node Pool** | `${var.node_pool.name}`: ${var.node_pool.minimum} to ${var.node_pool.maximum} × `${var.node_pool.machine_type}` in ${join(", ", var.node_pool.availability_zones)} | + | **Maintenance** | daily `${var.maintenance.start}` to `${var.maintenance.end}` | + + ## Platform admin kubeconfig + + The service account `kube-system/${local.admin_name}` is bound to `cluster-admin`. Its kubeconfig is + the key `kubeconfig` of the Vault secret `${local.vault_secret_path}` on mount `${local.vault_mount}`. + The token belongs to generation `${var.admin_token_generation}`; the one of the month before stays valid. + + ```sh + export VAULT_ADDR=${local.vault_address} + export VAULT_TOKEN=$(stackit auth get-access-token) + vault kv get -mount=${local.vault_mount} -field=kubeconfig ${local.vault_secret_path} > kubeconfig + ``` + + For your own access, STACKIT issues a kubeconfig that expires: + + ```sh + stackit ske kubeconfig create ${module.cluster.cluster.name} --project-id ${var.stackit_project_id} + ``` + EOT +} diff --git a/modules/ske/cluster/meshstack_integration.tf b/modules/ske/cluster/meshstack_integration.tf index 0c58dddfe..a378d44fc 100644 --- a/modules/ske/cluster/meshstack_integration.tf +++ b/modules/ske/cluster/meshstack_integration.tf @@ -289,6 +289,12 @@ resource "meshstack_building_block_definition" "this" { type = "STRING" assignment_type = "RESOURCE_URL" } + + summary = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } } permissions = [] diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index e8ef3d3d7..e53646b0e 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -114,4 +114,5 @@ Secret of its own service account. | Name | Description | |------|-------------| | [app\_link](#output\_app\_link) | Public URL for this stage application. | +| [summary](#output\_summary) | Markdown summary shown on the building block. | diff --git a/modules/ske/forgejo-connector/buildingblock/outputs.tf b/modules/ske/forgejo-connector/buildingblock/outputs.tf index b39bdc0b2..a6e4a82b5 100644 --- a/modules/ske/forgejo-connector/buildingblock/outputs.tf +++ b/modules/ske/forgejo-connector/buildingblock/outputs.tf @@ -2,3 +2,41 @@ output "app_link" { description = "Public URL for this stage application." value = "https://${var.app_hostname}" } + +locals { + repository_url = "${trimsuffix(data.external.env.result["FORGEJO_HOST"], "/")}/${var.repository_owner}/${var.repository_name}" +} + +output "summary" { + description = "Markdown summary shown on the building block." + value = <<-EOT + # Forgejo Connector: **${var.stage}** + + The `${var.stage}` branch of [${var.repository_owner}/${var.repository_name}](${local.repository_url}) deploys + to the namespace `${var.namespace}`. + + | Property | Value | + |----------|-------| + | **App** | [https://${var.app_hostname}](https://${var.app_hostname}) | + | **Pipeline** | [`pipeline.yaml` in Forgejo Actions](${local.repository_url}/actions?workflow=pipeline.yaml) | + | **Service Account** | `${kubernetes_service_account.forgejo_actions.metadata[0].name}`, `edit` on the namespace | + + ## Forgejo Actions secrets and variables + + Set in the [repository settings](${local.repository_url}/settings/actions/secrets): + + | Name | Kind | Holds | + |------|------|-------| + | `KUBECONFIG_${upper(var.stage)}` | secret | kubeconfig of the service account | + | `K8S_NAMESPACE_${upper(var.stage)}` | variable | `${var.namespace}` | + | `APP_HOSTNAME_${upper(var.stage)}` | variable | `${var.app_hostname}` | + + ## Kubernetes secrets in the namespace + + | Secret | Filled from the Vault secret | + |--------|------------------------------| + | `${kubernetes_secret.image_pull.metadata[0].name}`, the image pull secret of `default` | `${var.registry_secret_path}` |${join("", [for name, path in var.additional_kubernetes_secrets : "\n| `${name}` | `${path}` |"])} + + A changed Vault value reaches these secrets with the next increase of **Secrets Revision**. + EOT +} diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 2026727cb..d68aa40c0 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -325,6 +325,12 @@ resource "meshstack_building_block_definition" "this" { display_name = "Open App" type = "STRING" } + + "summary" = { + assignment_type = "SUMMARY" + display_name = "Summary" + type = "STRING" + } } } } diff --git a/modules/stackit/git/buildingblock/README.md b/modules/stackit/git/buildingblock/README.md index ab812135f..c0f2aaf92 100644 --- a/modules/stackit/git/buildingblock/README.md +++ b/modules/stackit/git/buildingblock/README.md @@ -165,5 +165,6 @@ With `release_on_destroy` set, destroying the building block leaves the instance | [instance\_url](#output\_instance\_url) | URL of the Forgejo instance. | | [local\_user\_username](#output\_local\_user\_username) | Username of the technical user the token belongs to. | | [organization\_url](#output\_organization\_url) | URL of the Forgejo organization. Falls back to the instance URL when no organization was asked for. | +| [summary](#output\_summary) | Markdown summary shown on the building block. | | [vault\_secret](#output\_vault\_secret) | `{path, secret_hash}` of the secret in Vault, where `secret_hash` changes with its content; `{}` without `output_to_vault`. | diff --git a/modules/stackit/git/buildingblock/SUMMARY.md.tftpl b/modules/stackit/git/buildingblock/SUMMARY.md.tftpl new file mode 100644 index 000000000..b5031ce06 --- /dev/null +++ b/modules/stackit/git/buildingblock/SUMMARY.md.tftpl @@ -0,0 +1,28 @@ +# STACKIT Git: **${instance_name}** + +| Property | Value | +|----------|-------| +| **Forgejo** | [${instance_url}](${instance_url}) | +%{ if forgejo_organization != null ~} +| **Organization** | [`${forgejo_organization}`](${organization_url}) | +%{ endif ~} +| **Technical User** | `${local_user_username}` | +| **Shared Runner** | ${length(shared_runner_labels) > 0 ? "`${join("`, `", shared_runner_labels)}`" : "none"} | +%{ if vault_secret_path != null ~} +| **API Token** | key `forgejo-api-token` of the Vault secret `${vault_secret_path}` on mount `${vault_mount}` | +%{ endif ~} +%{ if length(members) > 0 ~} + +## Organization members + +| Member | Role | Forgejo user | +|--------|------|--------------| +%{ for member in members ~} +| `${member.email}` | `${member.role}` | ${member.username != "" ? "✅ `${member.username}`" : "⚠️ not signed in yet"} | +%{ endfor ~} +%{ if length([for member in members : member if member.username == ""]) > 0 ~} + +A member joins their team once they have signed in to [${instance_url}](${instance_url}) with their +STACKIT account. After they have signed in, re-run this building block. +%{ endif ~} +%{ endif ~} diff --git a/modules/stackit/git/buildingblock/output_to_vault.tf b/modules/stackit/git/buildingblock/output_to_vault.tf index 2db2dc3ed..1e3a37292 100644 --- a/modules/stackit/git/buildingblock/output_to_vault.tf +++ b/modules/stackit/git/buildingblock/output_to_vault.tf @@ -1,6 +1,7 @@ locals { write_to_vault = nonsensitive(var.output_to_vault != null) vault_secret_path = local.write_to_vault ? "${nonsensitive(var.output_to_vault.path_prefix)}stackit/git" : null + vault_mount = local.write_to_vault ? nonsensitive(var.output_to_vault.mount) : null vault_data_json = jsonencode({ "forgejo-api-token" = local.forgejo_api_token }) diff --git a/modules/stackit/git/buildingblock/outputs.tf b/modules/stackit/git/buildingblock/outputs.tf index 1394d3a81..89e8e79e3 100644 --- a/modules/stackit/git/buildingblock/outputs.tf +++ b/modules/stackit/git/buildingblock/outputs.tf @@ -13,13 +13,17 @@ output "instance_url" { value = module.instance.instance.url } -output "organization_url" { - description = "URL of the Forgejo organization. Falls back to the instance URL when no organization was asked for." - value = local.create_forgejo_organization ? format( +locals { + organization_url = local.create_forgejo_organization ? format( "%s/%s", trimsuffix(module.instance.instance.url, "/"), var.forgejo_organization ) : module.instance.instance.url } +output "organization_url" { + description = "URL of the Forgejo organization. Falls back to the instance URL when no organization was asked for." + value = local.organization_url +} + output "forgejo_organization" { description = "Name of the Forgejo organization for this instance. Empty when none was asked for." # The name, not a claim that the organization exists: meshStack fails a building block whose @@ -42,3 +46,20 @@ output "vault_secret" { } : key => value if local.write_to_vault } } + +output "summary" { + description = "Markdown summary shown on the building block." + value = templatefile("${path.module}/SUMMARY.md.tftpl", { + instance_name = module.instance.instance.name + instance_url = module.instance.instance.url + forgejo_organization = local.create_forgejo_organization ? var.forgejo_organization : null + organization_url = local.organization_url + local_user_username = local.local_user_username + shared_runner_labels = var.shared_runner_labels + vault_secret_path = local.vault_secret_path + vault_mount = local.vault_mount + members = [ + for email, role in local.member_roles : { email = email, role = role, username = lookup(local.forgejo_usernames, email, "") } + ] + }) +} diff --git a/modules/stackit/git/meshstack_integration.tf b/modules/stackit/git/meshstack_integration.tf index bfbb383d3..9b89d428d 100644 --- a/modules/stackit/git/meshstack_integration.tf +++ b/modules/stackit/git/meshstack_integration.tf @@ -347,6 +347,12 @@ resource "meshstack_building_block_definition" "this" { type = "STRING" assignment_type = "NONE" } + + summary = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } } permissions = [] From 1a07877c7c73568be5c04c8cec3d5b0c5d5c7a6e Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 11:57:24 +0100 Subject: [PATCH 02/28] docs(ref-arch/stackit-kubernetes): explain how the starter kit reaches application teams Co-Authored-By: Claude Opus 5.5 --- .../stackit-kubernetes/README.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/reference-architectures/stackit-kubernetes/README.md b/reference-architectures/stackit-kubernetes/README.md index fb1996879..03ed34f74 100644 --- a/reference-architectures/stackit-kubernetes/README.md +++ b/reference-architectures/stackit-kubernetes/README.md @@ -174,6 +174,47 @@ export VAULT_TOKEN=$(stackit auth get-access-token) vault kv list -mount= / ``` +## Shipping the Starter Kit to Application Teams + +The [application-team diagram](#application-teams) shows the three definitions and what an order +creates. + +**Who registers it.** The nested **Platform Services** run registers the three definitions +application teams use, in `platform/buildingblock/starterkit.tf`. It does so only in phase 2, once +**Phase 2 Completed** is true, because their pipelines push images and their workloads pull them +with the robots phase 2 mints: + +1. **STACKIT Git Repository** creates a repository in the platform's Forgejo organization and + reads the Forgejo token and the push robot from the Secrets Manager. The **Starter Kit Identity + Federation** lets its runs act as the platform's service account. +2. **SKE Forgejo Connector** wires a repository to one namespace. It reads the admin kubeconfig, + the Forgejo token, the pull robot and the `stackit-ai` secret, and takes the repository from + the Git Repository building block it is a child of. +3. **SKE Starterkit** orders the other two. It gets the SKE platform, one landing zone per stage, + the DNS zone, the template repository and the version of each of the other two definitions. + +**Where it lives.** All three are owned by the workspace that ordered **STACKIT Kubernetes +Platform**, next to the platform's own definitions. They carry the `starterkit_building_block` +tags and the **Starterkit Approval Policies**. + +**When it is released.** `hub.bbd_draft` of this definition decides it, and only for these three. +With `true`, every Platform Services run updates their draft version, and only the owning +workspace can order them. Use it to try a Hub ref. With `false`, the run releases them, and +application teams see **SKE Starterkit** in the marketplace. A new Hub ref takes two runs: one with +`true` adds a draft version on top of the released one, one with `false` releases it. Existing +starter kits stay on their version until they are upgraded. + +**How an application team orders it.** The team orders **SKE Starterkit** in its own workspace +and gives a project name and a payment method. The run creates: + +- the Git repository, cloned from the template repository, +- one project per stage, each with a tenant on that stage's landing zone, +- one Forgejo Connector per tenant, which runs the pipeline for its stage, +- a Project Admin binding on every project for the user who ordered it. + +The app is served at `.` for `prod` and at `-.` for the +other stages. The starter kit's summary links each stage and the repository. + ## Adopting Existing Resources The optional **Imports** input takes over resources that already exist, for example a platform From 6cb490609c77ad806b9ebb32f9ea2c4a2c222c67 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 12:15:40 +0100 Subject: [PATCH 03/28] fix(ske/cluster): link the summary to the cluster's portal page The old link opened a portal path that does not exist. The summary now builds the cluster's overview URL itself, and the cluster_url output goes away because nothing else used it. Co-Authored-By: Claude Opus 5.5 --- modules/ske/cluster/buildingblock/README.md | 1 - modules/ske/cluster/buildingblock/outputs.tf | 11 +---------- modules/ske/cluster/meshstack_integration.tf | 6 ------ 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/modules/ske/cluster/buildingblock/README.md b/modules/ske/cluster/buildingblock/README.md index 243efb413..c3e8c61d3 100644 --- a/modules/ske/cluster/buildingblock/README.md +++ b/modules/ske/cluster/buildingblock/README.md @@ -70,7 +70,6 @@ place. `release_on_destroy` cannot change once the cluster is managed. | Name | Description | |------|-------------| | [cluster\_name](#output\_cluster\_name) | Name of the SKE cluster. | -| [cluster\_url](#output\_cluster\_url) | Deep link to the STACKIT project the cluster lives in, in the STACKIT portal. | | [kube\_host](#output\_kube\_host) | Kubernetes API server URL of the cluster. | | [summary](#output\_summary) | Markdown summary shown on the building block. | | [vault\_secret](#output\_vault\_secret) | `{path, secret_hash}` of the secret in Vault, where `secret_hash` changes with its content. | diff --git a/modules/ske/cluster/buildingblock/outputs.tf b/modules/ske/cluster/buildingblock/outputs.tf index 4b517e4f8..7f52ab29e 100644 --- a/modules/ske/cluster/buildingblock/outputs.tf +++ b/modules/ske/cluster/buildingblock/outputs.tf @@ -3,15 +3,6 @@ output "cluster_name" { value = module.cluster.cluster.name } -locals { - cluster_url = "https://portal.stackit.cloud/projects/${var.stackit_project_id}/ske/clusters" -} - -output "cluster_url" { - description = "Deep link to the STACKIT project the cluster lives in, in the STACKIT portal." - value = local.cluster_url -} - output "kube_host" { description = "Kubernetes API server URL of the cluster." value = terraform_data.kube_host.output @@ -32,7 +23,7 @@ output "summary" { | Property | Value | |----------|-------| - | **Cluster** | [Open in STACKIT Portal](${local.cluster_url}) | + | **Cluster** | [Open in STACKIT Portal](https://portal.stackit.cloud/kubernetes/clusters/details/${module.cluster.cluster.name}/overview?project=${var.stackit_project_id}) | | **Kubernetes** | `${module.cluster.cluster.kubernetes_version_used}` | | **API Server** | `${terraform_data.kube_host.output}` | | **Node Pool** | `${var.node_pool.name}`: ${var.node_pool.minimum} to ${var.node_pool.maximum} × `${var.node_pool.machine_type}` in ${join(", ", var.node_pool.availability_zones)} | diff --git a/modules/ske/cluster/meshstack_integration.tf b/modules/ske/cluster/meshstack_integration.tf index a378d44fc..83e611fd2 100644 --- a/modules/ske/cluster/meshstack_integration.tf +++ b/modules/ske/cluster/meshstack_integration.tf @@ -284,12 +284,6 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "NONE" } - cluster_url = { - display_name = "Open Cluster" - type = "STRING" - assignment_type = "RESOURCE_URL" - } - summary = { display_name = "Summary" type = "STRING" From db1c4930a37dc2916378f004ed2b61d469439f05 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 12:17:11 +0100 Subject: [PATCH 04/28] feat(ref-arch/stackit-kubernetes): open the summary links in a new tab meshPanel opens a Markdown link in the same tab, so the links in the cluster, Git and connector summaries are HTML anchors with target="_blank". Co-Authored-By: Claude Opus 5.5 --- modules/ske/cluster/buildingblock/outputs.tf | 2 +- modules/ske/forgejo-connector/buildingblock/outputs.tf | 8 ++++---- modules/stackit/git/buildingblock/SUMMARY.md.tftpl | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/ske/cluster/buildingblock/outputs.tf b/modules/ske/cluster/buildingblock/outputs.tf index 7f52ab29e..e0cc44c61 100644 --- a/modules/ske/cluster/buildingblock/outputs.tf +++ b/modules/ske/cluster/buildingblock/outputs.tf @@ -23,7 +23,7 @@ output "summary" { | Property | Value | |----------|-------| - | **Cluster** | [Open in STACKIT Portal](https://portal.stackit.cloud/kubernetes/clusters/details/${module.cluster.cluster.name}/overview?project=${var.stackit_project_id}) | + | **Cluster** | Open in STACKIT Portal | | **Kubernetes** | `${module.cluster.cluster.kubernetes_version_used}` | | **API Server** | `${terraform_data.kube_host.output}` | | **Node Pool** | `${var.node_pool.name}`: ${var.node_pool.minimum} to ${var.node_pool.maximum} × `${var.node_pool.machine_type}` in ${join(", ", var.node_pool.availability_zones)} | diff --git a/modules/ske/forgejo-connector/buildingblock/outputs.tf b/modules/ske/forgejo-connector/buildingblock/outputs.tf index a6e4a82b5..3b1992bfb 100644 --- a/modules/ske/forgejo-connector/buildingblock/outputs.tf +++ b/modules/ske/forgejo-connector/buildingblock/outputs.tf @@ -12,18 +12,18 @@ output "summary" { value = <<-EOT # Forgejo Connector: **${var.stage}** - The `${var.stage}` branch of [${var.repository_owner}/${var.repository_name}](${local.repository_url}) deploys + The `${var.stage}` branch of ${var.repository_owner}/${var.repository_name} deploys to the namespace `${var.namespace}`. | Property | Value | |----------|-------| - | **App** | [https://${var.app_hostname}](https://${var.app_hostname}) | - | **Pipeline** | [`pipeline.yaml` in Forgejo Actions](${local.repository_url}/actions?workflow=pipeline.yaml) | + | **App** | https://${var.app_hostname} | + | **Pipeline** | pipeline.yaml in Forgejo Actions | | **Service Account** | `${kubernetes_service_account.forgejo_actions.metadata[0].name}`, `edit` on the namespace | ## Forgejo Actions secrets and variables - Set in the [repository settings](${local.repository_url}/settings/actions/secrets): + Set in the repository settings: | Name | Kind | Holds | |------|------|-------| diff --git a/modules/stackit/git/buildingblock/SUMMARY.md.tftpl b/modules/stackit/git/buildingblock/SUMMARY.md.tftpl index b5031ce06..146040960 100644 --- a/modules/stackit/git/buildingblock/SUMMARY.md.tftpl +++ b/modules/stackit/git/buildingblock/SUMMARY.md.tftpl @@ -2,9 +2,9 @@ | Property | Value | |----------|-------| -| **Forgejo** | [${instance_url}](${instance_url}) | +| **Forgejo** | ${instance_url} | %{ if forgejo_organization != null ~} -| **Organization** | [`${forgejo_organization}`](${organization_url}) | +| **Organization** | ${forgejo_organization} | %{ endif ~} | **Technical User** | `${local_user_username}` | | **Shared Runner** | ${length(shared_runner_labels) > 0 ? "`${join("`, `", shared_runner_labels)}`" : "none"} | @@ -22,7 +22,7 @@ %{ endfor ~} %{ if length([for member in members : member if member.username == ""]) > 0 ~} -A member joins their team once they have signed in to [${instance_url}](${instance_url}) with their +A member joins their team once they have signed in to ${instance_url} with their STACKIT account. After they have signed in, re-run this building block. %{ endif ~} %{ endif ~} From 60e62efcf2e07427d297d397fd834e0dfe5c0180 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 12:19:06 +0100 Subject: [PATCH 05/28] feat(scorecard): check that a building block renders a summary with new-tab links summary_output fails a Terraform module whose integration declares no SUMMARY output or whose buildingblock/ has no `summary` output. summary_links_new_tab fails a summary that holds a Markdown link or an anchor without target="_blank" and rel="noopener". terraform-conventions.md documents both, and the module skill points at it. Co-Authored-By: Claude Opus 5.5 --- .agents/references/terraform-conventions.md | 8 +++ .agents/skills/module/SKILL.md | 1 + tools/scorecard/scorecard.mjs | 76 +++++++++++++++++++++ tools/scorecard/scorecard.test.mjs | 56 ++++++++++++++- 4 files changed, 140 insertions(+), 1 deletion(-) diff --git a/.agents/references/terraform-conventions.md b/.agents/references/terraform-conventions.md index c9fbbb215..74f86c3e9 100644 --- a/.agents/references/terraform-conventions.md +++ b/.agents/references/terraform-conventions.md @@ -94,6 +94,14 @@ The comment sits directly above the `try`, or above the attribute that owns the `try` is nested inside it. Prefer not needing it: a variable default, `optional()` or an explicit `null` check says the same thing without hiding the next error too. + +## Summary Output + +Every building block with an integration has a `summary` output, declared in the BBD with +`assignment_type = "SUMMARY"`, so meshPanel shows the result of a run. Write each link in it as +`…`: meshPanel opens a Markdown link in the same tab, +which takes the user out of the panel. + ## Output Content: Inline Heredoc Over a Template File Write a markdown output's content — the `summary` output most of all — **inline with a `<<-EOT` diff --git a/.agents/skills/module/SKILL.md b/.agents/skills/module/SKILL.md index f3692a7fe..bcc7c99e2 100644 --- a/.agents/skills/module/SKILL.md +++ b/.agents/skills/module/SKILL.md @@ -29,6 +29,7 @@ conventions, see the reference files in `.agents/references/`. ``` 3. **Implement `buildingblock/`** — `main.tf`, `variables.tf`, `outputs.tf`, `versions.tf`, `provider.tf`, `README.md` (with YAML front-matter), `logo.png` or `logo.svg` + - `outputs.tf` has a `summary` output (see [Summary Output](../../references/terraform-conventions.md#summary-output)) 4. **Implement `backplane/`** (if needed) — read the provider-specific reference: - AWS → `.agents/references/aws-backplane.md` diff --git a/tools/scorecard/scorecard.mjs b/tools/scorecard/scorecard.mjs index 1113f2bfe..19ade3ba8 100644 --- a/tools/scorecard/scorecard.mjs +++ b/tools/scorecard/scorecard.mjs @@ -723,6 +723,45 @@ const detectors = [ }; }, }, + { + id: "summary_output", + category: "integration", + name: "Building block renders a summary", + emoji: "🧾", + fn: (mod) => { + const content = readIntegrationTf(mod); + if (!content) return { pass: null, detail: "no integration file" }; + if (!runsTerraform(mod)) return { pass: null, detail: "not applicable — non-terraform implementation" }; + const missing = []; + if (!declaresSummaryOutput(content)) missing.push('a BBD output with assignment_type = "SUMMARY"'); + if (summaryOutputBody(readAllBuildingBlockTf(mod) ?? "") === null) missing.push('output "summary" in buildingblock/'); + if (missing.length === 0) return { pass: true }; + return { pass: false, detail: `add ${missing.join(" and ")}` }; + }, + }, + { + id: "summary_links_new_tab", + category: "integration", + name: "Summary links open in a new tab", + emoji: "🔗", + fn: (mod) => { + const body = summaryOutputBody(readAllBuildingBlockTf(mod) ?? ""); + if (body === null) return { pass: null, detail: "no summary output" }; + const sources = [["outputs", body]]; + for (const name of summaryTemplateFiles(body)) { + const p = join(mod.path, "buildingblock", name); + if (existsSync(p)) sources.push([name, readFileSync(p, "utf-8")]); + } + const violations = sources.flatMap(([file, text]) => summaryLinkViolations(text).map((v) => `${file}: ${v}`)); + if (violations.length === 0) return { pass: true }; + const shown = violations.slice(0, 3).join(", "); + const more = violations.length > 3 ? `, +${violations.length - 3} more` : ""; + return { + pass: false, + detail: `write links as : ${shown}${more}`, + }; + }, + }, { id: "wif_no_replicator", category: "integration", @@ -1932,6 +1971,43 @@ function findExpressionEnd(text, startIndex) { return text.length; } +export function declaresSummaryOutput(integrationContent) { + return /assignment_type\s*=\s*"SUMMARY"/.test(integrationContent); +} + +// Heredoc bodies are masked while matching braces, because Markdown in them may hold an unpaired +// quote or `#` that the HCL scanner would misread. +export function summaryOutputBody(tfContent) { + const masked = tfContent.replace(/<<-?\s*([A-Za-z_]\w*)\r?\n[\s\S]*?^\s*\1\s*$/gm, (heredoc) => { + const bodyStart = heredoc.indexOf("\n"); + return heredoc.slice(0, bodyStart) + heredoc.slice(bodyStart).replace(/[^\n]/g, " "); + }); + const m = /^output\s+"summary"\s*\{/m.exec(masked); + if (!m) return null; + const open = masked.indexOf("{", m.index); + const close = findMatchingBrace(masked, open); + return close > open ? tfContent.slice(m.index, close + 1) : null; +} + +export function summaryTemplateFiles(summaryBody) { + return [...summaryBody.matchAll(/templatefile\(\s*"\$\{path\.module\}\/([^"]+\.md\.tftpl)"/g)].map((m) => m[1]); +} + +export function summaryLinkViolations(markdown) { + const prose = markdown.replace(/^\s*(```|~~~)[^\n]*\n[\s\S]*?^\s*\1\s*$/gm, ""); + const violations = []; + for (const m of prose.matchAll(/\[[^\]\n]*\]\((?:https?:|\$\{)[^)\s]*\)/g)) { + violations.push(`Markdown link ${m[0]}`); + } + for (const m of prose.matchAll(/]*>/gi)) { + const missing = []; + if (!/\btarget=\\?"_blank\\?"/.test(m[0])) missing.push('target="_blank"'); + if (!/\brel=\\?"[^"\\]*\bnoopener\b/.test(m[0])) missing.push('rel="noopener"'); + if (missing.length > 0) violations.push(`${m[0]} lacks ${missing.join(" and ")}`); + } + return violations; +} + export function explicitDefaultViolations(content) { const variableBlocks = extractVariableBlocks(content); const violations = new Set(); diff --git a/tools/scorecard/scorecard.test.mjs b/tools/scorecard/scorecard.test.mjs index 1ae2169ac..3d5d126cb 100644 --- a/tools/scorecard/scorecard.test.mjs +++ b/tools/scorecard/scorecard.test.mjs @@ -1,6 +1,12 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { explicitDefaultViolations } from "./scorecard.mjs"; +import { + declaresSummaryOutput, + explicitDefaultViolations, + summaryLinkViolations, + summaryOutputBody, + summaryTemplateFiles, +} from "./scorecard.mjs"; const integration = (argument) => ` variable "meshstack" { @@ -42,3 +48,51 @@ test("an argument reading the whole object with an optional attribute fails", () "meshstack (missing default)", ]); }); + +test("a summary output is found past a heredoc with an unpaired quote and a heading", () => { + const tf = `output "other" { + value = 1 +} + +output "summary" { + value = <<-EOT + # It's "quoted + [repo](\${local.repository_url}) + EOT +} + +output "after" { + value = 2 +} +`; + const body = summaryOutputBody(tf); + assert.match(body, /^output "summary"/); + assert.match(body, /EOT\n}$/); + assert.equal(summaryOutputBody(`output "other" {\n value = 1\n}\n`), null); +}); + +test("the templates a summary renders are listed", () => { + assert.deepEqual(summaryTemplateFiles('value = templatefile("${path.module}/SUMMARY.md.tftpl", {})'), ["SUMMARY.md.tftpl"]); +}); + +test("a new-tab anchor passes", () => { + assert.deepEqual(summaryLinkViolations('x'), []); +}); + +test("Markdown links and anchors without a new tab fail", () => { + assert.deepEqual(summaryLinkViolations('[a](https://x.io) [b](${local.url}) y'), [ + "Markdown link [a](https://x.io)", + "Markdown link [b](${local.url})", + ' lacks target="_blank" and rel="noopener"', + ]); +}); + +test("mention tokens and links in a fenced code block pass", () => { + const markdown = "Runs on @buildingblock[1234] and @project[ws.p].\n\n```md\n[a](https://x.io)\ny\n```\n"; + assert.deepEqual(summaryLinkViolations(markdown), []); +}); + +test("a SUMMARY assignment in the integration is detected", () => { + assert.equal(declaresSummaryOutput('summary = {\n assignment_type = "SUMMARY"\n}'), true); + assert.equal(declaresSummaryOutput('url = {\n assignment_type = "RESOURCE_URL"\n}'), false); +}); From 557b9fff1e1be435fd899ac76817f48d32b3c610 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 12:21:57 +0100 Subject: [PATCH 06/28] fix(scorecard): give the summary checks emojis no other check uses Co-Authored-By: Claude Opus 5.5 --- tools/scorecard/scorecard.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/scorecard/scorecard.mjs b/tools/scorecard/scorecard.mjs index 19ade3ba8..f4ed60ee6 100644 --- a/tools/scorecard/scorecard.mjs +++ b/tools/scorecard/scorecard.mjs @@ -727,7 +727,7 @@ const detectors = [ id: "summary_output", category: "integration", name: "Building block renders a summary", - emoji: "🧾", + emoji: "🗒️", fn: (mod) => { const content = readIntegrationTf(mod); if (!content) return { pass: null, detail: "no integration file" }; @@ -743,7 +743,7 @@ const detectors = [ id: "summary_links_new_tab", category: "integration", name: "Summary links open in a new tab", - emoji: "🔗", + emoji: "↗️", fn: (mod) => { const body = summaryOutputBody(readAllBuildingBlockTf(mod) ?? ""); if (body === null) return { pass: null, detail: "no summary output" }; From 707302210c84aa6784481c2ffbceae6580abedd3 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 12:27:15 +0100 Subject: [PATCH 07/28] fix(ref-arch/stackit-kubernetes): write the summary links as Markdown links again Summaries hold no HTML anchors, so the cluster, Git and connector summaries go back to plain Markdown links. The cluster's portal link stays. Co-Authored-By: Claude Opus 5.5 --- modules/ske/cluster/buildingblock/outputs.tf | 2 +- modules/ske/forgejo-connector/buildingblock/outputs.tf | 8 ++++---- modules/stackit/git/buildingblock/SUMMARY.md.tftpl | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/ske/cluster/buildingblock/outputs.tf b/modules/ske/cluster/buildingblock/outputs.tf index e0cc44c61..7f52ab29e 100644 --- a/modules/ske/cluster/buildingblock/outputs.tf +++ b/modules/ske/cluster/buildingblock/outputs.tf @@ -23,7 +23,7 @@ output "summary" { | Property | Value | |----------|-------| - | **Cluster** | Open in STACKIT Portal | + | **Cluster** | [Open in STACKIT Portal](https://portal.stackit.cloud/kubernetes/clusters/details/${module.cluster.cluster.name}/overview?project=${var.stackit_project_id}) | | **Kubernetes** | `${module.cluster.cluster.kubernetes_version_used}` | | **API Server** | `${terraform_data.kube_host.output}` | | **Node Pool** | `${var.node_pool.name}`: ${var.node_pool.minimum} to ${var.node_pool.maximum} × `${var.node_pool.machine_type}` in ${join(", ", var.node_pool.availability_zones)} | diff --git a/modules/ske/forgejo-connector/buildingblock/outputs.tf b/modules/ske/forgejo-connector/buildingblock/outputs.tf index 3b1992bfb..a6e4a82b5 100644 --- a/modules/ske/forgejo-connector/buildingblock/outputs.tf +++ b/modules/ske/forgejo-connector/buildingblock/outputs.tf @@ -12,18 +12,18 @@ output "summary" { value = <<-EOT # Forgejo Connector: **${var.stage}** - The `${var.stage}` branch of ${var.repository_owner}/${var.repository_name} deploys + The `${var.stage}` branch of [${var.repository_owner}/${var.repository_name}](${local.repository_url}) deploys to the namespace `${var.namespace}`. | Property | Value | |----------|-------| - | **App** | https://${var.app_hostname} | - | **Pipeline** | pipeline.yaml in Forgejo Actions | + | **App** | [https://${var.app_hostname}](https://${var.app_hostname}) | + | **Pipeline** | [`pipeline.yaml` in Forgejo Actions](${local.repository_url}/actions?workflow=pipeline.yaml) | | **Service Account** | `${kubernetes_service_account.forgejo_actions.metadata[0].name}`, `edit` on the namespace | ## Forgejo Actions secrets and variables - Set in the repository settings: + Set in the [repository settings](${local.repository_url}/settings/actions/secrets): | Name | Kind | Holds | |------|------|-------| diff --git a/modules/stackit/git/buildingblock/SUMMARY.md.tftpl b/modules/stackit/git/buildingblock/SUMMARY.md.tftpl index 146040960..b5031ce06 100644 --- a/modules/stackit/git/buildingblock/SUMMARY.md.tftpl +++ b/modules/stackit/git/buildingblock/SUMMARY.md.tftpl @@ -2,9 +2,9 @@ | Property | Value | |----------|-------| -| **Forgejo** | ${instance_url} | +| **Forgejo** | [${instance_url}](${instance_url}) | %{ if forgejo_organization != null ~} -| **Organization** | ${forgejo_organization} | +| **Organization** | [`${forgejo_organization}`](${organization_url}) | %{ endif ~} | **Technical User** | `${local_user_username}` | | **Shared Runner** | ${length(shared_runner_labels) > 0 ? "`${join("`, `", shared_runner_labels)}`" : "none"} | @@ -22,7 +22,7 @@ %{ endfor ~} %{ if length([for member in members : member if member.username == ""]) > 0 ~} -A member joins their team once they have signed in to ${instance_url} with their +A member joins their team once they have signed in to [${instance_url}](${instance_url}) with their STACKIT account. After they have signed in, re-run this building block. %{ endif ~} %{ endif ~} From a6acfe354b1d921e86d4e1cf335a37226f7498d1 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 12:27:34 +0100 Subject: [PATCH 08/28] refactor(scorecard): drop the check that summary links open in a new tab summary_output stays. Summaries use plain Markdown links, so terraform-conventions.md no longer asks for HTML anchors. Co-Authored-By: Claude Opus 5.5 --- .agents/references/terraform-conventions.md | 6 +-- tools/scorecard/scorecard.mjs | 42 --------------------- tools/scorecard/scorecard.test.mjs | 23 ----------- 3 files changed, 2 insertions(+), 69 deletions(-) diff --git a/.agents/references/terraform-conventions.md b/.agents/references/terraform-conventions.md index 74f86c3e9..a364bd9d0 100644 --- a/.agents/references/terraform-conventions.md +++ b/.agents/references/terraform-conventions.md @@ -94,13 +94,11 @@ The comment sits directly above the `try`, or above the attribute that owns the `try` is nested inside it. Prefer not needing it: a variable default, `optional()` or an explicit `null` check says the same thing without hiding the next error too. - + ## Summary Output Every building block with an integration has a `summary` output, declared in the BBD with -`assignment_type = "SUMMARY"`, so meshPanel shows the result of a run. Write each link in it as -`…`: meshPanel opens a Markdown link in the same tab, -which takes the user out of the panel. +`assignment_type = "SUMMARY"`, so meshPanel shows the result of a run. ## Output Content: Inline Heredoc Over a Template File diff --git a/tools/scorecard/scorecard.mjs b/tools/scorecard/scorecard.mjs index f4ed60ee6..4c9e4304d 100644 --- a/tools/scorecard/scorecard.mjs +++ b/tools/scorecard/scorecard.mjs @@ -739,29 +739,6 @@ const detectors = [ return { pass: false, detail: `add ${missing.join(" and ")}` }; }, }, - { - id: "summary_links_new_tab", - category: "integration", - name: "Summary links open in a new tab", - emoji: "↗️", - fn: (mod) => { - const body = summaryOutputBody(readAllBuildingBlockTf(mod) ?? ""); - if (body === null) return { pass: null, detail: "no summary output" }; - const sources = [["outputs", body]]; - for (const name of summaryTemplateFiles(body)) { - const p = join(mod.path, "buildingblock", name); - if (existsSync(p)) sources.push([name, readFileSync(p, "utf-8")]); - } - const violations = sources.flatMap(([file, text]) => summaryLinkViolations(text).map((v) => `${file}: ${v}`)); - if (violations.length === 0) return { pass: true }; - const shown = violations.slice(0, 3).join(", "); - const more = violations.length > 3 ? `, +${violations.length - 3} more` : ""; - return { - pass: false, - detail: `write links as : ${shown}${more}`, - }; - }, - }, { id: "wif_no_replicator", category: "integration", @@ -1989,25 +1966,6 @@ export function summaryOutputBody(tfContent) { return close > open ? tfContent.slice(m.index, close + 1) : null; } -export function summaryTemplateFiles(summaryBody) { - return [...summaryBody.matchAll(/templatefile\(\s*"\$\{path\.module\}\/([^"]+\.md\.tftpl)"/g)].map((m) => m[1]); -} - -export function summaryLinkViolations(markdown) { - const prose = markdown.replace(/^\s*(```|~~~)[^\n]*\n[\s\S]*?^\s*\1\s*$/gm, ""); - const violations = []; - for (const m of prose.matchAll(/\[[^\]\n]*\]\((?:https?:|\$\{)[^)\s]*\)/g)) { - violations.push(`Markdown link ${m[0]}`); - } - for (const m of prose.matchAll(/]*>/gi)) { - const missing = []; - if (!/\btarget=\\?"_blank\\?"/.test(m[0])) missing.push('target="_blank"'); - if (!/\brel=\\?"[^"\\]*\bnoopener\b/.test(m[0])) missing.push('rel="noopener"'); - if (missing.length > 0) violations.push(`${m[0]} lacks ${missing.join(" and ")}`); - } - return violations; -} - export function explicitDefaultViolations(content) { const variableBlocks = extractVariableBlocks(content); const violations = new Set(); diff --git a/tools/scorecard/scorecard.test.mjs b/tools/scorecard/scorecard.test.mjs index 3d5d126cb..32387428d 100644 --- a/tools/scorecard/scorecard.test.mjs +++ b/tools/scorecard/scorecard.test.mjs @@ -3,9 +3,7 @@ import assert from "node:assert/strict"; import { declaresSummaryOutput, explicitDefaultViolations, - summaryLinkViolations, summaryOutputBody, - summaryTemplateFiles, } from "./scorecard.mjs"; const integration = (argument) => ` @@ -71,27 +69,6 @@ output "after" { assert.equal(summaryOutputBody(`output "other" {\n value = 1\n}\n`), null); }); -test("the templates a summary renders are listed", () => { - assert.deepEqual(summaryTemplateFiles('value = templatefile("${path.module}/SUMMARY.md.tftpl", {})'), ["SUMMARY.md.tftpl"]); -}); - -test("a new-tab anchor passes", () => { - assert.deepEqual(summaryLinkViolations('x'), []); -}); - -test("Markdown links and anchors without a new tab fail", () => { - assert.deepEqual(summaryLinkViolations('[a](https://x.io) [b](${local.url}) y'), [ - "Markdown link [a](https://x.io)", - "Markdown link [b](${local.url})", - ' lacks target="_blank" and rel="noopener"', - ]); -}); - -test("mention tokens and links in a fenced code block pass", () => { - const markdown = "Runs on @buildingblock[1234] and @project[ws.p].\n\n```md\n[a](https://x.io)\ny\n```\n"; - assert.deepEqual(summaryLinkViolations(markdown), []); -}); - test("a SUMMARY assignment in the integration is detected", () => { assert.equal(declaresSummaryOutput('summary = {\n assignment_type = "SUMMARY"\n}'), true); assert.equal(declaresSummaryOutput('url = {\n assignment_type = "RESOURCE_URL"\n}'), false); From a4ad833d87f6e05252b7d8b3e60c286617963fa7 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 12:29:08 +0100 Subject: [PATCH 09/28] feat(stackit/git-runner): add a summary to the building block The summary links the organization and its runners page, and lists the labels, the runner version, the server and the egress IP. Co-Authored-By: Claude Opus 5.5 --- .../stackit/git-runner/buildingblock/README.md | 1 + .../git-runner/buildingblock/outputs.tf | 18 ++++++++++++++++++ .../git-runner/meshstack_integration.tf | 6 ++++++ 3 files changed, 25 insertions(+) diff --git a/modules/stackit/git-runner/buildingblock/README.md b/modules/stackit/git-runner/buildingblock/README.md index 6d3b07830..e14a8fa2a 100644 --- a/modules/stackit/git-runner/buildingblock/README.md +++ b/modules/stackit/git-runner/buildingblock/README.md @@ -71,4 +71,5 @@ No modules. | [egress\_ip](#output\_egress\_ip) | Public SNAT IP of the runner's network router, for allowlisting. Empty on an existing network. | | [runner\_name](#output\_runner\_name) | Name the runner registered under in STACKIT Git. | | [server\_id](#output\_server\_id) | ID of the STACKIT server hosting the runner. | +| [summary](#output\_summary) | Markdown summary shown on the building block. | diff --git a/modules/stackit/git-runner/buildingblock/outputs.tf b/modules/stackit/git-runner/buildingblock/outputs.tf index 99e5bc000..91454ca00 100644 --- a/modules/stackit/git-runner/buildingblock/outputs.tf +++ b/modules/stackit/git-runner/buildingblock/outputs.tf @@ -12,3 +12,21 @@ output "egress_ip" { value = local.create_network ? stackit_network.this.public_ip : "" description = "Public SNAT IP of the runner's network router, for allowlisting. Empty on an existing network." } + +output "summary" { + description = "Markdown summary shown on the building block." + value = <<-EOT + # Git Runner: **${var.name}** + + The runner serves the STACKIT Git organization [`${var.git_organization}`](${var.git_base_url}/${var.git_organization}). + A workflow picks it with `runs-on` set to one of its labels. + + | Property | Value | + |----------|-------| + | **Runners** | [Organization runners](${var.git_base_url}/org/${var.git_organization}/settings/actions/runners) | + | **Labels** | `${join("`, `", var.runner_labels)}` | + | **Runner Version** | `${var.runner_version}` | + | **Server** | `${stackit_server.this.server_id}`: `${var.machine_type}` with ${var.disk_size_gb} GB in ${var.availability_zone} | + | **Egress IP** | ${local.create_network ? "`${stackit_network.this.public_ip}`" : "that of the network `${var.network_id}`"} | + EOT +} diff --git a/modules/stackit/git-runner/meshstack_integration.tf b/modules/stackit/git-runner/meshstack_integration.tf index 9393db7a6..602d173fc 100644 --- a/modules/stackit/git-runner/meshstack_integration.tf +++ b/modules/stackit/git-runner/meshstack_integration.tf @@ -317,6 +317,12 @@ resource "meshstack_building_block_definition" "this" { type = "STRING" assignment_type = "NONE" } + + summary = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } } } } From 12fcb285718730c4bd33f47d3b6514a50a0e045b Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 12:30:55 +0100 Subject: [PATCH 10/28] feat(ref-arch/stackit-landingzone): put the whole Kubernetes platform tip inside the tip block Co-Authored-By: Claude Opus 5.5 --- .../buildingblock/SUMMARY.md.tftpl | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl b/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl index 7113595ba..6f184b3dc 100644 --- a/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl +++ b/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl @@ -35,32 +35,32 @@ landing zone. Provide a `network` object to additionally enable hub-and-spoke ne > [!TIP] > ## Build a Kubernetes platform on top - -The [STACKIT Kubernetes Platform](https://hub.meshcloud.io/reference-architectures/stackit-kubernetes) -reference architecture runs a full SKE developer platform on top of this landing zone. It takes one -value from this landing zone. Paste this block into its **STACKIT Landing Zone** input: - +> +> The [STACKIT Kubernetes Platform](https://hub.meshcloud.io/reference-architectures/stackit-kubernetes) +> reference architecture runs a full SKE developer platform on top of this landing zone. It takes one +> value from this landing zone. Paste this block into its **STACKIT Landing Zone** input: +> +> ```hcl +> { +> platform_ref = { uuid = "${platform_ref.uuid}" } +> landingzone_refs = { +%{ for key, ref in landingzone_refs ~} +> ${key} = { name = "${ref.name}" } +%{ endfor ~} +> } +> service_account_bbd_version_ref = { uuid = "${service_account_bbd_version_uuid}" } +> service_account_federation_bbd_version_ref = { uuid = "${service_account_federation_bbd_version_uuid}" } +> } +> ``` +%{ if network_enabled ~} +> +> Set its **Landing Zone Variant** to `networked` to run the cluster inside the hub-and-spoke network. +%{ endif ~} %{ if !identity_definitions_released ~} + > [!WARNING] > The latest versions of the **STACKIT Service Account** and the **STACKIT Service Account > Federation** definitions are drafts, and another workspace may only order released versions. > Deploy this landing zone with `hub.bbd_draft = false`, publish both definitions to the > marketplace, then run this building block again. - -%{ endif ~} -```hcl -{ - platform_ref = { uuid = "${platform_ref.uuid}" } - landingzone_refs = { -%{ for key, ref in landingzone_refs ~} - ${key} = { name = "${ref.name}" } -%{ endfor ~} - } - service_account_bbd_version_ref = { uuid = "${service_account_bbd_version_uuid}" } - service_account_federation_bbd_version_ref = { uuid = "${service_account_federation_bbd_version_uuid}" } -} -``` -%{ if network_enabled ~} - -Set its **Landing Zone Variant** to `networked` to run the cluster inside the hub-and-spoke network. %{ endif ~} From bdd6919042f44ebf5cc5c38831be662ef83b53b4 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 13:14:01 +0100 Subject: [PATCH 11/28] feat(ref-arch/stackit-landingzone): explain the default landing zone variant in the tip Co-Authored-By: Claude Opus 5.5 --- .../stackit-landingzone/buildingblock/SUMMARY.md.tftpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl b/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl index 6f184b3dc..75acac295 100644 --- a/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl +++ b/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl @@ -55,6 +55,8 @@ landing zone. Provide a `network` object to additionally enable hub-and-spoke ne %{ if network_enabled ~} > > Set its **Landing Zone Variant** to `networked` to run the cluster inside the hub-and-spoke network. +> Otherwise keep the default `default`, which puts the cluster in a **STACKIT Sandbox** project with +> STACKIT's default flat networking. %{ endif ~} %{ if !identity_definitions_released ~} From b443a54e4467016955ad4910fc8a41471fe2684b Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 13:41:20 +0100 Subject: [PATCH 12/28] fix(stackit): link the portal pages of DNS zones, service accounts and projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DNS zone link and the service accounts list used `/projects//…` paths that the portal no longer opens. Each link now goes to the resource's own overview page, and project links use the dashboard URL the portal redirects to. Co-Authored-By: Claude Opus 5.5 --- modules/stackit/dns/buildingblock/outputs.tf | 4 ++-- modules/stackit/project/buildingblock/outputs.tf | 4 ++-- .../service-account-federation/buildingblock/SUMMARY.md.tftpl | 2 +- modules/stackit/service-account/buildingblock/README.md | 2 +- .../stackit/service-account/buildingblock/SUMMARY.md.tftpl | 2 +- modules/stackit/service-account/buildingblock/outputs.tf | 4 ++-- .../stackit-project-starterkit/buildingblock/this/outputs.tf | 4 ++-- .../stackit-kubernetes/buildingblock/outputs.tf | 2 +- .../platform/buildingblock/SUMMARY.md.tftpl | 2 +- .../stackit-landingzone/buildingblock/outputs.tf | 4 ++-- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/modules/stackit/dns/buildingblock/outputs.tf b/modules/stackit/dns/buildingblock/outputs.tf index eddbb9975..df3c4f1dc 100644 --- a/modules/stackit/dns/buildingblock/outputs.tf +++ b/modules/stackit/dns/buildingblock/outputs.tf @@ -9,14 +9,14 @@ output "zone_id" { } output "zone_url" { - value = "https://portal.stackit.cloud/projects/${var.stackit_project_id}/dns/${module.zone.zone.zone_id}" + value = "https://portal.stackit.cloud/dns/zones/${module.zone.zone.zone_id}/overview?project=${var.stackit_project_id}" description = "The zone in the STACKIT portal." } output "summary" { value = templatefile("${path.module}/SUMMARY.md.tftpl", { zone_name = local.zone_name - zone_url = "https://portal.stackit.cloud/projects/${var.stackit_project_id}/dns/${module.zone.zone.zone_id}" + zone_url = "https://portal.stackit.cloud/dns/zones/${module.zone.zone.zone_id}/overview?project=${var.stackit_project_id}" wildcard_target_ip = var.wildcard_target_ip default_ttl = var.default_ttl }) diff --git a/modules/stackit/project/buildingblock/outputs.tf b/modules/stackit/project/buildingblock/outputs.tf index 364651427..2c1bb4ff7 100644 --- a/modules/stackit/project/buildingblock/outputs.tf +++ b/modules/stackit/project/buildingblock/outputs.tf @@ -14,7 +14,7 @@ output "project_name" { } output "project_url" { - value = "https://portal.stackit.cloud/projects/${stackit_resourcemanager_project.project.project_id}" + value = "https://portal.stackit.cloud/dashboard?project=${stackit_resourcemanager_project.project.project_id}" description = "The deep link URL to access the project in the StackIt portal." } @@ -24,7 +24,7 @@ output "summary" { project_name = stackit_resourcemanager_project.project.name project_id = stackit_resourcemanager_project.project.project_id container_id = stackit_resourcemanager_project.project.container_id - project_url = "https://portal.stackit.cloud/projects/${stackit_resourcemanager_project.project.project_id}" + project_url = "https://portal.stackit.cloud/dashboard?project=${stackit_resourcemanager_project.project.project_id}" unknown_users = local.unknown_users membership_summary = fileexists("${path.module}/stackit_organization_membership_summary.md") ? file("${path.module}/stackit_organization_membership_summary.md") : "STACKIT organization membership summary was not generated." }) diff --git a/modules/stackit/service-account-federation/buildingblock/SUMMARY.md.tftpl b/modules/stackit/service-account-federation/buildingblock/SUMMARY.md.tftpl index 3eb08e445..8781b30f9 100644 --- a/modules/stackit/service-account-federation/buildingblock/SUMMARY.md.tftpl +++ b/modules/stackit/service-account-federation/buildingblock/SUMMARY.md.tftpl @@ -9,4 +9,4 @@ Runs of these building block definitions act as the service account through work _None._ %{ endif ~} -[Open service accounts](https://portal.stackit.cloud/projects/${project_id}/service-accounts) +[Open service account](https://portal.stackit.cloud/service-accounts/${service_account_email}/overview?project=${project_id}) diff --git a/modules/stackit/service-account/buildingblock/README.md b/modules/stackit/service-account/buildingblock/README.md index 9b2120d23..a30c4caf1 100644 --- a/modules/stackit/service-account/buildingblock/README.md +++ b/modules/stackit/service-account/buildingblock/README.md @@ -50,6 +50,6 @@ No modules. |------|-------------| | [service\_account\_email](#output\_service\_account\_email) | Email of the created STACKIT service account. Use it as the principal in external federation configs and STACKIT role assignments. | | [service\_account\_id](#output\_service\_account\_id) | Id of the created STACKIT service account. Forms asking for a service account often want this next to the email, e.g. linking a Harbor robot. | -| [service\_account\_url](#output\_service\_account\_url) | Deep link to the service accounts overview of the project in the STACKIT portal. | +| [service\_account\_url](#output\_service\_account\_url) | Deep link to the service account in the STACKIT portal. | | [summary](#output\_summary) | Summary of the created service account. | diff --git a/modules/stackit/service-account/buildingblock/SUMMARY.md.tftpl b/modules/stackit/service-account/buildingblock/SUMMARY.md.tftpl index 7533812d4..eb8d58f6b 100644 --- a/modules/stackit/service-account/buildingblock/SUMMARY.md.tftpl +++ b/modules/stackit/service-account/buildingblock/SUMMARY.md.tftpl @@ -7,4 +7,4 @@ | **Email** | `${service_account_email}` | | **Project** | `${project_id}` | | **Roles** | ${join(", ", roles)} | -| **Portal** | [Open service accounts](https://portal.stackit.cloud/projects/${project_id}/service-accounts) | +| **Portal** | [Open service account](https://portal.stackit.cloud/service-accounts/${service_account_email}/overview?project=${project_id}) | diff --git a/modules/stackit/service-account/buildingblock/outputs.tf b/modules/stackit/service-account/buildingblock/outputs.tf index e95f05db5..dc6710a9a 100644 --- a/modules/stackit/service-account/buildingblock/outputs.tf +++ b/modules/stackit/service-account/buildingblock/outputs.tf @@ -9,8 +9,8 @@ output "service_account_id" { } output "service_account_url" { - value = "https://portal.stackit.cloud/projects/${var.project_id}/service-accounts" - description = "Deep link to the service accounts overview of the project in the STACKIT portal." + value = "https://portal.stackit.cloud/service-accounts/${stackit_service_account.this.email}/overview?project=${var.project_id}" + description = "Deep link to the service account in the STACKIT portal." } output "summary" { diff --git a/modules/stackit/stackit-project-starterkit/buildingblock/this/outputs.tf b/modules/stackit/stackit-project-starterkit/buildingblock/this/outputs.tf index 3248f0e45..412559d9f 100644 --- a/modules/stackit/stackit-project-starterkit/buildingblock/this/outputs.tf +++ b/modules/stackit/stackit-project-starterkit/buildingblock/this/outputs.tf @@ -14,7 +14,7 @@ output "project_identifier" { output "stackit_project_url" { description = "Deep link to the created STACKIT project, once the tenant has replicated." - value = local.stackit_project_id == null ? "https://portal.stackit.cloud" : "https://portal.stackit.cloud/projects/${local.stackit_project_id}" + value = local.stackit_project_id == null ? "https://portal.stackit.cloud" : "https://portal.stackit.cloud/dashboard?project=${local.stackit_project_id}" } # `n/a` rather than null when there is no network. A declared building block definition output has to @@ -41,7 +41,7 @@ output "summary" { ) stackit_project = (local.stackit_project_id == null ? "provisioning — the STACKIT Project building block is still running" - : "[Open in STACKIT Portal](https://portal.stackit.cloud/projects/${local.stackit_project_id}) (`${local.stackit_project_id}`)" + : "[Open in STACKIT Portal](https://portal.stackit.cloud/dashboard?project=${local.stackit_project_id}) (`${local.stackit_project_id}`)" ) project_admin = local.creator_is_user ? var.creator.displayName : "not assigned — the creator is a ${var.creator.type}, which has no user to bind" }) diff --git a/reference-architectures/stackit-kubernetes/buildingblock/outputs.tf b/reference-architectures/stackit-kubernetes/buildingblock/outputs.tf index 6e73409d2..ef6de8686 100644 --- a/reference-architectures/stackit-kubernetes/buildingblock/outputs.tf +++ b/reference-architectures/stackit-kubernetes/buildingblock/outputs.tf @@ -1,6 +1,6 @@ output "ske_project_url" { description = "Deep link to the STACKIT project the SKE cluster and its assets run in." - value = "https://portal.stackit.cloud/projects/${local.stackit_project_id}" + value = "https://portal.stackit.cloud/dashboard?project=${local.stackit_project_id}" } output "summary" { diff --git a/reference-architectures/stackit-kubernetes/platform/buildingblock/SUMMARY.md.tftpl b/reference-architectures/stackit-kubernetes/platform/buildingblock/SUMMARY.md.tftpl index bd69eb977..d99c12aa6 100644 --- a/reference-architectures/stackit-kubernetes/platform/buildingblock/SUMMARY.md.tftpl +++ b/reference-architectures/stackit-kubernetes/platform/buildingblock/SUMMARY.md.tftpl @@ -11,7 +11,7 @@ | Property | Value | |----------|-------| -| **STACKIT Project** | [Open in STACKIT Portal](https://portal.stackit.cloud/projects/${stackit_project_id}) (`${stackit_project_id}`) | +| **STACKIT Project** | [Open in STACKIT Portal](https://portal.stackit.cloud/dashboard?project=${stackit_project_id}) (`${stackit_project_id}`) | | **Secrets Manager** | [Open in STACKIT Portal](https://portal.stackit.cloud/secrets-manager/instances/${secrets_manager_instance_id}/overview?project=${stackit_project_id}) | | **SKE Cluster** | `${cluster_name}` — @buildingblock[${cluster_bb_uuid}] | | **Ingress & Certificates** | `${ingress_load_balancer_ip}` — @buildingblock[${ingress_bb_uuid}] | diff --git a/reference-architectures/stackit-landingzone/buildingblock/outputs.tf b/reference-architectures/stackit-landingzone/buildingblock/outputs.tf index 8f709fbc1..2f7a31bb5 100644 --- a/reference-architectures/stackit-landingzone/buildingblock/outputs.tf +++ b/reference-architectures/stackit-landingzone/buildingblock/outputs.tf @@ -14,7 +14,7 @@ output "foundation_project_id" { } output "foundation_project_url" { - value = "https://portal.stackit.cloud/projects/${stackit_resourcemanager_project.foundation.project_id}" + value = "https://portal.stackit.cloud/dashboard?project=${stackit_resourcemanager_project.foundation.project_id}" description = "Deep link to the foundation project in the STACKIT portal." } @@ -31,7 +31,7 @@ output "summary" { lz_folder_container_id = stackit_resourcemanager_folder.this.container_id lz_folder_url = "https://portal.stackit.cloud/dashboard?organization=${var.stackit_org}&folder=${stackit_resourcemanager_folder.this.folder_id}" foundation_project_id = stackit_resourcemanager_project.foundation.project_id - foundation_project_url = "https://portal.stackit.cloud/projects/${stackit_resourcemanager_project.foundation.project_id}" + foundation_project_url = "https://portal.stackit.cloud/dashboard?project=${stackit_resourcemanager_project.foundation.project_id}" service_account_email = module.stackit_integration.service_account_email service_account_url = "https://portal.stackit.cloud/service-accounts/${module.stackit_integration.service_account_email}/overview?project=${stackit_resourcemanager_project.foundation.project_id}" From 757780138e702fa9dab64befdff76247fc30002b Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:12:54 +0100 Subject: [PATCH 13/28] fix(kubernetes/service-account): return the kubeconfig output as plain text meshStack has no sensitive outputs, so the OpenTofu sensitive flag only hid what the building block shows anyway. The comment points to output_to_vault, and the output stays "" there because meshStack fails a run whose declared output is missing. Co-Authored-By: Claude Opus 5.5 --- modules/kubernetes/service-account/buildingblock/outputs.tf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/kubernetes/service-account/buildingblock/outputs.tf b/modules/kubernetes/service-account/buildingblock/outputs.tf index d61533346..7993c3c99 100644 --- a/modules/kubernetes/service-account/buildingblock/outputs.tf +++ b/modules/kubernetes/service-account/buildingblock/outputs.tf @@ -37,10 +37,11 @@ output "instructions" { value = "Copy kubeconfig value into a file, which can be directly used. e.g. `kubectl --kubeconfig kubeconfig get pods`" } +# meshStack has no sensitive outputs, so this hands the token to everyone who can see the building +# block; prefer `output_to_vault`. It is "" there, because meshStack fails a run whose output is missing. output "kubeconfig" { description = "Kubeconfig file content for authenticating with the Kubernetes cluster. Empty when `output_to_vault` is set." - sensitive = true - value = local.write_to_vault ? "" : yamlencode(local.kubeconfig) + value = local.write_to_vault ? "" : nonsensitive(yamlencode(local.kubeconfig)) } output "vault_secret" { From b981c5f2133b72f30def8dfe2d74970b6046f482 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:25:27 +0100 Subject: [PATCH 14/28] docs(kubernetes/service-account): say in the kubeconfig description that it is sensitive Co-Authored-By: Claude Opus 5.5 --- modules/kubernetes/service-account/buildingblock/README.md | 2 +- .../kubernetes/service-account/buildingblock/outputs.tf | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/kubernetes/service-account/buildingblock/README.md b/modules/kubernetes/service-account/buildingblock/README.md index 267dd1ff6..00841031f 100644 --- a/modules/kubernetes/service-account/buildingblock/README.md +++ b/modules/kubernetes/service-account/buildingblock/README.md @@ -71,7 +71,7 @@ No modules. | Name | Description | |------|-------------| | [instructions](#output\_instructions) | Instructions for using the kubeconfig | -| [kubeconfig](#output\_kubeconfig) | Kubeconfig file content for authenticating with the Kubernetes cluster. Empty when `output_to_vault` is set. | +| [kubeconfig](#output\_kubeconfig) | Kubeconfig with the service account token. Sensitive, but in plain view to everyone who can see the building block; prefer `output_to_vault`, which leaves this empty. | | [summary](#output\_summary) | Markdown summary shown on the building block. | | [vault\_secret](#output\_vault\_secret) | `{path, secret_hash}` of the secret in Vault, where `secret_hash` changes with its content; `{}` without `output_to_vault`. | diff --git a/modules/kubernetes/service-account/buildingblock/outputs.tf b/modules/kubernetes/service-account/buildingblock/outputs.tf index 7993c3c99..c18601a8b 100644 --- a/modules/kubernetes/service-account/buildingblock/outputs.tf +++ b/modules/kubernetes/service-account/buildingblock/outputs.tf @@ -37,11 +37,10 @@ output "instructions" { value = "Copy kubeconfig value into a file, which can be directly used. e.g. `kubectl --kubeconfig kubeconfig get pods`" } -# meshStack has no sensitive outputs, so this hands the token to everyone who can see the building -# block; prefer `output_to_vault`. It is "" there, because meshStack fails a run whose output is missing. output "kubeconfig" { - description = "Kubeconfig file content for authenticating with the Kubernetes cluster. Empty when `output_to_vault` is set." - value = local.write_to_vault ? "" : nonsensitive(yamlencode(local.kubeconfig)) + description = "Kubeconfig with the service account token. Sensitive, but in plain view to everyone who can see the building block; prefer `output_to_vault`, which leaves this empty." + # "" because meshStack fails a run whose declared output is missing. + value = local.write_to_vault ? "" : nonsensitive(yamlencode(local.kubeconfig)) } output "vault_secret" { From f0c8cb5838aca6b65abec5bbe4907b66413aaf1e Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:27:29 +0100 Subject: [PATCH 15/28] docs(kubernetes/service-account): move the kubeconfig warning to the definition output Co-Authored-By: Claude Opus 5.5 --- modules/kubernetes/service-account/buildingblock/README.md | 2 +- modules/kubernetes/service-account/buildingblock/outputs.tf | 1 - modules/kubernetes/service-account/meshstack_integration.tf | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/kubernetes/service-account/buildingblock/README.md b/modules/kubernetes/service-account/buildingblock/README.md index 00841031f..cad83b6fd 100644 --- a/modules/kubernetes/service-account/buildingblock/README.md +++ b/modules/kubernetes/service-account/buildingblock/README.md @@ -71,7 +71,7 @@ No modules. | Name | Description | |------|-------------| | [instructions](#output\_instructions) | Instructions for using the kubeconfig | -| [kubeconfig](#output\_kubeconfig) | Kubeconfig with the service account token. Sensitive, but in plain view to everyone who can see the building block; prefer `output_to_vault`, which leaves this empty. | +| [kubeconfig](#output\_kubeconfig) | n/a | | [summary](#output\_summary) | Markdown summary shown on the building block. | | [vault\_secret](#output\_vault\_secret) | `{path, secret_hash}` of the secret in Vault, where `secret_hash` changes with its content; `{}` without `output_to_vault`. | diff --git a/modules/kubernetes/service-account/buildingblock/outputs.tf b/modules/kubernetes/service-account/buildingblock/outputs.tf index c18601a8b..708bea3f9 100644 --- a/modules/kubernetes/service-account/buildingblock/outputs.tf +++ b/modules/kubernetes/service-account/buildingblock/outputs.tf @@ -38,7 +38,6 @@ output "instructions" { } output "kubeconfig" { - description = "Kubeconfig with the service account token. Sensitive, but in plain view to everyone who can see the building block; prefer `output_to_vault`, which leaves this empty." # "" because meshStack fails a run whose declared output is missing. value = local.write_to_vault ? "" : nonsensitive(yamlencode(local.kubeconfig)) } diff --git a/modules/kubernetes/service-account/meshstack_integration.tf b/modules/kubernetes/service-account/meshstack_integration.tf index 3eebfaa64..290cc03d9 100644 --- a/modules/kubernetes/service-account/meshstack_integration.tf +++ b/modules/kubernetes/service-account/meshstack_integration.tf @@ -268,6 +268,7 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "NONE" } + # Carries the SA token in plain view, because BB outputs cannot be sensitive; output_to_vault leaves it "". kubeconfig = { display_name = "Kubeconfig" type = "STRING" From 814863675132e01d52c79a5d497edf33726d40b9 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:28:21 +0100 Subject: [PATCH 16/28] docs(kubernetes): name STACKIT only as an example in the kubernetes modules The kubernetes modules run on any cluster, so comments, descriptions and readmes describe Vault users and cluster definitions in general terms and mention STACKIT as one example. Co-Authored-By: Claude Opus 5.5 --- modules/kubernetes/ingress/meshstack_integration.tf | 2 +- modules/kubernetes/manifest/meshstack_integration.tf | 2 +- modules/kubernetes/meshstack_integration.tf | 6 +++--- modules/kubernetes/platform/README.md | 4 ++-- modules/kubernetes/platform/main.tf | 4 ++-- modules/kubernetes/platform/output_to_vault.tf | 3 ++- .../service-account/buildingblock/output_to_vault.tf | 3 ++- 7 files changed, 13 insertions(+), 11 deletions(-) diff --git a/modules/kubernetes/ingress/meshstack_integration.tf b/modules/kubernetes/ingress/meshstack_integration.tf index 049177e26..09ace6532 100644 --- a/modules/kubernetes/ingress/meshstack_integration.tf +++ b/modules/kubernetes/ingress/meshstack_integration.tf @@ -60,7 +60,7 @@ variable "cluster_definition_ref" { uuid = string }) default = null - description = "Ref of the SKE cluster definition whose building blocks parent this definition's building blocks; null declares no dependency." + description = "Ref of the cluster definition (for example `ske/cluster`) whose building blocks parent this definition's building blocks; null declares no dependency." } variable "bbd_display_name" { diff --git a/modules/kubernetes/manifest/meshstack_integration.tf b/modules/kubernetes/manifest/meshstack_integration.tf index 39049d295..6e72b4d6e 100644 --- a/modules/kubernetes/manifest/meshstack_integration.tf +++ b/modules/kubernetes/manifest/meshstack_integration.tf @@ -118,7 +118,7 @@ resource "meshstack_building_block_definition" "this" { symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/modules/kubernetes/manifest/buildingblock/logo.svg" description = coalesce(var.bbd_description, "Deploys arbitrary Kubernetes manifests into a tenant namespace via a local Helm chart, with operator-supplied templates and user-provided values.") target_type = "TENANT_LEVEL" - # TODO Correct would be to depend on the specific SKE cluster deployed (not just platform type), + # TODO Correct would be to depend on the specific cluster deployed (not just platform type), # as the kubeconfig.yaml input only works for one specific cluster (representing the meshPlatform here) supported_platforms = [{ name = "KUBERNETES" }] run_transparency = true diff --git a/modules/kubernetes/meshstack_integration.tf b/modules/kubernetes/meshstack_integration.tf index 2db6112ef..d97639b31 100644 --- a/modules/kubernetes/meshstack_integration.tf +++ b/modules/kubernetes/meshstack_integration.tf @@ -195,7 +195,7 @@ variable "cluster_definition_ref" { uuid = string }) default = null - description = "Ref of the SKE cluster definition whose building blocks parent this definition's building blocks; null declares no dependency." + description = "Ref of the cluster definition (for example `ske/cluster`) whose building blocks parent this definition's building blocks; null declares no dependency." } variable "bbd_display_name" { @@ -425,8 +425,8 @@ resource "meshstack_building_block_definition" "this" { ## 🎯 When to use it A platform team orders this once per cluster, as part of turning that cluster into a meshStack - platform — the **STACKIT Kubernetes Platform** reference architecture does it right after the - cluster exists and feeds the tokens into the platform it registers. It reaches the cluster + platform. The **STACKIT Kubernetes Platform** reference architecture, for example, does it right + after the cluster exists and feeds the tokens into the platform it registers. It reaches the cluster through the kubeconfig the ordering composition passes as an input. ## 📦 Resources created diff --git a/modules/kubernetes/platform/README.md b/modules/kubernetes/platform/README.md index ba2056b0f..e12603e09 100644 --- a/modules/kubernetes/platform/README.md +++ b/modules/kubernetes/platform/README.md @@ -28,8 +28,8 @@ is involved, so this runs on any conformant cluster. It does **not** register a meshPlatform. Deciding that a cluster becomes a meshStack platform — its identifier, location, landing zones and quota definitions — belongs to the composition that owns -the cluster, and the **STACKIT Kubernetes Platform** reference architecture does it in its own -`platform.tf` using the two tokens this block outputs. With `output_to_vault` set, it writes them to +the cluster. The **STACKIT Kubernetes Platform** reference architecture, for example, does it in +its own `platform.tf` using the two tokens this block outputs. With `output_to_vault` set, it writes them to the Vault KV v2 secret `kubernetes/platform` under the keys `replicator-token` and `metering-token` instead, and both outputs are empty. diff --git a/modules/kubernetes/platform/main.tf b/modules/kubernetes/platform/main.tf index 981a6c4dd..03b3c8589 100644 --- a/modules/kubernetes/platform/main.tf +++ b/modules/kubernetes/platform/main.tf @@ -1,6 +1,6 @@ # The names below are cluster-global: `meshfed-service` and `meshfed-metering` are ClusterRoles, and -# two of these deployed to one cluster would collide. That is fine here — each platform ordered from -# the STACKIT Kubernetes Platform reference architecture gets a cluster of its own — and it is why +# two of these deployed to one cluster would collide. That is fine while every cluster carries one +# meshStack platform (the STACKIT Kubernetes Platform reference architecture gives each its own), and it is why # the upstream module's `name_suffix` / `existing_clusterrole_name` machinery is not carried over. # Reintroduce it if a cluster ever has to carry more than one meshStack platform registration. locals { diff --git a/modules/kubernetes/platform/output_to_vault.tf b/modules/kubernetes/platform/output_to_vault.tf index 806d1e60b..7308c021c 100644 --- a/modules/kubernetes/platform/output_to_vault.tf +++ b/modules/kubernetes/platform/output_to_vault.tf @@ -13,7 +13,8 @@ locals { provider "vault" { address = var.output_to_vault == null ? null : var.output_to_vault.address - # A Secrets Manager user may not create the child token the provider asks for by default. + # A Vault user without `auth/token/create`, such as a STACKIT Secrets Manager user, cannot create + # the child token the provider asks for by default. skip_child_token = true dynamic "auth_login_userpass" { diff --git a/modules/kubernetes/service-account/buildingblock/output_to_vault.tf b/modules/kubernetes/service-account/buildingblock/output_to_vault.tf index 602499fac..fab61ee2b 100644 --- a/modules/kubernetes/service-account/buildingblock/output_to_vault.tf +++ b/modules/kubernetes/service-account/buildingblock/output_to_vault.tf @@ -11,7 +11,8 @@ locals { provider "vault" { address = var.output_to_vault == null ? null : var.output_to_vault.address - # A Secrets Manager user may not create the child token the provider asks for by default. + # A Vault user without `auth/token/create`, such as a STACKIT Secrets Manager user, cannot create + # the child token the provider asks for by default. skip_child_token = true dynamic "auth_login_userpass" { From 9c6febd4d0224f1fa12c4eda277a3055505e1544 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:28:46 +0100 Subject: [PATCH 17/28] docs(kubernetes): rewrap the lines that named STACKIT Co-Authored-By: Claude Opus 5.5 --- modules/kubernetes/meshstack_integration.tf | 4 ++-- modules/kubernetes/platform/README.md | 4 ++-- modules/kubernetes/platform/main.tf | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/kubernetes/meshstack_integration.tf b/modules/kubernetes/meshstack_integration.tf index d97639b31..fc0c3e95f 100644 --- a/modules/kubernetes/meshstack_integration.tf +++ b/modules/kubernetes/meshstack_integration.tf @@ -426,8 +426,8 @@ resource "meshstack_building_block_definition" "this" { A platform team orders this once per cluster, as part of turning that cluster into a meshStack platform. The **STACKIT Kubernetes Platform** reference architecture, for example, does it right - after the cluster exists and feeds the tokens into the platform it registers. It reaches the cluster - through the kubeconfig the ordering composition passes as an input. + after the cluster exists and feeds the tokens into the platform it registers. It reaches the + cluster through the kubeconfig the ordering composition passes as an input. ## 📦 Resources created diff --git a/modules/kubernetes/platform/README.md b/modules/kubernetes/platform/README.md index e12603e09..f4f7ff3c1 100644 --- a/modules/kubernetes/platform/README.md +++ b/modules/kubernetes/platform/README.md @@ -29,8 +29,8 @@ is involved, so this runs on any conformant cluster. It does **not** register a meshPlatform. Deciding that a cluster becomes a meshStack platform — its identifier, location, landing zones and quota definitions — belongs to the composition that owns the cluster. The **STACKIT Kubernetes Platform** reference architecture, for example, does it in -its own `platform.tf` using the two tokens this block outputs. With `output_to_vault` set, it writes them to -the Vault KV v2 secret `kubernetes/platform` under the keys `replicator-token` and +its own `platform.tf` using the two tokens this block outputs. With `output_to_vault` set, it writes +them to the Vault KV v2 secret `kubernetes/platform` under the keys `replicator-token` and `metering-token` instead, and both outputs are empty. ## The kubeconfig is an input of the building block diff --git a/modules/kubernetes/platform/main.tf b/modules/kubernetes/platform/main.tf index 03b3c8589..00e17927f 100644 --- a/modules/kubernetes/platform/main.tf +++ b/modules/kubernetes/platform/main.tf @@ -1,7 +1,8 @@ # The names below are cluster-global: `meshfed-service` and `meshfed-metering` are ClusterRoles, and # two of these deployed to one cluster would collide. That is fine while every cluster carries one -# meshStack platform (the STACKIT Kubernetes Platform reference architecture gives each its own), and it is why -# the upstream module's `name_suffix` / `existing_clusterrole_name` machinery is not carried over. +# meshStack platform (the STACKIT Kubernetes Platform reference architecture gives each its own), +# and it is why the upstream module's `name_suffix` / `existing_clusterrole_name` machinery is not +# carried over. # Reintroduce it if a cluster ever has to carry more than one meshStack platform registration. locals { replicator_name = "meshfed-service" From 4fb2fa43264da8ea35d99a0066d0a29d9266aa65 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:40:29 +0100 Subject: [PATCH 18/28] feat(ref-arch/stackit-kubernetes): name the building blocks and project after the platform identifier The outer building block is named "STACKIT Kubernetes Platform " through the definition's display_name_template, the nested one "STACKIT Kubernetes Platform Services ", and a fresh meshProject "STACKIT Kubernetes Platform ", so several installs in one workspace can be told apart. The provider renames the nested building block in place without a run. Co-Authored-By: Claude Opus 5.5 --- .../IMPLEMENTATION_DETAILS.md | 10 +- .../stackit-kubernetes/README.md | 22 +- .../buildingblock/README.md | 2 +- .../stackit-kubernetes/buildingblock/main.tf | 2 +- .../buildingblock/outputs.tf | 4 +- .../buildingblock/platform.tf | 2 +- .../meshstack_integration.tf | 6 +- .../platform/meshstack_integration.tf | 2 +- .../stackit-kubernetes-building-blocks.dot | 6 +- .../stackit-kubernetes-building-blocks.svg | 2 +- .../stackit-kubernetes/stackit-kubernetes.dot | 2 +- .../stackit-kubernetes/stackit-kubernetes.svg | 196 +++++++++--------- 12 files changed, 131 insertions(+), 125 deletions(-) diff --git a/reference-architectures/stackit-kubernetes/IMPLEMENTATION_DETAILS.md b/reference-architectures/stackit-kubernetes/IMPLEMENTATION_DETAILS.md index 62feab1e6..fe157e042 100644 --- a/reference-architectures/stackit-kubernetes/IMPLEMENTATION_DETAILS.md +++ b/reference-architectures/stackit-kubernetes/IMPLEMENTATION_DETAILS.md @@ -11,11 +11,13 @@ no STACKIT identity and only bootstraps, in one apply: `editor` and `iam.member-admin` (the registry block grants members their role with it), 3. the **Bootstrap Identity Federation**, its child, trusting only the nested **STACKIT Kubernetes Platform Services** definition, -4. one **Platform Services** building block, with the account's email as a known input. +4. one **STACKIT Kubernetes Platform Services** building block, with the account's email as a + known input. -Platform Services acts as the account from its first run and orders the tree. The outer run waits -for it and shows its summary. Every outer input the tree needs reaches it as a Platform Services -input, so updating the outer building block is the only way to change the platform. +STACKIT Kubernetes Platform Services acts as the account from its first run and orders the tree. +The outer run waits for it and shows its summary. Every outer input the tree needs reaches it as a +STACKIT Kubernetes Platform Services input, so updating the outer building block is the only way +to change the platform. ## Federations and parents diff --git a/reference-architectures/stackit-kubernetes/README.md b/reference-architectures/stackit-kubernetes/README.md index 03ed34f74..50594a631 100644 --- a/reference-architectures/stackit-kubernetes/README.md +++ b/reference-architectures/stackit-kubernetes/README.md @@ -56,7 +56,7 @@ it registers and what each provisions: ![STACKIT Kubernetes — platform-team bootstrap](stackit-kubernetes.svg) -The building blocks Platform Services orders, with their parents as meshPanel shows them: +The building blocks STACKIT Kubernetes Platform Services orders, with their parents as meshPanel shows them: ![STACKIT Kubernetes building block tree](stackit-kubernetes-building-blocks.svg) @@ -116,6 +116,10 @@ Once set up, it triggers the repository's workflow and waits for it to finish. STACKIT project is not protected against deletion. It cannot be changed after the order. 3. Finish phase 2. +The definition's `display_name_template` names the building block `STACKIT Kubernetes Platform +` in the order form. A foundation that orders it through the meshStack +provider sets `display_name` to the same string. + The main inputs: | Input | Purpose | @@ -142,7 +146,7 @@ otherwise no tenant can be created on the landing zone. **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. +and the summary of STACKIT Kubernetes Platform Services. ### Phase 2: link the Harbor robot @@ -179,8 +183,8 @@ vault kv list -mount= / The [application-team diagram](#application-teams) shows the three definitions and what an order creates. -**Who registers it.** The nested **Platform Services** run registers the three definitions -application teams use, in `platform/buildingblock/starterkit.tf`. It does so only in phase 2, once +**Who registers it.** The nested **STACKIT Kubernetes Platform Services** run registers the three +definitions application teams use, in `platform/buildingblock/starterkit.tf`. It does so only in phase 2, once **Phase 2 Completed** is true, because their pipelines push images and their workloads pull them with the robots phase 2 mints: @@ -198,11 +202,11 @@ Platform**, next to the platform's own definitions. They carry the `starterkit_b tags and the **Starterkit Approval Policies**. **When it is released.** `hub.bbd_draft` of this definition decides it, and only for these three. -With `true`, every Platform Services run updates their draft version, and only the owning -workspace can order them. Use it to try a Hub ref. With `false`, the run releases them, and -application teams see **SKE Starterkit** in the marketplace. A new Hub ref takes two runs: one with -`true` adds a draft version on top of the released one, one with `false` releases it. Existing -starter kits stay on their version until they are upgraded. +With `true`, every STACKIT Kubernetes Platform Services run updates their draft version, and only +the owning workspace can order them. Use it to try a Hub ref. With `false`, the run releases them, +and application teams see **SKE Starterkit** in the marketplace. A new Hub ref takes two runs: one +with `true` adds a draft version on top of the released one, one with `false` releases it. +Existing starter kits stay on their version until they are upgraded. **How an application team orders it.** The team orders **SKE Starterkit** in its own workspace and gives a project name and a payment method. The run creates: diff --git a/reference-architectures/stackit-kubernetes/buildingblock/README.md b/reference-architectures/stackit-kubernetes/buildingblock/README.md index b0473cc45..a07397beb 100644 --- a/reference-architectures/stackit-kubernetes/buildingblock/README.md +++ b/reference-architectures/stackit-kubernetes/buildingblock/README.md @@ -8,7 +8,7 @@ architecture. It holds no STACKIT credential and declares no `stackit` provider. - the **Automation Identity**, from the landing zone's STACKIT Service Account definition, - the **Bootstrap Identity Federation**, its child, trusting only the nested **STACKIT Kubernetes Platform Services** definition, -- one **Platform Services** building block from that definition. Its run is in +- one **STACKIT Kubernetes Platform Services** building block from that definition. Its run is in [`../platform/buildingblock`](../platform/buildingblock/README.md). `imports.project` takes over the existing meshProject and its tenant. The architecture and adoption diff --git a/reference-architectures/stackit-kubernetes/buildingblock/main.tf b/reference-architectures/stackit-kubernetes/buildingblock/main.tf index 57d6891a2..eb67af585 100644 --- a/reference-architectures/stackit-kubernetes/buildingblock/main.tf +++ b/reference-architectures/stackit-kubernetes/buildingblock/main.tf @@ -34,7 +34,7 @@ locals { project_display_name = coalesce( local.imports.project == null ? null : local.imports.project.display_name, - "STACKIT Kubernetes Platform: ${local.platform_identifier}" + "STACKIT Kubernetes Platform ${local.platform_identifier}" ) # A production platform deletes what it adopted, like anything it created. diff --git a/reference-architectures/stackit-kubernetes/buildingblock/outputs.tf b/reference-architectures/stackit-kubernetes/buildingblock/outputs.tf index ef6de8686..a6d35feb8 100644 --- a/reference-architectures/stackit-kubernetes/buildingblock/outputs.tf +++ b/reference-architectures/stackit-kubernetes/buildingblock/outputs.tf @@ -5,7 +5,7 @@ output "ske_project_url" { output "summary" { description = "Summary of the platform, as the nested building block reports it." - # `try` because a Platform Services building block whose run failed has no outputs, and the - # destroy run that removes it still evaluates this. + # `try` because a STACKIT Kubernetes Platform Services building block whose run failed has no + # outputs, and the destroy run that removes it still evaluates this. value = try(jsondecode(meshstack_building_block.platform.status.outputs["summary"].value), "") } diff --git a/reference-architectures/stackit-kubernetes/buildingblock/platform.tf b/reference-architectures/stackit-kubernetes/buildingblock/platform.tf index 2532795ea..ad272a969 100644 --- a/reference-architectures/stackit-kubernetes/buildingblock/platform.tf +++ b/reference-architectures/stackit-kubernetes/buildingblock/platform.tf @@ -48,7 +48,7 @@ resource "meshstack_building_block" "platform" { spec = { parent_building_block_refs = [meshstack_building_block.bootstrap_federation.ref] building_block_definition_version_ref = module.platform_integration.building_block_definition.version_ref - display_name = "Platform Services" + display_name = "STACKIT Kubernetes Platform Services ${local.platform_identifier}" target_ref = module.tenant.tenant.ref inputs = { diff --git a/reference-architectures/stackit-kubernetes/meshstack_integration.tf b/reference-architectures/stackit-kubernetes/meshstack_integration.tf index e6996cfc4..d50c0c179 100644 --- a/reference-architectures/stackit-kubernetes/meshstack_integration.tf +++ b/reference-architectures/stackit-kubernetes/meshstack_integration.tf @@ -111,7 +111,7 @@ resource "meshstack_building_block_definition" "this" { spec = { display_name = "STACKIT Kubernetes Platform Reference Architecture" - display_name_template = "STACKIT Kubernetes Platform" + display_name_template = "STACKIT Kubernetes Platform {{platform_identifier}}" symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/reference-architectures/stackit-kubernetes/buildingblock/logo.png" description = "Bootstraps a Kubernetes platform on STACKIT on top of a STACKIT Landing Zone: SKE cluster, in-cluster platform services and the meshStack SKE platform with one landing zone per stage." support_url = "https://portal.stackit.cloud/ske" @@ -128,8 +128,8 @@ resource "meshstack_building_block_definition" "this" { - **Hosting project** – a STACKIT project (self-hosted as a meshStack tenant) that the cluster runs in. - **Service account** – ordered from the landing zone's STACKIT Service Account definition. - - **Platform Services** – one nested building block that orders everything below as the service - account. + - **STACKIT Kubernetes Platform Services** – one nested building block that orders everything + below as the service account. - **Secrets Manager** – a STACKIT Secrets Manager instance that holds every credential the building blocks below create, so no building block shows one as an output. - **SKE cluster** – a managed STACKIT Kubernetes Engine cluster with an admin kubeconfig. diff --git a/reference-architectures/stackit-kubernetes/platform/meshstack_integration.tf b/reference-architectures/stackit-kubernetes/platform/meshstack_integration.tf index 5ccc22ecd..862074945 100644 --- a/reference-architectures/stackit-kubernetes/platform/meshstack_integration.tf +++ b/reference-architectures/stackit-kubernetes/platform/meshstack_integration.tf @@ -78,7 +78,7 @@ resource "meshstack_building_block_definition" "this" { spec = { display_name = "STACKIT Kubernetes Platform Services" - display_name_template = "Platform Services" + display_name_template = "STACKIT Kubernetes Platform Services" symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/reference-architectures/stackit-kubernetes/buildingblock/logo.png" description = "The nested part of the STACKIT Kubernetes Platform: the SKE cluster, its platform services and the meshStack SKE platform, ordered by the STACKIT Kubernetes Platform building block." support_url = "https://portal.stackit.cloud/ske" diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.dot b/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.dot index 680ff3b32..c833de693 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.dot +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.dot @@ -1,6 +1,6 @@ /* - * Building block tree of the nested Platform Services building block. The outer layer that orders - * it is described in README.md, not drawn here. + * Building block tree of the nested STACKIT Kubernetes Platform Services building block. The outer + * layer that orders it is described in README.md, not drawn here. * Conventions: .agents/references/diagrams.md — render with: task diagrams */ digraph stackit_kubernetes_building_blocks { @@ -24,7 +24,7 @@ digraph stackit_kubernetes_building_blocks { { rank=same; BBD_FED -> SA [style=invis] } subgraph cluster_platform { - label=" Platform Services building block — acts as the Automation Identity" + label=" STACKIT Kubernetes Platform Services building block — acts as the Automation Identity" labeljust=l fontname="Helvetica" fontsize=12 fontcolor="#697180" style="rounded" color="#d5dae0" diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.svg b/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.svg index b4b7d91a4..b8e34f399 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.svg +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.svg @@ -9,7 +9,7 @@ cluster_platform -  Platform Services building block — acts as the Automation Identity +  STACKIT Kubernetes Platform Services building block — acts as the Automation Identity diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot b/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot index b512ec3e0..2ed989c26 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot @@ -28,7 +28,7 @@ digraph stackit_kubernetes_platform { margin=18 OUTER [label="📦 STACKIT Kubernetes Platform BBD\nordered once by the platform team" fillcolor="#ecedfb" color="#9aa2e6"] - SVC [label="📦 Platform Services BBD\nregisters and orders everything below" fillcolor="#ecedfb" color="#9aa2e6"] + SVC [label="📦 STACKIT Kubernetes Platform Services BBD\nregisters and orders everything below" fillcolor="#ecedfb" color="#9aa2e6"] BBD_S [label="📦 stackit/secrets-manager BBD" fillcolor="#ecedfb" color="#9aa2e6"] BBD_K [label="📦 ske/cluster BBD" fillcolor="#ecedfb" color="#9aa2e6"] BBD_I [label="📦 kubernetes/ingress BBD" fillcolor="#ecedfb" color="#9aa2e6"] diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg b/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg index 9bc1576be..e532029d2 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg @@ -1,269 +1,269 @@ - + stackit_kubernetes_platform - + cluster_mesh - +  meshStack cluster_stackit - -  STACKIT — one project per platform + +  STACKIT — one project per platform LZBB - -🧱 STACKIT Landing Zone -separate reference architecture + +🧱 STACKIT Landing Zone +separate reference architecture PROJ - -🗂️ STACKIT project -tenant on the landing zone platform + +🗂️ STACKIT project +tenant on the landing zone platform LZBB->PROJ - - -hosts + + +hosts OUTER - -📦 STACKIT Kubernetes Platform BBD -ordered once by the platform team + +📦 STACKIT Kubernetes Platform BBD +ordered once by the platform team OUTER->PROJ - - + + SA - -🔑 Automation Identity -the platform's service account + +🔑 Automation Identity +the platform's service account OUTER->SA - - -provisions + + +provisions SVC - -📦 Platform Services BBD -registers and orders everything below + +📦 STACKIT Kubernetes Platform Services BBD +registers and orders everything below SVC->OUTER - - -registers + + +registers BBD_S - -📦 stackit/secrets-manager BBD + +📦 stackit/secrets-manager BBD SM - -🔒 Secrets Manager -every platform credential + +🔒 Secrets Manager +every platform credential BBD_S->SM - - + + BBD_K - -📦 ske/cluster BBD + +📦 ske/cluster BBD SKE - -☸️ SKE cluster + +☸️ SKE cluster BBD_K->SKE - - + + KSA - -🔑 Platform admin -Kubernetes service account + +🔑 Platform admin +Kubernetes service account BBD_K->KSA - - + + BBD_I - -📦 kubernetes/ingress BBD + +📦 kubernetes/ingress BBD ING - -🔐 Ingress -HAProxy · cert-manager · Let's Encrypt + +🔐 Ingress +HAProxy · cert-manager · Let's Encrypt BBD_I->ING - - + + BBD_A - -📦 kubernetes BBD + +📦 kubernetes BBD AGENT - -🔑 meshStack identities -replicator · metering + +🔑 meshStack identities +replicator · metering BBD_A->AGENT - - + + PLAT - -🛰️ SKE Platform -one landing zone per stage + +🛰️ SKE Platform +one landing zone per stage PLAT->AGENT - - -replicates as + + +replicates as BBD_G - -📦 stackit/git BBD + +📦 stackit/git BBD GIT - -🔀 STACKIT Git -Forgejo instance and organization + +🔀 STACKIT Git +Forgejo instance and organization BBD_G->GIT - - + + BBD_H - -📦 stackit/container-registry BBD + +📦 stackit/container-registry BBD HARBOR - -🗄️ Container Registry -Harbor project · push and pull robots + +🗄️ Container Registry +Harbor project · push and pull robots BBD_H->HARBOR - - + + BBD_D - -📦 stackit/dns BBD + +📦 stackit/dns BBD DNS - -🧭 DNS zone -wildcard to the load balancer + +🧭 DNS zone +wildcard to the load balancer BBD_D->DNS - - + + BBD_M - -📦 stackit/ai-llm BBD + +📦 stackit/ai-llm BBD MODEL - -🧠 Model Serving -API token + +🧠 Model Serving +API token BBD_M->MODEL - - + + From 915d6e1a059560d7e20c42ad19a78a1335a838c9 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:40:43 +0100 Subject: [PATCH 19/28] feat(ref-arch/stackit-landingzone): name the building block after the platform identifier The definition's display_name_template names the building block "STACKIT Landing Zone " in the order form, so several landing zones in one workspace can be told apart. Co-Authored-By: Claude Opus 5.5 --- .../stackit-landingzone/README.md | 4 ++++ .../stackit-landingzone/meshstack_integration.tf | 15 ++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/reference-architectures/stackit-landingzone/README.md b/reference-architectures/stackit-landingzone/README.md index 96b51ca80..e0e623faf 100644 --- a/reference-architectures/stackit-landingzone/README.md +++ b/reference-architectures/stackit-landingzone/README.md @@ -166,6 +166,10 @@ spoke `stackit/network` building block in the same apply. Application teams can projects and — when networking is enabled — order `stackit/network` inside their own STACKIT projects once those projects exist. +The definition's `display_name_template` names the building block `STACKIT Landing Zone ` in the order form. A foundation that orders it through the meshStack provider sets +`display_name` to the same string. + ### Root Folder One STACKIT organization can serve several meshStacks, for example a dev, a demo and a trial diff --git a/reference-architectures/stackit-landingzone/meshstack_integration.tf b/reference-architectures/stackit-landingzone/meshstack_integration.tf index 1dbb0da5a..0d8619671 100644 --- a/reference-architectures/stackit-landingzone/meshstack_integration.tf +++ b/reference-architectures/stackit-landingzone/meshstack_integration.tf @@ -135,13 +135,14 @@ resource "meshstack_building_block_definition" "this" { } spec = { - display_name = coalesce(var.bbd_display_name, "STACKIT Landing Zone Reference Architecture") - symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/reference-architectures/stackit-landingzone/buildingblock/logo.png" - description = coalesce(var.bbd_description, "Onboards a STACKIT sandbox platform into meshStack: a location, resourcemanager folder and the STACKIT Project platform with its default landing zone. Optionally layers on a hub-and-spoke network topology when a network config is provided.") - support_url = "https://portal.stackit.cloud" - target_type = "WORKSPACE_LEVEL" - run_transparency = true - approval_policies = var.approval_policies + display_name = coalesce(var.bbd_display_name, "STACKIT Landing Zone Reference Architecture") + display_name_template = "STACKIT Landing Zone {{platform_identifier}}" + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/reference-architectures/stackit-landingzone/buildingblock/logo.png" + description = coalesce(var.bbd_description, "Onboards a STACKIT sandbox platform into meshStack: a location, resourcemanager folder and the STACKIT Project platform with its default landing zone. Optionally layers on a hub-and-spoke network topology when a network config is provided.") + support_url = "https://portal.stackit.cloud" + target_type = "WORKSPACE_LEVEL" + run_transparency = true + approval_policies = var.approval_policies readme = coalesce(var.bbd_readme, chomp(<<-EOT The **STACKIT Landing Zone** building block bootstraps a complete STACKIT sandbox platform From 3181f2667d063ef49e727e4fae04200acd1d1048 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:41:27 +0100 Subject: [PATCH 20/28] feat(ref-arch/stackit-kubernetes)!: rename an adopted project to the standard name imports.project no longer takes a display_name: an adopted meshProject is renamed to "STACKIT Kubernetes Platform " like a fresh one. Its identifier stays, so the rename happens in place. BREAKING CHANGE: callers that pass imports.project.display_name must drop it. Co-Authored-By: Claude Opus 5.5 --- reference-architectures/stackit-kubernetes/README.md | 2 +- .../stackit-kubernetes/buildingblock/main.tf | 5 +---- .../stackit-kubernetes/buildingblock/variables.tf | 1 - .../stackit-kubernetes/meshstack_integration.tf | 1 - 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/reference-architectures/stackit-kubernetes/README.md b/reference-architectures/stackit-kubernetes/README.md index 50594a631..0f686777d 100644 --- a/reference-architectures/stackit-kubernetes/README.md +++ b/reference-architectures/stackit-kubernetes/README.md @@ -226,7 +226,7 @@ built before this architecture: | Key | Takes over | |---|---| -| `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 | +| `project = {admin_bindings}` | the meshProject `overrides.project_identifier` and its tenant on the landing zone platform, renamed to `STACKIT Kubernetes Platform `; `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 Domain** names and its wildcard record set | diff --git a/reference-architectures/stackit-kubernetes/buildingblock/main.tf b/reference-architectures/stackit-kubernetes/buildingblock/main.tf index eb67af585..b3f45cd11 100644 --- a/reference-architectures/stackit-kubernetes/buildingblock/main.tf +++ b/reference-architectures/stackit-kubernetes/buildingblock/main.tf @@ -32,10 +32,7 @@ locals { 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, - "STACKIT Kubernetes Platform ${local.platform_identifier}" - ) + project_display_name = "STACKIT Kubernetes Platform ${local.platform_identifier}" # A production platform deletes what it adopted, like anything it created. release_imports_on_destroy = var.playground_mode && local.imports.project != null diff --git a/reference-architectures/stackit-kubernetes/buildingblock/variables.tf b/reference-architectures/stackit-kubernetes/buildingblock/variables.tf index d7b645fee..27175f332 100644 --- a/reference-architectures/stackit-kubernetes/buildingblock/variables.tf +++ b/reference-architectures/stackit-kubernetes/buildingblock/variables.tf @@ -209,7 +209,6 @@ variable "starterkit_approval_policies" { variable "imports" { type = object({ project = optional(object({ - display_name = optional(string) admin_bindings = optional(map(string), {}) })) adopt_ske = optional(bool, false) diff --git a/reference-architectures/stackit-kubernetes/meshstack_integration.tf b/reference-architectures/stackit-kubernetes/meshstack_integration.tf index d50c0c179..a2c2d3eda 100644 --- a/reference-architectures/stackit-kubernetes/meshstack_integration.tf +++ b/reference-architectures/stackit-kubernetes/meshstack_integration.tf @@ -523,7 +523,6 @@ resource "meshstack_building_block_definition" "this" { 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" } } } } From a4fdc1b5e4daee6bd9b25c1682e8f45c090be634 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:48:21 +0100 Subject: [PATCH 21/28] test(ref-arch/stackit-kubernetes): name the e2e building block after the platform identifier The run id stays in front, so the cleanup still finds the block. Co-Authored-By: Claude Opus 5.5 --- reference-architectures/stackit-kubernetes/e2e/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference-architectures/stackit-kubernetes/e2e/main.tf b/reference-architectures/stackit-kubernetes/e2e/main.tf index eebfc7698..0cd58b0c3 100644 --- a/reference-architectures/stackit-kubernetes/e2e/main.tf +++ b/reference-architectures/stackit-kubernetes/e2e/main.tf @@ -120,7 +120,7 @@ resource "meshstack_building_block" "this" { spec = { building_block_definition_version_ref = { uuid = module.stackit_kubernetes.building_block_definition.version_ref.uuid } - display_name = "${var.test_context.run_id} STACKIT Kubernetes Platform" + display_name = "${var.test_context.run_id} STACKIT Kubernetes Platform ${var.test_context.run_id}" target_ref = { kind = "meshWorkspace" name = var.test_context.workspace From 0f266dddeadaea78b0af679e66d53179a626e665 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:52:43 +0100 Subject: [PATCH 22/28] docs(ref-arch/stackit-kubernetes): show the demo app inside the namespace in the app-team diagram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stages read as {dev,prod,…}, and the DNS zone no longer claims a wildcard hostname. Co-Authored-By: Claude Opus 5.5 --- .../stackit-kubernetes-app-team.dot | 16 +- .../stackit-kubernetes-app-team.svg | 171 +++++++++--------- 2 files changed, 97 insertions(+), 90 deletions(-) diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes-app-team.dot b/reference-architectures/stackit-kubernetes/stackit-kubernetes-app-team.dot index 428183eb2..e5eb0d2bf 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes-app-team.dot +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes-app-team.dot @@ -41,12 +41,18 @@ digraph stackit_kubernetes_app_team { margin=18 REPO [label="🔀 App repository ×1\ncloned from the template" fillcolor="#e5f2ea" color="#85bfa0"] - PROJ [label="🗂️ Project ×N\none per stage" fillcolor="#e5f2ea" color="#85bfa0"] - ACT [label="⚙️ Forgejo Actions ×N\none pipeline per stage" fillcolor="#e5f2ea" color="#85bfa0"] - NS [label="☸️ Namespace ×N\none per stage" fillcolor="#e5f2ea" color="#85bfa0"] + PROJ [label="🗂️ Project ×{dev,prod,…}" fillcolor="#e5f2ea" color="#85bfa0"] + ACT [label="⚙️ Forgejo Actions ×{dev,prod,…}" fillcolor="#e5f2ea" color="#85bfa0"] + subgraph cluster_ns { + label="☸️ Namespace ×{dev,prod,…}" + fontname="Helvetica" fontsize=11 + style="rounded,filled" fillcolor="#e5f2ea" color="#85bfa0" + margin=12 + + NS [label="🤖 ai-summarizer\ndemo Deployment" fillcolor="#ffffff" color="#85bfa0"] + } { rank=same; REPO -> PROJ [style=invis] } - { rank=same; ACT -> NS [style=invis] } REPO -> ACT [xlabel="runs pipeline"] PROJ -> NS [xlabel="tenant namespace"] @@ -62,7 +68,7 @@ digraph stackit_kubernetes_app_team { HARBOR [label="🗄️ Container Registry\nHarbor project per platform" fillcolor="#eef2f6" color="#93a7bb"] MODEL [label="🧠 Model Serving\nLLM API" fillcolor="#eef2f6" color="#93a7bb"] ING [label="🔐 Ingress + cert-manager\nHAProxy LoadBalancer" fillcolor="#eef2f6" color="#93a7bb"] - DNS [label="🧭 DNS zone\nwildcard hostname" fillcolor="#eef2f6" color="#93a7bb"] + DNS [label="🧭 DNS zone" fillcolor="#eef2f6" color="#93a7bb"] { rank=same; HARBOR -> MODEL -> ING -> DNS [style=invis] } } diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes-app-team.svg b/reference-architectures/stackit-kubernetes/stackit-kubernetes-app-team.svg index a4dbad199..60023eaa6 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes-app-team.svg +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes-app-team.svg @@ -1,186 +1,187 @@ - - + + stackit_kubernetes_app_team - + cluster_catalog - -  meshStack — self-service order + +  meshStack — self-service order cluster_team - -  Per Application Team + +  Per Application Team + + +cluster_ns + +☸️ Namespace ×{dev,prod,…} cluster_shared - -  STACKIT platform — shared services + +  STACKIT platform — shared services BBD_S - -📦 ske/ske-starterkit BBD -single catalog entry + +📦 ske/ske-starterkit BBD +single catalog entry BBD_R - -📦 stackit/git-repository BBD + +📦 stackit/git-repository BBD BBD_S->BBD_R - - -composes + + +composes BBD_C - -📦 ske/forgejo-connector BBD + +📦 ske/forgejo-connector BBD BBD_S->BBD_C - - + + PROJ - -🗂️ Project ×N -one per stage + +🗂️ Project ×{dev,prod,…} - + BBD_S->PROJ - - + + REPO - -🔀 App repository ×1 -cloned from the template + +🔀 App repository ×1 +cloned from the template - + BBD_R->REPO - - -provisions + + +provisions ACT - -⚙️ Forgejo Actions ×N -one pipeline per stage + +⚙️ Forgejo Actions ×{dev,prod,…} - + BBD_C->ACT - - + + - + REPO->ACT - - -runs pipeline + + +runs pipeline NS - -☸️ Namespace ×N -one per stage + +🤖 ai-summarizer +demo Deployment - + PROJ->NS - - -tenant namespace + + +tenant namespace - - + ACT->NS - - -deploys to + + +deploys to HARBOR - -🗄️ Container Registry -Harbor project per platform + +🗄️ Container Registry +Harbor project per platform - + ACT->HARBOR - - -pushes images + + +pushes images - + NS->HARBOR - - -pulls images + + +pulls images MODEL - -🧠 Model Serving -LLM API + +🧠 Model Serving +LLM API - + NS->MODEL - - -calls with injected key + + +calls with injected key ING - -🔐 Ingress + cert-manager -HAProxy LoadBalancer + +🔐 Ingress + cert-manager +HAProxy LoadBalancer - + NS->ING - - -exposed via + + +exposed via DNS - -🧭 DNS zone -wildcard hostname + +🧭 DNS zone From 1c6436ea74b3d5b95c589d1529ebacf9d484e10b Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:53:38 +0100 Subject: [PATCH 23/28] docs(ref-arch/stackit-kubernetes): draw only building blocks and their parents in the tree The tree showed Secrets Manager users, the meshStack platform and a definition as boxes, and labelled edges with data flows. It now has one box per building block, unlabelled parent-to-child arrows taken from the code, and frames for the run that orders them. Co-Authored-By: Claude Opus 5.5 --- .../stackit-kubernetes/README.md | 3 +- .../stackit-kubernetes-building-blocks.dot | 80 +++--- .../stackit-kubernetes-building-blocks.svg | 252 ++++++++---------- 3 files changed, 155 insertions(+), 180 deletions(-) diff --git a/reference-architectures/stackit-kubernetes/README.md b/reference-architectures/stackit-kubernetes/README.md index 0f686777d..12657569f 100644 --- a/reference-architectures/stackit-kubernetes/README.md +++ b/reference-architectures/stackit-kubernetes/README.md @@ -56,7 +56,8 @@ it registers and what each provisions: ![STACKIT Kubernetes — platform-team bootstrap](stackit-kubernetes.svg) -The building blocks STACKIT Kubernetes Platform Services orders, with their parents as meshPanel shows them: +Every building block of the platform, with an arrow from each parent to its child as meshPanel shows +them. The frames show which run orders them: ![STACKIT Kubernetes building block tree](stackit-kubernetes-building-blocks.svg) diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.dot b/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.dot index c833de693..a12540852 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.dot +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.dot @@ -1,67 +1,65 @@ /* - * Building block tree of the nested STACKIT Kubernetes Platform Services building block. The outer - * layer that orders it is described in README.md, not drawn here. + * Building blocks of the STACKIT Kubernetes Platform, with their parents as meshPanel shows them. + * Every box is a building block and every arrow points from parent to child; the frames show which + * run orders the boxes inside them. * Conventions: .agents/references/diagrams.md — render with: task diagrams */ digraph stackit_kubernetes_building_blocks { rankdir=TB splines=ortho - forcelabels=true bgcolor="white" nodesep=0.55 - ranksep=0.85 + ranksep=0.7 node [shape=box style="rounded,filled" fontname="Helvetica" fontsize=11 - fillcolor="#ffffff" color="#a2abb8" penwidth=1.1 margin="0.20,0.11"] - edge [fontname="Helvetica" fontsize=9 fontcolor="#697180" color="#8b95a3" arrowsize=0.7] + fillcolor="#e5f2ea" color="#85bfa0" penwidth=1.1 margin="0.20,0.11"] + edge [color="#8b95a3" arrowsize=0.7] - // Context only: the landing zone's definition and the outer run's identity. - BBD_FED [label="📦 stackit/service-account-federation BBD\nfrom the STACKIT Landing Zone" - style="rounded,filled,dashed" fillcolor="#f0f1f3" color="#cdd2d8" fontcolor="#99a1ac"] - SA [label="🔑 Automation Identity\nordered by the outer run" - style="rounded,filled,dashed" fillcolor="#f0f1f3" color="#cdd2d8" fontcolor="#99a1ac"] + subgraph cluster_outer { + label=" ordered by STACKIT Kubernetes Platform " + labeljust=l + fontname="Helvetica" fontsize=12 fontcolor="#697180" + style="rounded" color="#d5dae0" + margin=18 - { rank=same; BBD_FED -> SA [style=invis] } + SA [label="🔑 Automation Identity\nstackit/service-account"] + BOOT [label="🤝 Bootstrap Identity Federation\nstackit/service-account-federation"] + SVC [label="📦 STACKIT Kubernetes Platform Services " fillcolor="#ecedfb" color="#9aa2e6"] + } subgraph cluster_platform { - label=" STACKIT Kubernetes Platform Services building block — acts as the Automation Identity" + label=" ordered by STACKIT Kubernetes Platform Services " labeljust=l fontname="Helvetica" fontsize=12 fontcolor="#697180" style="rounded" color="#d5dae0" margin=18 - FED [label="🤝 Automation Identity Federation\ntrusts the six STACKIT definitions" fillcolor="#e5f2ea" color="#85bfa0"] - SM [label="🔒 Secrets Manager\nstackit/secrets-manager" fillcolor="#e5f2ea" color="#85bfa0"] - USERS [label="🔒 Secrets Manager users\nwriter · reader" fillcolor="#eef2f6" color="#93a7bb"] - CLUSTER [label="☸️ SKE Cluster\nske/cluster · platform admin" fillcolor="#e5f2ea" color="#85bfa0"] - GIT [label="🔀 STACKIT Git Instance\nstackit/git" fillcolor="#e5f2ea" color="#85bfa0"] - REG [label="🗄️ STACKIT Container Registry\nstackit/container-registry" fillcolor="#e5f2ea" color="#85bfa0"] - AI [label="🧠 AI Model Serving\nstackit/ai-llm" fillcolor="#e5f2ea" color="#85bfa0"] - CREDS [label="🔑 Kubernetes meshPlatform Credentials\nkubernetes" fillcolor="#e5f2ea" color="#85bfa0"] - ING [label="🔐 Kubernetes Ingress\nkubernetes/ingress" fillcolor="#e5f2ea" color="#85bfa0"] - DNS [label="🧭 DNS Zone\nstackit/dns" fillcolor="#e5f2ea" color="#85bfa0"] - MP [label="🛰️ meshStack SKE platform\nlanding zone per stage" fillcolor="#ecedfb" color="#9aa2e6"] - SKFED [label="🤝 Starter Kit Identity Federation\nphase 2 · trusts stackit/git-repository" - style="rounded,filled,dashed" fillcolor="#e5f2ea" color="#85bfa0"] + FED [label="🤝 Automation Identity Federation\nstackit/service-account-federation"] + SKFED [label="🤝 Starter Kit Identity Federation\nphase 2" style="rounded,filled,dashed"] + SM [label="🔒 Secrets Manager\nstackit/secrets-manager"] + CLUSTER [label="☸️ SKE Cluster\nske/cluster"] + GIT [label="🔀 STACKIT Git Instance\nstackit/git"] + REG [label="🗄️ STACKIT Container Registry\nstackit/container-registry"] + AI [label="🧠 AI Model Serving\nstackit/ai-llm · optional" style="rounded,filled,dashed"] + CREDS [label="🔑 Kubernetes meshPlatform Credentials\nkubernetes"] + ING [label="🔐 Kubernetes Ingress\nkubernetes/ingress"] + DNS [label="🧭 DNS Zone\nstackit/dns"] { rank=same; SM -> CLUSTER -> GIT -> REG -> AI [style=invis] } { rank=same; CREDS -> ING [style=invis] } - - FED -> CLUSTER [xlabel="parent of"] - FED -> GIT - FED -> REG - FED -> AI - FED -> SM - SM -> USERS [xlabel="instance"] - CLUSTER -> CREDS [xlabel="admin kubeconfig via\nSecrets Manager"] - CLUSTER -> ING - ING -> DNS [xlabel="load balancer IP"] - FED -> DNS [constraint=false] - CREDS -> MP [xlabel="tokens via\nSecrets Manager" style=dashed] } - SA -> FED [xlabel="parent of"] + SA -> BOOT + BOOT -> SVC + SA -> FED SA -> SKFED - BBD_FED -> FED [style=dotted] - BBD_FED -> SKFED [style=dotted] + FED -> SM + FED -> CLUSTER + FED -> GIT + FED -> REG + FED -> AI + CLUSTER -> CREDS + CLUSTER -> ING + ING -> DNS + FED -> DNS [constraint=false] } diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.svg b/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.svg index b8e34f399..a6199f36e 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.svg +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.svg @@ -1,217 +1,193 @@ - - + + stackit_kubernetes_building_blocks - + + +cluster_outer + +  ordered by STACKIT Kubernetes Platform <id> + cluster_platform - -  STACKIT Kubernetes Platform Services building block — acts as the Automation Identity + +  ordered by STACKIT Kubernetes Platform Services <id> - + -BBD_FED - -📦 stackit/service-account-federation BBD -from the STACKIT Landing Zone +SA + +🔑 Automation Identity +stackit/service-account - + -SA - -🔑 Automation Identity -ordered by the outer run +BOOT + +🤝 Bootstrap Identity Federation +stackit/service-account-federation + + + +SA->BOOT + + - - + FED - -🤝 Automation Identity Federation -trusts the six STACKIT definitions + +🤝 Automation Identity Federation +stackit/service-account-federation - - -BBD_FED->FED - - + + +SA->FED + + - + SKFED - -🤝 Starter Kit Identity Federation -phase 2 · trusts stackit/git-repository - - - -BBD_FED->SKFED - - - - - -SA->FED - - -parent of + +🤝 Starter Kit Identity Federation +phase 2 - + SA->SKFED - - + + + + + +SVC + +📦 STACKIT Kubernetes Platform Services <id> + + + +BOOT->SVC + + - + SM - -🔒 Secrets Manager -stackit/secrets-manager + +🔒 Secrets Manager +stackit/secrets-manager - + FED->SM - - + + - + CLUSTER - -☸️ SKE Cluster -ske/cluster · platform admin + +☸️ SKE Cluster +ske/cluster - + FED->CLUSTER - - -parent of + + - + GIT - -🔀 STACKIT Git Instance -stackit/git + +🔀 STACKIT Git Instance +stackit/git - + FED->GIT - - + + - + REG - -🗄️ STACKIT Container Registry -stackit/container-registry + +🗄️ STACKIT Container Registry +stackit/container-registry - + FED->REG - - + + - + AI - -🧠 AI Model Serving -stackit/ai-llm + +🧠 AI Model Serving +stackit/ai-llm · optional - + FED->AI - - + + - + DNS - -🧭 DNS Zone -stackit/dns + +🧭 DNS Zone +stackit/dns - + FED->DNS - - - - - -USERS - -🔒 Secrets Manager users -writer · reader - - - -SM->USERS - - -instance + + - + CREDS - -🔑 Kubernetes meshPlatform Credentials -kubernetes + +🔑 Kubernetes meshPlatform Credentials +kubernetes - + CLUSTER->CREDS - - -admin kubeconfig via -Secrets Manager + + - + ING - -🔐 Kubernetes Ingress -kubernetes/ingress + +🔐 Kubernetes Ingress +kubernetes/ingress - + CLUSTER->ING - - + + - - -MP - -🛰️ meshStack SKE platform -landing zone per stage - - - -CREDS->MP - - -tokens via -Secrets Manager - - + ING->DNS - - -load balancer IP + + From 4511e3763f597ac35df15295fb44ea36ac7f050f Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 14:56:43 +0100 Subject: [PATCH 24/28] docs(ref-arch/stackit-kubernetes): name the meshProject and meshTenant in the platform-team diagram The STACKIT resources sit in the STACKIT Kubernetes Platform meshProject, whose STACKIT project is a meshTenant on the landing zone's STACKIT meshPlatform. The BBD boxes lose their usage notes, and the README text above both platform-team diagrams says how they relate. Co-Authored-By: Claude Opus 5.5 --- .../stackit-kubernetes/README.md | 7 +- .../stackit-kubernetes/stackit-kubernetes.dot | 10 +- .../stackit-kubernetes/stackit-kubernetes.svg | 162 +++++++++--------- 3 files changed, 89 insertions(+), 90 deletions(-) diff --git a/reference-architectures/stackit-kubernetes/README.md b/reference-architectures/stackit-kubernetes/README.md index 12657569f..e06c66628 100644 --- a/reference-architectures/stackit-kubernetes/README.md +++ b/reference-architectures/stackit-kubernetes/README.md @@ -51,12 +51,13 @@ platform's STACKIT Secrets Manager. ### Platform team -The platform team orders the **STACKIT Kubernetes Platform** building block once. The definitions -it registers and what each provisions: +The platform team orders the **STACKIT Kubernetes Platform** building block. The definitions it +registers, and what each one provisions in the STACKIT project, which is the meshTenant of the +**STACKIT Kubernetes Platform** meshProject on the landing zone's STACKIT meshPlatform: ![STACKIT Kubernetes — platform-team bootstrap](stackit-kubernetes.svg) -Every building block of the platform, with an arrow from each parent to its child as meshPanel shows +The same building blocks as a tree, with an arrow from each parent to its child as meshPanel shows them. The frames show which run orders them: ![STACKIT Kubernetes building block tree](stackit-kubernetes-building-blocks.svg) diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot b/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot index 2ed989c26..b81e34bc0 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot @@ -27,13 +27,13 @@ digraph stackit_kubernetes_platform { style="rounded" color="#d5dae0" margin=18 - OUTER [label="📦 STACKIT Kubernetes Platform BBD\nordered once by the platform team" fillcolor="#ecedfb" color="#9aa2e6"] - SVC [label="📦 STACKIT Kubernetes Platform Services BBD\nregisters and orders everything below" fillcolor="#ecedfb" color="#9aa2e6"] + OUTER [label="📦 STACKIT Kubernetes Platform BBD" fillcolor="#ecedfb" color="#9aa2e6"] + SVC [label="📦 STACKIT Kubernetes Platform Services BBD" fillcolor="#ecedfb" color="#9aa2e6"] BBD_S [label="📦 stackit/secrets-manager BBD" fillcolor="#ecedfb" color="#9aa2e6"] BBD_K [label="📦 ske/cluster BBD" fillcolor="#ecedfb" color="#9aa2e6"] BBD_I [label="📦 kubernetes/ingress BBD" fillcolor="#ecedfb" color="#9aa2e6"] BBD_A [label="📦 kubernetes BBD" fillcolor="#ecedfb" color="#9aa2e6"] - PLAT [label="🛰️ SKE Platform\none landing zone per stage" fillcolor="#ecedfb" color="#9aa2e6"] + PLAT [label="🛰️ SKE meshPlatform\nlanding zones ×{dev,prod,…}" fillcolor="#ecedfb" color="#9aa2e6"] BBD_G [label="📦 stackit/git BBD" fillcolor="#ecedfb" color="#9aa2e6"] BBD_H [label="📦 stackit/container-registry BBD" fillcolor="#ecedfb" color="#9aa2e6"] BBD_D [label="📦 stackit/dns BBD" fillcolor="#ecedfb" color="#9aa2e6"] @@ -45,13 +45,13 @@ digraph stackit_kubernetes_platform { } subgraph cluster_stackit { - label=" STACKIT — one project per platform" + label=" STACKIT Kubernetes Platform meshProject" labeljust=l fontname="Helvetica" fontsize=12 fontcolor="#697180" style="rounded" color="#d5dae0" margin=18 - PROJ [label="🗂️ STACKIT project\ntenant on the landing zone platform" fillcolor="#eef2f6" color="#93a7bb"] + PROJ [label="🗂️ STACKIT project\nmeshTenant on the STACKIT meshPlatform" fillcolor="#eef2f6" color="#93a7bb"] SA [label="🔑 Automation Identity\nthe platform's service account" fillcolor="#eef2f6" color="#93a7bb"] SM [label="🔒 Secrets Manager\nevery platform credential" fillcolor="#eef2f6" color="#93a7bb"] SKE [label="☸️ SKE cluster" fillcolor="#eef2f6" color="#93a7bb"] diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg b/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg index e532029d2..0c7c0e05d 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg @@ -1,82 +1,80 @@ - + stackit_kubernetes_platform - + cluster_mesh - -  meshStack + +  meshStack cluster_stackit - -  STACKIT — one project per platform + +  STACKIT Kubernetes Platform meshProject LZBB - -🧱 STACKIT Landing Zone -separate reference architecture + +🧱 STACKIT Landing Zone +separate reference architecture PROJ - -🗂️ STACKIT project -tenant on the landing zone platform + +🗂️ STACKIT project +meshTenant on the STACKIT meshPlatform LZBB->PROJ - - -hosts + + +hosts OUTER - -📦 STACKIT Kubernetes Platform BBD -ordered once by the platform team + +📦 STACKIT Kubernetes Platform BBD OUTER->PROJ - - + + SA - -🔑 Automation Identity -the platform's service account + +🔑 Automation Identity +the platform's service account OUTER->SA - - -provisions + + +provisions SVC - -📦 STACKIT Kubernetes Platform Services BBD -registers and orders everything below + +📦 STACKIT Kubernetes Platform Services BBD SVC->OUTER - - -registers + + +registers @@ -88,15 +86,15 @@ SM - -🔒 Secrets Manager -every platform credential + +🔒 Secrets Manager +every platform credential BBD_S->SM - - + + @@ -108,27 +106,27 @@ SKE - -☸️ SKE cluster + +☸️ SKE cluster BBD_K->SKE - - + + KSA - -🔑 Platform admin -Kubernetes service account + +🔑 Platform admin +Kubernetes service account BBD_K->KSA - - + + @@ -140,15 +138,15 @@ ING - -🔐 Ingress -HAProxy · cert-manager · Let's Encrypt + +🔐 Ingress +HAProxy · cert-manager · Let's Encrypt BBD_I->ING - - + + @@ -160,30 +158,30 @@ AGENT - -🔑 meshStack identities -replicator · metering + +🔑 meshStack identities +replicator · metering BBD_A->AGENT - - + + PLAT -🛰️ SKE Platform -one landing zone per stage +🛰️ SKE meshPlatform +landing zones ×{dev,prod,…} PLAT->AGENT - - -replicates as + + +replicates as @@ -195,15 +193,15 @@ GIT - -🔀 STACKIT Git -Forgejo instance and organization + +🔀 STACKIT Git +Forgejo instance and organization BBD_G->GIT - - + + @@ -215,15 +213,15 @@ HARBOR - -🗄️ Container Registry -Harbor project · push and pull robots + +🗄️ Container Registry +Harbor project · push and pull robots BBD_H->HARBOR - - + + @@ -235,15 +233,15 @@ DNS - -🧭 DNS zone -wildcard to the load balancer + +🧭 DNS zone +wildcard to the load balancer BBD_D->DNS - - + + @@ -255,15 +253,15 @@ MODEL - -🧠 Model Serving -API token + +🧠 Model Serving +API token BBD_M->MODEL - - + + From 316cfa7b7cbf7f92638fb25db8157b8d09cde168 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 15:10:24 +0100 Subject: [PATCH 25/28] docs(ref-arch/stackit-kubernetes): drop the wildcard note from the platform-team diagram Co-Authored-By: Claude Opus 5.5 --- .../stackit-kubernetes/stackit-kubernetes.dot | 2 +- .../stackit-kubernetes/stackit-kubernetes.svg | 189 +++++++++--------- 2 files changed, 95 insertions(+), 96 deletions(-) diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot b/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot index b81e34bc0..7f403b1a0 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot @@ -60,7 +60,7 @@ digraph stackit_kubernetes_platform { AGENT [label="🔑 meshStack identities\nreplicator · metering" fillcolor="#eef2f6" color="#93a7bb"] GIT [label="🔀 STACKIT Git\nForgejo instance and organization" fillcolor="#eef2f6" color="#93a7bb"] HARBOR [label="🗄️ Container Registry\nHarbor project · push and pull robots" fillcolor="#eef2f6" color="#93a7bb"] - DNS [label="🧭 DNS zone\nwildcard to the load balancer" fillcolor="#eef2f6" color="#93a7bb"] + DNS [label="🧭 DNS zone" fillcolor="#eef2f6" color="#93a7bb"] MODEL [label="🧠 Model Serving\nAPI token" fillcolor="#eef2f6" color="#93a7bb"] // Keeps each resource level with its definition opposite the SKE Platform row. diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg b/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg index 0c7c0e05d..c9c9ec2b9 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg @@ -1,253 +1,252 @@ - - + + stackit_kubernetes_platform - + cluster_mesh - -  meshStack + +  meshStack cluster_stackit - -  STACKIT Kubernetes Platform meshProject + +  STACKIT Kubernetes Platform meshProject LZBB - -🧱 STACKIT Landing Zone -separate reference architecture + +🧱 STACKIT Landing Zone +separate reference architecture PROJ - -🗂️ STACKIT project -meshTenant on the STACKIT meshPlatform + +🗂️ STACKIT project +meshTenant on the STACKIT meshPlatform LZBB->PROJ - - -hosts + + +hosts OUTER - -📦 STACKIT Kubernetes Platform BBD + +📦 STACKIT Kubernetes Platform BBD OUTER->PROJ - - + + SA - -🔑 Automation Identity -the platform's service account + +🔑 Automation Identity +the platform's service account OUTER->SA - - -provisions + + +provisions SVC - -📦 STACKIT Kubernetes Platform Services BBD + +📦 STACKIT Kubernetes Platform Services BBD SVC->OUTER - - -registers + + +registers BBD_S - -📦 stackit/secrets-manager BBD + +📦 stackit/secrets-manager BBD SM - -🔒 Secrets Manager -every platform credential + +🔒 Secrets Manager +every platform credential BBD_S->SM - - + + BBD_K - -📦 ske/cluster BBD + +📦 ske/cluster BBD SKE - -☸️ SKE cluster + +☸️ SKE cluster BBD_K->SKE - - + + KSA - -🔑 Platform admin -Kubernetes service account + +🔑 Platform admin +Kubernetes service account BBD_K->KSA - - + + BBD_I - -📦 kubernetes/ingress BBD + +📦 kubernetes/ingress BBD ING - -🔐 Ingress -HAProxy · cert-manager · Let's Encrypt + +🔐 Ingress +HAProxy · cert-manager · Let's Encrypt BBD_I->ING - - + + BBD_A - -📦 kubernetes BBD + +📦 kubernetes BBD AGENT - -🔑 meshStack identities -replicator · metering + +🔑 meshStack identities +replicator · metering BBD_A->AGENT - - + + PLAT - -🛰️ SKE meshPlatform -landing zones ×{dev,prod,…} + +🛰️ SKE meshPlatform +landing zones ×{dev,prod,…} PLAT->AGENT - - -replicates as + + +replicates as BBD_G - -📦 stackit/git BBD + +📦 stackit/git BBD GIT - -🔀 STACKIT Git -Forgejo instance and organization + +🔀 STACKIT Git +Forgejo instance and organization BBD_G->GIT - - + + BBD_H - -📦 stackit/container-registry BBD + +📦 stackit/container-registry BBD HARBOR - -🗄️ Container Registry -Harbor project · push and pull robots + +🗄️ Container Registry +Harbor project · push and pull robots BBD_H->HARBOR - - + + BBD_D - -📦 stackit/dns BBD + +📦 stackit/dns BBD DNS - -🧭 DNS zone -wildcard to the load balancer + +🧭 DNS zone BBD_D->DNS - - + + BBD_M - -📦 stackit/ai-llm BBD + +📦 stackit/ai-llm BBD @@ -260,8 +259,8 @@ BBD_M->MODEL - - + + From ec14e561ce80bec1015f336f7f2578bc4dd7b3e6 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 15:16:24 +0100 Subject: [PATCH 26/28] fix(ske/cluster): bring back the Open Cluster button with the portal link The summary and the RESOURCE_URL output share the cluster's portal URL. Co-Authored-By: Claude Opus 5.5 --- modules/ske/cluster/buildingblock/README.md | 1 + modules/ske/cluster/buildingblock/outputs.tf | 11 ++++++++++- modules/ske/cluster/meshstack_integration.tf | 6 ++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/modules/ske/cluster/buildingblock/README.md b/modules/ske/cluster/buildingblock/README.md index c3e8c61d3..024a53dd8 100644 --- a/modules/ske/cluster/buildingblock/README.md +++ b/modules/ske/cluster/buildingblock/README.md @@ -70,6 +70,7 @@ place. `release_on_destroy` cannot change once the cluster is managed. | Name | Description | |------|-------------| | [cluster\_name](#output\_cluster\_name) | Name of the SKE cluster. | +| [cluster\_url](#output\_cluster\_url) | Deep link to the cluster in the STACKIT portal. | | [kube\_host](#output\_kube\_host) | Kubernetes API server URL of the cluster. | | [summary](#output\_summary) | Markdown summary shown on the building block. | | [vault\_secret](#output\_vault\_secret) | `{path, secret_hash}` of the secret in Vault, where `secret_hash` changes with its content. | diff --git a/modules/ske/cluster/buildingblock/outputs.tf b/modules/ske/cluster/buildingblock/outputs.tf index 7f52ab29e..b3501d6f3 100644 --- a/modules/ske/cluster/buildingblock/outputs.tf +++ b/modules/ske/cluster/buildingblock/outputs.tf @@ -3,6 +3,15 @@ output "cluster_name" { value = module.cluster.cluster.name } +locals { + cluster_url = "https://portal.stackit.cloud/kubernetes/clusters/details/${module.cluster.cluster.name}/overview?project=${var.stackit_project_id}" +} + +output "cluster_url" { + description = "Deep link to the cluster in the STACKIT portal." + value = local.cluster_url +} + output "kube_host" { description = "Kubernetes API server URL of the cluster." value = terraform_data.kube_host.output @@ -23,7 +32,7 @@ output "summary" { | Property | Value | |----------|-------| - | **Cluster** | [Open in STACKIT Portal](https://portal.stackit.cloud/kubernetes/clusters/details/${module.cluster.cluster.name}/overview?project=${var.stackit_project_id}) | + | **Cluster** | [Open in STACKIT Portal](${local.cluster_url}) | | **Kubernetes** | `${module.cluster.cluster.kubernetes_version_used}` | | **API Server** | `${terraform_data.kube_host.output}` | | **Node Pool** | `${var.node_pool.name}`: ${var.node_pool.minimum} to ${var.node_pool.maximum} × `${var.node_pool.machine_type}` in ${join(", ", var.node_pool.availability_zones)} | diff --git a/modules/ske/cluster/meshstack_integration.tf b/modules/ske/cluster/meshstack_integration.tf index 83e611fd2..a378d44fc 100644 --- a/modules/ske/cluster/meshstack_integration.tf +++ b/modules/ske/cluster/meshstack_integration.tf @@ -284,6 +284,12 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "NONE" } + cluster_url = { + display_name = "Open Cluster" + type = "STRING" + assignment_type = "RESOURCE_URL" + } + summary = { display_name = "Summary" type = "STRING" From 87f4e883ea431bf188888349357786d2313a6647 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 15:20:45 +0100 Subject: [PATCH 27/28] =?UTF-8?q?feat(stackit):=20name=20the=20resource=20?= =?UTF-8?q?buttons=20"Open=20=E2=80=A6"=20and=20add=20the=20missing=20ones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit STACKIT Git gets "Open Forgejo" before "Open Forgejo Org", the service account federation "Open Service Account", the Forgejo Connector "Open Forgejo Actions" and the Git runner "Open Forgejo Runners". DNS, service account, network and network area buttons are renamed to the same style. Co-Authored-By: Claude Opus 5.5 --- .../forgejo-connector/buildingblock/outputs.tf | 17 ++++++++++++----- .../forgejo-connector/meshstack_integration.tf | 6 ++++++ modules/stackit/dns/meshstack_integration.tf | 2 +- .../stackit/git-runner/buildingblock/README.md | 1 + .../stackit/git-runner/buildingblock/outputs.tf | 11 ++++++++++- .../stackit/git-runner/meshstack_integration.tf | 6 ++++++ modules/stackit/git/meshstack_integration.tf | 6 ++++-- .../network-area/meshstack_integration.tf | 2 +- .../stackit/network/meshstack_integration.tf | 2 +- .../buildingblock/README.md | 1 + .../buildingblock/SUMMARY.md.tftpl | 2 +- .../buildingblock/outputs.tf | 11 ++++++++++- .../meshstack_integration.tf | 6 ++++++ .../service-account/meshstack_integration.tf | 2 +- 14 files changed, 61 insertions(+), 14 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/outputs.tf b/modules/ske/forgejo-connector/buildingblock/outputs.tf index a6e4a82b5..50e53957e 100644 --- a/modules/ske/forgejo-connector/buildingblock/outputs.tf +++ b/modules/ske/forgejo-connector/buildingblock/outputs.tf @@ -1,10 +1,17 @@ +locals { + app_url = "https://${var.app_hostname}" + repository_url = "${trimsuffix(data.external.env.result["FORGEJO_HOST"], "/")}/${var.repository_owner}/${var.repository_name}" + actions_url = "${local.repository_url}/actions?workflow=pipeline.yaml" +} + output "app_link" { description = "Public URL for this stage application." - value = "https://${var.app_hostname}" + value = local.app_url } -locals { - repository_url = "${trimsuffix(data.external.env.result["FORGEJO_HOST"], "/")}/${var.repository_owner}/${var.repository_name}" +output "actions_url" { + description = "Forgejo Actions page of the repository's `pipeline.yaml` workflow." + value = local.actions_url } output "summary" { @@ -17,8 +24,8 @@ output "summary" { | Property | Value | |----------|-------| - | **App** | [https://${var.app_hostname}](https://${var.app_hostname}) | - | **Pipeline** | [`pipeline.yaml` in Forgejo Actions](${local.repository_url}/actions?workflow=pipeline.yaml) | + | **App** | [${local.app_url}](${local.app_url}) | + | **Pipeline** | [`pipeline.yaml` in Forgejo Actions](${local.actions_url}) | | **Service Account** | `${kubernetes_service_account.forgejo_actions.metadata[0].name}`, `edit` on the namespace | ## Forgejo Actions secrets and variables diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index d68aa40c0..78c13d5e6 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -326,6 +326,12 @@ resource "meshstack_building_block_definition" "this" { type = "STRING" } + "actions_url" = { + assignment_type = "RESOURCE_URL" + display_name = "Open Forgejo Actions" + type = "STRING" + } + "summary" = { assignment_type = "SUMMARY" display_name = "Summary" diff --git a/modules/stackit/dns/meshstack_integration.tf b/modules/stackit/dns/meshstack_integration.tf index 0818d0fd7..9c6baf526 100644 --- a/modules/stackit/dns/meshstack_integration.tf +++ b/modules/stackit/dns/meshstack_integration.tf @@ -259,7 +259,7 @@ resource "meshstack_building_block_definition" "this" { } zone_url = { - display_name = "Open Zone" + display_name = "Open DNS Zone" type = "STRING" assignment_type = "RESOURCE_URL" } diff --git a/modules/stackit/git-runner/buildingblock/README.md b/modules/stackit/git-runner/buildingblock/README.md index e14a8fa2a..6bdb51219 100644 --- a/modules/stackit/git-runner/buildingblock/README.md +++ b/modules/stackit/git-runner/buildingblock/README.md @@ -70,6 +70,7 @@ No modules. |------|-------------| | [egress\_ip](#output\_egress\_ip) | Public SNAT IP of the runner's network router, for allowlisting. Empty on an existing network. | | [runner\_name](#output\_runner\_name) | Name the runner registered under in STACKIT Git. | +| [runners\_url](#output\_runners\_url) | Forgejo page of the organization's runners. | | [server\_id](#output\_server\_id) | ID of the STACKIT server hosting the runner. | | [summary](#output\_summary) | Markdown summary shown on the building block. | diff --git a/modules/stackit/git-runner/buildingblock/outputs.tf b/modules/stackit/git-runner/buildingblock/outputs.tf index 91454ca00..2dd487bc3 100644 --- a/modules/stackit/git-runner/buildingblock/outputs.tf +++ b/modules/stackit/git-runner/buildingblock/outputs.tf @@ -13,6 +13,15 @@ output "egress_ip" { description = "Public SNAT IP of the runner's network router, for allowlisting. Empty on an existing network." } +locals { + runners_url = "${var.git_base_url}/org/${var.git_organization}/settings/actions/runners" +} + +output "runners_url" { + value = local.runners_url + description = "Forgejo page of the organization's runners." +} + output "summary" { description = "Markdown summary shown on the building block." value = <<-EOT @@ -23,7 +32,7 @@ output "summary" { | Property | Value | |----------|-------| - | **Runners** | [Organization runners](${var.git_base_url}/org/${var.git_organization}/settings/actions/runners) | + | **Runners** | [Organization runners](${local.runners_url}) | | **Labels** | `${join("`, `", var.runner_labels)}` | | **Runner Version** | `${var.runner_version}` | | **Server** | `${stackit_server.this.server_id}`: `${var.machine_type}` with ${var.disk_size_gb} GB in ${var.availability_zone} | diff --git a/modules/stackit/git-runner/meshstack_integration.tf b/modules/stackit/git-runner/meshstack_integration.tf index 602d173fc..22fdc73da 100644 --- a/modules/stackit/git-runner/meshstack_integration.tf +++ b/modules/stackit/git-runner/meshstack_integration.tf @@ -318,6 +318,12 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "NONE" } + runners_url = { + display_name = "Open Forgejo Runners" + type = "STRING" + assignment_type = "RESOURCE_URL" + } + summary = { display_name = "Summary" type = "STRING" diff --git a/modules/stackit/git/meshstack_integration.tf b/modules/stackit/git/meshstack_integration.tf index 9b89d428d..f22d2c630 100644 --- a/modules/stackit/git/meshstack_integration.tf +++ b/modules/stackit/git/meshstack_integration.tf @@ -325,15 +325,17 @@ resource "meshstack_building_block_definition" "this" { } instance_url = { - display_name = "Instance URL" + display_name = "Open Forgejo" type = "STRING" - assignment_type = "NONE" + assignment_type = "RESOURCE_URL" + display_order = 1 } organization_url = { display_name = "Open Forgejo Org" type = "STRING" assignment_type = "RESOURCE_URL" + display_order = 2 } forgejo_organization = { diff --git a/modules/stackit/network-area/meshstack_integration.tf b/modules/stackit/network-area/meshstack_integration.tf index 7a83ace62..9df383730 100644 --- a/modules/stackit/network-area/meshstack_integration.tf +++ b/modules/stackit/network-area/meshstack_integration.tf @@ -247,7 +247,7 @@ resource "meshstack_building_block_definition" "this" { outputs = { network_area_url = { - display_name = "Network Area URL" + display_name = "Open Network Area" type = "STRING" assignment_type = "RESOURCE_URL" } diff --git a/modules/stackit/network/meshstack_integration.tf b/modules/stackit/network/meshstack_integration.tf index 5eb531e7f..e4c9cba44 100644 --- a/modules/stackit/network/meshstack_integration.tf +++ b/modules/stackit/network/meshstack_integration.tf @@ -223,7 +223,7 @@ resource "meshstack_building_block_definition" "this" { outputs = { network_url = { - display_name = "Network URL" + display_name = "Open Network" type = "STRING" assignment_type = "RESOURCE_URL" } diff --git a/modules/stackit/service-account-federation/buildingblock/README.md b/modules/stackit/service-account-federation/buildingblock/README.md index 22e015d08..b784dc4c1 100644 --- a/modules/stackit/service-account-federation/buildingblock/README.md +++ b/modules/stackit/service-account-federation/buildingblock/README.md @@ -71,5 +71,6 @@ No modules. | Name | Description | |------|-------------| | [service\_account\_email](#output\_service\_account\_email) | Email of the federated service account. Children of this block authenticate as it. | +| [service\_account\_url](#output\_service\_account\_url) | STACKIT portal page of the federated service account. | | [summary](#output\_summary) | Summary of the federation. | diff --git a/modules/stackit/service-account-federation/buildingblock/SUMMARY.md.tftpl b/modules/stackit/service-account-federation/buildingblock/SUMMARY.md.tftpl index 8781b30f9..b64dd9187 100644 --- a/modules/stackit/service-account-federation/buildingblock/SUMMARY.md.tftpl +++ b/modules/stackit/service-account-federation/buildingblock/SUMMARY.md.tftpl @@ -9,4 +9,4 @@ Runs of these building block definitions act as the service account through work _None._ %{ endif ~} -[Open service account](https://portal.stackit.cloud/service-accounts/${service_account_email}/overview?project=${project_id}) +[Open service account](${service_account_url}) diff --git a/modules/stackit/service-account-federation/buildingblock/outputs.tf b/modules/stackit/service-account-federation/buildingblock/outputs.tf index 8f81b1034..1c224abf5 100644 --- a/modules/stackit/service-account-federation/buildingblock/outputs.tf +++ b/modules/stackit/service-account-federation/buildingblock/outputs.tf @@ -3,11 +3,20 @@ output "service_account_email" { description = "Email of the federated service account. Children of this block authenticate as it." } +locals { + service_account_url = "https://portal.stackit.cloud/service-accounts/${var.service_account_email}/overview?project=${var.project_id}" +} + +output "service_account_url" { + value = local.service_account_url + description = "STACKIT portal page of the federated service account." +} + output "summary" { description = "Summary of the federation." value = templatefile("${path.module}/SUMMARY.md.tftpl", { service_account_email = var.service_account_email - project_id = var.project_id + service_account_url = local.service_account_url definitions = var.federated_building_block_definitions }) } diff --git a/modules/stackit/service-account-federation/meshstack_integration.tf b/modules/stackit/service-account-federation/meshstack_integration.tf index c9f05b53c..3d5011188 100644 --- a/modules/stackit/service-account-federation/meshstack_integration.tf +++ b/modules/stackit/service-account-federation/meshstack_integration.tf @@ -219,6 +219,12 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "NONE" } + service_account_url = { + display_name = "Open Service Account" + type = "STRING" + assignment_type = "RESOURCE_URL" + } + summary = { display_name = "Summary" type = "STRING" diff --git a/modules/stackit/service-account/meshstack_integration.tf b/modules/stackit/service-account/meshstack_integration.tf index beb45bb23..33eacdcd2 100644 --- a/modules/stackit/service-account/meshstack_integration.tf +++ b/modules/stackit/service-account/meshstack_integration.tf @@ -235,7 +235,7 @@ resource "meshstack_building_block_definition" "this" { outputs = { service_account_url = { - display_name = "Service Account URL" + display_name = "Open Service Account" type = "STRING" assignment_type = "RESOURCE_URL" } From aa35ccb0074d5ccf58739d290422cdd9e762dff4 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 6 Nov 2026 15:23:56 +0100 Subject: [PATCH 28/28] feat(stackit): link Git, Container Registry, server and AI token to the STACKIT portal STACKIT Git and Container Registry get an "Open Portal" button after the buttons of their own UI, the server "Open Server" and AI Model Serving "Open Portal" on the token's page. The summaries link the same pages, and the Container Registry summary links the Harbor repositories and robot accounts. Co-Authored-By: Claude Opus 5.5 --- modules/stackit/ai-llm/buildingblock/README.md | 1 + .../stackit/ai-llm/buildingblock/SUMMARY.md.tftpl | 2 +- modules/stackit/ai-llm/buildingblock/outputs.tf | 10 ++++++++++ modules/stackit/ai-llm/meshstack_integration.tf | 6 ++++++ .../container-registry/buildingblock/README.md | 1 + .../buildingblock/SUMMARY.md.tftpl | 3 ++- .../stackit/container-registry/buildingblock/main.tf | 1 + .../container-registry/buildingblock/outputs.tf | 6 ++++++ .../container-registry/meshstack_integration.tf | 8 ++++++++ modules/stackit/git/buildingblock/README.md | 1 + modules/stackit/git/buildingblock/SUMMARY.md.tftpl | 1 + modules/stackit/git/buildingblock/outputs.tf | 7 +++++++ modules/stackit/git/meshstack_integration.tf | 7 +++++++ modules/stackit/server/buildingblock/README.md | 1 + modules/stackit/server/buildingblock/outputs.tf | 12 +++++++++++- modules/stackit/server/meshstack_integration.tf | 6 ++++++ 16 files changed, 70 insertions(+), 3 deletions(-) diff --git a/modules/stackit/ai-llm/buildingblock/README.md b/modules/stackit/ai-llm/buildingblock/README.md index 86e8f2486..994ff080f 100644 --- a/modules/stackit/ai-llm/buildingblock/README.md +++ b/modules/stackit/ai-llm/buildingblock/README.md @@ -80,5 +80,6 @@ No modules. | Name | Description | |------|-------------| | [summary](#output\_summary) | Markdown summary shown on the building block. | +| [token\_url](#output\_token\_url) | STACKIT portal page of the model serving token. | | [vault\_secret](#output\_vault\_secret) | `{path, secret_hash}` of the secret in Vault, where `secret_hash` changes with its content. | diff --git a/modules/stackit/ai-llm/buildingblock/SUMMARY.md.tftpl b/modules/stackit/ai-llm/buildingblock/SUMMARY.md.tftpl index 309158cda..b6bd0c539 100644 --- a/modules/stackit/ai-llm/buildingblock/SUMMARY.md.tftpl +++ b/modules/stackit/ai-llm/buildingblock/SUMMARY.md.tftpl @@ -4,7 +4,7 @@ |----------|-------| | **Endpoint** | `${base_url}` | | **Default model** | `${model}` | -| **Token** | `${token_name}` | +| **Token** | [`${token_name}`](${token_url}) | | **Region** | `${region}` | The endpoint speaks the OpenAI API, so any OpenAI client works against it by pointing its base URL diff --git a/modules/stackit/ai-llm/buildingblock/outputs.tf b/modules/stackit/ai-llm/buildingblock/outputs.tf index e70954eba..a09241a63 100644 --- a/modules/stackit/ai-llm/buildingblock/outputs.tf +++ b/modules/stackit/ai-llm/buildingblock/outputs.tf @@ -1,8 +1,18 @@ +locals { + token_url = "https://portal.stackit.cloud/model-serving/tokens/${stackit_modelserving_token.this.token_id}?project=${var.stackit_project_id}" +} + +output "token_url" { + value = local.token_url + description = "STACKIT portal page of the model serving token." +} + output "summary" { value = templatefile("${path.module}/SUMMARY.md.tftpl", { base_url = local.base_url model = var.model token_name = var.token_name + token_url = local.token_url region = var.stackit_region }) description = "Markdown summary shown on the building block." diff --git a/modules/stackit/ai-llm/meshstack_integration.tf b/modules/stackit/ai-llm/meshstack_integration.tf index e20231f4d..e6e30388c 100644 --- a/modules/stackit/ai-llm/meshstack_integration.tf +++ b/modules/stackit/ai-llm/meshstack_integration.tf @@ -229,6 +229,12 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "NONE" } + token_url = { + display_name = "Open Portal" + type = "STRING" + assignment_type = "RESOURCE_URL" + } + summary = { display_name = "Summary" type = "STRING" diff --git a/modules/stackit/container-registry/buildingblock/README.md b/modules/stackit/container-registry/buildingblock/README.md index e1b0b8edd..b144bcdcd 100644 --- a/modules/stackit/container-registry/buildingblock/README.md +++ b/modules/stackit/container-registry/buildingblock/README.md @@ -177,6 +177,7 @@ it in the registry. The default is an empty list, which mirrors nothing. | Name | Description | |------|-------------| +| [portal\_url](#output\_portal\_url) | Container registry page of the project in the STACKIT portal, which has no page for a single registry. | | [registry\_host](#output\_registry\_host) | Host applications pull images from and `docker login` targets. | | [registry\_name](#output\_registry\_name) | Name of the STACKIT container registry created in the project. | | [registry\_robot\_url](#output\_registry\_robot\_url) | Robot Accounts tab of the Harbor project, where the robot linked to the service account is created. | diff --git a/modules/stackit/container-registry/buildingblock/SUMMARY.md.tftpl b/modules/stackit/container-registry/buildingblock/SUMMARY.md.tftpl index 1bdcba4cb..280d34092 100644 --- a/modules/stackit/container-registry/buildingblock/SUMMARY.md.tftpl +++ b/modules/stackit/container-registry/buildingblock/SUMMARY.md.tftpl @@ -4,7 +4,8 @@ A STACKIT container registry is ready in project `${stackit_project_id}`. | Field | Value | |-------|-------| -| **Registry** | `${registry_url}` | +| **Harbor** | [Repositories](${registry_url}) · [Robot accounts](${registry_robot_url}) | +| **STACKIT Portal** | [Open in STACKIT Portal](${portal_url}) | | **Host** | `${registry_host}` | | **Pull path** | `${registry_host}/${registry_name}/:` | diff --git a/modules/stackit/container-registry/buildingblock/main.tf b/modules/stackit/container-registry/buildingblock/main.tf index f111cb95c..f70286dd6 100644 --- a/modules/stackit/container-registry/buildingblock/main.tf +++ b/modules/stackit/container-registry/buildingblock/main.tf @@ -31,6 +31,7 @@ locals { # project, so read-artifactory.sh waits for it. registry_url = data.external.artifactory.result.url registry_robot_url = "${trimsuffix(local.registry_url, "/repositories")}/robot-account" + portal_url = "https://portal.stackit.cloud/container-registry-lite?project=${var.stackit_project_id}" service_enablement_path = "/v2/projects/${var.stackit_project_id}/regions/${var.stackit_region}/services" artifactory_path = "/v1/projects/${var.stackit_project_id}/regions/${var.stackit_region}/artifactories" diff --git a/modules/stackit/container-registry/buildingblock/outputs.tf b/modules/stackit/container-registry/buildingblock/outputs.tf index dd905898e..7dca793b7 100644 --- a/modules/stackit/container-registry/buildingblock/outputs.tf +++ b/modules/stackit/container-registry/buildingblock/outputs.tf @@ -18,6 +18,11 @@ output "registry_robot_url" { value = local.registry_robot_url } +output "portal_url" { + description = "Container registry page of the project in the STACKIT portal, which has no page for a single registry." + value = local.portal_url +} + output "summary" { description = "Summary of the created registry and, until a robot is linked, the one manual step it still needs." value = templatefile("${path.module}/SUMMARY.md.tftpl", { @@ -25,6 +30,7 @@ output "summary" { registry_host = local.registry_host registry_url = local.registry_url registry_robot_url = local.registry_robot_url + portal_url = local.portal_url stackit_project_id = var.stackit_project_id user_assignments = values(local.registry_role_assignments) unknown_users = local.unknown_users diff --git a/modules/stackit/container-registry/meshstack_integration.tf b/modules/stackit/container-registry/meshstack_integration.tf index e03dd9921..93ac2f67f 100644 --- a/modules/stackit/container-registry/meshstack_integration.tf +++ b/modules/stackit/container-registry/meshstack_integration.tf @@ -299,6 +299,14 @@ resource "meshstack_building_block_definition" "this" { display_name = "Open Registry" type = "STRING" assignment_type = "RESOURCE_URL" + display_order = 1 + } + + portal_url = { + display_name = "Open Portal" + type = "STRING" + assignment_type = "RESOURCE_URL" + display_order = 2 } registry_robot_url = { diff --git a/modules/stackit/git/buildingblock/README.md b/modules/stackit/git/buildingblock/README.md index c0f2aaf92..2f560f1f4 100644 --- a/modules/stackit/git/buildingblock/README.md +++ b/modules/stackit/git/buildingblock/README.md @@ -165,6 +165,7 @@ With `release_on_destroy` set, destroying the building block leaves the instance | [instance\_url](#output\_instance\_url) | URL of the Forgejo instance. | | [local\_user\_username](#output\_local\_user\_username) | Username of the technical user the token belongs to. | | [organization\_url](#output\_organization\_url) | URL of the Forgejo organization. Falls back to the instance URL when no organization was asked for. | +| [portal\_url](#output\_portal\_url) | STACKIT portal page of the Git instance. | | [summary](#output\_summary) | Markdown summary shown on the building block. | | [vault\_secret](#output\_vault\_secret) | `{path, secret_hash}` of the secret in Vault, where `secret_hash` changes with its content; `{}` without `output_to_vault`. | diff --git a/modules/stackit/git/buildingblock/SUMMARY.md.tftpl b/modules/stackit/git/buildingblock/SUMMARY.md.tftpl index b5031ce06..7e456a2b5 100644 --- a/modules/stackit/git/buildingblock/SUMMARY.md.tftpl +++ b/modules/stackit/git/buildingblock/SUMMARY.md.tftpl @@ -3,6 +3,7 @@ | Property | Value | |----------|-------| | **Forgejo** | [${instance_url}](${instance_url}) | +| **STACKIT Portal** | [Open in STACKIT Portal](${portal_url}) | %{ if forgejo_organization != null ~} | **Organization** | [`${forgejo_organization}`](${organization_url}) | %{ endif ~} diff --git a/modules/stackit/git/buildingblock/outputs.tf b/modules/stackit/git/buildingblock/outputs.tf index 89e8e79e3..74deccac8 100644 --- a/modules/stackit/git/buildingblock/outputs.tf +++ b/modules/stackit/git/buildingblock/outputs.tf @@ -17,6 +17,7 @@ locals { organization_url = local.create_forgejo_organization ? format( "%s/%s", trimsuffix(module.instance.instance.url, "/"), var.forgejo_organization ) : module.instance.instance.url + portal_url = "https://portal.stackit.cloud/git/instances/${module.instance.instance.instance_id}/overview?project=${var.stackit_project_id}" } output "organization_url" { @@ -24,6 +25,11 @@ output "organization_url" { value = local.organization_url } +output "portal_url" { + description = "STACKIT portal page of the Git instance." + value = local.portal_url +} + output "forgejo_organization" { description = "Name of the Forgejo organization for this instance. Empty when none was asked for." # The name, not a claim that the organization exists: meshStack fails a building block whose @@ -52,6 +58,7 @@ output "summary" { value = templatefile("${path.module}/SUMMARY.md.tftpl", { instance_name = module.instance.instance.name instance_url = module.instance.instance.url + portal_url = local.portal_url forgejo_organization = local.create_forgejo_organization ? var.forgejo_organization : null organization_url = local.organization_url local_user_username = local.local_user_username diff --git a/modules/stackit/git/meshstack_integration.tf b/modules/stackit/git/meshstack_integration.tf index f22d2c630..9777ab6ca 100644 --- a/modules/stackit/git/meshstack_integration.tf +++ b/modules/stackit/git/meshstack_integration.tf @@ -338,6 +338,13 @@ resource "meshstack_building_block_definition" "this" { display_order = 2 } + portal_url = { + display_name = "Open Portal" + type = "STRING" + assignment_type = "RESOURCE_URL" + display_order = 3 + } + forgejo_organization = { display_name = "Forgejo Organization" type = "STRING" diff --git a/modules/stackit/server/buildingblock/README.md b/modules/stackit/server/buildingblock/README.md index 4d3b58411..7fd7a0048 100644 --- a/modules/stackit/server/buildingblock/README.md +++ b/modules/stackit/server/buildingblock/README.md @@ -64,6 +64,7 @@ No modules. |------|-------------| | [public\_ip](#output\_public\_ip) | Public IP the VM is reachable on over SSH. | | [server\_id](#output\_server\_id) | ID of the STACKIT server. | +| [server\_url](#output\_server\_url) | STACKIT portal page of the server. | | [ssh\_command](#output\_ssh\_command) | Ready-to-use SSH command once the generated private key is on disk (see ssh\_private\_key). | | [ssh\_private\_key](#output\_ssh\_private\_key) | Generated OpenSSH private key to log into the VM. Save it to a file, chmod 600, and pass it with ssh -i. | | [ssh\_username](#output\_ssh\_username) | Login user for the VM, as shipped by the chosen image. | diff --git a/modules/stackit/server/buildingblock/outputs.tf b/modules/stackit/server/buildingblock/outputs.tf index c901d61f8..48800c21e 100644 --- a/modules/stackit/server/buildingblock/outputs.tf +++ b/modules/stackit/server/buildingblock/outputs.tf @@ -19,6 +19,15 @@ output "ssh_private_key" { description = "Generated OpenSSH private key to log into the VM. Save it to a file, chmod 600, and pass it with ssh -i." } +locals { + server_url = "https://portal.stackit.cloud/server/servers/${stackit_server.this.server_id}/overview?project=${var.stackit_project_id}" +} + +output "server_url" { + value = local.server_url + description = "STACKIT portal page of the server." +} + output "server_id" { value = stackit_server.this.server_id description = "ID of the STACKIT server." @@ -29,7 +38,8 @@ output "summary" { value = <<-EOT # STACKIT VM `${var.name}` - Your `${var.machine_type}` VM is up at **`${stackit_public_ip.this.ip}`**. + Your `${var.machine_type}` VM is up at **`${stackit_public_ip.this.ip}`** + ([Open in STACKIT Portal](${local.server_url})). Log in with the generated key (from the **SSH Private Key** output): diff --git a/modules/stackit/server/meshstack_integration.tf b/modules/stackit/server/meshstack_integration.tf index 6c634e5ab..c4fd0832b 100644 --- a/modules/stackit/server/meshstack_integration.tf +++ b/modules/stackit/server/meshstack_integration.tf @@ -353,6 +353,12 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "NONE" } + server_url = { + display_name = "Open Server" + type = "STRING" + assignment_type = "RESOURCE_URL" + } + summary = { display_name = "Summary" type = "STRING"