Skip to content

Commit

Permalink
Fix reverse proxy pattern (#103)
Browse files Browse the repository at this point in the history
* Reverse proxied middleware pattern

* Added Zuul headers
  • Loading branch information
avara1986 committed Mar 24, 2020
1 parent 517e5b6 commit 266411c
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 20 deletions.
22 changes: 4 additions & 18 deletions pyms/flask/app/create_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
62 changes: 62 additions & 0 deletions pyms/flask/app/utils.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion pyms/flask/services/swagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion tests/test_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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):
"""
Expand Down

0 comments on commit 266411c

Please sign in to comment.