US08-02: Build a Reproducible Application Image (#97)
This commit was merged in pull request #97.
This commit is contained in:
22
docker/entrypoint.sh
Executable file
22
docker/entrypoint.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# One entrypoint, one role per container (US08-02).
|
||||
#
|
||||
# The first argument is the management command the image runs — `serve` and `worker`
|
||||
# are the two roles, and every other `python -m photo_pipeline` command (migrate,
|
||||
# diagnostics, backup, restore, dry-run) is passed through unchanged so operating the
|
||||
# container is operating the same CLI. No supervisor: two roles in one container would
|
||||
# share a process lock they are each meant to hold alone (US07-05).
|
||||
set -eu
|
||||
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
echo "refusing to run as root: start this image with a non-root UID/GID so files" \
|
||||
"it renames or writes keep the ownership the mounted library expects" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
role="${1:-serve}"
|
||||
# The health check has to know which role it is checking, and only the API has an
|
||||
# endpoint to check. /tmp is writable for the unprivileged user; /run may not be.
|
||||
printf '%s' "${role}" > "${PHOTO_PIPELINE_ROLE_FILE:-/tmp/photo-pipeline-role}" 2>/dev/null || true
|
||||
|
||||
exec python -m photo_pipeline "$@"
|
||||
72
docker/fetch-immich-go.py
Normal file
72
docker/fetch-immich-go.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Download one pinned immich-go release and verify it before unpacking (US08-02).
|
||||
|
||||
Run at image build time by the `uploader` stage, with the interpreter that is already
|
||||
in the base image: no curl, no wget, and no download tooling in the layer that ships.
|
||||
The checksum is not advisory — a release asset that does not match the pinned digest
|
||||
is a failed build, not a warning, because the uploader's flags and report format are
|
||||
what the upload parser is written against (concept §15).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import platform
|
||||
import tarfile
|
||||
import tempfile
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
RELEASE_URL = "https://github.com/simulot/immich-go/releases/download/v{version}/{asset}"
|
||||
# Debian/BuildKit architecture as the interpreter sees it → release asset name.
|
||||
ASSETS = {
|
||||
"x86_64": ("immich-go_Linux_x86_64.tar.gz", "amd64"),
|
||||
"amd64": ("immich-go_Linux_x86_64.tar.gz", "amd64"),
|
||||
"aarch64": ("immich-go_Linux_arm64.tar.gz", "arm64"),
|
||||
"arm64": ("immich-go_Linux_arm64.tar.gz", "arm64"),
|
||||
}
|
||||
TIMEOUT_SECONDS = 300
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--version", required=True, help="immich-go release, without the v")
|
||||
parser.add_argument("--sha256-amd64", required=True)
|
||||
parser.add_argument("--sha256-arm64", required=True)
|
||||
parser.add_argument("--into", default="/usr/local/bin")
|
||||
args = parser.parse_args()
|
||||
|
||||
machine = platform.machine().lower()
|
||||
if machine not in ASSETS:
|
||||
raise SystemExit(f"unsupported architecture: {machine}")
|
||||
asset, arch = ASSETS[machine]
|
||||
expected = {"amd64": args.sha256_amd64, "arm64": args.sha256_arm64}[arch]
|
||||
url = RELEASE_URL.format(version=args.version, asset=asset)
|
||||
|
||||
with urllib.request.urlopen(url, timeout=TIMEOUT_SECONDS) as response: # noqa: S310
|
||||
payload = response.read()
|
||||
digest = hashlib.sha256(payload).hexdigest()
|
||||
if digest != expected:
|
||||
raise SystemExit(f"checksum mismatch for {url}: {digest} != {expected}")
|
||||
|
||||
target = Path(args.into)
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory() as work:
|
||||
archive = Path(work) / asset
|
||||
archive.write_bytes(payload)
|
||||
with tarfile.open(archive) as tar:
|
||||
member = tar.getmember("immich-go")
|
||||
# Extract exactly the one file this pin is about, by name, so nothing
|
||||
# else in the archive can decide where it lands.
|
||||
extracted = tar.extractfile(member)
|
||||
if extracted is None:
|
||||
raise SystemExit("release archive contains no immich-go binary")
|
||||
binary = target / "immich-go"
|
||||
binary.write_bytes(extracted.read())
|
||||
binary.chmod(0o755)
|
||||
print(f"immich-go {args.version} ({arch}) verified {digest}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
34
docker/healthcheck.sh
Executable file
34
docker/healthcheck.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
# Container health for the `serve` role: readiness, not liveness (US08-02).
|
||||
#
|
||||
# /api/v1/health/ready is 503 until the database is reachable, migrated, and in WAL
|
||||
# mode with foreign keys on, so an unmigrated or misconfigured container never reports
|
||||
# healthy. Health endpoints need no session and no access secret, which is what lets an
|
||||
# orchestrator restart a container it holds no credentials for (US08-01).
|
||||
set -eu
|
||||
|
||||
role="$(cat "${PHOTO_PIPELINE_ROLE_FILE:-/tmp/photo-pipeline-role}" 2>/dev/null || echo unknown)"
|
||||
if [ "${role}" != "serve" ]; then
|
||||
# ponytail: the worker has no endpoint to probe; its liveness is its lease and job
|
||||
# heartbeat in the database. Add a `worker --health` command if a restart policy
|
||||
# ever needs to act on it.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
port="${PHOTO_PIPELINE_PORT:-8000}"
|
||||
exec python - "${port}" <<'PY'
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
url = f"http://127.0.0.1:{sys.argv[1]}/api/v1/health/ready"
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=5) as response: # noqa: S310 — loopback
|
||||
sys.exit(0 if response.status == 200 else 1)
|
||||
except urllib.error.HTTPError as error:
|
||||
print(f"not ready: HTTP {error.code}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except OSError as error:
|
||||
print(f"not ready: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
Reference in New Issue
Block a user