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
false + + conditions + []object + +
+ + false contentCache string @@ -1821,6 +1828,103 @@ GrafanaDashboardStatus defines the observed state of GrafanaDashboard + +### GrafanaDashboard.status.conditions[index] +[↩ Parent](#grafanadashboardstatus) + + + +Condition contains details for one aspect of the current state of this API Resource. +--- +This struct is intended for direct use as an array at the field path .status.conditions. For example, + + + type FooStatus struct{ + // Represents the observations of a foo's current state. + // Known .status.conditions.type are: "Available", "Progressing", and "Degraded" + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + + + // other fields + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
lastTransitionTimestring + 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
messagestring + message is a human readable message indicating details about the transition. +This may be an empty string.
+
true
reasonstring + 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
statusenum + status of the condition, one of True, False, Unknown.
+
+ Enum: True, False, Unknown
+
true
typestring + 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
observedGenerationinteger + 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
+ ## GrafanaDatasource [↩ Parent](#grafanaintegreatlyorgv1beta1 )