Probes connect to the panel directly and get the agent list as desired state. CI signs the image digest of agent and probe releases, the panel forwards the manifest, and a host updater verifies it with a local public key, pulls by digest and rolls back when the new version is unhealthy.
101 lines
4.1 KiB
C#
101 lines
4.1 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using Fleet.Core;
|
|
using Monitor.Protocol;
|
|
|
|
namespace Monitor.Measurement;
|
|
|
|
/// <summary>
|
|
/// 节点进程和宿主机更新器之间的文件交换(容器里 <c>/etc/monitor</c>,也就是数据目录):
|
|
/// 节点把面板发来的清单和签名写进 <c>update/</c>,把「更新」请求写成 <c>update/apply.request</c>;更新器验签并更新,状态写在 root 拥有的 <c>update-status/status.json</c>。
|
|
/// 节点进程不执行任何命令,也不信任清单的内容(验签在宿主机上)。没有安装更新器时目录写不了或没人读,调用只是返回 false。
|
|
/// </summary>
|
|
public sealed class UpdateBridge
|
|
{
|
|
private const int MaxManifestBytes = 4096;
|
|
private const int MaxSignatureBytes = 512;
|
|
private const int MaxStatusBytes = 4096;
|
|
|
|
private readonly string _updateDirectory;
|
|
private readonly string _statusFile;
|
|
private readonly Action<string, byte[]> _write;
|
|
private readonly INodeLog _log;
|
|
|
|
/// <param name="write">写数据目录里的文件(相对路径,原子替换):由节点传入 <c>ConfigStore.Write</c>——常驻进程里只有它可以写文件。</param>
|
|
public UpdateBridge(string dataDirectory, Action<string, byte[]> write, INodeLog log)
|
|
{
|
|
_updateDirectory = Path.Combine(dataDirectory, "update");
|
|
_statusFile = Path.Combine(dataDirectory, "update-status", "status.json");
|
|
_write = write;
|
|
_log = log;
|
|
}
|
|
|
|
/// <summary>放下面板发来的清单和签名(和已有的一样就不动,免得每次重连都惊动更新器)。先写清单、后写签名:更新器监视的是签名文件。</summary>
|
|
public bool WriteOffer(UpdateOfferParams offer)
|
|
{
|
|
byte[] manifest = Encoding.UTF8.GetBytes(offer.Manifest);
|
|
byte[] signature = Encoding.UTF8.GetBytes(offer.Signature);
|
|
if (manifest.Length is 0 or > MaxManifestBytes || signature.Length is 0 or > MaxSignatureBytes)
|
|
{
|
|
_log.Warn("ignoring an update offer of an unexpected size");
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
string manifestPath = Path.Combine(_updateDirectory, "offer.manifest");
|
|
string signaturePath = Path.Combine(_updateDirectory, "offer.sig");
|
|
if (Same(manifestPath, manifest) && Same(signaturePath, signature))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
_write("update/offer.manifest", manifest);
|
|
_write("update/offer.sig", signature);
|
|
_log.Info("update offer received");
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
{
|
|
_log.Warn("could not store the update offer (is the updater installed?)", ("err", ex.Message));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>请求更新:写一个空的 <c>apply.request</c>(更新器验签通过、清单更新才会真的动手)。</summary>
|
|
public bool RequestApply()
|
|
{
|
|
try
|
|
{
|
|
_write("update/apply.request", []);
|
|
_log.Info("update requested");
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
{
|
|
_log.Warn("could not request the update (is the updater installed?)", ("err", ex.Message));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>更新器的状态;没有安装更新器、文件不存在或内容不对时返回 null。</summary>
|
|
public UpdateStatus? ReadStatus()
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(_statusFile) || new FileInfo(_statusFile).Length > MaxStatusBytes)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return JsonSerializer.Deserialize(File.ReadAllBytes(_statusFile), MonitorJsonContext.Default.UpdateStatus);
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static bool Same(string path, byte[] content) => File.Exists(path) && File.ReadAllBytes(path).AsSpan().SequenceEqual(content);
|
|
}
|