diff --git a/pyms/flask/app/create_app.py b/pyms/flask/app/create_app.py index e7c7cf6..6bd8003 100644 --- a/pyms/flask/app/create_app.py +++ b/pyms/flask/app/create_app.py @@ -7,6 +7,7 @@ from pyms.config import get_conf from pyms.config.conf import validate_conf from pyms.constants import LOGGER_NAME, CONFIG_BASE +from pyms.flask.app.utils import SingletonMeta, ReverseProxied from pyms.flask.healthcheck import healthcheck_blueprint from pyms.flask.services.driver import ServicesManager from pyms.logger import CustomJsonFormatter @@ -15,24 +16,6 @@ logger = logging.getLogger(LOGGER_NAME) -class SingletonMeta(type): - """ - The Singleton class can be implemented in different ways in Python. Some - possible methods include: base class, decorator, metaclass. We will use the - metaclass because it is best suited for this purpose. - """ - _instances = {} - _singleton = True - - def __call__(cls, *args, **kwargs): - if cls not in cls._instances or not cls._singleton: - cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs) - else: - cls._instances[cls].__init__(*args, **kwargs) - - return cls._instances[cls] - - class Microservice(metaclass=SingletonMeta): """The class Microservice is the core of all microservices built with PyMS. You can create a simple microservice such as: @@ -184,6 +167,9 @@ def init_app(self) -> Flask: application.root_path = self.path + # Fix connexion issue https://github.com/zalando/connexion/issues/527 + application.wsgi_app = ReverseProxied(application.wsgi_app) + return application def init_metrics(self): diff --git a/pyms/flask/app/utils.py b/pyms/flask/app/utils.py new file mode 100644 index 0000000..640c0e0 --- /dev/null +++ b/pyms/flask/app/utils.py @@ -0,0 +1,62 @@ +class SingletonMeta(type): + """ + The Singleton class can be implemented in different ways in Python. Some + possible methods include: base class, decorator, metaclass. We will use the + metaclass because it is best suited for this purpose. + """ + _instances = {} + _singleton = True + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances or not cls._singleton: + cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs) + else: + cls._instances[cls].__init__(*args, **kwargs) + + return cls._instances[cls] + + +class ReverseProxied: + """ + Create a Proxy pattern https://microservices.io/patterns/apigateway.html. + You can run the microservice A in your local machine in http://localhost:5000/my-endpoint/ + If you deploy your microservice, in some cases this microservice run behind a cluster, a gateway... and this + gateway redirect traffic to the microservice with a specific path like yourdomian.com/my-ms-a/my-endpoint/. + This class understand this path if the gateway send a specific header + """ + + def __init__(self, app): + self.app = app + + @staticmethod + def _extract_prefix(environ): + """ + Get Path from environment from: + - Traefik with HTTP_X_SCRIPT_NAME https://docs.traefik.io/v2.0/middlewares/headers/ + - Nginx and Ingress with HTTP_X_SCRIPT_NAME https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/ + - Apache with HTTP_X_SCRIPT_NAME https://stackoverflow.com/questions/55619013/proxy-and-rewrite-to-webapp + - Zuul with HTTP_X_FORWARDER_PREFIX https://cloud.spring.io/spring-cloud-netflix/multi/multi__router_and_filter_zuul.html + :param environ: + :return: + """ + # Get path from Traefik, Nginx and Apache + path = environ.get('HTTP_X_SCRIPT_NAME', '') + if not path: + # Get path from Zuul + path = environ.get('HTTP_X_FORWARDED_PREFIX', '') + if path and not path.startswith("/"): + path = "/" + path + return path + + def __call__(self, environ, start_response): + script_name = self._extract_prefix(environ) + if script_name: + environ['SCRIPT_NAME'] = script_name + path_info = environ['PATH_INFO'] + if path_info.startswith(script_name): + environ['PATH_INFO'] = path_info[len(script_name):] + + scheme = environ.get('HTTP_X_SCHEME', '') + if scheme: + environ['wsgi.url_scheme'] = scheme + return self.app(environ, start_response) diff --git a/pyms/flask/services/swagger.py b/pyms/flask/services/swagger.py index 25c743f..d274f4b 100644 --- a/pyms/flask/services/swagger.py +++ b/pyms/flask/services/swagger.py @@ -83,7 +83,7 @@ def init_app(self, config, path): # Fix Connexion issue https://github.com/zalando/connexion/issues/1135 if application_root == "/": - params["base_path"] = "" + del params["base_path"] app.add_api(**params) # Invert the objects, instead connexion with a Flask object, a Flask object with diff --git a/tests/test_flask.py b/tests/test_flask.py index 3992ad3..d878162 100644 --- a/tests/test_flask.py +++ b/tests/test_flask.py @@ -7,7 +7,7 @@ from pyms.constants import CONFIGMAP_FILE_ENVIRONMENT from pyms.flask.app import Microservice, config from pyms.flask.services.driver import DriverService -from tests.common import MyMicroserviceNoSingleton, MyMicroservice +from tests.common import MyMicroservice def home(): @@ -73,6 +73,18 @@ def test_disabled_service(self): self.assertTrue(isinstance(self.app.ms.metrics, DriverService)) assert "'MyMicroservice' object has no attribute 'metrics'" in str(excinfo.value) + def test_reverse_proxy(self): + response = self.client.get('/my-proxy-path/ui/', headers={"X-Script-Name": "/my-proxy-path"}) + self.assertEqual(200, response.status_code) + + def test_reverse_proxy_no_slash(self): + response = self.client.get('/my-proxy-path/ui/', headers={"X-Script-Name": "my-proxy-path"}) + self.assertEqual(200, response.status_code) + + def test_reverse_proxy_zuul(self): + response = self.client.get('/my-proxy-path-zuul/ui/', headers={"X-Forwarded-Prefix": "my-proxy-path-zuul"}) + self.assertEqual(200, response.status_code) + class MicroserviceTest(unittest.TestCase): """