From 6940ab9151506f4d7dbf47fd6cc8f11d36bcec48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Mon, 9 Sep 2024 12:43:51 +0200 Subject: [PATCH] feat: contact point overrides --- api/v1beta1/common.go | 17 ++ api/v1beta1/grafanacontactpoint_types.go | 2 + api/v1beta1/grafanadatasource_types.go | 18 +- api/v1beta1/zz_generated.deepcopy.go | 14 +- ....integreatly.org_grafanacontactpoints.yaml | 64 +++++++ controllers/controller_shared.go | 28 +++ controllers/datasource_controller.go | 30 +-- controllers/grafanacontactpoint_controller.go | 32 +++- ....integreatly.org_grafanacontactpoints.yaml | 64 +++++++ deploy/kustomize/base/crds.yaml | 64 +++++++ docs/docs/api.md | 173 ++++++++++++++++++ 11 files changed, 451 insertions(+), 55 deletions(-) create mode 100644 api/v1beta1/common.go 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
false + + valuesFrom + []object + +
+ + false @@ -837,6 +844,172 @@ merge patch.
+### GrafanaContactPoint.spec.valuesFrom[index] +[↩ Parent](#grafanacontactpointspec) + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
targetPathstring +
+
true
valueFromobject +
+
true
+ + +### GrafanaContactPoint.spec.valuesFrom[index].valueFrom +[↩ Parent](#grafanacontactpointspecvaluesfromindex) + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
configMapKeyRefobject + Selects a key of a ConfigMap.
+
false
secretKeyRefobject + Selects a key of a Secret.
+
false
+ + +### GrafanaContactPoint.spec.valuesFrom[index].valueFrom.configMapKeyRef +[↩ Parent](#grafanacontactpointspecvaluesfromindexvaluefrom) + + + +Selects a key of a ConfigMap. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
keystring + The key to select.
+
true
namestring + 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
optionalboolean + Specify whether the ConfigMap or its key must be defined
+
false
+ + +### GrafanaContactPoint.spec.valuesFrom[index].valueFrom.secretKeyRef +[↩ Parent](#grafanacontactpointspecvaluesfromindexvaluefrom) + + + +Selects a key of a Secret. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
keystring + The key of the secret to select from. Must be a valid secret key.
+
true
namestring + 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
optionalboolean + Specify whether the Secret or its key must be defined
+
false
+ + ### GrafanaContactPoint.status [↩ Parent](#grafanacontactpoint)