Hub-Managed Services in Kubernetes

Hello everyone,

for some time now, the configuration ‘extra_handlers’ to register additional HTTP endpoints has been deprecated.
Before the configuration can no longer be used in a future update, I am currently trying to convert all created handlers to services.
As an example, I first wanted to test the whoami service. Unfortunately, I am already failing to get the service to run.

Since I am using Kubernetes, it seems to be much more complex to get this simple example running.
I tried to rebuild whoami using the example Hub-managed services in z2jh.
If I then click on the service I will be redirected from /service/whoami → to /hub/login?next=%2Fservices%2Fwhoami%2F → to /hub.
Here is the customised code:

jupyterhub_config.py

import sys


c.JupyterHub.services += [
    {
        'name': 'whoami',
        'url': 'http://hub:8181',
        'command': [sys.executable, '/services/whoami.py'],
        'display': True,
    }
]

c.JupyterHub.load_roles += [
    {
        "name": "user",
        # grant all users access to all services
        "scopes": ["access:services", "self"],
    }
]

whoami.py

class WhoAmIHandler(HubAuthenticated, RequestHandler):
    @authenticated
    def get(self):
        user_model = self.get_current_user()
        self.set_header('content-type', 'application/json')
        self.write(json.dumps(user_model, indent=1, sort_keys=True))


def main():
    app = Application(
        [
            (os.environ['JUPYTERHUB_SERVICE_PREFIX'] + '/?', WhoAmIHandler),
            (r'.*', WhoAmIHandler),
        ]
    )

    http_server = HTTPServer(app)
    http_server.listen(port=8181, address="0.0.0.0")

    IOLoop.current().start()


if __name__ == '__main__':
    main()

config.yaml

proxy:
  chp:
    networkPolicy:
      egress:
        - to:
            - podSelector:
                matchLabels:
                  app.kubernetes.io/name: jupyterhub
                  app.kubernetes.io/component: hub
          ports:
            - port: 8181
...
hub:
...
  networkPolicy:
    ingress:
      - ports:
          - port: 8181
        from:
          - podSelector:
              matchLabels:
                hub.jupyter.org/network-access-hub: "true"
  service:
    extraPorts:
      - port: 8181
        targetPort: 8181
        name: whoami
...

Log

Waiting 1s for server at http://hub:8181/services/whoami/
Server at http://hub:8181/services/whoami/ responded with 302

And I see that render_template from BaseHandler in this post then seems to cause other unresolved? problems.
But I won’t be able to take care of that until I get whoami up and running.

In any case, thanks for any help.

I guess this comment on the service file indicates that you need to access this service only in API mode.

If you would like to use it from UI, I think you need to use oauth variant of the service.

1 Like

Thanks, I had overlooked that it is only for API use.
I also had to add ‘oauth_no_confirm’: True in the c.JupyterHub.services block.

Now I’m rebuilding one of my own sites and ran into the same problem as here.

class ThingsHandler(HubOAuthenticated, BaseHandler, ABC):
    @authenticated
    def get(self):
        user = self.get_current_user()
        auth_state = user.get_auth_state()

        # Retrieve information from auth_state and generate information to be displayed to the user
        # .......

        html = self.render_template(
            'things_list.html',
            auth_state=auth_state,
            user=user,
            things_list=things_list,
            is_generated_error=is_generated_error
        )
        self.finish(html)
2024-05-31 12:24:29	Uncaught exception
2024-05-31 12:24:29	Traceback (most recent call last):
2024-05-31 12:24:29	  File "/usr/local/lib/python3.11/site-packages/tornado/http1connection.py", line 276, in _read_message
2024-05-31 12:24:29	    delegate.finish()
2024-05-31 12:24:29	  File "/usr/local/lib/python3.11/site-packages/tornado/routing.py", line 268, in finish
2024-05-31 12:24:29	    self.delegate.finish()
2024-05-31 12:24:29	  File "/usr/local/lib/python3.11/site-packages/tornado/web.py", line 2399, in finish
2024-05-31 12:24:29	    self.execute()
2024-05-31 12:24:29	  File "/usr/local/lib/python3.11/site-packages/tornado/web.py", line 2421, in execute
2024-05-31 12:24:29	    self.handler = self.handler_class(
2024-05-31 12:24:29	                   ^^^^^^^^^^^^^^^^^^^
2024-05-31 12:24:29	  File "/usr/local/lib/python3.11/site-packages/tornado/web.py", line 232, in __init__
2024-05-31 12:24:29	    self.clear()
2024-05-31 12:24:29	  File "/usr/local/lib/python3.11/site-packages/tornado/web.py", line 333, in clear
2024-05-31 12:24:29	    self.set_default_headers()
2024-05-31 12:24:29	  File "/usr/local/lib/python3.11/site-packages/jupyterhub/handlers/base.py", line 246, in set_default_headers
2024-05-31 12:24:29	    self.set_header('Content-Security-Policy', self.content_security_policy)
2024-05-31 12:24:29	                                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2024-05-31 12:24:29	  File "/usr/local/lib/python3.11/site-packages/jupyterhub/handlers/base.py", line 222, in content_security_policy
2024-05-31 12:24:29	    ["frame-ancestors 'none'", "report-uri " + self.csp_report_uri]
2024-05-31 12:24:29	                                               ^^^^^^^^^^^^^^^^^^^
2024-05-31 12:24:29	  File "/usr/local/lib/python3.11/site-packages/jupyterhub/handlers/base.py", line 208, in csp_report_uri
2024-05-31 12:24:29	    'csp_report_uri', url_path_join(self.hub.base_url, 'security/csp-report')
2024-05-31 12:24:29	                                    ^^^^^^^^
2024-05-31 12:24:29	  File "/usr/local/lib/python3.11/site-packages/jupyterhub/handlers/base.py", line 168, in hub
2024-05-31 12:24:29	    return self.settings['hub']
2024-05-31 12:24:29	           ~~~~~~~~~~~~~^^^^^^^
2024-05-31 12:24:29	KeyError: 'hub'

But if I don’t use the BaseHandler but the RequestHandler I can’t access the auth_state of the user and it can’t render a jinja2 template.

tornado.template.ParseError: unknown operator: 'endblock' at things_list.html:5

endblock is a Jinja2 addition to Tornado templates:

If you’re only using standard Tornado templates you can just use {% end %} instead

1 Like

Thanks, but the RequestHandler does not recognise the keys jinja2_env_sync and jinja2_env.
As far as I can see, I would have to write a completely new init_tornado_settings, which then only applies to the one service.
I was currently rewriting the ‘page.html’ as a test, because the RequestHandler throws an error every second line. Just replacing everything with {% end %} is not nearly enough. But that’s not the point. Maybe I am missing something, but it seems that it is currently not possible to convert a normal page from extra_handlers with self.render_template and auth_state to a service.
If one of you has a working example, I would be very happy about it.

You’re right. A service is an independent process, so you either need to implement or import everything.

As pointed out in

extra_handlers won’t be removed, but is considered a last resort.

The JupyterHub Tornado templates, variables and associated configuration are internal implementation details so whilst there’s nothing to stop you extending them there’s no guarantee it will work in the future.