diff --git a/playbooks/update-stack-image.yaml b/playbooks/update-stack-image.yaml new file mode 100644 index 0000000..73179a4 --- /dev/null +++ b/playbooks/update-stack-image.yaml @@ -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//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 `, 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"