using Scalar.Aspire; var builder = DistributedApplication.CreateBuilder(args); // ───────────────────────────────────────────────────────────────────────────── // Platform infrastructure (Keycloak, Vault, MinIO, Nginx, Dnsmasq) is // managed by infra/docker-compose.yml — NOT Aspire. // Run `docker compose up -d` from the infra/ folder before starting this host. // // Fixed dev URLs (hardcoded to match infra/docker-compose.yml): // Keycloak → http://localhost:8080 // Vault → http://localhost:8200 // MinIO → http://localhost:9000 // // ControlPlane owns: opc-postgres (opcdb + giteadb), RabbitMQ, Gitea. // ───────────────────────────────────────────────────────────────────────────── // Shared paths var clientAssetsPath = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", "ClientAssets")); var nginxConfDPath = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", "infra", "nginx", "conf.d")); var vaultKeysFile = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", "infra", "vault", "data", "init.json")); #region CONTROLPLANE POSTGRES // ControlPlane owns this — isolated from platform infra postgres. // Override via: dotnet user-secrets set "Parameters:cp-postgres-password" "yourpassword" var cpPostgresPassword = builder.AddParameter("cp-postgres-password", "controlplane-dev", secret: true); var cpPostgres = builder.AddPostgres("opc-postgres", password: cpPostgresPassword) .WithLifetime(ContainerLifetime.Persistent) .WithDataVolume("opc-postgres-data") .WithHostPort(5433) .WithPgAdmin(); var controlPlaneDb = cpPostgres.AddDatabase("opcdb"); #endregion #region RABBITMQ var rabbitPassword = builder.AddParameter("rabbitmq-password", "clarity-rabbit", secret: true); var rabbit = builder.AddRabbitMQ("rabbitmq", password: rabbitPassword) .WithLifetime(ContainerLifetime.Persistent) .WithManagementPlugin(); #endregion #region CONTROLPLANE API var api = builder.AddProject("controlplane-api") .WithReference(rabbit) .WaitFor(rabbit) .WithReference(controlPlaneDb) .WaitFor(controlPlaneDb) .WithEnvironment("ClientAssets__Folder", clientAssetsPath) .WithEnvironment("Docker__RepoRoot", Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", ".."))) // ClarityStack/ root — needed for Directory.*.props .WithExternalHttpEndpoints(); #endregion #region PROVISIONING WORKER builder.AddProject("controlplane-worker") .WithReference(rabbit) .WaitFor(rabbit) // Vault — fixed dev address from infra/docker-compose.yml .WithEnvironment("Vault__Address", "http://localhost:8200") .WithEnvironment("Vault__ContainerAddress", "http://vault:8200") .WithEnvironment("Vault__KeysFile", vaultKeysFile) // Keycloak — fixed dev address from infra/docker-compose.yml .WithEnvironment("Keycloak__AuthServerUrl", "http://localhost:8080") .WithEnvironment("Keycloak__ContainerUrl", "https://keycloak.clarity.test") .WithEnvironment("Keycloak__Realm", "master") .WithEnvironment("Keycloak__Resource", "admin-cli") .WithEnvironment("Keycloak__AdminUser", "admin") .WithEnvironment("Keycloak__AdminPassword", "Admin1234!") // Gateway .WithEnvironment("Gateway__TenantBaseUrl", "https://{subdomain}.clarity.test") // ClarityInfraOptions .WithEnvironment("Clarity__Domain", "clarity.test") .WithEnvironment("Clarity__Network", "clarity-net") .WithEnvironment("Clarity__KeycloakPublicUrl", "https://keycloak.clarity.test") .WithEnvironment("Clarity__KeycloakInternalUrl", "http://keycloak:8080") .WithEnvironment("Clarity__VaultInternalUrl", "http://vault:8200") .WithEnvironment("Clarity__NginxCertPath", "/etc/nginx/certs/clarity.test.crt") .WithEnvironment("Clarity__NginxCertKeyPath", "/etc/nginx/certs/clarity.test.key") // Nginx conf.d — points to infra/nginx/conf.d so platform nginx picks up tenant configs .WithEnvironment("Nginx__ConfDPath", nginxConfDPath) .WithEnvironment("ClientAssets__Folder", clientAssetsPath) // Platform Postgres connection string for tenant database provisioning (infra/docker-compose.yml) .WithEnvironment("ConnectionStrings__platformdb", "Host=localhost;Port=5432;Username=postgres;Password=postgres") .WithReference(controlPlaneDb) .WaitFor(controlPlaneDb); #endregion #region CONTROLPLANE UI builder.AddViteApp("controlplane-ui", "../clarity.controlplane") .WithReference((IResourceBuilder)api) .WaitFor(api); #endregion #region CLARITY-NET — connect RabbitMQ to platform network // Ensures RabbitMQ (the one container Aspire owns) is reachable from tenant containers // on clarity-net. All other platform containers are already on clarity-net via docker-compose. builder.Eventing.Subscribe(async (@event, ct) => { const string network = "clarity-net"; await Task.Delay(TimeSpan.FromSeconds(4), ct); var (inspectCode, _) = await DockerOutputAsync($"network inspect {network}", ct); if (inspectCode != 0) await DockerOutputAsync($"network create {network}", ct); var (idCode, idOut) = await DockerOutputAsync("ps --filter name=rabbitmq --format {{.ID}}", ct); if (idCode == 0 && !string.IsNullOrWhiteSpace(idOut)) { var containerId = idOut.Trim().Split('\n')[0].Trim(); await DockerOutputAsync($"network connect --alias rabbitmq {network} {containerId}", ct); } }); #endregion #region SCALAR API DOCS var scalar = builder.AddScalarApiReference(); scalar.WithApiReference(api); #endregion builder.Build().Run(); static async Task<(int ExitCode, string Output)> DockerOutputAsync(string args, CancellationToken ct) { var psi = new System.Diagnostics.ProcessStartInfo("docker", args) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }; using var proc = System.Diagnostics.Process.Start(psi)!; var output = await proc.StandardOutput.ReadToEndAsync(ct); await proc.WaitForExitAsync(ct); return (proc.ExitCode, output); }