diff --git a/source/Octopus.Manager.Tentacle/TentacleConfiguration/InstanceConfigurationLoader.cs b/source/Octopus.Manager.Tentacle/TentacleConfiguration/InstanceConfigurationLoader.cs index cbe365413..c0a043102 100644 --- a/source/Octopus.Manager.Tentacle/TentacleConfiguration/InstanceConfigurationLoader.cs +++ b/source/Octopus.Manager.Tentacle/TentacleConfiguration/InstanceConfigurationLoader.cs @@ -97,7 +97,6 @@ public SimpleApplicationInstanceSelectorForTentacleManager( { Current = LoadInstance(); canLoadCurrentInstance = true; - } catch { @@ -112,7 +111,7 @@ public SimpleApplicationInstanceSelectorForTentacleManager( ApplicationInstanceConfiguration LoadInstance() { var (aggregatedKeyValueStore, writableConfig) = LoadConfigurationStore(configFilePath); - return new ApplicationInstanceConfiguration(null, configFilePath, aggregatedKeyValueStore, writableConfig); + return new ApplicationInstanceConfiguration(null, configFilePath, aggregatedKeyValueStore, writableConfig, null); } (IKeyValueStore, IWritableKeyValueStore) LoadConfigurationStore(string configurationPath) diff --git a/source/Octopus.Tentacle.Tests.Integration/TrustConfigurationTests.cs b/source/Octopus.Tentacle.Tests.Integration/TrustConfigurationTests.cs new file mode 100644 index 000000000..5fcac0a71 --- /dev/null +++ b/source/Octopus.Tentacle.Tests.Integration/TrustConfigurationTests.cs @@ -0,0 +1,104 @@ +#nullable enable +using System; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using FluentAssertions; +using Halibut; +using NUnit.Framework; +using Octopus.Tentacle.Certificates; +using Octopus.Tentacle.Client; +using Octopus.Tentacle.Client.Retries; +using Octopus.Tentacle.Client.Scripts; +using Octopus.Tentacle.Contracts.Legacy; +using Octopus.Tentacle.Contracts.Observability; +using Octopus.Tentacle.Diagnostics; +using Octopus.Tentacle.Tests.Integration.Support; +using Octopus.Tentacle.Tests.Integration.Support.ExtensionMethods; +using Octopus.Tentacle.Tests.Integration.Util.Builders; + +namespace Octopus.Tentacle.Tests.Integration +{ + [IntegrationTestTimeout] + public class TrustConfigurationTests : IntegrationTest + { + [Test] + [TentacleConfigurations(testPolling: false)] + public async Task ChangingTheTrustedThumbprintsForAListeningTentacleShouldNotRequireARestart(TentacleConfigurationTestCase tentacleConfigurationTestCase) + { + await using (var clientAndTentacle = await tentacleConfigurationTestCase + .CreateBuilder() + .WithRetryDuration(TimeSpan.FromSeconds(20)) + .Build(CancellationToken)) + { + using var newCertificate = new CertificateGenerator(new SystemLog()).GenerateNew($"cn={Guid.NewGuid()}"); + + // Add a new trusted thumbprint + var addTrustCommand = new TestExecuteShellScriptCommandBuilder() + .SetScriptBodyForCurrentOs( +$@"cd ""{clientAndTentacle.RunningTentacle.TentacleExe.DirectoryName}"" +.\Tentacle.exe configure --instance {clientAndTentacle.RunningTentacle.InstanceName} --trust {newCertificate.Thumbprint}", +$@"#!/bin/sh +cd ""{clientAndTentacle.RunningTentacle.TentacleExe.DirectoryName}"" +./Tentacle configure --instance {clientAndTentacle.RunningTentacle.InstanceName} --trust {newCertificate.Thumbprint}") + .Build(); + + + var result = await clientAndTentacle.TentacleClient.ExecuteScript(addTrustCommand, CancellationToken); + result.LogExecuteScriptOutput(Logger); + result.ScriptExecutionResult.ExitCode.Should().Be(0); + result.ProcessOutput.Any(x => x.Text.Contains("Adding 1 trusted Octopus Servers")).Should().BeTrue("Adding 1 trusted Octopus Servers should be logged"); + + // Ensure the new thumbprint is trusted + var checkCommunicationCommand = new TestExecuteShellScriptCommandBuilder() + .SetScriptBody(new ScriptBuilder().Print("Success...")) + .Build(); + + var tentacleClientUsingNewCertificate = BuildTentacleClientForNewCertificate(newCertificate, clientAndTentacle); + + result = await tentacleClientUsingNewCertificate.ExecuteScript(checkCommunicationCommand, CancellationToken); + result.LogExecuteScriptOutput(Logger); + result.ScriptExecutionResult.ExitCode.Should().Be(0); + result.ProcessOutput.Any(x => x.Text.Contains("Success...")).Should().BeTrue("Success... should be logged");; + + // Remove trust for the old thumbprint + var removeTrustCommand = new TestExecuteShellScriptCommandBuilder() + .SetScriptBodyForCurrentOs( + $@"cd ""{clientAndTentacle.RunningTentacle.TentacleExe.DirectoryName}"" +.\Tentacle.exe configure --instance {clientAndTentacle.RunningTentacle.InstanceName} --remove-trust {clientAndTentacle.Server.Thumbprint}", + $@"#!/bin/sh +cd ""{clientAndTentacle.RunningTentacle.TentacleExe.DirectoryName}"" +./Tentacle configure --instance {clientAndTentacle.RunningTentacle.InstanceName} --remove-trust {clientAndTentacle.Server.Thumbprint}") + .Build(); + + result = await tentacleClientUsingNewCertificate.ExecuteScript(removeTrustCommand, CancellationToken); + result.LogExecuteScriptOutput(Logger); + result.ScriptExecutionResult.ExitCode.Should().Be(0); + result.ProcessOutput.Any(x => x.Text.Contains("Removing 1 trusted Octopus Servers")).Should().BeTrue("Removing 1 trusted Octopus Servers should be logged"); + + // Ensure the old thumbprint is no longer trusted + await AssertionExtensions + .Should(async () => await clientAndTentacle.TentacleClient.ExecuteScript(checkCommunicationCommand, CancellationToken)) + .ThrowAsync(); + } + } + + static TentacleClient BuildTentacleClientForNewCertificate(X509Certificate2 newCertificate, ClientAndTentacle clientAndTentacle) + { + var halibutRuntime = new HalibutRuntimeBuilder() + .WithServerCertificate(newCertificate) + .WithHalibutTimeoutsAndLimits(clientAndTentacle.Server.ServerHalibutRuntime.TimeoutsAndLimits) + .WithLegacyContractSupport() + .Build(); + + TentacleClient.CacheServiceWasNotFoundResponseMessages(halibutRuntime); + + var retrySettings = new RpcRetrySettings(true, TimeSpan.FromSeconds(5)); + var clientOptions = new TentacleClientOptions(retrySettings); + + var tentacleClient = new TentacleClient(clientAndTentacle.ServiceEndPoint, halibutRuntime, new DefaultScriptObserverBackoffStrategy(), new NoTentacleClientObserver(), clientOptions); + + return tentacleClient; + } + } +} diff --git a/source/Octopus.Tentacle.Tests/Commands/ConfigureCommandFixture.cs b/source/Octopus.Tentacle.Tests/Commands/ConfigureCommandFixture.cs index e9cc4d23e..c66ef2511 100644 --- a/source/Octopus.Tentacle.Tests/Commands/ConfigureCommandFixture.cs +++ b/source/Octopus.Tentacle.Tests/Commands/ConfigureCommandFixture.cs @@ -29,7 +29,7 @@ public override void SetUp() fileSystem = Substitute.For(); log = Substitute.For(); var selector = Substitute.For(); - selector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!)); + selector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!, null)); Command = new ConfigureCommand(new Lazy(() => tentacleConfiguration), new Lazy(() => new StubHomeConfiguration()), fileSystem, log, selector, Substitute.For()); } diff --git a/source/Octopus.Tentacle.Tests/Commands/DeregisterMachineCommandFixture.cs b/source/Octopus.Tentacle.Tests/Commands/DeregisterMachineCommandFixture.cs index 35f4ec264..f68959f16 100644 --- a/source/Octopus.Tentacle.Tests/Commands/DeregisterMachineCommandFixture.cs +++ b/source/Octopus.Tentacle.Tests/Commands/DeregisterMachineCommandFixture.cs @@ -34,7 +34,7 @@ public override void SetUp() proxyConfig = Substitute.For(); log = Substitute.For(); selector = Substitute.For(); - selector.Current.Returns(new ApplicationInstanceConfiguration("my-instance", "myconfig.config", Substitute.For(), Substitute.For())); + selector.Current.Returns(new ApplicationInstanceConfiguration("my-instance", "myconfig.config", Substitute.For(), Substitute.For(), null)); } [Test] diff --git a/source/Octopus.Tentacle.Tests/Commands/DeregisterWorkerCommandFixture.cs b/source/Octopus.Tentacle.Tests/Commands/DeregisterWorkerCommandFixture.cs index e6cda8d0b..9f8caeb7b 100644 --- a/source/Octopus.Tentacle.Tests/Commands/DeregisterWorkerCommandFixture.cs +++ b/source/Octopus.Tentacle.Tests/Commands/DeregisterWorkerCommandFixture.cs @@ -34,7 +34,7 @@ public override void SetUp() proxyConfig = Substitute.For(); log = Substitute.For(); selector = Substitute.For(); - selector.Current.Returns(new ApplicationInstanceConfiguration("my-instance", "myconfig.config", Substitute.For(), Substitute.For())); + selector.Current.Returns(new ApplicationInstanceConfiguration("my-instance", "myconfig.config", Substitute.For(), Substitute.For(), null)); } [Test] diff --git a/source/Octopus.Tentacle.Tests/Commands/ImportCertificateCommandFixture.cs b/source/Octopus.Tentacle.Tests/Commands/ImportCertificateCommandFixture.cs index f511a77e9..590783338 100644 --- a/source/Octopus.Tentacle.Tests/Commands/ImportCertificateCommandFixture.cs +++ b/source/Octopus.Tentacle.Tests/Commands/ImportCertificateCommandFixture.cs @@ -23,7 +23,7 @@ public void BeforeEachTest() { configuration = Substitute.For(); var selector = Substitute.For(); - selector.Current.Returns(_ => new ApplicationInstanceConfiguration(null, null!, null!, null!)); + selector.Current.Returns(_ => new ApplicationInstanceConfiguration(null, null!, null!, null!, null)); Command = new ImportCertificateCommand( new Lazy(() => configuration), Substitute.For(), diff --git a/source/Octopus.Tentacle.Tests/Commands/NewCertificateCommandFixture.cs b/source/Octopus.Tentacle.Tests/Commands/NewCertificateCommandFixture.cs index b2cd20a61..4ce7e9f8a 100644 --- a/source/Octopus.Tentacle.Tests/Commands/NewCertificateCommandFixture.cs +++ b/source/Octopus.Tentacle.Tests/Commands/NewCertificateCommandFixture.cs @@ -24,7 +24,7 @@ public override void SetUp() log = Substitute.For(); configuration = new StubTentacleConfiguration(); var selector = Substitute.For(); - selector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!)); + selector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!, null)); Command = new NewCertificateCommand(new Lazy(() => configuration), log, selector, new Lazy(() => Substitute.For()), Substitute.For()); } diff --git a/source/Octopus.Tentacle.Tests/Commands/ProxyConfigurationCommandFixture.cs b/source/Octopus.Tentacle.Tests/Commands/ProxyConfigurationCommandFixture.cs index de59febf5..bda40b2fb 100644 --- a/source/Octopus.Tentacle.Tests/Commands/ProxyConfigurationCommandFixture.cs +++ b/source/Octopus.Tentacle.Tests/Commands/ProxyConfigurationCommandFixture.cs @@ -27,7 +27,7 @@ public void SetupForEachTest() configFile = $"{homeDirectory}\\File.config"; applicationInstanceSelector = Substitute.For(); - applicationInstanceSelector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!)); + applicationInstanceSelector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!, null)); octopusFileSystem.AppendToFile(configFile, "\n" + diff --git a/source/Octopus.Tentacle.Tests/Commands/RegisterMachineCommandFixture.cs b/source/Octopus.Tentacle.Tests/Commands/RegisterMachineCommandFixture.cs index 283f33a22..e06bb018b 100644 --- a/source/Octopus.Tentacle.Tests/Commands/RegisterMachineCommandFixture.cs +++ b/source/Octopus.Tentacle.Tests/Commands/RegisterMachineCommandFixture.cs @@ -59,7 +59,7 @@ public void BeforeEachTest() octopusClientInitializer.CreateClient(Arg.Any(), false) .Returns(Task.FromResult(octopusAsyncClient)); var selector = Substitute.For(); - selector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!)); + selector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!, null)); Command = new RegisterMachineCommand(new Lazy(() => operation), new Lazy(() => configuration), log, diff --git a/source/Octopus.Tentacle.Tests/Commands/RegisterWorkerCommandFixture.cs b/source/Octopus.Tentacle.Tests/Commands/RegisterWorkerCommandFixture.cs index a3e350ece..df2fec2b4 100644 --- a/source/Octopus.Tentacle.Tests/Commands/RegisterWorkerCommandFixture.cs +++ b/source/Octopus.Tentacle.Tests/Commands/RegisterWorkerCommandFixture.cs @@ -60,7 +60,7 @@ public void BeforeEachTest() .Returns(Task.FromResult(octopusAsyncClient)); var applicationInstanceSelector = Substitute.For(); - applicationInstanceSelector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!)); + applicationInstanceSelector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!, null)); Command = new RegisterWorkerCommand(new Lazy(() => operation), new Lazy(() => configuration), diff --git a/source/Octopus.Tentacle.Tests/Commands/RunAgentCommandFixture.cs b/source/Octopus.Tentacle.Tests/Commands/RunAgentCommandFixture.cs index 5f0f36859..b9c50af0f 100644 --- a/source/Octopus.Tentacle.Tests/Commands/RunAgentCommandFixture.cs +++ b/source/Octopus.Tentacle.Tests/Commands/RunAgentCommandFixture.cs @@ -62,7 +62,7 @@ public override void SetUp() Substitute.For(), backgroundTasks.Select(bt => new Lazy(() => bt)).ToList()); - selector.Current.Returns(new ApplicationInstanceConfiguration("MyTentacle", null, null, null)); + selector.Current.Returns(new ApplicationInstanceConfiguration("MyTentacle", null, null, null, null)); } [Test] diff --git a/source/Octopus.Tentacle.Tests/Commands/ServerCommsCommandTest.cs b/source/Octopus.Tentacle.Tests/Commands/ServerCommsCommandTest.cs index 6994c9c5f..8d26260e9 100644 --- a/source/Octopus.Tentacle.Tests/Commands/ServerCommsCommandTest.cs +++ b/source/Octopus.Tentacle.Tests/Commands/ServerCommsCommandTest.cs @@ -27,7 +27,7 @@ public void SetUp() { configuration = new StubTentacleConfiguration(); var selector = Substitute.For(); - selector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!)); + selector.Current.Returns(info => new ApplicationInstanceConfiguration(null, null!, null!, null!, null)); command = new ServerCommsCommand( new Lazy(() => configuration), new InMemoryLog(), diff --git a/source/Octopus.Tentacle.Tests/Commands/StubTentacleConfiguration.cs b/source/Octopus.Tentacle.Tests/Commands/StubTentacleConfiguration.cs index 156c547fd..a7162617a 100644 --- a/source/Octopus.Tentacle.Tests/Commands/StubTentacleConfiguration.cs +++ b/source/Octopus.Tentacle.Tests/Commands/StubTentacleConfiguration.cs @@ -39,10 +39,13 @@ public IEnumerable TrustedOctopusThumbprints public IProxyConfiguration ProxyConfiguration { get; set; } = null!; public IPollingProxyConfiguration PollingProxyConfiguration { get; set; } = null!; public bool IsRegistered { get; set; } = false; + public ConfigurationChangedEventHandler? Changed { get; set; } + public void WriteTo(IWritableKeyValueStore outputStore, IEnumerable excluding) { throw new NotImplementedException(); } + public bool SetApplicationDirectory(string directory) { diff --git a/source/Octopus.Tentacle/Commands/ConfigureCommand.cs b/source/Octopus.Tentacle/Commands/ConfigureCommand.cs index a0490ddc4..50770d747 100644 --- a/source/Octopus.Tentacle/Commands/ConfigureCommand.cs +++ b/source/Octopus.Tentacle/Commands/ConfigureCommand.cs @@ -92,7 +92,6 @@ protected override void Start() { log.Info("Removing all trusted Octopus Servers..."); tentacleConfiguration.Value.ResetTrustedOctopusServers(); - VoteForRestart(); } if (octopusToRemove.Count > 0) @@ -101,7 +100,6 @@ protected override void Start() foreach (var toRemove in octopusToRemove) { tentacleConfiguration.Value.RemoveTrustedOctopusServersWithThumbprint(toRemove); - VoteForRestart(); } } @@ -113,7 +111,6 @@ protected override void Start() { var config = new OctopusServerConfiguration(toAdd) { CommunicationStyle = CommunicationStyle.TentaclePassive }; tentacleConfiguration.Value.AddOrUpdateTrustedOctopusServer(config); - VoteForRestart(); } } diff --git a/source/Octopus.Tentacle/Communications/HalibutInitializer.cs b/source/Octopus.Tentacle/Communications/HalibutInitializer.cs index 8daec4ccc..1b62b375b 100644 --- a/source/Octopus.Tentacle/Communications/HalibutInitializer.cs +++ b/source/Octopus.Tentacle/Communications/HalibutInitializer.cs @@ -40,14 +40,15 @@ public void Start() if (configuration.NoListen) { log.Info("Agent will not listen on any TCP ports"); - return; + } + else + { + var endpoint = GetEndPointToListenOn(); + halibut.Listen(endpoint); + log.Info("Agent listening on: " + endpoint); } - var endpoint = GetEndPointToListenOn(); - - halibut.Listen(endpoint); - - log.Info("Agent listening on: " + endpoint); + configuration.Changed += TentacleConfigurationChanged; } private void FixCommunicationStyle() @@ -82,9 +83,7 @@ void TrustOctopusServers() log.Info("The agent is not configured to trust any Octopus Servers."); } } - - - + void AddPollingEndpoints() { foreach (var pollingEndPoint in GetOctopusServersToPoll()) @@ -170,6 +169,21 @@ IPEndPoint GetEndPointToListenOn() return new IPEndPoint(address, configuration.ServicesPortNumber); } + void TentacleConfigurationChanged() + { + var thumbprints = GetTrustedOctopusThumbprints(); + + foreach (var thumbprint in thumbprints) + { + if (!halibut.IsTrusted(thumbprint)) + { + log.Info($"Agent will trust Octopus Servers with the thumbprint: {thumbprint}"); + } + } + + halibut.TrustOnly(thumbprints); + } + public void Stop() { } diff --git a/source/Octopus.Tentacle/Configuration/ConfigurationChangedEventHandler.cs b/source/Octopus.Tentacle/Configuration/ConfigurationChangedEventHandler.cs new file mode 100644 index 000000000..a09529cc7 --- /dev/null +++ b/source/Octopus.Tentacle/Configuration/ConfigurationChangedEventHandler.cs @@ -0,0 +1,6 @@ +using System; + +namespace Octopus.Tentacle.Configuration +{ + public delegate void ConfigurationChangedEventHandler(); +} \ No newline at end of file diff --git a/source/Octopus.Tentacle/Configuration/FlatDictionaryKeyValueStore.cs b/source/Octopus.Tentacle/Configuration/FlatDictionaryKeyValueStore.cs index a48df445c..7ceb2d104 100644 --- a/source/Octopus.Tentacle/Configuration/FlatDictionaryKeyValueStore.cs +++ b/source/Octopus.Tentacle/Configuration/FlatDictionaryKeyValueStore.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Newtonsoft.Json; using Octopus.Tentacle.Configuration.Crypto; using Octopus.Tentacle.Configuration.Instances; @@ -40,7 +41,7 @@ protected FlatDictionaryKeyValueStore(JsonSerializerSettings jsonSerializerSetti return JsonConvert.DeserializeObject((string)data, JsonSerializerSettings); } - catch (Exception e) + catch (Exception e) when (e is not IOException) { if (protectionLevel == ProtectionLevel.None) throw new FormatException($"Unable to parse configuration key '{name}' as a '{typeof(TData).Name}'. Value was '{valueAsString}'.", e); @@ -74,7 +75,7 @@ protected FlatDictionaryKeyValueStore(JsonSerializerSettings jsonSerializerSetti return (true, JsonConvert.DeserializeObject((string)data, JsonSerializerSettings)); } - catch (Exception e) + catch (Exception e) when (e is not IOException) { if (protectionLevel == ProtectionLevel.None) throw new FormatException($"Unable to parse configuration key '{name}' as a '{typeof(TData).Name}'. Value was '{valueAsString}'.", e); diff --git a/source/Octopus.Tentacle/Configuration/ITentacleConfiguration.cs b/source/Octopus.Tentacle/Configuration/ITentacleConfiguration.cs index d55b6ebd6..9584f2fe7 100644 --- a/source/Octopus.Tentacle/Configuration/ITentacleConfiguration.cs +++ b/source/Octopus.Tentacle/Configuration/ITentacleConfiguration.cs @@ -86,6 +86,8 @@ public interface ITentacleConfiguration bool IsRegistered { get; } void WriteTo(IWritableKeyValueStore outputStore, IEnumerable excluding); + + ConfigurationChangedEventHandler? Changed { get; set; } } public interface IWritableTentacleConfiguration : ITentacleConfiguration diff --git a/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceConfiguration.cs b/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceConfiguration.cs index fdbfef4d1..5011e35e4 100644 --- a/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceConfiguration.cs +++ b/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceConfiguration.cs @@ -2,17 +2,24 @@ namespace Octopus.Tentacle.Configuration.Instances { - public class ApplicationInstanceConfiguration { + public class ApplicationInstanceConfiguration : IDisposable + { public ApplicationInstanceConfiguration( string? instanceName, string? configurationPath, IKeyValueStore? configuration, - IWritableKeyValueStore? writableConfiguration) + IWritableKeyValueStore? writableConfiguration, + Func? loadConfigurationFunction) { InstanceName = instanceName; ConfigurationPath = configurationPath; Configuration = configuration; WritableConfiguration = writableConfiguration; + + if (configuration != null) + { + ChangeDetectingConfiguration = new ChangeDetectingKeyValueStore(configuration, configurationPath, loadConfigurationFunction); + } } public string? ConfigurationPath { get; } @@ -20,6 +27,13 @@ public ApplicationInstanceConfiguration( public IKeyValueStore? Configuration { get; } + public ChangeDetectingKeyValueStore? ChangeDetectingConfiguration { get; } + public IWritableKeyValueStore? WritableConfiguration { get; } + + public void Dispose() + { + ChangeDetectingConfiguration?.Dispose(); + } } } \ No newline at end of file diff --git a/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceSelector.cs b/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceSelector.cs index 1f12ac1c4..721c316b3 100644 --- a/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceSelector.cs +++ b/source/Octopus.Tentacle/Configuration/Instances/ApplicationInstanceSelector.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Octopus.Diagnostics; using Octopus.Tentacle.Kubernetes; @@ -7,7 +6,7 @@ namespace Octopus.Tentacle.Configuration.Instances { - class ApplicationInstanceSelector : IApplicationInstanceSelector + class ApplicationInstanceSelector : IApplicationInstanceSelector, IDisposable { readonly IApplicationInstanceStore applicationInstanceStore; readonly StartUpInstanceRequest startUpInstanceRequest; @@ -64,18 +63,23 @@ ApplicationInstanceConfiguration LoadCurrentInstance() } return current; - } - ApplicationInstanceConfiguration LoadInstance() - { - var appInstance = LocateApplicationPrimaryConfiguration(); + ApplicationInstanceConfiguration LoadInstance() + { + var appInstance = LocateApplicationPrimaryConfiguration(); - var (aggregatedKeyValueStore, writableConfig) = LoadConfigurationStore(appInstance); + var (aggregatedKeyValueStore, writableConfig) = LoadConfigurationStore(appInstance); - return new ApplicationInstanceConfiguration(appInstance.instanceName, appInstance.configurationpath, aggregatedKeyValueStore, writableConfig); + return new ApplicationInstanceConfiguration( + appInstance.instanceName, + appInstance.configurationpath, + aggregatedKeyValueStore, + writableConfig, + () => LoadConfigurationStore(new(appInstance.instanceName, appInstance.configurationpath)).KeyValueStore); + } } - (IKeyValueStore, IWritableKeyValueStore) LoadConfigurationStore((string? instanceName, string? configurationpath) appInstance) + (IKeyValueStore KeyValueStore, IWritableKeyValueStore WritableKeyValueStore) LoadConfigurationStore((string? instanceName, string? configurationpath) appInstance) { if (appInstance is { instanceName: not null, configurationpath: null } && PlatformDetection.Kubernetes.IsRunningAsKubernetesAgent) @@ -151,5 +155,10 @@ AggregatedKeyValueStore ContributeAdditionalConfiguration(IAggregatableKeyValueS } } } + + public void Dispose() + { + current?.Dispose(); + } } } \ No newline at end of file diff --git a/source/Octopus.Tentacle/Configuration/Instances/ChangeDetectingKeyValueStore.cs b/source/Octopus.Tentacle/Configuration/Instances/ChangeDetectingKeyValueStore.cs new file mode 100644 index 000000000..345e7c59c --- /dev/null +++ b/source/Octopus.Tentacle/Configuration/Instances/ChangeDetectingKeyValueStore.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; + +namespace Octopus.Tentacle.Configuration.Instances +{ + public class ChangeDetectingKeyValueStore : IKeyValueStore, IDisposable + { + IKeyValueStore configuration; + readonly Func? loadConfigurationFunction; + readonly FileSystemWatcher? configurationFileWatcher; + + public ChangeDetectingKeyValueStore(IKeyValueStore initialConfiguration, string? configurationPath, Func? loadConfigurationFunction) + { + this.configuration = initialConfiguration; + this.loadConfigurationFunction = loadConfigurationFunction; + + if (loadConfigurationFunction != null && configurationPath != null) + { + var configurationFileInfo = new FileInfo(configurationPath); + + if (configurationFileInfo.DirectoryName != null) + { + configurationFileWatcher = new FileSystemWatcher(configurationFileInfo.DirectoryName, configurationFileInfo.Name); + configurationFileWatcher.NotifyFilter = NotifyFilters.LastWrite; + configurationFileWatcher.EnableRaisingEvents = true; + configurationFileWatcher.Changed += ConfigurationChanged; + } + } + } + + public ConfigurationChangedEventHandler? Changed { get; set; } + + public string? Get(string name, ProtectionLevel protectionLevel = ProtectionLevel.None) + { + return configuration.Get(name, protectionLevel); + } + + public TData? Get(string name, TData? defaultValue = default, ProtectionLevel protectionLevel = ProtectionLevel.None) + { + return configuration.Get(name, defaultValue, protectionLevel); + } + + void ConfigurationChanged(object sender, FileSystemEventArgs args) + { + lock (configuration) + { + try + { + var updatedConfiguration = loadConfigurationFunction!(); + + if (updatedConfiguration != null) + { + configuration = updatedConfiguration; + + Changed?.Invoke(); + } + } + catch (IOException) + { + // Tentacle writes configuration changes non atomically so may still be writing to the file when the change notification is raised + } + catch (FormatException) + { + // If Tentacle is part way through writing the configuration and the OS allows reads to the configuration file then it may be invalid + } + } + } + + public void Dispose() + { + configurationFileWatcher?.Dispose(); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Tentacle/Configuration/TentacleConfiguration.cs b/source/Octopus.Tentacle/Configuration/TentacleConfiguration.cs index 7d60e5aa1..2beafc4e2 100644 --- a/source/Octopus.Tentacle/Configuration/TentacleConfiguration.cs +++ b/source/Octopus.Tentacle/Configuration/TentacleConfiguration.cs @@ -16,7 +16,7 @@ namespace Octopus.Tentacle.Configuration { - internal class TentacleConfiguration : ITentacleConfiguration + internal class TentacleConfiguration : ITentacleConfiguration, IDisposable { internal const string IsRegisteredSettingName = "Tentacle.Services.IsRegistered"; internal const string ServicesPortSettingName = "Tentacle.Services.PortNumber"; @@ -29,6 +29,7 @@ internal class TentacleConfiguration : ITentacleConfiguration internal const string LastReceivedHandshakeSettingName = "Tentacle.Communication.LastReceivedHandshake"; readonly IKeyValueStore settings; + readonly ChangeDetectingKeyValueStore changeDetectingSettings; readonly IHomeConfiguration home; readonly IProxyConfiguration proxyConfiguration; readonly IPollingProxyConfiguration pollingProxyConfiguration; @@ -46,6 +47,9 @@ public TentacleConfiguration( ISystemLog log) { settings = instanceSelector.Current.Configuration ?? throw new Exception("Unable to get KeyValueStore from instanceSelector"); + changeDetectingSettings = instanceSelector.Current.ChangeDetectingConfiguration!; + changeDetectingSettings.Changed += () => Changed?.Invoke(); + this.home = home; this.proxyConfiguration = proxyConfiguration; this.pollingProxyConfiguration = pollingProxyConfiguration; @@ -55,7 +59,7 @@ public TentacleConfiguration( [Obsolete("This configuration entry is obsolete as of 3.0. It is only used as a Subscription ID where one does not exist.")] public string? TentacleSquid => settings.Get("Octopus.Communications.Squid", (string?)null); - public IEnumerable TrustedOctopusServers => settings.Get>(TrustedServersSettingName) ?? new OctopusServerConfiguration[0]; + public IEnumerable TrustedOctopusServers => changeDetectingSettings.Get>(TrustedServersSettingName) ?? Array.Empty(); public IEnumerable TrustedOctopusThumbprints { @@ -158,6 +162,13 @@ public OctopusServerConfiguration? LastReceivedHandshake return JsonConvert.DeserializeObject(setting); } } + + public ConfigurationChangedEventHandler? Changed { get; set; } + + public void Dispose() + { + changeDetectingSettings?.Dispose(); + } } class WritableTentacleConfiguration : TentacleConfiguration, IWritableTentacleConfiguration