fix(deploy): restore pinned image when replacement creation fails

This commit is contained in:
WuYongJie 2026-10-13 12:35:21 +08:00
parent 94ea82e7cb
commit 90b2e6e83c
2 changed files with 65 additions and 3 deletions

View File

@ -2628,7 +2628,12 @@ restore_direct_previous_container() {
local previous_image="$2"
local previous_ip=""
docker rm -f "$name" >/dev/null 2>&1 || return 1
# Creation can fail before Docker allocates the replacement name. Absence
# must not prevent recreation of the previous immutable image.
[[ "$previous_image" =~ ^sha256:[0-9a-f]{64}$ ]] || return 1
if docker container inspect "$name" >/dev/null 2>&1; then
docker rm -f "$name" >/dev/null 2>&1 || return 1
fi
create_container "$name" "$previous_image" 0 || return 1
verify_container_uploads_mount_metadata "$name" || return 1
verify_container_upload_dir_config "$name" 1 || return 1
@ -2639,6 +2644,8 @@ restore_direct_previous_container() {
switch_proxy || return 1
verify_proxy_target "$name" || return 1
write_current_container "$name" || return 1
resume_video_transcode "$name" || return 1
resume_task_detail_upload "$name" || return 1
}
fail_direct_replace_with_restore() {
@ -2681,8 +2688,8 @@ direct_replace_active_container() {
*) fail "Direct replacement requires one active material slot" ;;
esac
previous_image="$(docker inspect -f '{{.Config.Image}}' "$name" 2>/dev/null || true)"
[ -n "$previous_image" ] || fail "Active container image could not be determined: $name"
previous_image="$(docker inspect -f '{{.Image}}' "$name" 2>/dev/null || true)"
[[ "$previous_image" =~ ^sha256:[0-9a-f]{64}$ ]] || fail "Active container immutable image could not be determined: $name"
verify_container_uploads_storage "$name" || fail "Active container uploads storage could not be verified before direct replacement"
verify_image_revision_label "$REGISTRY_COMMIT" "$COMMIT_SHA" || fail "Direct replacement image label does not match the exact Git commit"
prepare_worker_runtime_directories || fail "Worker runtime directories could not be prepared"

View File

@ -124,6 +124,61 @@ function shellFunction(name) {
return source.slice(start, end + 3)
}
test('direct rollback restores an absent or partial replacement from the immutable image', () => {
const rollback = shellFunction('restore_direct_previous_container')
const image = `sha256:${'c'.repeat(64)}`
for (const scenario of [
{ present: 0, removeFails: 0, status: 0, removed: false },
{ present: 1, removeFails: 0, status: 0, removed: true },
{ present: 1, removeFails: 1, status: 1, removed: true },
]) {
const input = `set -u
present=${scenario.present}
remove_fails=${scenario.removeFails}
removed=0
docker() {
case "$1" in
container) [ "$present" = 1 ] ;;
rm) removed=1; [ "$remove_fails" = 0 ] ;;
start) return 0 ;;
*) return 90 ;;
esac
}
create_container() { printf 'created:%s:%s\\n' "$1" "$2"; }
verify_container_uploads_mount_metadata() { return 0; }
verify_container_upload_dir_config() { return 0; }
verify_container_uploads_storage() { return 0; }
wait_for_container_health() { printf '172.18.0.9\\n'; }
write_proxy_config() { printf 'proxy:%s\\n' "$1"; }
switch_proxy() { return 0; }
verify_proxy_target() { printf 'proxy-verified\\n'; }
write_current_container() { return 0; }
resume_video_transcode() { printf 'video-resumed\\n'; }
resume_task_detail_upload() { printf 'upload-resumed\\n'; }
${rollback}
restore_direct_previous_container material-blue '${image}'
status=$?
printf 'removed:%s\\n' "$removed" >&2
exit "$status"
`
const result = spawnSync(bash, ['-s'], { input, encoding: 'utf8' })
assert.equal(result.status, scenario.status, result.stderr || result.stdout)
assert.equal(result.stderr.includes('removed:1'), scenario.removed)
if (scenario.status === 0) {
assert.match(result.stdout, new RegExp(`created:material-blue:${image}`))
assert.match(result.stdout, /proxy:172\.18\.0\.9:3000/)
assert.ok(result.stdout.indexOf('proxy-verified') < result.stdout.indexOf('video-resumed'))
assert.ok(result.stdout.indexOf('proxy-verified') < result.stdout.indexOf('upload-resumed'))
} else assert.doesNotMatch(result.stdout, /created:|proxy:/)
}
const invalid = spawnSync(bash, ['-s'], { input: `docker() { echo docker-was-called; }\n${rollback}\nrestore_direct_previous_container material-blue mutable:latest\n`, encoding: 'utf8' })
assert.equal(invalid.status, 1)
assert.doesNotMatch(invalid.stdout, /docker-was-called/)
const direct = shellFunction('direct_replace_active_container')
assert.match(direct, /previous_image="\$\(docker inspect -f '\{\{\.Image\}\}'/)
assert.doesNotMatch(direct, /\.Config\.Image/)
})
test('detail beta configuration validates enabled key/base without exposing provider secrets', () => {
const source = shellFunction('prepare_detail_generator_release')
const code = source.match(/<<'NODE'\n([\s\S]*?)\nNODE/)[1]