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, 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 targetIds, string jsonData) { var deploymentDocument = ConfigurationDocumentValidator.NormalizeAndValidate( jsonData, ConfigurationDocumentKind.DeploymentOverride); jsonData = deploymentDocument.JsonData; var deploymentGroup = _context.DeploymentGroups .AsNoTracking() .Include(group => group.Template) .ThenInclude(template => template.DeploymentRule) .ThenInclude(rule => rule.Steps) .Include(group => group.DeploymentRule) .ThenInclude(rule => rule!.Steps) .FirstOrDefault(group => group.Id == deploymentGroupId); if (deploymentGroup == null) { throw new InvalidOperationException("DeploymentGroup does not exist."); } var templateId = deploymentGroup.TemplateId; var resolvedTargetIds = targetIds .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, TargetIds = resolvedTargetIds, JsonData = jsonData, TargetCount = resolvedTargetIds.Count, Created = DateTime.UtcNow }; var queueJob = new QueueJobModel { Type = QueueJobType.DeploymentRequested, Status = QueueJobStatus.Pending, 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.Steps ??= new List(); foreach (var target in queueJob.Targets) { if (target.Status == QueueJobStatus.Failed || target.Status == QueueJobStatus.Cancelled) { target.Status = QueueJobStatus.Pending; target.ErrorMessage = 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; } } 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.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.ErrorMessage = comment ?? "Deployment step rejected."; return _context.SaveChanges() > 0; } private static List BuildDefaultJobSteps() { return new List { new() { Id = Guid.NewGuid(), SortOrder = 1, Name = "Provision", StepType = QueueJobStepType.Provision, Status = QueueJobStatus.Pending } }; } private static List 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); } } }