mirror of
https://github.com/akitaonrails/ai-memory.git
synced 2026-10-02 03:24:46 +08:00
fix(upgrade): verify compose container ownership
This commit is contained in:
@@ -134,6 +134,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- run: scripts/check-native-packaging.sh
|
||||
- run: tests/wrapper_upgrade.sh
|
||||
|
||||
# `bin/release` copies `## [Unreleased]` into the new version section
|
||||
# verbatim, so a repeated `### ` heading there ships release notes with the
|
||||
|
||||
@@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- `ai-memory upgrade` now verifies that a discovered Compose project actually
|
||||
owns the running `ai-memory` container before invoking `docker compose up`.
|
||||
A standalone `docker run` install could previously be mistaken for a Compose
|
||||
deployment merely because an unrelated `docker-compose.yml` existed in a
|
||||
conventional search path; Compose then failed with a container-name conflict
|
||||
and the safe standalone recreation-script fallback was skipped. Unowned
|
||||
containers now use that existing fallback, preserving their inspected ports,
|
||||
mounts, restart policy, command, and operator-set environment. (#469)
|
||||
- The Windows Docker wrapper's thin-client commands now reach a
|
||||
loopback-published server instead of failing with `Connection refused (os
|
||||
error 111)`. `ai-memory status`, `search`, `bootstrap` and every other
|
||||
|
||||
+33
-9
@@ -224,6 +224,27 @@ ENVEOF
|
||||
return 0
|
||||
}
|
||||
|
||||
# A compose file found in a conventional location is only relevant when its
|
||||
# project actually owns the running container. A stale or unrelated file may
|
||||
# declare the same container_name and `compose up` would then fail with a name
|
||||
# conflict instead of taking the safe standalone recovery path.
|
||||
compose_manages_container() {
|
||||
local container="$1" compose_dir="$2" container_id compose_id
|
||||
container_id="$("${DOCKER}" inspect "${container}" --format '{{.Id}}' 2>/dev/null)" || return 1
|
||||
[ -n "${container_id}" ] || return 1
|
||||
|
||||
while IFS= read -r compose_id; do
|
||||
[ -n "${compose_id}" ] || continue
|
||||
case "${container_id}" in
|
||||
"${compose_id}"*) return 0 ;;
|
||||
esac
|
||||
case "${compose_id}" in
|
||||
"${container_id}"*) return 0 ;;
|
||||
esac
|
||||
done < <((cd "${compose_dir}" && "${DOCKER}" compose ps -q) 2>/dev/null)
|
||||
return 1
|
||||
}
|
||||
|
||||
cmd_upgrade() {
|
||||
if [ -z "${AI_MEMORY_SKIP_SELF_UPGRADE:-}" ]; then
|
||||
self_upgrade_script
|
||||
@@ -271,11 +292,9 @@ cmd_upgrade() {
|
||||
| grep -q '^ai-memory$'; then
|
||||
# A LOCAL ai-memory container is running. We've just pulled a new
|
||||
# image; `docker restart` won't recreate from the new image, so we
|
||||
# need to stop+remove+recreate. The safe way is via `docker compose
|
||||
# up -d` when a compose file is reachable (it remembers all the
|
||||
# ports/volumes/env). Without compose we can't recreate safely
|
||||
# because we don't know the original `docker run` args — fall
|
||||
# back to a clear instruction.
|
||||
# need to stop+remove+recreate. Use `docker compose up -d` only when
|
||||
# the discovered project owns this container; otherwise reconstruct
|
||||
# the standalone `docker run` from its inspected runtime settings.
|
||||
local compose_dir=""
|
||||
if [ -f "$(pwd)/docker/docker-compose.yml" ]; then
|
||||
compose_dir="$(pwd)/docker"
|
||||
@@ -284,14 +303,19 @@ cmd_upgrade() {
|
||||
elif [ -f "${HOME}/deploy/ai-memory/docker-compose.yml" ]; then
|
||||
compose_dir="${HOME}/deploy/ai-memory"
|
||||
fi
|
||||
if [ -n "${compose_dir}" ]; then
|
||||
if [ -n "${compose_dir}" ] && compose_manages_container "ai-memory" "${compose_dir}"; then
|
||||
echo "→ restarting local ai-memory container via docker compose (${compose_dir})"
|
||||
( cd "${compose_dir}" && "${DOCKER}" compose up -d ) \
|
||||
|| echo " (compose restart failed; re-run manually: cd ${compose_dir} && docker compose up -d)"
|
||||
else
|
||||
echo "→ a local ai-memory container is running but no compose file"
|
||||
echo " was found in \$PWD/docker-compose.yml, ./docker/docker-compose.yml,"
|
||||
echo " or ~/deploy/ai-memory/docker-compose.yml."
|
||||
if [ -n "${compose_dir}" ]; then
|
||||
echo "→ the compose file at ${compose_dir} does not manage the running ai-memory container"
|
||||
echo " Treating it as a standalone docker run install to avoid a container-name conflict."
|
||||
else
|
||||
echo "→ a local ai-memory container is running but no compose file"
|
||||
echo " was found in \$PWD/docker-compose.yml, ./docker/docker-compose.yml,"
|
||||
echo " or ~/deploy/ai-memory/docker-compose.yml."
|
||||
fi
|
||||
local recreate_script="${CACHE_DIR}/recreate-ai-memory.sh"
|
||||
mkdir -p "${CACHE_DIR}" 2>/dev/null || true
|
||||
if emit_docker_run_script "ai-memory" "${recreate_script}"; then
|
||||
|
||||
@@ -1924,6 +1924,11 @@ image, re-stages hook scripts under
|
||||
prints how to restart the server container so the new binary is used.
|
||||
Re-running `install-hooks --apply` remains idempotent: ai-memory
|
||||
replaces only the hook entries it owns and leaves unrelated hooks alone.
|
||||
When a Compose file is found, the wrapper first verifies that its project owns
|
||||
the running `ai-memory` container. A standalone container is never handed to an
|
||||
unrelated Compose project just because its file occupies a conventional path;
|
||||
the wrapper instead writes the inspected standalone recreation script for
|
||||
review, preserving the existing `/data` mount and other runtime options.
|
||||
|
||||
Set `AI_MEMORY_NO_VERSION_CHECK=1` to silence the daily check. To pin wrapper
|
||||
self-upgrades to a fork or tagged release, set `AI_MEMORY_WRAPPER_URL=<url>`;
|
||||
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/ai-memory-wrapper-upgrade.XXXXXX")"
|
||||
trap 'rm -rf "${TMP_ROOT}"' EXIT
|
||||
|
||||
fail() {
|
||||
printf 'error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local file="$1" needle="$2"
|
||||
grep -Fq -- "${needle}" "${file}" \
|
||||
|| fail "${file} does not contain expected text: ${needle}"
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local file="$1" needle="$2"
|
||||
if grep -Fq -- "${needle}" "${file}"; then
|
||||
fail "${file} unexpectedly contains: ${needle}"
|
||||
fi
|
||||
}
|
||||
|
||||
FAKE_DOCKER="${TMP_ROOT}/docker"
|
||||
cat >"${FAKE_DOCKER}" <<'DOCKER'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\n' "$*" >>"${AI_MEMORY_WRAPPER_TEST_LOG}"
|
||||
|
||||
case "${1:-}" in
|
||||
pull)
|
||||
exit 0
|
||||
;;
|
||||
ps)
|
||||
printf 'ai-memory\n'
|
||||
;;
|
||||
compose)
|
||||
if [ "${2:-}" = "ps" ] && [ "${AI_MEMORY_TEST_COMPOSE_OWNS:-}" = "1" ]; then
|
||||
printf 'running-container-id\n'
|
||||
fi
|
||||
;;
|
||||
inspect)
|
||||
case "${4:-}" in
|
||||
'{{.Id}}') printf 'running-container-id\n' ;;
|
||||
'{{.Config.Image}}') printf 'akitaonrails/ai-memory:latest\n' ;;
|
||||
*PortBindings*) printf '%s\n' '-p 127.0.0.1:49374:49374/tcp ' ;;
|
||||
*Mounts*) printf '%s\n' '-v ai-memory-data:/data ' ;;
|
||||
*RestartPolicy*) printf '%s\n' '--restart unless-stopped' ;;
|
||||
'{{json .Config.Cmd}}') printf '[]\n' ;;
|
||||
*'.Config.Env'*) : ;;
|
||||
*) printf 'unexpected inspect format: %s\n' "${4:-<missing>}" >&2; exit 2 ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
printf 'unexpected docker command: %s\n' "$*" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
DOCKER
|
||||
chmod 0755 "${FAKE_DOCKER}"
|
||||
|
||||
run_upgrade_case() {
|
||||
local name="$1" owns="$2" case_dir log output
|
||||
case_dir="${TMP_ROOT}/${name}"
|
||||
log="${case_dir}/docker.log"
|
||||
output="${case_dir}/output.log"
|
||||
mkdir -p "${case_dir}/home" "${case_dir}/cache"
|
||||
: >"${case_dir}/docker-compose.yml"
|
||||
|
||||
(
|
||||
cd "${case_dir}"
|
||||
HOME="${case_dir}/home" \
|
||||
XDG_CACHE_HOME="${case_dir}/cache" \
|
||||
AI_MEMORY_DOCKER="${FAKE_DOCKER}" \
|
||||
AI_MEMORY_SKIP_SELF_UPGRADE=1 \
|
||||
AI_MEMORY_WRAPPER_TEST_LOG="${log}" \
|
||||
AI_MEMORY_TEST_COMPOSE_OWNS="${owns}" \
|
||||
"${ROOT}/bin/ai-memory" upgrade >"${output}" 2>&1
|
||||
)
|
||||
}
|
||||
|
||||
run_upgrade_case standalone 0
|
||||
assert_contains "${TMP_ROOT}/standalone/output.log" "does not manage the running ai-memory container"
|
||||
assert_not_contains "${TMP_ROOT}/standalone/docker.log" "compose up -d"
|
||||
assert_contains "${TMP_ROOT}/standalone/cache/ai-memory/recreate-ai-memory.sh" "-v ai-memory-data:/data"
|
||||
|
||||
run_upgrade_case compose 1
|
||||
assert_contains "${TMP_ROOT}/compose/output.log" "restarting local ai-memory container via docker compose"
|
||||
assert_contains "${TMP_ROOT}/compose/docker.log" "compose up -d"
|
||||
if [ -e "${TMP_ROOT}/compose/cache/ai-memory/recreate-ai-memory.sh" ]; then
|
||||
fail "Compose-owned container unexpectedly produced a standalone recreation script"
|
||||
fi
|
||||
|
||||
printf 'wrapper upgrade ownership checks passed\n'
|
||||
Reference in New Issue
Block a user