Skip to content
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
57 changes: 56 additions & 1 deletion .github/workflows/Alethic.Seq.Operator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,66 @@ jobs:
with:
name: tests
path: /tmp/tests.tar.gz
test:
strategy:
fail-fast: false
matrix:
run:
- Alethic.Seq.Operator.Tests
sys:
- win-x64
- linux-x64
- linux-arm64
tfm:
- net10.0
name: Test (${{ matrix.run }}:${{ matrix.tfm }}:${{ matrix.sys }})
needs:
- build
runs-on: ${{ fromJSON('{"win-x64":["windows-2022"],"linux-x64":["ubuntu-24.04"],"linux-arm64":["ubuntu-24.04-arm"]}')[matrix.sys] }}
steps:
- name: Setup .NET 10
uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.x
- name: Download Tests
uses: actions/download-artifact@v8
with:
name: tests
- name: Restore Tests
run: tar xzvf tests.tar.gz
- name: Execute Tests
timeout-minutes: 60
shell: pwsh
run: |
# assign powershell variables
$run = "${{ matrix.run }}"
$tfm = "${{ matrix.tfm }}"
$sys = "${{ matrix.sys }}"

# tests use Microsoft.Testing.Platform: execute the published test assembly directly, not `dotnet test`
$test = $(gi .\tests\$run\$tfm\$run.dll)
if ($test) {
Add-Content $env:GITHUB_ENV "`nRET=TestResults--$run--$tfm--$sys"
dotnet exec $test.FullName --results-directory TestResults --report-trx
} else {
throw "no test assembly found for $run/$tfm"
}
- name: Archive Test Results
if: always() && startsWith(env.RET, 'TestResults--')
shell: pwsh
run: tar czvf TestResults.tar.gz TestResults
- name: Upload Test Results
if: always() && startsWith(env.RET, 'TestResults--')
uses: actions/upload-artifact@v7
with:
name: ${{ env.RET }}
path: TestResults.tar.gz

release:
name: Release
if: github.event_name != 'pull_request'
needs:
- build
- test
runs-on: ubuntu-24.04
permissions:
contents: write
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TestingPlatformShowTestsFailure>true</TestingPlatformShowTestsFailure>
<!-- The operator sets SelfContained, and a non self-contained executable cannot reference a self-contained
one (NETSDK1150). The tests only need the operator's assembly, so skip that validation rather than
changing how the operator itself is published. -->
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Alethic.Seq.Operator\Alethic.Seq.Operator.csproj" />
</ItemGroup>

</Project>
172 changes: 172 additions & 0 deletions src/Alethic.Seq.Operator.Tests/EntityFinalizersTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Linq;

using Alethic.Seq.Operator.Alerts;
using Alethic.Seq.Operator.ApiKey;
using Alethic.Seq.Operator.Finalizers;
using Alethic.Seq.Operator.Instance;
using Alethic.Seq.Operator.RetentionPolicy;
using Alethic.Seq.Operator.Signals;

