From de8c2e70e0f55ed80ac576b3bb7f6ca56805069a Mon Sep 17 00:00:00 2001 From: Kevin Tchang Date: Mon, 30 Sep 2024 15:42:05 +1000 Subject: [PATCH 1/5] update filename --- ...sPodContainerRegistry.cs => KubernetesPodContainerResolver.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename source/Octopus.Tentacle/Kubernetes/{KubernetesPodContainerRegistry.cs => KubernetesPodContainerResolver.cs} (100%) diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesPodContainerRegistry.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesPodContainerResolver.cs similarity index 100% rename from source/Octopus.Tentacle/Kubernetes/KubernetesPodContainerRegistry.cs rename to source/Octopus.Tentacle/Kubernetes/KubernetesPodContainerResolver.cs From a8f2cb046405bb72fddae3880b51ef843b40e6a1 Mon Sep 17 00:00:00 2001 From: Kevin Tchang Date: Wed, 2 Oct 2024 11:29:15 +1000 Subject: [PATCH 2/5] try get tools image metadata to use the revision hash in the image tag --- .../Kubernetes/ClusterVersionTests.cs | 87 ++++++++++++ .../KubernetesPodContainerResolverTests.cs | 126 ++++++++++++++++++ .../KubernetesVersionParserTests.cs | 43 ------ .../Kubernetes/ClusterVersion.cs | 69 ++++++++++ ...ubernetesAgentToolsImageVersionMetadata.cs | 21 +++ ...sAgentToolsImageVersionMetadataProvider.cs | 40 ++++++ .../Kubernetes/KubernetesClusterService.cs | 2 +- .../Kubernetes/KubernetesModule.cs | 1 + .../KubernetesPodContainerResolver.cs | 57 +++++++- .../Kubernetes/KubernetesVersionParser.cs | 21 --- 10 files changed, 397 insertions(+), 70 deletions(-) create mode 100644 source/Octopus.Tentacle.Tests/Kubernetes/ClusterVersionTests.cs create mode 100644 source/Octopus.Tentacle.Tests/Kubernetes/KubernetesPodContainerResolverTests.cs delete mode 100644 source/Octopus.Tentacle.Tests/Kubernetes/KubernetesVersionParserTests.cs create mode 100644 source/Octopus.Tentacle/Kubernetes/ClusterVersion.cs create mode 100644 source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadata.cs create mode 100644 source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs delete mode 100644 source/Octopus.Tentacle/Kubernetes/KubernetesVersionParser.cs diff --git a/source/Octopus.Tentacle.Tests/Kubernetes/ClusterVersionTests.cs b/source/Octopus.Tentacle.Tests/Kubernetes/ClusterVersionTests.cs new file mode 100644 index 000000000..b3eb9edec --- /dev/null +++ b/source/Octopus.Tentacle.Tests/Kubernetes/ClusterVersionTests.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using k8s.Models; +using NUnit.Framework; +using Octopus.Tentacle.Kubernetes; + +namespace Octopus.Tentacle.Tests.Kubernetes +{ + [TestFixture] + public class ClusterVersionTests + { + [TestCase("0", "1", 0, 1, false)] + [TestCase("1abc", "1", 1, 1, false)] + [TestCase("0", "1abc", 0, 1, false)] + [TestCase("1+", "0", 1, 0, false)] + [TestCase("0", "1+", 0, 1, false)] + [TestCase("abc", "1", 999, 999, true)] + public void FromVersionInfo_SanitizesAndReturnsNewClusterVersion(string major, string minor, int expectedMajor, int expectedMinor, bool shouldFail) + { + try + { + var versionInfo = new VersionInfo + { + Major = major, + Minor = minor + }; + var result = ClusterVersion.FromVersionInfo(versionInfo); + result.Should().BeEquivalentTo(new ClusterVersion(expectedMajor, expectedMinor)); + } + catch (Exception e) + { + if (shouldFail) + e.Should().BeOfType(); + else + throw; + } + } + + static IEnumerable FromVersionTestData() + { + yield return new TestCaseData(new Version("0.0.1"), 0, 0); + yield return new TestCaseData(new Version("1.31"), 1, 31); + yield return new TestCaseData(new Version("2.24.4"), 2, 24); + } + + [TestCaseSource(nameof(FromVersionTestData))] + public void FromVersion_ReturnsNewClusterVersion(Version version, int expectedMajor, int expectedMinor) + { + var result = ClusterVersion.FromVersion(version); + result.Should().BeEquivalentTo(new ClusterVersion(expectedMajor, expectedMinor)); + } + + static IEnumerable CompareClusterVersionsTestData() + { + yield return new TestCaseData(new ClusterVersion(0, 0), null, 1); + yield return new TestCaseData(new ClusterVersion(0, 0), new ClusterVersion(0, 0), 0); + yield return new TestCaseData(new ClusterVersion(0, 5), new ClusterVersion(0, 6), -1); + yield return new TestCaseData(new ClusterVersion(1, 1), new ClusterVersion(2, 0), -1); + yield return new TestCaseData(new ClusterVersion(1, 30), new ClusterVersion(1, 29), 1); + yield return new TestCaseData(new ClusterVersion(3, 0), new ClusterVersion(2, 11), 1); + yield return new TestCaseData(new ClusterVersion(3, 14), new ClusterVersion(3, 14), 0); + } + + [TestCaseSource(nameof(CompareClusterVersionsTestData))] + public void CompareClusterVersions(ClusterVersion thisClusterVersion, ClusterVersion otherClusterVersion, int expected) + { + var result = thisClusterVersion.CompareTo(otherClusterVersion); + result.Should().Be(expected); + } + + static IEnumerable CheckEqualityClusterVersionsTestData() + { + yield return new TestCaseData(new ClusterVersion(0, 0), null, false); + yield return new TestCaseData(new ClusterVersion(0, 5), new ClusterVersion(0, 6), false); + yield return new TestCaseData(new ClusterVersion(1, 0), new ClusterVersion(2, 0), false); + yield return new TestCaseData(new ClusterVersion(3, 14), new ClusterVersion(3, 14), true); + } + + [TestCaseSource(nameof(CheckEqualityClusterVersionsTestData))] + public void CheckEqualityClusterVersions(ClusterVersion thisClusterVersion, ClusterVersion otherClusterVersion, bool expected) + { + var result = thisClusterVersion.Equals(otherClusterVersion); + result.Should().Be(expected); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Tentacle.Tests/Kubernetes/KubernetesPodContainerResolverTests.cs b/source/Octopus.Tentacle.Tests/Kubernetes/KubernetesPodContainerResolverTests.cs new file mode 100644 index 000000000..216938fdc --- /dev/null +++ b/source/Octopus.Tentacle.Tests/Kubernetes/KubernetesPodContainerResolverTests.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using NUnit.Framework; +using Octopus.Tentacle.Kubernetes; + +namespace Octopus.Tentacle.Tests.Kubernetes +{ + [TestFixture] + public class KubernetesPodContainerResolverTests + { + readonly KubernetesAgentToolsImageVersionMetadata testVersionMetadata = new(new KubernetesAgentToolVersions(new List + { + new("1.31.1"), + new("1.30.5"), + new("1.29.9"), + new("1.28.14") + }, new List { new("3.16.1") }, + new List { new("7.4.5") }), new Version("1.30"), "Juaa5J", new Dictionary + { + { new Version("1.26"), new KubernetesAgentToolDeprecation("1.26@sha256:a0892db") }, + { new Version("1.27"), new KubernetesAgentToolDeprecation("1.27@sha256:9d1ce87") } + }); + + readonly IToolsImageVersionMetadataProvider mockToolsImageVersionMetadataProvider = Substitute.For(); + + [SetUp] + public void Init() + { + mockToolsImageVersionMetadataProvider.TryGetVersionMetadata().Returns(testVersionMetadata); + } + + [TestCase(30)] + [TestCase(29)] + [TestCase(28)] + public async Task GetContainerImageForCluster_VersionMetadataExists_ClusterVersionSupported_GetsImageWithRevision(int clusterMinorVersion) + { + // Arrange + var clusterService = Substitute.For(); + clusterService.GetClusterVersion().Returns(new ClusterVersion(1, clusterMinorVersion)); + + var podContainerResolver = new KubernetesPodContainerResolver(clusterService, mockToolsImageVersionMetadataProvider); + + // Act + var result = await podContainerResolver.GetContainerImageForCluster(); + + // Assert + result.Should().Be($"octopusdeploy/kubernetes-agent-tools-base:1.{clusterMinorVersion}-Juaa5J"); + } + + [TestCase(27, "1.27@sha256:9d1ce87")] + [TestCase(26, "1.26@sha256:a0892db")] + public async Task GetContainerImageForCluster_VersionMetadataExists_ClusterVersionDeprecated_GetsLatestDeprecatedTag(int clusterMinorVersion, string expectedImageTag) + { + // Arrange + var clusterService = Substitute.For(); + clusterService.GetClusterVersion().Returns(new ClusterVersion(1, clusterMinorVersion)); + + var podContainerResolver = new KubernetesPodContainerResolver(clusterService, mockToolsImageVersionMetadataProvider); + + // Act + var result = await podContainerResolver.GetContainerImageForCluster(); + + // Assert + result.Should().Be($"octopusdeploy/kubernetes-agent-tools-base:{expectedImageTag}"); + } + + [Test] + public async Task GetContainerImageForCluster_VersionMetadataExists_ClusterVersionGreaterThanLatest_FallbackToLatest() + { + // Arrange + var clusterService = Substitute.For(); + clusterService.GetClusterVersion().Returns(new ClusterVersion(1, 31)); + + var podContainerResolver = new KubernetesPodContainerResolver(clusterService, mockToolsImageVersionMetadataProvider); + + // Act + var result = await podContainerResolver.GetContainerImageForCluster(); + + // Assert + result.Should().Be("octopusdeploy/kubernetes-agent-tools-base:latest"); + } + + [Test] + public async Task GetContainerImageForCluster_VersionMetadataExists_ClusterVersionNotFound_FallbackToLatest() + { + // Arrange + var clusterService = Substitute.For(); + clusterService.GetClusterVersion().Returns(new ClusterVersion(1, 40)); + + var podContainerResolver = new KubernetesPodContainerResolver(clusterService, mockToolsImageVersionMetadataProvider); + + // Act + var result = await podContainerResolver.GetContainerImageForCluster(); + + // Assert + result.Should().Be("octopusdeploy/kubernetes-agent-tools-base:latest"); + } + + [TestCase(31, "latest")] + [TestCase(30, "1.30")] + [TestCase(29, "1.29")] + [TestCase(28, "1.28")] + [TestCase(27, "1.27")] + [TestCase(26, "1.26")] + [TestCase(25, "latest")] + public async Task GetContainerImageForCluster_VersionMetadataNotFound_FallBackToKnownTags(int clusterMinorVersion, string expectedImageTag) + { + // Arrange + var clusterService = Substitute.For(); + clusterService.GetClusterVersion().Returns(new ClusterVersion(1, clusterMinorVersion)); + mockToolsImageVersionMetadataProvider.TryGetVersionMetadata().ReturnsNull(); + + var podContainerResolver = new KubernetesPodContainerResolver(clusterService, mockToolsImageVersionMetadataProvider); + + // Act + var result = await podContainerResolver.GetContainerImageForCluster(); + + // Assert + result.Should().Be($"octopusdeploy/kubernetes-agent-tools-base:{expectedImageTag}"); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Tentacle.Tests/Kubernetes/KubernetesVersionParserTests.cs b/source/Octopus.Tentacle.Tests/Kubernetes/KubernetesVersionParserTests.cs deleted file mode 100644 index 99143f6cf..000000000 --- a/source/Octopus.Tentacle.Tests/Kubernetes/KubernetesVersionParserTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using FluentAssertions; -using k8s.Models; -using NUnit.Framework; -using Octopus.Tentacle.Kubernetes; - -namespace Octopus.Tentacle.Tests.Kubernetes -{ - [TestFixture] - public class KubernetesVersionParserTests - { - [TestCase("0", "1", 0, 1, false)] - [TestCase("1abc", "1", 1, 1, false)] - [TestCase("0", "1abc", 0, 1, false)] - [TestCase("1+", "0", 1, 0, false)] - [TestCase("0", "1+", 0, 1, false)] - [TestCase("abc", "1", 999, 999, true)] - public void ParseClusterVersion_SanitizesAndReturnsClusterVersion(string major, string minor, int expectedMajor, int expectedMinor, bool shouldFail) - { - try - { - var versionInfo = new VersionInfo - { - Major = major, - Minor = minor - }; - var result = KubernetesVersionParser.ParseClusterVersion(versionInfo); - result.Should().BeEquivalentTo(new ClusterVersion(expectedMajor, expectedMinor)); - } - catch (Exception e) - { - if (shouldFail) - { - e.Should().BeOfType(); - } - else - { - throw; - } - } - } - } -} \ No newline at end of file diff --git a/source/Octopus.Tentacle/Kubernetes/ClusterVersion.cs b/source/Octopus.Tentacle/Kubernetes/ClusterVersion.cs new file mode 100644 index 000000000..2b6b5bbb9 --- /dev/null +++ b/source/Octopus.Tentacle/Kubernetes/ClusterVersion.cs @@ -0,0 +1,69 @@ +using System; +using System.Text.RegularExpressions; +using k8s.Models; + +namespace Octopus.Tentacle.Kubernetes +{ + public class ClusterVersion : IComparable + { + public ClusterVersion(int major, int minor) + { + Major = major; + Minor = minor; + } + + public int Major { get; } + public int Minor { get; } + + public static ClusterVersion FromVersion(Version version) + { + return new ClusterVersion(version.Major, version.Minor); + } + + public static ClusterVersion FromVersionInfo(VersionInfo versionInfo) + { + return new ClusterVersion(SanitizeAndParseVersionNumber(versionInfo.Major), SanitizeAndParseVersionNumber(versionInfo.Minor)); + } + + static int SanitizeAndParseVersionNumber(string version) + { + return int.Parse(Regex.Replace(version, "[^0-9]", "")); + } + + public int CompareTo(ClusterVersion? other) + { + if (other == null) return 1; + if (Major > other.Major || (Major == other.Major && Minor > other.Minor)) return 1; + if (Major == other.Major && Minor == other.Minor) return 0; + return -1; + } + + public override bool Equals(object? obj) + { + if (obj is not ClusterVersion clusterVersion) + return false; + + return clusterVersion.Major == Major && clusterVersion.Minor == Minor; + } + + public override int GetHashCode() + { +#if NET8_0_OR_GREATER + return HashCode.Combine(Major, Minor); +#else + unchecked // Overflow is fine in hash code calculations + { + int hash = 17; + hash = hash * 23 + Major.GetHashCode(); + hash = hash * 23 + Minor.GetHashCode(); + return hash; + } +#endif + } + + public override string ToString() + { + return $"{Major}.{Minor}"; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadata.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadata.cs new file mode 100644 index 000000000..9c8eed170 --- /dev/null +++ b/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadata.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Octopus.Tentacle.Kubernetes +{ + public record KubernetesAgentToolsImageVersionMetadata( + [JsonProperty("tools")] KubernetesAgentToolVersions ToolVersions, + [JsonProperty("latest")] Version Latest, + [JsonProperty("revisionHash")] string RevisionHash, + [JsonProperty("deprecations")] Dictionary Deprecations); + + public record KubernetesAgentToolVersions( + [JsonProperty("kubectl")] List Kubectl, + [JsonProperty("helm")] List Helm, + [JsonProperty("powershell")] List Powershell + ); + + public record KubernetesAgentToolDeprecation( + [JsonProperty("latestTag")] string LatestTag); +} \ No newline at end of file diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs new file mode 100644 index 000000000..b237a9558 --- /dev/null +++ b/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs @@ -0,0 +1,40 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using NuGet.Versioning; +using Octopus.Diagnostics; + +namespace Octopus.Tentacle.Kubernetes +{ + public interface IToolsImageVersionMetadataProvider + { + Task TryGetVersionMetadata(); + } + + public class KubernetesAgentToolsImageVersionMetadataProvider : IToolsImageVersionMetadataProvider + { + readonly ISystemLog log; + + public KubernetesAgentToolsImageVersionMetadataProvider(ISystemLog log) + { + this.log = log; + } + + public async Task TryGetVersionMetadata() + { + using var httpClient = new HttpClient(); + try + { + var response = await httpClient.GetAsync("https://oc.to/kubernetes-agent-tools-image-metadata"); + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + catch (Exception ex) + { + log.Error($"Failed to fetch version metadata for the agent tools container image. Details: {ex.Message}"); + return null; + } + } + } +} \ No newline at end of file diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesClusterService.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesClusterService.cs index 8d51ea7a9..af2435813 100644 --- a/source/Octopus.Tentacle/Kubernetes/KubernetesClusterService.cs +++ b/source/Octopus.Tentacle/Kubernetes/KubernetesClusterService.cs @@ -21,7 +21,7 @@ public KubernetesClusterService(IKubernetesClientConfigProvider configProvider, lazyVersion = new AsyncLazy(async () => { var versionInfo = await Client.Version.GetCodeAsync(); - return KubernetesVersionParser.ParseClusterVersion(versionInfo); + return ClusterVersion.FromVersionInfo(versionInfo); }, AsyncLazyFlags.RetryOnFailure); } diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs index d7d8e7bc1..cc27b09c2 100644 --- a/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs +++ b/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs @@ -15,6 +15,7 @@ protected override void Load(ContainerBuilder builder) { builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesPodContainerResolver.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesPodContainerResolver.cs index 21e24866f..eab1790bd 100644 --- a/source/Octopus.Tentacle/Kubernetes/KubernetesPodContainerResolver.cs +++ b/source/Octopus.Tentacle/Kubernetes/KubernetesPodContainerResolver.cs @@ -14,12 +14,17 @@ public interface IKubernetesPodContainerResolver public class KubernetesPodContainerResolver : IKubernetesPodContainerResolver { readonly IKubernetesClusterService clusterService; + readonly IToolsImageVersionMetadataProvider imageVersionMetadataProvider; - public KubernetesPodContainerResolver(IKubernetesClusterService clusterService) + public KubernetesPodContainerResolver(IKubernetesClusterService clusterService, IToolsImageVersionMetadataProvider imageVersionMetadataProvider) { this.clusterService = clusterService; + this.imageVersionMetadataProvider = imageVersionMetadataProvider; } + const string DefaultKubernetesAgentToolsImage = "octopusdeploy/kubernetes-agent-tools-base"; + const string FallbackImageTag = "latest"; + static readonly List KnownLatestContainerTags = new() { new(1, 26), @@ -34,23 +39,65 @@ public async Task GetContainerImageForCluster() var imageRepository = KubernetesConfig.ScriptPodContainerImage; if (imageRepository.IsNullOrEmpty()) { - return await GetKubernetesSpecificContainer(); + return await GetAgentToolsContainerImage(); } var imageTag = KubernetesConfig.ScriptPodContainerImageTag; return $"{imageRepository}:{imageTag}"; } - async Task GetKubernetesSpecificContainer() + async Task GetAgentToolsContainerImage() { var clusterVersion = await clusterService.GetClusterVersion(); + + var versionMetadata = await imageVersionMetadataProvider.TryGetVersionMetadata(); + if (TryGetImageTagFromVersionMetadata(versionMetadata, clusterVersion, out var imageTag)) + { + return $"{DefaultKubernetesAgentToolsImage}:{imageTag}"; + } - //find the highest tag for this cluster version + return GetFallbackAgentToolsImage(clusterVersion); + } + + static bool TryGetImageTagFromVersionMetadata(KubernetesAgentToolsImageVersionMetadata? versionMetadata, ClusterVersion clusterVersion, out string imageTag) + { + imageTag = ""; + if (versionMetadata is null) + { + return false; + } + + var versionDeprecation = versionMetadata.Deprecations.FirstOrDefault(kvp => ClusterVersion.FromVersion(kvp.Key).Equals(clusterVersion)); + if (versionDeprecation.Key is not null) + { + imageTag = versionDeprecation.Value.LatestTag; + return true; + } + + if (ClusterVersion.FromVersion(versionMetadata.Latest).CompareTo(clusterVersion) < 0) + { + imageTag = FallbackImageTag; + return true; + } + + var imageExists = versionMetadata.ToolVersions.Kubectl.Any(v => ClusterVersion.FromVersion(v).Equals(clusterVersion)); + if (imageExists) + { + imageTag = $"{clusterVersion}-{versionMetadata.RevisionHash}"; + return true; + } + + imageTag = FallbackImageTag; + return true; + } + + static string GetFallbackAgentToolsImage(ClusterVersion clusterVersion) + { var tagVersion = KnownLatestContainerTags.FirstOrDefault(tag => tag.Major == clusterVersion.Major && tag.Minor == clusterVersion.Minor); var tag = tagVersion?.ToString(2) ?? "latest"; - return $"octopusdeploy/kubernetes-agent-tools-base:{tag}"; + return $"{DefaultKubernetesAgentToolsImage}:{tag}"; } } } diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesVersionParser.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesVersionParser.cs deleted file mode 100644 index 53d0ce4cf..000000000 --- a/source/Octopus.Tentacle/Kubernetes/KubernetesVersionParser.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using k8s.Models; - -namespace Octopus.Tentacle.Kubernetes -{ - public static class KubernetesVersionParser - { - public static ClusterVersion ParseClusterVersion(VersionInfo versionInfo) - { - return new ClusterVersion(SanitizeAndParseVersionNumber(versionInfo.Major), SanitizeAndParseVersionNumber(versionInfo.Minor)); - } - - static int SanitizeAndParseVersionNumber(string version) - { - return int.Parse(Regex.Replace(version, "[^0-9]", "")); - } - - } - public record ClusterVersion(int Major, int Minor); -} \ No newline at end of file From 2681f8b2d0abc9dc0b4a350e09f509b72884de7f Mon Sep 17 00:00:00 2001 From: Kevin Tchang Date: Wed, 2 Oct 2024 13:59:53 +1000 Subject: [PATCH 3/5] add caching decorator for the metadata retriever --- ...sAgentToolsImageVersionMetadataProvider.cs | 24 +++++++++++++++++++ .../Kubernetes/KubernetesModule.cs | 8 ++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs index b237a9558..d8663643c 100644 --- a/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs +++ b/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs @@ -1,6 +1,7 @@ using System; using System.Net.Http; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; using Newtonsoft.Json; using NuGet.Versioning; using Octopus.Diagnostics; @@ -12,6 +13,29 @@ public interface IToolsImageVersionMetadataProvider Task TryGetVersionMetadata(); } + public class CachingKubernetesAgentToolsImageVersionMetadataProvider : IToolsImageVersionMetadataProvider + { + readonly IToolsImageVersionMetadataProvider inner; + readonly IMemoryCache memoryCache; + static readonly TimeSpan CacheExpiry = TimeSpan.FromHours(1); + + public CachingKubernetesAgentToolsImageVersionMetadataProvider(IToolsImageVersionMetadataProvider inner, IMemoryCache memoryCache) + { + this.inner = inner; + this.memoryCache = memoryCache; + } + + public Task TryGetVersionMetadata() + { + var cacheKey = $"{nameof(CachingKubernetesAgentToolsImageVersionMetadataProvider)}_VersionMetadata"; + return memoryCache.GetOrCreateAsync(cacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheExpiry; + return await inner.TryGetVersionMetadata(); + }); + } + } + public class KubernetesAgentToolsImageVersionMetadataProvider : IToolsImageVersionMetadataProvider { readonly ISystemLog log; diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs index cc27b09c2..619bfba9d 100644 --- a/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs +++ b/source/Octopus.Tentacle/Kubernetes/KubernetesModule.cs @@ -15,7 +15,13 @@ protected override void Load(ContainerBuilder builder) { builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); + + const string agentToolsImageMetadataProvider = "AgentToolsImageMetadataProvider"; + builder.RegisterType().Named(agentToolsImageMetadataProvider); + builder.RegisterDecorator( + (context, inner) => new CachingKubernetesAgentToolsImageVersionMetadataProvider( + inner, context.Resolve()), fromKey: agentToolsImageMetadataProvider); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); From 1d10ed70922bf74154ff716560dc01979571954f Mon Sep 17 00:00:00 2001 From: Kevin Tchang Date: Wed, 2 Oct 2024 14:01:07 +1000 Subject: [PATCH 4/5] remove unused import --- .../KubernetesAgentToolsImageVersionMetadataProvider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs index d8663643c..649f3877b 100644 --- a/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs +++ b/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using Newtonsoft.Json; -using NuGet.Versioning; using Octopus.Diagnostics; namespace Octopus.Tentacle.Kubernetes From bebb40e056ff04ace6299d07a69676f1d55a5817 Mon Sep 17 00:00:00 2001 From: Kevin Tchang Date: Wed, 2 Oct 2024 14:37:21 +1000 Subject: [PATCH 5/5] pass exception directly into log.Error --- .../KubernetesAgentToolsImageVersionMetadataProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs b/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs index 649f3877b..13c543be8 100644 --- a/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs +++ b/source/Octopus.Tentacle/Kubernetes/KubernetesAgentToolsImageVersionMetadataProvider.cs @@ -55,7 +55,7 @@ public KubernetesAgentToolsImageVersionMetadataProvider(ISystemLog log) } catch (Exception ex) { - log.Error($"Failed to fetch version metadata for the agent tools container image. Details: {ex.Message}"); + log.Error(ex, "Failed to fetch version metadata for the agent tools container image."); return null; } }