# services/keycloak/oneoffs/portal-e2e-target-client-job.yaml # One-off job for sso/keycloak-portal-e2e-target-7. # Purpose: keycloak portal e2e target 7 (see container args/env in this file). # Run by setting spec.suspend to false, reconcile, then set it back to true. # Safe to delete the finished Job/pod; it should not run continuously. apiVersion: batch/v1 kind: Job metadata: name: keycloak-portal-e2e-target-7 namespace: sso spec: suspend: true backoffLimit: 0 template: metadata: annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-pre-populate-only: "true" vault.hashicorp.com/role: "sso" vault.hashicorp.com/agent-inject-secret-keycloak-env.sh: "kv/data/atlas/shared/keycloak-admin" vault.hashicorp.com/agent-inject-template-keycloak-env.sh: | {{ with secret "kv/data/atlas/shared/keycloak-admin" }} export KEYCLOAK_ADMIN="{{ .Data.data.username }}" export KEYCLOAK_ADMIN_USER="{{ .Data.data.username }}" export KEYCLOAK_ADMIN_PASSWORD="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/sso/keycloak-db" }} export KC_DB_URL_DATABASE="{{ .Data.data.POSTGRES_DATABASE }}" export KC_DB_USERNAME="{{ .Data.data.POSTGRES_USER }}" export KC_DB_PASSWORD="{{ .Data.data.POSTGRES_PASSWORD }}" {{ end }} {{ with secret "kv/data/atlas/shared/portal-e2e-client" }} export PORTAL_E2E_CLIENT_ID="{{ .Data.data.client_id }}" export PORTAL_E2E_CLIENT_SECRET="{{ .Data.data.client_secret }}" {{ end }} {{ with secret "kv/data/atlas/sso/openldap-admin" }} export LDAP_ADMIN_PASSWORD="{{ .Data.data.LDAP_ADMIN_PASSWORD }}" export LDAP_CONFIG_PASSWORD="{{ .Data.data.LDAP_CONFIG_PASSWORD }}" export LDAP_BIND_PASSWORD="${LDAP_ADMIN_PASSWORD}" {{ end }} {{ with secret "kv/data/atlas/shared/postmark-relay" }} export KEYCLOAK_SMTP_USER="{{ index .Data.data "apikey" }}" export KEYCLOAK_SMTP_PASSWORD="{{ index .Data.data "apikey" }}" {{ end }} spec: restartPolicy: Never affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role.kubernetes.io/worker operator: Exists preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: kubernetes.io/arch operator: In values: ["arm64"] serviceAccountName: sso-vault containers: - name: configure image: python:3.11-alpine env: - name: KEYCLOAK_SERVER value: http://keycloak.sso.svc.cluster.local - name: KEYCLOAK_REALM value: atlas - name: TARGET_CLIENT_ID value: bstein-dev-home command: ["/bin/sh", "-c"] args: - | set -euo pipefail . /vault/secrets/keycloak-env.sh python - <<'PY' import json import os import urllib.parse import urllib.error import urllib.request base_url = os.environ["KEYCLOAK_SERVER"].rstrip("/") realm = os.environ["KEYCLOAK_REALM"] admin_user = os.environ["KEYCLOAK_ADMIN_USER"] admin_password = os.environ["KEYCLOAK_ADMIN_PASSWORD"] target_client_id = os.environ["TARGET_CLIENT_ID"] def http_json(method: str, url: str, token: str, payload=None): data = None headers = {"Authorization": f"Bearer {token}"} if payload is not None: data = json.dumps(payload).encode() headers["Content-Type"] = "application/json" req = urllib.request.Request(url, data=data, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=30) as resp: body = resp.read() if not body: return resp.status, None return resp.status, json.loads(body.decode()) except urllib.error.HTTPError as exc: raw = exc.read() if not raw: return exc.code, None try: return exc.code, json.loads(raw.decode()) except Exception: return exc.code, {"raw": raw.decode(errors="replace")} def get_admin_token() -> str: token_data = urllib.parse.urlencode( { "grant_type": "password", "client_id": "admin-cli", "username": admin_user, "password": admin_password, } ).encode() req = urllib.request.Request( f"{base_url}/realms/master/protocol/openid-connect/token", data=token_data, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=15) as resp: body = json.loads(resp.read().decode()) except urllib.error.HTTPError as exc: raw = exc.read().decode(errors="replace") raise SystemExit(f"Token request failed: status={exc.code} body={raw}") return body["access_token"] token = get_admin_token() status, clients = http_json( "GET", f"{base_url}/admin/realms/{realm}/clients?clientId={urllib.parse.quote(target_client_id)}", token, ) if status != 200 or not isinstance(clients, list) or not clients: raise SystemExit(f"Unable to find target client {target_client_id!r} (status={status})") client_uuid = None for item in clients: if isinstance(item, dict) and item.get("clientId") == target_client_id: client_uuid = item.get("id") break if not client_uuid: raise SystemExit(f"Target client {target_client_id!r} has no id") status, client_rep = http_json( "GET", f"{base_url}/admin/realms/{realm}/clients/{client_uuid}", token, ) if status != 200 or not isinstance(client_rep, dict): raise SystemExit(f"Unable to fetch client representation (status={status})") attrs = client_rep.get("attributes") or {} updated = False if attrs.get("oauth2.token.exchange.grant.enabled") != "true": attrs["oauth2.token.exchange.grant.enabled"] = "true" updated = True client_rep["attributes"] = attrs if updated: status, resp = http_json( "PUT", f"{base_url}/admin/realms/{realm}/clients/{client_uuid}", token, client_rep, ) if status not in (200, 204): raise SystemExit(f"Client update failed (status={status}) resp={resp}") print(f"OK: ensured token exchange enabled on client {target_client_id}") PY volumeMounts: volumes: