79c69e1363
Co-authored-by: Copilot <copilot@github.com>
173 lines
7.3 KiB
C#
173 lines
7.3 KiB
C#
using ControlPlane.Api.Consumers;
|
|
using ControlPlane.Api.Endpoints;
|
|
using ControlPlane.Api.Services;
|
|
using ControlPlane.Core.Models;
|
|
using ControlPlane.Core.Services;
|
|
using MassTransit;
|
|
using Npgsql;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.AddServiceDefaults();
|
|
builder.Services.AddOpenApi();
|
|
builder.Services.ConfigureHttpJsonOptions(o =>
|
|
o.SerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()));
|
|
|
|
// In-memory job store - swap for EF Core post-MVP
|
|
builder.Services.AddSingleton<Dictionary<Guid, ProvisioningJob>>();
|
|
|
|
// Tenant registry - reads ClientAssets/{subdomain}.xml files
|
|
builder.Services.AddSingleton<TenantRegistryService>();
|
|
|
|
// SSE event bus - ProgressConsumer writes here, SSE endpoint reads
|
|
builder.Services.AddSingleton<SseEventBus>();
|
|
|
|
// Build + release pipeline services
|
|
builder.Services.AddSingleton<BuildHistoryService>();
|
|
builder.Services.AddSingleton<ImageBuildService>();
|
|
builder.Services.AddSingleton<ReleaseService>();
|
|
builder.Services.AddSingleton<ProjectBuildService>();
|
|
builder.Services.AddSingleton<PromotionService>();
|
|
|
|
// OPC persistence (raw Npgsql)
|
|
var opcConnStr = builder.Configuration.GetConnectionString("opcdb");
|
|
if (!string.IsNullOrWhiteSpace(opcConnStr))
|
|
// Replace 'localhost' with '127.0.0.1' to avoid Npgsql trying [::1] first on Windows
|
|
builder.Services.AddSingleton(NpgsqlDataSource.Create(opcConnStr.Replace("localhost", "127.0.0.1")));
|
|
else
|
|
builder.Services.AddSingleton(NpgsqlDataSource.Create("Host=127.0.0.1;Port=5433;Database=opcdb;Username=postgres;Password=controlplane-dev"));
|
|
builder.Services.AddScoped<OpcService>();
|
|
|
|
// Named HttpClient for OpenRouter AI assist proxy
|
|
builder.Services.AddHttpClient("openrouter");
|
|
|
|
// Gitea integration
|
|
builder.Services.AddHttpClient("gitea").ConfigurePrimaryHttpMessageHandler(() =>
|
|
new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator });
|
|
builder.Services.AddScoped<GiteaService>();
|
|
|
|
builder.Services.AddMassTransit(x =>
|
|
{
|
|
x.SetKebabCaseEndpointNameFormatter();
|
|
|
|
// Receives ProvisioningProgressEvent from Worker and pushes to SSE
|
|
x.AddConsumer<ProvisioningProgressConsumer>();
|
|
|
|
x.UsingRabbitMq((ctx, cfg) =>
|
|
{
|
|
var connStr = builder.Configuration.GetConnectionString("rabbitmq");
|
|
if (!string.IsNullOrWhiteSpace(connStr))
|
|
cfg.Host(new Uri(connStr));
|
|
cfg.ConfigureEndpoints(ctx);
|
|
});
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
app.MapDefaultEndpoints();
|
|
|
|
if (app.Environment.IsDevelopment())
|
|
app.MapOpenApi();
|
|
|
|
app.MapProvisioningEndpoints();
|
|
app.MapTenantLogEndpoints();
|
|
app.MapImageBuildEndpoints();
|
|
app.MapReleaseEndpoints();
|
|
app.MapProjectBuildEndpoints();
|
|
app.MapGitEndpoints();
|
|
app.MapPromotionEndpoints();
|
|
app.MapOpcEndpoints();
|
|
app.MapGiteaEndpoints();
|
|
app.MapInfraEndpoints();
|
|
|
|
// Ensure OPC tables exist (idempotent — IF NOT EXISTS)
|
|
var ds = app.Services.GetRequiredService<NpgsqlDataSource>();
|
|
await using (var cmd = ds.CreateCommand("""
|
|
CREATE TABLE IF NOT EXISTS opc (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
number VARCHAR(20) NOT NULL UNIQUE,
|
|
title VARCHAR(500) NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
type VARCHAR(50) NOT NULL DEFAULT 'General',
|
|
status VARCHAR(50) NOT NULL DEFAULT 'New',
|
|
priority VARCHAR(20) NOT NULL DEFAULT 'Medium',
|
|
assignee VARCHAR(200) NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE TABLE IF NOT EXISTS opc_note (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
opc_id UUID NOT NULL REFERENCES opc(id) ON DELETE CASCADE,
|
|
author VARCHAR(200) NOT NULL,
|
|
content TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE TABLE IF NOT EXISTS opc_artifact (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
opc_id UUID NOT NULL REFERENCES opc(id) ON DELETE CASCADE,
|
|
artifact_type VARCHAR(50) NOT NULL,
|
|
title VARCHAR(500) NOT NULL DEFAULT '',
|
|
content TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE TABLE IF NOT EXISTS opc_pinned_commit (
|
|
opc_id UUID NOT NULL REFERENCES opc(id) ON DELETE CASCADE,
|
|
hash VARCHAR(40) NOT NULL,
|
|
short_hash VARCHAR(10) NOT NULL DEFAULT '',
|
|
subject VARCHAR(1000) NOT NULL DEFAULT '',
|
|
author VARCHAR(200) NOT NULL DEFAULT '',
|
|
pinned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
pinned_by VARCHAR(200) NOT NULL DEFAULT '',
|
|
PRIMARY KEY (opc_id, hash)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS ix_opc_number ON opc(number);
|
|
CREATE INDEX IF NOT EXISTS ix_opc_note_opc_id ON opc_note(opc_id);
|
|
CREATE INDEX IF NOT EXISTS ix_opc_artifact_opc_id ON opc_artifact(opc_id);
|
|
CREATE INDEX IF NOT EXISTS ix_opc_artifact_type ON opc_artifact(opc_id, artifact_type);
|
|
CREATE INDEX IF NOT EXISTS ix_opc_pinned_commit_opc_id ON opc_pinned_commit(opc_id);
|
|
|
|
-- ── Build + Release history ────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS build_record (
|
|
id VARCHAR(8) PRIMARY KEY,
|
|
kind VARCHAR(20) NOT NULL,
|
|
target VARCHAR(500) NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'Running',
|
|
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
finished_at TIMESTAMPTZ,
|
|
duration_ms INTEGER,
|
|
image_digest VARCHAR(200),
|
|
commit_sha VARCHAR(40),
|
|
log TEXT NOT NULL DEFAULT ''
|
|
);
|
|
CREATE TABLE IF NOT EXISTS release_record (
|
|
id VARCHAR(8) PRIMARY KEY,
|
|
environment VARCHAR(50) NOT NULL,
|
|
image_name VARCHAR(200) NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'Running',
|
|
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
finished_at TIMESTAMPTZ
|
|
);
|
|
CREATE TABLE IF NOT EXISTS release_tenant_result (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
release_id VARCHAR(8) NOT NULL REFERENCES release_record(id) ON DELETE CASCADE,
|
|
subdomain VARCHAR(200) NOT NULL,
|
|
container_name VARCHAR(200) NOT NULL,
|
|
success BOOLEAN NOT NULL DEFAULT FALSE,
|
|
error TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS ix_build_record_started_at ON build_record(started_at DESC);
|
|
CREATE INDEX IF NOT EXISTS ix_build_record_kind ON build_record(kind);
|
|
CREATE INDEX IF NOT EXISTS ix_release_record_started_at ON release_record(started_at DESC);
|
|
CREATE INDEX IF NOT EXISTS ix_release_tenant_release_id ON release_tenant_result(release_id);
|
|
"""))
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
// Idempotent column additions for schema migrations
|
|
await using (var migCmd = ds.CreateCommand("""
|
|
ALTER TABLE release_record ADD COLUMN IF NOT EXISTS opc_numbers TEXT[] NOT NULL DEFAULT '{}';
|
|
"""))
|
|
await migCmd.ExecuteNonQueryAsync();
|
|
|
|
app.Run();
|