using KubeOps.Abstractions.Reconciliation.Finalizer;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Alethic.Seq.Operator.Tests
{

/// <summary>
/// Pins the exact strings this operator writes into <c>metadata.finalizers</c>.
/// </summary>
/// <remarks>
/// These are not cosmetic. KubeOps derives a finalizer's identifier from its class name, and the reconciler
/// inspects <c>Finalizers()[0]</c> only, returning without removing it when nothing is registered under that
/// identifier. Renaming a finalizer, or deleting one of the legacy shims, therefore wedges every live entity
/// carrying the old identifier in <c>Terminating</c> forever. A failure here means an upgrade would strand
/// objects in the cluster.
/// </remarks>
[TestClass]
public class EntityFinalizersTests
{

const string Group = "seq.k8s.datalust.co";

/// <summary>
/// Maximum length of a finalizer identifier, as enforced by KubeOps.
/// </summary>
const int MaxIdentifierLength = 63;

/// <summary>
/// The identifier attached to new entities of each kind. Changing one of these is a breaking change for
/// every object already in a cluster.
/// </summary>
static IEnumerable<(Type Entity, string Identifier)> CurrentIdentifiers()
{
yield return (typeof(V1alpha1Alert), "seq.k8s.datalust.co/alertfinalizer");
yield return (typeof(V1alpha1ApiKey), "seq.k8s.datalust.co/apikeyfinalizer");
yield return (typeof(V1alpha1Instance), "seq.k8s.datalust.co/instancefinalizer");
yield return (typeof(V1alpha1RetentionPolicy), "seq.k8s.datalust.co/retentionpolicyfinalizer");
yield return (typeof(V1alpha1Signal), "seq.k8s.datalust.co/signalfinalizer");
}

/// <summary>
/// Identifiers written by previous releases. They are no longer attached, but stay registered so that
/// entities already carrying them can drain.
/// </summary>
static IEnumerable<(Type Entity, string Identifier)> RetiredIdentifiers()
{
yield return (typeof(V1alpha1Alert), "seq.k8s.datalust.co/v1alpha1alertfinalizer");
yield return (typeof(V1alpha1ApiKey), "seq.k8s.datalust.co/v1alpha1apikeyfinalizer");
yield return (typeof(V1alpha1Instance), "seq.k8s.datalust.co/v1alpha1instancefinalizer");
yield return (typeof(V1alpha1RetentionPolicy), "seq.k8s.datalust.co/v1alpha1retentionpolicyfinalizer");
yield return (typeof(V1alpha1Signal), "seq.k8s.datalust.co/v1alpha1signalfinalizer");
}

/// <summary>
/// Invokes the generic <see cref="EntityFinalizers.Current{TEntity}"/> for a runtime type.
/// </summary>
/// <param name="entity"></param>
static string? Current(Type entity)
{
return (string?)typeof(EntityFinalizers)
.GetMethod(nameof(EntityFinalizers.Current))!
.MakeGenericMethod(entity)
.Invoke(null, null);
}

/// <summary>
/// Invokes the generic <see cref="EntityFinalizers.Retired{TEntity}"/> for a runtime type.
/// </summary>
/// <param name="entity"></param>
static IReadOnlyCollection<string> Retired(Type entity)
{
return (IReadOnlyCollection<string>)typeof(EntityFinalizers)
.GetMethod(nameof(EntityFinalizers.Retired))!
.MakeGenericMethod(entity)
.Invoke(null, null)!;
}

/// <summary>
/// Reproduces the identifier KubeOps derives from a finalizer's class name.
/// </summary>
/// <param name="finalizer"></param>
static string Identifier(Type finalizer)
{
var name = finalizer.Name.ToLowerInvariant();
if (name.EndsWith("finalizer") == false)
name += "finalizer";

var identifier = $"{Group}/{name}";
return identifier.Length > MaxIdentifierLength ? identifier.Substring(0, MaxIdentifierLength) : identifier;
}

/// <summary>
/// Every concrete finalizer in the operator assembly, paired with the entity it finalizes.
/// </summary>
static IEnumerable<(Type Finalizer, Type Entity)> RegisteredFinalizers()
{
foreach (var type in typeof(EntityFinalizers).Assembly.GetTypes())
{
if (type.IsClass == false || type.IsAbstract)
continue;

foreach (var iface in type.GetInterfaces())
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEntityFinalizer<>))
yield return (type, iface.GetGenericArguments()[0]);
}
}

[TestMethod]
public void CurrentIdentifiersAreExactlyAsReleased()
{
foreach (var (entity, identifier) in CurrentIdentifiers())
Assert.AreEqual(identifier, Current(entity), $"current finalizer identifier for {entity.Name} changed");
}

