Skip to content

Commit

Permalink
feat(dashboard:jsonnet): support jsonnet external variables for Grafa…
Browse files Browse the repository at this point in the history
…naDashboard (#1130)

* feat(dashboard:jsonnet): support jsonnet external variables for GrafanaDashboard

* migrate env ref resources to dashboard.yaml files

* fix tailing whitespace issue in dashboards.md

* fix tailing whitespace issue in dashboards.md (2)

* add new line to 06-assert.yaml

* Update controllers/dashboard_controller.go

Co-authored-by: Hubert Stefanski <35736504+HubertStefanski@users.noreply.github.com>

---------

Co-authored-by: Hubert Stefanski <35736504+HubertStefanski@users.noreply.github.com>
  • Loading branch information
olejeglejeg and HVBE committed Jul 10, 2023
1 parent a79606f commit 473df6e
Show file tree
Hide file tree
Showing 21 changed files with 1,469 additions and 5 deletions.
32 changes: 32 additions & 0 deletions api/v1beta1/grafanadashboard_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"io"
"time"

v1 "k8s.io/api/core/v1"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -92,6 +94,36 @@ type GrafanaDashboardSpec struct {
// allow to import this resources from an operator in a different namespace
// +optional
AllowCrossNamespaceImport *bool `json:"allowCrossNamespaceImport,omitempty"`

// environments variables as a map
// +optional
Envs []GrafanaDashboardEnv `json:"envs,omitempty"`

// environments variables from secrets or config maps
// +optional
EnvsFrom []GrafanaDashboardEnvFromSource `json:"envFrom,omitempty"`
}

type GrafanaDashboardEnv struct {
Name string `json:"name"`
// Inline evn value
// +optional
Value string `json:"value:omitempty"`
// Selects a key of a ConfigMap.
// +optional
ConfigMapKeyRef *v1.ConfigMapKeySelector `json:"configMapKeyRef,omitempty"`
// Selects a key of a Secret.
// +optional
SecretKeyRef *v1.SecretKeySelector `json:"secretKeyRef,omitempty"`
}

type GrafanaDashboardEnvFromSource struct {
// Selects a key of a ConfigMap.
// +optional
ConfigMapKeyRef *v1.ConfigMapKeySelector `json:"configMapKeyRef,omitempty"`
// Selects a key of a Secret.
// +optional
SecretKeyRef *v1.SecretKeySelector `json:"secretKeyRef,omitempty"`
}

// GrafanaComDashbooardReference is a reference to a dashboard on grafana.com/dashboards
Expand Down
64 changes: 64 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

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

64 changes: 64 additions & 0 deletions config/crd/bases/grafana.integreatly.org_grafanadashboards.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,70 @@ spec:
- inputName
type: object
type: array
envFrom:
items:
properties:
configMapKeyRef:
properties:
key:
type: string
name:
type: string
optional:
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
secretKeyRef:
properties:
key:
type: string
name:
type: string
optional:
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
type: object
type: array
envs:
items:
properties:
configMapKeyRef:
properties:
key:
type: string
name:
type: string
optional:
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
name:
type: string
secretKeyRef:
properties:
key:
type: string
name:
type: string
optional:
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
value:omitempty:
type: string
required:
- name
type: object
type: array
folder:
type: string
grafanaCom:
Expand Down
93 changes: 93 additions & 0 deletions config/grafana.integreatly.org_grafanadashboards.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,99 @@ spec:
- inputName
type: object
type: array
envFrom:
description: environments variables from secrets or config maps
items:
properties:
configMapKeyRef:
description: Selects a key of 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
x-kubernetes-map-type: atomic
secretKeyRef:
description: Selects a key of a Secret.
properties:
key:
description: The key of the secret to select from. Must
be a valid secret key.
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 Secret or its key must
be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
type: object
type: array
envs:
description: environments variables as a map
items:
properties:
configMapKeyRef:
description: Selects a key of 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
x-kubernetes-map-type: atomic
name:
type: string
secretKeyRef:
description: Selects a key of a Secret.
properties:
key:
description: The key of the secret to select from. Must
be a valid secret key.
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 Secret or its key must
be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
value:omitempty:
description: Inline evn value
type: string
required:
- name
type: object
type: array
folder:
description: folder assignment for dashboard
type: string
Expand Down
49 changes: 46 additions & 3 deletions controllers/dashboard_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"

v1 "k8s.io/api/core/v1"
)

