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:
Lokesh Gopu
2020-04-13 21:33:13 -04:00
committed by TingluoHuang
parent 2eefff4b69
commit 826f58682e
11 changed files with 199 additions and 43 deletions

View File

@@ -87,6 +87,7 @@ namespace GitHub.Runner.Common
public static class Args public static class Args
{ {
public static readonly string Auth = "auth"; public static readonly string Auth = "auth";
public static readonly string Labels = "labels";
public static readonly string MonitorSocketAddress = "monitorsocketaddress"; public static readonly string MonitorSocketAddress = "monitorsocketaddress";
public static readonly string Name = "name"; public static readonly string Name = "name";
public static readonly string Pool = "pool"; public static readonly string Pool = "pool";

View File

@@ -39,6 +39,7 @@ namespace GitHub.Runner.Listener
private readonly string[] validArgs = private readonly string[] validArgs =
{ {
Constants.Runner.CommandLine.Args.Auth, Constants.Runner.CommandLine.Args.Auth,
Constants.Runner.CommandLine.Args.Labels,
Constants.Runner.CommandLine.Args.MonitorSocketAddress, Constants.Runner.CommandLine.Args.MonitorSocketAddress,
Constants.Runner.CommandLine.Args.Name, Constants.Runner.CommandLine.Args.Name,
Constants.Runner.CommandLine.Args.Pool, Constants.Runner.CommandLine.Args.Pool,
@@ -249,6 +250,24 @@ namespace GitHub.Runner.Listener
return GetArg(Constants.Runner.CommandLine.Args.StartupType); 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. // Private helpers.
// //
@@ -280,7 +299,8 @@ namespace GitHub.Runner.Listener
string name, string name,
string description, string description,
string defaultValue, string defaultValue,
Func<string, bool> validator) Func<string, bool> validator,
bool isOptional = false)
{ {
// Check for the arg in the command line parser. // Check for the arg in the command line parser.
ArgUtil.NotNull(validator, nameof(validator)); 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)), secret: Constants.Runner.CommandLine.Args.Secrets.Any(x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase)),
defaultValue: defaultValue, defaultValue: defaultValue,
validator: validator, validator: validator,
unattended: Unattended); unattended: Unattended,
isOptional: isOptional);
} }
private string GetEnvArg(string name) private string GetEnvArg(string name)

View File

@@ -166,6 +166,9 @@ namespace GitHub.Runner.Listener.Configuration
_term.WriteLine(); _term.WriteLine();
var userLabels = command.GetLabels();
_term.WriteLine();
var agents = await _runnerServer.GetAgentsAsync(runnerSettings.PoolId, runnerSettings.AgentName); var agents = await _runnerServer.GetAgentsAsync(runnerSettings.PoolId, runnerSettings.AgentName);
Trace.Verbose("Returns {0} agents", agents.Count); Trace.Verbose("Returns {0} agents", agents.Count);
agent = agents.FirstOrDefault(); agent = agents.FirstOrDefault();
@@ -175,7 +178,7 @@ namespace GitHub.Runner.Listener.Configuration
if (command.GetReplace()) if (command.GetReplace())
{ {
// Update existing agent with new PublicKey, agent version. // Update existing agent with new PublicKey, agent version.
agent = UpdateExistingAgent(agent, publicKey); agent = UpdateExistingAgent(agent, publicKey, userLabels);
try try
{ {
@@ -198,7 +201,7 @@ namespace GitHub.Runner.Listener.Configuration
else else
{ {
// Create a new agent. // Create a new agent.
agent = CreateNewAgent(runnerSettings.AgentName, publicKey); agent = CreateNewAgent(runnerSettings.AgentName, publicKey, userLabels);
try 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)); ArgUtil.NotNull(agent, nameof(agent));
agent.Authorization = new TaskAgentAuthorization agent.Authorization = new TaskAgentAuthorization
@@ -456,18 +459,25 @@ namespace GitHub.Runner.Listener.Configuration
PublicKey = new TaskAgentPublicKey(publicKey.Exponent, publicKey.Modulus), 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.Version = BuildConstants.RunnerPackage.Version;
agent.OSDescription = RuntimeInformation.OSDescription; agent.OSDescription = RuntimeInformation.OSDescription;
agent.Labels.Clear();
agent.Labels.Add("self-hosted"); agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
agent.Labels.Add(VarUtil.OS); agent.Labels.Add(new AgentLabel(VarUtil.OS, LabelType.System));
agent.Labels.Add(VarUtil.OSArchitecture); agent.Labels.Add(new AgentLabel(VarUtil.OSArchitecture, LabelType.System));
foreach (var userLabel in userLabels)
{
agent.Labels.Add(new AgentLabel(userLabel, LabelType.User));
}
return agent; return agent;
} }
private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey) private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey, ISet<string> userLabels)
{ {
TaskAgent agent = new TaskAgent(agentName) TaskAgent agent = new TaskAgent(agentName)
{ {
@@ -480,9 +490,14 @@ namespace GitHub.Runner.Listener.Configuration
OSDescription = RuntimeInformation.OSDescription, OSDescription = RuntimeInformation.OSDescription,
}; };
agent.Labels.Add("self-hosted"); agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
agent.Labels.Add(VarUtil.OS); agent.Labels.Add(new AgentLabel(VarUtil.OS, LabelType.System));
agent.Labels.Add(VarUtil.OSArchitecture); agent.Labels.Add(new AgentLabel(VarUtil.OSArchitecture, LabelType.System));
foreach (var userLabel in userLabels)
{
agent.Labels.Add(new AgentLabel(userLabel, LabelType.User));
}
return agent; return agent;
} }

View File

@@ -20,7 +20,8 @@ namespace GitHub.Runner.Listener.Configuration
bool secret, bool secret,
string defaultValue, string defaultValue,
Func<String, bool> validator, Func<String, bool> validator,
bool unattended); bool unattended,
bool isOptional = false);
} }
public sealed class PromptManager : RunnerService, IPromptManager public sealed class PromptManager : RunnerService, IPromptManager
@@ -56,7 +57,8 @@ namespace GitHub.Runner.Listener.Configuration
bool secret, bool secret,
string defaultValue, string defaultValue,
Func<string, bool> validator, Func<string, bool> validator,
bool unattended) bool unattended,
bool isOptional = false)
{ {
Trace.Info(nameof(ReadValue)); Trace.Info(nameof(ReadValue));
ArgUtil.NotNull(validator, nameof(validator)); ArgUtil.NotNull(validator, nameof(validator));
@@ -85,18 +87,28 @@ namespace GitHub.Runner.Listener.Configuration
{ {
_terminal.Write($"[press Enter for {defaultValue}] "); _terminal.Write($"[press Enter for {defaultValue}] ");
} }
else if (isOptional){
_terminal.Write($"[press Enter to skip] ");
}
// Read and trim the value. // Read and trim the value.
value = secret ? _terminal.ReadSecret() : _terminal.ReadLine(); value = secret ? _terminal.ReadSecret() : _terminal.ReadLine();
value = value?.Trim() ?? string.Empty; value = value?.Trim() ?? string.Empty;
// Return the default if not specified. // 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}'"); if (!string.IsNullOrEmpty(defaultValue))
return 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. // Return the value if it is not empty and it is valid.
// Otherwise try the loop again. // Otherwise try the loop again.
if (!string.IsNullOrEmpty(value)) if (!string.IsNullOrEmpty(value))

View File

@@ -1,6 +1,7 @@
using GitHub.Runner.Common.Util; using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using System; using System;
using System.Linq;
using System.IO; using System.IO;
using System.Security.Principal; using System.Security.Principal;
@@ -46,6 +47,21 @@ namespace GitHub.Runner.Listener.Configuration
string.Equals(value, "N", StringComparison.CurrentCultureIgnoreCase); 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) public static bool NonEmptyValidator(string value)
{ {
return !string.IsNullOrEmpty(value); return !string.IsNullOrEmpty(value);

View File

@@ -82,7 +82,7 @@ namespace GitHub.DistributedTask.WebApi
httpMethod, httpMethod,
locationId, locationId,
routeValues: routeValues, routeValues: routeValues,
version: new ApiResourceVersion(5.1, 1), version: new ApiResourceVersion(6.0, 2),
userState: userState, userState: userState,
cancellationToken: cancellationToken, cancellationToken: cancellationToken,
content: content); content: content);
@@ -109,7 +109,7 @@ namespace GitHub.DistributedTask.WebApi
httpMethod, httpMethod,
locationId, locationId,
routeValues: routeValues, routeValues: routeValues,
version: new ApiResourceVersion(5.1, 1), version: new ApiResourceVersion(6.0, 2),
userState: userState, userState: userState,
cancellationToken: cancellationToken).ConfigureAwait(false)) cancellationToken: cancellationToken).ConfigureAwait(false))
{ {
@@ -164,7 +164,7 @@ namespace GitHub.DistributedTask.WebApi
httpMethod, httpMethod,
locationId, locationId,
routeValues: routeValues, routeValues: routeValues,
version: new ApiResourceVersion(5.1, 1), version: new ApiResourceVersion(6.0, 2),
queryParameters: queryParams, queryParameters: queryParams,
userState: userState, userState: userState,
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
@@ -227,7 +227,7 @@ namespace GitHub.DistributedTask.WebApi
httpMethod, httpMethod,
locationId, locationId,
routeValues: routeValues, routeValues: routeValues,
version: new ApiResourceVersion(5.1, 1), version: new ApiResourceVersion(6.0, 2),
queryParameters: queryParams, queryParameters: queryParams,
userState: userState, userState: userState,
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
@@ -257,7 +257,7 @@ namespace GitHub.DistributedTask.WebApi
httpMethod, httpMethod,
locationId, locationId,
routeValues: routeValues, routeValues: routeValues,
version: new ApiResourceVersion(5.1, 1), version: new ApiResourceVersion(6.0, 2),
userState: userState, userState: userState,
cancellationToken: cancellationToken, cancellationToken: cancellationToken,
content: content); content: content);
@@ -287,7 +287,7 @@ namespace GitHub.DistributedTask.WebApi
httpMethod, httpMethod,
locationId, locationId,
routeValues: routeValues, routeValues: routeValues,
version: new ApiResourceVersion(5.1, 1), version: new ApiResourceVersion(6.0, 2),
userState: userState, userState: userState,
cancellationToken: cancellationToken, cancellationToken: cancellationToken,
content: content); content: content);

View 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);
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Runtime.Serialization;
namespace GitHub.DistributedTask.WebApi
{
[DataContract]
public enum LabelType
{
[EnumMember]
System = 0,
[EnumMember]
User = 1
}
}

View File

@@ -51,7 +51,7 @@ namespace GitHub.DistributedTask.WebApi
if (agentToBeCloned.m_labels != null && agentToBeCloned.m_labels.Count > 0) 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> /// <summary>
/// The labels of the runner /// The labels of the runner
/// </summary> /// </summary>
public ISet<string> Labels public ISet<AgentLabel> Labels
{ {
get get
{ {
if (m_labels == null) if (m_labels == null)
{ {
m_labels = new HashSet<string>(StringComparer.OrdinalIgnoreCase); m_labels = new HashSet<AgentLabel>();
} }
return m_labels; return m_labels;
} }
@@ -164,6 +164,6 @@ namespace GitHub.DistributedTask.WebApi
private PropertiesCollection m_properties; private PropertiesCollection m_properties;
[DataMember(IsRequired = false, EmitDefaultValue = false, Name = "Labels")] [DataMember(IsRequired = false, EmitDefaultValue = false, Name = "Labels")]
private HashSet<string> m_labels; private HashSet<AgentLabel> m_labels;
} }
} }

View File

@@ -317,7 +317,8 @@ namespace GitHub.Runner.Common.Tests
false, // secret false, // secret
Environment.MachineName, // defaultValue Environment.MachineName, // defaultValue
Validators.NonEmptyValidator, // validator Validators.NonEmptyValidator, // validator
true)) // unattended true, // unattended
false)) // isOptional
.Returns("some runner"); .Returns("some runner");
// Act. // Act.
@@ -344,7 +345,8 @@ namespace GitHub.Runner.Common.Tests
false, // secret false, // secret
Environment.MachineName, // defaultValue Environment.MachineName, // defaultValue
Validators.NonEmptyValidator, // validator Validators.NonEmptyValidator, // validator
false)) // unattended false, // unattended
false)) // isOptional
.Returns("some runner"); .Returns("some runner");
// Act. // Act.
@@ -371,7 +373,8 @@ namespace GitHub.Runner.Common.Tests
false, // secret false, // secret
"some default auth", // defaultValue "some default auth", // defaultValue
Validators.AuthSchemeValidator, // validator Validators.AuthSchemeValidator, // validator
false)) // unattended false, // unattended
false)) // isOptional
.Returns("some auth"); .Returns("some auth");
// Act. // Act.
@@ -398,7 +401,8 @@ namespace GitHub.Runner.Common.Tests
true, // secret true, // secret
string.Empty, // defaultValue string.Empty, // defaultValue
Validators.NonEmptyValidator, // validator Validators.NonEmptyValidator, // validator
false)) // unattended false, // unattended
false)) // isOptional
.Returns("some token"); .Returns("some token");
// Act. // Act.
@@ -475,7 +479,8 @@ namespace GitHub.Runner.Common.Tests
true, // secret true, // secret
string.Empty, // defaultValue string.Empty, // defaultValue
Validators.NonEmptyValidator, // validator Validators.NonEmptyValidator, // validator
false)) // unattended false, // unattended
false)) // isOptional
.Returns("some token"); .Returns("some token");
// Act. // Act.
@@ -502,7 +507,8 @@ namespace GitHub.Runner.Common.Tests
true, // secret true, // secret
string.Empty, // defaultValue string.Empty, // defaultValue
Validators.NonEmptyValidator, // validator Validators.NonEmptyValidator, // validator
false)) // unattended false, // unattended
false)) // isOptional
.Returns("some token"); .Returns("some token");
// Act. // Act.
@@ -529,7 +535,8 @@ namespace GitHub.Runner.Common.Tests
false, // secret false, // secret
string.Empty, // defaultValue string.Empty, // defaultValue
Validators.ServerUrlValidator, // validator Validators.ServerUrlValidator, // validator
false)) // unattended false, // unattended
false)) // isOptional
.Returns("some url"); .Returns("some url");
// Act. // Act.
@@ -556,7 +563,8 @@ namespace GitHub.Runner.Common.Tests
false, // secret false, // secret
"some default account", // defaultValue "some default account", // defaultValue
Validators.NTAccountValidator, // validator Validators.NTAccountValidator, // validator
false)) // unattended false, // unattended
false)) // isOptional
.Returns("some windows logon account"); .Returns("some windows logon account");
// Act. // Act.
@@ -584,7 +592,8 @@ namespace GitHub.Runner.Common.Tests
true, // secret true, // secret
string.Empty, // defaultValue string.Empty, // defaultValue
Validators.NonEmptyValidator, // validator Validators.NonEmptyValidator, // validator
false)) // unattended false, // unattended
false)) // isOptional
.Returns("some windows logon password"); .Returns("some windows logon password");
// Act. // Act.
@@ -611,7 +620,8 @@ namespace GitHub.Runner.Common.Tests
false, // secret false, // secret
"_work", // defaultValue "_work", // defaultValue
Validators.NonEmptyValidator, // validator Validators.NonEmptyValidator, // validator
false)) // unattended false, // unattended
false)) // isOptional
.Returns("some work"); .Returns("some work");
// Act. // Act.
@@ -640,7 +650,8 @@ namespace GitHub.Runner.Common.Tests
false, // secret false, // secret
string.Empty, // defaultValue string.Empty, // defaultValue
Validators.ServerUrlValidator, // validator Validators.ServerUrlValidator, // validator
false)) // unattended false, // unattended
false)) // isOptional
.Returns("some url"); .Returns("some url");
// Act. // Act.
@@ -669,7 +680,8 @@ namespace GitHub.Runner.Common.Tests
false, // secret false, // secret
string.Empty, // defaultValue string.Empty, // defaultValue
Validators.ServerUrlValidator, // validator Validators.ServerUrlValidator, // validator
false)) // unattended false, // unattended
false)) // isOptional
.Returns("some url"); .Returns("some url");
// Act. // Act.

View File

@@ -145,6 +145,8 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
IConfigurationManager configManager = new ConfigurationManager(); IConfigurationManager configManager = new ConfigurationManager();
configManager.Initialize(tc); configManager.Initialize(tc);
var userLabels = "userlabel1,userlabel2";
trace.Info("Preparing command line arguments"); trace.Info("Preparing command line arguments");
var command = new CommandSettings( var command = new CommandSettings(
tc, tc,
@@ -156,7 +158,8 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
"--pool", _expectedPoolName, "--pool", _expectedPoolName,
"--work", _expectedWorkFolder, "--work", _expectedWorkFolder,
"--auth", _expectedAuthType, "--auth", _expectedAuthType,
"--token", _expectedToken "--token", _expectedToken,
"--labels", userLabels
}); });
trace.Info("Constructed."); trace.Info("Constructed.");
_store.Setup(x => x.IsConfigured()).Returns(false); _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 // 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.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);
} }
} }
} }