fix(deploy): restore pinned image when replacement creation fails
This commit is contained in:
parent
94ea82e7cb
commit
90b2e6e83c
@ -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"
|
||||
|
||||
@ -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]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user