266 lines
11 KiB
C#
266 lines
11 KiB
C#
using System.Text.Json;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using ScadaLink.Commons.Entities.Deployment;
|
|
using ScadaLink.Commons.Interfaces.Repositories;
|
|
using ScadaLink.Commons.Interfaces.Services;
|
|
using ScadaLink.Commons.Messages.Artifacts;
|
|
using ScadaLink.Commons.Types;
|
|
using ScadaLink.Communication;
|
|
|
|
namespace ScadaLink.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;
|
|
|
|
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>
|
|
public async Task<DeployArtifactsCommand> BuildDeployArtifactsCommandAsync(
|
|
int siteId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
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 dataConnections = await _siteRepo.GetDataConnectionsBySiteIdAsync(siteId, 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 data connections
|
|
var dataConnectionArtifacts = dataConnections.Select(dc =>
|
|
new DataConnectionArtifact(dc.Name, dc.Protocol, dc.PrimaryConfiguration, dc.BackupConfiguration, dc.FailoverRetryCount)).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 DeployArtifactsCommand(
|
|
Guid.NewGuid().ToString("N"),
|
|
scriptArtifacts,
|
|
externalSystemArtifacts,
|
|
dbConnectionArtifacts,
|
|
notificationListArtifacts,
|
|
dataConnectionArtifacts,
|
|
smtpArtifacts,
|
|
DateTimeOffset.UtcNow);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deploys artifacts to all sites. Builds a per-site command with that site's data connections.
|
|
/// Returns per-site result matrix.
|
|
/// </summary>
|
|
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>();
|
|
|
|
// Build per-site commands sequentially (DbContext is not thread-safe)
|
|
var siteCommands = new Dictionary<int, DeployArtifactsCommand>();
|
|
foreach (var site in sites)
|
|
{
|
|
siteCommands[site.Id] = await BuildDeployArtifactsCommandAsync(site.Id, cancellationToken);
|
|
}
|
|
|
|
// 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
|
|
var record = new SystemArtifactDeploymentRecord("Artifacts", user)
|
|
{
|
|
DeployedAt = DateTimeOffset.UtcNow,
|
|
PerSiteStatus = JsonSerializer.Serialize(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>
|
|
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);
|