mirror of
https://github.com/actions/runner.git
synced 2025-12-13 19:03:44 +00:00
Resolve Actions Directly From Launch for Run Service Jobs (#2529)
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
This commit is contained in:
@@ -261,6 +261,7 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string AccessToken = "system.accessToken";
|
public static readonly string AccessToken = "system.accessToken";
|
||||||
public static readonly string Culture = "system.culture";
|
public static readonly string Culture = "system.culture";
|
||||||
public static readonly string PhaseDisplayName = "system.phaseDisplayName";
|
public static readonly string PhaseDisplayName = "system.phaseDisplayName";
|
||||||
|
public static readonly string JobRequestType = "system.jobRequestType";
|
||||||
public static readonly string OrchestrationId = "system.orchestrationId";
|
public static readonly string OrchestrationId = "system.orchestrationId";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/Runner.Common/LaunchServer.cs
Normal file
42
src/Runner.Common/LaunchServer.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Services.Launch.Client;
|
||||||
|
using GitHub.Services.WebApi;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common
|
||||||
|
{
|
||||||
|
[ServiceLocator(Default = typeof(LaunchServer))]
|
||||||
|
public interface ILaunchServer : IRunnerService
|
||||||
|
{
|
||||||
|
void InitializeLaunchClient(Uri uri, string token);
|
||||||
|
|
||||||
|
Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LaunchServer : RunnerService, ILaunchServer
|
||||||
|
{
|
||||||
|
private LaunchHttpClient _launchClient;
|
||||||
|
|
||||||
|
public void InitializeLaunchClient(Uri uri, string token)
|
||||||
|
{
|
||||||
|
var httpMessageHandler = HostContext.CreateHttpClientHandler();
|
||||||
|
this._launchClient = new LaunchHttpClient(uri, httpMessageHandler, token, disposeHandler: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_launchClient != null)
|
||||||
|
{
|
||||||
|
return _launchClient.GetResolveActionsDownloadInfoAsync(planId, jobId, actionReferenceList,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Launch client is not initialized.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Runner.Worker.Container;
|
using GitHub.Runner.Worker.Container;
|
||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
@@ -648,13 +649,21 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve download info
|
// Resolve download info
|
||||||
|
var launchServer = HostContext.GetService<ILaunchServer>();
|
||||||
var jobServer = HostContext.GetService<IJobServer>();
|
var jobServer = HostContext.GetService<IJobServer>();
|
||||||
var actionDownloadInfos = default(WebApi.ActionDownloadInfoCollection);
|
var actionDownloadInfos = default(WebApi.ActionDownloadInfoCollection);
|
||||||
for (var attempt = 1; attempt <= 3; attempt++)
|
for (var attempt = 1; attempt <= 3; attempt++)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
actionDownloadInfos = await jobServer.ResolveActionDownloadInfoAsync(executionContext.Global.Plan.ScopeIdentifier, executionContext.Global.Plan.PlanType, executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken);
|
if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType)))
|
||||||
|
{
|
||||||
|
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
actionDownloadInfos = await jobServer.ResolveActionDownloadInfoAsync(executionContext.Global.Plan.ScopeIdentifier, executionContext.Global.Plan.PlanType, executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (!executionContext.CancellationToken.IsCancellationRequested) // Do not retry if the run is cancelled.
|
catch (Exception ex) when (!executionContext.CancellationToken.IsCancellationRequested) // Do not retry if the run is cancelled.
|
||||||
|
|||||||
@@ -757,6 +757,9 @@ namespace GitHub.Runner.Worker
|
|||||||
// File table
|
// File table
|
||||||
Global.FileTable = new List<String>(message.FileTable ?? new string[0]);
|
Global.FileTable = new List<String>(message.FileTable ?? new string[0]);
|
||||||
|
|
||||||
|
// What type of job request is running (i.e. Run Service vs. pipelines)
|
||||||
|
Global.Variables.Set(Constants.Variables.System.JobRequestType, message.MessageType);
|
||||||
|
|
||||||
// Expression values
|
// Expression values
|
||||||
if (message.ContextData?.Count > 0)
|
if (message.ContextData?.Count > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,6 +58,18 @@ namespace GitHub.Runner.Worker
|
|||||||
await runServer.ConnectAsync(systemConnection.Url, jobServerCredential);
|
await runServer.ConnectAsync(systemConnection.Url, jobServerCredential);
|
||||||
server = runServer;
|
server = runServer;
|
||||||
|
|
||||||
|
message.Variables.TryGetValue("system.github.launch_endpoint", out VariableValue launchEndpointVariable);
|
||||||
|
var launchReceiverEndpoint = launchEndpointVariable?.Value;
|
||||||
|
|
||||||
|
if (systemConnection?.Authorization != null &&
|
||||||
|
systemConnection.Authorization.Parameters.TryGetValue("AccessToken", out var accessToken) &&
|
||||||
|
!string.IsNullOrEmpty(accessToken) &&
|
||||||
|
!string.IsNullOrEmpty(launchReceiverEndpoint))
|
||||||
|
{
|
||||||
|
Trace.Info("Initializing launch client");
|
||||||
|
var launchServer = HostContext.GetService<ILaunchServer>();
|
||||||
|
launchServer.InitializeLaunchClient(new Uri(launchReceiverEndpoint), accessToken);
|
||||||
|
}
|
||||||
_jobServerQueue = HostContext.GetService<IJobServerQueue>();
|
_jobServerQueue = HostContext.GetService<IJobServerQueue>();
|
||||||
_jobServerQueue.Start(message, resultServiceOnly: true);
|
_jobServerQueue.Start(message, resultServiceOnly: true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<NoWarn>NU1701;NU1603</NoWarn>
|
<NoWarn>NU1701;NU1603</NoWarn>
|
||||||
<Version>$(Version)</Version>
|
<Version>$(Version)</Version>
|
||||||
<DefineConstants>TRACE</DefineConstants>
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
<LangVersion>7.3</LangVersion>
|
<LangVersion>8.0</LangVersion>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
70
src/Sdk/WebApi/WebApi/LaunchContracts.cs
Normal file
70
src/Sdk/WebApi/WebApi/LaunchContracts.cs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace GitHub.Services.Launch.Contracts
|
||||||
|
{
|
||||||
|
[DataContract]
|
||||||
|
public class ActionReferenceRequest
|
||||||
|
{
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "action")]
|
||||||
|
public string Action { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "version")]
|
||||||
|
public string Version { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "path")]
|
||||||
|
public string Path { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
public class ActionReferenceRequestList
|
||||||
|
{
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "actions")]
|
||||||
|
public IList<ActionReferenceRequest> Actions { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
public class ActionDownloadInfoResponse
|
||||||
|
{
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "authentication")]
|
||||||
|
public ActionDownloadAuthenticationResponse Authentication { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "resolved_name")]
|
||||||
|
public string ResolvedName { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "resolved_sha")]
|
||||||
|
public string ResolvedSha { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "tar_url")]
|
||||||
|
public string TarUrl { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "version")]
|
||||||
|
public string Version { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "zip_url")]
|
||||||
|
public string ZipUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
public class ActionDownloadAuthenticationResponse
|
||||||
|
{
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "expires_at")]
|
||||||
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "token")]
|
||||||
|
public string Token { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
public class ActionDownloadInfoResponseCollection
|
||||||
|
{
|
||||||
|
/// <summary>A mapping of action specifications to their download information.</summary>
|
||||||
|
/// <remarks>The key is the full name of the action plus version, e.g. "actions/checkout@v2".</remarks>
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "actions")]
|
||||||
|
public IDictionary<string, ActionDownloadInfoResponse> Actions { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/Sdk/WebApi/WebApi/LaunchHttpClient.cs
Normal file
115
src/Sdk/WebApi/WebApi/LaunchHttpClient.cs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Formatting;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Services.Launch.Contracts;
|
||||||
|
|
||||||
|
using Sdk.WebApi.WebApi;
|
||||||
|
|
||||||
|
namespace GitHub.Services.Launch.Client
|
||||||
|
{
|
||||||
|
public class LaunchHttpClient : RawHttpClientBase
|
||||||
|
{
|
||||||
|
public LaunchHttpClient(
|
||||||
|
Uri baseUrl,
|
||||||
|
HttpMessageHandler pipeline,
|
||||||
|
string token,
|
||||||
|
bool disposeHandler)
|
||||||
|
: base(baseUrl, pipeline, disposeHandler)
|
||||||
|
{
|
||||||
|
m_token = token;
|
||||||
|
m_launchServiceUrl = baseUrl;
|
||||||
|
m_formatter = new JsonMediaTypeFormatter();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ActionDownloadInfoCollection> GetResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var GetResolveActionsDownloadInfoURLEndpoint = new Uri(m_launchServiceUrl, $"/actions/build/{planId.ToString()}/jobs/{jobId.ToString()}/runnerresolve/actions");
|
||||||
|
return ToServerData(await GetLaunchSignedURLResponse<ActionReferenceRequestList, ActionDownloadInfoResponseCollection>(GetResolveActionsDownloadInfoURLEndpoint, ToGitHubData(actionReferenceList), cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve Actions
|
||||||
|
private async Task<T> GetLaunchSignedURLResponse<R, T>(Uri uri, R request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri))
|
||||||
|
{
|
||||||
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_token);
|
||||||
|
requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
|
||||||
|
|
||||||
|
using (HttpContent content = new ObjectContent<R>(request, m_formatter))
|
||||||
|
{
|
||||||
|
requestMessage.Content = content;
|
||||||
|
using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken))
|
||||||
|
{
|
||||||
|
return await ReadJsonContentAsync<T>(response, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActionReferenceRequestList ToGitHubData(ActionReferenceList actionReferenceList)
|
||||||
|
{
|
||||||
|
return new ActionReferenceRequestList
|
||||||
|
{
|
||||||
|
Actions = actionReferenceList.Actions?.Select(ToGitHubData).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActionReferenceRequest ToGitHubData(ActionReference actionReference)
|
||||||
|
{
|
||||||
|
return new ActionReferenceRequest
|
||||||
|
{
|
||||||
|
Action = actionReference.NameWithOwner,
|
||||||
|
Version = actionReference.Ref,
|
||||||
|
Path = actionReference.Path
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActionDownloadInfoCollection ToServerData(ActionDownloadInfoResponseCollection actionDownloadInfoResponseCollection)
|
||||||
|
{
|
||||||
|
return new ActionDownloadInfoCollection
|
||||||
|
{
|
||||||
|
Actions = actionDownloadInfoResponseCollection.Actions?.ToDictionary(kvp => kvp.Key, kvp => ToServerData(kvp.Value))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActionDownloadInfo ToServerData(ActionDownloadInfoResponse actionDownloadInfoResponse)
|
||||||
|
{
|
||||||
|
return new ActionDownloadInfo
|
||||||
|
{
|
||||||
|
Authentication = ToServerData(actionDownloadInfoResponse.Authentication),
|
||||||
|
NameWithOwner = actionDownloadInfoResponse.Name,
|
||||||
|
ResolvedNameWithOwner = actionDownloadInfoResponse.ResolvedName,
|
||||||
|
ResolvedSha = actionDownloadInfoResponse.ResolvedSha,
|
||||||
|
TarballUrl = actionDownloadInfoResponse.TarUrl,
|
||||||
|
Ref = actionDownloadInfoResponse.Version,
|
||||||
|
ZipballUrl = actionDownloadInfoResponse.ZipUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActionDownloadAuthentication? ToServerData(ActionDownloadAuthenticationResponse? actionDownloadAuthenticationResponse)
|
||||||
|
{
|
||||||
|
if (actionDownloadAuthenticationResponse == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ActionDownloadAuthentication
|
||||||
|
{
|
||||||
|
ExpiresAt = actionDownloadAuthenticationResponse.ExpiresAt,
|
||||||
|
Token = actionDownloadAuthenticationResponse.Token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaTypeFormatter m_formatter;
|
||||||
|
private Uri m_launchServiceUrl;
|
||||||
|
private string m_token;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
private Mock<IDockerCommandManager> _dockerManager;
|
private Mock<IDockerCommandManager> _dockerManager;
|
||||||
private Mock<IExecutionContext> _ec;
|
private Mock<IExecutionContext> _ec;
|
||||||
private Mock<IJobServer> _jobServer;
|
private Mock<IJobServer> _jobServer;
|
||||||
|
private Mock<ILaunchServer> _launchServer;
|
||||||
private Mock<IRunnerPluginManager> _pluginManager;
|
private Mock<IRunnerPluginManager> _pluginManager;
|
||||||
private TestHostContext _hc;
|
private TestHostContext _hc;
|
||||||
private ActionManager _actionManager;
|
private ActionManager _actionManager;
|
||||||
@@ -2175,6 +2176,25 @@ runs:
|
|||||||
return Task.FromResult(result);
|
return Task.FromResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_launchServer = new Mock<ILaunchServer>();
|
||||||
|
_launchServer.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
|
||||||
|
foreach (var action in actions.Actions)
|
||||||
|
{
|
||||||
|
var key = $"{action.NameWithOwner}@{action.Ref}";
|
||||||
|
result.Actions[key] = new ActionDownloadInfo
|
||||||
|
{
|
||||||
|
NameWithOwner = action.NameWithOwner,
|
||||||
|
Ref = action.Ref,
|
||||||
|
TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}",
|
||||||
|
ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return Task.FromResult(result);
|
||||||
|
});
|
||||||
|
|
||||||
_pluginManager = new Mock<IRunnerPluginManager>();
|
_pluginManager = new Mock<IRunnerPluginManager>();
|
||||||
_pluginManager.Setup(x => x.GetPluginAction(It.IsAny<string>())).Returns(new RunnerPluginActionInfo() { PluginTypeName = "plugin.class, plugin", PostPluginTypeName = "plugin.cleanup, plugin" });
|
_pluginManager.Setup(x => x.GetPluginAction(It.IsAny<string>())).Returns(new RunnerPluginActionInfo() { PluginTypeName = "plugin.class, plugin", PostPluginTypeName = "plugin.cleanup, plugin" });
|
||||||
|
|
||||||
@@ -2183,6 +2203,7 @@ runs:
|
|||||||
|
|
||||||
_hc.SetSingleton<IDockerCommandManager>(_dockerManager.Object);
|
_hc.SetSingleton<IDockerCommandManager>(_dockerManager.Object);
|
||||||
_hc.SetSingleton<IJobServer>(_jobServer.Object);
|
_hc.SetSingleton<IJobServer>(_jobServer.Object);
|
||||||
|
_hc.SetSingleton<ILaunchServer>(_launchServer.Object);
|
||||||
_hc.SetSingleton<IRunnerPluginManager>(_pluginManager.Object);
|
_hc.SetSingleton<IRunnerPluginManager>(_pluginManager.Object);
|
||||||
_hc.SetSingleton<IActionManifestManager>(actionManifest);
|
_hc.SetSingleton<IActionManifestManager>(actionManifest);
|
||||||
_hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
_hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
||||||
|
|||||||
Reference in New Issue
Block a user