├── .asf.yaml ├── .coveragerc ├── .github ├── semantic.yml └── workflows │ ├── runner-e2e.yml │ ├── runner-license.yml │ ├── runner-lint.yml │ └── runner-unit.yml ├── .gitignore ├── .licenserc.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── apisix ├── __init__.py ├── plugins │ ├── __init__.py │ ├── rewrite.py │ └── stop.py └── runner │ ├── __init__.py │ ├── http │ ├── __init__.py │ ├── request.py │ └── response.py │ ├── plugin │ ├── __init__.py │ ├── cache.py │ └── core.py │ ├── server │ ├── __init__.py │ ├── config.py │ ├── handle.py │ ├── logger.py │ ├── protocol.py │ ├── response.py │ └── server.py │ └── utils │ ├── __init__.py │ └── common.py ├── bin └── py-runner ├── ci ├── apisix │ └── config.yaml └── docker-compose.yml ├── conf └── config.yaml ├── docs ├── assets │ └── images │ │ └── apisix-plugin-runner-overview.png └── en │ └── latest │ ├── config.json │ ├── developer-guide.md │ └── getting-started.md ├── pytest.ini ├── requirements.txt ├── setup.py └── tests ├── conftest.py ├── e2e ├── go.mod ├── go.sum ├── plugins │ ├── plugins_rewrite_test.go │ ├── plugins_stop_test.go │ └── plugins_suite_test.go └── tools │ └── tools.go └── runner ├── http ├── __init__.py ├── test_request.py └── test_response.py ├── plugin ├── __init__.py ├── test_cache.py └── test_core.py ├── server ├── __init__.py ├── test_config.py ├── test_handle.py ├── test_logger.py ├── test_protocol.py ├── test_response.py └── test_server.py └── utils └── test_common.py /.asf.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | github: 18 | description: Apache APISIX Python plugin runner 19 | homepage: https://apisix.apache.org/ 20 | labels: 21 | - apisix 22 | - python 23 | - gateway 24 | - plugin 25 | protected_branches: 26 | master: 27 | required_pull_request_reviews: 28 | dismiss_stale_reviews: true 29 | required_approving_review_count: 1 30 | enabled_merge_buttons: 31 | squash: true 32 | merge: false 33 | rebase: false 34 | features: 35 | issues: true 36 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | [run] 21 | omit = 22 | */__init__.py 23 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | titleOnly: true 2 | allowRevertCommits: true 3 | types: 4 | - feat 5 | - fix 6 | - docs 7 | - style 8 | - refactor 9 | - perf 10 | - test 11 | - build 12 | - ci 13 | - chore 14 | - revert 15 | - change 16 | -------------------------------------------------------------------------------- /.github/workflows/runner-e2e.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | name: Runner E2E Test 21 | 22 | on: 23 | push: 24 | branches: 25 | - master 26 | pull_request: 27 | branches: 28 | - master 29 | 30 | jobs: 31 | run-test: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | 36 | - name: setup python 37 | uses: actions/setup-python@v2 38 | with: 39 | python-version: '3.7' 40 | 41 | - name: setup go 42 | uses: actions/setup-go@v2.1.5 43 | with: 44 | go-version: "1.17" 45 | 46 | - name: setup runner 47 | run: | 48 | make setup 49 | make install 50 | 51 | - name: startup runner 52 | run: | 53 | make dev & 54 | 55 | - name: startup apisix 56 | run: | 57 | docker-compose -f ci/docker-compose.yml up -d 58 | sleep 5 59 | 60 | - name: install ginkgo cli 61 | run: go install github.com/onsi/ginkgo/ginkgo@v1.16.5 62 | 63 | - name: run tests 64 | working-directory: ./tests/e2e 65 | run: ginkgo -r 66 | -------------------------------------------------------------------------------- /.github/workflows/runner-license.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | name: Runner License Checker 21 | 22 | on: 23 | push: 24 | branches: 25 | - master 26 | pull_request: 27 | branches: 28 | - master 29 | 30 | jobs: 31 | Run: 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Check License Header 37 | uses: apache/skywalking-eyes@v0.1.0 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/runner-lint.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | name: Runner Lint Checker 21 | 22 | on: 23 | push: 24 | branches: 25 | - master 26 | pull_request: 27 | branches: 28 | - master 29 | jobs: 30 | Run: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout source codes 34 | uses: actions/checkout@v2 35 | with: 36 | submodules: true 37 | - name: Set up Python 38 | uses: actions/setup-python@v2 39 | with: 40 | python-version: 3.7 41 | - name: Lint codes 42 | run: make lint 43 | -------------------------------------------------------------------------------- /.github/workflows/runner-unit.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | name: Runner Unit Test 21 | 22 | on: 23 | push: 24 | branches: 25 | - master 26 | pull_request: 27 | branches: 28 | - master 29 | jobs: 30 | Run: 31 | runs-on: ubuntu-latest 32 | strategy: 33 | matrix: 34 | python-version: [ '3.7', '3.8', '3.9', '3.10' ] 35 | fail-fast: false 36 | steps: 37 | - name: Checkout source codes 38 | uses: actions/checkout@v2 39 | with: 40 | submodules: true 41 | 42 | - name: Set up Python ${{ matrix.python-version }} 43 | uses: actions/setup-python@v2 44 | with: 45 | python-version: ${{ matrix.python-version }} 46 | 47 | - name: Set up dependencies 48 | run: make setup install 49 | 50 | - name: Run unit tests 51 | run: make test 52 | 53 | - name: Upload coverage profile 54 | if: ${{ matrix.python-version == '3.7' }} 55 | run: bash <(curl -s https://codecov.io/bash) 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | .idea 21 | .vscode 22 | .DS_Store 23 | .coverage 24 | .pytest_cache/ 25 | **/__pycache__/ 26 | -------------------------------------------------------------------------------- /.licenserc.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | header: 19 | license: 20 | spdx-id: Apache-2.0 21 | copyright-owner: Apache Software Foundation 22 | 23 | paths-ignore: 24 | - '.gitignore' 25 | - '.coverage' 26 | - '.coveragerc' 27 | - 'LICENSE' 28 | - 'NOTICE' 29 | - 'pytest.ini' 30 | - '.pytest_cache/**' 31 | - '**/__pycache__/**' 32 | - '**/images/**' 33 | - '**/config.json' 34 | - '.github/' 35 | - '**/go.mod' 36 | - '**/go.sum' 37 | 38 | comment: on-failure 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelog 3 | --- 4 | 5 | 23 | 24 | ## Table of Contents 25 | 26 | - [0.1.0](#010) 27 | - [0.2.0](#020) 28 | 29 | ## 0.1.0 30 | 31 | This release mainly provides basic features and adds test cases. 32 | 33 | ### Core 34 | 35 | - complete project skeleton and available features. 36 | - complete united test with [APISIX 2.7](https://github.com/apache/apisix/tree/release/2.7). 37 | - supported debug mode. 38 | - supported custom `stop` and `rewrite` plugin development. 39 | 40 | [Back to TOC](#table-of-contents) 41 | 42 | ## 0.2.0 43 | 44 | This release mainly refactors the operation objects of request/response and the way of automatic loading of plugins for 45 | more efficient plugin development. 46 | 47 | ### Core 48 | 49 | - `Request` and `Response` operation object refactoring. 50 | - Plugin `auto registr` and `auto loading` refactoring. 51 | - Supports getting the `request body` and `Nginx built-in variables` in the plugin. 52 | - Specification and unifies the input and output of RPC requests. 53 | - Inheritance interface for specification plugins. 54 | 55 | [Back to TOC](#table-of-contents) 56 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | VERSION ?= latest 18 | RELEASE_SRC = apisix-python-plugin-runner-${VERSION}-src 19 | 20 | .PHONY: setup 21 | setup: 22 | python3 -m pip install --upgrade pip 23 | python3 -m pip install -r requirements.txt --ignore-installed 24 | 25 | 26 | .PHONY: test 27 | test: 28 | pytest --version || python3 -m pip install pytest-cov 29 | python3 -m pytest --cov=apisix/runner tests 30 | 31 | 32 | .PHONY: install 33 | install: clean 34 | python3 setup.py install --force 35 | 36 | 37 | .PHONY: lint 38 | lint: clean 39 | flake8 --version || python3 -m pip install flake8 40 | flake8 . --count --select=E9,F63,F7,F82 --show-source 41 | flake8 . --count --max-complexity=15 --max-line-length=120 42 | 43 | 44 | .PHONY: clean 45 | clean: 46 | rm -rf apache_apisix.egg-info dist build assets .coverage report.html release 47 | find . -name "__pycache__" -exec rm -r {} + 48 | find . -name ".pytest_cache" -exec rm -r {} + 49 | find . -name "*.pyc" -exec rm -r {} + 50 | 51 | 52 | .PHONY: dev 53 | dev: 54 | export PYTHONPATH=$(PWD) && python3 bin/py-runner start 55 | 56 | 57 | .PHONY: release 58 | release: 59 | tar -zcvf $(RELEASE_SRC).tgz apisix bin docs tests conf setup.py *.md \ 60 | LICENSE Makefile NOTICE pytest.ini requirements.txt 61 | gpg --batch --yes --armor --detach-sig $(RELEASE_SRC).tgz 62 | shasum -a 512 $(RELEASE_SRC).tgz > $(RELEASE_SRC).tgz.sha512 63 | mkdir -p release 64 | mv $(RELEASE_SRC).tgz release/$(RELEASE_SRC).tgz 65 | mv $(RELEASE_SRC).tgz.asc release/$(RELEASE_SRC).tgz.asc 66 | mv $(RELEASE_SRC).tgz.sha512 release/$(RELEASE_SRC).tgz.sha512 67 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Apache APISIX 2 | Copyright 2021-2022 The Apache Software Foundation 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 19 | 20 | # Python Plugin Runner for Apache APISIX 21 | 22 | [![Runner Unit Test](https://github.com/apache/apisix-python-plugin-runner/actions/workflows/runner-test.yml/badge.svg?branch=master)](https://github.com/apache/apisix-python-plugin-runner/actions) 23 | [![Runner Lint Checker](https://github.com/apache/apisix-python-plugin-runner/actions/workflows/runner-lint.yml/badge.svg?branch=master)](https://github.com/apache/apisix-python-plugin-runner/actions) 24 | [![Runner License Checker](https://github.com/apache/apisix-python-plugin-runner/actions/workflows/runner-license.yml/badge.svg?branch=master)](https://github.com/apache/apisix-python-plugin-runner/actions) 25 | [![Codecov](https://codecov.io/gh/apache/apisix-python-plugin-runner/branch/master/graph/badge.svg)](https://codecov.io/gh/apache/apisix-python-plugin-runner) 26 | 27 | Runs [Apache APISIX](http://apisix.apache.org/) plugins written in Python. Implemented as a sidecar that accompanies 28 | APISIX. 29 | 30 | ![apisix-plugin-runner-overview](./docs/assets/images/apisix-plugin-runner-overview.png) 31 | 32 | # Why apisix-python-plugin-runner 33 | 34 | APISIX offers many full-featured plugins covering areas such as authentication, security, traffic control, serverless, 35 | analytics & monitoring, transformations, logging. 36 | 37 | It also provides highly extensible API, allowing common phases to be mounted, and users can use these api to develop 38 | their own plugins. 39 | 40 | APISIX supports writing plugins in multiple languages in 41 | version [2.7.0](https://github.com/apache/apisix/blob/master/CHANGELOG.md#270), this project is APISIX Python side 42 | implementation that supports writing plugins in Python. 43 | 44 | # Use apisix-python-plugin-runner 45 | 46 | For configuration and use, please refer to the [Getting Started](./docs/en/latest/getting-started.md) document. 47 | 48 | # Get Involved in Development 49 | 50 | Welcome to make contributions, but before you start, please check out 51 | [Developer Guide](./docs/en/latest/developer-guide.md) to learn how to run and debug `apisix-python-plugin-runner` 52 | in your own environment. 53 | 54 | # Status 55 | 56 | This project is currently in the experimental stage and it is not recommended to be used in a production environment. 57 | 58 | # License 59 | 60 | [Apache 2.0 LICENSE](./LICENSE) -------------------------------------------------------------------------------- /apisix/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | -------------------------------------------------------------------------------- /apisix/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | -------------------------------------------------------------------------------- /apisix/plugins/rewrite.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | from typing import Any 19 | from apisix.runner.http.request import Request 20 | from apisix.runner.http.response import Response 21 | from apisix.runner.plugin.core import PluginBase 22 | 23 | 24 | class Rewrite(PluginBase): 25 | 26 | def name(self) -> str: 27 | """ 28 | The name of the plugin registered in the runner 29 | :return: 30 | """ 31 | return "rewrite" 32 | 33 | def config(self, conf: Any) -> Any: 34 | """ 35 | Parse plugin configuration 36 | :param conf: 37 | :return: 38 | """ 39 | return conf 40 | 41 | def filter(self, conf: Any, request: Request, response: Response): 42 | """ 43 | The plugin executes the main function 44 | :param conf: 45 | plugin configuration after parsing 46 | :param request: 47 | request parameters and information 48 | :param response: 49 | response parameters and information 50 | :return: 51 | """ 52 | 53 | # print plugin configuration 54 | print(conf) 55 | 56 | # Fetch request nginx variable `host` 57 | host = request.get_var("host") 58 | print(host) 59 | 60 | # Fetch request body 61 | body = request.get_body() 62 | print(body) 63 | 64 | # Rewrite request headers 65 | request.set_header("X-Resp-A6-Runner", "Python") 66 | 67 | # Rewrite request args 68 | request.set_arg("a6_runner", "Python") 69 | 70 | # Rewrite request path 71 | request.set_uri("/a6/python/runner") 72 | -------------------------------------------------------------------------------- /apisix/plugins/stop.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | from typing import Any 19 | from apisix.runner.http.request import Request 20 | from apisix.runner.http.response import Response 21 | from apisix.runner.plugin.core import PluginBase 22 | 23 | 24 | class Stop(PluginBase): 25 | 26 | def name(self) -> str: 27 | """ 28 | The name of the plugin registered in the runner 29 | :return: 30 | """ 31 | return "stop" 32 | 33 | def config(self, conf: Any) -> Any: 34 | """ 35 | Parse plugin configuration 36 | :param conf: 37 | :return: 38 | """ 39 | return conf 40 | 41 | def filter(self, conf: Any, request: Request, response: Response): 42 | """ 43 | The plugin executes the main function 44 | :param conf: 45 | plugin configuration after parsing 46 | :param request: 47 | request parameters and information 48 | :param response: 49 | response parameters and information 50 | :return: 51 | """ 52 | 53 | # print plugin configuration 54 | print(conf) 55 | 56 | # Fetch request nginx variable `host` 57 | host = request.get_var("host") 58 | print(host) 59 | 60 | # Fetch request body 61 | body = request.get_body() 62 | print(body) 63 | 64 | # Set response headers 65 | response.set_header("X-Resp-A6-Runner", "Python") 66 | 67 | # Set response body 68 | response.set_body("Hello, Python Runner of APISIX") 69 | 70 | # Set response status code 71 | response.set_status_code(201) 72 | -------------------------------------------------------------------------------- /apisix/runner/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | -------------------------------------------------------------------------------- /apisix/runner/http/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | -------------------------------------------------------------------------------- /apisix/runner/http/request.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import socket 19 | import flatbuffers 20 | import apisix.runner.utils.common as runner_utils 21 | 22 | from ipaddress import IPv4Address 23 | from ipaddress import IPv6Address 24 | from A6.HTTPReqCall import Rewrite as HCRw 25 | from A6.HTTPReqCall import Action as HCAction 26 | from A6.HTTPReqCall import Req as HCReq 27 | from A6.PrepareConf import Req as PCReq 28 | from A6.Err.Code import Code as A6ErrCode 29 | from A6.ExtraInfo import Var as EIVar 30 | from A6.ExtraInfo import ReqBody as EIBody 31 | from A6.ExtraInfo import Info as EIInfo 32 | from A6.ExtraInfo import Req as EIReq 33 | from A6.ExtraInfo import Resp as EIResp 34 | from apisix.runner.server.protocol import Protocol as RunnerServerProtocol 35 | from apisix.runner.server.response import RESP_STATUS_CODE_OK 36 | 37 | 38 | class Request: 39 | 40 | def __init__(self, r): 41 | """ 42 | Init and parse request 43 | :param r: 44 | rpc request object 45 | """ 46 | # request object 47 | self.r = r 48 | 49 | # request attribute 50 | self.__remote_addr = "" 51 | self.__headers = {} 52 | self.__args = {} 53 | self.__uri = "" 54 | self.__method = "" 55 | self.__vars = {} 56 | self.__body = "" 57 | 58 | # custom attribute 59 | self.__conf_token = 0 60 | self.__id = 0 61 | self.__configs = {} 62 | 63 | self.__init() 64 | 65 | def get_header(self, key: str) -> str: 66 | """ 67 | get request header 68 | :param key: 69 | :return: 70 | """ 71 | return self.__headers.get(key) 72 | 73 | def set_header(self, key: str, value: str) -> bool: 74 | """ 75 | set request header 76 | :param key: 77 | :param value: 78 | :return: 79 | """ 80 | if key and value: 81 | self.__headers[key] = value 82 | return True 83 | return False 84 | 85 | def get_headers(self) -> dict: 86 | """ 87 | get request headers 88 | :return: 89 | """ 90 | return self.__headers 91 | 92 | def set_headers(self, headers: dict) -> bool: 93 | """ 94 | get request headers 95 | :param headers: 96 | :return: 97 | """ 98 | if headers: 99 | self.__headers = headers 100 | return True 101 | return False 102 | 103 | def get_var(self, key: str) -> str: 104 | """ 105 | get nginx variable 106 | :param key: 107 | :return: 108 | """ 109 | if self.__vars.get(key): 110 | return self.__vars.get(key) 111 | # generate fetch variable RPC request data 112 | builder = runner_utils.new_builder() 113 | var_name = builder.CreateString(key) 114 | EIVar.Start(builder) 115 | EIVar.AddName(builder, var_name) 116 | var_req_data = EIVar.End(builder) 117 | val = self.__ask_extra_info(builder, EIInfo.Info.Var, var_req_data) 118 | self.set_var(key, val) 119 | return val 120 | 121 | def set_var(self, key: str, val: str) -> bool: 122 | """ 123 | set nginx variable 124 | :param key: 125 | :param val: 126 | :return: 127 | """ 128 | if key and val: 129 | self.__vars[key] = val 130 | return True 131 | return False 132 | 133 | def get_body(self) -> str: 134 | """ 135 | get request body 136 | :return: 137 | """ 138 | if self.__body: 139 | return self.__body 140 | # generate fetch body RPC request data 141 | builder = runner_utils.new_builder() 142 | EIBody.Start(builder) 143 | body_req_data = EIBody.End(builder) 144 | val = self.__ask_extra_info(builder, EIInfo.Info.ReqBody, body_req_data) 145 | self.set_body(val) 146 | return val 147 | 148 | def set_body(self, body: str) -> bool: 149 | """ 150 | set request body 151 | :param body: 152 | :return: 153 | """ 154 | if body: 155 | self.__body = body 156 | return True 157 | return False 158 | 159 | def __ask_extra_info(self, builder: flatbuffers.Builder, ty, data) -> str: 160 | """ 161 | nginx built-in variable and request body rpc calls 162 | :param builder: 163 | :param ty: 164 | :param data: 165 | :return: 166 | """ 167 | res_val = [] 168 | EIReq.Start(builder) 169 | EIReq.AddInfoType(builder, ty) 170 | EIReq.AddInfo(builder, data) 171 | res = EIReq.End(builder) 172 | builder.Finish(res) 173 | out = builder.Output() 174 | 175 | try: 176 | protocol = RunnerServerProtocol(out, runner_utils.RPC_EXTRA_INFO) 177 | protocol.encode() 178 | self.r.conn.sendall(protocol.buffer) 179 | except socket.timeout as e: 180 | self.r.log.info("connection timout: {}", e.args.__str__()) 181 | except socket.error as e: 182 | self.r.log.error("connection error: {}", e.args.__str__()) 183 | except BaseException as e: 184 | self.r.log.error("any error: {}", e.args.__str__()) 185 | else: 186 | buf = self.r.conn.recv(runner_utils.RPC_PROTOCOL_HEADER_LEN) 187 | protocol = RunnerServerProtocol(buf, 0) 188 | err = protocol.decode() 189 | if err.code == RESP_STATUS_CODE_OK: 190 | buf = self.r.conn.recv(protocol.length) 191 | resp = EIResp.Resp.GetRootAs(buf) 192 | for i in range(resp.ResultLength()): 193 | vector = resp.Result(i) 194 | res_val.append(chr(vector)) 195 | else: 196 | self.r.log.error(err.message) 197 | 198 | return "".join(res_val) 199 | 200 | def get_arg(self, key: str) -> str: 201 | """ 202 | get request param 203 | :param key: 204 | :return: 205 | """ 206 | return self.__args.get(key) 207 | 208 | def set_arg(self, key: str, value: str) -> bool: 209 | """ 210 | set request param 211 | :param key: 212 | :param value: 213 | :return: 214 | """ 215 | if key and value: 216 | self.__args[key] = value 217 | return True 218 | return False 219 | 220 | def get_args(self) -> dict: 221 | """ 222 | get request params 223 | :return: 224 | """ 225 | return self.__args 226 | 227 | def set_args(self, args: dict) -> bool: 228 | """ 229 | set request params 230 | :param args: 231 | :return: 232 | """ 233 | if args: 234 | self.__args = args 235 | return True 236 | return False 237 | 238 | def get_uri(self) -> str: 239 | """ 240 | get request uri 241 | :return: 242 | """ 243 | return self.__uri 244 | 245 | def set_uri(self, uri: str) -> bool: 246 | """ 247 | set request uri 248 | :param uri: 249 | :return: 250 | """ 251 | if uri and uri.startswith("/"): 252 | self.__uri = uri 253 | return True 254 | return False 255 | 256 | def get_remote_addr(self) -> str: 257 | """ 258 | get request client ip address 259 | :return: 260 | """ 261 | return self.__remote_addr 262 | 263 | def set_remote_addr(self, remote_addr: str) -> bool: 264 | """ 265 | set request client ip address 266 | :param remote_addr: 267 | :return: 268 | """ 269 | if remote_addr: 270 | self.__remote_addr = remote_addr 271 | return True 272 | return False 273 | 274 | def get_conf_token(self) -> int: 275 | """ 276 | get request config token 277 | :return: 278 | """ 279 | return self.__conf_token 280 | 281 | def set_conf_token(self, conf_token: int) -> bool: 282 | """ 283 | set request config token 284 | :param conf_token: 285 | :return: 286 | """ 287 | if conf_token: 288 | self.__conf_token = conf_token 289 | return True 290 | return False 291 | 292 | def get_id(self): 293 | """ 294 | get request id 295 | :return: 296 | """ 297 | return self.__id 298 | 299 | def set_id(self, id: int): 300 | """ 301 | set request id 302 | :param id: 303 | :return: 304 | """ 305 | if id: 306 | self.__id = id 307 | return True 308 | return False 309 | 310 | def set_method(self, method: str) -> bool: 311 | """ 312 | set request method 313 | :param method: 314 | :return: 315 | """ 316 | # support common request method setting 317 | if method and method.upper() in ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"]: 318 | self.__method = method 319 | return True 320 | return False 321 | 322 | def get_method(self) -> str: 323 | """ 324 | get request method 325 | :return: 326 | """ 327 | return self.__method 328 | 329 | def set_config(self, key: str, value: str): 330 | """ 331 | set plugin config 332 | :param key: 333 | :param value: 334 | :return: 335 | """ 336 | if key: 337 | self.__configs[key] = value 338 | return True 339 | return False 340 | 341 | def get_config(self, key: str) -> str: 342 | """ 343 | get plugin config 344 | :param key: 345 | :return: 346 | """ 347 | return self.__configs.get(key) 348 | 349 | def get_configs(self) -> dict: 350 | """ 351 | get plugin configs 352 | :return: 353 | """ 354 | return self.__configs 355 | 356 | def set_configs(self, configs: dict) -> bool: 357 | """ 358 | set plugin configs 359 | :return: 360 | """ 361 | if configs: 362 | self.__configs = configs 363 | return True 364 | return False 365 | 366 | def __init(self) -> None: 367 | """ 368 | init request handler 369 | :return: 370 | """ 371 | if self.r.request.ty == runner_utils.RPC_HTTP_REQ_CALL: 372 | req = HCReq.Req.GetRootAsReq(self.r.request.data) 373 | 374 | # fetch request id 375 | self.set_id(req.Id()) 376 | 377 | # fetch request conf token 378 | self.set_conf_token(req.ConfToken()) 379 | 380 | # fetch request method 381 | self.set_method(runner_utils.get_method_name_by_code(req.Method())) 382 | 383 | # fetch request remote_addr 384 | ip_list = runner_utils.parse_list_vector(req, runner_utils.VECTOR_TYPE_SOURCE_IP, True) 385 | if len(ip_list) == 16: 386 | self.set_remote_addr(IPv6Address(bytes(ip_list)).exploded) 387 | else: 388 | self.set_remote_addr(IPv4Address(bytes(ip_list)).exploded) 389 | 390 | # fetch request uri 391 | self.set_uri(req.Path().decode()) 392 | 393 | # fetch request headers 394 | hdr_dict = runner_utils.parse_dict_vector(req, runner_utils.VECTOR_TYPE_HEADER) 395 | self.set_headers(hdr_dict) 396 | 397 | # fetch request args 398 | arg_dict = runner_utils.parse_dict_vector(req, runner_utils.VECTOR_TYPE_QUERY) 399 | self.set_args(arg_dict) 400 | 401 | if self.r.request.ty == runner_utils.RPC_PREPARE_CONF: 402 | req = PCReq.Req.GetRootAsReq(self.r.request.data) 403 | for i in range(req.ConfLength()): 404 | # fetch request config 405 | name = req.Conf(i).Name().decode() 406 | config = req.Conf(i).Value().decode() 407 | self.set_config(name, config) 408 | 409 | def checked(self): 410 | """ 411 | check request params is valid 412 | :return: 413 | """ 414 | if len(self.__uri) == 0 and len(self.__headers) == 0 and len(self.__args) == 0: 415 | return False 416 | else: 417 | return True 418 | 419 | @runner_utils.response_config 420 | def config_handler(self, builder: flatbuffers.Builder): 421 | """ 422 | get config setting response 423 | :param builder: 424 | :return: 425 | """ 426 | return self.get_conf_token() 427 | 428 | @runner_utils.response_call(HCAction.Action.Rewrite) 429 | def call_handler(self, builder: flatbuffers.Builder): 430 | """ 431 | get http call response 432 | :param builder: 433 | :return: 434 | """ 435 | if not self.checked(): 436 | return None, 0 437 | 438 | path_vector = runner_utils.create_str_vector(builder, self.get_uri()) 439 | 440 | headers_vector = runner_utils.create_dict_vector(builder, self.get_headers(), HCAction.Action.Rewrite, 441 | runner_utils.VECTOR_TYPE_HEADER) 442 | 443 | args_vector = runner_utils.create_dict_vector(builder, self.get_args(), HCAction.Action.Rewrite, 444 | runner_utils.VECTOR_TYPE_QUERY) 445 | 446 | HCRw.RewriteStart(builder) 447 | HCRw.RewriteAddPath(builder, path_vector) 448 | HCRw.RewriteAddHeaders(builder, headers_vector) 449 | HCRw.RewriteAddArgs(builder, args_vector) 450 | rewrite = HCRw.RewriteEnd(builder) 451 | return rewrite, self.get_id() 452 | 453 | @runner_utils.response_unknown 454 | def unknown_handler(self, builder: flatbuffers.Builder): 455 | """ 456 | get unknown response 457 | :param builder: 458 | :return: 459 | """ 460 | return A6ErrCode.BAD_REQUEST 461 | -------------------------------------------------------------------------------- /apisix/runner/http/response.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import flatbuffers 19 | import apisix.runner.utils.common as runner_utils 20 | from A6.HTTPReqCall import Stop as HCStop 21 | from A6.HTTPReqCall import Action as HCAction 22 | 23 | RESP_MAX_DATA_SIZE = 2 << 24 - 1 24 | 25 | PLUGIN_ACTION_STOP = HCAction.Action.Stop 26 | PLUGIN_ACTION_REWRITE = HCAction.Action.Rewrite 27 | 28 | 29 | class Response: 30 | 31 | def __init__(self): 32 | """ 33 | Init and parse request 34 | """ 35 | 36 | # response attribute 37 | self.__body = "" 38 | self.__headers = {} 39 | self.__status_code = 0 40 | 41 | # custom attribute 42 | self.__req_id = 0 43 | 44 | def get_header(self, key: str) -> str: 45 | """ 46 | get response header 47 | :param key: 48 | :return: 49 | """ 50 | return self.__headers.get(key) 51 | 52 | def set_header(self, key: str, value: str) -> bool: 53 | """ 54 | set response header 55 | :param key: 56 | :param value: 57 | :return: 58 | """ 59 | if key and value: 60 | self.__headers[key] = value 61 | return True 62 | return False 63 | 64 | def get_headers(self) -> dict: 65 | """ 66 | get response headers 67 | :return: 68 | """ 69 | return self.__headers 70 | 71 | def set_headers(self, headers: dict) -> bool: 72 | """ 73 | get response headers 74 | :param headers: 75 | :return: 76 | """ 77 | if headers: 78 | self.__headers = headers 79 | return True 80 | return False 81 | 82 | def get_body(self) -> str: 83 | """ 84 | get response body 85 | :return: 86 | """ 87 | return self.__body 88 | 89 | def set_body(self, body: str) -> bool: 90 | """ 91 | get response body 92 | :return: 93 | """ 94 | if body: 95 | self.__body = body 96 | return True 97 | return False 98 | 99 | def get_status_code(self) -> int: 100 | """ 101 | get response status code 102 | :return: 103 | """ 104 | return self.__status_code or 200 105 | 106 | def set_status_code(self, status_code: int) -> bool: 107 | """ 108 | set response status code 109 | :param status_code: 110 | :return: 111 | """ 112 | if status_code and (100 <= status_code <= 599): 113 | self.__status_code = status_code 114 | return True 115 | return False 116 | 117 | def get_req_id(self) -> int: 118 | """ 119 | get request id 120 | :return: 121 | """ 122 | return self.__req_id 123 | 124 | def set_req_id(self, req_id: int) -> bool: 125 | """ 126 | set request id 127 | :param req_id: 128 | :return: 129 | """ 130 | if req_id: 131 | self.__req_id = req_id 132 | return True 133 | return False 134 | 135 | def changed(self) -> bool: 136 | """ 137 | check response handler is change 138 | :return: 139 | """ 140 | if self.__body or self.__headers or self.__status_code: 141 | return True 142 | else: 143 | return False 144 | 145 | @runner_utils.response_call(HCAction.Action.Stop) 146 | def call_handler(self, builder: flatbuffers.Builder): 147 | """ 148 | get http call response 149 | :param builder: 150 | :return: 151 | """ 152 | if not self.changed(): 153 | return None, 0 154 | headers_vector = runner_utils.create_dict_vector(builder, self.get_headers(), HCAction.Action.Stop, 155 | runner_utils.VECTOR_TYPE_HEADER) 156 | 157 | body_vector = runner_utils.create_str_vector(builder, self.get_body()) 158 | 159 | status_code = self.get_status_code() 160 | 161 | HCStop.StopStart(builder) 162 | HCStop.StopAddStatus(builder, status_code) 163 | HCStop.StopAddBody(builder, body_vector) 164 | HCStop.StopAddHeaders(builder, headers_vector) 165 | stop = HCStop.StopEnd(builder) 166 | return stop, self.get_req_id() 167 | -------------------------------------------------------------------------------- /apisix/runner/plugin/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | -------------------------------------------------------------------------------- /apisix/runner/plugin/cache.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | from minicache import cache 18 | 19 | RUNNER_CACHE_TOKEN = "RUNNER:CACHE:TOKEN" 20 | RUNNER_CACHE_ENTRY = "RUNNER:CACHE:ENTRY" 21 | 22 | 23 | def generate_token() -> int: 24 | token = cache.get(RUNNER_CACHE_TOKEN, 0) 25 | token = token + 1 26 | cache.update(RUNNER_CACHE_TOKEN, token) 27 | return token 28 | 29 | 30 | def set_config_by_token(token: int, configs: dict) -> bool: 31 | if len(configs) <= 0: 32 | return False 33 | cache_key = "%s:%s" % (RUNNER_CACHE_ENTRY, token) 34 | cache.update(cache_key, configs) 35 | return cache.has(cache_key) 36 | 37 | 38 | def get_config_by_token(token: int): 39 | cache_key = "%s:%s" % (RUNNER_CACHE_ENTRY, token) 40 | return cache.get(cache_key, {}) 41 | -------------------------------------------------------------------------------- /apisix/runner/plugin/core.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import importlib 20 | from typing import Any 21 | from pkgutil import iter_modules 22 | from apisix.runner.http.response import Response as HttpResponse 23 | from apisix.runner.http.request import Request as HttpRequest 24 | 25 | PLUGINS = {} 26 | 27 | 28 | class PluginBase: 29 | 30 | def __init_subclass__(cls: Any, **kwargs): 31 | """ 32 | register plugin object 33 | :param kwargs: 34 | :return: 35 | """ 36 | name = cls.name(cls) 37 | if name not in PLUGINS: 38 | PLUGINS[name] = cls 39 | 40 | def name(self) -> str: 41 | """ 42 | fetching plugin name 43 | :return: 44 | """ 45 | pass 46 | 47 | def config(self, conf: Any) -> Any: 48 | """ 49 | parsing plugin configuration 50 | :return: 51 | """ 52 | pass 53 | 54 | def filter(self, conf: Any, req: HttpRequest, reps: HttpResponse) -> None: 55 | """ 56 | execute plugin handler 57 | :param conf: plugin configuration 58 | :param req: request object 59 | :param reps: response object 60 | :return: 61 | """ 62 | pass 63 | 64 | 65 | class PluginProcess: 66 | """ 67 | plugin default package name 68 | """ 69 | package = "apisix.plugins" 70 | 71 | @staticmethod 72 | def register(): 73 | plugin_path = "%s/%s" % (os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), 74 | PluginProcess.package.replace(".", "/")) 75 | modules = iter_modules(path=[plugin_path]) 76 | for _, mod_name, _ in modules: 77 | importlib.import_module("%s.%s" % (PluginProcess.package, mod_name)) 78 | 79 | @staticmethod 80 | def execute(configs: dict, r, req: HttpRequest, reps: HttpResponse): 81 | for name, conf in configs.items(): 82 | try: 83 | p = PLUGINS.get(name)() 84 | conf = p.config(conf) 85 | p.filter(conf, req, reps) 86 | except AttributeError as e: 87 | r.log.error("execute plugin `%s` AttributeError, %s" % (name, e.args.__str__())) 88 | return False 89 | except TypeError as e: 90 | r.log.error("execute plugin `%s` TypeError, %s" % (name, e.args.__str__())) 91 | return False 92 | except BaseException as e: 93 | r.log.error("execute plugin `%s` AnyError, %s" % (name, e.args.__str__())) 94 | return False 95 | else: 96 | if reps.changed(): 97 | break 98 | return True 99 | -------------------------------------------------------------------------------- /apisix/runner/server/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | -------------------------------------------------------------------------------- /apisix/runner/server/config.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import yaml 20 | import logging 21 | 22 | 23 | class _ConfigSocket: 24 | 25 | def __init__(self): 26 | """ 27 | init socket config handler 28 | """ 29 | self.file = "/tmp/runner.sock" 30 | 31 | @property 32 | def file(self): 33 | """ 34 | get config file for socket 35 | :return: 36 | """ 37 | return self._file 38 | 39 | @file.setter 40 | def file(self, file: str) -> None: 41 | """ 42 | set config file for socket 43 | :param file: 44 | :return: 45 | """ 46 | self._file = file.replace("unix:", "") 47 | 48 | 49 | class _ConfigLogging: 50 | 51 | def __init__(self): 52 | self.level = "NOTSET" 53 | 54 | @property 55 | def level(self) -> int: 56 | """ 57 | get config level for socket 58 | :return: 59 | """ 60 | return self._level 61 | 62 | @level.setter 63 | def level(self, level: str) -> None: 64 | """ 65 | set config level for socket 66 | :param level: 67 | :return: 68 | """ 69 | level = level.upper() 70 | _name_to_level = { 71 | 'ERROR': logging.ERROR, 72 | 'WARN': logging.WARNING, 73 | 'INFO': logging.INFO, 74 | 'DEBUG': logging.DEBUG, 75 | 'NOTSET': logging.NOTSET, 76 | } 77 | self._level = _name_to_level.get(level, logging.NOTSET) 78 | 79 | 80 | class Config: 81 | 82 | def __init__(self, config_path: str = "", config_name: str = "config.yaml"): 83 | """ 84 | init config 85 | :param config_path: 86 | local config file path 87 | :param config_name: 88 | local config file name 89 | """ 90 | self.socket = _ConfigSocket() 91 | self.logging = _ConfigLogging() 92 | self._loading_config(config_path, config_name) 93 | 94 | @staticmethod 95 | def _get_env_config(config: str): 96 | """ 97 | get the configuration in the local environment variable 98 | :param config: 99 | :return: 100 | """ 101 | if isinstance(config, str) and config.find("$env.") != -1: 102 | env_name = config.replace("$env.", "") 103 | return os.getenv(env_name) 104 | return config 105 | 106 | def _loading_config(self, config_path: str, config_name: str): 107 | """ 108 | load local configuration file 109 | :param config_path: 110 | :param config_name: 111 | :return: 112 | """ 113 | if len(config_path) and os.path.exists(config_path): 114 | abs_path = config_path 115 | else: 116 | abs_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 117 | cf_path = "%s/conf/%s" % (abs_path, config_name) 118 | if not os.path.exists(cf_path): 119 | print("ERR: config file `%s` not exists" % cf_path) 120 | exit(1) 121 | 122 | # reading config file 123 | fs = open(cf_path, encoding="UTF-8") 124 | configs = yaml.load(fs, Loader=yaml.FullLoader) 125 | 126 | # socket config 127 | socket = configs.get("socket", {}) 128 | socket_file = self._get_env_config(socket.get("file")) 129 | if socket_file: 130 | self.socket.file = socket_file 131 | 132 | # logging config 133 | logger = configs.get("logging", {}) 134 | logger_level = self._get_env_config(logger.get("level")) 135 | if logger_level: 136 | self.logging.level = logger_level 137 | -------------------------------------------------------------------------------- /apisix/runner/server/handle.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import flatbuffers 19 | from apisix.runner.plugin.core import PluginProcess as runner_plugin 20 | import apisix.runner.plugin.cache as runner_cache 21 | import apisix.runner.utils.common as runner_utils 22 | from apisix.runner.http.response import Response as NewHttpResponse 23 | from apisix.runner.http.request import Request as NewHttpRequest 24 | from A6.Err.Code import Code as ErrCode 25 | 26 | 27 | class Handle: 28 | 29 | def __init__(self, r): 30 | """ 31 | Init RPC Handle 32 | :param r: 33 | rpc request protocol type 34 | """ 35 | self.r = r 36 | 37 | def dispatch(self) -> flatbuffers.Builder: 38 | # init builder 39 | builder = runner_utils.new_builder() 40 | # parse request 41 | req = NewHttpRequest(self.r) 42 | 43 | if self.r.request.ty == runner_utils.RPC_PREPARE_CONF: 44 | # generate token 45 | token = runner_cache.generate_token() 46 | # get plugins config 47 | configs = req.get_configs() 48 | # cache plugins config 49 | ok = runner_cache.set_config_by_token(token, configs) 50 | if not ok: 51 | self.r.log.error("token `%d` cache setting failed" % token) 52 | req.code = ErrCode.CONF_TOKEN_NOT_FOUND 53 | req.unknown_handler(builder) 54 | return builder 55 | 56 | req.set_conf_token(token) 57 | ok = req.config_handler(builder) 58 | if not ok: 59 | self.r.log.error("prepare conf request failure") 60 | req.code = ErrCode.BAD_REQUEST 61 | req.unknown_handler(builder) 62 | return builder 63 | 64 | return builder 65 | 66 | elif self.r.request.ty == runner_utils.RPC_HTTP_REQ_CALL: 67 | # get request token 68 | token = req.get_conf_token() 69 | # get plugins 70 | configs = runner_cache.get_config_by_token(token) 71 | 72 | if len(configs) == 0: 73 | self.r.log.error("token `%d` cache acquisition failed" % token) 74 | req.code = ErrCode.CONF_TOKEN_NOT_FOUND 75 | req.unknown_handler(builder) 76 | return builder 77 | 78 | # init response 79 | resp = NewHttpResponse() 80 | resp.set_req_id(req.get_id()) 81 | 82 | # execute plugins 83 | ok = runner_plugin.execute(configs, self.r, req, resp) 84 | if not ok: 85 | req.code = ErrCode.SERVICE_UNAVAILABLE 86 | req.unknown_handler(builder) 87 | return builder 88 | 89 | # response changed 90 | ok = resp.call_handler(builder) 91 | if ok: 92 | return builder 93 | 94 | # request changed 95 | ok = req.call_handler(builder) 96 | if not ok: 97 | self.r.log.error("http request call failure") 98 | req.code = ErrCode.BAD_REQUEST 99 | req.unknown_handler(builder) 100 | return builder 101 | 102 | return builder 103 | 104 | else: 105 | self.r.log.error("unknown request") 106 | req.code = ErrCode.BAD_REQUEST 107 | req.unknown_handler(builder) 108 | return builder 109 | -------------------------------------------------------------------------------- /apisix/runner/server/logger.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import sys 19 | import logging 20 | 21 | 22 | class Logger: 23 | def __init__(self, level: int = logging.NOTSET): 24 | """ 25 | init server logger handler 26 | :param level: 27 | logger level 28 | """ 29 | self.logger = logging 30 | self._init(level) 31 | 32 | def set_level(self, level: int): 33 | """ 34 | set level and reset logger 35 | :param level: 36 | :return: 37 | """ 38 | self._init(level) 39 | 40 | def _init(self, level: int): 41 | """ 42 | init logger 43 | :param level: 44 | :return: 45 | """ 46 | self.logger = logging.getLogger() 47 | self.logger.setLevel(level) 48 | 49 | handler = logging.StreamHandler(sys.stdout) 50 | handler.setLevel(level) 51 | formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") 52 | handler.setFormatter(formatter) 53 | self.logger.addHandler(handler) 54 | 55 | def info(self, message: str, *objs): 56 | """ 57 | info level logger output 58 | :param message: 59 | :param objs: 60 | :return: 61 | """ 62 | self.logger.info(message.format(*objs)) 63 | 64 | def error(self, message: str, *objs): 65 | """ 66 | error level logger output 67 | :param message: 68 | :param objs: 69 | :return: 70 | """ 71 | self.logger.error(message.format(*objs)) 72 | 73 | def debug(self, message: str, *objs): 74 | """ 75 | debug level logger output 76 | :param message: 77 | :param objs: 78 | :return: 79 | """ 80 | self.logger.debug(message.format(*objs)) 81 | 82 | def warn(self, message: str, *objs): 83 | """ 84 | warning level logger output 85 | :param message: 86 | :param objs: 87 | :return: 88 | """ 89 | self.logger.warning(message.format(*objs)) 90 | -------------------------------------------------------------------------------- /apisix/runner/server/protocol.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | from apisix.runner.server.response import Response as NewServerResponse 18 | from apisix.runner.server.response import RESP_STATUS_CODE_OK 19 | from apisix.runner.server.response import RESP_STATUS_MESSAGE_OK 20 | from apisix.runner.server.response import RESP_STATUS_CODE_BAD_REQUEST 21 | 22 | 23 | class Protocol: 24 | 25 | def __init__(self, buffer: bytes = b'', ty: int = 0): 26 | self.__buffer = buffer 27 | self.__type = ty 28 | self.__length = 0 29 | 30 | @property 31 | def length(self) -> int: 32 | """ 33 | get buffer length 34 | :return: 35 | """ 36 | return self.__length 37 | 38 | @property 39 | def type(self) -> int: 40 | """ 41 | get protocol type 42 | :return: 43 | """ 44 | return self.__type 45 | 46 | @property 47 | def buffer(self) -> bytes: 48 | """ 49 | get buffer data 50 | :return: 51 | """ 52 | return self.__buffer 53 | 54 | def encode(self) -> NewServerResponse: 55 | """ 56 | encode protocol buffer data 57 | :return: 58 | """ 59 | if len(self.__buffer) == 0: 60 | return NewServerResponse(RESP_STATUS_CODE_BAD_REQUEST, "send buffer is empty") 61 | response_len = len(self.__buffer) 62 | response_header = response_len.to_bytes(4, byteorder="big") 63 | response_header = bytearray(response_header) 64 | response_header[0] = self.__type 65 | response_header = bytes(response_header) 66 | self.__buffer = response_header + self.__buffer 67 | self.__length = len(self.__buffer) 68 | return NewServerResponse(code=RESP_STATUS_CODE_OK, message=RESP_STATUS_MESSAGE_OK) 69 | 70 | def decode(self) -> NewServerResponse: 71 | """ 72 | decode protocol buffer data 73 | :return: 74 | """ 75 | if len(self.__buffer) == 0: 76 | return NewServerResponse(RESP_STATUS_CODE_BAD_REQUEST, "recv buffer is empty") 77 | length = len(self.__buffer) 78 | if length != 4: 79 | return NewServerResponse(RESP_STATUS_CODE_BAD_REQUEST, 80 | "recv protocol type length is 4, got %d" % length) 81 | 82 | buf = bytearray(self.__buffer) 83 | self.__type = buf[0] 84 | buf[0] = 0 85 | self.__length = int.from_bytes(buf, byteorder="big") 86 | return NewServerResponse(code=RESP_STATUS_CODE_OK, message=RESP_STATUS_MESSAGE_OK) 87 | -------------------------------------------------------------------------------- /apisix/runner/server/response.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | from A6.Err import Code as A6ErrCode 19 | 20 | RESP_STATUS_CODE_OK = 200 21 | RESP_STATUS_MESSAGE_OK = "OK" 22 | RESP_STATUS_CODE_BAD_REQUEST = A6ErrCode.Code.BAD_REQUEST 23 | RESP_STATUS_MESSAGE_BAD_REQUEST = "Bad Request" 24 | RESP_STATUS_CODE_SERVICE_UNAVAILABLE = A6ErrCode.Code.SERVICE_UNAVAILABLE 25 | RESP_STATUS_CODE_CONF_TOKEN_NOT_FOUND = A6ErrCode.Code.CONF_TOKEN_NOT_FOUND 26 | 27 | 28 | class Response: 29 | def __init__(self, code: int = 0, message: str = '', data: bytes = b'', ty: int = 0): 30 | """ 31 | init response handler 32 | :param code: 33 | response code 34 | :param message: 35 | response message 36 | :param data: 37 | response data 38 | :param ty: 39 | response type 40 | """ 41 | self.__code = code 42 | self.__message = message 43 | self.__type = ty 44 | self.__data = data 45 | 46 | def __eq__(self, response) -> bool: 47 | """ 48 | compare response handler 49 | :param response: 50 | response handler 51 | :return: 52 | """ 53 | return ( 54 | self.code == response.code and self.message == response.message and self.data == response.data and 55 | self.type == response.type) 56 | 57 | @property 58 | def code(self) -> int: 59 | """ 60 | get code by response handler 61 | :return: 62 | """ 63 | return self.__code 64 | 65 | @property 66 | def message(self) -> str: 67 | """ 68 | get message by response handler 69 | :return: 70 | """ 71 | return self.__message 72 | 73 | @property 74 | def data(self) -> bytes: 75 | """ 76 | get data by response handler 77 | :return: 78 | """ 79 | return self.__data 80 | 81 | @property 82 | def type(self) -> int: 83 | """ 84 | get type by response handler 85 | :return: 86 | """ 87 | return self.__type 88 | -------------------------------------------------------------------------------- /apisix/runner/server/server.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import socket 20 | 21 | from threading import Thread as NewThread 22 | from apisix.runner.server.handle import Handle as NewServerHandle 23 | from apisix.runner.server.protocol import Protocol as NewServerProtocol 24 | from apisix.runner.server.config import Config as NewServerConfig 25 | from apisix.runner.server.logger import Logger as NewServerLogger 26 | from apisix.runner.server.response import RESP_STATUS_CODE_OK 27 | 28 | PROTOCOL_HEADER_LEN = 4 29 | 30 | 31 | class RPCData: 32 | def __init__(self, ty: int = 0, data: bytes = b''): 33 | self.ty = ty 34 | self.data = data 35 | 36 | 37 | class RPCRequest: 38 | def __init__(self, conn: socket.socket, log: NewServerLogger): 39 | self.conn = conn 40 | self.log = log 41 | self.request = RPCData() 42 | self.response = RPCData() 43 | 44 | 45 | def _threaded(r: RPCRequest): 46 | while True: 47 | try: 48 | buffer = r.conn.recv(PROTOCOL_HEADER_LEN) 49 | protocol = NewServerProtocol(buffer, 0) 50 | err = protocol.decode() 51 | if err.code != RESP_STATUS_CODE_OK: 52 | r.log.error(err.message) 53 | break 54 | 55 | r.request.ty = protocol.type 56 | r.log.info("request type:{}, len:{}", protocol.type, protocol.length) 57 | 58 | r.request.data = r.conn.recv(protocol.length) 59 | handler = NewServerHandle(r) 60 | response = handler.dispatch() 61 | protocol = NewServerProtocol(response.Output(), protocol.type) 62 | protocol.encode() 63 | 64 | r.log.info("response type:{}, len:{}", protocol.type, protocol.length) 65 | 66 | r.conn.sendall(protocol.buffer) 67 | except socket.timeout as e: 68 | r.log.info("connection timout: {}", e.args.__str__()) 69 | break 70 | except socket.error as e: 71 | r.log.error("connection error: {}", e.args.__str__()) 72 | break 73 | 74 | r.conn.close() 75 | del r 76 | 77 | 78 | class Server: 79 | def __init__(self, config: NewServerConfig): 80 | self.fd = config.socket.file 81 | if os.path.exists(self.fd): 82 | os.remove(self.fd) 83 | self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 84 | self.sock.bind(self.fd) 85 | self.sock.listen(1024) 86 | 87 | # the default socket permission is 0755, which prevents the 'nobody' worker process 88 | # from writing to it if the APISIX is run under root. 89 | os.chmod(self.fd, 0o766) 90 | if os.stat(self.fd).st_mode & 0xfff != 0o766: 91 | raise Exception("can't change mode for unix socket permission to 766") 92 | 93 | self.logger = NewServerLogger(config.logging.level) 94 | 95 | print("listening on unix:%s" % self.fd) 96 | 97 | def receive(self): 98 | while True: 99 | conn, address = self.sock.accept() 100 | conn.settimeout(60) 101 | 102 | r = RPCRequest(conn, self.logger) 103 | thread = NewThread(target=_threaded, args=(r,)) 104 | thread.setDaemon(True) 105 | thread.start() 106 | 107 | def __del__(self): 108 | self.sock.close() 109 | os.remove(self.fd) 110 | print("Bye") 111 | -------------------------------------------------------------------------------- /apisix/runner/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | -------------------------------------------------------------------------------- /apisix/runner/utils/common.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import flatbuffers 19 | from A6 import Method as A6Method 20 | from A6 import TextEntry as A6Entry 21 | from A6.Err.Code import Code as A6ErrCode 22 | from A6.HTTPReqCall import Rewrite as HCRewrite 23 | from A6.HTTPReqCall import Stop as HCStop 24 | from A6.HTTPReqCall import Action as HCAction 25 | from A6.HTTPReqCall import Resp as HCResp 26 | from A6.PrepareConf import Resp as PCResp 27 | from A6.Err import Resp as ErrResp 28 | 29 | RPC_PROTOCOL_HEADER_LEN = 4 30 | 31 | RPC_PREPARE_CONF = 1 32 | RPC_HTTP_REQ_CALL = 2 33 | RPC_EXTRA_INFO = 3 34 | RPC_UNKNOWN = 0 35 | 36 | VECTOR_TYPE_HEADER = 1 37 | VECTOR_TYPE_QUERY = 2 38 | VECTOR_TYPE_CONFIG = 3 39 | VECTOR_TYPE_SOURCE_IP = 4 40 | 41 | dictVectorParseFuncNames = { 42 | VECTOR_TYPE_HEADER: "Headers", 43 | VECTOR_TYPE_QUERY: "Args", 44 | VECTOR_TYPE_CONFIG: "Conf", 45 | } 46 | 47 | listVectorParseFuncNames = { 48 | VECTOR_TYPE_SOURCE_IP: "SrcIp", 49 | } 50 | 51 | A6MethodGET = "GET" 52 | A6MethodHEAD = "HEAD" 53 | A6MethodPOST = "POST" 54 | A6MethodPUT = "PUT" 55 | A6MethodDELETE = "DELETE" 56 | A6MethodMKCOL = "MKCOL" 57 | A6MethodCOPY = "COPY" 58 | A6MethodMOVE = "MOVE" 59 | A6MethodOPTIONS = "OPTIONS" 60 | A6MethodPROPFIND = "PROPFIND" 61 | A6MethodPROPPATCH = "PROPPATCH" 62 | A6MethodLOCK = "LOCK" 63 | A6MethodUNLOCK = "UNLOCK" 64 | A6MethodPATCH = "PATCH" 65 | A6MethodTRACE = "TRACE" 66 | 67 | methodNames = { 68 | A6Method.Method.GET: A6MethodGET, 69 | A6Method.Method.HEAD: A6MethodHEAD, 70 | A6Method.Method.POST: A6MethodPOST, 71 | A6Method.Method.PUT: A6MethodPUT, 72 | A6Method.Method.DELETE: A6MethodDELETE, 73 | A6Method.Method.MKCOL: A6MethodMKCOL, 74 | A6Method.Method.COPY: A6MethodCOPY, 75 | A6Method.Method.MOVE: A6MethodMOVE, 76 | A6Method.Method.OPTIONS: A6MethodOPTIONS, 77 | A6Method.Method.PROPFIND: A6MethodPROPFIND, 78 | A6Method.Method.PROPPATCH: A6MethodPROPPATCH, 79 | A6Method.Method.LOCK: A6MethodLOCK, 80 | A6Method.Method.UNLOCK: A6MethodUNLOCK, 81 | A6Method.Method.PATCH: A6MethodPATCH, 82 | A6Method.Method.TRACE: A6MethodTRACE, 83 | } 84 | 85 | methodCodes = { 86 | A6MethodGET: A6Method.Method.GET, 87 | A6MethodHEAD: A6Method.Method.HEAD, 88 | A6MethodPOST: A6Method.Method.POST, 89 | A6MethodPUT: A6Method.Method.PUT, 90 | A6MethodDELETE: A6Method.Method.DELETE, 91 | A6MethodMKCOL: A6Method.Method.MKCOL, 92 | A6MethodCOPY: A6Method.Method.COPY, 93 | A6MethodMOVE: A6Method.Method.MOVE, 94 | A6MethodOPTIONS: A6Method.Method.OPTIONS, 95 | A6MethodPROPFIND: A6Method.Method.PROPFIND, 96 | A6MethodPROPPATCH: A6Method.Method.PROPPATCH, 97 | A6MethodLOCK: A6Method.Method.LOCK, 98 | A6MethodUNLOCK: A6Method.Method.UNLOCK, 99 | A6MethodPATCH: A6Method.Method.PATCH, 100 | A6MethodTRACE: A6Method.Method.TRACE, 101 | } 102 | 103 | 104 | def create_dict_entry(builder: flatbuffers.Builder, data: dict) -> list: 105 | entries = [] 106 | if not isinstance(data, dict) or len(data) <= 0: 107 | return entries 108 | for key in data: 109 | val = data[key] 110 | key_bytes = builder.CreateString(key) 111 | val_bytes = builder.CreateString(val) 112 | A6Entry.Start(builder) 113 | A6Entry.AddName(builder, key_bytes) 114 | A6Entry.AddValue(builder, val_bytes) 115 | entry = A6Entry.End(builder) 116 | entries.append(entry) 117 | return entries 118 | 119 | 120 | def get_vector_object(action: int = 0, ty: int = 0): 121 | objects = { 122 | "%s:%s" % (HCAction.Action.Rewrite, VECTOR_TYPE_HEADER): HCRewrite.RewriteStartHeadersVector, 123 | "%s:%s" % (HCAction.Action.Rewrite, VECTOR_TYPE_QUERY): HCRewrite.RewriteStartArgsVector, 124 | "%s:%s" % (HCAction.Action.Stop, VECTOR_TYPE_HEADER): HCStop.StopStartHeadersVector, 125 | } 126 | return objects.get("%s:%s" % (action, ty), None) 127 | 128 | 129 | def create_dict_vector(builder: flatbuffers.Builder, data: dict, action: int = 0, ty: int = 0): 130 | res = 0 131 | entries = create_dict_entry(builder, data) 132 | entries_len = len(entries) 133 | if entries_len == 0: 134 | return res 135 | 136 | vector_object = get_vector_object(action, ty) 137 | if not vector_object: 138 | return res 139 | 140 | vector_object(builder, entries_len) 141 | for i in range(entries_len - 1, -1, -1): 142 | builder.PrependUOffsetTRelative(entries[i]) 143 | return builder.EndVector() 144 | 145 | 146 | def create_str_vector(builder: flatbuffers.Builder, data: str): 147 | res = 0 148 | if not data or len(data) <= 0: 149 | return res 150 | 151 | data = data.encode(encoding="UTF-8") 152 | return builder.CreateByteVector(data) 153 | 154 | 155 | def new_builder(): 156 | return flatbuffers.Builder(256) 157 | 158 | 159 | def get_method_name_by_code(code: int) -> str: 160 | return methodNames.get(code) 161 | 162 | 163 | def get_method_code_by_name(name: str) -> int: 164 | return methodCodes.get(name) 165 | 166 | 167 | def response_call(action_type: int): 168 | def decorator(func): 169 | def wrapper(cls, builder: flatbuffers.Builder): 170 | (action, id) = func(cls, builder) 171 | if not action or id == 0: 172 | return False 173 | 174 | HCResp.Start(builder) 175 | HCResp.AddId(builder, id) 176 | HCResp.AddActionType(builder, action_type) 177 | HCResp.AddAction(builder, action) 178 | res = HCResp.End(builder) 179 | builder.Finish(res) 180 | return True 181 | 182 | return wrapper 183 | 184 | return decorator 185 | 186 | 187 | def response_config(func): 188 | def wrapper(cls, builder: flatbuffers.Builder): 189 | token = func(cls, builder) 190 | if token <= 0: 191 | return False 192 | 193 | PCResp.Start(builder) 194 | PCResp.AddConfToken(builder, token) 195 | res = PCResp.End(builder) 196 | builder.Finish(res) 197 | return True 198 | 199 | return wrapper 200 | 201 | 202 | def response_unknown(func): 203 | def wrapper(cls, builder: flatbuffers.Builder): 204 | err_code = func(cls, builder) 205 | if not err_code: 206 | err_code = A6ErrCode.BAD_REQUEST 207 | ErrResp.Start(builder) 208 | ErrResp.AddCode(builder, err_code) 209 | res = ErrResp.End(builder) 210 | builder.Finish(res) 211 | return True 212 | 213 | return wrapper 214 | 215 | 216 | def parse_dict_vector(cls: object, ty: int) -> dict: 217 | res = {} 218 | fn = dictVectorParseFuncNames.get(ty) 219 | if not fn: 220 | return res 221 | 222 | length = getattr(cls, "%sLength" % fn)() 223 | if not length or length == 0: 224 | return res 225 | 226 | for i in range(length): 227 | key = getattr(cls, fn)(i).Name().decode() 228 | val = getattr(cls, fn)(i).Value().decode() 229 | res[key] = val 230 | 231 | return res 232 | 233 | 234 | def parse_list_vector(cls: object, ty: int, out_bytes: bool = False) -> list: 235 | res = [] 236 | if out_bytes: 237 | res = bytearray() 238 | fn = listVectorParseFuncNames.get(ty) 239 | if not fn: 240 | return res 241 | 242 | length = getattr(cls, "%sLength" % fn)() 243 | if not length or length == 0: 244 | return res 245 | 246 | for i in range(length): 247 | val = getattr(cls, fn)(i) 248 | res.append(val) 249 | 250 | return res 251 | -------------------------------------------------------------------------------- /bin/py-runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # 4 | # Licensed to the Apache Software Foundation (ASF) under one or more 5 | # contributor license agreements. See the NOTICE file distributed with 6 | # this work for additional information regarding copyright ownership. 7 | # The ASF licenses this file to You under the Apache License, Version 2.0 8 | # (the "License"); you may not use this file except in compliance with 9 | # the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | import os 21 | import click 22 | 23 | from apisix.runner.server.server import Server as RunnerServer 24 | from apisix.runner.server.config import Config as RunnerConfig 25 | from apisix.runner.plugin.core import PluginProcess as RunnerPlugin 26 | 27 | RUNNER_VERSION = "0.2.0" 28 | 29 | 30 | @click.group() 31 | @click.version_option(version=RUNNER_VERSION) 32 | def runner() -> None: 33 | pass 34 | 35 | 36 | @runner.command() 37 | def start() -> None: 38 | RunnerPlugin.register() 39 | config = RunnerConfig(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 40 | server = RunnerServer(config) 41 | server.receive() 42 | 43 | 44 | def main() -> None: 45 | runner() 46 | 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /ci/apisix/config.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | apisix: 19 | node_listen: 20 | - 9080 21 | enable_control: true 22 | control: 23 | ip: "0.0.0.0" 24 | port: 9092 25 | 26 | deployment: 27 | role: traditional 28 | role_traditional: 29 | config_provider: etcd 30 | admin: 31 | admin_key: 32 | - 33 | name: admin 34 | key: edd1c9f034335f136f87ad84b625c8f1 35 | role: admin 36 | 37 | enable_admin_cors: true 38 | allow_admin: 39 | - 0.0.0.0/0 40 | admin_listen: 41 | ip: 0.0.0.0 42 | port: 9180 43 | 44 | admin_api_version: v3 45 | 46 | etcd: 47 | host: 48 | - "http://etcd:2379" 49 | timeout: 30 50 | startup_retry: 2 51 | 52 | ext-plugin: 53 | path_for_test: /tmp/runner.sock 54 | nginx_config: 55 | user: root 56 | -------------------------------------------------------------------------------- /ci/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | version: "3" 19 | 20 | services: 21 | apisix: 22 | image: apache/apisix:dev 23 | restart: always 24 | volumes: 25 | - ./apisix/config.yaml:/usr/local/apisix/conf/config.yaml:ro 26 | - /tmp/runner.sock:/tmp/runner.sock 27 | depends_on: 28 | - etcd 29 | ports: 30 | - "9180:9180/tcp" 31 | - "9080:9080/tcp" 32 | - "9091:9091/tcp" 33 | - "9443:9443/tcp" 34 | - "9092:9092/tcp" 35 | networks: 36 | apisix: 37 | 38 | etcd: 39 | image: bitnami/etcd:3.4.9 40 | restart: always 41 | environment: 42 | ETCD_ENABLE_V2: "true" 43 | ALLOW_NONE_AUTHENTICATION: "yes" 44 | ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379" 45 | ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379" 46 | ports: 47 | - "2379:2379/tcp" 48 | networks: 49 | apisix: 50 | 51 | web: 52 | image: mendhak/http-https-echo 53 | environment: 54 | HTTP_PORT: 8888 55 | HTTPS_PORT: 9999 56 | restart: unless-stopped 57 | ports: 58 | - "8888:8888" 59 | - "9999:9999" 60 | networks: 61 | apisix: 62 | 63 | networks: 64 | apisix: 65 | driver: bridge -------------------------------------------------------------------------------- /conf/config.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | socket: 19 | file: $env.APISIX_LISTEN_ADDRESS # Environment variable or absolute path 20 | 21 | logging: 22 | level: warn # error warn info debug 23 | -------------------------------------------------------------------------------- /docs/assets/images/apisix-plugin-runner-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/apisix-python-plugin-runner/250b936f489fc540c465b434d427834448104ac4/docs/assets/images/apisix-plugin-runner-overview.png -------------------------------------------------------------------------------- /docs/en/latest/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 0.2, 3 | "sidebar": [ 4 | { 5 | "type": "doc", 6 | "id": "getting-started" 7 | }, 8 | { 9 | "type": "doc", 10 | "id": "developer-guide" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/en/latest/developer-guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Developer Guide 3 | --- 4 | 5 | 23 | 24 | ## Overview 25 | 26 | This documentation explains how to develop this project. 27 | 28 | ## Prerequisites 29 | 30 | * Python 3.7+ 31 | * APISIX 2.7.0+ 32 | 33 | ## Debug 34 | 35 | - Run `make setup` installation dependencies 36 | - Run `make install` installation runner to system 37 | - Run `make dev` to start it 38 | 39 | ## Plugin 40 | 41 | #### Plugin directory 42 | 43 | ``` 44 | /path/to/apisix-python-plugin-runner/apisix/plugin 45 | ``` 46 | 47 | the `.py` files in this directory autoload 48 | 49 | #### Plugin example 50 | 51 | ``` 52 | /path/to/apisix-python-plugin-runner/apisix/plugin/stop.py 53 | /path/to/apisix-python-plugin-runner/apisix/plugin/rewrite.py 54 | ``` 55 | 56 | #### Plugin Format 57 | 58 | ```python 59 | from typing import Any 60 | from apisix.runner.http.request import Request 61 | from apisix.runner.http.response import Response 62 | from apisix.runner.plugin.core import PluginBase 63 | 64 | 65 | class Test(PluginBase): 66 | 67 | def name(self) -> str: 68 | """ 69 | The name of the plugin registered in the runner 70 | :return: 71 | """ 72 | return "test" 73 | 74 | def config(self, conf: Any) -> Any: 75 | """ 76 | Parse plugin configuration 77 | :param conf: 78 | :return: 79 | """ 80 | return conf 81 | 82 | def filter(self, conf: Any, request: Request, response: Response): 83 | """ 84 | The plugin executes the main function 85 | :param conf: 86 | plugin configuration after parsing 87 | :param request: 88 | request parameters and information 89 | :param response: 90 | response parameters and information 91 | :return: 92 | """ 93 | 94 | # print plugin configuration 95 | print(conf) 96 | 97 | # Fetch request nginx variable `host` 98 | host = request.get_var("host") 99 | print(host) 100 | 101 | # Fetch request body 102 | body = request.get_body() 103 | print(body) 104 | 105 | # Set response headers 106 | response.set_header("X-Resp-A6-Runner", "Python") 107 | 108 | # Set response body 109 | response.set_body("Hello, Python Runner of APISIX") 110 | 111 | # Set response status code 112 | response.set_status_code(201) 113 | ``` 114 | 115 | - Plugins must inherit the `PluginBase` class and implement all functions. 116 | - `name` function: used to set the registered plugin name. 117 | - `config` function: used to parse plugin configuration. 118 | - `filter` function: used to filter requests. 119 | - `conf` parameter: plugin configuration after parsing. 120 | - `request` parameter: Request object, which can be used to get and set request information. 121 | - `response` parameter: Response object, which can be used to set response information. 122 | 123 | ## Test 124 | 125 | Run `make test`. 126 | 127 | ## Data Format 128 | 129 | [FlatBuffers](https://github.com/google/flatbuffers) 130 | 131 | ## Data Protocol 132 | 133 | ``` 134 | 1 byte of type + 3 bytes of length + data 135 | ``` 136 | -------------------------------------------------------------------------------- /docs/en/latest/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | --- 4 | 5 | 23 | 24 | ## Overview 25 | This document explains how to use Python Runner 26 | 27 | ## Prerequisites 28 | 29 | * Python 3.7+ 30 | * APISIX 2.7.0+ 31 | 32 | 33 | ## Installation 34 | 35 | ```bash 36 | $ git clone https://github.com/apache/apisix-python-plugin-runner.git 37 | $ cd apisix-python-plugin-runner 38 | $ make setup 39 | $ make install 40 | ``` 41 | 42 | ## Launch 43 | 44 | ### Configuration Python Runner 45 | 46 | > Development Mode 47 | 48 | #### Run APISIX Python Runner 49 | ```bash 50 | $ cd /path/to/apisix-python-plugin-runner 51 | $ make dev 52 | ``` 53 | 54 | #### Modify APISIX configuration file 55 | ```bash 56 | $ vim /path/to/apisix/conf/config.yaml 57 | apisix: 58 | admin_key: 59 | - name: "admin" 60 | key: edd1c9f034335f136f87ad84b625c8f1 61 | role: admin 62 | ext-plugin: 63 | path_for_test: /tmp/runner.sock 64 | ``` 65 | 66 | > Production Mode 67 | 68 | #### Modify APISIX configuration file 69 | ```bash 70 | $ vim /path/to/apisix/conf/config.yaml 71 | apisix: 72 | admin_key: 73 | - name: "admin" 74 | key: edd1c9f034335f136f87ad84b625c8f1 75 | role: admin 76 | ext-plugin: 77 | cmd: [ "python3", "/path/to/apisix-python-plugin-runner/bin/py-runner", "start" ] 78 | ``` 79 | 80 | ### Log level and socket configuration (Optional) 81 | 82 | ```bash 83 | $ vim /path/to/apisix-python-plugin-runner/conf/config.yaml 84 | socket: 85 | file: $env.APISIX_LISTEN_ADDRESS # Environment variable or absolute path 86 | 87 | logging: 88 | level: debug # error warn info debug 89 | ``` 90 | 91 | ### Start or Restart APISIX 92 | ```bash 93 | $ cd /path/to/apisix 94 | # Start or Restart 95 | $ ./bin/apisix [ start | restart ] 96 | ``` 97 | 98 | ### Configure APISIX Routing Rule 99 | ```bash 100 | $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' 101 | { 102 | "uri": "/get", 103 | "plugins": { 104 | "ext-plugin-pre-req": { 105 | "conf": [ 106 | { "name": "stop", "value":"{\"body\":\"hello\"}"} 107 | ] 108 | } 109 | }, 110 | "upstream": { 111 | "type": "roundrobin", 112 | "nodes": { 113 | "127.0.0.1:1980": 1 114 | } 115 | } 116 | }' 117 | ``` 118 | 119 | 120 | # Testing 121 | ```bash 122 | $ curl http://127.0.0.1:9080/get -i 123 | HTTP/1.1 200 OK 124 | Date: Fri, 13 Aug 2021 13:39:18 GMT 125 | Content-Type: text/plain; charset=utf-8 126 | Transfer-Encoding: chunked 127 | Connection: keep-alive 128 | host: 127.0.0.1:9080 129 | accept: */* 130 | user-agent: curl/7.64.1 131 | X-Resp-A6-Runner: Python 132 | Server: APISIX/2.7 133 | 134 | Hello, Python Runner of APISIX 135 | ``` 136 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | ; 2 | ; Licensed to the Apache Software Foundation (ASF) under one or more 3 | ; contributor license agreements. See the NOTICE file distributed with 4 | ; this work for additional information regarding copyright ownership. 5 | ; The ASF licenses this file to You under the Apache License, Version 2.0 6 | ; (the "License"); you may not use this file except in compliance with 7 | ; the License. You may obtain a copy of the License at 8 | ; 9 | ; http://www.apache.org/licenses/LICENSE-2.0 10 | ; 11 | ; Unless required by applicable law or agreed to in writing, software 12 | ; distributed under the License is distributed on an "AS IS" BASIS, 13 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ; See the License for the specific language governing permissions and 15 | ; limitations under the License. 16 | ; 17 | 18 | [pytest] 19 | addopts = --cov=apisix/runner -p no:warnings 20 | testpaths = tests 21 | python_files = test_* 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | a6pluginprotos==0.2.1 2 | click==8.0.1 3 | minicache==0.0.1 4 | PyYAML==5.4.1 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | from setuptools import setup, find_packages 19 | 20 | __version__ = "0.2.0" 21 | 22 | requirements = open('requirements.txt').readlines() 23 | 24 | setup( 25 | name="apache-apisix", 26 | version=__version__, 27 | description="Python Plugin Runner for Apache APISIX", 28 | url="https://github.com/apache/apisix-python-plugin-runner", 29 | author="Jinchao Shuai", 30 | author_email="dev@apisix.apache.org", 31 | license="Apache 2.0", 32 | python_requires=">=3.7.0", 33 | packages=find_packages(exclude=["tests"]), 34 | install_requires=requirements, 35 | ) 36 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import sys 20 | 21 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 22 | -------------------------------------------------------------------------------- /tests/e2e/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/apache/apisix-python-plugin-runner/tests/e2e 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gavv/httpexpect/v2 v2.3.1 7 | github.com/onsi/ginkgo v1.16.5 8 | github.com/onsi/gomega v1.18.1 9 | ) 10 | -------------------------------------------------------------------------------- /tests/e2e/go.sum: -------------------------------------------------------------------------------- 1 | github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= 2 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 3 | github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= 4 | github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 5 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 6 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 7 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/fasthttp/websocket v1.4.3-rc.6 h1:omHqsl8j+KXpmzRjF8bmzOSYJ8GnS0E3efi1wYT+niY= 12 | github.com/fasthttp/websocket v1.4.3-rc.6/go.mod h1:43W9OM2T8FeXpCWMsBd9Cb7nE2CACNqNvCqQCoty/Lc= 13 | github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU= 14 | github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 15 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 16 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 17 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 18 | github.com/gavv/httpexpect/v2 v2.3.1 h1:sGLlKMn8AuHS9ztK9Sb7AJ7OxIL8v2PcLdyxfKt1Fo4= 19 | github.com/gavv/httpexpect/v2 v2.3.1/go.mod h1:yOE8m/aqFYQDNrgprMeXgq4YynfN9h1NgcE1+1suV64= 20 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 21 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 22 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 23 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 24 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 25 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 26 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 27 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 28 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 29 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 30 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 31 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 32 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 33 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 34 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 35 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 36 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 37 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 38 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 39 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 40 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 41 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 42 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 43 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 44 | github.com/imkira/go-interpol v1.0.0 h1:HrmLyvOLJyjR0YofMw8QGdCIuYOs4TJUBDNU5sJC09E= 45 | github.com/imkira/go-interpol v1.0.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= 46 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= 47 | github.com/klauspost/compress v1.12.2 h1:2KCfW3I9M7nSc5wOqXAlW2v2U6v+w6cbjvbfp+OykW8= 48 | github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 49 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 50 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 51 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 52 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 53 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 54 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 55 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 56 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 57 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 58 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 59 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 60 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 61 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 62 | github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= 63 | github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 64 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 65 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 66 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 67 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 68 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 69 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= 70 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 71 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 72 | github.com/savsgio/gotils v0.0.0-20210617111740-97865ed5a873 h1:N3Af8f13ooDKcIhsmFT7Z05CStZWu4C7Md0uDEy4q6o= 73 | github.com/savsgio/gotils v0.0.0-20210617111740-97865ed5a873/go.mod h1:dmPawKuiAeG/aFYVs2i+Dyosoo7FNcm+Pi8iK6ZUrX8= 74 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 75 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 76 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 77 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 78 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 79 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 80 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 81 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 82 | github.com/valyala/fasthttp v1.27.0 h1:gDefRDL9aqSiwXV6aRW8aSBPs82y4KizSzHrBLf4NDI= 83 | github.com/valyala/fasthttp v1.27.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA= 84 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 85 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 86 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 87 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 88 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 89 | github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg= 90 | github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= 91 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= 92 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= 93 | github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= 94 | github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= 95 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= 96 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= 97 | github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= 98 | github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= 99 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 100 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 101 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 102 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 103 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 104 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 105 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 106 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 107 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 108 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 109 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 110 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 111 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 112 | golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I= 113 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 114 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 118 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 120 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 130 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 132 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 133 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 134 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 135 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 136 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 137 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 138 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 139 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 140 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 141 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 142 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 143 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 144 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 145 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 146 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 147 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 148 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 149 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 150 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 151 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 152 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 153 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 154 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 155 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 156 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 157 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 158 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 159 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 160 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 161 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 162 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 163 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 164 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 165 | moul.io/http2curl v1.0.1-0.20190925090545-5cd742060b0e h1:C7q+e9M5nggAvWfVg9Nl66kebKeuJlP3FD58V4RR5wo= 166 | moul.io/http2curl v1.0.1-0.20190925090545-5cd742060b0e/go.mod h1:nejbQVfXh96n9dSF6cH3Jsk/QI1Z2oEL7sSI2ifXFNA= 167 | -------------------------------------------------------------------------------- /tests/e2e/plugins/plugins_rewrite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package plugins_test 19 | 20 | import ( 21 | "github.com/apache/apisix-python-plugin-runner/tests/e2e/tools" 22 | "github.com/gavv/httpexpect/v2" 23 | "github.com/onsi/ginkgo" 24 | "github.com/onsi/ginkgo/extensions/table" 25 | "net/http" 26 | "time" 27 | ) 28 | 29 | var _ = ginkgo.Describe("Rewrite Plugin", func() { 30 | table.DescribeTable("create rewrite route plugin route and test", 31 | func(tc tools.HttpTestCase) { 32 | tools.RunTestCase(tc) 33 | }, 34 | table.Entry("create python runner rewrite plugin route success", tools.HttpTestCase{ 35 | Object: tools.PutA6Conf(), 36 | Method: http.MethodPut, 37 | Path: "/apisix/admin/routes/1", 38 | Body: `{ 39 | "uri":"/test/python/runner/rewrite", 40 | "plugins":{ 41 | "ext-plugin-pre-req":{ 42 | "conf":[ 43 | { 44 | "name":"rewrite", 45 | "value":"rewrite-config" 46 | } 47 | ] 48 | } 49 | }, 50 | "upstream":{ 51 | "nodes":{ 52 | "web:8888":1 53 | }, 54 | "type":"roundrobin" 55 | } 56 | }`, 57 | Headers: map[string]string{"X-API-KEY": tools.GetAdminToken()}, 58 | ExpectStatusRange: httpexpect.Status2xx, 59 | Sleep: time.Duration(1000), 60 | }), 61 | table.Entry("test python runner rewrite plugin route success", tools.HttpTestCase{ 62 | Object: tools.GetA6Expect(), 63 | Method: http.MethodGet, 64 | Path: "/test/python/runner/rewrite", 65 | ExpectBody: []string{"/a6/python/runner", "Python"}, 66 | ExpectStatus: http.StatusOK, 67 | }), 68 | ) 69 | }) 70 | -------------------------------------------------------------------------------- /tests/e2e/plugins/plugins_stop_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package plugins_test 19 | 20 | import ( 21 | "github.com/apache/apisix-python-plugin-runner/tests/e2e/tools" 22 | "github.com/gavv/httpexpect/v2" 23 | "github.com/onsi/ginkgo" 24 | "github.com/onsi/ginkgo/extensions/table" 25 | "net/http" 26 | "time" 27 | ) 28 | 29 | var _ = ginkgo.Describe("Stop Plugin", func() { 30 | table.DescribeTable("create stop route plugin route and test", 31 | func(tc tools.HttpTestCase) { 32 | tools.RunTestCase(tc) 33 | }, 34 | table.Entry("create python runner stop plugin route success", tools.HttpTestCase{ 35 | Object: tools.PutA6Conf(), 36 | Method: http.MethodPut, 37 | Path: "/apisix/admin/routes/1", 38 | Body: `{ 39 | "uri":"/test/python/runner/stop", 40 | "plugins":{ 41 | "ext-plugin-pre-req":{ 42 | "conf":[ 43 | { 44 | "name":"stop", 45 | "value":"stop-config" 46 | } 47 | ] 48 | } 49 | }, 50 | "upstream":{ 51 | "nodes":{ 52 | "web:8888":1 53 | }, 54 | "type":"roundrobin" 55 | } 56 | }`, 57 | Headers: map[string]string{"X-API-KEY": tools.GetAdminToken()}, 58 | ExpectStatusRange: httpexpect.Status2xx, 59 | Sleep: time.Duration(1000), 60 | }), 61 | table.Entry("test python runner stop plugin route success", tools.HttpTestCase{ 62 | Object: tools.GetA6Expect(), 63 | Method: http.MethodGet, 64 | Path: "/test/python/runner/stop", 65 | ExpectStatus: http.StatusCreated, 66 | ExpectHeaders: map[string]string{ 67 | "X-Resp-A6-Runner": "Python", 68 | }, 69 | ExpectBody: "Hello, Python Runner of APISIX", 70 | }), 71 | ) 72 | }) 73 | -------------------------------------------------------------------------------- /tests/e2e/plugins/plugins_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package plugins_test 19 | 20 | import ( 21 | "github.com/onsi/ginkgo" 22 | "github.com/onsi/gomega" 23 | "testing" 24 | ) 25 | 26 | func TestPlugins(t *testing.T) { 27 | gomega.RegisterFailHandler(ginkgo.Fail) 28 | ginkgo.RunSpecs(t, "Plugins Suite") 29 | } 30 | -------------------------------------------------------------------------------- /tests/e2e/tools/tools.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package tools 19 | 20 | import ( 21 | "github.com/gavv/httpexpect/v2" 22 | "github.com/onsi/ginkgo" 23 | "net/http" 24 | "strings" 25 | "time" 26 | ) 27 | 28 | var ( 29 | token = "edd1c9f034335f136f87ad84b625c8f1" 30 | A6_CP_Host = "http://127.0.0.1:9180" 31 | A6_DP_Host = "http://127.0.0.1:9080" 32 | ) 33 | 34 | func GetAdminToken() string { 35 | return token 36 | } 37 | 38 | func PutA6Conf() *httpexpect.Expect { 39 | t := ginkgo.GinkgoT() 40 | return httpexpect.New(t, A6_CP_Host) 41 | } 42 | 43 | func GetA6Expect() *httpexpect.Expect { 44 | t := ginkgo.GinkgoT() 45 | return httpexpect.New(t, A6_DP_Host) 46 | } 47 | 48 | type HttpTestCase struct { 49 | Object *httpexpect.Expect 50 | Method string 51 | Path string 52 | Query string 53 | Body string 54 | Headers map[string]string 55 | ExpectStatus int 56 | ExpectStatusRange httpexpect.StatusRange 57 | ExpectCode int 58 | ExpectBody interface{} 59 | ExpectHeaders map[string]string 60 | Sleep time.Duration //ms 61 | } 62 | 63 | func RunTestCase(htc HttpTestCase) { 64 | var req *httpexpect.Request 65 | expect := htc.Object 66 | switch htc.Method { 67 | case http.MethodGet: 68 | req = expect.GET(htc.Path) 69 | case http.MethodPost: 70 | req = expect.POST(htc.Path) 71 | case http.MethodPut: 72 | req = expect.PUT(htc.Path) 73 | case http.MethodDelete: 74 | req = expect.DELETE(htc.Path) 75 | case http.MethodOptions: 76 | req = expect.OPTIONS(htc.Path) 77 | default: 78 | } 79 | 80 | if req == nil { 81 | panic("init request failed") 82 | } 83 | 84 | if htc.Sleep == 0 { 85 | time.Sleep(time.Duration(100) * time.Millisecond) 86 | } else { 87 | time.Sleep(htc.Sleep) 88 | } 89 | 90 | if len(htc.Query) > 0 { 91 | req.WithQueryString(htc.Query) 92 | } 93 | 94 | setContentType := false 95 | for hk, hv := range htc.Headers { 96 | req.WithHeader(hk, hv) 97 | if strings.ToLower(hk) == "content-type" { 98 | setContentType = true 99 | } 100 | } 101 | 102 | if !setContentType { 103 | req.WithHeader("Content-Type", "application/json") 104 | } 105 | 106 | if len(htc.Body) > 0 { 107 | req.WithText(htc.Body) 108 | } 109 | 110 | resp := req.Expect() 111 | 112 | if htc.ExpectStatus != 0 { 113 | resp.Status(htc.ExpectStatus) 114 | } 115 | 116 | if htc.ExpectStatusRange > 0 { 117 | resp.StatusRange(htc.ExpectStatusRange) 118 | } 119 | 120 | if htc.ExpectHeaders != nil { 121 | for hk, hv := range htc.ExpectHeaders { 122 | resp.Header(hk).Equal(hv) 123 | } 124 | } 125 | 126 | if htc.ExpectBody != nil { 127 | if body, ok := htc.ExpectBody.(string); ok { 128 | if len(body) == 0 { 129 | resp.Body().Empty() 130 | } else { 131 | resp.Body().Contains(body) 132 | } 133 | } 134 | 135 | if bodies, ok := htc.ExpectBody.([]string); ok && len(bodies) > 0 { 136 | for _, b := range bodies { 137 | resp.Body().Contains(b) 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/runner/http/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | -------------------------------------------------------------------------------- /tests/runner/http/test_request.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import socket 19 | import logging 20 | import apisix.runner.utils.common as runner_utils 21 | from apisix.runner.server.logger import Logger as RunnerServerLogger 22 | from apisix.runner.server.server import RPCRequest as RunnerRPCRequest 23 | from apisix.runner.http.request import Request as RunnerHttpRequest 24 | 25 | 26 | def default_request(): 27 | sock = socket.socket() 28 | logger = RunnerServerLogger(logging.INFO) 29 | return RunnerRPCRequest(sock, logger) 30 | 31 | 32 | def test_request_unknown_handler(): 33 | builder = runner_utils.new_builder() 34 | r = default_request() 35 | req = RunnerHttpRequest(r) 36 | ok = req.unknown_handler(builder) 37 | assert ok 38 | 39 | 40 | def test_request_config_handler(): 41 | builder = runner_utils.new_builder() 42 | 43 | r = default_request() 44 | req = RunnerHttpRequest(r) 45 | req.set_conf_token(0) 46 | ok = req.config_handler(builder) 47 | assert not ok 48 | req.set_conf_token(1) 49 | ok = req.config_handler(builder) 50 | assert ok 51 | 52 | 53 | def test_request_call_handler(): 54 | builder = runner_utils.new_builder() 55 | r = default_request() 56 | req = RunnerHttpRequest(r) 57 | req.set_uri("") 58 | req.set_headers({}) 59 | req.set_args({}) 60 | ok = req.call_handler(builder) 61 | assert not ok 62 | req.set_header("X-Hello", "World") 63 | req.set_id(1) 64 | ok = req.call_handler(builder) 65 | assert ok 66 | req.set_uri("/hello") 67 | req.set_id(1) 68 | ok = req.call_handler(builder) 69 | assert ok 70 | 71 | 72 | def test_request_handler(): 73 | default_key = "hello" 74 | default_val = "world" 75 | default_empty_str = "" 76 | default_empty_dict = {} 77 | default_id = 1000 78 | default_token = 1 79 | default_uri = "/hello" 80 | default_method = "GET" 81 | default_ip = "127.0.0.1" 82 | 83 | r = default_request() 84 | req = RunnerHttpRequest(r) 85 | 86 | assert not req.set_id(0) 87 | assert req.set_id(default_id) 88 | assert req.get_id() == default_id 89 | 90 | assert not req.set_conf_token(0) 91 | assert req.set_conf_token(default_token) 92 | assert req.get_conf_token() == default_token 93 | 94 | assert not req.set_method(default_key) 95 | assert req.set_method(default_method) 96 | assert req.get_method() == default_method 97 | 98 | assert not req.set_uri(default_key) 99 | assert req.set_uri(default_uri) 100 | assert req.get_uri() == default_uri 101 | 102 | assert not req.set_header(default_key, default_empty_str) 103 | assert req.set_header(default_key, default_val) 104 | assert req.get_header(default_key) == default_val 105 | 106 | assert not req.set_headers(default_empty_dict) 107 | assert req.set_headers({default_key: default_val}) 108 | assert req.get_headers() == {default_key: default_val} 109 | 110 | assert not req.set_config(default_empty_str, default_empty_str) 111 | assert req.set_config(default_key, default_empty_str) 112 | assert req.set_config(default_key, default_val) 113 | assert req.get_config(default_key) == default_val 114 | 115 | assert not req.set_configs(default_empty_dict) 116 | assert req.set_configs({default_key: default_val}) 117 | assert req.get_configs() == {default_key: default_val} 118 | 119 | assert not req.set_arg(default_key, default_empty_str) 120 | assert req.set_arg(default_key, default_val) 121 | assert req.get_arg(default_key) == default_val 122 | 123 | assert not req.set_args(default_empty_dict) 124 | assert req.set_args({default_key: default_val}) 125 | assert req.get_args() == {default_key: default_val} 126 | 127 | assert not req.set_remote_addr(default_empty_str) 128 | assert req.set_remote_addr(default_ip) 129 | assert req.get_remote_addr() == default_ip 130 | 131 | assert not req.set_body(default_empty_str) 132 | assert req.get_body() == default_empty_str 133 | assert req.set_body(default_val) 134 | assert req.get_body() == default_val 135 | 136 | assert not req.set_var(default_key, default_empty_str) 137 | assert req.get_var(default_key) == default_empty_str 138 | assert req.set_var(default_key, default_val) 139 | assert req.get_var(default_key) == default_val 140 | -------------------------------------------------------------------------------- /tests/runner/http/test_response.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import apisix.runner.utils.common as runner_utils 19 | from apisix.runner.http.response import Response as RunnerHttpResponse 20 | 21 | 22 | def test_response_call_handler(): 23 | builder = runner_utils.new_builder() 24 | resp = RunnerHttpResponse() 25 | resp.set_req_id(1) 26 | ok = resp.call_handler(builder) 27 | assert not ok 28 | resp.set_body("Hello Python Runner") 29 | ok = resp.call_handler(builder) 30 | assert ok 31 | 32 | 33 | def test_response_handler(): 34 | default_key = "hello" 35 | default_val = "world" 36 | default_empty_str = "" 37 | default_empty_dict = {} 38 | default_num_zero = 0 39 | default_id = 1000 40 | default_code = 200 41 | 42 | resp = RunnerHttpResponse() 43 | 44 | assert not resp.set_header(default_key, default_empty_str) 45 | assert resp.set_header(default_key, default_val) 46 | assert resp.get_header(default_key) == default_val 47 | 48 | assert not resp.set_headers(default_empty_dict) 49 | assert resp.set_headers({default_key: default_val}) 50 | assert resp.get_headers() == {default_key: default_val} 51 | 52 | assert not resp.set_body(default_empty_str) 53 | assert resp.set_body(default_val) 54 | assert resp.get_body() == default_val 55 | 56 | assert not resp.set_req_id(0) 57 | assert resp.set_req_id(default_id) 58 | assert resp.get_req_id() == default_id 59 | 60 | assert not resp.set_status_code(default_num_zero) 61 | assert resp.set_status_code(default_code) 62 | assert resp.get_status_code() == default_code 63 | -------------------------------------------------------------------------------- /tests/runner/plugin/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | -------------------------------------------------------------------------------- /tests/runner/plugin/test_cache.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | from apisix.runner.plugin.cache import generate_token 19 | from apisix.runner.plugin.cache import get_config_by_token 20 | from apisix.runner.plugin.cache import set_config_by_token 21 | 22 | 23 | def test_cache(): 24 | cache_config = {"hello": "world"} 25 | token = generate_token() 26 | config = get_config_by_token(token) 27 | assert not config 28 | ok = set_config_by_token(token, cache_config) 29 | assert ok 30 | config = get_config_by_token(token) 31 | assert config == cache_config 32 | 33 | 34 | def test_generate_token(): 35 | token = generate_token() 36 | assert token 37 | 38 | 39 | def test_set_config_by_token(): 40 | ok = set_config_by_token(1, {}) 41 | assert not ok 42 | ok = set_config_by_token(1, {"q": "hello"}) 43 | assert ok 44 | 45 | 46 | def test_get_config_by_token(): 47 | token = 1 48 | data = {"q": "hello"} 49 | ok = set_config_by_token(token, data) 50 | assert ok 51 | d = get_config_by_token(token) 52 | assert d == data 53 | -------------------------------------------------------------------------------- /tests/runner/plugin/test_core.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import socket 19 | import logging 20 | 21 | from apisix.runner.plugin.core import PluginBase 22 | from apisix.runner.plugin.core import PluginProcess 23 | from apisix.runner.plugin.core import PLUGINS 24 | from apisix.runner.server.logger import Logger as RunnerServerLogger 25 | from apisix.runner.server.server import RPCRequest as RunnerRPCRequest 26 | from apisix.runner.http.request import Request as NewHttpRequest 27 | from apisix.runner.http.response import Response as NewHttpResponse 28 | 29 | 30 | class NonePlugin: 31 | pass 32 | 33 | 34 | class ErrorPlugin: 35 | 36 | def config(self, conf): 37 | return conf 38 | 39 | def filter(self, conf, req, reps): 40 | raise RuntimeError("Runtime Error") 41 | 42 | 43 | def default_request(): 44 | sock = socket.socket() 45 | logger = RunnerServerLogger(logging.INFO) 46 | return RunnerRPCRequest(sock, logger) 47 | 48 | 49 | def test_process_register(): 50 | assert PLUGINS == {} 51 | PluginProcess.register() 52 | assert len(PLUGINS) == 2 53 | 54 | 55 | def test_process_execute(): 56 | r = default_request() 57 | request = NewHttpRequest(r) 58 | response = NewHttpResponse() 59 | tests = [ 60 | { 61 | "conf": { 62 | "stop": "config" 63 | }, 64 | "autoload": True, 65 | "req": request, 66 | "resp": response, 67 | "expected": True 68 | }, 69 | { 70 | "conf": { 71 | "rewrite": "config" 72 | }, 73 | "autoload": True, 74 | "req": request, 75 | "resp": response, 76 | "expected": True 77 | }, 78 | # AnyError 79 | { 80 | "conf": { 81 | "any": "config" 82 | }, 83 | "plugins": { 84 | "any": ErrorPlugin 85 | }, 86 | "autoload": False, 87 | "expected": False 88 | }, 89 | # AttributeError 90 | { 91 | "conf": { 92 | "attr": "config" 93 | }, 94 | "plugins": { 95 | "attr": NonePlugin 96 | }, 97 | "autoload": False, 98 | "expected": False 99 | }, 100 | # TypeError 101 | { 102 | "conf": { 103 | "none": "config" 104 | }, 105 | "autoload": True, 106 | "expected": False 107 | }, 108 | ] 109 | 110 | for test in tests: 111 | if test.get("autoload"): 112 | PluginProcess.register() 113 | if test.get("plugins"): 114 | for plg_name, plg_obj in test.get("plugins").items(): 115 | PLUGINS[plg_name] = plg_obj 116 | res = PluginProcess.execute(test.get("conf"), r, test.get("req"), test.get("resp")) 117 | assert res == test.get("expected") 118 | 119 | 120 | def test_base(): 121 | r = default_request() 122 | pb = PluginBase() 123 | assert pb.name() is None 124 | assert pb.config(None) is None 125 | assert pb.filter(None, NewHttpRequest(r), NewHttpResponse()) is None 126 | -------------------------------------------------------------------------------- /tests/runner/server/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | -------------------------------------------------------------------------------- /tests/runner/server/test_config.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import logging 20 | from apisix.runner.server.config import Config as NewServerConfig 21 | 22 | 23 | def test_config_default(): 24 | config = NewServerConfig() 25 | 26 | config.logging.level = "INFO" 27 | assert config.logging.level == logging.INFO 28 | 29 | config.logging.level = "ERROR" 30 | assert config.logging.level == logging.ERROR 31 | 32 | config.logging.level = "WARN" 33 | assert config.logging.level == logging.WARNING 34 | 35 | config.logging.level = "NOTSET" 36 | assert config.logging.level == logging.NOTSET 37 | 38 | config.socket.file = "/test/runner.sock" 39 | assert config.socket.file == "/test/runner.sock" 40 | 41 | 42 | def test_config_custom(): 43 | config = NewServerConfig("%s" % os.path.abspath(os.path.join(os.getcwd())), "config.yaml") 44 | 45 | config.logging.level = "NOTSET" 46 | assert config.logging.level == logging.NOTSET 47 | 48 | config.logging.level = "INFO" 49 | assert config.logging.level == logging.INFO 50 | 51 | config.logging.level = "ERROR" 52 | assert config.logging.level == logging.ERROR 53 | 54 | config.logging.level = "WARN" 55 | assert config.logging.level == logging.WARNING 56 | 57 | config.socket.file = "/test/runner.sock" 58 | assert config.socket.file == "/test/runner.sock" 59 | -------------------------------------------------------------------------------- /tests/runner/server/test_handle.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import logging 18 | import socket 19 | 20 | import apisix.runner.utils.common as runner_utils 21 | from apisix.runner.server.handle import Handle as RunnerServerHandle 22 | from apisix.runner.server.logger import Logger as RunnerServerLogger 23 | from apisix.runner.server.server import RPCRequest as RunnerRPCRequest 24 | from A6.HTTPReqCall import Req as A6HTTPReqCallReq 25 | from A6.PrepareConf import Req as A6PrepareConfReq 26 | from A6.PrepareConf import Resp as A6PrepareConfResp 27 | from A6 import TextEntry as A6TextEntry 28 | from A6 import Method as A6Method 29 | from A6.Err.Resp import Resp as ErrResp 30 | from A6.HTTPReqCall.Resp import Resp as HCResp 31 | from A6.HTTPReqCall.Action import Action as HCAction 32 | from A6.Err.Code import Code as ErrCode 33 | from A6.HTTPReqCall.Stop import Stop as HCStop 34 | from A6.HTTPReqCall.Rewrite import Rewrite as HCRewrite 35 | 36 | 37 | def default_request(): 38 | sock = socket.socket() 39 | logger = RunnerServerLogger(logging.INFO) 40 | return RunnerRPCRequest(sock, logger) 41 | 42 | 43 | def default_plugin_buffer(name: str = "stop", enable_conf: bool = True): 44 | builder = runner_utils.new_builder() 45 | conf = 0 46 | if enable_conf: 47 | name = builder.CreateString(name) 48 | value = builder.CreateString('{"body":"Hello Python Runner"}') 49 | A6TextEntry.Start(builder) 50 | A6TextEntry.AddName(builder, name) 51 | A6TextEntry.AddValue(builder, value) 52 | conf_data = A6TextEntry.End(builder) 53 | 54 | A6PrepareConfReq.ReqStartConfVector(builder, 1) 55 | builder.PrependUOffsetTRelative(conf_data) 56 | conf = builder.EndVector() 57 | 58 | A6PrepareConfReq.Start(builder) 59 | A6PrepareConfReq.AddConf(builder, conf) 60 | req = A6PrepareConfReq.End(builder) 61 | builder.Finish(req) 62 | return builder.Output() 63 | 64 | 65 | def default_call_buffer(token: int = 0, id: int = 1, ipv6: bool = False): 66 | builder = runner_utils.new_builder() 67 | # request path 68 | path = builder.CreateString("/hello/python/runner") 69 | # request ip 70 | if ipv6: 71 | src_ip = bytes(bytearray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])) 72 | else: 73 | src_ip = bytes(bytearray([127, 0, 0, 1])) 74 | src_ip = builder.CreateByteVector(src_ip) 75 | # request args 76 | arg_k = builder.CreateString("hello") 77 | arg_v = builder.CreateString("world") 78 | A6TextEntry.Start(builder) 79 | A6TextEntry.AddName(builder, arg_k) 80 | A6TextEntry.AddValue(builder, arg_v) 81 | args = A6TextEntry.End(builder) 82 | A6HTTPReqCallReq.StartArgsVector(builder, 1) 83 | builder.PrependUOffsetTRelative(args) 84 | args_vec = builder.EndVector() 85 | # request headers 86 | head_k = builder.CreateString("hello") 87 | head_v = builder.CreateString("world") 88 | A6TextEntry.Start(builder) 89 | A6TextEntry.AddName(builder, head_k) 90 | A6TextEntry.AddValue(builder, head_v) 91 | headers = A6TextEntry.End(builder) 92 | A6HTTPReqCallReq.StartHeadersVector(builder, 1) 93 | builder.PrependUOffsetTRelative(headers) 94 | headers_vec = builder.EndVector() 95 | 96 | A6HTTPReqCallReq.Start(builder) 97 | A6HTTPReqCallReq.AddId(builder, id) 98 | A6HTTPReqCallReq.AddMethod(builder, A6Method.Method.GET) 99 | A6HTTPReqCallReq.AddPath(builder, path) 100 | A6HTTPReqCallReq.AddSrcIp(builder, src_ip) 101 | A6HTTPReqCallReq.AddArgs(builder, args_vec) 102 | A6HTTPReqCallReq.AddHeaders(builder, headers_vec) 103 | A6HTTPReqCallReq.AddConfToken(builder, token) 104 | req = A6HTTPReqCallReq.End(builder) 105 | builder.Finish(req) 106 | return builder.Output() 107 | 108 | 109 | def test_dispatch_unknown(): 110 | r = default_request() 111 | r.request.ty = runner_utils.RPC_UNKNOWN 112 | handle = RunnerServerHandle(r) 113 | response = handle.dispatch() 114 | err = ErrResp.GetRootAsResp(response.Output()) 115 | assert err.Code() == ErrCode.BAD_REQUEST 116 | 117 | 118 | def test_dispatch_config(): 119 | buf = default_plugin_buffer("stop", False) 120 | r = default_request() 121 | r.request.ty = runner_utils.RPC_PREPARE_CONF 122 | r.request.data = buf 123 | handle = RunnerServerHandle(r) 124 | response = handle.dispatch() 125 | err = ErrResp.GetRootAsResp(response.Output()) 126 | assert err.Code() == ErrCode.BAD_REQUEST 127 | 128 | buf = default_plugin_buffer("stop") 129 | r.request.ty = runner_utils.RPC_PREPARE_CONF 130 | r.request.data = buf 131 | handle = RunnerServerHandle(r) 132 | response = handle.dispatch() 133 | resp = A6PrepareConfResp.Resp.GetRootAs(response.Output()) 134 | assert resp.ConfToken() != 0 135 | 136 | 137 | def test_dispatch_call(): 138 | r = default_request() 139 | r.request.ty = runner_utils.RPC_PREPARE_CONF 140 | r.request.data = default_plugin_buffer("stop") 141 | handle = RunnerServerHandle(r) 142 | response = handle.dispatch() 143 | resp = A6PrepareConfResp.Resp.GetRootAs(response.Output()) 144 | assert resp.ConfToken() != 0 145 | 146 | buf = default_call_buffer(resp.ConfToken()) 147 | r.request.ty = runner_utils.RPC_HTTP_REQ_CALL 148 | r.request.data = buf 149 | handle = RunnerServerHandle(r) 150 | response = handle.dispatch() 151 | resp = HCResp.GetRootAsResp(response.Output()) 152 | assert resp.Id() > 0 153 | assert resp.ActionType() == HCAction.Stop 154 | stop = HCStop() 155 | stop.Init(resp.Action().Bytes, resp.Action().Pos) 156 | assert stop.BodyLength() == len("Hello, Python Runner of APISIX") 157 | assert stop.Status() == 201 158 | 159 | r.request.ty = runner_utils.RPC_PREPARE_CONF 160 | r.request.data = default_plugin_buffer("rewrite") 161 | handle = RunnerServerHandle(r) 162 | response = handle.dispatch() 163 | resp = A6PrepareConfResp.Resp.GetRootAs(response.Output()) 164 | assert resp.ConfToken() != 0 165 | conf_token = resp.ConfToken() 166 | r.request.ty = runner_utils.RPC_HTTP_REQ_CALL 167 | r.request.data = default_call_buffer(conf_token) 168 | handle = RunnerServerHandle(r) 169 | response = handle.dispatch() 170 | resp = HCResp.GetRootAsResp(response.Output()) 171 | assert resp.Id() > 0 172 | assert resp.ActionType() == HCAction.Rewrite 173 | rewrite = HCRewrite() 174 | rewrite.Init(resp.Action().Bytes, resp.Action().Pos) 175 | assert rewrite.Path() == b'/a6/python/runner' 176 | 177 | r.request.data = default_call_buffer(conf_token, ipv6=True) 178 | handle = RunnerServerHandle(r) 179 | response = handle.dispatch() 180 | resp = HCResp.GetRootAsResp(response.Output()) 181 | assert resp.Id() > 0 182 | assert resp.ActionType() == HCAction.Rewrite 183 | 184 | r.request.ty = runner_utils.RPC_HTTP_REQ_CALL 185 | r.request.data = default_call_buffer(conf_token, 0) 186 | handle = RunnerServerHandle(r) 187 | response = handle.dispatch() 188 | resp = ErrResp.GetRootAs(response.Output()) 189 | assert resp.Code() == ErrCode.BAD_REQUEST 190 | 191 | r.request.data = default_call_buffer() 192 | handle = RunnerServerHandle(r) 193 | response = handle.dispatch() 194 | reps = ErrResp.GetRootAs(response.Output()) 195 | assert reps.Code() == ErrCode.BAD_REQUEST 196 | -------------------------------------------------------------------------------- /tests/runner/server/test_logger.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import logging 19 | 20 | from apisix.runner.server.logger import Logger as NewServerLogger 21 | 22 | 23 | def test_logger(capsys): 24 | logger = NewServerLogger(logging.DEBUG) 25 | logger.error("test error log") 26 | logger.warn("test warn log") 27 | logger.info("test info log") 28 | logger.debug("test debug log") 29 | captured = capsys.readouterr() 30 | assert captured.out.find("test error log") != -1 31 | assert captured.out.find("test warn log") != -1 32 | assert captured.out.find("test info log") != -1 33 | assert captured.out.find("test debug log") != -1 34 | -------------------------------------------------------------------------------- /tests/runner/server/test_protocol.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import apisix.runner.utils.common as runner_utils 19 | from apisix.runner.server.protocol import Protocol as NewServerProtocol 20 | from apisix.runner.server.response import RESP_STATUS_CODE_OK 21 | from apisix.runner.server.response import RESP_STATUS_MESSAGE_OK 22 | 23 | 24 | def test_protocol_encode(): 25 | buf_str = "Hello Python Runner".encode() 26 | protocol = NewServerProtocol(buffer=buf_str, ty=runner_utils.RPC_PREPARE_CONF) 27 | err = protocol.encode() 28 | buf_len = len(buf_str) 29 | buf_arr = bytearray(buf_len.to_bytes(4, byteorder="big")) 30 | buf_arr[0] = runner_utils.RPC_PREPARE_CONF 31 | buf_data = bytes(buf_arr) + buf_str 32 | buf_len = len(buf_data) 33 | assert err.code == RESP_STATUS_CODE_OK 34 | assert err.message == RESP_STATUS_MESSAGE_OK 35 | assert protocol.type == runner_utils.RPC_PREPARE_CONF 36 | assert protocol.buffer == buf_data 37 | assert protocol.length == buf_len 38 | 39 | 40 | def test_protocol_decode(): 41 | buf_str = "Hello Python Runner".encode() 42 | buf_len = len(buf_str) 43 | buf_arr = bytearray(buf_len.to_bytes(4, byteorder="big")) 44 | buf_arr[0] = runner_utils.RPC_PREPARE_CONF 45 | buf_data = bytes(buf_arr) 46 | protocol = NewServerProtocol(buffer=buf_data) 47 | err = protocol.decode() 48 | assert err.code == RESP_STATUS_CODE_OK 49 | assert err.message == RESP_STATUS_MESSAGE_OK 50 | assert protocol.type == runner_utils.RPC_PREPARE_CONF 51 | assert protocol.length == buf_len 52 | -------------------------------------------------------------------------------- /tests/runner/server/test_response.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import apisix.runner.utils.common as runner_utils 19 | from apisix.runner.server.response import Response as NewServerResponse 20 | from apisix.runner.server.response import RESP_STATUS_CODE_BAD_REQUEST 21 | from apisix.runner.server.response import RESP_STATUS_CODE_SERVICE_UNAVAILABLE 22 | from apisix.runner.server.response import RESP_STATUS_CODE_CONF_TOKEN_NOT_FOUND 23 | from apisix.runner.server.response import RESP_STATUS_CODE_OK 24 | 25 | 26 | def test_response_code(): 27 | resp = NewServerResponse(code=RESP_STATUS_CODE_OK) 28 | assert resp.code == RESP_STATUS_CODE_OK 29 | resp = NewServerResponse(code=RESP_STATUS_CODE_BAD_REQUEST) 30 | assert resp.code == RESP_STATUS_CODE_BAD_REQUEST 31 | resp = NewServerResponse(code=RESP_STATUS_CODE_SERVICE_UNAVAILABLE) 32 | assert resp.code == RESP_STATUS_CODE_SERVICE_UNAVAILABLE 33 | resp = NewServerResponse(code=RESP_STATUS_CODE_CONF_TOKEN_NOT_FOUND) 34 | assert resp.code == RESP_STATUS_CODE_CONF_TOKEN_NOT_FOUND 35 | 36 | 37 | def test_response_message(): 38 | response = NewServerResponse(message="Hello Python Runner") 39 | assert response.message == "Hello Python Runner" 40 | 41 | 42 | def test_response_data(): 43 | response = NewServerResponse(data="Hello Python Runner".encode()) 44 | assert response.data == b'Hello Python Runner' 45 | 46 | 47 | def test_response_type(): 48 | response = NewServerResponse(ty=runner_utils.RPC_UNKNOWN) 49 | assert response.type == runner_utils.RPC_UNKNOWN 50 | response = NewServerResponse(ty=runner_utils.RPC_PREPARE_CONF) 51 | assert response.type == runner_utils.RPC_PREPARE_CONF 52 | response = NewServerResponse(ty=runner_utils.RPC_HTTP_REQ_CALL) 53 | assert response.type == runner_utils.RPC_HTTP_REQ_CALL 54 | 55 | 56 | def test_response_eq(): 57 | resp1 = NewServerResponse(code=RESP_STATUS_CODE_OK, message="Hello Python Runner", 58 | data="Hello Python Runner".encode(), ty=runner_utils.RPC_PREPARE_CONF) 59 | resp2 = NewServerResponse(code=RESP_STATUS_CODE_BAD_REQUEST, message="Hello Python Runner", 60 | data="Hello Python Runner".encode(), ty=runner_utils.RPC_PREPARE_CONF) 61 | resp3 = NewServerResponse(code=RESP_STATUS_CODE_OK, message="Hello Python Runner", 62 | data="Hello Python Runner".encode(), ty=runner_utils.RPC_PREPARE_CONF) 63 | assert resp1 != resp2 64 | assert resp1 == resp3 65 | -------------------------------------------------------------------------------- /tests/runner/server/test_server.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import socket 19 | import logging 20 | from apisix.runner.server.server import Server as RunnerServer 21 | from apisix.runner.server.server import RPCRequest as RunnerRPCRequest 22 | from apisix.runner.server.logger import Logger as RunnerServerLogger 23 | from apisix.runner.server.config import Config as RunnerConfig 24 | 25 | 26 | def test_server(capsys): 27 | config = RunnerConfig() 28 | server = RunnerServer(config) 29 | del server 30 | captured = capsys.readouterr() 31 | assert captured.out.find("listening on unix") != -1 32 | assert captured.out.find("Bye") != -1 33 | 34 | 35 | def test_rpc_request(): 36 | sock = socket.socket() 37 | logger = RunnerServerLogger(logging.INFO) 38 | r = RunnerRPCRequest(sock, logger) 39 | assert r.log == logger 40 | assert r.conn == sock 41 | assert r.request.ty == 0 42 | assert len(r.request.data) == 0 43 | -------------------------------------------------------------------------------- /tests/runner/utils/test_common.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import flatbuffers 19 | import apisix.runner.utils.common as runner_utils 20 | from apisix.runner.utils.common import VECTOR_TYPE_HEADER 21 | from apisix.runner.utils.common import VECTOR_TYPE_QUERY 22 | from A6.HTTPReqCall import Action as HCAction 23 | from A6.HTTPReqCall import Req as A6HTTPReqCallReq 24 | from A6 import TextEntry as A6TextEntry 25 | 26 | 27 | def get_request_buffer(): 28 | builder = runner_utils.new_builder() 29 | # request path 30 | path = builder.CreateString("/hello/python/runner") 31 | # request ip 32 | src_ip = builder.CreateByteVector(bytes([127, 0, 0, 1])) 33 | 34 | # request headers 35 | head_k = builder.CreateString("hello") 36 | head_v = builder.CreateString("world") 37 | A6TextEntry.Start(builder) 38 | A6TextEntry.AddName(builder, head_k) 39 | A6TextEntry.AddValue(builder, head_v) 40 | headers = A6TextEntry.End(builder) 41 | A6HTTPReqCallReq.StartHeadersVector(builder, 1) 42 | builder.PrependUOffsetTRelative(headers) 43 | headers_vec = builder.EndVector() 44 | 45 | A6HTTPReqCallReq.Start(builder) 46 | A6HTTPReqCallReq.AddId(builder, 1) 47 | A6HTTPReqCallReq.AddPath(builder, path) 48 | A6HTTPReqCallReq.AddSrcIp(builder, src_ip) 49 | A6HTTPReqCallReq.AddHeaders(builder, headers_vec) 50 | req = A6HTTPReqCallReq.End(builder) 51 | builder.Finish(req) 52 | return builder.Output() 53 | 54 | 55 | def test_get_method_code_by_name(): 56 | for name in runner_utils.methodCodes: 57 | assert runner_utils.get_method_code_by_name(name) == runner_utils.methodCodes.get(name) 58 | 59 | 60 | def test_get_method_name_by_code(): 61 | for code in runner_utils.methodNames: 62 | assert runner_utils.get_method_name_by_code(code) == runner_utils.methodNames.get(code) 63 | 64 | 65 | def test_new_builder(): 66 | builder = runner_utils.new_builder() 67 | assert isinstance(builder, flatbuffers.Builder) 68 | assert builder.Bytes == flatbuffers.Builder(256).Bytes 69 | assert builder.Bytes != flatbuffers.Builder(512).Bytes 70 | 71 | 72 | def test_create_dict_entry(): 73 | builder = runner_utils.new_builder() 74 | entries = runner_utils.create_dict_entry(builder, {}) 75 | assert not entries 76 | examples = {"q": "hello", "a": "world"} 77 | entries = runner_utils.create_dict_entry(builder, examples) 78 | assert len(entries) == 2 79 | 80 | 81 | def test_create_dict_vector(): 82 | builder = runner_utils.new_builder() 83 | b = runner_utils.create_dict_vector(builder, {}) 84 | assert not b 85 | b = runner_utils.create_dict_vector(builder, {"q": "hello", "a": "world"}, HCAction.Action.Rewrite, 86 | VECTOR_TYPE_HEADER) 87 | assert b > 0 88 | b = runner_utils.create_dict_vector(builder, {"q": "hello", "a": "world"}, HCAction.Action.Rewrite, 89 | VECTOR_TYPE_QUERY) 90 | assert b > 0 91 | b = runner_utils.create_dict_vector(builder, {"q": "hello", "a": "world"}, HCAction.Action.Stop, 92 | VECTOR_TYPE_HEADER) 93 | assert b > 0 94 | b = runner_utils.create_dict_vector(builder, {"q": "hello", "a": "world"}, 0, 0) 95 | assert not b 96 | 97 | 98 | def test_create_str_vector(): 99 | builder = runner_utils.new_builder() 100 | b = runner_utils.create_str_vector(builder, "") 101 | assert not b 102 | b = runner_utils.create_str_vector(builder, "Hello") 103 | assert b 104 | 105 | 106 | def test_get_vector_object(): 107 | obj = runner_utils.get_vector_object(HCAction.Action.Rewrite, VECTOR_TYPE_HEADER) 108 | assert obj 109 | obj = runner_utils.get_vector_object(HCAction.Action.Rewrite, VECTOR_TYPE_QUERY) 110 | assert obj 111 | obj = runner_utils.get_vector_object(HCAction.Action.Stop, VECTOR_TYPE_HEADER) 112 | assert obj 113 | obj = runner_utils.get_vector_object(HCAction.Action.Stop, VECTOR_TYPE_QUERY) 114 | assert not obj 115 | 116 | 117 | def test_parse_dict_vector(): 118 | buf = get_request_buffer() 119 | req = A6HTTPReqCallReq.Req.GetRootAsReq(buf) 120 | parse_fail = runner_utils.parse_dict_vector(req, 0) 121 | assert parse_fail == {} 122 | parse_ok = runner_utils.parse_dict_vector(req, runner_utils.VECTOR_TYPE_HEADER) 123 | assert parse_ok.get("hello") == "world" 124 | 125 | 126 | def test_parse_list_vector(): 127 | buf = get_request_buffer() 128 | req = A6HTTPReqCallReq.Req.GetRootAsReq(buf) 129 | parse_fail = runner_utils.parse_list_vector(req, 0) 130 | assert parse_fail == [] 131 | parse_ok = runner_utils.parse_list_vector(req, runner_utils.VECTOR_TYPE_SOURCE_IP) 132 | assert parse_ok == [127, 0, 0, 1] 133 | --------------------------------------------------------------------------------