Skip to content

Commit

Permalink
Merge pull request #767 from meln5674/feature/grafana-operator-726
Browse files Browse the repository at this point in the history
Add ability to provide compressed JSON for large dashboards
  • Loading branch information
pb82 committed Jul 12, 2022
2 parents 32bfcb1 + fae8a3b commit 0b033e7
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 20 deletions.
47 changes: 40 additions & 7 deletions api/integreatly/v1alpha1/grafanadashboard_types.go
Expand Up @@ -17,11 +17,15 @@ limitations under the License.
package v1alpha1

import (
"bytes"
"crypto/sha1" // nolint
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"io/ioutil"

"compress/gzip"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -32,11 +36,18 @@ import (

// GrafanaDashboardSpec defines the desired state of GrafanaDashboard
type GrafanaDashboardSpec struct {
Json string `json:"json,omitempty"`
Jsonnet string `json:"jsonnet,omitempty"`
Plugins PluginList `json:"plugins,omitempty"`
Url string `json:"url,omitempty"`
ConfigMapRef *corev1.ConfigMapKeySelector `json:"configMapRef,omitempty"`
// Json is the dashboard's JSON
Json string `json:"json,omitempty"`
// GzipJson the dashboard's JSON compressed with Gzip. Base64-encoded when in YAML.
GzipJson []byte `json:"gzipJson,omitempty"`
Jsonnet string `json:"jsonnet,omitempty"`
Plugins PluginList `json:"plugins,omitempty"`
Url string `json:"url,omitempty"`
// ConfigMapRef is a reference to a ConfigMap data field containing the dashboard's JSON
ConfigMapRef *corev1.ConfigMapKeySelector `json:"configMapRef,omitempty"`
// GzipConfigMapRef is a reference to a ConfigMap binaryData field containing
// the dashboard's JSON, compressed with Gzip.
GzipConfigMapRef *corev1.ConfigMapKeySelector `json:"gzipConfigMapRef,omitempty"`
Datasources []GrafanaDashboardDatasource `json:"datasources,omitempty"`
CustomFolderName string `json:"customFolderName,omitempty"`
GrafanaCom *GrafanaDashboardGrafanaComSource `json:"grafanaCom,omitempty"`
Expand Down Expand Up @@ -115,7 +126,8 @@ func (d *GrafanaDashboard) Hash() string {
io.WriteString(hash, input.InputName) // nolint
}

io.WriteString(hash, d.Spec.Json) // nolint
io.WriteString(hash, d.Spec.Json) // nolint
hash.Write(d.Spec.GzipJson)
io.WriteString(hash, d.Spec.Url) // nolint
io.WriteString(hash, d.Spec.Jsonnet) // nolint
io.WriteString(hash, d.Namespace) // nolint
Expand All @@ -125,6 +137,10 @@ func (d *GrafanaDashboard) Hash() string {
io.WriteString(hash, d.Spec.ConfigMapRef.Name) // nolint
io.WriteString(hash, d.Spec.ConfigMapRef.Key) // nolint
}
if d.Spec.GzipConfigMapRef != nil {
io.WriteString(hash, d.Spec.GzipConfigMapRef.Name) // nolint
io.WriteString(hash, d.Spec.GzipConfigMapRef.Key) // nolint
}

if d.Spec.GrafanaCom != nil {
io.WriteString(hash, fmt.Sprint((d.Spec.GrafanaCom.Id))) // nolint
Expand All @@ -141,7 +157,16 @@ func (d *GrafanaDashboard) Hash() string {
}

func (d *GrafanaDashboard) Parse(optional string) (map[string]interface{}, error) {
var dashboardBytes = []byte(d.Spec.Json)
var dashboardBytes []byte
if d.Spec.GzipJson != nil {
var err error
dashboardBytes, err = Gunzip(d.Spec.GzipJson)
if err != nil {
return nil, err
}
} else {
dashboardBytes = []byte(d.Spec.Json)
}
if optional != "" {
dashboardBytes = []byte(optional)
}
Expand All @@ -165,3 +190,11 @@ func (d *GrafanaDashboard) UID() string {
// Grafana allows for UIDs
return fmt.Sprintf("%x", sha1.Sum([]byte(d.Namespace+d.Name))) // nolint
}

func Gunzip(compressed []byte) ([]byte, error) {
decoder, err := gzip.NewReader(bytes.NewReader(compressed))
if err != nil {
return nil, err
}
return ioutil.ReadAll(decoder)
}
80 changes: 80 additions & 0 deletions api/integreatly/v1alpha1/grafanadashboard_types_test.go
@@ -0,0 +1,80 @@
package v1alpha1

import (
"encoding/base64"
"encoding/json"
"io/ioutil"
"reflect"
"strings"
"testing"
)

// Encoded via cat | gzip | base64
const encodedCompressedDashboard = `
H4sIAAAAAAAAA3WQMU/DQAyF9/6KU2aQYAAkVliZqFgQqpzGSaxczief2wqq/nd8l5DAwOb3+dnP
8nnjXEVN9ejCwfurrJTUo4Hqlcbo0T1D6msGaVwrPLonDi117gViNdmhS+Z+/ygq6ec03IAMs4FG
/OJQaC18SihTAxtSqItd5YCF9dSgJaiwz1tb8GlqdAKx3zJ7pWiN2wIjBPS/0nOUqbPVpvK5OTTw
6fq+L5nZwzOrTF+WsUj7wQ5bhjPbcVTisAYYF2wFU7+joChHmNPXVWg/A6XQras8Jf3rghBY4Wf3
v7Y5K997l6afpX2PI7yhJBvOf3go+LiAm6I9hWG+7LL5BgwYIaHkAQAA
`

const decodedDashboard = `
{
"id": null,
"title": "Simple Dashboard from Config Map",
"tags": [],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"graphTooltip": 1,
"panels": [],
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"time_options": [],
"refresh_intervals": []
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"refresh": "5s",
"schemaVersion": 17,
"version": 0,
"links": []
}
`

func TestDecompress(t *testing.T) {
var expected map[string]interface{}
var actual map[string]interface{}
decoded, err := ioutil.ReadAll(base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedCompressedDashboard)))
if err != nil {
t.Log("Failed to decode", err)
t.Fail()
}
decompressed, err := Gunzip(decoded)
if err != nil {
t.Log("Failed to decompress", err)
t.Fail()
}
err = json.Unmarshal([]byte(decompressed), &actual)
if err != nil {
t.Log("Failed to parse JSON from decoded", err)
t.Fail()
}
err = json.Unmarshal([]byte(decodedDashboard), &expected)
if err != nil {
t.Log("Failed to parse JSON from ground truth", err)
t.Fail()
}
if !reflect.DeepEqual(expected, actual) {
t.Log("Decoded JSONs were not the same")
t.Fail()
}
}
10 changes: 10 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.

27 changes: 26 additions & 1 deletion config/crd/bases/integreatly.org_grafanadashboards.yaml
Expand Up @@ -37,7 +37,8 @@ spec:
description: GrafanaDashboardSpec defines the desired state of GrafanaDashboard
properties:
configMapRef:
description: Selects a key from a ConfigMap.
description: ConfigMapRef is a reference to a ConfigMap data field
containing the dashboard's JSON
properties:
key:
description: The key to select.
Expand Down Expand Up @@ -84,7 +85,31 @@ spec:
required:
- id
type: object
gzipConfigMapRef:
description: GzipConfigMapRef is a reference to a ConfigMap binaryData
field containing the dashboard's JSON, compressed with Gzip.
properties:
key:
description: The key to select.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the ConfigMap or its key must be
defined
type: boolean
required:
- key
type: object
gzipJson:
description: GzipJson the dashboard's JSON compressed with Gzip. Base64-encoded
when in YAML.
format: byte
type: string
json:
description: Json is the dashboard's JSON
type: string
jsonnet:
type: string
Expand Down
35 changes: 31 additions & 4 deletions controllers/grafanadashboard/dashboard_pipeline.go
Expand Up @@ -160,14 +160,33 @@ func (r *DashboardPipelineImpl) obtainJson() error {
}

if r.Dashboard.Spec.ConfigMapRef != nil {
err := r.loadDashboardFromConfigMap()
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")
} else {
return nil
}
}

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")
} else {
return nil
}
}

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")
} else {
r.JSON = string(jsonBytes)
return nil
}
}