const (
Expand Down Expand Up @@ -186,7 +188,7 @@ func (r *GrafanaDashboardReconciler) Reconcile(ctx context.Context, req ctrl.Req

controllerLog.Info("found matching Grafana instances for dashboard", "count", len(instances.Items))

dashboardJson, err := r.fetchDashboardJson(cr)
dashboardJson, err := r.fetchDashboardJson(ctx, cr)
if err != nil {
controllerLog.Error(err, "error fetching dashboard", "dashboard", cr.Name)
return ctrl.Result{RequeueAfter: RequeueDelay}, nil
Expand Down Expand Up @@ -411,7 +413,7 @@ func (r *GrafanaDashboardReconciler) resolveDatasources(dashboard *v1beta1.Grafa

// fetchDashboardJson delegates obtaining the dashboard json definition to one of the known fetchers, for example
// from embedded raw json or from a url
func (r *GrafanaDashboardReconciler) fetchDashboardJson(dashboard *v1beta1.GrafanaDashboard) ([]byte, error) {
func (r *GrafanaDashboardReconciler) fetchDashboardJson(ctx context.Context, dashboard *v1beta1.GrafanaDashboard) ([]byte, error) {
sourceTypes := dashboard.GetSourceTypes()

if len(sourceTypes) == 0 {
Expand All @@ -430,14 +432,55 @@ func (r *GrafanaDashboardReconciler) fetchDashboardJson(dashboard *v1beta1.Grafa
case v1beta1.DashboardSourceTypeUrl:
return fetchers.FetchDashboardFromUrl(dashboard)
case v1beta1.DashboardSourceTypeJsonnet:
return fetchers.FetchJsonnet(dashboard, embeds.GrafonnetEmbed)
envs := make(map[string]string)
if dashboard.Spec.EnvsFrom != nil {
for _, ref := range dashboard.Spec.EnvsFrom {
key, val, err := r.getReferencedValue(ctx, dashboard, ref)
if err != nil {
return nil, fmt.Errorf("something went wrong processing envs, error: %w", err)
}
envs[key] = val
}
}
if dashboard.Spec.Envs != nil {
for _, ref := range dashboard.Spec.Envs {
envs[ref.Name] = ref.Value
}
}
return fetchers.FetchJsonnet(dashboard, envs, embeds.GrafonnetEmbed)
case v1beta1.DashboardSourceTypeGrafanaCom:
return fetchers.FetchDashboardFromGrafanaCom(dashboard)
default:
return nil, fmt.Errorf("unknown source type %v found in dashboard %v", sourceTypes[0], dashboard.Name)
}
}

func (r *GrafanaDashboardReconciler) getReferencedValue(ctx context.Context, cr *v1beta1.GrafanaDashboard, source v1beta1.GrafanaDashboardEnvFromSource) (string, string, error) {
if source.SecretKeyRef != nil {
s := &v1.Secret{}
err := r.Client.Get(ctx, client.ObjectKey{Namespace: cr.Namespace, Name: source.SecretKeyRef.Name}, s)
if err != nil {
return "", "", err
}
if val, ok := s.Data[source.SecretKeyRef.Key]; ok {
return source.SecretKeyRef.Key, string(val), nil
} else {
return "", "", fmt.Errorf("missing key %s in secret %s", source.SecretKeyRef.Key, source.ConfigMapKeyRef.Name)
}
} else {
s := &v1.ConfigMap{}
err := r.Client.Get(ctx, client.ObjectKey{Namespace: cr.Namespace, Name: source.SecretKeyRef.Name}, s)
if err != nil {
return "", "", err
}
if val, ok := s.Data[source.SecretKeyRef.Key]; ok {
return source.SecretKeyRef.Key, val, nil
} else {
return "", "", fmt.Errorf("missing key %s in configmap %s", source.SecretKeyRef.Key, source.ConfigMapKeyRef.Name)
}
}
}

// getDashboardModel resolves datasources, updates uid (if needed) and converts raw json to type grafana client accepts
func (r *GrafanaDashboardReconciler) getDashboardModel(cr *v1beta1.GrafanaDashboard, dashboardJson []byte) (map[string]interface{}, string, error) {
dashboardJson, err := r.resolveDatasources(cr, dashboardJson)
Expand Down
5 changes: 4 additions & 1 deletion controllers/fetchers/jsonnet_fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,14 @@ func (importer *EmbedFSImporter) Import(importedFrom, importedPath string) (cont
return foundContents, s, nil
}

func FetchJsonnet(dashboard *v1beta1.GrafanaDashboard, libsonnet embed.FS) ([]byte, error) {
func FetchJsonnet(dashboard *v1beta1.GrafanaDashboard, envs map[string]string, libsonnet embed.FS) ([]byte, error) {
if dashboard.Spec.Jsonnet == "" {
return nil, fmt.Errorf("no jsonnet Content Found, nil or empty string")
}
vm := jsonnet.MakeVM()
for k, v := range envs {
vm.ExtVar(k, v)
}

vm.Importer(&EmbedFSImporter{Embed: libsonnet})

Expand Down
Loading

0 comments on commit 473df6e

Please sign in to comment.