OPC # 0001: Extract OPC into standalone repo

This commit is contained in:
amadzarak
2026-04-25 17:26:42 -04:00
commit 42383bdc03
170 changed files with 21365 additions and 0 deletions
@@ -0,0 +1,108 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using ControlPlane.Core.Services;
namespace ControlPlane.Api.Endpoints;
public static class TenantLogEndpoints
{
public static IEndpointRouteBuilder MapTenantLogEndpoints(this IEndpointRouteBuilder app)
{
app.MapGet("/api/tenants/{subdomain}/logs", StreamTenantLogs).WithTags("Tenants");
return app;
}
private static async Task StreamTenantLogs(
string subdomain,
IConfiguration config,
TenantRegistryService registry,
HttpContext ctx,
CancellationToken cancellationToken)
{
var tenant = registry.GetAll().FirstOrDefault(t => t.Subdomain == subdomain);
if (tenant is null)
{
ctx.Response.StatusCode = 404;
return;
}
var containerName = tenant.ContainerName;
if (string.IsNullOrWhiteSpace(containerName))
{
ctx.Response.StatusCode = 404;
return;
}
ctx.Response.Headers.ContentType = "text/event-stream";
ctx.Response.Headers.CacheControl = "no-cache";
ctx.Response.Headers.Connection = "keep-alive";
var socketUri = config["Docker:Socket"] ?? "npipe://./pipe/docker_engine";
using var docker = new DockerClientConfiguration(new Uri(socketUri)).CreateClient();
var logParams = new ContainerLogsParameters
{
ShowStdout = true,
ShowStderr = true,
Follow = true,
Tail = "200",
Timestamps = true,
};
try
{
using var stream = await docker.Containers.GetContainerLogsAsync(
containerName, tty: false, logParams, cancellationToken);
// MultiplexedStream exposes CopyOutputToAsync which separates stdout/stderr
var stdoutBuf = new System.IO.MemoryStream();
var stderrBuf = new System.IO.MemoryStream();
// Stream with Follow=true won't complete until cancelled — use a pipe instead
var stdoutPipe = new System.IO.Pipelines.Pipe();
var stderrPipe = new System.IO.Pipelines.Pipe();
_ = Task.Run(async () =>
{
try
{
await stream.CopyOutputToAsync(
System.IO.Stream.Null,
stdoutPipe.Writer.AsStream(),
stderrPipe.Writer.AsStream(),
cancellationToken);
}
finally
{
stdoutPipe.Writer.Complete();
stderrPipe.Writer.Complete();
}
}, cancellationToken);
// Merge both pipes into SSE — read stdout line by line
var stdoutReader = new System.IO.StreamReader(stdoutPipe.Reader.AsStream());
var stderrReader = new System.IO.StreamReader(stderrPipe.Reader.AsStream());
var stdoutTask = ReadLinesAsync(stdoutReader, ctx, cancellationToken);
var stderrTask = ReadLinesAsync(stderrReader, ctx, cancellationToken);
await Task.WhenAll(stdoutTask, stderrTask);
}
catch (OperationCanceledException) { /* client disconnected — normal */ }
}
private static async Task ReadLinesAsync(
System.IO.StreamReader reader,
HttpContext ctx,
CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var line = await reader.ReadLineAsync(ct);
if (line is null) break;
if (string.IsNullOrWhiteSpace(line)) continue;
await ctx.Response.WriteAsync($"data: {line}\n\n", ct);
await ctx.Response.Body.FlushAsync(ct);
}
}
}