diff --git a/stacks/chrome-service/files/novnc/Dockerfile b/stacks/chrome-service/files/novnc/Dockerfile new file mode 100644 index 00000000..e447a6da --- /dev/null +++ b/stacks/chrome-service/files/novnc/Dockerfile @@ -0,0 +1,19 @@ +FROM docker.io/library/ubuntu:24.04 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + x11vnc \ + novnc \ + websockify \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# noVNC ships /usr/share/novnc/vnc.html; alias to index.html so / works. +RUN ln -sf /usr/share/novnc/vnc.html /usr/share/novnc/index.html + +EXPOSE 6080 + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +CMD ["/entrypoint.sh"] diff --git a/stacks/chrome-service/files/novnc/entrypoint.sh b/stacks/chrome-service/files/novnc/entrypoint.sh new file mode 100644 index 00000000..1ec6657f --- /dev/null +++ b/stacks/chrome-service/files/novnc/entrypoint.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Connect to the chrome-service container's Xvfb (shared pod network, TCP) +# and serve the noVNC HTML5 client + websockify bridge on :6080. +set -e + +for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do + if echo > /dev/tcp/127.0.0.1/6099 2>/dev/null; then + echo "Xvfb TCP up after attempt $i" + break + fi + echo "waiting for Xvfb TCP 6099 attempt=$i" + sleep 2 +done + +# websockify runs as PID 1; x11vnc is a child so its logs land on container stdout +# `-noshm` skips MIT-SHM probes that fail across container boundaries (each +# container has its own /dev/shm); `-noxdamage` skips XDAMAGE which Xvfb +# doesn't expose; `-quiet` keeps the polling chatter out of pod logs. +echo "starting x11vnc -> :5900" +x11vnc -display localhost:99 -nopw -listen 0.0.0.0 -rfbport 5900 \ + -forever -shared -noshm -noxdamage -quiet 2>&1 & +X11VNC_PID=$! + +for i in 1 2 3 4 5 6 7 8 9 10; do + if echo > /dev/tcp/127.0.0.1/5900 2>/dev/null; then + echo "x11vnc bound 5900 after attempt $i" + break + fi + echo "waiting for x11vnc :5900 attempt=$i" + sleep 2 +done + +if ! echo > /dev/tcp/127.0.0.1/5900 2>/dev/null; then + echo "ERROR: x11vnc did not bind 5900" + exit 1 +fi + +echo "starting websockify -> :6080" +exec websockify --web=/usr/share/novnc 6080 localhost:5900 diff --git a/stacks/chrome-service/main.tf b/stacks/chrome-service/main.tf index 0b2d8b68..bfefcc98 100644 --- a/stacks/chrome-service/main.tf +++ b/stacks/chrome-service/main.tf @@ -125,6 +125,12 @@ resource "kubernetes_deployment" "chrome_service" { labels = local.labels } spec { + # The noVNC sidecar pulls from registry.viktorbarzin.me which needs + # auth. Kyverno's `sync-registry-credentials` ClusterPolicy syncs + # the secret into every namespace. + image_pull_secrets { + name = "registry-credentials" + } security_context { run_as_user = 1000 run_as_group = 1000 @@ -169,7 +175,13 @@ resource "kubernetes_deployment" "chrome_service" { args = [ <<-EOT set -e - Xvfb :99 -screen 0 1280x720x24 & + # `-listen tcp` enables localhost:6099 so the noVNC sidecar can + # connect over the pod's shared network namespace (Ubuntu 24.04 + # defaults Xvfb to -nolisten tcp). + # `-ac` disables X access control so the noVNC sidecar can + # attach without an MIT-MAGIC-COOKIE; safe because Xvfb only + # listens on localhost (pod's lo). + Xvfb :99 -screen 0 1280x720x24 -listen tcp -ac & sleep 1 cat > /tmp/launch.json <chrome-service -

chrome-service

-

Headless-Chromium-as-a-service is running.

-

Connect via Playwright: chromium.connect("ws://chrome-service.chrome-service.svc.cluster.local:3000/<TOKEN>")

- EOT - } -} - # --- Services --- # WS endpoint (internal only, gated by NetworkPolicy + token). resource "kubernetes_service" "chrome_service" { @@ -346,8 +328,8 @@ resource "kubernetes_service" "chrome_service" { } } -# Health page (Authentik-gated, exposed via ingress). -resource "kubernetes_service" "chrome_health" { +# noVNC view (Authentik-gated, exposed via ingress). +resource "kubernetes_service" "chrome_novnc" { metadata { name = "chrome" namespace = kubernetes_namespace.chrome_service.metadata[0].name @@ -359,7 +341,7 @@ resource "kubernetes_service" "chrome_health" { port { name = "http" port = 80 - target_port = 8080 + target_port = 6080 protocol = "TCP" } } @@ -372,10 +354,12 @@ module "ingress" { name = "chrome" tls_secret_name = var.tls_secret_name protected = true + # noVNC defaults to /vnc.html — auto-redirect / there. + ingress_path = ["/"] extra_annotations = { "gethomepage.dev/enabled" = "true" "gethomepage.dev/name" = "Chrome Service" - "gethomepage.dev/description" = "Headed Chromium WebSocket pool" + "gethomepage.dev/description" = "Live noVNC view of headed Chromium" "gethomepage.dev/icon" = "chromium.png" "gethomepage.dev/group" = "Infrastructure" }