|
|
|
@@ -563,7 +563,7 @@ async function status(
|
|
|
|
|
config,
|
|
|
|
|
target.route,
|
|
|
|
|
["sh"],
|
|
|
|
|
statusScript(
|
|
|
|
|
renderPublicEdgeStatusScript(
|
|
|
|
|
target,
|
|
|
|
|
resolution.sites,
|
|
|
|
|
artifacts?.caddyfile ?? null,
|
|
|
|
@@ -884,7 +884,7 @@ PY
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function statusScript(
|
|
|
|
|
export function renderPublicEdgeStatusScript(
|
|
|
|
|
target: PublicEdgeTarget,
|
|
|
|
|
sites: ResolvedSite[],
|
|
|
|
|
desiredCaddyfile: string | null,
|
|
|
|
@@ -892,28 +892,48 @@ function statusScript(
|
|
|
|
|
desiredSourceCommit: string | null,
|
|
|
|
|
authorityId: string,
|
|
|
|
|
): string {
|
|
|
|
|
const probeItems = sites.map((site) => shQuote(`${site.hostname}|${site.healthPath}|${site.expectedA}`)).join(" ");
|
|
|
|
|
const probeCommands = sites.map((site, index) => {
|
|
|
|
|
const failureFile = `${String(index).padStart(6, "0")}.json`;
|
|
|
|
|
const dnsFailure = Buffer.from(JSON.stringify({
|
|
|
|
|
code: "public-edge-dns-mismatch",
|
|
|
|
|
siteId: site.id,
|
|
|
|
|
hostname: site.hostname,
|
|
|
|
|
expectedA: site.expectedA,
|
|
|
|
|
}), "utf8").toString("base64");
|
|
|
|
|
const siteFailure = Buffer.from(JSON.stringify({
|
|
|
|
|
code: "public-edge-site-probe-failed",
|
|
|
|
|
siteId: site.id,
|
|
|
|
|
hostname: site.hostname,
|
|
|
|
|
path: site.healthPath,
|
|
|
|
|
}), "utf8").toString("base64");
|
|
|
|
|
return `(
|
|
|
|
|
if ! getent ahostsv4 ${shQuote(site.hostname)} | awk '{print $1}' | grep -Fxq ${shQuote(site.expectedA)}; then
|
|
|
|
|
printf '%s' ${shQuote(dnsFailure)} | base64 -d >"$dns_failures_dir/${failureFile}"
|
|
|
|
|
fi
|
|
|
|
|
if ! curl --noproxy '*' -fsS --connect-timeout 3 --max-time 10 --resolve ${shQuote(`${site.hostname}:${target.listener.httpsPort}:127.0.0.1`)} ${shQuote(`https://${site.hostname}${site.healthPath}`)} >/dev/null; then
|
|
|
|
|
printf '%s' ${shQuote(siteFailure)} | base64 -d >"$site_failures_dir/${failureFile}"
|
|
|
|
|
fi
|
|
|
|
|
) &`;
|
|
|
|
|
}).join("\n");
|
|
|
|
|
const desiredFingerprint = desiredCaddyfile === null ? "" : sha256Fingerprint(desiredCaddyfile);
|
|
|
|
|
const desiredComposeFingerprint = desiredCompose === null ? "" : sha256Fingerprint(desiredCompose);
|
|
|
|
|
return `set -u
|
|
|
|
|
tmp="$(mktemp -d)"
|
|
|
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
|
dns_failures_dir="$tmp/dns-failures"
|
|
|
|
|
site_failures_dir="$tmp/site-failures"
|
|
|
|
|
mkdir -p "$dns_failures_dir" "$site_failures_dir"
|
|
|
|
|
container_rc=0; docker inspect -f '{{.State.Running}}' ${shQuote(target.runtime.containerName)} 2>/dev/null | grep -qx true || container_rc=1
|
|
|
|
|
legacy_rc=0; docker inspect -f '{{.State.Running}}' ${shQuote(target.runtime.legacy.containerName)} 2>/dev/null | grep -qx true || legacy_rc=1
|
|
|
|
|
listeners_rc=0; ss -ltn | grep -Eq '[:.]${target.listener.httpPort}[[:space:]]' || listeners_rc=1; ss -ltn | grep -Eq '[:.]${target.listener.httpsPort}[[:space:]]' || listeners_rc=1
|
|
|
|
|
udp_rc=0
|
|
|
|
|
${target.isolation.preservedUdpPorts.map((value) => `ss -lun | grep -Eq '[:.]${value}[[:space:]]' || udp_rc=1`).join("\n")}
|
|
|
|
|
validate_rc=0; docker exec ${shQuote(target.runtime.containerName)} caddy validate --config /etc/caddy/Caddyfile >/dev/null 2>/dev/null || validate_rc=1
|
|
|
|
|
sites_rc=0; dns_rc=0; probe_index=0
|
|
|
|
|
for item in ${probeItems}; do
|
|
|
|
|
probe_index=$((probe_index + 1))
|
|
|
|
|
(
|
|
|
|
|
host="${"${item%%|*}"}"; rest="${"${item#*|}"}"; path="${"${rest%%|*}"}"; path="/${"${path#/}"}"; expected="${"${rest#*|}"}"
|
|
|
|
|
getent ahostsv4 "$host" | awk '{print $1}' | grep -Fxq "$expected" || : >"/tmp/public-edge-dns-$$-$probe_index.failed"
|
|
|
|
|
curl --noproxy '*' -fsS --connect-timeout 3 --max-time 10 --resolve "$host:${target.listener.httpsPort}:127.0.0.1" "https://$host$path" >/dev/null || : >"/tmp/public-edge-site-$$-$probe_index.failed"
|
|
|
|
|
) &
|
|
|
|
|
done
|
|
|
|
|
sites_rc=0; dns_rc=0
|
|
|
|
|
${probeCommands}
|
|
|
|
|
wait || true
|
|
|
|
|
for failure in /tmp/public-edge-dns-$$-*.failed; do [ -f "$failure" ] || continue; dns_rc=1; rm -f "$failure"; done
|
|
|
|
|
for failure in /tmp/public-edge-site-$$-*.failed; do [ -f "$failure" ] || continue; sites_rc=1; rm -f "$failure"; done
|
|
|
|
|
for failure in "$dns_failures_dir"/*.json; do [ -f "$failure" ] || continue; dns_rc=1; done
|
|
|
|
|
for failure in "$site_failures_dir"/*.json; do [ -f "$failure" ] || continue; sites_rc=1; done
|
|
|
|
|
current_sha=""
|
|
|
|
|
if [ -f ${shQuote(target.runtime.caddyfilePath)} ]; then
|
|
|
|
|
current_sha="$(python3 - ${shQuote(target.runtime.caddyfilePath)} <<'PY'
|
|
|
|
@@ -927,10 +947,14 @@ PY
|
|
|
|
|
fi
|
|
|
|
|
current_compose_sha=""
|
|
|
|
|
if [ -f ${shQuote(target.runtime.composePath)} ]; then current_compose_sha="sha256:$(sha256sum ${shQuote(target.runtime.composePath)} | awk '{print $1}')"; fi
|
|
|
|
|
python3 - "$container_rc" "$legacy_rc" "$listeners_rc" "$udp_rc" "$validate_rc" "$dns_rc" "$sites_rc" "$current_sha" ${shQuote(desiredFingerprint)} "$current_compose_sha" ${shQuote(desiredComposeFingerprint)} ${shQuote(desiredSourceCommit ?? "")} ${shQuote(authorityId)} ${shQuote(target.runtime.statePath)} <<'PY'
|
|
|
|
|
python3 - "$container_rc" "$legacy_rc" "$listeners_rc" "$udp_rc" "$validate_rc" "$dns_rc" "$sites_rc" "$current_sha" ${shQuote(desiredFingerprint)} "$current_compose_sha" ${shQuote(desiredComposeFingerprint)} ${shQuote(desiredSourceCommit ?? "")} ${shQuote(authorityId)} ${shQuote(target.runtime.statePath)} "$dns_failures_dir" "$site_failures_dir" <<'PY'
|
|
|
|
|
import json, pathlib, sys
|
|
|
|
|
container, legacy, listeners, udp, validate, dns, sites = [int(value) for value in sys.argv[1:8]]
|
|
|
|
|
current, desired, current_compose, desired_compose, desired_commit, desired_authority, state_path = sys.argv[8:15]
|
|
|
|
|
current, desired, current_compose, desired_compose, desired_commit, desired_authority, state_path, dns_failures_path, site_failures_path = sys.argv[8:17]
|
|
|
|
|
def read_failures(directory):
|
|
|
|
|
return [json.loads(path.read_text(encoding="utf-8")) for path in sorted(pathlib.Path(directory).glob("*.json"))]
|
|
|
|
|
dns_failures = read_failures(dns_failures_path)
|
|
|
|
|
site_failures = read_failures(site_failures_path)
|
|
|
|
|
state = None
|
|
|
|
|
try:
|
|
|
|
|
state = json.loads(pathlib.Path(state_path).read_text(encoding="utf-8"))
|
|
|
|
@@ -949,7 +973,7 @@ state_matches_runtime = (
|
|
|
|
|
)
|
|
|
|
|
config_matches_desired = bool(desired) and current == desired and bool(desired_compose) and current_compose == desired_compose
|
|
|
|
|
ok = all(value == 0 for value in [container, listeners, udp, validate, dns, sites]) and legacy != 0 and state_matches_runtime and config_matches_desired
|
|
|
|
|
print(json.dumps({"ok": ok, "container": {"exitCode": container}, "legacyContainer": {"exitCode": legacy, "retired": legacy != 0}, "tcpListeners": {"exitCode": listeners}, "preservedUdpListeners": {"exitCode": udp}, "validate": {"exitCode": validate}, "dns": {"exitCode": dns}, "siteProbes": {"exitCode": sites}, "provenance": {"installed": state, "desiredSourceCommit": desired_commit or None, "matchesRuntime": state_matches_runtime}, "config": {"currentManagedFingerprint": current or None, "desiredManagedFingerprint": desired or None, "currentComposeFingerprint": current_compose or None, "desiredComposeFingerprint": desired_compose or None, "matchesDesired": config_matches_desired}, "valuesPrinted": False}))
|
|
|
|
|
print(json.dumps({"ok": ok, "container": {"exitCode": container}, "legacyContainer": {"exitCode": legacy, "retired": legacy != 0}, "tcpListeners": {"exitCode": listeners}, "preservedUdpListeners": {"exitCode": udp}, "validate": {"exitCode": validate}, "dns": {"exitCode": dns, "failures": dns_failures}, "siteProbes": {"exitCode": sites, "failures": site_failures}, "provenance": {"installed": state, "desiredSourceCommit": desired_commit or None, "matchesRuntime": state_matches_runtime}, "config": {"currentManagedFingerprint": current or None, "desiredManagedFingerprint": desired or None, "currentComposeFingerprint": current_compose or None, "desiredComposeFingerprint": desired_compose or None, "matchesDesired": config_matches_desired}, "valuesPrinted": False}))
|
|
|
|
|
raise SystemExit(0 if ok else 1)
|
|
|
|
|
PY
|
|
|
|
|
`;
|
|
|
|
|