Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update readme for this project #20

Merged
merged 1 commit into from Mar 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/workflows/release.yml
@@ -0,0 +1,66 @@
name: Release

on:
push:
branches:
- main

jobs:
verify:
name: Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:12-alpine
env:
POSTGRES_DB: postmand
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: 1.16
id: go

- name: Check out code into the Go module directory
uses: actions/checkout@v2

- name: Set cache
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-

- name: Get dependencies
run: go mod download

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: latest
args: -E gosec

- name: Run Tests
env:
POSTMAND_TEST_DATABASE_URL: "postgres://test:test@localhost:5432/postmand?sslmode=disable"
run: make db-test-migrate && go test -covermode=count -coverprofile=count.out -v ./...

release-please:
needs: verify
runs-on: ubuntu-latest
steps:
- uses: GoogleCloudPlatform/release-please-action@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
release-type: simple
package-name: postmand
3 changes: 0 additions & 3 deletions .gitignore
Expand Up @@ -25,6 +25,3 @@ migrate.linux-amd64

# dot env
.env

# goreleaser
dist/
29 changes: 0 additions & 29 deletions .goreleaser.yml

This file was deleted.

Empty file added CHANGELOG.md
Empty file.
8 changes: 7 additions & 1 deletion Makefile
Expand Up @@ -26,4 +26,10 @@ db-migrate: download-golang-migrate-binary
db-test-migrate: download-golang-migrate-binary
./migrate.$(PLATFORM)-amd64 -source file://db/migrations -database ${POSTMAND_TEST_DATABASE_URL} up

.PHONY: lint test mock download-golang-migrate-binary db-migrate db-test-migrate
run-server:
go run cmd/postmand/main.go server

run-worker:
go run cmd/postmand/main.go worker

