Two small changes to the deploy job:
- The registry password went through `-u armistace:$PASSWORD` on the curl
command line, where it is visible in the process list for the duration of
the request. It now goes through a curl config file on stdin instead.
- The kubeconfig is written with mode 600 rather than the default, and is
removed at the end of the job (with `if: always()`, so it is removed even
if a later step fails). It is cluster-admin-equivalent, so it should not
be readable by anything else on the runner.
Verified: `curl --config -` against the registry returns the same digest as
the `-u` form.
The review flagged `:latest` as a critical issue, and it is right: a mutable
tag makes `kubectl rollout undo` meaningless and lets different pods in the
same deployment run different code if they restart at different times.
- The build job now pushes the image with BOTH `:latest` and
`:${{ github.sha }}`.
- The deployment manifest carries a sentinel tag rather than `:latest`, and
the deploy job sets the real SHA with `kubectl set image`. That makes the
deploy a genuine manifest change, so the Deployment rolls because its pod
template changed — not because it was told to. `kubectl rollout undo` now
means something: it goes back to the previous SHA.
- The digest check now compares against the SHA tag rather than `:latest`,
and it checks EVERY running pod instead of `items[0]`. During a rolling
update `items[0]` may still be an old pod that has not been terminated,
which would compare against the wrong digest. The job also fails if the
deployment is not fully rolled out.
- The smoke test now checks the app through the Service as well as directly
in the pod. The pod check alone would pass even if the Service selector
or the port mapping were wrong.
- `readOnlyRootFilesystem: true` — the app writes no files, so the root
filesystem can be read-only. /tmp is still writable via the emptyDir.
- The kubectl install is skipped when the runner already has one.
The workflow built and pushed the image but never deployed it - the
cluster objects had been created by hand. This adds a `deploy` job that
applies the manifests in `kube/` and rolls the deployment.
The deploy job is separate from the build job on purpose. The
`KUBEC_CONFIG_BUILDX` secret is cluster-admin-equivalent, and a build step
that never talks to the cluster should not be able to read it.
Three things this job does that a naive `kubectl apply` does not:
1. `kubectl rollout restart`. The image tag is `:latest`, which is immutable
in the manifest, so `apply` alone does not roll the pods. Without the
restart the deployment reports success while the old pods keep serving
the old image - a deploy that silently changes nothing.
2. Verifies the running digest against the registry's `:latest`. CI green and
pods `Ready` do not prove the new code is live; the digest comparison does.
3. Smoke tests the running app - `/healthz` and `/readyz` - from inside the
pod, so a deploy that comes up but cannot serve is caught in CI rather
than by the next person to open the page.
The Secrets (`beer-db`, `beer-app`) are hand-provisioned and are deliberately
not in the apply set, so a deploy cannot clobber them.
The independent review flagged that the build job wrote the
`KUBEC_CONFIG_BUILDX` secret to disk even though the build does not need
cluster access at all. That secret is cluster-admin-equivalent: a build step
that never talks to the cluster should not be able to read it, and a
compromised dependency in the build would otherwise have a direct path to the
cluster.
The "Create Kubeconfig" step is removed from the build job. The deploy step,
added in a later PR, gets its own job with the kubeconfig - so the privilege
is only present where it is actually used.
Also checked the buildkit daemon's resources in response to the review: it
already has limits (2 CPU, 4Gi memory, 2Gi ephemeral storage) and requests
(500m, 1Gi). The crash that motivated the amd64-only change was qemu
exhausting memory during multi-arch emulation, not an unconstrained daemon, so
no change is needed there.
The build job failed at "Set up Docker Buildx":
Error: ERROR: error for bootstrap "builder-...":
Get "https://192.168.178.160:6443/apis/apps/v1/namespaces/gitea-runner/deployments/builder-...":
tls: failed to verify certificate: x509: certificate signed by unknown authority
Cause
------
The `kubernetes` buildx driver talks to the cluster API to create its builder
pods. That needs the k3s CA certificate, which the job container does not
have - the `catthehacker/ubuntu:act-latest` image ships the public CA set
only. So the bootstrap could not verify the API server.
Fix
---
Switch to the `remote` driver, which talks straight to the buildkit daemon
already running in the cluster:
driver: remote
endpoint: tcp://buildkit.gitea-runner.svc:1234
No cluster API access is needed at all, so no kubeconfig is needed for the
build. This is the same pattern the wedding-photos workflow uses.
`endpoint` must be a top-level key, not a driver-opt: the action appends it
as the POSITIONAL argument to `buildx create --driver remote <endpoint>`, and
passing it in driver-opts yields "ERROR: no remote endpoint provided".
Also
----
The build was multi-arch (linux/amd64,linux/arm64), which runs qemu
emulation and crashes the buildkit pod on this NUC (exit 255,
SandboxChanged), after which the build hangs for many minutes. The cluster
is amd64, so the image is built for linux/amd64 only.
Verified: `docker buildx create --driver remote tcp://buildkit.gitea-runner.svc:1234`
succeeds from the runner's dind container and reports buildkit v0.29.0 with
linux/amd64 among its supported platforms.
The setup-python action downloads a Python build from GitHub. This runner
cannot reach GitHub reliably for that, and the test job hung on it with no
output. The runner image already carries a usable python3, so the tests use
it directly and install into a local venv.
The old UI was two pages of raw pandas tables with no navigation, no styling
and no way to see a run's comments. This replaces it with a small,
testable application and a beer-themed design.
What changed
------------
Restructure
- The domain logic now lives in `brewing.py` as pure functions over plain
data, with no Flask or Mongo dependency. The gravity maths, the ABV
formula and the run-aggregation rules are unit-testable on their own.
- `app_factory.py` replaces the module-level app, and the routes move into
blueprints under `views/`. A request-scoped Mongo repository is created per
request and closed at teardown.
- `mongo/repository.py` replaces `build_db.py` / `query_db.py` / `get_conn.py`.
Reading a document no longer requires a second query to fetch the fields.
Interface
- A dark, beer-themed design system: warm amber `#be7f39` on near-black, a
condensed display face (Anton) for headings and Inter for body text. Both
fonts and the chart library are vendored into `static/`, so the container
needs no outbound network at runtime — the old templates pulled Bootstrap
from a CDN.
- A real navigation bar, a login page, a brew-process page with a proper
form, and a run-detail page listing every reading with its comment.
- The run summary tables now show the beer name and the latest comment, which
the old pandas tables did not.
- Native form controls (checkbox, number spinners, date icon, scrollbars)
are restyled for the dark theme.
Correctness
- The alcohol calculation now uses the standard homebrew formula,
`(OG - FG) x 131.25`. The old code used `(max - min) / 7.36 + 0.5`, which is
not a recognised brewing formula and overstated every result.
- Gravity points and decimal readings now convert correctly in both
directions (`1039` <-> `1.039`).
- A run's display name resolves to the most-used label across its readings,
because `beer_type` is stored per-reading and drifts across a run.
- Re-submitting a reading for the same run and day updates it in place
rather than creating a duplicate.
Security / production
- The container ran the Werkzeug debugger with `FLASK_DEBUG=1`, exposing an
interactive console on any unhandled error. It now runs waitress with
debugging off.
- `SECRET_KEY` comes from the environment.
- The app secret is no longer a hard-coded string in source.
Housekeeping
- Removes the bokeh/duckdb/pandas dependency chain and the static file it
generated at import time; removes the dead `add_user.py` and `test.py`.
- Splits `requirements.txt` (runtime) from `requirements-dev.txt` (tests).
- Adds a pytest suite (64 tests) and a `test` job to the CI workflow so the
image is only built when the tests pass.
- Adds a README describing the data model and a RUNBOOK for the cluster side.
Data compatibility
------------------
The existing `beer_db.beer_data` documents are read as they are and are not
migrated or rewritten. No collection is dropped, and no document is modified
by this change other than through the explicit "record a reading" form, which
keeps the original `(date, beer_run_id)` uniqueness rule.