Skip to content

Commit 3e93726

Browse files
committed
verify pvcs for ownerreferences pointing to VM
Signed-off-by: pruthvitd <prd@redhat.com>
1 parent c68f0c8 commit 3e93726

6 files changed

Lines changed: 241 additions & 7 deletions

File tree

cmd/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"k8s.io/apimachinery/pkg/runtime"
2424
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
2525
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
26+
2627
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
2728
// to ensure that exec-entrypoint and run can make use of them.
2829
_ "k8s.io/client-go/plugin/pkg/client/auth"
@@ -42,6 +43,7 @@ import (
4243
controllers "github.com/ramendr/ramen/internal/controller"
4344
argocdv1alpha1hack "github.com/ramendr/ramen/internal/controller/argocd"
4445
rmnutil "github.com/ramendr/ramen/internal/controller/util"
46+
4547
// +kubebuilder:scaffold:imports
4648
_ "github.com/ramendr/ramen/internal/dummy"
4749
)

config/rbac/role.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ rules:
144144
- get
145145
- list
146146
- watch
147+
- apiGroups:
148+
- cdi.kubevirt.io
149+
resources:
150+
- datavolumes
151+
verbs:
152+
- get
153+
- list
154+
- watch
147155
- apiGroups:
148156
- cluster.open-cluster-management.io
149157
resources:
@@ -232,8 +240,10 @@ rules:
232240
resources:
233241
- virtualmachines
234242
verbs:
243+
- delete
235244
- get
236245
- list
246+
- watch
237247
- apiGroups:
238248
- multicluster.x-k8s.io
239249
resources:

internal/controller/suite_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import (
5151
argocdv1alpha1hack "github.com/ramendr/ramen/internal/controller/argocd"
5252
testutils "github.com/ramendr/ramen/internal/controller/testutils"
5353
"github.com/ramendr/ramen/internal/controller/util"
54+
5455
// +kubebuilder:scaffold:imports
5556
_ "github.com/ramendr/ramen/internal/dummy"
5657
)

internal/controller/util/vm_util.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@ import (
66

77
"github.com/go-logr/logr"
88
k8serrors "k8s.io/apimachinery/pkg/api/errors"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/runtime/schema"
911
"k8s.io/apimachinery/pkg/types"
1012
virtv1 "kubevirt.io/api/core/v1"
1113
"sigs.k8s.io/controller-runtime/pkg/client"
1214

1315
"github.com/ramendr/ramen/internal/controller/core"
1416
)
1517

