refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
|
||||
|
||||
/// <summary>
|
||||
/// WP-7: System-wide artifact deployment.
|
||||
/// Broadcasts artifacts (shared scripts, external systems, notification lists, DB connections,
|
||||
/// data connections, and SMTP configurations) to all sites with per-site tracking.
|
||||
///
|
||||
/// - Successful sites are NOT rolled back on other failures.
|
||||
/// - Failed sites are retryable individually.
|
||||
/// - 120s timeout per site.
|
||||
/// - Cross-site version skew is supported.
|
||||
/// </summary>
|
||||
public class ArtifactDeploymentService
|
||||
{
|
||||
private readonly ISiteRepository _siteRepo;
|
||||
private readonly IDeploymentManagerRepository _deploymentRepo;
|
||||
private readonly ITemplateEngineRepository _templateRepo;
|
||||
private readonly IExternalSystemRepository _externalSystemRepo;
|
||||
private readonly INotificationRepository _notificationRepo;
|
||||
private readonly CommunicationService _communicationService;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly DeploymentManagerOptions _options;
|
||||
private readonly ILogger<ArtifactDeploymentService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ArtifactDeploymentService.
|
||||
/// </summary>
|
||||
/// <param name="siteRepo">Repository for site data.</param>
|
||||
/// <param name="deploymentRepo">Repository for deployment records.</param>
|
||||
/// <param name="templateRepo">Repository for templates.</param>
|
||||
/// <param name="externalSystemRepo">Repository for external systems.</param>
|
||||
/// <param name="notificationRepo">Repository for notifications.</param>
|
||||
/// <param name="communicationService">Service for communicating with sites.</param>
|
||||
/// <param name="auditService">Service for audit logging.</param>
|
||||
/// <param name="options">Deployment manager options.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
public ArtifactDeploymentService(
|
||||
ISiteRepository siteRepo,
|
||||
IDeploymentManagerRepository deploymentRepo,
|
||||
ITemplateEngineRepository templateRepo,
|
||||
IExternalSystemRepository externalSystemRepo,
|
||||
INotificationRepository notificationRepo,
|
||||
CommunicationService communicationService,
|
||||
IAuditService auditService,
|
||||
IOptions<DeploymentManagerOptions> options,
|
||||
ILogger<ArtifactDeploymentService> logger)
|
||||
{
|
||||
_siteRepo = siteRepo;
|
||||
_deploymentRepo = deploymentRepo;
|
||||
_templateRepo = templateRepo;
|
||||
_externalSystemRepo = externalSystemRepo;
|
||||
_notificationRepo = notificationRepo;
|
||||
_communicationService = communicationService;
|
||||
_auditService = auditService;
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects all artifact types from repositories and builds a <see cref="DeployArtifactsCommand"/>
|
||||
/// scoped to a specific site's data connections.
|
||||
/// </summary>
|
||||
/// <param name="siteId">The DB id of the site whose data connections are collected.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <param name="deploymentId">
|
||||
/// DeploymentManager-010: the logical deployment id for this artifact deployment. All per-site
|
||||
/// commands of one <see cref="DeployToAllSitesAsync"/> call share this id so the audit log,
|
||||
/// UI summary, and persisted record correlate. When <c>null</c> a fresh id is minted (used by
|
||||
/// single-site retries).
|
||||
/// </param>
|
||||
/// <returns>A deployment artifacts command for the site.</returns>
|
||||
/// <remarks>
|
||||
/// DeploymentManager-023: this convenience overload runs the global artifact queries
|
||||
/// for a single site (used by <see cref="RetryForSiteAsync"/>). The multi-site
|
||||
/// <see cref="DeployToAllSitesAsync"/> path hoists the global queries OUT of the
|
||||
/// per-site loop and calls the prefetched-globals overload to avoid the N+1
|
||||
/// re-query of every system-wide artifact set per site.
|
||||
/// </remarks>
|
||||
public async Task<DeployArtifactsCommand> BuildDeployArtifactsCommandAsync(
|
||||
int siteId,
|
||||
CancellationToken cancellationToken = default,
|
||||
string? deploymentId = null)
|
||||
{
|
||||
var globals = await FetchGlobalArtifactsAsync(cancellationToken);
|
||||
return await BuildDeployArtifactsCommandAsync(siteId, globals, cancellationToken, deploymentId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a per-site <see cref="DeployArtifactsCommand"/> using a previously-fetched
|
||||
/// snapshot of the global artifact sets (shared scripts, external systems + methods,
|
||||
/// DB connections, notification lists, SMTP configurations). Only the per-site
|
||||
/// data-connection query runs here.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// DeploymentManager-023: separating the global fetch from the per-site build lets
|
||||
/// <see cref="DeployToAllSitesAsync"/> issue the global queries exactly once across
|
||||
/// the whole multi-site sweep, eliminating the N+1 re-query of shared scripts,
|
||||
/// external systems, methods, DB connections, notification lists, and SMTP
|
||||
/// configurations.
|
||||
/// </remarks>
|
||||
private async Task<DeployArtifactsCommand> BuildDeployArtifactsCommandAsync(
|
||||
int siteId,
|
||||
GlobalArtifactSnapshot globals,
|
||||
CancellationToken cancellationToken,
|
||||
string? deploymentId)
|
||||
{
|
||||
var dataConnections = await _siteRepo.GetDataConnectionsBySiteIdAsync(siteId, cancellationToken);
|
||||
|
||||
// Map data connections
|
||||
var dataConnectionArtifacts = dataConnections.Select(dc =>
|
||||
new DataConnectionArtifact(dc.Name, dc.Protocol, dc.PrimaryConfiguration, dc.BackupConfiguration, dc.FailoverRetryCount)).ToList();
|
||||
|
||||
return new DeployArtifactsCommand(
|
||||
deploymentId ?? Guid.NewGuid().ToString("N"),
|
||||
globals.SharedScripts,
|
||||
globals.ExternalSystems,
|
||||
globals.DatabaseConnections,
|
||||
globals.NotificationLists,
|
||||
dataConnectionArtifacts,
|
||||
globals.SmtpConfigurations,
|
||||
DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the system-wide artifact sets that are identical across every site —
|
||||
/// shared scripts, external systems (with their methods serialized in), database
|
||||
/// connections, notification lists, and SMTP configurations. Used by
|
||||
/// <see cref="DeployToAllSitesAsync"/> to pre-load once before the per-site loop.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// DeploymentManager-023: the per-site artifact build path previously re-issued
|
||||
/// every one of these queries per site (≈ 5·N + M·N round trips for N sites
|
||||
/// and M external systems). Hoisting them here drops that to a single fetch.
|
||||
/// </remarks>
|
||||
private async Task<GlobalArtifactSnapshot> FetchGlobalArtifactsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var sharedScripts = await _templateRepo.GetAllSharedScriptsAsync(cancellationToken);
|
||||
var externalSystems = await _externalSystemRepo.GetAllExternalSystemsAsync(cancellationToken);
|
||||
var dbConnections = await _externalSystemRepo.GetAllDatabaseConnectionsAsync(cancellationToken);
|
||||
var notificationLists = await _notificationRepo.GetAllNotificationListsAsync(cancellationToken);
|
||||
var smtpConfigurations = await _notificationRepo.GetAllSmtpConfigurationsAsync(cancellationToken);
|
||||
|
||||
// Map shared scripts
|
||||
var scriptArtifacts = sharedScripts.Select(s =>
|
||||
new SharedScriptArtifact(s.Name, s.Code, s.ParameterDefinitions, s.ReturnDefinition)).ToList();
|
||||
|
||||
// Map external systems (serialize methods per system)
|
||||
var externalSystemArtifacts = new List<ExternalSystemArtifact>();
|
||||
foreach (var es in externalSystems)
|
||||
{
|
||||
var methods = await _externalSystemRepo.GetMethodsByExternalSystemIdAsync(es.Id, cancellationToken);
|
||||
var methodsJson = methods.Count > 0
|
||||
? JsonSerializer.Serialize(methods.Select(m => new
|
||||
{
|
||||
m.Name,
|
||||
m.HttpMethod,
|
||||
m.Path,
|
||||
m.ParameterDefinitions,
|
||||
m.ReturnDefinition
|
||||
}))
|
||||
: null;
|
||||
externalSystemArtifacts.Add(new ExternalSystemArtifact(
|
||||
es.Name, es.EndpointUrl, es.AuthType, es.AuthConfiguration, methodsJson));
|
||||
}
|
||||
|
||||
// Map database connections
|
||||
var dbConnectionArtifacts = dbConnections.Select(d =>
|
||||
new DatabaseConnectionArtifact(d.Name, d.ConnectionString, d.MaxRetries, d.RetryDelay)).ToList();
|
||||
|
||||
// Map notification lists
|
||||
var notificationListArtifacts = notificationLists.Select(nl =>
|
||||
new NotificationListArtifact(nl.Name, nl.Recipients.Select(r => r.EmailAddress).ToList())).ToList();
|
||||
|
||||
// Map SMTP configurations — use Host as the artifact name (matches SQLite PK on site)
|
||||
var smtpArtifacts = smtpConfigurations.Select(smtp =>
|
||||
new SmtpConfigurationArtifact(
|
||||
$"{smtp.Host}:{smtp.Port}", smtp.Host, smtp.Port, smtp.AuthType, smtp.FromAddress,
|
||||
smtp.Credentials, null, smtp.TlsMode)).ToList();
|
||||
|
||||
return new GlobalArtifactSnapshot(
|
||||
scriptArtifacts,
|
||||
externalSystemArtifacts,
|
||||
dbConnectionArtifacts,
|
||||
notificationListArtifacts,
|
||||
smtpArtifacts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bag of the global artifact sets that do not vary per site, captured once at
|
||||
/// the start of <see cref="DeployToAllSitesAsync"/> and reused for every per-site
|
||||
/// command build (DeploymentManager-023).
|
||||
/// </summary>
|
||||
private sealed record GlobalArtifactSnapshot(
|
||||
IReadOnlyList<SharedScriptArtifact> SharedScripts,
|
||||
IReadOnlyList<ExternalSystemArtifact> ExternalSystems,
|
||||
IReadOnlyList<DatabaseConnectionArtifact> DatabaseConnections,
|
||||
IReadOnlyList<NotificationListArtifact> NotificationLists,
|
||||
IReadOnlyList<SmtpConfigurationArtifact> SmtpConfigurations);
|
||||
|
||||
/// <summary>
|
||||
/// Deploys artifacts to all sites. Builds a per-site command with that site's data connections.
|
||||
/// Returns per-site result matrix.
|
||||
/// </summary>
|
||||
/// <param name="user">The user initiating the deployment.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A summary of the artifact deployment results for all sites.</returns>
|
||||
public async Task<Result<ArtifactDeploymentSummary>> DeployToAllSitesAsync(
|
||||
string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sites = await _siteRepo.GetAllSitesAsync(cancellationToken);
|
||||
if (sites.Count == 0)
|
||||
return Result<ArtifactDeploymentSummary>.Failure("No sites configured.");
|
||||
|
||||
var deploymentId = Guid.NewGuid().ToString("N");
|
||||
var perSiteResults = new Dictionary<string, SiteArtifactResult>();
|
||||
|
||||
// DeploymentManager-023: hoist the system-wide artifact queries (shared scripts,
|
||||
// external systems + methods, DB connections, notification lists, SMTP configs)
|
||||
// OUT of the per-site loop so they run ONCE instead of once per site. Only
|
||||
// data connections legitimately vary per site, so they stay inside the loop.
|
||||
var globals = await FetchGlobalArtifactsAsync(cancellationToken);
|
||||
|
||||
// Build per-site commands sequentially (DbContext is not thread-safe).
|
||||
// DeploymentManager-010: every per-site command carries the SAME logical
|
||||
// deploymentId, so the per-site commands, audit log, persisted record,
|
||||
// and UI summary all reference one id instead of N+1 unrelated GUIDs.
|
||||
var siteCommands = new Dictionary<int, DeployArtifactsCommand>();
|
||||
foreach (var site in sites)
|
||||
{
|
||||
siteCommands[site.Id] = await BuildDeployArtifactsCommandAsync(
|
||||
site.Id, globals, cancellationToken, deploymentId);
|
||||
}
|
||||
|
||||
// Deploy to each site in parallel with per-site timeout
|
||||
var tasks = sites.Select(async site =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(_options.ArtifactDeploymentTimeoutPerSite);
|
||||
|
||||
var command = siteCommands[site.Id];
|
||||
|
||||
_logger.LogInformation(
|
||||
"Deploying artifacts to site {SiteId} ({SiteName}), deploymentId={DeploymentId}",
|
||||
site.SiteIdentifier, site.Name, command.DeploymentId);
|
||||
|
||||
var response = await _communicationService.DeployArtifactsAsync(
|
||||
site.SiteIdentifier, command, cts.Token);
|
||||
|
||||
return new SiteArtifactResult(
|
||||
site.SiteIdentifier, site.Name, response.Success, response.ErrorMessage);
|
||||
}
|
||||
catch (Exception ex) when (ex is TimeoutException or OperationCanceledException or TaskCanceledException)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Artifact deployment to site {SiteId} timed out: {Error}",
|
||||
site.SiteIdentifier, ex.Message);
|
||||
|
||||
return new SiteArtifactResult(
|
||||
site.SiteIdentifier, site.Name, false, $"Timeout: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"Artifact deployment to site {SiteId} failed",
|
||||
site.SiteIdentifier);
|
||||
|
||||
return new SiteArtifactResult(
|
||||
site.SiteIdentifier, site.Name, false, ex.Message);
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
perSiteResults[result.SiteId] = result;
|
||||
}
|
||||
|
||||
// Persist the system artifact deployment record.
|
||||
// DeploymentManager-010: SystemArtifactDeploymentRecord has no dedicated
|
||||
// DeploymentId column (adding one is a Commons/ConfigurationDatabase
|
||||
// schema change outside this module). The logical deploymentId is
|
||||
// embedded in the PerSiteStatus payload so the persisted record can be
|
||||
// correlated with the audit log and UI summary that report the same id.
|
||||
var record = new SystemArtifactDeploymentRecord("Artifacts", user)
|
||||
{
|
||||
DeployedAt = DateTimeOffset.UtcNow,
|
||||
PerSiteStatus = JsonSerializer.Serialize(new
|
||||
{
|
||||
DeploymentId = deploymentId,
|
||||
Sites = perSiteResults
|
||||
})
|
||||
};
|
||||
await _deploymentRepo.AddSystemArtifactDeploymentAsync(record, cancellationToken);
|
||||
await _deploymentRepo.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var summary = new ArtifactDeploymentSummary(
|
||||
deploymentId,
|
||||
results.ToList(),
|
||||
results.Count(r => r.Success),
|
||||
results.Count(r => !r.Success));
|
||||
|
||||
await _auditService.LogAsync(user, "DeployArtifacts", "SystemArtifact",
|
||||
deploymentId, "Artifacts",
|
||||
new { summary.SuccessCount, summary.FailureCount },
|
||||
cancellationToken);
|
||||
|
||||
return Result<ArtifactDeploymentSummary>.Success(summary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-7: Retry artifact deployment to a specific site that previously failed.
|
||||
/// </summary>
|
||||
/// <param name="siteDbId">The database identifier of the site.</param>
|
||||
/// <param name="siteIdentifier">The site identifier string.</param>
|
||||
/// <param name="user">The user initiating the retry.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The result of the retry operation for the site.</returns>
|
||||
public async Task<Result<SiteArtifactResult>> RetryForSiteAsync(
|
||||
int siteDbId,
|
||||
string siteIdentifier,
|
||||
string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(_options.ArtifactDeploymentTimeoutPerSite);
|
||||
|
||||
var command = await BuildDeployArtifactsCommandAsync(siteDbId, cts.Token);
|
||||
var response = await _communicationService.DeployArtifactsAsync(siteIdentifier, command, cts.Token);
|
||||
|
||||
var result = new SiteArtifactResult(siteIdentifier, siteIdentifier, response.Success, response.ErrorMessage);
|
||||
|
||||
await _auditService.LogAsync(user, "RetryArtifactDeployment", "SystemArtifact",
|
||||
command.DeploymentId, siteIdentifier, new { response.Success }, cancellationToken);
|
||||
|
||||
return response.Success
|
||||
? Result<SiteArtifactResult>.Success(result)
|
||||
: Result<SiteArtifactResult>.Failure(response.ErrorMessage ?? "Retry failed.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<SiteArtifactResult>.Failure($"Retry failed for site {siteIdentifier}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-7: Per-site result for artifact deployment.
|
||||
/// </summary>
|
||||
public record SiteArtifactResult(
|
||||
string SiteId,
|
||||
string SiteName,
|
||||
bool Success,
|
||||
string? ErrorMessage);
|
||||
|
||||
/// <summary>
|
||||
/// WP-7: Summary of system-wide artifact deployment with per-site results.
|
||||
/// </summary>
|
||||
public record ArtifactDeploymentSummary(
|
||||
string DeploymentId,
|
||||
IReadOnlyList<SiteArtifactResult> SiteResults,
|
||||
int SuccessCount,
|
||||
int FailureCount);
|
||||
Reference in New Issue
Block a user