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

Update runner to handle Dotcom/runner-admin based registration flow #2487

Merged
merged 15 commits into from
Mar 21, 2023
3 changes: 3 additions & 0 deletions src/Runner.Common/ConfigurationStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public sealed class RunnerSettings
[DataMember(EmitDefaultValue = false)]
public string MonitorSocketAddress { get; set; }

[DataMember(EmitDefaultValue = false)]
public bool UseV2Flow { get; set; }

[IgnoreDataMember]
public bool IsHostedServer
{
Expand Down
Empty file.
285 changes: 279 additions & 6 deletions src/Runner.Listener/Configuration/ConfigurationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public async Task ConfigureAsync(CommandSettings command)
ICredentialProvider credProvider = null;
VssCredentials creds = null;
_term.WriteSection("Authentication");
string registerToken = string.Empty;
while (true)
{
// When testing against a dev deployment of Actions Service, set this environment variable
Expand All @@ -130,9 +131,11 @@ public async Task ConfigureAsync(CommandSettings command)
else
{
runnerSettings.GitHubUrl = inputUrl;
var registerToken = await GetRunnerTokenAsync(command, inputUrl, "registration");
registerToken = await GetRunnerTokenAsync(command, inputUrl, "registration");
GitHubAuthResult authResult = await GetTenantCredential(inputUrl, registerToken, Constants.RunnerEvent.Register);
runnerSettings.ServerUrl = authResult.TenantUrl;
runnerSettings.UseV2Flow = authResult.UseV2Flow;
_term.WriteLine($"Using V2 flow: {runnerSettings.UseV2Flow}");
creds = authResult.ToVssCredentials();
Trace.Info("cred retrieved via GitHub auth");
}
Expand Down Expand Up @@ -186,9 +189,17 @@ public async Task ConfigureAsync(CommandSettings command)
// If we have more than one runner group available, allow the user to specify which one to be added into
string poolName = null;
TaskAgentPool agentPool = null;
List<TaskAgentPool> agentPools = await _runnerServer.GetAgentPoolsAsync();
TaskAgentPool defaultPool = agentPools?.Where(x => x.IsInternal).FirstOrDefault();
List<TaskAgentPool> agentPools;
if (runnerSettings.UseV2Flow)
{
agentPools = await GetAgentPoolsAsyncV2(runnerSettings.GitHubUrl, registerToken);
}
else
{
agentPools = await _runnerServer.GetAgentPoolsAsync();
}

TaskAgentPool defaultPool = agentPools?.Where(x => x.IsInternal).FirstOrDefault();
if (agentPools?.Where(x => !x.IsHosted).Count() > 0)
{
poolName = command.GetRunnerGroupName(defaultPool?.Name);
Expand Down Expand Up @@ -226,8 +237,16 @@ public async Task ConfigureAsync(CommandSettings command)

var userLabels = command.GetLabels();
_term.WriteLine();
List<TaskAgent> agents;
if (runnerSettings.UseV2Flow)
{
agents = await GetAgentsAsyncV2(runnerSettings.PoolId, runnerSettings.GitHubUrl, registerToken, runnerSettings.AgentName);
}
else
{
agents = await _runnerServer.GetAgentsAsync(runnerSettings.PoolId, runnerSettings.AgentName);
}

var agents = await _runnerServer.GetAgentsAsync(runnerSettings.PoolId, runnerSettings.AgentName);
Trace.Verbose("Returns {0} agents", agents.Count);
agent = agents.FirstOrDefault();
if (agent != null)
Expand Down Expand Up @@ -271,10 +290,18 @@ public async Task ConfigureAsync(CommandSettings command)
{
// Create a new agent.
agent = CreateNewAgent(runnerSettings.AgentName, publicKey, userLabels, runnerSettings.Ephemeral, command.DisableUpdate);

try
{
agent = await _runnerServer.AddAgentAsync(runnerSettings.PoolId, agent);
if (runnerSettings.UseV2Flow)
{
agent = await AddAgentAsyncV2(runnerSettings.PoolId, agent, runnerSettings.GitHubUrl, registerToken);
}
else
{
agent = await _runnerServer.AddAgentAsync(runnerSettings.PoolId, agent);
}

if (command.DisableUpdate &&
command.DisableUpdate != agent.DisableUpdate)
{
Expand Down Expand Up @@ -682,6 +709,252 @@ private async Task<GitHubRunnerRegisterToken> GetJITRunnerTokenAsync(string gith
return null;
}

private async Task<TaskAgent> AddAgentAsyncV2(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken)
takost marked this conversation as resolved.
Show resolved Hide resolved
{
var githubApiUrl = "";
var gitHubUrlBuilder = new UriBuilder(githubUrl);
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/actions/runners/register";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/actions/runners/register";
}

int retryCount = 0;
while (retryCount < 3)
{
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
using (var httpClient = new HttpClient(httpClientHandler))
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("RemoteAuth", githubToken);
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);

var bodyObject = new Dictionary<string, Object>()
{
{"url", githubUrl},
{"group_id", runnerGroupId},
{"name", agent.Name},
{"version", agent.Version},
{"updates_disabled", agent.DisableUpdate},
{"ephemeral", agent.Ephemeral},
{"labels", agent.Labels},
};

var responseStatus = System.Net.HttpStatusCode.OK;
try
{
var response = await httpClient.PostAsync(githubApiUrl, new StringContent(StringUtil.ConvertToJson(bodyObject), null, "application/json"));
responseStatus = response.StatusCode;
var githubRequestId = GetGitHubRequestId(response.Headers);

if (response.IsSuccessStatusCode)
{
Trace.Info($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}' ({githubRequestId})");
var jsonResponse = await response.Content.ReadAsStringAsync();
var responseAgent = StringUtil.ConvertFromJson<TaskAgent>(jsonResponse);
agent.Id = responseAgent.Id;
return agent;
}
else
{
_term.WriteError($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}' (Request Id: {githubRequestId})");
var errorResponse = await response.Content.ReadAsStringAsync();
_term.WriteError(errorResponse);
response.EnsureSuccessStatusCode();
}
}
catch (Exception ex) when (retryCount < 2 && responseStatus != System.Net.HttpStatusCode.NotFound)
{
retryCount++;
Trace.Error($"Failed to get tenant credentials -- Atempt: {retryCount}");
Trace.Error(ex);
}
}
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5));
Trace.Info($"Retrying in {backOff.Seconds} seconds");
await Task.Delay(backOff);
}
return null;
}

private async Task<List<TaskAgent>> GetAgentsAsyncV2(int runnerGroupId, string githubUrl, string githubToken, string agentName = null)
takost marked this conversation as resolved.
Show resolved Hide resolved
{
var githubApiUrl = "";
var gitHubUrlBuilder = new UriBuilder(githubUrl);
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
if (path.Length == 1)
{
// org runner
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions/runner-groups/{runnerGroupId}/runners";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions/runner-groups/{runnerGroupId}/runners";
}
}
else if (path.Length == 2)
{
// repo or enterprise runner.
if (!string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase))
{
return null;
}

if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{path[0]}/{path[1]}/actions/runner-groups/{runnerGroupId}/runners";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{path[0]}/{path[1]}/actions/runner-groups/{runnerGroupId}/runners";
}
}
else
{
throw new ArgumentException($"'{githubUrl}' should point to an org or enterprise.");
}

