Add readWriteOncePod backupPVC config to enable mount-level SELinux labeling (#10339)
e2e-test-kind.yaml / extract (push) Failing after 10s
Run the E2E test on kind / get-go-version (push) Failing after 11s
Run the E2E test on kind / build (push) Skipped
Run the E2E test on kind / setup-test-matrix (push) Successful in 3s
Run the E2E test on kind / run-e2e-test (push) Skipped
push.yml / extract (push) Failing after 7s
Main CI / get-go-version (push) Failing after 8s
Main CI / Build (push) Skipped

* Add readWriteOncePod backupPVC config to enable mount-level SELinux labeling

On SELinux-enabled clusters the kubelet recursively relabels every file of
the backupPVC at mount time, which can take hours on volumes with a high
file count. Kubernetes avoids this when the volume is ReadWriteOncePod and
the CSI driver advertises SELinux mount support, by mounting with
-o context= instead.

Add an opt-in per-storage-class 'readWriteOncePod' backupPVC config option
that creates the backupPVC with the ReadWriteOncePod access mode and sets
the backup pod's SecurityContext.SELinuxChangePolicy to MountOption. It is
mutually exclusive with 'readOnly', which takes precedence.

Fixes #9873

Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com>

* Add changelog file

Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com>

* Do not set SELinuxChangePolicy for the backup pod

Live testing on OCP 4.22 (k8s 1.35) showed that setting
SecurityContext.SELinuxChangePolicy to MountOption makes backup pod
creation fail outright when the SELinuxMount feature gate is disabled,
which is the default on current clusters:

  Pod is invalid: spec.securityContext.seLinuxChangePolicy:
  Unsupported value: "MountOption": supported values: "Recursive"

The field is also unnecessary. For ReadWriteOncePod volumes the kubelet
already performs mount-level SELinux labeling via the
SELinuxMountReadWriteOncePod feature gate, which has been on by default
since k8s 1.28. Setting the backupPVC access mode to ReadWriteOncePod is
sufficient on its own, and is portable to clusters where the broader
SELinuxMount gate is still off.

Verified on-cluster that the backupPVC is mounted with
context="system_u:object_r:container_file_t:s0:c22,c28" instead of the
recursive seclabel mount used without the flag.

Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com>

---------

Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com>
This commit is contained in:
Shubham Pampattiwar
2026-08-25 08:32:47 -07:00
committed by GitHub
parent f6e6e554bc
commit 63fbf20cc5
6 changed files with 188 additions and 3 deletions
+13 -2
View File
@@ -246,6 +246,7 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O
backupPVCStorageClass := csiExposeParam.StorageClass
backupPVCReadOnly := false
spcNoRelabeling := false
backupPVCReadWriteOncePod := false
backupPVCAnnotations := map[string]string{}
intoleratableNodes := []string{}
if value, exists := csiExposeParam.BackupPVCConfig[csiExposeParam.StorageClass]; exists {
@@ -262,6 +263,14 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O
}
}
if value.ReadWriteOncePod {
if backupPVCReadOnly {
curLog.WithField("vs name", volumeSnapshot.Name).Warn("Ignoring readWriteOncePod for read-only volume")
} else {
backupPVCReadWriteOncePod = true
}
}
if len(value.Annotations) > 0 {
backupPVCAnnotations = value.Annotations
}
@@ -276,7 +285,7 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O
}
}
backupPVC, err := e.createBackupPVC(ctx, ownerObject, backupVS.Name, backupPVCStorageClass, csiExposeParam.AccessMode, volumeSize, backupPVCReadOnly, backupPVCAnnotations, csiExposeParam.DataMover)
backupPVC, err := e.createBackupPVC(ctx, ownerObject, backupVS.Name, backupPVCStorageClass, csiExposeParam.AccessMode, volumeSize, backupPVCReadOnly, backupPVCReadWriteOncePod, backupPVCAnnotations, csiExposeParam.DataMover)
if err != nil {
return errors.Wrap(err, "error to create backup pvc")
}
@@ -632,7 +641,7 @@ func (e *csiSnapshotExposer) createBackupVSC(ctx context.Context, ownerObject co
return e.csiSnapshotClient.VolumeSnapshotContents().Create(ctx, vsc, metav1.CreateOptions{})
}
func (e *csiSnapshotExposer) createBackupPVC(ctx context.Context, ownerObject corev1api.ObjectReference, backupVS, storageClass, accessMode string, resource resource.Quantity, readOnly bool, annotations map[string]string, dataMover string) (*corev1api.PersistentVolumeClaim, error) {
func (e *csiSnapshotExposer) createBackupPVC(ctx context.Context, ownerObject corev1api.ObjectReference, backupVS, storageClass, accessMode string, resource resource.Quantity, readOnly bool, readWriteOncePod bool, annotations map[string]string, dataMover string) (*corev1api.PersistentVolumeClaim, error) {
backupPVCName := ownerObject.Name
volumeMode, err := getVolumeModeByAccessMode(accessMode, dataMover)
@@ -644,6 +653,8 @@ func (e *csiSnapshotExposer) createBackupPVC(ctx context.Context, ownerObject co
if readOnly {
pvcAccessMode = corev1api.ReadOnlyMany
} else if readWriteOncePod {
pvcAccessMode = corev1api.ReadWriteOncePod
}
dataSource := &corev1api.TypedLocalObjectReference{
+153 -1
View File
@@ -219,6 +219,7 @@ func TestExpose(t *testing.T) {
err string
expectedVolumeSize *resource.Quantity
expectedReadOnlyPVC bool
expectedRWOPPVC bool
expectedBackupPVCStorageClass string
expectedAffinity *corev1api.Affinity
expectedPVCAnnotation map[string]string
@@ -672,6 +673,95 @@ func TestExpose(t *testing.T) {
},
},
},
{
name: "backupPVC uses ReadWriteOncePod access mode",
ownerBackup: backup,
exposeParam: CSISnapshotExposeParam{
SnapshotName: "fake-vs",
SourceNamespace: "fake-ns",
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
AccessMode: AccessModeFileSystem,
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
BackupPVCConfig: map[string]velerotypes.BackupPVC{
"fake-sc": {
ReadWriteOncePod: true,
},
},
},
snapshotClientObj: []runtime.Object{
vsObject,
vscObj,
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedRWOPPVC: true,
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: corev1api.LabelOSStable,
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
},
},
},
},
{
name: "readOnly takes precedence over readWriteOncePod",
ownerBackup: backup,
exposeParam: CSISnapshotExposeParam{
SnapshotName: "fake-vs",
SourceNamespace: "fake-ns",
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
AccessMode: AccessModeFileSystem,
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
BackupPVCConfig: map[string]velerotypes.BackupPVC{
"fake-sc": {
ReadOnly: true,
ReadWriteOncePod: true,
},
},
},
snapshotClientObj: []runtime.Object{
vsObject,
vscObj,
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedReadOnlyPVC: true,
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: corev1api.LabelOSStable,
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
},
},
},
},
{
name: "backupPod mounts backupPVC with storageClass specified in backupPVC config",
ownerBackup: backup,
@@ -1150,6 +1240,12 @@ func TestExpose(t *testing.T) {
assert.Equal(t, test.expectedReadOnlyPVC, gotReadOnlyAccessMode)
}
if test.expectedRWOPPVC {
assert.Equal(t, []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOncePod}, backupPVC.Spec.AccessModes)
} else {
assert.NotContains(t, backupPVC.Spec.AccessModes, corev1api.ReadWriteOncePod)
}
if test.expectedBackupPVCStorageClass != "" {
assert.Equal(t, test.expectedBackupPVCStorageClass, *backupPVC.Spec.StorageClassName)
}
@@ -1521,6 +1617,37 @@ func Test_csiSnapshotExposer_createBackupPVC(t *testing.T) {
},
}
backupPVCReadWriteOncePod := corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1.DefaultNamespace,
Name: "fake-backup",
Annotations: map[string]string{},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: backup.APIVersion,
Kind: backup.Kind,
Name: backup.Name,
UID: backup.UID,
Controller: ptr.To(true),
},
},
},
Spec: corev1api.PersistentVolumeClaimSpec{
AccessModes: []corev1api.PersistentVolumeAccessMode{
corev1api.ReadWriteOncePod,
},
VolumeMode: &volumeMode,
DataSource: dataSource,
DataSourceRef: nil,
StorageClassName: ptr.To("fake-storage-class"),
Resources: corev1api.VolumeResourceRequirements{
Requests: corev1api.ResourceList{
corev1api.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
}
tests := []struct {
name string
ownerBackup *velerov1.Backup
@@ -1529,6 +1656,7 @@ func Test_csiSnapshotExposer_createBackupPVC(t *testing.T) {
accessMode string
resource resource.Quantity
readOnly bool
readWriteOncePod bool
kubeClientObj []runtime.Object
snapshotClientObj []runtime.Object
want *corev1api.PersistentVolumeClaim
@@ -1556,6 +1684,30 @@ func Test_csiSnapshotExposer_createBackupPVC(t *testing.T) {
want: &backupPVCReadOnly,
wantErr: assert.NoError,
},
{
name: "backupPVC gets created with ReadWriteOncePod access mode when readWriteOncePod is set",
ownerBackup: backup,
backupVS: "fake-snapshot",
storageClass: "fake-storage-class",
accessMode: AccessModeFileSystem,
resource: resource.MustParse("1Gi"),
readOnly: false,
readWriteOncePod: true,
want: &backupPVCReadWriteOncePod,
wantErr: assert.NoError,
},
{
name: "readOnly takes precedence over readWriteOncePod",
ownerBackup: backup,
backupVS: "fake-snapshot",
storageClass: "fake-storage-class",
accessMode: AccessModeFileSystem,
resource: resource.MustParse("1Gi"),
readOnly: true,
readWriteOncePod: true,
want: &backupPVCReadOnly,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -1576,7 +1728,7 @@ func Test_csiSnapshotExposer_createBackupPVC(t *testing.T) {
APIVersion: tt.ownerBackup.APIVersion,
}
}
got, err := e.createBackupPVC(t.Context(), ownerObject, tt.backupVS, tt.storageClass, tt.accessMode, tt.resource, tt.readOnly, map[string]string{}, "")
got, err := e.createBackupPVC(t.Context(), ownerObject, tt.backupVS, tt.storageClass, tt.accessMode, tt.resource, tt.readOnly, tt.readWriteOncePod, map[string]string{}, "")
if !tt.wantErr(t, err, fmt.Sprintf("createBackupPVC(%v, %v, %v, %v, %v, %v)", ownerObject, tt.backupVS, tt.storageClass, tt.accessMode, tt.resource, tt.readOnly)) {
return
}
+6
View File
@@ -57,6 +57,12 @@ type BackupPVC struct {
// ignored if ReadOnly is false
SPCNoRelabeling bool `json:"spcNoRelabeling,omitempty"`
// ReadWriteOncePod sets the backupPVC's access mode to ReadWriteOncePod so the kubelet can use
// mount-level SELinux labeling (-o context) instead of per-file relabeling, when the CSI driver
// advertises SELinux mount support.
// ignored if ReadOnly is true
ReadWriteOncePod bool `json:"readWriteOncePod,omitempty"`
// Annotations permits setting annotations for the backupPVC
Annotations map[string]string `json:"annotations,omitempty"`