From ce00e7a0bab37bf07c4d7a40d65e4b4151006154 Mon Sep 17 00:00:00 2001 From: Thomas Polkowski Date: Thu, 23 Nov 2023 18:18:28 -0500 Subject: [PATCH] BOP-85/87 Add events & status to Addons & Manifests Refactor AddonStatus & ManifestStatus to use common Status; Add unhealthy status type BOP-85/BOP-87 Cleanup imports Add predicate to prevent infinite reconcilations Improve manifest event messages; Add delay for requeues due to issues reaching manifest url --- api/v1alpha1/addon_types.go | 36 ++++++++++ api/v1alpha1/manifest_types.go | 3 + api/v1alpha1/zz_generated.deepcopy.go | 22 +++++- .../bases/boundless.mirantis.com_addons.yaml | 26 ++++++- .../boundless.mirantis.com_manifests.yaml | 26 ++++++- config/rbac/role.yaml | 7 ++ controllers/addon_controller.go | 54 ++++++++++++++- controllers/manifest_controller.go | 67 +++++++++++++++++-- main.go | 10 +-- pkg/event/event.go | 10 +++ 10 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 pkg/event/event.go diff --git a/api/v1alpha1/addon_types.go b/api/v1alpha1/addon_types.go index 8eb30abb..4f3989e0 100644 --- a/api/v1alpha1/addon_types.go +++ b/api/v1alpha1/addon_types.go @@ -33,14 +33,50 @@ type ManifestInfo struct { URL string `json:"url"` } +// StatusType is a type of condition that may apply to a particular component. +type StatusType string + +const ( + // TypeComponentAvailable indicates that the component is healthy. + TypeComponentAvailable StatusType = "Available" + + // TypeComponentProgressing means that the component is in the process of being installed or upgraded. + TypeComponentProgressing StatusType = "Progressing" + + // TypeComponentDegraded means the component is not operating as desired and user action is required. + TypeComponentDegraded StatusType = "Degraded" + + // TypeComponentReady indicates that the component is healthy and ready.it is identical to Available. + TypeComponentReady StatusType = "Ready" + + // TypeComponentUnhealthy indicates the component is not functioning as intended. + TypeComponentUnhealthy StatusType = "Unhealthy" +) + +type Status struct { + // The type of condition. May be Available, Progressing, or Degraded. + Type StatusType `json:"type"` + + // The timestamp representing the start time for the current status. + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // A brief reason explaining the condition. + Reason string `json:"reason,omitempty"` + + // Optionally, a detailed message providing additional context. + Message string `json:"message,omitempty"` +} + // AddonStatus defines the observed state of Addon type AddonStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file + Status `json:",inline"` } //+kubebuilder:object:root=true //+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.type",description="Whether the component is running and stable." // Addon is the Schema for the addons API type Addon struct { diff --git a/api/v1alpha1/manifest_types.go b/api/v1alpha1/manifest_types.go index c1b9ee7b..ac868ac7 100644 --- a/api/v1alpha1/manifest_types.go +++ b/api/v1alpha1/manifest_types.go @@ -22,6 +22,8 @@ type ManifestSpec struct { type ManifestStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file + + Status `json:",inline"` } // ManifestObject consists of the fields required to update/delete an object @@ -33,6 +35,7 @@ type ManifestObject struct { //+kubebuilder:object:root=true //+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.type",description="Whether the component is running and stable." // Manifest is the Schema for the manifests API type Manifest struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8bde9cb4..4b93ab87 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -16,7 +16,7 @@ func (in *Addon) DeepCopyInto(out *Addon) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Addon. @@ -97,6 +97,7 @@ func (in *AddonSpec) DeepCopy() *AddonSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AddonStatus) DeepCopyInto(out *AddonStatus) { *out = *in + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddonStatus. @@ -363,7 +364,7 @@ func (in *Manifest) DeepCopyInto(out *Manifest) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Manifest. @@ -469,6 +470,7 @@ func (in *ManifestSpec) DeepCopy() *ManifestSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManifestStatus) DeepCopyInto(out *ManifestStatus) { *out = *in + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestStatus. @@ -480,3 +482,19 @@ func (in *ManifestStatus) DeepCopy() *ManifestStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Status) DeepCopyInto(out *Status) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. +func (in *Status) DeepCopy() *Status { + if in == nil { + return nil + } + out := new(Status) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/boundless.mirantis.com_addons.yaml b/config/crd/bases/boundless.mirantis.com_addons.yaml index 68b357d1..6f085f21 100644 --- a/config/crd/bases/boundless.mirantis.com_addons.yaml +++ b/config/crd/bases/boundless.mirantis.com_addons.yaml @@ -15,7 +15,12 @@ spec: singular: addon scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: Whether the component is running and stable. + jsonPath: .status.type + name: Status + type: string + name: v1alpha1 schema: openAPIV3Schema: description: Addon is the Schema for the addons API @@ -79,6 +84,25 @@ spec: type: object status: description: AddonStatus defines the observed state of Addon + properties: + lastTransitionTime: + description: The timestamp representing the start time for the current + status. + format: date-time + type: string + message: + description: Optionally, a detailed message providing additional context. + type: string + reason: + description: A brief reason explaining the condition. + type: string + type: + description: The type of condition. May be Available, Progressing, + or Degraded. + type: string + required: + - lastTransitionTime + - type type: object type: object served: true diff --git a/config/crd/bases/boundless.mirantis.com_manifests.yaml b/config/crd/bases/boundless.mirantis.com_manifests.yaml index 182c57b0..b9a2dbbb 100644 --- a/config/crd/bases/boundless.mirantis.com_manifests.yaml +++ b/config/crd/bases/boundless.mirantis.com_manifests.yaml @@ -15,7 +15,12 @@ spec: singular: manifest scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: Whether the component is running and stable. + jsonPath: .status.type + name: Status + type: string + name: v1alpha1 schema: openAPIV3Schema: description: Manifest is the Schema for the manifests API @@ -64,6 +69,25 @@ spec: type: object status: description: ManifestStatus defines the observed state of Manifest + properties: + lastTransitionTime: + description: The timestamp representing the start time for the current + status. + format: date-time + type: string + message: + description: Optionally, a detailed message providing additional context. + type: string + reason: + description: A brief reason explaining the condition. + type: string + type: + description: The type of condition. May be Available, Progressing, + or Degraded. + type: string + required: + - lastTransitionTime + - type type: object type: object served: true diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 19d42820..5ecbb652 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -5,6 +5,13 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch - apiGroups: - boundless.mirantis.com resources: diff --git a/controllers/addon_controller.go b/controllers/addon_controller.go index 34bba19c..2f3bf796 100644 --- a/controllers/addon_controller.go +++ b/controllers/addon_controller.go @@ -5,14 +5,20 @@ import ( "fmt" "strings" + "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" boundlessv1alpha1 "github.com/mirantis/boundless-operator/api/v1alpha1" + "github.com/mirantis/boundless-operator/pkg/event" "github.com/mirantis/boundless-operator/pkg/helm" "github.com/mirantis/boundless-operator/pkg/manifest" ) @@ -28,12 +34,14 @@ const ( // AddonReconciler reconciles a Addon object type AddonReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + Recorder record.EventRecorder } //+kubebuilder:rbac:groups=boundless.mirantis.com,resources=addons,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=boundless.mirantis.com,resources=addons/status,verbs=get;update;patch //+kubebuilder:rbac:groups=boundless.mirantis.com,resources=addons/finalizers,verbs=update +//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -75,6 +83,9 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl kind = instance.Spec.Kind } + // @TODO: Update addon status only once per reconcile; React to Statuses of HelmChart / Manifests + r.updateStatus(ctx, logger, req.NamespacedName, boundlessv1alpha1.TypeComponentProgressing, "Creating Addon") + switch kind { case kindChart: if instance.Spec.Chart == nil { @@ -110,6 +121,7 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl if err := hc.DeleteHelmChart(chart, instance.Spec.Namespace); err != nil { // if fail to delete the helm chart here, return with error // so that it can be retried + r.Recorder.AnnotatedEventf(instance, map[string]string{event.AddonAnnotationKey: instance.Name}, event.TypeWarning, event.ReasonFailedDelete, "Failed to Delete Chart Addon %s/%s: %s", instance.Spec.Namespace, instance.Name, err) return ctrl.Result{}, err } @@ -127,8 +139,13 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl logger.Info("Creating Addon HelmChart resource", "Name", chart.Name, "Version", chart.Version) if err := hc.CreateHelmChart(chart, instance.Spec.Namespace); err != nil { logger.Error(err, "failed to install addon", "Name", chart.Name, "Version", chart.Version) + r.Recorder.AnnotatedEventf(instance, map[string]string{event.AddonAnnotationKey: instance.Name}, event.TypeWarning, event.ReasonFailedCreate, "Failed to Create Chart Addon %s/%s : %s", instance.Spec.Namespace, instance.Name, err) + r.updateStatus(ctx, logger, req.NamespacedName, boundlessv1alpha1.TypeComponentUnhealthy, "Failed to Create HelmChart") return ctrl.Result{Requeue: true}, err } + r.Recorder.AnnotatedEventf(instance, map[string]string{event.AddonAnnotationKey: instance.Name}, event.TypeNormal, event.ReasonSuccessfulCreate, "Created Chart Addon %s/%s", instance.Spec.Namespace, instance.Name) + r.updateStatus(ctx, logger, req.NamespacedName, boundlessv1alpha1.TypeComponentAvailable, "Chart Addon Created") + case kindManifest: if instance.Spec.Manifest == nil { logger.Info("Manifest info is missing") @@ -154,6 +171,8 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl if err := mc.DeleteManifest(BoundlessNamespace, instance.Spec.Name, instance.Spec.Manifest.URL); err != nil { // if fail to delete the manifest here, return with error // so that it can be retried + r.Recorder.AnnotatedEventf(instance, map[string]string{event.AddonAnnotationKey: instance.Name}, event.TypeWarning, event.ReasonFailedDelete, "Failed to Delete Manifest Addon %s/%s : %s", instance.Spec.Namespace, instance.Name, err) + r.updateStatus(ctx, logger, req.NamespacedName, boundlessv1alpha1.TypeComponentUnhealthy, "Failed to Cleanup Manifest") return ctrl.Result{}, err } @@ -171,9 +190,14 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl err = mc.CreateManifest(BoundlessNamespace, instance.Spec.Name, instance.Spec.Manifest.URL) if err != nil { logger.Error(err, "failed to install addon via manifest", "URL", instance.Spec.Manifest.URL) + r.Recorder.AnnotatedEventf(instance, map[string]string{event.AddonAnnotationKey: instance.Name}, event.TypeWarning, event.ReasonFailedCreate, "Failed to Create Manifest Addon %s/%s : %s", instance.Spec.Namespace, instance.Name, err) + r.updateStatus(ctx, logger, req.NamespacedName, boundlessv1alpha1.TypeComponentUnhealthy, "Failed to Create Manifest") return ctrl.Result{Requeue: true}, err } + r.Recorder.AnnotatedEventf(instance, map[string]string{event.AddonAnnotationKey: instance.Name}, event.TypeNormal, event.ReasonSuccessfulCreate, "Created Manifest Addon %s/%s", instance.Spec.Namespace, instance.Name) + r.updateStatus(ctx, logger, req.NamespacedName, boundlessv1alpha1.TypeComponentAvailable, "Manifest Addon Created") + default: logger.Info("Unknown AddOn kind", "Kind", instance.Spec.Kind) return ctrl.Result{Requeue: false}, fmt.Errorf("Unknown AddOn Kind: %w", err) @@ -187,5 +211,33 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl func (r *AddonReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&boundlessv1alpha1.Addon{}). + WithEventFilter(predicate.GenerationChangedPredicate{}). Complete(r) } + +func (r *AddonReconciler) updateStatus(ctx context.Context, logger logr.Logger, namespacedName types.NamespacedName, conditionTypeToApply boundlessv1alpha1.StatusType, reasonToApply string, messageToApply ...string) error { + addon := &boundlessv1alpha1.Addon{} + err := r.Get(ctx, namespacedName, addon) + if err != nil { + logger.Error(err, "Failed to get addon to update status") + return err + } + + if addon.Status.Type == conditionTypeToApply && addon.Status.Reason == reasonToApply { + // avoid infinite reconciliation loops + logger.Info("No updates to status needed") + return nil + } + + logger.Info("Update status for addon", "Name", addon.Name) + + patch := client.MergeFrom(addon.DeepCopy()) + addon.Status.Type = conditionTypeToApply + addon.Status.Reason = reasonToApply + if len(messageToApply) > 0 { + addon.Status.Message = messageToApply[0] + } + addon.Status.LastTransitionTime = metav1.Now() + + return r.Status().Patch(ctx, addon, patch) +} diff --git a/controllers/manifest_controller.go b/controllers/manifest_controller.go index 3c270f40..ed138307 100644 --- a/controllers/manifest_controller.go +++ b/controllers/manifest_controller.go @@ -2,6 +2,7 @@ package controllers import ( "context" + "fmt" "io" "net/http" "reflect" @@ -12,6 +13,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -26,6 +28,7 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" boundlessv1alpha1 "github.com/mirantis/boundless-operator/api/v1alpha1" + "github.com/mirantis/boundless-operator/pkg/event" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" ) @@ -39,12 +42,14 @@ const ( // ManifestReconciler reconciles a Manifest object type ManifestReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + Recorder record.EventRecorder } //+kubebuilder:rbac:groups=boundless.mirantis.com,resources=manifests,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=boundless.mirantis.com,resources=manifests/status,verbs=get;update;patch //+kubebuilder:rbac:groups=boundless.mirantis.com,resources=manifests/finalizers,verbs=update +//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -79,6 +84,14 @@ func (r *ManifestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } } + // only update the status to progressing if its not already set to not trigger infinite reconciliations + if existing.Status.Type == "" { + err := r.updateStatus(ctx, logger, key, boundlessv1alpha1.TypeComponentProgressing, "Creating Manifest") + if err != nil { + return ctrl.Result{Requeue: true}, err + } + } + addonFinalizerName := "manifest/finalizer" if existing.ObjectMeta.DeletionTimestamp.IsZero() { @@ -89,7 +102,7 @@ func (r *ManifestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c controllerutil.AddFinalizer(existing, addonFinalizerName) if err := r.Update(ctx, existing); err != nil { logger.Info("failed to update manifest object with finalizer", "Name", req.Name, "Finalizer", addonFinalizerName) - return ctrl.Result{}, err + return ctrl.Result{Requeue: true}, err } return ctrl.Result{}, err } @@ -99,14 +112,16 @@ func (r *ManifestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c // The finalizer is present, so lets delete the objects for this manifest if err := r.DeleteManifestObjects(existing.Spec.Objects, ctx); err != nil { logger.Error(err, "failed to delete manifest objects") - return ctrl.Result{}, err + r.Recorder.AnnotatedEventf(existing, map[string]string{event.AddonAnnotationKey: existing.Name}, event.TypeWarning, event.ReasonFailedCreate, "failed to delete manifest objects %s/%s", existing.Namespace, existing.Name) + r.updateStatus(ctx, logger, key, boundlessv1alpha1.TypeComponentUnhealthy, "failed to delete manifest objects", fmt.Sprintf("failed to delete manifest objects : %s", err)) + return ctrl.Result{Requeue: true}, err } // Remove the finalizer from the list and update it. controllerutil.RemoveFinalizer(existing, addonFinalizerName) if err := r.Update(ctx, existing); err != nil { logger.Error(err, "failed to remove finalizer") - return ctrl.Result{}, err + return ctrl.Result{Requeue: true}, err } } @@ -145,6 +160,8 @@ func (r *ManifestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c // TODO: https://github.com/Mirantis/boundless-operator/pull/17#pullrequestreview-1754136032 if err := r.UpdateManifestObjects(req, ctx, existing); err != nil { logger.Error(err, "failed to update manifest") + r.Recorder.AnnotatedEventf(existing, map[string]string{event.AddonAnnotationKey: existing.Name}, event.TypeWarning, event.ReasonFailedCreate, "failed to update manifest %s/%s : %s", existing.Namespace, existing.Name, err.Error()) + r.updateStatus(ctx, logger, key, boundlessv1alpha1.TypeComponentUnhealthy, "failed to update manifest", fmt.Sprintf("failed to update manifest : %s", err)) return ctrl.Result{}, err } @@ -169,6 +186,8 @@ func (r *ManifestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c if err := r.Update(ctx, &updatedCRD); err != nil { logger.Error(err, "failed to update manifest crd while create operation") + r.Recorder.AnnotatedEventf(existing, map[string]string{event.AddonAnnotationKey: existing.Name}, event.TypeWarning, event.ReasonFailedCreate, "failed to update manifest crd while create operation %s/%s : %s", existing.Namespace, existing.Name, err.Error()) + r.updateStatus(ctx, logger, key, boundlessv1alpha1.TypeComponentUnhealthy, "failed to update manifest crd while create operation", fmt.Sprintf("failed to update manifest crd while create operation : %s", err)) return ctrl.Result{}, err } @@ -176,18 +195,29 @@ func (r *ManifestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c bodyBytes, err := r.ReadManifest(req, existing.Spec.Url, logger) if err != nil { logger.Error(err, "failed to fetch manifest file content for url: %s", existing.Spec.Url) - return ctrl.Result{}, err + r.Recorder.AnnotatedEventf(existing, map[string]string{event.AddonAnnotationKey: existing.Name}, event.TypeWarning, event.ReasonFailedCreate, "failed to fetch manifest file content for url %s/%s : %s", existing.Namespace, existing.Name, err.Error()) + r.updateStatus(ctx, logger, key, boundlessv1alpha1.TypeComponentUnhealthy, "failed to fetch manifest file content for url", fmt.Sprintf("failed to fetch manifest file content for url : %s", err)) + return ctrl.Result{RequeueAfter: time.Minute}, err } logger.Info("received new crd request. Creating manifest objects..") err = r.CreateManifestObjects(req, bodyBytes, logger, ctx, existing) if err != nil { logger.Error(err, "failed to create objects for the manifest", "Name", req.Name) + r.Recorder.AnnotatedEventf(existing, map[string]string{event.AddonAnnotationKey: existing.Name}, event.TypeWarning, event.ReasonFailedCreate, "failed to create objects for the manifest %s/%s : %s", existing.Namespace, existing.Name, err.Error()) + r.updateStatus(ctx, logger, key, boundlessv1alpha1.TypeComponentUnhealthy, "failed to create objects for the manifest", fmt.Sprintf("failed to create objects for the manifest : %s", err)) return ctrl.Result{}, err } } + r.Recorder.AnnotatedEventf(existing, map[string]string{event.AddonAnnotationKey: existing.Name}, event.TypeNormal, event.ReasonSuccessfulCreate, "Created Manifest %s/%s", existing.Namespace, existing.Name) + err = r.updateStatus(ctx, logger, key, boundlessv1alpha1.TypeComponentAvailable, "Manifest Created") + if err != nil { + logger.Error(err, "Failed to update status after manifest creation") + return ctrl.Result{Requeue: true}, err + } + return ctrl.Result{}, nil } @@ -2134,3 +2164,30 @@ func (r *ManifestReconciler) ReadManifest(req ctrl.Request, url string, logger l return bodyBytes, nil } + +func (r *ManifestReconciler) updateStatus(ctx context.Context, logger logr.Logger, namespacedName types.NamespacedName, typeToApply boundlessv1alpha1.StatusType, reasonToApply string, messageToApply ...string) error { + manifest := &boundlessv1alpha1.Manifest{} + err := r.Get(ctx, namespacedName, manifest) + if err != nil { + logger.Error(err, "Failed to get manifest to update status") + return err + } + + if manifest.Status.Type == typeToApply && manifest.Status.Reason == reasonToApply { + // avoid infinite reconciliation loops + logger.Info("No updates to status needed") + return nil + } + + logger.Info("Update status for manifest", "Name", manifest.Name) + + patch := client.MergeFrom(manifest.DeepCopy()) + manifest.Status.Type = typeToApply + manifest.Status.Reason = reasonToApply + if len(messageToApply) > 0 { + manifest.Status.Message = messageToApply[0] + } + manifest.Status.LastTransitionTime = metav1.Now() + + return r.Status().Patch(ctx, manifest, patch) +} diff --git a/main.go b/main.go index 02c88bc4..434345aa 100644 --- a/main.go +++ b/main.go @@ -79,8 +79,9 @@ func main() { } if err = (&controllers.AddonReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("addon controller"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Addon") os.Exit(1) @@ -100,8 +101,9 @@ func main() { os.Exit(1) } if err = (&controllers.ManifestReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("manifest controller"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Manifest") os.Exit(1) diff --git a/pkg/event/event.go b/pkg/event/event.go new file mode 100644 index 00000000..c3d33550 --- /dev/null +++ b/pkg/event/event.go @@ -0,0 +1,10 @@ +package event + +const AddonAnnotationKey = "Addon" + +const ReasonSuccessfulCreate = "SuccessfulCreate" +const ReasonFailedCreate = "FailedCreate" +const ReasonFailedDelete = "FailedDelete" + +const TypeWarning = "Warning" +const TypeNormal = "Normal"