feat: node updates by restart, auto by default; Podman Quadlet install from the panel alongside compose [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

This commit is contained in:
justme committed 2026-10-09 01:33:22 +08:00
1 parent 88b1a0a9bb
commit 535bb3517d
32 files changed
+751 -152

No files matched your search

@@ -94,6 +94,53 @@ public sealed class InstallTests
}
}
[Theory]
[InlineData("agent")]
[InlineData("probe")]
public async Task QuadletInstall_ChecksTheUnitHash_PointsTheImageAtTheConfiguredRepository_AndLeavesAutoUpdateOn(string role)
{
JsonNode created = await CreateAsync(role);
JsonNode quadlet = created["quadlet"]!;
string env = quadlet["env"]!.GetValue<string>();
string command = quadlet["command"]!.GetValue<string>();
string url = quadlet["url"]!.GetValue<string>();
Assert.EndsWith($"/install/quadlet/{role}.container", url, StringComparison.Ordinal);
Assert.Contains($"MOSHIRO_JOIN={created["plaintext"]!.GetValue<string>()}", env, StringComparison.Ordinal);
Assert.Contains("# MOSHIRO_UPDATE_POLICY=manual", env, StringComparison.Ordinal); // 默认自动更新,env 里只留一行注释说明怎么改成手动
Assert.DoesNotContain("\nMOSHIRO_UPDATE_POLICY=", env, StringComparison.Ordinal);
Assert.Contains($"~/.config/moshiro/{role}.env", command, StringComparison.Ordinal);
Assert.Contains("chmod 600", command, StringComparison.Ordinal);
Assert.Contains($"echo \"{quadlet["sha256"]!.GetValue<string>()} $HOME/.config/containers/systemd/moshiro-{role}.container\" | sha256sum -c -", command, StringComparison.Ordinal);
Assert.Contains($"Image=", command, StringComparison.Ordinal);
Assert.Contains($"/moshiro-{role}:latest", command, StringComparison.Ordinal);
Assert.EndsWith($"systemctl --user daemon-reload && systemctl --user start moshiro-{role}", command, StringComparison.Ordinal);
// 面板提供的就是命令里核对的那份:Pull=always(退出再被拉起就是更新),并告诉节点外面会重新拉取
using HttpResponseMessage served = await _backend.Anonymous.GetAsync($"/install/quadlet/{role}.container");
Assert.Equal(HttpStatusCode.OK, served.StatusCode);
byte[] body = await served.Content.ReadAsByteArrayAsync();
Assert.Equal(quadlet["sha256"]!.GetValue<string>(), Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(body)));
string unit = System.Text.Encoding.UTF8.GetString(body);
Assert.Contains("\nPull=always", unit, StringComparison.Ordinal);
Assert.Contains("Environment=MOSHIRO_UPDATE=restart", unit, StringComparison.Ordinal);
Assert.Contains("ReadOnly=true", unit, StringComparison.Ordinal);
Assert.Contains("DropCapability=ALL", unit, StringComparison.Ordinal);
Assert.Contains("NoNewPrivileges=true", unit, StringComparison.Ordinal);
Assert.DoesNotContain("jt_", unit, StringComparison.Ordinal);
}
[Theory]
[InlineData("evil.container")]
[InlineData("backend.container")]
[InlineData("agent")]
public async Task OnlyTheTwoQuadletUnits_AreServed(string file)
{
using HttpResponseMessage response = await _backend.Anonymous.GetAsync("/install/quadlet/" + file);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData("agent.yaml")]
[InlineData("probe.yaml")]
@@ -53,7 +53,7 @@ public sealed partial class OpenApiConformanceTests
foreach ((string path, _) in doc["paths"]!.AsObject())
{
string[] prefixes = ["/api/public/", "/api/admin/", "/api/agent/v1/"];
string[] roots = ["/healthz", "/metrics", "/install", "/install.ps1", "/install/manifest", "/install/manifest.sig", "/install/compose/{file}", "/api/release/offer"];
string[] roots = ["/healthz", "/metrics", "/install", "/install.ps1", "/install/manifest", "/install/manifest.sig", "/install/compose/{file}", "/install/quadlet/{file}", "/api/release/offer"];
Assert.True(prefixes.Any(p => path.StartsWith(p, StringComparison.Ordinal)) || roots.Contains(path), $"{path} is outside the documented layers");
foreach (string segment in path.Split('/', StringSplitOptions.RemoveEmptyEntries))
{