Skip to content

Commit 7a748fe

Browse files
committed
First version working
1 parent c6754dc commit 7a748fe

File tree

9 files changed

+156
-37
lines changed

9 files changed

+156
-37
lines changed

api/v1alpha1/ruleraction_types.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ type RulerActionSpec struct {
4242
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
4343
// Important: Run "make" to regenerate code after modifying this file
4444

45-
Webhook Webhook `json:"webhook"`
46-
FiringInterval string `json:"firingInterval"`
45+
Webhook Webhook `json:"webhook"`
4746
}
4847

4948
// RulerActionStatus defines the observed state of RulerAction.

config/crd/bases/searchruler.prosimcorp.com_ruleractions.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ spec:
3939
spec:
4040
description: RulerActionSpec defines the desired state of RulerAction.
4141
properties:
42-
firingInterval:
43-
type: string
4442
webhook:
4543
description: WebHook TODO
4644
properties:
@@ -79,7 +77,6 @@ spec:
7977
- verb
8078
type: object
8179
required:
82-
- firingInterval
8380
- webhook
8481
type: object
8582
status:

config/samples/searchruler_v1alpha1_ruleraction.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ metadata:
66
app.kubernetes.io/managed-by: kustomize
77
name: ruleraction-sample
88
spec:
9-
firingInterval: 1m
109
webhook:
11-
url: https://webhook.site/7688d5a6-af64-40d4-9991-01cb3b9035a7
10+
url: https://webhook.site/3f2e28af-68b2-47bd-b603-f8abcdf2ac93
1211
verb: POST
1312
headers: {}
14-
validator: alertmanager
13+
14+
#validator: alertmanager
15+
1516
# credentials:
1617
# secretRef:
1718
# name: alertmanager-credentials

config/samples/searchruler_v1alpha1_searchrule.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ spec:
1313
name: queryconnector-sample
1414

1515
# Interval time for checking the value of the query
16-
checkInterval: 5s
16+
checkInterval: 30s
1717

1818
# Query to execute in the QueryConnector
1919
elasticsearch:
@@ -82,15 +82,15 @@ spec:
8282
# greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual or equal
8383
operator: "greaterThan"
8484
threshold: "100"
85-
for: "15s"
85+
for: "1m"
8686

8787
# SearchAction item
8888
actionRef:
8989
name: ruleraction-sample
9090
data: |
9191
{{- $object := .object -}}
9292
{{- $value := .value -}}
93-
{{ printf "Hi, I'm on fire!" -}}
94-
{{ printf "Name: %s" $object.Name -}}
95-
{{ printf "Description: %s" $object.Spec.Description -}}
96-
{{ printf "Current value: %v" $value -}}
93+
{{ printf "Hi, I'm on fire!" }}
94+
{{ printf "Name: %s" $object.Name }}
95+
{{ printf "Description: %s" $object.Spec.Description }}
96+
{{ printf "Current value: %v" $value }}

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module prosimcorp.com/SearchRuler
33
go 1.22.0
44

