From 506212b337fecc6fcc9cd059c9287af3ce8393dc Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 14:38:14 +0100 Subject: [PATCH 01/39] fix: support 'null' workaround as optional input in Panel --- modules/stackit/git-repository/buildingblock/main.tf | 8 +++++--- modules/stackit/git-repository/buildingblock/variables.tf | 4 ++-- modules/stackit/git-repository/meshstack_integration.tf | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/stackit/git-repository/buildingblock/main.tf b/modules/stackit/git-repository/buildingblock/main.tf index d4b02a13..b8c9ad12 100644 --- a/modules/stackit/git-repository/buildingblock/main.tf +++ b/modules/stackit/git-repository/buildingblock/main.tf @@ -1,4 +1,6 @@ -# ── Repository ───────────────────────────────────────────────────────────────── +locals { + have_clone_addr = trimspace(var.clone_addr) != "" && var.clone_addr != "null" +} resource "forgejo_repository" "repository" { owner = var.forgejo_organization @@ -6,9 +8,9 @@ resource "forgejo_repository" "repository" { description = var.description private = var.private default_branch = var.default_branch - auto_init = var.clone_addr == "" + auto_init = !local.have_clone_addr # One-time clone (not an ongoing mirror) - clone_addr = var.clone_addr != "" ? var.clone_addr : null + clone_addr = local.have_clone_addr ? var.clone_addr : null mirror = false } diff --git a/modules/stackit/git-repository/buildingblock/variables.tf b/modules/stackit/git-repository/buildingblock/variables.tf index d9a843a9..c6d467c8 100644 --- a/modules/stackit/git-repository/buildingblock/variables.tf +++ b/modules/stackit/git-repository/buildingblock/variables.tf @@ -51,6 +51,6 @@ variable "default_branch" { variable "clone_addr" { type = string - description = "Optional URL to clone into this repository, e.g. 'https://github.com/owner/repo.git'. Leave empty to create an empty repository." - default = "" + description = "Optional URL to clone into this repository, e.g. 'https://github.com/owner/repo.git'. Leave empty or `null` to create an empty repository." + default = "null" # supporting the null string is a workaround for the Panel UI which does not support empty string as default for optional value } diff --git a/modules/stackit/git-repository/meshstack_integration.tf b/modules/stackit/git-repository/meshstack_integration.tf index a071a198..f57d8906 100644 --- a/modules/stackit/git-repository/meshstack_integration.tf +++ b/modules/stackit/git-repository/meshstack_integration.tf @@ -135,10 +135,10 @@ resource "meshstack_building_block_definition" "this" { clone_addr = { display_name = "Clone from URL" - description = "Optional URL to clone into this repository, e.g. 'https://github.com/owner/repo.git'. Leave empty to create an empty repository." + description = "Optional URL to clone into this repository, e.g. 'https://github.com/owner/repo.git'. Leave `null` to create an empty repository." type = "STRING" assignment_type = "USER_INPUT" - default_value = jsonencode("") + default_value = jsonencode("null") } } From 84092aea34fa08cca445ee0b74c5846da6a4e581 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 11:35:29 +0100 Subject: [PATCH 02/39] feat(git-repository): support action_secrets, refactor ske starterkit backplane to LCF --- modules/ske/ske-starterkit/backplane/main.tf | 42 ----------------- .../ske-starterkit/buildingblock/README.md | 6 +-- .../ske-starterkit/meshstack_integration.tf | 46 +++++++++---------- modules/stackit/git-repository/README.md | 1 + .../git-repository/buildingblock/README.md | 4 +- .../git-repository/buildingblock/main.tf | 8 ++++ .../git-repository/buildingblock/variables.tf | 12 +++++ .../git-repository/meshstack_integration.tf | 23 ++++++++++ 8 files changed, 71 insertions(+), 71 deletions(-) delete mode 100644 modules/ske/ske-starterkit/backplane/main.tf diff --git a/modules/ske/ske-starterkit/backplane/main.tf b/modules/ske/ske-starterkit/backplane/main.tf deleted file mode 100644 index 4221baac..00000000 --- a/modules/ske/ske-starterkit/backplane/main.tf +++ /dev/null @@ -1,42 +0,0 @@ -variable "meshstack" { - type = object({ - owning_workspace_identifier = string - }) -} - -variable "hub" { - type = object({ - git_ref = string - bbd_draft = bool - }) -} - -variable "forgejo_token" { - type = string - sensitive = true -} - -variable "forgejo_organization" { - type = string -} - -variable "forgejo_base_url" { - type = string -} - -output "building_block_definition_version_refs" { - value = { - "git-repository" : module.git_repository.building_block_definition_version_ref - } -} - -module "git_repository" { - source = "github.com/meshcloud/meshstack-hub//modules/stackit/git-repository?ref=2e990f277119f33db50af78032768c434ab4b7bb" - - meshstack = var.meshstack - hub = var.hub - - forgejo_token = var.forgejo_token - forgejo_organization = var.forgejo_organization - forgejo_base_url = var.forgejo_base_url -} diff --git a/modules/ske/ske-starterkit/buildingblock/README.md b/modules/ske/ske-starterkit/buildingblock/README.md index 351e0e13..1296cd20 100644 --- a/modules/ske/ske-starterkit/buildingblock/README.md +++ b/modules/ske/ske-starterkit/buildingblock/README.md @@ -34,11 +34,11 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [building\_block\_definition\_version\_refs](#input\_building\_block\_definition\_version\_refs) | n/a | `map(object({ uuid = string }))` | n/a | yes | -| [creator](#input\_creator) | Information about the creator of the resources who will be assigned Project Admin role |
object({
type = string
identifier = string
displayName = string
username = optional(string)
email = optional(string)
euid = optional(string)
})
| n/a | yes | +| [creator](#input\_creator) | Information about the creator of the resources who will be assigned Project Admin role |
object({
type = string
identifier = string
displayName = string
username = optional(string)
email = optional(string)
euid = optional(string)
})
| n/a | yes | | [full\_platform\_identifier](#input\_full\_platform\_identifier) | Full platform identifier of the SKE platform. | `string` | n/a | yes | -| [landing\_zone\_identifiers](#input\_landing\_zone\_identifiers) | SKE Landing zone identifiers for the dev/prod meshTenant. |
object({
dev = string
prod = string
})
| n/a | yes | +| [landing\_zone\_identifiers](#input\_landing\_zone\_identifiers) | SKE Landing zone identifiers for the dev/prod meshTenant. |
object({
dev = string
prod = string
})
| n/a | yes | | [name](#input\_name) | This name will be used for the created projects. | `string` | n/a | yes | -| [project\_tags](#input\_project\_tags) | Tags for dev/prod meshProject. |
object({
dev : map(list(string))
prod : map(list(string))

owner_tag_key = optional(string, null)
})
| n/a | yes | +| [project\_tags](#input\_project\_tags) | Tags for dev/prod meshProject. |
object({
dev : map(list(string))
prod : map(list(string))

owner_tag_key = optional(string, null)
})
| n/a | yes | | [repo\_clone\_addr](#input\_repo\_clone\_addr) | URL to clone into the starterkit git repository. | `string` | n/a | yes | | [workspace\_identifier](#input\_workspace\_identifier) | n/a | `string` | n/a | yes | diff --git a/modules/ske/ske-starterkit/meshstack_integration.tf b/modules/ske/ske-starterkit/meshstack_integration.tf index b2304344..095fb109 100644 --- a/modules/ske/ske-starterkit/meshstack_integration.tf +++ b/modules/ske/ske-starterkit/meshstack_integration.tf @@ -4,19 +4,6 @@ variable "meshstack" { }) } -variable "forgejo_token" { - type = string - sensitive = true -} - -variable "forgejo_organization" { - type = string -} - -variable "forgejo_base_url" { - type = string -} - variable "full_platform_identifier" { type = string } @@ -55,6 +42,13 @@ variable "notification_subscribers" { default = [] } +variable "building_block_definition_version_refs" { + type = map(object({ + content_hash = string # adding the content nicely tracks changes in dependent BBDs (draft mode) + uuid = string + })) +} + variable "hub" { type = object({ git_ref = optional(string, "main") @@ -67,17 +61,6 @@ variable "hub" { EOT } -module "backplane" { - source = "./backplane" # TODO revert to github.com/meshcloud/meshstack-hub//modules/ske/ske-starterkit/backplane link once pushed - - meshstack = var.meshstack - hub = var.hub - - forgejo_token = var.forgejo_token - forgejo_organization = var.forgejo_organization - forgejo_base_url = var.forgejo_base_url -} - locals { name_regex = "^[a-zA-Z0-9-]+$" # underscore and dots not allowed because of K8s namespace } @@ -204,7 +187,20 @@ EOT description = "Refs used to create auxiliary building blocks (composition)." display_name = "BBD Version Refs" # jsonencode twice is correct, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 - argument = jsonencode(jsonencode(module.backplane.building_block_definition_version_refs)) + argument = jsonencode(jsonencode(var.building_block_definition_version_refs)) + }, + # TODO remove before merge, leftover from dev attempts in grubinator2 instance + "git_repository_action_secrets" = { + assignment_type = "STATIC" + type = "CODE" + description = "REMOVEME Static sensitive Forgejo Actions secrets passed to the composed git-repository building block." + display_name = "REMOVEME Git Repository Action Secrets" + # jsonencode twice is correct, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 + sensitive = { + argument = { + secret_value = jsonencode({}) + } + } } } diff --git a/modules/stackit/git-repository/README.md b/modules/stackit/git-repository/README.md index a1fecd14..ec502005 100644 --- a/modules/stackit/git-repository/README.md +++ b/modules/stackit/git-repository/README.md @@ -14,6 +14,7 @@ It combines: - workspace-level target type - static inputs from backplane (`forgejo_base_url`, `forgejo_token`, `forgejo_organization`) +- optional static sensitive action secrets (`action_secrets`) - user inputs (`name`, `description`, `private`, `clone_addr`) - outputs exposed to users (`repository_html_url`, `repository_clone_url`, `repository_ssh_url`, `summary`) diff --git a/modules/stackit/git-repository/buildingblock/README.md b/modules/stackit/git-repository/buildingblock/README.md index 8ff4c960..92c494f7 100644 --- a/modules/stackit/git-repository/buildingblock/README.md +++ b/modules/stackit/git-repository/buildingblock/README.md @@ -22,12 +22,14 @@ No modules. | Name | Type | |------|------| | [forgejo_repository.repository](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository) | resource | +| [forgejo_repository_action_secret.action_secrets](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [clone\_addr](#input\_clone\_addr) | Optional URL to clone into this repository, e.g. 'https://github.com/owner/repo.git'. Leave empty to create an empty repository. | `string` | `""` | no | +| [action\_secrets](#input\_action\_secrets) | Map of Forgejo Actions secrets to create in the repository. | `map(string)` | `{}` | no | +| [clone\_addr](#input\_clone\_addr) | Optional URL to clone into this repository, e.g. 'https://github.com/owner/repo.git'. Leave empty or `null` to create an empty repository. | `string` | `"null"` | no | | [default\_branch](#input\_default\_branch) | Default branch name | `string` | `"main"` | no | | [description](#input\_description) | Short description of the repository | `string` | `""` | no | | [forgejo\_base\_url](#input\_forgejo\_base\_url) | STACKIT Git base URL | `string` | `"https://git-service.git.onstackit.cloud"` | no | diff --git a/modules/stackit/git-repository/buildingblock/main.tf b/modules/stackit/git-repository/buildingblock/main.tf index b8c9ad12..b00d0e4d 100644 --- a/modules/stackit/git-repository/buildingblock/main.tf +++ b/modules/stackit/git-repository/buildingblock/main.tf @@ -14,3 +14,11 @@ resource "forgejo_repository" "repository" { clone_addr = local.have_clone_addr ? var.clone_addr : null mirror = false } + +resource "forgejo_repository_action_secret" "action_secrets" { + for_each = var.action_secrets + + repository_id = forgejo_repository.repository.id + name = each.key + data = each.value +} diff --git a/modules/stackit/git-repository/buildingblock/variables.tf b/modules/stackit/git-repository/buildingblock/variables.tf index c6d467c8..c0ca2012 100644 --- a/modules/stackit/git-repository/buildingblock/variables.tf +++ b/modules/stackit/git-repository/buildingblock/variables.tf @@ -17,6 +17,18 @@ variable "forgejo_organization" { description = "STACKIT Git organization where the repository will be created" } +variable "action_secrets" { + type = map(string) + description = "Map of Forgejo Actions secrets to create in the repository." + sensitive = false # the whole map is not sensitive, but map values are! + default = {} + + validation { + condition = alltrue([for key in keys(var.action_secrets) : length(key) <= 30]) + error_message = "Forgejo Actions secret names must be 30 characters or less." + } +} + # ── User inputs (set per building block instance) ───────────────────────────── variable "name" { diff --git a/modules/stackit/git-repository/meshstack_integration.tf b/modules/stackit/git-repository/meshstack_integration.tf index f57d8906..152d1203 100644 --- a/modules/stackit/git-repository/meshstack_integration.tf +++ b/modules/stackit/git-repository/meshstack_integration.tf @@ -21,6 +21,17 @@ variable "forgejo_base_url" { type = string } +variable "action_secrets" { + type = map(string) + sensitive = false # the whole map is not sensitive, but map values are! + default = {} + + validation { + condition = alltrue([for key in keys(var.action_secrets) : length(key) <= 30]) + error_message = "Forgejo Actions secret names must be 30 characters or less." + } +} + variable "hub" { type = object({ git_ref = optional(string, "main") @@ -140,6 +151,18 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "USER_INPUT" default_value = jsonencode("null") } + action_secrets = { + display_name = "Repository Action Secrets" + description = "Static sensitive map of Forgejo Actions secrets created in each provisioned repository." + type = "CODE" + assignment_type = "STATIC" + sensitive = { + argument = { + secret_value = jsonencode(var.action_secrets) + secret_version = nonsensitive(sha256(jsonencode(var.action_secrets))) + } + } + } } outputs = { From e970c7c6dd2737a4e1067aec6ae6f3a243abece2 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 21:24:01 +0100 Subject: [PATCH 03/39] feat(ske): register forgejo connector as tenant building block Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../meshstack_integration.tf | 98 +++++++++++-------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 9511809a..7c7b9475 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -1,11 +1,10 @@ -# This file is an example showing how to register the STACKIT connector -# building block in a meshStack instance. +# This file registers the SKE Forgejo connector building block definition. terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = "~> 0.19.0" + version = "~> 0.20.0" } } } @@ -48,6 +47,11 @@ variable "forgejo_api_token" { sensitive = true } +variable "forgejo_repo_definition_uuid" { + type = string + description = "UUID of the Forgejo repository building block definition used as parent dependency." +} + variable "harbor_host" { type = string description = "The URL of the Harbor registry." @@ -66,11 +70,6 @@ variable "harbor_password" { sensitive = true } -variable "forgejo_repo_definition_uuid" { - type = string - description = "The Building Block definition UUID of the repository parent." -} - variable "meshstack" { type = object({ owning_workspace_identifier = string @@ -91,13 +90,13 @@ variable "hub" { output "building_block_definition_version_ref" { value = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release - description = "Version of BBD is consumed in Building Block compositions, for example in the backplane of starter kits." + description = "Version of BBD is consumed in building block compositions." } module "backplane" { source = "./backplane" cluster_host = var.cluster_host - cluster_ca_certificate = var.client_certificate + cluster_ca_certificate = var.cluster_ca_certificate client_key = var.client_key client_certificate = var.client_certificate cluster_kubeconfig = var.cluster_kubeconfig @@ -109,11 +108,11 @@ resource "meshstack_building_block_definition" "this" { } spec = { - display_name = "STACKIT connector" - symbol = "data:image/png;base64,${filebase64("${path.module}/buildingblock/logo.png")}" - description = "Forgejo Actions Integration with STACKIT Kubernetes" + display_name = "SKE Forgejo Connector" + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/modules/ske/forgejo-connector/buildingblock/logo.png" + description = "Connects a Forgejo repository with a tenant namespace on STACKIT SKE." support_url = "https://portal.stackit.cloud/git" - target_type = "WORKSPACE_LEVEL" + target_type = "TENANT_LEVEL" run_transparency = true } @@ -125,26 +124,25 @@ resource "meshstack_building_block_definition" "this" { terraform = { terraform_version = "1.9.0" repository_url = "https://github.com/meshcloud/meshstack-hub.git" - repository_path = "modules/stackit/git-connector/buildingblock" + repository_path = "modules/ske/forgejo-connector/buildingblock" ref_name = var.hub.git_ref async = false use_mesh_http_backend_fallback = true } } - dependency_refs = [{ uuid = "${var.forgejo_repo_definition_uuid}" }] + dependency_refs = [{ uuid = var.forgejo_repo_definition_uuid }] inputs = { - # ── Static inputs from backplane ────────────────────────────────────── - config_tf = { - display_name = "config_k8s" - description = "Static config for kubernetes" + "config.tf" = { + display_name = "config.tf" + description = "Static Kubernetes provider config and kubeconfig stub." type = "FILE" assignment_type = "STATIC" - is_environment = true sensitive = { argument = { - secret_value = jsonencode(module.backplane.config_tf) + secret_value = module.backplane.config_tf + secret_version = sha256(module.backplane.config_tf) } } } @@ -157,7 +155,8 @@ resource "meshstack_building_block_definition" "this" { is_environment = true sensitive = { argument = { - secret_value = jsonencode(var.forgejo_host) + secret_value = jsonencode(var.forgejo_host) + secret_version = sha256(jsonencode(var.forgejo_host)) } } } @@ -170,7 +169,8 @@ resource "meshstack_building_block_definition" "this" { is_environment = true sensitive = { argument = { - secret_value = jsonencode(var.forgejo_api_token) + secret_value = jsonencode(var.forgejo_api_token) + secret_version = sha256(jsonencode(var.forgejo_api_token)) } } } @@ -188,7 +188,12 @@ resource "meshstack_building_block_definition" "this" { description = "The username for the Harbor registry." type = "STRING" assignment_type = "STATIC" - argument = jsonencode(var.harbor_username) + sensitive = { + argument = { + secret_value = jsonencode(var.harbor_username) + secret_version = sha256(jsonencode(var.harbor_username)) + } + } } harbor_password = { @@ -198,38 +203,47 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "STATIC" sensitive = { argument = { - secret_value = jsonencode(var.harbor_password) + secret_value = jsonencode(var.harbor_password) + secret_version = sha256(jsonencode(var.harbor_password)) } } } - forgejo_repository_name = { - display_name = "forgejo_repository_name" - description = "The name of the Forgejo repository." - type = "STRING" - assignment_type = "BUILDING_BLOCK_OUTPUT" - argument = "${var.forgejo_repo_definition_uuid}.repo_name" - } - - forgejo_repository_owner = { - display_name = "forgejo_repository_owner" - description = "The owner of the Forgejo repository." + repository_id = { + display_name = "repository_id" + description = "ID of the parent Forgejo repository where action secrets are created." type = "STRING" assignment_type = "BUILDING_BLOCK_OUTPUT" - argument = "${var.forgejo_repo_definition_uuid}.repo_owner" + argument = "${var.forgejo_repo_definition_uuid}.repository_id" } - # ── User inputs ──────────────────────────────────────────────────────── - namespace = { display_name = "namespace" description = "Associated namespace in kubernetes cluster." type = "STRING" + assignment_type = "PLATFORM_TENANT_ID" + } + + stage = { + display_name = "stage" + description = "Deployment stage used for secret suffixing (`dev` or `prod`)." + type = "STRING" + assignment_type = "USER_INPUT" + value_validation_regex = "^(dev|prod)$" + validation_regex_error_message = "Stage must be either 'dev' or 'prod'." + } + + additional_environment_variables = { + display_name = "additional_environment_variables" + description = "Map of additional key/value pairs to create as repository action secrets." + type = "CODE" assignment_type = "USER_INPUT" + default_value = jsonencode({}) } } - outputs = { - } + outputs = {} + + permissions = ["TENANT_LIST", "TENANT_SAVE", "TENANT_DELETE"] } } From 656d321c893b4984d1d2c83a77c9a686dd3fd391 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 21:25:05 +0100 Subject: [PATCH 04/39] feat(ske): write stage-specific forgejo repo secrets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../forgejo-connector/buildingblock/README.md | 6 ++--- .../buildingblock/forgejo.tf | 23 +++++++++++-------- .../buildingblock/variables.tf | 15 ++++++++---- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index 396a213c..d87fa85e 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -58,24 +58,24 @@ No modules. | [forgejo_repository_action_secret.additional](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [forgejo_repository_action_secret.container_registry](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [forgejo_repository_action_secret.kubeconfig](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | +| [forgejo_repository_action_secret.namespace](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [kubernetes_cluster_role_binding.forgejo_actions_clusterissuer_access](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/cluster_role_binding) | resource | | [kubernetes_role_binding.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/role_binding) | resource | | [kubernetes_secret.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_secret.image_pull](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_service_account.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/service_account) | resource | -| [forgejo_repository.this](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/data-sources/repository) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_environment\_variables](#input\_additional\_environment\_variables) | Map of additional environment variable key/value pairs to set as Forgejo repository action secrets. | `map(string)` | `{}` | no | -| [forgejo\_repository\_name](#input\_forgejo\_repository\_name) | The name of the Forgejo repository. | `string` | n/a | yes | -| [forgejo\_repository\_owner](#input\_forgejo\_repository\_owner) | The owner of the Forgejo repository. | `string` | n/a | yes | | [harbor\_host](#input\_harbor\_host) | The URL of the Harbor registry. | `string` | `"https://registry.onstackit.cloud"` | no | | [harbor\_password](#input\_harbor\_password) | The password for the Harbor registry. | `string` | n/a | yes | | [harbor\_username](#input\_harbor\_username) | The username for the Harbor registry. | `string` | n/a | yes | | [namespace](#input\_namespace) | Associated namespace in kubernetes cluster. | `string` | n/a | yes | +| [repository\_id](#input\_repository\_id) | The ID of the Forgejo repository. | `string` | n/a | yes | +| [stage](#input\_stage) | Deployment stage used for secret suffixing (`dev` or `prod`). | `string` | n/a | yes | ## Outputs diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index 1498319f..162a5fe5 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -1,4 +1,6 @@ locals { + stage_suffix = upper(var.stage) + kubeconfig_user = { users = [ { @@ -23,17 +25,18 @@ locals { kubeconfig = merge(local.stackit_kubeconfig_stub, local.kubeconfig_user) } -data "forgejo_repository" "this" { - name = var.forgejo_repository_name - owner = var.forgejo_repository_owner -} - resource "forgejo_repository_action_secret" "kubeconfig" { - repository_id = data.forgejo_repository.this.id - name = "KUBECONFIG" + repository_id = var.repository_id + name = "KUBECONFIG_${local.stage_suffix}" data = yamlencode(local.kubeconfig) } +resource "forgejo_repository_action_secret" "namespace" { + repository_id = var.repository_id + name = "K8S_NAMESPACE_${local.stage_suffix}" + data = var.namespace +} + resource "forgejo_repository_action_secret" "container_registry" { for_each = { HOST = var.harbor_host @@ -41,7 +44,7 @@ resource "forgejo_repository_action_secret" "container_registry" { PASSWORD = var.harbor_password } - repository_id = data.forgejo_repository.this.id + repository_id = var.repository_id name = "STACKIT_HARBOR_${each.key}" data = each.value } @@ -49,7 +52,7 @@ resource "forgejo_repository_action_secret" "container_registry" { resource "forgejo_repository_action_secret" "additional" { for_each = var.additional_environment_variables - repository_id = data.forgejo_repository.this.id + repository_id = var.repository_id name = each.key data = each.value -} \ No newline at end of file +} diff --git a/modules/ske/forgejo-connector/buildingblock/variables.tf b/modules/ske/forgejo-connector/buildingblock/variables.tf index 537e202d..d5e0bd9a 100644 --- a/modules/ske/forgejo-connector/buildingblock/variables.tf +++ b/modules/ske/forgejo-connector/buildingblock/variables.tf @@ -16,14 +16,19 @@ variable "harbor_password" { sensitive = true } -variable "forgejo_repository_name" { +variable "repository_id" { type = string - description = "The name of the Forgejo repository." + description = "The ID of the Forgejo repository." } -variable "forgejo_repository_owner" { +variable "stage" { type = string - description = "The owner of the Forgejo repository." + description = "Deployment stage used for secret suffixing (`dev` or `prod`)." + + validation { + condition = can(regex("^(dev|prod)$", var.stage)) + error_message = "stage must be either 'dev' or 'prod'." + } } variable "additional_environment_variables" { @@ -35,4 +40,4 @@ variable "additional_environment_variables" { variable "namespace" { description = "Associated namespace in kubernetes cluster." type = string -} \ No newline at end of file +} From dafa0df083345fadb884edd447deb3fcc7e0f402 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 21:25:54 +0100 Subject: [PATCH 05/39] feat(ske): compose tenant forgejo connectors in starterkit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ske-starterkit/buildingblock/README.md | 2 ++ .../ske/ske-starterkit/buildingblock/main.tf | 29 +++++++++++++++++++ .../ske-starterkit/buildingblock/variables.tf | 5 ++++ 3 files changed, 36 insertions(+) diff --git a/modules/ske/ske-starterkit/buildingblock/README.md b/modules/ske/ske-starterkit/buildingblock/README.md index 1296cd20..e517a433 100644 --- a/modules/ske/ske-starterkit/buildingblock/README.md +++ b/modules/ske/ske-starterkit/buildingblock/README.md @@ -24,6 +24,7 @@ No modules. | Name | Type | |------|------| +| [meshstack_building_block_v2.forgejo_connector](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_v2) | resource | | [meshstack_building_block_v2.git_repository](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_v2) | resource | | [meshstack_project.this](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/project) | resource | | [meshstack_project_user_binding.creator_to_admin](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/project_user_binding) | resource | @@ -36,6 +37,7 @@ No modules. | [building\_block\_definition\_version\_refs](#input\_building\_block\_definition\_version\_refs) | n/a | `map(object({ uuid = string }))` | n/a | yes | | [creator](#input\_creator) | Information about the creator of the resources who will be assigned Project Admin role |
object({
type = string
identifier = string
displayName = string
username = optional(string)
email = optional(string)
euid = optional(string)
})
| n/a | yes | | [full\_platform\_identifier](#input\_full\_platform\_identifier) | Full platform identifier of the SKE platform. | `string` | n/a | yes | +| [git\_repository\_definition\_uuid](#input\_git\_repository\_definition\_uuid) | Definition UUID of the composed git-repository building block. | `string` | n/a | yes | | [landing\_zone\_identifiers](#input\_landing\_zone\_identifiers) | SKE Landing zone identifiers for the dev/prod meshTenant. |
object({
dev = string
prod = string
})
| n/a | yes | | [name](#input\_name) | This name will be used for the created projects. | `string` | n/a | yes | | [project\_tags](#input\_project\_tags) | Tags for dev/prod meshProject. |
object({
dev : map(list(string))
prod : map(list(string))

owner_tag_key = optional(string, null)
})
| n/a | yes | diff --git a/modules/ske/ske-starterkit/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf index 1a8801a4..e91dab3f 100644 --- a/modules/ske/ske-starterkit/buildingblock/main.tf +++ b/modules/ske/ske-starterkit/buildingblock/main.tf @@ -66,3 +66,32 @@ resource "meshstack_tenant_v4" "this" { landing_zone_identifier = each.value } } + +resource "meshstack_building_block_v2" "forgejo_connector" { + for_each = meshstack_tenant_v4.this + + depends_on = [meshstack_building_block_v2.git_repository] + + spec = { + building_block_definition_version_ref = var.building_block_definition_version_refs["forgejo-connector"] + + display_name = "${var.name} Forgejo Connector ${upper(each.key)}" + target_ref = { + kind = "meshTenant" + uuid = each.value.metadata.uuid + } + + parent_building_blocks = [{ + buildingblock_uuid = meshstack_building_block_v2.git_repository.metadata.uuid + definition_uuid = var.git_repository_definition_uuid + }] + + inputs = { + stage = { + value_string = each.key + } + } + } + + wait_for_completion = true +} diff --git a/modules/ske/ske-starterkit/buildingblock/variables.tf b/modules/ske/ske-starterkit/buildingblock/variables.tf index d0628fc5..53148869 100644 --- a/modules/ske/ske-starterkit/buildingblock/variables.tf +++ b/modules/ske/ske-starterkit/buildingblock/variables.tf @@ -50,3 +50,8 @@ variable "repo_clone_addr" { variable "building_block_definition_version_refs" { type = map(object({ uuid = string })) } + +variable "git_repository_definition_uuid" { + type = string + description = "Definition UUID of the composed git-repository building block." +} From b4b7b763db5a06f1c38bd7552cbeb2c042c36ccc Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 21:26:38 +0100 Subject: [PATCH 06/39] feat(ske): pass git repository definition uuid to starterkit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- modules/ske/ske-starterkit/meshstack_integration.tf | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/ske/ske-starterkit/meshstack_integration.tf b/modules/ske/ske-starterkit/meshstack_integration.tf index 095fb109..8e554b9e 100644 --- a/modules/ske/ske-starterkit/meshstack_integration.tf +++ b/modules/ske/ske-starterkit/meshstack_integration.tf @@ -49,6 +49,11 @@ variable "building_block_definition_version_refs" { })) } +variable "git_repository_definition_uuid" { + type = string + description = "Definition UUID of the STACKIT git-repository building block." +} + variable "hub" { type = object({ git_ref = optional(string, "main") @@ -202,6 +207,13 @@ EOT } } } + "git_repository_definition_uuid" = { + assignment_type = "STATIC" + type = "STRING" + display_name = "Git Repository Definition UUID" + description = "Definition UUID used for parent dependency wiring to the composed git-repository." + argument = jsonencode(var.git_repository_definition_uuid) + } } outputs = {} From 56326a4ae8460401cf44ee391e4298eca7783521 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 21:28:06 +0100 Subject: [PATCH 07/39] feat(stackit): expose git-repository definition uuid output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- modules/stackit/git-repository/meshstack_integration.tf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/stackit/git-repository/meshstack_integration.tf b/modules/stackit/git-repository/meshstack_integration.tf index 152d1203..16b554e7 100644 --- a/modules/stackit/git-repository/meshstack_integration.tf +++ b/modules/stackit/git-repository/meshstack_integration.tf @@ -49,6 +49,11 @@ output "building_block_definition_version_ref" { description = "Version of BBD is consumed in Building Block compositions, for example in the backplane of starter kits." } +output "building_block_definition_uuid" { + value = meshstack_building_block_definition.this.ref.uuid + description = "UUID of the STACKIT Git Repository building block definition." +} + module "backplane" { source = "github.com/meshcloud/meshstack-hub//modules/stackit/git-repository/backplane?ref=a3843c80c76c4a0298769eea8d93807bb2b271fc" From 1236697f6712ad5d053fad4c0d94cf7a760abe2b Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 21:29:54 +0100 Subject: [PATCH 08/39] fix(ske): use harbor vars for image pull secret auth Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ske/forgejo-connector/buildingblock/kubernetes.tf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf index dae6cb31..f57e3be8 100644 --- a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf +++ b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf @@ -65,12 +65,12 @@ resource "kubernetes_secret" "image_pull" { data = { ".dockerconfigjson" = jsonencode({ auths = { - "${local.harbor.host}" = { - username = local.harbor.username - password = local.harbor.password - auth = base64encode("${local.harbor.username}:${local.harbor.password}") + "${var.harbor_host}" = { + username = var.harbor_username + password = var.harbor_password + auth = base64encode("${var.harbor_username}:${var.harbor_password}") } } }) } -} \ No newline at end of file +} From 63895488f0bfd6a76cae3848604995ca2207ed17 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 21:30:38 +0100 Subject: [PATCH 09/39] fix(ske): declare supported platform for tenant connector Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ske/forgejo-connector/meshstack_integration.tf | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 7c7b9475..ae35eebc 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -108,12 +108,13 @@ resource "meshstack_building_block_definition" "this" { } spec = { - display_name = "SKE Forgejo Connector" - symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/modules/ske/forgejo-connector/buildingblock/logo.png" - description = "Connects a Forgejo repository with a tenant namespace on STACKIT SKE." - support_url = "https://portal.stackit.cloud/git" - target_type = "TENANT_LEVEL" - run_transparency = true + display_name = "SKE Forgejo Connector" + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/modules/ske/forgejo-connector/buildingblock/logo.png" + description = "Connects a Forgejo repository with a tenant namespace on STACKIT SKE." + support_url = "https://portal.stackit.cloud/git" + target_type = "TENANT_LEVEL" + supported_platforms = [{ name = "STACKIT_KUBERNETES_ENGINE" }] + run_transparency = true } version_spec = { From 32fb777c71c913cf9b13566cada6b8afc1e7722d Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 21:36:05 +0100 Subject: [PATCH 10/39] refactor(ske): drop obsolete git repository definition input Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- modules/ske/ske-starterkit/buildingblock/README.md | 1 - modules/ske/ske-starterkit/buildingblock/main.tf | 4 +--- .../ske/ske-starterkit/buildingblock/variables.tf | 5 ----- modules/ske/ske-starterkit/meshstack_integration.tf | 12 ------------ 4 files changed, 1 insertion(+), 21 deletions(-) diff --git a/modules/ske/ske-starterkit/buildingblock/README.md b/modules/ske/ske-starterkit/buildingblock/README.md index e517a433..acbf4e59 100644 --- a/modules/ske/ske-starterkit/buildingblock/README.md +++ b/modules/ske/ske-starterkit/buildingblock/README.md @@ -37,7 +37,6 @@ No modules. | [building\_block\_definition\_version\_refs](#input\_building\_block\_definition\_version\_refs) | n/a | `map(object({ uuid = string }))` | n/a | yes | | [creator](#input\_creator) | Information about the creator of the resources who will be assigned Project Admin role |
object({
type = string
identifier = string
displayName = string
username = optional(string)
email = optional(string)
euid = optional(string)
})
| n/a | yes | | [full\_platform\_identifier](#input\_full\_platform\_identifier) | Full platform identifier of the SKE platform. | `string` | n/a | yes | -| [git\_repository\_definition\_uuid](#input\_git\_repository\_definition\_uuid) | Definition UUID of the composed git-repository building block. | `string` | n/a | yes | | [landing\_zone\_identifiers](#input\_landing\_zone\_identifiers) | SKE Landing zone identifiers for the dev/prod meshTenant. |
object({
dev = string
prod = string
})
| n/a | yes | | [name](#input\_name) | This name will be used for the created projects. | `string` | n/a | yes | | [project\_tags](#input\_project\_tags) | Tags for dev/prod meshProject. |
object({
dev : map(list(string))
prod : map(list(string))

owner_tag_key = optional(string, null)
})
| n/a | yes | diff --git a/modules/ske/ske-starterkit/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf index e91dab3f..5b166344 100644 --- a/modules/ske/ske-starterkit/buildingblock/main.tf +++ b/modules/ske/ske-starterkit/buildingblock/main.tf @@ -70,8 +70,6 @@ resource "meshstack_tenant_v4" "this" { resource "meshstack_building_block_v2" "forgejo_connector" { for_each = meshstack_tenant_v4.this - depends_on = [meshstack_building_block_v2.git_repository] - spec = { building_block_definition_version_ref = var.building_block_definition_version_refs["forgejo-connector"] @@ -83,7 +81,7 @@ resource "meshstack_building_block_v2" "forgejo_connector" { parent_building_blocks = [{ buildingblock_uuid = meshstack_building_block_v2.git_repository.metadata.uuid - definition_uuid = var.git_repository_definition_uuid + definition_uuid = meshstack_building_block_v2.git_repository.spec.building_block_definition_version_ref.uuid }] inputs = { diff --git a/modules/ske/ske-starterkit/buildingblock/variables.tf b/modules/ske/ske-starterkit/buildingblock/variables.tf index 53148869..d0628fc5 100644 --- a/modules/ske/ske-starterkit/buildingblock/variables.tf +++ b/modules/ske/ske-starterkit/buildingblock/variables.tf @@ -50,8 +50,3 @@ variable "repo_clone_addr" { variable "building_block_definition_version_refs" { type = map(object({ uuid = string })) } - -variable "git_repository_definition_uuid" { - type = string - description = "Definition UUID of the composed git-repository building block." -} diff --git a/modules/ske/ske-starterkit/meshstack_integration.tf b/modules/ske/ske-starterkit/meshstack_integration.tf index 8e554b9e..095fb109 100644 --- a/modules/ske/ske-starterkit/meshstack_integration.tf +++ b/modules/ske/ske-starterkit/meshstack_integration.tf @@ -49,11 +49,6 @@ variable "building_block_definition_version_refs" { })) } -variable "git_repository_definition_uuid" { - type = string - description = "Definition UUID of the STACKIT git-repository building block." -} - variable "hub" { type = object({ git_ref = optional(string, "main") @@ -207,13 +202,6 @@ EOT } } } - "git_repository_definition_uuid" = { - assignment_type = "STATIC" - type = "STRING" - display_name = "Git Repository Definition UUID" - description = "Definition UUID used for parent dependency wiring to the composed git-repository." - argument = jsonencode(var.git_repository_definition_uuid) - } } outputs = {} From 0cd74b4dfde4884d3ca2499c79740890b183d9ef Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 21:39:57 +0100 Subject: [PATCH 11/39] chore(ske): commit pending forgejo connector adjustments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- modules/ske/forgejo-connector/meshstack_integration.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index ae35eebc..997a0794 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -215,7 +215,7 @@ resource "meshstack_building_block_definition" "this" { description = "ID of the parent Forgejo repository where action secrets are created." type = "STRING" assignment_type = "BUILDING_BLOCK_OUTPUT" - argument = "${var.forgejo_repo_definition_uuid}.repository_id" + argument = jsonencode("${var.forgejo_repo_definition_uuid}.repository_id") } namespace = { From 3a3a7929755bc3a8957ff4fa65da5b089ddf2a01 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 21:41:54 +0100 Subject: [PATCH 12/39] refactor(ske): remove unused additional connector env vars Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- modules/ske/forgejo-connector/buildingblock/README.md | 2 -- modules/ske/forgejo-connector/buildingblock/forgejo.tf | 8 -------- modules/ske/forgejo-connector/buildingblock/variables.tf | 6 ------ modules/ske/forgejo-connector/meshstack_integration.tf | 7 ------- 4 files changed, 23 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index d87fa85e..a0015d67 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -55,7 +55,6 @@ No modules. | Name | Type | |------|------| -| [forgejo_repository_action_secret.additional](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [forgejo_repository_action_secret.container_registry](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [forgejo_repository_action_secret.kubeconfig](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [forgejo_repository_action_secret.namespace](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | @@ -69,7 +68,6 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [additional\_environment\_variables](#input\_additional\_environment\_variables) | Map of additional environment variable key/value pairs to set as Forgejo repository action secrets. | `map(string)` | `{}` | no | | [harbor\_host](#input\_harbor\_host) | The URL of the Harbor registry. | `string` | `"https://registry.onstackit.cloud"` | no | | [harbor\_password](#input\_harbor\_password) | The password for the Harbor registry. | `string` | n/a | yes | | [harbor\_username](#input\_harbor\_username) | The username for the Harbor registry. | `string` | n/a | yes | diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index 162a5fe5..e7033937 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -48,11 +48,3 @@ resource "forgejo_repository_action_secret" "container_registry" { name = "STACKIT_HARBOR_${each.key}" data = each.value } - -resource "forgejo_repository_action_secret" "additional" { - for_each = var.additional_environment_variables - - repository_id = var.repository_id - name = each.key - data = each.value -} diff --git a/modules/ske/forgejo-connector/buildingblock/variables.tf b/modules/ske/forgejo-connector/buildingblock/variables.tf index d5e0bd9a..9f60702a 100644 --- a/modules/ske/forgejo-connector/buildingblock/variables.tf +++ b/modules/ske/forgejo-connector/buildingblock/variables.tf @@ -31,12 +31,6 @@ variable "stage" { } } -variable "additional_environment_variables" { - type = map(string) - description = "Map of additional environment variable key/value pairs to set as Forgejo repository action secrets." - default = {} -} - variable "namespace" { description = "Associated namespace in kubernetes cluster." type = string diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 997a0794..d0c57986 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -234,13 +234,6 @@ resource "meshstack_building_block_definition" "this" { validation_regex_error_message = "Stage must be either 'dev' or 'prod'." } - additional_environment_variables = { - display_name = "additional_environment_variables" - description = "Map of additional key/value pairs to create as repository action secrets." - type = "CODE" - assignment_type = "USER_INPUT" - default_value = jsonencode({}) - } } outputs = {} From d4eb0abd8f81deb4cea1d42407f4a75ea4d3ce3d Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 16 Mar 2026 21:46:08 +0100 Subject: [PATCH 13/39] fix(ske): restore kubeconfig stub and file input wiring Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../forgejo-connector/backplane/outputs.tf | 20 +------------------ .../forgejo-connector/buildingblock/README.md | 2 ++ .../buildingblock/forgejo.tf | 15 ++++++++++++++ .../buildingblock/variables.tf | 10 ++++++++++ .../meshstack_integration.tf | 18 ++++++++++++++++- 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/modules/ske/forgejo-connector/backplane/outputs.tf b/modules/ske/forgejo-connector/backplane/outputs.tf index cb7b901a..fd6330c0 100644 --- a/modules/ske/forgejo-connector/backplane/outputs.tf +++ b/modules/ske/forgejo-connector/backplane/outputs.tf @@ -8,23 +8,5 @@ output "config_tf" { client_certificate = base64decode("${var.client_certificate}") client_key = base64decode("${var.client_key}") } - - locals { - stackit_kubeconfig_stub = { - apiVersion = "v1" - kind = "Config" - current-context = "stackit_k8s" - - clusters = [ - { - name = "stackit_k8s" - cluster = { - server = "${var.cluster_host}" - certificate-authority-data = "${var.cluster_ca_certificate}" - } - } - ] - } - } EOF -} \ No newline at end of file +} diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index a0015d67..d5947bb1 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -68,6 +68,8 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [cluster\_ca\_certificate](#input\_cluster\_ca\_certificate) | Base64-encoded Kubernetes cluster CA certificate used for generated kubeconfig. | `string` | n/a | yes | +| [cluster\_host](#input\_cluster\_host) | Kubernetes API server URL used for generated kubeconfig. | `string` | n/a | yes | | [harbor\_host](#input\_harbor\_host) | The URL of the Harbor registry. | `string` | `"https://registry.onstackit.cloud"` | no | | [harbor\_password](#input\_harbor\_password) | The password for the Harbor registry. | `string` | n/a | yes | | [harbor\_username](#input\_harbor\_username) | The username for the Harbor registry. | `string` | n/a | yes | diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index e7033937..1e2184fe 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -1,5 +1,20 @@ locals { stage_suffix = upper(var.stage) + stackit_kubeconfig_stub = { + apiVersion = "v1" + kind = "Config" + current-context = "stackit_k8s" + + clusters = [ + { + name = "stackit_k8s" + cluster = { + server = var.cluster_host + certificate-authority-data = var.cluster_ca_certificate + } + } + ] + } kubeconfig_user = { users = [ diff --git a/modules/ske/forgejo-connector/buildingblock/variables.tf b/modules/ske/forgejo-connector/buildingblock/variables.tf index 9f60702a..c04575aa 100644 --- a/modules/ske/forgejo-connector/buildingblock/variables.tf +++ b/modules/ske/forgejo-connector/buildingblock/variables.tf @@ -4,6 +4,16 @@ variable "harbor_host" { default = "https://registry.onstackit.cloud" } +variable "cluster_host" { + type = string + description = "Kubernetes API server URL used for generated kubeconfig." +} + +variable "cluster_ca_certificate" { + type = string + description = "Base64-encoded Kubernetes cluster CA certificate used for generated kubeconfig." +} + variable "harbor_username" { type = string description = "The username for the Harbor registry." diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index d0c57986..7f734d4e 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -142,7 +142,7 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "STATIC" sensitive = { argument = { - secret_value = module.backplane.config_tf + secret_value = "data:application/octet-stream;base64,${base64encode(module.backplane.config_tf)}" secret_version = sha256(module.backplane.config_tf) } } @@ -184,6 +184,22 @@ resource "meshstack_building_block_definition" "this" { argument = jsonencode(var.harbor_host) } + cluster_host = { + display_name = "cluster_host" + description = "Kubernetes API server URL used for generated kubeconfig." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.cluster_host) + } + + cluster_ca_certificate = { + display_name = "cluster_ca_certificate" + description = "Base64-encoded Kubernetes cluster CA certificate used for generated kubeconfig." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.cluster_ca_certificate) + } + harbor_username = { display_name = "harbor_username" description = "The username for the Harbor registry." From 2ca4690590a7c0165b58e1db1a5637065d41848f Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 17 Mar 2026 14:24:21 +0100 Subject: [PATCH 14/39] feat(ske): rework forgejo connector kubeconfig wiring - remove cluster_kubeconfig from backplane interface\n- add encrypted CODE kubeconfig input in BBD\n- pass kubeconfig_cluster_name from backplane output\n- build KUBECONFIG secret from kubeconfig + generated user/context\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ske/forgejo-connector/backplane/README.md | 4 ++-- .../forgejo-connector/backplane/outputs.tf | 5 ++++ .../forgejo-connector/backplane/variables.tf | 6 ----- .../forgejo-connector/buildingblock/README.md | 4 ++-- .../buildingblock/forgejo.tf | 23 ++++-------------- .../buildingblock/variables.tf | 11 +++++---- .../meshstack_integration.tf | 24 +++++++++++-------- 7 files changed, 34 insertions(+), 43 deletions(-) diff --git a/modules/ske/forgejo-connector/backplane/README.md b/modules/ske/forgejo-connector/backplane/README.md index d94e3806..b0b41b53 100644 --- a/modules/ske/forgejo-connector/backplane/README.md +++ b/modules/ske/forgejo-connector/backplane/README.md @@ -28,11 +28,11 @@ No resources. | [client\_key](#input\_client\_key) | Base64-encoded private key corresponding to the client certificate, used for authentication with the Kubernetes API server. | `string` | n/a | yes | | [cluster\_ca\_certificate](#input\_cluster\_ca\_certificate) | Base64-encoded certificate authority (CA) certificate used to verify the Kubernetes API server's identity. | `string` | n/a | yes | | [cluster\_host](#input\_cluster\_host) | The endpoint of the Kubernetes cluster. | `string` | n/a | yes | -| [cluster\_kubeconfig](#input\_cluster\_kubeconfig) | Raw kubeconfig content containing the configuration required to access and authenticate to the Kubernetes cluster. | `string` | n/a | yes | ## Outputs | Name | Description | |------|-------------| | [config\_tf](#output\_config\_tf) | Generates a config.tf that can be dropped into meshStack's BuildingBlockDefinition as an encrypted file input to configure this building block. | - \ No newline at end of file +| [kubeconfig\_cluster\_name](#output\_kubeconfig\_cluster\_name) | Cluster name to use when merging static kubeconfig and generated service-account credentials. | + diff --git a/modules/ske/forgejo-connector/backplane/outputs.tf b/modules/ske/forgejo-connector/backplane/outputs.tf index fd6330c0..326522c4 100644 --- a/modules/ske/forgejo-connector/backplane/outputs.tf +++ b/modules/ske/forgejo-connector/backplane/outputs.tf @@ -10,3 +10,8 @@ output "config_tf" { } EOF } + +output "kubeconfig_cluster_name" { + description = "Cluster name to use when merging static kubeconfig and generated service-account credentials." + value = "stackit_k8s" +} diff --git a/modules/ske/forgejo-connector/backplane/variables.tf b/modules/ske/forgejo-connector/backplane/variables.tf index 83013d1f..08eaf08e 100644 --- a/modules/ske/forgejo-connector/backplane/variables.tf +++ b/modules/ske/forgejo-connector/backplane/variables.tf @@ -20,9 +20,3 @@ variable "client_key" { type = string sensitive = true } - -variable "cluster_kubeconfig" { - description = "Raw kubeconfig content containing the configuration required to access and authenticate to the Kubernetes cluster." - type = string - sensitive = true -} \ No newline at end of file diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index d5947bb1..09f52164 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -68,11 +68,11 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [cluster\_ca\_certificate](#input\_cluster\_ca\_certificate) | Base64-encoded Kubernetes cluster CA certificate used for generated kubeconfig. | `string` | n/a | yes | -| [cluster\_host](#input\_cluster\_host) | Kubernetes API server URL used for generated kubeconfig. | `string` | n/a | yes | | [harbor\_host](#input\_harbor\_host) | The URL of the Harbor registry. | `string` | `"https://registry.onstackit.cloud"` | no | | [harbor\_password](#input\_harbor\_password) | The password for the Harbor registry. | `string` | n/a | yes | | [harbor\_username](#input\_harbor\_username) | The username for the Harbor registry. | `string` | n/a | yes | +| [kubeconfig](#input\_kubeconfig) | Static kubeconfig content of the SKE cluster. | `any` | n/a | yes | +| [kubeconfig\_cluster\_name](#input\_kubeconfig\_cluster\_name) | Cluster name used in merged kubeconfig context entries. | `string` | n/a | yes | | [namespace](#input\_namespace) | Associated namespace in kubernetes cluster. | `string` | n/a | yes | | [repository\_id](#input\_repository\_id) | The ID of the Forgejo repository. | `string` | n/a | yes | | [stage](#input\_stage) | Deployment stage used for secret suffixing (`dev` or `prod`). | `string` | n/a | yes | diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index 1e2184fe..e55d29c8 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -1,22 +1,9 @@ locals { stage_suffix = upper(var.stage) - stackit_kubeconfig_stub = { - apiVersion = "v1" - kind = "Config" - current-context = "stackit_k8s" - - clusters = [ - { - name = "stackit_k8s" - cluster = { - server = var.cluster_host - certificate-authority-data = var.cluster_ca_certificate - } - } - ] - } kubeconfig_user = { + current-context = var.kubeconfig_cluster_name + users = [ { name = kubernetes_service_account.forgejo_actions.metadata[0].name @@ -28,16 +15,16 @@ locals { contexts = [ { - name = "stackit_k8s" + name = var.kubeconfig_cluster_name context = { - cluster = "stackit_k8s" + cluster = var.kubeconfig_cluster_name namespace = var.namespace user = kubernetes_service_account.forgejo_actions.metadata[0].name } } ] } - kubeconfig = merge(local.stackit_kubeconfig_stub, local.kubeconfig_user) + kubeconfig = merge(var.kubeconfig, local.kubeconfig_user) } resource "forgejo_repository_action_secret" "kubeconfig" { diff --git a/modules/ske/forgejo-connector/buildingblock/variables.tf b/modules/ske/forgejo-connector/buildingblock/variables.tf index c04575aa..f62fae60 100644 --- a/modules/ske/forgejo-connector/buildingblock/variables.tf +++ b/modules/ske/forgejo-connector/buildingblock/variables.tf @@ -4,14 +4,15 @@ variable "harbor_host" { default = "https://registry.onstackit.cloud" } -variable "cluster_host" { - type = string - description = "Kubernetes API server URL used for generated kubeconfig." +variable "kubeconfig" { + type = any + description = "Static kubeconfig content of the SKE cluster." + sensitive = true } -variable "cluster_ca_certificate" { +variable "kubeconfig_cluster_name" { type = string - description = "Base64-encoded Kubernetes cluster CA certificate used for generated kubeconfig." + description = "Cluster name used in merged kubeconfig context entries." } variable "harbor_username" { diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 7f734d4e..f3a8d282 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -99,7 +99,6 @@ module "backplane" { cluster_ca_certificate = var.cluster_ca_certificate client_key = var.client_key client_certificate = var.client_certificate - cluster_kubeconfig = var.cluster_kubeconfig } resource "meshstack_building_block_definition" "this" { @@ -184,20 +183,25 @@ resource "meshstack_building_block_definition" "this" { argument = jsonencode(var.harbor_host) } - cluster_host = { - display_name = "cluster_host" - description = "Kubernetes API server URL used for generated kubeconfig." - type = "STRING" + kubeconfig = { + display_name = "kubeconfig" + description = "Static cluster kubeconfig content merged with generated service-account credentials." + type = "CODE" assignment_type = "STATIC" - argument = jsonencode(var.cluster_host) + sensitive = { + argument = { + secret_value = jsonencode(yamldecode(var.cluster_kubeconfig)) + secret_version = sha256(var.cluster_kubeconfig) + } + } } - cluster_ca_certificate = { - display_name = "cluster_ca_certificate" - description = "Base64-encoded Kubernetes cluster CA certificate used for generated kubeconfig." + kubeconfig_cluster_name = { + display_name = "kubeconfig_cluster_name" + description = "Cluster name used to connect static kubeconfig and generated user/context entries." type = "STRING" assignment_type = "STATIC" - argument = jsonencode(var.cluster_ca_certificate) + argument = jsonencode(module.backplane.kubeconfig_cluster_name) } harbor_username = { From 86f793bafd49d35d99e3179dc829cfb4a87c5671 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 17 Mar 2026 20:47:19 +0100 Subject: [PATCH 15/39] fix: get ske/starterkit tg apply to work --- modules/ske/forgejo-connector/meshstack_integration.tf | 2 +- modules/stackit/git-repository/README.md | 2 +- modules/stackit/git-repository/meshstack_integration.tf | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index f3a8d282..cc257a22 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -112,7 +112,7 @@ resource "meshstack_building_block_definition" "this" { description = "Connects a Forgejo repository with a tenant namespace on STACKIT SKE." support_url = "https://portal.stackit.cloud/git" target_type = "TENANT_LEVEL" - supported_platforms = [{ name = "STACKIT_KUBERNETES_ENGINE" }] + supported_platforms = [{ name = "KUBERNETES" }] run_transparency = true } diff --git a/modules/stackit/git-repository/README.md b/modules/stackit/git-repository/README.md index ec502005..8ec97fee 100644 --- a/modules/stackit/git-repository/README.md +++ b/modules/stackit/git-repository/README.md @@ -16,7 +16,7 @@ It combines: - static inputs from backplane (`forgejo_base_url`, `forgejo_token`, `forgejo_organization`) - optional static sensitive action secrets (`action_secrets`) - user inputs (`name`, `description`, `private`, `clone_addr`) -- outputs exposed to users (`repository_html_url`, `repository_clone_url`, `repository_ssh_url`, `summary`) +- outputs exposed to users (`repository_id`, `repository_html_url`, `repository_clone_url`, `repository_ssh_url`, `summary`) This allows platform teams to publish a reusable self-service Git repository building block for tenants. diff --git a/modules/stackit/git-repository/meshstack_integration.tf b/modules/stackit/git-repository/meshstack_integration.tf index 16b554e7..1f038fca 100644 --- a/modules/stackit/git-repository/meshstack_integration.tf +++ b/modules/stackit/git-repository/meshstack_integration.tf @@ -171,6 +171,12 @@ resource "meshstack_building_block_definition" "this" { } outputs = { + repository_id = { + display_name = "Repository ID" + type = "STRING" + assignment_type = "NONE" + } + repository_html_url = { display_name = "Open Repository" type = "STRING" From 76af2aafe5d6ab33dfd3fc1d97de55f062cee5b4 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 17 Mar 2026 23:23:44 +0100 Subject: [PATCH 16/39] fix(ske): align repository_id wiring and platform support - align git-repository repository_id output to INTEGER and improve output description\n- align forgejo-connector dependent repository_id input/variable types\n- set forgejo-connector supported_platforms to KUBERNETES\n- enable consumer updates for safe git-repository inputs (description/private)\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../forgejo-connector/buildingblock/README.md | 2 +- .../buildingblock/variables.tf | 2 +- .../meshstack_integration.tf | 2 +- .../git-repository/meshstack_integration.tf | 25 +++++++++++-------- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index 09f52164..8ecd0383 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -74,7 +74,7 @@ No modules. | [kubeconfig](#input\_kubeconfig) | Static kubeconfig content of the SKE cluster. | `any` | n/a | yes | | [kubeconfig\_cluster\_name](#input\_kubeconfig\_cluster\_name) | Cluster name used in merged kubeconfig context entries. | `string` | n/a | yes | | [namespace](#input\_namespace) | Associated namespace in kubernetes cluster. | `string` | n/a | yes | -| [repository\_id](#input\_repository\_id) | The ID of the Forgejo repository. | `string` | n/a | yes | +| [repository\_id](#input\_repository\_id) | The ID of the Forgejo repository. | `number` | n/a | yes | | [stage](#input\_stage) | Deployment stage used for secret suffixing (`dev` or `prod`). | `string` | n/a | yes | ## Outputs diff --git a/modules/ske/forgejo-connector/buildingblock/variables.tf b/modules/ske/forgejo-connector/buildingblock/variables.tf index f62fae60..8e2a9722 100644 --- a/modules/ske/forgejo-connector/buildingblock/variables.tf +++ b/modules/ske/forgejo-connector/buildingblock/variables.tf @@ -28,7 +28,7 @@ variable "harbor_password" { } variable "repository_id" { - type = string + type = number description = "The ID of the Forgejo repository." } diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index cc257a22..cdb9447b 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -233,7 +233,7 @@ resource "meshstack_building_block_definition" "this" { repository_id = { display_name = "repository_id" description = "ID of the parent Forgejo repository where action secrets are created." - type = "STRING" + type = "INTEGER" assignment_type = "BUILDING_BLOCK_OUTPUT" argument = jsonencode("${var.forgejo_repo_definition_uuid}.repository_id") } diff --git a/modules/stackit/git-repository/meshstack_integration.tf b/modules/stackit/git-repository/meshstack_integration.tf index 1f038fca..25a5d430 100644 --- a/modules/stackit/git-repository/meshstack_integration.tf +++ b/modules/stackit/git-repository/meshstack_integration.tf @@ -134,19 +134,21 @@ resource "meshstack_building_block_definition" "this" { } description = { - display_name = "Repository Description" - description = "Short description of the repository." - type = "STRING" - assignment_type = "USER_INPUT" - default_value = jsonencode("") + display_name = "Repository Description" + description = "Short description of the repository." + type = "STRING" + assignment_type = "USER_INPUT" + updateable_by_consumer = true + default_value = jsonencode("") } private = { - display_name = "Private Repository" - description = "If true, the repository has private visibility in Forgejo." - type = "BOOLEAN" - assignment_type = "USER_INPUT" - default_value = jsonencode(true) + display_name = "Private Repository" + description = "If true, the repository has private visibility in Forgejo." + type = "BOOLEAN" + assignment_type = "USER_INPUT" + updateable_by_consumer = true + default_value = jsonencode(true) } clone_addr = { @@ -173,8 +175,9 @@ resource "meshstack_building_block_definition" "this" { outputs = { repository_id = { display_name = "Repository ID" - type = "STRING" + type = "INTEGER" assignment_type = "NONE" + description = "Numeric Forgejo repository ID, primarily intended for wiring dependent building blocks." } repository_html_url = { From b836d1e68ffcb08cf468aead8126ecf4bd063fa0 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 00:59:53 +0100 Subject: [PATCH 17/39] feat(ske): simplify kubeconfig flow and align forgejo env inputs - simplify forgejo-connector to consume kubeconfig.yaml input directly\n- add kubeconfig mock fallback file plus precondition guard against accidental use\n- replace stage with repository_secret_name_suffix and compose _DEV/_PROD in starterkit\n- align git-repository to use FORGEJO_HOST/FORGEJO_API_TOKEN env inputs\n- align repository_id typing/wiring and related docs\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ske/forgejo-connector/backplane/README.md | 38 ----- .../ske/forgejo-connector/backplane/main.tf | 11 -- .../forgejo-connector/backplane/outputs.tf | 17 -- .../forgejo-connector/backplane/variables.tf | 22 --- .../forgejo-connector/buildingblock/README.md | 5 +- .../buildingblock/forgejo.tf | 39 ++--- .../buildingblock/kubeconfig-mock.yaml | 18 ++ .../buildingblock/kubernetes.tf | 27 ++- .../buildingblock/provider.tf | 18 -- .../buildingblock/variables.tf | 45 ++--- .../{backplane => buildingblock}/versions.tf | 12 +- .../meshstack_integration.tf | 161 ++++++------------ .../ske/ske-starterkit/buildingblock/main.tf | 8 +- modules/stackit/git-repository/README.md | 2 +- .../git-repository/buildingblock/README.md | 2 - .../git-repository/buildingblock/main.tf | 4 + .../git-repository/buildingblock/provider.tf | 4 - .../git-repository/buildingblock/variables.tf | 12 -- .../git-repository/meshstack_integration.tf | 14 +- 19 files changed, 147 insertions(+), 312 deletions(-) delete mode 100644 modules/ske/forgejo-connector/backplane/README.md delete mode 100644 modules/ske/forgejo-connector/backplane/main.tf delete mode 100644 modules/ske/forgejo-connector/backplane/outputs.tf delete mode 100644 modules/ske/forgejo-connector/backplane/variables.tf create mode 100644 modules/ske/forgejo-connector/buildingblock/kubeconfig-mock.yaml delete mode 100644 modules/ske/forgejo-connector/buildingblock/provider.tf rename modules/ske/forgejo-connector/{backplane => buildingblock}/versions.tf (67%) delete mode 100644 modules/stackit/git-repository/buildingblock/provider.tf diff --git a/modules/ske/forgejo-connector/backplane/README.md b/modules/ske/forgejo-connector/backplane/README.md deleted file mode 100644 index b0b41b53..00000000 --- a/modules/ske/forgejo-connector/backplane/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Backplane module for stackit connector Building Block - -This backplane does not create any new resources. It simply transforms input variables -into a `config_tf` output that can be dropped into meshStack's BuildingBlockDefinition -as an encrypted file input to configure the access to the kubernetes cluster. - - -## Requirements - -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.0 | -| [kubernetes](#requirement\_kubernetes) | 2.35.1 | - -## Modules - -No modules. - -## Resources - -No resources. - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [client\_certificate](#input\_client\_certificate) | Base64-encoded client certificate used for authenticating to the Kubernetes API server. | `string` | n/a | yes | -| [client\_key](#input\_client\_key) | Base64-encoded private key corresponding to the client certificate, used for authentication with the Kubernetes API server. | `string` | n/a | yes | -| [cluster\_ca\_certificate](#input\_cluster\_ca\_certificate) | Base64-encoded certificate authority (CA) certificate used to verify the Kubernetes API server's identity. | `string` | n/a | yes | -| [cluster\_host](#input\_cluster\_host) | The endpoint of the Kubernetes cluster. | `string` | n/a | yes | - -## Outputs - -| Name | Description | -|------|-------------| -| [config\_tf](#output\_config\_tf) | Generates a config.tf that can be dropped into meshStack's BuildingBlockDefinition as an encrypted file input to configure this building block. | -| [kubeconfig\_cluster\_name](#output\_kubeconfig\_cluster\_name) | Cluster name to use when merging static kubeconfig and generated service-account credentials. | - diff --git a/modules/ske/forgejo-connector/backplane/main.tf b/modules/ske/forgejo-connector/backplane/main.tf deleted file mode 100644 index 25eec64a..00000000 --- a/modules/ske/forgejo-connector/backplane/main.tf +++ /dev/null @@ -1,11 +0,0 @@ -# BB creates -# - service account with permissions to -# - manage a "github-image-pull-secret" -# - manage pods and deployments -# -# BB puts kubeconfig for this SA into Forgejo -# -# Forgejo action workflow uses this SA to -# - update image pull secret -# - deployment -# \ No newline at end of file diff --git a/modules/ske/forgejo-connector/backplane/outputs.tf b/modules/ske/forgejo-connector/backplane/outputs.tf deleted file mode 100644 index 326522c4..00000000 --- a/modules/ske/forgejo-connector/backplane/outputs.tf +++ /dev/null @@ -1,17 +0,0 @@ -output "config_tf" { - description = "Generates a config.tf that can be dropped into meshStack's BuildingBlockDefinition as an encrypted file input to configure this building block." - sensitive = true - value = <<-EOF - provider "kubernetes" { - host = "${var.cluster_host}" - cluster_ca_certificate = base64decode("${var.cluster_ca_certificate}") - client_certificate = base64decode("${var.client_certificate}") - client_key = base64decode("${var.client_key}") - } - EOF -} - -output "kubeconfig_cluster_name" { - description = "Cluster name to use when merging static kubeconfig and generated service-account credentials." - value = "stackit_k8s" -} diff --git a/modules/ske/forgejo-connector/backplane/variables.tf b/modules/ske/forgejo-connector/backplane/variables.tf deleted file mode 100644 index 08eaf08e..00000000 --- a/modules/ske/forgejo-connector/backplane/variables.tf +++ /dev/null @@ -1,22 +0,0 @@ -variable "cluster_host" { - type = string - description = "The endpoint of the Kubernetes cluster." -} - -variable "cluster_ca_certificate" { - description = "Base64-encoded certificate authority (CA) certificate used to verify the Kubernetes API server's identity." - type = string - sensitive = true -} - -variable "client_certificate" { - description = "Base64-encoded client certificate used for authenticating to the Kubernetes API server." - type = string - sensitive = true -} - -variable "client_key" { - description = "Base64-encoded private key corresponding to the client certificate, used for authentication with the Kubernetes API server." - type = string - sensitive = true -} diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index 8ecd0383..236f2fd8 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -55,7 +55,6 @@ No modules. | Name | Type | |------|------| -| [forgejo_repository_action_secret.container_registry](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [forgejo_repository_action_secret.kubeconfig](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [forgejo_repository_action_secret.namespace](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [kubernetes_cluster_role_binding.forgejo_actions_clusterissuer_access](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/cluster_role_binding) | resource | @@ -71,11 +70,9 @@ No modules. | [harbor\_host](#input\_harbor\_host) | The URL of the Harbor registry. | `string` | `"https://registry.onstackit.cloud"` | no | | [harbor\_password](#input\_harbor\_password) | The password for the Harbor registry. | `string` | n/a | yes | | [harbor\_username](#input\_harbor\_username) | The username for the Harbor registry. | `string` | n/a | yes | -| [kubeconfig](#input\_kubeconfig) | Static kubeconfig content of the SKE cluster. | `any` | n/a | yes | -| [kubeconfig\_cluster\_name](#input\_kubeconfig\_cluster\_name) | Cluster name used in merged kubeconfig context entries. | `string` | n/a | yes | | [namespace](#input\_namespace) | Associated namespace in kubernetes cluster. | `string` | n/a | yes | | [repository\_id](#input\_repository\_id) | The ID of the Forgejo repository. | `number` | n/a | yes | -| [stage](#input\_stage) | Deployment stage used for secret suffixing (`dev` or `prod`). | `string` | n/a | yes | +| [repository\_secret\_name\_suffix](#input\_repository\_secret\_name\_suffix) | Optional suffix appended to created repository secret names. | `string` | `""` | no | ## Outputs diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index e55d29c8..b44eb2e7 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -1,8 +1,12 @@ -locals { - stage_suffix = upper(var.stage) +provider "forgejo" { + # configured via env variables FORGEJO_HOST, FORGEJO_API_TOKEN +} - kubeconfig_user = { - current-context = var.kubeconfig_cluster_name +resource "forgejo_repository_action_secret" "kubeconfig" { + repository_id = var.repository_id + name = "KUBECONFIG${var.repository_secret_name_suffix}" + data = yamlencode(merge(local.kubeconfig, { + current-context = local.kubeconfig_cluster_name users = [ { @@ -15,38 +19,19 @@ locals { contexts = [ { - name = var.kubeconfig_cluster_name + name = local.kubeconfig_cluster_name context = { - cluster = var.kubeconfig_cluster_name + cluster = local.kubeconfig_cluster_name namespace = var.namespace user = kubernetes_service_account.forgejo_actions.metadata[0].name } } ] - } - kubeconfig = merge(var.kubeconfig, local.kubeconfig_user) -} - -resource "forgejo_repository_action_secret" "kubeconfig" { - repository_id = var.repository_id - name = "KUBECONFIG_${local.stage_suffix}" - data = yamlencode(local.kubeconfig) + })) } resource "forgejo_repository_action_secret" "namespace" { repository_id = var.repository_id - name = "K8S_NAMESPACE_${local.stage_suffix}" + name = "K8S_NAMESPACE${var.repository_secret_name_suffix}" data = var.namespace } - -resource "forgejo_repository_action_secret" "container_registry" { - for_each = { - HOST = var.harbor_host - USERNAME = var.harbor_username - PASSWORD = var.harbor_password - } - - repository_id = var.repository_id - name = "STACKIT_HARBOR_${each.key}" - data = each.value -} diff --git a/modules/ske/forgejo-connector/buildingblock/kubeconfig-mock.yaml b/modules/ske/forgejo-connector/buildingblock/kubeconfig-mock.yaml new file mode 100644 index 00000000..305aca2c --- /dev/null +++ b/modules/ske/forgejo-connector/buildingblock/kubeconfig-mock.yaml @@ -0,0 +1,18 @@ +# This mock kubeconfig is only used as a fallback when kubeconfig.yaml is not +# injected by meshStack yet (e.g. local terraform validate). +current-context: mock-context +clusters: + - name: mock-cluster + cluster: + server: https://example.invalid # This must not be changed to avoid accidental usage via precondition check + certificate-authority-data: "" +users: + - name: mock-user + user: + client-certificate-data: "" + client-key-data: "" +contexts: + - name: mock-context + context: + cluster: mock-cluster + user: mock-user diff --git a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf index f57e3be8..cfb50801 100644 --- a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf +++ b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf @@ -1,9 +1,34 @@ +locals { + kubeconfig = try( + yamldecode(file("${path.module}/kubeconfig.yaml")), + yamldecode(file("${path.module}/kubeconfig-mock.yaml")) + ) + kubeconfig_cluster = one(local.kubeconfig["clusters"])["cluster"] + kubeconfig_cluster_name = one(local.kubeconfig["clusters"])["name"] + kubeconfig_admin_user = one(local.kubeconfig["users"])["user"] +} + +provider "kubernetes" { + host = local.kubeconfig_cluster["server"] + cluster_ca_certificate = base64decode(local.kubeconfig_cluster["certificate-authority-data"]) + client_certificate = base64decode(local.kubeconfig_admin_user["client-certificate-data"]) + client_key = base64decode(local.kubeconfig_admin_user["client-key-data"]) +} + # Service account for forgejo actions to use resource "kubernetes_service_account" "forgejo_actions" { metadata { name = "forgejo-actions" namespace = var.namespace } + + # Unfortunately, check blocks only supported in Terraform, not OpenTofu. + lifecycle { + precondition { + condition = local.kubeconfig_cluster["server"] != "https://example.invalid" + error_message = "Mock kubeconfig detected. Ensure meshStack injected kubeconfig.yaml before apply." + } + } } resource "kubernetes_secret" "forgejo_actions" { @@ -65,7 +90,7 @@ resource "kubernetes_secret" "image_pull" { data = { ".dockerconfigjson" = jsonencode({ auths = { - "${var.harbor_host}" = { + (var.harbor_host) = { username = var.harbor_username password = var.harbor_password auth = base64encode("${var.harbor_username}:${var.harbor_password}") diff --git a/modules/ske/forgejo-connector/buildingblock/provider.tf b/modules/ske/forgejo-connector/buildingblock/provider.tf deleted file mode 100644 index 0f9f92ed..00000000 --- a/modules/ske/forgejo-connector/buildingblock/provider.tf +++ /dev/null @@ -1,18 +0,0 @@ -terraform { - required_providers { - forgejo = { - source = "svalabs/forgejo" - } - - kubernetes = { - source = "hashicorp/kubernetes" - version = "2.35.1" - } - } -} - -# provide the following env variables for this Building Block: -# FORGEJO_HOST -# FORGEJO_API_TOKEN -provider "forgejo" { -} diff --git a/modules/ske/forgejo-connector/buildingblock/variables.tf b/modules/ske/forgejo-connector/buildingblock/variables.tf index 8e2a9722..c01cec21 100644 --- a/modules/ske/forgejo-connector/buildingblock/variables.tf +++ b/modules/ske/forgejo-connector/buildingblock/variables.tf @@ -1,18 +1,23 @@ -variable "harbor_host" { +variable "namespace" { + description = "Associated namespace in kubernetes cluster." type = string - description = "The URL of the Harbor registry." - default = "https://registry.onstackit.cloud" } -variable "kubeconfig" { - type = any - description = "Static kubeconfig content of the SKE cluster." - sensitive = true +variable "repository_id" { + type = number + description = "The ID of the Forgejo repository." } -variable "kubeconfig_cluster_name" { +variable "repository_secret_name_suffix" { type = string - description = "Cluster name used in merged kubeconfig context entries." + description = "Optional suffix appended to created repository secret names." + default = "" +} + +variable "harbor_host" { + type = string + description = "The URL of the Harbor registry." + default = "https://registry.onstackit.cloud" } variable "harbor_username" { @@ -25,24 +30,4 @@ variable "harbor_password" { type = string description = "The password for the Harbor registry." sensitive = true -} - -variable "repository_id" { - type = number - description = "The ID of the Forgejo repository." -} - -variable "stage" { - type = string - description = "Deployment stage used for secret suffixing (`dev` or `prod`)." - - validation { - condition = can(regex("^(dev|prod)$", var.stage)) - error_message = "stage must be either 'dev' or 'prod'." - } -} - -variable "namespace" { - description = "Associated namespace in kubernetes cluster." - type = string -} +} \ No newline at end of file diff --git a/modules/ske/forgejo-connector/backplane/versions.tf b/modules/ske/forgejo-connector/buildingblock/versions.tf similarity index 67% rename from modules/ske/forgejo-connector/backplane/versions.tf rename to modules/ske/forgejo-connector/buildingblock/versions.tf index f3bca1ee..54c4232c 100644 --- a/modules/ske/forgejo-connector/backplane/versions.tf +++ b/modules/ske/forgejo-connector/buildingblock/versions.tf @@ -1,10 +1,16 @@ terraform { - required_version = ">= 1.0" - required_providers { + forgejo = { + source = "svalabs/forgejo" + } + kubernetes = { source = "hashicorp/kubernetes" version = "2.35.1" } } -} \ No newline at end of file +} + + + + diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index cdb9447b..a9cd261c 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -1,40 +1,6 @@ -# This file registers the SKE Forgejo connector building block definition. - -terraform { - required_providers { - meshstack = { - source = "meshcloud/meshstack" - version = "~> 0.20.0" - } - } -} - -variable "cluster_host" { - type = string - description = "The endpoint of the Kubernetes cluster." -} - -variable "cluster_ca_certificate" { - description = "Base64-encoded certificate authority (CA) certificate used to verify the Kubernetes API server's identity." - type = string - sensitive = true -} - -variable "client_certificate" { - description = "Base64-encoded client certificate used for authenticating to the Kubernetes API server." - type = string - sensitive = true -} - -variable "client_key" { - description = "Base64-encoded private key corresponding to the client certificate, used for authentication with the Kubernetes API server." - type = string - sensitive = true -} - -variable "cluster_kubeconfig" { - description = "Raw kubeconfig content containing the configuration required to access and authenticate to the Kubernetes cluster." - type = string +variable "kubeconfig" { + description = "Kubeconfig content containing the configuration required to access and authenticate to the Kubernetes cluster." + type = any sensitive = true } @@ -49,7 +15,7 @@ variable "forgejo_api_token" { variable "forgejo_repo_definition_uuid" { type = string - description = "UUID of the Forgejo repository building block definition used as parent dependency." + description = "UUID of the Forgejo repository building block definition used as parent dependency for tenant building blocks (connector)." } variable "harbor_host" { @@ -93,14 +59,6 @@ output "building_block_definition_version_ref" { description = "Version of BBD is consumed in building block compositions." } -module "backplane" { - source = "./backplane" - cluster_host = var.cluster_host - cluster_ca_certificate = var.cluster_ca_certificate - client_key = var.client_key - client_certificate = var.client_certificate -} - resource "meshstack_building_block_definition" "this" { metadata = { owned_by_workspace = var.meshstack.owning_workspace_identifier @@ -134,31 +92,49 @@ resource "meshstack_building_block_definition" "this" { dependency_refs = [{ uuid = var.forgejo_repo_definition_uuid }] inputs = { - "config.tf" = { - display_name = "config.tf" - description = "Static Kubernetes provider config and kubeconfig stub." + namespace = { + display_name = "namespace" + description = "Associated namespace in kubernetes cluster." + type = "STRING" + assignment_type = "PLATFORM_TENANT_ID" + } + + "kubeconfig.yaml" = { + display_name = "kubeconfig.yaml" + description = "kubeconfig.yaml file providing admin credentials to cluster." type = "FILE" assignment_type = "STATIC" sensitive = { argument = { - secret_value = "data:application/octet-stream;base64,${base64encode(module.backplane.config_tf)}" - secret_version = sha256(module.backplane.config_tf) + secret_value = yamlencode(var.kubeconfig) + secret_version = nonsensitive(sha256(var.kubeconfig)) } } } + repository_id = { + display_name = "repository_id" + description = "ID of the parent Forgejo repository where action secrets are created." + type = "INTEGER" + assignment_type = "BUILDING_BLOCK_OUTPUT" + argument = jsonencode("${var.forgejo_repo_definition_uuid}.repository_id") + } + + repository_secret_name_suffix = { + display_name = "repository_secret_name_suffix" + description = "Optional suffix appended to created repository secret names (for example `_DEV`)." + type = "STRING" + assignment_type = "USER_INPUT" + default_value = jsonencode("") + } + FORGEJO_HOST = { display_name = "FORGEJO_HOST" - description = "The URL of the Forgejo instance to connect to." + description = "The Host of the Forgejo instance to connect to." type = "STRING" assignment_type = "STATIC" is_environment = true - sensitive = { - argument = { - secret_value = jsonencode(var.forgejo_host) - secret_version = sha256(jsonencode(var.forgejo_host)) - } - } + argument = jsonencode(var.forgejo_host) } FORGEJO_API_TOKEN = { @@ -169,8 +145,8 @@ resource "meshstack_building_block_definition" "this" { is_environment = true sensitive = { argument = { - secret_value = jsonencode(var.forgejo_api_token) - secret_version = sha256(jsonencode(var.forgejo_api_token)) + secret_value = var.forgejo_api_token + secret_version = nonsensitive(sha256(var.forgejo_api_token)) } } } @@ -183,27 +159,6 @@ resource "meshstack_building_block_definition" "this" { argument = jsonencode(var.harbor_host) } - kubeconfig = { - display_name = "kubeconfig" - description = "Static cluster kubeconfig content merged with generated service-account credentials." - type = "CODE" - assignment_type = "STATIC" - sensitive = { - argument = { - secret_value = jsonencode(yamldecode(var.cluster_kubeconfig)) - secret_version = sha256(var.cluster_kubeconfig) - } - } - } - - kubeconfig_cluster_name = { - display_name = "kubeconfig_cluster_name" - description = "Cluster name used to connect static kubeconfig and generated user/context entries." - type = "STRING" - assignment_type = "STATIC" - argument = jsonencode(module.backplane.kubeconfig_cluster_name) - } - harbor_username = { display_name = "harbor_username" description = "The username for the Harbor registry." @@ -211,8 +166,8 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "STATIC" sensitive = { argument = { - secret_value = jsonencode(var.harbor_username) - secret_version = sha256(jsonencode(var.harbor_username)) + secret_value = var.harbor_username + secret_version = nonsensitive(sha256(var.harbor_username)) } } } @@ -224,40 +179,22 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "STATIC" sensitive = { argument = { - secret_value = jsonencode(var.harbor_password) - secret_version = sha256(jsonencode(var.harbor_password)) + secret_value = var.harbor_password + secret_version = nonsensitive(sha256(var.harbor_password)) } } } - - repository_id = { - display_name = "repository_id" - description = "ID of the parent Forgejo repository where action secrets are created." - type = "INTEGER" - assignment_type = "BUILDING_BLOCK_OUTPUT" - argument = jsonencode("${var.forgejo_repo_definition_uuid}.repository_id") - } - - namespace = { - display_name = "namespace" - description = "Associated namespace in kubernetes cluster." - type = "STRING" - assignment_type = "PLATFORM_TENANT_ID" - } - - stage = { - display_name = "stage" - description = "Deployment stage used for secret suffixing (`dev` or `prod`)." - type = "STRING" - assignment_type = "USER_INPUT" - value_validation_regex = "^(dev|prod)$" - validation_regex_error_message = "Stage must be either 'dev' or 'prod'." - } - } outputs = {} - - permissions = ["TENANT_LIST", "TENANT_SAVE", "TENANT_DELETE"] } } + +terraform { + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = "~> 0.20.0" + } + } +} \ No newline at end of file diff --git a/modules/ske/ske-starterkit/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf index 5b166344..a677664d 100644 --- a/modules/ske/ske-starterkit/buildingblock/main.tf +++ b/modules/ske/ske-starterkit/buildingblock/main.tf @@ -2,7 +2,7 @@ resource "meshstack_building_block_v2" "git_repository" { spec = { building_block_definition_version_ref = var.building_block_definition_version_refs["git-repository"] # provisioned in backplane - display_name = "${var.name} Git Repo" + display_name = "Git Repo ${var.name}" target_ref = { kind = "meshWorkspace" identifier = var.workspace_identifier @@ -73,7 +73,7 @@ resource "meshstack_building_block_v2" "forgejo_connector" { spec = { building_block_definition_version_ref = var.building_block_definition_version_refs["forgejo-connector"] - display_name = "${var.name} Forgejo Connector ${upper(each.key)}" + display_name = "${var.name} Forgejo Connector ${title(each.key)}" target_ref = { kind = "meshTenant" uuid = each.value.metadata.uuid @@ -85,8 +85,8 @@ resource "meshstack_building_block_v2" "forgejo_connector" { }] inputs = { - stage = { - value_string = each.key + repository_secret_name_suffix = { + value_string = "_${upper(each.key)}" } } } diff --git a/modules/stackit/git-repository/README.md b/modules/stackit/git-repository/README.md index 8ec97fee..af0ff3f8 100644 --- a/modules/stackit/git-repository/README.md +++ b/modules/stackit/git-repository/README.md @@ -13,7 +13,7 @@ It combines: `meshstack_integration.tf` creates a `meshstack_building_block_definition` with: - workspace-level target type -- static inputs from backplane (`forgejo_base_url`, `forgejo_token`, `forgejo_organization`) +- static inputs from backplane (`FORGEJO_HOST`, `FORGEJO_API_TOKEN`, `forgejo_organization`) - optional static sensitive action secrets (`action_secrets`) - user inputs (`name`, `description`, `private`, `clone_addr`) - outputs exposed to users (`repository_id`, `repository_html_url`, `repository_clone_url`, `repository_ssh_url`, `summary`) diff --git a/modules/stackit/git-repository/buildingblock/README.md b/modules/stackit/git-repository/buildingblock/README.md index 92c494f7..2dca3511 100644 --- a/modules/stackit/git-repository/buildingblock/README.md +++ b/modules/stackit/git-repository/buildingblock/README.md @@ -32,9 +32,7 @@ No modules. | [clone\_addr](#input\_clone\_addr) | Optional URL to clone into this repository, e.g. 'https://github.com/owner/repo.git'. Leave empty or `null` to create an empty repository. | `string` | `"null"` | no | | [default\_branch](#input\_default\_branch) | Default branch name | `string` | `"main"` | no | | [description](#input\_description) | Short description of the repository | `string` | `""` | no | -| [forgejo\_base\_url](#input\_forgejo\_base\_url) | STACKIT Git base URL | `string` | `"https://git-service.git.onstackit.cloud"` | no | | [forgejo\_organization](#input\_forgejo\_organization) | STACKIT Git organization where the repository will be created | `string` | n/a | yes | -| [forgejo\_token](#input\_forgejo\_token) | STACKIT Git API token (from backplane) | `string` | n/a | yes | | [name](#input\_name) | Name of the Git repository to create | `string` | n/a | yes | | [private](#input\_private) | Whether the repository should be private | `bool` | `true` | no | diff --git a/modules/stackit/git-repository/buildingblock/main.tf b/modules/stackit/git-repository/buildingblock/main.tf index b00d0e4d..818c73c8 100644 --- a/modules/stackit/git-repository/buildingblock/main.tf +++ b/modules/stackit/git-repository/buildingblock/main.tf @@ -1,3 +1,7 @@ +provider "forgejo" { + # configured via env variables FORGEJO_HOST, FORGEJO_API_TOKEN +} + locals { have_clone_addr = trimspace(var.clone_addr) != "" && var.clone_addr != "null" } diff --git a/modules/stackit/git-repository/buildingblock/provider.tf b/modules/stackit/git-repository/buildingblock/provider.tf deleted file mode 100644 index 87ab7ea6..00000000 --- a/modules/stackit/git-repository/buildingblock/provider.tf +++ /dev/null @@ -1,4 +0,0 @@ -provider "forgejo" { - host = var.forgejo_base_url - api_token = var.forgejo_token -} diff --git a/modules/stackit/git-repository/buildingblock/variables.tf b/modules/stackit/git-repository/buildingblock/variables.tf index c0ca2012..e8b64204 100644 --- a/modules/stackit/git-repository/buildingblock/variables.tf +++ b/modules/stackit/git-repository/buildingblock/variables.tf @@ -1,17 +1,5 @@ # ── Backplane inputs (static, set once per building block definition) ────────── -variable "forgejo_base_url" { - type = string - description = "STACKIT Git base URL" - default = "https://git-service.git.onstackit.cloud" -} - -variable "forgejo_token" { - type = string - description = "STACKIT Git API token (from backplane)" - sensitive = true -} - variable "forgejo_organization" { type = string description = "STACKIT Git organization where the repository will be created" diff --git a/modules/stackit/git-repository/meshstack_integration.tf b/modules/stackit/git-repository/meshstack_integration.tf index 25a5d430..6f659151 100644 --- a/modules/stackit/git-repository/meshstack_integration.tf +++ b/modules/stackit/git-repository/meshstack_integration.tf @@ -94,19 +94,21 @@ resource "meshstack_building_block_definition" "this" { inputs = { # ── Static inputs from backplane ────────────────────────────────────── - forgejo_base_url = { - display_name = "STACKIT Git Base URL" - description = "Base URL of the STACKIT Git instance" + FORGEJO_HOST = { + display_name = "FORGEJO_HOST" + description = "The Host of the Forgejo instance to connect to." type = "STRING" assignment_type = "STATIC" + is_environment = true argument = jsonencode(var.forgejo_base_url) } - forgejo_token = { - display_name = "STACKIT Git API Token" - description = "Personal Access Token for the STACKIT Git API" + FORGEJO_API_TOKEN = { + display_name = "FORGEJO_API_TOKEN" + description = "The API token for authenticating with the Forgejo instance." type = "STRING" assignment_type = "STATIC" + is_environment = true sensitive = { argument = { secret_value = var.forgejo_token From 9864f8d99af71ff8d1d5222167c0b5339c1162db Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 01:44:47 +0100 Subject: [PATCH 18/39] feat(ske): align composition with BBD output object Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../meshstack_integration.tf | 13 +++++---- .../ske-starterkit/buildingblock/README.md | 2 +- .../ske/ske-starterkit/buildingblock/main.tf | 6 ++-- .../ske-starterkit/buildingblock/variables.tf | 9 ++++-- .../ske-starterkit/meshstack_integration.tf | 29 +++++++++++++------ .../git-repository/meshstack_integration.tf | 14 ++++----- 6 files changed, 45 insertions(+), 28 deletions(-) diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index a9cd261c..1d017f3a 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -54,9 +54,12 @@ variable "hub" { EOT } -output "building_block_definition_version_ref" { - value = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release - description = "Version of BBD is consumed in building block compositions." +output "building_block_definition" { + value = { + uuid = meshstack_building_block_definition.this.metadata.uuid + version_ref = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release + } + description = "BBD is consumed in building block compositions." } resource "meshstack_building_block_definition" "this" { @@ -106,8 +109,8 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "STATIC" sensitive = { argument = { - secret_value = yamlencode(var.kubeconfig) - secret_version = nonsensitive(sha256(var.kubeconfig)) + secret_value = "data:application/yaml;base64,${base64encode(yamlencode(var.kubeconfig))}" # data type application/yaml is ignored anyway + secret_version = nonsensitive(sha256(yamlencode(var.kubeconfig))) } } } diff --git a/modules/ske/ske-starterkit/buildingblock/README.md b/modules/ske/ske-starterkit/buildingblock/README.md index acbf4e59..5b7c851c 100644 --- a/modules/ske/ske-starterkit/buildingblock/README.md +++ b/modules/ske/ske-starterkit/buildingblock/README.md @@ -34,7 +34,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [building\_block\_definition\_version\_refs](#input\_building\_block\_definition\_version\_refs) | n/a | `map(object({ uuid = string }))` | n/a | yes | +| [building\_block\_definitions](#input\_building\_block\_definitions) | n/a |
map(object({
uuid = string
version_ref = object({
uuid = string
})
}))
| n/a | yes | | [creator](#input\_creator) | Information about the creator of the resources who will be assigned Project Admin role |
object({
type = string
identifier = string
displayName = string
username = optional(string)
email = optional(string)
euid = optional(string)
})
| n/a | yes | | [full\_platform\_identifier](#input\_full\_platform\_identifier) | Full platform identifier of the SKE platform. | `string` | n/a | yes | | [landing\_zone\_identifiers](#input\_landing\_zone\_identifiers) | SKE Landing zone identifiers for the dev/prod meshTenant. |
object({
dev = string
prod = string
})
| n/a | yes | diff --git a/modules/ske/ske-starterkit/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf index a677664d..3b0549fb 100644 --- a/modules/ske/ske-starterkit/buildingblock/main.tf +++ b/modules/ske/ske-starterkit/buildingblock/main.tf @@ -1,6 +1,6 @@ resource "meshstack_building_block_v2" "git_repository" { spec = { - building_block_definition_version_ref = var.building_block_definition_version_refs["git-repository"] # provisioned in backplane + building_block_definition_version_ref = var.building_block_definitions["git-repository"].version_ref # provisioned in backplane display_name = "Git Repo ${var.name}" target_ref = { @@ -71,7 +71,7 @@ resource "meshstack_building_block_v2" "forgejo_connector" { for_each = meshstack_tenant_v4.this spec = { - building_block_definition_version_ref = var.building_block_definition_version_refs["forgejo-connector"] + building_block_definition_version_ref = var.building_block_definitions["forgejo-connector"].version_ref display_name = "${var.name} Forgejo Connector ${title(each.key)}" target_ref = { @@ -81,7 +81,7 @@ resource "meshstack_building_block_v2" "forgejo_connector" { parent_building_blocks = [{ buildingblock_uuid = meshstack_building_block_v2.git_repository.metadata.uuid - definition_uuid = meshstack_building_block_v2.git_repository.spec.building_block_definition_version_ref.uuid + definition_uuid = var.building_block_definitions["git-repository"].uuid }] inputs = { diff --git a/modules/ske/ske-starterkit/buildingblock/variables.tf b/modules/ske/ske-starterkit/buildingblock/variables.tf index d0628fc5..2f8e6372 100644 --- a/modules/ske/ske-starterkit/buildingblock/variables.tf +++ b/modules/ske/ske-starterkit/buildingblock/variables.tf @@ -47,6 +47,11 @@ variable "repo_clone_addr" { description = "URL to clone into the starterkit git repository." } -variable "building_block_definition_version_refs" { - type = map(object({ uuid = string })) +variable "building_block_definitions" { + type = map(object({ + uuid = string + version_ref = object({ + uuid = string + }) + })) } diff --git a/modules/ske/ske-starterkit/meshstack_integration.tf b/modules/ske/ske-starterkit/meshstack_integration.tf index 095fb109..58556514 100644 --- a/modules/ske/ske-starterkit/meshstack_integration.tf +++ b/modules/ske/ske-starterkit/meshstack_integration.tf @@ -42,10 +42,13 @@ variable "notification_subscribers" { default = [] } -variable "building_block_definition_version_refs" { +variable "building_block_definitions" { type = map(object({ - content_hash = string # adding the content nicely tracks changes in dependent BBDs (draft mode) - uuid = string + uuid = string + version_ref = object({ + content_hash = string # adding the content nicely tracks changes in dependent BBDs (draft mode) + uuid = string + }) })) } @@ -181,20 +184,28 @@ EOT display_name = "Clone from URL" argument = jsonencode(var.repo_clone_addr) } + "building_block_definitions" = { + assignment_type = "STATIC" + type = "CODE" + description = "Definitions used to create auxiliary building blocks (composition)." + display_name = "BBDs" + # jsonencode twice is correct, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 + argument = jsonencode(jsonencode(var.building_block_definitions)) + }, + # TODO remove inputs below before merge, leftover from dev attempts in grubinator2 instance "building_block_definition_version_refs" = { assignment_type = "STATIC" type = "CODE" - description = "Refs used to create auxiliary building blocks (composition)." - display_name = "BBD Version Refs" + description = "REMOVEME" + display_name = "REMOVEME" # jsonencode twice is correct, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 - argument = jsonencode(jsonencode(var.building_block_definition_version_refs)) + argument = jsonencode(jsonencode(var.building_block_definitions)) }, - # TODO remove before merge, leftover from dev attempts in grubinator2 instance "git_repository_action_secrets" = { assignment_type = "STATIC" type = "CODE" - description = "REMOVEME Static sensitive Forgejo Actions secrets passed to the composed git-repository building block." - display_name = "REMOVEME Git Repository Action Secrets" + description = "REMOVEME" + display_name = "REMOVEME" # jsonencode twice is correct, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 sensitive = { argument = { diff --git a/modules/stackit/git-repository/meshstack_integration.tf b/modules/stackit/git-repository/meshstack_integration.tf index 6f659151..77d7133c 100644 --- a/modules/stackit/git-repository/meshstack_integration.tf +++ b/modules/stackit/git-repository/meshstack_integration.tf @@ -44,14 +44,12 @@ variable "hub" { EOT } -output "building_block_definition_version_ref" { - value = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release - description = "Version of BBD is consumed in Building Block compositions, for example in the backplane of starter kits." -} - -output "building_block_definition_uuid" { - value = meshstack_building_block_definition.this.ref.uuid - description = "UUID of the STACKIT Git Repository building block definition." +output "building_block_definition" { + value = { + uuid = meshstack_building_block_definition.this.metadata.uuid + version_ref = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release + } + description = "BBD is consumed in Building Block compositions, for example in the backplane of starter kits." } module "backplane" { From e8b1d42ab63195f8e8f6659e549d075dc350d53c Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 02:16:36 +0100 Subject: [PATCH 19/39] feat(ske): simplify forgejo connector kubeconfig input Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../forgejo-connector/buildingblock/README.md | 2 ++ .../buildingblock/kubernetes.tf | 23 ++++++++++++++++++- .../meshstack_integration.tf | 4 ++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index 236f2fd8..eed002bd 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -57,11 +57,13 @@ No modules. |------|------| | [forgejo_repository_action_secret.kubeconfig](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [forgejo_repository_action_secret.namespace](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | +| [kubernetes_cluster_role.clusterissuer_reader](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/cluster_role) | resource | | [kubernetes_cluster_role_binding.forgejo_actions_clusterissuer_access](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/cluster_role_binding) | resource | | [kubernetes_role_binding.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/role_binding) | resource | | [kubernetes_secret.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_secret.image_pull](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_service_account.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/service_account) | resource | +| [random_string.clusterissuer_reader_name_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | ## Inputs diff --git a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf index cfb50801..7d6e9379 100644 --- a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf +++ b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf @@ -61,6 +61,27 @@ resource "kubernetes_role_binding" "forgejo_actions" { } } +resource "random_string" "clusterissuer_reader_name_suffix" { + length = 8 + lower = true + upper = false + numeric = false + special = false +} + +# The ClusterIssuer access is needed so that SSL certificates can be issued for projects using the connector. +resource "kubernetes_cluster_role" "clusterissuer_reader" { + metadata { + name = "clusterissuer-reader-${random_string.clusterissuer_reader_name_suffix.result}" # random suffix ensures multiple roles can exist + } + + rule { + api_groups = ["cert-manager.io"] + resources = ["clusterissuers"] + verbs = ["get"] + } +} + resource "kubernetes_cluster_role_binding" "forgejo_actions_clusterissuer_access" { metadata { name = "forgejo-actions-clusterissuer-access-${var.namespace}" @@ -69,7 +90,7 @@ resource "kubernetes_cluster_role_binding" "forgejo_actions_clusterissuer_access role_ref { api_group = "rbac.authorization.k8s.io" kind = "ClusterRole" - name = "clusterissuer-reader" # This role is created in the backplane module + name = kubernetes_cluster_role.clusterissuer_reader.metadata[0].name } subject { diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 1d017f3a..8676a9ba 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -96,8 +96,8 @@ resource "meshstack_building_block_definition" "this" { inputs = { namespace = { - display_name = "namespace" - description = "Associated namespace in kubernetes cluster." + display_name = "K8S Namespace" + description = "Provided namespace in Kubernetes cluster." type = "STRING" assignment_type = "PLATFORM_TENANT_ID" } From 89ba13494aa5abeff96c95a8991606791bae6b86 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 02:50:24 +0100 Subject: [PATCH 20/39] feat(ske): configure default SA to use harbor pull secret Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- modules/ske/forgejo-connector/buildingblock/README.md | 1 + .../ske/forgejo-connector/buildingblock/kubernetes.tf | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index eed002bd..e60f48b9 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -59,6 +59,7 @@ No modules. | [forgejo_repository_action_secret.namespace](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [kubernetes_cluster_role.clusterissuer_reader](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/cluster_role) | resource | | [kubernetes_cluster_role_binding.forgejo_actions_clusterissuer_access](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/cluster_role_binding) | resource | +| [kubernetes_default_service_account.namespace_default](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/default_service_account) | resource | | [kubernetes_role_binding.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/role_binding) | resource | | [kubernetes_secret.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_secret.image_pull](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | diff --git a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf index 7d6e9379..4cd51ea4 100644 --- a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf +++ b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf @@ -120,3 +120,13 @@ resource "kubernetes_secret" "image_pull" { }) } } + +resource "kubernetes_default_service_account" "namespace_default" { + metadata { + namespace = var.namespace + } + + image_pull_secret { + name = kubernetes_secret.image_pull.metadata[0].name + } +} From b1aec4bb3b4e12793492a94c51a38a04e348d77f Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 06:45:37 +0100 Subject: [PATCH 21/39] feat(ske): add action variables and trigger pipeline dispatch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../forgejo-connector/buildingblock/README.md | 6 + .../buildingblock/forgejo.tf | 146 ++++++++++++++++++ .../buildingblock/versions.tf | 10 +- .../git-repository/buildingblock/README.md | 5 + .../git-repository/buildingblock/main.tf | 37 +++++ .../git-repository/buildingblock/variables.tf | 6 + .../git-repository/buildingblock/versions.tf | 8 + .../git-repository/meshstack_integration.tf | 12 ++ 8 files changed, 229 insertions(+), 1 deletion(-) diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index e60f48b9..56a8e2ea 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -45,7 +45,9 @@ the backplane module's `config_tf` output. | Name | Version | |------|---------| +| [external](#requirement\_external) | ~> 2.3.0 | | [kubernetes](#requirement\_kubernetes) | 2.35.1 | +| [restapi](#requirement\_restapi) | 3.0.0 | ## Modules @@ -65,6 +67,10 @@ No modules. | [kubernetes_secret.image_pull](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_service_account.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/service_account) | resource | | [random_string.clusterissuer_reader_name_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [restapi_object.pipeline_dispatch](https://registry.terraform.io/providers/Mastercard/restapi/3.0.0/docs/resources/object) | resource | +| [terraform_data.await_pipeline_workflow](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | +| [external_external.forgejo_env](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | +| [external_external.repository](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | ## Inputs diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index b44eb2e7..e1539f3e 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -2,6 +2,68 @@ provider "forgejo" { # configured via env variables FORGEJO_HOST, FORGEJO_API_TOKEN } +data "external" "forgejo_env" { + program = ["python3", "-c", <<-PY +import json +import os + +print(json.dumps({ + "forgejo_host": os.environ["FORGEJO_HOST"], + "forgejo_api_token": os.environ["FORGEJO_API_TOKEN"], +})) +PY + ] +} + +provider "restapi" { + uri = data.external.forgejo_env.result.forgejo_host + write_returns_object = false + + headers = { + Authorization = "token ${data.external.forgejo_env.result.forgejo_api_token}" + Content-Type = "application/json" + } +} + +data "external" "repository" { + program = ["python3", "-c", <<-PY +import json +import sys +import urllib.request + +query = json.loads(sys.stdin.read()) +host = query["FORGEJO_HOST"].rstrip("/") +token = query["FORGEJO_API_TOKEN"] +repository_id = query["FORGEJO_REPOSITORY_ID"] +req = urllib.request.Request( + f"{host}/api/v1/repositories/{repository_id}", + headers={"Authorization": f"token {token}", "Content-Type": "application/json"}, + method="GET", +) +with urllib.request.urlopen(req, timeout=30) as resp: + payload = json.loads(resp.read().decode("utf-8")) + +print(json.dumps({ + "owner": payload["owner"]["username"], + "name": payload["name"], + "default_branch": payload.get("default_branch", "main"), +})) +PY + ] + + query = { + FORGEJO_HOST = data.external.forgejo_env.result.forgejo_host + FORGEJO_API_TOKEN = data.external.forgejo_env.result.forgejo_api_token + FORGEJO_REPOSITORY_ID = tostring(var.repository_id) + } +} + +locals { + repository_owner = data.external.repository.result.owner + repository_name = data.external.repository.result.name + repository_default_branch = data.external.repository.result.default_branch +} + resource "forgejo_repository_action_secret" "kubeconfig" { repository_id = var.repository_id name = "KUBECONFIG${var.repository_secret_name_suffix}" @@ -35,3 +97,87 @@ resource "forgejo_repository_action_secret" "namespace" { name = "K8S_NAMESPACE${var.repository_secret_name_suffix}" data = var.namespace } + +resource "restapi_object" "pipeline_dispatch" { + path = "/api/v1/repositories" + id_attribute = "id" + object_id = tostring(var.repository_id) + create_path = "/api/v1/repos/${local.repository_owner}/${local.repository_name}/actions/workflows/pipeline.yaml/dispatches" + create_method = "POST" + update_path = "/api/v1/repos/${local.repository_owner}/${local.repository_name}/actions/workflows/pipeline.yaml/dispatches" + update_method = "POST" + data = jsonencode({ + ref = local.repository_default_branch + }) + ignore_all_server_changes = true + + depends_on = [ + forgejo_repository_action_secret.kubeconfig, + forgejo_repository_action_secret.namespace + ] +} + +resource "terraform_data" "await_pipeline_workflow" { + depends_on = [restapi_object.pipeline_dispatch] + + provisioner "local-exec" { + command = <<-EOT + python3 - <<'PY' +import json +import os +import time +import urllib.request + +host = os.environ["FORGEJO_HOST"].rstrip("/") +token = os.environ["FORGEJO_API_TOKEN"] +owner = os.environ["FORGEJO_REPOSITORY_OWNER"] +repo = os.environ["FORGEJO_REPOSITORY_NAME"] +branch = os.environ["FORGEJO_REPOSITORY_DEFAULT_BRANCH"] + +runs_url = f"{host}/api/v1/repos/{owner}/{repo}/actions/runs?limit=20" +headers = {"Authorization": f"token {token}", "Content-Type": "application/json"} + +def list_runs(): + req = urllib.request.Request(runs_url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=30) as resp: + payload = json.loads(resp.read().decode("utf-8")) + return payload.get("workflow_runs", []) + +deadline = time.time() + 900 +seen_run_id = None + +while time.time() < deadline: + runs = list_runs() + candidates = [r for r in runs if r.get("event") == "workflow_dispatch" and r.get("head_branch") == branch] + candidates.sort(key=lambda r: r.get("id", 0), reverse=True) + + if candidates: + run = candidates[0] + run_id = run.get("id") + status = run.get("status") + conclusion = run.get("conclusion") + html_url = run.get("html_url", "") + + if seen_run_id is None: + seen_run_id = run_id + + if run_id == seen_run_id and status == "completed": + if conclusion == "success": + print(f"Workflow run {run_id} completed successfully: {html_url}") + raise SystemExit(0) + raise SystemExit(f"Workflow run {run_id} failed with conclusion={conclusion}: {html_url}") + + time.sleep(10) + +raise SystemExit("Timed out waiting for dispatched pipeline workflow to complete.") +PY + EOT + environment = { + FORGEJO_HOST = data.external.forgejo_env.result.forgejo_host + FORGEJO_API_TOKEN = data.external.forgejo_env.result.forgejo_api_token + FORGEJO_REPOSITORY_OWNER = local.repository_owner + FORGEJO_REPOSITORY_NAME = local.repository_name + FORGEJO_REPOSITORY_DEFAULT_BRANCH = local.repository_default_branch + } + } +} diff --git a/modules/ske/forgejo-connector/buildingblock/versions.tf b/modules/ske/forgejo-connector/buildingblock/versions.tf index 54c4232c..83263289 100644 --- a/modules/ske/forgejo-connector/buildingblock/versions.tf +++ b/modules/ske/forgejo-connector/buildingblock/versions.tf @@ -1,5 +1,9 @@ terraform { required_providers { + external = { + source = "hashicorp/external" + version = "~> 2.3.0" + } forgejo = { source = "svalabs/forgejo" } @@ -8,9 +12,13 @@ terraform { source = "hashicorp/kubernetes" version = "2.35.1" } + + restapi = { + source = "Mastercard/restapi" + version = "3.0.0" + } } } - diff --git a/modules/stackit/git-repository/buildingblock/README.md b/modules/stackit/git-repository/buildingblock/README.md index 2dca3511..329cdf5b 100644 --- a/modules/stackit/git-repository/buildingblock/README.md +++ b/modules/stackit/git-repository/buildingblock/README.md @@ -11,7 +11,9 @@ description: Provisions a Git repository on STACKIT Git (Forgejo) with optional | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.4.0 | +| [external](#requirement\_external) | ~> 2.3.0 | | [forgejo](#requirement\_forgejo) | ~> 1.3.0 | +| [restapi](#requirement\_restapi) | 3.0.0 | ## Modules @@ -23,12 +25,15 @@ No modules. |------|------| | [forgejo_repository.repository](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository) | resource | | [forgejo_repository_action_secret.action_secrets](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | +| [restapi_object.action_variables](https://registry.terraform.io/providers/Mastercard/restapi/3.0.0/docs/resources/object) | resource | +| [external_external.forgejo_env](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [action\_secrets](#input\_action\_secrets) | Map of Forgejo Actions secrets to create in the repository. | `map(string)` | `{}` | no | +| [action\_variables](#input\_action\_variables) | Map of Forgejo Actions variables to create in the repository. | `map(string)` | `{}` | no | | [clone\_addr](#input\_clone\_addr) | Optional URL to clone into this repository, e.g. 'https://github.com/owner/repo.git'. Leave empty or `null` to create an empty repository. | `string` | `"null"` | no | | [default\_branch](#input\_default\_branch) | Default branch name | `string` | `"main"` | no | | [description](#input\_description) | Short description of the repository | `string` | `""` | no | diff --git a/modules/stackit/git-repository/buildingblock/main.tf b/modules/stackit/git-repository/buildingblock/main.tf index 818c73c8..23a060ae 100644 --- a/modules/stackit/git-repository/buildingblock/main.tf +++ b/modules/stackit/git-repository/buildingblock/main.tf @@ -2,6 +2,29 @@ provider "forgejo" { # configured via env variables FORGEJO_HOST, FORGEJO_API_TOKEN } +data "external" "forgejo_env" { + program = ["python3", "-c", <<-PY +import json +import os + +print(json.dumps({ + "forgejo_host": os.environ["FORGEJO_HOST"], + "forgejo_auth_header": f'token {os.environ["FORGEJO_API_TOKEN"]}', +})) +PY + ] +} + +provider "restapi" { + uri = data.external.forgejo_env.result.forgejo_host + write_returns_object = true + + headers = { + Authorization = data.external.forgejo_env.result.forgejo_auth_header + Content-Type = "application/json" + } +} + locals { have_clone_addr = trimspace(var.clone_addr) != "" && var.clone_addr != "null" } @@ -26,3 +49,17 @@ resource "forgejo_repository_action_secret" "action_secrets" { name = each.key data = each.value } + +resource "restapi_object" "action_variables" { + for_each = var.action_variables + + path = "/api/v1/repos/${var.forgejo_organization}/${forgejo_repository.repository.name}/actions/variables" + id_attribute = "name" + object_id = each.key + update_method = "PATCH" + data = jsonencode({ + name = each.key + value = each.value + }) + ignore_server_additions = true +} diff --git a/modules/stackit/git-repository/buildingblock/variables.tf b/modules/stackit/git-repository/buildingblock/variables.tf index e8b64204..effbd0d2 100644 --- a/modules/stackit/git-repository/buildingblock/variables.tf +++ b/modules/stackit/git-repository/buildingblock/variables.tf @@ -17,6 +17,12 @@ variable "action_secrets" { } } +variable "action_variables" { + type = map(string) + description = "Map of Forgejo Actions variables to create in the repository." + default = {} +} + # ── User inputs (set per building block instance) ───────────────────────────── variable "name" { diff --git a/modules/stackit/git-repository/buildingblock/versions.tf b/modules/stackit/git-repository/buildingblock/versions.tf index 3b581c8f..ebf1e4d5 100644 --- a/modules/stackit/git-repository/buildingblock/versions.tf +++ b/modules/stackit/git-repository/buildingblock/versions.tf @@ -2,9 +2,17 @@ terraform { required_version = ">= 1.4.0" required_providers { + external = { + source = "hashicorp/external" + version = "~> 2.3.0" + } forgejo = { source = "svalabs/forgejo" version = "~> 1.3.0" } + restapi = { + source = "Mastercard/restapi" + version = "3.0.0" + } } } diff --git a/modules/stackit/git-repository/meshstack_integration.tf b/modules/stackit/git-repository/meshstack_integration.tf index 77d7133c..78735bbb 100644 --- a/modules/stackit/git-repository/meshstack_integration.tf +++ b/modules/stackit/git-repository/meshstack_integration.tf @@ -32,6 +32,11 @@ variable "action_secrets" { } } +variable "action_variables" { + type = map(string) + default = {} +} + variable "hub" { type = object({ git_ref = optional(string, "main") @@ -170,6 +175,13 @@ resource "meshstack_building_block_definition" "this" { } } } + action_variables = { + display_name = "Repository Action Variables" + description = "Static non-sensitive map of Forgejo Actions variables created in each provisioned repository." + type = "CODE" + assignment_type = "STATIC" + argument = jsonencode(var.action_variables) + } } outputs = { From 0583aaccdd3d33c387d1b9b9f53bf2e569eb0528 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 09:09:36 +0100 Subject: [PATCH 22/39] chore: ignore python build files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 2088652d..f1dcfa92 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ yarn-error.log* *tfvars* .terraform.lock.hcl .env +__pycache__/ +*.pyc # Generated assets website/public/assets/building-block-logos/ From 176571e861ed0f33db49b382ea7224aed8049196 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 09:42:26 +0100 Subject: [PATCH 23/39] feat(ske): refactor forgejo connector workflow trigger wiring Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../forgejo-connector/buildingblock/README.md | 12 +- .../buildingblock/forgejo.tf | 203 +++++------------- .../get_forgejo_repository_context.py | 45 ++++ .../buildingblock/outputs.tf | 5 +- .../trigger_and_await_forgejo_workflow.py | 119 ++++++++++ .../meshstack_integration.tf | 11 +- 6 files changed, 240 insertions(+), 155 deletions(-) create mode 100644 modules/ske/forgejo-connector/buildingblock/get_forgejo_repository_context.py create mode 100644 modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index 56a8e2ea..c6c6d718 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -57,8 +57,7 @@ No modules. | Name | Type | |------|------| -| [forgejo_repository_action_secret.kubeconfig](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | -| [forgejo_repository_action_secret.namespace](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | +| [forgejo_repository_action_secret.action_secrets](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [kubernetes_cluster_role.clusterissuer_reader](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/cluster_role) | resource | | [kubernetes_cluster_role_binding.forgejo_actions_clusterissuer_access](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/cluster_role_binding) | resource | | [kubernetes_default_service_account.namespace_default](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/default_service_account) | resource | @@ -67,10 +66,9 @@ No modules. | [kubernetes_secret.image_pull](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_service_account.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/service_account) | resource | | [random_string.clusterissuer_reader_name_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | -| [restapi_object.pipeline_dispatch](https://registry.terraform.io/providers/Mastercard/restapi/3.0.0/docs/resources/object) | resource | +| [restapi_object.action_variables](https://registry.terraform.io/providers/Mastercard/restapi/3.0.0/docs/resources/object) | resource | | [terraform_data.await_pipeline_workflow](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | -| [external_external.forgejo_env](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | -| [external_external.repository](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | +| [external_external.repository_context](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | ## Inputs @@ -85,5 +83,7 @@ No modules. ## Outputs -No outputs. +| Name | Description | +|------|-------------| +| [action\_variables](#output\_action\_variables) | Action variables to expose for dependent building block wiring. | diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index e1539f3e..d927f500 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -2,182 +2,93 @@ provider "forgejo" { # configured via env variables FORGEJO_HOST, FORGEJO_API_TOKEN } -data "external" "forgejo_env" { - program = ["python3", "-c", <<-PY -import json -import os - -print(json.dumps({ - "forgejo_host": os.environ["FORGEJO_HOST"], - "forgejo_api_token": os.environ["FORGEJO_API_TOKEN"], -})) -PY - ] -} - provider "restapi" { - uri = data.external.forgejo_env.result.forgejo_host + uri = data.external.repository_context.result.forgejo_host write_returns_object = false headers = { - Authorization = "token ${data.external.forgejo_env.result.forgejo_api_token}" + Authorization = "token ${data.external.repository_context.result.forgejo_api_token}" Content-Type = "application/json" } } -data "external" "repository" { - program = ["python3", "-c", <<-PY -import json -import sys -import urllib.request - -query = json.loads(sys.stdin.read()) -host = query["FORGEJO_HOST"].rstrip("/") -token = query["FORGEJO_API_TOKEN"] -repository_id = query["FORGEJO_REPOSITORY_ID"] -req = urllib.request.Request( - f"{host}/api/v1/repositories/{repository_id}", - headers={"Authorization": f"token {token}", "Content-Type": "application/json"}, - method="GET", -) -with urllib.request.urlopen(req, timeout=30) as resp: - payload = json.loads(resp.read().decode("utf-8")) - -print(json.dumps({ - "owner": payload["owner"]["username"], - "name": payload["name"], - "default_branch": payload.get("default_branch", "main"), -})) -PY - ] +data "external" "repository_context" { + program = ["python3", "${path.module}/get_forgejo_repository_context.py"] query = { - FORGEJO_HOST = data.external.forgejo_env.result.forgejo_host - FORGEJO_API_TOKEN = data.external.forgejo_env.result.forgejo_api_token FORGEJO_REPOSITORY_ID = tostring(var.repository_id) } } locals { - repository_owner = data.external.repository.result.owner - repository_name = data.external.repository.result.name - repository_default_branch = data.external.repository.result.default_branch -} - -resource "forgejo_repository_action_secret" "kubeconfig" { - repository_id = var.repository_id - name = "KUBECONFIG${var.repository_secret_name_suffix}" - data = yamlencode(merge(local.kubeconfig, { - current-context = local.kubeconfig_cluster_name - - users = [ - { - name = kubernetes_service_account.forgejo_actions.metadata[0].name - user = { - "token" = kubernetes_secret.forgejo_actions.data.token + repository_owner = data.external.repository_context.result.owner + repository_name = data.external.repository_context.result.name + repository_default_branch = data.external.repository_context.result.default_branch + + action_secrets = { + "KUBECONFIG${var.repository_secret_name_suffix}" = yamlencode(merge(local.kubeconfig, { + current-context = local.kubeconfig_cluster_name + + users = [ + { + name = kubernetes_service_account.forgejo_actions.metadata[0].name + user = { + "token" = kubernetes_secret.forgejo_actions.data.token + } } - } - ] - - contexts = [ - { - name = local.kubeconfig_cluster_name - context = { - cluster = local.kubeconfig_cluster_name - namespace = var.namespace - user = kubernetes_service_account.forgejo_actions.metadata[0].name + ] + + contexts = [ + { + name = local.kubeconfig_cluster_name + context = { + cluster = local.kubeconfig_cluster_name + namespace = var.namespace + user = kubernetes_service_account.forgejo_actions.metadata[0].name + } } - } - ] - })) + ] + })) + } + + action_variables = { + "K8S_NAMESPACE${var.repository_secret_name_suffix}" = var.namespace + } } -resource "forgejo_repository_action_secret" "namespace" { +resource "forgejo_repository_action_secret" "action_secrets" { + for_each = local.action_secrets + repository_id = var.repository_id - name = "K8S_NAMESPACE${var.repository_secret_name_suffix}" - data = var.namespace + name = each.key + data = each.value } -resource "restapi_object" "pipeline_dispatch" { - path = "/api/v1/repositories" - id_attribute = "id" - object_id = tostring(var.repository_id) - create_path = "/api/v1/repos/${local.repository_owner}/${local.repository_name}/actions/workflows/pipeline.yaml/dispatches" - create_method = "POST" - update_path = "/api/v1/repos/${local.repository_owner}/${local.repository_name}/actions/workflows/pipeline.yaml/dispatches" - update_method = "POST" +resource "restapi_object" "action_variables" { + for_each = local.action_variables + + path = "/api/v1/repos/${local.repository_owner}/${local.repository_name}/actions/variables" + id_attribute = "name" + object_id = each.key + update_method = "PATCH" data = jsonencode({ - ref = local.repository_default_branch + name = each.key + value = each.value }) - ignore_all_server_changes = true - - depends_on = [ - forgejo_repository_action_secret.kubeconfig, - forgejo_repository_action_secret.namespace - ] + ignore_server_additions = true } resource "terraform_data" "await_pipeline_workflow" { - depends_on = [restapi_object.pipeline_dispatch] + depends_on = [ + forgejo_repository_action_secret.action_secrets, + restapi_object.action_variables, + ] provisioner "local-exec" { - command = <<-EOT - python3 - <<'PY' -import json -import os -import time -import urllib.request - -host = os.environ["FORGEJO_HOST"].rstrip("/") -token = os.environ["FORGEJO_API_TOKEN"] -owner = os.environ["FORGEJO_REPOSITORY_OWNER"] -repo = os.environ["FORGEJO_REPOSITORY_NAME"] -branch = os.environ["FORGEJO_REPOSITORY_DEFAULT_BRANCH"] - -runs_url = f"{host}/api/v1/repos/{owner}/{repo}/actions/runs?limit=20" -headers = {"Authorization": f"token {token}", "Content-Type": "application/json"} - -def list_runs(): - req = urllib.request.Request(runs_url, headers=headers, method="GET") - with urllib.request.urlopen(req, timeout=30) as resp: - payload = json.loads(resp.read().decode("utf-8")) - return payload.get("workflow_runs", []) - -deadline = time.time() + 900 -seen_run_id = None - -while time.time() < deadline: - runs = list_runs() - candidates = [r for r in runs if r.get("event") == "workflow_dispatch" and r.get("head_branch") == branch] - candidates.sort(key=lambda r: r.get("id", 0), reverse=True) - - if candidates: - run = candidates[0] - run_id = run.get("id") - status = run.get("status") - conclusion = run.get("conclusion") - html_url = run.get("html_url", "") - - if seen_run_id is None: - seen_run_id = run_id - - if run_id == seen_run_id and status == "completed": - if conclusion == "success": - print(f"Workflow run {run_id} completed successfully: {html_url}") - raise SystemExit(0) - raise SystemExit(f"Workflow run {run_id} failed with conclusion={conclusion}: {html_url}") - - time.sleep(10) - -raise SystemExit("Timed out waiting for dispatched pipeline workflow to complete.") -PY - EOT + command = "python3 ${path.module}/trigger_and_await_forgejo_workflow.py" environment = { - FORGEJO_HOST = data.external.forgejo_env.result.forgejo_host - FORGEJO_API_TOKEN = data.external.forgejo_env.result.forgejo_api_token - FORGEJO_REPOSITORY_OWNER = local.repository_owner - FORGEJO_REPOSITORY_NAME = local.repository_name - FORGEJO_REPOSITORY_DEFAULT_BRANCH = local.repository_default_branch + FORGEJO_REPOSITORY_ID = tostring(var.repository_id) + FORGEJO_WORKFLOW_NAME = "pipeline.yaml" } } } diff --git a/modules/ske/forgejo-connector/buildingblock/get_forgejo_repository_context.py b/modules/ske/forgejo-connector/buildingblock/get_forgejo_repository_context.py new file mode 100644 index 00000000..15ee1c25 --- /dev/null +++ b/modules/ske/forgejo-connector/buildingblock/get_forgejo_repository_context.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +import json +import os +import sys +import urllib.request + + +def normalize_host(raw_host: str) -> str: + host = raw_host.strip() + if not host.startswith(("https://", "http://")): + host = f"https://{host}" + return host.rstrip("/") + + +def main() -> None: + query = json.loads(sys.stdin.read()) + + forgejo_host = normalize_host(os.environ["FORGEJO_HOST"]) + forgejo_api_token = os.environ["FORGEJO_API_TOKEN"] + repository_id = query["FORGEJO_REPOSITORY_ID"] + + req = urllib.request.Request( + f"{forgejo_host}/api/v1/repositories/{repository_id}", + headers={"Authorization": f"token {forgejo_api_token}", "Content-Type": "application/json"}, + method="GET", + ) + with urllib.request.urlopen(req, timeout=30) as resp: + payload = json.loads(resp.read().decode("utf-8")) + + print( + json.dumps( + { + "forgejo_host": forgejo_host, + "forgejo_api_token": forgejo_api_token, + "owner": payload["owner"]["username"], + "name": payload["name"], + "default_branch": payload.get("default_branch", "main"), + } + ) + ) + + +if __name__ == "__main__": + main() diff --git a/modules/ske/forgejo-connector/buildingblock/outputs.tf b/modules/ske/forgejo-connector/buildingblock/outputs.tf index d3c76643..f574ab27 100644 --- a/modules/ske/forgejo-connector/buildingblock/outputs.tf +++ b/modules/ske/forgejo-connector/buildingblock/outputs.tf @@ -1 +1,4 @@ -# TODO: Implement outputs.tf +output "action_variables" { + description = "Action variables to expose for dependent building block wiring." + value = local.action_variables +} diff --git a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py new file mode 100644 index 00000000..108518f2 --- /dev/null +++ b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +import datetime as dt +import json +import os +import re +import time +import urllib.request + + +def normalize_host(raw_host: str) -> str: + host = raw_host.strip() + if not host.startswith(("https://", "http://")): + host = f"https://{host}" + return host.rstrip("/") + + +def request_json(host: str, token: str, method: str, path: str, payload: dict | None = None): + body = None if payload is None else json.dumps(payload).encode("utf-8") + req = urllib.request.Request( + f"{host}{path}", + headers={"Authorization": f"token {token}", "Content-Type": "application/json"}, + method=method, + data=body, + ) + with urllib.request.urlopen(req, timeout=30) as resp: + raw = resp.read().decode("utf-8") + headers = dict(resp.headers.items()) + status = resp.status + data = json.loads(raw) if raw else {} + return status, headers, data + + +def parse_timestamp(ts: str | None): + if not ts: + return None + return dt.datetime.fromisoformat(ts.replace("Z", "+00:00")) + + +def main() -> None: + host = normalize_host(os.environ["FORGEJO_HOST"]) + token = os.environ["FORGEJO_API_TOKEN"] + repository_id = os.environ["FORGEJO_REPOSITORY_ID"] + workflow_name = os.environ.get("FORGEJO_WORKFLOW_NAME", "pipeline.yaml") + + _, _, repo = request_json(host, token, "GET", f"/api/v1/repositories/{repository_id}") + owner = repo["owner"]["username"] + repo_name = repo["name"] + default_branch = repo.get("default_branch", "main") + + dispatch_at = dt.datetime.now(dt.timezone.utc) + status, headers, dispatch_response = request_json( + host, + token, + "POST", + f"/api/v1/repos/{owner}/{repo_name}/actions/workflows/{workflow_name}/dispatches", + {"ref": default_branch}, + ) + if status not in (200, 201, 202, 204): + raise SystemExit(f"Workflow dispatch failed with status {status}") + + expected_run_id = None + expected_run_number = None + + if "id" in dispatch_response: + expected_run_id = int(dispatch_response["id"]) + if "run_id" in dispatch_response: + expected_run_id = int(dispatch_response["run_id"]) + if "run_number" in dispatch_response: + expected_run_number = int(dispatch_response["run_number"]) + + location = headers.get("Location", "") + m = re.search(r"/actions/runs/(\\d+)", location) + if m: + expected_run_id = int(m.group(1)) + + deadline = time.time() + 900 + runs_path = f"/api/v1/repos/{owner}/{repo_name}/actions/runs?limit=30" + + while time.time() < deadline: + _, _, payload = request_json(host, token, "GET", runs_path) + runs = payload.get("workflow_runs", []) + + if expected_run_id is not None: + candidates = [r for r in runs if int(r.get("id", 0)) == expected_run_id] + elif expected_run_number is not None: + candidates = [r for r in runs if int(r.get("run_number", 0)) == expected_run_number] + else: + candidates = [] + for run in runs: + if run.get("event") != "workflow_dispatch": + continue + if run.get("head_branch") != default_branch: + continue + created_at = parse_timestamp(run.get("created_at")) + if created_at and created_at >= dispatch_at - dt.timedelta(seconds=2): + candidates.append(run) + candidates.sort(key=lambda r: int(r.get("id", 0)), reverse=True) + + if candidates: + run = candidates[0] + run_id = run.get("id") + status_val = run.get("status") + conclusion = run.get("conclusion") + html_url = run.get("html_url", "") + + if status_val == "completed": + if conclusion == "success": + print(f"Workflow run {run_id} completed successfully: {html_url}") + return + raise SystemExit(f"Workflow run {run_id} failed with conclusion={conclusion}: {html_url}") + + time.sleep(10) + + raise SystemExit("Timed out waiting for dispatched pipeline workflow to complete.") + + +if __name__ == "__main__": + main() diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 8676a9ba..5d0cab74 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -189,7 +189,14 @@ resource "meshstack_building_block_definition" "this" { } } - outputs = {} + outputs = { + action_variables = { + display_name = "Action Variables" + description = "Non-sensitive action variable map for dependent building block wiring." + type = "CODE" + assignment_type = "NONE" + } + } } } @@ -200,4 +207,4 @@ terraform { version = "~> 0.20.0" } } -} \ No newline at end of file +} From 37b3b6153f724a331648a4cfca3b86d4b64bae1d Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Wed, 18 Mar 2026 11:34:54 +0100 Subject: [PATCH 24/39] feat: ske-starterkit workflow trigger based on stage --- modules/ske/forgejo-connector/buildingblock/forgejo.tf | 6 ++++-- .../buildingblock/trigger_and_await_forgejo_workflow.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index d927f500..eac40071 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -24,6 +24,7 @@ locals { repository_owner = data.external.repository_context.result.owner repository_name = data.external.repository_context.result.name repository_default_branch = data.external.repository_context.result.default_branch + stage = lower(trimprefix(var.repository_secret_name_suffix, "_")) action_secrets = { "KUBECONFIG${var.repository_secret_name_suffix}" = yamlencode(merge(local.kubeconfig, { @@ -87,8 +88,9 @@ resource "terraform_data" "await_pipeline_workflow" { provisioner "local-exec" { command = "python3 ${path.module}/trigger_and_await_forgejo_workflow.py" environment = { - FORGEJO_REPOSITORY_ID = tostring(var.repository_id) - FORGEJO_WORKFLOW_NAME = "pipeline.yaml" + FORGEJO_REPOSITORY_ID = tostring(var.repository_id) + FORGEJO_WORKFLOW_NAME = "pipeline.yaml" + FORGEJO_WORKFLOW_STAGE = local.stage } } } diff --git a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py index 108518f2..e70c0290 100644 --- a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py +++ b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py @@ -42,6 +42,7 @@ def main() -> None: token = os.environ["FORGEJO_API_TOKEN"] repository_id = os.environ["FORGEJO_REPOSITORY_ID"] workflow_name = os.environ.get("FORGEJO_WORKFLOW_NAME", "pipeline.yaml") + stage = os.environ["FORGEJO_WORKFLOW_STAGE"] _, _, repo = request_json(host, token, "GET", f"/api/v1/repositories/{repository_id}") owner = repo["owner"]["username"] @@ -54,7 +55,7 @@ def main() -> None: token, "POST", f"/api/v1/repos/{owner}/{repo_name}/actions/workflows/{workflow_name}/dispatches", - {"ref": default_branch}, + {"ref": default_branch, "inputs": {"stage": stage}}, ) if status not in (200, 201, 202, 204): raise SystemExit(f"Workflow dispatch failed with status {status}") From 40adab2d12c1846ca61f3e876260317ffaef60ee Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 13:38:39 +0100 Subject: [PATCH 25/39] feat(ske): pass only_stage to forgejo workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- modules/ske/forgejo-connector/buildingblock/forgejo.tf | 8 ++++---- .../buildingblock/trigger_and_await_forgejo_workflow.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) mode change 100644 => 100755 modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index eac40071..f9be1c21 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -86,11 +86,11 @@ resource "terraform_data" "await_pipeline_workflow" { ] provisioner "local-exec" { - command = "python3 ${path.module}/trigger_and_await_forgejo_workflow.py" + command = "${path.module}/trigger_and_await_forgejo_workflow.py" environment = { - FORGEJO_REPOSITORY_ID = tostring(var.repository_id) - FORGEJO_WORKFLOW_NAME = "pipeline.yaml" - FORGEJO_WORKFLOW_STAGE = local.stage + FORGEJO_REPOSITORY_ID = tostring(var.repository_id) + FORGEJO_WORKFLOW_NAME = "pipeline.yaml" + FORGEJO_WORKFLOW_ONLY_STAGE = local.stage } } } diff --git a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py old mode 100644 new mode 100755 index e70c0290..999e45c8 --- a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py +++ b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py @@ -42,7 +42,7 @@ def main() -> None: token = os.environ["FORGEJO_API_TOKEN"] repository_id = os.environ["FORGEJO_REPOSITORY_ID"] workflow_name = os.environ.get("FORGEJO_WORKFLOW_NAME", "pipeline.yaml") - stage = os.environ["FORGEJO_WORKFLOW_STAGE"] + only_stage = os.environ["FORGEJO_WORKFLOW_ONLY_STAGE"] _, _, repo = request_json(host, token, "GET", f"/api/v1/repositories/{repository_id}") owner = repo["owner"]["username"] @@ -55,7 +55,7 @@ def main() -> None: token, "POST", f"/api/v1/repos/{owner}/{repo_name}/actions/workflows/{workflow_name}/dispatches", - {"ref": default_branch, "inputs": {"stage": stage}}, + {"ref": default_branch, "inputs": {"only_stage": only_stage}}, ) if status not in (200, 201, 202, 204): raise SystemExit(f"Workflow dispatch failed with status {status}") From c48db02e0a283cc27215df72cb8002c5bfd98e99 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 15:14:00 +0100 Subject: [PATCH 26/39] feat(ske): generalize connector secrets and stage input Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../forgejo-connector/buildingblock/README.md | 4 ++- .../buildingblock/forgejo.tf | 6 ++-- .../buildingblock/kubernetes.tf | 13 +++++++ .../buildingblock/variables.tf | 18 +++++++--- .../meshstack_integration.tf | 34 ++++++++++++++++--- .../ske/ske-starterkit/buildingblock/main.tf | 4 +-- 6 files changed, 65 insertions(+), 14 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index c6c6d718..8c3f7951 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -62,6 +62,7 @@ No modules. | [kubernetes_cluster_role_binding.forgejo_actions_clusterissuer_access](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/cluster_role_binding) | resource | | [kubernetes_default_service_account.namespace_default](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/default_service_account) | resource | | [kubernetes_role_binding.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/role_binding) | resource | +| [kubernetes_secret.additional](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_secret.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_secret.image_pull](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_service_account.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/service_account) | resource | @@ -74,12 +75,13 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [additional\_kubernetes\_secrets](#input\_additional\_kubernetes\_secrets) | Additional Kubernetes secrets to create in the tenant namespace. Map keys are secret names, values are secret data maps. | `map(map(string))` | `{}` | no | | [harbor\_host](#input\_harbor\_host) | The URL of the Harbor registry. | `string` | `"https://registry.onstackit.cloud"` | no | | [harbor\_password](#input\_harbor\_password) | The password for the Harbor registry. | `string` | n/a | yes | | [harbor\_username](#input\_harbor\_username) | The username for the Harbor registry. | `string` | n/a | yes | | [namespace](#input\_namespace) | Associated namespace in kubernetes cluster. | `string` | n/a | yes | | [repository\_id](#input\_repository\_id) | The ID of the Forgejo repository. | `number` | n/a | yes | -| [repository\_secret\_name\_suffix](#input\_repository\_secret\_name\_suffix) | Optional suffix appended to created repository secret names. | `string` | `""` | no | +| [stage](#input\_stage) | Deployment stage used for Forgejo workflow dispatch and action secret naming. Allowed values: dev, prod. | `string` | n/a | yes | ## Outputs diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index f9be1c21..64305867 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -24,10 +24,10 @@ locals { repository_owner = data.external.repository_context.result.owner repository_name = data.external.repository_context.result.name repository_default_branch = data.external.repository_context.result.default_branch - stage = lower(trimprefix(var.repository_secret_name_suffix, "_")) + stage = lower(var.stage) action_secrets = { - "KUBECONFIG${var.repository_secret_name_suffix}" = yamlencode(merge(local.kubeconfig, { + "KUBECONFIG_${upper(local.stage)}" = yamlencode(merge(local.kubeconfig, { current-context = local.kubeconfig_cluster_name users = [ @@ -53,7 +53,7 @@ locals { } action_variables = { - "K8S_NAMESPACE${var.repository_secret_name_suffix}" = var.namespace + "K8S_NAMESPACE_${upper(local.stage)}" = var.namespace } } diff --git a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf index 4cd51ea4..0627dfaf 100644 --- a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf +++ b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf @@ -121,6 +121,19 @@ resource "kubernetes_secret" "image_pull" { } } +resource "kubernetes_secret" "additional" { + for_each = var.additional_kubernetes_secrets + + metadata { + name = each.key + namespace = var.namespace + } + + type = "Opaque" + + data = each.value +} + resource "kubernetes_default_service_account" "namespace_default" { metadata { namespace = var.namespace diff --git a/modules/ske/forgejo-connector/buildingblock/variables.tf b/modules/ske/forgejo-connector/buildingblock/variables.tf index c01cec21..31fd5e2b 100644 --- a/modules/ske/forgejo-connector/buildingblock/variables.tf +++ b/modules/ske/forgejo-connector/buildingblock/variables.tf @@ -8,10 +8,20 @@ variable "repository_id" { description = "The ID of the Forgejo repository." } -variable "repository_secret_name_suffix" { +variable "stage" { type = string - description = "Optional suffix appended to created repository secret names." - default = "" + description = "Deployment stage used for Forgejo workflow dispatch and action secret naming. Allowed values: dev, prod." + + validation { + condition = contains(["dev", "prod"], lower(var.stage)) + error_message = "stage must be one of: dev, prod." + } +} + +variable "additional_kubernetes_secrets" { + type = map(map(string)) + description = "Additional Kubernetes secrets to create in the tenant namespace. Map keys are secret names, values are secret data maps." + default = {} } variable "harbor_host" { @@ -30,4 +40,4 @@ variable "harbor_password" { type = string description = "The password for the Harbor registry." sensitive = true -} \ No newline at end of file +} diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 5d0cab74..4c1e888b 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -36,6 +36,19 @@ variable "harbor_password" { sensitive = true } +variable "additional_kubernetes_secrets" { + type = map(map(string)) + description = "Additional Kubernetes secrets provisioned in tenant namespaces by the connector." + sensitive = true + default = { + "stackit-ai" = { + STACKIT_AI_BASE_URL = "https://example.invalid/v1" + STACKIT_AI_API_KEY = "dummy-api-key" + STACKIT_AI_MODEL = "dummy-model" + } + } +} + variable "meshstack" { type = object({ owning_workspace_identifier = string @@ -123,12 +136,25 @@ resource "meshstack_building_block_definition" "this" { argument = jsonencode("${var.forgejo_repo_definition_uuid}.repository_id") } - repository_secret_name_suffix = { - display_name = "repository_secret_name_suffix" - description = "Optional suffix appended to created repository secret names (for example `_DEV`)." + stage = { + display_name = "stage" + description = "Deployment stage for this connector instance (dev or prod)." type = "STRING" assignment_type = "USER_INPUT" - default_value = jsonencode("") + default_value = jsonencode("dev") + } + + additional_kubernetes_secrets = { + display_name = "additional_kubernetes_secrets" + description = "Static sensitive map of additional Kubernetes Opaque secrets to create in the tenant namespace." + type = "CODE" + assignment_type = "STATIC" + sensitive = { + argument = { + secret_value = jsonencode(var.additional_kubernetes_secrets) + secret_version = nonsensitive(sha256(jsonencode(var.additional_kubernetes_secrets))) + } + } } FORGEJO_HOST = { diff --git a/modules/ske/ske-starterkit/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf index 3b0549fb..e43c75a9 100644 --- a/modules/ske/ske-starterkit/buildingblock/main.tf +++ b/modules/ske/ske-starterkit/buildingblock/main.tf @@ -85,8 +85,8 @@ resource "meshstack_building_block_v2" "forgejo_connector" { }] inputs = { - repository_secret_name_suffix = { - value_string = "_${upper(each.key)}" + stage = { + value_string = each.key } } } From 3b73eb3a56aae8d7879baeedcb22a32e6606c8c3 Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Wed, 18 Mar 2026 16:13:48 +0100 Subject: [PATCH 27/39] chore: change restapi object in forgejo connector --- modules/ske/forgejo-connector/buildingblock/forgejo.tf | 3 ++- .../buildingblock/trigger_and_await_forgejo_workflow.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index 64305867..18cac09c 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -69,9 +69,10 @@ resource "restapi_object" "action_variables" { for_each = local.action_variables path = "/api/v1/repos/${local.repository_owner}/${local.repository_name}/actions/variables" + create_path = "/api/v1/repos/${local.repository_owner}/${local.repository_name}/actions/variables/${each.key}" id_attribute = "name" object_id = each.key - update_method = "PATCH" + update_method = "PUT" data = jsonencode({ name = each.key value = each.value diff --git a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py index 999e45c8..d9aaee47 100755 --- a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py +++ b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py @@ -76,7 +76,7 @@ def main() -> None: expected_run_id = int(m.group(1)) deadline = time.time() + 900 - runs_path = f"/api/v1/repos/{owner}/{repo_name}/actions/runs?limit=30" + runs_path = f"/api/v1/repos/{owner}/{repo_name}/actions/workflows/{workflow_name}/runs?limit=30" while time.time() < deadline: _, _, payload = request_json(host, token, "GET", runs_path) From 66a179eb5e43e17133eaeeaa4e39d802d8858fa6 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 20:14:50 +0100 Subject: [PATCH 28/39] feat(ske): align connector and CODE input wiring Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../forgejo-connector/buildingblock/README.md | 4 +--- .../forgejo-connector/buildingblock/forgejo.tf | 7 +++---- .../ske/forgejo-connector/buildingblock/main.tf | 1 - .../forgejo-connector/buildingblock/outputs.tf | 4 ---- .../forgejo-connector/meshstack_integration.tf | 9 --------- .../git-repository/buildingblock/variables.tf | 14 +++++++------- .../git-repository/meshstack_integration.tf | 17 ++++++++++------- 7 files changed, 21 insertions(+), 35 deletions(-) delete mode 100644 modules/ske/forgejo-connector/buildingblock/main.tf delete mode 100644 modules/ske/forgejo-connector/buildingblock/outputs.tf diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index 8c3f7951..34a98e11 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -85,7 +85,5 @@ No modules. ## Outputs -| Name | Description | -|------|-------------| -| [action\_variables](#output\_action\_variables) | Action variables to expose for dependent building block wiring. | +No outputs. diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index 18cac09c..5d0ed26b 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -21,10 +21,9 @@ data "external" "repository_context" { } locals { - repository_owner = data.external.repository_context.result.owner - repository_name = data.external.repository_context.result.name - repository_default_branch = data.external.repository_context.result.default_branch - stage = lower(var.stage) + repository_owner = data.external.repository_context.result.owner + repository_name = data.external.repository_context.result.name + stage = lower(var.stage) action_secrets = { "KUBECONFIG_${upper(local.stage)}" = yamlencode(merge(local.kubeconfig, { diff --git a/modules/ske/forgejo-connector/buildingblock/main.tf b/modules/ske/forgejo-connector/buildingblock/main.tf deleted file mode 100644 index bc63beba..00000000 --- a/modules/ske/forgejo-connector/buildingblock/main.tf +++ /dev/null @@ -1 +0,0 @@ -# empty \ No newline at end of file diff --git a/modules/ske/forgejo-connector/buildingblock/outputs.tf b/modules/ske/forgejo-connector/buildingblock/outputs.tf deleted file mode 100644 index f574ab27..00000000 --- a/modules/ske/forgejo-connector/buildingblock/outputs.tf +++ /dev/null @@ -1,4 +0,0 @@ -output "action_variables" { - description = "Action variables to expose for dependent building block wiring." - value = local.action_variables -} diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 4c1e888b..7d59ac8d 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -214,15 +214,6 @@ resource "meshstack_building_block_definition" "this" { } } } - - outputs = { - action_variables = { - display_name = "Action Variables" - description = "Non-sensitive action variable map for dependent building block wiring." - type = "CODE" - assignment_type = "NONE" - } - } } } diff --git a/modules/stackit/git-repository/buildingblock/variables.tf b/modules/stackit/git-repository/buildingblock/variables.tf index effbd0d2..e909783e 100644 --- a/modules/stackit/git-repository/buildingblock/variables.tf +++ b/modules/stackit/git-repository/buildingblock/variables.tf @@ -5,6 +5,12 @@ variable "forgejo_organization" { description = "STACKIT Git organization where the repository will be created" } +variable "action_variables" { + type = map(string) + description = "Map of Forgejo Actions variables to create in the repository." + default = {} +} + variable "action_secrets" { type = map(string) description = "Map of Forgejo Actions secrets to create in the repository." @@ -12,17 +18,11 @@ variable "action_secrets" { default = {} validation { - condition = alltrue([for key in keys(var.action_secrets) : length(key) <= 30]) + condition = alltrue([for key in keys(var.action_secrets) : (length(key) <= 30)]) error_message = "Forgejo Actions secret names must be 30 characters or less." } } -variable "action_variables" { - type = map(string) - description = "Map of Forgejo Actions variables to create in the repository." - default = {} -} - # ── User inputs (set per building block instance) ───────────────────────────── variable "name" { diff --git a/modules/stackit/git-repository/meshstack_integration.tf b/modules/stackit/git-repository/meshstack_integration.tf index 78735bbb..2435132b 100644 --- a/modules/stackit/git-repository/meshstack_integration.tf +++ b/modules/stackit/git-repository/meshstack_integration.tf @@ -163,6 +163,16 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "USER_INPUT" default_value = jsonencode("null") } + + action_variables = { + display_name = "Repository Action Variables" + description = "Static non-sensitive map of Forgejo Actions variables created in each provisioned repository." + type = "CODE" + assignment_type = "STATIC" + # jsonencode twice is correct, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 + argument = jsonencode(jsonencode(var.action_variables)) + } + action_secrets = { display_name = "Repository Action Secrets" description = "Static sensitive map of Forgejo Actions secrets created in each provisioned repository." @@ -175,13 +185,6 @@ resource "meshstack_building_block_definition" "this" { } } } - action_variables = { - display_name = "Repository Action Variables" - description = "Static non-sensitive map of Forgejo Actions variables created in each provisioned repository." - type = "CODE" - assignment_type = "STATIC" - argument = jsonencode(var.action_variables) - } } outputs = { From 3772424720cb69901134b8bd4d1fd5f19ffc250a Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 20:36:58 +0100 Subject: [PATCH 29/39] fix(ske): align forgejo variable API usage and naming Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../forgejo-connector/buildingblock/README.md | 4 +-- .../buildingblock/forgejo.tf | 30 ++++++++++++------- .../ske/ske-starterkit/buildingblock/main.tf | 4 +-- .../git-repository/buildingblock/README.md | 6 ++-- .../git-repository/buildingblock/main.tf | 28 ++++++++++++----- .../git-repository/buildingblock/outputs.tf | 12 ++++---- 6 files changed, 52 insertions(+), 32 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index 34a98e11..fe62dd62 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -57,7 +57,7 @@ No modules. | Name | Type | |------|------| -| [forgejo_repository_action_secret.action_secrets](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | +| [forgejo_repository_action_secret.this](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | | [kubernetes_cluster_role.clusterissuer_reader](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/cluster_role) | resource | | [kubernetes_cluster_role_binding.forgejo_actions_clusterissuer_access](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/cluster_role_binding) | resource | | [kubernetes_default_service_account.namespace_default](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/default_service_account) | resource | @@ -67,7 +67,7 @@ No modules. | [kubernetes_secret.image_pull](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_service_account.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/service_account) | resource | | [random_string.clusterissuer_reader_name_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | -| [restapi_object.action_variables](https://registry.terraform.io/providers/Mastercard/restapi/3.0.0/docs/resources/object) | resource | +| [restapi_object.action_variable](https://registry.terraform.io/providers/Mastercard/restapi/3.0.0/docs/resources/object) | resource | | [terraform_data.await_pipeline_workflow](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [external_external.repository_context](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index 5d0ed26b..4b3d61c1 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -25,7 +25,7 @@ locals { repository_name = data.external.repository_context.result.name stage = lower(var.stage) - action_secrets = { + action_secret = { "KUBECONFIG_${upper(local.stage)}" = yamlencode(merge(local.kubeconfig, { current-context = local.kubeconfig_cluster_name @@ -51,29 +51,27 @@ locals { })) } - action_variables = { + action_variable = { "K8S_NAMESPACE_${upper(local.stage)}" = var.namespace } } -resource "forgejo_repository_action_secret" "action_secrets" { - for_each = local.action_secrets +resource "forgejo_repository_action_secret" "this" { + for_each = local.action_secret repository_id = var.repository_id name = each.key data = each.value } -resource "restapi_object" "action_variables" { - for_each = local.action_variables +resource "restapi_object" "action_variable" { + for_each = local.action_variable - path = "/api/v1/repos/${local.repository_owner}/${local.repository_name}/actions/variables" - create_path = "/api/v1/repos/${local.repository_owner}/${local.repository_name}/actions/variables/${each.key}" + path = "/api/v1/repos/${local.repository_owner}/${local.repository_name}/actions/variables/${each.key}" id_attribute = "name" object_id = each.key update_method = "PUT" data = jsonencode({ - name = each.key value = each.value }) ignore_server_additions = true @@ -81,8 +79,8 @@ resource "restapi_object" "action_variables" { resource "terraform_data" "await_pipeline_workflow" { depends_on = [ - forgejo_repository_action_secret.action_secrets, - restapi_object.action_variables, + forgejo_repository_action_secret.this, + restapi_object.action_variable, ] provisioner "local-exec" { @@ -94,3 +92,13 @@ resource "terraform_data" "await_pipeline_workflow" { } } } + +moved { + from = forgejo_repository_action_secret.action_secrets + to = forgejo_repository_action_secret.this +} + +moved { + from = restapi_object.action_variables + to = restapi_object.action_variable +} diff --git a/modules/ske/ske-starterkit/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf index e43c75a9..30bc4822 100644 --- a/modules/ske/ske-starterkit/buildingblock/main.tf +++ b/modules/ske/ske-starterkit/buildingblock/main.tf @@ -85,9 +85,7 @@ resource "meshstack_building_block_v2" "forgejo_connector" { }] inputs = { - stage = { - value_string = each.key - } + stage = { value_string = each.key } } } diff --git a/modules/stackit/git-repository/buildingblock/README.md b/modules/stackit/git-repository/buildingblock/README.md index 329cdf5b..0cb4f7a7 100644 --- a/modules/stackit/git-repository/buildingblock/README.md +++ b/modules/stackit/git-repository/buildingblock/README.md @@ -23,9 +23,9 @@ No modules. | Name | Type | |------|------| -| [forgejo_repository.repository](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository) | resource | -| [forgejo_repository_action_secret.action_secrets](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | -| [restapi_object.action_variables](https://registry.terraform.io/providers/Mastercard/restapi/3.0.0/docs/resources/object) | resource | +| [forgejo_repository.this](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository) | resource | +| [forgejo_repository_action_secret.this](https://registry.terraform.io/providers/svalabs/forgejo/latest/docs/resources/repository_action_secret) | resource | +| [restapi_object.action_variable](https://registry.terraform.io/providers/Mastercard/restapi/3.0.0/docs/resources/object) | resource | | [external_external.forgejo_env](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | ## Inputs diff --git a/modules/stackit/git-repository/buildingblock/main.tf b/modules/stackit/git-repository/buildingblock/main.tf index 23a060ae..277ca1b2 100644 --- a/modules/stackit/git-repository/buildingblock/main.tf +++ b/modules/stackit/git-repository/buildingblock/main.tf @@ -29,7 +29,7 @@ locals { have_clone_addr = trimspace(var.clone_addr) != "" && var.clone_addr != "null" } -resource "forgejo_repository" "repository" { +resource "forgejo_repository" "this" { owner = var.forgejo_organization name = var.name description = var.description @@ -42,24 +42,38 @@ resource "forgejo_repository" "repository" { mirror = false } -resource "forgejo_repository_action_secret" "action_secrets" { +resource "forgejo_repository_action_secret" "this" { for_each = var.action_secrets - repository_id = forgejo_repository.repository.id + repository_id = forgejo_repository.this.id name = each.key data = each.value } -resource "restapi_object" "action_variables" { +resource "restapi_object" "action_variable" { for_each = var.action_variables - path = "/api/v1/repos/${var.forgejo_organization}/${forgejo_repository.repository.name}/actions/variables" + path = "/api/v1/repos/${var.forgejo_organization}/${forgejo_repository.this.name}/actions/variables/${each.key}" id_attribute = "name" object_id = each.key - update_method = "PATCH" + update_method = "PUT" data = jsonencode({ - name = each.key value = each.value }) ignore_server_additions = true } + +moved { + from = forgejo_repository.repository + to = forgejo_repository.this +} + +moved { + from = forgejo_repository_action_secret.action_secrets + to = forgejo_repository_action_secret.this +} + +moved { + from = restapi_object.action_variables + to = restapi_object.action_variable +} diff --git a/modules/stackit/git-repository/buildingblock/outputs.tf b/modules/stackit/git-repository/buildingblock/outputs.tf index 3e87adc2..b0ec3c3f 100644 --- a/modules/stackit/git-repository/buildingblock/outputs.tf +++ b/modules/stackit/git-repository/buildingblock/outputs.tf @@ -1,5 +1,5 @@ output "repository_id" { - value = forgejo_repository.repository.id + value = forgejo_repository.this.id description = "The ID of the created repository" } @@ -9,17 +9,17 @@ output "repository_name" { } output "repository_html_url" { - value = forgejo_repository.repository.html_url + value = forgejo_repository.this.html_url description = "Web URL of the repository" } output "repository_ssh_url" { - value = forgejo_repository.repository.ssh_url + value = forgejo_repository.this.ssh_url description = "SSH clone URL" } output "repository_clone_url" { - value = forgejo_repository.repository.clone_url + value = forgejo_repository.this.clone_url description = "HTTPS clone URL" } @@ -28,8 +28,8 @@ output "summary" { value = templatefile("${path.module}/SUMMARY.md.tftpl", { name = var.name owner = var.forgejo_organization - repo_html_url = forgejo_repository.repository.html_url - repo_clone_url = forgejo_repository.repository.clone_url + repo_html_url = forgejo_repository.this.html_url + repo_clone_url = forgejo_repository.this.clone_url clone_addr = var.clone_addr default_branch = var.default_branch }) From 28ef849e36c7d71230acabd4bbd8d2ecb0ded1d7 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 20:45:48 +0100 Subject: [PATCH 30/39] fix: simpler Connector name --- modules/ske/ske-starterkit/buildingblock/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ske/ske-starterkit/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf index 30bc4822..b9859117 100644 --- a/modules/ske/ske-starterkit/buildingblock/main.tf +++ b/modules/ske/ske-starterkit/buildingblock/main.tf @@ -73,7 +73,7 @@ resource "meshstack_building_block_v2" "forgejo_connector" { spec = { building_block_definition_version_ref = var.building_block_definitions["forgejo-connector"].version_ref - display_name = "${var.name} Forgejo Connector ${title(each.key)}" + display_name = "Forgejo Connector" target_ref = { kind = "meshTenant" uuid = each.value.metadata.uuid From 81eadb9316f9a78d0551e1ffff61a19509803dcd Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 21:03:50 +0100 Subject: [PATCH 31/39] fix(ske): harden forgejo workflow polling compatibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../trigger_and_await_forgejo_workflow.py | 34 +++++++++++++++++-- .../ske/ske-starterkit/buildingblock/main.tf | 2 +- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py index d9aaee47..c6c4e9b8 100755 --- a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py +++ b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py @@ -5,6 +5,7 @@ import os import re import time +import urllib.error import urllib.request @@ -71,12 +72,35 @@ def main() -> None: expected_run_number = int(dispatch_response["run_number"]) location = headers.get("Location", "") - m = re.search(r"/actions/runs/(\\d+)", location) + m = re.search(r"/actions/runs/(\d+)", location) if m: expected_run_id = int(m.group(1)) deadline = time.time() + 900 - runs_path = f"/api/v1/repos/{owner}/{repo_name}/actions/workflows/{workflow_name}/runs?limit=30" + # Forgejo API compatibility: some Forgejo installations expose action run listing via + # /actions/tasks instead of /actions/workflows/{workflow}/runs. This is Forgejo + # version/variant behavior (not intentional legacy Gitea support), so probe both. + run_list_candidates = [ + f"/api/v1/repos/{owner}/{repo_name}/actions/workflows/{workflow_name}/runs?limit=30", + f"/api/v1/repos/{owner}/{repo_name}/actions/tasks?limit=30", + ] + runs_path = None + + for candidate in run_list_candidates: + try: + request_json(host, token, "GET", candidate) + runs_path = candidate + break + except urllib.error.HTTPError as exc: + if exc.code == 404: + continue + raise + + if runs_path is None: + raise SystemExit( + "Could not find a supported Forgejo workflow runs endpoint. " + "Tried: " + ", ".join(run_list_candidates) + ) while time.time() < deadline: _, _, payload = request_json(host, token, "GET", runs_path) @@ -103,7 +127,11 @@ def main() -> None: run_id = run.get("id") status_val = run.get("status") conclusion = run.get("conclusion") - html_url = run.get("html_url", "") + html_url = run.get("html_url", run.get("url", "")) + + if conclusion is None and status_val in {"success", "failure", "cancelled", "skipped"}: + conclusion = status_val + status_val = "completed" if status_val == "completed": if conclusion == "success": diff --git a/modules/ske/ske-starterkit/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf index b9859117..35ef4451 100644 --- a/modules/ske/ske-starterkit/buildingblock/main.tf +++ b/modules/ske/ske-starterkit/buildingblock/main.tf @@ -73,7 +73,7 @@ resource "meshstack_building_block_v2" "forgejo_connector" { spec = { building_block_definition_version_ref = var.building_block_definitions["forgejo-connector"].version_ref - display_name = "Forgejo Connector" + display_name = "Forgejo Connector ${title(each.key)}" target_ref = { kind = "meshTenant" uuid = each.value.metadata.uuid From d2cde62b4a378d214359b8a0027cddabd9ea028b Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 21:39:33 +0100 Subject: [PATCH 32/39] harden workflow trigger script with parallel-dispatch safety - Add EXPECTED_WORKFLOW_TASK_NAME to disambiguate concurrent dispatches - Await all composition jobs (build_image, deploy_dev, deploy_prod) via /actions/tasks endpoint with per-job status logging - Add triggers_replace so terraform_data re-runs on script/secret/variable changes - Replace local.stage with var.stage, validate with [a-z]+ regex - Clean up env var names: drop FORGEJO_ prefix for resource-provided vars - Add comprehensive docstring documenting API compat and dispatch strategy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../forgejo-connector/buildingblock/README.md | 2 +- .../buildingblock/forgejo.tf | 18 +- .../trigger_and_await_forgejo_workflow.py | 193 +++++++++++++++--- .../buildingblock/variables.tf | 6 +- 4 files changed, 186 insertions(+), 33 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index fe62dd62..307c12d4 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -81,7 +81,7 @@ No modules. | [harbor\_username](#input\_harbor\_username) | The username for the Harbor registry. | `string` | n/a | yes | | [namespace](#input\_namespace) | Associated namespace in kubernetes cluster. | `string` | n/a | yes | | [repository\_id](#input\_repository\_id) | The ID of the Forgejo repository. | `number` | n/a | yes | -| [stage](#input\_stage) | Deployment stage used for Forgejo workflow dispatch and action secret naming. Allowed values: dev, prod. | `string` | n/a | yes | +| [stage](#input\_stage) | Deployment stage used for Forgejo workflow dispatch and action secret naming. | `string` | n/a | yes | ## Outputs diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index 4b3d61c1..f3d27457 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -23,10 +23,9 @@ data "external" "repository_context" { locals { repository_owner = data.external.repository_context.result.owner repository_name = data.external.repository_context.result.name - stage = lower(var.stage) action_secret = { - "KUBECONFIG_${upper(local.stage)}" = yamlencode(merge(local.kubeconfig, { + "KUBECONFIG_${upper(var.stage)}" = yamlencode(merge(local.kubeconfig, { current-context = local.kubeconfig_cluster_name users = [ @@ -52,7 +51,7 @@ locals { } action_variable = { - "K8S_NAMESPACE_${upper(local.stage)}" = var.namespace + "K8S_NAMESPACE_${upper(var.stage)}" = var.namespace } } @@ -83,12 +82,19 @@ resource "terraform_data" "await_pipeline_workflow" { restapi_object.action_variable, ] + triggers_replace = [ + sha256(file("${path.module}/trigger_and_await_forgejo_workflow.py")), + nonsensitive(sha256(jsonencode(local.action_secret))), + sha256(jsonencode(local.action_variable)), + ] + provisioner "local-exec" { command = "${path.module}/trigger_and_await_forgejo_workflow.py" environment = { - FORGEJO_REPOSITORY_ID = tostring(var.repository_id) - FORGEJO_WORKFLOW_NAME = "pipeline.yaml" - FORGEJO_WORKFLOW_ONLY_STAGE = local.stage + REPOSITORY_ID = tostring(var.repository_id) + WORKFLOW_NAME = "pipeline.yaml" + WORKFLOW_ONLY_STAGE = var.stage + EXPECTED_WORKFLOW_TASK_NAME = "deploy_${var.stage}" } } } diff --git a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py index c6c4e9b8..83cfbad5 100755 --- a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py +++ b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py @@ -1,4 +1,29 @@ #!/usr/bin/env python3 +"""Dispatch a Forgejo workflow and wait for completion. + +Triggers a workflow_dispatch event and polls until all jobs reach a terminal +state (success, failure, cancelled, skipped). + +Parallel-dispatch safety: Forgejo returns HTTP 204 with no body on dispatch, +so there is no run ID to correlate. Instead, we use a timestamp coincidence +window (2 s) combined with EXPECTED_WORKFLOW_TASK_NAME to identify the correct +run among concurrent dispatches (e.g. stage=dev vs stage=prod). + +API compatibility: Forgejo 8.x does not expose /actions/workflows/{wf}/runs. +The script probes that endpoint first, then falls back to /actions/tasks which +returns one entry per job. The tasks endpoint also lacks a separate 'conclusion' +field — the 'status' value IS the terminal state. + +Required environment variables: + FORGEJO_HOST – Forgejo instance URL (from provider) + FORGEJO_API_TOKEN – API token with repo action scope (from provider) + REPOSITORY_ID – numeric repository ID + WORKFLOW_ONLY_STAGE – value for the only_stage workflow input + EXPECTED_WORKFLOW_TASK_NAME – job name used to identify the correct run + +Optional: + WORKFLOW_NAME – workflow file name (default: pipeline.yaml) +""" import datetime as dt import json @@ -9,6 +34,13 @@ import urllib.request +def as_int(value, default: int = 0) -> int: + try: + return int(value) + except (TypeError, ValueError): + return default + + def normalize_host(raw_host: str) -> str: host = raw_host.strip() if not host.startswith(("https://", "http://")): @@ -41,9 +73,10 @@ def parse_timestamp(ts: str | None): def main() -> None: host = normalize_host(os.environ["FORGEJO_HOST"]) token = os.environ["FORGEJO_API_TOKEN"] - repository_id = os.environ["FORGEJO_REPOSITORY_ID"] - workflow_name = os.environ.get("FORGEJO_WORKFLOW_NAME", "pipeline.yaml") - only_stage = os.environ["FORGEJO_WORKFLOW_ONLY_STAGE"] + repository_id = os.environ["REPOSITORY_ID"] + workflow_name = os.environ.get("WORKFLOW_NAME", "pipeline.yaml") + only_stage = os.environ["WORKFLOW_ONLY_STAGE"] + expected_task_name = os.environ["EXPECTED_WORKFLOW_TASK_NAME"] _, _, repo = request_json(host, token, "GET", f"/api/v1/repositories/{repository_id}") owner = repo["owner"]["username"] @@ -70,6 +103,11 @@ def main() -> None: expected_run_id = int(dispatch_response["run_id"]) if "run_number" in dispatch_response: expected_run_number = int(dispatch_response["run_number"]) + expected_jobs = dispatch_response.get("jobs", []) + if not isinstance(expected_jobs, list): + expected_jobs = [] + if expected_jobs: + print(f"Dispatched workflow jobs: {', '.join(expected_jobs)}") location = headers.get("Location", "") m = re.search(r"/actions/runs/(\d+)", location) @@ -77,9 +115,6 @@ def main() -> None: expected_run_id = int(m.group(1)) deadline = time.time() + 900 - # Forgejo API compatibility: some Forgejo installations expose action run listing via - # /actions/tasks instead of /actions/workflows/{workflow}/runs. This is Forgejo - # version/variant behavior (not intentional legacy Gitea support), so probe both. run_list_candidates = [ f"/api/v1/repos/{owner}/{repo_name}/actions/workflows/{workflow_name}/runs?limit=30", f"/api/v1/repos/{owner}/{repo_name}/actions/tasks?limit=30", @@ -102,14 +137,58 @@ def main() -> None: "Tried: " + ", ".join(run_list_candidates) ) + uses_tasks_endpoint = "/actions/tasks" in runs_path + print(f"Polling workflow status via {runs_path}") + seen_job_status: dict[str, str] = {} + last_wait_reason: tuple | None = None + last_run_state: tuple | None = None + while time.time() < deadline: _, _, payload = request_json(host, token, "GET", runs_path) runs = payload.get("workflow_runs", []) - if expected_run_id is not None: - candidates = [r for r in runs if int(r.get("id", 0)) == expected_run_id] + if uses_tasks_endpoint: + if expected_run_number is not None: + candidates = [r for r in runs if as_int(r.get("run_number"), -1) == expected_run_number] + else: + candidates = [] + for run in runs: + if run.get("event") != "workflow_dispatch": + continue + if run.get("head_branch") != default_branch: + continue + created_at = parse_timestamp(run.get("created_at")) + if created_at and created_at >= dispatch_at - dt.timedelta(seconds=2): + candidates.append(run) + + # Disambiguate parallel dispatches: group by run_number and pick + # the run whose job list contains the expected task name. + if expected_task_name and candidates: + by_run: dict[int, list[dict]] = {} + for task in candidates: + rn = as_int(task.get("run_number"), -1) + if rn >= 0: + by_run.setdefault(rn, []).append(task) + + matched_run = None + for rn in sorted(by_run, reverse=True): + if any( + str(t.get("name", "")).strip() == expected_task_name + for t in by_run[rn] + ): + matched_run = rn + break + + if matched_run is not None: + candidates = by_run[matched_run] + expected_run_number = matched_run + print(f"Identified run {matched_run} by expected task '{expected_task_name}'") + else: + candidates = [] + elif expected_run_id is not None: + candidates = [r for r in runs if as_int(r.get("id")) == expected_run_id] elif expected_run_number is not None: - candidates = [r for r in runs if int(r.get("run_number", 0)) == expected_run_number] + candidates = [r for r in runs if as_int(r.get("run_number"), -1) == expected_run_number] else: candidates = [] for run in runs: @@ -120,24 +199,92 @@ def main() -> None: created_at = parse_timestamp(run.get("created_at")) if created_at and created_at >= dispatch_at - dt.timedelta(seconds=2): candidates.append(run) - candidates.sort(key=lambda r: int(r.get("id", 0)), reverse=True) + candidates.sort(key=lambda r: as_int(r.get("id")), reverse=True) if candidates: - run = candidates[0] - run_id = run.get("id") - status_val = run.get("status") - conclusion = run.get("conclusion") - html_url = run.get("html_url", run.get("url", "")) - - if conclusion is None and status_val in {"success", "failure", "cancelled", "skipped"}: - conclusion = status_val - status_val = "completed" - - if status_val == "completed": - if conclusion == "success": + if uses_tasks_endpoint: + # /actions/tasks returns one task per job in the workflow. + # Wait for all jobs in the dispatched composition to reach terminal states. + by_job_name: dict[str, dict] = {} + for task in candidates: + task_name = str(task.get("name") or task.get("display_title") or "").strip() + if not task_name: + continue + prev = by_job_name.get(task_name) + if prev is None or as_int(task.get("id")) > as_int(prev.get("id")): + by_job_name[task_name] = task + + if expected_jobs: + missing_jobs = [job for job in expected_jobs if job not in by_job_name] + if missing_jobs: + wait_reason = ("missing", tuple(missing_jobs)) + if wait_reason != last_wait_reason: + print(f"Awaiting jobs to appear: {', '.join(missing_jobs)}") + last_wait_reason = wait_reason + time.sleep(10) + continue + relevant_tasks = [by_job_name[job] for job in expected_jobs] + else: + relevant_tasks = list(by_job_name.values()) + + for task in relevant_tasks: + task_name = str(task.get("name") or task.get("display_title") or "unknown").strip() + task_status = str(task.get("status") or "unknown") + previous_status = seen_job_status.get(task_name) + if previous_status != task_status: + print(f"Job {task_name}: {task_status}") + seen_job_status[task_name] = task_status + + terminal_statuses = {"success", "failure", "cancelled", "skipped"} + non_terminal = [t for t in relevant_tasks if t.get("status") not in terminal_statuses] + if non_terminal: + running = tuple( + f"{str(t.get('name') or t.get('display_title') or 'unknown').strip()}={t.get('status')}" + for t in non_terminal + ) + wait_reason = ("running", running) + if wait_reason != last_wait_reason: + print(f"Awaiting jobs: {', '.join(running)}") + last_wait_reason = wait_reason + time.sleep(10) + continue + + last_wait_reason = None + failed = [ + t + for t in relevant_tasks + if t.get("status") not in {"success", "skipped"} + ] + run_id = expected_run_id if expected_run_id is not None else as_int(relevant_tasks[0].get("id")) + html_url = relevant_tasks[0].get("url", "") + if not failed: print(f"Workflow run {run_id} completed successfully: {html_url}") return - raise SystemExit(f"Workflow run {run_id} failed with conclusion={conclusion}: {html_url}") + + failed_statuses = ", ".join( + f"{t.get('name', t.get('display_title', 'unknown'))}={t.get('status')}" for t in failed + ) + raise SystemExit(f"Workflow run {run_id} failed jobs: {failed_statuses}: {html_url}") + else: + run = candidates[0] + run_id = run.get("id") + status_val = run.get("status") + conclusion = run.get("conclusion") + html_url = run.get("html_url", run.get("url", "")) + run_state = (status_val, conclusion) + if run_state != last_run_state: + print(f"Workflow run {run_id}: status={status_val} conclusion={conclusion}") + last_run_state = run_state + + if conclusion is None and status_val in {"success", "failure", "cancelled", "skipped"}: + conclusion = status_val + status_val = "completed" + + if status_val == "completed": + if conclusion == "success": + print(f"Workflow run {run_id} completed successfully: {html_url}") + return + raise SystemExit(f"Workflow run {run_id} failed with conclusion={conclusion}: {html_url}") time.sleep(10) diff --git a/modules/ske/forgejo-connector/buildingblock/variables.tf b/modules/ske/forgejo-connector/buildingblock/variables.tf index 31fd5e2b..bfef9c4b 100644 --- a/modules/ske/forgejo-connector/buildingblock/variables.tf +++ b/modules/ske/forgejo-connector/buildingblock/variables.tf @@ -10,11 +10,11 @@ variable "repository_id" { variable "stage" { type = string - description = "Deployment stage used for Forgejo workflow dispatch and action secret naming. Allowed values: dev, prod." + description = "Deployment stage used for Forgejo workflow dispatch and action secret naming." validation { - condition = contains(["dev", "prod"], lower(var.stage)) - error_message = "stage must be one of: dev, prod." + condition = can(regex("^[a-z]+$", var.stage)) + error_message = "stage must match ^[a-z]+$." } } From e4e12995bd34033677cb1d32ef12bd2c5f09efd8 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 22:26:04 +0100 Subject: [PATCH 33/39] add stage-specific app hostname to forgejo connector - add app_hostname building block input and terraform variable - publish APP_HOSTNAME_ as forgejo action variable - keep namespace/secret flow unchanged Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- modules/ske/forgejo-connector/buildingblock/README.md | 1 + modules/ske/forgejo-connector/buildingblock/forgejo.tf | 1 + modules/ske/forgejo-connector/buildingblock/variables.tf | 5 +++++ modules/ske/forgejo-connector/meshstack_integration.tf | 7 +++++++ 4 files changed, 14 insertions(+) diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index 307c12d4..216af969 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -76,6 +76,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_kubernetes\_secrets](#input\_additional\_kubernetes\_secrets) | Additional Kubernetes secrets to create in the tenant namespace. Map keys are secret names, values are secret data maps. | `map(map(string))` | `{}` | no | +| [app\_hostname](#input\_app\_hostname) | Public application hostname for this stage (used by deploy workflow and ingress). | `string` | n/a | yes | | [harbor\_host](#input\_harbor\_host) | The URL of the Harbor registry. | `string` | `"https://registry.onstackit.cloud"` | no | | [harbor\_password](#input\_harbor\_password) | The password for the Harbor registry. | `string` | n/a | yes | | [harbor\_username](#input\_harbor\_username) | The username for the Harbor registry. | `string` | n/a | yes | diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index f3d27457..be1be708 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -52,6 +52,7 @@ locals { action_variable = { "K8S_NAMESPACE_${upper(var.stage)}" = var.namespace + "APP_HOSTNAME_${upper(var.stage)}" = var.app_hostname } } diff --git a/modules/ske/forgejo-connector/buildingblock/variables.tf b/modules/ske/forgejo-connector/buildingblock/variables.tf index bfef9c4b..b5532371 100644 --- a/modules/ske/forgejo-connector/buildingblock/variables.tf +++ b/modules/ske/forgejo-connector/buildingblock/variables.tf @@ -18,6 +18,11 @@ variable "stage" { } } +variable "app_hostname" { + type = string + description = "Public application hostname for this stage (used by deploy workflow and ingress)." +} + variable "additional_kubernetes_secrets" { type = map(map(string)) description = "Additional Kubernetes secrets to create in the tenant namespace. Map keys are secret names, values are secret data maps." diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 7d59ac8d..6984e895 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -144,6 +144,13 @@ resource "meshstack_building_block_definition" "this" { default_value = jsonencode("dev") } + app_hostname = { + display_name = "app_hostname" + description = "Public application hostname for this stage (for example 'grubinator2-dev.likvid.stackit.run')." + type = "STRING" + assignment_type = "USER_INPUT" + } + additional_kubernetes_secrets = { display_name = "additional_kubernetes_secrets" description = "Static sensitive map of additional Kubernetes Opaque secrets to create in the tenant namespace." From 6d7bd1fe69cdf156ef3734830b0e42358a26e458 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 22:29:01 +0100 Subject: [PATCH 34/39] propagate stage hostnames through SKE starterkit - add apps_base_domain as starterkit input - compute stage hostnames (prod=name.domain, others=name-stage.domain) - pass app_hostname into forgejo connector instances - expose dev/prod application links as building block outputs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- modules/ske/ske-starterkit/buildingblock/README.md | 6 +++++- modules/ske/ske-starterkit/buildingblock/main.tf | 14 +++++++++++++- .../ske/ske-starterkit/buildingblock/outputs.tf | 11 +++++++++-- .../ske/ske-starterkit/buildingblock/variables.tf | 5 +++++ .../ske/ske-starterkit/meshstack_integration.tf | 11 +++++++++++ 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/modules/ske/ske-starterkit/buildingblock/README.md b/modules/ske/ske-starterkit/buildingblock/README.md index 5b7c851c..531514d8 100644 --- a/modules/ske/ske-starterkit/buildingblock/README.md +++ b/modules/ske/ske-starterkit/buildingblock/README.md @@ -34,6 +34,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [apps\_base\_domain](#input\_apps\_base\_domain) | Base domain used for application ingress hostnames (for example 'likvid.stackit.run'). | `string` | n/a | yes | | [building\_block\_definitions](#input\_building\_block\_definitions) | n/a |
map(object({
uuid = string
version_ref = object({
uuid = string
})
}))
| n/a | yes | | [creator](#input\_creator) | Information about the creator of the resources who will be assigned Project Admin role |
object({
type = string
identifier = string
displayName = string
username = optional(string)
email = optional(string)
euid = optional(string)
})
| n/a | yes | | [full\_platform\_identifier](#input\_full\_platform\_identifier) | Full platform identifier of the SKE platform. | `string` | n/a | yes | @@ -45,5 +46,8 @@ No modules. ## Outputs -No outputs. +| Name | Description | +|------|-------------| +| [dev\_link](#output\_dev\_link) | Public URL for the dev stage application. | +| [prod\_link](#output\_prod\_link) | Public URL for the prod stage application. | \ No newline at end of file diff --git a/modules/ske/ske-starterkit/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf index 35ef4451..c09018cc 100644 --- a/modules/ske/ske-starterkit/buildingblock/main.tf +++ b/modules/ske/ske-starterkit/buildingblock/main.tf @@ -1,3 +1,14 @@ +locals { + app_hostnames = { + for stage, _ in var.landing_zone_identifiers : + stage => ( + stage == "prod" + ? "${var.name}.${var.apps_base_domain}" + : "${var.name}-${stage}.${var.apps_base_domain}" + ) + } +} + resource "meshstack_building_block_v2" "git_repository" { spec = { building_block_definition_version_ref = var.building_block_definitions["git-repository"].version_ref # provisioned in backplane @@ -85,7 +96,8 @@ resource "meshstack_building_block_v2" "forgejo_connector" { }] inputs = { - stage = { value_string = each.key } + stage = { value_string = each.key } + app_hostname = { value_string = local.app_hostnames[each.key] } } } diff --git a/modules/ske/ske-starterkit/buildingblock/outputs.tf b/modules/ske/ske-starterkit/buildingblock/outputs.tf index a912bb20..81f12d28 100644 --- a/modules/ske/ske-starterkit/buildingblock/outputs.tf +++ b/modules/ske/ske-starterkit/buildingblock/outputs.tf @@ -1,2 +1,9 @@ -# Building Block composition does not have outputs -# Keep file to please CI validation +output "dev_link" { + value = "https://${var.name}-dev.${var.apps_base_domain}" + description = "Public URL for the dev stage application." +} + +output "prod_link" { + value = "https://${var.name}.${var.apps_base_domain}" + description = "Public URL for the prod stage application." +} diff --git a/modules/ske/ske-starterkit/buildingblock/variables.tf b/modules/ske/ske-starterkit/buildingblock/variables.tf index 2f8e6372..676a7a8b 100644 --- a/modules/ske/ske-starterkit/buildingblock/variables.tf +++ b/modules/ske/ske-starterkit/buildingblock/variables.tf @@ -47,6 +47,11 @@ variable "repo_clone_addr" { description = "URL to clone into the starterkit git repository." } +variable "apps_base_domain" { + type = string + description = "Base domain used for application ingress hostnames (for example 'likvid.stackit.run')." +} + variable "building_block_definitions" { type = map(object({ uuid = string diff --git a/modules/ske/ske-starterkit/meshstack_integration.tf b/modules/ske/ske-starterkit/meshstack_integration.tf index 58556514..da5825a2 100644 --- a/modules/ske/ske-starterkit/meshstack_integration.tf +++ b/modules/ske/ske-starterkit/meshstack_integration.tf @@ -32,6 +32,11 @@ variable "repo_clone_addr" { description = "URL to clone into the starterkit git repository." } +variable "apps_base_domain" { + type = string + description = "Base domain used for application ingress hostnames." +} + variable "tags" { type = map(list(string)) default = {} @@ -184,6 +189,12 @@ EOT display_name = "Clone from URL" argument = jsonencode(var.repo_clone_addr) } + "apps_base_domain" = { + assignment_type = "STATIC" + type = "STRING" + display_name = "Apps Base Domain" + argument = jsonencode(var.apps_base_domain) + } "building_block_definitions" = { assignment_type = "STATIC" type = "CODE" From 01d64b58c2d6cee7963244d95efd2cf9a646ca0e Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 23:08:44 +0100 Subject: [PATCH 35/39] pass workflow_run_title dispatch input from connector trigger - add WORKFLOW_RUN_TITLE env in forgejo connector trigger - send workflow_run_title as dispatch input in trigger script - fallback to dispatch without title input on 400/422 schema rejection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../buildingblock/forgejo.tf | 1 + .../trigger_and_await_forgejo_workflow.py | 37 +++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/forgejo.tf b/modules/ske/forgejo-connector/buildingblock/forgejo.tf index be1be708..06f2f2e2 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -96,6 +96,7 @@ resource "terraform_data" "await_pipeline_workflow" { WORKFLOW_NAME = "pipeline.yaml" WORKFLOW_ONLY_STAGE = var.stage EXPECTED_WORKFLOW_TASK_NAME = "deploy_${var.stage}" + WORKFLOW_RUN_TITLE = "Triggered by meshStack Forgejo Connector ${title(var.stage)}" } } } diff --git a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py index 83cfbad5..3e6e70f9 100755 --- a/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py +++ b/modules/ske/forgejo-connector/buildingblock/trigger_and_await_forgejo_workflow.py @@ -23,6 +23,7 @@ Optional: WORKFLOW_NAME – workflow file name (default: pipeline.yaml) + WORKFLOW_RUN_TITLE – optional workflow_run_title dispatch input """ import datetime as dt @@ -77,20 +78,42 @@ def main() -> None: workflow_name = os.environ.get("WORKFLOW_NAME", "pipeline.yaml") only_stage = os.environ["WORKFLOW_ONLY_STAGE"] expected_task_name = os.environ["EXPECTED_WORKFLOW_TASK_NAME"] + workflow_run_title = os.environ.get("WORKFLOW_RUN_TITLE", "").strip() _, _, repo = request_json(host, token, "GET", f"/api/v1/repositories/{repository_id}") owner = repo["owner"]["username"] repo_name = repo["name"] default_branch = repo.get("default_branch", "main") + dispatch_inputs = {"only_stage": only_stage} + if workflow_run_title: + dispatch_inputs["workflow_run_title"] = workflow_run_title + dispatch_at = dt.datetime.now(dt.timezone.utc) - status, headers, dispatch_response = request_json( - host, - token, - "POST", - f"/api/v1/repos/{owner}/{repo_name}/actions/workflows/{workflow_name}/dispatches", - {"ref": default_branch, "inputs": {"only_stage": only_stage}}, - ) + dispatch_path = f"/api/v1/repos/{owner}/{repo_name}/actions/workflows/{workflow_name}/dispatches" + try: + status, headers, dispatch_response = request_json( + host, + token, + "POST", + dispatch_path, + {"ref": default_branch, "inputs": dispatch_inputs}, + ) + except urllib.error.HTTPError as exc: + if workflow_run_title and exc.code in {400, 422}: + print( + "Dispatch input workflow_run_title was rejected by workflow schema. " + "Retrying without workflow_run_title." + ) + status, headers, dispatch_response = request_json( + host, + token, + "POST", + dispatch_path, + {"ref": default_branch, "inputs": {"only_stage": only_stage}}, + ) + else: + raise if status not in (200, 201, 202, 204): raise SystemExit(f"Workflow dispatch failed with status {status}") From 16aad2e142703018ee966871b90da484e5c92628 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Mar 2026 23:35:30 +0100 Subject: [PATCH 36/39] Remove grubinator2 example --- modules/ske/forgejo-connector/meshstack_integration.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 6984e895..2df4b35f 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -146,7 +146,7 @@ resource "meshstack_building_block_definition" "this" { app_hostname = { display_name = "app_hostname" - description = "Public application hostname for this stage (for example 'grubinator2-dev.likvid.stackit.run')." + description = "Public application hostname for this stage (for example 'ai-summarizer-klhyt-dev.likvid.stackit.run')." type = "STRING" assignment_type = "USER_INPUT" } From 0934566bc5faf1148260dec4d0f5569812d998d9 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 19 Mar 2026 00:00:00 +0100 Subject: [PATCH 37/39] refactor ske starterkit naming with optional random suffix - rename apps_base_domain to dns_zone_name in starterkit inputs - add add_random_name_suffix static BBD input (default true) - introduce local.name with optional random 5-char suffix - use local.name for git repo name, project names, and app hostnames - add random provider requirement for suffix generation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ske-starterkit/buildingblock/README.md | 5 ++++- .../ske/ske-starterkit/buildingblock/main.tf | 19 +++++++++++----- .../ske-starterkit/buildingblock/outputs.tf | 4 ++-- .../ske-starterkit/buildingblock/variables.tf | 9 ++++++-- .../ske-starterkit/buildingblock/versions.tf | 4 ++++ .../ske-starterkit/meshstack_integration.tf | 22 ++++++++++++++----- 6 files changed, 48 insertions(+), 15 deletions(-) diff --git a/modules/ske/ske-starterkit/buildingblock/README.md b/modules/ske/ske-starterkit/buildingblock/README.md index 531514d8..a1d132f3 100644 --- a/modules/ske/ske-starterkit/buildingblock/README.md +++ b/modules/ske/ske-starterkit/buildingblock/README.md @@ -15,6 +15,7 @@ This building block creates a dev and prod meshStack project pair, each with a d | Name | Version | |------|---------| | [meshstack](#requirement\_meshstack) | ~>0.20.0 | +| [random](#requirement\_random) | 3.8.1 | ## Modules @@ -29,14 +30,16 @@ No modules. | [meshstack_project.this](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/project) | resource | | [meshstack_project_user_binding.creator_to_admin](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/project_user_binding) | resource | | [meshstack_tenant_v4.this](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/tenant_v4) | resource | +| [random_string.name_suffix](https://registry.terraform.io/providers/hashicorp/random/3.8.1/docs/resources/string) | resource | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [apps\_base\_domain](#input\_apps\_base\_domain) | Base domain used for application ingress hostnames (for example 'likvid.stackit.run'). | `string` | n/a | yes | +| [add\_random\_name\_suffix](#input\_add\_random\_name\_suffix) | Whether to append a random suffix to the provided name for shared environments. | `bool` | n/a | yes | | [building\_block\_definitions](#input\_building\_block\_definitions) | n/a |
map(object({
uuid = string
version_ref = object({
uuid = string
})
}))
| n/a | yes | | [creator](#input\_creator) | Information about the creator of the resources who will be assigned Project Admin role |
object({
type = string
identifier = string
displayName = string
username = optional(string)
email = optional(string)
euid = optional(string)
})
| n/a | yes | +| [dns\_zone\_name](#input\_dns\_zone\_name) | DNS zone name used for application ingress hostnames (for example 'likvid.stackit.run'). | `string` | n/a | yes | | [full\_platform\_identifier](#input\_full\_platform\_identifier) | Full platform identifier of the SKE platform. | `string` | n/a | yes | | [landing\_zone\_identifiers](#input\_landing\_zone\_identifiers) | SKE Landing zone identifiers for the dev/prod meshTenant. |
object({
dev = string
prod = string
})
| n/a | yes | | [name](#input\_name) | This name will be used for the created projects. | `string` | n/a | yes | diff --git a/modules/ske/ske-starterkit/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf index c09018cc..561a5281 100644 --- a/modules/ske/ske-starterkit/buildingblock/main.tf +++ b/modules/ske/ske-starterkit/buildingblock/main.tf @@ -1,10 +1,19 @@ +resource "random_string" "name_suffix" { + length = 5 + upper = false + numeric = false + special = false +} + locals { + name = var.add_random_name_suffix ? "${var.name}-${random_string.name_suffix.result}" : var.name + app_hostnames = { for stage, _ in var.landing_zone_identifiers : stage => ( stage == "prod" - ? "${var.name}.${var.apps_base_domain}" - : "${var.name}-${stage}.${var.apps_base_domain}" + ? "${local.name}.${var.dns_zone_name}" + : "${local.name}-${stage}.${var.dns_zone_name}" ) } } @@ -13,14 +22,14 @@ resource "meshstack_building_block_v2" "git_repository" { spec = { building_block_definition_version_ref = var.building_block_definitions["git-repository"].version_ref # provisioned in backplane - display_name = "Git Repo ${var.name}" + display_name = "Git Repo ${local.name}" target_ref = { kind = "meshWorkspace" identifier = var.workspace_identifier } inputs = { - name = { value_string = var.name } + name = { value_string = local.name } clone_addr = { value_string = var.repo_clone_addr } } } @@ -30,7 +39,7 @@ resource "meshstack_building_block_v2" "git_repository" { resource "meshstack_project" "this" { for_each = tomap(var.landing_zone_identifiers) metadata = { - name = "${var.name}-${each.key}" + name = "${local.name}-${each.key}" owned_by_workspace = var.workspace_identifier } spec = { diff --git a/modules/ske/ske-starterkit/buildingblock/outputs.tf b/modules/ske/ske-starterkit/buildingblock/outputs.tf index 81f12d28..d595a194 100644 --- a/modules/ske/ske-starterkit/buildingblock/outputs.tf +++ b/modules/ske/ske-starterkit/buildingblock/outputs.tf @@ -1,9 +1,9 @@ output "dev_link" { - value = "https://${var.name}-dev.${var.apps_base_domain}" + value = "https://${local.name}-dev.${var.dns_zone_name}" description = "Public URL for the dev stage application." } output "prod_link" { - value = "https://${var.name}.${var.apps_base_domain}" + value = "https://${local.name}.${var.dns_zone_name}" description = "Public URL for the prod stage application." } diff --git a/modules/ske/ske-starterkit/buildingblock/variables.tf b/modules/ske/ske-starterkit/buildingblock/variables.tf index 676a7a8b..5d951f1b 100644 --- a/modules/ske/ske-starterkit/buildingblock/variables.tf +++ b/modules/ske/ske-starterkit/buildingblock/variables.tf @@ -47,9 +47,14 @@ variable "repo_clone_addr" { description = "URL to clone into the starterkit git repository." } -variable "apps_base_domain" { +variable "dns_zone_name" { type = string - description = "Base domain used for application ingress hostnames (for example 'likvid.stackit.run')." + description = "DNS zone name used for application ingress hostnames (for example 'likvid.stackit.run')." +} + +variable "add_random_name_suffix" { + type = bool + description = "Whether to append a random suffix to the provided name for shared environments." } variable "building_block_definitions" { diff --git a/modules/ske/ske-starterkit/buildingblock/versions.tf b/modules/ske/ske-starterkit/buildingblock/versions.tf index f9d4a802..f6259e42 100644 --- a/modules/ske/ske-starterkit/buildingblock/versions.tf +++ b/modules/ske/ske-starterkit/buildingblock/versions.tf @@ -5,5 +5,9 @@ terraform { source = "meshcloud/meshstack" version = "~>0.20.0" } + random = { + source = "hashicorp/random" + version = "3.8.1" + } } } diff --git a/modules/ske/ske-starterkit/meshstack_integration.tf b/modules/ske/ske-starterkit/meshstack_integration.tf index da5825a2..b93f747b 100644 --- a/modules/ske/ske-starterkit/meshstack_integration.tf +++ b/modules/ske/ske-starterkit/meshstack_integration.tf @@ -32,9 +32,15 @@ variable "repo_clone_addr" { description = "URL to clone into the starterkit git repository." } -variable "apps_base_domain" { +variable "dns_zone_name" { type = string - description = "Base domain used for application ingress hostnames." + description = "DNS zone name used for application ingress hostnames." +} + +variable "add_random_name_suffix" { + type = bool + default = true + description = "Whether to append a random suffix to starterkit names for shared environments." } variable "tags" { @@ -189,11 +195,17 @@ EOT display_name = "Clone from URL" argument = jsonencode(var.repo_clone_addr) } - "apps_base_domain" = { + "dns_zone_name" = { assignment_type = "STATIC" type = "STRING" - display_name = "Apps Base Domain" - argument = jsonencode(var.apps_base_domain) + display_name = "DNS Zone Name" + argument = jsonencode(var.dns_zone_name) + } + "add_random_name_suffix" = { + assignment_type = "STATIC" + type = "BOOLEAN" + display_name = "Add Random Name Suffix" + argument = jsonencode(var.add_random_name_suffix) } "building_block_definitions" = { assignment_type = "STATIC" From a2e185c3fc44887eebc3116325effc2aec3706dd Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 19 Mar 2026 00:14:09 +0100 Subject: [PATCH 38/39] Add some random suffixes/prefixes to k8s resources --- .../forgejo-connector/buildingblock/README.md | 4 ++- .../buildingblock/kubernetes.tf | 28 +++++++++---------- .../buildingblock/versions.tf | 11 +++++--- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/modules/ske/forgejo-connector/buildingblock/README.md b/modules/ske/forgejo-connector/buildingblock/README.md index 216af969..1460c015 100644 --- a/modules/ske/forgejo-connector/buildingblock/README.md +++ b/modules/ske/forgejo-connector/buildingblock/README.md @@ -46,7 +46,9 @@ the backplane module's `config_tf` output. | Name | Version | |------|---------| | [external](#requirement\_external) | ~> 2.3.0 | +| [forgejo](#requirement\_forgejo) | ~> 1.3.0 | | [kubernetes](#requirement\_kubernetes) | 2.35.1 | +| [random](#requirement\_random) | ~> 3.8.0 | | [restapi](#requirement\_restapi) | 3.0.0 | ## Modules @@ -66,7 +68,7 @@ No modules. | [kubernetes_secret.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_secret.image_pull](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/secret) | resource | | [kubernetes_service_account.forgejo_actions](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/service_account) | resource | -| [random_string.clusterissuer_reader_name_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [random_string.resource_name_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | | [restapi_object.action_variable](https://registry.terraform.io/providers/Mastercard/restapi/3.0.0/docs/resources/object) | resource | | [terraform_data.await_pipeline_workflow](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | | [external_external.repository_context](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | diff --git a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf index 0627dfaf..06b4f263 100644 --- a/modules/ske/forgejo-connector/buildingblock/kubernetes.tf +++ b/modules/ske/forgejo-connector/buildingblock/kubernetes.tf @@ -15,10 +15,18 @@ provider "kubernetes" { client_key = base64decode(local.kubeconfig_admin_user["client-key-data"]) } +resource "random_string" "resource_name_suffix" { + length = 5 + lower = true + upper = false + numeric = true + special = false +} + # Service account for forgejo actions to use resource "kubernetes_service_account" "forgejo_actions" { metadata { - name = "forgejo-actions" + name = "forgejo-actions-${random_string.resource_name_suffix.result}" namespace = var.namespace } @@ -33,7 +41,7 @@ resource "kubernetes_service_account" "forgejo_actions" { resource "kubernetes_secret" "forgejo_actions" { metadata { - name = "forgejo-actions" + name = "forgejo-actions-${random_string.resource_name_suffix.result}" namespace = var.namespace annotations = { "kubernetes.io/service-account.name" = kubernetes_service_account.forgejo_actions.metadata[0].name @@ -45,7 +53,7 @@ resource "kubernetes_secret" "forgejo_actions" { resource "kubernetes_role_binding" "forgejo_actions" { metadata { - name = "forgejo-actions" + name = "forgejo-actions-${random_string.resource_name_suffix.result}" namespace = var.namespace } role_ref { @@ -61,18 +69,10 @@ resource "kubernetes_role_binding" "forgejo_actions" { } } -resource "random_string" "clusterissuer_reader_name_suffix" { - length = 8 - lower = true - upper = false - numeric = false - special = false -} - # The ClusterIssuer access is needed so that SSL certificates can be issued for projects using the connector. resource "kubernetes_cluster_role" "clusterissuer_reader" { metadata { - name = "clusterissuer-reader-${random_string.clusterissuer_reader_name_suffix.result}" # random suffix ensures multiple roles can exist + name = "${var.namespace}-clusterissuer-reader-${random_string.resource_name_suffix.result}" } rule { @@ -84,7 +84,7 @@ resource "kubernetes_cluster_role" "clusterissuer_reader" { resource "kubernetes_cluster_role_binding" "forgejo_actions_clusterissuer_access" { metadata { - name = "forgejo-actions-clusterissuer-access-${var.namespace}" + name = "${var.namespace}-forgejo-actions-clusterissuer-access-${random_string.resource_name_suffix.result}" } role_ref { @@ -102,7 +102,7 @@ resource "kubernetes_cluster_role_binding" "forgejo_actions_clusterissuer_access resource "kubernetes_secret" "image_pull" { metadata { - name = "harbor-image-pull" + name = "harbor-image-pull-${random_string.resource_name_suffix.result}" namespace = var.namespace } diff --git a/modules/ske/forgejo-connector/buildingblock/versions.tf b/modules/ske/forgejo-connector/buildingblock/versions.tf index 83263289..9c27d488 100644 --- a/modules/ske/forgejo-connector/buildingblock/versions.tf +++ b/modules/ske/forgejo-connector/buildingblock/versions.tf @@ -5,7 +5,8 @@ terraform { version = "~> 2.3.0" } forgejo = { - source = "svalabs/forgejo" + source = "svalabs/forgejo" + version = "~> 1.3.0" } kubernetes = { @@ -13,12 +14,14 @@ terraform { version = "2.35.1" } + random = { + source = "hashicorp/random" + version = "~> 3.8.0" + } + restapi = { source = "Mastercard/restapi" version = "3.0.0" } } } - - - From 9a0594aed4f1949d62499eeda831b583a7bdb4a2 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 19 Mar 2026 00:17:54 +0100 Subject: [PATCH 39/39] fix: add unremovable starterkit input again --- modules/ske/ske-starterkit/meshstack_integration.tf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/ske/ske-starterkit/meshstack_integration.tf b/modules/ske/ske-starterkit/meshstack_integration.tf index b93f747b..8c638d51 100644 --- a/modules/ske/ske-starterkit/meshstack_integration.tf +++ b/modules/ske/ske-starterkit/meshstack_integration.tf @@ -216,6 +216,12 @@ EOT argument = jsonencode(jsonencode(var.building_block_definitions)) }, # TODO remove inputs below before merge, leftover from dev attempts in grubinator2 instance + "apps_base_domain" = { + assignment_type = "STATIC" + type = "STRING" + display_name = "REMOVEME" + argument = jsonencode(var.dns_zone_name) + } "building_block_definition_version_refs" = { assignment_type = "STATIC" type = "CODE"