@@ -4,12 +4,17 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7+ "io"
8+ "log"
79 "net/http"
10+ "strconv"
11+ "strings"
812
913 "github.com/gorilla/mux"
1014 redisv1beta1 "github.com/jaehanbyun/redis-operator/api/v1beta1"
1115 "github.com/jaehanbyun/redis-operator/k8sutils"
1216 corev1 "k8s.io/api/core/v1"
17+ "k8s.io/apimachinery/pkg/types"
1318 "sigs.k8s.io/controller-runtime/pkg/client"
1419)
1520
@@ -18,15 +23,48 @@ type node struct {
1823 Port int32 `json:"port"`
1924}
2025
26+ type AlertManagerPayload struct {
27+ Receiver string `json:"receiver"`
28+ Status string `json:"status"`
29+ Alerts []Alert `json:"alerts"`
30+ GroupLabels map [string ]string `json:"groupLabels"`
31+ CommonLabels map [string ]string `json:"commonLabels"`
32+ CommonAnnotations map [string ]string `json:"commonAnnotations"`
33+ ExternalURL string `json:"externalURL"`
34+ }
35+
36+ type Alert struct {
37+ Status string `json:"status"`
38+ Labels map [string ]string `json:"labels"`
39+ Annotations map [string ]string `json:"annotations"`
40+ StartsAt string `json:"startsAt"`
41+ EndsAt string `json:"endsAt"`
42+ GeneratorURL string `json:"generatorURL"`
43+ Fingerprint string `json:"fingerprint"`
44+ }
45+
46+ type AutoScalingAction struct {
47+ Type string `json:"type"` // "master" or "replica"
48+ ClusterName string `json:"clusterName"`
49+ Namespace string `json:"namespace"`
50+ Count int32 `json:"count"`
51+ ScalingAction string `json:"scalingAction"` // "up" or "down"
52+ }
53+
2154func StartHTTPServer (cl client.Client ) error {
2255 r := mux .NewRouter ()
2356 r .HandleFunc ("/cluster/nodes" , func (w http.ResponseWriter , r * http.Request ) {
2457 handleClusterNodesRequest (w , r , cl )
2558 }).Methods ("GET" )
2659
60+ r .HandleFunc ("/webhooks/alertmanager" , func (w http.ResponseWriter , r * http.Request ) {
61+ handleAlertManagerWebhook (w , r , cl )
62+ }).Methods ("POST" )
63+
2764 return http .ListenAndServe (":9090" , r )
2865}
2966
67+ // handleClusterNodesRequest is a function that handles the cluster nodes request.
3068func handleClusterNodesRequest (w http.ResponseWriter , r * http.Request , cl client.Client ) {
3169 clusterName := r .URL .Query ().Get ("clusterName" )
3270 if clusterName == "" {
@@ -79,3 +117,133 @@ func handleClusterNodesRequest(w http.ResponseWriter, r *http.Request, cl client
79117 return
80118 }
81119}
120+
121+ // handleAlertManagerWebhook is a function that handles Alertmanager webhook.
122+ func handleAlertManagerWebhook (w http.ResponseWriter , r * http.Request , cl client.Client ) {
123+ body , err := io .ReadAll (r .Body )
124+ if err != nil {
125+ http .Error (w , "Failed to read request body" , http .StatusBadRequest )
126+ return
127+ }
128+ defer r .Body .Close ()
129+
130+ var payload AlertManagerPayload
131+ if err := json .Unmarshal (body , & payload ); err != nil {
132+ http .Error (w , "Invalid JSON format" , http .StatusBadRequest )
133+ return
134+ }
135+
136+ for _ , alert := range payload .Alerts {
137+ if alert .Status == "firing" {
138+ action , err := parseAlertAction (alert )
139+ if err != nil {
140+ log .Printf ("Failed to parse alert action: %v" , err )
141+ continue
142+ }
143+
144+ if err := applyAutoScaling (cl , action ); err != nil {
145+ log .Printf ("Failed to apply auto-scaling: %v" , err )
146+ continue
147+ }
148+
149+ log .Printf ("Successfully applied auto-scaling: %+v" , action )
150+ }
151+ }
152+
153+ w .WriteHeader (http .StatusOK )
154+ if _ , err := w .Write ([]byte ("Alerts processed" )); err != nil {
155+ log .Printf ("Error writing response: %v" , err )
156+ }
157+ }
158+
159+ func parseAlertAction (alert Alert ) (* AutoScalingAction , error ) {
160+ clusterName , ok := alert .Labels ["redis_cluster" ]
161+ if ! ok {
162+ return nil , fmt .Errorf ("missing required label: redis_cluster" )
163+ }
164+
165+ namespace , ok := alert .Labels ["namespace" ]
166+ if ! ok {
167+ namespace = "default"
168+ }
169+
170+ var action AutoScalingAction
171+ action .ClusterName = clusterName
172+ action .Namespace = namespace
173+
174+ alertName , ok := alert .Labels ["alertname" ]
175+ if ! ok {
176+ return nil , fmt .Errorf ("missing required label: alertname" )
177+ }
178+
179+ switch {
180+ case strings .Contains (alertName , "HighMemoryUsage" ):
181+ action .Type = "master"
182+ action .ScalingAction = "up"
183+ action .Count = 1
184+ case strings .Contains (alertName , "HighThroughput" ):
185+ action .Type = "replica"
186+ action .ScalingAction = "up"
187+ action .Count = 1
188+ case strings .Contains (alertName , "LowMemoryUsage" ):
189+ action .Type = "master"
190+ action .ScalingAction = "down"
191+ action .Count = 1
192+ case strings .Contains (alertName , "LowThroughput" ):
193+ action .Type = "replica"
194+ action .ScalingAction = "down"
195+ action .Count = 1
196+ default :
197+ return nil , fmt .Errorf ("unknown alert name: %s" , alertName )
198+ }
199+
200+ if countStr , ok := alert .Annotations ["scale_count" ]; ok {
201+ count , err := strconv .ParseInt (countStr , 10 , 32 )
202+ if err == nil && count > 0 {
203+ action .Count = int32 (count )
204+ }
205+ }
206+
207+ return & action , nil
208+ }
209+
210+ // applyAutoScaling is a function that applies auto-scaling to a Redis cluster.
211+ func applyAutoScaling (cl client.Client , action * AutoScalingAction ) error {
212+ var redisCluster redisv1beta1.RedisCluster
213+ if err := cl .Get (context .Background (), types.NamespacedName {
214+ Name : action .ClusterName ,
215+ Namespace : action .Namespace ,
216+ }, & redisCluster ); err != nil {
217+ return fmt .Errorf ("failed to get RedisCluster: %v" , err )
218+ }
219+
220+ // 타입에 따라 마스터 또는 레플리카 수 조정
221+ switch action .Type {
222+ case "master" :
223+ if action .ScalingAction == "up" {
224+ redisCluster .Spec .Masters += action .Count
225+ } else if action .ScalingAction == "down" && redisCluster .Spec .Masters > action .Count {
226+ redisCluster .Spec .Masters -= action .Count
227+ } else {
228+ return fmt .Errorf ("invalid scaling down action: current masters %d, scaling down by %d" ,
229+ redisCluster .Spec .Masters , action .Count )
230+ }
231+ case "replica" :
232+ if action .ScalingAction == "up" {
233+ redisCluster .Spec .Replicas += action .Count
234+ } else if action .ScalingAction == "down" && redisCluster .Spec .Replicas > action .Count {
235+ redisCluster .Spec .Replicas -= action .Count
236+ } else {
237+ return fmt .Errorf ("invalid scaling down action: current replicas %d, scaling down by %d" ,
238+ redisCluster .Spec .Replicas , action .Count )
239+ }
240+ default :
241+ return fmt .Errorf ("unknown scaling type: %s" , action .Type )
242+ }
243+
244+ if err := cl .Update (context .Background (), & redisCluster ); err != nil {
245+ return fmt .Errorf ("failed to update RedisCluster: %v" , err )
246+ }
247+
248+ return nil
249+ }
0 commit comments