Skip to content

Commit a528085

Browse files
committed
[dashboard] refactor dashboard configuration
- Refactor code for dashboard resources creation - Move dashboard-config helm chart to dynamic dashboard controller Signed-off-by: Andrei Kvapil <[email protected]>
1 parent bb653e5 commit a528085

86 files changed

Lines changed: 4635 additions & 6004 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎internal/controller/cozystackresource_controller.go‎

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"sigs.k8s.io/controller-runtime/pkg/client"
2626
"sigs.k8s.io/controller-runtime/pkg/handler"
2727
"sigs.k8s.io/controller-runtime/pkg/log"
28+
"sigs.k8s.io/controller-runtime/pkg/manager"
2829
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2930
)
3031

@@ -39,6 +40,10 @@ type CozystackResourceDefinitionReconciler struct {
3940
lastHandled time.Time
4041

4142
mem *crdmem.Memory
43+
44+
// Track static resources initialization
45+
staticResourcesInitialized bool
46+
staticResourcesMutex sync.Mutex
4247
}
4348

4449
func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
@@ -70,13 +75,22 @@ func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, r
7075
return res, derr
7176
}
7277

78+
// After processing CRD, perform cleanup of orphaned resources
79+
// This should be done after cache warming to ensure all current resources are known
80+
if err := mgr.CleanupOrphanedResources(ctx); err != nil {
81+
logger.Error(err, "Failed to cleanup orphaned dashboard resources")
82+
// Don't fail the reconciliation, just log the error
83+
}
84+
7385
r.mu.Lock()
7486
r.lastEvent = time.Now()
7587
r.mu.Unlock()
7688
return ctrl.Result{}, nil
7789
}
78-
if err != nil && !apierrors.IsNotFound(err) {
79-
return ctrl.Result{}, err
90+
if err != nil {
91+
if !apierrors.IsNotFound(err) {
92+
return ctrl.Result{}, err
93+
}
8094
}
8195
if apierrors.IsNotFound(err) && r.mem != nil {
8296
r.mem.Delete(req.Name)
@@ -87,6 +101,40 @@ func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, r
87101
return ctrl.Result{}, nil
88102
}
89103

104+
// initializeStaticResourcesOnce ensures static resources are created only once
105+
func (r *CozystackResourceDefinitionReconciler) initializeStaticResourcesOnce(ctx context.Context) error {
106+
r.staticResourcesMutex.Lock()
107+
defer r.staticResourcesMutex.Unlock()
108+
109+
if r.staticResourcesInitialized {
110+
return nil // Already initialized
111+
}
112+
113+
// Create dashboard manager and initialize static resources
114+
mgr := dashboard.NewManager(
115+
r.Client,
116+
r.Scheme,
117+
dashboard.WithCRDListFunc(func(c context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error) {
118+
if r.mem != nil {
119+
return r.mem.ListFromCacheOrAPI(c, r.Client)
120+
}
121+
var list cozyv1alpha1.CozystackResourceDefinitionList
122+
if err := r.Client.List(c, &list); err != nil {
123+
return nil, err
124+
}
125+
return list.Items, nil
126+
}),
127+
)
128+
129+
if err := mgr.InitializeStaticResources(ctx); err != nil {
130+
return err
131+
}
132+
133+
r.staticResourcesInitialized = true
134+
log.FromContext(ctx).Info("Static dashboard resources initialized successfully")
135+
return nil
136+
}
137+
90138
func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error {
91139
if r.Debounce == 0 {
92140
r.Debounce = 5 * time.Second
@@ -97,6 +145,18 @@ func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manage
97145
if err := r.mem.EnsurePrimingWithManager(mgr); err != nil {
98146
return err
99147
}
148+
149+
// Initialize static resources once during controller startup using manager.Runnable
150+
if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
151+
if err := r.initializeStaticResourcesOnce(ctx); err != nil {
152+
log.FromContext(ctx).Error(err, "Failed to initialize static resources")
153+
return err
154+
}
155+
return nil
156+
})); err != nil {
157+
return err
158+
}
159+
100160
return ctrl.NewControllerManagedBy(mgr).
101161
Named("cozystackresource-controller").
102162
For(&cozyv1alpha1.CozystackResourceDefinition{}, builder.WithPredicates()).
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package dashboard
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
9+
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
10+
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
11+
12+
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
13+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
14+
)
15+
16+
// ensureBreadcrumb creates or updates a Breadcrumb resource for the given CRD
17+
func (m *Manager) ensureBreadcrumb(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
18+
_, _, kind := pickGVK(crd)
19+
20+
lowerKind := strings.ToLower(kind)
21+
detailID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind)
22+
23+
obj := &dashv1alpha1.Breadcrumb{}
24+
obj.SetName(detailID)
25+
26+
plural := pickPlural(kind, crd)
27+
28+
// Prefer dashboard.Plural for UI label if provided
29+
labelPlural := titleFromKindPlural(kind, plural)
30+
if crd != nil && crd.Spec.Dashboard != nil && crd.Spec.Dashboard.Plural != "" {
31+
labelPlural = crd.Spec.Dashboard.Plural
32+
}
33+
34+
key := plural // e.g., "virtualmachines"
35+
label := labelPlural
36+
link := fmt.Sprintf("/openapi-ui/{clusterName}/{namespace}/api-table/apps.cozystack.io/v1alpha1/%s", plural)
37+
// If Name is set, change the first breadcrumb item to "Tenant Modules"
38+
// TODO add parameter to this
39+
if crd.Spec.Dashboard != nil && strings.TrimSpace(crd.Spec.Dashboard.Name) != "" {
40+
key = "tenantmodules"
41+
label = "Tenant Modules"
42+
link = "/openapi-ui/{clusterName}/{namespace}/api-table/core.cozystack.io/v1alpha1/tenantmodules"
43+
}
44+
45+
items := []any{
46+
map[string]any{
47+
"key": key,
48+
"label": label,
49+
"link": link,
50+
},
51+
map[string]any{
52+
"key": strings.ToLower(kind), // "etcd"
53+
"label": "{6}", // literal, as in your example
54+
},
55+
}
56+
57+
spec := map[string]any{
58+
"id": detailID,
59+
"breadcrumbItems": items,
60+
}
61+
62+
_, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error {
63+
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
64+
return err
65+
}
66+
// Add dashboard labels to dynamic resources
67+
m.addDashboardLabels(obj, crd, ResourceTypeDynamic)
68+
b, err := json.Marshal(spec)
69+
if err != nil {
70+
return err
71+
}
72+
73+
// Only update spec if it's different to avoid unnecessary updates
74+
newSpec := dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}}
75+
if !compareArbitrarySpecs(obj.Spec, newSpec) {
76+
obj.Spec = newSpec
77+
}
78+
return nil
79+
})
80+
return err
81+
}

