This is an automated email from the ASF dual-hosted git repository.
milenkovicm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-ballista.git
The following commit(s) were added to refs/heads/main by this push:
new e0c4032e2 feat(scheduler): Redirect '/' to nighlies.a.o WebTUI (#2059)
e0c4032e2 is described below
commit e0c4032e2cbab0d48d90587191093cc540390278
Author: Martin Grigorov <[email protected]>
AuthorDate: Wed Aug 5 19:17:44 2026 +0300
feat(scheduler): Redirect '/' to nighlies.a.o WebTUI (#2059)
* feat(scheduler): Redirect '/' to nighlies.a.o WebTUI
Opening http://localhost:50050 in a browser will now redirect to the
convenience deployment of the WebTUI app at
https://nightlies.apache.org/datafusion/ballista/tui/...
* Fix rsync delete command: `--delete does not work without --recursive or
--dirs`
* Re-enable the CI job condition to deploy WebTUI only on push to main
* URL encode the query parameters
Use x-forwarded-proto request header to read the url protocol for
Scheduler server
* Use "See Other" redirect (code 303)
* Import std::collections::HashMap
* Own the parsed query parameter values
* Update python/Cargo.lock
* Re-enable the CI condition for deployment at nightlies.a.o
* Make it possible to use custom Axum route for the path redirecting to the
WebTUI at https://nightlies.apache.org
* Use config's external_host & bind_port as a fallback for the redirect url
to WebTUI
* clippy
* Use X-Forwarded-XYZ headers to read the protocol+host+port
Fallback to http://external_host:bind_port
* fmt
* clippy
* Add unit tests for the get_webtui() Axum handler
* taplo fmt
* fix clippy
---------
Co-authored-by: Marko Milenković <[email protected]>
---
.github/workflows/web-tui.yml | 4 +-
Cargo.lock | 12 +-
Cargo.toml | 1 +
ballista-cli/Cargo.toml | 6 +-
ballista-cli/src/tui/http_client.rs | 3 +-
ballista-cli/src/tui/infrastructure/config.rs | 15 +-
ballista/scheduler/Cargo.toml | 1 +
ballista/scheduler/src/api/handlers.rs | 297 +++++++++++++++++++++++++-
ballista/scheduler/src/api/routes.rs | 3 +
ballista/scheduler/src/config.rs | 34 ++-
python/Cargo.lock | 10 +
11 files changed, 360 insertions(+), 26 deletions(-)
diff --git a/.github/workflows/web-tui.yml b/.github/workflows/web-tui.yml
index d607676c2..7c182d945 100644
--- a/.github/workflows/web-tui.yml
+++ b/.github/workflows/web-tui.yml
@@ -90,7 +90,7 @@ jobs:
--no-default-features --features web
- name: Deploy at nightlies.a.o
- if: ${{ github.event == 'push' && github.ref == 'refs/heads/main' }}
+ if: ${{ github.event_name == 'push' && github.ref_name == 'main' }}
run: |
TUI_DIR="datafusion/ballista/tui/${{
steps.cargo_metadata.outputs.ballista_version }}"
REMOTE_TARGET_DIR="${{ secrets.NIGHTLIES_RSYNC_PATH }}/${TUI_DIR}/"
@@ -101,7 +101,7 @@ jobs:
ssh -p ${{ secrets.NIGHTLIES_RSYNC_PORT }} -l ${{
secrets.NIGHTLIES_RSYNC_USER }} ${{ secrets.NIGHTLIES_RSYNC_HOST }} "mkdir -p
${REMOTE_TARGET_DIR}"
- rsync --times --compress --delete --verbose -e "ssh -p ${{
secrets.NIGHTLIES_RSYNC_PORT }} -l ${{ secrets.NIGHTLIES_RSYNC_USER }}"
./target/web-tui/* ${{ secrets.NIGHTLIES_RSYNC_HOST }}:${REMOTE_TARGET_DIR}
+ rsync --times --compress --delete --recursive --verbose -e "ssh -p
${{ secrets.NIGHTLIES_RSYNC_PORT }} -l ${{ secrets.NIGHTLIES_RSYNC_USER }}"
./target/web-tui/* ${{ secrets.NIGHTLIES_RSYNC_HOST }}:${REMOTE_TARGET_DIR}
- name: Upload WASM32 application
uses: actions/upload-artifact@v7
diff --git a/Cargo.lock b/Cargo.lock
index ca8b6ef27..247de94cd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1084,7 +1084,6 @@ dependencies = [
"gloo-timers",
"js-sys",
"mimalloc",
- "percent-encoding",
"prometheus-parse",
"ratatui",
"ratzilla",
@@ -1098,6 +1097,7 @@ dependencies = [
"tracing-subscriber",
"tracing-web",
"tui-shimmer",
+ "url-escape",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@@ -1239,6 +1239,7 @@ dependencies = [
"tracing",
"tracing-appender",
"tracing-subscriber",
+ "url-escape",
"uuid",
]
@@ -7684,6 +7685,15 @@ dependencies = [
"serde_derive",
]
+[[package]]
+name = "url-escape"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd4105a4fc511f6b1c9616a372a1313ae7c15afcaac839617c1c7cf25ce0a965"
+dependencies = [
+ "percent-encoding",
+]
+
[[package]]
name = "urlencoding"
version = "2.1.3"
diff --git a/Cargo.toml b/Cargo.toml
index b29fc92dc..5a6b966dd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -81,6 +81,7 @@ tempfile = { version = "3.27" }
tokio = { version = "1" }
tokio-stream = { version = "0.1" }
url = { version = "2.5" }
+url-escape = { version = "0.1.2" }
uuid = { version = "1.23", features = ["v4", "v7"] }
# cargo build --profile release-lto
diff --git a/ballista-cli/Cargo.toml b/ballista-cli/Cargo.toml
index 573c1de6e..a61a63247 100644
--- a/ballista-cli/Cargo.toml
+++ b/ballista-cli/Cargo.toml
@@ -46,7 +46,6 @@ config = { version = "0.15.23", default-features = false,
features = ["yaml"], o
dirs = "6.0"
dotparser = { version = "0.3.0", optional = true }
futures = { version = "0.3.31", optional = true }
-percent-encoding = { version = "2.3.2", optional = true }
prometheus-parse = { version = "0.2", optional = true }
ratatui = { version = "0.30.1", default-features = false, optional = true }
reqwest = { version = "0.13.4", features = ["json"], optional = true }
@@ -55,6 +54,7 @@ serde_json = { version = "1", optional = true }
tracing = { version = "0.1" }
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt",
"local-time"] }
tui-shimmer = { version = "0.1", optional = true }
+url-escape = { workspace = true, optional = true }
# Native-only TUI deps
crossterm = { version = "0.29.0", features = ["event-stream"], optional = true
}
@@ -94,7 +94,7 @@ tui = [
"dep:crossterm",
"dep:dotparser",
"dep:futures",
- "dep:percent-encoding",
+ "dep:url-escape",
"dep:prometheus-parse",
"dep:ratatui",
"dep:reqwest",
@@ -116,7 +116,7 @@ web = [
"dep:critical-section",
"dep:dotparser",
"dep:futures",
- "dep:percent-encoding",
+ "dep:url-escape",
"dep:prometheus-parse",
"dep:ratatui",
"ratatui/serde",
diff --git a/ballista-cli/src/tui/http_client.rs
b/ballista-cli/src/tui/http_client.rs
index bccabec64..a315c734a 100644
--- a/ballista-cli/src/tui/http_client.rs
+++ b/ballista-cli/src/tui/http_client.rs
@@ -15,7 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-use percent_encoding::{NON_ALPHANUMERIC, percent_encode};
use reqwest::{Client, Response};
use serde::de::DeserializeOwned;
#[cfg(not(target_arch = "wasm32"))]
@@ -213,6 +212,6 @@ impl HttpClient {
}
fn url_encode(&self, job_id: &str) -> String {
- percent_encode(job_id.as_bytes(), NON_ALPHANUMERIC).to_string()
+ url_escape::encode_path(job_id).to_string()
}
}
diff --git a/ballista-cli/src/tui/infrastructure/config.rs
b/ballista-cli/src/tui/infrastructure/config.rs
index 2d64f0710..ffba3dbb4 100644
--- a/ballista-cli/src/tui/infrastructure/config.rs
+++ b/ballista-cli/src/tui/infrastructure/config.rs
@@ -114,9 +114,9 @@ mod web {
pub(super) fn parse() -> File<FileSourceString, FileFormat> {
let mut data_reload_interval_ms = 2000;
let mut repaint_interval_ms = 50;
- let mut scheduler_url = "http://localhost:50050";
+ let mut scheduler_url = String::from("http://localhost:50050");
let mut http_timeout_ms = 2000;
- let mut theme_name = "dark";
+ let mut theme_name = String::from("dark");
let mut theme_app_background_bg = String::from("black");
let query_string = Self::decode_request();
@@ -131,13 +131,18 @@ mod web {
"ballista_repaint_interval_ms" => {
repaint_interval_ms =
value.parse::<u64>().unwrap_or(50)
}
- "ballista_scheduler_url" => scheduler_url = value,
+ "ballista_scheduler_url" => {
+ scheduler_url =
url_escape::decode(value).to_string()
+ }
"ballista_http_timeout" => {
http_timeout_ms =
value.parse::<u64>().unwrap_or(2000)
}
- "ballista_theme_name" => theme_name = value,
+ "ballista_theme_name" => {
+ theme_name = url_escape::decode(value).to_string()
+ }
"ballista_theme_overrides_app_background_bg" => {
- theme_app_background_bg = value.replace("%23",
"#");
+ theme_app_background_bg =
+ url_escape::decode(value).to_string();
}
_ => {}
}
diff --git a/ballista/scheduler/Cargo.toml b/ballista/scheduler/Cargo.toml
index 6409a87d3..b8b712773 100644
--- a/ballista/scheduler/Cargo.toml
+++ b/ballista/scheduler/Cargo.toml
@@ -77,6 +77,7 @@ tower-http = { version = "0.7", features = ["cors"] }
tracing = { workspace = true, optional = true }
tracing-appender = { workspace = true, optional = true }
tracing-subscriber = { workspace = true, optional = true }
+url-escape = { workspace = true }
uuid = { workspace = true }
[[test]]
diff --git a/ballista/scheduler/src/api/handlers.rs
b/ballista/scheduler/src/api/handlers.rs
index a0e1a60d5..5409f0780 100644
--- a/ballista/scheduler/src/api/handlers.rs
+++ b/ballista/scheduler/src/api/handlers.rs
@@ -17,6 +17,7 @@ use crate::state::execution_graph_dot::ExecutionGraphDot;
use crate::state::execution_stage::TaskInfo;
use crate::{api::SchedulerErrorResponse, scheduler_server::SchedulerServer};
use axum::extract::Query;
+use axum::response::Redirect;
use axum::{
Json,
extract::{Path, State},
@@ -46,8 +47,9 @@ use graphviz_rust::{
exec,
printer::PrinterContext,
};
-use http::{StatusCode, header::CONTENT_TYPE};
+use http::{HeaderMap, StatusCode, header::CONTENT_TYPE};
use serde::Serialize;
+use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
@@ -230,6 +232,58 @@ pub enum PlanFormat {
Metrics,
}
+/// A handler for GET requests to the root (`/`).
+/// It redirects to
`https://nightlies.apache.org/datafusion/ballista/tui/<BALLISTA_VERSION>/`
+/// forwarding any query parameters
+pub async fn get_webtui<
+ T: AsLogicalPlan + Clone + Send + Sync + 'static,
+ U: AsExecutionPlan + Send + Sync + 'static,
+>(
+ header_map: HeaderMap,
+ Query(mut params): Query<HashMap<String, String>>,
+ State(data_server): State<Arc<SchedulerServer<T, U>>>,
+) -> Result<Redirect, (StatusCode, String)> {
+ const NIGHTLIES_URL: &str =
"https://nightlies.apache.org/datafusion/ballista/tui";
+ let external_host = &data_server.state.config.external_host;
+ let bind_port = data_server.state.config.bind_port;
+
+ let ballista_scheduler_url =
+ params.remove("ballista_scheduler_url").unwrap_or_else(|| {
+ let proto = header_map
+ .get("x-forwarded-proto")
+ .and_then(|v| v.to_str().ok())
+ .unwrap_or("http");
+ let host = header_map
+ .get("x-forwarded-host")
+ .and_then(|hv| hv.to_str().ok())
+ .unwrap_or(external_host);
+ let port = header_map
+ .get("x-forwarded-port")
+ .and_then(|hv| hv.to_str().ok())
+ .and_then(|v| v.parse::<u16>().ok())
+ .unwrap_or(bind_port);
+ format!("{proto}://{host}:{port}")
+ });
+
+ let mut query_string = String::new();
+ query_string.push_str(&format!(
+ "ballista_scheduler_url={}",
+ url_escape::encode_query(&ballista_scheduler_url)
+ ));
+
+ for (k, v) in params.iter() {
+ query_string.push_str(&format!(
+ "&{}={}",
+ url_escape::encode_query(k),
+ url_escape::encode_query(v)
+ ));
+ }
+
+ let target = format!("{NIGHTLIES_URL}/{BALLISTA_VERSION}/?{query_string}");
+
+ Ok(Redirect::to(&target))
+}
+
pub async fn get_scheduler_state<
T: AsLogicalPlan + Clone + Send + Sync + 'static,
U: AsExecutionPlan + Send + Sync + 'static,
@@ -549,7 +603,7 @@ pub async fn get_query_stages<
let metrics =
running_stage.stage_metrics.as_deref().unwrap_or(&[]);
summary.stage_plan = Some(match plan_format {
PlanFormat::Default =>
displayable(running_stage.plan.as_ref()).indent(false).to_string(),
- PlanFormat::Tree =>
displayable(running_stage.plan.as_ref()).tree_render().to_string(),
+ PlanFormat::Tree =>
displayable(running_stage.plan.as_ref()).tree_render().to_string(),
PlanFormat::Metrics =>
format_stage_metrics(running_stage.plan.as_ref(), metrics),
});
summary.input_rows = running_stage
@@ -599,7 +653,7 @@ pub async fn get_query_stages<
finish_time: info.finish_time as u64,
input_rows,
output_rows,
- status: task_status
+ status: task_status,
})
})
.collect();
@@ -607,7 +661,7 @@ pub async fn get_query_stages<
ExecutionStage::Successful(completed_stage) => {
summary.stage_plan = Some(match plan_format {
PlanFormat::Default =>
displayable(completed_stage.plan.as_ref()).indent(false).to_string(),
- PlanFormat::Tree =>
displayable(completed_stage.plan.as_ref()).tree_render().to_string(),
+ PlanFormat::Tree =>
displayable(completed_stage.plan.as_ref()).tree_render().to_string(),
PlanFormat::Metrics =>
format_stage_metrics(completed_stage.plan.as_ref(),
&completed_stage.stage_metrics),
});
summary.input_rows = get_combined_count(
@@ -648,7 +702,7 @@ pub async fn get_query_stages<
finish_time: task_info.finish_time as u64,
input_rows,
output_rows,
- status: task_status
+ status: task_status,
})
})
.collect();
@@ -923,7 +977,7 @@ pub async fn get_job_dot_graph<
})?
{
ExecutionGraphDot::generate(graph.as_ref())
- .map_err(|e| {
+ .map_err(|e| {
tracing::error!("Error occurred while getting the dot graph
for job '{job_id}' reason: {e:?}");
SchedulerErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
})
@@ -968,10 +1022,10 @@ pub async fn get_job_svg_graph<
&mut PrinterContext::default(),
vec![CommandArg::Format(Format::Svg)],
)
- .map_err(|e| {
- tracing::error!("Error occurred while getting job svg graph
for job '{job_id}' reason: {e:?}");
- SchedulerErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
- })?;
+ .map_err(|e| {
+ tracing::error!("Error occurred while getting job svg
graph for job '{job_id}' reason: {e:?}");
+
SchedulerErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
+ })?;
let svg = String::from_utf8_lossy(&result).to_string();
Ok(Response::builder()
@@ -1147,4 +1201,227 @@ mod tests {
fn test_job_elapsed_ms_end_before_start_saturates_to_zero() {
assert_eq!(super::job_elapsed_ms(500, 100), 0);
}
+
+ mod get_webtui {
+ use super::*;
+ use crate::config::SchedulerConfig;
+ use crate::metrics::default_metrics_collector;
+ use crate::test_utils::test_cluster_context;
+ use axum::response::IntoResponse;
+ use ballista_core::serde::BallistaCodec;
+ use datafusion_proto::protobuf::{LogicalPlanNode, PhysicalPlanNode};
+
+ fn test_scheduler(
+ config: SchedulerConfig,
+ ) -> Arc<SchedulerServer<LogicalPlanNode, PhysicalPlanNode>> {
+ let server = SchedulerServer::new(
+ "localhost:50050".to_owned(),
+ test_cluster_context(),
+ BallistaCodec::default(),
+ Arc::new(config),
+ default_metrics_collector().unwrap(),
+ );
+ Arc::new(server)
+ }
+
+ fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
+ let mut map = HeaderMap::new();
+ for (k, v) in pairs {
+ map.insert(
+ http::HeaderName::from_bytes(k.as_bytes()).unwrap(),
+ v.parse().unwrap(),
+ );
+ }
+ map
+ }
+
+ fn location_of(result: Result<Redirect, (StatusCode, String)>) ->
String {
+ let response = result.expect("handler should not
error").into_response();
+ assert_eq!(response.status(), StatusCode::SEE_OTHER);
+ response
+ .headers()
+ .get("location")
+ .expect("redirect response must have a location header")
+ .to_str()
+ .unwrap()
+ .to_owned()
+ }
+
+ #[tokio::test]
+ async fn defaults_to_config_external_host_and_bind_port() {
+ let scheduler = test_scheduler(SchedulerConfig::default());
+
+ let result =
+ get_webtui(HeaderMap::new(), Query(HashMap::new()),
State(scheduler))
+ .await;
+
+ let location = location_of(result);
+ assert!(
+
location.contains("ballista_scheduler_url=http://localhost:50050"),
+ "unexpected location: {location}"
+ );
+ assert!(
+ location
+
.starts_with("https://nightlies.apache.org/datafusion/ballista/tui/")
+ );
+ }
+
+ #[tokio::test]
+ async fn uses_custom_external_host_and_bind_port_from_config() {
+ let scheduler = test_scheduler(SchedulerConfig {
+ external_host: "scheduler.example.com".into(),
+ bind_port: 8080,
+ ..Default::default()
+ });
+
+ let result =
+ get_webtui(HeaderMap::new(), Query(HashMap::new()),
State(scheduler))
+ .await;
+
+ let location = location_of(result);
+ assert!(
+ location
+
.contains("ballista_scheduler_url=http://scheduler.example.com:8080"),
+ "unexpected location: {location}"
+ );
+ }
+
+ #[tokio::test]
+ async fn x_forwarded_headers_take_precedence_over_config() {
+ let scheduler = test_scheduler(SchedulerConfig {
+ external_host: "internal-host".into(),
+ bind_port: 50050,
+ ..Default::default()
+ });
+
+ let result = get_webtui(
+ headers(&[
+ ("x-forwarded-proto", "https"),
+ ("x-forwarded-host", "public-host.example.com"),
+ ("x-forwarded-port", "443"),
+ ]),
+ Query(HashMap::new()),
+ State(scheduler),
+ )
+ .await;
+
+ let location = location_of(result);
+ assert!(
+ location.contains(
+
"ballista_scheduler_url=https://public-host.example.com:443"
+ ),
+ "unexpected location: {location}"
+ );
+ }
+
+ #[tokio::test]
+ async fn partial_forwarded_headers_fall_back_to_config_for_the_rest() {
+ let scheduler = test_scheduler(SchedulerConfig {
+ external_host: "internal-host".into(),
+ bind_port: 50050,
+ ..Default::default()
+ });
+
+ let result = get_webtui(
+ headers(&[("x-forwarded-proto", "https")]),
+ Query(HashMap::new()),
+ State(scheduler),
+ )
+ .await;
+
+ let location = location_of(result);
+ assert!(
+
location.contains("ballista_scheduler_url=https://internal-host:50050"),
+ "unexpected location: {location}"
+ );
+ }
+
+ #[tokio::test]
+ async fn malformed_forwarded_port_falls_back_to_config_bind_port() {
+ let scheduler = test_scheduler(SchedulerConfig {
+ external_host: "internal-host".into(),
+ bind_port: 50050,
+ ..Default::default()
+ });
+
+ let result = get_webtui(
+ headers(&[
+ ("x-forwarded-proto", "https"),
+ ("x-forwarded-port", "not-a-port"),
+ ]),
+ Query(HashMap::new()),
+ State(scheduler),
+ )
+ .await;
+
+ let location = location_of(result);
+ assert!(
+
location.contains("ballista_scheduler_url=https://internal-host:50050"),
+ "unexpected location: {location}"
+ );
+ }
+
+ #[tokio::test]
+ async fn explicit_ballista_scheduler_url_param_takes_precedence() {
+ let scheduler = test_scheduler(SchedulerConfig::default());
+
+ let mut params = HashMap::new();
+ params.insert(
+ "ballista_scheduler_url".to_string(),
+ "https://override.example.com:1234".to_string(),
+ );
+
+ let result = get_webtui(
+ headers(&[("x-forwarded-host",
"should-be-ignored.example.com")]),
+ Query(params),
+ State(scheduler),
+ )
+ .await;
+
+ let location = location_of(result);
+ assert!(
+ location
+
.contains("ballista_scheduler_url=https://override.example.com:1234"),
+ "unexpected location: {location}"
+ );
+ assert!(
+ !location.contains("should-be-ignored"),
+ "explicit ballista_scheduler_url must not be overridden:
{location}"
+ );
+ }
+
+ #[tokio::test]
+ async fn additional_query_params_are_forwarded() {
+ let scheduler = test_scheduler(SchedulerConfig::default());
+
+ let mut params = HashMap::new();
+ params.insert("foo".to_string(), "bar".to_string());
+
+ let result =
+ get_webtui(HeaderMap::new(), Query(params),
State(scheduler)).await;
+
+ let location = location_of(result);
+ assert!(
+ location.contains("&foo=bar"),
+ "unexpected location: {location}"
+ );
+ }
+
+ #[tokio::test]
+ async fn query_param_values_are_url_escaped() {
+ let scheduler = test_scheduler(SchedulerConfig::default());
+
+ let mut params = HashMap::new();
+ params.insert("weird".to_string(), "a#b c".to_string());
+
+ let result =
+ get_webtui(HeaderMap::new(), Query(params),
State(scheduler)).await;
+
+ let location = location_of(result);
+ assert!(
+ location.contains("&weird=a%23b%20c"),
+ "unexpected location: {location}"
+ );
+ }
+ }
}
diff --git a/ballista/scheduler/src/api/routes.rs
b/ballista/scheduler/src/api/routes.rs
index 76f2b7851..6a3341934 100644
--- a/ballista/scheduler/src/api/routes.rs
+++ b/ballista/scheduler/src/api/routes.rs
@@ -27,7 +27,10 @@ pub fn get_routes<
>(
scheduler_server: Arc<SchedulerServer<T, U>>,
) -> Router {
+ let web_tui_route = scheduler_server.state.config.web_tui_route.clone();
+
let router = Router::new()
+ .route(&web_tui_route, get(handlers::get_webtui))
.route("/api/state", get(handlers::get_scheduler_state::<T, U>))
.route("/api/version", get(handlers::get_scheduler_version))
.route("/api/executors", get(handlers::get_executors::<T, U>))
diff --git a/ballista/scheduler/src/config.rs b/ballista/scheduler/src/config.rs
index 28320783f..c24face92 100644
--- a/ballista/scheduler/src/config.rs
+++ b/ballista/scheduler/src/config.rs
@@ -79,13 +79,26 @@ pub struct Config {
)]
pub advertise_flight_sql_endpoint: Option<String>,
/// Namespace for the ballista cluster.
- #[arg(short = 'n', long, default_value_t = String::from("ballista"), help
= "Namespace for the ballista cluster that this executor will join.")]
+ #[arg(
+ short = 'n',
+ long,
+ default_value_t = String::from("ballista"),
+ help = "Namespace for the ballista cluster that this executor will
join."
+ )]
pub namespace: String,
/// Local host name or IP address to bind to.
- #[arg(long, default_value_t = String::from("0.0.0.0"), help = "Local host
name or IP address to bind to.")]
+ #[arg(
+ long,
+ default_value_t = String::from("0.0.0.0"),
+ help = "Local host name or IP address to bind to."
+ )]
pub bind_host: String,
/// External host name for executors to connect to.
- #[arg(long, default_value_t = String::from("localhost"), help = "Host name
or IP address so that executors can connect to this scheduler.")]
+ #[arg(
+ long,
+ default_value_t = String::from("localhost"),
+ help = "Host name or IP address so that executors can connect to this
scheduler."
+ )]
pub external_host: String,
/// Port to bind the scheduler gRPC service.
#[arg(
@@ -267,6 +280,14 @@ pub struct Config {
help = "Comma-separated list of allowed methods for CORS. By default,
GET, PATCH, and OPTIONS are allowed."
)]
pub cors_allowed_methods: String,
+ #[cfg(feature = "rest-api")]
+ /// The HTTP path that will redirect to the WebTUI app at
`https://nightlies.apache.org`
+ #[arg(
+ long,
+ default_value_t = String::from("/"),
+ help = "The HTTP path that will redirect to the WebTUI app at
https://nightlies.apache.org."
+ )]
+ pub web_tui_route: String,
}
/// Configurations for the ballista scheduler of scheduling jobs and tasks
@@ -344,6 +365,9 @@ pub struct SchedulerConfig {
#[cfg(feature = "rest-api")]
/// Comma-separated list of allowed methods for CORS
pub cors_allowed_methods: String,
+ #[cfg(feature = "rest-api")]
+ /// The HTTP path that will redirect to the WebTUI app at
`https://nightlies.apache.org`
+ pub web_tui_route: String,
/// Callback invoked when new work becomes available for executors.
/// See [`OnWorkAvailableFn`].
pub on_work_available: Option<OnWorkAvailableFn>,
@@ -386,6 +410,8 @@ impl Default for SchedulerConfig {
cors_allowed_origins: String::default(),
#[cfg(feature = "rest-api")]
cors_allowed_methods: String::default(),
+ #[cfg(feature = "rest-api")]
+ web_tui_route: String::from("/"),
on_work_available: None,
}
}
@@ -645,6 +671,8 @@ impl TryFrom<Config> for SchedulerConfig {
cors_allowed_origins: opt.cors_allowed_origins,
#[cfg(feature = "rest-api")]
cors_allowed_methods: opt.cors_allowed_methods,
+ #[cfg(feature = "rest-api")]
+ web_tui_route: opt.web_tui_route,
on_work_available: None,
};
diff --git a/python/Cargo.lock b/python/Cargo.lock
index 111a1019b..f8e6b0225 100644
--- a/python/Cargo.lock
+++ b/python/Cargo.lock
@@ -588,6 +588,7 @@ dependencies = [
"tokio-stream",
"tonic",
"tower-http 0.7.0",
+ "url-escape",
"uuid",
]
@@ -4344,6 +4345,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "url-escape"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd4105a4fc511f6b1c9616a372a1313ae7c15afcaac839617c1c7cf25ce0a965"
+dependencies = [
+ "percent-encoding",
+]
+
[[package]]
name = "utf8_iter"
version = "1.0.4"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]