Skip to content

Commit 17ecba1

Browse files
committed
Improve RWX volumes with VM service VMs performace during validation in webhook.
1 parent 61f0a34 commit 17ecba1

File tree

4 files changed

+237
-17
lines changed

4 files changed

+237
-17
lines changed

pkg/syncer/admissionhandler/cnscsi_admissionhandler.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ const (
3737
MutationWebhookPath = "/mutate"
3838
DefaultWebhookPort = 9883
3939
DefaultWebhookMetricsBindAddress = "0"
40-
devopsUserLabelKey = "cns.vmware.com/user-created"
40+
vmNameLabelKey = "cns.vmware.com/vm-name"
41+
pvcNameLabelKey = "cns.vmware.com/pvc-name"
4142
)
4243

4344
var (
@@ -268,11 +269,13 @@ func (h *CSISupervisorMutationWebhook) mutateNewCnsFileAccessConfig(ctx context.
268269
if newCnsFileAccessConfig.Labels == nil {
269270
newCnsFileAccessConfig.Labels = make(map[string]string)
270271
}
271-
if _, ok := newCnsFileAccessConfig.Labels[devopsUserLabelKey]; ok {
272-
log.Debugf("Devops label already present on instance %s", newCnsFileAccessConfig.Name)
273-
return admission.Allowed("")
274-
}
275-
newCnsFileAccessConfig.Labels[devopsUserLabelKey] = "true"
272+
273+
// Add VM name and PVC name label.
274+
// If someone created this CR with these labels already present, CSI will overrite on them
275+
// with the correct values.
276+
newCnsFileAccessConfig.Labels[vmNameLabelKey] = newCnsFileAccessConfig.Spec.VMName
277+
newCnsFileAccessConfig.Labels[pvcNameLabelKey] = newCnsFileAccessConfig.Spec.PvcName
278+
276279
newRawCnsFileAccessConfig, err := json.Marshal(newCnsFileAccessConfig)
277280
if err != nil {
278281
return admission.Errored(http.StatusInternalServerError, err)

pkg/syncer/admissionhandler/validate_cnsfileaccessconfig.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"regexp"
88

99
admissionv1 "k8s.io/api/admission/v1"
10+
1011
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/labels"
1113
"sigs.k8s.io/controller-runtime/pkg/client"
1214
cnsoperatorv1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator"
1315

@@ -42,6 +44,25 @@ func validateCreateCnsFileAccessConfig(ctx context.Context, clientConfig *rest.C
4244
}
4345
}
4446

47+
// This validation is not required for PVCSI service account.
48+
isPvCSIServiceAccount, err := validatePvCSIServiceAccount(req.UserInfo.Username)
49+
if err != nil {
50+
// return AdmissionResponse result
51+
return &admissionv1.AdmissionResponse{
52+
Allowed: false,
53+
Result: &metav1.Status{
54+
Message: fmt.Sprintf("failed to validate user information: %v", err),
55+
},
56+
}
57+
}
58+
59+
// If user is PVCSI service account, allow this request.
60+
if isPvCSIServiceAccount {
61+
return &admissionv1.AdmissionResponse{
62+
Allowed: true,
63+
}
64+
}
65+
4566
vm := cnsFileAccessConfig.Spec.VMName
4667
pvc := cnsFileAccessConfig.Spec.PvcName
4768
namespace := cnsFileAccessConfig.Namespace
@@ -87,22 +108,31 @@ func cnsFileAccessConfigAlreadyExists(ctx context.Context, clientConfig *rest.Co
87108
return "", err
88109
}
89110

111+
// List only that CnsFileAccessConfig CRs which has the same VM name and PVC name labels.
112+
labelSelector := labels.SelectorFromSet(labels.Set{vmNameLabelKey: vm, pvcNameLabelKey: pvc})
90113
// Get the list of all CnsFileAccessConfig CRs in the given namespace.
91114
cnsFileAccessConfigList := &cnsfileaccessconfigv1alpha1.CnsFileAccessConfigList{}
92-
err = cnsOperatorClient.List(ctx, cnsFileAccessConfigList, &client.ListOptions{Namespace: namespace})
115+
err = cnsOperatorClient.List(ctx, cnsFileAccessConfigList, &client.ListOptions{
116+
Namespace: namespace,
117+
LabelSelector: labelSelector,
118+
})
93119
if err != nil {
94120
log.Errorf("failed to list CnsFileAccessConfigList CRs from %s namesapace. Error: %+v",
95121
namespace, err)
96122
return "", err
97123
}
98124

99-
for _, cnsFileAccessConfig := range cnsFileAccessConfigList.Items {
100-
if cnsFileAccessConfig.Spec.VMName == vm {
101-
if cnsFileAccessConfig.Spec.PvcName == pvc {
102-
return cnsFileAccessConfig.Name, nil
103-
}
104-
}
125+
if len(cnsFileAccessConfigList.Items) == 1 {
126+
// There should be only 1 CFC CR with the same VM and PVC
127+
return cnsFileAccessConfigList.Items[0].Name, nil
128+
}
129+
130+
if len(cnsFileAccessConfigList.Items) > 1 {
131+
// We should never reach here but it's good to have this check.
132+
return "", fmt.Errorf("invalid case, %d CnsFileAccessConfig instances detected "+
133+
"with the VM %s and PVC %s", len(cnsFileAccessConfigList.Items), vm, pvc)
105134
}
135+
106136
return "", nil
107137
}
108138

@@ -125,10 +155,10 @@ func validateDeleteCnsFileAccessConfig(ctx context.Context, clientConfig *rest.C
125155
},
126156
}
127157
}
128-
// If CR has devops user label, allow this request as
158+
// If CR has VM name label, allow this request as
129159
// it means that it is created by devops user or K8s admin
130160
// and not by VKS (CSI service account).
131-
if _, ok := cnsFileAccessConfig.Labels[devopsUserLabelKey]; ok {
161+
if _, ok := cnsFileAccessConfig.Labels[vmNameLabelKey]; ok {
132162
log.Infof("CnsFileAccessConfig %s has devops user label. Allow this reqeust.",
133163
cnsFileAccessConfig.Name)
134164
return &admissionv1.AdmissionResponse{

pkg/syncer/cnsoperator/controller/cnsfileaccessconfig/cnsfileaccessconfig_controller.go

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2021 The Kubernetes Authors.
2+
Copyright 2021-2025 The Kubernetes Authors.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -18,8 +18,10 @@ package cnsfileaccessconfig
1818

1919
import (
2020
"context"
21+
"errors"
2122
"fmt"
2223
"reflect"
24+
"strings"
2325
"sync"
2426
"time"
2527

@@ -60,6 +62,10 @@ import (
6062

6163
const (
6264
defaultMaxWorkerThreadsForFileAccessConfig = 10
65+
vmNameLabelKey = "cns.vmware.com/vm-name"
66+
pvcNameLabelKey = "cns.vmware.com/pvc-name"
67+
capvLabelKey = "capv.vmware.com"
68+
TKGServiceLabel = "TKGService"
6369
)
6470

6571
// backOffDuration is a map of cnsfileaccessconfig name's to the time after
@@ -211,6 +217,69 @@ type ReconcileCnsFileAccessConfig struct {
211217
recorder record.EventRecorder
212218
}
213219

220+
// validateVmAndPvc validates if the VM and PVC combination given in the instance is correct or not.
221+
// CnsFileAccessConfig CRs created by devpos users will have "cns.vmware.com/vm-name" and "cns.vmware.com/pvc-name" labels.
222+
// When these labels are present, the PVC and VM must not belong to TKG cluster.
223+
// This is verified by ensuring that:
224+
// The VM does not have a label applied by CAPV - example capv.vmware.com/cluster.name.
225+
// The PVC does not have a label applued by CAPV - example <TKG cluster namespace>/TKGService
226+
func validateVmAndPvc(ctx context.Context, instanceLabels map[string]string, instanceName string, pvcName string, namespace string, client client.Client, vm *vmoperatorv1alpha4.VirtualMachine) error {
227+
log := logger.GetLogger(ctx)
228+
229+
if instanceLabels == nil {
230+
log.Infof("No labels found on the instance %s. Nothing to validate.", instanceName)
231+
return nil
232+
}
233+
234+
isUserCreatedCnsFileAccessConfig := false
235+
236+
for key := range instanceLabels {
237+
if key == vmNameLabelKey || key == pvcNameLabelKey {
238+
isUserCreatedCnsFileAccessConfig = true
239+
break
240+
}
241+
}
242+
243+
if !isUserCreatedCnsFileAccessConfig {
244+
log.Infof("Instance %s is not created by devops user. Nothing to validate", instanceName)
245+
return nil
246+
}
247+
248+
vmLabels := vm.Labels
249+
if vmLabels != nil {
250+
for key := range vmLabels {
251+
if strings.Contains(key, capvLabelKey) {
252+
msg := fmt.Sprintf("CnsFileAccessConfig is created by devops user and has TKG VM %s. Invalid combination.", vm.Name)
253+
log.Errorf(msg)
254+
err := errors.New(msg)
255+
return err
256+
}
257+
}
258+
}
259+
260+
pvc := &v1.PersistentVolumeClaim{}
261+
err := client.Get(ctx, types.NamespacedName{Name: pvcName, Namespace: namespace}, pvc)
262+
if err != nil {
263+
log.Errorf("failed to get PVC with name %s in namespace %s", pvcName, namespace)
264+
return err
265+
}
266+
267+
pvcLabels := pvc.Labels
268+
if pvcLabels != nil {
269+
for key := range pvcLabels {
270+
if strings.Contains(key, TKGServiceLabel) {
271+
msg := fmt.Sprintf("CnsFileAccessConfig is created by devops user and has TKG PVC %s. Invalid combination.", pvcName)
272+
log.Errorf(msg)
273+
err := errors.New(msg)
274+
return err
275+
}
276+
}
277+
}
278+
279+
log.Infof("Successfully verified instance %s for VM/PVC combination", instanceName)
280+
return nil
281+
}
282+
214283
// Reconcile reads that cluster state for a CnsFileAccessConfig object and makes
215284
// changes based on the state read and what is in the CnsFileAccessConfig.Spec.
216285
// Note:
@@ -310,12 +379,24 @@ func (r *ReconcileCnsFileAccessConfig) Reconcile(ctx context.Context,
310379
}
311380
log.Debugf("Found virtualMachine instance for VM: %q/%q: %+v", instance.Namespace, instance.Spec.VMName, vm)
312381

382+
ifFileVolumesWithVmserviceVmsSupported := commonco.ContainerOrchestratorUtility.IsFSSEnabled(ctx, common.FileVolumesWithVmService)
383+
384+
if ifFileVolumesWithVmserviceVmsSupported {
385+
err = validateVmAndPvc(ctx, instance.Labels, instance.Name, instance.Spec.PvcName,
386+
instance.Namespace, r.client, vm)
387+
if err != nil {
388+
log.Errorf("failed to validate VM %s and PVC %s", vm.Name, instance.Spec.PvcName)
389+
setInstanceError(ctx, r, instance, err.Error())
390+
return reconcile.Result{RequeueAfter: timeout}, nil
391+
}
392+
}
393+
313394
if instance.DeletionTimestamp != nil {
314395
log.Infof("CnsFileAccessConfig instance %q has deletion timestamp set", instance.Name)
315396
volumeExists := true
316397
volumeID, err := cnsoperatorutil.GetVolumeID(ctx, r.client, instance.Spec.PvcName, instance.Namespace)
317398
if err != nil {
318-
if commonco.ContainerOrchestratorUtility.IsFSSEnabled(ctx, common.FileVolumesWithVmService) &&
399+
if ifFileVolumesWithVmserviceVmsSupported &&
319400
apierrors.IsNotFound(err) {
320401
log.Infof("Volume ID for PVC %s in namespace %s not found. No need to configure ACL",
321402
instance.Spec.PvcName, instance.Namespace)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package cnsfileaccessconfig
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
v1 "k8s.io/api/core/v1"
8+
"k8s.io/apimachinery/pkg/runtime"
9+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
10+
11+
vmoperatorv1alpha4 "github.com/vmware-tanzu/vm-operator/api/v1alpha4"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
)
14+
15+
func TestValidateVmAndPvc(t *testing.T) {
16+
scheme := runtime.NewScheme()
17+
_ = v1.AddToScheme(scheme)
18+
_ = vmoperatorv1alpha4.AddToScheme(scheme)
19+
20+
ctx := context.TODO()
21+
22+
tests := []struct {
23+
name string
24+
instanceLabels map[string]string
25+
vmLabels map[string]string
26+
pvcLabels map[string]string
27+
expectError bool
28+
setupPVC bool
29+
}{
30+
{
31+
name: "no instance labels",
32+
instanceLabels: nil,
33+
expectError: false,
34+
},
35+
{
36+
name: "labels but not devops user",
37+
instanceLabels: map[string]string{
38+
"random": "value",
39+
},
40+
expectError: false,
41+
},
42+
{
43+
name: "devops user with capv label on VM",
44+
instanceLabels: map[string]string{
45+
vmNameLabelKey: "my-vm",
46+
},
47+
vmLabels: map[string]string{
48+
"capv.vmware.com/cluster": "my-cluster",
49+
},
50+
expectError: true,
51+
},
52+
{
53+
name: "devops user with TKG label on PVC",
54+
instanceLabels: map[string]string{
55+
pvcNameLabelKey: "my-pvc",
56+
},
57+
setupPVC: true,
58+
pvcLabels: map[string]string{
59+
TKGServiceLabel: "tkg",
60+
},
61+
expectError: true,
62+
},
63+
{
64+
name: "valid input - no errors",
65+
instanceLabels: map[string]string{
66+
vmNameLabelKey: "vm",
67+
},
68+
setupPVC: true,
69+
expectError: false,
70+
},
71+
}
72+
73+
for _, test := range tests {
74+
t.Run(test.name, func(t *testing.T) {
75+
var objects []runtime.Object
76+
77+
vm := &vmoperatorv1alpha4.VirtualMachine{
78+
ObjectMeta: metav1.ObjectMeta{
79+
Name: "my-vm",
80+
Labels: test.vmLabels,
81+
},
82+
}
83+
84+
if test.setupPVC {
85+
pvc := &v1.PersistentVolumeClaim{
86+
ObjectMeta: metav1.ObjectMeta{
87+
Name: "my-pvc",
88+
Namespace: "default",
89+
Labels: test.pvcLabels,
90+
},
91+
}
92+
objects = append(objects, pvc)
93+
}
94+
95+
client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
96+
97+
err := validateVmAndPvc(ctx, test.instanceLabels, "my-instance", "my-pvc", "default", client, vm)
98+
99+
if test.expectError && err == nil {
100+
t.Errorf("expected error but got nil")
101+
} else if !test.expectError && err != nil {
102+
t.Errorf("unexpected error: %v", err)
103+
}
104+
})
105+
}
106+
}

0 commit comments

Comments
 (0)