‎internal/controller/dashboard/customcolumns.go‎

Lines changed: 2 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package dashboard
22

33
import (
44
"context"
5-
"crypto/sha1"
6-
"encoding/hex"
75
"encoding/json"
86
"fmt"
97
"strings"
@@ -12,7 +10,6 @@ import (
1210
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
1311

1412
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
15-
"k8s.io/apimachinery/pkg/runtime/schema"
1613
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1714
)
1815

@@ -38,11 +35,6 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al
3835
badgeColor := hexColorForKind(kind) // deterministic, dark enough for white text
3936

4037
obj := &dashv1alpha1.CustomColumnsOverride{}
41-
obj.SetGroupVersionKind(schema.GroupVersionKind{
42-
Group: "dashboard.cozystack.io",
43-
Version: "v1alpha1",
44-
Kind: "CustomColumnsOverride",
45-
})
4638
obj.SetName(name)
4739

4840
href := fmt.Sprintf("/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/%s/{reqsJsonPath[0]['.metadata.name']['-']}", detailsSegment)
@@ -158,6 +150,8 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al
158150
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
159151
return err
160152
}
153+
// Add dashboard labels to dynamic resources
154+
m.addDashboardLabels(obj, crd, ResourceTypeDynamic)
161155
b, err := json.Marshal(desired["spec"])
162156
if err != nil {
163157
return err
@@ -168,131 +162,3 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al
168162
// Return OperationResultCreated/Updated is not available here with unstructured; we can mimic Updated when no error.
169163
return controllerutil.OperationResultNone, err
170164
}
171-
172-
// --- helpers ---
173-
174-
// pickGVK tries to read group/version/kind from the CRD. We prefer the "application" section,
175-
// falling back to other likely fields if your schema differs.
176-
func pickGVK(crd *cozyv1alpha1.CozystackResourceDefinition) (group, version, kind string) {
177-
// Best guess based on your examples:
178-
if crd.Spec.Application.Kind != "" {
179-
kind = crd.Spec.Application.Kind
180-
}
181-
182-
// Reasonable fallbacks if any are empty:
183-
if group == "" {
184-
group = "apps.cozystack.io"
185-
}
186-
if version == "" {
187-
version = "v1alpha1"
188-
}
189-
if kind == "" {
190-
kind = "Resource"
191-
}
192-
return
193-
}
194-
195-
// pickPlural prefers a field on the CRD if you have it; otherwise do a simple lowercase + "s".
196-
func pickPlural(kind string, crd *cozyv1alpha1.CozystackResourceDefinition) string {
197-
// If you have crd.Spec.Application.Plural, prefer it. Example:
198-
if crd.Spec.Application.Plural != "" {
199-
return crd.Spec.Application.Plural
200-
}
201-
// naive pluralization
202-
k := strings.ToLower(kind)
203-
if strings.HasSuffix(k, "s") {
204-
return k
205-
}
206-
return k + "s"
207-
}
208-
209-
// initialsFromKind splits CamelCase and returns the first letters in upper case.
210-
// "VirtualMachine" -> "VM"; "Bucket" -> "B".
211-
func initialsFromKind(kind string) string {
212-
parts := splitCamel(kind)
213-
if len(parts) == 0 {
214-
return strings.ToUpper(kind)
215-
}
216-
var b strings.Builder
217-
for _, p := range parts {
218-
if p == "" {
219-
continue
220-
}
221-
b.WriteString(strings.ToUpper(string(p[0])))
222-
// Limit to 3 chars to keep the badge compact (VM, PVC, etc.)
223-
if b.Len() >= 3 {
224-
break
225-
}
226-
}
227-
return b.String()
228-
}
229-
230-
// hexColorForKind returns a dark, saturated color (hex) derived from a stable hash of the kind.
231-
// We map the hash to an HSL hue; fix S/L for consistent readability with white text.
232-
func hexColorForKind(kind string) string {
233-
// Stable short hash (sha1 → bytes → hue)
234-
sum := sha1.Sum([]byte(kind))
235-
// Use first two bytes for hue [0..359]
236-
hue := int(sum[0])<<8 | int(sum[1])
237-
hue = hue % 360
238-
239-
// Fixed S/L chosen to contrast with white text:
240-
// S = 80%, L = 35% (dark enough so #fff is readable)
241-
r, g, b := hslToRGB(float64(hue), 0.80, 0.35)
242-
243-
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
244-
}
245-
246-
// hslToRGB converts HSL (0..360, 0..1, 0..1) to sRGB (0..255).
247-
func hslToRGB(h float64, s float64, l float64) (uint8, uint8, uint8) {
248-
c := (1 - absFloat(2*l-1)) * s
249-
hp := h / 60.0
250-
x := c * (1 - absFloat(modFloat(hp, 2)-1))
251-
var r1, g1, b1 float64
252-
switch {
253-
case 0 <= hp && hp < 1:
254-
r1, g1, b1 = c, x, 0
255-
case 1 <= hp && hp < 2:
256-
r1, g1, b1 = x, c, 0
257-
case 2 <= hp && hp < 3:
258-
r1, g1, b1 = 0, c, x
259-
case 3 <= hp && hp < 4:
260-
r1, g1, b1 = 0, x, c
261-
case 4 <= hp && hp < 5:
262-
r1, g1, b1 = x, 0, c
263-
default:
264-
r1, g1, b1 = c, 0, x
265-
}
266-
m := l - c/2
267-
r := uint8(clamp01(r1+m) * 255.0)
268-
g := uint8(clamp01(g1+m) * 255.0)
269-
b := uint8(clamp01(b1+m) * 255.0)
270-
return r, g, b
271-
}
272-
273-
func absFloat(v float64) float64 {
274-
if v < 0 {
275-
return -v
276-
}
277-
return v
278-
}
279-
280-
func modFloat(a, b float64) float64 {
281-
return a - b*float64(int(a/b))
282-
}
283-
284-
func clamp01(v float64) float64 {
285-
if v < 0 {
286-
return 0
287-
}
288-
if v > 1 {
289-
return 1
290-
}
291-
return v
292-
}
293-
294-
// optional: tiny helper to expose the compact color hash (useful for debugging)
295-
func shortHashHex(s string) string {
296-
sum := sha1.Sum([]byte(s))
297-
return hex.EncodeToString(sum[:4])
298-
}

0 commit comments

Comments
 (0)