Document reuse-surface runtime secrets and Forgejo org webhook
Add runbook coverage for REUSE_SURFACE_TOKEN and REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET in reuse-surface-env, plus an idempotent make reuse-forgejo-webhook target that wires the coulomb org push webhook to the hub receiver.
This commit is contained in:
parent
8f4486f978
commit
9b31f229e8
4 changed files with 177 additions and 14 deletions
23
Makefile
23
Makefile
|
|
@ -246,6 +246,24 @@ forgejo-status: check-railiance01-kubeconfig ## Show Forgejo pods, svc, ingress,
|
||||||
forgejo-smoke: ## Verify Forgejo web and OCI registry challenge
|
forgejo-smoke: ## Verify Forgejo web and OCI registry challenge
|
||||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-smoke.sh
|
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-smoke.sh
|
||||||
|
|
||||||
|
forgejo-verify: check-railiance01-kubeconfig ## T05 acceptance: smoke, TLS, DB, mailer ESO, SSH operator, SOPS sentinels
|
||||||
|
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-verify.sh
|
||||||
|
|
||||||
|
forgejo-operator-bootstrap: ## Idempotent operator user + SSH key bootstrap (no secrets in Git)
|
||||||
|
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-operator-bootstrap.sh
|
||||||
|
|
||||||
|
forgejo-runner-registration-sops-bootstrap: check-railiance01-kubeconfig ## Capture cluster runner token into SOPS (operator age key)
|
||||||
|
KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-runner-registration-sops-bootstrap.sh
|
||||||
|
|
||||||
|
forgejo-secrets-check: ## Verify Forgejo SOPS sentinels exist and are encrypted
|
||||||
|
@test -f $(FORGEJO_SECRETS) && grep -q '^sops:' $(FORGEJO_SECRETS) || { echo "Missing encrypted $(FORGEJO_SECRETS)" >&2; exit 1; }
|
||||||
|
@if [ -f $(FORGEJO_RUNNER_REGISTRATION_SECRET) ]; then \
|
||||||
|
grep -q '^sops:' $(FORGEJO_RUNNER_REGISTRATION_SECRET) || { echo "Runner registration not SOPS-encrypted" >&2; exit 1; }; \
|
||||||
|
else \
|
||||||
|
echo "WARN: $(FORGEJO_RUNNER_REGISTRATION_SECRET) missing — run forgejo-runner-registration-sops-bootstrap on operator host" >&2; \
|
||||||
|
fi
|
||||||
|
@echo "ok: Forgejo SOPS sentinels"
|
||||||
|
|
||||||
forgejo-logs: check-railiance01-kubeconfig ## Tail Forgejo application logs
|
forgejo-logs: check-railiance01-kubeconfig ## Tail Forgejo application logs
|
||||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/instance=$(FORGEJO_RELEASE) -f --tail=50
|
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/instance=$(FORGEJO_RELEASE) -f --tail=50
|
||||||
|
|
||||||
|
|
@ -292,6 +310,9 @@ reuse-smoke: check-railiance01-kubeconfig ## Verify reuse public health, repo li
|
||||||
reuse-logs: check-railiance01-kubeconfig ## Tail reuse-surface service logs from Railiance01
|
reuse-logs: check-railiance01-kubeconfig ## Tail reuse-surface service logs from Railiance01
|
||||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl logs -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) -f --tail=50
|
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl logs -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) -f --tail=50
|
||||||
|
|
||||||
|
reuse-forgejo-webhook: check-railiance01-kubeconfig ## Idempotent org webhook on Forgejo -> reuse hub (REUSE-WP-0019-T03)
|
||||||
|
bash tools/reuse-forgejo-webhook.sh
|
||||||
|
|
||||||
##@ Help
|
##@ Help
|
||||||
|
|
||||||
help: ## Show this help
|
help: ## Show this help
|
||||||
|
|
@ -299,4 +320,4 @@ help: ## Show this help
|
||||||
/^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
|
/^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
|
||||||
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
|
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs help
|
.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-verify forgejo-operator-bootstrap forgejo-runner-registration-sops-bootstrap forgejo-secrets-check forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs help
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,74 @@ export REUSE_SURFACE_URL=http://127.0.0.1:18001
|
||||||
| Values | `helm/reuse-surface-values.yaml` |
|
| Values | `helm/reuse-surface-values.yaml` |
|
||||||
| Image | `gitea.coulomb.social/coulomb/reuse-surface:<tag>` |
|
| Image | `gitea.coulomb.social/coulomb/reuse-surface:<tag>` |
|
||||||
| Landing image | `nginxinc/nginx-unprivileged:1.27-alpine` |
|
| Landing image | `nginxinc/nginx-unprivileged:1.27-alpine` |
|
||||||
| Secret | `reuse-surface-env` (`REUSE_SURFACE_TOKEN`) |
|
| Secret | `reuse-surface-env` (see [Runtime secrets](#runtime-secrets)) |
|
||||||
|
|
||||||
|
## Runtime secrets
|
||||||
|
|
||||||
|
Custody is the Kubernetes Secret `reuse-surface-env` in namespace `reuse` on
|
||||||
|
Railiance01. This is **not** an OpenBao KV path today — see
|
||||||
|
**RAILIANCE-WP-0011** for a planned OpenBao + External Secrets migration.
|
||||||
|
|
||||||
|
| Key | Required | Purpose |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `REUSE_SURFACE_TOKEN` | yes | Bearer token for hub write API (`POST /v1/repos`, compose, etc.) |
|
||||||
|
| `REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET` | for webhooks | HMAC secret for `POST /v1/webhooks/forgejo` (REUSE-WP-0019-T02) |
|
||||||
|
|
||||||
|
The chart injects both keys via `envFrom.secretRef` on the `reuse-surface`
|
||||||
|
Deployment. After patching the Secret, restart the Deployment so new values
|
||||||
|
are picked up:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl --kubeconfig ~/.kube/config-hosteurope rollout restart deployment/reuse-surface -n reuse
|
||||||
|
kubectl --kubeconfig ~/.kube/config-hosteurope rollout status deployment/reuse-surface -n reuse
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create or rotate** (generate values locally — never commit or paste into chat):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl create namespace reuse --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
kubectl create secret generic reuse-surface-env \
|
||||||
|
--namespace reuse \
|
||||||
|
--from-literal=REUSE_SURFACE_TOKEN="$(openssl rand -hex 32)" \
|
||||||
|
--from-literal=REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET="$(openssl rand -hex 32)" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
kubectl --kubeconfig ~/.kube/config-hosteurope rollout restart deployment/reuse-surface -n reuse
|
||||||
|
```
|
||||||
|
|
||||||
|
**Export for a shell session** (streams to your terminal only):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export REUSE_SURFACE_TOKEN=$(
|
||||||
|
kubectl --kubeconfig ~/.kube/config-hosteurope get secret reuse-surface-env -n reuse \
|
||||||
|
-o jsonpath='{.data.REUSE_SURFACE_TOKEN}' | base64 -d
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or proxy via ops-warden: `warden access reuse-surface-hub-write-token --fetch`.
|
||||||
|
|
||||||
|
## Forgejo org webhook
|
||||||
|
|
||||||
|
Org-level push webhook on Forgejo (`coulomb`) triggers hub recompose when any
|
||||||
|
repo changes paths under `registry/indexes/`. The HMAC secret must match
|
||||||
|
`REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET` in `reuse-surface-env`.
|
||||||
|
|
||||||
|
Requires a Forgejo admin API token at `/tmp/forgejo-tegwick-api-token` (or
|
||||||
|
`FORGEJO_ADMIN_TOKEN`). Idempotent setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make reuse-forgejo-webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual verification (unsigned body should return `401`, not `503`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS -o /dev/null -w "%{http_code}\n" -X POST \
|
||||||
|
https://reuse.coulomb.social/v1/webhooks/forgejo \
|
||||||
|
-H "Content-Type: application/json" -d '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Live status (2026-07-07): org webhook id `1` on `coulomb` →
|
||||||
|
`https://reuse.coulomb.social/v1/webhooks/forgejo`, push events only.
|
||||||
|
|
||||||
## Browser landing page
|
## Browser landing page
|
||||||
|
|
||||||
|
|
@ -71,15 +138,10 @@ the API service.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Pin image tag in helm/reuse-surface-values.yaml
|
# 1. Pin image tag in helm/reuse-surface-values.yaml
|
||||||
# 2. Create secret (example — use SOPS handoff in production)
|
# 2. Ensure reuse-surface-env has both keys (see Runtime secrets above)
|
||||||
kubectl create namespace reuse --dry-run=client -o yaml | kubectl apply -f -
|
# 3. Production (Railiance01, defaults to ~/.kube/config-hosteurope)
|
||||||
kubectl create secret generic reuse-surface-env \
|
|
||||||
--namespace reuse \
|
|
||||||
--from-literal=REUSE_SURFACE_TOKEN='<token>' \
|
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
|
||||||
|
|
||||||
# Production (Railiance01, defaults to ~/.kube/config-hosteurope)
|
|
||||||
make reuse-deploy
|
make reuse-deploy
|
||||||
|
make reuse-forgejo-webhook # after webhook secret is in the cluster Secret
|
||||||
make reuse-status
|
make reuse-status
|
||||||
|
|
||||||
# Restore kubeconfig from the node if missing:
|
# Restore kubeconfig from the node if missing:
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Production overrides for reuse-surface federation hub.
|
# Production overrides for reuse-surface federation hub.
|
||||||
# REUSE_SURFACE_TOKEN is supplied via Secret reuse-surface-env.
|
# Runtime secrets (Secret reuse-surface-env, namespace reuse):
|
||||||
# REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET (REUSE-WP-0019-T02, POST
|
# REUSE_SURFACE_TOKEN — hub write API bearer token
|
||||||
# /v1/webhooks/forgejo) is the same Secret's job -- not yet added as of
|
# REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET — HMAC for POST /v1/webhooks/forgejo
|
||||||
# this image; the webhook endpoint returns 503 until it is.
|
# See docs/reuse-surface-on-railiance01.md#runtime-secrets and make reuse-forgejo-webhook.
|
||||||
|
|
||||||
image:
|
image:
|
||||||
tag: "e3ae22e"
|
tag: "e3ae22e"
|
||||||
|
|
|
||||||
80
tools/reuse-forgejo-webhook.sh
Executable file
80
tools/reuse-forgejo-webhook.sh
Executable file
|
|
@ -0,0 +1,80 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Idempotent org-level Forgejo webhook for reuse-surface hub recompose (REUSE-WP-0019-T03).
|
||||||
|
# Reads the HMAC secret from the live cluster Secret — never prints it.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
FORGEJO_API="${FORGEJO_API:-https://forgejo.coulomb.social/api/v1}"
|
||||||
|
FORGEJO_ORG="${FORGEJO_ORG:-coulomb}"
|
||||||
|
WEBHOOK_URL="${REUSE_WEBHOOK_URL:-https://reuse.coulomb.social/v1/webhooks/forgejo}"
|
||||||
|
KUBECONFIG_PATH="${KUBECONFIG:-$HOME/.kube/config-hosteurope}"
|
||||||
|
SECRET_NS="${REUSE_SECRET_NAMESPACE:-reuse}"
|
||||||
|
SECRET_NAME="${REUSE_SECRET_NAME:-reuse-surface-env}"
|
||||||
|
SECRET_KEY="${REUSE_WEBHOOK_SECRET_KEY:-REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET}"
|
||||||
|
TOKEN_FILE="${FORGEJO_TOKEN_FILE:-/tmp/forgejo-tegwick-api-token}"
|
||||||
|
|
||||||
|
if [[ -n "${FORGEJO_ADMIN_TOKEN:-}" ]]; then
|
||||||
|
TOKEN="${FORGEJO_ADMIN_TOKEN}"
|
||||||
|
elif [[ -f "${TOKEN_FILE}" ]]; then
|
||||||
|
TOKEN="$(cat "${TOKEN_FILE}")"
|
||||||
|
else
|
||||||
|
echo "Set FORGEJO_ADMIN_TOKEN or create ${TOKEN_FILE}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
WEBHOOK_SECRET="$(
|
||||||
|
kubectl --kubeconfig "${KUBECONFIG_PATH}" get secret "${SECRET_NAME}" -n "${SECRET_NS}" \
|
||||||
|
-o "jsonpath={.data.${SECRET_KEY}}" | base64 -d
|
||||||
|
)"
|
||||||
|
if [[ -z "${WEBHOOK_SECRET}" ]]; then
|
||||||
|
echo "Missing ${SECRET_KEY} in ${SECRET_NS}/${SECRET_NAME}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export FORGEJO_API FORGEJO_ORG WEBHOOK_URL TOKEN WEBHOOK_SECRET
|
||||||
|
python3 <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
api = os.environ["FORGEJO_API"].rstrip("/")
|
||||||
|
org = os.environ["FORGEJO_ORG"]
|
||||||
|
url = os.environ["WEBHOOK_URL"]
|
||||||
|
token = os.environ["TOKEN"]
|
||||||
|
secret = os.environ["WEBHOOK_SECRET"]
|
||||||
|
headers = {"Authorization": f"token {token}"}
|
||||||
|
|
||||||
|
def api_json(method: str, path: str, payload: dict | None = None):
|
||||||
|
data = None if payload is None else json.dumps(payload).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{api}{path}",
|
||||||
|
data=data,
|
||||||
|
headers={**headers, **({} if payload is None else {"Content-Type": "application/json"})},
|
||||||
|
method=method,
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
return json.load(resp)
|
||||||
|
|
||||||
|
hooks = api_json("GET", f"/orgs/{org}/hooks")
|
||||||
|
for hook in hooks:
|
||||||
|
cfg = hook.get("config") or {}
|
||||||
|
if cfg.get("url") == url and hook.get("active") and "push" in (hook.get("events") or []):
|
||||||
|
print(f"==> Forgejo org webhook already configured (id={hook['id']})")
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
created = api_json(
|
||||||
|
"POST",
|
||||||
|
f"/orgs/{org}/hooks",
|
||||||
|
{
|
||||||
|
"type": "gitea",
|
||||||
|
"config": {
|
||||||
|
"url": url,
|
||||||
|
"content_type": "json",
|
||||||
|
"secret": secret,
|
||||||
|
},
|
||||||
|
"events": ["push"],
|
||||||
|
"active": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print(f"==> Forgejo org webhook created (id={created['id']}) -> {url}")
|
||||||
|
PY
|
||||||
Loading…
Add table
Add a link
Reference in a new issue