Skip to content

Commit 9b68de4

Browse files
committed
major refactoring for v0.2, changing EVERYTHING
1 parent 7e01ce0 commit 9b68de4

File tree

21 files changed

+382
-428
lines changed

21 files changed

+382
-428
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,3 @@
66
__pycache__
77
build/
88
dist/
9-
webhooks_automata.egg-info/

Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM python:3.11
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt /tmp
6+
7+
RUN pip install --no-cache-dir -r /tmp/requirements.txt
8+
9+
COPY webhooks_automata ./webhooks_automata
10+
11+
ENV AUTOMATA_SETTINGS /config/wha_settings.yaml
12+
13+
VOLUME [ "/config" ]
14+
VOLUME [ "/plugins" ]
15+
16+
EXPOSE 8000
17+
18+
CMD ["uvicorn", "webhooks_automata.app:app", "--host", "0.0.0.0", "--port", "8000"]

README.md

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
2-
Webhook receiver for automations
3-
================================
1+
Webhook server for triggering automations
2+
=========================================
43

54
This project started as a dirty & quick hack to perform some deployment actions.
65
Initially, the focus was on Git webhooks, but after a while this project was
@@ -10,57 +9,94 @@ I looked a little bit at other projects and didn't found any one that suited my
109
needs, so I started this project... and then it grew a little bit and become
1110
something more versatile than the quick hack originally intended.
1211

13-
This project is aimed at DevOps or sysadmin that have git repositories, typically
14-
with a VIP-branch, and automatic deployment. You may want to have different branches
15-
for stage and production or set up push permissions differently to the different
16-
branches. This utility, when a webhook is received, will update the local Git repository
17-
and perform the commands in the settings.
12+
This project is aimed at DevOps or sysadmin that have git repositories in GitHub, Gitlab,
13+
or any other kind of webhook provider (git or otherwise).
1814

19-
Quickstart
20-
----------
15+
This utility starts a server that listens to webhooks and when a webhook is received,
16+
it will perform the actions in the settings.
2117

22-
- Create and activate a Virtual Environment:
18+
Quickstart demo with GitHub and ngrok
19+
-------------------------------------
2320

