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

feat(dashboard:jsonnet): support jsonnet external variables for GrafanaDashboard #1130

Merged
merged 9 commits into from
Jul 10, 2023
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