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); } }