From 51da6d88815a3cf8a2a80d05e96eef0d0c27adcf Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Sat, 18 Jul 2026 18:55:09 +0300 Subject: [PATCH] chore: upgrade project stack and deployment baseline --- Dockerfile | 21 +-- README.md | 19 ++- android/Dockerfile | 26 ++-- android/README.md | 2 + compose.yaml | 6 +- config/config.exs | 4 +- .../helm/who-need-help/templates/_helpers.tpl | 2 +- .../who-need-help/templates/deployments.yaml | 5 +- .../helm/who-need-help/templates/secret.yaml | 18 --- deploy/helm/who-need-help/values-kind.yaml | 6 +- deploy/helm/who-need-help/values.yaml | 6 +- deploy/kind/dependencies.yaml | 29 +++- docs/dependency-baseline.md | 95 +++++++++++++ docs/verification.md | 36 ++++- mix.exs | 4 +- mix.lock | 2 +- scripts/kind-up.sh | 126 ++++++++++++++++-- scripts/verify-realtime-cluster.sh | 5 +- 18 files changed, 330 insertions(+), 82 deletions(-) delete mode 100644 deploy/helm/who-need-help/templates/secret.yaml create mode 100644 docs/dependency-baseline.md diff --git a/Dockerfile b/Dockerfile index e8fa4f4..0cee593 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ # This file is based on these images: # # - https://hub.docker.com/r/hexpm/elixir/tags - for the builder image -# E.g.: docker.io/hexpm/elixir:1.18.4-erlang-27.3.4.13-debian-trixie-20260623-slim -# - https://hub.docker.com/_/debian/tags?name=trixie-20260623-slim - for the runner image -# E.g.: docker.io/debian:trixie-20260623-slim +# E.g.: docker.io/hexpm/elixir:1.20.2-erlang-29.0.3-debian-trixie-20260713-slim +# - https://hub.docker.com/_/debian/tags?name=trixie-20260713-slim - for the runner image +# E.g.: docker.io/debian:trixie-20260713-slim # # Find builder and runner images on Docker Hub or on Hex's Build Server (Bob). # We recommend using Bob's Web UI to find recent tags: @@ -17,18 +17,19 @@ # # For finding packages in Debian, search on https://packages.debian.org/. -ARG ELIXIR_VERSION=1.18.4 -ARG OTP_VERSION=27.3.4.13 -ARG DEBIAN_VERSION=trixie-20260623-slim +ARG ELIXIR_VERSION=1.20.2 +ARG OTP_VERSION=29.0.3 +ARG DEBIAN_VERSION=trixie-20260713-slim -ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" -ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}" +ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}@sha256:6fcd8ea864221b960c1ec418e3b10fa488298ff9e70c9e0f3db18070e610fb8a" +ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd" -FROM docker.io/node:22.21.1-bookworm-slim AS node_deps +FROM docker.io/node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d AS node_deps WORKDIR /assets COPY assets/package.json assets/package-lock.json ./ -RUN npm ci +RUN npm install --global npm@12.0.1 \ + && npm ci FROM ${BUILDER_IMAGE} AS builder diff --git a/README.md b/README.md index b08f2ba..f4724d0 100644 --- a/README.md +++ b/README.md @@ -151,12 +151,22 @@ Open: - Mailpit: The script refuses to modify a pre-existing cluster named `who-need-help` -unless the project ownership marker exists. +unless the project ownership marker exists. On first install it creates +independent random application and PostgreSQL credentials in the +`who-need-help-local` Kubernetes Secret without writing them to Git. The local +PostGIS data directory persists inside the kind node. When migrating an older +project-owned cluster from the former `emptyDir` deployment, the script creates +and validates a local dump before replacing the database workload, then restores +that dump. After a successful rollout it also removes the obsolete chart Secret +and only the local Helm history revisions that stored the former inline +credential fields. For an external cluster, provide a real PostgreSQL/PostGIS service and a -pre-created Secret through `existingSecret`. The chart intentionally has no -invented CPU/RAM limits or HPA thresholds; measure this application in the -target environment before setting them. +pre-created Secret through required `existingSecret`; the chart never renders +credentials from tracked values. The Secret must contain `DATABASE_URL`, +`SECRET_KEY_BASE`, `HANDOVER_SECRET`, and `RELEASE_COOKIE`. The chart +intentionally has no invented CPU/RAM limits or HPA thresholds; measure this +application in the target environment before setting them. ## Local Codex category review @@ -185,6 +195,7 @@ and no fallback provider. Recommendations require a human moderator action. - [Architecture](docs/architecture.md) - [Trust and safety](docs/trust-safety.md) - [Implementation verification and known limits](docs/verification.md) +- [Verified dependency baseline](docs/dependency-baseline.md) - [PostgreSQL/PostGIS ADR](docs/decisions/0001-postgresql-postgis-over-spacetimedb.md) Exact dependency versions are locked in `mix.lock`, diff --git a/android/Dockerfile b/android/Dockerfile index 31f1a24..27fa25b 100644 --- a/android/Dockerfile +++ b/android/Dockerfile @@ -2,8 +2,8 @@ FROM gradle:9.6.1-jdk17@sha256:7364ce528f33bb6038672bcef990d524f1ad8fbc292935819 USER root -ARG ANDROID_COMMAND_LINE_TOOLS_VERSION=14742923 -ARG ANDROID_COMMAND_LINE_TOOLS_SHA1=48833c34b761c10cb20bcd16582129395d121b27 +ARG ANDROID_COMMAND_LINE_TOOLS_VERSION=15859902 +ARG ANDROID_COMMAND_LINE_TOOLS_SHA1=040d3996a65543d22ec4bf73e4c37aa37a8d4af4 ENV ANDROID_HOME=/opt/android-sdk ENV ANDROID_SDK_ROOT=/opt/android-sdk @@ -21,12 +21,11 @@ RUN mkdir -p "${ANDROID_HOME}/cmdline-tools" \ | sha1sum -c - \ && unzip -q /tmp/android-command-line-tools.zip -d /tmp/android-command-line-tools \ && mv /tmp/android-command-line-tools/cmdline-tools "${ANDROID_HOME}/cmdline-tools/latest" \ - && rm -rf /tmp/android-command-line-tools /tmp/android-command-line-tools.zip \ - && yes | sdkmanager --licenses >/dev/null + && rm -rf /tmp/android-command-line-tools /tmp/android-command-line-tools.zip -RUN sdkmanager \ - "build-tools;37.0.0" \ - "platforms;android-37.0" \ +RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \ + "build-tools/37.0.0" \ + "platforms/android-37.0" \ && chown -R gradle:gradle "${ANDROID_HOME}" FROM android-base AS android-sdk @@ -52,6 +51,11 @@ FROM android-base AS emulator USER root +RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \ + "emulator" \ + "system-images/android-37.0/google_apis_ps16k/x86_64" \ + && chown -R gradle:gradle "${ANDROID_HOME}" + RUN apt-get update \ && apt-get install -y --no-install-recommends \ libdbus-1-3 \ @@ -63,20 +67,16 @@ RUN apt-get update \ libxcomposite1 \ libxcursor1 \ libxi6 \ + libxkbfile1 \ libxrandr2 \ libxrender1 \ libxtst6 \ && rm -rf /var/lib/apt/lists/* -RUN sdkmanager \ - "emulator" \ - "system-images;android-37.0;google_apis_ps16k;x86_64" \ - && chown -R gradle:gradle "${ANDROID_HOME}" - # The container is operated from the host through `docker exec ... adb`. # Keep platform-tools in the emulator target without invalidating the large # system-image layer when application sources change. -RUN sdkmanager "platform-tools" \ +RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install "platform-tools" \ && chown -R gradle:gradle "${ANDROID_HOME}" USER gradle diff --git a/android/README.md b/android/README.md index d432ca9..65dc98b 100644 --- a/android/README.md +++ b/android/README.md @@ -11,6 +11,8 @@ background-location permission. - Android Gradle Plugin 9.3.0 - Gradle 9.6.1 +- Android SDK Command-line Tools 22.0 +- Android CLI 1.0.15857036 (embedded in the locked Command-line Tools archive) - compileSdk / targetSdk 37 - Build Tools 37.0.0 - Java source and bytecode level 17 diff --git a/compose.yaml b/compose.yaml index c3e9731..582f641 100644 --- a/compose.yaml +++ b/compose.yaml @@ -27,7 +27,7 @@ x-app-environment: &app-environment services: proxy: - image: traefik:v3.7 + image: traefik:v3.7.8@sha256:4299bbed850421258fc5448c2e0e6ad350981d4d335a68de11b92448aedbefe5 command: - --api.dashboard=false - --providers.docker=true @@ -41,7 +41,7 @@ services: restart: unless-stopped db: - image: postgis/postgis:18-3.6-alpine + image: postgis/postgis:18-3.6-alpine@sha256:05d68c7f0f19b9aa0bf7c4a2049b2e8b38b44a63116392b95726a4c913766cf6 environment: POSTGRES_DB: ${POSTGRES_DB:?Set POSTGRES_DB in .env} POSTGRES_USER: ${POSTGRES_USER:?Set POSTGRES_USER in .env} @@ -57,7 +57,7 @@ services: restart: unless-stopped mailpit: - image: axllent/mailpit:v1.27 + image: axllent/mailpit:v1.30.4@sha256:5a49a77c5bdbe7c5474450b4f46348d09949df3695257729c93a30369382d4f6 ports: - "${MAILPIT_PORT:-8027}:8025" networks: [edge, internal] diff --git a/config/config.exs b/config/config.exs index c582777..7152122 100644 --- a/config/config.exs +++ b/config/config.exs @@ -72,7 +72,7 @@ config :who_need_help, WhoNeedHelp.Mailer, adapter: Swoosh.Adapters.Local # Configure esbuild (the version is required) config :esbuild, - version: "0.25.4", + version: "0.28.1", who_need_help: [ args: ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.), @@ -82,7 +82,7 @@ config :esbuild, # Configure tailwind (the version is required) config :tailwind, - version: "4.3.0", + version: "4.3.3", who_need_help: [ args: ~w( --input=assets/css/app.css diff --git a/deploy/helm/who-need-help/templates/_helpers.tpl b/deploy/helm/who-need-help/templates/_helpers.tpl index bce3c89..f3b725e 100644 --- a/deploy/helm/who-need-help/templates/_helpers.tpl +++ b/deploy/helm/who-need-help/templates/_helpers.tpl @@ -22,5 +22,5 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{- define "who-need-help.secretName" -}} -{{- default (include "who-need-help.fullname" .) .Values.existingSecret }} +{{- required "existingSecret must name a pre-created Kubernetes Secret" .Values.existingSecret }} {{- end }} diff --git a/deploy/helm/who-need-help/templates/deployments.yaml b/deploy/helm/who-need-help/templates/deployments.yaml index abe0738..d844a77 100644 --- a/deploy/helm/who-need-help/templates/deployments.yaml +++ b/deploy/helm/who-need-help/templates/deployments.yaml @@ -20,11 +20,10 @@ spec: app.kubernetes.io/component: {{ $component }} template: metadata: + {{- with $root.Values.podAnnotations }} annotations: - checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") $root | sha256sum }} - {{- with $root.Values.podAnnotations }} {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} labels: {{- include "who-need-help.selectorLabels" $root | nindent 8 }} app.kubernetes.io/component: {{ $component }} diff --git a/deploy/helm/who-need-help/templates/secret.yaml b/deploy/helm/who-need-help/templates/secret.yaml deleted file mode 100644 index f354edf..0000000 --- a/deploy/helm/who-need-help/templates/secret.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if not .Values.existingSecret }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "who-need-help.fullname" . }} - labels: - {{- include "who-need-help.labels" . | nindent 4 }} - annotations: - helm.sh/hook: pre-install,pre-upgrade - helm.sh/hook-weight: "-10" - helm.sh/hook-delete-policy: before-hook-creation -type: Opaque -stringData: - DATABASE_URL: {{ .Values.app.databaseUrl | quote }} - SECRET_KEY_BASE: {{ .Values.app.secretKeyBase | quote }} - HANDOVER_SECRET: {{ .Values.app.handoverSecret | quote }} - RELEASE_COOKIE: {{ .Values.app.releaseCookie | quote }} -{{- end }} diff --git a/deploy/helm/who-need-help/values-kind.yaml b/deploy/helm/who-need-help/values-kind.yaml index 27182d6..9e85201 100644 --- a/deploy/helm/who-need-help/values-kind.yaml +++ b/deploy/helm/who-need-help/values-kind.yaml @@ -8,11 +8,9 @@ service: port: 80 nodePort: 30080 +existingSecret: who-need-help-local + app: host: localhost scheme: http urlPort: "4011" - databaseUrl: ecto://postgres:postgres@postgis/who_need_help - secretKeyBase: y9XxDIDxcn8uHarbX1B4feQl0xXi413tdYO472d758DtvslbtlVdUs2HdxPEPXCQ - handoverSecret: L2a4yz6nLYkWvIBTlREj8uDByngsY39JfLIfdYNDH/7dnIQEVW4T8OCjCkHOGZ7H - releaseCookie: OwNDSCX7aQkkNXovILLiESVolaRuYm67xC+JbMC3j3ERTY6sHIM/F4gc0elm1l07 diff --git a/deploy/helm/who-need-help/values.yaml b/deploy/helm/who-need-help/values.yaml index c0d52dd..f3e6384 100644 --- a/deploy/helm/who-need-help/values.yaml +++ b/deploy/helm/who-need-help/values.yaml @@ -20,10 +20,6 @@ app: urlPort: "443" port: "4000" poolSize: "10" - databaseUrl: ecto://postgres:postgres@postgis/who_need_help - secretKeyBase: replace-before-public-deployment - handoverSecret: replace-before-public-deployment - releaseCookie: replace-before-public-deployment codexSessionId: not-configured # Shared limits are opt-in; set only after product policy thresholds are approved. rateLimitPoliciesJson: "{}" @@ -31,6 +27,8 @@ app: smtpRelay: mailpit smtpPort: "1025" +# Required. The Secret must contain DATABASE_URL, SECRET_KEY_BASE, +# HANDOVER_SECRET, and RELEASE_COOKIE. existingSecret: "" ingress: diff --git a/deploy/kind/dependencies.yaml b/deploy/kind/dependencies.yaml index 3c95c35..f0d7a26 100644 --- a/deploy/kind/dependencies.yaml +++ b/deploy/kind/dependencies.yaml @@ -1,8 +1,9 @@ apiVersion: apps/v1 -kind: Deployment +kind: StatefulSet metadata: name: postgis spec: + serviceName: postgis replicas: 1 selector: matchLabels: {app: postgis} @@ -13,10 +14,23 @@ spec: containers: - name: postgis image: postgis/postgis:18-3.6-alpine + imagePullPolicy: IfNotPresent env: - - {name: POSTGRES_DB, value: who_need_help} - - {name: POSTGRES_USER, value: postgres} - - {name: POSTGRES_PASSWORD, value: postgres} + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: who-need-help-local + key: POSTGRES_DB + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: who-need-help-local + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: who-need-help-local + key: POSTGRES_PASSWORD ports: - {name: postgres, containerPort: 5432} readinessProbe: @@ -27,7 +41,9 @@ spec: - {name: data, mountPath: /var/lib/postgresql} volumes: - name: data - emptyDir: {} + hostPath: + path: /var/local/who-need-help/postgres + type: DirectoryOrCreate --- apiVersion: v1 kind: Service @@ -52,7 +68,8 @@ spec: spec: containers: - name: mailpit - image: axllent/mailpit:v1.27 + image: axllent/mailpit:v1.30.4 + imagePullPolicy: IfNotPresent ports: - {name: smtp, containerPort: 1025} - {name: http, containerPort: 8025} diff --git a/docs/dependency-baseline.md b/docs/dependency-baseline.md new file mode 100644 index 0000000..51c4dba --- /dev/null +++ b/docs/dependency-baseline.md @@ -0,0 +1,95 @@ +# Dependency baseline + +Verified on 2026-07-18. This is a point-in-time stable baseline, not a claim +that future security updates or major-version migrations can be avoided. +Application locks and OCI digests remain committed so the same revision can be +rebuilt deterministically. + +## Runtime and application + +| Component | Locked version | +| --- | --- | +| Elixir | 1.20.2 | +| Erlang/OTP | 29.0.3 | +| Phoenix | 1.8.9 | +| Phoenix LiveView | 1.2.7 | +| Ecto / Ecto SQL | 3.14.1 / 3.14.0 | +| Oban | 2.23.0 | +| Node.js LTS | 24.18.0 | +| npm build tool | 12.0.1 | +| Tailwind CSS binary | 4.3.3 | +| esbuild binary | 0.28.1 | +| daisyUI | 5.6.18 | +| Heroicons | 2.2.0 | +| MapLibre GL JS | 5.24.0 | + +`mix hex.outdated` reported every direct Hex dependency as up-to-date. +`npm outdated --json` returned an empty object, and the production `npm ci` +audit reported zero known vulnerabilities. The full transitive versions and +package checksums are in `mix.lock` and `assets/package-lock.json`. + +## Services and container build + +| Component | Locked version | +| --- | --- | +| PostgreSQL | 18.4 | +| PostGIS | 3.6.4 | +| Traefik | 3.7.8 | +| Mailpit | 1.30.4 | +| Debian builder/runner snapshot | trixie-20260713-slim | + +Every external Compose/kind service image and every Dockerfile base image is +locked to both an exact tag and an OCI digest. The observed local Docker tooling +was Docker Engine/CLI 29.6.2, Compose 5.3.1, and Buildx 0.35.0; their official +release feeds reported those same versions as current during verification. + +## Android and local Kubernetes + +| Component | Locked version | +| --- | --- | +| Android Gradle Plugin | 9.3.0 | +| Gradle | 9.6.1 | +| Android SDK Command-line Tools | 22.0 | +| Android CLI | 1.0.15857036 | +| Android Emulator | 36.6.11.0 | +| compileSdk / targetSdk | 37 / 37 | +| Android Build Tools | 37.0.0 | +| AndroidX Activity | 1.13.0 | +| kubectl | 1.36.2 | +| kind | 0.32.0 | +| Helm | 4.2.3 | + +Android 17 API 37 is the stable target used here. API 37.1 was not selected +because the official SDK channel identifies it as a QPR beta. + +## Primary version sources + +- [Elixir releases](https://github.com/elixir-lang/elixir/releases) +- [Erlang/OTP releases](https://github.com/erlang/otp/releases) +- [Hex package registry](https://hex.pm/) +- [Node.js release schedule](https://nodejs.org/en/about/previous-releases) +- [npm registry](https://www.npmjs.com/package/npm) +- [PostgreSQL 18.4 release notes](https://www.postgresql.org/docs/release/18.4/) +- [PostGIS release notes](https://postgis.net/docs/en/release_notes.html) +- [Traefik releases](https://github.com/traefik/traefik/releases) +- [Mailpit releases](https://github.com/axllent/mailpit/releases) +- [Android Gradle Plugin 9.3.0 release notes](https://developer.android.com/build/releases/agp-9-3-0-release-notes) +- [Android 17 SDK setup](https://developer.android.com/about/versions/17/setup-sdk) +- [Gradle release notes](https://docs.gradle.org/current/release-notes.html) +- [kubectl releases](https://dl.k8s.io/release/stable.txt) +- [kind releases](https://github.com/kubernetes-sigs/kind/releases) +- [Helm releases](https://github.com/helm/helm/releases) +- [Docker Engine releases](https://github.com/moby/moby/releases) +- [Docker Compose releases](https://github.com/docker/compose/releases) +- [Docker Buildx releases](https://github.com/docker/buildx/releases) + +## Repeatable checks + +```bash +docker run --rm who-need-help:test mix hex.outdated +docker build --target node_deps --tag who-need-help:node-deps . +docker run --rm who-need-help:node-deps npm --version +docker run --rm who-need-help:node-deps npm outdated --json +./scripts/test.sh +./scripts/android-build.sh +``` diff --git a/docs/verification.md b/docs/verification.md index ede6a14..4a4faf0 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -21,7 +21,8 @@ results from product limits and unknown production properties. ## Reproducible checks -- `./scripts/test.sh`: 124 tests, 0 failures in the final pre-upgrade run. +- `./scripts/test.sh`: 124 tests, 0 failures in the final post-upgrade run on + Elixir 1.20.2 and Erlang/OTP 29.0.3. - `mix format --check-formatted`: passed in the final run. - Android Docker build target: `testDebugUnitTest`, `lintDebug`, and `assembleDebug` passed; the final lint report contains no errors or warnings. @@ -43,7 +44,7 @@ Local generated evidence (ignored by Git): Android artifact: - `android/dist/who-need-help-debug.apk` -- SHA-256 before the dependency-upgrade pass: +- Final post-upgrade SHA-256: `063f3d8d877009ee229a403692e4b16517244dd9e7dddab4c2b3c202c5def4b8` - Observed manifest values: version `0.1.0-debug`, minimum SDK 24, target and compile SDK 37. @@ -65,6 +66,37 @@ excluded from the root Docker build context. The final local Compose observation found 2 users, 1 help request, and 7 messages. These are local scenario data; the database is not assumed empty. +The final kind observation found 0 users, 0 help requests, and 0 messages. That +state was queried before changing the database workload. The migration from the +old `emptyDir` Deployment created a custom-format dump, validated it with +`pg_restore --list`, changed PostGIS to a persistent StatefulSet, and restored +the dump before the Helm upgrade. The final kind database reported PostgreSQL +18.4 and PostGIS 3.6.4. + +The local kind Secret contains independently generated values. Only their +decoded lengths were inspected: the PostgreSQL password is 64 characters and +each application secret is 128 characters. Secret values were not printed or +written to tracked files. + +## Dependency-upgrade observations + +- The running Compose and kind releases reported Elixir 1.20.2 and Erlang/OTP + 29. +- Compose reported Traefik 3.7.8 and Mailpit 1.30.4; Compose and kind both + reported PostgreSQL 18.4 and PostGIS 3.6.4. +- The production asset build reported Tailwind CSS 4.3.3 and daisyUI 5.6.18; + esbuild is configured at 0.28.1. +- The isolated Node build stage reported Node.js 24.18.0, npm 12.0.1, + `npm outdated --json` returned `{}`, and `npm ci` reported zero known + vulnerabilities. +- The Android build used Android Gradle Plugin 9.3.0, Gradle 9.6.1, Command-line + Tools 22.0, and its embedded Android CLI 1.0.15857036. Unit tests, lint, and + debug assembly passed; the lint report says “No errors or warnings.” The + rebuilt emulator image contains the API 37 ps16k AVD and APK, and its emulator + 36.6.11.0 binary returned its version successfully at runtime. +- The final complete `scripts/kind-up.sh` run was idempotent and finished with + both 2-replica Deployments Ready and a successful cross-replica PubSub probe. + The migration `20260718114233` and reverse block lookup index `blocks_blocked_id_blocker_id_index` were present. An actual `EXPLAIN ANALYZE` for that reverse lookup selected the index. The table contained no block rows, diff --git a/mix.exs b/mix.exs index 6b8faf5..808e4a0 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule WhoNeedHelp.MixProject do [ app: :who_need_help, version: "0.1.0", - elixir: "~> 1.17", + elixir: "~> 1.20", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, aliases: aliases(), @@ -63,7 +63,7 @@ defmodule WhoNeedHelp.MixProject do depth: 1}, {:daisyui, github: "saadeghi/daisyui", - tag: "v5.5.20", + tag: "v5.6.18", sparse: "packages/bundle", app: false, compile: false, diff --git a/mix.lock b/mix.lock index 1da723a..9fc73d1 100644 --- a/mix.lock +++ b/mix.lock @@ -3,7 +3,7 @@ "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, - "daisyui": {:git, "https://github.com/saadeghi/daisyui.git", "22ecff57f2c391b80a75617325748cf4d13fdf47", [tag: "v5.5.20", sparse: "packages/bundle", depth: 1]}, + "daisyui": {:git, "https://github.com/saadeghi/daisyui.git", "374d63758f997317e92e93e547569a5f9ad11250", [tag: "v5.6.18", sparse: "packages/bundle", depth: 1]}, "db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"}, "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, diff --git a/scripts/kind-up.sh b/scripts/kind-up.sh index 152f3ca..f71bf70 100755 --- a/scripts/kind-up.sh +++ b/scripts/kind-up.sh @@ -8,6 +8,61 @@ export PATH CLUSTER=who-need-help MARKER="$ROOT/.tools/${CLUSTER}.owned" +NAMESPACE=who-need-help +SECRET_NAME=who-need-help-local +POSTGIS_IMAGE=postgis/postgis:18-3.6-alpine +POSTGIS_SOURCE="${POSTGIS_IMAGE}@sha256:05d68c7f0f19b9aa0bf7c4a2049b2e8b38b44a63116392b95726a4c913766cf6" +MAILPIT_IMAGE=axllent/mailpit:v1.30.4 +MAILPIT_SOURCE="${MAILPIT_IMAGE}@sha256:5a49a77c5bdbe7c5474450b4f46348d09949df3695257729c93a30369382d4f6" + +kube() { + kubectl --context "kind-${CLUSTER}" "$@" +} + +case "$(uname -m)" in + x86_64) IMAGE_ARCH=amd64 ;; + aarch64|arm64) IMAGE_ARCH=arm64 ;; + *) echo "Unsupported container architecture: $(uname -m)" >&2; exit 1 ;; +esac + +load_pinned_image() { + source=$1 + local_tag=$2 + archive=$3 + + docker pull --platform "linux/${IMAGE_ARCH}" "$source" + image_id=$(docker image inspect "$source" --format '{{.Id}}') + docker tag "$image_id" "$local_tag" + docker image save --platform "linux/${IMAGE_ARCH}" --output "$archive" "$local_tag" + kind load image-archive "$archive" --name "$CLUSTER" + rm -f "$archive" +} + +remove_legacy_inline_secrets() { + legacy_secret=who-need-help-who-need-help + + # Older chart revisions rendered credentials from tracked Helm values. Once + # the new deployments are healthy, remove the obsolete Secret and only those + # release-history records that still contain the former inline secret keys. + kube --namespace "$NAMESPACE" delete secret "$legacy_secret" \ + --ignore-not-found >/dev/null + + for revision in $( + helm history who-need-help \ + --kube-context "kind-${CLUSTER}" \ + --namespace "$NAMESPACE" | + awk 'NR > 1 {print $1}' + ); do + if helm get values who-need-help \ + --revision "$revision" \ + --kube-context "kind-${CLUSTER}" \ + --namespace "$NAMESPACE" | + grep -Eq '^[[:space:]]*(databaseUrl|secretKeyBase|handoverSecret|releaseCookie):'; then + kube --namespace "$NAMESPACE" delete secret \ + --selector="owner=helm,name=who-need-help,version=${revision}" + fi + done +} if kind get clusters | grep -Fxq "$CLUSTER"; then if [ ! -f "$MARKER" ]; then @@ -20,25 +75,80 @@ else touch "$MARKER" fi +load_pinned_image "$POSTGIS_SOURCE" "$POSTGIS_IMAGE" "$ROOT/.tools/postgis-kind.tar" +load_pinned_image "$MAILPIT_SOURCE" "$MAILPIT_IMAGE" "$ROOT/.tools/mailpit-kind.tar" + docker build --tag who-need-help:local "$ROOT" kind load docker-image who-need-help:local --name "$CLUSTER" -kubectl create namespace who-need-help --dry-run=client --output=yaml | kubectl apply -f - -kubectl --namespace who-need-help apply -f "$ROOT/deploy/kind/dependencies.yaml" -kubectl --namespace who-need-help rollout status deployment/postgis -kubectl --namespace who-need-help rollout status deployment/mailpit +kube create namespace "$NAMESPACE" --dry-run=client --output=yaml | kube apply -f - + +legacy_backup= +if kube --namespace "$NAMESPACE" get deployment postgis >/dev/null 2>&1; then + backup_dir="$ROOT/output/backups" + mkdir -p "$backup_dir" + umask 077 + legacy_backup="$backup_dir/kind-who-need-help-$(date +%Y%m%d-%H%M%S).dump" + kube --namespace "$NAMESPACE" exec deployment/postgis -- \ + pg_dump --username postgres --dbname who_need_help --format custom >"$legacy_backup" + test -s "$legacy_backup" + kube --namespace "$NAMESPACE" exec --stdin deployment/postgis -- \ + pg_restore --list <"$legacy_backup" >/dev/null + echo "Validated legacy kind database backup: $legacy_backup" +fi + +if ! kube --namespace "$NAMESPACE" get secret "$SECRET_NAME" >/dev/null 2>&1; then + if kube --namespace "$NAMESPACE" get statefulset postgis >/dev/null 2>&1; then + echo "Secret '$SECRET_NAME' is missing while the persistent PostGIS StatefulSet exists." >&2 + echo "Refusing to generate credentials that would not match the existing database." >&2 + exit 1 + fi + + postgres_password=$(openssl rand -hex 32) + secret_key_base=$(openssl rand -hex 64) + handover_secret=$(openssl rand -hex 64) + release_cookie=$(openssl rand -hex 64) + + kube --namespace "$NAMESPACE" create secret generic "$SECRET_NAME" \ + --from-literal=POSTGRES_DB=who_need_help \ + --from-literal=POSTGRES_USER=postgres \ + --from-literal="POSTGRES_PASSWORD=$postgres_password" \ + --from-literal="DATABASE_URL=ecto://postgres:${postgres_password}@postgis/who_need_help" \ + --from-literal="SECRET_KEY_BASE=$secret_key_base" \ + --from-literal="HANDOVER_SECRET=$handover_secret" \ + --from-literal="RELEASE_COOKIE=$release_cookie" + + unset postgres_password secret_key_base handover_secret release_cookie +fi + +if [ -n "$legacy_backup" ]; then + kube --namespace "$NAMESPACE" delete deployment postgis --wait=true +fi + +kube --namespace "$NAMESPACE" apply -f "$ROOT/deploy/kind/dependencies.yaml" +kube --namespace "$NAMESPACE" rollout status statefulset/postgis +kube --namespace "$NAMESPACE" rollout status deployment/mailpit + +if [ -n "$legacy_backup" ]; then + kube --namespace "$NAMESPACE" exec --stdin statefulset/postgis -- \ + pg_restore --username postgres --dbname who_need_help \ + --clean --if-exists --no-owner <"$legacy_backup" + echo "Restored legacy kind database backup." +fi helm upgrade --install who-need-help "$ROOT/deploy/helm/who-need-help" \ - --namespace who-need-help \ + --kube-context "kind-${CLUSTER}" \ + --namespace "$NAMESPACE" \ --values "$ROOT/deploy/helm/who-need-help/values-kind.yaml" \ --set-string app.codexSessionId="${CODEX_SESSION_ID:-${CODEX_THREAD_ID:-not-configured}}" \ --wait -kubectl --namespace who-need-help rollout restart \ +kube --namespace "$NAMESPACE" rollout restart \ deployment/who-need-help-who-need-help-web \ deployment/who-need-help-who-need-help-worker -kubectl --namespace who-need-help rollout status deployment/who-need-help-who-need-help-web -kubectl --namespace who-need-help rollout status deployment/who-need-help-who-need-help-worker +kube --namespace "$NAMESPACE" rollout status deployment/who-need-help-who-need-help-web +kube --namespace "$NAMESPACE" rollout status deployment/who-need-help-who-need-help-worker +remove_legacy_inline_secrets "$ROOT/scripts/verify-realtime-cluster.sh" kind echo "Who Need Help: http://localhost:4011" diff --git a/scripts/verify-realtime-cluster.sh b/scripts/verify-realtime-cluster.sh index 8d8a133..3088f67 100755 --- a/scripts/verify-realtime-cluster.sh +++ b/scripts/verify-realtime-cluster.sh @@ -19,7 +19,10 @@ case "$MODE" in TARGET=$( kubectl --context kind-who-need-help --namespace who-need-help get pods \ -l app.kubernetes.io/component=web \ - -o jsonpath='{.items[0].metadata.name}' + --field-selector=status.phase=Running \ + --no-headers \ + -o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[0].ready,DELETING:.metadata.deletionTimestamp' | + awk '$2 == "true" && $3 == "" {print $1; exit}' ) if [ -z "$TARGET" ]; then echo "No running kind web replica was found." >&2