Skip to content

Commit 771c9be

Browse files
committed
Add e2e test for SELinux metrics
This is the commit message for patch #2 (refresh-temp):
1 parent f99c351 commit 771c9be

File tree

1 file changed

+217
-0
lines changed

1 file changed

+217
-0
lines changed

test/e2e/storage/csi_mock/csi_selinux_mount.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,22 @@ package csi_mock
1818

1919
import (
2020
"context"
21+
"fmt"
22+
"sort"
2123
"sync/atomic"
24+
"time"
2225

2326
"github.com/onsi/ginkgo/v2"
2427
"github.com/onsi/gomega"
2528
v1 "k8s.io/api/core/v1"
2629
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2730
"k8s.io/apimachinery/pkg/fields"
31+
"k8s.io/apimachinery/pkg/util/sets"
32+
"k8s.io/apimachinery/pkg/util/wait"
2833
"k8s.io/kubernetes/pkg/kubelet/events"
2934
"k8s.io/kubernetes/test/e2e/framework"
3035
e2eevents "k8s.io/kubernetes/test/e2e/framework/events"
36+
e2emetrics "k8s.io/kubernetes/test/e2e/framework/metrics"
3137
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
3238
e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
3339
"k8s.io/kubernetes/test/e2e/storage/utils"
@@ -237,3 +243,214 @@ var _ = utils.SIGDescribe("CSI Mock selinux on mount", func() {
237243
}
238244
})
239245
})
246+
247+
var _ = utils.SIGDescribe("CSI Mock selinux on mount metrics", func() {
248+
f := framework.NewDefaultFramework("csi-mock-volumes-selinux-metrics")
249+
f.NamespacePodSecurityEnforceLevel = admissionapi.LevelPrivileged
250+
m := newMockDriverSetup(f)
251+
252+
// [Serial]: the tests read global kube-controller-manager metrics, so no other test changes them in parallel.
253+
ginkgo.Context("SELinuxMount metrics [LinuxOnly][Feature:SELinux][Feature:SELinuxMountReadWriteOncePod][Serial]", func() {
254+
255+
// All SELinux metrics. Unless explicitly mentioned in test.expectIncreases, these metrics must not grow during
256+
// a test.
257+
allMetrics := sets.NewString(
258+
"volume_manager_selinux_container_errors_total",
259+
"volume_manager_selinux_container_warnings_total",
260+
"volume_manager_selinux_pod_context_mismatch_errors_total",
261+
"volume_manager_selinux_pod_context_mismatch_warnings_total",
262+
"volume_manager_selinux_volume_context_mismatch_errors_total",
263+
"volume_manager_selinux_volume_context_mismatch_warnings_total",
264+
"volume_manager_selinux_volumes_admitted_total",
265+
)
266+
267+
// Make sure all options are set so system specific defaults are not used.
268+
seLinuxOpts1 := v1.SELinuxOptions{
269+
User: "system_u",
270+
Role: "object_r",
271+
Type: "container_file_t",
272+
Level: "s0:c0,c1",
273+
}
274+
seLinuxOpts2 := v1.SELinuxOptions{
275+
User: "system_u",
276+
Role: "object_r",
277+
Type: "container_file_t",
278+
Level: "s0:c98,c99",
279+
}
280+
281+
tests := []struct {
282+
name string
283+
csiDriverSELinuxEnabled bool
284+
firstPodSELinuxOpts *v1.SELinuxOptions
285+
secondPodSELinuxOpts *v1.SELinuxOptions
286+
volumeMode v1.PersistentVolumeAccessMode
287+
waitForSecondPodStart bool
288+
secondPodFailureEvent string
289+
expectIncreases sets.String
290+
}{
291+
{
292+
name: "warning is not bumped on two Pods with the same context on RWO volume",
293+
csiDriverSELinuxEnabled: true,
294+
firstPodSELinuxOpts: &seLinuxOpts1,
295+
secondPodSELinuxOpts: &seLinuxOpts1,
296+
volumeMode: v1.ReadWriteOnce,
297+
waitForSecondPodStart: true,
298+
expectIncreases: sets.NewString( /* no metric is increased, admitted_total was already increased when the first pod started */ ),
299+
},
300+
{
301+
name: "warning is bumped on two Pods with a different context on RWO volume",
302+
csiDriverSELinuxEnabled: true,
303+
firstPodSELinuxOpts: &seLinuxOpts1,
304+
secondPodSELinuxOpts: &seLinuxOpts2,
305+
volumeMode: v1.ReadWriteOnce,
306+
waitForSecondPodStart: true,
307+
expectIncreases: sets.NewString("volume_manager_selinux_volume_context_mismatch_warnings_total"),
308+
},
309+
{
310+
name: "error is bumped on two Pods with a different context on RWOP volume",
311+
csiDriverSELinuxEnabled: true,
312+
firstPodSELinuxOpts: &seLinuxOpts1,
313+
secondPodSELinuxOpts: &seLinuxOpts2,
314+
secondPodFailureEvent: "conflicting SELinux labels of volume",
315+
volumeMode: v1.ReadWriteOncePod,
316+
waitForSecondPodStart: false,
317+
expectIncreases: sets.NewString("volume_manager_selinux_volume_context_mismatch_errors_total"),
318+
},
319+
}
320+
for _, t := range tests {
321+
t := t
322+
ginkgo.It(t.name, func(ctx context.Context) {
323+
if framework.NodeOSDistroIs("windows") {
324+
e2eskipper.Skipf("SELinuxMount is only applied on linux nodes -- skipping")
325+
}
326+
grabber, err := e2emetrics.NewMetricsGrabber(ctx, f.ClientSet, nil, f.ClientConfig(), true, false, false, false, false, false)
327+
framework.ExpectNoError(err, "creating the metrics grabber")
328+
329+
var nodeStageMountOpts, nodePublishMountOpts []string
330+
var unstageCalls, stageCalls, unpublishCalls, publishCalls atomic.Int32
331+
m.init(ctx, testParameters{
332+
disableAttach: true,
333+
registerDriver: true,
334+
enableSELinuxMount: &t.csiDriverSELinuxEnabled,
335+
hooks: createSELinuxMountPreHook(&nodeStageMountOpts, &nodePublishMountOpts, &stageCalls, &unstageCalls, &publishCalls, &unpublishCalls),
336+
})
337+
ginkgo.DeferCleanup(m.cleanup)
338+
339+
ginkgo.By("Starting the first pod")
340+
accessModes := []v1.PersistentVolumeAccessMode{t.volumeMode}
341+
_, claim, pod := m.createPodWithSELinux(ctx, accessModes, []string{}, t.firstPodSELinuxOpts)
342+
err = e2epod.WaitForPodNameRunningInNamespace(ctx, m.cs, pod.Name, pod.Namespace)
343+
framework.ExpectNoError(err, "starting the initial pod")
344+
345+
ginkgo.By("Grabbing initial metrics")
346+
pod, err = m.cs.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
347+
framework.ExpectNoError(err, "getting the initial pod")
348+
metrics, err := grabMetrics(ctx, grabber, pod.Spec.NodeName, allMetrics)
349+
framework.ExpectNoError(err, "collecting the initial metrics")
350+
dumpMetrics(metrics)
351+
352+
// Act
353+
ginkgo.By("Starting the second pod")
354+
// Skip scheduler, it would block scheduling the second pod with ReadWriteOncePod PV.
355+
nodeSelection := e2epod.NodeSelection{Name: pod.Spec.NodeName}
356+
pod2, err := startPausePodWithSELinuxOptions(f.ClientSet, claim, nodeSelection, f.Namespace.Name, t.secondPodSELinuxOpts)
357+
framework.ExpectNoError(err, "creating second pod with SELinux context %s", t.secondPodSELinuxOpts)
358+
m.pods = append(m.pods, pod2)
359+
360+
if t.waitForSecondPodStart {
361+
err := e2epod.WaitForPodNameRunningInNamespace(ctx, m.cs, pod2.Name, pod2.Namespace)
362+
framework.ExpectNoError(err, "starting the second pod")
363+
} else {
364+
ginkgo.By("Waiting for the second pod to fail to start")
365+
eventSelector := fields.Set{
366+
"involvedObject.kind": "Pod",
367+
"involvedObject.name": pod2.Name,
368+
"involvedObject.namespace": pod2.Namespace,
369+
"reason": events.FailedMountVolume,
370+
}.AsSelector().String()
371+
err = e2eevents.WaitTimeoutForEvent(ctx, m.cs, pod2.Namespace, eventSelector, t.secondPodFailureEvent, f.Timeouts.PodStart)
372+
framework.ExpectNoError(err, "waiting for event %q in the second test pod", t.secondPodFailureEvent)
373+
}
374+
375+
// Assert: count the metrics
376+
ginkgo.By("Waiting for expected metric changes")
377+
err = waitForMetricIncrease(ctx, grabber, pod.Spec.NodeName, allMetrics, t.expectIncreases, metrics, framework.PodStartShortTimeout)
378+
framework.ExpectNoError(err, "waiting for metrics %s to increase", t.expectIncreases)
379+
})
380+
}
381+
})
382+
})
383+
384+
func grabMetrics(ctx context.Context, grabber *e2emetrics.Grabber, nodeName string, metricNames sets.String) (map[string]float64, error) {
385+
response, err := grabber.GrabFromKubelet(ctx, nodeName)
386+
framework.ExpectNoError(err)
387+
388+
metrics := map[string]float64{}
389+
for method, samples := range response {
390+
if metricNames.Has(method) {
391+
if len(samples) == 0 {
392+
return nil, fmt.Errorf("metric %s has no samples", method)
393+
}
394+
lastSample := samples[len(samples)-1]
395+
metrics[method] = float64(lastSample.Value)
396+
}
397+
}
398+
399+
// Ensure all metrics were provided
400+
for name := range metricNames {
401+
if _, found := metrics[name]; !found {
402+
return nil, fmt.Errorf("metric %s not found", name)
403+
}
404+
}
405+
406+
return metrics, nil
407+
}
408+
409+
func waitForMetricIncrease(ctx context.Context, grabber *e2emetrics.Grabber, nodeName string, allMetricNames, expectedIncreaseNames sets.String, initialValues map[string]float64, timeout time.Duration) error {
410+
var noIncreaseMetrics sets.String
411+
var metrics map[string]float64
412+
413+
err := wait.Poll(time.Second, timeout, func() (bool, error) {
414+
var err error
415+
metrics, err = grabMetrics(ctx, grabber, nodeName, allMetricNames)
416+
if err != nil {
417+
return false, err
418+
}
419+
420+
noIncreaseMetrics = sets.NewString()
421+
// Always evaluate all SELinux metrics to check that the other metrics are not unexpectedly increased.
422+
for name := range allMetricNames {
423+
if expectedIncreaseNames.Has(name) {
424+
if metrics[name] <= initialValues[name] {
425+
noIncreaseMetrics.Insert(name)
426+
}
427+
} else {
428+
if initialValues[name] != metrics[name] {
429+
return false, fmt.Errorf("metric %s unexpectedly increased to %v", name, metrics[name])
430+
}
431+
}
432+
}
433+
return noIncreaseMetrics.Len() == 0, nil
434+
})
435+
436+
ginkgo.By("Dumping final metrics")
437+
dumpMetrics(metrics)
438+
439+
if err == context.DeadlineExceeded {
440+
return fmt.Errorf("timed out waiting for metrics %v", noIncreaseMetrics.List())
441+
}
442+
return err
443+
}
444+
445+
func dumpMetrics(metrics map[string]float64) {
446+
// Print the metrics sorted by metric name for better readability
447+
keys := make([]string, 0, len(metrics))
448+
for key := range metrics {
449+
keys = append(keys, key)
450+
}
451+
sort.Strings(keys)
452+
453+
for _, key := range keys {
454+
framework.Logf("Metric %s: %v", key, metrics[key])
455+
}
456+
}

0 commit comments

Comments
 (0)