Skip to content

[feature] Frontend: add Helm chart for deploying goquery-ui frontend#474

Merged
els0r merged 4 commits into
mainfrom
add-goquery-ui-helm-chart
Jun 16, 2026
Merged

[feature] Frontend: add Helm chart for deploying goquery-ui frontend#474
els0r merged 4 commits into
mainfrom
add-goquery-ui-helm-chart

Conversation

@els0r

@els0r els0r commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Adds charts/goquery-ui, a frontend-only Helm chart for the goprobe/frontend image (Caddy-served SPA on port 5137). Designed to sit behind a gateway: ships a ClusterIP Service with a named http port and no Ingress/Gateway/VirtualService — external routing is the deployer's responsibility.

  • backend.url is required (Caddy reverse-proxy target, GQ_BACKEND_HOST); render fails fast if unset.
  • Runtime settings (GQ_API_BASE_URL, HOST_RESOLVER_TYPES, SSE_ON_LOAD) come from a ConfigMap with a checksum/config annotation to roll pods on change.
  • Hardened pod: non-root 1000, drop ALL caps, readOnlyRootFilesystem, seccomp RuntimeDefault, with emptyDir for /var/run/env, /tmp, /data, /config.
  • Liveness/readiness probe GET / on the http port; dedicated ServiceAccount with token automount off; optional HPA and PDB, both off by default.

Records the no-in-cluster-routing decision in docs/adr/0001.

Packaging & distribution

