mirror of
https://github.com/actions/runner.git
synced 2025-12-10 20:36:49 +00:00
Added support for custom labels (#414)
* Added support for custom labels * ignore case * Added interactive config for labels * Fixing L0s * pr comments
This commit is contained in:
committed by
TingluoHuang
parent
2eefff4b69
commit
826f58682e
@@ -87,6 +87,7 @@ namespace GitHub.Runner.Common
|
||||
public static class Args
|
||||
{
|
||||
public static readonly string Auth = "auth";
|
||||
public static readonly string Labels = "labels";
|
||||
public static readonly string MonitorSocketAddress = "monitorsocketaddress";
|
||||
public static readonly string Name = "name";
|
||||
public static readonly string Pool = "pool";
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace GitHub.Runner.Listener
|
||||
private readonly string[] validArgs =
|
||||
{
|
||||
Constants.Runner.CommandLine.Args.Auth,
|
||||
Constants.Runner.CommandLine.Args.Labels,
|
||||
Constants.Runner.CommandLine.Args.MonitorSocketAddress,
|
||||
Constants.Runner.CommandLine.Args.Name,
|
||||
Constants.Runner.CommandLine.Args.Pool,
|
||||
@@ -249,6 +250,24 @@ namespace GitHub.Runner.Listener
|
||||
return GetArg(Constants.Runner.CommandLine.Args.StartupType);
|
||||
}
|
||||
|
||||
public ISet<string> GetLabels()
|
||||
{
|
||||
var labelSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
string labels = GetArgOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Args.Labels,
|
||||
description: $"This runner will have the following labels: 'self-hosted', '{VarUtil.OS}', '{VarUtil.OSArchitecture}' \nEnter any additional labels (ex. label-1,label-2):",
|
||||
defaultValue: string.Empty,
|
||||
validator: Validators.LabelsValidator,
|
||||
isOptional: true);
|
||||
|
||||
if (!string.IsNullOrEmpty(labels))
|
||||
{
|
||||
labelSet = labels.Split(',').Where(x => !string.IsNullOrEmpty(x)).ToHashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return labelSet;
|
||||
}
|
||||
|
||||
//
|
||||
// Private helpers.
|
||||
//
|
||||
@@ -280,7 +299,8 @@ namespace GitHub.Runner.Listener
|
||||
string name,
|
||||
string description,
|
||||
string defaultValue,
|
||||
Func<string, bool> validator)
|
||||
Func<string, bool> validator,
|
||||
bool isOptional = false)
|
||||
{
|
||||
// Check for the arg in the command line parser.
|
||||
ArgUtil.NotNull(validator, nameof(validator));
|
||||
@@ -311,7 +331,8 @@ namespace GitHub.Runner.Listener
|
||||
secret: Constants.Runner.CommandLine.Args.Secrets.Any(x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase)),
|
||||
defaultValue: defaultValue,
|
||||
validator: validator,
|
||||
unattended: Unattended);
|
||||
unattended: Unattended,
|
||||
isOptional: isOptional);
|
||||
}
|
||||
|
||||
private string GetEnvArg(string name)
|
||||
|
||||
@@ -166,6 +166,9 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
|
||||
_term.WriteLine();
|
||||
|
||||
var userLabels = command.GetLabels();
|
||||
_term.WriteLine();
|
||||
|
||||
var agents = await _runnerServer.GetAgentsAsync(runnerSettings.PoolId, runnerSettings.AgentName);
|
||||
Trace.Verbose("Returns {0} agents", agents.Count);
|
||||
agent = agents.FirstOrDefault();
|
||||
@@ -175,7 +178,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
if (command.GetReplace())
|
||||
{
|
||||
// Update existing agent with new PublicKey, agent version.
|
||||
agent = UpdateExistingAgent(agent, publicKey);
|
||||
agent = UpdateExistingAgent(agent, publicKey, userLabels);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -198,7 +201,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
else
|
||||
{
|
||||
// Create a new agent.
|
||||
agent = CreateNewAgent(runnerSettings.AgentName, publicKey);
|
||||
agent = CreateNewAgent(runnerSettings.AgentName, publicKey, userLabels);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -448,7 +451,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
}
|
||||
|
||||
|
||||
private TaskAgent UpdateExistingAgent(TaskAgent agent, RSAParameters publicKey)
|
||||
private TaskAgent UpdateExistingAgent(TaskAgent agent, RSAParameters publicKey, ISet<string> userLabels)
|
||||
{
|
||||
ArgUtil.NotNull(agent, nameof(agent));
|
||||
agent.Authorization = new TaskAgentAuthorization
|
||||
@@ -456,18 +459,25 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
PublicKey = new TaskAgentPublicKey(publicKey.Exponent, publicKey.Modulus),
|
||||
};
|
||||
|
||||
// update - update instead of delete so we don't lose labels etc...
|
||||
// update should replace the existing labels
|
||||
agent.Version = BuildConstants.RunnerPackage.Version;
|
||||
agent.OSDescription = RuntimeInformation.OSDescription;
|
||||
|
||||
agent.Labels.Clear();
|
||||
|
||||
agent.Labels.Add("self-hosted");
|
||||
agent.Labels.Add(VarUtil.OS);
|
||||
agent.Labels.Add(VarUtil.OSArchitecture);
|
||||
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
|
||||
agent.Labels.Add(new AgentLabel(VarUtil.OS, LabelType.System));
|
||||
agent.Labels.Add(new AgentLabel(VarUtil.OSArchitecture, LabelType.System));
|
||||
|
||||
foreach (var userLabel in userLabels)
|
||||
{
|
||||
agent.Labels.Add(new AgentLabel(userLabel, LabelType.User));
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey)
|
||||
private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey, ISet<string> userLabels)
|
||||
{
|
||||
TaskAgent agent = new TaskAgent(agentName)
|
||||
{
|
||||
@@ -480,9 +490,14 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
OSDescription = RuntimeInformation.OSDescription,
|
||||
};
|
||||
|
||||
agent.Labels.Add("self-hosted");
|
||||
agent.Labels.Add(VarUtil.OS);
|
||||
agent.Labels.Add(VarUtil.OSArchitecture);
|
||||
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
|
||||
agent.Labels.Add(new AgentLabel(VarUtil.OS, LabelType.System));
|
||||
agent.Labels.Add(new AgentLabel(VarUtil.OSArchitecture, LabelType.System));
|
||||
|
||||
foreach (var userLabel in userLabels)
|
||||
{
|
||||
agent.Labels.Add(new AgentLabel(userLabel, LabelType.User));
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
bool secret,
|
||||
string defaultValue,
|
||||
Func<String, bool> validator,
|
||||
bool unattended);
|
||||
bool unattended,
|
||||
bool isOptional = false);
|
||||
}
|
||||
|
||||
public sealed class PromptManager : RunnerService, IPromptManager
|
||||
@@ -56,7 +57,8 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
bool secret,
|
||||
string defaultValue,
|
||||
Func<string, bool> validator,
|
||||
bool unattended)
|
||||
bool unattended,
|
||||
bool isOptional = false)
|
||||
{
|
||||
Trace.Info(nameof(ReadValue));
|
||||
ArgUtil.NotNull(validator, nameof(validator));
|
||||
@@ -85,18 +87,28 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
_terminal.Write($"[press Enter for {defaultValue}] ");
|
||||
}
|
||||
else if (isOptional){
|
||||
_terminal.Write($"[press Enter to skip] ");
|
||||
}
|
||||
|
||||
// Read and trim the value.
|
||||
value = secret ? _terminal.ReadSecret() : _terminal.ReadLine();
|
||||
value = value?.Trim() ?? string.Empty;
|
||||
|
||||
// Return the default if not specified.
|
||||
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(defaultValue))
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
Trace.Info($"Falling back to the default: '{defaultValue}'");
|
||||
return defaultValue;
|
||||
if (!string.IsNullOrEmpty(defaultValue))
|
||||
{
|
||||
Trace.Info($"Falling back to the default: '{defaultValue}'");
|
||||
return defaultValue;
|
||||
}
|
||||
else if (isOptional)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Return the value if it is not empty and it is valid.
|
||||
// Otherwise try the loop again.
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Security.Principal;
|
||||
|
||||
@@ -46,6 +47,21 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
string.Equals(value, "N", StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool LabelsValidator(string labels)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(labels))
|
||||
{
|
||||
var labelSet = labels.Split(',').Where(x => !string.IsNullOrEmpty(x)).ToHashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (labelSet.Any(x => x.Length > 256))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool NonEmptyValidator(string value)
|
||||
{
|
||||
return !string.IsNullOrEmpty(value);
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace GitHub.DistributedTask.WebApi
|
||||
httpMethod,
|
||||
locationId,
|
||||
routeValues: routeValues,
|
||||
version: new ApiResourceVersion(5.1, 1),
|
||||
version: new ApiResourceVersion(6.0, 2),
|
||||
userState: userState,
|
||||
cancellationToken: cancellationToken,
|
||||
content: content);
|
||||
@@ -109,7 +109,7 @@ namespace GitHub.DistributedTask.WebApi
|
||||
httpMethod,
|
||||
locationId,
|
||||
routeValues: routeValues,
|
||||
version: new ApiResourceVersion(5.1, 1),
|
||||
version: new ApiResourceVersion(6.0, 2),
|
||||
userState: userState,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
@@ -164,7 +164,7 @@ namespace GitHub.DistributedTask.WebApi
|
||||
httpMethod,
|
||||
locationId,
|
||||
routeValues: routeValues,
|
||||
version: new ApiResourceVersion(5.1, 1),
|
||||
version: new ApiResourceVersion(6.0, 2),
|
||||
queryParameters: queryParams,
|
||||
userState: userState,
|
||||
cancellationToken: cancellationToken);
|
||||
@@ -227,7 +227,7 @@ namespace GitHub.DistributedTask.WebApi
|
||||
httpMethod,
|
||||
locationId,
|
||||
routeValues: routeValues,
|
||||
version: new ApiResourceVersion(5.1, 1),
|
||||
version: new ApiResourceVersion(6.0, 2),
|
||||
queryParameters: queryParams,
|
||||
userState: userState,
|
||||
cancellationToken: cancellationToken);
|
||||
@@ -257,7 +257,7 @@ namespace GitHub.DistributedTask.WebApi
|
||||
httpMethod,
|
||||
locationId,
|
||||
routeValues: routeValues,
|
||||
version: new ApiResourceVersion(5.1, 1),
|
||||
version: new ApiResourceVersion(6.0, 2),
|
||||
userState: userState,
|
||||
cancellationToken: cancellationToken,
|
||||
content: content);
|
||||
@@ -287,7 +287,7 @@ namespace GitHub.DistributedTask.WebApi
|
||||
httpMethod,
|
||||
locationId,
|
||||
routeValues: routeValues,
|
||||
version: new ApiResourceVersion(5.1, 1),
|
||||
version: new ApiResourceVersion(6.0, 2),
|
||||
userState: userState,
|
||||
cancellationToken: cancellationToken,
|
||||
content: content);
|
||||
|
||||
59
src/Sdk/DTWebApi/WebApi/AgentLabel.cs
Normal file
59
src/Sdk/DTWebApi/WebApi/AgentLabel.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.DistributedTask.WebApi
|
||||
{
|
||||
[DataContract]
|
||||
public class AgentLabel
|
||||
{
|
||||
[JsonConstructor]
|
||||
public AgentLabel()
|
||||
{
|
||||
}
|
||||
|
||||
public AgentLabel(string name)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Type = LabelType.System;
|
||||
}
|
||||
|
||||
public AgentLabel(string name, LabelType type)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Type = type;
|
||||
}
|
||||
|
||||
private AgentLabel(AgentLabel labelToBeCloned)
|
||||
{
|
||||
this.Id = labelToBeCloned.Id;
|
||||
this.Name = labelToBeCloned.Name;
|
||||
this.Type = labelToBeCloned.Type;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public int Id
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public LabelType Type
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public AgentLabel Clone()
|
||||
{
|
||||
return new AgentLabel(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Sdk/DTWebApi/WebApi/LabelType.cs
Normal file
14
src/Sdk/DTWebApi/WebApi/LabelType.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.DistributedTask.WebApi
|
||||
{
|
||||
[DataContract]
|
||||
public enum LabelType
|
||||
{
|
||||
[EnumMember]
|
||||
System = 0,
|
||||
|
||||
[EnumMember]
|
||||
User = 1
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ namespace GitHub.DistributedTask.WebApi
|
||||
|
||||
if (agentToBeCloned.m_labels != null && agentToBeCloned.m_labels.Count > 0)
|
||||
{
|
||||
m_labels = new HashSet<string>(agentToBeCloned.m_labels, StringComparer.OrdinalIgnoreCase);
|
||||
m_labels = new HashSet<AgentLabel>(agentToBeCloned.m_labels);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,13 +118,13 @@ namespace GitHub.DistributedTask.WebApi
|
||||
/// <summary>
|
||||
/// The labels of the runner
|
||||
/// </summary>
|
||||
public ISet<string> Labels
|
||||
public ISet<AgentLabel> Labels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_labels == null)
|
||||
{
|
||||
m_labels = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
m_labels = new HashSet<AgentLabel>();
|
||||
}
|
||||
return m_labels;
|
||||
}
|
||||
@@ -164,6 +164,6 @@ namespace GitHub.DistributedTask.WebApi
|
||||
private PropertiesCollection m_properties;
|
||||
|
||||
[DataMember(IsRequired = false, EmitDefaultValue = false, Name = "Labels")]
|
||||
private HashSet<string> m_labels;
|
||||
private HashSet<AgentLabel> m_labels;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +317,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
false, // secret
|
||||
Environment.MachineName, // defaultValue
|
||||
Validators.NonEmptyValidator, // validator
|
||||
true)) // unattended
|
||||
true, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some runner");
|
||||
|
||||
// Act.
|
||||
@@ -344,7 +345,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
false, // secret
|
||||
Environment.MachineName, // defaultValue
|
||||
Validators.NonEmptyValidator, // validator
|
||||
false)) // unattended
|
||||
false, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some runner");
|
||||
|
||||
// Act.
|
||||
@@ -371,7 +373,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
false, // secret
|
||||
"some default auth", // defaultValue
|
||||
Validators.AuthSchemeValidator, // validator
|
||||
false)) // unattended
|
||||
false, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some auth");
|
||||
|
||||
// Act.
|
||||
@@ -398,7 +401,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
true, // secret
|
||||
string.Empty, // defaultValue
|
||||
Validators.NonEmptyValidator, // validator
|
||||
false)) // unattended
|
||||
false, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some token");
|
||||
|
||||
// Act.
|
||||
@@ -475,7 +479,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
true, // secret
|
||||
string.Empty, // defaultValue
|
||||
Validators.NonEmptyValidator, // validator
|
||||
false)) // unattended
|
||||
false, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some token");
|
||||
|
||||
// Act.
|
||||
@@ -502,7 +507,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
true, // secret
|
||||
string.Empty, // defaultValue
|
||||
Validators.NonEmptyValidator, // validator
|
||||
false)) // unattended
|
||||
false, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some token");
|
||||
|
||||
// Act.
|
||||
@@ -529,7 +535,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
false, // secret
|
||||
string.Empty, // defaultValue
|
||||
Validators.ServerUrlValidator, // validator
|
||||
false)) // unattended
|
||||
false, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some url");
|
||||
|
||||
// Act.
|
||||
@@ -556,7 +563,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
false, // secret
|
||||
"some default account", // defaultValue
|
||||
Validators.NTAccountValidator, // validator
|
||||
false)) // unattended
|
||||
false, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some windows logon account");
|
||||
|
||||
// Act.
|
||||
@@ -584,7 +592,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
true, // secret
|
||||
string.Empty, // defaultValue
|
||||
Validators.NonEmptyValidator, // validator
|
||||
false)) // unattended
|
||||
false, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some windows logon password");
|
||||
|
||||
// Act.
|
||||
@@ -611,7 +620,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
false, // secret
|
||||
"_work", // defaultValue
|
||||
Validators.NonEmptyValidator, // validator
|
||||
false)) // unattended
|
||||
false, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some work");
|
||||
|
||||
// Act.
|
||||
@@ -640,7 +650,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
false, // secret
|
||||
string.Empty, // defaultValue
|
||||
Validators.ServerUrlValidator, // validator
|
||||
false)) // unattended
|
||||
false, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some url");
|
||||
|
||||
// Act.
|
||||
@@ -669,7 +680,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
false, // secret
|
||||
string.Empty, // defaultValue
|
||||
Validators.ServerUrlValidator, // validator
|
||||
false)) // unattended
|
||||
false, // unattended
|
||||
false)) // isOptional
|
||||
.Returns("some url");
|
||||
|
||||
// Act.
|
||||
|
||||
@@ -145,6 +145,8 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
||||
IConfigurationManager configManager = new ConfigurationManager();
|
||||
configManager.Initialize(tc);
|
||||
|
||||
var userLabels = "userlabel1,userlabel2";
|
||||
|
||||
trace.Info("Preparing command line arguments");
|
||||
var command = new CommandSettings(
|
||||
tc,
|
||||
@@ -156,7 +158,8 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
||||
"--pool", _expectedPoolName,
|
||||
"--work", _expectedWorkFolder,
|
||||
"--auth", _expectedAuthType,
|
||||
"--token", _expectedToken
|
||||
"--token", _expectedToken,
|
||||
"--labels", userLabels
|
||||
});
|
||||
trace.Info("Constructed.");
|
||||
_store.Setup(x => x.IsConfigured()).Returns(false);
|
||||
@@ -178,7 +181,10 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
||||
// validate GetAgentPoolsAsync gets called twice with automation pool type
|
||||
_runnerServer.Verify(x => x.GetAgentPoolsAsync(It.IsAny<string>(), It.Is<TaskAgentPoolType>(p => p == TaskAgentPoolType.Automation)), Times.Exactly(2));
|
||||
|
||||
_runnerServer.Verify(x => x.AddAgentAsync(It.IsAny<int>(), It.Is<TaskAgent>(a => a.Labels.Contains("self-hosted") && a.Labels.Contains(VarUtil.OS) && a.Labels.Contains(VarUtil.OSArchitecture))), Times.Once);
|
||||
var expectedLabels = new List<string>() { "self-hosted", VarUtil.OS, VarUtil.OSArchitecture};
|
||||
expectedLabels.AddRange(userLabels.Split(",").ToList());
|
||||
|
||||
_runnerServer.Verify(x => x.AddAgentAsync(It.IsAny<int>(), It.Is<TaskAgent>(a => a.Labels.Select(x => x.Name).ToHashSet().SetEquals(expectedLabels))), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user