Skip to content

Commit 2da9381

Browse files
JohannesRudolphgrubmeshi
authored andcommitted
feat: implement STACKIT Git Repository building block with template and webhook support
1 parent 4fb4064 commit 2da9381

16 files changed

Lines changed: 753 additions & 130 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# STACKIT Git Repository – Backplane
2+
3+
This module sets up the shared backplane configuration for the STACKIT Git Repository building block. It validates the API token and exposes credentials as outputs for use by individual building block instances. Currently there's nothing we can automate here using terraform, so this only validates your API token.
4+
5+
## Prerequisites
6+
7+
A Personal Access Token from STACKIT Git is required:
8+
9+
1. Log in to [STACKIT Git](https://git-service.git.onstackit.cloud)
10+
2. Go to **Settings → Applications → Manage Access Tokens**
11+
3. Generate a new token with the following scopes:
12+
- `write:repository` – create and manage repositories
13+
- `write:organization` – manage organization repositories
14+
- `read:user` – retrieve user information
15+
4. Copy the token (shown only once)
16+
17+
## Usage
18+
19+
```hcl
20+
module "git_repo_backplane" {
21+
source = "./backplane"
22+
23+
gitea_base_url = "https://git-service.git.onstackit.cloud"
24+
gitea_token = var.stackit_git_token
25+
gitea_organization = "my-platform-org"
26+
}
27+
```
28+
29+
## Inputs
30+
31+
| Name | Description | Type | Required |
32+
|------|-------------|------|----------|
33+
| `gitea_base_url` | STACKIT Git base URL | `string` | no (default provided) |
34+
| `gitea_token` | STACKIT Git Personal Access Token | `string` | yes |
35+
| `gitea_organization` | Default organization for repository creation | `string` | yes |
36+
37+
## Outputs
38+
39+
| Name | Description |
40+
|------|-------------|
41+
| `gitea_base_url` | STACKIT Git base URL |
42+
| `gitea_token` | STACKIT Git API token (sensitive) |
43+
| `gitea_organization` | Default STACKIT Git organization |
44+
45+
<!-- BEGIN_TF_DOCS -->
46+
## Requirements
47+
48+
| Name | Version |
49+
|------|---------|
50+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3.0 |
51+
| <a name="requirement_null"></a> [null](#requirement\_null) | ~> 3.2.3 |
52+
53+
## Modules
54+
55+
No modules.
56+
57+
## Resources
58+
59+
| Name | Type |
60+
|------|------|
61+
| [null_resource.validate_token](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
62+
63+
## Inputs
64+
65+
| Name | Description | Type | Default | Required |
66+
|------|-------------|------|---------|:--------:|
67+
| <a name="input_gitea_base_url"></a> [gitea\_base\_url](#input\_gitea\_base\_url) | STACKIT Git base URL | `string` | `"https://git-service.git.onstackit.cloud"` | no |
68+
| <a name="input_gitea_organization"></a> [gitea\_organization](#input\_gitea\_organization) | Default STACKIT Git organization where repositories will be created | `string` | n/a | yes |
69+
| <a name="input_gitea_token"></a> [gitea\_token](#input\_gitea\_token) | STACKIT Git Personal Access Token with write:repository, write:organization, and read:user scopes | `string` | n/a | yes |
70+
71+
## Outputs
72+
73+
| Name | Description |
74+
|------|-------------|
75+
| <a name="output_gitea_base_url"></a> [gitea\_base\_url](#output\_gitea\_base\_url) | STACKIT Git base URL |
76+
| <a name="output_gitea_organization"></a> [gitea\_organization](#output\_gitea\_organization) | Default STACKIT Git organization for repository creation |
77+
| <a name="output_gitea_token"></a> [gitea\_token](#output\_gitea\_token) | STACKIT Git API token for use by building block instances |
78+
<!-- END_TF_DOCS -->
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Validate that the API token is operational by checking the STACKIT Git API
2+
resource "null_resource" "validate_token" {
3+
triggers = {
4+
base_url = var.gitea_base_url
5+
token = sha256(var.gitea_token)
6+
}
7+
8+
provisioner "local-exec" {
9+
command = <<-EOT
10+
response=$(curl -s -o /dev/null -w "%\{http_code\}" \
11+
-H "Authorization: token ${var.gitea_token}" \
12+
"${var.gitea_base_url}/api/v1/user")
13+
14+
if [ "$response" -ne 200 ]; then
15+
echo "ERROR: STACKIT Git API token validation failed (HTTP $response)"
16+
echo "Please verify the token has the required scopes: read:user, write:repository, write:organization"
17+
exit 1
18+
fi
19+
20+
echo "STACKIT Git API token validated successfully"
21+
EOT
22+
}
23+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
output "gitea_base_url" {
2+
value = var.gitea_base_url
3+
description = "STACKIT Git base URL"
4+
}
5+
6+
output "gitea_token" {
7+
value = var.gitea_token
8+
description = "STACKIT Git API token for use by building block instances"
9+
sensitive = true
10+
}
11+
12+
output "gitea_organization" {
13+
value = var.gitea_organization
14+
description = "Default STACKIT Git organization for repository creation"
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
variable "gitea_base_url" {
2+
type = string
3+
description = "STACKIT Git base URL"
4+
default = "https://git-service.git.onstackit.cloud"
5+
}
6+
7+
variable "gitea_token" {
8+
type = string
9+
description = "STACKIT Git Personal Access Token with write:repository, write:organization, and read:user scopes"
10+
sensitive = true
11+
}
12+
13+
variable "gitea_organization" {
14+
type = string
15+
description = "Default STACKIT Git organization where repositories will be created"
16+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
terraform {
2+
required_version = ">= 1.3.0"
3+
4+
required_providers {
5+
null = {
6+
source = "hashicorp/null"
7+
version = "~> 3.2.3"
8+
}
9+
}
10+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
This building block provisions a **Git repository on STACKIT Git** — the platform's self-hosted Forgejo/Gitea instance at [git-service.git.onstackit.cloud](https://git-service.git.onstackit.cloud). Within seconds you get a fully configured repository, optionally pre-populated from an application template, with CI/CD webhooks ready to fire 🚀.
2+
3+
## 🎯 Who is this for?
4+
5+
This building block is for **application teams** that:
6+
7+
- Need a Git repository hosted on STACKIT infrastructure (data sovereignty, no external SaaS)
8+
- Want to start from a **pre-configured application template** (e.g., Python, Node.js) with Dockerfile, Kubernetes manifests, and CI/CD pipelines already in place
9+
- Are integrating with **Argo Workflows** for automated container builds triggered on every `git push`
10+
- Require a private repository within a shared STACKIT Git organization
11+
12+
## 🛠️ Usage Examples
13+
14+
### Example 1 – Empty private repository
15+
16+
Use this when you already have existing code to push or prefer a clean slate.
17+
18+
```bash
19+
# After the building block is provisioned, clone and start working:
20+
git clone https://git-service.git.onstackit.cloud/my-org/payments-service.git
21+
cd payments-service
22+
git add .
23+
git commit -m "Initial commit"
24+
git push origin main
25+
```
26+
27+
### Example 2 – Repository from Python application template
28+
29+
Use this when starting a new application. The `app-template-python` template provides a ready-to-run Python app with Docker and Kubernetes manifests.
30+
31+
```
32+
# After provisioning, your repo contains:
33+
your-repo/
34+
├── app/
35+
│ ├── Dockerfile ← Container build instructions
36+
│ ├── requirements.txt ← Python dependencies
37+
│ └── main.py ← Application entrypoint
38+
└── manifests/
39+
└── base/
40+
├── kustomization.yaml
41+
├── deployment.yaml
42+
└── service.yaml
43+
```
44+
45+
Clone it, modify `app/main.py`, and push — an Argo Workflows build will trigger automatically if a webhook was configured.
46+
47+
## 🤝 Shared Responsibility
48+
49+
| Responsibility | Platform Team | Application Team |
50+
|---|:---:|:---:|
51+
| Provision STACKIT Git infrastructure |||
52+
| Create and configure the repository |||
53+
| Set up webhooks for CI/CD |||
54+
| Manage STACKIT Git API tokens |||
55+
| Develop and maintain application source code |||
56+
| Commit and push code changes |||
57+
| Manage branches and pull requests |||
58+
| Review and merge code |||
59+
| Application runtime security & updates |||
60+
61+
## ℹ️ Additional Resources
62+
63+
- STACKIT Git: [https://git-service.git.onstackit.cloud](https://git-service.git.onstackit.cloud)
64+
- Contact the platform team for infrastructure issues or token access
65+
- Check Argo Workflows UI for build status after pushing code
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
name: STACKIT Git Repository
3+
supportedPlatforms:
4+
- stackit
5+
description: Provisions a Git repository on STACKIT Git (Forgejo/Gitea) with optional template initialization, webhook configuration, and CI/CD integration.
6+
---
7+
8+
<!-- BEGIN_TF_DOCS -->
9+
## Requirements
10+
11+
| Name | Version |
12+
|------|---------|
13+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3.0 |
14+
| <a name="requirement_gitea"></a> [gitea](#requirement\_gitea) | ~> 0.16.0 |
15+
| <a name="requirement_null"></a> [null](#requirement\_null) | ~> 3.2.3 |
16+
17+
## Modules
18+
19+
No modules.
20+
21+
## Resources
22+
23+
| Name | Type |
24+
|------|------|
25+
| [gitea_repository.repo](https://registry.terraform.io/providers/Lerentis/gitea/latest/docs/resources/repository) | resource |
26+
| [null_resource.template_repo](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
27+
| [null_resource.webhook](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
28+
29+
## Inputs
30+
31+
| Name | Description | Type | Default | Required |
32+
|------|-------------|------|---------|:--------:|
33+
| <a name="input_default_branch"></a> [default\_branch](#input\_default\_branch) | Default branch name | `string` | `"main"` | no |
34+
| <a name="input_gitea_base_url"></a> [gitea\_base\_url](#input\_gitea\_base\_url) | STACKIT Git base URL | `string` | `"https://git-service.git.onstackit.cloud"` | no |
35+
| <a name="input_gitea_organization"></a> [gitea\_organization](#input\_gitea\_organization) | STACKIT Git organization where the repository will be created | `string` | n/a | yes |
36+
| <a name="input_gitea_token"></a> [gitea\_token](#input\_gitea\_token) | STACKIT Git API token (from backplane) | `string` | n/a | yes |
37+
| <a name="input_repository_auto_init"></a> [repository\_auto\_init](#input\_repository\_auto\_init) | Auto-initialize the repository with a README | `bool` | `true` | no |
38+
| <a name="input_repository_description"></a> [repository\_description](#input\_repository\_description) | Short description of the repository | `string` | `""` | no |
39+
| <a name="input_repository_name"></a> [repository\_name](#input\_repository\_name) | Name of the Git repository to create | `string` | n/a | yes |
40+
| <a name="input_repository_private"></a> [repository\_private](#input\_repository\_private) | Whether the repository should be private | `bool` | `true` | no |
41+
| <a name="input_template_name"></a> [template\_name](#input\_template\_name) | Name of the template repository | `string` | `"app-template-python"` | no |
42+
| <a name="input_template_namespace"></a> [template\_namespace](#input\_template\_namespace) | Value for the NAMESPACE variable used during template substitution | `string` | `""` | no |
43+
| <a name="input_template_owner"></a> [template\_owner](#input\_template\_owner) | Owner/organization of the template repository | `string` | `"stackit"` | no |
44+
| <a name="input_template_repo_name"></a> [template\_repo\_name](#input\_template\_repo\_name) | Value for the REPO\_NAME variable used during template substitution | `string` | `""` | no |
45+
| <a name="input_use_template"></a> [use\_template](#input\_use\_template) | Create repository from a template repository instead of creating an empty one | `bool` | `false` | no |
46+
| <a name="input_webhook_events"></a> [webhook\_events](#input\_webhook\_events) | List of Gitea events that trigger the webhook | `list(string)` | <pre>[<br> "push",<br> "create"<br>]</pre> | no |
47+
| <a name="input_webhook_secret"></a> [webhook\_secret](#input\_webhook\_secret) | Secret for webhook authentication | `string` | `""` | no |
48+
| <a name="input_webhook_url"></a> [webhook\_url](#input\_webhook\_url) | Webhook URL to configure (e.g., Argo Workflows EventSource URL). Leave empty to skip. | `string` | `""` | no |
49+
50+
## Outputs
51+
52+
| Name | Description |
53+
|------|-------------|
54+
| <a name="output_repository_clone_url"></a> [repository\_clone\_url](#output\_repository\_clone\_url) | HTTPS clone URL |
55+
| <a name="output_repository_html_url"></a> [repository\_html\_url](#output\_repository\_html\_url) | Web URL of the repository |
56+
| <a name="output_repository_id"></a> [repository\_id](#output\_repository\_id) | The ID of the created repository |
57+
| <a name="output_repository_name"></a> [repository\_name](#output\_repository\_name) | Name of the created repository |
58+
| <a name="output_repository_ssh_url"></a> [repository\_ssh\_url](#output\_repository\_ssh\_url) | SSH clone URL |
59+
| <a name="output_summary"></a> [summary](#output\_summary) | Summary with next steps and links for the created repository |
60+
<!-- END_TF_DOCS -->
51.8 KB
Loading
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
locals {
2+
owner = var.gitea_organization
3+
4+
clone_url = "${var.gitea_base_url}/${local.owner}/${var.repository_name}.git"
5+
html_url = "${var.gitea_base_url}/${local.owner}/${var.repository_name}"
6+
ssh_url = "git@${replace(var.gitea_base_url, "https://", "")}:${local.owner}/${var.repository_name}.git"
7+
8+
template_variables = {
9+
REPO_NAME = var.template_repo_name != "" ? var.template_repo_name : var.repository_name
10+
NAMESPACE = var.template_namespace != "" ? var.template_namespace : var.repository_name
11+
CLONE_URL = local.clone_url
12+
}
13+
}
14+
15+
# ── Repository (empty, non-template) ──────────────────────────────────────────
16+
17+
resource "gitea_repository" "repo" {
18+
count = var.use_template ? 0 : 1
19+
20+
username = local.owner
21+
name = var.repository_name
22+
description = var.repository_description
23+
private = var.repository_private
24+
auto_init = var.repository_auto_init
25+
default_branch = var.default_branch
26+
}
27+
28+
# ── Repository (from template, via Forgejo API) ───────────────────────────────
29+
30+
resource "null_resource" "template_repo" {
31+
count = var.use_template ? 1 : 0
32+
33+
triggers = {
34+
repo_name = var.repository_name
35+
owner = local.owner
36+
template_owner = var.template_owner
37+
template_name = var.template_name
38+
template_vars = jsonencode(local.template_variables)
39+
}
40+
41+
provisioner "local-exec" {
42+
command = <<-EOT
43+
response=$(curl -s -w "\n%\{http_code\}" \
44+
-X POST "${var.gitea_base_url}/api/v1/repos/${var.template_owner}/${var.template_name}/generate" \
45+
-H "Authorization: token ${var.gitea_token}" \
46+
-H "Content-Type: application/json" \
47+
-d '{
48+
"owner": "${local.owner}",
49+
"name": "${var.repository_name}",
50+
"description": "${var.repository_description}",
51+
"private": ${var.repository_private},
52+
"git_content": true,
53+
"git_hooks": false
54+
}')
55+
56+
http_code=$(echo "$response" | tail -n1)
57+
body=$(echo "$response" | sed '$d')
58+
59+
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
60+
echo "Repository '${var.repository_name}' created from template '${var.template_owner}/${var.template_name}'"
61+
else
62+
echo "Failed to create repository from template. HTTP $http_code"
63+
echo "$body"
64+
exit 1
65+
fi
66+
EOT
67+
}
68+
}
69+
70+
# ── Webhook ────────────────────────────────────────────────────────────────────
71+
72+
resource "null_resource" "webhook" {
73+
count = var.webhook_url != "" ? 1 : 0
74+
75+
triggers = {
76+
webhook_url = var.webhook_url
77+
webhook_secret = sha256(var.webhook_secret)
78+
webhook_events = join(",", var.webhook_events)
79+
repo = "${local.owner}/${var.repository_name}"
80+
}
81+
82+
provisioner "local-exec" {
83+
command = <<-EOT
84+
curl -s -X POST "${var.gitea_base_url}/api/v1/repos/${local.owner}/${var.repository_name}/hooks" \
85+
-H "Authorization: token ${var.gitea_token}" \
86+
-H "Content-Type: application/json" \
87+
-d '{
88+
"type": "forgejo",
89+
"config": {
90+
"url": "${var.webhook_url}",
91+
"content_type": "json",
92+
"secret": "${var.webhook_secret}"
93+
},
94+
"events": ${jsonencode(var.webhook_events)},
95+
"active": true
96+
}'
97+
EOT
98+
}
99+
100+
depends_on = [gitea_repository.repo, null_resource.template_repo]
101+
}

0 commit comments

Comments
 (0)