Skip to content

Commit

Permalink
Merge pull request #1686 from Baarsgaard/master
Browse files Browse the repository at this point in the history
Implement `spec.uid` for `GrafanaFolder`
  • Loading branch information
theSuess authored Oct 16, 2024
2 parents eb1e1a8 + 9455545 commit 0100aa2
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 86 deletions.
88 changes: 48 additions & 40 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
###
# NOTE: this section almost matches outputs out kubebuilder v3.7.0
###
# Current Operator version
VERSION ?= 5.14.0

Expand All @@ -15,13 +12,6 @@ ifeq ($(USE_IMAGE_DIGESTS), true)
BUNDLE_GEN_FLAGS += --use-image-digests
endif

# Set the Operator SDK version to use. By default, what is installed on the system is used.
# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
OPERATOR_SDK_VERSION ?= v1.32.0

OPM_VERSION ?= v1.23.2
YQ_VERSION ?= v4.35.2

# Read Grafana Image and Version from go code
GRAFANA_IMAGE := $(shell grep 'GrafanaImage' controllers/config/operator_constants.go | sed 's/.*"\(.*\)".*/\1/')
GRAFANA_VERSION := $(shell grep 'GrafanaVersion' controllers/config/operator_constants.go | sed 's/.*"\(.*\)".*/\1/')
Expand Down Expand Up @@ -74,23 +64,6 @@ all: manifests test api-docs
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

.PHONY: yq
YQ = ./bin/yq
yq: ## Download yq locally if necessary.
ifeq (,$(wildcard $(YQ)))
ifeq (,$(shell which yq 2>/dev/null))
@{ \
set -e ;\
mkdir -p $(dir $(YQ)) ;\
OSTYPE=$(shell uname | awk '{print tolower($$0)}') && ARCH=$(shell go env GOARCH) && \
curl -sSLo $(YQ) https://github.com/mikefarah/yq/releases/download/$(YQ_VERSION)/yq_$${OSTYPE}_$${ARCH} ;\
chmod +x $(YQ) ;\
}
else
YQ = $(shell which yq)
endif
endif

##@ Development

.PHONY: manifests
Expand Down Expand Up @@ -161,6 +134,10 @@ deploy-chainsaw: manifests kustomize ## Deploy controller to the K8s cluster spe
undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
$(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f -

.PHONY: start-kind
start-kind: kind ## Start kind cluster locally
@hack/kind/start-kind.sh

##@ Build Dependencies

## Location to install dependencies to
Expand All @@ -169,13 +146,22 @@ $(LOCALBIN):
mkdir -p $(LOCALBIN)

## Tool Binaries
OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
YQ = $(LOCALBIN)/yq
KIND = $(LOCALBIN)/kind

## Tool Versions
# Set the Operator SDK version to use. By default, what is installed on the system is used.
# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
OPERATOR_SDK_VERSION ?= v1.32.0
KUSTOMIZE_VERSION ?= v5.1.1
CONTROLLER_TOOLS_VERSION ?= v0.16.3
OPM_VERSION ?= v1.23.2
YQ_VERSION ?= v4.35.2
KIND_VERSION ?= v0.24.0

KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
.PHONY: kustomize
Expand All @@ -194,7 +180,6 @@ $(ENVTEST): $(LOCALBIN)
test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest

.PHONY: operator-sdk
OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk
operator-sdk: ## Download operator-sdk locally if necessary.
ifeq (,$(wildcard $(OPERATOR_SDK)))
ifeq (, $(shell which operator-sdk 2>/dev/null))
Expand All @@ -210,9 +195,37 @@ OPERATOR_SDK = $(shell which operator-sdk)
endif
endif

###
# END OF kubebuilder SECTION
###
.PHONY: yq
yq: ## Download yq locally if necessary.
ifeq (,$(wildcard $(YQ)))
ifeq (,$(shell which yq 2>/dev/null))
@{ \
set -e ;\
mkdir -p $(dir $(YQ)) ;\
OSTYPE=$(shell uname | awk '{print tolower($$0)}') && ARCH=$(shell go env GOARCH) && \
curl -sSLo $(YQ) https://github.com/mikefarah/yq/releases/download/$(YQ_VERSION)/yq_$${OSTYPE}_$${ARCH} ;\
chmod +x $(YQ) ;\
}
else
YQ = $(shell which yq)
endif
endif

.PHONY: kind
kind: ## Download kind locally if necessary.
ifeq (,$(wildcard $(KIND)))
ifeq (,$(shell which kind 2>/dev/null))
@{ \
set -e ;\
mkdir -p $(dir $(KIND)) ;\
OSTYPE=$(shell uname | awk '{print tolower($$0)}') && ARCH=$(shell go env GOARCH) && \
curl -sSLo $(KIND) https://github.com/kubernetes-sigs/kind/releases/download/$(KIND_VERSION)/kind-$${OSTYPE}-$${ARCH} ;\
chmod +x $(KIND) ;\
}
else
KIND = $(shell which kind)
endif
endif

# CHANNELS define the bundle channels used in the bundle.
# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
Expand Down Expand Up @@ -309,16 +322,14 @@ export KO_DOCKER_REPO ?= ko.local/grafana/grafana-operator
export KIND_CLUSTER_NAME ?= kind-grafana
export KUBECONFIG ?= ${HOME}/.kube/kind-grafana-operator

# If you want to push ko to your local Docker daemon
.PHONY: ko-build-local
ko-build-local: ko
ko-build-local: ko ## Build Docker image with KO
$(KO) build --sbom=none --bare

# If you want to push ko to your kind cluster
.PHONY: ko-build-kind
ko-build-kind: ko
$(KO) build --sbom=none --bare
kind load docker-image $(KO_DOCKER_REPO) --name $(KIND_CLUSTER_NAME)
ko-build-kind: ko-build-local ## Build and Load Docker image into kind cluster
$(KIND) load docker-image $(KO_DOCKER_REPO) --name $(KIND_CLUSTER_NAME)

helm-docs:
ifeq (, $(shell which helm-docs))
@{ \
Expand All @@ -330,9 +341,6 @@ else
HELM_DOCS=$(shell which helm-docs)
endif

start-kind:
@hack/kind/start-kind.sh

.PHONY: helm/docs
helm/docs: helm-docs
$(HELM_DOCS)
Expand Down
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
Loading

0 comments on commit 0100aa2

Please sign in to comment.