From 71828d2848575a02006868c6b712338843b79639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Tue, 13 Feb 2024 14:50:41 +0100 Subject: [PATCH 01/26] feat: initial alerting support --- PROJECT | 9 + api/v1beta1/grafanaalertrulegroup_types.go | 129 ++++++++ api/v1beta1/zz_generated.deepcopy.go | 187 +++++++++++ ...ntegreatly.org_grafanaalertrulegroups.yaml | 186 +++++++++++ config/crd/kustomization.yaml | 3 + ...cainjection_in_grafanaalertrulegroups.yaml | 7 + .../webhook_in_grafanaalertrulegroups.yaml | 16 + ...ntegreatly.org_grafanaalertrulegroups.yaml | 266 +++++++++++++++ ...rafana-operator.clusterserviceversion.yaml | 6 + .../grafanaalertrulegroup_editor_role.yaml | 31 ++ .../grafanaalertrulegroup_viewer_role.yaml | 27 ++ config/rbac/role.yaml | 26 ++ ...grafana_v1beta1_grafanaalertrulegroup.yaml | 66 ++++ config/samples/kustomization.yaml | 1 + controllers/client/grafana_client.go | 48 +++ controllers/controller_shared.go | 26 ++ .../grafanaalertrulegroup_controller.go | 310 ++++++++++++++++++ ...ntegreatly.org_grafanaalertrulegroups.yaml | 186 +++++++++++ go.mod | 28 +- go.sum | 97 ++++-- main.go | 7 + 21 files changed, 1637 insertions(+), 25 deletions(-) create mode 100644 api/v1beta1/grafanaalertrulegroup_types.go create mode 100644 config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml create mode 100644 config/crd/patches/cainjection_in_grafanaalertrulegroups.yaml create mode 100644 config/crd/patches/webhook_in_grafanaalertrulegroups.yaml create mode 100644 config/grafana.integreatly.org_grafanaalertrulegroups.yaml create mode 100644 config/rbac/grafanaalertrulegroup_editor_role.yaml create mode 100644 config/rbac/grafanaalertrulegroup_viewer_role.yaml create mode 100644 config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml create mode 100644 controllers/grafanaalertrulegroup_controller.go create mode 100644 deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml diff --git a/PROJECT b/PROJECT index 02a361eba..34920e852 100644 --- a/PROJECT +++ b/PROJECT @@ -42,4 +42,13 @@ resources: kind: GrafanaFolder path: github.com/grafana/grafana-operator/api/v1beta1 version: v1beta1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: integreatly.org + group: grafana + kind: GrafanaAlertRuleGroup + path: github.com/grafana/grafana-operator/api/v1beta1 + version: v1beta1 version: "3" diff --git a/api/v1beta1/grafanaalertrulegroup_types.go b/api/v1beta1/grafanaalertrulegroup_types.go new file mode 100644 index 000000000..651b2e662 --- /dev/null +++ b/api/v1beta1/grafanaalertrulegroup_types.go @@ -0,0 +1,129 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "github.com/grafana/grafana-openapi-client-go/models" + apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GrafanaAlertRuleGroupSpec defines the desired state of GrafanaAlertRuleGroup +type GrafanaAlertRuleGroupSpec struct { + // +optional + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=duration + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$" + // +kubebuilder:default="10m" + ResyncPeriod metav1.Duration `json:"resyncPeriod,omitempty"` + + // selects Grafanas for import + InstanceSelector *metav1.LabelSelector `json:"instanceSelector"` + + // UID of the folder containing this rule group + FolderUID string `json:"folderUID"` + + Rules []AlertRule `json:"rules"` + + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=duration + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$" + // +kubebuilder:validation:Required + Interval metav1.Duration `json:"interval"` +} + +// AlertRule defines a specific rule to be evaluated. It is based on the upstream model with some k8s specific type mappings +type AlertRule struct { + Annotations map[string]string `json:"annotations,omitempty"` + + Condition string `json:"condition"` + + // +kubebuilder:validation:Required + Data []*AlertQuery `json:"data"` + + // +kubebuilder:validation:Enum=OK;Alerting;Error + ExecErrState string `json:"execErrState"` + + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=duration + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$" + // +kubebuilder:validation:Required + For *metav1.Duration `json:"for"` + + IsPaused bool `json:"isPaused,omitempty"` + + Labels map[string]string `json:"labels,omitempty"` + + // +kubebuilder:validation:Enum=Alerting;NoData;OK + NoDataState *string `json:"noDataState"` + + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=190 + // +kubebuilder:example="Always firing" + Title string `json:"title"` + + // +kubebuilder:validation:Pattern="^[a-zA-Z0-9-_]+$" + UID string `json:"uid"` +} + +type AlertQuery struct { + // Grafana data source unique identifier; it should be '__expr__' for a Server Side Expression operation. + DatasourceUID string `json:"datasourceUid,omitempty"` + + // JSON is the raw JSON query and includes the above properties as well as custom properties. + Model *apiextensions.JSON `json:"model,omitempty"` + + // QueryType is an optional identifier for the type of query. + // It can be used to distinguish different types of queries. + QueryType string `json:"queryType,omitempty"` + + // RefID is the unique identifier of the query, set by the frontend call. + RefID string `json:"refId,omitempty"` + + // relative time range + RelativeTimeRange *models.RelativeTimeRange `json:"relativeTimeRange,omitempty"` +} + +// GrafanaAlertRuleGroupStatus defines the observed state of GrafanaAlertRuleGroup +type GrafanaAlertRuleGroupStatus struct { + Conditions []metav1.Condition `json:"conditions"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// GrafanaAlertRuleGroup is the Schema for the grafanaalertrulegroups API +type GrafanaAlertRuleGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GrafanaAlertRuleGroupSpec `json:"spec,omitempty"` + Status GrafanaAlertRuleGroupStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// GrafanaAlertRuleGroupList contains a list of GrafanaAlertRuleGroup +type GrafanaAlertRuleGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GrafanaAlertRuleGroup `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GrafanaAlertRuleGroup{}, &GrafanaAlertRuleGroupList{}) +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 2241118f6..9c4b47c62 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -23,14 +23,91 @@ package v1beta1 import ( "encoding/json" + "github.com/grafana/grafana-openapi-client-go/models" routev1 "github.com/openshift/api/route/v1" appsv1 "k8s.io/api/apps/v1" "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertQuery) DeepCopyInto(out *AlertQuery) { + *out = *in + if in.Model != nil { + in, out := &in.Model, &out.Model + *out = new(apiextensionsv1.JSON) + (*in).DeepCopyInto(*out) + } + if in.RelativeTimeRange != nil { + in, out := &in.RelativeTimeRange, &out.RelativeTimeRange + *out = new(models.RelativeTimeRange) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertQuery. +func (in *AlertQuery) DeepCopy() *AlertQuery { + if in == nil { + return nil + } + out := new(AlertQuery) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertRule) DeepCopyInto(out *AlertRule) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = make([]*AlertQuery, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(AlertQuery) + (*in).DeepCopyInto(*out) + } + } + } + if in.For != nil { + in, out := &in.For, &out.For + *out = new(metav1.Duration) + **out = **in + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.NoDataState != nil { + in, out := &in.NoDataState, &out.NoDataState + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertRule. +func (in *AlertRule) DeepCopy() *AlertRule { + if in == nil { + return nil + } + out := new(AlertRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentV1) DeepCopyInto(out *DeploymentV1) { *out = *in @@ -329,6 +406,116 @@ func (in *Grafana) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GrafanaAlertRuleGroup) DeepCopyInto(out *GrafanaAlertRuleGroup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaAlertRuleGroup. +func (in *GrafanaAlertRuleGroup) DeepCopy() *GrafanaAlertRuleGroup { + if in == nil { + return nil + } + out := new(GrafanaAlertRuleGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GrafanaAlertRuleGroup) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GrafanaAlertRuleGroupList) DeepCopyInto(out *GrafanaAlertRuleGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GrafanaAlertRuleGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaAlertRuleGroupList. +func (in *GrafanaAlertRuleGroupList) DeepCopy() *GrafanaAlertRuleGroupList { + if in == nil { + return nil + } + out := new(GrafanaAlertRuleGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GrafanaAlertRuleGroupList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GrafanaAlertRuleGroupSpec) DeepCopyInto(out *GrafanaAlertRuleGroupSpec) { + *out = *in + out.ResyncPeriod = in.ResyncPeriod + if in.InstanceSelector != nil { + in, out := &in.InstanceSelector, &out.InstanceSelector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]AlertRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.Interval = in.Interval +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaAlertRuleGroupSpec. +func (in *GrafanaAlertRuleGroupSpec) DeepCopy() *GrafanaAlertRuleGroupSpec { + if in == nil { + return nil + } + out := new(GrafanaAlertRuleGroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GrafanaAlertRuleGroupStatus) DeepCopyInto(out *GrafanaAlertRuleGroupStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaAlertRuleGroupStatus. +func (in *GrafanaAlertRuleGroupStatus) DeepCopy() *GrafanaAlertRuleGroupStatus { + if in == nil { + return nil + } + out := new(GrafanaAlertRuleGroupStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GrafanaClient) DeepCopyInto(out *GrafanaClient) { *out = *in diff --git a/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml new file mode 100644 index 000000000..660cc6844 --- /dev/null +++ b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -0,0 +1,186 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.0 + name: grafanaalertrulegroups.grafana.integreatly.org +spec: + group: grafana.integreatly.org + names: + kind: GrafanaAlertRuleGroup + listKind: GrafanaAlertRuleGroupList + plural: grafanaalertrulegroups + singular: grafanaalertrulegroup + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + folderUID: + type: string + instanceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + interval: + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + resyncPeriod: + default: 10m + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + rules: + items: + properties: + annotations: + additionalProperties: + type: string + type: object + condition: + type: string + data: + items: + properties: + datasourceUid: + type: string + model: + x-kubernetes-preserve-unknown-fields: true + queryType: + type: string + refId: + type: string + relativeTimeRange: + properties: + from: + format: int64 + type: integer + to: + format: int64 + type: integer + type: object + type: object + type: array + execErrState: + enum: + - OK + - Alerting + - Error + type: string + for: + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + isPaused: + type: boolean + labels: + additionalProperties: + type: string + type: object + noDataState: + enum: + - Alerting + - NoData + - OK + type: string + title: + example: Always firing + maxLength: 190 + minLength: 1 + type: string + uid: + pattern: ^[a-zA-Z0-9-_]+$ + type: string + required: + - condition + - data + - execErrState + - for + - noDataState + - title + - uid + type: object + type: array + required: + - folderUID + - instanceSelector + - interval + - rules + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index db580130c..2187eb794 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,6 +6,7 @@ resources: - bases/grafana.integreatly.org_grafanadashboards.yaml - bases/grafana.integreatly.org_grafanadatasources.yaml - bases/grafana.integreatly.org_grafanafolders.yaml +- bases/grafana.integreatly.org_grafanaalertrulegroups.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -15,6 +16,7 @@ patchesStrategicMerge: #- patches/webhook_in_grafanadashboards.yaml #- patches/webhook_in_grafanadatasources.yaml #- patches/webhook_in_grafanafolders.yaml +#- patches/webhook_in_grafanaalertrulegroups.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -23,6 +25,7 @@ patchesStrategicMerge: #- patches/cainjection_in_grafanadashboards.yaml #- patches/cainjection_in_grafanadatasources.yaml #- patches/cainjection_in_grafanafolders.yaml +#- patches/cainjection_in_grafanaalertrulegroups.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_grafanaalertrulegroups.yaml b/config/crd/patches/cainjection_in_grafanaalertrulegroups.yaml new file mode 100644 index 000000000..67579ffa1 --- /dev/null +++ b/config/crd/patches/cainjection_in_grafanaalertrulegroups.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: grafanaalertrulegroups.grafana.integreatly.org diff --git a/config/crd/patches/webhook_in_grafanaalertrulegroups.yaml b/config/crd/patches/webhook_in_grafanaalertrulegroups.yaml new file mode 100644 index 000000000..bddb899db --- /dev/null +++ b/config/crd/patches/webhook_in_grafanaalertrulegroups.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: grafanaalertrulegroups.grafana.integreatly.org +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/grafana.integreatly.org_grafanaalertrulegroups.yaml b/config/grafana.integreatly.org_grafanaalertrulegroups.yaml new file mode 100644 index 000000000..fc8537d0a --- /dev/null +++ b/config/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -0,0 +1,266 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.0 + name: grafanaalertrulegroups.grafana.integreatly.org +spec: + group: grafana.integreatly.org + names: + kind: GrafanaAlertRuleGroup + listKind: GrafanaAlertRuleGroupList + plural: grafanaalertrulegroups + singular: grafanaalertrulegroup + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: GrafanaAlertRuleGroup is the Schema for the grafanaalertrulegroups + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: GrafanaAlertRuleGroupSpec defines the desired state of GrafanaAlertRuleGroup + properties: + folderUID: + description: UID of the folder containing this rule group + type: string + instanceSelector: + description: selects Grafanas for import + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + interval: + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + resyncPeriod: + default: 10m + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + rules: + items: + description: AlertRule defines a specific rule to be evaluated. + It is based on the upstream model with some k8s specific type + mappings + properties: + annotations: + additionalProperties: + type: string + type: object + condition: + type: string + data: + items: + properties: + datasourceUid: + description: Grafana data source unique identifier; it + should be '__expr__' for a Server Side Expression operation. + type: string + model: + description: JSON is the raw JSON query and includes the + above properties as well as custom properties. + x-kubernetes-preserve-unknown-fields: true + queryType: + description: QueryType is an optional identifier for the + type of query. It can be used to distinguish different + types of queries. + type: string + refId: + description: RefID is the unique identifier of the query, + set by the frontend call. + type: string + relativeTimeRange: + description: relative time range + properties: + from: + description: from + format: int64 + type: integer + to: + description: to + format: int64 + type: integer + type: object + type: object + type: array + execErrState: + enum: + - OK + - Alerting + - Error + type: string + for: + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + isPaused: + type: boolean + labels: + additionalProperties: + type: string + type: object + noDataState: + enum: + - Alerting + - NoData + - OK + type: string + title: + example: Always firing + maxLength: 190 + minLength: 1 + type: string + uid: + pattern: ^[a-zA-Z0-9-_]+$ + type: string + required: + - condition + - data + - execErrState + - for + - noDataState + - title + - uid + type: object + type: array + required: + - folderUID + - instanceSelector + - interval + - rules + type: object + status: + description: GrafanaAlertRuleGroupStatus defines the observed state of + GrafanaAlertRuleGroup + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/manifests/bases/grafana-operator.clusterserviceversion.yaml b/config/manifests/bases/grafana-operator.clusterserviceversion.yaml index e117e9946..a1e392123 100644 --- a/config/manifests/bases/grafana-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/grafana-operator.clusterserviceversion.yaml @@ -16,6 +16,12 @@ spec: apiservicedefinitions: {} customresourcedefinitions: owned: + - description: GrafanaAlertRuleGroup is the Schema for the grafanaalertrulegroups + API + displayName: Grafana Alert Rule Group + kind: GrafanaAlertRuleGroup + name: grafanaalertrulegroups.grafana.integreatly.org + version: v1beta1 - description: GrafanaDashboard is the Schema for the grafanadashboards API displayName: Grafana Dashboard kind: GrafanaDashboard diff --git a/config/rbac/grafanaalertrulegroup_editor_role.yaml b/config/rbac/grafanaalertrulegroup_editor_role.yaml new file mode 100644 index 000000000..61593a7b0 --- /dev/null +++ b/config/rbac/grafanaalertrulegroup_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit grafanaalertrulegroups. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: grafanaalertrulegroup-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: grafana-operator + app.kubernetes.io/part-of: grafana-operator + app.kubernetes.io/managed-by: kustomize + name: grafanaalertrulegroup-editor-role +rules: +- apiGroups: + - grafana.integreatly.org + resources: + - grafanaalertrulegroups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - grafana.integreatly.org + resources: + - grafanaalertrulegroups/status + verbs: + - get diff --git a/config/rbac/grafanaalertrulegroup_viewer_role.yaml b/config/rbac/grafanaalertrulegroup_viewer_role.yaml new file mode 100644 index 000000000..844c30357 --- /dev/null +++ b/config/rbac/grafanaalertrulegroup_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view grafanaalertrulegroups. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: grafanaalertrulegroup-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: grafana-operator + app.kubernetes.io/part-of: grafana-operator + app.kubernetes.io/managed-by: kustomize + name: grafanaalertrulegroup-viewer-role +rules: +- apiGroups: + - grafana.integreatly.org + resources: + - grafanaalertrulegroups + verbs: + - get + - list + - watch +- apiGroups: + - grafana.integreatly.org + resources: + - grafanaalertrulegroups/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 408c87b1b..c0c4dd603 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -42,6 +42,32 @@ rules: - patch - update - watch +- apiGroups: + - grafana.integreatly.org + resources: + - grafanaalertrulegroups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - grafana.integreatly.org + resources: + - grafanaalertrulegroups/finalizers + verbs: + - update +- apiGroups: + - grafana.integreatly.org + resources: + - grafanaalertrulegroups/status + verbs: + - get + - patch + - update - apiGroups: - grafana.integreatly.org resources: diff --git a/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml b/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml new file mode 100644 index 000000000..070daaa3f --- /dev/null +++ b/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml @@ -0,0 +1,66 @@ +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaAlertRuleGroup +metadata: + labels: + app.kubernetes.io/name: grafanaalertrulegroup + app.kubernetes.io/instance: grafanaalertrulegroup-sample + app.kubernetes.io/part-of: grafana-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: grafana-operator + name: grafanaalertrulegroup-sample +spec: + folderUID: f9b0a98d-2ed3-45a6-9521-18679c74d4f1 + instanceSelector: + dashboards: "grafana" + interval: 5m + rules: + - condition: B + data: + - datasourceUid: grafanacloud-demoinfra-prom + model: + datasource: + type: prometheus + uid: grafanacloud-demoinfra-prom + editorMode: code + expr: weather_temp_c{} + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: A + refId: A + relativeTimeRange: + from: 600 + - datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - 0 + type: gt + operator: + type: and + query: + params: + - C + reducer: + params: [] + type: last + type: query + datasource: + type: __expr__ + uid: __expr__ + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + refId: B + type: threshold + refId: B + relativeTimeRange: + from: 600 + execErrState: Error + for: 5m0s + noDataState: NoData + title: Temperature below freezing + uid: 4843de5c-4f8a-4af0-9509-23526a04faf8 diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 11f287f25..b67312ec7 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -4,4 +4,5 @@ resources: - grafana_v1beta1_grafanadashboard.yaml - grafana_v1beta1_grafanadatasource.yaml - grafana_v1beta1_grafanafolder.yaml +- grafana_v1beta1_grafanaalertrulegroup.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/controllers/client/grafana_client.go b/controllers/client/grafana_client.go index 9dc877b0b..d8f5e948e 100644 --- a/controllers/client/grafana_client.go +++ b/controllers/client/grafana_client.go @@ -10,7 +10,9 @@ import ( "github.com/grafana/grafana-operator/v5/controllers/metrics" v1 "k8s.io/api/core/v1" + httptransport "github.com/go-openapi/runtime/client" grapi "github.com/grafana/grafana-api-golang-client" + genapi "github.com/grafana/grafana-openapi-client-go/client" "github.com/grafana/grafana-operator/v5/api/v1beta1" "github.com/grafana/grafana-operator/v5/controllers/config" "github.com/grafana/grafana-operator/v5/controllers/model" @@ -167,3 +169,49 @@ func NewGrafanaClient(ctx context.Context, c client.Client, grafana *v1beta1.Gra return grafanaClient, nil } + +func NewGeneratedGrafanaClient(ctx context.Context, c client.Client, grafana *v1beta1.Grafana) (*genapi.GrafanaHTTPAPI, error) { + var timeout time.Duration + if grafana.Spec.Client != nil && grafana.Spec.Client.TimeoutSeconds != nil { + timeout = time.Duration(*grafana.Spec.Client.TimeoutSeconds) + if timeout < 0 { + timeout = 0 + } + } else { + timeout = 10 + } + + credentials, err := getAdminCredentials(ctx, c, grafana) + if err != nil { + return nil, err + } + + gURL, err := url.Parse(grafana.Status.AdminUrl) + if err != nil { + return nil, fmt.Errorf("parsing url for client: %w", err) + } + + cfg := &genapi.TransportConfig{ + Schemes: []string{gURL.Scheme}, + BasePath: "/api", + Host: gURL.Host, + // APIKey is an optional API key or service account token. + APIKey: credentials.apikey, + // NumRetries contains the optional number of attempted retries + NumRetries: 0, + } + if credentials.username != "" { + cfg.BasicAuth = url.UserPassword(credentials.username, credentials.password) + } + cl := genapi.NewHTTPClientWithConfig(nil, cfg) + transport := cl.Transport.(*httptransport.Runtime) + wrapped := transport.Transport + transport.Transport = &instrumentedRoundTripper{ + wrapped: wrapped, + metric: metrics.GrafanaApiRequests, + relatedResource: grafana.Name, + } + cl.SetTransport(transport) + + return cl, nil +} diff --git a/controllers/controller_shared.go b/controllers/controller_shared.go index 287ecd8b1..7863eb17d 100644 --- a/controllers/controller_shared.go +++ b/controllers/controller_shared.go @@ -4,14 +4,23 @@ import ( "bytes" "context" "encoding/json" + "time" "github.com/grafana/grafana-operator/v5/api/v1beta1" "github.com/grafana/grafana-operator/v5/controllers/model" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) +const grafanaFinalizer = "operator.grafana.com/finalizer" + +const ( + conditionNoMatchingInstance = "NoMatchingInstance" +) + func GetMatchingInstances(ctx context.Context, k8sClient client.Client, labelSelector *v1.LabelSelector) (v1beta1.GrafanaList, error) { if labelSelector == nil { return v1beta1.GrafanaList{}, nil @@ -54,3 +63,20 @@ func ReconcilePlugins(ctx context.Context, k8sClient client.Client, scheme *runt return nil } + +func setNoMatchingInstance(conditions *[]metav1.Condition, generation int64, reason, message string) { + meta.SetStatusCondition(conditions, metav1.Condition{ + Type: conditionNoMatchingInstance, + Status: "True", + ObservedGeneration: generation, + LastTransitionTime: metav1.Time{ + Time: time.Now(), + }, + Reason: reason, + Message: message, + }) +} + +func removeNoMatchingInstance(conditions *[]metav1.Condition) { + meta.RemoveStatusCondition(conditions, conditionNoMatchingInstance) +} diff --git a/controllers/grafanaalertrulegroup_controller.go b/controllers/grafanaalertrulegroup_controller.go new file mode 100644 index 000000000..d554357a8 --- /dev/null +++ b/controllers/grafanaalertrulegroup_controller.go @@ -0,0 +1,310 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + kuberr "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/go-openapi/strfmt" + "github.com/grafana/grafana-openapi-client-go/client/provisioning" + "github.com/grafana/grafana-openapi-client-go/models" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/go-logr/logr" + grafanav1beta1 "github.com/grafana/grafana-operator/v5/api/v1beta1" + client2 "github.com/grafana/grafana-operator/v5/controllers/client" +) + +// GrafanaAlertRuleGroupReconciler reconciles a GrafanaAlertRuleGroup object +type GrafanaAlertRuleGroupReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanaalertrulegroups,verbs=get;list;watch;create;update;patch;delete + +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanaalertrulegroups/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanaalertrulegroups/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanaalertrulegroups/finalizers,verbs=update + +func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + controllerLog := log.FromContext(ctx).WithName("GrafanaAlertRuleGroupReconciler") + r.Log = log.FromContext(ctx) + + group := &grafanav1beta1.GrafanaAlertRuleGroup{} + err := r.Client.Get(ctx, client.ObjectKey{ + Namespace: req.Namespace, + Name: req.Name, + }, group) + if err != nil { + if kuberr.IsNotFound(err) { + return ctrl.Result{}, nil + } + controllerLog.Error(err, "error getting grafana folder cr") + return ctrl.Result{RequeueAfter: RequeueDelay}, err + } + + if group.GetDeletionTimestamp() != nil { + if controllerutil.ContainsFinalizer(group, grafanaFinalizer) { + // still need to clean up + err := r.finalize(ctx, group) + if err != nil { + return ctrl.Result{RequeueAfter: RequeueDelay}, fmt.Errorf("cleaning up alert rule group: %w", err) + } + controllerutil.RemoveFinalizer(group, grafanaFinalizer) + if err := r.Update(ctx, group); err != nil { + r.Log.Error(err, "failed to remove finalizer") + return ctrl.Result{RequeueAfter: RequeueDelay}, err + } + } + return ctrl.Result{}, nil + } + if !controllerutil.ContainsFinalizer(group, grafanaFinalizer) { + controllerutil.AddFinalizer(group, grafanaFinalizer) + if err := r.Update(ctx, group); err != nil { + r.Log.Error(err, "failed to set finalizer") + return ctrl.Result{RequeueAfter: RequeueDelay}, err + } + } + + defer func() { + if err := r.Client.Status().Update(ctx, group); err != nil { + r.Log.Error(err, "updating status") + } + }() + + instances, err := r.GetMatchingInstances(ctx, group.Spec.InstanceSelector, r.Client) + if err != nil { + setNoMatchingInstance(&group.Status.Conditions, group.Generation, "ErrFetchingInstances", fmt.Sprintf("error occured during fetching of instances: %s", err.Error())) + r.Log.Error(err, "could not find matching instances") + return ctrl.Result{RequeueAfter: RequeueDelay}, err + } + if len(instances.Items) == 0 { + setNoMatchingInstance(&group.Status.Conditions, group.Generation, "EmptyAPIReply", "Instances could not be fetched, reconciliation will be retried") + return ctrl.Result{}, nil + } + + removeNoMatchingInstance(&group.Status.Conditions) + + applyErrors := make(map[string]string) + for _, grafana := range instances.Items { + // can be removed in go 1.22+ + grafana := grafana + if grafana.Status.Stage != grafanav1beta1.OperatorStageComplete || grafana.Status.StageStatus != grafanav1beta1.OperatorStageResultSuccess { + controllerLog.Info("grafana instance not ready", "grafana", grafana.Name) + continue + } + + err := r.reconcileWithInstance(ctx, &grafana, group) + if err != nil { + applyErrors[fmt.Sprintf("%s/%s", grafana.Namespace, grafana.Name)] = err.Error() + } + + } + condition := metav1.Condition{ + Type: "AlertGroupSynchronized", + ObservedGeneration: group.Generation, + LastTransitionTime: metav1.Time{ + Time: time.Now(), + }, + } + + if len(applyErrors) == 0 { + condition.Status = "True" + condition.Reason = "ApplySuccesfull" + condition.Message = fmt.Sprintf("Alert Rule Group was succesfully applied to %d instances", len(instances.Items)) + } else { + condition.Status = "False" + condition.Reason = "ApplyFailed" + + var sb strings.Builder + for i, err := range applyErrors { + sb.WriteString(fmt.Sprintf("\n- %s: %s", i, err)) + } + + condition.Message = fmt.Sprintf("Alert Rule Group failed to be applied for %d out of %d instances. Errors:%s", len(applyErrors), len(instances.Items), sb.String()) + } + meta.SetStatusCondition(&group.Status.Conditions, condition) + + return ctrl.Result{RequeueAfter: group.Spec.ResyncPeriod.Duration}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *GrafanaAlertRuleGroupReconciler) SetupWithManager(mgr ctrl.Manager, ctx context.Context) error { + return ctrl.NewControllerManagedBy(mgr). + For(&grafanav1beta1.GrafanaAlertRuleGroup{}). + Complete(r) +} + +func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Context, instance *grafanav1beta1.Grafana, group *grafanav1beta1.GrafanaAlertRuleGroup) error { + cl, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, instance) + if err != nil { + return fmt.Errorf("building grafana client: %w", err) + } + strue := "true" + + applied, err := cl.Provisioning.GetAlertRuleGroup(group.Name, group.Spec.FolderUID) + var ruleNotFound *provisioning.GetAlertRuleGroupNotFound + if err != nil && !errors.As(err, &ruleNotFound) { + return fmt.Errorf("fetching existing alert rule group: %w", err) + } + + currentRules := make(map[string]bool) + if applied != nil { + for _, rule := range applied.Payload.Rules { + currentRules[rule.UID] = false + } + } + + for _, rule := range group.Spec.Rules { + apiRule := &models.ProvisionedAlertRule{ + Annotations: rule.Annotations, + Condition: &rule.Condition, + Data: make([]*models.AlertQuery, len(rule.Data)), + ExecErrState: &rule.ExecErrState, + FolderUID: &group.Spec.FolderUID, + For: (*strfmt.Duration)(&rule.For.Duration), + IsPaused: rule.IsPaused, + Labels: rule.Labels, + NoDataState: rule.NoDataState, + RuleGroup: &group.Name, + Title: &rule.Title, + UID: rule.UID, + } + for idx, q := range rule.Data { + apiRule.Data[idx] = &models.AlertQuery{ + DatasourceUID: q.DatasourceUID, + Model: q.Model, + QueryType: q.QueryType, + RefID: q.RefID, + RelativeTimeRange: q.RelativeTimeRange, + } + } + + if _, ok := currentRules[rule.UID]; ok { + params := provisioning.NewPutAlertRuleParams(). + WithBody(apiRule). + WithXDisableProvenance(&strue). + WithUID(rule.UID) + _, err = cl.Provisioning.PutAlertRule(params) + if err != nil { + return fmt.Errorf("updating rule: %w", err) + } + } else { + params := provisioning.NewPostAlertRuleParams(). + WithBody(apiRule). + WithXDisableProvenance(&strue) + _, err = cl.Provisioning.PostAlertRule(params) + if err != nil { + return fmt.Errorf("creating rule: %w", err) + } + } + + currentRules[rule.UID] = true + } + + for uid, present := range currentRules { + if !present { + params := provisioning.NewDeleteAlertRuleParams(). + WithUID(uid). + WithXDisableProvenance(&strue) + _, err := cl.Provisioning.DeleteAlertRule(params) + if err != nil { + return fmt.Errorf("deleting old alert rule %s: %w", uid, err) + } + } + } + + mGroup := &models.AlertRuleGroup{ + FolderUID: group.Spec.FolderUID, + Interval: int64(group.Spec.Interval.Seconds()), + Rules: []*models.ProvisionedAlertRule{}, + Title: "", + } + params := provisioning.NewPutAlertRuleGroupParams(). + WithBody(mGroup). + WithGroup(group.Name). + WithFolderUID(group.Spec.FolderUID). + WithXDisableProvenance(&strue) + _, err = cl.Provisioning.PutAlertRuleGroup(params) + if err != nil { + return fmt.Errorf("updating group: %s", err.Error()) + } + return nil +} + +func (r *GrafanaAlertRuleGroupReconciler) finalize(ctx context.Context, group *grafanav1beta1.GrafanaAlertRuleGroup) error { + instances, err := r.GetMatchingInstances(ctx, group.Spec.InstanceSelector, r.Client) + if err != nil { + return fmt.Errorf("fetching instances: %w", err) + } + for _, i := range instances.Items { + instance := i + if err := r.removeFromInstance(ctx, &instance, group); err != nil { + return fmt.Errorf("removing from instance") + } + } + return nil +} + +func (r *GrafanaAlertRuleGroupReconciler) removeFromInstance(ctx context.Context, instance *grafanav1beta1.Grafana, group *grafanav1beta1.GrafanaAlertRuleGroup) error { + cl, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, instance) + if err != nil { + return fmt.Errorf("building grafana client: %w", err) + } + remote, err := cl.Provisioning.GetAlertRuleGroup(group.Name, group.Spec.FolderUID) + if err != nil { + var notFound *provisioning.GetAlertRuleGroupNotFound + if errors.As(err, ¬Found) { + // nothing to do + return nil + } + return fmt.Errorf("fetching alert rule group from instance %s: %w", instance.Status.AdminUrl, err) + } + for _, rule := range remote.Payload.Rules { + params := provisioning.NewDeleteAlertRuleParams().WithUID(rule.UID) + _, err := cl.Provisioning.DeleteAlertRule(params) + if err != nil { + return fmt.Errorf("deleting alert rule %s: %w", rule.UID, err) + } + } + return nil +} + +func (r *GrafanaAlertRuleGroupReconciler) GetMatchingInstances(ctx context.Context, selector *metav1.LabelSelector, k8sClient client.Client) (grafanav1beta1.GrafanaList, error) { + instances, err := GetMatchingInstances(ctx, k8sClient, selector) + if err != nil || len(instances.Items) == 0 { + return grafanav1beta1.GrafanaList{}, err + } + return instances, err +} diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml new file mode 100644 index 000000000..660cc6844 --- /dev/null +++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -0,0 +1,186 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.0 + name: grafanaalertrulegroups.grafana.integreatly.org +spec: + group: grafana.integreatly.org + names: + kind: GrafanaAlertRuleGroup + listKind: GrafanaAlertRuleGroupList + plural: grafanaalertrulegroups + singular: grafanaalertrulegroup + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + folderUID: + type: string + instanceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + interval: + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + resyncPeriod: + default: 10m + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + rules: + items: + properties: + annotations: + additionalProperties: + type: string + type: object + condition: + type: string + data: + items: + properties: + datasourceUid: + type: string + model: + x-kubernetes-preserve-unknown-fields: true + queryType: + type: string + refId: + type: string + relativeTimeRange: + properties: + from: + format: int64 + type: integer + to: + format: int64 + type: integer + type: object + type: object + type: array + execErrState: + enum: + - OK + - Alerting + - Error + type: string + for: + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + isPaused: + type: boolean + labels: + additionalProperties: + type: string + type: object + noDataState: + enum: + - Alerting + - NoData + - OK + type: string + title: + example: Always firing + maxLength: 190 + minLength: 1 + type: string + uid: + pattern: ^[a-zA-Z0-9-_]+$ + type: string + required: + - condition + - data + - execErrState + - for + - noDataState + - title + - uid + type: object + type: array + required: + - folderUID + - instanceSelector + - interval + - rules + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/go.mod b/go.mod index 21b7c8aa4..bfcff9bd0 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,11 @@ require ( github.com/bitly/go-simplejson v0.5.1 github.com/blang/semver v3.5.1+incompatible github.com/go-logr/logr v1.4.1 + github.com/go-openapi/runtime v0.27.1 + github.com/go-openapi/strfmt v0.22.0 github.com/google/go-jsonnet v0.20.0 github.com/grafana/grafana-api-golang-client v0.27.0 + github.com/grafana/grafana-openapi-client-go v0.0.0-20240205122950-d8758043064f github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.31.1 github.com/openshift/api v3.9.0+incompatible @@ -23,8 +26,23 @@ require ( ) require ( + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.22.2 // indirect + github.com/go-openapi/errors v0.21.0 // indirect + github.com/go-openapi/loads v0.21.5 // indirect + github.com/go-openapi/spec v0.20.14 // indirect + github.com/go-openapi/validate v0.23.0 // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + go.mongodb.org/mongo-driver v1.13.1 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/trace v1.19.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/sync v0.5.0 // indirect ) require ( @@ -35,15 +53,15 @@ require ( github.com/evanphx/json-patch/v5 v5.8.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect; indirectn github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/swag v0.22.9 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -73,7 +91,7 @@ require ( gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.29.0 // indirect + k8s.io/apiextensions-apiserver v0.29.0 k8s.io/component-base v0.29.0 // indirect k8s.io/klog/v2 v2.110.1 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect diff --git a/go.sum b/go.sum index e41b20ef5..6a7eaa326 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= @@ -6,7 +8,6 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -20,17 +21,34 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/analysis v0.22.2 h1:ZBmNoP2h5omLKr/srIC9bfqrUGzT6g6gNv03HE9Vpj0= +github.com/go-openapi/analysis v0.22.2/go.mod h1:pDF4UbZsQTo/oNuRfAWWd4dAh4yuYf//LYorPTjrpvo= +github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY= +github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/loads v0.21.5 h1:jDzF4dSoHw6ZFADCGltDb2lE4F6De7aWSpe+IcsRzT0= +github.com/go-openapi/loads v0.21.5/go.mod h1:PxTsnFBoBe+z89riT+wYt3prmSBP6GDAQh2l9H1Flz8= +github.com/go-openapi/runtime v0.27.1 h1:ae53yaOoh+fx/X5Eaq8cRmavHgDma65XPZuvBqvJYto= +github.com/go-openapi/runtime v0.27.1/go.mod h1:fijeJEiEclyS8BRurYE1DE5TLb9/KZl6eAdbzjsrlLU= +github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= +github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= +github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgAcaI= +github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4= +github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= +github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= +github.com/go-openapi/validate v0.23.0 h1:2l7PJLzCis4YUGEoW6eoQw3WhyM65WSIcjX6SQnlfDw= +github.com/go-openapi/validate v0.23.0/go.mod h1:EeiAZ5bmpSIOJV1WLfyYF9qp/B1ZgSaEpHTJHtN5cbE= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= @@ -51,11 +69,13 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -67,10 +87,12 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= +github.com/grafana/grafana-openapi-client-go v0.0.0-20240205122950-d8758043064f h1:3DoHm253biJZehX8F/hVZEEtuhhhLz3OOd/Ehos84bY= +github.com/grafana/grafana-openapi-client-go v0.0.0-20240205122950-d8758043064f/go.mod h1:J+/va7PHxPwcbwvoXlK6ZpocYuolEb0kht3IfALng9s= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -82,25 +104,28 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= @@ -113,6 +138,8 @@ github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= github.com/openshift/api v0.0.0-20190924102528-32369d4db2ad h1:MiZEukiPd7ll8BQDwBfc3LKBxbqyeXIx+wl4CzVj5EQ= github.com/openshift/api v0.0.0-20190924102528-32369d4db2ad/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -125,24 +152,34 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= +go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -152,10 +189,13 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -163,6 +203,9 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= @@ -171,6 +214,9 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -179,14 +225,25 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= @@ -196,6 +253,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -230,7 +288,6 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 11d016f98..7c994a7c1 100644 --- a/main.go +++ b/main.go @@ -187,6 +187,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "GrafanaFolder") os.Exit(1) } + if err = (&controllers.GrafanaAlertRuleGroupReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, ctx); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "GrafanaAlertRuleGroup") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { From ff8aa9ef1655e71b2912f765aa94955611f39f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Fri, 16 Feb 2024 08:21:07 +0100 Subject: [PATCH 02/26] fix: correctly set timeout parameter for generated client --- controllers/client/grafana_client.go | 21 +++++++++++---------- go.mod | 4 ++-- go.sum | 4 ++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/controllers/client/grafana_client.go b/controllers/client/grafana_client.go index d8f5e948e..b001b3f42 100644 --- a/controllers/client/grafana_client.go +++ b/controllers/client/grafana_client.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana-operator/v5/controllers/metrics" v1 "k8s.io/api/core/v1" - httptransport "github.com/go-openapi/runtime/client" grapi "github.com/grafana/grafana-api-golang-client" genapi "github.com/grafana/grafana-openapi-client-go/client" "github.com/grafana/grafana-operator/v5/api/v1beta1" @@ -178,7 +177,7 @@ func NewGeneratedGrafanaClient(ctx context.Context, c client.Client, grafana *v1 timeout = 0 } } else { - timeout = 10 + timeout = time.Second * 10 } credentials, err := getAdminCredentials(ctx, c, grafana) @@ -191,6 +190,15 @@ func NewGeneratedGrafanaClient(ctx context.Context, c client.Client, grafana *v1 return nil, fmt.Errorf("parsing url for client: %w", err) } + client := &http.Client{ + Transport: &instrumentedRoundTripper{ + relatedResource: grafana.Name, + wrapped: http.DefaultTransport.(*http.Transport).Clone(), + metric: metrics.GrafanaApiRequests, + }, + Timeout: timeout, + } + cfg := &genapi.TransportConfig{ Schemes: []string{gURL.Scheme}, BasePath: "/api", @@ -199,19 +207,12 @@ func NewGeneratedGrafanaClient(ctx context.Context, c client.Client, grafana *v1 APIKey: credentials.apikey, // NumRetries contains the optional number of attempted retries NumRetries: 0, + Client: client, } if credentials.username != "" { cfg.BasicAuth = url.UserPassword(credentials.username, credentials.password) } cl := genapi.NewHTTPClientWithConfig(nil, cfg) - transport := cl.Transport.(*httptransport.Runtime) - wrapped := transport.Transport - transport.Transport = &instrumentedRoundTripper{ - wrapped: wrapped, - metric: metrics.GrafanaApiRequests, - relatedResource: grafana.Name, - } - cl.SetTransport(transport) return cl, nil } diff --git a/go.mod b/go.mod index bfcff9bd0..bd1e44e82 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,10 @@ require ( github.com/bitly/go-simplejson v0.5.1 github.com/blang/semver v3.5.1+incompatible github.com/go-logr/logr v1.4.1 - github.com/go-openapi/runtime v0.27.1 github.com/go-openapi/strfmt v0.22.0 github.com/google/go-jsonnet v0.20.0 github.com/grafana/grafana-api-golang-client v0.27.0 - github.com/grafana/grafana-openapi-client-go v0.0.0-20240205122950-d8758043064f + github.com/grafana/grafana-openapi-client-go v0.0.0-20240215164046-eb0e60d27cb7 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.31.1 github.com/openshift/api v3.9.0+incompatible @@ -31,6 +30,7 @@ require ( github.com/go-openapi/analysis v0.22.2 // indirect github.com/go-openapi/errors v0.21.0 // indirect github.com/go-openapi/loads v0.21.5 // indirect + github.com/go-openapi/runtime v0.27.1 // indirect github.com/go-openapi/spec v0.20.14 // indirect github.com/go-openapi/validate v0.23.0 // indirect github.com/google/gnostic-models v0.6.8 // indirect diff --git a/go.sum b/go.sum index 6a7eaa326..10c086f58 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= -github.com/grafana/grafana-openapi-client-go v0.0.0-20240205122950-d8758043064f h1:3DoHm253biJZehX8F/hVZEEtuhhhLz3OOd/Ehos84bY= -github.com/grafana/grafana-openapi-client-go v0.0.0-20240205122950-d8758043064f/go.mod h1:J+/va7PHxPwcbwvoXlK6ZpocYuolEb0kht3IfALng9s= +github.com/grafana/grafana-openapi-client-go v0.0.0-20240215164046-eb0e60d27cb7 h1:3ckIV9HQ+g7ZF0EuFktYNxQP7h0p8ATwxOus0CfINGA= +github.com/grafana/grafana-openapi-client-go v0.0.0-20240215164046-eb0e60d27cb7/go.mod h1:J+/va7PHxPwcbwvoXlK6ZpocYuolEb0kht3IfALng9s= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= From 3e7846ca2b9853e0e29075d258e7a9fcb8c1ca6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Fri, 16 Feb 2024 08:30:10 +0100 Subject: [PATCH 03/26] chore: fix linting issues --- .github/workflows/pr-validation.yaml | 2 +- controllers/grafanaalertrulegroup_controller.go | 11 ++++++----- main.go | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-validation.yaml b/.github/workflows/pr-validation.yaml index ebaab4d3b..932eb655d 100644 --- a/.github/workflows/pr-validation.yaml +++ b/.github/workflows/pr-validation.yaml @@ -97,7 +97,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v4.0.0 with: - version: "v1.55.2" + version: "v1.56.2" test: runs-on: ubuntu-latest diff --git a/controllers/grafanaalertrulegroup_controller.go b/controllers/grafanaalertrulegroup_controller.go index d554357a8..e40fbf8f4 100644 --- a/controllers/grafanaalertrulegroup_controller.go +++ b/controllers/grafanaalertrulegroup_controller.go @@ -105,7 +105,7 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr instances, err := r.GetMatchingInstances(ctx, group.Spec.InstanceSelector, r.Client) if err != nil { - setNoMatchingInstance(&group.Status.Conditions, group.Generation, "ErrFetchingInstances", fmt.Sprintf("error occured during fetching of instances: %s", err.Error())) + setNoMatchingInstance(&group.Status.Conditions, group.Generation, "ErrFetchingInstances", fmt.Sprintf("error occurred during fetching of instances: %s", err.Error())) r.Log.Error(err, "could not find matching instances") return ctrl.Result{RequeueAfter: RequeueDelay}, err } @@ -129,7 +129,6 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr if err != nil { applyErrors[fmt.Sprintf("%s/%s", grafana.Namespace, grafana.Name)] = err.Error() } - } condition := metav1.Condition{ Type: "AlertGroupSynchronized", @@ -142,7 +141,7 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr if len(applyErrors) == 0 { condition.Status = "True" condition.Reason = "ApplySuccesfull" - condition.Message = fmt.Sprintf("Alert Rule Group was succesfully applied to %d instances", len(instances.Items)) + condition.Message = fmt.Sprintf("Alert Rule Group was successfully applied to %d instances", len(instances.Items)) } else { condition.Status = "False" condition.Reason = "ApplyFailed" @@ -160,7 +159,7 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr } // SetupWithManager sets up the controller with the Manager. -func (r *GrafanaAlertRuleGroupReconciler) SetupWithManager(mgr ctrl.Manager, ctx context.Context) error { +func (r *GrafanaAlertRuleGroupReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&grafanav1beta1.GrafanaAlertRuleGroup{}). Complete(r) @@ -187,6 +186,7 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont } for _, rule := range group.Spec.Rules { + rule := rule apiRule := &models.ProvisionedAlertRule{ Annotations: rule.Annotations, Condition: &rule.Condition, @@ -216,7 +216,7 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont WithBody(apiRule). WithXDisableProvenance(&strue). WithUID(rule.UID) - _, err = cl.Provisioning.PutAlertRule(params) + _, err := cl.Provisioning.PutAlertRule(params) if err != nil { return fmt.Errorf("updating rule: %w", err) } @@ -292,6 +292,7 @@ func (r *GrafanaAlertRuleGroupReconciler) removeFromInstance(ctx context.Context return fmt.Errorf("fetching alert rule group from instance %s: %w", instance.Status.AdminUrl, err) } for _, rule := range remote.Payload.Rules { + rule := rule params := provisioning.NewDeleteAlertRuleParams().WithUID(rule.UID) _, err := cl.Provisioning.DeleteAlertRule(params) if err != nil { diff --git a/main.go b/main.go index 7c994a7c1..4f23f02a3 100644 --- a/main.go +++ b/main.go @@ -190,7 +190,7 @@ func main() { if err = (&controllers.GrafanaAlertRuleGroupReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr, ctx); err != nil { + }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "GrafanaAlertRuleGroup") os.Exit(1) } From 9d090930030d92d0c02013efe3c5169d5a87cd11 Mon Sep 17 00:00:00 2001 From: Edvin Norling Date: Sun, 18 Feb 2024 12:06:25 +0100 Subject: [PATCH 04/26] match makefile with golangci-lint --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 549e3823a..5a7c531ca 100644 --- a/Makefile +++ b/Makefile @@ -268,7 +268,7 @@ golangci: ifeq (, $(shell which golangci-lint)) @{ \ set -e ;\ - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 ;\ + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56.2 ;\ } GOLANGCI=$(GOBIN)/golangci-lint else From bef3346315f2ee2b5ff7f319e04f84f07ee027e9 Mon Sep 17 00:00:00 2001 From: Edvin Norling Date: Sun, 18 Feb 2024 12:12:57 +0100 Subject: [PATCH 05/26] ignore lint errors --- controllers/grafanaalertrulegroup_controller.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/controllers/grafanaalertrulegroup_controller.go b/controllers/grafanaalertrulegroup_controller.go index e40fbf8f4..0a8be3c5f 100644 --- a/controllers/grafanaalertrulegroup_controller.go +++ b/controllers/grafanaalertrulegroup_controller.go @@ -216,7 +216,7 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont WithBody(apiRule). WithXDisableProvenance(&strue). WithUID(rule.UID) - _, err := cl.Provisioning.PutAlertRule(params) + _, err := cl.Provisioning.PutAlertRule(params) //nolint:errcheck if err != nil { return fmt.Errorf("updating rule: %w", err) } @@ -224,7 +224,7 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont params := provisioning.NewPostAlertRuleParams(). WithBody(apiRule). WithXDisableProvenance(&strue) - _, err = cl.Provisioning.PostAlertRule(params) + _, err = cl.Provisioning.PostAlertRule(params) //nolint:errcheck if err != nil { return fmt.Errorf("creating rule: %w", err) } @@ -238,7 +238,7 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont params := provisioning.NewDeleteAlertRuleParams(). WithUID(uid). WithXDisableProvenance(&strue) - _, err := cl.Provisioning.DeleteAlertRule(params) + _, err := cl.Provisioning.DeleteAlertRule(params) //nolint:errcheck if err != nil { return fmt.Errorf("deleting old alert rule %s: %w", uid, err) } @@ -256,7 +256,7 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont WithGroup(group.Name). WithFolderUID(group.Spec.FolderUID). WithXDisableProvenance(&strue) - _, err = cl.Provisioning.PutAlertRuleGroup(params) + _, err = cl.Provisioning.PutAlertRuleGroup(params) //nolint:errcheck if err != nil { return fmt.Errorf("updating group: %s", err.Error()) } @@ -294,7 +294,7 @@ func (r *GrafanaAlertRuleGroupReconciler) removeFromInstance(ctx context.Context for _, rule := range remote.Payload.Rules { rule := rule params := provisioning.NewDeleteAlertRuleParams().WithUID(rule.UID) - _, err := cl.Provisioning.DeleteAlertRule(params) + _, err := cl.Provisioning.DeleteAlertRule(params) //nolint:errcheck if err != nil { return fmt.Errorf("deleting alert rule %s: %w", rule.UID, err) } From d668c98c3af3664e90ac1d057bf2017daf1c24b8 Mon Sep 17 00:00:00 2001 From: Edvin Norling Date: Sun, 18 Feb 2024 12:25:55 +0100 Subject: [PATCH 06/26] gofumpt --- Makefile | 2 +- controllers/fetchers/jsonnet_fetcher.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 5a7c531ca..46e7729e7 100644 --- a/Makefile +++ b/Makefile @@ -283,7 +283,7 @@ gofumpt: ifeq (, $(shell which gofumpt)) @{ \ set -e ;\ - go install mvdan.cc/gofumpt@v0.4.0 ;\ + go install mvdan.cc/gofumpt@v0.6.0 ;\ } GOFUMPT=$(GOBIN)/gofumpt else diff --git a/controllers/fetchers/jsonnet_fetcher.go b/controllers/fetchers/jsonnet_fetcher.go index 7f3d569a7..76d18d5d0 100644 --- a/controllers/fetchers/jsonnet_fetcher.go +++ b/controllers/fetchers/jsonnet_fetcher.go @@ -247,7 +247,6 @@ func postJsonnetProjectBuild(buildName string) error { fmt.Println("Listing dashboards dir after deletion") err = listDirectoryContents(config.GrafanaDashboardsRuntimeBuild) - if err != nil { fmt.Println("Error listing directory contents:", err) return err From 82fe550ea0eb3f0563a46fb4ca46a87e4d859f2e Mon Sep 17 00:00:00 2001 From: Edvin Norling Date: Mon, 19 Feb 2024 16:05:55 +0100 Subject: [PATCH 07/26] add correct matchLabel example --- config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml b/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml index 070daaa3f..1f2c3885d 100644 --- a/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml +++ b/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml @@ -11,7 +11,8 @@ metadata: spec: folderUID: f9b0a98d-2ed3-45a6-9521-18679c74d4f1 instanceSelector: - dashboards: "grafana" + matchLabels: + dashboards: "grafana" interval: 5m rules: - condition: B From 5a106ffaf572b705ee153f41920710dd413cdfa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Tue, 20 Feb 2024 15:02:15 +0100 Subject: [PATCH 08/26] fix: ignore status updates on alertrulegroups this alleviates some race conditions when setting finalizers & status concurrently --- controllers/controller_shared.go | 11 +++++++++++ controllers/grafanaalertrulegroup_controller.go | 1 + 2 files changed, 12 insertions(+) diff --git a/controllers/controller_shared.go b/controllers/controller_shared.go index 7863eb17d..7464229a1 100644 --- a/controllers/controller_shared.go +++ b/controllers/controller_shared.go @@ -13,6 +13,8 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" ) const grafanaFinalizer = "operator.grafana.com/finalizer" @@ -80,3 +82,12 @@ func setNoMatchingInstance(conditions *[]metav1.Condition, generation int64, rea func removeNoMatchingInstance(conditions *[]metav1.Condition) { meta.RemoveStatusCondition(conditions, conditionNoMatchingInstance) } + +func ignoreStatusUpdates() predicate.Predicate { + return predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + // Ignore updates to CR status in which case metadata.Generation does not change + return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() + }, + } +} diff --git a/controllers/grafanaalertrulegroup_controller.go b/controllers/grafanaalertrulegroup_controller.go index 0a8be3c5f..4d50c7aaa 100644 --- a/controllers/grafanaalertrulegroup_controller.go +++ b/controllers/grafanaalertrulegroup_controller.go @@ -162,6 +162,7 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr func (r *GrafanaAlertRuleGroupReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&grafanav1beta1.GrafanaAlertRuleGroup{}). + WithEventFilter(ignoreStatusUpdates()). Complete(r) } From e3e12d7581954838b14da8493259bd8a48888823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Wed, 21 Feb 2024 14:53:11 +0100 Subject: [PATCH 09/26] feat: add folderSelector to grafanaalertrulegroup --- api/v1beta1/grafanaalertrulegroup_types.go | 6 +- api/v1beta1/zz_generated.deepcopy.go | 5 ++ ...ntegreatly.org_grafanaalertrulegroups.yaml | 26 ++++++++- ...ntegreatly.org_grafanaalertrulegroups.yaml | 50 ++++++++++++++++- controllers/client/round_tripper.go | 9 ++- controllers/controller_shared.go | 14 +++++ .../grafanaalertrulegroup_controller.go | 56 ++++++++++++++++--- ...ntegreatly.org_grafanaalertrulegroups.yaml | 26 ++++++++- 8 files changed, 177 insertions(+), 15 deletions(-) diff --git a/api/v1beta1/grafanaalertrulegroup_types.go b/api/v1beta1/grafanaalertrulegroup_types.go index 651b2e662..213f7b3df 100644 --- a/api/v1beta1/grafanaalertrulegroup_types.go +++ b/api/v1beta1/grafanaalertrulegroup_types.go @@ -35,7 +35,11 @@ type GrafanaAlertRuleGroupSpec struct { InstanceSelector *metav1.LabelSelector `json:"instanceSelector"` // UID of the folder containing this rule group - FolderUID string `json:"folderUID"` + // Overrides the FolderSelector + FolderUID string `json:"folderUID,omitempty"` + + // Match GrafanaFolders CRs to infer the uid + FolderSelector *metav1.LabelSelector `json:"folderSelector"` Rules []AlertRule `json:"rules"` diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 9c4b47c62..745c14277 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -474,6 +474,11 @@ func (in *GrafanaAlertRuleGroupSpec) DeepCopyInto(out *GrafanaAlertRuleGroupSpec *out = new(metav1.LabelSelector) (*in).DeepCopyInto(*out) } + if in.FolderSelector != nil { + in, out := &in.FolderSelector, &out.FolderSelector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } if in.Rules != nil { in, out := &in.Rules, &out.Rules *out = make([]AlertRule, len(*in)) diff --git a/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml index 660cc6844..e30071fde 100644 --- a/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -26,6 +26,30 @@ spec: type: object spec: properties: + folderSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic folderUID: type: string instanceSelector: @@ -133,7 +157,7 @@ spec: type: object type: array required: - - folderUID + - folderSelector - instanceSelector - interval - rules diff --git a/config/grafana.integreatly.org_grafanaalertrulegroups.yaml b/config/grafana.integreatly.org_grafanaalertrulegroups.yaml index fc8537d0a..5fbbe11de 100644 --- a/config/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/config/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -35,8 +35,54 @@ spec: spec: description: GrafanaAlertRuleGroupSpec defines the desired state of GrafanaAlertRuleGroup properties: + folderSelector: + description: Match GrafanaFolders CRs to infer the uid + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic folderUID: - description: UID of the folder containing this rule group + description: UID of the folder containing this rule group Overrides + the FolderSelector type: string instanceSelector: description: selects Grafanas for import @@ -179,7 +225,7 @@ spec: type: object type: array required: - - folderUID + - folderSelector - instanceSelector - interval - rules diff --git a/controllers/client/round_tripper.go b/controllers/client/round_tripper.go index 95fe6be4b..72f68315c 100644 --- a/controllers/client/round_tripper.go +++ b/controllers/client/round_tripper.go @@ -1,6 +1,7 @@ package client import ( + "crypto/tls" "net/http" "strconv" @@ -18,7 +19,13 @@ func NewInstrumentedRoundTripper(relatedResource string, metric *prometheus.Coun transport.DisableKeepAlives = true transport.MaxIdleConnsPerHost = -1 - transport.TLSClientConfig.InsecureSkipVerify = true //nolint + if transport.TLSClientConfig != nil { + transport.TLSClientConfig.InsecureSkipVerify = true //nolint + } else { + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } if !useProxy { transport.Proxy = nil diff --git a/controllers/controller_shared.go b/controllers/controller_shared.go index 7464229a1..bd32f9033 100644 --- a/controllers/controller_shared.go +++ b/controllers/controller_shared.go @@ -21,6 +21,7 @@ const grafanaFinalizer = "operator.grafana.com/finalizer" const ( conditionNoMatchingInstance = "NoMatchingInstance" + conditionNoMatchingFolder = "NoMatchingFolder" ) func GetMatchingInstances(ctx context.Context, k8sClient client.Client, labelSelector *v1.LabelSelector) (v1beta1.GrafanaList, error) { @@ -79,6 +80,19 @@ func setNoMatchingInstance(conditions *[]metav1.Condition, generation int64, rea }) } +func setNoMatchingFolder(conditions *[]metav1.Condition, generation int64, reason, message string) { + meta.SetStatusCondition(conditions, metav1.Condition{ + Type: conditionNoMatchingFolder, + Status: "True", + ObservedGeneration: generation, + LastTransitionTime: metav1.Time{ + Time: time.Now(), + }, + Reason: reason, + Message: message, + }) +} + func removeNoMatchingInstance(conditions *[]metav1.Condition) { meta.RemoveStatusCondition(conditions, conditionNoMatchingInstance) } diff --git a/controllers/grafanaalertrulegroup_controller.go b/controllers/grafanaalertrulegroup_controller.go index 4d50c7aaa..92e96fe90 100644 --- a/controllers/grafanaalertrulegroup_controller.go +++ b/controllers/grafanaalertrulegroup_controller.go @@ -115,6 +115,11 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr } removeNoMatchingInstance(&group.Status.Conditions) + folderUID := r.GetFolderUID(ctx, group) + if folderUID == "" { + // error is already set in conditions + return ctrl.Result{}, nil + } applyErrors := make(map[string]string) for _, grafana := range instances.Items { @@ -125,7 +130,7 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr continue } - err := r.reconcileWithInstance(ctx, &grafana, group) + err := r.reconcileWithInstance(ctx, &grafana, group, folderUID) if err != nil { applyErrors[fmt.Sprintf("%s/%s", grafana.Namespace, grafana.Name)] = err.Error() } @@ -166,14 +171,14 @@ func (r *GrafanaAlertRuleGroupReconciler) SetupWithManager(mgr ctrl.Manager) err Complete(r) } -func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Context, instance *grafanav1beta1.Grafana, group *grafanav1beta1.GrafanaAlertRuleGroup) error { +func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Context, instance *grafanav1beta1.Grafana, group *grafanav1beta1.GrafanaAlertRuleGroup, folderUID string) error { cl, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, instance) if err != nil { return fmt.Errorf("building grafana client: %w", err) } strue := "true" - applied, err := cl.Provisioning.GetAlertRuleGroup(group.Name, group.Spec.FolderUID) + applied, err := cl.Provisioning.GetAlertRuleGroup(group.Name, folderUID) var ruleNotFound *provisioning.GetAlertRuleGroupNotFound if err != nil && !errors.As(err, &ruleNotFound) { return fmt.Errorf("fetching existing alert rule group: %w", err) @@ -193,7 +198,7 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont Condition: &rule.Condition, Data: make([]*models.AlertQuery, len(rule.Data)), ExecErrState: &rule.ExecErrState, - FolderUID: &group.Spec.FolderUID, + FolderUID: &folderUID, For: (*strfmt.Duration)(&rule.For.Duration), IsPaused: rule.IsPaused, Labels: rule.Labels, @@ -247,7 +252,7 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont } mGroup := &models.AlertRuleGroup{ - FolderUID: group.Spec.FolderUID, + FolderUID: folderUID, Interval: int64(group.Spec.Interval.Seconds()), Rules: []*models.ProvisionedAlertRule{}, Title: "", @@ -255,7 +260,7 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont params := provisioning.NewPutAlertRuleGroupParams(). WithBody(mGroup). WithGroup(group.Name). - WithFolderUID(group.Spec.FolderUID). + WithFolderUID(folderUID). WithXDisableProvenance(&strue) _, err = cl.Provisioning.PutAlertRuleGroup(params) //nolint:errcheck if err != nil { @@ -265,25 +270,29 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont } func (r *GrafanaAlertRuleGroupReconciler) finalize(ctx context.Context, group *grafanav1beta1.GrafanaAlertRuleGroup) error { + folderUID := r.GetFolderUID(ctx, group) + if folderUID == "" { + return fmt.Errorf("failed to fetch folder uid") + } instances, err := r.GetMatchingInstances(ctx, group.Spec.InstanceSelector, r.Client) if err != nil { return fmt.Errorf("fetching instances: %w", err) } for _, i := range instances.Items { instance := i - if err := r.removeFromInstance(ctx, &instance, group); err != nil { + if err := r.removeFromInstance(ctx, &instance, group, folderUID); err != nil { return fmt.Errorf("removing from instance") } } return nil } -func (r *GrafanaAlertRuleGroupReconciler) removeFromInstance(ctx context.Context, instance *grafanav1beta1.Grafana, group *grafanav1beta1.GrafanaAlertRuleGroup) error { +func (r *GrafanaAlertRuleGroupReconciler) removeFromInstance(ctx context.Context, instance *grafanav1beta1.Grafana, group *grafanav1beta1.GrafanaAlertRuleGroup, folderUID string) error { cl, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, instance) if err != nil { return fmt.Errorf("building grafana client: %w", err) } - remote, err := cl.Provisioning.GetAlertRuleGroup(group.Name, group.Spec.FolderUID) + remote, err := cl.Provisioning.GetAlertRuleGroup(group.Name, folderUID) if err != nil { var notFound *provisioning.GetAlertRuleGroupNotFound if errors.As(err, ¬Found) { @@ -310,3 +319,32 @@ func (r *GrafanaAlertRuleGroupReconciler) GetMatchingInstances(ctx context.Conte } return instances, err } + +func (r *GrafanaAlertRuleGroupReconciler) GetMatchingFolders(ctx context.Context, selector *metav1.LabelSelector) (grafanav1beta1.GrafanaFolderList, error) { + var list grafanav1beta1.GrafanaFolderList + opts := []client.ListOption{ + client.MatchingLabels(selector.MatchLabels), + } + err := r.Client.List(ctx, &list, opts...) + return list, err +} + +func (r *GrafanaAlertRuleGroupReconciler) GetFolderUID(ctx context.Context, group *grafanav1beta1.GrafanaAlertRuleGroup) string { + if group.Spec.FolderUID != "" { + return group.Spec.FolderUID + } + folders, err := r.GetMatchingFolders(ctx, group.Spec.FolderSelector) + if err != nil { + setNoMatchingFolder(&group.Status.Conditions, group.Generation, "ErrFetchingFolders", fmt.Sprintf("Failed to fetch folders: %s", err.Error())) + return "" + } + if len(folders.Items) < 1 { + setNoMatchingFolder(&group.Status.Conditions, group.Generation, "NoMatches", "Folder selector did not match any folders") + return "" + } + if len(folders.Items) > 1 { + setNoMatchingFolder(&group.Status.Conditions, group.Generation, "MultipleMatches", fmt.Sprintf("Folder selector matched %d folders. Only one folder can be matched", len(folders.Items))) + return "" + } + return string(folders.Items[0].UID) +} diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml index 660cc6844..e30071fde 100644 --- a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -26,6 +26,30 @@ spec: type: object spec: properties: + folderSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic folderUID: type: string instanceSelector: @@ -133,7 +157,7 @@ spec: type: object type: array required: - - folderUID + - folderSelector - instanceSelector - interval - rules From b44d7c77dffdbd90098200494fd6b954b49fcae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Wed, 21 Feb 2024 15:00:27 +0100 Subject: [PATCH 10/26] fix: ignore finalization when folder no longer exists --- controllers/grafanaalertrulegroup_controller.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controllers/grafanaalertrulegroup_controller.go b/controllers/grafanaalertrulegroup_controller.go index 92e96fe90..3e00855bb 100644 --- a/controllers/grafanaalertrulegroup_controller.go +++ b/controllers/grafanaalertrulegroup_controller.go @@ -272,7 +272,8 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont func (r *GrafanaAlertRuleGroupReconciler) finalize(ctx context.Context, group *grafanav1beta1.GrafanaAlertRuleGroup) error { folderUID := r.GetFolderUID(ctx, group) if folderUID == "" { - return fmt.Errorf("failed to fetch folder uid") + r.Log.Info("ignoring finalization logic as folder no longer exists") + return nil } instances, err := r.GetMatchingInstances(ctx, group.Spec.InstanceSelector, r.Client) if err != nil { From ab8f4ff0cecb846627f38c69163bae1d93275ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Wed, 21 Feb 2024 16:27:44 +0100 Subject: [PATCH 11/26] test: add e2e test for alertrulegroups --- .../e2e/example-test/08-alert-rule-group.yaml | 74 +++++++++++++++++++ tests/e2e/example-test/08-assert.yaml | 8 ++ tests/e2e/example-test/chainsaw-test.yaml | 6 ++ 3 files changed, 88 insertions(+) create mode 100644 tests/e2e/example-test/08-alert-rule-group.yaml create mode 100644 tests/e2e/example-test/08-assert.yaml diff --git a/tests/e2e/example-test/08-alert-rule-group.yaml b/tests/e2e/example-test/08-alert-rule-group.yaml new file mode 100644 index 000000000..8507950a7 --- /dev/null +++ b/tests/e2e/example-test/08-alert-rule-group.yaml @@ -0,0 +1,74 @@ +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaFolder +metadata: + name: test-folder-from-operator + labels: + folder: "test-folder" +spec: + instanceSelector: + matchLabels: + dashboards: "grafana" +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaAlertRuleGroup +metadata: + name: test +spec: + folderSelector: + matchLabels: + folder: "test-folder" + instanceSelector: + matchLabels: + dashboards: "grafana" + interval: "5m" + rules: + - condition: B + data: + - datasourceUid: grafanacloud-demoinfra-prom + model: + datasource: + type: prometheus + uid: grafanacloud-demoinfra-prom + editorMode: code + expr: weather_temp_c{location="Toronto"} + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: A + refId: A + relativeTimeRange: + from: 600 + - datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - 10 + type: gt + operator: + type: and + query: + params: + - C + reducer: + params: [] + type: last + type: query + datasource: + type: __expr__ + uid: __expr__ + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + refId: B + type: threshold + refId: B + relativeTimeRange: + from: 600 + execErrState: Error + for: 5m0s + noDataState: NoData + title: test rule from operator updated + uid: 4843de5c-4f8a-4af0-9509-23526a04faf8 diff --git a/tests/e2e/example-test/08-assert.yaml b/tests/e2e/example-test/08-assert.yaml new file mode 100644 index 000000000..4920430b0 --- /dev/null +++ b/tests/e2e/example-test/08-assert.yaml @@ -0,0 +1,8 @@ +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaAlertRuleGroup +metadata: + name: test +status: + conditions: + - type: AlertGroupSynchronized + status: "True" diff --git a/tests/e2e/example-test/chainsaw-test.yaml b/tests/e2e/example-test/chainsaw-test.yaml index a3d537fc1..f15f819c7 100755 --- a/tests/e2e/example-test/chainsaw-test.yaml +++ b/tests/e2e/example-test/chainsaw-test.yaml @@ -53,3 +53,9 @@ spec: file: 07-jsonnet-project.yaml - assert: file: 07-assert.yaml + - name: step-08 + try: + - apply: + file: 08-alert-rule-group.yaml + - assert: + file: 08-assert.yaml From bc64dc106305048222db13f441306dd69f33abc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Wed, 21 Feb 2024 16:29:13 +0100 Subject: [PATCH 12/26] fix: add linting exception for insecure tls --- controllers/client/round_tripper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/client/round_tripper.go b/controllers/client/round_tripper.go index 72f68315c..4fda0b61b 100644 --- a/controllers/client/round_tripper.go +++ b/controllers/client/round_tripper.go @@ -23,7 +23,7 @@ func NewInstrumentedRoundTripper(relatedResource string, metric *prometheus.Coun transport.TLSClientConfig.InsecureSkipVerify = true //nolint } else { transport.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: true, //nolint } } From 3fc7edcdf983c378ad86c83d05d4b54ad8dd1e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Wed, 21 Feb 2024 17:03:41 +0100 Subject: [PATCH 13/26] fix: use constructor for instrumented roundtripper --- controllers/client/grafana_client.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/controllers/client/grafana_client.go b/controllers/client/grafana_client.go index b001b3f42..d51254739 100644 --- a/controllers/client/grafana_client.go +++ b/controllers/client/grafana_client.go @@ -190,13 +190,11 @@ func NewGeneratedGrafanaClient(ctx context.Context, c client.Client, grafana *v1 return nil, fmt.Errorf("parsing url for client: %w", err) } + transport := NewInstrumentedRoundTripper(grafana.Name, metrics.GrafanaApiRequests, grafana.IsExternal()) + client := &http.Client{ - Transport: &instrumentedRoundTripper{ - relatedResource: grafana.Name, - wrapped: http.DefaultTransport.(*http.Transport).Clone(), - metric: metrics.GrafanaApiRequests, - }, - Timeout: timeout, + Transport: transport, + Timeout: timeout, } cfg := &genapi.TransportConfig{ From 4076eb88136ceac68394ffd5d9905ad7c8650b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Wed, 21 Feb 2024 17:03:55 +0100 Subject: [PATCH 14/26] fix: check if folder exists before creation this prevents invisible orphaned alert rules (and helps debugging) --- controllers/grafanaalertrulegroup_controller.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/controllers/grafanaalertrulegroup_controller.go b/controllers/grafanaalertrulegroup_controller.go index 3e00855bb..288a2f0ea 100644 --- a/controllers/grafanaalertrulegroup_controller.go +++ b/controllers/grafanaalertrulegroup_controller.go @@ -30,6 +30,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "github.com/go-openapi/strfmt" + "github.com/grafana/grafana-openapi-client-go/client/folders" "github.com/grafana/grafana-openapi-client-go/client/provisioning" "github.com/grafana/grafana-openapi-client-go/models" "sigs.k8s.io/controller-runtime/pkg/client" @@ -178,6 +179,15 @@ func (r *GrafanaAlertRuleGroupReconciler) reconcileWithInstance(ctx context.Cont } strue := "true" + _, err = cl.Folders.GetFolderByUID(folderUID) //nolint:errcheck + if err != nil { + var folderNotFound *folders.GetFolderByUIDNotFound + if errors.As(err, &folderNotFound) { + return fmt.Errorf("folder with uid %s not found", folderUID) + } + return fmt.Errorf("fetching folder: %w", err) + } + applied, err := cl.Provisioning.GetAlertRuleGroup(group.Name, folderUID) var ruleNotFound *provisioning.GetAlertRuleGroupNotFound if err != nil && !errors.As(err, &ruleNotFound) { From 4bcee251d99d7f5291a628f09bd8747a73ef8285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Fri, 23 Feb 2024 14:02:46 +0100 Subject: [PATCH 15/26] feat: use validation rules to enforce exclusivity --- api/v1beta1/grafanaalertrulegroup_types.go | 3 ++- .../grafana.integreatly.org_grafanaalertrulegroups.yaml | 5 ++++- config/grafana.integreatly.org_grafanaalertrulegroups.yaml | 5 ++++- .../crds/grafana.integreatly.org_grafanaalertrulegroups.yaml | 5 ++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/api/v1beta1/grafanaalertrulegroup_types.go b/api/v1beta1/grafanaalertrulegroup_types.go index 213f7b3df..ea8a6dc90 100644 --- a/api/v1beta1/grafanaalertrulegroup_types.go +++ b/api/v1beta1/grafanaalertrulegroup_types.go @@ -23,6 +23,7 @@ import ( ) // GrafanaAlertRuleGroupSpec defines the desired state of GrafanaAlertRuleGroup +// +kubebuilder:validation:XValidation:rule="(has(self.folderUID) && !(has(self.folderSelector))) || (has(self.folderSelector) && !(has(self.folderUID)))", message="Only one of FolderUID or FolderSelector can be set" type GrafanaAlertRuleGroupSpec struct { // +optional // +kubebuilder:validation:Type=string @@ -39,7 +40,7 @@ type GrafanaAlertRuleGroupSpec struct { FolderUID string `json:"folderUID,omitempty"` // Match GrafanaFolders CRs to infer the uid - FolderSelector *metav1.LabelSelector `json:"folderSelector"` + FolderSelector *metav1.LabelSelector `json:"folderSelector,omitempty"` Rules []AlertRule `json:"rules"` diff --git a/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml index e30071fde..f7a02531a 100644 --- a/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -157,11 +157,14 @@ spec: type: object type: array required: - - folderSelector - instanceSelector - interval - rules type: object + x-kubernetes-validations: + - message: Only one of FolderUID or FolderSelector can be set + rule: (has(self.folderUID) && !(has(self.folderSelector))) || (has(self.folderSelector) + && !(has(self.folderUID))) status: properties: conditions: diff --git a/config/grafana.integreatly.org_grafanaalertrulegroups.yaml b/config/grafana.integreatly.org_grafanaalertrulegroups.yaml index 5fbbe11de..ec0bc68f4 100644 --- a/config/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/config/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -225,11 +225,14 @@ spec: type: object type: array required: - - folderSelector - instanceSelector - interval - rules type: object + x-kubernetes-validations: + - message: Only one of FolderUID or FolderSelector can be set + rule: (has(self.folderUID) && !(has(self.folderSelector))) || (has(self.folderSelector) + && !(has(self.folderUID))) status: description: GrafanaAlertRuleGroupStatus defines the observed state of GrafanaAlertRuleGroup diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml index e30071fde..f7a02531a 100644 --- a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -157,11 +157,14 @@ spec: type: object type: array required: - - folderSelector - instanceSelector - interval - rules type: object + x-kubernetes-validations: + - message: Only one of FolderUID or FolderSelector can be set + rule: (has(self.folderUID) && !(has(self.folderSelector))) || (has(self.folderSelector) + && !(has(self.folderUID))) status: properties: conditions: From e5aef4ba64591cddceb48a076d0b59cde6db57f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Tue, 27 Feb 2024 09:26:32 +0100 Subject: [PATCH 16/26] docs: add docs for alert rule groups --- examples/alertrulegroups/README.md | 11 +++ examples/alertrulegroups/resources.yaml | 91 +++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 examples/alertrulegroups/README.md create mode 100644 examples/alertrulegroups/resources.yaml diff --git a/examples/alertrulegroups/README.md b/examples/alertrulegroups/README.md new file mode 100644 index 000000000..93d0efc2f --- /dev/null +++ b/examples/alertrulegroups/README.md @@ -0,0 +1,11 @@ +--- +title: "Alert rule groups" +linkTitle: "Alert rule groups" +--- + +Shows how to create an alert rule group, contained in a folder. + +The easiest way to build alerts is using the Grafana UI. +After creating the rule, use the [Modify Export](https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/export-alerting-resources/#modify-and-export-alert-rules-without-saving-changes) feature to get the correct YAML from your resource. + +{{< readfile file="resources.yaml" code="true" lang="yaml" >}} diff --git a/examples/alertrulegroups/resources.yaml b/examples/alertrulegroups/resources.yaml new file mode 100644 index 000000000..3de54a6c1 --- /dev/null +++ b/examples/alertrulegroups/resources.yaml @@ -0,0 +1,91 @@ +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: Grafana +metadata: + name: grafana + labels: + dashboards: "grafana" +spec: + config: + log: + mode: "console" + auth: + disable_login_form: "false" + security: + admin_user: root + admin_password: secret +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaFolder +metadata: + name: test-folder + labels: + folder: test-folder +spec: + instanceSelector: + matchLabels: + dashboards: "grafana" +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaAlertRuleGroup +metadata: + name: grafanaalertrulegroup-sample +spec: + folderSelector: + matchLabels: + folder: test-folder + instanceSelector: + matchLabels: + dashboards: "grafana" + interval: 5m + rules: + - condition: B + data: + - datasourceUid: grafanacloud-demoinfra-prom + model: + datasource: + type: prometheus + uid: grafanacloud-demoinfra-prom + editorMode: code + expr: weather_temp_c{} + instant: true + intervalMs: 1000 + legendFormat: __auto + maxDataPoints: 43200 + range: false + refId: A + refId: A + relativeTimeRange: + from: 600 + - datasourceUid: __expr__ + model: + conditions: + - evaluator: + params: + - 0 + type: lt + operator: + type: and + query: + params: + - C + reducer: + params: [] + type: last + type: query + datasource: + type: __expr__ + uid: __expr__ + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + refId: B + type: threshold + refId: B + relativeTimeRange: + from: 600 + execErrState: Error + for: 5m0s + noDataState: NoData + title: Temperature below zero + uid: 4843de5c-4f8a-4af0-9509-23526a04faf8 From 991fede459788706bbd51a7b011861121a11beb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Tue, 27 Feb 2024 09:33:05 +0100 Subject: [PATCH 17/26] docs: add section on finalizers --- docs/docs/_index.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/docs/_index.md b/docs/docs/_index.md index f210f8518..7c3180483 100644 --- a/docs/docs/_index.md +++ b/docs/docs/_index.md @@ -89,3 +89,16 @@ This is because especially the data sources contain secret information and we do The Operator can use a proxy server when fetching URL-based / Grafana.com dashboards or making requests to external Grafana instances. The proxy settings can be controlled through environment variables as documented [here](https://pkg.go.dev/golang.org/x/net/http/httpproxy#FromEnvironment). + +## Deleting resources with a finalizer + +The operator marks some resources with [finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/) to clean up resources on deletion. +If the operator is not running, marked resources are unable to be deleted. This is intended. +However, this behavior can cause issues if, for example, you uninstalled the operator and now can't remove resources. +To manually remove the finalizer, use the following command: + +```bash +kubectl patch GrafanaAlertRuleGroup -p '{"metadata":{"finalizers":null}}' --type=merge +``` + +After running this, the resource can be deleted as usual. From 87441192486b596ed1c7096efa5d1b43b39d46c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Wed, 28 Feb 2024 14:35:43 +0100 Subject: [PATCH 18/26] refactor: use folderRef instead of folderSelector Matching multiple folders does not make sense as alert rules can only be stored in one folder --- api/v1beta1/grafanaalertrulegroup_types.go | 4 +- api/v1beta1/zz_generated.deepcopy.go | 5 -- ...ntegreatly.org_grafanaalertrulegroups.yaml | 30 ++--------- ...ntegreatly.org_grafanaalertrulegroups.yaml | 50 ++----------------- ...grafana_v1beta1_grafanaalertrulegroup.yaml | 3 +- .../grafanaalertrulegroup_controller.go | 22 ++++---- ...ntegreatly.org_grafanaalertrulegroups.yaml | 30 ++--------- .../e2e/example-test/08-alert-rule-group.yaml | 4 +- 8 files changed, 28 insertions(+), 120 deletions(-) diff --git a/api/v1beta1/grafanaalertrulegroup_types.go b/api/v1beta1/grafanaalertrulegroup_types.go index ea8a6dc90..287362d3c 100644 --- a/api/v1beta1/grafanaalertrulegroup_types.go +++ b/api/v1beta1/grafanaalertrulegroup_types.go @@ -23,7 +23,7 @@ import ( ) // GrafanaAlertRuleGroupSpec defines the desired state of GrafanaAlertRuleGroup -// +kubebuilder:validation:XValidation:rule="(has(self.folderUID) && !(has(self.folderSelector))) || (has(self.folderSelector) && !(has(self.folderUID)))", message="Only one of FolderUID or FolderSelector can be set" +// +kubebuilder:validation:XValidation:rule="(has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) && !(has(self.folderUID)))", message="Only one of FolderUID or FolderRef can be set" type GrafanaAlertRuleGroupSpec struct { // +optional // +kubebuilder:validation:Type=string @@ -40,7 +40,7 @@ type GrafanaAlertRuleGroupSpec struct { FolderUID string `json:"folderUID,omitempty"` // Match GrafanaFolders CRs to infer the uid - FolderSelector *metav1.LabelSelector `json:"folderSelector,omitempty"` + FolderRef string `json:"folderRef,omitempty"` Rules []AlertRule `json:"rules"` diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 745c14277..9c4b47c62 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -474,11 +474,6 @@ func (in *GrafanaAlertRuleGroupSpec) DeepCopyInto(out *GrafanaAlertRuleGroupSpec *out = new(metav1.LabelSelector) (*in).DeepCopyInto(*out) } - if in.FolderSelector != nil { - in, out := &in.FolderSelector, &out.FolderSelector - *out = new(metav1.LabelSelector) - (*in).DeepCopyInto(*out) - } if in.Rules != nil { in, out := &in.Rules, &out.Rules *out = make([]AlertRule, len(*in)) diff --git a/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml index f7a02531a..f64aaa4d0 100644 --- a/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -26,30 +26,8 @@ spec: type: object spec: properties: - folderSelector: - properties: - matchExpressions: - items: - properties: - key: - type: string - operator: - type: string - values: - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - type: object - type: object - x-kubernetes-map-type: atomic + folderRef: + type: string folderUID: type: string instanceSelector: @@ -162,8 +140,8 @@ spec: - rules type: object x-kubernetes-validations: - - message: Only one of FolderUID or FolderSelector can be set - rule: (has(self.folderUID) && !(has(self.folderSelector))) || (has(self.folderSelector) + - message: Only one of FolderUID or FolderRef can be set + rule: (has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) && !(has(self.folderUID))) status: properties: diff --git a/config/grafana.integreatly.org_grafanaalertrulegroups.yaml b/config/grafana.integreatly.org_grafanaalertrulegroups.yaml index ec0bc68f4..91c374200 100644 --- a/config/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/config/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -35,51 +35,9 @@ spec: spec: description: GrafanaAlertRuleGroupSpec defines the desired state of GrafanaAlertRuleGroup properties: - folderSelector: + folderRef: description: Match GrafanaFolders CRs to infer the uid - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. - The requirements are ANDed. - items: - description: A label selector requirement is a selector that - contains values, a key, and an operator that relates the key - and values. - properties: - key: - description: key is the label key that the selector applies - to. - type: string - operator: - description: operator represents a key's relationship to - a set of values. Valid operators are In, NotIn, Exists - and DoesNotExist. - type: string - values: - description: values is an array of string values. If the - operator is In or NotIn, the values array must be non-empty. - If the operator is Exists or DoesNotExist, the values - array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A single - {key,value} in the matchLabels map is equivalent to an element - of matchExpressions, whose key field is "key", the operator - is "In", and the values array contains only "value". The requirements - are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic + type: string folderUID: description: UID of the folder containing this rule group Overrides the FolderSelector @@ -230,8 +188,8 @@ spec: - rules type: object x-kubernetes-validations: - - message: Only one of FolderUID or FolderSelector can be set - rule: (has(self.folderUID) && !(has(self.folderSelector))) || (has(self.folderSelector) + - message: Only one of FolderUID or FolderRef can be set + rule: (has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) && !(has(self.folderUID))) status: description: GrafanaAlertRuleGroupStatus defines the observed state of diff --git a/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml b/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml index 1f2c3885d..ff96dd984 100644 --- a/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml +++ b/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml @@ -9,7 +9,8 @@ metadata: app.kubernetes.io/created-by: grafana-operator name: grafanaalertrulegroup-sample spec: - folderUID: f9b0a98d-2ed3-45a6-9521-18679c74d4f1 + # folderUID: f9b0a98d-2ed3-45a6-9521-18679c74d4f1 + folderRef: test-folder-from-operator instanceSelector: matchLabels: dashboards: "grafana" diff --git a/controllers/grafanaalertrulegroup_controller.go b/controllers/grafanaalertrulegroup_controller.go index 288a2f0ea..4d3bad320 100644 --- a/controllers/grafanaalertrulegroup_controller.go +++ b/controllers/grafanaalertrulegroup_controller.go @@ -344,18 +344,18 @@ func (r *GrafanaAlertRuleGroupReconciler) GetFolderUID(ctx context.Context, grou if group.Spec.FolderUID != "" { return group.Spec.FolderUID } - folders, err := r.GetMatchingFolders(ctx, group.Spec.FolderSelector) + var folder grafanav1beta1.GrafanaFolder + err := r.Client.Get(ctx, client.ObjectKey{ + Namespace: group.Namespace, + Name: group.Spec.FolderRef, + }, &folder) if err != nil { - setNoMatchingFolder(&group.Status.Conditions, group.Generation, "ErrFetchingFolders", fmt.Sprintf("Failed to fetch folders: %s", err.Error())) - return "" - } - if len(folders.Items) < 1 { - setNoMatchingFolder(&group.Status.Conditions, group.Generation, "NoMatches", "Folder selector did not match any folders") - return "" - } - if len(folders.Items) > 1 { - setNoMatchingFolder(&group.Status.Conditions, group.Generation, "MultipleMatches", fmt.Sprintf("Folder selector matched %d folders. Only one folder can be matched", len(folders.Items))) + if kuberr.IsNotFound(err) { + setNoMatchingFolder(&group.Status.Conditions, group.Generation, "NotFound", fmt.Sprintf("Folder with name %s not found in namespace %s", group.Spec.FolderRef, group.Namespace)) + return "" + } + setNoMatchingFolder(&group.Status.Conditions, group.Generation, "ErrFetchingFolder", fmt.Sprintf("Failed to fetch folder: %s", err.Error())) return "" } - return string(folders.Items[0].UID) + return string(folder.UID) } diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml index f7a02531a..f64aaa4d0 100644 --- a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -26,30 +26,8 @@ spec: type: object spec: properties: - folderSelector: - properties: - matchExpressions: - items: - properties: - key: - type: string - operator: - type: string - values: - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - type: object - type: object - x-kubernetes-map-type: atomic + folderRef: + type: string folderUID: type: string instanceSelector: @@ -162,8 +140,8 @@ spec: - rules type: object x-kubernetes-validations: - - message: Only one of FolderUID or FolderSelector can be set - rule: (has(self.folderUID) && !(has(self.folderSelector))) || (has(self.folderSelector) + - message: Only one of FolderUID or FolderRef can be set + rule: (has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) && !(has(self.folderUID))) status: properties: diff --git a/tests/e2e/example-test/08-alert-rule-group.yaml b/tests/e2e/example-test/08-alert-rule-group.yaml index 8507950a7..eba77bdcb 100644 --- a/tests/e2e/example-test/08-alert-rule-group.yaml +++ b/tests/e2e/example-test/08-alert-rule-group.yaml @@ -14,9 +14,7 @@ kind: GrafanaAlertRuleGroup metadata: name: test spec: - folderSelector: - matchLabels: - folder: "test-folder" + folderRef: "test-folder-from-operator" instanceSelector: matchLabels: dashboards: "grafana" From 20c4de2b405671532df966991b3caf6fbdee6fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Wed, 28 Feb 2024 16:10:57 +0100 Subject: [PATCH 19/26] feat: add AllowCrossNamespace parameter --- api/v1beta1/grafanaalertrulegroup_types.go | 3 ++ ...ntegreatly.org_grafanaalertrulegroups.yaml | 2 ++ ...ntegreatly.org_grafanaalertrulegroups.yaml | 2 ++ .../grafanaalertrulegroup_controller.go | 32 ++++++++++++------- ...ntegreatly.org_grafanaalertrulegroups.yaml | 2 ++ 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/api/v1beta1/grafanaalertrulegroup_types.go b/api/v1beta1/grafanaalertrulegroup_types.go index 287362d3c..1f2dc6475 100644 --- a/api/v1beta1/grafanaalertrulegroup_types.go +++ b/api/v1beta1/grafanaalertrulegroup_types.go @@ -49,6 +49,9 @@ type GrafanaAlertRuleGroupSpec struct { // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$" // +kubebuilder:validation:Required Interval metav1.Duration `json:"interval"` + + // +optional + AllowCrossNamespaceImport *bool `json:"allowCrossNamespaceImport,omitempty"` } // AlertRule defines a specific rule to be evaluated. It is based on the upstream model with some k8s specific type mappings diff --git a/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml index f64aaa4d0..8e952d1e1 100644 --- a/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -26,6 +26,8 @@ spec: type: object spec: properties: + allowCrossNamespaceImport: + type: boolean folderRef: type: string folderUID: diff --git a/config/grafana.integreatly.org_grafanaalertrulegroups.yaml b/config/grafana.integreatly.org_grafanaalertrulegroups.yaml index 91c374200..949b4ffd5 100644 --- a/config/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/config/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -35,6 +35,8 @@ spec: spec: description: GrafanaAlertRuleGroupSpec defines the desired state of GrafanaAlertRuleGroup properties: + allowCrossNamespaceImport: + type: boolean folderRef: description: Match GrafanaFolders CRs to infer the uid type: string diff --git a/controllers/grafanaalertrulegroup_controller.go b/controllers/grafanaalertrulegroup_controller.go index 4d3bad320..9f7871fc3 100644 --- a/controllers/grafanaalertrulegroup_controller.go +++ b/controllers/grafanaalertrulegroup_controller.go @@ -104,13 +104,13 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr } }() - instances, err := r.GetMatchingInstances(ctx, group.Spec.InstanceSelector, r.Client) + instances, err := r.GetMatchingInstances(ctx, group, r.Client) if err != nil { setNoMatchingInstance(&group.Status.Conditions, group.Generation, "ErrFetchingInstances", fmt.Sprintf("error occurred during fetching of instances: %s", err.Error())) r.Log.Error(err, "could not find matching instances") return ctrl.Result{RequeueAfter: RequeueDelay}, err } - if len(instances.Items) == 0 { + if len(instances) == 0 { setNoMatchingInstance(&group.Status.Conditions, group.Generation, "EmptyAPIReply", "Instances could not be fetched, reconciliation will be retried") return ctrl.Result{}, nil } @@ -123,7 +123,7 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr } applyErrors := make(map[string]string) - for _, grafana := range instances.Items { + for _, grafana := range instances { // can be removed in go 1.22+ grafana := grafana if grafana.Status.Stage != grafanav1beta1.OperatorStageComplete || grafana.Status.StageStatus != grafanav1beta1.OperatorStageResultSuccess { @@ -147,7 +147,7 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr if len(applyErrors) == 0 { condition.Status = "True" condition.Reason = "ApplySuccesfull" - condition.Message = fmt.Sprintf("Alert Rule Group was successfully applied to %d instances", len(instances.Items)) + condition.Message = fmt.Sprintf("Alert Rule Group was successfully applied to %d instances", len(instances)) } else { condition.Status = "False" condition.Reason = "ApplyFailed" @@ -157,7 +157,7 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr sb.WriteString(fmt.Sprintf("\n- %s: %s", i, err)) } - condition.Message = fmt.Sprintf("Alert Rule Group failed to be applied for %d out of %d instances. Errors:%s", len(applyErrors), len(instances.Items), sb.String()) + condition.Message = fmt.Sprintf("Alert Rule Group failed to be applied for %d out of %d instances. Errors:%s", len(applyErrors), len(instances), sb.String()) } meta.SetStatusCondition(&group.Status.Conditions, condition) @@ -285,11 +285,11 @@ func (r *GrafanaAlertRuleGroupReconciler) finalize(ctx context.Context, group *g r.Log.Info("ignoring finalization logic as folder no longer exists") return nil } - instances, err := r.GetMatchingInstances(ctx, group.Spec.InstanceSelector, r.Client) + instances, err := r.GetMatchingInstances(ctx, group, r.Client) if err != nil { return fmt.Errorf("fetching instances: %w", err) } - for _, i := range instances.Items { + for _, i := range instances { instance := i if err := r.removeFromInstance(ctx, &instance, group, folderUID); err != nil { return fmt.Errorf("removing from instance") @@ -323,12 +323,22 @@ func (r *GrafanaAlertRuleGroupReconciler) removeFromInstance(ctx context.Context return nil } -func (r *GrafanaAlertRuleGroupReconciler) GetMatchingInstances(ctx context.Context, selector *metav1.LabelSelector, k8sClient client.Client) (grafanav1beta1.GrafanaList, error) { - instances, err := GetMatchingInstances(ctx, k8sClient, selector) +func (r *GrafanaAlertRuleGroupReconciler) GetMatchingInstances(ctx context.Context, group *grafanav1beta1.GrafanaAlertRuleGroup, k8sClient client.Client) ([]grafanav1beta1.Grafana, error) { + instances, err := GetMatchingInstances(ctx, k8sClient, group.Spec.InstanceSelector) if err != nil || len(instances.Items) == 0 { - return grafanav1beta1.GrafanaList{}, err + return nil, err + } + if group.Spec.AllowCrossNamespaceImport != nil && *group.Spec.AllowCrossNamespaceImport { + return instances.Items, nil + } + items := []grafanav1beta1.Grafana{} + for _, i := range instances.Items { + if i.Namespace == group.Namespace { + items = append(items, i) + } } - return instances, err + + return items, err } func (r *GrafanaAlertRuleGroupReconciler) GetMatchingFolders(ctx context.Context, selector *metav1.LabelSelector) (grafanav1beta1.GrafanaFolderList, error) { diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml index f64aaa4d0..8e952d1e1 100644 --- a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -26,6 +26,8 @@ spec: type: object spec: properties: + allowCrossNamespaceImport: + type: boolean folderRef: type: string folderUID: From 4287d88d84523cce55120fec201f629b98e80bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Thu, 29 Feb 2024 10:47:11 +0100 Subject: [PATCH 20/26] fix: use folderRef in examples --- examples/alertrulegroups/resources.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/alertrulegroups/resources.yaml b/examples/alertrulegroups/resources.yaml index 3de54a6c1..216cfcc59 100644 --- a/examples/alertrulegroups/resources.yaml +++ b/examples/alertrulegroups/resources.yaml @@ -19,8 +19,6 @@ apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaFolder metadata: name: test-folder - labels: - folder: test-folder spec: instanceSelector: matchLabels: @@ -31,9 +29,7 @@ kind: GrafanaAlertRuleGroup metadata: name: grafanaalertrulegroup-sample spec: - folderSelector: - matchLabels: - folder: test-folder + folderRef: test-folder instanceSelector: matchLabels: dashboards: "grafana" From c67b1cba753798e2ca7f37f597f50602cc77b8eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Thu, 29 Feb 2024 10:48:34 +0100 Subject: [PATCH 21/26] fix: add generated deepcopy --- api/v1beta1/zz_generated.deepcopy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 9c4b47c62..bf87b4cb4 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -482,6 +482,11 @@ func (in *GrafanaAlertRuleGroupSpec) DeepCopyInto(out *GrafanaAlertRuleGroupSpec } } out.Interval = in.Interval + if in.AllowCrossNamespaceImport != nil { + in, out := &in.AllowCrossNamespaceImport, &out.AllowCrossNamespaceImport + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaAlertRuleGroupSpec. From e0deca0de6cd7ab2d77ef754d3cc2140441b6262 Mon Sep 17 00:00:00 2001 From: Edvin Norling Date: Tue, 5 Mar 2024 16:08:49 +0100 Subject: [PATCH 22/26] make consistent with other rbac --- config/rbac/grafanaalertrulegroup_editor_role.yaml | 7 ------- config/rbac/grafanaalertrulegroup_viewer_role.yaml | 7 ------- 2 files changed, 14 deletions(-) diff --git a/config/rbac/grafanaalertrulegroup_editor_role.yaml b/config/rbac/grafanaalertrulegroup_editor_role.yaml index 61593a7b0..7f0933c41 100644 --- a/config/rbac/grafanaalertrulegroup_editor_role.yaml +++ b/config/rbac/grafanaalertrulegroup_editor_role.yaml @@ -2,13 +2,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - labels: - app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: grafanaalertrulegroup-editor-role - app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: grafana-operator - app.kubernetes.io/part-of: grafana-operator - app.kubernetes.io/managed-by: kustomize name: grafanaalertrulegroup-editor-role rules: - apiGroups: diff --git a/config/rbac/grafanaalertrulegroup_viewer_role.yaml b/config/rbac/grafanaalertrulegroup_viewer_role.yaml index 844c30357..40c89233f 100644 --- a/config/rbac/grafanaalertrulegroup_viewer_role.yaml +++ b/config/rbac/grafanaalertrulegroup_viewer_role.yaml @@ -2,13 +2,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - labels: - app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: grafanaalertrulegroup-viewer-role - app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: grafana-operator - app.kubernetes.io/part-of: grafana-operator - app.kubernetes.io/managed-by: kustomize name: grafanaalertrulegroup-viewer-role rules: - apiGroups: From 35aa1b6b9868659a54fbd98a8b60d125efcb4a77 Mon Sep 17 00:00:00 2001 From: Edvin Norling Date: Tue, 5 Mar 2024 16:10:05 +0100 Subject: [PATCH 23/26] run make bundle/redhat to have less changes --- ...rafana-operator.clusterserviceversion.yaml | 130 +++++++++++- ...ntegreatly.org_grafanaalertrulegroups.yaml | 199 ++++++++++++++++++ 2 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 bundle/manifests/grafana.integreatly.org_grafanaalertrulegroups.yaml diff --git a/bundle/manifests/grafana-operator.clusterserviceversion.yaml b/bundle/manifests/grafana-operator.clusterserviceversion.yaml index 180763f6d..f516d1c02 100644 --- a/bundle/manifests/grafana-operator.clusterserviceversion.yaml +++ b/bundle/manifests/grafana-operator.clusterserviceversion.yaml @@ -29,6 +29,103 @@ metadata: } } }, + { + "apiVersion": "grafana.integreatly.org/v1beta1", + "kind": "GrafanaAlertRuleGroup", + "metadata": { + "labels": { + "app.kubernetes.io/created-by": "grafana-operator", + "app.kubernetes.io/instance": "grafanaalertrulegroup-sample", + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "grafanaalertrulegroup", + "app.kubernetes.io/part-of": "grafana-operator" + }, + "name": "grafanaalertrulegroup-sample" + }, + "spec": { + "folderRef": "test-folder-from-operator", + "instanceSelector": { + "matchLabels": { + "dashboards": "grafana" + } + }, + "interval": "5m", + "rules": [ + { + "condition": "B", + "data": [ + { + "datasourceUid": "grafanacloud-demoinfra-prom", + "model": { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-demoinfra-prom" + }, + "editorMode": "code", + "expr": "weather_temp_c{}", + "instant": true, + "intervalMs": 1000, + "legendFormat": "__auto", + "maxDataPoints": 43200, + "range": false, + "refId": "A" + }, + "refId": "A", + "relativeTimeRange": { + "from": 600 + } + }, + { + "datasourceUid": "__expr__", + "model": { + "conditions": [ + { + "evaluator": { + "params": [ + 0 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "C" + ] + }, + "reducer": { + "params": [], + "type": "last" + }, + "type": "query" + } + ], + "datasource": { + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "A", + "intervalMs": 1000, + "maxDataPoints": 43200, + "refId": "B", + "type": "threshold" + }, + "refId": "B", + "relativeTimeRange": { + "from": 600 + } + } + ], + "execErrState": "Error", + "for": "5m0s", + "noDataState": "NoData", + "title": "Temperature below freezing", + "uid": "4843de5c-4f8a-4af0-9509-23526a04faf8" + } + ] + } + }, { "apiVersion": "grafana.integreatly.org/v1beta1", "kind": "GrafanaDashboard", @@ -95,7 +192,7 @@ metadata: capabilities: Basic Install categories: Monitoring containerImage: ghcr.io/grafana/grafana-operator@sha256:97561cef949b58f55ec67d133c02ac205e2ec3fb77388aeb868dacfcebad0752 - createdAt: "2024-02-11T09:40:22Z" + createdAt: "2024-03-05T15:09:12Z" description: Deploys and manages Grafana instances, dashboards and data sources operators.operatorframework.io/builder: operator-sdk-v1.32.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 @@ -107,6 +204,11 @@ spec: apiservicedefinitions: {} customresourcedefinitions: owned: + - description: GrafanaAlertRuleGroup is the Schema for the grafanaalertrulegroups API + displayName: Grafana Alert Rule Group + kind: GrafanaAlertRuleGroup + name: grafanaalertrulegroups.grafana.integreatly.org + version: v1beta1 - description: GrafanaDashboard is the Schema for the grafanadashboards API displayName: Grafana Dashboard kind: GrafanaDashboard @@ -174,6 +276,32 @@ spec: - patch - update - watch + - apiGroups: + - grafana.integreatly.org + resources: + - grafanaalertrulegroups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - grafana.integreatly.org + resources: + - grafanaalertrulegroups/finalizers + verbs: + - update + - apiGroups: + - grafana.integreatly.org + resources: + - grafanaalertrulegroups/status + verbs: + - get + - patch + - update - apiGroups: - grafana.integreatly.org resources: diff --git a/bundle/manifests/grafana.integreatly.org_grafanaalertrulegroups.yaml b/bundle/manifests/grafana.integreatly.org_grafanaalertrulegroups.yaml new file mode 100644 index 000000000..a4c650348 --- /dev/null +++ b/bundle/manifests/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -0,0 +1,199 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.0 + creationTimestamp: null + name: grafanaalertrulegroups.grafana.integreatly.org +spec: + group: grafana.integreatly.org + names: + kind: GrafanaAlertRuleGroup + listKind: GrafanaAlertRuleGroupList + plural: grafanaalertrulegroups + singular: grafanaalertrulegroup + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + allowCrossNamespaceImport: + type: boolean + folderRef: + type: string + folderUID: + type: string + instanceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + interval: + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + resyncPeriod: + default: 10m + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + rules: + items: + properties: + annotations: + additionalProperties: + type: string + type: object + condition: + type: string + data: + items: + properties: + datasourceUid: + type: string + model: + x-kubernetes-preserve-unknown-fields: true + queryType: + type: string + refId: + type: string + relativeTimeRange: + properties: + from: + format: int64 + type: integer + to: + format: int64 + type: integer + type: object + type: object + type: array + execErrState: + enum: + - OK + - Alerting + - Error + type: string + for: + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + isPaused: + type: boolean + labels: + additionalProperties: + type: string + type: object + noDataState: + enum: + - Alerting + - NoData + - OK + type: string + title: + example: Always firing + maxLength: 190 + minLength: 1 + type: string + uid: + pattern: ^[a-zA-Z0-9-_]+$ + type: string + required: + - condition + - data + - execErrState + - for + - noDataState + - title + - uid + type: object + type: array + required: + - instanceSelector + - interval + - rules + type: object + x-kubernetes-validations: + - message: Only one of FolderUID or FolderRef can be set + rule: (has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) + && !(has(self.folderUID))) + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null From b00b1be75cae2599a9d5ff6ed2d6b1e68b3c1c21 Mon Sep 17 00:00:00 2001 From: Edvin Norling Date: Tue, 5 Mar 2024 16:13:16 +0100 Subject: [PATCH 24/26] remove the last labels --- config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml b/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml index ff96dd984..7f163e61a 100644 --- a/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml +++ b/config/samples/grafana_v1beta1_grafanaalertrulegroup.yaml @@ -1,12 +1,6 @@ apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaAlertRuleGroup metadata: - labels: - app.kubernetes.io/name: grafanaalertrulegroup - app.kubernetes.io/instance: grafanaalertrulegroup-sample - app.kubernetes.io/part-of: grafana-operator - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: grafana-operator name: grafanaalertrulegroup-sample spec: # folderUID: f9b0a98d-2ed3-45a6-9521-18679c74d4f1 From 4869fb38ab677c9ea0af267fa8a6cc7203804fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Mon, 11 Mar 2024 10:53:20 +0100 Subject: [PATCH 25/26] refactor: remove obsolete function --- controllers/grafanaalertrulegroup_controller.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/controllers/grafanaalertrulegroup_controller.go b/controllers/grafanaalertrulegroup_controller.go index 9f7871fc3..0f41c6f20 100644 --- a/controllers/grafanaalertrulegroup_controller.go +++ b/controllers/grafanaalertrulegroup_controller.go @@ -341,15 +341,6 @@ func (r *GrafanaAlertRuleGroupReconciler) GetMatchingInstances(ctx context.Conte return items, err } -func (r *GrafanaAlertRuleGroupReconciler) GetMatchingFolders(ctx context.Context, selector *metav1.LabelSelector) (grafanav1beta1.GrafanaFolderList, error) { - var list grafanav1beta1.GrafanaFolderList - opts := []client.ListOption{ - client.MatchingLabels(selector.MatchLabels), - } - err := r.Client.List(ctx, &list, opts...) - return list, err -} - func (r *GrafanaAlertRuleGroupReconciler) GetFolderUID(ctx context.Context, group *grafanav1beta1.GrafanaAlertRuleGroup) string { if group.Spec.FolderUID != "" { return group.Spec.FolderUID From 40e32cbfaa17429d25adcb6c729d0350a2777184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Mon, 11 Mar 2024 11:29:43 +0100 Subject: [PATCH 26/26] feat: remove finalizer when no instances match this reduces load on the operator and eases deletion when no instance can be found --- .../grafanaalertrulegroup_controller.go | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/controllers/grafanaalertrulegroup_controller.go b/controllers/grafanaalertrulegroup_controller.go index 0f41c6f20..ad401b1d2 100644 --- a/controllers/grafanaalertrulegroup_controller.go +++ b/controllers/grafanaalertrulegroup_controller.go @@ -42,6 +42,10 @@ import ( client2 "github.com/grafana/grafana-operator/v5/controllers/client" ) +const ( + conditionAlertGroupSynchronized = "AlertGroupSynchronized" +) + // GrafanaAlertRuleGroupReconciler reconciles a GrafanaAlertRuleGroup object type GrafanaAlertRuleGroupReconciler struct { client.Client @@ -90,27 +94,31 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr } return ctrl.Result{}, nil } - if !controllerutil.ContainsFinalizer(group, grafanaFinalizer) { - controllerutil.AddFinalizer(group, grafanaFinalizer) - if err := r.Update(ctx, group); err != nil { - r.Log.Error(err, "failed to set finalizer") - return ctrl.Result{RequeueAfter: RequeueDelay}, err - } - } defer func() { if err := r.Client.Status().Update(ctx, group); err != nil { r.Log.Error(err, "updating status") } + if meta.IsStatusConditionTrue(group.Status.Conditions, conditionNoMatchingInstance) { + controllerutil.RemoveFinalizer(group, grafanaFinalizer) + } else { + controllerutil.AddFinalizer(group, grafanaFinalizer) + } + if err := r.Update(ctx, group); err != nil { + r.Log.Error(err, "failed to set finalizer") + } }() instances, err := r.GetMatchingInstances(ctx, group, r.Client) if err != nil { setNoMatchingInstance(&group.Status.Conditions, group.Generation, "ErrFetchingInstances", fmt.Sprintf("error occurred during fetching of instances: %s", err.Error())) + meta.RemoveStatusCondition(&group.Status.Conditions, conditionAlertGroupSynchronized) r.Log.Error(err, "could not find matching instances") return ctrl.Result{RequeueAfter: RequeueDelay}, err } + if len(instances) == 0 { + meta.RemoveStatusCondition(&group.Status.Conditions, conditionAlertGroupSynchronized) setNoMatchingInstance(&group.Status.Conditions, group.Generation, "EmptyAPIReply", "Instances could not be fetched, reconciliation will be retried") return ctrl.Result{}, nil } @@ -137,7 +145,7 @@ func (r *GrafanaAlertRuleGroupReconciler) Reconcile(ctx context.Context, req ctr } } condition := metav1.Condition{ - Type: "AlertGroupSynchronized", + Type: conditionAlertGroupSynchronized, ObservedGeneration: group.Generation, LastTransitionTime: metav1.Time{ Time: time.Now(),