chrome-service: replace static health stub with noVNC view
The static nginx stub at chrome.viktorbarzin.me wasn't useful for debugging anti-bot interactions. Swap it for a live noVNC HTML5 view of the headed Chromium session: x11vnc taps Xvfb's :99 over localhost TCP (added `-listen tcp -ac` to Xvfb), websockify wraps it as a WS endpoint, and noVNC's vendored web client serves it on :6080. The ingress chain is unchanged — chrome.viktorbarzin.me stays Authentik-gated, dns_type=proxied, port 3000 (the Playwright WS) stays internal-only behind the NetworkPolicy + token. Custom image `registry.viktorbarzin.me/chrome-service-novnc:v4` (ubuntu:24.04 + x11vnc + websockify + novnc apt packages) needs imagePullSecrets, so also added registry-credentials reference to the deployment spec. x11vnc flags: `-noshm -noxdamage -nopw -shared -forever`. SHM is disabled because each container has its own /dev/shm so the X server can't grant access; XDAMAGE isn't compiled into the noble Xvfb. The sidecar entrypoint waits up to 30s for both Xvfb (:6099) and x11vnc (:5900) to bind before exec'ing websockify. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f18cd1d314
commit
8146d05191
3 changed files with 89 additions and 47 deletions
19
stacks/chrome-service/files/novnc/Dockerfile
Normal file
19
stacks/chrome-service/files/novnc/Dockerfile
Normal file
|
|
@ -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"]
|
||||||
39
stacks/chrome-service/files/novnc/entrypoint.sh
Normal file
39
stacks/chrome-service/files/novnc/entrypoint.sh
Normal file
|
|
@ -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
|
||||||
|
|
@ -125,6 +125,12 @@ resource "kubernetes_deployment" "chrome_service" {
|
||||||
labels = local.labels
|
labels = local.labels
|
||||||
}
|
}
|
||||||
spec {
|
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 {
|
security_context {
|
||||||
run_as_user = 1000
|
run_as_user = 1000
|
||||||
run_as_group = 1000
|
run_as_group = 1000
|
||||||
|
|
@ -169,7 +175,13 @@ resource "kubernetes_deployment" "chrome_service" {
|
||||||
args = [
|
args = [
|
||||||
<<-EOT
|
<<-EOT
|
||||||
set -e
|
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
|
sleep 1
|
||||||
cat > /tmp/launch.json <<JSON
|
cat > /tmp/launch.json <<JSON
|
||||||
{
|
{
|
||||||
|
|
@ -252,33 +264,25 @@ resource "kubernetes_deployment" "chrome_service" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Static health/admin page served behind Authentik. Lets a human
|
# noVNC sidecar — exposes a live HTML5 view of the headed Chromium
|
||||||
# confirm the service is up via the browser; the WS endpoint stays
|
# session via x11vnc + websockify, gated by the Authentik-protected
|
||||||
# internal-only. nginx-unprivileged listens on 8080 as user 101 by
|
# ingress at chrome.viktorbarzin.me. WS port 3000 (the Playwright
|
||||||
# default — works under the pod's non-root securityContext.
|
# endpoint) stays internal-only.
|
||||||
container {
|
container {
|
||||||
name = "health"
|
name = "novnc"
|
||||||
image = "docker.io/nginxinc/nginx-unprivileged:alpine"
|
image = "registry.viktorbarzin.me/chrome-service-novnc:v4"
|
||||||
image_pull_policy = "IfNotPresent"
|
image_pull_policy = "IfNotPresent"
|
||||||
port {
|
port {
|
||||||
name = "http"
|
name = "http"
|
||||||
container_port = 8080
|
container_port = 6080
|
||||||
protocol = "TCP"
|
protocol = "TCP"
|
||||||
}
|
}
|
||||||
volume_mount {
|
# x11vnc connects to the chrome-service container's Xvfb over
|
||||||
name = "health-html"
|
# localhost TCP (shared pod network). Same uid 1000 as chrome
|
||||||
mount_path = "/usr/share/nginx/html"
|
# container so we can read MIT-MAGIC-COOKIE if Xvfb adds one.
|
||||||
read_only = true
|
|
||||||
}
|
|
||||||
# nginx-unprivileged ships with /tmp, /var/cache/nginx and pidfile
|
|
||||||
# paths owned by UID 101, not 1000. Override the pod-level user.
|
|
||||||
security_context {
|
|
||||||
run_as_user = 101
|
|
||||||
run_as_group = 101
|
|
||||||
}
|
|
||||||
resources {
|
resources {
|
||||||
requests = { cpu = "10m", memory = "16Mi" }
|
requests = { cpu = "10m", memory = "32Mi" }
|
||||||
limits = { memory = "32Mi" }
|
limits = { memory = "96Mi" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,12 +299,6 @@ resource "kubernetes_deployment" "chrome_service" {
|
||||||
size_limit = "256Mi"
|
size_limit = "256Mi"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
volume {
|
|
||||||
name = "health-html"
|
|
||||||
config_map {
|
|
||||||
name = kubernetes_config_map.health_html.metadata[0].name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -310,22 +308,6 @@ resource "kubernetes_deployment" "chrome_service" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Static health page ConfigMaps (served by nginx sidecar) ---
|
|
||||||
resource "kubernetes_config_map" "health_html" {
|
|
||||||
metadata {
|
|
||||||
name = "chrome-service-health-html"
|
|
||||||
namespace = kubernetes_namespace.chrome_service.metadata[0].name
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
"index.html" = <<-EOT
|
|
||||||
<!doctype html><meta charset="utf-8"><title>chrome-service</title>
|
|
||||||
<h1>chrome-service</h1>
|
|
||||||
<p>Headless-Chromium-as-a-service is running.</p>
|
|
||||||
<p>Connect via Playwright: <code>chromium.connect("ws://chrome-service.chrome-service.svc.cluster.local:3000/<TOKEN>")</code></p>
|
|
||||||
EOT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Services ---
|
# --- Services ---
|
||||||
# WS endpoint (internal only, gated by NetworkPolicy + token).
|
# WS endpoint (internal only, gated by NetworkPolicy + token).
|
||||||
resource "kubernetes_service" "chrome_service" {
|
resource "kubernetes_service" "chrome_service" {
|
||||||
|
|
@ -346,8 +328,8 @@ resource "kubernetes_service" "chrome_service" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Health page (Authentik-gated, exposed via ingress).
|
# noVNC view (Authentik-gated, exposed via ingress).
|
||||||
resource "kubernetes_service" "chrome_health" {
|
resource "kubernetes_service" "chrome_novnc" {
|
||||||
metadata {
|
metadata {
|
||||||
name = "chrome"
|
name = "chrome"
|
||||||
namespace = kubernetes_namespace.chrome_service.metadata[0].name
|
namespace = kubernetes_namespace.chrome_service.metadata[0].name
|
||||||
|
|
@ -359,7 +341,7 @@ resource "kubernetes_service" "chrome_health" {
|
||||||
port {
|
port {
|
||||||
name = "http"
|
name = "http"
|
||||||
port = 80
|
port = 80
|
||||||
target_port = 8080
|
target_port = 6080
|
||||||
protocol = "TCP"
|
protocol = "TCP"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -372,10 +354,12 @@ module "ingress" {
|
||||||
name = "chrome"
|
name = "chrome"
|
||||||
tls_secret_name = var.tls_secret_name
|
tls_secret_name = var.tls_secret_name
|
||||||
protected = true
|
protected = true
|
||||||
|
# noVNC defaults to /vnc.html — auto-redirect / there.
|
||||||
|
ingress_path = ["/"]
|
||||||
extra_annotations = {
|
extra_annotations = {
|
||||||
"gethomepage.dev/enabled" = "true"
|
"gethomepage.dev/enabled" = "true"
|
||||||
"gethomepage.dev/name" = "Chrome Service"
|
"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/icon" = "chromium.png"
|
||||||
"gethomepage.dev/group" = "Infrastructure"
|
"gethomepage.dev/group" = "Infrastructure"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue