mirror of
https://github.com/ncarlier/webhookd.git
synced 2024-09-20 00:45:29 +00:00
refactor(): Complete refactoring.
- No external dependencies - No predefined directory structure - Able to launch any kind of shell script with custom parameters - Get script output as text event stream (SSE) - Using common Makefiles - Extends docker/dind Docker image
This commit is contained in:
parent
059f91bd17
commit
14c214efdf
|
@ -1 +1 @@
|
||||||
dist/
|
release/
|
||||||
|
|
|
@ -10,11 +10,6 @@ APP_WORKING_DIR=/var/opt/webhookd/work
|
||||||
# Defaults: ./scripts
|
# Defaults: ./scripts
|
||||||
APP_SCRIPTS_DIR=/var/opt/webhookd/scripts
|
APP_SCRIPTS_DIR=/var/opt/webhookd/scripts
|
||||||
|
|
||||||
# Redirect scripts output in the console.
|
|
||||||
# Warning: Only for debugging purpose.
|
|
||||||
# Defaults: false
|
|
||||||
APP_SCRIPTS_DEBUG=false
|
|
||||||
|
|
||||||
# Notifier.
|
# Notifier.
|
||||||
# Notify script execution result and logs.
|
# Notify script execution result and logs.
|
||||||
# Values:
|
# Values:
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,3 +1,2 @@
|
||||||
dist/
|
release/
|
||||||
ssh
|
.vscode/
|
||||||
etc/env.conf
|
|
||||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "makefiles"]
|
||||||
|
path = makefiles
|
||||||
|
url = https://github.com/ncarlier/makefiles.git
|
59
Dockerfile
59
Dockerfile
|
@ -1,42 +1,39 @@
|
||||||
# webhookd image.
|
#########################################
|
||||||
#
|
# Build stage
|
||||||
# VERSION 0.0.1
|
#########################################
|
||||||
#
|
FROM golang:1.8 AS builder
|
||||||
# BUILD-USING: docker build --rm -t ncarlier/webhookd .
|
MAINTAINER Nicolas Carlier <n.carlier@nunux.org>
|
||||||
|
|
||||||
FROM golang:1.3
|
# Repository location
|
||||||
|
ARG REPOSITORY=github.com/ncarlier
|
||||||
|
|
||||||
|
# Artifact name
|
||||||
|
ARG ARTIFACT=webhookd
|
||||||
|
|
||||||
# Install ssh-keygen
|
# Copy sources into the container
|
||||||
RUN apt-get update && apt-get install -y ssh sudo
|
ADD . /go/src/$REPOSITORY/$ARTIFACT
|
||||||
|
|
||||||
# Install the latest version of the docker CLI
|
# Set working directory
|
||||||
RUN curl -L -o /usr/local/bin/docker https://get.docker.io/builds/Linux/x86_64/docker-latest && \
|
WORKDIR /go/src/$REPOSITORY/$ARTIFACT
|
||||||
chmod +x /usr/local/bin/docker
|
|
||||||
|
|
||||||
# Install GO application
|
# Build the binary
|
||||||
WORKDIR /go/src/github.com/ncarlier/webhookd
|
RUN make
|
||||||
ADD ./src /go/src/github.com/ncarlier/webhookd
|
|
||||||
RUN go get github.com/ncarlier/webhookd
|
|
||||||
|
|
||||||
# Add scripts
|
#########################################
|
||||||
ADD ./scripts /var/opt/webhookd/scripts
|
# Distribution stage
|
||||||
|
#########################################
|
||||||
|
FROM docker:dind
|
||||||
|
MAINTAINER Nicolas Carlier <n.carlier@nunux.org>
|
||||||
|
|
||||||
# Create work and ssh directories
|
# Repository location
|
||||||
RUN mkdir /var/opt/webhookd/work
|
ARG REPOSITORY=github.com/ncarlier
|
||||||
|
|
||||||
# Generate SSH deploiment key (should be overwrite by a volume)
|
# Artifact name
|
||||||
RUN ssh-keygen -N "" -f /root/.ssh/id_rsa
|
ARG ARTIFACT=webhookd
|
||||||
|
|
||||||
# Ignor strict host key checking
|
# Fix lib dep
|
||||||
RUN echo "Host github.com\n\tStrictHostKeyChecking no\n" >> /root/.ssh/config && \
|
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
|
||||||
echo "Host bitbucket.org\n\tStrictHostKeyChecking no\n" >> /root/.ssh/config
|
|
||||||
|
|
||||||
# Change workdir
|
# Install binary
|
||||||
WORKDIR /var/opt/webhookd
|
COPY --from=builder /go/src/$REPOSITORY/$ARTIFACT/release/$ARTIFACT-linux-amd64 /usr/local/bin/$ARTIFACT
|
||||||
|
|
||||||
# Port
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
CMD []
|
|
||||||
ENTRYPOINT ["/go/bin/webhookd"]
|
|
||||||
|
|
116
Makefile
116
Makefile
|
@ -1,79 +1,69 @@
|
||||||
.SILENT :
|
.SILENT :
|
||||||
.PHONY : volume mount build clean start stop rm shell test dist
|
|
||||||
|
|
||||||
USERNAME:=ncarlier
|
# Author
|
||||||
APPNAME:=webhookd
|
AUTHOR=github.com/ncarlier
|
||||||
IMAGE:=$(USERNAME)/$(APPNAME)
|
|
||||||
|
|
||||||
TAG:=`git describe --abbrev=0 --tags`
|
# App name
|
||||||
LDFLAGS:=-X main.buildVersion $(TAG)
|
APPNAME=webhookd
|
||||||
ROOTPKG:=github.com/$(USERNAME)
|
|
||||||
PKGDIR:=$(GOPATH)/src/$(ROOTPKG)
|
|
||||||
|
|
||||||
define docker_run_flags
|
# Go configuration
|
||||||
--rm \
|
GOOS?=linux
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
GOARCH?=amd64
|
||||||
--env-file $(PWD)/etc/env.conf \
|
|
||||||
-P \
|
# Add exe extension if windows target
|
||||||
-i -t
|
is_windows:=$(filter windows,$(GOOS))
|
||||||
endef
|
EXT:=$(if $(is_windows),".exe","")
|
||||||
|
|
||||||
|
# Go app path
|
||||||
|
APPBASE=${GOPATH}/src/$(AUTHOR)
|
||||||
|
|
||||||
|
# Artefact name
|
||||||
|
ARTEFACT=release/$(APPNAME)-$(GOOS)-$(GOARCH)$(EXT)
|
||||||
|
|
||||||
|
# Extract version infos
|
||||||
|
VERSION:=`git describe --tags`
|
||||||
|
LDFLAGS=-ldflags "-X $(AUTHOR)/$(APPNAME)/version.App=${VERSION}"
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
volume:
|
# Include common Make tasks
|
||||||
echo "Building $(APPNAME) volumes..."
|
root_dir:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||||
sudo docker run \
|
makefiles:=$(root_dir)/makefiles
|
||||||
-v $(PWD)/src:/go/src/$(ROOTPKG)/$(APPNAME) \
|
include $(makefiles)/help.Makefile
|
||||||
-v $(PWD)/scripts:/var/opt/$(APPNAME)/scripts \
|
|
||||||
--name $(APPNAME)_volumes busybox true
|
|
||||||
|
|
||||||
key:
|
$(APPBASE)/$(APPNAME):
|
||||||
$(eval docker_run_flags += -v $(PWD)/ssh:/root/.ssh)
|
echo "Creating GO src link: $(APPBASE)/$(APPNAME) ..."
|
||||||
echo "Add private deploy key"
|
mkdir -p $(APPBASE)
|
||||||
|
ln -s $(root_dir) $(APPBASE)/$(APPNAME)
|
||||||
|
|
||||||
mount:
|
## Clean built files
|
||||||
$(eval docker_run_flags += --volumes-from $(APPNAME)_volumes)
|
clean:
|
||||||
echo "Using volumes from $(APPNAME)_volumes"
|
-rm -rf release
|
||||||
|
.PHONY: clean
|
||||||
|
|
||||||
build:
|
## Build executable
|
||||||
echo "Building $(IMAGE) docker image..."
|
build: $(APPBASE)/$(APPNAME)
|
||||||
sudo docker build --rm -t $(IMAGE) .
|
-mkdir -p release
|
||||||
|
echo "Building: $(ARTEFACT) ..."
|
||||||
|
GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(LDFLAGS) -o $(ARTEFACT)
|
||||||
|
.PHONY: build
|
||||||
|
|
||||||
clean: stop rm
|
$(ARTEFACT): build
|
||||||
echo "Removing $(IMAGE) docker image..."
|
|
||||||
sudo docker rmi $(IMAGE)
|
|
||||||
|
|
||||||
start:
|
|
||||||
echo "Running $(IMAGE) docker image..."
|
|
||||||
sudo docker run $(docker_run_flags) --name $(APPNAME) $(IMAGE)
|
|
||||||
|
|
||||||
stop:
|
|
||||||
echo "Stopping container $(APPNAME) ..."
|
|
||||||
-sudo docker stop $(APPNAME)
|
|
||||||
|
|
||||||
rm:
|
|
||||||
echo "Deleting container $(APPNAME) ..."
|
|
||||||
-sudo docker rm $(APPNAME)
|
|
||||||
|
|
||||||
shell:
|
|
||||||
echo "Running $(IMAGE) docker image with shell access..."
|
|
||||||
sudo docker run $(docker_run_flags) --entrypoint="/bin/bash" $(IMAGE) -c /bin/bash
|
|
||||||
|
|
||||||
|
## Run tests
|
||||||
test:
|
test:
|
||||||
echo "Running tests..."
|
go test
|
||||||
test.sh
|
.PHONY: test
|
||||||
|
|
||||||
dist-prepare:
|
## Install executable
|
||||||
rm -rf $(PKGDIR)
|
install: $(ARTEFACT)
|
||||||
mkdir -p $(PKGDIR)
|
echo "Installing $(ARTEFACT) to ${HOME}/.local/bin/$(APPNAME) ..."
|
||||||
ln -s $(PWD)/src $(PKGDIR)/$(APPNAME)
|
cp $(ARTEFACT) ${HOME}/.local/bin/$(APPNAME)
|
||||||
rm -rf dist
|
.PHONY: install
|
||||||
|
|
||||||
dist: dist-prepare
|
|
||||||
# godep restore
|
|
||||||
mkdir -p dist/linux/amd64 && GOOS=linux GOARCH=amd64 go build -o dist/linux/amd64/$(APPNAME) ./src
|
|
||||||
tar -cvzf dist/$(APPNAME)-linux-amd64-$(TAG).tar.gz -C dist/linux/amd64 $(APPNAME)
|
|
||||||
# mkdir -p dist/linux/i386 && GOOS=linux GOARCH=386 go build -o dist/linux/i386/$(APPNAME) ./src
|
|
||||||
# tar -cvzf dist/$(APPNAME)-linux-i386-i386$(TAG).tar.gz -C dist/linux/i386 $(APPNAME)
|
|
||||||
|
|
||||||
|
## Create Docker image
|
||||||
|
image:
|
||||||
|
echo "Building Docker inage ..."
|
||||||
|
docker build --rm -t ncarlier/$(APPNAME) .
|
||||||
|
.PHONY: image
|
||||||
|
|
||||||
|
|
138
README.md
138
README.md
|
@ -5,97 +5,151 @@
|
||||||
|
|
||||||
A very simple webhook server to launch shell scripts.
|
A very simple webhook server to launch shell scripts.
|
||||||
|
|
||||||
It can be used as a cheap alternative of Docker hub in order to build private Docker images.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Binaries
|
### Using the binary
|
||||||
|
|
||||||
Linux binaries for release [0.0.3](https://github.com/ncarlier/webhookd/releases)
|
Linux binaries for release [1.0.0](https://github.com/ncarlier/webhookd/releases)
|
||||||
|
|
||||||
* [amd64](https://github.com/ncarlier/webhookd/releases/download/v0.0.3/webhookd-linux-amd64-v0.0.3.tar.gz)
|
* [amd64](https://github.com/ncarlier/webhookd/releases/download/v1.0.0/webhookd-linux-amd64-v1.0.0.tar.gz)
|
||||||
|
|
||||||
Download the version you need, untar, and install to your PATH.
|
Download the version you need, untar, and install to your PATH.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ wget https://github.com/ncarlier/webhookd/releases/download/v0.0.3/webhookd-linux-amd64-v0.0.3.tar.gz
|
$ wget https://github.com/ncarlier/webhookd/releases/download/v1.0.0/webhookd-linux-amd64-v1.0.0.tar.gz
|
||||||
$ tar xvzf webhookd-linux-amd64-v0.0.3.tar.gz
|
$ tar xvzf webhookd-linux-amd64-v1.0.0.tar.gz
|
||||||
$ ./webhookd
|
$ ./webhookd
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
### Using Docker
|
||||||
|
|
||||||
Start the container mounting your scripts directory:
|
Start the container mounting your scripts directory:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ docker run -d --name=webhookd \
|
$ docker run -d --name=webhookd \
|
||||||
--env-file etc/env.conf \
|
--env-file .env \
|
||||||
-v ${PWD}/scripts:/var/opt/webhookd/scripts \
|
-v ${PWD}/scripts:/var/opt/webhookd/scripts \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
ncarlier/webhookd
|
ncarlier/webhookd webhookd
|
||||||
```
|
```
|
||||||
|
|
||||||
The provided environment file (`etc/env.conf`) is used to configure the app.
|
Check the provided environment file [.env](.env) for details.
|
||||||
Check [sample configuration](etc/env_sample.com) for details.
|
|
||||||
|
> Note that this image extends `docker:dind` Docker image. Therefore you are
|
||||||
|
> able to interact with a Docker daemon with yours shell scripts.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Create your own scripts template in the **scripts** directory.
|
### Directory structure
|
||||||
|
|
||||||
Respect the following structure:
|
Webhooks are simple scripts dispatched into a directory structure.
|
||||||
|
|
||||||
|
By default inside the `./scripts` directory.
|
||||||
|
You can override the default using the `APP_SCRIPTS_DIR` environment variable.
|
||||||
|
|
||||||
|
*Example:*
|
||||||
|
|
||||||
```
|
```
|
||||||
/scripts
|
/scripts
|
||||||
|--> /bitbucket
|
|
||||||
|--> /script_1.sh
|
|
||||||
|--> /script_2.sh
|
|
||||||
|--> /github
|
|--> /github
|
||||||
|--> /gitlab
|
|--> /build.sh
|
||||||
|--> /docker
|
|--> /deploy.sh
|
||||||
|
|--> /ping.sh
|
||||||
|
|--> ...
|
||||||
```
|
```
|
||||||
|
|
||||||
The hookname you will use will be related to the hook you want to use (github, bitbucket, ...) and the script name you want to call:
|
### Webhook URL
|
||||||
For instance if you are **gitlab** and want to call **build.sh** then you will need to use:
|
|
||||||
|
|
||||||
```
|
The directory structure define the webhook URL.
|
||||||
http://webhook_ip:port/gitlab/build
|
The Webhook can only be call with HTTP POST verb.
|
||||||
|
If the script exists, the HTTP response will be a `text/event-stream` content
|
||||||
|
type (Server-sent events).
|
||||||
|
|
||||||
|
*Example:*
|
||||||
|
|
||||||
|
The script: `./scripts/foo/bar.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "foo foo foo"
|
||||||
|
echo "bar bar bar"
|
||||||
```
|
```
|
||||||
|
|
||||||
It is important to use the right hook in order for your script to received parameters extract from the hook payload.
|
```bash
|
||||||
|
$ curl -XPOST http://localhost/foo/bar
|
||||||
|
data: Hook work request "foo/bar" queued...
|
||||||
|
|
||||||
|
data: Running foo/bar script...
|
||||||
|
|
||||||
For now, supported hooks are:
|
data: foo foo foo
|
||||||
|
|
||||||
- GitHub
|
data: bar bar bar
|
||||||
- Gitlab
|
|
||||||
- Bitbucket
|
|
||||||
- Docker Hub
|
|
||||||
|
|
||||||
|
data: done
|
||||||
Check the scripts directory for samples.
|
|
||||||
|
|
||||||
Once the action script created, you can trigger the webhook :
|
|
||||||
|
|
||||||
```
|
|
||||||
$ curl -H "Content-Type: application/json" \
|
|
||||||
--data @payload.json \
|
|
||||||
http://localhost:8080/<hookname>/<action>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The action script's output is collected and sent by email or by HTTP request.
|
### Webhook parameters
|
||||||
|
|
||||||
The HTTP notification need some configuration:
|
You can add query parameters to the webhook URL.
|
||||||
|
Those parameters will be available as environment variables into the shell
|
||||||
|
script.
|
||||||
|
You can also send a payload (text/plain or application/json) as request body.
|
||||||
|
This payload will be transmit to the shell script as first parameter.
|
||||||
|
|
||||||
|
*Example:*
|
||||||
|
|
||||||
|
The script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Environment parameters: foo=$foo"
|
||||||
|
echo "Script parameters: $1"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --data @test.json http://localhost/echo?foo=bar
|
||||||
|
data: Hook work request "echo" queued...
|
||||||
|
|
||||||
|
data: Running echo script...
|
||||||
|
|
||||||
|
data: Environment parameters: foo=bar
|
||||||
|
|
||||||
|
data: Script parameters: {"foo": "bar"}
|
||||||
|
|
||||||
|
data: done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
The script's output is collected and stored into a log file (configured by the
|
||||||
|
`APP_WORKING_DIR` environment variable).
|
||||||
|
|
||||||
|
Once the script executed, you can send the result and this log file to a
|
||||||
|
notification channel. Currently only two channels are supported: Email and HTTP.
|
||||||
|
|
||||||
|
#### HTTP notification
|
||||||
|
|
||||||
|
HTTP notification configuration:
|
||||||
|
|
||||||
- **APP_NOTIFIER**=http
|
- **APP_NOTIFIER**=http
|
||||||
- **APP_NOTIFIER_FROM**=webhookd <noreply@nunux.org>
|
- **APP_NOTIFIER_FROM**=webhookd <noreply@nunux.org>
|
||||||
- **APP_NOTIFIER_TO**=hostmaster@nunux.org
|
- **APP_NOTIFIER_TO**=hostmaster@nunux.org
|
||||||
- **APP_HTTP_NOTIFIER_URL**=http://requestb.in/v9b229v9
|
- **APP_HTTP_NOTIFIER_URL**=http://requestb.in/v9b229v9
|
||||||
|
|
||||||
> Note that the HTTP notification is compatible with [Mailgun](https://mailgun.com) API.
|
> Note that the HTTP notification is compatible with
|
||||||
|
[Mailgun](https://mailgun.com) API.
|
||||||
|
|
||||||
As the smtp notification:
|
#### Email notification
|
||||||
|
|
||||||
|
SMTP notification configuration:
|
||||||
|
|
||||||
- **APP_NOTIFIER**=smtp
|
- **APP_NOTIFIER**=smtp
|
||||||
- **APP_SMTP_NOTIFIER_HOST**=localhost:25
|
- **APP_SMTP_NOTIFIER_HOST**=localhost:25
|
||||||
|
|
||||||
|
The log file will be sent as an GZIP attachment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
{
|
|
||||||
"canon_url": "https://bitbucket.org",
|
|
||||||
"commits": [
|
|
||||||
{
|
|
||||||
"author": "marcus",
|
|
||||||
"branch": "master",
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"file": "somefile.py",
|
|
||||||
"type": "modified"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"message": "Added some more things to somefile.py\n",
|
|
||||||
"node": "620ade18607a",
|
|
||||||
"parents": [
|
|
||||||
"702c70160afc"
|
|
||||||
],
|
|
||||||
"raw_author": "Marcus Bertrand <marcus@somedomain.com>",
|
|
||||||
"raw_node": "620ade18607ac42d872b568bb92acaa9a28620e9",
|
|
||||||
"revision": null,
|
|
||||||
"size": -1,
|
|
||||||
"timestamp": "2012-05-30 05:58:56",
|
|
||||||
"utctimestamp": "2012-05-30 03:58:56+00:00"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"repository": {
|
|
||||||
"absolute_url": "/marcus/project-x/",
|
|
||||||
"fork": false,
|
|
||||||
"is_private": true,
|
|
||||||
"name": "Project X",
|
|
||||||
"owner": "marcus",
|
|
||||||
"scm": "git",
|
|
||||||
"slug": "project-x",
|
|
||||||
"website": "https://atlassian.com/"
|
|
||||||
},
|
|
||||||
"user": "marcus"
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
payload=%7B%22repository%22%3A+%7B%22website%22%3A+%22http%3A%2F%2Freader.nuunx.org%2F%22%2C+%22fork%22%3A+false%2C+%22name%22%3A+%22reader%22%2C+%22scm%22%3A+%22git%22%2C+%22owner%22%3A+%22ncarlier%22%2C+%22absolute_url%22%3A+%22%2Fncarlier%2Freader%2F%22%2C+%22slug%22%3A+%22reader%22%2C+%22is_private%22%3A+true%7D%2C+%22truncated%22%3A+false%2C+%22commits%22%3A+%5B%7B%22node%22%3A+%223f96fd0bfec5%22%2C+%22files%22%3A+%5B%7B%22type%22%3A+%22modified%22%2C+%22file%22%3A+%22.gitignore%22%7D%2C+%7B%22type%22%3A+%22added%22%2C+%22file%22%3A+%22etc%2Fenv_sample.conf%22%7D%2C+%7B%22type%22%3A+%22removed%22%2C+%22file%22%3A+%22etc%2Freader_sample.conf%22%7D%5D%2C+%22raw_author%22%3A+%22Nicolas+Carlier+%3Cn.carlier%40nunux.org%3E%22%2C+%22utctimestamp%22%3A+%222014-09-25+09%3A59%3A27%2B00%3A00%22%2C+%22author%22%3A+%22ncarlier%22%2C+%22timestamp%22%3A+%222014-09-25+11%3A59%3A27%22%2C+%22raw_node%22%3A+%223f96fd0bfec585820a481137860450c620b5e4c0%22%2C+%22parents%22%3A+%5B%2261215ed61077%22%5D%2C+%22branch%22%3A+%22master%22%2C+%22message%22%3A+%22chore%3A+Rename+env+configuration+file.%5Cn%22%2C+%22revision%22%3A+null%2C+%22size%22%3A+-1%7D%5D%2C+%22canon_url%22%3A+%22https%3A%2F%2Fbitbucket.org%22%2C+%22user%22%3A+%22ncarlier%22%7D
|
|
|
@ -1,28 +0,0 @@
|
||||||
{
|
|
||||||
"push_data":{
|
|
||||||
"pushed_at":1385141110,
|
|
||||||
"images":[
|
|
||||||
"imagehash1",
|
|
||||||
"imagehash2",
|
|
||||||
"imagehash3"
|
|
||||||
],
|
|
||||||
"pusher":"username"
|
|
||||||
},
|
|
||||||
"repository":{
|
|
||||||
"status":"Active",
|
|
||||||
"description":"my docker repo that does cool things",
|
|
||||||
"is_trusted":false,
|
|
||||||
"full_description":"This is my full description",
|
|
||||||
"repo_url":"https://registry.hub.docker.com/u/username/reponame/",
|
|
||||||
"owner":"username",
|
|
||||||
"is_official":false,
|
|
||||||
"is_private":false,
|
|
||||||
"name":"reponame",
|
|
||||||
"namespace":"username",
|
|
||||||
"star_count":1,
|
|
||||||
"comment_count":1,
|
|
||||||
"date_created":1370174400,
|
|
||||||
"dockerfile":"my full dockerfile is listed here",
|
|
||||||
"repo_name":"username/reponame"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
{
|
|
||||||
"ref": "refs/heads/gh-pages",
|
|
||||||
"after": "4d2ab4e76d0d405d17d1a0f2b8a6071394e3ab40",
|
|
||||||
"before": "993b46bdfc03ae59434816829162829e67c4d490",
|
|
||||||
"created": false,
|
|
||||||
"deleted": false,
|
|
||||||
"forced": false,
|
|
||||||
"compare": "https://github.com/baxterthehacker/public-repo/compare/993b46bdfc03...4d2ab4e76d0d",
|
|
||||||
"commits": [
|
|
||||||
{
|
|
||||||
"id": "4d2ab4e76d0d405d17d1a0f2b8a6071394e3ab40",
|
|
||||||
"distinct": true,
|
|
||||||
"message": "Trigger pages build",
|
|
||||||
"timestamp": "2014-07-25T12:37:40-04:00",
|
|
||||||
"url": "https://github.com/baxterthehacker/public-repo/commit/4d2ab4e76d0d405d17d1a0f2b8a6071394e3ab40",
|
|
||||||
"author": {
|
|
||||||
"name": "Kyle Daigle",
|
|
||||||
"email": "kyle.daigle@github.com",
|
|
||||||
"username": "kdaigle"
|
|
||||||
},
|
|
||||||
"committer": {
|
|
||||||
"name": "Kyle Daigle",
|
|
||||||
"email": "kyle.daigle@github.com",
|
|
||||||
"username": "kdaigle"
|
|
||||||
},
|
|
||||||
"added": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"removed": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"modified": [
|
|
||||||
"index.html"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"head_commit": {
|
|
||||||
"id": "4d2ab4e76d0d405d17d1a0f2b8a6071394e3ab40",
|
|
||||||
"distinct": true,
|
|
||||||
"message": "Trigger pages build",
|
|
||||||
"timestamp": "2014-07-25T12:37:40-04:00",
|
|
||||||
"url": "https://github.com/baxterthehacker/public-repo/commit/4d2ab4e76d0d405d17d1a0f2b8a6071394e3ab40",
|
|
||||||
"author": {
|
|
||||||
"name": "Kyle Daigle",
|
|
||||||
"email": "kyle.daigle@github.com",
|
|
||||||
"username": "kdaigle"
|
|
||||||
},
|
|
||||||
"committer": {
|
|
||||||
"name": "Kyle Daigle",
|
|
||||||
"email": "kyle.daigle@github.com",
|
|
||||||
"username": "kdaigle"
|
|
||||||
},
|
|
||||||
"added": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"removed": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"modified": [
|
|
||||||
"index.html"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"id": 20000106,
|
|
||||||
"name": "public-repo",
|
|
||||||
"full_name": "baxterthehacker/public-repo",
|
|
||||||
"owner": {
|
|
||||||
"name": "baxterthehacker",
|
|
||||||
"email": "baxterthehacker@users.noreply.github.com"
|
|
||||||
},
|
|
||||||
"private": false,
|
|
||||||
"html_url": "https://github.com/baxterthehacker/public-repo",
|
|
||||||
"description": "",
|
|
||||||
"fork": false,
|
|
||||||
"url": "https://github.com/baxterthehacker/public-repo",
|
|
||||||
"forks_url": "https://api.github.com/repos/baxterthehacker/public-repo/forks",
|
|
||||||
"keys_url": "https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}",
|
|
||||||
"collaborators_url": "https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}",
|
|
||||||
"teams_url": "https://api.github.com/repos/baxterthehacker/public-repo/teams",
|
|
||||||
"hooks_url": "https://api.github.com/repos/baxterthehacker/public-repo/hooks",
|
|
||||||
"issue_events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}",
|
|
||||||
"events_url": "https://api.github.com/repos/baxterthehacker/public-repo/events",
|
|
||||||
"assignees_url": "https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}",
|
|
||||||
"branches_url": "https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}",
|
|
||||||
"tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/tags",
|
|
||||||
"blobs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}",
|
|
||||||
"git_tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}",
|
|
||||||
"git_refs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}",
|
|
||||||
"trees_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}",
|
|
||||||
"statuses_url": "https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}",
|
|
||||||
"languages_url": "https://api.github.com/repos/baxterthehacker/public-repo/languages",
|
|
||||||
"stargazers_url": "https://api.github.com/repos/baxterthehacker/public-repo/stargazers",
|
|
||||||
"contributors_url": "https://api.github.com/repos/baxterthehacker/public-repo/contributors",
|
|
||||||
"subscribers_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscribers",
|
|
||||||
"subscription_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscription",
|
|
||||||
"commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}",
|
|
||||||
"git_commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}",
|
|
||||||
"comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}",
|
|
||||||
"issue_comment_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/comments/{number}",
|
|
||||||
"contents_url": "https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}",
|
|
||||||
"compare_url": "https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}",
|
|
||||||
"merges_url": "https://api.github.com/repos/baxterthehacker/public-repo/merges",
|
|
||||||
"archive_url": "https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}",
|
|
||||||
"downloads_url": "https://api.github.com/repos/baxterthehacker/public-repo/downloads",
|
|
||||||
"issues_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}",
|
|
||||||
"pulls_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}",
|
|
||||||
"milestones_url": "https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}",
|
|
||||||
"notifications_url": "https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}",
|
|
||||||
"labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}",
|
|
||||||
"releases_url": "https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}",
|
|
||||||
"created_at": 1400625583,
|
|
||||||
"updated_at": "2014-07-01T17:21:25Z",
|
|
||||||
"pushed_at": 1406306262,
|
|
||||||
"git_url": "git://github.com/baxterthehacker/public-repo.git",
|
|
||||||
"ssh_url": "git@github.com:baxterthehacker/public-repo.git",
|
|
||||||
"clone_url": "https://github.com/baxterthehacker/public-repo.git",
|
|
||||||
"svn_url": "https://github.com/baxterthehacker/public-repo",
|
|
||||||
"homepage": null,
|
|
||||||
"size": 612,
|
|
||||||
"stargazers_count": 0,
|
|
||||||
"watchers_count": 0,
|
|
||||||
"language": null,
|
|
||||||
"has_issues": true,
|
|
||||||
"has_downloads": true,
|
|
||||||
"has_wiki": true,
|
|
||||||
"forks_count": 0,
|
|
||||||
"mirror_url": null,
|
|
||||||
"open_issues_count": 25,
|
|
||||||
"forks": 0,
|
|
||||||
"open_issues": 25,
|
|
||||||
"watchers": 0,
|
|
||||||
"default_branch": "master",
|
|
||||||
"stargazers": 0,
|
|
||||||
"master_branch": "master"
|
|
||||||
},
|
|
||||||
"pusher": {
|
|
||||||
"name": "baxterthehacker",
|
|
||||||
"email": "baxterthehacker@users.noreply.github.com"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
{
|
|
||||||
"object_kind": "push",
|
|
||||||
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
|
|
||||||
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
|
||||||
"ref": "refs/heads/master",
|
|
||||||
"user_id": 4,
|
|
||||||
"user_name": "John Smith",
|
|
||||||
"user_email": "john@example.com",
|
|
||||||
"project_id": 15,
|
|
||||||
"repository": {
|
|
||||||
"name": "Diaspora",
|
|
||||||
"url": "git@example.com:mike/diasporadiaspora.git",
|
|
||||||
"description": "",
|
|
||||||
"homepage": "http://example.com/mike/diaspora",
|
|
||||||
"git_http_url":"http://example.com/mike/diaspora.git",
|
|
||||||
"git_ssh_url":"git@example.com:mike/diaspora.git",
|
|
||||||
"visibility_level":0
|
|
||||||
},
|
|
||||||
"commits": [
|
|
||||||
{
|
|
||||||
"id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
|
|
||||||
"message": "Update Catalan translation to e38cb41.",
|
|
||||||
"timestamp": "2011-12-12T14:27:31+02:00",
|
|
||||||
"url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
|
|
||||||
"author": {
|
|
||||||
"name": "Jordi Mallach",
|
|
||||||
"email": "jordi@softcatala.org"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
|
||||||
"message": "fixed readme",
|
|
||||||
"timestamp": "2012-01-03T23:36:29+02:00",
|
|
||||||
"url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
|
||||||
"author": {
|
|
||||||
"name": "GitLab dev user",
|
|
||||||
"email": "gitlabdev@dv6700.(none)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"total_commits_count": 4
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Webkookd Server
|
|
||||||
After=docker.service
|
|
||||||
Requires=docker.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
ExecStartPre=-/usr/bin/docker pull ncarlier/webhookd:latest
|
|
||||||
ExecStartPre=-/usr/bin/docker kill %p
|
|
||||||
ExecStartPre=-/usr/bin/docker rm %p
|
|
||||||
ExecStart=/usr/bin/docker run --rm --name %p \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
||||||
-v /media/data/webhookd/deploy_rsa:/root/.ssh/id_rsa \
|
|
||||||
--env-file /media/data/webhookd/env.conf \
|
|
||||||
-P ncarlier/webhookd
|
|
||||||
ExecStop=/usr/bin/docker stop %p
|
|
||||||
|
|
||||||
[X-Fleet]
|
|
||||||
X-Conflicts=%p@*.service
|
|
29
main.go
Normal file
29
main.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/api"
|
||||||
|
"github.com/ncarlier/webhookd/pkg/worker"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lAddr = flag.String("l", ":8080", "HTTP service address (e.g.address, ':8080')")
|
||||||
|
nbWorkers = flag.Int("n", 2, "The number of workers to start")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
log.Println("Starting webhookd server...")
|
||||||
|
|
||||||
|
// Start the dispatcher.
|
||||||
|
log.Printf("Starting the dispatcher (%d workers)...\n", *nbWorkers)
|
||||||
|
worker.StartDispatcher(*nbWorkers)
|
||||||
|
|
||||||
|
log.Printf("Starting the http server (%s)\n", *lAddr)
|
||||||
|
http.HandleFunc("/", api.WebhookHandler)
|
||||||
|
log.Fatal(http.ListenAndServe(*lAddr, nil))
|
||||||
|
}
|
1
makefiles
Submodule
1
makefiles
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit c249aaa5d479df146699dd164b206ad317d1e5be
|
78
pkg/api/api.go
Normal file
78
pkg/api/api.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/hook"
|
||||||
|
"github.com/ncarlier/webhookd/pkg/tools"
|
||||||
|
"github.com/ncarlier/webhookd/pkg/worker"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebhookHandler is the main handler of the API.
|
||||||
|
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
flusher, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get script location
|
||||||
|
p := strings.TrimPrefix(r.URL.Path, "/")
|
||||||
|
script, err := hook.ResolveScript(p)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading body: %v", err)
|
||||||
|
http.Error(w, "can't read body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := tools.QueryParamsToShellVars(r.URL.Query())
|
||||||
|
log.Printf("Calling hook script \"%s\" with params %s...\n", script, params)
|
||||||
|
|
||||||
|
// Create work
|
||||||
|
work := new(worker.WorkRequest)
|
||||||
|
work.Name = p
|
||||||
|
work.Script = script
|
||||||
|
work.Payload = string(body)
|
||||||
|
work.Args = params
|
||||||
|
work.MessageChan = make(chan []byte)
|
||||||
|
|
||||||
|
// Put work in queue
|
||||||
|
worker.WorkQueue <- *work
|
||||||
|
|
||||||
|
r.Header.Set("Content-Type", "text/event-stream")
|
||||||
|
r.Header.Set("Cache-Control", "no-cache")
|
||||||
|
r.Header.Set("Connection", "keep-alive")
|
||||||
|
r.Header.Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
log.Println("Work request queued:", script)
|
||||||
|
fmt.Fprintf(w, "data: Hook work request \"%s\" queued...\n\n", work.Name)
|
||||||
|
|
||||||
|
for {
|
||||||
|
msg, open := <-work.MessageChan
|
||||||
|
|
||||||
|
if !open {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", msg)
|
||||||
|
|
||||||
|
// Flush the data immediatly instead of buffering it for later.
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
28
pkg/hook/script.go
Normal file
28
pkg/hook/script.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package hook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scriptsdir = os.Getenv("APP_SCRIPTS_DIR")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResolveScript is resolving the target script.
|
||||||
|
func ResolveScript(p string) (string, error) {
|
||||||
|
if scriptsdir == "" {
|
||||||
|
scriptsdir = "scripts"
|
||||||
|
}
|
||||||
|
|
||||||
|
script := path.Join(scriptsdir, fmt.Sprintf("%s.sh", p))
|
||||||
|
log.Println("Resolving script: ", script, "...")
|
||||||
|
if _, err := os.Stat(script); os.IsNotExist(err) {
|
||||||
|
return "", errors.New("Script not found: " + script)
|
||||||
|
}
|
||||||
|
|
||||||
|
return script, nil
|
||||||
|
}
|
|
@ -14,15 +14,16 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HttpNotifier struct {
|
// HTTPNotifier is able to send a notification to a HTTP endpoint.
|
||||||
|
type HTTPNotifier struct {
|
||||||
URL string
|
URL string
|
||||||
From string
|
From string
|
||||||
To string
|
To string
|
||||||
User []string
|
User []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHttpNotifier() *HttpNotifier {
|
func newHTTPNotifier() *HTTPNotifier {
|
||||||
notifier := new(HttpNotifier)
|
notifier := new(HTTPNotifier)
|
||||||
notifier.URL = os.Getenv("APP_HTTP_NOTIFIER_URL")
|
notifier.URL = os.Getenv("APP_HTTP_NOTIFIER_URL")
|
||||||
if notifier.URL == "" {
|
if notifier.URL == "" {
|
||||||
log.Println("Unable to create HTTP notifier. APP_HTTP_NOTIFIER_URL not set.")
|
log.Println("Unable to create HTTP notifier. APP_HTTP_NOTIFIER_URL not set.")
|
||||||
|
@ -43,7 +44,8 @@ func NewHttpNotifier() *HttpNotifier {
|
||||||
return notifier
|
return notifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *HttpNotifier) Notify(subject string, text string, attachfile string) {
|
// Notify send a notification to a HTTP endpoint.
|
||||||
|
func (n *HTTPNotifier) Notify(subject string, text string, attachfile string) {
|
||||||
log.Println("Sending notification '" + subject + "' to " + n.URL + " ...")
|
log.Println("Sending notification '" + subject + "' to " + n.URL + " ...")
|
||||||
data := make(url.Values)
|
data := make(url.Values)
|
||||||
data.Set("from", n.From)
|
data.Set("from", n.From)
|
27
pkg/notification/notifier_factory.go
Normal file
27
pkg/notification/notifier_factory.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Notifier is able to send a notification.
|
||||||
|
type Notifier interface {
|
||||||
|
Notify(subject string, text string, attachfile string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifierFactory creates a notifier regarding the configuration.
|
||||||
|
func NotifierFactory() (Notifier, error) {
|
||||||
|
notifier := os.Getenv("APP_NOTIFIER")
|
||||||
|
switch notifier {
|
||||||
|
case "http":
|
||||||
|
return newHTTPNotifier(), nil
|
||||||
|
case "smtp":
|
||||||
|
return newSMTPNotifier(), nil
|
||||||
|
default:
|
||||||
|
if notifier == "" {
|
||||||
|
return nil, errors.New("notification provider not configured")
|
||||||
|
}
|
||||||
|
return nil, errors.New("unknown notification provider: " + notifier)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,14 +7,15 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SmtpNotifier struct {
|
// SMTPNotifier is able to send notifcation to a email destination.
|
||||||
|
type SMTPNotifier struct {
|
||||||
Host string
|
Host string
|
||||||
From string
|
From string
|
||||||
To string
|
To string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSmtpNotifier() *SmtpNotifier {
|
func newSMTPNotifier() *SMTPNotifier {
|
||||||
notifier := new(SmtpNotifier)
|
notifier := new(SMTPNotifier)
|
||||||
notifier.Host = os.Getenv("APP_SMTP_NOTIFIER_HOST")
|
notifier.Host = os.Getenv("APP_SMTP_NOTIFIER_HOST")
|
||||||
if notifier.Host == "" {
|
if notifier.Host == "" {
|
||||||
notifier.Host = "localhost:25"
|
notifier.Host = "localhost:25"
|
||||||
|
@ -30,7 +31,8 @@ func NewSmtpNotifier() *SmtpNotifier {
|
||||||
return notifier
|
return notifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *SmtpNotifier) Notify(subject string, text string, attachfile string) {
|
// Notify send a notification to a email destination.
|
||||||
|
func (n *SMTPNotifier) Notify(subject string, text string, attachfile string) {
|
||||||
log.Println("SMTP notification: ", subject)
|
log.Println("SMTP notification: ", subject)
|
||||||
// Connect to the remote SMTP server.
|
// Connect to the remote SMTP server.
|
||||||
c, err := smtp.Dial(n.Host)
|
c, err := smtp.Dial(n.Host)
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CompressFile is a simple file gzipper.
|
||||||
func CompressFile(filename string) (zipfile string, err error) {
|
func CompressFile(filename string) (zipfile string, err error) {
|
||||||
zipfile = fmt.Sprintf("%s.gz", filename)
|
zipfile = fmt.Sprintf("%s.gz", filename)
|
||||||
in, err := os.Open(filename)
|
in, err := os.Open(filename)
|
31
pkg/tools/query.go
Normal file
31
pkg/tools/query.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
|
||||||
|
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||||
|
|
||||||
|
// ToSnakeCase convert string to snakecase.
|
||||||
|
func ToSnakeCase(str string) string {
|
||||||
|
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
|
||||||
|
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
|
||||||
|
return strings.ToLower(snake)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryParamsToShellVars convert URL query parameters to shell vars.
|
||||||
|
func QueryParamsToShellVars(q url.Values) []string {
|
||||||
|
var params []string
|
||||||
|
for k, v := range q {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(ToSnakeCase(k))
|
||||||
|
buf.WriteString("=")
|
||||||
|
buf.WriteString(url.QueryEscape(strings.Join(v[:], ",")))
|
||||||
|
params = append(params, buf.String())
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
5
pkg/version/version.go
Normal file
5
pkg/version/version.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package version
|
||||||
|
|
||||||
|
var (
|
||||||
|
App string = "snapshot"
|
||||||
|
)
|
|
@ -1,19 +1,18 @@
|
||||||
package worker
|
package worker
|
||||||
|
|
||||||
import (
|
import "log"
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var WorkerQueue chan chan WorkRequest
|
var WorkerQueue chan chan WorkRequest
|
||||||
var WorkQueue = make(chan WorkRequest, 100)
|
var WorkQueue = make(chan WorkRequest, 100)
|
||||||
|
|
||||||
|
// StartDispatcher is charged to start n workers.
|
||||||
func StartDispatcher(nworkers int) {
|
func StartDispatcher(nworkers int) {
|
||||||
// First, initialize the channel we are going to but the workers' work channels into.
|
// First, initialize the channel we are going to but the workers' work channels into.
|
||||||
WorkerQueue = make(chan chan WorkRequest, nworkers)
|
WorkerQueue = make(chan chan WorkRequest, nworkers)
|
||||||
|
|
||||||
// Now, create all of our workers.
|
// Now, create all of our workers.
|
||||||
for i := 0; i < nworkers; i++ {
|
for i := 0; i < nworkers; i++ {
|
||||||
fmt.Println("Starting worker", i+1)
|
log.Println("Starting worker", i+1)
|
||||||
worker := NewWorker(i+1, WorkerQueue)
|
worker := NewWorker(i+1, WorkerQueue)
|
||||||
worker.Start()
|
worker.Start()
|
||||||
}
|
}
|
||||||
|
@ -22,11 +21,11 @@ func StartDispatcher(nworkers int) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case work := <-WorkQueue:
|
case work := <-WorkQueue:
|
||||||
fmt.Println("Received work request")
|
log.Println("Received work request:", work.Name)
|
||||||
go func() {
|
go func() {
|
||||||
worker := <-WorkerQueue
|
worker := <-WorkerQueue
|
||||||
|
|
||||||
fmt.Println("Dispatching work request")
|
log.Println("Dispatching work request:", work.Name)
|
||||||
worker <- work
|
worker <- work
|
||||||
}()
|
}()
|
||||||
}
|
}
|
94
pkg/worker/script_runner.go
Normal file
94
pkg/worker/script_runner.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package worker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChanWriter is a simple writer to a channel of byte.
|
||||||
|
type ChanWriter struct {
|
||||||
|
ByteChan chan []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ChanWriter) Write(p []byte) (int, error) {
|
||||||
|
c.ByteChan <- p
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
workingdir = os.Getenv("APP_WORKING_DIR")
|
||||||
|
)
|
||||||
|
|
||||||
|
func runScript(work *WorkRequest) (string, error) {
|
||||||
|
if workingdir == "" {
|
||||||
|
workingdir = os.TempDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Starting script:", work.Script, "...")
|
||||||
|
binary, err := exec.LookPath(work.Script)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec script with args...
|
||||||
|
cmd := exec.Command(binary, work.Payload)
|
||||||
|
// with env variables...
|
||||||
|
cmd.Env = append(os.Environ(), work.Args...)
|
||||||
|
|
||||||
|
// Open the out file for writing
|
||||||
|
logFilename := path.Join(workingdir, fmt.Sprintf("%s_%s.txt", tools.ToSnakeCase(work.Name), time.Now().Format("20060102_1504")))
|
||||||
|
logFile, err := os.Create(logFilename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer logFile.Close()
|
||||||
|
log.Println("Writing output to file: ", logFilename, "...")
|
||||||
|
|
||||||
|
wLogFile := bufio.NewWriter(logFile)
|
||||||
|
|
||||||
|
r, w := io.Pipe()
|
||||||
|
cmd.Stdout = w
|
||||||
|
cmd.Stderr = w
|
||||||
|
|
||||||
|
// Start the script...
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return logFilename, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write script output to log file and the work message cahnnel.
|
||||||
|
go func(reader io.Reader) {
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
// writing to the work channel
|
||||||
|
line := scanner.Text()
|
||||||
|
work.MessageChan <- []byte(line)
|
||||||
|
// writing to outfile
|
||||||
|
if _, err := wLogFile.WriteString(line + "\n"); err != nil {
|
||||||
|
log.Println("Error while writing into the log file:", logFilename, err)
|
||||||
|
}
|
||||||
|
if err = wLogFile.Flush(); err != nil {
|
||||||
|
log.Println("Error while flushing the log file:", logFilename, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
log.Println("Error scanning the script stdout: ", logFilename, err)
|
||||||
|
}
|
||||||
|
}(r)
|
||||||
|
|
||||||
|
err = cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Starting script:", work.Script, "-> ERROR")
|
||||||
|
return logFilename, err
|
||||||
|
}
|
||||||
|
log.Println("Starting script:", work.Script, "-> OK")
|
||||||
|
return logFilename, nil
|
||||||
|
}
|
10
pkg/worker/work_request.go
Normal file
10
pkg/worker/work_request.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package worker
|
||||||
|
|
||||||
|
// WorkRequest is a request of work for a worker
|
||||||
|
type WorkRequest struct {
|
||||||
|
Name string
|
||||||
|
Script string
|
||||||
|
Payload string
|
||||||
|
Args []string
|
||||||
|
MessageChan chan []byte
|
||||||
|
}
|
|
@ -2,8 +2,10 @@ package worker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ncarlier/webhookd/notification"
|
"log"
|
||||||
"github.com/ncarlier/webhookd/tools"
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/notification"
|
||||||
|
"github.com/ncarlier/webhookd/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewWorker creates, and returns a new Worker object. Its only argument
|
// NewWorker creates, and returns a new Worker object. Its only argument
|
||||||
|
@ -20,6 +22,7 @@ func NewWorker(id int, workerQueue chan chan WorkRequest) Worker {
|
||||||
return worker
|
return worker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Worker is a go routine in charge of executing a work.
|
||||||
type Worker struct {
|
type Worker struct {
|
||||||
ID int
|
ID int
|
||||||
Work chan WorkRequest
|
Work chan WorkRequest
|
||||||
|
@ -27,8 +30,8 @@ type Worker struct {
|
||||||
QuitChan chan bool
|
QuitChan chan bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function "starts" the worker by starting a goroutine, that is
|
// Start is the function to starts the worker by starting a goroutine.
|
||||||
// an infinite "for-select" loop.
|
// That is an infinite "for-select" loop.
|
||||||
func (w Worker) Start() {
|
func (w Worker) Start() {
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
|
@ -38,18 +41,20 @@ func (w Worker) Start() {
|
||||||
select {
|
select {
|
||||||
case work := <-w.Work:
|
case work := <-w.Work:
|
||||||
// Receive a work request.
|
// Receive a work request.
|
||||||
fmt.Printf("worker%d: Received work request %s/%s\n", w.ID, work.Name, work.Action)
|
log.Printf("Worker%d received work request: %s\n", w.ID, work.Name)
|
||||||
filename, err := RunScript(&work)
|
filename, err := runScript(&work)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
subject := fmt.Sprintf("Webhook %s/%s FAILED.", work.Name, work.Action)
|
subject := fmt.Sprintf("Webhook %s FAILED.", work.Name)
|
||||||
Notify(subject, err.Error(), filename)
|
work.MessageChan <- []byte(fmt.Sprintf("error: %s", err.Error()))
|
||||||
|
notify(subject, err.Error(), filename)
|
||||||
} else {
|
} else {
|
||||||
subject := fmt.Sprintf("Webhook %s/%s SUCCEEDED.", work.Name, work.Action)
|
subject := fmt.Sprintf("Webhook %s SUCCEEDED.", work.Name)
|
||||||
Notify(subject, "See attachment.", filename)
|
work.MessageChan <- []byte("done")
|
||||||
|
notify(subject, "See attachment.", filename)
|
||||||
}
|
}
|
||||||
|
close(work.MessageChan)
|
||||||
case <-w.QuitChan:
|
case <-w.QuitChan:
|
||||||
// We have been asked to stop.
|
log.Printf("Stopping worker%d...\n", w.ID)
|
||||||
fmt.Printf("worker%d stopping\n", w.ID)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +62,6 @@ func (w Worker) Start() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop tells the worker to stop listening for work requests.
|
// Stop tells the worker to stop listening for work requests.
|
||||||
//
|
|
||||||
// Note that the worker will only stop *after* it has finished its work.
|
// Note that the worker will only stop *after* it has finished its work.
|
||||||
func (w Worker) Stop() {
|
func (w Worker) Stop() {
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -65,14 +69,14 @@ func (w Worker) Stop() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Notify(subject string, text string, outfilename string) {
|
func notify(subject string, text string, outfilename string) {
|
||||||
var notifier, err = notification.NotifierFactory()
|
var notifier, err = notification.NotifierFactory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
log.Println("Unable to get the notifier. Notification skipped:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if notifier == nil {
|
if notifier == nil {
|
||||||
fmt.Println("Notification provider not found.")
|
log.Println("Notification provider not found. Notification skipped.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
export GIT_URL=$1
|
|
||||||
export REF_NAME=$2
|
|
||||||
|
|
||||||
if [ -z "$GIT_URL" ]; then
|
|
||||||
echo "GIT_URL not defined"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$REF_NAME" ]; then
|
|
||||||
echo "REF_NAME not defined"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Building $REF_NAME ..."
|
|
||||||
|
|
||||||
# Check that we've a valid working directory.
|
|
||||||
if [ ! -d "$APP_WORKING_DIR" ]; then
|
|
||||||
echo "Error, APP_WORKING_DIR not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check that the deploy key is valid.
|
|
||||||
export DEPLOY_KEY=/root/.ssh/id_rsa
|
|
||||||
if [ ! -f "$DEPLOY_KEY" ]; then
|
|
||||||
echo "Error, DEPLOY_KEY not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove old repository if exist
|
|
||||||
rm -rf $APP_WORKING_DIR/$REF_NAME
|
|
||||||
|
|
||||||
# Clone repository
|
|
||||||
echo "Cloning $GIT_URL into ${APP_WORKING_DIR}/${REF_NAME} ..."
|
|
||||||
ssh-agent bash -c 'ssh-add ${DEPLOY_KEY}; git clone --depth 1 ${GIT_URL} ${APP_WORKING_DIR}/${REF_NAME}'
|
|
||||||
if [ $? != 0 ]; then
|
|
||||||
echo "Error, unable to clone repository"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build Docke image
|
|
||||||
echo "Building image ..."
|
|
||||||
make -C $APP_WORKING_DIR/$REF_NAME
|
|
||||||
if [ $? != 0 ]; then
|
|
||||||
echo "Error, unable to build Docker image"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Build complete!"
|
|
||||||
exit 0
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
echo "bitbucket echo: $@"
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
echo "docker echo: $@"
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
echo "github echo: $@"
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
echo "gitlab echo: $@"
|
|
||||||
|
|
18
scripts/test.sh
Executable file
18
scripts/test.sh
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Running test script..."
|
||||||
|
|
||||||
|
echo "Environment parameters:"
|
||||||
|
echo "firstname: $firstname"
|
||||||
|
echo "lastname: $lastname"
|
||||||
|
|
||||||
|
echo "Script parameters: $1"
|
||||||
|
|
||||||
|
for i in {1..5}; do
|
||||||
|
sleep .5
|
||||||
|
echo "running..."
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Expected error."
|
||||||
|
|
||||||
|
exit 1
|
|
@ -1,54 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/ncarlier/webhookd/hook"
|
|
||||||
"github.com/ncarlier/webhookd/worker"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createWebhookHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
params := mux.Vars(r)
|
|
||||||
hookname := params["hookname"]
|
|
||||||
action := params["action"]
|
|
||||||
|
|
||||||
// Get hook decoder
|
|
||||||
record, err := hook.RecordFactory(hookname)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err.Error())
|
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Using hook %s with action %s.\n", hookname, action)
|
|
||||||
|
|
||||||
// Decode request
|
|
||||||
err = record.Decode(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err.Error())
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create work
|
|
||||||
work := new(worker.WorkRequest)
|
|
||||||
work.Name = hookname
|
|
||||||
work.Action = action
|
|
||||||
fmt.Println("Extracted data: ", record.GetURL(), record.GetName())
|
|
||||||
work.Args = []string{record.GetURL(), record.GetName()}
|
|
||||||
|
|
||||||
//Put work in queue
|
|
||||||
worker.WorkQueue <- *work
|
|
||||||
fmt.Printf("Work request queued: %s/%s\n", hookname, action)
|
|
||||||
|
|
||||||
fmt.Fprintf(w, "Action %s of hook %s queued.", action, hookname)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Handlers() *mux.Router{
|
|
||||||
r := mux.NewRouter()
|
|
||||||
r.HandleFunc("/{hookname:[a-z]+}/{action:[a-z]+}", createWebhookHandler).Methods("POST")
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
package api_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ncarlier/webhookd/api"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
server *httptest.Server
|
|
||||||
reader io.Reader
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
server = httptest.NewServer(api.Handlers())
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertHook(t *testing.T, url string, json string, expectedStatus int) {
|
|
||||||
reader = strings.NewReader(json)
|
|
||||||
request, err := http.NewRequest("POST", url, reader)
|
|
||||||
res, err := http.DefaultClient.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if res.StatusCode != expectedStatus {
|
|
||||||
t.Errorf("Status expected: %d, Actual status: %d", expectedStatus, res.StatusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBadHook(t *testing.T) {
|
|
||||||
url := fmt.Sprintf("%s/bad/echo", server.URL)
|
|
||||||
json := `{"foo": "bar"}`
|
|
||||||
assertHook(t, url, json, 404)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func TestGitlabHook(t *testing.T) {
|
|
||||||
url := fmt.Sprintf("%s/gitlab/echo", server.URL)
|
|
||||||
|
|
||||||
json := `{
|
|
||||||
"object_kind": "push",
|
|
||||||
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
|
|
||||||
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
|
||||||
"ref": "refs/heads/master",
|
|
||||||
"user_email": "john@example.com",
|
|
||||||
"project_id": 15,
|
|
||||||
"repository": {
|
|
||||||
"name": "Diaspora",
|
|
||||||
"url": "git@example.com:mike/diasporadiaspora.git",
|
|
||||||
"description": "",
|
|
||||||
"git_http_url":"http://example.com/mike/diaspora.git",
|
|
||||||
"git_ssh_url":"git@example.com:mike/diaspora.git"
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
assertHook(t, url, json, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGithubHook(t *testing.T) {
|
|
||||||
url := fmt.Sprintf("%s/github/echo", server.URL)
|
|
||||||
|
|
||||||
json := `{
|
|
||||||
"repository": {
|
|
||||||
"id": 20000106,
|
|
||||||
"name": "public-repo",
|
|
||||||
"full_name": "baxterthehacker/public-repo",
|
|
||||||
"html_url": "https://github.com/baxterthehacker/public-repo",
|
|
||||||
"description": "",
|
|
||||||
"url": "https://github.com/baxterthehacker/public-repo",
|
|
||||||
"git_url": "git://github.com/baxterthehacker/public-repo.git",
|
|
||||||
"ssh_url": "git@github.com:baxterthehacker/public-repo.git",
|
|
||||||
"homepage": null
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
assertHook(t, url, json, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDockerHook(t *testing.T) {
|
|
||||||
url := fmt.Sprintf("%s/docker/echo", server.URL)
|
|
||||||
|
|
||||||
json := `{
|
|
||||||
"repository":{
|
|
||||||
"status":"Active",
|
|
||||||
"description":"my docker repo that does cool things",
|
|
||||||
"full_description":"This is my full description",
|
|
||||||
"repo_url":"https://registry.hub.docker.com/u/username/reponame/",
|
|
||||||
"owner":"username",
|
|
||||||
"name":"reponame",
|
|
||||||
"namespace":"username",
|
|
||||||
"repo_name":"username/reponame"
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
assertHook(t, url, json, 200)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
package hook
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BitbucketRecord struct {
|
|
||||||
Repository struct {
|
|
||||||
Slug string `json:"slug"`
|
|
||||||
Owner string `json:"owner"`
|
|
||||||
} `json:"repository"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *BitbucketRecord) GetURL() string {
|
|
||||||
return fmt.Sprintf("git@bitbucket.org:%s/%s.git", r.Repository.Owner, r.Repository.Slug)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *BitbucketRecord) GetName() string {
|
|
||||||
return r.Repository.Slug
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *BitbucketRecord) Decode(req *http.Request) error {
|
|
||||||
if err := req.ParseForm(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload, ok := req.PostForm["payload"]; ok {
|
|
||||||
err := json.Unmarshal([]byte(payload[0]), &r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
decoder := json.NewDecoder(req.Body)
|
|
||||||
err := decoder.Decode(&r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package hook
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DockerRecord struct {
|
|
||||||
Repository struct {
|
|
||||||
Name string `json:"repo_name"`
|
|
||||||
URL string `json:"repo_url"`
|
|
||||||
} `json:"repository"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DockerRecord) GetURL() string {
|
|
||||||
return r.Repository.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DockerRecord) GetName() string {
|
|
||||||
return r.Repository.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DockerRecord) Decode(req *http.Request) error {
|
|
||||||
decoder := json.NewDecoder(req.Body)
|
|
||||||
err := decoder.Decode(&r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package hook
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GithubRecord struct {
|
|
||||||
Repository struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
URL string `json:"git_url"`
|
|
||||||
} `json:"repository"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *GithubRecord) GetURL() string {
|
|
||||||
return r.Repository.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *GithubRecord) GetName() string {
|
|
||||||
return r.Repository.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *GithubRecord) Decode(req *http.Request) error {
|
|
||||||
decoder := json.NewDecoder(req.Body)
|
|
||||||
err := decoder.Decode(&r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package hook
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GitlabRecord struct {
|
|
||||||
Repository struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
URL string `json:"git_ssh_url"`
|
|
||||||
} `json:"repository"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *GitlabRecord) GetURL() string {
|
|
||||||
return r.Repository.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *GitlabRecord) GetName() string {
|
|
||||||
return r.Repository.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *GitlabRecord) Decode(req *http.Request) error {
|
|
||||||
decoder := json.NewDecoder(req.Body)
|
|
||||||
err := decoder.Decode(&r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package hook
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Record interface {
|
|
||||||
GetURL() string
|
|
||||||
GetName() string
|
|
||||||
Decode(r *http.Request) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func RecordFactory(hookname string) (Record, error) {
|
|
||||||
switch hookname {
|
|
||||||
case "bitbucket":
|
|
||||||
return new(BitbucketRecord), nil
|
|
||||||
case "github":
|
|
||||||
return new(GithubRecord), nil
|
|
||||||
case "gitlab":
|
|
||||||
return new(GitlabRecord), nil
|
|
||||||
case "docker":
|
|
||||||
return new(DockerRecord), nil
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Unknown hookname: " + hookname)
|
|
||||||
}
|
|
||||||
}
|
|
27
src/main.go
27
src/main.go
|
@ -1,27 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"github.com/ncarlier/webhookd/api"
|
|
||||||
"github.com/ncarlier/webhookd/worker"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
LAddr = flag.String("l", ":8080", "HTTP service address (e.g.address, ':8080')")
|
|
||||||
NWorkers = flag.Int("n", 2, "The number of workers to start")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
log.Println("Starting webhookd server...")
|
|
||||||
|
|
||||||
// Start the dispatcher.
|
|
||||||
log.Println("Starting the dispatcher")
|
|
||||||
worker.StartDispatcher(*NWorkers)
|
|
||||||
|
|
||||||
log.Println("Starting the http server")
|
|
||||||
log.Fatal(http.ListenAndServe(*LAddr, api.Handlers()))
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
package notification
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Notifier interface {
|
|
||||||
Notify(subject string, text string, attachfile string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NotifierFactory() (Notifier, error) {
|
|
||||||
notifier := os.Getenv("APP_NOTIFIER")
|
|
||||||
switch notifier {
|
|
||||||
case "http":
|
|
||||||
return NewHttpNotifier(), nil
|
|
||||||
case "smtp":
|
|
||||||
return NewSmtpNotifier(), nil
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Unknown notification provider: " + notifier)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
package worker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
workingdir = os.Getenv("APP_WORKING_DIR")
|
|
||||||
scriptsdir = os.Getenv("APP_SCRIPTS_DIR")
|
|
||||||
scriptsdebug = os.Getenv("APP_SCRIPTS_DEBUG")
|
|
||||||
)
|
|
||||||
|
|
||||||
func RunScript(work *WorkRequest) (string, error) {
|
|
||||||
if workingdir == "" {
|
|
||||||
workingdir = os.TempDir()
|
|
||||||
}
|
|
||||||
if scriptsdir == "" {
|
|
||||||
scriptsdir = "scripts"
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptname := path.Join(scriptsdir, work.Name, fmt.Sprintf("%s.sh", work.Action))
|
|
||||||
fmt.Println("Exec script: ", scriptname, "...")
|
|
||||||
|
|
||||||
// Exec script...
|
|
||||||
cmd := exec.Command(scriptname, work.Args...)
|
|
||||||
|
|
||||||
// Open the out file for writing
|
|
||||||
outfilename := path.Join(workingdir, fmt.Sprintf("%s-%s.txt", work.Name, work.Action))
|
|
||||||
outfile, err := os.Create(outfilename)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer outfile.Close()
|
|
||||||
if scriptsdebug == "true" {
|
|
||||||
fmt.Println("Logging in console: ", scriptsdebug)
|
|
||||||
cmd.Stdout = io.MultiWriter(os.Stdout, outfile)
|
|
||||||
cmd.Stderr = io.MultiWriter(os.Stderr, outfile)
|
|
||||||
} else {
|
|
||||||
cmd.Stdout = outfile
|
|
||||||
cmd.Stderr = outfile
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
return outfilename, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cmd.Wait()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Exec script: ", scriptname, "KO!")
|
|
||||||
return outfilename, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Exec script: ", scriptname, "OK")
|
|
||||||
return outfilename, nil
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package worker
|
|
||||||
|
|
||||||
type WorkRequest struct {
|
|
||||||
Name string
|
|
||||||
Action string
|
|
||||||
Args []string
|
|
||||||
}
|
|
36
test.sh
36
test.sh
|
@ -1,36 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
IP=`sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}' webhookd`
|
|
||||||
PORT=${1:-8080}
|
|
||||||
|
|
||||||
echo "Test URL: http://$IP:$PORT"
|
|
||||||
echo "Test bad URL"
|
|
||||||
curl -H "Content-Type: application/json" \
|
|
||||||
--data @assets/bitbucket.json \
|
|
||||||
http://$IP:$PORT/bad/action
|
|
||||||
|
|
||||||
echo "Test Bitbucket hook"
|
|
||||||
curl -H "Content-Type: application/json" \
|
|
||||||
--data @assets/bitbucket.json \
|
|
||||||
http://$IP:$PORT/bitbucket/echo
|
|
||||||
|
|
||||||
echo "Test Bitbucket hook"
|
|
||||||
curl -H "Content-Type: application/x-www-form-urlencoded" \
|
|
||||||
--data @assets/bitbucket.raw \
|
|
||||||
http://$IP:$PORT/bitbucket/echo
|
|
||||||
|
|
||||||
echo "Test Github hook"
|
|
||||||
curl -H "Content-Type: application/json" \
|
|
||||||
--data @assets/github.json \
|
|
||||||
http://$IP:$PORT/github/echo
|
|
||||||
|
|
||||||
echo "Test Gitlab hook"
|
|
||||||
curl -H "Content-Type: application/json" \
|
|
||||||
--data @assets/gitlab.json \
|
|
||||||
http://$IP:$PORT/gitlab/echo
|
|
||||||
|
|
||||||
echo "Test Docker hook"
|
|
||||||
curl -H "Content-Type: application/json" \
|
|
||||||
--data @assets/docker.json \
|
|
||||||
http://$IP:$PORT/docker/echo
|
|
||||||
|
|
3
tests/test.json
Normal file
3
tests/test.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
14
tests/test.sh
Executable file
14
tests/test.sh
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
URL=http://localhost:8080
|
||||||
|
|
||||||
|
echo "Test URL: $URL"
|
||||||
|
echo "Test bad URL"
|
||||||
|
curl -H "Content-Type: application/json" \
|
||||||
|
--data @test.json \
|
||||||
|
$URL/bad/action
|
||||||
|
|
||||||
|
echo "Test hook"
|
||||||
|
curl -H "Content-Type: application/json" \
|
||||||
|
--data @test.json \
|
||||||
|
$URL/test?firstname=obi-wan\&lastname=kenobi
|
Loading…
Reference in New Issue
Block a user