Skip to content

Commit 266411c

Browse files
authored
Fix reverse proxy pattern (#103)
* Reverse proxied middleware pattern * Added Zuul headers
1 parent 517e5b6 commit 266411c

File tree

4 files changed

+80
-20
lines changed

4 files changed

+80
-20
lines changed

pyms/flask/app/create_app.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pyms.config import get_conf
88
from pyms.config.conf import validate_conf
99
from pyms.constants import LOGGER_NAME, CONFIG_BASE
10+
from pyms.flask.app.utils import SingletonMeta, ReverseProxied
1011
from pyms.flask.healthcheck import healthcheck_blueprint
1112
from pyms.flask.services.driver import ServicesManager
1213
from pyms.logger import CustomJsonFormatter
@@ -15,24 +16,6 @@
1516
logger = logging.getLogger(LOGGER_NAME)
1617

1718

18-
class SingletonMeta(type):
19-
"""
20-
The Singleton class can be implemented in different ways in Python. Some
21-
possible methods include: base class, decorator, metaclass. We will use the
22-
metaclass because it is best suited for this purpose.
23-
"""
24-
_instances = {}
25-
_singleton = True
26-
27-
def __call__(cls, *args, **kwargs):
28-
if cls not in cls._instances or not cls._singleton:
29-
cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)
30-
else:
31-
cls._instances[cls].__init__(*args, **kwargs)
32-
33-
return cls._instances[cls]
34-
35-
3619
class Microservice(metaclass=SingletonMeta):
3720
"""The class Microservice is the core of all microservices built with PyMS.
3821
You can create a simple microservice such as:
@@ -184,6 +167,9 @@ def init_app(self) -> Flask:
184167

185168
application.root_path = self.path
186169

170+
# Fix connexion issue https://github.com/zalando/connexion/issues/527
171+
application.wsgi_app = ReverseProxied(application.wsgi_app)
172+
187173
return application
188174

189175
def init_metrics(self):

pyms/flask/app/utils.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
class SingletonMeta(type):
2+
"""
3+
The Singleton class can be implemented in different ways in Python. Some
4+
possible methods include: base class, decorator, metaclass. We will use the
5+
metaclass because it is best suited for this purpose.
6+
"""
7+
_instances = {}
8+
_singleton = True
9+
10+
def __call__(cls, *args, **kwargs):
11+
if cls not in cls._instances or not cls._singleton:
12+
cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)
13+
else:
14+
cls._instances[cls].__init__(*args, **kwargs)
15+
16+
return cls._instances[cls]
17+
18+
19+
class ReverseProxied:
20+
"""
21+
Create a Proxy pattern https://microservices.io/patterns/apigateway.html.
22+
You can run the microservice A in your local machine in http://localhost:5000/my-endpoint/
23+
If you deploy your microservice, in some cases this microservice run behind a cluster, a gateway... and this
24+
gateway redirect traffic to the microservice with a specific path like yourdomian.com/my-ms-a/my-endpoint/.
25+
This class understand this path if the gateway send a specific header
26+
"""
27+
28+
def __init__(self, app):
29+
self.app = app
30+
31+
@staticmethod
32+
def _extract_prefix(environ):
33+
"""
34+
Get Path from environment from:
35+
- Traefik with HTTP_X_SCRIPT_NAME https://docs.traefik.io/v2.0/middlewares/headers/
36+
- Nginx and Ingress with HTTP_X_SCRIPT_NAME https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/
37+
- Apache with HTTP_X_SCRIPT_NAME https://stackoverflow.com/questions/55619013/proxy-and-rewrite-to-webapp
38+
- Zuul with HTTP_X_FORWARDER_PREFIX https://cloud.spring.io/spring-cloud-netflix/multi/multi__router_and_filter_zuul.html
39+
:param environ:
40+
:return:
41+
"""
42+
# Get path from Traefik, Nginx and Apache
43+
path = environ.get('HTTP_X_SCRIPT_NAME', '')
44+
if not path:
45+
# Get path from Zuul
46+
path = environ.get('HTTP_X_FORWARDED_PREFIX', '')
47+
if path and not path.startswith("/"):
48+
path = "/" + path
49+
return path
50+
51+
def __call__(self, environ, start_response):
52+
script_name = self._extract_prefix(environ)
53+
if script_name:
54+
environ['SCRIPT_NAME'] = script_name
55+
path_info = environ['PATH_INFO']
56+
if path_info.startswith(script_name):
57+
environ['PATH_INFO'] = path_info[len(script_name):]
58+
59+
scheme = environ.get('HTTP_X_SCHEME', '')
60+
if scheme:
61+
environ['wsgi.url_scheme'] = scheme
62+
return self.app(environ, start_response)

pyms/flask/services/swagger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def init_app(self, config, path):
8383

8484
# Fix Connexion issue https://github.com/zalando/connexion/issues/1135
8585
if application_root == "/":
86-
params["base_path"] = ""
86+
del params["base_path"]
8787

8888
app.add_api(**params)
8989
# Invert the objects, instead connexion with a Flask object, a Flask object with

tests/test_flask.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pyms.constants import CONFIGMAP_FILE_ENVIRONMENT
88
from pyms.flask.app import Microservice, config
99
from pyms.flask.services.driver import DriverService
10-
from tests.common import MyMicroserviceNoSingleton, MyMicroservice
10+
from tests.common import MyMicroservice
1111

1212

1313
def home():
@@ -73,6 +73,18 @@ def test_disabled_service(self):
7373
self.assertTrue(isinstance(self.app.ms.metrics, DriverService))
7474
assert "'MyMicroservice' object has no attribute 'metrics'" in str(excinfo.value)
7575

76+
def test_reverse_proxy(self):
77+
response = self.client.get('/my-proxy-path/ui/', headers={"X-Script-Name": "/my-proxy-path"})
78+
self.assertEqual(200, response.status_code)
79+
80+
def test_reverse_proxy_no_slash(self):
81+
response = self.client.get('/my-proxy-path/ui/', headers={"X-Script-Name": "my-proxy-path"})
82+
self.assertEqual(200, response.status_code)
83+
84+
def test_reverse_proxy_zuul(self):
85+
response = self.client.get('/my-proxy-path-zuul/ui/', headers={"X-Forwarded-Prefix": "my-proxy-path-zuul"})
86+
self.assertEqual(200, response.status_code)
87+
7688

7789
class MicroserviceTest(unittest.TestCase):
7890
"""

0 commit comments

Comments
 (0)