using AutoMapper; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.SelfService.Portal.Core.API.Authorization; using Microsoft.SelfService.Portal.Core.API.Context; using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentBatch.Add; using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentBatch.Edit; using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentBatch.Get; using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentComposition.Add; using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentComposition.Get; using Microsoft.SelfService.Portal.Core.API.Interfaces; using Microsoft.SelfService.Portal.Core.API.JsonDocuments; using Microsoft.SelfService.Portal.Core.API.Models; namespace Microsoft.SelfService.Portal.Core.API.Controllers { [Route("api/deployment-batches")] [ApiController] [Authorize(Policy = ApiPolicies.DeploymentRead)] public class DeploymentBatchController : Controller { private readonly IDeploymentBatchInterface _deploymentBatchInterface; private readonly DataContext _context; private readonly IMapper _mapper; public DeploymentBatchController(IDeploymentBatchInterface deploymentBatchInterface, DataContext context, IMapper mapper) { _deploymentBatchInterface = deploymentBatchInterface; _context = context; _mapper = mapper; } [HttpGet] [ProducesResponseType(200, Type = typeof(IEnumerable))] public IActionResult GetDeploymentBatches() { var deploymentBatches = _deploymentBatchInterface .GetDeploymentBatches() .Select(batch => { var primarySelection = batch.TemplateSelections .OrderBy(selection => selection.SortOrder) .FirstOrDefault(); return new GetDeploymentBatchDto { Id = batch.Id, TemplateId = batch.TemplateId, DeploymentRuleId = batch.DeploymentRuleId, Status = batch.Status, Created = batch.Created, CreatedBy = batch.CreatedBy, Modified = batch.Modified, ModifiedBy = batch.ModifiedBy, PrimaryTemplateVersionId = primarySelection?.TemplateVersionId, PrimaryTemplateName = primarySelection?.TemplateVersion?.Template?.Name, PrimaryTemplateVersion = primarySelection?.TemplateVersion?.Version, TemplateSelectionCount = batch.TemplateSelections.Count, TargetAssignmentCount = batch.TargetAssignments.Count }; }) .ToList(); if (!ModelState.IsValid) return BadRequest(ModelState); return Ok(deploymentBatches); } [HttpGet("{id}")] [ProducesResponseType(200, Type = typeof(DeploymentGroupModel))] [ProducesResponseType(400)] public IActionResult GetDeploymentBatchById(Guid id) { if (!_deploymentBatchInterface.CheckDeploymentBatchById(id)) return NotFound(); var deploymentBatch = _mapper.Map(_deploymentBatchInterface.GetDeploymentBatchById(id)); if (!ModelState.IsValid) return BadRequest(ModelState); return Ok(deploymentBatch); } [HttpPost] [Authorize(Policy = ApiPolicies.DeploymentWrite)] [ProducesResponseType(204)] [ProducesResponseType(400)] public IActionResult AddDeploymentBatch([FromBody] AddDeploymentBatchDto deploymentBatch) { if (deploymentBatch == null) return BadRequest(ModelState); if (!ModelState.IsValid) return BadRequest(ModelState); if (deploymentBatch.TemplateVersionId.HasValue && (deploymentBatch.TemplateSelections == null || deploymentBatch.TemplateSelections.Count == 0)) { deploymentBatch.TemplateSelections = new List { new() { TemplateVersionId = deploymentBatch.TemplateVersionId.Value, TemplateRole = "Service", SortOrder = 10 } }; } var deploymentBatchMap = _mapper.Map(deploymentBatch); if (!_deploymentBatchInterface.AddDeploymentBatchById(deploymentBatchMap, deploymentBatch.TargetIds)) { ModelState.AddModelError("", "Something went wrong while saving"); return StatusCode(500, ModelState); } return Ok(deploymentBatchMap.Id); } [HttpDelete("{id}")] [Authorize(Policy = ApiPolicies.DeploymentWrite)] [ProducesResponseType(204)] [ProducesResponseType(400)] [ProducesResponseType(404)] public IActionResult DeleteDeploymentBatchById(Guid id) { if (!_deploymentBatchInterface.CheckDeploymentBatchById(id)) return NotFound(); if (!ModelState.IsValid) return BadRequest(ModelState); var deploymentBatch = _deploymentBatchInterface.GetDeploymentBatchById(id); if (deploymentBatch == null) return NotFound(); if (!_deploymentBatchInterface.DeleteDeploymentBatchById(deploymentBatch)) { ModelState.AddModelError("", "Something went wrong while deleting"); return StatusCode(500, ModelState); } return NoContent(); } [HttpPut("{id}")] [Authorize(Policy = ApiPolicies.DeploymentWrite)] [ProducesResponseType(204)] [ProducesResponseType(400)] [ProducesResponseType(404)] public IActionResult EditDeploymentBatchById(Guid id, [FromBody] EditDeploymentBatchDto deploymentBatch) { if (deploymentBatch == null) return BadRequest(ModelState); if (!_deploymentBatchInterface.CheckDeploymentBatchById(id)) return NotFound(); if (!ModelState.IsValid) return BadRequest(ModelState); var deploymentBatchMap = _mapper.Map(deploymentBatch); deploymentBatchMap.Id = id; if (!_deploymentBatchInterface.EditDeploymentBatchById(deploymentBatchMap)) { ModelState.AddModelError("", "Something went wrong"); return StatusCode(500, ModelState); } return NoContent(); } [HttpGet("{id}/composition")] [ProducesResponseType(200, Type = typeof(GetDeploymentCompositionDto))] [ProducesResponseType(404)] public IActionResult GetDeploymentComposition(Guid id) { if (!_deploymentBatchInterface.CheckDeploymentBatchById(id)) return NotFound(); return Ok(LoadComposition(id)); } [HttpPost("{id}/template-selections")] [Authorize(Policy = ApiPolicies.DeploymentWrite)] [ProducesResponseType(200, Type = typeof(Guid))] [ProducesResponseType(400)] [ProducesResponseType(404)] public IActionResult AddTemplateSelection(Guid id, [FromBody] AddDeploymentTemplateSelectionDto templateSelection) { if (!_deploymentBatchInterface.CheckDeploymentBatchById(id)) return NotFound(); var templateVersion = _context.TemplateVersions .Include(version => version.TemplateRevisions) .FirstOrDefault(version => version.Id == templateSelection.TemplateVersionId); if (templateVersion == null) { ModelState.AddModelError(nameof(templateSelection.TemplateVersionId), "Template version was not found."); return BadRequest(ModelState); } var templateRevisionId = templateSelection.TemplateRevisionId ?? templateVersion.TemplateRevisions.FirstOrDefault(revision => revision.IsCurrent)?.Id; if (templateRevisionId.HasValue && !templateVersion.TemplateRevisions.Any(revision => revision.Id == templateRevisionId.Value)) { ModelState.AddModelError(nameof(templateSelection.TemplateRevisionId), "Template revision was not found in this template version."); return BadRequest(ModelState); } var sortOrder = templateSelection.SortOrder > 0 ? templateSelection.SortOrder : GetNextTemplateSelectionSortOrder(id); if (_context.DeploymentTemplateSelections.Any(selection => selection.DeploymentGroupId == id && selection.SortOrder == sortOrder)) { ModelState.AddModelError(nameof(templateSelection.SortOrder), "SortOrder already exists in this deployment batch."); return BadRequest(ModelState); } var model = _mapper.Map(templateSelection); model.Id = Guid.NewGuid(); model.DeploymentGroupId = id; model.SortOrder = sortOrder; model.TemplateRevisionId = templateRevisionId; _context.DeploymentTemplateSelections.Add(model); _context.SaveChanges(); return Ok(model.Id); } [HttpDelete("{id}/template-selections/{selectionId}")] [Authorize(Policy = ApiPolicies.DeploymentWrite)] [ProducesResponseType(204)] [ProducesResponseType(404)] public IActionResult DeleteTemplateSelection(Guid id, Guid selectionId) { var selection = _context.DeploymentTemplateSelections .FirstOrDefault(existing => existing.DeploymentGroupId == id && existing.Id == selectionId); if (selection == null) return NotFound(); _context.DeploymentTemplateSelections.Remove(selection); _context.SaveChanges(); return NoContent(); } [HttpPost("{id}/parameter-values")] [Authorize(Policy = ApiPolicies.DeploymentWrite)] [ProducesResponseType(200, Type = typeof(Guid))] [ProducesResponseType(400)] [ProducesResponseType(404)] public IActionResult AddParameterValue(Guid id, [FromBody] AddDeploymentParameterValueDto parameterValue) { if (!_deploymentBatchInterface.CheckDeploymentBatchById(id)) return NotFound(); if (parameterValue.DeploymentTemplateSelectionId.HasValue && !_context.DeploymentTemplateSelections.Any(selection => selection.DeploymentGroupId == id && selection.Id == parameterValue.DeploymentTemplateSelectionId.Value)) { ModelState.AddModelError(nameof(parameterValue.DeploymentTemplateSelectionId), "Template selection was not found in this deployment batch."); return BadRequest(ModelState); } if (_context.DeploymentParameterValues.Any(existing => existing.DeploymentGroupId == id && existing.DeploymentTemplateSelectionId == parameterValue.DeploymentTemplateSelectionId && existing.Name == parameterValue.Name)) { ModelState.AddModelError(nameof(parameterValue.Name), "Parameter value already exists in this scope."); return BadRequest(ModelState); } try { parameterValue.ValueJson = ConfigurationDocumentValidator.NormalizeAndValidate( parameterValue.ValueJson, ConfigurationDocumentKind.Metadata).JsonData; } catch (ConfigurationDocumentValidationException ex) { ModelState.AddModelError(nameof(parameterValue.ValueJson), ex.Message); return BadRequest(ModelState); } var model = _mapper.Map(parameterValue); model.Id = Guid.NewGuid(); model.DeploymentGroupId = id; _context.DeploymentParameterValues.Add(model); _context.SaveChanges(); return Ok(model.Id); } [HttpDelete("{id}/parameter-values/{parameterValueId}")] [Authorize(Policy = ApiPolicies.DeploymentWrite)] [ProducesResponseType(204)] [ProducesResponseType(404)] public IActionResult DeleteParameterValue(Guid id, Guid parameterValueId) { var parameterValue = _context.DeploymentParameterValues .FirstOrDefault(existing => existing.DeploymentGroupId == id && existing.Id == parameterValueId); if (parameterValue == null) return NotFound(); _context.DeploymentParameterValues.Remove(parameterValue); _context.SaveChanges(); return NoContent(); } [HttpPost("{id}/target-assignments")] [Authorize(Policy = ApiPolicies.DeploymentWrite)] [ProducesResponseType(200, Type = typeof(Guid))] [ProducesResponseType(400)] [ProducesResponseType(404)] public IActionResult AddTargetAssignment(Guid id, [FromBody] AddDeploymentTargetAssignmentDto targetAssignment) { if (!_deploymentBatchInterface.CheckDeploymentBatchById(id)) return NotFound(); if (!_context.Targets.Any(target => target.Id == targetAssignment.TargetId)) { ModelState.AddModelError(nameof(targetAssignment.TargetId), "Target target was not found."); return BadRequest(ModelState); } if (_context.DeploymentTargetAssignments.Any(existing => existing.DeploymentGroupId == id && existing.TargetId == targetAssignment.TargetId && existing.RoleKey == targetAssignment.RoleKey)) { ModelState.AddModelError(nameof(targetAssignment.TargetId), "Target assignment already exists for this role."); return BadRequest(ModelState); } try { targetAssignment.NodeDataJson = ConfigurationDocumentValidator.NormalizeAndValidate( targetAssignment.NodeDataJson, ConfigurationDocumentKind.Metadata).JsonData; } catch (ConfigurationDocumentValidationException ex) { ModelState.AddModelError(nameof(targetAssignment.NodeDataJson), ex.Message); return BadRequest(ModelState); } var sortOrder = targetAssignment.SortOrder > 0 ? targetAssignment.SortOrder : GetNextTargetAssignmentSortOrder(id); var model = _mapper.Map(targetAssignment); model.Id = Guid.NewGuid(); model.DeploymentGroupId = id; model.SortOrder = sortOrder; _context.DeploymentTargetAssignments.Add(model); _context.SaveChanges(); return Ok(model.Id); } [HttpDelete("{id}/target-assignments/{targetAssignmentId}")] [Authorize(Policy = ApiPolicies.DeploymentWrite)] [ProducesResponseType(204)] [ProducesResponseType(404)] public IActionResult DeleteTargetAssignment(Guid id, Guid targetAssignmentId) { var targetAssignment = _context.DeploymentTargetAssignments .FirstOrDefault(existing => existing.DeploymentGroupId == id && existing.Id == targetAssignmentId); if (targetAssignment == null) return NotFound(); _context.DeploymentTargetAssignments.Remove(targetAssignment); _context.SaveChanges(); return NoContent(); } private GetDeploymentCompositionDto LoadComposition(Guid deploymentBatchId) { return new GetDeploymentCompositionDto { DeploymentBatchId = deploymentBatchId, TemplateSelections = _mapper.Map>( _context.DeploymentTemplateSelections .AsNoTracking() .Include(selection => selection.TemplateVersion) .Where(selection => selection.DeploymentGroupId == deploymentBatchId) .OrderBy(selection => selection.SortOrder) .ToList()), ParameterValues = _mapper.Map>( _context.DeploymentParameterValues .AsNoTracking() .Where(parameterValue => parameterValue.DeploymentGroupId == deploymentBatchId) .OrderBy(parameterValue => parameterValue.Name) .ToList()), TargetAssignments = _mapper.Map>( _context.DeploymentTargetAssignments .AsNoTracking() .Include(target => target.Target) .Where(target => target.DeploymentGroupId == deploymentBatchId) .OrderBy(target => target.SortOrder) .ToList()) }; } private int GetNextTemplateSelectionSortOrder(Guid deploymentBatchId) { var maxSortOrder = _context.DeploymentTemplateSelections .Where(selection => selection.DeploymentGroupId == deploymentBatchId) .Select(selection => (int?)selection.SortOrder) .Max() ?? 0; return maxSortOrder + 10; } private int GetNextTargetAssignmentSortOrder(Guid deploymentBatchId) { var maxSortOrder = _context.DeploymentTargetAssignments .Where(target => target.DeploymentGroupId == deploymentBatchId) .Select(target => (int?)target.SortOrder) .Max() ?? 0; return maxSortOrder + 10; } } }