Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Monorepo:
| `frontend/` | Angular-21-App (Hauptfokus, siehe unten) |
| `backend/` | Spring-Boot-Service (Java 21, wird parallel gebaut) |
| `docs/` | Design-Brief, Screens, Pencil-Quelle |
| `deploy/` | GitOps-Deployment: Helm-Chart + ArgoCD (siehe §5) |

Maßgebliche fachliche Referenz: `docs/design/policy-hub-design.md` + `docs/design/screens/`.
(Die Root-`README.md` und `docs/README.md` sind aktuell nur Platzhalter.)
Expand Down Expand Up @@ -193,19 +194,64 @@ Kompakter Überblick; Details in `backend/README.md`.

---

## 5. Beitrag & Git-Konventionen
## 5. CI/CD, Docker & Deployment

> ⚠️ Staging-Infrastruktur, **nicht** produktiv. Details in `deploy/README.md`.

### Docker (lokal)
- **Root `docker-compose.yml`** — voller Stack **ohne** Mocks (Postgres + Backend + Frontend):
`docker compose up --build`. Frontend `:4200` (Dev-Image mit Live-Mount), Backend `:8080`,
Postgres `:5432`. Aus `frontend/` auch via `npm run docker:dev`.
- **Dockerfiles:** Backend `backend/Dockerfile`; Frontend `frontend/cicd/docker/Dockerfile.development`
(Dev) und `Dockerfile.production` (nginx, SPA-Fallback, Port 8080, `nginx.conf` daneben).

### GitHub Actions (`.github/workflows/`)
- **`test.yml`** — CI-Gate: `backend-test` (`./gradlew test`, JUnit 5 + Testcontainers/Docker),
`frontend-unit-test` (`npm test`, Vitest), `frontend-e2e-test` (`npm run e2e`, Cypress gegen
Mock-Modus). Trigger: PRs gegen `main`, Push auf `main` (ignoriert `deploy/**` und `**/*.md`),
`workflow_dispatch`.
- **`build-and-publish.yml`** — baut Backend- & Frontend-Images, pusht nach
`ghcr.io/project-construct-x/policy-hub-{backend,frontend}` (Tags `<short-sha>` + `main`) und
**committet die neuen Image-Tags zurück** in `deploy/helm/policy-hub/values.yaml`
(Commit `ci: … [skip ci]`). Trigger: Push auf `main` (ohne `deploy/**`, `**/*.md`).

### Deployment (GitOps)
- **ArgoCD** (auto-sync, self-heal) rendert das Umbrella-Helm-Chart `deploy/helm/policy-hub/`
in Namespace `policyhub`: Postgres-StatefulSet, Backend- & Frontend-Deployment + Services, Ingress
`policy-hub.staging.construct-x.net` (`/` → Frontend, `/api` → Backend).
- **Flow:** Push auf `main` → Images gebaut/gepusht → Tags in `values.yaml` gebumpt → ArgoCD rollt aus.
- Layout: `helm/policy-hub/` (Chart), `argocd/{project,application}.yaml`, `secrets/` (Doku zum
Secret-Handling, siehe `secrets/README.md`).
- **Secrets (⚠️ nicht production-ready — K8s-Secrets sind nur base64-kodiert, nicht verschlüsselt):**
- **DB-Passwort** erzeugt das Chart selbst (`templates/secrets.yaml`, `randAlphaNum 32`) — **einmalig
beim ersten Install, keine automatische Rotation**. Zwei Guards halten den Wert stabil und sind
beide zwingend: der Helm-`lookup` im Template (deckt `helm upgrade` ab) **und**
`ignoreDifferences` auf `/data/db-password` + `RespectIgnoreDifferences=true` in
`argocd/application.yaml` (deckt ArgoCD ab, dessen repo-server ohne Cluster-Zugriff rendert).
Fällt einer weg, rotiert das Passwort bei jedem Reconcile und das Backend bricht gegen das
bestehende Postgres-PVC.
- **HTTP Basic Auth ist ein Provisorium** bis das echte Construct-X-Auth-Verfahren feststeht.
Bewusst isoliert in `templates/secret-basic-auth.yaml` + `auth`-Block in `values.yaml`
(Passwort **absichtlich im Klartext in git**), damit es in einem Zug entfernt werden kann — die
Ausbau-Anleitung steht im Kopf-Kommentar des Templates.
- Nur `ghcr-creds` wird noch manuell per `kubectl` angelegt.
- `argocd/application.yaml` wird per `kubectl apply` gebootstrappt, ist also **nicht** selbst
GitOps-verwaltet — nach Änderungen daran erneut applyen.

