playbooks: generic per-service stack image update (pull + recreate + verify)

Adds playbooks/update-stack-image.yaml — pull the newest image for one
compose stack service and recreate it, with a verify phase that asserts
the container's image id equals what the tag now resolves to rather than
trusting a 'Up' line from docker ps.

Scoped to a single service on purpose: the recreate is 'up -d <service>',
never a bare 'up -d', which would recreate every service in the project.

Go template format strings are written bare; elway's {{ identifier }}
substitution leaves them alone, but {{end}} / {{else}} would match and
die as undefined variables, so the health read uses {{json .State.Health}}
instead of an if/else.

First use: drawio on esh-docker-vm, 28.1.2 -> 31.4.6.
This commit is contained in:
vh
2026-09-16 16:34:16 -07:00
parent e8086941e2
commit a5745dcf72
+86
View File
@@ -0,0 +1,86 @@
# Pull the newest image for one compose stack and recreate it.
#
# The generic "update a stack that tracks a floating tag" playbook. Works for
# any stack laid out per the house convention
# (`/opt/docker/compose/<stack>/compose.yaml`).
#
# Usage:
# scripts/elway esh-docker-vm --playbook playbooks/update-stack-image.yaml \
# --var stack=drawio
#
# `service` and `container` default to `stack`. Override them when the
# compose service name or `container_name:` differs from the directory name.
#
# ⚠ SCOPED TO ONE SERVICE ON PURPOSE. The recreate is `up -d <service>`, never
# a bare `up -d` — a bare one recreates every service in the project
# (auto-memory `feedback_compose_up_recreates_whole_stack`). A stack that
# really needs all its services bounced gets one run per service.
#
# ⚠ For stacks tracking a FLOATING tag (`:latest` and friends). A SHA-pinned
# stack has nothing to pull; updating that means moving the pin, which is a
# deliberate edit and not this.
#
# ⚠ Go template format strings are written bare (`{{.Image}}`). elway's
# substitution only matches `{{ identifier }}`, so those pass through
# untouched — but `{{end}}` / `{{else}}` DO match and would die as undefined
# variables. Hence `{{json .State.Health}}` below instead of an if/else.
vars:
compose_root: /opt/docker/compose
stack: ""
service: ""
container: ""
steps:
- name: Refuse to run without a stack name
shell: test -n "{{ stack }}"
changed_when: "false"
- name: Record the image the service runs BEFORE the pull
# Written to a file, not just echoed, so the verify phase can compare
# against it — an "updated" claim nobody can falsify is not a result.
shell: |
ctr="{{ container }}"; [ -n "$ctr" ] || ctr="{{ service }}"; [ -n "$ctr" ] || ctr="{{ stack }}"
docker inspect "$ctr" --format '{{.Image}}' | tee /tmp/elway-update-{{ stack }}.before
changed_when: "false"
- name: Pull the newest image for the service
shell: |
svc="{{ service }}"; [ -n "$svc" ] || svc="{{ stack }}"
cd {{ compose_root }}/{{ stack }}
docker compose pull "$svc"
- name: Recreate the service on the pulled image
shell: |
svc="{{ service }}"; [ -n "$svc" ] || svc="{{ stack }}"
cd {{ compose_root }}/{{ stack }}
docker compose up -d "$svc"
verify:
- name: Service runs the image its tag now resolves to
# `docker ps` saying "Up" only proves something is running. This asserts
# the container's image id equals what the tag resolves to right now, and
# reports whether that is actually a move off the pre-pull image.
shell: |
svc="{{ service }}"; [ -n "$svc" ] || svc="{{ stack }}"
ctr="{{ container }}"; [ -n "$ctr" ] || ctr="$svc"
cd {{ compose_root }}/{{ stack }}
tag=$(docker compose config --images "$svc" | head -1)
want=$(docker image inspect "$tag" --format '{{.Id}}')
have=$(docker inspect "$ctr" --format '{{.Image}}')
test "$want" = "$have" || { echo "MISMATCH: $ctr runs $have, $tag is $want"; exit 1; }
before=$(cat /tmp/elway-update-{{ stack }}.before 2>/dev/null)
if [ "$before" = "$have" ]; then
echo "already current: $tag == $have (nothing newer upstream)"
else
echo "updated: $before -> $have ($tag)"
fi
changed_when: "false"
- name: Container is up, and healthy if it declares a healthcheck
shell: |
ctr="{{ container }}"; [ -n "$ctr" ] || ctr="{{ service }}"; [ -n "$ctr" ] || ctr="{{ stack }}"
docker inspect "$ctr" --format 'status={{.State.Status}} started={{.State.StartedAt}}'
echo "health=$(docker inspect "$ctr" --format '{{json .State.Health}}' | head -c 120)"
test "$(docker inspect "$ctr" --format '{{.State.Status}}')" = running
changed_when: "false"