From 1a5a3d44dac2d9ea048f9d6b2d78dce6701aeaf1 Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Thu, 6 Aug 2026 15:40:33 -0500 Subject: [PATCH 1/7] feat: add e2e-ui local workbench charts and env Applications Add Helm deploy charts for api/ui with cluster-dev vs package posture, local PSQLCluster for the API, and hops.local Application YAMLs under gitops/env/local. Document hops local up as Option A for dogfood. Implements [[tasks/lwb-e2e-ui-gitops-charts]] [[tasks/lwb-docs-happy-path]] --- tests/e2e-ui/README.md | 17 +++++++ tests/e2e-ui/api/.gitops/deploy/Chart.yaml | 6 +++ .../api/.gitops/deploy/templates/_helpers.tpl | 9 ++++ .../templates/deployment-cluster-dev.yaml | 48 ++++++++++++++++++ .../deploy/templates/deployment-package.yaml | 31 ++++++++++++ .../.gitops/deploy/templates/psqlcluster.yaml | 15 ++++++ .../api/.gitops/deploy/templates/service.yaml | 13 +++++ tests/e2e-ui/api/.gitops/deploy/values.yaml | 44 ++++++++++++++++ tests/e2e-ui/gitops/env/local/api.yaml | 15 ++++++ tests/e2e-ui/gitops/env/local/ui.yaml | 15 ++++++ tests/e2e-ui/ui/.gitops/deploy/Chart.yaml | 6 +++ .../ui/.gitops/deploy/templates/_helpers.tpl | 9 ++++ .../templates/deployment-cluster-dev.yaml | 50 +++++++++++++++++++ .../deploy/templates/deployment-package.yaml | 27 ++++++++++ .../ui/.gitops/deploy/templates/service.yaml | 13 +++++ tests/e2e-ui/ui/.gitops/deploy/values.yaml | 32 ++++++++++++ 16 files changed, 350 insertions(+) create mode 100644 tests/e2e-ui/api/.gitops/deploy/Chart.yaml create mode 100644 tests/e2e-ui/api/.gitops/deploy/templates/_helpers.tpl create mode 100644 tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml create mode 100644 tests/e2e-ui/api/.gitops/deploy/templates/deployment-package.yaml create mode 100644 tests/e2e-ui/api/.gitops/deploy/templates/psqlcluster.yaml create mode 100644 tests/e2e-ui/api/.gitops/deploy/templates/service.yaml create mode 100644 tests/e2e-ui/api/.gitops/deploy/values.yaml create mode 100644 tests/e2e-ui/gitops/env/local/api.yaml create mode 100644 tests/e2e-ui/gitops/env/local/ui.yaml create mode 100644 tests/e2e-ui/ui/.gitops/deploy/Chart.yaml create mode 100644 tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl create mode 100644 tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml create mode 100644 tests/e2e-ui/ui/.gitops/deploy/templates/deployment-package.yaml create mode 100644 tests/e2e-ui/ui/.gitops/deploy/templates/service.yaml create mode 100644 tests/e2e-ui/ui/.gitops/deploy/values.yaml diff --git a/tests/e2e-ui/README.md b/tests/e2e-ui/README.md index 3d34c7d0..67a0df18 100644 --- a/tests/e2e-ui/README.md +++ b/tests/e2e-ui/README.md @@ -4,6 +4,23 @@ A copyable Distributed service and SvelteKit UI demonstrating one modeled projection from aggregate transition to server read model, generated GraphQL client, optimistic replica update, and causal confirmation. +## Option A — hops local workbench (preferred for cluster-shaped DX) + +One-time: `hops local start` (control plane). Then: + +```bash +cd tests/e2e-ui +hops local up ./gitops/env/local --name e2e +hops local status +hops local open +# when done +hops local down --name e2e +``` + +No volume/hostPath literacy required. Charts: `api/.gitops/deploy`, `ui/.gitops/deploy`. Env Applications: `gitops/env/local/`. + +## Option B — compose + host processes + ```bash cd tests/e2e-ui make up diff --git a/tests/e2e-ui/api/.gitops/deploy/Chart.yaml b/tests/e2e-ui/api/.gitops/deploy/Chart.yaml new file mode 100644 index 00000000..fde17c03 --- /dev/null +++ b/tests/e2e-ui/api/.gitops/deploy/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: e2e-ui-api +description: Local workbench dogfood chart for e2e-ui API (cluster-dev / package postures) +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/tests/e2e-ui/api/.gitops/deploy/templates/_helpers.tpl b/tests/e2e-ui/api/.gitops/deploy/templates/_helpers.tpl new file mode 100644 index 00000000..42efd301 --- /dev/null +++ b/tests/e2e-ui/api/.gitops/deploy/templates/_helpers.tpl @@ -0,0 +1,9 @@ +{{- define "e2e-ui-api.name" -}} +{{- default .Chart.Name .Values.name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "e2e-ui-api.labels" -}} +app.kubernetes.io/name: {{ include "e2e-ui-api.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} diff --git a/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml new file mode 100644 index 00000000..a615a2c3 --- /dev/null +++ b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml @@ -0,0 +1,48 @@ +{{- if or (eq .Values.appRuntime "cluster-dev") (and .Values.local (eq .Values.appRuntime "cluster-dev")) }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "e2e-ui-api.name" . }} + labels: + {{- include "e2e-ui-api.labels" . | nindent 4 }} + hops.ops.com.ai/runtime: cluster-dev +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: {{ include "e2e-ui-api.name" . }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "e2e-ui-api.name" . }} + hops.ops.com.ai/runtime: cluster-dev + spec: + containers: + - name: api + image: {{ .Values.clusterDev.image | quote }} + workingDir: {{ .Values.clusterDev.workingDir | quote }} + command: + {{- toYaml .Values.clusterDev.command | nindent 12 }} + ports: + - containerPort: {{ .Values.service.targetPort }} + name: http + env: + - name: BIND + value: {{ .Values.env.BIND | quote }} + volumeMounts: + - name: source + mountPath: {{ .Values.sourceDelivery.mountPath }} + - name: cargo-target + mountPath: {{ .Values.sourceDelivery.mountPath }}/target + volumes: + - name: source + {{- if eq .Values.sourceDelivery.mode "hostPath" }} + hostPath: + path: {{ .Values.sourceDelivery.hostPath | quote }} + type: Directory + {{- else }} + emptyDir: {} + {{- end }} + - name: cargo-target + emptyDir: {} +{{- end }} diff --git a/tests/e2e-ui/api/.gitops/deploy/templates/deployment-package.yaml b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-package.yaml new file mode 100644 index 00000000..679bb3a9 --- /dev/null +++ b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-package.yaml @@ -0,0 +1,31 @@ +{{- /* Package Deployment: production/CI image posture. Omitted for local/cluster-dev. */ -}} +{{- if and (not .Values.local) (ne .Values.appRuntime "cluster-dev") (ne .Values.appRuntime "host") }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "e2e-ui-api.name" . }} + labels: + {{- include "e2e-ui-api.labels" . | nindent 4 }} + hops.ops.com.ai/runtime: package +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: {{ include "e2e-ui-api.name" . }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "e2e-ui-api.name" . }} + hops.ops.com.ai/runtime: package + spec: + containers: + - name: api + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.service.targetPort }} + name: http + env: + - name: BIND + value: {{ .Values.env.BIND | quote }} +{{- end }} diff --git a/tests/e2e-ui/api/.gitops/deploy/templates/psqlcluster.yaml b/tests/e2e-ui/api/.gitops/deploy/templates/psqlcluster.yaml new file mode 100644 index 00000000..12eb9f56 --- /dev/null +++ b/tests/e2e-ui/api/.gitops/deploy/templates/psqlcluster.yaml @@ -0,0 +1,15 @@ +{{- if and .Values.database.enabled (or .Values.local (eq .Values.appRuntime "cluster-dev") (eq .Values.appRuntime "host")) }} +# Local-capable PSQLCluster XR. Storage class defaults to values.database.storage.class +# (local-path for k3s/dory; override for other provisioners). +apiVersion: hops.ops.com.ai/v1alpha1 +kind: PSQLCluster +metadata: + name: {{ .Values.database.name }} + labels: + {{- include "e2e-ui-api.labels" . | nindent 4 }} +spec: + clusterName: {{ .Values.database.clusterName }} + storage: + size: {{ .Values.database.storage.size }} + class: {{ .Values.database.storage.class }} +{{- end }} diff --git a/tests/e2e-ui/api/.gitops/deploy/templates/service.yaml b/tests/e2e-ui/api/.gitops/deploy/templates/service.yaml new file mode 100644 index 00000000..b6ff9833 --- /dev/null +++ b/tests/e2e-ui/api/.gitops/deploy/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "e2e-ui-api.name" . }} + labels: + {{- include "e2e-ui-api.labels" . | nindent 4 }} +spec: + selector: + app.kubernetes.io/name: {{ include "e2e-ui-api.name" . }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} diff --git a/tests/e2e-ui/api/.gitops/deploy/values.yaml b/tests/e2e-ui/api/.gitops/deploy/values.yaml new file mode 100644 index 00000000..5fddaa5e --- /dev/null +++ b/tests/e2e-ui/api/.gitops/deploy/values.yaml @@ -0,0 +1,44 @@ +# Local workbench values for e2e-ui API. +# Storage class for PSQLCluster: override per local CP (e.g. local-path, hostpath, psql). +# Default "local-path" works with k3s (dory/colima) and kind with local-path-provisioner. + +local: false +appRuntime: package # host | cluster-dev | package + +name: e2e-ui-api +image: + repository: ghcr.io/hops-ops/e2e-ui-api + tag: latest + pullPolicy: IfNotPresent + +# Generic runtime image for cluster-dev (no app package rebuild per edit). +clusterDev: + image: rust:1.85-bookworm + command: + - /bin/bash + - -lc + - cargo run -p e2e-runner --bin e2e-ui + workingDir: /workspace + sourcePath: /workspace + +service: + port: 8791 + targetPort: 8791 + +env: + BIND: "0.0.0.0:8791" + +database: + enabled: true + name: e2e-ui-api-psql + clusterName: local + storage: + size: 2Gi + # Override for your local CP storage class if needed. + class: local-path + +# Source delivery (injected by hops local when hostPath/sync is selected). +sourceDelivery: + mode: none # none | hostPath | sync + hostPath: "" + mountPath: /workspace diff --git a/tests/e2e-ui/gitops/env/local/api.yaml b/tests/e2e-ui/gitops/env/local/api.yaml new file mode 100644 index 00000000..3dd626fc --- /dev/null +++ b/tests/e2e-ui/gitops/env/local/api.yaml @@ -0,0 +1,15 @@ +apiVersion: hops.local/v1alpha1 +kind: Application +metadata: + name: e2e-ui-api +spec: + source: + path: ../../../api/.gitops/deploy + helm: + values: + local: true + appRuntime: cluster-dev + destination: + namespace: hops-wt-default + syncPolicy: + prune: false diff --git a/tests/e2e-ui/gitops/env/local/ui.yaml b/tests/e2e-ui/gitops/env/local/ui.yaml new file mode 100644 index 00000000..29f99bd9 --- /dev/null +++ b/tests/e2e-ui/gitops/env/local/ui.yaml @@ -0,0 +1,15 @@ +apiVersion: hops.local/v1alpha1 +kind: Application +metadata: + name: e2e-ui-ui +spec: + source: + path: ../../../ui/.gitops/deploy + helm: + values: + local: true + appRuntime: cluster-dev + destination: + namespace: hops-wt-default + syncPolicy: + prune: false diff --git a/tests/e2e-ui/ui/.gitops/deploy/Chart.yaml b/tests/e2e-ui/ui/.gitops/deploy/Chart.yaml new file mode 100644 index 00000000..88e9cbc9 --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: e2e-ui-ui +description: Local workbench dogfood chart for e2e-ui SvelteKit UI +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl b/tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl new file mode 100644 index 00000000..d15cfe4b --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl @@ -0,0 +1,9 @@ +{{- define "e2e-ui-ui.name" -}} +{{- default .Chart.Name .Values.name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "e2e-ui-ui.labels" -}} +app.kubernetes.io/name: {{ include "e2e-ui-ui.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml new file mode 100644 index 00000000..269618f7 --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml @@ -0,0 +1,50 @@ +{{- if eq .Values.appRuntime "cluster-dev" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "e2e-ui-ui.name" . }} + labels: + {{- include "e2e-ui-ui.labels" . | nindent 4 }} + hops.ops.com.ai/runtime: cluster-dev +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: {{ include "e2e-ui-ui.name" . }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "e2e-ui-ui.name" . }} + hops.ops.com.ai/runtime: cluster-dev + spec: + containers: + - name: ui + image: {{ .Values.clusterDev.image | quote }} + workingDir: {{ .Values.clusterDev.workingDir | quote }} + command: + {{- toYaml .Values.clusterDev.command | nindent 12 }} + ports: + - containerPort: {{ .Values.service.targetPort }} + name: http + env: + - name: HOST + value: {{ .Values.env.HOST | quote }} + - name: PORT + value: {{ .Values.env.PORT | quote }} + volumeMounts: + - name: source + mountPath: {{ .Values.sourceDelivery.mountPath }} + - name: node-modules + mountPath: {{ .Values.sourceDelivery.mountPath }}/node_modules + volumes: + - name: source + {{- if eq .Values.sourceDelivery.mode "hostPath" }} + hostPath: + path: {{ .Values.sourceDelivery.hostPath | quote }} + type: Directory + {{- else }} + emptyDir: {} + {{- end }} + - name: node-modules + emptyDir: {} +{{- end }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-package.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-package.yaml new file mode 100644 index 00000000..403b1cbc --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-package.yaml @@ -0,0 +1,27 @@ +{{- if and (not .Values.local) (ne .Values.appRuntime "cluster-dev") (ne .Values.appRuntime "host") }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "e2e-ui-ui.name" . }} + labels: + {{- include "e2e-ui-ui.labels" . | nindent 4 }} + hops.ops.com.ai/runtime: package +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: {{ include "e2e-ui-ui.name" . }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "e2e-ui-ui.name" . }} + hops.ops.com.ai/runtime: package + spec: + containers: + - name: ui + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.service.targetPort }} + name: http +{{- end }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/service.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/service.yaml new file mode 100644 index 00000000..1bc71f23 --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "e2e-ui-ui.name" . }} + labels: + {{- include "e2e-ui-ui.labels" . | nindent 4 }} +spec: + selector: + app.kubernetes.io/name: {{ include "e2e-ui-ui.name" . }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/values.yaml b/tests/e2e-ui/ui/.gitops/deploy/values.yaml new file mode 100644 index 00000000..d6b9bdec --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/values.yaml @@ -0,0 +1,32 @@ +# Local workbench values for e2e-ui UI. + +local: false +appRuntime: package # host | cluster-dev | package + +name: e2e-ui-ui +image: + repository: ghcr.io/hops-ops/e2e-ui-ui + tag: latest + pullPolicy: IfNotPresent + +clusterDev: + image: node:22-bookworm + command: + - /bin/bash + - -lc + - npm install && npm run dev -- --host 0.0.0.0 --port 5180 + workingDir: /workspace + sourcePath: /workspace + +service: + port: 5180 + targetPort: 5180 + +env: + HOST: "0.0.0.0" + PORT: "5180" + +sourceDelivery: + mode: none # none | hostPath | sync + hostPath: "" + mountPath: /workspace From caf53e5ea8d1719f29ccfa416fe2373997922e5f Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Thu, 6 Aug 2026 15:58:12 -0500 Subject: [PATCH 2/7] fix: cluster-dev wait for source sync before starting apps When sourceDelivery.mode=sync, wait for .hops-synced / package manifest before cargo/vite so tar/mutagen can populate emptyDir first. --- .../deploy/templates/deployment-cluster-dev.yaml | 16 ++++++++++++++++ .../deploy/templates/deployment-cluster-dev.yaml | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml index a615a2c3..95e50c04 100644 --- a/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml +++ b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml @@ -21,8 +21,24 @@ spec: - name: api image: {{ .Values.clusterDev.image | quote }} workingDir: {{ .Values.clusterDev.workingDir | quote }} + {{- if eq .Values.sourceDelivery.mode "sync" }} + # Wait for hops tar/mutagen sync to populate emptyDir before cargo run. + command: + - /bin/bash + - -lc + - | + set -e + for i in $(seq 1 180); do + if [ -f {{ .Values.sourceDelivery.mountPath }}/.hops-synced ] || [ -f {{ .Values.sourceDelivery.mountPath }}/Cargo.toml ]; then + break + fi + sleep 1 + done + {{ index .Values.clusterDev.command 2 }} + {{- else }} command: {{- toYaml .Values.clusterDev.command | nindent 12 }} + {{- end }} ports: - containerPort: {{ .Values.service.targetPort }} name: http diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml index 269618f7..80ef3208 100644 --- a/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml @@ -21,8 +21,24 @@ spec: - name: ui image: {{ .Values.clusterDev.image | quote }} workingDir: {{ .Values.clusterDev.workingDir | quote }} + {{- if eq .Values.sourceDelivery.mode "sync" }} + # Wait for hops tar/mutagen sync to populate emptyDir before starting vite. + command: + - /bin/bash + - -lc + - | + set -e + for i in $(seq 1 180); do + if [ -f {{ .Values.sourceDelivery.mountPath }}/.hops-synced ] || [ -f {{ .Values.sourceDelivery.mountPath }}/package.json ]; then + break + fi + sleep 1 + done + {{ index .Values.clusterDev.command 2 }} + {{- else }} command: {{- toYaml .Values.clusterDev.command | nindent 12 }} + {{- end }} ports: - containerPort: {{ .Values.service.targetPort }} name: http From 127923e1d7a0676733450df4959aa584e476ff26 Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Thu, 6 Aug 2026 16:22:19 -0500 Subject: [PATCH 3/7] fix: per-app deliveryPath in env Applications + infinite sync wait api deliveryPath points at e2e-ui Cargo root; ui at ui/ with vite. cluster-dev waits indefinitely for .hops-synced/package/Cargo before start. --- .../.gitops/deploy/templates/deployment-cluster-dev.yaml | 9 ++++----- tests/e2e-ui/gitops/env/local/api.yaml | 3 +++ tests/e2e-ui/gitops/env/local/ui.yaml | 3 +++ .../.gitops/deploy/templates/deployment-cluster-dev.yaml | 9 ++++----- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml index 95e50c04..8a034474 100644 --- a/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml +++ b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml @@ -27,13 +27,12 @@ spec: - /bin/bash - -lc - | - set -e - for i in $(seq 1 180); do - if [ -f {{ .Values.sourceDelivery.mountPath }}/.hops-synced ] || [ -f {{ .Values.sourceDelivery.mountPath }}/Cargo.toml ]; then - break - fi + echo "waiting for source sync at {{ .Values.sourceDelivery.mountPath }}..." + while [ ! -f {{ .Values.sourceDelivery.mountPath }}/.hops-synced ] \ + && [ ! -f {{ .Values.sourceDelivery.mountPath }}/Cargo.toml ]; do sleep 1 done + echo "source present; starting dev process" {{ index .Values.clusterDev.command 2 }} {{- else }} command: diff --git a/tests/e2e-ui/gitops/env/local/api.yaml b/tests/e2e-ui/gitops/env/local/api.yaml index 3dd626fc..995cfcf4 100644 --- a/tests/e2e-ui/gitops/env/local/api.yaml +++ b/tests/e2e-ui/gitops/env/local/api.yaml @@ -4,7 +4,10 @@ metadata: name: e2e-ui-api spec: source: + # Helm chart path: ../../../api/.gitops/deploy + # Cargo workspace root (Cargo.toml) — NOT the empty api/ chart dir. + deliveryPath: ../../.. helm: values: local: true diff --git a/tests/e2e-ui/gitops/env/local/ui.yaml b/tests/e2e-ui/gitops/env/local/ui.yaml index 29f99bd9..dc10abcd 100644 --- a/tests/e2e-ui/gitops/env/local/ui.yaml +++ b/tests/e2e-ui/gitops/env/local/ui.yaml @@ -4,7 +4,10 @@ metadata: name: e2e-ui-ui spec: source: + # Helm chart path: ../../../ui/.gitops/deploy + # SvelteKit app root (package.json with vite dev) — NOT the monorepo root. + deliveryPath: ../../../ui helm: values: local: true diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml index 80ef3208..b82d4dce 100644 --- a/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml @@ -27,13 +27,12 @@ spec: - /bin/bash - -lc - | - set -e - for i in $(seq 1 180); do - if [ -f {{ .Values.sourceDelivery.mountPath }}/.hops-synced ] || [ -f {{ .Values.sourceDelivery.mountPath }}/package.json ]; then - break - fi + echo "waiting for source sync at {{ .Values.sourceDelivery.mountPath }}..." + while [ ! -f {{ .Values.sourceDelivery.mountPath }}/.hops-synced ] \ + && [ ! -f {{ .Values.sourceDelivery.mountPath }}/package.json ]; do sleep 1 done + echo "source present; starting dev process" {{ index .Values.clusterDev.command 2 }} {{- else }} command: From 32c7db2a4b3ed7455e48f922d1bdc85b078616d9 Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Fri, 7 Aug 2026 21:41:53 -0500 Subject: [PATCH 4/7] feat(e2e-ui): cluster gitops for Auth/PSQL/identity and SecretStack vault Add gitops/cluster tree (packages, providers, AuthStack, PSQLStack, e2e-identity MRs) and SecretStack vault backend claim for local workbench. Wire app charts/env Applications for cluster-dev delivery. Keep secrets out of YAML; document kubectl bootstrap in SECRETS.md. --- tests/e2e-ui/README.md | 23 ++++- .../templates/deployment-cluster-dev.yaml | 29 +++++-- tests/e2e-ui/api/.gitops/deploy/values.yaml | 23 +++-- tests/e2e-ui/gitops/README.md | 26 ++++++ tests/e2e-ui/gitops/cluster/README.md | 72 ++++++++++++++++ tests/e2e-ui/gitops/cluster/SECRETS.md | 84 +++++++++++++++++++ tests/e2e-ui/gitops/cluster/auth/README.md | 54 ++++++++++++ tests/e2e-ui/gitops/cluster/auth/stack.yaml | 49 +++++++++++ .../gitops/cluster/e2e-identity/README.md | 70 ++++++++++++++++ .../cluster/e2e-identity/grants.yaml.example | 65 ++++++++++++++ .../gitops/cluster/e2e-identity/humans.yaml | 77 +++++++++++++++++ .../gitops/cluster/e2e-identity/oidc-app.yaml | 50 +++++++++++ .../gitops/cluster/e2e-identity/project.yaml | 17 ++++ .../gitops/cluster/e2e-identity/roles.yaml | 34 ++++++++ .../gitops/cluster/packages/auth-stack.yaml | 11 +++ .../gitops/cluster/packages/psql-stack.yaml | 12 +++ .../gitops/cluster/packages/secret-stack.yaml | 13 +++ .../cluster/providerconfigs/aws.yaml.example | 14 ++++ .../providerconfigs/github.yaml.example | 13 +++ .../gitops/cluster/providerconfigs/helm.yaml | 8 ++ .../cluster/providerconfigs/kubernetes.yaml | 8 ++ .../cluster/providerconfigs/zitadel.yaml | 12 +++ .../providerconfigs/zitadel.yaml.example | 13 +++ .../gitops/cluster/providers/aws.yaml.example | 11 +++ .../cluster/providers/github.yaml.example | 7 ++ .../e2e-ui/gitops/cluster/providers/helm.yaml | 9 ++ .../gitops/cluster/providers/kubernetes.yaml | 9 ++ .../gitops/cluster/providers/zitadel.yaml | 6 ++ .../cluster/providers/zitadel.yaml.example | 7 ++ tests/e2e-ui/gitops/cluster/psql/README.md | 7 ++ tests/e2e-ui/gitops/cluster/psql/stack.yaml | 25 ++++++ tests/e2e-ui/gitops/cluster/runtime/helm.yaml | 28 +++++++ .../gitops/cluster/runtime/kubernetes.yaml | 28 +++++++ .../e2e-ui/gitops/cluster/secrets/stack.yaml | 38 +++++++++ tests/e2e-ui/gitops/env/README.md | 5 ++ tests/e2e-ui/gitops/env/local/api.yaml | 7 +- tests/e2e-ui/gitops/env/local/ui.yaml | 5 +- tests/e2e-ui/gitops/envs/local/api.yaml | 19 +++++ tests/e2e-ui/gitops/envs/local/ui.yaml | 17 ++++ .../templates/deployment-cluster-dev.yaml | 61 +++++++++++--- tests/e2e-ui/ui/.gitops/deploy/values.yaml | 50 ++++++++++- tests/e2e-ui/ui/src/routes/+page.svelte | 2 +- tests/e2e-ui/ui/vite.config.ts | 20 ++++- 43 files changed, 1100 insertions(+), 38 deletions(-) create mode 100644 tests/e2e-ui/gitops/README.md create mode 100644 tests/e2e-ui/gitops/cluster/README.md create mode 100644 tests/e2e-ui/gitops/cluster/SECRETS.md create mode 100644 tests/e2e-ui/gitops/cluster/auth/README.md create mode 100644 tests/e2e-ui/gitops/cluster/auth/stack.yaml create mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/README.md create mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/grants.yaml.example create mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/humans.yaml create mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/oidc-app.yaml create mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/project.yaml create mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/roles.yaml create mode 100644 tests/e2e-ui/gitops/cluster/packages/auth-stack.yaml create mode 100644 tests/e2e-ui/gitops/cluster/packages/psql-stack.yaml create mode 100644 tests/e2e-ui/gitops/cluster/packages/secret-stack.yaml create mode 100644 tests/e2e-ui/gitops/cluster/providerconfigs/aws.yaml.example create mode 100644 tests/e2e-ui/gitops/cluster/providerconfigs/github.yaml.example create mode 100644 tests/e2e-ui/gitops/cluster/providerconfigs/helm.yaml create mode 100644 tests/e2e-ui/gitops/cluster/providerconfigs/kubernetes.yaml create mode 100644 tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml create mode 100644 tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml.example create mode 100644 tests/e2e-ui/gitops/cluster/providers/aws.yaml.example create mode 100644 tests/e2e-ui/gitops/cluster/providers/github.yaml.example create mode 100644 tests/e2e-ui/gitops/cluster/providers/helm.yaml create mode 100644 tests/e2e-ui/gitops/cluster/providers/kubernetes.yaml create mode 100644 tests/e2e-ui/gitops/cluster/providers/zitadel.yaml create mode 100644 tests/e2e-ui/gitops/cluster/providers/zitadel.yaml.example create mode 100644 tests/e2e-ui/gitops/cluster/psql/README.md create mode 100644 tests/e2e-ui/gitops/cluster/psql/stack.yaml create mode 100644 tests/e2e-ui/gitops/cluster/runtime/helm.yaml create mode 100644 tests/e2e-ui/gitops/cluster/runtime/kubernetes.yaml create mode 100644 tests/e2e-ui/gitops/cluster/secrets/stack.yaml create mode 100644 tests/e2e-ui/gitops/env/README.md create mode 100644 tests/e2e-ui/gitops/envs/local/api.yaml create mode 100644 tests/e2e-ui/gitops/envs/local/ui.yaml diff --git a/tests/e2e-ui/README.md b/tests/e2e-ui/README.md index 67a0df18..74311374 100644 --- a/tests/e2e-ui/README.md +++ b/tests/e2e-ui/README.md @@ -10,14 +10,33 @@ One-time: `hops local start` (control plane). Then: ```bash cd tests/e2e-ui -hops local up ./gitops/env/local --name e2e +# apps (workbench) +hops local up ./gitops/envs/local --name e2e hops local status hops local open # when done hops local down --name e2e ``` -No volume/hostPath literacy required. Charts: `api/.gitops/deploy`, `ui/.gitops/deploy`. Env Applications: `gitops/env/local/`. +Optional **platform XRs** under `gitops/cluster/` (AuthStack + PSQLStack): + +```bash +hops config install /xrs/stacks/k8s/psql +hops config install /xrs/stacks/k8s/auth +kubectl apply -f gitops/cluster/psql/stack.yaml +kubectl apply -f gitops/cluster/auth/stack.yaml +``` + +Optional cloud providers (non-secret YAML only): + +```bash +hops local aws --gitops ./gitops/cluster +hops local github --gitops ./gitops/cluster +``` + +Charts: `api/.gitops/deploy`, `ui/.gitops/deploy`. +App Applications: `gitops/envs/local/` (legacy: `gitops/env/local/`). +Control plane: `gitops/cluster/` (`PSQLStack`, `AuthStack`, packages). ## Option B — compose + host processes diff --git a/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml index 8a034474..c2563960 100644 --- a/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml +++ b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml @@ -22,18 +22,30 @@ spec: image: {{ .Values.clusterDev.image | quote }} workingDir: {{ .Values.clusterDev.workingDir | quote }} {{- if eq .Values.sourceDelivery.mode "sync" }} - # Wait for hops tar/mutagen sync to populate emptyDir before cargo run. command: - /bin/bash - - -lc + - -c - | + set -e echo "waiting for source sync at {{ .Values.sourceDelivery.mountPath }}..." + waited=0 while [ ! -f {{ .Values.sourceDelivery.mountPath }}/.hops-synced ] \ - && [ ! -f {{ .Values.sourceDelivery.mountPath }}/Cargo.toml ]; do - sleep 1 + && [ ! -f {{ .Values.clusterDev.workingDir }}/Cargo.toml ]; do + sleep 2 + waited=$((waited + 2)) + if [ $((waited % 30)) -eq 0 ]; then + echo "still waiting for hops source sync (${waited}s) — is tar watcher running?" + fi + if [ "$waited" -ge 600 ]; then + echo "ERROR: source sync timeout after ${waited}s (no .hops-synced / Cargo.toml)" + exit 1 + fi done echo "source present; starting dev process" - {{ index .Values.clusterDev.command 2 }} + command -v cargo + rustc --version + cd {{ .Values.clusterDev.workingDir }} + cargo run -p e2e-runner --bin e2e-ui {{- else }} command: {{- toYaml .Values.clusterDev.command | nindent 12 }} @@ -44,11 +56,16 @@ spec: env: - name: BIND value: {{ .Values.env.BIND | quote }} + - name: DATABASE_URL + value: {{ .Values.env.DATABASE_URL | default "sqlite:/tmp/e2e-ui.db?mode=rwc" | quote }} + # Keep cargo on PATH even if something mangles the environment. + - name: PATH + value: "/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" volumeMounts: - name: source mountPath: {{ .Values.sourceDelivery.mountPath }} - name: cargo-target - mountPath: {{ .Values.sourceDelivery.mountPath }}/target + mountPath: {{ .Values.clusterDev.workingDir }}/target volumes: - name: source {{- if eq .Values.sourceDelivery.mode "hostPath" }} diff --git a/tests/e2e-ui/api/.gitops/deploy/values.yaml b/tests/e2e-ui/api/.gitops/deploy/values.yaml index 5fddaa5e..9a0f1e8e 100644 --- a/tests/e2e-ui/api/.gitops/deploy/values.yaml +++ b/tests/e2e-ui/api/.gitops/deploy/values.yaml @@ -1,6 +1,5 @@ # Local workbench values for e2e-ui API. # Storage class for PSQLCluster: override per local CP (e.g. local-path, hostpath, psql). -# Default "local-path" works with k3s (dory/colima) and kind with local-path-provisioner. local: false appRuntime: package # host | cluster-dev | package @@ -11,14 +10,18 @@ image: tag: latest pullPolicy: IfNotPresent -# Generic runtime image for cluster-dev (no app package rebuild per edit). +# Source tree is the *distributed* monorepo root; API Cargo workspace is tests/e2e-ui. clusterDev: - image: rust:1.85-bookworm + image: rust:1.94-bookworm + # bash -c keeps Docker ENV (PATH includes /usr/local/cargo/bin). -lc can drop it. command: - /bin/bash - - -lc - - cargo run -p e2e-runner --bin e2e-ui - workingDir: /workspace + - -c + - | + set -e + cd /workspace/tests/e2e-ui + cargo run -p e2e-runner --bin e2e-ui + workingDir: /workspace/tests/e2e-ui sourcePath: /workspace service: @@ -27,17 +30,19 @@ service: env: BIND: "0.0.0.0:8791" + # Offline-friendly default until PSQL platform pack is installed. + DATABASE_URL: "sqlite:/tmp/e2e-ui.db?mode=rwc" database: - enabled: true + # Dogfood local CP often lacks PSQLCluster CRD; disable by default for up path. + # Enable when PSQL pack is installed: --set database.enabled=true via Application values. + enabled: false name: e2e-ui-api-psql clusterName: local storage: size: 2Gi - # Override for your local CP storage class if needed. class: local-path -# Source delivery (injected by hops local when hostPath/sync is selected). sourceDelivery: mode: none # none | hostPath | sync hostPath: "" diff --git a/tests/e2e-ui/gitops/README.md b/tests/e2e-ui/gitops/README.md new file mode 100644 index 00000000..b80301c0 --- /dev/null +++ b/tests/e2e-ui/gitops/README.md @@ -0,0 +1,26 @@ +# e2e-ui local gitops + +```text +gitops/ + cluster/ # fixture CP tree (real meta repos: put this at meta root) + envs/ + local/ # app Applications → worktree namespaces + env/local/ # deprecated alias of envs/local +``` + +```bash +# Bootstrap + cluster gitops apply/watch (stays in foreground = gitops cluster) +hops local start --backend dory --gitops ./gitops/cluster + +# Or cluster watch alone if CP already started +# hops local gitops cluster ./gitops/cluster + +# Per-worktree apps (watches by default) — separate terminal +hops local gitops worktree ./gitops/envs/local --name dogfood + +# One-shot (CI / scripts): add --once +# hops local gitops cluster ./gitops/cluster --once +``` + +On multi-project metas: put `gitops/cluster` at the **meta root**; projects keep +`.gitops/deploy` charts; env Application YAMLs point at those charts. diff --git a/tests/e2e-ui/gitops/cluster/README.md b/tests/e2e-ui/gitops/cluster/README.md new file mode 100644 index 00000000..8cf148ea --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/README.md @@ -0,0 +1,72 @@ +# `gitops/cluster` — local control-plane configuration + +> **Real meta repos:** put this tree at the **meta root** (`gitops/cluster/`), +> not inside a single project. One local CP (dory) serves every worktree; apps +> only get different **namespaces**. This e2e-ui copy is a self-contained +> fixture — same files, nested under the test app. Point hops with +> `hops local gitops cluster ./gitops/cluster` or `HOPS_LOCAL_CLUSTER` when +> applying a meta-root cluster tree. + +Platform / CP resources for the **shared** local control plane: + +```text +/ + gitops/cluster/ # ← this tree (AuthStack, PSQLStack, packages) + gitops/envs/local/ # Application YAMLs → hops-wt-* namespaces + clients/foo/.gitops/deploy/ + platform/api/.gitops/deploy/ +``` + +| Path | Contents | +|------|----------| +| `packages/` | Crossplane `Configuration` installs (psql-stack, auth-stack, secret-stack) | +| `psql/stack.yaml` | **`PSQLStack` XR** — CNPG + SC `psql` (local-path) | +| `auth/stack.yaml` | **`AuthStack` XR** — Zitadel + embedded Postgres | +| `secrets/stack.yaml` | **`SecretStack` XR** — ESO + optional Vault (local backend) | +| `providers/` | Optional cloud Provider installs (`hops local aws --gitops …`) | +| `providerconfigs/` | ProviderConfig shapes (`secretRef` only) | + +## Apply + watch (local gitops vibes) + +`hops local start --gitops PATH` is bootstrap then **`hops local gitops cluster +PATH`** (apply + watch). Start stays in the foreground until Ctrl+C. Day-to-day +without re-bootstrap: `hops local gitops cluster` alone. Use `--once` for a +single reconcile (CI/scripts). + +```bash +# Bootstrap + cluster apply/watch (Ctrl+C to stop) +hops local start --backend dory --gitops ./gitops/cluster + +# Cluster-only if CP already up +# hops local gitops cluster ./gitops/cluster + +# Per-worktree apps (other terminal) +hops local gitops worktree ./gitops/envs/local --name dogfood +``` + +Crossplane reconciles XRs (`PSQLStack`, `AuthStack`, `SecretStack`, …) after each apply. +Edit `auth/stack.yaml`, `psql/stack.yaml`, or `secrets/stack.yaml` → saved → applied → CP converges. + +**SecretStack (local):** install the Configuration from source first +(`hops config install --path …/xrs/stacks/aws/secret`), then apply this cluster tree +so `secrets/stack.yaml` is not stuck Unpacking. + +## vs `gitops/envs/` + +| Tree | Purpose | +|------|---------| +| **cluster/** | CP packages + platform XRs (PSQLStack, AuthStack) | +| **envs/\/xrs/stacks/aws/secret --context dory +hops local gitops cluster ./gitops/cluster +``` + +Do **not** put the SecretStack claim under meta `local/` — that folder is for +ad-hoc colima-style one-off claims, not the workbench cluster gitops path. + +## Rules + +1. **Never** put passwords, PATs, client secrets, or masterkeys in committed YAML. +2. Gitops may name Secrets and keys (`secretRef` / `secretEnv`). +3. Create Secrets before the XRs/Deployments that need them (or accept CrashLoop until present). +4. Cloud: prefer ESO / SOPS; local: `kubectl create` or `hops local zitadel|aws|github`. +5. Platform secrets engine for local CP lives in **`gitops/cluster/secrets/`**, not `local/`. diff --git a/tests/e2e-ui/gitops/cluster/auth/README.md b/tests/e2e-ui/gitops/cluster/auth/README.md new file mode 100644 index 00000000..d1e8c891 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/auth/README.md @@ -0,0 +1,54 @@ +# Auth (local) + +| File | Kind | +|------|------| +| `stack.yaml` | `AuthStack` XR — Zitadel + embedded `PSQLCluster` | + +## Local addressing (no ingress) + +| | Value | +|--|--------| +| Install namespace | **`auth`** (not `zitadel`) | +| AuthStack / release name | **`zitadel`** | +| Service FQDN | **`zitadel.auth.svc.cluster.local:8080`** | +| OIDC issuer | `http://zitadel.auth.svc.cluster.local:8080` | +| Gateway / ingress | disabled — ClusterIP only for now | + +## Apply order + +```bash +hops local start --backend dory --gitops ./gitops/cluster +# or after packages: +hops local gitops cluster ./gitops/cluster + +kubectl get authstack zitadel -n default +kubectl get svc -n auth +# expect: zitadel.auth.svc.cluster.local +``` + +## e2e-ui identity + +See [`../e2e-identity/`](../e2e-identity/) for Project / humans / OIDC app / Grant XRs. +ProviderConfig residual: + +```bash +hops local zitadel --source-context dory --source-namespace auth \ + --domain zitadel.auth.svc.cluster.local --port 8080 --insecure \ + --gitops ./gitops/cluster +``` + +## Secrets + +Masterkey is **not** inline — `secretRef` to `zitadel-masterkey` in ns `auth`: + +```bash +kubectl -n auth create secret generic zitadel-masterkey \ + --from-literal=masterkey='hops-like-basketball-but-for-ops' # exactly 32 bytes +``` + +Create **before** AuthStack becomes Ready (or re-apply after). Full table: +[../SECRETS.md](../SECRETS.md). + +## Residual + +- Host browser access without ingress is later (gateway / map / promote) diff --git a/tests/e2e-ui/gitops/cluster/auth/stack.yaml b/tests/e2e-ui/gitops/cluster/auth/stack.yaml new file mode 100644 index 00000000..4bc52ccc --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/auth/stack.yaml @@ -0,0 +1,49 @@ +# Local AuthStack: Zitadel + embedded PSQLCluster (owned by this XR). +# +# No ingress/gateway for local first step — Service is ClusterIP only. +# Issuer / ExternalDomain = in-cluster DNS (Option A): +# http://zitadel.auth.svc.cluster.local:8080 +# +# Install packages first, then: hops local gitops cluster ./gitops/cluster +apiVersion: hops.ops.com.ai/v1alpha1 +kind: AuthStack +metadata: + # Helm release / Service name → zitadel..svc.cluster.local + name: zitadel + namespace: default +spec: + clusterName: dory + helmProviderConfigRef: + name: default + kubernetesProviderConfigRef: + name: default + + # Install namespace (was default "zitadel"; use "auth" so FQDN is zitadel.auth…) + namespace: auth + + # In-cluster hostname only (no fake public DNS / ingress yet) + domain: zitadel.auth.svc.cluster.local + externalSecure: false + + gateway: + enabled: false + + externalSecrets: + enabled: false + + firstInstance: + org: hops-ops + masterkey: + # Live Secret in install ns (auth) — not inline in gitops. + # kubectl -n auth create secret generic zitadel-masterkey \ + # --from-literal=masterkey='hops-like-basketball-but-for-ops' + # Chart expects the key material under the secret name; see AuthStack XRD. + secretRef: + name: zitadel-masterkey + + # Embedded PG lands in the same install namespace (auth). + database: + embedded: + storage: + size: 2Gi + class: psql diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/README.md b/tests/e2e-ui/gitops/cluster/e2e-identity/README.md new file mode 100644 index 00000000..8903cbdc --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/e2e-identity/README.md @@ -0,0 +1,70 @@ +# e2e-ui identity (Zitadel gitops) + +Declarative stand-in for most of `scripts/up.sh` Management API bootstrap. + +## Layout + +| Path | What | +|------|------| +| `project.yaml` | Zitadel **Project** `e2e-ui` | +| `roles.yaml` | Project roles `user` + `admin` | +| `humans.yaml` | **HumanUser** MRs alice / bob / admin | +| `oidc-app.yaml` | OIDC web app for Auth.js (redirects → worktree UI) | +| `grants.yaml.example` | hops **Grant** XRs — copy to `grants.yaml` and fill UUIDs after humans Ready | +| `../providers/zitadel.yaml` | provider-upjet-zitadel pin | +| `../providerconfigs/zitadel.yaml` | PC shape (credentials residual) | + +## Apply order + +1. **AuthStack Ready** (`../auth/stack.yaml`) — Zitadel in ns **`auth`**, issuer + `http://zitadel.auth.svc.cluster.local:8080` (ClusterIP only; no ingress yet). + +2. **Credentials residual** (once `iam-admin-pat` exists in ns `auth`): + + ```bash + hops local zitadel \ + --source-context dory \ + --source-namespace auth \ + --domain zitadel.auth.svc.cluster.local \ + --port 8080 --insecure \ + --gitops ./gitops/cluster + ``` + +3. **Password residual** (K8s Secret, not values.yaml): + + ```bash + kubectl create secret generic e2e-human-passwords -n default \ + --from-literal=alice=Password1! \ + --from-literal=bob=Password1! \ + --from-literal=admin=Password1! + ``` + + Humans already `secretRef` that object. UI OIDC secrets: `e2e-ui-oidc` in the + worktree namespace (`secretEnv` in `ui/.gitops/deploy/values.yaml`). See + [../SECRETS.md](../SECRETS.md). + +4. `hops local gitops cluster ./gitops/cluster` — project, roles, humans, oidc + +5. Observe IDs, copy `grants.yaml.example` → `grants.yaml`, fill UUIDs, re-apply + +## XRs vs provider MRs + +| Resource | Kind | +|----------|------| +| Platform install | **AuthStack** XR | +| Role grants | **Grant** XR (`auth.hops.ops.com.ai`) | +| Project / Role / Human / OIDC app | provider MRs (no thin hops wrapper) | +| Machine identities (optional) | **MachineUser** XR | + +## Residuals + +- `zitadel-credentials` Secret (PAT JSON for ProviderConfig) +- `e2e-human-passwords` Secret +- OIDC **client secret** (only at app create — copy into UI env / Secret) +- Grant UUID fill-in until composition supports refs from HumanUser + +## App wiring + +UI chart values under `ui/.gitops/deploy` point `OIDC_ISSUER` at AuthStack issuer +(`http://auth.localtest.me` browser / in-cluster service for pods as needed). +Client id/secret still residual until connection details are automated. diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/grants.yaml.example b/tests/e2e-ui/gitops/cluster/e2e-identity/grants.yaml.example new file mode 100644 index 00000000..c6472970 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/e2e-identity/grants.yaml.example @@ -0,0 +1,65 @@ +# hops Grant XRs — simpler API than raw user/project grant MRs. +# After humans + project are Ready, copy UUIDs from: +# kubectl get humanuser e2e-alice -o jsonpath='{.status.atProvider.userId}' +# kubectl get project e2e-ui -o jsonpath='{.status.atProvider.id}' +# kubectl get humanuser e2e-alice -o jsonpath='{.status.atProvider.orgId}' +# +# PLACEHOLDER values below must be replaced before grants reconcile. +# (Future: Grant XR with userRef/projectRef — not in auth-stack yet.) +apiVersion: auth.hops.ops.com.ai/v1alpha1 +kind: Grant +metadata: + name: e2e-alice-user + namespace: default + labels: + hops.ops.com.ai/app: e2e-ui + hops.ops.com.ai/demo-user: alice +spec: + userId: "REPLACE_WITH_ALICE_USER_ID" + userOrgId: "REPLACE_WITH_ORG_ID" + projectId: "REPLACE_WITH_PROJECT_ID" + projectOrgId: "REPLACE_WITH_ORG_ID" + roles: + - user + providerConfigRef: + name: default + kind: ProviderConfig +--- +apiVersion: auth.hops.ops.com.ai/v1alpha1 +kind: Grant +metadata: + name: e2e-bob-user + namespace: default + labels: + hops.ops.com.ai/app: e2e-ui + hops.ops.com.ai/demo-user: bob +spec: + userId: "REPLACE_WITH_BOB_USER_ID" + userOrgId: "REPLACE_WITH_ORG_ID" + projectId: "REPLACE_WITH_PROJECT_ID" + projectOrgId: "REPLACE_WITH_ORG_ID" + roles: + - user + providerConfigRef: + name: default + kind: ProviderConfig +--- +apiVersion: auth.hops.ops.com.ai/v1alpha1 +kind: Grant +metadata: + name: e2e-admin-roles + namespace: default + labels: + hops.ops.com.ai/app: e2e-ui + hops.ops.com.ai/demo-user: admin +spec: + userId: "REPLACE_WITH_ADMIN_USER_ID" + userOrgId: "REPLACE_WITH_ORG_ID" + projectId: "REPLACE_WITH_PROJECT_ID" + projectOrgId: "REPLACE_WITH_ORG_ID" + roles: + - admin + - user + providerConfigRef: + name: default + kind: ProviderConfig diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/humans.yaml b/tests/e2e-ui/gitops/cluster/e2e-identity/humans.yaml new file mode 100644 index 00000000..0fb92f26 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/e2e-identity/humans.yaml @@ -0,0 +1,77 @@ +# Demo humans — scripts/up.sh alice / bob / admin. +# Passwords: Secret e2e-human-passwords (residual — not in git). +# kubectl create secret generic e2e-human-passwords -n default \ +# --from-literal=alice=Password1! \ +# --from-literal=bob=Password1! \ +# --from-literal=admin=Password1! +apiVersion: user.zitadel.m.crossplane.io/v1alpha1 +kind: HumanUser +metadata: + name: e2e-alice + namespace: default + labels: + hops.ops.com.ai/app: e2e-ui + hops.ops.com.ai/demo-user: alice +spec: + forProvider: + userName: alice + firstName: alice + lastName: E2E + displayName: alice + email: alice@e2e.local + isEmailVerified: true + initialSkipPasswordChange: true + initialPasswordSecretRef: + name: e2e-human-passwords + key: alice + providerConfigRef: + name: default + kind: ProviderConfig +--- +apiVersion: user.zitadel.m.crossplane.io/v1alpha1 +kind: HumanUser +metadata: + name: e2e-bob + namespace: default + labels: + hops.ops.com.ai/app: e2e-ui + hops.ops.com.ai/demo-user: bob +spec: + forProvider: + userName: bob + firstName: bob + lastName: E2E + displayName: bob + email: bob@e2e.local + isEmailVerified: true + initialSkipPasswordChange: true + initialPasswordSecretRef: + name: e2e-human-passwords + key: bob + providerConfigRef: + name: default + kind: ProviderConfig +--- +apiVersion: user.zitadel.m.crossplane.io/v1alpha1 +kind: HumanUser +metadata: + name: e2e-admin + namespace: default + labels: + hops.ops.com.ai/app: e2e-ui + hops.ops.com.ai/demo-user: admin +spec: + forProvider: + userName: admin + firstName: admin + lastName: E2E + displayName: admin + email: admin@e2e.local + isEmailVerified: true + initialSkipPasswordChange: true + initialPasswordSecretRef: + name: e2e-human-passwords + key: admin + providerConfigRef: + name: default + kind: ProviderConfig diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/oidc-app.yaml b/tests/e2e-ui/gitops/cluster/e2e-identity/oidc-app.yaml new file mode 100644 index 00000000..df9b9a37 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/e2e-identity/oidc-app.yaml @@ -0,0 +1,50 @@ +# OIDC web app for e2e-ui Auth.js — scripts/up.sh e2e-ui-web. +# Redirects include workbench Service DNS + localhost for host access. +# Client secret is residual (observe status / Zitadel console on create). +apiVersion: application.zitadel.m.crossplane.io/v1alpha1 +kind: Oidc +metadata: + name: e2e-ui-web + namespace: default + labels: + hops.ops.com.ai/app: e2e-ui +spec: + forProvider: + name: e2e-ui-web + projectIdRef: + name: e2e-ui + appType: OIDC_APP_TYPE_WEB + authMethodType: OIDC_AUTH_METHOD_TYPE_BASIC + responseTypes: + - OIDC_RESPONSE_TYPE_CODE + grantTypes: + - OIDC_GRANT_TYPE_AUTHORIZATION_CODE + - OIDC_GRANT_TYPE_REFRESH_TOKEN + accessTokenType: OIDC_TOKEN_TYPE_JWT + accessTokenRoleAssertion: true + idTokenRoleAssertion: true + idTokenUserinfoAssertion: true + devMode: true + version: OIDC_VERSION_1_0 + redirectUris: + # Workbench dogfood (cluster DNS / map) + - http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180/auth/callback/oidc + - http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180/auth/callback + # Host map / local vite + - http://127.0.0.1:5180/auth/callback/oidc + - http://localhost:5180/auth/callback/oidc + - http://127.0.0.1:18101/auth/callback/oidc + - http://localhost:18101/auth/callback/oidc + postLogoutRedirectUris: + - http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180/ + - http://127.0.0.1:5180/ + - http://localhost:5180/ + - http://127.0.0.1:18101/ + - http://localhost:18101/ + # Login V2 base = custom app login pages (Fieldnote UI), not stock zitadel-login. + loginVersion: + - loginV2: + - baseUri: http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180 + providerConfigRef: + name: default + kind: ProviderConfig diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/project.yaml b/tests/e2e-ui/gitops/cluster/e2e-identity/project.yaml new file mode 100644 index 00000000..233ae60e --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/e2e-identity/project.yaml @@ -0,0 +1,17 @@ +# e2e-ui Zitadel project — replaces scripts/up.sh "Project e2e-ui" +apiVersion: project.zitadel.m.crossplane.io/v1alpha1 +kind: Project +metadata: + name: e2e-ui + namespace: default + labels: + hops.ops.com.ai/app: e2e-ui +spec: + forProvider: + name: e2e-ui + projectRoleAssertion: true + projectRoleCheck: true + hasProjectCheck: true + providerConfigRef: + name: default + kind: ProviderConfig diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/roles.yaml b/tests/e2e-ui/gitops/cluster/e2e-identity/roles.yaml new file mode 100644 index 00000000..94b73505 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/e2e-identity/roles.yaml @@ -0,0 +1,34 @@ +# Project roles — scripts/up.sh roles user + admin +apiVersion: project.zitadel.m.crossplane.io/v1alpha1 +kind: Role +metadata: + name: e2e-ui-role-user + namespace: default + labels: + hops.ops.com.ai/app: e2e-ui +spec: + forProvider: + roleKey: user + displayName: user + projectIdRef: + name: e2e-ui + providerConfigRef: + name: default + kind: ProviderConfig +--- +apiVersion: project.zitadel.m.crossplane.io/v1alpha1 +kind: Role +metadata: + name: e2e-ui-role-admin + namespace: default + labels: + hops.ops.com.ai/app: e2e-ui +spec: + forProvider: + roleKey: admin + displayName: admin + projectIdRef: + name: e2e-ui + providerConfigRef: + name: default + kind: ProviderConfig diff --git a/tests/e2e-ui/gitops/cluster/packages/auth-stack.yaml b/tests/e2e-ui/gitops/cluster/packages/auth-stack.yaml new file mode 100644 index 00000000..76c5c0af --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/packages/auth-stack.yaml @@ -0,0 +1,11 @@ +# Crossplane Configuration for AuthStack (Zitadel + auth XRDs). +# Prefer building/pushing from source into the local registry: +# hops config install /xrs/stacks/k8s/auth +# Depends on: provider-helm, provider-kubernetes, (optional) provider-upjet-zitadel. +apiVersion: pkg.crossplane.io/v1 +kind: Configuration +metadata: + name: auth-stack +spec: + package: ghcr.io/hops-ops/auth-stack:v1.6.0 + # package: registry.crossplane-system.svc.cluster.local:5000/hops-ops/auth-stack: diff --git a/tests/e2e-ui/gitops/cluster/packages/psql-stack.yaml b/tests/e2e-ui/gitops/cluster/packages/psql-stack.yaml new file mode 100644 index 00000000..2427526f --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/packages/psql-stack.yaml @@ -0,0 +1,12 @@ +# Crossplane Configuration for the PostgreSQL platform stack. +# Prefer building/pushing from source into the local registry: +# hops config install /xrs/stacks/k8s/psql +# Or set package to a published tag when available. +apiVersion: pkg.crossplane.io/v1 +kind: Configuration +metadata: + name: psql-stack +spec: + package: ghcr.io/hops-ops/psql-stack:v0.9.1 + # Local registry after hops local start + config install: + # package: registry.crossplane-system.svc.cluster.local:5000/hops-ops/psql-stack: diff --git a/tests/e2e-ui/gitops/cluster/packages/secret-stack.yaml b/tests/e2e-ui/gitops/cluster/packages/secret-stack.yaml new file mode 100644 index 00000000..5338668f --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/packages/secret-stack.yaml @@ -0,0 +1,13 @@ +# Crossplane Configuration for platform SecretStack (ESO + AWS|Vault backends). +# Prefer source install for local dogfood of the multi-backend redesign: +# hops config install --path /xrs/stacks/aws/secret --context dory +# Then this YAML is optional (or pin a published tag once released). +apiVersion: pkg.crossplane.io/v1 +kind: Configuration +metadata: + name: secret-stack +spec: + # After `hops config install --path …/aws/secret`, package is local registry: + # package: registry.crossplane-system.svc.cluster.local:5000/hops-ops/secret-stack:dev- + package: ghcr.io/hops-ops/secret-stack:latest + # packagePullPolicy: Always diff --git a/tests/e2e-ui/gitops/cluster/providerconfigs/aws.yaml.example b/tests/e2e-ui/gitops/cluster/providerconfigs/aws.yaml.example new file mode 100644 index 00000000..e5c6132b --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providerconfigs/aws.yaml.example @@ -0,0 +1,14 @@ +# Non-secret ProviderConfig shape. Secret `aws-creds` is live-only until local ESO exists. +# hops local aws --gitops ./gitops/cluster +apiVersion: aws.m.upbound.io/v1beta1 +kind: ProviderConfig +metadata: + name: default + namespace: default +spec: + credentials: + source: Secret + secretRef: + namespace: default + name: aws-creds + key: credentials diff --git a/tests/e2e-ui/gitops/cluster/providerconfigs/github.yaml.example b/tests/e2e-ui/gitops/cluster/providerconfigs/github.yaml.example new file mode 100644 index 00000000..08b9051c --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providerconfigs/github.yaml.example @@ -0,0 +1,13 @@ +# Secret `github-creds` is live-only. +apiVersion: github.m.upbound.io/v1beta1 +kind: ProviderConfig +metadata: + name: default + namespace: default +spec: + credentials: + source: Secret + secretRef: + namespace: default + name: github-creds + key: credentials diff --git a/tests/e2e-ui/gitops/cluster/providerconfigs/helm.yaml b/tests/e2e-ui/gitops/cluster/providerconfigs/helm.yaml new file mode 100644 index 00000000..148cb4fc --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providerconfigs/helm.yaml @@ -0,0 +1,8 @@ +apiVersion: helm.m.crossplane.io/v1beta1 +kind: ProviderConfig +metadata: + name: default + namespace: default +spec: + credentials: + source: InjectedIdentity diff --git a/tests/e2e-ui/gitops/cluster/providerconfigs/kubernetes.yaml b/tests/e2e-ui/gitops/cluster/providerconfigs/kubernetes.yaml new file mode 100644 index 00000000..95029bcc --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providerconfigs/kubernetes.yaml @@ -0,0 +1,8 @@ +apiVersion: kubernetes.m.crossplane.io/v1alpha1 +kind: ProviderConfig +metadata: + name: default + namespace: default +spec: + credentials: + source: InjectedIdentity diff --git a/tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml b/tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml new file mode 100644 index 00000000..34578c1f --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml @@ -0,0 +1,12 @@ +apiVersion: zitadel.m.crossplane.io/v1beta1 +kind: ProviderConfig +metadata: + name: default + namespace: default +spec: + credentials: + source: Secret + secretRef: + namespace: default + name: zitadel-credentials + key: credentials diff --git a/tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml.example b/tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml.example new file mode 100644 index 00000000..9dbd9d2d --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml.example @@ -0,0 +1,13 @@ +# Secret `zitadel-credentials` is live-only (PAT from AuthStack / env). +apiVersion: zitadel.m.crossplane.io/v1beta1 +kind: ProviderConfig +metadata: + name: default + namespace: default +spec: + credentials: + source: Secret + secretRef: + namespace: default + name: zitadel-credentials + key: credentials diff --git a/tests/e2e-ui/gitops/cluster/providers/aws.yaml.example b/tests/e2e-ui/gitops/cluster/providers/aws.yaml.example new file mode 100644 index 00000000..ac4a0347 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providers/aws.yaml.example @@ -0,0 +1,11 @@ +# Example only — generate with: +# hops local aws --gitops ./gitops/cluster +# Then commit providers/aws.yaml (not secrets). +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: crossplane-contrib-provider-family-aws +spec: + package: xpkg.crossplane.io/crossplane-contrib/provider-family-aws:v2.4.0 + runtimeConfigRef: + name: aws diff --git a/tests/e2e-ui/gitops/cluster/providers/github.yaml.example b/tests/e2e-ui/gitops/cluster/providers/github.yaml.example new file mode 100644 index 00000000..f7bbf4f8 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providers/github.yaml.example @@ -0,0 +1,7 @@ +# hops local github --gitops ./gitops/cluster +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: crossplane-contrib-provider-upjet-github +spec: + package: xpkg.crossplane.io/crossplane-contrib/provider-upjet-github:v0.19.0 diff --git a/tests/e2e-ui/gitops/cluster/providers/helm.yaml b/tests/e2e-ui/gitops/cluster/providers/helm.yaml new file mode 100644 index 00000000..44d33b64 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providers/helm.yaml @@ -0,0 +1,9 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: crossplane-contrib-provider-helm +spec: + # Renovate: github-releases crossplane-contrib/provider-helm (cli/renovate.json) + package: xpkg.crossplane.io/crossplane-contrib/provider-helm:v1.3.0 + runtimeConfigRef: + name: local-dev-helm diff --git a/tests/e2e-ui/gitops/cluster/providers/kubernetes.yaml b/tests/e2e-ui/gitops/cluster/providers/kubernetes.yaml new file mode 100644 index 00000000..c400c387 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providers/kubernetes.yaml @@ -0,0 +1,9 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: crossplane-contrib-provider-kubernetes +spec: + # Renovate: github-releases crossplane-contrib/provider-kubernetes (cli/renovate.json) + package: xpkg.crossplane.io/crossplane-contrib/provider-kubernetes:v1.2.1 + runtimeConfigRef: + name: local-dev-kubernetes diff --git a/tests/e2e-ui/gitops/cluster/providers/zitadel.yaml b/tests/e2e-ui/gitops/cluster/providers/zitadel.yaml new file mode 100644 index 00000000..41d45afd --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providers/zitadel.yaml @@ -0,0 +1,6 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: crossplane-contrib-provider-upjet-zitadel +spec: + package: xpkg.crossplane.io/crossplane-contrib/provider-upjet-zitadel:v0.1.1 diff --git a/tests/e2e-ui/gitops/cluster/providers/zitadel.yaml.example b/tests/e2e-ui/gitops/cluster/providers/zitadel.yaml.example new file mode 100644 index 00000000..24a63c54 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/providers/zitadel.yaml.example @@ -0,0 +1,7 @@ +# hops local zitadel --source-context --gitops ./gitops/cluster +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: crossplane-contrib-provider-upjet-zitadel +spec: + package: xpkg.crossplane.io/crossplane-contrib/provider-upjet-zitadel:v0.1.1 diff --git a/tests/e2e-ui/gitops/cluster/psql/README.md b/tests/e2e-ui/gitops/cluster/psql/README.md new file mode 100644 index 00000000..00e41563 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/psql/README.md @@ -0,0 +1,7 @@ +# PSQL platform (local) + +| File | Kind | +|------|------| +| `stack.yaml` | `PSQLStack` XR — CNPG + StorageClass `psql` on `local-path` | + +AuthStack embedded DB and future e2e-ui `PSQLCluster` claims use `spec.storage.class: psql`. diff --git a/tests/e2e-ui/gitops/cluster/psql/stack.yaml b/tests/e2e-ui/gitops/cluster/psql/stack.yaml new file mode 100644 index 00000000..c4f32908 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/psql/stack.yaml @@ -0,0 +1,25 @@ +# Platform layer: CNPG + Atlas + StorageClass named `psql`. +# Install package first: `hops config install ` (or apply packages/psql-stack.yaml). +# +# Dory/k3s uses rancher.io/local-path (default SC). Snapshot class off (no CSI snapshotter). +apiVersion: hops.ops.com.ai/v1alpha1 +kind: PSQLStack +metadata: + name: psql + namespace: default +spec: + clusterName: dory + helmProviderConfigRef: + name: default + kubernetesProviderConfigRef: + name: default + storageClass: + enabled: true + name: psql + provisioner: rancher.io/local-path + allowVolumeExpansion: false + parameters: {} + snapshotClass: + enabled: false + scaleToZeroPlugin: + enabled: false diff --git a/tests/e2e-ui/gitops/cluster/runtime/helm.yaml b/tests/e2e-ui/gitops/cluster/runtime/helm.yaml new file mode 100644 index 00000000..0c5c6b0a --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/runtime/helm.yaml @@ -0,0 +1,28 @@ +# Per-provider DeploymentRuntimeConfig for provider-helm (local dev). +# +# Each provider gets its OWN uniquely-named DRC + cluster-admin ServiceAccount. +# Providers must NOT share a DRC: a shared runtime config lets one provider's +# image/SA silently clobber the other's pod (and makes drift impossible to +# attribute). This mirrors the per-provider DRCs the +# crossplane-helm-provider-stack composes for remote clusters. +apiVersion: pkg.crossplane.io/v1beta1 +kind: DeploymentRuntimeConfig +metadata: + name: local-dev-helm +spec: + serviceAccountTemplate: + metadata: + name: local-dev-helm +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: local-dev-helm-cluster-admin +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: local-dev-helm + namespace: crossplane-system diff --git a/tests/e2e-ui/gitops/cluster/runtime/kubernetes.yaml b/tests/e2e-ui/gitops/cluster/runtime/kubernetes.yaml new file mode 100644 index 00000000..7ce229dd --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/runtime/kubernetes.yaml @@ -0,0 +1,28 @@ +# Per-provider DeploymentRuntimeConfig for provider-kubernetes (local dev). +# +# Each provider gets its OWN uniquely-named DRC + cluster-admin ServiceAccount. +# Providers must NOT share a DRC: a shared runtime config lets one provider's +# image/SA silently clobber the other's pod (and makes drift impossible to +# attribute). This mirrors the per-provider DRCs the +# crossplane-kubernetes-provider-stack composes for remote clusters. +apiVersion: pkg.crossplane.io/v1beta1 +kind: DeploymentRuntimeConfig +metadata: + name: local-dev-kubernetes +spec: + serviceAccountTemplate: + metadata: + name: local-dev-kubernetes +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: local-dev-kubernetes-cluster-admin +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: local-dev-kubernetes + namespace: crossplane-system diff --git a/tests/e2e-ui/gitops/cluster/secrets/stack.yaml b/tests/e2e-ui/gitops/cluster/secrets/stack.yaml new file mode 100644 index 00000000..37881266 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/secrets/stack.yaml @@ -0,0 +1,38 @@ +# Platform SecretStack for local CP (dory workbench). +# +# Install Configuration from source first (breaking API: hops.ops.com.ai): +# hops config install --path /xrs/stacks/aws/secret --context dory +# +# Then: hops local gitops cluster ./gitops/cluster +# +# backend=vault + vault.install for local (no AWS PodIdentity required). +# clusterName/provider defaults match auth/psql stacks (helm PC name: default). +apiVersion: hops.ops.com.ai/v1alpha1 +kind: SecretStack +metadata: + name: external-secrets + namespace: default +spec: + clusterName: dory + backend: vault + namespace: external-secrets + helmProviderConfigRef: + name: default + kubernetesProviderConfigRef: + name: default + labels: + team: platform + hops.ops.com.ai/fixture: e2e-ui + secretStore: + enabled: true + scope: Cluster + name: vault + vault: + install: true + namespace: vault + path: secret + version: v2 + auth: + method: kubernetes + mountPath: kubernetes + role: external-secrets diff --git a/tests/e2e-ui/gitops/env/README.md b/tests/e2e-ui/gitops/env/README.md new file mode 100644 index 00000000..a9623e75 --- /dev/null +++ b/tests/e2e-ui/gitops/env/README.md @@ -0,0 +1,5 @@ +# Deprecated path + +Use **`gitops/envs/local`** instead of `gitops/env/local`. + +This directory remains so existing `hops local up ./gitops/env/local` commands keep working. diff --git a/tests/e2e-ui/gitops/env/local/api.yaml b/tests/e2e-ui/gitops/env/local/api.yaml index 995cfcf4..6dc3b124 100644 --- a/tests/e2e-ui/gitops/env/local/api.yaml +++ b/tests/e2e-ui/gitops/env/local/api.yaml @@ -4,14 +4,15 @@ metadata: name: e2e-ui-api spec: source: - # Helm chart path: ../../../api/.gitops/deploy - # Cargo workspace root (Cargo.toml) — NOT the empty api/ chart dir. - deliveryPath: ../../.. + # distributed monorepo root (Cargo workspace lives at tests/e2e-ui) + deliveryPath: ../../../../.. helm: values: local: true appRuntime: cluster-dev + database: + enabled: false destination: namespace: hops-wt-default syncPolicy: diff --git a/tests/e2e-ui/gitops/env/local/ui.yaml b/tests/e2e-ui/gitops/env/local/ui.yaml index dc10abcd..2b29b060 100644 --- a/tests/e2e-ui/gitops/env/local/ui.yaml +++ b/tests/e2e-ui/gitops/env/local/ui.yaml @@ -4,10 +4,9 @@ metadata: name: e2e-ui-ui spec: source: - # Helm chart path: ../../../ui/.gitops/deploy - # SvelteKit app root (package.json with vite dev) — NOT the monorepo root. - deliveryPath: ../../../ui + # distributed monorepo root (ui needs tests/e2e-ui/ui + js/ for file: dep) + deliveryPath: ../../../../.. helm: values: local: true diff --git a/tests/e2e-ui/gitops/envs/local/api.yaml b/tests/e2e-ui/gitops/envs/local/api.yaml new file mode 100644 index 00000000..6dc3b124 --- /dev/null +++ b/tests/e2e-ui/gitops/envs/local/api.yaml @@ -0,0 +1,19 @@ +apiVersion: hops.local/v1alpha1 +kind: Application +metadata: + name: e2e-ui-api +spec: + source: + path: ../../../api/.gitops/deploy + # distributed monorepo root (Cargo workspace lives at tests/e2e-ui) + deliveryPath: ../../../../.. + helm: + values: + local: true + appRuntime: cluster-dev + database: + enabled: false + destination: + namespace: hops-wt-default + syncPolicy: + prune: false diff --git a/tests/e2e-ui/gitops/envs/local/ui.yaml b/tests/e2e-ui/gitops/envs/local/ui.yaml new file mode 100644 index 00000000..2b29b060 --- /dev/null +++ b/tests/e2e-ui/gitops/envs/local/ui.yaml @@ -0,0 +1,17 @@ +apiVersion: hops.local/v1alpha1 +kind: Application +metadata: + name: e2e-ui-ui +spec: + source: + path: ../../../ui/.gitops/deploy + # distributed monorepo root (ui needs tests/e2e-ui/ui + js/ for file: dep) + deliveryPath: ../../../../.. + helm: + values: + local: true + appRuntime: cluster-dev + destination: + namespace: hops-wt-default + syncPolicy: + prune: false diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml index b82d4dce..a9a0ed7b 100644 --- a/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml @@ -22,18 +22,39 @@ spec: image: {{ .Values.clusterDev.image | quote }} workingDir: {{ .Values.clusterDev.workingDir | quote }} {{- if eq .Values.sourceDelivery.mode "sync" }} - # Wait for hops tar/mutagen sync to populate emptyDir before starting vite. + # Wait for tar/mutagen delivery, then run the same clusterDev command. command: - /bin/bash - - -lc + - -c - | + set -e echo "waiting for source sync at {{ .Values.sourceDelivery.mountPath }}..." + waited=0 while [ ! -f {{ .Values.sourceDelivery.mountPath }}/.hops-synced ] \ - && [ ! -f {{ .Values.sourceDelivery.mountPath }}/package.json ]; do - sleep 1 + && [ ! -f {{ .Values.clusterDev.workingDir }}/package.json ]; do + sleep 2 + waited=$((waited + 2)) + if [ $((waited % 30)) -eq 0 ]; then + echo "still waiting for hops source sync (${waited}s) — is tar watcher running?" + fi + if [ "$waited" -ge 600 ]; then + echo "ERROR: source sync timeout after ${waited}s (no .hops-synced / package.json)" + exit 1 + fi done echo "source present; starting dev process" - {{ index .Values.clusterDev.command 2 }} + {{- /* Unwrap Values.clusterDev.command ["/bin/bash","-c","script"] */ -}} + {{- $cmd := .Values.clusterDev.command -}} + {{- if and (kindIs "slice" $cmd) (ge (len $cmd) 3) }} + {{ index $cmd 2 | nindent 14 }} + {{- else }} + cd /workspace/js && npm install && npm run build + cd {{ .Values.clusterDev.workingDir }} + npm install + export DISTRIBUTED_SKIP_CLIENT_COMPILE=1 + export E2E_API_ORIGIN="${E2E_API_ORIGIN:-http://e2e-ui-api:8791}" + npm run dev -- --host 0.0.0.0 --port {{ .Values.service.targetPort }} + {{- end }} {{- else }} command: {{- toYaml .Values.clusterDev.command | nindent 12 }} @@ -42,15 +63,33 @@ spec: - containerPort: {{ .Values.service.targetPort }} name: http env: - - name: HOST - value: {{ .Values.env.HOST | quote }} - - name: PORT - value: {{ .Values.env.PORT | quote }} + {{- range $k, $v := .Values.env }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- /* Secrets from K8s Secret objects — never plain values in gitops */ -}} + {{- range $k, $ref := .Values.secretEnv }} + - name: {{ $k }} + valueFrom: + secretKeyRef: + name: {{ $ref.secretName | quote }} + key: {{ $ref.key | quote }} + {{- end }} + {{- with .Values.envFromSecrets }} + envFrom: + {{- range . }} + - secretRef: + name: {{ . | quote }} + {{- end }} + {{- end }} volumeMounts: - name: source mountPath: {{ .Values.sourceDelivery.mountPath }} - name: node-modules - mountPath: {{ .Values.sourceDelivery.mountPath }}/node_modules + mountPath: {{ .Values.clusterDev.workingDir }}/node_modules + # Keep monorepo js deps across restarts within the same pod lifecycle. + - name: js-node-modules + mountPath: /workspace/js/node_modules volumes: - name: source {{- if eq .Values.sourceDelivery.mode "hostPath" }} @@ -62,4 +101,6 @@ spec: {{- end }} - name: node-modules emptyDir: {} + - name: js-node-modules + emptyDir: {} {{- end }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/values.yaml b/tests/e2e-ui/ui/.gitops/deploy/values.yaml index d6b9bdec..bfcce65d 100644 --- a/tests/e2e-ui/ui/.gitops/deploy/values.yaml +++ b/tests/e2e-ui/ui/.gitops/deploy/values.yaml @@ -1,4 +1,5 @@ # Local workbench values for e2e-ui UI. +# Secrets live in K8s Secrets (secretEnv / envFromSecrets) — not here. local: false appRuntime: package # host | cluster-dev | package @@ -9,22 +10,65 @@ image: tag: latest pullPolicy: IfNotPresent +# Source tree is the *distributed* monorepo root (needs tests/e2e-ui/ui + js/). +# Application deliveryPath points there; workingDir is the Svelte app. clusterDev: image: node:22-bookworm + # Use bash -c (not -lc) so container ENV/PATH stay intact. command: - /bin/bash - - -lc - - npm install && npm run dev -- --host 0.0.0.0 --port 5180 - workingDir: /workspace + - -c + - | + set -e + cd /workspace/js && npm install && npm run build + cd /workspace/tests/e2e-ui/ui + npm install + # Skip dctl/cargo: generated clients are committed under src/lib/generated. + export DISTRIBUTED_SKIP_CLIENT_COMPILE=1 + # In-cluster API service (not host localhost). + export E2E_API_ORIGIN="${E2E_API_ORIGIN:-http://e2e-ui-api:8791}" + export E2E_BASE_URL="$E2E_API_ORIGIN" + npm run dev -- --host 0.0.0.0 --port 5180 + workingDir: /workspace/tests/e2e-ui/ui sourcePath: /workspace service: port: 5180 targetPort: 5180 +# Non-secret env only (safe in git). env: HOST: "0.0.0.0" PORT: "5180" + DISTRIBUTED_SKIP_CLIENT_COMPILE: "1" + E2E_API_ORIGIN: "http://e2e-ui-api:8791" + E2E_BASE_URL: "http://e2e-ui-api:8791" + PUBLIC_E2E_API_ORIGIN: "http://e2e-ui-api:8791" + AUTH_URL: "http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180" + AUTH_TRUST_HOST: "true" + AUTH_USE_SECURE_COOKIES: "false" + # AuthStack issuer (in-cluster ClusterIP Service — no ingress) + OIDC_ISSUER: "http://zitadel.auth.svc.cluster.local:8080" + +# Secret-backed env (keys must exist on Secret e2e-ui-oidc in the app namespace). +# Residual — create live (not in git): +# kubectl -n hops-wt-dogfood create secret generic e2e-ui-oidc \ +# --from-literal=AUTH_SECRET='…' \ +# --from-literal=OIDC_CLIENT_ID='…' \ +# --from-literal=OIDC_CLIENT_SECRET='…' +secretEnv: + AUTH_SECRET: + secretName: e2e-ui-oidc + key: AUTH_SECRET + OIDC_CLIENT_ID: + secretName: e2e-ui-oidc + key: OIDC_CLIENT_ID + OIDC_CLIENT_SECRET: + secretName: e2e-ui-oidc + key: OIDC_CLIENT_SECRET + +# Optional: mount entire secrets as env (unused by default) +envFromSecrets: [] sourceDelivery: mode: none # none | hostPath | sync diff --git a/tests/e2e-ui/ui/src/routes/+page.svelte b/tests/e2e-ui/ui/src/routes/+page.svelte index a07875f1..42139b1d 100644 --- a/tests/e2e-ui/ui/src/routes/+page.svelte +++ b/tests/e2e-ui/ui/src/routes/+page.svelte @@ -198,7 +198,7 @@ query Todos @load {

