diff --git a/internal/controller/dashboard/manager.go b/internal/controller/dashboard/manager.go index 49e42848..4065ec9e 100644 --- a/internal/controller/dashboard/manager.go +++ b/internal/controller/dashboard/manager.go @@ -12,6 +12,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -245,29 +246,29 @@ func (m *Manager) buildExpectedResourceSet(crds []cozyv1alpha1.CozystackResource continue } - // Skip resources with non-empty spec.dashboard.name (tenant modules) - if strings.TrimSpace(crd.Spec.Dashboard.Name) != "" { - continue - } + // Note: We include ALL resources with dashboard config, regardless of dashboard.name + // because ensureFactory and ensureBreadcrumb create resources for all CRDs with dashboard config g, v, kind := pickGVK(&crd) plural := pickPlural(kind, &crd) - // CustomColumnsOverride + // CustomColumnsOverride - created for ALL CRDs with dashboard config name := fmt.Sprintf("stock-namespace-%s.%s.%s", g, v, plural) expected["CustomColumnsOverride"][name] = true - // CustomFormsOverride + // CustomFormsOverride - created for ALL CRDs with dashboard config name = fmt.Sprintf("%s.%s.%s", g, v, plural) expected["CustomFormsOverride"][name] = true - // CustomFormsPrefill + // CustomFormsPrefill - created for ALL CRDs with dashboard config expected["CustomFormsPrefill"][name] = true - // MarketplacePanel (name matches CRD name) - expected["MarketplacePanel"][crd.Name] = true + // MarketplacePanel - only created for CRDs WITHOUT dashboard.name + if strings.TrimSpace(crd.Spec.Dashboard.Name) == "" { + expected["MarketplacePanel"][crd.Name] = true + } - // Sidebar resources (multiple per CRD) + // Sidebar resources - created for ALL CRDs with dashboard config lowerKind := strings.ToLower(kind) detailsID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind) expected["Sidebar"][detailsID] = true @@ -291,15 +292,15 @@ func (m *Manager) buildExpectedResourceSet(crds []cozyv1alpha1.CozystackResource expected["Sidebar"][sidebarID] = true } - // TableUriMapping + // TableUriMapping - created for ALL CRDs with dashboard config name = fmt.Sprintf("stock-namespace-%s.%s.%s", g, v, plural) expected["TableUriMapping"][name] = true - // Breadcrumb + // Breadcrumb - created for ALL CRDs with dashboard config detailID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind) expected["Breadcrumb"][detailID] = true - // Factory + // Factory - created for ALL CRDs with dashboard config factoryName := fmt.Sprintf("%s-details", lowerKind) expected["Factory"][factoryName] = true } @@ -423,22 +424,24 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O case *dashv1alpha1.BreadcrumbList: for _, item := range l.Items { 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 !apierrors.IsNotFound(err) { return err } - // Resource already deleted, continue } } } case *dashv1alpha1.FactoryList: for _, item := range l.Items { 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 !apierrors.IsNotFound(err) { return err } - // Resource already deleted, continue } } } diff --git a/internal/controller/dashboard/sidebar.go b/internal/controller/dashboard/sidebar.go index 59901924..cbce7c44 100644 --- a/internal/controller/dashboard/sidebar.go +++ b/internal/controller/dashboard/sidebar.go @@ -57,6 +57,9 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack categories := map[string][]item{} // category label -> children keysAndTags := map[string]any{} // plural -> []string{ "-sidebar" } + // Collect sidebar names for resources with dashboard.name + var moduleSidebars []any + for i := range all { def := &all[i] @@ -65,35 +68,46 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack continue } - // Skip resources with non-empty spec.dashboard.name - if strings.TrimSpace(def.Spec.Dashboard.Name) != "" { - continue - } - g, v, kind := pickGVK(def) plural := pickPlural(kind, def) - cat := safeCategory(def) // falls back to "Resources" if empty + lowerKind := strings.ToLower(kind) - // Label: prefer dashboard.Plural if provided - label := titleFromKindPlural(kind, plural) - if def.Spec.Dashboard.Plural != "" { - label = def.Spec.Dashboard.Plural + // Check if this resource has dashboard.name set + if strings.TrimSpace(def.Spec.Dashboard.Name) != "" { + // Add to modules sidebar list + moduleSidebars = append(moduleSidebars, fmt.Sprintf("%s-sidebar", lowerKind)) + } else { + // Add to keysAndTags for resources without dashboard.name + keysAndTags[plural] = []any{fmt.Sprintf("%s-sidebar", lowerKind)} } - // Weight (default 0) - weight := def.Spec.Dashboard.Weight + // Only add to menu categories if dashboard.name is empty + if strings.TrimSpace(def.Spec.Dashboard.Name) == "" { + cat := safeCategory(def) // falls back to "Resources" if empty - link := fmt.Sprintf("/openapi-ui/{clusterName}/{namespace}/api-table/%s/%s/%s", g, v, plural) + // Label: prefer dashboard.Plural if provided + label := titleFromKindPlural(kind, plural) + if def.Spec.Dashboard.Plural != "" { + label = def.Spec.Dashboard.Plural + } - categories[cat] = append(categories[cat], item{ - Key: plural, - Label: label, - Link: link, - Weight: weight, - }) + // Weight (default 0) + weight := def.Spec.Dashboard.Weight - // keysAndTags: plural -> [ "-sidebar" ] - keysAndTags[plural] = []any{fmt.Sprintf("%s-sidebar", strings.ToLower(kind))} + link := fmt.Sprintf("/openapi-ui/{clusterName}/{namespace}/api-table/%s/%s/%s", g, v, plural) + + categories[cat] = append(categories[cat], item{ + Key: plural, + Label: label, + Link: link, + Weight: weight, + }) + } + } + + // Add modules to keysAndTags if we have any module sidebars + if len(moduleSidebars) > 0 { + keysAndTags["modules"] = moduleSidebars } // 3) Sort items within each category by Weight (desc), then Label (A→Z) @@ -171,14 +185,8 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack }) // 6) Prepare the list of Sidebar IDs to upsert with the SAME content - _, _, thisKind := pickGVK(crd) - lowerThisKind := strings.ToLower(thisKind) - detailsID := fmt.Sprintf("stock-project-factory-%s-details", lowerThisKind) - + // Create sidebars for ALL CRDs with dashboard config targetIDs := []string{ - // original details sidebar - detailsID, - // stock-instance sidebars "stock-instance-api-form", "stock-instance-api-table", @@ -196,6 +204,18 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack "stock-project-crd-table", } + // Add details sidebars for all CRDs with dashboard config + for i := range all { + def := &all[i] + if def.Spec.Dashboard == nil { + continue + } + _, _, kind := pickGVK(def) + lowerKind := strings.ToLower(kind) + detailsID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind) + targetIDs = append(targetIDs, detailsID) + } + // 7) Upsert all target sidebars with identical menuItems and keysAndTags return m.upsertMultipleSidebars(ctx, crd, targetIDs, keysAndTags, menuItems) } @@ -219,11 +239,35 @@ func (m *Manager) upsertMultipleSidebars( obj.SetName(id) if _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error { - if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil { - return err + // 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") { + // This is a dynamic sidebar, set owner reference only if it matches the current CRD + _, _, kind := pickGVK(crd) + 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 { + return err + } + // Add dashboard labels to dynamic resources + m.addDashboardLabels(obj, crd, ResourceTypeDynamic) + } else { + // This is a different CRD's sidebar, don't modify owner references or labels + // Just update the spec + } + } else { + // This is a static sidebar, don't set owner references + // Add static labels + labels := obj.GetLabels() + if labels == nil { + labels = make(map[string]string) + } + labels[LabelManagedBy] = ManagedByValue + labels[LabelResourceType] = ResourceTypeStatic + obj.SetLabels(labels) } - // Add dashboard labels to dynamic resources - m.addDashboardLabels(obj, crd, ResourceTypeDynamic) + b, err := json.Marshal(spec) if err != nil { return err