From 65ea6fd85f372ff93ce9f69b76753ac9b938c8bc Mon Sep 17 00:00:00 2001 From: LFC Date: Mon, 27 Mar 2023 15:08:44 +0800 Subject: [PATCH] feat: embed dashboard into GreptimeDB binary (#1239) * feat: embed dashboard into GreptimeDB binary * fix: resolve PR comments --- Cargo.lock | 36 ++++++++++++++++++++ scripts/fetch-dashboard-assets.sh | 39 +++++++++++++++++++++ src/servers/Cargo.toml | 3 ++ src/servers/build.rs | 54 +++++++++++++++++++++++++++++ src/servers/dashboard/VERSION | 1 + src/servers/src/error.rs | 11 ++++-- src/servers/src/http.rs | 7 ++++ src/servers/src/http/dashboard.rs | 56 +++++++++++++++++++++++++++++++ 8 files changed, 205 insertions(+), 2 deletions(-) create mode 100755 scripts/fetch-dashboard-assets.sh create mode 100644 src/servers/build.rs create mode 100644 src/servers/dashboard/VERSION create mode 100644 src/servers/src/http/dashboard.rs diff --git a/Cargo.lock b/Cargo.lock index d258ad8762..6004018708 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6076,6 +6076,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "6.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb133b9a38b5543fad3807fb2028ea47c5f2b566f4f5e28a11902f1a358348b6" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4e0f0ced47ded9a68374ac145edd65a6c1fa13a96447b873660b2a568a0fd7" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512b0ab6853f7e14e3c8754acb43d6f748bb9ced66aa5915a6553ac8213f7731" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.18.0" @@ -6918,6 +6952,7 @@ dependencies = [ "hyper", "influxdb_line_protocol", "metrics", + "mime_guess", "mysql_async", "num_cpus", "once_cell", @@ -6932,6 +6967,7 @@ dependencies = [ "query", "rand", "regex", + "rust-embed", "rustls", "rustls-pemfile", "schemars", diff --git a/scripts/fetch-dashboard-assets.sh b/scripts/fetch-dashboard-assets.sh new file mode 100755 index 0000000000..8888a907d8 --- /dev/null +++ b/scripts/fetch-dashboard-assets.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# This script is used to download built dashboard assets from the "GreptimeTeam/dashboard" repository. + +set -e + +declare -r SCRIPT_DIR=$(cd $(dirname ${0}) >/dev/null 2>&1 && pwd) +declare -r ROOT_DIR=$(dirname ${SCRIPT_DIR}) +declare -r STATIC_DIR="$ROOT_DIR/src/servers/dashboard" + +RELEASE_VERSION="$(cat $STATIC_DIR/VERSION)" + +# Download the SHA256 checksum attached to the release. To verify the integrity +# of the download, this checksum will be used to check the download tar file +# containing the built dashboard assets. +curl -Ls https://github.com/GreptimeTeam/dashboard/releases/download/$RELEASE_VERSION/sha256.txt --output sha256.txt + +# Download the tar file containing the built dashboard assets. +curl -L https://github.com/GreptimeTeam/dashboard/releases/download/$RELEASE_VERSION/build.tar.gz --output build.tar.gz + +# Verify the checksums match; exit if they don't. +case "$(uname -s)" in + FreeBSD | Darwin) + echo "$(cat sha256.txt)" | shasum --algorithm 256 --check \ + || { echo "Checksums did not match for downloaded dashboard assets!"; exit 1; } ;; + Linux) + echo "$(cat sha256.txt)" | sha256sum --check -- \ + || { echo "Checksums did not match for downloaded dashboard assets!"; exit 1; } ;; + *) + echo "The '$(uname -s)' operating system is not supported as a build host for the dashboard" >&2 + exit 1 +esac + +# Extract the assets and clean up. +tar -xzf build.tar.gz -C "$STATIC_DIR" +rm sha256.txt +rm build.tar.gz + +echo "Successfully download dashboard assets to $STATIC_DIR" diff --git a/src/servers/Cargo.toml b/src/servers/Cargo.toml index 254e347209..cdc226e531 100644 --- a/src/servers/Cargo.toml +++ b/src/servers/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true [features] mem-prof = ["dep:common-mem-prof"] +dashboard = [] [dependencies] aide = { version = "0.9", features = ["axum"] } @@ -39,6 +40,7 @@ humantime-serde = "1.1" hyper = { version = "0.14", features = ["full"] } influxdb_line_protocol = { git = "https://github.com/evenyag/influxdb_iox", branch = "feat/line-protocol" } metrics = "0.20" +mime_guess = "2.0" num_cpus = "1.13" once_cell = "1.16" openmetrics-parser = "0.4" @@ -54,6 +56,7 @@ rand.workspace = true regex = "1.6" rustls = "0.20" rustls-pemfile = "1.0" +rust-embed = { version = "6.6", features = ["debug-embed"] } schemars = "0.8" serde.workspace = true serde_json = "1.0" diff --git a/src/servers/build.rs b/src/servers/build.rs new file mode 100644 index 0000000000..b32ce969b8 --- /dev/null +++ b/src/servers/build.rs @@ -0,0 +1,54 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +fn main() { + #[cfg(feature = "dashboard")] + fetch_dashboard_assets(); +} + +#[cfg(feature = "dashboard")] +fn fetch_dashboard_assets() { + use std::process::{Command, Stdio}; + + macro_rules! p { + ($($tokens: tt)*) => { + println!("cargo:warning={}", format!($($tokens)*)) + } + } + + let output = Command::new("./fetch-dashboard-assets.sh") + .current_dir("../../scripts") + .stdout(Stdio::piped()) + .spawn() + .and_then(|p| p.wait_with_output()); + match output { + Ok(output) => { + String::from_utf8_lossy(&output.stdout) + .lines() + .for_each(|x| p!("{}", x)); + + assert!(output.status.success()); + } + Err(e) => { + let e = format!( + r#" +Failed to fetch dashboard assets: {}. +You can manually execute './scripts/fetch-dashboard-assets.sh' to see why, +or it's a network error, just try again or enable/disable some proxy."#, + e + ); + panic!("{}", e); + } + } +} diff --git a/src/servers/dashboard/VERSION b/src/servers/dashboard/VERSION new file mode 100644 index 0000000000..df88b680a5 --- /dev/null +++ b/src/servers/dashboard/VERSION @@ -0,0 +1 @@ +v0.0.1-test diff --git a/src/servers/src/error.rs b/src/servers/src/error.rs index efa6ae7fdf..910303db81 100644 --- a/src/servers/src/error.rs +++ b/src/servers/src/error.rs @@ -18,7 +18,7 @@ use std::string::FromUtf8Error; use axum::http::StatusCode as HttpStatusCode; use axum::response::{IntoResponse, Response}; -use axum::Json; +use axum::{http, Json}; use base64::DecodeError; use catalog; use common_error::prelude::*; @@ -275,6 +275,12 @@ pub enum Error { source: tonic_reflection::server::Error, backtrace: Backtrace, }, + + #[snafu(display("Failed to build HTTP response, source: {source}"))] + BuildHttpResponse { + source: http::Error, + backtrace: Backtrace, + }, } pub type Result = std::result::Result; @@ -294,7 +300,8 @@ impl ErrorExt for Error { | TcpBind { .. } | CatalogError { .. } | GrpcReflectionService { .. } - | BuildingContext { .. } => StatusCode::Internal, + | BuildingContext { .. } + | BuildHttpResponse { .. } => StatusCode::Internal, InsertScript { source, .. } | ExecuteScript { source, .. } diff --git a/src/servers/src/http.rs b/src/servers/src/http.rs index 0f7b9da563..8d5b053262 100644 --- a/src/servers/src/http.rs +++ b/src/servers/src/http.rs @@ -20,6 +20,8 @@ pub mod prometheus; pub mod script; mod admin; +#[cfg(feature = "dashboard")] +mod dashboard; #[cfg(feature = "mem-prof")] pub mod mem_prof; @@ -477,6 +479,11 @@ impl HttpServer { routing::get(handler::health).post(handler::health), ); + #[cfg(feature = "dashboard")] + { + router = router.nest("/dashboard", dashboard::dashboard()); + } + router // middlewares .layer( diff --git a/src/servers/src/http/dashboard.rs b/src/servers/src/http/dashboard.rs new file mode 100644 index 0000000000..3078aced55 --- /dev/null +++ b/src/servers/src/http/dashboard.rs @@ -0,0 +1,56 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use axum::body::{boxed, Full}; +use axum::http::{header, StatusCode, Uri}; +use axum::response::{IntoResponse, Response}; +use axum::routing::Router; +use common_telemetry::debug; +use rust_embed::RustEmbed; +use snafu::ResultExt; + +use crate::error::{BuildHttpResponseSnafu, Result}; + +#[derive(RustEmbed)] +#[folder = "dashboard/"] +pub struct Assets; + +pub(crate) fn dashboard() -> Router { + Router::new().fallback(static_handler) +} + +#[axum_macros::debug_handler] +async fn static_handler(uri: Uri) -> Result { + debug!("[dashboard] requesting: {}", uri.path()); + + let mut path = uri.path().trim_start_matches('/'); + if path.is_empty() { + path = "index.html"; + } + + match Assets::get(path) { + Some(content) => { + let body = boxed(Full::from(content.data)); + let mime = mime_guess::from_path(path).first_or_octet_stream(); + + Response::builder() + .header(header::CONTENT_TYPE, mime.as_ref()) + .body(body) + } + None => Response::builder() + .status(StatusCode::NOT_FOUND) + .body(boxed(Full::from("404"))), + } + .context(BuildHttpResponseSnafu) +}