Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
148 lines
5.7 KiB
C#
148 lines
5.7 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using StellaOps.AirGap.Policy;
|
|
using StellaOps.TaskRunner.Core.Execution;
|
|
using StellaOps.TaskRunner.Core.Planning;
|
|
using StellaOps.TaskRunner.Core.TaskPacks;
|
|
|
|
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
|
|
|
public sealed class FilesystemPackRunDispatcher : IPackRunJobDispatcher, IPackRunJobScheduler
|
|
{
|
|
private readonly string queuePath;
|
|
private readonly string archivePath;
|
|
private readonly TaskPackManifestLoader manifestLoader = new();
|
|
private readonly TaskPackPlanner planner;
|
|
private readonly JsonSerializerOptions serializerOptions = new(JsonSerializerDefaults.Web);
|
|
|
|
public FilesystemPackRunDispatcher(string queuePath, string archivePath, IEgressPolicy? egressPolicy = null)
|
|
{
|
|
this.queuePath = queuePath ?? throw new ArgumentNullException(nameof(queuePath));
|
|
this.archivePath = archivePath ?? throw new ArgumentNullException(nameof(archivePath));
|
|
planner = new TaskPackPlanner(egressPolicy);
|
|
Directory.CreateDirectory(queuePath);
|
|
Directory.CreateDirectory(archivePath);
|
|
}
|
|
|
|
public string QueuePath => queuePath;
|
|
|
|
public async Task<PackRunExecutionContext?> TryDequeueAsync(CancellationToken cancellationToken)
|
|
{
|
|
var files = Directory.GetFiles(queuePath, "*.json", SearchOption.TopDirectoryOnly)
|
|
.OrderBy(path => path, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
foreach (var file in files)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
try
|
|
{
|
|
var jobJson = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false);
|
|
var job = JsonSerializer.Deserialize<JobEnvelope>(jobJson, serializerOptions);
|
|
if (job is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
TaskPackPlan? plan = job.Plan;
|
|
if (plan is null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(job.ManifestPath))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var manifestPath = ResolvePath(queuePath, job.ManifestPath);
|
|
var inputsPath = job.InputsPath is null ? null : ResolvePath(queuePath, job.InputsPath);
|
|
|
|
var manifest = manifestLoader.Load(manifestPath);
|
|
var inputs = await LoadInputsAsync(inputsPath, cancellationToken).ConfigureAwait(false);
|
|
var planResult = planner.Plan(manifest, inputs);
|
|
if (!planResult.Success || planResult.Plan is null)
|
|
{
|
|
throw new InvalidOperationException($"Failed to plan pack for run {job.RunId}: {string.Join(';', planResult.Errors.Select(e => e.Message))}");
|
|
}
|
|
|
|
plan = planResult.Plan;
|
|
}
|
|
|
|
var archiveFile = Path.Combine(archivePath, Path.GetFileName(file));
|
|
File.Move(file, archiveFile, overwrite: true);
|
|
|
|
var requestedAt = job.RequestedAt ?? DateTimeOffset.UtcNow;
|
|
var runId = string.IsNullOrWhiteSpace(job.RunId) ? Guid.NewGuid().ToString("n") : job.RunId;
|
|
return new PackRunExecutionContext(runId, plan, requestedAt);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var failedPath = file + ".failed";
|
|
File.Move(file, failedPath, overwrite: true);
|
|
Console.Error.WriteLine($"Failed to dequeue job '{file}': {ex.Message}");
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public async Task ScheduleAsync(PackRunExecutionContext context, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
|
|
var envelope = new JobEnvelope(
|
|
context.RunId,
|
|
ManifestPath: null,
|
|
InputsPath: null,
|
|
context.RequestedAt,
|
|
context.Plan);
|
|
|
|
Directory.CreateDirectory(queuePath);
|
|
var safeRunId = string.IsNullOrWhiteSpace(context.RunId) ? Guid.NewGuid().ToString("n") : SanitizeFileName(context.RunId);
|
|
var fileName = $"{safeRunId}-{DateTimeOffset.UtcNow:yyyyMMddHHmmssfff}.json";
|
|
var path = Path.Combine(queuePath, fileName);
|
|
var json = JsonSerializer.Serialize(envelope, serializerOptions);
|
|
await File.WriteAllTextAsync(path, json, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
private static string ResolvePath(string root, string relative)
|
|
=> Path.IsPathRooted(relative) ? relative : Path.Combine(root, relative);
|
|
|
|
private static async Task<IDictionary<string, JsonNode?>> LoadInputsAsync(string? path, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
|
{
|
|
return new Dictionary<string, JsonNode?>(StringComparer.Ordinal);
|
|
}
|
|
|
|
var json = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
|
|
var node = JsonNode.Parse(json) as JsonObject;
|
|
if (node is null)
|
|
{
|
|
return new Dictionary<string, JsonNode?>(StringComparer.Ordinal);
|
|
}
|
|
|
|
return node.ToDictionary(
|
|
pair => pair.Key,
|
|
pair => pair.Value,
|
|
StringComparer.Ordinal);
|
|
}
|
|
|
|
private sealed record JobEnvelope(
|
|
string? RunId,
|
|
string? ManifestPath,
|
|
string? InputsPath,
|
|
DateTimeOffset? RequestedAt,
|
|
TaskPackPlan? Plan);
|
|
|
|
private static string SanitizeFileName(string value)
|
|
{
|
|
var safe = value.Trim();
|
|
foreach (var invalid in Path.GetInvalidFileNameChars())
|
|
{
|
|
safe = safe.Replace(invalid, '_');
|
|
}
|
|
|
|
return safe;
|
|
}
|
|
}
|