Files
OPC/ControlPlane.AppHost/AppHost.cs
T
2026-04-26 16:12:00 -04:00

144 lines
6.8 KiB
C#

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"));
// Build working directory for BuildConsumer (clone/pull repos here when running builds)
var buildWorkDir = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", "..", "..", "clarity-builds"));
#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<Projects.ControlPlane_Api>("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
.WithEnvironment("Build__WorkDir", buildWorkDir)
.WithExternalHttpEndpoints();
#endregion
#region PROVISIONING WORKER
builder.AddProject<Projects.ControlPlane_Worker>("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")
.WithEnvironment("Build__WorkDir", buildWorkDir)
.WithReference(controlPlaneDb)
.WaitFor(controlPlaneDb);
#endregion
#region CONTROLPLANE UI
builder.AddViteApp("controlplane-ui", "../clarity.controlplane")
.WithReference((IResourceBuilder<IResourceWithServiceDiscovery>)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<AfterResourcesCreatedEvent>(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);
}