76 lines
2.9 KiB
C#
76 lines
2.9 KiB
C#
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);
|
|
}
|
|
}
|