[TestMethod]
public void RetiredIdentifiersAreStillRegistered()
{
foreach (var (entity, identifier) in RetiredIdentifiers())
Assert.IsTrue(Retired(entity).Contains(identifier), $"retired finalizer identifier {identifier} is no longer registered; entities carrying it could never finish deleting");
}

[TestMethod]
public void CurrentIdentifierIsNeverAlsoRetired()
{
foreach (var (entity, identifier) in CurrentIdentifiers())
Assert.IsFalse(Retired(entity).Contains(identifier), $"{identifier} is both attached and stripped, so reconciliation would never settle");
}

[TestMethod]
public void IdentifiersFitTheKubernetesLimit()
{
foreach (var identifier in CurrentIdentifiers().Concat(RetiredIdentifiers()).Select(i => i.Identifier))
Assert.IsTrue(identifier.Length <= MaxIdentifierLength, $"{identifier} is {identifier.Length} characters, over the {MaxIdentifierLength} KubeOps allows");
}

[TestMethod]
public void EveryFinalizerClassIsAccountedForInTheTable()
{
foreach (var (finalizer, entity) in RegisteredFinalizers())
{
var identifier = Identifier(finalizer);
var known = Current(entity) == identifier || Retired(entity).Contains(identifier);
Assert.IsTrue(known, $"{finalizer.Name} resolves to {identifier}, which EntityFinalizers does not list for {entity.Name}");
}
}

[TestMethod]
public void EveryTableEntryHasAFinalizerClass()
{
var registered = RegisteredFinalizers()
.Select(i => (i.Entity, Identifier: Identifier(i.Finalizer)))
.ToHashSet();

foreach (var (entity, identifier) in CurrentIdentifiers().Concat(RetiredIdentifiers()))
Assert.IsTrue(registered.Contains((entity, identifier)), $"EntityFinalizers lists {identifier} for {entity.Name}, but no finalizer class produces it, so KubeOps could never resolve it");
}

}

}
7 changes: 6 additions & 1 deletion src/Alethic.Seq.Operator/Alethic.Seq.Operator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<Nullable>enable</Nullable>
<SelfContained>true</SelfContained>
<!-- Self-contained only when a runtime identifier is actually in play, which is the container publish
(dist-containers sets RuntimeIdentifier=linux-x64). Set unconditionally it also applies to a plain
build, which makes the assembly a RID-scoped asset of anything referencing this project: the test
project then resolves it on win-x64 and linux-x64 but not on linux-arm64, which is not in
RuntimeIdentifiers. The published image is unaffected, since it always sets a RID. -->
<SelfContained Condition=" '$(RuntimeIdentifier)' != '' ">true</SelfContained>
<ContainerRepository>seq-operator-image</ContainerRepository>
<KubeOpsCli>dotnet tool run kubeops</KubeOpsCli>
<GenerateOperatorResources>true</GenerateOperatorResources>
Expand Down
5 changes: 1 addition & 4 deletions src/dist-tests/dist-tests.msbuildproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
</PropertyGroup>

<ItemGroup>
<TestTarget Include="IKVM.ByteCode.Tests|net472" ProjectName="IKVM.ByteCode.Tests" TargetFramework="net472" />
<TestTarget Include="IKVM.ByteCode.Tests|net6.0" ProjectName="IKVM.ByteCode.Tests" TargetFramework="net6.0" />
<TestTarget Include="IKVM.ByteCode.Tests|net7.0" ProjectName="IKVM.ByteCode.Tests" TargetFramework="net7.0" />
<TestTarget Include="IKVM.ByteCode.Tests|net8.0" ProjectName="IKVM.ByteCode.Tests" TargetFramework="net8.0" />
<TestTarget Include="Alethic.Seq.Operator.Tests|net10.0" ProjectName="Alethic.Seq.Operator.Tests" TargetFramework="net10.0" />
</ItemGroup>

<!-- Runs once per TFM/RID combination to generate individual output. -->
Expand Down
Loading