From f3ba8eca8e14f1339c778651f77ccd09766124db Mon Sep 17 00:00:00 2001 From: Timofei Larkin Date: Thu, 30 Oct 2025 18:14:16 +0300 Subject: [PATCH] [dashboard] Revert reconciler removal ## What this PR does In a previous patch (#1555) the reconciliation loop for the OpenAPI UI resources was accidentally removed. This patch reintroduces a separate controller, which handles updates to CozystackResourceDefinitions and creates, updates, or deletes the dashboard's custom resources. ### Release note ```release-note [dashboard] Reintroduce the accidentally removed reconciler that autoconfigures custom dashboard resources for the OpenAPI UI. ``` Signed-off-by: Timofei Larkin --- cmd/cozystack-controller/main.go | 13 ++- internal/controller/dashboard/breadcrumb.go | 4 +- .../controller/dashboard/customcolumns.go | 4 +- .../dashboard/customformsoverride.go | 4 +- .../dashboard/customformsprefill.go | 4 +- internal/controller/dashboard/factory.go | 4 +- internal/controller/dashboard/manager.go | 84 ++++++++++--------- .../controller/dashboard/marketplacepanel.go | 12 +-- internal/controller/dashboard/sidebar.go | 20 ++--- .../controller/dashboard/static_processor.go | 2 +- 10 files changed, 80 insertions(+), 71 deletions(-) diff --git a/cmd/cozystack-controller/main.go b/cmd/cozystack-controller/main.go index 737c7775..fc18a778 100644 --- a/cmd/cozystack-controller/main.go +++ b/cmd/cozystack-controller/main.go @@ -229,6 +229,15 @@ func main() { os.Exit(1) } + dashboardManager := &dashboard.Manager{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + } + if err = dashboardManager.SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "DashboardReconciler") + os.Exit(1) + } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { @@ -254,7 +263,9 @@ func main() { } setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + ctx := ctrl.SetupSignalHandler() + dashboardManager.InitializeStaticResources(ctx) + if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } diff --git a/internal/controller/dashboard/breadcrumb.go b/internal/controller/dashboard/breadcrumb.go index 0ad53ddb..5122f605 100644 --- a/internal/controller/dashboard/breadcrumb.go +++ b/internal/controller/dashboard/breadcrumb.go @@ -58,8 +58,8 @@ func (m *Manager) ensureBreadcrumb(ctx context.Context, crd *cozyv1alpha1.Cozyst "breadcrumbItems": items, } - _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error { - if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil { + _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { + if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/customcolumns.go b/internal/controller/dashboard/customcolumns.go index 441787fe..d737fd14 100644 --- a/internal/controller/dashboard/customcolumns.go +++ b/internal/controller/dashboard/customcolumns.go @@ -145,8 +145,8 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al }, } - _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error { - if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil { + _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { + if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/customformsoverride.go b/internal/controller/dashboard/customformsoverride.go index c06a21f3..ae637fa1 100644 --- a/internal/controller/dashboard/customformsoverride.go +++ b/internal/controller/dashboard/customformsoverride.go @@ -53,8 +53,8 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph "strategy": "merge", } - _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error { - if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil { + _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { + if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/customformsprefill.go b/internal/controller/dashboard/customformsprefill.go index b4dd2491..d2761873 100644 --- a/internal/controller/dashboard/customformsprefill.go +++ b/internal/controller/dashboard/customformsprefill.go @@ -56,8 +56,8 @@ func (m *Manager) ensureCustomFormsPrefill(ctx context.Context, crd *cozyv1alpha return reconcile.Result{}, err } - _, err = controllerutil.CreateOrUpdate(ctx, m.client, cfp, func() error { - if err := controllerutil.SetOwnerReference(crd, cfp, m.scheme); err != nil { + _, err = controllerutil.CreateOrUpdate(ctx, m.Client, cfp, func() error { + if err := controllerutil.SetOwnerReference(crd, cfp, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/factory.go b/internal/controller/dashboard/factory.go index 798d338c..add6940b 100644 --- a/internal/controller/dashboard/factory.go +++ b/internal/controller/dashboard/factory.go @@ -61,8 +61,8 @@ func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.Cozystack obj := &dashv1alpha1.Factory{} obj.SetName(factoryName) - _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error { - if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil { + _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { + if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/manager.go b/internal/controller/dashboard/manager.go index 4b5c2fb3..9c5d7e9d 100644 --- a/internal/controller/dashboard/manager.go +++ b/internal/controller/dashboard/manager.go @@ -10,7 +10,9 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -40,28 +42,42 @@ func AddToScheme(s *runtime.Scheme) error { // Manager owns logic for creating/updating dashboard resources derived from CRDs. // It’s easy to extend: add new ensure* methods and wire them into EnsureForCRD. type Manager struct { - client client.Client - scheme *runtime.Scheme - crdListFn func(context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error) -} - -// Option pattern so callers can inject a custom lister. -type Option func(*Manager) - -// WithCRDListFunc overrides how Manager lists all CozystackResourceDefinitions. -func WithCRDListFunc(fn func(context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error)) Option { - return func(m *Manager) { m.crdListFn = fn } + client.Client + Scheme *runtime.Scheme } // NewManager constructs a dashboard Manager. -func NewManager(c client.Client, scheme *runtime.Scheme, opts ...Option) *Manager { - m := &Manager{client: c, scheme: scheme} - for _, o := range opts { - o(m) - } +func NewManager(c client.Client, scheme *runtime.Scheme) *Manager { + m := &Manager{Client: c, Scheme: scheme} return m } +func (m *Manager) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + Named("dashboard-reconciler"). + For(&cozyv1alpha1.CozystackResourceDefinition{}). + Complete(m) +} + +func (m *Manager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := log.FromContext(ctx) + + crd := &cozyv1alpha1.CozystackResourceDefinition{} + + err := m.Get(ctx, types.NamespacedName{Name: req.Name}, crd) + if err != nil { + if apierrors.IsNotFound(err) { + if err := m.CleanupOrphanedResources(ctx); err != nil { + l.Error(err, "Failed to cleanup orphaned dashboard resources") + } + return ctrl.Result{}, nil // no point in requeuing here + } + return ctrl.Result{}, err + } + + return m.EnsureForCRD(ctx, crd) +} + // EnsureForCRD is the single entry-point used by the controller. // Add more ensure* calls here as you implement support for other resources: // @@ -171,21 +187,11 @@ func (m *Manager) getStaticResourceSelector() client.MatchingLabels { // CleanupOrphanedResources removes dashboard resources that are no longer needed // This should be called after cache warming to ensure all current resources are known func (m *Manager) CleanupOrphanedResources(ctx context.Context) error { - // Get all current CRDs to determine which resources should exist - var allCRDs []cozyv1alpha1.CozystackResourceDefinition - if m.crdListFn != nil { - s, err := m.crdListFn(ctx) - if err != nil { - return err - } - allCRDs = s - } else { - var crdList cozyv1alpha1.CozystackResourceDefinitionList - if err := m.client.List(ctx, &crdList, &client.ListOptions{}); err != nil { - return err - } - allCRDs = crdList.Items + var crdList cozyv1alpha1.CozystackResourceDefinitionList + if err := m.List(ctx, &crdList, &client.ListOptions{}); err != nil { + return err } + allCRDs := crdList.Items // Build a set of expected resource names for each type expectedResources := m.buildExpectedResourceSet(allCRDs) @@ -349,7 +355,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O } // List with dashboard labels - if err := m.client.List(ctx, list, m.getDashboardResourceSelector()); err != nil { + if err := m.List(ctx, list, m.getDashboardResourceSelector()); err != nil { return err } @@ -358,7 +364,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.CustomColumnsOverrideList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -369,7 +375,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.CustomFormsOverrideList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -380,7 +386,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.CustomFormsPrefillList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -391,7 +397,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.MarketplacePanelList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -402,7 +408,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.SidebarList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -413,7 +419,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.TableUriMappingList: for _, item := range l.Items { if !expected[item.Name] { - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -426,7 +432,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O if !expected[item.Name] { logger := log.FromContext(ctx) logger.Info("Deleting orphaned Breadcrumb resource", "name", item.Name) - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } @@ -438,7 +444,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O if !expected[item.Name] { logger := log.FromContext(ctx) logger.Info("Deleting orphaned Factory resource", "name", item.Name) - if err := m.client.Delete(ctx, &item); err != nil { + if err := m.Delete(ctx, &item); err != nil { if !apierrors.IsNotFound(err) { return err } diff --git a/internal/controller/dashboard/marketplacepanel.go b/internal/controller/dashboard/marketplacepanel.go index e58232f5..82a8f336 100644 --- a/internal/controller/dashboard/marketplacepanel.go +++ b/internal/controller/dashboard/marketplacepanel.go @@ -24,14 +24,14 @@ func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1. // If dashboard is not set, delete the panel if it exists. if crd.Spec.Dashboard == nil { - err := m.client.Get(ctx, client.ObjectKey{Name: mp.Name}, mp) + err := m.Get(ctx, client.ObjectKey{Name: mp.Name}, mp) if apierrors.IsNotFound(err) { return reconcile.Result{}, nil } if err != nil { return reconcile.Result{}, err } - if err := m.client.Delete(ctx, mp); err != nil && !apierrors.IsNotFound(err) { + if err := m.Delete(ctx, mp); err != nil && !apierrors.IsNotFound(err) { return reconcile.Result{}, err } logger.Info("Deleted MarketplacePanel because dashboard is not set", "name", mp.Name) @@ -40,14 +40,14 @@ func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1. // Skip module and tenant resources (they don't need MarketplacePanel) if crd.Spec.Dashboard.Module || crd.Spec.Application.Kind == "Tenant" { - err := m.client.Get(ctx, client.ObjectKey{Name: mp.Name}, mp) + err := m.Get(ctx, client.ObjectKey{Name: mp.Name}, mp) if apierrors.IsNotFound(err) { return reconcile.Result{}, nil } if err != nil { return reconcile.Result{}, err } - if err := m.client.Delete(ctx, mp); err != nil && !apierrors.IsNotFound(err) { + if err := m.Delete(ctx, mp); err != nil && !apierrors.IsNotFound(err) { return reconcile.Result{}, err } logger.Info("Deleted MarketplacePanel because resource is a module", "name", mp.Name) @@ -86,8 +86,8 @@ func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1. return reconcile.Result{}, err } - _, err = controllerutil.CreateOrUpdate(ctx, m.client, mp, func() error { - if err := controllerutil.SetOwnerReference(crd, mp, m.scheme); err != nil { + _, err = controllerutil.CreateOrUpdate(ctx, m.Client, mp, func() error { + if err := controllerutil.SetOwnerReference(crd, mp, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/sidebar.go b/internal/controller/dashboard/sidebar.go index 3edd6aca..97937921 100644 --- a/internal/controller/dashboard/sidebar.go +++ b/internal/controller/dashboard/sidebar.go @@ -33,19 +33,11 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack // 1) Fetch all CRDs var all []cozyv1alpha1.CozystackResourceDefinition - if m.crdListFn != nil { - s, err := m.crdListFn(ctx) - if err != nil { - return err - } - all = s - } else { - var crdList cozyv1alpha1.CozystackResourceDefinitionList - if err := m.client.List(ctx, &crdList, &client.ListOptions{}); err != nil { - return err - } - all = crdList.Items + var crdList cozyv1alpha1.CozystackResourceDefinitionList + if err := m.List(ctx, &crdList, &client.ListOptions{}); err != nil { + return err } + all = crdList.Items // 2) Build category -> []item map (only for CRDs with spec.dashboard != nil) type item struct { @@ -251,7 +243,7 @@ func (m *Manager) upsertMultipleSidebars( obj := &dashv1alpha1.Sidebar{} obj.SetName(id) - if _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error { + if _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error { // Only set owner reference for dynamic sidebars (stock-project-factory-{kind}-details) // Static sidebars (stock-instance-*, stock-project-*) should not have owner references if strings.HasPrefix(id, "stock-project-factory-") && strings.HasSuffix(id, "-details") { @@ -260,7 +252,7 @@ func (m *Manager) upsertMultipleSidebars( lowerKind := strings.ToLower(kind) expectedID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind) if id == expectedID { - if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil { + if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil { return err } // Add dashboard labels to dynamic resources diff --git a/internal/controller/dashboard/static_processor.go b/internal/controller/dashboard/static_processor.go index ef2e0341..902e3818 100644 --- a/internal/controller/dashboard/static_processor.go +++ b/internal/controller/dashboard/static_processor.go @@ -32,7 +32,7 @@ func (m *Manager) ensureStaticResource(ctx context.Context, obj client.Object) e // Add dashboard labels to static resources m.addDashboardLabels(resource, nil, ResourceTypeStatic) - _, err := controllerutil.CreateOrUpdate(ctx, m.client, resource, func() error { + _, err := controllerutil.CreateOrUpdate(ctx, m.Client, resource, func() error { // For static resources, we don't need to set owner references // as they are meant to be persistent across CRD changes // Copy Spec from the original object to the live object