Skip to content

Commit 0e7debc

Browse files
BaarsgaardtheSuess
authored andcommitted
feat: implement CustomUID for folders
1 parent 0516043 commit 0e7debc

9 files changed

+131
-38
lines changed

api/v1beta1/grafanafolder_types.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,26 @@ import (
3030

3131
// GrafanaFolderSpec defines the desired state of GrafanaFolder
3232
// +kubebuilder:validation:XValidation:rule="(has(self.parentFolderUID) && !(has(self.parentFolderRef))) || (has(self.parentFolderRef) && !(has(self.parentFolderUID))) || !(has(self.parentFolderRef) && (has(self.parentFolderUID)))", message="Only one of parentFolderUID or parentFolderRef can be set"
33+
// +kubebuilder:validation:XValidation:rule="((!has(oldSelf.uid) && !has(self.uid)) || (has(oldSelf.uid) && has(self.uid)))", message="spec.uid is immutable"
3334
type GrafanaFolderSpec struct {
35+
// Manually specify the UID the Folder is created with
36+
// +optional
37+
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec.uid is immutable"
38+
CustomUID string `json:"uid,omitempty"`
39+
40+
// Display name of the folder in Grafana
3441
// +optional
3542
Title string `json:"title,omitempty"`
3643

37-
// raw json with folder permissions
44+
// Raw json with folder permissions, potentially exported from Grafana
3845
// +optional
3946
Permissions string `json:"permissions,omitempty"`
4047

41-
// selects Grafanas for import
48+
// Selects Grafanas for import
4249
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
4350
InstanceSelector *metav1.LabelSelector `json:"instanceSelector"`
4451

45-
// allow to import this resources from an operator in a different namespace
52+
// Enable matching Grafana instances outside the current namespace
4653
// +optional
4754
AllowCrossNamespaceImport *bool `json:"allowCrossNamespaceImport,omitempty"`
4855

@@ -54,7 +61,7 @@ type GrafanaFolderSpec struct {
5461
// +optional
5562
ParentFolderRef string `json:"parentFolderRef,omitempty"`
5663

57-
// how often the folder is synced, defaults to 5m if not set
64+
// How often the folder is synced, defaults to 5m if not set
5865
// +optional
5966
// +kubebuilder:validation:Type=string
6067
// +kubebuilder:validation:Format=duration
@@ -115,6 +122,14 @@ func (in *GrafanaFolder) FolderUID() string {
115122
return in.Spec.ParentFolderUID
116123
}
117124

125+
// Wrapper around CustomUID or default metadata.uid
126+
func (in *GrafanaFolder) CustomUIDOrUID() string {
127+
if in.Spec.CustomUID != "" {
128+
return in.Spec.CustomUID
129+
}
130+
return string(in.ObjectMeta.UID)
131+
}
132+
118133
var _ operatorapi.FolderReferencer = (*GrafanaFolder)(nil)
119134

120135
//+kubebuilder:object:root=true

api/v1beta1/grafanafolder_types_test.go

+33
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,36 @@ func TestGrafanaFolder_GetTitle(t *testing.T) {
3939
})
4040
}
4141
}
42+
43+
func TestGrafanaFolder_GetUID(t *testing.T) {
44+
tests := []struct {
45+
name string
46+
cr GrafanaFolder
47+
want string
48+
}{
49+
{
50+
name: "No custom UID",
51+
cr: GrafanaFolder{
52+
ObjectMeta: metav1.ObjectMeta{UID: "92fd2e0a-ad63-4fcf-9890-68a527cbd674"},
53+
},
54+
want: "92fd2e0a-ad63-4fcf-9890-68a527cbd674",
55+
},
56+
{
57+
name: "Custom UID",
58+
cr: GrafanaFolder{
59+
ObjectMeta: metav1.ObjectMeta{UID: "92fd2e0a-ad63-4fcf-9890-68a527cbd674"},
60+
Spec: GrafanaFolderSpec{
61+
CustomUID: "custom-uid",
62+
},
63+
},
64+
want: "custom-uid",
65+
},
66+
}
67+
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
got := tt.cr.CustomUIDOrUID()
71+
assert.Equal(t, tt.want, got)
72+
})
73+
}
74+
}

config/crd/bases/grafana.integreatly.org_grafanafolders.yaml

