diff --git a/api/v1beta1/grafanadashboard_types.go b/api/v1beta1/grafanadashboard_types.go
index 74392407a..f63c09c48 100644
--- a/api/v1beta1/grafanadashboard_types.go
+++ b/api/v1beta1/grafanadashboard_types.go
@@ -168,6 +168,8 @@ type GrafanaDashboardStatus struct {
// Last time the dashboard was resynced
LastResync metav1.Time `json:"lastResync,omitempty"`
UID string `json:"uid,omitempty"`
+
+ Conditions []metav1.Condition `json:"conditions,omitempty"`
}
//+kubebuilder:object:root=true
@@ -194,6 +196,31 @@ type GrafanaDashboardList struct {
Items []GrafanaDashboard `json:"items"`
}
+// FolderRef implements FolderReferencer.
+func (in *GrafanaDashboard) FolderRef() string {
+ return in.Spec.FolderRef
+}
+
+// FolderUID implements FolderReferencer.
+func (in *GrafanaDashboard) FolderUID() string {
+ return in.Spec.FolderUID
+}
+
+// FolderNamespace implements FolderReferencer.
+func (in *GrafanaDashboard) FolderNamespace() string {
+ return in.Namespace
+}
+
+// Conditions implements FolderReferencer.
+func (in *GrafanaDashboard) Conditions() *[]metav1.Condition {
+ return &in.Status.Conditions
+}
+
+// CurrentGeneration implements FolderReferencer.
+func (in *GrafanaDashboard) CurrentGeneration() int64 {
+ return in.Generation
+}
+
func (in *GrafanaDashboard) Unchanged(hash string) bool {
return in.Status.Hash == hash
}
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 5c596b500..001f0f660 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -877,6 +877,13 @@ func (in *GrafanaDashboardStatus) DeepCopyInto(out *GrafanaDashboardStatus) {
}
in.ContentTimestamp.DeepCopyInto(&out.ContentTimestamp)
in.LastResync.DeepCopyInto(&out.LastResync)
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]metav1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDashboardStatus.
diff --git a/config/crd/bases/grafana.integreatly.org_grafanadashboards.yaml b/config/crd/bases/grafana.integreatly.org_grafanadashboards.yaml
index eebd2ae17..0e59d25d8 100644
--- a/config/crd/bases/grafana.integreatly.org_grafanadashboards.yaml
+++ b/config/crd/bases/grafana.integreatly.org_grafanadashboards.yaml
@@ -349,6 +349,75 @@ spec:
description: The dashboard instanceSelector can't find matching grafana
instances
type: boolean
+ conditions:
+ items:
+ description: "Condition contains details for one aspect of the current
+ state of this API Resource.\n---\nThis struct is intended for
+ direct use as an array at the field path .status.conditions. For
+ example,\n\n\n\ttype FooStatus struct{\n\t // Represents the
+ observations of a foo's current state.\n\t // Known .status.conditions.type
+ are: \"Available\", \"Progressing\", and \"Degraded\"\n\t //
+ +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t
+ \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\"
+ patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
+ \ // other fields\n\t}"
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: |-
+ type of condition in CamelCase or in foo.example.com/CamelCase.
+ ---
+ Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
+ useful (see .node.status.conditions), the ability to deconflict is important.
+ The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
contentCache:
format: byte
type: string
diff --git a/controllers/dashboard_controller.go b/controllers/dashboard_controller.go
index 33bc7d2e8..ab3d2776d 100644
--- a/controllers/dashboard_controller.go
+++ b/controllers/dashboard_controller.go
@@ -45,6 +45,7 @@ import (
"github.com/grafana/grafana-operator/v5/controllers/fetchers"
"github.com/grafana/grafana-operator/v5/controllers/metrics"
kuberr "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/discovery"
@@ -56,8 +57,10 @@ import (
)
const (
- initialSyncDelay = "10s"
- syncBatchSize = 100
+ initialSyncDelay = "10s"
+ syncBatchSize = 100
+ conditionDashboardSynchronized = "DashboardSynchronized"
+ conditionErrorFetchingInstance = "ErrFetchingInstances"
)
// GrafanaDashboardReconciler reconciles a GrafanaDashboard object
@@ -192,10 +195,13 @@ func (r *GrafanaDashboardReconciler) Reconcile(ctx context.Context, req ctrl.Req
instances, err := r.GetMatchingDashboardInstances(ctx, cr, r.Client)
if err != nil {
+ setNoMatchingInstance(&cr.Status.Conditions, cr.Generation, conditionErrorFetchingInstance, fmt.Sprintf("error occurred during fetching of instances: %s", err.Error()))
+ meta.RemoveStatusCondition(&cr.Status.Conditions, conditionDashboardSynchronized)
controllerLog.Error(err, "could not find matching instances", "name", cr.Name, "namespace", cr.Namespace)
return ctrl.Result{RequeueAfter: RequeueDelay}, err
}
+ removeNoMatchingInstance(&cr.Status.Conditions)
controllerLog.Info("found matching Grafana instances for dashboard", "count", len(instances.Items))
dashboardJson, err := r.fetchDashboardJson(ctx, cr)
@@ -232,6 +238,7 @@ func (r *GrafanaDashboardReconciler) Reconcile(ctx context.Context, req ctrl.Req
}
success := true
+ applyErrors := make(map[string]string)
for _, grafana := range instances.Items {
// check if this is a cross namespace import
if grafana.Namespace != cr.Namespace && !cr.IsAllowCrossNamespaceImport() {
@@ -262,9 +269,13 @@ func (r *GrafanaDashboardReconciler) Reconcile(ctx context.Context, req ctrl.Req
err = r.onDashboardCreated(ctx, &grafana, cr, dashboardModel, hash)
if err != nil {
controllerLog.Error(err, "error reconciling dashboard", "dashboard", cr.Name, "grafana", grafana.Name)
+ applyErrors[fmt.Sprintf("%s/%s", grafana.Namespace, grafana.Name)] = err.Error()
success = false
}
+ condition := buildSynchronizedCondition("Dashboard", conditionDashboardSynchronized, cr.Generation, applyErrors, len(instances.Items))
+ meta.SetStatusCondition(&cr.Status.Conditions, condition)
+
if grafana.Spec.Preferences != nil && uid == grafana.Spec.Preferences.HomeDashboardUID {
err = r.UpdateHomeDashboard(ctx, grafana, uid, cr)
if err != nil {
@@ -370,11 +381,18 @@ func (r *GrafanaDashboardReconciler) onDashboardCreated(ctx context.Context, gra
return err
}
- folderUID, err := r.retrieveFolderUID(ctx, grafanaClient, cr)
+ folderUID, err := getFolderUID(ctx, r.Client, cr)
if err != nil {
return err
}
+ if folderUID == "" {
+ folderUID, err = r.GetOrCreateFolder(grafanaClient, cr)
+ if err != nil {
+ return err
+ }
+ }
+
uid := fmt.Sprintf("%s", dashboardModel["uid"])
title := fmt.Sprintf("%s", dashboardModel["title"])
@@ -429,38 +447,6 @@ func (r *GrafanaDashboardReconciler) onDashboardCreated(ctx context.Context, gra
return r.Client.Status().Update(ctx, grafana)
}
-func (r *GrafanaDashboardReconciler) retrieveFolderUID(ctx context.Context, grafanaClient *genapi.GrafanaHTTPAPI, cr *v1beta1.GrafanaDashboard) (string, error) {
- if cr.Spec.FolderRef != "" && cr.Spec.FolderUID != "" {
- return "", fmt.Errorf("error folderRef and folderUID cannot be declared at the same time in the CR %s (%s)", cr.Name, cr.Namespace)
- }
-
- if cr.Spec.FolderRef != "" {
- if cr.Spec.FolderTitle != "" {
- r.Log.Info(fmt.Sprintf("warning folder and folderRef cannot be set at the same time. Ignoring folder field in %s (%s)", cr.Name, cr.Namespace))
- }
-
- folder := &v1beta1.GrafanaFolder{}
-
- err := r.Client.Get(ctx, client.ObjectKey{
- Namespace: cr.Namespace,
- Name: cr.Spec.FolderRef,
- }, folder)
- if err != nil {
- return "", err
- }
-
- return string(folder.ObjectMeta.UID), nil
- }
- if cr.Spec.FolderUID != "" {
- if cr.Spec.FolderTitle != "" {
- r.Log.Info(fmt.Sprintf("warning folder and folderUID cannot be set at the same time. Ignoring folder field in %s (%s)", cr.Name, cr.Namespace))
- }
- return cr.Spec.FolderUID, nil
- }
-
- return r.GetOrCreateFolder(grafanaClient, cr)
-}
-
// map data sources that are required in the dashboard to data sources that exist in the instance
func (r *GrafanaDashboardReconciler) resolveDatasources(dashboard *v1beta1.GrafanaDashboard, dashboardJson []byte) ([]byte, error) {
if len(dashboard.Spec.Datasources) == 0 {
diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanadashboards.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanadashboards.yaml
index eebd2ae17..0e59d25d8 100644
--- a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanadashboards.yaml
+++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanadashboards.yaml
@@ -349,6 +349,75 @@ spec:
description: The dashboard instanceSelector can't find matching grafana
instances
type: boolean
+ conditions:
+ items:
+ description: "Condition contains details for one aspect of the current
+ state of this API Resource.\n---\nThis struct is intended for
+ direct use as an array at the field path .status.conditions. For
+ example,\n\n\n\ttype FooStatus struct{\n\t // Represents the
+ observations of a foo's current state.\n\t // Known .status.conditions.type
+ are: \"Available\", \"Progressing\", and \"Degraded\"\n\t //
+ +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t
+ \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\"
+ patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
+ \ // other fields\n\t}"
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: |-
+ type of condition in CamelCase or in foo.example.com/CamelCase.
+ ---
+ Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
+ useful (see .node.status.conditions), the ability to deconflict is important.
+ The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
contentCache:
format: byte
type: string
diff --git a/deploy/kustomize/base/crds.yaml b/deploy/kustomize/base/crds.yaml
index ddb2df929..149335395 100644
--- a/deploy/kustomize/base/crds.yaml
+++ b/deploy/kustomize/base/crds.yaml
@@ -874,6 +874,75 @@ spec:
description: The dashboard instanceSelector can't find matching grafana
instances
type: boolean
+ conditions:
+ items:
+ description: "Condition contains details for one aspect of the current
+ state of this API Resource.\n---\nThis struct is intended for
+ direct use as an array at the field path .status.conditions. For
+ example,\n\n\n\ttype FooStatus struct{\n\t // Represents the
+ observations of a foo's current state.\n\t // Known .status.conditions.type
+ are: \"Available\", \"Progressing\", and \"Degraded\"\n\t //
+ +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t
+ \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\"
+ patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
+ \ // other fields\n\t}"
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: |-
+ type of condition in CamelCase or in foo.example.com/CamelCase.
+ ---
+ Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
+ useful (see .node.status.conditions), the ability to deconflict is important.
+ The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
contentCache:
format: byte
type: string
diff --git a/docs/docs/api.md b/docs/docs/api.md
index 59677615c..f72b8f536 100644
--- a/docs/docs/api.md
+++ b/docs/docs/api.md
@@ -1770,6 +1770,13 @@ GrafanaDashboardStatus defines the observed state of GrafanaDashboard
The dashboard instanceSelector can't find matching grafana instances
Name | +Type | +Description | +Required | +
---|---|---|---|
lastTransitionTime | +string | +
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + + Format: date-time + |
+ true | +
message | +string | +
+ message is a human readable message indicating details about the transition.
+This may be an empty string. + |
+ true | +
reason | +string | +
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+Producers of specific condition types may define expected values and meanings for this field,
+and whether the values are considered a guaranteed API.
+The value should be a CamelCase string.
+This field may not be empty. + |
+ true | +
status | +enum | +
+ status of the condition, one of True, False, Unknown. + + Enum: True, False, Unknown + |
+ true | +
type | +string | +
+ type of condition in CamelCase or in foo.example.com/CamelCase.
+---
+Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
+useful (see .node.status.conditions), the ability to deconflict is important.
+The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + |
+ true | +
observedGeneration | +integer | +
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+with respect to the current state of the instance. + + Format: int64 + Minimum: 0 + |
+ false | +