[ci] Auto-sync modules/docker-registry/* to registry VM + runbook docs
Replaces the manual scp+bounce sequence that landed registry:2.8.3 on
10.0.20.10 today (see commit 7cb44d72 + nginx-DNS-trap in runbook).
Addresses the "no repeat manual fixes" preference — future changes to
docker-compose.yml / fix-broken-blobs.sh / nginx_registry.conf /
config-private.yml / cleanup-tags.sh now deploy through CI.
Pipeline (.woodpecker/registry-config-sync.yml) mirrors
pve-nfs-exports-sync.yml: ssh-keyscan pin, scp the whole managed set,
bounce compose only when compose-visible files changed, always restart
nginx after a compose bounce (critical — nginx caches upstream DNS), end
with a dry-run fix-broken-blobs.sh to catch regressions.
Credentials:
- Woodpecker repo-secret `registry_ssh_key` (events: push, manual)
- Mirror at Vault `secret/woodpecker/registry_ssh_key`
(private_key / public_key / known_hosts_entry)
- Public key on /root/.ssh/authorized_keys on 10.0.20.10
- Key label: woodpecker-registry-config-sync
Runbook updated with "Auto-sync pipeline" section pointing at the new
flow + manual override command.
Closes: code-3vl
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a641dc744f
commit
34ee282d88
2 changed files with 184 additions and 0 deletions
156
.woodpecker/registry-config-sync.yml
Normal file
156
.woodpecker/registry-config-sync.yml
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# Sync modules/docker-registry/* → /opt/registry/ on docker-registry VM
|
||||
# (10.0.20.10) on change, and bounce containers + nginx when needed.
|
||||
#
|
||||
# Replaces the manual "ssh + scp + docker compose up -d" that was required
|
||||
# after the 2026-04-19 `registry:2 → registry:2.8.3` pin landed. The deploy
|
||||
# flow is now: edit a file in modules/docker-registry/ → git push → this
|
||||
# pipeline runs → registry VM picks up the change.
|
||||
#
|
||||
# Trigger: push to master that touches any managed file (see `when.path`),
|
||||
# or a manual run via Woodpecker UI / API.
|
||||
#
|
||||
# Credentials:
|
||||
# - registry_ssh_key: Woodpecker repo-secret (ed25519 keypair provisioned
|
||||
# 2026-04-19 as `woodpecker-registry-config-sync`). Public key lives in
|
||||
# /root/.ssh/authorized_keys on 10.0.20.10. Private key mirrored in
|
||||
# Vault `secret/woodpecker/registry_ssh_key` (subkeys private_key /
|
||||
# public_key / known_hosts_entry) for recovery.
|
||||
#
|
||||
# Why bounce nginx every time: nginx caches upstream DNS at startup, so if
|
||||
# any registry-* container gets recreated (new IP on the docker bridge),
|
||||
# nginx keeps forwarding to a stale address. Always restart nginx as the
|
||||
# last step — see docs/runbooks/registry-vm.md § "Bouncing registry
|
||||
# containers — the nginx DNS trap".
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: master
|
||||
path:
|
||||
include:
|
||||
- 'modules/docker-registry/docker-compose.yml'
|
||||
- 'modules/docker-registry/fix-broken-blobs.sh'
|
||||
- 'modules/docker-registry/cleanup-tags.sh'
|
||||
- 'modules/docker-registry/nginx_registry.conf'
|
||||
- 'modules/docker-registry/config-private.yml'
|
||||
- event: manual
|
||||
|
||||
clone:
|
||||
git:
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
depth: 1
|
||||
attempts: 3
|
||||
|
||||
steps:
|
||||
- name: deploy
|
||||
image: alpine:3.20
|
||||
environment:
|
||||
REGISTRY_SSH_KEY:
|
||||
from_secret: registry_ssh_key
|
||||
commands:
|
||||
- apk add --no-cache openssh-client rsync
|
||||
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||
- printf '%s\n' "$REGISTRY_SSH_KEY" > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
# Pin host key — CI's ~/.ssh/known_hosts is ephemeral, so accept-new on first pull.
|
||||
- ssh-keyscan -t ed25519 10.0.20.10 >> ~/.ssh/known_hosts 2>/dev/null
|
||||
- echo '---detecting changed files---'
|
||||
- |
|
||||
# Mirror the remote state of each file so we can diff and decide what bounces.
|
||||
CHANGED=""
|
||||
for f in docker-compose.yml fix-broken-blobs.sh cleanup-tags.sh nginx_registry.conf config-private.yml; do
|
||||
LOCAL="modules/docker-registry/$f"
|
||||
REMOTE="/opt/registry/$f"
|
||||
if [ ! -f "$LOCAL" ]; then
|
||||
echo "skip $f (not in repo)"
|
||||
continue
|
||||
fi
|
||||
# Pull the remote copy into /tmp for a diff. ssh -n avoids stdin-hogging.
|
||||
REMOTE_CONTENT=$(ssh -n -o BatchMode=yes root@10.0.20.10 "cat $REMOTE 2>/dev/null || true")
|
||||
LOCAL_CONTENT=$(cat "$LOCAL")
|
||||
if [ "$LOCAL_CONTENT" = "$REMOTE_CONTENT" ]; then
|
||||
echo "unchanged: $f"
|
||||
else
|
||||
echo "---diff: $f ---"
|
||||
echo "$REMOTE_CONTENT" > /tmp/remote.txt
|
||||
diff -u /tmp/remote.txt "$LOCAL" | head -40 || true
|
||||
CHANGED="$CHANGED $f"
|
||||
fi
|
||||
done
|
||||
echo "CHANGED_FILES=$CHANGED"
|
||||
printf '%s' "$CHANGED" > /tmp/changed
|
||||
- echo '---applying---'
|
||||
- |
|
||||
CHANGED=$(cat /tmp/changed)
|
||||
if [ -z "$CHANGED" ]; then
|
||||
echo "No files changed — exiting cleanly (manual run with no drift)."
|
||||
exit 0
|
||||
fi
|
||||
# Ship every managed file unconditionally — scp is cheap, idempotency is safe.
|
||||
scp -o BatchMode=yes \
|
||||
modules/docker-registry/docker-compose.yml \
|
||||
modules/docker-registry/fix-broken-blobs.sh \
|
||||
modules/docker-registry/cleanup-tags.sh \
|
||||
modules/docker-registry/nginx_registry.conf \
|
||||
modules/docker-registry/config-private.yml \
|
||||
root@10.0.20.10:/opt/registry/
|
||||
ssh -n -o BatchMode=yes root@10.0.20.10 '
|
||||
chmod +x /opt/registry/fix-broken-blobs.sh /opt/registry/cleanup-tags.sh
|
||||
'
|
||||
- echo '---bouncing containers + nginx---'
|
||||
- |
|
||||
CHANGED=$(cat /tmp/changed)
|
||||
# Compose-visible files: docker-compose.yml (image tag, mounts) and
|
||||
# config-private.yml (registry config → needs registry-private reload).
|
||||
BOUNCE_COMPOSE=0
|
||||
BOUNCE_NGINX=0
|
||||
echo "$CHANGED" | grep -q "docker-compose.yml" && BOUNCE_COMPOSE=1
|
||||
echo "$CHANGED" | grep -q "config-private.yml" && BOUNCE_COMPOSE=1
|
||||
echo "$CHANGED" | grep -q "nginx_registry.conf" && BOUNCE_NGINX=1
|
||||
|
||||
if [ "$BOUNCE_COMPOSE" = "1" ]; then
|
||||
echo "compose-visible change → pull + up -d"
|
||||
ssh -n -o BatchMode=yes root@10.0.20.10 '
|
||||
cd /opt/registry
|
||||
docker compose pull 2>&1 | tail -5
|
||||
docker compose up -d 2>&1 | tail -20
|
||||
'
|
||||
# Any compose recreate requires nginx DNS refresh too.
|
||||
BOUNCE_NGINX=1
|
||||
fi
|
||||
|
||||
if [ "$BOUNCE_NGINX" = "1" ]; then
|
||||
echo "bouncing nginx to flush upstream DNS cache"
|
||||
ssh -n -o BatchMode=yes root@10.0.20.10 '
|
||||
docker restart registry-nginx
|
||||
sleep 3
|
||||
docker ps --format "{{.Names}}\t{{.Image}}\t{{.Status}}" | grep -E "registry-"
|
||||
'
|
||||
fi
|
||||
|
||||
if [ "$BOUNCE_COMPOSE" = "0" ] && [ "$BOUNCE_NGINX" = "0" ]; then
|
||||
echo "only script files changed (cron-picks-up semantics) — no bounce needed"
|
||||
fi
|
||||
- echo '---verify---'
|
||||
- |
|
||||
ssh -n -o BatchMode=yes root@10.0.20.10 '
|
||||
echo "=== catalog ==="
|
||||
# Prove auth + routing survived.
|
||||
curl -sk -o /dev/null -w "catalog (unauth → 401 expected): HTTP %{http_code}\n" \
|
||||
https://127.0.0.1:5050/v2/
|
||||
echo "=== integrity scan (dry-run) ==="
|
||||
python3 /opt/registry/fix-broken-blobs.sh --dry-run 2>&1 | tail -5
|
||||
'
|
||||
|
||||
- name: slack
|
||||
image: curlimages/curl:8.11.0
|
||||
environment:
|
||||
SLACK_WEBHOOK:
|
||||
from_secret: slack_webhook
|
||||
commands:
|
||||
- |
|
||||
curl -s -X POST -H 'Content-type: application/json' \
|
||||
--data "{\"channel\":\"general\",\"text\":\"Registry config sync on 10.0.20.10: ${CI_PIPELINE_STATUS}\"}" \
|
||||
"$SLACK_WEBHOOK" || true
|
||||
when:
|
||||
status: [success, failure]
|
||||
|
|
@ -140,6 +140,34 @@ ssh root@10.0.20.10 '
|
|||
'
|
||||
```
|
||||
|
||||
## Auto-sync pipeline
|
||||
|
||||
Changes to `modules/docker-registry/{docker-compose.yml, fix-broken-blobs.sh,
|
||||
cleanup-tags.sh, nginx_registry.conf, config-private.yml}` deploy
|
||||
automatically via `.woodpecker/registry-config-sync.yml`:
|
||||
|
||||
- Fires on `push` to master touching any of those paths, or via `manual`
|
||||
event (Woodpecker UI / API).
|
||||
- SCPs every managed file to `/opt/registry/` on `10.0.20.10`.
|
||||
- Bounces containers + nginx when a compose-visible file changed; leaves
|
||||
them alone when only scripts changed (cron picks up automatically).
|
||||
- Runs a dry-run `fix-broken-blobs.sh` at the end to verify the registry
|
||||
is still coherent.
|
||||
|
||||
SSH credentials: Woodpecker repo-secret `registry_ssh_key` (ed25519,
|
||||
provisioned 2026-04-19). Public key at `/root/.ssh/authorized_keys` on
|
||||
`10.0.20.10`. Private key mirrored at `secret/woodpecker/registry_ssh_key`
|
||||
in Vault (subkeys `private_key` / `public_key` / `known_hosts_entry`).
|
||||
|
||||
Manual override if you need to sync right now:
|
||||
|
||||
```sh
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||
"https://ci.viktorbarzin.me/api/repos/1/pipelines" \
|
||||
-d '{"branch":"master"}' | jq .number
|
||||
```
|
||||
|
||||
## Bouncing registry containers — the nginx DNS trap
|
||||
|
||||
`docker compose up -d` on `/opt/registry/docker-compose.yml` recreates
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue