Add Makefile and bin/ scripts for one-command multi-arch release
make release builds, smoke-tests and pushes amd64 (on fsn1app1 via DOCKER_HOST) and arm64 (local), joins them into latest and dated multi-arch manifests, and commits the day's logs. Logic lives in bin/; the Makefile only wires dependencies so -j2 runs the arches in parallel.
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
# Build and publish the base image. All logic lives in bin/; targets here
|
||||||
|
# only wire dependencies and export configuration.
|
||||||
|
#
|
||||||
|
# make release build, test, push both arches, tag, commit logs
|
||||||
|
# make -j2 release same, with the two arches in parallel
|
||||||
|
# make build build for this machine's arch only
|
||||||
|
# make test run the image smoke test for this machine's arch
|
||||||
|
|
||||||
|
IMAGE ?= git.eeqj.de/sneak/baseimage
|
||||||
|
DATE ?= $(shell date +%Y-%m-%d)
|
||||||
|
ARCHES ?= amd64 arm64
|
||||||
|
|
||||||
|
# Docker daemon to use per arch. Empty means the local daemon.
|
||||||
|
DOCKER_HOST_amd64 ?= ssh://root@fsn1app1
|
||||||
|
DOCKER_HOST_arm64 ?=
|
||||||
|
|
||||||
|
NATIVE_ARCH := $(shell bin/native-arch)
|
||||||
|
|
||||||
|
export IMAGE DATE ARCHES DOCKER_HOST_amd64 DOCKER_HOST_arm64
|
||||||
|
|
||||||
|
.PHONY: release manifest commit-logs build test push clean \
|
||||||
|
$(ARCHES:%=build-%) $(ARCHES:%=test-%) $(ARCHES:%=push-%)
|
||||||
|
|
||||||
|
release: commit-logs
|
||||||
|
|
||||||
|
commit-logs: manifest
|
||||||
|
bin/commit-logs
|
||||||
|
|
||||||
|
manifest: $(ARCHES:%=push-%)
|
||||||
|
bin/manifest
|
||||||
|
|
||||||
|
# Static pattern rules: plain pattern rules are skipped for .PHONY targets.
|
||||||
|
$(ARCHES:%=push-%): push-%: test-%
|
||||||
|
bin/push $*
|
||||||
|
|
||||||
|
$(ARCHES:%=test-%): test-%: build-%
|
||||||
|
bin/test $*
|
||||||
|
|
||||||
|
$(ARCHES:%=build-%): build-%:
|
||||||
|
bin/build $*
|
||||||
|
|
||||||
|
build: build-$(NATIVE_ARCH)
|
||||||
|
test: test-$(NATIVE_ARCH)
|
||||||
|
push: push-$(NATIVE_ARCH)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
bin/clean
|
||||||
@@ -131,32 +131,31 @@ Installation leaves nothing behind that production does not need:
|
|||||||
Development headers (`-dev` packages) are deliberately kept: they are
|
Development headers (`-dev` packages) are deliberately kept: they are
|
||||||
what child images need to build things.
|
what child images need to build things.
|
||||||
|
|
||||||
## Building
|
## Building and releasing
|
||||||
|
|
||||||
Native architecture only:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker build -t git.eeqj.de/sneak/baseimage:latest .
|
make release # build, test, push both arches, tag, commit logs
|
||||||
|
make -j2 release # same, with the two arches in parallel
|
||||||
|
make build # build for this machine's arch only
|
||||||
|
make test # smoke-test this machine's build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Release build
|
Each architecture is built natively on a daemon of that architecture: arm64
|
||||||
|
on the local daemon, amd64 on `fsn1app1` via `DOCKER_HOST`. The daemon per
|
||||||
|
arch is set by `DOCKER_HOST_amd64` and `DOCKER_HOST_arm64` in the
|
||||||
|
`Makefile` and can be overridden on the command line. The release date
|
||||||
|
defaults to today and can be overridden with `DATE=YYYY-MM-DD`.
|
||||||
|
|
||||||
Each architecture is built natively on a host of that architecture (arm64
|
`make release` runs, per arch, `bin/build`, `bin/test`, and `bin/push`,
|
||||||
locally, amd64 on `fsn1app1` via `DOCKER_HOST=ssh://root@fsn1app1`), pushed
|
then `bin/manifest` to join the per-arch tags into `latest` and the dated
|
||||||
under a per-arch tag, and then joined into a multi-arch manifest:
|
tag, then `bin/commit-logs` to commit `logs/YYYY-MM-DD/` and push the repo.
|
||||||
|
`bin/test` checks that the toolchains resolve, yarn and pnpm work offline,
|
||||||
|
nothing is left in `/tmp`, and a runit service is stopped cleanly by
|
||||||
|
`docker stop` with exit code 0.
|
||||||
|
|
||||||
```sh
|
Registry login is done once on the local docker client with
|
||||||
# on each host, for its own arch
|
`docker login git.eeqj.de`; the client forwards the credentials to the
|
||||||
docker build --progress=plain -t baseimage:dev . 2>&1 | tee logs/YYYY-MM-DD/build-ARCH.log
|
remote daemon on push.
|
||||||
docker tag baseimage:dev git.eeqj.de/sneak/baseimage:ARCH
|
|
||||||
docker push git.eeqj.de/sneak/baseimage:ARCH
|
|
||||||
|
|
||||||
# once both per-arch tags are in the registry
|
|
||||||
docker buildx imagetools create \
|
|
||||||
-t git.eeqj.de/sneak/baseimage:latest \
|
|
||||||
-t git.eeqj.de/sneak/baseimage:YYYY-MM-DD \
|
|
||||||
git.eeqj.de/sneak/baseimage:amd64 git.eeqj.de/sneak/baseimage:arm64
|
|
||||||
```
|
|
||||||
|
|
||||||
Tags:
|
Tags:
|
||||||
|
|
||||||
@@ -184,6 +183,9 @@ Interactive shells (login shells, or anything that sources
|
|||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
|
- `Makefile` — target wiring and configuration only; no logic.
|
||||||
|
- `bin/` — one script per step: `build`, `test`, `push`, `manifest`,
|
||||||
|
`commit-logs`, `clean`, plus `lib.sh` (shared setup) and `native-arch`.
|
||||||
- `Dockerfile` — base, environment, one `COPY`, one `ADD`, one `RUN`.
|
- `Dockerfile` — base, environment, one `COPY`, one `ADD`, one `RUN`.
|
||||||
- `build.sh` — everything the `RUN` does, in order: apt, Go, runsvinit,
|
- `build.sh` — everything the `RUN` does, in order: apt, Go, runsvinit,
|
||||||
golangci-lint, Hugo, nvm/Node/corepack/wrangler, pyenv/Python, cleanup,
|
golangci-lint, Hugo, nvm/Node/corepack/wrangler, pyenv/Python, cleanup,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build the image for one arch on that arch's daemon, logging to logs/DATE/.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
. bin/lib.sh
|
||||||
|
|
||||||
|
arch="${1:?usage: bin/build <arch>}"
|
||||||
|
use_arch_host "$arch"
|
||||||
|
|
||||||
|
docker build --progress=plain -t "$(dev_tag "$arch")" . 2>&1 |
|
||||||
|
tee "$LOGDIR/build-$arch.log"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Remove dev-tagged images from every arch's daemon.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
. bin/lib.sh
|
||||||
|
|
||||||
|
for arch in ${ARCHES:-amd64 arm64}; do
|
||||||
|
use_arch_host "$arch" || continue
|
||||||
|
docker rmi -f "$(dev_tag "$arch")" 2>/dev/null || true
|
||||||
|
done
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Commit logs/DATE/ and push to origin.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
. bin/lib.sh
|
||||||
|
|
||||||
|
git add "$LOGDIR"
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "no new logs to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "Release $DATE: build and push logs"
|
||||||
|
git push origin HEAD
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
# Shared setup for bin/ scripts. Source after `set -euo pipefail`.
|
||||||
|
#
|
||||||
|
# Expects IMAGE and DATE in the environment (the Makefile exports them) and
|
||||||
|
# takes the target arch as $1 where relevant.
|
||||||
|
|
||||||
|
: "${IMAGE:=git.eeqj.de/sneak/baseimage}"
|
||||||
|
: "${DATE:=$(date +%Y-%m-%d)}"
|
||||||
|
|
||||||
|
LOGDIR="logs/$DATE"
|
||||||
|
mkdir -p "$LOGDIR"
|
||||||
|
|
||||||
|
# Point docker at the daemon for the given arch. DOCKER_HOST_<arch> empty or
|
||||||
|
# unset means the local daemon.
|
||||||
|
use_arch_host() {
|
||||||
|
local arch="$1" var host
|
||||||
|
var="DOCKER_HOST_${arch}"
|
||||||
|
host="${!var:-}"
|
||||||
|
if [ -n "$host" ]; then
|
||||||
|
export DOCKER_HOST="$host"
|
||||||
|
else
|
||||||
|
unset DOCKER_HOST
|
||||||
|
fi
|
||||||
|
local actual
|
||||||
|
actual="$(docker version --format '{{.Server.Arch}}')"
|
||||||
|
if [ "$actual" != "$arch" ]; then
|
||||||
|
echo "error: daemon for $arch (${host:-local}) reports arch $actual" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dev_tag() { echo "baseimage:dev-$1"; }
|
||||||
|
arch_tag() { echo "$IMAGE:$1"; }
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Join the per-arch tags into multi-arch manifests IMAGE:latest and
|
||||||
|
# IMAGE:DATE, and record what was published.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
. bin/lib.sh
|
||||||
|
unset DOCKER_HOST
|
||||||
|
|
||||||
|
refs=()
|
||||||
|
for arch in ${ARCHES:-amd64 arm64}; do
|
||||||
|
refs+=("$(arch_tag "$arch")")
|
||||||
|
done
|
||||||
|
|
||||||
|
docker buildx imagetools create \
|
||||||
|
-t "$IMAGE:latest" -t "$IMAGE:$DATE" "${refs[@]}"
|
||||||
|
|
||||||
|
docker buildx imagetools inspect "$IMAGE:$DATE" | tee "$LOGDIR/manifest.txt"
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Print this machine's arch in docker naming (amd64 or arm64).
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64 | amd64) echo amd64 ;;
|
||||||
|
aarch64 | arm64) echo arm64 ;;
|
||||||
|
*) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tag the built image IMAGE:<arch> and push it from that arch's daemon.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
. bin/lib.sh
|
||||||
|
|
||||||
|
arch="${1:?usage: bin/push <arch>}"
|
||||||
|
use_arch_host "$arch"
|
||||||
|
|
||||||
|
docker tag "$(dev_tag "$arch")" "$(arch_tag "$arch")"
|
||||||
|
docker push "$(arch_tag "$arch")" 2>&1 | tee "$LOGDIR/push-$arch.log"
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke-test a built image: toolchains resolve, yarn/pnpm work offline,
|
||||||
|
# nothing left in /tmp, and runsvinit stops a runit service cleanly on
|
||||||
|
# docker stop with exit code 0.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
. bin/lib.sh
|
||||||
|
|
||||||
|
arch="${1:?usage: bin/test <arch>}"
|
||||||
|
use_arch_host "$arch"
|
||||||
|
img="$(dev_tag "$arch")"
|
||||||
|
name="baseimage-test-$arch-$$"
|
||||||
|
|
||||||
|
echo "== $img: toolchains and cleanup"
|
||||||
|
docker run --rm "$img" bash -ec '
|
||||||
|
# cleanup checks first: node writes a compile cache into /tmp when run
|
||||||
|
test -z "$(ls -A /tmp)"
|
||||||
|
test ! -e /usr/share/doc/bash
|
||||||
|
test ! -e /tmp/build.sh
|
||||||
|
go version; node --version; python3 --version; hugo version | head -1
|
||||||
|
COREPACK_ENABLE_NETWORK=0 yarn --version
|
||||||
|
COREPACK_ENABLE_NETWORK=0 pnpm --version
|
||||||
|
'
|
||||||
|
|
||||||
|
echo "== $img: runit service lifecycle"
|
||||||
|
docker rm -f "$name" >/dev/null 2>&1 || true
|
||||||
|
docker run -d --name "$name" "$img" bash -ec '
|
||||||
|
mkdir -p /etc/sv/demo
|
||||||
|
printf "%s\n" "#!/bin/bash" "exec 2>&1" \
|
||||||
|
"trap \"echo demo: got TERM; sleep 1; echo demo: clean exit; exit 0\" TERM" \
|
||||||
|
"echo demo: started" "while true; do sleep 1; done" > /etc/sv/demo/run
|
||||||
|
chmod +x /etc/sv/demo/run
|
||||||
|
ln -s /etc/sv/demo /etc/service/demo
|
||||||
|
exec /usr/local/sbin/runsvinit
|
||||||
|
' >/dev/null
|
||||||
|
sleep 3
|
||||||
|
docker exec "$name" sv status demo
|
||||||
|
docker stop "$name" >/dev/null
|
||||||
|
code="$(docker inspect "$name" --format '{{.State.ExitCode}}')"
|
||||||
|
logs="$(docker logs "$name" 2>&1)"
|
||||||
|
docker rm "$name" >/dev/null
|
||||||
|
echo "$logs"
|
||||||
|
if [ "$code" != 0 ] || ! grep -q 'demo: clean exit' <<<"$logs"; then
|
||||||
|
echo "error: graceful stop failed (exit $code)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "== $img: ok"
|
||||||
Reference in New Issue
Block a user