24-
```bash
25-
$ virtualenv --python=/usr/bin/python3 /path/to/venv
26-
$ source /path/to/venv/bin/activate
27-
```
21+
Using [ngrok](https://ngrok.com/) is a quick way to obtain a publicly reachable HTTPS endpoint.
22+
We will be using that for this demo.
2823

29-
- Install the package: `pip install webhooks_automata`
30-
- Create a `settings.yaml`
31-
- Set up a service (e.g. a systemd _service_ file) that does something along:
24+
- Create and activate a Virtual Environment in a Python 3.11 environment.
25+
- Install the package: `pip install webhooks-automata`
26+
- If you want to use `main` branch, do
27+
`pip install git+https://github.com/alexbarcelo/webhooks-automata`
28+
instead.
29+
- Create a `settings.yaml`, like the one shown in [examples](examples/github_sample.yaml).
30+
- Start the server:
3231

3332
```bash
34-
$ /path/to/venv/bin/wh-automatad /path/to/settings.yaml
33+
$ AUTOMATA_SETTINGS=examples/github_sample.yaml uvicorn webhooks_automata.app:app
34+
INFO: Started server process [57971]
35+
INFO: Waiting for application startup.
36+
Current endpoints active:
37+
/my_webhook
38+
INFO: Application startup complete.
39+
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
3540
```
3641

42+
- Run the ngrok ingress to this service with `ngrok http 8000`
43+
- The webhook endpoint will be something like `https://c94e-2600-70ff-f2f3-0-13ef-f451-8b4f-cc76.ngrok-free.app/my_webhook`
44+
(the host on this URL is given by ngrok and the path is set in the settings YAML file).
45+
- Go to your GitHub repository > Settings > Webhooks > Add webhook.
46+
- Fill the form:
47+
- **Payload URL**: The webhook endpoint.
48+
- **Content type**: Does not matter, but use JSON for future proofing it.
49+
- **Secret**: Put the shared secret in the settings YAML file, in this demo it is `secret_token`
50+
- _Add webhook_
51+
- When a webhook is created, GitHub makes a "ping delivery". You can check in the Recent Deliveries
52+
tab if there was a problem with the webhook endpoint.
53+
54+
Deploying the webhook server
55+
----------------------------
56+
57+
Instead of using ngrok, you may want to deploy this application into a
58+
domain/server under your control. The steps are similar like on the previous
59+
demo, but you will need to include a reverse proxy (and you probably want
60+
to include SSL termination on it).
61+
62+
Take a look into [nginx](https://nginx.org/), [Traefik](https://traefik.io/)
63+
or [HAProxy](https://www.haproxy.org/).
64+
65+
In addition to that, you will want to automatically start the Uvicorn server,
66+
which may done with either systemd (or whatever is used by your OS) or
67+
[supervisord](http://supervisord.org/).
68+
3769
Settings
3870
--------
3971

4072
Extra CLI features
4173
------------------
4274

43-
To force a manul trigger, you can use
44-
the built-in `wh-automata-trigger` command. Example usage:
75+
The `wh-automatactl` is a CLI that can be used to aid and complement the webhook server.
4576

46-
```bash
47-
$ wh-git-trigger /path/to/setings.yaml entryname
48-
```
49-
50-
This will react just as if a trigger had been received. Future versions of this tool will
51-
include more fine-grain control --e.g. dry-run, display status information...
77+
You can check its documentation by calling `wh-automatactl --help`. Keep in mind that this
78+
CLI will be accessible from within the virtual environment; that typically means that the
79+
script can be found in /pat/to/venv/bin/wh-automatactl.
5280

5381
Implementation details
5482
----------------------
5583

56-
This project contains a minimal Flask server that answers the POST webhooks. A typical source
57-
of those webhooks call are Git servers such as GitLab, GitHub or Gogs. The server is started
58-
through the Flask's `app.run` method.
59-
60-
Not a lot of traffic is expected, but you may want to set up a reverse proxy in front
61-
of the Flask server, or add some fancier method like a WSGI or uWSGI or similar layer.
62-
63-
Typical webhooks expect the a quick reply (in general, HTTP connections are intended to be short
64-
lived) so there is a worker/tasks approach. There is a very simple implementation base on
65-
`Threading` and a shared `Queue`. More complex implementations may be added in the future
66-
(pull requests are welcome).
84+
This project contains a minimal [Starlette](https://www.starlette.io/) ASGI
85+
application that answers the POST webhooks. My recommendation is to start the
86+
application through [uvicorn](http://www.uvicorn.org/).
87+
88+
The actions are run as [Starlette Background Tasks](https://www.starlette.io/background/)
89+
because generally providers (GitHub, Gitlab, etc.) expect a quick webhook and
90+
do not care on the return. A 200 success response is given for all properly
91+
authorized requests and actions are run after that; a 200 success response
92+
**does not mean** that actions have run successfully (in fact, the actions start
93+
**after** the response).
94+
95+
Most internal code assumes that things can be made asynchronously, so the
96+
webhook server should be a lightweight but capable process --the heavy-lifting
97+
will be performed by asynchronous actions. This is assured by using
98+
[`asyncio-subprocess`](https://docs.python.org/3/library/asyncio-subprocess.html)
99+
and other async-centric tools.
100+
101+
Feel free to use other ASGI servers, or embed the Starlette routes into a more
102+
complex application.

examples/github_sample.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
automata:
2+
- endpoint:
3+
suffix: my_webhook
4+
provider: github
5+
secret_token: secret_token
6+
action:
7+
type: commands
8+
commands:
9+
- echo "Hello World"

pyproject.toml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "webhooks-automata"
7+
dynamic = ["version"]
8+
description = "Webhook receiver for flexible automations"
9+
readme = "README.md"
10+
requires-python = ">=3.11"
11+
license = "Apache-2.0"
12+
authors = [
13+
{ name = "Alex Barcelo", email = "alex@betarho.net" },
14+
]
15+
classifiers = [
16+
"Programming Language :: Python :: 3",
17+
"License :: OSI Approved :: Apache Software License",
18+
"Operating System :: OS Independent",
19+
]
20+
dependencies = [
21+
"starlette",
22+
"uvicorn",
23+
"pydantic>=2.0",
24+
"click",
25+
"pyyaml>=6.0.1",
26+
]
27+
28+
[project.urls]
29+
"Source code" = "https://github.com/alexbarcelo/webhooks-automata"
30+
31+
[project.scripts]
32+
wh-automatactl = "webhooks_automata.scripts:cli"
33+
34+
[tool.hatch.version]
35+
path = "webhooks_automata/__init__.py"

requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
starlette==0.30.0
2+
uvicorn==0.23.1
3+
pydantic==2.0.3
4+
click==8.1.6
5+
PyYAML==6.0.1

setup.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

webhooks_automata/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "v0.2.0"

webhooks_automata/app.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from typing import ClassVar, TYPE_CHECKING
2+
import yaml
3+
4+
from starlette.applications import Starlette
5+
from starlette.background import BackgroundTask
6+
from starlette.responses import JSONResponse
7+
from starlette.routing import Route
8+
from starlette.config import Config
9+
10+
from .settings import Settings, Automaton
11+
12+
if TYPE_CHECKING:
13+
from .union_models.actions import ActionBase
14+
from .union_models.endpoints import EndpointBase
15+
16+
config = Config(".env")
17+
settings_path = config("AUTOMATA_SETTINGS", default="wha_settings.yaml")
18+
with open(settings_path) as f:
19+
settings = Settings(**yaml.load(f, Loader=yaml.Loader))
20+
21+
22+
class ActionDispatcher:
23+
active_endpoints: ClassVar[list[str]] = list()
24+
action: "ActionBase"
25+
endpoint: "EndpointBase"
26+
27+
def __new__(cls, autom: Automaton):
28+
cls.active_endpoints.append(autom.endpoint.suffix)
29+
return super().__new__(cls)
30+
31+
def __init__(self, autom: Automaton):
32+
self.action = autom.action
33+
self.endpoint = autom.endpoint
34+
35+
async def dispatch(self, request):
36+
allowed = await self.endpoint.authorize(request)
37+
if not allowed:
38+
return JSONResponse({'status': 'unauthorized'}, status_code=401)
39+
task = BackgroundTask(self.action.perform_action, request)
40+
return JSONResponse({'status': 'triggered'}, background=task)
41+
42+
43+
def startup():
44+
print("Current active endpoints:")
45+
for s in ActionDispatcher.active_endpoints:
46+
print(f"\t/{s}")
47+
48+
routes = list()
49+
for automata in settings.automata:
50+
suffix = automata.endpoint.suffix
51+
routes.append(Route(f"/{suffix}", ActionDispatcher(automata).dispatch, methods=["POST"]))
52+
53+
app = Starlette(routes=routes, on_startup=[startup])

webhooks_automata/automata/__init__.py

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)