Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions config/dr-cluster/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ rules:
- ""
resources:
- configmaps
- persistentvolumes
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
Expand All @@ -33,17 +38,6 @@ rules:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- persistentvolumes
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
Expand Down
16 changes: 5 additions & 11 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ rules:
- ""
resources:
- configmaps
- persistentvolumes
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
Expand Down Expand Up @@ -43,17 +48,6 @@ rules:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- persistentvolumes
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
Expand Down
34 changes: 26 additions & 8 deletions internal/controller/controllers_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,11 +341,6 @@ func drclusterConditionExpect(
}

func validateClusterManifest(apiReader client.Reader, drcluster *ramen.DRCluster, disabled bool) {
expectedCount := 9
if disabled {
expectedCount = 4
}

clusterName := drcluster.Name

key := types.NamespacedName{
Expand All @@ -356,12 +351,35 @@ func validateClusterManifest(apiReader client.Reader, drcluster *ramen.DRCluster
manifestWork := &workv1.ManifestWork{}

Eventually(
func(g Gomega) []workv1.Manifest {
func(g Gomega) {
g.Expect(apiReader.Get(context.TODO(), key, manifestWork)).To(Succeed())

return manifestWork.Spec.Workload.Manifests
// Validate base ClusterRoles are always present (5 manifests)
g.Expect(manifestWork.Spec.Workload.Manifests).To(ContainElement(
HaveField("RawExtension.Raw", ContainSubstring("volrepgroup-edit"))))
g.Expect(manifestWork.Spec.Workload.Manifests).To(ContainElement(
HaveField("RawExtension.Raw", ContainSubstring("mmode-edit"))))
g.Expect(manifestWork.Spec.Workload.Manifests).To(ContainElement(
HaveField("RawExtension.Raw", ContainSubstring("drclusterconfig-edit"))))
g.Expect(manifestWork.Spec.Workload.Manifests).To(ContainElement(
HaveField("RawExtension.Raw", ContainSubstring("networkfence-edit"))))
g.Expect(manifestWork.Spec.Workload.Manifests).To(ContainElement(
HaveField("RawExtension.Raw", ContainSubstring("recipe-edit"))))

if !disabled {
// When deployment automation is enabled, validate additional manifests (5 more)
g.Expect(manifestWork.Spec.Workload.Manifests).To(ContainElement(
HaveField("RawExtension.Raw", ContainSubstring("olm-edit"))))
g.Expect(manifestWork.Spec.Workload.Manifests).To(ContainElement(
HaveField("RawExtension.Raw", ContainSubstring("OperatorGroup"))))
g.Expect(manifestWork.Spec.Workload.Manifests).To(ContainElement(
HaveField("RawExtension.Raw", ContainSubstring("Subscription"))))
// Two Namespace manifests
g.Expect(manifestWork.Spec.Workload.Manifests).To(ContainElement(
HaveField("RawExtension.Raw", And(ContainSubstring("Namespace"), ContainSubstring("ramen")))))
}
}, timeout, interval,
).Should(HaveLen(expectedCount))
).Should(Succeed())

Expect(manifestWork.GetAnnotations()[controllers.DRClusterNameAnnotation]).Should(Equal(clusterName))
// TODO: Validate fencing status
Expand Down
98 changes: 98 additions & 0 deletions internal/controller/drplacementcontrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import (
"time"

"github.com/go-logr/logr"
recipev1 "github.com/ramendr/recipe/api/v1alpha1"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
clrapiv1beta1 "open-cluster-management.io/api/cluster/v1beta1"
ocmworkv1 "open-cluster-management.io/api/work/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

rmn "github.com/ramendr/ramen/api/v1alpha1"
Expand Down Expand Up @@ -3158,3 +3160,99 @@ func (d *DRPCInstance) filterSCCAnnotations(annotations map[string]string) (map[

return filteredAnnotations, nil
}

func (d *DRPCInstance) ensureRecipeManifestWork(srcCluster, dstCluster string) error {
if d.instance.Spec.KubeObjectProtection == nil || d.instance.Spec.KubeObjectProtection.RecipeRef == nil {
return nil
}

recipeName := d.instance.Spec.KubeObjectProtection.RecipeRef.Name
recipeNamespace := d.instance.Spec.KubeObjectProtection.RecipeRef.Namespace

// Always fetch the latest recipe from source cluster
recipe, err := d.reconciler.MCVGetter.GetRecipeFromManagedCluster(srcCluster, recipeName, recipeNamespace)
if err != nil {
return fmt.Errorf("failed to get recipe %s/%s in cluster %s: %w", recipeNamespace, recipeName, srcCluster, err)
}

// Check if recipe MW exists on destination
existingMW, err := d.mwu.FindManifestWorkByType(rmnutil.MWTypeRecipe, dstCluster)
if err != nil {
if !k8serrors.IsNotFound(err) {
return fmt.Errorf("failed to get recipe MW in cluster %s: %w", dstCluster, err)
}
// MW doesn't exist, create it
return d.mwu.CreateOrUpdateRecipeManifestWork(recipe, dstCluster)
}

// MW exists, check if recipe needs update by comparing Generation or ResourceVersion
// Extract existing recipe from ManifestWork and compare
if needsUpdate, err := d.recipeNeedsUpdate(existingMW, recipe); err != nil {
return fmt.Errorf("failed to check if recipe needs update: %w", err)
} else if needsUpdate {
d.log.Info("Recipe has been updated, updating ManifestWork",
"recipe", recipeName,
"namespace", recipeNamespace,
"srcCluster", srcCluster,
"dstCluster", dstCluster,
"generation", recipe.Generation,
"resourceVersion", recipe.ResourceVersion)

return d.mwu.CreateOrUpdateRecipeManifestWork(recipe, dstCluster)
}

return nil
}

// recipeNeedsUpdate compares the recipe in the ManifestWork with the source recipe
// Returns true if the recipe needs to be updated
func (d *DRPCInstance) recipeNeedsUpdate(mw *ocmworkv1.ManifestWork, sourceRecipe *recipev1.Recipe) (bool, error) {
if mw == nil || len(mw.Spec.Workload.Manifests) == 0 {
return true, nil
}

// Extract recipe from ManifestWork
existingRecipe := &recipev1.Recipe{}
if err := rmnutil.ExtractResourceFromManifestWork(mw, existingRecipe,
schema.GroupVersionKind{
Group: recipev1.GroupVersion.Group,
Version: recipev1.GroupVersion.Version,
Kind: "Recipe",
}); err != nil {
return false, fmt.Errorf("failed to extract recipe from ManifestWork: %w", err)
}

// Compare Spec - this is the most important part
if !reflect.DeepEqual(existingRecipe.Spec, sourceRecipe.Spec) {
d.log.V(1).Info("Recipe Spec has changed", "recipe", sourceRecipe.Name)

return true, nil
}

// Compare Labels - only if both have labels
// Handle nil vs empty map equivalence
if !mapsEqual(existingRecipe.Labels, sourceRecipe.Labels) {
d.log.V(1).Info("Recipe Labels have changed", "recipe", sourceRecipe.Name)

return true, nil
}

// Compare Annotations - only if both have annotations
// Handle nil vs empty map equivalence
if !mapsEqual(existingRecipe.Annotations, sourceRecipe.Annotations) {
d.log.V(1).Info("Recipe Annotations have changed", "recipe", sourceRecipe.Name)

return true, nil
}

return false, nil
}

// mapsEqual compares two string maps, treating nil and empty maps as equivalent
func mapsEqual(a, b map[string]string) bool {
if len(a) == 0 && len(b) == 0 {
return true
}

return reflect.DeepEqual(a, b)
}
36 changes: 13 additions & 23 deletions internal/controller/drplacementcontrol_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ const (

var ErrInitialWaitTimeForDRPCPlacementRule = errors.New("waiting for DRPC Placement to produces placement decision")

// Extend this metric to other DR progression(rmn.ProgressionState) states by adding them to the `states` slice.
var trackedDRProgressionStates = []string{
string(rmn.ProgressionWaitOnUserToCleanUp),
}

// ProgressCallback of function type
type ProgressCallback func(string, string)

Expand Down Expand Up @@ -419,15 +424,10 @@ func (r *DRPlacementControlReconciler) setDRProgressionStateMetric(drpc *rmn.DRP

log.Info(fmt.Sprintf("setting metric: (%s)", DRProgressionState))

// Extend this metric to other DR progression(rmn.ProgressionState) states by adding them to the `states` slice.
states := []string{
string(rmn.ProgressionWaitOnUserToCleanUp),
}

currentState := string(drpc.Status.Progression)
for _, state := range states {
labels := DRProgressionStateMetricLabels(drpc)
if state == currentState {
for _, trackedState := range trackedDRProgressionStates {
labels := DRProgressionStateMetricLabels(drpc, trackedState)
if trackedState == currentState {
drpcProgressionState.With(labels).Set(1)
} else {
drpcProgressionState.With(labels).Set(0)
Expand Down Expand Up @@ -556,17 +556,6 @@ func (r *DRPlacementControlReconciler) createCGEnabledMetricsInstance(
}
}

func (r *DRPlacementControlReconciler) createDRProgressionStateMetricsInstance(
drpc *rmn.DRPlacementControl,
) *DRProgressionStateMetrics {
drProgressionStateLabels := DRProgressionStateMetricLabels(drpc)
drProgressionStateMetrics := NewDRPCProgressionStateMetric(drProgressionStateLabels)

return &DRProgressionStateMetrics{
DRProgressionState: drProgressionStateMetrics.DRProgressionState,
}
}

func (r *DRPlacementControlReconciler) createGlobalActionMetricsInstance(
drpc *rmn.DRPlacementControl,
) *GlobalActionMetrics {
Expand Down Expand Up @@ -852,8 +841,10 @@ func (r *DRPlacementControlReconciler) finalizeDRPC(ctx context.Context, drpc *r
cgEnabledMetricLabels := CGEnabledMetricLabels(drpc)
DeleteCGEnabledMetric(cgEnabledMetricLabels)

DRProgressionStateMetricsLabels := DRProgressionStateMetricLabels(drpc)
DeleteDRPCProgressionStateMetric(DRProgressionStateMetricsLabels)
for _, trackedState := range trackedDRProgressionStates {
labels := DRProgressionStateMetricLabels(drpc, trackedState)
DeleteDRPCProgressionStateMetric(labels)
}

globalActionLabels := GlobalActionLabels(drpc)
DeleteGlobalActionMetric(globalActionLabels)
Expand Down Expand Up @@ -1769,8 +1760,7 @@ func (r *DRPlacementControlReconciler) setDRPCMetrics(ctx context.Context,
r.setLastSyncBytesMetric(&syncMetrics.SyncDataBytesMetrics, drpc.Status.LastGroupSyncBytes, log)
}

appDRCleanupMetrics := r.createDRProgressionStateMetricsInstance(drpc)
r.setDRProgressionStateMetric(drpc, appDRCleanupMetrics, log)
r.setDRProgressionStateMetric(drpc, &DRProgressionStateMetrics{}, log)

return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,7 @@ func verifyVRGManifestWorkCreatedAsPrimary(namespace, managedCluster string) {
return err == nil
}, timeout, interval).Should(BeTrue())

Expect(len(createdVRGRolesManifest.Spec.Workload.Manifests)).To(Equal(9))
Expect(len(createdVRGRolesManifest.Spec.Workload.Manifests)).To(Equal(10))

vrgClusterRoleManifest := createdVRGRolesManifest.Spec.Workload.Manifests[0]
Expect(vrgClusterRoleManifest).ToNot(BeNil())
Expand Down
7 changes: 7 additions & 0 deletions internal/controller/drplacementcontrolvolsync.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,13 @@ func (d *DRPCInstance) createOrUpdateSecondaryManifestWork(srcCluster string) (c
d.instance.Namespace, dstCluster)
}

err = d.ensureRecipeManifestWork(srcCluster, dstCluster)
if err != nil {
return ctrlutil.OperationResultNone,
fmt.Errorf("creating ManifestWork couldn't ensure recipe on cluster %s exists",
dstCluster)
}

annotations := make(map[string]string)

annotations[DRPCNameAnnotation] = d.instance.Name
Expand Down
7 changes: 7 additions & 0 deletions internal/controller/fake_mcv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
volrep "github.com/csi-addons/kubernetes-csi-addons/api/replication.storage/v1alpha1"
"github.com/go-logr/logr"
snapv1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
recipev1 "github.com/ramendr/recipe/api/v1alpha1"
groupsnapv1beta1 "github.com/red-hat-storage/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -202,3 +203,9 @@ func (f FakeMCVGetter) DeleteManagedClusterView(clusterName, mcvName string, log
func (f FakeMCVGetter) GetNSFromManagedCluster(managedCluster, resourceName string) (*corev1.Namespace, error) {
return nil, nil
}

func (f FakeMCVGetter) GetRecipeFromManagedCluster(managedCluster, resourceName,
resourceNamespace string,
) (*recipev1.Recipe, error) {
return nil, nil
}
Loading
Loading