Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Web UI support for API Server #839

Merged
merged 7 commits into from
Jul 7, 2020
Merged

Custom Web UI support for API Server #839

merged 7 commits into from
Jul 7, 2020

Conversation

Korusuke
Copy link
Contributor

@Korusuke Korusuke commented Jun 24, 2020

Description

Closes #739

Requires a directory to be passed containing index.html and static dir.
Note: Any files not inside static will not be accessible.
Example deployment: https://bentoml-her0ku-mtu5mtkwnjq0nao.herokuapp.com/

The example deployment is still WIP and has no loading indicator/error handling.
Please recommend changes for the demo deployment so the same can be used for documentation.

Motivation and Context

Implements #739

How Has This Been Tested?

Tested locally and deployed on Heroku

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • New feature and improvements (non-breaking change which adds/improves functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Code Refactoring (internal change which is not user facing)
  • Documentation
  • Test, CI, or build

Component(s) if applicable

  • BentoService (service definition, dependency management, API input/output adapters)
  • Model Artifact (model serialization, multi-framework support)
  • Model Server (mico-batching, dockerisation, logging, OpenAPI, instruments)
  • YataiService gRPC server (model registry, cloud deployment automation)
  • YataiService web server (nodejs HTTP server and web UI)
  • Internal (BentoML's own configuration, logging, utility, exception handling)
  • BentoML CLI

Checklist:

  • My code follows the bentoml code style, both ./dev/format.sh and
    ./dev/lint.sh script have passed
    (instructions).
  • My change reduces project test coverage and requires unit tests to be added
  • I have added unit tests covering my code change
  • My change requires a change to the documentation
  • I have updated the documentation accordingly

@@ -177,6 +177,7 @@ def load(bundle_path):

svc_cls = load_bento_service_class(bundle_path)
svc = svc_cls()
svc._path = bundle_path
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this, there may be a better way of getting the bundle path.

Copy link
Member

@parano parano Jun 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use svc._bento_service_bundle_path, which is equivalent and used for loading artifacts

mimetype="text/html",
)
if hasattr(self.bento_service, '_static_files'):
return render_template('index.html')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case custom UI is enabled, do we want to also have swagger UI on some different endpoint?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that'd be great for debugging, I think we can add a /swagger.html endpoint which always renders the swagger UI

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it actually need the redner_template method? the user can not really use the templating functionality right? I'm more inclined to simply put all the static files under the static directory if they are just static files

Copy link
Contributor Author

@Korusuke Korusuke Jun 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense!

so now the dir structure will be:

webui_dir/
     |-- js/
     |--css/
     |--imgs/
     |--index.html

Here webui_dir is the supplied path by the user.

instead of render_template now it uses send_from_directory

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that'd be great for debugging, I think we can add a /swagger.html endpoint which always renders the swagger UI

In my opinion, /docs endpoint would be better for swagger UI since this is the same as what fastapi uses. And also if in future we change from swagger to something else, the path can remain the same.

So /docs for the HTML page and /docs.json for openapi json.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it should serve all files under the static_folder directory not just index.html, you may also need to specify static_url_path: app = Flask(__name__, static_folder='../web_client', static_url_path='/')
and in your index view function do something like app.send_static_file('index.html')

yeh, this is working currently!

Adding static_url_path might break the swagger js/css files tho, you may need to adjust accordingly

Fixed that using send_from_directory and changing swagger static dir name.

I think this way it is more friendly to the web frontend developer, for example, if the user creates the frontend project with something like https://github.com/facebook/create-react-app or use webpack to build their js project, they can simply supply the built dist directory for BentoML to serve as static files, and everything should just work.

Yep, I am trying to achieve this...currently the only issue is serving any_path/index.html for any_path/, any_path/index.html and also any_path/index.

trying to find some better solution instead of just writing down all the cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One another doubt is - should we allow user-defined routes to override the system defined (predefined by bento)?

currently, if the user-defined function name is index then the server doesn't start, so should we allow to override that?
this may provide some extra flexibility but may even be a bit confusing at times.

Copy link
Member

@parano parano Jul 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently, if the user-defined function name is index then the server doesn't start, so should we allow to override that?

.. the server doesn't start .. is that because it has added two rules handling the same path? I think it is probably not a good idea to allow user-defined @api to override the system ones. Maybe we should validate the @api name and make sure it is not in the reserved name list, such as index, healthz, metrics etc. Currently there's only a isidentifier check, you can add this validation here: https://github.com/bentoml/BentoML/blob/v0.8.2/bentoml/service.py#L311

Copy link
Contributor Author

@Korusuke Korusuke Jul 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.. the server doesn't start .. is that because it has added two rules handling the same path?

Yep

Okey great!
should I include this in the same PR or create another issue/PR for this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Korusuke a smaller PR that adds the validation would be nice, but it's ok to just include it in this PR as well, whichever is easier for you

@codecov
Copy link

codecov bot commented Jun 24, 2020

Codecov Report

Merging #839 into master will increase coverage by 4.76%.
The diff coverage is 70.21%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #839      +/-   ##
==========================================
+ Coverage   55.89%   60.65%   +4.76%     
==========================================
  Files         114      118       +4     
  Lines        8400     8002     -398     
==========================================
+ Hits         4695     4854     +159     
+ Misses       3705     3148     -557     
Impacted Files Coverage Δ
bentoml/__init__.py 100.00% <ø> (ø)
bentoml/saved_bundle/bundler.py 88.63% <16.66%> (-5.20%) ⬇️
bentoml/server/bento_api_server.py 74.35% <62.50%> (-2.12%) ⬇️
bentoml/service.py 89.12% <100.00%> (-1.72%) ⬇️
bentoml/yatai/proto/yatai_service_pb2_grpc.py 29.16% <0.00%> (-13.04%) ⬇️
bentoml/yatai/validator/deployment_pb_validator.py 84.00% <0.00%> (-11.00%) ⬇️
bentoml/adapters/tensorflow_tensor_input.py 60.75% <0.00%> (-9.25%) ⬇️
bentoml/adapters/utils.py 83.67% <0.00%> (-7.38%) ⬇️
bentoml/yatai/deployment_utils.py 77.08% <0.00%> (-6.64%) ⬇️
bentoml/yatai/deployment/operator.py 55.17% <0.00%> (-6.37%) ⬇️
... and 64 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2f138fc...e89f7fc. Read the comment docs.

Copy link
Contributor

@flosincapite flosincapite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool! The demo looks slick.

@Korusuke Korusuke marked this pull request as ready for review June 25, 2020 22:00
@Korusuke Korusuke changed the title [WIP] Custom Web UI support for API Server Custom Web UI support for API Server Jun 25, 2020
@parano parano self-requested a review June 30, 2020 17:28
@@ -553,6 +574,10 @@ def artifacts(self):
def env(self):
return self._env

@property
def webui(self):
Copy link
Member

@parano parano Jun 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webui and _static_files here seems to be referencing the path to web UI static files. maybe rename both to just web_static_content? This API technically can also be used to host other static files right? e.g. it can be a just a markdown documentation page for using this API and allow end-user to download example input json/image files

@@ -107,15 +117,18 @@ def start(self):
self.app.run(port=self.port, threaded=False)

@staticmethod
def index_view_func():
def index_view_func(bento_service):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider refactor this to two different functions? swagger_ui_func and user_defined_ui_func, and decide which function to use in the setup_routes method?

@Korusuke
Copy link
Contributor Author

Korusuke commented Jul 1, 2020

There are a bunch of new changes to try and support everything. Below are some reasons for doing what I have done!

  • send_from_directory: it's just a more secure version of send_static_file which checks if the file actually belongs to the specified dir

  • static_folder = None: Without this, it was just not working as the inbuilt function interfered with /<path:file_path> route.

  • and lastly, the reason for setting up so many routes is to support almost all possible cases!

Supported paths for any type of static files:

  • /styles.css
  • /
  • /any_path/ -> this will try to find the file and if not found will give /any_path/index.html instead
  • /any_path/index -> returns /any_path/index.html

Known unsupported paths:

  • /index (I just realized this is not supported)

Edit: Just to be clear / and /index.html works just /index doesn't work which was not supported previously too.

@Korusuke
Copy link
Contributor Author

Korusuke commented Jul 1, 2020

I think the tests failed since bento_service._bento_service_bundle_path is undefined in the test environment.

So should I add a check? or define _bento_service_bundle_path in the tests?


self.app = Flask(
self.static_path = os.path.join(
self.bento_service._bento_service_bundle_path,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_bento_service_bundle_path is the only set when loading from a saved BentoService bundle.

Maybe you could add a method get_web_static_content_path:

def get_web_static_content_path(self):
   if self.bento_service._bento_service_bundle_path:
      return os.path.join(
            self.bento_service._bento_service_bundle_path, 'web_static_content'
      )
    elif: bento_service.web_static_content:
        return os.path.join(os.getcwd(), bento_service.web_static_content)
    else:
        return None / raise ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should fix the error you see in the unit test, it will also allow the user to test the BentoAPIServer without saving and loading, this can be super useful for debugging in an interactive session or Jupyter notebook

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is perfect!!
I wanted something like this but was not sure how to implement it😅

@@ -172,8 +200,29 @@ def setup_routes(self):
/classify
/predict
"""
if self.static_path:
self.app.add_url_rule(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some inline comments about these three url_rule here? reading this code it is not immediately clear to me what it does

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry...this is a neccessary evil😅 as flask doesn't handle it automagically.

            # serve static files for any given path
            # this will also serve index.html from directory /any_path/
            # for path as /any_path/
            self.app.add_url_rule(
                "/<path:file_path>",
                "static_proxy",
                partial(self.static_serve, self.static_path),
            )
            # serve index.html from the directory /any_path
            # for path as /any_path/index
            self.app.add_url_rule(
                "/<path:file_path>/index",
                "static_proxy2",
                partial(self.static_serve, self.static_path),
            )
            # serve index.html from root directory for path as /
            self.app.add_url_rule(
                "/", "index", partial(self.index_view_func, self.static_path)
            )

Any changes to this? Sorry but I am still trying to improve my commenting and documentation skills.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Korusuke that looks great, adding a little context here helps a lot!


@property
def get_web_static_content_path(self):
if self._bento_service_bundle_path and self.name:
Copy link
Member

@parano parano Jul 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be if self.web_static_content and self._bento_service_bundle_path to make BentoService that does not have web_static_content to show an index page properly right?

self.name should always be presented

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be if self.web_static_content and self._bento_service_bundle_path to make BentoService that does not have web_static_content to show an index page properly right?

Yeh, sorry missed that in the final commit

self.name should always be presented

I swear previously I was getting an error on self.name but now it's working😅
I think I may have used _bento_service_name while testing and cause that was giving error I replaced everything with self.name and then never checked it properly.

Args:
web_static_content: path to directory containg index.html and static dir

>>> @webui('./ui/')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit inclined to just name it @web_static_content('./ui/') as the user-facing API, thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine either way, will change it to web_static_content

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good! I was thinking that users can potentially use this to host other files other than web UI files, e.g., using this to host test input files, JSON config file(some use cases require exposing server-side config to the client), or even pdf documentation, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, the only downside is users currently cannot specify custom / path. So by default bento will look for index.html.
So for example, if someone is trying to serve index.json at /, then this won't work but /index.json will work.

We can add option to customize this, but I am more inclined not to as for production deployment it is better to just use nginx or something as serving static files via flask is both slow and processor intensive.

dest_web_static_content_dir = os.path.join(
module_base_path, 'web_static_content'
)
# os.mkdir(dest_web_dir)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove?

# copy custom web_static_content if enabled
if bento_service.web_static_content:
src_web_static_content_dir = os.path.join(
os.getcwd(), bento_service.web_static_content
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a os.path.isdir(src_web_static_content_dir) check here? , would be great to also support absolute path

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, it already supports absolute path. os.path.join is very smart!

return self._web_static_content

@property
def get_web_static_content_path(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usually property won't have the verb get_, I feel making this a method instead of a property would be better

@parano
Copy link
Member

parano commented Jul 7, 2020

@Korusuke finished a pass with a few minor comments, otherwise this looks great! It would be great if you could add your web UI demo to the bentoml/gallery repository, it looks super cool!

@parano
Copy link
Member

parano commented Jul 7, 2020

@Korusuke Thanks again for the awesome contribution, can't wait to try out building some ML-powered web apps with this feature. Merging now!

@parano parano merged commit 4b68b1a into bentoml:master Jul 7, 2020
@parano
Copy link
Member

parano commented Jul 7, 2020

Would be great to add some integration test for this later, currently, the related test infra are still working-in-progress #855

@Korusuke Korusuke mentioned this pull request Jul 8, 2020
18 tasks
aarnphm pushed a commit to aarnphm/BentoML that referenced this pull request Jul 29, 2022
* Custom web UI

* Fix _path

* Add _static_files property

* static serve

* Refactor

* Fix tests

* Address comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Custom service web UI to replace default swagger UI
3 participants