| | | 1 | | using Elsa.Platform.Integration.Models; |
| | | 2 | | using Elsa.Platform.Integration.Options; |
| | | 3 | | using Microsoft.Extensions.DependencyInjection; |
| | | 4 | | using Microsoft.Extensions.Hosting; |
| | | 5 | | using Microsoft.Extensions.Logging; |
| | | 6 | | using Microsoft.Extensions.Options; |
| | | 7 | | |
| | | 8 | | namespace Elsa.Platform.Integration.Services; |
| | | 9 | | |
| | 0 | 10 | | public class ElsaPlatformDeploymentWorker( |
| | 0 | 11 | | IServiceScopeFactory scopeFactory, |
| | 0 | 12 | | IOptions<ElsaPlatformIntegrationOptions> options, |
| | 0 | 13 | | ILogger<ElsaPlatformDeploymentWorker> logger) : BackgroundService |
| | | 14 | | { |
| | 0 | 15 | | private readonly ElsaPlatformIntegrationOptions _options = options.Value; |
| | | 16 | | |
| | | 17 | | protected override async Task ExecuteAsync(CancellationToken stoppingToken) |
| | | 18 | | { |
| | 0 | 19 | | _options.Validate(); |
| | 0 | 20 | | if (!_options.Enabled) |
| | 0 | 21 | | return; |
| | | 22 | | |
| | 0 | 23 | | while (!stoppingToken.IsCancellationRequested) |
| | | 24 | | { |
| | | 25 | | try |
| | | 26 | | { |
| | 0 | 27 | | await ProcessAvailableCommandsAsync(stoppingToken); |
| | 0 | 28 | | } |
| | 0 | 29 | | catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) |
| | | 30 | | { |
| | 0 | 31 | | return; |
| | | 32 | | } |
| | 0 | 33 | | catch (Exception ex) |
| | | 34 | | { |
| | 0 | 35 | | logger.LogError(ex, "Elsa Platform deployment worker failed while polling commands."); |
| | 0 | 36 | | } |
| | | 37 | | |
| | 0 | 38 | | await Task.Delay(_options.PollInterval, stoppingToken); |
| | | 39 | | } |
| | 0 | 40 | | } |
| | | 41 | | |
| | | 42 | | private async Task ProcessAvailableCommandsAsync(CancellationToken cancellationToken) |
| | | 43 | | { |
| | 0 | 44 | | await using var scope = scopeFactory.CreateAsyncScope(); |
| | 0 | 45 | | var client = scope.ServiceProvider.GetRequiredService<IPlatformRuntimeCommandClient>(); |
| | 0 | 46 | | var applier = scope.ServiceProvider.GetRequiredService<IPlatformRecipeArtifactApplier>(); |
| | 0 | 47 | | var commands = await client.PollAsync(cancellationToken); |
| | | 48 | | |
| | 0 | 49 | | foreach (var command in commands) |
| | | 50 | | { |
| | 0 | 51 | | var claim = await client.ClaimAsync(command.Id, cancellationToken); |
| | 0 | 52 | | if (claim is null) |
| | | 53 | | continue; |
| | | 54 | | |
| | 0 | 55 | | await ProcessClaimedCommandAsync(client, applier, claim, cancellationToken); |
| | | 56 | | } |
| | 0 | 57 | | } |
| | | 58 | | |
| | | 59 | | private async Task ProcessClaimedCommandAsync( |
| | | 60 | | IPlatformRuntimeCommandClient client, |
| | | 61 | | IPlatformRecipeArtifactApplier applier, |
| | | 62 | | PlatformRuntimeCommandClaimResponse claim, |
| | | 63 | | CancellationToken cancellationToken) |
| | | 64 | | { |
| | 0 | 65 | | var command = claim.Command; |
| | 0 | 66 | | if (command.Action is not PlatformRuntimeCommandAction.Deploy and not PlatformRuntimeCommandAction.Rollback) |
| | | 67 | | { |
| | 0 | 68 | | await client.RejectAsync( |
| | 0 | 69 | | command.Id, |
| | 0 | 70 | | new PlatformRuntimeCommandRejectRequest( |
| | 0 | 71 | | claim.LeaseToken, |
| | 0 | 72 | | [PlatformDiagnosticSanitizer.Error("elsa-platform.command-unsupported", "Runtime command action is n |
| | 0 | 73 | | cancellationToken); |
| | 0 | 74 | | return; |
| | | 75 | | } |
| | | 76 | | |
| | 0 | 77 | | if (command.Artifacts is not { Count: > 0 }) |
| | | 78 | | { |
| | 0 | 79 | | await client.RejectAsync( |
| | 0 | 80 | | command.Id, |
| | 0 | 81 | | new PlatformRuntimeCommandRejectRequest( |
| | 0 | 82 | | claim.LeaseToken, |
| | 0 | 83 | | [PlatformDiagnosticSanitizer.Error("elsa-platform.artifact-missing", "Runtime command did not includ |
| | 0 | 84 | | cancellationToken); |
| | 0 | 85 | | return; |
| | | 86 | | } |
| | | 87 | | |
| | 0 | 88 | | var outcomes = new List<PlatformArtifactOutcome>(); |
| | | 89 | | try |
| | | 90 | | { |
| | 0 | 91 | | foreach (var artifact in command.Artifacts) |
| | | 92 | | { |
| | 0 | 93 | | await client.ReportProgressAsync(command.Id, claim.LeaseToken, "downloading", 20, "Downloading recipe ar |
| | 0 | 94 | | await using var artifactZip = await client.DownloadArtifactAsync(command, artifact, claim.LeaseToken, ca |
| | 0 | 95 | | await client.ReportProgressAsync(command.Id, claim.LeaseToken, "applying", 60, "Applying recipe artifact |
| | 0 | 96 | | var result = await applier.ApplyAsync(command, artifact, artifactZip, cancellationToken); |
| | 0 | 97 | | outcomes.Add(new PlatformArtifactOutcome( |
| | 0 | 98 | | artifact.ArtifactRecordId, |
| | 0 | 99 | | result.Status, |
| | 0 | 100 | | result.ObservedDigest, |
| | 0 | 101 | | result.RuntimeReference, |
| | 0 | 102 | | result.Diagnostics)); |
| | | 103 | | |
| | 0 | 104 | | if (!result.Succeeded) |
| | | 105 | | break; |
| | 0 | 106 | | } |
| | | 107 | | |
| | 0 | 108 | | var failed = outcomes.FirstOrDefault(x => x.Status == PlatformArtifactStatus.Failed); |
| | 0 | 109 | | if (failed is not null) |
| | | 110 | | { |
| | 0 | 111 | | await client.FailAsync( |
| | 0 | 112 | | command.Id, |
| | 0 | 113 | | new PlatformRuntimeCommandFailRequest( |
| | 0 | 114 | | claim.LeaseToken, |
| | 0 | 115 | | failed.Diagnostics ?? [PlatformDiagnosticSanitizer.Error("elsa-platform.artifact-failed", "Recip |
| | 0 | 116 | | outcomes), |
| | 0 | 117 | | cancellationToken); |
| | 0 | 118 | | return; |
| | | 119 | | } |
| | | 120 | | |
| | 0 | 121 | | var rejected = outcomes.FirstOrDefault(x => x.Status == PlatformArtifactStatus.Rejected); |
| | 0 | 122 | | if (rejected is not null) |
| | | 123 | | { |
| | 0 | 124 | | await client.RejectAsync( |
| | 0 | 125 | | command.Id, |
| | 0 | 126 | | new PlatformRuntimeCommandRejectRequest( |
| | 0 | 127 | | claim.LeaseToken, |
| | 0 | 128 | | rejected.Diagnostics ?? [PlatformDiagnosticSanitizer.Error("elsa-platform.artifact-rejected", "R |
| | 0 | 129 | | outcomes), |
| | 0 | 130 | | cancellationToken); |
| | 0 | 131 | | return; |
| | | 132 | | } |
| | | 133 | | |
| | 0 | 134 | | var first = outcomes.FirstOrDefault(); |
| | 0 | 135 | | await client.CompleteAsync( |
| | 0 | 136 | | command.Id, |
| | 0 | 137 | | new PlatformRuntimeCommandCompleteRequest( |
| | 0 | 138 | | claim.LeaseToken, |
| | 0 | 139 | | first?.ObservedDigest, |
| | 0 | 140 | | first?.RuntimeReference, |
| | 0 | 141 | | [PlatformDiagnosticSanitizer.Info("elsa-platform.command-completed", "Recipe deployment command comp |
| | 0 | 142 | | outcomes), |
| | 0 | 143 | | cancellationToken); |
| | 0 | 144 | | } |
| | 0 | 145 | | catch (Exception ex) when (ex is not OperationCanceledException) |
| | | 146 | | { |
| | 0 | 147 | | logger.LogError(ex, "Elsa Platform deployment worker failed to apply command {CommandId}.", command.Id); |
| | 0 | 148 | | await client.FailAsync( |
| | 0 | 149 | | command.Id, |
| | 0 | 150 | | new PlatformRuntimeCommandFailRequest( |
| | 0 | 151 | | claim.LeaseToken, |
| | 0 | 152 | | [PlatformDiagnosticSanitizer.Error("elsa-platform.command-failed", ex.Message)], |
| | 0 | 153 | | outcomes), |
| | 0 | 154 | | cancellationToken); |
| | | 155 | | } |
| | 0 | 156 | | } |
| | | 157 | | } |