+16-5
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ spec:
4949
description: GrafanaFolderSpec defines the desired state of GrafanaFolder
5050
properties:
5151
allowCrossNamespaceImport:
52-
description: allow to import this resources from an operator in a
53-
different namespace
52+
description: Enable matching Grafana instances outside the current
53+
namespace
5454
type: boolean
5555
instanceSelector:
56-
description: selects Grafanas for import
56+
description: Selects Grafanas for import
5757
properties:
5858
matchExpressions:
5959
description: matchExpressions is a list of label selector requirements.
@@ -110,17 +110,25 @@ spec:
110110
be created
111111
type: string
112112
permissions:
113-
description: raw json with folder permissions
113+
description: Raw json with folder permissions, potnetially exported
114+
from Grafana
114115
type: string
115116
resyncPeriod:
116117
default: 5m
117-
description: how often the folder is synced, defaults to 5m if not
118+
description: How often the folder is synced, defaults to 5m if not
118119
set
119120
format: duration
120121
pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$
121122
type: string
122123
title:
124+
description: Display name of the folder in Grafana
125+
type: string
126+
uid:
127+
description: Manually specify the UID the Folder is created with
123128
type: string
129+
x-kubernetes-validations:
130+
- message: spec.uid is immutable
131+
rule: self == oldSelf
124132
required:
125133
- instanceSelector
126134
type: object
@@ -129,6 +137,9 @@ spec:
129137
rule: (has(self.parentFolderUID) && !(has(self.parentFolderRef))) ||
130138
(has(self.parentFolderRef) && !(has(self.parentFolderUID))) || !(has(self.parentFolderRef)
131139
&& (has(self.parentFolderUID)))
140+
- message: spec.uid is immutable
141+
rule: ((!has(oldSelf.uid) && !has(self.uid)) || (has(oldSelf.uid) &&
142+
has(self.uid)))
132143
status:
133144
description: GrafanaFolderStatus defines the observed state of GrafanaFolder
134145
properties:

controllers/controller_shared.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func getFolderUID(ctx context.Context, k8sClient client.Client, ref operatorapi.
8282
}
8383
removeNoMatchingFolder(ref.Conditions())
8484

85-
return string(folder.ObjectMeta.UID), nil
85+
return folder.CustomUIDOrUID(), nil
8686
}
8787

