diff --git a/api/integreatly/v1alpha1/grafana_types.go b/api/integreatly/v1alpha1/grafana_types.go index a7c23760b..0989ffd20 100644 --- a/api/integreatly/v1alpha1/grafana_types.go +++ b/api/integreatly/v1alpha1/grafana_types.go @@ -38,6 +38,10 @@ type GrafanaSpec struct { InitImage string `json:"initImage,omitempty"` LivenessProbeSpec *LivenessProbeSpec `json:"livenessProbeSpec,omitempty"` ReadinessProbeSpec *ReadinessProbeSpec `json:"readinessProbeSpec,omitempty"` + + // DashboardContentCacheDuration sets a default for when a `GrafanaDashboard` resource doesn't specify a `contentCacheDuration`. + // If left unset or 0 the default behavior is to cache indefinitely. + DashboardContentCacheDuration *metav1.Duration `json:"dashboardContentCacheDuration,omitempty"` } type ReadinessProbeSpec struct { diff --git a/api/integreatly/v1alpha1/grafanadashboard_types.go b/api/integreatly/v1alpha1/grafanadashboard_types.go index 03c190a35..c405aa09f 100644 --- a/api/integreatly/v1alpha1/grafanadashboard_types.go +++ b/api/integreatly/v1alpha1/grafanadashboard_types.go @@ -40,7 +40,14 @@ type GrafanaDashboardSpec struct { Datasources []GrafanaDashboardDatasource `json:"datasources,omitempty"` CustomFolderName string `json:"customFolderName,omitempty"` GrafanaCom *GrafanaDashboardGrafanaComSource `json:"grafanaCom,omitempty"` + + // ContentCacheDuration sets how often the operator should resync with the external source when using + // the `grafanaCom.id` or `url` field to specify the source of the dashboard. The default value is + // decided by the `dashboardContentCacheDuration` field in the `Grafana` resource. The default is 0 which + // is interpreted as never refetching. + ContentCacheDuration *metav1.Duration `json:"contentCacheDuration,omitempty"` } + type GrafanaDashboardDatasource struct { InputName string `json:"inputName"` DatasourceName string `json:"datasourceName"` @@ -63,7 +70,16 @@ type GrafanaDashboardRef struct { } type GrafanaDashboardStatus struct { - // Empty + Content string `json:"content,omitempty"` + ContentTimestamp *metav1.Time `json:"contentTimestamp,omitempty"` + ContentUrl string `json:"contentUrl,omitempty"` + Error *GrafanaDashboardError `json:"error,omitempty"` +} + +type GrafanaDashboardError struct { + Code int `json:"code"` + Message string `json:"error"` + Retries int `json:"retries,omitempty"` } // GrafanaDashboard is the Schema for the grafanadashboards API @@ -117,6 +133,10 @@ func (d *GrafanaDashboard) Hash() string { } } + if d.Status.Content != "" { + io.WriteString(hash, d.Status.Content) //nolint + } + return fmt.Sprintf("%x", hash.Sum(nil)) } diff --git a/api/integreatly/v1alpha1/zz_generated.deepcopy.go b/api/integreatly/v1alpha1/zz_generated.deepcopy.go index d07f415f1..f0cbbfe67 100644 --- a/api/integreatly/v1alpha1/zz_generated.deepcopy.go +++ b/api/integreatly/v1alpha1/zz_generated.deepcopy.go @@ -1316,7 +1316,7 @@ func (in *GrafanaDashboard) DeepCopyInto(out *GrafanaDashboard) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDashboard. @@ -1352,6 +1352,21 @@ func (in *GrafanaDashboardDatasource) DeepCopy() *GrafanaDashboardDatasource { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GrafanaDashboardError) DeepCopyInto(out *GrafanaDashboardError) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDashboardError. +func (in *GrafanaDashboardError) DeepCopy() *GrafanaDashboardError { + if in == nil { + return nil + } + out := new(GrafanaDashboardError) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GrafanaDashboardGrafanaComSource) DeepCopyInto(out *GrafanaDashboardGrafanaComSource) { *out = *in @@ -1447,6 +1462,11 @@ func (in *GrafanaDashboardSpec) DeepCopyInto(out *GrafanaDashboardSpec) { *out = new(GrafanaDashboardGrafanaComSource) (*in).DeepCopyInto(*out) } + if in.ContentCacheDuration != nil { + in, out := &in.ContentCacheDuration, &out.ContentCacheDuration + *out = new(metav1.Duration) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDashboardSpec. @@ -1462,6 +1482,15 @@ func (in *GrafanaDashboardSpec) DeepCopy() *GrafanaDashboardSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GrafanaDashboardStatus) DeepCopyInto(out *GrafanaDashboardStatus) { *out = *in + if in.ContentTimestamp != nil { + in, out := &in.ContentTimestamp, &out.ContentTimestamp + *out = (*in).DeepCopy() + } + if in.Error != nil { + in, out := &in.Error, &out.Error + *out = new(GrafanaDashboardError) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDashboardStatus. @@ -2254,6 +2283,11 @@ func (in *GrafanaSpec) DeepCopyInto(out *GrafanaSpec) { *out = new(ReadinessProbeSpec) (*in).DeepCopyInto(*out) } + if in.DashboardContentCacheDuration != nil { + in, out := &in.DashboardContentCacheDuration, &out.DashboardContentCacheDuration + *out = new(metav1.Duration) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaSpec. diff --git a/config/crd/bases/integreatly.org_grafanadashboards.yaml b/config/crd/bases/integreatly.org_grafanadashboards.yaml index a40764f87..55df41356 100644 --- a/config/crd/bases/integreatly.org_grafanadashboards.yaml +++ b/config/crd/bases/integreatly.org_grafanadashboards.yaml @@ -53,6 +53,14 @@ spec: required: - key type: object + contentCacheDuration: + description: ContentCacheDuration sets how often the operator should + resync with the external source when using the `grafanaCom.id` or + `url` field to specify the source of the dashboard. The default + value is decided by the `dashboardContentCacheDuration` field in + the `Grafana` resource. The default is 0 which is interpreted as + never refetching. + type: string customFolderName: type: string datasources: @@ -97,6 +105,26 @@ spec: type: string type: object status: + properties: + content: + type: string + contentTimestamp: + format: date-time + type: string + contentUrl: + type: string + error: + properties: + code: + type: integer + error: + type: string + retries: + type: integer + required: + - code + - error + type: object type: object type: object served: true diff --git a/config/crd/bases/integreatly.org_grafanas.yaml b/config/crd/bases/integreatly.org_grafanas.yaml index 3d2891e35..e527bb0cc 100644 --- a/config/crd/bases/integreatly.org_grafanas.yaml +++ b/config/crd/bases/integreatly.org_grafanas.yaml @@ -1839,6 +1839,11 @@ spec: - name type: object type: array + dashboardContentCacheDuration: + description: DashboardContentCacheDuration sets a default for when + a `GrafanaDashboard` resource doesn't specify a `contentCacheDuration`. + If left unset or 0 the default behavior is to cache indefinitely. + type: string dashboardLabelSelector: items: description: A label selector is a label query over a set of resources. diff --git a/controllers/common/controllerState.go b/controllers/common/controllerState.go index dbbc52977..7d698a3f8 100644 --- a/controllers/common/controllerState.go +++ b/controllers/common/controllerState.go @@ -5,9 +5,10 @@ import v1 "k8s.io/apimachinery/pkg/apis/meta/v1" var ControllerEvents = make(chan ControllerState, 1) type ControllerState struct { - DashboardSelectors []*v1.LabelSelector - DashboardNamespaceSelector *v1.LabelSelector - AdminUrl string - GrafanaReady bool - ClientTimeout int + DashboardSelectors []*v1.LabelSelector + DashboardNamespaceSelector *v1.LabelSelector + DashboardContentCacheDuration *v1.Duration + AdminUrl string + GrafanaReady bool + ClientTimeout int } diff --git a/controllers/grafana/grafana_controller.go b/controllers/grafana/grafana_controller.go index 8066b8299..0ace74817 100644 --- a/controllers/grafana/grafana_controller.go +++ b/controllers/grafana/grafana_controller.go @@ -16,6 +16,7 @@ import ( v1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/tools/record" @@ -299,6 +300,10 @@ func (r *ReconcileGrafana) manageSuccess(cr *grafanav1alpha1.Grafana, state *com cr.Status.InstalledDashboards = r.Config.GetDashboards("") } + if cr.Spec.DashboardContentCacheDuration == nil { + cr.Spec.DashboardContentCacheDuration = &metav1.Duration{Duration: 0} + } + instance := &grafanav1alpha1.Grafana{} err := r.Client.Get(r.Context, request.NamespacedName, instance) if err != nil { @@ -319,11 +324,12 @@ func (r *ReconcileGrafana) manageSuccess(cr *grafanav1alpha1.Grafana, state *com // Publish controller state controllerState := common.ControllerState{ - DashboardSelectors: cr.Spec.DashboardLabelSelector, - DashboardNamespaceSelector: cr.Spec.DashboardNamespaceSelector, - AdminUrl: url, - GrafanaReady: true, - ClientTimeout: DefaultClientTimeoutSeconds, + DashboardSelectors: cr.Spec.DashboardLabelSelector, + DashboardNamespaceSelector: cr.Spec.DashboardNamespaceSelector, + DashboardContentCacheDuration: cr.Spec.DashboardContentCacheDuration, + AdminUrl: url, + GrafanaReady: true, + ClientTimeout: DefaultClientTimeoutSeconds, } if cr.Spec.Client != nil && cr.Spec.Client.TimeoutSeconds != nil { diff --git a/controllers/grafanadashboard/dashboard_pipeline.go b/controllers/grafanadashboard/dashboard_pipeline.go index b484feb5e..98eeab2d0 100644 --- a/controllers/grafanadashboard/dashboard_pipeline.go +++ b/controllers/grafanadashboard/dashboard_pipeline.go @@ -13,12 +13,15 @@ import ( "path" "strconv" "strings" + "time" "github.com/go-logr/logr" "github.com/google/go-jsonnet" "github.com/grafana-operator/grafana-operator/v4/api/integreatly/v1alpha1" "github.com/grafana-operator/grafana-operator/v4/controllers/config" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -51,6 +54,9 @@ type DashboardPipelineImpl struct { } func NewDashboardPipeline(client client.Client, dashboard *v1alpha1.GrafanaDashboard, ctx context.Context) DashboardPipeline { + if dashboard.Spec.ContentCacheDuration == nil { + dashboard.Spec.ContentCacheDuration = &metav1.Duration{Duration: 24 * time.Hour} + } return &DashboardPipelineImpl{ Client: client, Dashboard: dashboard, @@ -112,6 +118,13 @@ func (r *DashboardPipelineImpl) validateJson() error { return err } +func (r *DashboardPipelineImpl) shouldUseContentCache() bool { + cacheDuration := r.Dashboard.Spec.ContentCacheDuration.Duration + contentTimeStamp := r.Dashboard.Status.ContentTimestamp + + return cacheDuration > 0 && contentTimeStamp.Add(cacheDuration).After(time.Now()) +} + // Try to get the dashboard json definition either from a provided URL or from the // raw json in the dashboard resource. The priority is as follows: // 1) try to fetch from url or grafanaCom if provided @@ -124,6 +137,11 @@ func (r *DashboardPipelineImpl) obtainJson() error { return errors.New("both dashboard url and grafana.com source specified") } + if r.Dashboard.Status.Content != "" && r.shouldUseContentCache() { + r.JSON = r.Dashboard.Status.Content + return nil + } + if r.Dashboard.Spec.GrafanaCom != nil { if err := r.loadDashboardFromGrafanaCom(); err != nil { r.Logger.Error(err, "failed to request dashboard from grafana.com, falling back to config map; if specified") @@ -196,14 +214,31 @@ func (r *DashboardPipelineImpl) loadDashboardFromURL() error { } defer resp.Body.Close() - if resp.StatusCode != 200 { - return fmt.Errorf("request failed with status %v", resp.StatusCode) - } - body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } + if resp.StatusCode != 200 { + retries := 0 + r.refreshDashboard() + if r.Dashboard.Status.Error != nil { + retries = r.Dashboard.Status.Error.Retries + } + r.Dashboard.Status = v1alpha1.GrafanaDashboardStatus{ + Error: &v1alpha1.GrafanaDashboardError{ + Message: string(body), + Code: resp.StatusCode, + Retries: retries + 1, + }, + ContentTimestamp: &metav1.Time{Time: time.Now()}, + } + + if err := r.Client.Status().Update(r.Context, r.Dashboard); err != nil { + return fmt.Errorf("failed to request dashboard: %s\nfailed to update status : %w", string(body), err) + } + + return fmt.Errorf("request failed with status %v", resp.StatusCode) + } sourceType := r.getFileType(url.Path) switch sourceType { @@ -218,18 +253,27 @@ func (r *DashboardPipelineImpl) loadDashboardFromURL() error { r.JSON = json } - // Update dashboard spec so that URL would not be refetched - if r.JSON != r.Dashboard.Spec.Json { - r.Dashboard.Spec.Json = r.JSON - err := r.Client.Update(r.Context, r.Dashboard) - if err != nil { - return err - } + r.refreshDashboard() + r.Dashboard.Status = v1alpha1.GrafanaDashboardStatus{ + Content: r.JSON, + ContentTimestamp: &metav1.Time{Time: time.Now()}, + ContentUrl: r.Dashboard.Spec.Url, + } + + if err := r.Client.Status().Update(r.Context, r.Dashboard); err != nil { + return fmt.Errorf("failed to update status with content for dashboard %s/%s: %w", r.Dashboard.Namespace, r.Dashboard.Name, err) } return nil } +func (r *DashboardPipelineImpl) refreshDashboard() { + err := r.Client.Get(r.Context, types.NamespacedName{Name: r.Dashboard.Name, Namespace: r.Dashboard.Namespace}, r.Dashboard) + if err != nil { + r.Logger.V(1).Error(err, "refreshing dashboard generation failed") + } +} + func (r *DashboardPipelineImpl) loadDashboardFromGrafanaCom() error { url, err := r.getGrafanaComDashboardUrl() if err != nil { @@ -246,14 +290,40 @@ func (r *DashboardPipelineImpl) loadDashboardFromGrafanaCom() error { if err != nil { return err } - r.JSON = string(body) - // Update JSON so dashboard is not refetched - if r.JSON != r.Dashboard.Spec.Json { - r.Dashboard.Spec.Json = r.JSON - if err := r.Client.Update(r.Context, r.Dashboard); err != nil { - return err + if resp.StatusCode != 200 { + r.refreshDashboard() + retries := 0 + if r.Dashboard.Status.Error != nil { + retries = r.Dashboard.Status.Error.Retries + } + r.Dashboard.Status = v1alpha1.GrafanaDashboardStatus{ + Error: &v1alpha1.GrafanaDashboardError{ + Message: string(body), + Code: resp.StatusCode, + Retries: retries + 1, + }, + ContentTimestamp: &metav1.Time{Time: time.Now()}, + } + + if err := r.Client.Status().Update(r.Context, r.Dashboard); err != nil { + return fmt.Errorf("failed to request dashboard and failed to update status %s: %w", string(body), err) } + + return fmt.Errorf("failed to request dashboard: %s", string(body)) + } + + r.JSON = string(body) + + r.refreshDashboard() + r.Dashboard.Status = v1alpha1.GrafanaDashboardStatus{ + Content: r.JSON, + ContentTimestamp: &metav1.Time{Time: time.Now()}, + ContentUrl: url, + } + + if err := r.Client.Status().Update(r.Context, r.Dashboard); err != nil { + return fmt.Errorf("failed to update status with content for dashboard %s/%s: %w", r.Dashboard.Namespace, r.Dashboard.Name, err) } return nil @@ -316,12 +386,13 @@ func (r *DashboardPipelineImpl) getLatestRevisionForGrafanaComDashboard() (int, // direction. func (r *DashboardPipelineImpl) getMaximumRevisionFromListDashboardRevisionsResponse(resp *listDashboardRevisionsResponse) int { if resp.OrderBy == "revision" { - if resp.Direction == "asc" { - return resp.Items[len(resp.Items)-1].Revision - } - - if resp.Direction == "desc" { - return resp.Items[0].Revision + // resp.Direction seems to be inverted in the response (as of 2022-05-09), so let's ignore it and grab the bigger value + first := resp.Items[0].Revision + last := resp.Items[len(resp.Items)-1].Revision + if first > last { + return first + } else { + return last } } diff --git a/controllers/grafanadashboard/grafanadashboard_controller.go b/controllers/grafanadashboard/grafanadashboard_controller.go index 0c689e17d..b45bcf76f 100644 --- a/controllers/grafanadashboard/grafanadashboard_controller.go +++ b/controllers/grafanadashboard/grafanadashboard_controller.go @@ -21,6 +21,7 @@ import ( "crypto/tls" "errors" "fmt" + "math" "net/http" "os" @@ -263,9 +264,10 @@ func (r *GrafanaDashboardReconciler) reconcileDashboards(request reconcile.Reque } // Process new/updated dashboards - for i, dashboard := range namespaceDashboards.Items { + for i := range namespaceDashboards.Items { + dashboard := namespaceDashboards.Items[i] // Is this a dashboard we care about (matches the label selectors)? - if !r.isMatch(&namespaceDashboards.Items[i]) { + if !r.isMatch(&dashboard) { log.Log.Info("dashboard found but selectors do not match", "namespace", dashboard.Namespace, "name", dashboard.Name) continue @@ -276,11 +278,21 @@ func (r *GrafanaDashboardReconciler) reconcileDashboards(request reconcile.Reque folderName = dashboard.Spec.CustomFolderName } + if dashboard.Status.Error != nil { + backoffDuration := 30 * time.Second * time.Duration(math.Pow(2, float64(dashboard.Status.Error.Retries))) + retryTime := dashboard.Status.ContentTimestamp.Add(backoffDuration) + + if retryTime.After(time.Now()) { + log.Log.Info("delaying retry of failing dashboard", "folder", folderName, "dashboard", request.Name, "namespace", request.Namespace, "retryTime", retryTime) + continue + } + } + folder, err := grafanaClient.CreateOrUpdateFolder(folderName) if err != nil { log.Log.Error(err, "failed to get or create namespace folder for dashboard", "folder", folderName, "dashboard", request.Name) - r.manageError(&namespaceDashboards.Items[i], err) + r.manageError(&dashboard, err) continue } @@ -291,11 +303,15 @@ func (r *GrafanaDashboardReconciler) reconcileDashboards(request reconcile.Reque folderId = *folder.ID } + if dashboard.Spec.ContentCacheDuration == nil { + dashboard.Spec.ContentCacheDuration = r.state.DashboardContentCacheDuration + } + // Process the dashboard. Use the known hash of an existing dashboard // to determine if an update is required - knownHash := findHash(knownDashboards, &namespaceDashboards.Items[i]) - knownUid := findUid(knownDashboards, &namespaceDashboards.Items[i]) - pipeline := NewDashboardPipeline(r.Client, &namespaceDashboards.Items[i], r.context) + knownHash := findHash(knownDashboards, &dashboard) + knownUid := findUid(knownDashboards, &dashboard) + pipeline := NewDashboardPipeline(r.Client, &dashboard, r.context) processed, err := pipeline.ProcessDashboard(knownHash, &folderId, folderName, false) // Check known dashboards exist on grafana instance and recreate if not @@ -306,32 +322,32 @@ func (r *GrafanaDashboardReconciler) reconcileDashboards(request reconcile.Reque } if *response.Dashboard.ID == uint(0) { - log.Log.Info(fmt.Sprintf("Dashboard %v has been deleted via grafana console. Recreating.", namespaceDashboards.Items[i].ObjectMeta.Name)) + log.Log.Info(fmt.Sprintf("Dashboard %v has been deleted via grafana console. Recreating.", dashboard.ObjectMeta.Name)) processed, err = pipeline.ProcessDashboard(knownHash, &folderId, folderName, true) if err != nil { log.Log.Error(err, "cannot process dashboard", "namespace", dashboard.Namespace, "name", dashboard.Name) - r.manageError(&namespaceDashboards.Items[i], err) + r.manageError(&dashboard, err) continue } } } if err != nil { - log.Log.Error(err, "cannot process dashboard", "namespace", dashboard.Namespace, "name", dashboard.Name) - r.manageError(&namespaceDashboards.Items[i], err) + // log.Log.Error(err, "cannot process dashboard", "namespace", dashboard.Namespace, "name", dashboard.Name) + r.manageError(&dashboard, err) continue } if processed == nil { - r.config.SetPluginsFor(&namespaceDashboards.Items[i]) + r.config.SetPluginsFor(&dashboard) continue } // Check labels only when DashboardNamespaceSelector isnt empty if r.state.DashboardNamespaceSelector != nil { - matchesNamespaceLabels, err := r.checkNamespaceLabels(&namespaceDashboards.Items[i]) + matchesNamespaceLabels, err := r.checkNamespaceLabels(&dashboard) if err != nil { - r.manageError(&namespaceDashboards.Items[i], err) + r.manageError(&dashboard, err) continue } @@ -344,12 +360,12 @@ func (r *GrafanaDashboardReconciler) reconcileDashboards(request reconcile.Reque _, err = grafanaClient.CreateOrUpdateDashboard(processed, folderId, folderName) if err != nil { //log.Log.Error(err, "cannot submit dashboard %v/%v", "namespace", dashboard.Namespace, "name", dashboard.Name) - r.manageError(&namespaceDashboards.Items[i], err) + r.manageError(&dashboard, err) continue } - r.manageSuccess(&namespaceDashboards.Items[i], &folderId, folderName) + r.manageSuccess(&dashboard, &folderId, folderName) } for _, dashboard := range dashboardsToDelete { @@ -464,7 +480,7 @@ func (r *GrafanaDashboardReconciler) manageError(dashboard *grafanav1alpha1.Graf if k8serrors.IsConflict(issue) || k8serrors.IsServiceUnavailable(issue) { return } - log.Log.Error(issue, "error updating dashboard") + log.Log.Error(issue, "error updating dashboard", "name", dashboard.Name, "namespace", dashboard.Namespace) } func (r *GrafanaDashboardReconciler) SetupWithManager(mgr manager.Manager) error { diff --git a/deploy/examples/dashboards/DashboardFromURLWithCacheDuration.yaml b/deploy/examples/dashboards/DashboardFromURLWithCacheDuration.yaml new file mode 100644 index 000000000..4d45560e9 --- /dev/null +++ b/deploy/examples/dashboards/DashboardFromURLWithCacheDuration.yaml @@ -0,0 +1,10 @@ +apiVersion: integreatly.org/v1alpha1 +kind: GrafanaDashboard +metadata: + name: grafana-dashboard-from-url + labels: + app: grafana +spec: + url: https://raw.githubusercontent.com/grafana-operator/grafana-operator/master/deploy/examples/remote/grafana-dashboard.json + # Only cache this for 15 minutes before checking if the upstream source has changed. + contentCacheDuration: 15m diff --git a/deploy/manifests/latest/crds.yaml b/deploy/manifests/latest/crds.yaml index 18774160a..9e610c078 100644 --- a/deploy/manifests/latest/crds.yaml +++ b/deploy/manifests/latest/crds.yaml @@ -51,6 +51,14 @@ spec: required: - key type: object + contentCacheDuration: + description: ContentCacheDuration sets how often the operator should + resync with the external source when using the `grafanaCom.id` or + `url` field to specify the source of the dashboard. The default + value is decided by the `dashboardContentCacheDuration` field in + the `Grafana` resource. The default is 0 which is interpreted as + never refetching. + type: string customFolderName: type: string datasources: @@ -95,6 +103,26 @@ spec: type: string type: object status: + properties: + content: + type: string + contentTimestamp: + format: date-time + type: string + contentUrl: + type: string + error: + properties: + code: + type: integer + error: + type: string + retries: + type: integer + required: + - code + - error + type: object type: object type: object served: true @@ -2436,6 +2464,11 @@ spec: - name type: object type: array + dashboardContentCacheDuration: + description: DashboardContentCacheDuration sets a default for when + a `GrafanaDashboard` resource doesn't specify a `contentCacheDuration`. + If left unset or 0 the default behavior is to cache indefinitely. + type: string dashboardLabelSelector: items: description: A label selector is a label query over a set of resources. diff --git a/documentation/api.md b/documentation/api.md index 6f83cc37d..20482f834 100644 --- a/documentation/api.md +++ b/documentation/api.md @@ -63,7 +63,7 @@ GrafanaDashboard is the Schema for the grafanadashboards API false - status + status object
@@ -96,6 +96,13 @@ GrafanaDashboardSpec defines the desired state of GrafanaDashboard Selects a key from a ConfigMap.
false + + contentCacheDuration + string + + ContentCacheDuration sets how often the operator should resync with the external source when using the `grafanaCom.id` or `url` field to specify the source of the dashboard. The default value is decided by the `dashboardContentCacheDuration` field in the `Grafana` resource. The default is 0 which is interpreted as never refetching.
+ + false customFolderName string @@ -291,6 +298,97 @@ GrafanaPlugin contains information about a single plugin + +### GrafanaDashboard.status +[↩ Parent](#grafanadashboard) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
contentstring +
+
false
contentTimestampstring +
+
+ Format: date-time
+
false
contentUrlstring +
+
false
errorobject +
+
false
+ + +### GrafanaDashboard.status.error +[↩ Parent](#grafanadashboardstatus) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
codeinteger +
+
true
errorstring +
+
true
retriesinteger +
+
false
+ ## GrafanaDataSource [↩ Parent](#integreatlyorgv1alpha1 ) @@ -1868,6 +1966,13 @@ Grafana is the Schema for the grafanas API
false + + dashboardContentCacheDuration + string + + DashboardContentCacheDuration sets a default for when a `GrafanaDashboard` resource doesn't specify a `contentCacheDuration`. If left unset or 0 the default behavior is to cache indefinitely.
+ + false dashboardLabelSelector []object