mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-06 13:27:43 +00:00
291 lines
8.8 KiB
Bash
Executable File
291 lines
8.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Run the ignored RustFS KMS failover test against an ephemeral, official
|
|
# three-node Vault cluster using integrated Raft storage. The active container
|
|
# is killed only after KV2 and Transit decrypt loops report ready.
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
VAULT_IMAGE="${RUSTFS_TEST_VAULT_IMAGE:-hashicorp/vault:1.17.6}"
|
|
|
|
for command in docker jq; do
|
|
if ! command -v "$command" >/dev/null 2>&1; then
|
|
echo "$command is required for the live Vault HA test" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
if ! docker info >/dev/null 2>&1; then
|
|
echo "Docker is not available" >&2
|
|
exit 1
|
|
fi
|
|
if ! docker image inspect "$VAULT_IMAGE" >/dev/null 2>&1; then
|
|
docker pull "$VAULT_IMAGE" >/dev/null
|
|
fi
|
|
|
|
TMP_DIR="$(mktemp -d -t rustfs-vault-ha-live.XXXXXX)"
|
|
RUN_ID="rustfs-kms-ha-$$-${RANDOM}"
|
|
NETWORK="${RUN_ID}"
|
|
MARKER="${TMP_DIR}/failover-ready"
|
|
ROOT_TOKEN=""
|
|
UNSEAL_KEY=""
|
|
TEST_PID=""
|
|
declare -a NODES=("${RUN_ID}-1" "${RUN_ID}-2" "${RUN_ID}-3")
|
|
|
|
cleanup() {
|
|
if [[ -n "$TEST_PID" ]]; then
|
|
kill "$TEST_PID" 2>/dev/null || true
|
|
wait "$TEST_PID" 2>/dev/null || true
|
|
fi
|
|
for node in "${NODES[@]}"; do
|
|
docker rm -f "$node" >/dev/null 2>&1 || true
|
|
done
|
|
docker network rm "$NETWORK" >/dev/null 2>&1 || true
|
|
rm -rf "$TMP_DIR"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
docker network create "$NETWORK" >/dev/null
|
|
|
|
start_node() {
|
|
local index="$1"
|
|
local node="${NODES[$((index - 1))]}"
|
|
local config
|
|
config="$(jq -cn \
|
|
--arg api "http://${node}:8200" \
|
|
--arg cluster "http://${node}:8201" \
|
|
--arg node_id "node${index}" \
|
|
'{
|
|
ui: false,
|
|
disable_mlock: true,
|
|
api_addr: $api,
|
|
cluster_addr: $cluster,
|
|
listener: [{tcp: {
|
|
address: "0.0.0.0:8200",
|
|
cluster_address: "0.0.0.0:8201",
|
|
tls_disable: true
|
|
}}],
|
|
storage: {raft: {path: "/vault/file", node_id: $node_id, performance_multiplier: 1}}
|
|
}')"
|
|
docker run -d \
|
|
--name "$node" \
|
|
--network "$NETWORK" \
|
|
--cap-add IPC_LOCK \
|
|
-p 127.0.0.1::8200 \
|
|
-e VAULT_LOCAL_CONFIG="$config" \
|
|
"$VAULT_IMAGE" server >/dev/null
|
|
}
|
|
|
|
vault_exec() {
|
|
local node="$1"
|
|
shift
|
|
docker exec \
|
|
-e VAULT_ADDR=http://127.0.0.1:8200 \
|
|
-e VAULT_TOKEN="$ROOT_TOKEN" \
|
|
"$node" vault "$@"
|
|
}
|
|
|
|
wait_started() {
|
|
local node="$1"
|
|
local status
|
|
for _ in $(seq 1 120); do
|
|
status="$(docker exec -e VAULT_ADDR=http://127.0.0.1:8200 "$node" vault status -format=json 2>/dev/null || true)"
|
|
if jq -e '.initialized == false or .sealed == true or .sealed == false' >/dev/null 2>&1 <<<"$status"; then
|
|
return 0
|
|
fi
|
|
sleep 0.25
|
|
done
|
|
echo "$node did not start" >&2
|
|
docker logs "$node" >&2 || true
|
|
return 1
|
|
}
|
|
|
|
active_node() {
|
|
local node status address
|
|
for node in "${NODES[@]}"; do
|
|
if ! docker inspect "$node" >/dev/null 2>&1; then
|
|
continue
|
|
fi
|
|
status="$(docker exec -e VAULT_ADDR=http://127.0.0.1:8200 "$node" vault status -format=json 2>/dev/null || true)"
|
|
address="http://${node}:8200"
|
|
if jq -e --arg address "$address" \
|
|
'.ha_enabled == true and .sealed == false and (.is_self == true or (.leader_address == $address and .active_time != "0001-01-01T00:00:00Z"))' \
|
|
>/dev/null 2>&1 <<<"$status"; then
|
|
printf '%s\n' "$node"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
host_address() {
|
|
local node="$1"
|
|
local port
|
|
port="$(docker port "$node" 8200/tcp | awk -F: 'NR == 1 {print $NF}')"
|
|
if [[ ! "$port" =~ ^[0-9]+$ ]]; then
|
|
echo "failed to resolve host port for $node" >&2
|
|
return 1
|
|
fi
|
|
printf 'http://127.0.0.1:%s\n' "$port"
|
|
}
|
|
|
|
for index in 1 2 3; do
|
|
start_node "$index"
|
|
wait_started "${NODES[$((index - 1))]}"
|
|
done
|
|
|
|
INIT_JSON="$(docker exec -e VAULT_ADDR=http://127.0.0.1:8200 "${NODES[0]}" vault operator init -format=json -key-shares=1 -key-threshold=1)"
|
|
ROOT_TOKEN="$(jq -r '.root_token' <<<"$INIT_JSON")"
|
|
UNSEAL_KEY="$(jq -r '.unseal_keys_b64[0]' <<<"$INIT_JSON")"
|
|
if [[ -z "$ROOT_TOKEN" || -z "$UNSEAL_KEY" || "$ROOT_TOKEN" == null || "$UNSEAL_KEY" == null ]]; then
|
|
echo "Vault initialization did not return the expected credentials" >&2
|
|
exit 1
|
|
fi
|
|
|
|
vault_exec "${NODES[0]}" operator unseal "$UNSEAL_KEY" >/dev/null
|
|
for node in "${NODES[@]:1}"; do
|
|
vault_exec "$node" operator raft join "http://${NODES[0]}:8200" >/dev/null
|
|
vault_exec "$node" operator unseal "$UNSEAL_KEY" >/dev/null
|
|
done
|
|
|
|
for _ in $(seq 1 240); do
|
|
if vault_exec "${NODES[0]}" operator raft list-peers -format=json 2>/dev/null \
|
|
| jq -e '.data.config.servers | length == 3 and all(.[]; .voter == true)' >/dev/null; then
|
|
break
|
|
fi
|
|
sleep 0.25
|
|
done
|
|
if ! vault_exec "${NODES[0]}" operator raft list-peers -format=json \
|
|
| jq -e '.data.config.servers | length == 3 and all(.[]; .voter == true)' >/dev/null; then
|
|
echo "Vault Raft cluster did not stabilize with three voters" >&2
|
|
vault_exec "${NODES[0]}" operator raft list-peers -format=json >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
OLD_LEADER="$(active_node)"
|
|
if [[ -z "$OLD_LEADER" ]]; then
|
|
echo "Vault did not elect an active node" >&2
|
|
exit 1
|
|
fi
|
|
STANDBY=""
|
|
for node in "${NODES[@]}"; do
|
|
if [[ "$node" != "$OLD_LEADER" ]]; then
|
|
STANDBY="$node"
|
|
break
|
|
fi
|
|
done
|
|
VAULT_ADDR="$(host_address "$STANDBY")"
|
|
|
|
vault_exec "$OLD_LEADER" audit enable file file_path=/tmp/vault-audit.log >/dev/null
|
|
vault_exec "$OLD_LEADER" secrets enable -path=secret kv-v2 >/dev/null
|
|
vault_exec "$OLD_LEADER" secrets enable -path=transit transit >/dev/null
|
|
vault_exec "$OLD_LEADER" auth enable approle >/dev/null
|
|
|
|
POLICY_NAME="rustfs-kms-ha"
|
|
ROLE_NAME="rustfs-kms-ha"
|
|
POLICY_FILE="${TMP_DIR}/kms-policy.hcl"
|
|
cat >"$POLICY_FILE" <<'EOF'
|
|
path "secret/data/rustfs/kms/ha-kv2/*" {
|
|
capabilities = ["create", "read", "update"]
|
|
}
|
|
path "secret/metadata/rustfs/kms/ha-kv2/*" {
|
|
capabilities = ["list", "read", "delete"]
|
|
}
|
|
path "secret/metadata/rustfs/kms/ha-kv2" {
|
|
capabilities = ["list"]
|
|
}
|
|
path "secret/data/rustfs/kms/ha-transit-metadata/*" {
|
|
capabilities = ["create", "read", "update"]
|
|
}
|
|
path "secret/metadata/rustfs/kms/ha-transit-metadata/*" {
|
|
capabilities = ["list", "read", "delete"]
|
|
}
|
|
path "secret/metadata/rustfs/kms/ha-transit-metadata" {
|
|
capabilities = ["list"]
|
|
}
|
|
path "transit/keys" {
|
|
capabilities = ["list"]
|
|
}
|
|
path "transit/keys/*" {
|
|
capabilities = ["create", "read", "update"]
|
|
}
|
|
path "transit/encrypt/*" {
|
|
capabilities = ["update"]
|
|
}
|
|
path "transit/decrypt/*" {
|
|
capabilities = ["update"]
|
|
}
|
|
EOF
|
|
docker cp "$POLICY_FILE" "${OLD_LEADER}:/tmp/kms-policy.hcl"
|
|
vault_exec "$OLD_LEADER" policy write "$POLICY_NAME" /tmp/kms-policy.hcl >/dev/null
|
|
vault_exec "$OLD_LEADER" write "auth/approle/role/${ROLE_NAME}" \
|
|
token_policies="$POLICY_NAME" \
|
|
token_ttl=10m \
|
|
token_max_ttl=20m \
|
|
secret_id_ttl=30m \
|
|
secret_id_num_uses=0 \
|
|
>/dev/null
|
|
ROLE_ID="$(vault_exec "$OLD_LEADER" read -field=role_id "auth/approle/role/${ROLE_NAME}/role-id")"
|
|
SECRET_ID="$(vault_exec "$OLD_LEADER" write -f -field=secret_id "auth/approle/role/${ROLE_NAME}/secret-id")"
|
|
|
|
cd "$PROJECT_ROOT"
|
|
cargo test -p rustfs-kms --test vault_ha_failover_live --no-run
|
|
env \
|
|
-u RUSTFS_KMS_VAULT_TOKEN \
|
|
-u RUSTFS_KMS_VAULT_TOKEN_FILE \
|
|
HTTP_PROXY= HTTPS_PROXY= http_proxy= https_proxy= \
|
|
NO_PROXY=127.0.0.1,localhost no_proxy=127.0.0.1,localhost \
|
|
RUSTFS_TEST_VAULT_ADDRESS="$VAULT_ADDR" \
|
|
RUSTFS_TEST_VAULT_ROLE_ID="$ROLE_ID" \
|
|
RUSTFS_TEST_VAULT_SECRET_ID="$SECRET_ID" \
|
|
RUSTFS_TEST_VAULT_FAILOVER_MARKER="$MARKER" \
|
|
RUSTFS_TEST_VAULT_OLD_LEADER="$OLD_LEADER" \
|
|
cargo test -p rustfs-kms --test vault_ha_failover_live \
|
|
vault_raft_leader_failure_preserves_kv2_and_transit_decrypts -- \
|
|
--ignored --nocapture --test-threads=1 &
|
|
TEST_PID=$!
|
|
|
|
for _ in $(seq 1 240); do
|
|
if [[ -f "$MARKER" ]]; then
|
|
break
|
|
fi
|
|
if ! kill -0 "$TEST_PID" 2>/dev/null; then
|
|
wait "$TEST_PID"
|
|
fi
|
|
sleep 0.25
|
|
done
|
|
if [[ ! -f "$MARKER" ]]; then
|
|
echo "KMS live test did not reach failover readiness" >&2
|
|
exit 1
|
|
fi
|
|
|
|
docker kill "$OLD_LEADER" >/dev/null
|
|
NEW_LEADER=""
|
|
for _ in $(seq 1 240); do
|
|
NEW_LEADER="$(active_node || true)"
|
|
if [[ -n "$NEW_LEADER" && "$NEW_LEADER" != "$OLD_LEADER" ]]; then
|
|
break
|
|
fi
|
|
sleep 0.25
|
|
done
|
|
if [[ -z "$NEW_LEADER" || "$NEW_LEADER" == "$OLD_LEADER" ]]; then
|
|
echo "Vault did not elect a replacement leader after killing $OLD_LEADER" >&2
|
|
exit 1
|
|
fi
|
|
printf '%s' "$NEW_LEADER" >"${MARKER%/*}/failover-ready.elected"
|
|
|
|
wait "$TEST_PID"
|
|
TEST_PID=""
|
|
|
|
for index in 1 2 3; do
|
|
docker cp "${NODES[$((index - 1))]}:/tmp/vault-audit.log" "${TMP_DIR}/vault-audit-${index}.log" >/dev/null 2>&1 || true
|
|
done
|
|
APPROLE_LOGINS="$(jq -s '[.[] | select(.type == "request" and .request.path == "auth/approle/login" and .request.operation == "update")] | length' \
|
|
"${TMP_DIR}"/vault-audit-*.log 2>/dev/null || true)"
|
|
if [[ "$APPROLE_LOGINS" != 2 ]]; then
|
|
echo "expected exactly two AppRole logins (one per backend), got ${APPROLE_LOGINS:-unavailable}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Vault HA failover passed: 3 Raft voters, ${OLD_LEADER} killed, ${NEW_LEADER} elected, 2 AppRole logins"
|