55
require (
6+
github.com/BurntSushi/toml v1.4.0
7+
github.com/Masterminds/sprig v2.22.0+incompatible
68
github.com/onsi/ginkgo/v2 v2.19.0
79
github.com/onsi/gomega v1.33.1
810
github.com/tidwall/gjson v1.18.0
@@ -11,13 +13,12 @@ require (
1113
k8s.io/apimachinery v0.31.0
1214
k8s.io/client-go v0.31.0
1315
sigs.k8s.io/controller-runtime v0.19.0
16+
sigs.k8s.io/yaml v1.4.0
1417
)
1518

1619
require (
17-
github.com/BurntSushi/toml v1.4.0 // indirect
1820
github.com/Masterminds/goutils v1.1.1 // indirect
1921
github.com/Masterminds/semver v1.5.0 // indirect
20-
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
2122
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
2223
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
2324
github.com/beorn7/perks v1.0.1 // indirect
@@ -105,5 +106,4 @@ require (
105106
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
106107
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
107108
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
108-
sigs.k8s.io/yaml v1.4.0 // indirect
109109
)

internal/controller/ruleraction_controller.go

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,18 @@ package controller
1919
import (
2020
"context"
2121
"fmt"
22-
"time"
2322

2423
corev1 "k8s.io/api/core/v1"
24+
"k8s.io/apimachinery/pkg/api/errors"
2525
"k8s.io/apimachinery/pkg/runtime"
26+
"k8s.io/apimachinery/pkg/types"
2627
searchrulerv1alpha1 "prosimcorp.com/SearchRuler/api/v1alpha1"
2728
"prosimcorp.com/SearchRuler/internal/pools"
2829
ctrl "sigs.k8s.io/controller-runtime"
2930
"sigs.k8s.io/controller-runtime/pkg/client"
3031
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3132
"sigs.k8s.io/controller-runtime/pkg/event"
33+
"sigs.k8s.io/controller-runtime/pkg/handler"
3234
"sigs.k8s.io/controller-runtime/pkg/log"
3335
"sigs.k8s.io/controller-runtime/pkg/predicate"
3436
)
@@ -56,7 +58,6 @@ type RulerActionReconciler struct {
5658
func (r *RulerActionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
5759

5860
logger := log.FromContext(ctx)
59-
triggeredByEvent := false
6061

6162
// 1. Get the content of the Patch
6263

@@ -65,16 +66,31 @@ func (r *RulerActionReconciler) Reconcile(ctx context.Context, req ctrl.Request)
6566
err = r.Get(ctx, req.NamespacedName, RulerActionResource)
6667

6768
// 1.2 If there are an error, try with Event type resource
68-
if err != nil {
69+
if errors.IsNotFound(err) {
6970
EventResource := &corev1.Event{}
7071
err = r.Get(ctx, req.NamespacedName, EventResource)
71-
if err != nil {
72-
triggeredByEvent = true
72+
73+
// If the resource is an event, then get the SearchRule resource associated to the event
74+
// and then the RulerAction resource associated to the SearchRule
75+
if err == nil {
76+
SearchRuleResource := &searchrulerv1alpha1.SearchRule{}
77+
SearchRuleNamespacedName := types.NamespacedName{
78+
Namespace: EventResource.InvolvedObject.Namespace,
79+
Name: EventResource.InvolvedObject.Name,
80+
}
81+
err = r.Get(ctx, SearchRuleNamespacedName, SearchRuleResource)
82+
if err == nil {
83+
RulerActionNamespacedName := types.NamespacedName{
84+
Namespace: SearchRuleResource.Namespace,
85+
Name: SearchRuleResource.Spec.ActionRef.Name,
86+
}
87+
err = r.Get(ctx, RulerActionNamespacedName, RulerActionResource)
88+
}
7389
}
7490
}
7591

7692
// 2. If it is not Event or RulerAction, then check existence on the cluster
77-
if err != nil && !triggeredByEvent {
93+
if err != nil {
7894

7995
// 2.1 It does NOT exist: manage removal
8096
if err = client.IgnoreNotFound(err); err == nil {
@@ -88,7 +104,7 @@ func (r *RulerActionReconciler) Reconcile(ctx context.Context, req ctrl.Request)
88104
}
89105

90106
// 3. Check if the SearchRule instance is marked to be deleted: indicated by the deletion timestamp being set
91-
if !RulerActionResource.DeletionTimestamp.IsZero() && !triggeredByEvent {
107+
if !RulerActionResource.DeletionTimestamp.IsZero() {
92108
if controllerutil.ContainsFinalizer(RulerActionResource, resourceFinalizer) {
93109
// Remove the finalizers on Patch CR
94110
controllerutil.RemoveFinalizer(RulerActionResource, resourceFinalizer)
@@ -104,7 +120,7 @@ func (r *RulerActionReconciler) Reconcile(ctx context.Context, req ctrl.Request)
104120
}
105121

106122
// 4. Add finalizer to the SearchRule CR
107-
if !controllerutil.ContainsFinalizer(RulerActionResource, resourceFinalizer) && !triggeredByEvent {
123+
if !controllerutil.ContainsFinalizer(RulerActionResource, resourceFinalizer) {
108124
controllerutil.AddFinalizer(RulerActionResource, resourceFinalizer)
109125
err = r.Update(ctx, RulerActionResource)
110126
if err != nil {
@@ -118,22 +134,27 @@ func (r *RulerActionReconciler) Reconcile(ctx context.Context, req ctrl.Request)
118134
if err != nil {
119135
logger.Info(fmt.Sprintf(resourceConditionUpdateError, RulerActionResourceType, req.NamespacedName, err.Error()))
120136
}
137+
121138
}()
122139

123140
// 6. Schedule periodical request
124-
if !triggeredByEvent {
125-
RequeueTime, err := time.ParseDuration(RulerActionResource.Spec.FiringInterval)
126-
if err != nil {
127-
logger.Info(fmt.Sprintf(resourceSyncTimeRetrievalError, RulerActionResourceType, req.NamespacedName, err.Error()))
128-
return result, err
129-
}
130-
result = ctrl.Result{
131-
RequeueAfter: RequeueTime,
132-
}
133-
}
141+
// if !triggeredByEvent {
142+
// RequeueTime, err := time.ParseDuration(RulerActionResource.Spec.FiringInterval)
143+
// if err != nil {
144+
// logger.Info(fmt.Sprintf(resourceSyncTimeRetrievalError, RulerActionResourceType, req.NamespacedName, err.Error()))
145+
// return result, err
146+
// }
147+
// result = ctrl.Result{
148+
// RequeueAfter: RequeueTime,
149+
// }
150+
// }
134151

135152
// 7. Sync credentials if defined
136153
err = r.Sync(ctx, RulerActionResource)
154+
if err != nil {
155+
logger.Info(fmt.Sprintf("error: %v", err.Error()))
156+
return result, err
157+
}
137158
if err != nil {
138159
r.UpdateConditionKubernetesApiCallFailure(RulerActionResource)
139160
logger.Info(fmt.Sprintf(syncTargetError, RulerActionResourceType, req.NamespacedName, err.Error()))
@@ -162,5 +183,6 @@ func (r *RulerActionReconciler) SetupWithManager(mgr ctrl.Manager) error {
162183
return false
163184
},
164185
}).
186+
Watches(&corev1.Event{}, &handler.EnqueueRequestForObject{}).
165187
Complete(r)
166188
}

internal/controller/ruleraction_sync.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,28 @@ import (
1111
"k8s.io/apimachinery/pkg/types"
1212
"prosimcorp.com/SearchRuler/api/v1alpha1"
1313
"prosimcorp.com/SearchRuler/internal/template"
14+
"prosimcorp.com/SearchRuler/internal/validators"
1415
"sigs.k8s.io/controller-runtime/pkg/log"
1516
)
1617

18+
const (
19+
//
20+
HttpEventPattern = `{"data":"%s","timestamp":"%s"}`
21+
22+
//
23+
ValidatorNotFoundErrorMessage = "validator %s not found"
24+
ValidationFailedErrorMessage = "validation failed: %s"
25+
HttpRequestCreationErrorMessage = "error creating http request: %s"
26+
HttpRequestSendingErrorMessage = "error sending http request: %s"
27+
)
28+
29+
var (
30+
// validatorsMap is a map of integration names and their respective validation functions
31+
validatorsMap = map[string]func(data string) (result bool, hint string, err error){
32+
"alertmanager": validators.ValidateAlertmanager,
33+
}
34+
)
35+
1736
// Sync
1837
func (r *RulerActionReconciler) Sync(ctx context.Context, resource *v1alpha1.RulerAction) (err error) {
1938

@@ -44,7 +63,6 @@ func (r *RulerActionReconciler) Sync(ctx context.Context, resource *v1alpha1.Rul
4463
}
4564

4665
// Check alerts
47-
logger.Info(fmt.Sprintf("Triggered by: %s", resource.Name))
4866
alerts := r.AlertsPool.GetByRegex(fmt.Sprintf("%s/%s/*", resource.Namespace, resource.Name))
4967
for _, alert := range alerts {
5068

@@ -62,6 +80,25 @@ func (r *RulerActionReconciler) Sync(ctx context.Context, resource *v1alpha1.Rul
6280
httpRequest.Header.Set(headerKey, headerValue)
6381
}
6482

83+
// Check if the webhook has a validator and execute it when available
84+
if resource.Spec.Webhook.Validator != "" {
85+
86+
_, validatorFound := validatorsMap[resource.Spec.Webhook.Validator]
87+
if !validatorFound {
88+
return fmt.Errorf(ValidatorNotFoundErrorMessage, resource.Spec.Webhook.Validator)
89+
}
90+
91+
//
92+
validatorResult, validatorHint, err := validatorsMap[resource.Spec.Webhook.Validator](alert.SearchRule.Spec.ActionRef.Data)
93+
if err != nil {
94+
return fmt.Errorf(ValidationFailedErrorMessage, err.Error())
95+
}
96+
97+
if !validatorResult {
98+
return fmt.Errorf(ValidationFailedErrorMessage, validatorHint)
99+
}
100+
}
101+
65102
// Add data to the request
66103
templateInjectedObject := map[string]interface{}{}
67104
templateInjectedObject["value"] = alert.Value

internal/controller/searchrule_ruler.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ func evaluateCondition(value float64, operator string, threshold string) (bool,
7070

7171
// createKubeEvent creates a modern event in Kubernetes with data given by params
7272
func createKubeEvent(ctx context.Context, rule v1alpha1.SearchRule, action, message string) (err error) {
73-
7473
eventObj := eventsv1.Event{
7574
ObjectMeta: metav1.ObjectMeta{
7675
GenerateName: "alert-",

internal/validators/validators.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package validators
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"reflect"
7+
)
8+
9+
const (
10+
amgrAlertDataUnmarshalErrorMessage = "error decoding JSON from 'message.data' for Alertmanager validator: %s"
11+
amgrAlertDataRequiredStructureErrorMessage = "notification field 'message.data' does not meet the syntax requirements for Alertmanager: %s"
12+
)
13+
14+
// TODO
15+
type AlertmanagerAlertList []AlertmanagerAlert
16+
17+
// Alert represents the structure of an alert in Alertmanager
18+
// Ref: https://prometheus.io/docs/alerting/latest/clients/
19+
// Ref: https://raw.githubusercontent.com/prometheus/alertmanager/main/api/v2/openapi.yaml
20+
type AlertmanagerAlert struct {
21+
Labels map[string]string `json:"labels"`
22+
Annotations map[string]string `json:"annotations"`
23+
24+
// Optional params
25+
StartsAt string `json:"startsAt,omitempty"`
26+
EndsAt string `json:"endsAt,omitempty"`
27+
GeneratorUrl string `json:"generatorURL,omitempty"`
28+
}
29+
30+
// ValidateAlertmanager checks whether the notification data meets the requirements for Alertmanager
31+
func ValidateAlertmanager(data string) (result bool, hint string, err error) {
32+
33+
alertList := AlertmanagerAlertList{AlertmanagerAlert{}}
34+
35+
//
36+
err = json.Unmarshal([]byte(data), &alertList)
37+
if err != nil {
38+
return false, hint, fmt.Errorf(amgrAlertDataUnmarshalErrorMessage, err)
39+
}
40+
41+
if reflect.ValueOf(alertList).IsZero() {
42+
return false, amgrAlertDataRequiredStructureErrorMessage, nil
43+
}
44+
45+
//
46+
for _, alert := range alertList {
47+
48+
// Check the main label for the alert.
49+
// This is required only on API v1
50+
_, alertNameFound := alert.Labels["alertname"]
51+
if !alertNameFound {
52+
hint = fmt.Sprintf("%s: %s", amgrAlertDataRequiredStructureErrorMessage, "label 'alertname' not found")
53+
return false, hint, nil
54+
}
55+
56+
// Check whether startAt field is set
57+
if alert.StartsAt == "" {
58+
hint = fmt.Sprintf("%s: %s", amgrAlertDataRequiredStructureErrorMessage, "field 'startsAt' not found")
59+
return false, hint, nil
60+
}
61+
}
62+
63+
return true, hint, nil
64+
}

0 commit comments

Comments
 (0)