feat(sglang-deploy): replace nginx with caddy

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-29 00:18:09 +02:00
parent bd291890db
commit 9455c65f10
4 changed files with 820 additions and 186 deletions
+188 -24
View File
@@ -16,7 +16,11 @@ my $LOG = "$WORK/log/stubs.log";
my $SCRIPT = $ENV{SGLANG_RIG_SCRIPT} // "$RIG/../../sglang-deploy.pl";
my $UNIT = '/etc/systemd/system/sglang.service';
my $ENVFILE = '/etc/sysconfig/sglang';
my $NGINX = '/etc/nginx/conf.d/sglang.conf';
my $CADDY = '/etc/caddy/Caddyfile.d/sglang.caddyfile';
my $CADDY_MAIN = '/etc/caddy/Caddyfile';
my $CADDY_UNIT = '/etc/systemd/system/caddy.service';
my $CADDY_BIN = '/usr/local/bin/caddy';
my $NGINX_LEGACY = '/etc/nginx/conf.d/sglang.conf';
my $CERTDIR = '/etc/ssl/sglang';
my $STATEDIR = '/opt/sglang';
@@ -105,22 +109,27 @@ sub mode_of {
}
sub reset_fixture {
for my $path ($UNIT, $ENVFILE, $NGINX) {
for my $path ($UNIT, $ENVFILE, $CADDY, $CADDY_MAIN, $CADDY_UNIT, $CADDY_BIN,
$NGINX_LEGACY) {
unlink($path);
}
remove_tree('/etc/caddy');
remove_tree($CERTDIR);
remove_tree($STATEDIR);
remove_tree($FIX);
remove_tree($ST);
# Directories a host that ran server-setup.pl has: nginx's configuration drop-in
# and systemd's unit directory.
# Directories a host that ran server-setup.pl has: the legacy nginx drop-in
# directory an earlier release wrote into, and systemd's unit directory.
make_dirs($FIX, $ST, "$WORK/log", '/etc/nginx/conf.d', '/etc/systemd/system');
my $fh;
open($fh, q{>}, $LOG) and close($fh);
# Packages a real host or a previous run has already installed.
# Packages a real host or a previous run has already installed. nginx stands
# for the drop-in the previous release left behind.
write_fixture('rpm-installed', "nginx\n");
write_fixture('fw-services', "\n");
# firewalld is running: server-setup.pl ensures it, and the firewall step needs it.
# The handle is declared first: a my inside the open's argument list does not
# reach the right-hand operand of the and on this interpreter.
my $fw;
open($fw, '>', "$ST/active.firewalld") and close($fw);
return;
@@ -184,8 +193,8 @@ sub reset_log {
# so the script's own PATH lookup finds them and nothing of the real system is used.
sub prepare_stubs {
make_dirs($BIN, $FIX, $ST, "$WORK/log");
for my $name (qw(lspci rpm dnf podman systemctl curl openssl nginx
firewall-cmd getenforce getsebool setsebool)) {
for my $name (qw(lspci rpm dnf podman systemctl curl openssl caddy tar useradd
semodule firewall-cmd getenforce getsebool setsebool)) {
my $link = "$BIN/$name";
# A link left over from a work directory that moved reads as broken to
# -e, and its stale target would leave the stubs unreachable: it is
@@ -197,6 +206,21 @@ sub prepare_stubs {
return;
}
# The caddy package creates its group; the rig creates it the same way, so the
# key permission the package makes possible is exercised for real. The group
# file is edited directly: the minimal openEuler image carries no groupadd to
# call, and a group entry is all the getgrnam in the script needs.
sub prepare_group {
return if defined getgrnam('caddy');
my $existing = slurp_file('/etc/group');
my $gid = 995;
$gid++ while $existing =~ /^[^:]+:[^:]*:\Q$gid\E:/m;
open(my $fh, '>>', '/etc/group') or die "cannot append to /etc/group: $!\n";
print {$fh} "caddy:x:$gid:\n";
close($fh);
return;
}
sub prepare_devices {
# The driver creates these on a real host; the rig fakes them (needs --privileged).
return if -e q{/dev/kfd};
@@ -214,12 +238,15 @@ sub scenario_fresh {
check($rc == 0, 'fresh: exit 0');
check(-f $UNIT, 'fresh: unit written');
check(-f $ENVFILE, 'fresh: environment file written');
check(-f $NGINX, 'fresh: nginx configuration written');
check(-f $CADDY, 'fresh: caddy drop-in written');
check(-f $CADDY_MAIN, 'fresh: main Caddyfile written');
check(-f "$CERTDIR/sglang.crt" && -f "$CERTDIR/sglang.key", 'fresh: certificate written');
check(-d "$STATEDIR/modelscope", 'fresh: model cache directory created');
check(mode_of($ENVFILE) eq '0600', 'fresh: environment file is 0600 (' . mode_of($ENVFILE) . ')');
check(mode_of("$CERTDIR/sglang.key") eq '0600', 'fresh: key is 0600');
check(mode_of("$CERTDIR/sglang.key") eq '0640', 'fresh: key is 0640 for the caddy group (' . mode_of("$CERTDIR/sglang.key") . ')');
check(mode_of("$CERTDIR/sglang.crt") eq '0644', 'fresh: certificate is 0644');
check((stat("$CERTDIR/sglang.key"))[5] == getgrnam('caddy'),
'fresh: the key belongs to the caddy group');
my $env = slurp_file($ENVFILE);
check_like($env, qr/^SGLANG_API_KEY=([A-Za-z0-9_-]{43})\n$/, 'fresh: generated key, url-safe, 43 chars');
@@ -231,19 +258,29 @@ sub scenario_fresh {
check_like($unit, qr/--mem-fraction-static 0\.9/, 'fresh: memory fraction default');
check_unlike($unit, qr/SGLANG_USE_AITER/, 'fresh: no Radeon variables on an Instinct host');
my $nginx = slurp_file($NGINX);
check_like($nginx, qr|proxy_pass http://\[::1\]:8000;|, 'fresh: nginx proxies to the loopback engine');
check_like($nginx, qr|ssl_certificate /etc/ssl/sglang/sglang\.crt;|, 'fresh: nginx uses the sglang certificate');
my $caddy = slurp_file($CADDY);
check_like($caddy, qr/reverse_proxy \[::1\]:8000 \{/, 'fresh: caddy proxies to the loopback engine');
check_like($caddy, qr|tls /etc/ssl/sglang/sglang\.crt /etc/ssl/sglang/sglang\.key|,
'fresh: caddy uses the sglang certificate pair');
check_like($caddy, qr/flush_interval -1/, 'fresh: streaming is unbuffered');
my $main = slurp_file($CADDY_MAIN);
check_like($main, qr/^import Caddyfile\.d\/\*\.caddyfile$/m, 'fresh: the main Caddyfile imports the drop-ins');
check(count_in_log(qr/^podman pull /) == 1, 'fresh: exactly one image pull');
check_like(stub_log(), qr/^podman pull docker\.io\/lmsysorg\/sglang:v0\.5\.19-rocm724-mi30x$/m,
'fresh: the pulled image is the resolved tag');
check_like(stub_log(), qr/^caddy version$/m, 'fresh: caddy is reported from the binary');
check_like(stub_log(), qr/^caddy validate --config \/etc\/caddy\/Caddyfile$/m,
'fresh: the configuration is validated before the reload');
check_like(stub_log(), qr/^systemctl enable --now caddy$/m, 'fresh: caddy enabled and started');
check_like(stub_log(), qr/^systemctl reload caddy$/m, 'fresh: caddy reloaded');
check(count_in_log(qr/^systemctl enable sglang$/) == 1, 'fresh: service enabled');
check(count_in_log(qr/^systemctl start sglang$/) == 1, 'fresh: service started');
check_like(stub_log(), qr/^firewall-cmd --permanent --add-service=https$/m, 'fresh: HTTPS opened');
check_like($out, qr/Engine image: docker\.io\/lmsysorg\/sglang:v0\.5\.19-rocm724-mi30x/,
'fresh: summary names the image');
check_like($out, qr/systemd: sglang running on \[::1\]:8000/, 'fresh: summary names the endpoint');
check_like($out, qr/Caddy: configured and reloaded/, 'fresh: summary reports caddy');
check_like($out, qr/API key \(shown once, store it securely\)/, 'fresh: the generated key is shown once');
check_unlike($out, qr/✗/, 'fresh: no failed step');
return;
@@ -261,6 +298,7 @@ sub scenario_rerun {
check_like(stub_log(), qr/^podman image exists /m, 'rerun: the image is checked instead');
check(count_in_log(qr/^systemctl enable sglang$/) == 0, 'rerun: no second enable');
check(count_in_log(qr/^systemctl start sglang$/) == 0, 'rerun: no second start');
check(count_in_log(qr/^systemctl enable --now caddy$/) == 0, 'rerun: no second caddy enable');
check(count_in_log(qr/try-restart/) == 0, 'rerun: no restart, the unit did not change');
check(count_in_log(qr/^dnf install /) == 0, 'rerun: no second package transaction');
check(slurp_file($ENVFILE) eq $key_before, 'rerun: the API key is reused, not regenerated');
@@ -282,7 +320,8 @@ sub scenario_dry_run {
check($rc == 0, 'dry run: exit 0');
check(!-f $UNIT, 'dry run: no unit written');
check(!-f $ENVFILE, 'dry run: no environment file written');
check(!-f $NGINX, 'dry run: no nginx configuration written');
check(!-f $CADDY, 'dry run: no caddy drop-in written');
check(!-e '/etc/caddy', 'dry run: no caddy directory created');
check(!-d $CERTDIR, 'dry run: no certificate directory');
check(count_in_log(qr/^podman pull /) == 0, 'dry run: no pull');
check(count_in_log(qr/^systemctl (enable|start) /) == 0, 'dry run: no service change');
@@ -302,14 +341,17 @@ sub scenario_uninstall {
check($rc == 0, 'uninstall: exit 0');
check(!-f $UNIT, 'uninstall: unit removed');
check(!-f $ENVFILE, 'uninstall: environment file removed');
check(!-f $NGINX, 'uninstall: nginx configuration removed');
check(!-f $CADDY, 'uninstall: caddy drop-in removed');
check(-f $CADDY_MAIN, 'uninstall: the main Caddyfile is kept');
check(!-d $CERTDIR, 'uninstall: certificate directory removed');
check(-d $STATEDIR, 'uninstall: model cache kept');
check(-e "$ST/image.docker.io_lmsysorg_sglang_v0.5.19-rocm724-mi30x",
'uninstall: the image is kept in podman');
check_like(stub_log(), qr/^systemctl stop sglang$/m, 'uninstall: service stopped');
check_like(stub_log(), qr/^systemctl disable sglang$/m, 'uninstall: service disabled');
check_like(stub_log(), qr/^systemctl reload caddy$/m, 'uninstall: caddy reloaded after the drop-in');
check_like($out, qr/SGLang service stopped/, 'uninstall: summary reports the stop');
check_like($out, qr/caddy drop-in removed/, 'uninstall: summary reports the drop-in');
check_like($out, qr/Kept on the system/, 'uninstall: the kept state is listed');
check_like($out, qr/ModelScope cache/, 'uninstall: the cache is named as kept');
return;
@@ -322,6 +364,78 @@ sub scenario_uninstall_twice {
check($rc == 0, 'uninstall twice: exit 0');
check_like($out, qr/already absent/, 'uninstall twice: idempotent');
check(count_in_log(qr/^systemctl stop /) == 0, 'uninstall twice: nothing to stop');
check(count_in_log(qr/^systemctl reload caddy$/) == 0,
'uninstall twice: no reload without a drop-in');
return;
}
# A host deployed by the nginx release carries its drop-in. Both a deploy and
# an uninstall remove it, so exactly one proxy owns :443 afterwards.
sub scenario_legacy_nginx {
reset_fixture();
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
write_fixture('rocm.txt', "7.2.4\n");
open(my $fh, '>', $NGINX_LEGACY) or die "cannot write $NGINX_LEGACY: $!\n";
print {$fh} "server {\n listen 443 ssl;\n}\n";
close($fh);
# The stub state: nginx is running on this host, so the removal reloads it.
my $st;
open($st, '>', "$ST/active.nginx") and close($st);
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
check($rc == 0, 'legacy nginx: exit 0');
check(!-f $NGINX_LEGACY, 'legacy nginx: the old drop-in is gone on deploy');
check_like(stub_log(), qr/^systemctl reload nginx$/m,
'legacy nginx: the running nginx is reloaded');
check_like($out, qr/Legacy nginx configuration: removed/, 'legacy nginx: the summary names it');
# An uninstall on a host the new release never deployed cleans it too.
reset_fixture();
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
write_fixture('rocm.txt', "7.2.4\n");
open($fh, '>', $NGINX_LEGACY) or die "cannot write $NGINX_LEGACY: $!\n";
print {$fh} "server {\n listen 443 ssl;\n}\n";
close($fh);
reset_log();
($rc, $out) = run_script('--uninstall');
check($rc == 0, 'legacy nginx: uninstall exit 0');
check(!-f $NGINX_LEGACY, 'legacy nginx: the old drop-in is gone on uninstall');
check_like($out, qr/legacy nginx configuration removed/, 'legacy nginx: the uninstall summary names it');
return;
}
# Where no repository carries the caddy package, the official release binary
# takes its place: download, extract, install, the service user, and the unit
# file the package would have carried.
sub scenario_caddy_binary {
reset_fixture();
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
write_fixture('rocm.txt', "7.2.4\n");
write_fixture('caddy-no-package', "1\n");
unlink('/usr/bin/caddy'); # order independence: no package binary, no stub
unlink("$BIN/caddy") or die "cannot remove the caddy stub: $!\n";
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
check($rc == 0, 'caddy binary: exit 0');
check_like(stub_log(), qr/^dnf install -y caddy$/m, 'caddy binary: the package install was attempted');
check_like(stub_log(), qr{^curl -fsSL -o \S+ https://github\.com/caddyserver/caddy/releases/download/v2\.10\.2/caddy_2\.10\.2_linux_amd64\.tar\.gz$}m,
'caddy binary: the release asset is downloaded');
check_like(stub_log(), qr/^tar -xzf \S+ -C \S+$/m, 'caddy binary: the archive is extracted');
check(-x $CADDY_BIN, 'caddy binary: the binary is installed executable');
check_like(stub_log(), qr/^useradd --system --home-dir \/var\/lib\/caddy --create-home --shell \/sbin\/nologin caddy$/m,
'caddy binary: the service user is created');
check(-f $CADDY_UNIT, 'caddy binary: the unit file is written');
my $unit = slurp_file($CADDY_UNIT);
check_like($unit, qr|ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile|,
'caddy binary: the unit runs the release binary');
check_like(stub_log(), qr/^systemctl daemon-reload$/m, 'caddy binary: systemd reloaded');
check_like(stub_log(), qr{^caddy validate --config /etc/caddy/Caddyfile$}m,
'caddy binary: the installed binary validates');
check_like($out, qr/Caddy: configured and reloaded/, 'caddy binary: the deployment completes');
unlink($CADDY_BIN);
unlink($CADDY_UNIT);
unlink("$FIX/caddy-no-package");
symlink("$RIG/stub.pl", "$BIN/caddy") or die "cannot restore the caddy stub: $!\n";
return;
}
@@ -495,7 +609,7 @@ sub scenario_validation {
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--port', '443', '--dry-run');
check($rc == 1, 'validation: port 443 refused');
check_like($out, qr/must be 1-65535 and not 443/, 'validation: port message');
check_like($out, qr/must be an integer 1-65535 and not 443/, 'validation: port message');
($rc, $out) = run_script('--model', 'not-a-model-id', '--dry-run');
check($rc == 1, 'validation: bad model ID refused');
check_like($out, qr/Invalid model ID/, 'validation: model message');
@@ -545,7 +659,7 @@ sub scenario_non_root {
my $rc = $? >> 8;
my $out = slurp_file($out_file);
check($rc == 0, 'non-root: --version works without root');
check_like($out, qr/^sglang-deploy\.pl 2\.0\.0$/, 'non-root: the version is printed');
check_like($out, qr/^sglang-deploy\.pl 2\.1\.0$/, 'non-root: the version is printed');
my $pid3 = fork();
die "cannot fork: $!\n" unless defined $pid3;
@@ -583,12 +697,12 @@ sub scenario_dependency_section {
reset_fixture();
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
write_fixture('rocm.txt', "7.2.4\n");
write_fixture('rpm-installed', "pciutils\ncurl\nopenssl\npodman\nnginx\n");
write_fixture('rpm-installed', "pciutils\ncurl\nopenssl\npodman\ntar\n");
reset_log();
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--dry-run');
check($rc == 0, 'deps: exit 0');
check(count_in_log(qr/^dnf install /) == 0, 'deps: no transaction when all packages are present');
check_like($out, qr/All 4 packages already installed/, 'deps: reported as present');
check_like($out, qr/All 5 packages already installed/, 'deps: reported as present');
reset_fixture();
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
@@ -596,10 +710,41 @@ sub scenario_dependency_section {
write_fixture('rpm-installed', "\n");
reset_log();
($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
check_like(stub_log(), qr/^dnf install -y pciutils curl openssl podman$/m,
'deps: the four packages are installed in one transaction');
check_like($out, qr/nginx \(reverse proxy\) is not installed: run server-setup.pl first/,
'deps: the missing reverse proxy is warned about');
check_like(stub_log(), qr/^dnf install -y pciutils curl openssl podman tar$/m,
'deps: the five packages are installed in one transaction');
check_like(stub_log(), qr/^caddy version$/m, 'deps: caddy is checked after the transaction');
check_unlike($out, qr/run server-setup\.pl first/,
'deps: no warning points at server-setup.pl, caddy is installed here');
return;
}
# CentOS Stream carries caddy in EPEL, and the repository file installs first,
# which is what makes the package transaction below it resolvable.
sub scenario_epel {
reset_fixture();
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
write_fixture('rocm.txt', "7.2.4\n");
my $real = slurp_file('/etc/os-release');
open(my $fh, '>', '/etc/os-release') or die "cannot write /etc/os-release: $!\n";
print {$fh} "ID=centos\nVERSION_ID=\"10\"\n";
close($fh);
unlink('/usr/bin/caddy'); # order independence: no binary, no stub
unlink("$BIN/caddy");
reset_log();
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
open(my $back, '>', '/etc/os-release') or die "cannot restore /etc/os-release: $!\n";
print {$back} $real;
close($back);
check($rc == 0, 'epel: exit 0');
check_like(stub_log(), qr/^dnf install -y epel-release$/m, 'epel: the repository file installs first');
check_like(stub_log(), qr/^dnf install -y caddy$/m, 'epel: the package installs after it');
check_like($out, qr/Installing caddy\.\.\. package/, 'epel: the package path is taken');
check_like($out, qr/Caddy: configured and reloaded/, 'epel: the deployment completes');
unlink('/usr/bin/caddy');
unlink("$FIX/caddy-no-package");
symlink("$RIG/stub.pl", "$BIN/caddy") or die "cannot restore the caddy stub: $!\n";
return;
}
@@ -619,7 +764,7 @@ sub scenario_custom_layout {
);
check($rc == 0, 'custom layout: exit 0');
check(-f '/etc/systemd/system/llm.service', 'custom layout: the named unit is written');
check(-f '/etc/nginx/conf.d/llm.conf', 'custom layout: the named nginx file is written');
check(-f '/etc/caddy/Caddyfile.d/llm.caddyfile', 'custom layout: the named caddy drop-in is written');
# The certificate file names follow the program, as they did before, not the
# service name; the directory follows --cert-dir.
check(-f '/etc/ssl/llm/sglang.crt', 'custom layout: certificates follow the directory');
@@ -632,7 +777,7 @@ sub scenario_custom_layout {
check_like($unit, qr|--volume /srv/llm/modelscope:/root/\.cache/modelscope:Z|,
'custom layout: the cache volume follows the state directory');
unlink('/etc/systemd/system/llm.service');
unlink('/etc/nginx/conf.d/llm.conf');
unlink('/etc/caddy/Caddyfile.d/llm.caddyfile');
remove_tree('/etc/ssl/llm');
remove_tree('/srv/llm');
return;
@@ -674,17 +819,32 @@ sub scenario_selinux {
check(count_in_log(qr/^setsebool /) == 0, 'selinux: nothing set while permissive');
check_like($out, qr/SELinux: permissive \(skipped\)/, 'selinux: reported as skipped');
# Enforcing with no confined caddy policy: the distributions run caddy
# unconfined, so no boolean is touched.
reset_fixture();
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
write_fixture('rocm.txt', "7.2.4\n");
$ENV{STUB_SELINUX} = 'Enforcing';
reset_log();
($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
check(count_in_log(qr/^setsebool /) == 0, 'selinux: no boolean without a confined policy');
check_like($out, qr/SELinux: caddy runs unconfined \(nothing to do\)/,
'selinux: the unconfined case is stated');
# Enforcing with a confined caddy policy loaded: the boolean is set.
reset_fixture();
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
write_fixture('rocm.txt', "7.2.4\n");
write_fixture('semodule-caddy', "1\n");
reset_log();
($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
check_like(stub_log(), qr/^semodule -l$/m, 'selinux: the loaded modules are asked for');
check_like(stub_log(), qr/^setsebool -P httpd_can_network_connect=1$/m, 'selinux: the boolean is set');
check_like($out, qr/SELinux: httpd_can_network_connect on/, 'selinux: reported as on');
reset_log();
my ($rc2, $out2) = run_script('--model', 'ZhipuAI/GLM-5.3');
check(count_in_log(qr/^setsebool /) == 0, 'selinux: nothing set when already on');
unlink("$FIX/semodule-caddy");
delete $ENV{STUB_SELINUX};
return;
}
@@ -695,6 +855,9 @@ my %scenarios = (
dry_run => \&scenario_dry_run,
uninstall => \&scenario_uninstall,
uninstall_twice => \&scenario_uninstall_twice,
legacy_nginx => \&scenario_legacy_nginx,
caddy_binary => \&scenario_caddy_binary,
epel => \&scenario_epel,
radeon => \&scenario_radeon,
radeon_dev => \&scenario_radeon_dev,
radeon_with_image => \&scenario_radeon_with_image,
@@ -715,6 +878,7 @@ my %scenarios = (
);
prepare_stubs();
prepare_group();
prepare_devices();
my @wanted = @ARGV ? @ARGV : sort keys %scenarios;
Regular → Executable
+71 -5
View File
@@ -74,6 +74,12 @@ if ($name eq 'rpm') {
if ($name eq 'dnf') {
my @pkgs = grep { !/^-/ && $_ ne 'install' } @args;
# Parenthesised on purpose: a named list operator swallows a trailing &&,
# and the unparenthesised form asks the grep about a boolean, not the list.
if ((grep { $_ eq 'caddy' } @pkgs) && -f "$FIX/caddy-no-package") {
print STDERR "Error: Unable to find a match: caddy\n";
exit 1;
}
my $installed = fixture_text('rpm-installed', '');
for my $pkg (@pkgs) {
$installed .= "$pkg\n" unless $installed =~ /^\Q$pkg\E$/m;
@@ -82,6 +88,12 @@ if ($name eq 'dnf') {
print {$fh} $installed;
close($fh);
}
# A package transaction that installs caddy leaves the binary where PATH
# finds it, the way the real package does. The grep is parenthesised as
# above: a named list operator swallows a trailing &&.
if ((grep { $_ eq 'caddy' } @pkgs) && !-e '/usr/bin/caddy') {
symlink($0, '/usr/bin/caddy');
}
print "Installing: @pkgs\n";
exit 0;
}
@@ -138,14 +150,32 @@ if ($name eq 'curl') {
print "\n__HTTP__$code\n" if $joined =~ /__HTTP__/;
exit 0;
}
if ($url =~ m{api\.github\.com/repos/caddyserver/caddy}) {
print fixture_text('caddy-release.json', qq({"tag_name":"v2.10.2"}\n));
exit 0;
}
if ($url =~ m{api\.github\.com}) {
print fixture_text('releases.json', qq({"tag_name":"v0.5.19"}\n));
exit 0;
}
if ($url =~ m{github\.com/caddyserver/caddy/releases/download/}) {
my $dest;
for my $i (0 .. $#args) {
$dest = $args[$i + 1] if $args[$i] eq '-o';
}
if (defined $dest) {
open(my $fh, '>', $dest) or exit 1;
print {$fh} "stub caddy release archive\n";
close($fh);
}
exit 0;
}
if ($url =~ m{hub\.docker\.com}) {
if (-f "$FIX/image-missing") {
print STDERR "curl: (22) The requested URL returned error: 404\n";
exit 22;
# A definitive 404 on a tag lookup is what the script reads as
# unpublished; a curl-level failure would read as cannot-tell.
if ($url =~ m{/tags/[A-Za-z0-9._-]+$} && -f "$FIX/image-missing") {
print "404";
exit 0;
}
if ($url =~ /tags\?/) {
# A tag listing, newest first: the rig's AMD development build.
@@ -181,8 +211,44 @@ if ($name eq 'openssl') {
exit 0;
}
if ($name eq 'nginx') {
print "nginx: configuration file /etc/nginx/nginx.conf test is successful\n";
if ($name eq 'caddy') {
my $joined = join(' ', @args);
if ($joined =~ /version/) {
print "v2.10.2 h1:stub\n";
exit 0;
}
if ($joined =~ /validate/) {
print "Valid configuration\n";
exit 0;
}
exit 0;
}
if ($name eq 'tar') {
# The release-binary install extracts the archive and installs the binary it
# names; the stub lays down a link to this dispatcher, so the installed
# stand-in answers and logs like every other stubbed command.
my $dest_dir;
for my $i (0 .. $#args) {
$dest_dir = $args[$i + 1] if $args[$i] eq '-C';
}
if (defined $dest_dir) {
unlink("$dest_dir/caddy");
symlink($0, "$dest_dir/caddy");
}
exit 0;
}
if ($name eq 'useradd') {
exit 0;
}
if ($name eq 'semodule') {
# A loaded module line looks like "100 caddy\tpp"; the fixture decides
# whether this host carries a confined caddy policy.
if (-f "$FIX/semodule-caddy") {
print "100 caddy\tpp\n";
}
exit 0;
}
+37 -16
View File
@@ -138,7 +138,7 @@ is((valid_dir_path('/opt/sg lang') ? 1 : 0), 0, 'valid_dir_path: whitespace is r
is((valid_dir_path('/opt/%h/sglang') ? 1 : 0), 0,
'valid_dir_path: a systemd specifier is refused');
is((valid_dir_path('/opt/x;/sglang') ? 1 : 0), 0,
'valid_dir_path: an nginx directive end is refused');
'valid_dir_path: a path with a semicolon is refused');
# ---- run(): exit status, signals and timeout ------------------------------
my $killed = run([$^X, '-e', 'kill 9, $$']);
@@ -164,22 +164,43 @@ is(scalar @{ radeon_env_for(undef, 1) }, 2,
'env: a custom image on a Radeon-only host carries the Radeon defaults');
is(scalar @{ radeon_env_for('mi30x', 0) }, 0, 'env: an Instinct host carries none');
# ---- nginx config --------------------------------------------------------
my $conf = nginx_conf_content(8000, '[::1]', '/etc/ssl/sglang');
like($conf, qr/listen \[::\]:443 ssl;/, 'nginx: IPv6 listener on a dual-stack kernel');
like($conf, qr/listen 443 ssl;/, 'nginx: IPv4 listener');
like($conf, qr|ssl_certificate /etc/ssl/sglang/sglang\.crt;|, 'nginx: certificate path');
like($conf, qr/ssl_certificate_key \/etc\/ssl\/sglang\/sglang\.key;/, 'nginx: key path');
like($conf, qr|proxy_pass http://\[::1\]:8000;|, 'nginx: loopback upstream with the port');
like($conf, qr/proxy_http_version 1\.1;/, 'nginx: HTTP/1.1 for streaming');
like($conf, qr/proxy_buffering off;/, 'nginx: buffering off for streaming');
like($conf, qr/proxy_set_header Host \$host;/, 'nginx: $host survives the heredoc');
like($conf, qr/proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;/,
'nginx: $proxy_add_x_forwarded_for survives the heredoc');
# ---- caddy drop-in --------------------------------------------------------
my $conf = caddyfile_content(8000, '[::1]', '/etc/ssl/sglang');
like($conf, qr/^:443 \{$/m, 'caddy: the endpoint site on 443');
like($conf, qr|tls /etc/ssl/sglang/sglang\.crt /etc/ssl/sglang/sglang\.key|,
'caddy: certificate pair paths');
like($conf, qr/reverse_proxy \[::1\]:8000 \{/, 'caddy: loopback upstream with the port');
like($conf, qr/flush_interval -1/, 'caddy: unbuffered streaming');
like($conf, qr/header_up X-Real-IP \{remote_host\}/, 'caddy: X-Real-IP survives the heredoc');
like($conf, qr/max_size 50MB/, 'caddy: the request body limit');
like($conf, qr/\treverse_proxy /, 'caddy: tab-indented as the Caddyfile is formatted');
check_unlike($conf, qr/\bbind\b/, 'caddy: no bind directive, the kernel decides the families');
check_unlike($conf, qr/acme|on_demand|http:\/\/\{/i, 'caddy: no ACME, the self-signed pair is used');
my $conf4 = nginx_conf_content(8000, '127.0.0.1', '/etc/ssl/sglang');
is($conf4 =~ /\[::\]/ ? 'yes' : 'no',
(-e '/proc/net/if_inet6' ? 'yes' : 'no'), 'nginx: IPv6 listener follows the kernel');
my $conf4 = caddyfile_content(9000, '127.0.0.1', '/etc/ssl/llm');
like($conf4, qr/reverse_proxy 127\.0\.0\.1:9000 \{/, 'caddy: an IPv4 upstream carries the port');
like($conf4, qr|tls /etc/ssl/llm/sglang\.crt|, 'caddy: the certificate directory follows --cert-dir');
is(caddyfile_path('sglang'), '/etc/caddy/Caddyfile.d/sglang.caddyfile',
'caddy: the drop-in path follows the service name');
is(caddy_main_config(), '/etc/caddy/Caddyfile', 'caddy: the main Caddyfile path');
# ---- caddy release binary -------------------------------------------------
is(uname_to_arch('x86_64'), 'amd64', 'caddy binary: x86_64 maps to amd64');
is(uname_to_arch('aarch64'), 'arm64', 'caddy binary: aarch64 maps to arm64');
is(uname_to_arch('ppc64le'), undef, 'caddy binary: an unmapped machine is refused');
is(caddy_asset_url('2.10.2', 'amd64'),
'https://github.com/caddyserver/caddy/releases/download/v2.10.2/caddy_2.10.2_linux_amd64.tar.gz',
'caddy binary: the release asset URL');
my $caddy_unit = caddy_unit_content();
like($caddy_unit, qr|ExecStartPre=/usr/local/bin/caddy validate --config /etc/caddy/Caddyfile|,
'caddy binary: the unit validates before it starts');
like($caddy_unit, qr|ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile|,
'caddy binary: the unit runs the release binary');
like($caddy_unit, qr|ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile|,
'caddy binary: the unit reloads through the admin endpoint');
like($caddy_unit, qr/^User=caddy$/m, 'caddy binary: the unit runs as the caddy user');
like($caddy_unit, qr/AmbientCapabilities=CAP_NET_BIND_SERVICE/, 'caddy binary: the port capability');
# ---- systemd unit --------------------------------------------------------
my $unit = systemd_content({