Some checks are pending
CI / Build og unit tests (push) Waiting to run
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
Rule packs (support dates and newest CU per release) are published in a signed feed and hot-loaded without a restart; the verified pack is reloaded at startup. New versions are announced (Notify) or downloaded and staged by the unprivileged agent (Automatic), and installed by a privileged apply-update task that re-verifies signature and hash in an admin-only folder, keeps appsettings/web.config, health-checks the services and rolls back on failure. Stable and preview channels, manifest expiry and rollback protection, https or folder/share feeds for offline sites. Vendor tool gains rulepack and publish; the Hub can serve the feed; the web UI gets a System page with update, rule pack, license and exactly what is sent to the Hub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
92 lines
4.2 KiB
C#
92 lines
4.2 KiB
C#
using System.Text;
|
|
using SQLInsight.Licensing;
|
|
using SQLInsight.Rules.Lifecycle;
|
|
|
|
namespace SQLInsight.Updates;
|
|
|
|
public sealed record CheckedManifest(UpdateManifest Manifest, string Token);
|
|
|
|
/// <summary>
|
|
/// The unprivileged half of updating, run by the agent: reads the signed manifest, installs newer rule packs, and
|
|
/// downloads and stages application packages. It never replaces program files; <see cref="UpdateApplier"/> does that.
|
|
/// </summary>
|
|
public sealed class UpdateChecker(UpdateFeed feed, TrustedKeys keys, UpdateDirectories directories, string channel, TimeProvider time)
|
|
{
|
|
public string Channel { get; } = channel;
|
|
|
|
public async Task<CheckedManifest> FetchAsync(CancellationToken cancellationToken)
|
|
{
|
|
var token = await feed.ReadManifestAsync(Channel, cancellationToken).ConfigureAwait(false);
|
|
var manifest = ManifestVerifier.Verify(token, keys, Channel, time.GetUtcNow(), directories.LastManifest(Channel));
|
|
directories.RememberManifest(Channel, manifest.PublishedAt);
|
|
return new CheckedManifest(manifest, token);
|
|
}
|
|
|
|
/// <summary>Downloads the manifest's rule pack if it is newer than <paramref name="current"/>. Returns the new pack or null.</summary>
|
|
public async Task<RulePack?> UpdateRulePackAsync(CheckedManifest source, RulePack current, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(source);
|
|
ArgumentNullException.ThrowIfNull(current);
|
|
if (source.Manifest.RulePack is not { } reference || !Version.TryParse(reference.Version, out var offered) || offered <= current.Version)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var bytes = await feed.DownloadAsync(reference.File, cancellationToken).ConfigureAwait(false);
|
|
RulePack pack;
|
|
try
|
|
{
|
|
pack = RulePack.Parse(bytes, $"regelpakke fra {source.Manifest.Channel}-kanalen, {source.Manifest.PublishedAt:yyyy-MM-dd}");
|
|
}
|
|
catch (InvalidDataException ex)
|
|
{
|
|
throw new UpdateException(ex.Message, ex);
|
|
}
|
|
|
|
if (pack.Version != offered)
|
|
{
|
|
throw new UpdateException($"Regelpakkens version ({pack.Version}) svarer ikke til manifestet ({offered}).");
|
|
}
|
|
|
|
new RulePackStore(directories).Save(bytes, source.Token);
|
|
return pack;
|
|
}
|
|
|
|
/// <summary>The release in the manifest if it is newer than the running version, otherwise null.</summary>
|
|
public static AppRelease? NewerRelease(UpdateManifest manifest, AppVersion installed)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(manifest);
|
|
return manifest.Release is { } release && AppVersion.Parse(release.Version) > installed ? release : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads the package for this platform into the pending folder, where the privileged updater picks it up.
|
|
/// Returns false if there is nothing newer than <paramref name="installed"/>.
|
|
/// </summary>
|
|
public async Task<bool> StageAsync(CheckedManifest source, AppVersion installed, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(source);
|
|
if (NewerRelease(source.Manifest, installed) is not { } release)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var asset = release.Assets.FirstOrDefault(a => string.Equals(a.Platform, Platform.Current, StringComparison.OrdinalIgnoreCase))
|
|
?? throw new UpdateException($"Version {release.Version} har ingen pakke til {Platform.Current}.");
|
|
|
|
var package = Path.Combine(directories.Pending, "package.zip");
|
|
var manifestFile = Path.Combine(directories.Pending, UpdateFeed.ManifestFile);
|
|
if (File.Exists(package) && File.Exists(manifestFile) && await File.ReadAllTextAsync(manifestFile, cancellationToken).ConfigureAwait(false) == source.Token)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
Directory.CreateDirectory(directories.Pending);
|
|
File.Delete(manifestFile);
|
|
var download = package + ".download";
|
|
await feed.DownloadToFileAsync(asset.File, download, cancellationToken).ConfigureAwait(false);
|
|
File.Move(download, package, overwrite: true);
|
|
UpdateDirectories.WriteAtomic(manifestFile, Encoding.UTF8.GetBytes(source.Token));
|
|
return true;
|
|
}
|
|
}
|