diff --git a/PROJECT b/PROJECT index 3e7f000..1c75ea8 100644 --- a/PROJECT +++ b/PROJECT @@ -35,4 +35,21 @@ resources: kind: QueryConnector path: prosimcorp.com/SearchRuler/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: prosimcorp.com + group: searchruler + kind: ClusterQueryConnector + path: prosimcorp.com/SearchRuler/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + domain: prosimcorp.com + group: searchruler + kind: ClusterRulerAction + path: prosimcorp.com/SearchRuler/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/README.md b/README.md index 77dbf1c..535533a 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ Well, no more! searchruler is here to save the day. This Kubernetes operator let ### 🛠️ How It Works Setting up searchruler is a breeze. Here are the three main building blocks that’ll make your log life so much easier: -* 🔗 **QueryConnector**: This is where the magic starts. Connect to your log source—whether it’s Elasticsearch, Opensearch, or something cool we’re cooking up for the future. +* 🔗 **QueryConnector**: This is where the magic starts. Connect to your log source—whether it’s Elasticsearch, Opensearch, or something cool we’re cooking up for the future. The clustered scope solution is named **ClusterQueryConnector**. -* 🚀 **RulerAction**: When a rule is triggered, where should the alert go? Set up webhooks, Slack channels, or anything else you need. We keep it simple, starting with a generic webhook (because everyone loves webhooks). +* 🚀 **RulerAction**: When a rule is triggered, where should the alert go? Set up webhooks, Slack channels, or anything else you need. We keep it simple, starting with a generic webhook (because everyone loves webhooks). The clustered scope solution is named **ClusterRulerAction**. * 📜 **SearchRule**: The heart of it all! Define your rules, set the conditions, and craft the message to send when something’s off. This is where you turn log data into actionable alerts. @@ -112,9 +112,12 @@ spec: secretRef: name: elasticsearch-main-credentials + namespace: default keyUsername: username keyPassword: password ``` + +For cluster scope just change **QueryConnector** for **ClusterQueryConenctor**. ### 🚀 RulerAction A RulerAction defines where your alerts will be sent when a SearchRule is triggered (a.k.a. "firing"). Whether it’s a Slack channel, a webhook endpoint, alertmanager or another notification service—you’re in control! 🛠️ @@ -156,10 +159,12 @@ spec: # credentials: # secretRef: # name: alertmanager-credentials + # namespace: default # keyUsername: username # keyPassword: password ``` +For cluster scope just change **QueryConnector** for **ClusterRulerAction**. ### 📜 SearchRule This is where the magic happens! SearchRules define the conditions to check in your log sources (via queryconnectors) and specify where to send alerts (using ruleractions). You get to decide what matters and how to act on it. 🎯 @@ -185,6 +190,8 @@ spec: # QueryConnector reference to execute the queries for the rule evaluation. queryConnectorRef: name: queryconnector-sample + # Empty namespace it searchs for a clusterqueryconnector resource + namespace: "default" # Interval time for checking the value of the query. For example, every 30s we will # execute the query value to elasticsearch @@ -253,6 +260,8 @@ spec: # RuleAction reference to execute when the condition is true. actionRef: name: ruleraction-sample + # Empty namespace it searchs for a clusterruleraction resource + namespace: "default" # Message template to send in the RuleAction execution. It is a Go template with the # object, value and, if exists, elasticsearch aggregations field variables. The object # variable is the SearchRule object and the value variable is the value of the conditionField. @@ -356,6 +365,7 @@ spec: # QueryConnector reference to execute the queries for the rule evaluation. queryConnectorRef: name: queryconnector-sample + namespace: default # Interval time for checking the value of the query. For example, every 30s we will # execute the query value to elasticsearch @@ -398,6 +408,7 @@ spec: # RuleAction reference to execute when the condition is true. actionRef: name: ruleraction-sample + namespace: default # Message template to send in the RuleAction execution. It is a Go template with the # object, value and, if exists, elasticsearch aggregations field variables. The object # variable is the SearchRule object and the value variable is the value of the conditionField. diff --git a/api/v1alpha1/clusterqueryconnector_types.go b/api/v1alpha1/clusterqueryconnector_types.go new file mode 100644 index 0000000..e3188c3 --- /dev/null +++ b/api/v1alpha1/clusterqueryconnector_types.go @@ -0,0 +1,50 @@ +/* +Copyright 2024. + +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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"ResourceSynced\")].status",description="" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"State\")].reason",description="" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" + +// ClusterQueryConnector is the Schema for the clusterqueryconnectors API. +type ClusterQueryConnector struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec QueryConnectorSpec `json:"spec,omitempty"` + Status QueryConnectorStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterQueryConnectorList contains a list of ClusterQueryConnector. +type ClusterQueryConnectorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterQueryConnector `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClusterQueryConnector{}, &ClusterQueryConnectorList{}) +} diff --git a/api/v1alpha1/clusterruleraction_types.go b/api/v1alpha1/clusterruleraction_types.go new file mode 100644 index 0000000..6e03a24 --- /dev/null +++ b/api/v1alpha1/clusterruleraction_types.go @@ -0,0 +1,50 @@ +/* +Copyright 2024. + +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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"ResourceSynced\")].status",description="" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"State\")].reason",description="" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" + +// ClusterRulerAction is the Schema for the clusterruleractions API. +type ClusterRulerAction struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RulerActionSpec `json:"spec,omitempty"` + Status RulerActionStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterRulerActionList contains a list of ClusterRulerAction. +type ClusterRulerActionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterRulerAction `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClusterRulerAction{}, &ClusterRulerActionList{}) +} diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go index 73bbd3e..331b219 100644 --- a/api/v1alpha1/common_types.go +++ b/api/v1alpha1/common_types.go @@ -3,6 +3,7 @@ package v1alpha1 // SecretRef TODO type SecretRef struct { Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` KeyUsername string `json:"keyUsername"` KeyPassword string `json:"keyPassword"` } diff --git a/api/v1alpha1/searchrule_types.go b/api/v1alpha1/searchrule_types.go index 1392090..052fe5b 100644 --- a/api/v1alpha1/searchrule_types.go +++ b/api/v1alpha1/searchrule_types.go @@ -40,13 +40,15 @@ type Condition struct { // ActionRef TODO type ActionRef struct { - Name string `json:"name"` - Data string `json:"data,omitempty"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Data string `json:"data"` } // QueryConnectorRef TODO type QueryConnectorRef struct { - Name string `json:"name"` + Name string `json:"name"` + Namespace string `json:"namespace"` } // SearchRuleSpec defines the desired state of SearchRule. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8d80e3e..29b3681 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,124 @@ func (in *ActionRef) DeepCopy() *ActionRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterQueryConnector) DeepCopyInto(out *ClusterQueryConnector) { + *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 ClusterQueryConnector. +func (in *ClusterQueryConnector) DeepCopy() *ClusterQueryConnector { + if in == nil { + return nil + } + out := new(ClusterQueryConnector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterQueryConnector) 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 *ClusterQueryConnectorList) DeepCopyInto(out *ClusterQueryConnectorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterQueryConnector, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterQueryConnectorList. +func (in *ClusterQueryConnectorList) DeepCopy() *ClusterQueryConnectorList { + if in == nil { + return nil + } + out := new(ClusterQueryConnectorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterQueryConnectorList) 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 *ClusterRulerAction) DeepCopyInto(out *ClusterRulerAction) { + *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 ClusterRulerAction. +func (in *ClusterRulerAction) DeepCopy() *ClusterRulerAction { + if in == nil { + return nil + } + out := new(ClusterRulerAction) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterRulerAction) 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 *ClusterRulerActionList) DeepCopyInto(out *ClusterRulerActionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterRulerAction, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRulerActionList. +func (in *ClusterRulerActionList) DeepCopy() *ClusterRulerActionList { + if in == nil { + return nil + } + out := new(ClusterRulerActionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterRulerActionList) 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 *Condition) DeepCopyInto(out *Condition) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index b63e66d..f1b4f81 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -21,6 +21,9 @@ import ( "crypto/tls" "flag" "os" + "prosimcorp.com/SearchRuler/internal/controller/queryconnector" + "prosimcorp.com/SearchRuler/internal/controller/ruleraction" + "prosimcorp.com/SearchRuler/internal/controller/searchrule" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -37,7 +40,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" searchrulerv1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" - "prosimcorp.com/SearchRuler/internal/controller" "prosimcorp.com/SearchRuler/internal/globals" "prosimcorp.com/SearchRuler/internal/pools" "prosimcorp.com/SearchRuler/internal/webserver" @@ -177,7 +179,7 @@ func main() { os.Exit(1) } - if err = (&controller.RulerActionReconciler{ + if err = (&ruleraction.RulerActionReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), AlertsPool: AlertsPool, @@ -186,7 +188,7 @@ func main() { os.Exit(1) } mgr.GetEventRecorderFor("CREATE") - if err = (&controller.SearchRuleReconciler{ + if err = (&searchrule.SearchRuleReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), QueryConnectorCredentialsPool: QueryConnectorCredentialsPool, @@ -196,7 +198,7 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "SearchRule") os.Exit(1) } - if err = (&controller.QueryConnectorReconciler{ + if err = (&queryconnector.QueryConnectorReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), CredentialsPool: QueryConnectorCredentialsPool, diff --git a/config/crd/bases/searchruler.prosimcorp.com_clusterqueryconnectors.yaml b/config/crd/bases/searchruler.prosimcorp.com_clusterqueryconnectors.yaml new file mode 100644 index 0000000..41d6397 --- /dev/null +++ b/config/crd/bases/searchruler.prosimcorp.com_clusterqueryconnectors.yaml @@ -0,0 +1,154 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: clusterqueryconnectors.searchruler.prosimcorp.com +spec: + group: searchruler.prosimcorp.com + names: + kind: ClusterQueryConnector + listKind: ClusterQueryConnectorList + plural: clusterqueryconnectors + singular: clusterqueryconnector + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="ResourceSynced")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="State")].reason + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterQueryConnector is the Schema for the clusterqueryconnectors + 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: QueryConnectorSpec defines the desired state of QueryConnector. + properties: + credentials: + description: QueryConnectorCredentials TODO + properties: + secretRef: + description: SecretRef TODO + properties: + keyPassword: + type: string + keyUsername: + type: string + name: + type: string + namespace: + type: string + required: + - keyPassword + - keyUsername + - name + type: object + syncInterval: + type: string + required: + - secretRef + type: object + headers: + additionalProperties: + type: string + type: object + tlsSkipVerify: + type: boolean + url: + type: string + required: + - url + type: object + status: + description: QueryConnectorStatus defines the observed state of QueryConnector. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + 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. + 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/bases/searchruler.prosimcorp.com_clusterruleractions.yaml b/config/crd/bases/searchruler.prosimcorp.com_clusterruleractions.yaml new file mode 100644 index 0000000..08a663e --- /dev/null +++ b/config/crd/bases/searchruler.prosimcorp.com_clusterruleractions.yaml @@ -0,0 +1,163 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: clusterruleractions.searchruler.prosimcorp.com +spec: + group: searchruler.prosimcorp.com + names: + kind: ClusterRulerAction + listKind: ClusterRulerActionList + plural: clusterruleractions + singular: clusterruleraction + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="ResourceSynced")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="State")].reason + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterRulerAction is the Schema for the clusterruleractions + 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: RulerActionSpec defines the desired state of RulerAction. + properties: + webhook: + description: WebHook TODO + properties: + credentials: + description: RulerActionCredentials TODO + properties: + secretRef: + description: SecretRef TODO + properties: + keyPassword: + type: string + keyUsername: + type: string + name: + type: string + namespace: + type: string + required: + - keyPassword + - keyUsername + - name + type: object + required: + - secretRef + type: object + headers: + additionalProperties: + type: string + type: object + tlsSkipVerify: + type: boolean + url: + type: string + validator: + type: string + verb: + type: string + required: + - url + - verb + type: object + required: + - webhook + type: object + status: + description: RulerActionStatus defines the observed state of RulerAction. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + 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. + 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/bases/searchruler.prosimcorp.com_queryconnectors.yaml b/config/crd/bases/searchruler.prosimcorp.com_queryconnectors.yaml index 379d9ab..43f6622 100644 --- a/config/crd/bases/searchruler.prosimcorp.com_queryconnectors.yaml +++ b/config/crd/bases/searchruler.prosimcorp.com_queryconnectors.yaml @@ -61,6 +61,8 @@ spec: type: string name: type: string + namespace: + type: string required: - keyPassword - keyUsername diff --git a/config/crd/bases/searchruler.prosimcorp.com_ruleractions.yaml b/config/crd/bases/searchruler.prosimcorp.com_ruleractions.yaml index 0961366..dcdfbc7 100644 --- a/config/crd/bases/searchruler.prosimcorp.com_ruleractions.yaml +++ b/config/crd/bases/searchruler.prosimcorp.com_ruleractions.yaml @@ -64,6 +64,8 @@ spec: type: string name: type: string + namespace: + type: string required: - keyPassword - keyUsername diff --git a/config/crd/bases/searchruler.prosimcorp.com_searchrules.yaml b/config/crd/bases/searchruler.prosimcorp.com_searchrules.yaml index 4674fde..043e557 100644 --- a/config/crd/bases/searchruler.prosimcorp.com_searchrules.yaml +++ b/config/crd/bases/searchruler.prosimcorp.com_searchrules.yaml @@ -56,8 +56,12 @@ spec: type: string name: type: string + namespace: + type: string required: + - data - name + - namespace type: object checkInterval: type: string @@ -97,8 +101,11 @@ spec: properties: name: type: string + namespace: + type: string required: - name + - namespace type: object required: - actionRef diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 58dc4b2..07c57f2 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -5,6 +5,8 @@ resources: - bases/searchruler.prosimcorp.com_ruleractions.yaml - bases/searchruler.prosimcorp.com_searchrules.yaml - bases/searchruler.prosimcorp.com_queryconnectors.yaml +- bases/searchruler.prosimcorp.com_clusterqueryconnectors.yaml +- bases/searchruler.prosimcorp.com_clusterruleractions.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/config/rbac/clusterqueryconnector_editor_role.yaml b/config/rbac/clusterqueryconnector_editor_role.yaml new file mode 100644 index 0000000..cc89ea6 --- /dev/null +++ b/config/rbac/clusterqueryconnector_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit clusterqueryconnectors. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: search-ruler + app.kubernetes.io/managed-by: kustomize + name: clusterqueryconnector-editor-role +rules: +- apiGroups: + - searchruler.prosimcorp.com + resources: + - clusterqueryconnectors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - searchruler.prosimcorp.com + resources: + - clusterqueryconnectors/status + verbs: + - get diff --git a/config/rbac/clusterqueryconnector_viewer_role.yaml b/config/rbac/clusterqueryconnector_viewer_role.yaml new file mode 100644 index 0000000..0b63d57 --- /dev/null +++ b/config/rbac/clusterqueryconnector_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view clusterqueryconnectors. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: search-ruler + app.kubernetes.io/managed-by: kustomize + name: clusterqueryconnector-viewer-role +rules: +- apiGroups: + - searchruler.prosimcorp.com + resources: + - clusterqueryconnectors + verbs: + - get + - list + - watch +- apiGroups: + - searchruler.prosimcorp.com + resources: + - clusterqueryconnectors/status + verbs: + - get diff --git a/config/rbac/clusterruleraction_editor_role.yaml b/config/rbac/clusterruleraction_editor_role.yaml new file mode 100644 index 0000000..22fe713 --- /dev/null +++ b/config/rbac/clusterruleraction_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit clusterruleractions. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: search-ruler + app.kubernetes.io/managed-by: kustomize + name: clusterruleraction-editor-role +rules: +- apiGroups: + - searchruler.prosimcorp.com + resources: + - clusterruleractions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - searchruler.prosimcorp.com + resources: + - clusterruleractions/status + verbs: + - get diff --git a/config/rbac/clusterruleraction_viewer_role.yaml b/config/rbac/clusterruleraction_viewer_role.yaml new file mode 100644 index 0000000..bfbb586 --- /dev/null +++ b/config/rbac/clusterruleraction_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view clusterruleractions. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: search-ruler + app.kubernetes.io/managed-by: kustomize + name: clusterruleraction-viewer-role +rules: +- apiGroups: + - searchruler.prosimcorp.com + resources: + - clusterruleractions + verbs: + - get + - list + - watch +- apiGroups: + - searchruler.prosimcorp.com + resources: + - clusterruleractions/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index acc9672..2c7992e 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -22,6 +22,10 @@ resources: # default, aiding admins in cluster management. Those roles are # not used by the Project itself. You can comment the following lines # if you do not want those helpers be installed with your Project. +- clusterruleraction_editor_role.yaml +- clusterruleraction_viewer_role.yaml +- clusterqueryconnector_editor_role.yaml +- clusterqueryconnector_viewer_role.yaml - queryconnector_editor_role.yaml - queryconnector_viewer_role.yaml - searchrule_editor_role.yaml diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 4582147..6d23d43 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -3,4 +3,6 @@ resources: - searchruler_v1alpha1_ruleraction.yaml - searchruler_v1alpha1_searchrule.yaml - searchruler_v1alpha1_queryconnector.yaml +- searchruler_v1alpha1_clusterqueryconnector.yaml +- searchruler_v1alpha1_clusterruleraction.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/searchruler_v1alpha1_clusterqueryconnector.yaml b/config/samples/searchruler_v1alpha1_clusterqueryconnector.yaml new file mode 100644 index 0000000..c19f57f --- /dev/null +++ b/config/samples/searchruler_v1alpha1_clusterqueryconnector.yaml @@ -0,0 +1,30 @@ +apiVersion: searchruler.prosimcorp.com/v1alpha1 +kind: ClusterQueryConnector +metadata: + labels: + app.kubernetes.io/name: search-ruler + app.kubernetes.io/managed-by: kustomize + name: clusterqueryconnector-sample +spec: + + # URL for the query connector. We will execute the queries in this URL + url: "https://127.0.0.1:9200" + + # Additional headers if needed for the connection + headers: {} + + # Skip certificate verification if the connection is HTTPS + tlsSkipVerify: true + + # Secret reference to get the credentials if needed for the connection + credentials: + + # Interval to check secret credentials for any changes + # Default value is 1m + #syncInterval: 1m + + secretRef: + name: elasticsearch-main-credentials + namespace: default + keyUsername: username + keyPassword: password diff --git a/config/samples/searchruler_v1alpha1_clusterruleraction.yaml b/config/samples/searchruler_v1alpha1_clusterruleraction.yaml new file mode 100644 index 0000000..4e71d92 --- /dev/null +++ b/config/samples/searchruler_v1alpha1_clusterruleraction.yaml @@ -0,0 +1,38 @@ +apiVersion: searchruler.prosimcorp.com/v1alpha1 +kind: ClusterRulerAction +metadata: + labels: + app.kubernetes.io/name: search-ruler + app.kubernetes.io/managed-by: kustomize + name: clusterruleraction-sample +spec: + + # Webhook integration configuration to send alerts. + # Note: The webhook integration is the only one implemented yet. + webhook: + + # URL to send the webhook message + url: http://127.0.0.1:8080 + + # HTTP method to send the webhook message + verb: POST + + # Skip certificate verification if the connection is HTTPS + tlsSkipVerify: false + + # Additional headers if needed for the connection + headers: {} + + # Validator configuration to validate the response of the webhook + # Just alertmanager validation available yet. + # If you use alertmanager validator, message data must be in alertmanager format: + # https://prometheus.io/docs/alerting/latest/clients/ + # validator: alertmanager + + # Credentials to authenticate in the webhook if needed + # credentials: + # secretRef: + # name: alertmanager-credentials + # namespace: default + # keyUsername: username + # keyPassword: password diff --git a/config/samples/searchruler_v1alpha1_queryconnector.yaml b/config/samples/searchruler_v1alpha1_queryconnector.yaml index c442fb7..118ca7f 100644 --- a/config/samples/searchruler_v1alpha1_queryconnector.yaml +++ b/config/samples/searchruler_v1alpha1_queryconnector.yaml @@ -25,5 +25,6 @@ spec: secretRef: name: elasticsearch-main-credentials + namespace: default keyUsername: username keyPassword: password diff --git a/config/samples/searchruler_v1alpha1_ruleraction.yaml b/config/samples/searchruler_v1alpha1_ruleraction.yaml index 641acea..801e8c7 100644 --- a/config/samples/searchruler_v1alpha1_ruleraction.yaml +++ b/config/samples/searchruler_v1alpha1_ruleraction.yaml @@ -33,5 +33,6 @@ spec: # credentials: # secretRef: # name: alertmanager-credentials + # namespace: default # keyUsername: username # keyPassword: password diff --git a/config/samples/searchruler_v1alpha1_searchrule.yaml b/config/samples/searchruler_v1alpha1_searchrule.yaml index 4b3d070..e1558cb 100644 --- a/config/samples/searchruler_v1alpha1_searchrule.yaml +++ b/config/samples/searchruler_v1alpha1_searchrule.yaml @@ -13,7 +13,8 @@ spec: # QueryConnector reference to execute the queries for the rule evaluation. queryConnectorRef: - name: queryconnector-sample + name: clusterqueryconnector-sample + namespace: "" # Interval time for checking the value of the query. For example, every 30s we will # execute the query value to elasticsearch @@ -100,6 +101,7 @@ spec: # RuleAction reference to execute when the condition is true. actionRef: name: ruleraction-sample + namespace: "" # Message template to send in the RuleAction execution. It is a Go template with the # object, value and, if exists, elasticsearch aggregations field variables. The object # variable is the SearchRule object and the value variable is the value of the conditionField. diff --git a/docs/.DS_Store b/docs/.DS_Store new file mode 100644 index 0000000..00f0bb9 Binary files /dev/null and b/docs/.DS_Store differ diff --git a/go.mod b/go.mod index 9f94811..dbf4b98 100644 --- a/go.mod +++ b/go.mod @@ -95,7 +95,7 @@ require ( go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect diff --git a/go.sum b/go.sum index bdc457b..a69e2b9 100644 --- a/go.sum +++ b/go.sum @@ -215,8 +215,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/internal/controller/commons.go b/internal/controller/commons.go index fb9c8e5..bb5e411 100644 --- a/internal/controller/commons.go +++ b/internal/controller/commons.go @@ -3,21 +3,22 @@ package controller const ( // Resource types - SearchRuleResourceType = "SearchRule" - RulerActionResourceType = "RulerAction" - QueryConnectorResourceType = "QueryConnector" + SearchRuleResourceType = "SearchRule" + RulerActionResourceType = "RulerAction" + QueryConnectorResourceType = "QueryConnector" + ClusterQueryConnectorResourceType = "ClusterQueryConnector" + ClusterRulerActionResourceType = "ClusterRulerAction" // Sync interval to check if secrets of SearchRuleAction and SearchRuleQueryConnector are up to date - defaultSyncInterval = "1m" + DefaultSyncInterval = "1m" // Error messages - resourceNotFoundError = "%s '%s' resource not found. Ignoring since object must be deleted." - resourceRetrievalError = "Error getting the %s '%s' from the cluster: %s" - resourceTargetsDeleteError = "Failed to delete targets of %s '%s': %s" - resourceFinalizersUpdateError = "Failed to update finalizer of %s '%s': %s" - resourceConditionUpdateError = "Failed to update the condition on %s '%s': %s" - resourceSyncTimeRetrievalError = "can not get synchronization time from the %s '%s': %s" - syncTargetError = "can not sync the target for the %s '%s': %s" + ResourceNotFoundError = "%s '%s' resource not found. Ignoring since object must be deleted." + CanNotGetResourceError = "%s '%s' resource not found. Error: %v" + ResourceFinalizersUpdateError = "Failed to update finalizer of %s '%s': %s" + ResourceConditionUpdateError = "Failed to update the condition on %s '%s': %s" + ResourceSyncTimeRetrievalError = "can not get synchronization time from the %s '%s': %s" + SyncTargetError = "can not sync the target for the %s '%s': %s" ValidatorNotFoundErrorMessage = "validator %s not found" ValidationFailedErrorMessage = "validation failed: %s" HttpRequestCreationErrorMessage = "error creating http request: %s" @@ -25,7 +26,6 @@ const ( AlertFiringInfoMessage = "alert firing for searchRule with namespaced name %s/%s. Description: %s" SecretNotFoundErrorMessage = "error fetching secret %s: %v" MissingCredentialsMessage = "missing credentials in secret %s" - GetRulerActionErrorMessage = "error getting RulerAction from event: %v" EvaluateTemplateErrorMessage = "error evaluating template message: %v" AlertsPoolErrorMessage = "error getting alerts pool: %v" QueryConnectorNotFoundMessage = "queryConnector %s not found in the resource namespace %s" @@ -41,8 +41,5 @@ const ( KubeEventCreationErrorMessage = "error creating kube event: %v" // Finalizer - resourceFinalizer = "searchruler.prosimcorp.com/finalizer" - - // HTTP event pattern - HttpEventPattern = `{"data":"%s","timestamp":"%s"}` + ResourceFinalizer = "searchruler.prosimcorp.com/finalizer" ) diff --git a/internal/controller/queryconnector/controller.go b/internal/controller/queryconnector/controller.go new file mode 100644 index 0000000..6af4902 --- /dev/null +++ b/internal/controller/queryconnector/controller.go @@ -0,0 +1,215 @@ +/* +Copyright 2024. + +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 queryconnector + +import ( + "context" + "fmt" + "reflect" + "time" + + // + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + 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/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + // + searchrulerv1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" + "prosimcorp.com/SearchRuler/internal/controller" + "prosimcorp.com/SearchRuler/internal/pools" +) + +// QueryConnectorReconciler reconciles a QueryConnector object +type QueryConnectorReconciler struct { + client.Client + Scheme *runtime.Scheme + CredentialsPool *pools.CredentialsStore +} + +type CompoundQueryConnectorResource struct { + QueryConnectorResource *searchrulerv1alpha1.QueryConnector + ClusterQueryConnectorResource *searchrulerv1alpha1.ClusterQueryConnector +} + +var ( + resourceType string + containsFinalizer bool +) + +// +kubebuilder:rbac:groups=searchruler.prosimcorp.com,resources=queryconnectors,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=searchruler.prosimcorp.com,resources=queryconnectors/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=searchruler.prosimcorp.com,resources=queryconnectors/finalizers,verbs=update + +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/reconcile +func (r *QueryConnectorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { + logger := log.FromContext(ctx) + + // 1. Get the content of the Patch + CompoundQueryConnectorResource := &CompoundQueryConnectorResource{ + QueryConnectorResource: &searchrulerv1alpha1.QueryConnector{}, + ClusterQueryConnectorResource: &searchrulerv1alpha1.ClusterQueryConnector{}, + } + switch req.Namespace { + case "": + resourceType = controller.ClusterQueryConnectorResourceType + err = r.Get(ctx, req.NamespacedName, CompoundQueryConnectorResource.ClusterQueryConnectorResource) + default: + resourceType = controller.QueryConnectorResourceType + err = r.Get(ctx, req.NamespacedName, CompoundQueryConnectorResource.QueryConnectorResource) + } + + // 2. Check existence on the cluster + if err != nil { + + // 2.1 It does NOT exist: manage removal + if err = client.IgnoreNotFound(err); err == nil { + logger.Info(fmt.Sprintf(controller.ResourceNotFoundError, resourceType, req.NamespacedName)) + return result, err + } + + // 2.2 Failed to get the resource, requeue the request + logger.Info(fmt.Sprintf(controller.ResourceSyncTimeRetrievalError, resourceType, req.NamespacedName, err.Error())) + return result, err + } + + // 3. Check if the SearchRule instance is marked to be deleted: indicated by the deletion timestamp being set + deletionTimestamp := &v1.Time{} + switch resourceType { + case controller.ClusterQueryConnectorResourceType: + deletionTimestamp = CompoundQueryConnectorResource.ClusterQueryConnectorResource.DeletionTimestamp + containsFinalizer = controllerutil.ContainsFinalizer(CompoundQueryConnectorResource.ClusterQueryConnectorResource, controller.ResourceFinalizer) + default: + deletionTimestamp = CompoundQueryConnectorResource.QueryConnectorResource.DeletionTimestamp + containsFinalizer = controllerutil.ContainsFinalizer(CompoundQueryConnectorResource.QueryConnectorResource, controller.ResourceFinalizer) + } + if !deletionTimestamp.IsZero() { + if containsFinalizer { + + // 3.1 Delete the resources associated with the QueryConnector + err = r.Sync(ctx, watch.Deleted, CompoundQueryConnectorResource, resourceType) + + // Remove the finalizers on Patch CR + switch resourceType { + case controller.ClusterQueryConnectorResourceType: + controllerutil.RemoveFinalizer(CompoundQueryConnectorResource.ClusterQueryConnectorResource, controller.ResourceFinalizer) + err = r.Update(ctx, CompoundQueryConnectorResource.ClusterQueryConnectorResource) + default: + controllerutil.RemoveFinalizer(CompoundQueryConnectorResource.QueryConnectorResource, controller.ResourceFinalizer) + err = r.Update(ctx, CompoundQueryConnectorResource.QueryConnectorResource) + } + if err != nil { + logger.Info(fmt.Sprintf(controller.ResourceFinalizersUpdateError, resourceType, req.NamespacedName, err.Error())) + } + } + + result = ctrl.Result{} + err = nil + return result, err + } + + // 4. Add finalizer to the SearchRule CR + if !containsFinalizer { + switch resourceType { + case controller.ClusterQueryConnectorResourceType: + controllerutil.AddFinalizer(CompoundQueryConnectorResource.ClusterQueryConnectorResource, controller.ResourceFinalizer) + err = r.Update(ctx, CompoundQueryConnectorResource.ClusterQueryConnectorResource) + default: + controllerutil.AddFinalizer(CompoundQueryConnectorResource.QueryConnectorResource, controller.ResourceFinalizer) + err = r.Update(ctx, CompoundQueryConnectorResource.QueryConnectorResource) + } + if err != nil { + return result, err + } + } + + // 5. Update the status before the requeue + defer func() { + switch resourceType { + case controller.ClusterQueryConnectorResourceType: + err = r.Status().Update(ctx, CompoundQueryConnectorResource.ClusterQueryConnectorResource) + default: + err = r.Status().Update(ctx, CompoundQueryConnectorResource.QueryConnectorResource) + } + if err != nil { + logger.Info(fmt.Sprintf(controller.ResourceConditionUpdateError, resourceType, req.NamespacedName, err.Error())) + } + }() + + // 6. Schedule periodical request + syncInterval := controller.DefaultSyncInterval + switch resourceType { + case controller.ClusterQueryConnectorResourceType: + if !reflect.ValueOf(CompoundQueryConnectorResource.ClusterQueryConnectorResource.Spec.Credentials.SyncInterval).IsZero() { + syncInterval = CompoundQueryConnectorResource.ClusterQueryConnectorResource.Spec.Credentials.SyncInterval + } + default: + if !reflect.ValueOf(CompoundQueryConnectorResource.QueryConnectorResource.Spec.Credentials.SyncInterval).IsZero() { + syncInterval = CompoundQueryConnectorResource.QueryConnectorResource.Spec.Credentials.SyncInterval + } + } + + RequeueTime, err := time.ParseDuration(syncInterval) + if err != nil { + logger.Info(fmt.Sprintf(controller.ResourceSyncTimeRetrievalError, resourceType, req.NamespacedName, err.Error())) + return result, err + } + result = ctrl.Result{ + RequeueAfter: RequeueTime, + } + + // 7. Sync credentials if defined + credentials := CompoundQueryConnectorResource.QueryConnectorResource.Spec.Credentials + if resourceType == controller.ClusterQueryConnectorResourceType { + credentials = CompoundQueryConnectorResource.ClusterQueryConnectorResource.Spec.Credentials + } + + if !reflect.ValueOf(credentials).IsZero() { + err = r.Sync(ctx, watch.Modified, CompoundQueryConnectorResource, resourceType) + if err != nil { + r.UpdateConditionKubernetesApiCallFailure(CompoundQueryConnectorResource, resourceType) + logger.Info(fmt.Sprintf(controller.SyncTargetError, resourceType, req.NamespacedName, err.Error())) + return result, err + } + } + + // 8. Success, update the status + r.UpdateConditionSuccess(CompoundQueryConnectorResource, resourceType) + + return result, err +} + +// SetupWithManager sets up the controller with the Manager. +func (r *QueryConnectorReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&searchrulerv1alpha1.QueryConnector{}). + WithEventFilter(predicate.GenerationChangedPredicate{}). + Named("QueryConnector"). + Watches(&searchrulerv1alpha1.ClusterQueryConnector{}, &handler.EnqueueRequestForObject{}). + Complete(r) +} diff --git a/internal/controller/queryconnector_status.go b/internal/controller/queryconnector/status.go similarity index 50% rename from internal/controller/queryconnector_status.go rename to internal/controller/queryconnector/status.go index 916d071..c926637 100644 --- a/internal/controller/queryconnector_status.go +++ b/internal/controller/queryconnector/status.go @@ -14,55 +14,76 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package queryconnector import ( - "prosimcorp.com/SearchRuler/internal/globals" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" + + // + "prosimcorp.com/SearchRuler/internal/controller" + "prosimcorp.com/SearchRuler/internal/globals" ) -// UpdateConditionSuccess updates the status of the QueryConnector resource with a success condition -func (r *QueryConnectorReconciler) UpdateConditionSuccess(QueryConnector *v1alpha1.QueryConnector) { +// UpdateConditionSuccess updates the status of the resource with a success condition +func (r *QueryConnectorReconciler) UpdateConditionSuccess(resource *CompoundQueryConnectorResource, resourceType string) { // Create the new condition with the success status condition := globals.NewCondition(globals.ConditionTypeResourceSynced, metav1.ConditionTrue, globals.ConditionReasonTargetSynced, globals.ConditionReasonTargetSyncedMessage) // Update the status of the QueryConnector resource - globals.UpdateCondition(&QueryConnector.Status.Conditions, condition) + switch resourceType { + case controller.ClusterQueryConnectorResourceType: + globals.UpdateCondition(&resource.ClusterQueryConnectorResource.Status.Conditions, condition) + default: + globals.UpdateCondition(&resource.QueryConnectorResource.Status.Conditions, condition) + } } -// UpdateConditionKubernetesApiCallFailure updates the status of the QueryConnector resource with a failure condition -func (r *QueryConnectorReconciler) UpdateConditionKubernetesApiCallFailure(QueryConnector *v1alpha1.QueryConnector) { +// UpdateConditionKubernetesApiCallFailure updates the status of the resource with a failure condition +func (r *QueryConnectorReconciler) UpdateConditionKubernetesApiCallFailure(resource *CompoundQueryConnectorResource, resourceType string) { // Create the new condition with the failure status condition := globals.NewCondition(globals.ConditionTypeResourceSynced, metav1.ConditionTrue, globals.ConditionReasonKubernetesApiCallErrorType, globals.ConditionReasonKubernetesApiCallErrorMessage) // Update the status of the QueryConnector resource - globals.UpdateCondition(&QueryConnector.Status.Conditions, condition) + switch resourceType { + case controller.ClusterQueryConnectorResourceType: + globals.UpdateCondition(&resource.ClusterQueryConnectorResource.Status.Conditions, condition) + default: + globals.UpdateCondition(&resource.QueryConnectorResource.Status.Conditions, condition) + } } -// UpdateStateSuccess updates the status of the QueryConnector resource with a Success condition -func (r *QueryConnectorReconciler) UpdateStateSuccess(QueryConnector *v1alpha1.QueryConnector) { +// UpdateStateSuccess updates the status of the resource with a Success condition +func (r *QueryConnectorReconciler) UpdateStateSuccess(resource *CompoundQueryConnectorResource, resourceType string) { // Create the new condition with the success status condition := globals.NewCondition(globals.ConditionTypeState, metav1.ConditionTrue, globals.ConditionReasonStateSuccessType, globals.ConditionReasonStateSuccessMessage) // Update the status of the QueryConnector resource - globals.UpdateCondition(&QueryConnector.Status.Conditions, condition) + switch resourceType { + case controller.ClusterQueryConnectorResourceType: + globals.UpdateCondition(&resource.ClusterQueryConnectorResource.Status.Conditions, condition) + default: + globals.UpdateCondition(&resource.QueryConnectorResource.Status.Conditions, condition) + } } -// UpdateConditionNoCredsFound updates the status of the QueryConnector resource with a NoCreds condition -func (r *QueryConnectorReconciler) UpdateConditionNoCredsFound(QueryConnector *v1alpha1.QueryConnector) { +// UpdateConditionNoCredsFound updates the status of the resource with a NoCreds condition +func (r *QueryConnectorReconciler) UpdateConditionNoCredsFound(resource *CompoundQueryConnectorResource, resourceType string) { // Create the new condition with the success status condition := globals.NewCondition(globals.ConditionTypeState, metav1.ConditionTrue, globals.ConditionReasonNoCredsFoundType, globals.ConditionReasonNoCredsFoundMessage) // Update the status of the QueryConnector resource - globals.UpdateCondition(&QueryConnector.Status.Conditions, condition) + switch resourceType { + case controller.ClusterQueryConnectorResourceType: + globals.UpdateCondition(&resource.ClusterQueryConnectorResource.Status.Conditions, condition) + default: + globals.UpdateCondition(&resource.QueryConnectorResource.Status.Conditions, condition) + } } diff --git a/internal/controller/queryconnector_sync.go b/internal/controller/queryconnector/sync.go similarity index 55% rename from internal/controller/queryconnector_sync.go rename to internal/controller/queryconnector/sync.go index 67120bd..dea0ed8 100644 --- a/internal/controller/queryconnector_sync.go +++ b/internal/controller/queryconnector/sync.go @@ -14,27 +14,49 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package queryconnector import ( "context" "fmt" + // v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" + + // "prosimcorp.com/SearchRuler/api/v1alpha1" + "prosimcorp.com/SearchRuler/internal/controller" "prosimcorp.com/SearchRuler/internal/pools" ) +var ( + resourceNamespace string + resourceName string + resourceSpec v1alpha1.QueryConnectorSpec +) + // Sync function is used to synchronize the QueryConnector resource with the credentials. Adds the credentials to the // credentials pool to be used in SearchRule resources. Just executed when the resource has a secretRef defined. -func (r *QueryConnectorReconciler) Sync(ctx context.Context, eventType watch.EventType, resource *v1alpha1.QueryConnector) (err error) { +func (r *QueryConnectorReconciler) Sync(ctx context.Context, eventType watch.EventType, resource *CompoundQueryConnectorResource, resourceType string) (err error) { + + // Get the resource values depending on the resourceType + switch resourceType { + case controller.ClusterQueryConnectorResourceType: + resourceNamespace = "" + resourceName = resource.ClusterQueryConnectorResource.Name + resourceSpec = resource.ClusterQueryConnectorResource.Spec + case controller.QueryConnectorResourceType: + resourceNamespace = resource.QueryConnectorResource.Namespace + resourceName = resource.QueryConnectorResource.Name + resourceSpec = resource.QueryConnectorResource.Spec + } // If the eventType is Deleted, remove the credentials from the pool // In other cases get the credentials from the secret and add them to the pool if eventType == watch.Deleted { - credentialsKey := fmt.Sprintf("%s_%s", resource.Namespace, resource.Name) + credentialsKey := fmt.Sprintf("%s_%s", resourceNamespace, resourceName) r.CredentialsPool.Delete(credentialsKey) return nil } @@ -43,36 +65,40 @@ func (r *QueryConnectorReconciler) Sync(ctx context.Context, eventType watch.Eve // First get secret with the credentials. The secret must be in the same // namespace as the QueryConnector resource. QueryConnectorCredsSecret := &v1.Secret{} + secretNamespace := resourceSpec.Credentials.SecretRef.Namespace + if secretNamespace == "" { + secretNamespace = resourceNamespace + } namespacedName := types.NamespacedName{ - Namespace: resource.Namespace, - Name: resource.Spec.Credentials.SecretRef.Name, + Namespace: secretNamespace, + Name: resourceSpec.Credentials.SecretRef.Name, } err = r.Get(ctx, namespacedName, QueryConnectorCredsSecret) if err != nil { // Updates status to NoCredsFound - r.UpdateConditionNoCredsFound(resource) - return fmt.Errorf(SecretNotFoundErrorMessage, namespacedName, err) + r.UpdateConditionNoCredsFound(resource, resourceType) + return fmt.Errorf(controller.SecretNotFoundErrorMessage, namespacedName, err) } // Get username and password from the secret data - username := string(QueryConnectorCredsSecret.Data[resource.Spec.Credentials.SecretRef.KeyUsername]) - password := string(QueryConnectorCredsSecret.Data[resource.Spec.Credentials.SecretRef.KeyPassword]) + username := string(QueryConnectorCredsSecret.Data[resourceSpec.Credentials.SecretRef.KeyUsername]) + password := string(QueryConnectorCredsSecret.Data[resourceSpec.Credentials.SecretRef.KeyPassword]) // If username or password are empty, return an error if username == "" || password == "" { // Updates status to NoCredsFound - r.UpdateConditionNoCredsFound(resource) - return fmt.Errorf(MissingCredentialsMessage, namespacedName) + r.UpdateConditionNoCredsFound(resource, resourceType) + return fmt.Errorf(controller.MissingCredentialsMessage, namespacedName) } // Save credentials in the credentials pool - key := fmt.Sprintf("%s_%s", resource.Namespace, resource.Name) + key := fmt.Sprintf("%s_%s", resourceNamespace, resourceName) r.CredentialsPool.Set(key, &pools.Credentials{ Username: username, Password: password, }) // Updates status to Success - r.UpdateStateSuccess(resource) + r.UpdateStateSuccess(resource, resourceType) return nil } diff --git a/internal/controller/queryconnector_controller.go b/internal/controller/queryconnector_controller.go deleted file mode 100644 index 589fc24..0000000 --- a/internal/controller/queryconnector_controller.go +++ /dev/null @@ -1,149 +0,0 @@ -/* -Copyright 2024. - -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 controller - -import ( - "context" - "fmt" - "reflect" - "time" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" - 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" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - searchrulerv1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" - "prosimcorp.com/SearchRuler/internal/pools" -) - -// QueryConnectorReconciler reconciles a QueryConnector object -type QueryConnectorReconciler struct { - client.Client - Scheme *runtime.Scheme - CredentialsPool *pools.CredentialsStore -} - -// +kubebuilder:rbac:groups=searchruler.prosimcorp.com,resources=queryconnectors,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=searchruler.prosimcorp.com,resources=queryconnectors/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=searchruler.prosimcorp.com,resources=queryconnectors/finalizers,verbs=update - -// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/reconcile -func (r *QueryConnectorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { - logger := log.FromContext(ctx) - - // 1. Get the content of the Patch - QueryConnectorResource := &searchrulerv1alpha1.QueryConnector{} - err = r.Get(ctx, req.NamespacedName, QueryConnectorResource) - - // 2. Check existence on the cluster - if err != nil { - - // 2.1 It does NOT exist: manage removal - if err = client.IgnoreNotFound(err); err == nil { - logger.Info(fmt.Sprintf(resourceNotFoundError, QueryConnectorResourceType, req.NamespacedName)) - return result, err - } - - // 2.2 Failed to get the resource, requeue the request - logger.Info(fmt.Sprintf(resourceSyncTimeRetrievalError, QueryConnectorResourceType, req.NamespacedName, err.Error())) - return result, err - } - - // 3. Check if the SearchRule instance is marked to be deleted: indicated by the deletion timestamp being set - if !QueryConnectorResource.DeletionTimestamp.IsZero() { - if controllerutil.ContainsFinalizer(QueryConnectorResource, resourceFinalizer) { - - // 3.1 Delete the resources associated with the QueryConnector - err = r.Sync(ctx, watch.Deleted, QueryConnectorResource) - - // Remove the finalizers on Patch CR - controllerutil.RemoveFinalizer(QueryConnectorResource, resourceFinalizer) - err = r.Update(ctx, QueryConnectorResource) - if err != nil { - logger.Info(fmt.Sprintf(resourceFinalizersUpdateError, QueryConnectorResourceType, req.NamespacedName, err.Error())) - } - } - - result = ctrl.Result{} - err = nil - return result, err - } - - // 4. Add finalizer to the SearchRule CR - if !controllerutil.ContainsFinalizer(QueryConnectorResource, resourceFinalizer) { - controllerutil.AddFinalizer(QueryConnectorResource, resourceFinalizer) - err = r.Update(ctx, QueryConnectorResource) - if err != nil { - return result, err - } - } - - // 5. Update the status before the requeue - defer func() { - err = r.Status().Update(ctx, QueryConnectorResource) - if err != nil { - logger.Info(fmt.Sprintf(resourceConditionUpdateError, QueryConnectorResourceType, req.NamespacedName, err.Error())) - } - }() - - // 6. Schedule periodical request - if QueryConnectorResource.Spec.Credentials.SyncInterval == "" { - QueryConnectorResource.Spec.Credentials.SyncInterval = defaultSyncInterval - } - RequeueTime, err := time.ParseDuration(QueryConnectorResource.Spec.Credentials.SyncInterval) - if err != nil { - logger.Info(fmt.Sprintf(resourceSyncTimeRetrievalError, QueryConnectorResourceType, req.NamespacedName, err.Error())) - return result, err - } - result = ctrl.Result{ - RequeueAfter: RequeueTime, - } - - // 7. Sync credentials if defined - if !reflect.ValueOf(QueryConnectorResource.Spec.Credentials).IsZero() { - err = r.Sync(ctx, watch.Modified, QueryConnectorResource) - if err != nil { - r.UpdateConditionKubernetesApiCallFailure(QueryConnectorResource) - logger.Info(fmt.Sprintf(syncTargetError, QueryConnectorResourceType, req.NamespacedName, err.Error())) - return result, err - } - } - - // 8. Success, update the status - r.UpdateConditionSuccess(QueryConnectorResource) - - return result, err -} - -// SetupWithManager sets up the controller with the Manager. -func (r *QueryConnectorReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&searchrulerv1alpha1.QueryConnector{}). - WithEventFilter(predicate.GenerationChangedPredicate{}). - Named("QueryConnector"). - Complete(r) -} diff --git a/internal/controller/queryconnector_controller_test.go b/internal/controller/queryconnector_controller_test.go deleted file mode 100644 index b396100..0000000 --- a/internal/controller/queryconnector_controller_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2024. - -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 controller - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - searchrulerv1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" -) - -var _ = Describe("QueryConnector Controller", func() { - Context("When reconciling a resource", func() { - const resourceName = "test-resource" - - ctx := context.Background() - - typeNamespacedName := types.NamespacedName{ - Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed - } - QueryConnector := &searchrulerv1alpha1.QueryConnector{} - - BeforeEach(func() { - By("creating the custom resource for the Kind QueryConnector") - err := k8sClient.Get(ctx, typeNamespacedName, QueryConnector) - if err != nil && errors.IsNotFound(err) { - resource := &searchrulerv1alpha1.QueryConnector{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", - }, - // TODO(user): Specify other spec details if needed. - } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - } - }) - - AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. - resource := &searchrulerv1alpha1.QueryConnector{} - err := k8sClient.Get(ctx, typeNamespacedName, resource) - Expect(err).NotTo(HaveOccurred()) - - By("Cleanup the specific resource instance QueryConnector") - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) - }) - It("should successfully reconcile the resource", func() { - By("Reconciling the created resource") - controllerReconciler := &QueryConnectorReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - } - - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, - }) - Expect(err).NotTo(HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. - }) - }) -}) diff --git a/internal/controller/ruleraction_controller.go b/internal/controller/ruleraction/controller.go similarity index 51% rename from internal/controller/ruleraction_controller.go rename to internal/controller/ruleraction/controller.go index 375510c..2595bb3 100644 --- a/internal/controller/ruleraction_controller.go +++ b/internal/controller/ruleraction/controller.go @@ -14,17 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package ruleraction import ( "context" "fmt" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - searchrulerv1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" - "prosimcorp.com/SearchRuler/internal/globals" - "prosimcorp.com/SearchRuler/internal/pools" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -32,6 +31,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" + + // + searchrulerv1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" + "prosimcorp.com/SearchRuler/internal/controller" + "prosimcorp.com/SearchRuler/internal/globals" + "prosimcorp.com/SearchRuler/internal/pools" ) // RulerActionReconciler reconciles a RulerAction object @@ -41,6 +46,17 @@ type RulerActionReconciler struct { AlertsPool *pools.AlertsStore } +type CompoundRulerActionResource struct { + RulerActionResource *searchrulerv1alpha1.RulerAction + ClusterRulerActionResource *searchrulerv1alpha1.ClusterRulerAction +} + +var ( + resourceType string + containsFinalizer bool + deletionTimestamp *v1.Time +) + // +kubebuilder:rbac:groups=searchruler.prosimcorp.com,resources=ruleractions,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=searchruler.prosimcorp.com,resources=ruleractions/status,verbs=get;update;patch // +kubebuilder:rbac:groups=searchruler.prosimcorp.com,resources=ruleractions/finalizers,verbs=update @@ -58,40 +74,66 @@ func (r *RulerActionReconciler) Reconcile(ctx context.Context, req ctrl.Request) logger := log.FromContext(ctx) // 1. Get the content of the Patch + CompoundRulerActionResource := &CompoundRulerActionResource{ + RulerActionResource: &searchrulerv1alpha1.RulerAction{}, + ClusterRulerActionResource: &searchrulerv1alpha1.ClusterRulerAction{}, + } // 1.1 Try with Event resource first. If it is not an Event, then it will return an error // but reconcile will try if it is a RulerAction resource relationated with an Event - RulerActionResource := &searchrulerv1alpha1.RulerAction{} - *RulerActionResource, err = r.GetEventRuleAction(ctx, req.Namespace, req.Name) + tempCompoundRulerActionResource, err := r.GetEventRuleAction(ctx, req.Namespace, req.Name, resourceType) if err == nil { + *CompoundRulerActionResource = tempCompoundRulerActionResource goto processEvent } - // 1.2 Try with RulerAction resource - err = r.Get(ctx, req.NamespacedName, RulerActionResource) + // 1.2 Try with RulerAction or ClusterRulerAction resource + switch req.Namespace { + case "": + resourceType = controller.ClusterRulerActionResourceType + err = r.Get(ctx, req.NamespacedName, CompoundRulerActionResource.ClusterRulerActionResource) + default: + resourceType = controller.RulerActionResourceType + err = r.Get(ctx, req.NamespacedName, CompoundRulerActionResource.RulerActionResource) + } // 2. Check existence on the cluster if err != nil { // 2.1 It does NOT exist: manage removal if err = client.IgnoreNotFound(err); err == nil { - logger.Info(fmt.Sprintf(resourceNotFoundError, RulerActionResourceType, req.NamespacedName)) + logger.Info(fmt.Sprintf(controller.ResourceNotFoundError, controller.RulerActionResourceType, req.NamespacedName)) return result, err } // 2.2 Failed to get the resource, requeue the request - logger.Info(fmt.Sprintf(resourceSyncTimeRetrievalError, RulerActionResourceType, req.NamespacedName, err.Error())) + logger.Info(fmt.Sprintf(controller.CanNotGetResourceError, controller.RulerActionResourceType, req.NamespacedName, err.Error())) return result, err } - // 3. Check if the SearchRule instance is marked to be deleted: indicated by the deletion timestamp being set - if !RulerActionResource.DeletionTimestamp.IsZero() { - if controllerutil.ContainsFinalizer(RulerActionResource, resourceFinalizer) { + // 3. Check if the RulerAction instance is marked to be deleted: indicated by the deletion timestamp being set + switch resourceType { + case controller.ClusterRulerActionResourceType: + deletionTimestamp = CompoundRulerActionResource.ClusterRulerActionResource.DeletionTimestamp + containsFinalizer = controllerutil.ContainsFinalizer(CompoundRulerActionResource.ClusterRulerActionResource, controller.ResourceFinalizer) + default: + deletionTimestamp = CompoundRulerActionResource.RulerActionResource.DeletionTimestamp + containsFinalizer = controllerutil.ContainsFinalizer(CompoundRulerActionResource.RulerActionResource, controller.ResourceFinalizer) + } + if !deletionTimestamp.IsZero() { + if containsFinalizer { + // Remove the finalizers on Patch CR - controllerutil.RemoveFinalizer(RulerActionResource, resourceFinalizer) - err = r.Update(ctx, RulerActionResource) + switch resourceType { + case controller.ClusterRulerActionResourceType: + controllerutil.RemoveFinalizer(CompoundRulerActionResource.ClusterRulerActionResource, controller.ResourceFinalizer) + err = r.Update(ctx, CompoundRulerActionResource.ClusterRulerActionResource) + default: + controllerutil.RemoveFinalizer(CompoundRulerActionResource.RulerActionResource, controller.ResourceFinalizer) + err = r.Update(ctx, CompoundRulerActionResource.RulerActionResource) + } if err != nil { - logger.Info(fmt.Sprintf(resourceFinalizersUpdateError, RulerActionResourceType, req.NamespacedName, err.Error())) + logger.Info(fmt.Sprintf(controller.ResourceFinalizersUpdateError, resourceType, req.NamespacedName, err.Error())) } } @@ -100,10 +142,16 @@ func (r *RulerActionReconciler) Reconcile(ctx context.Context, req ctrl.Request) return result, err } - // 4. Add finalizer to the SearchRule CR - if !controllerutil.ContainsFinalizer(RulerActionResource, resourceFinalizer) { - controllerutil.AddFinalizer(RulerActionResource, resourceFinalizer) - err = r.Update(ctx, RulerActionResource) + // 4. Add finalizer to the RulerAction CR + if !containsFinalizer { + switch resourceType { + case controller.ClusterRulerActionResourceType: + controllerutil.AddFinalizer(CompoundRulerActionResource.ClusterRulerActionResource, controller.ResourceFinalizer) + err = r.Update(ctx, CompoundRulerActionResource.ClusterRulerActionResource) + default: + controllerutil.AddFinalizer(CompoundRulerActionResource.RulerActionResource, controller.ResourceFinalizer) + err = r.Update(ctx, CompoundRulerActionResource.RulerActionResource) + } if err != nil { return result, err } @@ -111,11 +159,15 @@ func (r *RulerActionReconciler) Reconcile(ctx context.Context, req ctrl.Request) // 5. Update the status before the requeue defer func() { - err = r.Status().Update(ctx, RulerActionResource) + switch resourceType { + case controller.ClusterRulerActionResourceType: + err = r.Status().Update(ctx, CompoundRulerActionResource.ClusterRulerActionResource) + default: + err = r.Status().Update(ctx, CompoundRulerActionResource.RulerActionResource) + } if err != nil { - logger.Info(fmt.Sprintf(resourceConditionUpdateError, RulerActionResourceType, req.NamespacedName, err.Error())) + logger.Info(fmt.Sprintf(controller.ResourceConditionUpdateError, resourceType, req.NamespacedName, err.Error())) } - }() // 6. Schedule periodical request @@ -132,15 +184,15 @@ func (r *RulerActionReconciler) Reconcile(ctx context.Context, req ctrl.Request) // 7. Sync credentials if defined processEvent: - err = r.Sync(ctx, RulerActionResource) + err = r.Sync(ctx, CompoundRulerActionResource, resourceType) if err != nil { - r.UpdateConditionKubernetesApiCallFailure(RulerActionResource) - logger.Info(fmt.Sprintf(syncTargetError, RulerActionResourceType, req.NamespacedName, err.Error())) + r.UpdateConditionKubernetesApiCallFailure(CompoundRulerActionResource, resourceType) + logger.Info(fmt.Sprintf(controller.SyncTargetError, controller.RulerActionResourceType, req.NamespacedName, err.Error())) return result, err } // 8. Success, update the status - r.UpdateConditionSuccess(RulerActionResource) + r.UpdateConditionSuccess(CompoundRulerActionResource, resourceType) return result, err } @@ -154,6 +206,7 @@ func (r *RulerActionReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&searchrulerv1alpha1.RulerAction{}). Named("RulerAction"). WithEventFilter(predicate.GenerationChangedPredicate{}). + Watches(&searchrulerv1alpha1.ClusterRulerAction{}, &handler.EnqueueRequestForObject{}). Watches(&corev1.Event{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(prefixFilter)). // Also watch for events, so SearchRule controller throws events when a rule is firing Complete(r) } diff --git a/internal/controller/ruleraction_status.go b/internal/controller/ruleraction/status.go similarity index 56% rename from internal/controller/ruleraction_status.go rename to internal/controller/ruleraction/status.go index 26891a4..b9e1e81 100644 --- a/internal/controller/ruleraction_status.go +++ b/internal/controller/ruleraction/status.go @@ -14,77 +14,109 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package ruleraction import ( - "prosimcorp.com/SearchRuler/internal/globals" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" + + // + + "prosimcorp.com/SearchRuler/internal/controller" + "prosimcorp.com/SearchRuler/internal/globals" ) // UpdateConditionSuccess updates the status of the RulerAction resource with a success condition -func (r *RulerActionReconciler) UpdateConditionSuccess(RulerAction *v1alpha1.RulerAction) { +func (r *RulerActionReconciler) UpdateConditionSuccess(resource *CompoundRulerActionResource, resourceType string) { // Create the new condition with the success status condition := globals.NewCondition(globals.ConditionTypeResourceSynced, metav1.ConditionTrue, globals.ConditionReasonTargetSynced, globals.ConditionReasonTargetSyncedMessage) // Update the status of the RulerAction resource - globals.UpdateCondition(&RulerAction.Status.Conditions, condition) + switch resourceType { + case controller.ClusterRulerActionResourceType: + globals.UpdateCondition(&resource.ClusterRulerActionResource.Status.Conditions, condition) + default: + globals.UpdateCondition(&resource.RulerActionResource.Status.Conditions, condition) + } } // UpdateConditionKubernetesApiCallFailure updates the status of the RulerAction resource with a failure condition -func (r *RulerActionReconciler) UpdateConditionKubernetesApiCallFailure(RulerAction *v1alpha1.RulerAction) { +func (r *RulerActionReconciler) UpdateConditionKubernetesApiCallFailure(resource *CompoundRulerActionResource, resourceType string) { // Create the new condition with the failure status condition := globals.NewCondition(globals.ConditionTypeResourceSynced, metav1.ConditionTrue, globals.ConditionReasonKubernetesApiCallErrorType, globals.ConditionReasonKubernetesApiCallErrorMessage) // Update the status of the RulerAction resource - globals.UpdateCondition(&RulerAction.Status.Conditions, condition) + switch resourceType { + case controller.ClusterRulerActionResourceType: + globals.UpdateCondition(&resource.ClusterRulerActionResource.Status.Conditions, condition) + default: + globals.UpdateCondition(&resource.RulerActionResource.Status.Conditions, condition) + } } // UpdateStateSuccess updates the status of the RulerAction resource with a Success condition -func (r *RulerActionReconciler) UpdateStateSuccess(RulerAction *v1alpha1.RulerAction) { +func (r *RulerActionReconciler) UpdateStateSuccess(resource *CompoundRulerActionResource, resourceType string) { // Create the new condition with the success status condition := globals.NewCondition(globals.ConditionTypeState, metav1.ConditionTrue, globals.ConditionReasonStateSuccessType, globals.ConditionReasonStateSuccessMessage) // Update the status of the RulerAction resource - globals.UpdateCondition(&RulerAction.Status.Conditions, condition) + switch resourceType { + case controller.ClusterRulerActionResourceType: + globals.UpdateCondition(&resource.ClusterRulerActionResource.Status.Conditions, condition) + default: + globals.UpdateCondition(&resource.RulerActionResource.Status.Conditions, condition) + } } // UpdateConditionConnectionError updates the status of the RulerAction resource with a ConnectionError condition -func (r *RulerActionReconciler) UpdateConditionConnectionError(RulerAction *v1alpha1.RulerAction) { +func (r *RulerActionReconciler) UpdateConditionConnectionError(resource *CompoundRulerActionResource, resourceType string) { // Create the new condition with the failure status condition := globals.NewCondition(globals.ConditionTypeState, metav1.ConditionTrue, globals.ConditionReasonConnectionErrorType, globals.ConditionReasonConnectionErrorMessage) // Update the status of the RulerAction resource - globals.UpdateCondition(&RulerAction.Status.Conditions, condition) + switch resourceType { + case controller.ClusterRulerActionResourceType: + globals.UpdateCondition(&resource.ClusterRulerActionResource.Status.Conditions, condition) + default: + globals.UpdateCondition(&resource.RulerActionResource.Status.Conditions, condition) + } } // UpdateConditionEvaluateTemplateError updates the status of the RulerAction resource with a EvaluateTemplateError condition -func (r *RulerActionReconciler) UpdateConditionEvaluateTemplateError(RulerAction *v1alpha1.RulerAction) { +func (r *RulerActionReconciler) UpdateConditionEvaluateTemplateError(resource *CompoundRulerActionResource, resourceType string) { // Create the new condition with the failure status condition := globals.NewCondition(globals.ConditionTypeState, metav1.ConditionTrue, globals.ConditionReasonEvaluateTemplateErrorType, globals.ConditionReasonEvaluateTemplateErrorMessage) // Update the status of the RulerAction resource - globals.UpdateCondition(&RulerAction.Status.Conditions, condition) + switch resourceType { + case controller.ClusterRulerActionResourceType: + globals.UpdateCondition(&resource.ClusterRulerActionResource.Status.Conditions, condition) + default: + globals.UpdateCondition(&resource.RulerActionResource.Status.Conditions, condition) + } } -// UpdateConditionNoCredsFound updates the status of the QueryConnector resource with a NoCreds condition -func (r *RulerActionReconciler) UpdateConditionNoCredsFound(RulerAction *v1alpha1.RulerAction) { +// UpdateConditionNoCredsFound updates the status of the RulerAction resource with a NoCreds condition +func (r *RulerActionReconciler) UpdateConditionNoCredsFound(resource *CompoundRulerActionResource, resourceType string) { // Create the new condition with the success status condition := globals.NewCondition(globals.ConditionTypeState, metav1.ConditionTrue, globals.ConditionReasonNoCredsFoundType, globals.ConditionReasonNoCredsFoundMessage) // Update the status of the RulerAction resource - globals.UpdateCondition(&RulerAction.Status.Conditions, condition) + switch resourceType { + case controller.ClusterRulerActionResourceType: + globals.UpdateCondition(&resource.ClusterRulerActionResource.Status.Conditions, condition) + default: + globals.UpdateCondition(&resource.RulerActionResource.Status.Conditions, condition) + } } diff --git a/internal/controller/ruleraction_sync.go b/internal/controller/ruleraction/sync.go similarity index 63% rename from internal/controller/ruleraction_sync.go rename to internal/controller/ruleraction/sync.go index 42aa73c..bad899b 100644 --- a/internal/controller/ruleraction_sync.go +++ b/internal/controller/ruleraction/sync.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package ruleraction import ( "bytes" @@ -27,11 +27,14 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/log" + + // "prosimcorp.com/SearchRuler/api/v1alpha1" + "prosimcorp.com/SearchRuler/internal/controller" "prosimcorp.com/SearchRuler/internal/pools" "prosimcorp.com/SearchRuler/internal/template" "prosimcorp.com/SearchRuler/internal/validators" - "sigs.k8s.io/controller-runtime/pkg/log" ) var ( @@ -39,66 +42,84 @@ var ( validatorsMap = map[string]func(data string) (result bool, hint string, err error){ "alertmanager": validators.ValidateAlertmanager, } + resourceNamespace string + resourceName string + resourceSpec v1alpha1.RulerActionSpec ) // Sync function is used to synchronize the RulerAction resource with the alerts. Executes the webhook defined in the // resource for each alert found in the AlertsPool. -func (r *RulerActionReconciler) Sync(ctx context.Context, resource *v1alpha1.RulerAction) (err error) { +func (r *RulerActionReconciler) Sync(ctx context.Context, resource *CompoundRulerActionResource, resourceType string) (err error) { logger := log.FromContext(ctx) + // Get the resource values depending on the resourceType + switch resourceType { + case controller.ClusterRulerActionResourceType: + resourceNamespace = "" + resourceName = resource.ClusterRulerActionResource.Name + resourceSpec = resource.ClusterRulerActionResource.Spec + case controller.RulerActionResourceType: + resourceNamespace = resource.RulerActionResource.Namespace + resourceName = resource.RulerActionResource.Name + resourceSpec = resource.RulerActionResource.Spec + } // Get credentials for the Action in the secret associated if defined username := "" password := "" - if !reflect.ValueOf(resource.Spec.Webhook.Credentials).IsZero() { + if !reflect.ValueOf(resourceSpec.Webhook.Credentials).IsZero() { // First get secret with the credentials RulerActionCredsSecret := &corev1.Secret{} + secretNamespace := resourceSpec.Webhook.Credentials.SecretRef.Namespace + if secretNamespace == "" { + secretNamespace = resourceNamespace + } namespacedName := types.NamespacedName{ - Namespace: resource.Namespace, - Name: resource.Spec.Webhook.Credentials.SecretRef.Name, + Namespace: secretNamespace, + Name: resourceSpec.Webhook.Credentials.SecretRef.Name, } err = r.Get(ctx, namespacedName, RulerActionCredsSecret) if err != nil { - r.UpdateConditionNoCredsFound(resource) - return fmt.Errorf(SecretNotFoundErrorMessage, namespacedName, err) + r.UpdateConditionNoCredsFound(resource, resourceType) + return fmt.Errorf(controller.SecretNotFoundErrorMessage, namespacedName, err) } // Get username and password - username = string(RulerActionCredsSecret.Data[resource.Spec.Webhook.Credentials.SecretRef.KeyUsername]) - password = string(RulerActionCredsSecret.Data[resource.Spec.Webhook.Credentials.SecretRef.KeyPassword]) + username = string(RulerActionCredsSecret.Data[resourceSpec.Webhook.Credentials.SecretRef.KeyUsername]) + password = string(RulerActionCredsSecret.Data[resourceSpec.Webhook.Credentials.SecretRef.KeyPassword]) if username == "" || password == "" { - r.UpdateConditionNoCredsFound(resource) - return fmt.Errorf(MissingCredentialsMessage, namespacedName) + r.UpdateConditionNoCredsFound(resource, resourceType) + return fmt.Errorf(controller.MissingCredentialsMessage, namespacedName) } } // Check alert pool for alerts related to this rulerAction // Alerts key pattern: namespace/rulerActionName/searchRuleName - alerts, err := r.getRulerActionAssociatedAlerts(resource) + alerts, err := r.getRulerActionAssociatedAlerts(resourceName) if err != nil { - return fmt.Errorf(AlertsPoolErrorMessage, err) + return fmt.Errorf(controller.AlertsPoolErrorMessage, err) } - // If there are alerts for the rulerAction, initialice the HTTP client + // If there are alerts for the rulerAction, initialize the HTTP client if len(alerts) > 0 { // Create the HTTP client httpClient := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: resource.Spec.Webhook.TlsSkipVerify, + InsecureSkipVerify: resourceSpec.Webhook.TlsSkipVerify, }, }, } // Create the request with the configured verb and URL - httpRequest, err := http.NewRequest(resource.Spec.Webhook.Verb, resource.Spec.Webhook.Url, nil) + httpRequest, err := http.NewRequest(resourceSpec.Webhook.Verb, resourceSpec.Webhook.Url, nil) if err != nil { - return fmt.Errorf(HttpRequestCreationErrorMessage, err) + return fmt.Errorf(controller.HttpRequestCreationErrorMessage, err) } // Add headers to the request if set httpRequest.Header.Set("Content-Type", "application/json") - for headerKey, headerValue := range resource.Spec.Webhook.Headers { + for headerKey, headerValue := range resourceSpec.Webhook.Headers { httpRequest.Header.Set(headerKey, headerValue) } @@ -113,7 +134,7 @@ func (r *RulerActionReconciler) Sync(ctx context.Context, resource *v1alpha1.Rul // Log alert firing logger.Info(fmt.Sprintf( - AlertFiringInfoMessage, + controller.AlertFiringInfoMessage, alert.SearchRule.Namespace, alert.SearchRule.Name, alert.SearchRule.Spec.Description, @@ -130,31 +151,31 @@ func (r *RulerActionReconciler) Sync(ctx context.Context, resource *v1alpha1.Rul // Evaluate the data template with the injected object parsedMessage, err := template.EvaluateTemplate(alert.SearchRule.Spec.ActionRef.Data, templateInjectedObject) if err != nil { - r.UpdateConditionEvaluateTemplateError(resource) - return fmt.Errorf(EvaluateTemplateErrorMessage, err) + r.UpdateConditionEvaluateTemplateError(resource, resourceType) + return fmt.Errorf(controller.EvaluateTemplateErrorMessage, err) } // Check if the webhook has a validator and execute it when available - if resource.Spec.Webhook.Validator != "" { + if resourceSpec.Webhook.Validator != "" { // Check if the validator is available - _, validatorFound := validatorsMap[resource.Spec.Webhook.Validator] + _, validatorFound := validatorsMap[resourceSpec.Webhook.Validator] if !validatorFound { - r.UpdateConditionEvaluateTemplateError(resource) - return fmt.Errorf(ValidatorNotFoundErrorMessage, resource.Spec.Webhook.Validator) + r.UpdateConditionEvaluateTemplateError(resource, resourceType) + return fmt.Errorf(controller.ValidatorNotFoundErrorMessage, resourceSpec.Webhook.Validator) } // Execute the validator to the data of the alert - validatorResult, validatorHint, err := validatorsMap[resource.Spec.Webhook.Validator](parsedMessage) + validatorResult, validatorHint, err := validatorsMap[resourceSpec.Webhook.Validator](parsedMessage) if err != nil { - r.UpdateConditionEvaluateTemplateError(resource) - return fmt.Errorf(ValidationFailedErrorMessage, err.Error()) + r.UpdateConditionEvaluateTemplateError(resource, resourceType) + return fmt.Errorf(controller.ValidationFailedErrorMessage, err.Error()) } // Check the result of the validator if !validatorResult { - r.UpdateConditionEvaluateTemplateError(resource) - return fmt.Errorf(ValidationFailedErrorMessage, validatorHint) + r.UpdateConditionEvaluateTemplateError(resource, resourceType) + return fmt.Errorf(controller.ValidationFailedErrorMessage, validatorHint) } } @@ -165,8 +186,8 @@ func (r *RulerActionReconciler) Sync(ctx context.Context, resource *v1alpha1.Rul // Send HTTP request to the webhook httpResponse, err := httpClient.Do(httpRequest) if err != nil { - r.UpdateConditionConnectionError(resource) - return fmt.Errorf(HttpRequestSendingErrorMessage, err) + r.UpdateConditionConnectionError(resource, resourceType) + return fmt.Errorf(controller.HttpRequestSendingErrorMessage, err) } defer httpResponse.Body.Close() @@ -174,12 +195,12 @@ func (r *RulerActionReconciler) Sync(ctx context.Context, resource *v1alpha1.Rul } // Updates status to Success - r.UpdateStateSuccess(resource) + r.UpdateStateSuccess(resource, resourceType) return nil } // GetRuleActionFromEvent returns the RulerAction resource associated with the event that triggered the reconcile -func (r *RulerActionReconciler) GetEventRuleAction(ctx context.Context, namespace, name string) (ruleAction v1alpha1.RulerAction, err error) { +func (r *RulerActionReconciler) GetEventRuleAction(ctx context.Context, namespace, name, resourceType string) (ruleAction CompoundRulerActionResource, err error) { // Get event resource from the namespace and name of the event that triggered the reconcile EventResource := &corev1.Event{} @@ -213,12 +234,17 @@ func (r *RulerActionReconciler) GetEventRuleAction(ctx context.Context, namespac } // Get RulerAction resource from searchRule resource - ruleAction = v1alpha1.RulerAction{} ruleActionNamespacedName := types.NamespacedName{ - Namespace: searchRule.Namespace, + Namespace: searchRule.Spec.ActionRef.Namespace, Name: searchRule.Spec.ActionRef.Name, } - err = r.Get(ctx, ruleActionNamespacedName, &ruleAction) + + switch resourceType { + case controller.ClusterRulerActionResourceType: + err = r.Get(ctx, ruleActionNamespacedName, ruleAction.ClusterRulerActionResource) + default: + err = r.Get(ctx, ruleActionNamespacedName, ruleAction.RulerActionResource) + } if err != nil { return ruleAction, fmt.Errorf( "error fetching RulerAction %s from searchRule %s: %v", @@ -232,14 +258,14 @@ func (r *RulerActionReconciler) GetEventRuleAction(ctx context.Context, namespac } // getRulerActionAssociatedAlerts returns all alerts associated with the RulerAction -func (r *RulerActionReconciler) getRulerActionAssociatedAlerts(resource *v1alpha1.RulerAction) (alerts []*pools.Alert, err error) { +func (r *RulerActionReconciler) getRulerActionAssociatedAlerts(resourceName string) (alerts []*pools.Alert, err error) { // Get all alerts from the AlertsPool alertsPool := r.AlertsPool.GetAll() // Iterate over the alerts in the pool and check if the alert is associated with the RulerAction for _, alert := range alertsPool { - if alert.RulerActionName == resource.Name { + if alert.RulerActionName == resourceName { alerts = append(alerts, alert) } } diff --git a/internal/controller/ruleraction_controller_test.go b/internal/controller/ruleraction_controller_test.go deleted file mode 100644 index 865a3b4..0000000 --- a/internal/controller/ruleraction_controller_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2024. - -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 controller - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - searchrulerv1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" -) - -var _ = Describe("RulerAction Controller", func() { - Context("When reconciling a resource", func() { - const resourceName = "test-resource" - - ctx := context.Background() - - typeNamespacedName := types.NamespacedName{ - Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed - } - RulerAction := &searchrulerv1alpha1.RulerAction{} - - BeforeEach(func() { - By("creating the custom resource for the Kind RulerAction") - err := k8sClient.Get(ctx, typeNamespacedName, RulerAction) - if err != nil && errors.IsNotFound(err) { - resource := &searchrulerv1alpha1.RulerAction{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", - }, - // TODO(user): Specify other spec details if needed. - } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - } - }) - - AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. - resource := &searchrulerv1alpha1.RulerAction{} - err := k8sClient.Get(ctx, typeNamespacedName, resource) - Expect(err).NotTo(HaveOccurred()) - - By("Cleanup the specific resource instance RulerAction") - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) - }) - It("should successfully reconcile the resource", func() { - By("Reconciling the created resource") - controllerReconciler := &RulerActionReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - } - - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, - }) - Expect(err).NotTo(HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. - }) - }) -}) diff --git a/internal/controller/searchrule_controller.go b/internal/controller/searchrule/controller.go similarity index 77% rename from internal/controller/searchrule_controller.go rename to internal/controller/searchrule/controller.go index 34163fe..5598471 100644 --- a/internal/controller/searchrule_controller.go +++ b/internal/controller/searchrule/controller.go @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package searchrule import ( "context" "fmt" "time" + // "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" ctrl "sigs.k8s.io/controller-runtime" @@ -29,7 +30,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" + // searchrulerv1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" + "prosimcorp.com/SearchRuler/internal/controller" "prosimcorp.com/SearchRuler/internal/pools" ) @@ -65,27 +68,27 @@ func (r *SearchRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) // 2.1 It does NOT exist: manage removal if err = client.IgnoreNotFound(err); err == nil { - logger.Info(fmt.Sprintf(resourceNotFoundError, SearchRuleResourceType, req.NamespacedName)) + logger.Info(fmt.Sprintf(controller.ResourceNotFoundError, controller.SearchRuleResourceType, req.NamespacedName)) return result, err } // 2.2 Failed to get the resource, requeue the request - logger.Info(fmt.Sprintf(resourceSyncTimeRetrievalError, SearchRuleResourceType, req.NamespacedName, err.Error())) + logger.Info(fmt.Sprintf(controller.ResourceSyncTimeRetrievalError, controller.SearchRuleResourceType, req.NamespacedName, err.Error())) return result, err } // 3. Check if the SearchRule instance is marked to be deleted: indicated by the deletion timestamp being set if !searchRuleResource.DeletionTimestamp.IsZero() { - if controllerutil.ContainsFinalizer(searchRuleResource, resourceFinalizer) { + if controllerutil.ContainsFinalizer(searchRuleResource, controller.ResourceFinalizer) { // 3.1 Delete the resources associated with the SearchRule err = r.Sync(ctx, watch.Deleted, searchRuleResource) // Remove the finalizers on Patch CR - controllerutil.RemoveFinalizer(searchRuleResource, resourceFinalizer) + controllerutil.RemoveFinalizer(searchRuleResource, controller.ResourceFinalizer) err = r.Update(ctx, searchRuleResource) if err != nil { - logger.Info(fmt.Sprintf(resourceFinalizersUpdateError, SearchRuleResourceType, req.NamespacedName, err.Error())) + logger.Info(fmt.Sprintf(controller.ResourceFinalizersUpdateError, controller.SearchRuleResourceType, req.NamespacedName, err.Error())) } } @@ -95,8 +98,8 @@ func (r *SearchRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // 4. Add finalizer to the SearchRule CR - if !controllerutil.ContainsFinalizer(searchRuleResource, resourceFinalizer) { - controllerutil.AddFinalizer(searchRuleResource, resourceFinalizer) + if !controllerutil.ContainsFinalizer(searchRuleResource, controller.ResourceFinalizer) { + controllerutil.AddFinalizer(searchRuleResource, controller.ResourceFinalizer) err = r.Update(ctx, searchRuleResource) if err != nil { return result, err @@ -107,14 +110,14 @@ func (r *SearchRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) defer func() { err = r.Status().Update(ctx, searchRuleResource) if err != nil { - logger.Info(fmt.Sprintf(resourceConditionUpdateError, SearchRuleResourceType, req.NamespacedName, err.Error())) + logger.Info(fmt.Sprintf(controller.ResourceConditionUpdateError, controller.SearchRuleResourceType, req.NamespacedName, err.Error())) } }() // 6. Schedule periodical request RequeueTime, err := time.ParseDuration(searchRuleResource.Spec.CheckInterval) if err != nil { - logger.Info(fmt.Sprintf(resourceSyncTimeRetrievalError, SearchRuleResourceType, req.NamespacedName, err.Error())) + logger.Info(fmt.Sprintf(controller.ResourceSyncTimeRetrievalError, controller.SearchRuleResourceType, req.NamespacedName, err.Error())) return result, err } result = ctrl.Result{ @@ -125,7 +128,7 @@ func (r *SearchRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) err = r.Sync(ctx, watch.Modified, searchRuleResource) if err != nil { r.UpdateConditionKubernetesApiCallFailure(searchRuleResource) - logger.Info(fmt.Sprintf(syncTargetError, SearchRuleResourceType, req.NamespacedName, err.Error())) + logger.Info(fmt.Sprintf(controller.SyncTargetError, controller.SearchRuleResourceType, req.NamespacedName, err.Error())) return result, err } diff --git a/internal/controller/searchrule_status.go b/internal/controller/searchrule/status.go similarity index 98% rename from internal/controller/searchrule_status.go rename to internal/controller/searchrule/status.go index 064dc0c..5333337 100644 --- a/internal/controller/searchrule_status.go +++ b/internal/controller/searchrule/status.go @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package searchrule import ( - "prosimcorp.com/SearchRuler/internal/globals" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" + + // + "prosimcorp.com/SearchRuler/api/v1alpha1" + "prosimcorp.com/SearchRuler/internal/globals" ) // UpdateConditionSuccess updates the status of the SearchRule resource with a success condition diff --git a/internal/controller/searchrule_sync.go b/internal/controller/searchrule/sync.go similarity index 82% rename from internal/controller/searchrule_sync.go rename to internal/controller/searchrule/sync.go index 791164d..40636cb 100644 --- a/internal/controller/searchrule_sync.go +++ b/internal/controller/searchrule/sync.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package searchrule import ( "bytes" @@ -28,18 +28,21 @@ import ( "strconv" "time" - "sigs.k8s.io/controller-runtime/pkg/log" + "k8s.io/apimachinery/pkg/runtime/schema" corev1 "k8s.io/api/core/v1" eventsv1 "k8s.io/api/events/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/tidwall/gjson" + + // "prosimcorp.com/SearchRuler/api/v1alpha1" + "prosimcorp.com/SearchRuler/internal/controller" "prosimcorp.com/SearchRuler/internal/globals" "prosimcorp.com/SearchRuler/internal/pools" - - "github.com/tidwall/gjson" ) const ( @@ -87,29 +90,55 @@ func (r *SearchRuleReconciler) Sync(ctx context.Context, eventType watch.EventTy return nil } - // Get QueryConnector associated to the rule - QueryConnectorResource := &v1alpha1.QueryConnector{} - QueryConnectorNamespacedName := types.NamespacedName{ - Namespace: resource.Namespace, - Name: resource.Spec.QueryConnectorRef.Name, + // Get QueryConnector associated to the rule with KubeRawClient + gvr := schema.GroupVersionResource{ + Group: v1alpha1.GroupVersion.Group, + Version: v1alpha1.GroupVersion.Version, + Resource: "clusterqueryconnectors", + } + + queryConnectorWrapper := globals.Application.KubeRawClient.Resource(gvr) + if resource.Spec.QueryConnectorRef.Namespace != "" { + gvr.Resource = "queryconnectors" + queryConnectorWrapper = globals.Application.KubeRawClient.Resource(gvr) + queryConnectorWrapper.Namespace(resource.Spec.QueryConnectorRef.Namespace) } - err = r.Get(ctx, QueryConnectorNamespacedName, QueryConnectorResource) + + QueryConnectorResource, err := queryConnectorWrapper.Get(ctx, resource.Spec.QueryConnectorRef.Name, metav1.GetOptions{}) + if err != nil { + // TODO: Improve this + return err + } + + // If QueryConnector is empty then error if reflect.ValueOf(QueryConnectorResource).IsZero() { r.UpdateConditionQueryConnectorNotFound(resource) return fmt.Errorf( - QueryConnectorNotFoundMessage, + controller.QueryConnectorNotFoundMessage, resource.Spec.QueryConnectorRef.Name, resource.Namespace, ) } + // Tricky for save queryConnector resource with QueryConnectorSpec type + QueryConnectorSpec := &v1alpha1.QueryConnectorSpec{} + QueryConnectorSpecI := QueryConnectorResource.Object["spec"] + specBytes, err := json.Marshal(QueryConnectorSpecI) + if err != nil { + return fmt.Errorf(controller.JSONMarshalErrorMessage, err) + } + err = json.Unmarshal(specBytes, QueryConnectorSpec) + if err != nil { + return fmt.Errorf(controller.JSONMarshalErrorMessage, err) + } + // Get credentials for QueryConnector attached if defined - if !reflect.ValueOf(QueryConnectorResource.Spec.Credentials).IsZero() { - key := fmt.Sprintf("%s_%s", resource.Namespace, QueryConnectorResource.Name) + if !reflect.ValueOf(QueryConnectorSpec.Credentials).IsZero() { + key := fmt.Sprintf("%s_%s", QueryConnectorResource.GetNamespace(), QueryConnectorResource.GetName()) queryConnectorCreds, credsExists = r.QueryConnectorCredentialsPool.Get(key) if !credsExists { r.UpdateConditionNoCredsFound(resource) - return fmt.Errorf(MissingCredentialsMessage, key) + return fmt.Errorf(controller.MissingCredentialsMessage, key) } } @@ -117,19 +146,19 @@ func (r *SearchRuleReconciler) Sync(ctx context.Context, eventType watch.EventTy // then the rule is really ocurring and must be an alert forDuration, err := time.ParseDuration(resource.Spec.Condition.For) if err != nil { - return fmt.Errorf(ForValueParseErrorMessage, err) + return fmt.Errorf(controller.ForValueParseErrorMessage, err) } // Check if query is defined in the resource if resource.Spec.Elasticsearch.Query == nil && resource.Spec.Elasticsearch.QueryJSON == "" { r.UpdateConditionNoQueryFound(resource) - return fmt.Errorf(QueryNotDefinedErrorMessage, resource.Name) + return fmt.Errorf(controller.QueryNotDefinedErrorMessage, resource.Name) } // Check if both query and queryJson are defined. If true, return error if resource.Spec.Elasticsearch.Query != nil && resource.Spec.Elasticsearch.QueryJSON != "" { r.UpdateConditionNoQueryFound(resource) - return fmt.Errorf(QueryDefinedInBothErrorMessage, resource.Name) + return fmt.Errorf(controller.QueryDefinedInBothErrorMessage, resource.Name) } // Select query to use and marshall to JSON @@ -138,7 +167,7 @@ func (r *SearchRuleReconciler) Sync(ctx context.Context, eventType watch.EventTy if resource.Spec.Elasticsearch.Query != nil { elasticQuery, err = json.Marshal(resource.Spec.Elasticsearch.Query) if err != nil { - return fmt.Errorf(JSONMarshalErrorMessage, err) + return fmt.Errorf(controller.JSONMarshalErrorMessage, err) } } // If queryJSON is defined in the resource, it is already a JSON, just convert it to bytes @@ -150,7 +179,7 @@ func (r *SearchRuleReconciler) Sync(ctx context.Context, eventType watch.EventTy httpClient := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: QueryConnectorResource.Spec.TlsSkipVerify, + InsecureSkipVerify: QueryConnectorSpec.TlsSkipVerify, }, }, } @@ -158,23 +187,23 @@ func (r *SearchRuleReconciler) Sync(ctx context.Context, eventType watch.EventTy // Generate URL for search to elasticsearch searchURL := fmt.Sprintf( ElasticsearchSearchURL, - QueryConnectorResource.Spec.URL, + QueryConnectorSpec.URL, resource.Spec.Elasticsearch.Index, ) req, err := http.NewRequest("POST", searchURL, bytes.NewBuffer(elasticQuery)) if err != nil { r.UpdateConditionConnectionError(resource) - return fmt.Errorf(HttpRequestCreationErrorMessage, err) + return fmt.Errorf(controller.HttpRequestCreationErrorMessage, err) } // Add headers and custom headers for elasticsearch queries req.Header.Set("Content-Type", "application/json") - for key, value := range QueryConnectorResource.Spec.Headers { + for key, value := range QueryConnectorSpec.Headers { req.Header.Set(key, value) } // Add authentication if set for elasticsearch queries - if QueryConnectorResource.Spec.Credentials.SecretRef.Name != "" { + if QueryConnectorSpec.Credentials.SecretRef.Name != "" { req.SetBasicAuth(queryConnectorCreds.Username, queryConnectorCreds.Password) } @@ -182,7 +211,7 @@ func (r *SearchRuleReconciler) Sync(ctx context.Context, eventType watch.EventTy resp, err := httpClient.Do(req) if err != nil { r.UpdateConditionConnectionError(resource) - return fmt.Errorf(ElasticsearchQueryErrorMessage, string(elasticQuery), err) + return fmt.Errorf(controller.ElasticsearchQueryErrorMessage, string(elasticQuery), err) } defer resp.Body.Close() @@ -190,12 +219,12 @@ func (r *SearchRuleReconciler) Sync(ctx context.Context, eventType watch.EventTy responseBody, err := io.ReadAll(resp.Body) if err != nil { r.UpdateConditionQueryError(resource) - return fmt.Errorf(ResponseBodyReadErrorMessage, err) + return fmt.Errorf(controller.ResponseBodyReadErrorMessage, err) } if resp.StatusCode != http.StatusOK { r.UpdateConditionQueryError(resource) return fmt.Errorf( - ElasticsearchQueryResponseErrorMessage, + controller.ElasticsearchQueryResponseErrorMessage, string(elasticQuery), string(responseBody), ) @@ -206,7 +235,7 @@ func (r *SearchRuleReconciler) Sync(ctx context.Context, eventType watch.EventTy if !conditionValue.Exists() { r.UpdateConditionQueryError(resource) return fmt.Errorf( - ConditionFieldNotFoundMessage, + controller.ConditionFieldNotFoundMessage, resource.Spec.Elasticsearch.ConditionField, string(responseBody), ) @@ -225,7 +254,7 @@ func (r *SearchRuleReconciler) Sync(ctx context.Context, eventType watch.EventTy if err != nil { r.UpdateConditionQueryError(resource) return fmt.Errorf( - EvaluatingConditionErrorMessage, + controller.EvaluatingConditionErrorMessage, err, ) } @@ -289,7 +318,7 @@ func (r *SearchRuleReconciler) Sync(ctx context.Context, eventType watch.EventTy fmt.Sprintf("Rule is in firing state. Current value is %v", conditionValue), ) if err != nil { - return fmt.Errorf(KubeEventCreationErrorMessage, err) + return fmt.Errorf(controller.KubeEventCreationErrorMessage, err) } // Log the alert and change the AlertStatus to Firing of the searchRule diff --git a/internal/controller/searchrule_controller_test.go b/internal/controller/searchrule_controller_test.go deleted file mode 100644 index 170b66f..0000000 --- a/internal/controller/searchrule_controller_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2024. - -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 controller - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - searchrulerv1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1" -) - -var _ = Describe("SearchRule Controller", func() { - Context("When reconciling a resource", func() { - const resourceName = "test-resource" - - ctx := context.Background() - - typeNamespacedName := types.NamespacedName{ - Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed - } - searchrule := &searchrulerv1alpha1.SearchRule{} - - BeforeEach(func() { - By("creating the custom resource for the Kind SearchRule") - err := k8sClient.Get(ctx, typeNamespacedName, searchrule) - if err != nil && errors.IsNotFound(err) { - resource := &searchrulerv1alpha1.SearchRule{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", - }, - // TODO(user): Specify other spec details if needed. - } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - } - }) - - AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. - resource := &searchrulerv1alpha1.SearchRule{} - err := k8sClient.Get(ctx, typeNamespacedName, resource) - Expect(err).NotTo(HaveOccurred()) - - By("Cleanup the specific resource instance SearchRule") - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) - }) - It("should successfully reconcile the resource", func() { - By("Reconciling the created resource") - controllerReconciler := &SearchRuleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - } - - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, - }) - Expect(err).NotTo(HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. - }) - }) -}) diff --git a/internal/webserver/static/templates/rule_detail.html b/internal/webserver/static/templates/rule_detail.html index 84031f1..4dbfd60 100644 --- a/internal/webserver/static/templates/rule_detail.html +++ b/internal/webserver/static/templates/rule_detail.html @@ -61,7 +61,7 @@

Rule Detail

QueryConnector - {{ .Rule.SearchRule.Spec.QueryConnectorRef.Name }} + {{ .Rule.SearchRule.Spec.QueryConnectorRef.Namespace }}/{{ .Rule.SearchRule.Spec.QueryConnectorRef.Name }} Index