├── .babelrc ├── .npmignore ├── logo.png ├── bin ├── kongfig ├── kongfig-apply └── kongfig-dump ├── test-integration ├── config │ ├── global-plugin.example.yml │ ├── simple-api.example.yml │ ├── consumer.example.yml │ ├── key-auth.example.yml │ ├── key-auth-anonymous.example.yml │ └── plugin-per-consumer.example.yml ├── bugs.test.js ├── __snapshots__ │ ├── bugs.test.js.snap │ ├── plugin.test.js.snap │ └── upstream.test.js.snap ├── config.test.js ├── util.js ├── plugin.test.js ├── upstream.test.js ├── customers.test.js ├── api.test.js └── plugin-per-consumer.test.js ├── src ├── cli.js ├── kongStateLocal.js ├── reducers │ ├── index.js │ ├── plugins.js │ ├── apis.js │ ├── upstreams.js │ └── consumers.js ├── prettyConfig.js ├── diff.js ├── parsers │ └── upstreams.js ├── migrate.js ├── stateSelector.js ├── consumerCredentials.js ├── utils.js ├── configLoader.js ├── requester.js ├── logger.js ├── cli-dump.js ├── router.js ├── actions │ └── upstreams.js ├── kongState.js ├── cli-apply.js ├── adminApi.js ├── actions.js └── readKongApi.js ├── .editorconfig ├── .gitignore ├── config.yml.sample ├── CONTRIBUTING.md ├── examples ├── global-plugin.example.md ├── simple-api.example.md ├── consumer.example.md ├── key-auth.example.md ├── key-auth-anonymous.example.md ├── upstream.example.md └── plugin-per-consumer.example.md ├── .travis.yml ├── LICENSE ├── config.js.sample ├── config.json.sample ├── package.json ├── test ├── utils.js ├── plugins.js ├── diff.js ├── requester.js ├── apis.js ├── upstreams.js └── consumers.js ├── docs └── guide.md └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | .travis.yml 4 | webpack.config.js 5 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tes/kongfig/master/logo.png -------------------------------------------------------------------------------- /bin/kongfig: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("babel-polyfill"); 4 | require('../lib/cli.js'); 5 | -------------------------------------------------------------------------------- /bin/kongfig-apply: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("babel-polyfill"); 4 | require('../lib/cli-apply.js'); 5 | -------------------------------------------------------------------------------- /bin/kongfig-dump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("babel-polyfill"); 4 | require('../lib/cli-dump.js'); 5 | -------------------------------------------------------------------------------- /test-integration/config/global-plugin.example.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - name: cors 3 | attributes: 4 | config: 5 | credentials: false 6 | preflight_continue: false 7 | max_age: 7000 8 | -------------------------------------------------------------------------------- /test-integration/config/simple-api.example.yml: -------------------------------------------------------------------------------- 1 | apis: 2 | - name: "mockbin" 3 | ensure: "present" 4 | attributes: 5 | upstream_url: "http://mockbin.com" 6 | hosts: 7 | - "mockbin.com" 8 | -------------------------------------------------------------------------------- /test-integration/config/consumer.example.yml: -------------------------------------------------------------------------------- 1 | consumers: 2 | - username: "iphone-app" 3 | ensure: "present" 4 | acls: 5 | - group: "foo-group" 6 | ensure: "present" 7 | credentials: 8 | - name: "key-auth" 9 | ensure: "present" 10 | attributes: 11 | key: "very-secret-key" 12 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | import commander from 'commander'; 2 | 3 | let pkg = require("../package.json"); 4 | 5 | commander 6 | .version(pkg.version) 7 | .allowUnknownOption() 8 | .command('apply', 'Apply config to a kong server', {isDefault: true}) 9 | .command('dump', 'Dump the configuration from a kong server') 10 | .parse(process.argv); 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | # Matches multiple files with brace expansion notation 9 | # Set default charset 10 | [*.{js,jsx}] 11 | charset = utf-8 12 | indent_style = space 13 | indent_size = 4 14 | trim_trailing_whitespace = true 15 | -------------------------------------------------------------------------------- /src/kongStateLocal.js: -------------------------------------------------------------------------------- 1 | import { parseApiPostV10, parsePlugin, parseConsumer, parseAcl, parseGlobalPlugin } from './readKongApi'; 2 | import reducer from './reducers'; 3 | 4 | export const logReducer = (state = {}, log) => { 5 | if (log.type !== 'response' && log.type !== 'kong-info') { 6 | return state; 7 | } 8 | 9 | return reducer(state, log); 10 | }; 11 | -------------------------------------------------------------------------------- /test-integration/config/key-auth.example.yml: -------------------------------------------------------------------------------- 1 | apis: 2 | - name: "mockbin" 3 | ensure: "present" 4 | attributes: 5 | upstream_url: "http://mockbin.com" 6 | hosts: 7 | - "mockbin.com" 8 | plugins: 9 | - name: "key-auth" 10 | attributes: 11 | config: 12 | key_names: 13 | - very-secret-key 14 | 15 | consumers: 16 | - username: "iphone-app" 17 | ensure: "present" 18 | credentials: 19 | - name: "key-auth" 20 | ensure: "present" 21 | attributes: 22 | key: "very-secret-key" 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | # Publishing es5 in lib 31 | lib 32 | -------------------------------------------------------------------------------- /test-integration/config/key-auth-anonymous.example.yml: -------------------------------------------------------------------------------- 1 | apis: 2 | - name: "mockbin" 3 | ensure: "present" 4 | attributes: 5 | upstream_url: "http://mockbin.com" 6 | hosts: 7 | - "mockbin.com" 8 | plugins: 9 | - name: "key-auth" 10 | attributes: 11 | config: 12 | anonymous_username: anonymous-user 13 | key_names: 14 | - very-secret-key 15 | 16 | consumers: 17 | - username: "anonymous-user" 18 | ensure: "present" 19 | - username: "iphone-app" 20 | ensure: "present" 21 | credentials: 22 | - name: "key-auth" 23 | ensure: "present" 24 | attributes: 25 | key: "very-secret-key" 26 | -------------------------------------------------------------------------------- /config.yml.sample: -------------------------------------------------------------------------------- 1 | --- 2 | host: "localhost:8001" 3 | apis: 4 | - name: "mockbin" 5 | ensure: "present" 6 | attributes: 7 | upstream_url: "http://mockbin.com" 8 | hosts: 9 | - "mockbin.com" 10 | plugins: 11 | - name: "key-auth" 12 | - name: cors 13 | attributes: 14 | config: 15 | credentials: false 16 | preflight_continue: false 17 | max_age: 7000 18 | 19 | consumers: 20 | - username: "iphone-app" 21 | ensure: "present" 22 | credentials: 23 | - name: "key-auth" 24 | ensure: "present" 25 | attributes: 26 | key: "very-secret-key" 27 | 28 | - username: "ie" 29 | ensure: "removed" 30 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import apis from './apis'; 2 | import plugins from './plugins'; 3 | import consumers from './consumers'; 4 | import upstreams from './upstreams'; 5 | 6 | const combine = reducers => (state = {}, log) => { 7 | return Object.keys(reducers).reduce((nextState, key) => { 8 | nextState[key] = reducers[key](state[key], log); 9 | 10 | return nextState; 11 | }, state); 12 | }; 13 | 14 | const _info = (state = {}, log) => { 15 | const { type } = log; 16 | 17 | switch (type) { 18 | case 'kong-info': 19 | return { ...state, version: log.version }; 20 | default: return state; 21 | } 22 | } 23 | 24 | export default combine({ 25 | _info, 26 | apis, 27 | plugins, 28 | consumers, 29 | upstreams 30 | }); 31 | -------------------------------------------------------------------------------- /src/reducers/plugins.js: -------------------------------------------------------------------------------- 1 | import { parseGlobalPlugin } from '../readKongApi'; 2 | 3 | export default (state = [], log) => { 4 | if (log.type !== 'response') { 5 | return state; 6 | } 7 | 8 | const { params: { type, endpoint: { params } }, content } = log; 9 | 10 | switch (type) { 11 | case 'add-global-plugin': return [...state, parseGlobalPlugin(content)]; 12 | case 'update-global-plugin': return state.map(plugin => { 13 | if (plugin._info.id !== params.pluginId) { 14 | return plugin; 15 | } 16 | 17 | return parseGlobalPlugin(content); 18 | }); 19 | case 'remove-global-plugin': return state.filter(plugin => plugin._info.id !== params.pluginId); 20 | default: return state; 21 | } 22 | 23 | return state; 24 | }; 25 | -------------------------------------------------------------------------------- /test-integration/config/plugin-per-consumer.example.yml: -------------------------------------------------------------------------------- 1 | consumers: 2 | - username: user-john 3 | 4 | apis: 5 | - name: mockbin-foo 6 | attributes: 7 | upstream_url: http://mockbin.com 8 | uris: 9 | - /foo 10 | plugins: 11 | - name: rate-limiting 12 | ensure: "present" 13 | attributes: 14 | username: user-john 15 | config: 16 | second: 10 17 | 18 | - name: mockbin-bar 19 | attributes: 20 | upstream_url: http://mockbin.com 21 | uris: 22 | - /bar 23 | 24 | plugins: 25 | - name: rate-limiting 26 | attributes: 27 | username: user-john 28 | enabled: true 29 | config: 30 | minute: 60 31 | 32 | - name: rate-limiting 33 | attributes: 34 | enabled: true 35 | config: 36 | minute: 30 37 | -------------------------------------------------------------------------------- /test-integration/bugs.test.js: -------------------------------------------------------------------------------- 1 | import execute from '../lib/core'; 2 | import { testAdminApi, logger, exportToYaml, getLog, getLocalState, tearDown } from './util'; 3 | import readKongApi from '../lib/readKongApi'; 4 | 5 | beforeEach(tearDown); 6 | 7 | it('should allow updating a global plugin with no attributes', async () => { 8 | const config = { 9 | plugins: [{ 10 | name: "cors", 11 | attributes: { 12 | enabled: true 13 | } 14 | }] 15 | }; 16 | 17 | await execute(config, testAdminApi, logger); 18 | await execute(config, testAdminApi, logger); 19 | const kongState = await readKongApi(testAdminApi); 20 | 21 | expect(getLog()).toMatchSnapshot(); 22 | expect(exportToYaml(kongState)).toMatchSnapshot(); 23 | expect(getLocalState()).toEqual(kongState); 24 | }); 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Kongfig 2 | 3 | We are very grateful for any contributions you can make to the project. 4 | 5 | ## Submitting a Pull Request 6 | 7 | Before submitting your request, we kindly ask you to: 8 | 9 | - Include tests related to your changes 10 | - Consider squashing your commits, so as to keep the history clean and succinct 11 | 12 | 13 | ## Setup 14 | 15 | To compile the `kongfig` you need babel-cli 16 | 17 | ```bash 18 | npm install --global babel-cli 19 | ``` 20 | 21 | Install the package dependencies 22 | 23 | ```bash 24 | npm install 25 | ``` 26 | 27 | ## Running tests 28 | 29 | ```bash 30 | npm test 31 | ``` 32 | 33 | or even more conveniently you can run the test continuously on file changes. 34 | 35 | ```bash 36 | npm test -- --watch 37 | ``` 38 | 39 | ## Compiling bin/kongfig 40 | 41 | ```bash 42 | npm run build 43 | ``` 44 | 45 | or compile continuously on file changes. 46 | 47 | ```bash 48 | npm run build -- --watch 49 | ``` 50 | -------------------------------------------------------------------------------- /examples/global-plugin.example.md: -------------------------------------------------------------------------------- 1 | global plugin example 2 | --------------------- 3 | 4 | ## Config file 5 | 6 | ```yaml 7 | plugins: 8 | - name: cors 9 | attributes: 10 | config: 11 | credentials: false 12 | preflight_continue: false 13 | max_age: 7000 14 | 15 | ``` 16 | 17 | ## Using curl 18 | 19 | For illustrative purpose a cURL calls would be the following 20 | 21 | ### add global plugin 22 | 23 | ```sh 24 | $ curl -i -X POST -H "Content-Type: application/json" \ 25 | --url http://localhost:8001/plugins \ 26 | --data '{"config":{"credentials":false,"preflight_continue":false,"max_age":7000},"name":"cors"}' 27 | ``` 28 | 29 | ``` 30 | HTTP 201 Created 31 | ``` 32 | 33 | ``` 34 | { 35 | "created_at": "___created_at___", 36 | "config": { 37 | "credentials": false, 38 | "max_age": 7000, 39 | "preflight_continue": false 40 | }, 41 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 42 | "enabled": true, 43 | "name": "cors" 44 | } 45 | ``` -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | language: node_js 5 | env: 6 | global: 7 | - TEST_INTEGRATION_KONG_HOST="127.0.0.1:8001" 8 | - KONG_VERSION=latest 9 | matrix: 10 | - EXPERIMENTAL_USE_LOCAL_STATE=0 11 | - EXPERIMENTAL_USE_LOCAL_STATE=1 12 | node_js: 13 | - "stable" 14 | - "4" 15 | - "6" 16 | before_install: 17 | - docker run -d --name kong-database -e "POSTGRES_USER=kong" -e "POSTGRES_DB=kong" postgres:9.4 18 | - sleep 5 19 | - docker run --rm --link kong-database:kong-database -e "KONG_DATABASE=postgres" -e "KONG_PG_HOST=kong-database" kong:latest kong migrations up 20 | - sleep 5 21 | - docker run -d --name kong --link kong-database:kong-database -e "KONG_DATABASE=postgres" -e "KONG_PG_HOST=kong-database" -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" -e "KONG_PROXY_ERROR_LOG=/dev/stderr" -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" -p 127.0.0.1:8001:8001 -p 127.0.0.1:8000:8000 kong:$KONG_VERSION 22 | - sleep 5 23 | - curl localhost:8001 24 | -------------------------------------------------------------------------------- /src/prettyConfig.js: -------------------------------------------------------------------------------- 1 | import prettyjson from 'prettyjson'; 2 | import yaml from 'js-yaml'; 3 | 4 | export function pretty(format) { 5 | switch (format) { 6 | case 'json': return config => prettyJson(removeInfo(config)); 7 | case 'yaml': return config => prettyYaml(removeInfo(config)); 8 | case 'yml': return config => prettyYaml(removeInfo(config)); 9 | case 'screen': return prettyScreen; 10 | default: 11 | throw new Error('Unknown --format ' + format); 12 | } 13 | } 14 | 15 | export function prettyScreen(config) { 16 | return prettyjson.render(config, {}); 17 | } 18 | 19 | export function prettyJson(config) { 20 | return JSON.stringify(config, null, ' '); 21 | } 22 | 23 | export function prettyYaml(config) { 24 | return yaml.safeDump(config); 25 | } 26 | 27 | export function removeInfo(config) { 28 | return JSON.parse(JSON.stringify(config, (key, value) => { 29 | if (key == '_info') { 30 | return undefined; 31 | } 32 | 33 | return value; 34 | })); 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017 MyBuilder Limited 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/diff.js: -------------------------------------------------------------------------------- 1 | const isValueSameOneArrayElement = (a, b) => { 2 | return typeof a === 'string' 3 | && Array.isArray(b) 4 | && b.length === 1 5 | && !isValueDifferent(a, b[0]); 6 | }; 7 | 8 | const isValueDifferent = (a, b) => { 9 | if (Array.isArray(a)) { 10 | return !Array.isArray(b) 11 | || a.length != b.length 12 | || a.filter(x => b.indexOf(x) === -1).length > 0; 13 | } 14 | 15 | return JSON.stringify(a) !== JSON.stringify(b); 16 | } 17 | 18 | export default (defined = {}, server = {}) => { 19 | const keys = Object.keys(defined); 20 | 21 | return keys.reduce((changed, key) => { 22 | if (key === 'redirect_uri') { 23 | // hack for >=0.8.2 that allows multiple redirect_uris, 24 | // but accepts a string as well 25 | if (isValueSameOneArrayElement(defined[key], server[key])) { 26 | return changed; 27 | } 28 | } 29 | 30 | if (isValueDifferent(defined[key], server[key])) { 31 | return [...changed, key]; 32 | } 33 | 34 | return changed; 35 | }, []); 36 | }; 37 | -------------------------------------------------------------------------------- /examples/simple-api.example.md: -------------------------------------------------------------------------------- 1 | simple api example 2 | ------------------ 3 | 4 | ## Config file 5 | 6 | ```yaml 7 | apis: 8 | - name: "mockbin" 9 | ensure: "present" 10 | attributes: 11 | upstream_url: "http://mockbin.com" 12 | hosts: 13 | - "mockbin.com" 14 | 15 | ``` 16 | 17 | ## Using curl 18 | 19 | For illustrative purpose a cURL calls would be the following 20 | 21 | ### create api 22 | 23 | ```sh 24 | $ curl -i -X POST -H "Content-Type: application/json" \ 25 | --url http://localhost:8001/apis \ 26 | --data '{"upstream_url":"http://mockbin.com","hosts":["mockbin.com"],"name":"mockbin"}' 27 | ``` 28 | 29 | ``` 30 | HTTP 201 Created 31 | ``` 32 | 33 | ``` 34 | { 35 | "created_at": "___created_at___", 36 | "strip_uri": true, 37 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 38 | "hosts": [ 39 | "mockbin.com" 40 | ], 41 | "name": "mockbin", 42 | "http_if_terminated": false, 43 | "preserve_host": false, 44 | "upstream_url": "http://mockbin.com", 45 | "upstream_connect_timeout": 60000, 46 | "upstream_send_timeout": 60000, 47 | "upstream_read_timeout": 60000, 48 | "retries": 5, 49 | "https_only": false 50 | } 51 | ``` -------------------------------------------------------------------------------- /src/parsers/upstreams.js: -------------------------------------------------------------------------------- 1 | 2 | export const parseUpstream = ({ 3 | name, 4 | slots, 5 | id, 6 | created_at, 7 | orderlist 8 | }) => { 9 | return { 10 | name, 11 | attributes: { 12 | slots 13 | }, 14 | _info: { 15 | id, 16 | created_at, 17 | orderlist 18 | } 19 | }; 20 | }; 21 | 22 | export const parseUpstreams = (upstreams) => { 23 | return upstreams.map(upstream => { 24 | const { name, ...rest } = parseUpstream(upstream); 25 | 26 | return { name, targets: parseUpstreamTargets(upstream.targets), ...rest }; 27 | }); 28 | }; 29 | 30 | export const parseTarget = ({ 31 | target, 32 | weight, 33 | id, 34 | upstream_id, 35 | created_at 36 | }) => { 37 | return { 38 | target, 39 | attributes: { 40 | weight 41 | }, 42 | _info: { 43 | id, 44 | upstream_id, 45 | created_at 46 | } 47 | } 48 | }; 49 | 50 | function parseUpstreamTargets(targets) { 51 | if (!Array.isArray(targets)) { 52 | return []; 53 | } 54 | 55 | return targets.map(parseTarget); 56 | } 57 | -------------------------------------------------------------------------------- /config.js.sample: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | host: "localhost:8001", 3 | apis: [ 4 | { 5 | name: "mockbin", 6 | ensure: "present", 7 | attributes: { 8 | upstream_url: "http://mockbin.com", 9 | hosts: ["mockbin.com"] 10 | }, 11 | plugins: [ 12 | { 13 | name: "key-auth" 14 | }, 15 | { 16 | name: "cors", 17 | attributes: { 18 | config: { 19 | credentials: false, 20 | preflight_continue: false, 21 | max_age: 7000 22 | } 23 | } 24 | } 25 | ] 26 | } 27 | ], 28 | consumers: [ 29 | { 30 | username: "iphone-app", 31 | ensure: "present", 32 | credentials: [ 33 | { 34 | name: "key-auth", 35 | ensure: "present", 36 | attributes: { 37 | key: "very-secret-key" 38 | } 39 | } 40 | ] 41 | }, 42 | { 43 | username: "ie", 44 | ensure: "removed" 45 | } 46 | ] 47 | }; 48 | -------------------------------------------------------------------------------- /config.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost:8001", 3 | "apis": [ 4 | { 5 | "name": "mockbin", 6 | "ensure": "present", 7 | "attributes": { 8 | "upstream_url": "http://mockbin.com", 9 | "hosts": ["mockbin.com"] 10 | }, 11 | "plugins": [ 12 | { 13 | "name": "key-auth" 14 | }, 15 | { 16 | "name": "cors", 17 | "attributes": { 18 | "config": { 19 | "credentials": false, 20 | "preflight_continue": false, 21 | "max_age": 7000 22 | } 23 | } 24 | } 25 | ] 26 | } 27 | ], 28 | "consumers": [ 29 | { 30 | "username": "iphone-app", 31 | "ensure": "present", 32 | "credentials": [ 33 | { 34 | "name": "key-auth", 35 | "ensure": "present", 36 | "attributes": { 37 | "key": "very-secret-key" 38 | } 39 | } 40 | ] 41 | }, 42 | { 43 | "username": "ie", 44 | "ensure": "removed" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/migrate.js: -------------------------------------------------------------------------------- 1 | import semVer from 'semver'; 2 | 3 | const DEFINITION_V1 = 'v1'; 4 | const DEFINITION_V2 = 'v2'; 5 | 6 | const _removeUndefined = x => JSON.parse(JSON.stringify(x)); 7 | 8 | const _guessDefinitionVersion = (api) => { 9 | if (['hosts', 'uris', 'methods'].filter(x => api.attributes.hasOwnProperty(x)).length > 0) { 10 | return DEFINITION_V2; 11 | } 12 | 13 | return DEFINITION_V1; 14 | }; 15 | 16 | const _migrateV1toV2 = (api) => { 17 | const { 18 | request_host, 19 | request_path, 20 | strip_request_path, 21 | ...oldAttributes, 22 | } = api.attributes; 23 | 24 | const newAttributes = { 25 | hosts: api.attributes.request_host ? [api.attributes.request_host] : undefined, 26 | uris: api.attributes.request_path ? [api.attributes.request_path] : undefined, 27 | strip_uri: api.attributes.strip_request_path, 28 | }; 29 | 30 | return _removeUndefined({ ...api, attributes: { ...oldAttributes, ...newAttributes }}); 31 | }; 32 | 33 | const _migrateApiDefinitionToVersion = (api, kongVersion) => { 34 | switch (_guessDefinitionVersion(api)) { 35 | case DEFINITION_V1: 36 | if (semVer.gte(kongVersion, '0.10.0')) { 37 | return _migrateV1toV2(api); 38 | } 39 | 40 | return api; 41 | default: 42 | return api; 43 | } 44 | } 45 | 46 | export const migrateApiDefinition = (api, fn) => world => fn(_migrateApiDefinitionToVersion(api, world.getVersion()), world); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kongfig", 3 | "version": "1.4.2", 4 | "description": "A tool for Kong to allow declarative configuration.", 5 | "repository": "https://github.com/mybuilder/kongfig", 6 | "bin": { 7 | "kongfig": "./bin/kongfig" 8 | }, 9 | "preferGlobal": true, 10 | "scripts": { 11 | "pretest": "yarn build", 12 | "test": "jest --runInBand", 13 | "build": "babel src --out-dir lib", 14 | "preversion": "yarn test", 15 | "version": "yarn build", 16 | "postversion": "git push && git push --tags", 17 | "publish-patch": "npm version patch && npm publish" 18 | }, 19 | "author": "MyBuilder Ltd", 20 | "license": "MIT", 21 | "dependencies": { 22 | "babel-polyfill": "^6.2.0", 23 | "colors": "^1.1.2", 24 | "commander": "^2.9.0", 25 | "invariant": "^2.2.2", 26 | "isomorphic-fetch": "^2.2.0", 27 | "js-yaml": "^3.4.6", 28 | "minimist": "^1.2.0", 29 | "object-assign": "^4.0.1", 30 | "pad": "^1.1.0", 31 | "prettyjson": "^1.1.3", 32 | "semver": "^5.3.0" 33 | }, 34 | "devDependencies": { 35 | "babel-cli": "^6.24.1", 36 | "babel-preset-es2015": "^6.1.18", 37 | "babel-preset-stage-2": "^6.1.18", 38 | "expect.js": "^0.3.1", 39 | "jest": "^20.0.4" 40 | }, 41 | "jest": { 42 | "setupFiles": [ 43 | "/node_modules/babel-polyfill/dist/polyfill.js" 44 | ], 45 | "testMatch": [ 46 | "/test/*.js", 47 | "/test-integration/*.test.js" 48 | ], 49 | "transformIgnorePatterns": [ 50 | "/node_modules/", 51 | "/lib/" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/stateSelector.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | 3 | const getConsumerById = (id, consumers) => { 4 | const consumer = consumers.find(x => x._info.id === id); 5 | 6 | invariant(consumer, `Unable to find a consumer for ${id}`); 7 | 8 | return consumer; 9 | }; 10 | 11 | export default state => { 12 | const fixPluginAnonymous = ({ name, attributes: { config, ...attributes }, ...plugin }) => { 13 | if (config && config.anonymous) { 14 | const { anonymous, ...restOfConfig } = config; 15 | const { username } = getConsumerById(anonymous, state.consumers); 16 | 17 | return { name, attributes: { ...attributes, config: { anonymous_username: username, ...restOfConfig } }, ...plugin }; 18 | } 19 | 20 | return { name, attributes: { ...attributes, config }, ...plugin }; 21 | } 22 | 23 | const fixPluginUsername = ({ name, attributes: { consumer_id, ...attributes }, ...plugin }) => { 24 | if (!consumer_id) { 25 | return { name, attributes, ...plugin }; 26 | } 27 | 28 | const { username } = getConsumerById(consumer_id, state.consumers); 29 | 30 | return { name, attributes: { username, ...attributes }, ...plugin }; 31 | }; 32 | 33 | const fixApiPluginUsername = api => ({ 34 | ...api, 35 | plugins: (api.plugins || []).map(fixPluginUsername).map(fixPluginAnonymous), 36 | }); 37 | 38 | return { 39 | ...state, 40 | apis: state.apis && state.apis.map(fixApiPluginUsername), 41 | plugins: state.plugins && state.plugins.map(fixPluginUsername), 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/consumerCredentials.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const schema = { 4 | 'oauth2': { 5 | id: 'client_id' 6 | }, 7 | 'key-auth': { 8 | id: 'key' 9 | }, 10 | 'jwt': { 11 | id: 'key' 12 | }, 13 | 'basic-auth': { 14 | id: 'username' 15 | }, 16 | 'hmac-auth': { 17 | id: 'username' 18 | } 19 | }; 20 | 21 | export function getSupportedCredentials() { 22 | return Object.keys(schema); 23 | } 24 | 25 | export function getSchema(name) { 26 | if (false === schema.hasOwnProperty(name)) { 27 | throw new Error(`Unknown credential "${name}"`); 28 | } 29 | 30 | return schema[name]; 31 | } 32 | 33 | export function addSchema(name, val){ 34 | if (schema.hasOwnProperty(name)){ 35 | throw new Error(`There is already a schema with name '${name}'`); 36 | } 37 | if (!val || !val.hasOwnProperty('id')){ 38 | throw new Error(`Credential schema ${name} should have a property named "id"`); 39 | } 40 | schema[name] = val; 41 | } 42 | 43 | export function addSchemasFromOptions(opts){ 44 | if (!opts || opts.length === 0) return; 45 | 46 | opts.forEach(val => { 47 | var vals = val.split(':'); 48 | if (vals.length != 2) { 49 | throw new Error(`Use : format in ${val}`); 50 | } 51 | addSchema(vals[0], {id: vals[1]}); 52 | }); 53 | } 54 | 55 | export function addSchemasFromConfig(config){ 56 | if (!config.credentialSchemas) return; 57 | 58 | for (let key in config.credentialSchemas){ 59 | addSchema(key, config.credentialSchemas[key]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function normalize(attr) { 2 | if (attr === null || typeof attr !== 'object' || Object.prototype.toString.call(attr) === '[object Array]') { 3 | return attr; 4 | } 5 | 6 | let mutable = {}; 7 | Object.keys(attr).forEach(key => { 8 | _setOnPath(mutable, key.split('.'), normalize(attr[key])); 9 | }); 10 | 11 | return mutable; 12 | } 13 | 14 | function _setOnPath(obj, path, value) { 15 | if (!path) { 16 | return obj; 17 | } 18 | 19 | var currentPath = path[0]; 20 | if (path.length === 1) { 21 | var oldVal = obj[currentPath]; 22 | 23 | if (oldVal === undefined) { 24 | obj[currentPath] = value; 25 | } 26 | 27 | return oldVal; 28 | } 29 | 30 | if (obj[currentPath] === undefined) { 31 | obj[currentPath] = {}; 32 | } 33 | 34 | return _setOnPath(obj[currentPath], path.slice(1), value); 35 | } 36 | 37 | export function repeatableOptionCallback(val, result) { 38 | result.push(val); 39 | return result; 40 | } 41 | 42 | export function parseVersion(version) { 43 | if (!version.includes("enterprise-edition")) { 44 | // remove any postfix, i.e., 0.11.0-rc1 should be 0.11.0 45 | return version.split("-")[0]; 46 | } 47 | 48 | // Kong EE versioning is X.Y(-Z)-enterprise-edition 49 | var vAry = version.split("-") 50 | 51 | if (vAry.length == 4) { 52 | version = vAry[0] + "." + vAry[1] 53 | } else { 54 | version = vAry[0]; 55 | } 56 | 57 | // add .0 so that kong EE has a patch version, i.e, 0.29 should be 0.29.0 58 | if (version.split(".").length == 2) { 59 | version = version + ".0" 60 | } 61 | 62 | return version 63 | } -------------------------------------------------------------------------------- /src/configLoader.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import yaml from 'js-yaml'; 4 | 5 | const log = { 6 | info: message => console.log(message.green), 7 | error: message => console.error(message.red) 8 | } 9 | 10 | export default (configPath) => { 11 | if (!fs.existsSync(configPath)) { 12 | log.error(`Supplied --path '${configPath}' doesn't exist`.red); 13 | return process.exit(1); 14 | } 15 | 16 | if(/(\.yml)|(\.yaml)/.test(configPath)) { 17 | return yaml.safeLoad(fs.readFileSync(configPath)); 18 | } 19 | 20 | if (/(\.json)/.test(configPath)) { 21 | return JSON.parse(fs.readFileSync(configPath)); 22 | } 23 | 24 | if (/(\.js)/.test(configPath)) { 25 | try { 26 | let config = require(resolvePath(configPath)); 27 | 28 | if (config === null || typeof config !== 'object' || Object.keys(config).length == 0) { 29 | log.error('Config file must export an object!\n' + CONFIG_SYNTAX_HELP); 30 | 31 | return process.exit(1); 32 | } 33 | 34 | return config; 35 | } catch (e) { 36 | if (e.code === 'MODULE_NOT_FOUND' && e.message.indexOf(configPath) !== -1) { 37 | log.error('File %s does not exist!', configPath); 38 | } else { 39 | log.error('Invalid config file!\n ' + e.stack); 40 | } 41 | 42 | return process.exit(1); 43 | } 44 | } 45 | } 46 | 47 | function resolvePath(configPath) { 48 | if (path.isAbsolute(configPath)) { 49 | return configPath; 50 | } 51 | 52 | return path.resolve(process.cwd(), configPath); 53 | } 54 | 55 | const CONFIG_SYNTAX_HELP = 56 | ' module.exports = {\n' + 57 | ' // your config\n' + 58 | ' };\n'; 59 | -------------------------------------------------------------------------------- /src/requester.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch'); 2 | 3 | let headers = {}; 4 | 5 | const addHeader = (name, value) => { headers[name] = value }; 6 | const clearHeaders = () => { headers = {} }; 7 | 8 | const get = (uri) => { 9 | const options = { 10 | method: 'GET', 11 | headers: { 12 | 'Connection': 'keep-alive', 13 | 'Accept': 'application/json' 14 | } 15 | }; 16 | 17 | return request(uri, options); 18 | }; 19 | 20 | const request = (uri, opts) => { 21 | const requestHeaders = Object.assign( 22 | {}, 23 | opts.headers, 24 | headers 25 | ); 26 | 27 | const options = Object.assign( 28 | {}, 29 | opts, 30 | { headers: requestHeaders } 31 | ); 32 | 33 | return fetchWithRetry(uri, options); 34 | }; 35 | 36 | function fetchWithRetry(url, options) { 37 | var retries = 3; 38 | var retryDelay = 500; 39 | 40 | if (options && options.retries) { 41 | retries = options.retries; 42 | } 43 | 44 | if (options && options.retryDelay) { 45 | retryDelay = options.retryDelay; 46 | } 47 | 48 | return new Promise(function(resolve, reject) { 49 | var wrappedFetch = (n) => { 50 | fetch(url, options) 51 | .then(response => { 52 | resolve(response); 53 | }) 54 | .catch(error => { 55 | if (n <= retries) { 56 | setTimeout(() => { 57 | wrappedFetch(n + 1); 58 | }, retryDelay * Math.pow(2, n)); 59 | } else { 60 | reject(error); 61 | } 62 | }); 63 | }; 64 | wrappedFetch(0); 65 | }); 66 | } 67 | 68 | export default { 69 | addHeader, 70 | clearHeaders, 71 | get, 72 | request 73 | }; 74 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import {normalize, parseVersion} from '../src/utils' 3 | 4 | describe("normalize utils", () => { 5 | it("should normalize attributes", () => { 6 | const attr = { 7 | "config.foo": 1, 8 | "config.bar": 2, 9 | } 10 | 11 | expect(normalize(attr)).to.be.eql({config: {foo: 1, bar: 2}}); 12 | }); 13 | 14 | it("should normalize nested attributes", () => { 15 | const attr = { 16 | "config": { 17 | "foo.bar": null, 18 | "foo.bar2": 1 19 | } 20 | } 21 | 22 | expect(normalize(attr)).to.be.eql({config: {foo: {bar: null, bar2: 1}}}); 23 | }); 24 | 25 | it("should preserve arrays", () => { 26 | const attr = { 27 | "config": { 28 | "foobar": ["a", "b"] 29 | } 30 | } 31 | 32 | const actual = normalize(attr); 33 | 34 | expect(normalize(attr)).to.be.eql({config: { foobar: ["a", "b"] }}); 35 | expect(actual.config.foobar).to.be.an('array'); 36 | }); 37 | }); 38 | 39 | describe("parseVersion utils", () => { 40 | it("should return the CE version", () => { 41 | expect(parseVersion("0.10.0")).to.be.eql("0.10.0"); 42 | }); 43 | 44 | it("should return the CE version", () => { 45 | expect(parseVersion("0.11.0-rc1")).to.be.eql("0.11.0"); 46 | }); 47 | 48 | it("should return the EE version", () => { 49 | expect(parseVersion("0.29-0-enterprise-edition")).to.be.eql("0.29.0"); 50 | }); 51 | 52 | it("should return the EE version", () => { 53 | expect(parseVersion("0.29-1-enterprise-edition")).to.be.eql("0.29.1"); 54 | }); 55 | 56 | it("should return the EE version with no patch", () => { 57 | expect(parseVersion("0.29-enterprise-edition")).to.be.eql("0.29.0"); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /examples/consumer.example.md: -------------------------------------------------------------------------------- 1 | consumer example 2 | ---------------- 3 | 4 | ## Config file 5 | 6 | ```yaml 7 | consumers: 8 | - username: "iphone-app" 9 | ensure: "present" 10 | acls: 11 | - group: "foo-group" 12 | ensure: "present" 13 | credentials: 14 | - name: "key-auth" 15 | ensure: "present" 16 | attributes: 17 | key: "very-secret-key" 18 | 19 | ``` 20 | 21 | ## Using curl 22 | 23 | For illustrative purpose a cURL calls would be the following 24 | 25 | ### create customer 26 | 27 | ```sh 28 | $ curl -i -X POST -H "Content-Type: application/json" \ 29 | --url http://localhost:8001/consumers \ 30 | --data '{"username":"iphone-app"}' 31 | ``` 32 | 33 | ``` 34 | HTTP 201 Created 35 | ``` 36 | 37 | ``` 38 | { 39 | "created_at": "___created_at___", 40 | "username": "iphone-app", 41 | "id": "2b47ba9b-761a-492d-9a0c-000000000001" 42 | } 43 | ``` 44 | 45 | ### add customer credential 46 | 47 | ```sh 48 | $ curl -i -X POST -H "Content-Type: application/json" \ 49 | --url http://localhost:8001/consumers/2b47ba9b-761a-492d-9a0c-000000000001/key-auth \ 50 | --data '{"key":"very-secret-key"}' 51 | ``` 52 | 53 | ``` 54 | HTTP 201 Created 55 | ``` 56 | 57 | ``` 58 | { 59 | "id": "2b47ba9b-761a-492d-9a0c-000000000002", 60 | "created_at": "___created_at___", 61 | "key": "very-secret-key", 62 | "consumer_id": "2b47ba9b-761a-492d-9a0c-000000000001" 63 | } 64 | ``` 65 | 66 | ### add customer acls 67 | 68 | ```sh 69 | $ curl -i -X POST -H "Content-Type: application/json" \ 70 | --url http://localhost:8001/consumers/2b47ba9b-761a-492d-9a0c-000000000001/acls \ 71 | --data '{"group":"foo-group"}' 72 | ``` 73 | 74 | ``` 75 | HTTP 201 Created 76 | ``` 77 | 78 | ``` 79 | { 80 | "group": "foo-group", 81 | "created_at": "___created_at___", 82 | "id": "2b47ba9b-761a-492d-9a0c-000000000003", 83 | "consumer_id": "2b47ba9b-761a-492d-9a0c-000000000001" 84 | } 85 | ``` -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | const createLogHandler = handlers => message => { 2 | if (handlers.hasOwnProperty(message.type)) { 3 | return handlers[message.type](message); 4 | } 5 | 6 | return handlers['unknown'](message); 7 | }; 8 | 9 | export const screenLogger = createLogHandler({ 10 | noop: message => createLogHandler({ 11 | 'noop-api': ({ api }) => console.log(`api ${api.name.bold} ${'is up to date'.bold.green}`), 12 | 'noop-plugin': ({ plugin }) => console.log(`- plugin ${plugin.name.bold} ${'is up to date'.bold.green}`), 13 | 'noop-global-plugin': ({ plugin }) => console.log(`global plugin ${plugin.name.bold} ${'is up to date'.bold.green}`), 14 | 'noop-consumer': ({ consumer }) => console.log(`consumer ${consumer.username.bold} ${'is up to date'.bold.green}`), 15 | 'noop-credential': ({ credential, credentialIdName }) => console.log(`- credential ${credential.name.bold} with ${credentialIdName.bold}: ${credential.attributes[credentialIdName].bold} ${'is up to date'.bold.green}`), 16 | 'noop-upstream': ({ upstream }) => console.log(`upstream ${upstream.name.bold} ${'is up to date'.bold.green}`), 17 | 'noop-target': ({ target }) => console.log(`target ${target.target.bold} ${'is up to date'.bold.green}`), 18 | 19 | unknown: action => console.log('unknown action', action), 20 | })(message.params), 21 | request: ({ uri, params: { method, body } }) => console.log( 22 | `\n${method.bold.blue}`, uri.blue, "\n", body ? body : '' 23 | ), 24 | response: ({ ok, status, statusText, content }) => console.log( 25 | ok ? `${status} ${statusText.bold}`.green : `${status} ${statusText.bold}`.red, 26 | content 27 | ), 28 | debug: () => {}, 29 | 'experimental-features': ({ message }) => console.log(message), 30 | 'kong-info': ({ version }) => console.log(`Kong version: ${version}`), 31 | unknown: message => console.log('unknown', message), 32 | }); 33 | -------------------------------------------------------------------------------- /test/plugins.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import {apis, plugins, globalPlugins} from '../src/core.js'; 3 | import { 4 | noop, 5 | addGlobalPlugin, 6 | removeGlobalPlugin, 7 | updateGlobalPlugin 8 | } from '../src/actions.js'; 9 | 10 | describe("plugins", () => { 11 | it("should add new global plugin", () => { 12 | var actual = globalPlugins([{ 13 | "ensure": "present", 14 | "name": "cors", 15 | "attributes": { 16 | 'config.foo': "bar" 17 | } 18 | }]) 19 | .map(x => x({hasGlobalPlugin: () => false})); 20 | 21 | expect(actual).to.be.eql([ 22 | addGlobalPlugin('cors', {'config.foo': "bar"}) 23 | ]); 24 | }); 25 | 26 | it("should remove a global plugin", () => { 27 | var actual = globalPlugins([{ 28 | "name": "cors", 29 | "ensure": "removed", 30 | "attributes": { 31 | 'config.foo': "bar" 32 | } 33 | }]) 34 | .map(x => x({ 35 | hasGlobalPlugin: () => true, 36 | getGlobalPluginId: () => 123 37 | })); 38 | 39 | expect(actual).to.be.eql([ 40 | removeGlobalPlugin(123) 41 | ]); 42 | }); 43 | 44 | it('should update a global plugin', () => { 45 | var actual = globalPlugins([{ 46 | 'name': 'cors', 47 | 'attributes': { 48 | 'config.foo': 'bar' 49 | }}] 50 | ).map(x => x({ 51 | hasGlobalPlugin: () => true, 52 | getGlobalPluginId: () => 123, 53 | isGlobalPluginUpToDate: () => false 54 | })); 55 | 56 | expect(actual).to.be.eql([ 57 | updateGlobalPlugin(123, {'config.foo': 'bar'}) 58 | ]); 59 | }); 60 | 61 | it("should validate ensure enum", () => { 62 | expect(() => globalPlugins([{ 63 | "ensure": "not-valid", 64 | "name": "cors" 65 | }])).to.throwException(/Invalid ensure/); 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /src/cli-dump.js: -------------------------------------------------------------------------------- 1 | import readKongApi from './readKongApi'; 2 | import {pretty} from './prettyConfig'; 3 | import adminApi from './adminApi'; 4 | import colors from 'colors'; 5 | import requester from './requester'; 6 | import {repeatableOptionCallback} from './utils'; 7 | import {addSchemasFromOptions} from './consumerCredentials'; 8 | 9 | import program from 'commander'; 10 | 11 | program 12 | .version(require("../package.json").version) 13 | .option('-f, --format ', 'Export format [screen, json, yaml] (default: yaml)', /^(screen|json|yaml|yml)$/, 'yaml') 14 | .option('--host ', 'Kong admin host (default: localhost:8001)', 'localhost:8001') 15 | .option('--https', 'Use https for admin API requests') 16 | .option('--ignore-consumers', 'Ignore consumers in kong') 17 | .option('--header [value]', 'Custom headers to be added to all requests', (nextHeader, headers) => { headers.push(nextHeader); return headers }, []) 18 | .option('--credential-schema ', 'Add custom auth plugin in : format. Ex: custom_jwt:key. Repeat option for multiple custom plugins', repeatableOptionCallback, []) 19 | .parse(process.argv); 20 | 21 | if (!program.host) { 22 | console.error('--host to the kong admin is required e.g. localhost:8001'.red); 23 | process.exit(1); 24 | } 25 | 26 | try { 27 | addSchemasFromOptions(program.credentialSchema); 28 | } catch(e){ 29 | console.error(e.message.red); 30 | process.exit(1); 31 | } 32 | 33 | let headers = program.header || []; 34 | 35 | headers 36 | .map((h) => h.split(':')) 37 | .forEach(([name, value]) => requester.addHeader(name, value)); 38 | 39 | readKongApi(adminApi({ host: program.host, https: program.https, ignoreConsumers: program.ignoreConsumers })) 40 | .then(results => { 41 | return {host: program.host, https: program.https, headers, ...results}; 42 | }) 43 | .then(pretty(program.format)) 44 | .then(config => { 45 | process.stdout.write(config + '\n'); 46 | }) 47 | .catch(error => { 48 | console.error(`${error}`.red, '\n', error.stack); 49 | process.exit(1); 50 | }); 51 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | export default function createRouter(host, https) { 2 | const protocol = https ? 'https' : 'http'; 3 | const adminApiRoot = `${protocol}://${host}`; 4 | return ({name, params}) => { 5 | switch (name) { 6 | case 'apis': return `${adminApiRoot}/apis`; 7 | case 'api': return `${adminApiRoot}/apis/${params.name}`; 8 | case 'api-plugins': return `${adminApiRoot}/apis/${params.apiId}/plugins`; 9 | case 'api-plugin': return `${adminApiRoot}/apis/${params.apiId}/plugins/${params.pluginId}`; 10 | case 'consumers': return `${adminApiRoot}/consumers`; 11 | case 'consumer': return `${adminApiRoot}/consumers/${params.consumerId}`; 12 | case 'consumer-credentials': return `${adminApiRoot}/consumers/${params.consumerId}/${params.plugin}`; 13 | case 'consumer-credential': return `${adminApiRoot}/consumers/${params.consumerId}/${params.plugin}/${params.credentialId}`; 14 | case 'consumer-acls': return `${adminApiRoot}/consumers/${params.consumerId}/acls`; 15 | case 'consumer-acl': return `${adminApiRoot}/consumers/${params.consumerId}/acls/${params.aclId}`; 16 | 17 | case 'plugins': return `${adminApiRoot}/plugins`; 18 | case 'plugin': return `${adminApiRoot}/plugins/${params.pluginId}`; 19 | case 'plugins-enabled': return `${adminApiRoot}/plugins/enabled`; 20 | case 'plugins-scheme': return `${adminApiRoot}/plugins/schema/${params.plugin}`; 21 | 22 | case 'upstreams': return `${adminApiRoot}/upstreams`; 23 | case 'upstream': return `${adminApiRoot}/upstreams/${params.name}`; 24 | case 'upstream-targets': return `${adminApiRoot}/upstreams/${params.upstreamId}/targets`; 25 | // Note: this uri must end with a slash for kong version 11 26 | case 'upstream-targets-active': return `${adminApiRoot}/upstreams/${params.upstreamId}/targets/active/`; 27 | 28 | case 'root': return `${adminApiRoot}`; 29 | 30 | default: 31 | throw new Error(`Unknown route "${name}"`); 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/reducers/apis.js: -------------------------------------------------------------------------------- 1 | import { parseApiPostV10, parsePlugin, parseConsumer, parseAcl, parseGlobalPlugin } from '../readKongApi'; 2 | 3 | const plugins = (state, log) => { 4 | const { params: { type, endpoint: { params, body } }, content } = log; 5 | 6 | switch (type) { 7 | case 'add-api-plugin': return [ ...state, parsePlugin(content) ]; 8 | case 'update-api-plugin': return state.map(state => { 9 | if (state._info.id !== content.id) { 10 | return state; 11 | } 12 | 13 | return parsePlugin(content); 14 | }); 15 | case 'remove-api-plugin': return state.filter(plugin => plugin._info.id !== params.pluginId); 16 | default: return state; 17 | } 18 | }; 19 | 20 | const api = (state, log) => { 21 | const { params: { type, endpoint: { params, body } }, content } = log; 22 | 23 | switch (type) { 24 | case 'create-api': return { 25 | ...parseApiPostV10(content), 26 | plugins: [] 27 | }; 28 | case 'update-api': 29 | if (state._info.id !== content.id) { 30 | return state; 31 | } 32 | 33 | return { 34 | ...state, 35 | ...parseApiPostV10(content) 36 | }; 37 | 38 | case 'add-api-plugin': 39 | case 'update-api-plugin': 40 | case 'remove-api-plugin': 41 | if (state._info.id !== params.apiId) { 42 | return state; 43 | } 44 | 45 | return { 46 | ...state, 47 | plugins: plugins(state.plugins, log) 48 | }; 49 | 50 | default: return state; 51 | } 52 | }; 53 | 54 | export default (state = [], log) => { 55 | if (log.type !== 'response') { 56 | return state; 57 | } 58 | 59 | const { params: { type, endpoint: { params } }, content } = log; 60 | 61 | switch (type) { 62 | case 'create-api': return [...state, api(undefined, log)]; 63 | case 'remove-api': return state.filter(api => api.name !== params.name); 64 | 65 | case 'add-api-plugin': 66 | case 'update-api-plugin': 67 | case 'remove-api-plugin': 68 | case 'update-api': return state.map(state => api(state, log)); 69 | 70 | default: return state; 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /src/actions/upstreams.js: -------------------------------------------------------------------------------- 1 | import assign from 'object-assign'; 2 | 3 | export function createUpstream(name, params) { 4 | return { 5 | type: 'create-upstream', 6 | endpoint: { name: 'upstreams' }, 7 | method: 'POST', 8 | body: assign({}, params, {name}) 9 | }; 10 | } 11 | 12 | export function removeUpstream(name) { 13 | return { 14 | type: 'remove-upstream', 15 | endpoint: { name: 'upstream', params: {name} }, 16 | method: 'DELETE' 17 | }; 18 | } 19 | 20 | export function updateUpstream(name, params) { 21 | // orderlist must be passed if the slots value is changed, 22 | // however we don't want to store orderlist in the config 23 | // because this can be a very large array. 24 | // Clone the params object and add a randomly generated 25 | // orderlist to it based on slot value. 26 | return { 27 | type: 'update-upstream', 28 | endpoint: { name: 'upstream', params: {name} }, 29 | method: 'PATCH', 30 | body: addOrderlistToUpstreamAttributes(params) 31 | } 32 | } 33 | 34 | export function addUpstreamTarget(upstreamId, targetName, params) { 35 | return { 36 | type: 'add-upstream-target', 37 | endpoint: { name: 'upstream-targets', params: {upstreamId, targetName} }, 38 | method: 'POST', 39 | body: assign({}, params, {target: targetName}) 40 | }; 41 | } 42 | 43 | export function removeUpstreamTarget(upstreamId, targetName) { 44 | return { 45 | type: 'remove-upstream-target', 46 | endpoint: { name: 'upstream-targets', params: {upstreamId, targetName} }, 47 | method: 'POST', 48 | body: { target: targetName, weight: 0 } 49 | }; 50 | } 51 | 52 | export function updateUpstreamTarget(upstreamId, targetName, params) { 53 | return addUpstreamTarget(upstreamId, targetName, params); 54 | } 55 | 56 | function addOrderlistToUpstreamAttributes(attributes) { 57 | if (attributes.slots) { 58 | attributes = Object.assign({}, attributes); 59 | attributes.orderlist = []; 60 | for (let i = 1; i <= attributes.slots; i++) { 61 | let pos = Math.floor(Math.random() * i); 62 | attributes.orderlist.splice(pos, 0, i); 63 | } 64 | } 65 | 66 | return attributes; 67 | } 68 | -------------------------------------------------------------------------------- /test-integration/__snapshots__/bugs.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should allow updating a global plugin with no attributes 1`] = ` 4 | Array [ 5 | Object { 6 | "type": "kong-info", 7 | "version": "___version___", 8 | }, 9 | Object { 10 | "params": Object { 11 | "body": Object { 12 | "enabled": true, 13 | "name": "cors", 14 | }, 15 | "endpoint": Object { 16 | "name": "plugins", 17 | "params": Object { 18 | "pluginName": "cors", 19 | }, 20 | }, 21 | "method": "POST", 22 | "type": "add-global-plugin", 23 | }, 24 | "type": "request", 25 | "uri": "http://localhost:8001/plugins", 26 | }, 27 | Object { 28 | "content": Object { 29 | "config": Object { 30 | "credentials": false, 31 | "preflight_continue": false, 32 | }, 33 | "created_at": "___created_at___", 34 | "enabled": true, 35 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 36 | "name": "cors", 37 | }, 38 | "ok": true, 39 | "params": Object { 40 | "body": Object { 41 | "enabled": true, 42 | "name": "cors", 43 | }, 44 | "endpoint": Object { 45 | "name": "plugins", 46 | "params": Object { 47 | "pluginName": "cors", 48 | }, 49 | }, 50 | "method": "POST", 51 | "type": "add-global-plugin", 52 | }, 53 | "status": 201, 54 | "statusText": "Created", 55 | "type": "response", 56 | "uri": "http://localhost:8001/plugins", 57 | }, 58 | Object { 59 | "type": "kong-info", 60 | "version": "___version___", 61 | }, 62 | Object { 63 | "params": Object { 64 | "noop": true, 65 | "plugin": Object { 66 | "attributes": Object { 67 | "enabled": true, 68 | }, 69 | "name": "cors", 70 | }, 71 | "type": "noop-global-plugin", 72 | }, 73 | "type": "noop", 74 | }, 75 | ] 76 | `; 77 | 78 | exports[`should allow updating a global plugin with no attributes 2`] = ` 79 | "apis: [] 80 | consumers: [] 81 | plugins: 82 | - name: cors 83 | attributes: 84 | enabled: true 85 | config: 86 | credentials: false 87 | preflight_continue: false 88 | upstreams: [] 89 | " 90 | `; 91 | -------------------------------------------------------------------------------- /src/reducers/upstreams.js: -------------------------------------------------------------------------------- 1 | import { parseUpstream, parseTarget } from '../parsers/upstreams'; 2 | 3 | const targets = (state, log) => { 4 | const { params: { type, endpoint: { params, body } }, content } = log; 5 | 6 | switch(type) { 7 | case 'add-upstream-target': return [ 8 | // target with the same name overrides the previous target 9 | ...state.filter(target => target.target !== params.targetName), 10 | parseTarget(content) 11 | ]; 12 | case 'update-upstream-target': return state.map(state => { 13 | if (state._info.id !== content.id) { 14 | return state; 15 | } 16 | 17 | return parseTarget(content); 18 | }); 19 | case 'remove-upstream-target': return state.filter(target => target.target !== params.targetName); 20 | default: return state; 21 | } 22 | }; 23 | 24 | const upstream = (state, log) => { 25 | const { params: { type, endpoint: { params, body } }, content } = log; 26 | 27 | switch (type) { 28 | case 'create-upstream': return { 29 | ...parseUpstream(content), 30 | targets: [] 31 | }; 32 | case 'update-upstream': 33 | if (state._info.id !== content.id) { 34 | return state 35 | } 36 | 37 | return { 38 | ...state, 39 | ...parseUpstream(content) 40 | }; 41 | 42 | case 'add-upstream-target': 43 | case 'update-upstream-target': 44 | case 'remove-upstream-target': 45 | if (state._info.id !== params.upstreamId) { 46 | return state; 47 | } 48 | 49 | return { 50 | ...state, 51 | targets: targets(state.targets, log) 52 | }; 53 | 54 | default: return state; 55 | } 56 | }; 57 | 58 | export default (state = [], log) => { 59 | if (log.type !== 'response') { 60 | return state; 61 | } 62 | 63 | const { params: { type, endpoint: { params } }, content } = log; 64 | 65 | switch (type) { 66 | case 'create-upstream': return [...state, upstream(undefined, log)]; 67 | case 'remove-upstream': return state.filter(upstream => upstream.name !== params.name); 68 | 69 | case 'add-upstream-target': 70 | case 'update-upstream-target': 71 | case 'remove-upstream-target': 72 | case 'update-upstream': return state.map(state => upstream(state, log)); 73 | 74 | default: return state; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /test/diff.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import diff from '../src/diff'; 3 | 4 | describe("diff", () => { 5 | const defined = { 6 | uris: [ '/foobar', '/baz' ], 7 | strip_uri: true, 8 | preserve_host: true, 9 | upstream_url: 'http://localhost:8001' 10 | }; 11 | 12 | const server = { 13 | uris: [ '/foobar', '/baz' ], 14 | id: 'e24e355f-3861-4479-bd34-1aa8a995421e', 15 | upstream_read_timeout: 60000, 16 | preserve_host: true, 17 | created_at: 1492015892000, 18 | upstream_connect_timeout: 60000, 19 | upstream_url: 'http://localhost:8001', 20 | strip_uri: true, 21 | https_only: false, 22 | name: 'foobar', 23 | http_if_terminated: true, 24 | upstream_send_timeout: 60000, 25 | retries: 5, 26 | plugins: [] 27 | }; 28 | 29 | it("noting changed", () => { 30 | expect(diff(defined, server)).to.be.eql([]); 31 | }); 32 | 33 | it("strip_uri changed", () => { 34 | expect(diff({...defined, strip_uri: false}, server)).to.be.eql(['strip_uri']); 35 | }); 36 | 37 | it("uris one removed", () => { 38 | expect(diff({...defined, uris: ['/foobar']}, server)).to.be.eql(['uris']); 39 | }); 40 | 41 | it("uris one added", () => { 42 | expect(diff({...defined, uris: ['/foobar', '/baz', '/added']}, server)).to.be.eql(['uris']); 43 | }); 44 | 45 | it("uris one changed", () => { 46 | expect(diff({...defined, uris: ['/foobar', '/changed']}, server)).to.be.eql(['uris']); 47 | }); 48 | 49 | it('methods added', () => { 50 | expect(diff({...defined, methods: ['GET']}, server)).to.be.eql(['methods']); 51 | }); 52 | }); 53 | 54 | 55 | describe("diff hacks", () => { 56 | it("should be same redirect_uri when a string", () => { 57 | expect(diff({ redirect_uri: 'foobar' }, { redirect_uri: 'foobar' })).to.be.eql([]); 58 | expect(diff({ redirect_uri: 'foobar' }, { redirect_uri: ['foobar'] })).to.be.eql([]); 59 | }); 60 | 61 | it("should be same redirect_uri when an array", () => { 62 | expect(diff({ redirect_uri: ['foobar'] }, { redirect_uri: ['foobar'] })).to.be.eql([]); 63 | }); 64 | 65 | it("should be different redirect_uri when a string", () => { 66 | expect(diff({ redirect_uri: 'foobar2' }, { redirect_uri: ['foobar'] })).to.be.eql(['redirect_uri']); 67 | }); 68 | 69 | it("should be different redirect_uri when an array", () => { 70 | expect(diff({ redirect_uri: ['foobar2'] }, { redirect_uri: ['foobar'] })).to.be.eql(['redirect_uri']); 71 | }) 72 | }); 73 | -------------------------------------------------------------------------------- /src/kongState.js: -------------------------------------------------------------------------------- 1 | import semVer from 'semver'; 2 | import {getSupportedCredentials} from './consumerCredentials' 3 | 4 | const fetchUpstreamsWithTargets = async ({ version, fetchUpstreams, fetchTargets }) => { 5 | if (semVer.lte(version, '0.10.0')) { 6 | return Promise.resolve([]); 7 | } 8 | 9 | const upstreams = await fetchUpstreams(); 10 | 11 | return await Promise.all( 12 | upstreams.map(async item => { 13 | const targets = await fetchTargets(item.id); 14 | 15 | return { ...item, targets }; 16 | }) 17 | ); 18 | }; 19 | 20 | export default async ({fetchApis, fetchPlugins, fetchGlobalPlugins, fetchConsumers, fetchConsumerCredentials, fetchConsumerAcls, fetchUpstreams, fetchTargets, fetchKongVersion}) => { 21 | const version = await fetchKongVersion(); 22 | const apis = await fetchApis(); 23 | const apisWithPlugins = await Promise.all(apis.map(async item => { 24 | const plugins = await fetchPlugins(item.id); 25 | 26 | return {...item, plugins}; 27 | })); 28 | 29 | const consumers = await fetchConsumers(); 30 | const consumersWithCredentialsAndAcls = await Promise.all(consumers.map(async consumer => { 31 | if (consumer.custom_id && !consumer.username) { 32 | console.log(`Consumers with only custom_id not supported: ${consumer.custom_id}`); 33 | 34 | return consumer; 35 | } 36 | 37 | const allCredentials = Promise.all(getSupportedCredentials().map(name => { 38 | return fetchConsumerCredentials(consumer.id, name) 39 | .then(credentials => [name, credentials]); 40 | })); 41 | 42 | var aclsFetched = await fetchConsumerAcls(consumer.id); 43 | 44 | var consumerWithCredentials = allCredentials 45 | .then(result => { 46 | return { 47 | ...consumer, 48 | credentials: result.reduce((acc, [name, credentials]) => { 49 | return {...acc, [name]: credentials}; 50 | }, {}), 51 | acls: aclsFetched 52 | 53 | }; 54 | }); 55 | 56 | return consumerWithCredentials; 57 | 58 | })); 59 | 60 | const allPlugins = await fetchGlobalPlugins(); 61 | const globalPlugins = allPlugins.filter(plugin => { 62 | return plugin.api_id === undefined; 63 | }); 64 | 65 | const upstreamsWithTargets = await fetchUpstreamsWithTargets({ version, fetchUpstreams, fetchTargets }); 66 | 67 | return { 68 | apis: apisWithPlugins, 69 | consumers: consumersWithCredentialsAndAcls, 70 | plugins: globalPlugins, 71 | upstreams: upstreamsWithTargets, 72 | version, 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /src/cli-apply.js: -------------------------------------------------------------------------------- 1 | import execute from './core'; 2 | import adminApi from './adminApi'; 3 | import colors from 'colors'; 4 | import configLoader from './configLoader'; 5 | import program from 'commander'; 6 | import requester from './requester'; 7 | import {repeatableOptionCallback} from './utils'; 8 | import { screenLogger } from './logger'; 9 | import {addSchemasFromOptions, addSchemasFromConfig} from './consumerCredentials'; 10 | 11 | program 12 | .version(require("../package.json").version) 13 | .option('--path ', 'Path to the configuration file') 14 | .option('--host ', 'Kong admin host (default: localhost:8001)') 15 | .option('--https', 'Use https for admin API requests') 16 | .option('--no-cache', 'Do not cache kong state in memory') 17 | .option('--ignore-consumers', 'Do not sync consumers') 18 | .option('--header [value]', 'Custom headers to be added to all requests', (nextHeader, headers) => { headers.push(nextHeader); return headers }, []) 19 | .option('--credential-schema ', 'Add custom auth plugin in : format. Ex: custom_jwt:key. Repeat option for multiple custom plugins', repeatableOptionCallback, []) 20 | .parse(process.argv); 21 | 22 | if (!program.path) { 23 | console.error('--path to the config file is required'.red); 24 | process.exit(1); 25 | } 26 | 27 | try{ 28 | addSchemasFromOptions(program.credentialSchema); 29 | }catch(e){ 30 | console.error(e.message.red); 31 | process.exit(1); 32 | } 33 | 34 | console.log(`Loading config ${program.path}`); 35 | 36 | let config = configLoader(program.path); 37 | let host = program.host || config.host || 'localhost:8001'; 38 | let https = program.https || config.https || false; 39 | let ignoreConsumers = program.ignoreConsumers || !config.consumers || config.consumers.length === 0 || false; 40 | let cache = program.cache; 41 | 42 | config.headers = config.headers || []; 43 | 44 | let headers = new Map(); 45 | ([...config.headers, ...program.header]) 46 | .map((h) => h.split(':')) 47 | .forEach(([name, value]) => headers.set(name, value)); 48 | 49 | headers 50 | .forEach((value, name) => requester.addHeader(name, value)); 51 | 52 | if (!host) { 53 | console.error('Kong admin host must be specified in config or --host'.red); 54 | process.exit(1); 55 | } 56 | 57 | if (ignoreConsumers) { 58 | config.consumers = []; 59 | } 60 | else { 61 | try{ 62 | addSchemasFromConfig(config); 63 | } catch(e) { 64 | console.error(e.message.red); 65 | process.exit(1); 66 | } 67 | } 68 | 69 | console.log(`Apply config to ${host}`.green); 70 | 71 | execute(config, adminApi({host, https, ignoreConsumers, cache}), screenLogger) 72 | .catch(error => { 73 | console.error(`${error}`.red, '\n', error.stack); 74 | process.exit(1); 75 | }); 76 | -------------------------------------------------------------------------------- /test-integration/config.test.js: -------------------------------------------------------------------------------- 1 | import execute from '../lib/core'; 2 | import { testAdminApi, exportToYaml, logger, getLog, getLocalState, tearDown } from './util'; 3 | import readKongApi, { parseApiPostV10, parsePlugin, parseConsumer, parseAcl, parseGlobalPlugin } from '../lib/readKongApi'; 4 | import configLoader from '../lib/configLoader'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import pad from 'pad'; 8 | 9 | beforeEach(tearDown); 10 | 11 | const requestToCurl = (uri, method, body) => { 12 | switch (method) { 13 | case 'POST': return `$ curl -i -X POST -H "Content-Type: application/json" \\\n --url ${uri} \\\n --data '${JSON.stringify(body)}'`; 14 | default: return ``; 15 | } 16 | }; 17 | 18 | const ignoreConfigOrder = state => ({ 19 | ...state, 20 | apis: state.apis.sort((a, b) => a.name > b.name ? 1 : -1), 21 | consumers: state.consumers.sort((a, b) => a.username > b.username ? 1 : -1), 22 | plugins: state.plugins.sort((a, b) => a.attributes.config.minute - b.attributes.config.minute), 23 | }); 24 | 25 | const codeBlock = (code, lang = '') => `\`\`\`${lang}\n${code}\n\`\`\``; 26 | const title = text => `${text}\n${'-'.repeat(text.length)}`; 27 | const header = (text, level = 2) => `${'#'.repeat(level)} ${text}`; 28 | const append = (md, ...block) => block.reduce((md, block) => `${md}\n\n${block}`, md); 29 | const replaceDashWithSpace = text => text.split('-').join(' '); 30 | 31 | const curlExample = `For illustrative purpose a cURL calls would be the following`; 32 | 33 | const addExampleFile = (configPath, filename, log) => { 34 | const head = append(title(replaceDashWithSpace(filename.replace('.example.yml', '')) + " example"), header('Config file'), codeBlock(fs.readFileSync(configPath), 'yaml'), header('Using curl'), curlExample); 35 | const content = getLog().reduce((content, log) => { 36 | switch (log.type) { 37 | case 'request': return append(content, header(replaceDashWithSpace(log.params.type), 3), codeBlock(requestToCurl(log.uri, log.params.method, log.params.body), 'sh')); 38 | case 'response': return append(content, codeBlock(`HTTP ${log.status} ${log.statusText}`), codeBlock(JSON.stringify(log.content, null, 2))); 39 | 40 | default: return content; 41 | } 42 | }, head); 43 | 44 | fs.writeFileSync(path.resolve(__dirname, '../examples', filename.replace('.yml', '.md')), content, "UTF-8", { 'flags': 'w+' }); 45 | }; 46 | 47 | fs.readdirSync(path.resolve(__dirname, './config')).forEach(filename => { 48 | it(`should apply ${filename}`, async () => { 49 | const configPath = path.resolve(__dirname, './config', filename); 50 | const config = configLoader(configPath); 51 | 52 | await execute(config, testAdminApi, logger); 53 | await execute(config, testAdminApi, logger); // all the actions should be no-op 54 | const kongState = ignoreConfigOrder(await readKongApi(testAdminApi)); 55 | 56 | expect(getLog()).toMatchSnapshot(); 57 | expect(exportToYaml(kongState)).toMatchSnapshot(); 58 | expect(ignoreConfigOrder(getLocalState())).toEqual(kongState); 59 | 60 | if (filename.endsWith('example.yml')) { 61 | addExampleFile(configPath, filename, getLog()); 62 | } 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /examples/key-auth.example.md: -------------------------------------------------------------------------------- 1 | key auth example 2 | ---------------- 3 | 4 | ## Config file 5 | 6 | ```yaml 7 | apis: 8 | - name: "mockbin" 9 | ensure: "present" 10 | attributes: 11 | upstream_url: "http://mockbin.com" 12 | hosts: 13 | - "mockbin.com" 14 | plugins: 15 | - name: "key-auth" 16 | attributes: 17 | config: 18 | key_names: 19 | - very-secret-key 20 | 21 | consumers: 22 | - username: "iphone-app" 23 | ensure: "present" 24 | credentials: 25 | - name: "key-auth" 26 | ensure: "present" 27 | attributes: 28 | key: "very-secret-key" 29 | 30 | ``` 31 | 32 | ## Using curl 33 | 34 | For illustrative purpose a cURL calls would be the following 35 | 36 | ### create customer 37 | 38 | ```sh 39 | $ curl -i -X POST -H "Content-Type: application/json" \ 40 | --url http://localhost:8001/consumers \ 41 | --data '{"username":"iphone-app"}' 42 | ``` 43 | 44 | ``` 45 | HTTP 201 Created 46 | ``` 47 | 48 | ``` 49 | { 50 | "created_at": "___created_at___", 51 | "username": "iphone-app", 52 | "id": "2b47ba9b-761a-492d-9a0c-000000000001" 53 | } 54 | ``` 55 | 56 | ### add customer credential 57 | 58 | ```sh 59 | $ curl -i -X POST -H "Content-Type: application/json" \ 60 | --url http://localhost:8001/consumers/2b47ba9b-761a-492d-9a0c-000000000001/key-auth \ 61 | --data '{"key":"very-secret-key"}' 62 | ``` 63 | 64 | ``` 65 | HTTP 201 Created 66 | ``` 67 | 68 | ``` 69 | { 70 | "id": "2b47ba9b-761a-492d-9a0c-000000000002", 71 | "created_at": "___created_at___", 72 | "key": "very-secret-key", 73 | "consumer_id": "2b47ba9b-761a-492d-9a0c-000000000001" 74 | } 75 | ``` 76 | 77 | ### create api 78 | 79 | ```sh 80 | $ curl -i -X POST -H "Content-Type: application/json" \ 81 | --url http://localhost:8001/apis \ 82 | --data '{"upstream_url":"http://mockbin.com","hosts":["mockbin.com"],"name":"mockbin"}' 83 | ``` 84 | 85 | ``` 86 | HTTP 201 Created 87 | ``` 88 | 89 | ``` 90 | { 91 | "created_at": "___created_at___", 92 | "strip_uri": true, 93 | "id": "2b47ba9b-761a-492d-9a0c-000000000003", 94 | "hosts": [ 95 | "mockbin.com" 96 | ], 97 | "name": "mockbin", 98 | "http_if_terminated": false, 99 | "preserve_host": false, 100 | "upstream_url": "http://mockbin.com", 101 | "upstream_connect_timeout": 60000, 102 | "upstream_send_timeout": 60000, 103 | "upstream_read_timeout": 60000, 104 | "retries": 5, 105 | "https_only": false 106 | } 107 | ``` 108 | 109 | ### add api plugin 110 | 111 | ```sh 112 | $ curl -i -X POST -H "Content-Type: application/json" \ 113 | --url http://localhost:8001/apis/2b47ba9b-761a-492d-9a0c-000000000003/plugins \ 114 | --data '{"config":{"key_names":["very-secret-key"]},"name":"key-auth"}' 115 | ``` 116 | 117 | ``` 118 | HTTP 201 Created 119 | ``` 120 | 121 | ``` 122 | { 123 | "created_at": "___created_at___", 124 | "config": { 125 | "key_names": [ 126 | "very-secret-key" 127 | ], 128 | "key_in_body": false, 129 | "anonymous": "", 130 | "run_on_preflight": true, 131 | "hide_credentials": false 132 | }, 133 | "id": "2b47ba9b-761a-492d-9a0c-000000000004", 134 | "name": "key-auth", 135 | "api_id": "2b47ba9b-761a-492d-9a0c-000000000003", 136 | "enabled": true 137 | } 138 | ``` -------------------------------------------------------------------------------- /test-integration/util.js: -------------------------------------------------------------------------------- 1 | import adminApi from '../lib/adminApi'; 2 | import readKongApi from '../lib/readKongApi'; 3 | import execute from '../lib/core'; 4 | import { logReducer } from '../lib/kongStateLocal'; 5 | import getCurrentStateSelector from '../lib/stateSelector'; 6 | import invariant from 'invariant'; 7 | import pad from 'pad'; 8 | import { pretty } from '../lib/prettyConfig'; 9 | 10 | invariant(process.env.TEST_INTEGRATION_KONG_HOST, ` 11 | Please set ${'TEST_INTEGRATION_KONG_HOST'.bold} env variable 12 | 13 | TEST_INTEGRATION_KONG_HOST=localhost:8001 yarn test 14 | 15 | ${'WARNING! Running integration tests are going to remove all data from the kong'.red.bold}. 16 | `); 17 | 18 | const UUIDRegex = /[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}/g; 19 | let uuids = {}; 20 | let log = []; 21 | let rawLog = []; 22 | 23 | export const exportToYaml = pretty('yaml'); 24 | export const getLocalState = () => getCurrentStateSelector(rawLog.reduce(logReducer, undefined)); 25 | 26 | export const testAdminApi = adminApi({ 27 | host: process.env.TEST_INTEGRATION_KONG_HOST, 28 | https: false, 29 | ignoreConsumers: false, 30 | cache: false, 31 | }); 32 | 33 | export const getLog = () => log; 34 | export const logger = message => { 35 | if (message.type === 'experimental-features') { 36 | // cannot include these in tests because they change based on test matrix 37 | return; 38 | } 39 | 40 | const m = cloneObject(message); 41 | 42 | if (m.hasOwnProperty('uri')) { 43 | m.uri = m.uri.replace(process.env.TEST_INTEGRATION_KONG_HOST, 'localhost:8001'); 44 | } 45 | 46 | rawLog.push(m); 47 | log.push(ignoreKeys(m, ['created_at', 'version', 'orderlist'])); 48 | }; 49 | 50 | const _ignoreKeys = (obj, keys) => { 51 | if (Array.isArray(obj)) { 52 | return obj; 53 | } 54 | 55 | if (typeof obj !== 'object') { 56 | return obj; 57 | } 58 | 59 | return Object.keys(obj).reduce((x, key) => { 60 | if (typeof obj[key] === 'string' && obj[key].match(UUIDRegex)) { 61 | const value = obj[key].match(UUIDRegex).reduce((value, uuid) => { 62 | if (!uuids.hasOwnProperty(uuid)) { 63 | const id = pad(12, `${Object.keys(uuids).length + 1}`, '0'); 64 | uuids[uuid] = `2b47ba9b-761a-492d-9a0c-${id}`; 65 | } 66 | 67 | return value.replace(uuid, uuids[uuid]); 68 | }, obj[key]); 69 | 70 | return { ...x, [key]: value }; 71 | } else if (keys.indexOf(key) !== -1) { 72 | return { ...x, [key]: `___${key}___` }; 73 | } 74 | 75 | return { ...x, [key]: _ignoreKeys(obj[key], keys) }; 76 | }, {}); 77 | }; 78 | 79 | const cloneObject = obj => JSON.parse(JSON.stringify(obj)); 80 | 81 | export const ignoreKeys = (message, keys) => _ignoreKeys(cloneObject(message), keys); 82 | 83 | const cleanupKong = async () => { 84 | const results = await readKongApi(testAdminApi); 85 | await execute({ 86 | apis: results.apis.map(api => ({ ...api, ensure: 'removed' })), 87 | consumers: results.consumers.map(consumer => ({ ...consumer, ensure: 'removed' })), 88 | plugins: results.plugins.map(plugin => ({ ...plugin, ensure: 'removed' })), 89 | upstreams: results.upstreams.map(upstream => ({ ...upstream, ensure: 'removed' })) 90 | }, testAdminApi); 91 | }; 92 | 93 | export const tearDown = async () => { 94 | uuids = {}; 95 | log = []; 96 | rawLog = []; 97 | await cleanupKong(); 98 | }; 99 | -------------------------------------------------------------------------------- /src/reducers/consumers.js: -------------------------------------------------------------------------------- 1 | import { parseApiPostV10, parsePlugin, parseConsumer, parseAcl, parseGlobalPlugin } from '../readKongApi'; 2 | 3 | 4 | const acls = (state, log) => { 5 | const { params: { type, endpoint: { params } }, content } = log; 6 | 7 | switch (type) { 8 | case 'add-customer-acls': return [ ...state, parseAcl(content)]; 9 | case 'remove-customer-acls': return state.filter(acl => acl._info.id !== params.aclId); 10 | default: return state; 11 | } 12 | } 13 | 14 | // the implementation in the readKongApi is not compatible 15 | // because the payload doesn't contain the plugin name 16 | const parseCredential = (name, { consumer_id, id, created_at, ...attributes }) => { 17 | return { 18 | name, 19 | attributes, 20 | _info: { id, consumer_id, created_at } 21 | } 22 | } 23 | 24 | const credentials = (state, log) => { 25 | const { params: { type, endpoint: { params } }, content } = log; 26 | 27 | switch (type) { 28 | case 'add-customer-credential': return [ ...state, parseCredential(params.plugin, content) ]; 29 | case 'remove-customer-credential': return state.filter(credential => credential._info.id !== params.credentialId); 30 | case 'update-customer-credential': return state.map(state => { 31 | if (state._info.id !== params.credentialId) { 32 | return state; 33 | } 34 | 35 | return parseCredential(params.plugin, content); 36 | }) 37 | default: return state; 38 | } 39 | } 40 | 41 | const customer = (state, log) => { 42 | const { params: { type, endpoint: { params } }, content } = log; 43 | 44 | switch (type) { 45 | case 'create-customer': return { 46 | ...parseConsumer(content), 47 | acls: [], 48 | credentials: [] 49 | }; 50 | case 'update-customer': return { 51 | ...state, 52 | ...parseConsumer(content), 53 | }; 54 | 55 | case 'remove-customer-acls': 56 | case 'add-customer-acls': 57 | if (state._info.id !== params.consumerId) { 58 | return state; 59 | } 60 | 61 | return { 62 | ...state, 63 | acls: acls(state.acls, log) 64 | }; 65 | 66 | case 'update-customer-credential': 67 | case 'remove-customer-credential': 68 | case 'add-customer-credential': 69 | if (state._info.id !== params.consumerId) { 70 | return state; 71 | } 72 | 73 | return { 74 | ...state, 75 | credentials: credentials(state.credentials, log), 76 | }; 77 | default: return state; 78 | } 79 | }; 80 | 81 | export default (state = [], log) => { 82 | if (log.type !== 'response') { 83 | return state; 84 | } 85 | 86 | const { params: { type, endpoint: { params } }, content } = log; 87 | 88 | switch (type) { 89 | case 'create-customer': return [...state, customer(undefined, log)]; 90 | case 'remove-customer': return state.filter(consumer => consumer._info.id !== params.consumerId); 91 | 92 | case 'add-customer-credential': 93 | case 'update-customer-credential': 94 | case 'remove-customer-credential': 95 | case 'add-customer-acls': 96 | case 'remove-customer-acls': 97 | case 'update-customer': return state.map(state => { 98 | if (state._info.id !== params.consumerId) { 99 | return state; 100 | } 101 | 102 | return customer(state, log); 103 | }); 104 | default: return state; 105 | } 106 | }; 107 | -------------------------------------------------------------------------------- /test-integration/plugin.test.js: -------------------------------------------------------------------------------- 1 | import execute from '../lib/core'; 2 | import { testAdminApi, logger, exportToYaml, getLog, getLocalState, tearDown } from './util'; 3 | import readKongApi from '../lib/readKongApi'; 4 | 5 | beforeEach(tearDown); 6 | 7 | describe("Integration global plugin", () => { 8 | it("should add the plugin", async () => { 9 | const config = { 10 | plugins: [{ 11 | name: "cors", 12 | attributes: { 13 | config: { 14 | credentials: false, 15 | preflight_continue: false, 16 | max_age: 7000 17 | } 18 | } 19 | }] 20 | }; 21 | 22 | await execute(config, testAdminApi, logger); 23 | const kongState = await readKongApi(testAdminApi); 24 | 25 | expect(getLog()).toMatchSnapshot(); 26 | expect(exportToYaml(kongState)).toMatchSnapshot(); 27 | expect(getLocalState()).toEqual(kongState); 28 | }); 29 | 30 | it("should update the global plugin", async () => { 31 | const config = { 32 | plugins: [{ 33 | name: "cors", 34 | attributes: { 35 | config: { 36 | credentials: false, 37 | preflight_continue: false, 38 | max_age: 7000 39 | } 40 | } 41 | }] 42 | }; 43 | 44 | await execute(config, testAdminApi, logger); 45 | 46 | config.plugins[0].attributes.enabled = false; 47 | 48 | await execute(config, testAdminApi, logger); 49 | const kongState = await readKongApi(testAdminApi); 50 | 51 | expect(getLog()).toMatchSnapshot(); 52 | expect(exportToYaml(kongState)).toMatchSnapshot(); 53 | expect(getLocalState()).toEqual(kongState); 54 | }); 55 | 56 | it("should not update if already up to date", async () => { 57 | const config = { 58 | plugins: [{ 59 | name: "cors", 60 | attributes: { 61 | config: { 62 | credentials: false, 63 | preflight_continue: false, 64 | max_age: 7000 65 | } 66 | } 67 | }] 68 | }; 69 | 70 | await execute(config, testAdminApi, logger); 71 | await execute(config, testAdminApi, logger); 72 | const kongState = await readKongApi(testAdminApi); 73 | 74 | expect(getLog()).toMatchSnapshot(); 75 | expect(exportToYaml(kongState)).toMatchSnapshot(); 76 | expect(getLocalState()).toEqual(kongState); 77 | }); 78 | 79 | it("should remove the global plugin", async () => { 80 | const config = { 81 | plugins: [{ 82 | name: "cors", 83 | attributes: { 84 | config: { 85 | credentials: false, 86 | preflight_continue: false, 87 | max_age: 7000 88 | } 89 | } 90 | }] 91 | }; 92 | 93 | await execute(config, testAdminApi, logger); 94 | 95 | config.plugins[0].ensure = 'removed'; 96 | 97 | await execute(config, testAdminApi, logger); 98 | const kongState = await readKongApi(testAdminApi); 99 | 100 | expect(getLog()).toMatchSnapshot(); 101 | expect(exportToYaml(kongState)).toMatchSnapshot(); 102 | expect(getLocalState()).toEqual(kongState); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /examples/key-auth-anonymous.example.md: -------------------------------------------------------------------------------- 1 | key auth anonymous example 2 | -------------------------- 3 | 4 | ## Config file 5 | 6 | ```yaml 7 | apis: 8 | - name: "mockbin" 9 | ensure: "present" 10 | attributes: 11 | upstream_url: "http://mockbin.com" 12 | hosts: 13 | - "mockbin.com" 14 | plugins: 15 | - name: "key-auth" 16 | attributes: 17 | config: 18 | anonymous_username: anonymous-user 19 | key_names: 20 | - very-secret-key 21 | 22 | consumers: 23 | - username: "anonymous-user" 24 | ensure: "present" 25 | - username: "iphone-app" 26 | ensure: "present" 27 | credentials: 28 | - name: "key-auth" 29 | ensure: "present" 30 | attributes: 31 | key: "very-secret-key" 32 | 33 | ``` 34 | 35 | ## Using curl 36 | 37 | For illustrative purpose a cURL calls would be the following 38 | 39 | ### create customer 40 | 41 | ```sh 42 | $ curl -i -X POST -H "Content-Type: application/json" \ 43 | --url http://localhost:8001/consumers \ 44 | --data '{"username":"anonymous-user"}' 45 | ``` 46 | 47 | ``` 48 | HTTP 201 Created 49 | ``` 50 | 51 | ``` 52 | { 53 | "created_at": "___created_at___", 54 | "username": "anonymous-user", 55 | "id": "2b47ba9b-761a-492d-9a0c-000000000001" 56 | } 57 | ``` 58 | 59 | ### create customer 60 | 61 | ```sh 62 | $ curl -i -X POST -H "Content-Type: application/json" \ 63 | --url http://localhost:8001/consumers \ 64 | --data '{"username":"iphone-app"}' 65 | ``` 66 | 67 | ``` 68 | HTTP 201 Created 69 | ``` 70 | 71 | ``` 72 | { 73 | "created_at": "___created_at___", 74 | "username": "iphone-app", 75 | "id": "2b47ba9b-761a-492d-9a0c-000000000002" 76 | } 77 | ``` 78 | 79 | ### add customer credential 80 | 81 | ```sh 82 | $ curl -i -X POST -H "Content-Type: application/json" \ 83 | --url http://localhost:8001/consumers/2b47ba9b-761a-492d-9a0c-000000000002/key-auth \ 84 | --data '{"key":"very-secret-key"}' 85 | ``` 86 | 87 | ``` 88 | HTTP 201 Created 89 | ``` 90 | 91 | ``` 92 | { 93 | "id": "2b47ba9b-761a-492d-9a0c-000000000003", 94 | "created_at": "___created_at___", 95 | "key": "very-secret-key", 96 | "consumer_id": "2b47ba9b-761a-492d-9a0c-000000000002" 97 | } 98 | ``` 99 | 100 | ### create api 101 | 102 | ```sh 103 | $ curl -i -X POST -H "Content-Type: application/json" \ 104 | --url http://localhost:8001/apis \ 105 | --data '{"upstream_url":"http://mockbin.com","hosts":["mockbin.com"],"name":"mockbin"}' 106 | ``` 107 | 108 | ``` 109 | HTTP 201 Created 110 | ``` 111 | 112 | ``` 113 | { 114 | "created_at": "___created_at___", 115 | "strip_uri": true, 116 | "id": "2b47ba9b-761a-492d-9a0c-000000000004", 117 | "hosts": [ 118 | "mockbin.com" 119 | ], 120 | "name": "mockbin", 121 | "http_if_terminated": false, 122 | "preserve_host": false, 123 | "upstream_url": "http://mockbin.com", 124 | "upstream_connect_timeout": 60000, 125 | "upstream_send_timeout": 60000, 126 | "upstream_read_timeout": 60000, 127 | "retries": 5, 128 | "https_only": false 129 | } 130 | ``` 131 | 132 | ### add api plugin 133 | 134 | ```sh 135 | $ curl -i -X POST -H "Content-Type: application/json" \ 136 | --url http://localhost:8001/apis/2b47ba9b-761a-492d-9a0c-000000000004/plugins \ 137 | --data '{"config":{"anonymous":"2b47ba9b-761a-492d-9a0c-000000000001","key_names":["very-secret-key"]},"name":"key-auth"}' 138 | ``` 139 | 140 | ``` 141 | HTTP 201 Created 142 | ``` 143 | 144 | ``` 145 | { 146 | "created_at": "___created_at___", 147 | "config": { 148 | "key_names": [ 149 | "very-secret-key" 150 | ], 151 | "key_in_body": false, 152 | "anonymous": "2b47ba9b-761a-492d-9a0c-000000000001", 153 | "run_on_preflight": true, 154 | "hide_credentials": false 155 | }, 156 | "id": "2b47ba9b-761a-492d-9a0c-000000000005", 157 | "name": "key-auth", 158 | "api_id": "2b47ba9b-761a-492d-9a0c-000000000004", 159 | "enabled": true 160 | } 161 | ``` -------------------------------------------------------------------------------- /examples/upstream.example.md: -------------------------------------------------------------------------------- 1 | upstream example 2 | ------------------ 3 | 4 | ## Config file 5 | 6 | ```yaml 7 | upstreams: 8 | - name: "mockbinUpstream" 9 | ensure: "present" 10 | targets: 11 | - target: "server1.mockbin:3001" 12 | attributes: 13 | weight: 50 14 | - target: "server2.mockbin:3001" 15 | attributes: 16 | weight: 50 17 | attributes: 18 | slots: 10 19 | ``` 20 | 21 | Please note that the port is required. If the port is not provided, then Kong will automatically provide a port using it's logic and your config will have a mismatch (see [details here](https://getkong.org/docs/latest/admin-api/#add-target)). 22 | 23 | For more information regarding upstreams and targets in Kong, read their [load balancing reference](https://getkong.org/docs/latest/loadbalancing/). 24 | 25 | ## Using cURL 26 | 27 | For illustrative purpose a cURL call would be the following 28 | 29 | ### create upstream 30 | 31 | ```bash 32 | curl -X POST \ 33 | http://localhost:8001/upstreams \ 34 | -H 'content-type: application/json' \ 35 | -d '{ 36 | "name": "mockbinUpstream", 37 | "slots": 10 38 | }' 39 | ``` 40 | 41 | ``` 42 | HTTP 201 Created 43 | ``` 44 | 45 | ```json 46 | { 47 | "orderlist": [ 48 | 10, 49 | 4, 50 | 3, 51 | 2, 52 | 6, 53 | 1, 54 | 9, 55 | 5, 56 | 8, 57 | 7 58 | ], 59 | "slots": 10, 60 | "id": "4afe745b-0ed9-4e17-82cd-5257f1ad2f1b", 61 | "name": "mockbinUpstream", 62 | "created_at": 1510849638942 63 | } 64 | ``` 65 | 66 | ### create first target 67 | 68 | ```bash 69 | curl -X POST \ 70 | http://localhost:8001/upstreams/mockbinUpstream/targets \ 71 | -H 'content-type: application/json' \ 72 | -d '{ 73 | "target": "server1.mockbin:3001", 74 | "weight": 50 75 | }' 76 | ``` 77 | 78 | ```bash 79 | HTTP 201 Created 80 | ``` 81 | 82 | ```json 83 | { 84 | "target": "server1.mockbin:3001", 85 | "id": "8899003f-79c4-48ee-906d-8d2692fd0b98", 86 | "weight": 50, 87 | "created_at": 1510849726293, 88 | "upstream_id": "4afe745b-0ed9-4e17-82cd-5257f1ad2f1b" 89 | } 90 | ``` 91 | 92 | ### create second target 93 | 94 | ```bash 95 | curl -X POST \ 96 | http://localhost:8001/upstreams/mockbinUpstream/targets \ 97 | -H 'content-type: application/json' \ 98 | -d '{ 99 | "target": "server2.mockbin:3001", 100 | "weight": 50 101 | }' 102 | ``` 103 | 104 | ```bash 105 | HTTP 201 Created 106 | ``` 107 | 108 | ```json 109 | { 110 | "target": "server2.mockbin:3001", 111 | "id": "77149168-25c9-4d04-abb6-9dcb980bd523", 112 | "weight": 50, 113 | "created_at": 1510850271896, 114 | "upstream_id": "4afe745b-0ed9-4e17-82cd-5257f1ad2f1b" 115 | } 116 | ``` 117 | 118 | Kong, by design, does not delete targets but keeps adding them. So, if there are two targets with a value of "server1.mockbin:3001", Kong chooses the most recently created one. If the most recent one has a weight of 0 (zero), then the target does not get used. To see the active targets, use the [active targets api](https://getkong.org/docs/latest/admin-api/#list-active-targets): 119 | 120 | ```bash 121 | curl -X GET \ 122 | http://localhost:8001/upstreams/mockbinUpstream/targets/active/ 123 | ``` 124 | 125 | ```bash 126 | HTTP 200 OK 127 | ``` 128 | 129 | ```json 130 | { 131 | "data": [ 132 | { 133 | "weight": 50, 134 | "id": "77149168-25c9-4d04-abb6-9dcb980bd523", 135 | "target": "server2.mockbin:3001", 136 | "created_at": 1510850271896, 137 | "upstream_id": "4afe745b-0ed9-4e17-82cd-5257f1ad2f1b" 138 | }, 139 | { 140 | "weight": 50, 141 | "id": "8899003f-79c4-48ee-906d-8d2692fd0b98", 142 | "target": "server1.mockbin:3001", 143 | "created_at": 1510849726293, 144 | "upstream_id": "4afe745b-0ed9-4e17-82cd-5257f1ad2f1b" 145 | } 146 | ], 147 | "total": 2 148 | } 149 | ``` 150 | -------------------------------------------------------------------------------- /test-integration/upstream.test.js: -------------------------------------------------------------------------------- 1 | import execute from '../lib/core'; 2 | import { testAdminApi, logger, exportToYaml, getLog, getLocalState, tearDown } from './util'; 3 | import readKongApi from '../lib/readKongApi'; 4 | 5 | beforeEach(tearDown); 6 | 7 | describe("Upstream", () => { 8 | const config = {}; 9 | 10 | beforeEach(() => { 11 | config.upstreams = [{ 12 | name: "mockbinUpstream", 13 | ensure: "present", 14 | attributes: { 15 | slots: 10 16 | } 17 | }]; 18 | }); 19 | 20 | it("should add the upstream", async () => { 21 | await execute(config, testAdminApi, logger); 22 | const kongState = await readKongApi(testAdminApi); 23 | 24 | expect(getLog()).toMatchSnapshot(); 25 | expect(exportToYaml(kongState)).toMatchSnapshot(); 26 | expect(getLocalState()).toEqual(kongState); 27 | }); 28 | 29 | it("should not update if already up to date", async () => { 30 | await execute(config, testAdminApi, logger); 31 | await execute(config, testAdminApi, logger); 32 | const kongState = await readKongApi(testAdminApi); 33 | 34 | expect(getLog()).toMatchSnapshot(); 35 | expect(exportToYaml(kongState)).toMatchSnapshot(); 36 | expect(getLocalState()).toEqual(kongState); 37 | }); 38 | 39 | it("should remove the upstream", async () => { 40 | config.upstreams[0].ensure = "removed"; 41 | await execute(config, testAdminApi, logger); 42 | const kongState = await readKongApi(testAdminApi); 43 | 44 | expect(getLog()).toMatchSnapshot(); 45 | expect(exportToYaml(kongState)).toMatchSnapshot(); 46 | expect(getLocalState()).toEqual(kongState); 47 | }); 48 | 49 | it("should update the upstream", async () => { 50 | await execute(config, testAdminApi, logger); 51 | 52 | config.upstreams[0].attributes.slots = 20; 53 | 54 | await execute(config, testAdminApi, logger); 55 | const kongState = await readKongApi(testAdminApi); 56 | 57 | expect(getLog()).toMatchSnapshot(); 58 | expect(exportToYaml(kongState)).toMatchSnapshot(); 59 | expect(getLocalState()).toEqual(kongState); 60 | }); 61 | }); 62 | 63 | describe("Upstream Targets", () => { 64 | let config = {}; 65 | let upstream, target1; 66 | 67 | beforeEach(() => { 68 | config.upstreams = [{ 69 | name: "mockbinUpstream", 70 | ensure: "present", 71 | attributes: { 72 | slots: 10 73 | }, 74 | targets: [{ 75 | ensure: "present", 76 | target: "server1.mockbin:8080", 77 | attributes: { 78 | weight: 50 79 | } 80 | }] 81 | }]; 82 | 83 | upstream = config.upstreams[0]; 84 | target1 = upstream.targets[0]; 85 | }); 86 | 87 | it("should add mockbin upstream with target", async () => { 88 | await execute(config, testAdminApi, logger); 89 | const kongState = await readKongApi(testAdminApi); 90 | 91 | expect(getLog()).toMatchSnapshot(); 92 | expect(exportToYaml(kongState)).toMatchSnapshot(); 93 | expect(getLocalState()).toEqual(kongState); 94 | }); 95 | 96 | it("should remove target from mockbin upstream", async () => { 97 | await execute(config, testAdminApi, logger); 98 | 99 | target1.ensure = 'removed'; 100 | 101 | await execute(config, testAdminApi, logger); 102 | 103 | const kongState = await readKongApi(testAdminApi); 104 | 105 | expect(getLog()).toMatchSnapshot(); 106 | expect(exportToYaml(kongState)).toMatchSnapshot(); 107 | expect(getLocalState()).toEqual(kongState); 108 | }); 109 | 110 | it("should update mockbin upstream target", async () => { 111 | await execute(config, testAdminApi, logger); 112 | 113 | let weight = target1.attributes.weight; 114 | target1.attributes.weight = weight * 2; 115 | 116 | await execute(config, testAdminApi, logger); 117 | 118 | const kongState = await readKongApi(testAdminApi); 119 | 120 | expect(getLog()).toMatchSnapshot(); 121 | expect(exportToYaml(kongState)).toMatchSnapshot(); 122 | expect(getLocalState()).toEqual(kongState); 123 | }) 124 | }); 125 | -------------------------------------------------------------------------------- /src/adminApi.js: -------------------------------------------------------------------------------- 1 | import createRouter from './router'; 2 | import requester from './requester'; 3 | import { parseVersion } from './utils.js' 4 | 5 | let pluginSchemasCache; 6 | let kongVersionCache; 7 | let resultsCache = {}; 8 | 9 | export default ({host, https, ignoreConsumers, cache}) => { 10 | const router = createRouter(host, https); 11 | 12 | return createApi({ 13 | router, 14 | ignoreConsumers, 15 | getPaginatedJson: cache ? getPaginatedJsonCache : getPaginatedJson, 16 | }); 17 | } 18 | 19 | function createApi({ router, getPaginatedJson, ignoreConsumers }) { 20 | return { 21 | router, 22 | fetchApis: () => getPaginatedJson(router({name: 'apis'})), 23 | fetchGlobalPlugins: () => getPaginatedJson(router({name: 'plugins'})), 24 | fetchPlugins: apiId => getPaginatedJson(router({name: 'api-plugins', params: {apiId}})), 25 | fetchConsumers: () => ignoreConsumers ? Promise.resolve([]) : getPaginatedJson(router({name: 'consumers'})), 26 | fetchConsumerCredentials: (consumerId, plugin) => getPaginatedJson(router({name: 'consumer-credentials', params: {consumerId, plugin}})), 27 | fetchConsumerAcls: (consumerId) => getPaginatedJson(router({name: 'consumer-acls', params: {consumerId}})), 28 | fetchUpstreams: () => getPaginatedJson(router({name: 'upstreams'})), 29 | fetchTargets: (upstreamId) => getPaginatedJson(router({name: 'upstream-targets-active', params: {upstreamId}})), 30 | 31 | // this is very chatty call and doesn't change so its cached 32 | fetchPluginSchemas: () => { 33 | if (pluginSchemasCache) { 34 | return Promise.resolve(pluginSchemasCache); 35 | } 36 | 37 | return getPaginatedJson(router({name: 'plugins-enabled'})) 38 | .then(json => Promise.all(getEnabledPluginNames(json.enabled_plugins).map(plugin => getPluginScheme(plugin, plugin => router({name: 'plugins-scheme', params: {plugin}}))))) 39 | .then(all => pluginSchemasCache = new Map(all)); 40 | }, 41 | fetchKongVersion: () => { 42 | if (kongVersionCache) { 43 | return Promise.resolve(kongVersionCache); 44 | } 45 | 46 | return getPaginatedJson(router({name: 'root'})) 47 | .then(json => Promise.resolve(json.version)) 48 | .then(version => kongVersionCache = parseVersion(version)); 49 | }, 50 | requestEndpoint: (endpoint, params) => { 51 | resultsCache = {}; 52 | return requester.request(router(endpoint), prepareOptions(params)); 53 | } 54 | }; 55 | } 56 | 57 | function getEnabledPluginNames(enabledPlugins) { 58 | if (!Array.isArray(enabledPlugins)) { 59 | return Object.keys(enabledPlugins); 60 | } 61 | 62 | return enabledPlugins; 63 | } 64 | 65 | function getPaginatedJsonCache(uri) { 66 | if (resultsCache.hasOwnProperty(uri)) { 67 | return resultsCache[uri]; 68 | } 69 | 70 | let result = getPaginatedJson(uri); 71 | resultsCache[uri] = result; 72 | 73 | return result; 74 | } 75 | 76 | function getPluginScheme(plugin, schemaRoute) { 77 | return getPaginatedJson(schemaRoute(plugin)) 78 | .then(({fields}) => [plugin, fields]); 79 | } 80 | 81 | function getPaginatedJson(uri) { 82 | return requester.get(uri) 83 | .then(response => { 84 | if (!response.ok) { 85 | const error = new Error(`${uri}: ${response.status} ${response.statusText}`); 86 | error.response = response; 87 | 88 | throw error; 89 | } 90 | 91 | return response; 92 | }) 93 | .then(r => r.json()) 94 | .then(json => { 95 | if (!json.data) return json; 96 | if (!json.next) return json.data; 97 | 98 | if (json.data.length < 100) { 99 | // FIXME an hopeful hack to prevent a loop 100 | return json.data; 101 | } 102 | 103 | return getPaginatedJson(json.next).then(data => json.data.concat(data)); 104 | }); 105 | } 106 | 107 | const prepareOptions = ({method, body}) => { 108 | if (body) { 109 | return { 110 | method: method, 111 | headers: { 112 | 'Accept': 'application/json', 113 | 'Content-Type': 'application/json' 114 | }, 115 | body: JSON.stringify(body) 116 | }; 117 | } 118 | 119 | return { 120 | method: method, 121 | headers: { 122 | 'Accept': 'application/json', 123 | } 124 | }; 125 | } -------------------------------------------------------------------------------- /test/requester.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import requester from '../src/requester.js'; 3 | 4 | let actualRequest = {}; 5 | 6 | global.fetch = (url, options) => { 7 | actualRequest = { 8 | url, 9 | options 10 | }; 11 | 12 | const promise = { 13 | then: () => promise, 14 | catch: () => promise, 15 | }; 16 | 17 | return promise; 18 | }; 19 | 20 | describe('requester', () => { 21 | beforeEach(() => { 22 | actualRequest = {}; 23 | requester.clearHeaders(); 24 | }); 25 | 26 | it('should get', () => { 27 | const expectedRequest = { 28 | url: 'http://example.com', 29 | options: { 30 | method: 'GET', 31 | headers: { 32 | 'Connection': 'keep-alive', 33 | 'Accept': 'application/json' 34 | } 35 | } 36 | }; 37 | 38 | requester.get('http://example.com'); 39 | 40 | expect(actualRequest).to.be.eql(expectedRequest); 41 | }); 42 | 43 | it('should get with custom headers', () => { 44 | const expectedRequest = { 45 | url: 'http://example.com', 46 | options: { 47 | method: 'GET', 48 | headers: { 49 | 'Connection': 'keep-alive', 50 | 'Accept': 'application/json', 51 | 'CustomHeader1': 'CustomValue1', 52 | 'CustomHeader2': 'CustomValue2' 53 | } 54 | } 55 | }; 56 | 57 | requester.addHeader('CustomHeader1', 'CustomValue1'); 58 | requester.addHeader('CustomHeader2', 'CustomValue2'); 59 | requester.get('http://example.com'); 60 | 61 | expect(actualRequest).to.be.eql(expectedRequest); 62 | }); 63 | 64 | it('should make requests', () => { 65 | const expectedRequest = { 66 | url: 'http://example.com', 67 | options: { 68 | method: 'POST', 69 | headers: { 70 | 'Connection': 'keep-alive', 71 | 'Accept': 'application/json', 72 | 'Content-Type': 'application/json' 73 | }, 74 | body: 'This is the body' 75 | } 76 | }; 77 | 78 | requester.request('http://example.com', { 79 | method: 'POST', 80 | body: 'This is the body', 81 | headers: { 82 | 'Connection': 'keep-alive', 83 | 'Accept': 'application/json', 84 | 'Content-Type': 'application/json' 85 | } 86 | }); 87 | 88 | expect(actualRequest).to.be.eql(expectedRequest); 89 | }); 90 | 91 | it('should make requests with custom headers', () => { 92 | const expectedRequest = { 93 | url: 'http://example.com', 94 | options: { 95 | method: 'POST', 96 | headers: { 97 | 'Connection': 'keep-alive', 98 | 'Accept': 'application/json', 99 | 'Content-Type': 'application/json', 100 | 'CustomHeader1': 'CustomValue1', 101 | 'CustomHeader2': 'CustomValue2' 102 | }, 103 | body: 'This is the body' 104 | } 105 | }; 106 | 107 | requester.addHeader('CustomHeader1', 'CustomValue1'); 108 | requester.addHeader('CustomHeader2', 'CustomValue2'); 109 | requester.request('http://example.com', { 110 | method: 'POST', 111 | body: 'This is the body', 112 | headers: { 113 | 'Connection': 'keep-alive', 114 | 'Accept': 'application/json', 115 | 'Content-Type': 'application/json' 116 | } 117 | }); 118 | 119 | expect(actualRequest).to.be.eql(expectedRequest); 120 | }); 121 | 122 | it('should clear headers', () => { 123 | const expectedRequest = { 124 | url: 'http://example.com', 125 | options: { 126 | method: 'GET', 127 | headers: { 128 | 'Connection': 'keep-alive', 129 | 'Accept': 'application/json' 130 | } 131 | } 132 | }; 133 | 134 | requester.addHeader('CustomHeader1', 'CustomValue1'); 135 | requester.addHeader('CustomHeader2', 'CustomValue2'); 136 | requester.clearHeaders(); 137 | requester.get('http://example.com'); 138 | 139 | expect(actualRequest).to.be.eql(expectedRequest); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import assign from 'object-assign'; 2 | import invariant from 'invariant'; 3 | 4 | export function noop({ type, ...rest} = {}) { 5 | invariant(type, 'No-op must have a type'); 6 | 7 | return { 8 | type, 9 | noop: true, 10 | ...rest, 11 | }; 12 | } 13 | 14 | export function createApi(name, params) { 15 | return { 16 | type: 'create-api', 17 | endpoint: {name: 'apis'}, 18 | method: 'POST', 19 | body: assign({}, params, {name}) 20 | }; 21 | }; 22 | 23 | export function removeApi(name) { 24 | return { 25 | type: 'remove-api', 26 | endpoint: {name: 'api', params: {name}}, 27 | method: 'DELETE', 28 | }; 29 | } 30 | 31 | export function updateApi(name, params) { 32 | return { 33 | type: 'update-api', 34 | endpoint: {name: 'api', params: {name}}, 35 | method: 'PATCH', 36 | body: params 37 | }; 38 | } 39 | 40 | export function addApiPlugin(apiId, pluginName, params) { 41 | return { 42 | type: 'add-api-plugin', 43 | endpoint: {name: 'api-plugins', params: {apiId, pluginName}}, 44 | method: 'POST', 45 | body: assign({}, params, {name: pluginName}) 46 | }; 47 | } 48 | 49 | export function removeApiPlugin(apiId, pluginId) { 50 | return { 51 | type: 'remove-api-plugin', 52 | endpoint: {name: 'api-plugin', params: {apiId, pluginId}}, 53 | method: 'DELETE', 54 | }; 55 | } 56 | 57 | export function updateApiPlugin(apiId, pluginId, params) { 58 | return { 59 | type: 'update-api-plugin', 60 | endpoint: {name: 'api-plugin', params: {apiId, pluginId}}, 61 | method: 'PATCH', 62 | body: params 63 | }; 64 | } 65 | 66 | export function addGlobalPlugin(pluginName, params) { 67 | return { 68 | type: 'add-global-plugin', 69 | endpoint: {name: 'plugins', params: {pluginName}}, 70 | method: 'POST', 71 | body: assign({}, params, {name: pluginName}) 72 | }; 73 | } 74 | 75 | export function removeGlobalPlugin(pluginId) { 76 | return { 77 | type: 'remove-global-plugin', 78 | endpoint: {name: 'plugin', params: {pluginId}}, 79 | method: 'DELETE', 80 | }; 81 | } 82 | 83 | export function updateGlobalPlugin(pluginId, params) { 84 | return { 85 | type: 'update-global-plugin', 86 | endpoint: {name: 'plugin', params: {pluginId}}, 87 | method: 'PATCH', 88 | body: params 89 | }; 90 | } 91 | 92 | export function createConsumer(username, custom_id) { 93 | return { 94 | type: 'create-customer', 95 | endpoint: { name: 'consumers' }, 96 | method: 'POST', 97 | body: { username, custom_id } 98 | }; 99 | } 100 | 101 | export function updateConsumer(consumerId, params) { 102 | return { 103 | type: 'update-customer', 104 | endpoint: {name: 'consumer', params: {consumerId}}, 105 | method: 'PATCH', 106 | body: params 107 | }; 108 | } 109 | 110 | export function removeConsumer(consumerId) { 111 | return { 112 | type: 'remove-customer', 113 | endpoint: {name: 'consumer', params: {consumerId}}, 114 | method: 'DELETE' 115 | }; 116 | } 117 | 118 | export function addConsumerCredentials(consumerId, plugin, params) { 119 | return { 120 | type: 'add-customer-credential', 121 | endpoint: {name: 'consumer-credentials', params: {consumerId, plugin}}, 122 | method: 'POST', 123 | body: params 124 | }; 125 | } 126 | 127 | export function updateConsumerCredentials(consumerId, plugin, credentialId, params) { 128 | return { 129 | type: 'update-customer-credential', 130 | endpoint: {name: 'consumer-credential', params: {consumerId, plugin, credentialId}}, 131 | method: 'PATCH', 132 | body: params 133 | }; 134 | } 135 | 136 | export function removeConsumerCredentials(consumerId, plugin, credentialId) { 137 | return { 138 | type: 'remove-customer-credential', 139 | endpoint: {name: 'consumer-credential', params: {consumerId, plugin, credentialId}}, 140 | method: 'DELETE' 141 | }; 142 | } 143 | 144 | export function addConsumerAcls(consumerId, groupName) { 145 | return { 146 | type: 'add-customer-acls', 147 | endpoint: {name: 'consumer-acls', params: {consumerId}}, 148 | method: 'POST', 149 | body: { 150 | group: groupName 151 | } 152 | }; 153 | } 154 | 155 | export function removeConsumerAcls(consumerId, aclId) { 156 | return { 157 | type: 'remove-customer-acls', 158 | endpoint: {name: 'consumer-acl', params: {consumerId, aclId}}, 159 | method: 'DELETE' 160 | }; 161 | } 162 | -------------------------------------------------------------------------------- /test/apis.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import {apis, plugins} from '../src/core.js'; 3 | import { 4 | noop, 5 | createApi, 6 | removeApi, 7 | updateApi, 8 | addApiPlugin, 9 | removeApiPlugin, 10 | updateApiPlugin 11 | } from '../src/actions.js'; 12 | 13 | describe("apis", () => { 14 | it("should add new api", () => { 15 | var actual = apis([{ 16 | "ensure": "present", 17 | "name": "leads", 18 | "attributes": { 19 | "upstream_url": "bar" 20 | } 21 | }]) 22 | .map(x => x({hasApi: () => false, getVersion: () => '0.10.0'})); 23 | 24 | expect(actual).to.be.eql([ 25 | createApi('leads', {upstream_url: "bar"}) 26 | ]); 27 | }); 28 | 29 | it("should remove api", () => { 30 | var actual = apis([{ 31 | "name": "leads", 32 | "ensure": "removed", 33 | "attributes": { 34 | "upstream_url": "bar" 35 | } 36 | }]) 37 | .map(x => x({hasApi: () => true, getVersion: () => '0.10.0'})); 38 | 39 | expect(actual).to.be.eql([ 40 | removeApi('leads') 41 | ]); 42 | }); 43 | 44 | it("should do no op if api is already removed", () => { 45 | const api = { 46 | "name": "leads", 47 | "ensure": "removed", 48 | "attributes": { 49 | "upstream_url": "bar" 50 | } 51 | }; 52 | var actual = apis([api]) 53 | .map(x => x({hasApi: () => false, getVersion: () => '0.10.0'})); 54 | 55 | expect(actual).to.be.eql([ 56 | noop({ type: 'noop-api', api }) 57 | ]); 58 | }); 59 | 60 | it("should update api", () => { 61 | var actual = apis([{ 62 | "ensure": "present", 63 | "name": "leads", 64 | "attributes": { 65 | "upstream_url": "bar" 66 | } 67 | }]) 68 | .map(x => x({hasApi: () => true, isApiUpToDate: () => false, getVersion: () => '0.10.0'})); 69 | 70 | expect(actual).to.be.eql([ 71 | updateApi('leads', {upstream_url: "bar"}) 72 | ]); 73 | }); 74 | 75 | it("should validate ensure enum", () => { 76 | expect(() => apis([{ 77 | "ensure": "not-valid", 78 | "name": "leads" 79 | }])).to.throwException(/Invalid ensure/); 80 | }); 81 | 82 | it('should add api with plugins', () => { 83 | var actual = apis([{ 84 | "ensure": "present", 85 | "name": "leads", 86 | "attributes": { 87 | "upstream_url": "bar" 88 | }, 89 | 'plugins': [{ 90 | "name": 'cors', 91 | "ensure": "present", 92 | 'attributes': { 93 | 'config.foo': "bar" 94 | } 95 | }] 96 | }]).map(x => x({ 97 | hasApi: () => false, 98 | hasPlugin: () => false, 99 | getApiId: () => 'abcd-1234', 100 | getVersion: () => '0.10.0' 101 | })); 102 | 103 | expect(actual).to.be.eql([ 104 | createApi('leads', {upstream_url: "bar"}), 105 | addApiPlugin('abcd-1234', 'cors', {'config.foo': "bar"}) 106 | ]); 107 | }); 108 | 109 | describe("plugins", () => { 110 | it("should add a plugin to an api", () => { 111 | var actual = plugins( 112 | 'leads', [{ 113 | "name": "cors", 114 | 'attributes': { 115 | "config.foo": 'bar' 116 | }}] 117 | ).map(x => x({hasPlugin: () => false, getApiId: () => 'abcd-1234'})); 118 | 119 | expect(actual).to.be.eql([ 120 | addApiPlugin('abcd-1234', 'cors', {"config.foo": 'bar'}) 121 | ]); 122 | }); 123 | 124 | it("should remove api plugin", () => { 125 | var actual = plugins( 126 | 'leads', [{ 127 | "name": "cors", 128 | "ensure": "removed"}] 129 | ).map(x => x({ 130 | hasPlugin: () => true, 131 | getPluginId: () => 123, 132 | getApiId: () => 'abcd-1234', 133 | })); 134 | 135 | expect(actual).to.be.eql([ 136 | removeApiPlugin('abcd-1234', 123) 137 | ]); 138 | }); 139 | 140 | it('should update api plugin', () => { 141 | var actual = plugins( 142 | 'leads', [{ 143 | 'name': 'cors', 144 | 'attributes': { 145 | 'config.foo': 'bar' 146 | }}] 147 | ).map(x => x({ 148 | hasPlugin: () => true, 149 | getPluginId: () => 123, 150 | getApiId: () => 'abcd-1234', 151 | isApiPluginUpToDate: () => false 152 | })); 153 | 154 | expect(actual).to.be.eql([ 155 | updateApiPlugin('abcd-1234', 123, {'config.foo': 'bar'}) 156 | ]) 157 | }); 158 | 159 | it("should validate ensure enum", () => { 160 | expect(() => plugins("leads", [{ 161 | "ensure": "not-valid", 162 | "name": "leads" 163 | }])).to.throwException(/Invalid ensure/); 164 | }); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /examples/plugin-per-consumer.example.md: -------------------------------------------------------------------------------- 1 | plugin per consumer example 2 | --------------------------- 3 | 4 | ## Config file 5 | 6 | ```yaml 7 | consumers: 8 | - username: user-john 9 | 10 | apis: 11 | - name: mockbin-foo 12 | attributes: 13 | upstream_url: http://mockbin.com 14 | uris: 15 | - /foo 16 | plugins: 17 | - name: rate-limiting 18 | ensure: "present" 19 | attributes: 20 | username: user-john 21 | config: 22 | second: 10 23 | 24 | - name: mockbin-bar 25 | attributes: 26 | upstream_url: http://mockbin.com 27 | uris: 28 | - /bar 29 | 30 | plugins: 31 | - name: rate-limiting 32 | attributes: 33 | username: user-john 34 | enabled: true 35 | config: 36 | minute: 60 37 | 38 | - name: rate-limiting 39 | attributes: 40 | enabled: true 41 | config: 42 | minute: 30 43 | 44 | ``` 45 | 46 | ## Using curl 47 | 48 | For illustrative purpose a cURL calls would be the following 49 | 50 | ### create customer 51 | 52 | ```sh 53 | $ curl -i -X POST -H "Content-Type: application/json" \ 54 | --url http://localhost:8001/consumers \ 55 | --data '{"username":"user-john"}' 56 | ``` 57 | 58 | ``` 59 | HTTP 201 Created 60 | ``` 61 | 62 | ``` 63 | { 64 | "created_at": "___created_at___", 65 | "username": "user-john", 66 | "id": "2b47ba9b-761a-492d-9a0c-000000000001" 67 | } 68 | ``` 69 | 70 | ### create api 71 | 72 | ```sh 73 | $ curl -i -X POST -H "Content-Type: application/json" \ 74 | --url http://localhost:8001/apis \ 75 | --data '{"upstream_url":"http://mockbin.com","uris":["/foo"],"name":"mockbin-foo"}' 76 | ``` 77 | 78 | ``` 79 | HTTP 201 Created 80 | ``` 81 | 82 | ``` 83 | { 84 | "created_at": "___created_at___", 85 | "strip_uri": true, 86 | "id": "2b47ba9b-761a-492d-9a0c-000000000002", 87 | "name": "mockbin-foo", 88 | "http_if_terminated": false, 89 | "preserve_host": false, 90 | "upstream_url": "http://mockbin.com", 91 | "uris": [ 92 | "/foo" 93 | ], 94 | "upstream_connect_timeout": 60000, 95 | "upstream_send_timeout": 60000, 96 | "upstream_read_timeout": 60000, 97 | "retries": 5, 98 | "https_only": false 99 | } 100 | ``` 101 | 102 | ### add api plugin 103 | 104 | ```sh 105 | $ curl -i -X POST -H "Content-Type: application/json" \ 106 | --url http://localhost:8001/apis/2b47ba9b-761a-492d-9a0c-000000000002/plugins \ 107 | --data '{"consumer_id":"2b47ba9b-761a-492d-9a0c-000000000001","config":{"second":10},"name":"rate-limiting"}' 108 | ``` 109 | 110 | ``` 111 | HTTP 201 Created 112 | ``` 113 | 114 | ``` 115 | { 116 | "created_at": "___created_at___", 117 | "config": { 118 | "second": 10, 119 | "redis_database": 0, 120 | "policy": "cluster", 121 | "hide_client_headers": false, 122 | "redis_timeout": 2000, 123 | "redis_port": 6379, 124 | "limit_by": "consumer", 125 | "fault_tolerant": true 126 | }, 127 | "id": "2b47ba9b-761a-492d-9a0c-000000000003", 128 | "enabled": true, 129 | "name": "rate-limiting", 130 | "api_id": "2b47ba9b-761a-492d-9a0c-000000000002", 131 | "consumer_id": "2b47ba9b-761a-492d-9a0c-000000000001" 132 | } 133 | ``` 134 | 135 | ### create api 136 | 137 | ```sh 138 | $ curl -i -X POST -H "Content-Type: application/json" \ 139 | --url http://localhost:8001/apis \ 140 | --data '{"upstream_url":"http://mockbin.com","uris":["/bar"],"name":"mockbin-bar"}' 141 | ``` 142 | 143 | ``` 144 | HTTP 201 Created 145 | ``` 146 | 147 | ``` 148 | { 149 | "created_at": "___created_at___", 150 | "strip_uri": true, 151 | "id": "2b47ba9b-761a-492d-9a0c-000000000004", 152 | "name": "mockbin-bar", 153 | "http_if_terminated": false, 154 | "preserve_host": false, 155 | "upstream_url": "http://mockbin.com", 156 | "uris": [ 157 | "/bar" 158 | ], 159 | "upstream_connect_timeout": 60000, 160 | "upstream_send_timeout": 60000, 161 | "upstream_read_timeout": 60000, 162 | "retries": 5, 163 | "https_only": false 164 | } 165 | ``` 166 | 167 | ### add global plugin 168 | 169 | ```sh 170 | $ curl -i -X POST -H "Content-Type: application/json" \ 171 | --url http://localhost:8001/plugins \ 172 | --data '{"consumer_id":"2b47ba9b-761a-492d-9a0c-000000000001","enabled":true,"config":{"minute":60},"name":"rate-limiting"}' 173 | ``` 174 | 175 | ``` 176 | HTTP 201 Created 177 | ``` 178 | 179 | ``` 180 | { 181 | "created_at": "___created_at___", 182 | "config": { 183 | "hide_client_headers": false, 184 | "minute": 60, 185 | "policy": "cluster", 186 | "redis_database": 0, 187 | "redis_timeout": 2000, 188 | "redis_port": 6379, 189 | "limit_by": "consumer", 190 | "fault_tolerant": true 191 | }, 192 | "id": "2b47ba9b-761a-492d-9a0c-000000000005", 193 | "name": "rate-limiting", 194 | "enabled": true, 195 | "consumer_id": "2b47ba9b-761a-492d-9a0c-000000000001" 196 | } 197 | ``` 198 | 199 | ### add global plugin 200 | 201 | ```sh 202 | $ curl -i -X POST -H "Content-Type: application/json" \ 203 | --url http://localhost:8001/plugins \ 204 | --data '{"enabled":true,"config":{"minute":30},"name":"rate-limiting"}' 205 | ``` 206 | 207 | ``` 208 | HTTP 201 Created 209 | ``` 210 | 211 | ``` 212 | { 213 | "created_at": "___created_at___", 214 | "config": { 215 | "hide_client_headers": false, 216 | "minute": 30, 217 | "policy": "cluster", 218 | "redis_database": 0, 219 | "redis_timeout": 2000, 220 | "redis_port": 6379, 221 | "limit_by": "consumer", 222 | "fault_tolerant": true 223 | }, 224 | "id": "2b47ba9b-761a-492d-9a0c-000000000006", 225 | "enabled": true, 226 | "name": "rate-limiting" 227 | } 228 | ``` -------------------------------------------------------------------------------- /src/readKongApi.js: -------------------------------------------------------------------------------- 1 | import semVer from 'semver'; 2 | import kongState from './kongState'; 3 | import { parseUpstreams } from './parsers/upstreams'; 4 | import getCurrentStateSelector from './stateSelector'; 5 | 6 | export default async (adminApi) => { 7 | return Promise.all([kongState(adminApi), adminApi.fetchPluginSchemas(), adminApi.fetchKongVersion()]) 8 | .then(([state, schemas, version]) => { 9 | return getCurrentStateSelector({ 10 | _info: { version }, 11 | apis: parseApis(state.apis, version), 12 | consumers: parseConsumers(state.consumers), 13 | plugins: parseGlobalPlugins(state.plugins), 14 | upstreams: semVer.gte(version, '0.10.0') ? parseUpstreams(state.upstreams) : undefined, 15 | }); 16 | }) 17 | }; 18 | 19 | export const parseConsumer = ({ username, custom_id, credentials, acls, ..._info }) => { 20 | return { 21 | username, 22 | custom_id, 23 | _info, 24 | }; 25 | }; 26 | 27 | export const parseAcl = ({group, ..._info}) => ({group, _info}); 28 | 29 | function parseConsumers(consumers) { 30 | return consumers.map(({username, custom_id, credentials, acls, ..._info}) => { 31 | return { 32 | ...parseConsumer({ username, custom_id, ..._info}), 33 | acls: Array.isArray(acls) ? acls.map(parseAcl) : [], 34 | credentials: zip(Object.keys(credentials), Object.values(credentials)) 35 | .map(parseCredential) 36 | .reduce((acc, x) => acc.concat(x), []) 37 | }; 38 | }); 39 | } 40 | 41 | function zip(a, b) { 42 | return a.map((n, index) => [n, b[index]]); 43 | } 44 | 45 | function parseCredential([credentialName, credentials]) { 46 | if (!Array.isArray(credentials)) { 47 | return []; 48 | } 49 | 50 | return credentials.map(({consumer_id, id, created_at, ...attributes}) => { 51 | return { 52 | name: credentialName, 53 | attributes, 54 | _info: {id, consumer_id, created_at} 55 | } 56 | }); 57 | } 58 | 59 | function parseApis(apis, kongVersion) { 60 | if (semVer.gte(kongVersion, '0.10.0')) { 61 | return parseApisV10(apis); 62 | } 63 | 64 | return parseApisBeforeV10(apis); 65 | } 66 | 67 | const parseApiPreV10 = ({ 68 | name, 69 | request_host, request_path, strip_request_path, preserve_host, upstream_url, 70 | id, created_at}) => { 71 | return { 72 | name, 73 | plugins: [], 74 | attributes: { 75 | request_host, 76 | request_path, 77 | strip_request_path, 78 | preserve_host, 79 | upstream_url, 80 | }, 81 | _info: { 82 | id, 83 | created_at 84 | } 85 | }; 86 | }; 87 | 88 | export const parseApiPostV10 = ({ 89 | name, plugins, 90 | hosts, uris, methods, 91 | strip_uri, preserve_host, upstream_url, id, created_at, 92 | https_only, http_if_terminated, 93 | retries, upstream_connect_timeout, upstream_read_timeout, upstream_send_timeout}) => { 94 | return { 95 | name, 96 | attributes: { 97 | hosts, 98 | uris, 99 | methods, 100 | strip_uri, 101 | preserve_host, 102 | upstream_url, 103 | retries, 104 | upstream_connect_timeout, 105 | upstream_read_timeout, 106 | upstream_send_timeout, 107 | https_only, 108 | http_if_terminated 109 | }, 110 | _info: { 111 | id, 112 | created_at 113 | } 114 | }; 115 | }; 116 | 117 | const withParseApiPlugins = (parseApi) => api => { 118 | const { name, ...rest} = parseApi(api); 119 | 120 | return { name, plugins: parseApiPlugins(api.plugins), ...rest }; 121 | }; 122 | 123 | function parseApisBeforeV10(apis) { 124 | return apis.map(withParseApiPlugins(parseApiPreV10)); 125 | } 126 | 127 | function parseApisV10(apis) { 128 | return apis.map(withParseApiPlugins(parseApiPostV10)); 129 | } 130 | 131 | export const parsePlugin = ({ 132 | name, 133 | config, 134 | id, api_id, consumer_id, enabled, created_at 135 | }) => { 136 | return { 137 | name, 138 | attributes: { 139 | enabled, 140 | consumer_id, 141 | config: stripConfig(config) 142 | }, 143 | _info: { 144 | id, 145 | //api_id, 146 | consumer_id, 147 | created_at 148 | } 149 | }; 150 | }; 151 | 152 | function parseApiPlugins(plugins) { 153 | if (!Array.isArray(plugins)) { 154 | return []; 155 | } 156 | 157 | return plugins.map(parsePlugin); 158 | } 159 | 160 | export const parseGlobalPlugin = ({ 161 | name, 162 | enabled, 163 | config, 164 | id, api_id, consumer_id, created_at 165 | }) => { 166 | return { 167 | name, 168 | attributes: { 169 | enabled, 170 | consumer_id, 171 | config: stripConfig(config) 172 | }, 173 | _info: { 174 | id, 175 | api_id, 176 | consumer_id, 177 | created_at 178 | } 179 | }; 180 | }; 181 | 182 | function parseGlobalPlugins(plugins) { 183 | if (!Array.isArray(plugins)) { 184 | return []; 185 | } 186 | 187 | return plugins 188 | .map(parseGlobalPlugin) 189 | .filter(x => x.name); 190 | } 191 | 192 | function stripConfig(config) { 193 | const mutableConfig = {...config}; 194 | 195 | // remove some cache values 196 | delete mutableConfig['_key_der_cache']; 197 | delete mutableConfig['_cert_der_cache']; 198 | 199 | return mutableConfig; 200 | } 201 | -------------------------------------------------------------------------------- /test-integration/customers.test.js: -------------------------------------------------------------------------------- 1 | import execute from '../lib/core'; 2 | import { testAdminApi, logger, exportToYaml, getLog, getLocalState, tearDown } from './util'; 3 | import readKongApi from '../lib/readKongApi'; 4 | 5 | beforeEach(tearDown); 6 | 7 | describe("Integration consumers", () => { 8 | it("should add the consumer", async () => { 9 | const config = { 10 | consumers: [{ 11 | username: "iphone-app", 12 | ensure: "present", 13 | }] 14 | }; 15 | 16 | await execute(config, testAdminApi, logger); 17 | const kongState = await readKongApi(testAdminApi); 18 | 19 | expect(getLog()).toMatchSnapshot(); 20 | expect(exportToYaml(kongState)).toMatchSnapshot(); 21 | expect(getLocalState()).toEqual(kongState); 22 | }); 23 | 24 | it("should update the consumer", async () => { 25 | const config = { 26 | consumers: [{ 27 | username: "iphone-app", 28 | ensure: "present", 29 | }] 30 | }; 31 | 32 | await execute(config, testAdminApi, logger); 33 | 34 | config.consumers[0].custom_id = 'foobar123'; 35 | await execute(config, testAdminApi, logger); 36 | 37 | const kongState = await readKongApi(testAdminApi); 38 | 39 | expect(getLog()).toMatchSnapshot(); 40 | expect(exportToYaml(kongState)).toMatchSnapshot(); 41 | expect(getLocalState()).toEqual(kongState); 42 | }); 43 | 44 | it("should remove the consumer", async () => { 45 | const config = { 46 | consumers: [{ 47 | username: "iphone-app", 48 | ensure: "present", 49 | }] 50 | }; 51 | 52 | await execute(config, testAdminApi, logger); 53 | 54 | config.consumers[0].ensure = 'removed'; 55 | await execute(config, testAdminApi, logger); 56 | 57 | const kongState = await readKongApi(testAdminApi); 58 | 59 | expect(getLog()).toMatchSnapshot(); 60 | expect(exportToYaml(kongState)).toMatchSnapshot(); 61 | expect(getLocalState()).toEqual(kongState); 62 | }); 63 | }); 64 | 65 | describe('Integration consumers credentials', () => { 66 | it("should add the credential", async () => { 67 | const config = { 68 | consumers: [{ 69 | username: "iphone-app", 70 | ensure: "present", 71 | credentials: [{ 72 | name: "key-auth", 73 | ensure: "present", 74 | attributes: { 75 | key: "very-secret-key" 76 | } 77 | }] 78 | }] 79 | }; 80 | 81 | await execute(config, testAdminApi, logger); 82 | const kongState = await readKongApi(testAdminApi); 83 | 84 | expect(getLog()).toMatchSnapshot(); 85 | expect(exportToYaml(kongState)).toMatchSnapshot(); 86 | expect(getLocalState()).toEqual(kongState); 87 | }); 88 | 89 | it("should update the credential", async () => { 90 | const config = { 91 | consumers: [{ 92 | username: "iphone-app", 93 | ensure: "present", 94 | credentials: [{ 95 | name: "hmac-auth", 96 | ensure: "present", 97 | attributes: { 98 | username: "my-user", 99 | secret: "the secrent" 100 | } 101 | }] 102 | }] 103 | }; 104 | 105 | await execute(config, testAdminApi, logger); 106 | 107 | config.consumers[0].credentials[0].attributes.secret = 'changed-pass'; 108 | 109 | await execute(config, testAdminApi, logger); 110 | 111 | const kongState = await readKongApi(testAdminApi); 112 | 113 | expect(getLog()).toMatchSnapshot(); 114 | expect(exportToYaml(kongState)).toMatchSnapshot(); 115 | expect(getLocalState()).toEqual(kongState); 116 | }); 117 | 118 | it("should remove the credential", async () => { 119 | const config = { 120 | consumers: [{ 121 | username: "iphone-app", 122 | ensure: "present", 123 | credentials: [{ 124 | name: "key-auth", 125 | ensure: "present", 126 | attributes: { 127 | key: "very-secret-key" 128 | } 129 | }] 130 | }] 131 | }; 132 | 133 | await execute(config, testAdminApi, logger); 134 | 135 | config.consumers[0].credentials[0].ensure = 'removed'; 136 | await execute(config, testAdminApi, logger); 137 | 138 | const kongState = await readKongApi(testAdminApi); 139 | 140 | expect(getLog()).toMatchSnapshot(); 141 | expect(exportToYaml(kongState)).toMatchSnapshot(); 142 | expect(getLocalState()).toEqual(kongState); 143 | }); 144 | }); 145 | 146 | describe('Integration consumers acls', () => { 147 | it("should add the acl", async () => { 148 | const config = { 149 | consumers: [{ 150 | username: "iphone-app", 151 | ensure: "present", 152 | acls: [{ 153 | group: "foobar", 154 | ensure: "present", 155 | }] 156 | }] 157 | }; 158 | 159 | await execute(config, testAdminApi, logger); 160 | const kongState = await readKongApi(testAdminApi); 161 | 162 | expect(getLog()).toMatchSnapshot(); 163 | expect(exportToYaml(kongState)).toMatchSnapshot(); 164 | expect(getLocalState()).toEqual(kongState); 165 | }); 166 | 167 | it("should remove the acl", async () => { 168 | const config = { 169 | consumers: [{ 170 | username: "iphone-app", 171 | ensure: "present", 172 | acls: [{ 173 | group: "foobar", 174 | ensure: "present", 175 | }] 176 | }] 177 | }; 178 | 179 | await execute(config, testAdminApi, logger); 180 | 181 | config.consumers[0].acls[0].ensure = 'removed'; 182 | await execute(config, testAdminApi, logger); 183 | 184 | const kongState = await readKongApi(testAdminApi); 185 | 186 | expect(getLog()).toMatchSnapshot(); 187 | expect(exportToYaml(kongState)).toMatchSnapshot(); 188 | expect(getLocalState()).toEqual(kongState); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /test-integration/api.test.js: -------------------------------------------------------------------------------- 1 | import execute from '../lib/core'; 2 | import { testAdminApi, logger, exportToYaml, getLog, getLocalState, tearDown } from './util'; 3 | import readKongApi from '../lib/readKongApi'; 4 | 5 | describe("API", () => { 6 | beforeEach(tearDown); 7 | 8 | it("should add the API", async () => { 9 | const config = { 10 | apis: [{ 11 | name: "mockbin", 12 | ensure: "present", 13 | attributes: { 14 | upstream_url: "http://mockbin.com", 15 | hosts: ["mockbin.com"] 16 | } 17 | }] 18 | }; 19 | 20 | await execute(config, testAdminApi, logger); 21 | const kongState = await readKongApi(testAdminApi); 22 | 23 | expect(getLog()).toMatchSnapshot(); 24 | expect(exportToYaml(kongState)).toMatchSnapshot(); 25 | expect(getLocalState()).toEqual(kongState); 26 | }); 27 | 28 | it("should not update if already up to date", async () => { 29 | const config = { 30 | apis: [{ 31 | name: "mockbin", 32 | ensure: "present", 33 | attributes: { 34 | upstream_url: "http://mockbin.com", 35 | hosts: ["mockbin.com"] 36 | } 37 | }] 38 | }; 39 | 40 | await execute(config, testAdminApi, logger); 41 | await execute(config, testAdminApi, logger); 42 | const kongState = await readKongApi(testAdminApi); 43 | 44 | expect(getLog()).toMatchSnapshot(); 45 | expect(exportToYaml(kongState)).toMatchSnapshot(); 46 | expect(getLocalState()).toEqual(kongState); 47 | }); 48 | 49 | it("should remove the api", async () => { 50 | const config = { 51 | apis: [{ 52 | name: "mockbin", 53 | ensure: "present", 54 | attributes: { 55 | upstream_url: "http://mockbin.com", 56 | hosts: ["mockbin.com"] 57 | } 58 | }] 59 | }; 60 | 61 | await execute(config, testAdminApi, logger); 62 | 63 | config.apis[0].ensure = 'removed'; 64 | 65 | await execute(config, testAdminApi, logger); 66 | const kongState = await readKongApi(testAdminApi); 67 | 68 | expect(getLog()).toMatchSnapshot(); 69 | expect(exportToYaml(kongState)).toMatchSnapshot(); 70 | expect(getLocalState()).toEqual(kongState); 71 | }); 72 | 73 | it("should update the api", async () => { 74 | const config = { 75 | apis: [{ 76 | name: "mockbin", 77 | ensure: "present", 78 | attributes: { 79 | upstream_url: "http://mockbin.com", 80 | hosts: ["mockbin.com"] 81 | } 82 | }] 83 | }; 84 | 85 | await execute(config, testAdminApi, logger); 86 | 87 | config.apis[0].attributes.preserve_host = true; 88 | 89 | await execute(config, testAdminApi, logger); 90 | const kongState = await readKongApi(testAdminApi); 91 | 92 | expect(getLog()).toMatchSnapshot(); 93 | expect(exportToYaml(kongState)).toMatchSnapshot(); 94 | expect(getLocalState()).toEqual(kongState); 95 | }); 96 | }); 97 | 98 | describe("API plugins", () => { 99 | beforeEach(tearDown); 100 | 101 | it("should add mockbin API with a plugins", async () => { 102 | const config = { 103 | apis: [{ 104 | name: "mockbin", 105 | ensure: "present", 106 | attributes: { 107 | upstream_url: "http://mockbin.com", 108 | hosts: ["mockbin.com"] 109 | }, 110 | plugins: [{ 111 | name: "key-auth", 112 | attributes: { 113 | config: { 114 | key_names: ['foobar'] 115 | } 116 | } 117 | }] 118 | }] 119 | }; 120 | 121 | await execute(config, testAdminApi, logger); 122 | const kongState = await readKongApi(testAdminApi); 123 | 124 | expect(getLog()).toMatchSnapshot(); 125 | expect(exportToYaml(kongState)).toMatchSnapshot(); 126 | expect(getLocalState()).toEqual(kongState); 127 | }); 128 | 129 | it("should remove mockbin api plugin", async () => { 130 | const config = { 131 | apis: [{ 132 | name: "mockbin", 133 | ensure: "present", 134 | attributes: { 135 | upstream_url: "http://mockbin.com", 136 | hosts: ["mockbin.com"] 137 | }, 138 | plugins: [{ 139 | name: "key-auth", 140 | attributes: { 141 | config: { 142 | key_names: ['foobar'] 143 | } 144 | } 145 | }] 146 | }] 147 | }; 148 | 149 | await execute(config, testAdminApi, logger); 150 | 151 | config.apis[0].plugins[0].ensure = 'removed'; 152 | 153 | await execute(config, testAdminApi, logger); 154 | 155 | const kongState = await readKongApi(testAdminApi); 156 | 157 | expect(getLog()).toMatchSnapshot(); 158 | expect(exportToYaml(kongState)).toMatchSnapshot(); 159 | expect(getLocalState()).toEqual(kongState); 160 | }); 161 | 162 | it("should update mockbin api plugin", async () => { 163 | const config = { 164 | apis: [{ 165 | name: "mockbin", 166 | ensure: "present", 167 | attributes: { 168 | upstream_url: "http://mockbin.com", 169 | hosts: ["mockbin.com"] 170 | }, 171 | plugins: [{ 172 | name: "key-auth", 173 | attributes: { 174 | config: { 175 | key_names: ['foobar'] 176 | } 177 | } 178 | }] 179 | }] 180 | }; 181 | 182 | await execute(config, testAdminApi, logger); 183 | 184 | config.apis[0].plugins[0].attributes.enabled = false; 185 | 186 | await execute(config, testAdminApi, logger); 187 | 188 | const kongState = await readKongApi(testAdminApi); 189 | 190 | expect(getLog()).toMatchSnapshot(); 191 | expect(exportToYaml(kongState)).toMatchSnapshot(); 192 | expect(getLocalState()).toEqual(kongState); 193 | }); 194 | 195 | it('should add a customer specific plugin'); 196 | it('should update a customer specific plugin'); 197 | it('should remove a customer specific plugin'); 198 | }); 199 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | ## Loosely following the Kong's 5-minute Quickstart guide 2 | 3 | Read more on [Kong's docs - 5-minute Quickstart](https://getkong.org/docs/latest/getting-started/quickstart) 4 | 5 | ## Add your API using the declarative config 6 | 7 | Create a `config.json` or `config.yml` file and describe your api. 8 | 9 | ```yaml 10 | --- 11 | apis: 12 | - 13 | name: "mockbin" 14 | attributes: 15 | upstream_url: "http://mockbin.com/" 16 | request_host: "mockbin.com" 17 | ``` 18 | 19 | ```json 20 | { 21 | "apis": [ 22 | { 23 | "name": "mockbin", 24 | "attributes": { 25 | "upstream_url": "http://mockbin.com/", 26 | "request_host": "mockbin.com" 27 | } 28 | } 29 | ] 30 | } 31 | ``` 32 | 33 | Apply this configuration using: 34 | 35 | ```bash 36 | kongfig apply --path ./config.json --host localhost:8001 37 | ``` 38 | 39 | You should see a similar command output when applying the config: 40 | 41 | ```bash 42 | ... 43 | 44 | POST 201 http://localhost:8001/apis 45 | { upstream_url: 'http://mockbin.com/', 46 | request_host: 'mockbin.com', 47 | name: 'mockbin' } 48 | Response status Created: 49 | { upstream_url: 'http://mockbin.com/', 50 | id: '94219b08-e70e-44a4-c4cd-bf3c9bc31ca1', 51 | name: 'mockbin', 52 | created_at: 1445247516000, 53 | request_host: 'mockbin.com' } 54 | ``` 55 | 56 | ### Forward your requests through Kong 57 | 58 | ```bash 59 | curl -i -X GET \ 60 | --url http://localhost:8000/ \ 61 | --header 'Host: mockbin.com' 62 | ``` 63 | 64 | 65 | ## Enabling Plugins 66 | 67 | > Prerequisite: Ensure any plugin you want to use has been enabled within Kong's configuration - [Enabling Plugins](https://getkong.org/docs/latest/getting-started/enabling-plugins) 68 | 69 | ### Configure the plugin for your API 70 | 71 | Update the `config.json` file that describes your api. 72 | 73 | ```json 74 | { 75 | "apis": [ 76 | { 77 | "name": "mockbin", 78 | "attributes": { 79 | "upstream_url": "http://mockbin.com/", 80 | "request_host": "mockbin.com" 81 | }, 82 | "plugins": [ 83 | { 84 | "name": "key-auth" 85 | } 86 | ] 87 | } 88 | ] 89 | } 90 | ``` 91 | 92 | Apply this configuration using: 93 | 94 | ```bash 95 | kongfig apply --path ./config.json --host localhost:8001 96 | ``` 97 | 98 | You should see the following within the command output when applying the config: 99 | 100 | ```bash 101 | ... 102 | 103 | POST 201 http://localhost:8001/apis/mockbin/plugins 104 | { name: 'key-auth' } 105 | Response status Created: 106 | { api_id: '94219b08-e70e-44a4-c4cd-bf3c9bc31ca1', 107 | id: '2e34fd09-dce2-4f5d-cf55-99bc68e19843', 108 | created_at: 1445248597000, 109 | enabled: true, 110 | name: 'key-auth', 111 | config: { key_names: [ 'apikey' ], hide_credentials: false } } 112 | ``` 113 | 114 | 115 | ### Verify that the plugin is enabled for your API 116 | 117 | Issue the following cURL request to verify that the *key-auth* plugin was enabled for your API: 118 | 119 | ```bash 120 | curl -i -X GET \ 121 | --url http://localhost:8000/ \ 122 | --header 'Host: mockbin.com' 123 | ``` 124 | 125 | The response should be 403 Forbidden. 126 | 127 | 128 | ## Adding Consumers 129 | 130 | We currently have support for managing a limited set of consumers i.e. internal applications and services. 131 | 132 | If you have a finite set of your own applications that need access to your API's (i.e. a mobile app, micro-services deployed by Puppet), then the following approach will work well for you. 133 | 134 | > Currently limited to the first page of consumers (that is 100) returned by the Kong API 135 | 136 | 137 | ### Create a Consumer 138 | 139 | Declare your consumer in your `config.json` file 140 | 141 | ```json 142 | { 143 | "apis": [ 144 | "..." 145 | ], 146 | 147 | "consumers": [ 148 | { 149 | "username": "iphone-app" 150 | } 151 | ] 152 | } 153 | ``` 154 | 155 | ```bash 156 | kongfig apply --path ./config.json --host localhost:8001 157 | ``` 158 | 159 | You should see the following within the command output when applying the config: 160 | 161 | ```bash 162 | POST 201 http://localhost:8001/consumers 163 | { username: 'iphone-app' } 164 | Response status Created: 165 | { username: 'iphone-app', 166 | created_at: 1445251342000, 167 | id: '3f18c498-10fe-4937-c910-3b6cb7cc7b49' } 168 | ``` 169 | 170 | ### Adding credentials 171 | 172 | Declare your consumer credentials in your `config.json` file 173 | 174 | ```json 175 | { 176 | "consumers": [ 177 | { 178 | "username": "iphone-app", 179 | "credentials": [ 180 | { 181 | "name": "key-auth", 182 | "attributes": { 183 | "key": "very-secret-key" 184 | } 185 | } 186 | ] 187 | } 188 | ] 189 | } 190 | ``` 191 | 192 | You should see the following within the command output when applying the config: 193 | 194 | ```bash 195 | ... 196 | 197 | POST 201 http://localhost:8001/consumers/iphone-app/key-auth 198 | { key: 'very-secret-key' } 199 | Response status Created: 200 | { created_at: 1445251445000, 201 | consumer_id: '3f18c498-10fe-4937-c910-3b6cb7cc7b49', 202 | key: 'very-secret-key', 203 | id: 'feb787a9-fc44-47b3-c743-7575ef6a65a5' } 204 | ``` 205 | 206 | ### Verify that your Consumer credentials are valid 207 | 208 | ```bash 209 | curl -i -X GET \ 210 | --url http://localhost:8000 \ 211 | --header "Host: mockbin.com" \ 212 | --header "apikey: very-secret-key" 213 | ``` 214 | 215 | ## Specifying the Kong Host 216 | 217 | You can alternatively specify the desired host in the configuration file itself, like so: 218 | 219 | ```json 220 | { 221 | "host": "localhost:8001", 222 | "apis": [ 223 | "..." 224 | ] 225 | } 226 | ``` 227 | 228 | ## Removing Declarations 229 | 230 | You are able to ensure that previous declarations have been removed, like so: 231 | 232 | ```json 233 | { 234 | "apis": [ 235 | { 236 | "name": "mockbin", 237 | "ensure": "removed" 238 | } 239 | ] 240 | } 241 | ``` 242 | 243 | ## Using Custom Headers 244 | 245 | You can specify any number of custom headers to be included with every request made by Kongfig. 246 | 247 | These can be used via the command line, like so: 248 | 249 | ```bash 250 | kongfig apply --host localhost:8001 --path config.yaml --header apikey:secret --header name:value 251 | ``` 252 | 253 | or via the config file, like so: 254 | 255 | ```yaml 256 | --- 257 | headers: 258 | - 'apikey:secret' 259 | - 'name:value' 260 | 261 | apis: 262 | - 263 | name: "mockbin" 264 | attributes: 265 | upstream_url: "http://mockbin.com/" 266 | request_host: "mockbin.com" 267 | ``` 268 | -------------------------------------------------------------------------------- /test/upstreams.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import {upstreams, targets} from '../src/core.js'; 3 | import { noop } from '../src/actions.js'; 4 | import { 5 | createUpstream, 6 | removeUpstream, 7 | updateUpstream, 8 | addUpstreamTarget, 9 | removeUpstreamTarget, 10 | updateUpstreamTarget 11 | } from '../src/actions/upstreams.js'; 12 | 13 | describe("upstreams", () => { 14 | it("should add new upstream", () => { 15 | const actual = upstreams([{ 16 | "ensure": "present", 17 | "name": "leadsUpstream", 18 | "attributes": { 19 | "slots": 10 20 | } 21 | }]) 22 | .map(x => x({hasUpstream: () => false})); 23 | 24 | expect(actual).to.be.eql([ 25 | createUpstream('leadsUpstream', {slots: 10}) 26 | ]); 27 | }); 28 | 29 | it("should remove upstream", () => { 30 | const actual = upstreams([{ 31 | "name": "leadsUpstream", 32 | "ensure": "removed", 33 | "attributes": { 34 | "slots": 10 35 | } 36 | }]) 37 | .map(x => x({ 38 | hasUpstream: () => true 39 | })); 40 | 41 | expect(actual).to.be.eql([ 42 | removeUpstream('leadsUpstream') 43 | ]); 44 | }); 45 | 46 | it("should do no-op if upstream is already removed", () => { 47 | const upstream = { 48 | "name": "leadsUpstream", 49 | "ensure": "removed", 50 | "attributes": { 51 | "slots": 10 52 | } 53 | }; 54 | const actual = upstreams([upstream]) 55 | .map(x => x({hasUpstream: () => false})); 56 | 57 | expect(actual).to.be.eql([ 58 | noop({ type: 'noop-upstream', upstream}) 59 | ]); 60 | }); 61 | 62 | it("should update the upstream", () => { 63 | const actual = upstreams([{ 64 | "name": "leadsUpstream", 65 | "attributes": { 66 | "slots": 10 67 | } 68 | }]) 69 | .map(x => x({hasUpstream: () => true, 70 | isUpstreamUpToDate: () => false 71 | })); 72 | 73 | expect(actual).to.be.an('array'); 74 | expect(actual).to.have.length(1); 75 | 76 | expectActualToBeAnUpdateAction(actual[0], updateUpstream('leadsUpstream', {slots: 10})); 77 | }); 78 | 79 | it("should validate ensure enum", () => { 80 | expect(() => upstreams([{ 81 | "ensure": "not-valid", 82 | "name": "leadsUpstream" 83 | }])).to.throwException(/Invalid ensure/); 84 | }); 85 | 86 | it('should add upstream with targets', () => { 87 | const actual = upstreams([{ 88 | "ensure": "present", 89 | "name": "leadsUpstream", 90 | "attributes": { 91 | "slots": 10 92 | }, 93 | "targets": [{ 94 | "target": "server1.leads:8080", 95 | "ensure": "present", 96 | "attributes": { 97 | "weight": 50 98 | } 99 | }] 100 | }]).map(x => x({ 101 | hasUpstream: () => false, 102 | hasUpstreamTarget: () => false, 103 | getUpstreamId: () => 'abcd-1234' 104 | })); 105 | 106 | expect(actual).to.be.eql([ 107 | createUpstream('leadsUpstream', {slots: 10}), 108 | addUpstreamTarget('abcd-1234', 'server1.leads:8080', {weight: 50}) 109 | ]); 110 | }); 111 | 112 | it('should update the upstream target', () => { 113 | const upstream = { 114 | "name": "leadsUpstream", 115 | "attributes": { 116 | "slots": 10 117 | }, 118 | "targets": [{ 119 | "target": "server1.leads:8080", 120 | "attributes": { 121 | "weight": 50 122 | } 123 | }] 124 | }; 125 | 126 | const actual = upstreams([upstream]).map(x => x({ 127 | hasUpstream: () => true, 128 | isUpstreamUpToDate: () => true, 129 | hasUpstreamTarget: () => true, 130 | isUpstreamTargetUpToDate: () => false, 131 | getUpstreamId: () => 'abcd-1234' 132 | })); 133 | 134 | expect(actual).to.be.eql([ 135 | noop({ type: 'noop-upstream', upstream: upstream }), 136 | updateUpstreamTarget('abcd-1234', 'server1.leads:8080', {weight: 50}) 137 | ]); 138 | }); 139 | 140 | it('should remove target from upstream', () => { 141 | const upstream = { 142 | "name": "leadsUpstream", 143 | "attributes": { 144 | "slots": 10 145 | }, 146 | "targets": [{ 147 | "target": "server1.leads:8080", 148 | "ensure": "removed", 149 | "attributes": { 150 | "weight": 50 151 | } 152 | }] 153 | }; 154 | 155 | const actual = upstreams([upstream]).map(x => x({ 156 | hasUpstream: () => true, 157 | isUpstreamUpToDate: () => true, 158 | hasUpstreamTarget: () => true, 159 | getUpstreamId: () => 'abcd-1234' 160 | })); 161 | 162 | expect(actual).to.be.eql([ 163 | noop({ type: 'noop-upstream', upstream: upstream }), 164 | removeUpstreamTarget('abcd-1234', 'server1.leads:8080') 165 | ]); 166 | }); 167 | 168 | it('should do no-op if target was already removed', () => { 169 | const upstream = { 170 | "name": "leadsUpstream", 171 | "attributes": { 172 | "slots": 10 173 | }, 174 | "targets": [{ 175 | "target": "server1.leads:8080", 176 | "ensure": "removed", 177 | "attributes": { 178 | "weight": 50 179 | } 180 | }] 181 | }; 182 | 183 | const actual = upstreams([upstream]).map(x => x({ 184 | hasUpstream: () => true, 185 | isUpstreamUpToDate: () => true, 186 | hasUpstreamTarget: () => false 187 | })); 188 | 189 | expect(actual).to.be.eql([ 190 | noop({ type: 'noop-upstream', upstream: upstream }), 191 | noop({ type: 'noop-target', target: upstream.targets[0] }), 192 | ]); 193 | }); 194 | }); 195 | 196 | function expectActualToBeAnUpdateAction(actual, expected) { 197 | // make copies, don't mutate originals 198 | actual = Object.assign({}, actual); 199 | expected = Object.assign({}, expected); 200 | 201 | let orderlist = actual.body.orderlist; 202 | 203 | expect(orderlist).to.have.length(expected.body.slots); 204 | 205 | for(let i = 1; i <= expected.body.slots; i++) { 206 | expect(orderlist).to.contain(i); 207 | orderlist.splice(orderlist.indexOf(i), 1); 208 | } 209 | 210 | expect(orderlist).to.be.empty(); 211 | 212 | delete actual.body.orderlist; 213 | delete expected.body.orderlist; 214 | 215 | expect(actual).to.be.eql(expected); 216 | } 217 | -------------------------------------------------------------------------------- /test-integration/plugin-per-consumer.test.js: -------------------------------------------------------------------------------- 1 | import execute from '../lib/core'; 2 | import { testAdminApi, logger, exportToYaml, getLog, getLocalState, tearDown } from './util'; 3 | import readKongApi from '../lib/readKongApi'; 4 | 5 | beforeEach(tearDown); 6 | 7 | const ignoreConfigOrder = state => ({ 8 | ...state, 9 | consumers: state.consumers.sort((a, b) => a.username > b.username ? 1 : -1), 10 | plugins: state.plugins.sort((a, b) => a.attributes.config.minute - b.attributes.config.minute), 11 | }); 12 | 13 | describe("per user api plugins by username", () => { 14 | it("should add an api rate limiting plugin for a user", async () => { 15 | const config = { 16 | consumers: [{ 17 | username: "user-limited", 18 | ensure: "present", 19 | }], 20 | 21 | apis: [{ 22 | name: "mockbin", 23 | ensure: "present", 24 | attributes: { 25 | upstream_url: "http://mockbin.com", 26 | hosts: ["mockbin.com"] 27 | }, 28 | plugins: [{ 29 | name: "rate-limiting", 30 | attributes: { 31 | username: "user-limited", 32 | config: { 33 | minute: 1, 34 | } 35 | } 36 | }] 37 | }] 38 | }; 39 | 40 | await execute(config, testAdminApi, logger); 41 | 42 | const kongState = ignoreConfigOrder(await readKongApi(testAdminApi)); 43 | 44 | expect(getLog()).toMatchSnapshot(); 45 | expect(exportToYaml(kongState)).toMatchSnapshot(); 46 | expect(ignoreConfigOrder(getLocalState())).toEqual(kongState); 47 | }); 48 | }); 49 | 50 | describe("per user global plugins by username", () => { 51 | it("should add global rate limiting plugin for a user", async () => { 52 | const config = { 53 | consumers: [{ 54 | username: "user-limited", 55 | ensure: "present", 56 | }], 57 | plugins: [{ 58 | name: "rate-limiting", 59 | attributes: { 60 | username: "user-limited", 61 | config: { 62 | minute: 1, 63 | } 64 | } 65 | }] 66 | }; 67 | 68 | await execute(config, testAdminApi, logger); 69 | 70 | const kongState = ignoreConfigOrder(await readKongApi(testAdminApi)); 71 | 72 | expect(kongState.consumers[0].username).toEqual('user-limited'); 73 | expect(kongState.plugins[0]._info.consumer_id).toEqual(kongState.consumers[0]._info.id); 74 | expect(kongState.plugins[0].attributes.username).toEqual(kongState.consumers[0].username); 75 | 76 | expect(getLog()).toMatchSnapshot(); 77 | expect(exportToYaml(kongState)).toMatchSnapshot(); 78 | expect(ignoreConfigOrder(getLocalState())).toEqual(kongState); 79 | }); 80 | 81 | it("should add global rate limiting plugin for multiple users", async () => { 82 | const config = { 83 | consumers: [{ 84 | username: "user-limited", 85 | ensure: "present", 86 | }, { 87 | username: "user-limited-another", 88 | ensure: "present", 89 | }], 90 | plugins: [{ 91 | name: "rate-limiting", 92 | attributes: { 93 | username: "user-limited", 94 | config: { 95 | minute: 1, 96 | } 97 | } 98 | }, { 99 | name: "rate-limiting", 100 | attributes: { 101 | username: "user-limited-another", 102 | config: { 103 | minute: 10, 104 | } 105 | } 106 | }] 107 | }; 108 | 109 | await execute(config, testAdminApi, logger); 110 | 111 | const kongState = ignoreConfigOrder(await readKongApi(testAdminApi)); 112 | 113 | expect(kongState.consumers[0].username).toEqual('user-limited'); 114 | expect(kongState.plugins[0]._info.consumer_id).toEqual(kongState.consumers[0]._info.id); 115 | expect(kongState.plugins[0].attributes.username).toEqual(kongState.consumers[0].username); 116 | 117 | expect(getLog()).toMatchSnapshot(); 118 | expect(exportToYaml(kongState)).toMatchSnapshot(); 119 | expect(ignoreConfigOrder(getLocalState())).toEqual(kongState); 120 | }); 121 | 122 | it("should remove global rate limiting plugin for the user", async () => { 123 | const config = { 124 | consumers: [{ 125 | username: "user-limited", 126 | ensure: "present", 127 | }, { 128 | username: "user-limited-another", 129 | ensure: "present", 130 | }], 131 | plugins: [{ 132 | name: "rate-limiting", 133 | attributes: { 134 | username: "user-limited", 135 | config: { 136 | minute: 1, 137 | } 138 | } 139 | }, { 140 | name: "rate-limiting", 141 | attributes: { 142 | username: "user-limited-another", 143 | config: { 144 | minute: 10, 145 | } 146 | } 147 | }] 148 | }; 149 | 150 | await execute(config, testAdminApi, logger); 151 | 152 | config.consumers[0].ensure = 'removed'; 153 | config.plugins[0].ensure = 'removed'; 154 | 155 | await execute(config, testAdminApi, logger); 156 | 157 | const kongState = ignoreConfigOrder(await readKongApi(testAdminApi)); 158 | 159 | expect(kongState.consumers[0].username).toEqual('user-limited-another'); 160 | expect(kongState.plugins[0].attributes.username).toEqual('user-limited-another'); 161 | expect(kongState.plugins[0]._info.consumer_id).toEqual(kongState.consumers[0]._info.id); 162 | 163 | expect(getLog()).toMatchSnapshot(); 164 | expect(exportToYaml(kongState)).toMatchSnapshot(); 165 | expect(ignoreConfigOrder(getLocalState())).toEqual(kongState); 166 | }); 167 | 168 | it("should update global rate limiting plugin for the user", async () => { 169 | const config = { 170 | consumers: [{ 171 | username: "user-limited", 172 | ensure: "present", 173 | }, { 174 | username: "user-limited-another", 175 | ensure: "present", 176 | }], 177 | plugins: [{ 178 | name: "rate-limiting", 179 | attributes: { 180 | username: "user-limited", 181 | config: { 182 | minute: 1, 183 | } 184 | } 185 | }, { 186 | name: "rate-limiting", 187 | attributes: { 188 | username: "user-limited-another", 189 | config: { 190 | minute: 10, 191 | } 192 | } 193 | }] 194 | }; 195 | 196 | await execute(config, testAdminApi, logger); 197 | 198 | config.plugins[1].attributes.config.minute = 20; 199 | 200 | await execute(config, testAdminApi, logger); 201 | 202 | const kongState = ignoreConfigOrder(await readKongApi(testAdminApi)); 203 | 204 | expect(getLog()).toMatchSnapshot(); 205 | expect(exportToYaml(kongState)).toMatchSnapshot(); 206 | expect(ignoreConfigOrder(getLocalState())).toEqual(kongState); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Kongfig 6 | 7 | A tool for [Kong](https://getkong.org/) to allow declarative configuration. 8 | 9 | Simply define your list of APIs and consumers in json and then run kongfig to ensure that your Kong is configured correctly. 10 | 11 | [![Build Status](https://travis-ci.org/mybuilder/kongfig.svg?branch=master)](https://travis-ci.org/mybuilder/kongfig) 12 | 13 | ## Install 14 | 15 | ### Manually 16 | We recommend installing Kongfig globally 17 | 18 | ``` 19 | npm install -g kongfig 20 | ``` 21 | 22 | ### Puppet 23 | Use our [Puppet-Kongfig](https://forge.puppetlabs.com/mybuilder/kongfig) module to install and configure Kongfig 24 | 25 | ``` 26 | puppet module install mybuilder-kongfig 27 | ``` 28 | 29 | 30 | ## Quick start 31 | 32 | You can follow the [quick start](docs/guide.md) to get going in 5 minutes. 33 | 34 | 35 | ## Apply config 36 | 37 | You can have your config in [json](config.json.sample), [yaml](config.yml.sample), or [js](config.js.sample) if you need to support multiple environments. 38 | 39 | ``` 40 | kongfig apply --path config.yml --host localhost:8001 41 | ``` 42 | 43 | ## Dump config 44 | 45 | You can dump the existing configuration to a file or view it on a screen 46 | 47 | ``` 48 | kongfig dump --host localhost:8001 > config.yml 49 | ``` 50 | > You can omit the `--host` option if kong is on `localhost:8001` as this is the default value 51 | 52 | You can specify the desired format by giving `--format` option with possible options of `json`, `yaml`, or `screen` that prints the config with colours. 53 | 54 | ```bash 55 | kongfig dump --format screen 56 | ``` 57 | 58 | For APIs which uses custom consumer credential plugins, specify plugin and id name in : format with `--credential-schema` option. 59 | 60 | ``` 61 | kongfig apply --path config.yml --host localhost:8001 --credential-schema custom_jwt:key 62 | ``` 63 | 64 | For multiple plugins use --credential-schema as many as necessary 65 | 66 | ``` 67 | kongfig apply --path config.yml --host localhost:8001 --credential-schema "custom_jwt:key" --credential-schema "custom_oauth2:client_id" 68 | ``` 69 | 70 | ## Schema 71 | 72 | Note: If you change the name of an API/Plugin/Consumer and want to ensure the old one is removed automatically, do not delete or modify the old API/Plugin/Consumer section, other than to add the `ensure: "removed"` flag. Examples shown below. 73 | 74 | > Notice the `attributes.username` config parameter below, this is used to map given username to consumer uuid 75 | 76 | Api schema: 77 | 78 | ```yaml 79 | apis: 80 | - name: mockbin # unique api name 81 | ensure: "present" # Set to "removed" to have Kongfig ensure the API is removed. Default is present. 82 | attributes: 83 | upstream_url: string # (required) 84 | hosts: [string] 85 | uris: [string] 86 | methods: ["POST", "GET"] 87 | strip_uri: bool 88 | preserve_host: bool 89 | retries: int 90 | upstream_connect_timeout: int 91 | upstream_read_timeout: int 92 | upstream_send_timeout: int 93 | https_only: bool # (required) 94 | http_if_terminated: bool 95 | 96 | ``` 97 | 98 | Api plugin schema: 99 | 100 | ```yaml 101 | apis: 102 | - name: mockbin # unique api name 103 | attributes: # ... 104 | plugins: 105 | - name: rate-limiting # kong plugin name 106 | ensure: "present" # Set to "removed" to have Kongfig ensure the plugin is removed. Default is present. 107 | attributes: # the plugin attributes 108 | username: # optional, to reference a consumer, same as consumer_id in kong documentation 109 | config: 110 | 111 | ``` 112 | 113 | Global plugin schema: 114 | 115 | ```yaml 116 | plugins: 117 | - name: cors 118 | attributes: 119 | username: # optional, to reference a consumer, same as consumer_id in kong documentation 120 | enabled: true 121 | config: 122 | credentials: false 123 | preflight_continue: false 124 | max_age: 7000 125 | ``` 126 | 127 | All of the kong plugins should be supported if you find one that doesn't work please [add an issue](https://github.com/mybuilder/kongfig/issues/new). 128 | 129 | Consumer schema: 130 | 131 | ```yaml 132 | consumers: 133 | - username: iphone-app 134 | custom_id: foobar-1234 # optional 135 | ``` 136 | 137 | Consumer credential schema: 138 | 139 | ```yaml 140 | consumers: 141 | - username: iphone-app 142 | credentials: 143 | - name: key-auth 144 | attributes: # credential config attributes 145 | ``` 146 | 147 | Consumer ACL schema: 148 | 149 | ```yaml 150 | consumers: 151 | - username: iphone-app 152 | acls: 153 | - group: acl-group-name 154 | ``` 155 | 156 | ### Supported consumer credentials 157 | 158 | > Notice the `anonymous_username` config parameter below, this is used to map username to consumer uuid 159 | 160 | [Key Authentication](https://getkong.org/plugins/key-authentication/) 161 | 162 | ```yaml 163 | apis: 164 | - name: mockbin # unique api name 165 | attributes: # ... 166 | plugins: 167 | - name: key-auth 168 | attributes: 169 | config: 170 | anonymous_username: # optional, same as just anonymous in kong api, maps given username to consumer uuid 171 | key_names: 172 | hide_credentials: 173 | 174 | consumers: 175 | - username: iphone-app 176 | credentials: 177 | - name: key-auth 178 | attributes: 179 | key: # required 180 | ``` 181 | 182 | [Basic Authentication](https://getkong.org/plugins/basic-authentication/) 183 | 184 | ```yaml 185 | apis: 186 | - name: mockbin 187 | attributes: # ... 188 | plugins: 189 | - name: basic-auth 190 | attributes: 191 | config: 192 | hide_credentials: 193 | 194 | consumers: 195 | - username: iphone-app 196 | credentials: 197 | - name: basic-auth 198 | attributes: 199 | username: # required 200 | password: 201 | ``` 202 | 203 | [OAuth 2.0 Authentication](https://getkong.org/plugins/oauth2-authentication/) 204 | 205 | ```yaml 206 | apis: 207 | - name: mockbin 208 | attributes: # ... 209 | plugins: 210 | - name: oauth2 211 | attributes: 212 | config: 213 | scopes: 214 | mandatory_scope: 215 | token_expiration: 216 | enable_authorization_code: 217 | enable_client_credentials: 218 | enable_implicit_grant: 219 | enable_password_grant: 220 | hide_credentials: 221 | 222 | consumers: 223 | - username: iphone-app 224 | credentials: 225 | - name: oauth2 226 | attributes: 227 | name: 228 | client_id: # required 229 | client_secret: 230 | redirect_uri: string | [string] # required by kong 231 | ``` 232 | 233 | [HMAC Authentication](https://getkong.org/plugins/hmac-authentication/) 234 | 235 | ```yaml 236 | apis: 237 | - name: mockbin 238 | attributes: # ... 239 | plugins: 240 | - name: hmac-auth 241 | attributes: 242 | config: 243 | hide_credentials: 244 | clock_skew: 245 | 246 | consumers: 247 | - username: iphone-app 248 | credentials: 249 | - name: hmac-auth 250 | attributes: 251 | username: # required 252 | secret: 253 | ``` 254 | 255 | [JWT](https://getkong.org/plugins/jwt/) 256 | 257 | ```yaml 258 | apis: 259 | - name: mockbin 260 | attributes: # ... 261 | plugins: 262 | - name: jwt 263 | attributes: 264 | config: 265 | uri_param_names: 266 | claims_to_verify: 267 | 268 | consumers: 269 | - username: iphone-app 270 | credentials: 271 | - name: jwt 272 | attributes: 273 | key: # required 274 | secret: 275 | ``` 276 | 277 | ### Custom Credential Schemas 278 | 279 | It is possible to work with custom consumer credential plugins. 280 | 281 | ```yaml 282 | apis: 283 | - name: mockbin 284 | attributes: # ... 285 | plugins: 286 | - name: custom_jwt 287 | attributes: 288 | config: 289 | uri_param_names: 290 | claims_to_verify: 291 | 292 | consumers: 293 | - username: iphone-app 294 | credentials: 295 | - name: custom_jwt 296 | attributes: 297 | key: # required 298 | secret: 299 | 300 | credentialSchema: 301 | custom_jwt: 302 | id: "key" # credential id name 303 | ``` 304 | 305 | 306 | ### ACL Support 307 | 308 | [Kong ACL documentation](https://getkong.org/plugins/acl/) 309 | 310 | ```yaml 311 | apis: 312 | - name: mockbin 313 | attributes: # ... 314 | plugins: 315 | - name: "acl" 316 | ensure: "present" 317 | attributes: 318 | config.whitelist: "foo-group" 319 | 320 | consumers: 321 | - username: "some-username" 322 | ensure: "present" 323 | acls: 324 | - group: "foo-group" 325 | ensure: "present" 326 | 327 | - group: "bar-group" 328 | ensure: "present" 329 | ``` 330 | 331 | ### Upstream/Target Schema 332 | 333 | [Kong Upstream Load Balancing Reference](https://getkong.org/docs/latest/loadbalancing/) 334 | 335 | ```yaml 336 | upstreams: 337 | - name: "mockbinUpstream" 338 | ensure: "present" 339 | targets: 340 | - target: "server1.mockbin:3001" 341 | attributes: 342 | weight: 50 343 | - target: "server2.mockbin:3001" 344 | attributes: 345 | weight: 50 346 | attributes: 347 | slots: 100 348 | ``` 349 | 350 | 351 | ## Migrating from Kong <=0.9 to >=0.10 352 | 353 | kongfig translates pre `>=0.10` kong config files automatically when applying them. 354 | 355 | So you can export your config from `<=0.9` kong instance by running: 356 | 357 | ```bash 358 | kongfig dump --host kong_9:8001 > config.v9.yml 359 | ``` 360 | 361 | Then apply it to kong `0.10` instance 362 | 363 | ```bash 364 | kongfig apply --path config.v9.yml --host kong_10:8001 365 | ``` 366 | 367 | `apis` endpoint changed between `<=0.9` and `>=0.10`: 368 | * `request_host: string` to `hosts: [string]` 369 | * `request_path: string` to `uris: [string]` 370 | * `strip_request_path: bool` -> `strip_uri: bool` 371 | * Adds `methods`, `retries`, `upstream_connect_timeout`, `upstream_read_timeout`, `upstream_send_timeout`, `https_only`, `http_if_terminated` 372 | 373 | --- 374 | Created by [MyBuilder](http://www.mybuilder.com/) - Check out our [blog](http://tech.mybuilder.com/) for more information and our other open-source projects. 375 | 376 | ## Contributing to Kongfig 377 | 378 | We are very grateful for any contributions you can make to the project. 379 | 380 | Visit the [Contributing](CONTRIBUTING.md) documentation for submission guidelines. 381 | -------------------------------------------------------------------------------- /test-integration/__snapshots__/plugin.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Integration global plugin should add the plugin 1`] = ` 4 | Array [ 5 | Object { 6 | "type": "kong-info", 7 | "version": "___version___", 8 | }, 9 | Object { 10 | "params": Object { 11 | "body": Object { 12 | "config": Object { 13 | "credentials": false, 14 | "max_age": 7000, 15 | "preflight_continue": false, 16 | }, 17 | "name": "cors", 18 | }, 19 | "endpoint": Object { 20 | "name": "plugins", 21 | "params": Object { 22 | "pluginName": "cors", 23 | }, 24 | }, 25 | "method": "POST", 26 | "type": "add-global-plugin", 27 | }, 28 | "type": "request", 29 | "uri": "http://localhost:8001/plugins", 30 | }, 31 | Object { 32 | "content": Object { 33 | "config": Object { 34 | "credentials": false, 35 | "max_age": 7000, 36 | "preflight_continue": false, 37 | }, 38 | "created_at": "___created_at___", 39 | "enabled": true, 40 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 41 | "name": "cors", 42 | }, 43 | "ok": true, 44 | "params": Object { 45 | "body": Object { 46 | "config": Object { 47 | "credentials": false, 48 | "max_age": 7000, 49 | "preflight_continue": false, 50 | }, 51 | "name": "cors", 52 | }, 53 | "endpoint": Object { 54 | "name": "plugins", 55 | "params": Object { 56 | "pluginName": "cors", 57 | }, 58 | }, 59 | "method": "POST", 60 | "type": "add-global-plugin", 61 | }, 62 | "status": 201, 63 | "statusText": "Created", 64 | "type": "response", 65 | "uri": "http://localhost:8001/plugins", 66 | }, 67 | ] 68 | `; 69 | 70 | exports[`Integration global plugin should add the plugin 2`] = ` 71 | "apis: [] 72 | consumers: [] 73 | plugins: 74 | - name: cors 75 | attributes: 76 | enabled: true 77 | config: 78 | credentials: false 79 | max_age: 7000 80 | preflight_continue: false 81 | upstreams: [] 82 | " 83 | `; 84 | 85 | exports[`Integration global plugin should not update if already up to date 1`] = ` 86 | Array [ 87 | Object { 88 | "type": "kong-info", 89 | "version": "___version___", 90 | }, 91 | Object { 92 | "params": Object { 93 | "body": Object { 94 | "config": Object { 95 | "credentials": false, 96 | "max_age": 7000, 97 | "preflight_continue": false, 98 | }, 99 | "name": "cors", 100 | }, 101 | "endpoint": Object { 102 | "name": "plugins", 103 | "params": Object { 104 | "pluginName": "cors", 105 | }, 106 | }, 107 | "method": "POST", 108 | "type": "add-global-plugin", 109 | }, 110 | "type": "request", 111 | "uri": "http://localhost:8001/plugins", 112 | }, 113 | Object { 114 | "content": Object { 115 | "config": Object { 116 | "credentials": false, 117 | "max_age": 7000, 118 | "preflight_continue": false, 119 | }, 120 | "created_at": "___created_at___", 121 | "enabled": true, 122 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 123 | "name": "cors", 124 | }, 125 | "ok": true, 126 | "params": Object { 127 | "body": Object { 128 | "config": Object { 129 | "credentials": false, 130 | "max_age": 7000, 131 | "preflight_continue": false, 132 | }, 133 | "name": "cors", 134 | }, 135 | "endpoint": Object { 136 | "name": "plugins", 137 | "params": Object { 138 | "pluginName": "cors", 139 | }, 140 | }, 141 | "method": "POST", 142 | "type": "add-global-plugin", 143 | }, 144 | "status": 201, 145 | "statusText": "Created", 146 | "type": "response", 147 | "uri": "http://localhost:8001/plugins", 148 | }, 149 | Object { 150 | "type": "kong-info", 151 | "version": "___version___", 152 | }, 153 | Object { 154 | "params": Object { 155 | "noop": true, 156 | "plugin": Object { 157 | "attributes": Object { 158 | "config": Object { 159 | "credentials": false, 160 | "max_age": 7000, 161 | "preflight_continue": false, 162 | }, 163 | }, 164 | "name": "cors", 165 | }, 166 | "type": "noop-global-plugin", 167 | }, 168 | "type": "noop", 169 | }, 170 | ] 171 | `; 172 | 173 | exports[`Integration global plugin should not update if already up to date 2`] = ` 174 | "apis: [] 175 | consumers: [] 176 | plugins: 177 | - name: cors 178 | attributes: 179 | enabled: true 180 | config: 181 | credentials: false 182 | max_age: 7000 183 | preflight_continue: false 184 | upstreams: [] 185 | " 186 | `; 187 | 188 | exports[`Integration global plugin should remove the global plugin 1`] = ` 189 | Array [ 190 | Object { 191 | "type": "kong-info", 192 | "version": "___version___", 193 | }, 194 | Object { 195 | "params": Object { 196 | "body": Object { 197 | "config": Object { 198 | "credentials": false, 199 | "max_age": 7000, 200 | "preflight_continue": false, 201 | }, 202 | "name": "cors", 203 | }, 204 | "endpoint": Object { 205 | "name": "plugins", 206 | "params": Object { 207 | "pluginName": "cors", 208 | }, 209 | }, 210 | "method": "POST", 211 | "type": "add-global-plugin", 212 | }, 213 | "type": "request", 214 | "uri": "http://localhost:8001/plugins", 215 | }, 216 | Object { 217 | "content": Object { 218 | "config": Object { 219 | "credentials": false, 220 | "max_age": 7000, 221 | "preflight_continue": false, 222 | }, 223 | "created_at": "___created_at___", 224 | "enabled": true, 225 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 226 | "name": "cors", 227 | }, 228 | "ok": true, 229 | "params": Object { 230 | "body": Object { 231 | "config": Object { 232 | "credentials": false, 233 | "max_age": 7000, 234 | "preflight_continue": false, 235 | }, 236 | "name": "cors", 237 | }, 238 | "endpoint": Object { 239 | "name": "plugins", 240 | "params": Object { 241 | "pluginName": "cors", 242 | }, 243 | }, 244 | "method": "POST", 245 | "type": "add-global-plugin", 246 | }, 247 | "status": 201, 248 | "statusText": "Created", 249 | "type": "response", 250 | "uri": "http://localhost:8001/plugins", 251 | }, 252 | Object { 253 | "type": "kong-info", 254 | "version": "___version___", 255 | }, 256 | Object { 257 | "params": Object { 258 | "endpoint": Object { 259 | "name": "plugin", 260 | "params": Object { 261 | "pluginId": "2b47ba9b-761a-492d-9a0c-000000000001", 262 | }, 263 | }, 264 | "method": "DELETE", 265 | "type": "remove-global-plugin", 266 | }, 267 | "type": "request", 268 | "uri": "http://localhost:8001/plugins/2b47ba9b-761a-492d-9a0c-000000000001", 269 | }, 270 | Object { 271 | "content": "", 272 | "ok": true, 273 | "params": Object { 274 | "endpoint": Object { 275 | "name": "plugin", 276 | "params": Object { 277 | "pluginId": "2b47ba9b-761a-492d-9a0c-000000000001", 278 | }, 279 | }, 280 | "method": "DELETE", 281 | "type": "remove-global-plugin", 282 | }, 283 | "status": 204, 284 | "statusText": "No Content", 285 | "type": "response", 286 | "uri": "http://localhost:8001/plugins/2b47ba9b-761a-492d-9a0c-000000000001", 287 | }, 288 | ] 289 | `; 290 | 291 | exports[`Integration global plugin should remove the global plugin 2`] = ` 292 | "apis: [] 293 | consumers: [] 294 | plugins: [] 295 | upstreams: [] 296 | " 297 | `; 298 | 299 | exports[`Integration global plugin should update the global plugin 1`] = ` 300 | Array [ 301 | Object { 302 | "type": "kong-info", 303 | "version": "___version___", 304 | }, 305 | Object { 306 | "params": Object { 307 | "body": Object { 308 | "config": Object { 309 | "credentials": false, 310 | "max_age": 7000, 311 | "preflight_continue": false, 312 | }, 313 | "name": "cors", 314 | }, 315 | "endpoint": Object { 316 | "name": "plugins", 317 | "params": Object { 318 | "pluginName": "cors", 319 | }, 320 | }, 321 | "method": "POST", 322 | "type": "add-global-plugin", 323 | }, 324 | "type": "request", 325 | "uri": "http://localhost:8001/plugins", 326 | }, 327 | Object { 328 | "content": Object { 329 | "config": Object { 330 | "credentials": false, 331 | "max_age": 7000, 332 | "preflight_continue": false, 333 | }, 334 | "created_at": "___created_at___", 335 | "enabled": true, 336 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 337 | "name": "cors", 338 | }, 339 | "ok": true, 340 | "params": Object { 341 | "body": Object { 342 | "config": Object { 343 | "credentials": false, 344 | "max_age": 7000, 345 | "preflight_continue": false, 346 | }, 347 | "name": "cors", 348 | }, 349 | "endpoint": Object { 350 | "name": "plugins", 351 | "params": Object { 352 | "pluginName": "cors", 353 | }, 354 | }, 355 | "method": "POST", 356 | "type": "add-global-plugin", 357 | }, 358 | "status": 201, 359 | "statusText": "Created", 360 | "type": "response", 361 | "uri": "http://localhost:8001/plugins", 362 | }, 363 | Object { 364 | "type": "kong-info", 365 | "version": "___version___", 366 | }, 367 | Object { 368 | "params": Object { 369 | "body": Object { 370 | "config": Object { 371 | "credentials": false, 372 | "max_age": 7000, 373 | "preflight_continue": false, 374 | }, 375 | "enabled": false, 376 | }, 377 | "endpoint": Object { 378 | "name": "plugin", 379 | "params": Object { 380 | "pluginId": "2b47ba9b-761a-492d-9a0c-000000000001", 381 | }, 382 | }, 383 | "method": "PATCH", 384 | "type": "update-global-plugin", 385 | }, 386 | "type": "request", 387 | "uri": "http://localhost:8001/plugins/2b47ba9b-761a-492d-9a0c-000000000001", 388 | }, 389 | Object { 390 | "content": Object { 391 | "config": Object { 392 | "credentials": false, 393 | "max_age": 7000, 394 | "preflight_continue": false, 395 | }, 396 | "created_at": "___created_at___", 397 | "enabled": false, 398 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 399 | "name": "cors", 400 | }, 401 | "ok": true, 402 | "params": Object { 403 | "body": Object { 404 | "config": Object { 405 | "credentials": false, 406 | "max_age": 7000, 407 | "preflight_continue": false, 408 | }, 409 | "enabled": false, 410 | }, 411 | "endpoint": Object { 412 | "name": "plugin", 413 | "params": Object { 414 | "pluginId": "2b47ba9b-761a-492d-9a0c-000000000001", 415 | }, 416 | }, 417 | "method": "PATCH", 418 | "type": "update-global-plugin", 419 | }, 420 | "status": 200, 421 | "statusText": "OK", 422 | "type": "response", 423 | "uri": "http://localhost:8001/plugins/2b47ba9b-761a-492d-9a0c-000000000001", 424 | }, 425 | ] 426 | `; 427 | 428 | exports[`Integration global plugin should update the global plugin 2`] = ` 429 | "apis: [] 430 | consumers: [] 431 | plugins: 432 | - name: cors 433 | attributes: 434 | enabled: false 435 | config: 436 | credentials: false 437 | max_age: 7000 438 | preflight_continue: false 439 | upstreams: [] 440 | " 441 | `; 442 | -------------------------------------------------------------------------------- /test/consumers.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import {consumers, credentials, acls} from '../src/core.js'; 3 | import {createConsumer, removeConsumer, addConsumerCredentials, updateConsumerCredentials, removeConsumerCredentials, addConsumerAcls, removeConsumerAcls} from '../src/actions.js'; 4 | import {getSupportedCredentials, addSchema, getSchema, addSchemasFromOptions, addSchemasFromConfig} from '../src/consumerCredentials.js'; 5 | 6 | describe("consumers", () => { 7 | it("should add new consumer", () => { 8 | var actual = consumers([{ 9 | "ensure": "present", 10 | "username": "marketplace" 11 | }]) 12 | .map(x => x({ 13 | hasConsumer: () => false 14 | })); 15 | 16 | expect(actual).to.be.eql([ 17 | createConsumer('marketplace') 18 | ]); 19 | }); 20 | 21 | it("should remove consumer", () => { 22 | var actual = consumers([{ 23 | "ensure": "removed", 24 | "username": "marketplace" 25 | }]).map(x => x({ 26 | hasConsumer: (name) => name == 'marketplace', 27 | getConsumerId: username => 'user-1234', 28 | })); 29 | 30 | expect(actual).to.be.eql([ 31 | removeConsumer('user-1234') 32 | ]); 33 | }); 34 | 35 | it("should validate consumer", () => { 36 | expect(() => consumers([{ 37 | "ensure": "present" 38 | }])).to.throwException(/Consumer username must be specified/); 39 | }); 40 | 41 | describe("credentials", () => { 42 | it("should add oauth2 credential", () => { 43 | var actual = credentials('app-name', [{ 44 | "name": "oauth2", 45 | 'attributes': { 46 | "client_id": 'foo' 47 | } 48 | }] 49 | ).map(x => x({ 50 | getConsumerId: username => 'user-1234', 51 | hasConsumerCredential: () => false, 52 | })); 53 | 54 | expect(actual).to.be.eql([ 55 | addConsumerCredentials('user-1234', 'oauth2', {"client_id": 'foo'}) 56 | ]); 57 | }); 58 | 59 | it("should update the oauth2 credential", () => { 60 | var actual = credentials('app-name', [{ 61 | "name": "oauth2", 62 | 'attributes': { 63 | "client_id": 'foo', 64 | "redirect-uri": 'foo/bar' 65 | } 66 | }] 67 | ).map(x => x({ 68 | getConsumerId: username => 'user-1234', 69 | getConsumerCredentialId: () => '1234', 70 | hasConsumerCredential: () => true, 71 | isConsumerCredentialUpToDate: () => false})); 72 | 73 | expect(actual).to.be.eql([ 74 | updateConsumerCredentials('user-1234', 'oauth2', '1234', {"client_id": 'foo', "redirect-uri": 'foo/bar'}) 75 | ]); 76 | }); 77 | 78 | it("should remove consumer credentials", () => { 79 | var actual = credentials('app-name', [{ 80 | "name": "oauth2", 81 | "ensure": 'removed', 82 | 'attributes': { 83 | "client_id": 'foo' 84 | } 85 | }] 86 | ).map(x => x({ 87 | getConsumerId: username => 'user-1234', 88 | getConsumerCredentialId: () => '1234', 89 | hasConsumerCredential: () => true}), 90 | ); 91 | 92 | expect(actual).to.be.eql([ 93 | removeConsumerCredentials('user-1234', 'oauth2', '1234') 94 | ]); 95 | }); 96 | }); 97 | 98 | describe("jwt credentials", () => { 99 | it("should add jwt credential", () => { 100 | var actual = credentials('app-name', [{ 101 | "name": "jwt", 102 | 'attributes': { 103 | "key": 'somekey', 104 | "secret": 'super-secret' 105 | } 106 | }] 107 | ).map(x => x({ 108 | getConsumerId: username => 'user-1234', 109 | hasConsumerCredential: () => false, 110 | })); 111 | 112 | expect(actual).to.be.eql([ 113 | addConsumerCredentials('user-1234', 'jwt', {"key": 'somekey', "secret": 'super-secret'}) 114 | ]); 115 | }); 116 | 117 | it("should update the jwt credential", () => { 118 | var actual = credentials('app-name', [{ 119 | "name": "jwt", 120 | 'attributes': { 121 | "key": 'somekey', 122 | "secret": 'new-super-secret' 123 | } 124 | }] 125 | ).map(x => x({ 126 | getConsumerId: username => 'user-1234', 127 | getConsumerCredentialId: () => '1234', 128 | hasConsumerCredential: () => true, 129 | isConsumerCredentialUpToDate: () => false})); 130 | 131 | expect(actual).to.be.eql([ 132 | updateConsumerCredentials('user-1234', 'jwt', '1234', {"key": 'somekey', "secret": 'new-super-secret'}) 133 | ]); 134 | }); 135 | 136 | it("should remove consumer", () => { 137 | var actual = credentials('app-name', [{ 138 | "name": "jwt", 139 | "ensure": 'removed', 140 | 'attributes': { 141 | key: 'somekey' 142 | } 143 | }] 144 | ).map(x => x({ 145 | getConsumerId: username => 'user-1234', 146 | getConsumerCredentialId: () => '1234', 147 | hasConsumerCredential: () => true}) 148 | ); 149 | 150 | expect(actual).to.be.eql([ 151 | removeConsumerCredentials('user-1234', 'jwt', '1234') 152 | ]); 153 | }); 154 | }); 155 | 156 | describe('basic-auth', () => { 157 | it("should add basic auth credential", () => { 158 | var actual = credentials('app-name', [{ 159 | "name": "basic-auth", 160 | 'attributes': { 161 | "username": 'user', 162 | "password": 'password' 163 | } 164 | }] 165 | ).map(x => x({ 166 | getConsumerId: username => 'user-1234', 167 | hasConsumerCredential: () => false, 168 | })); 169 | 170 | expect(actual).to.be.eql([ 171 | addConsumerCredentials('user-1234', 'basic-auth', {"username": 'user', "password": 'password'}) 172 | ]); 173 | }); 174 | 175 | it("should update the basic auth credential", () => { 176 | var actual = credentials('app-name', [{ 177 | "name": "basic-auth", 178 | 'attributes': { 179 | "username": 'user', 180 | "password": 'new-password' 181 | } 182 | }] 183 | ).map(x => x({ 184 | getConsumerId: username => 'user-1234', 185 | getConsumerCredentialId: () => '1234', 186 | hasConsumerCredential: () => true, 187 | isConsumerCredentialUpToDate: () => false 188 | })); 189 | 190 | expect(actual).to.be.eql([ 191 | updateConsumerCredentials('user-1234', 'basic-auth', '1234', {"username": 'user', "password": 'new-password'}) 192 | ]); 193 | }); 194 | 195 | it("should remove consumer credential", () => { 196 | var actual = credentials('app-name', [{ 197 | "name": "basic-auth", 198 | "ensure": 'removed', 199 | 'attributes': { 200 | username: 'user' 201 | } 202 | }] 203 | ).map(x => x({ 204 | getConsumerId: username => 'user-1234', 205 | getConsumerCredentialId: () => '1234', 206 | hasConsumerCredential: () => true 207 | })); 208 | 209 | expect(actual).to.be.eql([ 210 | removeConsumerCredentials('user-1234', 'basic-auth', '1234') 211 | ]); 212 | }); 213 | }); 214 | 215 | describe('acl', () => { 216 | it("should add acl", () => { 217 | var actual = acls('app-name', [{ 218 | "name": "acls", 219 | 'group': 'super-group-name' 220 | }] 221 | ).map(x => x({ 222 | getConsumerId: username => 'user-1234', 223 | hasConsumerAcl: () => false, 224 | }) 225 | ); 226 | 227 | expect(actual).to.be.eql([ 228 | addConsumerAcls('user-1234', "super-group-name") 229 | ]); 230 | }); 231 | 232 | it("should remove consumer acl", () => { 233 | var actual = acls('app-name', [{ 234 | "name": "acls", 235 | "ensure": 'removed', 236 | 'group': 'super-group-name', 237 | }]).map(x => x({ 238 | getConsumerId: username => 'user-1234', 239 | getConsumerAclId: () => '1234', 240 | hasConsumerAcl: () => true, 241 | })); 242 | 243 | expect(actual).to.be.eql([ 244 | removeConsumerAcls('user-1234', '1234') 245 | ]); 246 | }); 247 | }); 248 | 249 | describe('consumer credentials', () => { 250 | it("should get credentials", () => { 251 | const credentials = getSupportedCredentials(); 252 | credentials.forEach(name => { 253 | const schema = getSchema(name); 254 | expect(schema).not.to.be.null; 255 | expect(schema).to.have.property('id'); 256 | }) 257 | }); 258 | 259 | it("should add custom credential", () => { 260 | const name = 'custom_jwt'; 261 | const schema = { 262 | "id": "key" 263 | } 264 | 265 | addSchema(name, schema); 266 | expect(getSchema(name)).to.be.eql(schema); 267 | }); 268 | 269 | it("should not add custom credential without id", () => { 270 | const name = 'custom_jwt2'; 271 | const schema = { 272 | "noid": "value" 273 | } 274 | 275 | expect(() => { addSchema(name, schema) }).to.throwException(Error); 276 | }); 277 | 278 | it("should not update credential", () => { 279 | const name = 'jwt'; 280 | const schema = { 281 | "id": "key" 282 | } 283 | 284 | expect(() => { addSchema(name, schema) }).to.throwException(Error); 285 | }); 286 | 287 | it("should add custom credentials from cli options", () => { 288 | const opts = ['custom_jwt3:key', 'custom_oauth2:client_id']; 289 | 290 | expect(() => { addSchemasFromOptions(opts) }).to.not.throwException(Error); 291 | expect(getSchema('custom_jwt3')).to.be.eql({id: 'key'}); 292 | expect(getSchema('custom_oauth2')).to.be.eql({id: 'client_id'}); 293 | }); 294 | 295 | it("should validate custom credentials from cli options", () => { 296 | ['custom_jwt4|nocolon', 'custom_oauth2_2:client_id:extracolon'] 297 | .forEach((opt) => { 298 | expect(() => { addSchemasFromOptions([opt]) }).to.throwException(Error); 299 | }) 300 | }); 301 | 302 | it("should add custom credentials from config", () => { 303 | const conf = { 304 | credentialSchemas: { 305 | custom_jwt5: {id: 'key'}, 306 | custom_oauth2_3: {id: 'client_id'}, 307 | } 308 | } 309 | 310 | expect(() => { addSchemasFromConfig(conf) }).to.not.throwException(Error); 311 | expect(getSchema('custom_jwt5')).to.be.eql({id: 'key'}); 312 | expect(getSchema('custom_oauth2_3')).to.be.eql({id: 'client_id'}); 313 | }); 314 | }); 315 | }); 316 | -------------------------------------------------------------------------------- /test-integration/__snapshots__/upstream.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Upstream Targets should add mockbin upstream with target 1`] = ` 4 | Array [ 5 | Object { 6 | "type": "kong-info", 7 | "version": "___version___", 8 | }, 9 | Object { 10 | "params": Object { 11 | "body": Object { 12 | "name": "mockbinUpstream", 13 | "slots": 10, 14 | }, 15 | "endpoint": Object { 16 | "name": "upstreams", 17 | }, 18 | "method": "POST", 19 | "type": "create-upstream", 20 | }, 21 | "type": "request", 22 | "uri": "http://localhost:8001/upstreams", 23 | }, 24 | Object { 25 | "content": Object { 26 | "created_at": "___created_at___", 27 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 28 | "name": "mockbinUpstream", 29 | "orderlist": "___orderlist___", 30 | "slots": 10, 31 | }, 32 | "ok": true, 33 | "params": Object { 34 | "body": Object { 35 | "name": "mockbinUpstream", 36 | "slots": 10, 37 | }, 38 | "endpoint": Object { 39 | "name": "upstreams", 40 | }, 41 | "method": "POST", 42 | "type": "create-upstream", 43 | }, 44 | "status": 201, 45 | "statusText": "Created", 46 | "type": "response", 47 | "uri": "http://localhost:8001/upstreams", 48 | }, 49 | Object { 50 | "params": Object { 51 | "body": Object { 52 | "target": "server1.mockbin:8080", 53 | "weight": 50, 54 | }, 55 | "endpoint": Object { 56 | "name": "upstream-targets", 57 | "params": Object { 58 | "targetName": "server1.mockbin:8080", 59 | "upstreamId": "2b47ba9b-761a-492d-9a0c-000000000001", 60 | }, 61 | }, 62 | "method": "POST", 63 | "type": "add-upstream-target", 64 | }, 65 | "type": "request", 66 | "uri": "http://localhost:8001/upstreams/2b47ba9b-761a-492d-9a0c-000000000001/targets", 67 | }, 68 | Object { 69 | "content": Object { 70 | "created_at": "___created_at___", 71 | "id": "2b47ba9b-761a-492d-9a0c-000000000002", 72 | "target": "server1.mockbin:8080", 73 | "upstream_id": "2b47ba9b-761a-492d-9a0c-000000000001", 74 | "weight": 50, 75 | }, 76 | "ok": true, 77 | "params": Object { 78 | "body": Object { 79 | "target": "server1.mockbin:8080", 80 | "weight": 50, 81 | }, 82 | "endpoint": Object { 83 | "name": "upstream-targets", 84 | "params": Object { 85 | "targetName": "server1.mockbin:8080", 86 | "upstreamId": "2b47ba9b-761a-492d-9a0c-000000000001", 87 | }, 88 | }, 89 | "method": "POST", 90 | "type": "add-upstream-target", 91 | }, 92 | "status": 201, 93 | "statusText": "Created", 94 | "type": "response", 95 | "uri": "http://localhost:8001/upstreams/2b47ba9b-761a-492d-9a0c-000000000001/targets", 96 | }, 97 | ] 98 | `; 99 | 100 | exports[`Upstream Targets should add mockbin upstream with target 2`] = ` 101 | "apis: [] 102 | consumers: [] 103 | plugins: [] 104 | upstreams: 105 | - name: mockbinUpstream 106 | targets: 107 | - target: 'server1.mockbin:8080' 108 | attributes: 109 | weight: 50 110 | attributes: 111 | slots: 10 112 | " 113 | `; 114 | 115 | exports[`Upstream Targets should remove target from mockbin upstream 1`] = ` 116 | Array [ 117 | Object { 118 | "type": "kong-info", 119 | "version": "___version___", 120 | }, 121 | Object { 122 | "params": Object { 123 | "body": Object { 124 | "name": "mockbinUpstream", 125 | "slots": 10, 126 | }, 127 | "endpoint": Object { 128 | "name": "upstreams", 129 | }, 130 | "method": "POST", 131 | "type": "create-upstream", 132 | }, 133 | "type": "request", 134 | "uri": "http://localhost:8001/upstreams", 135 | }, 136 | Object { 137 | "content": Object { 138 | "created_at": "___created_at___", 139 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 140 | "name": "mockbinUpstream", 141 | "orderlist": "___orderlist___", 142 | "slots": 10, 143 | }, 144 | "ok": true, 145 | "params": Object { 146 | "body": Object { 147 | "name": "mockbinUpstream", 148 | "slots": 10, 149 | }, 150 | "endpoint": Object { 151 | "name": "upstreams", 152 | }, 153 | "method": "POST", 154 | "type": "create-upstream", 155 | }, 156 | "status": 201, 157 | "statusText": "Created", 158 | "type": "response", 159 | "uri": "http://localhost:8001/upstreams", 160 | }, 161 | Object { 162 | "params": Object { 163 | "body": Object { 164 | "target": "server1.mockbin:8080", 165 | "weight": 50, 166 | }, 167 | "endpoint": Object { 168 | "name": "upstream-targets", 169 | "params": Object { 170 | "targetName": "server1.mockbin:8080", 171 | "upstreamId": "2b47ba9b-761a-492d-9a0c-000000000001", 172 | }, 173 | }, 174 | "method": "POST", 175 | "type": "add-upstream-target", 176 | }, 177 | "type": "request", 178 | "uri": "http://localhost:8001/upstreams/2b47ba9b-761a-492d-9a0c-000000000001/targets", 179 | }, 180 | Object { 181 | "content": Object { 182 | "created_at": "___created_at___", 183 | "id": "2b47ba9b-761a-492d-9a0c-000000000002", 184 | "target": "server1.mockbin:8080", 185 | "upstream_id": "2b47ba9b-761a-492d-9a0c-000000000001", 186 | "weight": 50, 187 | }, 188 | "ok": true, 189 | "params": Object { 190 | "body": Object { 191 | "target": "server1.mockbin:8080", 192 | "weight": 50, 193 | }, 194 | "endpoint": Object { 195 | "name": "upstream-targets", 196 | "params": Object { 197 | "targetName": "server1.mockbin:8080", 198 | "upstreamId": "2b47ba9b-761a-492d-9a0c-000000000001", 199 | }, 200 | }, 201 | "method": "POST", 202 | "type": "add-upstream-target", 203 | }, 204 | "status": 201, 205 | "statusText": "Created", 206 | "type": "response", 207 | "uri": "http://localhost:8001/upstreams/2b47ba9b-761a-492d-9a0c-000000000001/targets", 208 | }, 209 | Object { 210 | "type": "kong-info", 211 | "version": "___version___", 212 | }, 213 | Object { 214 | "params": Object { 215 | "noop": true, 216 | "type": "noop-upstream", 217 | "upstream": Object { 218 | "attributes": Object { 219 | "slots": 10, 220 | }, 221 | "ensure": "present", 222 | "name": "mockbinUpstream", 223 | "targets": Array [ 224 | Object { 225 | "attributes": Object { 226 | "weight": 50, 227 | }, 228 | "ensure": "removed", 229 | "target": "server1.mockbin:8080", 230 | }, 231 | ], 232 | }, 233 | }, 234 | "type": "noop", 235 | }, 236 | Object { 237 | "params": Object { 238 | "body": Object { 239 | "target": "server1.mockbin:8080", 240 | "weight": 0, 241 | }, 242 | "endpoint": Object { 243 | "name": "upstream-targets", 244 | "params": Object { 245 | "targetName": "server1.mockbin:8080", 246 | "upstreamId": "2b47ba9b-761a-492d-9a0c-000000000001", 247 | }, 248 | }, 249 | "method": "POST", 250 | "type": "remove-upstream-target", 251 | }, 252 | "type": "request", 253 | "uri": "http://localhost:8001/upstreams/2b47ba9b-761a-492d-9a0c-000000000001/targets", 254 | }, 255 | Object { 256 | "content": Object { 257 | "created_at": "___created_at___", 258 | "id": "2b47ba9b-761a-492d-9a0c-000000000003", 259 | "target": "server1.mockbin:8080", 260 | "upstream_id": "2b47ba9b-761a-492d-9a0c-000000000001", 261 | "weight": 0, 262 | }, 263 | "ok": true, 264 | "params": Object { 265 | "body": Object { 266 | "target": "server1.mockbin:8080", 267 | "weight": 0, 268 | }, 269 | "endpoint": Object { 270 | "name": "upstream-targets", 271 | "params": Object { 272 | "targetName": "server1.mockbin:8080", 273 | "upstreamId": "2b47ba9b-761a-492d-9a0c-000000000001", 274 | }, 275 | }, 276 | "method": "POST", 277 | "type": "remove-upstream-target", 278 | }, 279 | "status": 201, 280 | "statusText": "Created", 281 | "type": "response", 282 | "uri": "http://localhost:8001/upstreams/2b47ba9b-761a-492d-9a0c-000000000001/targets", 283 | }, 284 | ] 285 | `; 286 | 287 | exports[`Upstream Targets should remove target from mockbin upstream 2`] = ` 288 | "apis: [] 289 | consumers: [] 290 | plugins: [] 291 | upstreams: 292 | - name: mockbinUpstream 293 | targets: [] 294 | attributes: 295 | slots: 10 296 | " 297 | `; 298 | 299 | exports[`Upstream Targets should update mockbin upstream target 1`] = ` 300 | Array [ 301 | Object { 302 | "type": "kong-info", 303 | "version": "___version___", 304 | }, 305 | Object { 306 | "params": Object { 307 | "body": Object { 308 | "name": "mockbinUpstream", 309 | "slots": 10, 310 | }, 311 | "endpoint": Object { 312 | "name": "upstreams", 313 | }, 314 | "method": "POST", 315 | "type": "create-upstream", 316 | }, 317 | "type": "request", 318 | "uri": "http://localhost:8001/upstreams", 319 | }, 320 | Object { 321 | "content": Object { 322 | "created_at": "___created_at___", 323 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 324 | "name": "mockbinUpstream", 325 | "orderlist": "___orderlist___", 326 | "slots": 10, 327 | }, 328 | "ok": true, 329 | "params": Object { 330 | "body": Object { 331 | "name": "mockbinUpstream", 332 | "slots": 10, 333 | }, 334 | "endpoint": Object { 335 | "name": "upstreams", 336 | }, 337 | "method": "POST", 338 | "type": "create-upstream", 339 | }, 340 | "status": 201, 341 | "statusText": "Created", 342 | "type": "response", 343 | "uri": "http://localhost:8001/upstreams", 344 | }, 345 | Object { 346 | "params": Object { 347 | "body": Object { 348 | "target": "server1.mockbin:8080", 349 | "weight": 50, 350 | }, 351 | "endpoint": Object { 352 | "name": "upstream-targets", 353 | "params": Object { 354 | "targetName": "server1.mockbin:8080", 355 | "upstreamId": "2b47ba9b-761a-492d-9a0c-000000000001", 356 | }, 357 | }, 358 | "method": "POST", 359 | "type": "add-upstream-target", 360 | }, 361 | "type": "request", 362 | "uri": "http://localhost:8001/upstreams/2b47ba9b-761a-492d-9a0c-000000000001/targets", 363 | }, 364 | Object { 365 | "content": Object { 366 | "created_at": "___created_at___", 367 | "id": "2b47ba9b-761a-492d-9a0c-000000000002", 368 | "target": "server1.mockbin:8080", 369 | "upstream_id": "2b47ba9b-761a-492d-9a0c-000000000001", 370 | "weight": 50, 371 | }, 372 | "ok": true, 373 | "params": Object { 374 | "body": Object { 375 | "target": "server1.mockbin:8080", 376 | "weight": 50, 377 | }, 378 | "endpoint": Object { 379 | "name": "upstream-targets", 380 | "params": Object { 381 | "targetName": "server1.mockbin:8080", 382 | "upstreamId": "2b47ba9b-761a-492d-9a0c-000000000001", 383 | }, 384 | }, 385 | "method": "POST", 386 | "type": "add-upstream-target", 387 | }, 388 | "status": 201, 389 | "statusText": "Created", 390 | "type": "response", 391 | "uri": "http://localhost:8001/upstreams/2b47ba9b-761a-492d-9a0c-000000000001/targets", 392 | }, 393 | Object { 394 | "type": "kong-info", 395 | "version": "___version___", 396 | }, 397 | Object { 398 | "params": Object { 399 | "noop": true, 400 | "type": "noop-upstream", 401 | "upstream": Object { 402 | "attributes": Object { 403 | "slots": 10, 404 | }, 405 | "ensure": "present", 406 | "name": "mockbinUpstream", 407 | "targets": Array [ 408 | Object { 409 | "attributes": Object { 410 | "weight": 100, 411 | }, 412 | "ensure": "present", 413 | "target": "server1.mockbin:8080", 414 | }, 415 | ], 416 | }, 417 | }, 418 | "type": "noop", 419 | }, 420 | Object { 421 | "params": Object { 422 | "body": Object { 423 | "target": "server1.mockbin:8080", 424 | "weight": 100, 425 | }, 426 | "endpoint": Object { 427 | "name": "upstream-targets", 428 | "params": Object { 429 | "targetName": "server1.mockbin:8080", 430 | "upstreamId": "2b47ba9b-761a-492d-9a0c-000000000001", 431 | }, 432 | }, 433 | "method": "POST", 434 | "type": "add-upstream-target", 435 | }, 436 | "type": "request", 437 | "uri": "http://localhost:8001/upstreams/2b47ba9b-761a-492d-9a0c-000000000001/targets", 438 | }, 439 | Object { 440 | "content": Object { 441 | "created_at": "___created_at___", 442 | "id": "2b47ba9b-761a-492d-9a0c-000000000003", 443 | "target": "server1.mockbin:8080", 444 | "upstream_id": "2b47ba9b-761a-492d-9a0c-000000000001", 445 | "weight": 100, 446 | }, 447 | "ok": true, 448 | "params": Object { 449 | "body": Object { 450 | "target": "server1.mockbin:8080", 451 | "weight": 100, 452 | }, 453 | "endpoint": Object { 454 | "name": "upstream-targets", 455 | "params": Object { 456 | "targetName": "server1.mockbin:8080", 457 | "upstreamId": "2b47ba9b-761a-492d-9a0c-000000000001", 458 | }, 459 | }, 460 | "method": "POST", 461 | "type": "add-upstream-target", 462 | }, 463 | "status": 201, 464 | "statusText": "Created", 465 | "type": "response", 466 | "uri": "http://localhost:8001/upstreams/2b47ba9b-761a-492d-9a0c-000000000001/targets", 467 | }, 468 | ] 469 | `; 470 | 471 | exports[`Upstream Targets should update mockbin upstream target 2`] = ` 472 | "apis: [] 473 | consumers: [] 474 | plugins: [] 475 | upstreams: 476 | - name: mockbinUpstream 477 | targets: 478 | - target: 'server1.mockbin:8080' 479 | attributes: 480 | weight: 100 481 | attributes: 482 | slots: 10 483 | " 484 | `; 485 | 486 | exports[`Upstream should add the upstream 1`] = ` 487 | Array [ 488 | Object { 489 | "type": "kong-info", 490 | "version": "___version___", 491 | }, 492 | Object { 493 | "params": Object { 494 | "body": Object { 495 | "name": "mockbinUpstream", 496 | "slots": 10, 497 | }, 498 | "endpoint": Object { 499 | "name": "upstreams", 500 | }, 501 | "method": "POST", 502 | "type": "create-upstream", 503 | }, 504 | "type": "request", 505 | "uri": "http://localhost:8001/upstreams", 506 | }, 507 | Object { 508 | "content": Object { 509 | "created_at": "___created_at___", 510 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 511 | "name": "mockbinUpstream", 512 | "orderlist": "___orderlist___", 513 | "slots": 10, 514 | }, 515 | "ok": true, 516 | "params": Object { 517 | "body": Object { 518 | "name": "mockbinUpstream", 519 | "slots": 10, 520 | }, 521 | "endpoint": Object { 522 | "name": "upstreams", 523 | }, 524 | "method": "POST", 525 | "type": "create-upstream", 526 | }, 527 | "status": 201, 528 | "statusText": "Created", 529 | "type": "response", 530 | "uri": "http://localhost:8001/upstreams", 531 | }, 532 | ] 533 | `; 534 | 535 | exports[`Upstream should add the upstream 2`] = ` 536 | "apis: [] 537 | consumers: [] 538 | plugins: [] 539 | upstreams: 540 | - name: mockbinUpstream 541 | targets: [] 542 | attributes: 543 | slots: 10 544 | " 545 | `; 546 | 547 | exports[`Upstream should not update if already up to date 1`] = ` 548 | Array [ 549 | Object { 550 | "type": "kong-info", 551 | "version": "___version___", 552 | }, 553 | Object { 554 | "params": Object { 555 | "body": Object { 556 | "name": "mockbinUpstream", 557 | "slots": 10, 558 | }, 559 | "endpoint": Object { 560 | "name": "upstreams", 561 | }, 562 | "method": "POST", 563 | "type": "create-upstream", 564 | }, 565 | "type": "request", 566 | "uri": "http://localhost:8001/upstreams", 567 | }, 568 | Object { 569 | "content": Object { 570 | "created_at": "___created_at___", 571 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 572 | "name": "mockbinUpstream", 573 | "orderlist": "___orderlist___", 574 | "slots": 10, 575 | }, 576 | "ok": true, 577 | "params": Object { 578 | "body": Object { 579 | "name": "mockbinUpstream", 580 | "slots": 10, 581 | }, 582 | "endpoint": Object { 583 | "name": "upstreams", 584 | }, 585 | "method": "POST", 586 | "type": "create-upstream", 587 | }, 588 | "status": 201, 589 | "statusText": "Created", 590 | "type": "response", 591 | "uri": "http://localhost:8001/upstreams", 592 | }, 593 | Object { 594 | "type": "kong-info", 595 | "version": "___version___", 596 | }, 597 | Object { 598 | "params": Object { 599 | "noop": true, 600 | "type": "noop-upstream", 601 | "upstream": Object { 602 | "attributes": Object { 603 | "slots": 10, 604 | }, 605 | "ensure": "present", 606 | "name": "mockbinUpstream", 607 | }, 608 | }, 609 | "type": "noop", 610 | }, 611 | ] 612 | `; 613 | 614 | exports[`Upstream should not update if already up to date 2`] = ` 615 | "apis: [] 616 | consumers: [] 617 | plugins: [] 618 | upstreams: 619 | - name: mockbinUpstream 620 | targets: [] 621 | attributes: 622 | slots: 10 623 | " 624 | `; 625 | 626 | exports[`Upstream should remove the upstream 1`] = ` 627 | Array [ 628 | Object { 629 | "type": "kong-info", 630 | "version": "___version___", 631 | }, 632 | Object { 633 | "params": Object { 634 | "noop": true, 635 | "type": "noop-upstream", 636 | "upstream": Object { 637 | "attributes": Object { 638 | "slots": 10, 639 | }, 640 | "ensure": "removed", 641 | "name": "mockbinUpstream", 642 | }, 643 | }, 644 | "type": "noop", 645 | }, 646 | ] 647 | `; 648 | 649 | exports[`Upstream should remove the upstream 2`] = ` 650 | "apis: [] 651 | consumers: [] 652 | plugins: [] 653 | upstreams: [] 654 | " 655 | `; 656 | 657 | exports[`Upstream should update the upstream 1`] = ` 658 | Array [ 659 | Object { 660 | "type": "kong-info", 661 | "version": "___version___", 662 | }, 663 | Object { 664 | "params": Object { 665 | "body": Object { 666 | "name": "mockbinUpstream", 667 | "slots": 10, 668 | }, 669 | "endpoint": Object { 670 | "name": "upstreams", 671 | }, 672 | "method": "POST", 673 | "type": "create-upstream", 674 | }, 675 | "type": "request", 676 | "uri": "http://localhost:8001/upstreams", 677 | }, 678 | Object { 679 | "content": Object { 680 | "created_at": "___created_at___", 681 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 682 | "name": "mockbinUpstream", 683 | "orderlist": "___orderlist___", 684 | "slots": 10, 685 | }, 686 | "ok": true, 687 | "params": Object { 688 | "body": Object { 689 | "name": "mockbinUpstream", 690 | "slots": 10, 691 | }, 692 | "endpoint": Object { 693 | "name": "upstreams", 694 | }, 695 | "method": "POST", 696 | "type": "create-upstream", 697 | }, 698 | "status": 201, 699 | "statusText": "Created", 700 | "type": "response", 701 | "uri": "http://localhost:8001/upstreams", 702 | }, 703 | Object { 704 | "type": "kong-info", 705 | "version": "___version___", 706 | }, 707 | Object { 708 | "params": Object { 709 | "body": Object { 710 | "orderlist": "___orderlist___", 711 | "slots": 20, 712 | }, 713 | "endpoint": Object { 714 | "name": "upstream", 715 | "params": Object { 716 | "name": "mockbinUpstream", 717 | }, 718 | }, 719 | "method": "PATCH", 720 | "type": "update-upstream", 721 | }, 722 | "type": "request", 723 | "uri": "http://localhost:8001/upstreams/mockbinUpstream", 724 | }, 725 | Object { 726 | "content": Object { 727 | "created_at": "___created_at___", 728 | "id": "2b47ba9b-761a-492d-9a0c-000000000001", 729 | "name": "mockbinUpstream", 730 | "orderlist": "___orderlist___", 731 | "slots": 20, 732 | }, 733 | "ok": true, 734 | "params": Object { 735 | "body": Object { 736 | "orderlist": "___orderlist___", 737 | "slots": 20, 738 | }, 739 | "endpoint": Object { 740 | "name": "upstream", 741 | "params": Object { 742 | "name": "mockbinUpstream", 743 | }, 744 | }, 745 | "method": "PATCH", 746 | "type": "update-upstream", 747 | }, 748 | "status": 200, 749 | "statusText": "OK", 750 | "type": "response", 751 | "uri": "http://localhost:8001/upstreams/mockbinUpstream", 752 | }, 753 | ] 754 | `; 755 | 756 | exports[`Upstream should update the upstream 2`] = ` 757 | "apis: [] 758 | consumers: [] 759 | plugins: [] 760 | upstreams: 761 | - name: mockbinUpstream 762 | targets: [] 763 | attributes: 764 | slots: 20 765 | " 766 | `; 767 | --------------------------------------------------------------------------------