Skip to content

CI fix, /status, sidecar readiness, URL validation, TLS modes, date versioning - #4

Merged
f3l1x merged 13 commits into
masterfrom
feat/status-tls-versioning
Oct 12, 2026
Merged

f3l1x merged 13 commits into
masterfrom
feat/status-tls-versioning

Conversation

@f3l1x

@f3l1x f3l1x commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

Fixes the red master CI (no images have been published since 85f9ac8) and adds the next round of features. Each commit stands on its own.

CI fix

  • staticcheck v0.8.1 can't read go1.27.2 export data and crashed the lint job, which skipped every image build. The action is pinned to master commit 452d5bb86b45 until a newer release ships.
  • SA1019: httputil.ReverseProxy.Director is deprecated since Go 1.26, so the slot proxy now uses Rewrite. X-Forwarded-For is still appended by hand.
  • internal/proxy gets its first tests.

Features

  • /status: the sidecar's GET /status and the control-center's GET /api/slot/{i}/status report each slot's latest request. Phases are queued, refused, deduped, downloading, failed (with an error) and launched (with the app's state). The UI shows it next to the buttons.
  • Readiness: a slot counts as ready only when both its VNC port and its sidecar /healthz answer.
  • URL validation: internal/sanitize rejects a non-http(s) url and keys that collide once upper-cased. This happens in both the control-center and the sidecar, before anything reaches viewdoc.sh.
  • TLS: TLS_CERT picks the cert:
    • embedded (default) keeps the pre-generated cert.
    • generate makes a self-signed cert on demand, stored in TLS_CERT_DIR or kept in memory.
    • file loads your own TLS_CERT_FILE and TLS_KEY_FILE.
  • Versioning: versions are release dates, YYYY.MM.DD (.N for a second release on the same day).
    • VERSION holds the current one and make release bumps it.
    • It's built into both binaries, the image labels and /healthz, and CI publishes <variant>-<VERSION> tags.
    • A new release job tags the commit and creates the GitHub release once all images are pushed.

Cleanups

  • Compose: each viewer image is built once and the second replica reuses it.
  • Code: encoding/json replaces the hand-written jsonString, log keys are sorted, and a ?url= is sent to a slot only the first time it's shown.
  • CI: the Docker Hub login only runs when pushing, as in dockette/.github.

Verification

  • Checks: go test -race passes in 5 packages (all new tests), and shellcheck, hadolint and actionlint are clean.
  • Live stack (arm64): every /status phase, URL rejection, all three TLS modes (embedded / file / in-memory / persisted volume / bad config), a cold compose start building each viewer image once, and the version in labels, logs, /healthz and /status.
  • arm64 CI: the arm64 image builds run under QEMU for the first time when this merges.

After merge

  • The first master run publishes <variant>-2026.10.12 and creates release 2026.10.12. Run make release before merging if you want a different date.
  • CI: sync Docker Hub description via shared workflow #3 has been rebased onto master. It doesn't overlap this PR, so it can merge before or after.

f3l1x added 11 commits October 10, 2026 21:32
/api/slots and /healthz only probed the VNC port, so a slot whose
sidecar was down showed as ready and opening a file then failed with
502. Probe the sidecar's /healthz too (1s timeout) and report vnc,
sidecar and ready (= both) per slot. The tab tooltip says which half
is down. First tests for internal/api cover all four combinations.
- staticcheck v0.8.1 (latest release) can't read go1.27.2 export data
  and crashed the lint job, which skipped every image build. Pin the
  action to master commit 452d5bb86b45 until a newer release ships.
- With go.mod at go 1.27, staticcheck now flags SA1019:
  httputil.ReverseProxy.Director is deprecated since Go 1.26. Move the
  slot proxy to Rewrite. Rewrite strips inbound X-Forwarded-*, so append
  the client to X-Forwarded-For by hand to keep what upstreams saw
  (SetXForwarded would also add X-Forwarded-Host/Proto).
- Add internal/proxy tests (path strip, Host, identity encoding, XFF,
  bare-prefix redirect, HTML shim + JS rewrite); they pass against both
  the old Director code and the new Rewrite code.
- proxy: replace hand-rolled jsonString with encoding/json (also escapes
  <, > and &, so the slot prefix can't close the shim's <script>); gofmt
  the shim doc-comment list.
- api: paramsForLog prints keys sorted, so identical requests log
  identically.
