diff --git a/modules/stackit/git-repository/buildingblock/versions.tf b/modules/stackit/git-repository/buildingblock/versions.tf index d26605a..df2b03f 100644 --- a/modules/stackit/git-repository/buildingblock/versions.tf +++ b/modules/stackit/git-repository/buildingblock/versions.tf @@ -3,8 +3,8 @@ terraform { required_providers { gitea = { - source = "Lerentis/gitea" - version = "~> 0.16.0" + source = "go-gitea/gitea" + version = "~> 0.7.0" } null = { source = "hashicorp/null" diff --git a/modules/stackit/git-repository/meshstack_integration.tf b/modules/stackit/git-repository/meshstack_integration.tf index e50438b..584f3c5 100644 --- a/modules/stackit/git-repository/meshstack_integration.tf +++ b/modules/stackit/git-repository/meshstack_integration.tf @@ -182,3 +182,13 @@ resource "meshstack_building_block_definition" "stackit_git_repo" { } } } + +output "bbd_uuid" { + description = "UUID of the STACKIT Git repository building block definition." + value = meshstack_building_block_definition.stackit_git_repo.ref.uuid +} + +output "bbd_version_uuid" { + description = "UUID of the latest version of the STACKIT Git repository building block definition." + value = meshstack_building_block_definition.stackit_git_repo.version_latest.uuid +} diff --git a/modules/stackit/ske/backplane/README.md b/modules/stackit/ske/backplane/README.md new file mode 100644 index 0000000..1e62fc9 --- /dev/null +++ b/modules/stackit/ske/backplane/README.md @@ -0,0 +1,82 @@ +# SKE Backplane + +This module provisions the STACKIT Kubernetes Engine (SKE) cluster and sets up the +meshStack platform integration (replicator and metering service accounts). + +## What it creates + +- **SKE Cluster** with a configurable node pool (machine type, count, availability zones) +- **Kubeconfig** for cluster access (180-day expiration, auto-refresh) +- **meshStack platform integration** via [terraform-kubernetes-meshplatform](https://github.com/meshcloud/terraform-kubernetes-meshplatform): + - Replicator service account for namespace provisioning + - Metering service account for usage data collection + +## Usage + +This module is called from `meshstack_integration.tf` and its outputs are wired into the +`meshstack_platform` resource's `config.kubernetes` block. + +```hcl +module "backplane" { + source = "./backplane" + + stackit_project_id = "your-project-id" + cluster_name = "ske-cluster" + region = "eu01" +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [kubernetes](#requirement\_kubernetes) | ~> 2.0 | +| [stackit](#requirement\_stackit) | >= 0.68.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [meshplatform](#module\_meshplatform) | git::https://github.com/meshcloud/terraform-kubernetes-meshplatform.git | v0.1.0 | + +## Resources + +| Name | Type | +|------|------| +| [stackit_ske_cluster.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/ske_cluster) | resource | +| [stackit_ske_kubeconfig.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/ske_kubeconfig) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [availability\_zones](#input\_availability\_zones) | Availability zones for the default node pool. | `list(string)` |
[
"eu01-1"
]
| no | +| [cluster\_name](#input\_cluster\_name) | Name of the SKE cluster. | `string` | `"ske-cluster"` | no | +| [enable\_kubernetes\_version\_updates](#input\_enable\_kubernetes\_version\_updates) | Enable automatic Kubernetes version updates during maintenance windows. | `bool` | `true` | no | +| [enable\_machine\_image\_version\_updates](#input\_enable\_machine\_image\_version\_updates) | Enable automatic machine image version updates during maintenance windows. | `bool` | `true` | no | +| [machine\_type](#input\_machine\_type) | Machine type for the default node pool. | `string` | `"c2i.2"` | no | +| [maintenance\_end](#input\_maintenance\_end) | End of the maintenance window (UTC). | `string` | `"06:00:00Z"` | no | +| [maintenance\_start](#input\_maintenance\_start) | Start of the maintenance window (UTC). | `string` | `"02:00:00Z"` | no | +| [meshplatform\_namespace](#input\_meshplatform\_namespace) | Kubernetes namespace for the meshStack platform integration (replicator + metering service accounts). | `string` | `"meshcloud"` | no | +| [node\_count](#input\_node\_count) | Number of nodes in the default node pool. | `number` | `1` | no | +| [region](#input\_region) | STACKIT region for the SKE cluster. | `string` | `"eu01"` | no | +| [stackit\_project\_id](#input\_stackit\_project\_id) | STACKIT project ID where the SKE cluster will be created. | `string` | n/a | yes | +| [volume\_size](#input\_volume\_size) | Volume size in GB for nodes in the default node pool. | `number` | `25` | no | +| [volume\_type](#input\_volume\_type) | Volume type for nodes in the default node pool. | `string` | `"storage_premium_perf0"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [client\_certificate](#output\_client\_certificate) | PEM-encoded client certificate for authentication. | +| [client\_key](#output\_client\_key) | PEM-encoded client key for authentication. | +| [cluster\_ca\_certificate](#output\_cluster\_ca\_certificate) | PEM-encoded CA certificate for the cluster. | +| [cluster\_name](#output\_cluster\_name) | Name of the SKE cluster. | +| [console\_url](#output\_console\_url) | URL to the STACKIT portal for this SKE cluster. | +| [kube\_host](#output\_kube\_host) | Kubernetes API server endpoint. | +| [kubernetes\_version](#output\_kubernetes\_version) | Kubernetes version running on the cluster. | +| [metering\_token](#output\_metering\_token) | Access token for the meshStack metering service account. | +| [replicator\_token](#output\_replicator\_token) | Access token for the meshStack replicator service account. | + \ No newline at end of file diff --git a/modules/stackit/ske/backplane/main.tf b/modules/stackit/ske/backplane/main.tf new file mode 100644 index 0000000..212dd23 --- /dev/null +++ b/modules/stackit/ske/backplane/main.tf @@ -0,0 +1,55 @@ +resource "stackit_ske_cluster" "this" { + project_id = var.stackit_project_id + name = var.cluster_name + node_pools = [ + { + name = "default" + machine_type = var.machine_type + minimum = var.node_count + maximum = var.node_count + availability_zones = var.availability_zones + volume_size = var.volume_size + volume_type = var.volume_type + } + ] + maintenance = { + enable_kubernetes_version_updates = var.enable_kubernetes_version_updates + enable_machine_image_version_updates = var.enable_machine_image_version_updates + start = var.maintenance_start + end = var.maintenance_end + } + + lifecycle { + ignore_changes = [kubernetes_version_used, node_pools[0].os_version_used] + } +} + +resource "stackit_ske_kubeconfig" "this" { + project_id = var.stackit_project_id + cluster_name = stackit_ske_cluster.this.name + expiration = "15552000" # 180 days + refresh = true +} + +locals { + kubeconfig = yamldecode(stackit_ske_kubeconfig.this.kube_config) + kube_host = local.kubeconfig["clusters"][0]["cluster"]["server"] + cluster_ca_certificate = base64decode(local.kubeconfig["clusters"][0]["cluster"]["certificate-authority-data"]) + client_certificate = base64decode(local.kubeconfig["users"][0]["user"]["client-certificate-data"]) + client_key = base64decode(local.kubeconfig["users"][0]["user"]["client-key-data"]) +} + +provider "kubernetes" { + host = local.kube_host + cluster_ca_certificate = local.cluster_ca_certificate + client_certificate = local.client_certificate + client_key = local.client_key +} + +module "meshplatform" { + source = "git::https://github.com/meshcloud/terraform-kubernetes-meshplatform.git?ref=v0.1.0" + + namespace = var.meshplatform_namespace + replicator_enabled = true + metering_enabled = true +} diff --git a/modules/stackit/ske/backplane/outputs.tf b/modules/stackit/ske/backplane/outputs.tf new file mode 100644 index 0000000..88c278c --- /dev/null +++ b/modules/stackit/ske/backplane/outputs.tf @@ -0,0 +1,50 @@ +output "cluster_name" { + description = "Name of the SKE cluster." + value = stackit_ske_cluster.this.name +} + +output "kube_host" { + description = "Kubernetes API server endpoint." + value = local.kube_host + sensitive = true +} + +output "cluster_ca_certificate" { + description = "PEM-encoded CA certificate for the cluster." + value = local.cluster_ca_certificate + sensitive = true +} + +output "client_certificate" { + description = "PEM-encoded client certificate for authentication." + value = local.client_certificate + sensitive = true +} + +output "client_key" { + description = "PEM-encoded client key for authentication." + value = local.client_key + sensitive = true +} + +output "replicator_token" { + description = "Access token for the meshStack replicator service account." + value = module.meshplatform.replicator_token + sensitive = true +} + +output "metering_token" { + description = "Access token for the meshStack metering service account." + value = module.meshplatform.metering_token + sensitive = true +} + +output "kubernetes_version" { + description = "Kubernetes version running on the cluster." + value = stackit_ske_cluster.this.kubernetes_version_used +} + +output "console_url" { + description = "URL to the STACKIT portal for this SKE cluster." + value = "https://portal.stackit.cloud/project/${stackit_ske_cluster.this.project_id}/kubernetes/${stackit_ske_cluster.this.name}" +} diff --git a/modules/stackit/ske/backplane/variables.tf b/modules/stackit/ske/backplane/variables.tf new file mode 100644 index 0000000..93ace28 --- /dev/null +++ b/modules/stackit/ske/backplane/variables.tf @@ -0,0 +1,76 @@ +variable "stackit_project_id" { + type = string + description = "STACKIT project ID where the SKE cluster will be created." +} + +variable "cluster_name" { + type = string + description = "Name of the SKE cluster." + default = "ske-cluster" +} + +variable "region" { + type = string + description = "STACKIT region for the SKE cluster." + default = "eu01" +} + +variable "node_count" { + type = number + description = "Number of nodes in the default node pool." + default = 1 +} + +variable "machine_type" { + type = string + description = "Machine type for the default node pool." + default = "c2i.2" +} + +variable "availability_zones" { + type = list(string) + description = "Availability zones for the default node pool." + default = ["eu01-1"] +} + +variable "volume_size" { + type = number + description = "Volume size in GB for nodes in the default node pool." + default = 25 +} + +variable "volume_type" { + type = string + description = "Volume type for nodes in the default node pool." + default = "storage_premium_perf0" +} + +variable "maintenance_start" { + type = string + description = "Start of the maintenance window (UTC)." + default = "02:00:00Z" +} + +variable "maintenance_end" { + type = string + description = "End of the maintenance window (UTC)." + default = "06:00:00Z" +} + +variable "enable_kubernetes_version_updates" { + type = bool + description = "Enable automatic Kubernetes version updates during maintenance windows." + default = true +} + +variable "enable_machine_image_version_updates" { + type = bool + description = "Enable automatic machine image version updates during maintenance windows." + default = true +} + +variable "meshplatform_namespace" { + type = string + description = "Kubernetes namespace for the meshStack platform integration (replicator + metering service accounts)." + default = "meshcloud" +} diff --git a/modules/stackit/ske/backplane/versions.tf b/modules/stackit/ske/backplane/versions.tf new file mode 100644 index 0000000..a2edfb0 --- /dev/null +++ b/modules/stackit/ske/backplane/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = ">= 0.68.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.0" + } + } +} diff --git a/modules/stackit/ske/forgejo-connector/buildingblock/APP_TEAM_README.md b/modules/stackit/ske/forgejo-connector/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..b3446fd --- /dev/null +++ b/modules/stackit/ske/forgejo-connector/buildingblock/APP_TEAM_README.md @@ -0,0 +1,41 @@ +# Forgejo Actions Integration with SKE + +## Description + +This building block connects your Forgejo (STACKIT Git) repository to a STACKIT Kubernetes +Engine (SKE) namespace via Forgejo Actions. It provides your CI/CD workflows with secure +access to deploy applications into your Kubernetes namespace. + +## Usage Motivation + +Use this building block when you want to automate deployments from your STACKIT Git repository +to SKE using Forgejo Actions workflows, similar to GitHub Actions. + +## Usage Examples + +- Push code to your repository and have Forgejo Actions automatically build and deploy your + application to the connected SKE namespace. +- Set up a workflow that runs tests and deploys on merge to the main branch. + +## Prerequisites + +- A Forgejo repository (created via the STACKIT Git Repository building block) +- An SKE namespace (created via the STACKIT Starterkit or manually) + +## Shared Responsibility + +| Responsibility | Platform Team | Application Team | +|---------------------------------------------------|---------------|------------------| +| Setting up Forgejo Actions connector and secrets | ✅ | ❌ | +| Managing SKE cluster and namespace | ✅ | ❌ | +| Writing Forgejo Actions workflow files | ❌ | ✅ | +| Writing and maintaining Kubernetes manifests | ❌ | ✅ | +| Monitoring deployments and troubleshooting | ❌ | ✅ | + +## Recommendations + +- **Use namespace-scoped resources**: The service account only has `edit` access within your + namespace — do not attempt to create cluster-scoped resources. +- **Keep secrets secure**: The `KUBECONFIG` secret is automatically managed. Do not expose it + in workflow logs. +- **Use deployment strategies**: Implement rolling updates for minimal downtime. diff --git a/modules/stackit/ske/forgejo-connector/buildingblock/README.md b/modules/stackit/ske/forgejo-connector/buildingblock/README.md new file mode 100644 index 0000000..85f1eb1 --- /dev/null +++ b/modules/stackit/ske/forgejo-connector/buildingblock/README.md @@ -0,0 +1,19 @@ +--- +name: Forgejo Actions Integration with SKE +supportedPlatforms: + - stackit +description: | + CI/CD pipeline using Forgejo Actions for deploying to STACKIT Kubernetes Engine (SKE). +--- + +# Forgejo Actions Integration with SKE + +This Terraform module provisions the necessary resources to integrate Forgejo Actions +with an SKE cluster namespace. It creates a Kubernetes service account with deployment +permissions and stores the kubeconfig as a Forgejo Actions secret. + +## Features + +- Secure authentication using Kubernetes service accounts +- Kubeconfig automatically stored as a Forgejo Actions repository secret +- Namespace-scoped RBAC (edit role) for least-privilege deployments diff --git a/modules/stackit/ske/forgejo-connector/buildingblock/logo.png b/modules/stackit/ske/forgejo-connector/buildingblock/logo.png new file mode 100644 index 0000000..d4ce3d0 Binary files /dev/null and b/modules/stackit/ske/forgejo-connector/buildingblock/logo.png differ diff --git a/modules/stackit/ske/forgejo-connector/buildingblock/main.tf b/modules/stackit/ske/forgejo-connector/buildingblock/main.tf new file mode 100644 index 0000000..4b9f0bf --- /dev/null +++ b/modules/stackit/ske/forgejo-connector/buildingblock/main.tf @@ -0,0 +1,128 @@ +# ── Kubernetes service account for Forgejo Actions ──────────────────────────── + +resource "kubernetes_service_account" "forgejo_actions" { + metadata { + name = "forgejo-actions" + namespace = var.namespace + } +} + +resource "kubernetes_secret" "forgejo_actions_token" { + metadata { + name = "forgejo-actions-token" + namespace = var.namespace + annotations = { + "kubernetes.io/service-account.name" = kubernetes_service_account.forgejo_actions.metadata[0].name + } + } + + type = "kubernetes.io/service-account-token" +} + +resource "kubernetes_role_binding" "forgejo_actions" { + metadata { + name = "forgejo-actions" + namespace = var.namespace + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = "edit" + } + + subject { + kind = "ServiceAccount" + name = kubernetes_service_account.forgejo_actions.metadata[0].name + namespace = var.namespace + } +} + +# ── Generate kubeconfig for the service account ────────────────────────────── + +locals { + kubeconfig = yamlencode({ + apiVersion = "v1" + kind = "Config" + current-context = "ske" + clusters = [{ + name = "ske" + cluster = { + server = var.cluster_server + certificate-authority-data = var.cluster_ca_certificate + } + }] + users = [{ + name = kubernetes_service_account.forgejo_actions.metadata[0].name + user = { + token = kubernetes_secret.forgejo_actions_token.data["token"] + } + }] + contexts = [{ + name = "ske" + context = { + cluster = "ske" + namespace = var.namespace + user = kubernetes_service_account.forgejo_actions.metadata[0].name + } + }] + }) +} + +# ── Store kubeconfig as Forgejo Actions secret ──────────────────────────────── + +resource "gitea_repository_actions_secret" "kubeconfig" { + repository_owner = var.gitea_organization + repository = var.repository_name + secret_name = "KUBECONFIG" + secret_value = local.kubeconfig +} + +# ── Harbor container registry secrets (optional) ───────────────────────────── + +resource "gitea_repository_actions_secret" "harbor_url" { + count = var.harbor != null ? 1 : 0 + repository_owner = var.gitea_organization + repository = var.repository_name + secret_name = "HARBOR_URL" + secret_value = var.harbor.url +} + +resource "gitea_repository_actions_secret" "harbor_username" { + count = var.harbor != null ? 1 : 0 + repository_owner = var.gitea_organization + repository = var.repository_name + secret_name = "HARBOR_USERNAME" + secret_value = var.harbor.robot_username +} + +resource "gitea_repository_actions_secret" "harbor_token" { + count = var.harbor != null ? 1 : 0 + repository_owner = var.gitea_organization + repository = var.repository_name + secret_name = "HARBOR_TOKEN" + secret_value = var.harbor.robot_token +} + +resource "kubernetes_secret" "harbor_pull_secret" { + count = var.harbor != null ? 1 : 0 + + metadata { + name = "harbor-pull-secret" + namespace = var.namespace + } + + type = "kubernetes.io/dockerconfigjson" + + data = { + ".dockerconfigjson" = jsonencode({ + auths = { + (var.harbor.url) = { + username = var.harbor.robot_username + password = var.harbor.robot_token + auth = base64encode("${var.harbor.robot_username}:${var.harbor.robot_token}") + } + } + }) + } +} diff --git a/modules/stackit/ske/forgejo-connector/buildingblock/outputs.tf b/modules/stackit/ske/forgejo-connector/buildingblock/outputs.tf new file mode 100644 index 0000000..e6718d7 --- /dev/null +++ b/modules/stackit/ske/forgejo-connector/buildingblock/outputs.tf @@ -0,0 +1,35 @@ +output "summary" { + description = "Summary of the Forgejo Actions connector setup." + sensitive = true + value = <<-EOT +# ✅ Forgejo Actions Connector Configured + +**${var.repository_name}** is now connected to SKE namespace **${var.namespace}**. + +## What was set up + +- Kubernetes service account `forgejo-actions` in namespace `${var.namespace}` +- Role binding granting `edit` permissions in the namespace +- `KUBECONFIG` secret stored in the Forgejo repository for use in Actions workflows +${var.harbor != null ? "- `HARBOR_URL`, `HARBOR_USERNAME`, `HARBOR_TOKEN` secrets stored in the Forgejo repository\n- `harbor-pull-secret` created in namespace `${var.namespace}` for image pulls" : ""} + +## Next Steps + +1. Create a `.forgejo/workflows/deploy.yml` in your repository +2. Use the `KUBECONFIG` secret to authenticate with the SKE cluster: + ```yaml + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Deploy to SKE + env: + KUBECONFIG_DATA: $${secrets.KUBECONFIG} + run: | + echo "$KUBECONFIG_DATA" > /tmp/kubeconfig + kubectl --kubeconfig /tmp/kubeconfig apply -f k8s/ + ``` +${var.harbor != null ? "3. Use Harbor secrets to push container images:\n ```yaml\n - name: Push to Harbor\n run: |\n echo \"$${secrets.HARBOR_TOKEN}\" | docker login $${secrets.HARBOR_URL} -u \"$${secrets.HARBOR_USERNAME}\" --password-stdin\n docker push $${secrets.HARBOR_URL}/registry/$${your-app}:latest\n ```" : ""} +EOT +} diff --git a/modules/stackit/ske/forgejo-connector/buildingblock/provider.tf b/modules/stackit/ske/forgejo-connector/buildingblock/provider.tf new file mode 100644 index 0000000..390aa91 --- /dev/null +++ b/modules/stackit/ske/forgejo-connector/buildingblock/provider.tf @@ -0,0 +1,4 @@ +provider "gitea" { + base_url = var.gitea_base_url + token = var.gitea_token +} diff --git a/modules/stackit/ske/forgejo-connector/buildingblock/variables.tf b/modules/stackit/ske/forgejo-connector/buildingblock/variables.tf new file mode 100644 index 0000000..134aaaf --- /dev/null +++ b/modules/stackit/ske/forgejo-connector/buildingblock/variables.tf @@ -0,0 +1,53 @@ +# ── Backplane inputs (static, set once per building block definition) ────────── + +variable "gitea_base_url" { + type = string + description = "STACKIT Git (Forgejo) base URL." + default = "https://git-service.git.onstackit.cloud" +} + +variable "gitea_token" { + type = string + description = "STACKIT Git API token with permissions to manage repository secrets." + sensitive = true +} + +variable "gitea_organization" { + type = string + description = "STACKIT Git organization that owns the repository." +} + +# ── Inputs wired from parent building block / platform ──────────────────────── + +variable "namespace" { + type = string + description = "SKE namespace to connect the Forgejo Actions pipeline to." +} + +variable "repository_name" { + type = string + description = "Name of the Forgejo repository to store deployment secrets in." +} + +variable "cluster_server" { + type = string + description = "SKE cluster API server URL for kubeconfig generation." +} + +variable "cluster_ca_certificate" { + type = string + description = "Base64-encoded CA certificate of the SKE cluster." +} + +# ── Optional container registry credentials ────────────────────────────────── + +variable "harbor" { + type = object({ + url = string + robot_username = string + robot_token = string + }) + sensitive = true + default = null + description = "Harbor registry credentials. When provided, stores push credentials as Forgejo Actions secrets and creates an image pull secret in the namespace." +} diff --git a/modules/stackit/ske/forgejo-connector/buildingblock/versions.tf b/modules/stackit/ske/forgejo-connector/buildingblock/versions.tf new file mode 100644 index 0000000..70d5b64 --- /dev/null +++ b/modules/stackit/ske/forgejo-connector/buildingblock/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.35.0" + } + gitea = { + source = "go-gitea/gitea" + version = "~> 0.7.0" + } + } +} diff --git a/modules/stackit/ske/forgejo-connector/meshstack_integration.tf b/modules/stackit/ske/forgejo-connector/meshstack_integration.tf new file mode 100644 index 0000000..249d8b7 --- /dev/null +++ b/modules/stackit/ske/forgejo-connector/meshstack_integration.tf @@ -0,0 +1,183 @@ +# This file is an example showing how to register the Forgejo Actions connector +# building block in a meshStack instance. Adapt variables to your setup. + +variable "hub" { + type = object({ + git_ref = string + }) + default = { + git_ref = "main" + } + description = "Hub release reference. Set git_ref to a tag (e.g. 'v1.2.3') or branch for the meshstack-hub repo." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + }) + description = "Shared meshStack context passed down from the IaC runtime." +} + +variable "gitea" { + type = object({ + base_url = string + token = string + organization = string + }) + sensitive = true + description = "STACKIT Git (Forgejo) credentials and organization." +} + +variable "ske" { + type = object({ + cluster_server = string + cluster_ca_certificate = string + }) + description = "SKE cluster connection details for kubeconfig generation." +} + +variable "git_repo_bbd" { + type = object({ + uuid = string + }) + description = "Reference to the STACKIT Git Repository building block definition (dependency)." +} + +variable "harbor" { + type = object({ + url = string + robot_username = string + robot_token = string + }) + sensitive = true + default = null + description = "Harbor registry credentials. When provided, stores push credentials as Forgejo Actions secrets and creates an image pull secret in the namespace." +} + +terraform { + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.19.3" + } + } +} + +resource "meshstack_building_block_definition" "forgejo_connector" { + metadata = { + owned_by_workspace = var.meshstack.owning_workspace_identifier + } + + spec = { + description = "CI/CD pipeline using Forgejo Actions for deploying to STACKIT Kubernetes Engine (SKE)." + display_name = "Forgejo Actions Integration with SKE" + symbol = provider::meshstack::load_image_file("${path.module}/buildingblock/logo.png") + target_type = "TENANT_LEVEL" + supported_platforms = [{ name = "STACKIT_KUBERNETES_ENGINE" }] + run_transparency = true + readme = file("${path.module}/buildingblock/README.md") + } + + version_spec = { + draft = true + implementation = { + terraform = { + repository_url = "https://github.com/meshcloud/meshstack-hub.git" + terraform_version = "1.9.0" + async = false + ref_name = var.hub.git_ref + repository_path = "modules/stackit/ske/forgejo-connector/buildingblock" + use_mesh_http_backend_fallback = true + } + } + dependency_refs = [ + { uuid = var.git_repo_bbd.uuid } + ] + inputs = merge({ + "gitea_base_url" = { + display_name = "STACKIT Git Base URL" + description = "Base URL of the STACKIT Git (Forgejo) instance." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.gitea.base_url) + } + "gitea_token" = { + display_name = "STACKIT Git API Token" + description = "API token for managing Forgejo repository secrets." + type = "STRING" + assignment_type = "STATIC" + sensitive = { + argument = { + secret_value = var.gitea.token + } + } + } + "gitea_organization" = { + display_name = "STACKIT Git Organization" + description = "Organization that owns the repository." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.gitea.organization) + } + "repository_name" = { + display_name = "Repository Name" + description = "Name of the Forgejo repository (wired from Git Repository BBD output)." + type = "STRING" + assignment_type = "BUILDING_BLOCK_OUTPUT" + argument = jsonencode("${var.git_repo_bbd.uuid}.repository_name") + } + "namespace" = { + display_name = "SKE Namespace" + type = "STRING" + assignment_type = "PLATFORM_TENANT_ID" + } + "cluster_server" = { + display_name = "SKE Cluster Server" + description = "API server URL of the SKE cluster." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.ske.cluster_server) + } + "cluster_ca_certificate" = { + display_name = "SKE Cluster CA Certificate" + description = "Base64-encoded CA certificate of the SKE cluster." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(var.ske.cluster_ca_certificate) + } + }, var.harbor != null ? { + "harbor" = { + display_name = "Harbor Registry Credentials" + description = "Harbor registry URL and robot account credentials for container image push/pull." + type = "CODE" + assignment_type = "STATIC" + sensitive = { + argument = { + secret_value = jsonencode({ + url = var.harbor.url + robot_username = var.harbor.robot_username + robot_token = var.harbor.robot_token + }) + } + } + } + } : {}) + outputs = { + "summary" = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } + } + } +} + +output "bbd_uuid" { + description = "UUID of the Forgejo Actions connector building block definition." + value = meshstack_building_block_definition.forgejo_connector.ref.uuid +} + +output "bbd_version_uuid" { + description = "UUID of the latest version of the Forgejo Actions connector building block definition." + value = meshstack_building_block_definition.forgejo_connector.version_latest.uuid +} diff --git a/modules/stackit/ske/meshstack_integration.tf b/modules/stackit/ske/meshstack_integration.tf new file mode 100644 index 0000000..dc6f18c --- /dev/null +++ b/modules/stackit/ske/meshstack_integration.tf @@ -0,0 +1,158 @@ +# Adapt these variables to your STACKIT and meshStack setup. + +variable "hub" { + type = object({ + git_ref = string + }) + default = { + git_ref = "main" + } + description = "Hub release reference. Set git_ref to a tag (e.g. 'v1.2.3') or branch for the meshstack-hub repo." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + }) + description = "Shared meshStack context passed down from the IaC runtime." +} + +variable "ske" { + type = object({ + platform_identifier = string + location_identifier = string + + # Cluster connection + base_url = string + disable_ssl_validation = optional(bool, true) + + # Replication + namespace_name_pattern = optional(string, "#{workspaceIdentifier}-#{projectIdentifier}") + }) + description = "STACKIT Kubernetes Engine platform configuration." +} + +module "backplane" { + source = "./backplane" + + stackit_project_id = var.stackit_project_id + cluster_name = var.cluster_name + region = var.region +} + +variable "stackit_project_id" { + type = string + description = "STACKIT project ID where the SKE cluster will be created." +} + +variable "cluster_name" { + type = string + description = "Name of the SKE cluster." + default = "ske-cluster" +} + +variable "region" { + type = string + description = "STACKIT region for the SKE cluster." + default = "eu01" +} + +resource "meshstack_platform" "ske" { + metadata = { + name = var.ske.platform_identifier + owned_by_workspace = var.meshstack.owning_workspace_identifier + } + + spec = { + description = "STACKIT Kubernetes Engine (SKE). Create a k8s namespace in our SKE cluster." + display_name = "SKE Namespace" + endpoint = var.ske.base_url + + location_ref = { + name = var.ske.location_identifier + } + + availability = { + restriction = "PUBLIC" + publication_state = "PUBLISHED" + } + + config = { + kubernetes = { + base_url = var.ske.base_url + disable_ssl_validation = var.ske.disable_ssl_validation + + replication = { + client_config = { + access_token = { + secret_value = module.backplane.replicator_token + secret_version = sha256(module.backplane.replicator_token) + } + } + namespace_name_pattern = var.ske.namespace_name_pattern + } + + metering = { + client_config = { + access_token = { + secret_value = module.backplane.metering_token + secret_version = sha256(module.backplane.metering_token) + } + } + processing = {} + } + } + } + } +} + +resource "meshstack_landingzone" "ske_default" { + metadata = { + name = "${var.ske.platform_identifier}-default" + owned_by_workspace = var.meshstack.owning_workspace_identifier + } + + spec = { + description = "Default SKE landing zone" + display_name = "SKE Default" + + platform_ref = meshstack_platform.ske.metadata + + automate_deletion_approval = true + automate_deletion_replication = true + + platform_properties = { + kubernetes = { + kubernetes_role_mappings = [ + { + platform_roles = ["admin"] + project_role_ref = { + name = "admin" + } + }, + { + platform_roles = ["edit"] + project_role_ref = { + name = "user" + } + }, + { + platform_roles = ["view"] + project_role_ref = { + name = "reader" + } + } + ] + } + } + } +} + +terraform { + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = "~> 0.19.1" + } + } +} diff --git a/modules/stackit/ske/starterkit/backplane/main.tf b/modules/stackit/ske/starterkit/backplane/main.tf new file mode 100644 index 0000000..874e4d2 --- /dev/null +++ b/modules/stackit/ske/starterkit/backplane/main.tf @@ -0,0 +1,22 @@ +module "git_repo_bbd" { + source = "../../../git-repository" + + owning_workspace_identifier = var.meshstack.owning_workspace_identifier + meshstack_hub_git_ref = var.hub.git_ref + gitea_base_url = var.gitea.base_url + gitea_token = var.gitea.token + gitea_organization = var.gitea.organization +} + +module "forgejo_connector_bbd" { + source = "../../forgejo-connector" + + hub = var.hub + meshstack = var.meshstack + gitea = var.gitea + ske = var.ske + harbor = var.harbor + git_repo_bbd = { + uuid = module.git_repo_bbd.bbd_uuid + } +} diff --git a/modules/stackit/ske/starterkit/backplane/outputs.tf b/modules/stackit/ske/starterkit/backplane/outputs.tf new file mode 100644 index 0000000..05f8c18 --- /dev/null +++ b/modules/stackit/ske/starterkit/backplane/outputs.tf @@ -0,0 +1,14 @@ +output "git_repo_bbd_uuid" { + description = "UUID of the STACKIT Git repository building block definition." + value = module.git_repo_bbd.bbd_uuid +} + +output "git_repo_bbd_version_uuid" { + description = "UUID of the latest version of the STACKIT Git repository building block definition." + value = module.git_repo_bbd.bbd_version_uuid +} + +output "forgejo_connector_bbd_version_uuid" { + description = "UUID of the latest version of the Forgejo Actions connector building block definition." + value = module.forgejo_connector_bbd.bbd_version_uuid +} diff --git a/modules/stackit/ske/starterkit/backplane/variables.tf b/modules/stackit/ske/starterkit/backplane/variables.tf new file mode 100644 index 0000000..4b64faa --- /dev/null +++ b/modules/stackit/ske/starterkit/backplane/variables.tf @@ -0,0 +1,42 @@ +variable "hub" { + type = object({ + git_ref = string + }) + description = "Hub release reference. Set git_ref to a tag (e.g. 'v1.2.3') or branch for the meshstack-hub repo." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + }) + description = "Shared meshStack context passed down from the IaC runtime." +} + +variable "gitea" { + type = object({ + base_url = string + token = string + organization = string + }) + sensitive = true + description = "STACKIT Git (Forgejo) credentials and organization." +} + +variable "ske" { + type = object({ + cluster_server = string + cluster_ca_certificate = string + }) + description = "SKE cluster connection details for kubeconfig generation in the Forgejo connector." +} + +variable "harbor" { + type = object({ + url = string + robot_username = string + robot_token = string + }) + sensitive = true + default = null + description = "Harbor registry credentials. When provided, stores push credentials as Forgejo Actions secrets and creates an image pull secret in each namespace." +} diff --git a/modules/stackit/ske/starterkit/backplane/versions.tf b/modules/stackit/ske/starterkit/backplane/versions.tf new file mode 100644 index 0000000..6c6a9bf --- /dev/null +++ b/modules/stackit/ske/starterkit/backplane/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.19.3" + } + } +} diff --git a/modules/stackit/ske/starterkit/buildingblock/APP_TEAM_README.md b/modules/stackit/ske/starterkit/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..886afd3 --- /dev/null +++ b/modules/stackit/ske/starterkit/buildingblock/APP_TEAM_README.md @@ -0,0 +1,41 @@ +# STACKIT SKE Starterkit — Application Team Guide + +## What does this building block do? + +The STACKIT SKE Starterkit provisions a complete development environment for your team: + +- **Git Repository** on STACKIT Git (Forgejo/Gitea) for your application source code +- **Dev Namespace** on STACKIT Kubernetes Engine for development workloads +- **Prod Namespace** on STACKIT Kubernetes Engine for production workloads + +## When to use it + +Use this starterkit when your team needs to deploy containerized applications on a sovereign +European Kubernetes platform with separate dev and prod environments. + +## Usage + +1. Request the starterkit from the meshStack marketplace +2. Provide a project name — this will be used for naming all resources +3. Once provisioned, clone the Git repository and start developing +4. Set up your own CI/CD pipeline to deploy to the dev and prod namespaces + +## Shared Responsibility Matrix + +| Responsibility | Platform Team | Application Team | +| --------------------------------------- | ------------- | ---------------- | +| Provision and manage SKE cluster | ✅ | ❌ | +| Create Git repository | ✅ | ❌ | +| Create Kubernetes namespaces | ✅ | ❌ | +| Manage access to projects | ✅ | ✅ | +| Deploy applications to namespaces | ❌ | ✅ | +| Develop and maintain application code | ❌ | ✅ | +| Set up CI/CD pipelines | ❌ | ✅ | +| Monitor application health | ❌ | ✅ | + +## Best Practices + +- Use the **dev** namespace for testing before promoting to **prod** +- Set up branch protection rules in the Git repository +- Configure resource limits in your Kubernetes deployments +- Use Kubernetes Secrets for sensitive configuration diff --git a/modules/stackit/ske/starterkit/buildingblock/README.md b/modules/stackit/ske/starterkit/buildingblock/README.md new file mode 100644 index 0000000..41e72c1 --- /dev/null +++ b/modules/stackit/ske/starterkit/buildingblock/README.md @@ -0,0 +1,18 @@ +--- +name: STACKIT SKE Starterkit +supportedPlatforms: + - stackit +description: One-click dev/prod environment with STACKIT Git repository and SKE Kubernetes namespaces. +--- + +# STACKIT SKE Starterkit + +This building block creates a complete dev/prod environment on STACKIT Kubernetes Engine (SKE), +including a Git repository on STACKIT Git and dedicated Kubernetes namespaces. + +## Resources Created + +- 2 meshStack projects (dev + prod) +- 2 SKE tenants (Kubernetes namespaces via meshStack replicator) +- 1 STACKIT Git repository (Forgejo/Gitea) +- Project Admin bindings for the creator diff --git a/modules/stackit/ske/starterkit/buildingblock/logo.png b/modules/stackit/ske/starterkit/buildingblock/logo.png new file mode 100644 index 0000000..d4ce3d0 Binary files /dev/null and b/modules/stackit/ske/starterkit/buildingblock/logo.png differ diff --git a/modules/stackit/ske/starterkit/buildingblock/main.tf b/modules/stackit/ske/starterkit/buildingblock/main.tf new file mode 100644 index 0000000..07f3649 --- /dev/null +++ b/modules/stackit/ske/starterkit/buildingblock/main.tf @@ -0,0 +1,162 @@ +locals { + identifier = lower(replace(replace(var.name, "/[^a-zA-Z0-9\\s\\-\\_]/", ""), "/[\\s\\-\\_]+/", "-")) + project_tags_config = yamldecode(var.project_tags_yaml) + repo_name = "${local.identifier}-${random_id.repo_suffix.hex}" +} + +resource "random_id" "repo_suffix" { + byte_length = 4 +} + +resource "meshstack_project" "dev" { + metadata = { + name = "${local.identifier}-dev" + owned_by_workspace = var.workspace_identifier + } + spec = { + display_name = "${var.name} Dev" + tags = try(local.project_tags_config.dev, {}) + } +} + +resource "meshstack_project" "prod" { + metadata = { + name = "${local.identifier}-prod" + owned_by_workspace = var.workspace_identifier + } + spec = { + display_name = "${var.name} Prod" + tags = try(local.project_tags_config.prod, {}) + } +} + +resource "meshstack_project_user_binding" "creator_dev_admin" { + count = var.creator.type == "User" && var.creator.username != null ? 1 : 0 + + metadata = { + name = uuid() + } + + role_ref = { + name = "Project Admin" + } + + target_ref = { + owned_by_workspace = var.workspace_identifier + name = meshstack_project.dev.metadata.name + } + + subject = { + name = var.creator.username + } +} + +resource "meshstack_project_user_binding" "creator_prod_admin" { + count = var.creator.type == "User" && var.creator.username != null ? 1 : 0 + + metadata = { + name = uuid() + } + + role_ref = { + name = "Project Admin" + } + + target_ref = { + owned_by_workspace = var.workspace_identifier + name = meshstack_project.prod.metadata.name + } + + subject = { + name = var.creator.username + } +} + +resource "meshstack_tenant_v4" "dev" { + metadata = { + owned_by_workspace = var.workspace_identifier + owned_by_project = meshstack_project.dev.metadata.name + } + + spec = { + platform_identifier = var.full_platform_identifier + landing_zone_identifier = var.landing_zone_dev_identifier + } +} + +resource "meshstack_tenant_v4" "prod" { + metadata = { + owned_by_workspace = var.workspace_identifier + owned_by_project = meshstack_project.prod.metadata.name + } + + spec = { + platform_identifier = var.full_platform_identifier + landing_zone_identifier = var.landing_zone_prod_identifier + } +} + +resource "meshstack_building_block_v2" "git_repo" { + spec = { + building_block_definition_version_ref = { + uuid = var.git_repo_definition_version_uuid + } + + display_name = local.identifier + target_ref = { + kind = "meshWorkspace" + identifier = var.workspace_identifier + } + + inputs = { + repository_name = { + value_string = local.repo_name + } + repository_description = { + value_string = "Application repository for ${var.name}" + } + repository_private = { + value_bool = true + } + use_template = { + value_bool = false + } + } + } +} + +resource "meshstack_building_block_v2" "forgejo_connector_dev" { + spec = { + building_block_definition_version_ref = { + uuid = var.forgejo_connector_definition_version_uuid + } + target_ref = { + kind = "meshTenant" + uuid = meshstack_tenant_v4.dev.metadata.uuid + } + display_name = "Forgejo Connector Dev" + parent_building_blocks = [{ + buildingblock_uuid = meshstack_building_block_v2.git_repo.metadata.uuid + definition_uuid = var.git_repo_definition_uuid + }] + } +} + +resource "meshstack_building_block_v2" "forgejo_connector_prod" { + depends_on = [meshstack_building_block_v2.forgejo_connector_dev] + + spec = { + building_block_definition_version_ref = { + uuid = var.forgejo_connector_definition_version_uuid + } + target_ref = { + kind = "meshTenant" + uuid = meshstack_tenant_v4.prod.metadata.uuid + } + display_name = "Forgejo Connector Prod" + parent_building_blocks = [{ + buildingblock_uuid = meshstack_building_block_v2.git_repo.metadata.uuid + definition_uuid = var.git_repo_definition_uuid + }] + } +} diff --git a/modules/stackit/ske/starterkit/buildingblock/outputs.tf b/modules/stackit/ske/starterkit/buildingblock/outputs.tf new file mode 100644 index 0000000..3c76f4a --- /dev/null +++ b/modules/stackit/ske/starterkit/buildingblock/outputs.tf @@ -0,0 +1,56 @@ +output "git_repo_url" { + description = "URL of the created STACKIT Git repository." + value = meshstack_building_block_v2.git_repo.status.outputs.repository_html_url.value_string +} + +output "summary" { + description = "Summary with next steps and insights into created resources." + value = <<-EOT +# STACKIT Starter Kit + +✅ **Your environment is ready!** + +This starter kit has set up the following resources in workspace `${var.workspace_identifier}`: + +@buildingblock[${meshstack_building_block_v2.git_repo.metadata.uuid}] + +@project[${meshstack_project.dev.metadata.owned_by_workspace}.${meshstack_project.dev.metadata.name}]\ +    @tenant[${meshstack_tenant_v4.dev.metadata.uuid}] +    @buildingblock[${meshstack_building_block_v2.forgejo_connector_dev.metadata.uuid}] + +@project[${meshstack_project.prod.metadata.owned_by_workspace}.${meshstack_project.prod.metadata.name}]\ +    @tenant[${meshstack_tenant_v4.prod.metadata.uuid}] +    @buildingblock[${meshstack_building_block_v2.forgejo_connector_prod.metadata.uuid}] + +--- + +## What's Included + +- **Git Repository**: `${local.repo_name}` on STACKIT Git +- **Dev Project**: `${meshstack_project.dev.metadata.name}` with SKE namespace + - Forgejo Actions CI/CD connector for development deployments +- **Prod Project**: `${meshstack_project.prod.metadata.name}` with SKE namespace + - Forgejo Actions CI/CD connector for production deployments + +--- + +## Next Steps + +### 1. Develop +- Clone the repository and start developing +- Push changes to deploy to your Kubernetes namespaces via Forgejo Actions + +### 2. Access SKE Namespaces +- [Dev Namespace](/#/w/${var.workspace_identifier}/p/${meshstack_project.dev.metadata.name}/tenants) +- [Prod Namespace](/#/w/${var.workspace_identifier}/p/${meshstack_project.prod.metadata.name}/tenants) + +### 3. Manage Access +- Invite team members via meshStack: + - [Dev Access](#/w/${var.workspace_identifier}/p/${meshstack_project.dev.metadata.name}/access-management/role-mapping/overview) + - [Prod Access](#/w/${var.workspace_identifier}/p/${meshstack_project.prod.metadata.name}/access-management/role-mapping/overview) + +--- + +🎉 Happy coding! +EOT +} diff --git a/modules/stackit/ske/starterkit/buildingblock/variables.tf b/modules/stackit/ske/starterkit/buildingblock/variables.tf new file mode 100644 index 0000000..ad5f38a --- /dev/null +++ b/modules/stackit/ske/starterkit/buildingblock/variables.tf @@ -0,0 +1,86 @@ +variable "workspace_identifier" { + type = string + description = "meshStack workspace identifier." +} + +variable "name" { + type = string + description = "This name will be used for the created projects and STACKIT Git repository." +} + +variable "full_platform_identifier" { + type = string + description = "Full platform identifier of the SKE Namespace platform." +} + +variable "landing_zone_dev_identifier" { + type = string + description = "SKE landing zone identifier for the development tenant." +} + +variable "landing_zone_prod_identifier" { + type = string + description = "SKE landing zone identifier for the production tenant." +} + +variable "git_repo_definition_version_uuid" { + type = string + description = "UUID of the STACKIT Git repository building block definition version." +} + +variable "git_repo_definition_uuid" { + type = string + description = "UUID of the STACKIT Git repository building block definition." +} + +variable "forgejo_connector_definition_version_uuid" { + type = string + description = "UUID of the Forgejo Actions connector building block definition version." +} + +variable "creator" { + type = object({ + type = string + identifier = string + displayName = string + username = optional(string) + email = optional(string) + euid = optional(string) + }) + description = "Information about the creator of the resources who will be assigned Project Admin role." +} + +variable "project_tags_yaml" { + type = string + description = <