Skip to content

Plan: CLI Commands for Customer Stage Removal & Installations View#

Context#

Two pieces of infrastructure exist but aren't wired into the Orchestrator CLI:

  1. ICustomerStageRemovalService — defined and implemented in src/BenefitManager.Orchestrator/Services/CustomerStageCleanupService.cs but not registered in DI and not consumed by any command. Allows removing or deactivating a customer stage by its Guid identifier.

  2. Agent GET /api/installations — the Agent exposes this endpoint (in ManagementEndpoints.cs), returning List<InstallationSummary> with per-component health (filesystem, Windows Service, IIS). However, IAgentClient has no method to call it, so the Orchestrator CLI cannot query installation state.

Goal: Add two new CLI commands: remove-stage to expose customer stage removal, and status to query and display installation state from agent server(s).


Step 1: Register ICustomerStageRemovalService in DI#

File: src/BenefitManager.Orchestrator/Program.cs

Add alongside the existing service registrations:

services.AddSingleton<ICustomerStageRemovalService, CustomerStageRemovalService>();

  • [ ] Service registered in Program.cs

Step 2: New remove-stage CLI command#

New file: src/BenefitManager.Orchestrator/Commands/StageRemoveCommand.cs

Settings#

public sealed class StageRemoveSettings : CommandSettings
{
    [CommandArgument(0, "<ENVIRONMENT>")]
    [Description("The target environment (e.g. Acceptatie or Productie)")]
    public string Environment { get; init; } = null!;

    [CommandArgument(1, "<STAGE_IDENTIFIER>")]
    [Description("The Guid identifier of the customer stage to remove")]
    public Guid StageIdentifier { get; init; }

    [CommandOption("--force")]
    [Description("Deactivate instead of delete (sets Active=0, Online=0)")]
    public bool Force { get; init; }
}

Command#

public sealed class StageRemoveCommand(ICustomerStageRemovalService stageRemovalService) : AsyncCommand<StageRemoveSettings>
  • Inject ICustomerStageRemovalService only — this is a SQL-only operation, no agent interaction needed, so no --server option.
  • Call stageRemovalService.RemoveAsync(env, stageIdentifier, force, ct).
  • Catch InvalidOperationException (thrown when FK constraint fails without --force) and display the error message suggesting --force.
  • Return 0 on success, 1 on failure.

Registration in Program.cs#

cli.AddCommand<StageRemoveCommand>("remove-stage")
   .WithDescription("Removes or deactivates a customer stage from the CommandCenter database.")
   .WithExample(["remove-stage", "Productie", "a1b2c3d4-..."]);
  • [ ] StageRemoveSettings class created
  • [ ] StageRemoveCommand class created with error handling
  • [ ] Command registered in Program.cs

Step 3: Add GetInstallationsAsync to IAgentClient#

Files: - src/BenefitManager.Orchestrator/Services/IAgentClient.cs - src/BenefitManager.Orchestrator/Services/AgentClient.cs

Interface addition#

Task<List<InstallationSummary>> GetInstallationsAsync(CancellationToken cancellationToken = default);

Implementation#

public async Task<List<InstallationSummary>> GetInstallationsAsync(CancellationToken cancellationToken = default)
{
    var response = await httpClient.GetAsync("/api/installations", cancellationToken);
    response.EnsureSuccessStatusCode();
    var body = await response.Content.ReadAsStringAsync(cancellationToken);
    return JsonSerializer.Deserialize<List<InstallationSummary>>(body, JsonOptions)
        ?? throw new InvalidOperationException("Empty response from /api/installations.");
}

The InstallationSummary record is already defined in BenefitManager.Contracts.Models.ManagementModels:

public record InstallationSummary(
    string Environment, string Version, string Path,
    List<ComponentStatus> Components, DateTimeOffset LastChecked);

  • [ ] GetInstallationsAsync added to IAgentClient
  • [ ] GetInstallationsAsync implemented in AgentClient

Step 4: New status CLI command#

New file: src/BenefitManager.Orchestrator/Commands/StatusCommand.cs

Settings#

public sealed class StatusSettings : CommandSettings
{
    [CommandOption("--server")]
    [Description("Target specific server(s). Defaults to all configured agents.")]
    public string[]? Servers { get; init; }
}

No required arguments — queries all installations across targeted servers.

Command#

public sealed class StatusCommand(IAgentClientFactory agentClientFactory) : AsyncCommand<StatusSettings>
  • Resolve targets via the standard --server pattern.
  • For each server: call agentClient.GetInstallationsAsync(ct).
  • Render results using a Spectre.Console table:
Server: vmbenefitsbmp1
┌─────────────┬─────────┬─────────────────────────────┬──────────────────────────┐
│ Environment │ Version │ Path                        │ Components               │
├─────────────┼─────────┼─────────────────────────────┼──────────────────────────┤
│ Productie   │ 25.5.29 │ F:\BenefitManager\Prod\25.5 │ FS:Running Svc:Running … │
│ Acceptatie  │ 25.6.1  │ F:\BenefitManager\Acc\25.6  │ FS:Running Svc:Stopped … │
└─────────────┴─────────┴─────────────────────────────┴──────────────────────────┘
  • Use color coding: Running = green, Stopped = yellow, Missing/Error = red, Unknown = grey.
  • On per-server error (unreachable agent): print error line, continue to next server.
  • Return 0 if all servers responded, 1 if any failed.

Registration in Program.cs#

cli.AddCommand<StatusCommand>("status")
   .WithDescription("Queries installed versions and component health from agent server(s).")
   .WithExample(["status"]);
  • [ ] StatusSettings class created
  • [ ] StatusCommand class created with table rendering and error handling
  • [ ] Command registered in Program.cs

Step 5: Tests#

File: tests/BenefitManager.Orchestrator.Tests/StatusCommandTests.cs (new)

Test GetInstallationsAsync by mocking IAgentClientFactory + IAgentClient: - Single-server returns installations → verify client method called. - Server unreachable (throws) → verify command handles gracefully.

File: tests/BenefitManager.Orchestrator.Tests/StageRemoveCommandTests.cs (new)

Test StageRemoveCommand by mocking ICustomerStageRemovalService: - Successful removal → verify RemoveAsync called with correct args, return 0. - FK constraint error (without --force) → verify return 1. - Force mode → verify RemoveAsync called with force: true.

  • [ ] StatusCommandTests.cs created with single/error test cases
  • [ ] StageRemoveCommandTests.cs created with success/error/force test cases

Files Summary#

Action File
Modify src/BenefitManager.Orchestrator/Program.cs
Modify src/BenefitManager.Orchestrator/Services/IAgentClient.cs
Modify src/BenefitManager.Orchestrator/Services/AgentClient.cs
New src/BenefitManager.Orchestrator/Commands/StageRemoveCommand.cs
New src/BenefitManager.Orchestrator/Commands/StatusCommand.cs
New tests/BenefitManager.Orchestrator.Tests/StatusCommandTests.cs
New tests/BenefitManager.Orchestrator.Tests/StageRemoveCommandTests.cs

Unchanged: CustomerStageCleanupService.cs, ManagementEndpoints.cs, ManagementModels.cs, all existing commands.


Verification#

  1. dotnet build — 0 errors, 0 warnings
  2. dotnet test — all existing + new tests pass
  3. orchestrator remove-stage Productie <guid> — removes the stage from SQL
  4. orchestrator remove-stage Productie <guid> --force — deactivates instead of deleting
  5. orchestrator status — shows installations table from all configured agents
  6. orchestrator status --server vmbenefitsbmp1 — shows installations from specific server only
  7. orchestrator --help — new commands appear in help output