diff --git a/doc/source/admin/installation.rst b/doc/source/admin/installation.rst
index 6ba6def8e2..3be964a2f1 100644
--- a/doc/source/admin/installation.rst
+++ b/doc/source/admin/installation.rst
@@ -86,8 +86,11 @@ White Label
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.
+ images to the Reverse Proxy server.
+
+Static External
+ Serve the static files from a completely separate location that does not
+ support programmatic rewrite rules such as a Swift Object Store.
Sub-URL
Serve a Zuul dashboard from a location below the root URL as part of
@@ -107,4 +110,64 @@ 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``.
-.. TODO(mordred): Fill in specifics for all three methods
+All of the cases require a rewrite rule for the websocket streaming, so the
+simplest reverse-proxy case is::
+
+ RewriteEngine on
+ RewriteRule ^/api/tenant/(.*)/console-stream ws://localhost:9000/api/tenant/$1/console-stream [P]
+ RewriteRule ^/(.*)$ http://localhost:9000/$1 [P]
+
+
+Static Offload
+--------------
+
+To have the Reverse Proxy serve the static html/javscript assets instead of
+proxying them to the REST layer, register the location where you unpacked
+the web application as the document root and add a simple rewrite rule::
+
+ DocumentRoot /var/lib/html
+
+ Require all granted
+
+ RewriteEngine on
+ RewriteRule ^/t/.*/(.*)$ /$1 [L]
+ RewriteRule ^/api/tenant/(.*)/console-stream ws://localhost:9000/api/tenant/$1/console-stream [P]
+ RewriteRule ^/api/(.*)$ http://localhost:9000/api/$1 [P]
+
+White Labeled Tenant
+--------------------
+
+Running a white-labeled tenant is similar to the offload case, but adds a
+rule to ensure connection webhooks don't try to get put into the tenant scope.
+
+.. note::
+
+ It's possible to do white-labelling without static offload, but it is more
+ complex with no benefit.
+
+Assuming the zuul tenant name is "example", the rewrite rules are::
+
+ DocumentRoot /var/lib/html
+
+ Require all granted
+
+ RewriteEngine on
+ RewriteRule ^/api/connection/(.*)$ http://localhost:9000/api/connection/$1 [P]
+ RewriteRule ^/api/console-stream ws://localhost:9000/api/tenant/example/console-stream [P]
+ RewriteRule ^/api/(.*)$ http://localhost:9000/api/tenant/example/$1 [P]
+
+Static External
+---------------
+
+.. note::
+
+ Hosting zuul dashboard on an external static location that does not support
+ dynamic url rewrite rules only works for white-labeled deployments.
+
+In order to serve the zuul dashboard code from an external static location,
+``ZUUL_API_URL`` must be set at javascript build time by passing the
+``--define`` flag to the ``npm build:dist`` command.
+
+.. code-block:: bash
+
+ npm build:dist -- --define "ZUUL_API_URL='http://zuul-web.example.com'"
diff --git a/tests/base.py b/tests/base.py
index 123d98a149..fb0d15076b 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -1031,7 +1031,7 @@ class FakeGithubConnection(githubconnection.GithubConnection):
if use_zuulweb:
return requests.post(
- 'http://127.0.0.1:%s/connection/%s/payload'
+ 'http://127.0.0.1:%s/api/connection/%s/payload'
% (self.zuul_web_port, self.connection_name),
json=data, headers=headers)
else:
@@ -1863,7 +1863,8 @@ class ZuulWebFixture(fixtures.Fixture):
# Start the web server
self.web = zuul.web.ZuulWeb(
listen_address='127.0.0.1', listen_port=0,
- gear_server='127.0.0.1', gear_port=self.gearman_server_port)
+ gear_server='127.0.0.1', gear_port=self.gearman_server_port,
+ info=zuul.model.WebInfo())
loop = asyncio.new_event_loop()
loop.set_debug(True)
ws_thread = threading.Thread(target=self.web.run, args=(loop,))
diff --git a/tests/unit/test_streaming.py b/tests/unit/test_streaming.py
index e17fc52443..bba77e6761 100644
--- a/tests/unit/test_streaming.py
+++ b/tests/unit/test_streaming.py
@@ -170,7 +170,7 @@ class TestStreaming(tests.base.AnsibleZuulTestCase):
def runWSClient(self, build_uuid, event):
async def client(loop, build_uuid, event):
- uri = 'http://[::1]:9000/tenant-one/console-stream'
+ uri = 'http://[::1]:9000/api/tenant/tenant-one/console-stream'
try:
session = aiohttp.ClientSession(loop=loop)
async with session.ws_connect(uri) as ws:
diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py
index 13924c4e1a..b6d0035634 100644
--- a/tests/unit/test_web.py
+++ b/tests/unit/test_web.py
@@ -114,7 +114,7 @@ class TestWeb(BaseTestWeb):
self.executor_server.release('project-merge')
self.waitUntilSettled()
- resp = self.get_url("tenant-one/status")
+ resp = self.get_url("api/tenant/tenant-one/status")
self.assertIn('Content-Length', resp.headers)
self.assertIn('Content-Type', resp.headers)
self.assertEqual(
@@ -211,7 +211,7 @@ class TestWeb(BaseTestWeb):
self.executor_server.release('project-merge')
self.waitUntilSettled()
- resp = self.get_url("tenants")
+ resp = self.get_url("api/tenants")
self.assertIn('Content-Length', resp.headers)
self.assertIn('Content-Type', resp.headers)
self.assertEqual(
@@ -230,7 +230,7 @@ class TestWeb(BaseTestWeb):
self.executor_server.release()
self.waitUntilSettled()
- data = self.get_url("tenants").json()
+ data = self.get_url("api/tenants").json()
self.assertEqual('tenant-one', data[0]['name'])
self.assertEqual(3, data[0]['projects'])
self.assertEqual(0, data[0]['queue'])
@@ -242,12 +242,12 @@ class TestWeb(BaseTestWeb):
def test_web_find_change(self):
# can we filter by change id
- data = self.get_url("tenant-one/status/change/1,1").json()
+ data = self.get_url("api/tenant/tenant-one/status/change/1,1").json()
self.assertEqual(1, len(data), data)
self.assertEqual("org/project", data[0]['project'])
- data = self.get_url("tenant-one/status/change/2,1").json()
+ data = self.get_url("api/tenant/tenant-one/status/change/2,1").json()
self.assertEqual(1, len(data), data)
self.assertEqual("org/project1", data[0]['project'], data)
@@ -256,11 +256,11 @@ class TestWeb(BaseTestWeb):
with open(os.path.join(FIXTURE_DIR, 'public.pem'), 'rb') as f:
public_pem = f.read()
- resp = self.get_url("tenant-one/org/project.pub")
+ resp = self.get_url("api/tenant/tenant-one/key/org/project.pub")
self.assertEqual(resp.content, public_pem)
def test_web_404_on_unknown_tenant(self):
- resp = self.get_url("non-tenant/status")
+ resp = self.get_url("api/tenant/non-tenant/status")
self.assertEqual(404, resp.status_code)
@@ -275,7 +275,7 @@ class TestInfo(BaseTestWeb):
self.stats_prefix = statsd_config.get('prefix')
def test_info(self):
- info = self.get_url("info").json()
+ info = self.get_url("api/info").json()
self.assertEqual(
info, {
"info": {
@@ -293,7 +293,7 @@ class TestInfo(BaseTestWeb):
})
def test_tenant_info(self):
- info = self.get_url("tenant-one/info").json()
+ info = self.get_url("api/tenant/tenant-one/info").json()
self.assertEqual(
info, {
"info": {
diff --git a/tests/unit/test_web_urls.py b/tests/unit/test_web_urls.py
index 3e7f69e206..8e372fb79e 100644
--- a/tests/unit/test_web_urls.py
+++ b/tests/unit/test_web_urls.py
@@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import json
import urllib
from bs4 import BeautifulSoup
@@ -66,7 +67,7 @@ class TestDirect(TestWebURLs, ZuulTestCase):
self.port = self.web.port
def test_status_page(self):
- self._crawl('/tenant-one/status.html')
+ self._crawl('/t/tenant-one/status.html')
class TestWhiteLabel(TestWebURLs, ZuulTestCase):
@@ -75,8 +76,7 @@ class TestWhiteLabel(TestWebURLs, ZuulTestCase):
def setUp(self):
super(TestWhiteLabel, self).setUp()
rules = [
- ('^/(.*)$', 'http://localhost:{}/tenant-one/\\1'.format(
- self.web.port)),
+ ('^/(.*)$', 'http://localhost:{}/\\1'.format(self.web.port)),
]
self.proxy = self.useFixture(WebProxyFixture(rules))
self.port = self.proxy.port
@@ -85,6 +85,24 @@ class TestWhiteLabel(TestWebURLs, ZuulTestCase):
self._crawl('/status.html')
+class TestWhiteLabelAPI(TestWebURLs, ZuulTestCase):
+ # Test a zuul-web behind a whitelabel proxy (i.e., what
+ # zuul.openstack.org does).
+ def setUp(self):
+ super(TestWhiteLabelAPI, self).setUp()
+ rules = [
+ ('^/api/(.*)$',
+ 'http://localhost:{}/api/tenant/tenant-one/\\1'.format(
+ self.web.port)),
+ ]
+ self.proxy = self.useFixture(WebProxyFixture(rules))
+ self.port = self.proxy.port
+
+ def test_info(self):
+ info = json.loads(self._get(self.port, '/api/info').decode('utf-8'))
+ self.assertEqual('tenant-one', info['info']['tenant'])
+
+
class TestSuburl(TestWebURLs, ZuulTestCase):
# Test a zuul-web mounted on a suburl (i.e., what software factory
# does).
@@ -98,4 +116,4 @@ class TestSuburl(TestWebURLs, ZuulTestCase):
self.port = self.proxy.port
def test_status_page(self):
- self._crawl('/zuul3/tenant-one/status.html')
+ self._crawl('/zuul3/t/tenant-one/status.html')
diff --git a/tools/encrypt_secret.py b/tools/encrypt_secret.py
index 45ad68ca6e..f086402d9d 100755
--- a/tools/encrypt_secret.py
+++ b/tools/encrypt_secret.py
@@ -70,7 +70,7 @@ def main():
"unencrypted connection. Your secret may get "
"compromised.\n")
- req = Request("%s/%s.pub" % (args.url.rstrip('/'), args.project))
+ req = Request("%s/key/%s.pub" % (args.url.rstrip('/'), args.project))
pubkey = urlopen(req)
if args.infile:
diff --git a/web/dashboard.js b/web/dashboard.js
index 77884e5b50..409d2bdf26 100644
--- a/web/dashboard.js
+++ b/web/dashboard.js
@@ -29,7 +29,7 @@ angular.module('zuulTenants', []).controller(
'mainController', function ($scope, $http, $location) {
$scope.tenants = undefined
$scope.tenants_fetch = function () {
- $http.get(getSourceUrl('tenants', $location))
+ $http.get(getSourceUrl('api/tenants', $location))
.then(function success (result) {
$scope.tenants = result.data
})
diff --git a/web/status.js b/web/status.js
index 81658e624e..56a698b303 100644
--- a/web/status.js
+++ b/web/status.js
@@ -36,7 +36,7 @@ import { getSourceUrl } from './util'
/**
* @return The $.zuul instance
*/
-function zuulStart ($) {
+function zuulStart ($, $location) {
// Start the zuul app (expects default dom)
let $container, $indicator
@@ -60,7 +60,7 @@ function zuulStart ($) {
params['source_data'] = DemoStatusTree
}
} else {
- params['source'] = getSourceUrl('status')
+ params['source'] = getSourceUrl('status', $location)
}
let zuul = $.zuul(params)
@@ -120,7 +120,7 @@ if (module.hot) {
}
angular.module('zuulStatus', []).controller(
- 'mainController', function ($scope, $http) {
- zuulStart(jQuery)
+ 'mainController', function ($scope, $http, $location) {
+ zuulStart(jQuery, $location)
}
)
diff --git a/web/stream.js b/web/stream.js
index 824948da11..aade1594b6 100644
--- a/web/stream.js
+++ b/web/stream.js
@@ -24,6 +24,8 @@
import angular from 'angular'
import './styles/stream.css'
+import { getSourceUrl } from './util'
+
function escapeLog (text) {
const pattern = /[<>&"']/g
@@ -32,7 +34,7 @@ function escapeLog (text) {
})
}
-function zuulStartStream () {
+function zuulStartStream ($location) {
let pageUpdateInMS = 250
let receiveBuffer = ''
@@ -71,7 +73,7 @@ function zuulStartStream () {
} else {
protocol = 'ws://'
}
- let path = url['pathname'].replace(/stream.html.*$/g, '') + 'console-stream'
+ let path = getSourceUrl('console-stream', $location)
params['websocket_url'] = protocol + url['host'] + path
}
let ws = new WebSocket(params['websocket_url'])
@@ -93,7 +95,7 @@ function zuulStartStream () {
}
angular.module('zuulStream', []).controller(
- 'mainController', function ($scope, $http) {
- window.onload = zuulStartStream()
+ 'mainController', function ($scope, $http, $location) {
+ window.onload = zuulStartStream($location)
}
)
diff --git a/web/templates/builds.ejs b/web/templates/builds.ejs
index 25461ac781..a093ccc3e2 100644
--- a/web/templates/builds.ejs
+++ b/web/templates/builds.ejs
@@ -23,7 +23,7 @@ under the License.