Skip to content

Commit a1fcd6a

Browse files
authored
Add internal command to register GitLab runners (#340)
This makes it easier to register GitLab runners with the new authentication flow.
1 parent d57b88a commit a1fcd6a

File tree

7 files changed

+281
-0
lines changed

7 files changed

+281
-0
lines changed

UET/uet/Commands/Internal/InternalCommand.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
using UET.Commands.Internal.Patch;
3838
using UET.Commands.Internal.Service;
3939
using UET.Commands.Internal.GitCredentialHelper;
40+
using UET.Commands.Internal.RegisterGitLabRunner;
4041

4142
internal sealed class InternalCommand
4243
{
@@ -82,6 +83,7 @@ public static Command CreateInternalCommand(HashSet<Command> globalCommands)
8283
PatchCommand.CreatePatchCommand(),
8384
ServiceCommand.CreateServiceCommand(),
8485
GitCredentialHelperCommand.CreateGitCredentialHelperCommand(),
86+
RegisterGitLabRunnerCommand.CreateRegisterGitLabRunnerCommand(),
8587
};
8688

8789
var command = new Command("internal", "Internal commands used by UET when it needs to call back into itself.");
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace UET.Commands.Internal.RegisterGitLabRunner
2+
{
3+
using System.Text.Json.Serialization;
4+
5+
internal class GitLabGetRunnerResponse
6+
{
7+
[JsonPropertyName("id"), JsonRequired]
8+
public required int Id { get; set; }
9+
}
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace UET.Commands.Internal.RegisterGitLabRunner
2+
{
3+
using System.Text.Json.Serialization;
4+
5+
internal class GitLabRegisterRunnerResponse
6+
{
7+
[JsonPropertyName("id"), JsonRequired]
8+
public required int Id { get; set; }
9+
10+
[JsonPropertyName("token"), JsonRequired]
11+
public required string Token { get; set; }
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace UET.Commands.Internal.RegisterGitLabRunner
2+
{
3+
using System.Text.Json.Serialization;
4+
5+
internal class GitLabRunnerRegistrationSpec : GitLabRunnerSpec
6+
{
7+
[JsonPropertyName("id_path"), JsonRequired]
8+
public required string IdPath { get; set; }
9+
10+
[JsonPropertyName("token_path"), JsonRequired]
11+
public required string TokenPath { get; set; }
12+
}
13+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace UET.Commands.Internal.RegisterGitLabRunner
2+
{
3+
using System.Text.Json.Serialization;
4+
5+
internal class GitLabRunnerSpec
6+
{
7+
/// <summary>
8+
/// One of: "instance_type", "group_type" or "project_type".
9+
/// </summary>
10+
[JsonPropertyName("runner_type"), JsonRequired]
11+
public required string RunnerType { get; set; }
12+
13+
[JsonPropertyName("group_id")]
14+
public int? GroupId { get; set; }
15+
16+
[JsonPropertyName("project_id")]
17+
public int? ProjectId { get; set; }
18+
19+
[JsonPropertyName("description")]
20+
public string? Description { get; set; }
21+
22+
[JsonPropertyName("paused")]
23+
public bool Paused { get; set; }
24+
25+
[JsonPropertyName("locked")]
26+
public bool Locked { get; set; }
27+
28+
[JsonPropertyName("run_untagged")]
29+
public bool RunUntagged { get; set; }
30+
31+
[JsonPropertyName("tag_list")]
32+
public string? TagList { get; set; }
33+
34+
/// <summary>
35+
/// One of: "not_protected", "ref_protected".
36+
/// </summary>
37+
[JsonPropertyName("access_level")]
38+
public string? AccessLevel { get; set; }
39+
40+
[JsonPropertyName("maximum_timeout")]
41+
public int? MaximumTimeout { get; set; }
42+
43+
[JsonPropertyName("maintenance_note")]
44+
public string? MaintainenceNote { get; set; }
45+
}
46+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
namespace UET.Commands.Internal.RegisterGitLabRunner
2+
{
3+
using Microsoft.Extensions.Logging;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.CommandLine;
7+
using System.CommandLine.Invocation;
8+
using System.Globalization;
9+
using System.Linq;
10+
using System.Net.Http.Json;
11+
using System.Text;
12+
using System.Text.Json;
13+
using System.Threading.Tasks;
14+
15+
internal sealed class RegisterGitLabRunnerCommand
16+
{
17+
internal sealed class Options
18+
{
19+
}
20+
21+
public static Command CreateRegisterGitLabRunnerCommand()
22+
{
23+
var options = new Options();
24+
var command = new Command(
25+
"register-gitlab-runner",
26+
"Register a GitLab runner with the GitLab API using the new authentication flow.");
27+
command.AddAllOptions(options);
28+
command.AddCommonHandler<RegisterGitLabRunnerCommandInstance>(options);
29+
return command;
30+
}
31+
32+
private sealed class RegisterGitLabRunnerCommandInstance : ICommandInstance
33+
{
34+
private readonly ILogger<RegisterGitLabRunnerCommandInstance> _logger;
35+
36+
public RegisterGitLabRunnerCommandInstance(
37+
ILogger<RegisterGitLabRunnerCommandInstance> logger)
38+
{
39+
_logger = logger;
40+
}
41+
42+
public async Task<int> ExecuteAsync(InvocationContext context)
43+
{
44+
var baseUrl = Environment.GetEnvironmentVariable("UET_GITLAB_BASE_URL");
45+
var personalAccessToken = Environment.GetEnvironmentVariable("UET_GITLAB_PERSONAL_ACCESS_TOKEN");
46+
var registrationSpecRaw = Environment.GetEnvironmentVariable("UET_GITLAB_REGISTER_SPEC");
47+
GitLabRunnerRegistrationSpec[] registrationSpec;
48+
try
49+
{
50+
registrationSpec = JsonSerializer.Deserialize(registrationSpecRaw!, RegisterGitLabRunnerJsonSerializerContext.Default.GitLabRunnerRegistrationSpecArray)!;
51+
ArgumentNullException.ThrowIfNull(registrationSpec);
52+
}
53+
catch (Exception ex)
54+
{
55+
_logger.LogError(ex, "Environment variable 'UET_GITLAB_REGISTER_SPEC' was invalid.");
56+
return 1;
57+
}
58+
if (string.IsNullOrWhiteSpace(baseUrl))
59+
{
60+
_logger.LogError("Environment variable 'UET_GITLAB_BASE_URL' was invalid.");
61+
return 1;
62+
}
63+
if (string.IsNullOrWhiteSpace(baseUrl))
64+
{
65+
_logger.LogError("Environment variable 'UET_GITLAB_PERSONAL_ACCESS_TOKEN' was invalid.");
66+
return 1;
67+
}
68+
69+
using (var client = new HttpClient())
70+
{
71+
client.DefaultRequestHeaders.Add(
72+
"PRIVATE-TOKEN",
73+
personalAccessToken);
74+
75+
// Iterate through registration specs, registering where necessary.
76+
foreach (var spec in registrationSpec)
77+
{
78+
if (File.Exists(spec.IdPath))
79+
{
80+
_logger.LogInformation($"Checking if existing runner with ID at path {spec.IdPath} is already registered...");
81+
try
82+
{
83+
var id = (await File.ReadAllTextAsync(spec.IdPath, context.GetCancellationToken()).ConfigureAwait(false)).Trim();
84+
var json = await client.GetFromJsonAsync(
85+
$"{baseUrl}/api/v4/runners/{id}",
86+
RegisterGitLabRunnerJsonSerializerContext.Default.GitLabGetRunnerResponse);
87+
_logger.LogInformation($"Runner with ID at path {spec.IdPath} is already registered.");
88+
continue;
89+
}
90+
catch (Exception ex)
91+
{
92+
_logger.LogWarning(ex, "Runner is not registered or failed to read from GitLab.");
93+
}
94+
}
95+
96+
var form = new Dictionary<string, string>
97+
{
98+
{ "runner_type", spec.RunnerType },
99+
};
100+
if (spec.GroupId.HasValue)
101+
{
102+
form.Add("group_id", spec.GroupId.Value.ToString(CultureInfo.InvariantCulture));
103+
}
104+
if (spec.ProjectId.HasValue)
105+
{
106+
form.Add("project_id", spec.ProjectId.Value.ToString(CultureInfo.InvariantCulture));
107+
}
108+
if (!string.IsNullOrWhiteSpace(spec.Description))
109+
{
110+
form.Add("description", spec.Description
111+
.Replace(
112+
"__HOSTNAME__",
113+
Environment.MachineName.ToLowerInvariant(),
114+
StringComparison.Ordinal));
115+
}
116+
form.Add("paused", spec.Paused ? "true" : "false");
117+
form.Add("locked", spec.Locked ? "true" : "false");
118+
form.Add("run_untagged", spec.RunUntagged ? "true" : "false");
119+
if (!string.IsNullOrWhiteSpace(spec.TagList))
120+
{
121+
form.Add("tag_list", spec.TagList
122+
.Replace(
123+
"__HOSTNAME__",
124+
Environment.MachineName.ToLowerInvariant(),
125+
StringComparison.Ordinal));
126+
}
127+
if (!string.IsNullOrWhiteSpace(spec.AccessLevel))
128+
{
129+
form.Add("access_level", spec.AccessLevel);
130+
}
131+
if (spec.MaximumTimeout.HasValue)
132+
{
133+
form.Add("maximum_timeout", spec.MaximumTimeout.Value.ToString(CultureInfo.InvariantCulture));
134+
}
135+
if (!string.IsNullOrWhiteSpace(spec.MaintainenceNote))
136+
{
137+
form.Add("maintenance_note", spec.MaintainenceNote);
138+
}
139+
140+
_logger.LogInformation($"Registering new GitLab runner...");
141+
var response = await client.PostAsync(
142+
new Uri($"{baseUrl}/api/v4/user/runners"),
143+
new FormUrlEncodedContent(form),
144+
context.GetCancellationToken());
145+
if (!response.IsSuccessStatusCode)
146+
{
147+
_logger.LogError($"Got unexpected response from GitLab: {await response.Content.ReadAsStringAsync(context.GetCancellationToken())}");
148+
response.EnsureSuccessStatusCode();
149+
}
150+
151+
var register = await response.Content.ReadFromJsonAsync(
152+
RegisterGitLabRunnerJsonSerializerContext.Default.GitLabRegisterRunnerResponse,
153+
context.GetCancellationToken());
154+
if (register == null)
155+
{
156+
throw new InvalidOperationException("Expected non-null response from runner registration.");
157+
}
158+
159+
var idPathDirectory = Path.GetDirectoryName(spec.IdPath);
160+
var tokenPathDirectory = Path.GetDirectoryName(spec.TokenPath);
161+
if (!string.IsNullOrWhiteSpace(idPathDirectory))
162+
{
163+
Directory.CreateDirectory(idPathDirectory);
164+
}
165+
if (!string.IsNullOrWhiteSpace(tokenPathDirectory))
166+
{
167+
Directory.CreateDirectory(tokenPathDirectory);
168+
}
169+
await File.WriteAllTextAsync(
170+
spec.IdPath,
171+
register.Id.ToString(CultureInfo.InvariantCulture),
172+
context.GetCancellationToken());
173+
await File.WriteAllTextAsync(
174+
spec.TokenPath,
175+
register.Token,
176+
context.GetCancellationToken());
177+
178+
_logger.LogInformation($"Runner with ID at path {spec.IdPath} now successfully registered.");
179+
}
180+
}
181+
182+
return 0;
183+
}
184+
}
185+
}
186+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace UET.Commands.Internal.RegisterGitLabRunner
2+
{
3+
using System.Text.Json.Serialization;
4+
5+
[JsonSerializable(typeof(GitLabRunnerRegistrationSpec[]))]
6+
[JsonSerializable(typeof(GitLabGetRunnerResponse))]
7+
[JsonSerializable(typeof(GitLabRegisterRunnerResponse))]
8+
internal partial class RegisterGitLabRunnerJsonSerializerContext : JsonSerializerContext
9+
{
10+
}
11+
}

0 commit comments

Comments
 (0)