[LIVY-468] Reverse proxy support
## What changes were proposed in this pull request?
Implements reverse proxy support, by adding basePath/URL concept, configurable using livy.conf
Resolves: [LIVY-468](https://issues.apache.org/jira/browse/LIVY-468)
## How was this patch tested?
Tested manually with Kong 0.11 and Spark 2.1.0 in cluster mode (using spark dispatcher).
Author: M Wcislo <mwcislo999@gmail.com>
Closes #93 from m-wcislo/LIVY-468_reverse_proxy_support.
diff --git a/conf/livy.conf.template b/conf/livy.conf.template
index 86ca9ab..6f50e2f 100644
--- a/conf/livy.conf.template
+++ b/conf/livy.conf.template
@@ -29,6 +29,10 @@
# What port to start the server on.
# livy.server.port = 8998
+# What base path ui should work on. By default UI is mounted on "/".
+# E.g.: livy.ui.basePath = /my_livy - result in mounting UI on /my_livy/
+# livy.ui.basePath = ""
+
# What spark master Livy sessions should use.
# livy.spark.master = local
diff --git a/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js b/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js
index 28b08a8..64b06df 100644
--- a/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js
+++ b/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js
@@ -48,9 +48,9 @@
var numBatches = 0;
$(document).ready(function () {
- var sessionsReq = $.getJSON(location.origin + "/sessions", function(response) {
+ var sessionsReq = $.getJSON(location.origin + prependBasePath("/sessions"), function(response) {
if (response && response.total > 0) {
- $("#interactive-sessions").load("/static/html/sessions-table.html .sessions-template", function() {
+ $("#interactive-sessions").load(prependBasePath("/static/html/sessions-table.html .sessions-template"), function() {
loadSessionsTable(response.sessions);
$("#interactive-sessions-table").DataTable();
$('#interactive-sessions [data-toggle="tooltip"]').tooltip();
@@ -59,9 +59,9 @@
numSessions = response.total;
});
- var batchesReq = $.getJSON(location.origin + "/batches", function(response) {
+ var batchesReq = $.getJSON(location.origin + prependBasePath("/batches"), function(response) {
if (response && response.total > 0) {
- $("#batches").load("/static/html/batches-table.html .sessions-template", function() {
+ $("#batches").load(prependBasePath("/static/html/batches-table.html .sessions-template"), function() {
loadBatchesTable(response.sessions);
$("#batches-table").DataTable();
$('#batches [data-toggle="tooltip"]').tooltip();
diff --git a/server/src/main/resources/org/apache/livy/server/ui/static/js/livy-ui.js b/server/src/main/resources/org/apache/livy/server/ui/static/js/livy-ui.js
index 6eef2de..f2d743a 100644
--- a/server/src/main/resources/org/apache/livy/server/ui/static/js/livy-ui.js
+++ b/server/src/main/resources/org/apache/livy/server/ui/static/js/livy-ui.js
@@ -27,6 +27,8 @@
'=': '='
};
+var basePath = "";
+
function escapeHtml(string) {
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
return entityMap[s];
@@ -34,7 +36,7 @@
}
function uiLink(relativePath, inner) {
- return anchorLink("/ui/" + relativePath, inner);
+ return anchorLink(prependBasePath("/ui/") + relativePath, inner);
}
function anchorLink(link, inner) {
@@ -89,10 +91,19 @@
function getPathArray() {
var pathArr = location.pathname.split("/");
- pathArr.splice(0, 2);
+ var baseUrlEnd = 2 + (basePath.match(/\//g) || []).length;
+ pathArr.splice(0, baseUrlEnd);
return pathArr;
}
+function setBasePath(path) {
+ basePath = path;
+}
+
+function prependBasePath(path) {
+ return basePath + path;
+}
+
$.extend( $.fn.dataTable.defaults, {
stateSave: true,
});
diff --git a/server/src/main/resources/org/apache/livy/server/ui/static/js/session-log.js b/server/src/main/resources/org/apache/livy/server/ui/static/js/session-log.js
index a3072f9..b6411a0 100644
--- a/server/src/main/resources/org/apache/livy/server/ui/static/js/session-log.js
+++ b/server/src/main/resources/org/apache/livy/server/ui/static/js/session-log.js
@@ -70,7 +70,7 @@
var type = pathArr.shift();
var id = pathArr.shift();
- $.getJSON(location.origin + getLogPath(type, id), {size: -1}, function(response) {
+ $.getJSON(location.origin + prependBasePath(getLogPath(type, id)), {size: -1}, function(response) {
if (response) {
$("#session-log").append(parseLog(response.log));
$('#session-log [data-toggle="tooltip"]').tooltip();
diff --git a/server/src/main/resources/org/apache/livy/server/ui/static/js/session.js b/server/src/main/resources/org/apache/livy/server/ui/static/js/session.js
index 7b85546..9f38719 100644
--- a/server/src/main/resources/org/apache/livy/server/ui/static/js/session.js
+++ b/server/src/main/resources/org/apache/livy/server/ui/static/js/session.js
@@ -80,13 +80,13 @@
$(document).ready(function () {
var id = getPathArray().pop();
- $.getJSON(location.origin + "/sessions/" + id, function(response) {
+ $.getJSON(location.origin + prependBasePath("/sessions/") + id, function(response) {
if (response) {
appendSummary(response);
- $.getJSON(location.origin + "/sessions/" + id + "/statements", function(statementsRes) {
+ $.getJSON(location.origin + prependBasePath("/sessions/") + id + "/statements", function(statementsRes) {
if (statementsRes && statementsRes.total_statements > 0) {
- $("#session-statements").load("/static/html/statements-table.html .statements-template",
+ $("#session-statements").load(prependBasePath("/static/html/statements-table.html .statements-template"),
function() {
loadStatementsTable(statementsRes.statements);
$("#statements-table").DataTable();
diff --git a/server/src/main/scala/org/apache/livy/LivyConf.scala b/server/src/main/scala/org/apache/livy/LivyConf.scala
index 48ea7dd..3f6a86c 100644
--- a/server/src/main/scala/org/apache/livy/LivyConf.scala
+++ b/server/src/main/scala/org/apache/livy/LivyConf.scala
@@ -61,6 +61,7 @@
val SERVER_HOST = Entry("livy.server.host", "0.0.0.0")
val SERVER_PORT = Entry("livy.server.port", 8998)
+ val SERVER_BASE_PATH = Entry("livy.ui.basePath", "")
val UI_ENABLED = Entry("livy.ui.enabled", true)
diff --git a/server/src/main/scala/org/apache/livy/server/LivyServer.scala b/server/src/main/scala/org/apache/livy/server/LivyServer.scala
index f540484..b0b84a2 100644
--- a/server/src/main/scala/org/apache/livy/server/LivyServer.scala
+++ b/server/src/main/scala/org/apache/livy/server/LivyServer.scala
@@ -62,6 +62,7 @@
val host = livyConf.get(SERVER_HOST)
val port = livyConf.getInt(SERVER_PORT)
+ val basePath = livyConf.get(SERVER_BASE_PATH)
val multipartConfig = MultipartConfig(
maxFileSize = Some(livyConf.getLong(LivyConf.FILE_UPLOAD_MAX_SIZE))
).toMultipartConfigElement
@@ -198,12 +199,12 @@
mount(context, batchServlet, "/batches/*")
if (livyConf.getBoolean(UI_ENABLED)) {
- val uiServlet = new UIServlet
+ val uiServlet = new UIServlet(basePath)
mount(context, uiServlet, "/ui/*")
mount(context, staticResourceServlet, "/static/*")
- mount(context, uiRedirectServlet("/ui/"), "/*")
+ mount(context, uiRedirectServlet(basePath + "/ui/"), "/*")
} else {
- mount(context, uiRedirectServlet("/metrics"), "/*")
+ mount(context, uiRedirectServlet(basePath + "/metrics"), "/*")
}
context.mountMetricsAdminServlet("/metrics")
diff --git a/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala b/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala
index 0b0d56a..47d6eae 100644
--- a/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala
+++ b/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala
@@ -21,7 +21,7 @@
import org.scalatra.ScalatraServlet
-class UIServlet extends ScalatraServlet {
+class UIServlet(val basePath: String) extends ScalatraServlet {
before() { contentType = "text/html" }
sealed trait Page { val name: String }
@@ -37,14 +37,21 @@
private def getHeader(pageName: String): Seq[Node] =
<head>
- <link rel="stylesheet" href="/static/css/bootstrap.min.css" type="text/css"/>
- <link rel="stylesheet" href="/static/css/dataTables.bootstrap.min.css" type="text/css"/>
- <link rel="stylesheet" href="/static/css/livy-ui.css" type="text/css"/>
- <script src="/static/js/jquery-3.2.1.min.js"></script>
- <script src="/static/js/bootstrap.min.js"></script>
- <script src="/static/js/jquery.dataTables.min.js"></script>
- <script src="/static/js/dataTables.bootstrap.min.js"></script>
- <script src="/static/js/livy-ui.js"></script>
+ <link rel="stylesheet"
+ href={basePath + "/static/css/bootstrap.min.css"}
+ type="text/css"/>
+ <link rel="stylesheet"
+ href={basePath + "/static/css/dataTables.bootstrap.min.css"}
+ type="text/css"/>
+ <link rel="stylesheet" href={basePath + "/static/css/livy-ui.css"} type="text/css"/>
+ <script src={basePath + "/static/js/jquery-3.2.1.min.js"}></script>
+ <script src={basePath + "/static/js/bootstrap.min.js"}></script>
+ <script src={basePath + "/static/js/jquery.dataTables.min.js"}></script>
+ <script src={basePath + "/static/js/dataTables.bootstrap.min.js"}></script>
+ <script src={basePath + "/static/js/livy-ui.js"}></script>
+ <script type="text/javascript">
+ setBasePath({"'" + basePath + "'"});
+ </script>
<title>Livy - {pageName}</title>
</head>
@@ -52,8 +59,8 @@
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
- <a class="navbar-brand" href="/ui">
- <img alt="Livy" src="/static/img/livy-mini-logo.png"/>
+ <a class="navbar-brand" href={basePath + "/ui"}>
+ <img alt="Livy" src={basePath + "/static/img/livy-mini-logo.png"}/>
</a>
</div>
<div class="collapse navbar-collapse">
@@ -68,12 +75,14 @@
val tabs: Seq[Node] = page match {
case _: AllSessionsPage => <li class="active"><a href="#">Sessions</a></li>
case sessionPage: SessionPage => {
- <li><a href="/ui">Sessions</a></li> ++
+ <li><a href={basePath + "/ui"}>Sessions</a></li> ++
<li class="active"><a href="#">{sessionPage.name}</a></li>
}
case logPage: LogPage => {
- val sessionLink = if (logPage.sessionType == "Session") "/ui/session/" + logPage.id else "#"
- <li><a href="/ui">Sessions</a></li> ++
+ val sessionLink = if (logPage.sessionType == "Session") {
+ basePath + "/ui/session/" + logPage.id
+ } else "#"
+ <li><a href={basePath + "/ui"}>Sessions</a></li> ++
<li><a href={sessionLink}>{logPage.sessionName}</a></li> ++
<li class="active"><a href="#">Log</a></li>
}
@@ -102,7 +111,7 @@
<div id="all-sessions">
<div id="interactive-sessions"></div>
<div id="batches"></div>
- <script src="/static/js/all-sessions.js"></script>
+ <script src={basePath + "/static/js/all-sessions.js"}></script>
</div>
createPage(AllSessionsPage(), content)
@@ -113,7 +122,7 @@
<div id="session-page">
<div id="session-summary"></div>
<div id="session-statements"></div>
- <script src="/static/js/session.js"></script>
+ <script src={basePath + "/static/js/session.js"}></script>
</div>
createPage(SessionPage(params("id").toInt), content)
@@ -123,7 +132,7 @@
val content =
<div id="log-page">
<div id="session-log"></div>
- <script src="/static/js/session-log.js"></script>
+ <script src={basePath + "/static/js/session-log.js"}></script>
</div>
createPage(page, content)