diff options
author | Oliver Poignant <oliver@poignant.se> | 2016-12-09 19:56:11 +0100 |
---|---|---|
committer | Oliver Poignant <oliver@poignant.se> | 2016-12-09 19:56:11 +0100 |
commit | 1c33c18730af7eb7fdc7bc88eb3c1730c34077c9 (patch) | |
tree | d01edb9a28320e53cdc0de1d77821c19ae2b853a | |
parent | eda3e82575e90f9102df2b7a6871e6b48dad889a (diff) | |
download | Git-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.md | 106 | ||||
-rw-r--r-- | gitautodeploy/cli/config.py | 14 | ||||
-rw-r--r-- | gitautodeploy/httpserver.py | 121 |
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 |