Makes a redeploy safe for the key ring and the schema
Nothing here is visible in normal operation. Its whole purpose is that swapping the release directory on deploy cannot silently destroy state. Data Protection secures the API keys that authenticate master/slave communication. Two separate defaults would each have destroyed them: keys are held on the filesystem, which a release swap discards, and the application discriminator is derived from the content root path, which changes with every release directory — so even keys stored in a database would have stopped being derivable. Keys now live in ApplicationDbContext and the discriminator is a fixed constant. Losing them produces no error. It produces stored keys that no longer decrypt, which presents as an apparent network fault between a Master and its slaves and is easily misdiagnosed. That is also why the tests assert the resulting configuration rather than the registration: the XmlRepository must be the EF one and the discriminator must be the constant, plus a round-trip proving a value encrypted before a deploy is readable after one. A test that only checked "Data Protection is registered" would have passed in the broken case too. Both modules previously called AddDataProtection() themselves. Module registration runs after the host's, so those calls re-registered the configuration chain and would have overridden the persistent store while IDataProtector still resolved. They are removed, with a comment at each site — the deletion otherwise looks like a regression. Each module's own test project now guards against it being reintroduced. ApplicationDbContext also migrates itself at startup. Deploy targets offer no CLI, so migrations cannot be a manual step on the server. Failures are classified rather than treated alike: a connection failure means the database is not up yet, normal when the app and the database start together after a reboot, and is retried with backoff; a migration failure means something is broken and fails at once. Either way the process does not start, which is what makes the liveness health check trustworthy — an application that cannot reach its schema never answers /health, so monitoring goes red instead of reporting a healthy instance that cannot serve a request. The cost of migrating without a human gate is that migrations must stay forward-compatible and non-destructive, since rollback is "redeploy the previous release". The new migration is purely additive. Also wires this and the preceding hosting commit into both hosts, as they touch the same lines of Program.cs. Two constraints are enforced by documentation rather than code, and belong in the deployment instructions: the key table must never be pruned, and only one instance may migrate a given database at a time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using SlpModularCms.Api.Extensions;
|
||||
using SlpModularCms.Core.Hosting;
|
||||
using SlpModularCms.Core.Hosting.Health;
|
||||
using Scalar.AspNetCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -15,6 +17,12 @@ orchestrator.DiscoverModules();
|
||||
builder.Services.AddCoreInfrastructure(builder.Configuration);
|
||||
builder.Services.AddCmsCors(builder.Configuration);
|
||||
builder.Services.AddCmsRateLimiting(builder.Configuration);
|
||||
builder.Services.AddCmsHealthChecks();
|
||||
|
||||
// Registered BEFORE module services: modules must not configure Data Protection themselves,
|
||||
// because a later registration would override this persistent key store (see
|
||||
// DataProtectionExtensions).
|
||||
builder.Services.AddCmsDataProtection();
|
||||
|
||||
// 3. Add Module Services
|
||||
orchestrator.RegisterModuleServices(builder.Services);
|
||||
@@ -32,6 +40,13 @@ builder.Services.AddControllers(options =>
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Bring the Core schema up to date before serving any traffic. Runs before the module
|
||||
// middleware below, because the Data Protection keys table lives in this context and the
|
||||
// modules resolve an IDataProtector as soon as they start. Fails fast: a host that cannot
|
||||
// migrate does not start, so /health goes silent and monitoring goes red — which is exactly
|
||||
// what makes a liveness-only health check trustworthy.
|
||||
app.MigrateCoreDatabase();
|
||||
|
||||
// 5. Global Exception Handling
|
||||
app.UseExceptionHandler();
|
||||
|
||||
@@ -47,10 +62,11 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
// Serve the public website ('/') and the CMS admin SPA ('/admin') from wwwroot.
|
||||
// wwwroot/index.html + assets -> public website (built and deployed separately, not part of this repo)
|
||||
// wwwroot/web/index.html + assets -> public website (built and deployed separately, not part of this repo)
|
||||
// wwwroot/admin/index.html + assets -> CMS admin build (see frontend/, copied in on publish)
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
// Registered before the module middleware below: static files short-circuit the pipeline, so
|
||||
// anything that must observe them has to come first.
|
||||
app.UseCmsStaticContent();
|
||||
|
||||
app.UseCors();
|
||||
|
||||
@@ -62,9 +78,14 @@ app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
// Infrastructure liveness. Deliberately outside /api/v1 and on the availability gate's bypass
|
||||
// list: this reports whether the process is alive, which is a different question from whether
|
||||
// the CMS is switched on (/api/v1/Availability/status) or which modules it carries
|
||||
// (/api/v1/System/capabilities). Those are CMS domain state and must not be used for monitoring.
|
||||
app.MapCmsHealthChecks();
|
||||
|
||||
// SPA fallbacks so client-side routes (e.g. /admin/dashboard) resolve to the right index.html
|
||||
// instead of 404ing. The "nonfile" constraint keeps genuinely missing assets (e.g. /admin/assets/x.js) as 404s.
|
||||
app.MapFallbackToFile("/admin/{*path:nonfile}", "admin/index.html");
|
||||
app.MapFallbackToFile("{*path:nonfile}", "index.html");
|
||||
app.MapCmsSpaFallbacks();
|
||||
|
||||
app.Run();
|
||||
|
||||
Reference in New Issue
Block a user