Register Runners against V2 servers (#2505)

* Parse runners and send publicKey

* wip

* Fix conflicts

* Cleanup

* Cleanup

* fix test

* fix test

* Add trace for broker message listener

* Feedback

* refactor

* remove dead code

* Remove old comment
This commit is contained in:
Luke Tomlinson
2023-03-28 15:45:00 -04:00
committed by GitHub
parent 92258f9ea1
commit 9f778b814d
7 changed files with 99 additions and 29 deletions

View File

@@ -17,7 +17,7 @@ namespace GitHub.Runner.Common
{ {
Task<List<TaskAgent>> GetRunnersAsync(int runnerGroupId, string githubUrl, string githubToken, string agentName); Task<List<TaskAgent>> GetRunnersAsync(int runnerGroupId, string githubUrl, string githubToken, string agentName);
Task<TaskAgent> AddRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken); Task<DistributedTask.WebApi.Runner> AddRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken, string publicKey);
Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken); Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken);
string GetGitHubRequestId(HttpResponseHeaders headers); string GetGitHubRequestId(HttpResponseHeaders headers);
@@ -136,7 +136,7 @@ namespace GitHub.Runner.Common
return agentPools?.ToAgentPoolList(); return agentPools?.ToAgentPoolList();
} }
public async Task<TaskAgent> AddRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken) public async Task<DistributedTask.WebApi.Runner> AddRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken, string publicKey)
{ {
var gitHubUrlBuilder = new UriBuilder(githubUrl); var gitHubUrlBuilder = new UriBuilder(githubUrl);
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries); var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
@@ -159,12 +159,12 @@ namespace GitHub.Runner.Common
{"updates_disabled", agent.DisableUpdate}, {"updates_disabled", agent.DisableUpdate},
{"ephemeral", agent.Ephemeral}, {"ephemeral", agent.Ephemeral},
{"labels", agent.Labels}, {"labels", agent.Labels},
{"public_key", publicKey}
}; };
var body = new StringContent(StringUtil.ConvertToJson(bodyObject), null, "application/json"); var body = new StringContent(StringUtil.ConvertToJson(bodyObject), null, "application/json");
var responseAgent = await RetryRequest<TaskAgent>(githubApiUrl, githubToken, RequestType.Post, 3, "Failed to add agent", body);
agent.Id = responseAgent.Id; return await RetryRequest<DistributedTask.WebApi.Runner>(githubApiUrl, githubToken, RequestType.Post, 3, "Failed to add agent", body);
return agent;
} }
private async Task<T> RetryRequest<T>(string githubApiUrl, string githubToken, RequestType requestType, int maxRetryAttemptsCount = 5, string errorMessage = null, StringContent body = null) private async Task<T> RetryRequest<T>(string githubApiUrl, string githubToken, RequestType requestType, int maxRetryAttemptsCount = 5, string errorMessage = null, StringContent body = null)

View File

@@ -196,6 +196,11 @@ namespace GitHub.Runner.Listener
var configManager = HostContext.GetService<IConfigurationManager>(); var configManager = HostContext.GetService<IConfigurationManager>();
_settings = configManager.LoadSettings(); _settings = configManager.LoadSettings();
if (_settings.ServerUrlV2 == null)
{
throw new InvalidOperationException("ServerUrlV2 is not set");
}
var credMgr = HostContext.GetService<ICredentialManager>(); var credMgr = HostContext.GetService<ICredentialManager>();
VssCredentials creds = credMgr.LoadCredentials(); VssCredentials creds = credMgr.LoadCredentials();
await _brokerServer.ConnectAsync(new Uri(_settings.ServerUrlV2), creds); await _brokerServer.ConnectAsync(new Uri(_settings.ServerUrlV2), creds);

View File

