// 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; using Microsoft.Extensions.Logging; using EpicGames.Horde.Storage.Bundles; using EpicGames.Horde.Storage.Clients; using EpicGames.Horde.Storage; using EpicGames.Horde.Storage.Nodes; using System.Threading; using System.Data; using EpicGames.Horde.Storage.Backends; #nullable enable namespace AutomationTool.Tasks { /// /// Parameters for a DeployTool task /// public class DeployToolTaskParameters { /// /// Identifier for the tool /// [TaskParameter] public string Id = String.Empty; /// /// Settings file to use for the deployment. Should be a JSON file containing server name and access token. /// [TaskParameter] public string Settings = String.Empty; /// /// Version number for the new tool /// [TaskParameter] public string Version = String.Empty; /// /// Duration over which to roll out the tool, in minutes. /// [TaskParameter(Optional = true)] public int Duration = 0; /// /// Whether to create the deployment as paused /// [TaskParameter(Optional = true)] public bool Paused = false; /// /// Zip file containing files to upload /// [TaskParameter(Optional = true)] public string? File = null!; /// /// Directory to upload for the tool /// [TaskParameter(Optional = true)] public string? Directory = null!; } /// /// Deploys a tool update through Horde /// [TaskElement("DeployTool", typeof(DeployToolTaskParameters))] public class DeployToolTask : SpawnTaskBase { class DeploySettings { public string Server { get; set; } = String.Empty; public string? Token { get; set; } } /// /// Options for a new deployment /// class CreateDeploymentRequest { public string Version { get; set; } = "Unknown"; public double? Duration { get; set; } public bool? CreatePaused { get; set; } public string? Node { get; set; } } /// /// Parameters for this task /// DeployToolTaskParameters Parameters; /// /// Construct a Helm task /// /// Parameters for the task public DeployToolTask(DeployToolTaskParameters InParameters) { Parameters = InParameters; } /// /// Execute the task. /// /// Information about the current job /// Set of build products produced by this node. /// Mapping from tag names to the set of files they include public override async Task ExecuteAsync(JobContext Job, HashSet BuildProducts, Dictionary> 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(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}"); } Uri serverUri = new Uri(settings.Server); HttpClient CreateHttpClient() { HttpClient httpClient = new HttpClient(); httpClient.BaseAddress = serverUri; if (settings?.Token != null) { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", settings.Token); } return httpClient; } IBlobHandle handle; string basePath = $"api/v1/tools/{Parameters.Id}"; using HttpStorageBackend httpStorageBackend = new HttpStorageBackend(basePath, CreateHttpClient, Logger); using HttpStorageClient httpStorageClient = new HttpStorageClient(basePath, CreateHttpClient, httpStorageBackend, Logger); using BundleStorageClient storageClient = new BundleStorageClient(httpStorageClient, BundleCache.None, Logger); await using (IStorageWriter treeWriter = storageClient.CreateWriter()) { DirectoryNode sandbox = new DirectoryNode(); if (Parameters.File != null) { using FileStream stream = FileReference.Open(ResolveFile(Parameters.File), FileMode.Open, FileAccess.Read); await sandbox.CopyFromZipStreamAsync(stream, treeWriter, new ChunkingOptions()); } else if (Parameters.Directory != null) { DirectoryInfo directoryInfo = ResolveDirectory(Parameters.Directory).ToDirectoryInfo(); await sandbox.CopyFromDirectoryAsync(directoryInfo, new ChunkingOptions(), treeWriter, null); } else { throw new AutomationException("Either File=... or Directory=... must be specified"); } handle = await treeWriter.FlushAsync(sandbox); } CreateDeploymentRequest request = new CreateDeploymentRequest(); request.Version = Parameters.Version; if (Parameters.Duration != 0) { request.Duration = Parameters.Duration; } if (Parameters.Paused) { request.CreatePaused = true; } request.Node = handle.GetLocator().ToString(); using (HttpClient httpClient = CreateHttpClient()) { using (HttpResponseMessage response = await httpClient.PostAsync(new Uri(serverUri, $"api/v2/tools/{Parameters.Id}/deployments"), request, CancellationToken.None)) { if (!response.IsSuccessStatusCode) { string? responseContent; try { responseContent = await response.Content.ReadAsStringAsync(); } catch { responseContent = "(No message)"; } throw new AutomationException($"Upload failed ({response.StatusCode}): {responseContent}"); } } } } /// /// Output this task out to an XML writer. /// public override void Write(XmlWriter Writer) { Write(Writer, Parameters); } /// /// Find all the tags which are used as inputs to this task /// /// The tag names which are read by this task public override IEnumerable FindConsumedTagNames() { yield break; } /// /// Find all the tags which are modified by this task /// /// The tag names which are modified by this task public override IEnumerable FindProducedTagNames() { yield break; } } }