From 792e8f422a0f3f3addb375373928578915365d64 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 20 Aug 2026 09:46:27 -0400 Subject: [PATCH] fix(host): Program.cs derives roles from the one authority, not OTOPCUA_ROLES alone (archreview 01/S-12 = 03/C5) --- ...3-04-one-role-authority-plan.md.tasks.json | 2 +- src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/archreview/plans/R3-04-one-role-authority-plan.md.tasks.json b/archreview/plans/R3-04-one-role-authority-plan.md.tasks.json index 8a84f165..27da61a3 100644 --- a/archreview/plans/R3-04-one-role-authority-plan.md.tasks.json +++ b/archreview/plans/R3-04-one-role-authority-plan.md.tasks.json @@ -19,7 +19,7 @@ { "id": "T3", "subject": "Program.cs derives hasAdmin/hasDriver/roleSuffix/banner from the authority, not OTOPCUA_ROLES alone (01/S-12 = 03/C5)", - "status": "pending", + "status": "completed", "blockedBy": [ "T2" ] diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs index 12eeee6b..5567a2c0 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs @@ -57,13 +57,19 @@ using ZB.MOM.WW.Secrets.DependencyInjection; using ZB.MOM.WW.Secrets.Replicator.AkkaDotNet; using ZB.MOM.WW.Secrets.Sqlite; -// Roles drive the entire conditional wiring below — see ZB.MOM.WW.OtOpcUa.Cluster.RoleParser. -var roles = RoleParser.Parse(Environment.GetEnvironmentVariable("OTOPCUA_ROLES")); -var hasAdmin = roles.Contains("admin"); -var hasDriver = roles.Contains("driver"); - var builder = WebApplication.CreateBuilder(args); +// The SINGLE role authority: Cluster:Roles first, OTOPCUA_ROLES as the fallback (archreview +// 01/S-12 = 03/C5, R3-04). Resolved off builder.Configuration BEFORE the per-role overlay is +// added, because the overlay's FILENAME depends on the answer — which is why a role overlay may +// not itself set Cluster:Roles (AkkaClusterOptionsValidator fails start if one ever does). +// EffectiveRoles drives the entire conditional wiring below — see ZB.MOM.WW.OtOpcUa.Cluster. +var effectiveRoles = EffectiveRoles.Resolve(builder.Configuration); +var roles = effectiveRoles.Gossiped; +var hasAdmin = effectiveRoles.HasAdmin; +var hasDriver = effectiveRoles.HasDriver; +builder.Services.AddSingleton(effectiveRoles); + // Razor class library static assets (_content//...) only auto-enable in // the Development environment. Opt in explicitly so the AdminUI's CSS/JS works // regardless of ASPNETCORE_ENVIRONMENT. @@ -77,7 +83,7 @@ builder.Host.UseWindowsService(options => options.ServiceName = "OtOpcUaHost"); // Per-role appsettings overlay: appsettings.{role}.json (single role) or appsettings.admin-driver.json // (both). Optional — base appsettings.json carries enough to boot if these don't exist. -var roleSuffix = roles.Length == 0 ? null : string.Join('-', roles.OrderBy(r => r, StringComparer.Ordinal)); +var roleSuffix = roles.Count == 0 ? null : string.Join('-', roles.OrderBy(r => r, StringComparer.Ordinal)); if (roleSuffix is not null) { builder.Configuration.AddJsonFile($"appsettings.{roleSuffix}.json", optional: true, reloadOnChange: true); @@ -786,8 +792,9 @@ if (hasDriver && telemetryListenPort > 0) app.MapOtOpcUaHealth(); app.MapOtOpcUaMetrics(); -Log.Information("OtOpcUa.Host starting with roles=[{Roles}] (admin={HasAdmin}, driver={HasDriver})", - string.Join(",", roles), hasAdmin, hasDriver); +Log.Information( + "OtOpcUa.Host starting with roles=[{Roles}] (admin={HasAdmin}, driver={HasDriver}) source={Source}", + string.Join(",", roles), hasAdmin, hasDriver, effectiveRoles.Source); // #561 — startup growth self-check: the banner above went through the live pipeline; verify each // configured file sink physically grew. A sink whose writes silently fail (the wonder-app-vd03