-
Notifications
You must be signed in to change notification settings - Fork 990
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
Changes from 10 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
35e0a48
.
takost c853ac1
.
takost e1b78b9
.
takost 4372a36
.
takost 4abf017
.
takost 093dd0e
.
takost e6643f2
.
takost 1db5321
Rename a bit
takost 9826ebe
refactor
takost 05ce5c7
Renaming
takost 17a1a62
resolve comments
takost 0657e76
fix linting
takost ff114a0
Merge branch 'main' into takost/runner-admin-flow
takost 8bcabbb
fix tests
takost 3d20a31
.
takost File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
using GitHub.DistributedTask.WebApi; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using GitHub.Services.WebApi; | ||
using GitHub.Services.Common; | ||
using GitHub.Runner.Sdk; | ||
using System.Net.Http; | ||
using System.Net.Http.Headers; | ||
using System.Linq; | ||
|
||
namespace GitHub.Runner.Common | ||
{ | ||
[ServiceLocator(Default = typeof(RunnerDotcomServer))] | ||
public interface IRunnerDotcomServer : IRunnerService | ||
{ | ||
Task<List<TaskAgent>> GetRunnersAsync(int runnerGroupId, string githubUrl, string githubToken, string agentName); | ||
|
||
Task<TaskAgent> AddRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken); | ||
Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken); | ||
|
||
string GetGitHubRequestId(HttpResponseHeaders headers); | ||
} | ||
|
||
public enum RequestType | ||
{ | ||
Get, | ||
Post, | ||
Patch, | ||
Delete | ||
} | ||
|
||
public class RunnerDotcomServer : RunnerService, IRunnerDotcomServer | ||
{ | ||
private ITerminal _term; | ||
|
||
public override void Initialize(IHostContext hostContext) | ||
{ | ||
base.Initialize(hostContext); | ||
_term = hostContext.GetService<ITerminal>(); | ||
} | ||
|
||
|
||
public async Task<List<TaskAgent>> GetRunnersAsync(int runnerGroupId, string githubUrl, string githubToken, string agentName = null) | ||
{ | ||
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."); | ||
} | ||
|
||
var response = await RetryRequest(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools"); | ||
var agents = StringUtil.ConvertFromJson<ListRunnersResponse>(response); | ||
|
||
if (string.IsNullOrEmpty(agentName)) | ||
{ | ||
return agents.Runners; | ||
} | ||
|
||
return agents.Runners.Where(x => string.Equals(x.Name, agentName, StringComparison.OrdinalIgnoreCase)).ToList(); | ||
} | ||
|
||
public async Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken) | ||
{ | ||
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."); | ||
} | ||
|
||
var response = await RetryRequest(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools"); | ||
var agentPools = StringUtil.ConvertFromJson<RunnerGroupList>(response); | ||
|
||
return agentPools?.ToAgentPoolList(); | ||
} | ||
|
||
public async Task<TaskAgent> AddRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken) | ||
{ | ||
var gitHubUrlBuilder = new UriBuilder(githubUrl); | ||
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries); | ||
string githubApiUrl; | ||
if (UrlUtil.IsHostedServer(gitHubUrlBuilder)) | ||
{ | ||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/actions/runners/register"; | ||
} | ||
else | ||
{ | ||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/actions/runners/register"; | ||
} | ||
|
||
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 body = new StringContent(StringUtil.ConvertToJson(bodyObject), null, "application/json"); | ||
var response = await RetryRequest(githubApiUrl, githubToken, RequestType.Post, 3, "Failed to add agent", body); | ||
var responseAgent = StringUtil.ConvertFromJson<TaskAgent>(response); | ||
agent.Id = responseAgent.Id; | ||
return agent; | ||
} | ||
|
||
private async Task<string> RetryRequest(string githubApiUrl, string githubToken, RequestType requestType, int maxRetryAttemptsCount = 5, string errorMessage = null, StringContent body = null) | ||
takost marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
int retry = 0; | ||
while (true) | ||
{ | ||
retry++; | ||
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 | ||
{ | ||
HttpResponseMessage response = null; | ||
if (requestType == RequestType.Get) | ||
{ | ||
response = await httpClient.GetAsync(githubApiUrl); | ||
} | ||
else | ||
{ | ||
response = await httpClient.PostAsync(githubApiUrl, body); | ||
} | ||
|
||
if (response != null) | ||
{ | ||
responseStatus = response.StatusCode; | ||
var githubRequestId = GetGitHubRequestId(response.Headers); | ||
|
||
if (response.IsSuccessStatusCode) | ||
{ | ||
Trace.Info($"Http response code: {response.StatusCode} from '{requestType.ToString()} {githubApiUrl}' ({githubRequestId})"); | ||
var jsonResponse = await response.Content.ReadAsStringAsync(); | ||
return jsonResponse; | ||
takost marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
else | ||
{ | ||
_term.WriteError($"Http response code: {response.StatusCode} from '{requestType.ToString()} {githubApiUrl}' (Request Id: {githubRequestId})"); | ||
var errorResponse = await response.Content.ReadAsStringAsync(); | ||
_term.WriteError(errorResponse); | ||
response.EnsureSuccessStatusCode(); | ||
} | ||
} | ||
|
||
} | ||
catch (Exception ex) when (retry < maxRetryAttemptsCount && responseStatus != System.Net.HttpStatusCode.NotFound) | ||
{ | ||
Trace.Error($"{errorMessage} -- Atempt: {retry}"); | ||
Trace.Error(ex); | ||
} | ||
} | ||
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5)); | ||
Trace.Info($"Retrying in {backOff.Seconds} seconds"); | ||
await Task.Delay(backOff); | ||
} | ||
} | ||
|
||
public string GetGitHubRequestId(HttpResponseHeaders headers) | ||
{ | ||
if (headers.TryGetValues("x-github-request-id", out var headerValues)) | ||
{ | ||
return headerValues.FirstOrDefault(); | ||
} | ||
return string.Empty; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice! I like the refactor to it's own class