@@ -279,3 +279,124 @@ func WriteKubeconfigToFile(exec commands.Executor, clusterID string, configDir s
279279
280280 return kubeconfigPath , nil
281281}
282+
283+ // CollectPVCVolumeUUIDs collects the UUIDs of any UpCloud storage volumes that were provisioned
284+ // by PVCs in the given namespace (typically the Helm release namespace).
285+ func CollectPVCVolumeUUIDs (ctx context.Context , exec commands.Executor , kubeClient * kubernetes.Clientset , namespace string ) ([]string , error ) {
286+ var uuids []string
287+
288+ // List PVCs in the namespace
289+ pvcs , err := kubeClient .CoreV1 ().PersistentVolumeClaims (namespace ).List (ctx , v1.ListOptions {})
290+ if err != nil {
291+ return nil , fmt .Errorf ("listing PVCs in namespace %q: %w" , namespace , err )
292+ }
293+
294+ if len (pvcs .Items ) == 0 {
295+ msg := fmt .Sprintf ("No PVCs found in namespace %q" , namespace )
296+ exec .PushProgressStarted (msg )
297+ exec .PushProgressSuccess (msg )
298+ return nil , nil
299+ }
300+
301+ // Announce PVC check
302+ msg := fmt .Sprintf ("Checking %d PVC(s) in namespace %q for backing volumes" , len (pvcs .Items ), namespace )
303+ exec .PushProgressStarted (msg )
304+ exec .PushProgressSuccess (msg )
305+
306+ // List PVs once
307+ pvs , err := kubeClient .CoreV1 ().PersistentVolumes ().List (ctx , v1.ListOptions {})
308+ if err != nil {
309+ return nil , fmt .Errorf ("listing PVs: %w" , err )
310+ }
311+
312+ // Iterate over PVCs belonging to this namespace
313+ for _ , pvc := range pvcs .Items {
314+ pvcMsg := fmt .Sprintf ("PVC %s/%s" , namespace , pvc .Name )
315+ exec .PushProgressStarted (pvcMsg )
316+
317+ if pvc .Spec .VolumeName == "" {
318+ exec .PushProgressStarted (fmt .Sprintf ("%s → no bound PV, skipping" , pvcMsg ))
319+ exec .PushProgressSuccess (fmt .Sprintf ("%s → no bound PV, skipping" , pvcMsg ))
320+ continue
321+ }
322+
323+ // Find the matching PV
324+ var matchedPV * corev1.PersistentVolume
325+ for _ , pv := range pvs .Items {
326+ if pv .Name == pvc .Spec .VolumeName {
327+ matchedPV = & pv
328+ break
329+ }
330+ }
331+ if matchedPV == nil {
332+ exec .PushProgressStarted (fmt .Sprintf ("%s → bound PV %q not found, skipping" , pvcMsg , pvc .Spec .VolumeName ))
333+ exec .PushProgressSuccess (fmt .Sprintf ("%s → bound PV %q not found, skipping" , pvcMsg , pvc .Spec .VolumeName ))
334+ continue
335+ }
336+
337+ if matchedPV .Spec .CSI == nil {
338+ exec .PushProgressStarted (fmt .Sprintf ("%s → PV %q has no CSI spec, skipping" , pvcMsg , matchedPV .Name ))
339+ exec .PushProgressSuccess (fmt .Sprintf ("%s → PV %q has no CSI spec, skipping" , pvcMsg , matchedPV .Name ))
340+ continue
341+ }
342+
343+ storageUUID := matchedPV .Spec .CSI .VolumeHandle
344+ if storageUUID == "" {
345+ exec .PushProgressStarted (fmt .Sprintf ("%s → PV %q has empty VolumeHandle, skipping" , pvcMsg , matchedPV .Name ))
346+ exec .PushProgressSuccess (fmt .Sprintf ("%s → PV %q has empty VolumeHandle, skipping" , pvcMsg , matchedPV .Name ))
347+ continue
348+ }
349+
350+ uuids = append (uuids , storageUUID )
351+ exec .PushProgressSuccess (pvcMsg )
352+ }
353+
354+ return uuids , nil
355+ }
356+
357+ func DeletePVCVolumesByUUIDs (exec commands.Executor , uuids []string ) error {
358+ for _ , storageUUID := range uuids {
359+ deleteMsg := fmt .Sprintf ("Deleting UpCloud storage %s" , storageUUID )
360+ exec .PushProgressStarted (deleteMsg )
361+
362+ // Wait for the storage to be detached (in case the PVC was recently deleted)
363+ if err := waitForStorageToBeDetached (exec , storageUUID , 5 * time .Minute ); err != nil {
364+ exec .PushProgressUpdateMessage (fmt .Sprintf ("storage %s" , storageUUID ), fmt .Sprintf ("waiting failed: %v" , err ))
365+ continue
366+ }
367+
368+ req := & request.DeleteStorageRequest {UUID : storageUUID }
369+ if err := exec .All ().DeleteStorage (exec .Context (), req ); err != nil {
370+ exec .PushProgressStarted (fmt .Sprintf ("Failed to delete UpCloud storage %s: %v" , storageUUID , err ))
371+ exec .PushProgressSuccess (fmt .Sprintf ("Failed to delete UpCloud storage %s: %v" , storageUUID , err ))
372+ continue
373+ }
374+
375+ exec .PushProgressSuccess (deleteMsg )
376+ }
377+ return nil
378+ }
379+
380+ // WaitForNamespaceDeletion waits until the given namespace is deleted, or times out after the specified duration.
381+ func WaitForNamespaceDeletion (ctx context.Context , kubeClient * kubernetes.Clientset , name string , timeout time.Duration ) error {
382+ ticker := time .NewTicker (2 * time .Second )
383+ defer ticker .Stop ()
384+ timeoutCh := time .After (timeout )
385+
386+ for {
387+ select {
388+ case <- timeoutCh :
389+ return fmt .Errorf ("namespace %q not deleted within %s" , name , timeout )
390+ case <- ticker .C :
391+ _ , err := kubeClient .CoreV1 ().Namespaces ().Get (ctx , name , v1.GetOptions {})
392+ if err != nil {
393+ if apierrors .IsNotFound (err ) {
394+ // Namespace gone
395+ return nil
396+ }
397+ return fmt .Errorf ("error checking namespace %q: %w" , name , err )
398+ }
399+ // still exists, keep waiting
400+ }
401+ }
402+ }
0 commit comments