blob: 95c3882f734c94f7aebd668c805c936c10f1ce33 [file] [log] [blame]
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"go.opencensus.io/trace"
"go.skia.org/infra/go/alogin"
"go.skia.org/infra/go/auditlog"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/roles"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/perf/go/alerts"
"go.skia.org/infra/perf/go/bug"
"go.skia.org/infra/perf/go/dryrun"
"go.skia.org/infra/perf/go/notify"
"go.skia.org/infra/perf/go/subscription"
)
// alertsApi provides a struct to manage api endpoints for Alerts.
type alertsApi struct {
loginProvider alogin.Login
configProvider alerts.ConfigProvider
alertStore alerts.Store
notifier notify.Notifier
subStore subscription.Store
dryrunRequests *dryrun.Requests
}
// NewAlertsApi returns a new instance of the alertsApi struct.
func NewAlertsApi(loginProvider alogin.Login, configProvider alerts.ConfigProvider, alertStore alerts.Store, notifier notify.Notifier, subStore subscription.Store, dryRunRequests *dryrun.Requests) alertsApi {
return alertsApi{
loginProvider: loginProvider,
configProvider: configProvider,
alertStore: alertStore,
notifier: notifier,
subStore: subStore,
dryrunRequests: dryRunRequests,
}
}
// RegisterHandlers registers the api handlers for their respective routes.
func (a alertsApi) RegisterHandlers(router *chi.Mux) {
router.Get("/_/alert/list/{show}", a.alertListHandler)
router.Get("/_/alert/new", a.alertNewHandler)
router.Post("/_/alert/update", a.alertUpdateHandler)
router.Post("/_/alert/delete/{id:[0-9]+}", a.alertDeleteHandler)
router.Post("/_/alert/bug/try", a.alertBugTryHandler)
router.Post("/_/alert/notify/try", a.alertNotifyTryHandler)
router.Get("/_/subscriptions", a.subscriptionsHandler)
router.Post("/_/dryrun/start", a.dryrunRequests.StartHandler)
}
// alertListHandler returns a list of alert configs in the database.
func (a alertsApi) alertListHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout)
defer cancel()
w.Header().Set("Content-Type", "application/json")
show := chi.URLParam(r, "show")
resp, err := a.configProvider.GetAllAlertConfigs(ctx, show == "true")
if err != nil {
httputils.ReportError(w, err, "Failed to retrieve alert configs.", http.StatusInternalServerError)
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
sklog.Errorf("Failed to write JSON response: %s", err)
}
}
// alertNewHandler returns a new empty alert config.
func (a alertsApi) alertNewHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(alerts.NewConfig()); err != nil {
sklog.Errorf("Failed to write JSON response: %s", err)
}
}
// AlertUpdateResponse is the JSON response when an Alert is created or udpated.
type AlertUpdateResponse struct {
IDAsString string
}
// alertUpdateHandler updates the alert config data.
func (a alertsApi) alertUpdateHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout)
defer cancel()
defer refreshConfigProvider(ctx, a.configProvider)
w.Header().Set("Content-Type", "application/json")
cfg := &alerts.Alert{}
if err := json.NewDecoder(r.Body).Decode(cfg); err != nil {
httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError)
return
}
if !a.isEditor(w, r, "alert-update", cfg) {
return
}
if err := cfg.Validate(); err != nil {
httputils.ReportError(w, err, "Invalid Alert", http.StatusInternalServerError)
}
if err := a.alertStore.Save(ctx, &alerts.SaveRequest{Cfg: cfg}); err != nil {
httputils.ReportError(w, err, "Failed to save alerts.Config.", http.StatusInternalServerError)
}
err := json.NewEncoder(w).Encode(AlertUpdateResponse{
IDAsString: cfg.IDAsString,
})
if err != nil {
sklog.Errorf("Failed to write JSON response: %s", err)
}
}
// alertDeleteHandler deletes the specified alert config.
func (a alertsApi) alertDeleteHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout)
defer cancel()
defer refreshConfigProvider(ctx, a.configProvider)
w.Header().Set("Content-Type", "application/json")
sid := chi.URLParam(r, "id")
id, err := strconv.ParseInt(sid, 10, 64)
if err != nil {
httputils.ReportError(w, err, "Failed to parse alert id.", http.StatusInternalServerError)
}
if !a.isEditor(w, r, "alert-delete", sid) {
return
}
if err := a.alertStore.Delete(ctx, int(id)); err != nil {
httputils.ReportError(w, err, "Failed to delete the alerts.Config.", http.StatusInternalServerError)
return
}
}
// refreshConfigProvider refreshes the alert config provider.
func refreshConfigProvider(ctx context.Context, configProvider alerts.ConfigProvider) {
err := configProvider.Refresh(ctx)
if err != nil {
sklog.Errorf("Error refreshing alert configs: %s", err)
}
}
// TryBugRequest is a request to try a bug template URI.
type TryBugRequest struct {
BugURITemplate string `json:"bug_uri_template"`
}
// TryBugResponse is response to a TryBugRequest.
type TryBugResponse struct {
URL string `json:"url"`
}
// alertBugTryHandler attempts to dry run the bug creation flow for the alert config.
func (a alertsApi) alertBugTryHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
req := &TryBugRequest{}
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError)
return
}
if !a.isEditor(w, r, "alert-bug-try", req) {
return
}
resp := &TryBugResponse{
URL: bug.ExampleExpand(req.BugURITemplate),
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
sklog.Errorf("Failed to encode response: %s", err)
}
}
// alertNotifyTryHandler attempts to send a test notification based on the alert config.
func (a alertsApi) alertNotifyTryHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout)
defer cancel()
w.Header().Set("Content-Type", "application/json")
req := &alerts.Alert{}
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError)
return
}
if !a.isEditor(w, r, "alert-notify-try", req) {
return
}
if err := a.notifier.ExampleSend(ctx, req); err != nil {
httputils.ReportError(w, err, "Failed to send notification: Have you given the service account for this instance Issue Editor permissions on the component?", http.StatusInternalServerError)
}
}
// subscriptionsHandler is an API endpoint handler that fetches all the subscriptions from the db
func (a alertsApi) subscriptionsHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout)
defer cancel()
ctx, span := trace.StartSpan(ctx, "subscriptionQueryRequest")
defer span.End()
subscriptionList, err := a.subStore.GetAllSubscriptions(ctx)
if err != nil {
httputils.ReportError(w, err, "Unable to fetch subscription", http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(subscriptionList); err != nil {
sklog.Errorf("Failed to write or encode output: %s", err)
}
}
func (a alertsApi) isEditor(w http.ResponseWriter, r *http.Request, action string, body interface{}) bool {
user := a.loginProvider.LoggedInAs(r)
if !a.loginProvider.HasRole(r, roles.Editor) {
httputils.ReportError(w, fmt.Errorf("Not logged in."), "You must be logged in to complete this action.", http.StatusUnauthorized)
return false
}
auditlog.LogWithUser(r, user.String(), action, body)
return true
}