Files
Microsoft.SelfService.Porta…/Services/QueueJobService.cs
Torsten Brendgen dc93ea0813 feat: Implement API client and token models with hashing and JWT authentication
- Added ApiClientModel and ApiTokenModel for managing API clients and tokens.
- Introduced ConfigurationDefinitionModel and ConfigurationValueModel for configuration management.
- Created CredentialSecretModel for storing credential secrets.
- Developed DeploymentArtifactModel and DeploymentBatchModel for deployment management.
- Enhanced DeploymentTargetModel and DeploymentTemplateSelectionModel to support template revisions.
- Added TemplateRevisionModel and TemplateVersionModel for versioning templates.
- Implemented ApiClientSecretHasher for secure secret hashing.
- Created ApiTokenService for generating and validating JWT tokens.
- Updated QueueJobService to handle deployment requests with artifacts.
- Configured authentication settings in appsettings.json for JWT and Negotiate authentication.
2026-07-09 14:44:38 +02:00

415 lines
16 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.SelfService.Portal.Core.API.Context;
using Microsoft.SelfService.Portal.Core.API.Interfaces;
using Microsoft.SelfService.Portal.Core.API.JsonDocuments;
using Microsoft.SelfService.Portal.Core.API.Models;
using System.Text.Json;
namespace Microsoft.SelfService.Portal.Core.API.Services
{
public class QueueJobService : IQueueJobService
{
private readonly DataContext _context;
public QueueJobService(DataContext context)
{
_context = context;
}
public Guid EnqueueTemplateJsonChanged(Guid templateId, string oldJsonData, string newJsonData)
{
var deployments = _context.Deployments
.AsNoTracking()
.Include(deployment => deployment.DeploymentGroup)
.Where(deployment => deployment.DeploymentGroup.TemplateId == templateId)
.ToList();
var payload = new
{
TemplateId = templateId,
OldJsonData = oldJsonData,
NewJsonData = newJsonData,
TargetCount = deployments.Count,
Created = DateTime.UtcNow
};
var queueJob = new QueueJobModel
{
Type = QueueJobType.TemplateJsonChanged,
Status = QueueJobStatus.Pending,
CorrelationId = Guid.NewGuid(),
PayloadJson = JsonSerializer.Serialize(payload),
Targets = deployments.Select(deployment => new QueueJobTargetModel
{
TargetId = deployment.TargetId,
DeploymentGroupId = deployment.DeploymentGroupId,
TemplateId = templateId,
Status = QueueJobStatus.Pending
}).ToList()
};
queueJob.Steps = BuildDefaultJobSteps();
_context.QueueJobs.Add(queueJob);
_context.SaveChanges();
return queueJob.Id;
}
public Guid EnqueueDeploymentRequest(Guid deploymentGroupId, ICollection<Guid> targetIds, string jsonData, Guid? deploymentArtifactId = null)
{
var deploymentGroup = _context.DeploymentGroups
.AsNoTracking()
.Include(group => group.Template)
.ThenInclude(template => template.DeploymentRule)
.ThenInclude(rule => rule.Steps)
.Include(group => group.TemplateSelections)
.ThenInclude(selection => selection.TemplateVersion)
.ThenInclude(version => version.Template)
.Include(group => group.TargetAssignments)
.Include(group => group.DeploymentRule)
.ThenInclude(rule => rule!.Steps)
.FirstOrDefault(group => group.Id == deploymentGroupId);
if (deploymentGroup == null)
{
throw new InvalidOperationException("DeploymentGroup does not exist.");
}
var currentArtifact = deploymentArtifactId.HasValue
? _context.DeploymentArtifacts
.AsNoTracking()
.FirstOrDefault(artifact =>
artifact.Id == deploymentArtifactId.Value
&& artifact.DeploymentGroupId == deploymentGroupId
&& !artifact.IsStale)
: _context.DeploymentArtifacts
.AsNoTracking()
.Where(artifact =>
artifact.DeploymentGroupId == deploymentGroupId
&& artifact.ArtifactType == DeploymentArtifactTypes.ResolvedConfigurationData
&& !artifact.IsStale)
.OrderByDescending(artifact => artifact.Created)
.FirstOrDefault();
if (deploymentArtifactId.HasValue && currentArtifact == null)
{
throw new InvalidOperationException("Deployment artifact does not exist or is stale.");
}
if (currentArtifact != null)
{
jsonData = currentArtifact.DeploymentJson;
}
else
{
var deploymentDocument = ConfigurationDocumentValidator.NormalizeAndValidate(
jsonData,
ConfigurationDocumentKind.DeploymentOverride);
jsonData = deploymentDocument.JsonData;
}
var primaryTemplateSelection = deploymentGroup.TemplateSelections
.OrderBy(selection => selection.SortOrder)
.FirstOrDefault();
var templateId = primaryTemplateSelection?.TemplateVersion.TemplateId ?? deploymentGroup.TemplateId;
var resolvedTargetIds = targetIds
.Distinct()
.ToList();
if (resolvedTargetIds.Count == 0)
{
resolvedTargetIds = deploymentGroup.TargetAssignments
.OrderBy(assignment => assignment.SortOrder)
.Select(assignment => assignment.TargetId)
.Distinct()
.ToList();
}
if (resolvedTargetIds.Count == 0)
{
throw new InvalidOperationException("No target Targets provided.");
}
var existingTargets = _context.Targets
.AsNoTracking()
.Where(target => resolvedTargetIds.Contains(target.Id))
.Select(target => target.Id)
.ToHashSet();
var missingTargets = resolvedTargetIds
.Where(targetId => !existingTargets.Contains(targetId))
.ToList();
if (missingTargets.Count > 0)
{
throw new InvalidOperationException($"Unknown Target IDs: {string.Join(", ", missingTargets)}");
}
foreach (var targetId in resolvedTargetIds)
{
var deployment = _context.Deployments
.FirstOrDefault(existing =>
existing.DeploymentGroupId == deploymentGroupId
&& existing.TargetId == targetId);
if (deployment == null)
{
deployment = new DeploymentModel
{
DeploymentGroupId = deploymentGroupId,
TargetId = targetId,
Status = QueueJobStatus.Pending,
JSONData = jsonData
};
_context.Deployments.Add(deployment);
}
else
{
deployment.Status = QueueJobStatus.Pending;
deployment.JSONData = jsonData;
}
}
var resolvedRule = deploymentGroup.DeploymentRule ?? deploymentGroup.Template?.DeploymentRule;
var payload = new
{
DeploymentGroupId = deploymentGroupId,
TemplateId = templateId,
DeploymentRuleId = resolvedRule?.Id,
TemplateSelections = deploymentGroup.TemplateSelections
.OrderBy(selection => selection.SortOrder)
.Select(selection => new
{
selection.Id,
selection.TemplateVersionId,
selection.TemplateRevisionId,
TemplateId = selection.TemplateVersion.TemplateId,
TemplateName = selection.TemplateVersion.Template.Name,
selection.TemplateVersion.Version,
selection.TemplateVersion.JsonHash,
selection.TemplateRole,
selection.SortOrder,
selection.Alias
}),
TargetAssignments = deploymentGroup.TargetAssignments
.OrderBy(assignment => assignment.SortOrder)
.Where(assignment => resolvedTargetIds.Contains(assignment.TargetId))
.Select(assignment => new
{
assignment.Id,
assignment.TargetId,
assignment.RoleKey,
assignment.SortOrder,
assignment.NodeDataJson
}),
TargetIds = resolvedTargetIds,
DeploymentArtifactId = currentArtifact?.Id,
DeploymentJson = currentArtifact?.DeploymentJson,
JsonData = jsonData,
TargetCount = resolvedTargetIds.Count,
Created = DateTime.UtcNow
};
var queueJob = new QueueJobModel
{
Type = QueueJobType.DeploymentRequested,
Status = QueueJobStatus.Pending,
CorrelationId = Guid.NewGuid(),
PayloadJson = JsonSerializer.Serialize(payload),
RuleSnapshotJson = resolvedRule != null
? SerializeRuleSnapshot(resolvedRule)
: null,
Targets = resolvedTargetIds.Select(targetId => new QueueJobTargetModel
{
TargetId = targetId,
DeploymentGroupId = deploymentGroupId,
TemplateId = templateId,
Status = QueueJobStatus.Pending
}).ToList()
};
queueJob.Steps = BuildDeploymentSteps(resolvedRule);
_context.QueueJobs.Add(queueJob);
_context.SaveChanges();
return queueJob.Id;
}
public bool RetryQueueJob(Guid queueJobId)
{
var queueJob = _context.QueueJobs
.Include(job => job.Targets)
.FirstOrDefault(job => job.Id == queueJobId);
if (queueJob == null)
{
return false;
}
queueJob.Status = QueueJobStatus.Pending;
queueJob.ErrorMessage = null;
queueJob.Finished = null;
queueJob.LockedUntil = null;
queueJob.LockedBy = null;
queueJob.HeartbeatAt = null;
queueJob.WorkerName = null;
queueJob.Steps ??= new List<QueueJobStepModel>();
foreach (var target in queueJob.Targets)
{
if (target.Status == QueueJobStatus.Failed || target.Status == QueueJobStatus.Cancelled)
{
target.Status = QueueJobStatus.Pending;
target.ErrorMessage = null;
target.Started = null;
target.Finished = null;
target.OutputMetadataJson = null;
}
}
foreach (var step in queueJob.Steps)
{
if (step.Status == QueueJobStatus.Failed
|| step.Status == QueueJobStatus.Cancelled
|| step.Status == QueueJobStatus.WaitingForApproval
|| step.Status == QueueJobStatus.Rejected)
{
step.Status = QueueJobStatus.Pending;
step.ApprovedAt = null;
step.ApprovedBy = null;
step.ApprovalComment = null;
step.Started = null;
step.Finished = null;
step.ErrorMessage = null;
step.OutputMetadataJson = null;
}
}
return _context.SaveChanges() > 0;
}
public bool ApproveQueueJobStep(Guid queueJobStepId, string approvedBy, string? comment)
{
var step = _context.QueueJobSteps
.Include(existing => existing.QueueJob)
.FirstOrDefault(existing => existing.Id == queueJobStepId);
if (step == null || step.StepType != QueueJobStepType.Approval)
{
return false;
}
step.Status = QueueJobStatus.Succeeded;
step.ApprovedAt = DateTime.UtcNow;
step.ApprovedBy = approvedBy;
step.ApprovalComment = comment;
step.QueueJob.Status = QueueJobStatus.Pending;
step.QueueJob.LockedUntil = null;
step.QueueJob.LockedBy = null;
step.QueueJob.HeartbeatAt = null;
step.QueueJob.WorkerName = null;
step.QueueJob.ErrorMessage = null;
return _context.SaveChanges() > 0;
}
public bool RejectQueueJobStep(Guid queueJobStepId, string approvedBy, string? comment)
{
var step = _context.QueueJobSteps
.Include(existing => existing.QueueJob)
.FirstOrDefault(existing => existing.Id == queueJobStepId);
if (step == null || step.StepType != QueueJobStepType.Approval)
{
return false;
}
step.Status = QueueJobStatus.Rejected;
step.ApprovedAt = DateTime.UtcNow;
step.ApprovedBy = approvedBy;
step.ApprovalComment = comment;
step.QueueJob.Status = QueueJobStatus.Rejected;
step.QueueJob.Finished = DateTime.UtcNow;
step.QueueJob.LockedUntil = null;
step.QueueJob.LockedBy = null;
step.QueueJob.HeartbeatAt = null;
step.QueueJob.WorkerName = null;
step.QueueJob.ErrorMessage = comment ?? "Deployment step rejected.";
return _context.SaveChanges() > 0;
}
private static List<QueueJobStepModel> BuildDefaultJobSteps()
{
return new List<QueueJobStepModel>
{
new()
{
Id = Guid.NewGuid(),
SortOrder = 1,
Name = "Provision",
StepType = QueueJobStepType.Provision,
Status = QueueJobStatus.Pending
}
};
}
private static List<QueueJobStepModel> BuildDeploymentSteps(DeploymentRuleModel? deploymentRule)
{
if (deploymentRule?.Steps == null || deploymentRule.Steps.Count == 0)
{
return BuildDefaultJobSteps();
}
var steps = deploymentRule.Steps
.OrderBy(step => step.SortOrder)
.Select(step => new QueueJobStepModel
{
Id = Guid.NewGuid(),
SortOrder = step.SortOrder,
Name = step.Name,
StepType = step.RequiresApproval ? QueueJobStepType.Approval : step.StepType,
Status = QueueJobStatus.Pending,
MetadataJson = step.MetadataJson
})
.ToList();
for (var i = 1; i < steps.Count; i++)
{
steps[i].DependsOnQueueJobStepId = steps[i - 1].Id;
}
return steps;
}
private static string? SerializeRuleSnapshot(DeploymentRuleModel? rule)
{
if (rule == null)
{
return null;
}
var snapshot = new
{
rule.Id,
rule.Name,
Steps = rule.Steps
.OrderBy(step => step.SortOrder)
.Select(step => new
{
step.Id,
step.SortOrder,
step.Name,
step.StepType,
step.RequiresApproval,
step.MetadataJson
})
};
return JsonSerializer.Serialize(snapshot);
}
}
}