Files
scadalink-design/docs/plans/2026-03-17-management-service-cli.md
2026-03-17 14:35:52 -04:00

11 KiB

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:

public record AuthenticatedUser(string Username, string DisplayName, IReadOnlyList<string> Roles, IReadOnlyList<string> PermittedSiteIds);
public record ManagementEnvelope(AuthenticatedUser User, object Command, string CorrelationId);

Step 2: Create ManagementResponse.cs — base response types:

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<ManagementEnvelope> 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:

// 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:

public class ClusterConnection : IAsyncDisposable
{
    public async Task ConnectAsync(IReadOnlyList<string> contactPoints);
    public async Task<object> 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:

public static class OutputFormatter
{
    public static void WriteJson(object data);
    public static void WriteTable(IEnumerable<object> 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.