Wire bp-openclaw to the per-tenant Keycloak realm (OIDC SSO) and the per-tenant NewAPI (OpenAI-compatible LLM endpoint, NOT direct OpenAI), delivering C3 of umbrella epic #915. Chart changes (bp-openclaw 0.1.0 → 0.2.0): - Add canonical `oidc.{issuerURL,clientId,clientSecret.{name,key}}` block. - Add canonical `llm.{baseURL,apiKey.{name,key},defaultModel}` block. - Controller Deployment now emits OIDC_*, LLM_*, OPENAI_API_{BASE,KEY}, LLM_DEFAULT_MODEL envs (legacy KEYCLOAK_*/NEWAPI_BASE_URL_DEFAULT retained for back-compat with current controller image). - Per-user pods carry OPENAI_API_BASE / OPENAI_API_KEY / LLM_DEFAULT_MODEL alongside the identity-blind NEWAPI_BASE_URL / NEWAPI_KEY (ADR-0003 §3.3 unchanged). - Legacy `keycloak.*` / `newapi.*` keys remain accepted as fallbacks; helpers prefer canonical blocks but fall back to the legacy alias when the canonical block is unset (or still at placeholder). - assertNoPlaceholders guard updated to check resolved canonical values. - render-toggles.sh smoke test extended: asserts both canonical and legacy code-paths render and that all expected envs reach the rendered Deployment. Orchestrator changes (catalyst-api smeTenantBPOpenClaw template): - Emit per-tenant `oidc.issuerURL` = https://keycloak.<sub>.<parent>/realms/sme-<sub> - Emit per-tenant `oidc.clientId` = openclaw, secret from openclaw-oidc-client-secret/OIDC_CLIENT_SECRET (rendered by bp-keycloak's post-install hook). - Emit per-tenant `llm.baseURL` = https://api.<sub>.<parent>/v1 (alice's own NewAPI ingress, NOT the otech-wide newapi.<otech-fqdn>); apiKey from openclaw-newapi-controller-token/NEWAPI_KEY. - Emit `llm.defaultModel: qwen3.6` — NewAPI uses this to select the backing channel; C4 of #915 wires Qwen3.6@BankDhofar at tenant-create. - Legacy keycloak/newapi blocks still emitted for back-compat with bp-openclaw < 0.2.0. Tests: - New TestRenderSMETenantOverlay_OpenClawOIDCAndLLMBlocks asserts the rendered HelmRelease contains the canonical oidc + llm blocks with per-tenant values, and that llm.baseURL is the per-tenant api.<sub>.<parent>/v1 (NOT the otech-wide newapi). - bp-openclaw render-toggles.sh extended (Case 2b/2c). Co-authored-by: alierenbaysal <alierenbaysal@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dcf6cf70b4
commit
61c8d77b58
@ -73,13 +73,19 @@ The chart fails to render if any of these are unset (see
|
||||
|
||||
| Value | Example |
|
||||
|---|---|
|
||||
| `keycloak.realmURL` | `https://keycloak.acme.<otech-fqdn>/realms/acme` |
|
||||
| `keycloak.clientSecretName` | `openclaw-oidc` (ExternalSecret with key `OIDC_CLIENT_SECRET`) |
|
||||
| `oidc.issuerURL` | `https://keycloak.acme.<parent-domain>/realms/sme-acme` |
|
||||
| `oidc.clientId` | `openclaw` |
|
||||
| `oidc.clientSecret.name` | `openclaw-oidc-client-secret` (Secret with key `OIDC_CLIENT_SECRET`) |
|
||||
| `llm.baseURL` | `https://api.acme.<parent-domain>/v1` (per-tenant NewAPI OpenAI-compatible endpoint) |
|
||||
| `llm.apiKey.name` | `openclaw-newapi-controller-token` (Secret with key `NEWAPI_KEY`) |
|
||||
| `llm.defaultModel` | `qwen3.6` (NewAPI maps this to a backing channel — e.g. Qwen3.6@BankDhofar) |
|
||||
| `tenant.namespace` | `sme-acme` |
|
||||
| `newapi.baseURL` | `https://newapi.<otech-fqdn>` |
|
||||
| `controller.image.tag` | SHA-pinned tag (Inviolable Principle 4) |
|
||||
| `perUserPod.image.tag` | SHA-pinned tag (Inviolable Principle 4) |
|
||||
| `ingress.host` | `openclaw.acme.<otech-fqdn>` |
|
||||
| `ingress.host` | `openclaw.acme.<parent-domain>` |
|
||||
|
||||
Legacy `keycloak.*` / `newapi.*` keys remain accepted for back-compat
|
||||
(see umbrella epic #915).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
apiVersion: v2
|
||||
name: bp-openclaw
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
version: 0.2.0
|
||||
appVersion: "0.2.0"
|
||||
description: |
|
||||
Catalyst Blueprint chart for the OpenClaw workspace controller pattern
|
||||
— a multi-tenant controller deployment in an SME tenant namespace plus
|
||||
@ -11,10 +11,11 @@ description: |
|
||||
|
||||
Deployment shape:
|
||||
- One controller Deployment per SME tenant namespace. Authenticates
|
||||
end users via the SME-vcluster Keycloak realm (OIDC) and validates
|
||||
end users via the per-tenant Keycloak realm (OIDC) and validates
|
||||
the JWT signature + sub claim before spawning a per-user pod.
|
||||
- Per-user pods carry only two env vars (NEWAPI_BASE_URL, NEWAPI_KEY)
|
||||
mounted from the per-user Secret labelled
|
||||
- Per-user pods carry NEWAPI_BASE_URL + NEWAPI_KEY (plus their
|
||||
OpenAI-compatible aliases OPENAI_API_BASE / OPENAI_API_KEY +
|
||||
LLM_DEFAULT_MODEL) mounted from the per-user Secret labelled
|
||||
`catalyst.openova.io/sme-user-uuid=<uuid>`. The runtime image is
|
||||
identity-blind: no Keycloak code, no key-management code.
|
||||
|
||||
@ -25,8 +26,24 @@ description: |
|
||||
SHA in the consuming overlay (per Inviolable Principle 4 — never use
|
||||
floating tags in production manifests).
|
||||
|
||||
Pairs with bp-newapi (Sovereign-level metered LLM gateway), bp-keycloak
|
||||
(SME-vcluster realm), bp-cert-manager (per-host TLS).
|
||||
Pairs with bp-newapi (per-tenant OpenAI-compatible LLM gateway),
|
||||
bp-keycloak (per-tenant realm), bp-cert-manager (per-host TLS).
|
||||
|
||||
Changelog
|
||||
─────────
|
||||
0.2.0 (2026-05-05, umbrella #915)
|
||||
- Add canonical `oidc.{issuerURL,clientId,clientSecret}` block
|
||||
(per-tenant Keycloak SSO).
|
||||
- Add canonical `llm.{baseURL,apiKey,defaultModel}` block
|
||||
(per-tenant NewAPI as OpenAI-compatible LLM gateway).
|
||||
- Controller now emits OIDC_*, LLM_*, OPENAI_API_{BASE,KEY},
|
||||
LLM_DEFAULT_MODEL envs (legacy KEYCLOAK_*/NEWAPI_BASE_URL_DEFAULT
|
||||
retained for back-compat).
|
||||
- Per-user pods now also carry OPENAI_API_BASE / OPENAI_API_KEY /
|
||||
LLM_DEFAULT_MODEL.
|
||||
- Legacy `keycloak.*` / `newapi.*` keys remain as fallbacks.
|
||||
0.1.0 (2026-04, epic #795)
|
||||
- Initial chart: controller Deployment + per-user pod ConfigMap.
|
||||
|
||||
type: application
|
||||
keywords: [catalyst, blueprint, openclaw, sme, workspace-controller, per-user-pod, llm, agent]
|
||||
|
||||
@ -71,6 +71,98 @@ matches the operator's intent.
|
||||
{{- default .Release.Namespace .Values.tenant.namespace }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
─── OIDC + LLM resolution helpers (umbrella #915) ─────────────────────
|
||||
|
||||
The chart's canonical config blocks are `oidc.*` and `llm.*`; legacy
|
||||
overlays may still set `keycloak.*` / `newapi.*`. Helpers prefer the
|
||||
canonical value and fall back to the legacy alias when unset.
|
||||
*/}}
|
||||
{{/*
|
||||
Resolution rule: when an overlay sets a legacy key (`keycloak.*` /
|
||||
`newapi.*`) AND leaves the canonical block at its placeholder default,
|
||||
the legacy key wins (back-compat). When the canonical block is
|
||||
explicitly set to a non-placeholder value, it always wins.
|
||||
*/}}
|
||||
{{- define "bp-openclaw.oidc.issuerURL" -}}
|
||||
{{- $oidc := .Values.oidc.issuerURL | default "" -}}
|
||||
{{- $legacy := .Values.keycloak.realmURL | default "" -}}
|
||||
{{- if and $legacy (or (eq $oidc "") (eq $oidc "https://keycloak.example.local/realms/example")) -}}
|
||||
{{- $legacy -}}
|
||||
{{- else if $oidc -}}
|
||||
{{- $oidc -}}
|
||||
{{- else -}}
|
||||
{{- "https://keycloak.example.local/realms/example" -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "bp-openclaw.oidc.clientId" -}}
|
||||
{{- $oidc := .Values.oidc.clientId | default "" -}}
|
||||
{{- $legacy := .Values.keycloak.clientID | default "" -}}
|
||||
{{- if and $legacy (or (eq $oidc "") (eq $oidc "openclaw")) -}}
|
||||
{{- $legacy -}}
|
||||
{{- else if $oidc -}}
|
||||
{{- $oidc -}}
|
||||
{{- else -}}
|
||||
{{- "openclaw" -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "bp-openclaw.oidc.clientSecretName" -}}
|
||||
{{- $oidc := "" -}}
|
||||
{{- if .Values.oidc.clientSecret -}}
|
||||
{{- $oidc = .Values.oidc.clientSecret.name | default "" -}}
|
||||
{{- end -}}
|
||||
{{- $legacy := .Values.keycloak.clientSecretName | default "" -}}
|
||||
{{- if and $legacy (or (eq $oidc "") (eq $oidc "openclaw-oidc")) -}}
|
||||
{{- $legacy -}}
|
||||
{{- else if $oidc -}}
|
||||
{{- $oidc -}}
|
||||
{{- else -}}
|
||||
{{- "openclaw-oidc" -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "bp-openclaw.oidc.clientSecretKey" -}}
|
||||
{{- if and .Values.oidc.clientSecret .Values.oidc.clientSecret.key -}}
|
||||
{{- .Values.oidc.clientSecret.key -}}
|
||||
{{- else -}}
|
||||
{{- "OIDC_CLIENT_SECRET" -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "bp-openclaw.llm.baseURL" -}}
|
||||
{{- $llm := .Values.llm.baseURL | default "" -}}
|
||||
{{- $legacy := .Values.newapi.baseURL | default "" -}}
|
||||
{{- if and $legacy (or (eq $llm "") (eq $llm "https://newapi.example.local/v1")) -}}
|
||||
{{- $legacy -}}
|
||||
{{- else if $llm -}}
|
||||
{{- $llm -}}
|
||||
{{- else -}}
|
||||
{{- "https://newapi.example.local/v1" -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "bp-openclaw.llm.apiKeySecretName" -}}
|
||||
{{- if and .Values.llm.apiKey .Values.llm.apiKey.name -}}
|
||||
{{- .Values.llm.apiKey.name -}}
|
||||
{{- else -}}
|
||||
{{- "openclaw-llm-apikey" -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "bp-openclaw.llm.apiKeySecretKey" -}}
|
||||
{{- if and .Values.llm.apiKey .Values.llm.apiKey.key -}}
|
||||
{{- .Values.llm.apiKey.key -}}
|
||||
{{- else -}}
|
||||
{{- "NEWAPI_KEY" -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "bp-openclaw.llm.defaultModel" -}}
|
||||
{{- default "qwen3.6" .Values.llm.defaultModel -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Placeholder-rejection assertions. The chart ships placeholder defaults
|
||||
(see values.yaml) so `helm template` smoke-renders cleanly in CI
|
||||
@ -94,11 +186,11 @@ HelmRelease.
|
||||
{{- if eq .Values.perUserPod.image.tag "0.1.0-placeholder" }}
|
||||
{{- fail "perUserPod.image.tag is still the placeholder — overlay must supply a SHA-pinned tag (Inviolable Principle 4)" }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.keycloak.realmURL "https://keycloak.example.local/realms/example" }}
|
||||
{{- fail "keycloak.realmURL is still the placeholder — overlay must supply the SME-vcluster Keycloak realm URL" }}
|
||||
{{- if eq (include "bp-openclaw.oidc.issuerURL" .) "https://keycloak.example.local/realms/example" }}
|
||||
{{- fail "oidc.issuerURL is still the placeholder — overlay must supply the per-tenant Keycloak realm URL" }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.newapi.baseURL "https://newapi.example.local" }}
|
||||
{{- fail "newapi.baseURL is still the placeholder — overlay must supply the NewAPI customer-facing hostname" }}
|
||||
{{- if eq (include "bp-openclaw.llm.baseURL" .) "https://newapi.example.local/v1" }}
|
||||
{{- fail "llm.baseURL is still the placeholder — overlay must supply the per-tenant NewAPI OpenAI-compatible endpoint" }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.tenant.namespace "sme-example" }}
|
||||
{{- fail "tenant.namespace is still the placeholder — overlay must supply the SME tenant namespace" }}
|
||||
|
||||
@ -34,28 +34,69 @@ spec:
|
||||
containerPort: {{ .Values.controller.port }}
|
||||
protocol: TCP
|
||||
env:
|
||||
# ── Identity: SME-vcluster Keycloak realm ─────────────────
|
||||
# ── Identity: per-tenant Keycloak (OIDC) ──────────────────
|
||||
# Canonical OIDC_* envs (umbrella #915). Legacy KEYCLOAK_*
|
||||
# envs are emitted alongside for back-compat with controller
|
||||
# builds that haven't yet been recompiled to the new names.
|
||||
- name: OIDC_ISSUER_URL
|
||||
value: {{ include "bp-openclaw.oidc.issuerURL" . | quote }}
|
||||
- name: OIDC_CLIENT_ID
|
||||
value: {{ include "bp-openclaw.oidc.clientId" . | quote }}
|
||||
- name: OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "bp-openclaw.oidc.clientSecretName" . }}
|
||||
key: {{ include "bp-openclaw.oidc.clientSecretKey" . }}
|
||||
- name: KEYCLOAK_REALM_URL
|
||||
value: {{ .Values.keycloak.realmURL | quote }}
|
||||
value: {{ include "bp-openclaw.oidc.issuerURL" . | quote }}
|
||||
- name: KEYCLOAK_CLIENT_ID
|
||||
value: {{ .Values.keycloak.clientID | quote }}
|
||||
value: {{ include "bp-openclaw.oidc.clientId" . | quote }}
|
||||
- name: KEYCLOAK_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.keycloak.clientSecretName }}
|
||||
key: OIDC_CLIENT_SECRET
|
||||
name: {{ include "bp-openclaw.oidc.clientSecretName" . }}
|
||||
key: {{ include "bp-openclaw.oidc.clientSecretKey" . }}
|
||||
|
||||
# ── Tenant placement for per-user pods ────────────────────
|
||||
- name: TENANT_NAMESPACE
|
||||
value: {{ include "bp-openclaw.tenantNamespace" . | quote }}
|
||||
|
||||
# ── LLM gateway: per-tenant NewAPI (OpenAI-compatible) ────
|
||||
# The controller knows the tenant's NewAPI /v1 endpoint and
|
||||
# default model so it can run /readyz probes and any
|
||||
# controller-side LLM call that pre-dates a user session.
|
||||
# Per-user pods continue to read NEWAPI_BASE_URL/NEWAPI_KEY
|
||||
# from the per-user `newapi-key-{uuid}` Secret per ADR-0003
|
||||
# §3.3 — the controller does NOT inject its own service
|
||||
# token into the user-facing pod.
|
||||
- name: LLM_BASE_URL
|
||||
value: {{ include "bp-openclaw.llm.baseURL" . | quote }}
|
||||
- name: LLM_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "bp-openclaw.llm.apiKeySecretName" . }}
|
||||
key: {{ include "bp-openclaw.llm.apiKeySecretKey" . }}
|
||||
optional: true
|
||||
- name: LLM_DEFAULT_MODEL
|
||||
value: {{ include "bp-openclaw.llm.defaultModel" . | quote }}
|
||||
# OpenAI-SDK-compatible aliases (drop-in for any controller
|
||||
# subroutine that uses an off-the-shelf OpenAI client).
|
||||
- name: OPENAI_API_BASE
|
||||
value: {{ include "bp-openclaw.llm.baseURL" . | quote }}
|
||||
- name: OPENAI_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "bp-openclaw.llm.apiKeySecretName" . }}
|
||||
key: {{ include "bp-openclaw.llm.apiKeySecretKey" . }}
|
||||
optional: true
|
||||
|
||||
# ── NewAPI customer-facing base URL (fallback default) ───
|
||||
# Authoritative value per ADR-0003 §3.3 lives on each
|
||||
# `newapi-key-{uuid}` Secret as `base-url`. The controller
|
||||
# reads that field; this env is the chart-render-time
|
||||
# fallback for overlays that haven't yet been migrated.
|
||||
- name: NEWAPI_BASE_URL_DEFAULT
|
||||
value: {{ .Values.newapi.baseURL | quote }}
|
||||
value: {{ include "bp-openclaw.llm.baseURL" . | quote }}
|
||||
|
||||
# ── Per-user pod template ConfigMap reference ─────────────
|
||||
- name: POD_TEMPLATE_CONFIGMAP
|
||||
|
||||
@ -47,6 +47,8 @@ data:
|
||||
image: "{{ .Values.perUserPod.image.repository }}:{{ .Values.perUserPod.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.perUserPod.image.pullPolicy }}
|
||||
env:
|
||||
# NewAPI gateway — per-user Secret (ADR-0003 §3.3). The
|
||||
# runtime is identity-blind: it reads only these env vars.
|
||||
- name: NEWAPI_BASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@ -57,6 +59,24 @@ data:
|
||||
secretKeyRef:
|
||||
name: ${SECRET_NAME}
|
||||
key: api-key
|
||||
# OpenAI-compatible aliases (any pre-built OpenAI SDK in the
|
||||
# runtime image picks these up without code changes).
|
||||
- name: OPENAI_API_BASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ${SECRET_NAME}
|
||||
key: base-url
|
||||
- name: OPENAI_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ${SECRET_NAME}
|
||||
key: api-key
|
||||
# Default model identifier sent by the runtime when the
|
||||
# client doesn't override. NewAPI uses the model name to
|
||||
# select a channel; for #915 DoD this is "qwen3.6" and C4
|
||||
# wires the BankDhofar channel routing.
|
||||
- name: LLM_DEFAULT_MODEL
|
||||
value: {{ include "bp-openclaw.llm.defaultModel" . | quote }}
|
||||
{{- range .Values.perUserPod.extraEnv }}
|
||||
- {{- toYaml . | nindent 14 }}
|
||||
{{- end }}
|
||||
|
||||
@ -49,13 +49,15 @@ if ! grep -q "placeholder" "$TMP/assert.err"; then
|
||||
fi
|
||||
echo " PASS"
|
||||
|
||||
echo "[render-toggles] Case 2b: assertNoPlaceholders=true with all real values renders successfully"
|
||||
echo "[render-toggles] Case 2b: assertNoPlaceholders=true with all real values (canonical oidc/llm blocks) renders successfully"
|
||||
if ! helm template smoke-openclaw . \
|
||||
--set "assertNoPlaceholders=true" \
|
||||
--set "keycloak.realmURL=https://kc.acme.example/realms/acme" \
|
||||
--set "keycloak.clientSecretName=openclaw-oidc" \
|
||||
--set "oidc.issuerURL=https://kc.acme.example/realms/sme-acme" \
|
||||
--set "oidc.clientSecret.name=openclaw-oidc-client-secret" \
|
||||
--set "llm.baseURL=https://api.acme.example/v1" \
|
||||
--set "llm.apiKey.name=openclaw-newapi-controller-token" \
|
||||
--set "llm.defaultModel=qwen3.6" \
|
||||
--set "tenant.namespace=sme-acme" \
|
||||
--set "newapi.baseURL=https://newapi.example" \
|
||||
--set "controller.image.tag=sha-deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" \
|
||||
--set "perUserPod.image.tag=sha-cafef00dcafef00dcafef00dcafef00dcafef00d" \
|
||||
--set "ingress.host=openclaw.acme.example" \
|
||||
@ -64,6 +66,61 @@ if ! helm template smoke-openclaw . \
|
||||
cat "$TMP/real.err" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Assert canonical envs are present on the controller.
|
||||
for env in OIDC_ISSUER_URL OIDC_CLIENT_ID OIDC_CLIENT_SECRET \
|
||||
LLM_BASE_URL LLM_API_KEY LLM_DEFAULT_MODEL \
|
||||
OPENAI_API_BASE OPENAI_API_KEY \
|
||||
KEYCLOAK_REALM_URL NEWAPI_BASE_URL_DEFAULT; do
|
||||
if ! grep -q "name: ${env}" "$TMP/real.yaml"; then
|
||||
echo "FAIL: controller env ${env} missing from rendered manifests" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# Assert the per-user pod-template ConfigMap carries OPENAI_API_BASE +
|
||||
# LLM_DEFAULT_MODEL so OpenAI-SDK-based runtimes work without code change.
|
||||
if ! grep -q "OPENAI_API_BASE" "$TMP/real.yaml"; then
|
||||
echo "FAIL: per-user pod template missing OPENAI_API_BASE env" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q "LLM_DEFAULT_MODEL" "$TMP/real.yaml"; then
|
||||
echo "FAIL: per-user pod template missing LLM_DEFAULT_MODEL env" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Assert OIDC issuer + LLM baseURL values reach the rendered Deployment
|
||||
# verbatim from --set.
|
||||
if ! grep -q "https://kc.acme.example/realms/sme-acme" "$TMP/real.yaml"; then
|
||||
echo "FAIL: OIDC issuer URL not propagated to rendered Deployment" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q "https://api.acme.example/v1" "$TMP/real.yaml"; then
|
||||
echo "FAIL: LLM baseURL not propagated to rendered Deployment" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " PASS"
|
||||
|
||||
echo "[render-toggles] Case 2c: legacy keycloak.* / newapi.* keys still work as fallbacks"
|
||||
if ! helm template smoke-openclaw . \
|
||||
--set "assertNoPlaceholders=true" \
|
||||
--set "keycloak.realmURL=https://kc.legacy.example/realms/legacy" \
|
||||
--set "keycloak.clientSecretName=openclaw-oidc-legacy" \
|
||||
--set "newapi.baseURL=https://newapi.legacy.example/v1" \
|
||||
--set "tenant.namespace=sme-legacy" \
|
||||
--set "controller.image.tag=sha-deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" \
|
||||
--set "perUserPod.image.tag=sha-cafef00dcafef00dcafef00dcafef00dcafef00d" \
|
||||
--set "ingress.host=openclaw.legacy.example" \
|
||||
> "$TMP/legacy.yaml" 2> "$TMP/legacy.err"; then
|
||||
echo "FAIL: legacy-key render failed:" >&2
|
||||
cat "$TMP/legacy.err" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q "https://kc.legacy.example/realms/legacy" "$TMP/legacy.yaml"; then
|
||||
echo "FAIL: legacy keycloak.realmURL did not reach rendered Deployment" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q "https://newapi.legacy.example/v1" "$TMP/legacy.yaml"; then
|
||||
echo "FAIL: legacy newapi.baseURL did not reach rendered Deployment" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " PASS"
|
||||
|
||||
echo "[render-toggles] Case 3: RBAC — 'create' verb is NOT combined with resourceNames"
|
||||
|
||||
@ -6,14 +6,20 @@
|
||||
# rebuilding the Blueprint OCI artifact.
|
||||
#
|
||||
# Required at install time (overlay MUST set):
|
||||
# - keycloak.realmURL (SME-vcluster Keycloak realm OIDC issuer)
|
||||
# - keycloak.clientSecretName (ExternalSecret with OIDC client_secret)
|
||||
# - oidc.issuerURL (per-tenant Keycloak realm OIDC issuer)
|
||||
# - oidc.clientSecret.name (ExternalSecret with OIDC client_secret)
|
||||
# - llm.baseURL (per-tenant NewAPI OpenAI-compatible endpoint)
|
||||
# - llm.apiKey.name (Secret carrying NewAPI bearer token)
|
||||
# - tenant.namespace (SME tenant namespace where per-user pods run)
|
||||
# - ingress.host (controller public hostname)
|
||||
# - newapi.baseURL (NewAPI customer-facing hostname)
|
||||
# - controller.image.tag (SHA-pinned tag)
|
||||
# - perUserPod.image.tag (SHA-pinned tag)
|
||||
#
|
||||
# Legacy keys `keycloak.*` and `newapi.*` remain accepted for back-compat
|
||||
# (they fall back when the canonical `oidc.*` / `llm.*` block omits a
|
||||
# field); new overlays SHOULD use the canonical blocks. See umbrella
|
||||
# epic #915 (per-tenant SSO + per-tenant NewAPI gateway DoD).
|
||||
#
|
||||
# Placeholder defaults below let `helm template` smoke-render cleanly in
|
||||
# CI without --set (mirroring the bp-self-sovereign-cutover pattern).
|
||||
# Runtime use with the placeholder values is non-functional by design;
|
||||
@ -80,32 +86,79 @@ controller:
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
# ─── Auth: SME-vcluster Keycloak (OIDC) ──────────────────────────────────
|
||||
# ─── Auth: per-tenant Keycloak (OIDC) — canonical block ─────────────────
|
||||
# The controller validates inbound end-user JWTs against the SME's own
|
||||
# Keycloak realm. It NEVER uses the OTECH-ops realm, NEVER trusts a JWT
|
||||
# from outside the SME vcluster.
|
||||
keycloak:
|
||||
# Required at install time: full realm issuer URL.
|
||||
# e.g. "https://keycloak.<sme-domain>/realms/<sme-realm>"
|
||||
# Placeholder default lets `helm template` smoke-render pass.
|
||||
realmURL: "https://keycloak.example.local/realms/example"
|
||||
# OIDC client registered in the SME's Keycloak realm for OpenClaw.
|
||||
clientID: "openclaw"
|
||||
# ExternalSecret name carrying the OIDC client_secret. Required key:
|
||||
#
|
||||
# Per umbrella epic #915 the orchestrator (catalyst-api smeTenantBPOpenClaw
|
||||
# template) emits this block per-tenant: `oidc.issuerURL` resolves to
|
||||
# https://keycloak.<tenant-domain>/realms/<tenant>, `oidc.clientId` to
|
||||
# `openclaw`, and `oidc.clientSecret.name` to the per-tenant Keycloak
|
||||
# client-secret (Secret rendered by bp-keycloak post-install).
|
||||
oidc:
|
||||
# Required at install time: full realm issuer URL (the OIDC `iss` claim
|
||||
# the controller will validate JWTs against). Placeholder default lets
|
||||
# `helm template` smoke-render pass.
|
||||
issuerURL: "https://keycloak.example.local/realms/example"
|
||||
# OIDC client registered in the per-tenant Keycloak realm.
|
||||
clientId: "openclaw"
|
||||
# OIDC client_secret. Loaded from a K8s Secret via secretKeyRef so the
|
||||
# plaintext NEVER lands in the HelmRelease values block (Inviolable
|
||||
# Principle 10 — credential hygiene). Required Secret key:
|
||||
# OIDC_CLIENT_SECRET. Provisioned by the SME-tenant onboarding pipeline
|
||||
# (epic #795 ticket #804). Placeholder default lets smoke render pass.
|
||||
clientSecretName: "openclaw-oidc"
|
||||
# (epic #795 ticket #804).
|
||||
clientSecret:
|
||||
# Secret name (defaults to legacy `keycloak.clientSecretName` if
|
||||
# unset — see _helpers.tpl).
|
||||
name: "openclaw-oidc"
|
||||
key: "OIDC_CLIENT_SECRET"
|
||||
|
||||
# ─── NewAPI integration ──────────────────────────────────────────────────
|
||||
# Per-user pods talk to NewAPI via its CUSTOMER-FACING hostname (egress
|
||||
# through the OTECH ingress with TLS), NEVER the in-cluster admin URL.
|
||||
# Per ADR-0003 §3.3 the per-user Secret carries `base-url` matching this
|
||||
# value; we read the value from the Secret at pod-spawn time so this
|
||||
# field acts as a fallback default for overlays that don't yet set
|
||||
# `base-url` on the Secret.
|
||||
# ─── LLM gateway: per-tenant NewAPI ─────────────────────────────────────
|
||||
# OpenClaw uses the per-tenant NewAPI as its OpenAI-compatible LLM
|
||||
# gateway (NOT direct OpenAI). The orchestrator points this at
|
||||
# https://api.<tenant-domain>/v1; NewAPI then routes to the configured
|
||||
# channel (e.g. Qwen3.6@BankDhofar — wired by C4 of #915).
|
||||
#
|
||||
# Per ADR-0003 §3.3 each end-user has a per-user Secret rendered by
|
||||
# the unified-rbac hook carrying `base-url` + `api-key`; per-user pods
|
||||
# read those at session-start. The chart-level `llm.*` block carries the
|
||||
# CONTROLLER-side defaults (used for /readyz probes, fallback when the
|
||||
# per-user Secret is missing the field, and for the OpenAI-compatible
|
||||
# `LLM_DEFAULT_MODEL` env that NewAPI maps to a channel).
|
||||
llm:
|
||||
# Required at install time. The customer-facing OpenAI-compatible
|
||||
# endpoint (i.e. NewAPI's /v1 surface). Placeholder default lets
|
||||
# smoke render pass.
|
||||
baseURL: "https://newapi.example.local/v1"
|
||||
# Bearer token sourced from a K8s Secret (Inviolable Principle 10).
|
||||
# Required Secret key: NEWAPI_KEY (or `api-key` for ADR-0003-aligned
|
||||
# per-user secrets). Per-user pods continue to read the per-user
|
||||
# `newapi-key-{uuid}` Secret per ADR-0003 §3.3; this controller-side
|
||||
# secret is the controller's own service-token (used for /readyz
|
||||
# probes and for any controller-side LLM call that pre-dates a user
|
||||
# session).
|
||||
apiKey:
|
||||
name: "openclaw-llm-apikey"
|
||||
key: "NEWAPI_KEY"
|
||||
# The default OpenAI-compatible model identifier sent on /v1/chat/
|
||||
# completions when the client doesn't override. NewAPI uses the
|
||||
# `model` field to select a channel; for the umbrella #915 DoD this
|
||||
# is "qwen3.6" — C4 wires the BankDhofar channel routing at
|
||||
# tenant-create time.
|
||||
defaultModel: "qwen3.6"
|
||||
|
||||
# ─── Legacy aliases (back-compat, deprecated) ───────────────────────────
|
||||
# Old overlays may still set `keycloak.*` / `newapi.*`. The chart's
|
||||
# helpers prefer `oidc.*` / `llm.*` and fall back to these when the
|
||||
# canonical block is unset (or still at placeholder). Removed once every
|
||||
# overlay has migrated.
|
||||
keycloak:
|
||||
realmURL: ""
|
||||
clientID: ""
|
||||
clientSecretName: ""
|
||||
newapi:
|
||||
# Required at install time. Placeholder default lets smoke render pass.
|
||||
baseURL: "https://newapi.example.local"
|
||||
baseURL: ""
|
||||
|
||||
# ─── Tenant namespace ────────────────────────────────────────────────────
|
||||
# SME tenant namespace where:
|
||||
|
||||
@ -837,8 +837,18 @@ spec:
|
||||
issuer: letsencrypt-prod
|
||||
`
|
||||
|
||||
const smeTenantBPOpenClaw = `# bp-openclaw (#803) — workspace controller pre-wired to the SME
|
||||
# Keycloak + the otech NewAPI gateway.
|
||||
const smeTenantBPOpenClaw = `# bp-openclaw (#803, #915) — workspace controller pre-wired to the
|
||||
# per-tenant Keycloak realm (SSO) and the per-tenant NewAPI gateway
|
||||
# (OpenAI-compatible LLM endpoint, NOT direct OpenAI).
|
||||
#
|
||||
# Per umbrella epic #915:
|
||||
# - oidc.issuerURL → per-tenant Keycloak realm (alice's users log in
|
||||
# to OpenClaw via alice's own Keycloak).
|
||||
# - llm.baseURL → per-tenant NewAPI /v1 (alice's OpenClaw chats
|
||||
# route through alice's NewAPI which proxies to the configured
|
||||
# channel — Qwen3.6@BankDhofar wired by C4).
|
||||
# - llm.defaultModel → "qwen3.6" placeholder; NewAPI maps the model
|
||||
# name to a channel.
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
@ -858,15 +868,36 @@ spec:
|
||||
- name: bp-keycloak
|
||||
namespace: {{.Namespace}}
|
||||
values:
|
||||
# ── OIDC (per-tenant Keycloak SSO) ─────────────────────────────────
|
||||
oidc:
|
||||
issuerURL: https://keycloak.{{.Subdomain}}.{{.ParentDomain}}/realms/sme-{{.Subdomain}}
|
||||
clientId: openclaw
|
||||
clientSecret:
|
||||
name: openclaw-oidc-client-secret
|
||||
key: OIDC_CLIENT_SECRET
|
||||
# ── LLM gateway (per-tenant NewAPI, OpenAI-compatible) ─────────────
|
||||
# newapi runs as a per-tenant HelmRelease (bp-newapi) — alice has
|
||||
# her own NewAPI at api.<sme-domain>; OpenClaw points its OpenAI
|
||||
# client there. The per-user newapi-key-{uuid} Secret carries the
|
||||
# end-user's bearer token (ADR-0003 §3.3); the controller-side
|
||||
# service token below is used for /readyz probes and any
|
||||
# controller-side LLM call that pre-dates a user session.
|
||||
llm:
|
||||
baseURL: https://api.{{.Subdomain}}.{{.ParentDomain}}/v1
|
||||
apiKey:
|
||||
name: openclaw-newapi-controller-token
|
||||
key: NEWAPI_KEY
|
||||
# NewAPI uses the model name to select a backing channel. C4
|
||||
# provisions channel #1 = Qwen3.6@BankDhofar at tenant-create
|
||||
# time so this default routes to the correct upstream.
|
||||
defaultModel: qwen3.6
|
||||
# ── Legacy aliases (back-compat with chart < 0.2.0) ────────────────
|
||||
keycloak:
|
||||
realmURL: https://keycloak.{{.Subdomain}}.{{.ParentDomain}}/realms/sme-{{.Subdomain}}
|
||||
clientID: openclaw
|
||||
clientSecretName: openclaw-oidc-client-secret
|
||||
newapi:
|
||||
# newapi runs on the otech (Sovereign) ingress regardless of the
|
||||
# SME's chosen parent zone — there's exactly one NewAPI per
|
||||
# Sovereign and it's anchored to OTECHFQDN.
|
||||
baseURL: https://newapi.{{.OTECHFQDN}}
|
||||
baseURL: https://api.{{.Subdomain}}.{{.ParentDomain}}/v1
|
||||
tenant:
|
||||
namespace: {{.Namespace}}
|
||||
ingress:
|
||||
|
||||
@ -525,6 +525,74 @@ func TestRenderSMETenantOverlay_NoVersionsDefaultsToStar(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRenderSMETenantOverlay_OpenClawOIDCAndLLMBlocks asserts that the
|
||||
// bp-openclaw HelmRelease emits the canonical oidc.{issuerURL,clientId,
|
||||
// clientSecret} + llm.{baseURL,apiKey,defaultModel} blocks per umbrella
|
||||
// epic openova-io/openova#915. These blocks pre-wire OpenClaw to:
|
||||
// - per-tenant Keycloak (alice's users log in via alice's Keycloak)
|
||||
// - per-tenant NewAPI as the OpenAI-compatible LLM gateway
|
||||
// (alice's OpenClaw chats route through alice's NewAPI which
|
||||
// proxies to the configured channel — Qwen3.6@BankDhofar wired
|
||||
// by C4 of #915).
|
||||
func TestRenderSMETenantOverlay_OpenClawOIDCAndLLMBlocks(t *testing.T) {
|
||||
rec := store.SMETenantProvisionRecord{
|
||||
SMETenantID: "t-alice",
|
||||
Subdomain: "alice",
|
||||
ParentDomain: "omantel.omani.works",
|
||||
DomainMode: store.SMEDomainFreeSubdomain,
|
||||
AdminEmail: "admin@alice.test",
|
||||
CompanyName: "Alice Corp",
|
||||
OTECHFQDN: "otech107.omani.works",
|
||||
VClusterName: "vc-alice",
|
||||
TenantNamespace: "sme-t-alice",
|
||||
}
|
||||
files, err := renderSMETenantOverlay(rec, SMETenantChartVersions{})
|
||||
if err != nil {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
body, ok := files["bp-openclaw.yaml"]
|
||||
if !ok {
|
||||
t.Fatalf("bp-openclaw.yaml missing from rendered overlay")
|
||||
}
|
||||
// OIDC block (canonical).
|
||||
wantOIDC := []string{
|
||||
" oidc:",
|
||||
" issuerURL: https://keycloak.alice.omantel.omani.works/realms/sme-alice",
|
||||
" clientId: openclaw",
|
||||
" clientSecret:",
|
||||
" name: openclaw-oidc-client-secret",
|
||||
" key: OIDC_CLIENT_SECRET",
|
||||
}
|
||||
for _, line := range wantOIDC {
|
||||
if !strings.Contains(body, line) {
|
||||
t.Errorf("bp-openclaw oidc block missing line %q\n--- rendered ---\n%s", line, body)
|
||||
}
|
||||
}
|
||||
// LLM block (canonical) — per-tenant NewAPI endpoint, NOT direct
|
||||
// OpenAI; defaultModel is the placeholder NewAPI maps to the
|
||||
// Qwen3.6@BankDhofar channel C4 wires at tenant-create time.
|
||||
wantLLM := []string{
|
||||
" llm:",
|
||||
" baseURL: https://api.alice.omantel.omani.works/v1",
|
||||
" apiKey:",
|
||||
" name: openclaw-newapi-controller-token",
|
||||
" key: NEWAPI_KEY",
|
||||
" defaultModel: qwen3.6",
|
||||
}
|
||||
for _, line := range wantLLM {
|
||||
if !strings.Contains(body, line) {
|
||||
t.Errorf("bp-openclaw llm block missing line %q\n--- rendered ---\n%s", line, body)
|
||||
}
|
||||
}
|
||||
// Per-tenant LLM endpoint MUST be the SME's own api.<sub>.<parent>,
|
||||
// NEVER the otech-wide newapi.<otech-fqdn> (that would route every
|
||||
// SME's traffic through one shared gateway, defeating per-tenant
|
||||
// channel routing).
|
||||
if strings.Contains(body, "https://newapi.otech107.omani.works") {
|
||||
t.Errorf("bp-openclaw llm.baseURL must be per-tenant api.<sub>.<parent>, not otech-wide newapi: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepsForState(t *testing.T) {
|
||||
cases := []struct {
|
||||
state store.SMETenantProvisionState
|
||||
|
||||
Loading…
Reference in New Issue
Block a user