python-restx / flask-restx

Fork of Flask-RESTPlus: Fully featured framework for fast, easy and documented API development with Flask
https://flask-restx.readthedocs.io/en/latest/
Other
2.16k stars 335 forks source link

Swagger doc page wont render when deployed in kubernetes #610

Closed will-m-buchanan closed 4 months ago

will-m-buchanan commented 4 months ago

Ask a question I have written a Flask app using flask-restx. The docs render properly when I run locally, but not when deployed in k8s.

I run locally via a __main__ if in my main file:

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8899, debug=True)

This runs fine, and the swagger docs are rendered properly at 127.0.0.1:8899/

In my k8s deployment, I use gunicorn --bind 0.0.0.0:8899 ... to run the app, I have a Service that maps 8899 to 8080 and an Ingress that runs my Service on the path "/my-app(/|$)(.*)"

This all seems to work such that when I hit the app's endpoints in postman (i.e. my-host.com/my-app/endpointx), I get the responses I'm looking for. However, when I navigate to my-host.com/my-app/, the expected swagger docs do not render. Instead I get a blank page.

Investigating the html, I see that the <head> block is rendered properly (so, for instance, the page has <title>My App<\title>, as defined in the code with

api = Api(app, title="My App" ...)

But within the <body> block, the div which – locally – contains the main content: <div id="swagger-ui">, is completely empty in the k8s deployment.

The last difference I've been able to find is in swagger.json. The swagger json exists at my-host.com/my-app/swagger.json1. The big difference I notice is that at the top level of the json, locally I have "basePath": "\/", whereas in k8s I have "basePath": "/",. All other paths are similarly escaped locally but not in k8s. e.g. local:

    "paths": {
        "\/path\/endpoint": {

k8s:

"paths": {"/path/endpoint": {

Any ideas what is going on?

1: when I navigate here in my browser the file renders in a single wrapped line, whereas locally http://127.0.0.1:8899/swagger.json is pretty-printed. This isn't a big deal that I need to solve I don't think, but it is a curious difference between the local run and the k8s deployment

peter-doggart commented 4 months ago

I think this is related to flask being behind a reserve proxy on k8 that isn't present in your development environment. Have you tried configuring Flask's ProxyFix? I think this is possibly the same issue as was discussed over on this issue, with a few alternative solutions proposed: https://github.com/python-restx/flask-restx/issues/58

will-m-buchanan commented 4 months ago

Thanks @peter-doggart. That issue was very helpful. What worked for me was a combination of solutions:

I added

app.wsgi_app = ProxyFix(app.wsgi_app, x_for=0, x_proto=0, x_host=0, x_port=0, x_prefix=1)

after defining app, then I also included

@api.documentation
def custom_ui():
    """Use a custom swagger UI endpoint.

    Updates specs_url to point to the custom endpoint.
    """
    return render_template("swagger-ui.html",
                           title=api.title,
                           specs_url=APPLICATION_ROOT + SWAGGER_SPEC)

@api.route(SWAGGER_SPEC)
@api.hide
class SwaggerDoc(Resource):
    """Endpoint for rendering JSON swagger spec in browser"""
    @api.doc(security=[])
    def get(self):
        """Modify apischema to customize swagger spec"""
        apischema = copy.copy(api.__schema__)
        apischema["basePath"] = APPLICATION_ROOT + "/"

        return apischema

as per @CpiliotisSTLA's suggestion. Finally, I added

nginx.ingress.kubernetes.io/x-forwarded-prefix: /my-app

to my Ingress' metadata.annotations, as per @rodrigocaus' comment. All that seemed to create a cocktail that worked for me!

Joeylu-master commented 4 months ago

@will-m-buchanan, Where are the APPLICATION_ROOT and SWAGGER_SPEC definitions?

will-m-buchanan commented 4 months ago

APPLICATION_ROOT is the root path named in the ingress (/my-app in my example's case), and SWAGGER_SPEC is the endpoint that the UI will call on to get the JSON that renders the API docs (/swagger-spec in my case)