chore: update configuration and Docker setup for JWT and test results
ci/woodpecker/push/build-arm Pipeline was successful

Enhanced the .gitignore to exclude test results and updated the Dockerfile to include a new entrypoint script for improved container initialization. Refactored JWT configuration to support additional parameters for automatic refresh intervals, ensuring better control over token management. Updated the ConfigurationResolver to enforce required environment variables without hardcoded fallbacks, enhancing security and flexibility.
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-15 03:23:23 +03:00
parent 7025f4d075
commit 78dea8ebab
40 changed files with 1990 additions and 510 deletions
@@ -8,9 +8,10 @@
## Preconditions
- `DATABASE_URL` resolves (env or hardcoded dev fallback).
- `DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` all resolve via `ConfigurationResolver.ResolveRequiredOrThrow`. Any missing value aborts startup before the host is built — there are no hardcoded fallbacks.
- `postgres-local` is reachable.
- The `azaion` database itself exists in PostgreSQL (created at provisioning time, NOT by this migrator).
- `admin` does NOT need to be reachable at process start. The JWKS fetch is lazy — it happens on the first protected request, not during the startup sequence diagrammed below.
## Sequence Diagram
@@ -26,9 +27,12 @@ sequenceDiagram
participant DB as postgres-local
Docker->>Host: ENTRYPOINT dotnet Azaion.Missions.dll
Host->>Cfg: read DATABASE_URL → ConvertPostgresUrl → Npgsql connection string
Host->>Cfg: read JWT_SECRET (env or hardcoded fallback)
Host->>Identity: AddJwtAuth(jwtSecret) — DI registration only, no network
Host->>Cfg: ResolveRequiredOrThrow DATABASE_URL → ConvertPostgresUrl → Npgsql connection string
Host->>Cfg: ResolveRequiredOrThrow JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL — throw on any miss
Host->>Identity: AddJwtAuth(builder.Configuration) — DI registration + ConfigurationManager<JsonWebKeySet> wiring (no network yet)
Host->>Cfg: read CorsConfig:AllowedOrigins, CorsConfig:AllowAnyOrigin
Host->>Cfg: CorsConfigurationValidator.EnsureSafeForEnvironment — throws in Production when origins=[] AND allowAnyOrigin=false
Host->>DI: register CORS policy (permissive OR WithOrigins(...))
Host->>DI: register controllers + middleware + scoped AppDataConnection + scoped services
Host->>DI: build Host
Host->>Migrator: scope.Resolve<AppDataConnection>(); Migrate(db)
@@ -41,8 +45,9 @@ sequenceDiagram
Migrator->>DB: DROP TABLE IF EXISTS gps_corrections
note right of Migrator: B9 one-shot. Idempotent on devices that already cleaned up.
Migrator-->>Host: void
Host->>Host: emit PermissiveDefaultWarning if implicit-permissive CORS applies (non-Production with empty origins)
Host->>Host: register ErrorHandlingMiddleware FIRST in pipeline
Host->>Host: UseAuthentication / UseAuthorization
Host->>Host: UseCors / UseAuthentication / UseAuthorization
Host->>Host: MapControllers + MapGet("/health") + UseSwagger
Host->>Docker: app.Run() — listening on 0.0.0.0:8080
```
@@ -51,15 +56,24 @@ sequenceDiagram
```mermaid
flowchart TD
Start([Container start]) --> ReadCfg[Read DATABASE_URL + JWT_SECRET]
ReadCfg --> RegDI[DI registrations: controllers, middleware, scoped DB + services]
Start([Container start]) --> ResolveDB[ResolveRequiredOrThrow DATABASE_URL]
ResolveDB --> ResolveJwt["ResolveRequiredOrThrow JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL"]
ResolveJwt --> CorsCfg[Read CorsConfig:AllowedOrigins + CorsConfig:AllowAnyOrigin]
CorsCfg --> CorsGate{Production AND origins=[] AND allowAnyOrigin=false?}
CorsGate -->|yes| FailFast([InvalidOperationException — Watchtower restarts])
CorsGate -->|no| RegDI[DI registrations: JWT bearer + JWKS manager, CORS, controllers, scoped DB + services]
RegDI --> Build[Build Host]
Build --> OpenScope[Open startup scope]
Build --> CorsWarn{Implicit-permissive CORS in this env?}
CorsWarn -->|yes| LogWarn[Log PermissiveDefaultWarning]
CorsWarn -->|no| OpenScope[Open startup scope]
LogWarn --> OpenScope
OpenScope --> Migrate[Run DatabaseMigrator.Migrate]
Migrate --> Create["CREATE TABLE IF NOT EXISTS x4 + indexes"]
Create --> Drop["DROP TABLE IF EXISTS orthophotos, gps_corrections (B9)"]
Drop --> Pipeline[Wire pipeline: error MW first, auth, controllers, /health, Swagger]
Pipeline --> Run([app.Run on :8080])
ResolveDB -. on missing value .-> FailFast
ResolveJwt -. on missing value .-> FailFast
Migrate -. on failure .-> Crash([Process exits non-zero — Watchtower restarts])
```
@@ -67,15 +81,18 @@ flowchart TD
| Step | From | To | Data | Format |
|------|------|----|------|--------|
| 1 | Environment / appsettings | `Program.cs` | `DATABASE_URL`, `JWT_SECRET` | string |
| 2 | `Program.cs` | DI container | service registrations | C# code |
| 3 | `DatabaseMigrator` | `postgres-local` | DDL statements (CREATE / INDEX / DROP) | SQL |
| 4 | `Program.cs` | OS / Docker | bind to `0.0.0.0:8080` | TCP listener |
| 1 | Environment / IConfiguration | `Program.cs` (via `ConfigurationResolver`) | `DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` | string (required) |
| 2 | Environment / IConfiguration | `Program.cs` | `CorsConfig:AllowedOrigins` (string[]), `CorsConfig:AllowAnyOrigin` (bool) | optional |
| 3 | `Program.cs` | DI container | service registrations + JWT bearer + JWKS `ConfigurationManager` | C# code |
| 4 | `DatabaseMigrator` | `postgres-local` | DDL statements (CREATE / INDEX / DROP) | SQL |
| 5 | `Program.cs` | OS / Docker | bind to `0.0.0.0:8080` | TCP listener |
## Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Missing `DATABASE_URL` / `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` | `ConfigurationResolver.ResolveRequiredOrThrow` | env var + config key both empty/whitespace | Process exits non-zero with `InvalidOperationException` whose message names the missing env var and config key. Watchtower restarts but the new container hits the same failure. **Fix**: provide the value via env or `appsettings.json` |
| CORS misconfigured in Production (`CorsConfig:AllowedOrigins=[]` AND `CorsConfig:AllowAnyOrigin!=true`) | `CorsConfigurationValidator.EnsureSafeForEnvironment` at startup | hard-fail guard | Process exits with `InvalidOperationException("CORS is misconfigured: ...")`. **Fix**: set `CorsConfig:AllowedOrigins` to the production UI origins, or set `CorsConfig:AllowAnyOrigin=true` to opt in explicitly |
| `postgres-local` unreachable | Migrate step | Npgsql `IOException` / `SocketException` | Process exits non-zero. Watchtower restarts; `flight-gate` prevents restart mid-mission. **Fix**: ensure `postgres-local` healthcheck passes before `missions` starts (compose `depends_on` with `condition: service_healthy`) |
| `azaion` database missing | Migrate step | Npgsql `3D000` (`invalid_catalog_name`) | Process exits. Operator must create the database — provisioning concern, not this service. Documented in `../../suite/_docs/00_top_level_architecture.md` |
| `DROP TABLE IF EXISTS orthophotos` fails because table is locked by `gps-denied` | B9 one-shot | Lock timeout or `55006` | Process exits, Watchtower restarts in a few seconds. **Out-of-band ordering**: deploy `gps-denied` FIRST so it has its own copy of the schema before `missions` drops the legacy tables. Documented in B9 ticket |