diff --git a/api/v1alpha1/utils.go b/api/v1alpha1/utils.go index c0152073..cf7e3f29 100644 --- a/api/v1alpha1/utils.go +++ b/api/v1alpha1/utils.go @@ -45,3 +45,7 @@ func IsPruneDisabled(obj AnnotationHolder) bool { } return isDisabled } + +const ( + EventAnnKeyUserName = "cosmo-workspace.github.io/user" +) diff --git a/internal/cmd/user/get_events.go b/internal/cmd/user/get_events.go index 9381bf1a..d560351b 100644 --- a/internal/cmd/user/get_events.go +++ b/internal/cmd/user/get_events.go @@ -10,7 +10,6 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "k8s.io/utils/ptr" - "github.com/cosmo-workspace/cosmo/api/v1alpha1" "github.com/cosmo-workspace/cosmo/pkg/apiconv" "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/cosmo-workspace/cosmo/pkg/clog" @@ -125,7 +124,7 @@ func regarding(v *dashv1alpha1.ObjectReference) string { func (o *GetEventsOption) GetEventsByKubeClient(ctx context.Context) ([]*dashv1alpha1.Event, error) { c := o.KosmoClient - events, err := c.ListEvents(ctx, v1alpha1.UserNamespace(o.UserName)) + events, err := c.ListEventsForUser(ctx, o.UserName) if err != nil { return nil, err } diff --git a/internal/controllers/cluster_instance_controller.go b/internal/controllers/cluster_instance_controller.go index c3630280..ba76513d 100644 --- a/internal/controllers/cluster_instance_controller.go +++ b/internal/controllers/cluster_instance_controller.go @@ -51,24 +51,28 @@ func (r *ClusterInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ inst.Status.TemplateName = tmpl.Name inst.Status.TemplateResourceVersion = tmpl.ResourceVersion + eventAnn := map[string]string{ + cosmov1alpha1.EventAnnKeyUserName: userNameFromOwnerReferences(inst.GetOwnerReferences()), + } + // 1. Build Unstructured objects objects, err := template.BuildObjects(tmpl.Spec, &inst) if err != nil { - r.Recorder.Event(&inst, corev1.EventTypeWarning, "BuildFailed", err.Error()) + r.Recorder.AnnotatedEventf(&inst, eventAnn, corev1.EventTypeWarning, "BuildFailed", "Failed to build manifests from Template: %v", err) return ctrl.Result{}, err } // 2. Transform the objects objects, err = transformer.ApplyTransformers(ctx, transformer.AllTransformers(&inst, r.Scheme, tmpl), objects) if err != nil { - r.Recorder.Event(&inst, corev1.EventTypeWarning, "BuildFailed", err.Error()) + r.Recorder.AnnotatedEventf(&inst, eventAnn, corev1.EventTypeWarning, "BuildFailed", "Failed to build resources: %v", err) return ctrl.Result{}, err } // 3. Reconcile objects if errs := r.impl.reconcileObjects(ctx, &inst, objects); len(errs) != 0 { for _, err := range errs { - r.Recorder.Event(&inst, corev1.EventTypeWarning, "SyncFailed", err.Error()) + r.Recorder.AnnotatedEventf(&inst, eventAnn, corev1.EventTypeWarning, "SyncFailed", "Failed to sync objects: %v", err) } // requeue return ctrl.Result{}, fmt.Errorf("apply child objects failed: %w", errs[0]) diff --git a/internal/controllers/instance_controller.go b/internal/controllers/instance_controller.go index c5cbce58..0d7672b0 100644 --- a/internal/controllers/instance_controller.go +++ b/internal/controllers/instance_controller.go @@ -112,6 +112,17 @@ type instanceReconciler struct { FieldManager string } +func (r *instanceReconciler) Eventf(inst cosmov1alpha1.InstanceObject, eventtype, reason, messageFmt string, args ...any) { + if inst.GetScope() == meta.RESTScopeRoot { + ann := map[string]string{ + cosmov1alpha1.EventAnnKeyUserName: userNameFromOwnerReferences(inst.GetOwnerReferences()), + } + r.Recorder.AnnotatedEventf(inst, ann, eventtype, reason, messageFmt, args...) + } else { + r.Recorder.Eventf(inst, eventtype, reason, messageFmt, args...) + } +} + func (r *instanceReconciler) reconcileObjects(ctx context.Context, inst cosmov1alpha1.InstanceObject, objects []unstructured.Unstructured) []error { log := clog.FromContext(ctx).WithCaller() errs := make([]error, 0) @@ -162,7 +173,7 @@ func (r *instanceReconciler) reconcileObjects(ctx context.Context, inst cosmov1a if err != nil { errs = append(errs, fmt.Errorf("failed to create resource: kind = %s name = %s: %w", built.GetKind(), built.GetName(), err)) } else { - r.Recorder.Eventf(inst, corev1.EventTypeNormal, "Synced", "%s %s created", built.GetKind(), built.GetName()) + r.Eventf(inst, corev1.EventTypeNormal, "Synced", "%s %s is created", built.GetKind(), built.GetName()) } currAppliedMap[created.GetUID()] = unstToObjectRef(created) @@ -190,7 +201,7 @@ func (r *instanceReconciler) reconcileObjects(ctx context.Context, inst cosmov1a if _, err := r.apply(ctx, &built, r.FieldManager); err != nil { errs = append(errs, fmt.Errorf("failed to apply resource %s %s: %w", built.GetKind(), built.GetName(), err)) } else { - r.Recorder.Eventf(inst, corev1.EventTypeNormal, "Synced", "%s %s is not desired state, synced", built.GetKind(), built.GetName()) + r.Eventf(inst, corev1.EventTypeNormal, "Synced", "%s %s is not desired state, synced", built.GetKind(), built.GetName()) } } } @@ -203,10 +214,10 @@ func (r *instanceReconciler) reconcileObjects(ctx context.Context, inst cosmov1a for _, d := range shouldDeletes { if skip, err := prune(ctx, r.Client, d); err != nil { log.Error(err, "failed to delete unused obj", "pruneAPIVersion", d.APIVersion, "pruneKind", d.Kind, "pruneName", d.Name, "pruneNamespace", d.Namespace) - r.Recorder.Eventf(inst, corev1.EventTypeWarning, "GCFailed", "failed to delete unused obj: kind=%s name=%s namespace=%s", d.Kind, d.Name, d.Namespace) + r.Eventf(inst, corev1.EventTypeWarning, "GCFailed", "Failed to delete unused obj: kind=%s name=%s namespace=%s", d.Kind, d.Name, d.Namespace) } else if !skip { log.Info("deleted unmanaged object", "apiVersion", d.APIVersion, "kind", d.Kind, "name", d.Name, "namespace", d.Namespace) - r.Recorder.Eventf(inst, corev1.EventTypeNormal, "GC", "deleted unmanaged object: kind=%s name=%s namespace=%s", d.Kind, d.Name, d.Namespace) + r.Eventf(inst, corev1.EventTypeNormal, "GC", "Deleted unmanaged object: kind=%s name=%s namespace=%s", d.Kind, d.Name, d.Namespace) } } } diff --git a/internal/controllers/template_controller.go b/internal/controllers/template_controller.go index 2d21f6cf..efe952e3 100644 --- a/internal/controllers/template_controller.go +++ b/internal/controllers/template_controller.go @@ -6,6 +6,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" @@ -93,7 +94,15 @@ func notifyUpdateToInstances(ctx context.Context, c client.Client, rec record.Ev if err := c.Status().Update(ctx, inst); err != nil { errs = append(errs, fmt.Errorf("failed to update instance status: %s: %w", inst.GetName(), err)) } - rec.Eventf(inst, corev1.EventTypeNormal, "TemplateUpdated", "Detected Template %s is updated", tmpl.GetName()) + + if inst.GetScope() == meta.RESTScopeRoot { + ann := map[string]string{ + cosmov1alpha1.EventAnnKeyUserName: userNameFromOwnerReferences(inst.GetOwnerReferences()), + } + rec.AnnotatedEventf(inst, ann, corev1.EventTypeNormal, "TemplateUpdated", "Detected Template %s is updated", tmpl.GetName()) + } else { + rec.Eventf(inst, corev1.EventTypeNormal, "TemplateUpdated", "Detected Template %s is updated", tmpl.GetName()) + } } return errs } diff --git a/internal/controllers/user_controller.go b/internal/controllers/user_controller.go index a5f4b575..f14a68bb 100644 --- a/internal/controllers/user_controller.go +++ b/internal/controllers/user_controller.go @@ -8,6 +8,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" apierrs "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" @@ -206,3 +207,12 @@ func (r *UserReconciler) patchNamespaceToUserDesired(ns *corev1.Namespace, user return nil } + +func userNameFromOwnerReferences(refs []metav1.OwnerReference) string { + for _, ref := range refs { + if ref.Kind == "User" { + return ref.Name + } + } + return "" +} diff --git a/internal/dashboard/user_handler.go b/internal/dashboard/user_handler.go index e2858e8c..57ebb3c7 100644 --- a/internal/dashboard/user_handler.go +++ b/internal/dashboard/user_handler.go @@ -101,9 +101,9 @@ func (s *Server) GetUser(ctx context.Context, req *connect_go.Request[dashv1alph if err != nil { return nil, ErrResponse(log, err) } - events, err := s.Klient.ListEvents(ctx, cosmov1alpha1.UserNamespace(user.Name)) + events, err := s.Klient.ListEventsForUser(ctx, user.Name) if err != nil { - log.Error(err, "failed to list events", "namespace", cosmov1alpha1.UserNamespace(user.Name)) + log.Error(err, "failed to list events", "user", user.Name) } res := &dashv1alpha1.GetUserResponse{ diff --git a/pkg/kosmo/event.go b/pkg/kosmo/event.go index 1e6da16c..0224916f 100644 --- a/pkg/kosmo/event.go +++ b/pkg/kosmo/event.go @@ -8,6 +8,7 @@ import ( apierrs "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" + cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" "github.com/cosmo-workspace/cosmo/pkg/clog" ) @@ -21,3 +22,31 @@ func (c *Client) ListEvents(ctx context.Context, namespace string) ([]eventsv1.E return events.Items, nil } } + +func (c *Client) ListEventsForUser(ctx context.Context, user string) ([]eventsv1.Event, error) { + ns := cosmov1alpha1.UserNamespace(user) + + // List events in user namespace + userEvents, err := c.ListEvents(ctx, ns) + if err != nil { + return nil, err + } + + // List events in default namespace which is annotated user name + // cluster scoped events are found in defualt namespace + var defaultEvents eventsv1.EventList + if err := c.List(ctx, &defaultEvents, &client.ListOptions{Namespace: "default"}); err != nil { + return nil, apierrs.NewInternalError(fmt.Errorf("failed to list Event in default namespace: %w", err)) + } + for _, e := range defaultEvents.Items { + if e.Annotations == nil { + continue + } + if e.Annotations[cosmov1alpha1.EventAnnKeyUserName] == user { + userEvents = append(userEvents, e) + } + } + + return userEvents, nil + +}