using AutoMapper; using Microsoft.AspNetCore.Mvc; using Microsoft.SelfService.Portal.Core.API.Dto.Deployment.Add; using Microsoft.SelfService.Portal.Core.API.Dto.Deployment.Get; using Microsoft.SelfService.Portal.Core.API.Dto.Deployment.Edit; using Microsoft.SelfService.Portal.Core.API.Interfaces; using Microsoft.SelfService.Portal.Core.API.Models; using Microsoft.SelfService.Portal.Core.API.Context; using Microsoft.SelfService.Portal.Core.API.Events.Interfaces; using System.Reflection; using System.Collections; using System.Dynamic; using System.Text.Json; using Microsoft.EntityFrameworkCore; namespace Microsoft.SelfService.Portal.Core.API.Controllers { [Route("api/[controller]")] [ApiController] public class DeploymentController : Controller { private readonly IDeploymentInterface _deploymentInterface; private readonly IQueueJobService _queueJobService; private readonly DataContext _context; private readonly IMapper _mapper; private readonly IEventHandlerInterface _eventHandlerInterface; public DeploymentController( IDeploymentInterface deploymentInterface, IQueueJobService queueJobService, DataContext context, IMapper mapper, IEventHandlerInterface eventHandlerInterface) { _deploymentInterface = deploymentInterface; _queueJobService = queueJobService; _context = context; _mapper = mapper; _eventHandlerInterface = eventHandlerInterface; } [HttpGet] [ProducesResponseType(200, Type = typeof(IEnumerable))] public IActionResult GetDeployments() { var deployment = _mapper.Map>(_deploymentInterface.GetDeployments()); if (!ModelState.IsValid) return BadRequest(ModelState); return Ok(deployment); } [HttpGet("{Id}")] [ProducesResponseType(200, Type = typeof(DeploymentModel))] [ProducesResponseType(400)] public IActionResult GetDeploymentById(Guid Id) { if (!_deploymentInterface.CheckDeploymentById(Id)) return NotFound(); var deployment = _mapper.Map(_deploymentInterface.GetDeploymentById(Id)); if (!ModelState.IsValid) return BadRequest(ModelState); return Ok(deployment); } [HttpPost] [ProducesResponseType(204)] [ProducesResponseType(400)] public IActionResult AddDeploymentById([FromBody] AddDeploymentDto deployment) { if (deployment == null) return BadRequest(ModelState); if (!ModelState.IsValid) return BadRequest(ModelState); var deploymentMap = _mapper.Map(deployment); if (!_deploymentInterface.AddDeploymentById(deploymentMap)) { ModelState.AddModelError("", "Something went wrong while saving"); return StatusCode(500, ModelState); } return Ok(deploymentMap.Id); } [HttpPost("Request")] [ProducesResponseType(200)] [ProducesResponseType(400)] public IActionResult AddDeploymentRequest([FromBody] AddDeploymentRequestDto request) { if (request == null) return BadRequest(ModelState); if (request.VirtualMachineIds == null || request.VirtualMachineIds.Count == 0) { ModelState.AddModelError("", "At least one VirtualMachine must be selected."); return BadRequest(ModelState); } if (!ModelState.IsValid) return BadRequest(ModelState); try { var queueJobId = _queueJobService.EnqueueDeploymentRequest( request.DeploymentGroupId, request.VirtualMachineIds, request.JsonData); return Ok(queueJobId); } catch (InvalidOperationException ex) { ModelState.AddModelError("", ex.Message); return BadRequest(ModelState); } } [HttpGet("QueueJobs")] [ProducesResponseType(200, Type = typeof(IEnumerable))] public IActionResult GetQueueJobs() { var queueJobs = _context.QueueJobs .AsNoTracking() .Include(job => job.Targets) .Include(job => job.Steps) .OrderByDescending(job => job.Created) .Select(job => new GetQueueJobDto { Id = job.Id, Type = job.Type, Status = job.Status, Attempts = job.Attempts, MaxAttempts = job.MaxAttempts, Started = job.Started, Finished = job.Finished, ErrorMessage = job.ErrorMessage, TargetCount = job.Targets.Count, SucceededTargetCount = job.Targets.Count(target => target.Status == QueueJobStatus.Succeeded), FailedTargetCount = job.Targets.Count(target => target.Status == QueueJobStatus.Failed) }) .ToList(); return Ok(queueJobs); } [HttpGet("QueueJobs/{Id}")] [ProducesResponseType(200, Type = typeof(GetQueueJobDetailsDto))] [ProducesResponseType(404)] public IActionResult GetQueueJobById(Guid Id) { var queueJob = _context.QueueJobs .AsNoTracking() .Include(job => job.Targets) .Include(job => job.Steps) .FirstOrDefault(job => job.Id == Id); if (queueJob == null) return NotFound(); var details = new GetQueueJobDetailsDto { Id = queueJob.Id, Type = queueJob.Type, Status = queueJob.Status, PayloadJson = queueJob.PayloadJson, Attempts = queueJob.Attempts, MaxAttempts = queueJob.MaxAttempts, Started = queueJob.Started, Finished = queueJob.Finished, ErrorMessage = queueJob.ErrorMessage, MetadataJson = queueJob.MetadataJson, RuleSnapshotJson = queueJob.RuleSnapshotJson, Created = queueJob.Created, CreatedBy = queueJob.CreatedBy, Modified = queueJob.Modified, ModifiedBy = queueJob.ModifiedBy, Targets = queueJob.Targets.Select(target => new GetQueueJobTargetDto { Id = target.Id, VirtualMachineId = target.VirtualMachineId, DeploymentGroupId = target.DeploymentGroupId, TemplateId = target.TemplateId, Status = target.Status, Attempts = target.Attempts, ErrorMessage = target.ErrorMessage }).ToList(), Steps = queueJob.Steps .OrderBy(step => step.SortOrder) .Select(step => new GetQueueJobStepDto { Id = step.Id, DependsOnQueueJobStepId = step.DependsOnQueueJobStepId, SortOrder = step.SortOrder, Name = step.Name, StepType = step.StepType, Status = step.Status, MetadataJson = step.MetadataJson, ApprovedAt = step.ApprovedAt, ApprovedBy = step.ApprovedBy, ApprovalComment = step.ApprovalComment }).ToList() }; return Ok(details); } [HttpPost("QueueJobs/{Id}/Retry")] [ProducesResponseType(204)] [ProducesResponseType(404)] public IActionResult RetryQueueJob(Guid Id) { if (!_queueJobService.RetryQueueJob(Id)) return NotFound(); return NoContent(); } [HttpPost("QueueJobs/Steps/{stepId}/Approve")] [ProducesResponseType(204)] [ProducesResponseType(404)] public IActionResult ApproveQueueJobStep(Guid stepId, [FromBody] QueueJobStepApprovalDto? payload) { var approvedBy = payload?.ApprovedBy ?? User?.Identity?.Name ?? "System"; if (!_queueJobService.ApproveQueueJobStep(stepId, approvedBy, payload?.Comment)) return NotFound(); return NoContent(); } [HttpPost("QueueJobs/Steps/{stepId}/Reject")] [ProducesResponseType(204)] [ProducesResponseType(404)] public IActionResult RejectQueueJobStep(Guid stepId, [FromBody] QueueJobStepApprovalDto? payload) { var approvedBy = payload?.ApprovedBy ?? User?.Identity?.Name ?? "System"; if (!_queueJobService.RejectQueueJobStep(stepId, approvedBy, payload?.Comment)) return NotFound(); return NoContent(); } [HttpDelete("{Id}")] [ProducesResponseType(204)] [ProducesResponseType(400)] [ProducesResponseType(404)] public IActionResult DeleteDeploymentById(Guid Id) { if (!_deploymentInterface.CheckDeploymentById(Id)) return NotFound(); if (!ModelState.IsValid) return BadRequest(ModelState); var deployment = _deploymentInterface.GetDeploymentById(Id); if (!_deploymentInterface.DeleteDeploymentById(deployment)) { ModelState.AddModelError("", "Something went wrong while deleting"); return StatusCode(500, ModelState); } return NoContent(); } [HttpPut("{Id}")] [ProducesResponseType(204)] [ProducesResponseType(400)] [ProducesResponseType(404)] public IActionResult EditDeploymentById(Guid Id, [FromBody] EditDeploymentDto deployment) { if (deployment == null) return BadRequest(ModelState); if (!_deploymentInterface.CheckDeploymentById(Id)) return NotFound(); if (!ModelState.IsValid) return BadRequest(ModelState); var deploymentMap = _mapper.Map(deployment); deploymentMap.Id = Id; if (!_deploymentInterface.EditDeploymentById(deploymentMap)) { ModelState.AddModelError("", "Something went wrong"); return StatusCode(500, ModelState); } _eventHandlerInterface.FireEvent(this.GetType().Name, MethodBase.GetCurrentMethod().Name, Id); return NoContent(); } } }