feat: add remote control plane and whitelist reads
Build web service image / build (push) Successful in 1m53s
Build web service image / build (push) Successful in 1m53s
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace WxAgent.Core;
|
||||
|
||||
public sealed record RemoteConfigurationAuditEntry
|
||||
{
|
||||
[JsonPropertyName("at")]
|
||||
public DateTimeOffset At { get; init; }
|
||||
|
||||
[JsonPropertyName("action")]
|
||||
public string Action { get; init; } = "";
|
||||
|
||||
[JsonPropertyName("configVersion")]
|
||||
public long ConfigVersion { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RemoteNodeConfiguration
|
||||
{
|
||||
[JsonPropertyName("remote")]
|
||||
public RemoteAgentOptions Remote { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("reporting")]
|
||||
public ReportingConfig Reporting { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("audit")]
|
||||
public IReadOnlyList<RemoteConfigurationAuditEntry> Audit { get; init; } = [];
|
||||
|
||||
public RemoteNodeConfiguration NormalizeAndValidate()
|
||||
{
|
||||
var reporting = (Reporting ?? new ReportingConfig()).NormalizeAndValidate();
|
||||
if (Remote is { IsConfigured: true }) Remote.Validate();
|
||||
return this with
|
||||
{
|
||||
Remote = Remote ?? new RemoteAgentOptions(),
|
||||
Reporting = reporting,
|
||||
Audit = (Audit ?? []).TakeLast(1000).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public RemoteNodeConfiguration WithAudit(string action)
|
||||
{
|
||||
RemoteAgentOptions.ValidateIdentifier(action, "action", 120);
|
||||
return this with
|
||||
{
|
||||
Audit = (Audit ?? []).Append(new RemoteConfigurationAuditEntry
|
||||
{
|
||||
At = DateTimeOffset.UtcNow,
|
||||
Action = action,
|
||||
ConfigVersion = Reporting?.ConfigVersion ?? 0
|
||||
}).TakeLast(1000).ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class RemoteNodeConfigurationStore
|
||||
{
|
||||
private static readonly SemaphoreSlim Gate = new(1, 1);
|
||||
|
||||
public static async Task<RemoteNodeConfiguration> LoadAsync(string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var stream = File.OpenRead(path);
|
||||
var configuration = await JsonSerializer.DeserializeAsync<RemoteNodeConfiguration>(stream, RemoteJson.Options, cancellationToken);
|
||||
return (configuration ?? new RemoteNodeConfiguration()).NormalizeAndValidate();
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return new RemoteNodeConfiguration();
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
return new RemoteNodeConfiguration();
|
||||
}
|
||||
catch (JsonException exception)
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Remote node configuration is invalid JSON.", exception);
|
||||
}
|
||||
catch (WxAgentException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SaveAsync(string path, RemoteNodeConfiguration configuration, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalized = configuration.NormalizeAndValidate();
|
||||
var destination = Path.GetFullPath(path);
|
||||
var directory = Path.GetDirectoryName(destination) ?? AppContext.BaseDirectory;
|
||||
Directory.CreateDirectory(directory);
|
||||
await Gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var temporary = destination + ".tmp-" + Guid.NewGuid().ToString("N");
|
||||
try
|
||||
{
|
||||
await using (var stream = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(stream, normalized, RemoteJson.Options, cancellationToken);
|
||||
await stream.FlushAsync(cancellationToken);
|
||||
}
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
try { File.SetUnixFileMode(temporary, UnixFileMode.UserRead | UnixFileMode.UserWrite); }
|
||||
catch (PlatformNotSupportedException) { }
|
||||
}
|
||||
if (OperatingSystem.IsWindows() && File.Exists(destination)) File.Replace(temporary, destination, null);
|
||||
else File.Move(temporary, destination, true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(temporary)) File.Delete(temporary);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Gate.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user