// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.Core; using Horde.Build.Acls; using Horde.Build.Utilities; using HordeCommon; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.IO; using System.Linq; using System.Threading.Tasks; namespace Horde.Build.Tools { using ToolId = StringId; using ToolDeploymentId = ObjectId; /// /// Response from creating a deployment /// public class CreateDeploymentResponse { /// /// Identifier for the created deployment /// public ToolDeploymentId Id { get; set; } } /// /// Update an existing deployment /// public class UpdateDeploymentRequest { /// /// New state for the deployment /// public ToolDeploymentState? State { get; set; } } /// /// Action for a deployment /// public enum GetToolAction { /// /// Query for information about the deployment /// Info, /// /// Download the deployment data /// Download, } /// /// Controller for the /api/v1/agents endpoint /// [ApiController] [Authorize] [Route("[controller]")] public class ToolsController : HordeControllerBase { readonly AclService _aclService; readonly ToolCollection _toolCollection; /// /// Constructor /// public ToolsController(AclService aclService, ToolCollection toolCollection) { _aclService = aclService; _toolCollection = toolCollection; } /// /// Create a new deployment of the given tool. /// /// Information about the registered agent [HttpPost] [Route("/api/v1/tools/{id}/deployments")] public async Task> CreateDeploymentAsync(ToolId id, [FromForm] ToolDeploymentOptions options, [FromForm] IFormFile file) { Tool? tool = await _toolCollection.GetAsync(id); if (tool == null) { return NotFound(id); } if (!await tool.AuthorizeAsync(AclAction.UploadTool, User, _aclService, null)) { return Forbid(AclAction.UploadTool, id); } using (Stream stream = file.OpenReadStream()) { tool = await _toolCollection.CreateDeploymentAsync(tool, options, stream); if (tool == null) { return NotFound(id); } } return new CreateDeploymentResponse { Id = tool.Deployments[^1].Id }; } /// /// Register an agent to perform remote work. /// /// Information about the registered agent [HttpGet] [Route("/api/v1/tools/{id}/deployments/{deploymentId}")] public async Task GetDeploymentAsync(ToolId id, ToolDeploymentId deploymentId, [FromQuery] GetToolAction action = GetToolAction.Info) { Tool? tool = await _toolCollection.GetAsync(id); if (tool == null) { return NotFound(id); } if (!await tool.AuthorizeAsync(AclAction.DownloadTool, User, _aclService, null)) { return Forbid(AclAction.DownloadTool, id); } ToolDeployment? deployment = tool.Deployments.FirstOrDefault(x => x.Id == deploymentId); if(deployment == null) { return NotFound(id, deploymentId); } return await GetDeploymentResponseAsync(tool, deployment, action); } private async Task GetDeploymentResponseAsync(Tool tool, ToolDeployment deployment, GetToolAction action) { if (action == GetToolAction.Info) { return Ok(deployment); } else { Stream stream = await _toolCollection.GetDeploymentPayloadAsync(tool, deployment); return new FileStreamResult(stream, "application/zip"); } } /// /// Updates the state of an active deployment. /// [HttpPatch] [Route("/api/v1/tools/{id}/deployments/{deploymentId}")] public async Task UpdateDeploymentAsync(ToolId id, ToolDeploymentId deploymentId, [FromBody] UpdateDeploymentRequest request) { Tool? tool = await _toolCollection.GetAsync(id); if (tool == null) { return NotFound(id); } if (!await tool.AuthorizeAsync(AclAction.UploadTool, User, _aclService, null)) { return Forbid(AclAction.UploadTool, id); } if (request.State != null) { tool = await _toolCollection.UpdateDeploymentAsync(tool, deploymentId, request.State.Value); if (tool == null) { return NotFound(id, deploymentId); } } return Ok(); } } /// /// Public methods available without authorization (or with very custom authorization) /// [ApiController] public class PublicToolsController : HordeControllerBase { readonly AclService _aclService; readonly ToolCollection _toolCollection; readonly IClock _clock; /// /// Constructor /// public PublicToolsController(AclService aclService, ToolCollection toolCollection, IClock clock) { _aclService = aclService; _toolCollection = toolCollection; _clock = clock; } /// /// Register an agent to perform remote work. /// /// Information about the registered agent [HttpGet] [Route("/api/v1/tools/{id}")] public async Task GetToolAsync(ToolId id, GetToolAction action = GetToolAction.Info) { Tool? tool = await _toolCollection.GetAsync(id); if (tool == null) { return NotFound(id); } if (!await AuthorizeDownloadAsync(tool)) { return Forbid(AclAction.DownloadTool, id); } if (action == GetToolAction.Info) { return Ok(tool); } else { if (tool.Deployments.Count == 0) { return NotFound(LogEvent.Create(LogLevel.Error, "Tool {ToolId} does not currently have any deployments", id)); } return await GetDeploymentPayloadAsync(tool, tool.Deployments[^1]); } } /// /// Register an agent to perform remote work. /// /// The tool identifier /// Value indicating the client's preference for deployment to receive. /// Information about the returned deployment /// Information about the registered agent [HttpGet] [Route("/api/v1/tools/{id}/deployments")] public async Task FindDeploymentAsync(ToolId id, [FromQuery] double phase = 0.0, [FromQuery] GetToolAction action = GetToolAction.Info) { Tool? tool = await _toolCollection.GetAsync(id); if (tool == null) { return NotFound(id); } if (!await AuthorizeDownloadAsync(tool)) { return Forbid(AclAction.DownloadTool, id); } if (tool.Deployments.Count == 0) { return NotFound(LogEvent.Create(LogLevel.Error, "Tool {ToolId} does not currently have any deployments", id)); } DateTime utcNow = _clock.UtcNow; int idx = tool.Deployments.Count - 1; for (; idx > 0; idx--) { if (phase < tool.Deployments[idx].GetProgressValue(utcNow)) { break; } } if (action == GetToolAction.Info) { return Ok(tool.Deployments[idx]); } else { return await GetDeploymentPayloadAsync(tool, tool.Deployments[idx]); } } async Task GetDeploymentPayloadAsync(Tool tool, ToolDeployment deployment) { Stream stream = await _toolCollection.GetDeploymentPayloadAsync(tool, deployment); return new FileStreamResult(stream, deployment.MimeType); } async ValueTask AuthorizeDownloadAsync(Tool tool) { if (!tool.Public) { if (User.Identity == null || !User.Identity.IsAuthenticated) { return false; } if (!await tool.AuthorizeAsync(AclAction.DownloadTool, User, _aclService, null)) { return false; } } return true; } } }