- web: open the ?url= doc in a slot only the first time it's shown, not
  on every tab switch (re-sending stacked duplicate windows in apps that
  hand off to a running instance).
- control-center: drop trailing blank line (gofmt).
…tree-sidecar-readiness' into feat/status

* worktree-fix-ci:
  test(proxy): accept either JSON escaping of the slot prefix in the shim
  fix(ci): make the lint job pass on Go 1.27.2

* worktree-small-cleanups:
  chore: small cleanups

* worktree-sidecar-readiness:
  feat(control-center): count a slot as ready only when its sidecar is up
Opening a file only answered {"ok":true} once the hook was forked;
download failures, refused URLs and crashed apps were visible only in
the viewer's logs.

- viewdoc.sh records the outcome in /tmp/viewdoc.status: refused,
  deduped, downloading, failed (+ error) or launched (+ app). A
  dispatch helper names the app; $STATE also keeps the exit code.
- Sidecar: GET /status returns {phase, url, app, error, updated,
  app_state, exit_code}. POST /params marks the request queued first,
  app_state comes from $STATE (a dead running pid reads as exited),
  and startup clears the file like the last-URL file.
- Control-center: GET /api/slot/{i}/status relays it (1s timeout).
- UI: status next to the buttons for the active slot, polled every 1s
  while queued/downloading and every 5s otherwise.
- Tests for the sidecar handler and the relay; docs for both APIs.
viewdoc.sh already refuses non-http(s) URLs, but the control-center and
sidecar accepted anything, so a value like --version.mp4 travelled all
the way to the hook. Validate in internal/sanitize (used by both):

- url (any case — keys are upper-cased into VIEWDOC_Q_*) must be an
  absolute http:// or https:// URL with a host;
- keys that are equal after upper-casing are rejected, since they would
  map to the same shell variable.

First tests for internal/sanitize.
The control-center could only serve the pre-generated self-signed cert
embedded from certs/. Keep that as the default and add two sources:

- embedded (default): certs/server.{crt,key} embedded in the binary
  (make cert regenerates it) — same fingerprint everywhere, so the
  browser's "accept once" sticks.
- generate: a P-256 self-signed pair generated on demand, persisted in
  TLS_CERT_DIR (key 0600, reused on later starts; extra SANs via
  TLS_HOSTS) or kept in memory when no dir is set.
- file: your own TLS_CERT_FILE + TLS_KEY_FILE, e.g. CA-issued.

Unset TLS_CERT infers file / generate / embedded from which variables
are set. The startup log names the source and its SHA-256 fingerprint.
Mode selection lives in tlsself.Config with table tests (plus
persistence, key mode and SAN tests). gen-cert reuses the package and
takes -dir. compose documents the alternatives and mounts a certs
volume at /certs (the image ships it owned by nonroot) for generate.
Both replicas of each viewer carried a build: section (via the YAML
anchor), so compose built the Kasm and Webtop images twice. Move the
shared settings into x-kasm / x-webtop fragments, give build: only to
the first replica, and set pull_policy: never on the second so it
reuses the local image instead of pulling the published tag.

Also spell VIEWER_SLOTS with the kasm:// / webtop:// pseudo-schemes.
viewdoc had no version: images were only published as rolling
<variant> tags. Follow the dockette pattern (a version pinned in the
repo, tagged images, a Versions table — cf. dockette/grafana, tika)
with a release date instead of an upstream version:

- VERSION holds YYYY.MM.DD (YYYY.MM.DD.N for a same-day re-release);
  make release sets it to today's date, skipping already-tagged ones.
- internal/version.Version is set via -ldflags from the VERSION build
  arg (compose: VIEWDOC_VERSION, make build), logged by both binaries
  and returned by /healthz and the sidecar's /status. Images carry
  org.opencontainers.image.version.
- CI: validates VERSION, publishes <variant>-<VERSION> next to
  <variant> and <variant>-<sha>, and a new release job tags the commit
  and creates the GitHub release once all images are pushed. Docker
  Hub login now runs only when pushing (as in dockette/.github).
- README: Versions section and release steps.
f3l1x added 2 commits October 11, 2026 20:12
The CI runner's shellcheck flags it (the local stable image doesn't);
use an explicit if so the lint job passes.
@f3l1x
f3l1x merged commit 0e19d4e into master Oct 12, 2026
4 checks passed
@f3l1x
f3l1x deleted the feat/status-tls-versioning branch October 12, 2026 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant