Skip to content

Commit 7ba0b66

Browse files
committed
fix(edge): sync Caddy HLS ladder playback routes
1 parent a7d3366 commit 7ba0b66

4 files changed

Lines changed: 209 additions & 12 deletions

File tree

scripts/deploy-release-over-ssh.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ if [[ "$role" == "control-plane" ]]; then
172172
else
173173
echo "Deploying edge on $target"
174174
remote_sudo \
175-
"cd $remote_dir_q && scripts/preflight-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") --skip-bind-port-check && scripts/deploy-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") --dry-run && scripts/deploy-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") && curl -fsS http://127.0.0.1:4100/readyz && curl -fsS http://127.0.0.1:4100/healthz"
175+
"cd $remote_dir_q && scripts/sync-edge-caddy-playback-routes.sh && scripts/preflight-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") --skip-bind-port-check && scripts/deploy-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") --dry-run && scripts/deploy-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") && curl -fsS http://127.0.0.1:4100/readyz && curl -fsS http://127.0.0.1:4100/healthz"
176176
fi
177177

178178
echo "$role deploy completed on $target"

scripts/playback-readiness-gate.mjs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -607,35 +607,58 @@ function artifactPathFromUrl(urlValue, assetId) {
607607
return parts.slice(2).join("/");
608608
}
609609

