From c36de676f3a651fbfb1a45ee2a5b468ec3b2e570 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 17 Mar 2026 14:35:52 -0400 Subject: [PATCH] Add implementation plan: Management Service + CLI --- .../2026-03-17-management-service-cli.md | 218 ++++++++++++++++++ ...03-17-management-service-cli.md.tasks.json | 14 ++ 2 files changed, 232 insertions(+) create mode 100644 docs/plans/2026-03-17-management-service-cli.md create mode 100644 docs/plans/2026-03-17-management-service-cli.md.tasks.json diff --git a/docs/plans/2026-03-17-management-service-cli.md b/docs/plans/2026-03-17-management-service-cli.md new file mode 100644 index 0000000..e78dcdb --- /dev/null +++ b/docs/plans/2026-03-17-management-service-cli.md @@ -0,0 +1,218 @@ +# Management Service + CLI Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. + +**Goal:** Add a ManagementService component (Akka.NET actor on Central exposing all admin operations via ClusterClientReceptionist) and a CLI tool (console app using ClusterClient) for scripting. + +**Architecture:** ManagementActor is a cluster singleton on Central that receives typed command/response messages and delegates to the same services/repositories the Central UI uses. The CLI creates a minimal ActorSystem, connects via ClusterClient, authenticates via LDAP, and sends management commands. Message contracts live in Commons. + +**Tech Stack:** C# / .NET 10, Akka.NET 1.5.62 (Cluster.Tools for ClusterClient/Receptionist), System.CommandLine, xUnit + NSubstitute + +--- + +### Task 1: Create ManagementService project and test project + +**Files:** +- Create: `src/ScadaLink.ManagementService/ScadaLink.ManagementService.csproj` +- Create: `src/ScadaLink.ManagementService/ServiceCollectionExtensions.cs` +- Create: `src/ScadaLink.ManagementService/ManagementServiceOptions.cs` +- Create: `tests/ScadaLink.ManagementService.Tests/ScadaLink.ManagementService.Tests.csproj` +- Modify: `ScadaLink.slnx` — add both projects + +**Step 1:** Create the csproj following the existing pattern (net10.0, TreatWarningsAsErrors, ImplicitUsings). Reference Commons plus all service component projects needed: TemplateEngine, DeploymentManager, Communication, ExternalSystemGateway, NotificationService, Security, HealthMonitoring. Add Akka.Cluster.Tools 1.5.62. + +**Step 2:** Create `ServiceCollectionExtensions.cs` with `AddManagementService()` — register `ManagementServiceOptions` binding. Create empty `ManagementServiceOptions.cs`. + +**Step 3:** Create the test project csproj (xUnit, NSubstitute, reference ManagementService + Commons). + +**Step 4:** Add both projects to `ScadaLink.slnx`. + +**Step 5:** Build: `dotnet build ScadaLink.slnx` + +**Step 6:** Commit: `feat: scaffold ManagementService project and test project` + +--- + +### Task 2: Define management message contracts in Commons + +**Files:** +- Create: `src/ScadaLink.Commons/Messages/Management/ManagementEnvelope.cs` +- Create: `src/ScadaLink.Commons/Messages/Management/ManagementResponse.cs` +- Create: `src/ScadaLink.Commons/Messages/Management/TemplateCommands.cs` +- Create: `src/ScadaLink.Commons/Messages/Management/InstanceCommands.cs` +- Create: `src/ScadaLink.Commons/Messages/Management/SiteCommands.cs` +- Create: `src/ScadaLink.Commons/Messages/Management/DataConnectionCommands.cs` +- Create: `src/ScadaLink.Commons/Messages/Management/DeploymentCommands.cs` +- Create: `src/ScadaLink.Commons/Messages/Management/ExternalSystemCommands.cs` +- Create: `src/ScadaLink.Commons/Messages/Management/NotificationCommands.cs` +- Create: `src/ScadaLink.Commons/Messages/Management/SecurityCommands.cs` +- Create: `src/ScadaLink.Commons/Messages/Management/AuditLogCommands.cs` +- Create: `src/ScadaLink.Commons/Messages/Management/HealthCommands.cs` + +**Step 1:** Create `ManagementEnvelope.cs` — wrapper record carrying `AuthenticatedUser` (username, displayName, roles, permittedSiteIds) and the inner command: +```csharp +public record AuthenticatedUser(string Username, string DisplayName, IReadOnlyList Roles, IReadOnlyList PermittedSiteIds); +public record ManagementEnvelope(AuthenticatedUser User, object Command, string CorrelationId); +``` + +**Step 2:** Create `ManagementResponse.cs` — base response types: +```csharp +public record ManagementSuccess(string CorrelationId, object? Data); +public record ManagementError(string CorrelationId, string Error, string ErrorCode); +public record ManagementUnauthorized(string CorrelationId, string Message); +``` + +**Step 3:** Create all command/response files. Each file contains records for one domain area. Follow the additive-only evolution convention. Examples: +- `TemplateCommands.cs`: `ListTemplatesCommand`, `ListTemplatesResponse`, `GetTemplateCommand`, `GetTemplateResponse`, `CreateTemplateCommand`, `UpdateTemplateCommand`, `DeleteTemplateCommand` +- `InstanceCommands.cs`: `ListInstancesCommand(int? SiteId, int? TemplateId)`, `CreateInstanceCommand(string Name, int TemplateId, int SiteId)`, `DeployInstanceCommand(int InstanceId)`, `EnableInstanceCommand(int InstanceId)`, etc. +- Continue for all 10 message groups from the component doc. + +**Step 4:** Build: `dotnet build ScadaLink.slnx` + +**Step 5:** Commit: `feat: define management message contracts in Commons` + +--- + +### Task 3: Implement ManagementActor + +**Files:** +- Create: `src/ScadaLink.ManagementService/ManagementActor.cs` + +**Step 1:** Create `ManagementActor` extending `ReceiveActor`. Constructor takes `IServiceProvider` (to resolve scoped services per request). Register `Receive` handler. + +**Step 2:** The envelope handler: +1. Extracts `AuthenticatedUser` and inner command +2. Checks authorization based on command type → required role +3. Creates a DI scope, resolves the needed service +4. Delegates to a private handler method per command type +5. Replies with `ManagementSuccess`, `ManagementError`, or `ManagementUnauthorized` + +**Step 3:** Implement handler methods for each command group. Each handler follows the same pattern — resolve service from scope, call the existing service method, wrap result in response. Start with Templates and Instances as they're the most exercised. + +**Step 4:** Build: `dotnet build ScadaLink.slnx` + +**Step 5:** Commit: `feat: implement ManagementActor with all command handlers` + +--- + +### Task 4: Register ManagementActor on Central and set up ClusterClientReceptionist + +**Files:** +- Modify: `src/ScadaLink.Host/Actors/AkkaHostedService.cs` — RegisterCentralActors +- Modify: `src/ScadaLink.Host/Program.cs` — Central role, add `services.AddManagementService()` +- Modify: `src/ScadaLink.ManagementService/ServiceCollectionExtensions.cs` — ensure DI is wired + +**Step 1:** In `RegisterCentralActors()`, after creating CentralCommunicationActor: +```csharp +// Management Service — ClusterClient accessible +var mgmtActor = _actorSystem!.ActorOf( + Props.Create(() => new ManagementActor(_serviceProvider)), + "management"); +ClusterClientReceptionist.Get(_actorSystem).RegisterService(mgmtActor); +_logger.LogInformation("ManagementActor registered with ClusterClientReceptionist"); +``` + +**Step 2:** In `Program.cs` Central role, add `builder.Services.AddManagementService()`. + +**Step 3:** Build and verify Central starts with ManagementActor logged. + +**Step 4:** Commit: `feat: register ManagementActor on Central with ClusterClientReceptionist` + +--- + +### Task 5: Create CLI project with System.CommandLine scaffolding + +**Files:** +- Create: `src/ScadaLink.CLI/ScadaLink.CLI.csproj` +- Create: `src/ScadaLink.CLI/Program.cs` +- Create: `src/ScadaLink.CLI/CliConfig.cs` +- Create: `src/ScadaLink.CLI/ClusterConnection.cs` +- Modify: `ScadaLink.slnx` — add CLI project + +**Step 1:** Create csproj as console app. Reference Commons (for messages), Akka + Akka.Remote + Akka.Cluster.Tools (for ClusterClient), System.CommandLine, Novell.Directory.Ldap.NETStandard (for LDAP auth — same lib used by Security component). + +**Step 2:** Create `CliConfig.cs` — reads config from env vars, `~/.scadalink/config.json`, and CLI options. Properties: ContactPoints, LdapServer, LdapPort, LdapUseTls, DefaultFormat. + +**Step 3:** Create `ClusterConnection.cs` — manages the ActorSystem and ClusterClient lifecycle: +```csharp +public class ClusterConnection : IAsyncDisposable +{ + public async Task ConnectAsync(IReadOnlyList contactPoints); + public async Task AskManagementAsync(ManagementEnvelope envelope, TimeSpan timeout); + public async ValueTask DisposeAsync(); +} +``` + +**Step 4:** Create `Program.cs` with root command, global options (`--contact-points`, `--username`, `--password`, `--format`), and placeholder subcommands. Wire up LDAP auth flow in the root handler. + +**Step 5:** Add to `ScadaLink.slnx`. Build: `dotnet build ScadaLink.slnx`. + +**Step 6:** Commit: `feat: scaffold CLI project with ClusterClient connection` + +--- + +### Task 6: Implement CLI command groups + +**Files:** +- Create: `src/ScadaLink.CLI/Commands/TemplateCommands.cs` +- Create: `src/ScadaLink.CLI/Commands/InstanceCommands.cs` +- Create: `src/ScadaLink.CLI/Commands/SiteCommands.cs` +- Create: `src/ScadaLink.CLI/Commands/DeployCommands.cs` +- Create: `src/ScadaLink.CLI/Commands/DataConnectionCommands.cs` +- Create: `src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs` +- Create: `src/ScadaLink.CLI/Commands/NotificationCommands.cs` +- Create: `src/ScadaLink.CLI/Commands/SecurityCommands.cs` +- Create: `src/ScadaLink.CLI/Commands/AuditLogCommands.cs` +- Create: `src/ScadaLink.CLI/Commands/HealthCommands.cs` +- Create: `src/ScadaLink.CLI/OutputFormatter.cs` + +**Step 1:** Create `OutputFormatter.cs` — handles JSON and table output: +```csharp +public static class OutputFormatter +{ + public static void WriteJson(object data); + public static void WriteTable(IEnumerable rows, string[] columns); + public static void WriteError(string message, string code); +} +``` + +**Step 2:** Implement each command group file. Each follows the same pattern: +- Define a `Command` with subcommands using System.CommandLine +- Each subcommand handler: build the typed command record, wrap in ManagementEnvelope with auth user, call `ClusterConnection.AskManagementAsync`, format output + +**Step 3:** Register all command groups in Program.cs root command. + +**Step 4:** Build: `dotnet build ScadaLink.slnx` + +**Step 5:** Commit: `feat: implement all CLI command groups` + +--- + +### Task 7: Write ManagementActor unit tests + +**Files:** +- Create: `tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs` + +**Step 1:** Test authorization enforcement — send commands without required role, verify `ManagementUnauthorized` response. + +**Step 2:** Test template operations — mock `ITemplateEngineRepository`, send `ListTemplatesCommand`, verify response contains expected data. + +**Step 3:** Test instance operations — mock services, send `CreateInstanceCommand`, verify delegation. + +**Step 4:** Run tests: `dotnet test tests/ScadaLink.ManagementService.Tests/` + +**Step 5:** Commit: `test: add ManagementActor unit tests` + +--- + +### Task 8: End-to-end integration test + +**Step 1:** Start Central with ManagementActor registered. Verify log shows "ManagementActor registered with ClusterClientReceptionist". + +**Step 2:** Run CLI: `dotnet run --project src/ScadaLink.CLI -- --contact-points akka.tcp://scadalink@localhost:8081 --username admin --password password template list` + +**Step 3:** Verify CLI connects, authenticates, sends command, receives response, prints templates. + +**Step 4:** Test error cases: wrong password (exit code 2), unauthorized role (exit code 3), unreachable host (exit code 4). + +**Step 5:** Commit any fixes. diff --git a/docs/plans/2026-03-17-management-service-cli.md.tasks.json b/docs/plans/2026-03-17-management-service-cli.md.tasks.json new file mode 100644 index 0000000..2bae888 --- /dev/null +++ b/docs/plans/2026-03-17-management-service-cli.md.tasks.json @@ -0,0 +1,14 @@ +{ + "planPath": "docs/plans/2026-03-17-management-service-cli.md", + "tasks": [ + {"id": 11, "subject": "Task 1: Create ManagementService project and test project", "status": "pending"}, + {"id": 12, "subject": "Task 2: Define management message contracts in Commons", "status": "pending"}, + {"id": 13, "subject": "Task 3: Implement ManagementActor", "status": "pending", "blockedBy": [11, 12]}, + {"id": 14, "subject": "Task 4: Register ManagementActor on Central with ClusterClientReceptionist", "status": "pending", "blockedBy": [13]}, + {"id": 15, "subject": "Task 5: Create CLI project with ClusterClient scaffolding", "status": "pending", "blockedBy": [12]}, + {"id": 16, "subject": "Task 6: Implement CLI command groups", "status": "pending", "blockedBy": [15]}, + {"id": 17, "subject": "Task 7: Write ManagementActor unit tests", "status": "pending", "blockedBy": [13]}, + {"id": 18, "subject": "Task 8: End-to-end integration test", "status": "pending", "blockedBy": [14, 16, 17]} + ], + "lastUpdated": "2026-03-17T17:00:00Z" +}