int retryCount = 0;
while (retryCount < 3)
{
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
using (var httpClient = new HttpClient(httpClientHandler))
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("RemoteAuth", githubToken);
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);

var responseStatus = System.Net.HttpStatusCode.OK;
try
{
var response = await httpClient.GetAsync(githubApiUrl);
responseStatus = response.StatusCode;
var githubRequestId = GetGitHubRequestId(response.Headers);

if (response.IsSuccessStatusCode)
{
Trace.Info($"Http response code: {response.StatusCode} from 'GET {githubApiUrl}' ({githubRequestId})");
var jsonResponse = await response.Content.ReadAsStringAsync();
var agents = StringUtil.ConvertFromJson<ListRunnersResponse>(jsonResponse);

if (string.IsNullOrEmpty(agentName))
{
return agents.Runners;
}

return agents.Runners.Where(x => string.Equals(x.Name, agentName, StringComparison.OrdinalIgnoreCase)).ToList();
}
else
{
_term.WriteError($"Http response code: {response.StatusCode} from 'GET {githubApiUrl}' (Request Id: {githubRequestId})");
var errorResponse = await response.Content.ReadAsStringAsync();
_term.WriteError(errorResponse);
response.EnsureSuccessStatusCode();
}
}
catch (Exception ex) when (retryCount < 2 && responseStatus != System.Net.HttpStatusCode.NotFound)
{
retryCount++;
Trace.Error($"Failed to get agent pools -- Atempt: {retryCount}");
Trace.Error(ex);
}
}
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5));
Trace.Info($"Retrying in {backOff.Seconds} seconds");
await Task.Delay(backOff);
}
return null;
}

