Skip to content

Commit

Permalink
Gzip content cache and bugfix cache time ecalculation
Browse files Browse the repository at this point in the history
  • Loading branch information
addreas committed Jul 19, 2022
1 parent 9c54a39 commit 749d5af
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 24 deletions.
17 changes: 14 additions & 3 deletions api/integreatly/v1alpha1/grafanadashboard_types.go
Expand Up @@ -81,7 +81,7 @@ type GrafanaDashboardRef struct {
}

type GrafanaDashboardStatus struct {
Content string `json:"content,omitempty"`
ContentGzip []byte `json:"contentGzip,omitempty"`
ContentTimestamp *metav1.Time `json:"contentTimestamp,omitempty"`
ContentUrl string `json:"contentUrl,omitempty"`
Error *GrafanaDashboardError `json:"error,omitempty"`
Expand Down Expand Up @@ -149,8 +149,8 @@ func (d *GrafanaDashboard) Hash() string {
}
}

if d.Status.Content != "" {
io.WriteString(hash, d.Status.Content) //nolint
if d.Status.ContentGzip != nil {
hash.Write(d.Status.ContentGzip)
}

return fmt.Sprintf("%x", hash.Sum(nil))
Expand Down Expand Up @@ -198,3 +198,14 @@ func Gunzip(compressed []byte) ([]byte, error) {
}
return ioutil.ReadAll(decoder)
}

