From 7e68cc835ecfa2468e47dcbbfd709f1dca07f8be Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 14 Aug 2026 01:37:44 +0200 Subject: [PATCH] Start TEN-WP-0005-T05: recover deploy manifests and add CI image build Production still serves the TEN-WP-0004 image, which has no lifecycle routes. Recover the live railiance01 objects into deploy/ so rollback does not depend on a cluster annotation, and add the fleet CI image workflow so the lifecycle image is built from a forge revision. --- .forgejo/workflows/image.yaml | 59 ++++++++ deploy/README.md | 55 +++++++ deploy/tenant-engine.yaml | 140 ++++++++++++++++++ ...P-0005-tenant-update-and-retirement-api.md | 11 +- 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 .forgejo/workflows/image.yaml create mode 100644 deploy/README.md create mode 100644 deploy/tenant-engine.yaml diff --git a/.forgejo/workflows/image.yaml b/.forgejo/workflows/image.yaml new file mode 100644 index 0000000..5578290 --- /dev/null +++ b/.forgejo/workflows/image.yaml @@ -0,0 +1,59 @@ +name: Build and Publish Container Image + +# Modelled on activity-core/.forgejo/workflows/image.yaml — the fleet's +# canonical image-publish pattern. Images are built by CI from a tarball of +# the pushed commit, never from a workstation working tree, so the artifact's +# provenance is a forge revision. + +on: + push: + branches: + - main + paths: + - ".forgejo/workflows/image.yaml" + - "Containerfile" + - "src/**" + - "pyproject.toml" + - "README.md" + - "LICENSE" + workflow_dispatch: + +env: + REGISTRY: forgejo.coulomb.social + IMAGE_NAME: coulomb/tenant-engine + DOCKER_HOST: tcp://127.0.0.1:2375 + +jobs: + build-and-push: + runs-on: container-build + steps: + - name: Build and push image + env: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -eu + REF="${GITHUB_SHA:-main}" + SHORT="${REF:0:7}" + mkdir -p buildctx "${HOME}/bin" + wget -qO /tmp/repo.tar.gz \ + "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz" + tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1 + wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \ + | tar xz --strip-components=1 -C "${HOME}/bin" docker/docker + export PATH="${HOME}/bin:${PATH}" + echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${REGISTRY_USER}" --password-stdin + IMAGE="${REGISTRY}/${IMAGE_NAME}" + docker build -f buildctx/Containerfile -t "${IMAGE}:latest" -t "${IMAGE}:main-${SHORT}" buildctx + docker push "${IMAGE}:latest" + docker push "${IMAGE}:main-${SHORT}" + echo "pushed ${IMAGE}:latest and ${IMAGE}:main-${SHORT}" + + - name: Report immutable digest + run: | + set -eu + export PATH="${HOME}/bin:${PATH}" + IMAGE="${REGISTRY}/${IMAGE_NAME}" + SHORT="${GITHUB_SHA:0:7}" + # Deployments pin by digest, never by tag -- print it for the rollout step. + docker inspect --format='{{index .RepoDigests 0}}' "${IMAGE}:main-${SHORT}" diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..19516fd --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,55 @@ +# tenant-engine production deployment + +Manifests recovered from the live objects' +`kubectl.kubernetes.io/last-applied-configuration` on 2026-08-14 so that a +rollback does not depend on a cluster annotation. Applied originally during +TEN-WP-0004; TEN-WP-0005 keeps them in-repo and pins by digest. + +| File | Deployment | Service DNS | +| --- | --- | --- | +| `tenant-engine.yaml` | `tenant-engine` | `tenant-engine.tenant-engine.svc.cluster.local:8090` | + +The file is a five-document manifest: `Namespace`, `PersistentVolumeClaim`, +`Deployment`, `Service`, and a least-privilege `NetworkPolicy`. Ingress is +restricted to the `user-engine` workload; egress is restricted to +`flex-auth-tenant-engine` on 8080 plus cluster DNS. + +## Rolling out an image + +**Do not build images on a workstation.** `.forgejo/workflows/image.yaml` +builds from a pushed forge commit on the `container-build` runner. + +```bash +# 1. Push the commit you intend to ship; CI builds :latest and :main- +git push origin main + +# 2. Take the immutable digest from the workflow's "Report immutable digest" +# step -- deploy by digest, never by tag + +# 3. Edit the image digest in deploy/tenant-engine.yaml, then apply +kubectl apply -f deploy/tenant-engine.yaml +kubectl -n tenant-engine rollout status deploy/tenant-engine --timeout=120s +``` + +The Deployment uses `Recreate` because the SQLite PVC is `ReadWriteOnce`. +A new pod applies the forward-only lifecycle migration on startup against +the existing database. + +## Rollback + +```bash +kubectl -n tenant-engine rollout undo deploy/tenant-engine +``` + +If the ReplicaSet history has been pruned, re-apply the manifest with the +last-known-good digest below. + +| Deployment | Digest | State | +| --- | --- | --- | +| `tenant-engine` | `sha256:2249e8c6ee44ae36081cddc52daf9c3f63acd18a95a5d620ab4fa7ac85149207` | **current** — TEN-WP-0004 create/role/plan API, live since 2026-08-09 | +| `tenant-engine` *(earlier)* | `sha256:33c5dd84eaf1c2f5e067c04931219e13f9348a764a153d641035a6d019095d4f` | first TEN-WP-0004 revision | + +Rolling back to `2249e8c6…` removes the lifecycle routes (`GET /tenants/{id}`, +`PATCH`, retire, reactivate). Create, role grant/revoke, and plan assign keep +working. The SQLite lifecycle columns added by the TEN-WP-0005 migration are +forward-only and stay in place; the older image ignores them. diff --git a/deploy/tenant-engine.yaml b/deploy/tenant-engine.yaml new file mode 100644 index 0000000..65af39a --- /dev/null +++ b/deploy/tenant-engine.yaml @@ -0,0 +1,140 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: tenant-engine + labels: + net-kingdom/component: tenant-engine +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: tenant-engine-data + namespace: tenant-engine +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tenant-engine + namespace: tenant-engine +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: tenant-engine + strategy: + type: Recreate + template: + metadata: + labels: + app.kubernetes.io/name: tenant-engine + spec: + automountServiceAccountToken: false + containers: + - name: tenant-engine + env: + - name: TENANT_ENGINE_DATABASE_PATH + value: /data/tenant-engine.db + - name: TENANT_ENGINE_FLEX_AUTH_URL + value: http://flex-auth-tenant-engine.flex-auth.svc.cluster.local:8080 + image: forgejo.coulomb.social/coulomb/tenant-engine@sha256:2249e8c6ee44ae36081cddc52daf9c3f63acd18a95a5d620ab4fa7ac85149207 + livenessProbe: + httpGet: + path: /health + port: http + periodSeconds: 20 + ports: + - containerPort: 8090 + name: http + readinessProbe: + httpGet: + path: /health + port: http + periodSeconds: 5 + resources: + limits: + cpu: 300m + memory: 192Mi + requests: + cpu: 25m + memory: 48Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /data + name: data + securityContext: + fsGroup: 10001 + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + volumes: + - name: data + persistentVolumeClaim: + claimName: tenant-engine-data +--- +apiVersion: v1 +kind: Service +metadata: + name: tenant-engine + namespace: tenant-engine +spec: + ports: + - name: http + port: 8090 + targetPort: http + selector: + app.kubernetes.io/name: tenant-engine +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: tenant-engine + namespace: tenant-engine +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: tenant-engine + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: user-engine + podSelector: + matchLabels: + app.kubernetes.io/name: user-engine + ports: + - port: 8090 + protocol: TCP + egress: + - ports: + - port: 8080 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: flex-auth + podSelector: + matchLabels: + app.kubernetes.io/name: flex-auth-tenant-engine + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system diff --git a/workplans/TEN-WP-0005-tenant-update-and-retirement-api.md b/workplans/TEN-WP-0005-tenant-update-and-retirement-api.md index 0525a99..c528639 100644 --- a/workplans/TEN-WP-0005-tenant-update-and-retirement-api.md +++ b/workplans/TEN-WP-0005-tenant-update-and-retirement-api.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: tenant-lifecycle created: "2026-08-10" -updated: "2026-08-10" +updated: "2026-08-14" depends_on: - TEN-WP-0004 unblocks: @@ -154,7 +154,7 @@ Not covered: no PostgreSQL tests exist, per the T02 note above. ```task id: TEN-WP-0005-T05 -status: wait +status: progress priority: high state_hub_task_id: "9b21782a-1b26-4fac-81a1-06bd7e8cf70f" ``` @@ -177,3 +177,10 @@ correctly resolves to deny), and image build plus rollout need cluster access. The consumer-facing contract is finalized and ready to hand over: `docs/tenant-lifecycle-api.md`. +Status 2026-08-14: flex-auth reports the seven-action policy live on +`flex-auth-tenant-engine` (`@sha256:9320df394a642eff24da8af4a0ee8886a7bb78b0f14d8ee1deeb30ea8eeeaba7`, +commit `e9911eb`). Production tenant-engine still runs the TEN-WP-0004 image +(`@sha256:2249e8c6…85149207`); its OpenAPI has no lifecycle routes. Recovered +the live manifests into `deploy/` and added the fleet CI image workflow so the +lifecycle image is built from a forge revision rather than a workstation. +