Skip to content

Commit bbb626c

Browse files
committed
Add CnsVolumeAttachment CRD
1 parent 986aa2d commit bbb626c

File tree

17 files changed

+901
-22
lines changed

17 files changed

+901
-22
lines changed

manifests/supervisorcluster/1.29/cns-csi.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ rules:
7474
verbs: ["get"]
7575
- apiGroups: ["cns.vmware.com"]
7676
resources: ["cnsvolumeoperationrequests"]
77-
verbs: ["create", "get", "list", "update", "delete", "watch"]
77+
verbs: ["create", "get", "list", "update", "delete", "watch", "patch"]
7878
- apiGroups: ["cns.vmware.com"]
7979
resources: ["storagepolicyusages"]
8080
verbs: ["create", "get", "list", "patch", "delete"]
@@ -117,6 +117,9 @@ rules:
117117
- apiGroups: ["encryption.vmware.com"]
118118
resources: ["encryptionclasses"]
119119
verbs: ["get", "list", "watch"]
120+
- apiGroups: ["cns.vmware.com"]
121+
resources: ["cnsvolumeattachments"]
122+
verbs: ["create", "get", "list", "update", "delete", "watch", "patch"]
120123
---
121124
kind: ClusterRoleBinding
122125
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
@@ -120,6 +120,9 @@ rules:
120120
- apiGroups: ["cluster.x-k8s.io"]
121121
resources: ["clusters"]
122122
verbs: ["get", "list", "watch"]
123+
- apiGroups: ["cns.vmware.com"]
124+
resources: ["cnsvolumeattachments"]
125+
verbs: ["create", "get", "list", "update", "delete", "watch", "patch"]
123126
---
124127
kind: ClusterRoleBinding
125128
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
@@ -120,6 +120,9 @@ rules:
120120
- apiGroups: ["cluster.x-k8s.io"]
121121
resources: ["clusters"]
122122
verbs: ["get", "list", "watch"]
123+
- apiGroups: ["cns.vmware.com"]
124+
resources: ["cnsvolumeattachments"]
125+
verbs: ["create", "get", "list", "update", "delete", "watch", "patch"]
123126
---
124127
kind: ClusterRoleBinding
125128
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
@@ -120,6 +120,9 @@ rules:
120120
- apiGroups: ["cluster.x-k8s.io"]
121121
resources: ["clusters"]
122122
verbs: ["get", "list", "watch"]
123+
- apiGroups: ["cns.vmware.com"]
124+
resources: ["cnsvolumeattachments"]
125+
verbs: ["create", "get", "list", "update", "delete", "watch", "patch"]
123126
---
124127
kind: ClusterRoleBinding
125128
apiVersion: rbac.authorization.k8s.io/v1
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cnsvolumeattachment
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"sync"
23+
24+
"k8s.io/apimachinery/pkg/api/errors"
25+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/types"
27+
"k8s.io/client-go/tools/cache"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
30+
k8s "sigs.k8s.io/vsphere-csi-driver/v3/pkg/kubernetes"
31+
32+
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/csi/service/logger"
33+
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/internalapis"
34+
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/internalapis/cnsoperator/cnsvolumeattachment/v1alpha1"
35+
cnsoperatortypes "sigs.k8s.io/vsphere-csi-driver/v3/pkg/syncer/cnsoperator/types"
36+
)
37+
38+
var (
39+
cnsVolumeAttachmentInstanceLock sync.Mutex
40+
cnsVolumeAttachmentInstance *cnsVolumeAttachment
41+
)
42+
43+
// CnsVolumeAttachment exposes an interface to support adding
44+
// and removing information about attached VMs to a PVC.
45+
type CnsVolumeAttachment interface {
46+
// AddVmToAttachedList adds the input VM instance UUID to the list of
47+
// attached VMs for the given volume.
48+
AddVmToAttachedList(ctx context.Context, volumeName, VmInstanceUUID string) error
49+
// RemoveVmFromAttachedList removes the input instance VM UUID from
50+
// the list of attached VMs for the given volume.
51+
RemoveVmFromAttachedList(ctx context.Context, volumeName, VmInstanceUUID string) (error, bool)
52+
}
53+
54+
// cnsVolumeAttachment maintains a client to the API
55+
// server for operations on CnsVolumeAttachment instance.
56+
// It also contains a per instance lock to handle
57+
// concurrent operations.
58+
type cnsVolumeAttachment struct {
59+
client client.Client
60+
// Per volume lock for concurrent access to CnsVolumeAttachment instances.
61+
// Keys are strings representing PVC names.
62+
// Values are individual sync.Mutex locks that need to be held
63+
// to make updates to the CnsVolumeAttachment instance on the API server.
64+
volumeLock *sync.Map
65+
}
66+
67+
// GetCnsVolumeAttachmentInstance returns a singleton of type CnsVolumeAttachment.
68+
// Initializes the singleton if not already initialized.
69+
func GetCnsVolumeAttachmentInstance(ctx context.Context) (CnsVolumeAttachment, error) {
70+
log := logger.GetLogger(ctx)
71+
72+
cnsVolumeAttachmentInstanceLock.Lock()
73+
log.Infof("Acquired lock for cnsVolumeAttachmentInstanceLock")
74+
defer func() {
75+
cnsVolumeAttachmentInstanceLock.Unlock()
76+
log.Infof("Released lock for cnsVolumeAttachmentInstanceLock")
77+
}()
78+
79+
if cnsVolumeAttachmentInstance == nil {
80+
config, err := k8s.GetKubeConfig(ctx)
81+
if err != nil {
82+
log.Errorf("failed to get kubeconfig. Err: %v", err)
83+
return nil, err
84+
}
85+
k8sclient, err := k8s.NewClientForGroup(ctx, config, internalapis.GroupName)
86+
if err != nil {
87+
log.Errorf("failed to create k8s client. Err: %v", err)
88+
return nil, err
89+
}
90+
cnsVolumeAttachmentInstance = &cnsVolumeAttachment{
91+
client: k8sclient,
92+
volumeLock: &sync.Map{},
93+
}
94+
}
95+
96+
return cnsVolumeAttachmentInstance, nil
97+
}
98+
99+
// Add adds the input VM InstanceUUID to the list of
100+
// attached VMs for the given volume.
101+
// Callers need to specify cnsVolumeAttachment as a combination of
102+
// "<SV-namespace>/<SV-PVC-name>". This combination is used to uniquely
103+
// identify CnsVolumeAttachment instances.
104+
// The instance is created if it doesn't exist.
105+
// Returns an error if the operation cannot be persisted on the API server.
106+
func (f *cnsVolumeAttachment) AddVmToAttachedList(ctx context.Context,
107+
volumeName, VmInstanceUUID string) error {
108+
log := logger.GetLogger(ctx)
109+
110+
log.Infof("Adding VM %s to cnsVolumeAttachment %s",
111+
VmInstanceUUID, volumeName)
112+
actual, _ := f.volumeLock.LoadOrStore(volumeName, &sync.Mutex{})
113+
instanceLock, ok := actual.(*sync.Mutex)
114+
if !ok {
115+
return fmt.Errorf("failed to cast lock for cnsVolumeAttachment instance: %s", volumeName)
116+
}
117+
instanceLock.Lock()
118+
log.Infof("Acquired lock for cnsVolumeAttachment instance %s", volumeName)
119+
defer func() {
120+
instanceLock.Unlock()
121+
log.Infof("Released lock for instance %s", volumeName)
122+
}()
123+
124+
instance := &v1alpha1.CnsVolumeAttachment{}
125+
instanceNamespace, instanceName, err := cache.SplitMetaNamespaceKey(volumeName)
126+
if err != nil {
127+
log.Errorf("failed to split key %s with error: %+v", volumeName, err)
128+
return err
129+
}
130+
instanceKey := types.NamespacedName{
131+
Namespace: instanceNamespace,
132+
Name: instanceName,
133+
}
134+
err = f.client.Get(ctx, instanceKey, instance)
135+
if err != nil {
136+
if errors.IsNotFound(err) {
137+
// Create the instance as it does not exist on the API server.
138+
instance = &v1alpha1.CnsVolumeAttachment{
139+
ObjectMeta: v1.ObjectMeta{
140+
Name: instanceName,
141+
Namespace: instanceNamespace,
142+
// Add finalizer so that CnsVolumeAttachment instance doesn't get deleted abruptly
143+
Finalizers: []string{cnsoperatortypes.CNSFinalizer},
144+
},
145+
Spec: v1alpha1.CnsVolumeAttachmentSpec{
146+
AttachedVms: []string{
147+
VmInstanceUUID,
148+
},
149+
},
150+
}
151+
log.Debugf("Creating cnsVolumeAttachment instance %s with spec: %+v", volumeName, instance)
152+
err = f.client.Create(ctx, instance)
153+
if err != nil {
154+
log.Errorf("failed to create cnsVolumeAttachment instance %s with error: %+v", volumeName, err)
155+
return err
156+
}
157+
return nil
158+
}
159+
log.Errorf("failed to get cnsVolumeAttachment instance %s with error: %+v", volumeName, err)
160+
return err
161+
}
162+
163+
// Verify if input VmInstanceUUID exists in existing AttachedVMs list.
164+
log.Debugf("Verifying if VM %s exists in current list of attached Vms. Current list: %+v",
165+
VmInstanceUUID, instance.Spec.AttachedVms)
166+
currentAttachedVmsList := instance.Spec.AttachedVms
167+
for _, currentAttachedVM := range currentAttachedVmsList {
168+
if currentAttachedVM == VmInstanceUUID {
169+
log.Debugf("Found VM %s in list. Returning.", VmInstanceUUID)
170+
return nil
171+
}
172+
}
173+
newAttachVmsList := append(currentAttachedVmsList, VmInstanceUUID)
174+
instance.Spec.AttachedVms = newAttachVmsList
175+
log.Debugf("Updating cnsVolumeAttachment instance %s with spec: %+v", volumeName, instance)
176+
err = f.client.Update(ctx, instance)
177+
if err != nil {
178+
log.Errorf("failed to update cnsVolumeAttachment instance %s/%s with error: %+v", volumeName, err)
179+
}
180+
return err
181+
}
182+
183+
// RemoveVmFromAttachedList removes the input VM UUID from
184+
// the list of attached VMs for the given volume.
185+
// Callers need to specify volumeName as a combination of
186+
// "<SV-namespace>/<SV-PVC-name>". This combination is used to uniquely
187+
// identify CnsVolumeAttachment instances.
188+
// If the given VM was the last client for this file volume, the instance is
189+
// deleted from the API server.
190+
// Returns an error if the operation cannot be persisted on the API server.
191+
func (f *cnsVolumeAttachment) RemoveVmFromAttachedList(ctx context.Context,
192+
volumeName, VmInstanceUUID string) (error, bool) {
193+
log := logger.GetLogger(ctx)
194+
log.Infof("Removing VmInstanceUUID %s from cnsVolumeAttachment %s",
195+
VmInstanceUUID, volumeName)
196+
actual, _ := f.volumeLock.LoadOrStore(volumeName, &sync.Mutex{})
197+
instanceLock, ok := actual.(*sync.Mutex)
198+
if !ok {
199+
return fmt.Errorf("failed to cast lock for cnsVolumeAttachment instance: %s", volumeName),
200+
false
201+
}
202+
instanceLock.Lock()
203+
log.Infof("Acquired lock for cnsVolumeAttachment instance %s", volumeName)
204+
defer func() {
205+
instanceLock.Unlock()
206+
log.Infof("Released lock for instance %s", volumeName)
207+
}()
208+
209+
instance := &v1alpha1.CnsVolumeAttachment{}
210+
instanceNamespace, instanceName, err := cache.SplitMetaNamespaceKey(volumeName)
211+
if err != nil {
212+
log.Errorf("failed to split key %s with error: %+v", volumeName, err)
213+
return err, false
214+
}
215+
instanceKey := types.NamespacedName{
216+
Namespace: instanceNamespace,
217+
Name: instanceName,
218+
}
219+
err = f.client.Get(ctx, instanceKey, instance)
220+
if err != nil {
221+
if errors.IsNotFound(err) {
222+
log.Infof("cnsVolumeAttachment instance %s does not exist on API server", volumeName)
223+
return nil, true
224+
}
225+
log.Errorf("failed to get cnsVolumeAttachment instance %s with error: %+v", volumeName, err)
226+
return err, false
227+
}
228+
229+
log.Infof("Verifying if VM UUID %s exists in list of already attached VMs. Current list: %+v",
230+
volumeName, instance.Spec.AttachedVms)
231+
for index, existingAttachedVM := range instance.Spec.AttachedVms {
232+
if VmInstanceUUID != existingAttachedVM {
233+
continue
234+
}
235+
log.Infof("Removing VmUUID %s from Attached VMs list", VmInstanceUUID)
236+
instance.Spec.AttachedVms = append(
237+
instance.Spec.AttachedVms[:index],
238+
instance.Spec.AttachedVms[index+1:]...)
239+
if len(instance.Spec.AttachedVms) == 0 {
240+
log.Infof("Deleting cnsVolumeAttachment instance %s from API server", volumeName)
241+
// Remove finalizer from CnsVolumeAttachment instance
242+
err = removeFinalizer(ctx, f.client, instance)
243+
if err != nil {
244+
log.Errorf("failed to remove finalizer from cnsVolumeAttachment instance %s with error: %+v",
245+
volumeName, err)
246+
return err, false
247+
}
248+
err = f.client.Delete(ctx, instance)
249+
if err != nil {
250+
// In case of namespace deletion, we will have deletion timestamp added on the
251+
// CnsVolumeAttachment instance. So, as soon as we delete finalizer, instance might
252+
// get deleted immediately. In such cases we will get NotFound error here, return success
253+
// if instance is already deleted.
254+
if errors.IsNotFound(err) {
255+
log.Infof("cnsVolumeAttachment instance %s seems to be already deleted.", volumeName)
256+
f.volumeLock.Delete(volumeName)
257+
return nil, true
258+
}
259+
log.Errorf("failed to delete cnsVolumeAttachment instance %s with error: %+v", volumeName, err)
260+
return err, false
261+
}
262+
log.Infof("Successfully deleted cnsVolumeAttachment instance %s", volumeName)
263+
f.volumeLock.Delete(volumeName)
264+
return nil, true
265+
}
266+
log.Debugf("Updating cnsVolumeAttachment instance %s with spec: %+v", volumeName, instance)
267+
err = f.client.Update(ctx, instance)
268+
if err != nil {
269+
log.Errorf("failed to update cnsVolumeAttachment instance %s with error: %+v", volumeName, err)
270+
}
271+
return err, false
272+
}
273+
log.Infof("Could not find VM %s in list. Returning.", VmInstanceUUID)
274+
return nil, false
275+
}
276+
277+
// removeFinalizer will remove the CNS Finalizer = cns.vmware.com,
278+
// from a given CnsVolumeAttachment instance.
279+
func removeFinalizer(ctx context.Context, client client.Client,
280+
instance *v1alpha1.CnsVolumeAttachment) error {
281+
log := logger.GetLogger(ctx)
282+
283+
if !controllerutil.ContainsFinalizer(instance, cnsoperatortypes.CNSFinalizer) {
284+
// Finalizer not present on instance. Nothing to do.
285+
return nil
286+
}
287+
288+
finalizersOnInstance := instance.Finalizers
289+
for i, finalizer := range instance.Finalizers {
290+
if finalizer == cnsoperatortypes.CNSFinalizer {
291+
log.Infof("Removing %q finalizer from CnsNodeVmBatchAttachment instance with name: %q on namespace: %q",
292+
cnsoperatortypes.CNSFinalizer, instance.Name, instance.Namespace)
293+
finalizersOnInstance = append(instance.Finalizers[:i], instance.Finalizers[i+1:]...)
294+
break
295+
}
296+
}
297+
return k8s.PatchFinalizers(ctx, client, instance, finalizersOnInstance)
298+
}

0 commit comments

Comments
 (0)