Skip to content

Commit 01e9794

Browse files
committed
convert restapi to use Go server APIs
1 parent fc2fda0 commit 01e9794

File tree

6 files changed

+158
-90
lines changed

6 files changed

+158
-90
lines changed

cmd/alertmanager/lambdahandler.go

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,14 @@ package main
22

33
import (
44
"context"
5-
"encoding/json"
65
"errors"
76
"github.com/aws/aws-lambda-go/events"
87
"github.com/aws/aws-lambda-go/lambda"
98
"github.com/function61/lambda-alertmanager/pkg/lambdautils"
109
)
1110

1211
func lambdaHandler() {
13-
// respIsNil because:
14-
// https://stackoverflow.com/questions/13476349/check-for-nil-and-nil-interface-in-go
15-
jsonOutHandler := func(resp interface{}, respIsNil bool, err error) ([]byte, error) {
16-
if respIsNil {
17-
return nil, err
18-
}
19-
20-
asJson, errMarshal := json.Marshal(resp)
21-
if errMarshal != nil {
22-
return nil, errMarshal
23-
}
24-
25-
return asJson, err
26-
}
12+
restApi := newRestApi(context.Background())
2713

2814
handler := func(ctx context.Context, polymorphicEvent interface{}) ([]byte, error) {
2915
switch event := polymorphicEvent.(type) {
@@ -32,9 +18,10 @@ func lambdaHandler() {
3218
case *events.SNSEvent:
3319
return nil, handleSnsIngest(ctx, *event)
3420
case *events.APIGatewayProxyRequest:
35-
resp, err := handleRestCall(ctx, *event)
36-
37-
return jsonOutHandler(resp, resp == nil, err)
21+
return lambdautils.ServeApiGatewayProxyRequestUsingHttpHandler(
22+
ctx,
23+
event,
24+
restApi)
3825
default:
3926
return nil, errors.New("cannot identify type of request")
4027
}

cmd/alertmanager/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ func main() {
3737

3838
app.AddCommand(ehcli.Entrypoint())
3939

40+
app.AddCommand(restApiCliEntry())
41+
4042
app.AddCommand(&cobra.Command{
4143
Use: "lambda-scheduler",
4244
Short: "Run what Lambda would invoke in response to scheduler event",

cmd/alertmanager/restapi.go

Lines changed: 118 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,171 @@
11
package main
22

33
import (
4-
"bytes"
54
"context"
5+
"encoding/json"
66
"fmt"
7-
"github.com/aws/aws-lambda-go/events"
7+
"github.com/function61/gokit/httputils"
88
"github.com/function61/gokit/jsonfile"
9+
"github.com/function61/gokit/logex"
10+
"github.com/function61/gokit/ossignal"
11+
"github.com/function61/gokit/taskrunner"
912
"github.com/function61/lambda-alertmanager/pkg/alertmanagertypes"
1013
"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"
1217
"os"
1318
"time"
1419
)
1520

16-
func handleRestCall(ctx context.Context, req events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
21+
func newRestApi(ctx context.Context) http.Handler {
1722
app, err := getApp(ctx)
1823
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+
})
2027
}
2128

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()
3430

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) {
3736
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
4040
}
41-
alert.Id = amstate.NewAlertId()
41+
alert.Id = amstate.NewAlertId() // FIXME: bad design
4242

43-
created, err := ingestAlertsAndReturnCreatedFlag(ctx, []amstate.Alert{alert}, app)
43+
created, err := ingestAlertsAndReturnCreatedFlag(r.Context(), []amstate.Alert{alert}, app)
4444
if err != nil {
45-
return lambdautils.InternalServerError(err.Error()), nil
45+
http.Error(w, err.Error(), http.StatusInternalServerError)
46+
return
4647
}
4748

4849
if created {
49-
return lambdautils.Created(), nil
50+
w.WriteHeader(http.StatusCreated)
5051
} 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
5262
}
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) {
5473
// 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"),
5879
})
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) {
6083
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
6387
}
6488

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+
})
8292

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+
})
8796

88-
return lambdautils.OkText(fmt.Sprintf("Ack ok for %s", id))
97+
return mux
8998
}
9099

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+
) {
99105
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
101108
}
102109

103110
now := time.Now()
104111

105112
ttl, err := parseTtlSpec(raw.TTL, now)
106113
if err != nil {
107-
return lambdautils.BadRequest(err.Error()), nil
114+
http.Error(w, err.Error(), http.StatusBadRequest)
115+
return
108116
}
109117

110-
alertAcked, err := deadmansswitchCheckin(ctx, raw.Subject, ttl)
118+
alertAcked, err := deadmansswitchCheckin(r.Context(), raw.Subject, ttl)
111119
if err != nil {
112-
return lambdautils.InternalServerError(err.Error()), nil
120+
http.Error(w, err.Error(), http.StatusInternalServerError)
121+
return
113122
}
114123

115124
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)
117136
}
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))
118167

119-
return lambdautils.OkText("Check-in noted")
168+
return tasks.Wait()
120169
}
121170