18+
const (
19+
KindVirtualMachine = "VirtualMachine"
20+
KubeVirtAPIVersion = "kubevirt.io/v1"
21+
)
22+
1623
func ListVMsByLabelSelector(
1724
ctx context.Context,
1825
apiReader client.Reader,
@@ -89,3 +96,91 @@ func ListVMsByVMNamespace(
8996

9097
return nil, notFoundErr
9198
}
99+
100+
func IsVMDeletionInProgress(ctx context.Context,
101+
k8sclient client.Client,
102+
vmList []string,
103+
vmNamespaceList []string,
104+
log logr.Logger,
105+
) bool {
106+
log.Info("Checking if VirtualMachines are being deleted",
107+
"vmCount", len(vmList),
108+
"vmNames", vmList)
109+
110+
foundVM := &virtv1.VirtualMachine{}
111+
112+
for _, ns := range vmNamespaceList {
113+
for _, vm := range vmList {
114+
vmLookUp := types.NamespacedName{Namespace: ns, Name: vm}
115+
if err := k8sclient.Get(ctx, vmLookUp, foundVM); err != nil {
116+
// Continuing with remaining list of VMs as the current one might already have been deleted
117+
continue
118+
}
119+
120+
if !foundVM.GetDeletionTimestamp().IsZero() {
121+
// Deletion of vm has been requested
122+
return true
123+
}
124+
}
125+
}
126+
127+
return false
128+
}
129+
130+
func DeleteVMs(
131+
ctx context.Context,
132+
k8sclient client.Client,
133+
vmList []string,
134+
vmNamespaceList []string,
135+
log logr.Logger,
136+
) error {
137+
for _, ns := range vmNamespaceList {
138+
for _, vmName := range vmList {
139+
vm := &virtv1.VirtualMachine{}
140+
key := client.ObjectKey{Name: vmName, Namespace: ns}
141+
142+
if err := k8sclient.Get(ctx, key, vm); err != nil {
143+
log.Error(err, "Failed to get VM", "namespace", ns, "name", vmName)
144+
145+
return fmt.Errorf("failed to get VM %s/%s: %w", ns, vmName, err)
146+
}
147+
148+
if err := k8sclient.Delete(ctx, vm); err != nil {
149+
log.Error(err, "Failed to delete VM", "namespace", ns, "name", vmName)
150+
151+
return fmt.Errorf("failed to delete VM %s/%s: %w", ns, vmName, err)
152+
}
153+
154+
log.Info("Deleted VM successfully", "namespace", ns, "name", vmName)
155+
}
156+
}
157+
158+
return nil
159+
}
160+
161+
// IsOwnedByVM recursively traverses ownerReferences until it finds a VirtualMachine.
162+
func IsOwnedByVM(ctx context.Context, c client.Client, obj client.Object, log logr.Logger) (string, error) {
163+
for {
164+
owners := obj.GetOwnerReferences()
165+
if len(owners) == 0 {
166+
return "", fmt.Errorf("no VM found in ownership chain")
167+
}
168+
169+
for _, owner := range owners {
170+
if owner.Kind == KindVirtualMachine && owner.APIVersion == KubeVirtAPIVersion {
171+
return owner.Name, nil // Found VM root
172+
}
173+
174+
// Fetch only metadata of the owner
175+
ownerMeta := &metav1.PartialObjectMetadata{}
176+
ownerMeta.SetGroupVersionKind(schema.FromAPIVersionAndKind(owner.APIVersion, owner.Kind))
177+
178+
if err := c.Get(ctx, client.ObjectKey{Namespace: obj.GetNamespace(), Name: owner.Name}, ownerMeta); err != nil {
179+
return "", err
180+
}
181+
182+
// Continue traversal with the owner
183+
obj = ownerMeta
184+
}
185+
}
186+
}

internal/controller/volumereplicationgroup_controller.go

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,8 @@ func filterPVC(reader client.Reader, pvc *corev1.PersistentVolumeClaim, log logr
397397
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=list;watch
398398
// +kubebuilder:rbac:groups="apiextensions.k8s.io",resources=customresourcedefinitions,verbs=get;list;watch
399399
// +kubebuilder:rbac:groups=core,resources=pods/exec,verbs=create
400-
// +kubebuilder:rbac:groups="kubevirt.io",resources=virtualmachines,verbs=get;list
400+
// +kubebuilder:rbac:groups="kubevirt.io",resources=virtualmachines,verbs=get;list;watch;delete
401+
// +kubebuilder:rbac:groups="cdi.kubevirt.io",resources=datavolumes,verbs=get;list;watch
401402

402403
// Reconcile is part of the main kubernetes reconciliation loop which aims to
403404
// move the current state of the cluster closer to the desired state.
@@ -1660,6 +1661,35 @@ func (v *VRGInstance) processAsSecondary() ctrl.Result {
16601661

16611662
func (v *VRGInstance) reconcileAsSecondary() ctrl.Result {
16621663
result := ctrl.Result{}
1664+
1665+
if v.isVMRecipeProtection() {
1666+
v.log.Info("Checking VM cleanup and cross-cluster resource conflicts",
1667+
"name", v.instance.GetName(),
1668+
"namespace", v.instance.GetNamespace(),
1669+
"recipeName", "vm-recipe")
1670+
1671+
if err := v.validateVMsForStandaloneProtection(); err != nil {
1672+
result.Requeue = true
1673+
}
1674+
1675+
if !result.Requeue {
1676+
v.log.Info("VRG status observed",
1677+
"name", v.instance.GetName(),
1678+
"namespace", v.instance.GetNamespace(),
1679+
"replicationState", v.instance.Spec.ReplicationState,
1680+
"statusState", v.instance.Status.State,
1681+
"generation", v.instance.GetGeneration(),
1682+
"resourceVersion", v.instance.GetResourceVersion(),
1683+
)
1684+
1685+
if v.ShouldCleanupVMForSecondary() {
1686+
v.log.Info("Requeuing until VM cleanup is complete")
1687+
1688+
result.Requeue = true
1689+
}
1690+
}
1691+
}
1692+
16631693
result.Requeue = v.reconcileVolSyncAsSecondary() || result.Requeue
16641694
result.Requeue = v.reconcileVolRepsAsSecondary() || result.Requeue
16651695

@@ -1678,12 +1708,6 @@ func (v *VRGInstance) reconcileAsSecondary() ctrl.Result {
16781708
v.instance.Status.Conditions = []metav1.Condition{}
16791709
}
16801710

1681-
if !result.Requeue && v.isVMRecipeProtection() {
1682-
if err := v.validateVMsForStandaloneProtection(); err != nil {
1683-
result.Requeue = true
1684-
}
1685-
}
1686-
16871711
return result
16881712
}
16891713

@@ -1791,6 +1815,8 @@ func (v *VRGInstance) updateVRGStatus(result ctrl.Result) ctrl.Result {
17911815

17921816
v.updateStatusState()
17931817

1818+
result.Requeue = v.instance.Status.State == ramendrv1alpha1.UnknownState
1819+
17941820
v.instance.Status.ObservedGeneration = v.instance.Generation
17951821

17961822
if !reflect.DeepEqual(v.savedInstanceStatus, v.instance.Status) {

internal/controller/vrg_volrep.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2424

2525
ramendrv1alpha1 "github.com/ramendr/ramen/api/v1alpha1"
26+
recipecore "github.com/ramendr/ramen/internal/controller/core"
2627
rmnutil "github.com/ramendr/ramen/internal/controller/util"
2728
)
2829

@@ -2895,3 +2896,102 @@ func PruneAnnotations(annotations map[string]string) map[string]string {
28952896

28962897
return result
28972898
}
2899+
2900+
func (v *VRGInstance) ShouldCleanupVMForSecondary() bool {
2901+
if !v.IsDRActionInProgress() {
2902+
v.log.Info("Skip VM cleanup; reconcile as secondary")
2903+
2904+
return false
2905+
}
2906+
2907+
v.log.Info(
2908+
"DR action progressing",
2909+
"component", "VRGController",
2910+
"action", "reconcile",
2911+
"vrgName", v.instance.GetName(),
2912+
"namespace", v.instance.GetNamespace(),
2913+
"desiredState", v.instance.Spec.ReplicationState,
2914+
"currentState", v.instance.Status.State,
2915+
)
2916+
2917+
vmNamespaceList := v.instance.Spec.KubeObjectProtection.RecipeParameters[recipecore.ProtectedVMNamespace]
2918+
vmList := v.instance.Spec.KubeObjectProtection.RecipeParameters[recipecore.VMList]
2919+
2920+
if len(vmList) > 0 {
2921+
if rmnutil.IsVMDeletionInProgress(v.ctx, v.reconciler.Client, vmList, vmNamespaceList, v.log) {
2922+
v.log.Info("VM deletion is in progress, skipping ownerreferences check")
2923+
2924+
return true
2925+
}
2926+
2927+
var foundVMs []string
2928+
2929+
foundVMs, err := rmnutil.ListVMsByVMNamespace(v.ctx, v.reconciler.APIReader,
2930+
v.log, vmNamespaceList, vmList)
2931+
if len(foundVMs) == 0 || err != nil {
2932+
v.log.Info(
2933+
"No VirtualMachines found for cleanup; deletion appears complete",
2934+
"vmList", vmList,
2935+
"namespaceList", vmNamespaceList,
2936+
)
2937+
2938+
return false
2939+
}
2940+
}
2941+
2942+
if v.IsAllProtectedPVCsOwnedByProtectedVMs() {
2943+
v.log.Info("all protected PVCs have ownerreferences to protected list of VMs")
2944+
// Cleanup VM resources
2945+
err := rmnutil.DeleteVMs(v.ctx, v.reconciler.Client, vmList, vmNamespaceList, v.log)
2946+
if err != nil {
2947+
v.log.Error(err, "Failed to delete VMs",
2948+
"vmList", vmList,
2949+
)
2950+
2951+
return false
2952+
}
2953+
2954+
return true
2955+
}
2956+
2957+
v.log.Info("not all protected PVCs have ownerReferences to the protected list of VMs")
2958+
2959+
return false
2960+
}
2961+
2962+
func (v *VRGInstance) IsAllProtectedPVCsOwnedByProtectedVMs() bool {
2963+
yes := true
2964+
vmList := v.instance.Spec.KubeObjectProtection.RecipeParameters[recipecore.VMList]
2965+
2966+
allPVCs := make([]corev1.PersistentVolumeClaim, 0, len(v.volRepPVCs)+len(v.volSyncPVCs))
2967+
allPVCs = append(allPVCs, v.volRepPVCs...)
2968+
allPVCs = append(allPVCs, v.volSyncPVCs...)
2969+
2970+
for idx := range allPVCs {
2971+
pvc := &allPVCs[idx]
2972+
log := logWithPvcName(v.log, pvc)
2973+
2974+
vmName, err := rmnutil.IsOwnedByVM(v.ctx, v.reconciler.Client, pvc, log)
2975+
if err != nil {
2976+
log.Error(err, "Skipping cleanup",
2977+
"pvc", pvc.Name,
2978+
"vm", vmName,
2979+
"reason", "invalid ownerReferences",
2980+
"action", "manual cleanup required")
2981+
return false
2982+
}
2983+
2984+
v.log.Info("PVC is owned by VM", "pvc", pvc.Name, "vm", vmName)
2985+
2986+
if !slices.Contains(vmList, vmName) {
2987+
v.log.Info("PVC is owned by a VM that is not in the protected VM list",
2988+
"pvc", pvc.Name,
2989+
"vm", vmName,
2990+
"protectedVMs", vmList)
2991+
2992+
return false
2993+
}
2994+
}
2995+
2996+
return yes
2997+
}

0 commit comments

Comments
 (0)