Skip to content

Commit f85c5ff

Browse files
authored
Allow devops users to attach GC RWX file volume with VM service VMs (#3611)
1 parent aabc278 commit f85c5ff

File tree

8 files changed

+164
-101
lines changed

8 files changed

+164
-101
lines changed

manifests/supervisorcluster/1.29/cns-csi.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,9 @@ rules:
732732
- apiGroups: ["cns.vmware.com"]
733733
resources: ["cnsfileaccessconfigs"]
734734
verbs: ["get", "list", "update"]
735+
- apiGroups: ["vmoperator.vmware.com"]
736+
resources: ["virtualmachines"]
737+
verbs: ["get", "list"]
735738
---
736739
kind: ClusterRoleBinding
737740
apiVersion: rbac.authorization.k8s.io/v1

manifests/supervisorcluster/1.30/cns-csi.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,9 @@ rules:
737737
- apiGroups: ["cns.vmware.com"]
738738
resources: ["cnsfileaccessconfigs"]
739739
verbs: ["get", "list", "update"]
740+
- apiGroups: ["vmoperator.vmware.com"]
741+
resources: ["virtualmachines"]
742+
verbs: ["get", "list"]
740743
---
741744
kind: ClusterRoleBinding
742745
apiVersion: rbac.authorization.k8s.io/v1

manifests/supervisorcluster/1.31/cns-csi.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,9 @@ rules:
737737
- apiGroups: ["cns.vmware.com"]
738738
resources: ["cnsfileaccessconfigs"]
739739
verbs: ["get", "list", "update"]
740+
- apiGroups: ["vmoperator.vmware.com"]
741+
resources: ["virtualmachines"]
742+
verbs: ["get", "list"]
740743
---
741744
kind: ClusterRoleBinding
742745
apiVersion: rbac.authorization.k8s.io/v1

manifests/supervisorcluster/1.32/cns-csi.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,9 @@ rules:
737737
- apiGroups: ["cns.vmware.com"]
738738
resources: ["cnsfileaccessconfigs"]
739739
verbs: ["get", "list", "update"]
740+
- apiGroups: ["vmoperator.vmware.com"]
741+
resources: ["virtualmachines"]
742+
verbs: ["get", "list"]
740743
---
741744
kind: ClusterRoleBinding
742745
apiVersion: rbac.authorization.k8s.io/v1

pkg/syncer/admissionhandler/cnscsi_admissionhandler.go

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strconv"
1414
"strings"
1515

16+
vmoperatortypes "github.com/vmware-tanzu/vm-operator/api/v1alpha5"
1617
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/csi/service/common/commonco/k8sorchestrator"
1718

1819
admissionv1 "k8s.io/api/admission/v1"
@@ -29,9 +30,12 @@ import (
2930
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/csi/service/common"
3031
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/csi/service/common/commonco"
3132

33+
apitypes "k8s.io/apimachinery/pkg/types"
3234
cnsfileaccessconfigv1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator/cnsfileaccessconfig/v1alpha1"
3335
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/cns-lib/crypto"
36+
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/utils"
3437
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/csi/service/logger"
38+
k8s "sigs.k8s.io/vsphere-csi-driver/v3/pkg/kubernetes"
3539
)
3640

3741
const (
@@ -40,8 +44,8 @@ const (
4044
DefaultWebhookPort = 9883
4145
DefaultWebhookMetricsBindAddress = "0"
4246
devopsUserLabelKey = "cns.vmware.com/user-created"
43-
vmNameLabelKey = "cns.vmware.com/vm-name"
44-
pvcNameLabelKey = "cns.vmware.com/pvc-name"
47+
vmUIDLabelKey = "cns.vmware.com/vm-uid"
48+
pvcUIDLabelKey = "cns.vmware.com/pvc-uid"
4549
)
4650

4751
var (
@@ -248,6 +252,71 @@ func (h *CSISupervisorMutationWebhook) Handle(ctx context.Context, req admission
248252
return admission.Allowed("")
249253
}
250254

255+
// getVmUID returns the VM UID for the given VM
256+
func getVmUID(ctx context.Context,
257+
vmName string, namespace string) (string, error) {
258+
log := logger.GetLogger(ctx)
259+
260+
restClientConfig, err := k8s.GetKubeConfig(ctx)
261+
if err != nil {
262+
log.Errorf("failed to initialize rest clientconfig. Error: %s", err)
263+
return "", err
264+
}
265+
266+
vmOperatorClient, err := k8s.NewClientForGroup(ctx, restClientConfig, vmoperatortypes.GroupName)
267+
if err != nil {
268+
log.Error("failed to initialize vmOperatorClient. Error: %s", err)
269+
return "", err
270+
}
271+
272+
vm, _, err := getVirtualMachine(ctx, vmOperatorClient, vmName, namespace)
273+
if err != nil {
274+
log.Error("failed to get virtualmachine. Error: %s", err)
275+
return "", err
276+
}
277+
278+
log.Infof("Found UID %s for VM %s", string(vm.ObjectMeta.UID), vm.Name)
279+
return string(vm.ObjectMeta.UID), nil
280+
}
281+
282+
// getVirtualMachine returns the VM object for the given vmName and namespace.
283+
func getVirtualMachine(ctx context.Context, vmOperatorClient client.Client,
284+
vmName string, namespace string) (*vmoperatortypes.VirtualMachine, string, error) {
285+
log := logger.GetLogger(ctx)
286+
vmKey := apitypes.NamespacedName{
287+
Namespace: namespace,
288+
Name: vmName,
289+
}
290+
virtualMachine, apiVersion, err := utils.GetVirtualMachineAllApiVersions(ctx,
291+
vmKey, vmOperatorClient)
292+
if err != nil {
293+
log.Error("failed to get virtualmachine instance for the VM with name: %q. Error: %+v", vmName, err)
294+
return nil, apiVersion, err
295+
}
296+
return virtualMachine, apiVersion, nil
297+
}
298+
299+
// getPVCUID retrieves the UID of a PersistentVolumeClaim by name and namespace.
300+
func getPVCUID(ctx context.Context, pvcName, namespace string) (string, error) {
301+
log := logger.GetLogger(ctx)
302+
303+
k8sClient, err := k8s.NewClient(ctx)
304+
if err != nil {
305+
log.Errorf("failed to create k8s client. Errror: %s", err)
306+
return "", err
307+
}
308+
309+
pvc, err := k8sClient.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, pvcName,
310+
v1.GetOptions{})
311+
if err != nil {
312+
log.Errorf("failed to obtain PVC %s. Errror: %s", pvcName, err)
313+
return "", err
314+
}
315+
316+
log.Infof("Found UID %s for PVC %s", string(pvc.UID), pvcName)
317+
return string(pvc.UID), nil
318+
}
319+
251320
// mutateNewCnsFileAccessConfig adds devops label on a CnsFileAccessConfig CR
252321
// if it is being created by a devops user.
253322
func (h *CSISupervisorMutationWebhook) mutateNewCnsFileAccessConfig(ctx context.Context,
@@ -275,12 +344,26 @@ func (h *CSISupervisorMutationWebhook) mutateNewCnsFileAccessConfig(ctx context.
275344
newCnsFileAccessConfig.Labels = make(map[string]string)
276345
}
277346

347+
// Obtain VM's UID
348+
vmUID, err := getVmUID(ctx, newCnsFileAccessConfig.Spec.VMName, newCnsFileAccessConfig.Namespace)
349+
if err != nil {
350+
log.Errorf("faield to get VM UID for VM %s. Err: %s", newCnsFileAccessConfig.Spec.VMName, err)
351+
return admission.Errored(http.StatusInternalServerError, err)
352+
}
353+
354+
// Obtain PVC's UID
355+
pvcUID, err := getPVCUID(ctx, newCnsFileAccessConfig.Spec.PvcName, newCnsFileAccessConfig.Namespace)
356+
if err != nil {
357+
log.Errorf("faield to get PVC UID for PVC %s. Err: %s", newCnsFileAccessConfig.Spec.PvcName, err)
358+
return admission.Errored(http.StatusInternalServerError, err)
359+
}
360+
278361
// Add VM name and PVC name label.
279362
// If someone created this CR with these labels already present, CSI will overrite on them
280363
// with the correct values.
281364
newCnsFileAccessConfig.Labels[devopsUserLabelKey] = "true"
282-
newCnsFileAccessConfig.Labels[vmNameLabelKey] = newCnsFileAccessConfig.Spec.VMName
283-
newCnsFileAccessConfig.Labels[pvcNameLabelKey] = newCnsFileAccessConfig.Spec.PvcName
365+
newCnsFileAccessConfig.Labels[vmUIDLabelKey] = vmUID
366+
newCnsFileAccessConfig.Labels[pvcUIDLabelKey] = pvcUID
284367

285368
newRawCnsFileAccessConfig, err := json.Marshal(newCnsFileAccessConfig)
286369
if err != nil {

pkg/syncer/admissionhandler/validate_cnsfileaccessconfig.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,23 @@ func cnsFileAccessConfigAlreadyExists(ctx context.Context, clientConfig *rest.Co
108108
return "", err
109109
}
110110

111+
// Obtain VM's UID.
112+
vmUID, err := getVmUID(ctx, vm, namespace)
113+
if err != nil {
114+
log.Errorf("failed to get VM UID for VM %s. Err: %s", vm, err)
115+
return "", err
116+
}
117+
118+
// Obtain PVC's UID
119+
pvcUID, err := getPVCUID(ctx, pvc, namespace)
120+
if err != nil {
121+
log.Errorf("failed to get PVC UID for PVC %s. Err: %s", pvc, err)
122+
return "", err
123+
}
124+
111125
// 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})
126+
labelSelector := labels.SelectorFromSet(labels.Set{vmUIDLabelKey: vmUID, pvcUIDLabelKey: pvcUID})
127+
113128
// Get the list of all CnsFileAccessConfig CRs in the given namespace.
114129
cnsFileAccessConfigList := &cnsfileaccessconfigv1alpha1.CnsFileAccessConfigList{}
115130
err = cnsOperatorClient.List(ctx, cnsFileAccessConfigList, &client.ListOptions{

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

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,7 @@ import (
6262

6363
const (
6464
defaultMaxWorkerThreadsForFileAccessConfig = 10
65-
vmNameLabelKey = "cns.vmware.com/vm-name"
66-
pvcNameLabelKey = "cns.vmware.com/pvc-name"
6765
capvVmLabelKey = "capv.vmware.com"
68-
capvPvcLabelKey = "TKGService"
6966
devopsUserLabelKey = "cns.vmware.com/user-created"
7067
)
7168

@@ -822,12 +819,10 @@ func (r *ReconcileCnsFileAccessConfig) getVMExternalIP(ctx context.Context,
822819
}
823820

824821
// 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/user-created", "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.
822+
// CnsFileAccessConfig CRs created by devpos users will have "cns.vmware.com/user-created" label.
823+
// When this labels are present, the VM must not belong to TKG cluster.
828824
// This is verified by ensuring that:
829825
// The VM does not have a label applied by CAPV - example capv.vmware.com/cluster.name.
830-
// The PVC does not have a label applied by CAPV - example <TKG cluster namespace>/TKGService
831826
func validateVmAndPvc(ctx context.Context, instanceLabels map[string]string, instanceName string, pvcName string,
832827
namespace string, client client.Client, vm *vmoperatortypes.VirtualMachine) error {
833828
log := logger.GetLogger(ctx)
@@ -860,23 +855,6 @@ func validateVmAndPvc(ctx context.Context, instanceLabels map[string]string, ins
860855
}
861856
}
862857

863-
pvc := &v1.PersistentVolumeClaim{}
864-
err := client.Get(ctx, types.NamespacedName{Name: pvcName, Namespace: namespace}, pvc)
865-
if err != nil {
866-
log.Errorf("failed to get PVC with name %s in namespace %s", pvcName, namespace)
867-
return err
868-
}
869-
870-
for key := range pvc.Labels {
871-
if strings.Contains(key, capvPvcLabelKey) {
872-
msg := fmt.Sprintf("CnsFileAccessConfig is created by devops user and has "+
873-
"TKG PVC %s. Invalid combination.", pvcName)
874-
log.Errorf(msg)
875-
err := errors.New(msg)
876-
return err
877-
}
878-
}
879-
880858
log.Infof("Successfully verified instance %s for VM/PVC combination", instanceName)
881859
return nil
882860
}

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

Lines changed: 47 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,90 +4,65 @@ import (
44
"context"
55
"testing"
66

7-
v1 "k8s.io/api/core/v1"
8-
"k8s.io/apimachinery/pkg/runtime"
9-
"sigs.k8s.io/controller-runtime/pkg/client/fake"
10-
7+
"github.com/stretchr/testify/assert"
118
vmoperatortypes "github.com/vmware-tanzu/vm-operator/api/v1alpha5"
129
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1310
)
1411

15-
func TestValidateVmAndPvc(t *testing.T) {
16-
scheme := runtime.NewScheme()
17-
_ = v1.AddToScheme(scheme)
18-
_ = vmoperatortypes.AddToScheme(scheme)
12+
func TestValidateVmAndPvc_NoLabels(t *testing.T) {
13+
ctx := context.Background()
1914

20-
ctx := context.TODO()
15+
err := validateVmAndPvc(ctx, nil, "vm-1", "pvc-1", "ns-1", nil, &vmoperatortypes.VirtualMachine{})
16+
assert.NoError(t, err)
17+
}
2118

22-
tests := []struct {
23-
name string
24-
instanceLabels map[string]string
25-
vmLabels map[string]string
26-
expectError bool
27-
setupPVC bool
28-
}{
29-
{
30-
name: "no instance labels",
31-
instanceLabels: nil,
32-
expectError: false,
33-
},
34-
{
35-
name: "labels but not devops user",
36-
instanceLabels: map[string]string{
37-
"random": "value",
38-
},
39-
expectError: false,
40-
},
41-
{
42-
name: "devops user with capv label on VM",
43-
instanceLabels: map[string]string{
44-
devopsUserLabelKey: "true",
45-
},
46-
vmLabels: map[string]string{
47-
capvVmLabelKey + "/cluster": "my-cluster",
48-
},
49-
expectError: true,
50-
},
51-
{
52-
name: "valid input - no errors",
53-
instanceLabels: map[string]string{
54-
devopsUserLabelKey: "true",
55-
},
56-
setupPVC: true,
57-
expectError: false,
58-
},
19+
func TestValidateVmAndPvc_NotDevOpsUser(t *testing.T) {
20+
ctx := context.Background()
21+
22+
labels := map[string]string{
23+
"other.label": "value",
5924
}
6025

61-
for _, test := range tests {
62-
t.Run(test.name, func(t *testing.T) {
63-
var objects []runtime.Object
26+
err := validateVmAndPvc(ctx, labels, "vm-1", "pvc-1", "ns-1", nil, &vmoperatortypes.VirtualMachine{})
27+
assert.NoError(t, err)
28+
}
29+
30+
func TestValidateVmAndPvc_DevOpsUser_ValidVM(t *testing.T) {
31+
ctx := context.Background()
6432

65-
vm := &vmoperatortypes.VirtualMachine{
66-
ObjectMeta: metav1.ObjectMeta{
67-
Name: "my-vm",
68-
Labels: test.vmLabels,
69-
},
70-
}
33+
labels := map[string]string{
34+
devopsUserLabelKey: "true",
35+
}
7136

72-
if test.setupPVC {
73-
pvc := &v1.PersistentVolumeClaim{
74-
ObjectMeta: metav1.ObjectMeta{
75-
Name: "my-pvc",
76-
Namespace: "default",
77-
},
78-
}
79-
objects = append(objects, pvc)
80-
}
37+
vm := &vmoperatortypes.VirtualMachine{
38+
ObjectMeta: metav1.ObjectMeta{
39+
Name: "my-vm",
40+
Labels: map[string]string{"custom.label": "value"},
41+
},
42+
}
8143

82-
client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
44+
err := validateVmAndPvc(ctx, labels, "vm-1", "pvc-1", "ns-1", nil, vm)
45+
assert.NoError(t, err)
46+
}
8347

84-
err := validateVmAndPvc(ctx, test.instanceLabels, "my-instance", "my-pvc", "default", client, vm)
48+
func TestValidateVmAndPvc_DevOpsUser_InvalidVM(t *testing.T) {
49+
ctx := context.Background()
8550

86-
if test.expectError && err == nil {
87-
t.Errorf("expected error but got nil")
88-
} else if !test.expectError && err != nil {
89-
t.Errorf("unexpected error: %v", err)
90-
}
91-
})
51+
labels := map[string]string{
52+
devopsUserLabelKey: "true",
9253
}
54+
55+
vm := &vmoperatortypes.VirtualMachine{
56+
ObjectMeta: metav1.ObjectMeta{
57+
Name: "my-vm",
58+
Labels: map[string]string{
59+
capvVmLabelKey + "/machine": "value", // Contains capvVmLabelKey
60+
},
61+
},
62+
}
63+
64+
err := validateVmAndPvc(ctx, labels, "vm-1", "pvc-1", "ns-1", nil, vm)
65+
assert.Error(t, err)
66+
assert.Contains(t, err.Error(), "Invalid combination")
67+
assert.Contains(t, err.Error(), "my-vm")
9368
}

0 commit comments

Comments
 (0)