diff --git a/pkg/controller/volume/selinuxwarning/cache/volumecache.go b/pkg/controller/volume/selinuxwarning/cache/volumecache.go index 4b19c985c86..dba6c5b93de 100644 --- a/pkg/controller/volume/selinuxwarning/cache/volumecache.go +++ b/pkg/controller/volume/selinuxwarning/cache/volumecache.go @@ -23,6 +23,7 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" + "k8s.io/kubernetes/pkg/controller/volume/selinuxwarning/internal/parse" "k8s.io/kubernetes/pkg/controller/volume/selinuxwarning/translator" ) @@ -81,6 +82,8 @@ type podInfo struct { // SELinux seLinuxLabel to be applied to the volume in the Pod. // Either as mount option or recursively by the container runtime. seLinuxLabel string + // Pre-parsed SELinux label parts for fast conflict detection. + seLinuxParts [4]string // SELinuxChangePolicy of the Pod. changePolicy v1.PodSELinuxChangePolicy } @@ -89,6 +92,7 @@ func newPodInfoListForPod(podKey cache.ObjectName, seLinuxLabel string, changePo return map[cache.ObjectName]podInfo{ podKey: { seLinuxLabel: seLinuxLabel, + seLinuxParts: parse.ParseSELinuxLabel(seLinuxLabel), changePolicy: changePolicy, }, } @@ -116,6 +120,7 @@ func (c *volumeCache) AddVolume(logger klog.Logger, volumeName v1.UniqueVolumeNa // The volume is already known podInfo := podInfo{ seLinuxLabel: label, + seLinuxParts: parse.ParseSELinuxLabel(label), changePolicy: changePolicy, } oldPodInfo, found := volume.pods[podKey] @@ -148,7 +153,7 @@ func (c *volumeCache) AddVolume(logger klog.Logger, volumeName v1.UniqueVolumeNa OtherPropertyValue: string(changePolicy), }) } - if c.seLinuxTranslator.Conflicts(otherPodInfo.seLinuxLabel, label) { + if c.seLinuxTranslator.ConflictsParsed(otherPodInfo.seLinuxParts, podInfo.seLinuxParts) { // Send conflict to both pods conflicts = append(conflicts, Conflict{ PropertyName: "SELinuxLabel", diff --git a/pkg/controller/volume/selinuxwarning/cache/volumecache_test.go b/pkg/controller/volume/selinuxwarning/cache/volumecache_test.go index 5bba301b692..71f916647b2 100644 --- a/pkg/controller/volume/selinuxwarning/cache/volumecache_test.go +++ b/pkg/controller/volume/selinuxwarning/cache/volumecache_test.go @@ -25,6 +25,7 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" "k8s.io/klog/v2/ktesting" + "k8s.io/kubernetes/pkg/controller/volume/selinuxwarning/internal/parse" "k8s.io/kubernetes/pkg/controller/volume/selinuxwarning/translator" ) @@ -436,6 +437,7 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) { } expectedPodInfo := podInfo{ seLinuxLabel: tt.podToAdd.label, + seLinuxParts: parse.ParseSELinuxLabel(tt.podToAdd.label), changePolicy: tt.podToAdd.changePolicy, } if !reflect.DeepEqual(existingInfo, expectedPodInfo) { diff --git a/pkg/controller/volume/selinuxwarning/internal/parse/selinux_label.go b/pkg/controller/volume/selinuxwarning/internal/parse/selinux_label.go new file mode 100644 index 00000000000..0fd48ed8b67 --- /dev/null +++ b/pkg/controller/volume/selinuxwarning/internal/parse/selinux_label.go @@ -0,0 +1,32 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parse + +import "strings" + +// ParseSELinuxLabel parses a SELinux label string into its components. +// Format: "user:role:type:level" -> [user, role, type, level] +// Missing components are represented as empty strings. +func ParseSELinuxLabel(label string) [4]string { + var parts [4]string + if label == "" { + return parts + } + split := strings.SplitN(label, ":", 4) + copy(parts[:], split) + return parts +} diff --git a/pkg/controller/volume/selinuxwarning/internal/parse/selinux_label_test.go b/pkg/controller/volume/selinuxwarning/internal/parse/selinux_label_test.go new file mode 100644 index 00000000000..e82feed748f --- /dev/null +++ b/pkg/controller/volume/selinuxwarning/internal/parse/selinux_label_test.go @@ -0,0 +1,106 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parse + +import ( + "reflect" + "testing" +) + +func TestParseSELinuxLabel(t *testing.T) { + tests := []struct { + name string + label string + expectedParts []string + }{ + { + name: "empty label", + label: "", + expectedParts: []string{"", "", "", ""}, + }, + { + name: "complete label with all components", + label: "system_u:system_r:container_t:s0:c0,c1", + expectedParts: []string{"system_u", "system_r", "container_t", "s0:c0,c1"}, + }, + { + name: "label with user, role, and type only", + label: "system_u:system_r:container_t", + expectedParts: []string{"system_u", "system_r", "container_t", ""}, + }, + { + name: "label with user and role only", + label: "system_u:system_r", + expectedParts: []string{"system_u", "system_r", "", ""}, + }, + { + name: "label with user only", + label: "system_u", + expectedParts: []string{"system_u", "", "", ""}, + }, + { + name: "label missing user but with role and type", + label: ":system_r:container_t", + expectedParts: []string{"", "system_r", "container_t", ""}, + }, + { + name: "label missing user and role but with type", + label: "::container_t", + expectedParts: []string{"", "", "container_t", ""}, + }, + { + name: "label missing user and role but with type and level", + label: "::container_t:s0", + expectedParts: []string{"", "", "container_t", "s0"}, + }, + { + name: "label with all empty components except level", + label: ":::s0:c0,c1", + expectedParts: []string{"", "", "", "s0:c0,c1"}, + }, + { + name: "label with special characters in components", + label: "user_with_underscore:role-with-dash:type.with.dots:s0:c0.c1", + expectedParts: []string{"user_with_underscore", "role-with-dash", "type.with.dots", "s0:c0.c1"}, + }, + { + name: "label with extra colons in level component", + label: "user:role:type:s0:c0,c1:extra", + expectedParts: []string{"user", "role", "type", "s0:c0,c1:extra"}, + }, + { + name: "multiple colons only", + label: ":::", + expectedParts: []string{"", "", "", ""}, + }, + { + name: "five colons", + label: ":::::", + expectedParts: []string{"", "", "", "::"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parts := ParseSELinuxLabel(tt.label) + partsSlice := parts[:] + if !reflect.DeepEqual(partsSlice, tt.expectedParts) { + t.Errorf("ParseSELinuxLabel(%q) = %v, expected parts = %v", tt.label, partsSlice, tt.expectedParts) + } + }) + } +} diff --git a/pkg/controller/volume/selinuxwarning/translator/selinux_translator.go b/pkg/controller/volume/selinuxwarning/translator/selinux_translator.go index 99ce3e97dd7..db599c98cd7 100644 --- a/pkg/controller/volume/selinuxwarning/translator/selinux_translator.go +++ b/pkg/controller/volume/selinuxwarning/translator/selinux_translator.go @@ -20,6 +20,7 @@ import ( "strings" v1 "k8s.io/api/core/v1" + "k8s.io/kubernetes/pkg/controller/volume/selinuxwarning/internal/parse" "k8s.io/kubernetes/pkg/volume/util" ) @@ -70,18 +71,16 @@ func (c *ControllerSELinuxTranslator) SELinuxOptionsToFileLabel(opts *v1.SELinux // However: "system_u:system_r:container_t:s0:c1,c2" *does* conflict with ":::s0:c98,c99". // And ":::s0:c1,c2" *does* conflict with "" or ":::", because it's never defaulted by the OS. func (c *ControllerSELinuxTranslator) Conflicts(labelA, labelB string) bool { - partsA := strings.SplitN(labelA, ":", 4) - partsB := strings.SplitN(labelB, ":", 4) + return c.ConflictsParsed(parse.ParseSELinuxLabel(labelA), parse.ParseSELinuxLabel(labelB)) +} - // Reorder, so partsA is always longer than partsB - if len(partsA) < len(partsB) { - partsB, partsA = partsA, partsB - } - - for len(partsB) < len(partsA) { - partsB = append(partsB, "") - } - for i := range partsA { +// ConflictsParsed returns true if two pre-parsed SELinux labels conflict. +// This is an optimized version of Conflicts() that operates on pre-split labels +// to avoid repeated string allocations in hot paths (e.g., metrics collection). +// partsA and partsB must be 4-element arrays in the format: [user, role, type, level] +func (c *ControllerSELinuxTranslator) ConflictsParsed(partsA, partsB [4]string) bool { + // Compare each component + for i := range 4 { if partsA[i] == partsB[i] { continue }