// 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); }