From 3e29bc964d35f5f3a34936e6ba53e5335acd4732 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 19 Jun 2026 21:05:18 +0200 Subject: [PATCH] Add railiance01 deployment artifacts and fix container image build Introduce Dockerfile, entrypoint, and k8s/railiance manifests for the ArgoCD GitOps pilot (ISSUE-WP-0003). Rename the Gitea PyPI build arg to GITEA_PYPI_INDEX_URL so pip still resolves dependencies from PyPI. --- Dockerfile | 33 +++ docker-entrypoint.sh | 30 +++ k8s/railiance/configmap-backends.yaml | 24 ++ k8s/railiance/deployment.yaml | 71 ++++++ k8s/railiance/externalsecret.yaml | 37 +++ k8s/railiance/kustomization.yaml | 12 + k8s/railiance/service.yaml | 19 ++ .../ISSUE-WP-0003-railiance01-deployment.md | 235 ++++++++++++++++++ 8 files changed, 461 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-entrypoint.sh create mode 100644 k8s/railiance/configmap-backends.yaml create mode 100644 k8s/railiance/deployment.yaml create mode 100644 k8s/railiance/externalsecret.yaml create mode 100644 k8s/railiance/kustomization.yaml create mode 100644 k8s/railiance/service.yaml create mode 100644 workplans/ISSUE-WP-0003-railiance01-deployment.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..34b2949 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# issue-core REST ingestion service image. +# +# Installs the published issue-core[api] package from the Coulomb Gitea PyPI +# index (no sibling-checkout build context) and runs the FastAPI ingestion +# server on :8765. Built and pushed to gitea.coulomb.social/coulomb/issue-core. +FROM python:3.12-slim AS runtime + +ARG ISSUE_CORE_VERSION=">=0.2,<0.3" +# Do not name this PIP_INDEX_URL — Docker exposes ARGs as env vars during RUN, +# and pip treats PIP_INDEX_URL as the sole primary index (excluding PyPI). +ARG GITEA_PYPI_INDEX_URL=https://gitea.coulomb.social/api/packages/coulomb/pypi/simple/ + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + HOME=/home/app + +# Non-root runtime user; HOME drives issue-core's config dir +# (~/.config/issue-tracker/backends.json). +RUN useradd --create-home --home-dir /home/app --uid 10001 app + +RUN pip install --no-cache-dir \ + --index-url https://pypi.org/simple \ + --extra-index-url "${GITEA_PYPI_INDEX_URL}" \ + "issue-core[api]${ISSUE_CORE_VERSION}" + +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +USER app +EXPOSE 8765 + +# Entrypoint renders backends.json from env, then execs the server. +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..e89a947 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# Render issue-core backends.json from environment, then start the API. +# +# The backend structure (host/owner/repo/default) is non-secret and supplied +# via the BACKENDS_TEMPLATE env (a ConfigMap), with the Gitea token injected +# from GITEA_BACKEND_TOKEN (an ExternalSecret-materialized Secret). The token +# is never baked into the image or committed to Git. +set -eu + +CONFIG_DIR="${HOME}/.config/issue-tracker" +mkdir -p "${CONFIG_DIR}" + +: "${BACKENDS_TEMPLATE:?BACKENDS_TEMPLATE env is required}" + +# Substitute the token placeholder using python (always present in the image) +# to avoid shell-escaping issues with the secret value. +GITEA_BACKEND_TOKEN="${GITEA_BACKEND_TOKEN:-}" \ +BACKENDS_TEMPLATE="${BACKENDS_TEMPLATE}" \ +python - "${CONFIG_DIR}/backends.json" <<'PY' +import json, os, sys +tmpl = json.loads(os.environ["BACKENDS_TEMPLATE"]) +token = os.environ.get("GITEA_BACKEND_TOKEN", "") +for cfg in tmpl.values(): + if isinstance(cfg, dict) and cfg.get("token") == "__FROM_ENV__": + cfg["token"] = token +with open(sys.argv[1], "w") as fh: + json.dump(tmpl, fh, indent=2) +PY + +exec issue serve --host 0.0.0.0 --port 8765 --log-level "${LOG_LEVEL:-info}" diff --git a/k8s/railiance/configmap-backends.yaml b/k8s/railiance/configmap-backends.yaml new file mode 100644 index 0000000..94b6372 --- /dev/null +++ b/k8s/railiance/configmap-backends.yaml @@ -0,0 +1,24 @@ +# Non-secret backend structure for issue-core inside railiance01. +# Default backend = cluster Gitea (markitect). The Gitea token is NOT here; +# it is injected at startup from GITEA_BACKEND_TOKEN (ExternalSecret) where the +# template carries the sentinel "__FROM_ENV__". +apiVersion: v1 +kind: ConfigMap +metadata: + name: issue-core-backends + namespace: issue-core + labels: + app.kubernetes.io/name: issue-core + app.kubernetes.io/part-of: railiance-gitops +data: + backends.json: | + { + "markitect": { + "type": "gitea", + "base_url": "http://gitea-http.default.svc.cluster.local:3000", + "owner": "coulomb", + "repo": "markitect_project", + "token": "__FROM_ENV__" + }, + "default": "markitect" + } diff --git a/k8s/railiance/deployment.yaml b/k8s/railiance/deployment.yaml new file mode 100644 index 0000000..12d3a83 --- /dev/null +++ b/k8s/railiance/deployment.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: issue-core + namespace: issue-core + labels: + app.kubernetes.io/name: issue-core + app.kubernetes.io/part-of: railiance-gitops + annotations: + argocd.argoproj.io/sync-wave: "1" # after the ExternalSecret (wave 0) +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: issue-core + template: + metadata: + labels: + app.kubernetes.io/name: issue-core + app.kubernetes.io/part-of: railiance-gitops + spec: + # Image is public-pullable from the Gitea registry (per railiance-forge + # docs). Add imagePullSecrets: [{name: gitea-registry}] if it becomes private. + containers: + - name: issue-core + image: gitea.coulomb.social/coulomb/issue-core:0.2.0 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8765 + env: + - name: ISSUE_CORE_API_KEY + valueFrom: + secretKeyRef: + name: issue-core-runtime + key: ISSUE_CORE_API_KEY + - name: GITEA_BACKEND_TOKEN + valueFrom: + secretKeyRef: + name: issue-core-runtime + key: GITEA_BACKEND_TOKEN + - name: BACKENDS_TEMPLATE + valueFrom: + configMapKeyRef: + name: issue-core-backends + key: backends.json + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + drop: ["ALL"] diff --git a/k8s/railiance/externalsecret.yaml b/k8s/railiance/externalsecret.yaml new file mode 100644 index 0000000..8806713 --- /dev/null +++ b/k8s/railiance/externalsecret.yaml @@ -0,0 +1,37 @@ +# Runtime secrets for issue-core, materialized from OpenBao by External Secrets +# Operator (cluster default per railiance-platform docs/argocd-gitops.md). +# +# DEPENDENCY: External Secrets Operator is not yet installed on railiance01 and +# the OpenBao path below must be provisioned by railiance-platform. Until then +# this resource will not reconcile and the Deployment stays Pending the Secret. +# +# OpenBao path: platform/workloads/issue-core/issue-core/issue-core-runtime +# properties: ISSUE_CORE_API_KEY, GITEA_BACKEND_TOKEN +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: issue-core-runtime + namespace: issue-core + labels: + app.kubernetes.io/name: issue-core + app.kubernetes.io/part-of: railiance-gitops + annotations: + argocd.argoproj.io/sync-wave: "0" # before the Deployment (wave 1) +spec: + refreshInterval: 1h + secretStoreRef: + # Provisioned by railiance-platform during ESO install; name TBC on bootstrap. + name: openbao + kind: ClusterSecretStore + target: + name: issue-core-runtime + creationPolicy: Owner + data: + - secretKey: ISSUE_CORE_API_KEY + remoteRef: + key: platform/workloads/issue-core/issue-core/issue-core-runtime + property: ISSUE_CORE_API_KEY + - secretKey: GITEA_BACKEND_TOKEN + remoteRef: + key: platform/workloads/issue-core/issue-core/issue-core-runtime + property: GITEA_BACKEND_TOKEN diff --git a/k8s/railiance/kustomization.yaml b/k8s/railiance/kustomization.yaml new file mode 100644 index 0000000..48dede5 --- /dev/null +++ b/k8s/railiance/kustomization.yaml @@ -0,0 +1,12 @@ +# issue-core workload manifests, synced by the ArgoCD `issue-core` Application +# (path k8s/railiance, destination namespace issue-core, CreateNamespace=true). +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: issue-core + +resources: + - externalsecret.yaml + - configmap-backends.yaml + - deployment.yaml + - service.yaml diff --git a/k8s/railiance/service.yaml b/k8s/railiance/service.yaml new file mode 100644 index 0000000..e899134 --- /dev/null +++ b/k8s/railiance/service.yaml @@ -0,0 +1,19 @@ +# ClusterIP exposing issue-core on 8765 as +# issue-core.issue-core.svc.cluster.local:8765 — the address activity-core's +# ISSUE_CORE_URL points at once its k8s runtime port is corrected (8010 -> 8765). +apiVersion: v1 +kind: Service +metadata: + name: issue-core + namespace: issue-core + labels: + app.kubernetes.io/name: issue-core + app.kubernetes.io/part-of: railiance-gitops +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: issue-core + ports: + - name: http + port: 8765 + targetPort: http diff --git a/workplans/ISSUE-WP-0003-railiance01-deployment.md b/workplans/ISSUE-WP-0003-railiance01-deployment.md new file mode 100644 index 0000000..2f6c77e --- /dev/null +++ b/workplans/ISSUE-WP-0003-railiance01-deployment.md @@ -0,0 +1,235 @@ +--- +id: ISSUE-WP-0003 +type: workplan +title: "Deploy issue-core as a service on railiance01 (ArgoCD GitOps pilot)" +domain: custodian +repo: issue-core +status: active +owner: claude +topic_slug: custodian +created: "2026-06-19" +updated: "2026-06-19" +state_hub_workstream_id: "" +--- + +# Deploy issue-core as a service on railiance01 (ArgoCD GitOps pilot) + +`issue-core` is the authoritative task-lifecycle manager and the REST ingestion +target for activity-core's `IssueSink`. Deployment artifacts (`Dockerfile`, +`docker-entrypoint.sh`, `k8s/railiance/`) are now in-repo; the image builds +locally and `/healthz` returns 200. The railiance01 cluster still has no +`issue-core` namespace or workload — nothing is deployed until T01 push and +T02 ArgoCD bootstrap complete. + +This workplan stands up `issue-core` as a first-class in-cluster service on +railiance01 **via ArgoCD GitOps** — making issue-core the cluster's first +declarative Application and turning on the idle GitOps capability. + +## Current state (verified 2026-06-19) + +- **Deployment artifacts in-repo:** `Dockerfile`, `docker-entrypoint.sh`, and + `k8s/railiance/` (Kustomize: ExternalSecret, ConfigMap, Deployment, Service). + Image builds locally; `docker run` + `GET /healthz` returns 200. Image **not + yet pushed** to `gitea.coulomb.social/coulomb/issue-core:0.2.0`. +- **Dockerfile fix (2026-06-19):** build arg renamed `GITEA_PYPI_INDEX_URL` — + `ARG PIP_INDEX_URL` leaked into the build env and pip used Gitea as the sole + index, so dependencies like `click` were not found. +- **railiance01 cluster:** no `issue-core` namespace; no issue-core + Deployment/Service/Pod in any namespace. +- **Dangling reference:** `activity-core/k8s/railiance/20-runtime.yaml` sets + `ISSUE_CORE_URL: http://issue-core.issue-core.svc.cluster.local:8010` — a + service that does not exist, on the **wrong port** (issue-core serves 8765) — + with `ISSUE_SINK_TYPE: "null"` so emission is disabled. It is a placeholder. +- **Packaging precursor is done:** `ISSUE-WP-0002` published + `issue-core==0.2.0` to the Coulomb Gitea PyPI index. +- **ArgoCD is installed but unused:** all 7 components healthy (~290d), but + **0 Applications, 0 ApplicationSets, 0 registered git repos**, only the stock + `default` AppProject. No `kind: Application` manifests exist in any infra repo. +- **Existing deploy pattern is imperative** (the path we are *replacing* for + this service): local `docker build` → `k3s ctr images import` (side-load, no + registry) → `rsync` manifests → `kubectl apply` (see + `activity-core/k8s/railiance/README.md`). + +## Decisions + +- **Deployment method = ArgoCD GitOps** (operator decision 2026-06-19). + issue-core is the pilot Application; the imperative side-load pattern is not + used for this service. +- **ArgoCD bootstrap owned by `railiance-platform`** (operator decision + 2026-06-19). Platform owns repo registration, AppProject/app-of-apps + conventions, and the External-Secrets/OpenBao plumbing. issue-core only + **contributes** its `Application` manifest + workload manifests into the + agreed GitOps source. T02 is therefore a cross-repo dependency, not + issue-core work — see handoff to railiance-platform. +- **Backend = cluster Gitea (markitect)** (operator decision 2026-06-19). + Ingested tasks route to the existing Gitea backend; no new Postgres/PVC. +- **Secret management = OpenBao.** `ISSUE_CORE_API_KEY` is a shared ingestion + key injected from OpenBao on both issue-core and the activity-core worker. + ops-warden does **not** vend it (see + `~/ops-warden/wiki/playbooks/activity-core-issue-sink.md`). Coordinate the + canonical path with `railiance-platform` (`issue-core-ingestion-api-key`). +- **Image delivery = container registry, not side-load.** GitOps requires a + pullable image tag in a registry the cluster can reach (the Coulomb Gitea + container registry); side-loading defeats declarative reproducibility. + +## Open questions + +- **GitOps source repo.** Resolved by `railiance-platform` as part of the + bootstrap (T02 dependency): where issue-core's `Application` + manifests are + expected to live (its own `issue-core/k8s/` vs. a platform GitOps repo) and + the AppProject/app-of-apps convention to follow. +- **Registry path & pull secret.** Confirm the Coulomb Gitea container registry + path and the cluster pull-secret posture (tracked in `railiance-forge` + container-registry docs and `railiance-apps-WP-0004` I03). + +--- + +## Container image published to a pullable registry + +```task +id: ISSUE-WP-0003-T01 +status: in_progress +priority: high +``` + +**Goal.** A reproducible, registry-hosted image ArgoCD-managed pods can pull. + +- [x] Add `Dockerfile` installing `issue-core[api]>=0.2,<0.3` from the Gitea + PyPI index (with explicit PyPI primary index). Entrypoint renders + `backends.json` then `issue serve --host 0.0.0.0 --port 8765`. +- [x] Local build succeeds; `docker run` + `GET /healthz` returns 200. +- [ ] Build and **push to the Coulomb Gitea container registry** (confirm path + per Open questions); tag `0.2.0`. +- [ ] Configure the cluster pull secret so `issue-core` namespace pods can pull. +- [ ] Verify: `POST /issues/` smoke; pushed tag pullable from the cluster. + +## ArgoCD bootstrap (railiance-platform dependency) + issue-core Application + +```task +id: ISSUE-WP-0003-T02 +status: wait +priority: high +``` + +**Owner split.** ArgoCD bootstrap is **railiance-platform's** (operator +decision 2026-06-19): repo registration in ArgoCD, AppProject/app-of-apps +convention, and the agreed GitOps source layout. This task is `wait` on that +handoff. issue-core's part is to **contribute** the `Application` manifest + +workload manifests into the layout platform defines. + +- **(railiance-platform)** Register the GitOps source repo (repository Secret + + creds); define AppProject for cluster services; publish the source-repo/path + convention and sync policy. +- **(issue-core)** Once the convention is known: author the `issue-core` ArgoCD + `Application` manifest (source repo/path/revision → destination `issue-core` + namespace) per the platform layout. +- Verify: `kubectl get applications -n argocd` shows `issue-core` + Synced/Healthy; ArgoCD reconciles a trivial manifest change. + +## Kubernetes manifests (namespace, Deployment, Service) in GitOps source + +```task +id: ISSUE-WP-0003-T03 +status: in_progress +priority: high +``` + +**Goal.** Declarative manifests in the GitOps source repo, synced by T02. + +- [x] `k8s/railiance/` Kustomize bundle (namespace via ArgoCD + `CreateNamespace=true`). +- [x] Deployment: registry image tag `0.2.0`; port 8765; `/healthz` probes; + resource requests/limits; env from ExternalSecret (T04) and ConfigMap (T05). +- [x] Service: ClusterIP on **8765** as + `issue-core.issue-core.svc.cluster.local`. +- [ ] Verify: ArgoCD syncs the manifests; Pod Ready; `/healthz` 200 from a debug + pod (blocked on T01 push + T02 bootstrap + T04 secrets). + +## OpenBao secret: ISSUE_CORE_API_KEY + +```task +id: ISSUE-WP-0003-T04 +status: todo +priority: high +``` + +**Goal.** The shared ingestion key delivered to both sides from OpenBao. + +- Provision `ISSUE_CORE_API_KEY` in OpenBao at the canonical path (coordinate + with `railiance-platform`; catalog id `issue-core-ingestion-api-key`). +- Deliver into the issue-core Deployment (T03) and the activity-core worker + (T06) with the **same** value (External Secrets / Bao injector — match the + cluster's established mechanism). +- Never write the value to Git, manifests, State Hub, or logs. +- Verify: both pods resolve a non-empty key; auth round-trip (401 without, + 201 with). + +## In-cluster backend config (cluster Gitea / markitect) + +```task +id: ISSUE-WP-0003-T05 +status: in_progress +priority: medium +``` + +**Goal.** issue-core's `backends.json` inside the cluster points `default` at +the cluster Gitea (markitect) backend. + +- [x] ConfigMap `issue-core-backends` with in-cluster Gitea URL + (`gitea-http.default.svc.cluster.local:3000`); token sentinel `__FROM_ENV__`. +- [x] `docker-entrypoint.sh` renders `~/.config/issue-tracker/backends.json` + from `BACKENDS_TEMPLATE` + `GITEA_BACKEND_TOKEN` at startup. +- [ ] Verify: a `POST /issues/` creates a real Gitea issue and returns + `issue_url` (blocked on T04 secrets + in-cluster deployment). + +## Wire activity-core to the live service + +```task +id: ISSUE-WP-0003-T06 +status: todo +priority: high +``` + +**Goal.** activity-core emits to the live issue-core Service. + +- Fix `activity-core/k8s/railiance/20-runtime.yaml`: + `ISSUE_CORE_URL` port `8010 -> 8765`; flip `ISSUE_SINK_TYPE` `null -> rest` + once issue-core is Ready. +- Inject `ISSUE_CORE_API_KEY` into the activity-core worker from the same + OpenBao secret (T04). +- **Contract gap:** issue-core requires `triggering_event_id` as a UUID; + activity-core cron paths may send non-UUID keys (e.g. `"scheduled"`). + Event-driven emission with real event UUIDs works (the `str()` guard in + `issue_sink.py`, commit f05c56e, handles UUID objects). Align schemas before + enabling `rest` for cron-triggered rules. +- Verify: an activity-core run emits a task that lands in cluster Gitea via + issue-core. + +## End-to-end verification + GitOps runbook + +```task +id: ISSUE-WP-0003-T07 +status: todo +priority: medium +``` + +**Goal.** Confirm the deployed service is healthy and document the new path. + +- ArgoCD Application Synced/Healthy; issue-core Pod Ready; Service reachable + cluster-internal. +- activity-core → issue-core emission returns 201 and creates a Gitea issue. +- Document the GitOps runbook (image build/push, ArgoCD sync, secret rotation, + rollback) in `docs/`. +- Emit an `add_progress_event` milestone to the hub on completion. + +--- + +## See also + +- `ISSUE-WP-0002` — Gitea PyPI publication (packaging precursor, finished). +- `railiance-apps-WP-0004` I03 — issue-core packaging/image enablement notes. +- `railiance-forge` — Gitea container registry docs. +- `activity-core/docs/issue-core-emission-boundary.md` — emission contract. +- `activity-core/k8s/railiance/README.md` — the imperative pattern being + superseded for this service. +- `~/ops-warden/wiki/playbooks/activity-core-issue-sink.md` — key routing.