Skip to content

Commit

Permalink
Improve readme with example usage
Browse files Browse the repository at this point in the history
  • Loading branch information
expelledboy committed Feb 26, 2020
1 parent 7aebfb0 commit 8d886a9
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 43 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
SECRET
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
FROM alpine:3.9 as build
FROM alpine:3.9

RUN apk add --no-cache make nodejs npm

WORKDIR /root
COPY package*.json ./
RUN npm set progress=false && npm config set depth 0
RUN npm install --only=production

COPY src /root/src

EXPOSE 3000
CMD ["npm", "start"]
ENV TERM xterm-256color
CMD ["node", "/root/src/server.js"]
49 changes: 43 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
REGISTRY_HOST = docker.io
REGISTRY = docker.io
USERNAME = expelledboy
NAME = $(shell basename $(CURDIR))
IMAGE = $(REGISTRY_HOST)/$(USERNAME)/$(NAME)
IMAGE = $(REGISTRY)/$(USERNAME)/$(NAME)

.EXPORT_ALL_VARIABLES:
.DEFAULT: help
Expand All @@ -12,13 +12,15 @@ IMAGE = $(REGISTRY_HOST)/$(USERNAME)/$(NAME)
help: ## Print help messages
@sed -n 's/^\([a-zA-Z_-]*\):.*## \(.*\)$$/\1 -- \2/p' Makefile

build: VERSION = $(shell git describe --always)
build: ## Build docker image
docker build -t $(IMAGE) .
docker tag $(IMAGE):latest $(IMAGE):$(VERSION)

test: ## Run simple unit test
docker run -d --rm \
--name $(NAME) \
-v $(PWD):/webhook/ \
-v $(PWD):/webhooks/ \
-p 3000:3000 \
$(IMAGE)
sleep 5
Expand All @@ -42,8 +44,43 @@ bump: bump-package ## Bump release version eg. `make IMPACT=patch bump`
git tag $(VERSION)

publish: VERSION = $(shell git describe --always)
publish: on-tag build ## Push docker image to $(REGISTRY_HOST)
echo docker push $(IMAGE):$(VERSION)
echo docker push $(IMAGE):latest
publish: on-tag build ## Push docker image to $(REGISTRY)
docker push $(IMAGE):$(VERSION)
docker push $(IMAGE):latest

# ==
.PHONY: web intro start-webhooks hello-world crash run-webhook

intro:
@cat ./docs/welcome.md

web: ## Run webhooks interactively in docker.
docker run -it --rm \
--name $(NAME) \
--volume $(PWD):/webhooks \
--publish 80:3000 \
expelledboy/make-webhooks

start-webhooks: env-HOSTNAME ## Deploy as service into docker swarm with traefik.
docker service create \
--name make-webhooks \
--mode global \
--network web \
--mount src="$(PWD)",target=/webhooks,type=bind \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--label traefik.enable=true \
--label traefik.docker.network=web \
--label traefik.port=3000 \
--label traefik.frontend.rule=Host:$(HOSTNAME) \
$(IMAGE)

hello-world: GREET ?= "World"
hello-world: ## Example target using environment variables.
@echo "Hello, $(GREET)!"

crash: ## Target that always fails.
exit 10

run-webhook: HOOK = hello-world
run-webhook: ## Run webhook from Makefile and fail on error
curl -fs http://localhost/$(HOOK)
71 changes: 58 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,64 @@
# Makefile Hook
# make-webhooks

Webhooks for your project made simple. Most projects have a Makefile, why not
just expose an API to run them?
> Just try it: `make web`. ;)
You are now exposing this projects `Makefile` to the web!

> $ curl http://localhost/hello-world
> {"status":0}
Change environment variables. All vars are uppercased by default.
Dont forget HTTP URL encoding!

> curl http://localhost/hello-world\?GREET\=Anthony
> curl "http://localhost/hello-world?GREET=Anthony"
> curl "http://localhost/hello-world?greet=Anthony"
You can use the webhooks return status as an exit code in another script.

```sh
#!/bin/bash

if [ "$1" -eq "run-webhook" ]; then
exit $(curl -s http://localhost/crash | jq '.status')
fi
```
echo 'test:\n\texit 0' > Makefile
docker run -d --rm \
-v $(PWD):/webhook \
-p 3000:3000 \
--name make-webhook \
expelledboy/make-webhook
curl http://localhost:3000/test
# Cleanup
docker stop make-webhook

> ./script.sh; echo $?
> 0
> ./script.sh run-webhook; echo $?
> 2
Example usage in the command line.

> `exit $(curl -s http://localhost/crash | jq '.status')`; echo $?
> 2
But best yet, in a make target!

```make
run-webhook:
@curl -fs http://localhost/crash
```

> $ make run-webhook
> make: *** [Makefile:86: run-webhook] Error 22

### Security

