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
diff --git a/doc/source/admin/components.rst b/doc/source/admin/components.rst
index 84ebc10..b555abc 100644
--- a/doc/source/admin/components.rst
+++ b/doc/source/admin/components.rst
@@ -618,9 +618,13 @@
 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 @@
 
       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 ae7d571..735b315 100644
--- a/doc/source/admin/installation.rst
+++ b/doc/source/admin/installation.rst
@@ -67,3 +67,57 @@
 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 602209f..75cf8f3 100644
--- a/tests/unit/test_web.py
+++ b/tests/unit/test_web.py
@@ -254,7 +254,7 @@
         self.assertEqual(
             info, {
                 "info": {
-                    "endpoint": "http://localhost:%s" % self.port,
+                    "rest_api_url": None,
                     "capabilities": {
                         "job_history": False
                     },
@@ -275,7 +275,7 @@
         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 44e8d06..a434834 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -3214,16 +3214,16 @@
 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 @@
 
     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 31eac7d..8f4bf72 100755
--- a/zuul/web/__init__.py
+++ b/zuul/web/__init__.py
@@ -261,19 +261,12 @@
         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):