## 6. Beitrag & Git-Konventionen

- PRs gegen `main`; jeden PR möglichst an ein Issue verlinken (Templates unter `.github/`).
- **Conventional Commits** (bevorzugt), **signierte Commits** (bevorzugt), **License-Header** pro Datei.
- Branch-Namen: `feature/*`, `fix/*`.
- Dual-License: Apache-2.0 (Code) / CC-BY-4.0 (Non-Code).

## 6. Dokumentation
## 7. Dokumentation

- UI-Referenz: `docs/design/policy-hub-design.md` + `docs/design/screens/`.
- Barrierefreiheit: `docs/accessibility.md` (WCAG-2.2-AA-Stand + bekannte Restrisiken).
- Design-Quelle: `docs/design/Policy_hub.pen` (Pencil).
- Backend-Details: `backend/README.md`. Frontend-Details: `frontend/README.md`.
Deployment-Details: `deploy/README.md`.

---

Expand Down
23 changes: 17 additions & 6 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,24 @@ ArgoCD (auto-sync, self-heal) → namespace "policyhub"
| `helm/policy-hub/` | Umbrella Helm chart (backend, frontend, in-cluster postgres, ingress) |
| `argocd/project.yaml` | ArgoCD `AppProject` `policyhub` |
| `argocd/application.yaml` | Workload `Application` → `helm/policy-hub` |
| `secrets/` | Manual `kubectl` secret creation (see `secrets/README.md`) |
| `secrets/` | Secret handling — what the chart generates, what you create (see `secrets/README.md`) |

## Prerequisites in the cluster

- **ingress-nginx** controller and **cert-manager** with a `ClusterIssuer`
(set `ingress.clusterIssuer` in `values.yaml` — currently `letsencrypt-staging`).
- The two secrets (`policy-hub-secrets`, `ghcr-creds`) created in the namespace
- The image pull secret `ghcr-creds` created in the namespace — the only secret
still created by hand. The database password is generated by the chart on the
first install and the Basic-Auth password comes from `values.yaml`
(see `secrets/README.md`).
- ArgoCD has read access to `github.com/project-construct-x/policy-hub`
(add a repo credential in ArgoCD if the repo is private).

## First-time bootstrap

```sh
# 1. Namespace + secrets (once, out-of-band — see deploy/secrets/README.md).
# 1. Namespace + image pull secret (once, out-of-band — see deploy/secrets/README.md).
kubectl create namespace policyhub
kubectl create secret generic policy-hub-secrets -n policyhub \
--from-literal=db-password='<db-pw>' \
--from-literal=security-user-password='<admin-pw>'
kubectl create secret docker-registry ghcr-creds -n policyhub \
--docker-server=ghcr.io --docker-username=<user> --docker-password=<token>

Expand All @@ -58,6 +57,10 @@ kubectl apply -f deploy/argocd/project.yaml
kubectl apply -f deploy/argocd/application.yaml
```

> `deploy/argocd/application.yaml` is bootstrapped with `kubectl`, so it is not
> GitOps-managed: re-apply it whenever that file changes (e.g. after the
> `ignoreDifferences` block was added).

ArgoCD then syncs everything. Subsequent deploys are fully automatic: a push to
`main` publishes new images and bumps the tags, and ArgoCD rolls them out.

Expand All @@ -83,6 +86,14 @@ stack (`docker compose up --build`).
`postgres:16-alpine` image (not the Bitnami subchart — Bitnami moved its free
images to `bitnamilegacy` in 2025). Switch to an external DB by setting
`postgresql.enabled: false` and pointing `database.*` at it.
- **DB password**: generated once by the chart, then kept stable by a Helm
`lookup` plus the `ignoreDifferences` / `RespectIgnoreDifferences=true` pair
in `argocd/application.yaml`. Both guards are load-bearing — see
`secrets/README.md` before touching either.
- **Basic Auth is temporary and not production-ready.** The backend credential
lives in plaintext in `values.yaml` on purpose; the whole block is scoped to
`templates/secret-basic-auth.yaml` so it can be deleted in one step once the
real Construct-X auth mechanism is decided.
- **Recommended next step:** add a test gate to CI before publishing —
frontend `npm run lint` + `npm test`, backend `./gradlew build` (Testcontainers
work on GitHub-hosted runners).
13 changes: 13 additions & 0 deletions deploy/argocd/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,22 @@ spec:
destination:
server: https://kubernetes.default.svc
namespace: policyhub
# The database password is generated by the chart on the FIRST install only.
# ArgoCD's repo-server renders without cluster access, so the Helm `lookup`
# in templates/secrets.yaml cannot see the existing Secret and produces a
# fresh random value on every render. Ignoring that field — and respecting
# the ignore during sync — is what keeps the password stable instead of
# rotating (and breaking the Postgres PVC) on every reconcile.
ignoreDifferences:
- group: ""
kind: Secret
name: policy-hub-secrets
jsonPointers:
- /data/db-password
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- RespectIgnoreDifferences=true
14 changes: 12 additions & 2 deletions deploy/helm/policy-hub/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,15 @@ The app will be reachable at:
https://{{ .Values.ingress.host }}/api/... (backend)
{{- end }}

