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/ diff --git a/modules/ske/forgejo-connector/backplane/README.md b/modules/ske/forgejo-connector/backplane/README.md deleted file mode 100644 index d94e3806..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 | -| [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 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 cb7b901a..00000000 --- a/modules/ske/forgejo-connector/backplane/outputs.tf +++ /dev/null @@ -1,30 +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}") - } - - 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/backplane/variables.tf b/modules/ske/forgejo-connector/backplane/variables.tf deleted file mode 100644 index 83013d1f..00000000 --- a/modules/ske/forgejo-connector/backplane/variables.tf +++ /dev/null @@ -1,28 +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 -} - -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/backplane/versions.tf b/modules/ske/forgejo-connector/backplane/versions.tf deleted file mode 100644 index f3bca1ee..00000000 --- a/modules/ske/forgejo-connector/backplane/versions.tf +++ /dev/null @@ -1,10 +0,0 @@ -terraform { - required_version = ">= 1.0" - - required_providers { - kubernetes = { - source = "hashicorp/kubernetes" - version = "2.35.1" - } - } -} \ 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 396a213c..c6c6d718 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 @@ -55,29 +57,33 @@ 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.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 | | [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 | +| [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 | +| [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 | ## 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. | `number` | n/a | yes | +| [repository\_secret\_name\_suffix](#input\_repository\_secret\_name\_suffix) | Optional suffix appended to created repository secret names. | `string` | `""` | no | ## 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 1498319f..d927f500 100644 --- a/modules/ske/forgejo-connector/buildingblock/forgejo.tf +++ b/modules/ske/forgejo-connector/buildingblock/forgejo.tf @@ -1,55 +1,94 @@ -locals { - kubeconfig_user = { - users = [ - { - name = kubernetes_service_account.forgejo_actions.metadata[0].name - user = { - "token" = kubernetes_secret.forgejo_actions.data.token - } - } - ] - - contexts = [ - { - name = "stackit_k8s" - context = { - cluster = "stackit_k8s" - namespace = var.namespace - user = kubernetes_service_account.forgejo_actions.metadata[0].name - } - } - ] - } - kubeconfig = merge(local.stackit_kubeconfig_stub, local.kubeconfig_user) +provider "forgejo" { + # configured via env variables FORGEJO_HOST, FORGEJO_API_TOKEN } -data "forgejo_repository" "this" { - name = var.forgejo_repository_name - owner = var.forgejo_repository_owner +provider "restapi" { + uri = data.external.repository_context.result.forgejo_host + write_returns_object = false + + headers = { + Authorization = "token ${data.external.repository_context.result.forgejo_api_token}" + Content-Type = "application/json" + } } -resource "forgejo_repository_action_secret" "kubeconfig" { - repository_id = data.forgejo_repository.this.id - name = "KUBECONFIG" - data = yamlencode(local.kubeconfig) +data "external" "repository_context" { + program = ["python3", "${path.module}/get_forgejo_repository_context.py"] + + query = { + FORGEJO_REPOSITORY_ID = tostring(var.repository_id) + } } -resource "forgejo_repository_action_secret" "container_registry" { - for_each = { - HOST = var.harbor_host - USERNAME = var.harbor_username - PASSWORD = var.harbor_password +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 + + 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 + } + } + ] + })) } - repository_id = data.forgejo_repository.this.id - name = "STACKIT_HARBOR_${each.key}" - data = each.value + action_variables = { + "K8S_NAMESPACE${var.repository_secret_name_suffix}" = var.namespace + } } -resource "forgejo_repository_action_secret" "additional" { - for_each = var.additional_environment_variables +resource "forgejo_repository_action_secret" "action_secrets" { + for_each = local.action_secrets - repository_id = data.forgejo_repository.this.id + repository_id = var.repository_id name = each.key data = each.value -} \ No newline at end of file +} + +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({ + name = each.key + value = each.value + }) + ignore_server_additions = true +} + +resource "terraform_data" "await_pipeline_workflow" { + depends_on = [ + forgejo_repository_action_secret.action_secrets, + restapi_object.action_variables, + ] + + 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" + } + } +} 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/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 dae6cb31..4cd51ea4 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" { @@ -36,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}" @@ -44,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 { @@ -65,12 +111,22 @@ 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 +} + +resource "kubernetes_default_service_account" "namespace_default" { + metadata { + namespace = var.namespace + } + + image_pull_secret { + name = kubernetes_secret.image_pull.metadata[0].name + } +} 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/buildingblock/variables.tf b/modules/ske/forgejo-connector/buildingblock/variables.tf index 537e202d..c01cec21 100644 --- a/modules/ske/forgejo-connector/buildingblock/variables.tf +++ b/modules/ske/forgejo-connector/buildingblock/variables.tf @@ -1,3 +1,19 @@ +variable "namespace" { + description = "Associated namespace in kubernetes cluster." + type = string +} + +variable "repository_id" { + type = number + description = "The ID of the Forgejo repository." +} + +variable "repository_secret_name_suffix" { + type = string + description = "Optional suffix appended to created repository secret names." + default = "" +} + variable "harbor_host" { type = string description = "The URL of the Harbor registry." @@ -14,25 +30,4 @@ variable "harbor_password" { type = string description = "The password for the Harbor registry." sensitive = true -} - -variable "forgejo_repository_name" { - type = string - description = "The name of the Forgejo repository." -} - -variable "forgejo_repository_owner" { - type = string - description = "The owner of the Forgejo repository." -} - -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 } \ No newline at end of file diff --git a/modules/ske/forgejo-connector/buildingblock/provider.tf b/modules/ske/forgejo-connector/buildingblock/versions.tf similarity index 51% rename from modules/ske/forgejo-connector/buildingblock/provider.tf rename to modules/ske/forgejo-connector/buildingblock/versions.tf index 0f9f92ed..83263289 100644 --- a/modules/ske/forgejo-connector/buildingblock/provider.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,11 +12,13 @@ terraform { source = "hashicorp/kubernetes" version = "2.35.1" } + + restapi = { + source = "Mastercard/restapi" + version = "3.0.0" + } } } -# provide the following env variables for this Building Block: -# FORGEJO_HOST -# FORGEJO_API_TOKEN -provider "forgejo" { -} + + diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index 9511809a..5d0cab74 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -1,41 +1,6 @@ -# This file is an example showing how to register the STACKIT connector -# building block in a meshStack instance. - -terraform { - required_providers { - meshstack = { - source = "meshcloud/meshstack" - version = "~> 0.19.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 } @@ -48,6 +13,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 for tenant building blocks (connector)." +} + variable "harbor_host" { type = string description = "The URL of the Harbor registry." @@ -66,11 +36,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 @@ -89,18 +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, for example in the backplane of starter kits." -} - -module "backplane" { - source = "./backplane" - cluster_host = var.cluster_host - cluster_ca_certificate = var.client_certificate - client_key = var.client_key - client_certificate = var.client_certificate - cluster_kubeconfig = var.cluster_kubeconfig +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" { @@ -109,12 +68,13 @@ 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" - support_url = "https://portal.stackit.cloud/git" - target_type = "WORKSPACE_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 = "KUBERNETES" }] + run_transparency = true } version_spec = { @@ -125,41 +85,59 @@ 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" + namespace = { + display_name = "K8S Namespace" + description = "Provided 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" - is_environment = true sensitive = { argument = { - secret_value = jsonencode(module.backplane.config_tf) + secret_value = "data:application/yaml;base64,${base64encode(yamlencode(var.kubeconfig))}" # data type application/yaml is ignored anyway + secret_version = nonsensitive(sha256(yamlencode(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) - } - } + argument = jsonencode(var.forgejo_host) } FORGEJO_API_TOKEN = { @@ -170,7 +148,8 @@ resource "meshstack_building_block_definition" "this" { is_environment = true sensitive = { argument = { - secret_value = jsonencode(var.forgejo_api_token) + secret_value = var.forgejo_api_token + secret_version = nonsensitive(sha256(var.forgejo_api_token)) } } } @@ -188,7 +167,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 = var.harbor_username + secret_version = nonsensitive(sha256(var.harbor_username)) + } + } } harbor_password = { @@ -198,38 +182,29 @@ resource "meshstack_building_block_definition" "this" { assignment_type = "STATIC" sensitive = { argument = { - secret_value = jsonencode(var.harbor_password) + secret_value = var.harbor_password + secret_version = nonsensitive(sha256(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." - type = "STRING" - assignment_type = "BUILDING_BLOCK_OUTPUT" - argument = "${var.forgejo_repo_definition_uuid}.repo_owner" - } - - # ── User inputs ──────────────────────────────────────────────────────── - - namespace = { - display_name = "namespace" - description = "Associated namespace in kubernetes cluster." - type = "STRING" - assignment_type = "USER_INPUT" + outputs = { + action_variables = { + display_name = "Action Variables" + description = "Non-sensitive action variable map for dependent building block wiring." + type = "CODE" + assignment_type = "NONE" } } + } +} - outputs = { +terraform { + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = "~> 0.20.0" } } } 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..5b7c851c 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 | @@ -33,12 +34,12 @@ 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 |
+| [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 |
+| [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/buildingblock/main.tf b/modules/ske/ske-starterkit/buildingblock/main.tf
index 1a8801a4..3b0549fb 100644
--- a/modules/ske/ske-starterkit/buildingblock/main.tf
+++ b/modules/ske/ske-starterkit/buildingblock/main.tf
@@ -1,8 +1,8 @@
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 = "${var.name} Git Repo"
+ display_name = "Git Repo ${var.name}"
target_ref = {
kind = "meshWorkspace"
identifier = var.workspace_identifier
@@ -66,3 +66,30 @@ resource "meshstack_tenant_v4" "this" {
landing_zone_identifier = each.value
}
}
+
+resource "meshstack_building_block_v2" "forgejo_connector" {
+ for_each = meshstack_tenant_v4.this
+
+ spec = {
+ building_block_definition_version_ref = var.building_block_definitions["forgejo-connector"].version_ref
+
+ display_name = "${var.name} Forgejo Connector ${title(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.building_block_definitions["git-repository"].uuid
+ }]
+
+ inputs = {
+ repository_secret_name_suffix = {
+ value_string = "_${upper(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..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 b2304344..58556514 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,16 @@ variable "notification_subscribers" {
default = []
}
+variable "building_block_definitions" {
+ type = map(object({
+ uuid = string
+ version_ref = 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 +64,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
}
@@ -198,13 +184,34 @@ 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_definitions))
+ },
+ "git_repository_action_secrets" = {
+ assignment_type = "STATIC"
+ type = "CODE"
+ 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(module.backplane.building_block_definition_version_refs))
+ sensitive = {
+ argument = {
+ secret_value = jsonencode({})
+ }
+ }
}
}
diff --git a/modules/stackit/git-repository/README.md b/modules/stackit/git-repository/README.md
index a1fecd14..af0ff3f8 100644
--- a/modules/stackit/git-repository/README.md
+++ b/modules/stackit/git-repository/README.md
@@ -13,9 +13,10 @@ 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_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/buildingblock/README.md b/modules/stackit/git-repository/buildingblock/README.md
index 8ff4c960..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
@@ -22,17 +24,20 @@ 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 |
+| [external_external.forgejo_env](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source |
## 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 |
+| [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 |
-| [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 d4b02a13..23a060ae 100644
--- a/modules/stackit/git-repository/buildingblock/main.tf
+++ b/modules/stackit/git-repository/buildingblock/main.tf
@@ -1,4 +1,33 @@
-# ── Repository ─────────────────────────────────────────────────────────────────
+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"
+}
resource "forgejo_repository" "repository" {
owner = var.forgejo_organization
@@ -6,9 +35,31 @@ 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
}
+
+resource "forgejo_repository_action_secret" "action_secrets" {
+ for_each = var.action_secrets
+
+ repository_id = forgejo_repository.repository.id
+ 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/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 d9a843a9..effbd0d2 100644
--- a/modules/stackit/git-repository/buildingblock/variables.tf
+++ b/modules/stackit/git-repository/buildingblock/variables.tf
@@ -1,20 +1,26 @@
# ── Backplane inputs (static, set once per building block definition) ──────────
-variable "forgejo_base_url" {
+variable "forgejo_organization" {
type = string
- description = "STACKIT Git base URL"
- default = "https://git-service.git.onstackit.cloud"
+ description = "STACKIT Git organization where the repository will be created"
}
-variable "forgejo_token" {
- type = string
- description = "STACKIT Git API token (from backplane)"
- sensitive = true
+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."
+ }
}
-variable "forgejo_organization" {
- type = string
- 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 = {}
}
# ── User inputs (set per building block instance) ─────────────────────────────
@@ -51,6 +57,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/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 a071a198..78735bbb 100644
--- a/modules/stackit/git-repository/meshstack_integration.tf
+++ b/modules/stackit/git-repository/meshstack_integration.tf
@@ -21,6 +21,22 @@ 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 "action_variables" {
+ type = map(string)
+ default = {}
+}
+
variable "hub" {
type = object({
git_ref = optional(string, "main")
@@ -33,9 +49,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" {
+ 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" {
@@ -78,19 +97,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
@@ -118,31 +139,59 @@ 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 = {
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")
+ }
+ 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)))
+ }
+ }
+ }
+ 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 = {
+ repository_id = {
+ display_name = "Repository ID"
+ type = "INTEGER"
+ assignment_type = "NONE"
+ description = "Numeric Forgejo repository ID, primarily intended for wiring dependent building blocks."
+ }
+
repository_html_url = {
display_name = "Open Repository"
type = "STRING"