@@ -181,9 +181,11 @@ namespace GitHub.Runner.Listener.Configuration
// We want to use the native CSP of the platform for storage, so we use the RSACSP directly // We want to use the native CSP of the platform for storage, so we use the RSACSP directly
RSAParameters publicKey; RSAParameters publicKey;
var keyManager = HostContext.GetService<IRSAKeyManager>(); var keyManager = HostContext.GetService<IRSAKeyManager>();
string publicKeyXML;
using (var rsa = keyManager.CreateKey()) using (var rsa = keyManager.CreateKey())
{ {
publicKey = rsa.ExportParameters(false); publicKey = rsa.ExportParameters(false);
publicKeyXML = rsa.ToXmlString(includePrivateParameters: false);
} }
_term.WriteSection("Runner Registration"); _term.WriteSection("Runner Registration");
@@ -297,7 +299,15 @@ namespace GitHub.Runner.Listener.Configuration
{ {
if (runnerSettings.UseV2Flow) if (runnerSettings.UseV2Flow)
{ {
agent = await _dotcomServer.AddRunnerAsync(runnerSettings.PoolId, agent, runnerSettings.GitHubUrl, registerToken); var runner = await _dotcomServer.AddRunnerAsync(runnerSettings.PoolId, agent, runnerSettings.GitHubUrl, registerToken, publicKeyXML);
runnerSettings.ServerUrlV2 = runner.RunnerAuthorization.ServerUrl;
agent.Id = runner.Id;
agent.Authorization = new TaskAgentAuthorization()
{
AuthorizationUrl = runner.RunnerAuthorization.AuthorizationUrl,
ClientId = new Guid(runner.RunnerAuthorization.ClientId)
};
} }
else else
{ {
@@ -354,24 +364,28 @@ namespace GitHub.Runner.Listener.Configuration
} }
// Testing agent connection, detect any potential connection issue, like local clock skew that cause OAuth token expired. // Testing agent connection, detect any potential connection issue, like local clock skew that cause OAuth token expired.
var credMgr = HostContext.GetService<ICredentialManager>();
VssCredentials credential = credMgr.LoadCredentials(); if (!runnerSettings.UseV2Flow)
try
{ {
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), credential); var credMgr = HostContext.GetService<ICredentialManager>();
// ConnectAsync() hits _apis/connectionData which is an anonymous endpoint VssCredentials credential = credMgr.LoadCredentials();
// Need to hit an authenticate endpoint to trigger OAuth token exchange. try
await _runnerServer.GetAgentPoolsAsync(); {
_term.WriteSuccessMessage("Runner connection is good"); await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), credential);
} // ConnectAsync() hits _apis/connectionData which is an anonymous endpoint
catch (VssOAuthTokenRequestException ex) when (ex.Message.Contains("Current server time is")) // Need to hit an authenticate endpoint to trigger OAuth token exchange.
{ await _runnerServer.GetAgentPoolsAsync();
// there are two exception messages server send that indicate clock skew. _term.WriteSuccessMessage("Runner connection is good");
// 1. The bearer token expired on {jwt.ValidTo}. Current server time is {DateTime.UtcNow}. }
// 2. The bearer token is not valid until {jwt.ValidFrom}. Current server time is {DateTime.UtcNow}. catch (VssOAuthTokenRequestException ex) when (ex.Message.Contains("Current server time is"))
Trace.Error("Catch exception during test agent connection."); {
Trace.Error(ex); // there are two exception messages server send that indicate clock skew.
throw new Exception("The local machine's clock may be out of sync with the server time by more than five minutes. Please sync your clock with your domain or internet time and try again."); // 1. The bearer token expired on {jwt.ValidTo}. Current server time is {DateTime.UtcNow}.
// 2. The bearer token is not valid until {jwt.ValidFrom}. Current server time is {DateTime.UtcNow}.
Trace.Error("Catch exception during test agent connection.");
Trace.Error(ex);
throw new Exception("The local machine's clock may be out of sync with the server time by more than five minutes. Please sync your clock with your domain or internet time and try again.");
}
} }
_term.WriteSection("Runner settings"); _term.WriteSection("Runner settings");

