2023-03-03 13:13:27 -05:00
// Copyright Epic Games, Inc. All Rights Reserved.
using AutomationTool ;
using EpicGames.Core ;
using System ;
using System.Collections.Generic ;
using System.Text.Json ;
using System.IO ;
using System.Net.Http ;
using System.Net.Http.Headers ;
using System.Text ;
using System.Threading.Tasks ;
using System.Xml ;
using UnrealBuildBase ;
2023-03-07 21:23:47 -05:00
using Microsoft.Extensions.Logging ;
2023-06-24 16:37:55 -04:00
using EpicGames.Horde.Storage.Bundles ;
2023-09-09 10:14:12 -04:00
using EpicGames.Horde.Storage.Clients ;
2023-05-22 16:05:23 -04:00
using EpicGames.Horde.Storage ;
using EpicGames.Horde.Storage.Nodes ;
using System.Threading ;
using System.Data ;
2023-09-21 19:20:20 -04:00
using EpicGames.Horde.Storage.Backends ;
2024-01-21 13:52:05 -05:00
using Microsoft.Extensions.DependencyInjection ;
using EpicGames.Horde ;
using EpicGames.Horde.Tools ;
using EpicGames.Horde.Server ;
2023-03-03 13:13:27 -05:00
#nullable enable
namespace AutomationTool.Tasks
{
/// <summary>
/// Parameters for a DeployTool task
/// </summary>
public class DeployToolTaskParameters
{
/// <summary>
/// Identifier for the tool
/// </summary>
[TaskParameter]
public string Id = String . Empty ;
/// <summary>
/// Settings file to use for the deployment. Should be a JSON file containing server name and access token.
/// </summary>
[TaskParameter]
public string Settings = String . Empty ;
2023-03-06 14:39:54 -05:00
/// <summary>
/// Version number for the new tool
/// </summary>
[TaskParameter]
public string Version = String . Empty ;
2023-03-10 10:37:10 -05:00
/// <summary>
/// Duration over which to roll out the tool, in minutes.
/// </summary>
[TaskParameter(Optional = true)]
public int Duration = 0 ;
/// <summary>
/// Whether to create the deployment as paused
/// </summary>
[TaskParameter(Optional = true)]
public bool Paused = false ;
2023-05-26 11:55:13 -04:00
/// <summary>
/// Zip file containing files to upload
/// </summary>
[TaskParameter(Optional = true)]
public string? File = null ! ;
2023-03-03 13:13:27 -05:00
/// <summary>
2023-05-22 16:05:23 -04:00
/// Directory to upload for the tool
2023-03-03 13:13:27 -05:00
/// </summary>
2023-05-26 11:55:13 -04:00
[TaskParameter(Optional = true)]
public string? Directory = null ! ;
2023-03-03 13:13:27 -05:00
}
/// <summary>
/// Deploys a tool update through Horde
/// </summary>
[TaskElement("DeployTool", typeof(DeployToolTaskParameters))]
public class DeployToolTask : SpawnTaskBase
{
class DeploySettings
{
public string Server { get ; set ; } = String . Empty ;
public string? Token { get ; set ; }
}
2023-05-22 16:05:23 -04:00
/// <summary>
/// Options for a new deployment
/// </summary>
class CreateDeploymentRequest
{
public string Version { get ; set ; } = "Unknown" ;
public double? Duration { get ; set ; }
public bool? CreatePaused { get ; set ; }
public string? Node { get ; set ; }
}
2023-03-03 13:13:27 -05:00
/// <summary>
/// Parameters for this task
/// </summary>
DeployToolTaskParameters Parameters ;
/// <summary>
/// Construct a Helm task
/// </summary>
/// <param name="InParameters">Parameters for the task</param>
public DeployToolTask ( DeployToolTaskParameters InParameters )
{
Parameters = InParameters ;
}
/// <summary>
/// Execute the task.
/// </summary>
/// <param name="Job">Information about the current job</param>
/// <param name="BuildProducts">Set of build products produced by this node.</param>
/// <param name="TagNameToFileSet">Mapping from tag names to the set of files they include</param>
public override async Task ExecuteAsync ( JobContext Job , HashSet < FileReference > BuildProducts , Dictionary < string , HashSet < FileReference > > TagNameToFileSet )
{
FileReference settingsFile = ResolveFile ( Parameters . Settings ) ;
if ( ! FileReference . Exists ( settingsFile ) )
{
throw new AutomationException ( $"Settings file '{settingsFile}' does not exist" ) ;
}
byte [ ] settingsData = await FileReference . ReadAllBytesAsync ( settingsFile ) ;
JsonSerializerOptions jsonOptions = new JsonSerializerOptions { AllowTrailingCommas = true , ReadCommentHandling = JsonCommentHandling . Skip , PropertyNameCaseInsensitive = true } ;
DeploySettings ? settings = JsonSerializer . Deserialize < DeploySettings > ( settingsData , jsonOptions ) ;
if ( settings = = null )
{
throw new AutomationException ( $"Unable to read settings file {settingsFile}" ) ;
}
else if ( settings . Server = = null )
{
throw new AutomationException ( $"Missing 'server' key from {settingsFile}" ) ;
}
2024-01-21 13:52:05 -05:00
ServiceCollection serviceCollection = new ServiceCollection ( ) ;
serviceCollection . Configure < HordeOptions > ( options = >
2023-03-03 13:13:27 -05:00
{
2024-01-21 13:52:05 -05:00
options . ServerUrl = new Uri ( settings . Server ) ;
options . AccessToken = settings . Token ;
} ) ;
serviceCollection . AddHttpClient ( ) ;
serviceCollection . AddHorde ( ) ;
await using ServiceProvider serviceProvider = serviceCollection . BuildServiceProvider ( ) ;
IHordeClient hordeClient = serviceProvider . GetRequiredService < IHordeClient > ( ) ;
using HordeHttpClient hordeHttpClient = hordeClient . CreateHttpClient ( ) ;
GetServerInfoResponse infoResponse = await hordeHttpClient . GetServerInfoAsync ( ) ;
Logger . LogInformation ( "Uploading tool to {ServerUrl} (Version: {Version}, API v{ApiVersion})..." , settings . Server , infoResponse . ServerVersion , ( int ) infoResponse . ApiVersion ) ;
BlobSerializerOptions serializerOptions = BlobSerializerOptions . Create ( infoResponse . ApiVersion ) ;
2023-05-22 16:05:23 -04:00
2023-11-06 20:47:29 -05:00
IBlobHandle handle ;
2023-05-22 16:05:23 -04:00
2024-01-21 13:52:05 -05:00
ToolId toolId = new ToolId ( Parameters . Id ) ;
2023-09-22 09:22:32 -04:00
2024-01-21 13:52:05 -05:00
using IStorageClient storageClient = hordeClient . CreateStorageClient ( toolId ) ;
2024-01-17 15:55:37 -05:00
await using ( IBlobWriter treeWriter = storageClient . CreateBlobWriter ( ) )
2023-05-22 16:05:23 -04:00
{
DirectoryNode sandbox = new DirectoryNode ( ) ;
2023-05-26 11:55:13 -04:00
if ( Parameters . File ! = null )
{
using FileStream stream = FileReference . Open ( ResolveFile ( Parameters . File ) , FileMode . Open , FileAccess . Read ) ;
2024-01-21 13:52:05 -05:00
await sandbox . CopyFromZipStreamAsync ( stream , treeWriter , new ChunkingOptions ( ) , serializerOptions ) ;
2023-05-26 11:55:13 -04:00
}
else if ( Parameters . Directory ! = null )
{
DirectoryInfo directoryInfo = ResolveDirectory ( Parameters . Directory ) . ToDirectoryInfo ( ) ;
2024-01-21 13:52:05 -05:00
await sandbox . AddFilesAsync ( directoryInfo , treeWriter , serializerOptions : serializerOptions ) ;
2023-05-26 11:55:13 -04:00
}
else
{
throw new AutomationException ( "Either File=... or Directory=... must be specified" ) ;
}
2024-01-21 13:52:05 -05:00
handle = await treeWriter . WriteBlobAsync ( sandbox , serializerOptions ) ;
2024-01-12 08:59:27 -05:00
await treeWriter . FlushAsync ( ) ;
2023-05-22 16:05:23 -04:00
}
2024-01-21 13:52:05 -05:00
double? duration = null ;
2023-05-22 16:05:23 -04:00
if ( Parameters . Duration ! = 0 )
{
2024-01-21 13:52:05 -05:00
duration = Parameters . Duration ;
2023-05-22 16:05:23 -04:00
}
2024-01-21 13:52:05 -05:00
bool? createPaused = null ;
2023-05-22 16:05:23 -04:00
if ( Parameters . Paused )
{
2024-01-21 13:52:05 -05:00
createPaused = true ;
2023-05-22 16:05:23 -04:00
}
2024-01-21 13:52:05 -05:00
BlobLocator locator = handle . GetLocator ( ) ;
ToolDeploymentId deploymentId = await hordeHttpClient . CreateToolDeploymentAsync ( toolId , Parameters . Version , duration , createPaused , locator ) ;
Logger . LogInformation ( "Created {ToolId} deployment {DeploymentId}" , toolId , deploymentId ) ;
2023-03-03 13:13:27 -05:00
}
/// <summary>
/// Output this task out to an XML writer.
/// </summary>
public override void Write ( XmlWriter Writer )
{
Write ( Writer , Parameters ) ;
}
/// <summary>
/// Find all the tags which are used as inputs to this task
/// </summary>
/// <returns>The tag names which are read by this task</returns>
public override IEnumerable < string > FindConsumedTagNames ( )
{
yield break ;
}
/// <summary>
/// Find all the tags which are modified by this task
/// </summary>
/// <returns>The tag names which are modified by this task</returns>
public override IEnumerable < string > FindProducedTagNames ( )
{
yield break ;
}
}
}