diff --git a/Gopkg.lock b/Gopkg.lock index 8a9b7f4ab3..866f525874 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1487,8 +1487,10 @@ "sigs.k8s.io/controller-runtime/pkg/client/fake", "sigs.k8s.io/controller-runtime/pkg/controller", "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil", + "sigs.k8s.io/controller-runtime/pkg/event", "sigs.k8s.io/controller-runtime/pkg/handler", "sigs.k8s.io/controller-runtime/pkg/manager", + "sigs.k8s.io/controller-runtime/pkg/predicate", "sigs.k8s.io/controller-runtime/pkg/reconcile", "sigs.k8s.io/controller-runtime/pkg/runtime/log", "sigs.k8s.io/controller-runtime/pkg/runtime/scheme", diff --git a/hack/tests/e2e-helm.sh b/hack/tests/e2e-helm.sh index e2fefff5c9..f47b844ebf 100755 --- a/hack/tests/e2e-helm.sh +++ b/hack/tests/e2e-helm.sh @@ -63,13 +63,13 @@ fi # create CR kubectl create -f deploy/crds/helm_v1alpha1_nginx_cr.yaml trap_add 'kubectl delete --ignore-not-found -f ${DIR2}/deploy/crds/helm_v1alpha1_nginx_cr.yaml' EXIT -if ! timeout 1m bash -c -- 'until kubectl get nginxes.helm.example.com example-nginx -o jsonpath="{..status.release.info.status.code}" | grep 1; do sleep 1; done'; +if ! timeout 1m bash -c -- 'until kubectl get nginxes.helm.example.com example-nginx -o jsonpath="{..status.conditions[1].release.info.status.code}" | grep 1; do sleep 1; done'; then kubectl logs deployment/nginx-operator exit 1 fi -release_name=$(kubectl get nginxes.helm.example.com example-nginx -o jsonpath="{..status.release.name}") +release_name=$(kubectl get nginxes.helm.example.com example-nginx -o jsonpath="{..status.conditions[1].release.name}") nginx_deployment=$(kubectl get deployment -l "app.kubernetes.io/instance=${release_name}" -o jsonpath="{..metadata.name}") if ! timeout 1m kubectl rollout status deployment/${nginx_deployment}; diff --git a/pkg/ansible/controller/controller.go b/pkg/ansible/controller/controller.go index 9dc53c86b6..0909bf924c 100644 --- a/pkg/ansible/controller/controller.go +++ b/pkg/ansible/controller/controller.go @@ -22,6 +22,7 @@ import ( "github.com/operator-framework/operator-sdk/pkg/ansible/events" "github.com/operator-framework/operator-sdk/pkg/ansible/runner" + "github.com/operator-framework/operator-sdk/pkg/predicate" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -79,7 +80,7 @@ func Add(mgr manager.Manager, options Options) { } u := &unstructured.Unstructured{} u.SetGroupVersionKind(options.GVK) - if err := c.Watch(&source.Kind{Type: u}, &crthandler.EnqueueRequestForObject{}); err != nil { + if err := c.Watch(&source.Kind{Type: u}, &crthandler.EnqueueRequestForObject{}, predicate.GenerationChangedPredicate{}); err != nil { log.Error(err, "") os.Exit(1) } diff --git a/pkg/helm/controller/controller.go b/pkg/helm/controller/controller.go index 48dca8ece6..eacff406eb 100644 --- a/pkg/helm/controller/controller.go +++ b/pkg/helm/controller/controller.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" "github.com/operator-framework/operator-sdk/pkg/helm/release" + "github.com/operator-framework/operator-sdk/pkg/predicate" ) var log = logf.Log.WithName("helm.controller") @@ -66,7 +67,7 @@ func Add(mgr manager.Manager, options WatchOptions) error { o := &unstructured.Unstructured{} o.SetGroupVersionKind(options.GVK) - if err := c.Watch(&source.Kind{Type: o}, &crthandler.EnqueueRequestForObject{}); err != nil { + if err := c.Watch(&source.Kind{Type: o}, &crthandler.EnqueueRequestForObject{}, predicate.GenerationChangedPredicate{}); err != nil { return err } diff --git a/pkg/helm/controller/reconcile.go b/pkg/helm/controller/reconcile.go index 7f181eacad..ad4527b340 100644 --- a/pkg/helm/controller/reconcile.go +++ b/pkg/helm/controller/reconcile.go @@ -80,14 +80,29 @@ func (r HelmOperatorReconciler) Reconcile(request reconcile.Request) (reconcile. log.V(1).Info("Adding finalizer", "finalizer", finalizer) finalizers := append(pendingFinalizers, finalizer) o.SetFinalizers(finalizers) - err := r.Client.Update(context.TODO(), o) - return reconcile.Result{}, err + err = r.updateResource(o) + + // Need to requeue because finalizer update does not change metadata.generation + return reconcile.Result{Requeue: true}, err } + status.SetCondition(types.HelmAppCondition{ + Type: types.ConditionInitialized, + Status: types.StatusTrue, + }) + if err := manager.Sync(context.TODO()); err != nil { log.Error(err, "failed to sync release") + status.SetCondition(types.HelmAppCondition{ + Type: types.ConditionIrreconcilable, + Status: types.StatusTrue, + Reason: types.ReasonReconcileError, + Message: err.Error(), + }) + _ = r.updateResourceStatus(o, status) return reconcile.Result{}, err } + status.RemoveCondition(types.ConditionIrreconcilable) if deleted { if !contains(pendingFinalizers, finalizer) { @@ -98,8 +113,17 @@ func (r HelmOperatorReconciler) Reconcile(request reconcile.Request) (reconcile. uninstalledRelease, err := manager.UninstallRelease(context.TODO()) if err != nil && err != release.ErrNotFound { log.Error(err, "failed to uninstall release") + status.SetCondition(types.HelmAppCondition{ + Type: types.ConditionReleaseFailed, + Status: types.StatusTrue, + Reason: types.ReasonUninstallError, + Message: err.Error(), + }) + _ = r.updateResourceStatus(o, status) return reconcile.Result{}, err } + status.RemoveCondition(types.ConditionReleaseFailed) + if err == release.ErrNotFound { log.Info("Release not found, removing finalizer") } else { @@ -107,7 +131,16 @@ func (r HelmOperatorReconciler) Reconcile(request reconcile.Request) (reconcile. if log.Enabled() { fmt.Println(diffutil.Diff(uninstalledRelease.GetManifest(), "")) } + status.SetCondition(types.HelmAppCondition{ + Type: types.ConditionDeployed, + Status: types.StatusFalse, + Reason: types.ReasonUninstallSuccessful, + }) + } + if err := r.updateResourceStatus(o, status); err != nil { + return reconcile.Result{}, err } + finalizers := []string{} for _, pendingFinalizer := range pendingFinalizers { if pendingFinalizer != finalizer { @@ -115,24 +148,40 @@ func (r HelmOperatorReconciler) Reconcile(request reconcile.Request) (reconcile. } } o.SetFinalizers(finalizers) - err = r.Client.Update(context.TODO(), o) - return reconcile.Result{}, err + err = r.updateResource(o) + + // Need to requeue because finalizer update does not change metadata.generation + return reconcile.Result{Requeue: true}, err } if !manager.IsInstalled() { installedRelease, err := manager.InstallRelease(context.TODO()) if err != nil { log.Error(err, "failed to install release") + status.SetCondition(types.HelmAppCondition{ + Type: types.ConditionReleaseFailed, + Status: types.StatusTrue, + Reason: types.ReasonInstallError, + Message: err.Error(), + Release: installedRelease, + }) + _ = r.updateResourceStatus(o, status) return reconcile.Result{}, err } + status.RemoveCondition(types.ConditionReleaseFailed) log.Info("Installed release") if log.Enabled() { fmt.Println(diffutil.Diff("", installedRelease.GetManifest())) } log.V(1).Info("Config values", "values", installedRelease.GetConfig()) - status.SetRelease(installedRelease) - status.SetPhase(types.PhaseApplied, types.ReasonApplySuccessful, installedRelease.GetInfo().GetStatus().GetNotes()) + status.SetCondition(types.HelmAppCondition{ + Type: types.ConditionDeployed, + Status: types.StatusTrue, + Reason: types.ReasonInstallSuccessful, + Message: installedRelease.GetInfo().GetStatus().GetNotes(), + Release: installedRelease, + }) err = r.updateResourceStatus(o, status) return reconcile.Result{RequeueAfter: r.ResyncPeriod}, err } @@ -141,15 +190,30 @@ func (r HelmOperatorReconciler) Reconcile(request reconcile.Request) (reconcile. previousRelease, updatedRelease, err := manager.UpdateRelease(context.TODO()) if err != nil { log.Error(err, "failed to update release") + status.SetCondition(types.HelmAppCondition{ + Type: types.ConditionReleaseFailed, + Status: types.StatusTrue, + Reason: types.ReasonUpdateError, + Message: err.Error(), + Release: updatedRelease, + }) + _ = r.updateResourceStatus(o, status) return reconcile.Result{}, err } + status.RemoveCondition(types.ConditionReleaseFailed) + log.Info("Updated release") if log.Enabled() { fmt.Println(diffutil.Diff(previousRelease.GetManifest(), updatedRelease.GetManifest())) } log.V(1).Info("Config values", "values", updatedRelease.GetConfig()) - status.SetRelease(updatedRelease) - status.SetPhase(types.PhaseApplied, types.ReasonApplySuccessful, updatedRelease.GetInfo().GetStatus().GetNotes()) + status.SetCondition(types.HelmAppCondition{ + Type: types.ConditionDeployed, + Status: types.StatusTrue, + Reason: types.ReasonUpdateSuccessful, + Message: updatedRelease.GetInfo().GetStatus().GetNotes(), + Release: updatedRelease, + }) err = r.updateResourceStatus(o, status) return reconcile.Result{RequeueAfter: r.ResyncPeriod}, err } @@ -157,11 +221,24 @@ func (r HelmOperatorReconciler) Reconcile(request reconcile.Request) (reconcile. _, err = manager.ReconcileRelease(context.TODO()) if err != nil { log.Error(err, "failed to reconcile release") + status.SetCondition(types.HelmAppCondition{ + Type: types.ConditionIrreconcilable, + Status: types.StatusTrue, + Reason: types.ReasonReconcileError, + Message: err.Error(), + }) + _ = r.updateResourceStatus(o, status) return reconcile.Result{}, err } + status.RemoveCondition(types.ConditionIrreconcilable) log.Info("Reconciled release") - return reconcile.Result{RequeueAfter: r.ResyncPeriod}, nil + err = r.updateResourceStatus(o, status) + return reconcile.Result{RequeueAfter: r.ResyncPeriod}, err +} + +func (r HelmOperatorReconciler) updateResource(o *unstructured.Unstructured) error { + return r.Client.Update(context.TODO(), o) } func (r HelmOperatorReconciler) updateResourceStatus(o *unstructured.Unstructured, status *types.HelmAppStatus) error { diff --git a/pkg/helm/engine/ownerref.go b/pkg/helm/engine/ownerref.go index 3a9be56ddf..7b7b5eaa8c 100644 --- a/pkg/helm/engine/ownerref.go +++ b/pkg/helm/engine/ownerref.go @@ -48,6 +48,9 @@ func (o *OwnerRefEngine) Render(chart *chart.Chart, values chartutil.Values) (ma ownedRenderedFiles := map[string]string{} for fileName, renderedFile := range rendered { if !strings.HasSuffix(fileName, ".yaml") { + // Pass non-YAML files through untouched. + // This is required for NOTES.txt + ownedRenderedFiles[fileName] = renderedFile continue } withOwner, err := o.addOwnerRefs(renderedFile) diff --git a/pkg/helm/internal/types/types.go b/pkg/helm/internal/types/types.go index b8a2532ee1..8d6789bb3e 100644 --- a/pkg/helm/internal/types/types.go +++ b/pkg/helm/internal/types/types.go @@ -38,32 +38,41 @@ type HelmApp struct { type HelmAppSpec map[string]interface{} -type ResourcePhase string +type HelmAppConditionType string +type ConditionStatus string +type HelmAppConditionReason string -const ( - PhaseNone ResourcePhase = "" - PhaseApplying ResourcePhase = "Applying" - PhaseApplied ResourcePhase = "Applied" - PhaseFailed ResourcePhase = "Failed" -) +type HelmAppCondition struct { + Type HelmAppConditionType `json:"type"` + Status ConditionStatus `json:"status"` + Reason HelmAppConditionReason `json:"reason,omitempty"` + Message string `json:"message,omitempty"` + Release *release.Release `json:"release,omitempty"` -type ConditionReason string + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` +} const ( - ReasonUnknown ConditionReason = "Unknown" - ReasonCustomResourceAdded ConditionReason = "CustomResourceAdded" - ReasonCustomResourceUpdated ConditionReason = "CustomResourceUpdated" - ReasonApplySuccessful ConditionReason = "ApplySuccessful" - ReasonApplyFailed ConditionReason = "ApplyFailed" + ConditionInitialized HelmAppConditionType = "Initialized" + ConditionDeployed HelmAppConditionType = "Deployed" + ConditionReleaseFailed HelmAppConditionType = "ReleaseFailed" + ConditionIrreconcilable HelmAppConditionType = "Irreconcilable" + + StatusTrue ConditionStatus = "True" + StatusFalse ConditionStatus = "False" + StatusUnknown ConditionStatus = "Unknown" + + ReasonInstallSuccessful HelmAppConditionReason = "InstallSuccessful" + ReasonUpdateSuccessful HelmAppConditionReason = "UpdateSuccessful" + ReasonUninstallSuccessful HelmAppConditionReason = "UninstallSuccessful" + ReasonInstallError HelmAppConditionReason = "InstallError" + ReasonUpdateError HelmAppConditionReason = "UpdateError" + ReasonReconcileError HelmAppConditionReason = "ReconcileError" + ReasonUninstallError HelmAppConditionReason = "UninstallError" ) type HelmAppStatus struct { - Release *release.Release `json:"release"` - Phase ResourcePhase `json:"phase"` - Reason ConditionReason `json:"reason,omitempty"` - Message string `json:"message,omitempty"` - LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"` - LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + Conditions []HelmAppCondition `json:"conditions"` } func (s *HelmAppStatus) ToMap() (map[string]interface{}, error) { @@ -76,37 +85,53 @@ func (s *HelmAppStatus) ToMap() (map[string]interface{}, error) { return out, nil } -// SetPhase takes a custom resource status and returns the updated status, without updating the resource in the cluster. -func (s *HelmAppStatus) SetPhase(phase ResourcePhase, reason ConditionReason, message string) *HelmAppStatus { - s.LastUpdateTime = metav1.Now() - if s.Phase != phase { - s.Phase = phase - s.LastTransitionTime = metav1.Now() +// SetCondition sets a condition on the status object. If the condition already +// exists, it will be replaced. SetCondition does not update the resource in +// the cluster. +func (s *HelmAppStatus) SetCondition(condition HelmAppCondition) *HelmAppStatus { + now := metav1.Now() + for i := range s.Conditions { + if s.Conditions[i].Type == condition.Type { + if s.Conditions[i].Status != condition.Status { + condition.LastTransitionTime = now + } else { + condition.LastTransitionTime = s.Conditions[i].LastTransitionTime + } + s.Conditions[i] = condition + return s + } } - s.Message = message - s.Reason = reason + + // If the condition does not exist, + // initialize the lastTransitionTime + condition.LastTransitionTime = now + s.Conditions = append(s.Conditions, condition) return s } -// SetRelease takes a release object and adds or updates the release on the status object -func (s *HelmAppStatus) SetRelease(release *release.Release) *HelmAppStatus { - s.Release = release +// RemoveCondition removes the condition with the passed condition type from +// the status object. If the condition is not already present, the returned +// status object is returned unchanged. RemoveCondition does not update the +// resource in the cluster. +func (s *HelmAppStatus) RemoveCondition(conditionType HelmAppConditionType) *HelmAppStatus { + for i := range s.Conditions { + if s.Conditions[i].Type == conditionType { + s.Conditions = append(s.Conditions[:i], s.Conditions[i+1:]...) + return s + } + } return s } // StatusFor safely returns a typed status block from a custom resource. func StatusFor(cr *unstructured.Unstructured) *HelmAppStatus { - switch cr.Object["status"].(type) { + switch s := cr.Object["status"].(type) { case *HelmAppStatus: - return cr.Object["status"].(*HelmAppStatus) + return s case map[string]interface{}: var status *HelmAppStatus - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(cr.Object["status"].(map[string]interface{}), &status); err != nil { - return &HelmAppStatus{ - Phase: PhaseFailed, - Reason: ReasonApplyFailed, - Message: err.Error(), - } + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(s, &status); err != nil { + return &HelmAppStatus{} } return status default: diff --git a/pkg/helm/internal/types/types_test.go b/pkg/helm/internal/types/types_test.go index 0083675067..65810f9141 100644 --- a/pkg/helm/internal/types/types_test.go +++ b/pkg/helm/internal/types/types_test.go @@ -30,15 +30,38 @@ const ( var now = metav1.Now() -func TestSetPhase(t *testing.T) { - newStatus, err := newTestStatus().SetPhase(PhaseApplying, ReasonCustomResourceUpdated, "working on it").ToMap() +func TestSetCondition(t *testing.T) { + message := "uninstall was successful" + releaseName := "TestRelease" + newStatus, err := newTestStatus().SetCondition(HelmAppCondition{ + Type: ConditionDeployed, + Status: StatusFalse, + Reason: ReasonUninstallSuccessful, + Message: message, + Release: &release.Release{Name: releaseName}, + }).ToMap() assert.NoError(t, err) - assert.Equal(t, string(PhaseApplying), newStatus["phase"]) - assert.Equal(t, string(ReasonCustomResourceUpdated), newStatus["reason"]) - assert.Equal(t, "working on it", newStatus["message"]) - assert.NotEqual(t, metav1.Now(), newStatus["lastUpdateTime"]) - assert.NotEqual(t, metav1.Now(), newStatus["lastTransitionTime"]) + resource := newTestResource() + resource.Object["status"] = newStatus + actual := StatusFor(resource) + + assert.Equal(t, ConditionDeployed, actual.Conditions[0].Type) + assert.Equal(t, StatusFalse, actual.Conditions[0].Status) + assert.Equal(t, ReasonUninstallSuccessful, actual.Conditions[0].Reason) + assert.Equal(t, message, actual.Conditions[0].Message) + assert.Equal(t, releaseName, actual.Conditions[0].Release.Name) + assert.NotEqual(t, metav1.Now(), actual.Conditions[0].LastTransitionTime) +} +func TestRemoveCondition(t *testing.T) { + newStatus, err := newTestStatus().RemoveCondition(ConditionDeployed).ToMap() + assert.NoError(t, err) + + resource := newTestResource() + resource.Object["status"] = newStatus + actual := StatusFor(resource) + + assert.Empty(t, actual.Conditions) } func TestStatusForEmpty(t *testing.T) { @@ -52,9 +75,7 @@ func TestStatusForFilled(t *testing.T) { expectedResource.Object["status"] = newTestStatus() status := StatusFor(expectedResource) - assert.EqualValues(t, newTestStatus().Phase, status.Phase) - assert.EqualValues(t, newTestStatus().Reason, status.Reason) - assert.EqualValues(t, newTestStatus().Message, status.Message) + assert.EqualValues(t, newTestStatus().Conditions, status.Conditions) } func TestStatusForFilledRaw(t *testing.T) { @@ -62,16 +83,12 @@ func TestStatusForFilledRaw(t *testing.T) { expectedResource.Object["status"] = newTestStatusRaw() status := StatusFor(expectedResource) - assert.EqualValues(t, newTestStatus().Phase, status.Phase) - assert.EqualValues(t, newTestStatus().Reason, status.Reason) - assert.EqualValues(t, newTestStatus().Message, status.Message) -} - -func TestSetRelease(t *testing.T) { - releaseName := "TestRelease" - release := release.Release{Name: releaseName} - newStatus := newTestStatus().SetRelease(&release) - assert.EqualValues(t, newStatus.Release.Name, releaseName) + assert.Equal(t, ConditionDeployed, status.Conditions[0].Type) + assert.Equal(t, StatusTrue, status.Conditions[0].Status) + assert.Equal(t, ReasonInstallSuccessful, status.Conditions[0].Reason) + assert.Equal(t, "some message", status.Conditions[0].Message) + assert.Equal(t, "SomeRelease", status.Conditions[0].Release.Name) + assert.NotEqual(t, metav1.Now(), status.Conditions[0].LastTransitionTime) } func newTestResource() *unstructured.Unstructured { @@ -94,20 +111,30 @@ func newTestResource() *unstructured.Unstructured { func newTestStatus() *HelmAppStatus { return &HelmAppStatus{ - Phase: PhaseApplied, - Reason: ReasonApplySuccessful, - Message: "some message", - LastUpdateTime: now, - LastTransitionTime: now, + Conditions: []HelmAppCondition{ + { + Type: ConditionDeployed, + Status: StatusTrue, + Reason: ReasonInstallSuccessful, + Message: "some message", + Release: &release.Release{Name: "SomeRelease"}, + LastTransitionTime: now, + }, + }, } } func newTestStatusRaw() map[string]interface{} { return map[string]interface{}{ - "phase": PhaseApplied, - "reason": ReasonApplySuccessful, - "message": "some message", - "lastUpdateTime": now.UTC(), - "lastTransitionTime": now.UTC(), + "conditions": []map[string]interface{}{ + { + "type": "Deployed", + "status": "True", + "reason": "InstallSuccessful", + "message": "some message", + "release": map[string]interface{}{"name": "SomeRelease"}, + "lastTransitionTime": now.UTC(), + }, + }, } } diff --git a/pkg/helm/release/manager.go b/pkg/helm/release/manager.go index 9073ce269e..1665c7de21 100644 --- a/pkg/helm/release/manager.go +++ b/pkg/helm/release/manager.go @@ -146,12 +146,19 @@ func (m *manager) Sync(ctx context.Context) error { } func (m manager) syncReleaseStatus(status types.HelmAppStatus) error { - if status.Release == nil { + var release *rpb.Release + for _, condition := range status.Conditions { + if condition.Type == types.ConditionDeployed && condition.Status == types.StatusTrue { + release = condition.Release + break + } + } + if release == nil { return nil } - name := status.Release.GetName() - version := status.Release.GetVersion() + name := release.GetName() + version := release.GetVersion() _, err := m.storageBackend.Get(name, version) if err == nil { return nil @@ -160,7 +167,7 @@ func (m manager) syncReleaseStatus(status types.HelmAppStatus) error { if !notFoundErr(err) { return err } - return m.storageBackend.Create(status.Release) + return m.storageBackend.Create(release) } func notFoundErr(err error) bool { diff --git a/pkg/predicate/predicate.go b/pkg/predicate/predicate.go new file mode 100644 index 0000000000..48093ac46d --- /dev/null +++ b/pkg/predicate/predicate.go @@ -0,0 +1,53 @@ +// Copyright 2018 The Operator-SDK Authors +// +// 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 predicate + +import ( + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" +) + +var log = logf.Log.WithName("predicate").WithName("eventFilters") + +// GenerationChangedPredicate implements a default update predicate function on generation change +// (adapted from sigs.k8s.io/controller-runtime/pkg/predicate/predicate.ResourceVersionChangedPredicate) +type GenerationChangedPredicate struct { + predicate.Funcs +} + +// Update implements default UpdateEvent filter for validating generation change +func (GenerationChangedPredicate) Update(e event.UpdateEvent) bool { + if e.MetaOld == nil { + log.Error(nil, "update event has no old metadata", "event", e) + return false + } + if e.ObjectOld == nil { + log.Error(nil, "update event has no old runtime object to update", "event", e) + return false + } + if e.ObjectNew == nil { + log.Error(nil, "update event has no new runtime object for update", "event", e) + return false + } + if e.MetaNew == nil { + log.Error(nil, "update event has no new metadata", "event", e) + return false + } + if e.MetaNew.GetGeneration() == e.MetaOld.GetGeneration() { + return false + } + return true +}