610+
function isHlsRenditionManifestPath(artifactPath) {
611+
return /^hls\/(?:720p|1080p|2k|4k)\/index\.m3u8$/.test(artifactPath);
612+
}
613+
614+
function isHlsSegmentPath(artifactPath) {
615+
return /^hls\/(?:(?:720p|1080p|2k|4k)\/)?segment_[0-9]+\.ts$/.test(artifactPath);
616+
}
617+
610618
function extractBootstrapArtifacts(bootstrap, assetId) {
611619
const hints = Array.isArray(bootstrap.prefetch_hints) ? bootstrap.prefetch_hints : [];
612-
const firstSegment = hints.find((hint) => typeof hint.artifact_path === "string" && hint.artifact_path.endsWith(".ts"));
613-
if (!bootstrap.opener_url || !bootstrap.manifest_url || !firstSegment?.url) {
614-
throw new SafeError("playback bootstrap did not include opener, manifest, and first segment hints");
620+
const renditionManifest = hints.find((hint) =>
621+
typeof hint.artifact_path === "string" && isHlsRenditionManifestPath(hint.artifact_path) && hint.url
622+
);
623+
const firstSegment = hints.find((hint) =>
624+
typeof hint.artifact_path === "string" && isHlsSegmentPath(hint.artifact_path) && hint.url
625+
);
626+
if (!bootstrap.manifest_url || !renditionManifest?.url || !firstSegment?.url) {
627+
throw new SafeError("playback bootstrap did not include HLS master, startup playlist, and first segment hints");
615628
}
616629
const artifacts = [
617-
{
618-
label: "opener",
619-
artifact_path: artifactPathFromUrl(bootstrap.opener_url, assetId),
620-
content_type: bootstrap.opener_content_type || "video/mp4",
621-
},
622630
{
623631
label: "manifest",
624632
artifact_path: artifactPathFromUrl(bootstrap.manifest_url, assetId),
625633
content_type: bootstrap.manifest_content_type || "application/vnd.apple.mpegurl",
626634
},
635+
{
636+
label: "variant_manifest",
637+
artifact_path: artifactPathFromUrl(renditionManifest.url, assetId),
638+
content_type: renditionManifest.content_type || "application/vnd.apple.mpegurl",
639+
},
627640
{
628641
label: "segment",
629642
artifact_path: artifactPathFromUrl(firstSegment.url, assetId),
630643
content_type: firstSegment.content_type || "video/mp2t",
631644
},
632645
];
633-
if (artifacts[0].artifact_path !== "opener.mp4") {
634-
throw new SafeError("playback bootstrap opener did not use opener.mp4");
646+
if (bootstrap.opener_url) {
647+
artifacts.push({
648+
label: "opener",
649+
artifact_path: artifactPathFromUrl(bootstrap.opener_url, assetId),
650+
content_type: bootstrap.opener_content_type || "video/mp4",
651+
});
635652
}
636-
if (artifacts[1].artifact_path !== "hls/master.m3u8") {
653+
if (artifacts[0].artifact_path !== "hls/master.m3u8") {
637654
throw new SafeError("playback bootstrap manifest did not use hls/master.m3u8");
638655
}
656+
if (!isHlsRenditionManifestPath(artifacts[1].artifact_path)) {
657+
throw new SafeError("playback bootstrap startup playlist did not use a supported HLS rendition path");
658+
}
659+
if (!isHlsSegmentPath(artifacts[2].artifact_path)) {
660+
throw new SafeError("playback bootstrap first segment did not use a supported HLS segment path");
661+
}
639662
return artifacts;
640663
}
641664

@@ -1176,6 +1199,7 @@ async function runFixture(config, fixtureName, thresholds, warnings, failures, c
11761199
timings: {
11771200
opener: summarizeArtifactTimings(edgeResults, "opener"),
11781201
manifest: summarizeArtifactTimings(edgeResults, "manifest"),
1202+
variant_manifest: summarizeArtifactTimings(edgeResults, "variant_manifest"),
11791203
segment: summarizeArtifactTimings(edgeResults, "segment"),
11801204
},
11811205
cache_mix: aggregateCacheMix(edgeResults),
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
caddyfile="${REND_CADDYFILE:-/etc/caddy/Caddyfile}"
5+
dry_run=false
6+
skip_reload=false
7+
8+
usage() {
9+
cat <<'EOF'
10+
Usage: scripts/sync-edge-caddy-playback-routes.sh [options]
11+
12+
Patch the edge Caddy public playback matcher so HLS ladder playlists and
13+
segments reach rend-edge. The script edits only the signed playback regexp,
14+
backs up the Caddyfile, validates the result, and reloads Caddy. Hosts that
15+
do not keep a Caddy playback allowlist are left unchanged.
16+
17+
Options:
18+
--caddyfile FILE Caddyfile path. Default: /etc/caddy/Caddyfile.
19+
--dry-run Print whether the file would change without writing.
20+
--skip-reload Skip caddy fmt/validate and systemctl reload.
21+
-h, --help Show this help.
22+
EOF
23+
}
24+
25+
die() {
26+
echo "error: $*" >&2
27+
exit 1
28+
}
29+
30+
require_command() {
31+
command -v "$1" >/dev/null 2>&1 || die "$1 is required"
32+
}
33+
34+
while [[ $# -gt 0 ]]; do
35+
case "$1" in
36+
--caddyfile)
37+
caddyfile="${2:?missing value for $1}"
38+
shift 2
39+
;;
40+
--dry-run)
41+
dry_run=true
42+
shift
43+
;;
44+
--skip-reload)
45+
skip_reload=true
46+
shift
47+
;;
48+
-h | --help)
49+
usage
50+
exit 0
51+
;;
52+
*)
53+
die "unknown argument: $1"
54+
;;
55+
esac
56+
done
57+
58+
[[ -f "$caddyfile" ]] || die "missing Caddyfile: $caddyfile"
59+
require_command python3
60+
61+
tmp="$(mktemp "${TMPDIR:-/tmp}/rend-edge-caddy.XXXXXX")"
62+
cleanup() {
63+
rm -f "$tmp"
64+
}
65+
trap cleanup EXIT
66+
67+
patch_status="$(
68+
python3 - "$caddyfile" "$tmp" <<'PY'
69+
import sys
70+
from pathlib import Path
71+
72+
source = Path(sys.argv[1])
73+
target = Path(sys.argv[2])
74+
ladder = r"hls/(720p|1080p|2k|4k)/(index\.m3u8|segment_[0-9]+\.ts)"
75+
strict_pattern = (
76+
r"^/v/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-"
77+
r"[0-9a-f]{4}-[0-9a-f]{12}/"
78+
r"(opener\.mp4|hls/master\.m3u8|hls/segment_[0-9]+\.ts|"
79+
+ ladder
80+
+ r")$"
81+
)
82+
lines = source.read_text(encoding="utf-8").splitlines(keepends=True)
83+
found = False
84+
changed = False
85+
output = []
86+
87+
for line in lines:
88+
stripped = line.rstrip("\n")
89+
newline = "\n" if line.endswith("\n") else ""
90+
leading = stripped[: len(stripped) - len(stripped.lstrip())]
91+
tokens = stripped.strip().split()
92+
if (
93+
"path_regexp" in line
94+
and "^/v/" in line
95+
and "hls/master\\.m3u8" in line
96+
and "hls/segment_[0-9]+\\.ts" in line
97+
):
98+
found = True
99+
if ladder in stripped:
100+
output.append(line)
101+
continue
102+
if not stripped.endswith(")$"):
103+
raise SystemExit("signed playback regexp has an unsupported shape")
104+
output.append(f"{stripped[:-2]}|{ladder})${newline}")
105+
changed = True
106+
elif (
107+
len(tokens) >= 2
108+
and "/v/" in line
109+
and "hls/master.m3u8" in line
110+
and "hls/segment_" in line
111+
and ("path" in {tokens[0], tokens[1]})
112+
):
113+
found = True
114+
if tokens[0].startswith("@") and tokens[1] == "path":
115+
output.append(f"{leading}{tokens[0]} path_regexp canonical_playback {strict_pattern}{newline}")
116+
elif tokens[0] == "path":
117+
output.append(f"{leading}path_regexp canonical_playback {strict_pattern}{newline}")
118+
else:
119+
raise SystemExit("signed playback path matcher has an unsupported shape")
120+
changed = True
121+
else:
122+
output.append(line)
123+
124+
if not found:
125+
target.write_text("".join(output), encoding="utf-8")
126+
print("not_found")
127+
raise SystemExit(0)
128+
129+
target.write_text("".join(output), encoding="utf-8")
130+
print("changed" if changed else "unchanged")
131+
PY
132+
)"
133+
134+
case "$patch_status" in
135+
changed | unchanged | not_found) ;;
136+
*) die "unexpected patch status: $patch_status" ;;
137+
esac
138+
139+
if [[ "$patch_status" == "not_found" ]]; then
140+
echo "Edge Caddy playback routes do not use a deploy-managed allowlist; leaving $caddyfile unchanged"
141+
exit 0
142+
fi
143+
144+
if [[ "$patch_status" == "unchanged" ]]; then
145+
echo "Edge Caddy playback routes already include HLS ladder paths"
146+
exit 0
147+
fi
148+
149+
if [[ "$dry_run" == "true" ]]; then
150+
echo "Edge Caddy playback routes would be updated in $caddyfile"
151+
exit 0
152+
fi
153+
154+
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
155+
cp -p "$caddyfile" "${caddyfile}.bak.${stamp}"
156+
cat "$tmp" > "$caddyfile"
157+
echo "Updated edge Caddy playback routes in $caddyfile"
158+
159+
if [[ "$skip_reload" == "true" ]]; then
160+
echo "Skipped Caddy validate/reload"
161+
exit 0
162+
fi
163+
164+
require_command caddy
165+
require_command systemctl
166+
167+
caddy fmt --overwrite "$caddyfile"
168+
caddy validate --config "$caddyfile"
169+
systemctl reload caddy
170+
echo "Caddy reload completed"

