using AutoMapper; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.SelfService.Portal.Core.API.Context; using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentArtifact.Add; using Microsoft.SelfService.Portal.Core.API.Dto.DeploymentArtifact.Get; using Microsoft.SelfService.Portal.Core.API.JsonDocuments; using Microsoft.SelfService.Portal.Core.API.Models; using System.Text.Json; namespace Microsoft.SelfService.Portal.Core.API.Controllers { [Route("api/deployment-artifacts")] [ApiController] public class DeploymentArtifactController : Controller { private readonly DataContext _context; private readonly IMapper _mapper; public DeploymentArtifactController(DataContext context, IMapper mapper) { _context = context; _mapper = mapper; } [HttpGet] [ProducesResponseType(200, Type = typeof(IEnumerable))] public IActionResult GetDeploymentArtifacts([FromQuery] Guid? deploymentGroupId, [FromQuery] Guid? targetId) { var query = _context.DeploymentArtifacts .AsNoTracking() .AsQueryable(); if (deploymentGroupId.HasValue) query = query.Where(artifact => artifact.DeploymentGroupId == deploymentGroupId.Value); if (targetId.HasValue) query = query.Where(artifact => artifact.TargetId == targetId.Value); var artifacts = query .OrderByDescending(artifact => artifact.Created) .ToList(); return Ok(_mapper.Map>(artifacts)); } [HttpGet("{id}")] [ProducesResponseType(200, Type = typeof(GetDeploymentArtifactDto))] [ProducesResponseType(404)] public IActionResult GetDeploymentArtifactById(Guid id) { var artifact = _context.DeploymentArtifacts .AsNoTracking() .FirstOrDefault(existing => existing.Id == id); if (artifact == null) return NotFound(); return Ok(_mapper.Map(artifact)); } [HttpPost] [ProducesResponseType(200, Type = typeof(Guid))] [ProducesResponseType(400)] public IActionResult AddDeploymentArtifact([FromBody] AddDeploymentArtifactDto deploymentArtifact) { if (deploymentArtifact == null) return BadRequest(ModelState); if (!_context.DeploymentGroups.Any(group => group.Id == deploymentArtifact.DeploymentGroupId)) { ModelState.AddModelError(nameof(deploymentArtifact.DeploymentGroupId), "Deployment group was not found."); return BadRequest(ModelState); } if (deploymentArtifact.TargetId.HasValue && !_context.Targets.Any(target => target.Id == deploymentArtifact.TargetId.Value)) { ModelState.AddModelError(nameof(deploymentArtifact.TargetId), "Target was not found."); return BadRequest(ModelState); } try { deploymentArtifact.DeploymentJson = NormalizeJson(deploymentArtifact.DeploymentJson); deploymentArtifact.SourceJson = NormalizeOptionalJson(deploymentArtifact.SourceJson); deploymentArtifact.ResolvedJson = NormalizeOptionalJson(deploymentArtifact.ResolvedJson); deploymentArtifact.SourceSnapshotJson = NormalizeOptionalJson(deploymentArtifact.SourceSnapshotJson); } catch (ConfigurationDocumentValidationException ex) { ModelState.AddModelError(nameof(deploymentArtifact.DeploymentJson), ex.Message); return BadRequest(ModelState); } var model = _mapper.Map(deploymentArtifact); model.Id = Guid.NewGuid(); model.InputHash = TemplateVersionModel.ComputeSha256(model.SourceJson ?? model.DeploymentJson); model.OutputHash = TemplateVersionModel.ComputeSha256(model.ResolvedJson ?? model.DeploymentJson); model.IsStale = false; _context.DeploymentArtifacts.Add(model); if (model.TargetId.HasValue) { var deployment = _context.Deployments.FirstOrDefault(existing => existing.DeploymentGroupId == model.DeploymentGroupId && existing.TargetId == model.TargetId.Value); if (deployment != null) { deployment.CurrentArtifactId = model.Id; deployment.SourceJson = model.SourceJson; deployment.JSONData = model.DeploymentJson; } } _context.SaveChanges(); return Ok(model.Id); } [HttpPost("{id}/stale-check")] [ProducesResponseType(200, Type = typeof(GetDeploymentArtifactDto))] [ProducesResponseType(404)] public IActionResult CheckDeploymentArtifactStale(Guid id) { var artifact = _context.DeploymentArtifacts.FirstOrDefault(existing => existing.Id == id); if (artifact == null) return NotFound(); var staleReasons = BuildStaleReasons(artifact); artifact.IsStale = staleReasons.Count > 0; artifact.StaleReasonJson = staleReasons.Count > 0 ? JsonSerializer.Serialize(staleReasons) : null; _context.SaveChanges(); return Ok(_mapper.Map(artifact)); } private static string NormalizeJson(string json) { return ConfigurationDocumentValidator.NormalizeAndValidate( string.IsNullOrWhiteSpace(json) ? "{}" : json, ConfigurationDocumentKind.Metadata).JsonData; } private static string? NormalizeOptionalJson(string? json) { return string.IsNullOrWhiteSpace(json) ? null : NormalizeJson(json); } private List BuildStaleReasons(DeploymentArtifactModel artifact) { var reasons = new List(); if (string.IsNullOrWhiteSpace(artifact.SourceSnapshotJson)) { return reasons; } using var snapshot = JsonDocument.Parse(artifact.SourceSnapshotJson); if (!snapshot.RootElement.TryGetProperty("templateRevisions", out var revisions) || revisions.ValueKind != JsonValueKind.Array) { return reasons; } foreach (var source in revisions.EnumerateArray()) { if (!source.TryGetProperty("id", out var idElement) || !Guid.TryParse(idElement.GetString(), out var revisionId) || !source.TryGetProperty("hash", out var hashElement)) { continue; } var current = _context.TemplateRevisions .AsNoTracking() .FirstOrDefault(revision => revision.Id == revisionId); if (current == null) { reasons.Add(new { Type = "TemplateRevisionMissing", RevisionId = revisionId }); continue; } var expectedHash = hashElement.GetString(); if (!string.Equals(current.JsonHash, expectedHash, StringComparison.OrdinalIgnoreCase)) { reasons.Add(new { Type = "TemplateRevisionHashChanged", RevisionId = revisionId, OldHash = expectedHash, CurrentHash = current.JsonHash }); } } return reasons; } } }