// Copyright Epic Games, Inc. All Rights Reserved. using Datadog.Trace; using EpicGames.Core; using HordeServer.Api; using HordeServer.Collections; using HordeCommon; using HordeServer.Models; using HordeServer.Services; using HordeServer.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MongoDB.Bson; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Security.Claims; using System.Text.RegularExpressions; using System.Threading.Tasks; using StreamId = HordeServer.Utilities.StringId; using TemplateRefId = HordeServer.Utilities.StringId; using Microsoft.Extensions.Logging; using HordeServer.Notifications; namespace HordeServer.Controllers { /// /// Controller for the /api/v1/jobs endpoing /// [ApiController] [Authorize] [Route("[controller]")] public class JobsController : ControllerBase { /// /// Instance of the ACL service /// private readonly AclService AclService; /// /// Collection of graphs /// private readonly IGraphCollection Graphs; /// /// The perforce service instance /// private readonly IPerforceService Perforce; /// /// Instance of the stream service /// private readonly StreamService StreamService; /// /// Instance of the JobService singleton /// private readonly JobService JobService; /// /// Instance of the TemplateService singleton /// private readonly TemplateService TemplateService; /// /// /// private readonly ArtifactService ArtifactService; /// /// Instance of the notification service singleton /// private readonly INotificationService NotificationService; /// /// Logger instance /// private ILogger Logger; /// /// Constructor /// /// Instance of the ACL service /// Collection of graph documents /// The Perforce service instance /// Instance of the stream service /// Instance of the JobService singleton /// Instance of the TemplateService singleton /// Instance of the ArtifactService singleton /// Instance of the NotificationService singelton /// The logger instance public JobsController(AclService AclService, IGraphCollection Graphs, IPerforceService Perforce, StreamService StreamService, JobService JobService, TemplateService TemplateService, ArtifactService ArtifactService, INotificationService NotificationService, ILogger Logger) { this.AclService = AclService; this.Graphs = Graphs; this.Perforce = Perforce; this.StreamService = StreamService; this.JobService = JobService; this.TemplateService = TemplateService; this.ArtifactService = ArtifactService; this.NotificationService = NotificationService; this.Logger = Logger; } /// /// Creates a new job /// /// Properties of the new job /// Id of the new job [HttpPost] [Route("/api/v1/jobs")] public async Task> CreateJobAsync([FromBody] CreateJobRequest Create) { IStream? Stream = await StreamService.GetStreamAsync(new StreamId(Create.StreamId)); if (Stream == null) { return BadRequest("Invalid StreamId parameter"); } if (!await StreamService.AuthorizeAsync(Stream, AclAction.CreateJob, User, null)) { return Forbid(); } // Get the name of the template ref TemplateRefId TemplateRefId = new TemplateRefId(Create.TemplateId); // Augment the request with template properties TemplateRef? TemplateRef; if (!Stream.Templates.TryGetValue(TemplateRefId, out TemplateRef)) { return BadRequest($"Invalid {Create.TemplateId} parameter"); } if (!await StreamService.AuthorizeAsync(Stream, TemplateRef, AclAction.CreateJob, User, null)) { return Forbid(); } ITemplate? Template = await TemplateService.GetTemplateAsync(TemplateRef.Hash); if (Template == null) { return BadRequest($"Missing template referenced by {Create.TemplateId}"); } if (!Template.AllowPreflights && Create.PreflightChange > 0) { return BadRequest("Template does not allow preflights"); } // Get the name of the new job string Name = Create.Name ?? Template.Name; if (Create.TemplateId.Equals("stage-to-marketplace", StringComparison.Ordinal) && Create.Arguments != null) { foreach (string Argument in Create.Arguments) { const string Prefix = "-set:UserContentItems="; if (Argument.StartsWith(Prefix, StringComparison.Ordinal)) { Name += $" - {Argument.Substring(Prefix.Length)}"; break; } } } // Get the priority of the new job Priority Priority = Create.Priority ?? Template.Priority ?? Priority.Normal; // New groups for the job IGraph Graph = await Graphs.AddAsync(Template); if (Create.Groups != null) { Graph = await Graphs.AppendAsync(Graph, Create.Groups, null, null); } if (Create.Aggregates != null) { Graph = await Graphs.AppendAsync(Graph, null, Create.Aggregates, null); } if (Create.Labels != null) { Graph = await Graphs.AppendAsync(Graph, null, null, Create.Labels); } // Get the change to build int Change; if (Create.Change.HasValue) { Change = Create.Change.Value; } else if (Create.ChangeQuery != null) { Change = await ExecuteChangeQueryAsync(Stream, new TemplateRefId(Create.ChangeQuery.TemplateId ?? Create.TemplateId), Create.ChangeQuery.Target, Create.ChangeQuery.Outcomes ?? new List { JobStepOutcome.Success }); } else if (Create.PreflightChange == null && Template.SubmitNewChange != null) { Change = await Perforce.CreateNewChangeForTemplateAsync(Stream, Template); } else { Change = await Perforce.GetLatestChangeAsync(Stream.Name, null); } // And get the matching code changelist int CodeChange = await Perforce.GetCodeChangeAsync(Stream.Name, Change); // New properties for the job List Arguments = Create.Arguments ?? Template.GetDefaultArguments(); // Create the job IJob Job = await JobService.CreateJobAsync(null, Stream.Id, TemplateRefId, Template.Id, Graph, Name, Change, CodeChange, Create.PreflightChange, null, User.GetUserId(), User.GetUserName(), Priority, Create.AutoSubmit, Create.UpdateIssues, TemplateRef.ChainedJobs, TemplateRef.ShowUgsBadges, TemplateRef.ShowUgsAlerts, TemplateRef.NotificationChannel, TemplateRef.NotificationChannelFilter, null, Template.Counters, Arguments); await UpdateNotificationsAsync(Job.Id.ToString(), new UpdateNotificationsRequest { Slack = true }); return new CreateJobResponse(Job.Id.ToString()); } /// /// Evaluate a change query to determine which CL to run a job at /// /// /// /// /// /// async Task ExecuteChangeQueryAsync(IStream Stream, TemplateRefId TemplateId, string? Target, List Outcomes) { IList Jobs = await JobService.FindJobsAsync(StreamId: Stream.Id, Templates: new[] { TemplateId }, Target: Target, State: new[] { JobStepState.Completed }, Outcome: Outcomes.ToArray(), Count: 1); if (Jobs.Count == 0) { Logger.LogInformation("Unable to find successful build of {TemplateRefId} target {Target}. Using latest change instead", TemplateId, Target); return await Perforce.GetLatestChangeAsync(Stream.Name, null); } else { Logger.LogInformation("Last successful build of {TemplateRefId} target {Target} was job {JobId} at change {Change}", TemplateId, Target, Jobs[0].Id, Jobs[0].Change); return Jobs[0].Change; } } /// /// Deletes a specific job. /// /// Id of the job to delete /// Async task [HttpDelete] [Route("/api/v1/jobs/{JobId}")] public async Task DeleteJobAsync(string JobId) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.DeleteJob, User, null)) { return Forbid(); } if (!await JobService.DeleteJobAsync(Job)) { return NotFound(); } return Ok(); } /// /// Updates a specific job. /// /// Id of the job to find /// Settings to update in the job /// Async task [HttpPut] [Route("/api/v1/jobs/{JobId}")] public async Task UpdateJobAsync(string JobId, [FromBody] UpdateJobRequest Request) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } StreamPermissionsCache PermissionsCache = new StreamPermissionsCache(); if (!await JobService.AuthorizeAsync(Job, AclAction.UpdateJob, User, PermissionsCache)) { return Forbid(); } if (Request.Acl != null && !await JobService.AuthorizeAsync(Job, AclAction.ChangePermissions, User, PermissionsCache)) { return Forbid(); } // Convert legacy behavior of clearing out the argument to setting the aborted flag if (Request.Arguments != null && Request.Arguments.Count == 0) { Request.Aborted = true; Request.Arguments = null; } string? AbortedByUser = null; if (Request.Aborted ?? false) { AbortedByUser = User.Identity.Name; } if (!await JobService.UpdateJobAsync(Job, Name: Request.Name, Priority: Request.Priority, AutoSubmit: Request.AutoSubmit, AbortedByUser: AbortedByUser, Arguments: Request.Arguments)) { return NotFound(); } return Ok(); } /// /// Updates notifications for a specific job. /// /// Id of the job to find /// The notification request /// Information about the requested job [HttpPut] [Route("/api/v1/jobs/{JobId}/notifications")] public async Task UpdateNotificationsAsync(string JobId, [FromBody] UpdateNotificationsRequest Request) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.CreateSubscription, User, null)) { return Forbid(); } ObjectId TriggerId = Job.NotificationTriggerId ?? ObjectId.GenerateNewId(); if (!await JobService.UpdateJobAsync(Job, null, null, null, null, TriggerId, null, null)) { return NotFound(); } await NotificationService.UpdateSubscriptionsAsync(TriggerId, User, Request.Email, Request.Slack); return Ok(); } /// /// Gets information about a specific job. /// /// Id of the job to find /// Information about the requested job [HttpGet] [Route("/api/v1/jobs/{JobId}/notifications")] public async Task> GetNotificationsAsync(string JobId) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.CreateSubscription, User, null)) { return Forbid(); } INotificationSubscription? Subscription; if (Job.NotificationTriggerId == null) { Subscription = null; } else { Subscription = await NotificationService.GetSubscriptionsAsync(Job.NotificationTriggerId.Value, User); } return new GetNotificationResponse(Subscription); } /// /// Gets information about a specific job. /// /// Id of the job to find /// If specified, returns an empty response unless the job's update time is equal to or less than the given value /// Filter for the fields to return /// Information about the requested job [HttpGet] [Route("/api/v1/jobs/{JobId}")] [ProducesResponseType(typeof(GetJobResponse), 200)] public async Task> GetJobAsync(string JobId, [FromQuery] DateTimeOffset? ModifiedAfter = null, [FromQuery] PropertyFilter? Filter = null) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } StreamPermissionsCache Cache = new StreamPermissionsCache(); if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, Cache)) { return Forbid(); } if (ModifiedAfter != null && Job.UpdateTimeUtc <= ModifiedAfter.Value) { return new Dictionary(); } IGraph Graph = await JobService.GetGraphAsync(Job); bool bIncludeAcl = await JobService.AuthorizeAsync(Job, AclAction.ViewPermissions, User, Cache); return Job.ToResponse(Graph, bIncludeAcl, Filter); } /// /// Gets information about the graph for a specific job. /// /// Id of the job to find /// Filter for the fields to return /// Information about the requested job [HttpGet] [Route("/api/v1/jobs/{JobId}/graph")] [ProducesResponseType(typeof(GetGraphResponse), 200)] public async Task> GetJobGraphAsync(string JobId, [FromQuery] PropertyFilter? Filter = null) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); return PropertyFilter.Apply(new GetGraphResponse(Graph), Filter); } /// /// Gets timing information about the graph for a specific job. /// /// Id of the job to find /// Filter for the fields to return /// Information about the requested job [HttpGet] [Route("/api/v1/jobs/{JobId}/timing")] [ProducesResponseType(typeof(GetJobTimingResponse), 200)] public async Task> GetJobTimingAsync(string JobId, [FromQuery] PropertyFilter? Filter = null) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IJobTiming JobTiming = await JobService.GetJobTimingAsync(Job); IGraph Graph = await JobService.GetGraphAsync(Job); Dictionary NodeToTimingInfo = Job.GetTimingInfo(Graph, JobTiming); Dictionary Steps = new Dictionary(); foreach (IJobStepBatch Batch in Job.Batches) { foreach (IJobStep Step in Batch.Steps) { INode Node = Graph.Groups[Batch.GroupIdx].Nodes[Step.NodeIdx]; Steps[Step.Id.ToString()] = new GetStepTimingInfoResponse(Node.Name, NodeToTimingInfo[Node]); } } List Labels = new List(); foreach (ILabel Label in Graph.Labels) { TimingInfo TimingInfo = TimingInfo.Max(Label.GetDependencies(Graph.Groups).Select(x => NodeToTimingInfo[x])); Labels.Add(new GetLabelTimingInfoResponse(Label, TimingInfo)); } return PropertyFilter.Apply(new GetJobTimingResponse(Steps, Labels), Filter); } /// /// Gets information about the template for a specific job. /// /// Id of the job to find /// Filter for the fields to return /// Information about the requested job [HttpGet] [Route("/api/v1/jobs/{JobId}/template")] [ProducesResponseType(typeof(GetTemplateResponse), 200)] public async Task> GetJobTemplateAsync(string JobId, [FromQuery] PropertyFilter? Filter = null) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null || Job.TemplateHash == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } ITemplate? Template = await TemplateService.GetTemplateAsync(Job.TemplateHash); if(Template == null) { return NotFound(); } return new GetTemplateResponse(Template).ApplyFilter(Filter); } /// /// Find jobs matching a criteria /// /// The job ids to return /// Name of the job to find /// List of templates to find /// The stream to search for /// The minimum changelist number /// The maximum changelist number /// Whether to include preflight jobs /// The preflighted changelist /// User id for which to include preflight jobs /// Minimum creation time /// Maximum creation time /// If specified, only jobs updated before the give time will be returned /// If specified, only jobs updated after the give time will be returned /// Target to filter the returned jobs by /// Filter state of the returned jobs /// Filter outcome of the returned jobs /// Filter for properties to return /// Index of the first result to be returned /// Number of results to return /// List of jobs [HttpGet] [Route("/api/v1/jobs")] [ProducesResponseType(typeof(List), 200)] public async Task>> FindJobsAsync( [FromQuery(Name = "Id")] string[]? Ids = null, [FromQuery] string? StreamId = null, [FromQuery] string? Name = null, [FromQuery(Name = "template")] string[]? Templates = null, [FromQuery] int? MinChange = null, [FromQuery] int? MaxChange = null, [FromQuery] bool IncludePreflight = true, [FromQuery] int? PreflightChange = null, [FromQuery] string? PreflightStartedByUserId = null, [FromQuery] DateTimeOffset? MinCreateTime = null, [FromQuery] DateTimeOffset? MaxCreateTime = null, [FromQuery] DateTimeOffset? ModifiedBefore = null, [FromQuery] DateTimeOffset? ModifiedAfter = null, [FromQuery] string? Target = null, [FromQuery] JobStepState[]? State = null, [FromQuery] JobStepOutcome[]? Outcome = null, [FromQuery] PropertyFilter? Filter = null, [FromQuery] int Index = 0, [FromQuery] int Count = 100) { ObjectId[]? JobIdValues = (Ids == null) ? (ObjectId[]?)null : Array.ConvertAll(Ids, x => x.ToObjectId()); StreamId? StreamIdValue = (StreamId == null)? (StreamId?)null : new StreamId(StreamId); TemplateRefId[]? TemplateRefIds = (Templates != null && Templates.Length > 0) ? Templates.Select(x => new TemplateRefId(x)).ToArray() : null; if (IncludePreflight == false) { PreflightChange = 0; } ObjectId? PreflightStartedByUserIdValue = null; if (PreflightStartedByUserId != null) { PreflightStartedByUserIdValue = new ObjectId(PreflightStartedByUserId); } List Jobs; using (Scope _ = Tracer.Instance.StartActive("FindJobs")) { Jobs = await JobService.FindJobsAsync(JobIdValues, StreamIdValue, Name, TemplateRefIds, MinChange, MaxChange, PreflightChange, PreflightStartedByUserIdValue, MinCreateTime?.UtcDateTime, MaxCreateTime?.UtcDateTime, Target, State, Outcome, ModifiedBefore, ModifiedAfter, Index, Count); } StreamPermissionsCache PermissionsCache = new StreamPermissionsCache(); List Responses = new List(); foreach (IJob Job in Jobs) { using Scope JobScope = Tracer.Instance.StartActive("JobIteration"); JobScope.Span.SetTag("jobId", Job.Id.ToString()); bool ViewJobAuthorized; using (Scope _ = Tracer.Instance.StartActive("AuthorizeViewJob")) { ViewJobAuthorized = await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, PermissionsCache); } if (ViewJobAuthorized) { IGraph Graph; using (Scope _ = Tracer.Instance.StartActive("GetGraph")) { Graph = await JobService.GetGraphAsync(Job); } bool bIncludeAcl; using (Scope _ = Tracer.Instance.StartActive("AuthorizeViewPermissions")) { bIncludeAcl = await JobService.AuthorizeAsync(Job, AclAction.ViewPermissions, User, PermissionsCache); } using (Scope _ = Tracer.Instance.StartActive("CreateResponse")) { Responses.Add(Job.ToResponse(Graph, bIncludeAcl, Filter)); } } } return Responses; } /// /// Adds an array of nodes to be executed for a job /// /// Unique id for the job /// Properties of the new nodes /// Id of the new job [HttpPost] [Route("/api/v1/jobs/{JobId}/groups")] public async Task CreateGroupsAsync(string JobId, [FromBody] List Requests) { Dictionary ExpectedDurationCache = new Dictionary(); ObjectId JobIdValue = JobId.ToObjectId(); for (; ; ) { IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ExecuteJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); Graph = await Graphs.AppendAsync(Graph, Requests, null, null); if (await JobService.TryUpdateGraphAsync(Job, Graph)) { return Ok(); } } } /// /// Gets the nodes to be executed for a job /// /// Unique id for the job /// Filter for the properties to return /// List of nodes to be executed [HttpGet] [Route("/api/v1/jobs/{JobId}/groups")] [ProducesResponseType(typeof(List), 200)] public async Task>> GetGroupsAsync(string JobId, [FromQuery] PropertyFilter? Filter = null) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); return Graph.Groups.ConvertAll(x => new GetGroupResponse(x, Graph.Groups).ApplyFilter(Filter)); } /// /// Gets the nodes in a group to be executed for a job /// /// Unique id for the job /// The group index /// Filter for the properties to return /// List of nodes to be executed [HttpGet] [Route("/api/v1/jobs/{JobId}/groups/{GroupIdx}")] [ProducesResponseType(typeof(GetGroupResponse), 200)] public async Task> GetGroupAsync(string JobId, int GroupIdx, [FromQuery] PropertyFilter? Filter = null) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); if (GroupIdx < 0 || GroupIdx >= Graph.Groups.Count) { return NotFound(); } return new GetGroupResponse(Graph.Groups[GroupIdx], Graph.Groups).ApplyFilter(Filter); } /// /// Gets the nodes for a particular group /// /// Unique id for the job /// Index of the group containing the node to update /// Filter for the properties to return [HttpGet] [Route("/api/v1/jobs/{JobId}/groups/{GroupIdx}/nodes")] [ProducesResponseType(typeof(List), 200)] public async Task>> GetNodesAsync(string JobId, int GroupIdx, [FromQuery] PropertyFilter? Filter = null) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); if (GroupIdx < 0 || GroupIdx >= Graph.Groups.Count) { return NotFound(); } return Graph.Groups[GroupIdx].Nodes.ConvertAll(x => new GetNodeResponse(x, Graph.Groups).ApplyFilter(Filter)); } /// /// Gets a particular node definition /// /// Unique id for the job /// Index of the group containing the node to update /// Index of the node to update /// Filter for the properties to return [HttpGet] [Route("/api/v1/jobs/{JobId}/groups/{GroupIdx}/nodes/{NodeIdx}")] [ProducesResponseType(typeof(GetNodeResponse), 200)] public async Task> GetNodeAsync(string JobId, int GroupIdx, int NodeIdx, [FromQuery] PropertyFilter? Filter = null) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); if (GroupIdx < 0 || GroupIdx >= Graph.Groups.Count || NodeIdx < 0 || NodeIdx >= Graph.Groups[GroupIdx].Nodes.Count) { return NotFound(); } return new GetNodeResponse(Graph.Groups[GroupIdx].Nodes[NodeIdx], Graph.Groups).ApplyFilter(Filter); } /// /// Gets the steps currently scheduled to be executed for a job /// /// Unique id for the job /// Filter for the properties to return /// List of nodes to be executed [HttpGet] [Route("/api/v1/jobs/{JobId}/batches")] [ProducesResponseType(typeof(List), 200)] public async Task>> GetBatchesAsync(string JobId, [FromQuery] PropertyFilter? Filter = null) { ObjectId JobIdValue = JobId.ToObjectId(); IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); List Responses = new List(); foreach (IJobStepBatch Batch in Job.Batches) { Responses.Add(new GetBatchResponse(Batch).ApplyFilter(Filter)); } return Responses; } /// /// Updates the state of a jobstep /// /// Unique id for the job /// Unique id for the step /// Updates to apply to the node [HttpPut] [Route("/api/v1/jobs/{JobId}/batches/{BatchId}")] public async Task UpdateBatchAsync(string JobId, string BatchId, [FromBody] UpdateBatchRequest Request) { ObjectId JobIdValue = JobId.ToObjectId(); SubResourceId BatchIdValue = BatchId.ToSubResourceId(); IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } IJobStepBatch Batch = Job.Batches.FirstOrDefault(x => x.Id == BatchIdValue); if (Batch == null) { return NotFound(); } if (Batch.SessionId == null || !User.HasSessionClaim(Batch.SessionId.Value)) { return Forbid(); } if (!await JobService.UpdateBatchAsync(Job, BatchIdValue, Request.LogId?.ToObjectId(), Request.State)) { return NotFound(); } return Ok(); } /// /// Gets a particular step currently scheduled to be executed for a job /// /// Unique id for the job /// Unique id for the step /// Filter for the properties to return /// List of nodes to be executed [HttpGet] [Route("/api/v1/jobs/{JobId}/batches/{BatchId}")] [ProducesResponseType(typeof(GetBatchResponse), 200)] public async Task> GetBatchAsync(string JobId, string BatchId, [FromQuery] PropertyFilter? Filter = null) { ObjectId JobIdValue = JobId.ToObjectId(); SubResourceId BatchIdValue = BatchId.ToSubResourceId(); IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); foreach (IJobStepBatch Batch in Job.Batches) { if (Batch.Id == BatchIdValue) { return new GetBatchResponse(Batch).ApplyFilter(Filter); } } return NotFound(); } /// /// Gets the steps currently scheduled to be executed for a job /// /// Unique id for the job /// Unique id for the batch /// Filter for the properties to return /// List of nodes to be executed [HttpGet] [Route("/api/v1/jobs/{JobId}/batches/{BatchId}/steps")] [ProducesResponseType(typeof(List), 200)] public async Task>> GetStepsAsync(string JobId, string BatchId, [FromQuery] PropertyFilter? Filter = null) { ObjectId JobIdValue = JobId.ToObjectId(); SubResourceId BatchIdValue = BatchId.ToSubResourceId(); IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); foreach (IJobStepBatch Batch in Job.Batches) { if (Batch.Id == BatchIdValue) { INodeGroup Group = Graph.Groups[Batch.GroupIdx]; List Responses = new List(); foreach (IJobStep Step in Batch.Steps) { Responses.Add(new GetStepResponse(Step).ApplyFilter(Filter)); } return Responses; } } return NotFound(); } /// /// Updates the state of a jobstep /// /// Unique id for the job /// Unique id for the batch /// Unique id for the step /// Updates to apply to the node [HttpPut] [Route("/api/v1/jobs/{JobId}/batches/{BatchId}/steps/{StepId}")] public async Task> UpdateStepAsync(string JobId, string BatchId, string StepId, [FromBody] UpdateStepRequest Request) { ObjectId JobIdValue = JobId.ToObjectId(); SubResourceId BatchIdValue = BatchId.ToSubResourceId(); SubResourceId StepIdValue = StepId.ToSubResourceId(); IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } // Check permissions for updating this step. Only the agent executing the step can modify the state of it. if (Request.State != JobStepState.Unspecified || Request.Outcome != JobStepOutcome.Unspecified) { IJobStepBatch Batch = Job.Batches.FirstOrDefault(x => x.Id == BatchIdValue); if (Batch == null) { return NotFound(); } if (!Batch.SessionId.HasValue || !User.HasSessionClaim(Batch.SessionId.Value)) { return Forbid(); } } if (Request.Retry != null || Request.Priority != null) { if (!await JobService.AuthorizeAsync(Job, AclAction.RetryJobStep, User, null)) { return Forbid(); } } if (Request.Properties != null) { if (!await JobService.AuthorizeAsync(Job, AclAction.UpdateJob, User, null)) { return Forbid(); } } string? RetryByUser = (Request.Retry.HasValue && Request.Retry.Value) ? (User.Identity.Name ?? "Anonymous") : null; string? AbortByUser = (Request.AbortRequested.HasValue && Request.AbortRequested.Value) ? (User.Identity.Name ?? "Anonymous") : null; try { IJob? NewJob = await JobService.UpdateStepAsync(Job, BatchIdValue, StepIdValue, Request.State, Request.Outcome, Request.AbortRequested, AbortByUser, Request.LogId?.ToObjectId(), null, RetryByUser, Request.Priority, null, Request.Properties); if (NewJob == null) { return NotFound(); } UpdateStepResponse Response = new UpdateStepResponse(); if (Request.Retry ?? false) { JobStepRefId? RetriedStepId = FindRetriedStep(Job, BatchIdValue, StepIdValue); if (RetriedStepId != null) { Response.BatchId = RetriedStepId.Value.BatchId.ToString(); Response.StepId = RetriedStepId.Value.StepId.ToString(); } } return Response; } catch (RetryNotAllowedException Ex) { return BadRequest(Ex.Message); } } /// /// Find the first retried step after the given step /// /// The job being run /// Batch id of the last step instance /// Step id of the last instance /// The retried step information static JobStepRefId? FindRetriedStep(IJob Job, SubResourceId BatchId, SubResourceId StepId) { NodeRef? LastNodeRef = null; foreach (IJobStepBatch Batch in Job.Batches) { if ((LastNodeRef == null && Batch.Id == BatchId) || (LastNodeRef != null && Batch.GroupIdx == LastNodeRef.GroupIdx)) { foreach (IJobStep Step in Batch.Steps) { if (LastNodeRef == null && Step.Id == StepId) { LastNodeRef = new NodeRef(Batch.GroupIdx, Step.NodeIdx); } else if (LastNodeRef != null && Step.NodeIdx == LastNodeRef.NodeIdx) { return new JobStepRefId(Job.Id, Batch.Id, Step.Id); } } } } return null; } /// /// Gets a particular step currently scheduled to be executed for a job /// /// Unique id for the job /// Unique id for the batch /// Unique id for the step /// Filter for the properties to return /// List of nodes to be executed [HttpGet] [Route("/api/v1/jobs/{JobId}/batches/{BatchId}/steps/{StepId}")] [ProducesResponseType(typeof(GetStepResponse), 200)] public async Task> GetStepAsync(string JobId, string BatchId, string StepId, [FromQuery] PropertyFilter? Filter = null) { ObjectId JobIdValue = JobId.ToObjectId(); SubResourceId BatchIdValue = BatchId.ToSubResourceId(); SubResourceId StepIdValue = StepId.ToSubResourceId(); IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); foreach (IJobStepBatch Batch in Job.Batches) { if (Batch.Id == BatchIdValue) { foreach (IJobStep Step in Batch.Steps) { if (Step.Id == StepIdValue) { return new GetStepResponse(Step).ApplyFilter(Filter); } } break; } } return NotFound(); } /// /// Updates notifications for a specific job. /// /// Unique id for the job /// Unique id for the batch /// Unique id for the step /// The notification request /// Information about the requested job [HttpPut] [Route("/api/v1/jobs/{JobId}/batches/{BatchId}/steps/{StepId}/notifications")] public async Task UpdateStepNotificationsAsync(string JobId, string BatchId, string StepId, [FromBody] UpdateNotificationsRequest Request) { ObjectId JobIdValue = JobId.ToObjectId(); SubResourceId BatchIdValue = BatchId.ToSubResourceId(); SubResourceId StepIdValue = StepId.ToSubResourceId(); IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.CreateSubscription, User, null)) { return Forbid(); } if (!Job.TryGetBatch(BatchIdValue, out IJobStepBatch? Batch)) { return NotFound(); } if (!Batch.TryGetStep(StepIdValue, out IJobStep? Step)) { return NotFound(); } ObjectId? TriggerId = Step.NotificationTriggerId; if (TriggerId == null) { TriggerId = ObjectId.GenerateNewId(); if (await JobService.UpdateStepAsync(Job, BatchIdValue, StepIdValue, JobStepState.Unspecified, JobStepOutcome.Unspecified, null, null, null, TriggerId, null, null, null) == null) { return NotFound(); } } await NotificationService.UpdateSubscriptionsAsync(TriggerId.Value, User, Request.Email, Request.Slack); return Ok(); } /// /// Gets information about a specific job. /// /// Id of the job to find /// Unique id for the batch /// Unique id for the step /// Information about the requested job [HttpGet] [Route("/api/v1/jobs/{JobId}/batches/{BatchId}/steps/{StepId}/notifications")] public async Task> GetStepNotificationsAsync(string JobId, string BatchId, string StepId) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } IJobStep? Step; if(!Job.TryGetStep(BatchId.ToSubResourceId(), StepId.ToSubResourceId(), out Step)) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.CreateSubscription, User, null)) { return Forbid(); } INotificationSubscription? Subscription; if (Step.NotificationTriggerId == null) { Subscription = null; } else { Subscription = await NotificationService.GetSubscriptionsAsync(Step.NotificationTriggerId.Value, User); } return new GetNotificationResponse(Subscription); } /// /// Gets a particular step currently scheduled to be executed for a job /// /// Unique id for the job /// Unique id for the batch /// Unique id for the step /// /// List of nodes to be executed [HttpGet] [Route("/api/v1/jobs/{JobId}/batches/{BatchId}/steps/{StepId}/artifacts/{*Name}")] public async Task GetArtifactAsync(string JobId, string BatchId, string StepId, string Name) { ObjectId JobIdValue = JobId.ToObjectId(); SubResourceId BatchIdValue = BatchId.ToSubResourceId(); SubResourceId StepIdValue = StepId.ToSubResourceId(); IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } if (!Job.TryGetStep(BatchId.ToSubResourceId(), StepId.ToSubResourceId(), out _)) { return NotFound(); } List Artifacts = await ArtifactService.GetArtifactsAsync(JobIdValue, StepIdValue, Name); if (Artifacts.Count == 0) { return NotFound(); } Artifact Artifact = Artifacts[0]; return new FileStreamResult(ArtifactService.OpenArtifactReadStream(Artifact), Artifact.MimeType); } /// /// Gets a particular step currently scheduled to be executed for a job /// /// Unique id for the job /// Unique id for the batch /// Unique id for the step /// List of nodes to be executed [HttpGet] [Route("/api/v1/jobs/{JobId}/batches/{BatchId}/steps/{StepId}/trace")] public async Task GetStepTraceAsync(string JobId, string BatchId, string StepId) { ObjectId JobIdValue = JobId.ToObjectId(); SubResourceId BatchIdValue = BatchId.ToSubResourceId(); SubResourceId StepIdValue = StepId.ToSubResourceId(); IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } if (!Job.TryGetStep(BatchId.ToSubResourceId(), StepId.ToSubResourceId(), out _)) { return NotFound(); } List Artifacts = await ArtifactService.GetArtifactsAsync(JobIdValue, StepIdValue, null); foreach (Artifact Artifact in Artifacts) { if (Artifact.Name.Equals("trace.json", StringComparison.OrdinalIgnoreCase)) { return new FileStreamResult(ArtifactService.OpenArtifactReadStream(Artifact), "text/json"); } } return NotFound(); } /// /// Adds an array of aggregates to a job /// /// Unique id for the job /// Properties of the new aggregates /// Response object with the first index of the new aggregates [HttpPost] [Route("/api/v1/jobs/{JobId}/aggregates")] public async Task CreateAggregatesAsync(string JobId, [FromBody] List Requests) { ObjectId JobIdValue = JobId.ToObjectId(); for (; ; ) { IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ExecuteJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); Graph = await Graphs.AppendAsync(Graph, null, Requests, null); if (await JobService.TryUpdateGraphAsync(Job, Graph)) { return Ok(); } } } /// /// Gets the aggregates for a job /// /// Unique id for the job /// Filter for the properties to return /// List of aggregates for the job [HttpGet] [Route("/api/v1/jobs/{JobId}/aggregates")] [ProducesResponseType(typeof(List), 200)] public async Task>> GetAggregatesAsync(string JobId, [FromQuery] PropertyFilter? Filter = null) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); List Aggregates = new List(); Job.GetLabelStateResponses(Graph, Aggregates); return Aggregates.ConvertAll(x => PropertyFilter.Apply(x, Filter)); } /// /// Gets an aggregate in a job /// /// Unique id for the job /// The aggregate index /// Filter for the properties to return /// Aggregate information [HttpGet] [Route("/api/v1/jobs/{JobId}/aggregates/{AggregateIdx}")] [ProducesResponseType(typeof(GetLabelStateResponse), 200)] public async Task> GetAggregateAsync(string JobId, int AggregateIdx, [FromQuery] PropertyFilter? Filter = null) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.ViewJob, User, null)) { return Forbid(); } IGraph Graph = await JobService.GetGraphAsync(Job); List Responses = new List(); Job.GetLabelStateResponses(Graph, Responses); if (AggregateIdx < 0 || AggregateIdx >= Responses.Count) { return NotFound(); } else { return PropertyFilter.Apply(Responses[AggregateIdx], Filter); } } /// /// Updates notifications for a specific label. /// /// Unique id for the job /// Index for the label /// The notification request [HttpPut] [Route("/api/v1/jobs/{JobId}/labels/{LabelIndex}/notifications")] public async Task UpdateLabelNotificationsAsync(string JobId, int LabelIndex, [FromBody] UpdateNotificationsRequest Request) { ObjectId JobIdValue = JobId.ToObjectId(); ObjectId TriggerId; for (; ; ) { IJob? Job = await JobService.GetJobAsync(JobIdValue); if (Job == null) { return NotFound(); } if (!await JobService.AuthorizeAsync(Job, AclAction.CreateSubscription, User, null)) { return Forbid(); } ObjectId NewTriggerId; if (Job.LabelIdxToTriggerId.TryGetValue(LabelIndex, out NewTriggerId)) { TriggerId = NewTriggerId; break; } NewTriggerId = ObjectId.GenerateNewId(); if (await JobService.UpdateJobAsync(Job, LabelIdxToTriggerId: new KeyValuePair(LabelIndex, NewTriggerId))) { TriggerId = NewTriggerId; break; } } await NotificationService.UpdateSubscriptionsAsync(TriggerId, User, Request.Email, Request.Slack); return Ok(); } /// /// Gets notification info about a specific label in a job. /// /// Id of the job to find /// Index for the label /// Notification info for the requested label in the job [HttpGet] [Route("/api/v1/jobs/{JobId}/labels/{LabelIndex}/notifications")] public async Task> GetLabelNotificationsAsync(string JobId, int LabelIndex) { IJob? Job = await JobService.GetJobAsync(JobId.ToObjectId()); if (Job == null) { return NotFound(); } INotificationSubscription? Subscription; if (!Job.LabelIdxToTriggerId.ContainsKey(LabelIndex) || Job.LabelIdxToTriggerId[LabelIndex] == null) { Subscription = null; } else { Subscription = await NotificationService.GetSubscriptionsAsync(Job.LabelIdxToTriggerId[LabelIndex], User); } return new GetNotificationResponse(Subscription); } } }