From 434c80c2c328df44a3619737d4724105f3604b90 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 25 Jun 2026 15:15:30 +0200 Subject: [PATCH] feat(statehub): add railiance deployment manifests --- Makefile | 58 +++++++++++- deploy/railiance/README.md | 88 +++++++++++++++++++ .../apps/charts/state-hub/Chart.yaml | 6 ++ .../charts/state-hub/templates/_helpers.tpl | 26 ++++++ .../charts/state-hub/templates/configmap.yaml | 9 ++ .../state-hub/templates/deployment.yaml | 66 ++++++++++++++ .../charts/state-hub/templates/ingress.yaml | 28 ++++++ .../charts/state-hub/templates/namespace.yaml | 8 ++ .../charts/state-hub/templates/service.yaml | 13 +++ .../apps/charts/state-hub/values.yaml | 67 ++++++++++++++ .../railiance/apps/helm/state-hub-values.yaml | 8 ++ .../state-hub-env.secret.sops.yaml.template | 18 ++++ .../platform/state-hub-db-cluster.yaml | 28 ++++++ ...tate-hub-db-credentials.sops.yaml.template | 19 ++++ .../state-hub-db-networkpolicies.yaml | 74 ++++++++++++++++ ...P-0011-state-hub-threephoenix-migration.md | 21 ++++- 16 files changed, 535 insertions(+), 2 deletions(-) create mode 100644 deploy/railiance/README.md create mode 100644 deploy/railiance/apps/charts/state-hub/Chart.yaml create mode 100644 deploy/railiance/apps/charts/state-hub/templates/_helpers.tpl create mode 100644 deploy/railiance/apps/charts/state-hub/templates/configmap.yaml create mode 100644 deploy/railiance/apps/charts/state-hub/templates/deployment.yaml create mode 100644 deploy/railiance/apps/charts/state-hub/templates/ingress.yaml create mode 100644 deploy/railiance/apps/charts/state-hub/templates/namespace.yaml create mode 100644 deploy/railiance/apps/charts/state-hub/templates/service.yaml create mode 100644 deploy/railiance/apps/charts/state-hub/values.yaml create mode 100644 deploy/railiance/apps/helm/state-hub-values.yaml create mode 100644 deploy/railiance/apps/manifests/state-hub-env.secret.sops.yaml.template create mode 100644 deploy/railiance/platform/state-hub-db-cluster.yaml create mode 100644 deploy/railiance/platform/state-hub-db-credentials.sops.yaml.template create mode 100644 deploy/railiance/platform/state-hub-db-networkpolicies.yaml diff --git a/Makefile b/Makefile index 5877c0c..e34fb44 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,17 @@ -.PHONY: install install-cli dashboard-install dashboard-check db db-tools migrate seed api dashboard check test test-python clean register-project register-codex-project register-mcp bootstrap-env validate-adr add-domain rename-domain add-repo list-repos register-path register-from-classification register-from-classification-all cleanup-stale tunnels-up tunnels-status tunnels-check bridges install-hooks install-hooks-all gitea-inventory token-reconcile +.PHONY: install install-cli dashboard-install dashboard-check db db-tools migrate seed api dashboard check test test-python clean register-project register-codex-project register-mcp bootstrap-env validate-adr add-domain rename-domain add-repo list-repos register-path register-from-classification register-from-classification-all cleanup-stale tunnels-up tunnels-status tunnels-check bridges install-hooks install-hooks-all gitea-inventory token-reconcile railiance-state-hub-render railiance-state-hub-client-dry-run railiance-state-hub-server-dry-run COMPOSE = docker compose -f infra/docker-compose.yml --env-file .env PYTHON ?= python3 +HELM ?= $(shell command -v helm 2>/dev/null || if [ -x "$$HOME/.local/bin/helm" ]; then printf "%s" "$$HOME/.local/bin/helm"; else printf "%s" "helm"; fi) +KUBECTL ?= $(shell command -v kubectl 2>/dev/null || if [ -x "$$HOME/.local/bin/kubectl" ]; then printf "%s" "$$HOME/.local/bin/kubectl"; else printf "%s" "kubectl"; fi) + +RAILIANCE_STATE_HUB_RELEASE ?= state-hub +RAILIANCE_STATE_HUB_NAMESPACE ?= state-hub +RAILIANCE_STATE_HUB_CHART ?= deploy/railiance/apps/charts/state-hub +RAILIANCE_STATE_HUB_VALUES ?= deploy/railiance/apps/helm/state-hub-values.yaml +RAILIANCE_STATE_HUB_IMAGE_TAG ?= b536741 +RAILIANCE_STATE_HUB_PLATFORM_DIR ?= deploy/railiance/platform +RAILIANCE_STATE_HUB_APP_MANIFESTS ?= deploy/railiance/apps/manifests # Codex/WSL non-login shells may not source ~/.profile; keep uv discoverable. UV ?= $(shell command -v uv 2>/dev/null || if [ -x "$$HOME/.local/bin/uv" ]; then printf "%s" "$$HOME/.local/bin/uv"; else printf "%s" "uv"; fi) @@ -61,6 +71,52 @@ dashboard: check: curl -sf http://127.0.0.1:8000/state/health | python3 -m json.tool +railiance-state-hub-render: + $(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \ + --namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \ + -f $(RAILIANCE_STATE_HUB_VALUES) \ + --set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) + +railiance-state-hub-client-dry-run: + @set -e; \ + tmpdir="$$(mktemp -d)"; \ + trap 'rm -rf "$$tmpdir"' EXIT; \ + $(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \ + --namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \ + -f $(RAILIANCE_STATE_HUB_VALUES) \ + --set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) > "$$tmpdir/state-hub.yaml"; \ + $(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \ + $(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-cluster.yaml; \ + $(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-networkpolicies.yaml; \ + $(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \ + $(KUBECTL) apply --dry-run=client -f "$$tmpdir/state-hub.yaml" + +railiance-state-hub-server-dry-run: + @set -e; \ + tmpdir="$$(mktemp -d)"; \ + trap 'rm -rf "$$tmpdir"' EXIT; \ + $(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \ + --namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \ + -f $(RAILIANCE_STATE_HUB_VALUES) \ + --set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) > "$$tmpdir/state-hub.yaml"; \ + $(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \ + --namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \ + -f $(RAILIANCE_STATE_HUB_VALUES) \ + --set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) \ + --show-only templates/namespace.yaml > "$$tmpdir/state-hub-namespace.yaml"; \ + $(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \ + $(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-cluster.yaml; \ + $(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-networkpolicies.yaml; \ + $(KUBECTL) apply --dry-run=server -f "$$tmpdir/state-hub-namespace.yaml"; \ + if $(KUBECTL) get namespace $(RAILIANCE_STATE_HUB_NAMESPACE) >/dev/null 2>&1; then \ + $(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \ + $(KUBECTL) apply --dry-run=server -f "$$tmpdir/state-hub.yaml"; \ + else \ + echo "Namespace $(RAILIANCE_STATE_HUB_NAMESPACE) does not exist; validating namespaced app manifests with client dry-run."; \ + $(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \ + $(KUBECTL) apply --dry-run=client -f "$$tmpdir/state-hub.yaml"; \ + fi + test: test-python dashboard-check test-python: diff --git a/deploy/railiance/README.md b/deploy/railiance/README.md new file mode 100644 index 0000000..3df6765 --- /dev/null +++ b/deploy/railiance/README.md @@ -0,0 +1,88 @@ +# State Hub Railiance Deployment Handoff + +This directory contains the State Hub deployment handoff for `CUST-WP-0011`. +It is source-owned by `state-hub` and split along the Railiance ownership +boundaries used for the actual cluster rollout. + +## Ownership + +- `deploy/railiance/platform/` is the `railiance-platform` handoff for the + `state-hub-db` CloudNativePG cluster, database bootstrap credential, and + database NetworkPolicies in the `databases` namespace. +- `deploy/railiance/apps/` is the `railiance-apps` handoff for the State Hub API + Helm chart, non-secret production values, and app namespace runtime Secret + template. +- Runtime secret values are not stored here. Replace placeholder passwords only + in an operator-controlled file, then encrypt or deliver through the approved + platform secret path. + +## Image + +The current image is pinned to: + +```text +gitea.coulomb.social/coulomb/state-hub:b536741 +``` + +railiance01 has already pulled this tag with `crictl`, and the image serves +`GET /state/health` against the local WSL database in smoke testing. + +## Render And Dry-Run + +Render the app chart without touching the cluster: + +```bash +make railiance-state-hub-render +``` + +Run client-side Kubernetes validation for the platform manifests, app Secret +template, and rendered chart: + +```bash +make railiance-state-hub-client-dry-run +``` + +Run server-side dry-run against the configured representative cluster: + +```bash +KUBECONFIG=~/.kube/config-hosteurope make railiance-state-hub-server-dry-run +``` + +Server-side dry-run requires the CNPG CRDs, namespace permissions, and dry-run +permission for resources in `databases` and `state-hub`. +Before the `state-hub` namespace exists, Kubernetes cannot server-dry-run namespaced app +objects into that namespace because dry-run Namespace creation is not persisted. +The Make target therefore server-validates the platform and Namespace manifests, +then falls back to client dry-run for namespaced app manifests with an explicit +notice. + +## Promotion Notes + +Platform promotion into `railiance-platform`: + +- copy `platform/state-hub-db-credentials.sops.yaml.template` to a real SOPS + secret file with an operator-generated password; +- apply or GitOps-manage `platform/state-hub-db-cluster.yaml`; +- apply or GitOps-manage `platform/state-hub-db-networkpolicies.yaml`. + +App promotion into `railiance-apps`: + +- copy `apps/charts/state-hub/` to `charts/state-hub/`; +- copy `apps/helm/state-hub-values.yaml` to `helm/state-hub-values.yaml`; +- create `state-hub-env` in the `state-hub` namespace from the approved + secret-delivery path; +- deploy with Helm only after `state-hub-db` is healthy. + +## Runtime Secret Contract + +The app chart expects a Kubernetes Secret named `state-hub-env` in the +`state-hub` namespace with at least: + +```text +DATABASE_URL=postgresql+asyncpg://state_hub:@state-hub-db-rw.databases.svc.cluster.local:5432/state_hub +``` + +Optional runtime settings such as `CORS_ORIGINS` can live in the chart +ConfigMap. The default chart keeps public ingress disabled; access should use +the existing private tunnel/ops-bridge path until a separate exposure decision +is recorded. \ No newline at end of file diff --git a/deploy/railiance/apps/charts/state-hub/Chart.yaml b/deploy/railiance/apps/charts/state-hub/Chart.yaml new file mode 100644 index 0000000..1d2fa65 --- /dev/null +++ b/deploy/railiance/apps/charts/state-hub/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: state-hub +description: State Hub API service for private Railiance operation +type: application +version: 0.1.0 +appVersion: "b536741" \ No newline at end of file diff --git a/deploy/railiance/apps/charts/state-hub/templates/_helpers.tpl b/deploy/railiance/apps/charts/state-hub/templates/_helpers.tpl new file mode 100644 index 0000000..f02f9f4 --- /dev/null +++ b/deploy/railiance/apps/charts/state-hub/templates/_helpers.tpl @@ -0,0 +1,26 @@ +{{- define "statehub.fullname" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- printf "%s" $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "statehub.labels" -}} +app: {{ include "statehub.fullname" . }} +app.kubernetes.io/name: {{ include "statehub.fullname" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/part-of: railiance-apps +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }} +railiance.io/layer: s5-app +{{- end -}} + +{{- define "statehub.selectorLabels" -}} +app: {{ include "statehub.fullname" . }} +{{- end -}} + +{{- define "statehub.image" -}} +{{- if not .Values.image.tag -}} +{{- fail "image.tag is required - pin it in deploy/railiance/apps/helm/state-hub-values.yaml or pass --set image.tag=" -}} +{{- end -}} +{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}} +{{- end -}} \ No newline at end of file diff --git a/deploy/railiance/apps/charts/state-hub/templates/configmap.yaml b/deploy/railiance/apps/charts/state-hub/templates/configmap.yaml new file mode 100644 index 0000000..69de021 --- /dev/null +++ b/deploy/railiance/apps/charts/state-hub/templates/configmap.yaml @@ -0,0 +1,9 @@ +{{- if .Values.config.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.config.name }} + labels: {{- include "statehub.labels" . | nindent 4 }} +data: + CORS_ORIGINS: {{ .Values.config.corsOrigins | quote }} +{{- end }} \ No newline at end of file diff --git a/deploy/railiance/apps/charts/state-hub/templates/deployment.yaml b/deploy/railiance/apps/charts/state-hub/templates/deployment.yaml new file mode 100644 index 0000000..8821fc2 --- /dev/null +++ b/deploy/railiance/apps/charts/state-hub/templates/deployment.yaml @@ -0,0 +1,66 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "statehub.fullname" . }} + labels: {{- include "statehub.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: {{- include "statehub.selectorLabels" . | nindent 6 }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: {{- include "statehub.labels" . | nindent 8 }} + spec: + securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: state-hub + image: {{ include "statehub.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + envFrom: + {{- if .Values.config.enabled }} + - configMapRef: + name: {{ .Values.config.name | quote }} + {{- end }} + - secretRef: + name: {{ .Values.secret.name | quote }} + {{- if .Values.probes.enabled }} + readinessProbe: + httpGet: + path: {{ .Values.probes.path }} + port: {{ .Values.probes.port }} + initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.readiness.periodSeconds }} + timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }} + failureThreshold: {{ .Values.probes.readiness.failureThreshold }} + livenessProbe: + httpGet: + path: {{ .Values.probes.path }} + port: {{ .Values.probes.port }} + initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.liveness.periodSeconds }} + timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }} + failureThreshold: {{ .Values.probes.liveness.failureThreshold }} + {{- end }} + resources: {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: {{- toYaml . | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/deploy/railiance/apps/charts/state-hub/templates/ingress.yaml b/deploy/railiance/apps/charts/state-hub/templates/ingress.yaml new file mode 100644 index 0000000..b6f2ada --- /dev/null +++ b/deploy/railiance/apps/charts/state-hub/templates/ingress.yaml @@ -0,0 +1,28 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "statehub.fullname" . }} + labels: {{- include "statehub.labels" . | nindent 4 }} + annotations: + {{- toYaml .Values.ingress.annotations | nindent 4 }} +spec: + ingressClassName: {{ .Values.ingress.className }} + {{- if .Values.ingress.tls }} + tls: + - hosts: + - {{ .Values.ingress.host }} + secretName: {{ include "statehub.fullname" . }}-tls + {{- end }} + rules: + - host: {{ .Values.ingress.host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "statehub.fullname" . }} + port: + number: {{ .Values.service.port }} +{{- end }} \ No newline at end of file diff --git a/deploy/railiance/apps/charts/state-hub/templates/namespace.yaml b/deploy/railiance/apps/charts/state-hub/templates/namespace.yaml new file mode 100644 index 0000000..8c4215f --- /dev/null +++ b/deploy/railiance/apps/charts/state-hub/templates/namespace.yaml @@ -0,0 +1,8 @@ +{{- if .Values.namespace.create }} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Release.Namespace }} + labels: + {{- toYaml .Values.namespace.labels | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/deploy/railiance/apps/charts/state-hub/templates/service.yaml b/deploy/railiance/apps/charts/state-hub/templates/service.yaml new file mode 100644 index 0000000..60a2817 --- /dev/null +++ b/deploy/railiance/apps/charts/state-hub/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "statehub.fullname" . }} + labels: {{- include "statehub.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: http + selector: {{- include "statehub.selectorLabels" . | nindent 4 }} \ No newline at end of file diff --git a/deploy/railiance/apps/charts/state-hub/values.yaml b/deploy/railiance/apps/charts/state-hub/values.yaml new file mode 100644 index 0000000..8d929ec --- /dev/null +++ b/deploy/railiance/apps/charts/state-hub/values.yaml @@ -0,0 +1,67 @@ +image: + repository: gitea.coulomb.social/coulomb/state-hub + tag: "" + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +replicaCount: 1 + +namespace: + create: true + labels: + railiance.io/postgres-client: state-hub-db + railiance.io/layer: s5-app + +service: + type: ClusterIP + port: 8000 + targetPort: 8000 + +config: + enabled: true + name: state-hub-config + corsOrigins: "http://localhost:3000,http://127.0.0.1:3000,http://localhost:3001,http://127.0.0.1:3001" + +secret: + name: state-hub-env + +resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 1000m + memory: 2Gi + +ingress: + enabled: false + className: traefik + host: state-hub.coulomb.social + tls: true + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod + +probes: + enabled: true + path: /state/health + port: 8000 + liveness: + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readiness: + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + +podSecurityContext: {} +securityContext: {} + +nodeSelector: {} +tolerations: [] +affinity: {} \ No newline at end of file diff --git a/deploy/railiance/apps/helm/state-hub-values.yaml b/deploy/railiance/apps/helm/state-hub-values.yaml new file mode 100644 index 0000000..7033b17 --- /dev/null +++ b/deploy/railiance/apps/helm/state-hub-values.yaml @@ -0,0 +1,8 @@ +# Production values for the State Hub Railiance chart handoff. +# Non-secret values only. DATABASE_URL comes from the Secret `state-hub-env`. + +image: + tag: "b536741" + +ingress: + enabled: false \ No newline at end of file diff --git a/deploy/railiance/apps/manifests/state-hub-env.secret.sops.yaml.template b/deploy/railiance/apps/manifests/state-hub-env.secret.sops.yaml.template new file mode 100644 index 0000000..2129d9b --- /dev/null +++ b/deploy/railiance/apps/manifests/state-hub-env.secret.sops.yaml.template @@ -0,0 +1,18 @@ +# Template for the State Hub runtime Secret in the state-hub namespace. +# DO NOT commit this file with real credentials. +# Encrypt with: sops -e -i state-hub-env.sops.yaml +# Apply with: kubectl apply -f <(sops -d state-hub-env.sops.yaml) +--- +apiVersion: v1 +kind: Secret +metadata: + name: state-hub-env + namespace: state-hub + labels: + app.kubernetes.io/name: state-hub + app.kubernetes.io/component: runtime-env + app.kubernetes.io/managed-by: manual + railiance.io/layer: s5-app +type: Opaque +stringData: + DATABASE_URL: postgresql+asyncpg://state_hub:REPLACE_WITH_URL_ENCODED_PASSWORD@state-hub-db-rw.databases.svc.cluster.local:5432/state_hub \ No newline at end of file diff --git a/deploy/railiance/platform/state-hub-db-cluster.yaml b/deploy/railiance/platform/state-hub-db-cluster.yaml new file mode 100644 index 0000000..26280c2 --- /dev/null +++ b/deploy/railiance/platform/state-hub-db-cluster.yaml @@ -0,0 +1,28 @@ +--- +# Dedicated CNPG Cluster for State Hub episodic memory. +# Owned by railiance-platform (S3). Operator lives in cnpg-system. +# +# Pre-condition: state-hub-db-credentials Secret exists in databases namespace. +# Runtime app Secret is separate and lives in the state-hub namespace. +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: state-hub-db + namespace: databases + labels: + app.kubernetes.io/name: state-hub-db + app.kubernetes.io/component: database + app.kubernetes.io/managed-by: manual + railiance.io/layer: s3-platform + railiance.io/role: state-hub-database +spec: + instances: 1 + imageName: ghcr.io/cloudnative-pg/postgresql:16 + storage: + size: 10Gi + bootstrap: + initdb: + database: state_hub + owner: state_hub + secret: + name: state-hub-db-credentials \ No newline at end of file diff --git a/deploy/railiance/platform/state-hub-db-credentials.sops.yaml.template b/deploy/railiance/platform/state-hub-db-credentials.sops.yaml.template new file mode 100644 index 0000000..21dd9af --- /dev/null +++ b/deploy/railiance/platform/state-hub-db-credentials.sops.yaml.template @@ -0,0 +1,19 @@ +# Template for the state-hub-db bootstrap Secret. +# DO NOT commit this file with real credentials. +# Encrypt with: sops -e -i state-hub-db-credentials.sops.yaml +# Apply with: kubectl apply -f <(sops -d state-hub-db-credentials.sops.yaml) +--- +apiVersion: v1 +kind: Secret +metadata: + name: state-hub-db-credentials + namespace: databases + labels: + app.kubernetes.io/name: state-hub-db + app.kubernetes.io/component: database-bootstrap + app.kubernetes.io/managed-by: manual + railiance.io/layer: s3-platform +type: kubernetes.io/basic-auth +stringData: + username: state_hub + password: REPLACE_WITH_PASSWORD \ No newline at end of file diff --git a/deploy/railiance/platform/state-hub-db-networkpolicies.yaml b/deploy/railiance/platform/state-hub-db-networkpolicies.yaml new file mode 100644 index 0000000..4faed89 --- /dev/null +++ b/deploy/railiance/platform/state-hub-db-networkpolicies.yaml @@ -0,0 +1,74 @@ +--- +# NetworkPolicies for the dedicated State Hub CNPG cluster. +# Namespaces that need database access must carry: +# railiance.io/postgres-client: state-hub-db +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-egress-kube-api-state-hub-db + namespace: databases + labels: + app.kubernetes.io/name: state-hub-db + railiance.io/layer: s3-platform +spec: + podSelector: + matchLabels: + cnpg.io/cluster: state-hub-db + policyTypes: + - Egress + egress: + - to: + - namespaceSelector: {} + ports: + - protocol: TCP + port: 6443 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-ingress-from-cnpg-operator-state-hub-db + namespace: databases + labels: + app.kubernetes.io/name: state-hub-db + railiance.io/layer: s3-platform +spec: + podSelector: + matchLabels: + cnpg.io/cluster: state-hub-db + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: cnpg-system + ports: + - protocol: TCP + port: 5432 + - protocol: TCP + port: 8000 + - protocol: TCP + port: 9187 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-ingress-from-state-hub-namespace-state-hub-db + namespace: databases + labels: + app.kubernetes.io/name: state-hub-db + railiance.io/layer: s3-platform +spec: + podSelector: + matchLabels: + cnpg.io/cluster: state-hub-db + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + railiance.io/postgres-client: state-hub-db + ports: + - protocol: TCP + port: 5432 \ No newline at end of file diff --git a/workplans/CUST-WP-0011-state-hub-threephoenix-migration.md b/workplans/CUST-WP-0011-state-hub-threephoenix-migration.md index 7902324..fcac92c 100644 --- a/workplans/CUST-WP-0011-state-hub-threephoenix-migration.md +++ b/workplans/CUST-WP-0011-state-hub-threephoenix-migration.md @@ -225,8 +225,9 @@ and verified railiance01 can pull it with `sudo crictl pull`. ```task id: CUST-WP-0011-T04 -status: todo +status: done priority: high +completed: "2026-06-25" state_hub_task_id: "a7baf2eb-abd7-4aa3-b2cb-a5370ac09844" ``` @@ -241,6 +242,24 @@ Create the cluster-side deployment assets using current Railiance boundaries: **Done when:** manifests lint/apply in a non-destructive dry run and ownership boundaries are documented. +Completed 2026-06-25: added a source-owned Railiance deployment handoff under +`deploy/railiance/` with platform manifests for `state-hub-db` CNPG, database +credentials, database NetworkPolicies, an app Helm chart, production values, and +a `state-hub-env` Secret template. Added Make targets for rendering, +client-side dry-run validation, and namespace-aware server-side dry-run +validation. Verified: + +- `make railiance-state-hub-render` +- `make railiance-state-hub-client-dry-run` +- `make railiance-state-hub-server-dry-run` + +The server dry-run validates platform resources and the Namespace manifest +against the live cluster API. Because the `state-hub` namespace does not yet +exist, it explicitly falls back to client dry-run for namespaced app manifests; +Kubernetes cannot persist a dry-run Namespace for subsequent namespaced +server-side validation. Ownership boundaries and promotion notes are documented +in `deploy/railiance/README.md`. + --- ### T05 — Deploy empty State Hub and run migrations on railiance01