too much for one commit
This commit is contained in:
739
tests/EntKube.Clusters.Tests/Features/WorkloadRemediatorTests.cs
Normal file
739
tests/EntKube.Clusters.Tests/Features/WorkloadRemediatorTests.cs
Normal file
@@ -0,0 +1,739 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user