forked from rook/rook
The CRD watcher has been replaced by the new controller-runtime framework. This brings robustness in our operator, meaning that any resources that are modified will be reconciled into the desired state. Closes: https://github.com/rook/rook/issues/4941 Signed-off-by: Sébastien Han <seb@redhat.com>
241 lines
8.3 KiB
Go
241 lines
8.3 KiB
Go
/*
|
|
Copyright 2020 The Rook Authors. All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package controller
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/banzaicloud/k8s-objectmatcher/patch"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/pkg/errors"
|
|
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/event"
|
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
|
)
|
|
|
|
// WatchControllerPredicate is a special update filter for update events
|
|
// do not reconcile if the the status changes, this avoids a reconcile storm loop
|
|
//
|
|
// returning 'true' means triggering a reconciliation
|
|
// returning 'false' means do NOT trigger a reconciliation
|
|
func WatchControllerPredicate() predicate.Funcs {
|
|
return predicate.Funcs{
|
|
CreateFunc: func(e event.CreateEvent) bool {
|
|
logger.Debug("create event from the parent object")
|
|
return true
|
|
},
|
|
DeleteFunc: func(e event.DeleteEvent) bool {
|
|
logger.Debug("delete event from the parent object")
|
|
return true
|
|
},
|
|
UpdateFunc: func(e event.UpdateEvent) bool {
|
|
logger.Debug("update event from the parent object")
|
|
// resource.Quantity has non-exportable fields, so we use its comparator method
|
|
resourceQtyComparer := cmp.Comparer(func(x, y resource.Quantity) bool { return x.Cmp(y) == 0 })
|
|
|
|
switch objOld := e.ObjectOld.(type) {
|
|
case *cephv1.CephObjectStore:
|
|
objNew := e.ObjectNew.(*cephv1.CephObjectStore)
|
|
logger.Debug("update event from the parent object CephObjectStore")
|
|
diff := cmp.Diff(objOld.Spec, objNew.Spec, resourceQtyComparer)
|
|
if diff != "" ||
|
|
objOld.GetDeletionTimestamp() != objNew.GetDeletionTimestamp() ||
|
|
objOld.GetGeneration() != objNew.GetGeneration() {
|
|
// Checking if diff is not empty so we don't print it when the CR gets deleted
|
|
if diff != "" {
|
|
logger.Infof("CR has changed for %q. diff=%s", objNew.Name, diff)
|
|
}
|
|
return true
|
|
}
|
|
|
|
case *cephv1.CephObjectStoreUser:
|
|
objNew := e.ObjectNew.(*cephv1.CephObjectStoreUser)
|
|
logger.Debug("update event from the parent object CephObjectStoreUser")
|
|
diff := cmp.Diff(objOld.Spec, objNew.Spec, resourceQtyComparer)
|
|
if diff != "" ||
|
|
objOld.GetDeletionTimestamp() != objNew.GetDeletionTimestamp() ||
|
|
objOld.GetGeneration() != objNew.GetGeneration() {
|
|
// Checking if diff is not empty so we don't print it when the CR gets deleted
|
|
if diff != "" {
|
|
logger.Infof("CR has changed for %q. diff=%s", objNew.Name, diff)
|
|
}
|
|
return true
|
|
}
|
|
|
|
case *cephv1.CephBlockPool:
|
|
objNew := e.ObjectNew.(*cephv1.CephBlockPool)
|
|
logger.Debug("update event from the parent object CephBlockPool")
|
|
diff := cmp.Diff(objOld.Spec, objNew.Spec, resourceQtyComparer)
|
|
if diff != "" ||
|
|
objOld.GetDeletionTimestamp() != objNew.GetDeletionTimestamp() ||
|
|
objOld.GetGeneration() != objNew.GetGeneration() {
|
|
// Checking if diff is not empty so we don't print it when the CR gets deleted
|
|
if diff != "" {
|
|
logger.Infof("CR has changed for %q. diff=%s", objNew.Name, diff)
|
|
}
|
|
return true
|
|
}
|
|
|
|
case *cephv1.CephFilesystem:
|
|
objNew := e.ObjectNew.(*cephv1.CephFilesystem)
|
|
logger.Debug("update event from the parent object CephFilesystem")
|
|
diff := cmp.Diff(objOld.Spec, objNew.Spec, resourceQtyComparer)
|
|
if diff != "" ||
|
|
objOld.GetDeletionTimestamp() != objNew.GetDeletionTimestamp() ||
|
|
objOld.GetGeneration() != objNew.GetGeneration() {
|
|
// Checking if diff is not empty so we don't print it when the CR gets deleted
|
|
if diff != "" {
|
|
logger.Infof("CR has changed for %q. diff=%s", objNew.Name, diff)
|
|
}
|
|
return true
|
|
}
|
|
|
|
case *cephv1.CephNFS:
|
|
objNew := e.ObjectNew.(*cephv1.CephNFS)
|
|
logger.Debug("update event from the parent object CephNFS")
|
|
diff := cmp.Diff(objOld.Spec, objNew.Spec, resourceQtyComparer)
|
|
if diff != "" ||
|
|
objOld.GetDeletionTimestamp() != objNew.GetDeletionTimestamp() ||
|
|
objOld.GetGeneration() != objNew.GetGeneration() {
|
|
// Checking if diff is not empty so we don't print it when the CR gets deleted
|
|
if diff != "" {
|
|
logger.Infof("CR has changed for %q. diff=%s", objNew.Name, diff)
|
|
}
|
|
return true
|
|
}
|
|
|
|
}
|
|
|
|
logger.Debug("wont update unknown object")
|
|
return false
|
|
},
|
|
GenericFunc: func(e event.GenericEvent) bool {
|
|
return false
|
|
},
|
|
}
|
|
}
|
|
|
|
// objectChanged checks whether the object has been updated
|
|
func objectChanged(oldObj, newObj runtime.Object) (bool, error) {
|
|
var doReconcile bool
|
|
old := oldObj.DeepCopyObject()
|
|
new := newObj.DeepCopyObject()
|
|
|
|
// Set resource version
|
|
accessor := meta.NewAccessor()
|
|
currentResourceVersion, err := accessor.ResourceVersion(old)
|
|
if err == nil {
|
|
accessor.SetResourceVersion(new, currentResourceVersion)
|
|
}
|
|
|
|
// Calculate diff between old and new object
|
|
diff, err := patch.DefaultPatchMaker.Calculate(old, new)
|
|
if err != nil {
|
|
doReconcile = true
|
|
return doReconcile, errors.Wrap(err, "failed to calculate object diff")
|
|
} else if diff.IsEmpty() {
|
|
return doReconcile, nil
|
|
}
|
|
|
|
logger.Debugf("diff is %+v", diff.String())
|
|
return isValidEvent(diff.Patch), nil
|
|
}
|
|
|
|
// WatchPredicateForNonCRDObject is a special filter for create events
|
|
// It only applies to non-CRD objects, meaning, for instance a cephv1.CephBlockPool{}
|
|
// object will not have this filter
|
|
// Only for objects like &v1.Secret{} etc...
|
|
//
|
|
// We return 'false' on a create event so we don't overstep with the main watcher on cephv1.CephBlockPool{}
|
|
// This avoids a double reconcile when the secret gets deleted.
|
|
func WatchPredicateForNonCRDObject(owner runtime.Object, scheme *runtime.Scheme) predicate.Funcs {
|
|
// Initialize the Owner Matcher, which is the main controller object: e.g. cephv1.CephBlockPool{}
|
|
ownerMatcher := NewOwnerReferenceMatcher(owner, scheme)
|
|
|
|
return predicate.Funcs{
|
|
CreateFunc: func(e event.CreateEvent) bool {
|
|
return false
|
|
},
|
|
DeleteFunc: func(e event.DeleteEvent) bool {
|
|
match, object, err := ownerMatcher.Match(e.Object)
|
|
if err != nil {
|
|
logger.Errorf("failed to check if object kind %q matched. %v", e.Object.GetObjectKind(), err)
|
|
}
|
|
if match {
|
|
logger.Debugf("object %q matched on delete", object.GetName())
|
|
return true
|
|
}
|
|
|
|
logger.Debugf("object %q did not match on delete", object.GetName())
|
|
return false
|
|
},
|
|
UpdateFunc: func(e event.UpdateEvent) bool {
|
|
match, object, err := ownerMatcher.Match(e.ObjectNew)
|
|
if err != nil {
|
|
logger.Errorf("failed to check if object matched. %v", err)
|
|
}
|
|
if match {
|
|
logger.Debugf("object %q matched on update", object.GetName())
|
|
objectChanged, err := objectChanged(e.ObjectOld, e.ObjectNew)
|
|
if err != nil {
|
|
logger.Errorf("failed to check if object %q changed. %v", object.GetName(), err)
|
|
}
|
|
return objectChanged
|
|
}
|
|
|
|
logger.Debugf("object %q did not match on update", object.GetName())
|
|
return false
|
|
},
|
|
GenericFunc: func(e event.GenericEvent) bool {
|
|
return false
|
|
},
|
|
}
|
|
}
|
|
|
|
// isValidEvent analyses the diff between two objects events and determines
|
|
// if we should reconcile that event or not
|
|
// The goal is to avoid double-reconcile as much as possible
|
|
func isValidEvent(patch []byte) bool {
|
|
patchString := string(patch)
|
|
|
|
// Seem a bit 'weak' but since we can't get a real struct of the object
|
|
// (unless we use the unstructured package, but that over complicates things)
|
|
// That's probably the most straightforward approach for now...
|
|
//
|
|
// The downscale only shows a "deletionTimestamp" which is not appropriate to catch
|
|
if strings.Contains(patchString, "Created new replica set") {
|
|
logger.Debug("don't reconcile on replicaset addition")
|
|
return false
|
|
}
|
|
|
|
// It looks like there is a diff
|
|
// if the status changed, we do nothing
|
|
var p map[string]interface{}
|
|
json.Unmarshal(patch, &p)
|
|
delete(p, "status")
|
|
if len(p) == 0 {
|
|
return false
|
|
}
|
|
|
|
logger.Infof("will reconcile based on patch %s", patchString)
|
|
return true
|
|
}
|