Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to provide compressed JSON for large dashboards #767

Merged
merged 17 commits into from Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 29 additions & 1 deletion 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 @@ -33,10 +37,12 @@ import (
// GrafanaDashboardSpec defines the desired state of GrafanaDashboard
type GrafanaDashboardSpec struct {
Json string `json:"json,omitempty"`
GzipJson []byte `json:"gzipJson,omitempty"`
NissesSenap marked this conversation as resolved.
Show resolved Hide resolved
Jsonnet string `json:"jsonnet,omitempty"`
Plugins PluginList `json:"plugins,omitempty"`
Url string `json:"url,omitempty"`
ConfigMapRef *corev1.ConfigMapKeySelector `json:"configMapRef,omitempty"`
GzipConfigMapRef *corev1.ConfigMapKeySelector `json:"gzipConfigMapRef,omitempty"`
NissesSenap marked this conversation as resolved.
Show resolved Hide resolved
Datasources []GrafanaDashboardDatasource `json:"datasources,omitempty"`
CustomFolderName string `json:"customFolderName,omitempty"`
GrafanaCom *GrafanaDashboardGrafanaComSource `json:"grafanaCom,omitempty"`
Expand Down Expand Up @@ -100,6 +106,7 @@ func (d *GrafanaDashboard) Hash() string {
}

io.WriteString(hash, d.Spec.Json) // nolint
hash.Write(d.Spec.GzipJson) // nolint
io.WriteString(hash, d.Spec.Url) // nolint
io.WriteString(hash, d.Spec.Jsonnet) // nolint
io.WriteString(hash, d.Namespace) // nolint
Expand All @@ -109,6 +116,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 @@ -121,7 +132,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 @@ -145,3 +165,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.

20 changes: 20 additions & 0 deletions config/crd/bases/integreatly.org_grafanadashboards.yaml
Expand Up @@ -76,6 +76,26 @@ spec:
required:
- id
type: object
gzipConfigMapRef:
description: Selects a key from a ConfigMap.
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:
format: byte
type: string
json:
type: string
jsonnet:
Expand Down
35 changes: 31 additions & 4 deletions controllers/grafanadashboard/dashboard_pipeline.go
Expand Up @@ -142,14 +142,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 @@ -385,17 +404,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==
@@ -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=
20 changes: 20 additions & 0 deletions deploy/manifests/latest/crds.yaml
Expand Up @@ -74,6 +74,26 @@ spec:
required:
- id
type: object
gzipConfigMapRef:
description: Selects a key from a ConfigMap.
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:
format: byte
type: string
json:
type: string
jsonnet:
Expand Down