ci/woodpecker/push/main-amd64-build Pipeline was successful
The hash a node reports about itself cannot be trusted (a swapped binary can copy the old hash), so the runtime binding only caught files that were replaced by something that did not lie. Provenance stays where it can be verified: the signed release manifest (digest) and the signed SHA256SUMS. - Remove BinaryBindingService, the node_bindings table (schema v15 drops it), BinaryPolicy, the handshake hash/manifest fields and the admin binary column. - Drop the per-platform "binaries" list from the release manifest, the hash scripts and the updater's installed.manifest copy. - ReleaseService.PublishAsync now verifies the release signature, so the admin POST /releases route is no longer an unsigned path; tests sign with a test key. - Docs: section 16 explains why the runtime check is gone.
136 lines
5.7 KiB
C#
136 lines
5.7 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using Fleet.Rpc;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Fleet.Server;
|
|
|
|
/// <summary>Fleet 服务端的接入点:产品在这里挂上自己的状态、收件箱、事件和 RPC 方法。</summary>
|
|
public sealed class FleetServerBuilder
|
|
{
|
|
private readonly List<Action<RpcRouter<NodeCallContext>, IServiceProvider>> _routeConfigurators = [];
|
|
private MethodTable? _methods;
|
|
private Func<NodeRecord, string?>? _roleOf;
|
|
|
|
internal FleetServerBuilder(IServiceCollection services)
|
|
{
|
|
Services = services;
|
|
}
|
|
|
|
public IServiceCollection Services { get; }
|
|
|
|
internal IReadOnlyList<Action<RpcRouter<NodeCallContext>, IServiceProvider>> RouteConfigurators => _routeConfigurators;
|
|
|
|
internal MethodTable Methods => _methods ?? throw new InvalidOperationException("call WithMethods(...) when adding the Fleet server: the method table is required");
|
|
|
|
internal Func<NodeRecord, string?>? RoleOf => _roleOf;
|
|
|
|
/// <summary>
|
|
/// 方法声明表(框架的加产品的,见 <see cref="MethodTable.Combine"/>)和节点的角色解析(例如按节点 ID 前缀区分 Agent 和 Probe)。必须设置:
|
|
/// 路由器据此核对每个处理函数的方向、类型和角色,并拒绝没有声明或不该由节点发起的方法。
|
|
/// </summary>
|
|
public FleetServerBuilder WithMethods(MethodTable methods, Func<NodeRecord, string?> roleOf)
|
|
{
|
|
_methods = methods;
|
|
_roleOf = roleOf;
|
|
return this;
|
|
}
|
|
|
|
public FleetServerBuilder AddState<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TProvider>()
|
|
where TProvider : class, IStateProvider
|
|
{
|
|
Services.AddSingleton<IStateProvider, TProvider>();
|
|
return this;
|
|
}
|
|
|
|
public FleetServerBuilder AddInbox<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TInbox>()
|
|
where TInbox : class, IInbox
|
|
{
|
|
Services.AddSingleton<IInbox, TInbox>();
|
|
return this;
|
|
}
|
|
|
|
public FleetServerBuilder OnNodeEvent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TSink>()
|
|
where TSink : class, INodeEventSink
|
|
{
|
|
Services.AddSingleton<INodeEventSink, TSink>();
|
|
return this;
|
|
}
|
|
|
|
public FleetServerBuilder OnEnrolled<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THook>()
|
|
where THook : class, IEnrollmentHook
|
|
{
|
|
Services.AddSingleton<IEnrollmentHook, THook>();
|
|
return this;
|
|
}
|
|
|
|
/// <summary>注册产品的 RPC 方法(<c>moshiro.*</c> 等)。处理函数通过 <see cref="IServiceProvider"/> 在调用时解析服务,避免构造期的循环依赖。</summary>
|
|
public FleetServerBuilder MapMethods(Action<RpcRouter<NodeCallContext>, IServiceProvider> configure)
|
|
{
|
|
_routeConfigurators.Add(configure);
|
|
return this;
|
|
}
|
|
}
|
|
|
|
public static class FleetServerExtensions
|
|
{
|
|
/// <summary>需要事先在 DI 中注册 <see cref="IFleetStore"/>。</summary>
|
|
public static FleetServerBuilder AddFleetServer(this IServiceCollection services, Action<FleetServerOptions>? configure = null)
|
|
{
|
|
var options = new FleetServerOptions();
|
|
configure?.Invoke(options);
|
|
services.AddSingleton(options);
|
|
|
|
var builder = new FleetServerBuilder(services);
|
|
services.AddSingleton(provider =>
|
|
{
|
|
ILogger logger = provider.GetRequiredService<ILoggerFactory>().CreateLogger("Fleet.Rpc");
|
|
Func<NodeRecord, string?>? roleOf = builder.RoleOf;
|
|
var router = new RpcRouter<NodeCallContext>(
|
|
RpcSide.Server,
|
|
builder.Methods,
|
|
callerRole: roleOf is null ? null : context => roleOf(context.Node),
|
|
onUnhandled: (method, ex) => FleetLog.HandlerFailed(logger, ex, method));
|
|
foreach (Action<RpcRouter<NodeCallContext>, IServiceProvider> configurator in builder.RouteConfigurators)
|
|
{
|
|
configurator(router, provider);
|
|
}
|
|
|
|
return new FleetRouter(router, roleOf);
|
|
});
|
|
services.AddSingleton(provider => new FleetLinkHost(
|
|
provider.GetRequiredService<IFleetStore>(),
|
|
provider.GetRequiredService<FleetServerOptions>(),
|
|
provider.GetRequiredService<TimeProvider>(),
|
|
provider.GetRequiredService<ILogger<FleetLinkHost>>(),
|
|
provider.GetServices<IStateProvider>(),
|
|
provider.GetServices<IInbox>(),
|
|
provider.GetServices<INodeEventSink>(),
|
|
provider.GetRequiredService<FleetRouter>(),
|
|
provider.GetService<IServerIdentity>()));
|
|
services.AddSingleton<IFleetConnections>(provider => provider.GetRequiredService<FleetLinkHost>());
|
|
services.AddSingleton<IStateNotifier>(provider => provider.GetRequiredService<FleetLinkHost>());
|
|
return builder;
|
|
}
|
|
|
|
/// <summary>映射 WebSocket 端点。调用方需要先 <c>app.UseWebSockets()</c>。</summary>
|
|
public static IEndpointRouteBuilder MapFleetLink(this IEndpointRouteBuilder app, string path)
|
|
{
|
|
app.Map(path, async (HttpContext http) =>
|
|
{
|
|
if (!http.WebSockets.IsWebSocketRequest)
|
|
{
|
|
http.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
return;
|
|
}
|
|
|
|
using System.Net.WebSockets.WebSocket socket = await http.WebSockets.AcceptWebSocketAsync();
|
|
await http.RequestServices.GetRequiredService<FleetLinkHost>().RunConnectionAsync(socket, http.RequestAborted);
|
|
});
|
|
return app;
|
|
}
|
|
}
|