Skip to content

Commit

Permalink
Add AWS Lambda support (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
int128 committed Aug 16, 2019
1 parent 36e2d5d commit 6892276
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 39 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
/dist/
/.idea

# Lambda
/lambda/jira-to-slack
/lambda/packaged.yaml

# https://github.com/github/gitignore/blob/master/Go.gitignore
# Binaries for programs and plugins
*.exe
Expand Down
4 changes: 0 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ check:
$(TARGET): $(wildcard *.go)
go build -o $@ -ldflags "$(LDFLAGS)"

.PHONY: run-appengine
run-appengine:
dev_appserver.py --port 3000 appengine/app.yaml

dist:
goxzst -d dist -o "$(TARGET)" -- -ldflags "$(LDFLAGS)"

Expand Down
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# jira-to-slack [![CircleCI](https://circleci.com/gh/int128/jira-to-slack.svg?style=shield)](https://circleci.com/gh/int128/jira-to-slack)

This is a Slack and Mattermost integration for notifying Jira events.
It is written in Go and ready on App Engine.
It is written in Go and ready on Docker, App Engine and Lambda.


## Examples
Expand Down Expand Up @@ -74,20 +74,34 @@ docker run --rm -p 3000:3000 int128/jira-to-slack

### App Engine

You can deploy jira-to-slack to App Engine:
You can deploy jira-to-slack to App Engine.

```sh
# Install SDK
brew cask install google-cloud-sdk
gcloud components install app-engine-go

# Run
dev_appserver.py appengine/app.yaml
make -C appengine run

# Deploy
gcloud app deploy --project=jira-to-slack appengine/app.yaml
```

### Lambda

You can deploy jira-to-slack to AWS Lambda.

```sh
# Run
make -C lambda run

# Deploy
make -C lambda deploy SAM_S3_BUCKET_NAME=YOUR_BUCKET_NAME
```

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


## How it works

Expand Down Expand Up @@ -130,12 +144,6 @@ make
./jira-to-slack
```

App Engine:

```sh
make run-appengine
```

### E2E Test

You can send actual payloads of actual Jira events by the following script:
Expand Down
3 changes: 3 additions & 0 deletions appengine/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.PHONY: run
run:
dev_appserver.py --port 3000 app.yaml
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module github.com/int128/jira-to-slack

require (
github.com/aws/aws-lambda-go v1.12.1
github.com/go-test/deep v1.0.2
github.com/gorilla/handlers v1.4.0
github.com/gorilla/mux v1.6.2
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/aws/aws-lambda-go v1.12.1 h1:rMToYOcPFYDixQ7VNNPg78LmiqPgWD5f8zdLL+EsDAk=
github.com/aws/aws-lambda-go v1.12.1/go.mod h1:z4ywteZ5WwbIEzG0tXizIAUlUwkTNNknX4upd5Z5XJM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
Expand All @@ -8,8 +12,14 @@ github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/int128/slack v1.0.0 h1:scN8pKqZfehGcjySBDcACfePxn1PSjTvcLilW8DRLpI=
github.com/int128/slack v1.0.0/go.mod h1:ggEcWsAursfXSZUgnTVJVsLSthFPenN+KdZam1m7bp4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/appengine v1.2.0 h1:S0iUepdCWODXRvtE+gcRDd15L+k+k1AiHlMiMjefH24=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
19 changes: 19 additions & 0 deletions lambda/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
TARGET := jira-to-slack
VERSION := v0.0.0
LDFLAGS := -X main.version=$(VERSION)

all: $(TARGET)

$(TARGET): $(wildcard *.go)
GOOS=linux GOARCH=amd64 go build -o $@ -ldflags "$(LDFLAGS)"

.PHONY: run
run: $(TARGET)
sam local start-api

packaged.yaml: $(TARGET)
sam package --template-file template.yaml --output-template-file $@ --s3-bucket $(SAM_S3_BUCKET_NAME)

.PHONY: deploy
deploy: packaged.yaml
sam deploy --template-file $< --stack-name $(TARGET) --capabilities CAPABILITY_IAM
80 changes: 80 additions & 0 deletions lambda/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

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

"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"
)

func handleIndex(_ context.Context, r events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
params, err := handlers.ParseWebhookParams(r.MultiValueQueryStringParameters)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusBadRequest,
Body: err.Error(),
}, nil
}
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Body: fmt.Sprintf("Parameter=%+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,
Body: err.Error(),
}, nil
}
var event jira.Event
if err := json.Unmarshal([]byte(r.Body), &event); err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusBadRequest,
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,
Body: err.Error(),
}, nil
}
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
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)
}
39 changes: 39 additions & 0 deletions lambda/template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: jira-to-slack

Globals:
Function:
Timeout: 5

Resources:
JiraToSlackFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Handler: jira-to-slack
Runtime: go1.x
Tracing: Active
Events:
# https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Index:
Type: Api
Properties:
Path: /
Method: GET
Webhook:
Type: Api
Properties:
Path: /
Method: POST

Outputs:
JiraToSlackAPI:
Description: "API Gateway endpoint URL for JiraToSlackFunction"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/"
JiraToSlackFunction:
Description: "JiraToSlackFunction function"
Value: !GetAtt JiraToSlackFunction.Arn
HelloWorldFunctionIamRole:
Description: "Implicit IAM Role created for JiraToSlackFunction function"
Value: !GetAtt JiraToSlackFunctionRole.Arn
5 changes: 2 additions & 3 deletions pkg/handlers/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import (
type Index struct{}

func (h *Index) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p, err := parseWebhookParams(r)
params, err := ParseWebhookParams(r.URL.Query())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

if _, err := fmt.Fprintf(w, "Parameter=%+v", p); err != nil {
if _, err := fmt.Fprintf(w, "Parameter=%+v", params); err != nil {
log.Printf("Error while writing response: %s", err)
}
}
46 changes: 23 additions & 23 deletions pkg/handlers/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"net/http"
"net/url"

"github.com/int128/jira-to-slack/pkg/jira"
"github.com/int128/jira-to-slack/pkg/usecases"
Expand All @@ -16,38 +17,37 @@ type Webhook struct {
HTTPClientFactory func(*http.Request) *http.Client // Default to http.DefaultClient
}

type webhookParams struct {
webhook string
username string
icon string
dialect dialect.Dialect
debug bool
type WebhookParams struct {
Webhook string
Username string
Icon string
Dialect dialect.Dialect
Debug bool
}

func parseWebhookParams(r *http.Request) (*webhookParams, error) {
var p webhookParams
q := r.URL.Query()
p.webhook = q.Get("webhook")
if p.webhook == "" {
func ParseWebhookParams(q url.Values) (*WebhookParams, error) {
var p WebhookParams
p.Webhook = q.Get("webhook")
if p.Webhook == "" {
return nil, fmt.Errorf("missing query parameter. Request with ?webhook=https://hooks.slack.com/xxx")
}
p.username = q.Get("username")
p.icon = q.Get("icon")
p.Username = q.Get("username")
p.Icon = q.Get("icon")
switch q.Get("dialect") {
case "":
p.dialect = &dialect.Slack{}
p.Dialect = &dialect.Slack{}
case "slack":
p.dialect = &dialect.Slack{}
p.Dialect = &dialect.Slack{}
case "mattermost":
p.dialect = &dialect.Mattermost{}
p.Dialect = &dialect.Mattermost{}
default:
return nil, fmt.Errorf("dialect must be slack (default) or mattermost")
}
switch q.Get("debug") {
case "": // default
case "0": // default
case "1":
p.debug = true
p.Debug = true
default:
return nil, fmt.Errorf("debug must be 0 (default) or 1")
}
Expand All @@ -65,7 +65,7 @@ func parseWebhookBody(r *http.Request) (*jira.Event, error) {
func (h *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer r.Body.Close()
params, err := parseWebhookParams(r)
params, err := ParseWebhookParams(r.URL.Query())
if err != nil {
log.Print(err)
http.Error(w, err.Error(), http.StatusBadRequest)
Expand All @@ -77,7 +77,7 @@ func (h *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if params.debug {
if params.Debug {
log.Printf("Received parameters %+v", params)
log.Printf("Received event %+v", &event)
}
Expand All @@ -88,10 +88,10 @@ func (h *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {

in := usecases.WebhookIn{
JiraEvent: event,
SlackWebhookURL: params.webhook,
SlackUsername: params.username,
SlackIcon: params.icon,
SlackDialect: params.dialect,
SlackWebhookURL: params.Webhook,
SlackUsername: params.Username,
SlackIcon: params.Icon,
SlackDialect: params.Dialect,
HTTPClient: hc,
}
var u usecases.Webhook
Expand Down

0 comments on commit 6892276

Please sign in to comment.