Skip to content

Commit

Permalink
feat: implement CustomUID for folders
Browse files Browse the repository at this point in the history
  • Loading branch information
Baarsgaard authored and theSuess committed Oct 16, 2024
1 parent 0516043 commit 9455545
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 38 deletions.
23 changes: 19 additions & 4 deletions api/v1beta1/grafanafolder_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,26 @@ import (

// GrafanaFolderSpec defines the desired state of GrafanaFolder
// +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"
// +kubebuilder:validation:XValidation:rule="((!has(oldSelf.uid) && !has(self.uid)) || (has(oldSelf.uid) && has(self.uid)))", message="spec.uid is immutable"
type GrafanaFolderSpec struct {
// Manually specify the UID the Folder is created with
// +optional
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec.uid is immutable"
CustomUID string `json:"uid,omitempty"`

// Display name of the folder in Grafana
// +optional
Title string `json:"title,omitempty"`

// raw json with folder permissions
// Raw json with folder permissions, potentially exported from Grafana
// +optional
Permissions string `json:"permissions,omitempty"`

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

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

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

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

// Wrapper around CustomUID or default metadata.uid
func (in *GrafanaFolder) CustomUIDOrUID() string {
if in.Spec.CustomUID != "" {
return in.Spec.CustomUID
}
return string(in.ObjectMeta.UID)
}

var _ operatorapi.FolderReferencer = (*GrafanaFolder)(nil)

//+kubebuilder:object:root=true
Expand Down
33 changes: 33 additions & 0 deletions api/v1beta1/grafanafolder_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,36 @@ func TestGrafanaFolder_GetTitle(t *testing.T) {
})
}
}

func TestGrafanaFolder_GetUID(t *testing.T) {
tests := []struct {
name string
cr GrafanaFolder
want string
}{
{
name: "No custom UID",
cr: GrafanaFolder{
ObjectMeta: metav1.ObjectMeta{UID: "92fd2e0a-ad63-4fcf-9890-68a527cbd674"},
},
want: "92fd2e0a-ad63-4fcf-9890-68a527cbd674",
},
{
name: "Custom UID",
cr: GrafanaFolder{
ObjectMeta: metav1.ObjectMeta{UID: "92fd2e0a-ad63-4fcf-9890-68a527cbd674"},
Spec: GrafanaFolderSpec{
CustomUID: "custom-uid",
},
},
want: "custom-uid",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.cr.CustomUIDOrUID()
assert.Equal(t, tt.want, got)
})
}
}
21 changes: 16 additions & 5 deletions config/crd/bases/grafana.integreatly.org_grafanafolders.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ spec:
description: GrafanaFolderSpec defines the desired state of GrafanaFolder
properties:
allowCrossNamespaceImport:
description: allow to import this resources from an operator in a
different namespace
description: Enable matching Grafana instances outside the current
namespace
type: boolean
instanceSelector:
description: selects Grafanas for import
description: Selects Grafanas for import
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements.
Expand Down Expand Up @@ -110,17 +110,25 @@ spec:
be created
type: string
permissions:
description: raw json with folder permissions
description: Raw json with folder permissions, potentially exported
from Grafana
type: string
resyncPeriod:
default: 5m
description: how often the folder is synced, defaults to 5m if not
description: How often the folder is synced, defaults to 5m if not
set
format: duration
pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$
type: string
title:
description: Display name of the folder in Grafana
type: string
uid:
description: Manually specify the UID the Folder is created with
type: string
x-kubernetes-validations:
- message: spec.uid is immutable
rule: self == oldSelf
required:
- instanceSelector
type: object
Expand All @@ -129,6 +137,9 @@ spec:
rule: (has(self.parentFolderUID) && !(has(self.parentFolderRef))) ||
(has(self.parentFolderRef) && !(has(self.parentFolderUID))) || !(has(self.parentFolderRef)
&& (has(self.parentFolderUID)))
- message: spec.uid is immutable
rule: ((!has(oldSelf.uid) && !has(self.uid)) || (has(oldSelf.uid) &&
has(self.uid)))
status:
description: GrafanaFolderStatus defines the observed state of GrafanaFolder
properties:
Expand Down
2 changes: 1 addition & 1 deletion controllers/controller_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func getFolderUID(ctx context.Context, k8sClient client.Client, ref operatorapi.
}
removeNoMatchingFolder(ref.Conditions())

