mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-02 11:29:17 +00:00
f99956eade
* test(lifecycle): classify mixed rollout harness results Classify Docker #1508 evidence as strict, baseline, blocked, or failed so tiered-storage baseline runs cannot be mistaken for strict mixed-version rollout closure evidence. Co-Authored-By: heihutu <heihutu@gmail.com> * test: classify Docker manual transition preemption Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com>
822 lines
26 KiB
Bash
Executable File
822 lines
26 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Copyright 2026 RustFS Team
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
|
|
PROJECT_NAME="${PROJECT_NAME:-rustfs-1508-mixed}"
|
|
OLD_IMAGE="${OLD_IMAGE:-rustfs/rustfs:latest}"
|
|
NEW_IMAGE="${NEW_IMAGE:-rustfs/rustfs:latest}"
|
|
COLD_IMAGE="${COLD_IMAGE:-${NEW_IMAGE}}"
|
|
BASE_PORT="${BASE_PORT:-19400}"
|
|
OUT_DIR="${OUT_DIR:-${PROJECT_ROOT}/target/manual-transition-1508-docker/$(date +%Y%m%dT%H%M%S)}"
|
|
KEEP_UP=false
|
|
ROLLBACK_PHASE="${ROLLBACK_PHASE:-after-terminal}"
|
|
OLD_NODE_PHASE="${OLD_NODE_PHASE:-initial}"
|
|
WAIT_TIMEOUT_SECS="${WAIT_TIMEOUT_SECS:-180}"
|
|
POLL_SECONDS="${POLL_SECONDS:-180}"
|
|
TRANSITION_WORKERS="${TRANSITION_WORKERS:-2}"
|
|
TRANSITION_QUEUE_CAPACITY="${TRANSITION_QUEUE_CAPACITY:-64}"
|
|
FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT="${FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT:-false}"
|
|
|
|
HOT_ACCESS_KEY="${HOT_ACCESS_KEY:-mvadmin}"
|
|
HOT_SECRET_KEY="${HOT_SECRET_KEY:-mvsecret}"
|
|
COLD_ACCESS_KEY="${COLD_ACCESS_KEY:-coldadmin}"
|
|
COLD_SECRET_KEY="${COLD_SECRET_KEY:-coldsecret}"
|
|
TIER_NAME="${TIER_NAME:-COLDMV}"
|
|
TIER_BUCKET="${TIER_BUCKET:-manual-transition-cold-tier}"
|
|
TIER_PREFIX="${TIER_PREFIX:-tiered}"
|
|
JOB_BUCKET="${JOB_BUCKET:-manual-transition-1508}"
|
|
JOB_PREFIX="${JOB_PREFIX:-transition/manual-rollout}"
|
|
OBJECT_COUNT="${OBJECT_COUNT:-4}"
|
|
AWS_SIGV4_SCOPE="${AWS_SIGV4_SCOPE:-aws:amz:us-east-1:s3}"
|
|
|
|
COMPOSE_FILE=""
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage:
|
|
scripts/manual_transition_mixed_version_docker_harness.sh [options]
|
|
|
|
Options:
|
|
--project-name <name> Docker compose project name
|
|
--old-image <image> Old RustFS image for node1 and rollback node2
|
|
--new-image <image> New RustFS image for node2-node4
|
|
--cold-image <image> Cold-tier RustFS image (default: new image)
|
|
--base-port <port> Host port base; cold uses base, hot nodes use base+1..4
|
|
--out-dir <path> Artifact output directory
|
|
--object-count <n> Non-empty probe object count
|
|
--tier <name> Remote tier name
|
|
--keep-up Leave Docker services running
|
|
--rollback-phase <phase> Rollback node2 timing: after-terminal, in-flight, none
|
|
--old-node-phase <phase> Old node1 timing: initial, before-job
|
|
--no-rollback Alias for --rollback-phase none
|
|
-h, --help Show help
|
|
|
|
Environment:
|
|
PROJECT_NAME OLD_IMAGE NEW_IMAGE COLD_IMAGE BASE_PORT OUT_DIR KEEP_UP
|
|
HOT_ACCESS_KEY HOT_SECRET_KEY COLD_ACCESS_KEY COLD_SECRET_KEY
|
|
TIER_NAME TIER_BUCKET TIER_PREFIX JOB_BUCKET JOB_PREFIX OBJECT_COUNT
|
|
ROLLBACK_PHASE WAIT_TIMEOUT_SECS POLL_SECONDS TRANSITION_WORKERS
|
|
TRANSITION_QUEUE_CAPACITY OLD_NODE_PHASE FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT AWS_SIGV4_SCOPE
|
|
|
|
Artifacts:
|
|
compose.yml, image inspect files, health/readiness logs, API responses,
|
|
terminal status, old-node readback, container logs, summary.env.
|
|
|
|
Result classifications:
|
|
strict_mixed_rollout_pass Real old/new images, non-empty completed transition, zero failures
|
|
baseline_tiered_storage_pass Same old/new image completed transition; useful baseline, not #1508 closure
|
|
blocked_manual_api_not_implemented Manual transition API returned 501 before job admission
|
|
blocked_manual_api_unavailable Manual transition API did not return a usable job_id
|
|
blocked_cluster_readiness_failed Docker cluster did not reach health/readiness before admission
|
|
blocked_empty_scan_or_lifecycle Job completed without lifecycle-matching transition work
|
|
blocked_manual_job_preempted_by_lifecycle_queue Lifecycle/immediate transition queued work before the job
|
|
strict_mixed_rollout_fail Mixed rollout ran but did not satisfy the strict #1508 gate
|
|
USAGE
|
|
}
|
|
|
|
log_info() {
|
|
printf '[INFO] %s\n' "$*"
|
|
}
|
|
|
|
log_warn() {
|
|
printf '[WARN] %s\n' "$*"
|
|
}
|
|
|
|
log_error() {
|
|
printf '[ERROR] %s\n' "$*" >&2
|
|
}
|
|
|
|
arg_value() {
|
|
local flag="$1"
|
|
local value="${2:-}"
|
|
if [[ -z "$value" || "$value" == --* ]]; then
|
|
log_error "missing value for ${flag}"
|
|
exit 1
|
|
fi
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
require_cmd() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
log_error "required command not found: $1"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
parse_positive_int() {
|
|
local name="$1"
|
|
local value="$2"
|
|
if ! [[ "$value" =~ ^[0-9]+$ ]] || (( value <= 0 )); then
|
|
log_error "${name} must be a positive integer"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
validate_rollback_phase() {
|
|
case "$ROLLBACK_PHASE" in
|
|
after-terminal|in-flight|none)
|
|
;;
|
|
*)
|
|
log_error "--rollback-phase must be one of: after-terminal, in-flight, none"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
validate_old_node_phase() {
|
|
case "$OLD_NODE_PHASE" in
|
|
initial|before-job)
|
|
;;
|
|
*)
|
|
log_error "--old-node-phase must be one of: initial, before-job"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--project-name)
|
|
PROJECT_NAME="$(arg_value "$1" "${2:-}")"
|
|
shift 2
|
|
;;
|
|
--old-image)
|
|
OLD_IMAGE="$(arg_value "$1" "${2:-}")"
|
|
shift 2
|
|
;;
|
|
--new-image)
|
|
NEW_IMAGE="$(arg_value "$1" "${2:-}")"
|
|
shift 2
|
|
;;
|
|
--cold-image)
|
|
COLD_IMAGE="$(arg_value "$1" "${2:-}")"
|
|
shift 2
|
|
;;
|
|
--base-port)
|
|
BASE_PORT="$(arg_value "$1" "${2:-}")"
|
|
shift 2
|
|
;;
|
|
--out-dir)
|
|
OUT_DIR="$(arg_value "$1" "${2:-}")"
|
|
shift 2
|
|
;;
|
|
--object-count)
|
|
OBJECT_COUNT="$(arg_value "$1" "${2:-}")"
|
|
shift 2
|
|
;;
|
|
--tier)
|
|
TIER_NAME="$(arg_value "$1" "${2:-}")"
|
|
shift 2
|
|
;;
|
|
--keep-up)
|
|
KEEP_UP=true
|
|
shift
|
|
;;
|
|
--rollback-phase)
|
|
ROLLBACK_PHASE="$(arg_value "$1" "${2:-}")"
|
|
shift 2
|
|
;;
|
|
--old-node-phase)
|
|
OLD_NODE_PHASE="$(arg_value "$1" "${2:-}")"
|
|
shift 2
|
|
;;
|
|
--no-rollback)
|
|
ROLLBACK_PHASE=none
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
log_error "unknown argument: $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
compose() {
|
|
docker compose --project-name "$PROJECT_NAME" -f "$COMPOSE_FILE" "$@"
|
|
}
|
|
|
|
network_name() {
|
|
printf '%s_rustfs-1508-net' "$PROJECT_NAME"
|
|
}
|
|
|
|
hot_volumes() {
|
|
printf 'http://node{1...4}:9000/data/rustfs{0...3}'
|
|
}
|
|
|
|
hot_endpoint() {
|
|
local node_index="$1"
|
|
printf 'http://127.0.0.1:%s' "$((BASE_PORT + node_index))"
|
|
}
|
|
|
|
cold_endpoint() {
|
|
printf 'http://127.0.0.1:%s' "$BASE_PORT"
|
|
}
|
|
|
|
cleanup() {
|
|
collect_logs || true
|
|
if [[ "$KEEP_UP" == "true" ]]; then
|
|
log_info "KEEP_UP=true, leaving Docker services running"
|
|
return
|
|
fi
|
|
compose down -v >/dev/null 2>&1 || true
|
|
}
|
|
|
|
write_compose_file() {
|
|
local node1_image
|
|
COMPOSE_FILE="${OUT_DIR}/compose.yml"
|
|
node1_image="$OLD_IMAGE"
|
|
if [[ "$OLD_NODE_PHASE" == "before-job" ]]; then
|
|
node1_image="$NEW_IMAGE"
|
|
fi
|
|
cat >"$COMPOSE_FILE" <<EOF
|
|
services:
|
|
cold:
|
|
image: ${COLD_IMAGE}
|
|
hostname: cold
|
|
user: "0:0"
|
|
environment:
|
|
- RUSTFS_ADDRESS=:9000
|
|
- RUSTFS_ACCESS_KEY=${COLD_ACCESS_KEY}
|
|
- RUSTFS_SECRET_KEY=${COLD_SECRET_KEY}
|
|
- RUSTFS_VOLUMES=/data/rustfs0 /data/rustfs1 /data/rustfs2 /data/rustfs3
|
|
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true
|
|
- RUSTFS_OBS_LOGGER_LEVEL=warn
|
|
volumes:
|
|
- cold_data_0:/data/rustfs0
|
|
- cold_data_1:/data/rustfs1
|
|
- cold_data_2:/data/rustfs2
|
|
- cold_data_3:/data/rustfs3
|
|
ports:
|
|
- "${BASE_PORT}:9000"
|
|
networks:
|
|
- rustfs-1508-net
|
|
|
|
node1:
|
|
image: ${node1_image}
|
|
hostname: node1
|
|
user: "0:0"
|
|
environment: &hot-env
|
|
- RUSTFS_ADDRESS=:9000
|
|
- RUSTFS_ACCESS_KEY=${HOT_ACCESS_KEY}
|
|
- RUSTFS_SECRET_KEY=${HOT_SECRET_KEY}
|
|
- RUSTFS_VOLUMES=$(hot_volumes)
|
|
- RUSTFS_SCANNER_ENABLED=false
|
|
- RUSTFS_SCANNER_CYCLE=3600
|
|
- RUSTFS_SCANNER_START_DELAY_SECS=3600
|
|
- RUSTFS_MAX_TRANSITION_WORKERS=${TRANSITION_WORKERS}
|
|
- RUSTFS_TRANSITION_QUEUE_CAPACITY=${TRANSITION_QUEUE_CAPACITY}
|
|
- RUSTFS_TEST_FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT=${FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT}
|
|
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true
|
|
- RUSTFS_OBS_LOGGER_LEVEL=warn
|
|
volumes:
|
|
- node1_data_0:/data/rustfs0
|
|
- node1_data_1:/data/rustfs1
|
|
- node1_data_2:/data/rustfs2
|
|
- node1_data_3:/data/rustfs3
|
|
ports:
|
|
- "$((BASE_PORT + 1)):9000"
|
|
networks:
|
|
- rustfs-1508-net
|
|
|
|
node2:
|
|
image: ${NEW_IMAGE}
|
|
hostname: node2
|
|
user: "0:0"
|
|
environment: *hot-env
|
|
volumes:
|
|
- node2_data_0:/data/rustfs0
|
|
- node2_data_1:/data/rustfs1
|
|
- node2_data_2:/data/rustfs2
|
|
- node2_data_3:/data/rustfs3
|
|
ports:
|
|
- "$((BASE_PORT + 2)):9000"
|
|
networks:
|
|
- rustfs-1508-net
|
|
|
|
node3:
|
|
image: ${NEW_IMAGE}
|
|
hostname: node3
|
|
user: "0:0"
|
|
environment: *hot-env
|
|
volumes:
|
|
- node3_data_0:/data/rustfs0
|
|
- node3_data_1:/data/rustfs1
|
|
- node3_data_2:/data/rustfs2
|
|
- node3_data_3:/data/rustfs3
|
|
ports:
|
|
- "$((BASE_PORT + 3)):9000"
|
|
networks:
|
|
- rustfs-1508-net
|
|
|
|
node4:
|
|
image: ${NEW_IMAGE}
|
|
hostname: node4
|
|
user: "0:0"
|
|
environment: *hot-env
|
|
volumes:
|
|
- node4_data_0:/data/rustfs0
|
|
- node4_data_1:/data/rustfs1
|
|
- node4_data_2:/data/rustfs2
|
|
- node4_data_3:/data/rustfs3
|
|
ports:
|
|
- "$((BASE_PORT + 4)):9000"
|
|
networks:
|
|
- rustfs-1508-net
|
|
|
|
networks:
|
|
rustfs-1508-net:
|
|
|
|
volumes:
|
|
cold_data_0:
|
|
cold_data_1:
|
|
cold_data_2:
|
|
cold_data_3:
|
|
node1_data_0:
|
|
node1_data_1:
|
|
node1_data_2:
|
|
node1_data_3:
|
|
node2_data_0:
|
|
node2_data_1:
|
|
node2_data_2:
|
|
node2_data_3:
|
|
node3_data_0:
|
|
node3_data_1:
|
|
node3_data_2:
|
|
node3_data_3:
|
|
node4_data_0:
|
|
node4_data_1:
|
|
node4_data_2:
|
|
node4_data_3:
|
|
EOF
|
|
}
|
|
|
|
curl_signed() {
|
|
local access_key="$1"
|
|
local secret_key="$2"
|
|
local method="$3"
|
|
local url="$4"
|
|
shift 4
|
|
curl -sS --aws-sigv4 "$AWS_SIGV4_SCOPE" --user "${access_key}:${secret_key}" -X "$method" "$url" "$@"
|
|
}
|
|
|
|
curl_hot() {
|
|
local method="$1"
|
|
local url="$2"
|
|
shift 2
|
|
curl_signed "$HOT_ACCESS_KEY" "$HOT_SECRET_KEY" "$method" "$url" "$@"
|
|
}
|
|
|
|
curl_cold() {
|
|
local method="$1"
|
|
local url="$2"
|
|
shift 2
|
|
curl_signed "$COLD_ACCESS_KEY" "$COLD_SECRET_KEY" "$method" "$url" "$@"
|
|
}
|
|
|
|
url_encode() {
|
|
jq -rn --arg v "$1" '$v|@uri'
|
|
}
|
|
|
|
wait_http_ok() {
|
|
local url="$1"
|
|
local tag="$2"
|
|
local start now code
|
|
start="$(date +%s)"
|
|
while true; do
|
|
code="$(curl -sS -o /dev/null -w '%{http_code}' "$url" || true)"
|
|
printf '%s %s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$url" "$code" >>"${OUT_DIR}/health-${tag}.log"
|
|
if [[ "$code" == "200" ]]; then
|
|
return 0
|
|
fi
|
|
now="$(date +%s)"
|
|
if (( now - start >= WAIT_TIMEOUT_SECS )); then
|
|
log_error "timed out waiting for ${url}"
|
|
return 1
|
|
fi
|
|
sleep 2
|
|
done
|
|
}
|
|
|
|
wait_cluster_ready() {
|
|
local index endpoint
|
|
wait_http_ok "$(cold_endpoint)/health" "cold-live"
|
|
wait_http_ok "$(cold_endpoint)/health/ready" "cold-ready"
|
|
for index in 1 2 3 4; do
|
|
endpoint="$(hot_endpoint "$index")"
|
|
wait_http_ok "${endpoint}/health" "node${index}-live"
|
|
wait_http_ok "${endpoint}/health/ready" "node${index}-ready"
|
|
done
|
|
}
|
|
|
|
create_bucket() {
|
|
local endpoint="$1"
|
|
local bucket="$2"
|
|
local result_file="$3"
|
|
curl_hot PUT "${endpoint%/}/${bucket}" -o "${result_file}.response" -w "%{http_code}\n" >"${result_file}.http_code" || true
|
|
}
|
|
|
|
add_tier() {
|
|
local body="${OUT_DIR}/add-tier.json"
|
|
cat >"$body" <<EOF
|
|
{
|
|
"type": "rustfs",
|
|
"rustfs": {
|
|
"name": "${TIER_NAME}",
|
|
"endpoint": "http://cold:9000",
|
|
"accessKey": "${COLD_ACCESS_KEY}",
|
|
"secretKey": "${COLD_SECRET_KEY}",
|
|
"bucket": "${TIER_BUCKET}",
|
|
"prefix": "${TIER_PREFIX}",
|
|
"region": "us-east-1",
|
|
"storageClass": ""
|
|
}
|
|
}
|
|
EOF
|
|
curl_hot PUT "$(hot_endpoint 2)/rustfs/admin/v3/tier" \
|
|
-H 'Content-Type: application/json' \
|
|
--data-binary "@${body}" \
|
|
-o "${OUT_DIR}/add-tier.response" \
|
|
-w "%{http_code}\n" >"${OUT_DIR}/add-tier.http_code"
|
|
}
|
|
|
|
put_lifecycle() {
|
|
local lifecycle="${OUT_DIR}/lifecycle.xml"
|
|
cat >"$lifecycle" <<EOF
|
|
<LifecycleConfiguration>
|
|
<Rule>
|
|
<ID>manual-transition-1508</ID>
|
|
<Filter><Prefix>${JOB_PREFIX}/</Prefix></Filter>
|
|
<Status>Enabled</Status>
|
|
<Transition><Days>0</Days><StorageClass>${TIER_NAME}</StorageClass></Transition>
|
|
</Rule>
|
|
</LifecycleConfiguration>
|
|
EOF
|
|
curl_hot PUT "$(hot_endpoint 2)/${JOB_BUCKET}?lifecycle" \
|
|
-H 'Content-Type: application/xml' \
|
|
--data-binary "@${lifecycle}" \
|
|
-o "${OUT_DIR}/put-lifecycle.response" \
|
|
-w "%{http_code}\n" >"${OUT_DIR}/put-lifecycle.http_code"
|
|
}
|
|
|
|
seed_objects() {
|
|
local index key
|
|
for index in $(seq 1 "$OBJECT_COUNT"); do
|
|
key="${JOB_PREFIX}/object-$(printf '%02d' "$index").txt"
|
|
printf 'rustfs #1508 mixed-version transition probe %s\n' "$index" >"${OUT_DIR}/object-${index}.txt"
|
|
curl_hot PUT "$(hot_endpoint 2)/${JOB_BUCKET}/${key}" \
|
|
--data-binary "@${OUT_DIR}/object-${index}.txt" \
|
|
-o "${OUT_DIR}/put-object-${index}.response" \
|
|
-w "%{http_code}\n" >"${OUT_DIR}/put-object-${index}.http_code"
|
|
printf '%s\n' "$key" >>"${OUT_DIR}/object-keys.txt"
|
|
done
|
|
}
|
|
|
|
start_transition_job() {
|
|
local query response job_id run_http_code
|
|
query="bucket=$(url_encode "$JOB_BUCKET")"
|
|
query="${query}&prefix=$(url_encode "${JOB_PREFIX}/")"
|
|
query="${query}&tier=$(url_encode "$TIER_NAME")"
|
|
query="${query}&dryRun=false&maxObjects=${OBJECT_COUNT}&mode=async"
|
|
curl_hot POST "$(hot_endpoint 2)/rustfs/admin/v3/ilm/transition/run?${query}" \
|
|
-o "${OUT_DIR}/run-response.json" \
|
|
-w "%{http_code}\n" >"${OUT_DIR}/run-response.http_code" || true
|
|
response="$(cat "${OUT_DIR}/run-response.json" 2>/dev/null || true)"
|
|
run_http_code="$(cat "${OUT_DIR}/run-response.http_code" 2>/dev/null || true)"
|
|
job_id="$(printf '%s' "$response" | jq -r '.job_id // empty')"
|
|
if [[ -z "$job_id" ]]; then
|
|
log_warn "manual transition response omitted job_id, http_code=${run_http_code:-unknown}"
|
|
return 1
|
|
fi
|
|
printf '%s\n' "$job_id" >"${OUT_DIR}/job-id.txt"
|
|
}
|
|
|
|
replace_node2_with_old_image() {
|
|
local network
|
|
network="$(network_name)"
|
|
log_info "Replacing node2 with old image ${OLD_IMAGE} for ${ROLLBACK_PHASE} rollback readback"
|
|
compose stop node2 >/dev/null
|
|
compose rm -f node2 >/dev/null
|
|
docker run -d \
|
|
--name "${PROJECT_NAME}-node2-rollback-old" \
|
|
--network "$network" \
|
|
--network-alias node2 \
|
|
--user 0:0 \
|
|
-p "$((BASE_PORT + 2)):9000" \
|
|
-e RUSTFS_ADDRESS=:9000 \
|
|
-e RUSTFS_ACCESS_KEY="$HOT_ACCESS_KEY" \
|
|
-e RUSTFS_SECRET_KEY="$HOT_SECRET_KEY" \
|
|
-e RUSTFS_VOLUMES="$(hot_volumes)" \
|
|
-e RUSTFS_SCANNER_ENABLED=false \
|
|
-e RUSTFS_SCANNER_CYCLE=3600 \
|
|
-e RUSTFS_SCANNER_START_DELAY_SECS=3600 \
|
|
-e RUSTFS_MAX_TRANSITION_WORKERS="$TRANSITION_WORKERS" \
|
|
-e RUSTFS_TRANSITION_QUEUE_CAPACITY="$TRANSITION_QUEUE_CAPACITY" \
|
|
-e RUSTFS_TEST_FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT="$FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT" \
|
|
-e RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true \
|
|
-e RUSTFS_OBS_LOGGER_LEVEL=warn \
|
|
-v "${PROJECT_NAME}_node2_data_0:/data/rustfs0" \
|
|
-v "${PROJECT_NAME}_node2_data_1:/data/rustfs1" \
|
|
-v "${PROJECT_NAME}_node2_data_2:/data/rustfs2" \
|
|
-v "${PROJECT_NAME}_node2_data_3:/data/rustfs3" \
|
|
"$OLD_IMAGE" >/dev/null
|
|
}
|
|
|
|
replace_node1_with_old_image() {
|
|
local network
|
|
network="$(network_name)"
|
|
log_info "Replacing node1 with old image ${OLD_IMAGE} before manual transition job"
|
|
compose stop node1 >/dev/null
|
|
compose rm -f node1 >/dev/null
|
|
docker run -d \
|
|
--name "${PROJECT_NAME}-node1-before-job-old" \
|
|
--network "$network" \
|
|
--network-alias node1 \
|
|
--user 0:0 \
|
|
-p "$((BASE_PORT + 1)):9000" \
|
|
-e RUSTFS_ADDRESS=:9000 \
|
|
-e RUSTFS_ACCESS_KEY="$HOT_ACCESS_KEY" \
|
|
-e RUSTFS_SECRET_KEY="$HOT_SECRET_KEY" \
|
|
-e RUSTFS_VOLUMES="$(hot_volumes)" \
|
|
-e RUSTFS_SCANNER_ENABLED=false \
|
|
-e RUSTFS_SCANNER_CYCLE=3600 \
|
|
-e RUSTFS_SCANNER_START_DELAY_SECS=3600 \
|
|
-e RUSTFS_MAX_TRANSITION_WORKERS="$TRANSITION_WORKERS" \
|
|
-e RUSTFS_TRANSITION_QUEUE_CAPACITY="$TRANSITION_QUEUE_CAPACITY" \
|
|
-e RUSTFS_TEST_FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT="$FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT" \
|
|
-e RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true \
|
|
-e RUSTFS_OBS_LOGGER_LEVEL=warn \
|
|
-v "${PROJECT_NAME}_node1_data_0:/data/rustfs0" \
|
|
-v "${PROJECT_NAME}_node1_data_1:/data/rustfs1" \
|
|
-v "${PROJECT_NAME}_node1_data_2:/data/rustfs2" \
|
|
-v "${PROJECT_NAME}_node1_data_3:/data/rustfs3" \
|
|
"$OLD_IMAGE" >/dev/null
|
|
}
|
|
|
|
poll_terminal_status() {
|
|
local job_id="$1"
|
|
local status_url status_json terminal_state
|
|
status_url="$(hot_endpoint 3)/rustfs/admin/v3/ilm/transition/jobs/${job_id}"
|
|
for _ in $(seq 1 "$POLL_SECONDS"); do
|
|
status_json="$(curl_hot GET "$status_url")"
|
|
printf '%s\n' "$status_json" >"${OUT_DIR}/status-terminal.json"
|
|
terminal_state="$(printf '%s' "$status_json" | jq -r '.status // ""')"
|
|
case "$terminal_state" in
|
|
completed|partial|failed|cancelled|unknown)
|
|
printf '%s\n' "$terminal_state" >"${OUT_DIR}/terminal-state.txt"
|
|
return 0
|
|
;;
|
|
esac
|
|
sleep 1
|
|
done
|
|
log_error "manual transition job did not reach a terminal status within ${POLL_SECONDS}s"
|
|
return 1
|
|
}
|
|
|
|
capture_old_node_readback() {
|
|
local job_id="$1"
|
|
curl_hot GET "$(hot_endpoint 1)/rustfs/admin/v3/ilm/transition/jobs/${job_id}" \
|
|
-o "${OUT_DIR}/old-node-status.response" \
|
|
-w "%{http_code}\n" >"${OUT_DIR}/old-node-status.http_code" || true
|
|
curl_hot GET "$(hot_endpoint 2)/rustfs/admin/v3/ilm/transition/jobs/${job_id}" \
|
|
-o "${OUT_DIR}/rollback-node2-status.response" \
|
|
-w "%{http_code}\n" >"${OUT_DIR}/rollback-node2-status.http_code" || true
|
|
}
|
|
|
|
head_probe() {
|
|
local key
|
|
key="$(head -n 1 "${OUT_DIR}/object-keys.txt")"
|
|
curl_hot HEAD "$(hot_endpoint 3)/${JOB_BUCKET}/${key}" \
|
|
-D "${OUT_DIR}/head-object.headers" \
|
|
-o /dev/null \
|
|
-w "%{http_code}\n" >"${OUT_DIR}/head-object.http_code" || true
|
|
}
|
|
|
|
image_id() {
|
|
local file="$1"
|
|
jq -r '.[0].Id // ""' "$file" 2>/dev/null || true
|
|
}
|
|
|
|
is_compat_readback_code() {
|
|
case "$1" in
|
|
200|501)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
classify_result() {
|
|
local terminal_state="$1"
|
|
local transition_completed="$2"
|
|
local transition_failed="$3"
|
|
local tier_failure="$4"
|
|
local old_code="$5"
|
|
local rollback_code="$6"
|
|
local run_http_code="$7"
|
|
local lifecycle_config_found="$8"
|
|
local scanned="$9"
|
|
local eligible="${10}"
|
|
local skipped_already_transitioned="${11}"
|
|
local skipped_already_in_flight="${12}"
|
|
local old_image_id new_image_id images_are_mixed readback_ok
|
|
|
|
if [[ -f "${OUT_DIR}/readiness-failed" ]]; then
|
|
printf 'blocked_cluster_readiness_failed\n'
|
|
return
|
|
fi
|
|
|
|
old_image_id="$(image_id "${OUT_DIR}/old-image.inspect.json")"
|
|
new_image_id="$(image_id "${OUT_DIR}/new-image.inspect.json")"
|
|
images_are_mixed=false
|
|
if [[ "$OLD_IMAGE" != "$NEW_IMAGE" && -n "$old_image_id" && -n "$new_image_id" && "$old_image_id" != "$new_image_id" ]]; then
|
|
images_are_mixed=true
|
|
fi
|
|
|
|
readback_ok=false
|
|
if is_compat_readback_code "$old_code"; then
|
|
if [[ "$ROLLBACK_PHASE" == "none" ]] || is_compat_readback_code "$rollback_code"; then
|
|
readback_ok=true
|
|
fi
|
|
fi
|
|
|
|
if [[ "$run_http_code" == "501" ]]; then
|
|
printf 'blocked_manual_api_not_implemented\n'
|
|
return
|
|
fi
|
|
if [[ -z "$(cat "${OUT_DIR}/job-id.txt" 2>/dev/null || true)" ]]; then
|
|
printf 'blocked_manual_api_unavailable\n'
|
|
return
|
|
fi
|
|
if [[ "$terminal_state" == "completed" && ( "$lifecycle_config_found" != "true" || "$scanned" == "0" || "$eligible" == "0" || "$transition_completed" == "0" ) ]]; then
|
|
printf 'blocked_empty_scan_or_lifecycle\n'
|
|
return
|
|
fi
|
|
if [[ "$transition_completed" == "0" && "$eligible" != "0" && "$transition_failed" == "0" && "$tier_failure" == "0" ]]; then
|
|
if [[ "$skipped_already_transitioned" != "0" || "$skipped_already_in_flight" != "0" ]]; then
|
|
printf 'blocked_manual_job_preempted_by_lifecycle_queue\n'
|
|
return
|
|
fi
|
|
fi
|
|
if [[ "$terminal_state" == "completed" && "$transition_completed" != "0" && "$transition_failed" == "0" && "$tier_failure" == "0" ]]; then
|
|
if [[ "$images_are_mixed" == "true" && "$readback_ok" == "true" ]]; then
|
|
printf 'strict_mixed_rollout_pass\n'
|
|
return
|
|
fi
|
|
printf 'baseline_tiered_storage_pass\n'
|
|
return
|
|
fi
|
|
printf 'strict_mixed_rollout_fail\n'
|
|
}
|
|
|
|
collect_logs() {
|
|
local service
|
|
if [[ -z "$COMPOSE_FILE" || ! -f "$COMPOSE_FILE" ]]; then
|
|
return 0
|
|
fi
|
|
for service in cold node1 node2 node3 node4; do
|
|
compose logs --no-color "$service" >"${OUT_DIR}/${service}.log" 2>/dev/null || true
|
|
done
|
|
docker logs "${PROJECT_NAME}-node1-before-job-old" >"${OUT_DIR}/node1-before-job-old.log" 2>/dev/null || true
|
|
docker logs "${PROJECT_NAME}-node2-rollback-old" >"${OUT_DIR}/node2-rollback-old.log" 2>/dev/null || true
|
|
}
|
|
|
|
summarize() {
|
|
local terminal_state transition_completed tier_failure transition_failed old_code rollback_code run_http_code lifecycle_config_found scanned eligible skipped_already_transitioned skipped_already_in_flight queue_queued queue_active result_classification
|
|
terminal_state="$(cat "${OUT_DIR}/terminal-state.txt" 2>/dev/null || true)"
|
|
transition_completed="$(jq -r '.report.transition_completed // 0' "${OUT_DIR}/status-terminal.json" 2>/dev/null || printf '0')"
|
|
tier_failure="$(jq -r '.report.tier_failure // 0' "${OUT_DIR}/status-terminal.json" 2>/dev/null || printf '0')"
|
|
transition_failed="$(jq -r '.report.transition_failed // 0' "${OUT_DIR}/status-terminal.json" 2>/dev/null || printf '0')"
|
|
old_code="$(cat "${OUT_DIR}/old-node-status.http_code" 2>/dev/null || true)"
|
|
rollback_code="$(cat "${OUT_DIR}/rollback-node2-status.http_code" 2>/dev/null || true)"
|
|
run_http_code="$(cat "${OUT_DIR}/run-response.http_code" 2>/dev/null || true)"
|
|
lifecycle_config_found="$(jq -r '.report.lifecycle_config_found // false' "${OUT_DIR}/status-terminal.json" 2>/dev/null || printf 'false')"
|
|
scanned="$(jq -r '.report.scanned // 0' "${OUT_DIR}/status-terminal.json" 2>/dev/null || printf '0')"
|
|
eligible="$(jq -r '.report.eligible // 0' "${OUT_DIR}/status-terminal.json" 2>/dev/null || printf '0')"
|
|
skipped_already_transitioned="$(jq -r '.report.skipped_already_transitioned // 0' "${OUT_DIR}/status-terminal.json" 2>/dev/null || printf '0')"
|
|
skipped_already_in_flight="$(jq -r '.report.skipped_already_in_flight // 0' "${OUT_DIR}/status-terminal.json" 2>/dev/null || printf '0')"
|
|
queue_queued="$(jq -r '.queue_snapshot.queued // 0' "${OUT_DIR}/status-terminal.json" 2>/dev/null || printf '0')"
|
|
queue_active="$(jq -r '.queue_snapshot.active // 0' "${OUT_DIR}/status-terminal.json" 2>/dev/null || printf '0')"
|
|
result_classification="$(classify_result "$terminal_state" "$transition_completed" "$transition_failed" "$tier_failure" "$old_code" "$rollback_code" "$run_http_code" "$lifecycle_config_found" "$scanned" "$eligible" "$skipped_already_transitioned" "$skipped_already_in_flight")"
|
|
cat >"${OUT_DIR}/summary.env" <<EOF
|
|
project_name=${PROJECT_NAME}
|
|
old_image=${OLD_IMAGE}
|
|
new_image=${NEW_IMAGE}
|
|
cold_image=${COLD_IMAGE}
|
|
old_image_id=$(image_id "${OUT_DIR}/old-image.inspect.json")
|
|
new_image_id=$(image_id "${OUT_DIR}/new-image.inspect.json")
|
|
base_port=${BASE_PORT}
|
|
tier=${TIER_NAME}
|
|
job_bucket=${JOB_BUCKET}
|
|
job_prefix=${JOB_PREFIX}
|
|
object_count=${OBJECT_COUNT}
|
|
transition_workers=${TRANSITION_WORKERS}
|
|
transition_queue_capacity=${TRANSITION_QUEUE_CAPACITY}
|
|
force_immediate_transition_enqueue_timeout=${FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT}
|
|
run_http_code=${run_http_code}
|
|
terminal_state=${terminal_state}
|
|
lifecycle_config_found=${lifecycle_config_found}
|
|
scanned=${scanned}
|
|
eligible=${eligible}
|
|
skipped_already_transitioned=${skipped_already_transitioned}
|
|
skipped_already_in_flight=${skipped_already_in_flight}
|
|
transition_completed=${transition_completed}
|
|
transition_failed=${transition_failed}
|
|
tier_failure=${tier_failure}
|
|
queue_queued=${queue_queued}
|
|
queue_active=${queue_active}
|
|
old_node_status_http_code=${old_code}
|
|
rollback_node2_status_http_code=${rollback_code}
|
|
rollback_phase=${ROLLBACK_PHASE}
|
|
old_node_phase=${OLD_NODE_PHASE}
|
|
rollback_new2_to_old=$([[ "$ROLLBACK_PHASE" == "none" ]] && printf 'false' || printf 'true')
|
|
result_classification=${result_classification}
|
|
EOF
|
|
cat "${OUT_DIR}/summary.env"
|
|
if [[ "$result_classification" != "strict_mixed_rollout_pass" ]]; then
|
|
log_error "strict #1508 mixed-version transition gate failed; see ${OUT_DIR}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
parse_positive_int "--base-port" "$BASE_PORT"
|
|
parse_positive_int "--object-count" "$OBJECT_COUNT"
|
|
validate_rollback_phase
|
|
validate_old_node_phase
|
|
require_cmd docker
|
|
require_cmd curl
|
|
require_cmd jq
|
|
|
|
mkdir -p "$OUT_DIR"
|
|
write_compose_file
|
|
trap cleanup EXIT INT TERM
|
|
|
|
log_info "Writing artifacts under ${OUT_DIR}"
|
|
docker image inspect "$OLD_IMAGE" >"${OUT_DIR}/old-image.inspect.json"
|
|
docker image inspect "$NEW_IMAGE" >"${OUT_DIR}/new-image.inspect.json"
|
|
docker image inspect "$COLD_IMAGE" >"${OUT_DIR}/cold-image.inspect.json"
|
|
|
|
compose up -d
|
|
if ! wait_cluster_ready; then
|
|
printf 'cluster readiness failed before manual transition admission\n' >"${OUT_DIR}/readiness-failed"
|
|
collect_logs
|
|
summarize
|
|
return 1
|
|
fi
|
|
|
|
curl_cold PUT "$(cold_endpoint)/${TIER_BUCKET}" -o "${OUT_DIR}/create-cold-bucket.response" -w "%{http_code}\n" >"${OUT_DIR}/create-cold-bucket.http_code"
|
|
create_bucket "$(hot_endpoint 2)" "$JOB_BUCKET" "${OUT_DIR}/create-hot-bucket"
|
|
add_tier
|
|
seed_objects
|
|
put_lifecycle
|
|
if [[ "$OLD_NODE_PHASE" == "before-job" ]]; then
|
|
replace_node1_with_old_image
|
|
wait_http_ok "$(hot_endpoint 1)/health" "node1-old-live" || true
|
|
wait_http_ok "$(hot_endpoint 1)/health/ready" "node1-old-ready" || true
|
|
fi
|
|
if start_transition_job; then
|
|
if [[ "$ROLLBACK_PHASE" == "in-flight" ]]; then
|
|
replace_node2_with_old_image
|
|
fi
|
|
poll_terminal_status "$(cat "${OUT_DIR}/job-id.txt")" || true
|
|
if [[ "$ROLLBACK_PHASE" == "after-terminal" ]]; then
|
|
replace_node2_with_old_image
|
|
wait_http_ok "$(hot_endpoint 2)/health" "rollback-node2-live" || true
|
|
fi
|
|
capture_old_node_readback "$(cat "${OUT_DIR}/job-id.txt")"
|
|
head_probe
|
|
else
|
|
log_warn "Skipping terminal polling because no manual transition job was admitted"
|
|
fi
|
|
collect_logs
|
|
summarize
|
|
}
|
|
|
|
main "$@"
|