scripts/validate-edge-deploy-templates.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ require_file scripts/deploy-edge-host.sh
186186
require_file scripts/quarantine-telemetry-spool-lines.sh
187187
require_file scripts/verify-first-host-deploy.sh
188188
require_file scripts/sync-edge-deploy-env-over-ssh.sh
189+
require_file scripts/sync-edge-caddy-playback-routes.sh
189190

190191
require_compose_service compose.yml rend-api
191192
require_compose_service compose.yml rend-media-worker
@@ -269,6 +270,8 @@ require_contains scripts/verify-first-host-deploy.sh "operator_psql_database_url
269270
require_contains scripts/verify-first-host-deploy.sh "/v/not-a-uuid/hls/master.m3u8"
270271
require_contains scripts/verify-first-host-deploy.sh "rend_edge_telemetry_spool_bytes"
271272
require_contains scripts/sync-edge-deploy-env-over-ssh.sh "REND_EDGE_CORS_ALLOWED_ORIGINS"
273+
require_contains scripts/sync-edge-caddy-playback-routes.sh "hls/(720p|1080p|2k|4k)/(index\\.m3u8|segment_[0-9]+\\.ts)"
274+
require_contains scripts/deploy-release-over-ssh.sh "scripts/sync-edge-caddy-playback-routes.sh"
272275
require_contains .github/workflows/release-deploy.yml "REND_EDGE_CORS_ALLOWED_ORIGINS"
273276
require_contains .github/workflows/release-deploy.yml "scripts/sync-edge-deploy-env-over-ssh.sh"
274277

0 commit comments

Comments
 (0)