|
1 | 1 | package main |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "bytes" |
5 | 4 | "context" |
| 5 | + "encoding/json" |
6 | 6 | "fmt" |
7 | | - "github.com/aws/aws-lambda-go/events" |
| 7 | + "github.com/function61/gokit/httputils" |
8 | 8 | "github.com/function61/gokit/jsonfile" |
| 9 | + "github.com/function61/gokit/logex" |
| 10 | + "github.com/function61/gokit/ossignal" |
| 11 | + "github.com/function61/gokit/taskrunner" |
9 | 12 | "github.com/function61/lambda-alertmanager/pkg/alertmanagertypes" |
10 | 13 | "github.com/function61/lambda-alertmanager/pkg/amstate" |
11 | | - "github.com/function61/lambda-alertmanager/pkg/lambdautils" |
| 14 | + "github.com/spf13/cobra" |
| 15 | + "log" |
| 16 | + "net/http" |
12 | 17 | "os" |
13 | 18 | "time" |
14 | 19 | ) |
15 | 20 |
|
16 | | -func handleRestCall(ctx context.Context, req events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) { |
| 21 | +func newRestApi(ctx context.Context) http.Handler { |
17 | 22 | app, err := getApp(ctx) |
18 | 23 | if err != nil { |
19 | | - return lambdautils.InternalServerError(err.Error()), nil |
| 24 | + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 25 | + http.Error(w, err.Error(), http.StatusInternalServerError) |
| 26 | + }) |
20 | 27 | } |
21 | 28 |
|
22 | | - synopsis := req.HTTPMethod + " " + req.Path |
23 | | - |
24 | | - switch synopsis { |
25 | | - case "GET /alerts": |
26 | | - return handleGetAlerts(ctx, req, app) |
27 | | - case "GET /alerts/acknowledge": |
28 | | - // this endpoint should really be a POST (since it mutates state), but we've to be |
29 | | - // pragmatic here because we want acks to be ack-able from emails |
30 | | - id := req.QueryStringParameters["id"] |
31 | | - if id == "" { |
32 | | - return lambdautils.BadRequest("id not specified"), nil |
33 | | - } |
| 29 | + mux := httputils.NewMethodMux() |
34 | 30 |
|
35 | | - return handleAcknowledgeAlert(ctx, id) |
36 | | - case "POST /alerts/ingest": |
| 31 | + mux.GET.HandleFunc("/alerts", func(w http.ResponseWriter, r *http.Request) { |
| 32 | + handleJsonOutput(w, app.State.ActiveAlerts()) |
| 33 | + }) |
| 34 | + |
| 35 | + mux.POST.HandleFunc("/alerts/ingest", func(w http.ResponseWriter, r *http.Request) { |
37 | 36 | alert := amstate.Alert{} |
38 | | - if err := jsonfile.Unmarshal(bytes.NewBufferString(req.Body), &alert, true); err != nil { |
39 | | - return lambdautils.BadRequest(err.Error()), nil |
| 37 | + if err := jsonfile.Unmarshal(r.Body, &alert, true); err != nil { |
| 38 | + http.Error(w, err.Error(), http.StatusBadRequest) |
| 39 | + return |
40 | 40 | } |
41 | | - alert.Id = amstate.NewAlertId() |
| 41 | + alert.Id = amstate.NewAlertId() // FIXME: bad design |
42 | 42 |
|
43 | | - created, err := ingestAlertsAndReturnCreatedFlag(ctx, []amstate.Alert{alert}, app) |
| 43 | + created, err := ingestAlertsAndReturnCreatedFlag(r.Context(), []amstate.Alert{alert}, app) |
44 | 44 | if err != nil { |
45 | | - return lambdautils.InternalServerError(err.Error()), nil |
| 45 | + http.Error(w, err.Error(), http.StatusInternalServerError) |
| 46 | + return |
46 | 47 | } |
47 | 48 |
|
48 | 49 | if created { |
49 | | - return lambdautils.Created(), nil |
| 50 | + w.WriteHeader(http.StatusCreated) |
50 | 51 | } else { |
51 | | - return lambdautils.NoContent(), nil |
| 52 | + w.WriteHeader(http.StatusNoContent) |
| 53 | + } |
| 54 | + }) |
| 55 | + |
| 56 | + mux.GET.HandleFunc("/alerts/acknowledge", func(w http.ResponseWriter, r *http.Request) { |
| 57 | + id := r.URL.Query().Get("id") |
| 58 | + |
| 59 | + if err := alertAck(r.Context(), id); err != nil { |
| 60 | + http.Error(w, err.Error(), http.StatusInternalServerError) |
| 61 | + return |
52 | 62 | } |
53 | | - case "GET /deadmansswitch/checkin": // /deadmansswitch/checkin?subject=ubackup_done&ttl=24h30m |
| 63 | + |
| 64 | + fmt.Fprintf(w, "Ack ok for %s", id) |
| 65 | + }) |
| 66 | + |
| 67 | + mux.GET.HandleFunc("/deadmansswitches", func(w http.ResponseWriter, r *http.Request) { |
| 68 | + handleJsonOutput(w, app.State.DeadMansSwitches()) |
| 69 | + }) |
| 70 | + |
| 71 | + // /deadmansswitch/checkin?subject=ubackup_done&ttl=24h30m |
| 72 | + mux.GET.HandleFunc("/deadmansswitch/checkin", func(w http.ResponseWriter, r *http.Request) { |
54 | 73 | // same semantic hack here as acknowledge endpoint |
55 | | - return handleDeadMansSwitchCheckin(ctx, alertmanagertypes.DeadMansSwitchCheckinRequest{ |
56 | | - Subject: req.QueryStringParameters["subject"], |
57 | | - TTL: req.QueryStringParameters["ttl"], |
| 74 | + |
| 75 | + // handles validation |
| 76 | + handleDeadMansSwitchCheckin(w, r, alertmanagertypes.DeadMansSwitchCheckinRequest{ |
| 77 | + Subject: r.URL.Query().Get("subject"), |
| 78 | + TTL: r.URL.Query().Get("ttl"), |
58 | 79 | }) |
59 | | - case "POST /deadmansswitch/checkin": // {"subject":"ubackup_done","ttl":"24h30m"} |
| 80 | + }) |
| 81 | + |
| 82 | + mux.POST.HandleFunc("/deadmansswitch/checkin", func(w http.ResponseWriter, r *http.Request) { |
60 | 83 | checkin := alertmanagertypes.DeadMansSwitchCheckinRequest{} |
61 | | - if err := jsonfile.Unmarshal(bytes.NewBufferString(req.Body), &checkin, true); err != nil { |
62 | | - return lambdautils.BadRequest(err.Error()), nil |
| 84 | + if err := jsonfile.Unmarshal(r.Body, &checkin, true); err != nil { |
| 85 | + http.Error(w, err.Error(), http.StatusBadRequest) |
| 86 | + return |
63 | 87 | } |
64 | 88 |
|
65 | | - return handleDeadMansSwitchCheckin(ctx, checkin) |
66 | | - case "GET /deadmansswitches": |
67 | | - return handleGetDeadMansSwitches(ctx, app) |
68 | | - case "POST /prometheus-alertmanager/api/v1/alerts": |
69 | | - return lambdautils.InternalServerError("not implemented yet"), nil |
70 | | - default: |
71 | | - return lambdautils.BadRequest(fmt.Sprintf("unknown endpoint: %s", synopsis)), nil |
72 | | - } |
73 | | -} |
74 | | - |
75 | | -func handleGetAlerts( |
76 | | - ctx context.Context, |
77 | | - req events.APIGatewayProxyRequest, |
78 | | - app *amstate.App, |
79 | | -) (*events.APIGatewayProxyResponse, error) { |
80 | | - return lambdautils.RespondJson(app.State.ActiveAlerts()) |
81 | | -} |
| 89 | + // handles validation |
| 90 | + handleDeadMansSwitchCheckin(w, r, checkin) |
| 91 | + }) |
82 | 92 |
|
83 | | -func handleAcknowledgeAlert(ctx context.Context, id string) (*events.APIGatewayProxyResponse, error) { |
84 | | - if err := alertAck(ctx, id); err != nil { |
85 | | - return lambdautils.InternalServerError(err.Error()), nil |
86 | | - } |
| 93 | + mux.POST.HandleFunc("/prometheus-alertmanager/api/v1/alerts", func(w http.ResponseWriter, r *http.Request) { |
| 94 | + http.Error(w, "not implemented yet", http.StatusInternalServerError) |
| 95 | + }) |
87 | 96 |
|
88 | | - return lambdautils.OkText(fmt.Sprintf("Ack ok for %s", id)) |
| 97 | + return mux |
89 | 98 | } |
90 | 99 |
|
91 | | -func handleGetDeadMansSwitches( |
92 | | - ctx context.Context, |
93 | | - app *amstate.App, |
94 | | -) (*events.APIGatewayProxyResponse, error) { |
95 | | - return lambdautils.RespondJson(app.State.DeadMansSwitches()) |
96 | | -} |
97 | | - |
98 | | -func handleDeadMansSwitchCheckin(ctx context.Context, raw alertmanagertypes.DeadMansSwitchCheckinRequest) (*events.APIGatewayProxyResponse, error) { |
| 100 | + func handleDeadMansSwitchCheckin( |
| 101 | + w http.ResponseWriter, |
| 102 | + r *http.Request, |
| 103 | + raw alertmanagertypes.DeadMansSwitchCheckinRequest, |
| 104 | + ) { |
99 | 105 | if raw.Subject == "" || raw.TTL == "" { |
100 | | - return lambdautils.BadRequest("subject or ttl empty"), nil |
| 106 | + http.Error(w, "subject or ttl empty", http.StatusBadRequest) |
| 107 | + return |
101 | 108 | } |
102 | 109 |
|
103 | 110 | now := time.Now() |
104 | 111 |
|
105 | 112 | ttl, err := parseTtlSpec(raw.TTL, now) |
106 | 113 | if err != nil { |
107 | | - return lambdautils.BadRequest(err.Error()), nil |
| 114 | + http.Error(w, err.Error(), http.StatusBadRequest) |
| 115 | + return |
108 | 116 | } |
109 | 117 |
|
110 | | - alertAcked, err := deadmansswitchCheckin(ctx, raw.Subject, ttl) |
| 118 | + alertAcked, err := deadmansswitchCheckin(r.Context(), raw.Subject, ttl) |
111 | 119 | if err != nil { |
112 | | - return lambdautils.InternalServerError(err.Error()), nil |
| 120 | + http.Error(w, err.Error(), http.StatusInternalServerError) |
| 121 | + return |
113 | 122 | } |
114 | 123 |
|
115 | 124 | if alertAcked { |
116 | | - return lambdautils.OkText("Check-in noted; alert that was firing for this dead mans's switch was acked") |
| 125 | + fmt.Fprintln(w, "Check-in noted; alert that was firing for this dead mans's switch was acked") |
| 126 | + } else { |
| 127 | + fmt.Fprintln(w, "Check-in noted") |
| 128 | + } |
| 129 | +} |
| 130 | + |
| 131 | +func handleJsonOutput(w http.ResponseWriter, output interface{}) { |
| 132 | + w.Header().Set("Content-Type", "application/json") |
| 133 | + |
| 134 | + if err := json.NewEncoder(w).Encode(output); err != nil { |
| 135 | + panic(err) |
117 | 136 | } |
| 137 | +} |
| 138 | + |
| 139 | +func restApiCliEntry() *cobra.Command { |
| 140 | + return &cobra.Command{ |
| 141 | + Use: "restapi", |
| 142 | + Short: "Start REST API (used mainly for dev/testing)", |
| 143 | + Args: cobra.NoArgs, |
| 144 | + Run: func(cmd *cobra.Command, args []string) { |
| 145 | + logger := logex.StandardLogger() |
| 146 | + |
| 147 | + exitIfError(runStandaloneRestApi( |
| 148 | + ossignal.InterruptOrTerminateBackgroundCtx(logger), |
| 149 | + logger)) |
| 150 | + }, |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +func runStandaloneRestApi(ctx context.Context, logger *log.Logger) error { |
| 155 | + srv := &http.Server{ |
| 156 | + Addr: ":80", |
| 157 | + Handler: newRestApi(ctx), |
| 158 | + } |
| 159 | + |
| 160 | + tasks := taskrunner.New(ctx, logger) |
| 161 | + |
| 162 | + tasks.Start("listener "+srv.Addr, func(_ context.Context, _ string) error { |
| 163 | + return httputils.RemoveGracefulServerClosedError(srv.ListenAndServe()) |
| 164 | + }) |
| 165 | + |
| 166 | + tasks.Start("listenershutdowner", httputils.ServerShutdownTask(srv)) |
118 | 167 |
|
119 | | - return lambdautils.OkText("Check-in noted") |
| 168 | + return tasks.Wait() |
120 | 169 | } |
121 | 170 |
|
122 | 171 | func ackLink(alert amstate.Alert) string { |
|
0 commit comments