// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using EpicGames.Core;
using EpicGames.Horde.Artifacts;
#pragma warning disable CA2227 // Change x to be read-only by removing the property setter
namespace EpicGames.Horde.Jobs.Graphs
{
///
/// A unique dependency graph instance
///
public interface IGraph
{
///
/// Hash of this graph
///
public ContentHash Id { get; }
///
/// Schema version for this document
///
public int Schema { get; }
///
/// List of groups for this graph
///
public IReadOnlyList Groups { get; }
///
/// List of aggregates for this graph
///
public IReadOnlyList Aggregates { get; }
///
/// Status labels for this graph
///
public IReadOnlyList Labels { get; }
///
/// Artifacts for this graph
///
public IReadOnlyList Artifacts { get; }
}
///
/// Extension methods for graphs
///
public static class GraphExtensions
{
///
/// Gets the node from a node reference
///
/// The graph instance
/// The node reference
/// The node for the given reference
public static INode GetNode(this IGraph graph, NodeRef nodeRef)
{
return graph.Groups[nodeRef.GroupIdx].Nodes[nodeRef.NodeIdx];
}
///
/// Tries to find a node by name
///
/// The graph to search
/// Name of the node
/// Receives the node reference
/// True if the node was found, false otherwise
public static bool TryFindNode(this IGraph graph, string nodeName, out NodeRef nodeRef)
{
for (int groupIdx = 0; groupIdx < graph.Groups.Count; groupIdx++)
{
INodeGroup group = graph.Groups[groupIdx];
for (int nodeIdx = 0; nodeIdx < group.Nodes.Count; nodeIdx++)
{
INode node = group.Nodes[nodeIdx];
if (String.Equals(node.Name, nodeName, StringComparison.OrdinalIgnoreCase))
{
nodeRef = new NodeRef(groupIdx, nodeIdx);
return true;
}
}
}
nodeRef = new NodeRef(0, 0);
return false;
}
///
/// Tries to find a node by name
///
/// The graph to search
/// Name of the node
/// Receives the node
/// True if the node was found, false otherwise
public static bool TryFindNode(this IGraph graph, string nodeName, [NotNullWhen(true)] out INode? node)
{
NodeRef nodeRef;
if (TryFindNode(graph, nodeName, out nodeRef))
{
node = graph.Groups[nodeRef.GroupIdx].Nodes[nodeRef.NodeIdx];
return true;
}
else
{
node = null;
return false;
}
}
///
/// Tries to find a node by name
///
/// The graph to search
/// Name of the node
/// Receives the aggregate index
/// True if the node was found, false otherwise
public static bool TryFindAggregate(this IGraph graph, string name, out int aggregateIdx)
{
aggregateIdx = graph.Aggregates.FindIndex(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
return aggregateIdx != -1;
}
///
/// Tries to find a node by name
///
/// The graph to search
/// Name of the node
/// Receives the aggregate
/// True if the node was found, false otherwise
public static bool TryFindAggregate(this IGraph graph, string name, [NotNullWhen(true)] out IAggregate? aggregate)
{
int aggregateIdx;
if (TryFindAggregate(graph, name, out aggregateIdx))
{
aggregate = graph.Aggregates[aggregateIdx];
return true;
}
else
{
aggregate = null;
return false;
}
}
///
/// Gets a list of dependencies for the given node
///
/// The graph instance
/// The node to return dependencies for
/// List of dependencies
public static IEnumerable GetDependencies(this IGraph graph, INode node)
{
return Enumerable.Concat(node.InputDependencies, node.OrderDependencies).Select(x => graph.GetNode(x));
}
}
///
/// Represents a node in the graph
///
public interface INode
{
///
/// The name of this node
///
public string Name { get; }
///
/// References to inputs for this node
///
public IReadOnlyList Inputs { get; }
///
/// List of output names
///
public IReadOnlyList OutputNames { get; }
///
/// Indices of nodes which must have succeeded for this node to run
///
public NodeRef[] InputDependencies { get; }
///
/// Indices of nodes which must have completed for this node to run
///
public NodeRef[] OrderDependencies { get; }
///
/// The priority that this node should be run at, within this job
///
public Priority Priority { get; }
///
/// Whether this node can be run multiple times
///
public bool AllowRetry { get; }
///
/// This node can start running early, before dependencies of other nodes in the same group are complete
///
public bool RunEarly { get; }
///
/// Whether to include warnings in the output (defaults to true)
///
public bool Warnings { get; }
///
/// List of credentials required for this node. Each entry maps an environment variable name to a credential in the form "CredentialName.PropertyName".
///
public IReadOnlyDictionary? Credentials { get; }
///
/// Properties for this node
///
public IReadOnlyDictionary? Properties { get; }
///
/// Annotations for this node
///
public IReadOnlyNodeAnnotations Annotations { get; }
}
///
/// Information about a sequence of nodes which can execute on a single agent
///
public interface INodeGroup
{
///
/// The type of agent to execute this group
///
public string AgentType { get; }
///
/// Nodes in this group
///
public IReadOnlyList Nodes { get; }
}
///
/// Reference to a node within another grup
///
[DebuggerDisplay("Group: {GroupIdx}, Node: {NodeIdx}")]
public class NodeRef
{
///
/// The group index of the referenced node
///
public int GroupIdx { get; set; }
///
/// The node index of the referenced node
///
public int NodeIdx { get; set; }
///
/// Private constructor for serialization
///
private NodeRef()
{
GroupIdx = 0;
NodeIdx = 0;
}
///
/// Constructor
///
/// Index of thr group containing the node
/// Index of the node within the group
public NodeRef(int groupIdx, int nodeIdx)
{
GroupIdx = groupIdx;
NodeIdx = nodeIdx;
}
///
public override bool Equals(object? other)
{
NodeRef? otherNodeRef = other as NodeRef;
return otherNodeRef != null && otherNodeRef.GroupIdx == GroupIdx && otherNodeRef.NodeIdx == NodeIdx;
}
///
public override int GetHashCode()
{
return HashCode.Combine(GroupIdx, NodeIdx);
}
///
/// Converts this reference to a node name
///
/// List of groups that this reference points to
/// Name of the referenced node
public INode ToNode(IReadOnlyList groups)
{
return groups[GroupIdx].Nodes[NodeIdx];
}
}
///
/// Output from a node
///
[DebuggerDisplay("{NodeRef}, Output: {OutputIdx}")]
public class NodeOutputRef
{
///
/// Node producing the output
///
public NodeRef NodeRef { get; set; }
///
/// Index of the output
///
public int OutputIdx { get; set; }
///
/// Constructor
///
public NodeOutputRef(NodeRef nodeRef, int outputIdx)
{
NodeRef = nodeRef;
OutputIdx = outputIdx;
}
///
public override bool Equals(object? other) => other is NodeOutputRef otherRef && otherRef.NodeRef == NodeRef && otherRef.OutputIdx == OutputIdx;
///
public override int GetHashCode() => HashCode.Combine(NodeRef.GetHashCode(), OutputIdx);
}
///
/// An collection of node references
///
public interface IAggregate
{
///
/// Name of the aggregate
///
public string Name { get; }
///
/// List of nodes for the aggregate to be valid
///
public IReadOnlyList Nodes { get; }
}
///
/// Change at which to display a label
///
public enum LabelChange
{
///
/// The current changelist
///
Current = 0,
///
/// The last code changelist
///
Code = 1
}
///
/// Label indicating the status of a set of nodes
///
public interface ILabel
{
///
/// Label to show in the dashboard. Null if does not need to be shown.
///
public string? DashboardName { get; }
///
/// Category for the label. May be null.
///
public string? DashboardCategory { get; }
///
/// Name to display for this label in UGS
///
public string? UgsName { get; }
///
/// Project which this label applies to, for UGS
///
public string? UgsProject { get; }
///
/// Which change to display the label on
///
public LabelChange Change { get; }
///
/// List of required nodes for the aggregate to be valid
///
public List RequiredNodes { get; }
///
/// List of optional nodes to include in the aggregate state
///
public List IncludedNodes { get; }
}
///
/// Extension methods for ILabel
///
public static class LabelExtensions
{
///
/// Enumerate all the required dependencies of this node group
///
/// The label instance
/// List of groups for the job containing this aggregate
/// Sequence of nodes
public static IEnumerable GetDependencies(this ILabel label, IReadOnlyList groups)
{
foreach (NodeRef requiredNode in label.RequiredNodes)
{
yield return requiredNode.ToNode(groups);
}
foreach (NodeRef includedNode in label.IncludedNodes)
{
yield return includedNode.ToNode(groups);
}
}
}
///
/// Artifact produced by a graph
///
public interface IGraphArtifact
{
///
/// Name of the artifact
///
public ArtifactName Name { get; }
///
/// Type of the artifact
///
public ArtifactType Type { get; }
///
/// Description for the artifact
///
public string Description { get; }
///
/// Base path for files in the artifact
///
public string BasePath { get; }
///
/// Keys for finding the artifact
///
public IReadOnlyList Keys { get; }
///
/// Metadata for the artifact
///
public IReadOnlyList Metadata { get; }
///
/// Name of the node producing this artifact
///
public string? NodeName { get; }
///
/// Tag for the artifact files
///
public string? OutputName { get; }
}
///
/// Information required to create a node
///
public class NewNode
{
///
/// The name of this node
///
public string Name { get; set; } = null!;
///
/// Input names
///
public List? Inputs { get; set; }
///
/// Output names
///
public List? Outputs { get; set; }
///
/// List of nodes which must succeed for this node to run
///
public List? InputDependencies { get; set; }
///
/// List of nodes which must have completed for this node to run
///
public List? OrderDependencies { get; set; }
///
/// The priority of this node
///
public Priority? Priority { get; set; }
///
/// This node can be run multiple times
///
public bool? AllowRetry { get; set; }
///
/// This node can start running early, before dependencies of other nodes in the same group are complete
///
public bool? RunEarly { get; set; }
///
/// Whether to include warnings in the diagnostic output
///
public bool? Warnings { get; set; }
///
/// Credentials required for this node to run. This dictionary maps from environment variable names to a credential property in the format 'CredentialName.PropertyName'.
///
public Dictionary? Credentials { get; set; }
///
/// Properties for this node
///
public Dictionary? Properties { get; set; }
///
/// Additional user annotations for this node
///
public NodeAnnotations? Annotations { get; set; }
///
/// Constructor
///
/// Name of the node
/// List of inputs for the node
/// List of output names for the node
/// List of nodes which must have completed succesfully for this node to run
/// List of nodes which must have completed for this node to run
/// Priority of this node
/// Whether the node can be run multiple times
/// Whether the node can run early, before dependencies of other nodes in the same group complete
/// Whether to include warnings in the diagnostic output (defaults to true)
/// Credentials required for this node to run
/// Properties for the node
/// User annotations for this node
public NewNode(string name, List? inputs = null, List? outputs = null, List? inputDependencies = null, List? orderDependencies = null, Priority? priority = null, bool? allowRetry = null, bool? runEarly = null, bool? warnings = null, Dictionary? credentials = null, Dictionary? properties = null, IReadOnlyNodeAnnotations? annotations = null)
{
Name = name;
Inputs = inputs;
Outputs = outputs;
InputDependencies = inputDependencies;
OrderDependencies = orderDependencies;
Priority = priority;
AllowRetry = allowRetry;
RunEarly = runEarly;
Warnings = warnings;
Credentials = credentials;
Properties = properties;
if (annotations != null)
{
Annotations = new NodeAnnotations(annotations);
}
}
///
/// Constructor
///
/// Existing graph containing a node
/// Node to copy
public NewNode(IGraph graph, INode node)
: this(node.Name, node.Inputs.Select(x => graph.GetNode(x.NodeRef).Name).ToList(), node.OutputNames.ToList(), node.InputDependencies.Select(x => graph.GetNode(x).Name).ToList(), node.OrderDependencies.Select(x => graph.GetNode(x).Name).ToList(), node.Priority, node.AllowRetry, node.RunEarly, node.Warnings, node.Credentials?.ToDictionary(x => x.Key, x => x.Value), node.Properties?.ToDictionary(x => x.Key, x => x.Value), node.Annotations)
{
}
}
///
/// Information about a group of nodes
///
public class NewGroup
{
///
/// The type of agent to execute this group
///
public string AgentType { get; set; }
///
/// Nodes in the group
///
public List Nodes { get; set; }
///
/// Constructor
///
/// The type of agent to execute this group
/// Nodes in this group
public NewGroup(string agentType, List nodes)
{
AgentType = agentType;
Nodes = nodes;
}
///
/// Constructor
///
/// Graph containing the node group
/// Node group to copy
public NewGroup(IGraph graph, INodeGroup group)
: this(group.AgentType, group.Nodes.Select(x => new NewNode(graph, x)).ToList())
{
}
}
///
/// Information about a group of nodes
///
public class NewLabel
{
///
/// Category for this label
///
[Obsolete("Use DashboardCategory instead")]
public string? Category => DashboardCategory;
///
/// Name of the aggregate
///
[Obsolete("Use DashboardName instead")]
public string? Name => DashboardName;
///
/// Name of the aggregate
///
public string? DashboardName { get; set; }
///
/// Category for this label
///
public string? DashboardCategory { get; set; }
///
/// Name of the badge in UGS
///
public string? UgsName { get; set; }
///
/// Project to show this label for in UGS
///
public string? UgsProject { get; set; }
///
/// Which change the label applies to
///
public LabelChange Change { get; set; }
///
/// Nodes which must be part of the job for the aggregate to be valid
///
public List RequiredNodes { get; set; } = new List();
///
/// Nodes which must be part of the job for the aggregate to be valid
///
public List IncludedNodes { get; set; } = new List();
}
///
/// Information about a group of nodes
///
public class NewAggregate
{
///
/// Name of the aggregate
///
public string Name { get; set; }
///
/// Nodes which must be part of the job for the aggregate to be valid
///
public List Nodes { get; set; }
///
/// Constructor
///
/// Name of this aggregate
/// Nodes which must be part of the job for the aggregate to be shown
public NewAggregate(string name, List nodes)
{
Name = name;
Nodes = nodes;
}
}
///
/// Information about an artifact
///
public record class NewGraphArtifact(ArtifactName Name, ArtifactType Type, string Description, string BasePath, List Keys, List Metadata, string? NodeName, string? OutputName);
}