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:
Nicolas Carlier 2018-01-02 16:11:59 +00:00
parent 059f91bd17
commit 14c214efdf
47 changed files with 559 additions and 976 deletions

View File

@ -1 +1 @@
dist/ release/

View File

@ -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
View File

@ -1,3 +1,2 @@
dist/ release/
ssh .vscode/
etc/env.conf

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "makefiles"]
path = makefiles
url = https://github.com/ncarlier/makefiles.git

View File

@ -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
View File

@ -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
View File

@ -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.
---

View File

@ -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"
}

View File

@ -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

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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
}

View File

@ -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
View 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

@ -0,0 +1 @@
Subproject commit c249aaa5d479df146699dd164b206ad317d1e5be

78
pkg/api/api.go Normal file
View 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
View 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
}

View File

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

View 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)
}
}

View File

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

View File

@ -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
View 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
View File

@ -0,0 +1,5 @@
package version
var (
App string = "snapshot"
)

View File

@ -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
}() }()
} }

View 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
}

View 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
}

View File

@ -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
} }

View File

@ -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

View File

@ -1,4 +0,0 @@
#!/bin/sh
echo "bitbucket echo: $@"

View File

@ -1,4 +0,0 @@
#!/bin/sh
echo "docker echo: $@"

View File

@ -1,4 +0,0 @@
#!/bin/sh
echo "github echo: $@"

View File

@ -1,4 +0,0 @@
#!/bin/sh
echo "gitlab echo: $@"

18
scripts/test.sh Executable file
View 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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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()))
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -1,7 +0,0 @@
package worker
type WorkRequest struct {
Name string
Action string
Args []string
}

36
test.sh
View File

@ -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
View File

@ -0,0 +1,3 @@
{
"foo": "bar"
}

14
tests/test.sh Executable file
View 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