feat!: probes generate their own keys and enroll with a join token; node and peer keys are separate [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

This commit is contained in:
justme committed 2026-10-07 16:57:34 +08:00
1 parent 1ff847e45a
commit a9f562441f
51 files changed
+505 -749

No files matched your search

@@ -2,7 +2,8 @@ using Fleet.Core;
namespace MoShiro.Backend.Application;
public sealed record CreateJoinTokenRequest(string? Note, IReadOnlyList<string>? Labels, bool AutoApprove, int MaxUses, int ExpiresInHours);
/// <param name="Role"><c>agent</c>(默认)或 <c>probe</c>:用这个令牌注册出来的是哪种节点。</param>
public sealed record CreateJoinTokenRequest(string? Note, IReadOnlyList<string>? Labels, bool AutoApprove, int MaxUses, int ExpiresInHours, string Role = JoinTokenService.AgentRole);
/// <summary>各个系统上的安装命令(Linux 和 macOS 都用 install.sh,Windows 用 install.ps1)。</summary>
public sealed record InstallCommands(string Linux, string Macos, string Windows);
@@ -12,6 +13,14 @@ public sealed record CreatedJoinToken(JoinTokenInfo Info, string Plaintext, Inst
/// <summary>加入令牌:明文只在创建时返回一次,服务端只保存 SHA-256。</summary>
public sealed class JoinTokenService
{
public const string AgentRole = "agent";
public const string ProbeRole = "probe";
/// <summary>令牌里存的「种类」:空表示 Agent,其余就是角色名。</summary>
public static string KindOf(string role) => role == ProbeRole ? ProbeRole : "";
public static string RoleOf(string kind) => kind.Length == 0 ? AgentRole : kind;
private readonly IJoinTokenStore _store;
private readonly BackendOptions _options;
private readonly TimeProvider _time;
@@ -26,6 +35,11 @@ public sealed class JoinTokenService
public async ValueTask<Result<CreatedJoinToken>> CreateAsync(CreateJoinTokenRequest request, string? requestBaseUrl, CancellationToken ct)
{
var errors = new Dictionary<string, string>();
if (request.Role is not (AgentRole or ProbeRole))
{
errors["role"] = "must be agent or probe";
}
if (request.MaxUses is < 1 or > 1000)
{
errors["max_uses"] = "must be between 1 and 1000";
@@ -54,13 +68,18 @@ public sealed class JoinTokenService
string plaintext = TokenHash.NewToken("jt");
long now = _time.GetUtcNow().ToUnixTimeMilliseconds();
JoinTokenInfo info = await _store.CreateAsync(
Ulid.NewPrefixed("jtk", _time), TokenHash.Sha256(plaintext), request.Note, request.Labels ?? [], request.AutoApprove,
Ulid.NewPrefixed("jtk", _time), TokenHash.Sha256(plaintext), request.Note, request.Labels ?? [], request.AutoApprove, KindOf(request.Role),
request.MaxUses, now + (request.ExpiresInHours * 3_600_000L), now, ct);
string baseUrl = (_options.PublicUrl ?? requestBaseUrl ?? "https://panel.example.com").TrimEnd('/');
string shell = $"curl -fsSL {baseUrl}/install.sh | sh -s -- --join {plaintext} --name <node-name>";
// Probe 和 Agent 一样用加入令牌注册;容器节点用部署脚本,Linux 二进制用 install.sh
string shell = request.Role == ProbeRole
? $"sudo sh deploy-node.sh probe --server {baseUrl} --join {plaintext} --name <node-name>"
: $"curl -fsSL {baseUrl}/install.sh | sh -s -- --join {plaintext} --name <node-name>";
// PowerShell 6+(pwsh):在 cmd 或 PowerShell 里都能直接粘贴
string powershell = $"pwsh -NoProfile -ExecutionPolicy Bypass -Command \"& ([scriptblock]::Create((irm {baseUrl}/install.ps1))) -Join {plaintext} -Name <node-name>\"";
string powershell = request.Role == ProbeRole
? $"fleet-enroll --server {baseUrl} --join {plaintext} --name <node-name> --identity-dir <dir>"
: $"pwsh -NoProfile -ExecutionPolicy Bypass -Command \"& ([scriptblock]::Create((irm {baseUrl}/install.ps1))) -Join {plaintext} -Name <node-name>\"";
return Result<CreatedJoinToken>.Ok(new CreatedJoinToken(info, plaintext, new InstallCommands(shell, shell, powershell)));
}