summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOliver Poignant <oliver@poignant.se>2016-12-09 19:56:11 +0100
committerOliver Poignant <oliver@poignant.se>2016-12-09 19:56:11 +0100
commit1c33c18730af7eb7fdc7bc88eb3c1730c34077c9 (patch)
treed01edb9a28320e53cdc0de1d77821c19ae2b853a
parenteda3e82575e90f9102df2b7a6871e6b48dad889a (diff)
downloadGit-Auto-Deploy-1c33c18730af7eb7fdc7bc88eb3c1730c34077c9.zip
Git-Auto-Deploy-1c33c18730af7eb7fdc7bc88eb3c1730c34077c9.tar.gz
Git-Auto-Deploy-1c33c18730af7eb7fdc7bc88eb3c1730c34077c9.tar.bz2
Support for header filters
-rw-r--r--docs/Configuration.md106
-rw-r--r--gitautodeploy/cli/config.py14
-rw-r--r--gitautodeploy/httpserver.py121
3 files changed, 189 insertions, 52 deletions
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 24ce2ac..f2914f6 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -44,15 +44,107 @@ Repository configurations are comprised of the following elements:
be cloned, only the deploy scripts will be executed.
- **deploy**: A command to be executed. If `path` is set, the command is
executed after a successfull `pull`.
- - **filters**: Filters to apply to the web hook events so that only the desired
- events result in executing the deploy actions. See section *Filters* for more
- details.
+ - **payload-filter**: A list of inclusive filters/rules that is applied to the request body of incoming web hook requests and determines whether the deploy command should be executed or not. See section *Filters* for more details.
+ - **header-filter**: A set of inclusive filters/rules that is applied to the request header of incoming web hook requests and determines whether the deploy command should be executed or not. See section *Filters* for more details.
- **secret-token**: The secret token set for your webhook (currently only implemented for [GitHub](https://developer.github.com/webhooks/securing/) and GitLab)
## Filters
*(Currently only supported for GitHub and GitLab)*
-With filter, it is possible to trigger the deploy only if the criteria are met.
+With filters, it is possible to trigger the deploy only if a set of specific criterias are met. The filter can be applied to the web hook request header (if specified using the *header-filter* option) or to the request body (*payload-filter*).
+
+### Allow web hooks with specific header values only (header-filter)
+
+Some Git providers will add custom HTTP headers in their web hook requests when sending them to GAD. Using a *header-filter*, you can configure GAD to only process web hooks that has a specific HTTP header specified.
+
+For example, if you'd like to only process requests that has the *X-Event-Key* header set to the value *pullrequest:fulfilled*, you could use the following config;
+
+```json
+{
+ ...
+ "repositories": [
+ {
+ ...
+ "header-filter": {
+ "X-Event-Key": "pullrequest:fulfilled"
+ }
+ }
+ ]
+}
+```
+
+If a header name is specified but with the value set to true, any request that has the header specified will pass without regard to the header value.
+
+```json
+{
+ ...
+ "repositories": [
+ {
+ ...
+ "header-filter": {
+ "X-Event-Key": true
+ }
+ }
+ ]
+}
+```
+
+### Allow web hooks with specific payload only (payload-filter)
+
+A web hook request typically contains a payload, or a request body, made up of a JSON object. The JSON object in the request body will follow a format choosen by the Git server. Thus, it's format will differ depending on whether you are using GitHub, GitLab, Bitbucket or any other Git provider.
+
+A *payload-filter* can be used to set specific criterias for which incoming web hook requests should actually trigger the deploy command. Filter can be setup to only trigger deploys when a commit is made to a specific branch, or when a pull request is closed and has a specific destination branch.
+
+Since the format of the payload differs depending on what Git provider you are using, you'll need to inspect the web hook request format yourself and write a filter that matches its structure.
+
+To specify a filter that should be applied further down the object tree, a dot notation (".") is used. For example, if the request body looks like this;
+```json
+{
+ "action": "opened",
+ "number": 69,
+ "pull_request": {
+ "url": "https://api.github.com/repos/olipo186/Git-Auto-Deploy/pulls/69",
+ "id": 61793882,
+ "html_url": "https://github.com/olipo186/Git-Auto-Deploy/pull/69",
+ "diff_url": "https://github.com/olipo186/Git-Auto-Deploy/pull/69.diff",
+ "patch_url": "https://github.com/olipo186/Git-Auto-Deploy/pull/69.patch",
+ "issue_url": "https://api.github.com/repos/olipo186/Git-Auto-Deploy/issues/69",
+ "number": 69,
+ "state": "open",
+ "locked": false,
+ "title": "Refactoring. Fixed some imminent issues.",
+ "user": {
+ "login": "olipo186",
+ "id": 1056476,
+ "avatar_url": "https://avatars.githubusercontent.com/u/1056476?v=3",
+ "gravatar_id": "",
+ ...
+ },
+ ...
+}
+```
+
+You could specify the following filter, which would only trigger on pull requests created by olipo186.
+
+```json
+{
+ ...
+ "repositories": [
+ {
+ ...
+ "payload-filter": [
+ {
+ "action": "opened",
+ "pull_request.user.login": "olipo186"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## Legacy filters (older format)
+
For example, deploy on `push` to the `master` branch only, ignore other branches.
Filters are defined by providing keys/values to be looked up in the original
@@ -72,9 +164,9 @@ For example, GitLab web hook data looks like this:
A filter can use `object_kind` and `ref` attributes for example to execute the
deploy action only on a `build` event on the `master` branch.
-# Examples
+### Examples
-## GitHub
+#### GitHub
The following example will trigger when a pull request with **master** as base is closed.
```json
@@ -104,7 +196,7 @@ The following example will trigger when a pull request with **master** as base i
}
```
-## GitLab
+#### GitLab
*(Note: the filter examples below are valid for GitLab)*
Execute pre-deploy script, don't `pull` the repository but execute a deploy
diff --git a/gitautodeploy/cli/config.py b/gitautodeploy/cli/config.py
index e5e68eb..17ab683 100644
--- a/gitautodeploy/cli/config.py
+++ b/gitautodeploy/cli/config.py
@@ -282,11 +282,19 @@ def init_config(config):
if 'path' in repo_config:
repo_config['path'] = os.path.expanduser(repo_config['path'])
- if 'filters' not in repo_config:
- repo_config['filters'] = []
+ # Support for legacy config format
+ if 'filters' in repo_config:
+ repo_config['payload-filter'] = repo_config['filters']
+ del repo_config['filters']
+
+ if 'payload-filter' not in repo_config:
+ repo_config['payload-filter'] = []
+
+ if 'header-filter' not in repo_config:
+ repo_config['header-filter'] = {}
# Rewrite some legacy filter config syntax
- for filter in repo_config['filters']:
+ for filter in repo_config['payload-filter']:
# Legacy config syntax?
if ('kind' in filter and filter['kind'] == 'pull-request-handler') or ('type' in filter and filter['type'] == 'pull-request-filter'):
diff --git a/gitautodeploy/httpserver.py b/gitautodeploy/httpserver.py
index 5313823..654fd83 100644
--- a/gitautodeploy/httpserver.py
+++ b/gitautodeploy/httpserver.py
@@ -66,7 +66,7 @@ class WebhookRequestHandler(BaseHTTPRequestHandler):
return
# Make git pulls and trigger deploy commands
- res = self.process_repositories(repo_configs, ref, action, request_body)
+ res = self.process_repositories(repo_configs, ref, action, request_body, request_headers)
if 'detailed-response' in self._config and self._config['detailed-response']:
self.send_response(200, 'OK')
@@ -161,7 +161,78 @@ class WebhookRequestHandler(BaseHTTPRequestHandler):
logger.error("Unable to recognize request origin. Don't know how to handle the request.")
return
- def process_repositories(self, repo_configs, ref, action, request_body):
+ def passes_payload_filter(self, payload_filters, data, action):
+ import logging
+
+ logger = logging.getLogger()
+
+ # At least one filter must match
+ for filter in payload_filters:
+
+ # All options specified in the filter must match
+ for filter_key, filter_value in filter.iteritems():
+
+ # Ignore filters with value None (let them pass)
+ if filter_value == None:
+ continue
+
+ # Support for earlier version so it's non-breaking functionality
+ if filter_key == 'action' and filter_value == action:
+ continue
+
+ # Interpret dots in filter name as path notations
+ node_value = data
+ for node_key in filter_key.split('.'):
+
+ # If the path is not valid the filter does not match
+ if not node_key in node_value:
+ logger.info("Filter '%s' does not match since the path is invalid" % (filter_key))
+
+ # Filter does not match, do not process this repo config
+ return False
+
+ node_value = node_value[node_key]
+
+ if filter_value == node_value:
+ continue
+
+ # If the filter value is set to True. the filter
+ # will pass regardless of the actual value
+ if filter_value == True:
+ continue
+
+ logger.info("Filter '%s'' does not match ('%s' != '%s')" % (filter_key, filter_value, (str(node_value)[:75] + '..') if len(str(node_value)) > 75 else str(node_value)))
+
+ # Filter does not match, do not process this repo config
+ return False
+
+ # Filter does match, proceed
+ return True
+
+ def passes_header_filter(self, header_filter, request_headers):
+ import logging
+
+ logger = logging.getLogger()
+
+ # At least one filter must match
+ for key in header_filter:
+
+ # Verify that the request has the required header attribute
+ if key.lower() not in request_headers:
+ return False
+
+ # "True" indicates that any header value is accepted
+ if header_filter[key] is True:
+ continue
+
+ # Verify that the request has the required header value
+ if header_filter[key] != request_headers[key.lower()]:
+ return False
+
+ # Filter does match, proceed
+ return True
+
+ def process_repositories(self, repo_configs, ref, action, request_body, request_headers):
"""Verify that the suggested repositories has matching settings and
issue git pull and/or deploy commands."""
import os
@@ -181,48 +252,14 @@ class WebhookRequestHandler(BaseHTTPRequestHandler):
repo_result = {}
- try:
- # Verify that all filters matches the request (if any filters are specified)
- if 'filters' in repo_config:
-
- # At least one filter must match
- for filter in repo_config['filters']:
-
- # All options specified in the filter must match
- for filter_key, filter_value in filter.iteritems():
+ # Verify that all payload filters matches the request (if any payload filters are specified)
+ if 'payload-filter' in repo_config and not self.passes_payload_filter(repo_config['payload-filter'], data, action):
- # Ignore filters with value None (let them pass)
- if filter_value == None:
- continue
-
- # Support for earlier version so it's non-breaking functionality
- if filter_key == 'action' and filter_value == action:
- continue
-
- # Interpret dots in filter name as path notations
- node_value = data
- for node_key in filter_key.split('.'):
-
- # If the path is not valid the filter does not match
- if not node_key in node_value:
- logger.info("Filter '%s' does not match since the path is invalid" % (filter_key))
- raise FilterMatchError()
-
- node_value = node_value[node_key]
-
- if filter_value == node_value:
- continue
-
- # If the filter value is set to True. the filter
- # will pass regardless of the actual value
- if filter_value == True:
- continue
-
- logger.info("Filter '%s'' does not match ('%s' != '%s')" % (filter_key, filter_value, (str(node_value)[:75] + '..') if len(str(node_value)) > 75 else str(node_value)))
-
- raise FilterMatchError()
+ # Filter does not match, do not process this repo config
+ continue
- except FilterMatchError as e:
+ # Verify that all header filters matches the request (if any header filters are specified)
+ if 'header-filter' in repo_config and not self.passes_header_filter(repo_config['header-filter'], request_headers):
# Filter does not match, do not process this repo config
continue