fix: installer checks containers only; the panel reads signed manifests from the registry when it has none; unsigned-updater nodes are labeled from the panel's manifests; sign node release images; release 0.3.0 [CI]
This commit is contained in:
1 parent
4a1e48db20
commit
879d1c8593
15 files changed
+372
-18
No files matched your search
@@ -0,0 +1,123 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MoShiro.Backend.Application;
|
||||
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 面板没有登记过清单时(清过库、CI 登记那一步网络失败),直接从镜像仓库里匿名读取签名清单;取回来的要用 CI 的发布公钥验过才采用,
|
||||
/// 所以仓库(或者路上的人)给一份伪造的,面板拒绝,安装脚本也就得不到它。
|
||||
/// </summary>
|
||||
public sealed class ReleaseFeedTests : IAsyncLifetime
|
||||
{
|
||||
private WebApplication _registry = null!;
|
||||
private string _registryUrl = "";
|
||||
private string _label = "";
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(["--urls", "http://127.0.0.1:0"]);
|
||||
_registry = builder.Build();
|
||||
const string ConfigDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000001";
|
||||
_registry.MapGet("/v2/moshiro-dev/moshiro-agent-manifest/manifests/dev", () => Results.Text(new JsonObject { ["schemaVersion"] = 2, ["config"] = new JsonObject { ["digest"] = ConfigDigest } }.ToJsonString(), "application/vnd.oci.image.manifest.v1+json"));
|
||||
_registry.MapGet("/v2/moshiro-dev/moshiro-agent-manifest/blobs/" + ConfigDigest, () => Results.Text(
|
||||
new JsonObject { ["config"] = new JsonObject { ["Labels"] = new JsonObject { ["org.moshiro.manifest"] = _label, ["org.moshiro.signature"] = "c2ln" } } }.ToJsonString(), "application/json"));
|
||||
await _registry.StartAsync();
|
||||
_registryUrl = _registry.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>()!.Addresses.First();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _registry.StopAsync();
|
||||
await _registry.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheManifestAndSignature_AreReadFromTheLabelsOfTheSmallImage()
|
||||
{
|
||||
string manifest = "{\"schema\":1,\"component\":\"agent\",\"seq\":7}";
|
||||
_label = Convert.ToBase64String(Encoding.UTF8.GetBytes(manifest));
|
||||
using var feed = new ReleaseFeed(new BackendOptions { NodeImageRegistry = _registryUrl });
|
||||
|
||||
(string Manifest, string Signature)? fetched = await feed.FetchAsync("agent", "dev", default);
|
||||
|
||||
Assert.Equal(manifest, fetched!.Value.Manifest);
|
||||
Assert.Equal("c2ln", fetched.Value.Signature);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("probe", "dev")] // 仓库里没有这个组件的清单
|
||||
[InlineData("agent", "release")] // 没有这个通道
|
||||
public async Task NothingThere_MeansNull_NotAnException(string component, string channel)
|
||||
{
|
||||
using var feed = new ReleaseFeed(new BackendOptions { NodeImageRegistry = _registryUrl });
|
||||
|
||||
Assert.Null(await feed.FetchAsync(component, channel, default));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnUnreachableOrEmptyRegistry_MeansNull()
|
||||
{
|
||||
using var unreachable = new ReleaseFeed(new BackendOptions { NodeImageRegistry = "http://127.0.0.1:1" });
|
||||
using var empty = new ReleaseFeed(new BackendOptions { NodeImageRegistry = "" });
|
||||
|
||||
Assert.Null(await unreachable.FetchAsync("agent", "dev", default));
|
||||
Assert.Null(await empty.FetchAsync("agent", "dev", default));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AForgedManifestInTheRegistry_IsNeverServedToTheInstaller()
|
||||
{
|
||||
// 起一个指向这个假仓库的后端:仓库里是一份没有 CI 签名的清单,安装脚本向面板要清单时必须拿不到
|
||||
string directory = Path.Combine(Path.GetTempPath(), "moshiro-feed-" + Guid.NewGuid().ToString("N"));
|
||||
_label = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"schema\":1,\"component\":\"agent\",\"channel\":\"dev\",\"image\":\"evil/agent\",\"digest\":\"sha256:" + new string('a', 64) + "\",\"tag\":\"x\",\"version\":\"9\",\"seq\":999999999999}"));
|
||||
await using WebApplication panel = BackendHost.Build(
|
||||
["--urls", "http://127.0.0.1:0", "--Logging:LogLevel:Default=Warning"],
|
||||
options =>
|
||||
{
|
||||
options.DataDirectory = directory;
|
||||
options.Pbkdf2Iterations = 1000;
|
||||
options.SecureCookies = false;
|
||||
options.ClaimCode = "FEED-TEST-CODE";
|
||||
options.NodeImageRegistry = _registryUrl;
|
||||
options.UpdateChannel = "dev";
|
||||
options.GeoLookupUrl = string.Empty;
|
||||
options.RequireOwner = false;
|
||||
});
|
||||
await panel.StartAsync();
|
||||
try
|
||||
{
|
||||
using var http = new HttpClient { BaseAddress = new Uri(panel.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>()!.Addresses.First()) };
|
||||
using HttpResponseMessage setup = await http.PostAsync("/api/admin/auth/setup", BackendFixture.Json(new JsonObject { ["claim_code"] = "FEED-TEST-CODE", ["username"] = "owner", ["password"] = "correct horse battery", ["return_session"] = true }));
|
||||
Assert.Equal(HttpStatusCode.Created, setup.StatusCode);
|
||||
http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", JsonNode.Parse(await setup.Content.ReadAsStringAsync())!["session_token"]!.GetValue<string>());
|
||||
using HttpResponseMessage created = await http.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "agent" }));
|
||||
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
|
||||
string code = JsonNode.Parse(await created.Content.ReadAsStringAsync())!["plaintext"]!.GetValue<string>();
|
||||
|
||||
using HttpResponseMessage manifest = await http.GetAsync("/install/manifest?code=" + Uri.EscapeDataString(code));
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, manifest.StatusCode); // 没有有效的 CI 签名,面板不采用
|
||||
}
|
||||
finally
|
||||
{
|
||||
await panel.StopAsync();
|
||||
await panel.DisposeAsync();
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
try
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// 系统临时目录里的残留
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user