diff --git a/api/v1beta1/common.go b/api/v1beta1/common.go
new file mode 100644
index 000000000..9982ea7a5
--- /dev/null
+++ b/api/v1beta1/common.go
@@ -0,0 +1,17 @@
+package v1beta1
+
+import v1 "k8s.io/api/core/v1"
+
+type ValueFrom struct {
+ TargetPath string `json:"targetPath"`
+ ValueFrom ValueFromSource `json:"valueFrom"`
+}
+
+type ValueFromSource 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"`
+}
diff --git a/api/v1beta1/grafanacontactpoint_types.go b/api/v1beta1/grafanacontactpoint_types.go
index 7489e0109..988e21a56 100644
--- a/api/v1beta1/grafanacontactpoint_types.go
+++ b/api/v1beta1/grafanacontactpoint_types.go
@@ -45,6 +45,8 @@ type GrafanaContactPointSpec struct {
Settings *apiextensions.JSON `json:"settings"`
+ ValuesFrom []ValueFrom `json:"valuesFrom,omitempty"`
+
// +kubebuilder:validation:Enum=alertmanager;prometheus-alertmanager;dingding;discord;email;googlechat;kafka;line;opsgenie;pagerduty;pushover;sensugo;sensu;slack;teams;telegram;threema;victorops;webhook;wecom;hipchat;oncall
Type string `json:"type,omitempty"`
diff --git a/api/v1beta1/grafanadatasource_types.go b/api/v1beta1/grafanadatasource_types.go
index b0878dfca..285db9afb 100644
--- a/api/v1beta1/grafanadatasource_types.go
+++ b/api/v1beta1/grafanadatasource_types.go
@@ -23,8 +23,6 @@ import (
"fmt"
"time"
- v1 "k8s.io/api/core/v1"
-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -72,7 +70,7 @@ type GrafanaDatasourceSpec struct {
// environments variables from secrets or config maps
// +optional
- ValuesFrom []GrafanaDatasourceValueFrom `json:"valuesFrom,omitempty"`
+ ValuesFrom []ValueFrom `json:"valuesFrom,omitempty"`
// how often the datasource is refreshed, defaults to 5m if not set
// +optional
@@ -87,20 +85,6 @@ type GrafanaDatasourceSpec struct {
AllowCrossNamespaceImport *bool `json:"allowCrossNamespaceImport,omitempty"`
}
-type GrafanaDatasourceValueFrom struct {
- TargetPath string `json:"targetPath"`
- ValueFrom GrafanaDatasourceValueFromSource `json:"valueFrom"`
-}
-
-type GrafanaDatasourceValueFromSource 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"`
-}
-
// GrafanaDatasourceStatus defines the observed state of GrafanaDatasource
type GrafanaDatasourceStatus struct {
Hash string `json:"hash,omitempty"`
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index d2cfdbe44..a4242bf31 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -1095,7 +1095,7 @@ func (in *GrafanaDatasourceSpec) DeepCopyInto(out *GrafanaDatasourceSpec) {
}
if in.ValuesFrom != nil {
in, out := &in.ValuesFrom, &out.ValuesFrom
- *out = make([]GrafanaDatasourceValueFrom, len(*in))
+ *out = make([]ValueFrom, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -1134,23 +1134,23 @@ func (in *GrafanaDatasourceStatus) DeepCopy() *GrafanaDatasourceStatus {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *GrafanaDatasourceValueFrom) DeepCopyInto(out *GrafanaDatasourceValueFrom) {
+func (in *ValueFrom) DeepCopyInto(out *ValueFrom) {
*out = *in
in.ValueFrom.DeepCopyInto(&out.ValueFrom)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDatasourceValueFrom.
-func (in *GrafanaDatasourceValueFrom) DeepCopy() *GrafanaDatasourceValueFrom {
+func (in *ValueFrom) DeepCopy() *ValueFrom {
if in == nil {
return nil
}
- out := new(GrafanaDatasourceValueFrom)
+ out := new(ValueFrom)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *GrafanaDatasourceValueFromSource) DeepCopyInto(out *GrafanaDatasourceValueFromSource) {
+func (in *ValueFromSource) DeepCopyInto(out *ValueFromSource) {
*out = *in
if in.ConfigMapKeyRef != nil {
in, out := &in.ConfigMapKeyRef, &out.ConfigMapKeyRef
@@ -1165,11 +1165,11 @@ func (in *GrafanaDatasourceValueFromSource) DeepCopyInto(out *GrafanaDatasourceV
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDatasourceValueFromSource.
-func (in *GrafanaDatasourceValueFromSource) DeepCopy() *GrafanaDatasourceValueFromSource {
+func (in *ValueFromSource) DeepCopy() *ValueFromSource {
if in == nil {
return nil
}
- out := new(GrafanaDatasourceValueFromSource)
+ out := new(ValueFromSource)
in.DeepCopyInto(out)
return out
}
diff --git a/config/crd/bases/grafana.integreatly.org_grafanacontactpoints.yaml b/config/crd/bases/grafana.integreatly.org_grafanacontactpoints.yaml
index 9f307df55..c201e0d23 100644
--- a/config/crd/bases/grafana.integreatly.org_grafanacontactpoints.yaml
+++ b/config/crd/bases/grafana.integreatly.org_grafanacontactpoints.yaml
@@ -129,6 +129,70 @@ spec:
- hipchat
- oncall
type: string
+ valuesFrom:
+ items:
+ properties:
+ targetPath:
+ type: string
+ valueFrom:
+ properties:
+ configMapKeyRef:
+ description: Selects a key of a ConfigMap.
+ properties:
+ key:
+ description: The key to select.
+ type: string
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ 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:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ 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
+ required:
+ - targetPath
+ - valueFrom
+ type: object
+ type: array
required:
- instanceSelector
- name
diff --git a/controllers/controller_shared.go b/controllers/controller_shared.go
index 935a46822..31ab88b33 100644
--- a/controllers/controller_shared.go
+++ b/controllers/controller_shared.go
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana-operator/v5/api/v1beta1"
grafanav1beta1 "github.com/grafana/grafana-operator/v5/api/v1beta1"
"github.com/grafana/grafana-operator/v5/controllers/model"
+ corev1 "k8s.io/api/core/v1"
kuberr "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -230,3 +231,30 @@ func buildSynchronizedCondition(resource string, syncType string, generation int
}
return condition
}
+
+func getReferencedValue(ctx context.Context, cl client.Client, cr metav1.ObjectMetaAccessor, source v1beta1.ValueFromSource) (string, string, error) {
+ objMeta := cr.GetObjectMeta()
+ if source.SecretKeyRef != nil {
+ s := &corev1.Secret{}
+ err := cl.Get(ctx, client.ObjectKey{Namespace: objMeta.GetNamespace(), Name: source.SecretKeyRef.Name}, s)
+ if err != nil {
+ return "", "", err
+ }
+ if val, ok := s.Data[source.SecretKeyRef.Key]; ok {
+ return string(val), source.SecretKeyRef.Key, nil
+ } else {
+ return "", "", fmt.Errorf("missing key %s in secret %s", source.SecretKeyRef.Key, source.SecretKeyRef.Name)
+ }
+ } else {
+ s := &corev1.ConfigMap{}
+ err := cl.Get(ctx, client.ObjectKey{Namespace: objMeta.GetNamespace(), Name: source.ConfigMapKeyRef.Name}, s)
+ if err != nil {
+ return "", "", err
+ }
+ if val, ok := s.Data[source.ConfigMapKeyRef.Key]; ok {
+ return val, source.ConfigMapKeyRef.Key, nil
+ } else {
+ return "", "", fmt.Errorf("missing key %s in configmap %s", source.ConfigMapKeyRef.Key, source.ConfigMapKeyRef.Name)
+ }
+ }
+}
diff --git a/controllers/datasource_controller.go b/controllers/datasource_controller.go
index cdf4e7ce9..a34cf783f 100644
--- a/controllers/datasource_controller.go
+++ b/controllers/datasource_controller.go
@@ -25,8 +25,6 @@ import (
"strings"
"time"
- v1 "k8s.io/api/core/v1"
-
simplejson "github.com/bitly/go-simplejson"
"github.com/grafana/grafana-openapi-client-go/client/datasources"
"github.com/grafana/grafana-openapi-client-go/models"
@@ -453,7 +451,7 @@ func (r *GrafanaDatasourceReconciler) getDatasourceContent(ctx context.Context,
for _, ref := range cr.Spec.ValuesFrom {
ref := ref
- val, key, err := r.getReferencedValue(ctx, cr, &ref.ValueFrom)
+ val, key, err := getReferencedValue(ctx, r.Client, cr, ref.ValueFrom)
if err != nil {
return nil, "", err
}
@@ -485,29 +483,3 @@ func (r *GrafanaDatasourceReconciler) getDatasourceContent(ctx context.Context,
return &res, fmt.Sprintf("%x", hash.Sum(nil)), nil
}
-
-func (r *GrafanaDatasourceReconciler) getReferencedValue(ctx context.Context, cr *v1beta1.GrafanaDatasource, source *v1beta1.GrafanaDatasourceValueFromSource) (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 string(val), source.SecretKeyRef.Key, nil
- } else {
- return "", "", fmt.Errorf("missing key %s in secret %s", source.SecretKeyRef.Key, source.SecretKeyRef.Name)
- }
- } else {
- s := &v1.ConfigMap{}
- err := r.Client.Get(ctx, client.ObjectKey{Namespace: cr.Namespace, Name: source.ConfigMapKeyRef.Name}, s)
- if err != nil {
- return "", "", err
- }
- if val, ok := s.Data[source.ConfigMapKeyRef.Key]; ok {
- return val, source.ConfigMapKeyRef.Key, nil
- } else {
- return "", "", fmt.Errorf("missing key %s in configmap %s", source.ConfigMapKeyRef.Key, source.ConfigMapKeyRef.Name)
- }
- }
-}
diff --git a/controllers/grafanacontactpoint_controller.go b/controllers/grafanacontactpoint_controller.go
index ff5cf375a..14854960d 100644
--- a/controllers/grafanacontactpoint_controller.go
+++ b/controllers/grafanacontactpoint_controller.go
@@ -18,6 +18,7 @@ package controllers
import (
"context"
+ "encoding/json"
"fmt"
"strings"
"time"
@@ -31,6 +32,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
+ simplejson "github.com/bitly/go-simplejson"
"github.com/go-logr/logr"
"github.com/grafana/grafana-openapi-client-go/client/provisioning"
"github.com/grafana/grafana-openapi-client-go/models"
@@ -179,13 +181,18 @@ func (r *GrafanaContactPointReconciler) reconcileWithInstance(ctx context.Contex
return fmt.Errorf("getting contact point by UID: %w", err)
}
+ settings, err := r.buildSettings(ctx, contactPoint)
+ if err != nil {
+ return fmt.Errorf("overriding settings: %w", err)
+ }
+
if applied.UID == "" {
// create
cp := &models.EmbeddedContactPoint{
DisableResolveMessage: contactPoint.Spec.DisableResolveMessage,
Name: contactPoint.Spec.Name,
Type: &contactPoint.Spec.Type,
- Settings: contactPoint.Spec.Settings,
+ Settings: settings,
UID: string(contactPoint.UID),
}
_, err := cl.Provisioning.PostContactpoints(provisioning.NewPostContactpointsParams().WithBody(cp)) //nolint:errcheck
@@ -197,7 +204,7 @@ func (r *GrafanaContactPointReconciler) reconcileWithInstance(ctx context.Contex
var updatedCP models.EmbeddedContactPoint
updatedCP.Name = contactPoint.Spec.Name
updatedCP.Type = &contactPoint.Spec.Type
- updatedCP.Settings = contactPoint.Spec.Settings
+ updatedCP.Settings = settings
_, err := cl.Provisioning.PutContactpoint(provisioning.NewPutContactpointParams().WithUID(applied.UID).WithBody(&updatedCP)) //nolint:errcheck
if err != nil {
return fmt.Errorf("updating contact point: %w", err)
@@ -206,6 +213,27 @@ func (r *GrafanaContactPointReconciler) reconcileWithInstance(ctx context.Contex
return nil
}
+func (r *GrafanaContactPointReconciler) buildSettings(ctx context.Context, contactPoint *grafanav1beta1.GrafanaContactPoint) (models.JSON, error) {
+ marshaled, err := json.Marshal(contactPoint.Spec.Settings)
+ if err != nil {
+ return nil, fmt.Errorf("encoding existing settings as json: %w", err)
+ }
+ simpleContent, err := simplejson.NewJson(marshaled)
+ if err != nil {
+ return nil, fmt.Errorf("parsing marshaled json as simplejson")
+ }
+ for _, override := range contactPoint.Spec.ValuesFrom {
+ val, _, err := getReferencedValue(ctx, r.Client, contactPoint, override.ValueFrom)
+ if err != nil {
+ return nil, fmt.Errorf("getting referenced value: %w", err)
+ }
+ r.Log.Info("overriding value", "key", override.TargetPath, "value", val)
+
+ simpleContent.SetPath(strings.Split(override.TargetPath, "."), val)
+ }
+ return simpleContent.Interface(), nil
+}
+
func (r *GrafanaContactPointReconciler) getContactPointFromUID(ctx context.Context, instance *grafanav1beta1.Grafana, contactPoint *grafanav1beta1.GrafanaContactPoint) (models.EmbeddedContactPoint, error) {
cl, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, instance)
if err != nil {
diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanacontactpoints.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanacontactpoints.yaml
index 9f307df55..c201e0d23 100644
--- a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanacontactpoints.yaml
+++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanacontactpoints.yaml
@@ -129,6 +129,70 @@ spec:
- hipchat
- oncall
type: string
+ valuesFrom:
+ items:
+ properties:
+ targetPath:
+ type: string
+ valueFrom:
+ properties:
+ configMapKeyRef:
+ description: Selects a key of a ConfigMap.
+ properties:
+ key:
+ description: The key to select.
+ type: string
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ 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:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ 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
+ required:
+ - targetPath
+ - valueFrom
+ type: object
+ type: array
required:
- instanceSelector
- name
diff --git a/deploy/kustomize/base/crds.yaml b/deploy/kustomize/base/crds.yaml
index e3cb3f95d..9275e12bd 100644
--- a/deploy/kustomize/base/crds.yaml
+++ b/deploy/kustomize/base/crds.yaml
@@ -439,6 +439,70 @@ spec:
- hipchat
- oncall
type: string
+ valuesFrom:
+ items:
+ properties:
+ targetPath:
+ type: string
+ valueFrom:
+ properties:
+ configMapKeyRef:
+ description: Selects a key of a ConfigMap.
+ properties:
+ key:
+ description: The key to select.
+ type: string
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ 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:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ 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
+ required:
+ - targetPath
+ - valueFrom
+ type: object
+ type: array
required:
- instanceSelector
- name
diff --git a/docs/docs/api.md b/docs/docs/api.md
index 3691d10d0..97ba833f2 100644
--- a/docs/docs/api.md
+++ b/docs/docs/api.md
@@ -751,6 +751,13 @@ GrafanaContactPointSpec defines the desired state of GrafanaContactPoint
Enum: alertmanager, prometheus-alertmanager, dingding, discord, email, googlechat, kafka, line, opsgenie, pagerduty, pushover, sensugo, sensu, slack, teams, telegram, threema, victorops, webhook, wecom, hipchat, oncall
Name | +Type | +Description | +Required | +
---|---|---|---|
targetPath | +string | +
+ + |
+ true | +
valueFrom | +object | +
+ + |
+ true | +
Name | +Type | +Description | +Required | +
---|---|---|---|
configMapKeyRef | +object | +
+ Selects a key of a ConfigMap. + |
+ false | +
secretKeyRef | +object | +
+ Selects a key of a Secret. + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
key | +string | +
+ The key to select. + |
+ true | +
name | +string | +
+ Name of the referent.
+This field is effectively required, but due to backwards compatibility is
+allowed to be empty. Instances of this type with an empty value here are
+almost certainly wrong.
+TODO: Add other useful fields. apiVersion, kind, uid?
+More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + + Default: + |
+ false | +
optional | +boolean | +
+ Specify whether the ConfigMap or its key must be defined + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
key | +string | +
+ The key of the secret to select from. Must be a valid secret key. + |
+ true | +
name | +string | +
+ Name of the referent.
+This field is effectively required, but due to backwards compatibility is
+allowed to be empty. Instances of this type with an empty value here are
+almost certainly wrong.
+TODO: Add other useful fields. apiVersion, kind, uid?
+More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + + Default: + |
+ false | +
optional | +boolean | +
+ Specify whether the Secret or its key must be defined + |
+ false | +