208 lines
7.7 KiB
Bash
Executable file
208 lines
7.7 KiB
Bash
Executable file
#!/bin/sh
|
|
set -eu
|
|
|
|
if [ "${GLOVE_REMOTE_WORKERD_VALIDATE:-}" != "1" ]; then
|
|
echo "refusing: set GLOVE_REMOTE_WORKERD_VALIDATE=1 for this validation-only run" >&2
|
|
exit 2
|
|
fi
|
|
if [ "$#" -ne 2 ]; then
|
|
echo "usage: GLOVE_REMOTE_WORKERD_VALIDATE=1 $0 /absolute/glove-remote-exec /absolute/schema-2-config" >&2
|
|
exit 2
|
|
fi
|
|
|
|
executor=$1
|
|
config=$2
|
|
case "$executor:$config" in
|
|
/*:/*) ;;
|
|
*) echo "executor and config must be absolute paths" >&2; exit 2 ;;
|
|
esac
|
|
if [ ! -f "$executor" ] || [ -L "$executor" ] || [ ! -x "$executor" ]; then
|
|
echo "executor must be a preverified regular executable, not a symlink" >&2
|
|
exit 2
|
|
fi
|
|
if [ ! -f "$config" ] || [ -L "$config" ]; then
|
|
echo "config must be a preverified regular file, not a symlink" >&2
|
|
exit 2
|
|
fi
|
|
for required in \
|
|
GLOVE_REMOTE_EXECUTOR_SHA256 \
|
|
GLOVE_REMOTE_IMAGE \
|
|
GLOVE_REMOTE_WORKERD_SHA256 \
|
|
GLOVE_REMOTE_DESCRIPTOR_SHA256
|
|
do
|
|
eval "value=\${$required:-}"
|
|
if [ -z "$value" ]; then
|
|
echo "refusing: $required must be supplied from preverified operator input" >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
|
|
# Python is used only from the local standard library. This script never
|
|
# installs, fetches, logs in, mounts host paths, or supplies Docker options.
|
|
exec /usr/bin/env -i \
|
|
GLOVE_REMOTE_EXECUTOR_SHA256="$GLOVE_REMOTE_EXECUTOR_SHA256" \
|
|
GLOVE_REMOTE_IMAGE="$GLOVE_REMOTE_IMAGE" \
|
|
GLOVE_REMOTE_WORKERD_SHA256="$GLOVE_REMOTE_WORKERD_SHA256" \
|
|
GLOVE_REMOTE_DESCRIPTOR_SHA256="$GLOVE_REMOTE_DESCRIPTOR_SHA256" \
|
|
/usr/bin/python3 - "$executor" "$config" <<'PY'
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import secrets
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
executor = Path(sys.argv[1])
|
|
config_path = Path(sys.argv[2])
|
|
expected_keys = [
|
|
"schema_version",
|
|
"executor_digest",
|
|
"container_image",
|
|
"container_image_digest",
|
|
"workerd_digest",
|
|
"descriptor_digest",
|
|
"staging_root",
|
|
"max_sessions",
|
|
"max_ttl_ms",
|
|
"docker_executable",
|
|
"docker_executable_digest",
|
|
]
|
|
|
|
raw = config_path.read_text(encoding="ascii")
|
|
if not raw.endswith("\n") or "\r" in raw or "\x00" in raw:
|
|
raise SystemExit("invalid canonical executor config")
|
|
lines = raw.splitlines()
|
|
if len(lines) != len(expected_keys):
|
|
raise SystemExit("invalid canonical executor config field count")
|
|
config = {}
|
|
for key, line in zip(expected_keys, lines):
|
|
prefix = key + "="
|
|
if not line.startswith(prefix) or not line[len(prefix):]:
|
|
raise SystemExit("invalid canonical executor config ordering")
|
|
config[key] = line[len(prefix):]
|
|
|
|
sha = lambda path: "sha256:" + hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
digest_re = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
|
image_re = re.compile(r"[a-z0-9][a-z0-9._-]*(?:/[a-z0-9][a-z0-9._-]*)+@sha256:[0-9a-f]{64}\Z")
|
|
if config["schema_version"] != "2":
|
|
raise SystemExit("validation requires schema_version=2")
|
|
if config["executor_digest"] != os.environ["GLOVE_REMOTE_EXECUTOR_SHA256"]:
|
|
raise SystemExit("operator executor digest does not match config")
|
|
if sha(executor) != config["executor_digest"]:
|
|
raise SystemExit("executor binary digest mismatch")
|
|
if config["container_image"] != os.environ["GLOVE_REMOTE_IMAGE"]:
|
|
raise SystemExit("operator image identity does not match config")
|
|
if not image_re.fullmatch(config["container_image"]):
|
|
raise SystemExit("image must be a canonical digest-qualified name")
|
|
if not config["container_image"].endswith("@" + config["container_image_digest"]):
|
|
raise SystemExit("image digest suffix mismatch")
|
|
if config["workerd_digest"] != os.environ["GLOVE_REMOTE_WORKERD_SHA256"]:
|
|
raise SystemExit("operator workerd digest does not match config")
|
|
if config["descriptor_digest"] != os.environ["GLOVE_REMOTE_DESCRIPTOR_SHA256"]:
|
|
raise SystemExit("operator descriptor digest does not match config")
|
|
if not digest_re.fullmatch(config["workerd_digest"]) or not digest_re.fullmatch(config["descriptor_digest"]):
|
|
raise SystemExit("invalid fixed descriptor identity")
|
|
if config["docker_executable"] != "/usr/bin/docker":
|
|
raise SystemExit("validation executor requires /usr/bin/docker")
|
|
if sha("/usr/bin/docker") != config["docker_executable_digest"]:
|
|
raise SystemExit("Docker executable digest mismatch")
|
|
|
|
# `image inspect` is local and read-only. `run --pull never` is also hardcoded
|
|
# inside the checked executor, so a missing image fails instead of fetching.
|
|
subprocess.run(
|
|
["/usr/bin/docker", "image", "inspect", config["container_image"]],
|
|
env={}, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL, check=True, timeout=10,
|
|
)
|
|
|
|
process = subprocess.Popen(
|
|
[str(executor), "--local-validation-stdio", str(config_path)],
|
|
env={}, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None,
|
|
)
|
|
assert process.stdin is not None and process.stdout is not None
|
|
session_id = "local-workerd-validation"
|
|
epoch = secrets.token_hex(16)
|
|
descriptor = config["descriptor_digest"]
|
|
ttl = min(int(config["max_ttl_ms"]), 60_000)
|
|
request_number = 0
|
|
|
|
def canonical(method, payload):
|
|
value = (
|
|
"glove-remote-validation-v1\n"
|
|
f"method={method}\n"
|
|
f"session_id={payload['session_id']}\n"
|
|
f"session_epoch={payload['session_epoch']}\n"
|
|
f"descriptor_digest={payload['descriptor_digest']}\n"
|
|
f"idempotency_key={payload['idempotency_key']}"
|
|
)
|
|
if method == "remote_read":
|
|
value += f"\ncursor={payload['cursor']}\nmax_bytes={payload['max_bytes']}"
|
|
return value + "\n"
|
|
|
|
def call(method, key, **specific):
|
|
global request_number
|
|
request_number += 1
|
|
payload = {
|
|
"schema_version": 1,
|
|
"session_id": session_id,
|
|
"session_epoch": epoch,
|
|
"descriptor_digest": descriptor,
|
|
"idempotency_key": key,
|
|
"payload_digest": "sha256:" + "0" * 64,
|
|
**specific,
|
|
}
|
|
payload["payload_digest"] = "sha256:" + hashlib.sha256(
|
|
canonical(method, payload).encode("ascii")
|
|
).hexdigest()
|
|
request = json.dumps({
|
|
"jsonrpc": "2.0",
|
|
"id": f"local-{request_number}",
|
|
"method": method,
|
|
"deadline_remaining_ms": ttl,
|
|
"payload": payload,
|
|
}, separators=(",", ":")).encode("utf-8")
|
|
process.stdin.write(struct.pack(">I", len(request)) + request)
|
|
process.stdin.flush()
|
|
header = process.stdout.read(4)
|
|
if len(header) != 4:
|
|
raise RuntimeError("executor closed before response")
|
|
size = struct.unpack(">I", header)[0]
|
|
if size == 0 or size > 1024 * 1024:
|
|
raise RuntimeError("invalid executor response frame")
|
|
response = json.loads(process.stdout.read(size))
|
|
if response.get("error"):
|
|
raise RuntimeError(response["error"]["code"] + ": " + response["error"]["message"])
|
|
return response["result"]
|
|
|
|
try:
|
|
print(json.dumps(call("remote_prepare", "prepare"), sort_keys=True))
|
|
print(json.dumps(call("remote_start", "start"), sort_keys=True))
|
|
cursor = 0
|
|
for attempt in range(600):
|
|
chunk = call("remote_read", f"read-{attempt}", cursor=cursor, max_bytes=16384)
|
|
if chunk["bytes"]:
|
|
sys.stdout.write(chunk["bytes"])
|
|
sys.stdout.flush()
|
|
cursor = chunk["next_cursor"]
|
|
terminal = call("remote_wait", f"wait-{attempt}")
|
|
if terminal["state"] != "running":
|
|
print(json.dumps(terminal, sort_keys=True))
|
|
break
|
|
time.sleep(0.05)
|
|
else:
|
|
raise RuntimeError("validator did not terminate within the bounded polling window")
|
|
print(json.dumps(call("remote_cleanup", "cleanup"), sort_keys=True))
|
|
finally:
|
|
process.stdin.close()
|
|
try:
|
|
process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait()
|
|
if process.returncode != 0:
|
|
raise SystemExit(process.returncode)
|
|
PY
|