diff --git a/services/game-stream/kustomization.yaml b/services/game-stream/kustomization.yaml index 145381dfc..503cadc4f 100644 --- a/services/game-stream/kustomization.yaml +++ b/services/game-stream/kustomization.yaml @@ -4,6 +4,7 @@ kind: Kustomization resources: - namespace.yaml - vault-serviceaccount.yaml + - wolf-config-ensure-configmap.yaml - wolf-api-proxy-configmap.yaml - wolf-gatekeeper-configmap.yaml - wolf-service.yaml diff --git a/services/game-stream/wolf-config-ensure-configmap.yaml b/services/game-stream/wolf-config-ensure-configmap.yaml new file mode 100644 index 000000000..75d94d51e --- /dev/null +++ b/services/game-stream/wolf-config-ensure-configmap.yaml @@ -0,0 +1,144 @@ +# services/game-stream/wolf-config-ensure-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: wolf-config-ensure + namespace: game-stream +data: + wolf_config_ensure.py: | + #!/usr/bin/env python3 + import os + import re + import sys + import uuid + from pathlib import Path + + CONFIG_PATH = Path(os.environ.get("WOLF_CFG_FILE", "/etc/wolf/cfg/config.toml")) + APP_TITLE = "MECCHA CHAMELEON" + APP_ID = "4704690" + + APP_BLOCK = f""" + + [[profiles.apps]] + title = "{APP_TITLE}" + render_node = "/dev/dri/renderD128" + start_audio_server = true + start_virtual_compositor = true + icon_png_path = "https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/{APP_ID}/capsule_616x353.jpg" + [profiles.apps.runner] + type = "docker" + name = "WolfSteam" + image = "ghcr.io/games-on-whales/steam:edge" + mounts = [] + env = [ + "PROTON_LOG=1", + "RUN_SWAY=true", + "GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*", + "STEAM_STARTUP_FLAGS=steam://rungameid/{APP_ID}" + ] + devices = [] + ports = [] + base_create_json = \"\"\" + {{ + "HostConfig": {{ + "IpcMode": "host", + "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"], + "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"], + "Ulimits": [{{"Name":"nofile", "Hard":10240, "Soft":10240}}], + "Privileged": false, + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] + }} + }} + \"\"\" + """.rstrip() + + PROFILE_HEADER = re.compile(r"(?m)^\s*\[\[profiles\]\]\s*$") + EMPTY_PROFILES = re.compile(r"(?m)^\s*profiles\s*=\s*\[\]\s*$\n?") + + + def _seed_config() -> str: + return f"""hostname = "wolf" + support_hevc = true + config_version = 2 + uuid = "{uuid.uuid4()}" + + paired_clients = [] + gstreamer = {{}} + + [[profiles]] + id = "moonlight-profile-id" + name = "Moonlight" + {APP_BLOCK} + + [[profiles]] + id = "user" + name = "User" + {APP_BLOCK} + """ + + + def _has_app(section: str) -> bool: + return re.search(rf'(?m)^\s*title\s*=\s*["\']{re.escape(APP_TITLE)}["\']\s*$', section) is not None + + + def _ensure_app(text: str) -> str: + if not text.strip(): + return _seed_config() + + starts = [match.start() for match in PROFILE_HEADER.finditer(text)] + if not starts: + return EMPTY_PROFILES.sub("", text).rstrip() + "\n\n[[profiles]]\nid = \"moonlight-profile-id\"\nname = \"Moonlight\"" + APP_BLOCK + "\n" + + parts = [] + cursor = 0 + changed = False + for index, start in enumerate(starts): + end = starts[index + 1] if index + 1 < len(starts) else len(text) + parts.append(text[cursor:start]) + section = text[start:end] + if _has_app(section): + parts.append(section) + else: + parts.append(section.rstrip() + APP_BLOCK + "\n\n") + changed = True + cursor = end + parts.append(text[cursor:]) + if not changed: + return text + return "".join(parts).rstrip() + "\n" + + + def _validate_toml(text: str) -> None: + try: + import tomllib + except Exception: + return + tomllib.loads(text) + + + def main() -> int: + CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) + original = CONFIG_PATH.read_text(encoding="utf-8") if CONFIG_PATH.exists() else "" + updated = _ensure_app(original) + if updated == original: + print(f"{APP_TITLE} already present in {CONFIG_PATH}") + return 0 + + _validate_toml(updated) + if CONFIG_PATH.exists(): + backup = CONFIG_PATH.with_suffix(CONFIG_PATH.suffix + ".pre-meccha-chameleon") + backup.write_text(original, encoding="utf-8") + + tmp_path = CONFIG_PATH.with_suffix(CONFIG_PATH.suffix + ".tmp") + tmp_path.write_text(updated, encoding="utf-8") + tmp_path.replace(CONFIG_PATH) + print(f"ensured {APP_TITLE} Steam app {APP_ID} in {CONFIG_PATH}") + return 0 + + + if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as exc: + print(f"failed to ensure {APP_TITLE}: {exc}", file=sys.stderr) + raise diff --git a/services/game-stream/wolf-statefulset.yaml b/services/game-stream/wolf-statefulset.yaml index 91fe1283e..f6779e2f8 100644 --- a/services/game-stream/wolf-statefulset.yaml +++ b/services/game-stream/wolf-statefulset.yaml @@ -17,6 +17,8 @@ spec: labels: app: wolf atlas.bstein.dev/game-mode: wolf + annotations: + game-stream.bstein.dev/wolf-config-revision: "2026-07-13-meccha-chameleon" spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet @@ -39,6 +41,19 @@ spec: mountPath: /wolf-runtime - name: wolf-socket mountPath: /wolf-socket + - name: wolf-config-ensure + image: ghcr.io/games-on-whales/wolf:stable + imagePullPolicy: IfNotPresent + command: ["/usr/bin/python3", "/opt/wolf-config-ensure/wolf_config_ensure.py"] + env: + - name: WOLF_CFG_FILE + value: /etc/wolf/cfg/config.toml + volumeMounts: + - name: wolf-state + mountPath: /etc/wolf + - name: wolf-config-ensure + mountPath: /opt/wolf-config-ensure + readOnly: true nodeSelector: kubernetes.io/hostname: titan-24 tolerations: @@ -175,6 +190,10 @@ spec: configMap: name: wolf-api-proxy defaultMode: 0555 + - name: wolf-config-ensure + configMap: + name: wolf-config-ensure + defaultMode: 0555 - name: wolfmanager-data hostPath: path: /etc/wolfmanager