diff --git a/.agents/references/terraform-conventions.md b/.agents/references/terraform-conventions.md index c9fbbb215..a364bd9d0 100644 --- a/.agents/references/terraform-conventions.md +++ b/.agents/references/terraform-conventions.md @@ -94,6 +94,12 @@ 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. + ## 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/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..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" { @@ -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/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..fc0c3e95f 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,9 +425,9 @@ 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 - through the kubeconfig the ordering composition passes as an input. + 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..f4f7ff3c1 100644 --- a/modules/kubernetes/platform/README.md +++ b/modules/kubernetes/platform/README.md @@ -28,9 +28,9 @@ 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 Vault KV v2 secret `kubernetes/platform` under the keys `replicator-token` and +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. ## The kubeconfig is an input of the building block diff --git a/modules/kubernetes/platform/main.tf b/modules/kubernetes/platform/main.tf index 981a6c4dd..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 here — each platform ordered from -# the STACKIT Kubernetes Platform reference architecture gets a cluster of its own — and it is why -# the upstream module's `name_suffix` / `existing_clusterrole_name` machinery is not carried over. +# 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 { replicator_name = "meshfed-service" 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/README.md b/modules/kubernetes/service-account/buildingblock/README.md index 4ff56b8e3..cad83b6fd 100644 --- a/modules/kubernetes/service-account/buildingblock/README.md +++ b/modules/kubernetes/service-account/buildingblock/README.md @@ -71,6 +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) | 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/output_to_vault.tf b/modules/kubernetes/service-account/buildingblock/output_to_vault.tf index 2420ac671..fab61ee2b 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) }) @@ -10,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" { diff --git a/modules/kubernetes/service-account/buildingblock/outputs.tf b/modules/kubernetes/service-account/buildingblock/outputs.tf index 795221a49..708bea3f9 100644 --- a/modules/kubernetes/service-account/buildingblock/outputs.tf +++ b/modules/kubernetes/service-account/buildingblock/outputs.tf @@ -38,9 +38,8 @@ output "instructions" { } 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) + # "" because meshStack fails a run whose declared output is missing. + value = local.write_to_vault ? "" : nonsensitive(yamlencode(local.kubeconfig)) } output "vault_secret" { @@ -52,3 +51,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..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" @@ -279,6 +280,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..024a53dd8 100644 --- a/modules/ske/cluster/buildingblock/README.md +++ b/modules/ske/cluster/buildingblock/README.md @@ -70,7 +70,8 @@ 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. | +| [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/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..b3501d6f3 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/kubernetes/clusters/details/${module.cluster.cluster.name}/overview?project=${var.stackit_project_id}" +} + 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" + description = "Deep link to the cluster in the STACKIT portal." + 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..50e53957e 100644 --- a/modules/ske/forgejo-connector/buildingblock/outputs.tf +++ b/modules/ske/forgejo-connector/buildingblock/outputs.tf @@ -1,4 +1,49 @@ +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 +} + +output "actions_url" { + description = "Forgejo Actions page of the repository's `pipeline.yaml` workflow." + value = local.actions_url +} + +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** | [${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 + + 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..78c13d5e6 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -325,6 +325,18 @@ resource "meshstack_building_block_definition" "this" { display_name = "Open App" type = "STRING" } + + "actions_url" = { + assignment_type = "RESOURCE_URL" + display_name = "Open Forgejo Actions" + type = "STRING" + } + + "summary" = { + assignment_type = "SUMMARY" + display_name = "Summary" + type = "STRING" + } } } } 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/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/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 6d3b07830..6bdb51219 100644 --- a/modules/stackit/git-runner/buildingblock/README.md +++ b/modules/stackit/git-runner/buildingblock/README.md @@ -70,5 +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 99e5bc000..2dd487bc3 100644 --- a/modules/stackit/git-runner/buildingblock/outputs.tf +++ b/modules/stackit/git-runner/buildingblock/outputs.tf @@ -12,3 +12,30 @@ 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." } + +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 + # 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](${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} | + | **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..22fdc73da 100644 --- a/modules/stackit/git-runner/meshstack_integration.tf +++ b/modules/stackit/git-runner/meshstack_integration.tf @@ -317,6 +317,18 @@ resource "meshstack_building_block_definition" "this" { type = "STRING" assignment_type = "NONE" } + + runners_url = { + display_name = "Open Forgejo Runners" + type = "STRING" + assignment_type = "RESOURCE_URL" + } + + summary = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } } } } diff --git a/modules/stackit/git/buildingblock/README.md b/modules/stackit/git/buildingblock/README.md index ab812135f..2f560f1f4 100644 --- a/modules/stackit/git/buildingblock/README.md +++ b/modules/stackit/git/buildingblock/README.md @@ -165,5 +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 new file mode 100644 index 000000000..7e456a2b5 --- /dev/null +++ b/modules/stackit/git/buildingblock/SUMMARY.md.tftpl @@ -0,0 +1,29 @@ +# STACKIT Git: **${instance_name}** + +| 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 ~} +| **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..74deccac8 100644 --- a/modules/stackit/git/buildingblock/outputs.tf +++ b/modules/stackit/git/buildingblock/outputs.tf @@ -13,11 +13,21 @@ 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 + portal_url = "https://portal.stackit.cloud/git/instances/${module.instance.instance.instance_id}/overview?project=${var.stackit_project_id}" +} + +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 "portal_url" { + description = "STACKIT portal page of the Git instance." + value = local.portal_url } output "forgejo_organization" { @@ -42,3 +52,21 @@ 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 + 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 + 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..9777ab6ca 100644 --- a/modules/stackit/git/meshstack_integration.tf +++ b/modules/stackit/git/meshstack_integration.tf @@ -325,15 +325,24 @@ 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 + } + + portal_url = { + display_name = "Open Portal" + type = "STRING" + assignment_type = "RESOURCE_URL" + display_order = 3 } forgejo_organization = { @@ -347,6 +356,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/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/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/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" 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 3eb08e445..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 accounts](https://portal.stackit.cloud/projects/${project_id}/service-accounts) +[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/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/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" } 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/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 fb1996879..e06c66628 100644 --- a/reference-architectures/stackit-kubernetes/README.md +++ b/reference-architectures/stackit-kubernetes/README.md @@ -51,12 +51,14 @@ 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) -The building blocks Platform Services orders, with their parents as meshPanel shows them: +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) @@ -116,6 +118,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 +148,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 @@ -174,6 +180,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 **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: + +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 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: + +- 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 @@ -181,7 +228,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/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..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/outputs.tf b/reference-architectures/stackit-kubernetes/buildingblock/outputs.tf index 6e73409d2..a6d35feb8 100644 --- a/reference-architectures/stackit-kubernetes/buildingblock/outputs.tf +++ b/reference-architectures/stackit-kubernetes/buildingblock/outputs.tf @@ -1,11 +1,11 @@ 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" { 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/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/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 diff --git a/reference-architectures/stackit-kubernetes/meshstack_integration.tf b/reference-architectures/stackit-kubernetes/meshstack_integration.tf index e6996cfc4..a2c2d3eda 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. @@ -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" } } } } 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-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-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 diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.dot b/reference-architectures/stackit-kubernetes/stackit-kubernetes-building-blocks.dot index 680ff3b32..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 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=" 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 b4b7d91a4..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 - -  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 + + diff --git a/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot b/reference-architectures/stackit-kubernetes/stackit-kubernetes.dot index b512ec3e0..7f403b1a0 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="📦 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"] @@ -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 9bc1576be..c9c9ec2b9 100644 --- a/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg +++ b/reference-architectures/stackit-kubernetes/stackit-kubernetes.svg @@ -1,269 +1,266 @@ - - + + 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 - -📦 Platform Services BBD -registers and orders everything below + +📦 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 Platform -one landing zone per stage + +🛰️ 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 MODEL - -🧠 Model Serving -API token + +🧠 Model Serving +API token BBD_M->MODEL - - + + 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/buildingblock/SUMMARY.md.tftpl b/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl index 7113595ba..75acac295 100644 --- a/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl +++ b/reference-architectures/stackit-landingzone/buildingblock/SUMMARY.md.tftpl @@ -35,32 +35,34 @@ 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. +> 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 ~} + > [!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 ~} 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}" 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 diff --git a/tools/scorecard/scorecard.mjs b/tools/scorecard/scorecard.mjs index 1113f2bfe..4c9e4304d 100644 --- a/tools/scorecard/scorecard.mjs +++ b/tools/scorecard/scorecard.mjs @@ -723,6 +723,22 @@ 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: "wif_no_replicator", category: "integration", @@ -1932,6 +1948,24 @@ 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 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..32387428d 100644 --- a/tools/scorecard/scorecard.test.mjs +++ b/tools/scorecard/scorecard.test.mjs @@ -1,6 +1,10 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { explicitDefaultViolations } from "./scorecard.mjs"; +import { + declaresSummaryOutput, + explicitDefaultViolations, + summaryOutputBody, +} from "./scorecard.mjs"; const integration = (argument) => ` variable "meshstack" { @@ -42,3 +46,30 @@ 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("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); +});