| package main |
| |
| import ( |
| "context" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "html/template" |
| "net/http" |
| "path/filepath" |
| "sort" |
| "time" |
| |
| "cloud.google.com/go/pubsub" |
| "github.com/go-chi/chi/v5" |
| "github.com/unrolled/secure" |
| "golang.org/x/oauth2/google" |
| "google.golang.org/api/option" |
| |
| "go.skia.org/infra/am/go/audit" |
| "go.skia.org/infra/am/go/incident" |
| "go.skia.org/infra/am/go/note" |
| "go.skia.org/infra/am/go/reminder" |
| "go.skia.org/infra/am/go/silence" |
| "go.skia.org/infra/am/go/types" |
| "go.skia.org/infra/email/go/emailclient" |
| "go.skia.org/infra/go/alerts" |
| "go.skia.org/infra/go/allowed" |
| "go.skia.org/infra/go/alogin" |
| "go.skia.org/infra/go/alogin/proxylogin" |
| "go.skia.org/infra/go/auth" |
| "go.skia.org/infra/go/baseapp" |
| "go.skia.org/infra/go/ds" |
| "go.skia.org/infra/go/httputils" |
| "go.skia.org/infra/go/metrics2" |
| "go.skia.org/infra/go/pubsub/sub" |
| "go.skia.org/infra/go/roles" |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/sklog" |
| "go.skia.org/infra/go/util" |
| ) |
| |
| // flags |
| var ( |
| assignGroup = flag.String("assign_group", "google/skia-root@google.com", "The chrome infra auth group to use for users incidents can be assigned to.") |
| host = flag.String("host", "am.skia.org", "HTTP service host") |
| namespace = flag.String("namespace", "", "The Cloud Datastore namespace, such as 'alert-manager'.") |
| internalPort = flag.String("internal_port", ":9000", "HTTP internal service address (e.g., ':9000') for unauthenticated in-cluster requests.") |
| project = flag.String("project", "skia-public", "The Google Cloud project name.") |
| |
| silenceRecentlyExpiredDuration = flag.Duration("recently_expired_duration", 2*time.Hour, "Incidents with silences that recently expired within this duration are shown with an icon.") |
| ) |
| |
| const ( |
| // expireDuration is the time to wait before expiring an incident. |
| expireDuration = 5 * time.Minute |
| |
| // Constants for sending reminder emails. |
| reminderNumThreshold = 10 |
| reminderDurationThreshold = 600 |
| reminderDurationPercentage = 0.60 |
| |
| numPubSubReceiverGoRoutines = 10 |
| ) |
| |
| // server is the state of the server. |
| type server struct { |
| incidentStore *incident.Store |
| silenceStore *silence.Store |
| templates *template.Template |
| assign allowed.Allow // A list of people that incidents can be assigned to. |
| alogin *proxylogin.ProxyLogin |
| } |
| |
| // See baseapp.Constructor. |
| func New() (baseapp.App, error) { |
| var assign allowed.Allow |
| ctx := context.Background() |
| |
| ts, err := google.DefaultTokenSource(ctx, pubsub.ScopePubSub, auth.ScopeUserinfoEmail, "https://www.googleapis.com/auth/datastore") |
| if err != nil { |
| return nil, err |
| } |
| |
| if !*baseapp.Local { |
| client := httputils.DefaultClientConfig().WithTokenSource(ts).With2xxOnly().Client() |
| assign, err = allowed.NewAllowedFromChromeInfraAuth(client, *assignGroup) |
| if err != nil { |
| return nil, err |
| } |
| } else { |
| assign = allowed.NewAllowedFromList([]string{"betty@example.org", "fred@example.org", "barney@example.org", "wilma@example.org"}) |
| } |
| |
| if *namespace == "" { |
| return nil, fmt.Errorf("The --namespace flag is required. See infra/DATASTORE.md for format details.\n") |
| } |
| if !*baseapp.Local && !util.In(*namespace, []string{ds.ALERT_MANAGER_NS}) { |
| return nil, fmt.Errorf("When running in prod the datastore namespace must be a known value.") |
| } |
| if err := ds.InitWithOpt(*project, *namespace, option.WithTokenSource(ts)); err != nil { |
| return nil, fmt.Errorf("Failed to init Cloud Datastore: %s", err) |
| } |
| |
| sub, err := sub.New(ctx, *baseapp.Local, *project, alerts.TOPIC, numPubSubReceiverGoRoutines) |
| if err != nil { |
| return nil, skerr.Wrapf(err, "Failed to create subscription.") |
| } |
| |
| srv := &server{ |
| incidentStore: incident.NewStore(ds.DS, []string{"kubernetes_pod_name", "instance", "pod_template_hash", "pod", "exported_pod", "uid"}), |
| silenceStore: silence.NewStore(ds.DS), |
| assign: assign, |
| alogin: proxylogin.NewWithDefaults(), |
| } |
| srv.loadTemplates() |
| |
| // Start goroutine to send reminders to active alert owners. |
| reminder.StartReminderTicker(srv.incidentStore, srv.silenceStore, emailclient.New()) |
| |
| // livenesses gets populated as notifications arrive. |
| livenesses := map[string]metrics2.Liveness{} |
| |
| // Process all incoming PubSub requests. |
| go func() { |
| for { |
| err := sub.Receive(ctx, func(ctx context.Context, msg *pubsub.Message) { |
| msg.Ack() |
| var m map[string]string |
| if err := json.Unmarshal(msg.Data, &m); err != nil { |
| sklog.Error(err) |
| return |
| } |
| if m[alerts.TYPE] == alerts.TYPE_HEALTHZ { |
| location := m[alerts.LOCATION] |
| sklog.Infof("healthz received: %q", location) |
| if l, ok := livenesses[m[alerts.LOCATION]]; ok { |
| l.Reset() |
| } else { |
| livenesses[location] = metrics2.NewLiveness("alert_to_pubsub_alive", map[string]string{alerts.LOCATION: location}) |
| } |
| } else { |
| if _, err := srv.incidentStore.AlertArrival(m); err != nil { |
| sklog.Errorf("Error processing alert: %s", err) |
| } |
| } |
| }) |
| if err != nil { |
| sklog.Errorf("Failed receiving pubsub message: %s", err) |
| } |
| } |
| }() |
| |
| // This is really just a backstop in case we miss a resolved event for the incident. |
| go func() { |
| for range time.Tick(1 * time.Minute) { |
| ins, err := srv.incidentStore.GetAll() |
| if err != nil { |
| sklog.Errorf("Failed to load incidents: %s", err) |
| continue |
| } |
| now := time.Now() |
| for _, in := range ins { |
| // If it was last updated too long ago then it should be archived. |
| if time.Unix(in.LastSeen, 0).Add(expireDuration).Before(now) { |
| if _, err := srv.incidentStore.Archive(in.Key); err != nil { |
| sklog.Errorf("Failed to archive incident: %s", err) |
| } |
| } |
| } |
| } |
| }() |
| |
| srv.startInternalServer() |
| |
| return srv, nil |
| } |
| |
| func (srv *server) loadTemplates() { |
| srv.templates = template.Must(template.New("").Delims("{%", "%}").ParseFiles( |
| filepath.Join(*baseapp.ResourcesDir, "index.html"), |
| )) |
| } |
| |
| func (srv *server) mainHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "text/html") |
| if *baseapp.Local { |
| srv.loadTemplates() |
| } |
| if err := srv.templates.ExecuteTemplate(w, "index.html", map[string]string{ |
| "Nonce": secure.CSPNonce(r.Context()), |
| }); err != nil { |
| sklog.Errorf("Failed to expand template: %s", err) |
| } |
| } |
| |
| type addNoteRequest struct { |
| Text string `json:"text"` |
| Key string `json:"key"` |
| } |
| |
| // user returns the currently logged in user, or a placeholder if running locally. |
| func (srv *server) user(r *http.Request) string { |
| user := "barney@example.org" |
| if !*baseapp.Local { |
| user = string(srv.alogin.LoggedInAs(r)) |
| } |
| return user |
| } |
| |
| func (srv *server) addNoteHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var req addNoteRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode add note request.", http.StatusInternalServerError) |
| return |
| } |
| audit.Log(r, "add-note", req, srv.alogin) |
| |
| note := note.Note{ |
| Text: req.Text, |
| TS: time.Now().Unix(), |
| Author: srv.user(r), |
| } |
| in, err := srv.incidentStore.AddNote(req.Key, note) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to add incident note.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(in); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| func (srv *server) addSilenceNoteHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var req addNoteRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode add note request.", http.StatusInternalServerError) |
| return |
| } |
| audit.Log(r, "add-silence-note", req, srv.alogin) |
| |
| note := note.Note{ |
| Text: req.Text, |
| TS: time.Now().Unix(), |
| Author: srv.user(r), |
| } |
| in, err := srv.silenceStore.AddNote(req.Key, note) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to add silence note.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(in); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| type delNoteRequest struct { |
| Index int `json:"index"` |
| Key string `json:"key"` |
| } |
| |
| func (srv *server) delNoteHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var req delNoteRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode add note request.", http.StatusInternalServerError) |
| return |
| } |
| audit.Log(r, "del-note", req, srv.alogin) |
| in, err := srv.incidentStore.DeleteNote(req.Key, req.Index) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to del incident note.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(in); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| func (srv *server) delSilenceNoteHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var req delNoteRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode add note request.", http.StatusInternalServerError) |
| return |
| } |
| audit.Log(r, "del-silence-note", req, srv.alogin) |
| in, err := srv.silenceStore.DeleteNote(req.Key, req.Index) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to del silence note.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(in); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| type TakeRequest struct { |
| Key string `json:"key"` |
| } |
| |
| func (srv *server) takeHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var req TakeRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode take request.", http.StatusInternalServerError) |
| return |
| } |
| audit.Log(r, "take", req, srv.alogin) |
| |
| in, err := srv.incidentStore.Assign(req.Key, srv.user(r)) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to assign.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(in); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| type StatsResponseSlice types.StatsResponse |
| |
| func (p StatsResponseSlice) Len() int { return len(p) } |
| func (p StatsResponseSlice) Less(i, j int) bool { return p[i].Num > p[j].Num } |
| func (p StatsResponseSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } |
| |
| func (srv *server) statsHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| |
| var req types.StatsRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode stats request.", http.StatusInternalServerError) |
| return |
| } |
| ins, err := srv.incidentStore.GetRecentlyResolvedInRange(req.Range) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to query for Incidents.", http.StatusInternalServerError) |
| } |
| count := map[string]*types.Stat{} |
| for _, in := range ins { |
| if stat, ok := count[in.ID]; !ok { |
| count[in.ID] = &types.Stat{ |
| Num: 1, |
| Incident: in, |
| } |
| } else { |
| stat.Num += 1 |
| } |
| } |
| ret := types.StatsResponse{} |
| for _, v := range count { |
| ret = append(ret, v) |
| } |
| sort.Sort(StatsResponseSlice(ret)) |
| if err := json.NewEncoder(w).Encode(ret); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| func (srv *server) incidentsInRangeHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| |
| var req types.IncidentsInRangeRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode incident range request.", http.StatusInternalServerError) |
| return |
| } |
| ret, err := srv.incidentStore.GetRecentlyResolvedInRangeWithID(req.Range, req.Incident.ID) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to query for incidents.", http.StatusInternalServerError) |
| } |
| if err := json.NewEncoder(w).Encode(ret); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| type AssignRequest struct { |
| Key string `json:"key"` |
| Email string `json:"email"` |
| } |
| |
| func (srv *server) assignHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var req AssignRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode take request.", http.StatusInternalServerError) |
| return |
| } |
| audit.Log(r, "assign", req, srv.alogin) |
| in, err := srv.incidentStore.Assign(req.Key, req.Email) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to assign.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(in); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| type AssignMultipleRequest struct { |
| Keys []string `json:"keys"` |
| Email string `json:"email"` |
| } |
| |
| func (srv *server) assignMultipleHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var req AssignMultipleRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode assign multiple request.", http.StatusInternalServerError) |
| return |
| } |
| audit.Log(r, "assign multiple", req, srv.alogin) |
| |
| for _, k := range req.Keys { |
| if _, err := srv.incidentStore.Assign(k, req.Email); err != nil { |
| httputils.ReportError(w, err, "Failed to assign multiple.", http.StatusInternalServerError) |
| return |
| } |
| } |
| |
| ins, err := srv.getActiveAndRecentlyResolvedIncidents() |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load incidents.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(ins); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| func (srv *server) getActiveAndRecentlyResolvedIncidents() ([]incident.Incident, error) { |
| ins, err := srv.incidentStore.GetAll() |
| if err != nil { |
| return nil, fmt.Errorf("Failed to load incidents: %s", err) |
| } |
| recents, err := srv.incidentStore.GetRecentlyResolved() |
| if err != nil { |
| return nil, fmt.Errorf("Failed to load recents: %s", err) |
| } |
| ins = append(ins, recents...) |
| return ins, nil |
| } |
| |
| func (srv *server) emailsHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| emails := srv.assign.Emails() |
| sort.Strings(emails) |
| if err := json.NewEncoder(w).Encode(&emails); err != nil { |
| sklog.Errorf("Failed to encode emails: %s", err) |
| } |
| } |
| |
| func (srv *server) silencesHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| silences, err := srv.silenceStore.GetAll() |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load recents.", http.StatusInternalServerError) |
| return |
| } |
| if silences == nil { |
| silences = []silence.Silence{} |
| } |
| recents, err := srv.silenceStore.GetRecentlyArchived(0) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load recents.", http.StatusInternalServerError) |
| return |
| } |
| silences = append(silences, recents...) |
| if err := json.NewEncoder(w).Encode(silences); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| func (srv *server) auditLogsHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| logs, err := audit.GetLogs(r.Context()) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load audit logs.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(logs); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| func (srv *server) incidentHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| ins, err := srv.getActiveAndRecentlyResolvedIncidents() |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load incidents.", http.StatusInternalServerError) |
| return |
| } |
| |
| idsToRecentlyExpiredSilences := map[string]bool{} |
| archivedSilences, err := srv.silenceStore.GetRecentlyArchived(*silenceRecentlyExpiredDuration) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load archived silences.", http.StatusInternalServerError) |
| } |
| if len(archivedSilences) > 0 { |
| for _, i := range ins { |
| idsToRecentlyExpiredSilences[i.ID] = i.IsSilenced(archivedSilences, false) |
| } |
| } |
| resp := types.IncidentsResponse{ |
| Incidents: ins, |
| IdsToRecentlyExpiredSilences: idsToRecentlyExpiredSilences, |
| } |
| if err := json.NewEncoder(w).Encode(resp); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| func (srv *server) recentIncidentsHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| id := r.FormValue("id") |
| key := r.FormValue("key") |
| ins, err := srv.incidentStore.GetRecentlyResolvedForID(id, key) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load incidents.", http.StatusInternalServerError) |
| return |
| } |
| |
| // Get recently archived silences to see if the incident recently expired. |
| archivedSilences, err := srv.silenceStore.GetRecentlyArchived(*silenceRecentlyExpiredDuration) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load archived silences.", http.StatusInternalServerError) |
| return |
| } |
| recentlyExpired := false |
| if len(ins) > 0 { |
| recentlyExpired = ins[0].IsSilenced(archivedSilences, false) |
| } |
| |
| resp := types.RecentIncidentsResponse{ |
| Incidents: ins, |
| Flaky: incident.AreIncidentsFlaky(ins, reminderNumThreshold, reminderDurationThreshold, reminderDurationPercentage), |
| RecentlyExpiredSilence: recentlyExpired, |
| } |
| if err := json.NewEncoder(w).Encode(resp); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| func (srv *server) saveSilenceHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var req silence.Silence |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode silence creation request.", http.StatusInternalServerError) |
| return |
| } |
| if err := req.ValidateRegexes(); err != nil { |
| httputils.ReportError(w, err, "Silence has invalid regex.", http.StatusInternalServerError) |
| return |
| } |
| |
| audit.Log(r, "create-silence", req, srv.alogin) |
| silence, err := srv.silenceStore.Put(&req) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to create silence.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(silence); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| func (srv *server) archiveSilenceHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var req silence.Silence |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode silence creation request.", http.StatusInternalServerError) |
| return |
| } |
| if err := req.ValidateRegexes(); err != nil { |
| httputils.ReportError(w, err, "Silence has invalid regex.", http.StatusInternalServerError) |
| return |
| } |
| |
| audit.Log(r, "archive-silence", req, srv.alogin) |
| silence, err := srv.silenceStore.Archive(req.Key) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to archive silence.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(silence); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| func (srv *server) reactivateSilenceHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var req silence.Silence |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode silence reactivation request.", http.StatusInternalServerError) |
| return |
| } |
| if err := req.ValidateRegexes(); err != nil { |
| httputils.ReportError(w, err, "Silence has invalid regex.", http.StatusInternalServerError) |
| return |
| } |
| |
| audit.Log(r, "reactivate-silence", req, srv.alogin) |
| silence, err := srv.silenceStore.Reactivate(req.Key, req.Duration, srv.user(r)) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to reactivate silence.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(silence); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| func (srv *server) deleteSilenceHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var sil silence.Silence |
| if err := json.NewDecoder(r.Body).Decode(&sil); err != nil { |
| httputils.ReportError(w, err, "Failed to decode silence deletion request.", http.StatusInternalServerError) |
| return |
| } |
| audit.Log(r, "delete-silence", sil, srv.alogin) |
| if err := srv.silenceStore.Delete(sil.Key); err != nil { |
| httputils.ReportError(w, err, "Failed to delete silence.", http.StatusInternalServerError) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(sil); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| // newSilenceHandler creates and returns a new Silence pre-populated with good defaults. |
| func (srv *server) newSilenceHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| s := silence.New(srv.user(r)) |
| if err := json.NewEncoder(w).Encode(s); err != nil { |
| sklog.Errorf("Failed to send response: %s", err) |
| } |
| } |
| |
| // See baseapp.App. |
| func (srv *server) AddHandlers(r chi.Router) { |
| r.HandleFunc("/", srv.mainHandler) |
| r.HandleFunc(("/_/login/status"), alogin.LoginStatusHandler(srv.alogin)) |
| |
| // GETs |
| r.Get("/_/emails", srv.emailsHandler) |
| r.Get("/_/incidents", srv.incidentHandler) |
| r.Get("/_/new_silence", srv.newSilenceHandler) |
| r.Get("/_/recent_incidents", srv.recentIncidentsHandler) |
| r.Get("/_/silences", srv.silencesHandler) |
| |
| // POSTs |
| r.Post("/_/add_note", srv.addNoteHandler) |
| r.Post("/_/add_silence_note", srv.addSilenceNoteHandler) |
| r.Post("/_/archive_silence", srv.archiveSilenceHandler) |
| r.Post("/_/assign", srv.assignHandler) |
| r.Post("/_/assign_multiple", srv.assignMultipleHandler) |
| r.Post("/_/audit_logs", srv.auditLogsHandler) |
| r.Post("/_/del_note", srv.delNoteHandler) |
| r.Post("/_/del_silence_note", srv.delSilenceNoteHandler) |
| r.Post("/_/del_silence", srv.deleteSilenceHandler) |
| r.Post("/_/reactivate_silence", srv.reactivateSilenceHandler) |
| r.Post("/_/save_silence", srv.saveSilenceHandler) |
| r.Post("/_/take", srv.takeHandler) |
| r.Post("/_/stats", srv.statsHandler) |
| r.Post("/_/incidents_in_range", srv.incidentsInRangeHandler) |
| } |
| |
| // See baseapp.App. |
| func (srv *server) AddMiddleware() []func(http.Handler) http.Handler { |
| ret := []func(http.Handler) http.Handler{} |
| if !*baseapp.Local { |
| ret = append(ret, alogin.ForceRoleMiddleware(srv.alogin, roles.Viewer)) |
| } |
| return ret |
| } |
| |
| func (srv *server) startInternalServer() { |
| // Internal endpoints that are only accessible from within the cluster. |
| unprotected := chi.NewRouter() |
| unprotected.Get("/_/incidents", srv.incidentHandler) |
| unprotected.Get("/_/silences", srv.silencesHandler) |
| go func() { |
| sklog.Fatal(http.ListenAndServe(*internalPort, unprotected)) |
| }() |
| } |
| |
| func main() { |
| // Parse flags to be able to send *host to baseapp.Serve |
| flag.Parse() |
| baseapp.Serve(New, []string{*host}, baseapp.DisableLoggingRequestResponse{}) |
| } |