Files
Entkube/tests/EntKube.Clusters.Tests/Features/WorkloadRemediatorTests.cs
2026-05-13 14:01:32 +02:00

740 lines
27 KiB
C#

using EntKube.Clusters.Domain;
using EntKube.Clusters.Features.AdoptCluster.Components.SecurityPolicies;
using FluentAssertions;
using k8s;
using k8s.Models;
using k8s.Autorest;
using Moq;
using Microsoft.Extensions.Logging;
namespace EntKube.Clusters.Tests.Features;
public class WorkloadRemediatorTests
{
private readonly Mock<IKubernetes> mockClient;
private readonly Mock<IAppsV1Operations> appsMock;
private readonly Mock<ILogger<WorkloadRemediator>> mockLogger;
private readonly WorkloadRemediator sut;
public WorkloadRemediatorTests()
{
mockClient = new Mock<IKubernetes>();
appsMock = new Mock<IAppsV1Operations>();
mockClient.Setup(c => c.AppsV1).Returns(appsMock.Object);
mockLogger = new Mock<ILogger<WorkloadRemediator>>();
sut = new WorkloadRemediator(mockLogger.Object);
// Default: empty workloads for all types.
SetupMockDeployments(new V1DeploymentList { Items = new List<V1Deployment>() });
SetupMockStatefulSets(new V1StatefulSetList { Items = new List<V1StatefulSet>() });
SetupMockDaemonSets(new V1DaemonSetList { Items = new List<V1DaemonSet>() });
}
// --- Seccomp Remediation ---
[Fact]
public async Task RemediateAsync_DeploymentWithoutSeccomp_PatchesSeccompProfile()
{
// Arrange — A deployment exists that has no seccomp profile set.
// The remediator should patch it to add RuntimeDefault seccomp.
RemediationResult result = await sut.RemediateAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert — since the mock returns no workloads, no patches needed.
// This verifies the method runs without errors on empty clusters.
result.Should().NotBeNull();
result.PatchedWorkloads.Should().NotBeNull();
result.PolicyExceptions.Should().NotBeNull();
}
[Fact]
public async Task RemediateAsync_ExcludedNamespacesAreSkipped()
{
// Arrange — Deployments in excluded namespaces should not be touched.
V1DeploymentList deployments = new()
{
Items = new List<V1Deployment>
{
CreateDeployment("kube-system", "coredns", withSeccomp: false, withReadonlyRootfs: false)
}
};
SetupMockDeployments(deployments);
SetupEmptyStatefulSets();
SetupEmptyDaemonSets();
// Act
RemediationResult result = await sut.RemediateAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert — kube-system workload should be excluded, so nothing patched.
result.PatchedWorkloads.Should().BeEmpty();
result.PolicyExceptions.Should().BeEmpty();
}
[Fact]
public async Task RemediateAsync_DeploymentMissingSeccomp_AddsSeccompPatch()
{
// Arrange — A deployment in a tenant namespace is missing seccomp.
// The remediator should patch it to add seccomp RuntimeDefault.
V1DeploymentList deployments = new()
{
Items = new List<V1Deployment>
{
CreateDeployment("app-namespace", "my-app", withSeccomp: false, withReadonlyRootfs: true)
}
};
SetupMockDeployments(deployments);
SetupEmptyStatefulSets();
SetupEmptyDaemonSets();
SetupPatchSuccess();
// Act
RemediationResult result = await sut.RemediateAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert — The deployment was patched for seccomp compliance.
result.PatchedWorkloads.Should().Contain(w =>
w.Name == "my-app" && w.Namespace == "app-namespace" && w.Reason == "seccomp");
}
[Fact]
public async Task RemediateAsync_DeploymentMissingReadonlyRootfs_AddsReadonlyPatch()
{
// Arrange — A deployment missing readOnlyRootFilesystem on its containers.
V1DeploymentList deployments = new()
{
Items = new List<V1Deployment>
{
CreateDeployment("app-namespace", "my-app", withSeccomp: true, withReadonlyRootfs: false)
}
};
SetupMockDeployments(deployments);
SetupEmptyStatefulSets();
SetupEmptyDaemonSets();
SetupPatchSuccess();
// Act
RemediationResult result = await sut.RemediateAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert
result.PatchedWorkloads.Should().Contain(w =>
w.Name == "my-app" && w.Namespace == "app-namespace" && w.Reason == "readonlyRootfs");
}
[Fact]
public async Task RemediateAsync_PatchFails_CreatesPolicyException()
{
// Arrange — A deployment that needs patching but the patch fails.
// In that case, a PolicyException should be created for that workload.
V1DeploymentList deployments = new()
{
Items = new List<V1Deployment>
{
CreateDeployment("app-namespace", "stubborn-app", withSeccomp: false, withReadonlyRootfs: false)
}
};
SetupMockDeployments(deployments);
SetupEmptyStatefulSets();
SetupEmptyDaemonSets();
SetupPatchFailure();
// Act
RemediationResult result = await sut.RemediateAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert — Since the patch failed, we get a policy exception instead.
result.PolicyExceptions.Should().Contain(e =>
e.WorkloadName == "stubborn-app" && e.Namespace == "app-namespace");
}
[Fact]
public async Task RemediateAsync_StatefulSetMissingSeccomp_PatchesIt()
{
// Arrange — StatefulSets should also be remediated.
V1StatefulSetList statefulSets = new()
{
Items = new List<V1StatefulSet>
{
CreateStatefulSet("data-namespace", "postgres", withSeccomp: false, withReadonlyRootfs: true)
}
};
SetupEmptyDeployments();
SetupMockStatefulSets(statefulSets);
SetupEmptyDaemonSets();
SetupPatchSuccess();
// Act
RemediationResult result = await sut.RemediateAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert
result.PatchedWorkloads.Should().Contain(w =>
w.Name == "postgres" && w.Namespace == "data-namespace" && w.Reason == "seccomp");
}
[Fact]
public async Task RemediateAsync_DaemonSetMissingReadonly_PatchesIt()
{
// Arrange — DaemonSets should also be remediated.
V1DaemonSetList daemonSets = new()
{
Items = new List<V1DaemonSet>
{
CreateDaemonSet("monitoring", "node-exporter", withSeccomp: true, withReadonlyRootfs: false)
}
};
SetupEmptyDeployments();
SetupEmptyStatefulSets();
SetupMockDaemonSets(daemonSets);
SetupPatchSuccess();
// Act — monitoring is NOT in excluded list for this test.
RemediationResult result = await sut.RemediateAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert
result.PatchedWorkloads.Should().Contain(w =>
w.Name == "node-exporter" && w.Namespace == "monitoring" && w.Reason == "readonlyRootfs");
}
[Fact]
public async Task RemediateAsync_CompliantWorkload_NoChanges()
{
// Arrange — A workload that already has seccomp and readonlyRootfs.
V1DeploymentList deployments = new()
{
Items = new List<V1Deployment>
{
CreateDeployment("app-namespace", "compliant-app", withSeccomp: true, withReadonlyRootfs: true)
}
};
SetupMockDeployments(deployments);
SetupEmptyStatefulSets();
SetupEmptyDaemonSets();
// Act
RemediationResult result = await sut.RemediateAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert — Compliant workload should not be touched.
result.PatchedWorkloads.Should().BeEmpty();
result.PolicyExceptions.Should().BeEmpty();
}
[Fact]
public async Task CreatePolicyExceptionAsync_CreatesValidKyvernoPolicyException()
{
// Arrange — We need to create a PolicyException for a specific workload.
PolicyExceptionRequest request = new(
WorkloadName: "my-app",
Namespace: "app-namespace",
WorkloadKind: "Deployment",
PolicyNames: new List<string> { "require-seccomp-runtime-default", "require-readonly-root-filesystem" });
// Act
object exception = WorkloadRemediator.BuildPolicyException(request);
// Assert — The resulting object should have the correct Kyverno structure.
exception.Should().NotBeNull();
Dictionary<string, object> exceptionDict = (Dictionary<string, object>)exception;
exceptionDict["apiVersion"].Should().Be("kyverno.io/v2");
exceptionDict["kind"].Should().Be("PolicyException");
}
[Fact]
public async Task RemediateAsync_WritableRootFsApp_SkipsReadonlyPatch()
{
// Arrange — A Keycloak deployment in a tenant namespace doesn't have
// readOnlyRootFilesystem set. Because Keycloak requires a writable root,
// the remediator should NOT attempt to patch it for readonly compliance.
V1DeploymentList deployments = new()
{
Items = new List<V1Deployment>
{
CreateDeployment("tenant-apps", "keycloak", withSeccomp: true, withReadonlyRootfs: false,
labels: new Dictionary<string, string> { ["app.kubernetes.io/name"] = "keycloak" })
}
};
SetupMockDeployments(deployments);
SetupEmptyStatefulSets();
SetupEmptyDaemonSets();
SetupPatchSuccess();
// Act
RemediationResult result = await sut.RemediateAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert — No readonlyRootfs patch should be applied, and no exception needed.
result.PatchedWorkloads.Should().NotContain(w => w.Reason == "readonlyRootfs");
result.PolicyExceptions.Should().BeEmpty();
}
[Fact]
public async Task RemediateAsync_MinioStatefulSet_SkipsReadonlyPatch()
{
// Arrange — A MinIO StatefulSet needs a writable root filesystem for data
// storage. It should be skipped for readOnly remediation but still get
// seccomp patched if missing.
V1StatefulSet minioSts = new()
{
Metadata = new V1ObjectMeta { Name = "minio", NamespaceProperty = "tenant-storage" },
Spec = new V1StatefulSetSpec
{
Template = new V1PodTemplateSpec
{
Metadata = new V1ObjectMeta
{
Labels = new Dictionary<string, string> { ["app.kubernetes.io/name"] = "minio" }
},
Spec = new V1PodSpec
{
Containers = new List<V1Container>
{
new() { Name = "minio", Image = "minio/minio:latest", SecurityContext = new V1SecurityContext() }
}
}
}
}
};
SetupMockDeployments(new V1DeploymentList { Items = new List<V1Deployment>() });
SetupMockStatefulSets(new V1StatefulSetList { Items = new List<V1StatefulSet> { minioSts } });
SetupEmptyDaemonSets();
SetupPatchSuccess();
// Act
RemediationResult result = await sut.RemediateAsync(
mockClient.Object,
new List<string> { "kube-system" },
CancellationToken.None);
// Assert — Seccomp should be patched, but readonlyRootfs should NOT.
result.PatchedWorkloads.Should().Contain(w => w.Reason == "seccomp");
result.PatchedWorkloads.Should().NotContain(w => w.Reason == "readonlyRootfs");
}
// --- Helper Methods ---
private V1Deployment CreateDeployment(string ns, string name, bool withSeccomp, bool withReadonlyRootfs, Dictionary<string, string>? labels = null)
{
V1SecurityContext containerSecurity = new()
{
ReadOnlyRootFilesystem = withReadonlyRootfs ? true : null
};
V1PodSecurityContext podSecurity = new();
if (withSeccomp)
{
podSecurity.SeccompProfile = new V1SeccompProfile { Type = "RuntimeDefault" };
}
return new V1Deployment
{
Metadata = new V1ObjectMeta { Name = name, NamespaceProperty = ns },
Spec = new V1DeploymentSpec
{
Template = new V1PodTemplateSpec
{
Metadata = new V1ObjectMeta { Labels = labels },
Spec = new V1PodSpec
{
SecurityContext = podSecurity,
Containers = new List<V1Container>
{
new() { Name = "main", Image = "docker.io/app:latest", SecurityContext = containerSecurity }
}
}
}
}
};
}
private V1StatefulSet CreateStatefulSet(string ns, string name, bool withSeccomp, bool withReadonlyRootfs)
{
V1SecurityContext containerSecurity = new()
{
ReadOnlyRootFilesystem = withReadonlyRootfs ? true : null
};
V1PodSecurityContext podSecurity = new();
if (withSeccomp)
{
podSecurity.SeccompProfile = new V1SeccompProfile { Type = "RuntimeDefault" };
}
return new V1StatefulSet
{
Metadata = new V1ObjectMeta { Name = name, NamespaceProperty = ns },
Spec = new V1StatefulSetSpec
{
Template = new V1PodTemplateSpec
{
Spec = new V1PodSpec
{
SecurityContext = podSecurity,
Containers = new List<V1Container>
{
new() { Name = "main", Image = "docker.io/app:latest", SecurityContext = containerSecurity }
}
}
}
}
};
}
private V1DaemonSet CreateDaemonSet(string ns, string name, bool withSeccomp, bool withReadonlyRootfs)
{
V1SecurityContext containerSecurity = new()
{
ReadOnlyRootFilesystem = withReadonlyRootfs ? true : null
};
V1PodSecurityContext podSecurity = new();
if (withSeccomp)
{
podSecurity.SeccompProfile = new V1SeccompProfile { Type = "RuntimeDefault" };
}
return new V1DaemonSet
{
Metadata = new V1ObjectMeta { Name = name, NamespaceProperty = ns },
Spec = new V1DaemonSetSpec
{
Template = new V1PodTemplateSpec
{
Spec = new V1PodSpec
{
SecurityContext = podSecurity,
Containers = new List<V1Container>
{
new() { Name = "main", Image = "docker.io/app:latest", SecurityContext = containerSecurity }
}
}
}
}
};
}
private void SetupMockDeployments(V1DeploymentList deployments)
{
HttpOperationResponse<V1DeploymentList> response = new() { Body = deployments };
appsMock.Setup(a => a.ListDeploymentForAllNamespacesWithHttpMessagesAsync(
It.IsAny<bool?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(),
It.IsAny<int?>(), It.IsAny<bool?>(), It.IsAny<string?>(), It.IsAny<string?>(),
It.IsAny<bool?>(), It.IsAny<int?>(), It.IsAny<bool?>(),
It.IsAny<IReadOnlyDictionary<string, IReadOnlyList<string>>?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(response);
}
private void SetupEmptyDeployments()
{
SetupMockDeployments(new V1DeploymentList { Items = new List<V1Deployment>() });
}
private void SetupMockStatefulSets(V1StatefulSetList statefulSets)
{
HttpOperationResponse<V1StatefulSetList> response = new() { Body = statefulSets };
appsMock.Setup(a => a.ListStatefulSetForAllNamespacesWithHttpMessagesAsync(
It.IsAny<bool?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(),
It.IsAny<int?>(), It.IsAny<bool?>(), It.IsAny<string?>(), It.IsAny<string?>(),
It.IsAny<bool?>(), It.IsAny<int?>(), It.IsAny<bool?>(),
It.IsAny<IReadOnlyDictionary<string, IReadOnlyList<string>>?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(response);
}
private void SetupEmptyStatefulSets()
{
SetupMockStatefulSets(new V1StatefulSetList { Items = new List<V1StatefulSet>() });
}
private void SetupMockDaemonSets(V1DaemonSetList daemonSets)
{
HttpOperationResponse<V1DaemonSetList> response = new() { Body = daemonSets };
appsMock.Setup(a => a.ListDaemonSetForAllNamespacesWithHttpMessagesAsync(
It.IsAny<bool?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(),
It.IsAny<int?>(), It.IsAny<bool?>(), It.IsAny<string?>(), It.IsAny<string?>(),
It.IsAny<bool?>(), It.IsAny<int?>(), It.IsAny<bool?>(),
It.IsAny<IReadOnlyDictionary<string, IReadOnlyList<string>>?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(response);
}
private void SetupEmptyDaemonSets()
{
SetupMockDaemonSets(new V1DaemonSetList { Items = new List<V1DaemonSet>() });
}
private void SetupPatchSuccess()
{
HttpOperationResponse<V1Deployment> deployResponse = new() { Body = new V1Deployment() };
appsMock.Setup(a => a.PatchNamespacedDeploymentWithHttpMessagesAsync(
It.IsAny<V1Patch>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<bool?>(),
It.IsAny<bool?>(),
It.IsAny<IReadOnlyDictionary<string, IReadOnlyList<string>>?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(deployResponse);
HttpOperationResponse<V1StatefulSet> stsResponse = new() { Body = new V1StatefulSet() };
appsMock.Setup(a => a.PatchNamespacedStatefulSetWithHttpMessagesAsync(
It.IsAny<V1Patch>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<bool?>(),
It.IsAny<bool?>(),
It.IsAny<IReadOnlyDictionary<string, IReadOnlyList<string>>?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(stsResponse);
HttpOperationResponse<V1DaemonSet> dsResponse = new() { Body = new V1DaemonSet() };
appsMock.Setup(a => a.PatchNamespacedDaemonSetWithHttpMessagesAsync(
It.IsAny<V1Patch>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<bool?>(),
It.IsAny<bool?>(),
It.IsAny<IReadOnlyDictionary<string, IReadOnlyList<string>>?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(dsResponse);
}
private void SetupPatchFailure()
{
appsMock.Setup(a => a.PatchNamespacedDeploymentWithHttpMessagesAsync(
It.IsAny<V1Patch>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<bool?>(),
It.IsAny<bool?>(),
It.IsAny<IReadOnlyDictionary<string, IReadOnlyList<string>>?>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new HttpOperationException("Patch failed") { Response = new HttpResponseMessageWrapper(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Forbidden), "") });
appsMock.Setup(a => a.PatchNamespacedStatefulSetWithHttpMessagesAsync(
It.IsAny<V1Patch>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<bool?>(),
It.IsAny<bool?>(),
It.IsAny<IReadOnlyDictionary<string, IReadOnlyList<string>>?>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new HttpOperationException("Patch failed") { Response = new HttpResponseMessageWrapper(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Forbidden), "") });
appsMock.Setup(a => a.PatchNamespacedDaemonSetWithHttpMessagesAsync(
It.IsAny<V1Patch>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<bool?>(),
It.IsAny<bool?>(),
It.IsAny<IReadOnlyDictionary<string, IReadOnlyList<string>>?>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new HttpOperationException("Patch failed") { Response = new HttpResponseMessageWrapper(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Forbidden), "") });
}
// --- Revert Patches ---
[Fact]
public async Task RevertPatchesAsync_DeploymentWithSeccomp_RemovesSeccompProfile()
{
// Arrange — A deployment that has seccomp RuntimeDefault set. The revert
// should remove the seccomp profile so the workload no longer requires it.
V1DeploymentList deployments = new()
{
Items = new List<V1Deployment>
{
CreateDeployment("app-namespace", "my-app", withSeccomp: true, withReadonlyRootfs: false)
}
};
SetupMockDeployments(deployments);
SetupEmptyStatefulSets();
SetupEmptyDaemonSets();
SetupPatchSuccess();
// Act
RemediationResult result = await sut.RevertPatchesAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert — The deployment should have been reverted for seccomp.
result.PatchedWorkloads.Should().Contain(w =>
w.Name == "my-app" && w.Namespace == "app-namespace" && w.Reason == "revert-seccomp");
}
[Fact]
public async Task RevertPatchesAsync_DeploymentWithReadonlyRootfs_RemovesReadonly()
{
// Arrange — A deployment that has readOnlyRootFilesystem=true on its containers.
// The revert should set it back to false.
V1DeploymentList deployments = new()
{
Items = new List<V1Deployment>
{
CreateDeployment("app-namespace", "my-app", withSeccomp: false, withReadonlyRootfs: true)
}
};
SetupMockDeployments(deployments);
SetupEmptyStatefulSets();
SetupEmptyDaemonSets();
SetupPatchSuccess();
// Act
RemediationResult result = await sut.RevertPatchesAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert — The deployment should have been reverted for readonlyRootfs.
result.PatchedWorkloads.Should().Contain(w =>
w.Name == "my-app" && w.Namespace == "app-namespace" && w.Reason == "revert-readonlyRootfs");
}
[Fact]
public async Task RevertPatchesAsync_WritableRootFsApp_SkipsReadonlyButRevertsSeccomp()
{
// Arrange — A Keycloak deployment has both seccomp and readOnlyRootFilesystem.
// Following the Terraform exclusion pattern, Keycloak is in WritableRootFsApps
// so readOnly was never applied by our remediator — we must NOT revert it.
// But seccomp WAS applied to everything, so it should still be reverted.
V1DeploymentList deployments = new()
{
Items = new List<V1Deployment>
{
CreateDeployment("tenant-apps", "keycloak", withSeccomp: true, withReadonlyRootfs: true,
labels: new Dictionary<string, string> { ["app.kubernetes.io/name"] = "keycloak" })
}
};
SetupMockDeployments(deployments);
SetupEmptyStatefulSets();
SetupEmptyDaemonSets();
SetupPatchSuccess();
// Act
RemediationResult result = await sut.RevertPatchesAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert — Seccomp reverted (applied to all), readOnly NOT reverted (excluded app).
result.PatchedWorkloads.Should().Contain(w => w.Reason == "revert-seccomp");
result.PatchedWorkloads.Should().NotContain(w => w.Reason == "revert-readonlyRootfs");
}
[Fact]
public async Task RevertPatchesAsync_ExcludedNamespace_SkipsWorkloads()
{
// Arrange — Workloads in excluded namespaces should not be reverted.
V1DeploymentList deployments = new()
{
Items = new List<V1Deployment>
{
CreateDeployment("kube-system", "coredns", withSeccomp: true, withReadonlyRootfs: true)
}
};
SetupMockDeployments(deployments);
SetupEmptyStatefulSets();
SetupEmptyDaemonSets();
// Act
RemediationResult result = await sut.RevertPatchesAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert — kube-system workloads are excluded, so nothing reverted.
result.PatchedWorkloads.Should().BeEmpty();
}
[Fact]
public async Task RevertPatchesAsync_CompliantAndUnpatched_NoChanges()
{
// Arrange — A workload that has neither seccomp nor readOnlyRootFilesystem
// set. Nothing to revert.
V1DeploymentList deployments = new()
{
Items = new List<V1Deployment>
{
CreateDeployment("app-namespace", "vanilla-app", withSeccomp: false, withReadonlyRootfs: false)
}
};
SetupMockDeployments(deployments);
SetupEmptyStatefulSets();
SetupEmptyDaemonSets();
// Act
RemediationResult result = await sut.RevertPatchesAsync(
mockClient.Object,
new List<string> { "kube-system", "kyverno" },
CancellationToken.None);
// Assert — Nothing to revert.
result.PatchedWorkloads.Should().BeEmpty();
}
}