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
+76
View File
@@ -0,0 +1,76 @@
using ControlPlane.Core.Config;
using ControlPlane.Core.Interfaces;
using ControlPlane.Core.Models;
using ControlPlane.Worker.Services;
using Microsoft.Extensions.Options;
namespace ControlPlane.Worker.Steps;
/// <summary>
/// Final saga step — launches the clarity-server Docker container with the fully
/// enriched SagaContext (connection strings, Keycloak realm, etc. all known).
/// Runs LAST so all env vars are available at container start.
/// </summary>
public class LaunchStep(
ILogger<LaunchStep> logger,
IConfiguration config,
IOptions<ClarityInfraOptions> infraOptions,
ClarityContainerService containers) : ISagaStep
{
public string StepName => "Container Launch";
public async Task ExecuteAsync(SagaContext context, CancellationToken cancellationToken)
{
var job = context.Job;
logger.LogInformation("[{JobId}] Launching container {Env}-app-clarity-{Site}",
job.Id, job.Environment, job.SiteCode);
var containerName = await containers.StartTenantContainerAsync(
environment: job.Environment,
siteCode: job.SiteCode,
subdomain: job.Subdomain,
keycloakRealm: $"clarity-{job.Subdomain.ToLowerInvariant()}",
postgresConnectionString: context.TenantConnectionString,
vaultToken: ReadVaultToken(config),
jobId: job.Id,
cancellationToken: cancellationToken);
context.ContainerName = containerName;
context.TenantApiBaseUrl = infraOptions.Value.TenantPublicUrl(job.Subdomain);
logger.LogInformation("[{JobId}] Container {Name} live at {Url}",
job.Id, containerName, context.TenantApiBaseUrl);
context.Job.CompletedSteps |= CompletedSteps.InfrastructureProvisioned;
}
public async Task CompensateAsync(SagaContext context, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(context.ContainerName)) return;
logger.LogWarning("[{JobId}] Compensating: removing container {Name}", context.Job.Id, context.ContainerName);
await containers.StopAndRemoveAsync(context.ContainerName, cancellationToken);
await containers.RemoveNginxConfigAsync(context.Job.Subdomain, cancellationToken);
}
// Reads the Vault root token from the persisted init.json on the Vault volume.
// Falls back to config["Vault:Token"] then "root" for local dev.
private static string? ReadVaultToken(IConfiguration config)
{
var keysFile = config["Vault:KeysFile"];
if (!string.IsNullOrWhiteSpace(keysFile) && File.Exists(keysFile))
{
try
{
var json = File.ReadAllText(keysFile);
using var doc = System.Text.Json.JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("root_token", out var tok))
return tok.GetString();
}
catch { /* fall through */ }
}
return config["Vault:Token"] ?? "root";
}
}