openova/platform/harbor/chart/values.yaml
e3mrah 88a8ecd8bb
fix(cutover): Reflector-mirror harbor-admin Secret + in-cluster trigger endpoint (#935) (#947)
Two bugs surfaced live on otech113 2026-05-05 blocking Self-Sovereignty
Cutover end-to-end. Fix both in lockstep:

Bug 1 — bp-self-sovereign-cutover Step 02 (harbor-projects) Job in
`catalyst` namespace was hitting `secret "harbor-core" not found` for
11+ retries because the upstream Harbor `harbor-core` Secret only
exists in the `harbor` namespace and Kubernetes forbids cross-namespace
secretKeyRef. Step 02 was stuck in CreateContainerConfigError forever.

  Fix: bp-harbor 1.2.13 → 1.2.14 ships a Catalyst-curated `harbor-admin`
  Secret in the `harbor` namespace with Reflector mirror annotations
  (allowed-namespaces=catalyst, auto-enabled). The same Secret name
  auto-materialises in `catalyst` so the cutover Job's secretKeyRef
  resolves natively. Password is randomly generated on first install
  (32-char alphanum, 190 bits entropy per feedback_passwords.md) and
  preserved across reconciles via `lookup`. The upstream Harbor subchart
  consumes it via `existingSecretAdminPassword: harbor-admin`.
  bp-self-sovereign-cutover 0.1.16 → 0.1.17 updates
  `harbor.adminSecretRef.name` from `harbor-core` to `harbor-admin`.

Bug 2 — The 0.1.16 auto-trigger Helm post-install Job (#933) POSTed
/api/v1/sovereign/cutover/start which sits behind RequireSession
middleware. The Job has no human session cookie — every request 401'd
forever and cutover never started.

  Fix: new catalyst-api endpoint POST /api/v1/internal/cutover/trigger
  lives OUTSIDE RequireSession and validates the bearer token via the
  apiserver's TokenReview API + checks the resolved username matches
  the canonical `bp-self-sovereign-cutover-runner` SA. Same engine,
  same idempotency, same state machine — different auth surface.
  The auto-trigger Job now mounts its projected SA token at
  /var/run/secrets/kubernetes.io/serviceaccount/token and sends it
  as `Authorization: Bearer <token>`. SA username + accepted list are
  runtime-overridable per Inviolable Principle #4.

Tests
  - 6 Go unit tests for HandleCutoverInternalTrigger covering happy
    path, missing bearer (401), TokenReview rejection (502), wrong SA
    (403), idempotency (no Jobs created when complete), wrong method
    (405). All pass.
  - bp-harbor admin-secret contract test (5 cases) — Secret renders,
    HARBOR_ADMIN_PASSWORD key present, Reflector annotations, keep
    policy, upstream consumes via existingSecretAdminPassword.
  - bp-self-sovereign-cutover cutover-contract test extended with 3
    new cases — auto-trigger uses /internal/cutover/trigger, sends
    SA bearer token, references harbor-admin (not harbor-core).
  - All 12 cutover-contract gates green; all 4 observability-toggle
    gates green; helm template + helm lint clean on both charts.

Bootstrap-kit slot pins
  - clusters/_template/bootstrap-kit/19-harbor.yaml: 1.2.13 → 1.2.14
  - clusters/_template/bootstrap-kit/06a-bp-self-sovereign-cutover.yaml:
    0.1.16 → 0.1.17

Closes #935

Co-authored-by: hatiyildiz <269457768+hatiyildiz@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:12:50 +04:00

494 lines
20 KiB
YAML

# Catalyst Blueprint umbrella metadata — the upstream chart is resolved as
# a Helm subchart via Chart.yaml `dependencies:`. Catalyst-curated values
# under the `harbor:` key flow into the upstream subchart unchanged.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), every operationally-
# meaningful value is configurable; cluster overlays in clusters/<sovereign>/
# may override any of these without rebuilding the Blueprint OCI artifact.
global:
# When set, ALL image pulls in this chart route through this registry.
# Used post-handover when the Sovereign's own Harbor takes over the
# proxy_cache role from contabo's central Harbor. Empty = no rewrite
# (image references use upstream defaults). The upstream harbor subchart
# exposes per-component `harbor.<component>.image.repository` for override.
# Per-Sovereign overlays should wire those alongside this value. Tracked under #560.
imageRegistry: ""
catalystBlueprint:
upstream:
chart: harbor
version: "1.18.3"
repo: "https://helm.goharbor.io"
# ─── Upstream chart values (subchart key: harbor) ────────────────────────
harbor:
# ─── External hostname / endpoint ──────────────────────────────────────
# Per-Sovereign overlay MUST set externalURL (e.g.
# "https://harbor.<location-code>.<sovereign-domain>"). Default is a
# placeholder so `helm template` smoke renders without `--set`.
externalURL: "https://harbor.example.local"
# ─── Admin password (issue #935) ───────────────────────────────────────
# The upstream goharbor/harbor chart writes its `harbor-core` Secret
# with key HARBOR_ADMIN_PASSWORD. Either it generates a fresh password
# from the chart values (`harborAdminPassword`) every reconcile — which
# rotates every helm upgrade and breaks every running session — OR it
# consumes a pre-existing Secret via `existingSecretAdminPassword`.
#
# The umbrella chart owns the Secret (templates/admin-secret.yaml)
# because:
# 1. Per docs/INVIOLABLE-PRINCIPLES.md #10, the password MUST be
# fully-random + per-install (no shared default across Sovereigns).
# 2. The bp-self-sovereign-cutover Step 02 (harbor-projects) Job
# runs in the `catalyst` namespace and must read the admin
# password via secretKeyRef — K8s forbids cross-namespace
# secretKeyRef. The umbrella's admin-secret carries Reflector
# annotations that mirror the Secret into `catalyst` so the
# cutover Job's references resolve. (Caught live on otech113
# 2026-05-05 — Job CreateContainerConfigError "secret
# harbor-core not found".)
# 3. lookup-on-reconcile keeps the bytes stable across helm upgrade
# so the rotation pathology above never fires.
#
# Operator override: set .Values.harborAdmin.password at the umbrella
# values layer to inject a specific password (e.g. when sealed-secrets
# already pin it). Empty = generate at first install + lookup on
# subsequent reconciles.
existingSecretAdminPassword: "harbor-admin"
existingSecretAdminPasswordKey: "HARBOR_ADMIN_PASSWORD"
# ─── Expose (clusterIP + Cilium Gateway HTTPRoute) ─────────────────────
# Catalyst standard (issue #387): expose harbor-core as a ClusterIP
# Service and route external traffic through the per-Sovereign Cilium
# Gateway via the HTTPRoute in templates/httproute.yaml. TLS terminates
# at the Gateway (wildcard cert from cert-manager — see
# clusters/_template/bootstrap-kit/01-cilium.yaml), so the upstream
# chart's expose.tls block is disabled here.
expose:
type: clusterIP
tls:
enabled: false
# ingress block retained for documentation / fallback only — when
# expose.type=clusterIP, the upstream chart does not render an Ingress.
ingress:
hosts:
core: "harbor.example.local"
# ─── Image-chart storage ───────────────────────────────────────────────
# Per ADR-0001 §13 + docs/omantel-handover-wbs.md §3 + §3a, S3-aware
# apps (Harbor is one) write DIRECTLY to the cloud-provider's native S3
# endpoint. On Hetzner Sovereigns that is Hetzner Object Storage
# (https://<region>.your-objectstorage.com), NOT SeaweedFS — SeaweedFS
# is reserved as a POSIX→S3 buffer for legacy POSIX-only writers and
# is not in the minimal Sovereign set.
#
# Defaults below leave bucket/region/regionendpoint EMPTY because the
# umbrella chart MUST render cleanly on contabo (which has no Hetzner
# credentials). Per-Sovereign HelmRelease in
# clusters/<sovereign>/bootstrap-kit/19-harbor.yaml supplies real
# values via Flux `valuesFrom` against the flux-system/object-storage
# Secret (issue #371, vendor-agnostic since #425).
#
# `type: filesystem` keeps the upstream chart on the local PVC path
# at default-render time (so an empty bucket/regionendpoint doesn't
# produce a half-broken S3 driver); the per-Sovereign overlay MUST
# flip type back to `s3` and set `existingSecret: harbor-objectstorage-credentials`
# once it supplies real config — see the bootstrap-kit slot for the
# wiring.
persistence:
enabled: true
resourcePolicy: "keep"
persistentVolumeClaim:
registry:
# Used in `type: filesystem` mode (contabo dev/test) AND as a
# tiny cache when `type: s3` is engaged. Per-Sovereign overlay
# MAY override storageClass.
existingClaim: ""
storageClass: ""
subPath: ""
accessMode: ReadWriteOnce
size: 5Gi
jobservice:
jobLog:
existingClaim: ""
storageClass: ""
subPath: ""
accessMode: ReadWriteOnce
size: 1Gi
database:
# Catalyst uses bp-cnpg for DB — no embedded postgres PVC. The
# upstream chart still requires this block to exist when
# `database.type: external`; size kept at upstream default.
existingClaim: ""
storageClass: ""
subPath: ""
accessMode: ReadWriteOnce
size: 1Gi
redis:
existingClaim: ""
storageClass: ""
subPath: ""
accessMode: ReadWriteOnce
size: 1Gi
trivy:
existingClaim: ""
storageClass: ""
subPath: ""
accessMode: ReadWriteOnce
size: 5Gi
imageChartStorage:
disableRedirect: false
# Default `filesystem` so contabo dev/test renders cleanly with
# no S3 endpoint configured. Per-Sovereign overlay flips this to
# `s3` and supplies bucket/region/regionendpoint/existingSecret
# via Flux valuesFrom.
type: filesystem
filesystem:
rootdirectory: /storage
s3:
# Per-Sovereign overlay populates these via Flux `valuesFrom`
# against flux-system/object-storage Secret (#371, vendor-
# agnostic since #425). The corresponding `targetPath` entries
# are documented in clusters/_template/bootstrap-kit/19-harbor.yaml.
region: ""
bucket: ""
# Credentials flow through `existingSecret` (NOT inline
# accesskey/secretkey) so the umbrella's templated Secret
# carries them — see templates/objectstorage-credentials.yaml.
# Per-Sovereign overlay MUST set existingSecret to the value of
# `objectStorage.credentialsSecretName` (default
# `harbor-objectstorage-credentials`).
existingSecret: ""
accesskey: ""
secretkey: ""
regionendpoint: ""
encrypt: false
keyid: ""
secure: true
v4auth: true
chunksize: ""
rootdirectory: ""
storageclass: "STANDARD"
multipartcopychunksize: ""
multipartcopymaxconcurrency: ""
multipartcopythresholdsize: ""
# ─── Database (external CNPG) ──────────────────────────────────────────
# Per-Sovereign overlay supplies CNPG Cluster ref via ExternalSecret +
# populates `host`, `password` from the secret. Default is placeholder
# so `helm template` smoke renders without `--set`.
database:
type: external
external:
host: "harbor-pg-rw.harbor.svc.cluster.local"
port: "5432"
username: "harbor"
password: ""
coreDatabase: "registry"
existingSecret: "harbor-database-secret" # ESO-projected from CNPG
sslmode: "disable"
maxIdleConns: 100
maxOpenConns: 900
podAnnotations: {}
# ─── Redis (internal — for solo Sovereign; HA overlay swaps to Valkey) ─
redis:
type: internal
internal:
image:
repository: goharbor/redis-photon
tag: "v2.14.3"
resources:
requests:
memory: 64Mi
cpu: 50m
external:
addr: ""
sentinelMasterSet: ""
coreDatabaseIndex: "0"
jobserviceDatabaseIndex: "1"
registryDatabaseIndex: "2"
trivyAdapterIndex: "5"
password: ""
existingSecret: ""
# ─── Image pin (per docs/INVIOLABLE-PRINCIPLES.md #4) ─────────────────
# The upstream chart sub-images come pre-pinned to appVersion 2.14.3 by
# the bundled defaults; we leave them at upstream defaults rather than
# diverging.
# ─── Component replica counts ──────────────────────────────────────────
portal:
image:
repository: goharbor/harbor-portal
tag: "v2.14.3"
replicas: 1
resources:
requests:
memory: 64Mi
cpu: 50m
limits:
memory: 256Mi
cpu: 500m
core:
image:
repository: goharbor/harbor-core
tag: "v2.14.3"
replicas: 1
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 1
# `secretName` lets per-Sovereign overlay supply the Harbor admin
# secret (CSRF key, JWT signing key, admin password). Default empty
# — chart auto-generates on first install (rotated by overlay later).
secretName: ""
# Internal TLS — Catalyst's Cilium WireGuard handles cluster-internal
# traffic encryption; Harbor's internal TLS off by default.
tokenCert: ""
tokenKey: ""
jobservice:
image:
repository: goharbor/harbor-jobservice
tag: "v2.14.3"
replicas: 1
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 1
maxJobWorkers: 10
jobLoggers:
- file
loggerSweeperDuration: 14
registry:
registry:
image:
repository: goharbor/registry-photon
tag: "v2.14.3"
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 1
controller:
image:
repository: goharbor/harbor-registryctl
tag: "v2.14.3"
resources:
requests:
memory: 64Mi
cpu: 50m
limits:
memory: 256Mi
cpu: 500m
replicas: 1
middleware:
enabled: false
# ─── Trivy vulnerability scanner ───────────────────────────────────────
# DEFAULT FALSE per docs/BLUEPRINT-AUTHORING.md §11.2 logic — bp-trivy
# (slot 30) reconciles AFTER bp-harbor (slot 19) in the bootstrap kit
# ordering, so Trivy scanning here would race against an absent CRD
# surface. Per-Sovereign overlay flips this on once bp-trivy is Ready.
trivy:
enabled: false
image:
repository: goharbor/trivy-adapter-photon
tag: "v2.14.3"
replicas: 1
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 1
skipUpdate: false
offlineScan: false
timeout: 5m0s
# ─── Notary (image signing) — DEPRECATED in upstream Harbor 2.x ────────
# Sigstore (bp-sigstore, slot 32) is the Catalyst signing path. Notary
# OFF.
notary:
enabled: false
# ─── Exporter / metrics ────────────────────────────────────────────────
# ServiceMonitor / metrics — DEFAULT FALSE per docs/BLUEPRINT-AUTHORING
# .md §11.2 (Observability toggles default false; kube-prometheus-stack
# CRDs may not exist on a fresh Sovereign).
metrics:
enabled: false
core:
path: /metrics
port: 8001
registry:
path: /metrics
port: 8001
jobservice:
path: /metrics
port: 8001
exporter:
path: /metrics
port: 8001
serviceMonitor:
enabled: false
# ─── Logging ──────────────────────────────────────────────────────────
logLevel: info
# ─── Update strategy ──────────────────────────────────────────────────
updateStrategy:
type: RollingUpdate
# ─── Harbor admin credentials (issue #935) ────────────────────────────────
# Catalyst-curated admin Secret that the umbrella chart materialises in
# the harbor namespace and Reflector-mirrors into `catalyst` so the
# bp-self-sovereign-cutover Step 02 Job (which lives in the `catalyst`
# namespace) can read HARBOR_ADMIN_PASSWORD via secretKeyRef.
#
# Persistence: lookup-on-reconcile keeps the password stable across
# every helm upgrade. First-install path generates a 32-char alphanum
# password (190 bits entropy, per feedback_passwords.md). Operator may
# inject a fixed password via .password (e.g. when sealed-secrets pin
# it). The Secret is annotated with helm.sh/resource-policy=keep so
# `helm uninstall` does NOT delete the bytes — a re-install picks the
# same password back up via lookup.
harborAdmin:
# Name of the Secret materialised in `.Release.Namespace` (the harbor
# namespace by convention). The upstream Harbor subchart's
# `existingSecretAdminPassword` field (default "harbor-admin", set in
# the harbor: subchart values above) MUST match this value.
secretName: "harbor-admin"
# Operator-supplied password override. Empty = generate at first
# install + preserve via lookup on every reconcile thereafter.
password: ""
# Comma-separated list of namespaces Reflector mirrors the admin
# Secret into. `catalyst` is required for the cutover Job; operators
# may add more if other in-cluster consumers need the bytes.
reflectionAllowedNamespaces: "catalyst"
# ─── Catalyst overlay values (consumed by templates/ in this chart) ──────
# Reserved for Catalyst-side overlays added in a follow-up PR once
# bp-harbor is consumed in clusters/_template/:
# - NetworkPolicy isolating harbor namespace from non-Catalyst traffic
# - ExternalSecret projecting CNPG password + SeaweedFS S3 keys via ESO
# (bp-external-secrets) so values.database.external.password and
# values.persistence.imageChartStorage.s3.{accesskey,secretkey} land
# via Reloader-restart-on-rotation rather than chart-render-time.
# - cert-manager Certificate for harbor.<location-code>.<sovereign-domain>
# pointing at the per-Sovereign Issuer.
# - Keycloak OIDC client wiring for SSO when ssoEnabled flips true.
# - Harbor Project + Pull-mirror policy bootstrap (project: catalyst,
# pull-mirror: ghcr.io/openova-io/* every 6h).
harborOverlay:
networkPolicy:
enabled: false
cnpgNamespace: "cnpg"
keycloakNamespace: "keycloak"
serviceMonitor:
enabled: false # docs/BLUEPRINT-AUTHORING.md §11.2
interval: "30s"
namespace: "" # default: release namespace
externalSecret:
enabled: false
certificate:
enabled: false
issuerRef:
kind: ClusterIssuer
name: ""
sso:
enabled: false
keycloakRealm: ""
keycloakClientId: ""
pullMirror:
enabled: false
upstreams: [] # list of {name, url, schedule, filter} entries
# ─── Catalyst HTTPRoute (Cilium Gateway API, issue #387) ──────────────────
# Replaces the upstream chart's networking.k8s.io/v1.Ingress on Sovereigns.
# Per-Sovereign overlay supplies `gateway.host` (e.g. registry.<sovereign-fqdn>).
# parentRef defaults to cilium-gateway/kube-system, the per-Sovereign
# Gateway shipped by clusters/_template/bootstrap-kit/01-cilium.yaml.
gateway:
enabled: true
# host: registry.<sovereign-fqdn> — REQUIRED, no default per Inviolable Principle #4
host: ""
path: "/"
# Backend Service — defaults to <release>-harbor-core (harbor subchart core Service)
backendService: ""
backendPort: 80
parentRef:
name: cilium-gateway
namespace: kube-system
sectionName: https
# ─── Vendor-agnostic Object Storage backend config (issue #383 / #425) ───
#
# Per ADR-0001 §13 + docs/omantel-handover-wbs.md §3 + §3a, S3-aware
# apps (Harbor is one) write DIRECTLY to the cloud-provider's native S3
# endpoint. The per-Sovereign HelmRelease populates these fields via
# Flux `valuesFrom` against the canonical `flux-system/object-storage`
# Secret. Per #425 the seam is vendor-agnostic — same Secret name +
# same chart-values shape support a future AWS / Azure / GCP / OCI
# Sovereign without renaming the seam.
#
# Default values below produce a clean render on contabo (no
# credentials configured, type: filesystem) — see the bootstrap-kit
# slot at clusters/_template/bootstrap-kit/19-harbor.yaml for the
# canonical mapping.
# ─── CNPG Postgres cluster for Harbor metadata DB ────────────────────────
# These values are consumed by templates/cnpg-cluster.yaml (the Cluster CR)
# and templates/database-secret.yaml (the harbor-database-secret Secret).
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), all operationally-
# meaningful sizing/naming values are overridable via per-Sovereign overlays.
postgres:
cluster:
name: harbor-pg
# Default to the chart's own targetNamespace. Override via per-Sovereign
# overlay if the CNPG cluster lives in a different namespace.
namespace: harbor
instances: 1 # bump to 3 alongside HA overlay once beyond Phase-0
storageSize: 5Gi
storageClass: local-path # Contabo k3s default; Hetzner Sovereign overlays → hcloud-volumes
pgVersion: "16"
# MUST match harbor.database.external.coreDatabase. Harbor upstream
# defaults coreDatabase="registry" and connects to a DB named "registry".
database: registry
owner: harbor
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }
objectStorage:
# When false, no credentials Secret is rendered (contabo, local dev,
# etc.). Per-Sovereign overlay flips to true.
enabled: false
# When true, skip rendering the harbor-namespace credentials Secret
# entirely — the operator has already created one out-of-band
# (sealed-secret / external-secret / cloud-init / etc.).
useExistingSecret: false
# Override the default secret name — must match the value supplied to
# the upstream chart's persistence.imageChartStorage.s3.existingSecret
# field by the per-Sovereign overlay.
credentialsSecretName: ""
s3:
# Operator-issued S3 access/secret keys. Plaintext at runtime ONLY
# — Flux populates these via valuesFrom at HelmRelease apply time,
# never committed to git.
accessKey: ""
secretKey: ""