return string(folder.ObjectMeta.UID), nil
return folder.CustomUIDOrUID(), nil
}

func labelsSatisfyMatchExpressions(labels map[string]string, matchExpressions []metav1.LabelSelectorRequirement) bool {
Expand Down
11 changes: 6 additions & 5 deletions controllers/dashboard_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,15 +699,16 @@ func (r *GrafanaDashboardReconciler) GetFolderUID(
limit := int64(1000)
for {
params := folders.NewGetFoldersParams().WithPage(&page).WithLimit(&limit)
resp, err := client.Folders.GetFolders(params)

foldersResp, err := client.Folders.GetFolders(params)
if err != nil {
return false, "", err
}
folders := resp.GetPayload()
folders := foldersResp.GetPayload()

for _, folder := range folders {
if strings.EqualFold(folder.Title, title) {
return true, folder.UID, nil
for _, remoteFolder := range folders {
if strings.EqualFold(remoteFolder.Title, title) {
return true, remoteFolder.UID, nil
}
}
if len(folders) < int(limit) {
Expand Down
16 changes: 9 additions & 7 deletions controllers/grafanafolder_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func (r *GrafanaFolderReconciler) Reconcile(ctx context.Context, req ctrl.Reques
}
}()

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

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

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

uidResp, err := client.Folders.GetFolderByUID(uid)
if err == nil {
Expand All @@ -429,12 +429,14 @@ func (r *GrafanaFolderReconciler) Exists(client *genapi.GrafanaHTTPAPI, cr *graf
if err != nil {
return false, "", "", err
}
for _, folder := range foldersResp.Payload {
if strings.EqualFold(folder.Title, title) {
return true, folder.UID, folder.ParentUID, nil
folders := foldersResp.GetPayload()

for _, remoteFolder := range folders {
if strings.EqualFold(remoteFolder.Title, title) {
return true, remoteFolder.UID, remoteFolder.ParentUID, nil
}
}
if len(foldersResp.Payload) < int(limit) {
if len(folders) < int(limit) {
return false, "", "", nil
}
page++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ spec:
description: GrafanaFolderSpec defines the desired state of GrafanaFolder
properties:
allowCrossNamespaceImport:
description: allow to import this resources from an operator in a
different namespace
description: Enable matching Grafana instances outside the current
namespace
type: boolean
instanceSelector:
description: selects Grafanas for import
description: Selects Grafanas for import
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements.
Expand Down Expand Up @@ -110,17 +110,25 @@ spec:
be created
type: string
permissions:
description: raw json with folder permissions
description: Raw json with folder permissions, potentially exported
from Grafana
type: string
resyncPeriod:
default: 5m
description: how often the folder is synced, defaults to 5m if not
description: How often the folder is synced, defaults to 5m if not
set
format: duration
pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$
type: string
title:
description: Display name of the folder in Grafana
type: string
uid:
description: Manually specify the UID the Folder is created with
type: string
x-kubernetes-validations:
- message: spec.uid is immutable
rule: self == oldSelf
required:
- instanceSelector
type: object
Expand All @@ -129,6 +137,9 @@ spec:
rule: (has(self.parentFolderUID) && !(has(self.parentFolderRef))) ||
(has(self.parentFolderRef) && !(has(self.parentFolderUID))) || !(has(self.parentFolderRef)
&& (has(self.parentFolderUID)))
- message: spec.uid is immutable
rule: ((!has(oldSelf.uid) && !has(self.uid)) || (has(oldSelf.uid) &&
has(self.uid)))
status:
description: GrafanaFolderStatus defines the observed state of GrafanaFolder
properties:
Expand Down
21 changes: 16 additions & 5 deletions deploy/kustomize/base/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1348,11 +1348,11 @@ spec:
description: GrafanaFolderSpec defines the desired state of GrafanaFolder
properties:
allowCrossNamespaceImport:
description: allow to import this resources from an operator in a
different namespace
description: Enable matching Grafana instances outside the current
namespace
type: boolean
instanceSelector:
description: selects Grafanas for import
description: Selects Grafanas for import
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements.
Expand Down Expand Up @@ -1409,17 +1409,25 @@ spec:
be created
type: string
permissions:
description: raw json with folder permissions
description: Raw json with folder permissions, potentially exported
from Grafana
type: string
resyncPeriod:
default: 5m
description: how often the folder is synced, defaults to 5m if not
description: How often the folder is synced, defaults to 5m if not
set
format: duration
pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$
type: string
title:
description: Display name of the folder in Grafana
type: string
uid:
description: Manually specify the UID the Folder is created with
type: string
x-kubernetes-validations:
- message: spec.uid is immutable
rule: self == oldSelf
required:
- instanceSelector
type: object
Expand All @@ -1428,6 +1436,9 @@ spec:
rule: (has(self.parentFolderUID) && !(has(self.parentFolderRef))) ||
(has(self.parentFolderRef) && !(has(self.parentFolderUID))) || !(has(self.parentFolderRef)
&& (has(self.parentFolderUID)))
- message: spec.uid is immutable
rule: ((!has(oldSelf.uid) && !has(self.uid)) || (has(oldSelf.uid) &&
has(self.uid)))
status:
description: GrafanaFolderStatus defines the observed state of GrafanaFolder
properties:
Expand Down
21 changes: 15 additions & 6 deletions docs/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2807,7 +2807,7 @@ GrafanaFolder is the Schema for the grafanafolders API
<td>
GrafanaFolderSpec defines the desired state of GrafanaFolder<br/>
<br/>
<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>
<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>
</td>
<td>false</td>
</tr><tr>
Expand Down Expand Up @@ -2841,7 +2841,7 @@ GrafanaFolderSpec defines the desired state of GrafanaFolder
<td><b><a href="#grafanafolderspecinstanceselector">instanceSelector</a></b></td>
<td>object</td>
<td>
selects Grafanas for import<br/>
Selects Grafanas for import<br/>
<br/>
<i>Validations</i>:<li>self == oldSelf: Value is immutable</li>
</td>
Expand All @@ -2850,7 +2850,7 @@ GrafanaFolderSpec defines the desired state of GrafanaFolder
<td><b>allowCrossNamespaceImport</b></td>
<td>boolean</td>
<td>
allow to import this resources from an operator in a different namespace<br/>
Enable matching Grafana instances outside the current namespace<br/>
</td>
<td>false</td>
</tr><tr>
Expand All @@ -2871,14 +2871,14 @@ GrafanaFolderSpec defines the desired state of GrafanaFolder
<td><b>permissions</b></td>
<td>string</td>
<td>
raw json with folder permissions<br/>
Raw json with folder permissions, potentially exported from Grafana<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>resyncPeriod</b></td>
<td>string</td>
<td>
how often the folder is synced, defaults to 5m if not set<br/>
How often the folder is synced, defaults to 5m if not set<br/>
<br/>
<i>Format</i>: duration<br/>
<i>Default</i>: 5m<br/>
Expand All @@ -2888,7 +2888,16 @@ GrafanaFolderSpec defines the desired state of GrafanaFolder
<td><b>title</b></td>
<td>string</td>
<td>
Display name of the folder in Grafana<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>uid</b></td>
<td>string</td>
<td>
Manually specify the UID the Folder is created with<br/>
<br/>
<i>Validations</i>:<li>self == oldSelf: spec.uid is immutable</li>
</td>
<td>false</td>
</tr></tbody>
Expand All @@ -2900,7 +2909,7 @@ GrafanaFolderSpec defines the desired state of GrafanaFolder



selects Grafanas for import
Selects Grafanas for import

<table>
<thead>
Expand Down

0 comments on commit 9455545

Please sign in to comment.