Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement image metadata check for Kubernetes Agent tools to use the latest image tag revision for pull policy workaround #1010

Merged
merged 6 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions source/Octopus.Tentacle.Tests/Kubernetes/ClusterVersionTests.cs
Original file line number Diff line number Diff line change
@@ -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<FormatException>();
else
throw;
}
}

static IEnumerable<TestCaseData> 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<TestCaseData> 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<TestCaseData> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Version>
{
new("1.31.1"),
new("1.30.5"),
new("1.29.9"),
new("1.28.14")
}, new List<Version> { new("3.16.1") },
new List<Version> { new("7.4.5") }), new Version("1.30"), "Juaa5J", new Dictionary<Version, KubernetesAgentToolDeprecation>
{
{ 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<IToolsImageVersionMetadataProvider>();

[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<IKubernetesClusterService>();
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<IKubernetesClusterService>();
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<IKubernetesClusterService>();
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<IKubernetesClusterService>();
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<IKubernetesClusterService>();
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}");
}
}
}

This file was deleted.

69 changes: 69 additions & 0 deletions source/Octopus.Tentacle/Kubernetes/ClusterVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Text.RegularExpressions;
using k8s.Models;

namespace Octopus.Tentacle.Kubernetes
{
public class ClusterVersion : IComparable<ClusterVersion>
{
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if there's a way to avoid doing this. I ran into a compiler error similar to the one described here because HashCode is not available in the .NET Framework. Since we only build the Kubernetes Tentacle with .NET 8, this part of the code would never actually be used.

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}";
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Version, KubernetesAgentToolDeprecation> Deprecations);

public record KubernetesAgentToolVersions(
[JsonProperty("kubectl")] List<Version> Kubectl,
[JsonProperty("helm")] List<Version> Helm,
[JsonProperty("powershell")] List<Version> Powershell
);

public record KubernetesAgentToolDeprecation(
[JsonProperty("latestTag")] string LatestTag);
}
Loading