├── .busted ├── .editorconfig ├── .github └── workflows │ └── CI_tests.yml ├── .gitignore ├── .luacheckrc ├── .pongo └── pongorc ├── LICENSE ├── README.md ├── kong-plugin-qos-classifier-0.1.0-1.rockspec ├── kong └── plugins │ └── qos-classifier │ ├── access.lua │ ├── config.lua │ ├── counter.lua │ ├── handler.lua │ ├── nodes_updater.lua │ ├── prometheus.lua │ ├── schema.lua │ └── window.lua └── spec ├── fixtures └── custom_nginx.template └── qos-classifier └── 01-integration_spec.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | default = { 3 | verbose = true, 4 | coverage = false, 5 | output = "gtest", 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.lua] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [kong/templates/nginx*] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.template] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [Makefile] 22 | indent_style = tab 23 | -------------------------------------------------------------------------------- /.github/workflows/CI_tests.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | pull_request: 7 | branches: [ '*' ] 8 | tags: [ '*' ] 9 | 10 | workflow_dispatch: 11 | 12 | env: 13 | KONG_VERSION: 2.0.5 14 | 15 | jobs: 16 | build: 17 | name: Build and Test 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | 22 | - name: Checkout github branch 23 | uses: actions/checkout@v2 24 | 25 | - name: Build 26 | uses: nick-fields/retry@v2 27 | with: 28 | timeout_minutes: 5 29 | max_attempts: 3 30 | retry_on: error 31 | command: | 32 | git clone https://github.com/Kong/kong-pongo.git ../kong-pongo 33 | cd ../kong-pongo && git reset --hard 19ee5c20c1c68510430f637cae8bfa43836ab810 && cd "$OLDPWD" 34 | ../kong-pongo/pongo.sh clean && ../kong-pongo/pongo.sh up && ../kong-pongo/pongo.sh build && ../kong-pongo/pongo.sh run -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # servroot is typically the nginx/Kong workingdirectory when testing 2 | servroot 3 | 4 | # packed distribution format for LuaRocks 5 | *.rock 6 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | -- Configuration file for LuaCheck 2 | -- see: https://luacheck.readthedocs.io/en/stable/ 3 | -- 4 | -- To run do: `luacheck .` from the repo 5 | 6 | std = "ngx_lua" 7 | unused_args = false 8 | redefined = false 9 | max_line_length = false 10 | 11 | 12 | globals = { 13 | "_KONG", 14 | "kong", 15 | "ngx.IS_CLI", 16 | } 17 | 18 | 19 | not_globals = { 20 | "string.len", 21 | "table.getn", 22 | } 23 | 24 | 25 | ignore = { 26 | "6.", -- ignore whitespace warnings 27 | } 28 | 29 | 30 | exclude_files = { 31 | } 32 | 33 | 34 | files["spec/**/*.lua"] = { 35 | std = "ngx_lua+busted", 36 | } 37 | -------------------------------------------------------------------------------- /.pongo/pongorc: -------------------------------------------------------------------------------- 1 | --postgres 2 | --cassandra 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Dreamplug Technologies Private Limited 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚦 kong-plugin-qos-classifier 2 | [![CI Tests](https://github.com/CRED-CLUB/kong-plugin-qos-classifier/workflows/CI%20Tests/badge.svg)](https://github.com/CRED-CLUB/kong-plugin-qos-classifier/actions/workflows/CI_tests.yml) 3 | [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | 5 | ## What is QoS? 6 | 7 | Quality of service (QoS) is the use of mechanisms or technologies to control traffic and ensure the performance of critical applications. The term QoS originated in the field of telecommunication and is widely used today in computer networking for traffic prioritization. 8 | 9 | ## What is `kong-plugin-qos-classifier`? 10 | 11 | This is a custom kong plugin which can classify requests into configured QoS classes based on thresholds defined in terms of `requests/sec` and inject the value in the configured HTTP header. 12 | 13 | ## Use-cases of the plugin 14 | 15 | This plugin can help in progressive and pro-active load shedding, at flow level, whenever unexpected spikes in traffic are seen. This is discussed in detail in [part 1](https://engineering.cred.club/qos-through-progressive-load-shedding-during-high-scale-events-part-1-9ab2282c0040) and [part 2](https://engineering.cred.club/qos-through-progressive-load-shedding-during-high-scale-events-part-2-8e44dea863df) series of blogs. 16 | 17 | ## Features of the plugin 18 | 19 | - Categorizes the requests into various classes based on the `req/s` threshold defined. 20 | - Sends custom value of a predefined header to the upstream service based on the class identified, based on which the upstream service can gracefully handle degradations by load shedding desired flows. 21 | - Supports terminating the requests after breaching the highest threshold with custom status code. 22 | - Also supports sending custom header to clients when a request is terminated. Eg, sending a `Location` header with a static CDN URL and `302` status code. 23 | - The classification of requests happen locally in a kong node without any dependency on any central datastore, which makes it performant. 24 | - An endpoint, like that from [`kong-plugin-cluster-stats`](https://github.com/CRED-CLUB/kong-plugin-cluster-stats), could be configured to dynamically update the calculation based on the current number of kong nodes in the cluster. 25 | 26 | ### Prometheus metrics 27 | 28 | The plugin also emits the following prometheus metrics with kong version >= `2.6.0` 29 | 30 | ``` 31 | # HELP kong_qos_request_threshold Threshold for QoS class differentiation 32 | # TYPE kong_qos_request_threshold gauge 33 | kong_qos_request_threshold{class="",route="",service=""} 1 34 | # HELP kong_qos_total_requests Total requests received so far 35 | # TYPE kong_qos_total_requests counter 36 | kong_qos_total_requests{class="",route="",service=""} 10 37 | ``` 38 | 39 | ## Installation and Loading of the plugin 40 | 41 | Follow [standard procedure](https://docs.konghq.com/gateway-oss/2.0.x/plugin-development/distribution/) to install and load the plugin. 42 | 43 | This plugin also requires the following `shared_dict`s to be defined in nginx configuration. The following is the kong configuration for the same: 44 | 45 | ``` 46 | nginx_http_lua_shared_dict = qos_shared 12m; lua_shared_dict qos_lock 100k 47 | ``` 48 | 49 | ## Enabling the plugin 50 | 51 | The plugin can be enabled at `service`, `route` or `global` levels. In case of multiple instances of plugins enabled for a request, evaluation only happens for the highest level. The order of precedence is (starting from the highest level): 52 | 53 | - `route` 54 | - `service` 55 | - `global` 56 | 57 | ## Configuring the plugin 58 | 59 | The plugin has the following configuration object: 60 | ``` 61 | { 62 | "config": { 63 | "termination": { 64 | "status_code": 302, 65 | "header_name": "Location", 66 | "header_value": "https://cred.club" 67 | }, 68 | "upstream_header_name": "X-QOS-CLASS", 69 | "node_count": { 70 | "http_timeout_in_ms": 15, 71 | "update_initial_delay_in_sec": 5, 72 | "update_url": "http://localhost:8001/cluster-stats", 73 | "initial": 2, 74 | "update_frequency_in_sec": 1 75 | }, 76 | "classes": { 77 | "class_1": { 78 | "threshold": 4, 79 | "header_value": "green" 80 | }, 81 | "class_2": { 82 | "threshold": 6, 83 | "header_value": "red" 84 | }, 85 | "class_3": { 86 | "threshold": null, 87 | "header_value": null 88 | }, 89 | "class_4": { 90 | "threshold": null, 91 | "header_value": null 92 | } 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | ### Configuration parameters 99 | 100 | | Parameter | Description | 101 | | ------ | ------ | 102 | |upstream_header_name|HTTP header to be sent to upstream services which carries the class of the request (default: X-QOS-CLASS)| 103 | |classes.class_1.threshold|Threshold in req/s for 1st class of requests| 104 | |classes.class_1.header_value|Value of the header identified by `upstream_header_name` when the requests is within `classes.class_1.threshold`| 105 | |node_count.initial|Initial number of kong nodes| 106 | |termination.status_code|HTTP status code to send when the threshold of the highest class is crossed (default: 429)| 107 | |termination.header_name|HTTP header to send when the request is terminated| 108 | |termination.header_value|Value of `termination.header_name` header to be sent| 109 | |node_count.update_url|HTTP endpoint to hit to get updated number of nodes. The response should have `num_nodes` json field. If this is not set, node counts are not updated dynamically and `node_count.initial` is used| 110 | |node_count.http_timeout_in_ms|HTTP timeout in ms for `node_count.update_url` endpoint (default: 10)| 111 | |node_count.update_frequency_in_sec|Interval at which to update the node count (default: 1)| 112 | |node_count.update_initial_delay_in_sec|Number of seconds to wait before initiating node update calls. This is to make sure that the cluster stabalises before making these calls, eg in case of a rolling deployment of the cluster. Till this time, the configured `node_count.initial` is used for the calculation (default: 15)| 113 | -------------------------------------------------------------------------------- /kong-plugin-qos-classifier-0.1.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-plugin-qos-classifier" 2 | version = "0.1.0-1" 3 | -- The version '0.1.0' is the source code version, the trailing '1' is the version of this rockspec. 4 | -- whenever the source version changes, the rockspec should be reset to 1. The rockspec version is only 5 | -- updated (incremented) when this file changes, but the source remains the same. 6 | supported_platforms = {"linux", "macosx"} 7 | 8 | source = { 9 | url = "https://github.com/CRED-CLUB/kong-plugin-qos-classifier", 10 | tag = "0.1.0" 11 | } 12 | 13 | description = { 14 | summary = "Plugin to classify requests based on the req/s thresholds", 15 | license = "Apache 2.0" 16 | } 17 | 18 | dependencies = { 19 | } 20 | 21 | build = { 22 | type = "builtin", 23 | modules = { 24 | ["kong.plugins.qos-classifier.handler"] = "kong/plugins/qos-classifier/handler.lua", 25 | ["kong.plugins.qos-classifier.schema"] = "kong/plugins/qos-classifier/schema.lua", 26 | ["kong.plugins.qos-classifier.access"] = "kong/plugins/qos-classifier/access.lua", 27 | ["kong.plugins.qos-classifier.config"] = "kong/plugins/qos-classifier/config.lua", 28 | ["kong.plugins.qos-classifier.counter"] = "kong/plugins/qos-classifier/counter.lua", 29 | ["kong.plugins.qos-classifier.window"] = "kong/plugins/qos-classifier/window.lua", 30 | ["kong.plugins.qos-classifier.nodes_updater"] = "kong/plugins/qos-classifier/nodes_updater.lua", 31 | ["kong.plugins.qos-classifier.prometheus"] = "kong/plugins/qos-classifier/prometheus.lua", 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /kong/plugins/qos-classifier/access.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local kong = kong 4 | local set_header = kong.service.request.set_header 5 | 6 | local window = require 'kong.plugins.qos-classifier.window' 7 | -- Prometheus is imported and used if availaible in the current version 8 | local prometheus_importer = require 'kong.plugins.qos-classifier.prometheus' 9 | local prometheus, prometheus_metrics = 10 | prometheus_importer.get_prometheus_if_available() 11 | 12 | -- computes the class of the request and returns the appropriate header 13 | -- value along with a boolean field to indicate if the request should 14 | -- be throttled, once all limits are breached 15 | local function get_class(class_conf, value, num_nodes) 16 | -- since tables are unordered in lua, iterate by ordered list of classes 17 | local class_index = {"class_1", "class_2", "class_3", "class_4"} 18 | for _, class_name in pairs(class_index) do 19 | local class_attrs = class_conf[class_name] 20 | if class_attrs.threshold and class_attrs.header_value then 21 | if value <= (class_attrs.threshold / num_nodes) then 22 | return class_attrs.header_value, false, class_attrs.threshold 23 | end 24 | end 25 | end 26 | return "", true, -1 27 | end 28 | 29 | local function get_scope(plugin_conf) 30 | -- fetch information about service and router from the router 31 | local service = kong.router.get_service() 32 | local route = kong.router.get_route() 33 | 34 | local service_id = service.name or service.id 35 | local route_id = route.name or route.id 36 | 37 | local scope = (plugin_conf.service_id or plugin_conf.route_id) or 'global' 38 | 39 | return scope, service_id, route_id 40 | end 41 | 42 | function _M.execute(plugin_conf, num_nodes) 43 | -- find the scope at which the plugin is enabled 44 | -- if service id or route id is nil, then the plugin is applied globally 45 | local scope, service_id, route_id = get_scope(plugin_conf) 46 | local curr_time = ngx.now() 47 | 48 | -- get the weighted request count in the window 49 | local req_count = window:get_usage(plugin_conf, curr_time, scope) 50 | 51 | -- get the value of the header as defined for this class of request 52 | -- also check if the request breaches all limits and should be throttled 53 | local header_value, should_terminate, class_threshold = get_class( 54 | plugin_conf.classes, 55 | req_count, num_nodes) 56 | 57 | -- set current QoS value, to be used in response header to client 58 | kong.ctx.plugin.qos_value = header_value 59 | 60 | -- increment the counter 61 | window:incr(curr_time, scope) 62 | 63 | -- Set prometheus metrics 64 | if prometheus then 65 | prometheus_metrics.total_requests:inc(1, 66 | {header_value, route_id, service_id}) 67 | prometheus_metrics.threshold:set(class_threshold, 68 | {header_value, route_id, service_id}) 69 | end 70 | 71 | -- Check rate_limiting state 72 | if should_terminate then 73 | if plugin_conf.termination.header_name and 74 | plugin_conf.termination.header_value then 75 | -- set termination header with value 76 | kong.response.set_header(plugin_conf.termination.header_name, 77 | plugin_conf.termination.header_value) 78 | end 79 | -- exit with termination status code 80 | kong.response.exit(plugin_conf.termination.status_code) 81 | end 82 | 83 | -- the request is still under defined limits 84 | -- set the appropriate class in the header to be passed to the upstream 85 | set_header(plugin_conf.upstream_header_name, header_value) 86 | end 87 | 88 | return _M 89 | -------------------------------------------------------------------------------- /kong/plugins/qos-classifier/config.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | -- TTL value of counters which keep the number of requests in a window 4 | _M.COUNTER_TTL_IN_SECS = 5 5 | 6 | -- sync interval for counters to lua shared dict 7 | _M.COUNTER_SYNC_INTERVAL = 1 8 | 9 | -- name of the lua shared dict, for counters, as defined in nginx config 10 | _M.QOS_SHARED_DICT = "qos_shared" 11 | 12 | -- name of the lua shared dict, for resty locks, as defined in nginx config 13 | _M.QOS_SHARED_LOCK = "qos_lock" 14 | 15 | return _M 16 | -------------------------------------------------------------------------------- /kong/plugins/qos-classifier/counter.lua: -------------------------------------------------------------------------------- 1 | local ngx_shared = ngx.shared 2 | local pairs = pairs 3 | local ngx = ngx 4 | local error = error 5 | local setmetatable = setmetatable 6 | local tonumber = tonumber 7 | local config = require 'kong.plugins.qos-classifier.config' 8 | 9 | local clear_tab 10 | do 11 | local ok 12 | ok, clear_tab = pcall(require, "table.clear") 13 | if not ok then 14 | clear_tab = function(tab) for k in pairs(tab) do tab[k] = nil end end 15 | end 16 | end 17 | 18 | local _M = {} 19 | local mt = {__index = _M} 20 | 21 | -- local cache of counters increments 22 | local increments = {} 23 | -- boolean flags of per worker sync timers 24 | local timer_started = {} 25 | 26 | local id 27 | 28 | local function sync(_, self) 29 | local err, _ 30 | local ok = true 31 | for k, v in pairs(self.increments) do 32 | _, err, _ = self.dict:incr(k, v, 0, config.COUNTER_TTL_IN_SECS) 33 | if err then 34 | ngx.log(ngx.WARN, "error increasing counter in shdict key: ", k, 35 | ", err: ", err) 36 | ok = false 37 | end 38 | end 39 | 40 | clear_tab(self.increments) 41 | return ok 42 | end 43 | 44 | function _M.new(shdict_name, sync_interval) 45 | id = ngx.worker.id() 46 | 47 | if not ngx_shared[shdict_name] then 48 | error("shared dict \"" .. (shdict_name or "nil") .. "\" not defined", 2) 49 | end 50 | 51 | if not increments[shdict_name] then increments[shdict_name] = {} end 52 | 53 | local self = setmetatable({ 54 | dict = ngx_shared[shdict_name], 55 | increments = increments[shdict_name] 56 | }, mt) 57 | 58 | if sync_interval then 59 | sync_interval = tonumber(sync_interval) 60 | if not sync_interval or sync_interval < 0 then 61 | error("expect sync_interval to be a positive number", 2) 62 | end 63 | if not timer_started[shdict_name] then 64 | ngx.log(ngx.DEBUG, "start timer for shdict ", shdict_name, " on worker ", 65 | id) 66 | ngx.timer.every(sync_interval, sync, self) 67 | timer_started[shdict_name] = true 68 | end 69 | end 70 | 71 | return self 72 | end 73 | 74 | function _M:sync() return sync(false, self) end 75 | 76 | function _M:incr(key, step) 77 | step = step or 1 78 | local v = self.increments[key] 79 | if v then step = step + v end 80 | 81 | self.increments[key] = step 82 | return true 83 | end 84 | 85 | function _M:reset(key, number) 86 | if not number then return nil, "expect a number at #2" end 87 | return self.dict:incr(key, -number, number) 88 | end 89 | 90 | function _M:get(key) return self.dict:get(key) end 91 | 92 | function _M:get_keys(max_count) return self.dict:get_keys(max_count) end 93 | 94 | return _M 95 | -------------------------------------------------------------------------------- /kong/plugins/qos-classifier/handler.lua: -------------------------------------------------------------------------------- 1 | local config = require 'kong.plugins.qos-classifier.config' 2 | local access = require 'kong.plugins.qos-classifier.access' 3 | local BasePlugin = require 'kong.plugins.base_plugin' 4 | local nodes_updater = require 'kong.plugins.qos-classifier.nodes_updater' 5 | local window = require 'kong.plugins.qos-classifier.window' 6 | 7 | local QOSClassifierHandler = BasePlugin:extend() 8 | 9 | window.init() 10 | 11 | QOSClassifierHandler.VERSION = "0.1.0" 12 | QOSClassifierHandler.PRIORITY = 899 -- run this plugin immediately after the rate limiting plugins 13 | 14 | function QOSClassifierHandler:new() 15 | QOSClassifierHandler.super.new(self, "qos-classifier-plugin") 16 | end 17 | 18 | function QOSClassifierHandler:init_worker() 19 | QOSClassifierHandler.super.init_worker(self) 20 | 21 | window:init_worker(config.COUNTER_SYNC_INTERVAL) 22 | end 23 | 24 | function QOSClassifierHandler:access(config) 25 | -- try getting the number of kong nodes in the cluster 26 | local num_nodes = nodes_updater.try_fetch(config.node_count.initial, 27 | config.node_count.update_url, 28 | config.node_count.http_timeout_in_ms, 29 | config.node_count 30 | .update_frequency_in_sec, 31 | config.node_count 32 | .update_initial_delay_in_sec) 33 | 34 | access.execute(config, num_nodes) 35 | end 36 | 37 | function QOSClassifierHandler:response(config) 38 | if config.send_header_in_response_to_client then 39 | kong.response.set_header(config.upstream_header_name, 40 | kong.ctx.plugin.qos_value) 41 | end 42 | end 43 | 44 | return QOSClassifierHandler 45 | -------------------------------------------------------------------------------- /kong/plugins/qos-classifier/nodes_updater.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local kong = kong 4 | local ngx_now = ngx.now 5 | local qos_shm = ngx.shared.qos_shared 6 | local json = require "cjson" 7 | local httpc = require "resty.http" 8 | local resty_lock = require "resty.lock" 9 | local config = require 'kong.plugins.qos-classifier.config' 10 | 11 | local NODE_COUNT_LAST_UPDATED_AT = "NODE_COUNT_LAST_UPDATED_AT" 12 | local LAST_NODE_COUNT = "LAST_NODE_COUNT" 13 | local NODE_INIT_TIME = "NODE_INIT_TIME" 14 | local NODE_UPDATE_LOCK = "NODE_UPDATE_LOCK" 15 | 16 | local function isempty(s) return s == nil or s == '' end 17 | 18 | local function handle_response(response_code, response_body) 19 | if response_code ~= 200 then 20 | kong.log.err("received non 200 response code: ", response_code) 21 | return 0, false 22 | end 23 | 24 | local r, err = json.decode(response_body) 25 | if err then 26 | kong.log.err("unable to parse response body: ", err) 27 | return 0, false 28 | end 29 | 30 | return r.num_nodes, true 31 | end 32 | 33 | local function http_request(url, timeout) 34 | local client = httpc.new() 35 | client:set_timeout(timeout) 36 | kong.log.notice("making http request") 37 | local res, err = client:request_uri(url, { 38 | method = "GET", 39 | headers = {["Content-Type"] = "application/json"} 40 | }) 41 | if not res then 42 | kong.log.err("http request failed ", err) 43 | return 0, false 44 | end 45 | return handle_response(res.status, res.body) 46 | end 47 | 48 | local function fetch(url, timeout) return http_request(url, timeout) end 49 | 50 | local function local_fetch() 51 | local curr_node_count, _ = qos_shm:get(LAST_NODE_COUNT) 52 | 53 | -- if due to some reason, fetch from shm fails, 54 | -- return nil 55 | if curr_node_count == nil then 56 | kong.log.err("error in getting current node count") 57 | return nil 58 | end 59 | 60 | return curr_node_count 61 | end 62 | 63 | local function wait_for_initial_delay(curr_time, initial_delay) 64 | local init_time, _ = qos_shm:get(NODE_INIT_TIME) 65 | if init_time == nil then 66 | local ok, err, _ = qos_shm:set(NODE_INIT_TIME, curr_time) 67 | if err then kong.log.err("error in setting node init time: ", err) end 68 | init_time = curr_time 69 | end 70 | 71 | if curr_time > init_time + initial_delay then return false end 72 | 73 | return true 74 | end 75 | 76 | -- tries to fetch the current count of kong nodes in the clusters 77 | -- and returns the number of nodes 78 | function _M.try_fetch(initial_count, url, timeout, frequency, initial_delay) 79 | local now = ngx_now() 80 | 81 | -- return the configured count of nodes if the URL is not set 82 | -- or the initial delay period has not expired 83 | if isempty(url) or wait_for_initial_delay(now, initial_delay) then 84 | return initial_count 85 | end 86 | 87 | -- get the time when the node count was last updated and compare 88 | -- if the current time is past the defined frequency in secs 89 | local last_updated_at, _ = qos_shm:get(NODE_COUNT_LAST_UPDATED_AT) 90 | 91 | if last_updated_at and now <= last_updated_at + frequency then 92 | -- if the current time is still below the frequency 93 | -- fetch the count from shm to return 94 | return local_fetch() or initial_count 95 | end 96 | 97 | -- prepare lock opts 98 | -- set exptime as http timeout + 10ms 99 | -- set timeout i.e as 0, ie 0 wait to acquire lock 100 | local opts = {} 101 | opts["exptime"] = (timeout + 10) / 1000 102 | opts["timeout"] = 0 103 | 104 | local lock, err = resty_lock:new(config.QOS_SHARED_LOCK, opts) 105 | if not lock then 106 | kong.log.err("failed to create lock: ", err) 107 | return local_fetch() or initial_count 108 | end 109 | 110 | -- try acquire lock 111 | local elapsed, err = lock:lock(NODE_UPDATE_LOCK) 112 | if err then return local_fetch() or initial_count end 113 | 114 | -- check again if any other thread has uodated the node count 115 | last_updated_at, _ = qos_shm:get(NODE_COUNT_LAST_UPDATED_AT) 116 | if last_updated_at and now <= last_updated_at + frequency then 117 | local ok, err = lock:unlock() 118 | if not ok then kong.log.err("failed to unlock: ", err) end 119 | return local_fetch() or initial_count 120 | end 121 | 122 | -- fetch the node count 123 | local num_nodes, _ = fetch(url, timeout) 124 | 125 | -- if the number of nodes fetched is 0, then 126 | -- an error had occured, return the last node count 127 | if not num_nodes or num_nodes == 0 then 128 | local ok, err = lock:unlock() 129 | if not ok then kong.log.err("failed to unlock: ", err) end 130 | return local_fetch() or initial_count 131 | end 132 | 133 | -- node count just got updated, update the key in shm 134 | local ok, err, _ = qos_shm:set(NODE_COUNT_LAST_UPDATED_AT, now) 135 | if err then 136 | kong.log.err("error in setting node count last updated at: ", err) 137 | end 138 | 139 | -- also update the current count of nodes 140 | local ok, err, _ = qos_shm:set(LAST_NODE_COUNT, num_nodes) 141 | if err then kong.log.err("error in setting last node count: ", err) end 142 | 143 | local ok, err = lock:unlock() 144 | if not ok then kong.log.err("failed to unlock: ", err) end 145 | 146 | return num_nodes 147 | end 148 | 149 | return _M 150 | -------------------------------------------------------------------------------- /kong/plugins/qos-classifier/prometheus.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local kong = kong 4 | local prometheus_exp = require 'kong.plugins.prometheus.exporter' 5 | 6 | function _M.get_prometheus_if_available() 7 | 8 | local ok 9 | local prometheus 10 | local prometheus_metrics = {} 11 | 12 | -- Import prometheus if it is availaible 13 | ok, prometheus = pcall(prometheus_exp.get_prometheus, {}) 14 | 15 | if not ok then 16 | kong.log.warn( 17 | "Failed to import Prometheus. Make sure you are using kong version >= 2.6.0") 18 | prometheus = nil 19 | else 20 | prometheus_metrics.total_requests = prometheus:counter("qos_total_requests", 21 | "Total requests received so far", 22 | { 23 | "class", "route", "service" 24 | }) 25 | prometheus_metrics.threshold = prometheus:gauge("qos_request_threshold", 26 | "Threshold for QoS class differentiation", 27 | { 28 | "class", "route", "service" 29 | }) 30 | end 31 | return prometheus, prometheus_metrics 32 | end 33 | 34 | return _M 35 | -------------------------------------------------------------------------------- /kong/plugins/qos-classifier/schema.lua: -------------------------------------------------------------------------------- 1 | local typedefs = require "kong.db.schema.typedefs" 2 | 3 | local request_class_record = { 4 | type = "record", 5 | fields = { 6 | {threshold = {type = "number", gt = 0}}, {header_value = {type = "string"}} 7 | } 8 | } 9 | 10 | local schema = { 11 | name = "qos-classifier", 12 | fields = { 13 | {consumer = typedefs.no_consumer}, {protocols = typedefs.protocols_http}, { 14 | config = { 15 | type = "record", 16 | fields = { 17 | {upstream_header_name = {type = "string", default = 'X-QOS-CLASS'}}, 18 | { 19 | send_header_in_response_to_client = { 20 | type = "boolean", 21 | default = false 22 | } 23 | }, { 24 | strategy = { 25 | type = "string", 26 | default = "bucket", 27 | one_of = {"bucket", "blanket"} 28 | } 29 | }, { 30 | classes = { 31 | type = "record", 32 | fields = { 33 | {class_1 = request_class_record}, 34 | {class_2 = request_class_record}, 35 | {class_3 = request_class_record}, 36 | {class_4 = request_class_record} 37 | } 38 | } 39 | }, { 40 | termination = { 41 | type = "record", 42 | fields = { 43 | {header_name = typedefs.header_name}, 44 | {header_value = {type = "string"}}, 45 | { 46 | status_code = { 47 | type = "integer", 48 | default = 429, 49 | between = {100, 599} 50 | } 51 | } 52 | } 53 | } 54 | }, { 55 | node_count = { 56 | type = "record", 57 | fields = { 58 | {initial = {type = "integer", required = true}}, 59 | {update_url = typedefs.url}, { 60 | http_timeout_in_ms = { 61 | type = "integer", 62 | required = true, 63 | gt = 0, 64 | default = 10 65 | } 66 | }, { 67 | update_frequency_in_sec = { 68 | type = "integer", 69 | required = true, 70 | gt = 0, 71 | default = 1 72 | } 73 | }, { 74 | update_initial_delay_in_sec = { 75 | type = "integer", 76 | required = true, 77 | gt = 0, 78 | default = 15 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | return schema 91 | -------------------------------------------------------------------------------- /kong/plugins/qos-classifier/window.lua: -------------------------------------------------------------------------------- 1 | local config = require 'kong.plugins.qos-classifier.config' 2 | local async_counter_lib = require 'kong.plugins.qos-classifier.counter' 3 | local math = require 'math' 4 | 5 | local kong = kong 6 | local ngx_now = ngx.now 7 | 8 | local function cache_key(scope, time) return scope .. ":" .. time end 9 | 10 | local _M = {} 11 | local mt = {__index = _M} 12 | 13 | function _M.init() 14 | if ngx.get_phase() ~= 'init' and ngx.get_phase() ~= 'init_worker' and 15 | ngx.get_phase() ~= 'timer' then 16 | error('init can only be called from ' .. 17 | 'init_by_lua_block, init_worker_by_lua_block or timer', 2) 18 | end 19 | local self = setmetatable({}, mt) 20 | 21 | if ngx.get_phase() == 'init_worker' then self:init_worker(1) end 22 | 23 | return self 24 | end 25 | 26 | function _M:init_worker(sync_interval) 27 | if ngx.get_phase() ~= 'init_worker' then 28 | error('init_worker can only be called in ' .. 'init_worker_by_lua_block', 2) 29 | end 30 | 31 | if self._counter then 32 | ngx.log(ngx.WARN, 'init_worker() has been called twice. ' .. 33 | 'Please do not explicitly call init_worker. ' .. 34 | 'Instead, call init() in the init_worker_by_lua_block') 35 | return 36 | end 37 | 38 | local c, err = async_counter_lib.new(config.QOS_SHARED_DICT, sync_interval) 39 | if err ~= nil then kong.log.err("error in init counter: ", err) end 40 | 41 | self._counter = c 42 | 43 | return self 44 | end 45 | 46 | function _M:get_usage(plugin_conf, curr_time, scope) 47 | local rounded_off_time = math.floor(curr_time) 48 | 49 | -- it may take upto 1s for counters to sync, hence 50 | local key = cache_key(scope, rounded_off_time - 2) 51 | 52 | local value, err = self._counter:get(key) 53 | if not value or value == 0 then return 0 end 54 | 55 | if plugin_conf.strategy and plugin_conf.strategy == "blanket" then 56 | return value 57 | end 58 | 59 | return ((curr_time - rounded_off_time) * value) 60 | end 61 | 62 | function _M:incr(curr_time, scope) 63 | local rounded_off_time = math.floor(curr_time) 64 | 65 | local key = cache_key(scope, rounded_off_time) 66 | 67 | local ok = self._counter:incr(key, 1) 68 | end 69 | 70 | return _M 71 | -------------------------------------------------------------------------------- /spec/fixtures/custom_nginx.template: -------------------------------------------------------------------------------- 1 | # This is a custom nginx configuration template for Kong specs 2 | 3 | pid pids/nginx.pid; # mandatory even for custom config templates 4 | error_log ${{PROXY_ERROR_LOG}} ${{LOG_LEVEL}}; 5 | 6 | # injected nginx_main_* directives 7 | > for _, el in ipairs(nginx_main_directives) do 8 | $(el.name) $(el.value); 9 | > end 10 | 11 | > if database == "off" then 12 | lmdb_environment_path ${{LMDB_ENVIRONMENT_PATH}}; 13 | lmdb_map_size ${{LMDB_MAP_SIZE}}; 14 | > end 15 | 16 | events { 17 | # injected nginx_events_* directives 18 | > for _, el in ipairs(nginx_events_directives) do 19 | $(el.name) $(el.value); 20 | > end 21 | } 22 | 23 | > if role == "control_plane" or #proxy_listeners > 0 or #admin_listeners > 0 or #status_listeners > 0 then 24 | http { 25 | charset UTF-8; 26 | server_tokens off; 27 | 28 | error_log ${{PROXY_ERROR_LOG}} ${{LOG_LEVEL}}; 29 | 30 | lua_package_path '${{LUA_PACKAGE_PATH}};;'; 31 | lua_package_cpath '${{LUA_PACKAGE_CPATH}};;'; 32 | lua_socket_pool_size ${{LUA_SOCKET_POOL_SIZE}}; 33 | lua_socket_log_errors off; 34 | lua_max_running_timers 4096; 35 | lua_max_pending_timers 16384; 36 | lua_ssl_verify_depth ${{LUA_SSL_VERIFY_DEPTH}}; 37 | > if lua_ssl_trusted_certificate_combined then 38 | lua_ssl_trusted_certificate '${{LUA_SSL_TRUSTED_CERTIFICATE_COMBINED}}'; 39 | > end 40 | 41 | lua_shared_dict kong 5m; 42 | lua_shared_dict kong_locks 8m; 43 | lua_shared_dict kong_healthchecks 5m; 44 | lua_shared_dict kong_process_events 5m; 45 | lua_shared_dict kong_cluster_events 5m; 46 | lua_shared_dict kong_rate_limiting_counters 12m; 47 | lua_shared_dict kong_core_db_cache ${{MEM_CACHE_SIZE}}; 48 | lua_shared_dict kong_core_db_cache_miss 12m; 49 | lua_shared_dict kong_db_cache ${{MEM_CACHE_SIZE}}; 50 | lua_shared_dict kong_db_cache_miss 12m; 51 | lua_shared_dict qos_shared 12m; 52 | lua_shared_dict qos_lock 100k; 53 | 54 | > if database == "cassandra" then 55 | lua_shared_dict kong_cassandra 5m; 56 | > end 57 | > if role == "control_plane" then 58 | lua_shared_dict kong_clustering 5m; 59 | > end 60 | lua_shared_dict kong_mock_upstream_loggers 10m; 61 | 62 | underscores_in_headers on; 63 | > if ssl_ciphers then 64 | ssl_ciphers ${{SSL_CIPHERS}}; 65 | > end 66 | 67 | # injected nginx_http_* directives 68 | > for _, el in ipairs(nginx_http_directives) do 69 | $(el.name) $(el.value); 70 | > end 71 | 72 | init_by_lua_block { 73 | Kong = require 'kong' 74 | Kong.init() 75 | } 76 | 77 | init_worker_by_lua_block { 78 | Kong.init_worker() 79 | } 80 | 81 | > if (role == "traditional" or role == "data_plane") and #proxy_listeners > 0 then 82 | 83 | upstream kong_upstream { 84 | server 0.0.0.1; 85 | 86 | # injected nginx_upstream_* directives 87 | > for _, el in ipairs(nginx_upstream_directives) do 88 | $(el.name) $(el.value); 89 | > end 90 | 91 | balancer_by_lua_block { 92 | Kong.balancer() 93 | } 94 | } 95 | 96 | server { 97 | server_name kong; 98 | > for _, entry in ipairs(proxy_listeners) do 99 | listen $(entry.listener); 100 | > end 101 | 102 | error_page 400 404 408 411 412 413 414 417 494 /kong_error_handler; 103 | error_page 500 502 503 504 /kong_error_handler; 104 | 105 | access_log ${{PROXY_ACCESS_LOG}}; 106 | error_log ${{PROXY_ERROR_LOG}} ${{LOG_LEVEL}}; 107 | 108 | > if proxy_ssl_enabled then 109 | ssl_certificate ${{SSL_CERT}}; 110 | ssl_certificate_key ${{SSL_CERT_KEY}}; 111 | ssl_session_cache shared:SSL:10m; 112 | ssl_certificate_by_lua_block { 113 | Kong.ssl_certificate() 114 | } 115 | > end 116 | 117 | # injected nginx_proxy_* directives 118 | > for _, el in ipairs(nginx_proxy_directives) do 119 | $(el.name) $(el.value); 120 | > end 121 | > for _, ip in ipairs(trusted_ips) do 122 | set_real_ip_from $(ip); 123 | > end 124 | 125 | rewrite_by_lua_block { 126 | Kong.rewrite() 127 | } 128 | 129 | access_by_lua_block { 130 | Kong.access() 131 | } 132 | 133 | header_filter_by_lua_block { 134 | Kong.header_filter() 135 | } 136 | 137 | body_filter_by_lua_block { 138 | Kong.body_filter() 139 | } 140 | 141 | log_by_lua_block { 142 | Kong.log() 143 | } 144 | 145 | location / { 146 | default_type ''; 147 | 148 | set $ctx_ref ''; 149 | set $upstream_te ''; 150 | set $upstream_host ''; 151 | set $upstream_upgrade ''; 152 | set $upstream_connection ''; 153 | set $upstream_scheme ''; 154 | set $upstream_uri ''; 155 | set $upstream_x_forwarded_for ''; 156 | set $upstream_x_forwarded_proto ''; 157 | set $upstream_x_forwarded_host ''; 158 | set $upstream_x_forwarded_port ''; 159 | set $upstream_x_forwarded_path ''; 160 | set $upstream_x_forwarded_prefix ''; 161 | set $kong_proxy_mode 'http'; 162 | 163 | proxy_http_version 1.1; 164 | proxy_buffering on; 165 | proxy_request_buffering on; 166 | 167 | proxy_set_header TE $upstream_te; 168 | proxy_set_header Host $upstream_host; 169 | proxy_set_header Upgrade $upstream_upgrade; 170 | proxy_set_header Connection $upstream_connection; 171 | proxy_set_header X-Forwarded-For $upstream_x_forwarded_for; 172 | proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto; 173 | proxy_set_header X-Forwarded-Host $upstream_x_forwarded_host; 174 | proxy_set_header X-Forwarded-Port $upstream_x_forwarded_port; 175 | proxy_set_header X-Forwarded-Path $upstream_x_forwarded_path; 176 | proxy_set_header X-Forwarded-Prefix $upstream_x_forwarded_prefix; 177 | proxy_set_header X-Real-IP $remote_addr; 178 | proxy_pass_header Server; 179 | proxy_pass_header Date; 180 | proxy_ssl_name $upstream_host; 181 | proxy_ssl_server_name on; 182 | > if client_ssl then 183 | proxy_ssl_certificate ${{CLIENT_SSL_CERT}}; 184 | proxy_ssl_certificate_key ${{CLIENT_SSL_CERT_KEY}}; 185 | > end 186 | proxy_pass $upstream_scheme://kong_upstream$upstream_uri; 187 | } 188 | 189 | location @unbuffered { 190 | internal; 191 | default_type ''; 192 | set $kong_proxy_mode 'unbuffered'; 193 | 194 | proxy_http_version 1.1; 195 | proxy_buffering off; 196 | proxy_request_buffering off; 197 | 198 | proxy_set_header TE $upstream_te; 199 | proxy_set_header Host $upstream_host; 200 | proxy_set_header Upgrade $upstream_upgrade; 201 | proxy_set_header Connection $upstream_connection; 202 | proxy_set_header X-Forwarded-For $upstream_x_forwarded_for; 203 | proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto; 204 | proxy_set_header X-Forwarded-Host $upstream_x_forwarded_host; 205 | proxy_set_header X-Forwarded-Port $upstream_x_forwarded_port; 206 | proxy_set_header X-Forwarded-Path $upstream_x_forwarded_path; 207 | proxy_set_header X-Forwarded-Prefix $upstream_x_forwarded_prefix; 208 | proxy_set_header X-Real-IP $remote_addr; 209 | proxy_pass_header Server; 210 | proxy_pass_header Date; 211 | proxy_ssl_name $upstream_host; 212 | proxy_ssl_server_name on; 213 | > if client_ssl then 214 | proxy_ssl_certificate ${{CLIENT_SSL_CERT}}; 215 | proxy_ssl_certificate_key ${{CLIENT_SSL_CERT_KEY}}; 216 | > end 217 | proxy_pass $upstream_scheme://kong_upstream$upstream_uri; 218 | } 219 | 220 | location @unbuffered_request { 221 | internal; 222 | default_type ''; 223 | set $kong_proxy_mode 'unbuffered'; 224 | 225 | proxy_http_version 1.1; 226 | proxy_buffering on; 227 | proxy_request_buffering off; 228 | 229 | proxy_set_header TE $upstream_te; 230 | proxy_set_header Host $upstream_host; 231 | proxy_set_header Upgrade $upstream_upgrade; 232 | proxy_set_header Connection $upstream_connection; 233 | proxy_set_header X-Forwarded-For $upstream_x_forwarded_for; 234 | proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto; 235 | proxy_set_header X-Forwarded-Host $upstream_x_forwarded_host; 236 | proxy_set_header X-Forwarded-Port $upstream_x_forwarded_port; 237 | proxy_set_header X-Forwarded-Prefix $upstream_x_forwarded_prefix; 238 | proxy_set_header X-Real-IP $remote_addr; 239 | proxy_pass_header Server; 240 | proxy_pass_header Date; 241 | proxy_ssl_name $upstream_host; 242 | proxy_ssl_server_name on; 243 | > if client_ssl then 244 | proxy_ssl_certificate ${{CLIENT_SSL_CERT}}; 245 | proxy_ssl_certificate_key ${{CLIENT_SSL_CERT_KEY}}; 246 | > end 247 | proxy_pass $upstream_scheme://kong_upstream$upstream_uri; 248 | } 249 | 250 | location @unbuffered_response { 251 | internal; 252 | default_type ''; 253 | set $kong_proxy_mode 'unbuffered'; 254 | 255 | proxy_http_version 1.1; 256 | proxy_buffering off; 257 | proxy_request_buffering on; 258 | 259 | proxy_set_header TE $upstream_te; 260 | proxy_set_header Host $upstream_host; 261 | proxy_set_header Upgrade $upstream_upgrade; 262 | proxy_set_header Connection $upstream_connection; 263 | proxy_set_header X-Forwarded-For $upstream_x_forwarded_for; 264 | proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto; 265 | proxy_set_header X-Forwarded-Host $upstream_x_forwarded_host; 266 | proxy_set_header X-Forwarded-Port $upstream_x_forwarded_port; 267 | proxy_set_header X-Forwarded-Prefix $upstream_x_forwarded_prefix; 268 | proxy_set_header X-Real-IP $remote_addr; 269 | proxy_pass_header Server; 270 | proxy_pass_header Date; 271 | proxy_ssl_name $upstream_host; 272 | proxy_ssl_server_name on; 273 | > if client_ssl then 274 | proxy_ssl_certificate ${{CLIENT_SSL_CERT}}; 275 | proxy_ssl_certificate_key ${{CLIENT_SSL_CERT_KEY}}; 276 | > end 277 | proxy_pass $upstream_scheme://kong_upstream$upstream_uri; 278 | } 279 | 280 | location @grpc { 281 | internal; 282 | default_type ''; 283 | set $kong_proxy_mode 'grpc'; 284 | 285 | grpc_set_header TE $upstream_te; 286 | grpc_set_header Host $upstream_host; 287 | grpc_set_header X-Forwarded-For $upstream_x_forwarded_for; 288 | grpc_set_header X-Forwarded-Proto $upstream_x_forwarded_proto; 289 | grpc_set_header X-Forwarded-Host $upstream_x_forwarded_host; 290 | grpc_set_header X-Forwarded-Port $upstream_x_forwarded_port; 291 | grpc_set_header X-Forwarded-Path $upstream_x_forwarded_path; 292 | grpc_set_header X-Forwarded-Prefix $upstream_x_forwarded_prefix; 293 | grpc_set_header X-Real-IP $remote_addr; 294 | grpc_pass_header Server; 295 | grpc_pass_header Date; 296 | grpc_pass grpc://kong_upstream; 297 | } 298 | 299 | location @grpcs { 300 | internal; 301 | default_type ''; 302 | set $kong_proxy_mode 'grpc'; 303 | 304 | grpc_set_header TE $upstream_te; 305 | grpc_set_header Host $upstream_host; 306 | grpc_set_header X-Forwarded-For $upstream_x_forwarded_for; 307 | grpc_set_header X-Forwarded-Proto $upstream_x_forwarded_proto; 308 | grpc_set_header X-Forwarded-Host $upstream_x_forwarded_host; 309 | grpc_set_header X-Forwarded-Port $upstream_x_forwarded_port; 310 | grpc_set_header X-Forwarded-Path $upstream_x_forwarded_path; 311 | grpc_set_header X-Forwarded-Prefix $upstream_x_forwarded_prefix; 312 | grpc_set_header X-Real-IP $remote_addr; 313 | grpc_pass_header Server; 314 | grpc_pass_header Date; 315 | grpc_ssl_name $upstream_host; 316 | grpc_ssl_server_name on; 317 | > if client_ssl then 318 | grpc_ssl_certificate ${{CLIENT_SSL_CERT}}; 319 | grpc_ssl_certificate_key ${{CLIENT_SSL_CERT_KEY}}; 320 | > end 321 | grpc_pass grpcs://kong_upstream; 322 | } 323 | 324 | location = /kong_buffered_http { 325 | internal; 326 | default_type ''; 327 | set $kong_proxy_mode 'http'; 328 | 329 | rewrite_by_lua_block {;} 330 | access_by_lua_block {;} 331 | header_filter_by_lua_block {;} 332 | body_filter_by_lua_block {;} 333 | log_by_lua_block {;} 334 | 335 | proxy_http_version 1.1; 336 | proxy_set_header TE $upstream_te; 337 | proxy_set_header Host $upstream_host; 338 | proxy_set_header Upgrade $upstream_upgrade; 339 | proxy_set_header Connection $upstream_connection; 340 | proxy_set_header X-Forwarded-For $upstream_x_forwarded_for; 341 | proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto; 342 | proxy_set_header X-Forwarded-Host $upstream_x_forwarded_host; 343 | proxy_set_header X-Forwarded-Port $upstream_x_forwarded_port; 344 | proxy_set_header X-Forwarded-Path $upstream_x_forwarded_path; 345 | proxy_set_header X-Forwarded-Prefix $upstream_x_forwarded_prefix; 346 | proxy_set_header X-Real-IP $remote_addr; 347 | proxy_pass_header Server; 348 | proxy_pass_header Date; 349 | proxy_ssl_name $upstream_host; 350 | proxy_ssl_server_name on; 351 | > if client_ssl then 352 | proxy_ssl_certificate ${{CLIENT_SSL_CERT}}; 353 | proxy_ssl_certificate_key ${{CLIENT_SSL_CERT_KEY}}; 354 | > end 355 | proxy_pass $upstream_scheme://kong_upstream$upstream_uri; 356 | } 357 | 358 | location = /kong_error_handler { 359 | internal; 360 | default_type ''; 361 | 362 | uninitialized_variable_warn off; 363 | 364 | rewrite_by_lua_block {;} 365 | access_by_lua_block {;} 366 | 367 | content_by_lua_block { 368 | Kong.handle_error() 369 | } 370 | } 371 | } 372 | > end -- (role == "traditional" or role == "data_plane") and #proxy_listeners > 0 373 | 374 | > if (role == "control_plane" or role == "traditional") and #admin_listeners > 0 then 375 | server { 376 | server_name kong_admin; 377 | > for _, entry in ipairs(admin_listeners) do 378 | listen $(entry.listener); 379 | > end 380 | 381 | access_log ${{ADMIN_ACCESS_LOG}}; 382 | error_log ${{ADMIN_ERROR_LOG}} ${{LOG_LEVEL}}; 383 | 384 | > if admin_ssl_enabled then 385 | ssl_certificate ${{ADMIN_SSL_CERT}}; 386 | ssl_certificate_key ${{ADMIN_SSL_CERT_KEY}}; 387 | ssl_session_cache shared:AdminSSL:10m; 388 | > end 389 | 390 | # injected nginx_admin_* directives 391 | > for _, el in ipairs(nginx_admin_directives) do 392 | $(el.name) $(el.value); 393 | > end 394 | 395 | location / { 396 | default_type application/json; 397 | content_by_lua_block { 398 | Kong.admin_content() 399 | } 400 | header_filter_by_lua_block { 401 | Kong.admin_header_filter() 402 | } 403 | } 404 | 405 | location /nginx_status { 406 | internal; 407 | access_log off; 408 | stub_status; 409 | } 410 | 411 | location /robots.txt { 412 | return 200 'User-agent: *\nDisallow: /'; 413 | } 414 | } 415 | > end -- (role == "control_plane" or role == "traditional") and #admin_listeners > 0 416 | 417 | > if #status_listeners > 0 then 418 | server { 419 | server_name kong_status; 420 | > for _, entry in ipairs(status_listeners) do 421 | listen $(entry.listener); 422 | > end 423 | 424 | access_log ${{STATUS_ACCESS_LOG}}; 425 | error_log ${{STATUS_ERROR_LOG}} ${{LOG_LEVEL}}; 426 | 427 | > if status_ssl_enabled then 428 | ssl_certificate ${{STATUS_SSL_CERT}}; 429 | ssl_certificate_key ${{STATUS_SSL_CERT_KEY}}; 430 | ssl_session_cache shared:StatusSSL:1m; 431 | > end 432 | 433 | # injected nginx_status_* directives 434 | > for _, el in ipairs(nginx_status_directives) do 435 | $(el.name) $(el.value); 436 | > end 437 | 438 | location / { 439 | default_type application/json; 440 | content_by_lua_block { 441 | Kong.status_content() 442 | } 443 | header_filter_by_lua_block { 444 | Kong.status_header_filter() 445 | } 446 | } 447 | 448 | location /nginx_status { 449 | internal; 450 | access_log off; 451 | stub_status; 452 | } 453 | 454 | location /robots.txt { 455 | return 200 'User-agent: *\nDisallow: /'; 456 | } 457 | } 458 | > end 459 | 460 | > if role == "control_plane" then 461 | server { 462 | server_name kong_cluster_listener; 463 | > for _, entry in ipairs(cluster_listeners) do 464 | listen $(entry.listener) ssl; 465 | > end 466 | 467 | access_log off; 468 | 469 | ssl_verify_client optional_no_ca; 470 | ssl_certificate ${{CLUSTER_CERT}}; 471 | ssl_certificate_key ${{CLUSTER_CERT_KEY}}; 472 | ssl_session_cache shared:ClusterSSL:10m; 473 | 474 | location = /v1/outlet { 475 | content_by_lua_block { 476 | Kong.serve_cluster_listener() 477 | } 478 | } 479 | 480 | location = /v1/wrpc { 481 | content_by_lua_block { 482 | Kong.serve_wrpc_listener() 483 | } 484 | } 485 | 486 | location = /version-handshake { 487 | content_by_lua_block { 488 | Kong.serve_version_handshake() 489 | } 490 | } 491 | } 492 | > end -- role == "control_plane" 493 | 494 | server { 495 | server_name mock_upstream; 496 | 497 | listen 15555; 498 | listen 15556 ssl; 499 | 500 | ssl_certificate ${{SSL_CERT}}; 501 | ssl_certificate_key ${{SSL_CERT_KEY}}; 502 | ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; 503 | 504 | set_real_ip_from 127.0.0.1; 505 | 506 | location / { 507 | content_by_lua_block { 508 | local mu = require "spec.fixtures.mock_upstream" 509 | ngx.status = 404 510 | return mu.send_default_json_response() 511 | } 512 | } 513 | 514 | location = / { 515 | content_by_lua_block { 516 | local mu = require "spec.fixtures.mock_upstream" 517 | return mu.send_default_json_response({ 518 | valid_routes = { 519 | ["/ws"] = "Websocket echo server", 520 | ["/get"] = "Accepts a GET request and returns it in JSON format", 521 | ["/xml"] = "Returns a simple XML document", 522 | ["/post"] = "Accepts a POST request and returns it in JSON format", 523 | ["/response-headers?:key=:val"] = "Returns given response headers", 524 | ["/cache/:n"] = "Sets a Cache-Control header for n seconds", 525 | ["/anything"] = "Accepts any request and returns it in JSON format", 526 | ["/request"] = "Alias to /anything", 527 | ["/delay/:duration"] = "Delay the response for seconds", 528 | ["/basic-auth/:user/:pass"] = "Performs HTTP basic authentication with the given credentials", 529 | ["/status/:code"] = "Returns a response with the specified ", 530 | ["/stream/:num"] = "Stream chunks of JSON data via chunked Transfer Encoding", 531 | }, 532 | }) 533 | } 534 | } 535 | 536 | location = /ws { 537 | content_by_lua_block { 538 | local mu = require "spec.fixtures.mock_upstream" 539 | return mu.serve_web_sockets() 540 | } 541 | } 542 | 543 | location /get { 544 | access_by_lua_block { 545 | local mu = require "spec.fixtures.mock_upstream" 546 | return mu.filter_access_by_method("GET") 547 | } 548 | content_by_lua_block { 549 | local mu = require "spec.fixtures.mock_upstream" 550 | return mu.send_default_json_response() 551 | } 552 | } 553 | 554 | location /xml { 555 | content_by_lua_block { 556 | local mu = require "spec.fixtures.mock_upstream" 557 | local xml = [[ 558 | 559 | 560 | Kong, Monolith destroyer. 561 | 562 | ]] 563 | return mu.send_text_response(xml, "application/xml") 564 | } 565 | } 566 | 567 | location /post { 568 | access_by_lua_block { 569 | local mu = require "spec.fixtures.mock_upstream" 570 | return mu.filter_access_by_method("POST") 571 | } 572 | content_by_lua_block { 573 | local mu = require "spec.fixtures.mock_upstream" 574 | return mu.send_default_json_response() 575 | } 576 | } 577 | 578 | location = /response-headers { 579 | access_by_lua_block { 580 | local mu = require "spec.fixtures.mock_upstream" 581 | return mu.filter_access_by_method("GET") 582 | } 583 | content_by_lua_block { 584 | local mu = require "spec.fixtures.mock_upstream" 585 | return mu.send_default_json_response({}, ngx.req.get_uri_args()) 586 | } 587 | } 588 | 589 | location = /hop-by-hop { 590 | content_by_lua_block { 591 | local header = ngx.header 592 | header["Keep-Alive"] = "timeout=5, max=1000" 593 | header["Proxy"] = "Remove-Me" 594 | header["Proxy-Connection"] = "close" 595 | header["Proxy-Authenticate"] = "Basic" 596 | header["Proxy-Authorization"] = "Basic YWxhZGRpbjpvcGVuc2VzYW1l" 597 | header["Transfer-Encoding"] = "chunked" 598 | header["Content-Length"] = nil 599 | header["TE"] = "trailers, deflate;q=0.5" 600 | header["Trailer"] = "Expires" 601 | header["Upgrade"] = "example/1, foo/2" 602 | 603 | ngx.print("hello\r\n\r\nExpires: Wed, 21 Oct 2015 07:28:00 GMT\r\n\r\n") 604 | ngx.exit(200) 605 | } 606 | } 607 | 608 | location ~ "^/cache/(?\d+)$" { 609 | content_by_lua_block { 610 | local mu = require "spec.fixtures.mock_upstream" 611 | return mu.send_default_json_response({}, { 612 | ["Cache-Control"] = "public, max-age=" .. ngx.var.n, 613 | }) 614 | } 615 | } 616 | 617 | location ~ "^/basic-auth/(?[a-zA-Z0-9_]+)/(?.+)$" { 618 | access_by_lua_block { 619 | local mu = require "spec.fixtures.mock_upstream" 620 | return mu.filter_access_by_basic_auth(ngx.var.username, 621 | ngx.var.password) 622 | } 623 | content_by_lua_block { 624 | local mu = require "spec.fixtures.mock_upstream" 625 | return mu.send_default_json_response({ 626 | authenticated = true, 627 | user = ngx.var.username, 628 | }) 629 | } 630 | } 631 | 632 | location ~ "^/(request|anything)" { 633 | content_by_lua_block { 634 | local mu = require "spec.fixtures.mock_upstream" 635 | return mu.send_default_json_response() 636 | } 637 | } 638 | 639 | location ~ "^/delay/(?\d{1,3})$" { 640 | content_by_lua_block { 641 | local mu = require "spec.fixtures.mock_upstream" 642 | local delay_seconds = tonumber(ngx.var.delay_seconds) 643 | if not delay_seconds then 644 | return ngx.exit(ngx.HTTP_NOT_FOUND) 645 | end 646 | 647 | ngx.sleep(delay_seconds) 648 | 649 | return mu.send_default_json_response({ 650 | delay = delay_seconds, 651 | }) 652 | } 653 | } 654 | 655 | location ~ "^/status/(?\d{3})$" { 656 | content_by_lua_block { 657 | local mu = require "spec.fixtures.mock_upstream" 658 | local code = tonumber(ngx.var.code) 659 | if not code then 660 | return ngx.exit(ngx.HTTP_NOT_FOUND) 661 | end 662 | ngx.status = code 663 | return mu.send_default_json_response({ 664 | code = code, 665 | }) 666 | } 667 | } 668 | 669 | location ~ "^/stream/(?\d+)$" { 670 | content_by_lua_block { 671 | local mu = require "spec.fixtures.mock_upstream" 672 | local rep = tonumber(ngx.var.num) 673 | local res = require("cjson").encode(mu.get_default_json_response()) 674 | 675 | ngx.header["X-Powered-By"] = "mock_upstream" 676 | ngx.header["Content-Type"] = "application/json" 677 | 678 | for i = 1, rep do 679 | ngx.say(res) 680 | end 681 | } 682 | } 683 | 684 | location ~ "^/post_log/(?[a-z0-9_]+)$" { 685 | content_by_lua_block { 686 | local mu = require "spec.fixtures.mock_upstream" 687 | return mu.store_log(ngx.var.logname) 688 | } 689 | } 690 | 691 | location ~ "^/post_auth_log/(?[a-z0-9_]+)/(?[a-zA-Z0-9_]+)/(?.+)$" { 692 | access_by_lua_block { 693 | local mu = require "spec.fixtures.mock_upstream" 694 | return mu.filter_access_by_basic_auth(ngx.var.username, 695 | ngx.var.password) 696 | } 697 | content_by_lua_block { 698 | local mu = require "spec.fixtures.mock_upstream" 699 | return mu.store_log(ngx.var.logname) 700 | } 701 | } 702 | 703 | location ~ "^/read_log/(?[a-z0-9_]+)$" { 704 | content_by_lua_block { 705 | local mu = require "spec.fixtures.mock_upstream" 706 | return mu.retrieve_log(ngx.var.logname) 707 | } 708 | } 709 | 710 | location ~ "^/count_log/(?[a-z0-9_]+)$" { 711 | content_by_lua_block { 712 | local mu = require "spec.fixtures.mock_upstream" 713 | return mu.count_log(ngx.var.logname) 714 | } 715 | } 716 | 717 | location ~ "^/reset_log/(?[a-z0-9_]+)$" { 718 | content_by_lua_block { 719 | local mu = require "spec.fixtures.mock_upstream" 720 | return mu.reset_log(ngx.var.logname) 721 | } 722 | } 723 | 724 | location = /echo_sni { 725 | return 200 'SNI=$ssl_server_name\n'; 726 | } 727 | 728 | location = /ocsp { 729 | content_by_lua_block { 730 | local mu = require "spec.fixtures.mock_upstream" 731 | return mu.handle_ocsp() 732 | } 733 | } 734 | 735 | location = /set_ocsp { 736 | content_by_lua_block { 737 | local mu = require "spec.fixtures.mock_upstream" 738 | return mu.set_ocsp(ngx.var.arg_status) 739 | } 740 | } 741 | } 742 | 743 | include '*.http_mock'; 744 | } 745 | > end 746 | 747 | > if #stream_listeners > 0 then 748 | stream { 749 | log_format basic '$remote_addr [$time_local] ' 750 | '$protocol $status $bytes_sent $bytes_received ' 751 | '$session_time'; 752 | 753 | lua_package_path '${{LUA_PACKAGE_PATH}};;'; 754 | lua_package_cpath '${{LUA_PACKAGE_CPATH}};;'; 755 | lua_socket_pool_size ${{LUA_SOCKET_POOL_SIZE}}; 756 | lua_socket_log_errors off; 757 | lua_max_running_timers 4096; 758 | lua_max_pending_timers 16384; 759 | lua_ssl_verify_depth ${{LUA_SSL_VERIFY_DEPTH}}; 760 | > if lua_ssl_trusted_certificate_combined then 761 | lua_ssl_trusted_certificate '${{LUA_SSL_TRUSTED_CERTIFICATE_COMBINED}}'; 762 | > end 763 | 764 | lua_shared_dict stream_kong 5m; 765 | lua_shared_dict stream_kong_locks 8m; 766 | lua_shared_dict stream_kong_healthchecks 5m; 767 | lua_shared_dict stream_kong_process_events 5m; 768 | lua_shared_dict stream_kong_cluster_events 5m; 769 | lua_shared_dict stream_kong_rate_limiting_counters 12m; 770 | lua_shared_dict stream_kong_core_db_cache ${{MEM_CACHE_SIZE}}; 771 | lua_shared_dict stream_kong_core_db_cache_miss 12m; 772 | lua_shared_dict stream_kong_db_cache ${{MEM_CACHE_SIZE}}; 773 | lua_shared_dict stream_kong_db_cache_miss 12m; 774 | > if database == "cassandra" then 775 | lua_shared_dict stream_kong_cassandra 5m; 776 | > end 777 | 778 | > if ssl_ciphers then 779 | ssl_ciphers ${{SSL_CIPHERS}}; 780 | > end 781 | 782 | # injected nginx_stream_* directives 783 | > for _, el in ipairs(nginx_stream_directives) do 784 | $(el.name) $(el.value); 785 | > end 786 | 787 | init_by_lua_block { 788 | -- shared dictionaries conflict between stream/http modules. use a prefix. 789 | local shared = ngx.shared 790 | ngx.shared = setmetatable({}, { 791 | __index = function(t, k) 792 | return shared["stream_" .. k] 793 | end, 794 | }) 795 | 796 | Kong = require 'kong' 797 | Kong.init() 798 | } 799 | 800 | init_worker_by_lua_block { 801 | Kong.init_worker() 802 | } 803 | 804 | upstream kong_upstream { 805 | server 0.0.0.1:1; 806 | balancer_by_lua_block { 807 | Kong.balancer() 808 | } 809 | 810 | # injected nginx_supstream_* directives 811 | > for _, el in ipairs(nginx_supstream_directives) do 812 | $(el.name) $(el.value); 813 | > end 814 | } 815 | 816 | > if #stream_listeners > 0 then 817 | # non-SSL listeners, and the SSL terminator 818 | server { 819 | > for _, entry in ipairs(stream_listeners) do 820 | listen $(entry.listener); 821 | > end 822 | 823 | access_log ${{PROXY_ACCESS_LOG}}; 824 | error_log ${{PROXY_ERROR_LOG}} ${{LOG_LEVEL}}; 825 | 826 | > for _, ip in ipairs(trusted_ips) do 827 | set_real_ip_from $(ip); 828 | > end 829 | # injected nginx_sproxy_* directives 830 | > for _, el in ipairs(nginx_sproxy_directives) do 831 | $(el.name) $(el.value); 832 | > end 833 | 834 | > if stream_proxy_ssl_enabled then 835 | ssl_certificate ${{SSL_CERT}}; 836 | ssl_certificate_key ${{SSL_CERT_KEY}}; 837 | ssl_session_cache shared:StreamSSL:10m; 838 | ssl_certificate_by_lua_block { 839 | Kong.ssl_certificate() 840 | } 841 | > end 842 | 843 | preread_by_lua_block { 844 | Kong.preread() 845 | } 846 | 847 | proxy_ssl on; 848 | proxy_ssl_server_name on; 849 | > if client_ssl then 850 | proxy_ssl_certificate ${{CLIENT_SSL_CERT}}; 851 | proxy_ssl_certificate_key ${{CLIENT_SSL_CERT_KEY}}; 852 | > end 853 | proxy_pass kong_upstream; 854 | 855 | log_by_lua_block { 856 | Kong.log() 857 | } 858 | } 859 | 860 | > if database == "off" then 861 | server { 862 | listen unix:${{PREFIX}}/stream_config.sock; 863 | 864 | error_log ${{PROXY_ERROR_LOG}} ${{LOG_LEVEL}}; 865 | 866 | content_by_lua_block { 867 | Kong.stream_config_listener() 868 | } 869 | } 870 | > end -- database == "off" 871 | > end -- #stream_listeners > 0 872 | 873 | server { 874 | listen 15557; 875 | listen 15558 ssl; 876 | listen 15557 udp; 877 | 878 | ssl_certificate ${{SSL_CERT}}; 879 | ssl_certificate_key ${{SSL_CERT_KEY}}; 880 | ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; 881 | 882 | content_by_lua_block { 883 | local sock = assert(ngx.req.socket()) 884 | local data = sock:receive() -- read a line from downstream 885 | 886 | if ngx.var.protocol == "TCP" then 887 | ngx.say(data) 888 | 889 | else 890 | sock:send(data) -- echo whatever was sent 891 | end 892 | } 893 | } 894 | 895 | include '*.stream_mock'; 896 | } 897 | > end -------------------------------------------------------------------------------- /spec/qos-classifier/01-integration_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require "spec.helpers" 2 | local cjson = require "cjson" 3 | 4 | local PLUGIN_NAME = "qos-classifier" 5 | local TERMINATE_HOST = "terminate.host" 6 | local GREEN_HOST = "green.host" 7 | local ORANGE_HOST = "orange.host" 8 | local RED_HOST = "red.host" 9 | 10 | local config = { 11 | termination = { 12 | status_code = 302, 13 | header_name = "Location", 14 | header_value = "https://cred.club" 15 | }, 16 | upstream_header_name = "X-QOS-CLASS", 17 | node_count = { 18 | http_timeout_in_ms = 15, 19 | update_initial_delay_in_sec = 5, 20 | initial = 1, 21 | update_frequency_in_sec = 1 22 | } 23 | } 24 | 25 | local function set_classes_with_thresholds(green, orange, red) 26 | classes = { 27 | class_1 = {threshold = green, header_value = "Green"}, 28 | class_2 = {threshold = orange, header_value = "Orange"}, 29 | class_3 = {threshold = red, header_value = "Red"} 30 | } 31 | return classes 32 | end 33 | 34 | local function make_requests_to_upstream(host, start_time) 35 | local client 36 | while true do 37 | client = helpers.proxy_client() 38 | assert(client:get("/get", {headers = {Host = host}})) 39 | 40 | -- Make requests to upstream in every 10 ms to avoid 41 | -- getting blocked by nginx for making infinite requests 42 | ngx.sleep(0.01) 43 | 44 | -- QoS Classifier Plugin returns header on the basis 45 | -- of requests received in the previous seconds. 46 | -- It takes 2 seconds for the plugin to detect QoS levels 47 | -- Therefore, 2.2 seconds is used as a window 48 | if (ngx.now() - start_time > 2.2) then break end 49 | end 50 | end 51 | 52 | for _, strategy in helpers.each_strategy() do 53 | describe(PLUGIN_NAME .. ": (integration) [#" .. strategy .. "]", function() 54 | local bp, db 55 | 56 | setup(function() 57 | bp, db = helpers.get_db_utils(strategy, {"plugins"}, {PLUGIN_NAME}) 58 | local service = bp.services:insert() 59 | 60 | local route1 = bp.routes:insert{ 61 | hosts = {TERMINATE_HOST}, 62 | service = service 63 | } 64 | 65 | local route2 = bp.routes:insert{hosts = {RED_HOST}, service = service} 66 | 67 | local route3 = bp.routes:insert{hosts = {ORANGE_HOST}, service = service} 68 | 69 | local route4 = bp.routes:insert{hosts = {GREEN_HOST}, service = service} 70 | 71 | -- Enable Plugin on to check termination 72 | config.classes = set_classes_with_thresholds(1, 2, 3) 73 | assert(bp.plugins:insert{ 74 | name = PLUGIN_NAME, 75 | route = route1, 76 | config = config 77 | }) 78 | 79 | -- Enable Plugin on to check header_value = Red 80 | config.classes = set_classes_with_thresholds(1, 2, 1000) 81 | assert(bp.plugins:insert{ 82 | name = PLUGIN_NAME, 83 | route = route2, 84 | config = config 85 | }) 86 | 87 | -- Enable Plugin on to check header_value = Orange 88 | config.classes = set_classes_with_thresholds(1, 1000, 1001) 89 | assert(bp.plugins:insert{ 90 | name = PLUGIN_NAME, 91 | route = route3, 92 | config = config 93 | }) 94 | 95 | -- Enable Plugin on to check header_value = Green 96 | config.classes = set_classes_with_thresholds(1000, 1001, 1002) 97 | assert(bp.plugins:insert{ 98 | name = PLUGIN_NAME, 99 | route = route4, 100 | config = config 101 | }) 102 | 103 | -- Start kong 104 | assert(helpers.start_kong({ 105 | database = strategy, 106 | nginx_conf = "/kong-plugin/spec/fixtures/custom_nginx.template", 107 | plugins = "bundled, " .. PLUGIN_NAME 108 | })) 109 | end) 110 | 111 | teardown(function() 112 | helpers.stop_kong() 113 | assert(db:truncate()) 114 | end) 115 | 116 | before_each(function() 117 | proxy_client = helpers.proxy_client() 118 | admin_client = helpers.admin_client() 119 | end) 120 | 121 | after_each(function() 122 | if proxy_client then proxy_client:close() end 123 | if admin_client then admin_client:close() end 124 | end) 125 | 126 | it("Check for termination", function() 127 | local now = ngx.now() 128 | make_requests_to_upstream(TERMINATE_HOST, now) 129 | local client = helpers.proxy_client() 130 | local res = client:get("/get", {headers = {Host = TERMINATE_HOST}}) 131 | assert.res_status(302, res) 132 | end) 133 | 134 | it("Check Red Header", function() 135 | local now = ngx.now() 136 | make_requests_to_upstream(RED_HOST, now) 137 | local client = helpers.proxy_client() 138 | local res = client:get("/get", {headers = {Host = RED_HOST}}) 139 | body = assert.res_status(200, res) 140 | json = cjson.decode(body) 141 | assert.are.same("Red", json.headers["x-qos-class"]) 142 | end) 143 | 144 | it("Check Orange Header", function() 145 | local now = ngx.now() 146 | make_requests_to_upstream(ORANGE_HOST, now) 147 | local client = helpers.proxy_client() 148 | local res = client:get("/get", {headers = {Host = ORANGE_HOST}}) 149 | body = assert.res_status(200, res) 150 | json = cjson.decode(body) 151 | assert.are.same("Orange", json.headers["x-qos-class"]) 152 | end) 153 | 154 | it("Check Green Header", function() 155 | local now = ngx.now() 156 | make_requests_to_upstream(GREEN_HOST, now) 157 | local client = helpers.proxy_client() 158 | local res = client:get("/get", {headers = {Host = GREEN_HOST}}) 159 | body = assert.res_status(200, res) 160 | json = cjson.decode(body) 161 | assert.are.same("Green", json.headers["x-qos-class"]) 162 | end) 163 | 164 | end) 165 | end 166 | --------------------------------------------------------------------------------