The chart is published from CI so it can be consumed as a dependency instead of copied by directory.

  • Publish workflow (.github/workflows/publish-chart.yml): on push to main touching charts/goquery-ui/**, packages the chart and pushes it to GHCR as an OCI artifact at oci://ghcr.io/els0r/charts/goquery-ui. Publishing is existence-checked, not diff-triggered — it ships only when the Chart.yaml version: is not already in the registry, so a failed run self-heals on the next push and an already-published version is never clobbered. Uses the built-in GITHUB_TOKEN (packages: write); no new secret required.
  • Decoupled versioning: the chart carries its own semver in Chart.yaml, independent of the app's v*.*.* release tags. Bump version: to ship; that field is the single source of truth.
  • PR lint (lint-chart in ci-pr.yml): helm lint + helm template (with a representative backend.url) + ct lint --check-version-increment, so chart breakage and missing version bumps fail review.

Consumers then declare it as a dependency:

dependencies:
  - name: goquery-ui
    version: 0.1.0
    repository: oci://ghcr.io/els0r/charts

Note

One-time GHCR setup after the first publish: the package is created private and owned by the user. Set its visibility to public (anonymous helm pull) and link it to the repo in the GHCR package settings — this can't be done from the workflow.

Adds charts/goquery-ui, a frontend-only Helm chart for the goprobe/frontend
image (Caddy-served SPA on port 5137). Designed to sit behind a gateway:
ships a ClusterIP Service with a named http port and no Ingress/Gateway/
VirtualService — external routing is the deployer's responsibility.

- backend.url is required (Caddy reverse-proxy target, GQ_BACKEND_HOST);
  render fails fast if unset.
- Runtime settings (GQ_API_BASE_URL, HOST_RESOLVER_TYPES, SSE_ON_LOAD) come
  from a ConfigMap with a checksum/config annotation to roll pods on change.
- Hardened pod: non-root 1000, drop ALL caps, readOnlyRootFilesystem, seccomp
  RuntimeDefault, with emptyDir for /var/run/env, /tmp, /data, /config.
- Liveness/readiness probe GET / on the http port; dedicated ServiceAccount
  with token automount off; optional HPA and PDB, both off by default.

Records the no-in-cluster-routing decision in docs/adr/0001.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@els0r els0r changed the title [feature] Add Helm chart for deploying goquery-ui frontend [feature] Frontend: add Helm chart for deploying goquery-ui frontend Jun 16, 2026
@els0r els0r self-assigned this Jun 16, 2026
@els0r els0r requested a review from fako1024 June 16, 2026 17:25
Lennart Elsen and others added 3 commits June 16, 2026 21:28
Publish the chart as an OCI artifact on push to main whenever its
Chart.yaml version is not already present in ghcr.io/els0r/charts.
Existence-checked rather than diff-triggered so a failed run self-heals
and a published version is never clobbered. Chart semver stays decoupled
from the app's v*.*.* release tags.

Add a PR lint-chart job (helm lint + helm template + ct lint with
--check-version-increment) so chart breakage and missing version bumps
fail review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ct validated the maintainer "goProbe maintainers" as a GitHub user and
404'd. It's a team label with a URL, not a handle, so disable
--validate-maintainers. Also drop --charts so ct detects changed charts
by diffing the target branch, which is what makes --check-version-increment
actually run (it was reported disabled otherwise).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@els0r els0r merged commit 1cf1714 into main Jun 16, 2026
9 checks passed
els0r added a commit that referenced this pull request Jun 17, 2026
## Problem

Deploying the `goquery-ui` Helm chart fails at container startup:

```
env.js written with GQ_API_BASE_URL=<empty/proxy>
/docker-entrypoint.sh: exec: line 27: caddy: Operation not permitted
```

The chart runs the pod with a hardened `securityContext`
(`allowPrivilegeEscalation: false`, `readOnlyRootFilesystem: true`,
`capabilities.drop: [ALL]`, non-root). Two image-level assumptions broke
under it.

## Root causes & fixes

1. **Caddy file capability → EPERM.** The `caddy` binary copied from
`caddy:2-alpine` carries `cap_net_bind_service=+ep` (so upstream Caddy
can bind :80/:443 as non-root). BuildKit's `COPY` preserves that file
capability. With `allowPrivilegeEscalation: false`, the kernel's
`no_new_privs` flag makes `exec()` of a file bearing file caps return
`EPERM` — exactly the error above, at the entrypoint's `exec "$@"`. We
listen on `:5137` (non-privileged), so the capability is unnecessary →
strip it with `setcap -r`.

2. **Caddy storage on read-only rootfs.** The `alpine` base never sets
`XDG_DATA_HOME`/`XDG_CONFIG_HOME`, so Caddy fell back to
`$HOME/.local/share` & `$HOME/.config` on the read-only rootfs —
producing startup errors and leaving the chart's `/data` & `/config`
mounts inert. Point both env vars at `/data` and `/config`.

## Reproduce-locally harness

`docker-compose.yml` is now a single default service that runs the
production image hardened **1:1** with the chart's `securityContext`
(`no-new-privileges`, `read_only`, `cap_drop ALL`, `user 1000`,
tmpfs-backed writable paths). No profiles — `docker compose up` just
works. This is what surfaced both bugs and would have caught them before
the cluster:

```
docker compose up --build    # or: make docker-up  → http://localhost:8080
```

Verified end-to-end: container `Up (healthy)`, `/` returns 200,
`/env.js` generated, Caddy writes to `/data/caddy` & `/config/caddy`
with no read-only errors.

Local hot-reload development is `npm run dev` (`:5173`) — the old
container-wrapped dev path only ran that same command, so it and its
`Dockerfile.dev` / `.env.development` were removed.

## Versioning — no `Chart.yaml` change here

This is an **image fix** (Dockerfile) with no chart-template change, so
it deliberately leaves `Chart.yaml` untouched. Chart releases follow two
avenues:

1. **`version:` bump** — chart-only changes (templates/values),
independent of the app's `v*.*.*` tags; published by `publish-chart.yml`
on merge to `main`.
2. **Tagged release** — bumps `appVersion` (and `version:`) to the new
`goprobe/frontend` image and ships a new chart.

This fix belongs to avenue 2: it reaches chart users when **`v4.2.12`**
is cut — `build-docker.yml` rebuilds `goprobe/frontend:4.2.12` and the
release bumps the chart's `appVersion`.

> [!NOTE]
> Avenue 2 isn't automated yet — `appVersion` is bumped by hand today
(as it was for `4.2.11` in #474). Wiring the `appVersion` bump into the
release process is a worthwhile follow-up, but out of scope for this
fix.

## Files
- `frontend/goquery-ui/Dockerfile` — `setcap -r` +
`XDG_DATA_HOME`/`XDG_CONFIG_HOME`
- `frontend/goquery-ui/docker-compose.yml` — single hardened prod-parity
service (no profiles)
- `frontend/goquery-ui/Makefile` — `docker-up`/`-stop`/`-logs` on the
default service; drop `docker-dev`
- `frontend/goquery-ui/README.md` — `npm run dev` for dev, `docker
compose up` for prod-like
- removed `Dockerfile.dev` / `.env.development` (+ its `.gitignore`
exception) — orphaned by the profile collapse

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Lennart Elsen <lelsen@open-systems.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant