// Copyright Epic Games, Inc. All Rights Reserved. using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using EpicGames.Core; using EpicGames.Horde.Agents.Pools; using EpicGames.Horde.Compute; using Horde.Server.Acls; using Horde.Server.Agents; using Horde.Server.Server; using Horde.Server.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace Horde.Server.Compute { /// /// Controller for the /api/v2/compute endpoint /// [ApiController] [Authorize] [Route("[controller]")] public class ComputeControllerV2 : HordeControllerBase { readonly ComputeService _computeService; readonly IOptionsSnapshot _globalConfig; /// /// Constructor /// public ComputeControllerV2(ComputeService computeService, IOptionsSnapshot globalConfig) { _computeService = computeService; _globalConfig = globalConfig; } /// /// Add tasks to be executed remotely /// /// Id of the compute cluster /// The request parameters /// Cancellation token for the operation /// [HttpPost] [Authorize] [Route("/api/v2/compute/{clusterId}")] public async Task> AssignComputeResourceAsync(ClusterId clusterId, [FromBody] AssignComputeRequest request, CancellationToken cancellationToken) { if (!_globalConfig.Value.TryGetComputeCluster(clusterId, out ComputeClusterConfig? clusterConfig)) { return NotFound(clusterId); } if (!clusterConfig.Authorize(ComputeAclAction.AddComputeTasks, User)) { return Forbid(ComputeAclAction.AddComputeTasks, clusterId); } AllocateResourceParams arp = new(clusterId, (ComputeProtocol)request.Protocol, request.Requirements) { RequestId = request.RequestId, RequesterIp = HttpContext.Connection.RemoteIpAddress, ParentLeaseId = User.GetLeaseClaim(), Ports = request.Connection?.Ports ?? new Dictionary(), ConnectionMode = request.Connection?.ModePreference, RequesterPublicIp = request.Connection?.ClientPublicIp, UsePublicIp = request.Connection?.PreferPublicIp, Encryption = ComputeService.ConvertEncryptionToProto(request.Connection?.Encryption) }; ComputeResource? computeResource; try { computeResource = await _computeService.TryAllocateResourceAsync(arp, cancellationToken); if (computeResource == null) { return StatusCode((int)HttpStatusCode.ServiceUnavailable, "No resources available"); } } catch (ComputeServiceException cse) { return cse.ShowToUser ? StatusCode((int)HttpStatusCode.InternalServerError, cse.Message) : StatusCode((int)HttpStatusCode.InternalServerError); } Dictionary responsePorts = new (); foreach ((string name, ComputeResourcePort crp) in computeResource.Ports) { responsePorts[name] = new ConnectionMetadataPort(crp.Port, crp.AgentPort); } AssignComputeResponse response = new AssignComputeResponse(); response.Ip = computeResource.Ip.ToString(); response.Port = computeResource.Ports[ConnectionMetadataPort.ComputeId].Port; response.ConnectionMode = computeResource.ConnectionMode; response.ConnectionAddress = computeResource.ConnectionAddress; response.Ports = responsePorts; response.Encryption = ComputeService.ConvertEncryptionFromProto(computeResource.Task.Encryption); response.Nonce = StringUtils.FormatHexString(computeResource.Task.Nonce.Span); response.Key = StringUtils.FormatHexString(computeResource.Task.Key.Span); response.Certificate = StringUtils.FormatHexString(computeResource.Task.Certificate.Span); response.AgentId = computeResource.AgentId; response.LeaseId = computeResource.LeaseId; response.Properties = computeResource.Properties; response.Protocol = computeResource.Task.Protocol; foreach (KeyValuePair pair in computeResource.Task.Resources) { response.AssignedResources.Add(pair.Key, pair.Value); } return response; } /// /// Get current resource needs for active sessions /// /// ID of the compute cluster /// List of resource needs [HttpGet] [Authorize] [Route("/api/v2/compute/{clusterId}/resource-needs")] public async Task> GetResourceNeedsAsync(ClusterId clusterId) { if (!_globalConfig.Value.TryGetComputeCluster(clusterId, out ComputeClusterConfig? clusterConfig)) { return NotFound(clusterId); } if (!clusterConfig.Authorize(ComputeAclAction.GetComputeTasks, User)) { return Forbid(ComputeAclAction.GetComputeTasks, clusterId); } List resourceNeeds = (await _computeService.GetResourceNeedsAsync()) .Where(x => x.ClusterId == clusterId.ToString()) .OrderBy(x => x.Timestamp) .Select(x => new ResourceNeedsMessage { SessionId = x.SessionId, Pool = x.Pool, ResourceNeeds = x.ResourceNeeds }) .ToList(); return new GetResourceNeedsResponse { ResourceNeeds = resourceNeeds }; } /// /// Declare resource needs for a session to help server calculate current demand /// for resource name property names /// /// Id of the compute cluster /// Resource needs request /// [HttpPost] [Authorize] [Route("/api/v2/compute/{clusterId}/resource-needs")] public async Task> SetResourceNeedsAsync(ClusterId clusterId, [FromBody] ResourceNeedsMessage request) { if (!_globalConfig.Value.TryGetComputeCluster(clusterId, out ComputeClusterConfig? clusterConfig)) { return NotFound(clusterId); } if (!clusterConfig.Authorize(ComputeAclAction.AddComputeTasks, User)) { return Forbid(ComputeAclAction.AddComputeTasks, clusterId); } await _computeService.SetResourceNeedsAsync(clusterId, request.SessionId, new PoolId(request.Pool).ToString(), request.ResourceNeeds); return Ok(new { message = "Resource needs set" }); } } }