From c82f15b190d143ca3629c63559cf5247a5869d79 Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Tue, 10 Mar 2026 19:23:30 +0000 Subject: [PATCH] feat: add tuwunel/matrix support with OpenSearch ML post-apply hooks - Add matrix to MANAGED_NS and tuwunel to restart/build targets - Add post-apply hooks for matrix namespace: - _patch_tuwunel_oauth2_redirect: reads client_id from hydra-maester Secret and patches OAuth2Client redirectUris dynamically - _inject_opensearch_model_id: reads model_id from ingest pipeline and writes to ConfigMap for tuwunel deployment env var injection - Add post-apply hook for data namespace: - _ensure_opensearch_ml: idempotently registers/deploys all-mpnet-base-v2 (768-dim) model, creates ingest + hybrid search pipelines - Add tuwunel secrets to OpenBao seed (OIDC, TURN, registration token) - Refactor secret seeding to only write dirty paths (avoid VSO churn) - Add ACME email fallback from config when not provided via CLI flag --- sunbeam/cli.py | 21 ++-- sunbeam/manifests.py | 233 ++++++++++++++++++++++++++++++++++++++++++- sunbeam/secrets.py | 166 ++++++++++++++++++++---------- sunbeam/services.py | 8 +- 4 files changed, 362 insertions(+), 66 deletions(-) diff --git a/sunbeam/cli.py b/sunbeam/cli.py index d2eddb0..647a883 100644 --- a/sunbeam/cli.py +++ b/sunbeam/cli.py @@ -82,7 +82,8 @@ def main() -> None: "docs-frontend", "people-frontend", "people", "messages", "messages-backend", "messages-frontend", "messages-mta-in", "messages-mta-out", - "messages-mpa", "messages-socks-proxy"], + "messages-mpa", "messages-socks-proxy", + "tuwunel"], help="What to build") p_build.add_argument("--push", action="store_true", help="Push image to registry after building") @@ -104,12 +105,14 @@ def main() -> None: p_config = sub.add_parser("config", help="Manage sunbeam configuration") config_sub = p_config.add_subparsers(dest="config_action", metavar="action") - # sunbeam config set --host HOST --infra-dir DIR + # sunbeam config set --host HOST --infra-dir DIR --acme-email EMAIL p_config_set = config_sub.add_parser("set", help="Set configuration values") p_config_set.add_argument("--host", default="", help="Production SSH host (e.g. user@server.example.com)") p_config_set.add_argument("--infra-dir", default="", help="Infrastructure directory root") + p_config_set.add_argument("--acme-email", default="", + help="ACME email for Let's Encrypt certificates (e.g. ops@sunbeam.pt)") # sunbeam config get config_sub.add_parser("get", help="Get current configuration") @@ -249,17 +252,21 @@ def main() -> None: p_config.print_help() sys.exit(0) elif action == "set": - config = SunbeamConfig( - production_host=args.host if args.host else "", - infra_directory=args.infra_dir if args.infra_dir else "" - ) + config = load_config() + if args.host: + config.production_host = args.host + if args.infra_dir: + config.infra_directory = args.infra_dir + if args.acme_email: + config.acme_email = args.acme_email save_config(config) elif action == "get": from sunbeam.output import ok config = load_config() ok(f"Production host: {config.production_host or '(not set)'}") ok(f"Infrastructure directory: {config.infra_directory or '(not set)'}") - + ok(f"ACME email: {config.acme_email or '(not set)'}") + # Also show effective production host (from config or env) effective_host = get_production_host() if effective_host: diff --git a/sunbeam/manifests.py b/sunbeam/manifests.py index 983940d..464ecde 100644 --- a/sunbeam/manifests.py +++ b/sunbeam/manifests.py @@ -5,9 +5,10 @@ from pathlib import Path from sunbeam.kube import kube, kube_out, kube_ok, kube_apply, kustomize_build, get_lima_ip, get_domain from sunbeam.output import step, ok, warn -REPO_ROOT = Path(__file__).parents[2] / "infrastructure" -MANAGED_NS = ["data", "devtools", "ingress", "lasuite", "media", "monitoring", "ory", - "storage", "vault-secrets-operator"] +from sunbeam.config import get_infra_dir as _get_infra_dir +REPO_ROOT = _get_infra_dir() +MANAGED_NS = ["data", "devtools", "ingress", "lasuite", "matrix", "media", "monitoring", + "ory", "storage", "vault-secrets-operator"] def pre_apply_cleanup(namespaces=None): @@ -156,6 +157,219 @@ def _filter_by_namespace(manifests: str, namespace: str) -> str: return "---\n" + "\n---\n".join(kept) + "\n" +def _patch_tuwunel_oauth2_redirect(domain: str): + """Patch the tuwunel OAuth2Client redirect URI with the actual client_id. + + Hydra-maester generates the client_id when it first reconciles the + OAuth2Client CRD, storing it in the oidc-tuwunel Secret. We read that + secret and patch the CRD's redirectUris to include the correct callback + path that tuwunel will use. + """ + import base64, json + + client_id_b64 = kube_out("get", "secret", "oidc-tuwunel", "-n", "matrix", + "-o=jsonpath={.data.CLIENT_ID}", "--ignore-not-found") + if not client_id_b64: + warn("oidc-tuwunel secret not yet available — skipping redirect URI patch. " + "Re-run 'sunbeam apply matrix' after hydra-maester has reconciled.") + return + + client_id = base64.b64decode(client_id_b64).decode() + redirect_uri = f"https://messages.{domain}/_matrix/client/unstable/login/sso/callback/{client_id}" + + # Check current redirect URIs to avoid unnecessary patches. + current = kube_out("get", "oauth2client", "tuwunel", "-n", "matrix", + "-o=jsonpath={.spec.redirectUris[*]}", "--ignore-not-found") + if redirect_uri in current.split(): + return + + patch = json.dumps({"spec": {"redirectUris": [redirect_uri]}}) + kube("patch", "oauth2client", "tuwunel", "-n", "matrix", + "--type=merge", f"-p={patch}", check=False) + ok(f"Patched tuwunel OAuth2Client redirect URI.") + + +def _os_api(path: str, method: str = "GET", data: str | None = None) -> str: + """Call OpenSearch API via kubectl exec. Returns response body.""" + cmd = ["exec", "deploy/opensearch", "-n", "data", "-c", "opensearch", "--"] + curl = ["curl", "-sf", f"http://localhost:9200{path}"] + if method != "GET": + curl += ["-X", method] + if data is not None: + curl += ["-H", "Content-Type: application/json", "-d", data] + return kube_out(*cmd, *curl) + + +def _ensure_opensearch_ml(): + """Idempotently configure OpenSearch ML Commons for neural search. + + 1. Sets cluster settings to allow ML on data nodes. + 2. Registers and deploys all-mpnet-base-v2 (pre-trained, 384-dim). + 3. Creates ingest + search pipelines for hybrid BM25+neural scoring. + """ + import json, time + + # Check OpenSearch is reachable. + if not _os_api("/_cluster/health"): + warn("OpenSearch not reachable — skipping ML setup.") + return + + # 1. Ensure ML Commons cluster settings (idempotent PUT). + _os_api("/_cluster/settings", "PUT", json.dumps({"persistent": { + "plugins.ml_commons.only_run_on_ml_node": False, + "plugins.ml_commons.native_memory_threshold": 90, + "plugins.ml_commons.model_access_control_enabled": False, + "plugins.ml_commons.allow_registering_model_via_url": True, + }})) + + # 2. Check if model already registered and deployed. + search_resp = _os_api("/_plugins/_ml/models/_search", "POST", + '{"query":{"match":{"name":"huggingface/sentence-transformers/all-mpnet-base-v2"}}}') + if not search_resp: + warn("OpenSearch ML search API failed — skipping ML setup.") + return + + resp = json.loads(search_resp) + hits = resp.get("hits", {}).get("hits", []) + model_id = None + + for hit in hits: + state = hit.get("_source", {}).get("model_state", "") + if state == "DEPLOYED": + model_id = hit["_id"] + break + elif state in ("REGISTERED", "DEPLOYING"): + model_id = hit["_id"] + + if model_id and any(h["_source"].get("model_state") == "DEPLOYED" for h in hits): + pass # Already deployed, skip to pipelines. + elif model_id: + # Registered but not deployed — deploy it. + ok("Deploying OpenSearch ML model...") + _os_api(f"/_plugins/_ml/models/{model_id}/_deploy", "POST") + for _ in range(30): + time.sleep(5) + r = _os_api(f"/_plugins/_ml/models/{model_id}") + if r and '"DEPLOYED"' in r: + break + else: + # Register from pre-trained hub. + ok("Registering OpenSearch ML model (all-mpnet-base-v2)...") + reg_resp = _os_api("/_plugins/_ml/models/_register", "POST", json.dumps({ + "name": "huggingface/sentence-transformers/all-mpnet-base-v2", + "version": "1.0.1", + "model_format": "TORCH_SCRIPT", + })) + if not reg_resp: + warn("Failed to register ML model — skipping.") + return + task_id = json.loads(reg_resp).get("task_id", "") + if not task_id: + warn("No task_id from model registration — skipping.") + return + + # Wait for registration. + ok("Waiting for model registration...") + for _ in range(60): + time.sleep(10) + task_resp = _os_api(f"/_plugins/_ml/tasks/{task_id}") + if not task_resp: + continue + task = json.loads(task_resp) + state = task.get("state", "") + if state == "COMPLETED": + model_id = task.get("model_id", "") + break + if state == "FAILED": + warn(f"ML model registration failed: {task_resp}") + return + + if not model_id: + warn("ML model registration timed out.") + return + + # Deploy. + ok("Deploying ML model...") + _os_api(f"/_plugins/_ml/models/{model_id}/_deploy", "POST") + for _ in range(30): + time.sleep(5) + r = _os_api(f"/_plugins/_ml/models/{model_id}") + if r and '"DEPLOYED"' in r: + break + + if not model_id: + warn("No ML model available — skipping pipeline setup.") + return + + # 3. Create/update ingest pipeline (PUT is idempotent). + _os_api("/_ingest/pipeline/tuwunel_embedding_pipeline", "PUT", json.dumps({ + "description": "Tuwunel message embedding pipeline", + "processors": [{"text_embedding": { + "model_id": model_id, + "field_map": {"body": "embedding"}, + }}], + })) + + # 4. Create/update search pipeline (PUT is idempotent). + _os_api("/_search/pipeline/tuwunel_hybrid_pipeline", "PUT", json.dumps({ + "description": "Tuwunel hybrid BM25+neural search pipeline", + "phase_results_processors": [{"normalization-processor": { + "normalization": {"technique": "min_max"}, + "combination": {"technique": "arithmetic_mean", "parameters": {"weights": [0.3, 0.7]}}, + }}], + })) + + ok(f"OpenSearch ML ready (model: {model_id}).") + return model_id + + +def _inject_opensearch_model_id(): + """Read deployed ML model_id from OpenSearch, write to ConfigMap in matrix ns. + + The tuwunel deployment reads TUWUNEL_SEARCH_OPENSEARCH_MODEL_ID from this + ConfigMap. Creates or updates the ConfigMap idempotently. + + Reads the model_id from the ingest pipeline (which _ensure_opensearch_ml + already configured with the correct model_id). + """ + import json + + # Read model_id from the ingest pipeline that _ensure_opensearch_ml created. + pipe_resp = _os_api("/_ingest/pipeline/tuwunel_embedding_pipeline") + if not pipe_resp: + warn("OpenSearch ingest pipeline not found — skipping model_id injection. " + "Run 'sunbeam apply data' first.") + return + + pipe = json.loads(pipe_resp) + processors = (pipe.get("tuwunel_embedding_pipeline", {}) + .get("processors", [])) + model_id = None + for proc in processors: + model_id = proc.get("text_embedding", {}).get("model_id") + if model_id: + break + + if not model_id: + warn("No model_id in ingest pipeline — tuwunel hybrid search will be unavailable.") + return + + # Check if ConfigMap already has this value. + current = kube_out("get", "configmap", "opensearch-ml-config", "-n", "matrix", + "-o=jsonpath={.data.model_id}", "--ignore-not-found") + if current == model_id: + return + + cm = json.dumps({ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": {"name": "opensearch-ml-config", "namespace": "matrix"}, + "data": {"model_id": model_id}, + }) + kube("apply", "--server-side", "-f", "-", input=cm) + ok(f"Injected OpenSearch model_id ({model_id}) into matrix/opensearch-ml-config.") + + def cmd_apply(env: str = "local", domain: str = "", email: str = "", namespace: str = ""): """Build kustomize overlay for env, substitute domain/email, kubectl apply. @@ -163,6 +377,11 @@ def cmd_apply(env: str = "local", domain: str = "", email: str = "", namespace: cert-manager registers a ValidatingWebhook that must be running before ClusterIssuer / Certificate resources can be created. """ + # Fall back to config for ACME email if not provided via CLI flag. + if not email: + from sunbeam.config import load_config + email = load_config().acme_email + if env == "production": if not domain: # Try to discover domain from running cluster @@ -207,4 +426,12 @@ def cmd_apply(env: str = "local", domain: str = "", email: str = "", namespace: kube("apply", "--server-side", "--force-conflicts", "-f", "-", input=manifests2) _restart_for_changed_configmaps(before, _snapshot_configmaps()) + + # Post-apply hooks for namespaces that need runtime patching. + if not namespace or namespace == "matrix": + _patch_tuwunel_oauth2_redirect(domain) + _inject_opensearch_model_id() + if not namespace or namespace == "data": + _ensure_opensearch_ml() + ok("Applied.") diff --git a/sunbeam/secrets.py b/sunbeam/secrets.py index 1dcea52..eca52eb 100644 --- a/sunbeam/secrets.py +++ b/sunbeam/secrets.py @@ -15,6 +15,11 @@ from sunbeam.output import step, ok, warn, die ADMIN_USERNAME = "estudio-admin" +def _gen_fernet_key() -> str: + """Generate a Fernet-compatible key (32 random bytes, URL-safe base64).""" + return base64.urlsafe_b64encode(_secrets.token_bytes(32)).decode() + + def _gen_dkim_key_pair() -> tuple[str, str]: """Generate an RSA 2048-bit DKIM key pair using openssl. @@ -133,6 +138,9 @@ def _seed_openbao() -> dict: return {} # Read-or-generate helper: preserves existing KV values; only generates missing ones. + # Tracks which paths had new values so we only write back when necessary. + _dirty_paths: set = set() + def get_or_create(path, **fields): raw = bao( f"BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN='{root_token}' " @@ -145,7 +153,12 @@ def _seed_openbao() -> dict: pass result = {} for key, default_fn in fields.items(): - result[key] = existing.get(key) or default_fn() + val = existing.get(key) + if val: + result[key] = val + else: + result[key] = default_fn() + _dirty_paths.add(path) return result def rand(): @@ -193,7 +206,9 @@ def _seed_openbao() -> dict: kratos_admin = get_or_create("kratos-admin", **{"cookie-secret": rand, "csrf-cookie-secret": rand, - "admin-identity-ids": lambda: ""}) + "admin-identity-ids": lambda: "", + "s3-access-key": lambda: seaweedfs["access-key"], + "s3-secret-key": lambda: seaweedfs["secret-key"]}) docs = get_or_create("docs", **{"django-secret-key": rand, @@ -225,15 +240,16 @@ def _seed_openbao() -> dict: _dkim_private, _dkim_public = _gen_dkim_key_pair() messages = get_or_create("messages", - **{"django-secret-key": rand, - "salt-key": rand, - "mda-api-secret": rand, - "dkim-private-key": lambda: _dkim_private, - "dkim-public-key": lambda: _dkim_public, - "rspamd-password": rand, - "socks-proxy-users": lambda: f"sunbeam:{rand()}", - "mta-out-smtp-username": lambda: "sunbeam", - "mta-out-smtp-password": rand}) + **{"django-secret-key": rand, + "salt-key": rand, + "mda-api-secret": rand, + "oidc-refresh-token-key": _gen_fernet_key, + "dkim-private-key": lambda: _dkim_private, + "dkim-public-key": lambda: _dkim_public, + "rspamd-password": rand, + "socks-proxy-users": lambda: f"sunbeam:{rand()}", + "mta-out-smtp-username": lambda: "sunbeam", + "mta-out-smtp-password": rand}) collabora = get_or_create("collabora", **{"username": lambda: "admin", @@ -262,48 +278,92 @@ def _seed_openbao() -> dict: **{"access-key-id": lambda: _scw_config("access-key"), "secret-access-key": lambda: _scw_config("secret-key")}) - # Write all secrets to KV (idempotent -- puts same values back) - # messages secrets written separately first (multi-field KV, avoids line-length issues) - bao(f"BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN='{root_token}' sh -c '" - f"bao kv put secret/messages" - f" django-secret-key=\"{messages['django-secret-key']}\"" - f" salt-key=\"{messages['salt-key']}\"" - f" mda-api-secret=\"{messages['mda-api-secret']}\"" - f" rspamd-password=\"{messages['rspamd-password']}\"" - f" socks-proxy-users=\"{messages['socks-proxy-users']}\"" - f" mta-out-smtp-username=\"{messages['mta-out-smtp-username']}\"" - f" mta-out-smtp-password=\"{messages['mta-out-smtp-password']}\"" - f"'") - # DKIM keys stored separately (large PEM values) - dkim_priv_b64 = base64.b64encode(messages['dkim-private-key'].encode()).decode() - dkim_pub_b64 = base64.b64encode(messages['dkim-public-key'].encode()).decode() - bao(f"BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN='{root_token}' sh -c '" - f"echo {dkim_priv_b64} | base64 -d > /tmp/dkim_priv.pem && " - f"echo {dkim_pub_b64} | base64 -d > /tmp/dkim_pub.pem && " - f"bao kv patch secret/messages" - f" dkim-private-key=\"$(cat /tmp/dkim_priv.pem)\"" - f" dkim-public-key=\"$(cat /tmp/dkim_pub.pem)\" && " - f"rm /tmp/dkim_priv.pem /tmp/dkim_pub.pem" - f"'") + # Only write secrets to OpenBao KV for paths that have new/missing values. + # This avoids unnecessary KV version bumps which trigger VSO re-syncs and + # rollout restarts across the cluster. + if not _dirty_paths: + ok("All OpenBao KV secrets already present -- skipping writes.") + else: + ok(f"Writing new secrets to OpenBao KV ({', '.join(sorted(_dirty_paths))})...") - bao(f"BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN='{root_token}' sh -c '" - f"bao kv put secret/hydra system-secret=\"{hydra['system-secret']}\" cookie-secret=\"{hydra['cookie-secret']}\" pairwise-salt=\"{hydra['pairwise-salt']}\" && " - f"bao kv put secret/kratos secrets-default=\"{kratos['secrets-default']}\" secrets-cookie=\"{kratos['secrets-cookie']}\" smtp-connection-uri=\"{kratos['smtp-connection-uri']}\" && " - f"bao kv put secret/gitea admin-username=\"{gitea['admin-username']}\" admin-password=\"{gitea['admin-password']}\" && " - f"bao kv put secret/seaweedfs access-key=\"{seaweedfs['access-key']}\" secret-key=\"{seaweedfs['secret-key']}\" && " - f"bao kv put secret/hive oidc-client-id=\"{hive['oidc-client-id']}\" oidc-client-secret=\"{hive['oidc-client-secret']}\" && " - f"bao kv put secret/livekit api-key=\"{livekit['api-key']}\" api-secret=\"{livekit['api-secret']}\" && " - f"bao kv put secret/people django-secret-key=\"{people['django-secret-key']}\" && " - f"bao kv put secret/login-ui cookie-secret=\"{login_ui['cookie-secret']}\" csrf-cookie-secret=\"{login_ui['csrf-cookie-secret']}\" && " - f"bao kv put secret/kratos-admin cookie-secret=\"{kratos_admin['cookie-secret']}\" csrf-cookie-secret=\"{kratos_admin['csrf-cookie-secret']}\" admin-identity-ids=\"{kratos_admin['admin-identity-ids']}\" && " - f"bao kv put secret/docs django-secret-key=\"{docs['django-secret-key']}\" collaboration-secret=\"{docs['collaboration-secret']}\" && " - f"bao kv put secret/meet django-secret-key=\"{meet['django-secret-key']}\" application-jwt-secret-key=\"{meet['application-jwt-secret-key']}\" && " - f"bao kv put secret/drive django-secret-key=\"{drive['django-secret-key']}\" && " - f"bao kv put secret/collabora username=\"{collabora['username']}\" password=\"{collabora['password']}\" && " - f"bao kv put secret/grafana admin-password=\"{grafana['admin-password']}\" && " - f"bao kv put secret/scaleway-s3 access-key-id=\"{scaleway_s3['access-key-id']}\" secret-access-key=\"{scaleway_s3['secret-access-key']}\" && " - f"bao kv put secret/tuwunel oidc-client-id=\"{tuwunel['oidc-client-id']}\" oidc-client-secret=\"{tuwunel['oidc-client-secret']}\" turn-secret=\"{tuwunel['turn-secret']}\" registration-token=\"{tuwunel['registration-token']}\"" - f"'") + def _kv_put(path, **kv): + pairs = " ".join(f'{k}="{v}"' for k, v in kv.items()) + bao(f"BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN='{root_token}' " + f"bao kv put secret/{path} {pairs}") + + if "messages" in _dirty_paths: + _kv_put("messages", + **{"django-secret-key": messages["django-secret-key"], + "salt-key": messages["salt-key"], + "mda-api-secret": messages["mda-api-secret"], + "oidc-refresh-token-key": messages["oidc-refresh-token-key"], + "rspamd-password": messages["rspamd-password"], + "socks-proxy-users": messages["socks-proxy-users"], + "mta-out-smtp-username": messages["mta-out-smtp-username"], + "mta-out-smtp-password": messages["mta-out-smtp-password"]}) + # DKIM keys stored separately (large PEM values) + dkim_priv_b64 = base64.b64encode(messages['dkim-private-key'].encode()).decode() + dkim_pub_b64 = base64.b64encode(messages['dkim-public-key'].encode()).decode() + bao(f"BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN='{root_token}' sh -c '" + f"echo {dkim_priv_b64} | base64 -d > /tmp/dkim_priv.pem && " + f"echo {dkim_pub_b64} | base64 -d > /tmp/dkim_pub.pem && " + f"bao kv patch secret/messages" + f" dkim-private-key=\"$(cat /tmp/dkim_priv.pem)\"" + f" dkim-public-key=\"$(cat /tmp/dkim_pub.pem)\" && " + f"rm /tmp/dkim_priv.pem /tmp/dkim_pub.pem" + f"'") + if "hydra" in _dirty_paths: + _kv_put("hydra", **{"system-secret": hydra["system-secret"], + "cookie-secret": hydra["cookie-secret"], + "pairwise-salt": hydra["pairwise-salt"]}) + if "kratos" in _dirty_paths: + _kv_put("kratos", **{"secrets-default": kratos["secrets-default"], + "secrets-cookie": kratos["secrets-cookie"], + "smtp-connection-uri": kratos["smtp-connection-uri"]}) + if "gitea" in _dirty_paths: + _kv_put("gitea", **{"admin-username": gitea["admin-username"], + "admin-password": gitea["admin-password"]}) + if "seaweedfs" in _dirty_paths: + _kv_put("seaweedfs", **{"access-key": seaweedfs["access-key"], + "secret-key": seaweedfs["secret-key"]}) + if "hive" in _dirty_paths: + _kv_put("hive", **{"oidc-client-id": hive["oidc-client-id"], + "oidc-client-secret": hive["oidc-client-secret"]}) + if "livekit" in _dirty_paths: + _kv_put("livekit", **{"api-key": livekit["api-key"], + "api-secret": livekit["api-secret"]}) + if "people" in _dirty_paths: + _kv_put("people", **{"django-secret-key": people["django-secret-key"]}) + if "login-ui" in _dirty_paths: + _kv_put("login-ui", **{"cookie-secret": login_ui["cookie-secret"], + "csrf-cookie-secret": login_ui["csrf-cookie-secret"]}) + if "kratos-admin" in _dirty_paths: + _kv_put("kratos-admin", **{"cookie-secret": kratos_admin["cookie-secret"], + "csrf-cookie-secret": kratos_admin["csrf-cookie-secret"], + "admin-identity-ids": kratos_admin["admin-identity-ids"], + "s3-access-key": kratos_admin["s3-access-key"], + "s3-secret-key": kratos_admin["s3-secret-key"]}) + if "docs" in _dirty_paths: + _kv_put("docs", **{"django-secret-key": docs["django-secret-key"], + "collaboration-secret": docs["collaboration-secret"]}) + if "meet" in _dirty_paths: + _kv_put("meet", **{"django-secret-key": meet["django-secret-key"], + "application-jwt-secret-key": meet["application-jwt-secret-key"]}) + if "drive" in _dirty_paths: + _kv_put("drive", **{"django-secret-key": drive["django-secret-key"]}) + if "collabora" in _dirty_paths: + _kv_put("collabora", **{"username": collabora["username"], + "password": collabora["password"]}) + if "grafana" in _dirty_paths: + _kv_put("grafana", **{"admin-password": grafana["admin-password"]}) + if "scaleway-s3" in _dirty_paths: + _kv_put("scaleway-s3", **{"access-key-id": scaleway_s3["access-key-id"], + "secret-access-key": scaleway_s3["secret-access-key"]}) + if "tuwunel" in _dirty_paths: + _kv_put("tuwunel", **{"oidc-client-id": tuwunel["oidc-client-id"], + "oidc-client-secret": tuwunel["oidc-client-secret"], + "turn-secret": tuwunel["turn-secret"], + "registration-token": tuwunel["registration-token"]}) # Configure Kubernetes auth method so VSO can authenticate with OpenBao ok("Configuring Kubernetes auth for VSO...") @@ -519,7 +579,7 @@ def _seed_kratos_admin_identity(ob_pod: str, root_token: str) -> tuple[str, str] ok(f" admin identity exists ({identity_id[:8]}...)") else: identity = _kratos_api(base, "/identities", method="POST", body={ - "schema_id": "default", + "schema_id": "employee", "traits": {"email": admin_email}, "state": "active", }) diff --git a/sunbeam/services.py b/sunbeam/services.py index 999eac8..05ffc1a 100644 --- a/sunbeam/services.py +++ b/sunbeam/services.py @@ -8,8 +8,8 @@ from sunbeam.kube import kube, kube_out, parse_target from sunbeam.tools import ensure_tool from sunbeam.output import step, ok, warn, die -MANAGED_NS = ["data", "devtools", "ingress", "lasuite", "media", "ory", "storage", - "vault-secrets-operator"] +MANAGED_NS = ["data", "devtools", "ingress", "lasuite", "matrix", "media", "ory", + "storage", "vault-secrets-operator"] SERVICES_TO_RESTART = [ ("ory", "hydra"), @@ -22,6 +22,7 @@ SERVICES_TO_RESTART = [ ("lasuite", "people-frontend"), ("lasuite", "people-celery-worker"), ("lasuite", "people-celery-beat"), + ("matrix", "tuwunel"), ("media", "livekit-server"), ] @@ -186,8 +187,9 @@ def cmd_logs(target: str, follow: bool): if not name: die("Logs require a service name, e.g. 'ory/kratos'.") + _kube_mod.ensure_tunnel() kubectl = str(ensure_tool("kubectl")) - cmd = [kubectl, "--context=sunbeam", "-n", ns, "logs", + cmd = [kubectl, _kube_mod.context_arg(), "-n", ns, "logs", "-l", f"app={name}", "--tail=100"] if follow: cmd.append("--follow")