Skip to content

Commit

Permalink
Resolve Actions Directly From Launch for Run Service Jobs (#2529)
Browse files Browse the repository at this point in the history

Co-authored-by: Tingluo Huang <tingluohuang@github.com>
  • Loading branch information
jherns and TingluoHuang authored May 3, 2023
1 parent 229b9b8 commit 22d1938
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/Runner.Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ public static class System
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";
}
}
Expand Down
42 changes: 42 additions & 0 deletions src/Runner.Common/LaunchServer.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 1 deletion src/Runner.Worker/ActionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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;
Expand Down Expand Up @@ -648,13 +649,21 @@ private async Task BuildActionContainerAsync(IExecutionContext executionContext,
}

// 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.
Expand Down
3 changes: 3 additions & 0 deletions src/Runner.Worker/ExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,9 @@ public void InitializeJob(Pipelines.AgentJobRequestMessage message, Cancellation
// 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)
{
Expand Down
12 changes: 12 additions & 0 deletions src/Runner.Worker/JobRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ public async Task<TaskResult> RunAsync(AgentJobRequestMessage message, Cancellat
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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Sdk/Sdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
70 changes: 70 additions & 0 deletions src/Sdk/WebApi/WebApi/LaunchContracts.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 115 additions & 0 deletions src/Sdk/WebApi/WebApi/LaunchHttpClient.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
21 changes: 21 additions & 0 deletions src/Test/L0/Worker/ActionManagerL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public sealed class ActionManagerL0
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;
Expand Down Expand Up @@ -2175,6 +2176,25 @@ private void Setup([CallerMemberName] string name = "", bool enableComposite = t
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" });

Expand All @@ -2183,6 +2203,7 @@ private void Setup([CallerMemberName] string name = "", bool enableComposite = t

_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());
Expand Down

0 comments on commit 22d1938

Please sign in to comment.