Fix health endpoint, align with the documented manual runbook
The health router is mounted at prefix="/api" in domaindingo's main.py, so /health 404s. Checking it would have failed every deploy and rolled back a perfectly good image. Also adopts three conventions from the vault's DomainDingo Development note: recreate a single named service (--no-deps --force-recreate), validate with compose config --quiet before touching the running container, and write the rollback record in the documented format and location.
This commit is contained in:
+61
-18
@@ -236,14 +236,19 @@ readonly DEPLOY_IMAGE
|
|||||||
|
|
||||||
# --------------------------------------------- remember what is running now ---
|
# --------------------------------------------- remember what is running now ---
|
||||||
|
|
||||||
PREVIOUS_IMAGE="$(
|
RunningImageId="$(docker inspect --format '{{.Image}}' "${CD_CONTAINER}" 2>/dev/null || true)"
|
||||||
container_image="$(docker inspect --format '{{.Image}}' "$CD_CONTAINER" 2>/dev/null || true)"
|
PREVIOUS_IMAGE=""
|
||||||
[[ -n "$container_image" ]] && image_digest_ref "$container_image" || true
|
PREVIOUS_REVISION=""
|
||||||
)"
|
if [[ -n "${RunningImageId}" ]]; then
|
||||||
readonly PREVIOUS_IMAGE
|
PREVIOUS_IMAGE="$(image_digest_ref "${RunningImageId}")"
|
||||||
|
PREVIOUS_REVISION="$(docker image inspect \
|
||||||
|
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \
|
||||||
|
"${RunningImageId}" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
readonly PREVIOUS_IMAGE PREVIOUS_REVISION
|
||||||
|
|
||||||
if [[ -n "$PREVIOUS_IMAGE" ]]; then
|
if [[ -n "${PREVIOUS_IMAGE}" ]]; then
|
||||||
log "current ${PREVIOUS_IMAGE}"
|
log "current ${PREVIOUS_IMAGE} (revision ${PREVIOUS_REVISION:0:12})"
|
||||||
else
|
else
|
||||||
log "current (nothing running -- first deploy, or container absent)"
|
log "current (nothing running -- first deploy, or container absent)"
|
||||||
fi
|
fi
|
||||||
@@ -251,15 +256,44 @@ fi
|
|||||||
# ----------------------------------------------------------------- deploy ---
|
# ----------------------------------------------------------------- deploy ---
|
||||||
|
|
||||||
compose_up() {
|
compose_up() {
|
||||||
local image="$1"
|
local Image="$1"
|
||||||
# `up -d` recreates only what actually changed. `down` is deliberately not
|
# Matches the manual runbook in the vault's "DomainDingo Development" note:
|
||||||
# used here: it causes avoidable downtime, and `down -v` would destroy the
|
# recreate exactly one service, leave its dependencies alone, and never run
|
||||||
# data volumes these stacks depend on.
|
# `docker compose down` -- that causes avoidable downtime, and `down -v`
|
||||||
( cd "$CD_STACK_DIR" \
|
# would destroy the bind-mounted data these stacks depend on.
|
||||||
&& env "${CD_IMAGE_ENV_VAR}=${image}" \
|
#
|
||||||
"${CD_PULL_POLICY_ENV_VAR}=missing" \
|
# `--pull always` is deliberately absent: the image is already pinned to a
|
||||||
timeout "$CD_COMPOSE_TIMEOUT_SECONDS" \
|
# digest and pulled, so there is nothing left to resolve.
|
||||||
docker compose -f "$CD_COMPOSE_FILE" -p "$CD_COMPOSE_PROJECT" up -d --no-build )
|
( cd "${CD_STACK_DIR}" \
|
||||||
|
&& env "${CD_IMAGE_ENV_VAR}=${Image}" \
|
||||||
|
"${CD_PULL_POLICY_ENV_VAR}=never" \
|
||||||
|
timeout "${CD_COMPOSE_TIMEOUT_SECONDS}" \
|
||||||
|
docker compose -f "${CD_COMPOSE_FILE}" -p "${CD_COMPOSE_PROJECT}" \
|
||||||
|
up -d --no-build --no-deps --force-recreate "${CD_COMPOSE_SERVICE}" )
|
||||||
|
}
|
||||||
|
|
||||||
|
compose_validate() {
|
||||||
|
local Image="$1"
|
||||||
|
( cd "${CD_STACK_DIR}" \
|
||||||
|
&& env "${CD_IMAGE_ENV_VAR}=${Image}" \
|
||||||
|
"${CD_PULL_POLICY_ENV_VAR}=never" \
|
||||||
|
docker compose -f "${CD_COMPOSE_FILE}" -p "${CD_COMPOSE_PROJECT}" \
|
||||||
|
config --quiet )
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write the rollback record where the manual runbook looks for it, in the same
|
||||||
|
# format, so an operator following that note can recover from an automated
|
||||||
|
# deploy without knowing this tool exists.
|
||||||
|
write_rollback_record() {
|
||||||
|
local Image="$1" Revision="$2" Dir="${CD_STACK_DIR}/${CD_ROLLBACK_RECORD_DIR}"
|
||||||
|
local Record="${Dir}/${CD_COMPOSE_SERVICE}-rollback-$(date -u +%Y%m%d-%H%M%S).env"
|
||||||
|
|
||||||
|
[[ -d "${Dir}" ]] || { warn "no rollback record directory at ${Dir}"; return 0; }
|
||||||
|
|
||||||
|
( umask 077
|
||||||
|
printf 'DD_ROLLBACK_IMAGE=%s\nDD_ROLLBACK_REVISION=%s\nDD_ROLLBACK_CAPTURED_AT=%s\n' \
|
||||||
|
"${Image}" "${Revision}" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" > "${Record}" )
|
||||||
|
log "rollback recorded at ${Record}"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_health() {
|
check_health() {
|
||||||
@@ -289,14 +323,23 @@ sys.exit(0 if str(data.get(key, "")).lower() == want.lower() else 1)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if [[ $DRY_RUN -eq 1 ]]; then
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
log "dry run: would run docker compose up -d with ${CD_IMAGE_ENV_VAR}=${DEPLOY_IMAGE}"
|
log "dry run: would recreate service ${CD_COMPOSE_SERVICE} with ${CD_IMAGE_ENV_VAR}=${DEPLOY_IMAGE}"
|
||||||
log "dry run: would health-check ${CD_HEALTH_URL}"
|
log "dry run: would health-check ${CD_HEALTH_URL}"
|
||||||
log "dry run complete -- no changes made"
|
log "dry run complete -- no changes made"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Catch a broken compose file or a bad interpolation before the running
|
||||||
|
# container is touched.
|
||||||
|
if ! compose_validate "${DEPLOY_IMAGE}"; then
|
||||||
|
notify fail "compose config is invalid for ${SHA:0:12} -- nothing was changed"
|
||||||
|
die "docker compose config failed; the running stack was not touched"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "${PREVIOUS_IMAGE}" ]] && write_rollback_record "${PREVIOUS_IMAGE}" "${PREVIOUS_REVISION}"
|
||||||
|
|
||||||
log "deploying ${DEPLOY_IMAGE}"
|
log "deploying ${DEPLOY_IMAGE}"
|
||||||
if ! compose_up "$DEPLOY_IMAGE"; then
|
if ! compose_up "${DEPLOY_IMAGE}"; then
|
||||||
notify fail "docker compose up failed for ${SHA:0:12} -- stack left as-is"
|
notify fail "docker compose up failed for ${SHA:0:12} -- stack left as-is"
|
||||||
die "docker compose up failed"
|
die "docker compose up failed"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -37,3 +37,21 @@ Short notes on things that were not obvious. Prune stale ones.
|
|||||||
- **`docker compose down -v` destroys the data volumes** for these stacks. The
|
- **`docker compose down -v` destroys the data volumes** for these stacks. The
|
||||||
retired paradicsomleves test script used it. `cd-deploy` only ever runs
|
retired paradicsomleves test script used it. `cd-deploy` only ever runs
|
||||||
`up -d`.
|
`up -d`.
|
||||||
|
|
||||||
|
- **domaindingo's health endpoint is `/api/health`, not `/health`.** The route is
|
||||||
|
declared as `/health` in `src/domaindingo/api/status.py`, but `main.py` mounts
|
||||||
|
the router with `prefix="/api"`. Checking `/health` gets a 404, which reads as
|
||||||
|
a failed deploy and triggers a rollback. The vault's
|
||||||
|
"DomainDingo Development" note is the authority here.
|
||||||
|
|
||||||
|
- **The manual runbook recreates one service, not the project.**
|
||||||
|
`docker compose up -d --pull always --no-deps --force-recreate <service>`.
|
||||||
|
`cd-deploy` matches it (minus `--pull always`, since it has already pulled a
|
||||||
|
pinned digest), so an automated deploy and a hand-run one converge on the same
|
||||||
|
state.
|
||||||
|
|
||||||
|
- **Rollback records are a documented convention.**
|
||||||
|
`<service>-rollback-<UTC timestamp>.env` in `backup-test`/`backup-prod`,
|
||||||
|
holding `DD_ROLLBACK_IMAGE`, `DD_ROLLBACK_REVISION`, `DD_ROLLBACK_CAPTURED_AT`.
|
||||||
|
`cd-deploy` writes the same format in the same place, so an operator following
|
||||||
|
the manual note can recover from an automated deploy.
|
||||||
|
|||||||
+10
-21
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
|
|
||||||
"_comment": [
|
"_comment": [
|
||||||
"Single source of truth for the fleet's continuous-deployment targets.",
|
"Single source of truth for the fleet's continuous-deployment targets.",
|
||||||
"A target is uniquely identified by (repo, branch). The 'host' field decides",
|
"A target is uniquely identified by (repo, branch). The 'host' field decides",
|
||||||
@@ -10,20 +9,16 @@
|
|||||||
"Adding an environment is an edit here plus a re-run of install/install.sh on",
|
"Adding an environment is an edit here plus a re-run of install/install.sh on",
|
||||||
"the owning host. It is never a change to the project repository."
|
"the owning host. It is never a change to the project repository."
|
||||||
],
|
],
|
||||||
|
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"ntfy_base_url": "https://ntfy.fisher.hu",
|
"ntfy_base_url": "https://ntfy.fisher.hu",
|
||||||
|
|
||||||
"image_wait_seconds": 900,
|
"image_wait_seconds": 900,
|
||||||
"image_poll_interval": 15,
|
"image_poll_interval": 15,
|
||||||
|
|
||||||
"health_retries": 30,
|
"health_retries": 30,
|
||||||
"health_interval": 5,
|
"health_interval": 5,
|
||||||
|
|
||||||
"lock_wait_seconds": 600,
|
"lock_wait_seconds": 600,
|
||||||
"compose_timeout_seconds": 300
|
"compose_timeout_seconds": 300,
|
||||||
|
"rollback_record_dir": "."
|
||||||
},
|
},
|
||||||
|
|
||||||
"hosts": {
|
"hosts": {
|
||||||
"s5.fisher.hu": {
|
"s5.fisher.hu": {
|
||||||
"bind": "10.255.255.1",
|
"bind": "10.255.255.1",
|
||||||
@@ -34,7 +29,6 @@
|
|||||||
"port": 21600
|
"port": 21600
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"name": "domaindingo-test",
|
"name": "domaindingo-test",
|
||||||
@@ -43,24 +37,21 @@
|
|||||||
"branch": "test",
|
"branch": "test",
|
||||||
"env": "test",
|
"env": "test",
|
||||||
"host": "s5.fisher.hu",
|
"host": "s5.fisher.hu",
|
||||||
|
|
||||||
"stack_dir": "/home/fisher/S/uas-ng/docker/domaindingo/s5.fisher.hu",
|
"stack_dir": "/home/fisher/S/uas-ng/docker/domaindingo/s5.fisher.hu",
|
||||||
"compose_file": "docker-compose.yml",
|
"compose_file": "docker-compose.yml",
|
||||||
"compose_project": "domaindingo-test",
|
"compose_project": "domaindingo-test",
|
||||||
"container": "domaindingo-test",
|
"container": "domaindingo-test",
|
||||||
|
"compose_service": "domaindingo-test",
|
||||||
"image_repo": "gitea.fisher.hu/webdev/domaindingo",
|
"image_repo": "gitea.fisher.hu/webdev/domaindingo",
|
||||||
"image_tag_template": "{branch}-sha-{short7}",
|
"image_tag_template": "{branch}-sha-{short7}",
|
||||||
"image_env_var": "DOMAINDINGO_TEST_IMAGE",
|
"image_env_var": "DOMAINDINGO_TEST_IMAGE",
|
||||||
"pull_policy_env_var": "DOMAINDINGO_TEST_PULL_POLICY",
|
"pull_policy_env_var": "DOMAINDINGO_TEST_PULL_POLICY",
|
||||||
|
"health_url": "http://127.0.0.1:9101/api/health",
|
||||||
"health_url": "http://127.0.0.1:9101/health",
|
|
||||||
"health_expect_key": "db",
|
"health_expect_key": "db",
|
||||||
"health_expect_value": "ok",
|
"health_expect_value": "ok",
|
||||||
|
"rollback_on_failure": true,
|
||||||
"rollback_on_failure": true
|
"rollback_record_dir": "backup-test"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "domaindingo-prod",
|
"name": "domaindingo-prod",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -68,22 +59,20 @@
|
|||||||
"branch": "prod",
|
"branch": "prod",
|
||||||
"env": "prod",
|
"env": "prod",
|
||||||
"host": "s5.fisher.hu",
|
"host": "s5.fisher.hu",
|
||||||
|
|
||||||
"stack_dir": "/home/fisher/S/uas-ng/docker/domaindingo/s5.fisher.hu",
|
"stack_dir": "/home/fisher/S/uas-ng/docker/domaindingo/s5.fisher.hu",
|
||||||
"compose_file": "docker-compose-prod.yml",
|
"compose_file": "docker-compose-prod.yml",
|
||||||
"compose_project": "domaindingo-prod",
|
"compose_project": "domaindingo-prod",
|
||||||
"container": "domaindingo-prod",
|
"container": "domaindingo-prod",
|
||||||
|
"compose_service": "domaindingo-prod",
|
||||||
"image_repo": "gitea.fisher.hu/webdev/domaindingo",
|
"image_repo": "gitea.fisher.hu/webdev/domaindingo",
|
||||||
"image_tag_template": "{branch}-sha-{short7}",
|
"image_tag_template": "{branch}-sha-{short7}",
|
||||||
"image_env_var": "DOMAINDINGO_PROD_IMAGE",
|
"image_env_var": "DOMAINDINGO_PROD_IMAGE",
|
||||||
"pull_policy_env_var": "DOMAINDINGO_PROD_PULL_POLICY",
|
"pull_policy_env_var": "DOMAINDINGO_PROD_PULL_POLICY",
|
||||||
|
"health_url": "http://127.0.0.1:9103/api/health",
|
||||||
"health_url": "http://127.0.0.1:9103/health",
|
|
||||||
"health_expect_key": "db",
|
"health_expect_key": "db",
|
||||||
"health_expect_value": "ok",
|
"health_expect_value": "ok",
|
||||||
|
"rollback_on_failure": true,
|
||||||
"rollback_on_failure": true
|
"rollback_record_dir": "backup-prod"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user