Require managed-containerd protocol v1
This commit is contained in:
@@ -15,12 +15,13 @@ d7d2fd7ab651d1f2f026bdb11d62e2c0c6ce6ee3128c9814d09550cf4304b3f2 ocf/loxthelion
|
|||||||
3a27ac9c63c61310da8b8711fa2c4400b37ea8eae2ee2a79e28bc63180b06c7c ocf/loxthelion/managed-container.d/container.sh
|
3a27ac9c63c61310da8b8711fa2c4400b37ea8eae2ee2a79e28bc63180b06c7c ocf/loxthelion/managed-container.d/container.sh
|
||||||
70431e0d390a3612339c7b5887286c1be4d6b305cbe8898e7895a211c26b6087 ocf/loxthelion/managed-container.d/engine.sh
|
70431e0d390a3612339c7b5887286c1be4d6b305cbe8898e7895a211c26b6087 ocf/loxthelion/managed-container.d/engine.sh
|
||||||
6c92c095ce10ac95d843fdc1c49e31c19491ca22fdac960affd8649606d76d82 ocf/loxthelion/managed-container.d/lifecycles/systemd.sh
|
6c92c095ce10ac95d843fdc1c49e31c19491ca22fdac960affd8649606d76d82 ocf/loxthelion/managed-container.d/lifecycles/systemd.sh
|
||||||
259e8cafa5b6d3fb54896ab37ff3917942c62c51eaa6e9bfa8ccf3afc6a7327b ocf/loxthelion/managed-container.d/proxy.sh
|
9f934833ac7d1ad845244f2fe9c3ce3d93787e36f3c36b0c5896364cbd82fd77 ocf/loxthelion/managed-container.d/proxy.sh
|
||||||
f82e2571d309bce8e4d7e6aee6b61b68feb7d99dc85bb8b688b6123c13ccf6a7 ocf/loxthelion/managed-container.d/runtimes/podman.sh
|
f82e2571d309bce8e4d7e6aee6b61b68feb7d99dc85bb8b688b6123c13ccf6a7 ocf/loxthelion/managed-container.d/runtimes/podman.sh
|
||||||
4a99efea3d4a185e0630db89cfacc28aa8777bad9c747794ae69f70d87cd0327 pacemaker/README.md
|
4a99efea3d4a185e0630db89cfacc28aa8777bad9c747794ae69f70d87cd0327 pacemaker/README.md
|
||||||
a42bb59a058ffeb59c1baa96e403856910bbbaea98f9104ff93daf4b5970c780 postgres-host/postgres-ha-bootstrap
|
a42bb59a058ffeb59c1baa96e403856910bbbaea98f9104ff93daf4b5970c780 postgres-host/postgres-ha-bootstrap
|
||||||
957a5a72ed277d0216e070abb80733483be3d421b9fa488e27fdd75bb2919548 postgres-host/postgres-ha-peer-config.sh
|
957a5a72ed277d0216e070abb80733483be3d421b9fa488e27fdd75bb2919548 postgres-host/postgres-ha-peer-config.sh
|
||||||
deb09e3ae40629f43b262ba4f9a7ebf59a1dca3da9d8c94478f670aea28bd0e1 quadlet/postgres.container
|
deb09e3ae40629f43b262ba4f9a7ebf59a1dca3da9d8c94478f670aea28bd0e1 quadlet/postgres.container
|
||||||
04a125068677bbee6e71d313ca9d95d35faa32851b79de781cd7b9f466561445 quadlet/postgres.env.example
|
04a125068677bbee6e71d313ca9d95d35faa32851b79de781cd7b9f466561445 quadlet/postgres.env.example
|
||||||
93f8b5c4a0c0e390fadeb680848156d198d356086574793032c5d2286c90c86b tests/run-all.sh
|
45f398c0e494c86efb92bfa9381617c7dbb4b929c9efb5ff86473d2dc6fe729e tests/run-all.sh
|
||||||
5e5d4fdd2b6c7e0b8b146de7aad06c1f96b221a10fec525b00d40e0d85709fb0 tests/test-entrypoint-structure.sh
|
5e5d4fdd2b6c7e0b8b146de7aad06c1f96b221a10fec525b00d40e0d85709fb0 tests/test-entrypoint-structure.sh
|
||||||
|
c3b988d7b2b1b138bb2530df9172f96bb0f5edc2f1561f38f0055695f4126134 tests/test-proxy-protocol.sh
|
||||||
|
|||||||
@@ -146,7 +146,10 @@ path, resource = sys.argv[1], sys.argv[2]
|
|||||||
TRANSPORT_FAILURE = int(sys.argv[3])
|
TRANSPORT_FAILURE = int(sys.argv[3])
|
||||||
PROTOCOL_FAILURE = int(sys.argv[4])
|
PROTOCOL_FAILURE = int(sys.argv[4])
|
||||||
|
|
||||||
|
PROTOCOL_VERSION = 1
|
||||||
|
|
||||||
request = {
|
request = {
|
||||||
|
"protocol_version": PROTOCOL_VERSION,
|
||||||
"action": "monitor",
|
"action": "monitor",
|
||||||
"resource": resource,
|
"resource": resource,
|
||||||
}
|
}
|
||||||
@@ -189,9 +192,20 @@ except (UnicodeDecodeError, json.JSONDecodeError):
|
|||||||
if not isinstance(obj, dict):
|
if not isinstance(obj, dict):
|
||||||
raise SystemExit(PROTOCOL_FAILURE)
|
raise SystemExit(PROTOCOL_FAILURE)
|
||||||
|
|
||||||
|
# The daemon is reachable at this point, so any protocol mismatch is a hard
|
||||||
|
# control-plane failure. It must never be hidden by cached application health.
|
||||||
|
if obj.get("protocol_version") != PROTOCOL_VERSION:
|
||||||
|
raise SystemExit(PROTOCOL_FAILURE)
|
||||||
|
|
||||||
if not obj.get("ok"):
|
if not obj.get("ok"):
|
||||||
raise SystemExit(PROTOCOL_FAILURE)
|
raise SystemExit(PROTOCOL_FAILURE)
|
||||||
|
|
||||||
|
if obj.get("resource") != resource:
|
||||||
|
raise SystemExit(PROTOCOL_FAILURE)
|
||||||
|
|
||||||
|
if obj.get("action") != "monitor":
|
||||||
|
raise SystemExit(PROTOCOL_FAILURE)
|
||||||
|
|
||||||
result = obj.get("result")
|
result = obj.get("result")
|
||||||
if not isinstance(result, dict):
|
if not isinstance(result, dict):
|
||||||
raise SystemExit(PROTOCOL_FAILURE)
|
raise SystemExit(PROTOCOL_FAILURE)
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ echo
|
|||||||
echo ">>> Entrypoint / shell structure"
|
echo ">>> Entrypoint / shell structure"
|
||||||
./tests/test-entrypoint-structure.sh
|
./tests/test-entrypoint-structure.sh
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo ">>> Proxy protocol contract"
|
||||||
|
./tests/test-proxy-protocol.sh
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo ">>> Python syntax"
|
echo ">>> Python syntax"
|
||||||
|
|
||||||
|
|||||||
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