using ControlPlane.Core.Config; using ControlPlane.Core.Interfaces; using ControlPlane.Core.Models; using ControlPlane.Worker.Services; using Microsoft.Extensions.Options; namespace ControlPlane.Worker.Steps; /// /// 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. /// public class LaunchStep( ILogger logger, IConfiguration config, IOptions 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: context.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"; } }