pagerduty-cachet

git clone git://archive.git.mtrnord.blog/MTRNord/pagerduty-cachet.git
Log | Files | Refs | README

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 }