diff --git a/src/Runner.Common/ConfigurationStore.cs b/src/Runner.Common/ConfigurationStore.cs index 8528e5095..576360c6f 100644 --- a/src/Runner.Common/ConfigurationStore.cs +++ b/src/Runner.Common/ConfigurationStore.cs @@ -119,8 +119,11 @@ namespace GitHub.Runner.Common CredentialData GetCredentials(); CredentialData GetMigratedCredentials(); RunnerSettings GetSettings(); + RunnerSettings GetMigratedSettings(); void SaveCredential(CredentialData credential); + void SaveMigratedCredential(CredentialData credential); void SaveSettings(RunnerSettings settings); + void SaveMigratedSettings(RunnerSettings settings); void DeleteCredential(); void DeleteMigratedCredential(); void DeleteSettings(); @@ -130,6 +133,7 @@ namespace GitHub.Runner.Common { private string _binPath; private string _configFilePath; + private string _migratedConfigFilePath; private string _credFilePath; private string _migratedCredFilePath; private string _serviceConfigFilePath; @@ -137,6 +141,7 @@ namespace GitHub.Runner.Common private CredentialData _creds; private CredentialData _migratedCreds; private RunnerSettings _settings; + private RunnerSettings _migratedSettings; public override void Initialize(IHostContext hostContext) { @@ -154,6 +159,9 @@ namespace GitHub.Runner.Common _configFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Runner); Trace.Info("ConfigFilePath: {0}", _configFilePath); + _migratedConfigFilePath = hostContext.GetConfigFile(WellKnownConfigFile.MigratedRunner); + Trace.Info("MigratedConfigFilePath: {0}", _migratedConfigFilePath); + _credFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Credentials); Trace.Info("CredFilePath: {0}", _credFilePath); @@ -169,7 +177,7 @@ namespace GitHub.Runner.Common public bool HasCredentials() { Trace.Info("HasCredentials()"); - bool credsStored = (new FileInfo(_credFilePath)).Exists || (new FileInfo(_migratedCredFilePath)).Exists; + bool credsStored = new FileInfo(_credFilePath).Exists || new FileInfo(_migratedCredFilePath).Exists; Trace.Info("stored {0}", credsStored); return credsStored; } @@ -177,7 +185,7 @@ namespace GitHub.Runner.Common public bool IsConfigured() { Trace.Info("IsConfigured()"); - bool configured = new FileInfo(_configFilePath).Exists; + bool configured = new FileInfo(_configFilePath).Exists || new FileInfo(_migratedConfigFilePath).Exists; Trace.Info("IsConfigured: {0}", configured); return configured; } @@ -185,7 +193,7 @@ namespace GitHub.Runner.Common public bool IsServiceConfigured() { Trace.Info("IsServiceConfigured()"); - bool serviceConfigured = (new FileInfo(_serviceConfigFilePath)).Exists; + bool serviceConfigured = new FileInfo(_serviceConfigFilePath).Exists; Trace.Info($"IsServiceConfigured: {serviceConfigured}"); return serviceConfigured; } @@ -229,6 +237,25 @@ namespace GitHub.Runner.Common return _settings; } + public RunnerSettings GetMigratedSettings() + { + if (_migratedSettings == null) + { + RunnerSettings configuredSettings = null; + if (File.Exists(_migratedConfigFilePath)) + { + string json = File.ReadAllText(_migratedConfigFilePath, Encoding.UTF8); + Trace.Info($"Read migrated setting file: {json.Length} chars"); + configuredSettings = StringUtil.ConvertFromJson(json); + } + + ArgUtil.NotNull(configuredSettings, nameof(configuredSettings)); + _migratedSettings = configuredSettings; + } + + return _migratedSettings; + } + public void SaveCredential(CredentialData credential) { Trace.Info("Saving {0} credential @ {1}", credential.Scheme, _credFilePath); @@ -244,6 +271,21 @@ namespace GitHub.Runner.Common File.SetAttributes(_credFilePath, File.GetAttributes(_credFilePath) | FileAttributes.Hidden); } + public void SaveMigratedCredential(CredentialData credential) + { + Trace.Info("Saving {0} migrated credential @ {1}", credential.Scheme, _migratedCredFilePath); + if (File.Exists(_migratedCredFilePath)) + { + // Delete existing credential file first, since the file is hidden and not able to overwrite. + Trace.Info("Delete exist runner migrated credential file."); + IOUtil.DeleteFile(_migratedCredFilePath); + } + + IOUtil.SaveObject(credential, _migratedCredFilePath); + Trace.Info("Migrated Credentials Saved."); + File.SetAttributes(_migratedCredFilePath, File.GetAttributes(_migratedCredFilePath) | FileAttributes.Hidden); + } + public void SaveSettings(RunnerSettings settings) { Trace.Info("Saving runner settings."); @@ -259,6 +301,21 @@ namespace GitHub.Runner.Common File.SetAttributes(_configFilePath, File.GetAttributes(_configFilePath) | FileAttributes.Hidden); } + public void SaveMigratedSettings(RunnerSettings settings) + { + Trace.Info("Saving runner migrated settings"); + if (File.Exists(_migratedConfigFilePath)) + { + // Delete existing settings file first, since the file is hidden and not able to overwrite. + Trace.Info("Delete exist runner migrated settings file."); + IOUtil.DeleteFile(_migratedConfigFilePath); + } + + IOUtil.SaveObject(settings, _migratedConfigFilePath); + Trace.Info("Migrated Settings Saved."); + File.SetAttributes(_migratedConfigFilePath, File.GetAttributes(_migratedConfigFilePath) | FileAttributes.Hidden); + } + public void DeleteCredential() { IOUtil.Delete(_credFilePath, default(CancellationToken)); @@ -273,6 +330,12 @@ namespace GitHub.Runner.Common public void DeleteSettings() { IOUtil.Delete(_configFilePath, default(CancellationToken)); + IOUtil.Delete(_migratedConfigFilePath, default(CancellationToken)); + } + + public void DeleteMigratedSettings() + { + IOUtil.Delete(_migratedConfigFilePath, default(CancellationToken)); } } } diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index 53628d51a..041b5be9a 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -18,6 +18,7 @@ namespace GitHub.Runner.Common public enum WellKnownConfigFile { Runner, + MigratedRunner, Credentials, MigratedCredentials, RSACredentials, diff --git a/src/Runner.Common/HostContext.cs b/src/Runner.Common/HostContext.cs index db0c54226..8475cd43b 100644 --- a/src/Runner.Common/HostContext.cs +++ b/src/Runner.Common/HostContext.cs @@ -343,6 +343,12 @@ namespace GitHub.Runner.Common ".runner"); break; + case WellKnownConfigFile.MigratedRunner: + path = Path.Combine( + GetDirectory(WellKnownDirectory.Root), + ".runner_migrated"); + break; + case WellKnownConfigFile.Credentials: path = Path.Combine( GetDirectory(WellKnownDirectory.Root), diff --git a/src/Runner.Common/RunnerServer.cs b/src/Runner.Common/RunnerServer.cs index 139ac684f..b2e4e498a 100644 --- a/src/Runner.Common/RunnerServer.cs +++ b/src/Runner.Common/RunnerServer.cs @@ -1,11 +1,11 @@ -using GitHub.DistributedTask.WebApi; -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using GitHub.Services.WebApi; -using GitHub.Services.Common; +using GitHub.DistributedTask.WebApi; using GitHub.Runner.Sdk; +using GitHub.Services.Common; +using GitHub.Services.WebApi; namespace GitHub.Runner.Common { @@ -50,7 +50,10 @@ namespace GitHub.Runner.Common Task GetPackageAsync(string packageType, string platform, string version, bool includeToken, CancellationToken cancellationToken); // agent update - Task UpdateAgentUpdateStateAsync(int agentPoolId, ulong agentId, string currentState, string trace); + Task UpdateAgentUpdateStateAsync(int agentPoolId, ulong agentId, string currentState, string trace, CancellationToken cancellationToken = default); + + // runner config refresh + Task RefreshRunnerConfigAsync(int agentId, string configType, string encodedRunnerConfig, CancellationToken cancellationToken); } public sealed class RunnerServer : RunnerService, IRunnerServer @@ -315,10 +318,17 @@ namespace GitHub.Runner.Common return _genericTaskAgentClient.GetPackageAsync(packageType, platform, version, includeToken, cancellationToken: cancellationToken); } - public Task UpdateAgentUpdateStateAsync(int agentPoolId, ulong agentId, string currentState, string trace) + public Task UpdateAgentUpdateStateAsync(int agentPoolId, ulong agentId, string currentState, string trace, CancellationToken cancellationToken = default) { CheckConnection(RunnerConnectionType.Generic); - return _genericTaskAgentClient.UpdateAgentUpdateStateAsync(agentPoolId, agentId, currentState, trace); + return _genericTaskAgentClient.UpdateAgentUpdateStateAsync(agentPoolId, agentId, currentState, trace, cancellationToken: cancellationToken); + } + + // runner config refresh + public Task RefreshRunnerConfigAsync(int agentId, string configType, string encodedRunnerConfig, CancellationToken cancellationToken) + { + CheckConnection(RunnerConnectionType.Generic); + return _genericTaskAgentClient.RefreshRunnerConfigAsync(agentId, configType, encodedRunnerConfig, cancellationToken: cancellationToken); } } } diff --git a/src/Runner.Listener/MessageListener.cs b/src/Runner.Listener/MessageListener.cs index d7bf9ba32..fcff3a507 100644 --- a/src/Runner.Listener/MessageListener.cs +++ b/src/Runner.Listener/MessageListener.cs @@ -533,7 +533,8 @@ namespace GitHub.Runner.Listener } else if (ex is TaskAgentPoolNotFoundException || ex is AccessDeniedException || - ex is VssUnauthorizedException) + ex is VssUnauthorizedException || + (ex is VssOAuthTokenRequestException oauthEx && oauthEx.Error != "server_error")) { Trace.Info($"Non-retriable exception: {ex.Message}"); return false; diff --git a/src/Runner.Listener/Runner.cs b/src/Runner.Listener/Runner.cs index 850f128c0..28b65d877 100644 --- a/src/Runner.Listener/Runner.cs +++ b/src/Runner.Listener/Runner.cs @@ -635,6 +635,17 @@ namespace GitHub.Runner.Listener Trace.Info("Received ForceTokenRefreshMessage"); await _listener.RefreshListenerTokenAsync(messageQueueLoopTokenSource.Token); } + else if (string.Equals(message.MessageType, RunnerRefreshConfigMessage.MessageType)) + { + var runnerRefreshConfigMessage = JsonUtility.FromString(message.Body); + Trace.Info($"Received RunnerRefreshConfigMessage for '{runnerRefreshConfigMessage.ConfigType}' config file"); + var configUpdater = HostContext.GetService(); + await configUpdater.UpdateRunnerConfigAsync( + runnerQualifiedId: runnerRefreshConfigMessage.RunnerQualifiedId, + configType: runnerRefreshConfigMessage.ConfigType, + serviceType: runnerRefreshConfigMessage.ServiceType, + configRefreshUrl: runnerRefreshConfigMessage.ConfigRefreshUrl); + } else { Trace.Error($"Received message {message.MessageId} with unsupported message type {message.MessageType}."); diff --git a/src/Runner.Listener/RunnerConfigUpdater.cs b/src/Runner.Listener/RunnerConfigUpdater.cs new file mode 100644 index 000000000..34c4fea44 --- /dev/null +++ b/src/Runner.Listener/RunnerConfigUpdater.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using GitHub.Runner.Common; +using GitHub.Runner.Sdk; +using GitHub.Services.Common; + +namespace GitHub.Runner.Listener +{ + [ServiceLocator(Default = typeof(RunnerConfigUpdater))] + public interface IRunnerConfigUpdater : IRunnerService + { + Task UpdateRunnerConfigAsync(string runnerQualifiedId, string configType, string serviceType, string configRefreshUrl); + } + + public sealed class RunnerConfigUpdater : RunnerService, IRunnerConfigUpdater + { + private RunnerSettings _settings; + private CredentialData _credData; + private IRunnerServer _runnerServer; + private IConfigurationStore _store; + + public override void Initialize(IHostContext hostContext) + { + base.Initialize(hostContext); + _store = hostContext.GetService(); + _settings = _store.GetSettings(); + _credData = _store.GetCredentials(); + _runnerServer = HostContext.GetService(); + } + + public async Task UpdateRunnerConfigAsync(string runnerQualifiedId, string configType, string serviceType, string configRefreshUrl) + { + Trace.Entering(); + try + { + ArgUtil.NotNullOrEmpty(runnerQualifiedId, nameof(runnerQualifiedId)); + ArgUtil.NotNullOrEmpty(configType, nameof(configType)); + ArgUtil.NotNullOrEmpty(serviceType, nameof(serviceType)); + ArgUtil.NotNullOrEmpty(configRefreshUrl, nameof(configRefreshUrl)); + + // make sure the runner qualified id matches the current runner + if (!await VerifyRunnerQualifiedId(runnerQualifiedId)) + { + return; + } + + // keep the timeout short to avoid blocking the main thread + using (var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30))) + { + switch (configType.ToLowerInvariant()) + { + case "runner": + await UpdateRunnerSettingsAsync(serviceType, configRefreshUrl, tokenSource.Token); + break; + case "credentials": + await UpdateRunnerCredentialsAsync(serviceType, configRefreshUrl, tokenSource.Token); + break; + default: + Trace.Error($"Invalid config type '{configType}'."); + await ReportTelemetryAsync($"Invalid config type '{configType}'."); + return; + } + } + } + catch (Exception ex) + { + Trace.Error($"Failed to update runner '{configType}' config."); + Trace.Error(ex); + await ReportTelemetryAsync($"Failed to update runner '{configType}' config: {ex}"); + } + } + + private async Task UpdateRunnerSettingsAsync(string serviceType, string configRefreshUrl, CancellationToken token) + { + Trace.Entering(); + // read the current runner settings and encode with base64 + var runnerConfig = HostContext.GetConfigFile(WellKnownConfigFile.Runner); + string runnerConfigContent = File.ReadAllText(runnerConfig, Encoding.UTF8); + var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(runnerConfigContent)); + if (string.IsNullOrEmpty(encodedConfig)) + { + await ReportTelemetryAsync("Failed to get encoded runner settings."); + return; + } + + // exchange the encoded runner settings with the service + string refreshedEncodedConfig = await RefreshRunnerConfigAsync(encodedConfig, serviceType, "runner", configRefreshUrl, token); + if (string.IsNullOrEmpty(refreshedEncodedConfig)) + { + // service will return empty string if there is no change in the config + return; + } + + var decodedConfig = Encoding.UTF8.GetString(Convert.FromBase64String(refreshedEncodedConfig)); + RunnerSettings refreshedRunnerConfig; + try + { + refreshedRunnerConfig = StringUtil.ConvertFromJson(decodedConfig); + } + catch (Exception ex) + { + Trace.Error($"Failed to convert runner config from json '{decodedConfig}'."); + Trace.Error(ex); + await ReportTelemetryAsync($"Failed to convert runner config '{decodedConfig}' from json: {ex}"); + return; + } + + // make sure the runner id and name in the refreshed config match the current runner + if (refreshedRunnerConfig?.AgentId != _settings.AgentId) + { + Trace.Error($"Runner id in refreshed config '{refreshedRunnerConfig?.AgentId.ToString() ?? "Empty"}' does not match the current runner '{_settings.AgentId}'."); + await ReportTelemetryAsync($"Runner id in refreshed config '{refreshedRunnerConfig?.AgentId.ToString() ?? "Empty"}' does not match the current runner '{_settings.AgentId}'."); + return; + } + + if (refreshedRunnerConfig?.AgentName != _settings.AgentName) + { + Trace.Error($"Runner name in refreshed config '{refreshedRunnerConfig?.AgentName ?? "Empty"}' does not match the current runner '{_settings.AgentName}'."); + await ReportTelemetryAsync($"Runner name in refreshed config '{refreshedRunnerConfig?.AgentName ?? "Empty"}' does not match the current runner '{_settings.AgentName}'."); + return; + } + + // save the refreshed runner settings as a separate file + _store.SaveMigratedSettings(refreshedRunnerConfig); + await ReportTelemetryAsync("Runner settings updated successfully."); + } + + private async Task UpdateRunnerCredentialsAsync(string serviceType, string configRefreshUrl, CancellationToken token) + { + Trace.Entering(); + // read the current runner credentials and encode with base64 + var credConfig = HostContext.GetConfigFile(WellKnownConfigFile.Credentials); + string credConfigContent = File.ReadAllText(credConfig, Encoding.UTF8); + var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(credConfigContent)); + if (string.IsNullOrEmpty(encodedConfig)) + { + await ReportTelemetryAsync("Failed to get encoded credentials."); + return; + } + + CredentialData currentCred = _store.GetCredentials(); + if (currentCred == null) + { + await ReportTelemetryAsync("Failed to get current credentials."); + return; + } + + // we only support refreshing OAuth credentials which is used by self-hosted runners. + if (currentCred.Scheme != Constants.Configuration.OAuth) + { + await ReportTelemetryAsync($"Not supported credential scheme '{currentCred.Scheme}'."); + return; + } + + // exchange the encoded runner credentials with the service + string refreshedEncodedConfig = await RefreshRunnerConfigAsync(encodedConfig, serviceType, "credentials", configRefreshUrl, token); + if (string.IsNullOrEmpty(refreshedEncodedConfig)) + { + // service will return empty string if there is no change in the config + return; + } + + var decodedConfig = Encoding.UTF8.GetString(Convert.FromBase64String(refreshedEncodedConfig)); + CredentialData refreshedCredConfig; + try + { + refreshedCredConfig = StringUtil.ConvertFromJson(decodedConfig); + } + catch (Exception ex) + { + Trace.Error($"Failed to convert credentials config from json '{decodedConfig}'."); + Trace.Error(ex); + await ReportTelemetryAsync($"Failed to convert credentials config '{decodedConfig}' from json: {ex}"); + return; + } + + // make sure the credential scheme in the refreshed config match the current credential scheme + if (refreshedCredConfig?.Scheme != _credData.Scheme) + { + Trace.Error($"Credential scheme in refreshed config '{refreshedCredConfig?.Scheme ?? "Empty"}' does not match the current credential scheme '{_credData.Scheme}'."); + await ReportTelemetryAsync($"Credential scheme in refreshed config '{refreshedCredConfig?.Scheme ?? "Empty"}' does not match the current credential scheme '{_credData.Scheme}'."); + return; + } + + if (_credData.Scheme == Constants.Configuration.OAuth) + { + // make sure the credential clientId in the refreshed config match the current credential clientId for OAuth auth scheme + var clientId = _credData.Data.GetValueOrDefault("clientId", null); + var refreshedClientId = refreshedCredConfig.Data.GetValueOrDefault("clientId", null); + if (clientId != refreshedClientId) + { + Trace.Error($"Credential clientId in refreshed config '{refreshedClientId ?? "Empty"}' does not match the current credential clientId '{clientId}'."); + await ReportTelemetryAsync($"Credential clientId in refreshed config '{refreshedClientId ?? "Empty"}' does not match the current credential clientId '{clientId}'."); + return; + } + } + + // save the refreshed runner credentials as a separate file + _store.SaveMigratedCredential(refreshedCredConfig); + await ReportTelemetryAsync("Runner credentials updated successfully."); + } + + private async Task VerifyRunnerQualifiedId(string runnerQualifiedId) + { + Trace.Entering(); + Trace.Info($"Verifying runner qualified id: {runnerQualifiedId}"); + var idParts = runnerQualifiedId.Split("/", StringSplitOptions.RemoveEmptyEntries); + if (idParts.Length != 4 || idParts[3] != _settings.AgentId.ToString()) + { + Trace.Error($"Runner qualified id '{runnerQualifiedId}' does not match the current runner '{_settings.AgentId}'."); + await ReportTelemetryAsync($"Runner qualified id '{runnerQualifiedId}' does not match the current runner '{_settings.AgentId}'."); + return false; + } + return true; + } + + private async Task RefreshRunnerConfigAsync(string encodedConfig, string serviceType, string configType, string configRefreshUrl, CancellationToken token) + { + string refreshedEncodedConfig; + switch (serviceType.ToLowerInvariant()) + { + case "pipelines": + try + { + refreshedEncodedConfig = await _runnerServer.RefreshRunnerConfigAsync((int)_settings.AgentId, configType, encodedConfig, token); + } + catch (Exception ex) + { + Trace.Error($"Failed to refresh runner {configType} config with service."); + Trace.Error(ex); + await ReportTelemetryAsync($"Failed to refresh {configType} config: {ex}"); + return null; + } + break; + case "runner-admin": + throw new NotSupportedException("Runner admin service is not supported."); + default: + Trace.Error($"Invalid service type '{serviceType}'."); + await ReportTelemetryAsync($"Invalid service type '{serviceType}'."); + return null; + } + + return refreshedEncodedConfig; + } + + private async Task ReportTelemetryAsync(string telemetry) + { + Trace.Entering(); + try + { + using (var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30))) + { + await _runnerServer.UpdateAgentUpdateStateAsync(_settings.PoolId, _settings.AgentId, "RefreshConfig", telemetry, tokenSource.Token); + } + } + catch (Exception ex) + { + Trace.Error("Failed to report telemetry."); + Trace.Error(ex); + } + } + } +} diff --git a/src/Sdk/DTGenerated/Generated/TaskAgentHttpClientBase.cs b/src/Sdk/DTGenerated/Generated/TaskAgentHttpClientBase.cs index a084664b3..3ca676594 100644 --- a/src/Sdk/DTGenerated/Generated/TaskAgentHttpClientBase.cs +++ b/src/Sdk/DTGenerated/Generated/TaskAgentHttpClientBase.cs @@ -23,8 +23,8 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; using System.Net.Http.Formatting; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using GitHub.Services.Common; @@ -827,5 +827,36 @@ namespace GitHub.DistributedTask.WebApi userState: userState, cancellationToken: cancellationToken); } + + /// + /// [Preview API] + /// + /// + /// + /// + /// + /// The cancellation token to cancel operation. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual Task RefreshRunnerConfigAsync( + int agentId, + string configType, + string encodedRunnerConfig, + object userState = null, + CancellationToken cancellationToken = default) + { + HttpMethod httpMethod = new HttpMethod("POST"); + Guid locationId = new Guid("13b5d709-74aa-470b-a8e9-bf9f3ded3f18"); + object routeValues = new { agentId = agentId, configType = configType }; + HttpContent content = new ObjectContent(encodedRunnerConfig, new VssJsonMediaTypeFormatter(true)); + + return SendAsync( + httpMethod, + locationId, + routeValues: routeValues, + version: new ApiResourceVersion(6.0, 1), + userState: userState, + cancellationToken: cancellationToken, + content: content); + } } } diff --git a/src/Sdk/DTWebApi/WebApi/RunnerRefreshConfigMessage.cs b/src/Sdk/DTWebApi/WebApi/RunnerRefreshConfigMessage.cs new file mode 100644 index 000000000..064ce928a --- /dev/null +++ b/src/Sdk/DTWebApi/WebApi/RunnerRefreshConfigMessage.cs @@ -0,0 +1,58 @@ +using System; +using System.Runtime.Serialization; +using GitHub.Services.WebApi; +using Newtonsoft.Json; + +namespace GitHub.DistributedTask.WebApi +{ + [DataContract] + public sealed class RunnerRefreshConfigMessage + { + public static readonly String MessageType = "RunnerRefreshConfig"; + + [JsonConstructor] + internal RunnerRefreshConfigMessage() + { + } + + public RunnerRefreshConfigMessage( + string runnerQualifiedId, + string configType, + string serviceType, + string configRefreshUrl) + { + this.RunnerQualifiedId = runnerQualifiedId; + this.ConfigType = configType; + this.ServiceType = serviceType; + this.ConfigRefreshUrl = configRefreshUrl; + } + + [DataMember(Name = "runnerQualifiedId")] + public String RunnerQualifiedId + { + get; + private set; + } + + [DataMember(Name = "configType")] + public String ConfigType + { + get; + private set; + } + + [DataMember(Name = "serviceType")] + public String ServiceType + { + get; + private set; + } + + [DataMember(Name = "configRefreshURL")] + public String ConfigRefreshUrl + { + get; + private set; + } + } +} diff --git a/src/Test/L0/Listener/RunnerConfigUpdaterTests.cs b/src/Test/L0/Listener/RunnerConfigUpdaterTests.cs new file mode 100644 index 000000000..70ee07cc3 --- /dev/null +++ b/src/Test/L0/Listener/RunnerConfigUpdaterTests.cs @@ -0,0 +1,579 @@ +using System; +using System.Threading.Tasks; +using GitHub.Runner.Listener; +using GitHub.Runner.Common; +using GitHub.Runner.Sdk; +using Moq; +using Xunit; +using System.Threading; +using GitHub.Runner.Common.Tests; +using System.Text; + +namespace GitHub.Runner.Tests.Listener +{ + public class RunnerConfigUpdaterL0 + { + private Mock _configurationStore; + private Mock _runnerServer; + + public RunnerConfigUpdaterL0() + { + _configurationStore = new Mock(); + _runnerServer = new Mock(); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_InvalidRunnerQualifiedId_ShouldReportTelemetry() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var invalidRunnerQualifiedId = "invalid/runner/qualified/id"; + var configType = "runner"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(invalidRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is((s) => s.Contains("Runner qualified id")), It.IsAny()), Times.Once); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_ValidRunnerQualifiedId_ShouldNotReportTelemetry() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "runner"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is((s) => s.Contains("Runner qualified id")), It.IsAny()), Times.Never); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_InvalidConfigType_ShouldReportTelemetry() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var invalidConfigType = "invalidConfigType"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, invalidConfigType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is((s) => s.Contains("Invalid config type")), It.IsAny()), Times.Once); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_UpdateRunnerSettings_ShouldSucceed() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + + var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(setting))); + _runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny(), It.Is(s => s == "runner"), It.IsAny(), It.IsAny())).ReturnsAsync(encodedConfig); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "runner"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.RefreshRunnerConfigAsync(1, "runner", It.IsAny(), It.IsAny()), Times.Once); + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is(s => s.Contains("Runner settings updated successfully")), It.IsAny()), Times.Once); + _configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny()), Times.Once); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_UpdateRunnerSettings_IgnoredEmptyRefreshResult() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "runner"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.RefreshRunnerConfigAsync(1, "runner", It.IsAny(), It.IsAny()), Times.Once); + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is(s => s.Contains("Runner settings updated successfully")), It.IsAny()), Times.Never); + _configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny()), Times.Never); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_UpdateRunnerCredentials_ShouldSucceed() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + var credData = new CredentialData + { + Scheme = "OAuth" + }; + credData.Data.Add("ClientId", "12345"); + _configurationStore.Setup(x => x.GetCredentials()).Returns(credData); + + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + IOUtil.SaveObject(credData, hc.GetConfigFile(WellKnownConfigFile.Credentials)); + + var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(credData))); + _runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny(), It.Is(s => s == "credentials"), It.IsAny(), It.IsAny())).ReturnsAsync(encodedConfig); + + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "credentials"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.RefreshRunnerConfigAsync(1, "credentials", It.IsAny(), It.IsAny()), Times.Once); + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is(s => s.Contains("Runner credentials updated successfully")), It.IsAny()), Times.Once); + _configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny()), Times.Once); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_UpdateRunnerCredentials_IgnoredEmptyRefreshResult() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + var credData = new CredentialData + { + Scheme = "OAuth" + }; + credData.Data.Add("ClientId", "12345"); + _configurationStore.Setup(x => x.GetCredentials()).Returns(credData); + + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + IOUtil.SaveObject(credData, hc.GetConfigFile(WellKnownConfigFile.Credentials)); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "credentials"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.RefreshRunnerConfigAsync(1, "credentials", It.IsAny(), It.IsAny()), Times.Once); + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is(s => s.Contains("Runner credentials updated successfully")), It.IsAny()), Times.Never); + _configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny()), Times.Never); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_RefreshRunnerSettingsFailure_ShouldReportTelemetry() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + _runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny(), It.Is(s => s == "runner"), It.IsAny(), It.IsAny())).ThrowsAsync(new Exception("Refresh failed")); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "runner"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is((s) => s.Contains("Failed to refresh")), It.IsAny()), Times.Once); + _configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny()), Times.Never); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_RefreshRunnerCredetialsFailure_ShouldReportTelemetry() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + var credData = new CredentialData + { + Scheme = "OAuth" + }; + credData.Data.Add("ClientId", "12345"); + _configurationStore.Setup(x => x.GetCredentials()).Returns(credData); + + _runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny(), It.Is(s => s == "credentials"), It.IsAny(), It.IsAny())).ThrowsAsync(new Exception("Refresh failed")); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "credentials"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is((s) => s.Contains("Failed to refresh")), It.IsAny()), Times.Once); + _configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny()), Times.Never); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_RefreshRunnerSettingsWithDifferentRunnerId_ShouldReportTelemetry() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + + var differentRunnerSetting = new RunnerSettings { AgentId = 2, AgentName = "agent1" }; + var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(differentRunnerSetting))); + _runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny(), It.Is(s => s == "runner"), It.IsAny(), It.IsAny())).ReturnsAsync(encodedConfig); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "runner"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is(s => s.Contains("Runner id in refreshed config")), It.IsAny()), Times.Once); + _configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny()), Times.Never); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_RefreshRunnerSettingsWithDifferentRunnerName_ShouldReportTelemetry() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + + var differentRunnerSetting = new RunnerSettings { AgentId = 1, AgentName = "agent2" }; + var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(differentRunnerSetting))); + _runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny(), It.Is(s => s == "runner"), It.IsAny(), It.IsAny())).ReturnsAsync(encodedConfig); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "runner"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is(s => s.Contains("Runner name in refreshed config")), It.IsAny()), Times.Once); + _configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny()), Times.Never); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_RefreshCredentialsWithDifferentScheme_ShouldReportTelemetry() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + var credData = new CredentialData + { + Scheme = "OAuth" + }; + credData.Data.Add("ClientId", "12345"); + _configurationStore.Setup(x => x.GetCredentials()).Returns(credData); + + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + IOUtil.SaveObject(credData, hc.GetConfigFile(WellKnownConfigFile.Credentials)); + + var differentCredData = new CredentialData + { + Scheme = "PAT" + }; + differentCredData.Data.Add("ClientId", "12345"); + var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(differentCredData))); + _runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny(), It.Is(s => s == "credentials"), It.IsAny(), It.IsAny())).ReturnsAsync(encodedConfig); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "credentials"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is(s => s.Contains("Credential scheme in refreshed config")), It.IsAny()), Times.Once); + _configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny()), Times.Never); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_RefreshOAuthCredentialsWithDifferentClientId_ShouldReportTelemetry() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + var credData = new CredentialData + { + Scheme = "OAuth" + }; + credData.Data.Add("clientId", "12345"); + _configurationStore.Setup(x => x.GetCredentials()).Returns(credData); + + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + IOUtil.SaveObject(credData, hc.GetConfigFile(WellKnownConfigFile.Credentials)); + + var differentCredData = new CredentialData + { + Scheme = "OAuth" + }; + differentCredData.Data.Add("clientId", "67890"); + var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(differentCredData))); + _runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny(), It.Is(s => s == "credentials"), It.IsAny(), It.IsAny())).ReturnsAsync(encodedConfig); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "credentials"; + var serviceType = "pipelines"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is(s => s.Contains("Credential clientId in refreshed config")), It.IsAny()), Times.Once); + _configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny()), Times.Never); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_UnsupportedServiceType_ShouldReportTelemetry() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "runner"; + var serviceType = "unsupported-service"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is((s) => s.Contains("Invalid service type")), It.IsAny()), Times.Once); + _runnerServer.Verify(x => x.RefreshRunnerConfigAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + _configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny()), Times.Never); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Runner")] + public async Task UpdateRunnerConfigAsync_RunnerAdminService_ShouldThrowNotSupported() + { + using (var hc = new TestHostContext(this)) + { + hc.SetSingleton(_configurationStore.Object); + hc.SetSingleton(_runnerServer.Object); + + // Arrange + var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" }; + _configurationStore.Setup(x => x.GetSettings()).Returns(setting); + IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner)); + + var _runnerConfigUpdater = new RunnerConfigUpdater(); + _runnerConfigUpdater.Initialize(hc); + + var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; + var configType = "runner"; + var serviceType = "runner-admin"; + var configRefreshUrl = "http://example.com"; + + // Act + await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl); + + // Assert + _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.Is((s) => s.Contains("Runner admin service is not supported")), It.IsAny()), Times.Once); + _runnerServer.Verify(x => x.RefreshRunnerConfigAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + _configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny()), Times.Never); + } + } + } +} diff --git a/src/Test/L0/Listener/SelfUpdaterL0.cs b/src/Test/L0/Listener/SelfUpdaterL0.cs index 26ba65e71..be095ce90 100644 --- a/src/Test/L0/Listener/SelfUpdaterL0.cs +++ b/src/Test/L0/Listener/SelfUpdaterL0.cs @@ -107,8 +107,8 @@ namespace GitHub.Runner.Common.Tests.Listener hc.EnqueueInstance(p3); updater.Initialize(hc); - _runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny(), It.IsAny())) - .Callback((int p, ulong a, string s, string t) => + _runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((int p, ulong a, string s, string t, CancellationToken token) => { hc.GetTrace().Info(t); }) @@ -168,8 +168,8 @@ namespace GitHub.Runner.Common.Tests.Listener _runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.200.0", true, It.IsAny())) .Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.200.0"), DownloadUrl = _packageUrl })); - _runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny(), It.IsAny())) - .Callback((int p, ulong a, string s, string t) => + _runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((int p, ulong a, string s, string t, CancellationToken token) => { hc.GetTrace().Info(t); }) @@ -220,8 +220,8 @@ namespace GitHub.Runner.Common.Tests.Listener hc.EnqueueInstance(p3); updater.Initialize(hc); - _runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny(), It.IsAny())) - .Callback((int p, ulong a, string s, string t) => + _runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((int p, ulong a, string s, string t, CancellationToken token) => { hc.GetTrace().Info(t); }) @@ -273,8 +273,8 @@ namespace GitHub.Runner.Common.Tests.Listener hc.EnqueueInstance(p3); updater.Initialize(hc); - _runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny(), It.IsAny())) - .Callback((int p, ulong a, string s, string t) => + _runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((int p, ulong a, string s, string t, CancellationToken token) => { hc.GetTrace().Info(t); }) diff --git a/src/Test/L0/TestHostContext.cs b/src/Test/L0/TestHostContext.cs index 124bcd5bd..2818215e3 100644 --- a/src/Test/L0/TestHostContext.cs +++ b/src/Test/L0/TestHostContext.cs @@ -256,12 +256,24 @@ namespace GitHub.Runner.Common.Tests ".agent"); break; + case WellKnownConfigFile.MigratedRunner: + path = Path.Combine( + GetDirectory(WellKnownDirectory.Root), + ".agent_migrated"); + break; + case WellKnownConfigFile.Credentials: path = Path.Combine( GetDirectory(WellKnownDirectory.Root), ".credentials"); break; + case WellKnownConfigFile.MigratedCredentials: + path = Path.Combine( + GetDirectory(WellKnownDirectory.Root), + ".credentials_migrated"); + break; + case WellKnownConfigFile.RSACredentials: path = Path.Combine( GetDirectory(WellKnownDirectory.Root), diff --git a/src/Test/Test.csproj b/src/Test/Test.csproj index bdb1d2957..105400524 100644 --- a/src/Test/Test.csproj +++ b/src/Test/Test.csproj @@ -18,7 +18,6 @@ -