Browse Source

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
* 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

Change-Id: I6b85a93db6c70c997bbff1329373fbfc2d1007c6
Monty Taylor 3 years ago
No known key found for this signature in database GPG Key ID: 7BAE94BC7141A594
5 changed files with 84 additions and 27 deletions
  1. +12
  2. +54
  3. +2
  4. +14
  5. +2

+ 12
- 3
doc/source/admin/components.rst View File

@ -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

+ 54
- 0
doc/source/admin/installation.rst View File

@ -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. 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.
Serve a Zuul dashboard from a location below the root URL as part of
presenting integration with other application. 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
.. TODO: Fill in specifics in the next patch

+ 2
- 2
tests/unit/ View File

@ -254,7 +254,7 @@ class TestInfo(BaseTestWeb):
info, {
"info": {
"endpoint": "http://localhost:%s" % self.port,
"rest_api_url": None,
"capabilities": {
"job_history": False
@ -275,7 +275,7 @@ class TestInfo(BaseTestWeb):
info, {
"info": {
"endpoint": "http://localhost:%s" % self.port,
"rest_api_url": None,
"tenant": "tenant-one",
"capabilities": {
"job_history": False

+ 14
- 13
zuul/ View File

@ -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 '<WebInfo 0x%x capabilities=%s>' % (
@ -3231,32 +3231,33 @@ class WebInfo(object):
def copy(self):
return WebInfo(
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

+ 2
- 9
zuul/web/ View File

@ -261,19 +261,12 @@ class ZuulWeb(object):
return await self.log_streaming_handler.processRequest(
async def _handleRootInfo(self, request):
info =
info.endpoint = str(request.url.parent)
return self._handleInfo(info)
def _handleRootInfo(self, request):
return self._handleInfo(
def _handleTenantInfo(self, request):
info =
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
info.endpoint = str(request.url.parent.parent.parent)
return self._handleInfo(info)
def _handleInfo(self, info):