122171
func ackLink(alert amstate.Alert) string {

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ go 1.13
44

55
require (
66
github.com/apcera/termtables v0.0.0-20170405184538-bcbc5dc54055 // indirect
7+
github.com/apex/gateway v1.1.1
78
github.com/aws/aws-lambda-go v1.14.0
89
github.com/aws/aws-sdk-go v1.29.0
910
github.com/function61/eventhorizon v0.2.1-0.20200227140656-f89fe5d462ca
10-
github.com/function61/gokit v0.0.0-20200229073330-25232411c1f6
11+
github.com/function61/gokit v0.0.0-20200229115114-3eca8c87d0cc
1112
github.com/mattn/go-runewidth v0.0.8 // indirect
1213
github.com/scylladb/termtables v1.0.0
1314
github.com/spf13/cobra v0.0.6
1415
github.com/stretchr/testify v1.5.1 // indirect
16+
github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 // indirect
1517
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b // indirect
1618
)

go.sum

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
77
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
88
github.com/apcera/termtables v0.0.0-20170405184538-bcbc5dc54055 h1:IkPAzP+QjchKXXFX6LCcpDKa89b/e/0gPCUbQGWtUUY=
99
github.com/apcera/termtables v0.0.0-20170405184538-bcbc5dc54055/go.mod h1:8mHYHlOef9UC51cK1/WRvE/iQVM8O8QlYFa8eh8r5I8=
10+
github.com/apex/gateway v1.1.1 h1:dPE3y2LQ/fSJuZikCOvekqXLyn/Wrbgt10MSECobH/Q=
11+
github.com/apex/gateway v1.1.1/go.mod h1:x7iPY22zu9D8sfrynawEwh1wZEO/kQTRaOM5ye02tWU=
1012
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
1113
github.com/aws/aws-lambda-go v1.13.2/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
1214
github.com/aws/aws-lambda-go v1.14.0 h1:kTr1VPabIgJsMVzHuZpNhs/5RR46LU6wyWUiHxtb3ag=
@@ -40,8 +42,8 @@ github.com/function61/eventhorizon v0.2.1-0.20200227140656-f89fe5d462ca h1:FY7y7
4042
github.com/function61/eventhorizon v0.2.1-0.20200227140656-f89fe5d462ca/go.mod h1:SztwDAaqWnLPSFyVmV0zbBgRsvExr0GzrlOj9sWTC+Y=
4143
github.com/function61/gokit v0.0.0-20200226141201-fe205250686d h1:sbcDaH7t/trZpAv77EU9hl/rjjse+Mc70MO8MAhILnM=
4244
github.com/function61/gokit v0.0.0-20200226141201-fe205250686d/go.mod h1:jAkW2pwxa4N3qxEUDA3zDhfiJInBfF4BjsbkWAU13aU=
43-
github.com/function61/gokit v0.0.0-20200229073330-25232411c1f6 h1:UyuV6K5s+Y1eE7CdaBfwm+Yh6AiYFV2i6CfOuSJtvgw=
44-
github.com/function61/gokit v0.0.0-20200229073330-25232411c1f6/go.mod h1:jAkW2pwxa4N3qxEUDA3zDhfiJInBfF4BjsbkWAU13aU=
45+
github.com/function61/gokit v0.0.0-20200229115114-3eca8c87d0cc h1:FWizzRbJISAu9y2vKheno6BpHFkMnBl4mFl1nV9wKBI=
46+
github.com/function61/gokit v0.0.0-20200229115114-3eca8c87d0cc/go.mod h1:jAkW2pwxa4N3qxEUDA3zDhfiJInBfF4BjsbkWAU13aU=
4547
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
4648
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
4749
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -105,6 +107,7 @@ github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTK
105107
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
106108
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
107109
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
110+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
108111
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
109112
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
110113
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -156,6 +159,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
156159
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
157160
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
158161
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
162+
github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 h1:NSWpaDaurcAJY7PkL8Xt0PhZE7qpvbZl5ljd8r6U0bI=
163+
github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
159164
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
160165
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
161166
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=

pkg/lambdautils/apigatewayutils.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
11
package lambdautils
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
7+
"github.com/apex/gateway"
68
"github.com/aws/aws-lambda-go/events"
79
"net/http"
810
)
911

12+
// github.com/akrylysov/algnhsa has similar implementation than apex/gateway, but had the
13+
// useful bits non-exported and it used httptest for production code
14+
func ServeApiGatewayProxyRequestUsingHttpHandler(
15+
ctx context.Context,
16+
proxyRequest *events.APIGatewayProxyRequest,
17+
httpHandler http.Handler,
18+
) ([]byte, error) {
19+
request, err := gateway.NewRequest(ctx, *proxyRequest)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
response := gateway.NewResponse()
25+
26+
httpHandler.ServeHTTP(response, request)
27+
28+
proxyResponse := response.End()
29+
30+
return json.Marshal(&proxyResponse)
31+
}
32+
1033
func Redirect(to string) events.APIGatewayProxyResponse {
1134
return events.APIGatewayProxyResponse{
1235
StatusCode: http.StatusFound,

0 commit comments

Comments
 (0)