8888
func labelsSatisfyMatchExpressions(labels map[string]string, matchExpressions []metav1.LabelSelectorRequirement) bool {

controllers/dashboard_controller.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -699,15 +699,16 @@ func (r *GrafanaDashboardReconciler) GetFolderUID(
699699
limit := int64(1000)
700700
for {
701701
params := folders.NewGetFoldersParams().WithPage(&page).WithLimit(&limit)
702-
resp, err := client.Folders.GetFolders(params)
702+
703+
foldersResp, err := client.Folders.GetFolders(params)
703704
if err != nil {
704705
return false, "", err
705706
}
706-
folders := resp.GetPayload()
707+
folders := foldersResp.GetPayload()
707708

708-
for _, folder := range folders {
709-
if strings.EqualFold(folder.Title, title) {
710-
return true, folder.UID, nil
709+
for _, remoteFolder := range folders {
710+
if strings.EqualFold(remoteFolder.Title, title) {
711+
return true, remoteFolder.UID, nil
711712
}
712713
}
713714
if len(folders) < int(limit) {

controllers/grafanafolder_controller.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ func (r *GrafanaFolderReconciler) Reconcile(ctx context.Context, req ctrl.Reques
186186
}
187187
}()
188188

189-
if folder.Spec.ParentFolderUID == string(folder.UID) {
189+
if folder.Spec.ParentFolderUID == folder.CustomUIDOrUID() {
190190
setInvalidSpec(&folder.Status.Conditions, folder.Generation, "CyclicParent", "The value of parentFolderUID must not be the uid of the current folder")
191191
meta.RemoveStatusCondition(&folder.Status.Conditions, conditionFolderSynchronized)
192192
return ctrl.Result{}, fmt.Errorf("cyclic folder reference")
@@ -314,7 +314,7 @@ func (r *GrafanaFolderReconciler) onFolderDeleted(ctx context.Context, namespace
314314

315315
func (r *GrafanaFolderReconciler) onFolderCreated(ctx context.Context, grafana *grafanav1beta1.Grafana, cr *grafanav1beta1.GrafanaFolder) error {
316316
title := cr.GetTitle()
317-
uid := string(cr.UID)
317+
uid := cr.CustomUIDOrUID()
318318

319319
grafanaClient, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, grafana)
320320
if err != nil {
@@ -413,7 +413,7 @@ func (r *GrafanaFolderReconciler) UpdateStatus(ctx context.Context, cr *grafanav
413413
// Check if the folder exists. Matches UID first and fall back to title. Title matching only works for non-nested folders
414414
func (r *GrafanaFolderReconciler) Exists(client *genapi.GrafanaHTTPAPI, cr *grafanav1beta1.GrafanaFolder) (bool, string, string, error) {
415415
title := cr.GetTitle()
416-
uid := string(cr.UID)
416+
uid := cr.CustomUIDOrUID()
417417

418418
uidResp, err := client.Folders.GetFolderByUID(uid)
419419
if err == nil {
@@ -429,12 +429,14 @@ func (r *GrafanaFolderReconciler) Exists(client *genapi.GrafanaHTTPAPI, cr *graf
429429
if err != nil {
430430
return false, "", "", err
431431
}
432-
for _, folder := range foldersResp.Payload {
433-
if strings.EqualFold(folder.Title, title) {
434-
return true, folder.UID, folder.ParentUID, nil
432+
folders := foldersResp.GetPayload()
433+
434+
for _, remoteFolder := range folders {
435+
if strings.EqualFold(remoteFolder.Title, title) {
436+
return true, remoteFolder.UID, remoteFolder.ParentUID, nil
435437
}
436438
}
437-
if len(foldersResp.Payload) < int(limit) {
439+
if len(folders) < int(limit) {
438440
return false, "", "", nil
439441
}
440442
page++

deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanafolders.yaml

+16-5
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ spec:
4949
description: GrafanaFolderSpec defines the desired state of GrafanaFolder
5050
properties:
5151
allowCrossNamespaceImport:
52-
description: allow to import this resources from an operator in a
53-
different namespace
52+
description: Enable matching Grafana instances outside the current
53+
namespace
5454
type: boolean
5555
instanceSelector:
56-
description: selects Grafanas for import
56+
description: Selects Grafanas for import
5757
properties:
5858
matchExpressions:
5959
description: matchExpressions is a list of label selector requirements.
@@ -110,17 +110,25 @@ spec:
110110
be created
111111
type: string
112112
permissions:
113-
description: raw json with folder permissions
113+
description: Raw json with folder permissions, potnetially exported
114+
from Grafana
114115
type: string
115116
resyncPeriod:
116117
default: 5m
117-
description: how often the folder is synced, defaults to 5m if not
118+
description: How often the folder is synced, defaults to 5m if not
118119
set
119120
format: duration
120121
pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$
121122
type: string
122123
title:
124+
description: Display name of the folder in Grafana
125+
type: string
126+
uid:
127+
description: Manually specify the UID the Folder is created with
123128
type: string
129+
x-kubernetes-validations:
130+
- message: spec.uid is immutable
131+
rule: self == oldSelf
124132
required:
125133
- instanceSelector
126134
type: object
@@ -129,6 +137,9 @@ spec:
129137
rule: (has(self.parentFolderUID) && !(has(self.parentFolderRef))) ||
130138
(has(self.parentFolderRef) && !(has(self.parentFolderUID))) || !(has(self.parentFolderRef)
131139
&& (has(self.parentFolderUID)))
140+
- message: spec.uid is immutable
141+
rule: ((!has(oldSelf.uid) && !has(self.uid)) || (has(oldSelf.uid) &&
142+
has(self.uid)))
132143
status:
133144
description: GrafanaFolderStatus defines the observed state of GrafanaFolder
134145
properties:

deploy/kustomize/base/crds.yaml

+16-5
Original file line numberDiff line numberDiff line change
@@ -1348,11 +1348,11 @@ spec:
13481348
description: GrafanaFolderSpec defines the desired state of GrafanaFolder
13491349
properties:
13501350
allowCrossNamespaceImport:
1351-
description: allow to import this resources from an operator in a
1352-
different namespace
1351+
description: Enable matching Grafana instances outside the current
1352+
namespace
13531353
type: boolean
13541354
instanceSelector:
1355-
description: selects Grafanas for import
1355+
description: Selects Grafanas for import
13561356
properties:
13571357
matchExpressions:
13581358
description: matchExpressions is a list of label selector requirements.
@@ -1409,17 +1409,25 @@ spec:
14091409
be created
14101410
type: string
14111411
permissions:
1412-
description: raw json with folder permissions
1412+
description: Raw json with folder permissions, potnetially exported
1413+
from Grafana
14131414
type: string
14141415
resyncPeriod:
14151416
default: 5m
1416-
description: how often the folder is synced, defaults to 5m if not
1417+
description: How often the folder is synced, defaults to 5m if not
14171418
set
14181419
format: duration
14191420
pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$
14201421
type: string
14211422
title:
1423+
description: Display name of the folder in Grafana
14221424
type: string
1425+
uid:
1426+
description: Manually specify the UID the Folder is created with
1427+
type: string
1428+
x-kubernetes-validations:
1429+
- message: spec.uid is immutable
1430+
rule: self == oldSelf
14231431
required:
14241432
- instanceSelector
14251433
type: object
@@ -1428,6 +1436,9 @@ spec:
14281436
rule: (has(self.parentFolderUID) && !(has(self.parentFolderRef))) ||
14291437
(has(self.parentFolderRef) && !(has(self.parentFolderUID))) || !(has(self.parentFolderRef)
14301438
&& (has(self.parentFolderUID)))
1439+
- message: spec.uid is immutable
1440+
rule: ((!has(oldSelf.uid) && !has(self.uid)) || (has(oldSelf.uid) &&
1441+
has(self.uid)))
14311442
status:
14321443
description: GrafanaFolderStatus defines the observed state of GrafanaFolder
14331444
properties:

docs/docs/api.md

+15-6
Original file line numberDiff line numberDiff line change
@@ -2807,7 +2807,7 @@ GrafanaFolder is the Schema for the grafanafolders API
28072807
<td>
28082808
GrafanaFolderSpec defines the desired state of GrafanaFolder<br/>
28092809
<br/>
2810-
<i>Validations</i>:<li>(has(self.parentFolderUID) && !(has(self.parentFolderRef))) || (has(self.parentFolderRef) && !(has(self.parentFolderUID))) || !(has(self.parentFolderRef) && (has(self.parentFolderUID))): Only one of parentFolderUID or parentFolderRef can be set</li>
2810+
<i>Validations</i>:<li>(has(self.parentFolderUID) && !(has(self.parentFolderRef))) || (has(self.parentFolderRef) && !(has(self.parentFolderUID))) || !(has(self.parentFolderRef) && (has(self.parentFolderUID))): Only one of parentFolderUID or parentFolderRef can be set</li><li>((!has(oldSelf.uid) && !has(self.uid)) || (has(oldSelf.uid) && has(self.uid))): spec.uid is immutable</li>
28112811
</td>
28122812
<td>false</td>
28132813
</tr><tr>
@@ -2841,7 +2841,7 @@ GrafanaFolderSpec defines the desired state of GrafanaFolder
28412841
<td><b><a href="#grafanafolderspecinstanceselector">instanceSelector</a></b></td>
28422842
<td>object</td>
28432843
<td>
2844-
selects Grafanas for import<br/>
2844+
Selects Grafanas for import<br/>
28452845
<br/>
28462846
<i>Validations</i>:<li>self == oldSelf: Value is immutable</li>
28472847
</td>
@@ -2850,7 +2850,7 @@ GrafanaFolderSpec defines the desired state of GrafanaFolder
28502850
<td><b>allowCrossNamespaceImport</b></td>
28512851
<td>boolean</td>
28522852
<td>
2853-
allow to import this resources from an operator in a different namespace<br/>
2853+
Enable matching Grafana instances outside the current namespace<br/>
28542854
</td>
28552855
<td>false</td>
28562856
</tr><tr>
@@ -2871,14 +2871,14 @@ GrafanaFolderSpec defines the desired state of GrafanaFolder
28712871
<td><b>permissions</b></td>
28722872
<td>string</td>
28732873
<td>
2874-
raw json with folder permissions<br/>
2874+
Raw json with folder permissions, potnetially exported from Grafana<br/>
28752875
</td>
28762876
<td>false</td>
28772877
</tr><tr>
28782878
<td><b>resyncPeriod</b></td>
28792879
<td>string</td>
28802880
<td>
2881-
how often the folder is synced, defaults to 5m if not set<br/>
2881+
How often the folder is synced, defaults to 5m if not set<br/>
28822882
<br/>
28832883
<i>Format</i>: duration<br/>
28842884
<i>Default</i>: 5m<br/>
@@ -2888,7 +2888,16 @@ GrafanaFolderSpec defines the desired state of GrafanaFolder
28882888
<td><b>title</b></td>
28892889
<td>string</td>
28902890
<td>
2891+
Display name of the folder in Grafana<br/>
2892+
</td>
2893+
<td>false</td>
2894+
</tr><tr>
2895+
<td><b>uid</b></td>
2896+
<td>string</td>
2897+
<td>
2898+
Manually specify the UID the Folder is created with<br/>
28912899
<br/>
2900+
<i>Validations</i>:<li>self == oldSelf: spec.uid is immutable</li>
28922901
</td>
28932902
<td>false</td>
28942903
</tr></tbody>
@@ -2900,7 +2909,7 @@ GrafanaFolderSpec defines the desired state of GrafanaFolder
29002909

29012910

29022911

2903-
selects Grafanas for import
2912+
Selects Grafanas for import
29042913

29052914
<table>
29062915
<thead>

0 commit comments

Comments
 (0)