diff --git a/panelctl.sh b/panelctl.sh index 72f2264..0369cc4 100644 --- a/panelctl.sh +++ b/panelctl.sh @@ -17,6 +17,7 @@ PANEL_GROUP="${PANEL_GROUP:-panelroutes}" # as KEY=VALUE words (passed to compose via env(1), never sourced). CURRENT_APP="" COMPOSE_ARGS=() +ORPHAN_ARGS=() APP_ENV_ARGS=() APP_ENV_KEYS=() @@ -198,6 +199,7 @@ load_app() { local manifest manifest="$(app_manifest "${name}")" [[ -f "${manifest}" ]] || fail "app '${name}' does not exist" + unset APP_COMPOSE_PROJECT # shellcheck disable=SC1090 source "${manifest}" @@ -235,10 +237,20 @@ app_ports_override() { 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. refresh_compose_args() { 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 if [[ -f "${override}" ]]; then COMPOSE_ARGS+=(-f "${override}") @@ -597,6 +609,56 @@ cmd_render_route() { 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() { local name="$1" validate_name "${name}" @@ -607,10 +669,21 @@ cmd_deploy() { cmd_render_route "${name}" prepare_ports_override "${name}" prepare_env_override "${name}" + adopt_legacy_project # Stream compose output as it happens (the panel shows it live in the # 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}'" fail "compose up failed" fi @@ -625,11 +698,12 @@ cmd_restart() { 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_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" fi @@ -641,7 +715,7 @@ cmd_stop() { validate_name "${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}'" } @@ -742,7 +816,7 @@ cmd_remove() { validate_name "${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. local route_file @@ -878,7 +952,7 @@ cmd_volume_clear() { load_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" if [[ -d "${data_dir}" ]]; then