OPC # 0001: Extract OPC into standalone repo
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
using ControlPlane.Api.Services;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ControlPlane.Api.Endpoints;
|
||||
|
||||
public static class ImageBuildEndpoints
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public static IEndpointRouteBuilder MapImageBuildEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/api/image").WithTags("Image");
|
||||
|
||||
group.MapGet("/status", GetStatus);
|
||||
group.MapPost("/build", TriggerBuild);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>Returns the last known build status without triggering a new build.</summary>
|
||||
private static async Task<IResult> GetStatus(ImageBuildService svc) =>
|
||||
Results.Ok(await svc.GetStatusAsync());
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a docker build and streams the output line-by-line as SSE.
|
||||
/// The build context is the repo root, which must be configured via
|
||||
/// Docker:RepoRoot in appsettings / environment.
|
||||
/// </summary>
|
||||
private static async Task TriggerBuild(
|
||||
HttpContext ctx,
|
||||
ImageBuildService svc,
|
||||
IConfiguration config,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var repoRoot = config["Docker:RepoRoot"];
|
||||
if (string.IsNullOrWhiteSpace(repoRoot) || !Directory.Exists(repoRoot))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
error = "Docker:RepoRoot is not configured or does not exist.",
|
||||
hint = "Add Docker__RepoRoot to the worker environment pointing at the repo root directory.",
|
||||
}, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Response.Headers.ContentType = "text/event-stream";
|
||||
ctx.Response.Headers.CacheControl = "no-cache";
|
||||
ctx.Response.Headers.Connection = "keep-alive";
|
||||
|
||||
// Use a Channel so the Progress<T> callback (sync) can safely hand lines
|
||||
// to the async SSE writer without blocking the Docker build thread.
|
||||
var channel = System.Threading.Channels.Channel.CreateUnbounded<string?>(
|
||||
new System.Threading.Channels.UnboundedChannelOptions { SingleWriter = true, SingleReader = true });
|
||||
|
||||
void OnLine(string line) => channel.Writer.TryWrite(line);
|
||||
|
||||
// Run the build on a background thread so we can drain the channel here
|
||||
var buildTask = Task.Run(() => svc.BuildAsync(repoRoot, OnLine, ct), ct)
|
||||
.ContinueWith(_ => channel.Writer.TryComplete(), TaskScheduler.Default);
|
||||
|
||||
await foreach (var line in channel.Reader.ReadAllAsync(ct))
|
||||
{
|
||||
var json = JsonSerializer.Serialize(new { line }, JsonOpts);
|
||||
await ctx.Response.WriteAsync($"data: {json}\n\n", ct);
|
||||
await ctx.Response.Body.FlushAsync(ct);
|
||||
}
|
||||
|
||||
await buildTask; // ensure build is fully done
|
||||
|
||||
// Signal stream end
|
||||
await ctx.Response.WriteAsync("data: {\"done\":true}\n\n", ct);
|
||||
await ctx.Response.Body.FlushAsync(ct);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user