feat: Implement Policy Engine Evaluation Service and Cache with unit tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Temp commit to debug
This commit is contained in:
@@ -1,242 +1,242 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
using StellaOps.TaskRunner.Core.Execution.Simulation;
|
||||
using StellaOps.TaskRunner.Core.Planning;
|
||||
using StellaOps.TaskRunner.Core.TaskPacks;
|
||||
using StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
using StellaOps.TaskRunner.WebService;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.Configure<TaskRunnerServiceOptions>(builder.Configuration.GetSection("TaskRunner"));
|
||||
builder.Services.AddSingleton<TaskPackManifestLoader>();
|
||||
builder.Services.AddSingleton<TaskPackPlanner>();
|
||||
builder.Services.AddSingleton<PackRunSimulationEngine>();
|
||||
builder.Services.AddSingleton<PackRunExecutionGraphBuilder>();
|
||||
builder.Services.AddSingleton<IPackRunStateStore>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<TaskRunnerServiceOptions>>().Value;
|
||||
return new FilePackRunStateStore(options.RunStatePath);
|
||||
});
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.MapPost("/v1/task-runner/simulations", async (
|
||||
[FromBody] SimulationRequest request,
|
||||
TaskPackManifestLoader loader,
|
||||
TaskPackPlanner planner,
|
||||
PackRunSimulationEngine simulationEngine,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Manifest))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Manifest is required." });
|
||||
}
|
||||
|
||||
TaskPackManifest manifest;
|
||||
try
|
||||
{
|
||||
manifest = loader.Deserialize(request.Manifest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = "Invalid manifest", detail = ex.Message });
|
||||
}
|
||||
|
||||
var inputs = ConvertInputs(request.Inputs);
|
||||
var planResult = planner.Plan(manifest, inputs);
|
||||
if (!planResult.Success || planResult.Plan is null)
|
||||
{
|
||||
return Results.BadRequest(new
|
||||
{
|
||||
errors = planResult.Errors.Select(error => new { error.Path, error.Message })
|
||||
});
|
||||
}
|
||||
|
||||
var plan = planResult.Plan;
|
||||
var simulation = simulationEngine.Simulate(plan);
|
||||
var response = SimulationMapper.ToResponse(plan, simulation);
|
||||
return Results.Ok(response);
|
||||
}).WithName("SimulateTaskPack");
|
||||
|
||||
app.MapGet("/v1/task-runner/runs/{runId}", async (
|
||||
string runId,
|
||||
IPackRunStateStore stateStore,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(runId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "runId is required." });
|
||||
}
|
||||
|
||||
var state = await stateStore.GetAsync(runId, cancellationToken).ConfigureAwait(false);
|
||||
if (state is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(RunStateMapper.ToResponse(state));
|
||||
}).WithName("GetRunState");
|
||||
|
||||
app.MapGet("/", () => Results.Redirect("/openapi"));
|
||||
|
||||
app.Run();
|
||||
|
||||
static IDictionary<string, JsonNode?>? ConvertInputs(JsonObject? node)
|
||||
{
|
||||
if (node is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var dictionary = new Dictionary<string, JsonNode?>(StringComparer.Ordinal);
|
||||
foreach (var property in node)
|
||||
{
|
||||
dictionary[property.Key] = property.Value?.DeepClone();
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
internal sealed record SimulationRequest(string Manifest, JsonObject? Inputs);
|
||||
|
||||
internal sealed record SimulationResponse(
|
||||
string PlanHash,
|
||||
FailurePolicyResponse FailurePolicy,
|
||||
IReadOnlyList<SimulationStepResponse> Steps,
|
||||
IReadOnlyList<SimulationOutputResponse> Outputs,
|
||||
bool HasPendingApprovals);
|
||||
|
||||
internal sealed record SimulationStepResponse(
|
||||
string Id,
|
||||
string TemplateId,
|
||||
string Kind,
|
||||
bool Enabled,
|
||||
string Status,
|
||||
string? StatusReason,
|
||||
string? Uses,
|
||||
string? ApprovalId,
|
||||
string? GateMessage,
|
||||
int? MaxParallel,
|
||||
bool ContinueOnError,
|
||||
IReadOnlyList<SimulationStepResponse> Children);
|
||||
|
||||
internal sealed record SimulationOutputResponse(
|
||||
string Name,
|
||||
string Type,
|
||||
bool RequiresRuntimeValue,
|
||||
string? PathExpression,
|
||||
string? ValueExpression);
|
||||
|
||||
internal sealed record FailurePolicyResponse(int MaxAttempts, int BackoffSeconds, bool ContinueOnError);
|
||||
|
||||
internal sealed record RunStateResponse(
|
||||
string RunId,
|
||||
string PlanHash,
|
||||
FailurePolicyResponse FailurePolicy,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt,
|
||||
IReadOnlyList<RunStateStepResponse> Steps);
|
||||
|
||||
internal sealed record RunStateStepResponse(
|
||||
string StepId,
|
||||
string Kind,
|
||||
bool Enabled,
|
||||
bool ContinueOnError,
|
||||
int? MaxParallel,
|
||||
string? ApprovalId,
|
||||
string? GateMessage,
|
||||
string Status,
|
||||
int Attempts,
|
||||
DateTimeOffset? LastTransitionAt,
|
||||
DateTimeOffset? NextAttemptAt,
|
||||
string? StatusReason);
|
||||
|
||||
internal static class SimulationMapper
|
||||
{
|
||||
public static SimulationResponse ToResponse(TaskPackPlan plan, PackRunSimulationResult result)
|
||||
{
|
||||
var failurePolicy = result.FailurePolicy ?? PackRunExecutionGraph.DefaultFailurePolicy;
|
||||
var steps = result.Steps.Select(MapStep).ToList();
|
||||
var outputs = result.Outputs.Select(MapOutput).ToList();
|
||||
|
||||
return new SimulationResponse(
|
||||
plan.Hash,
|
||||
new FailurePolicyResponse(failurePolicy.MaxAttempts, failurePolicy.BackoffSeconds, failurePolicy.ContinueOnError),
|
||||
steps,
|
||||
outputs,
|
||||
result.HasPendingApprovals);
|
||||
}
|
||||
|
||||
private static SimulationStepResponse MapStep(PackRunSimulationNode node)
|
||||
{
|
||||
var children = node.Children.Select(MapStep).ToList();
|
||||
return new SimulationStepResponse(
|
||||
node.Id,
|
||||
node.TemplateId,
|
||||
node.Kind.ToString(),
|
||||
node.Enabled,
|
||||
node.Status.ToString(),
|
||||
node.Status.ToString() switch
|
||||
{
|
||||
nameof(PackRunSimulationStatus.RequiresApproval) => "requires-approval",
|
||||
nameof(PackRunSimulationStatus.RequiresPolicy) => "requires-policy",
|
||||
nameof(PackRunSimulationStatus.Skipped) => "condition-false",
|
||||
_ => null
|
||||
},
|
||||
node.Uses,
|
||||
node.ApprovalId,
|
||||
node.GateMessage,
|
||||
node.MaxParallel,
|
||||
node.ContinueOnError,
|
||||
children);
|
||||
}
|
||||
|
||||
private static SimulationOutputResponse MapOutput(PackRunSimulationOutput output)
|
||||
=> new(
|
||||
output.Name,
|
||||
output.Type,
|
||||
output.RequiresRuntimeValue,
|
||||
output.Path?.Expression,
|
||||
output.Expression?.Expression);
|
||||
}
|
||||
|
||||
internal static class RunStateMapper
|
||||
{
|
||||
public static RunStateResponse ToResponse(PackRunState state)
|
||||
{
|
||||
var failurePolicy = state.FailurePolicy ?? PackRunExecutionGraph.DefaultFailurePolicy;
|
||||
var steps = state.Steps.Values
|
||||
.OrderBy(step => step.StepId, StringComparer.Ordinal)
|
||||
.Select(step => new RunStateStepResponse(
|
||||
step.StepId,
|
||||
step.Kind.ToString(),
|
||||
step.Enabled,
|
||||
step.ContinueOnError,
|
||||
step.MaxParallel,
|
||||
step.ApprovalId,
|
||||
step.GateMessage,
|
||||
step.Status.ToString(),
|
||||
step.Attempts,
|
||||
step.LastTransitionAt,
|
||||
step.NextAttemptAt,
|
||||
step.StatusReason))
|
||||
.ToList();
|
||||
|
||||
return new RunStateResponse(
|
||||
state.RunId,
|
||||
state.PlanHash,
|
||||
new FailurePolicyResponse(failurePolicy.MaxAttempts, failurePolicy.BackoffSeconds, failurePolicy.ContinueOnError),
|
||||
state.CreatedAt,
|
||||
state.UpdatedAt,
|
||||
steps);
|
||||
}
|
||||
}
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
using StellaOps.TaskRunner.Core.Execution.Simulation;
|
||||
using StellaOps.TaskRunner.Core.Planning;
|
||||
using StellaOps.TaskRunner.Core.TaskPacks;
|
||||
using StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
using StellaOps.TaskRunner.WebService;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.Configure<TaskRunnerServiceOptions>(builder.Configuration.GetSection("TaskRunner"));
|
||||
builder.Services.AddSingleton<TaskPackManifestLoader>();
|
||||
builder.Services.AddSingleton<TaskPackPlanner>();
|
||||
builder.Services.AddSingleton<PackRunSimulationEngine>();
|
||||
builder.Services.AddSingleton<PackRunExecutionGraphBuilder>();
|
||||
builder.Services.AddSingleton<IPackRunStateStore>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<TaskRunnerServiceOptions>>().Value;
|
||||
return new FilePackRunStateStore(options.RunStatePath);
|
||||
});
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.MapPost("/v1/task-runner/simulations", async (
|
||||
[FromBody] SimulationRequest request,
|
||||
TaskPackManifestLoader loader,
|
||||
TaskPackPlanner planner,
|
||||
PackRunSimulationEngine simulationEngine,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Manifest))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Manifest is required." });
|
||||
}
|
||||
|
||||
TaskPackManifest manifest;
|
||||
try
|
||||
{
|
||||
manifest = loader.Deserialize(request.Manifest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = "Invalid manifest", detail = ex.Message });
|
||||
}
|
||||
|
||||
var inputs = ConvertInputs(request.Inputs);
|
||||
var planResult = planner.Plan(manifest, inputs);
|
||||
if (!planResult.Success || planResult.Plan is null)
|
||||
{
|
||||
return Results.BadRequest(new
|
||||
{
|
||||
errors = planResult.Errors.Select(error => new { error.Path, error.Message })
|
||||
});
|
||||
}
|
||||
|
||||
var plan = planResult.Plan;
|
||||
var simulation = simulationEngine.Simulate(plan);
|
||||
var response = SimulationMapper.ToResponse(plan, simulation);
|
||||
return Results.Ok(response);
|
||||
}).WithName("SimulateTaskPack");
|
||||
|
||||
app.MapGet("/v1/task-runner/runs/{runId}", async (
|
||||
string runId,
|
||||
IPackRunStateStore stateStore,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(runId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "runId is required." });
|
||||
}
|
||||
|
||||
var state = await stateStore.GetAsync(runId, cancellationToken).ConfigureAwait(false);
|
||||
if (state is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(RunStateMapper.ToResponse(state));
|
||||
}).WithName("GetRunState");
|
||||
|
||||
app.MapGet("/", () => Results.Redirect("/openapi"));
|
||||
|
||||
app.Run();
|
||||
|
||||
static IDictionary<string, JsonNode?>? ConvertInputs(JsonObject? node)
|
||||
{
|
||||
if (node is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var dictionary = new Dictionary<string, JsonNode?>(StringComparer.Ordinal);
|
||||
foreach (var property in node)
|
||||
{
|
||||
dictionary[property.Key] = property.Value?.DeepClone();
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
internal sealed record SimulationRequest(string Manifest, JsonObject? Inputs);
|
||||
|
||||
internal sealed record SimulationResponse(
|
||||
string PlanHash,
|
||||
FailurePolicyResponse FailurePolicy,
|
||||
IReadOnlyList<SimulationStepResponse> Steps,
|
||||
IReadOnlyList<SimulationOutputResponse> Outputs,
|
||||
bool HasPendingApprovals);
|
||||
|
||||
internal sealed record SimulationStepResponse(
|
||||
string Id,
|
||||
string TemplateId,
|
||||
string Kind,
|
||||
bool Enabled,
|
||||
string Status,
|
||||
string? StatusReason,
|
||||
string? Uses,
|
||||
string? ApprovalId,
|
||||
string? GateMessage,
|
||||
int? MaxParallel,
|
||||
bool ContinueOnError,
|
||||
IReadOnlyList<SimulationStepResponse> Children);
|
||||
|
||||
internal sealed record SimulationOutputResponse(
|
||||
string Name,
|
||||
string Type,
|
||||
bool RequiresRuntimeValue,
|
||||
string? PathExpression,
|
||||
string? ValueExpression);
|
||||
|
||||
internal sealed record FailurePolicyResponse(int MaxAttempts, int BackoffSeconds, bool ContinueOnError);
|
||||
|
||||
internal sealed record RunStateResponse(
|
||||
string RunId,
|
||||
string PlanHash,
|
||||
FailurePolicyResponse FailurePolicy,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt,
|
||||
IReadOnlyList<RunStateStepResponse> Steps);
|
||||
|
||||
internal sealed record RunStateStepResponse(
|
||||
string StepId,
|
||||
string Kind,
|
||||
bool Enabled,
|
||||
bool ContinueOnError,
|
||||
int? MaxParallel,
|
||||
string? ApprovalId,
|
||||
string? GateMessage,
|
||||
string Status,
|
||||
int Attempts,
|
||||
DateTimeOffset? LastTransitionAt,
|
||||
DateTimeOffset? NextAttemptAt,
|
||||
string? StatusReason);
|
||||
|
||||
internal static class SimulationMapper
|
||||
{
|
||||
public static SimulationResponse ToResponse(TaskPackPlan plan, PackRunSimulationResult result)
|
||||
{
|
||||
var failurePolicy = result.FailurePolicy ?? PackRunExecutionGraph.DefaultFailurePolicy;
|
||||
var steps = result.Steps.Select(MapStep).ToList();
|
||||
var outputs = result.Outputs.Select(MapOutput).ToList();
|
||||
|
||||
return new SimulationResponse(
|
||||
plan.Hash,
|
||||
new FailurePolicyResponse(failurePolicy.MaxAttempts, failurePolicy.BackoffSeconds, failurePolicy.ContinueOnError),
|
||||
steps,
|
||||
outputs,
|
||||
result.HasPendingApprovals);
|
||||
}
|
||||
|
||||
private static SimulationStepResponse MapStep(PackRunSimulationNode node)
|
||||
{
|
||||
var children = node.Children.Select(MapStep).ToList();
|
||||
return new SimulationStepResponse(
|
||||
node.Id,
|
||||
node.TemplateId,
|
||||
node.Kind.ToString(),
|
||||
node.Enabled,
|
||||
node.Status.ToString(),
|
||||
node.Status.ToString() switch
|
||||
{
|
||||
nameof(PackRunSimulationStatus.RequiresApproval) => "requires-approval",
|
||||
nameof(PackRunSimulationStatus.RequiresPolicy) => "requires-policy",
|
||||
nameof(PackRunSimulationStatus.Skipped) => "condition-false",
|
||||
_ => null
|
||||
},
|
||||
node.Uses,
|
||||
node.ApprovalId,
|
||||
node.GateMessage,
|
||||
node.MaxParallel,
|
||||
node.ContinueOnError,
|
||||
children);
|
||||
}
|
||||
|
||||
private static SimulationOutputResponse MapOutput(PackRunSimulationOutput output)
|
||||
=> new(
|
||||
output.Name,
|
||||
output.Type,
|
||||
output.RequiresRuntimeValue,
|
||||
output.Path?.Expression,
|
||||
output.Expression?.Expression);
|
||||
}
|
||||
|
||||
internal static class RunStateMapper
|
||||
{
|
||||
public static RunStateResponse ToResponse(PackRunState state)
|
||||
{
|
||||
var failurePolicy = state.FailurePolicy ?? PackRunExecutionGraph.DefaultFailurePolicy;
|
||||
var steps = state.Steps.Values
|
||||
.OrderBy(step => step.StepId, StringComparer.Ordinal)
|
||||
.Select(step => new RunStateStepResponse(
|
||||
step.StepId,
|
||||
step.Kind.ToString(),
|
||||
step.Enabled,
|
||||
step.ContinueOnError,
|
||||
step.MaxParallel,
|
||||
step.ApprovalId,
|
||||
step.GateMessage,
|
||||
step.Status.ToString(),
|
||||
step.Attempts,
|
||||
step.LastTransitionAt,
|
||||
step.NextAttemptAt,
|
||||
step.StatusReason))
|
||||
.ToList();
|
||||
|
||||
return new RunStateResponse(
|
||||
state.RunId,
|
||||
state.PlanHash,
|
||||
new FailurePolicyResponse(failurePolicy.MaxAttempts, failurePolicy.BackoffSeconds, failurePolicy.ContinueOnError),
|
||||
state.CreatedAt,
|
||||
state.UpdatedAt,
|
||||
steps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace StellaOps.TaskRunner.WebService;
|
||||
|
||||
public sealed class TaskRunnerServiceOptions
|
||||
{
|
||||
public string RunStatePath { get; set; } = Path.Combine(AppContext.BaseDirectory, "state", "runs");
|
||||
}
|
||||
namespace StellaOps.TaskRunner.WebService;
|
||||
|
||||
public sealed class TaskRunnerServiceOptions
|
||||
{
|
||||
public string RunStatePath { get; set; } = Path.Combine(AppContext.BaseDirectory, "state", "runs");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user