├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config.yml ├── docker-compose.yml ├── inventory ├── library ├── __init__.py ├── kong_api.py ├── kong_consumer.py ├── kong_plugin.py ├── requirements.txt ├── test_kong.py ├── test_kong_consumer.py └── test_kong_plugin.py └── playbook.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | env2.7 64 | env3.4 65 | .DS_Store 66 | reports 67 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toast38coza/ansible-kong-module/c1e7b471a517d1ec99c5629f3729ebc34088bd64/Dockerfile -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Christo Crampton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ansible-kong-module 2 | A Module to help manage a [Kong](http://getkong.com) API Gateway 3 | 4 | > For a full write-up, please see the blog series: [Kong Up and Running](http://blog.toast38coza.me/kong-up-and-running/) 5 | 6 | **Requirements** 7 | 8 | * Ansible 9 | * python requests library 10 | * Docker and Docker Compose 11 | 12 | **Quickstart** 13 | 14 | 15 | From a Docker-enabled terminal run: 16 | 17 | ``` 18 | git clone git@github.com:toast38coza/ansible-kong-module.git && cd ansible-kong-module 19 | docker-compose up 20 | ``` 21 | 22 | This might take a while to run ... after it is finished you can find the IP of your docker machine with: 23 | 24 | ``` 25 | $ docker-machine ip default 26 | > 1.2.3.4 27 | ``` 28 | (assuming your docker-machine is called default). 29 | 30 | You can then access your Kong API at: 31 | 32 | * **Admin interface:** 1.2.3.4:8001 33 | * **REST Interface:** 1.2.3.4:8000 34 | 35 | 36 | **Configure your Kong instance with:** 37 | 38 | ``` 39 | ansible-playbook playbook.yml -i inventory --extra-vars "kong_admin_base_url=1.2.3.4:8001 kong_base_url=1.2.3.4:8000" 40 | ``` 41 | 42 | * set kong_admin_base_url and kong_base_url to your Kong instance's urls -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | vars: 3 | - admin_base_url: "http://192.168.99.100:8001" 4 | - api_base_url: "http://192.168.99.100:8000" 5 | 6 | tasks: 7 | 8 | - name: Register APIs 9 | kong_api: 10 | kong_admin_uri: "{{admin_base_url}}" 11 | name: "{{item.name}}" 12 | upstream_url: "{{item.upstream_url}}" 13 | request_host: "{{item.request_host}}" 14 | state: present 15 | request_path: "{{item.request_path}}" 16 | with_items: 17 | - name: "mockbin" 18 | upstream_url: "http://mockbin.com" 19 | request_host: "mockbin.com" 20 | request_path: "/mockbin" 21 | strip_request_path: yes 22 | register: response 23 | 24 | - debug: var=response -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | cassandra: 2 | image: cassandra:2.2.4 3 | container_name: cassandra 4 | ports: 5 | - "9042:9042" 6 | web: 7 | image: mashape/kong 8 | container_name: kong 9 | ports: 10 | - "8000:8000" 11 | - "8443:8443" 12 | - "8001:8001" 13 | - "7946:7946" 14 | - "7946:7946/udp" 15 | links: 16 | - cassandra 17 | ui: 18 | image: pgbi/kong-dashboard 19 | ports: 20 | - "8080:8080" 21 | -------------------------------------------------------------------------------- /inventory: -------------------------------------------------------------------------------- 1 | [localhost] 2 | localhost ansible_connection=local 3 | -------------------------------------------------------------------------------- /library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toast38coza/ansible-kong-module/c1e7b471a517d1ec99c5629f3729ebc34088bd64/library/__init__.py -------------------------------------------------------------------------------- /library/kong_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | DOCUMENTATION = ''' 4 | --- 5 | module: kong 6 | short_description: Configure a Kong API Gateway 7 | 8 | ''' 9 | 10 | EXAMPLES = ''' 11 | - name: Register a site 12 | kong: 13 | kong_admin_uri: http://127.0.0.1:8001/apis/ 14 | name: "Mockbin" 15 | taget_url: "http://mockbin.com" 16 | request_host: "mockbin.com" 17 | state: present 18 | 19 | - name: Delete a site 20 | kong: 21 | kong_admin_uri: http://127.0.0.1:8001/apis/ 22 | name: "Mockbin" 23 | state: absent 24 | 25 | ''' 26 | 27 | import json, requests, os 28 | 29 | class KongAPI: 30 | 31 | def __init__(self, base_url, auth_username=None, auth_password=None): 32 | self.base_url = base_url 33 | if auth_username is not None and auth_password is not None: 34 | self.auth = (auth_username, auth_password) 35 | else: 36 | self.auth = None 37 | 38 | def __url(self, path): 39 | return "{}{}" . format (self.base_url, path) 40 | 41 | def _api_exists(self, name, api_list): 42 | for api in api_list: 43 | if name == api.get("name", None): 44 | return True 45 | return False 46 | 47 | def add_or_update(self, name, upstream_url, request_host=None, request_path=None, strip_request_path=False, preserve_host=False): 48 | 49 | method = "post" 50 | url = self.__url("/apis/") 51 | api_list = self.list().json().get("data", []) 52 | api_exists = self._api_exists(name, api_list) 53 | 54 | if api_exists: 55 | method = "patch" 56 | url = "{}{}" . format (url, name) 57 | 58 | data = { 59 | "name": name, 60 | "upstream_url": upstream_url, 61 | "strip_request_path": strip_request_path, 62 | "preserve_host": preserve_host 63 | } 64 | if request_host is not None: 65 | data['request_host'] = request_host 66 | if request_path is not None: 67 | data['request_path'] = request_path 68 | 69 | return getattr(requests, method)(url, data, auth=self.auth) 70 | 71 | 72 | def list(self): 73 | url = self.__url("/apis") 74 | return requests.get(url, auth=self.auth) 75 | 76 | def info(self, id): 77 | url = self.__url("/apis/{}" . format (id)) 78 | return requests.get(url, auth=self.auth) 79 | 80 | def delete_by_name(self, name): 81 | info = self.info(name) 82 | id = info.json().get("id") 83 | return self.delete(id) 84 | 85 | def delete(self, id): 86 | path = "/apis/{}" . format (id) 87 | url = self.__url(path) 88 | return requests.delete(url, auth=self.auth) 89 | 90 | class ModuleHelper: 91 | 92 | def __init__(self, fields): 93 | self.fields = fields 94 | 95 | def get_module(self): 96 | 97 | args = dict( 98 | kong_admin_uri = dict(required=False, type='str'), 99 | kong_admin_username = dict(required=False, type='str'), 100 | kong_admin_password = dict(required=False, type='str'), 101 | name = dict(required=False, type='str'), 102 | upstream_url = dict(required=False, type='str'), 103 | request_host = dict(required=False, type='str'), 104 | request_path = dict(required=False, type='str'), 105 | strip_request_path = dict(required=False, default=False, type='bool'), 106 | preserve_host = dict(required=False, default=False, type='bool'), 107 | state = dict(required=False, default="present", choices=['present', 'absent', 'latest', 'list', 'info'], type='str'), 108 | ) 109 | return AnsibleModule(argument_spec=args,supports_check_mode=False) 110 | 111 | def prepare_inputs(self, module): 112 | url = module.params['kong_admin_uri'] 113 | auth_user = module.params['kong_admin_username'] 114 | auth_password = module.params['kong_admin_password'] 115 | state = module.params['state'] 116 | data = {} 117 | 118 | for field in self.fields: 119 | value = module.params.get(field, None) 120 | if value is not None: 121 | data[field] = value 122 | 123 | return (url, data, state, auth_user, auth_password) 124 | 125 | def get_response(self, response, state): 126 | 127 | if state == "present": 128 | meta = response.json() 129 | has_changed = response.status_code in [201, 200] 130 | 131 | if state == "absent": 132 | meta = {} 133 | has_changed = response.status_code == 204 134 | 135 | if state == "list": 136 | meta = response.json() 137 | has_changed = False 138 | 139 | return (has_changed, meta) 140 | 141 | def main(): 142 | 143 | fields = [ 144 | 'name', 145 | 'upstream_url', 146 | 'request_host', 147 | 'request_path', 148 | 'strip_request_path', 149 | 'preserve_host' 150 | ] 151 | 152 | helper = ModuleHelper(fields) 153 | 154 | global module # might not need this 155 | module = helper.get_module() 156 | base_url, data, state, auth_user, auth_password = helper.prepare_inputs(module) 157 | 158 | api = KongAPI(base_url, auth_user, auth_password) 159 | if state == "present": 160 | response = api.add_or_update(**data) 161 | if state == "absent": 162 | response = api.delete_by_name(data.get("name")) 163 | if state == "list": 164 | response = api.list() 165 | 166 | if response.status_code == 401: 167 | module.fail_json(msg="Please specify kong_admin_username and kong_admin_password", meta=response.json()) 168 | elif response.status_code == 403: 169 | module.fail_json(msg="Please check kong_admin_username and kong_admin_password", meta=response.json()) 170 | else: 171 | has_changed, meta = helper.get_response(response, state) 172 | module.exit_json(changed=has_changed, meta=meta) 173 | 174 | from ansible.module_utils.basic import * 175 | from ansible.module_utils.urls import * 176 | 177 | if __name__ == '__main__': 178 | main() 179 | 180 | -------------------------------------------------------------------------------- /library/kong_consumer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import requests 4 | 5 | class KongConsumer: 6 | 7 | def __init__(self, base_url, auth_username=None, auth_password=None): 8 | self.base_url = "{}/consumers" . format(base_url) 9 | if auth_username is not None and auth_password is not None: 10 | self.auth = (auth_username, auth_password) 11 | else: 12 | self.auth = None 13 | 14 | def list(self): 15 | return requests.get(self.base_url, auth=self.auth) 16 | 17 | def add(self, username=None, custom_id=None): 18 | 19 | assert [username, custom_id] != [None, None], \ 20 | 'Please provide at least one of username or custom_id' 21 | 22 | data = {} 23 | if username is not None: 24 | data['username'] = username 25 | if custom_id is not None: 26 | data['custom_id'] = custom_id 27 | 28 | return requests.post(self.base_url, data, auth=self.auth) 29 | 30 | def delete(self, id): 31 | url = "{}/{}" . format (self.base_url, id) 32 | return requests.delete(url, auth=self.auth) 33 | 34 | def configure_for_plugin(self, username_or_id, api, data): 35 | """This could possibly go in it's own plugin""" 36 | 37 | url = "{}/{}/{}" . format (self.base_url, username_or_id, api) 38 | return requests.post(url, data, auth=self.auth) 39 | 40 | class ModuleHelper: 41 | 42 | def get_module(self): 43 | 44 | args = dict( 45 | kong_admin_uri = dict(required=True, type='str'), 46 | kong_admin_username = dict(required=False, type='str'), 47 | kong_admin_password = dict(required=False, type='str'), 48 | username = dict(required=False, type='str'), 49 | custom_id = dict(required=False, type='str'), 50 | state = dict(required=False, default="present", choices=['present', 'absent', 'list', 'configure'], type='str'), 51 | data = dict(required=False, type='dict'), 52 | api_name = dict(required=False, type='str'), 53 | ) 54 | return AnsibleModule(argument_spec=args,supports_check_mode=False) 55 | 56 | def prepare_inputs(self, module): 57 | url = module.params['kong_admin_uri'] 58 | auth_user = module.params['kong_admin_username'] 59 | auth_password = module.params['kong_admin_password'] 60 | state = module.params['state'] 61 | username = module.params.get('username', None) 62 | custom_id = module.params.get('custom_id', None) 63 | data = module.params.get('data', None) 64 | api_name = module.params.get('api_name', None) 65 | 66 | return (url, username, custom_id, state, api_name, data, auth_user, auth_password) 67 | 68 | def get_response(self, response, state): 69 | 70 | if state in ["present", "configure"]: 71 | meta = json.dumps(response.content) 72 | has_changed = response.status_code == 201 73 | 74 | if state == "absent": 75 | meta = {} 76 | has_changed = response.status_code == 204 77 | if state == "list": 78 | meta = response.json() 79 | has_changed = False 80 | 81 | return (has_changed, meta) 82 | 83 | def main(): 84 | 85 | helper = ModuleHelper() 86 | 87 | global module # might not need this 88 | module = helper.get_module() 89 | base_url, username, id, state, api_name, data, auth_user, auth_password = helper.prepare_inputs(module) 90 | 91 | api = KongConsumer(base_url, auth_user, auth_password) 92 | if state == "present": 93 | response = api.add(username, id) 94 | if state == "absent": 95 | response = api.delete(username) 96 | if state == "configure": 97 | response = api.configure_for_plugin(username, api_name, data) 98 | if state == "list": 99 | response = api.list() 100 | 101 | if response.status_code == 401: 102 | module.fail_json(msg="Please specify kong_admin_username and kong_admin_password", meta=response.json()) 103 | elif response.status_code == 403: 104 | module.fail_json(msg="Please check kong_admin_username and kong_admin_password", meta=response.json()) 105 | else: 106 | has_changed, meta = helper.get_response(response, state) 107 | module.exit_json(changed=has_changed, meta=meta) 108 | 109 | 110 | from ansible.module_utils.basic import * 111 | from ansible.module_utils.urls import * 112 | 113 | if __name__ == '__main__': 114 | main() -------------------------------------------------------------------------------- /library/kong_plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import requests 4 | 5 | class KongPlugin: 6 | 7 | def __init__(self, base_url, api_name, auth_username=None, auth_password=None): 8 | self.base_url = "{}/apis/{}/plugins" . format(base_url, api_name) 9 | if auth_username is not None and auth_password is not None: 10 | self.auth = (auth_username, auth_password) 11 | else: 12 | self.auth = None 13 | self.api = api_name 14 | 15 | def list(self): 16 | 17 | return requests.get(self.base_url, auth=self.auth) 18 | 19 | def _get_plugin_id(self, name, plugins_list): 20 | """Scans the list of plugins for an ID. 21 | returns None if no matching name is found""" 22 | 23 | for plugin in plugins_list: 24 | if plugin.get("name") == name: 25 | return plugin.get("id") 26 | 27 | return None 28 | 29 | def add_or_update(self, name, config=None): 30 | 31 | # does it exist already? 32 | plugins_response = self.list() 33 | plugins_list = plugins_response.json().get('data', []) 34 | 35 | data = { 36 | "name": name, 37 | } 38 | if config is not None: 39 | data.update(config) 40 | 41 | plugin_id = self._get_plugin_id(name, plugins_list) 42 | if plugin_id is None: 43 | return requests.post(self.base_url, data, auth=self.auth) 44 | else: 45 | url = "{}/{}" . format (self.base_url, plugin_id) 46 | return requests.patch(url, data, auth=self.auth) 47 | 48 | def delete(self, id): 49 | 50 | url = "{}/{}" . format (self.base_url, id) 51 | return requests.delete(url, auth=self.auth) 52 | 53 | 54 | class ModuleHelper: 55 | 56 | def get_module(self): 57 | 58 | args = dict( 59 | kong_admin_uri = dict(required=True, type='str'), 60 | kong_admin_username = dict(required=False, type='str'), 61 | kong_admin_password = dict(required=False, type='str'), 62 | api_name = dict(required=False, type='str'), 63 | plugin_name = dict(required=False, type='str'), 64 | plugin_id = dict(required=False, type='str'), 65 | config = dict(required=False, type='dict'), 66 | state = dict(required=False, default="present", choices=['present', 'absent', 'list'], type='str'), 67 | ) 68 | return AnsibleModule(argument_spec=args,supports_check_mode=False) 69 | 70 | def prepare_inputs(self, module): 71 | url = module.params['kong_admin_uri'] 72 | auth_user = module.params['kong_admin_username'] 73 | auth_password = module.params['kong_admin_password'] 74 | api_name = module.params['api_name'] 75 | state = module.params['state'] 76 | data = { 77 | "name": module.params['plugin_name'], 78 | "config": module.params['config'] 79 | } 80 | 81 | return (url, api_name, data, state, auth_user, auth_password) 82 | 83 | def get_response(self, response, state): 84 | 85 | if state == "present": 86 | meta = json.dumps(response.content) 87 | has_changed = response.status_code == 201 88 | 89 | if state == "absent": 90 | meta = {} 91 | has_changed = response.status_code == 204 92 | 93 | if state == "list": 94 | meta = response.json() 95 | has_changed = False 96 | 97 | return (has_changed, meta) 98 | 99 | 100 | def main(): 101 | 102 | state_to_method = { 103 | "present": "add", 104 | "absent": "delete" 105 | } 106 | helper = ModuleHelper() 107 | 108 | global module # might not need this 109 | module = helper.get_module() 110 | base_url, api_name, data, state, auth_user, auth_password = helper.prepare_inputs(module) 111 | 112 | method_to_call = state_to_method.get(state) 113 | 114 | api = KongPlugin(base_url, api_name, auth_user, auth_password) 115 | if state == "present": 116 | response = api.add_or_update(**data) 117 | if state == "absent": 118 | response = api.delete(module.params['plugin_id']) 119 | if state == "list": 120 | response = api.list() 121 | 122 | if response.status_code == 401: 123 | module.fail_json(msg="Please specify kong_admin_username and kong_admin_password", meta=response.json()) 124 | elif response.status_code == 403: 125 | module.fail_json(msg="Please check kong_admin_username and kong_admin_password", meta=response.json()) 126 | else: 127 | has_changed, meta = helper.get_response(response, state) 128 | module.exit_json(changed=has_changed, meta=meta) 129 | 130 | 131 | from ansible.module_utils.basic import * 132 | from ansible.module_utils.urls import * 133 | 134 | if __name__ == '__main__': 135 | main() -------------------------------------------------------------------------------- /library/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | responses 3 | mock 4 | nose 5 | sniffer 6 | ansible 7 | coverage 8 | -------------------------------------------------------------------------------- /library/test_kong.py: -------------------------------------------------------------------------------- 1 | import unittest, responses, json, mock, requests 2 | from urlparse import parse_qsl, parse_qs 3 | from kong_api import KongAPI, ModuleHelper, main 4 | from ansible.module_utils.basic import AnsibleModule 5 | 6 | mock_kong_admin_url = "http://192.168.99.100:8001" 7 | 8 | class ModuleHelperTestCase(unittest.TestCase): 9 | 10 | def setUp(self): 11 | class MockModule: 12 | pass 13 | fields = [ 14 | "name", 15 | "upstream_url", 16 | "request_host", 17 | "request_path", 18 | "strip_request_path", 19 | "preserve_host" 20 | ] 21 | self.helper = ModuleHelper(fields) 22 | self.module = MockModule() 23 | self.module.params = { 24 | "kong_admin_uri": mock_kong_admin_url, 25 | "kong_admin_username": None, 26 | "kong_admin_password": None, 27 | "name": "mockbin", 28 | "request_host": "mockbin.com", 29 | "request_path": "/mockbin", 30 | "strip_request_path": True, 31 | "preserve_host": False, 32 | "upstream_url": "http://mockbin.com", 33 | "state": "present" 34 | } 35 | 36 | def test_prepare_inputs(self): 37 | 38 | url, data, state, auth_user, auth_password = self.helper.prepare_inputs(self.module) 39 | 40 | assert url == mock_kong_admin_url 41 | assert state == "present" 42 | for field in self.helper.fields: 43 | value = data.get(field, None) 44 | assert value is not None, \ 45 | "Expect field {} to be set. Actual value was {}" . format (field, value) 46 | 47 | 48 | 49 | class MainTestCase(unittest.TestCase): 50 | 51 | def setUp(self): 52 | class MockModule: 53 | pass 54 | 55 | fields = [ 56 | 'name', 57 | 'upstream_url', 58 | 'request_host', 59 | 'request_path', 60 | 'strip_request_path', 61 | 'preserve_host' 62 | ] 63 | 64 | self.helper = ModuleHelper(fields) 65 | self.module = MockModule() 66 | self.module.params = { 67 | "kong_admin_uri": "http://192.168.99.100:8001", 68 | "name":"mockbin", 69 | "upstream_url":"http://mockbin.com", 70 | "request_host" : "mockbin.com" 71 | } 72 | 73 | @mock.patch.object(ModuleHelper, 'get_response') 74 | @mock.patch.object(AnsibleModule, 'exit_json') 75 | @mock.patch.object(KongAPI, 'add_or_update') 76 | @mock.patch.object(ModuleHelper, 'get_module') 77 | @mock.patch.object(ModuleHelper, 'prepare_inputs') 78 | def test_main_add(self, mock_prepare_inputs, mock_module, mock_add, mock_exit_json, mock_get_response): 79 | 80 | mock_prepare_inputs.return_value = (mock_kong_admin_url, {}, "present", None, None) 81 | mock_get_response.return_value = (True, {}) 82 | main() 83 | 84 | assert mock_add.called 85 | 86 | @mock.patch.object(ModuleHelper, 'get_response') 87 | @mock.patch.object(AnsibleModule, 'exit_json') 88 | @mock.patch.object(KongAPI, 'delete_by_name') 89 | @mock.patch.object(ModuleHelper, 'get_module') 90 | @mock.patch.object(ModuleHelper, 'prepare_inputs') 91 | def test_main_delete(self, mock_prepare_inputs, mock_module, mock_delete, mock_exit_json, mock_get_response): 92 | 93 | mock_prepare_inputs.return_value = (mock_kong_admin_url, {}, "absent", None, None) 94 | mock_get_response.return_value = (True, {}) 95 | main() 96 | 97 | assert mock_delete.called 98 | 99 | @mock.patch.object(ModuleHelper, 'get_response') 100 | @mock.patch.object(AnsibleModule, 'exit_json') 101 | @mock.patch.object(KongAPI, 'list') 102 | @mock.patch.object(ModuleHelper, 'get_module') 103 | @mock.patch.object(ModuleHelper, 'prepare_inputs') 104 | def test_main_add(self, mock_prepare_inputs, mock_module, mock_list, mock_exit_json, mock_get_response): 105 | 106 | mock_prepare_inputs.return_value = (mock_kong_admin_url, {}, "list", None, None) 107 | mock_get_response.return_value = (True, {}) 108 | main() 109 | 110 | assert mock_list.called 111 | 112 | 113 | 114 | class KongAPITestCase(unittest.TestCase): 115 | 116 | def setUp(self): 117 | self.api = KongAPI(mock_kong_admin_url) 118 | 119 | def test__api_exists(self): 120 | api_list = [ 121 | {"name": "foo"}, 122 | {"name": "bar"}, 123 | ] 124 | exists = self.api._api_exists("foo", api_list) 125 | assert exists == True 126 | 127 | def test__api_doesnt_exist(self): 128 | api_list = [ 129 | {"name": "foo"}, 130 | {"name": "bar"}, 131 | ] 132 | exists = self.api._api_exists("baz", api_list) 133 | assert exists == False 134 | 135 | @responses.activate 136 | def test_api_add_new(self): 137 | 138 | api_list = {'data': [ 139 | {"name": "foo"}, 140 | {"name": "bar"}, 141 | ]} 142 | expected_url = '{}/apis' . format (mock_kong_admin_url) 143 | responses.add(responses.GET, expected_url, status=201, body=json.dumps(api_list)) 144 | 145 | expected_url = '{}/apis/' . format (mock_kong_admin_url) 146 | responses.add(responses.POST, expected_url, status=201) 147 | 148 | request_data = { 149 | "name":"mockbin", 150 | "upstream_url":"http://mockbin.com", 151 | "request_host" : "mockbin.com", 152 | "request_path" : "/mockbin" 153 | } 154 | response = self.api.add_or_update(**request_data) 155 | 156 | assert response.status_code == 201 157 | 158 | data = parse_qs(responses.calls[1].request.body) 159 | expected_keys = ['name', 'upstream_url', 'request_host', 'request_path', 'strip_request_path', 'preserve_host'] 160 | for key in expected_keys: 161 | assert data.get(key, None) is not None, \ 162 | "Expect all required data to have been sent. What was actually sent: {}" . format (data) 163 | 164 | @responses.activate 165 | def test_api_add_update(self): 166 | 167 | api_list = {'data': [ 168 | {"name": "foo"}, 169 | {"name": "bar"}, 170 | {"name": "mockbin"} 171 | ]} 172 | expected_url = '{}/apis' . format (mock_kong_admin_url) 173 | responses.add(responses.GET, expected_url, status=201, body=json.dumps(api_list)) 174 | 175 | expected_url = '{}/apis/mockbin' . format (mock_kong_admin_url) 176 | responses.add(responses.PATCH, expected_url, status=201) 177 | 178 | request_data = { 179 | "name":"mockbin", 180 | "upstream_url":"http://mockbin.com", 181 | "request_host" : "mockbin.com", 182 | "request_path" : "/mockbin" 183 | } 184 | response = self.api.add_or_update(**request_data) 185 | 186 | assert response.status_code == 201 187 | 188 | data = parse_qs(responses.calls[1].request.body) 189 | expected_keys = ['name', 'upstream_url', 'request_host', 'request_path', 'strip_request_path', 'preserve_host'] 190 | for key in expected_keys: 191 | assert data.get(key, None) is not None, \ 192 | "Expect all required data to have been sent. What was actually sent: {}" . format (data) 193 | 194 | @responses.activate 195 | def test_list_apis(self): 196 | expected_url = '{}/apis' . format (mock_kong_admin_url) 197 | responses.add(responses.GET, expected_url, status=200) 198 | 199 | response = self.api.list() 200 | assert response.status_code == 200 201 | 202 | @responses.activate 203 | def test_api_info(self): 204 | expected_url = '{}/apis/123' . format (mock_kong_admin_url) 205 | responses.add(responses.GET, expected_url, status=200) 206 | response = self.api.info("123") 207 | 208 | assert response.status_code == 200 209 | 210 | @responses.activate 211 | def test_api_delete(self): 212 | 213 | expected_url = '{}/apis/123' . format (mock_kong_admin_url) 214 | responses.add(responses.DELETE, expected_url, status=204) 215 | response = self.api.delete("123") 216 | 217 | assert response.status_code == 204 218 | 219 | @responses.activate 220 | def test_api_delete_by_name(self): 221 | 222 | expected_get_url = '{}/apis/mockbin' . format (mock_kong_admin_url) 223 | get_body_response = { 224 | "upstream_url": "https://api.github.com", 225 | "id": "123", 226 | "name": "Github", 227 | "created_at": 1454348543000, 228 | "request_host": "github.com" 229 | } 230 | expected_del_url = '{}/apis/123' . format (mock_kong_admin_url) 231 | responses.add(responses.GET, expected_get_url, status=200, body=json.dumps(get_body_response)) 232 | responses.add(responses.DELETE, expected_del_url, status=204) 233 | 234 | response = self.api.delete_by_name("mockbin") 235 | 236 | assert response.status_code == 204, \ 237 | "Expect 204 DELETED response. Got: {}: {}" . format (response.status_code, response.content) 238 | 239 | 240 | class IntegrationTests(unittest.TestCase): 241 | 242 | def setUp(self): 243 | self.api = KongAPI("http://192.168.99.100:8001") 244 | 245 | @unittest.skip("integration") 246 | def test_add_api(self): 247 | 248 | request_data = { 249 | "name":"mockbin", 250 | "upstream_url":"http://mockbin.com", 251 | "request_host" : "mockbin.com", 252 | "request_path" : "/mockbin", 253 | "strip_request_path": True 254 | } 255 | response = self.api.add_or_update(**request_data) 256 | import pdb;pdb.set_trace() 257 | assert response.status_code in [201, 409], \ 258 | "Expect status 201 Created. Got: {}: {}" . format (response.status_code, response.content) 259 | 260 | if __name__ == '__main__': 261 | unittest.main() -------------------------------------------------------------------------------- /library/test_kong_consumer.py: -------------------------------------------------------------------------------- 1 | import unittest, responses, requests, json, mock 2 | from mock import call 3 | from urlparse import parse_qsl, parse_qs 4 | from kong_consumer import KongConsumer, ModuleHelper, main 5 | 6 | from ansible.module_utils.basic import * 7 | 8 | # python3 compatible imports: 9 | from six.moves.urllib.parse import parse_qsl, parse_qs 10 | 11 | 12 | 13 | mock_kong_admin_url = "http://192.168.99.100:8001" 14 | 15 | class KongPluginTestCase(unittest.TestCase): 16 | 17 | def setUp(self): 18 | self.api = KongConsumer(mock_kong_admin_url) 19 | 20 | @responses.activate 21 | def test_list(self): 22 | expected_url = "{}/consumers" . format(mock_kong_admin_url) 23 | responses.add(responses.GET, expected_url) 24 | response = self.api.list() 25 | 26 | @responses.activate 27 | def test_delete(self): 28 | 29 | expected_url = "{}/consumers/{}" . format(mock_kong_admin_url, 123) 30 | responses.add(responses.DELETE, expected_url, status=204) 31 | response = self.api.delete(123) 32 | 33 | assert response.status_code == 204 34 | 35 | @responses.activate 36 | def test_add(self): 37 | 38 | expected_url = "{}/consumers" . format(mock_kong_admin_url) 39 | responses.add(responses.POST, expected_url, status=201) 40 | 41 | response = self.api.add(username='joesoap') 42 | assert response.status_code == 201 43 | 44 | 45 | @responses.activate 46 | def test_configure_for_plugin(self): 47 | 48 | expected_url = "{}/consumers/joe/auth-key" . format (mock_kong_admin_url) 49 | responses.add(responses.POST, expected_url, status=201) 50 | 51 | data = { "key": "123" } 52 | response = self.api.configure_for_plugin("joe", "auth-key", data) 53 | 54 | assert response.status_code == 201 55 | 56 | body = parse_qs(responses.calls[0].request.body) 57 | body_exactly = parse_qsl(responses.calls[0].request.body) 58 | assert body['key'][0] == "123", \ 59 | "Expect correct. data to be sent. Got: {}" . format (body_exactly) 60 | 61 | @responses.activate 62 | def test_add_invalid_inputs(self): 63 | self.assertRaises(AssertionError, self.api.add) 64 | 65 | class ModuleHelperTestCase(unittest.TestCase): 66 | 67 | def setUp(self): 68 | self.helper = ModuleHelper() 69 | 70 | @mock.patch.object(ModuleHelper, 'get_response') 71 | @mock.patch.object(AnsibleModule, 'exit_json') 72 | @mock.patch.object(KongConsumer, 'add') 73 | @mock.patch.object(ModuleHelper, 'get_module') 74 | @mock.patch.object(ModuleHelper, 'prepare_inputs') 75 | def test_main_add(self, mock_prepare_inputs, mock_module, mock_add, mock_exit_json, mock_get_response): 76 | 77 | mock_prepare_inputs.return_value = (mock_kong_admin_url, "1","joesoap", "present", None, None, None, None) 78 | mock_get_response.return_value = (True, requests.Response()) 79 | main() 80 | 81 | assert mock_add.called 82 | 83 | @mock.patch.object(ModuleHelper, 'get_response') 84 | @mock.patch.object(AnsibleModule, 'exit_json') 85 | @mock.patch.object(KongConsumer, 'delete') 86 | @mock.patch.object(ModuleHelper, 'get_module') 87 | @mock.patch.object(ModuleHelper, 'prepare_inputs') 88 | def test_main_delete(self, mock_prepare_inputs, mock_module, mock_delete, mock_exit_json, mock_get_response): 89 | 90 | mock_prepare_inputs.return_value = (mock_kong_admin_url, "1","joesoap", "absent", None, None, None, None) 91 | mock_get_response.return_value = (True, requests.Response()) 92 | main() 93 | 94 | assert mock_delete.called 95 | 96 | @mock.patch.object(ModuleHelper, 'get_response') 97 | @mock.patch.object(AnsibleModule, 'exit_json') 98 | @mock.patch.object(KongConsumer, 'list') 99 | @mock.patch.object(ModuleHelper, 'get_module') 100 | @mock.patch.object(ModuleHelper, 'prepare_inputs') 101 | def test_main_list(self, mock_prepare_inputs, mock_module, mock_list, mock_exit_json, mock_get_response): 102 | 103 | mock_prepare_inputs.return_value = (mock_kong_admin_url, "1","joesoap", "list", None, None, None, None) 104 | mock_get_response.return_value = (True, requests.Response()) 105 | main() 106 | 107 | assert mock_list.called 108 | 109 | @mock.patch.object(ModuleHelper, 'get_response') 110 | @mock.patch.object(AnsibleModule, 'exit_json') 111 | @mock.patch.object(KongConsumer, 'configure_for_plugin') 112 | @mock.patch.object(ModuleHelper, 'get_module') 113 | @mock.patch.object(ModuleHelper, 'prepare_inputs') 114 | def test_main_list(self, mock_prepare_inputs, mock_module, mock_configure_for_plugin, mock_exit_json, mock_get_response): 115 | 116 | mock_prepare_inputs.return_value = (mock_kong_admin_url, "1","joesoap", "configure", "auth-key", {"key": "123"}, None, None) 117 | mock_get_response.return_value = (True, requests.Response()) 118 | main() 119 | 120 | assert mock_configure_for_plugin.called 121 | 122 | expected_call = call('1', 'auth-key', {'key': '123'}) 123 | assert mock_configure_for_plugin.call_args_list[0] == expected_call 124 | 125 | 126 | def test_prepare_inputs(self): 127 | class MockModule: 128 | pass 129 | 130 | mock_module = MockModule() 131 | mock_module.params = { 132 | 'kong_admin_uri': mock_kong_admin_url, 133 | 'kong_admin_username': None, 134 | 'kong_admin_password': None, 135 | 'state': 'present', 136 | 'username': 'joesoap', 137 | } 138 | url, username, id, state, api_name, data, auth_username, auth_password = self.helper.prepare_inputs(mock_module) 139 | 140 | assert url == mock_kong_admin_url 141 | assert state == 'present' 142 | assert username == 'joesoap' 143 | assert id == None 144 | assert data is None 145 | 146 | 147 | 148 | if __name__ == '__main__': 149 | unittest.main() -------------------------------------------------------------------------------- /library/test_kong_plugin.py: -------------------------------------------------------------------------------- 1 | import unittest, responses, requests, json, mock 2 | from urlparse import parse_qsl, parse_qs 3 | from kong_plugin import KongPlugin, ModuleHelper, main 4 | 5 | from ansible.module_utils.basic import * 6 | 7 | 8 | mock_kong_admin_url = "http://192.168.99.100:8001" 9 | 10 | class KongPluginTestCase(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.api = KongPlugin(mock_kong_admin_url, "mockbin") 14 | 15 | @responses.activate 16 | def test_plugin_add_new(self): 17 | 18 | no_plugin_response = {"data":[]} 19 | expected_url = '{}/apis/mockbin/plugins' . format (mock_kong_admin_url) 20 | responses.add(responses.GET, expected_url, status=201, body=json.dumps(no_plugin_response)) 21 | 22 | expected_url = "{}/apis/mockbin/plugins" . format (mock_kong_admin_url) 23 | responses.add(responses.POST, expected_url, status=201) 24 | 25 | example_data = { 26 | "name":"request-transformer", 27 | "config": { 28 | "config.add.headers":"x-new-header:some_value, x-another-header:some_value", 29 | "config.add.querystring":"new-param:some_value, another-param:some_value", 30 | "config.add.form":"new-form-param:some_value, another-form-param:some_value", 31 | "config.remove.headers":"x-toremove, x-another-one", 32 | "config.remove.querystring":"param-toremove, param-another-one", 33 | "config.remove.form":"formparam-toremove, formparam-another-one" 34 | } 35 | } 36 | response = self.api.add_or_update(**example_data) 37 | 38 | assert response.status_code == 201, \ 39 | "Expect 201 Created, got: {}: {}" . format (response.status_code, response.content) 40 | 41 | def test__get_plugin_id(self): 42 | 43 | plugins_list = [ 44 | {"name":"needle", "id": 123}, 45 | {"name":"request-transformer"} 46 | ] 47 | 48 | plugin_id = self.api._get_plugin_id("needle", plugins_list) 49 | assert plugin_id == 123, \ 50 | 'Expect the correct plugin_id to be returned. Expected 123. Got: {}' . format (plugin_id) 51 | 52 | def test__get_plugin_id_plugin_doesnt_exist(self): 53 | 54 | plugins_list = [ 55 | {"name":"haystack"}, 56 | {"name":"request-transformer"} 57 | ] 58 | 59 | plugin = self.api._get_plugin_id("needle", plugins_list) 60 | assert plugin is None, \ 61 | 'Expect it to return None if no plugin is found. Expected None. Got: {}' . format (plugin) 62 | 63 | 64 | @responses.activate 65 | def test_plugin_update(self): 66 | example_response = {"data":[ 67 | {"id":"1", "name":"basic-auth"}, 68 | {"id":"2", "name":"request-transformer"} 69 | ] 70 | } 71 | 72 | expected_url = "{}/apis/mockbin/plugins" . format (mock_kong_admin_url) 73 | responses.add(responses.GET, expected_url, status=200, body=json.dumps(example_response)) 74 | 75 | expected_url = "{}/apis/mockbin/plugins/1" . format (mock_kong_admin_url) 76 | responses.add(responses.PATCH, expected_url, status=200) 77 | 78 | response = self.api.add_or_update("basic-auth") 79 | 80 | assert response.status_code == 200 81 | 82 | 83 | @responses.activate 84 | def test_plugin_delete(self): 85 | 86 | id = "123" 87 | expected_url = "{}/apis/mockbin/plugins/{}" . format (mock_kong_admin_url, id) 88 | responses.add(responses.DELETE, expected_url, status=204) 89 | 90 | self.api.delete(id) 91 | 92 | class MainTestCase(unittest.TestCase): 93 | 94 | def setUp(self): 95 | class MockModule: 96 | pass 97 | 98 | self.module = MockModule 99 | self.module.params = { 100 | "kong_admin_uri": mock_kong_admin_url, 101 | "state": "present", 102 | "api_name": "mockbin", 103 | "plugin_name": "request-transformer", 104 | "config": { 105 | "foo": "bar" 106 | } 107 | } 108 | 109 | @mock.patch.object(ModuleHelper, 'get_response') 110 | @mock.patch.object(AnsibleModule, 'exit_json') 111 | @mock.patch.object(KongPlugin, 'add_or_update') 112 | @mock.patch.object(ModuleHelper, 'get_module') 113 | @mock.patch.object(ModuleHelper, 'prepare_inputs') 114 | def test_main_present(self, mock_prepare_inputs, mock_module, mock_add_or_update, mock_exit_json, mock_get_response): 115 | 116 | mock_prepare_inputs.return_value = ("","mockbin", {}, "present", None, None) 117 | mock_get_response.return_value = (True, requests.Response()) 118 | main() 119 | 120 | assert mock_add_or_update.called 121 | 122 | @mock.patch.object(ModuleHelper, 'get_response') 123 | @mock.patch.object(AnsibleModule, 'exit_json') 124 | @mock.patch.object(KongPlugin, 'delete') 125 | @mock.patch.object(ModuleHelper, 'get_module') 126 | @mock.patch.object(ModuleHelper, 'prepare_inputs') 127 | def test_main_delete(self, mock_prepare_inputs, mock_module, mock_delete, mock_exit_json, mock_get_response): 128 | 129 | mock_prepare_inputs.return_value = ("","mockbin", {}, "absent", None, None) 130 | mock_get_response.return_value = (True, requests.Response()) 131 | main() 132 | 133 | assert mock_delete.called 134 | 135 | @mock.patch.object(ModuleHelper, 'get_response') 136 | @mock.patch.object(AnsibleModule, 'exit_json') 137 | @mock.patch.object(KongPlugin, 'list') 138 | @mock.patch.object(ModuleHelper, 'get_module') 139 | @mock.patch.object(ModuleHelper, 'prepare_inputs') 140 | def test_main_list(self, mock_prepare_inputs, mock_module, mock_list, mock_exit_json, mock_get_response): 141 | 142 | mock_prepare_inputs.return_value = ("","mockbin", {}, "list", None, None) 143 | mock_get_response.return_value = (True, requests.Response()) 144 | main() 145 | 146 | assert mock_list.called 147 | 148 | 149 | @unittest.skip("..") 150 | def test_prepare_inputs(self): 151 | 152 | base_url, api_name, data, state = prepare_inputs(self.module) 153 | 154 | assert api_name == 'mockbin', 'Expect api_name to be mockbin. Got: {}' . format (api_name) 155 | assert base_url == mock_kong_admin_url 156 | assert state == "present" 157 | 158 | expected_keys = ['name', 'config'] 159 | for expected_key in expected_keys: 160 | assert data.get(expected_key, None) is not None 161 | 162 | assert data.get("config") == {"foo": "bar"} 163 | 164 | def test_handle_response_present_201(self): 165 | 166 | mock_response = requests.Response() 167 | mock_response.status_code = 201 168 | 169 | has_changed, meta = ModuleHelper().get_response(mock_response, "present") 170 | 171 | assert has_changed == True 172 | 173 | def test_handle_response_present_not_201(self, ): 174 | 175 | mock_response = requests.Response() 176 | mock_response.status_code = 409 177 | 178 | has_changed, meta = ModuleHelper().get_response(mock_response, "present") 179 | 180 | assert has_changed == False 181 | 182 | def test_handle_response_absent_204(self): 183 | 184 | mock_response = requests.Response() 185 | mock_response.status_code = 204 186 | 187 | has_changed, meta = ModuleHelper().get_response(mock_response, "absent") 188 | 189 | assert has_changed == True 190 | 191 | 192 | def test_handle_response_absent_not_204(self): 193 | 194 | mock_response = requests.Response() 195 | mock_response.status_code = 409 196 | 197 | has_changed, meta = ModuleHelper().get_response(mock_response, "absent") 198 | 199 | assert has_changed == False 200 | 201 | 202 | if __name__ == '__main__': 203 | unittest.main() -------------------------------------------------------------------------------- /playbook.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | vars: 3 | - kong_admin_base_url: "http://192.168.99.100:8001" 4 | - kong_base_url: "http://192.168.99.100:8000" 5 | - kong_consumers: 6 | - username: Jason 7 | key: 123 8 | tasks: 9 | 10 | - name: Register APIs 11 | kong_api: 12 | kong_admin_uri: "{{kong_admin_base_url}}" 13 | name: "mockbin" 14 | upstream_url: "http://mockbin.com" 15 | request_host: "mockbin.com" 16 | request_path: "/mockbin" 17 | strip_request_path: yes 18 | state: present 19 | 20 | - name: Verify API was added 21 | uri: 22 | url: "{{kong_admin_base_url}}/apis/mockbin" 23 | status_code: 200 24 | 25 | - name: Add key authentication 26 | kong_plugin: 27 | kong_admin_uri: "{{kong_admin_base_url}}" 28 | api_name: "mockbin" 29 | plugin_name: "key-auth" 30 | state: present 31 | 32 | - name: Verify key auth was added 33 | uri: 34 | url: "{{kong_base_url}}/mockbin" 35 | status_code: 401 36 | 37 | - name: Add a consumer 38 | kong_consumer: 39 | kong_admin_uri: "{{kong_admin_base_url}}" 40 | username: "{{item.username}}" 41 | state: present 42 | with_items: "{{kong_consumers}}" 43 | 44 | - name: Configure consumer 45 | kong_consumer: 46 | kong_admin_uri: "{{kong_admin_base_url}}" 47 | username: "{{item.username}}" 48 | api_name: key-auth 49 | data: 50 | key: "{{item.key}}" 51 | state: configure 52 | with_items: "{{kong_consumers}}" 53 | 54 | - name: Verify consumers can access API 55 | uri: 56 | url: "{{kong_base_url}}/mockbin" 57 | HEADER_apikey: "{{item.key}}" 58 | status_code: 200 59 | with_items: "{{kong_consumers}}" --------------------------------------------------------------------------------