We match the Bearer token in the Authorization header, either with environment
variable `SECRET`, or the contents of the file located at `/webhooks/SECRET`.

> $ echo mySecret > SECRET
> $ curl http://localhost/hello-world
> Unauthorized
> $ curl -H 'Authorization: Bearer mySecret' http://localhost/hello-world
> {"status":0}

Can even do key rotations!

> $ echo -n myNewSecret > SECRET
> $ curl -H 'Authorization: Bearer mySecret' http://localhost/hello-world ; echo ''
> Unauthorized
> $ curl -H 'Authorization: Bearer myNewSecret' http://localhost/hello-world ; echo ''
> {"status":0}
5 changes: 5 additions & 0 deletions docs/exit-on-status.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

if [ "$1" == "run-webhook" ]; then
exit $(curl -s http://localhost/crash | jq '.status')
fi
5 changes: 5 additions & 0 deletions docs/welcome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

You are now exposing this projects `Makefile` to the web!

> $ curl http://localhost/hello-world
> {"status":0}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "make-webhook",
"name": "make-webhooks",
"version": "1.0.2",
"description": "Run make targets through webhooks",
"repository": "https://github.com/expelledboy/make-webhook",
"repository": "https://github.com/expelledboy/make-webhooks",
"author": "expelledboy",
"license": "Apache-2.0",
"main": "src/server.js",
Expand Down Expand Up @@ -34,7 +34,7 @@
},
"scripts": {
"start": "node src/server.js",
"dev": "DEBUG=webhook:* nodemon src/server.js",
"dev": "DEBUG=webhooks:* nodemon src/server.js",
"lint": "eslint src",
"lint:fix": "npm run lint -- --fix"
}
Expand Down
76 changes: 58 additions & 18 deletions src/server.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,83 @@
const fs = require("fs");
const path = require("path");
const express = require("express");
const process = require("process");
const { exec } = require("child_process");

// Constants
const HOST = "0.0.0.0";
const PORT = parseInt(process.env.PORT || "3000", 10);
const { TOKEN } = process.env;
const BASE_DIR = process.env.BASE_DIR || "/webhook";
const MAKEFILE = `${BASE_DIR}/Makefile`;
const PORT = 3000;
const MAKE_DIR = process.env.MAKE_DIR || "/webhooks";
const MAKEFILE = `${MAKE_DIR}/Makefile`;
const SECRET_FILE = `${MAKE_DIR}/SECRET`;

const app = express();

if (TOKEN)
app.use((req, res, next) => {
if (req.headers.authorization !== `Bearer ${TOKEN}`)
return res.status(401).send("Unauthorized");
return next();
});
// Get hot reloaded secret
const getSecret = () => {
if (fs.existsSync(SECRET_FILE)) {
return fs.readFileSync(SECRET_FILE);
}
return process.env.SECRET;
};

// Security
app.use((req, res, next) => {
const secret = getSecret();
if (secret && req.headers.authorization !== `Bearer ${secret}`) {
return res.status(401).send("Unauthorized");
}
return next();
});

// Sanity check
if (!fs.existsSync(MAKEFILE)) {
console.error(`${MAKEFILE} does not exists`);
process.exit(1);
}

// Print an optional container introduction on startup
process.nextTick(() => {
const opts = { cwd: MAKE_DIR };
exec(`make intro`, opts, (error, stdout) => console.log(stdout));
});

// All GET requests
app.get("*", (req, res) => {
// Parse URL
const { name: target } = path.parse(req.path);
const config = { env: req.query, cwd: BASE_DIR };
const ENV = Object.entries(req.query).reduce((acc, [key, value]) => {
acc[key.toUpperCase()] = value;
return acc;
}, {});

exec(`make ${target}`, config, (error, stdout, stderr) => {
// Marshal command results into json
const handleExec = (error, stdout, stderr) => {
if (error) {
console.error(new Date(), { target, env: req.query, status: error.code });
return res.status(500).send(stderr);
const response = { target, env: ENV, status: error.code };
console.log(stderr);
return res.status(500).json(response);
}
console.log(stdout);
return res.json({ status: 0 });
};

console.log(new Date(), { status: 0 });
return res.send("OK");
});
// Execute make target
const opts = { env: ENV, cwd: MAKE_DIR };
const command = `make ${target}`;
const env = Object.entries(ENV)
.map(([key, value]) => `${key}=${value}`)
.join(" ");
console.log("## ==", new Date(), env, command);
exec(command, opts, handleExec);
});

// Start HTTP server
app.listen(PORT, HOST);
console.log(`# Running at http://${HOST}:${PORT}`);

console.log(`==> Running at http://localhost:${PORT}`);
// Terminate with Ctrl-C
process.on("SIGINT", () => {
console.log("# Interrupted");
process.exit(0);
});

0 comments on commit 8d886a9

Please sign in to comment.