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;
}
/// Returns the last known build status without triggering a new build.
private static async Task GetStatus(ImageBuildService svc) =>
Results.Ok(await svc.GetStatusAsync());
///
/// 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.
///
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 callback (sync) can safely hand lines
// to the async SSE writer without blocking the Docker build thread.
var channel = System.Threading.Channels.Channel.CreateUnbounded(
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);
}
}