private async Task<List<TaskAgentPool>> GetAgentPoolsAsyncV2(string githubUrl, string githubToken)
takost marked this conversation as resolved.
Show resolved Hide resolved
{
var githubApiUrl = "";
var gitHubUrlBuilder = new UriBuilder(githubUrl);
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
if (path.Length == 1)
{
// org runner
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions/runner-groups";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions/runner-groups";
}
}
else if (path.Length == 2)
{
// repo or enterprise runner.
if (!string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase))
{
return null;
}

if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{path[0]}/{path[1]}/actions/runner-groups";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{path[0]}/{path[1]}/actions/runner-groups";
}
}
else
{
throw new ArgumentException($"'{githubUrl}' should point to an org or enterprise.");
}

int retryCount = 0;
while (retryCount < 3)
{
takost marked this conversation as resolved.
Show resolved Hide resolved
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
using (var httpClient = new HttpClient(httpClientHandler))
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("RemoteAuth", githubToken);
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);

var responseStatus = System.Net.HttpStatusCode.OK;
try
{
var response = await httpClient.GetAsync(githubApiUrl);
responseStatus = response.StatusCode;
var githubRequestId = GetGitHubRequestId(response.Headers);

if (response.IsSuccessStatusCode)
{
Trace.Info($"Http response code: {response.StatusCode} from 'GET {githubApiUrl}' ({githubRequestId})");
var jsonResponse = await response.Content.ReadAsStringAsync();
var agentPools = StringUtil.ConvertFromJson<RunnerGroupList>(jsonResponse);

return agentPools?.ToAgentPoolList();
}
else
{
_term.WriteError($"Http response code: {response.StatusCode} from 'GET {githubApiUrl}' (Request Id: {githubRequestId})");
var errorResponse = await response.Content.ReadAsStringAsync();
_term.WriteError(errorResponse);
response.EnsureSuccessStatusCode();
}
}
catch (Exception ex) when (retryCount < 2 && responseStatus != System.Net.HttpStatusCode.NotFound)
{
retryCount++;
Trace.Error($"Failed to get agent pools -- Atempt: {retryCount}");
Trace.Error(ex);
}
}
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5));
Trace.Info($"Retrying in {backOff.Seconds} seconds");
await Task.Delay(backOff);
}
return null;
}

private async Task<GitHubAuthResult> GetTenantCredential(string githubUrl, string githubToken, string runnerEvent)
{
var githubApiUrl = "";
Expand Down
3 changes: 3 additions & 0 deletions src/Runner.Listener/Configuration/CredentialManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ public sealed class GitHubAuthResult
[DataMember(Name = "token")]
public string Token { get; set; }

[DataMember(Name = "use_v2_flow")]
public bool UseV2Flow { get; set; }

public VssCredentials ToVssCredentials()
{
ArgUtil.NotNullOrEmpty(TokenSchema, nameof(TokenSchema));
Expand Down
42 changes: 42 additions & 0 deletions src/Sdk/DTWebApi/WebApi/TaskRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using GitHub.Services.WebApi;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Newtonsoft.Json;

namespace GitHub.DistributedTask.WebApi
takost marked this conversation as resolved.
Show resolved Hide resolved
{

public class ListRunnersResponse
{
public ListRunnersResponse()
{
}

public ListRunnersResponse(ListRunnersResponse responseToBeCloned)
{
this.TotalCount = responseToBeCloned.TotalCount;
this.Runners = responseToBeCloned.Runners;
}

[JsonProperty("total_count")]
public int TotalCount
{
get;
set;
}

[JsonProperty("runners")]
public List<TaskAgent> Runners
{
get;
set;
}

public ListRunnersResponse Clone()
{
return new ListRunnersResponse(this);
}
}

}
Loading