From f93c2fb5f1751a69f8a73c2626a4638a5c0ed734 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Feb 2018 14:38:47 -0600 Subject: [PATCH] Make Info.endpoint a config override The endpoint field in the info payload is intended to help the javascript web code find out where the API endpoint is, and to allow people who are deploying html/js as static assets to an external web server drop a json file in that deployment to tell it where their zuul-web server is. The only scenario where the endpoint information as served by the /info or /{tenant}/info endpoints of zuul-web is useful is same-host/single-apache deployments that are hosted on a sub-url ... and unfortunately, it's not possible for the aiohttp code to be aware of such suburl deployments from http headers. request.url has the actual location (such as http://localhost:8080/info) and X-Forwarded-Host will only contain the host, not the path. The actual important aspects of the payload are: * A payload always be able to be found no matter the deployment scenario. * That a deployer can communicate to the javascript code the root of the REST API in the scenarios where relative paths will resolve to the incorrect thing. With that in mind, change the Info.endpoint field returned by zuul-web to default to None (or actually json null), or to a value provided by the deployer in the zuul.conf file similar to websocket_url. This way the web app can view 'null' as meaning "I'm deployed in such a manner that relative paths are the correct thing to fetch from" and a value as "the deployer has told me explicitly where to fetch from, I will join my relative paths to the value first." Because it is a value that is provided by the deployer if it is to exist, rename it to "rest_api_url" to better match websocket_url and stats_url. Change-Id: I6b85a93db6c70c997bbff1329373fbfc2d1007c6 --- doc/source/admin/components.rst | 15 +++++++-- doc/source/admin/installation.rst | 54 +++++++++++++++++++++++++++++++ tests/unit/test_web.py | 4 +-- zuul/model.py | 27 ++++++++-------- zuul/web/__init__.py | 11 ++----- 5 files changed, 84 insertions(+), 27 deletions(-) diff --git a/doc/source/admin/components.rst b/doc/source/admin/components.rst index 84ebc10617..b555abc884 100644 --- a/doc/source/admin/components.rst +++ b/doc/source/admin/components.rst @@ -618,9 +618,13 @@ and ``zuul-executor unverbose``. Web Server ---------- -The Zuul web server currently acts as a websocket interface to live log -streaming. Eventually, it will serve as the single process handling all -HTTP interactions with Zuul. +.. TODO: Turn REST API into a link to swagger docs when we grow them + +The Zuul web server serves as the single process handling all HTTP +interactions with Zuul. This includes the websocket interface for live +log streaming, the REST API and the html/javascript dashboard. All three are +served as a holistic web application. For information on additional supported +deployment schemes, see :ref:`web-deployment-options`. Web servers need to be able to connect to the Gearman server (usually the scheduler host). If the SQL reporter is used, they need to be @@ -655,6 +659,11 @@ sections of ``zuul.conf`` are used by the web server: Port to use for web server process. + .. attr:: rest_api_url + + Base URL on which the zuul-web REST service is exposed, if different + than the base URL where the web application is hosted. + .. attr:: websocket_url Base URL on which the websocket service is exposed, if different diff --git a/doc/source/admin/installation.rst b/doc/source/admin/installation.rst index ae7d571de5..735b315636 100644 --- a/doc/source/admin/installation.rst +++ b/doc/source/admin/installation.rst @@ -67,3 +67,57 @@ that version of Ansible in its python package metadata, and normally the correct version will be installed automatically with Zuul. Because of the close integration of Zuul and Ansible, attempting to use other versions of Ansible with Zuul is not recommended. + +.. _web-deployment-options: + +Web Deployment Options +====================== + +The ``zuul-web`` service provides an web dashboard, a REST API and a websocket +log streaming service as a single holistic web application. For production use +it is recommended to run it behind a reverse proxy, such as Apache or Nginx. + +More advanced users may desire to do one or more exciting things such as: + +White Label + Serve the dashboard of an individual tenant at the root of its own domain. + https://zuul.openstack.org is an example of a Zuul dashboard that has been + white labeled for the ``openstack`` tenant of its Zuul. + +Static Offload + Shift the duties of serving static files, such as HTML, Javascript, CSS or + images either to the Reverse Proxy server or to a completely separate + location such as a Swift Object Store or a CDN-enabled static web server. + +Sub-URL + Serve a Zuul dashboard from a location below the root URL as part of + presenting integration with other application. + https://softwarefactory-project.io/zuul3/ is an example of a Zuul dashboard + that is being served from a Sub-URL. + +None of those make any sense for simple non-production oriented deployments, so +all discussion will assume that the ``zuul-web`` service is exposed via a +Reverse Proxy. Where rewrite rule examples are given, they will be given +with Apache syntax, but any other Reverse Proxy should work just fine. + +Basic Reverse Proxy +------------------- + +Using Apache as the Reverse Proxy requires the ``mod_proxy``, +``mod_proxy_http`` and ``mod_proxy_wstunnel`` modules to be installed and +enabled. Static Offload and White Label additionally require ``mod_rewrite``. + +Static Offload +-------------- + +.. TODO: Fill in specifics in the next patch + +White Labeled Tenant +-------------------- + +.. TODO: Fill in specifics in the next patch + +Sub-URL +------- + +.. TODO: Fill in specifics in the next patch diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py index 602209f00e..75cf8f3427 100644 --- a/tests/unit/test_web.py +++ b/tests/unit/test_web.py @@ -254,7 +254,7 @@ class TestInfo(BaseTestWeb): self.assertEqual( info, { "info": { - "endpoint": "http://localhost:%s" % self.port, + "rest_api_url": None, "capabilities": { "job_history": False }, @@ -275,7 +275,7 @@ class TestInfo(BaseTestWeb): self.assertEqual( info, { "info": { - "endpoint": "http://localhost:%s" % self.port, + "rest_api_url": None, "tenant": "tenant-one", "capabilities": { "job_history": False diff --git a/zuul/model.py b/zuul/model.py index 44e8d06f05..a434834611 100644 --- a/zuul/model.py +++ b/zuul/model.py @@ -3214,16 +3214,16 @@ class Capabilities(object): class WebInfo(object): """Information about the system needed by zuul-web /info.""" - def __init__(self, websocket_url=None, endpoint=None, + def __init__(self, websocket_url=None, rest_api_url=None, capabilities=None, stats_url=None, stats_prefix=None, stats_type=None): self.capabilities = capabilities or Capabilities() - self.websocket_url = websocket_url - self.stats_url = stats_url + self.rest_api_url = rest_api_url self.stats_prefix = stats_prefix self.stats_type = stats_type - self.endpoint = endpoint + self.stats_url = stats_url self.tenant = None + self.websocket_url = websocket_url def __repr__(self): return '' % ( @@ -3231,32 +3231,33 @@ class WebInfo(object): def copy(self): return WebInfo( - websocket_url=self.websocket_url, - endpoint=self.endpoint, - stats_url=self.stats_url, + capabilities=self.capabilities.copy(), + rest_api_url=self.rest_api_url, stats_prefix=self.stats_prefix, stats_type=self.stats_type, - capabilities=self.capabilities.copy()) + stats_url=self.stats_url, + websocket_url=self.websocket_url) @staticmethod def fromConfig(config): return WebInfo( - websocket_url=get_default(config, 'web', 'websocket_url', None), - stats_url=get_default(config, 'web', 'stats_url', None), + rest_api_url=get_default(config, 'web', 'rest_api_url', None), stats_prefix=get_default(config, 'statsd', 'prefix'), stats_type=get_default(config, 'web', 'stats_type', 'graphite'), + stats_url=get_default(config, 'web', 'stats_url', None), + websocket_url=get_default(config, 'web', 'websocket_url', None), ) def toDict(self): d = dict() + d['capabilities'] = self.capabilities.toDict() + d['rest_api_url'] = self.rest_api_url d['websocket_url'] = self.websocket_url stats = dict() - stats['url'] = self.stats_url stats['prefix'] = self.stats_prefix stats['type'] = self.stats_type + stats['url'] = self.stats_url d['stats'] = stats - d['endpoint'] = self.endpoint - d['capabilities'] = self.capabilities.toDict() if self.tenant: d['tenant'] = self.tenant return d diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py index 31eac7d886..8f4bf7286a 100755 --- a/zuul/web/__init__.py +++ b/zuul/web/__init__.py @@ -261,19 +261,12 @@ class ZuulWeb(object): return await self.log_streaming_handler.processRequest( request) - async def _handleRootInfo(self, request): - info = self.info.copy() - info.endpoint = str(request.url.parent) - return self._handleInfo(info) + def _handleRootInfo(self, request): + return self._handleInfo(self.info) def _handleTenantInfo(self, request): info = self.info.copy() info.tenant = request.match_info["tenant"] - # yarl.URL.parent on a root url returns the root url, so this is - # both safe and accurate for white-labeled tenants like OpenStack, - # zuul-web running on / and zuul-web running on a sub-url like - # softwarefactory-project.io - info.endpoint = str(request.url.parent.parent.parent) return self._handleInfo(info) def _handleInfo(self, info):