Require managed-containerd protocol v1
This commit is contained in:
Executable
+270
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.." &&
|
||||
pwd
|
||||
)"
|
||||
|
||||
PROXY="$ROOT/ocf/loxthelion/managed-container.d/proxy.sh"
|
||||
|
||||
TMPDIR_TEST="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR_TEST"' EXIT
|
||||
|
||||
export OCF_RESOURCE_INSTANCE="postgresql-ha"
|
||||
export MC_PROXY_STATE_DIR="$TMPDIR_TEST/state"
|
||||
|
||||
readonly MC_PROXY_RC_TRANSPORT=10
|
||||
readonly MC_PROXY_RC_HARD_FAILURE=11
|
||||
|
||||
OCF_SUCCESS=0
|
||||
OCF_RUNNING_PROMOTED=8
|
||||
|
||||
ocf_log()
|
||||
{
|
||||
:
|
||||
}
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$PROXY"
|
||||
|
||||
|
||||
start_fake_daemon()
|
||||
{
|
||||
local mode="$1"
|
||||
local socket_path="$2"
|
||||
|
||||
python3 - "$mode" "$socket_path" <<'PY' &
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
mode, path = sys.argv[1], sys.argv[2]
|
||||
|
||||
try:
|
||||
os.unlink(path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server.bind(path)
|
||||
server.listen(1)
|
||||
|
||||
conn, _ = server.accept()
|
||||
|
||||
with conn:
|
||||
raw = b""
|
||||
|
||||
while b"\n" not in raw:
|
||||
chunk = conn.recv(65536)
|
||||
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
raw += chunk
|
||||
|
||||
if mode == "timeout":
|
||||
time.sleep(5)
|
||||
raise SystemExit(0)
|
||||
|
||||
if mode == "empty":
|
||||
# Clean EOF after a successful request exchange. This is a reachable
|
||||
# daemon that failed to produce a protocol response, so it is a hard
|
||||
# protocol/result failure rather than a transport failure.
|
||||
raise SystemExit(0)
|
||||
|
||||
if mode == "reset":
|
||||
# Force an abortive close instead of a clean EOF. On Linux this should
|
||||
# surface to the client as a connection-reset transport error.
|
||||
conn.setsockopt(
|
||||
socket.SOL_SOCKET,
|
||||
socket.SO_LINGER,
|
||||
struct.pack("ii", 1, 0),
|
||||
)
|
||||
raise SystemExit(0)
|
||||
|
||||
if mode == "malformed":
|
||||
conn.sendall(b"{not-json}\n")
|
||||
raise SystemExit(0)
|
||||
|
||||
req = json.loads(raw.decode("utf-8").strip())
|
||||
|
||||
# Every structured-response test simultaneously verifies that the
|
||||
# authoritative proxy sent the v1 request envelope.
|
||||
if (
|
||||
req.get("protocol_version") != 1
|
||||
or req.get("action") != "monitor"
|
||||
or req.get("resource") != "postgresql-ha"
|
||||
):
|
||||
response = {
|
||||
"ok": False,
|
||||
"protocol_version": 1,
|
||||
"error": "bad request envelope",
|
||||
}
|
||||
else:
|
||||
response = {
|
||||
"ok": True,
|
||||
"protocol_version": 1,
|
||||
"resource": "postgresql-ha",
|
||||
"action": "monitor",
|
||||
"result": {
|
||||
"ha": {
|
||||
"ocf_rc": 0,
|
||||
"health": "healthy",
|
||||
"ha_role": "unpromoted",
|
||||
"state": "streaming",
|
||||
"promotion_eligible": True,
|
||||
},
|
||||
"role": "standby",
|
||||
"streaming": True,
|
||||
"wal_receiver": {
|
||||
"slot_name": "ha_pg_test",
|
||||
"sender_host": "192.0.2.1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if mode == "missing_protocol":
|
||||
response.pop("protocol_version", None)
|
||||
|
||||
elif mode == "wrong_protocol":
|
||||
response["protocol_version"] = 999
|
||||
|
||||
elif mode == "wrong_resource":
|
||||
response["resource"] = "some-other-resource"
|
||||
|
||||
elif mode == "wrong_action":
|
||||
response["action"] = "start"
|
||||
|
||||
elif mode == "daemon_error":
|
||||
response = {
|
||||
"ok": False,
|
||||
"protocol_version": 1,
|
||||
"error_code": "request_error",
|
||||
"error": "synthetic daemon error",
|
||||
}
|
||||
|
||||
elif mode == "bad_rc":
|
||||
response["result"]["ha"]["ocf_rc"] = 42
|
||||
|
||||
conn.sendall(
|
||||
json.dumps(response).encode("utf-8") + b"\n"
|
||||
)
|
||||
|
||||
server.close()
|
||||
PY
|
||||
|
||||
FAKE_PID=$!
|
||||
|
||||
# Wait until the fake daemon has actually created its socket.
|
||||
local i
|
||||
for i in {1..100}; do
|
||||
[[ -S "$socket_path" ]] && return 0
|
||||
sleep 0.01
|
||||
done
|
||||
|
||||
echo "ERROR: fake daemon failed to create socket: $socket_path" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
run_case()
|
||||
{
|
||||
local name="$1"
|
||||
local mode="$2"
|
||||
local expected_rc="$3"
|
||||
|
||||
local socket_path="$TMPDIR_TEST/${name}.sock"
|
||||
local output=""
|
||||
local actual_rc=0
|
||||
|
||||
rm -f "$socket_path"
|
||||
|
||||
if [[ "$mode" == "missing_socket" ]]; then
|
||||
export MC_PROXY_SOCKET="$socket_path"
|
||||
|
||||
elif [[ "$mode" == "refused" ]]; then
|
||||
# Leave a Unix socket inode behind with no listening process.
|
||||
# The shell-level -S check succeeds, but connect() must fail with
|
||||
# ECONNREFUSED/OSError and therefore classify as transport failure.
|
||||
python3 - "$socket_path" <<'PY_REFUSED'
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
|
||||
try:
|
||||
os.unlink(path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
s.bind(path)
|
||||
s.close()
|
||||
PY_REFUSED
|
||||
|
||||
export MC_PROXY_SOCKET="$socket_path"
|
||||
|
||||
else
|
||||
start_fake_daemon "$mode" "$socket_path"
|
||||
export MC_PROXY_SOCKET="$socket_path"
|
||||
fi
|
||||
|
||||
set +e
|
||||
output="$(mc_proxy_monitor)"
|
||||
actual_rc=$?
|
||||
set -e
|
||||
|
||||
case "$mode" in
|
||||
missing_socket|refused)
|
||||
;;
|
||||
*)
|
||||
wait "${FAKE_PID:-}" 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%-22s expected=%-2s actual=%-2s' \
|
||||
"$name" \
|
||||
"$expected_rc" \
|
||||
"$actual_rc"
|
||||
|
||||
if [[ -n "$output" ]]; then
|
||||
printf ' output=%s' "$output"
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
|
||||
if [[ "$actual_rc" -ne "$expected_rc" ]]; then
|
||||
echo "FAIL: $name returned $actual_rc, expected $expected_rc" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
echo "===== PROXY PROTOCOL CLASSIFICATION ====="
|
||||
|
||||
run_case valid valid 0
|
||||
run_case missing_protocol missing_protocol 11
|
||||
run_case wrong_protocol wrong_protocol 11
|
||||
run_case wrong_resource wrong_resource 11
|
||||
run_case wrong_action wrong_action 11
|
||||
run_case daemon_error daemon_error 11
|
||||
run_case malformed_json malformed 11
|
||||
run_case invalid_ocf_rc bad_rc 11
|
||||
run_case empty_response empty 11
|
||||
|
||||
# AF_UNIX abortive close after a completed request is observed on this
|
||||
# platform as EOF, not ECONNRESET. A reachable peer that supplies no
|
||||
# response is therefore correctly classified as a hard protocol failure.
|
||||
run_case abortive_close reset 11
|
||||
|
||||
run_case missing_socket missing_socket 10
|
||||
run_case refused_socket refused 10
|
||||
run_case timeout timeout 10
|
||||
|
||||
echo
|
||||
echo "PROXY_PROTOCOL_TEST_PASS=1"
|
||||
Reference in New Issue
Block a user