controller/selinuxwarning: Pre-parse SELinux label

When calling ControllerSELinuxTranslator.Conflicts(), the SELinux label
is repeatedly split into []string to detect conflicts. This causes a huge
number of allocations when there are many comparisons.

This is now made more efficient by pre-parsing the SELinux label and
storing it in podInfo as [4]string for fast comparison when needed.
This commit is contained in:
Ondra Kupka
2026-02-24 11:53:18 +01:00
committed by Hemant Kumar
parent 90640ec14a
commit 9f52f4d26c
5 changed files with 156 additions and 12 deletions

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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
}