Distributed is a state-of-the-art framework - for building distributed systems and realtime applications. + for building distributed systems, and realtime applications.

Not a partial toolkit. An end-to-end stack — domain, service, query edge, and live client — diff --git a/tests/e2e-ui/ui/vite.config.ts b/tests/e2e-ui/ui/vite.config.ts index c71fdbab..bbdcaa81 100644 --- a/tests/e2e-ui/ui/vite.config.ts +++ b/tests/e2e-ui/ui/vite.config.ts @@ -1,3 +1,6 @@ +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + import { sveltekit } from '@sveltejs/kit/vite'; import { distributedGraphqlProxy, @@ -9,13 +12,28 @@ import { distributedViteOptions } from './distributed.config.js'; const api = process.env.E2E_API_ORIGIN || process.env.E2E_BASE_URL || 'http://127.0.0.1:8791'; +// Cluster-dev node images often lack cargo/dctl. Prefer committed generated +// clients when present so vite can start without a Rust toolchain. +const generatedReady = distributedViteOptions.clients.every((client) => + existsSync(resolve(distributedViteOptions.cwd, client.out, 'sveltekit.ts')) +); +const skipClientCompile = + process.env.DISTRIBUTED_SKIP_CLIENT_COMPILE === '1' || + (process.env.DISTRIBUTED_SKIP_CLIENT_COMPILE !== '0' && generatedReady); + export default defineConfig({ - plugins: [distributedSvelteKit(distributedViteOptions), sveltekit()], + plugins: [ + ...(skipClientCompile ? [] : [distributedSvelteKit(distributedViteOptions)]), + sveltekit() + ], css: { devSourcemap: true }, // blob-domain pure package (wasm-pack --features wasm) assetsInclude: ['**/*.wasm'], server: { port: 5180, + // hops local cluster-DNS mode uses svc.ns.svc.cluster.local Host headers. + host: true, + allowedHosts: true, // GraphQL-only public API (commands are mutations, not POST /todo.*). proxy: distributedGraphqlProxy(api) }, From 5d48315925f632afa7f7add1bf5e5b7c43946c8a Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Sun, 9 Aug 2026 00:49:55 -0500 Subject: [PATCH 5/7] feat(e2e-ui): local identity scope and cluster gitops layout Move shared Zitadel Project/roles/humans to cluster scope, keep OIDC apps worktree-scoped for redirects and Login V2 baseUri. Restructure cluster gitops (configurations/, stacks/, DRCs) and ExternalSecrets hooks for OIDC + demo passwords. --- tests/e2e-ui/.gitignore | 1 + tests/e2e-ui/README.md | 6 +- tests/e2e-ui/api/.gitops/deploy/README.md | 13 ++ .../templates/deployment-cluster-dev.yaml | 15 ++- tests/e2e-ui/api/.gitops/deploy/values.yaml | 16 +++ tests/e2e-ui/gitops/cluster/README.md | 16 +-- tests/e2e-ui/gitops/cluster/SECRETS.md | 83 ++++++++++--- tests/e2e-ui/gitops/cluster/auth/README.md | 26 ++-- .../auth/external-secret-masterkey.yaml | 27 +++++ .../auth-stack.yaml | 0 .../psql-stack.yaml | 0 .../secret-stack.yaml | 0 .../gitops/cluster/e2e-identity/README.md | 70 ----------- .../cluster/e2e-identity/grants.yaml.example | 65 ---------- .../gitops/cluster/e2e-identity/humans.yaml | 77 ------------ .../gitops/cluster/e2e-identity/oidc-app.yaml | 50 -------- .../gitops/cluster/e2e-identity/project.yaml | 17 --- .../gitops/cluster/e2e-identity/roles.yaml | 34 ------ .../cluster/providerconfigs/aws.yaml.example | 14 --- .../providerconfigs/github.yaml.example | 13 -- .../providerconfigs/zitadel.yaml.example | 13 -- .../gitops/cluster/providers/aws.yaml.example | 11 -- .../cluster/providers/github.yaml.example | 7 -- .../helm.yaml => providers/helm-drc.yaml} | 4 +- .../kubernetes-drc.yaml} | 4 +- .../cluster/providers/zitadel.yaml.example | 7 -- tests/e2e-ui/gitops/cluster/psql/README.md | 7 -- .../e2e-ui/gitops/cluster/secrets/stack.yaml | 112 +++++++++++++++++- .../cluster/secrets/vault-auth-delegator.yaml | 17 +++ .../{auth/stack.yaml => stacks/auth.yaml} | 15 +-- .../{psql/stack.yaml => stacks/psql.yaml} | 2 +- tests/e2e-ui/gitops/env/local/ui.yaml | 5 + tests/e2e-ui/gitops/envs/local/ui.yaml | 14 +++ tests/e2e-ui/ui/.gitops/deploy/README.md | 56 +++++++++ .../ui/.gitops/deploy/templates/_helpers.tpl | 55 +++++++++ .../templates/deployment-cluster-dev.yaml | 6 + .../external-secret-human-passwords.yaml | 43 +++++++ .../templates/external-secret-oidc.yaml | 45 +++++++ .../deploy/templates/identity-humans.yaml | 44 +++++++ .../deploy/templates/identity-oidc.yaml | 54 +++++++++ .../deploy/templates/identity-project.yaml | 26 ++++ .../deploy/templates/identity-roles.yaml | 46 +++++++ tests/e2e-ui/ui/.gitops/deploy/values.yaml | 83 ++++++++++++- .../ui/src/routes/login/+page.server.ts | 2 + 44 files changed, 779 insertions(+), 442 deletions(-) create mode 100644 tests/e2e-ui/api/.gitops/deploy/README.md create mode 100644 tests/e2e-ui/gitops/cluster/auth/external-secret-masterkey.yaml rename tests/e2e-ui/gitops/cluster/{packages => configurations}/auth-stack.yaml (100%) rename tests/e2e-ui/gitops/cluster/{packages => configurations}/psql-stack.yaml (100%) rename tests/e2e-ui/gitops/cluster/{packages => configurations}/secret-stack.yaml (100%) delete mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/README.md delete mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/grants.yaml.example delete mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/humans.yaml delete mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/oidc-app.yaml delete mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/project.yaml delete mode 100644 tests/e2e-ui/gitops/cluster/e2e-identity/roles.yaml delete mode 100644 tests/e2e-ui/gitops/cluster/providerconfigs/aws.yaml.example delete mode 100644 tests/e2e-ui/gitops/cluster/providerconfigs/github.yaml.example delete mode 100644 tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml.example delete mode 100644 tests/e2e-ui/gitops/cluster/providers/aws.yaml.example delete mode 100644 tests/e2e-ui/gitops/cluster/providers/github.yaml.example rename tests/e2e-ui/gitops/cluster/{runtime/helm.yaml => providers/helm-drc.yaml} (87%) rename tests/e2e-ui/gitops/cluster/{runtime/kubernetes.yaml => providers/kubernetes-drc.yaml} (86%) delete mode 100644 tests/e2e-ui/gitops/cluster/providers/zitadel.yaml.example delete mode 100644 tests/e2e-ui/gitops/cluster/psql/README.md create mode 100644 tests/e2e-ui/gitops/cluster/secrets/vault-auth-delegator.yaml rename tests/e2e-ui/gitops/cluster/{auth/stack.yaml => stacks/auth.yaml} (67%) rename tests/e2e-ui/gitops/cluster/{psql/stack.yaml => stacks/psql.yaml} (93%) create mode 100644 tests/e2e-ui/ui/.gitops/deploy/README.md create mode 100644 tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-human-passwords.yaml create mode 100644 tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-oidc.yaml create mode 100644 tests/e2e-ui/ui/.gitops/deploy/templates/identity-humans.yaml create mode 100644 tests/e2e-ui/ui/.gitops/deploy/templates/identity-oidc.yaml create mode 100644 tests/e2e-ui/ui/.gitops/deploy/templates/identity-project.yaml create mode 100644 tests/e2e-ui/ui/.gitops/deploy/templates/identity-roles.yaml diff --git a/tests/e2e-ui/.gitignore b/tests/e2e-ui/.gitignore index 0e859a8b..4849ea6f 100644 --- a/tests/e2e-ui/.gitignore +++ b/tests/e2e-ui/.gitignore @@ -14,3 +14,4 @@ e2e/.auth playwright-report test-results blob-report +gitops/cluster/e2e-identity/grants.yaml diff --git a/tests/e2e-ui/README.md b/tests/e2e-ui/README.md index 74311374..2709df2b 100644 --- a/tests/e2e-ui/README.md +++ b/tests/e2e-ui/README.md @@ -23,8 +23,8 @@ Optional **platform XRs** under `gitops/cluster/` (AuthStack + PSQLStack): ```bash hops config install /xrs/stacks/k8s/psql hops config install /xrs/stacks/k8s/auth -kubectl apply -f gitops/cluster/psql/stack.yaml -kubectl apply -f gitops/cluster/auth/stack.yaml +kubectl apply -f gitops/cluster/stacks/psql.yaml +kubectl apply -f gitops/cluster/stacks/auth.yaml ``` Optional cloud providers (non-secret YAML only): @@ -36,7 +36,7 @@ hops local github --gitops ./gitops/cluster Charts: `api/.gitops/deploy`, `ui/.gitops/deploy`. App Applications: `gitops/envs/local/` (legacy: `gitops/env/local/`). -Control plane: `gitops/cluster/` (`PSQLStack`, `AuthStack`, packages). +Control plane: `gitops/cluster/` (`stacks/`, `configurations/`, …). ## Option B — compose + host processes diff --git a/tests/e2e-ui/api/.gitops/deploy/README.md b/tests/e2e-ui/api/.gitops/deploy/README.md new file mode 100644 index 00000000..08a1c32f --- /dev/null +++ b/tests/e2e-ui/api/.gitops/deploy/README.md @@ -0,0 +1,13 @@ +# e2e-ui API chart (workbench) + +Renders: + +- **Deployment / Service** (`appRuntime: cluster-dev` \| `package`) +- **Optional PSQLCluster** when `database.enabled: true` + +OIDC project / demo humans / web app live on the **UI** chart +(`ui/.gitops/deploy` `identity.*`). This chart only consumes residual OIDC +settings (`OIDC_ISSUER`, `OIDC_AUDIENCE`, shared `e2e-ui-oidc` secret keys) so +Bearer tokens map engine roles. + +Platform AuthStack + ProviderConfig stay under `gitops/cluster/`. diff --git a/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml index c2563960..aad2afda 100644 --- a/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml +++ b/tests/e2e-ui/api/.gitops/deploy/templates/deployment-cluster-dev.yaml @@ -54,10 +54,17 @@ spec: - containerPort: {{ .Values.service.targetPort }} name: http env: - - name: BIND - value: {{ .Values.env.BIND | quote }} - - name: DATABASE_URL - value: {{ .Values.env.DATABASE_URL | default "sqlite:/tmp/e2e-ui.db?mode=rwc" | quote }} + {{- range $k, $v := .Values.env }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- range $k, $ref := .Values.secretEnv }} + - name: {{ $k }} + valueFrom: + secretKeyRef: + name: {{ $ref.secretName | quote }} + key: {{ $ref.key | quote }} + {{- end }} # Keep cargo on PATH even if something mangles the environment. - name: PATH value: "/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" diff --git a/tests/e2e-ui/api/.gitops/deploy/values.yaml b/tests/e2e-ui/api/.gitops/deploy/values.yaml index 9a0f1e8e..794b91b4 100644 --- a/tests/e2e-ui/api/.gitops/deploy/values.yaml +++ b/tests/e2e-ui/api/.gitops/deploy/values.yaml @@ -32,6 +32,22 @@ env: BIND: "0.0.0.0:8791" # Offline-friendly default until PSQL platform pack is installed. DATABASE_URL: "sqlite:/tmp/e2e-ui.db?mode=rwc" + # OidcBearer — required so SSR Bearer tokens map engine roles (user/admin). + # Without this, API stays on DevHeaders and todos/blob/chat user surfaces are hidden. + OIDC_ISSUER: "http://zitadel-zitadel.auth.svc.cluster.local:8080" + # Project id (role assertion audience) — residual after identity Project Ready. + OIDC_AUDIENCE: "" + OIDC_JWKS_URI: "http://zitadel-zitadel.auth.svc.cluster.local:8080/oauth/v2/keys" + +# Residual secrets (same e2e-ui-oidc object as the UI chart, in app namespace). +# OIDC app + humans live on the UI chart (identity.*); API only consumes tokens. +secretEnv: + OIDC_CLIENT_ID: + secretName: e2e-ui-oidc + key: OIDC_CLIENT_ID + ZITADEL_SERVICE_USER_TOKEN: + secretName: e2e-ui-oidc + key: ZITADEL_SERVICE_USER_TOKEN database: # Dogfood local CP often lacks PSQLCluster CRD; disable by default for up path. diff --git a/tests/e2e-ui/gitops/cluster/README.md b/tests/e2e-ui/gitops/cluster/README.md index 8cf148ea..7ad3fe54 100644 --- a/tests/e2e-ui/gitops/cluster/README.md +++ b/tests/e2e-ui/gitops/cluster/README.md @@ -11,7 +11,7 @@ Platform / CP resources for the **shared** local control plane: ```text / - gitops/cluster/ # ← this tree (AuthStack, PSQLStack, packages) + gitops/cluster/ # ← this tree (AuthStack, PSQLStack, configurations) gitops/envs/local/ # Application YAMLs → hops-wt-* namespaces clients/foo/.gitops/deploy/ platform/api/.gitops/deploy/ @@ -19,12 +19,14 @@ Platform / CP resources for the **shared** local control plane: | Path | Contents | |------|----------| -| `packages/` | Crossplane `Configuration` installs (psql-stack, auth-stack, secret-stack) | -| `psql/stack.yaml` | **`PSQLStack` XR** — CNPG + SC `psql` (local-path) | -| `auth/stack.yaml` | **`AuthStack` XR** — Zitadel + embedded Postgres | -| `secrets/stack.yaml` | **`SecretStack` XR** — ESO + optional Vault (local backend) | -| `providers/` | Optional cloud Provider installs (`hops local aws --gitops …`) | +| `configurations/` | Crossplane `Configuration` installs (psql-stack, auth-stack, secret-stack) | +| `stacks/psql.yaml` | **`PSQLStack` XR** — CNPG + SC `psql` (local-path) | +| `stacks/auth.yaml` | **`AuthStack` XR** — Zitadel + embedded Postgres | +| `secrets/` | **`SecretStack`** + vault-auth-delegator CRB (ESO + Vault; k8s auth via Helm postStart) | +| `auth/` | Auth residuals (e.g. masterkey ExternalSecret), not the XR claim | +| `providers/` | Provider installs + per-provider DRCs (`helm.yaml` / `helm-drc.yaml`, …) | | `providerconfigs/` | ProviderConfig shapes (`secretRef` only) | +| `e2e-identity/` | **Moved** — worktree UI chart `identity.*` values (pointer README only) | ## Apply + watch (local gitops vibes) @@ -45,7 +47,7 @@ hops local gitops worktree ./gitops/envs/local --name dogfood ``` Crossplane reconciles XRs (`PSQLStack`, `AuthStack`, `SecretStack`, …) after each apply. -Edit `auth/stack.yaml`, `psql/stack.yaml`, or `secrets/stack.yaml` → saved → applied → CP converges. +Edit `stacks/auth.yaml`, `stacks/psql.yaml`, or `secrets/stack.yaml` → saved → applied → CP converges. **SecretStack (local):** install the Configuration from source first (`hops config install --path …/xrs/stacks/aws/secret`), then apply this cluster tree diff --git a/tests/e2e-ui/gitops/cluster/SECRETS.md b/tests/e2e-ui/gitops/cluster/SECRETS.md index 2298e510..ce9a23ee 100644 --- a/tests/e2e-ui/gitops/cluster/SECRETS.md +++ b/tests/e2e-ui/gitops/cluster/SECRETS.md @@ -16,17 +16,28 @@ ProviderConfig for MRs (after PAT exists): ```bash hops local zitadel --source-context dory --source-namespace auth \ - --domain zitadel.auth.svc.cluster.local --port 8080 --insecure \ - --gitops ./gitops/cluster + --domain zitadel-zitadel.auth.svc.cluster.local --port 8080 --insecure # creates default/zitadel-credentials + ProviderConfig secretRef ``` -## e2e identity (`ns: default` for MRs) +Helm default fullname is **`zitadel-zitadel`** — use that Service FQDN for +`domain` / OIDC issuer (no alias Service). Optional later: +`chartValues.fullnameOverride: zitadel`. + +## Identity (UI chart) + +Renders from **`ui/.gitops/deploy`** when `identity.enabled: true` +(see `gitops/envs/local/ui.yaml`). + +| Scope | Resources | +|-------|-----------| +| **Cluster** | Project `e2e-ui`, roles, humans `alice`/`bob`/`admin` | +| **Worktree** | OIDC web app only (`e2e-ui--web` — redirects + Login V2 baseUri) | | Secret | Purpose | |--------|---------| -| `e2e-human-passwords` | Keys `alice`, `bob`, `admin` — HumanUser `initialPasswordSecretRef` | -| `zitadel-credentials` | Provider PC JSON (`access_token`, `domain`, `port`, `insecure`) | +| `e2e-human-passwords` | Keys `alice`, `bob`, `admin` (cluster-shared) | +| `zitadel-credentials` | Provider PC / ClusterProviderConfig (`hops local zitadel`) | ```bash kubectl -n default create secret generic e2e-human-passwords \ @@ -35,35 +46,76 @@ kubectl -n default create secret generic e2e-human-passwords \ --from-literal=admin=Password1! ``` +Demo login (every env): **alice / bob / admin · Password1!** + ## e2e-ui app (`ns: hops-wt-*`) | Secret | Keys | Used by | |--------|------|---------| -| `e2e-ui-oidc` | `AUTH_SECRET`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` | UI chart `secretEnv` → `secretKeyRef` | +| `e2e-ui-oidc` | `AUTH_SECRET`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `ZITADEL_SERVICE_USER_TOKEN` | UI chart `secretEnv` → `secretKeyRef` | ```bash +# OIDC client secret: regenerate via Management API _generate_client_secret +# Login-client PAT: AuthStack residual secret auth/login-client key pat kubectl -n hops-wt-dogfood create secret generic e2e-ui-oidc \ --from-literal=AUTH_SECRET='local-workbench-dogfood-auth-secret-not-for-prod' \ --from-literal=OIDC_CLIENT_ID='…from Oidc app…' \ - --from-literal=OIDC_CLIENT_SECRET='…from Oidc app create…' + --from-literal=OIDC_CLIENT_SECRET='…from Oidc app create…' \ + --from-literal=ZITADEL_SERVICE_USER_TOKEN="$(kubectl -n auth get secret login-client -o jsonpath='{.data.pat}' | base64 -d)" ``` Non-secret OIDC config (issuer, AUTH_URL) stays in `ui/.gitops/deploy/values.yaml`. -## SecretStack / ESO + Vault (`secrets/stack.yaml`) +### Login V2 (required for browser sign-in) + +Custom e2e-ui `/login` pages own Login V2 (Oidc `loginVersion.loginV2.baseUri` +on the **worktree** OIDC app). Prefer app-level baseUri over instance-wide +defaults so multiple worktrees can coexist. + +## SecretStack / ESO + Vault (`secrets/`) Local fixture uses **`backend: vault`** with **`vault.install: true`** (in-cluster Vault Helm + ESO). No AWS PodIdentity on dory. | Resource | Purpose | |----------|---------| -| Helm `external-secrets` | ESO operator | -| Helm `vault` | Dev Vault (when `vault.install`) | -| ClusterSecretStore `vault` | ESO → Vault (after both Helms exist) | +| `secrets/stack.yaml` | SecretStack XR — ESO + Vault Helm + ClusterSecretStore | +| `secrets/vault-auth-delegator.yaml` | CRB: Vault SA → `system:auth-delegator` (TokenReview) | +| Helm `vault` `server.postStart` | Init/unseal (once), KV mount, k8s auth + ESO role | +| PVC (`dataStorage` 1Gi) | File storage under `/vault/data` (survives pod restarts) | +| ClusterSecretStore `vault` | ESO → Vault (Ready after postStart + CRB) | + +### Persistence -Auth for Vault SecretStore is **kubernetes** auth (role `external-secrets`). -Bootstrap Vault k8s auth + role is still manual/dev until automation lands; -stack is Ready for ESO+Vault install even if SecretStore auth needs follow-up. +`server.dev` is **off**. Vault runs standalone with `storage "file"` on a PVC +(default StorageClass, dory: `local-path`). First start initializes and writes: + +```text +/vault/data/.hops-init # Unseal Key 1 + Initial Root Token (chmod 600) +``` + +Later starts: unseal from that file, re-apply k8s auth (idempotent). **KV data +survives pod restarts**; deleting the PVC is a full reset. + +Root token is **not** the fixed string `root` (that was dev-only). For CLI writes: + +```bash +export VAULT_TOKEN="$( + kubectl -n vault exec vault-0 -- \ + awk '/^Initial Root Token:/{print $NF}' /vault/data/.hops-init +)" +export VAULT_ADDR=http://127.0.0.1:8200 +# kubectl port-forward -n vault svc/vault 8200:8200 # if needed +hops secrets sync vault -y +``` + +### Declarative trust path + +1. Apply cluster gitops (SecretStack + CRB). +2. postStart: init (once) → unseal → `secret/` KV v2 → k8s auth for ESO. +3. ClusterSecretStore `vault` Ready. +4. **Write** with `hops secrets sync vault`. +5. **Read** via ExternalSecrets (auth masterkey, UI oidc/humans). Install package from source before first apply: @@ -72,6 +124,9 @@ hops config install --path /xrs/stacks/aws/secret --context dory hops local gitops cluster ./gitops/cluster ``` +Switching an existing **dev/inmem** Vault release to file storage may need a +one-time recreate of the Helm release / PVC (data was ephemeral anyway). + Do **not** put the SecretStack claim under meta `local/` — that folder is for ad-hoc colima-style one-off claims, not the workbench cluster gitops path. diff --git a/tests/e2e-ui/gitops/cluster/auth/README.md b/tests/e2e-ui/gitops/cluster/auth/README.md index d1e8c891..10c90945 100644 --- a/tests/e2e-ui/gitops/cluster/auth/README.md +++ b/tests/e2e-ui/gitops/cluster/auth/README.md @@ -2,39 +2,41 @@ | File | Kind | |------|------| -| `stack.yaml` | `AuthStack` XR — Zitadel + embedded `PSQLCluster` | +| [`../stacks/auth.yaml`](../stacks/auth.yaml) | `AuthStack` XR — Zitadel + embedded `PSQLCluster` | +| `external-secret-masterkey.yaml` | Optional ESO materialize of `zitadel-masterkey` from Vault | ## Local addressing (no ingress) | | Value | |--|--------| -| Install namespace | **`auth`** (not `zitadel`) | -| AuthStack / release name | **`zitadel`** | -| Service FQDN | **`zitadel.auth.svc.cluster.local:8080`** | -| OIDC issuer | `http://zitadel.auth.svc.cluster.local:8080` | +| Install namespace | **`auth`** | +| AuthStack XR name | **`zitadel`** | +| Helm Service (default fullname) | **`zitadel-zitadel.auth.svc.cluster.local:8080`** | +| OIDC issuer / `domain` | `http://zitadel-zitadel.auth.svc.cluster.local:8080` | | Gateway / ingress | disabled — ClusterIP only for now | ## Apply order ```bash hops local start --backend dory --gitops ./gitops/cluster -# or after packages: +# or after configurations: hops local gitops cluster ./gitops/cluster kubectl get authstack zitadel -n default kubectl get svc -n auth -# expect: zitadel.auth.svc.cluster.local +# expect: zitadel-zitadel (API) — not a separate short-name alias ``` -## e2e-ui identity +## e2e-ui identity (worktree) -See [`../e2e-identity/`](../e2e-identity/) for Project / humans / OIDC app / Grant XRs. -ProviderConfig residual: +App Project / humans / OIDC app render from the **UI** chart when +`gitops/envs/local/ui.yaml` sets `identity.enabled: true` (not cluster gitops). + +ProviderConfig residual (cluster): ```bash hops local zitadel --source-context dory --source-namespace auth \ - --domain zitadel.auth.svc.cluster.local --port 8080 --insecure \ - --gitops ./gitops/cluster + --domain zitadel-zitadel.auth.svc.cluster.local --port 8080 --insecure ``` ## Secrets diff --git a/tests/e2e-ui/gitops/cluster/auth/external-secret-masterkey.yaml b/tests/e2e-ui/gitops/cluster/auth/external-secret-masterkey.yaml new file mode 100644 index 00000000..3ed7a699 --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/auth/external-secret-masterkey.yaml @@ -0,0 +1,27 @@ +# Materialize AuthStack masterkey from Vault so it survives ns/worktree churn. +# Requires ClusterSecretStore vault Ready (Vault Helm postStart + vault-auth-delegator) +# and path auth/zitadel-masterkey written via: hops secrets sync vault +# See SECRETS.md. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: zitadel-masterkey + namespace: auth + labels: + hops.ops.com.ai/fixture: e2e-ui + hops.ops.com.ai/secret: zitadel-masterkey +spec: + refreshInterval: 1m + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: zitadel-masterkey + creationPolicy: Owner + template: + type: Opaque + data: + - secretKey: masterkey + remoteRef: + key: auth/zitadel-masterkey + property: masterkey diff --git a/tests/e2e-ui/gitops/cluster/packages/auth-stack.yaml b/tests/e2e-ui/gitops/cluster/configurations/auth-stack.yaml similarity index 100% rename from tests/e2e-ui/gitops/cluster/packages/auth-stack.yaml rename to tests/e2e-ui/gitops/cluster/configurations/auth-stack.yaml diff --git a/tests/e2e-ui/gitops/cluster/packages/psql-stack.yaml b/tests/e2e-ui/gitops/cluster/configurations/psql-stack.yaml similarity index 100% rename from tests/e2e-ui/gitops/cluster/packages/psql-stack.yaml rename to tests/e2e-ui/gitops/cluster/configurations/psql-stack.yaml diff --git a/tests/e2e-ui/gitops/cluster/packages/secret-stack.yaml b/tests/e2e-ui/gitops/cluster/configurations/secret-stack.yaml similarity index 100% rename from tests/e2e-ui/gitops/cluster/packages/secret-stack.yaml rename to tests/e2e-ui/gitops/cluster/configurations/secret-stack.yaml diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/README.md b/tests/e2e-ui/gitops/cluster/e2e-identity/README.md deleted file mode 100644 index 8903cbdc..00000000 --- a/tests/e2e-ui/gitops/cluster/e2e-identity/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# e2e-ui identity (Zitadel gitops) - -Declarative stand-in for most of `scripts/up.sh` Management API bootstrap. - -## Layout - -| Path | What | -|------|------| -| `project.yaml` | Zitadel **Project** `e2e-ui` | -| `roles.yaml` | Project roles `user` + `admin` | -| `humans.yaml` | **HumanUser** MRs alice / bob / admin | -| `oidc-app.yaml` | OIDC web app for Auth.js (redirects → worktree UI) | -| `grants.yaml.example` | hops **Grant** XRs — copy to `grants.yaml` and fill UUIDs after humans Ready | -| `../providers/zitadel.yaml` | provider-upjet-zitadel pin | -| `../providerconfigs/zitadel.yaml` | PC shape (credentials residual) | - -## Apply order - -1. **AuthStack Ready** (`../auth/stack.yaml`) — Zitadel in ns **`auth`**, issuer - `http://zitadel.auth.svc.cluster.local:8080` (ClusterIP only; no ingress yet). - -2. **Credentials residual** (once `iam-admin-pat` exists in ns `auth`): - - ```bash - hops local zitadel \ - --source-context dory \ - --source-namespace auth \ - --domain zitadel.auth.svc.cluster.local \ - --port 8080 --insecure \ - --gitops ./gitops/cluster - ``` - -3. **Password residual** (K8s Secret, not values.yaml): - - ```bash - kubectl create secret generic e2e-human-passwords -n default \ - --from-literal=alice=Password1! \ - --from-literal=bob=Password1! \ - --from-literal=admin=Password1! - ``` - - Humans already `secretRef` that object. UI OIDC secrets: `e2e-ui-oidc` in the - worktree namespace (`secretEnv` in `ui/.gitops/deploy/values.yaml`). See - [../SECRETS.md](../SECRETS.md). - -4. `hops local gitops cluster ./gitops/cluster` — project, roles, humans, oidc - -5. Observe IDs, copy `grants.yaml.example` → `grants.yaml`, fill UUIDs, re-apply - -## XRs vs provider MRs - -| Resource | Kind | -|----------|------| -| Platform install | **AuthStack** XR | -| Role grants | **Grant** XR (`auth.hops.ops.com.ai`) | -| Project / Role / Human / OIDC app | provider MRs (no thin hops wrapper) | -| Machine identities (optional) | **MachineUser** XR | - -## Residuals - -- `zitadel-credentials` Secret (PAT JSON for ProviderConfig) -- `e2e-human-passwords` Secret -- OIDC **client secret** (only at app create — copy into UI env / Secret) -- Grant UUID fill-in until composition supports refs from HumanUser - -## App wiring - -UI chart values under `ui/.gitops/deploy` point `OIDC_ISSUER` at AuthStack issuer -(`http://auth.localtest.me` browser / in-cluster service for pods as needed). -Client id/secret still residual until connection details are automated. diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/grants.yaml.example b/tests/e2e-ui/gitops/cluster/e2e-identity/grants.yaml.example deleted file mode 100644 index c6472970..00000000 --- a/tests/e2e-ui/gitops/cluster/e2e-identity/grants.yaml.example +++ /dev/null @@ -1,65 +0,0 @@ -# hops Grant XRs — simpler API than raw user/project grant MRs. -# After humans + project are Ready, copy UUIDs from: -# kubectl get humanuser e2e-alice -o jsonpath='{.status.atProvider.userId}' -# kubectl get project e2e-ui -o jsonpath='{.status.atProvider.id}' -# kubectl get humanuser e2e-alice -o jsonpath='{.status.atProvider.orgId}' -# -# PLACEHOLDER values below must be replaced before grants reconcile. -# (Future: Grant XR with userRef/projectRef — not in auth-stack yet.) -apiVersion: auth.hops.ops.com.ai/v1alpha1 -kind: Grant -metadata: - name: e2e-alice-user - namespace: default - labels: - hops.ops.com.ai/app: e2e-ui - hops.ops.com.ai/demo-user: alice -spec: - userId: "REPLACE_WITH_ALICE_USER_ID" - userOrgId: "REPLACE_WITH_ORG_ID" - projectId: "REPLACE_WITH_PROJECT_ID" - projectOrgId: "REPLACE_WITH_ORG_ID" - roles: - - user - providerConfigRef: - name: default - kind: ProviderConfig ---- -apiVersion: auth.hops.ops.com.ai/v1alpha1 -kind: Grant -metadata: - name: e2e-bob-user - namespace: default - labels: - hops.ops.com.ai/app: e2e-ui - hops.ops.com.ai/demo-user: bob -spec: - userId: "REPLACE_WITH_BOB_USER_ID" - userOrgId: "REPLACE_WITH_ORG_ID" - projectId: "REPLACE_WITH_PROJECT_ID" - projectOrgId: "REPLACE_WITH_ORG_ID" - roles: - - user - providerConfigRef: - name: default - kind: ProviderConfig ---- -apiVersion: auth.hops.ops.com.ai/v1alpha1 -kind: Grant -metadata: - name: e2e-admin-roles - namespace: default - labels: - hops.ops.com.ai/app: e2e-ui - hops.ops.com.ai/demo-user: admin -spec: - userId: "REPLACE_WITH_ADMIN_USER_ID" - userOrgId: "REPLACE_WITH_ORG_ID" - projectId: "REPLACE_WITH_PROJECT_ID" - projectOrgId: "REPLACE_WITH_ORG_ID" - roles: - - admin - - user - providerConfigRef: - name: default - kind: ProviderConfig diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/humans.yaml b/tests/e2e-ui/gitops/cluster/e2e-identity/humans.yaml deleted file mode 100644 index 0fb92f26..00000000 --- a/tests/e2e-ui/gitops/cluster/e2e-identity/humans.yaml +++ /dev/null @@ -1,77 +0,0 @@ -# Demo humans — scripts/up.sh alice / bob / admin. -# Passwords: Secret e2e-human-passwords (residual — not in git). -# kubectl create secret generic e2e-human-passwords -n default \ -# --from-literal=alice=Password1! \ -# --from-literal=bob=Password1! \ -# --from-literal=admin=Password1! -apiVersion: user.zitadel.m.crossplane.io/v1alpha1 -kind: HumanUser -metadata: - name: e2e-alice - namespace: default - labels: - hops.ops.com.ai/app: e2e-ui - hops.ops.com.ai/demo-user: alice -spec: - forProvider: - userName: alice - firstName: alice - lastName: E2E - displayName: alice - email: alice@e2e.local - isEmailVerified: true - initialSkipPasswordChange: true - initialPasswordSecretRef: - name: e2e-human-passwords - key: alice - providerConfigRef: - name: default - kind: ProviderConfig ---- -apiVersion: user.zitadel.m.crossplane.io/v1alpha1 -kind: HumanUser -metadata: - name: e2e-bob - namespace: default - labels: - hops.ops.com.ai/app: e2e-ui - hops.ops.com.ai/demo-user: bob -spec: - forProvider: - userName: bob - firstName: bob - lastName: E2E - displayName: bob - email: bob@e2e.local - isEmailVerified: true - initialSkipPasswordChange: true - initialPasswordSecretRef: - name: e2e-human-passwords - key: bob - providerConfigRef: - name: default - kind: ProviderConfig ---- -apiVersion: user.zitadel.m.crossplane.io/v1alpha1 -kind: HumanUser -metadata: - name: e2e-admin - namespace: default - labels: - hops.ops.com.ai/app: e2e-ui - hops.ops.com.ai/demo-user: admin -spec: - forProvider: - userName: admin - firstName: admin - lastName: E2E - displayName: admin - email: admin@e2e.local - isEmailVerified: true - initialSkipPasswordChange: true - initialPasswordSecretRef: - name: e2e-human-passwords - key: admin - providerConfigRef: - name: default - kind: ProviderConfig diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/oidc-app.yaml b/tests/e2e-ui/gitops/cluster/e2e-identity/oidc-app.yaml deleted file mode 100644 index df9b9a37..00000000 --- a/tests/e2e-ui/gitops/cluster/e2e-identity/oidc-app.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# OIDC web app for e2e-ui Auth.js — scripts/up.sh e2e-ui-web. -# Redirects include workbench Service DNS + localhost for host access. -# Client secret is residual (observe status / Zitadel console on create). -apiVersion: application.zitadel.m.crossplane.io/v1alpha1 -kind: Oidc -metadata: - name: e2e-ui-web - namespace: default - labels: - hops.ops.com.ai/app: e2e-ui -spec: - forProvider: - name: e2e-ui-web - projectIdRef: - name: e2e-ui - appType: OIDC_APP_TYPE_WEB - authMethodType: OIDC_AUTH_METHOD_TYPE_BASIC - responseTypes: - - OIDC_RESPONSE_TYPE_CODE - grantTypes: - - OIDC_GRANT_TYPE_AUTHORIZATION_CODE - - OIDC_GRANT_TYPE_REFRESH_TOKEN - accessTokenType: OIDC_TOKEN_TYPE_JWT - accessTokenRoleAssertion: true - idTokenRoleAssertion: true - idTokenUserinfoAssertion: true - devMode: true - version: OIDC_VERSION_1_0 - redirectUris: - # Workbench dogfood (cluster DNS / map) - - http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180/auth/callback/oidc - - http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180/auth/callback - # Host map / local vite - - http://127.0.0.1:5180/auth/callback/oidc - - http://localhost:5180/auth/callback/oidc - - http://127.0.0.1:18101/auth/callback/oidc - - http://localhost:18101/auth/callback/oidc - postLogoutRedirectUris: - - http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180/ - - http://127.0.0.1:5180/ - - http://localhost:5180/ - - http://127.0.0.1:18101/ - - http://localhost:18101/ - # Login V2 base = custom app login pages (Fieldnote UI), not stock zitadel-login. - loginVersion: - - loginV2: - - baseUri: http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180 - providerConfigRef: - name: default - kind: ProviderConfig diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/project.yaml b/tests/e2e-ui/gitops/cluster/e2e-identity/project.yaml deleted file mode 100644 index 233ae60e..00000000 --- a/tests/e2e-ui/gitops/cluster/e2e-identity/project.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# e2e-ui Zitadel project — replaces scripts/up.sh "Project e2e-ui" -apiVersion: project.zitadel.m.crossplane.io/v1alpha1 -kind: Project -metadata: - name: e2e-ui - namespace: default - labels: - hops.ops.com.ai/app: e2e-ui -spec: - forProvider: - name: e2e-ui - projectRoleAssertion: true - projectRoleCheck: true - hasProjectCheck: true - providerConfigRef: - name: default - kind: ProviderConfig diff --git a/tests/e2e-ui/gitops/cluster/e2e-identity/roles.yaml b/tests/e2e-ui/gitops/cluster/e2e-identity/roles.yaml deleted file mode 100644 index 94b73505..00000000 --- a/tests/e2e-ui/gitops/cluster/e2e-identity/roles.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Project roles — scripts/up.sh roles user + admin -apiVersion: project.zitadel.m.crossplane.io/v1alpha1 -kind: Role -metadata: - name: e2e-ui-role-user - namespace: default - labels: - hops.ops.com.ai/app: e2e-ui -spec: - forProvider: - roleKey: user - displayName: user - projectIdRef: - name: e2e-ui - providerConfigRef: - name: default - kind: ProviderConfig ---- -apiVersion: project.zitadel.m.crossplane.io/v1alpha1 -kind: Role -metadata: - name: e2e-ui-role-admin - namespace: default - labels: - hops.ops.com.ai/app: e2e-ui -spec: - forProvider: - roleKey: admin - displayName: admin - projectIdRef: - name: e2e-ui - providerConfigRef: - name: default - kind: ProviderConfig diff --git a/tests/e2e-ui/gitops/cluster/providerconfigs/aws.yaml.example b/tests/e2e-ui/gitops/cluster/providerconfigs/aws.yaml.example deleted file mode 100644 index e5c6132b..00000000 --- a/tests/e2e-ui/gitops/cluster/providerconfigs/aws.yaml.example +++ /dev/null @@ -1,14 +0,0 @@ -# Non-secret ProviderConfig shape. Secret `aws-creds` is live-only until local ESO exists. -# hops local aws --gitops ./gitops/cluster -apiVersion: aws.m.upbound.io/v1beta1 -kind: ProviderConfig -metadata: - name: default - namespace: default -spec: - credentials: - source: Secret - secretRef: - namespace: default - name: aws-creds - key: credentials diff --git a/tests/e2e-ui/gitops/cluster/providerconfigs/github.yaml.example b/tests/e2e-ui/gitops/cluster/providerconfigs/github.yaml.example deleted file mode 100644 index 08b9051c..00000000 --- a/tests/e2e-ui/gitops/cluster/providerconfigs/github.yaml.example +++ /dev/null @@ -1,13 +0,0 @@ -# Secret `github-creds` is live-only. -apiVersion: github.m.upbound.io/v1beta1 -kind: ProviderConfig -metadata: - name: default - namespace: default -spec: - credentials: - source: Secret - secretRef: - namespace: default - name: github-creds - key: credentials diff --git a/tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml.example b/tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml.example deleted file mode 100644 index 9dbd9d2d..00000000 --- a/tests/e2e-ui/gitops/cluster/providerconfigs/zitadel.yaml.example +++ /dev/null @@ -1,13 +0,0 @@ -# Secret `zitadel-credentials` is live-only (PAT from AuthStack / env). -apiVersion: zitadel.m.crossplane.io/v1beta1 -kind: ProviderConfig -metadata: - name: default - namespace: default -spec: - credentials: - source: Secret - secretRef: - namespace: default - name: zitadel-credentials - key: credentials diff --git a/tests/e2e-ui/gitops/cluster/providers/aws.yaml.example b/tests/e2e-ui/gitops/cluster/providers/aws.yaml.example deleted file mode 100644 index ac4a0347..00000000 --- a/tests/e2e-ui/gitops/cluster/providers/aws.yaml.example +++ /dev/null @@ -1,11 +0,0 @@ -# Example only — generate with: -# hops local aws --gitops ./gitops/cluster -# Then commit providers/aws.yaml (not secrets). -apiVersion: pkg.crossplane.io/v1 -kind: Provider -metadata: - name: crossplane-contrib-provider-family-aws -spec: - package: xpkg.crossplane.io/crossplane-contrib/provider-family-aws:v2.4.0 - runtimeConfigRef: - name: aws diff --git a/tests/e2e-ui/gitops/cluster/providers/github.yaml.example b/tests/e2e-ui/gitops/cluster/providers/github.yaml.example deleted file mode 100644 index f7bbf4f8..00000000 --- a/tests/e2e-ui/gitops/cluster/providers/github.yaml.example +++ /dev/null @@ -1,7 +0,0 @@ -# hops local github --gitops ./gitops/cluster -apiVersion: pkg.crossplane.io/v1 -kind: Provider -metadata: - name: crossplane-contrib-provider-upjet-github -spec: - package: xpkg.crossplane.io/crossplane-contrib/provider-upjet-github:v0.19.0 diff --git a/tests/e2e-ui/gitops/cluster/runtime/helm.yaml b/tests/e2e-ui/gitops/cluster/providers/helm-drc.yaml similarity index 87% rename from tests/e2e-ui/gitops/cluster/runtime/helm.yaml rename to tests/e2e-ui/gitops/cluster/providers/helm-drc.yaml index 0c5c6b0a..c9953f16 100644 --- a/tests/e2e-ui/gitops/cluster/runtime/helm.yaml +++ b/tests/e2e-ui/gitops/cluster/providers/helm-drc.yaml @@ -1,10 +1,10 @@ # Per-provider DeploymentRuntimeConfig for provider-helm (local dev). +# Colocated with providers/helm.yaml (runtimeConfigRef: local-dev-helm). # # Each provider gets its OWN uniquely-named DRC + cluster-admin ServiceAccount. # Providers must NOT share a DRC: a shared runtime config lets one provider's # image/SA silently clobber the other's pod (and makes drift impossible to -# attribute). This mirrors the per-provider DRCs the -# crossplane-helm-provider-stack composes for remote clusters. +# attribute). apiVersion: pkg.crossplane.io/v1beta1 kind: DeploymentRuntimeConfig metadata: diff --git a/tests/e2e-ui/gitops/cluster/runtime/kubernetes.yaml b/tests/e2e-ui/gitops/cluster/providers/kubernetes-drc.yaml similarity index 86% rename from tests/e2e-ui/gitops/cluster/runtime/kubernetes.yaml rename to tests/e2e-ui/gitops/cluster/providers/kubernetes-drc.yaml index 7ce229dd..76f6f5a6 100644 --- a/tests/e2e-ui/gitops/cluster/runtime/kubernetes.yaml +++ b/tests/e2e-ui/gitops/cluster/providers/kubernetes-drc.yaml @@ -1,10 +1,10 @@ # Per-provider DeploymentRuntimeConfig for provider-kubernetes (local dev). +# Colocated with providers/kubernetes.yaml (runtimeConfigRef: local-dev-kubernetes). # # Each provider gets its OWN uniquely-named DRC + cluster-admin ServiceAccount. # Providers must NOT share a DRC: a shared runtime config lets one provider's # image/SA silently clobber the other's pod (and makes drift impossible to -# attribute). This mirrors the per-provider DRCs the -# crossplane-kubernetes-provider-stack composes for remote clusters. +# attribute). apiVersion: pkg.crossplane.io/v1beta1 kind: DeploymentRuntimeConfig metadata: diff --git a/tests/e2e-ui/gitops/cluster/providers/zitadel.yaml.example b/tests/e2e-ui/gitops/cluster/providers/zitadel.yaml.example deleted file mode 100644 index 24a63c54..00000000 --- a/tests/e2e-ui/gitops/cluster/providers/zitadel.yaml.example +++ /dev/null @@ -1,7 +0,0 @@ -# hops local zitadel --source-context --gitops ./gitops/cluster -apiVersion: pkg.crossplane.io/v1 -kind: Provider -metadata: - name: crossplane-contrib-provider-upjet-zitadel -spec: - package: xpkg.crossplane.io/crossplane-contrib/provider-upjet-zitadel:v0.1.1 diff --git a/tests/e2e-ui/gitops/cluster/psql/README.md b/tests/e2e-ui/gitops/cluster/psql/README.md deleted file mode 100644 index 00e41563..00000000 --- a/tests/e2e-ui/gitops/cluster/psql/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# PSQL platform (local) - -| File | Kind | -|------|------| -| `stack.yaml` | `PSQLStack` XR — CNPG + StorageClass `psql` on `local-path` | - -AuthStack embedded DB and future e2e-ui `PSQLCluster` claims use `spec.storage.class: psql`. diff --git a/tests/e2e-ui/gitops/cluster/secrets/stack.yaml b/tests/e2e-ui/gitops/cluster/secrets/stack.yaml index 37881266..33870bea 100644 --- a/tests/e2e-ui/gitops/cluster/secrets/stack.yaml +++ b/tests/e2e-ui/gitops/cluster/secrets/stack.yaml @@ -6,7 +6,14 @@ # Then: hops local gitops cluster ./gitops/cluster # # backend=vault + vault.install for local (no AWS PodIdentity required). -# clusterName/provider defaults match auth/psql stacks (helm PC name: default). +# +# Persistence: file storage on a PVC (not server.dev / inmem). postStart +# init+unseal once, stores root token + unseal key on the volume: +# /vault/data/.hops-init +# K8s auth for ESO is re-applied every start. Seed KV with: +# export VAULT_TOKEN=$(kubectl -n vault exec vault-0 -- \ +# awk '/^Initial Root Token:/{print $NF}' /vault/data/.hops-init) +# hops secrets sync vault apiVersion: hops.ops.com.ai/v1alpha1 kind: SecretStack metadata: @@ -36,3 +43,106 @@ spec: method: kubernetes mountPath: kubernetes role: external-secrets + # Merged into official Vault Helm chart (SecretStack composition defaults + # use server.dev + no PVC; we override for durable local dogfood). + values: + server: + # Leave dev mode — inmem cannot use dataStorage. + dev: + enabled: false + standalone: + enabled: true + config: | + ui = true + disable_mlock = true + listener "tcp" { + tls_disable = 1 + address = "[::]:8200" + cluster_address = "[::]:8201" + } + storage "file" { + path = "/vault/data" + } + dataStorage: + enabled: true + size: 1Gi + # omit storageClass → cluster default (dory: local-path) + postStart: + - /bin/sh + - -c + - | + # NOTE: do not use bare `set -e` around `vault status` — sealed/uninit + # returns exit 2 and would abort the hook (killing the container). + set -u + export VAULT_ADDR=http://127.0.0.1:8200 + INIT_FILE=/vault/data/.hops-init + + vault_status_rc() { + vault status >/dev/null 2>&1 + echo $? + } + + # API is up when status returns 0 (unsealed) or 2 (sealed / not init). + i=0 + while [ "$i" -lt 90 ]; do + rc=$(vault_status_rc) + if [ "$rc" -eq 0 ] || [ "$rc" -eq 2 ]; then + break + fi + i=$((i + 1)) + sleep 1 + done + rc=$(vault_status_rc) + if [ "$rc" -ne 0 ] && [ "$rc" -ne 2 ]; then + echo "vault postStart: API not ready after wait (rc=$rc)" >&2 + exit 1 + fi + + if [ ! -f "$INIT_FILE" ]; then + echo "vault postStart: initializing (1 share / threshold 1)" + vault operator init -key-shares=1 -key-threshold=1 >"$INIT_FILE" + chmod 600 "$INIT_FILE" || true + fi + + UNSEAL_KEY=$(awk '/Unseal Key 1:/{print $NF}' "$INIT_FILE") + ROOT_TOKEN=$(awk '/Initial Root Token:/{print $NF}' "$INIT_FILE") + if [ -z "${UNSEAL_KEY}" ] || [ -z "${ROOT_TOKEN}" ]; then + echo "vault postStart: failed to parse $INIT_FILE" >&2 + cat "$INIT_FILE" >&2 || true + exit 1 + fi + + # Unseal when sealed (status exit 2). + rc=$(vault_status_rc) + if [ "$rc" -ne 0 ]; then + vault operator unseal "$UNSEAL_KEY" >/dev/null + fi + export VAULT_TOKEN="$ROOT_TOKEN" + + # Non-dev installs do not auto-mount secret/ — enable KV v2 for ESO. + if ! vault secrets list -format=json 2>/dev/null | grep -q '"secret/"'; then + vault secrets enable -path=secret kv-v2 + fi + + if ! vault auth list -format=json 2>/dev/null | grep -q '"kubernetes/"'; then + vault auth enable kubernetes + fi + + vault write auth/kubernetes/config \ + kubernetes_host="https://kubernetes.default.svc" \ + token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ + kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ + disable_iss_validation=true + + printf '%s\n' \ + 'path "secret/data/*" { capabilities = ["read", "list"] }' \ + 'path "secret/metadata/*" { capabilities = ["read", "list"] }' \ + | vault policy write external-secrets - + + vault write auth/kubernetes/role/external-secrets \ + bound_service_account_names=external-secrets \ + bound_service_account_namespaces=external-secrets \ + policies=external-secrets \ + ttl=24h + + echo "vault postStart: unsealed + kubernetes auth ready (init file $INIT_FILE)" diff --git a/tests/e2e-ui/gitops/cluster/secrets/vault-auth-delegator.yaml b/tests/e2e-ui/gitops/cluster/secrets/vault-auth-delegator.yaml new file mode 100644 index 00000000..7b9e772a --- /dev/null +++ b/tests/e2e-ui/gitops/cluster/secrets/vault-auth-delegator.yaml @@ -0,0 +1,17 @@ +# Allow the Vault server SA to call TokenReview (required for kubernetes auth). +# Independent of the Vault pod lifecycle — apply with cluster gitops. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: vault-auth-delegator + labels: + hops.ops.com.ai/fixture: e2e-ui + hops.ops.com.ai/component: vault +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator +subjects: + - kind: ServiceAccount + name: vault + namespace: vault diff --git a/tests/e2e-ui/gitops/cluster/auth/stack.yaml b/tests/e2e-ui/gitops/cluster/stacks/auth.yaml similarity index 67% rename from tests/e2e-ui/gitops/cluster/auth/stack.yaml rename to tests/e2e-ui/gitops/cluster/stacks/auth.yaml index 4bc52ccc..f7779f71 100644 --- a/tests/e2e-ui/gitops/cluster/auth/stack.yaml +++ b/tests/e2e-ui/gitops/cluster/stacks/auth.yaml @@ -1,14 +1,15 @@ # Local AuthStack: Zitadel + embedded PSQLCluster (owned by this XR). # # No ingress/gateway for local first step — Service is ClusterIP only. -# Issuer / ExternalDomain = in-cluster DNS (Option A): -# http://zitadel.auth.svc.cluster.local:8080 +# Helm chart fullname is release+chart → Service **zitadel-zitadel** (default). +# Issuer / ExternalDomain must match that FQDN (Option A): +# http://zitadel-zitadel.auth.svc.cluster.local:8080 +# (Later: chartValues.fullnameOverride: zitadel if we want a shorter name.) # -# Install packages first, then: hops local gitops cluster ./gitops/cluster +# Install configurations first, then: hops local gitops cluster ./gitops/cluster apiVersion: hops.ops.com.ai/v1alpha1 kind: AuthStack metadata: - # Helm release / Service name → zitadel..svc.cluster.local name: zitadel namespace: default spec: @@ -18,11 +19,11 @@ spec: kubernetesProviderConfigRef: name: default - # Install namespace (was default "zitadel"; use "auth" so FQDN is zitadel.auth…) + # Install namespace namespace: auth - # In-cluster hostname only (no fake public DNS / ingress yet) - domain: zitadel.auth.svc.cluster.local + # Must match Helm Service FQDN (default fullname = zitadel-zitadel) + domain: zitadel-zitadel.auth.svc.cluster.local externalSecure: false gateway: diff --git a/tests/e2e-ui/gitops/cluster/psql/stack.yaml b/tests/e2e-ui/gitops/cluster/stacks/psql.yaml similarity index 93% rename from tests/e2e-ui/gitops/cluster/psql/stack.yaml rename to tests/e2e-ui/gitops/cluster/stacks/psql.yaml index c4f32908..dc1a1378 100644 --- a/tests/e2e-ui/gitops/cluster/psql/stack.yaml +++ b/tests/e2e-ui/gitops/cluster/stacks/psql.yaml @@ -1,5 +1,5 @@ # Platform layer: CNPG + Atlas + StorageClass named `psql`. -# Install package first: `hops config install ` (or apply packages/psql-stack.yaml). +# Install package first: `hops config install ` (or apply configurations/psql-stack.yaml). # # Dory/k3s uses rancher.io/local-path (default SC). Snapshot class off (no CSI snapshotter). apiVersion: hops.ops.com.ai/v1alpha1 diff --git a/tests/e2e-ui/gitops/env/local/ui.yaml b/tests/e2e-ui/gitops/env/local/ui.yaml index 2b29b060..e7ea080a 100644 --- a/tests/e2e-ui/gitops/env/local/ui.yaml +++ b/tests/e2e-ui/gitops/env/local/ui.yaml @@ -11,6 +11,11 @@ spec: values: local: true appRuntime: cluster-dev + # Worktree OIDC project/users/app (requires AuthStack + ProviderConfig). + # Set orgId residual after: GET …/management/v1/orgs/me + identity: + enabled: true + orgId: "" # residual — required for roles/humans destination: namespace: hops-wt-default syncPolicy: diff --git a/tests/e2e-ui/gitops/envs/local/ui.yaml b/tests/e2e-ui/gitops/envs/local/ui.yaml index 2b29b060..01adb159 100644 --- a/tests/e2e-ui/gitops/envs/local/ui.yaml +++ b/tests/e2e-ui/gitops/envs/local/ui.yaml @@ -11,6 +11,20 @@ spec: values: local: true appRuntime: cluster-dev + # Identity: cluster Project+roles+humans; OIDC app is worktree-scoped. + # orgId residual after: GET …/management/v1/orgs/me + identity: + enabled: true + # hops-ops org on local AuthStack (GET …/management/v1/orgs/me) + orgId: "385408591454077587" + projectName: e2e-ui + projectNamespace: default + humansNamespace: default + mrNamespace: default + demoUsers: true + providerConfigRef: + name: default + kind: ClusterProviderConfig destination: namespace: hops-wt-default syncPolicy: diff --git a/tests/e2e-ui/ui/.gitops/deploy/README.md b/tests/e2e-ui/ui/.gitops/deploy/README.md new file mode 100644 index 00000000..b7337422 --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/README.md @@ -0,0 +1,56 @@ +# e2e-ui UI chart (workbench) + +Renders: + +- **Deployment / Service** (`appRuntime: cluster-dev` \| `package`) +- **Optional identity** when `identity.enabled: true`: + - **Cluster-shared:** Project, roles (`user` / `admin`), demo humans + - **Per worktree:** OIDC web app only (redirects + Login V2 `baseUri`) + +The OIDC web app + Login V2 `baseUri` belong with the UI (browser sign-in), +not the API. + +## Identity scope + +| Resource | Scope | K8s name example | +|----------|--------|------------------| +| Project | **Cluster** | `e2e-ui` | +| Roles | **Cluster** | `e2e-role-user`, `e2e-role-admin` | +| HumanUsers | **Cluster** | `e2e-alice` (login `alice`) | +| Password secret | **Cluster** | `e2e-human-passwords` | +| OIDC app | **Worktree** | `e2e-ui-dogfood-web` | + +Demo login (every local env): **alice / bob / admin · Password1!** + +## Values + +| Value | Purpose | +|-------|---------| +| `identity.enabled` | Gate identity templates | +| `identity.orgId` | Residual FirstInstance org id | +| `identity.projectName` | Shared Project name (default `e2e-ui`) | +| `identity.workspace` | Worktree id for OIDC app names only | +| `identity.demoUsers` | Cluster-shared alice / bob / admin | +| `identity.projectNamespace` | Namespace for Project + Role MRs | +| `identity.humansNamespace` | Namespace for HumanUser MRs | +| `identity.mrNamespace` | Namespace for worktree OIDC app MRs | +| `identity.uiBaseURL` | Optional; else `http://e2e-ui-ui..svc…:5180` | +| `identity.passwordSecret.name` | Shared password secret (default `e2e-human-passwords`) | + +```yaml +# gitops/envs/local/ui.yaml +identity: + enabled: true + orgId: "…" # residual after AuthStack Ready + demoUsers: true + providerConfigRef: + name: default + kind: ClusterProviderConfig +``` + +User grants (human → project roles) are residual once per CP after Project + +humans are Ready. Prefer a **ClusterProviderConfig** so all identity MRs share +credentials. + +API still needs residual `OIDC_AUDIENCE` / project id (and app `e2e-ui-oidc` +secret keys) after the shared Project and worktree OIDC app are Ready. diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl b/tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl index d15cfe4b..eaad5d59 100644 --- a/tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl @@ -7,3 +7,58 @@ app.kubernetes.io/name: {{ include "e2e-ui-ui.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end -}} + +{{/* +Workspace id for multi-worktree OIDC apps (redirects / Login V2 baseUri). +Prefers identity.workspace; else strips hops-wt- from injected namespace. +*/}} +{{- define "e2e-ui-ui.workspace" -}} +{{- if .Values.identity.workspace -}} +{{- .Values.identity.workspace -}} +{{- else if .Values.namespace -}} +{{- $ns := .Values.namespace -}} +{{- if hasPrefix "hops-wt-" $ns -}} +{{- trimPrefix "hops-wt-" $ns -}} +{{- else -}} +{{- $ns -}} +{{- end -}} +{{- else -}} +local +{{- end -}} +{{- end -}} + +{{/* Worktree-scoped OIDC app name prefix: e2e-ui- */}} +{{- define "e2e-ui-ui.identityPrefix" -}} +{{- printf "e2e-ui-%s" (include "e2e-ui-ui.workspace" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* Cluster-shared Zitadel Project MR name (one per control plane). */}} +{{- define "e2e-ui-ui.clusterProjectName" -}} +{{- default "e2e-ui" .Values.identity.projectName -}} +{{- end -}} + +{{- define "e2e-ui-ui.identityLabels" -}} +app.kubernetes.io/name: e2e-ui +app.kubernetes.io/component: identity +hops.ops.com.ai/app: e2e-ui +hops.ops.com.ai/workspace: {{ include "e2e-ui-ui.workspace" . | quote }} +{{- end -}} + +{{- define "e2e-ui-ui.identityClusterLabels" -}} +app.kubernetes.io/name: e2e-ui +app.kubernetes.io/component: identity +hops.ops.com.ai/app: e2e-ui +hops.ops.com.ai/identity-scope: cluster +{{- end -}} + +{{/* UI public base for OIDC redirects / Login V2 baseUri */}} +{{- define "e2e-ui-ui.uiBaseURL" -}} +{{- if .Values.identity.uiBaseURL -}} +{{- .Values.identity.uiBaseURL | trimSuffix "/" -}} +{{- else -}} +{{- $ns := default "default" .Values.namespace -}} +{{- $svc := default (include "e2e-ui-ui.name" .) .Values.identity.uiService -}} +{{- $port := default .Values.service.port .Values.identity.uiPort -}} +{{- printf "http://%s.%s.svc.cluster.local:%v" $svc $ns $port -}} +{{- end -}} +{{- end -}} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml index a9a0ed7b..81008aa0 100644 --- a/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/deployment-cluster-dev.yaml @@ -64,9 +64,15 @@ spec: name: http env: {{- range $k, $v := .Values.env }} + {{- /* AUTH_URL rewritten from injected workspace namespace when present */ -}} + {{- if and (eq $k "AUTH_URL") $.Values.namespace }} + - name: AUTH_URL + value: {{ printf "http://e2e-ui-ui.%s.svc.cluster.local:5180" $.Values.namespace | quote }} + {{- else }} - name: {{ $k }} value: {{ $v | quote }} {{- end }} + {{- end }} {{- /* Secrets from K8s Secret objects — never plain values in gitops */ -}} {{- range $k, $ref := .Values.secretEnv }} - name: {{ $k }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-human-passwords.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-human-passwords.yaml new file mode 100644 index 00000000..f895ca61 --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-human-passwords.yaml @@ -0,0 +1,43 @@ +{{- if and .Values.externalSecrets .Values.externalSecrets.enabled .Values.externalSecrets.humanPasswords .Values.externalSecrets.humanPasswords.enabled }} +{{- $path := .Values.externalSecrets.humanPasswords.vaultPath | default "e2e-ui/shared/human-passwords" -}} +{{- $target := .Values.externalSecrets.humanPasswords.targetName | default "e2e-human-passwords" -}} +{{- $ns := .Values.identity.humansNamespace | default .Values.identity.mrNamespace | default "default" -}} +# Cluster-shared password secret for demo HumanUsers (not per-worktree). +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ $target }} + namespace: {{ $ns }} + labels: + {{- include "e2e-ui-ui.labels" . | nindent 4 }} + hops.ops.com.ai/app: e2e-ui + hops.ops.com.ai/workspace: {{ $ws | quote }} + hops.ops.com.ai/secret: human-passwords +spec: + refreshInterval: {{ .Values.externalSecrets.refreshInterval | default "1m" | quote }} + secretStoreRef: + name: {{ .Values.externalSecrets.secretStoreRef.name | default "vault" | quote }} + kind: {{ .Values.externalSecrets.secretStoreRef.kind | default "ClusterSecretStore" | quote }} + target: + name: {{ $target | quote }} + creationPolicy: Owner + template: + type: Opaque + metadata: + labels: + hops.ops.com.ai/app: e2e-ui + hops.ops.com.ai/workspace: {{ $ws | quote }} + data: + - secretKey: alice + remoteRef: + key: {{ $path | quote }} + property: alice + - secretKey: bob + remoteRef: + key: {{ $path | quote }} + property: bob + - secretKey: admin + remoteRef: + key: {{ $path | quote }} + property: admin +{{- end }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-oidc.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-oidc.yaml new file mode 100644 index 00000000..f442f658 --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-oidc.yaml @@ -0,0 +1,45 @@ +{{- if and .Values.externalSecrets .Values.externalSecrets.enabled }} +{{- $ws := include "e2e-ui-ui.workspace" . -}} +{{- $path := .Values.externalSecrets.oidc.vaultPath | default (printf "e2e-ui/%s/oidc" $ws) -}} +{{- $target := .Values.externalSecrets.oidc.targetName | default "e2e-ui-oidc" -}} +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ $target }} + labels: + {{- include "e2e-ui-ui.labels" . | nindent 4 }} + hops.ops.com.ai/app: e2e-ui + hops.ops.com.ai/workspace: {{ $ws | quote }} + hops.ops.com.ai/secret: oidc +spec: + refreshInterval: {{ .Values.externalSecrets.refreshInterval | default "1m" | quote }} + secretStoreRef: + name: {{ .Values.externalSecrets.secretStoreRef.name | default "vault" | quote }} + kind: {{ .Values.externalSecrets.secretStoreRef.kind | default "ClusterSecretStore" | quote }} + target: + name: {{ $target | quote }} + creationPolicy: Owner + template: + type: Opaque + metadata: + labels: + hops.ops.com.ai/app: e2e-ui + hops.ops.com.ai/workspace: {{ $ws | quote }} + data: + - secretKey: AUTH_SECRET + remoteRef: + key: {{ $path | quote }} + property: AUTH_SECRET + - secretKey: OIDC_CLIENT_ID + remoteRef: + key: {{ $path | quote }} + property: OIDC_CLIENT_ID + - secretKey: OIDC_CLIENT_SECRET + remoteRef: + key: {{ $path | quote }} + property: OIDC_CLIENT_SECRET + - secretKey: ZITADEL_SERVICE_USER_TOKEN + remoteRef: + key: {{ $path | quote }} + property: ZITADEL_SERVICE_USER_TOKEN +{{- end }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/identity-humans.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-humans.yaml new file mode 100644 index 00000000..cf5467f2 --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-humans.yaml @@ -0,0 +1,44 @@ +{{- /* + Cluster-shared demo humans (alice / bob / admin). + + One set per local control plane — not per worktree. Login names stay plain + so every env uses the same credentials. Project + roles are also cluster- + shared; only the OIDC web app is worktree-scoped. +*/ -}} +{{- if and .Values.identity.enabled .Values.identity.orgId .Values.identity.demoUsers }} +{{- $ns := .Values.identity.humansNamespace | default .Values.identity.projectNamespace | default .Values.identity.mrNamespace | default "default" -}} +{{- $pc := .Values.identity.providerConfigRef.name | default "default" -}} +{{- $pck := .Values.identity.providerConfigRef.kind | default "ProviderConfig" -}} +{{- $pwSecret := .Values.identity.passwordSecret.name | default "e2e-human-passwords" -}} +{{- range $user := list "alice" "bob" "admin" }} +--- +apiVersion: user.zitadel.m.crossplane.io/v1alpha1 +kind: HumanUser +metadata: + # Stable cluster-scoped names (no worktree prefix). + name: e2e-{{ $user }} + namespace: {{ $ns }} + labels: + app.kubernetes.io/name: e2e-ui + app.kubernetes.io/component: identity + hops.ops.com.ai/app: e2e-ui + hops.ops.com.ai/identity-scope: cluster + hops.ops.com.ai/demo-user: {{ $user | quote }} +spec: + forProvider: + orgId: {{ $.Values.identity.orgId | quote }} + userName: {{ $user | quote }} + firstName: {{ $user | quote }} + lastName: E2E + displayName: {{ $user | quote }} + email: {{ printf "%s@e2e.local" $user | quote }} + isEmailVerified: true + initialSkipPasswordChange: true + initialPasswordSecretRef: + name: {{ $pwSecret | quote }} + key: {{ $user | quote }} + providerConfigRef: + name: {{ $pc | quote }} + kind: {{ $pck | quote }} +{{- end }} +{{- end }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/identity-oidc.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-oidc.yaml new file mode 100644 index 00000000..80214de3 --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-oidc.yaml @@ -0,0 +1,54 @@ +{{- /* + Worktree-scoped OIDC web app (redirects + Login V2 baseUri differ per env). + + Lives under the cluster-shared Project (identity.projectName / e2e-ui). +*/ -}} +{{- if .Values.identity.enabled }} +{{- $prefix := include "e2e-ui-ui.identityPrefix" . -}} +{{- $project := include "e2e-ui-ui.clusterProjectName" . -}} +{{- $uiBase := include "e2e-ui-ui.uiBaseURL" . -}} +{{- $ns := .Values.identity.mrNamespace | default "default" -}} +apiVersion: application.zitadel.m.crossplane.io/v1alpha1 +kind: Oidc +metadata: + name: {{ $prefix }}-web + namespace: {{ $ns }} + labels: + {{- include "e2e-ui-ui.identityLabels" . | nindent 4 }} +spec: + forProvider: + name: {{ printf "%s-web" $prefix | quote }} + projectIdRef: + name: {{ $project }} + appType: OIDC_APP_TYPE_WEB + authMethodType: OIDC_AUTH_METHOD_TYPE_BASIC + responseTypes: + - OIDC_RESPONSE_TYPE_CODE + grantTypes: + - OIDC_GRANT_TYPE_AUTHORIZATION_CODE + - OIDC_GRANT_TYPE_REFRESH_TOKEN + accessTokenType: OIDC_TOKEN_TYPE_JWT + accessTokenRoleAssertion: true + idTokenRoleAssertion: true + idTokenUserinfoAssertion: true + devMode: true + version: OIDC_VERSION_1_0 + redirectUris: + - {{ printf "%s/auth/callback/oidc" $uiBase | quote }} + - {{ printf "%s/auth/callback" $uiBase | quote }} + {{- range .Values.identity.extraRedirectUris }} + - {{ . | quote }} + {{- end }} + postLogoutRedirectUris: + - {{ printf "%s/" $uiBase | quote }} + {{- range .Values.identity.extraPostLogoutRedirectUris }} + - {{ . | quote }} + {{- end }} + # Custom Login V2 = app /login pages (not stock zitadel-login). + loginVersion: + - loginV2: + - baseUri: {{ $uiBase | quote }} + providerConfigRef: + name: {{ .Values.identity.providerConfigRef.name | default "default" | quote }} + kind: {{ .Values.identity.providerConfigRef.kind | default "ProviderConfig" | quote }} +{{- end }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/identity-project.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-project.yaml new file mode 100644 index 00000000..60df7ddb --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-project.yaml @@ -0,0 +1,26 @@ +{{- /* + Cluster-shared Zitadel Project (one e2e-ui project for the whole CP). + + Roles and demo humans attach here. OIDC apps stay worktree-scoped and + projectIdRef this name. +*/ -}} +{{- if .Values.identity.enabled }} +{{- $name := include "e2e-ui-ui.clusterProjectName" . -}} +{{- $ns := .Values.identity.projectNamespace | default .Values.identity.mrNamespace | default "default" -}} +apiVersion: project.zitadel.m.crossplane.io/v1alpha1 +kind: Project +metadata: + name: {{ $name }} + namespace: {{ $ns }} + labels: + {{- include "e2e-ui-ui.identityClusterLabels" . | nindent 4 }} +spec: + forProvider: + name: {{ $name | quote }} + projectRoleAssertion: true + projectRoleCheck: true + hasProjectCheck: true + providerConfigRef: + name: {{ .Values.identity.providerConfigRef.name | default "default" | quote }} + kind: {{ .Values.identity.providerConfigRef.kind | default "ProviderConfig" | quote }} +{{- end }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/identity-roles.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-roles.yaml new file mode 100644 index 00000000..502a3c84 --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-roles.yaml @@ -0,0 +1,46 @@ +{{- /* + Cluster-shared project roles (user / admin) on the shared e2e-ui Project. +*/ -}} +{{- if and .Values.identity.enabled .Values.identity.orgId }} +{{- $project := include "e2e-ui-ui.clusterProjectName" . -}} +{{- $ns := .Values.identity.projectNamespace | default .Values.identity.mrNamespace | default "default" -}} +{{- $pc := .Values.identity.providerConfigRef.name | default "default" -}} +{{- $pck := .Values.identity.providerConfigRef.kind | default "ProviderConfig" -}} +apiVersion: project.zitadel.m.crossplane.io/v1alpha1 +kind: Role +metadata: + name: e2e-role-user + namespace: {{ $ns }} + labels: + {{- include "e2e-ui-ui.identityClusterLabels" . | nindent 4 }} + hops.ops.com.ai/demo-role: user +spec: + forProvider: + orgId: {{ .Values.identity.orgId | quote }} + roleKey: user + displayName: user + projectIdRef: + name: {{ $project }} + providerConfigRef: + name: {{ $pc | quote }} + kind: {{ $pck | quote }} +--- +apiVersion: project.zitadel.m.crossplane.io/v1alpha1 +kind: Role +metadata: + name: e2e-role-admin + namespace: {{ $ns }} + labels: + {{- include "e2e-ui-ui.identityClusterLabels" . | nindent 4 }} + hops.ops.com.ai/demo-role: admin +spec: + forProvider: + orgId: {{ .Values.identity.orgId | quote }} + roleKey: admin + displayName: admin + projectIdRef: + name: {{ $project }} + providerConfigRef: + name: {{ $pc | quote }} + kind: {{ $pck | quote }} +{{- end }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/values.yaml b/tests/e2e-ui/ui/.gitops/deploy/values.yaml index bfcce65d..a0184ca3 100644 --- a/tests/e2e-ui/ui/.gitops/deploy/values.yaml +++ b/tests/e2e-ui/ui/.gitops/deploy/values.yaml @@ -44,18 +44,25 @@ env: E2E_API_ORIGIN: "http://e2e-ui-api:8791" E2E_BASE_URL: "http://e2e-ui-api:8791" PUBLIC_E2E_API_ORIGIN: "http://e2e-ui-api:8791" - AUTH_URL: "http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180" + # Overridden at render when hops injects namespace (hops-wt-*). + AUTH_URL: "http://e2e-ui-ui.hops-wt-default.svc.cluster.local:5180" AUTH_TRUST_HOST: "true" AUTH_USE_SECURE_COOKIES: "false" - # AuthStack issuer (in-cluster ClusterIP Service — no ingress) - OIDC_ISSUER: "http://zitadel.auth.svc.cluster.local:8080" + # AuthStack issuer (Helm default Service fullname) + OIDC_ISSUER: "http://zitadel-zitadel.auth.svc.cluster.local:8080" + # Project id residual after workspace identity Project Ready + OIDC_AUDIENCE: "" + ZITADEL_PROJECT_ID: "" # Secret-backed env (keys must exist on Secret e2e-ui-oidc in the app namespace). # Residual — create live (not in git): # kubectl -n hops-wt-dogfood create secret generic e2e-ui-oidc \ # --from-literal=AUTH_SECRET='…' \ # --from-literal=OIDC_CLIENT_ID='…' \ -# --from-literal=OIDC_CLIENT_SECRET='…' +# --from-literal=OIDC_CLIENT_SECRET='…' \ +# --from-literal=ZITADEL_SERVICE_USER_TOKEN="$(kubectl -n auth get secret login-client -o jsonpath='{.data.pat}' | base64 -d)" +# Also set instance Login V2 (once per AuthStack): +# PUT /v2/features/instance {"loginV2":{"required":true,"baseUri":"http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180"}} secretEnv: AUTH_SECRET: secretName: e2e-ui-oidc @@ -66,10 +73,78 @@ secretEnv: OIDC_CLIENT_SECRET: secretName: e2e-ui-oidc key: OIDC_CLIENT_SECRET + # login-client PAT (IAM_LOGIN_CLIENT) for custom /login Session API + CreateCallback + ZITADEL_SERVICE_USER_TOKEN: + secretName: e2e-ui-oidc + key: ZITADEL_SERVICE_USER_TOKEN # Optional: mount entire secrets as env (unused by default) envFromSecrets: [] +# Zitadel identity on the UI chart (OIDC web client + Login V2 host). +# +# Scope: +# - Project + roles + demo humans — **cluster-shared** (one set for the CP) +# - OIDC web app — **per worktree** (redirect URIs + Login V2 baseUri) +# +# Enable from Application values when the env needs OIDC dogfood. +identity: + enabled: false + # Residual after AuthStack Ready: GET …/management/v1/orgs/me + orgId: "" + # Worktree id for OIDC app names only (e2e-ui--web). Default: + # trimPrefix "hops-wt-" of injected namespace. + workspace: "" + # Cluster-shared Project MR name (roles + humans hang off this). + projectName: e2e-ui + # Cluster-shared demo humans (alice / bob / admin · Password1!) + demoUsers: true + # Namespace for worktree OIDC app MRs. + mrNamespace: default + # Namespace for shared Project / Role MRs (defaults to mrNamespace). + projectNamespace: default + # Namespace for shared HumanUser MRs (defaults to projectNamespace). + humansNamespace: default + # Optional overrides; defaults = this chart name + service.port + uiService: "" + uiPort: "" + # Optional full override of redirect base (Login V2 baseUri + callbacks) + uiBaseURL: "" + extraRedirectUris: + - http://127.0.0.1:5180/auth/callback/oidc + - http://localhost:5180/auth/callback/oidc + - http://127.0.0.1:18101/auth/callback/oidc + - http://localhost:18101/auth/callback/oidc + extraPostLogoutRedirectUris: + - http://127.0.0.1:5180/ + - http://localhost:5180/ + - http://127.0.0.1:18101/ + - http://localhost:18101/ + passwordSecret: + # Residual Secret keys: alice, bob, admin + # Cluster-shared default name (not per-worktree). + name: e2e-human-passwords + providerConfigRef: + name: default + # Prefer ClusterProviderConfig so shared + worktree MRs share credentials. + kind: ClusterProviderConfig + +# Materialize residual secrets from Vault (ClusterSecretStore "vault"). +# Enable after SecretStack CSS is Ready + hops secrets sync vault. +externalSecrets: + enabled: false + refreshInterval: 1m + secretStoreRef: + name: vault + kind: ClusterSecretStore + oidc: + vaultPath: "" # default e2e-ui//oidc + targetName: e2e-ui-oidc + humanPasswords: + enabled: true + vaultPath: "" # default e2e-ui//human-passwords + targetName: "" # default e2e-human-passwords- + sourceDelivery: mode: none # none | hostPath | sync hostPath: "" diff --git a/tests/e2e-ui/ui/src/routes/login/+page.server.ts b/tests/e2e-ui/ui/src/routes/login/+page.server.ts index ad29d05b..63b34527 100644 --- a/tests/e2e-ui/ui/src/routes/login/+page.server.ts +++ b/tests/e2e-ui/ui/src/routes/login/+page.server.ts @@ -19,6 +19,8 @@ export const load: PageServerLoad = async (event) => { return { authRequest, + // Workspace-prefixed when identity.workspace is set (e.g. dogfood-alice). + // Local dogfood also seeds plain alice/bob/admin for the same password. demoHint: 'Demo: alice / bob / admin · Password1!' }; }; From ab7ed979673805ae24e36615617841774376ae85 Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Sun, 9 Aug 2026 01:34:09 -0500 Subject: [PATCH 6/7] fix(e2e-ui): set OIDC audience residual for OidcBearer dogfood Wire project id into local Application env so the API does not fall back to DevHeaders, and drop hand-rolled deliveryPath so delivery uses the git worktree root by default. --- tests/e2e-ui/api/.gitops/deploy/values.yaml | 8 ++++--- tests/e2e-ui/gitops/env/local/api.yaml | 8 +++++-- tests/e2e-ui/gitops/env/local/ui.yaml | 25 ++++++++++++++++----- tests/e2e-ui/gitops/envs/local/api.yaml | 8 +++++-- tests/e2e-ui/gitops/envs/local/ui.yaml | 10 +++++++-- tests/e2e-ui/ui/src/routes/+page.svelte | 2 +- 6 files changed, 46 insertions(+), 15 deletions(-) diff --git a/tests/e2e-ui/api/.gitops/deploy/values.yaml b/tests/e2e-ui/api/.gitops/deploy/values.yaml index 794b91b4..f1046c62 100644 --- a/tests/e2e-ui/api/.gitops/deploy/values.yaml +++ b/tests/e2e-ui/api/.gitops/deploy/values.yaml @@ -32,12 +32,14 @@ env: BIND: "0.0.0.0:8791" # Offline-friendly default until PSQL platform pack is installed. DATABASE_URL: "sqlite:/tmp/e2e-ui.db?mode=rwc" - # OidcBearer — required so SSR Bearer tokens map engine roles (user/admin). - # Without this, API stays on DevHeaders and todos/blob/chat user surfaces are hidden. + # OidcBearer — requires issuer **and** audience (project id). If either is empty + # the API falls back to DevHeaders and live user surfaces misbehave. OIDC_ISSUER: "http://zitadel-zitadel.auth.svc.cluster.local:8080" - # Project id (role assertion audience) — residual after identity Project Ready. + # Residual after Zitadel Project Ready (external-name / status.atProvider.id). + # Override from Application values (gitops/envs/local/api.yaml) for local dogfood. OIDC_AUDIENCE: "" OIDC_JWKS_URI: "http://zitadel-zitadel.auth.svc.cluster.local:8080/oauth/v2/keys" + ZITADEL_PROJECT_ID: "" # Residual secrets (same e2e-ui-oidc object as the UI chart, in app namespace). # OIDC app + humans live on the UI chart (identity.*); API only consumes tokens. diff --git a/tests/e2e-ui/gitops/env/local/api.yaml b/tests/e2e-ui/gitops/env/local/api.yaml index 6dc3b124..ecd092ad 100644 --- a/tests/e2e-ui/gitops/env/local/api.yaml +++ b/tests/e2e-ui/gitops/env/local/api.yaml @@ -5,14 +5,18 @@ metadata: spec: source: path: ../../../api/.gitops/deploy - # distributed monorepo root (Cargo workspace lives at tests/e2e-ui) - deliveryPath: ../../../../.. + # Default delivery = git worktree root (shared across UI/API pods). helm: values: local: true appRuntime: cluster-dev database: enabled: false + # Residual after shared Zitadel Project Ready (project id for role assertion). + # Dogfood live: e2e-ui-dogfood project until cluster Project `e2e-ui` is recreated. + env: + OIDC_AUDIENCE: "385467744310526613" + ZITADEL_PROJECT_ID: "385467744310526613" destination: namespace: hops-wt-default syncPolicy: diff --git a/tests/e2e-ui/gitops/env/local/ui.yaml b/tests/e2e-ui/gitops/env/local/ui.yaml index e7ea080a..5f1bad6e 100644 --- a/tests/e2e-ui/gitops/env/local/ui.yaml +++ b/tests/e2e-ui/gitops/env/local/ui.yaml @@ -5,17 +5,32 @@ metadata: spec: source: path: ../../../ui/.gitops/deploy - # distributed monorepo root (ui needs tests/e2e-ui/ui + js/ for file: dep) - deliveryPath: ../../../../.. + # Default delivery = git worktree root (shared across UI/API pods). + # Do not set deliveryPath unless you need a narrower override. helm: values: local: true appRuntime: cluster-dev - # Worktree OIDC project/users/app (requires AuthStack + ProviderConfig). - # Set orgId residual after: GET …/management/v1/orgs/me + # Residual: Zitadel project id for role scopes / API audience (must match + # Project Ready external-name). Dogfood live project until cluster e2e-ui + # Project is recreated on current AuthStack. + env: + OIDC_AUDIENCE: "385467744310526613" + ZITADEL_PROJECT_ID: "385467744310526613" + # Identity: cluster Project+roles+humans; OIDC app is worktree-scoped. + # orgId residual after: GET …/management/v1/orgs/me identity: enabled: true - orgId: "" # residual — required for roles/humans + # hops-ops org on local AuthStack (GET …/management/v1/orgs/me) + orgId: "385408591454077587" + projectName: e2e-ui + projectNamespace: default + humansNamespace: default + mrNamespace: default + demoUsers: true + providerConfigRef: + name: default + kind: ClusterProviderConfig destination: namespace: hops-wt-default syncPolicy: diff --git a/tests/e2e-ui/gitops/envs/local/api.yaml b/tests/e2e-ui/gitops/envs/local/api.yaml index 6dc3b124..ecd092ad 100644 --- a/tests/e2e-ui/gitops/envs/local/api.yaml +++ b/tests/e2e-ui/gitops/envs/local/api.yaml @@ -5,14 +5,18 @@ metadata: spec: source: path: ../../../api/.gitops/deploy - # distributed monorepo root (Cargo workspace lives at tests/e2e-ui) - deliveryPath: ../../../../.. + # Default delivery = git worktree root (shared across UI/API pods). helm: values: local: true appRuntime: cluster-dev database: enabled: false + # Residual after shared Zitadel Project Ready (project id for role assertion). + # Dogfood live: e2e-ui-dogfood project until cluster Project `e2e-ui` is recreated. + env: + OIDC_AUDIENCE: "385467744310526613" + ZITADEL_PROJECT_ID: "385467744310526613" destination: namespace: hops-wt-default syncPolicy: diff --git a/tests/e2e-ui/gitops/envs/local/ui.yaml b/tests/e2e-ui/gitops/envs/local/ui.yaml index 01adb159..5f1bad6e 100644 --- a/tests/e2e-ui/gitops/envs/local/ui.yaml +++ b/tests/e2e-ui/gitops/envs/local/ui.yaml @@ -5,12 +5,18 @@ metadata: spec: source: path: ../../../ui/.gitops/deploy - # distributed monorepo root (ui needs tests/e2e-ui/ui + js/ for file: dep) - deliveryPath: ../../../../.. + # Default delivery = git worktree root (shared across UI/API pods). + # Do not set deliveryPath unless you need a narrower override. helm: values: local: true appRuntime: cluster-dev + # Residual: Zitadel project id for role scopes / API audience (must match + # Project Ready external-name). Dogfood live project until cluster e2e-ui + # Project is recreated on current AuthStack. + env: + OIDC_AUDIENCE: "385467744310526613" + ZITADEL_PROJECT_ID: "385467744310526613" # Identity: cluster Project+roles+humans; OIDC app is worktree-scoped. # orgId residual after: GET …/management/v1/orgs/me identity: diff --git a/tests/e2e-ui/ui/src/routes/+page.svelte b/tests/e2e-ui/ui/src/routes/+page.svelte index 42139b1d..fd140bc6 100644 --- a/tests/e2e-ui/ui/src/routes/+page.svelte +++ b/tests/e2e-ui/ui/src/routes/+page.svelte @@ -201,7 +201,7 @@ query Todos @load { for building distributed systems, and realtime applications.

- Not a partial toolkit. An end-to-end stack — domain, service, query edge, and live client — + Not a partial toolkit. An end-to-end cloud native stack — domain, service, query edge, live client, and even gitops — so engineers who care about quality code can stay on the model and still ship polished, fast, maintainable products.

From 9e48f8758bb4e4f4e7e132e1bb55c1c7cac40ca7 Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Mon, 10 Aug 2026 00:37:40 -0500 Subject: [PATCH 7/7] feat(e2e-ui): gitops instance Login V2 Features from release namespace Manage Zitadel instance loginV2 via Features MR so authorize redirects to this worktree's custom /login. Derive baseUri and OIDC placement from hops --name / helm release namespace; drop hand-set residual docs. --- .../skills/distributed-graphql/SKILL.md | 7 +++++ .../skills/distributed-usage/SKILL.md | 8 +++++ tests/e2e-ui/gitops/cluster/README.md | 2 +- tests/e2e-ui/gitops/cluster/SECRETS.md | 17 ++++++++--- tests/e2e-ui/gitops/env/local/api.yaml | 2 -- tests/e2e-ui/gitops/env/local/ui.yaml | 2 -- tests/e2e-ui/gitops/envs/local/api.yaml | 2 -- tests/e2e-ui/gitops/envs/local/ui.yaml | 2 -- .../ui/.gitops/deploy/templates/_helpers.tpl | 21 +++++++------- .../external-secret-human-passwords.yaml | 2 -- .../templates/identity-instance-features.yaml | 29 +++++++++++++++++++ .../deploy/templates/identity-oidc.yaml | 3 +- tests/e2e-ui/ui/.gitops/deploy/values.yaml | 29 ++++++++++--------- 13 files changed, 87 insertions(+), 39 deletions(-) create mode 100644 tests/e2e-ui/ui/.gitops/deploy/templates/identity-instance-features.yaml diff --git a/distributed_cli/skills/distributed-graphql/SKILL.md b/distributed_cli/skills/distributed-graphql/SKILL.md index c805d707..49d00681 100644 --- a/distributed_cli/skills/distributed-graphql/SKILL.md +++ b/distributed_cli/skills/distributed-graphql/SKILL.md @@ -185,6 +185,13 @@ distributed scaffold my-service --query-api --read-models --store sqlite Emits typed causal handlers, a service-derived GraphQL engine, `src/query/` permissions, the `graphql` feature, and repository/token-key wiring. +## Client / replica drift (dogfood) + +If unit tests pass but the live UI 500s with schema/surface errors: the API +pod is still compiling or clients/`js/dist` are stale. Rebuild and wait — +don't redesign roles. Bare `__typename` (role fingerprint) ≠ application +surface clients. + ## Reference - Framework docs: `README § GraphQL query service` in the Distributed repo diff --git a/distributed_cli/skills/distributed-usage/SKILL.md b/distributed_cli/skills/distributed-usage/SKILL.md index f76d85c4..91da624e 100644 --- a/distributed_cli/skills/distributed-usage/SKILL.md +++ b/distributed_cli/skills/distributed-usage/SKILL.md @@ -309,6 +309,14 @@ Rules the fixture demonstrates: Run the full app: `cd tests/e2e-ui && make`. Suite: `make test`. +### e2e-ui cluster-dev dogfood (agents) + +Namespace = hops `--name`. Fix the live pods, not a host `make run`. After +source changes: `make gen-client`, restart, **wait until API logs `listening` +and Vite is ready**, then curl the FQDNs. Schema/pure errors usually mean +stale build, not a roles redesign. Full checklist: hops skill +`references/local-workbench.md` § Dogfood. + ## Manifest entrypoint Every service should export `read_model_catalog()` registering its read diff --git a/tests/e2e-ui/gitops/cluster/README.md b/tests/e2e-ui/gitops/cluster/README.md index 7ad3fe54..c68c9fd3 100644 --- a/tests/e2e-ui/gitops/cluster/README.md +++ b/tests/e2e-ui/gitops/cluster/README.md @@ -12,7 +12,7 @@ Platform / CP resources for the **shared** local control plane: ```text / gitops/cluster/ # ← this tree (AuthStack, PSQLStack, configurations) - gitops/envs/local/ # Application YAMLs → hops-wt-* namespaces + gitops/envs/local/ # Application YAMLs → namespace = --name clients/foo/.gitops/deploy/ platform/api/.gitops/deploy/ ``` diff --git a/tests/e2e-ui/gitops/cluster/SECRETS.md b/tests/e2e-ui/gitops/cluster/SECRETS.md index ce9a23ee..5252f5b5 100644 --- a/tests/e2e-ui/gitops/cluster/SECRETS.md +++ b/tests/e2e-ui/gitops/cluster/SECRETS.md @@ -48,7 +48,7 @@ kubectl -n default create secret generic e2e-human-passwords \ Demo login (every env): **alice / bob / admin · Password1!** -## e2e-ui app (`ns: hops-wt-*`) +## e2e-ui app (`ns: --name`) | Secret | Keys | Used by | |--------|------|---------| @@ -57,7 +57,7 @@ Demo login (every env): **alice / bob / admin · Password1!** ```bash # OIDC client secret: regenerate via Management API _generate_client_secret # Login-client PAT: AuthStack residual secret auth/login-client key pat -kubectl -n hops-wt-dogfood create secret generic e2e-ui-oidc \ +kubectl -n dogfood create secret generic e2e-ui-oidc \ --from-literal=AUTH_SECRET='local-workbench-dogfood-auth-secret-not-for-prod' \ --from-literal=OIDC_CLIENT_ID='…from Oidc app…' \ --from-literal=OIDC_CLIENT_SECRET='…from Oidc app create…' \ @@ -68,8 +68,17 @@ Non-secret OIDC config (issuer, AUTH_URL) stays in `ui/.gitops/deploy/values.yam ### Login V2 (required for browser sign-in) -Custom e2e-ui `/login` pages own Login V2 (Oidc `loginVersion.loginV2.baseUri` -on the **worktree** OIDC app). Prefer app-level baseUri over instance-wide +Gitops (UI chart when `identity.enabled`): + +1. **Oidc** app MR — redirects + `loginVersion.loginV2.baseUri` from release ns +2. **Features** MR (`instance.zitadel…/Features`) — instance `loginV2.required` + + `baseUri` so authorize redirects to this UI’s `/login` + +Both use `http://e2e-ui-ui..svc.cluster.local:5180`. Instance Features +is global per AuthStack (last applied worktree wins). Do not hand-`PUT` +`/v2/features/instance` — re-reconcile the UI chart instead. + +Prefer app-level baseUri over instance-wide defaults so multiple worktrees can coexist. ## SecretStack / ESO + Vault (`secrets/`) diff --git a/tests/e2e-ui/gitops/env/local/api.yaml b/tests/e2e-ui/gitops/env/local/api.yaml index ecd092ad..4e953c87 100644 --- a/tests/e2e-ui/gitops/env/local/api.yaml +++ b/tests/e2e-ui/gitops/env/local/api.yaml @@ -17,7 +17,5 @@ spec: env: OIDC_AUDIENCE: "385467744310526613" ZITADEL_PROJECT_ID: "385467744310526613" - destination: - namespace: hops-wt-default syncPolicy: prune: false diff --git a/tests/e2e-ui/gitops/env/local/ui.yaml b/tests/e2e-ui/gitops/env/local/ui.yaml index 5f1bad6e..73f2fa27 100644 --- a/tests/e2e-ui/gitops/env/local/ui.yaml +++ b/tests/e2e-ui/gitops/env/local/ui.yaml @@ -31,7 +31,5 @@ spec: providerConfigRef: name: default kind: ClusterProviderConfig - destination: - namespace: hops-wt-default syncPolicy: prune: false diff --git a/tests/e2e-ui/gitops/envs/local/api.yaml b/tests/e2e-ui/gitops/envs/local/api.yaml index ecd092ad..4e953c87 100644 --- a/tests/e2e-ui/gitops/envs/local/api.yaml +++ b/tests/e2e-ui/gitops/envs/local/api.yaml @@ -17,7 +17,5 @@ spec: env: OIDC_AUDIENCE: "385467744310526613" ZITADEL_PROJECT_ID: "385467744310526613" - destination: - namespace: hops-wt-default syncPolicy: prune: false diff --git a/tests/e2e-ui/gitops/envs/local/ui.yaml b/tests/e2e-ui/gitops/envs/local/ui.yaml index 5f1bad6e..73f2fa27 100644 --- a/tests/e2e-ui/gitops/envs/local/ui.yaml +++ b/tests/e2e-ui/gitops/envs/local/ui.yaml @@ -31,7 +31,5 @@ spec: providerConfigRef: name: default kind: ClusterProviderConfig - destination: - namespace: hops-wt-default syncPolicy: prune: false diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl b/tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl index eaad5d59..571867fb 100644 --- a/tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/_helpers.tpl @@ -10,21 +10,19 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Workspace id for multi-worktree OIDC apps (redirects / Login V2 baseUri). -Prefers identity.workspace; else strips hops-wt- from injected namespace. +Prefers identity.workspace; else release/workspace namespace (= hops --name). */}} {{- define "e2e-ui-ui.workspace" -}} {{- if .Values.identity.workspace -}} {{- .Values.identity.workspace -}} -{{- else if .Values.namespace -}} -{{- $ns := .Values.namespace -}} -{{- if hasPrefix "hops-wt-" $ns -}} -{{- trimPrefix "hops-wt-" $ns -}} {{- else -}} -{{- $ns -}} +{{- include "e2e-ui-ui.releaseNamespace" . -}} {{- end -}} -{{- else -}} -local {{- end -}} + +{{/* App namespace: hops injects values.namespace; helm --namespace is Release.Namespace. */}} +{{- define "e2e-ui-ui.releaseNamespace" -}} +{{- coalesce .Values.namespace .Release.Namespace "default" -}} {{- end -}} {{/* Worktree-scoped OIDC app name prefix: e2e-ui- */}} @@ -51,12 +49,15 @@ hops.ops.com.ai/app: e2e-ui hops.ops.com.ai/identity-scope: cluster {{- end -}} -{{/* UI public base for OIDC redirects / Login V2 baseUri */}} +{{/* +UI public base for OIDC redirects + app Login V2 baseUri. +Uses the release/workspace namespace so --name dogfood → e2e-ui-ui.dogfood.svc… +*/}} {{- define "e2e-ui-ui.uiBaseURL" -}} {{- if .Values.identity.uiBaseURL -}} {{- .Values.identity.uiBaseURL | trimSuffix "/" -}} {{- else -}} -{{- $ns := default "default" .Values.namespace -}} +{{- $ns := include "e2e-ui-ui.releaseNamespace" . -}} {{- $svc := default (include "e2e-ui-ui.name" .) .Values.identity.uiService -}} {{- $port := default .Values.service.port .Values.identity.uiPort -}} {{- printf "http://%s.%s.svc.cluster.local:%v" $svc $ns $port -}} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-human-passwords.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-human-passwords.yaml index f895ca61..3d4dd4b4 100644 --- a/tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-human-passwords.yaml +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/external-secret-human-passwords.yaml @@ -11,7 +11,6 @@ metadata: labels: {{- include "e2e-ui-ui.labels" . | nindent 4 }} hops.ops.com.ai/app: e2e-ui - hops.ops.com.ai/workspace: {{ $ws | quote }} hops.ops.com.ai/secret: human-passwords spec: refreshInterval: {{ .Values.externalSecrets.refreshInterval | default "1m" | quote }} @@ -26,7 +25,6 @@ spec: metadata: labels: hops.ops.com.ai/app: e2e-ui - hops.ops.com.ai/workspace: {{ $ws | quote }} data: - secretKey: alice remoteRef: diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/identity-instance-features.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-instance-features.yaml new file mode 100644 index 00000000..2b7f8833 --- /dev/null +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-instance-features.yaml @@ -0,0 +1,29 @@ +{{- /* + Instance Login V2 features (gitops). + + Zitadel honors instance loginV2.baseUri when required=true. That is what + redirects authorize → custom e2e-ui /login (not stock zitadel-login). + + baseUri tracks the UI release namespace (hops --name / helm --namespace). + Note: Features is instance-global in Zitadel — last applied worktree wins if + several UIs enable this on one AuthStack. +*/ -}} +{{- if and .Values.identity.enabled (ne .Values.identity.instanceLoginV2 false) }} +{{- $uiBase := include "e2e-ui-ui.uiBaseURL" . -}} +{{- $ns := include "e2e-ui-ui.releaseNamespace" . -}} +apiVersion: instance.zitadel.m.crossplane.io/v1alpha1 +kind: Features +metadata: + name: e2e-ui-login-v2 + namespace: {{ $ns }} + labels: + {{- include "e2e-ui-ui.identityLabels" . | nindent 4 }} +spec: + forProvider: + loginV2: + - required: true + baseUri: {{ $uiBase | quote }} + providerConfigRef: + name: {{ .Values.identity.providerConfigRef.name | default "default" | quote }} + kind: {{ .Values.identity.providerConfigRef.kind | default "ProviderConfig" | quote }} +{{- end }} diff --git a/tests/e2e-ui/ui/.gitops/deploy/templates/identity-oidc.yaml b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-oidc.yaml index 80214de3..3f52fca6 100644 --- a/tests/e2e-ui/ui/.gitops/deploy/templates/identity-oidc.yaml +++ b/tests/e2e-ui/ui/.gitops/deploy/templates/identity-oidc.yaml @@ -7,7 +7,8 @@ {{- $prefix := include "e2e-ui-ui.identityPrefix" . -}} {{- $project := include "e2e-ui-ui.clusterProjectName" . -}} {{- $uiBase := include "e2e-ui-ui.uiBaseURL" . -}} -{{- $ns := .Values.identity.mrNamespace | default "default" -}} +{{/* Worktree OIDC app lives with the UI release (namespace = hops --name). */}} +{{- $ns := .Values.identity.mrNamespace | default (include "e2e-ui-ui.releaseNamespace" .) -}} apiVersion: application.zitadel.m.crossplane.io/v1alpha1 kind: Oidc metadata: diff --git a/tests/e2e-ui/ui/.gitops/deploy/values.yaml b/tests/e2e-ui/ui/.gitops/deploy/values.yaml index a0184ca3..f6576dc3 100644 --- a/tests/e2e-ui/ui/.gitops/deploy/values.yaml +++ b/tests/e2e-ui/ui/.gitops/deploy/values.yaml @@ -44,8 +44,8 @@ env: E2E_API_ORIGIN: "http://e2e-ui-api:8791" E2E_BASE_URL: "http://e2e-ui-api:8791" PUBLIC_E2E_API_ORIGIN: "http://e2e-ui-api:8791" - # Overridden at render when hops injects namespace (hops-wt-*). - AUTH_URL: "http://e2e-ui-ui.hops-wt-default.svc.cluster.local:5180" + # Overridden at render when hops injects namespace (= --name). + AUTH_URL: "http://e2e-ui-ui.default.svc.cluster.local:5180" AUTH_TRUST_HOST: "true" AUTH_USE_SECURE_COOKIES: "false" # AuthStack issuer (Helm default Service fullname) @@ -55,14 +55,12 @@ env: ZITADEL_PROJECT_ID: "" # Secret-backed env (keys must exist on Secret e2e-ui-oidc in the app namespace). -# Residual — create live (not in git): -# kubectl -n hops-wt-dogfood create secret generic e2e-ui-oidc \ +# Create live (or ExternalSecrets) — client id/secret from the worktree OIDC app: +# kubectl -n create secret generic e2e-ui-oidc \ # --from-literal=AUTH_SECRET='…' \ # --from-literal=OIDC_CLIENT_ID='…' \ # --from-literal=OIDC_CLIENT_SECRET='…' \ # --from-literal=ZITADEL_SERVICE_USER_TOKEN="$(kubectl -n auth get secret login-client -o jsonpath='{.data.pat}' | base64 -d)" -# Also set instance Login V2 (once per AuthStack): -# PUT /v2/features/instance {"loginV2":{"required":true,"baseUri":"http://e2e-ui-ui.hops-wt-dogfood.svc.cluster.local:5180"}} secretEnv: AUTH_SECRET: secretName: e2e-ui-oidc @@ -83,28 +81,33 @@ envFromSecrets: [] # Zitadel identity on the UI chart (OIDC web client + Login V2 host). # -# Scope: -# - Project + roles + demo humans — **cluster-shared** (one set for the CP) -# - OIDC web app — **per worktree** (redirect URIs + Login V2 baseUri) +# Scope (all gitops when identity.enabled): +# - Project + roles + demo humans — cluster-shared (projectNamespace) +# - OIDC web app — per worktree (redirects + app loginV2.baseUri) +# - Instance Features loginV2 — points authorize → this UI's /login +# (instance-global in Zitadel; last applied worktree wins on one AuthStack) # # Enable from Application values when the env needs OIDC dogfood. identity: enabled: false - # Residual after AuthStack Ready: GET …/management/v1/orgs/me + # After AuthStack Ready: GET …/management/v1/orgs/me orgId: "" # Worktree id for OIDC app names only (e2e-ui--web). Default: - # trimPrefix "hops-wt-" of injected namespace. + # release namespace (= hops --name / helm --namespace). workspace: "" # Cluster-shared Project MR name (roles + humans hang off this). projectName: e2e-ui # Cluster-shared demo humans (alice / bob / admin · Password1!) demoUsers: true - # Namespace for worktree OIDC app MRs. - mrNamespace: default + # Namespace for worktree OIDC app MR. Empty → release namespace. + mrNamespace: "" # Namespace for shared Project / Role MRs (defaults to mrNamespace). projectNamespace: default # Namespace for shared HumanUser MRs (defaults to projectNamespace). humansNamespace: default + # Gitops Features MR: required Login V2 → uiBaseURL (this chart's /login). + # Set false only if something else owns instance Features. + instanceLoginV2: true # Optional overrides; defaults = this chart name + service.port uiService: "" uiPort: ""