This is an automated email from the ASF dual-hosted git repository. shuaijinchao pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix-python-plugin-runner.git
The following commit(s) were added to refs/heads/master by this push: new 232450b refactor: request processing flow (#36) 232450b is described below commit 232450b9a11baaad6453700b3b8d78def330475d Author: 帅进超 <shuaijinc...@gmail.com> AuthorDate: Mon Dec 6 09:58:48 2021 +0800 refactor: request processing flow (#36) --- .github/workflows/runner-lint.yml | 2 +- .github/workflows/runner-test.yml | 8 +- apisix/plugins/rewrite.py | 14 +- apisix/plugins/stop.py | 7 +- apisix/runner/http/method.py | 77 -------- apisix/runner/http/request.py | 88 ++++++--- apisix/runner/http/response.py | 131 +++----------- apisix/runner/plugin/base.py | 35 ---- apisix/runner/plugin/cache.py | 2 + apisix/runner/plugin/core.py | 48 ++--- apisix/runner/server/config.py | 4 +- apisix/runner/server/handle.py | 198 +++++++++------------ apisix/runner/server/response.py | 2 +- apisix/runner/server/server.py | 51 ++++-- .../runner/{http/protocol.py => utils/__init__.py} | 9 - apisix/runner/utils/common.py | 198 +++++++++++++++++++++ apisix/main.py => bin/py-runner | 4 +- {apisix => conf}/config.yaml | 0 docs/en/latest/developer-guide.md | 29 +-- docs/en/latest/getting-started.md | 7 +- requirements.txt | 2 +- setup.py | 2 +- tests/runner/http/test_method.py | 55 ------ tests/runner/http/test_protocol.py | 26 --- tests/runner/http/test_request.py | 122 +++++-------- tests/runner/http/test_response.py | 115 ++---------- tests/runner/plugin/test_base.py | 4 + tests/runner/plugin/test_cache.py | 21 +++ tests/runner/plugin/test_core.py | 72 ++++---- tests/runner/server/test_config.py | 2 +- tests/runner/server/test_handle.py | 194 ++++++++++++-------- tests/runner/server/test_protocol.py | 12 +- tests/runner/server/test_response.py | 22 ++- tests/runner/server/test_server.py | 22 ++- tests/runner/utils/test_common.py | 84 +++++++++ 35 files changed, 817 insertions(+), 852 deletions(-) diff --git a/.github/workflows/runner-lint.yml b/.github/workflows/runner-lint.yml index 6222861..1372061 100644 --- a/.github/workflows/runner-lint.yml +++ b/.github/workflows/runner-lint.yml @@ -37,6 +37,6 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.6 + python-version: 3.7 - name: Lint codes run: make lint diff --git a/.github/workflows/runner-test.yml b/.github/workflows/runner-test.yml index af36bef..9cd9859 100644 --- a/.github/workflows/runner-test.yml +++ b/.github/workflows/runner-test.yml @@ -31,21 +31,25 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ 3.6, 3.7, 3.8, 3.9 ] + python-version: [ '3.7', '3.8', '3.9', '3.10' ] fail-fast: false steps: - name: Checkout source codes uses: actions/checkout@v2 with: submodules: true + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Set up dependencies run: make setup install + - name: Run unit tests run: make test + - name: Upload coverage profile - if: ${{ matrix.python-version == '3.6' }} + if: ${{ matrix.python-version == '3.7' }} run: bash <(curl -s https://codecov.io/bash) diff --git a/apisix/plugins/rewrite.py b/apisix/plugins/rewrite.py index af034ea..12543dc 100644 --- a/apisix/plugins/rewrite.py +++ b/apisix/plugins/rewrite.py @@ -42,18 +42,10 @@ class Rewrite(Base): # print(self.config) # Rewrite request headers - headers = request.headers - headers["X-Resp-A6-Runner"] = "Python" - response.headers = headers + request.headers["X-Resp-A6-Runner"] = "Python" # Rewrite request args - args = request.args - args["a6_runner"] = "Python" - response.args = args + request.args["a6_runner"] = "Python" # Rewrite request path - path = request.path - response.path = path - - # Set plugin to `rewrite` type, default `rewrite` - self.rewrite() + request.path = "/a6/python/runner" diff --git a/apisix/plugins/stop.py b/apisix/plugins/stop.py index 6434afc..f0bdd8f 100644 --- a/apisix/plugins/stop.py +++ b/apisix/plugins/stop.py @@ -42,15 +42,10 @@ class Stop(Base): # print(self.config) # Set response headers - headers = request.headers - headers["X-Resp-A6-Runner"] = "Python" - response.headers = headers + response.headers["X-Resp-A6-Runner"] = "Python" # Set response body response.body = "Hello, Python Runner of APISIX" # Set response status code response.status_code = 201 - - # Set plugin to `stop` type, default `rewrite` - self.stop() diff --git a/apisix/runner/http/method.py b/apisix/runner/http/method.py deleted file mode 100644 index df90f8d..0000000 --- a/apisix/runner/http/method.py +++ /dev/null @@ -1,77 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from a6pluginproto import Method as A6Method - -A6MethodGET = "GET" -A6MethodHEAD = "HEAD" -A6MethodPOST = "POST" -A6MethodPUT = "PUT" -A6MethodDELETE = "DELETE" -A6MethodMKCOL = "MKCOL" -A6MethodCOPY = "COPY" -A6MethodMOVE = "MOVE" -A6MethodOPTIONS = "OPTIONS" -A6MethodPROPFIND = "PROPFIND" -A6MethodPROPPATCH = "PROPPATCH" -A6MethodLOCK = "LOCK" -A6MethodUNLOCK = "UNLOCK" -A6MethodPATCH = "PATCH" -A6MethodTRACE = "TRACE" - -methodName = { - A6Method.Method.GET: A6MethodGET, - A6Method.Method.HEAD: A6MethodHEAD, - A6Method.Method.POST: A6MethodPOST, - A6Method.Method.PUT: A6MethodPUT, - A6Method.Method.DELETE: A6MethodDELETE, - A6Method.Method.MKCOL: A6MethodMKCOL, - A6Method.Method.COPY: A6MethodCOPY, - A6Method.Method.MOVE: A6MethodMOVE, - A6Method.Method.OPTIONS: A6MethodOPTIONS, - A6Method.Method.PROPFIND: A6MethodPROPFIND, - A6Method.Method.PROPPATCH: A6MethodPROPPATCH, - A6Method.Method.LOCK: A6MethodLOCK, - A6Method.Method.UNLOCK: A6MethodUNLOCK, - A6Method.Method.PATCH: A6MethodPATCH, - A6Method.Method.TRACE: A6MethodTRACE, -} - -methodCode = { - A6MethodGET: A6Method.Method.GET, - A6MethodHEAD: A6Method.Method.HEAD, - A6MethodPOST: A6Method.Method.POST, - A6MethodPUT: A6Method.Method.PUT, - A6MethodDELETE: A6Method.Method.DELETE, - A6MethodMKCOL: A6Method.Method.MKCOL, - A6MethodCOPY: A6Method.Method.COPY, - A6MethodMOVE: A6Method.Method.MOVE, - A6MethodOPTIONS: A6Method.Method.OPTIONS, - A6MethodPROPFIND: A6Method.Method.PROPFIND, - A6MethodPROPPATCH: A6Method.Method.PROPPATCH, - A6MethodLOCK: A6Method.Method.LOCK, - A6MethodUNLOCK: A6Method.Method.UNLOCK, - A6MethodPATCH: A6Method.Method.PATCH, - A6MethodTRACE: A6Method.Method.TRACE, -} - - -def get_name_by_code(code: int) -> str: - return methodName.get(code) - - -def get_code_by_name(name: str) -> int: - return methodCode.get(name) diff --git a/apisix/runner/http/request.py b/apisix/runner/http/request.py index 844b647..98bfb00 100644 --- a/apisix/runner/http/request.py +++ b/apisix/runner/http/request.py @@ -14,30 +14,32 @@ # See the License for the specific language governing permissions and # limitations under the License. # + import json +import flatbuffers +import apisix.runner.plugin.core as runner_plugin +import apisix.runner.utils.common as runner_utils + from ipaddress import IPv4Address from ipaddress import IPv6Address -import apisix.runner.plugin.core as RunnerPlugin -import apisix.runner.http.method as RunnerMethod -from apisix.runner.http.protocol import RPC_HTTP_REQ_CALL -from apisix.runner.http.protocol import RPC_PREPARE_CONF -from a6pluginproto.HTTPReqCall import Req as A6HTTPReqCallReq -from a6pluginproto.PrepareConf import Req as A6PrepareConfReq +from A6.HTTPReqCall import Rewrite as HCRw +from A6.HTTPReqCall import Action as HCAction +from A6.HTTPReqCall import Req as HCReq +from A6.PrepareConf import Req as PCReq class Request: - def __init__(self, ty: int = 0, buf: bytes = b''): + def __init__(self, r): """ Init and parse request - :param ty: - rpc request protocol type - :param buf: - rpc request buffer data + :param r: + rpc request object """ - self._rpc_type = ty - self._rpc_buf = buf + self._rpc_type = r.request.ty + self._rpc_buf = r.request.data self._req_id = 0 + self.code = 0 self._req_conf_token = 0 self._req_method = "" self._req_path = "" @@ -228,7 +230,7 @@ class Request: self._req_args = {} self._req_src_ip = "" - def _parse_src_ip(self, req: A6HTTPReqCallReq) -> None: + def _parse_src_ip(self, req: HCReq) -> None: """ parse request source ip address :param req: @@ -249,7 +251,7 @@ class Request: if ip_len == 16: self.src_ip = IPv6Address(ip_byte).exploded - def _parse_headers(self, req: A6HTTPReqCallReq) -> None: + def _parse_headers(self, req: HCReq) -> None: """ parse request headers :param req: @@ -263,7 +265,7 @@ class Request: headers[key] = val self.headers = headers - def _parse_args(self, req: A6HTTPReqCallReq) -> None: + def _parse_args(self, req: HCReq) -> None: """ parse request args :param req: @@ -277,14 +279,14 @@ class Request: args[key] = val self.args = args - def _parse_configs(self, req: A6PrepareConfReq) -> None: + def _parse_configs(self, req: PCReq) -> None: """ parse request plugin configs :param req: :return: """ if not req.ConfIsNone(): - plugins = RunnerPlugin.loading() + plugins = runner_plugin.loading() configs = {} for i in range(req.ConfLength()): name = str(req.Conf(i).Name(), encoding="UTF-8").lower() @@ -302,16 +304,56 @@ class Request: init request handler :return: """ - if self.rpc_type == RPC_HTTP_REQ_CALL: - req = A6HTTPReqCallReq.Req.GetRootAsReq(self.rpc_buf) + if self.rpc_type == runner_utils.RPC_HTTP_REQ_CALL: + req = HCReq.Req.GetRootAsReq(self.rpc_buf) self.id = req.Id() - self.method = RunnerMethod.get_name_by_code(req.Method()) + self.method = runner_utils.get_method_name_by_code(req.Method()) self.path = str(req.Path(), encoding="UTF-8") self.conf_token = req.ConfToken() self._parse_src_ip(req) self._parse_headers(req) self._parse_args(req) - if self.rpc_type == RPC_PREPARE_CONF: - req = A6PrepareConfReq.Req.GetRootAsReq(self.rpc_buf) + if self.rpc_type == runner_utils.RPC_PREPARE_CONF: + req = PCReq.Req.GetRootAsReq(self.rpc_buf) self._parse_configs(req) + + def checked(self): + """ + check request params is valid + :return: + """ + if len(self._req_path) == 0 and len(self._req_headers) == 0 and len(self._req_args) == 0: + return False + else: + return True + + @runner_utils.response_config + def config_handler(self, builder: flatbuffers.Builder): + return self.conf_token + + @runner_utils.response_call(HCAction.Action.Rewrite) + def call_handler(self, builder: flatbuffers.Builder): + if not self.checked(): + return None, 0 + + if len(self._req_path) <= 0: + self._req_path = "/" + path_vector = runner_utils.create_str_vector(builder, self._req_path) + + headers_vector = runner_utils.create_dict_vector(builder, self._req_headers, HCAction.Action.Rewrite, + runner_utils.VECTOR_TYPE_HEADER) + + args_vector = runner_utils.create_dict_vector(builder, self._req_args, HCAction.Action.Rewrite, + runner_utils.VECTOR_TYPE_QUERY) + + HCRw.RewriteStart(builder) + HCRw.RewriteAddPath(builder, path_vector) + HCRw.RewriteAddHeaders(builder, headers_vector) + HCRw.RewriteAddArgs(builder, args_vector) + rewrite = HCRw.RewriteEnd(builder) + return rewrite, self._req_id + + @runner_utils.response_unknown + def unknown_handler(self, builder: flatbuffers.Builder): + return self.code diff --git a/apisix/runner/http/response.py b/apisix/runner/http/response.py index f39bcfb..4f06034 100644 --- a/apisix/runner/http/response.py +++ b/apisix/runner/http/response.py @@ -16,21 +16,14 @@ # import flatbuffers -from a6pluginproto import TextEntry as A6TextEntry -from a6pluginproto.Err import Resp as A6ErrResp -from a6pluginproto.HTTPReqCall import Stop as A6HTTPReqCallStop -from a6pluginproto.HTTPReqCall import Rewrite as A6HTTPReqCallRewrite -from a6pluginproto.HTTPReqCall import Resp as A6HTTPReqCallResp -from a6pluginproto.HTTPReqCall import Action as A6HTTPReqCallAction -from a6pluginproto.PrepareConf import Resp as A6PrepareConfResp -from apisix.runner.http.protocol import new_builder -from apisix.runner.http.protocol import RPC_PREPARE_CONF -from apisix.runner.http.protocol import RPC_HTTP_REQ_CALL +import apisix.runner.utils.common as runner_utils +from A6.HTTPReqCall import Stop as HCStop +from A6.HTTPReqCall import Action as HCAction RESP_MAX_DATA_SIZE = 2 << 24 - 1 -PLUGIN_ACTION_STOP = A6HTTPReqCallAction.Action.Stop -PLUGIN_ACTION_REWRITE = A6HTTPReqCallAction.Action.Rewrite +PLUGIN_ACTION_STOP = HCAction.Action.Stop +PLUGIN_ACTION_REWRITE = HCAction.Action.Rewrite class Response: @@ -239,98 +232,22 @@ class Response: else: return False - def _gen_config_flat(self, builder: flatbuffers.Builder) -> int: - A6PrepareConfResp.Start(builder) - A6PrepareConfResp.AddConfToken(builder, self.token) - return A6PrepareConfResp.End(builder) - - def _gen_unknown_flat(self, builder: flatbuffers.Builder) -> int: - A6ErrResp.Start(builder) - A6ErrResp.AddCode(builder, self.error_code) - return A6ErrResp.End(builder) - - def _gen_request_flat(self, builder: flatbuffers.Builder) -> int: - def _to_a6_entry(data: dict) -> list: - entries = [] - if not isinstance(data, dict) and len(data) <= 0: - return entries - for key in data: - val = data[key] - key_b = builder.CreateString(key) - val_b = builder.CreateString(val) - A6TextEntry.Start(builder) - A6TextEntry.AddName(builder, key_b) - A6TextEntry.AddValue(builder, val_b) - entry = A6TextEntry.End(builder) - entries.append(entry) - return entries - - if self.action_type == A6HTTPReqCallAction.Action.Stop: - headers_entry = _to_a6_entry(self.headers) - headers_len = len(headers_entry) - A6HTTPReqCallStop.StopStartHeadersVector(builder, headers_len) - for i in range(headers_len - 1, -1, -1): - builder.PrependUOffsetTRelative(headers_entry[i]) - headers_vector = builder.EndVector() - - body = b'' - if self.body and len(self.body) > 0: - body = self.body.encode(encoding="UTF-8") - body_vector = builder.CreateByteVector(body) - - status_code = 200 - if self.status_code > 0: - status_code = self.status_code - - A6HTTPReqCallStop.StopStart(builder) - A6HTTPReqCallStop.StopAddStatus(builder, status_code) - A6HTTPReqCallStop.StopAddBody(builder, body_vector) - A6HTTPReqCallStop.StopAddHeaders(builder, headers_vector) - action = A6HTTPReqCallStop.StopEnd(builder) - else: - args_entry = _to_a6_entry(self.args) - args_len = len(args_entry) - A6HTTPReqCallRewrite.RewriteStartArgsVector(builder, args_len) - for i in range(args_len - 1, -1, -1): - builder.PrependUOffsetTRelative(args_entry[i]) - args_vector = builder.EndVector() - - headers_entry = _to_a6_entry(self.headers) - headers_len = len(headers_entry) - A6HTTPReqCallRewrite.RewriteStartHeadersVector(builder, headers_len) - for i in range(headers_len - 1, -1, -1): - builder.PrependUOffsetTRelative(headers_entry[i]) - headers_vector = builder.EndVector() - - path = b'/' - if self.path and len(self.path) > 0: - path = self.path.encode(encoding="UTF-8") - path_vector = builder.CreateByteVector(path) - - A6HTTPReqCallRewrite.RewriteStart(builder) - A6HTTPReqCallRewrite.RewriteAddPath(builder, path_vector) - A6HTTPReqCallRewrite.RewriteAddArgs(builder, args_vector) - A6HTTPReqCallRewrite.RewriteAddHeaders(builder, headers_vector) - action = A6HTTPReqCallRewrite.RewriteEnd(builder) - - A6HTTPReqCallResp.Start(builder) - A6HTTPReqCallResp.AddId(builder, self.id) - A6HTTPReqCallResp.AddActionType(builder, self.action_type) - A6HTTPReqCallResp.AddAction(builder, action) - return A6HTTPReqCallResp.End(builder) - - def flatbuffers(self) -> flatbuffers.Builder: - """ - response to flat buffer object - :return: - """ - builder = new_builder() - - rpc_handlers = { - RPC_PREPARE_CONF: self._gen_config_flat, - RPC_HTTP_REQ_CALL: self._gen_request_flat - } - - res = rpc_handlers.get(self.rpc_type, self._gen_unknown_flat)(builder) - builder.Finish(res) - return builder + @runner_utils.response_call(HCAction.Action.Stop) + def call_handler(self, builder: flatbuffers.Builder): + if not self.changed(): + return None, 0 + headers_vector = runner_utils.create_dict_vector(builder, self.headers, HCAction.Action.Stop, + runner_utils.VECTOR_TYPE_HEADER) + + body_vector = runner_utils.create_str_vector(builder, self.body) + + status_code = 200 + if self.status_code > 0: + status_code = self.status_code + + HCStop.StopStart(builder) + HCStop.StopAddStatus(builder, status_code) + HCStop.StopAddBody(builder, body_vector) + HCStop.StopAddHeaders(builder, headers_vector) + stop = HCStop.StopEnd(builder) + return stop, self.id diff --git a/apisix/runner/plugin/base.py b/apisix/runner/plugin/base.py index 00d7ccf..6a2202d 100644 --- a/apisix/runner/plugin/base.py +++ b/apisix/runner/plugin/base.py @@ -15,9 +15,6 @@ # limitations under the License. # -from apisix.runner.http.response import PLUGIN_ACTION_REWRITE -from apisix.runner.http.response import PLUGIN_ACTION_STOP - class Base: def __init__(self, name: str): @@ -28,7 +25,6 @@ class Base: """ self._name = name self._config = {} - self._action = PLUGIN_ACTION_REWRITE @property def name(self) -> str: @@ -66,34 +62,3 @@ class Base: self._config = config else: self._config = {} - - @property - def action(self) -> int: - """ - get plugin type - :return: - """ - return self._action - - @action.setter - def action(self, action: int) -> None: - """ - set plugin type - :param action: - :return: - """ - self._action = action - - def stop(self) -> None: - """ - Set plugin to `Stop` type - :return: - """ - self.action = PLUGIN_ACTION_STOP - - def rewrite(self) -> None: - """ - Set plugin to `Rewrite` type - :return: - """ - self.action = PLUGIN_ACTION_REWRITE diff --git a/apisix/runner/plugin/cache.py b/apisix/runner/plugin/cache.py index 30d5c60..ce22b13 100644 --- a/apisix/runner/plugin/cache.py +++ b/apisix/runner/plugin/cache.py @@ -28,6 +28,8 @@ def generate_token() -> int: def set_config_by_token(token: int, configs: dict) -> bool: + if len(configs) <= 0: + return False cache_key = "%s:%s" % (RUNNER_CACHE_ENTRY, token) cache.update(cache_key, configs) return cache.has(cache_key) diff --git a/apisix/runner/plugin/core.py b/apisix/runner/plugin/core.py index e5667fb..abade21 100644 --- a/apisix/runner/plugin/core.py +++ b/apisix/runner/plugin/core.py @@ -17,52 +17,26 @@ import os import importlib from pkgutil import iter_modules -from typing import Tuple -from apisix.runner.server.response import RESP_STATUS_CODE_OK -from apisix.runner.server.response import RESP_STATUS_MESSAGE_OK -from apisix.runner.server.response import RESP_STATUS_CODE_SERVICE_UNAVAILABLE -from apisix.runner.server.response import RESP_STATUS_CODE_BAD_REQUEST +from apisix.runner.http.response import Response as HttpResponse +from apisix.runner.http.request import Request as HttpRequest -def execute(configs: dict, request, response) -> Tuple[int, str]: +def execute(configs: dict, r, req: HttpRequest, reps: HttpResponse) -> bool: for name in configs: plugin = configs.get(name) if type(plugin).__name__.lower() != name.lower(): - return RESP_STATUS_CODE_BAD_REQUEST, "execute plugin `%s`, plugin handler is not object" % name + r.log.error("execute plugin `%s`, plugin handler is not object" % name) + return False try: - plugin.filter(request, response) + plugin.filter(req, reps) except AttributeError as e: - return RESP_STATUS_CODE_SERVICE_UNAVAILABLE, "execute plugin `%s`, %s" % (name, e.args.__str__()) + r.log.error("execute plugin `%s`, %s" % (name, e.args.__str__())) + return False except TypeError as e: - return RESP_STATUS_CODE_BAD_REQUEST, "execute plugin `%s`, %s" % (name, e.args.__str__()) - else: - response.action_type = plugin.action - refresh_response(request, response) - - return RESP_STATUS_CODE_OK, RESP_STATUS_MESSAGE_OK - - -def refresh_response(request, response) -> None: - # setting default header - if len(request.headers) >= 1: - for req_hk in request.headers.keys(): - req_hv = request.headers.get(req_hk) - resp_hv = response.headers.get(req_hk) - if not resp_hv: - response.headers[req_hk] = req_hv - - # setting default path - if not response.path or len(response.path) == 0: - response.path = request.path - - # setting default args - if len(request.args) >= 1: - for req_ak in request.args.keys(): - req_av = request.args.get(req_ak) - resp_av = response.args.get(req_ak) - if not resp_av: - response.args[req_ak] = req_av + r.log.error("execute plugin `%s`, %s" % (name, e.args.__str__())) + return False + return True def loading() -> dict: diff --git a/apisix/runner/server/config.py b/apisix/runner/server/config.py index 9e9f4a9..0234168 100644 --- a/apisix/runner/server/config.py +++ b/apisix/runner/server/config.py @@ -113,8 +113,8 @@ class Config: if len(config_path) and os.path.exists(config_path): abs_path = config_path else: - abs_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) - cf_path = "%s/%s" % (abs_path, config_name) + abs_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + cf_path = "%s/conf/%s" % (abs_path, config_name) if not os.path.exists(cf_path): print("ERR: config file `%s` not exists" % cf_path) exit(1) diff --git a/apisix/runner/server/handle.py b/apisix/runner/server/handle.py index 761c23d..c79f1e3 100644 --- a/apisix/runner/server/handle.py +++ b/apisix/runner/server/handle.py @@ -15,128 +15,94 @@ # limitations under the License. # -import apisix.runner.plugin.core as RunnerPlugin -import apisix.runner.plugin.cache as RunnerCache +import flatbuffers +import apisix.runner.plugin.core as runner_plugin +import apisix.runner.plugin.cache as runner_cache +import apisix.runner.utils.common as runner_utils from apisix.runner.http.response import Response as NewHttpResponse -from apisix.runner.http.response import RESP_MAX_DATA_SIZE from apisix.runner.http.request import Request as NewHttpRequest -from apisix.runner.server.response import Response as NewServerResponse -from apisix.runner.server.response import RESP_STATUS_CODE_OK -from apisix.runner.server.response import RESP_STATUS_MESSAGE_OK -from apisix.runner.server.response import RESP_STATUS_CODE_BAD_REQUEST -from apisix.runner.server.response import RESP_STATUS_MESSAGE_BAD_REQUEST -from apisix.runner.server.response import RESP_STATUS_CODE_CONF_TOKEN_NOT_FOUND -from apisix.runner.server.response import RESP_STATUS_CODE_SERVICE_UNAVAILABLE -from apisix.runner.http.protocol import RPC_PREPARE_CONF -from apisix.runner.http.protocol import RPC_HTTP_REQ_CALL -from apisix.runner.http.protocol import RPC_UNKNOWN +from A6.Err.Code import Code as ErrCode class Handle: - def __init__(self, ty: int = 0, buf: bytes = b'', debug: bool = False): + def __init__(self, r): """ - Init Python runner server - :param ty: + Init RPC Handle + :param r: rpc request protocol type - :param buf: - rpc request buffer data - :param debug: - enable debug mode """ - self.type = ty - self.buffer = buf - self.debug = debug - - @property - def type(self) -> int: - return self._type - - @type.setter - def type(self, ty: int = 0) -> None: - self._type = ty - - @property - def buffer(self) -> bytes: - return self._buffer - - @buffer.setter - def buffer(self, buf: bytes = b'') -> None: - self._buffer = buf - - @property - def debug(self) -> bool: - return self._debug - - @debug.setter - def debug(self, debug: bool = False) -> None: - self._debug = debug - - def _rpc_config(self) -> NewServerResponse: - # init request - req = NewHttpRequest(RPC_PREPARE_CONF, self.buffer) - # generate token - token = RunnerCache.generate_token() - # get plugins config - configs = req.configs - # cache plugins config - ok = RunnerCache.set_config_by_token(token, configs) - if not ok: - return NewServerResponse(code=RESP_STATUS_CODE_SERVICE_UNAVAILABLE, - message="token `%d` cache setting failed" % token) - # init response - resp = NewHttpResponse(RPC_PREPARE_CONF) - resp.token = token - response = resp.flatbuffers() - - return NewServerResponse(code=RESP_STATUS_CODE_OK, message=RESP_STATUS_MESSAGE_OK, data=response.Output(), - ty=self.type) - - def _rpc_call(self) -> NewServerResponse: - # init request - req = NewHttpRequest(RPC_HTTP_REQ_CALL, self.buffer) - # get request token - token = req.conf_token - # get plugins - configs = RunnerCache.get_config_by_token(token) - if len(configs) == 0: - return NewServerResponse(code=RESP_STATUS_CODE_CONF_TOKEN_NOT_FOUND, - message="token `%d` cache acquisition failed" % token) - # init response - resp = NewHttpResponse(RPC_HTTP_REQ_CALL) - # execute plugins - (code, message) = RunnerPlugin.execute(configs, req, resp) - - response = resp.flatbuffers() - return NewServerResponse(code=code, message=message, data=response.Output(), - ty=self.type) - - @staticmethod - def _rpc_unknown(err_code: int = RESP_STATUS_CODE_BAD_REQUEST, - err_message: str = RESP_STATUS_MESSAGE_BAD_REQUEST) -> NewServerResponse: - resp = NewHttpResponse(RPC_UNKNOWN) - resp.error_code = err_code - response = resp.flatbuffers() - return NewServerResponse(code=err_code, message=err_message, data=response.Output(), - ty=RPC_UNKNOWN) - - def dispatch(self) -> NewServerResponse: - resp = None - - if self.type == RPC_PREPARE_CONF: - resp = self._rpc_config() - - if self.type == RPC_HTTP_REQ_CALL: - resp = self._rpc_call() - - if not resp: - return self._rpc_unknown() - - size = len(resp.data) - if (size > RESP_MAX_DATA_SIZE or size <= 0) and resp.code == RESP_STATUS_CODE_OK: - resp = NewServerResponse(RESP_STATUS_CODE_SERVICE_UNAVAILABLE, - "The maximum length of the data is %d, the minimum is 1, but got %d" % ( - RESP_MAX_DATA_SIZE, size)) - if resp.code != 200: - resp = self._rpc_unknown(resp.code, resp.message) - return resp + self.r = r + + def dispatch(self) -> flatbuffers.Builder: + # init builder + builder = runner_utils.new_builder() + # parse request + req = NewHttpRequest(self.r) + + if self.r.request.ty == runner_utils.RPC_PREPARE_CONF: + # generate token + token = runner_cache.generate_token() + # get plugins config + configs = req.configs + # cache plugins config + ok = runner_cache.set_config_by_token(token, configs) + if not ok: + self.r.log.error("token `%d` cache setting failed" % token) + req.code = ErrCode.CONF_TOKEN_NOT_FOUND + req.unknown_handler(builder) + return builder + + req.conf_token = token + ok = req.config_handler(builder) + if not ok: + self.r.log.error("prepare conf request failure") + req.code = ErrCode.BAD_REQUEST + req.unknown_handler(builder) + return builder + + return builder + + elif self.r.request.ty == runner_utils.RPC_HTTP_REQ_CALL: + # get request token + token = req.conf_token + # get plugins + configs = runner_cache.get_config_by_token(token) + + if len(configs) == 0: + self.r.log.error("token `%d` cache acquisition failed" % token) + req.code = ErrCode.CONF_TOKEN_NOT_FOUND + req.unknown_handler(builder) + return builder + + # init response + resp = NewHttpResponse(self.r.request.ty) + resp.id = req.id + + # execute plugins + ok = runner_plugin.execute(configs, self.r, req, resp) + if not ok: + req.code = ErrCode.SERVICE_UNAVAILABLE + req.unknown_handler(builder) + return builder + + # response changed + ok = resp.call_handler(builder) + if ok: + return builder + + # request changed + ok = req.call_handler(builder) + if not ok: + self.r.log.error("http request call failure") + req.code = ErrCode.BAD_REQUEST + req.unknown_handler(builder) + return builder + + return builder + + else: + self.r.log.error("unknown request") + req.code = ErrCode.BAD_REQUEST + req.unknown_handler(builder) + return builder diff --git a/apisix/runner/server/response.py b/apisix/runner/server/response.py index 8dc7a42..dcfe9c8 100644 --- a/apisix/runner/server/response.py +++ b/apisix/runner/server/response.py @@ -15,7 +15,7 @@ # limitations under the License. # -from a6pluginproto.Err import Code as A6ErrCode +from A6.Err import Code as A6ErrCode RESP_STATUS_CODE_OK = 200 RESP_STATUS_MESSAGE_OK = "OK" diff --git a/apisix/runner/server/server.py b/apisix/runner/server/server.py index ca311a7..eb7cd3a 100644 --- a/apisix/runner/server/server.py +++ b/apisix/runner/server/server.py @@ -25,41 +25,54 @@ from apisix.runner.server.config import Config as NewServerConfig from apisix.runner.server.logger import Logger as NewServerLogger from apisix.runner.server.response import RESP_STATUS_CODE_OK -logger = NewServerLogger() +PROTOCOL_HEADER_LEN = 4 -def _threaded(conn: socket.socket): +class RPCData: + def __init__(self, ty: int = 0, data: bytes = b''): + self.ty = ty + self.data = data + + +class RPCRequest: + def __init__(self, conn: socket.socket, log: NewServerLogger): + self.conn = conn + self.log = log + self.request = RPCData() + self.response = RPCData() + + +def _threaded(r: RPCRequest): while True: try: - buffer = conn.recv(4) + buffer = r.conn.recv(PROTOCOL_HEADER_LEN) protocol = NewServerProtocol(buffer, 0) err = protocol.decode() if err.code != RESP_STATUS_CODE_OK: - logger.error(err.message) + r.log.error(err.message) break - logger.info("request type:{}, len:{}", protocol.type, protocol.length) + r.request.ty = protocol.type + r.log.info("request type:{}, len:{}", protocol.type, protocol.length) - buffer = conn.recv(protocol.length) - handler = NewServerHandle(protocol.type, buffer) + r.request.data = r.conn.recv(protocol.length) + handler = NewServerHandle(r) response = handler.dispatch() - if response.code != RESP_STATUS_CODE_OK: - logger.error(response.message) - - protocol = NewServerProtocol(response.data, response.type) + protocol = NewServerProtocol(response.Output(), protocol.type) protocol.encode() - logger.info("response type:{}, len:{}", protocol.type, protocol.length) + r.log.info("response type:{}, len:{}", protocol.type, protocol.length) - conn.sendall(protocol.buffer) + r.conn.sendall(protocol.buffer) except socket.timeout as e: - logger.info("connection timout: {}", e.args.__str__()) + r.log.info("connection timout: {}", e.args.__str__()) break except socket.error as e: - logger.error("connection error: {}", e.args.__str__()) + r.log.error("connection error: {}", e.args.__str__()) break - conn.close() + r.conn.close() + del r class Server: @@ -71,7 +84,8 @@ class Server: self.sock.bind(self.fd) self.sock.listen(1024) - logger.set_level(config.logging.level) + self.logger = NewServerLogger(config.logging.level) + print("listening on unix:%s" % self.fd) def receive(self): @@ -79,7 +93,8 @@ class Server: conn, address = self.sock.accept() conn.settimeout(60) - thread = NewThread(target=_threaded, args=(conn,)) + r = RPCRequest(conn, self.logger) + thread = NewThread(target=_threaded, args=(r,)) thread.setDaemon(True) thread.start() diff --git a/apisix/runner/http/protocol.py b/apisix/runner/utils/__init__.py similarity index 85% rename from apisix/runner/http/protocol.py rename to apisix/runner/utils/__init__.py index 57ae400..b1312a0 100644 --- a/apisix/runner/http/protocol.py +++ b/apisix/runner/utils/__init__.py @@ -14,12 +14,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import flatbuffers - -RPC_PREPARE_CONF = 1 -RPC_HTTP_REQ_CALL = 2 -RPC_UNKNOWN = 0 - - -def new_builder(): - return flatbuffers.Builder(256) diff --git a/apisix/runner/utils/common.py b/apisix/runner/utils/common.py new file mode 100644 index 0000000..cdba9a5 --- /dev/null +++ b/apisix/runner/utils/common.py @@ -0,0 +1,198 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import flatbuffers +from A6 import Method as A6Method +from A6 import TextEntry as A6Entry +from A6.Err.Code import Code as A6ErrCode +from A6.HTTPReqCall import Rewrite as HCRewrite +from A6.HTTPReqCall import Stop as HCStop +from A6.HTTPReqCall import Action as HCAction +from A6.HTTPReqCall import Resp as HCResp +from A6.PrepareConf import Resp as PCResp +from A6.Err import Resp as ErrResp + +RPC_PREPARE_CONF = 1 +RPC_HTTP_REQ_CALL = 2 +RPC_UNKNOWN = 0 + +VECTOR_TYPE_HEADER = 1 +VECTOR_TYPE_QUERY = 2 + +A6MethodGET = "GET" +A6MethodHEAD = "HEAD" +A6MethodPOST = "POST" +A6MethodPUT = "PUT" +A6MethodDELETE = "DELETE" +A6MethodMKCOL = "MKCOL" +A6MethodCOPY = "COPY" +A6MethodMOVE = "MOVE" +A6MethodOPTIONS = "OPTIONS" +A6MethodPROPFIND = "PROPFIND" +A6MethodPROPPATCH = "PROPPATCH" +A6MethodLOCK = "LOCK" +A6MethodUNLOCK = "UNLOCK" +A6MethodPATCH = "PATCH" +A6MethodTRACE = "TRACE" + +methodNames = { + A6Method.Method.GET: A6MethodGET, + A6Method.Method.HEAD: A6MethodHEAD, + A6Method.Method.POST: A6MethodPOST, + A6Method.Method.PUT: A6MethodPUT, + A6Method.Method.DELETE: A6MethodDELETE, + A6Method.Method.MKCOL: A6MethodMKCOL, + A6Method.Method.COPY: A6MethodCOPY, + A6Method.Method.MOVE: A6MethodMOVE, + A6Method.Method.OPTIONS: A6MethodOPTIONS, + A6Method.Method.PROPFIND: A6MethodPROPFIND, + A6Method.Method.PROPPATCH: A6MethodPROPPATCH, + A6Method.Method.LOCK: A6MethodLOCK, + A6Method.Method.UNLOCK: A6MethodUNLOCK, + A6Method.Method.PATCH: A6MethodPATCH, + A6Method.Method.TRACE: A6MethodTRACE, +} + +methodCodes = { + A6MethodGET: A6Method.Method.GET, + A6MethodHEAD: A6Method.Method.HEAD, + A6MethodPOST: A6Method.Method.POST, + A6MethodPUT: A6Method.Method.PUT, + A6MethodDELETE: A6Method.Method.DELETE, + A6MethodMKCOL: A6Method.Method.MKCOL, + A6MethodCOPY: A6Method.Method.COPY, + A6MethodMOVE: A6Method.Method.MOVE, + A6MethodOPTIONS: A6Method.Method.OPTIONS, + A6MethodPROPFIND: A6Method.Method.PROPFIND, + A6MethodPROPPATCH: A6Method.Method.PROPPATCH, + A6MethodLOCK: A6Method.Method.LOCK, + A6MethodUNLOCK: A6Method.Method.UNLOCK, + A6MethodPATCH: A6Method.Method.PATCH, + A6MethodTRACE: A6Method.Method.TRACE, +} + + +def create_dict_entry(builder: flatbuffers.Builder, data: dict) -> list: + entries = [] + if not isinstance(data, dict) or len(data) <= 0: + return entries + for key in data: + val = data[key] + key_bytes = builder.CreateString(key) + val_bytes = builder.CreateString(val) + A6Entry.Start(builder) + A6Entry.AddName(builder, key_bytes) + A6Entry.AddValue(builder, val_bytes) + entry = A6Entry.End(builder) + entries.append(entry) + return entries + + +def get_vector_object(action: int = 0, ty: int = 0): + objects = { + "%s:%s" % (HCAction.Action.Rewrite, VECTOR_TYPE_HEADER): HCRewrite.RewriteStartHeadersVector, + "%s:%s" % (HCAction.Action.Rewrite, VECTOR_TYPE_QUERY): HCRewrite.RewriteStartArgsVector, + "%s:%s" % (HCAction.Action.Stop, VECTOR_TYPE_HEADER): HCStop.StopStartHeadersVector, + } + return objects.get("%s:%s" % (action, ty), None) + + +def create_dict_vector(builder: flatbuffers.Builder, data: dict, action: int = 0, ty: int = 0): + res = 0 + entries = create_dict_entry(builder, data) + entries_len = len(entries) + if entries_len == 0: + return res + + vector_object = get_vector_object(action, ty) + if not vector_object: + return res + + vector_object(builder, entries_len) + for i in range(entries_len - 1, -1, -1): + builder.PrependUOffsetTRelative(entries[i]) + return builder.EndVector() + + +def create_str_vector(builder: flatbuffers.Builder, data: str): + res = 0 + if not data or len(data) <= 0: + return res + + data = data.encode(encoding="UTF-8") + return builder.CreateByteVector(data) + + +def new_builder(): + return flatbuffers.Builder(256) + + +def get_method_name_by_code(code: int) -> str: + return methodNames.get(code) + + +def get_method_code_by_name(name: str) -> int: + return methodCodes.get(name) + + +def response_call(action_type: int): + def decorator(func): + def wrapper(cls, builder: flatbuffers.Builder): + (action, id) = func(cls, builder) + if not action or id == 0: + return False + + HCResp.Start(builder) + HCResp.AddId(builder, id) + HCResp.AddActionType(builder, action_type) + HCResp.AddAction(builder, action) + res = HCResp.End(builder) + builder.Finish(res) + return True + + return wrapper + + return decorator + + +def response_config(func): + def wrapper(cls, builder: flatbuffers.Builder): + token = func(cls, builder) + if token <= 0: + return False + + PCResp.Start(builder) + PCResp.AddConfToken(builder, token) + res = PCResp.End(builder) + builder.Finish(res) + return True + + return wrapper + + +def response_unknown(func): + def wrapper(cls, builder: flatbuffers.Builder): + err_code = func(cls, builder) + if not err_code: + err_code = A6ErrCode.BAD_REQUEST + ErrResp.Start(builder) + ErrResp.AddCode(builder, err_code) + res = ErrResp.End(builder) + builder.Finish(res) + return True + + return wrapper diff --git a/apisix/main.py b/bin/py-runner old mode 100644 new mode 100755 similarity index 92% rename from apisix/main.py rename to bin/py-runner index 00ae269..9ec88aa --- a/apisix/main.py +++ b/bin/py-runner @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + # # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with @@ -32,7 +34,7 @@ def runner() -> None: @runner.command() def start() -> None: - config = NewConfig(os.path.dirname(os.path.abspath(__file__))) + config = NewConfig(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) server = NewServer(config) server.receive() diff --git a/apisix/config.yaml b/conf/config.yaml similarity index 100% rename from apisix/config.yaml rename to conf/config.yaml diff --git a/docs/en/latest/developer-guide.md b/docs/en/latest/developer-guide.md index 79d9c4b..d034f0b 100644 --- a/docs/en/latest/developer-guide.md +++ b/docs/en/latest/developer-guide.md @@ -27,12 +27,13 @@ This documentation explains how to develop this project. ## Prerequisites -* Python 3.6+ +* Python 3.7+ * APISIX 2.7.0 ## Debug -- Run `make setup` Installation dependencies +- Run `make setup` installation dependencies +- Run `make install` installation runner to system - Run `make dev` to start it ## Plugin @@ -76,29 +77,35 @@ class Test(Base): # Get plugin configuration information through `self.config` # print(self.config) + # Setting the request object will continue to forward the request + + # Rewrite request headers + request.headers["X-Resp-A6-Runner"] = "Python" + + # Rewrite request args + request.args["a6_runner"] = "Python" + + # Rewrite request path + request.path = "/a6/python/runner" + + # Setting the response object will terminate the request and respond to the data + # Set response headers - headers = request.headers - headers["X-Resp-A6-Runner"] = "Python" - response.headers = headers + response.headers["X-Resp-A6-Runner"] = "Python" # Set response body response.body = "Hello, Python Runner of APISIX" # Set response status code response.status_code = 201 - - # Set the plug-in to `stop` type, default `rewrite`, use `self.rewrite()` to declare it as `rewrite` type. - self.stop() ``` - The plugin must inherit the `Base` class - The plugin must implement the `filter` function - `filter` function parameters can only contain `Request` and `Response` classes as parameters -- Request parameter can get request information +- Request parameter can get and set request information - Response parameter can set response information - `self.config` can get plug-in configuration information -- Use `self.stop()` to set the plugin as a `stop` type plugin, which will interrupt the request. -- Use `self.rewrite()` to set the plugin as a `rewrite` type plugin, which will not interrupt the request. ## Test diff --git a/docs/en/latest/getting-started.md b/docs/en/latest/getting-started.md index 1212848..9e26b01 100644 --- a/docs/en/latest/getting-started.md +++ b/docs/en/latest/getting-started.md @@ -26,7 +26,7 @@ This document explains how to use Python Runner ## Prerequisites -* Python 3.6+ +* Python 3.7+ * APISIX 2.7.0 @@ -35,6 +35,7 @@ This document explains how to use Python Runner ```bash $ git clone https://github.com/apache/apisix-python-plugin-runner.git $ cd apisix-python-plugin-runner +$ make setup $ make install ``` @@ -47,7 +48,7 @@ $ make install #### Run APISIX Python Runner ```bash $ cd /path/to/apisix-python-plugin-runner -$ APISIX_LISTEN_ADDRESS=unix:/tmp/runner.sock python3 apisix/main.py start +$ APISIX_LISTEN_ADDRESS=unix:/tmp/runner.sock python3 bin/py-runner start ``` #### Modify APISIX configuration file @@ -79,7 +80,7 @@ ext-plugin: ### Log level and socket configuration (Optional) ```bash -$ vim /path/to/apisix-python-plugin-runner/apisix/config.yaml +$ vim /path/to/apisix-python-plugin-runner/conf/config.yaml socket: file: $env.APISIX_LISTEN_ADDRESS # Environment variable or absolute path diff --git a/requirements.txt b/requirements.txt index 1b4de80..8bcdef9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -a6pluginprotos==0.1.0 +a6pluginprotos==0.2.1 click==8.0.1 minicache==0.0.1 PyYAML==5.4.1 diff --git a/setup.py b/setup.py index 79aa082..07d1438 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( author="Jinchao Shuai", author_email="d...@apisix.apache.org", license="Apache 2.0", - python_requires=">=3.6.0", + python_requires=">=3.7.0", packages=find_packages(exclude=["tests"]), install_requires=requirements, ) diff --git a/tests/runner/http/test_method.py b/tests/runner/http/test_method.py deleted file mode 100644 index 388efb0..0000000 --- a/tests/runner/http/test_method.py +++ /dev/null @@ -1,55 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import apisix.runner.http.method as RunnerMethod -from a6pluginproto import Method as A6Method - - -def test_get_name_by_code(): - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodGET) == A6Method.Method.GET - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodHEAD) == A6Method.Method.HEAD - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodPOST) == A6Method.Method.POST - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodPUT) == A6Method.Method.PUT - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodDELETE) == A6Method.Method.DELETE - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodMKCOL) == A6Method.Method.MKCOL - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodCOPY) == A6Method.Method.COPY - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodMOVE) == A6Method.Method.MOVE - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodOPTIONS) == A6Method.Method.OPTIONS - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodPROPFIND) == A6Method.Method.PROPFIND - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodPROPPATCH) == A6Method.Method.PROPPATCH - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodLOCK) == A6Method.Method.LOCK - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodUNLOCK) == A6Method.Method.UNLOCK - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodPATCH) == A6Method.Method.PATCH - assert RunnerMethod.get_code_by_name(RunnerMethod.A6MethodTRACE) == A6Method.Method.TRACE - - -def test_get_code_by_name(): - assert RunnerMethod.get_name_by_code(A6Method.Method.GET) == RunnerMethod.A6MethodGET - assert RunnerMethod.get_name_by_code(A6Method.Method.HEAD) == RunnerMethod.A6MethodHEAD - assert RunnerMethod.get_name_by_code(A6Method.Method.POST) == RunnerMethod.A6MethodPOST - assert RunnerMethod.get_name_by_code(A6Method.Method.PUT) == RunnerMethod.A6MethodPUT - assert RunnerMethod.get_name_by_code(A6Method.Method.DELETE) == RunnerMethod.A6MethodDELETE - assert RunnerMethod.get_name_by_code(A6Method.Method.MKCOL) == RunnerMethod.A6MethodMKCOL - assert RunnerMethod.get_name_by_code(A6Method.Method.COPY) == RunnerMethod.A6MethodCOPY - assert RunnerMethod.get_name_by_code(A6Method.Method.MOVE) == RunnerMethod.A6MethodMOVE - assert RunnerMethod.get_name_by_code(A6Method.Method.OPTIONS) == RunnerMethod.A6MethodOPTIONS - assert RunnerMethod.get_name_by_code(A6Method.Method.PROPFIND) == RunnerMethod.A6MethodPROPFIND - assert RunnerMethod.get_name_by_code(A6Method.Method.PROPPATCH) == RunnerMethod.A6MethodPROPPATCH - assert RunnerMethod.get_name_by_code(A6Method.Method.LOCK) == RunnerMethod.A6MethodLOCK - assert RunnerMethod.get_name_by_code(A6Method.Method.UNLOCK) == RunnerMethod.A6MethodUNLOCK - assert RunnerMethod.get_name_by_code(A6Method.Method.PATCH) == RunnerMethod.A6MethodPATCH - assert RunnerMethod.get_name_by_code(A6Method.Method.TRACE) == RunnerMethod.A6MethodTRACE diff --git a/tests/runner/http/test_protocol.py b/tests/runner/http/test_protocol.py deleted file mode 100644 index be4764e..0000000 --- a/tests/runner/http/test_protocol.py +++ /dev/null @@ -1,26 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import flatbuffers -from apisix.runner.http.protocol import new_builder - - -def test_new_builder(): - builder = new_builder() - assert isinstance(builder, flatbuffers.Builder) - assert builder.Bytes == flatbuffers.Builder(256).Bytes - assert builder.Bytes != flatbuffers.Builder(512).Bytes diff --git a/tests/runner/http/test_request.py b/tests/runner/http/test_request.py index c0ccdb6..f8a4a30 100644 --- a/tests/runner/http/test_request.py +++ b/tests/runner/http/test_request.py @@ -14,96 +14,66 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import flatbuffers -from apisix.runner.http.request import Request as NewHttpRequest -from apisix.runner.http.protocol import RPC_PREPARE_CONF -from apisix.runner.http.protocol import RPC_HTTP_REQ_CALL -from apisix.runner.http.protocol import RPC_UNKNOWN -from apisix.runner.http.protocol import new_builder -from apisix.runner.http.method import get_name_by_code -from apisix.runner.plugin.core import loading -from a6pluginproto.HTTPReqCall import Req as A6HTTPReqCallReq -from a6pluginproto.PrepareConf import Req as A6PrepareConfReq -from a6pluginproto import TextEntry as A6TextEntry -from a6pluginproto import Method as A6Method +import socket +import logging +import apisix.runner.utils.common as runner_utils +from apisix.runner.server.logger import Logger as RunnerServerLogger +from apisix.runner.server.server import RPCRequest as RunnerRPCRequest +from apisix.runner.http.request import Request as RunnerHttpRequest -def _create_entry(builder: flatbuffers.Builder, name: str, value: str) -> int: - name = builder.CreateString(name) - value = builder.CreateString(value) - A6TextEntry.Start(builder) - A6TextEntry.AddName(builder, name) - A6TextEntry.AddValue(builder, value) - return A6TextEntry.End(builder) +def default_request(): + sock = socket.socket() + logger = RunnerServerLogger(logging.INFO) + return RunnerRPCRequest(sock, logger) -def test_request_config(): - builder = new_builder() - plugins = loading() - conf_data = 0 - for name in plugins: - conf_data = _create_entry(builder, name, '{"runner":"Python"}') - break - A6PrepareConfReq.ReqStartConfVector(builder, 1) - builder.PrependUOffsetTRelative(conf_data) - conf = builder.EndVector() - A6PrepareConfReq.Start(builder) - A6PrepareConfReq.AddConf(builder, conf) - req = A6PrepareConfReq.End(builder) - builder.Finish(req) - buf = builder.Output() - req = NewHttpRequest(ty=RPC_PREPARE_CONF, buf=buf) - assert req.configs - assert len(req.configs) >= 1 +def test_request_unknown_handler(): + builder = runner_utils.new_builder() + r = default_request() + req = RunnerHttpRequest(r) + ok = req.unknown_handler(builder) + assert ok -def test_request_call(): - req_path = "/hello/python/runner" - req_src_ip = [127, 0, 0, 1] - req_args = {"a": "args"} - req_headers = {"h": "headers"} +def test_request_config_handler(): + builder = runner_utils.new_builder() + r = default_request() + req = RunnerHttpRequest(r) + req.conf_token = 0 + ok = req.config_handler(builder) + assert not ok + req.conf_token = 1 + ok = req.config_handler(builder) + assert ok - builder = new_builder() - path = builder.CreateString(req_path) - src_ip = bytes(bytearray(req_src_ip)) - src_ip = builder.CreateByteVector(src_ip) - args = _create_entry(builder, "a", req_args.get("a")) - A6HTTPReqCallReq.StartArgsVector(builder, 1) - builder.PrependUOffsetTRelative(args) - args_vec = builder.EndVector() - - headers = _create_entry(builder, "h", req_headers.get("h")) - A6HTTPReqCallReq.StartHeadersVector(builder, 1) - builder.PrependUOffsetTRelative(headers) - headers_vec = builder.EndVector() - - A6HTTPReqCallReq.Start(builder) - A6HTTPReqCallReq.AddId(builder, 1) - A6HTTPReqCallReq.AddMethod(builder, A6Method.Method.GET) - A6HTTPReqCallReq.AddPath(builder, path) - A6HTTPReqCallReq.AddSrcIp(builder, src_ip) - A6HTTPReqCallReq.AddArgs(builder, args_vec) - A6HTTPReqCallReq.AddHeaders(builder, headers_vec) - req = A6HTTPReqCallReq.End(builder) - builder.Finish(req) - buf = builder.Output() - req = NewHttpRequest(ty=RPC_HTTP_REQ_CALL, buf=buf) - - assert req.src_ip == ".".join('%s' % ip for ip in req_src_ip) - assert req.path == req_path - assert req.args.get("a") == req_args.get("a") - assert req.headers.get("h") == req_headers.get("h") - assert req.method == get_name_by_code(A6Method.Method.GET) +def test_request_call_handler(): + builder = runner_utils.new_builder() + r = default_request() + req = RunnerHttpRequest(r) + req.path = "" + req.headers = {} + req.args = {} + ok = req.call_handler(builder) + assert not ok + req.headers["X-Hello"] = "World" + req.id = 1 + ok = req.call_handler(builder) + assert ok + req.path = "/hello" + ok = req.call_handler(builder) + assert ok def test_request_handler(): - req = NewHttpRequest() + r = default_request() + req = RunnerHttpRequest(r) req.id = 1000 assert req.id == 1000 - req.rpc_type = RPC_UNKNOWN - assert req.rpc_type == RPC_UNKNOWN + req.rpc_type = runner_utils.RPC_UNKNOWN + assert req.rpc_type == runner_utils.RPC_UNKNOWN req.rpc_buf = b'hello' assert req.rpc_buf == b'hello' req.conf_token = 10 diff --git a/tests/runner/http/test_response.py b/tests/runner/http/test_response.py index 3867495..50689fa 100644 --- a/tests/runner/http/test_response.py +++ b/tests/runner/http/test_response.py @@ -15,112 +15,25 @@ # limitations under the License. # -from apisix.runner.http.response import Response as NewHttpResponse -from apisix.runner.http.protocol import RPC_HTTP_REQ_CALL -from apisix.runner.http.protocol import RPC_PREPARE_CONF -from apisix.runner.http.protocol import RPC_UNKNOWN -from a6pluginproto.PrepareConf.Resp import Resp as PrepareConfResp -from a6pluginproto.HTTPReqCall.Resp import Resp as HTTPReqCallResp -from a6pluginproto.HTTPReqCall.Action import Action as HTTPReqCallAction -from a6pluginproto.HTTPReqCall.Stop import Stop as HTTPReqCallStop -from a6pluginproto.HTTPReqCall.Rewrite import Rewrite as HTTPReqCallRewrite -from a6pluginproto.Err.Code import Code as A6ErrCode -from a6pluginproto.Err.Resp import Resp as A6ErrResp +import apisix.runner.utils.common as runner_utils +from apisix.runner.http.response import Response as RunnerHttpResponse -def test_response_config(): - token = 1 - resp = NewHttpResponse(ty=RPC_PREPARE_CONF) - resp.token = token - response = resp.flatbuffers() - flat_resp = PrepareConfResp.GetRootAs(response.Output()) - assert resp.changed() - assert flat_resp.ConfToken() == token - - -def test_response_call(): - headers = { - "X-TEST-HELLO": "hello", - "X-TEST-WORLD": "world" - } - args = { - "A-TEST-HELLO": "hello", - "A-TEST-WORLD": "world", - } - body = "hello world" - resp = NewHttpResponse(ty=RPC_HTTP_REQ_CALL) - resp.headers = headers - resp.body = body - resp.status_code = 200 - resp.action_type = HTTPReqCallAction.Stop - response = resp.flatbuffers() - flat_resp = HTTPReqCallResp.GetRootAs(response.Output()) - assert resp.changed() - assert flat_resp.ActionType() == HTTPReqCallAction.Stop - action = flat_resp.Action() - stop = HTTPReqCallStop() - stop.Init(action.Bytes, action.Pos) - body_list = [] - body_len = stop.BodyLength() - for i in range(body_len): - body_list.append(chr(stop.Body(i))) - assert "".join(body_list) == body - header_dict = {} - header_len = stop.HeadersLength() - for j in range(header_len): - entry = stop.Headers(j) - hk = str(entry.Name(), encoding="utf-8") - hv = str(entry.Value(), encoding="utf-8") - header_dict[hk] = hv - assert header_dict.get("X-TEST-HELLO") == headers.get("X-TEST-HELLO") - assert header_dict.get("X-TEST-WORLD") == headers.get("X-TEST-WORLD") - assert stop.Status() == resp.status_code - - resp = NewHttpResponse(ty=RPC_HTTP_REQ_CALL) - resp.headers = headers - resp.args = args - resp.path = "/hello/runner" - resp.action_type = HTTPReqCallAction.Rewrite - response = resp.flatbuffers() - flat_resp = HTTPReqCallResp.GetRootAs(response.Output()) - assert resp.changed() - assert flat_resp.ActionType() == HTTPReqCallAction.Rewrite - action = flat_resp.Action() - rewrite = HTTPReqCallRewrite() - rewrite.Init(action.Bytes, action.Pos) - args_dict = {} - args_len = rewrite.ArgsLength() - for k in range(args_len): - entry = rewrite.Args(k) - ak = str(entry.Name(), encoding="utf-8") - av = str(entry.Value(), encoding="utf-8") - args_dict[ak] = av - assert args_dict.get("A-TEST-HELLO") == args.get("A-TEST-HELLO") - assert args_dict.get("A-TEST-WORLD") == args.get("A-TEST-WORLD") - header_dict = {} - header_len = rewrite.HeadersLength() - for j in range(header_len): - entry = rewrite.Headers(j) - hk = str(entry.Name(), encoding="utf-8") - hv = str(entry.Value(), encoding="utf-8") - header_dict[hk] = hv - assert header_dict.get("X-TEST-HELLO") == headers.get("X-TEST-HELLO") - assert header_dict.get("X-TEST-WORLD") == headers.get("X-TEST-WORLD") - assert rewrite.Path().decode(encoding="UTF-8") == resp.path - - -def test_response_unknown(): - resp = NewHttpResponse(ty=RPC_UNKNOWN) - resp.error_code = A6ErrCode.BAD_REQUEST - response = resp.flatbuffers() - flat_resp = A6ErrResp.GetRootAs(response.Output()) - assert flat_resp.Code() == A6ErrCode.BAD_REQUEST +def test_response_call_handler(): + builder = runner_utils.new_builder() + resp = RunnerHttpResponse() + resp.id = 1 + ok = resp.call_handler(builder) + assert not ok + resp.body = "Hello Python Runner" + ok = resp.call_handler(builder) + assert ok def test_response_handler(): - resp = NewHttpResponse() - resp.rpc_type = RPC_UNKNOWN - assert resp.rpc_type == RPC_UNKNOWN + resp = RunnerHttpResponse() + resp.rpc_type = runner_utils.RPC_UNKNOWN + assert resp.rpc_type == runner_utils.RPC_UNKNOWN resp.token = 1000 assert resp.token == 1000 resp.headers = {"X-HELLO": "Python"} diff --git a/tests/runner/plugin/test_base.py b/tests/runner/plugin/test_base.py index a5f814d..df7192f 100644 --- a/tests/runner/plugin/test_base.py +++ b/tests/runner/plugin/test_base.py @@ -25,6 +25,8 @@ def test_base(): hello.config = hello_config assert hello.name == hello_name assert hello.config == hello_config + hello.name = "hello1" + assert hello.name != hello_name world_name = "world" world_config = "apisxi" @@ -32,3 +34,5 @@ def test_base(): world.config = world_config assert world.name == world_name assert world.config != world_config + world.name = "world1" + assert world.name != world_name diff --git a/tests/runner/plugin/test_cache.py b/tests/runner/plugin/test_cache.py index 91d4435..982316c 100644 --- a/tests/runner/plugin/test_cache.py +++ b/tests/runner/plugin/test_cache.py @@ -29,3 +29,24 @@ def test_cache(): assert ok config = get_config_by_token(token) assert config == cache_config + + +def test_generate_token(): + token = generate_token() + assert token + + +def test_set_config_by_token(): + ok = set_config_by_token(1, {}) + assert not ok + ok = set_config_by_token(1, {"q": "hello"}) + assert ok + + +def test_get_config_by_token(): + token = 1 + data = {"q": "hello"} + ok = set_config_by_token(token, data) + assert ok + d = get_config_by_token(token) + assert d == data diff --git a/tests/runner/plugin/test_core.py b/tests/runner/plugin/test_core.py index 86ac90a..eff0b20 100644 --- a/tests/runner/plugin/test_core.py +++ b/tests/runner/plugin/test_core.py @@ -16,28 +16,16 @@ # import os +import socket +import logging from pkgutil import iter_modules from apisix.runner.plugin.core import loading as plugin_loading from apisix.runner.plugin.core import execute as plugin_execute -from apisix.runner.plugin.core import refresh_response as refresh_response +from apisix.runner.server.logger import Logger as RunnerServerLogger +from apisix.runner.server.server import RPCRequest as RunnerRPCRequest from apisix.runner.http.request import Request as NewHttpRequest from apisix.runner.http.response import Response as NewHttpResponse -from apisix.runner.server.response import RESP_STATUS_CODE_OK -from apisix.runner.server.response import RESP_STATUS_CODE_SERVICE_UNAVAILABLE -from apisix.runner.server.response import RESP_STATUS_CODE_BAD_REQUEST - - -class Test: - """ - test plugin - """ - def filter(self): - """ - test plugin handler - :return: - """ - pass def test_loading(): @@ -51,31 +39,39 @@ def test_loading(): def test_execute(): - request = NewHttpRequest() + sock = socket.socket() + logger = RunnerServerLogger(logging.INFO) + r = RunnerRPCRequest(sock, logger) + request = NewHttpRequest(r) response = NewHttpResponse() configs = plugin_loading() for p_name in configs: configs[p_name] = configs.get(p_name)() - (code, _) = plugin_execute(configs, request, response) - assert code == RESP_STATUS_CODE_OK - (code, _) = plugin_execute(configs, request, None) - assert code == RESP_STATUS_CODE_SERVICE_UNAVAILABLE - configs["test"] = Test() - (code, _) = plugin_execute(configs, request, response) - assert code == RESP_STATUS_CODE_BAD_REQUEST + ok = plugin_execute(configs, r, request, response) + assert ok + # stop plugin + assert response.headers.get("X-Resp-A6-Runner") == "Python" + assert response.body == "Hello, Python Runner of APISIX" + assert response.status_code == 201 + # rewrite plugin + assert request.headers.get("X-Resp-A6-Runner") == "Python" + assert request.args.get("a6_runner") == "Python" + assert request.path == "/a6/python/runner" + configs = {"test": {}} + ok = plugin_execute(configs, r, request, response) + assert not ok + class AttributeErrorExample: + pass -def test_refresh_response(): - request = NewHttpRequest() - request.path = "/hello" - request.args = { - "q": "hello" - } - request.headers = { - "h": "world" - } - response = NewHttpResponse() - refresh_response(request, response) - assert request.path == response.path - assert request.args == response.args - assert request.headers == response.headers + configs = {AttributeErrorExample.__name__.lower(): AttributeErrorExample()} + ok = plugin_execute(configs, r, request, response) + assert not ok + + class TypeErrorExample: + def __init__(self): + self.filter = 10 + + configs = {TypeErrorExample.__name__.lower(): TypeErrorExample()} + ok = plugin_execute(configs, r, request, response) + assert not ok diff --git a/tests/runner/server/test_config.py b/tests/runner/server/test_config.py index a5df767..5c95fcc 100644 --- a/tests/runner/server/test_config.py +++ b/tests/runner/server/test_config.py @@ -40,7 +40,7 @@ def test_config_default(): def test_config_custom(): - config = NewServerConfig("%s/apisix" % os.path.abspath(os.path.join(os.getcwd())), "config.yaml") + config = NewServerConfig("%s" % os.path.abspath(os.path.join(os.getcwd())), "config.yaml") config.logging.level = "NOTSET" assert config.logging.level == logging.NOTSET diff --git a/tests/runner/server/test_handle.py b/tests/runner/server/test_handle.py index 2904c21..9d07f61 100644 --- a/tests/runner/server/test_handle.py +++ b/tests/runner/server/test_handle.py @@ -14,74 +14,56 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -from apisix.runner.server.handle import Handle as NewServerHandle -from apisix.runner.http.protocol import RPC_PREPARE_CONF -from apisix.runner.http.protocol import RPC_HTTP_REQ_CALL -from apisix.runner.http.protocol import RPC_UNKNOWN -from apisix.runner.http.protocol import new_builder -from apisix.runner.server.response import RESP_STATUS_CODE_OK -from apisix.runner.server.response import RESP_STATUS_MESSAGE_OK -from apisix.runner.server.response import RESP_STATUS_CODE_BAD_REQUEST -from apisix.runner.server.response import RESP_STATUS_MESSAGE_BAD_REQUEST -from apisix.runner.server.response import RESP_STATUS_CODE_CONF_TOKEN_NOT_FOUND -from a6pluginproto.HTTPReqCall import Req as A6HTTPReqCallReq -from a6pluginproto.PrepareConf import Req as A6PrepareConfReq -from a6pluginproto.PrepareConf import Resp as A6PrepareConfResp -from a6pluginproto import TextEntry as A6TextEntry -from a6pluginproto import Method as A6Method - - -def test_type(): - handle = NewServerHandle(ty=RPC_UNKNOWN) - assert handle.type == RPC_UNKNOWN - handle = NewServerHandle(ty=RPC_PREPARE_CONF) - assert handle.type == RPC_PREPARE_CONF - handle = NewServerHandle(ty=RPC_HTTP_REQ_CALL) - assert handle.type == RPC_HTTP_REQ_CALL - - -def test_buffer(): - handle = NewServerHandle(buf="Hello Python Runner".encode()) - assert handle.buffer == b"Hello Python Runner" - - -def test_debug(): - handle = NewServerHandle(debug=False) - assert not handle.debug - handle = NewServerHandle(debug=True) - assert handle.debug - - -def test_dispatch_config(): - builder = new_builder() - name = builder.CreateString("say") - value = builder.CreateString('{"body":"Hello Python Runner"}') - A6TextEntry.Start(builder) - A6TextEntry.AddName(builder, name) - A6TextEntry.AddValue(builder, value) - conf_data = A6TextEntry.End(builder) - - A6PrepareConfReq.ReqStartConfVector(builder, 1) - builder.PrependUOffsetTRelative(conf_data) - conf = builder.EndVector() +import logging +import socket + +import apisix.runner.utils.common as runner_utils +from apisix.runner.server.handle import Handle as RunnerServerHandle +from apisix.runner.server.logger import Logger as RunnerServerLogger +from apisix.runner.server.server import RPCRequest as RunnerRPCRequest +from A6.HTTPReqCall import Req as A6HTTPReqCallReq +from A6.PrepareConf import Req as A6PrepareConfReq +from A6.PrepareConf import Resp as A6PrepareConfResp +from A6 import TextEntry as A6TextEntry +from A6 import Method as A6Method +from A6.Err.Resp import Resp as ErrResp +from A6.HTTPReqCall.Resp import Resp as HCResp +from A6.HTTPReqCall.Action import Action as HCAction +from A6.Err.Code import Code as ErrCode +from A6.HTTPReqCall.Stop import Stop as HCStop +from A6.HTTPReqCall.Rewrite import Rewrite as HCRewrite + + +def default_request(): + sock = socket.socket() + logger = RunnerServerLogger(logging.INFO) + return RunnerRPCRequest(sock, logger) + + +def default_plugin_buffer(name: str = "stop", enable_conf: bool = True): + builder = runner_utils.new_builder() + conf = 0 + if enable_conf: + name = builder.CreateString(name) + value = builder.CreateString('{"body":"Hello Python Runner"}') + A6TextEntry.Start(builder) + A6TextEntry.AddName(builder, name) + A6TextEntry.AddValue(builder, value) + conf_data = A6TextEntry.End(builder) + + A6PrepareConfReq.ReqStartConfVector(builder, 1) + builder.PrependUOffsetTRelative(conf_data) + conf = builder.EndVector() A6PrepareConfReq.Start(builder) A6PrepareConfReq.AddConf(builder, conf) req = A6PrepareConfReq.End(builder) builder.Finish(req) - buf = builder.Output() - handle = NewServerHandle(ty=RPC_PREPARE_CONF, buf=buf) - response = handle.dispatch() - resp = A6PrepareConfResp.Resp.GetRootAs(response.data) - assert response.code == RESP_STATUS_CODE_OK - assert response.message == RESP_STATUS_MESSAGE_OK - assert response.type == RPC_PREPARE_CONF - assert resp.ConfToken() != 0 + return builder.Output() -def test_dispatch_call(): - builder = new_builder() +def default_call_buffer(token: int = 0, id: int = 1): + builder = runner_utils.new_builder() # request path path = builder.CreateString("/hello/python/runner") # request ip @@ -109,25 +91,95 @@ def test_dispatch_call(): headers_vec = builder.EndVector() A6HTTPReqCallReq.Start(builder) - A6HTTPReqCallReq.AddId(builder, 1) + A6HTTPReqCallReq.AddId(builder, id) A6HTTPReqCallReq.AddMethod(builder, A6Method.Method.GET) A6HTTPReqCallReq.AddPath(builder, path) A6HTTPReqCallReq.AddSrcIp(builder, src_ip) A6HTTPReqCallReq.AddArgs(builder, args_vec) A6HTTPReqCallReq.AddHeaders(builder, headers_vec) + A6HTTPReqCallReq.AddConfToken(builder, token) req = A6HTTPReqCallReq.End(builder) builder.Finish(req) - buf = builder.Output() + return builder.Output() + - handle = NewServerHandle(ty=RPC_HTTP_REQ_CALL, buf=buf) +def test_dispatch_unknown(): + r = default_request() + r.request.ty = runner_utils.RPC_UNKNOWN + handle = RunnerServerHandle(r) response = handle.dispatch() - assert response.code == RESP_STATUS_CODE_CONF_TOKEN_NOT_FOUND - assert response.type == RPC_UNKNOWN + err = ErrResp.GetRootAsResp(response.Output()) + assert err.Code() == ErrCode.BAD_REQUEST -def test_dispatch_unknown(): - handle = NewServerHandle(ty=RPC_UNKNOWN) +def test_dispatch_config(): + buf = default_plugin_buffer("stop", False) + r = default_request() + r.request.ty = runner_utils.RPC_PREPARE_CONF + r.request.data = buf + handle = RunnerServerHandle(r) + response = handle.dispatch() + err = ErrResp.GetRootAsResp(response.Output()) + assert err.Code() == ErrCode.CONF_TOKEN_NOT_FOUND + + buf = default_plugin_buffer("stop") + r.request.ty = runner_utils.RPC_PREPARE_CONF + r.request.data = buf + handle = RunnerServerHandle(r) + response = handle.dispatch() + resp = A6PrepareConfResp.Resp.GetRootAs(response.Output()) + assert resp.ConfToken() != 0 + + +def test_dispatch_call(): + r = default_request() + r.request.ty = runner_utils.RPC_PREPARE_CONF + r.request.data = default_plugin_buffer("stop") + handle = RunnerServerHandle(r) + response = handle.dispatch() + resp = A6PrepareConfResp.Resp.GetRootAs(response.Output()) + assert resp.ConfToken() != 0 + + buf = default_call_buffer(resp.ConfToken()) + r.request.ty = runner_utils.RPC_HTTP_REQ_CALL + r.request.data = buf + handle = RunnerServerHandle(r) + response = handle.dispatch() + resp = HCResp.GetRootAsResp(response.Output()) + assert resp.Id() > 0 + assert resp.ActionType() == HCAction.Stop + stop = HCStop() + stop.Init(resp.Action().Bytes, resp.Action().Pos) + assert stop.BodyLength() == len("Hello, Python Runner of APISIX") + assert stop.Status() == 201 + + r.request.ty = runner_utils.RPC_PREPARE_CONF + r.request.data = default_plugin_buffer("rewrite") + handle = RunnerServerHandle(r) + response = handle.dispatch() + resp = A6PrepareConfResp.Resp.GetRootAs(response.Output()) + assert resp.ConfToken() != 0 + conf_token = resp.ConfToken() + r.request.ty = runner_utils.RPC_HTTP_REQ_CALL + r.request.data = default_call_buffer(conf_token) + handle = RunnerServerHandle(r) + response = handle.dispatch() + resp = HCResp.GetRootAsResp(response.Output()) + assert resp.Id() > 0 + assert resp.ActionType() == HCAction.Rewrite + rewrite = HCRewrite() + rewrite.Init(resp.Action().Bytes, resp.Action().Pos) + assert rewrite.Path() == b'/a6/python/runner' + + r.request.ty = runner_utils.RPC_HTTP_REQ_CALL + r.request.data = default_call_buffer(conf_token, 0) + handle = RunnerServerHandle(r) + response = handle.dispatch() + resp = ErrResp.GetRootAs(response.Output()) + assert resp.Code() == ErrCode.BAD_REQUEST + + r.request.data = default_call_buffer() + handle = RunnerServerHandle(r) response = handle.dispatch() - assert response.code == RESP_STATUS_CODE_BAD_REQUEST - assert response.message == RESP_STATUS_MESSAGE_BAD_REQUEST - assert response.type == RPC_UNKNOWN + reps = ErrResp.GetRootAs(response.Output()) + assert reps.Code() == ErrCode.CONF_TOKEN_NOT_FOUND diff --git a/tests/runner/server/test_protocol.py b/tests/runner/server/test_protocol.py index a38c7b7..2e6ccc6 100644 --- a/tests/runner/server/test_protocol.py +++ b/tests/runner/server/test_protocol.py @@ -15,24 +15,24 @@ # limitations under the License. # +import apisix.runner.utils.common as runner_utils from apisix.runner.server.protocol import Protocol as NewServerProtocol -from apisix.runner.http.protocol import RPC_PREPARE_CONF from apisix.runner.server.response import RESP_STATUS_CODE_OK from apisix.runner.server.response import RESP_STATUS_MESSAGE_OK def test_protocol_encode(): buf_str = "Hello Python Runner".encode() - protocol = NewServerProtocol(buffer=buf_str, ty=RPC_PREPARE_CONF) + protocol = NewServerProtocol(buffer=buf_str, ty=runner_utils.RPC_PREPARE_CONF) err = protocol.encode() buf_len = len(buf_str) buf_arr = bytearray(buf_len.to_bytes(4, byteorder="big")) - buf_arr[0] = RPC_PREPARE_CONF + buf_arr[0] = runner_utils.RPC_PREPARE_CONF buf_data = bytes(buf_arr) + buf_str buf_len = len(buf_data) assert err.code == RESP_STATUS_CODE_OK assert err.message == RESP_STATUS_MESSAGE_OK - assert protocol.type == RPC_PREPARE_CONF + assert protocol.type == runner_utils.RPC_PREPARE_CONF assert protocol.buffer == buf_data assert protocol.length == buf_len @@ -41,11 +41,11 @@ def test_protocol_decode(): buf_str = "Hello Python Runner".encode() buf_len = len(buf_str) buf_arr = bytearray(buf_len.to_bytes(4, byteorder="big")) - buf_arr[0] = RPC_PREPARE_CONF + buf_arr[0] = runner_utils.RPC_PREPARE_CONF buf_data = bytes(buf_arr) protocol = NewServerProtocol(buffer=buf_data) err = protocol.decode() assert err.code == RESP_STATUS_CODE_OK assert err.message == RESP_STATUS_MESSAGE_OK - assert protocol.type == RPC_PREPARE_CONF + assert protocol.type == runner_utils.RPC_PREPARE_CONF assert protocol.length == buf_len diff --git a/tests/runner/server/test_response.py b/tests/runner/server/test_response.py index 8fe6652..47a52aa 100644 --- a/tests/runner/server/test_response.py +++ b/tests/runner/server/test_response.py @@ -15,14 +15,12 @@ # limitations under the License. # +import apisix.runner.utils.common as runner_utils from apisix.runner.server.response import Response as NewServerResponse from apisix.runner.server.response import RESP_STATUS_CODE_BAD_REQUEST from apisix.runner.server.response import RESP_STATUS_CODE_SERVICE_UNAVAILABLE from apisix.runner.server.response import RESP_STATUS_CODE_CONF_TOKEN_NOT_FOUND from apisix.runner.server.response import RESP_STATUS_CODE_OK -from apisix.runner.http.protocol import RPC_PREPARE_CONF -from apisix.runner.http.protocol import RPC_HTTP_REQ_CALL -from apisix.runner.http.protocol import RPC_UNKNOWN def test_response_code(): @@ -47,20 +45,20 @@ def test_response_data(): def test_response_type(): - response = NewServerResponse(ty=RPC_UNKNOWN) - assert response.type == RPC_UNKNOWN - response = NewServerResponse(ty=RPC_PREPARE_CONF) - assert response.type == RPC_PREPARE_CONF - response = NewServerResponse(ty=RPC_HTTP_REQ_CALL) - assert response.type == RPC_HTTP_REQ_CALL + response = NewServerResponse(ty=runner_utils.RPC_UNKNOWN) + assert response.type == runner_utils.RPC_UNKNOWN + response = NewServerResponse(ty=runner_utils.RPC_PREPARE_CONF) + assert response.type == runner_utils.RPC_PREPARE_CONF + response = NewServerResponse(ty=runner_utils.RPC_HTTP_REQ_CALL) + assert response.type == runner_utils.RPC_HTTP_REQ_CALL def test_response_eq(): resp1 = NewServerResponse(code=RESP_STATUS_CODE_OK, message="Hello Python Runner", - data="Hello Python Runner".encode(), ty=RPC_PREPARE_CONF) + data="Hello Python Runner".encode(), ty=runner_utils.RPC_PREPARE_CONF) resp2 = NewServerResponse(code=RESP_STATUS_CODE_BAD_REQUEST, message="Hello Python Runner", - data="Hello Python Runner".encode(), ty=RPC_PREPARE_CONF) + data="Hello Python Runner".encode(), ty=runner_utils.RPC_PREPARE_CONF) resp3 = NewServerResponse(code=RESP_STATUS_CODE_OK, message="Hello Python Runner", - data="Hello Python Runner".encode(), ty=RPC_PREPARE_CONF) + data="Hello Python Runner".encode(), ty=runner_utils.RPC_PREPARE_CONF) assert resp1 != resp2 assert resp1 == resp3 diff --git a/tests/runner/server/test_server.py b/tests/runner/server/test_server.py index cfe5d98..8d407be 100644 --- a/tests/runner/server/test_server.py +++ b/tests/runner/server/test_server.py @@ -15,14 +15,28 @@ # limitations under the License. # -from apisix.runner.server.server import Server as NewServer -from apisix.runner.server.config import Config as NewConfig +import socket +import logging +from apisix.runner.server.server import Server as RunnerServer +from apisix.runner.server.server import RPCRequest as RunnerRPCRequest +from apisix.runner.server.logger import Logger as RunnerServerLogger +from apisix.runner.server.config import Config as RunnerConfig def test_server(capsys): - config = NewConfig() - server = NewServer(config) + config = RunnerConfig() + server = RunnerServer(config) del server captured = capsys.readouterr() assert captured.out.find("listening on unix") != -1 assert captured.out.find("Bye") != -1 + + +def test_rpc_request(): + sock = socket.socket() + logger = RunnerServerLogger(logging.INFO) + r = RunnerRPCRequest(sock, logger) + assert r.log == logger + assert r.conn == sock + assert r.request.ty == 0 + assert len(r.request.data) == 0 diff --git a/tests/runner/utils/test_common.py b/tests/runner/utils/test_common.py new file mode 100644 index 0000000..accbf65 --- /dev/null +++ b/tests/runner/utils/test_common.py @@ -0,0 +1,84 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import flatbuffers +import apisix.runner.utils.common as runner_utils +from apisix.runner.utils.common import VECTOR_TYPE_HEADER +from apisix.runner.utils.common import VECTOR_TYPE_QUERY +from A6.HTTPReqCall import Action as HCAction + + +def test_get_method_code_by_name(): + for name in runner_utils.methodCodes: + assert runner_utils.get_method_code_by_name(name) == runner_utils.methodCodes.get(name) + + +def test_get_method_name_by_code(): + for code in runner_utils.methodNames: + assert runner_utils.get_method_name_by_code(code) == runner_utils.methodNames.get(code) + + +def test_new_builder(): + builder = runner_utils.new_builder() + assert isinstance(builder, flatbuffers.Builder) + assert builder.Bytes == flatbuffers.Builder(256).Bytes + assert builder.Bytes != flatbuffers.Builder(512).Bytes + + +def test_create_dict_entry(): + builder = runner_utils.new_builder() + entries = runner_utils.create_dict_entry(builder, {}) + assert not entries + examples = {"q": "hello", "a": "world"} + entries = runner_utils.create_dict_entry(builder, examples) + assert len(entries) == 2 + + +def test_create_dict_vector(): + builder = runner_utils.new_builder() + b = runner_utils.create_dict_vector(builder, {}) + assert not b + b = runner_utils.create_dict_vector(builder, {"q": "hello", "a": "world"}, HCAction.Action.Rewrite, + VECTOR_TYPE_HEADER) + assert b > 0 + b = runner_utils.create_dict_vector(builder, {"q": "hello", "a": "world"}, HCAction.Action.Rewrite, + VECTOR_TYPE_QUERY) + assert b > 0 + b = runner_utils.create_dict_vector(builder, {"q": "hello", "a": "world"}, HCAction.Action.Stop, + VECTOR_TYPE_HEADER) + assert b > 0 + b = runner_utils.create_dict_vector(builder, {"q": "hello", "a": "world"}, 0, 0) + assert not b + + +def test_create_str_vector(): + builder = runner_utils.new_builder() + b = runner_utils.create_str_vector(builder, "") + assert not b + b = runner_utils.create_str_vector(builder, "Hello") + assert b + + +def test_get_vector_object(): + obj = runner_utils.get_vector_object(HCAction.Action.Rewrite, VECTOR_TYPE_HEADER) + assert obj + obj = runner_utils.get_vector_object(HCAction.Action.Rewrite, VECTOR_TYPE_QUERY) + assert obj + obj = runner_utils.get_vector_object(HCAction.Action.Stop, VECTOR_TYPE_HEADER) + assert obj + obj = runner_utils.get_vector_object(HCAction.Action.Stop, VECTOR_TYPE_QUERY) + assert not obj