Prepare durable isolated Vergabe pilot deployment and recovery
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
eff457ef52
commit
959eba637f
11 changed files with 408 additions and 9 deletions
|
|
@ -4,7 +4,7 @@ description: |
|
||||||
Vergabe Teilnahme — internal Django tender/bid management web app.
|
Vergabe Teilnahme — internal Django tender/bid management web app.
|
||||||
Single-instance v1 deployment; HA and canary are deferred.
|
Single-instance v1 deployment; HA and canary are deferred.
|
||||||
type: application
|
type: application
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
appVersion: "0.1.0"
|
appVersion: "0.1.0"
|
||||||
keywords:
|
keywords:
|
||||||
- django
|
- django
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,34 @@ app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{- define "vergabe.image" -}}
|
{{- define "vergabe.image" -}}
|
||||||
|
{{- if .Values.image.digest -}}
|
||||||
|
{{- if not (regexMatch "^sha256:[a-f0-9]{64}$" .Values.image.digest) -}}
|
||||||
|
{{- fail "image.digest must be a sha256 OCI digest" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- printf "%s@%s" .Values.image.repository .Values.image.digest -}}
|
||||||
|
{{- else -}}
|
||||||
{{- if not .Values.image.tag -}}
|
{{- if not .Values.image.tag -}}
|
||||||
{{- fail "image.tag is required — pin it in helm/vergabe-teilnahme-values.yaml" -}}
|
{{- fail "image.tag is required — pin it in helm/vergabe-teilnahme-values.yaml" -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}
|
{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "vergabe.validatePilot" -}}
|
||||||
|
{{- if .Values.pilot.enabled -}}
|
||||||
|
{{- if ne (int .Values.replicaCount) 1 -}}
|
||||||
|
{{- fail "invited pilot requires exactly one application replica" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- if or (not .Values.persistence.media.enabled) (not .Values.persistence.appState.enabled) -}}
|
||||||
|
{{- fail "invited pilot requires persistent media and appState" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- if not .Values.image.digest -}}
|
||||||
|
{{- fail "invited pilot requires an immutable image.digest" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $mediaClaim := default (printf "%s-media" (include "vergabe.fullname" .)) .Values.persistence.media.existingClaim -}}
|
||||||
|
{{- $stateClaim := default (printf "%s-app-state" (include "vergabe.fullname" .)) .Values.persistence.appState.existingClaim -}}
|
||||||
|
{{- if eq $mediaClaim $stateClaim -}}
|
||||||
|
{{- fail "media and appState must use distinct claims; operational state must not be downloadable" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
{{- include "vergabe.validatePilot" . }}
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
|
@ -8,10 +9,14 @@ spec:
|
||||||
selector:
|
selector:
|
||||||
matchLabels: {{- include "vergabe.selectorLabels" . | nindent 6 }}
|
matchLabels: {{- include "vergabe.selectorLabels" . | nindent 6 }}
|
||||||
strategy:
|
strategy:
|
||||||
|
{{- if or .Values.persistence.media.enabled .Values.persistence.appState.enabled }}
|
||||||
|
type: Recreate
|
||||||
|
{{- else }}
|
||||||
type: RollingUpdate
|
type: RollingUpdate
|
||||||
rollingUpdate:
|
rollingUpdate:
|
||||||
maxSurge: 1
|
maxSurge: 1
|
||||||
maxUnavailable: 0
|
maxUnavailable: 0
|
||||||
|
{{- end }}
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels: {{- include "vergabe.selectorLabels" . | nindent 8 }}
|
labels: {{- include "vergabe.selectorLabels" . | nindent 8 }}
|
||||||
|
|
@ -59,16 +64,29 @@ spec:
|
||||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
resources: {{- toYaml .Values.resources | nindent 12 }}
|
resources: {{- toYaml .Values.resources | nindent 12 }}
|
||||||
{{- if .Values.persistence.media.enabled }}
|
{{- if or .Values.persistence.media.enabled .Values.persistence.appState.enabled }}
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
|
{{- if .Values.persistence.media.enabled }}
|
||||||
- name: media
|
- name: media
|
||||||
mountPath: /app/media
|
mountPath: /app/media
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.persistence.appState.enabled }}
|
||||||
|
- name: app-state
|
||||||
|
mountPath: /app/.issue-facade
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.persistence.media.enabled }}
|
{{- if or .Values.persistence.media.enabled .Values.persistence.appState.enabled }}
|
||||||
volumes:
|
volumes:
|
||||||
|
{{- if .Values.persistence.media.enabled }}
|
||||||
- name: media
|
- name: media
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: {{ include "vergabe.fullname" . }}-media
|
claimName: {{ default (printf "%s-media" (include "vergabe.fullname" .)) .Values.persistence.media.existingClaim }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.persistence.appState.enabled }}
|
||||||
|
- name: app-state
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ default (printf "%s-app-state" (include "vergabe.fullname" .)) .Values.persistence.appState.existingClaim }}
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with .Values.nodeSelector }}
|
{{- with .Values.nodeSelector }}
|
||||||
nodeSelector: {{- toYaml . | nindent 8 }}
|
nodeSelector: {{- toYaml . | nindent 8 }}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,14 @@ spec:
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
name: http
|
name: http
|
||||||
selector: {{- include "vergabe.selectorLabels" . | nindent 4 }}
|
selector: {{- include "vergabe.selectorLabels" . | nindent 4 }}
|
||||||
{{- if .Values.persistence.media.enabled }}
|
{{- if and .Values.persistence.media.enabled (not .Values.persistence.media.existingClaim) }}
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "vergabe.fullname" . }}-media
|
name: {{ include "vergabe.fullname" . }}-media
|
||||||
|
annotations:
|
||||||
|
helm.sh/resource-policy: keep
|
||||||
labels: {{- include "vergabe.labels" . | nindent 4 }}
|
labels: {{- include "vergabe.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
storageClassName: {{ .Values.persistence.media.storageClass }}
|
storageClassName: {{ .Values.persistence.media.storageClass }}
|
||||||
|
|
@ -25,3 +27,19 @@ spec:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.persistence.media.size }}
|
storage: {{ .Values.persistence.media.size }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- if and .Values.persistence.appState.enabled (not .Values.persistence.appState.existingClaim) }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ include "vergabe.fullname" . }}-app-state
|
||||||
|
annotations:
|
||||||
|
helm.sh/resource-policy: keep
|
||||||
|
labels: {{- include "vergabe.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
storageClassName: {{ .Values.persistence.appState.storageClass }}
|
||||||
|
accessModes: [{{ .Values.persistence.appState.accessMode }}]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.appState.size }}
|
||||||
|
{{- end }}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
image:
|
image:
|
||||||
repository: forgejo.coulomb.social/coulomb/vergabe-teilnahme
|
repository: forgejo.coulomb.social/coulomb/vergabe-teilnahme
|
||||||
tag: "" # required; pinned via helm/vergabe-teilnahme-values.yaml
|
tag: "" # required; pinned via helm/vergabe-teilnahme-values.yaml
|
||||||
|
digest: "" # preferred; takes precedence over tag
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
replicaCount: 1 # v1 is single-instance; HA is deferred (RAILIANCE-WP-0002 Notes)
|
replicaCount: 1 # v1 is single-instance; HA is deferred (RAILIANCE-WP-0002 Notes)
|
||||||
|
|
||||||
|
# An invited company pilot requires an immutable image and both data volumes.
|
||||||
|
pilot:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
service:
|
service:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
port: 80
|
port: 80
|
||||||
|
|
@ -19,8 +24,7 @@ resources:
|
||||||
memory: 1Gi
|
memory: 1Gi
|
||||||
|
|
||||||
# Env from the K8s Secret created out-of-band (vergabe-teilnahme-env).
|
# Env from the K8s Secret created out-of-band (vergabe-teilnahme-env).
|
||||||
# Holds SECRET_KEY + DATABASE_URL. Created by the operator with kubectl
|
# Holds SECRET_KEY + DATABASE_URL. Deliver via the admitted platform custody lane.
|
||||||
# create secret generic vergabe-teilnahme-env --from-literal=...
|
|
||||||
envSecretName: vergabe-teilnahme-env
|
envSecretName: vergabe-teilnahme-env
|
||||||
|
|
||||||
# Non-secret env injected directly into the Deployment.
|
# Non-secret env injected directly into the Deployment.
|
||||||
|
|
@ -45,14 +49,21 @@ probes:
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
|
|
||||||
# PVC for media uploads is deferred — Django MEDIA is in-pod ephemeral
|
# Existing installations remain opt-in. Pilot mode refuses ephemeral state.
|
||||||
# for v1. Switch to true + a storageClass once media uploads land.
|
# PVCs are retained on Helm uninstall; a verified off-host backup is still required.
|
||||||
persistence:
|
persistence:
|
||||||
media:
|
media:
|
||||||
enabled: false
|
enabled: false
|
||||||
storageClass: local-path
|
storageClass: local-path
|
||||||
size: 5Gi
|
size: 5Gi
|
||||||
accessMode: ReadWriteOnce
|
accessMode: ReadWriteOnce
|
||||||
|
existingClaim: ""
|
||||||
|
appState:
|
||||||
|
enabled: false
|
||||||
|
storageClass: local-path
|
||||||
|
size: 1Gi
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
existingClaim: ""
|
||||||
|
|
||||||
podSecurityContext:
|
podSecurityContext:
|
||||||
runAsNonRoot: true
|
runAsNonRoot: true
|
||||||
|
|
|
||||||
12
docs/evidence/2026-09-11-vergabe-pilot-inventory.json
Normal file
12
docs/evidence/2026-09-11-vergabe-pilot-inventory.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"schema": "railiance.vergabe-pilot-inventory.v1",
|
||||||
|
"observed_at": "2026-09-11T14:27:23.565171+00:00",
|
||||||
|
"cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786",
|
||||||
|
"historical_namespace_present": false,
|
||||||
|
"matching_deployments": [],
|
||||||
|
"query_scope": "Deployment names/images on connected cluster only; databases and other hosts not inventoried",
|
||||||
|
"live_mutations": 0,
|
||||||
|
"customer_data_read": false,
|
||||||
|
"credentials_read": false,
|
||||||
|
"claim": "Historical runbook does not establish current deployment; no data-loss conclusion."
|
||||||
|
}
|
||||||
96
docs/vergabe-teilnahme-pilot.md
Normal file
96
docs/vergabe-teilnahme-pilot.md
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
# Invited company pilot
|
||||||
|
|
||||||
|
User decision, 2026-09-11: one company, several users, manual onboarding; pricing
|
||||||
|
later. VERGABE-WP-0019 owns product acceptance. RAPPS-WP-0014 owns placement,
|
||||||
|
release and recovery. This is a review and execution contract, not a claim that
|
||||||
|
the historical deployment is still live.
|
||||||
|
|
||||||
|
## Exact release packet
|
||||||
|
|
||||||
|
Before native admission, record the following non-secret values in the company
|
||||||
|
binding. Company/host/data selection is pending; do not treat example values as
|
||||||
|
an assigned tenant.
|
||||||
|
|
||||||
|
| Binding | Required evidence |
|
||||||
|
| --- | --- |
|
||||||
|
| Company and data | Named company contact, expected users, empty workspace or explicit import source/owner |
|
||||||
|
| Placement | Cluster UID, dedicated namespace/release, admitted operator and resource capacity |
|
||||||
|
| Release | Login-protected source commit, live CI result, published OCI digest, chart commit/version |
|
||||||
|
| Public route | Assigned HTTPS hostname, TLS receipt, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, edge login abuse control |
|
||||||
|
| Custody | Dedicated database/role, runtime Secret reference and platform delivery receipt; no values in the packet |
|
||||||
|
| Durable data | Media PVC and distinct issue-facade state PVC, database backup owner, storage class/capacity |
|
||||||
|
| Recovery/support | Consistent off-host backup, isolated restore and rollback receipt, cadence/retention, incident contact |
|
||||||
|
|
||||||
|
Use `helm/vergabe-teilnahme-pilot-values.example.yaml` as the starting point.
|
||||||
|
It intentionally has no image digest and cannot render until one is supplied.
|
||||||
|
The old published image `main-fa9f082` does not contain the new access gate.
|
||||||
|
Do not promote it merely because the older source suite passed.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
helm lint charts/vergabe-teilnahme -f path/to/reviewed-company-values.yaml
|
||||||
|
helm template vergabe-teilnahme charts/vergabe-teilnahme \
|
||||||
|
--namespace reviewed-company-namespace -f path/to/reviewed-company-values.yaml
|
||||||
|
python3 -m unittest discover -s tests -p 'test_vergabe_pilot_chart.py'
|
||||||
|
```
|
||||||
|
|
||||||
|
The default resource names are intentionally unchanged; company isolation is by
|
||||||
|
separate namespace/database/claims. Do not install two company releases in the
|
||||||
|
same namespace. Resolve the registry digest before setting `image.digest`.
|
||||||
|
Run the existing owner-approved server dry-run and deployment lane against the
|
||||||
|
reviewed placement after its packet is complete.
|
||||||
|
|
||||||
|
## Persistence and recovery
|
||||||
|
|
||||||
|
Mount media at `/app/media` and issue state at `/app/.issue-facade` on distinct
|
||||||
|
claims. Never expose the issue database through MEDIA_ROOT, ingress file serving
|
||||||
|
or an object bucket used for public assets. Production document downloads route
|
||||||
|
through Django authentication. All active users belong to this single company;
|
||||||
|
there is no per-tender ACL or shared-application tenant discriminator in v1.
|
||||||
|
|
||||||
|
The chart uses Recreate with persistence to avoid overlapping old/new pods and
|
||||||
|
RWO attachment conflicts. UID/GID/fsGroup 999 match the runtime image. Helm
|
||||||
|
uninstall retains claims; namespace deletion or node loss can still destroy
|
||||||
|
local-path data. Retention annotations are not a backup.
|
||||||
|
|
||||||
|
Before customer data is admitted, create a synthetic tender, lot, task, uploaded
|
||||||
|
document and issue-facade record; capture identifiers/content checksums. Replace
|
||||||
|
the application pod and verify all data and both user accounts remain usable.
|
||||||
|
For a coherent backup, quiesce application writes through the operator's
|
||||||
|
maintenance procedure, capture PostgreSQL plus media and a consistent SQLite
|
||||||
|
backup (including any required journal state), and retain the matched recovery
|
||||||
|
point off-host. Restore to an isolated database and new claims; `existingClaim`
|
||||||
|
can select those restored claims. Repeat login, document checksum and workflow
|
||||||
|
checks there. Record elapsed recovery time, backup age, image/schema revision
|
||||||
|
and operator. Never rehearse by overwriting historical `vergabe_db`.
|
||||||
|
|
||||||
|
Inspect every release's migrations before rollback; reverting an image alone
|
||||||
|
cannot undo an incompatible schema or restore lost data. Record the previously
|
||||||
|
accepted digest and demonstrated data recovery path. Short planned interruption
|
||||||
|
is acceptable for the invited pilot once the operator and company contact agree;
|
||||||
|
HA is not claimed.
|
||||||
|
|
||||||
|
## Manual onboarding and acceptance
|
||||||
|
|
||||||
|
Use the existing Django administration via the admitted operator path to create
|
||||||
|
ordinary active members. Keep staff/superuser access with the designated
|
||||||
|
operator. Supply initial credentials over the existing private human channel;
|
||||||
|
never paste them into Git, State Hub, command arguments or chat. Members can
|
||||||
|
change passwords in the UI; operators handle reset and deactivation manually.
|
||||||
|
Do not run `seed_dev` on a pilot/customer database.
|
||||||
|
|
||||||
|
Before admitting the first users, verify HTTPS login, CSRF failure behavior,
|
||||||
|
anonymous document refusal, two separate user sessions and deactivation of an
|
||||||
|
already logged-in account. Complete the tender → lot → task/document → domain
|
||||||
|
approval → submission workflow and feedback with the company contact. Customer
|
||||||
|
support contact, incident routing and backup responsibility must be recorded.
|
||||||
|
Pricing, automated invitation email, SSO and shared tenancy can be considered
|
||||||
|
later; none is implied by this initial pilot contract.
|
||||||
|
|
||||||
|
## Current inventory — 2026-09-11
|
||||||
|
|
||||||
|
Read-only checks on the connected Railiance cluster found no namespace
|
||||||
|
`vergabe-teilnahme` and no Deployment whose name/image contains vergabe or
|
||||||
|
teilnahme. The older `docs/vergabe-teilnahme.md` is historical deployment and
|
||||||
|
recovery evidence, not proof of current placement. Database contents and other
|
||||||
|
hosts have not been inventoried; no data-loss conclusion follows from namespace
|
||||||
|
absence. RAPPS-WP-0014-T02 retains that exact inventory/admission step.
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
> Current status, 2026-09-11: this is historical deployment evidence. The checked
|
||||||
|
> Railiance cluster has no `vergabe-teilnahme` namespace or matching Deployment.
|
||||||
|
> Use [the invited-pilot contract](vergabe-teilnahme-pilot.md) and RAPPS-WP-0014
|
||||||
|
> for fresh placement, release, custody and recovery. Do not execute the older
|
||||||
|
> credential/bootstrap recipes against an unverified target.
|
||||||
|
|
||||||
# vergabe-teilnahme — operator runbook
|
# vergabe-teilnahme — operator runbook
|
||||||
|
|
||||||
Production deployment of the Django tender-management app, shipped
|
Production deployment of the Django tender-management app, shipped
|
||||||
|
|
|
||||||
20
helm/vergabe-teilnahme-pilot-values.example.yaml
Normal file
20
helm/vergabe-teilnahme-pilot-values.example.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Review-only template. Copy into the exact admitted company binding, fill the
|
||||||
|
# tested image digest/host/Secret, and use a dedicated namespace and database.
|
||||||
|
# This does not upgrade or reuse the historical vergabe_db installation.
|
||||||
|
pilot:
|
||||||
|
enabled: true
|
||||||
|
image:
|
||||||
|
digest: "" # Required; the old published image lacks the pilot access gate.
|
||||||
|
replicaCount: 1
|
||||||
|
envSecretName: vergabe-pilot-env
|
||||||
|
env:
|
||||||
|
DJANGO_SETTINGS_MODULE: vergabe_teilnahme.settings.prod
|
||||||
|
ALLOWED_HOSTS: pilot.example.invalid,localhost
|
||||||
|
CSRF_TRUSTED_ORIGINS: https://pilot.example.invalid
|
||||||
|
probes:
|
||||||
|
hostHeader: pilot.example.invalid
|
||||||
|
persistence:
|
||||||
|
media:
|
||||||
|
enabled: true
|
||||||
|
appState:
|
||||||
|
enabled: true
|
||||||
110
tests/test_vergabe_pilot_chart.py
Normal file
110
tests/test_vergabe_pilot_chart.py
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
"""Render-level checks for data durability and separation; no cluster mutations.
|
||||||
|
|
||||||
|
Run with: python3 -m unittest discover -s tests -p 'test_vergabe_pilot_chart.py'
|
||||||
|
Requires Helm and PyYAML.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
CHART = Path(__file__).resolve().parents[1] / 'charts/vergabe-teilnahme'
|
||||||
|
DIGEST = 'sha256:' + 'a' * 64
|
||||||
|
|
||||||
|
|
||||||
|
def render(values):
|
||||||
|
return subprocess.run(
|
||||||
|
['helm', 'template', 'pilot', str(CHART), '-n', 'pilot-test', '-f', '-'],
|
||||||
|
input=yaml.safe_dump(values), text=True, capture_output=True, check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pilot():
|
||||||
|
return {
|
||||||
|
'pilot': {'enabled': True},
|
||||||
|
'image': {'digest': DIGEST},
|
||||||
|
'persistence': {'media': {'enabled': True}, 'appState': {'enabled': True}},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PilotChartTests(unittest.TestCase):
|
||||||
|
def objects(self, values):
|
||||||
|
result = render(values)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
return list(yaml.safe_load_all(result.stdout))
|
||||||
|
|
||||||
|
def test_existing_tag_profile_retains_original_rollout(self):
|
||||||
|
objects = self.objects({'image': {'tag': 'legacy-test'}})
|
||||||
|
deployment = next(o for o in objects if o['kind'] == 'Deployment')
|
||||||
|
self.assertEqual(deployment['spec']['strategy']['type'], 'RollingUpdate')
|
||||||
|
self.assertFalse(any(o['kind'] == 'PersistentVolumeClaim' for o in objects))
|
||||||
|
|
||||||
|
def test_pilot_pins_image_and_separates_durable_state(self):
|
||||||
|
objects = self.objects(pilot())
|
||||||
|
deployment = next(o for o in objects if o['kind'] == 'Deployment')
|
||||||
|
claims = {o['metadata']['name']: o for o in objects
|
||||||
|
if o['kind'] == 'PersistentVolumeClaim'}
|
||||||
|
self.assertEqual(len(claims), 2)
|
||||||
|
self.assertEqual(deployment['spec']['strategy'], {'type': 'Recreate'})
|
||||||
|
self.assertEqual(deployment['spec']['replicas'], 1)
|
||||||
|
pod = deployment['spec']['template']['spec']
|
||||||
|
app = pod['containers'][0]
|
||||||
|
self.assertTrue(app['image'].endswith('@' + DIGEST))
|
||||||
|
self.assertEqual({v['name']: v['mountPath'] for v in app['volumeMounts']},
|
||||||
|
{'media': '/app/media', 'app-state': '/app/.issue-facade'})
|
||||||
|
for volume in pod['volumes']:
|
||||||
|
claim = claims[volume['persistentVolumeClaim']['claimName']]
|
||||||
|
self.assertEqual(claim['metadata']['annotations']['helm.sh/resource-policy'], 'keep')
|
||||||
|
self.assertEqual(pod['securityContext']['fsGroup'], 999)
|
||||||
|
|
||||||
|
def test_pilot_refuses_each_ephemeral_store(self):
|
||||||
|
for store in ('media', 'appState'):
|
||||||
|
with self.subTest(store=store):
|
||||||
|
values = pilot()
|
||||||
|
values['persistence'][store]['enabled'] = False
|
||||||
|
result = render(values)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn('persistent media and appState', result.stderr)
|
||||||
|
|
||||||
|
def test_pilot_refuses_mutable_image_and_bad_digest(self):
|
||||||
|
for image in ({'tag': 'latest', 'digest': ''}, {'digest': 'sha256:invalid'}):
|
||||||
|
with self.subTest(image=image):
|
||||||
|
values = pilot()
|
||||||
|
values['image'] = image
|
||||||
|
self.assertNotEqual(render(values).returncode, 0)
|
||||||
|
|
||||||
|
def test_pilot_refuses_multiple_replicas(self):
|
||||||
|
values = pilot()
|
||||||
|
values['replicaCount'] = 2
|
||||||
|
result = render(values)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn('exactly one', result.stderr)
|
||||||
|
|
||||||
|
def test_restored_claims_are_reused_without_recreation(self):
|
||||||
|
values = pilot()
|
||||||
|
for store in ('media', 'appState'):
|
||||||
|
values['persistence'][store]['existingClaim'] = 'restored-' + store.lower()
|
||||||
|
objects = self.objects(values)
|
||||||
|
self.assertFalse(any(o['kind'] == 'PersistentVolumeClaim' for o in objects))
|
||||||
|
deployment = next(o for o in objects if o['kind'] == 'Deployment')
|
||||||
|
volumes = deployment['spec']['template']['spec']['volumes']
|
||||||
|
self.assertEqual({v['persistentVolumeClaim']['claimName'] for v in volumes},
|
||||||
|
{'restored-media', 'restored-appstate'})
|
||||||
|
|
||||||
|
def test_pilot_refuses_operational_state_in_download_volume(self):
|
||||||
|
for media, state in (('shared-data', 'shared-data'),
|
||||||
|
('vergabe-teilnahme-app-state', ''),
|
||||||
|
('', 'vergabe-teilnahme-media')):
|
||||||
|
with self.subTest(media=media, state=state):
|
||||||
|
values = pilot()
|
||||||
|
values['persistence']['media']['existingClaim'] = media
|
||||||
|
values['persistence']['appState']['existingClaim'] = state
|
||||||
|
result = render(values)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn('distinct claims', result.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
82
workplans/RAPPS-WP-0014-vergabe-invited-pilot.md
Normal file
82
workplans/RAPPS-WP-0014-vergabe-invited-pilot.md
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
---
|
||||||
|
id: RAPPS-WP-0014
|
||||||
|
type: workplan
|
||||||
|
title: "Deploy and recover the first invited Vergabe company pilot"
|
||||||
|
domain: financials
|
||||||
|
repo: railiance-apps
|
||||||
|
status: active
|
||||||
|
owner: the-custodian
|
||||||
|
topic_slug: railiance
|
||||||
|
created: "2026-09-11"
|
||||||
|
updated: "2026-09-11"
|
||||||
|
related: [VERGABE-WP-0019, VERGABE-WP-0018, HFACT-WP-0001]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Invited Vergabe pilot on Railiance
|
||||||
|
|
||||||
|
## Prepare a durable and immutable single-company chart
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAPPS-WP-0014-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
assignee: the-custodian
|
||||||
|
```
|
||||||
|
|
||||||
|
Extend the existing media PVC support with a distinct issue-facade state PVC,
|
||||||
|
optional existing claims for restore, retained claims on Helm uninstall and
|
||||||
|
Recreate rollout when persistent local state is mounted. Pilot mode requires
|
||||||
|
one replica, both durable stores, distinct claims and a valid OCI image digest.
|
||||||
|
The legacy opt-in behavior remains available for non-pilot installations.
|
||||||
|
Seven render regression tests and Helm lint pass. Chart 0.2.0 and the review-only
|
||||||
|
values template prepare the deployment; no live resources were changed.
|
||||||
|
|
||||||
|
## Bind the exact company, release, placement, data and access
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAPPS-WP-0014-T02
|
||||||
|
status: progress
|
||||||
|
priority: high
|
||||||
|
assignee: the-custodian
|
||||||
|
```
|
||||||
|
|
||||||
|
Consume VERGABE-WP-0019-T02's login-protected release after live CI/publication.
|
||||||
|
Record exact image/chart revision, company, user count, host/TLS, dedicated
|
||||||
|
namespace, database/role, both PVCs and admitted runtime Secret custody. Obtain
|
||||||
|
the user's empty-vs-existing-data disposition. Existing vergabe_db is not test
|
||||||
|
data. Resolve target inventory before using historical runbook names: the
|
||||||
|
checked Railiance cluster has no vergabe-teilnahme namespace or matching
|
||||||
|
Deployment on 2026-09-11. Do not infer data loss or authorization to recreate it.
|
||||||
|
|
||||||
|
Use `docs/vergabe-teilnahme-pilot.md` for the review packet. Secret creation,
|
||||||
|
operator access and placement consume existing platform lanes; they do not
|
||||||
|
create a parallel identity framework. The public edge needs an admitted login
|
||||||
|
abuse-control policy and TLS; Django's authentication gate alone is not a rate
|
||||||
|
limiter. Keep `/media/` behind the app gate. Exact customer identity/host/data
|
||||||
|
selection is pending user input; source preparation can proceed meanwhile.
|
||||||
|
|
||||||
|
## Demonstrate restart, isolated restore, rollback and operating ownership
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAPPS-WP-0014-T03
|
||||||
|
status: wait
|
||||||
|
priority: high
|
||||||
|
assignee: the-custodian
|
||||||
|
depends_on: [RAPPS-WP-0014-T02]
|
||||||
|
blocking_reason: "Await exact placement/release/data binding before native rehearsal and admission."
|
||||||
|
```
|
||||||
|
|
||||||
|
With synthetic fixture data on the admitted deployment, prove login and health,
|
||||||
|
two-user collaboration, document upload/download and issue state across pod
|
||||||
|
replacement. Establish a consistent recovery point for PostgreSQL, media and
|
||||||
|
issue-facade SQLite; rehearse recovery into a separate database and separate
|
||||||
|
claims, repeat the workflow, and record recovery time and checksums without
|
||||||
|
customer content. Record backup owner/cadence/retention/off-host destination,
|
||||||
|
restore command revision and monitoring/incident owner. Retained local-path
|
||||||
|
PVCs are neither off-host backup nor node-failure protection.
|
||||||
|
|
||||||
|
Review upgrade/migration effects and exact rollback image/data handling. A
|
||||||
|
single-writer Recreate release has a short service interruption; do not promise
|
||||||
|
HA. Return evidence to VERGABE-WP-0019-T03/T04 before customer invitations.
|
||||||
|
Native factory-produced delivery remains VERGABE-WP-0018/HFACT-WP-0001's separate
|
||||||
|
claim. Pricing and shared-app tenancy are outside this invited-pilot milestone.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue