Files
runner/src/Runner.Listener/CommandSettings.cs
Cory Miller b18bda773f Add generateServiceConfig option for configure command (#2226)
For runners that are already configured but need to add systemd services after the fact, a new command option is added to generate the file only. Once generated, ./svc.sh install and other commands will be available.

This also adds support for falling back to the tenant name in the Actions URL in cases when the GitHub URL is not provided, such as for our hosted larger runners.
2022-10-26 08:48:23 -04:00

479 lines
18 KiB
C#

using GitHub.Runner.Listener.Configuration;
using GitHub.Runner.Common.Util;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Listener
{
public sealed class CommandSettings
{
private readonly Dictionary<string, string> _envArgs = new(StringComparer.OrdinalIgnoreCase);
private readonly CommandLineParser _parser;
private readonly IPromptManager _promptManager;
private readonly Tracing _trace;
// Valid flags for all commands
private readonly string[] genericOptions =
{
Constants.Runner.CommandLine.Flags.Help,
Constants.Runner.CommandLine.Flags.Version,
Constants.Runner.CommandLine.Flags.Commit,
Constants.Runner.CommandLine.Flags.Check
};
// Valid flags and args for specific command - key: command, value: array of valid flags and args
private readonly Dictionary<string, string[]> validOptions = new()
{
// Valid configure flags and args
[Constants.Runner.CommandLine.Commands.Configure] =
new string[]
{
Constants.Runner.CommandLine.Flags.DisableUpdate,
Constants.Runner.CommandLine.Flags.Ephemeral,
Constants.Runner.CommandLine.Flags.GenerateServiceConfig,
Constants.Runner.CommandLine.Flags.Replace,
Constants.Runner.CommandLine.Flags.RunAsService,
Constants.Runner.CommandLine.Flags.Unattended,
Constants.Runner.CommandLine.Args.Auth,
Constants.Runner.CommandLine.Args.Labels,
Constants.Runner.CommandLine.Args.MonitorSocketAddress,
Constants.Runner.CommandLine.Args.Name,
Constants.Runner.CommandLine.Args.PAT,
Constants.Runner.CommandLine.Args.RunnerGroup,
Constants.Runner.CommandLine.Args.Token,
Constants.Runner.CommandLine.Args.Url,
Constants.Runner.CommandLine.Args.UserName,
Constants.Runner.CommandLine.Args.WindowsLogonAccount,
Constants.Runner.CommandLine.Args.WindowsLogonPassword,
Constants.Runner.CommandLine.Args.Work
},
// Valid remove flags and args
[Constants.Runner.CommandLine.Commands.Remove] =
new string[]
{
Constants.Runner.CommandLine.Args.Token,
Constants.Runner.CommandLine.Args.PAT
},
// Valid run flags and args
[Constants.Runner.CommandLine.Commands.Run] =
new string[]
{
Constants.Runner.CommandLine.Flags.Once,
Constants.Runner.CommandLine.Args.JitConfig,
Constants.Runner.CommandLine.Args.StartupType
},
// valid warmup flags and args
[Constants.Runner.CommandLine.Commands.Warmup] =
new string[] { }
};
// Commands.
public bool Configure => TestCommand(Constants.Runner.CommandLine.Commands.Configure);
public bool Remove => TestCommand(Constants.Runner.CommandLine.Commands.Remove);
public bool Run => TestCommand(Constants.Runner.CommandLine.Commands.Run);
public bool Warmup => TestCommand(Constants.Runner.CommandLine.Commands.Warmup);
// Flags.
public bool Check => TestFlag(Constants.Runner.CommandLine.Flags.Check);
public bool Commit => TestFlag(Constants.Runner.CommandLine.Flags.Commit);
public bool DisableUpdate => TestFlag(Constants.Runner.CommandLine.Flags.DisableUpdate);
public bool Ephemeral => TestFlag(Constants.Runner.CommandLine.Flags.Ephemeral);
public bool GenerateServiceConfig => TestFlag(Constants.Runner.CommandLine.Flags.GenerateServiceConfig);
public bool Help => TestFlag(Constants.Runner.CommandLine.Flags.Help);
public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended);
public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version);
// Keep this around since customers still relies on it
public bool RunOnce => TestFlag(Constants.Runner.CommandLine.Flags.Once);
// Constructor.
public CommandSettings(IHostContext context, string[] args)
{
ArgUtil.NotNull(context, nameof(context));
_promptManager = context.GetService<IPromptManager>();
_trace = context.GetTrace(nameof(CommandSettings));
// Parse the command line args.
_parser = new CommandLineParser(
hostContext: context,
secretArgNames: Constants.Runner.CommandLine.Args.Secrets);
_parser.Parse(args);
// Store and remove any args passed via environment variables.
IDictionary environment = Environment.GetEnvironmentVariables();
string envPrefix = "ACTIONS_RUNNER_INPUT_";
foreach (DictionaryEntry entry in environment)
{
// Test if starts with ACTIONS_RUNNER_INPUT_.
string fullKey = entry.Key as string ?? string.Empty;
if (fullKey.StartsWith(envPrefix, StringComparison.OrdinalIgnoreCase))
{
string val = (entry.Value as string ?? string.Empty).Trim();
if (!string.IsNullOrEmpty(val))
{
// Extract the name.
string name = fullKey.Substring(envPrefix.Length);
// Mask secrets.
bool secret = Constants.Runner.CommandLine.Args.Secrets.Any(x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase));
if (secret)
{
context.SecretMasker.AddValue(val);
}
// Store the value.
_envArgs[name] = val;
}
// Remove from the environment block.
_trace.Info($"Removing env var: '{fullKey}'");
Environment.SetEnvironmentVariable(fullKey, null);
}
}
}
// Validate commandline parser result
public List<string> Validate()
{
List<string> unknowns = new();
// detect unknown commands
unknowns.AddRange(_parser.Commands.Where(x => !validOptions.Keys.Contains(x, StringComparer.OrdinalIgnoreCase)));
if (unknowns.Count == 0)
{
// detect unknown flags and args for valid commands
foreach (var command in _parser.Commands)
{
if (validOptions.TryGetValue(command, out string[] options))
{
unknowns.AddRange(_parser.Flags.Where(x => !options.Contains(x, StringComparer.OrdinalIgnoreCase) && !genericOptions.Contains(x, StringComparer.OrdinalIgnoreCase)));
unknowns.AddRange(_parser.Args.Keys.Where(x => !options.Contains(x, StringComparer.OrdinalIgnoreCase)));
}
}
}
return unknowns;
}
public string GetCommandName()
{
string command = string.Empty;
if (Configure)
{
command = Constants.Runner.CommandLine.Commands.Configure;
}
else if (Remove)
{
command = Constants.Runner.CommandLine.Commands.Remove;
}
else if (Run)
{
command = Constants.Runner.CommandLine.Commands.Run;
}
else if (Warmup)
{
command = Constants.Runner.CommandLine.Commands.Warmup;
}
return command;
}
//
// Interactive flags.
//
public bool GetReplace()
{
return TestFlagOrPrompt(
name: Constants.Runner.CommandLine.Flags.Replace,
description: "Would you like to replace the existing runner? (Y/N)",
defaultValue: false);
}
public bool GetRunAsService()
{
return TestFlagOrPrompt(
name: Constants.Runner.CommandLine.Flags.RunAsService,
description: "Would you like to run the runner as service? (Y/N)",
defaultValue: false);
}
//
// Args.
//
public string GetAuth(string defaultValue)
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.Auth,
description: "How would you like to authenticate?",
defaultValue: defaultValue,
validator: Validators.AuthSchemeValidator);
}
public string GetJitConfig()
{
return GetArg(
name: Constants.Runner.CommandLine.Args.JitConfig);
}
public string GetRunnerName()
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.Name,
description: "Enter the name of runner:",
defaultValue: Environment.MachineName ?? "myrunner",
validator: Validators.NonEmptyValidator);
}
public string GetRunnerGroupName(string defaultPoolName = null)
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.RunnerGroup,
description: "Enter the name of the runner group to add this runner to:",
defaultValue: defaultPoolName ?? "default",
validator: Validators.NonEmptyValidator);
}
public string GetToken()
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.Token,
description: "What is your pool admin oauth access token?",
defaultValue: string.Empty,
validator: Validators.NonEmptyValidator);
}
public string GetGitHubPersonalAccessToken(bool required = false)
{
if (required)
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.PAT,
description: "What is your GitHub personal access token?",
defaultValue: string.Empty,
validator: Validators.NonEmptyValidator);
}
else
{
return GetArg(name: Constants.Runner.CommandLine.Args.PAT);
}
}
public string GetRunnerRegisterToken()
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.Token,
description: "What is your runner register token?",
defaultValue: string.Empty,
validator: Validators.NonEmptyValidator);
}
public string GetRunnerDeletionToken()
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.Token,
description: "Enter runner remove token:",
defaultValue: string.Empty,
validator: Validators.NonEmptyValidator);
}
public string GetUrl(bool suppressPromptIfEmpty = false)
{
// Note, GetArg does not consume the arg (like GetArgOrPrompt does).
if (suppressPromptIfEmpty &&
string.IsNullOrEmpty(GetArg(Constants.Runner.CommandLine.Args.Url)))
{
return string.Empty;
}
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.Url,
description: "What is the URL of your repository?",
defaultValue: string.Empty,
validator: Validators.ServerUrlValidator);
}
#if OS_WINDOWS
public string GetWindowsLogonAccount(string defaultValue, string descriptionMsg)
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.WindowsLogonAccount,
description: descriptionMsg,
defaultValue: defaultValue,
validator: Validators.NTAccountValidator);
}
public string GetWindowsLogonPassword(string accountName)
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.WindowsLogonPassword,
description: $"Password for the account {accountName}",
defaultValue: string.Empty,
validator: Validators.NonEmptyValidator);
}
#endif
public string GetWork()
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.Work,
description: "Enter name of work folder:",
defaultValue: Constants.Path.WorkDirectory,
validator: Validators.NonEmptyValidator);
}
public string GetMonitorSocketAddress()
{
return GetArg(Constants.Runner.CommandLine.Args.MonitorSocketAddress);
}
// This is used to find out the source from where the Runner.Listener.exe was launched at the time of run
public string GetStartupType()
{
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 string GetArg(string name)
{
string result;
if (!_parser.Args.TryGetValue(name, out result))
{
result = GetEnvArg(name);
}
return result;
}
private void RemoveArg(string name)
{
if (_parser.Args.ContainsKey(name))
{
_parser.Args.Remove(name);
}
if (_envArgs.ContainsKey(name))
{
_envArgs.Remove(name);
}
}
private string GetArgOrPrompt(
string name,
string description,
string defaultValue,
Func<string, bool> validator,
bool isOptional = false)
{
// Check for the arg in the command line parser.
ArgUtil.NotNull(validator, nameof(validator));
string result = GetArg(name);
// Return the arg if it is not empty and is valid.
_trace.Info($"Arg '{name}': '{result}'");
if (!string.IsNullOrEmpty(result))
{
// After read the arg from input commandline args, remove it from Arg dictionary,
// This will help if bad arg value passed through CommandLine arg, when ConfigurationManager ask CommandSetting the second time,
// It will prompt for input instead of continue use the bad input.
_trace.Info($"Remove {name} from Arg dictionary.");
RemoveArg(name);
if (validator(result))
{
return result;
}
_trace.Info("Arg is invalid.");
}
// Otherwise prompt for the arg.
return _promptManager.ReadValue(
argName: name,
description: description,
secret: Constants.Runner.CommandLine.Args.Secrets.Any(x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase)),
defaultValue: defaultValue,
validator: validator,
unattended: Unattended,
isOptional: isOptional);
}
private string GetEnvArg(string name)
{
string val;
if (_envArgs.TryGetValue(name, out val) && !string.IsNullOrEmpty(val))
{
_trace.Info($"Env arg '{name}': '{val}'");
return val;
}
return null;
}
private bool TestCommand(string name)
{
bool result = _parser.IsCommand(name);
_trace.Info($"Command '{name}': '{result}'");
return result;
}
private bool TestFlag(string name)
{
bool result = _parser.Flags.Contains(name);
if (!result)
{
string envStr = GetEnvArg(name);
if (!bool.TryParse(envStr, out result))
{
result = false;
}
}
_trace.Info($"Flag '{name}': '{result}'");
return result;
}
private bool TestFlagOrPrompt(
string name,
string description,
bool defaultValue)
{
bool result = TestFlag(name);
if (!result)
{
result = _promptManager.ReadBool(
argName: name,
description: description,
defaultValue: defaultValue,
unattended: Unattended);
}
return result;
}
}
}