8b8de0fdca3f022a8c910df25209127e5a4bd91f
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7bec2a13d8 |
feat(deploy): add a replication health check for the read replica
my.jorgecuadros.com reads customer data from the Oracle VPS replica, and a replica that has silently stopped applying serves stale balances rather than erroring — so "is it replicating" needed an answer that is not a human squinting at SHOW REPLICA STATUS. Runs entirely against the replica over ssh, so it needs no credentials for the galactus master, and exits non-zero on failure so it can be driven from cron or a monitor. It deliberately does not trust the two fields an operator reaches for first. Replica_IO_Running reports Yes while the SQL thread is stopped, because the network thread is still downloading binlog it will never apply — verified by stopping SQL_THREAD and watching IO stay Yes. Seconds_Behind_Source reads 0 both when there is nothing to apply and when nothing is connected. The trustworthy signal is GTID_SUBTRACT(Retrieved, Executed): binlog fetched but not applied. NULL lag means either thread is down, so it is reported as "not applying" rather than blamed on a specific thread — the thread fields above already say which, and guessing there produced a wrong diagnosis. Uses sed rather than `head -n1`; on this machine `head` resolves to LWP's HTTP head(1), which mangles the pipeline instead of failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d5ebb86cae |
fix(deploy): dump from a dedicated container as root, and prove the dump is real
The pre-migrate backup ran INSIDE the API container, which made it depend on that image's toolchain — and deadlocked: the running image shipped a MySQL client that could not authenticate, so the backup failed, which blocked the very deploy that would have replaced the broken image. A backup must not depend on the thing being deployed. The dump now runs in a throwaway container built from mysql:8.4 with the API's backup volume mounted. The volume name is discovered from the API container's mounts, so the file still lands where the Operaciones restore screen looks. As a container rather than an exec, its logs can simply be read — no more failures reported as a bare exit code. The image is pulled if the host lacks it, since a scope:app deploy never touches the db stack. Three further defects found while verifying, none of which would have surfaced without dumping against the real database: - The dump now runs as root. mysqldump --single-transaction issues FLUSH TABLES, needing the global RELOAD privilege; the MySQL image grants the application user only ALL ON `<db>`.*, and --skip-lock-tables does not avoid it. Elevating the app's own runtime user would have been the worse trade. - --set-gtid-purged=OFF. galactus is the replication SOURCE with GTID on, so a default dump embeds SET @@GLOBAL.GTID_PURGED and is unrestorable onto the server it came from. Verified: 0 GTID_PURGED lines in the output. - Verification was too weak to be worth having. `test -s` plus `gzip -t` passes on a 372-byte gzip containing no tables, which is exactly what a dump that died on its first statement produces. It now asserts a CREATE TABLE count and logs it. A failed attempt also deletes its own output, so a truncated file never appears in the restore list. Verified against live prod, both paths: success writes a 31-table dump the API container can see; a wrong password fails with mysqldump's own error quoted and leaves the volume empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
19f03198d6 |
fix(docker): install the MySQL 8.4 auth plugin; report why a dump fails
The pre-migrate backup failed with "mysqldump exited 2" and nothing else.
Reproduced on the host with stderr captured:
ERROR 1045: Plugin caching_sha2_password could not be loaded:
/usr/lib/mariadb/plugin/caching_sha2_password.so: No such file or directory
Alpine's `mysql-client` is MariaDB's client and ships an EMPTY plugin
directory, so it cannot perform caching_sha2_password — MySQL 8.4's default and
effectively only auth method. `mariadb-connector-c` provides the plugin.
This was never about the deploy backup alone. Every mysqldump/mysql call from
the API container was broken, which means the whole Operaciones panel — backup,
restore, sync, re-import — could not work in a container. It went unnoticed
because that feature had only ever been run with the API on a developer
machine, where the Oracle client is installed. Verified after the fix: dump
exits 0, gzip valid, 31 CREATE TABLEs.
Also fixed, both found while chasing the above:
- The backup script reported an exit code and nothing else, because a detached
exec captures no output — which is precisely why this needed a manual
reproduction. mysqldump's stderr is now redirected to a file and read back
through a short attached exec on failure, so the deploy log states the cause.
Verified against live prod: the log now carries the 1045 line itself.
- Listing ONLY 100.100.100.100 as the containers' resolver costs them public
DNS, since MagicDNS does not forward upstream unless the tailnet defines
global nameservers. Nothing at runtime needed it, but `apk` inside the
container stopped resolving, and anything outbound would have too. A public
fallback resolver is now listed after MagicDNS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7e3b530174 |
fix(deploy): pull images explicitly, and detect api/web drift by commit
The first successful galactus deploy came up all-green while the web tier was running a build from two commits earlier. The registry held web:latest from 3ff56e6; the host still had a web:latest cached from 4ee7ec7; the deploy reported success and served the old one. The API was only current because it had been pulled by hand during earlier debugging. Two independent failures, both fixed here. 1. Images are not pulled. The deploy action's `pull: true` does not reliably refresh an already-cached moving tag on a standalone endpoint. Added a Pull images step (deploy/scripts/pull-images.mjs) that pulls each image through Portainer's Docker API with registry credentials and fails the deploy if a pull fails — note the endpoint answers 200 even when the pull errored, so the stream body has to be inspected, not just the status. 2. The drift check could not see it. Both the verify step and the web footer compared APP_VERSION, but on a branch build BOTH tiers report "master", so equality proved nothing. They now compare gitSha, which is the only field that differs between two builds of the same branch. api and web come from one matrix run, so a difference can only mean an image was not replaced. This needed a /version on the web tier too — previously its build identity was only readable by scraping window.__APP_BUILD__ out of the HTML. pull-images.mjs builds the X-Registry-Auth header as URL-safe base64 WITH padding: Node's "base64url" omits the padding and Portainer's Go decoder rejects it with "Illegal base64 data at input byte N". Verified against galactus: pulls both images, and exits non-zero on a nonexistent tag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4ee7ec71f0 |
feat(deploy): prisma migration history, /version, galactus standalone deploy
Closes the gap between "what tag did I deploy" and "what is actually running", and gives the schema a history that can be reasoned about across releases. Migrations - Baseline the existing schema as 0000_init (migrate diff --from-empty). The schema had only ever been applied with `prisma db push`, so no history existed and schema state was disconnected from app version. Existing databases must be baselined once with `migrate resolve --applied 0000_init`; the workflows print this remedy on P3005. - Run `prisma migrate deploy` as a deploy STEP, not the container CMD — as a CMD, N replicas would race each other applying the same migration. Version reporting - GET /version on the API reports the APP_VERSION / GIT_SHA / BUILD_DATE that build.yml already baked into both images but nothing ever read. - The web footer shows the web build and flags an api/web mismatch. The two cannot drift at build time (one matrix run) but can at deploy time. - Both deploy workflows now fail if the running API does not report the tag that was dispatched — a stack naming a tag is not proof of what is running. - scripts/set-version.mjs stamps every package.json, which had all sat at 0.1.0 while real releases shipped as v1.x. Pre-migrate backup - deploy/scripts/pre-migrate-backup.mjs dumps the database from INSIDE the still-running old API container over Portainer's Docker API, so the file lands in the volume the Operaciones restore screen reads. A dump taken on the CI runner would be unreachable by the only restore path we have. Verifies the artefact with `gzip -t` before letting the migration proceed. galactus - deploy/galactus/*.compose.yml: standalone-Docker ports of the Swarm stacks. Plain compose silently ignores `deploy:`, so restart_policy becomes `restart: unless-stopped` — without it nothing returns after a host reboot. - .gitea/workflows/deploy-galactus.yml drives endpoint 3 with its own secrets. Fixes - deploy.yml passed `endpoint_id` and `pull_image` to cssnr/portainer-stack-deploy-action, which has no such inputs (they are `endpoint` and `pull`). The endpoint was silently never set. docs/DEPLOY_AND_MIGRATIONS.md documents expand/contract as the rule for schema changes: Prisma has no down-migrations, so a code rollback never rolls the schema back, and restoring the replication master from a dump diverges every replica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |