feat: one-line install for agents and probes from one shared script (curl | bash, zsh, PowerShell) [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
This commit is contained in:
1 parent
5528753d45
commit
af2962fc58
29 files changed
+558
-124
No files matched your search
@@ -222,11 +222,12 @@ public sealed class ApiBasicsTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InstallScripts_AreNotShippedYet()
|
||||
public async Task InstallScripts_RequireAValidCode()
|
||||
{
|
||||
// 一行安装脚本凭一次性代码获取;没有代码或代码不能用是 404(完整行为见 InstallTests)
|
||||
using HttpResponseMessage response = await _backend.Anonymous.GetAsync("/install.sh");
|
||||
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
|
||||
Assert.Equal("not_implemented", JsonNode.Parse(await response.Content.ReadAsStringAsync())!["code"]!.GetValue<string>());
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Equal("not_found", JsonNode.Parse(await response.Content.ReadAsStringAsync())!["code"]!.GetValue<string>());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Net;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>一行安装:Agent 和 Probe 共用同一份脚本,角色、网关、镜像由一次性代码决定,节点名字默认取主机名。</summary>
|
||||
[Collection("probe-e2e")]
|
||||
public sealed class InstallTests
|
||||
{
|
||||
private readonly BackendFixture _backend;
|
||||
|
||||
public InstallTests(BackendFixture backend)
|
||||
{
|
||||
_backend = backend;
|
||||
}
|
||||
|
||||
private async Task<JsonNode> CreateAsync(string role)
|
||||
{
|
||||
using HttpResponseMessage response = await _backend.Admin.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = role }));
|
||||
return JsonNode.Parse(await response.Content.ReadAsStringAsync())!;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("agent")]
|
||||
[InlineData("probe")]
|
||||
public async Task Commands_AreOneLiners_WithoutAPlaceholder_AndTheScriptIsFilledInForTheRole(string role)
|
||||
{
|
||||
JsonNode created = await CreateAsync(role);
|
||||
string code = created["plaintext"]!.GetValue<string>();
|
||||
JsonNode commands = created["install_commands"]!;
|
||||
string linux = commands["linux"]!.GetValue<string>();
|
||||
string macos = commands["macos"]!.GetValue<string>();
|
||||
string windows = commands["windows"]!.GetValue<string>();
|
||||
Assert.StartsWith("curl -fsSL '", linux, StringComparison.Ordinal);
|
||||
Assert.EndsWith("/install?code=" + code + "' | bash", linux, StringComparison.Ordinal);
|
||||
Assert.EndsWith("/install?code=" + code + "' | zsh", macos, StringComparison.Ordinal);
|
||||
Assert.StartsWith("irm '", windows, StringComparison.Ordinal);
|
||||
Assert.EndsWith("/install.ps1?code=" + code + "' | iex", windows, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("<", linux + macos + windows, StringComparison.Ordinal); // 没有要用户自己替换的占位符
|
||||
|
||||
// 脚本不需要登录(拿代码就是授权),不带缓存
|
||||
using HttpResponseMessage sh = await _backend.Anonymous.GetAsync("/install?code=" + code);
|
||||
string script = await sh.Content.ReadAsStringAsync();
|
||||
Assert.Equal(HttpStatusCode.OK, sh.StatusCode);
|
||||
Assert.Equal("no-store", sh.Headers.CacheControl?.ToString());
|
||||
Assert.Contains($"ROLE='{role}'", script, StringComparison.Ordinal);
|
||||
Assert.Contains($"CODE='{code}'", script, StringComparison.Ordinal);
|
||||
Assert.Contains($"/moshiro-{role}:latest'", script, StringComparison.Ordinal);
|
||||
Assert.Contains("SERVER='http", script, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("@@", script, StringComparison.Ordinal);
|
||||
|
||||
using HttpResponseMessage ps = await _backend.Anonymous.GetAsync("/install.ps1?code=" + code);
|
||||
string powershell = await ps.Content.ReadAsStringAsync();
|
||||
Assert.Equal(HttpStatusCode.OK, ps.StatusCode);
|
||||
Assert.Contains($"$role = '{role}'", powershell, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("@@", powershell, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Script_IsNotServedForUnknownRevokedOrUsedUpCodes()
|
||||
{
|
||||
foreach (string path in new[] { "/install", "/install?code=", "/install?code=jt_nope", "/install.ps1?code=jt_nope", "/install.sh?code=jt_nope" })
|
||||
{
|
||||
using HttpResponseMessage response = await _backend.Anonymous.GetAsync(path);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
JsonNode revoked = await CreateAsync("agent");
|
||||
using HttpResponseMessage delete = await _backend.Admin.DeleteAsync("/api/admin/join-tokens/" + revoked["token"]!["id"]!.GetValue<string>());
|
||||
Assert.True(delete.IsSuccessStatusCode);
|
||||
using HttpResponseMessage gone = await _backend.Anonymous.GetAsync("/install?code=" + revoked["plaintext"]!.GetValue<string>());
|
||||
Assert.Equal(HttpStatusCode.NotFound, gone.StatusCode);
|
||||
|
||||
// 用完了(一次性令牌已经注册过一个节点)
|
||||
JsonNode used = await CreateAsync("probe");
|
||||
string code = used["plaintext"]!.GetValue<string>();
|
||||
using HttpResponseMessage before = await _backend.Anonymous.GetAsync("/install?code=" + code);
|
||||
Assert.Equal(HttpStatusCode.OK, before.StatusCode);
|
||||
await using (await MoShiro.TestSupport.AgentSimulator.EnrollAsync(_backend.Anonymous, code, _backend.UniqueName("used"))) { }
|
||||
using HttpResponseMessage after = await _backend.Anonymous.GetAsync("/install?code=" + code);
|
||||
Assert.Equal(HttpStatusCode.NotFound, after.StatusCode);
|
||||
}
|
||||
}
|
||||
@@ -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.sh", "/install.ps1"];
|
||||
string[] roots = ["/healthz", "/metrics", "/install", "/install.ps1"];
|
||||
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))
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user