if r.Dashboard.Spec.Json != "" {
r.JSON = r.Dashboard.Spec.Json
return nil
Expand Down Expand Up @@ -456,17 +475,25 @@ func (r *DashboardPipelineImpl) getFileType(path string) SourceType {
}

// Try to obtain the dashboard json from a config map
func (r *DashboardPipelineImpl) loadDashboardFromConfigMap() error {
func (r *DashboardPipelineImpl) loadDashboardFromConfigMap(ref *corev1.ConfigMapKeySelector, binaryCompressed bool) error {
ctx := context.Background()
objectKey := client.ObjectKey{Name: r.Dashboard.Spec.ConfigMapRef.Name, Namespace: r.Dashboard.Namespace}
objectKey := client.ObjectKey{Name: ref.Name, Namespace: r.Dashboard.Namespace}

var cm corev1.ConfigMap
err := r.Client.Get(ctx, objectKey, &cm)
if err != nil {
return err
}

r.JSON = cm.Data[r.Dashboard.Spec.ConfigMapRef.Key]
if binaryCompressed {
jsonBytes, err := v1alpha1.Gunzip(cm.BinaryData[ref.Key])
if err != nil {
return err
}
r.JSON = string(jsonBytes)
} else {
r.JSON = cm.Data[ref.Key]
}

return nil
}
Expand Down
9 changes: 9 additions & 0 deletions deploy/examples/dashboards/CompressedDashboard.yaml
@@ -0,0 +1,9 @@
apiVersion: integreatly.org/v1alpha1
kind: GrafanaDashboard
metadata:
name: compressed-dashboard
labels:
app: grafana
spec:
gzipJson: |-
H4sIAAAAAAAAA3WQwU7DMAyG73uKqmeQ4ABIXMcjIC4ITe7iNtbSOLK9TTDt3UnS0sKBm//Pv/1bvmyapiXXPjfxGMJNUUYWMIN2y2MSVEXXvID6jkFcO1lg0Ox4/6hK7XMacCCH2UAjfnGstBM+K8rUQEcGXbWbHLEyTw63HE04lK09BJ0ag0Dyr8zBKOXGfYUJIoZf6SUqq0uus+qFxxIa+Xz76Gtm8fDM2qyvy1ii/SEftgwXtuNkxHENyFywz3/wO4qGcoI5fV2FYwpgFId1VSC1vy6IkQ1+dv9rm7PKvQ86/Uz3Hkd4Q9E8XP7wVPFpAXdVB4qH+bLr5huze+uv2AEAAA==
23 changes: 23 additions & 0 deletions deploy/examples/dashboards/DashboardFromCompressedConfigMap.yaml
@@ -0,0 +1,23 @@
apiVersion: integreatly.org/v1alpha1
kind: GrafanaDashboard
metadata:
name: grafana-dashboard-from-compressed-config-map
labels:
app: grafana
spec:
json: ""
gzipConfigMapRef:
name: simple-dashboard-from-compressed-cm
key: foo
---
apiVersion: v1
kind: ConfigMap
metadata:
name: simple-dashboard-from-compressed-cm
binaryData:
foo: |-
H4sIAAAAAAAAA3VQPU/EMAzd71dEnUGCAZBYYWXixILQyb24jdU0jhLfneB0/x0nLS0MbH7P78Py
eWNMQ7Z5NOHg/VVBQuJRieaVxujRPEN2LUOypks8miceY8Kc0eoYOurNC8RmckKf1fj+UVGWzynH
QhpmAY34xaGybeJTxjQt0JJAW+WSDlg5Rxa1QRL7ktqBz9OiTxDdltkLRV3cVjJCQP+rvVQpOuus
qFxeSgOfru9d7SwanrlG8WWxRdoPethiLtyOoxCHtUD5hJ0+wu0oCKYjzO1rFOr7QCj0a5SnLH9V
EAIL/GT/K5u7yr13efpZ3jsc4Q1TVnP5w0OljwtxU7GnMMyXXTbf/Wift+8BAAA=
27 changes: 26 additions & 1 deletion deploy/manifests/latest/crds.yaml
Expand Up @@ -35,7 +35,8 @@ spec:
description: GrafanaDashboardSpec defines the desired state of GrafanaDashboard
properties:
configMapRef:
description: Selects a key from a ConfigMap.
description: ConfigMapRef is a reference to a ConfigMap data field
containing the dashboard's JSON
properties:
key:
description: The key to select.
Expand Down Expand Up @@ -82,7 +83,31 @@ spec:
required:
- id
type: object
gzipConfigMapRef:
description: GzipConfigMapRef is a reference to a ConfigMap binaryData
field containing the dashboard's JSON, compressed with Gzip.
properties:
key:
description: The key to select.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the ConfigMap or its key must be
defined
type: boolean
required:
- key
type: object
gzipJson:
description: GzipJson the dashboard's JSON compressed with Gzip. Base64-encoded
when in YAML.
format: byte
type: string
json:
description: Json is the dashboard's JSON
type: string
jsonnet:
type: string
Expand Down

0 comments on commit 0b033e7

Please sign in to comment.