.PHONY: lint test mock download-golang-migrate-binary db-migrate db-test-migrate run-server run-worker
236 changes: 236 additions & 0 deletions README.md
@@ -1,2 +1,238 @@
# postmand
[![Build Status](https://github.com/allisson/postmand/workflows/release/badge.svg)](https://github.com/allisson/postmand/actions)
[![Go Report Card](https://goreportcard.com/badge/github.com/allisson/postmand)](https://goreportcard.com/report/github.com/allisson/postmand)
[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/allisson/postmand)
[![Docker Image](https://img.shields.io/docker/cloud/build/allisson/postmand)](https://hub.docker.com/r/allisson/postmand)

Simple webhook delivery system powered by Golang and PostgreSQL.

## Features

- Simple rest api with only three endpoints (webhooks/deliveries/delivery-attempts).
- Select the status codes that are considered valid for a delivery.
- Control the maximum amount of delivery attempts and delay between these attempts (min and max backoff).
- Locks control of worker deliveries using PostgreSQL SELECT FOR UPDATE SKIP LOCKED.
- Sending the X-Hub-Signature header if the webhook is configured with a secret token.
- Simplicity, it does the minimum necessary, it will not have authentication/permission scheme among other things, the idea is to use it internally in the cloud and not leave exposed.

## Quickstart

Let's start with the basic concepts, we have three main entities that we must know to start:

- Webhook: The configuration of the webhook.
- Delivery: The content sent to a webhook.
- Delivery Attempt: An attempt to deliver the content to the webhook.

### Run the server

To run the server it is necessary to have a database available from postgresql, in this example we will consider that we have a database called postmand running in localhost with user and password equal to user.

#### Docker

```bash
docker run --rm --env POSTMAND_DATABASE_URL='postgres://user:password@host.docker.internal:5432/postmand?sslmode=disable' allisson/postmand migrate # create database schema
```

```bash
docker run -p 8000:8000 -p 8001:8001 --env POSTMAND_DATABASE_URL='postgres://user:password@host.docker.internal:5432/postmand?sslmode=disable' allisson/postmand server # run the server
```

#### Local

```bash
git clone https://github.com/allisson/postmand
cd postmand
cp local.env .env # and edit .env
make run-migrate # create database schema
make run-server # run the server
```

### Create a new webhook

The fields delivery_attempt_timeout/retry_min_backoff/retry_max_backoff are in seconds.

```bash
curl --location --request POST 'http://localhost:8000/v1/webhooks' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Httpbin Post",
"url": "https://httpbin.org/post",
"content_type": "application/json",
"valid_status_codes": [
200,
201
],
"secret_token": "my-secret-token",
"active": true,
"max_delivery_attempts": 5,
"delivery_attempt_timeout": 1,
"retry_min_backoff": 10,
"retry_max_backoff": 60
}'
```

```javascript
{
"id":"a6e9a525-ac5a-488c-b118-bd7327ce6d8d",
"name":"Httpbin Post",
"url":"https://httpbin.org/post",
"content_type":"application/json",
"valid_status_codes":[
200,
201
],
"secret_token":"my-secret-token",
"active":true,
"max_delivery_attempts":5,
"delivery_attempt_timeout":1,
"retry_min_backoff":10,
"retry_max_backoff":60,
"created_at":"2021-03-08T20:41:25.433671Z",
"updated_at":"2021-03-08T20:41:25.433671Z"
}
```

### Create a new delivery

```bash
curl --location --request POST 'http://localhost:8000/v1/deliveries' \
--header 'Content-Type: application/json' \
--data-raw '{
"webhook_id": "a6e9a525-ac5a-488c-b118-bd7327ce6d8d",
"payload": "{\"success\": true}"
}'
```

```javascript
{
"id":"bc76122c-e56b-45c7-8dc3-b80a861191d5",
"webhook_id":"a6e9a525-ac5a-488c-b118-bd7327ce6d8d",
"payload":"{\"success\": true}",
"scheduled_at":"2021-03-08T20:43:49.986771Z",
"delivery_attempts":0,
"status":"pending",
"created_at":"2021-03-08T20:43:49.986771Z",
"updated_at":"2021-03-08T20:43:49.986771Z"
}
```

### Run the worker

The worker is responsible to delivery content to the webhooks.

#### Docker

```bash
docker run --env POSTMAND_DATABASE_URL='postgres://user:pass@host.docker.internal:5432/postmand?sslmode=disable' allisson/postmand worker
{"level":"info","ts":1615236411.115703,"caller":"service/worker.go:74","msg":"worker-started"}
{"level":"info","ts":1615236411.1158803,"caller":"http/server.go:60","msg":"http-server-listen-and-server"}
{"level":"info","ts":1615236411.687701,"caller":"service/worker.go:42","msg":"worker-delivery-attempt-created","id":"d72719d6-5a79-4df7-a2c2-2029ab0e1848","webhook_id":"a6e9a525-ac5a-488c-b118-bd7327ce6d8d","delivery_id":"bc76122c-e56b-45c7-8dc3-b80a861191d5","response_status_code":200,"execution_duration":547,"success":true}
```

#### Local

```bash
make run-worker
go run cmd/postmand/main.go worker
{"level":"info","ts":1615236411.115703,"caller":"service/worker.go:74","msg":"worker-started"}
{"level":"info","ts":1615236411.1158803,"caller":"http/server.go:60","msg":"http-server-listen-and-server"}
{"level":"info","ts":1615236411.687701,"caller":"service/worker.go:42","msg":"worker-delivery-attempt-created","id":"d72719d6-5a79-4df7-a2c2-2029ab0e1848","webhook_id":"a6e9a525-ac5a-488c-b118-bd7327ce6d8d","delivery_id":"bc76122c-e56b-45c7-8dc3-b80a861191d5","response_status_code":200,"execution_duration":547,"success":true}
```

### Get deliveries

```bash
curl --location --request GET 'http://localhost:8000/v1/deliveries?webhook_id=a6e9a525-ac5a-488c-b118-bd7327ce6d8d'
```

```javascript
{
"deliveries":[
{
"id":"bc76122c-e56b-45c7-8dc3-b80a861191d5",
"webhook_id":"a6e9a525-ac5a-488c-b118-bd7327ce6d8d",
"payload":"{\"success\": true}",
"scheduled_at":"2021-03-08T20:43:49.986771Z",
"delivery_attempts":1,
"status":"succeeded",
"created_at":"2021-03-08T20:43:49.986771Z",
"updated_at":"2021-03-08T20:46:51.674623Z"
}
],
"limit":50,
"offset":0
}
```

### Get delivery

```bash
curl --location --request GET 'http://localhost:8000/v1/deliveries/bc76122c-e56b-45c7-8dc3-b80a861191d5'
```

```javascript
{
"id":"bc76122c-e56b-45c7-8dc3-b80a861191d5",
"webhook_id":"a6e9a525-ac5a-488c-b118-bd7327ce6d8d",
"payload":"{\"success\": true}",
"scheduled_at":"2021-03-08T20:43:49.986771Z",
"delivery_attempts":1,
"status":"succeeded",
"created_at":"2021-03-08T20:43:49.986771Z",
"updated_at":"2021-03-08T20:46:51.674623Z"
}
```

### Get delivery attempts

```bash
curl --location --request GET 'http://localhost:8000/v1/delivery-attempts?delivery_id=bc76122c-e56b-45c7-8dc3-b80a861191d5'
```

```javascript
{
"delivery_attempts":[
{
"id":"d72719d6-5a79-4df7-a2c2-2029ab0e1848",
"webhook_id":"a6e9a525-ac5a-488c-b118-bd7327ce6d8d",
"delivery_id":"bc76122c-e56b-45c7-8dc3-b80a861191d5",
"raw_request":"POST /post HTTP/1.1\r\nHost: httpbin.org\r\nContent-Type: application/json\r\nX-Hub-Signature: 3fc5d4b8ff4efb404be24faf543667d29902d6a1306bd0c1ef2084497300cee9\r\n\r\n{\"success\": true}",
"raw_response":"HTTP/2.0 200 OK\r\nContent-Length: 538\r\nAccess-Control-Allow-Credentials: true\r\nAccess-Control-Allow-Origin: *\r\nContent-Type: application/json\r\nDate: Mon, 08 Mar 2021 20:46:51 GMT\r\nServer: gunicorn/19.9.0\r\n\r\n{\n \"args\": {}, \n \"data\": \"{\\\"success\\\": true}\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Accept-Encoding\": \"gzip\", \n \"Content-Length\": \"17\", \n \"Content-Type\": \"application/json\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"Go-http-client/2.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-60468d3b-36d312777a03ec3e1c564e3b\", \n \"X-Hub-Signature\": \"3fc5d4b8ff4efb404be24faf543667d29902d6a1306bd0c1ef2084497300cee9\"\n }, \n \"json\": {\n \"success\": true\n }, \n \"origin\": \"191.35.122.74\", \n \"url\": \"https://httpbin.org/post\"\n}\n",
"response_status_code":200,
"execution_duration":547,
"success":true,
"error":"",
"created_at":"2021-03-08T20:46:51.680846Z"
}
],
"limit":50,
"offset":0
}
```

### Get delivery attempt

```bash
curl --location --request GET 'http://localhost:8000/v1/delivery-attempts/d72719d6-5a79-4df7-a2c2-2029ab0e1848'
```

```javascript
{
"id":"d72719d6-5a79-4df7-a2c2-2029ab0e1848",
"webhook_id":"a6e9a525-ac5a-488c-b118-bd7327ce6d8d",
"delivery_id":"bc76122c-e56b-45c7-8dc3-b80a861191d5",
"raw_request":"POST /post HTTP/1.1\r\nHost: httpbin.org\r\nContent-Type: application/json\r\nX-Hub-Signature: 3fc5d4b8ff4efb404be24faf543667d29902d6a1306bd0c1ef2084497300cee9\r\n\r\n{\"success\": true}",
"raw_response":"HTTP/2.0 200 OK\r\nContent-Length: 538\r\nAccess-Control-Allow-Credentials: true\r\nAccess-Control-Allow-Origin: *\r\nContent-Type: application/json\r\nDate: Mon, 08 Mar 2021 20:46:51 GMT\r\nServer: gunicorn/19.9.0\r\n\r\n{\n \"args\": {}, \n \"data\": \"{\\\"success\\\": true}\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Accept-Encoding\": \"gzip\", \n \"Content-Length\": \"17\", \n \"Content-Type\": \"application/json\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"Go-http-client/2.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-60468d3b-36d312777a03ec3e1c564e3b\", \n \"X-Hub-Signature\": \"3fc5d4b8ff4efb404be24faf543667d29902d6a1306bd0c1ef2084497300cee9\"\n }, \n \"json\": {\n \"success\": true\n }, \n \"origin\": \"191.35.122.74\", \n \"url\": \"https://httpbin.org/post\"\n}\n",
"response_status_code":200,
"execution_duration":547,
"success":true,
"error":"",
"created_at":"2021-03-08T20:46:51.680846Z"
}
```

## How to build docker image

```
docker build -f Dockerfile -t postmand .
```
4 changes: 2 additions & 2 deletions cmd/postmand/main.go
Expand Up @@ -23,7 +23,7 @@ func healthcheckServer(db *sqlx.DB, logger *zap.Logger) {
pingHandler := handler.NewPing(pingService, logger)
mux := http.NewRouter(logger)
mux.Get("/healthz", pingHandler.Healthz)
server := http.NewServer(mux, env.GetInt("POSTMAND_HEALTH_CHECK_HTTP_PORT", 8000), logger)
server := http.NewServer(mux, env.GetInt("POSTMAND_HEALTH_CHECK_HTTP_PORT", 8001), logger)
server.Run()
}

Expand Down Expand Up @@ -66,7 +66,7 @@ func main() {
db,
env.GetString("POSTMAND_DATABASE_MIGRATION_DIR", "file:///db/migrations"),
)
migrationService := service.NewMigration(migrationRepository)
migrationService := service.NewMigration(migrationRepository, logger)
return migrationService.Run(c.Context)
},
},
Expand Down
2 changes: 1 addition & 1 deletion http/handler/delivery.go
Expand Up @@ -23,7 +23,7 @@ type Delivery struct {

// List deliveries.
func (d Delivery) List(w http.ResponseWriter, r *http.Request) {
listOptions, err := makeListOptions(r, []string{})
listOptions, err := makeListOptions(r, []string{"webhook_id", "status"})
if err != nil {
er := errorResponses["internal_server_error"]
makeErrorResponse(w, &er, d.logger)
Expand Down