@@ -18,16 +18,22 @@ package csi_mock
1818
1919import (
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