Merge pull request 'Recreate containers when a deploy rebuilds their image' (#2) from redeploy-fix into main

Reviewed-on: #2
This commit is contained in:
reudy 2026-09-27 22:44:23 +02:00
commit ba0a0155c8

View file

@ -17,6 +17,7 @@ PANEL_GROUP="${PANEL_GROUP:-panelroutes}"
# as KEY=VALUE words (passed to compose via env(1), never sourced). # as KEY=VALUE words (passed to compose via env(1), never sourced).
CURRENT_APP="" CURRENT_APP=""
COMPOSE_ARGS=() COMPOSE_ARGS=()
ORPHAN_ARGS=()
APP_ENV_ARGS=() APP_ENV_ARGS=()
APP_ENV_KEYS=() APP_ENV_KEYS=()
@ -198,6 +199,7 @@ load_app() {
local manifest local manifest
manifest="$(app_manifest "${name}")" manifest="$(app_manifest "${name}")"
[[ -f "${manifest}" ]] || fail "app '${name}' does not exist" [[ -f "${manifest}" ]] || fail "app '${name}' does not exist"
unset APP_COMPOSE_PROJECT
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "${manifest}" source "${manifest}"
@ -235,10 +237,20 @@ app_ports_override() {
echo "${STACKS_DIR}/$1/.panel-ports.yaml" echo "${STACKS_DIR}/$1/.panel-ports.yaml"
} }
# The compose project the app's containers belong to: the app's name, unless
# the manifest keeps an older one (see adopt_legacy_project).
compose_project() {
echo "${APP_COMPOSE_PROJECT:-${CURRENT_APP}}"
}
# The app's compose file followed by whichever generated overrides exist. # The app's compose file followed by whichever generated overrides exist.
refresh_compose_args() { refresh_compose_args() {
local override local override
COMPOSE_ARGS=(-f "${APP_COMPOSE_FILE}") COMPOSE_ARGS=(-p "$(compose_project)" -f "${APP_COMPOSE_FILE}")
# Other git apps may share a kept legacy project; their containers would
# look like orphans.
ORPHAN_ARGS=(--remove-orphans)
[[ "$(compose_project)" == "${CURRENT_APP}" ]] || ORPHAN_ARGS=()
for override in "$(app_ports_override "${CURRENT_APP}")" "$(app_env_override "${CURRENT_APP}")"; do for override in "$(app_ports_override "${CURRENT_APP}")" "$(app_env_override "${CURRENT_APP}")"; do
if [[ -f "${override}" ]]; then if [[ -f "${override}" ]]; then
COMPOSE_ARGS+=(-f "${override}") COMPOSE_ARGS+=(-f "${override}")
@ -597,6 +609,56 @@ cmd_render_route() {
log info "rendered route ${route_file}" log info "rendered route ${route_file}"
} }
# Before panelctl passed -p, compose named the project after the compose
# file's directory, so every git app (repo/compose.yaml) shared the project
# "repo". Move the app's old containers out of the way so they are recreated
# under its own name. Named volumes are prefixed with the project name, so an
# app whose containers use them keeps its old project instead.
adopt_legacy_project() {
[[ -z "${APP_COMPOSE_PROJECT:-}" ]] || return 0
local dir id project legacy="" volumes
local ids=()
dir="$(cd "$(dirname "${APP_COMPOSE_FILE}")" 2>/dev/null && pwd -P)" || return 0
while IFS='|' read -r id project; do
[[ -n "${id}" && -n "${project}" && "${project}" != "${CURRENT_APP}" ]] || continue
ids+=("${id}")
legacy="${project}"
done < <(run_podman ps --all --filter "label=com.docker.compose.project.working_dir=${dir}" \
--format '{{.ID}}|{{index .Labels "com.docker.compose.project"}}' 2>/dev/null || true)
[[ ${#ids[@]} -gt 0 ]] || return 0
volumes="$(run_podman inspect --format '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}} {{end}}{{end}}' "${ids[@]}" 2>/dev/null | xargs || true)"
if [[ -n "${volumes}" ]]; then
echo "APP_COMPOSE_PROJECT=\"${legacy}\"" >>"$(app_manifest "${CURRENT_APP}")"
APP_COMPOSE_PROJECT="${legacy}"
refresh_compose_args
echo "Keeping compose project '${legacy}', because the app's containers use named volumes (${volumes})"
log warning "app '${CURRENT_APP}' keeps compose project '${legacy}' because its containers use named volumes"
return 0
fi
echo "Moving the app's containers from compose project '${legacy}' to '${CURRENT_APP}'"
run_podman rm --force "${ids[@]}" >/dev/null || fail "could not remove the old containers"
# The shared pod goes away once no other app's containers are left in it.
run_podman pod rm "pod_${legacy}" >/dev/null 2>&1 || true
}
# True when a container of the app runs an older image than its tag now
# points at, e.g. after a rebuild. podman-compose only recreates containers
# when the compose file itself changes, so without this a push that only
# changes code would build a new image and keep running the old one.
containers_outdated() {
local id image current
while IFS='|' read -r id image; do
[[ -n "${id}" && -n "${image}" ]] || continue
current="$(run_podman image inspect --format '{{.Id}}' "${image}" 2>/dev/null)" || continue
[[ "${current}" == "${id}"* || "${id}" == "${current}"* ]] || return 0
done < <(run_podman ps --all --filter "label=com.docker.compose.project=$(compose_project)" \
--format '{{.ImageID}}|{{.Image}}' 2>/dev/null || true)
return 1
}
cmd_deploy() { cmd_deploy() {
local name="$1" local name="$1"
validate_name "${name}" validate_name "${name}"
@ -607,10 +669,21 @@ cmd_deploy() {
cmd_render_route "${name}" cmd_render_route "${name}"
prepare_ports_override "${name}" prepare_ports_override "${name}"
prepare_env_override "${name}" prepare_env_override "${name}"
adopt_legacy_project
# Stream compose output as it happens (the panel shows it live in the # Stream compose output as it happens (the panel shows it live in the
# deployment log) and keep a copy in the journal. # deployment log) and keep a copy in the journal.
if ! run_compose "${COMPOSE_ARGS[@]}" up -d --build --remove-orphans 2>&1 | tee >(journal_copy); then if ! run_compose "${COMPOSE_ARGS[@]}" build 2>&1 | tee >(journal_copy); then
log err "Deployment failed for app '${name}'"
fail "image build failed"
fi
local up_args=(up -d --no-build "${ORPHAN_ARGS[@]}")
if containers_outdated; then
echo "Images changed, recreating the app's containers"
up_args+=(--force-recreate)
fi
if ! run_compose "${COMPOSE_ARGS[@]}" "${up_args[@]}" 2>&1 | tee >(journal_copy); then
log err "Deployment failed for app '${name}'" log err "Deployment failed for app '${name}'"
fail "compose up failed" fail "compose up failed"
fi fi
@ -625,11 +698,12 @@ cmd_restart() {
log info "Restarting app '${name}'" log info "Restarting app '${name}'"
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans || fail "compose down failed" adopt_legacy_project
run_compose "${COMPOSE_ARGS[@]}" down "${ORPHAN_ARGS[@]}" || fail "compose down failed"
prepare_ports_override "${name}" prepare_ports_override "${name}"
prepare_env_override "${name}" prepare_env_override "${name}"
if ! run_compose "${COMPOSE_ARGS[@]}" up -d --build --remove-orphans 2>&1; then if ! run_compose "${COMPOSE_ARGS[@]}" up -d --build "${ORPHAN_ARGS[@]}" 2>&1; then
fail "compose up failed during restart" fail "compose up failed during restart"
fi fi
@ -641,7 +715,7 @@ cmd_stop() {
validate_name "${name}" validate_name "${name}"
load_app "${name}" load_app "${name}"
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans || fail "compose down failed" run_compose "${COMPOSE_ARGS[@]}" down "${ORPHAN_ARGS[@]}" || fail "compose down failed"
log info "stopped app '${name}'" log info "stopped app '${name}'"
} }
@ -742,7 +816,7 @@ cmd_remove() {
validate_name "${name}" validate_name "${name}"
load_app "${name}" load_app "${name}"
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans 2>/dev/null || true run_compose "${COMPOSE_ARGS[@]}" down "${ORPHAN_ARGS[@]}" 2>/dev/null || true
# Remove this app's block from the aggregate routes file. # Remove this app's block from the aggregate routes file.
local route_file local route_file
@ -878,7 +952,7 @@ cmd_volume_clear() {
load_app "${name}" load_app "${name}"
log info "clearing volume data for app '${name}'" log info "clearing volume data for app '${name}'"
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans 2>/dev/null || true run_compose "${COMPOSE_ARGS[@]}" down "${ORPHAN_ARGS[@]}" 2>/dev/null || true
local data_dir="${APP_VOLUME_DIR}/data" local data_dir="${APP_VOLUME_DIR}/data"
if [[ -d "${data_dir}" ]]; then if [[ -d "${data_dir}" ]]; then