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 342630e refactor(plugin): plugins automatically register and standard
interfaces (#41)
342630e is described below
commit 342630ed2e29c9aa81cf31e299e15ab479a8e6d2
Author: 帅进超 <[email protected]>
AuthorDate: Mon Feb 14 09:23:59 2022 +0800
refactor(plugin): plugins automatically register and standard interfaces
(#41)
---
Makefile | 2 +-
apisix/plugins/rewrite.py | 32 ++++++---
apisix/plugins/stop.py | 32 ++++++---
apisix/runner/http/request.py | 16 ++---
apisix/runner/plugin/base.py | 64 ------------------
apisix/runner/plugin/core.py | 104 ++++++++++++++++++++---------
apisix/runner/server/handle.py | 2 +-
bin/py-runner | 10 +--
tests/runner/plugin/test_base.py | 38 -----------
tests/runner/plugin/test_core.py | 138 ++++++++++++++++++++++++++-------------
10 files changed, 223 insertions(+), 215 deletions(-)
diff --git a/Makefile b/Makefile
index f60fadf..6f28d41 100644
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ lint: clean
.PHONY: clean
clean:
- rm -rf apache_apisix.egg-info dist build .coverage
+ rm -rf apache_apisix.egg-info dist build assets .coverage report.html
find . -name "__pycache__" -exec rm -r {} +
find . -name ".pytest_cache" -exec rm -r {} +
find . -name "*.pyc" -exec rm -r {} +
diff --git a/apisix/plugins/rewrite.py b/apisix/plugins/rewrite.py
index 12543dc..e44900f 100644
--- a/apisix/plugins/rewrite.py
+++ b/apisix/plugins/rewrite.py
@@ -15,31 +15,43 @@
# limitations under the License.
#
-from apisix.runner.plugin.base import Base
+from typing import Any
from apisix.runner.http.request import Request
from apisix.runner.http.response import Response
+from apisix.runner.plugin.core import PluginBase
-class Rewrite(Base):
- def __init__(self):
+class Rewrite(PluginBase):
+
+ def name(self) -> str:
"""
- Examples of `rewrite` type plugins, features:
- This type of plugin can customize the request `args`, `header`,
and `path`
- This type of plugin does not interrupt the request
+ The name of the plugin registered in the runner
+ :return:
"""
- super(Rewrite, self).__init__(self.__class__.__name__)
+ return "rewrite"
- def filter(self, request: Request, response: Response):
+ def config(self, conf: Any) -> Any:
+ """
+ Parse plugin configuration
+ :param conf:
+ :return:
+ """
+ return conf
+
+ def filter(self, conf: Any, request: Request, response: Response):
"""
The plugin executes the main function
+ :param conf:
+ plugin configuration after parsing
:param request:
request parameters and information
:param response:
response parameters and information
:return:
"""
- # Get plugin configuration information through `self.config`
- # print(self.config)
+
+ # print plugin configuration
+ # print(conf)
# Rewrite request headers
request.headers["X-Resp-A6-Runner"] = "Python"
diff --git a/apisix/plugins/stop.py b/apisix/plugins/stop.py
index f0bdd8f..86ce5e9 100644
--- a/apisix/plugins/stop.py
+++ b/apisix/plugins/stop.py
@@ -15,31 +15,43 @@
# limitations under the License.
#
-from apisix.runner.plugin.base import Base
+from typing import Any
from apisix.runner.http.request import Request
from apisix.runner.http.response import Response
+from apisix.runner.plugin.core import PluginBase
-class Stop(Base):
- def __init__(self):
+class Stop(PluginBase):
+
+ def name(self) -> str:
"""
- Example of `stop` type plugin, features:
- This type of plugin can customize response `body`, `header`,
`http_code`
- This type of plugin will interrupt the request
+ The name of the plugin registered in the runner
+ :return:
"""
- super(Stop, self).__init__(self.__class__.__name__)
+ return "stop"
- def filter(self, request: Request, response: Response):
+ def config(self, conf: Any) -> Any:
+ """
+ Parse plugin configuration
+ :param conf:
+ :return:
+ """
+ return conf
+
+ def filter(self, conf: Any, request: Request, response: Response):
"""
The plugin executes the main function
+ :param conf:
+ plugin configuration after parsing
:param request:
request parameters and information
:param response:
response parameters and information
:return:
"""
- # Get plugin configuration information through `self.config`
- # print(self.config)
+
+ # print plugin configuration
+ # print(conf)
# Set response headers
response.headers["X-Resp-A6-Runner"] = "Python"
diff --git a/apisix/runner/http/request.py b/apisix/runner/http/request.py
index 1764839..a318198 100644
--- a/apisix/runner/http/request.py
+++ b/apisix/runner/http/request.py
@@ -15,9 +15,7 @@
# 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
@@ -290,18 +288,12 @@ class Request:
if req.ConfIsNone():
return
- # loading plugin
- plugins = runner_plugin.loading()
configs = {}
for i in range(req.ConfLength()):
- name = str(req.Conf(i).Name().decode()).lower()
- plugin = plugins.get(name)
- if not plugin:
- continue
- value = req.Conf(i).Value().decode()
- plugin = plugin()
- plugin.config = json.loads(value)
- configs[name] = plugin
+ # fetch request config
+ name = req.Conf(i).Name().decode()
+ config = req.Conf(i).Value().decode()
+ configs[name] = config
self.configs = configs
def checked(self):
diff --git a/apisix/runner/plugin/base.py b/apisix/runner/plugin/base.py
deleted file mode 100644
index 6a2202d..0000000
--- a/apisix/runner/plugin/base.py
+++ /dev/null
@@ -1,64 +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.
-#
-
-
-class Base:
- def __init__(self, name: str):
- """
- plugin base class
- :param name:
- instance plugin name
- """
- self._name = name
- self._config = {}
-
- @property
- def name(self) -> str:
- """
- get plugin type
- :return:
- """
- return self._name
-
- @name.setter
- def name(self, name: str) -> None:
- """
- set plugin type
- :param name:
- :return:
- """
- self._name = name
-
- @property
- def config(self) -> dict:
- """
- set plugin config
- :return:
- """
- return self._config
-
- @config.setter
- def config(self, config: dict) -> None:
- """
- get plugin config
- :param config:
- :return:
- """
- if config and isinstance(config, dict):
- self._config = config
- else:
- self._config = {}
diff --git a/apisix/runner/plugin/core.py b/apisix/runner/plugin/core.py
index f130b97..af2e827 100644
--- a/apisix/runner/plugin/core.py
+++ b/apisix/runner/plugin/core.py
@@ -17,38 +17,82 @@
import os
import importlib
+from typing import Any
from pkgutil import iter_modules
from apisix.runner.http.response import Response as HttpResponse
from apisix.runner.http.request import Request as HttpRequest
+PLUGINS = {}
-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():
- r.log.error("execute plugin `%s`, plugin handler is not object" %
name)
- return False
-
- try:
- plugin.filter(req, reps)
- except AttributeError as e:
- r.log.error("execute plugin `%s` AttributeError, %s" % (name,
e.args.__str__()))
- return False
- except TypeError as e:
- r.log.error("execute plugin `%s` TypeError, %s" % (name,
e.args.__str__()))
- return False
- return True
-
-
-def loading() -> dict:
- path = "%s/plugins" %
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
- modules = iter_modules(path=[path])
- plugins = {}
-
- for loader, moduleName, _ in modules:
- classNameConversion = list(map(lambda name: name.capitalize(),
moduleName.split("_")))
- className = "".join(classNameConversion)
- classInstance = getattr(importlib.import_module("apisix.plugins.%s" %
moduleName), className)
- plugins[str(moduleName).lower()] = classInstance
-
- return plugins
+
+class PluginBase:
+
+ def __init_subclass__(cls: Any, **kwargs):
+ """
+ register plugin object
+ :param kwargs:
+ :return:
+ """
+ name = cls.name(cls)
+ if name not in PLUGINS:
+ PLUGINS[name] = cls
+
+ def name(self) -> str:
+ """
+ fetching plugin name
+ :return:
+ """
+ pass
+
+ def config(self, conf: Any) -> Any:
+ """
+ parsing plugin configuration
+ :return:
+ """
+ pass
+
+ def filter(self, conf: Any, req: HttpRequest, reps: HttpResponse) -> None:
+ """
+ execute plugin handler
+ :param conf: plugin configuration
+ :param req: request object
+ :param reps: response object
+ :return:
+ """
+ pass
+
+
+class PluginProcess:
+ """
+ plugin default package name
+ """
+ package = "apisix.plugins"
+
+ @staticmethod
+ def register():
+ plugin_path = "%s/%s" %
(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))),
+ PluginProcess.package.replace(".", "/"))
+ modules = iter_modules(path=[plugin_path])
+ for _, mod_name, _ in modules:
+ importlib.import_module("%s.%s" % (PluginProcess.package,
mod_name))
+
+ @staticmethod
+ def execute(configs: dict, r, req: HttpRequest, reps: HttpResponse):
+ for name, conf in configs.items():
+ try:
+ p = PLUGINS.get(name)()
+ conf = p.config(conf)
+ p.filter(conf, req, reps)
+ except AttributeError as e:
+ r.log.error("execute plugin `%s` AttributeError, %s" % (name,
e.args.__str__()))
+ return False
+ except TypeError as e:
+ r.log.error("execute plugin `%s` TypeError, %s" % (name,
e.args.__str__()))
+ return False
+ except BaseException as e:
+ r.log.error("execute plugin `%s` AnyError, %s" % (name,
e.args.__str__()))
+ return False
+ else:
+ if reps.changed():
+ break
+ return True
diff --git a/apisix/runner/server/handle.py b/apisix/runner/server/handle.py
index c79f1e3..9f43719 100644
--- a/apisix/runner/server/handle.py
+++ b/apisix/runner/server/handle.py
@@ -16,7 +16,7 @@
#
import flatbuffers
-import apisix.runner.plugin.core as runner_plugin
+from apisix.runner.plugin.core import PluginProcess 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
diff --git a/bin/py-runner b/bin/py-runner
index 9ec88aa..2731211 100755
--- a/bin/py-runner
+++ b/bin/py-runner
@@ -20,8 +20,9 @@
import os
import click
-from apisix.runner.server.server import Server as NewServer
-from apisix.runner.server.config import Config as NewConfig
+from apisix.runner.server.server import Server as RunnerServer
+from apisix.runner.server.config import Config as RunnerConfig
+from apisix.runner.plugin.core import PluginProcess as RunnerPlugin
RUNNER_VERSION = "0.1.0"
@@ -34,8 +35,9 @@ def runner() -> None:
@runner.command()
def start() -> None:
- config =
NewConfig(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- server = NewServer(config)
+ RunnerPlugin.register()
+ config =
RunnerConfig(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ server = RunnerServer(config)
server.receive()
diff --git a/tests/runner/plugin/test_base.py b/tests/runner/plugin/test_base.py
deleted file mode 100644
index df7192f..0000000
--- a/tests/runner/plugin/test_base.py
+++ /dev/null
@@ -1,38 +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 apisix.runner.plugin.base import Base
-
-
-def test_base():
- hello_name = "hello"
- hello_config = {"body": "apisix"}
- hello = Base(hello_name)
- 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"
- world = Base(world_name)
- 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_core.py b/tests/runner/plugin/test_core.py
index eff0b20..5c54d88 100644
--- a/tests/runner/plugin/test_core.py
+++ b/tests/runner/plugin/test_core.py
@@ -15,63 +15,111 @@
# limitations under the License.
#
-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 PluginBase
+from apisix.runner.plugin.core import PluginProcess
+from apisix.runner.plugin.core import PLUGINS
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
-def test_loading():
- configs = plugin_loading()
- assert isinstance(configs, dict)
- config_keys = configs.keys()
- path = "%s/plugins" %
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
- modules = iter_modules(path=[path])
- for _, name, _ in modules:
- assert name in config_keys
+class NonePlugin:
+ pass
-def test_execute():
+class ErrorPlugin:
+
+ def config(self, conf):
+ return conf
+
+ def filter(self, conf, req, reps):
+ raise RuntimeError("Runtime Error")
+
+
+def default_request():
sock = socket.socket()
logger = RunnerServerLogger(logging.INFO)
- r = RunnerRPCRequest(sock, logger)
+ return RunnerRPCRequest(sock, logger)
+
+
+def test_process_register():
+ assert PLUGINS == {}
+ PluginProcess.register()
+ assert len(PLUGINS) == 2
+
+
+def test_process_execute():
+ r = default_request()
request = NewHttpRequest(r)
response = NewHttpResponse()
- configs = plugin_loading()
- for p_name in configs:
- configs[p_name] = configs.get(p_name)()
- 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
-
- 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
+ tests = [
+ {
+ "conf": {
+ "stop": "config"
+ },
+ "autoload": True,
+ "req": request,
+ "resp": response,
+ "expected": True
+ },
+ {
+ "conf": {
+ "rewrite": "config"
+ },
+ "autoload": True,
+ "req": request,
+ "resp": response,
+ "expected": True
+ },
+ # AnyError
+ {
+ "conf": {
+ "any": "config"
+ },
+ "plugins": {
+ "any": ErrorPlugin
+ },
+ "autoload": False,
+ "expected": False
+ },
+ # AttributeError
+ {
+ "conf": {
+ "attr": "config"
+ },
+ "plugins": {
+ "attr": NonePlugin
+ },
+ "autoload": False,
+ "expected": False
+ },
+ # TypeError
+ {
+ "conf": {
+ "none": "config"
+ },
+ "autoload": True,
+ "expected": False
+ },
+ ]
+
+ for test in tests:
+ if test.get("autoload"):
+ PluginProcess.register()
+ if test.get("plugins"):
+ for plg_name, plg_obj in test.get("plugins").items():
+ PLUGINS[plg_name] = plg_obj
+ res = PluginProcess.execute(test.get("conf"), r, test.get("req"),
test.get("resp"))
+ assert res == test.get("expected")
+
+
+def test_base():
+ r = default_request()
+ pb = PluginBase()
+ assert pb.name() is None
+ assert pb.config(None) is None
+ assert pb.filter(None, NewHttpRequest(r), NewHttpResponse()) is None