using Microsoft.EntityFrameworkCore; using Microsoft.SelfService.Portal.Core.API.Context; using Microsoft.SelfService.Portal.Core.API.Interfaces; 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 { VirtualMachineId = deployment.VirtualMachineId, 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 virtualMachineIds, string jsonData) { var deploymentGroup = _context.DeploymentGroups .AsNoTracking() .Include(group => group.Template) .ThenInclude(template => template.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 resolvedVirtualMachineIds = virtualMachineIds .Distinct() .ToList(); if (resolvedVirtualMachineIds.Count == 0) { throw new InvalidOperationException("No target VirtualMachines provided."); } var existingVirtualMachines = _context.VirtualMachines .AsNoTracking() .Where(virtualMachine => resolvedVirtualMachineIds.Contains(virtualMachine.Id)) .Select(virtualMachine => virtualMachine.Id) .ToHashSet(); var missingVirtualMachines = resolvedVirtualMachineIds .Where(virtualMachineId => !existingVirtualMachines.Contains(virtualMachineId)) .ToList(); if (missingVirtualMachines.Count > 0) { throw new InvalidOperationException($"Unknown VirtualMachine IDs: {string.Join(", ", missingVirtualMachines)}"); } foreach (var virtualMachineId in resolvedVirtualMachineIds) { var deployment = _context.Deployments .FirstOrDefault(existing => existing.DeploymentGroupId == deploymentGroupId && existing.VirtualMachineId == virtualMachineId); if (deployment == null) { deployment = new DeploymentModel { DeploymentGroupId = deploymentGroupId, VirtualMachineId = virtualMachineId, Status = QueueJobStatus.Pending, JSONData = jsonData }; _context.Deployments.Add(deployment); } else { deployment.Status = QueueJobStatus.Pending; deployment.JSONData = jsonData; } } var payload = new { DeploymentGroupId = deploymentGroupId, TemplateId = templateId, VirtualMachineIds = resolvedVirtualMachineIds, JsonData = jsonData, TargetCount = resolvedVirtualMachineIds.Count, Created = DateTime.UtcNow }; var queueJob = new QueueJobModel { Type = QueueJobType.DeploymentRequested, Status = QueueJobStatus.Pending, PayloadJson = JsonSerializer.Serialize(payload), RuleSnapshotJson = deploymentGroup.Template?.DeploymentRuleId.HasValue == true ? SerializeRuleSnapshot(deploymentGroup.Template!.DeploymentRule) : null, Targets = resolvedVirtualMachineIds.Select(virtualMachineId => new QueueJobTargetModel { VirtualMachineId = virtualMachineId, DeploymentGroupId = deploymentGroupId, TemplateId = templateId, Status = QueueJobStatus.Pending }).ToList() }; queueJob.Steps = BuildDeploymentSteps(deploymentGroup.Template?.DeploymentRule); _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); } } }