mirror of
https://github.com/actions/runner.git
synced 2025-12-12 05:37:01 +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 Culture = "system.culture";
|
||||
public static readonly string PhaseDisplayName = "system.phaseDisplayName";
|
||||
public static readonly string JobRequestType = "system.jobRequestType";
|
||||
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 GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Services.Common;
|
||||
@@ -648,13 +649,21 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
// Resolve download info
|
||||
var launchServer = HostContext.GetService<ILaunchServer>();
|
||||
var jobServer = HostContext.GetService<IJobServer>();
|
||||
var actionDownloadInfos = default(WebApi.ActionDownloadInfoCollection);
|
||||
for (var attempt = 1; attempt <= 3; attempt++)
|
||||
{
|
||||
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;
|
||||
}
|
||||
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
|
||||
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
|
||||
if (message.ContextData?.Count > 0)
|
||||
{
|
||||
|
||||
@@ -58,6 +58,18 @@ namespace GitHub.Runner.Worker
|
||||
await runServer.ConnectAsync(systemConnection.Url, jobServerCredential);
|
||||
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.Start(message, resultServiceOnly: true);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<NoWarn>NU1701;NU1603</NoWarn>
|
||||
<Version>$(Version)</Version>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</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<IExecutionContext> _ec;
|
||||
private Mock<IJobServer> _jobServer;
|
||||
private Mock<ILaunchServer> _launchServer;
|
||||
private Mock<IRunnerPluginManager> _pluginManager;
|
||||
private TestHostContext _hc;
|
||||
private ActionManager _actionManager;
|
||||
@@ -2175,6 +2176,25 @@ runs:
|
||||
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.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<IJobServer>(_jobServer.Object);
|
||||
_hc.SetSingleton<ILaunchServer>(_launchServer.Object);
|
||||
_hc.SetSingleton<IRunnerPluginManager>(_pluginManager.Object);
|
||||
_hc.SetSingleton<IActionManifestManager>(actionManifest);
|
||||
_hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
||||
|
||||
Reference in New Issue
Block a user