feat: use Alpine AOT images, static SQLite and Podman host metrics [CI]
This commit is contained in:
1 parent
6e945657c1
commit
103ef27ed3
37 files changed
+719
-309
No files matched your search
@@ -35,11 +35,20 @@ public sealed class AotSmokeTests
|
||||
string token = JsonNode.Parse(await tokenResponse.Content.ReadAsStringAsync())!["plaintext"]!.GetValue<string>();
|
||||
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
using HttpResponseMessage settingsResponse = await http.PatchAsync("/api/admin/settings", BackendFixture.Json(new JsonObject { ["site.public"] = true }, "application/merge-patch+json"));
|
||||
settingsResponse.EnsureSuccessStatusCode();
|
||||
using HttpResponseMessage probeResponse = await http.PostAsync("/api/admin/probes", BackendFixture.Json(new JsonObject { ["name"] = "aot-probe-" + Guid.NewGuid().ToString("N")[..6] }));
|
||||
probeResponse.EnsureSuccessStatusCode();
|
||||
string probeId = JsonNode.Parse(await probeResponse.Content.ReadAsStringAsync())!["probe_id"]!.GetValue<string>();
|
||||
|
||||
// Agent:加入令牌 → 注册(ECDSA 持有证明)→ WebSocket 签名认证 → 期望状态
|
||||
using HttpResponseMessage joinResponse = await http.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["max_uses"] = 1 }));
|
||||
joinResponse.EnsureSuccessStatusCode();
|
||||
string joinToken = JsonNode.Parse(await joinResponse.Content.ReadAsStringAsync())!["plaintext"]!.GetValue<string>();
|
||||
using var anonymous = new HttpClient { BaseAddress = baseAddress };
|
||||
await using AgentSimulator agent = await AgentSimulator.EnrollAsync(anonymous, joinToken, "aot-" + Guid.NewGuid().ToString("N")[..6]);
|
||||
agent.ProbeForChunks = new ProbeId(probeId);
|
||||
string lineId = $"{agent.NodeId.Value}~{probeId}";
|
||||
await agent.ConnectAsync(baseAddress);
|
||||
TaskSet tasks = await agent.WaitForTaskSetAsync(s => s.Tasks.Count >= 2);
|
||||
Assert.Contains(tasks.Tasks, t => t.Kind == "udp_echo");
|
||||
@@ -54,7 +63,7 @@ public sealed class AotSmokeTests
|
||||
|
||||
// 查询:热层的精确汇总,随后是持久层的分钟窗口
|
||||
string range = $"from={Iso(baseMs)}&to={Iso(baseMs + minute)}&metrics=loss.total,rtt.p95";
|
||||
JsonNode hot = await GetAsync(http, $"/api/public/lines/{agent.NodeId.Value}/series?{range}&step=1s");
|
||||
JsonNode hot = await GetAsync(http, $"/api/public/lines/{lineId}/series?{range}&step=1s");
|
||||
Assert.Equal("hot", hot["source"]!.GetValue<string>());
|
||||
Assert.Equal(600, hot["summary"]!["samples"]!["udp_echo"]!.GetValue<long>());
|
||||
Assert.Equal(10.0, hot["summary"]!["metrics"]!["loss.total"]!.GetValue<double>(), 6);
|
||||
@@ -62,7 +71,7 @@ public sealed class AotSmokeTests
|
||||
JsonNode persisted;
|
||||
do
|
||||
{
|
||||
persisted = await GetAsync(http, $"/api/public/lines/{agent.NodeId.Value}/series?{range}&step=1m");
|
||||
persisted = await GetAsync(http, $"/api/public/lines/{lineId}/series?{range}&step=1m");
|
||||
if (persisted["summary"]!["samples"]!["udp_echo"]?.GetValue<long>() == 600)
|
||||
{
|
||||
break;
|
||||
@@ -80,16 +89,16 @@ public sealed class AotSmokeTests
|
||||
Assert.True(RawChunkCodec.Decode(bytes).IsSuccess);
|
||||
|
||||
// SSE:快照 + 增量
|
||||
await using SseClient sse = await SseClient.ConnectAsync(http, $"/api/public/live?topics=nodes,series:{agent.NodeId.Value}:1s:udp");
|
||||
await using SseClient sse = await SseClient.ConnectAsync(http, $"/api/public/live?topics=nodes,series:{lineId}:1s:udp");
|
||||
await sse.WaitForAsync(e => e.Name == "series");
|
||||
|
||||
// 按需测试:Probe 事件 → 下发 → 状态
|
||||
await agent.SendEventAsync(AgentEventKinds.ProbeConnected);
|
||||
await agent.SendEventAsync(AgentEventKinds.ProbeConnected, probeId);
|
||||
JsonNode started = null!;
|
||||
long begun = TimeProvider.System.GetTimestamp();
|
||||
while (TimeProvider.System.GetElapsedTime(begun) < TimeSpan.FromSeconds(10))
|
||||
{
|
||||
using HttpResponseMessage response = await http.PostAsync($"/api/admin/lines/{agent.NodeId.Value}/tests", BackendFixture.Json(new JsonObject { ["plan"] = "reality_session" }));
|
||||
using HttpResponseMessage response = await http.PostAsync($"/api/admin/lines/{lineId}/tests", BackendFixture.Json(new JsonObject { ["plan"] = "reality_session" }));
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Accepted)
|
||||
{
|
||||
started = JsonNode.Parse(await response.Content.ReadAsStringAsync())!;
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class HostSamplerTests
|
||||
Assert.Equal((1000L, 5000L), HostSampler.ParseNetwork(Dev1));
|
||||
Assert.Equal(3600L, HostSampler.ParseUptime("3600.55 7000.1\n"));
|
||||
Assert.Null(HostSampler.ParseNetwork("nothing useful"));
|
||||
Assert.Equal((1000L, 5000L), HostSampler.ParseNetwork(Dev1 + "podman0: 8888 1 0 0 0 0 0 0 9999 1 0 0 0 0 0 0\n"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -29,7 +30,7 @@ public sealed class HostSamplerTests
|
||||
var time = new Microsoft.Extensions.Time.Testing.FakeTimeProvider();
|
||||
string stat = Stat1;
|
||||
string dev = Dev1;
|
||||
var sampler = new HostSampler(time, path => path switch { "/proc/stat" => stat, "/proc/net/dev" => dev, "/proc/uptime" => "100.0 1.0", _ => null }, () => (1000, 250));
|
||||
var sampler = new HostSampler(time, path => path switch { "/proc/stat" => stat, "/proc/net/dev" => dev, "/proc/uptime" => "100.0 1.0", _ => null }, () => (1000, 250), procRoot: "/proc", sysRoot: "/sys");
|
||||
HostSnapshot first = sampler.Sample();
|
||||
Assert.Null(first.CpuPercent); // 第一次没有基线
|
||||
Assert.Null(first.NetRxBps);
|
||||
@@ -45,6 +46,54 @@ public sealed class HostSamplerTests
|
||||
Assert.Equal(2000, second.NetTxBps);
|
||||
Assert.Equal(3000, second.NetRxTotal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sample_UsesMountedHostProcAndSys_InsteadOfContainerPaths()
|
||||
{
|
||||
var reads = new List<string>();
|
||||
var sampler = new HostSampler(TimeProvider.System, path =>
|
||||
{
|
||||
reads.Add(path);
|
||||
return path switch
|
||||
{
|
||||
"/host/proc/stat" => Stat1,
|
||||
"/host/proc/meminfo" => "MemTotal: 8192 kB\nMemAvailable: 6144 kB\n",
|
||||
"/host/proc/net/dev" => Dev1,
|
||||
"/host/proc/uptime" => "3600.0 7000.0",
|
||||
"/host/sys/class/net/eth0/speed" => "10000\n",
|
||||
_ => throw new InvalidOperationException("unexpected container path: " + path),
|
||||
};
|
||||
}, () => (1000, 250), procRoot: "/host/proc", sysRoot: "/host/sys");
|
||||
|
||||
HostSnapshot snapshot = sampler.Sample();
|
||||
Assert.Equal(8L * 1024 * 1024, snapshot.MemTotal);
|
||||
Assert.Equal(1000, snapshot.NetRxTotal);
|
||||
Assert.Equal(10000, snapshot.NetLinkMbps);
|
||||
Assert.Equal(3600, snapshot.UptimeSeconds);
|
||||
Assert.DoesNotContain(reads, path => path.StartsWith("/proc/", StringComparison.Ordinal));
|
||||
Assert.DoesNotContain(reads, path => path.Contains("docker0", StringComparison.Ordinal) || path.Contains("/lo/", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("-1")]
|
||||
[InlineData("0")]
|
||||
[InlineData("not supported")]
|
||||
public void Sample_DoesNotInventLinkSpeedOrContainerDisk_WhenHostDataIsUnavailable(string? speed)
|
||||
{
|
||||
var sampler = new HostSampler(TimeProvider.System,
|
||||
path => path.EndsWith("net/dev", StringComparison.Ordinal) ? Dev1 : path.EndsWith("/speed", StringComparison.Ordinal) ? speed : null,
|
||||
procRoot: "/host/proc", sysRoot: "/host/sys", diskRoot: Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
|
||||
|
||||
HostSnapshot snapshot = sampler.Sample();
|
||||
Assert.Null(snapshot.NetLinkMbps);
|
||||
Assert.Null(snapshot.DiskTotal);
|
||||
Assert.Null(snapshot.DiskUsed);
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
Assert.Null(snapshot.UptimeSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Collection("backend")]
|
||||
@@ -61,6 +110,7 @@ public sealed class HostStatusTests
|
||||
{
|
||||
CpuPercent = cpu, MemUsed = 2L << 30, MemTotal = 8L << 30, DiskUsed = 10L << 30, DiskTotal = 40L << 30,
|
||||
NetRxTotal = 600L << 30, NetTxTotal = 180L << 30, NetRxBps = 2500, NetTxBps = 4800, UptimeSeconds = 3_000_000,
|
||||
NetLinkMbps = 1000,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
@@ -81,6 +131,7 @@ public sealed class HostStatusTests
|
||||
JsonNode item = list["items"]!.AsArray().Single(i => i!["agent_id"]!.GetValue<string>() == id)!;
|
||||
Assert.False(item["stale"]!.GetValue<bool>());
|
||||
Assert.Equal(40L << 30, item["host"]!["disk_total"]!.GetValue<long>());
|
||||
Assert.Equal(1000, item["host"]!["net_link_mbps"]!.GetValue<long>());
|
||||
|
||||
// 展示页能拿到的只有汇总数字:整份响应里没有型号、主机名、地址之类的字段
|
||||
string text = list.ToJsonString();
|
||||
|
||||
Reference in new issue
Block a user