Prerequisite: the Secret "{{ .Values.secret.name }}" (keys: {{ .Values.secret.keys.dbPassword }}, {{ .Values.secret.keys.authPassword }})
and the image pull secret must exist in the namespace. See deploy/secrets/README.md.
Prerequisite: the image pull secret must exist in the namespace — it is the only
one created out-of-band. See deploy/secrets/README.md.

The database password in "{{ .Values.secret.name }}" is generated on the first install and
re-used afterwards (no automatic rotation). Read it with:
kubectl -n {{ .Release.Namespace }} get secret {{ .Values.secret.name }} -o jsonpath='{.data.{{ .Values.secret.keys.dbPassword }}}' | base64 -d
{{- if .Values.auth.enabled }}

TEMPORARY: the backend is behind HTTP Basic Auth ({{ .Values.auth.username }} / see values.yaml ->
auth.password, plaintext in git). Placeholder until the real Construct-X auth
mechanism lands — see templates/secret-basic-auth.yaml.
{{- end }}
9 changes: 7 additions & 2 deletions deploy/helm/policy-hub/templates/backend-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,18 @@ spec:
secretKeyRef:
name: {{ .Values.secret.name }}
key: {{ .Values.secret.keys.dbPassword }}
# --- TEMPORARY: HTTP Basic Auth, remove together with -----------
# --- templates/secret-basic-auth.yaml (see the notes there). ----
{{- if .Values.auth.enabled }}
- name: SPRING_SECURITY_USER_NAME
value: {{ .Values.auth.username | quote }}
- name: SPRING_SECURITY_USER_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.secret.name }}
key: {{ .Values.secret.keys.authPassword }}
name: {{ .Values.auth.secretName }}
key: {{ .Values.auth.secretKey }}
{{- end }}
# --- end TEMPORARY ----------------------------------------------
# Startup probe gives the app (Flyway migration + boot) time before
# the liveness probe can restart it.
startupProbe:
Expand Down
7 changes: 0 additions & 7 deletions deploy/helm/policy-hub/templates/ingress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,6 @@ spec:
name: {{ include "policy-hub.fullname" . }}-backend
port:
number: {{ .Values.backend.service.port }}
- path: /swagger-ui
pathType: Prefix
backend:
service:
name: {{ include "policy-hub.fullname" . }}-backend
port:
number: {{ .Values.backend.service.port }}
- path: /v3/api-docs
pathType: Prefix
backend:
Expand Down
40 changes: 40 additions & 0 deletions deploy/helm/policy-hub/templates/secret-basic-auth.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{{- /*
==========================================================================
TEMPORARY — HTTP Basic Auth for the backend. DO NOT BUILD ON THIS.
==========================================================================
The backend's Basic Auth (backend/src/main/java/org/constructx/policyhub/
core/security/SecurityConfig.java + application-prod.yaml) is a placeholder
until the real Construct-X authentication mechanism is decided. Which one
that will be is still open, so this is kept as small and as isolated as
possible — one file, one values block, one env block.

It is deliberately NOT production-ready:
- the password is committed to git IN PLAINTEXT (values.yaml -> auth.password)
because it is a throwaway staging credential; anyone with read access to
the repository can see it;
- Kubernetes Secrets are only base64-encoded, not encrypted.

Removing this once the real mechanism lands is self-contained:
1. delete this file,
2. delete the `auth:` block in values.yaml,
3. delete the SPRING_SECURITY_USER_* env entries in backend-deployment.yaml,
4. drop the basic-auth notes in deploy/secrets/README.md, deploy/README.md
and templates/NOTES.txt.

Note: application-prod.yaml has no defaults for SPRING_SECURITY_USER_NAME /
_PASSWORD, so the backend fails to start when `auth.enabled` is false. The
flag exists to prepare step 3 above, not as a runtime switch — keep it true
until the backend itself drops Basic Auth.
==========================================================================
*/ -}}
{{- if .Values.auth.enabled }}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.auth.secretName }}
labels:
{{- include "policy-hub.labels" . | nindent 4 }}
type: Opaque
stringData:
{{ .Values.auth.secretKey }}: {{ .Values.auth.password | quote }}
{{- end }}
44 changes: 41 additions & 3 deletions deploy/helm/policy-hub/templates/secrets.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,52 @@
{{- /*
Application secret — database password.

NOT PRODUCTION-READY: Kubernetes Secrets are only base64-encoded, never
encrypted. Anyone with read access to the namespace — or to an etcd backup —
can decode this value. Replace this with a real secrets manager (Sealed
Secrets, External Secrets Operator, Vault, …) before this leaves staging.

NO AUTOMATIC ROTATION: the password is generated exactly once, on the first
install, and re-used from then on. `randAlphaNum` is re-evaluated on every
render, so two independent guards keep the value stable:
1. the `lookup` below — covers `helm upgrade` with cluster access;
2. `ignoreDifferences` + `RespectIgnoreDifferences=true` in
deploy/argocd/application.yaml — covers ArgoCD, whose repo-server
renders without cluster access and therefore always gets nil here.
Losing either guard means the password rotates on every reconcile.

Rotating on purpose also requires re-initialising the database: Postgres
only honours POSTGRES_PASSWORD on the first initdb, so an existing PVC keeps
the old password. See deploy/secrets/README.md.

Precedence: explicit .Values.secret.dbPassword > value already in the
cluster > freshly generated.
*/ -}}
{{- if .Values.secret.create }}
{{- $dbKey := .Values.secret.keys.dbPassword }}
{{- $dbPassword := randAlphaNum 32 | b64enc }}
{{- $existing := lookup "v1" "Secret" .Release.Namespace .Values.secret.name }}
{{- if $existing }}
{{- $found := index (default (dict) $existing.data) $dbKey }}
{{- if $found }}{{ $dbPassword = $found }}{{ end }}
{{- end }}
{{- if .Values.secret.dbPassword }}
{{- $dbPassword = .Values.secret.dbPassword | b64enc }}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.secret.name }}
labels:
{{- include "policy-hub.labels" . | nindent 4 }}
annotations:
# Never prune or delete this Secret: it holds the only copy of the database
# password, and the Postgres PVC cannot be re-keyed without a re-init.
helm.sh/resource-policy: keep
argocd.argoproj.io/sync-options: Prune=false
type: Opaque
stringData:
{{ .Values.secret.keys.dbPassword }}: {{ .Values.secret.dbPassword | quote }}
{{ .Values.secret.keys.authPassword }}: {{ .Values.secret.authPassword | quote }}
data:
{{ $dbKey }}: {{ $dbPassword | quote }}
{{- end }}
---
{{- if .Values.ghcrCreds.create }}
Expand Down
28 changes: 19 additions & 9 deletions deploy/helm/policy-hub/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,32 @@ frontend:
database:
name: policyhub
username: policyhub
# Basic-auth user for the backend (non-secret parts). Password from the Secret.
# ---------------------------------------------------------------------------
# TEMPORARY — HTTP Basic Auth for the backend. NOT PRODUCTION-READY.
# Placeholder until the real Construct-X auth mechanism is decided. The
# password is plaintext in git ON PURPOSE: it is a throwaway staging
# credential, and the whole block is meant to be deleted rather than secured.
# templates/secret-basic-auth.yaml documents how to remove this in one go.
# ---------------------------------------------------------------------------
auth:
enabled: true
username: admin
# Name + keys of the Secret holding the sensitive values. Creates the Secret
# from dbPassword/authPassword below when `create: true`. Leave create: false
# (default) and set those two as ArgoCD Application parameter overrides
# (Application -> Parameters in the UI) instead of committing them here —
# see deploy/secrets/README.md.
password: ph-staging-8Qk2vTn4
secretName: policy-hub-basic-auth
secretKey: security-user-password
# Secret holding the database password. The chart owns and creates it: on the
# first install the password is generated (randAlphaNum 32), every later render
# re-uses the value already in the cluster — there is no automatic rotation.
# See templates/secrets.yaml and deploy/secrets/README.md.
# Set `dbPassword` only to force a specific password (restore / disaster
# recovery), e.g. as an ArgoCD Application parameter override; leave it empty
# otherwise. NOT PRODUCTION-READY: Secrets are base64-encoded, not encrypted.
secret:
name: policy-hub-secrets
create: false
create: true
dbPassword: ""
authPassword: ""
keys:
dbPassword: db-password
authPassword: security-user-password
# ---------------------------------------------------------------------------
# In-cluster PostgreSQL (self-contained StatefulSet, official image).
# Set enabled: false to use an external/managed database instead — then point
Expand Down
Loading
Loading