From fd8079e5c11749a64f6bb7bb49e08cf39b525e95 Mon Sep 17 00:00:00 2001 From: Thomas Polkowski Date: Thu, 23 Nov 2023 18:18:28 -0500 Subject: [PATCH] WIP: BOP-85/87 --- api/v1alpha1/addon_types.go | 30 ++++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 3 +- .../bases/boundless.mirantis.com_addons.yaml | 26 +++++++++- config/rbac/role.yaml | 7 +++ controllers/addon_controller.go | 48 +++++++++++++++++-- controllers/manifest_controller.go | 13 ++++- main.go | 10 ++-- pkg/event/event.go | 15 ++++++ 8 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 pkg/event/event.go diff --git a/api/v1alpha1/addon_types.go b/api/v1alpha1/addon_types.go index 1bea06ec..f409329d 100644 --- a/api/v1alpha1/addon_types.go +++ b/api/v1alpha1/addon_types.go @@ -33,14 +33,44 @@ type ManifestInfo struct { URL string `json:"url"` } +// StatusConditionType is a type of condition that may apply to a particular component. +type StatusConditionType string + +const ( + // ComponentAvailable indicates that the component is healthy. + ComponentAvailable StatusConditionType = "Available" + + // ComponentProgressing means that the component is in the process of being installed or upgraded. + ComponentProgressing StatusConditionType = "Progressing" + + // ComponentDegraded means the component is not operating as desired and user action is required. + ComponentDegraded StatusConditionType = "Degraded" + + // ComponentReady indicates that the component is healthy and ready.it is identical to Available and used in Status conditions for CRs. + ComponentReady StatusConditionType = "Ready" +) + // 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 + + // The type of condition. May be Available, Progressing, or Degraded. + Type StatusConditionType `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"` } //+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/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 1839d7ff..8f5ed896 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. @@ -89,6 +89,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.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddonStatus. 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/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 4ff7c49a..a4d2ace0 100644 --- a/controllers/addon_controller.go +++ b/controllers/addon_controller.go @@ -3,9 +3,12 @@ package controllers import ( "context" "fmt" - + "github.com/go-logr/logr" + "github.com/mirantis/boundless-operator/pkg/event" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "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" @@ -27,12 +30,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. @@ -63,6 +68,11 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, err } + // only update the status to progressing if its not already set to not trigger infinite reconciliations + if instance.Status.Type == "" { + r.updateStatus(ctx, logger, instance, boundlessv1alpha1.ComponentProgressing, "Creating Addon") + } + switch instance.Spec.Kind { case kindChart: chart := helm.Chart{ @@ -94,6 +104,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 } @@ -108,11 +119,15 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, nil } - 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, instance, boundlessv1alpha1.ComponentDegraded, "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, instance, boundlessv1alpha1.ComponentAvailable, "Chart Addon Created") + case kindManifest: mc := manifest.NewManifestController(r.Client, logger) @@ -133,6 +148,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, instance, boundlessv1alpha1.ComponentDegraded, "Failed to Cleanup Manifest") return ctrl.Result{}, err } @@ -150,9 +167,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, instance, boundlessv1alpha1.ComponentDegraded, "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, instance, boundlessv1alpha1.ComponentAvailable, "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) @@ -168,3 +190,23 @@ func (r *AddonReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&boundlessv1alpha1.Addon{}). Complete(r) } + +func (r *AddonReconciler) updateStatus(ctx context.Context, logger logr.Logger, addon *boundlessv1alpha1.Addon, statusType boundlessv1alpha1.StatusConditionType, reason string) { + if addon.Status.Type == statusType && addon.Status.Reason == reason { + // avoid infinite reconciliation loops + logger.Info("No updates to status needed") + return + } + + addon.Status.Type = statusType + addon.Status.Reason = reason + addon.Status.LastTransitionTime = metav1.Now() + + logger.Info("Updating status for addon", "Name", addon.Name) + err := r.Status().Update(ctx, addon) + if err != nil { + // just log the error for now - don't stop reconciliation because we failed to update the status + logger.Error(err, "Failed to update status for addon", "Name", addon.Name) + } + +} diff --git a/controllers/manifest_controller.go b/controllers/manifest_controller.go index 8d241cea..f637496d 100644 --- a/controllers/manifest_controller.go +++ b/controllers/manifest_controller.go @@ -2,7 +2,9 @@ package controllers import ( "context" + "github.com/mirantis/boundless-operator/pkg/event" "io" + "k8s.io/client-go/tools/record" "net/http" "strings" "time" @@ -32,7 +34,8 @@ import ( // ManifestReconciler reconciles a Manifest object type ManifestReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + Recorder record.EventRecorder } // The checkSum map stores the checksum for each manifest. @@ -43,6 +46,7 @@ var checkSum = make(map[string]string) //+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. @@ -97,6 +101,7 @@ func (r *ManifestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c if err := r.DeleteManifestObjects(req, ctx); err != nil { logger.Error(err, "failed to remove finalizer") + r.Recorder.AnnotatedEventf(existing, map[string]string{event.AddonAnnotationKey: existing.Name}, event.TypeWarning, event.ReasonFailedCreate, "Failed to Delete Manifest %s/%s", existing.Namespace, existing.Name) return ctrl.Result{}, err } @@ -140,6 +145,7 @@ func (r *ManifestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c resp, err := client.Get(existing.Spec.Url) if err != nil { logger.Error(err, "failed to fetch manifest file content for url: %s", existing.Spec.Url) + r.Recorder.AnnotatedEventf(existing, map[string]string{event.AddonAnnotationKey: existing.Name}, event.TypeWarning, event.ReasonFailedCreate, "Failed to Create Manifest %s/%s : %s", existing.Namespace, existing.Name, err.Error()) return ctrl.Result{}, err } @@ -150,19 +156,24 @@ func (r *ManifestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c bodyBytes, err = io.ReadAll(resp.Body) if err != nil { logger.Error(err, "failed to read http response body") + r.Recorder.AnnotatedEventf(existing, map[string]string{event.AddonAnnotationKey: existing.Name}, event.TypeWarning, event.ReasonFailedCreate, "Failed to Create Manifest %s/%s : %s", existing.Namespace, existing.Name, err.Error()) return ctrl.Result{}, err } } else { logger.Error(err, "failure in http get request", "ResponseCode", resp.StatusCode) + r.Recorder.AnnotatedEventf(existing, map[string]string{event.AddonAnnotationKey: existing.Name}, event.TypeWarning, event.ReasonFailedCreate, "Failed to Create Manifest %s/%s : failure in http get request response code : %d", existing.Namespace, existing.Name, resp.StatusCode) return ctrl.Result{}, err } err = r.CreateManifestObjects(req, bodyBytes, logger, ctx) if err != nil { logger.Error(err, "failed to create manifest objects", "ResponseCode", resp.StatusCode) + r.Recorder.AnnotatedEventf(existing, map[string]string{event.AddonAnnotationKey: existing.Name}, event.TypeWarning, event.ReasonFailedCreate, "Failed to Create Manifest %s/%s : %s", existing.Namespace, existing.Name, 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) + return ctrl.Result{}, nil } 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..0b561a4c --- /dev/null +++ b/pkg/event/event.go @@ -0,0 +1,15 @@ +package event + +const AddonAnnotationKey = "Addon" + +const ReasonSuccessfulCreate = "SuccessfulCreate" +const ReasonSuccessfulDelete = "SuccessfulDelete" +const ReasonFailedCreate = "FailedCreate" +const ReasonFailedDelete = "FailedDelete" + +const TypeWarning = "Warning" +const TypeNormal = "Normal" + +func createEvent() { + +}