func Gzip(content string) ([]byte, error) {
buf := bytes.NewBuffer([]byte{})
writer := gzip.NewWriter(buf)
_, err := writer.Write([]byte(content))
if err != nil {
return nil, err
}
writer.Close()
return ioutil.ReadAll(buf)
}
5 changes: 5 additions & 0 deletions api/integreatly/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion config/crd/bases/integreatly.org_grafanadashboards.yaml
Expand Up @@ -131,7 +131,8 @@ spec:
type: object
status:
properties:
content:
contentGzip:
format: byte
type: string
contentTimestamp:
format: date-time
Expand Down
75 changes: 58 additions & 17 deletions controllers/grafanadashboard/dashboard_pipeline.go
Expand Up @@ -20,6 +20,7 @@ import (
"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"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -119,10 +120,13 @@ func (r *DashboardPipelineImpl) validateJson() error {
}

func (r *DashboardPipelineImpl) shouldUseContentCache() bool {
cacheDuration := r.Dashboard.Spec.ContentCacheDuration.Duration
contentTimeStamp := r.Dashboard.Status.ContentTimestamp
if r.Dashboard.Status.ContentGzip != nil && r.Dashboard.Spec.ContentCacheDuration != nil && r.Dashboard.Status.ContentTimestamp != nil {
cacheDuration := r.Dashboard.Spec.ContentCacheDuration.Duration
contentTimeStamp := r.Dashboard.Status.ContentTimestamp

return cacheDuration > 0 && contentTimeStamp.Add(cacheDuration).After(time.Now())
return cacheDuration <= 0 || contentTimeStamp.Add(cacheDuration).After(time.Now())
}
return false
}

// Try to get the dashboard json definition either from a provided URL or from the
Expand All @@ -137,14 +141,21 @@ 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
var returnErr error

if r.shouldUseContentCache() {
jsonBytes, err := v1alpha1.Gunzip(r.Dashboard.Status.ContentGzip)
if err != nil {
returnErr = fmt.Errorf("failed to decode/decompress gzipped json: %w", err)
} else {
r.JSON = string(jsonBytes)
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")
returnErr = fmt.Errorf("failed to request dashboard from grafana.com, falling back to config map; if specified: %w", err)
} else {
return nil
}
Expand All @@ -153,7 +164,7 @@ func (r *DashboardPipelineImpl) obtainJson() error {
if r.Dashboard.Spec.Url != "" {
err := r.loadDashboardFromURL()
if err != nil {
r.Logger.Error(err, "failed to request dashboard url, falling back to config map; if specified")
returnErr = fmt.Errorf("failed to request dashboard url, falling back to config map; if specified: %w", err)
} else {
return nil
}
Expand All @@ -162,7 +173,7 @@ func (r *DashboardPipelineImpl) obtainJson() error {
if r.Dashboard.Spec.ConfigMapRef != nil {
err := r.loadDashboardFromConfigMap(r.Dashboard.Spec.ConfigMapRef, false)
if err != nil {
r.Logger.Error(err, "failed to get config map, falling back to raw json")
returnErr = fmt.Errorf("failed to get config map, falling back to raw json: %w", err)
} else {
return nil
}
Expand All @@ -171,7 +182,7 @@ func (r *DashboardPipelineImpl) obtainJson() error {
if r.Dashboard.Spec.GzipConfigMapRef != nil {
err := r.loadDashboardFromConfigMap(r.Dashboard.Spec.GzipConfigMapRef, true)
if err != nil {
r.Logger.Error(err, "failed to get config map, falling back to raw json")
returnErr = fmt.Errorf("failed to get config map, falling back to raw json: %w", err)
} else {
return nil
}
Expand All @@ -180,7 +191,7 @@ func (r *DashboardPipelineImpl) obtainJson() error {
if r.Dashboard.Spec.GzipJson != nil {
jsonBytes, err := v1alpha1.Gunzip(r.Dashboard.Spec.GzipJson)
if err != nil {
r.Logger.Error(err, "failed to decode/decompress gzipped json")
returnErr = fmt.Errorf("failed to decode/decompress gzipped json: %w", err)
} else {
r.JSON = string(jsonBytes)
return nil
Expand All @@ -195,14 +206,14 @@ func (r *DashboardPipelineImpl) obtainJson() error {
if r.Dashboard.Spec.Jsonnet != "" {
json, err := r.loadJsonnet(r.Dashboard.Spec.Jsonnet)
if err != nil {
r.Logger.Error(err, "failed to parse jsonnet")
returnErr = fmt.Errorf("failed to parse jsonnet: %w", err)
} else {
r.JSON = json
return nil
}
}

return errors.New("unable to obtain dashboard contents")
return returnErr
}

// Compiles jsonnet to json and makes the grafonnet library available to
Expand Down Expand Up @@ -272,15 +283,30 @@ func (r *DashboardPipelineImpl) loadDashboardFromURL() error {
r.JSON = json
}

content, err := v1alpha1.Gzip(r.JSON)
if err != nil {
return err
}

r.refreshDashboard()
if r.Dashboard.Spec.Json != "" {
r.Dashboard.Spec.Json = ""
err = r.Client.Update(r.Context, r.Dashboard)
if err != nil {
return err
}
}

r.Dashboard.Status = v1alpha1.GrafanaDashboardStatus{
Content: r.JSON,
ContentGzip: content,
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)
if !k8serrors.IsConflict(err) {
return fmt.Errorf("failed to update status with content for dashboard %s/%s: %w", r.Dashboard.Namespace, r.Dashboard.Name, err)
}
}

return nil
Expand Down Expand Up @@ -334,15 +360,30 @@ func (r *DashboardPipelineImpl) loadDashboardFromGrafanaCom() error {

r.JSON = string(body)

content, err := v1alpha1.Gzip(r.JSON)
if err != nil {
return err
}

r.refreshDashboard()
if r.Dashboard.Spec.Json != "" {
r.Dashboard.Spec.Json = ""
err = r.Client.Update(r.Context, r.Dashboard)
if err != nil {
return err
}
}

r.Dashboard.Status = v1alpha1.GrafanaDashboardStatus{
Content: r.JSON,
ContentGzip: content,
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)
if !k8serrors.IsConflict(err) {
return fmt.Errorf("failed to update status with content for dashboard %s/%s: %w", r.Dashboard.Namespace, r.Dashboard.Name, err)
}
}

return nil
Expand Down
4 changes: 2 additions & 2 deletions controllers/grafanadashboard/grafanadashboard_controller.go
Expand Up @@ -283,7 +283,7 @@ func (r *GrafanaDashboardReconciler) reconcileDashboards(request reconcile.Reque
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)
log.Log.V(1).Info("delaying retry of failing dashboard", "folder", folderName, "dashboard", dashboard.Name, "namespace", dashboard.Namespace, "retryTime", retryTime, "backoffDuration", backoffDuration)
continue
}
}
Expand Down Expand Up @@ -322,7 +322,7 @@ 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.", dashboard.ObjectMeta.Name))
log.Log.Info(fmt.Sprintf("Dashboard %v (%s) has been deleted via grafana console. Recreating.", dashboard.ObjectMeta.Name, knownUid))
processed, err = pipeline.ProcessDashboard(knownHash, &folderId, folderName, true)

if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions deploy/manifests/latest/crds.yaml
Expand Up @@ -131,6 +131,9 @@ spec:
properties:
content:
type: string
contentGzip:
format: byte
type: string
contentTimestamp:
format: date-time
type: string
Expand Down
9 changes: 9 additions & 0 deletions documentation/api.md
Expand Up @@ -379,6 +379,15 @@ GrafanaPlugin contains information about a single plugin
<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>contentGzip</b></td>
<td>string</td>
<td>
<br/>
<br/>
<i>Format</i>: byte<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>contentTimestamp</b></td>
<td>string</td>
Expand Down
2 changes: 1 addition & 1 deletion documentation/dashboards.md
Expand Up @@ -16,7 +16,7 @@ The following properties are accepted in the `spec`:
* *jsonnet*: Jsonnet source. The [Grafonnet](https://grafana.github.io/grafonnet-lib/) library is made available
automatically and can be imported.
* *url*: Url address to download a json or jsonnet string with the dashboard contents.
* ***Warning***: If both url and json are specified then the json field will be updated with fetched.
* ***Warning***: If both url and json are specified then the json field will be cleared.
* *The dashboard fetch priority by parameter is: url > configmap > json > jsonnet.*
* *plugins*: A list of plugins required by the dashboard. They will be installed by the operator if not already present.
* *datasources*: A list of datasources to be used as inputs. See [datasource inputs](#datasource-inputs).
Expand Down

0 comments on commit 749d5af

Please sign in to comment.