View File

@@ -343,6 +343,7 @@ namespace GitHub.Runner.Listener
{ {
if (settings.UseV2Flow) if (settings.UseV2Flow)
{ {
Trace.Info($"Using BrokerMessageListener");
var brokerListener = new BrokerMessageListener(); var brokerListener = new BrokerMessageListener();
brokerListener.Initialize(HostContext); brokerListener.Initialize(HostContext);
return brokerListener; return brokerListener;

View File

@@ -41,8 +41,6 @@ namespace GitHub.DistributedTask.WebApi
public List<TaskAgent> ToTaskAgents() public List<TaskAgent> ToTaskAgents()
{ {
List<TaskAgent> taskAgents = new List<TaskAgent>();
return Runners.Select(runner => new TaskAgent() { Name = runner.Name }).ToList(); return Runners.Select(runner => new TaskAgent() { Name = runner.Name }).ToList();
} }
} }

View File

@@ -1,12 +1,44 @@
using System;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace GitHub.DistributedTask.WebApi namespace GitHub.DistributedTask.WebApi
{ {
public class Runner public class Runner
{ {
/// <summary>
/// Name of the agent public class Authorization
/// </summary> {
/// <summary>
/// The url to refresh tokens
/// </summary>
[JsonProperty("authorization_url")]
public Uri AuthorizationUrl
{
get;
internal set;
}
/// <summary>
/// The url to connect to to poll for messages
/// </summary>
[JsonProperty("server_url")]
public string ServerUrl
{
get;
internal set;
}
/// <summary>
/// The client id to use when connecting to the authorization_url
/// </summary>
[JsonProperty("client_id")]
public string ClientId
{
get;
internal set;
}
}
[JsonProperty("name")] [JsonProperty("name")]
public string Name public string Name
{ {
@@ -14,5 +46,18 @@ namespace GitHub.DistributedTask.WebApi
internal set; internal set;
} }
[JsonProperty("id")]
public Int32 Id
{
get;
internal set;
}
[JsonProperty("authorization")]
public Authorization RunnerAuthorization
{
get;
internal set;
}
} }
} }

View File

@@ -73,6 +73,13 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
AuthorizationUrl = new Uri("http://localhost:8080/pipelines"), AuthorizationUrl = new Uri("http://localhost:8080/pipelines"),
}; };
var expectedRunner = new GitHub.DistributedTask.WebApi.Runner() { Name = expectedAgent.Name, Id = 1 };
expectedRunner.RunnerAuthorization = new GitHub.DistributedTask.WebApi.Runner.Authorization
{
ClientId = expectedAgent.Authorization.ClientId.ToString(),
AuthorizationUrl = new Uri("http://localhost:8080/pipelines"),
};
var connectionData = new ConnectionData() var connectionData = new ConnectionData()
{ {
InstanceId = Guid.NewGuid(), InstanceId = Guid.NewGuid(),
@@ -110,7 +117,7 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
_dotcomServer.Setup(x => x.GetRunnersAsync(It.IsAny<int>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(expectedAgents)); _dotcomServer.Setup(x => x.GetRunnersAsync(It.IsAny<int>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(expectedAgents));
_dotcomServer.Setup(x => x.GetRunnerGroupsAsync(It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(expectedPools)); _dotcomServer.Setup(x => x.GetRunnerGroupsAsync(It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(expectedPools));
_dotcomServer.Setup(x => x.AddRunnerAsync(It.IsAny<int>(), It.IsAny<TaskAgent>(), It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(expectedAgent)); _dotcomServer.Setup(x => x.AddRunnerAsync(It.IsAny<int>(), It.IsAny<TaskAgent>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(expectedRunner));
rsa = new RSACryptoServiceProvider(2048); rsa = new RSACryptoServiceProvider(2048);