Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve Actions Directly From Launch for Run Service Jobs #2529

Merged
merged 34 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5d35e06
Add launch client
jherns Mar 21, 2023
3fa88c6
fix errors
jherns Mar 22, 2023
180f2b2
add Launch Server
jherns Apr 4, 2023
ce7d5f1
fix stuff
jherns Apr 4, 2023
c139011
add payload
jherns Apr 4, 2023
eccf619
use payload
jherns Apr 4, 2023
ffe4899
client changes
jherns Apr 4, 2023
ba4368d
server changes
jherns Apr 4, 2023
f67e4eb
add job id
jherns Apr 4, 2023
523a4f2
use job id
jherns Apr 4, 2023
25eedad
minor stuff
jherns Apr 4, 2023
1ce356d
pass guids
jherns Apr 4, 2023
a29ff09
return type
jherns Apr 4, 2023
d67a5f9
fix runner stuff
jherns Apr 8, 2023
98d2c3d
make it work
jherns Apr 10, 2023
62c2ad6
update lang version
jherns Apr 11, 2023
8dd3295
remove hardcode
jherns Apr 12, 2023
e968b5a
indenting
jherns Apr 12, 2023
4889151
init jobServerQueue for run service jobs
jherns Apr 12, 2023
c015ed6
remove empty diffs
jherns Apr 12, 2023
0c71d05
this time?
jherns Apr 12, 2023
9d26a6b
setup mocks for _launchServer
jherns Apr 13, 2023
4c6287a
typo
jherns Apr 13, 2023
080c58b
linting
jherns Apr 13, 2023
069aefe
get jobid properly
jherns Apr 13, 2023
306a4ec
fix formatting
jherns Apr 26, 2023
43bca84
linting
jherns May 1, 2023
2bde421
Update src/Sdk/WebApi/WebApi/LaunchHttpClient.cs
jherns May 2, 2023
ea42fb2
move private method down file
jherns May 2, 2023
b6d96fe
use constant for jobRequestType
jherns May 2, 2023
98374e4
initialize launch server in JobRunner.cs
jherns May 2, 2023
3768a5d
Merge remote-tracking branch 'upstream/main'
jherns May 2, 2023
1a78a1d
linting
jherns May 2, 2023
064967f
Apply suggestions from code review
jherns May 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
TingluoHuang marked this conversation as resolved.
Show resolved Hide resolved
<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; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is currently called Ref instead of Version, what's the reason for the rename?

Copy link
Contributor Author

@jherns jherns May 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is copied pretty much directly from https://github.com/github/actions-dotnet/blob/179ed33568dd1ad80c457fb1c328c964e372accf/Launch/Sdk/Server/Models/GitHub.cs#L90-L152. Since the runner is now the one receiving the payload from launch, it needs to parse the JSON payload the same way. The same goes for sending the payload to Launch. The runner needs to serialize the data into JSON that Launch expects. I don't believe this is a rename


[DataMember(EmitDefaultValue = false, Name = "path")]
public string Path { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the server care about the Path of the action?

}

[DataContract]
public class ActionReferenceRequestList
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need an object to wrap around a list?
Can we just send the List as json to service?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The endpoint for resolving actions expects an object wrapped around a list

{
[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; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question about version vs ref


[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; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really care about the token's expiresAt?

Copy link
Contributor Author

@jherns jherns May 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


[DataMember(EmitDefaultValue = false, Name = "token")]
public string Token { get; set; }
}

[DataContract]
public class ActionDownloadInfoResponseCollection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question, why do we need this wrapper around a dictionary?

{
/// <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)
jherns marked this conversation as resolved.
Show resolved Hide resolved
{
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