openova/platform/openclaw/chart/templates/per-user-pod-template.yaml
e3mrah 61c8d77b58
feat(bp-openclaw): per-tenant Keycloak SSO + NewAPI as OpenAI-compatible LLM gateway (#915) (#917)
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>
2026-05-05 13:26:59 +04:00

98 lines
3.9 KiB
YAML

{{- if .Values.controller.enabled }}
{{- $tenantNs := include "bp-openclaw.tenantNamespace" . -}}
# ─── Per-user pod template (consumed by the controller) ──────────────────
# This ConfigMap holds the pod-spec template the controller renders per
# session. The controller reads it from /etc/openclaw/pod-template.yaml,
# substitutes ${USER_UUID} and ${SECRET_NAME}, and submits the resulting
# Pod to the K8s api-server.
#
# Substitution variables (filled by the controller at session-start):
# ${USER_UUID} — the SME user UUID (= JWT `sub` claim)
# ${SECRET_NAME} — `newapi-key-${USER_UUID}` (per ADR-0003 §3.3)
#
# Per [A] of #795: the runtime image reads ONLY two env vars
# (NEWAPI_BASE_URL + NEWAPI_KEY). It carries NO Keycloak code, NO key-
# management code, NO knowledge of the SME tenant model. Identity-blind
# by construction.
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "bp-openclaw.podTemplateConfigMapName" . }}
labels:
{{- include "bp-openclaw.labels" . | nindent 4 }}
data:
pod-template.yaml: |
apiVersion: v1
kind: Pod
metadata:
# The controller post-processes this name to be unique-per-session
# (e.g. openclaw-user-${USER_UUID}-${SESSION_ID}).
name: openclaw-user-${USER_UUID}
namespace: {{ $tenantNs }}
labels:
app.kubernetes.io/name: {{ include "bp-openclaw.name" . }}
app.kubernetes.io/component: per-user-pod
catalyst.openova.io/blueprint: bp-openclaw
catalyst.openova.io/openclaw-user: ${USER_UUID}
# Ownership label — the controller's RBAC scope (Role rules 2/3
# in controller-rbac.yaml) is enforced at the application layer
# by selecting only pods that carry this label.
spec:
restartPolicy: Never
automountServiceAccountToken: false
securityContext:
{{- toYaml .Values.perUserPod.securityContext | nindent 8 }}
containers:
- name: runtime
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:
name: ${SECRET_NAME}
key: base-url
- name: NEWAPI_KEY
valueFrom:
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 }}
resources:
{{- toYaml .Values.perUserPod.resources | nindent 12 }}
volumeMounts:
- name: workspace
mountPath: /workspace
- name: tmp
mountPath: /tmp
volumes:
- name: workspace
emptyDir:
sizeLimit: 1Gi
- name: tmp
emptyDir:
sizeLimit: 256Mi
{{- end }}