Skip to content
This repository has been archived by the owner on Jul 20, 2024. It is now read-only.

Commit

Permalink
Add Lambda ALB Target Group support (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
int128 authored Sep 26, 2019
1 parent cfdbaef commit ef322f2
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 114 deletions.
12 changes: 1 addition & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ gcloud app deploy --project=jira-to-slack appengine/app.yaml

### AWS Lambda

You can deploy the application to AWS Lambda and API Gateway.
You can deploy the application to AWS Lambda.

```sh
# Run locally
Expand All @@ -128,16 +128,6 @@ make -C lambda deploy SAM_S3_BUCKET_NAME=YOUR_BUCKET_NAME

You need to create a S3 bucket in the same region before deploying.

If you want to deploy the application to AWS Lambda and ALB Target Group,
you need to change the request and response types as follows:

```sh
sed -i \
-e s/APIGatewayProxyRequest/ALBTargetGroupRequest/g \
-e s/APIGatewayProxyResponse/ALBTargetGroupResponse/g \
lambda/main.go
```


## How it works

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ require (
github.com/gorilla/handlers v1.4.2
github.com/gorilla/mux v1.6.2
github.com/int128/slack v1.5.0
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
google.golang.org/appengine v1.6.2
)
87 changes: 11 additions & 76 deletions lambda/main.go
Original file line number Diff line number Diff line change
@@ -1,85 +1,20 @@
package main

import (
"context"
"encoding/json"
"fmt"
"net/http"
"log"
"os"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/int128/jira-to-slack/pkg/handlers"
"github.com/int128/jira-to-slack/pkg/jira"
"github.com/int128/jira-to-slack/pkg/usecases"
lambdaRouter "github.com/int128/jira-to-slack/pkg/router/lambda"
)

func handleIndex(_ context.Context, r events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
params, err := handlers.ParseWebhookParams(r.MultiValueQueryStringParameters)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Headers: map[string]string{"content-type": "text/plain"},
Body: fmt.Sprintf("OK\n%s", err.Error()),
}, nil
}
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Body: fmt.Sprintf("OK\nreceived the parameters: %+v", params),
}, nil
}

func handleWebhook(ctx context.Context, r events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
params, err := handlers.ParseWebhookParams(r.MultiValueQueryStringParameters)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusBadRequest,
Headers: map[string]string{"content-type": "text/plain"},
Body: err.Error(),
}, nil
}
var event jira.Event
if err := json.Unmarshal([]byte(r.Body), &event); err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusBadRequest,
Headers: map[string]string{"content-type": "text/plain"},
Body: fmt.Sprintf("could not decode json of response body: %s", err),
}, nil
}
in := usecases.WebhookIn{
JiraEvent: &event,
SlackWebhookURL: params.Webhook,
SlackUsername: params.Username,
SlackIcon: params.Icon,
SlackDialect: params.Dialect,
}
var u usecases.Webhook
if err := u.Do(ctx, in); err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Headers: map[string]string{"content-type": "text/plain"},
Body: err.Error(),
}, nil
}
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Headers: map[string]string{"content-type": "text/plain"},
Body: "OK",
}, nil
}

func handler(ctx context.Context, r events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
if r.HTTPMethod == "GET" {
return handleIndex(ctx, r)
}
if r.HTTPMethod == "POST" {
return handleWebhook(ctx, r)
}
return events.APIGatewayProxyResponse{
StatusCode: http.StatusMethodNotAllowed,
Body: "Method Not Allowed",
}, nil
}

func main() {
lambda.Start(handler)
switch os.Getenv("LAMBDA_TYPE") {
case "APIGateway":
lambda.Start(lambdaRouter.APIGateway)
case "ALBTargetGroup":
lambda.Start(lambdaRouter.ALBTargetGroup)
default:
log.Fatal("you need to set LAMBDA_TYPE environment variable")
}
}
4 changes: 4 additions & 0 deletions lambda/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Resources:
Handler: jira-to-slack
Runtime: go1.x
Tracing: Active
Environment:
Variables:
# you can set APIGateway or ALBTargetGroup
LAMBDA_TYPE: APIGateway
Events:
# https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Index:
Expand Down
20 changes: 16 additions & 4 deletions pkg/handlers/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,30 @@ import (
"fmt"
"log"
"net/http"
"net/url"

"golang.org/x/xerrors"
)

// Index handles index requests.
type Index struct{}

func (h *Index) ServeHTTP(w http.ResponseWriter, r *http.Request) {
params, err := ParseWebhookParams(r.URL.Query())
code, body, err := h.Serve(r.URL.Query())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, err.Error(), code)
return
}
if _, err := fmt.Fprintf(w, "Parameter=%+v", params); err != nil {
log.Printf("Error while writing response: %s", err)
w.WriteHeader(code)
if _, err := fmt.Fprint(w, body); err != nil {
log.Printf("could not write body: %s", err)
}
}

func (h *Index) Serve(v url.Values) (int, string, error) {
params, err := parseWebhookParams(v)
if err != nil {
return http.StatusBadRequest, "", xerrors.Errorf("could not parse query: %w", err)
}
return http.StatusOK, fmt.Sprintf("%+v", *params), nil
}
42 changes: 19 additions & 23 deletions pkg/handlers/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"

"github.com/int128/jira-to-slack/pkg/jira"
"github.com/int128/jira-to-slack/pkg/usecases"
"github.com/int128/slack/dialect"
"golang.org/x/xerrors"
)

// Webhook handles requests from JIRA webhook.
Expand All @@ -27,7 +29,7 @@ type WebhookParams struct {
Debug bool
}

func ParseWebhookParams(q url.Values) (*WebhookParams, error) {
func parseWebhookParams(q url.Values) (*WebhookParams, error) {
var p WebhookParams
p.Webhook = q.Get("webhook")
if p.Webhook == "" {
Expand Down Expand Up @@ -57,40 +59,35 @@ func ParseWebhookParams(q url.Values) (*WebhookParams, error) {
return &p, nil
}

func parseWebhookBody(r *http.Request) (*jira.Event, error) {
var event jira.Event
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
return nil, fmt.Errorf("could not decode json of request body: %s", err)
}
return &event, nil
}

func (h *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer r.Body.Close()
params, err := ParseWebhookParams(r.URL.Query())
code, err := h.Serve(r.Context(), r.URL.Query(), r.Body)
if err != nil {
log.Print(err)
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, err.Error(), code)
return
}
event, err := parseWebhookBody(r)
w.WriteHeader(code)
}

func (h *Webhook) Serve(ctx context.Context, v url.Values, body io.Reader) (int, error) {
params, err := parseWebhookParams(v)
if err != nil {
log.Print(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
return http.StatusBadRequest, xerrors.Errorf("could not parse query: %w", err)
}
var event jira.Event
if err := json.NewDecoder(body).Decode(&event); err != nil {
return http.StatusBadRequest, xerrors.Errorf("could not parse body: %w", err)
}
if params.Debug {
log.Printf("Received parameters %+v", params)
log.Printf("Received event %+v", &event)
log.Printf("Received event %+v", event)
}
var hc *http.Client
if h.HTTPClientFactory != nil {
hc = h.HTTPClientFactory(ctx)
}

in := usecases.WebhookIn{
JiraEvent: event,
JiraEvent: &event,
SlackWebhookURL: params.Webhook,
SlackUsername: params.Username,
SlackChannel: params.Channel,
Expand All @@ -100,8 +97,7 @@ func (h *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
var u usecases.Webhook
if err := u.Do(ctx, in); err != nil {
log.Print(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
return http.StatusInternalServerError, xerrors.Errorf("error: %w", err)
}
return http.StatusOK, nil
}
53 changes: 53 additions & 0 deletions pkg/router/lambda/alb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package lambda

import (
"context"
"net/http"
"strings"

"github.com/aws/aws-lambda-go/events"
"github.com/int128/jira-to-slack/pkg/handlers"
)

func ALBTargetGroup(ctx context.Context, r events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) {
switch {
case r.HTTPMethod == "GET":
var h handlers.Index
code, body, err := h.Serve(r.MultiValueQueryStringParameters)
if err != nil {
return events.ALBTargetGroupResponse{
StatusCode: http.StatusOK,
Headers: map[string]string{"content-type": "text/plain"},
Body: err.Error(),
}, nil
}
return events.ALBTargetGroupResponse{
StatusCode: code,
Headers: map[string]string{"content-type": "text/plain"},
Body: body,
}, nil

case r.HTTPMethod == "POST":
var h handlers.Webhook
code, err := h.Serve(ctx, r.MultiValueQueryStringParameters, strings.NewReader(r.Body))
if err != nil {
return events.ALBTargetGroupResponse{
StatusCode: code,
Headers: map[string]string{"content-type": "text/plain"},
Body: err.Error(),
}, nil
}
return events.ALBTargetGroupResponse{
StatusCode: code,
Headers: map[string]string{"content-type": "text/plain"},
Body: "OK",
}, nil

default:
return events.ALBTargetGroupResponse{
StatusCode: http.StatusMethodNotAllowed,
Headers: map[string]string{"content-type": "text/plain"},
Body: "Method Not Allowed",
}, nil
}
}
53 changes: 53 additions & 0 deletions pkg/router/lambda/api_gateway.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package lambda

import (
"context"
"net/http"
"strings"

"github.com/aws/aws-lambda-go/events"
"github.com/int128/jira-to-slack/pkg/handlers"
)

func APIGateway(ctx context.Context, r events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
switch {
case r.HTTPMethod == "GET":
var h handlers.Index
code, body, err := h.Serve(r.MultiValueQueryStringParameters)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Headers: map[string]string{"content-type": "text/plain"},
Body: err.Error(),
}, nil
}
return events.APIGatewayProxyResponse{
StatusCode: code,
Headers: map[string]string{"content-type": "text/plain"},
Body: body,
}, nil

case r.HTTPMethod == "POST":
var h handlers.Webhook
code, err := h.Serve(ctx, r.MultiValueQueryStringParameters, strings.NewReader(r.Body))
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: code,
Headers: map[string]string{"content-type": "text/plain"},
Body: err.Error(),
}, nil
}
return events.APIGatewayProxyResponse{
StatusCode: code,
Headers: map[string]string{"content-type": "text/plain"},
Body: "OK",
}, nil

default:
return events.APIGatewayProxyResponse{
StatusCode: http.StatusMethodNotAllowed,
Headers: map[string]string{"content-type": "text/plain"},
Body: "Method Not Allowed",
}, nil
}
}

0 comments on commit ef322f2

Please sign in to comment.