Skip to content

Commit 9873011

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

90 files changed

Lines changed: 4861 additions & 6014 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.

‎cmd/cozystack-controller/main.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,12 @@ func main() {
153153
// this setup is not recommended for production.
154154
}
155155

156-
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
156+
// Configure rate limiting for the Kubernetes client
157+
config := ctrl.GetConfigOrDie()
158+
config.QPS = 50.0 // Increased from default 5.0
159+
config.Burst = 100 // Increased from default 10
160+
161+
mgr, err := ctrl.NewManager(config, ctrl.Options{
157162
Scheme: scheme,
158163
Metrics: metricsServerOptions,
159164
WebhookServer: webhookServer,

‎internal/controller/cozystackresource_controller.go‎

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import (
2323
ctrl "sigs.k8s.io/controller-runtime"
2424
"sigs.k8s.io/controller-runtime/pkg/builder"
2525
"sigs.k8s.io/controller-runtime/pkg/client"
26+
"sigs.k8s.io/controller-runtime/pkg/controller"
2627
"sigs.k8s.io/controller-runtime/pkg/handler"
2728
"sigs.k8s.io/controller-runtime/pkg/log"
29+
"sigs.k8s.io/controller-runtime/pkg/manager"
2830
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2931
)
3032

@@ -39,6 +41,10 @@ type CozystackResourceDefinitionReconciler struct {
3941
lastHandled time.Time
4042

4143
mem *crdmem.Memory
44+
45+
// Track static resources initialization
46+
staticResourcesInitialized bool
47+
staticResourcesMutex sync.Mutex
4248
}
4349

4450
func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
@@ -70,15 +76,25 @@ func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, r
7076
return res, derr
7177
}
7278

79+
// After processing CRD, perform cleanup of orphaned resources
80+
// This should be done after cache warming to ensure all current resources are known
81+
if cleanupErr := mgr.CleanupOrphanedResources(ctx); cleanupErr != nil {
82+
logger.Error(cleanupErr, "Failed to cleanup orphaned dashboard resources")
83+
// Don't fail the reconciliation, just log the error
84+
}
85+
7386
r.mu.Lock()
7487
r.lastEvent = time.Now()
7588
r.mu.Unlock()
7689
return ctrl.Result{}, nil
7790
}
78-
if err != nil && !apierrors.IsNotFound(err) {
91+
92+
// Handle error cases (err is guaranteed to be non-nil here)
93+
if !apierrors.IsNotFound(err) {
7994
return ctrl.Result{}, err
8095
}
81-
if apierrors.IsNotFound(err) && r.mem != nil {
96+
// If resource is not found, clean up from memory
97+
if r.mem != nil {
8298
r.mem.Delete(req.Name)
8399
}
84100
if req.Namespace == "cozy-system" && req.Name == "cozystack-api" {
@@ -87,6 +103,40 @@ func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, r
87103
return ctrl.Result{}, nil
88104
}
89105

106+
// initializeStaticResourcesOnce ensures static resources are created only once
107+
func (r *CozystackResourceDefinitionReconciler) initializeStaticResourcesOnce(ctx context.Context) error {
108+
r.staticResourcesMutex.Lock()
109+
defer r.staticResourcesMutex.Unlock()
110+
111+
if r.staticResourcesInitialized {
112+
return nil // Already initialized
113+
}
114+
115+
// Create dashboard manager and initialize static resources
116+
mgr := dashboard.NewManager(
117+
r.Client,
118+
r.Scheme,
119+
dashboard.WithCRDListFunc(func(c context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error) {
120+
if r.mem != nil {
121+
return r.mem.ListFromCacheOrAPI(c, r.Client)
122+
}
123+
var list cozyv1alpha1.CozystackResourceDefinitionList
124+
if err := r.Client.List(c, &list); err != nil {
125+
return nil, err
126+
}
127+
return list.Items, nil
128+
}),
129+
)
130+
131+
if err := mgr.InitializeStaticResources(ctx); err != nil {
132+
return err
133+
}
134+
135+
r.staticResourcesInitialized = true
136+
log.FromContext(ctx).Info("Static dashboard resources initialized successfully")
137+
return nil
138+
}
139+
90140
func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error {
91141
if r.Debounce == 0 {
92142
r.Debounce = 5 * time.Second
@@ -97,6 +147,18 @@ func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manage
97147
if err := r.mem.EnsurePrimingWithManager(mgr); err != nil {
98148
return err
99149
}
150+
151+
// Initialize static resources once during controller startup using manager.Runnable
152+
if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
153+
if err := r.initializeStaticResourcesOnce(ctx); err != nil {
154+
log.FromContext(ctx).Error(err, "Failed to initialize static resources")
155+
return err
156+
}
157+
return nil
158+
})); err != nil {
159+
return err
160+
}
161+
100162
return ctrl.NewControllerManagedBy(mgr).
101163
Named("cozystackresource-controller").
102164
For(&cozyv1alpha1.CozystackResourceDefinition{}, builder.WithPredicates()).
@@ -114,6 +176,9 @@ func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manage
114176
}}
115177
}),
116178
).
179+
WithOptions(controller.Options{
180+
MaxConcurrentReconciles: 5, // Allow more concurrent reconciles with proper rate limiting
181+
}).
117182
Complete(r)
118183
}
119184

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+
group, version, 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/%s/%s/%s", strings.ToLower(group), strings.ToLower(version), 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: 8 additions & 138 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)
@@ -153,146 +145,24 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al
153145
},
154146
}
155147

156-
// CreateOrUpdate using typed resource
157148
_, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error {
158149
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
159150
return err
160151
}
152+
// Add dashboard labels to dynamic resources
153+
m.addDashboardLabels(obj, crd, ResourceTypeDynamic)
161154
b, err := json.Marshal(desired["spec"])
162155
if err != nil {
163156
return err
164157
}
165-
obj.Spec = dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}}
158+
159+
// Only update spec if it's different to avoid unnecessary updates
160+
newSpec := dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}}
161+
if !compareArbitrarySpecs(obj.Spec, newSpec) {
162+
obj.Spec = newSpec
163+
}
166164
return nil
167165
})
168166
// Return OperationResultCreated/Updated is not available here with unstructured; we can mimic Updated when no error.
169167
return controllerutil.OperationResultNone, err
170168
}
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)