Skip to content

Commit 48a2c70

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

File tree

4 files changed

+224
-13
lines changed

4 files changed

+224
-13
lines changed

pkg/syncer/admissionhandler/cnscsi_admissionhandler.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const (
3838
DefaultWebhookPort = 9883
3939
DefaultWebhookMetricsBindAddress = "0"
4040
devopsUserLabelKey = "cns.vmware.com/user-created"
41+
vmNameLabelKey = "cns.vmware.com/vm-name"
42+
pvcNameLabelKey = "cns.vmware.com/pvc-name"
4143
)
4244

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

pkg/syncer/admissionhandler/validate_cnsfileaccessconfig.go

Lines changed: 37 additions & 7 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

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,11 @@ 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"
69+
devopsUserLabelKey = "cns.vmware.com/user-created"
6370
)
6471

6572
// backOffDuration is a map of cnsfileaccessconfig name's to the time after
@@ -310,12 +317,25 @@ func (r *ReconcileCnsFileAccessConfig) Reconcile(ctx context.Context,
310317
}
311318
log.Debugf("Found virtualMachine instance for VM: %q/%q: %+v", instance.Namespace, instance.Spec.VMName, vm)
312319

320+
ifFileVolumesWithVmserviceVmsSupported := commonco.ContainerOrchestratorUtility.IsFSSEnabled(ctx,
321+
common.FileVolumesWithVmService)
322+
323+
if ifFileVolumesWithVmserviceVmsSupported {
324+
err = validateVmAndPvc(ctx, instance.Labels, instance.Name, instance.Spec.PvcName,
325+
instance.Namespace, r.client, vm)
326+
if err != nil {
327+
log.Errorf("failed to validate VM %s and PVC %s", vm.Name, instance.Spec.PvcName)
328+
setInstanceError(ctx, r, instance, err.Error())
329+
return reconcile.Result{RequeueAfter: timeout}, nil
330+
}
331+
}
332+
313333
if instance.DeletionTimestamp != nil {
314334
log.Infof("CnsFileAccessConfig instance %q has deletion timestamp set", instance.Name)
315335
volumeExists := true
316336
volumeID, err := cnsoperatorutil.GetVolumeID(ctx, r.client, instance.Spec.PvcName, instance.Namespace)
317337
if err != nil {
318-
if commonco.ContainerOrchestratorUtility.IsFSSEnabled(ctx, common.FileVolumesWithVmService) &&
338+
if ifFileVolumesWithVmserviceVmsSupported &&
319339
apierrors.IsNotFound(err) {
320340
log.Infof("Volume ID for PVC %s in namespace %s not found. No need to configure ACL",
321341
instance.Spec.PvcName, instance.Namespace)
@@ -801,6 +821,67 @@ func (r *ReconcileCnsFileAccessConfig) getVMExternalIP(ctx context.Context,
801821
return tkgVMIP, nil
802822
}
803823

824+
// validateVmAndPvc validates if the VM and PVC combination given in the instance is correct or not.
825+
// CnsFileAccessConfig CRs created by devpos users will have "cns.vmware.com/vm-name"
826+
// and "cns.vmware.com/pvc-name" labels.
827+
// When these labels are present, the PVC and VM must not belong to TKG cluster.
828+
// This is verified by ensuring that:
829+
// The VM does not have a label applied by CAPV - example capv.vmware.com/cluster.name.
830+
// The PVC does not have a label applued by CAPV - example <TKG cluster namespace>/TKGService
831+
func validateVmAndPvc(ctx context.Context, instanceLabels map[string]string, instanceName string, pvcName string,
832+
namespace string, client client.Client, vm *vmoperatorv1alpha4.VirtualMachine) error {
833+
log := logger.GetLogger(ctx)
834+
835+
if instanceLabels == nil {
836+
log.Infof("No labels found on the instance %s. Nothing to validate.", instanceName)
837+
return nil
838+
}
839+
840+
isUserCreatedCnsFileAccessConfig := false
841+
842+
for key := range instanceLabels {
843+
if key == devopsUserLabelKey {
844+
isUserCreatedCnsFileAccessConfig = true
845+
break
846+
}
847+
}
848+
849+
if !isUserCreatedCnsFileAccessConfig {
850+
log.Infof("Instance %s is not created by devops user. Nothing to validate", instanceName)
851+
return nil
852+
}
853+
854+
for key := range vm.Labels {
855+
if strings.Contains(key, capvLabelKey) {
856+
msg := fmt.Sprintf("CnsFileAccessConfig is created by devops user and has TKG VM %s. "+
857+
"Invalid combination.", vm.Name)
858+
log.Errorf(msg)
859+
err := errors.New(msg)
860+
return err
861+
}
862+
}
863+
864+
pvc := &v1.PersistentVolumeClaim{}
865+
err := client.Get(ctx, types.NamespacedName{Name: pvcName, Namespace: namespace}, pvc)
866+
if err != nil {
867+
log.Errorf("failed to get PVC with name %s in namespace %s", pvcName, namespace)
868+
return err
869+
}
870+
871+
for key := range pvc.Labels {
872+
if strings.Contains(key, TKGServiceLabel) {
873+
msg := fmt.Sprintf("CnsFileAccessConfig is created by devops user and has "+
874+
"TKG PVC %s. Invalid combination.", pvcName)
875+
log.Errorf(msg)
876+
err := errors.New(msg)
877+
return err
878+
}
879+
}
880+
881+
log.Infof("Successfully verified instance %s for VM/PVC combination", instanceName)
882+
return nil
883+
}
884+
804885
// setInstanceSuccess sets instance to success and records an event on the
805886
// CnsFileAccessConfig instance.
806887
func setInstanceSuccess(ctx context.Context, r *ReconcileCnsFileAccessConfig,
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
devopsUserLabelKey: "true",
46+
},
47+
vmLabels: map[string]string{
48+
"capv.vmware.com/cluster": "my-cluster",
49+
},
50+
expectError: true,
51+
},
52+
{
53+
name: "valid input - no errors",
54+
instanceLabels: map[string]string{
55+
devopsUserLabelKey: "true",
56+
},
57+
setupPVC: true,
58+
expectError: false,
59+
},
60+
}
61+
62+
for _, test := range tests {
63+
t.Run(test.name, func(t *testing.T) {
64+
var objects []runtime.Object
65+
66+
vm := &vmoperatorv1alpha4.VirtualMachine{
67+
ObjectMeta: metav1.ObjectMeta{
68+
Name: "my-vm",
69+
Labels: test.vmLabels,
70+
},
71+
}
72+
73+
if test.setupPVC {
74+
pvc := &v1.PersistentVolumeClaim{
75+
ObjectMeta: metav1.ObjectMeta{
76+
Name: "my-pvc",
77+
Namespace: "default",
78+
Labels: test.pvcLabels,
79+
},
80+
}
81+
objects = append(objects, pvc)
82+
}
83+
84+
client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
85+
86+
err := validateVmAndPvc(ctx, test.instanceLabels, "my-instance", "my-pvc", "default", client, vm)
87+
88+
if test.expectError && err == nil {
89+
t.Errorf("expected error but got nil")
90+
} else if !test.expectError && err != nil {
91+
t.Errorf("unexpected error: %v", err)
92+
}
93+
})
94+
}
95+
}

0 commit comments

Comments
 (0)