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
201 lines
6.1 KiB
C#
201 lines
6.1 KiB
C#
using System.Text.Json;
|
|
using StellaOps.TaskRunner.Core.Execution;
|
|
using StellaOps.TaskRunner.Core.Planning;
|
|
|
|
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
|
|
|
/// <summary>
|
|
/// File-system backed implementation of <see cref="IPackRunStateStore"/> intended for development and air-gapped smoke tests.
|
|
/// </summary>
|
|
public sealed class FilePackRunStateStore : IPackRunStateStore
|
|
{
|
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
|
{
|
|
WriteIndented = true
|
|
};
|
|
|
|
private readonly string rootPath;
|
|
private readonly SemaphoreSlim mutex = new(1, 1);
|
|
|
|
public FilePackRunStateStore(string rootPath)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
|
|
|
|
this.rootPath = Path.GetFullPath(rootPath);
|
|
Directory.CreateDirectory(this.rootPath);
|
|
}
|
|
|
|
public async Task<PackRunState?> GetAsync(string runId, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
|
|
|
|
var path = GetPath(runId);
|
|
if (!File.Exists(path))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
await using var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
var document = await JsonSerializer.DeserializeAsync<StateDocument>(stream, SerializerOptions, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
return document?.ToDomain();
|
|
}
|
|
|
|
public async Task SaveAsync(PackRunState state, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(state);
|
|
|
|
var path = GetPath(state.RunId);
|
|
var document = StateDocument.FromDomain(state);
|
|
|
|
Directory.CreateDirectory(rootPath);
|
|
|
|
await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
await using var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None);
|
|
await JsonSerializer.SerializeAsync(stream, document, SerializerOptions, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
mutex.Release();
|
|
}
|
|
}
|
|
|
|
public async Task<IReadOnlyList<PackRunState>> ListAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!Directory.Exists(rootPath))
|
|
{
|
|
return Array.Empty<PackRunState>();
|
|
}
|
|
|
|
var states = new List<PackRunState>();
|
|
|
|
var files = Directory.EnumerateFiles(rootPath, "*.json", SearchOption.TopDirectoryOnly)
|
|
.OrderBy(file => file, StringComparer.Ordinal);
|
|
|
|
foreach (var file in files)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
await using var stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
var document = await JsonSerializer.DeserializeAsync<StateDocument>(stream, SerializerOptions, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (document is not null)
|
|
{
|
|
states.Add(document.ToDomain());
|
|
}
|
|
}
|
|
|
|
return states;
|
|
}
|
|
|
|
private string GetPath(string runId)
|
|
{
|
|
var safeName = SanitizeFileName(runId);
|
|
return Path.Combine(rootPath, $"{safeName}.json");
|
|
}
|
|
|
|
private static string SanitizeFileName(string value)
|
|
{
|
|
var result = value.Trim();
|
|
foreach (var invalid in Path.GetInvalidFileNameChars())
|
|
{
|
|
result = result.Replace(invalid, '_');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private sealed record StateDocument(
|
|
string RunId,
|
|
string PlanHash,
|
|
TaskPackPlan Plan,
|
|
TaskPackPlanFailurePolicy FailurePolicy,
|
|
DateTimeOffset RequestedAt,
|
|
DateTimeOffset CreatedAt,
|
|
DateTimeOffset UpdatedAt,
|
|
IReadOnlyList<StepDocument> Steps,
|
|
string? TenantId)
|
|
{
|
|
public static StateDocument FromDomain(PackRunState state)
|
|
{
|
|
var steps = state.Steps.Values
|
|
.OrderBy(step => step.StepId, StringComparer.Ordinal)
|
|
.Select(step => new StepDocument(
|
|
step.StepId,
|
|
step.Kind,
|
|
step.Enabled,
|
|
step.ContinueOnError,
|
|
step.MaxParallel,
|
|
step.ApprovalId,
|
|
step.GateMessage,
|
|
step.Status,
|
|
step.Attempts,
|
|
step.LastTransitionAt,
|
|
step.NextAttemptAt,
|
|
step.StatusReason))
|
|
.ToList();
|
|
|
|
return new StateDocument(
|
|
state.RunId,
|
|
state.PlanHash,
|
|
state.Plan,
|
|
state.FailurePolicy,
|
|
state.RequestedAt,
|
|
state.CreatedAt,
|
|
state.UpdatedAt,
|
|
steps,
|
|
state.TenantId);
|
|
}
|
|
|
|
public PackRunState ToDomain()
|
|
{
|
|
var steps = Steps.ToDictionary(
|
|
step => step.StepId,
|
|
step => new PackRunStepStateRecord(
|
|
step.StepId,
|
|
step.Kind,
|
|
step.Enabled,
|
|
step.ContinueOnError,
|
|
step.MaxParallel,
|
|
step.ApprovalId,
|
|
step.GateMessage,
|
|
step.Status,
|
|
step.Attempts,
|
|
step.LastTransitionAt,
|
|
step.NextAttemptAt,
|
|
step.StatusReason),
|
|
StringComparer.Ordinal);
|
|
|
|
return new PackRunState(
|
|
RunId,
|
|
PlanHash,
|
|
Plan,
|
|
FailurePolicy,
|
|
RequestedAt,
|
|
CreatedAt,
|
|
UpdatedAt,
|
|
steps,
|
|
TenantId);
|
|
}
|
|
}
|
|
|
|
private sealed record StepDocument(
|
|
string StepId,
|
|
PackRunStepKind Kind,
|
|
bool Enabled,
|
|
bool ContinueOnError,
|
|
int? MaxParallel,
|
|
string? ApprovalId,
|
|
string? GateMessage,
|
|
PackRunStepExecutionStatus Status,
|
|
int Attempts,
|
|
DateTimeOffset? LastTransitionAt,
|
|
DateTimeOffset? NextAttemptAt,
|
|
string? StatusReason);
|
|
}
|