diff --git a/PROJECT b/PROJECT index 34920e852..d9fba0503 100644 --- a/PROJECT +++ b/PROJECT @@ -1,3 +1,7 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: integreatly.org layout: - go.kubebuilder.io/v3 @@ -51,4 +55,13 @@ resources: kind: GrafanaAlertRuleGroup path: github.com/grafana/grafana-operator/api/v1beta1 version: v1beta1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: integreatly.org + group: grafana + kind: GrafanaContactPoint + path: github.com/grafana/grafana-operator/api/v1beta1 + version: v1beta1 version: "3" diff --git a/api/v1beta1/grafanacontactpoint_types.go b/api/v1beta1/grafanacontactpoint_types.go new file mode 100644 index 000000000..bf2fdc20c --- /dev/null +++ b/api/v1beta1/grafanacontactpoint_types.go @@ -0,0 +1,84 @@ +/* +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 ( + apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// GrafanaContactPointSpec defines the desired state of GrafanaContactPoint +type GrafanaContactPointSpec 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"` + + // +optional + DisableResolveMessage bool `json:"disableResolveMessage,omitempty"` + + // +kubebuilder:validation:type=string + Name string `json:"name"` + + Settings *apiextensions.JSON `json:"settings"` + + // +kubebuilder:validation:Enum=alertmanager;dingding;discord;email;googlechat;kafka;line;opsgenie;pagerduty;pushover;sensugo;slack;teams;telegram;threema;victorops;webhook;wecom + Type string `json:"type,omitempty"` + + // +optional + AllowCrossNamespaceImport *bool `json:"allowCrossNamespaceImport,omitempty"` +} + +// GrafanaContactPointStatus defines the observed state of GrafanaContactPoint +type GrafanaContactPointStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + Conditions []metav1.Condition `json:"conditions"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// GrafanaContactPoint is the Schema for the grafanacontactpoints API +type GrafanaContactPoint struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GrafanaContactPointSpec `json:"spec,omitempty"` + Status GrafanaContactPointStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// GrafanaContactPointList contains a list of GrafanaContactPoint +type GrafanaContactPointList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GrafanaContactPoint `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GrafanaContactPoint{}, &GrafanaContactPointList{}) +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index bf87b4cb4..17f43a5d6 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -566,6 +566,118 @@ func (in *GrafanaComDashboardReference) DeepCopy() *GrafanaComDashboardReference return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GrafanaContactPoint) DeepCopyInto(out *GrafanaContactPoint) { + *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 GrafanaContactPoint. +func (in *GrafanaContactPoint) DeepCopy() *GrafanaContactPoint { + if in == nil { + return nil + } + out := new(GrafanaContactPoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GrafanaContactPoint) 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 *GrafanaContactPointList) DeepCopyInto(out *GrafanaContactPointList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GrafanaContactPoint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaContactPointList. +func (in *GrafanaContactPointList) DeepCopy() *GrafanaContactPointList { + if in == nil { + return nil + } + out := new(GrafanaContactPointList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GrafanaContactPointList) 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 *GrafanaContactPointSpec) DeepCopyInto(out *GrafanaContactPointSpec) { + *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.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(apiextensionsv1.JSON) + (*in).DeepCopyInto(*out) + } + 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 GrafanaContactPointSpec. +func (in *GrafanaContactPointSpec) DeepCopy() *GrafanaContactPointSpec { + if in == nil { + return nil + } + out := new(GrafanaContactPointSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GrafanaContactPointStatus) DeepCopyInto(out *GrafanaContactPointStatus) { + *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 GrafanaContactPointStatus. +func (in *GrafanaContactPointStatus) DeepCopy() *GrafanaContactPointStatus { + if in == nil { + return nil + } + out := new(GrafanaContactPointStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GrafanaDashboard) DeepCopyInto(out *GrafanaDashboard) { *out = *in diff --git a/config/crd/bases/grafana.integreatly.org_grafanacontactpoints.yaml b/config/crd/bases/grafana.integreatly.org_grafanacontactpoints.yaml new file mode 100644 index 000000000..a273dd277 --- /dev/null +++ b/config/crd/bases/grafana.integreatly.org_grafanacontactpoints.yaml @@ -0,0 +1,137 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.0 + name: grafanacontactpoints.grafana.integreatly.org +spec: + group: grafana.integreatly.org + names: + kind: GrafanaContactPoint + listKind: GrafanaContactPointList + plural: grafanacontactpoints + singular: grafanacontactpoint + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + allowCrossNamespaceImport: + type: boolean + disableResolveMessage: + type: boolean + 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 + name: + type: string + resyncPeriod: + default: 10m + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + settings: + x-kubernetes-preserve-unknown-fields: true + type: + enum: + - alertmanager + - dingding + - discord + - email + - googlechat + - kafka + - line + - opsgenie + - pagerduty + - pushover + - sensugo + - slack + - teams + - telegram + - threema + - victorops + - webhook + - wecom + type: string + required: + - instanceSelector + - name + - settings + 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 2187eb794..b3f304d0c 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,6 +7,7 @@ resources: - bases/grafana.integreatly.org_grafanadatasources.yaml - bases/grafana.integreatly.org_grafanafolders.yaml - bases/grafana.integreatly.org_grafanaalertrulegroups.yaml +- bases/grafana.integreatly.org_grafanacontactpoints.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -17,6 +18,7 @@ patchesStrategicMerge: #- patches/webhook_in_grafanadatasources.yaml #- patches/webhook_in_grafanafolders.yaml #- patches/webhook_in_grafanaalertrulegroups.yaml +#- patches/webhook_in_grafanacontactpoints.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -26,6 +28,7 @@ patchesStrategicMerge: #- patches/cainjection_in_grafanadatasources.yaml #- patches/cainjection_in_grafanafolders.yaml #- patches/cainjection_in_grafanaalertrulegroups.yaml +#- patches/cainjection_in_grafanacontactpoints.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_grafanacontactpoints.yaml b/config/crd/patches/cainjection_in_grafanacontactpoints.yaml new file mode 100644 index 000000000..4d815c779 --- /dev/null +++ b/config/crd/patches/cainjection_in_grafanacontactpoints.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: grafanacontactpoints.grafana.integreatly.org diff --git a/config/crd/patches/webhook_in_grafanacontactpoints.yaml b/config/crd/patches/webhook_in_grafanacontactpoints.yaml new file mode 100644 index 000000000..2b91fd2ed --- /dev/null +++ b/config/crd/patches/webhook_in_grafanacontactpoints.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: grafanacontactpoints.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_grafanacontactpoints.yaml b/config/grafana.integreatly.org_grafanacontactpoints.yaml new file mode 100644 index 000000000..eda51e818 --- /dev/null +++ b/config/grafana.integreatly.org_grafanacontactpoints.yaml @@ -0,0 +1,203 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.0 + name: grafanacontactpoints.grafana.integreatly.org +spec: + group: grafana.integreatly.org + names: + kind: GrafanaContactPoint + listKind: GrafanaContactPointList + plural: grafanacontactpoints + singular: grafanacontactpoint + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: GrafanaContactPoint is the Schema for the grafanacontactpoints + 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: GrafanaContactPointSpec defines the desired state of GrafanaContactPoint + properties: + allowCrossNamespaceImport: + type: boolean + disableResolveMessage: + type: boolean + 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 + name: + type: string + resyncPeriod: + default: 10m + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + settings: + x-kubernetes-preserve-unknown-fields: true + type: + enum: + - alertmanager + - dingding + - discord + - email + - googlechat + - kafka + - line + - opsgenie + - pagerduty + - pushover + - sensugo + - slack + - teams + - telegram + - threema + - victorops + - webhook + - wecom + type: string + required: + - instanceSelector + - name + - settings + type: object + status: + description: GrafanaContactPointStatus defines the observed state of GrafanaContactPoint + properties: + conditions: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + 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/rbac/grafanacontactpoint_editor_role.yaml b/config/rbac/grafanacontactpoint_editor_role.yaml new file mode 100644 index 000000000..42f29eb6b --- /dev/null +++ b/config/rbac/grafanacontactpoint_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit grafanacontactpoints. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: grafanacontactpoint-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: grafanacontactpoint-editor-role +rules: +- apiGroups: + - grafana.integreatly.org + resources: + - grafanacontactpoints + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - grafana.integreatly.org + resources: + - grafanacontactpoints/status + verbs: + - get diff --git a/config/rbac/grafanacontactpoint_viewer_role.yaml b/config/rbac/grafanacontactpoint_viewer_role.yaml new file mode 100644 index 000000000..18ff3b5bf --- /dev/null +++ b/config/rbac/grafanacontactpoint_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view grafanacontactpoints. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: grafanacontactpoint-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: grafanacontactpoint-viewer-role +rules: +- apiGroups: + - grafana.integreatly.org + resources: + - grafanacontactpoints + verbs: + - get + - list + - watch +- apiGroups: + - grafana.integreatly.org + resources: + - grafanacontactpoints/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index c0c4dd603..bd5995871 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -68,6 +68,32 @@ rules: - get - patch - update +- apiGroups: + - grafana.integreatly.org + resources: + - grafanacontactpoints + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - grafana.integreatly.org + resources: + - grafanacontactpoints/finalizers + verbs: + - update +- apiGroups: + - grafana.integreatly.org + resources: + - grafanacontactpoints/status + verbs: + - get + - patch + - update - apiGroups: - grafana.integreatly.org resources: diff --git a/config/samples/grafana_v1beta1_grafanacontactpoint.yaml b/config/samples/grafana_v1beta1_grafanacontactpoint.yaml new file mode 100644 index 000000000..0cf1bcd79 --- /dev/null +++ b/config/samples/grafana_v1beta1_grafanacontactpoint.yaml @@ -0,0 +1,18 @@ +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaContactPoint +metadata: + labels: + app.kubernetes.io/name: grafanacontactpoint + app.kubernetes.io/instance: grafanacontactpoint-sample + app.kubernetes.io/part-of: grafana-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: grafana-operator + name: grafanacontactpoint-sample +spec: + name: grafanacontactpoint-sample + type: "email" + instanceSelector: + matchLabels: + dashboards: "grafana-a" + settings: + email: diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index b67312ec7..9567dfd5b 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -5,4 +5,5 @@ resources: - grafana_v1beta1_grafanadatasource.yaml - grafana_v1beta1_grafanafolder.yaml - grafana_v1beta1_grafanaalertrulegroup.yaml +- grafana_v1beta1_grafanacontactpoint.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/controllers/grafanacontactpoint_controller.go b/controllers/grafanacontactpoint_controller.go new file mode 100644 index 000000000..90c1450c8 --- /dev/null +++ b/controllers/grafanacontactpoint_controller.go @@ -0,0 +1,286 @@ +/* +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" + "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" + "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" + "github.com/grafana/grafana-openapi-client-go/client/provisioning" + "github.com/grafana/grafana-openapi-client-go/models" + grafanav1beta1 "github.com/grafana/grafana-operator/v5/api/v1beta1" + client2 "github.com/grafana/grafana-operator/v5/controllers/client" +) + +const ( + conditionContactPointSynchronized = "ContactPointSynchronized" +) + +// GrafanaContactPointReconciler reconciles a GrafanaContactPoint object +type GrafanaContactPointReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanacontactpoints,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanacontactpoints/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanacontactpoints/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. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the GrafanaContactPoint object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile +func (r *GrafanaContactPointReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + controllerLog := log.FromContext(ctx).WithName("GrafanaContactPointReconciler") + r.Log = log.FromContext(ctx) + + contactPoint := &grafanav1beta1.GrafanaContactPoint{} + err := r.Client.Get(ctx, client.ObjectKey{ + Namespace: req.Namespace, + Name: req.Name, + }, contactPoint) + if err != nil { + if kuberr.IsNotFound(err) { + return ctrl.Result{}, nil + } + controllerLog.Error(err, "Failed to get GrafanaContactPoint") + return ctrl.Result{RequeueAfter: RequeueDelay}, err + } + + if contactPoint.GetDeletionTimestamp() != nil { + if controllerutil.ContainsFinalizer(contactPoint, grafanaFinalizer) { + err := r.finalize(ctx, contactPoint) + if err != nil { + return ctrl.Result{RequeueAfter: RequeueDelay}, fmt.Errorf("failed to finalize GrafanaContactPoint: %w", err) + } + controllerutil.RemoveFinalizer(contactPoint, grafanaFinalizer) + if err := r.Update(ctx, contactPoint); err != nil { + r.Log.Error(err, "failed to remove finalizer") + return ctrl.Result{RequeueAfter: RequeueDelay}, fmt.Errorf("failed to update GrafanaContactPoint: %w", err) + } + } + return ctrl.Result{}, nil + } + + defer func() { + if err := r.Client.Status().Update(ctx, contactPoint); err != nil { + r.Log.Error(err, "updating status") + } + if meta.IsStatusConditionTrue(contactPoint.Status.Conditions, conditionNoMatchingInstance) { + controllerutil.RemoveFinalizer(contactPoint, grafanaFinalizer) + } else { + controllerutil.AddFinalizer(contactPoint, grafanaFinalizer) + } + if err := r.Update(ctx, contactPoint); err != nil { + r.Log.Error(err, "failed to set finalizer") + } + }() + + instances, err := r.GetMatchingInstances(ctx, contactPoint, r.Client) + if err != nil { + setNoMatchingInstance(&contactPoint.Status.Conditions, contactPoint.Generation, "ErrFetchingInstances", fmt.Sprintf("error occurred during fetching of instances: %s", err.Error())) + meta.RemoveStatusCondition(&contactPoint.Status.Conditions, conditionContactPointSynchronized) + r.Log.Error(err, "could not find matching instances") + return ctrl.Result{RequeueAfter: RequeueDelay}, err + } + + if len(instances) == 0 { + meta.RemoveStatusCondition(&contactPoint.Status.Conditions, conditionContactPointSynchronized) + setNoMatchingInstance(&contactPoint.Status.Conditions, contactPoint.Generation, "EmptyAPIReply", "Instances could not be fetched, reconciliation will be retried") + return ctrl.Result{}, nil + } + + removeNoMatchingInstance(&contactPoint.Status.Conditions) + + applyErrors := make(map[string]string) + for _, grafana := range instances { + // 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, contactPoint) + if err != nil { + applyErrors[fmt.Sprintf("%s/%s", grafana.Namespace, grafana.Name)] = err.Error() + } + } + condition := metav1.Condition{ + Type: conditionContactPointSynchronized, + ObservedGeneration: contactPoint.Generation, + LastTransitionTime: metav1.Time{ + Time: time.Now(), + }, + } + + if len(applyErrors) == 0 { + condition.Status = "True" + condition.Reason = "ApplySuccesfull" + condition.Message = fmt.Sprintf("Contact point was successfully applied to %d instances", len(instances)) + } 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("Contact point failed to be applied for %d out of %d instances. Errors:%s", len(applyErrors), len(instances), sb.String()) + } + meta.SetStatusCondition(&contactPoint.Status.Conditions, condition) + + return ctrl.Result{RequeueAfter: contactPoint.Spec.ResyncPeriod.Duration}, nil +} + +func (r *GrafanaContactPointReconciler) reconcileWithInstance(ctx context.Context, instance *grafanav1beta1.Grafana, contactPoint *grafanav1beta1.GrafanaContactPoint) error { + cl, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, instance) + if err != nil { + return fmt.Errorf("building grafana client: %w", err) + } + + var applied models.EmbeddedContactPoint + + applied, err = r.getContactPointFromUID(ctx, instance, contactPoint) + if err != nil { + return fmt.Errorf("getting contact point by UID: %w", err) + } + + if applied.UID == "" { + // create + cp := &models.EmbeddedContactPoint{ + DisableResolveMessage: contactPoint.Spec.DisableResolveMessage, + Name: contactPoint.Spec.Name, + Type: &contactPoint.Spec.Type, + Settings: contactPoint.Spec.Settings, + UID: string(contactPoint.UID), + } + _, err := cl.Provisioning.PostContactpoints(provisioning.NewPostContactpointsParams().WithBody(cp)) //nolint:errcheck + if err != nil { + return fmt.Errorf("creating contact point: %w", err) + } + } else { + // update + var updatedCP models.EmbeddedContactPoint + updatedCP.Name = contactPoint.Spec.Name + updatedCP.Type = &contactPoint.Spec.Type + updatedCP.Settings = contactPoint.Spec.Settings + _, err := cl.Provisioning.PutContactpoint(provisioning.NewPutContactpointParams().WithUID(applied.UID).WithBody(&updatedCP)) //nolint:errcheck + if err != nil { + return fmt.Errorf("updating contact point: %w", err) + } + } + return nil +} + +func (r *GrafanaContactPointReconciler) getContactPointFromUID(ctx context.Context, instance *grafanav1beta1.Grafana, contactPoint *grafanav1beta1.GrafanaContactPoint) (models.EmbeddedContactPoint, error) { + cl, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, instance) + if err != nil { + return models.EmbeddedContactPoint{}, fmt.Errorf("building grafana client: %w", err) + } + + params := provisioning.NewGetContactpointsParams() + remote, err := cl.Provisioning.GetContactpoints(params) + if err != nil { + return models.EmbeddedContactPoint{}, fmt.Errorf("getting contact points: %w", err) + } + for _, cp := range remote.Payload { + if cp.UID == string(contactPoint.UID) { + return *cp, nil + } + } + return models.EmbeddedContactPoint{}, nil +} + +func (r *GrafanaContactPointReconciler) finalize(ctx context.Context, contactPoint *grafanav1beta1.GrafanaContactPoint) error { + r.Log.Info("Finalizing GrafanaContactPoint") + + instances, err := r.GetMatchingInstances(ctx, contactPoint, r.Client) + if err != nil { + return fmt.Errorf("fetching instances: %w", err) + } + for _, i := range instances { + instance := i + if err := r.removeFromInstance(ctx, &instance, contactPoint); err != nil { + return fmt.Errorf("removing contact point from instance: %w", err) + } + } + + return nil +} + +func (r *GrafanaContactPointReconciler) removeFromInstance(ctx context.Context, instance *grafanav1beta1.Grafana, contactPoint *grafanav1beta1.GrafanaContactPoint) error { + cl, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, instance) + if err != nil { + return fmt.Errorf("building grafana client: %w", err) + } + + _, err = r.getContactPointFromUID(ctx, instance, contactPoint) + if err != nil { + return fmt.Errorf("getting contact point by UID: %w", err) + } + _, err = cl.Provisioning.DeleteContactpoints(string(contactPoint.UID)) //nolint:errcheck + if err != nil { + return fmt.Errorf("deleting contact point: %w", err) + } + + return nil +} + +func (r *GrafanaContactPointReconciler) GetMatchingInstances(ctx context.Context, contactPoint *grafanav1beta1.GrafanaContactPoint, k8sClient client.Client) ([]grafanav1beta1.Grafana, error) { + instances, err := GetMatchingInstances(ctx, k8sClient, contactPoint.Spec.InstanceSelector) + if err != nil || len(instances.Items) == 0 { + return nil, err + } + if contactPoint.Spec.AllowCrossNamespaceImport != nil && *contactPoint.Spec.AllowCrossNamespaceImport { + return instances.Items, nil + } + items := []grafanav1beta1.Grafana{} + for _, i := range instances.Items { + if i.Namespace == contactPoint.Namespace { + items = append(items, i) + } + } + + return items, err +} + +// SetupWithManager sets up the controller with the Manager. +func (r *GrafanaContactPointReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&grafanav1beta1.GrafanaContactPoint{}). + Complete(r) +} diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanacontactpoints.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanacontactpoints.yaml new file mode 100644 index 000000000..a273dd277 --- /dev/null +++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanacontactpoints.yaml @@ -0,0 +1,137 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.0 + name: grafanacontactpoints.grafana.integreatly.org +spec: + group: grafana.integreatly.org + names: + kind: GrafanaContactPoint + listKind: GrafanaContactPointList + plural: grafanacontactpoints + singular: grafanacontactpoint + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + allowCrossNamespaceImport: + type: boolean + disableResolveMessage: + type: boolean + 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 + name: + type: string + resyncPeriod: + default: 10m + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + settings: + x-kubernetes-preserve-unknown-fields: true + type: + enum: + - alertmanager + - dingding + - discord + - email + - googlechat + - kafka + - line + - opsgenie + - pagerduty + - pushover + - sensugo + - slack + - teams + - telegram + - threema + - victorops + - webhook + - wecom + type: string + required: + - instanceSelector + - name + - settings + 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/deploy/helm/grafana-operator/templates/rbac.yaml b/deploy/helm/grafana-operator/templates/rbac.yaml index 69ebc9206..098369968 100644 --- a/deploy/helm/grafana-operator/templates/rbac.yaml +++ b/deploy/helm/grafana-operator/templates/rbac.yaml @@ -118,6 +118,32 @@ rules: - get - patch - update + - apiGroups: + - grafana.integreatly.org + resources: + - grafanacontactpoints + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - grafana.integreatly.org + resources: + - grafanacontactpoints/finalizers + verbs: + - update + - apiGroups: + - grafana.integreatly.org + resources: + - grafanacontactpoints/status + verbs: + - get + - patch + - update - apiGroups: - grafana.integreatly.org resources: diff --git a/hack/kind/resources/default/grafana-contactpoint.yaml b/hack/kind/resources/default/grafana-contactpoint.yaml new file mode 100644 index 000000000..77a35e735 --- /dev/null +++ b/hack/kind/resources/default/grafana-contactpoint.yaml @@ -0,0 +1,18 @@ +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaContactPoint +metadata: + labels: + app.kubernetes.io/name: grafanacontactpoint + app.kubernetes.io/instance: grafanacontactpoint-sample + app.kubernetes.io/part-of: grafana-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: grafana-operator + name: grafanacontactpoint-sample +spec: + name: grafanacontactpoint-sample + type: "email" + instanceSelector: + matchLabels: + dashboards: "grafana" + settings: + addresses: "email@email.com" diff --git a/hack/kind/resources/default/kustomization.yaml b/hack/kind/resources/default/kustomization.yaml index 34d79d521..ddecf6c73 100644 --- a/hack/kind/resources/default/kustomization.yaml +++ b/hack/kind/resources/default/kustomization.yaml @@ -2,3 +2,4 @@ resources: - grafana.yaml - grafana-dashboard.yaml - grafana-datasource.yaml + - grafana-contactpoint.yaml diff --git a/main.go b/main.go index 509e3e122..c5bc5ce26 100644 --- a/main.go +++ b/main.go @@ -234,6 +234,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "GrafanaAlertRuleGroup") os.Exit(1) } + if err = (&controllers.GrafanaContactPointReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "GrafanaContactPoint") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/tests/e2e/example-test/09-assert.yaml b/tests/e2e/example-test/09-assert.yaml new file mode 100644 index 000000000..911058fcf --- /dev/null +++ b/tests/e2e/example-test/09-assert.yaml @@ -0,0 +1,8 @@ +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaContactPoint +metadata: + name: test +status: + conditions: + - type: ContactPointSynchronized + status: "True" diff --git a/tests/e2e/example-test/09-contactpoint.yaml b/tests/e2e/example-test/09-contactpoint.yaml new file mode 100644 index 000000000..92c3f1334 --- /dev/null +++ b/tests/e2e/example-test/09-contactpoint.yaml @@ -0,0 +1,12 @@ +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaContactPoint +metadata: + name: test +spec: + name: test + type: "email" + instanceSelector: + matchLabels: + dashboards: "grafana" + settings: + addresses: "email@email.com" diff --git a/tests/e2e/example-test/chainsaw-test.yaml b/tests/e2e/example-test/chainsaw-test.yaml index f15f819c7..6610e3355 100755 --- a/tests/e2e/example-test/chainsaw-test.yaml +++ b/tests/e2e/example-test/chainsaw-test.yaml @@ -59,3 +59,9 @@ spec: file: 08-alert-rule-group.yaml - assert: file: 08-assert.yaml + - name: step-09 + try: + - apply: + file: 09-contactpoint.yaml + - assert: + file: 09-assert.yaml