41a5ecf14a
An external system's MaxRetries/RetryDelay existed on the central entity but never reached a site and had no management surface, so every ExternalSystem.CachedCall buffered with the S&F engine default (DefaultMaxRetries 50 x DefaultRetryInterval 30s ~= 25 min to park) whatever central had configured. The sibling DatabaseConnectionArtifact had always carried its equivalents. The consumption end was already wired: ExternalSystemClient.CachedCallAsync already read system.MaxRetries/RetryDelay and passed them to EnqueueAsync with the 0-as-unset convention. Only the transport and persistence were missing. Commons - ExternalSystemArtifact gains MaxRetries/RetryDelay as optional positional params (0 / TimeSpan.Zero = unset), matching DatabaseConnectionArtifact rather than going nullable, so it maps 1:1 onto the entity and the proto3 scalars. - Create/UpdateExternalSystemCommand gain nullable MaxRetries/RetryDelay, following the existing TimeoutSeconds "omit to preserve" convention. Both additive. Central - ManagementActor create maps both; update preserves-if-null. - ArtifactDeploymentService projects them onto the artifact. Wire - site_command.proto ExternalSystemArtifactDto gains max_retries=7 / retry_delay=8 (google.protobuf.Duration), mirroring DatabaseConnectionArtifactDto. Additive: a pre-existing site decodes them as 0/absent, which IS the unset value, so old behaviour is preserved. Vendored C# regenerated via docker/regen-proto.sh sitecommand; csproj Protobuf item re-commented, no message types added or removed. Site - external_systems gains max_retries / retry_delay_ms via AddColumnIfMissing, DEFAULT 0 = unset. Storage upsert, repository SELECT/mapping (new ordinals 6/7 appended so existing ones are untouched), apply path, and the legacy migrator's explicit column list all carry them. Replicated-table decision: external_systems IS in SiteLocalDbSetup.ReplicatedTables, so this was checked against the AckTime/native_alarm_state precedent. Real columns are correct here, not a JSON bag: the table has no general-purpose config column (auth_configuration and method_definitions are both semantically specific), timeout_seconds already set the additive-column precedent on this exact table, and sibling database_connections stores the same two values as plain columns. Verified in LocalDb source that RegisterReplicated re-reads PRAGMA table_info every boot and its install script is DROP-then-CREATE, so capture triggers rebuild over the new column set automatically, and SiteStorageSchema.Apply runs before registration on the same boot. The only cost is the ReplicatedTable.ComputeDigest change, which the sync handshake compares fail-closed - a mixed-version pair refuses to sync loudly instead of diverging silently. Both-nodes-together is already the standing site-pair rule, so no new operational constraint; recorded in topology-guide.md. Also fixed: ArtifactDiff.CompareExternalSystem never compared MaxRetries/RetryDelay, so a bundle import that changed a system's retry budget previewed as "Identical" - harmless while the values were inert, a live behaviour change now. No EF migration: the central ExternalSystemDefinitions columns already exist (InitialSchema). Central UI already had both form fields. Tests: site storage round-trip + unset-default, site apply path, central artifact projection, proto round-trip (via the shared sample), management create/preserve, CLI option parse + alias, and diff reporting. Fail-before verified on the two core tests (expected 7 got 0 on the read path; expected 9 got 0 on the apply path). Docs: Component-ExternalSystemGateway, Component-StoreAndForward, Component-CLI, components/StoreAndForward, components/SiteRuntime, deployment/topology-guide, CLI README (which was also missing --timeout-seconds entirely).
264 lines
15 KiB
C#
264 lines
15 KiB
C#
using System.CommandLine;
|
|
using System.CommandLine.Parsing;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
|
|
|
public static class ExternalSystemCommands
|
|
{
|
|
/// <summary>
|
|
/// Builds the <c>external-system</c> CLI command group with subcommands for managing external systems.
|
|
/// </summary>
|
|
/// <param name="urlOption">Global option for the management URL.</param>
|
|
/// <param name="formatOption">Global option for the output format.</param>
|
|
/// <param name="usernameOption">Global option for the authentication username.</param>
|
|
/// <param name="passwordOption">Global option for the authentication password.</param>
|
|
/// <returns>The fully configured <c>external-system</c> <see cref="Command"/> with all subcommands registered.</returns>
|
|
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var command = new Command("external-system") { Description = "Manage external systems" };
|
|
|
|
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildMethodGroup(urlOption, formatOption, usernameOption, passwordOption));
|
|
|
|
return command;
|
|
}
|
|
|
|
private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var idOption = new Option<int>("--id") { Description = "External system ID", Required = true };
|
|
var cmd = new Command("get") { Description = "Get an external system by ID" };
|
|
cmd.Add(idOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
var id = result.GetValue(idOption);
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption, new GetExternalSystemCommand(id));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var idOption = new Option<int>("--id") { Description = "External system ID", Required = true };
|
|
var nameOption = new Option<string>("--name") { Description = "System name", Required = true };
|
|
var endpointUrlOption = new Option<string>("--endpoint-url") { Description = "Endpoint URL", Required = true };
|
|
var authTypeOption = new Option<string>("--auth-type") { Description = "Auth type", Required = true };
|
|
var authConfigOption = new Option<string?>("--auth-config") { Description = "Auth configuration JSON" };
|
|
var timeoutOption = new Option<int?>("--timeout-seconds") { Description = "Per-system HTTP timeout in seconds (0 = use gateway default); omit to preserve" };
|
|
var maxRetriesOption = new Option<int?>("--max-retries") { Description = "Store-and-forward retry budget for cached calls (0 = use engine default); omit to preserve" };
|
|
var retryDelayOption = new Option<int?>("--retry-delay-seconds") { Description = "Fixed interval between store-and-forward retries, in seconds (0 = use engine default); omit to preserve" };
|
|
retryDelayOption.Aliases.Add("--retry-delay");
|
|
|
|
var cmd = new Command("update") { Description = "Update an external system" };
|
|
cmd.Add(idOption);
|
|
cmd.Add(nameOption);
|
|
cmd.Add(endpointUrlOption);
|
|
cmd.Add(authTypeOption);
|
|
cmd.Add(authConfigOption);
|
|
cmd.Add(timeoutOption);
|
|
cmd.Add(maxRetriesOption);
|
|
cmd.Add(retryDelayOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
var id = result.GetValue(idOption);
|
|
var name = result.GetValue(nameOption)!;
|
|
var endpointUrl = result.GetValue(endpointUrlOption)!;
|
|
var authType = result.GetValue(authTypeOption)!;
|
|
var authConfig = result.GetValue(authConfigOption);
|
|
var timeoutSeconds = result.GetValue(timeoutOption);
|
|
var maxRetries = result.GetValue(maxRetriesOption);
|
|
var retryDelaySeconds = result.GetValue(retryDelayOption);
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new UpdateExternalSystemCommand(id, name, endpointUrl, authType, authConfig, timeoutSeconds,
|
|
maxRetries,
|
|
retryDelaySeconds is { } rd ? TimeSpan.FromSeconds(rd) : null));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var cmd = new Command("list") { Description = "List all external systems" };
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption, new ListExternalSystemsCommand());
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var nameOption = new Option<string>("--name") { Description = "System name", Required = true };
|
|
var endpointUrlOption = new Option<string>("--endpoint-url") { Description = "Endpoint URL", Required = true };
|
|
var authTypeOption = new Option<string>("--auth-type") { Description = "Auth type (ApiKey, BasicAuth)", Required = true };
|
|
var authConfigOption = new Option<string?>("--auth-config") { Description = "Auth configuration JSON" };
|
|
var timeoutOption = new Option<int?>("--timeout-seconds") { Description = "Per-system HTTP timeout in seconds (0 = use gateway default)" };
|
|
var maxRetriesOption = new Option<int?>("--max-retries") { Description = "Store-and-forward retry budget for cached calls (0 = use engine default)" };
|
|
var retryDelayOption = new Option<int?>("--retry-delay-seconds") { Description = "Fixed interval between store-and-forward retries, in seconds (0 = use engine default)" };
|
|
retryDelayOption.Aliases.Add("--retry-delay");
|
|
|
|
var cmd = new Command("create") { Description = "Create an external system" };
|
|
cmd.Add(nameOption);
|
|
cmd.Add(endpointUrlOption);
|
|
cmd.Add(authTypeOption);
|
|
cmd.Add(authConfigOption);
|
|
cmd.Add(timeoutOption);
|
|
cmd.Add(maxRetriesOption);
|
|
cmd.Add(retryDelayOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
var name = result.GetValue(nameOption)!;
|
|
var endpointUrl = result.GetValue(endpointUrlOption)!;
|
|
var authType = result.GetValue(authTypeOption)!;
|
|
var authConfig = result.GetValue(authConfigOption);
|
|
var timeoutSeconds = result.GetValue(timeoutOption);
|
|
var maxRetries = result.GetValue(maxRetriesOption);
|
|
var retryDelaySeconds = result.GetValue(retryDelayOption);
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new CreateExternalSystemCommand(name, endpointUrl, authType, authConfig, timeoutSeconds,
|
|
maxRetries,
|
|
retryDelaySeconds is { } rd ? TimeSpan.FromSeconds(rd) : null));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var idOption = new Option<int>("--id") { Description = "External system ID", Required = true };
|
|
var cmd = new Command("delete") { Description = "Delete an external system" };
|
|
cmd.Add(idOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
var id = result.GetValue(idOption);
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteExternalSystemCommand(id));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
// -- Method subcommands --
|
|
|
|
private static Command BuildMethodGroup(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var group = new Command("method") { Description = "Manage external system methods" };
|
|
group.Add(BuildMethodList(urlOption, formatOption, usernameOption, passwordOption));
|
|
group.Add(BuildMethodGet(urlOption, formatOption, usernameOption, passwordOption));
|
|
group.Add(BuildMethodCreate(urlOption, formatOption, usernameOption, passwordOption));
|
|
group.Add(BuildMethodUpdate(urlOption, formatOption, usernameOption, passwordOption));
|
|
group.Add(BuildMethodDelete(urlOption, formatOption, usernameOption, passwordOption));
|
|
return group;
|
|
}
|
|
|
|
private static Command BuildMethodList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var sysIdOption = new Option<int>("--external-system-id") { Description = "External system ID", Required = true };
|
|
var cmd = new Command("list") { Description = "List methods for an external system" };
|
|
cmd.Add(sysIdOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new ListExternalSystemMethodsCommand(result.GetValue(sysIdOption)));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildMethodGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var idOption = new Option<int>("--id") { Description = "Method ID", Required = true };
|
|
var cmd = new Command("get") { Description = "Get an external system method by ID" };
|
|
cmd.Add(idOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new GetExternalSystemMethodCommand(result.GetValue(idOption)));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildMethodCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var sysIdOption = new Option<int>("--external-system-id") { Description = "External system ID", Required = true };
|
|
var nameOption = new Option<string>("--name") { Description = "Method name", Required = true };
|
|
var httpMethodOption = new Option<string>("--http-method") { Description = "HTTP method (GET, POST, PUT, DELETE)", Required = true };
|
|
var pathOption = new Option<string>("--path") { Description = "URL path (e.g. /api/Add)", Required = true };
|
|
var paramsOption = new Option<string?>("--params") { Description = "Parameter definitions JSON" };
|
|
var returnOption = new Option<string?>("--return") { Description = "Return definition JSON" };
|
|
|
|
var cmd = new Command("create") { Description = "Create an external system method" };
|
|
cmd.Add(sysIdOption);
|
|
cmd.Add(nameOption);
|
|
cmd.Add(httpMethodOption);
|
|
cmd.Add(pathOption);
|
|
cmd.Add(paramsOption);
|
|
cmd.Add(returnOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new CreateExternalSystemMethodCommand(
|
|
result.GetValue(sysIdOption),
|
|
result.GetValue(nameOption)!,
|
|
result.GetValue(httpMethodOption)!,
|
|
result.GetValue(pathOption)!,
|
|
result.GetValue(paramsOption),
|
|
result.GetValue(returnOption)));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildMethodUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var idOption = new Option<int>("--id") { Description = "Method ID", Required = true };
|
|
var nameOption = new Option<string?>("--name") { Description = "Method name" };
|
|
var httpMethodOption = new Option<string?>("--http-method") { Description = "HTTP method" };
|
|
var pathOption = new Option<string?>("--path") { Description = "URL path" };
|
|
var paramsOption = new Option<string?>("--params") { Description = "Parameter definitions JSON" };
|
|
var returnOption = new Option<string?>("--return") { Description = "Return definition JSON" };
|
|
|
|
var cmd = new Command("update") { Description = "Update an external system method" };
|
|
cmd.Add(idOption);
|
|
cmd.Add(nameOption);
|
|
cmd.Add(httpMethodOption);
|
|
cmd.Add(pathOption);
|
|
cmd.Add(paramsOption);
|
|
cmd.Add(returnOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new UpdateExternalSystemMethodCommand(
|
|
result.GetValue(idOption),
|
|
result.GetValue(nameOption),
|
|
result.GetValue(httpMethodOption),
|
|
result.GetValue(pathOption),
|
|
result.GetValue(paramsOption),
|
|
result.GetValue(returnOption)));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildMethodDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var idOption = new Option<int>("--id") { Description = "Method ID", Required = true };
|
|
var cmd = new Command("delete") { Description = "Delete an external system method" };
|
|
cmd.Add(idOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new DeleteExternalSystemMethodCommand(result.GetValue(idOption)));
|
|
});
|
|
return cmd;
|
|
}
|
|
}
|