server.go (12892B)
1 package main 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "log" 10 "net/http" 11 "os" 12 "time" 13 14 "github.com/MTRNord/cachet_go" 15 "github.com/PagerDuty/go-pagerduty" 16 "github.com/PagerDuty/go-pagerduty/webhookv3" 17 "github.com/gorilla/mux" 18 "github.com/maniartech/gotime" 19 ) 20 21 var ( 22 cachetURL = os.Getenv("CACHET_URL") 23 cachetKey = os.Getenv("CACHET_KEY") 24 pagerdutyKey = os.Getenv("PAGERDUTY_KEY") 25 secret = os.Getenv("WEBHOOK_SECRET") 26 ) 27 28 func syncMaintenanceWindows() { 29 client := pagerduty.NewClient(pagerdutyKey) 30 31 cachetClient, _ := cachet_go.NewClient(cachetURL, nil) 32 cachetClient.Authentication.SetTokenAuth(cachetKey) 33 _, status, err := cachetClient.General.Ping() 34 if err != nil || status.StatusCode != 200 { 35 log.Println(err) 36 return 37 } 38 39 // Fethc maintenance windows every 30 minutes 40 for { 41 maintenanceWindows, err := fetchMaintenanceWindows(client) 42 if err != nil { 43 log.Println(err) 44 continue 45 } 46 log.Printf("Fetched %d maintenance windows\n", len(maintenanceWindows)) 47 48 // Set them in cachet as schedules if they don't exist 49 50 // Get all schedules 51 schedules, _, err := cachetClient.Schedules.GetAll(&cachet_go.SchedulesQueryParams{ 52 QueryOptions: cachet_go.QueryOptions{ 53 PerPage: 100, 54 }, 55 }) 56 if err != nil { 57 log.Println(err) 58 continue 59 } 60 61 // Create a map of schedules by start and end time 62 schedulesMap := map[string]*cachet_go.Schedule{} 63 for _, s := range schedules.Schedules { 64 schedulesMap[s.ScheduledAt+"_"+s.CompletedAt] = &s 65 } 66 67 // Load Europe/Berlin timezone 68 loc, err := time.LoadLocation("Europe/Berlin") 69 if err != nil { 70 log.Println(err) 71 continue 72 } 73 74 // Create a map of maintenance windows by start and end time if they don't exist 75 for _, mw := range maintenanceWindows { 76 // Convert the pagerduty time in the format "2015-11-09T20:00:00-05:00" to "Y-m-d H:i:sO". 77 // Keep in mind that cachet is in Europe/Berlin timezone 78 // Keep in mind that pagerduty is giving us a string 79 80 // Parse the start and end time 81 startTime, err := time.Parse(time.RFC3339, mw.StartTime) 82 if err != nil { 83 log.Println(err) 84 continue 85 } 86 endTime, err := time.Parse(time.RFC3339, mw.EndTime) 87 if err != nil { 88 log.Println(err) 89 continue 90 } 91 92 // If the endTime is before now, skip 93 if endTime.Before(time.Now()) { 94 continue 95 } 96 97 // Convert the start and end time to Europe/Berlin timezone 98 startTime = startTime.In(loc) 99 endTime = endTime.In(loc) 100 101 // Convert the start and end time to the format "Y-m-d H:i:sO" 102 mw.StartTime = gotime.Format(startTime, "yyyy-mm-dd hhh:ii") 103 mw.EndTime = gotime.Format(endTime, "yyyy-mm-dd hhh:ii") 104 // Also convert it with space instead of + for the lookup table 105 mwStart := gotime.Format(startTime, "yyyy-mm-dd hhh:ii:ss") 106 mwEnd := gotime.Format(endTime, "yyyy-mm-dd hhh:ii:ss") 107 108 // If there is no summary then we want to set it to "Maintenance" 109 if mw.Description == "" { 110 mw.Description = "Maintenance" 111 } 112 113 log.Printf("Maintenance window (%s): %s - %s\n", mw.Summary, mw.StartTime, mw.EndTime) 114 115 var components []cachet_go.Component 116 if mw.Services != nil { 117 for _, s := range mw.Services { 118 component, err := findComponentByName(cachetClient, s.Summary) 119 if err != nil { 120 log.Println(err) 121 continue 122 } 123 if component != nil { 124 components = append(components, *component) 125 } 126 } 127 } 128 129 if _, ok := schedulesMap[mwStart+"_"+mwEnd]; !ok { 130 newSchedule := &cachet_go.Schedule{ 131 Name: mw.Description, 132 Message: mw.Description, 133 Status: "0", 134 ScheduledAt: mw.StartTime, 135 CompletedAt: mw.EndTime, 136 Components: components, 137 } 138 // Print the schedule to the console 139 log.Printf("%+v\n", newSchedule) 140 141 _, _, err := cachetClient.Schedules.Create(newSchedule) 142 if err != nil { 143 log.Println(err) 144 continue 145 } 146 } 147 } 148 149 time.Sleep(30 * time.Minute) 150 } 151 } 152 153 func main() { 154 router := mux.NewRouter() 155 156 router.HandleFunc("/webhook", handler).Methods("POST") 157 router.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { 158 w.WriteHeader(http.StatusOK) 159 }).Methods("GET") 160 router.Use(mux.CORSMethodMiddleware(router)) 161 http.Handle("/", router) 162 163 go syncMaintenanceWindows() 164 165 log.Println("Listening on 0.0.0.0:8080") 166 log.Fatal(http.ListenAndServe("0.0.0.0:8080", router)) 167 } 168 169 func findComponentByName(client *cachet_go.Client, name string) (*cachet_go.Component, error) { 170 components, _, err := client.Components.GetAll(&cachet_go.ComponentsQueryParams{ 171 QueryOptions: cachet_go.QueryOptions{ 172 PerPage: 100, 173 }, 174 }) 175 if err != nil { 176 return nil, err 177 } 178 179 for _, c := range components.Components { 180 if c.Name == name { 181 return &c, nil 182 } 183 } 184 185 return nil, nil 186 } 187 188 func fetchMaintenanceWindows(client *pagerduty.Client) ([]pagerduty.MaintenanceWindow, error) { 189 ctx := context.TODO() 190 maintenanceWindows := []pagerduty.MaintenanceWindow{} 191 opts := pagerduty.ListMaintenanceWindowsOptions{} 192 for { 193 resp, err := client.ListMaintenanceWindowsWithContext(ctx, opts) 194 if err != nil { 195 return nil, err 196 } 197 maintenanceWindows = append(maintenanceWindows, resp.MaintenanceWindows...) 198 if !resp.More { 199 break 200 } 201 opts.Offset = resp.Offset 202 } 203 return maintenanceWindows, nil 204 } 205 206 func fetchIncident(client *cachet_go.Client, incidentID string) (*cachet_go.Incident, error) { 207 incidents, _, err := client.Incidents.GetAll(&cachet_go.IncidentsQueryParams{ 208 QueryOptions: cachet_go.QueryOptions{ 209 PerPage: 100, 210 }, 211 }) 212 if err != nil { 213 return nil, err 214 } 215 216 for _, i := range incidents.Incidents { 217 v, ok := i.Meta.(map[string]interface{}) 218 if ok { 219 v, ok := v["pagerduty"].(map[string]interface{}) 220 if ok { 221 if v["incident_id"] == incidentID { 222 return &i, nil 223 } 224 } 225 } 226 } 227 228 return nil, nil 229 } 230 231 func handler(w http.ResponseWriter, r *http.Request) { 232 client, _ := cachet_go.NewClient(cachetURL, nil) 233 client.Authentication.SetTokenAuth(cachetKey) 234 _, status, err := client.General.Ping() 235 if err != nil || status.StatusCode != 200 { 236 log.Println(err) 237 w.WriteHeader(http.StatusInternalServerError) 238 return 239 } 240 241 err = webhookv3.VerifySignature(r, secret) 242 if err != nil { 243 switch err { 244 case webhookv3.ErrNoValidSignatures: 245 log.Println("no valid signatures") 246 w.WriteHeader(http.StatusUnauthorized) 247 248 case webhookv3.ErrMalformedBody, webhookv3.ErrMalformedHeader: 249 log.Println("malformed body or header") 250 w.WriteHeader(http.StatusBadRequest) 251 252 default: 253 log.Println("internal server error") 254 w.WriteHeader(http.StatusInternalServerError) 255 } 256 log.Println(err) 257 258 fmt.Fprintf(w, "%v", err) 259 return 260 } 261 262 // Read body as json 263 bytedata, err := io.ReadAll(r.Body) 264 r.Body.Close() 265 if err != nil { 266 log.Println("error reading body") 267 w.WriteHeader(http.StatusBadRequest) 268 fmt.Fprintf(w, "error reading body") 269 return 270 } 271 272 r.Body = io.NopCloser(bytes.NewBuffer(bytedata)) 273 decodedJSON := WebhookMinimalEvent{} 274 err = json.NewDecoder(r.Body).Decode(&decodedJSON) 275 if err != nil { 276 log.Println("error decoding json") 277 w.WriteHeader(http.StatusBadRequest) 278 fmt.Fprintf(w, "error decoding json") 279 return 280 } 281 r.Body = io.NopCloser(bytes.NewBuffer(bytedata)) 282 283 if decodedJSON.Event.EventType == "incident.triggered" { 284 incident := WebhookIncidentTriggered{} 285 err = json.NewDecoder(r.Body).Decode(&incident) 286 if err != nil { 287 log.Println("error decoding json") 288 w.WriteHeader(http.StatusBadRequest) 289 fmt.Fprintf(w, "error decoding json") 290 return 291 } 292 log.Printf("%+v\n", incident) 293 294 // Get description or fallback to title 295 description := "This incident has been triggered automatically. Please stand by for updates." 296 297 // If the urgency is high, set the status to degraded otherwise set the component status to normal 298 componentStatus := cachet_go.ComponentStatusOperational 299 if incident.Event.Data.Urgency == "high" { 300 componentStatus = cachet_go.ComponentStatusPerformanceIssues 301 } 302 303 /* 304 Create a meta map which looks like this: 305 { 306 "meta": { 307 "pagerduty": { 308 "incident_id": "ABC" 309 } 310 } 311 } 312 */ 313 vars := map[string]map[string]string{ 314 "pagerduty": { 315 "incident_id": incident.Event.Data.ID, 316 }, 317 } 318 319 // Create a new incident in cachet 320 cachetIncident := &cachet_go.Incident{ 321 Name: incident.Event.Data.Title, 322 Message: description, 323 Status: cachet_go.IncidentStatusInvestigating, 324 ComponentID: 1, 325 ComponentStatus: componentStatus, 326 Meta: vars, 327 } 328 _, cachetResp, err := client.Incidents.Create(cachetIncident) 329 if err != nil { 330 log.Println(err) 331 // Print the cachetResp body as a string to the console 332 bytedata, err := io.ReadAll(cachetResp.Body) 333 r.Body.Close() 334 if err != nil { 335 log.Println("error reading cachetResp body") 336 w.WriteHeader(http.StatusInternalServerError) 337 return 338 } 339 log.Println(string(bytedata)) 340 w.WriteHeader(http.StatusInternalServerError) 341 return 342 } 343 344 } else if decodedJSON.Event.EventType == "incident.acknowledged" { 345 incident := WebhookIncidentAcknowledged{} 346 err = json.NewDecoder(r.Body).Decode(&incident) 347 if err != nil { 348 log.Println("error decoding json") 349 w.WriteHeader(http.StatusBadRequest) 350 fmt.Fprintf(w, "error decoding json") 351 return 352 } 353 log.Printf("%+v\n", incident) 354 355 cachetIncident, err := fetchIncident(client, incident.Event.Data.ID) 356 if err != nil { 357 log.Println(err) 358 w.WriteHeader(http.StatusInternalServerError) 359 return 360 } 361 362 // Fallthrough without error if the incident is not found 363 if cachetIncident == nil { 364 log.Println("incident not found") 365 return 366 } 367 368 newCachetIncidentUpdate := &cachet_go.IncidentUpdate{ 369 Status: cachet_go.IncidentStatusWatching, 370 HumanStatus: "Watching", 371 Message: "This incident has been acknowledged. We are currently investigating the issue. Thank you for your patience.", 372 ComponentID: cachetIncident.ComponentID, 373 ComponentStatus: cachet_go.ComponentStatusPerformanceIssues, 374 } 375 _, cachetResp, err := client.IncidentUpdates.Create(cachetIncident.ID, newCachetIncidentUpdate) 376 if err != nil { 377 log.Println(err) 378 // Print the cachetResp body as a string to the console 379 bytedata, err := io.ReadAll(cachetResp.Body) 380 r.Body.Close() 381 if err != nil { 382 log.Println("error reading cachetResp body") 383 w.WriteHeader(http.StatusInternalServerError) 384 return 385 } 386 log.Println(string(bytedata)) 387 w.WriteHeader(http.StatusInternalServerError) 388 return 389 } 390 391 } else if decodedJSON.Event.EventType == "incident.resolved" { 392 incident := WebhookIncidentResolved{} 393 err = json.NewDecoder(r.Body).Decode(&incident) 394 if err != nil { 395 log.Println("error decoding json") 396 w.WriteHeader(http.StatusBadRequest) 397 fmt.Fprintf(w, "error decoding json") 398 return 399 } 400 log.Printf("%+v\n", incident) 401 402 cachetIncident, err := fetchIncident(client, incident.Event.Data.ID) 403 if err != nil { 404 log.Println(err) 405 w.WriteHeader(http.StatusInternalServerError) 406 return 407 } 408 409 // Fallthrough without error if the incident is not found 410 if cachetIncident == nil { 411 log.Println("incident not found") 412 return 413 } 414 415 newCachetIncidentUpdate := &cachet_go.IncidentUpdate{ 416 Status: cachet_go.IncidentStatusFixed, 417 HumanStatus: "Fixed", 418 Message: "This incident has been resolved. Thank you for your patience.", 419 ComponentID: cachetIncident.ComponentID, 420 ComponentStatus: cachet_go.ComponentStatusOperational, 421 } 422 423 _, cachetResp, err := client.IncidentUpdates.Create(cachetIncident.ID, newCachetIncidentUpdate) 424 if err != nil { 425 log.Println(err) 426 // Print the cachetResp body as a string to the console 427 bytedata, err := io.ReadAll(cachetResp.Body) 428 r.Body.Close() 429 if err != nil { 430 log.Println("error reading cachetResp body") 431 w.WriteHeader(http.StatusInternalServerError) 432 return 433 } 434 log.Println(string(bytedata)) 435 w.WriteHeader(http.StatusInternalServerError) 436 return 437 } 438 439 // Update the component status to operational 440 _, cachetResp, err = client.Components.Update(cachetIncident.ComponentID, &cachet_go.Component{ 441 ID: cachetIncident.ComponentID, 442 Status: cachet_go.ComponentStatusOperational, 443 }) 444 if err != nil { 445 log.Println(err) 446 // Print the cachetResp body as a string to the console 447 bytedata, err := io.ReadAll(cachetResp.Body) 448 r.Body.Close() 449 if err != nil { 450 log.Println("error reading cachetResp body") 451 w.WriteHeader(http.StatusInternalServerError) 452 return 453 } 454 log.Println(string(bytedata)) 455 w.WriteHeader(http.StatusInternalServerError) 456 return 457 } 458 } else { 459 log.Println("unknown event type") 460 461 // Print the body as a string to the console 462 log.Println(string(bytedata)) 463 464 w.WriteHeader(http.StatusBadRequest) 465 fmt.Fprintf(w, "unknown event type") 466 return 467 } 468 469 fmt.Fprintf(w, "received signed webhook") 470 }