pagerduty-cachet

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

commit d3ba1ddf513c536b964938a86d016c7fb28235c9
Author: MTRNord <mtrnord1@gmail.com>
Date:   Mon,  7 Oct 2024 00:51:11 +0200

Initial commit

Diffstat:
A.gitignore | 2++
ADockerfile | 18++++++++++++++++++
AREADME.md | 1+
Ago.mod | 14++++++++++++++
Ago.sum | 26++++++++++++++++++++++++++
Aserver.go | 445+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atypes.go | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 694 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +pagerduty_cachet +\ No newline at end of file diff --git a/Dockerfile b/Dockerfile @@ -0,0 +1,17 @@ +# A distroless image in a multi-stage build ready for multi-arch builds + +FROM golang:1.23 AS build + +WORKDIR /usr/src/app + +# pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change +COPY go.mod go.sum ./ +RUN go mod download && go mod verify + +COPY . . +RUN CGO_ENABLED=0 go build -o /go/bin/app + +FROM gcr.io/distroless/static-debian12 + +COPY --from=build /go/bin/app / +CMD ["/app"] +\ No newline at end of file diff --git a/README.md b/README.md @@ -0,0 +1 @@ +Quick and dirty script to convert pagerduty data to cachet data diff --git a/go.mod b/go.mod @@ -0,0 +1,14 @@ +module github.com/MTRNord/pagerduty_cachet + +go 1.22.7 + +require ( + github.com/MTRNord/cachet_go v0.0.0-20241006221602-7ba2699ecb22 + github.com/PagerDuty/go-pagerduty v1.8.0 + github.com/gorilla/mux v1.8.1 +) + +require ( + github.com/google/go-querystring v1.1.0 // indirect + github.com/maniartech/gotime v1.1.0 +) diff --git a/go.sum b/go.sum @@ -0,0 +1,26 @@ +github.com/MTRNord/cachet_go v0.0.0-20241006205042-f6a7cfbf083a h1:p59KqmFrtLOTfC/pjG/xZz64MTfvQXZsS2vCCj+/cXw= +github.com/MTRNord/cachet_go v0.0.0-20241006205042-f6a7cfbf083a/go.mod h1:/ZPTgU1MGuo0ljNQBg/lKyaCStNtuKKsUGL2Qa7wD8I= +github.com/MTRNord/cachet_go v0.0.0-20241006205608-5315f3f91883 h1:0iYD4qyhdQAJ1VSIuJSh3djAbE+7mReer9Qb6hCS+e0= +github.com/MTRNord/cachet_go v0.0.0-20241006205608-5315f3f91883/go.mod h1:/ZPTgU1MGuo0ljNQBg/lKyaCStNtuKKsUGL2Qa7wD8I= +github.com/MTRNord/cachet_go v0.0.0-20241006220744-fd9a34f22726 h1:He+2TKExSz4BZjMhKnYFwjMdgUwpExVoQkpxAKCUcMU= +github.com/MTRNord/cachet_go v0.0.0-20241006220744-fd9a34f22726/go.mod h1:/ZPTgU1MGuo0ljNQBg/lKyaCStNtuKKsUGL2Qa7wD8I= +github.com/MTRNord/cachet_go v0.0.0-20241006220857-4b4dfdb80106 h1:K3hamw51KRc24wZvcyzpNX/5IZ65QYcDNguepdFpGeI= +github.com/MTRNord/cachet_go v0.0.0-20241006220857-4b4dfdb80106/go.mod h1:/ZPTgU1MGuo0ljNQBg/lKyaCStNtuKKsUGL2Qa7wD8I= +github.com/MTRNord/cachet_go v0.0.0-20241006221122-19db994469fe h1:nK9AXe5HrbeQBYlcUPU4slfbG9P2gfG+qsaTcyNg++A= +github.com/MTRNord/cachet_go v0.0.0-20241006221122-19db994469fe/go.mod h1:/ZPTgU1MGuo0ljNQBg/lKyaCStNtuKKsUGL2Qa7wD8I= +github.com/MTRNord/cachet_go v0.0.0-20241006221602-7ba2699ecb22 h1:wTOWvpulSKZMnP+QwDETx3fKtf8zgpCjrc30PE2rDI0= +github.com/MTRNord/cachet_go v0.0.0-20241006221602-7ba2699ecb22/go.mod h1:/ZPTgU1MGuo0ljNQBg/lKyaCStNtuKKsUGL2Qa7wD8I= +github.com/PagerDuty/go-pagerduty v1.8.0 h1:MTFqTffIcAervB83U7Bx6HERzLbyaSPL/+oxH3zyluI= +github.com/PagerDuty/go-pagerduty v1.8.0/go.mod h1:nzIeAqyFSJAFkjWKvMzug0JtwDg+V+UoCWjFrfFH5mI= +github.com/andygrunwald/cachet v0.0.0-20210622170326-34d0d1408d1c h1:4IMzwKxv5CE0nmdxlfTNEq0lSxORvaLsGGVpSDaH6U4= +github.com/andygrunwald/cachet v0.0.0-20210622170326-34d0d1408d1c/go.mod h1:75LSejNsjxG/979su0CUUAJ956qsllNywDdBZwh4ByQ= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/maniartech/gotime v1.1.0 h1:/xsp3E0o0zPQ4pjCNOs3NGWMk/mpCQNNaGNfhTbps5E= +github.com/maniartech/gotime v1.1.0/go.mod h1:NBM2sTwhRbRF87XPpPBAugxd0H3B3GT0W85SJYvb+To= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/server.go b/server.go @@ -0,0 +1,445 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "time" + + "github.com/MTRNord/cachet_go" + "github.com/PagerDuty/go-pagerduty" + "github.com/PagerDuty/go-pagerduty/webhookv3" + "github.com/gorilla/mux" + "github.com/maniartech/gotime" +) + +var ( + cachetURL = os.Getenv("CACHET_URL") + cachetKey = os.Getenv("CACHET_KEY") + pagerdutyKey = os.Getenv("PAGERDUTY_KEY") + secret = os.Getenv("WEBHOOK_SECRET") +) + +func main() { + router := mux.NewRouter() + + router.HandleFunc("/webhook", handler).Methods("POST") + router.Use(mux.CORSMethodMiddleware(router)) + http.Handle("/", router) + + go func() { + client := pagerduty.NewClient(pagerdutyKey) + + cachetClient, _ := cachet_go.NewClient(cachetURL, nil) + cachetClient.Authentication.SetTokenAuth(cachetKey) + _, status, err := cachetClient.General.Ping() + if err != nil || status.StatusCode != 200 { + log.Println(err) + return + } + + // Fethc maintenance windows every 30 minutes + for { + maintenanceWindows, err := fetchMaintenanceWindows(client) + if err != nil { + log.Println(err) + continue + } + log.Printf("Fetched %d maintenance windows\n", len(maintenanceWindows)) + + // Set them in cachet as schedules if they don't exist + + // Get all schedules + schedules, _, err := cachetClient.Schedules.GetAll(&cachet_go.SchedulesQueryParams{ + QueryOptions: cachet_go.QueryOptions{ + PerPage: 100, + }, + }) + if err != nil { + log.Println(err) + continue + } + + // Create a map of schedules by start and end time + schedulesMap := map[string]*cachet_go.Schedule{} + for _, s := range schedules.Schedules { + schedulesMap[s.ScheduledAt+"_"+s.CompletedAt] = &s + } + + // Load Europe/Berlin timezone + loc, err := time.LoadLocation("Europe/Berlin") + if err != nil { + log.Println(err) + continue + } + + // Create a map of maintenance windows by start and end time if they don't exist + for _, mw := range maintenanceWindows { + // Convert the pagerduty time in the format "2015-11-09T20:00:00-05:00" to "Y-m-d H:i:sO". + // Keep in mind that cachet is in Europe/Berlin timezone + // Keep in mind that pagerduty is giving us a string + + // Parse the start and end time + startTime, err := time.Parse(time.RFC3339, mw.StartTime) + if err != nil { + log.Println(err) + continue + } + endTime, err := time.Parse(time.RFC3339, mw.EndTime) + if err != nil { + log.Println(err) + continue + } + + // If the endTime is before now, skip + if endTime.Before(time.Now()) { + continue + } + + // Convert the start and end time to Europe/Berlin timezone + startTime = startTime.In(loc) + endTime = endTime.In(loc) + + // Convert the start and end time to the format "Y-m-d H:i:sO" + mw.StartTime = gotime.Format(startTime, "yyyy-mm-dd hhh:ii") + mw.EndTime = gotime.Format(endTime, "yyyy-mm-dd hhh:ii") + // Also convert it with space instead of + for the lookup table + mwStart := gotime.Format(startTime, "yyyy-mm-dd hhh:ii:ss") + mwEnd := gotime.Format(endTime, "yyyy-mm-dd hhh:ii:ss") + + // If there is no summary then we want to set it to "Maintenance" + if mw.Description == "" { + mw.Description = "Maintenance" + } + + log.Printf("Maintenance window (%s): %s - %s\n", mw.Summary, mw.StartTime, mw.EndTime) + + var components []cachet_go.Component + if mw.Services != nil { + for _, s := range mw.Services { + component, err := findComponentByName(cachetClient, s.Summary) + if err != nil { + log.Println(err) + continue + } + if component != nil { + components = append(components, *component) + } + } + } + + if _, ok := schedulesMap[mwStart+"_"+mwEnd]; !ok { + newSchedule := &cachet_go.Schedule{ + Name: mw.Description, + Message: mw.Description, + Status: "0", + ScheduledAt: mw.StartTime, + CompletedAt: mw.EndTime, + Components: components, + } + // Print the schedule to the console + log.Printf("%+v\n", newSchedule) + + _, _, err := cachetClient.Schedules.Create(newSchedule) + if err != nil { + log.Println(err) + continue + } + } + } + + time.Sleep(30 * time.Minute) + } + }() + + log.Println("Listening on 0.0.0.0:8080") + log.Fatal(http.ListenAndServe("0.0.0.0:8080", router)) +} + +func findComponentByName(client *cachet_go.Client, name string) (*cachet_go.Component, error) { + components, _, err := client.Components.GetAll(&cachet_go.ComponentsQueryParams{ + QueryOptions: cachet_go.QueryOptions{ + PerPage: 100, + }, + }) + if err != nil { + return nil, err + } + + for _, c := range components.Components { + if c.Name == name { + return &c, nil + } + } + + return nil, nil +} + +func fetchMaintenanceWindows(client *pagerduty.Client) ([]pagerduty.MaintenanceWindow, error) { + ctx := context.TODO() + maintenanceWindows := []pagerduty.MaintenanceWindow{} + opts := pagerduty.ListMaintenanceWindowsOptions{} + for { + resp, err := client.ListMaintenanceWindowsWithContext(ctx, opts) + if err != nil { + return nil, err + } + maintenanceWindows = append(maintenanceWindows, resp.MaintenanceWindows...) + if !resp.More { + break + } + opts.Offset = resp.Offset + } + return maintenanceWindows, nil +} + +func fetchIncident(client *cachet_go.Client, incidentID string) (*cachet_go.Incident, error) { + incidents, _, err := client.Incidents.GetAll(&cachet_go.IncidentsQueryParams{ + QueryOptions: cachet_go.QueryOptions{ + PerPage: 100, + }, + }) + if err != nil { + return nil, err + } + + for _, i := range incidents.Incidents { + v, ok := i.Meta.(map[string]interface{}) + if ok { + v, ok := v["pagerduty"].(map[string]interface{}) + if ok { + if v["incident_id"] == incidentID { + return &i, nil + } + } + } + } + + return nil, nil +} + +func handler(w http.ResponseWriter, r *http.Request) { + client, _ := cachet_go.NewClient(cachetURL, nil) + client.Authentication.SetTokenAuth(cachetKey) + _, status, err := client.General.Ping() + if err != nil || status.StatusCode != 200 { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + err = webhookv3.VerifySignature(r, secret) + if err != nil { + switch err { + case webhookv3.ErrNoValidSignatures: + log.Println("no valid signatures") + w.WriteHeader(http.StatusUnauthorized) + + case webhookv3.ErrMalformedBody, webhookv3.ErrMalformedHeader: + log.Println("malformed body or header") + w.WriteHeader(http.StatusBadRequest) + + default: + log.Println("internal server error") + w.WriteHeader(http.StatusInternalServerError) + } + log.Println(err) + + fmt.Fprintf(w, "%v", err) + return + } + + // Read body as json + bytedata, err := io.ReadAll(r.Body) + r.Body.Close() + if err != nil { + log.Println("error reading body") + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "error reading body") + return + } + + r.Body = io.NopCloser(bytes.NewBuffer(bytedata)) + decodedJSON := WebhookMinimalEvent{} + err = json.NewDecoder(r.Body).Decode(&decodedJSON) + if err != nil { + log.Println("error decoding json") + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "error decoding json") + return + } + r.Body = io.NopCloser(bytes.NewBuffer(bytedata)) + + if decodedJSON.Event.EventType == "incident.triggered" { + incident := WebhookIncidentTriggered{} + err = json.NewDecoder(r.Body).Decode(&incident) + if err != nil { + log.Println("error decoding json") + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "error decoding json") + return + } + log.Printf("%+v\n", incident) + + // Get description or fallback to title + description := "This incident has been triggered automatically. Please stand by for updates." + + // If the urgency is high, set the status to degraded otherwise set the component status to normal + componentStatus := cachet_go.ComponentStatusOperational + if incident.Event.Data.Urgency == "high" { + componentStatus = cachet_go.ComponentStatusPerformanceIssues + } + + /* + Create a meta map which looks like this: + { + "meta": { + "pagerduty": { + "incident_id": "ABC" + } + } + } + */ + vars := map[string]map[string]string{ + "pagerduty": { + "incident_id": incident.Event.Data.ID, + }, + } + + // Create a new incident in cachet + cachetIncident := &cachet_go.Incident{ + Name: incident.Event.Data.Title, + Message: description, + Status: cachet_go.IncidentStatusInvestigating, + ComponentID: 1, + ComponentStatus: componentStatus, + Meta: vars, + } + _, cachetResp, err := client.Incidents.Create(cachetIncident) + if err != nil { + log.Println(err) + // Print the cachetResp body as a string to the console + bytedata, err := io.ReadAll(cachetResp.Body) + r.Body.Close() + if err != nil { + log.Println("error reading cachetResp body") + w.WriteHeader(http.StatusInternalServerError) + return + } + log.Println(string(bytedata)) + w.WriteHeader(http.StatusInternalServerError) + return + } + + } else if decodedJSON.Event.EventType == "incident.acknowledged" { + incident := WebhookIncidentAcknowledged{} + err = json.NewDecoder(r.Body).Decode(&incident) + if err != nil { + log.Println("error decoding json") + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "error decoding json") + return + } + log.Printf("%+v\n", incident) + + cachetIncident, err := fetchIncident(client, incident.Event.Data.ID) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Fallthrough without error if the incident is not found + if cachetIncident == nil { + log.Println("incident not found") + return + } + + newCachetIncidentUpdate := &cachet_go.IncidentUpdate{ + Status: cachet_go.IncidentStatusWatching, + HumanStatus: "Watching", + Message: "This incident has been acknowledged. We are currently investigating the issue. Thank you for your patience.", + ComponentID: cachetIncident.ComponentID, + ComponentStatus: cachet_go.ComponentStatusPerformanceIssues, + } + _, cachetResp, err := client.IncidentUpdates.Create(cachetIncident.ID, newCachetIncidentUpdate) + if err != nil { + log.Println(err) + // Print the cachetResp body as a string to the console + bytedata, err := io.ReadAll(cachetResp.Body) + r.Body.Close() + if err != nil { + log.Println("error reading cachetResp body") + w.WriteHeader(http.StatusInternalServerError) + return + } + log.Println(string(bytedata)) + w.WriteHeader(http.StatusInternalServerError) + return + } + + } else if decodedJSON.Event.EventType == "incident.resolved" { + incident := WebhookIncidentResolved{} + err = json.NewDecoder(r.Body).Decode(&incident) + if err != nil { + log.Println("error decoding json") + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "error decoding json") + return + } + log.Printf("%+v\n", incident) + + cachetIncident, err := fetchIncident(client, incident.Event.Data.ID) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Fallthrough without error if the incident is not found + if cachetIncident == nil { + log.Println("incident not found") + return + } + + newCachetIncidentUpdate := &cachet_go.IncidentUpdate{ + Status: cachet_go.IncidentStatusFixed, + HumanStatus: "Fixed", + Message: "This incident has been resolved. Thank you for your patience.", + ComponentID: cachetIncident.ComponentID, + ComponentStatus: cachet_go.ComponentStatusOperational, + } + + _, cachetResp, err := client.IncidentUpdates.Create(cachetIncident.ID, newCachetIncidentUpdate) + if err != nil { + log.Println(err) + // Print the cachetResp body as a string to the console + bytedata, err := io.ReadAll(cachetResp.Body) + r.Body.Close() + if err != nil { + log.Println("error reading cachetResp body") + w.WriteHeader(http.StatusInternalServerError) + return + } + log.Println(string(bytedata)) + w.WriteHeader(http.StatusInternalServerError) + return + } + } else { + log.Println("unknown event type") + + // Print the body as a string to the console + log.Println(string(bytedata)) + + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "unknown event type") + return + } + + fmt.Fprintf(w, "received signed webhook") +} diff --git a/types.go b/types.go @@ -0,0 +1,188 @@ +package main + +import "time" + +type WebhookMinimalEvent struct { + Event struct { + EventType string `json:"event_type,omitempty"` + ResourceType string `json:"resource_type,omitempty"` + } `json:"event,omitempty"` +} + +type WebhookIncidentTriggered struct { + Event struct { + ID string `json:"id,omitempty"` + EventType string `json:"event_type,omitempty"` + ResourceType string `json:"resource_type,omitempty"` + OccurredAt time.Time `json:"occurred_at,omitempty"` + Agent struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"agent,omitempty"` + Client any `json:"client,omitempty"` + Data struct { + ID string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Self string `json:"self,omitempty"` + HTMLURL string `json:"html_url,omitempty"` + Number int `json:"number,omitempty"` + Status string `json:"status,omitempty"` + IncidentKey string `json:"incident_key,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Service struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"service,omitempty"` + Assignees []struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"assignees,omitempty"` + EscalationPolicy struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"escalation_policy,omitempty"` + Teams []any `json:"teams,omitempty"` + Priority struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"priority,omitempty"` + Urgency string `json:"urgency,omitempty"` + ConferenceBridge any `json:"conference_bridge,omitempty"` + ResolveReason any `json:"resolve_reason,omitempty"` + IncidentType any `json:"incident_type,omitempty"` + } `json:"data,omitempty"` + } `json:"event,omitempty"` +} + +type WebhookIncidentResolved struct { + Event struct { + ID string `json:"id,omitempty"` + EventType string `json:"event_type,omitempty"` + ResourceType string `json:"resource_type,omitempty"` + OccurredAt time.Time `json:"occurred_at,omitempty"` + Agent struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"agent,omitempty"` + Client any `json:"client,omitempty"` + Data struct { + ID string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Self string `json:"self,omitempty"` + HTMLURL string `json:"html_url,omitempty"` + Number int `json:"number,omitempty"` + Status string `json:"status,omitempty"` + IncidentKey string `json:"incident_key,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + Title string `json:"title,omitempty"` + Service struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"service,omitempty"` + Assignees []any `json:"assignees,omitempty"` + EscalationPolicy struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"escalation_policy,omitempty"` + Teams []any `json:"teams,omitempty"` + Priority struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"priority,omitempty"` + Urgency string `json:"urgency,omitempty"` + ConferenceBridge any `json:"conference_bridge,omitempty"` + ResolveReason any `json:"resolve_reason,omitempty"` + IncidentType any `json:"incident_type,omitempty"` + } `json:"data,omitempty"` + } `json:"event,omitempty"` +} + +type WebhookIncidentAcknowledged struct { + Event struct { + ID string `json:"id,omitempty"` + EventType string `json:"event_type,omitempty"` + ResourceType string `json:"resource_type,omitempty"` + OccurredAt time.Time `json:"occurred_at,omitempty"` + Agent struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"agent,omitempty"` + Client any `json:"client,omitempty"` + Data struct { + ID string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Self string `json:"self,omitempty"` + HTMLURL string `json:"html_url,omitempty"` + Number int `json:"number,omitempty"` + Status string `json:"status,omitempty"` + IncidentKey string `json:"incident_key,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + Title string `json:"title,omitempty"` + Service struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"service,omitempty"` + Assignees []struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"assignees,omitempty"` + EscalationPolicy struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"escalation_policy,omitempty"` + Teams []any `json:"teams,omitempty"` + Priority struct { + HTMLURL string `json:"html_url,omitempty"` + ID string `json:"id,omitempty"` + Self string `json:"self,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + } `json:"priority,omitempty"` + Urgency string `json:"urgency,omitempty"` + ConferenceBridge any `json:"conference_bridge,omitempty"` + ResolveReason any `json:"resolve_reason,omitempty"` + IncidentType any `json:"incident_type,omitempty"` + } `json:"data,omitempty"` + } `json:"event,omitempty"` +}