diff --git a/.dockerignore b/.dockerignore index 849ddff..151080d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1 @@ -dist/ +release/ diff --git a/etc/env_sample.conf b/.env similarity index 89% rename from etc/env_sample.conf rename to .env index 7cb2b85..dd7ba71 100644 --- a/etc/env_sample.conf +++ b/.env @@ -10,11 +10,6 @@ APP_WORKING_DIR=/var/opt/webhookd/work # Defaults: ./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. # Notify script execution result and logs. # Values: diff --git a/.gitignore b/.gitignore index f911392..c9a0a10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -dist/ -ssh -etc/env.conf +release/ +.vscode/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f3ad6ac --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "makefiles"] + path = makefiles + url = https://github.com/ncarlier/makefiles.git diff --git a/Dockerfile b/Dockerfile index f4defca..b00a37f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,42 +1,39 @@ -# webhookd image. -# -# VERSION 0.0.1 -# -# BUILD-USING: docker build --rm -t ncarlier/webhookd . +######################################### +# Build stage +######################################### +FROM golang:1.8 AS builder +MAINTAINER Nicolas Carlier -FROM golang:1.3 +# Repository location +ARG REPOSITORY=github.com/ncarlier +# Artifact name +ARG ARTIFACT=webhookd -# Install ssh-keygen -RUN apt-get update && apt-get install -y ssh sudo +# Copy sources into the container +ADD . /go/src/$REPOSITORY/$ARTIFACT -# Install the latest version of the docker CLI -RUN curl -L -o /usr/local/bin/docker https://get.docker.io/builds/Linux/x86_64/docker-latest && \ - chmod +x /usr/local/bin/docker +# Set working directory +WORKDIR /go/src/$REPOSITORY/$ARTIFACT -# Install GO application -WORKDIR /go/src/github.com/ncarlier/webhookd -ADD ./src /go/src/github.com/ncarlier/webhookd -RUN go get github.com/ncarlier/webhookd +# Build the binary +RUN make -# Add scripts -ADD ./scripts /var/opt/webhookd/scripts +######################################### +# Distribution stage +######################################### +FROM docker:dind +MAINTAINER Nicolas Carlier -# Create work and ssh directories -RUN mkdir /var/opt/webhookd/work +# Repository location +ARG REPOSITORY=github.com/ncarlier -# Generate SSH deploiment key (should be overwrite by a volume) -RUN ssh-keygen -N "" -f /root/.ssh/id_rsa +# Artifact name +ARG ARTIFACT=webhookd -# Ignor strict host key checking -RUN echo "Host github.com\n\tStrictHostKeyChecking no\n" >> /root/.ssh/config && \ - echo "Host bitbucket.org\n\tStrictHostKeyChecking no\n" >> /root/.ssh/config +# Fix lib dep +RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 -# Change workdir -WORKDIR /var/opt/webhookd +# Install binary +COPY --from=builder /go/src/$REPOSITORY/$ARTIFACT/release/$ARTIFACT-linux-amd64 /usr/local/bin/$ARTIFACT -# Port -EXPOSE 8080 - -CMD [] -ENTRYPOINT ["/go/bin/webhookd"] diff --git a/Makefile b/Makefile index 8adf42f..42dd384 100644 --- a/Makefile +++ b/Makefile @@ -1,79 +1,69 @@ .SILENT : -.PHONY : volume mount build clean start stop rm shell test dist -USERNAME:=ncarlier -APPNAME:=webhookd -IMAGE:=$(USERNAME)/$(APPNAME) +# Author +AUTHOR=github.com/ncarlier -TAG:=`git describe --abbrev=0 --tags` -LDFLAGS:=-X main.buildVersion $(TAG) -ROOTPKG:=github.com/$(USERNAME) -PKGDIR:=$(GOPATH)/src/$(ROOTPKG) +# App name +APPNAME=webhookd -define docker_run_flags ---rm \ --v /var/run/docker.sock:/var/run/docker.sock \ ---env-file $(PWD)/etc/env.conf \ --P \ --i -t -endef +# Go configuration +GOOS?=linux +GOARCH?=amd64 + +# Add exe extension if windows target +is_windows:=$(filter windows,$(GOOS)) +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 -volume: - echo "Building $(APPNAME) volumes..." - sudo docker run \ - -v $(PWD)/src:/go/src/$(ROOTPKG)/$(APPNAME) \ - -v $(PWD)/scripts:/var/opt/$(APPNAME)/scripts \ - --name $(APPNAME)_volumes busybox true +# Include common Make tasks +root_dir:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +makefiles:=$(root_dir)/makefiles +include $(makefiles)/help.Makefile -key: - $(eval docker_run_flags += -v $(PWD)/ssh:/root/.ssh) - echo "Add private deploy key" +$(APPBASE)/$(APPNAME): + echo "Creating GO src link: $(APPBASE)/$(APPNAME) ..." + mkdir -p $(APPBASE) + ln -s $(root_dir) $(APPBASE)/$(APPNAME) -mount: - $(eval docker_run_flags += --volumes-from $(APPNAME)_volumes) - echo "Using volumes from $(APPNAME)_volumes" +## Clean built files +clean: + -rm -rf release +.PHONY: clean -build: - echo "Building $(IMAGE) docker image..." - sudo docker build --rm -t $(IMAGE) . +## Build executable +build: $(APPBASE)/$(APPNAME) + -mkdir -p release + echo "Building: $(ARTEFACT) ..." + GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(LDFLAGS) -o $(ARTEFACT) +.PHONY: build -clean: stop rm - 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 +$(ARTEFACT): build +## Run tests test: - echo "Running tests..." - test.sh + go test +.PHONY: test -dist-prepare: - rm -rf $(PKGDIR) - mkdir -p $(PKGDIR) - ln -s $(PWD)/src $(PKGDIR)/$(APPNAME) - rm -rf dist - -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) +## Install executable +install: $(ARTEFACT) + echo "Installing $(ARTEFACT) to ${HOME}/.local/bin/$(APPNAME) ..." + cp $(ARTEFACT) ${HOME}/.local/bin/$(APPNAME) +.PHONY: install +## Create Docker image +image: + echo "Building Docker inage ..." + docker build --rm -t ncarlier/$(APPNAME) . +.PHONY: image diff --git a/README.md b/README.md index 7fcb4ea..f2860c4 100644 --- a/README.md +++ b/README.md @@ -5,97 +5,151 @@ 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 -### 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. ``` -$ wget https://github.com/ncarlier/webhookd/releases/download/v0.0.3/webhookd-linux-amd64-v0.0.3.tar.gz -$ tar xvzf 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-v1.0.0.tar.gz $ ./webhookd ``` -### Docker +### Using Docker Start the container mounting your scripts directory: ``` $ docker run -d --name=webhookd \ - --env-file etc/env.conf \ + --env-file .env \ -v ${PWD}/scripts:/var/opt/webhookd/scripts \ -p 8080:8080 \ - ncarlier/webhookd + ncarlier/webhookd webhookd ``` -The provided environment file (`etc/env.conf`) is used to configure the app. -Check [sample configuration](etc/env_sample.com) for details. +Check the provided environment file [.env](.env) 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 -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 -|--> /bitbucket - |--> /script_1.sh - |--> /script_2.sh |--> /github -|--> /gitlab -|--> /docker + |--> /build.sh + |--> /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: -For instance if you are **gitlab** and want to call **build.sh** then you will need to use: +### Webhook URL -``` -http://webhook_ip:port/gitlab/build +The directory structure define the webhook URL. +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 -- Gitlab -- Bitbucket -- Docker Hub +data: bar bar bar - -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// +data: done ``` -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_FROM**=webhookd - **APP_NOTIFIER_TO**=hostmaster@nunux.org - **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_SMTP_NOTIFIER_HOST**=localhost:25 +The log file will be sent as an GZIP attachment. + +--- + diff --git a/assets/bitbucket.json b/assets/bitbucket.json deleted file mode 100644 index 5422fe0..0000000 --- a/assets/bitbucket.json +++ /dev/null @@ -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 ", - "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" -} diff --git a/assets/bitbucket.raw b/assets/bitbucket.raw deleted file mode 100644 index beb94b1..0000000 --- a/assets/bitbucket.raw +++ /dev/null @@ -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 diff --git a/assets/docker.json b/assets/docker.json deleted file mode 100644 index 2900405..0000000 --- a/assets/docker.json +++ /dev/null @@ -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" - } -} diff --git a/assets/github.json b/assets/github.json deleted file mode 100644 index dfb1fc2..0000000 --- a/assets/github.json +++ /dev/null @@ -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" - } -} diff --git a/assets/gitlab.json b/assets/gitlab.json deleted file mode 100644 index fb37661..0000000 --- a/assets/gitlab.json +++ /dev/null @@ -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 -} diff --git a/etc/units/webhookd@.service b/etc/units/webhookd@.service deleted file mode 100644 index fb4f9ae..0000000 --- a/etc/units/webhookd@.service +++ /dev/null @@ -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 diff --git a/main.go b/main.go new file mode 100644 index 0000000..35642a7 --- /dev/null +++ b/main.go @@ -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)) +} diff --git a/makefiles b/makefiles new file mode 160000 index 0000000..c249aaa --- /dev/null +++ b/makefiles @@ -0,0 +1 @@ +Subproject commit c249aaa5d479df146699dd164b206ad317d1e5be diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 0000000..9499f97 --- /dev/null +++ b/pkg/api/api.go @@ -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() + } +} diff --git a/pkg/hook/script.go b/pkg/hook/script.go new file mode 100644 index 0000000..df5f570 --- /dev/null +++ b/pkg/hook/script.go @@ -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 +} diff --git a/src/notification/http_notifier.go b/pkg/notification/http_notifier.go similarity index 91% rename from src/notification/http_notifier.go rename to pkg/notification/http_notifier.go index 7e6eae2..5449876 100644 --- a/src/notification/http_notifier.go +++ b/pkg/notification/http_notifier.go @@ -14,15 +14,16 @@ import ( "strings" ) -type HttpNotifier struct { +// HTTPNotifier is able to send a notification to a HTTP endpoint. +type HTTPNotifier struct { URL string From string To string User []string } -func NewHttpNotifier() *HttpNotifier { - notifier := new(HttpNotifier) +func newHTTPNotifier() *HTTPNotifier { + notifier := new(HTTPNotifier) notifier.URL = os.Getenv("APP_HTTP_NOTIFIER_URL") if notifier.URL == "" { log.Println("Unable to create HTTP notifier. APP_HTTP_NOTIFIER_URL not set.") @@ -43,7 +44,8 @@ func NewHttpNotifier() *HttpNotifier { 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 + " ...") data := make(url.Values) data.Set("from", n.From) diff --git a/pkg/notification/notifier_factory.go b/pkg/notification/notifier_factory.go new file mode 100644 index 0000000..05952e7 --- /dev/null +++ b/pkg/notification/notifier_factory.go @@ -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) + } +} diff --git a/src/notification/smtp_notifier.go b/pkg/notification/smtp_notifier.go similarity index 80% rename from src/notification/smtp_notifier.go rename to pkg/notification/smtp_notifier.go index 8d3c500..996833f 100644 --- a/src/notification/smtp_notifier.go +++ b/pkg/notification/smtp_notifier.go @@ -7,14 +7,15 @@ import ( "os" ) -type SmtpNotifier struct { +// SMTPNotifier is able to send notifcation to a email destination. +type SMTPNotifier struct { Host string From string To string } -func NewSmtpNotifier() *SmtpNotifier { - notifier := new(SmtpNotifier) +func newSMTPNotifier() *SMTPNotifier { + notifier := new(SMTPNotifier) notifier.Host = os.Getenv("APP_SMTP_NOTIFIER_HOST") if notifier.Host == "" { notifier.Host = "localhost:25" @@ -30,7 +31,8 @@ func NewSmtpNotifier() *SmtpNotifier { 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) // Connect to the remote SMTP server. c, err := smtp.Dial(n.Host) diff --git a/src/tools/compress.go b/pkg/tools/compress.go similarity index 94% rename from src/tools/compress.go rename to pkg/tools/compress.go index c6b14cb..911be85 100644 --- a/src/tools/compress.go +++ b/pkg/tools/compress.go @@ -8,6 +8,7 @@ import ( "os" ) +// CompressFile is a simple file gzipper. func CompressFile(filename string) (zipfile string, err error) { zipfile = fmt.Sprintf("%s.gz", filename) in, err := os.Open(filename) diff --git a/pkg/tools/query.go b/pkg/tools/query.go new file mode 100644 index 0000000..0c10ae5 --- /dev/null +++ b/pkg/tools/query.go @@ -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 +} diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 0000000..987b46a --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,5 @@ +package version + +var ( + App string = "snapshot" +) diff --git a/src/worker/dispatcher.go b/pkg/worker/dispatcher.go similarity index 72% rename from src/worker/dispatcher.go rename to pkg/worker/dispatcher.go index 5a73123..23de5dc 100644 --- a/src/worker/dispatcher.go +++ b/pkg/worker/dispatcher.go @@ -1,19 +1,18 @@ package worker -import ( - "fmt" -) +import "log" var WorkerQueue chan chan WorkRequest var WorkQueue = make(chan WorkRequest, 100) +// StartDispatcher is charged to start n workers. func StartDispatcher(nworkers int) { // First, initialize the channel we are going to but the workers' work channels into. WorkerQueue = make(chan chan WorkRequest, nworkers) // Now, create all of our workers. for i := 0; i < nworkers; i++ { - fmt.Println("Starting worker", i+1) + log.Println("Starting worker", i+1) worker := NewWorker(i+1, WorkerQueue) worker.Start() } @@ -22,11 +21,11 @@ func StartDispatcher(nworkers int) { for { select { case work := <-WorkQueue: - fmt.Println("Received work request") + log.Println("Received work request:", work.Name) go func() { worker := <-WorkerQueue - fmt.Println("Dispatching work request") + log.Println("Dispatching work request:", work.Name) worker <- work }() } diff --git a/pkg/worker/script_runner.go b/pkg/worker/script_runner.go new file mode 100644 index 0000000..b64d7cf --- /dev/null +++ b/pkg/worker/script_runner.go @@ -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 +} diff --git a/pkg/worker/work_request.go b/pkg/worker/work_request.go new file mode 100644 index 0000000..4a71bc3 --- /dev/null +++ b/pkg/worker/work_request.go @@ -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 +} diff --git a/src/worker/worker.go b/pkg/worker/worker.go similarity index 58% rename from src/worker/worker.go rename to pkg/worker/worker.go index c8b50e3..b134485 100644 --- a/src/worker/worker.go +++ b/pkg/worker/worker.go @@ -2,8 +2,10 @@ package worker import ( "fmt" - "github.com/ncarlier/webhookd/notification" - "github.com/ncarlier/webhookd/tools" + "log" + + "github.com/ncarlier/webhookd/pkg/notification" + "github.com/ncarlier/webhookd/pkg/tools" ) // 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 } +// Worker is a go routine in charge of executing a work. type Worker struct { ID int Work chan WorkRequest @@ -27,8 +30,8 @@ type Worker struct { QuitChan chan bool } -// This function "starts" the worker by starting a goroutine, that is -// an infinite "for-select" loop. +// Start is the function to starts the worker by starting a goroutine. +// That is an infinite "for-select" loop. func (w Worker) Start() { go func() { for { @@ -38,18 +41,20 @@ func (w Worker) Start() { select { case work := <-w.Work: // Receive a work request. - fmt.Printf("worker%d: Received work request %s/%s\n", w.ID, work.Name, work.Action) - filename, err := RunScript(&work) + log.Printf("Worker%d received work request: %s\n", w.ID, work.Name) + filename, err := runScript(&work) if err != nil { - subject := fmt.Sprintf("Webhook %s/%s FAILED.", work.Name, work.Action) - Notify(subject, err.Error(), filename) + subject := fmt.Sprintf("Webhook %s FAILED.", work.Name) + work.MessageChan <- []byte(fmt.Sprintf("error: %s", err.Error())) + notify(subject, err.Error(), filename) } else { - subject := fmt.Sprintf("Webhook %s/%s SUCCEEDED.", work.Name, work.Action) - Notify(subject, "See attachment.", filename) + subject := fmt.Sprintf("Webhook %s SUCCEEDED.", work.Name) + work.MessageChan <- []byte("done") + notify(subject, "See attachment.", filename) } + close(work.MessageChan) case <-w.QuitChan: - // We have been asked to stop. - fmt.Printf("worker%d stopping\n", w.ID) + log.Printf("Stopping worker%d...\n", w.ID) return } } @@ -57,7 +62,6 @@ func (w Worker) Start() { } // Stop tells the worker to stop listening for work requests. -// // Note that the worker will only stop *after* it has finished its work. func (w Worker) Stop() { 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() if err != nil { - fmt.Println(err) + log.Println("Unable to get the notifier. Notification skipped:", err) return } if notifier == nil { - fmt.Println("Notification provider not found.") + log.Println("Notification provider not found. Notification skipped.") return } diff --git a/scripts/bitbucket/build.sh b/scripts/bitbucket/build.sh deleted file mode 100755 index 8887bca..0000000 --- a/scripts/bitbucket/build.sh +++ /dev/null @@ -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 - diff --git a/scripts/bitbucket/echo.sh b/scripts/bitbucket/echo.sh deleted file mode 100755 index db1c12f..0000000 --- a/scripts/bitbucket/echo.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -echo "bitbucket echo: $@" - diff --git a/scripts/docker/echo.sh b/scripts/docker/echo.sh deleted file mode 100755 index c028408..0000000 --- a/scripts/docker/echo.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -echo "docker echo: $@" - diff --git a/scripts/github/echo.sh b/scripts/github/echo.sh deleted file mode 100755 index 4274c42..0000000 --- a/scripts/github/echo.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -echo "github echo: $@" - diff --git a/scripts/gitlab/echo.sh b/scripts/gitlab/echo.sh deleted file mode 100755 index da19837..0000000 --- a/scripts/gitlab/echo.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -echo "gitlab echo: $@" - diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..e5c8301 --- /dev/null +++ b/scripts/test.sh @@ -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 diff --git a/src/api/api.go b/src/api/api.go deleted file mode 100644 index 6982a4f..0000000 --- a/src/api/api.go +++ /dev/null @@ -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 -} - diff --git a/src/api/api_test.go b/src/api/api_test.go deleted file mode 100644 index c53b3fe..0000000 --- a/src/api/api_test.go +++ /dev/null @@ -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) -} - diff --git a/src/hook/bitbucket_hook.go b/src/hook/bitbucket_hook.go deleted file mode 100644 index 6af8576..0000000 --- a/src/hook/bitbucket_hook.go +++ /dev/null @@ -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 -} diff --git a/src/hook/docker_hook.go b/src/hook/docker_hook.go deleted file mode 100644 index 9d5a2d6..0000000 --- a/src/hook/docker_hook.go +++ /dev/null @@ -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 -} diff --git a/src/hook/github_hook.go b/src/hook/github_hook.go deleted file mode 100644 index a491dd3..0000000 --- a/src/hook/github_hook.go +++ /dev/null @@ -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 -} diff --git a/src/hook/gitlab_hook.go b/src/hook/gitlab_hook.go deleted file mode 100644 index c0d641a..0000000 --- a/src/hook/gitlab_hook.go +++ /dev/null @@ -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 -} diff --git a/src/hook/hook_factory.go b/src/hook/hook_factory.go deleted file mode 100644 index 2f79071..0000000 --- a/src/hook/hook_factory.go +++ /dev/null @@ -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) - } -} diff --git a/src/main.go b/src/main.go deleted file mode 100644 index 2bcae62..0000000 --- a/src/main.go +++ /dev/null @@ -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())) -} diff --git a/src/notification/notifier_factory.go b/src/notification/notifier_factory.go deleted file mode 100644 index 1c34de0..0000000 --- a/src/notification/notifier_factory.go +++ /dev/null @@ -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) - } -} diff --git a/src/worker/script_runner.go b/src/worker/script_runner.go deleted file mode 100644 index bf2ea57..0000000 --- a/src/worker/script_runner.go +++ /dev/null @@ -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 -} diff --git a/src/worker/work_request.go b/src/worker/work_request.go deleted file mode 100644 index 0f12e5b..0000000 --- a/src/worker/work_request.go +++ /dev/null @@ -1,7 +0,0 @@ -package worker - -type WorkRequest struct { - Name string - Action string - Args []string -} diff --git a/test.sh b/test.sh deleted file mode 100755 index bcda3af..0000000 --- a/test.sh +++ /dev/null @@ -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 - diff --git a/tests/test.json b/tests/test.json new file mode 100644 index 0000000..c8c4105 --- /dev/null +++ b/tests/test.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 0000000..62283ac --- /dev/null +++ b/tests/test.sh @@ -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