diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..9e1afd01 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[alias] +build-backend="build -p backend --features swagger --no-default-features" +build-frontend="build -p frontend --no-default-features" +run-backend="run -p backend --features swagger --no-default-features" + diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index e5de610c..00000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM debian:buster-slim - -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y locales && \ - sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ - dpkg-reconfigure --frontend=noninteractive locales && \ - update-locale LANG=en_US.UTF-8 -ENV LANG en_US.UTF-8 - -# Rust -RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - build-essential \ - ca-certificates \ - curl \ - git \ - ssh \ - libssl-dev \ - pkg-config && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - -ENV RUSTUP_HOME=/rust -ENV CARGO_HOME=/cargo -ENV PATH=/cargo/bin:/rust/bin:$PATH - -RUN echo "(curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly-2021-06-23 --no-modify-path) \ - && rustup default nightly-2021-06-23 \ - && rustup component add rust-analysis \ - && rustup component add rust-src \ - && rustup component add rls" | sh \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 1ae0b11f..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "Bob-management Dev", - "build": { - "dockerfile": "Dockerfile" - }, - "extensions": [ - "rust-lang.rust", - "karunamurti.tera", - "bungcip.better-toml" - ] -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ada8be92..ae432828 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,14 @@ Cargo.lock **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information -*.pdb \ No newline at end of file +*.pdb + +/frontend/dist +/.cargo/.build +/.cargo/tmp +.env* +!.env.example +build.log +.DS_Store +/.idea/ + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..091d120d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +Bob Management GUI changelog + +## [Unreleased] + +#### Added + +- Initial project structure, backend only (#9) diff --git a/Cargo.toml b/Cargo.toml index 398db379..8846b6cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,32 @@ +[workspace.package] +version = "0.0.0" +authors = ["Romanov Simeon ArchArcheoss@proton.me"] +repository = "https://github.com/qoollo/bob-management" +readme = "./README.md" +license-file = "./LICENSE" +edition = "2021" + [workspace] -members = [ - "bob-management-server", -] \ No newline at end of file +members = [ "cli", "backend", "frontend" ] +default-members = [ "backend", "frontend" ] +resolver = "2" + +[profile.release] +# Optimize for size +# opt-level = "s" +# Optimize for speed +opt-level = 3 + +# Slightly increase perfomance and reduce binary size +panic = "abort" + +[profile.release-lto] +inherits = "release" +# Link Time optimization, causes a bit longer compilation +lto = true +# Maximize size reduction optimization, causes longer compilation +codegen-units = 1 + +[profile.min-size] +inherits = "release" +opt-level = "s" diff --git a/Rocket.toml b/Rocket.toml deleted file mode 100644 index dae8a6ab..00000000 --- a/Rocket.toml +++ /dev/null @@ -1,2 +0,0 @@ -[default] -template_dir = "bob-management-server/templates" \ No newline at end of file diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 00000000..891ef9ce --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "backend" +description = "Bob Management GUI" +publish = false +keywords = [ "BOB", "Management", "GUI" ] +version.workspace = true +authors.workspace = true +license-file.workspace = true +edition.workspace = true +readme.workspace = true +repository.workspace = true + +[dependencies] +# Backend (lib.rs) +## Axum related +axum = "0.6" +axum-macros = "0.3" +axum-login = "0.6" +axum-sessions = "0.5" +tower = "0.4" +tower-http = { version = "0.4", features = ["cors"] } + +## Logging +tracing = "0.1" +tracing-subscriber = "0.3" + +## Error Handling +error-stack = "0.4" +thiserror = "1.0" + +## General +tokio = { version = "1.32", features = ["rt", "macros", "rt-multi-thread"] } +hyper = "0.14" + +## OpenAPI + Swagger +utoipa = { version = "3.4", features = ["axum_extras", "chrono", "openapi_extensions"], optional = true } +utoipa-swagger-ui = { version = "3.1", features = ["axum"], optional = true } +utoipa-redoc = { version = "0.1", features = ["axum"], optional = true } +utoipa-rapidoc = { version = "0.1", features = ["axum"], optional = true } + +## CLI +cli = { path = "../cli" } + +[features] +default = [ "swagger" ] +swagger = [ "utoipa", "utoipa-swagger-ui" , "utoipa-redoc", "utoipa-rapidoc" ] + diff --git a/backend/src/config.rs b/backend/src/config.rs new file mode 100644 index 00000000..9cce6bd5 --- /dev/null +++ b/backend/src/config.rs @@ -0,0 +1,16 @@ +use cli::Config; +use tower_http::cors::CorsLayer; + +pub trait ConfigExt { + /// Return either very permissive [`CORS`](`CorsLayer`) configuration + /// or empty one based on `cors_allow_all` field + fn get_cors_configuration(&self) -> CorsLayer; +} + +impl ConfigExt for Config { + fn get_cors_configuration(&self) -> CorsLayer { + self.cors_allow_all + .then_some(CorsLayer::very_permissive()) + .unwrap_or_default() + } +} diff --git a/backend/src/connector/mod.rs b/backend/src/connector/mod.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/backend/src/connector/mod.rs @@ -0,0 +1 @@ + diff --git a/backend/src/error.rs b/backend/src/error.rs new file mode 100644 index 00000000..76485d9c --- /dev/null +++ b/backend/src/error.rs @@ -0,0 +1,24 @@ +#![allow(clippy::module_name_repetitions)] +use axum::response::{IntoResponse, Response}; +use hyper::StatusCode; +use thiserror::Error; + +/// Server start up errors +#[derive(Debug, Error)] +pub enum AppError { + #[error("Server initialization failed")] + InitializationError, + #[error("Server start up failed")] + StartUpError, +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + tracing::error!("{}", self); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Something went wrong".to_string(), + ) + .into_response() + } +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs new file mode 100644 index 00000000..7a075b01 --- /dev/null +++ b/backend/src/lib.rs @@ -0,0 +1,59 @@ +#![allow(clippy::multiple_crate_versions)] + +#[cfg(feature = "swagger")] +use axum::Router; +#[cfg(feature = "swagger")] +use utoipa::OpenApi; +pub mod config; +pub mod connector; +pub mod error; +pub mod models; +pub mod services; + +// [TEMP] +// TODO: Remove when the actual API will be implemented +#[allow(clippy::unused_async)] +#[cfg_attr(feature = "swagger", utoipa::path( + get, + path = "/", + responses( + (status = 200, description = "Hello Bob!") + ) + ))] +pub async fn root() -> &'static str { + "Hello Bob!" +} + +/// Generate openapi documentation for the project +#[cfg(feature = "swagger")] +pub fn openapi_doc() -> Router { + use utoipa_rapidoc::RapiDoc; + use utoipa_redoc::{Redoc, Servable}; + use utoipa_swagger_ui::SwaggerUi; + + /* Swagger-only routes */ + #[derive(OpenApi)] + #[openapi( + paths(root), + tags( + (name = "bob", description = "BOB management API") + ) + )] + struct ApiDoc; + /* Mount Swagger ui */ + Router::new() + .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) + .merge(Redoc::with_url("/redoc", ApiDoc::openapi())) + // There is no need to create `RapiDoc::with_openapi` because the OpenApi is served + // via SwaggerUi instead we only make rapidoc to point to the existing doc. + .merge(RapiDoc::new("/api-docs/openapi.json").path("/rapidoc")) + // Alternative to above + // .merge(RapiDoc::with_openapi("/api-docs/openapi2.json", ApiDoc::openapi()).path("/rapidoc")) +} + +pub mod prelude { + #![allow(unused_imports)] + pub use crate::error::AppError; + pub use axum::response::Result as AxumResult; + pub use error_stack::{Context, Report, Result, ResultExt}; +} diff --git a/backend/src/main.rs b/backend/src/main.rs new file mode 100644 index 00000000..b7a33864 --- /dev/null +++ b/backend/src/main.rs @@ -0,0 +1,55 @@ +#![allow(clippy::multiple_crate_versions)] + +use axum::{routing::get, Router}; +use backend::{config::ConfigExt, prelude::*, root, services::api_router}; +use cli::Parser; +use error_stack::{Result, ResultExt}; +use std::path::PathBuf; +use tower::ServiceBuilder; +use tower_http::cors::CorsLayer; +use tracing::Level; + +#[tokio::main] +#[allow(clippy::unwrap_used, clippy::expect_used)] +async fn main() -> Result<(), AppError> { + let config = cli::Config::try_from(cli::Args::parse()) + .change_context(AppError::InitializationError) + .attach_printable("Couldn't get config file.")?; + + let logger = &config.logger; + + init_tracer(&logger.log_file, logger.trace_level); + tracing::info!("Logger: {logger:?}"); + + let cors: CorsLayer = config.get_cors_configuration(); + tracing::info!("CORS: {cors:?}"); + + let addr = config.address; + tracing::info!("Listening on {addr}"); + + let app = router(cors); + #[cfg(feature = "swagger")] + let app = app.merge(backend::openapi_doc()); + + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .change_context(AppError::StartUpError) + .attach_printable("Failed to start axum server")?; + + Ok(()) +} + +fn init_tracer(_log_file: &Option, trace_level: Level) { + let subscriber = tracing_subscriber::fmt().with_max_level(trace_level); + subscriber.init(); +} + +fn router(cors: CorsLayer) -> Router { + // Add api + Router::new() + // Unsecured Routes + .route("/", get(root)) + .nest("/api", api_router()) + .layer(ServiceBuilder::new().layer(cors)) +} diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/backend/src/models/mod.rs @@ -0,0 +1 @@ + diff --git a/backend/src/services/mod.rs b/backend/src/services/mod.rs new file mode 100644 index 00000000..d32e834c --- /dev/null +++ b/backend/src/services/mod.rs @@ -0,0 +1,30 @@ +use axum::{ + response::{IntoResponse, Response}, + Router, +}; +use hyper::{Body, StatusCode}; +use thiserror::Error; + +/// Export all secured routes +#[allow(dead_code)] +pub fn api_router() -> Router<(), Body> { + Router::new() +} + +/// Errors that happend during API request proccessing +#[derive(Debug, Error)] +pub enum APIError { + #[error("The request to the specified resource failed")] + RequestFailed, + #[error("Server received invalid status code from client: `{0}`")] + InvalidStatusCode(StatusCode), +} + +impl IntoResponse for APIError { + fn into_response(self) -> Response { + match self { + Self::RequestFailed => (StatusCode::NOT_FOUND, self.to_string()).into_response(), + Self::InvalidStatusCode(code) => code.into_response(), + } + } +} diff --git a/bob-management-server/Cargo.toml b/bob-management-server/Cargo.toml deleted file mode 100644 index d682205e..00000000 --- a/bob-management-server/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "bob-management-server" -version = "0.1.0" -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -serde = "1.0.126" -jsonwebtoken = "7.2" -log = "0.4" -time = "0.2.27" -reqwest = { version = "0.11", features = ["json"] } - -[dependencies.rocket] -version = "0.5.0-rc.1" -features = ["secrets"] - -[dependencies.rocket_dyn_templates] -version = "0.1.0-rc.1" -features = ["tera"] \ No newline at end of file diff --git a/bob-management-server/src/main.rs b/bob-management-server/src/main.rs deleted file mode 100644 index b24eaba0..00000000 --- a/bob-management-server/src/main.rs +++ /dev/null @@ -1,25 +0,0 @@ -#[macro_use] -extern crate rocket; -#[macro_use] -extern crate log; - -use rocket::response::Redirect; -use rocket_dyn_templates::Template; - -mod routes; -mod services; -mod storages; - -#[get("/")] -fn default() -> Redirect { - Redirect::to(uri!(routes::auth::login::get)) -} - -#[launch] -fn rocket() -> _ { - let mut routes = routes::get_routes(); - routes.extend(routes![default]); - rocket::build() - .mount("/", routes) - .attach(Template::fairing()) -} diff --git a/bob-management-server/src/routes/auth/login.rs b/bob-management-server/src/routes/auth/login.rs deleted file mode 100644 index 443c6b43..00000000 --- a/bob-management-server/src/routes/auth/login.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::storages::session_data_storage::*; -use rocket::form::Form; -use rocket::http::CookieJar; -use rocket::response::Redirect; -use rocket_dyn_templates::Template; -use std::collections::BTreeMap; - -#[derive(FromForm)] -pub struct LoginUserInput { - pub cluster_addr: String, -} - -#[get("/auth/login")] -pub fn get(cookie_jar: &CookieJar) -> Template { - let mut context = BTreeMap::new(); - if let Some(addr) = cookie_jar.find_cluster_addr() { - debug!("Addr already set to {:?}!", addr); - context.insert("current_cluster_addr", addr.to_string()); - } - Template::render("auth/login", context) -} - -#[post("/auth/login", data = "")] -pub fn post(input: Form, cookie_jar: &CookieJar) -> Redirect { - debug!("received cluster addr {}", input.cluster_addr); - if let Ok(addr) = input.cluster_addr.parse() { - cookie_jar.save_cluster_addr(addr); - } - Redirect::to(uri!(crate::routes::cluster::index::get)) -} diff --git a/bob-management-server/src/routes/auth/mod.rs b/bob-management-server/src/routes/auth/mod.rs deleted file mode 100644 index 20521eb3..00000000 --- a/bob-management-server/src/routes/auth/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -use rocket::Route; - -pub mod login; - -pub fn get_routes() -> Vec { - routes![login::get, login::post] -} diff --git a/bob-management-server/src/routes/cluster/index.rs b/bob-management-server/src/routes/cluster/index.rs deleted file mode 100644 index cc31da98..00000000 --- a/bob-management-server/src/routes/cluster/index.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::services::bob::{get_nodes, is_active, Node}; -use crate::storages::session_data_storage::*; -use rocket::http::CookieJar; -use rocket::response::Redirect; -use rocket_dyn_templates::Template; -use serde::Serialize; - -#[derive(Serialize)] // TODO add proper eq and ord -pub struct NodeDto { - name: String, - addr: String, - active: bool, - vdisks: Vec, -} - -#[derive(Serialize)] -pub struct VDiskDto { - id: u32, - replicas: Vec, -} - -#[derive(Serialize)] -pub struct ReplicaDto { - disk: String, - path: String, -} - -impl NodeDto { - fn new(node: &Node, active: bool) -> Self { - let vdisks = node - .vdisks - .iter() - .filter(|vd| vd.replicas.iter().any(|r| r.node == node.name)) - .map(|vd| { - let replicas = vd - .replicas - .iter() - .filter(|r| r.node == node.name) - .map(|r| ReplicaDto { - disk: r.disk.clone(), - path: r.path.clone(), - }) - .collect(); - VDiskDto { - id: vd.id, - replicas, - } - }) - .collect(); - NodeDto { - name: node.name.clone(), - addr: node.address.to_string(), - active, - vdisks, - } - } -} - -#[derive(Serialize)] -pub struct IndexContext { - nodes: Vec, - error: Option, -} - -impl IndexContext { - fn from_nodes(nodes: Vec) -> Self { - Self { nodes, error: None } - } - - fn from_error(error: String) -> Self { - Self { - error: Some(error), - nodes: vec![], - } - } -} - -#[get("/cluster")] -pub async fn get(cookie_jar: &CookieJar<'_>) -> Result { - if let Some(addr) = cookie_jar.find_cluster_addr() { - let nodes_from_bob = get_nodes(&addr).await; - let context = match nodes_from_bob { - Ok(nodes) => { - let mut result = vec![]; - for node in &nodes { - let active = is_active(node).await.unwrap_or(false); - result.push(NodeDto::new(node, active)); - } - result.sort_by(|x, y| x.name.cmp(&y.name)); - IndexContext::from_nodes(result) - } - Err(e) => IndexContext::from_error(format!("{:?}", e)), - }; - - Ok(Template::render("cluster/index", context)) - } else { - Err(Redirect::to(uri!(crate::routes::auth::login::get))) - } -} diff --git a/bob-management-server/src/routes/cluster/mod.rs b/bob-management-server/src/routes/cluster/mod.rs deleted file mode 100644 index 2377d4f9..00000000 --- a/bob-management-server/src/routes/cluster/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -use rocket::Route; - -pub mod index; - -pub fn get_routes() -> Vec { - routes![index::get] -} diff --git a/bob-management-server/src/routes/mod.rs b/bob-management-server/src/routes/mod.rs deleted file mode 100644 index 7e76ed0f..00000000 --- a/bob-management-server/src/routes/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -use rocket::Route; - -pub mod auth; -pub mod cluster; - -pub fn get_routes() -> Vec { - vec![auth::get_routes(), cluster::get_routes()] - .into_iter() - .flatten() - .collect() -} diff --git a/bob-management-server/src/services/bob/mod.rs b/bob-management-server/src/services/bob/mod.rs deleted file mode 100644 index 03306a9f..00000000 --- a/bob-management-server/src/services/bob/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -pub use crate::services::bob::node::Node; -use std::net::SocketAddr; - -mod node; - -#[derive(Debug)] -pub enum BobError { - Unreachable, - UnexpectedResponse, -} - -pub async fn get_nodes(addr: &SocketAddr) -> Result, BobError> { - let resp = perform_request(get_addr(addr, "/nodes")).await?; - let result = resp - .json::>() - .await - .map_err(|_| BobError::UnexpectedResponse)?; - Ok(result) -} - -pub async fn is_active(node: &Node) -> Result { - let api_addr = node.get_api_addr(); - let _resp = perform_request(get_addr(&api_addr, "/status")).await?; - Ok(true) // If we reached this point request was completed -} - -async fn perform_request(addr: String) -> Result { - reqwest::get(addr).await.map_err(|e| { - error!("{:?}", e); - BobError::Unreachable - }) -} - -fn get_addr(node_addr: &SocketAddr, absolute_path: &str) -> String { - let mut result = String::from("http://"); - result.push_str(&node_addr.to_string()); - result.push_str(absolute_path); - result -} diff --git a/bob-management-server/src/services/bob/node.rs b/bob-management-server/src/services/bob/node.rs deleted file mode 100644 index 5a7c9e11..00000000 --- a/bob-management-server/src/services/bob/node.rs +++ /dev/null @@ -1,30 +0,0 @@ -use serde::Deserialize; -use std::net::SocketAddr; - -#[derive(Deserialize)] -pub struct Node { - pub name: String, - pub address: SocketAddr, - pub vdisks: Vec, -} - -impl Node { - pub fn get_api_addr(&self) -> SocketAddr { - let mut clone = self.address.clone(); - clone.set_port(8000); - clone - } -} - -#[derive(Deserialize)] -pub struct VDisk { - pub id: u32, - pub replicas: Vec, -} - -#[derive(Deserialize)] -pub struct Replica { - pub node: String, - pub disk: String, - pub path: String, -} \ No newline at end of file diff --git a/bob-management-server/src/services/mod.rs b/bob-management-server/src/services/mod.rs deleted file mode 100644 index 09c68797..00000000 --- a/bob-management-server/src/services/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod bob; \ No newline at end of file diff --git a/bob-management-server/src/storages/mod.rs b/bob-management-server/src/storages/mod.rs deleted file mode 100644 index cc9797d8..00000000 --- a/bob-management-server/src/storages/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod session_data_storage; \ No newline at end of file diff --git a/bob-management-server/src/storages/session_data_storage.rs b/bob-management-server/src/storages/session_data_storage.rs deleted file mode 100644 index 6d85ed00..00000000 --- a/bob-management-server/src/storages/session_data_storage.rs +++ /dev/null @@ -1,25 +0,0 @@ -use rocket::http::{Cookie, CookieJar}; -use std::net::SocketAddr; -use time::Duration; - -const ADDR_COOKIE_NAME: &'static str = "bob-cluster-addr"; - -pub trait SessionDataStorage { - fn save_cluster_addr(&self, addr: SocketAddr) -> bool; - fn find_cluster_addr(&self) -> Option; -} - -impl SessionDataStorage for CookieJar<'_> { - fn save_cluster_addr(&self, addr: SocketAddr) -> bool { - let cookie = Cookie::build(ADDR_COOKIE_NAME, addr.to_string()) - .max_age(Duration::days(365)) - .finish(); - self.add_private(cookie); - true - } - - fn find_cluster_addr(&self) -> Option { - self.get_private(ADDR_COOKIE_NAME) - .and_then(|c| c.value().parse().ok()) - } -} diff --git a/bob-management-server/templates/auth/login.html.tera b/bob-management-server/templates/auth/login.html.tera deleted file mode 100644 index 15987ce3..00000000 --- a/bob-management-server/templates/auth/login.html.tera +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "shared/base" %} - -{% block content %} -
-
- - - Example: 127.0.0.1:8000 - {% if current_cluster_addr %} - Currently set to: {{ current_cluster_addr }} - {% endif%} -
-
- -
-
-{% endblock content %} \ No newline at end of file diff --git a/bob-management-server/templates/cluster/index.html.tera b/bob-management-server/templates/cluster/index.html.tera deleted file mode 100644 index db21bd0c..00000000 --- a/bob-management-server/templates/cluster/index.html.tera +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "shared/base" %} - -{% block content %} -{% if error %} -

Error getting nodes: {{ error }}

-{% endif %} - -

Cluster nodes

- -{% for node in nodes %} -
-
{{node.name}}
-

{{node.addr}}

-
- {% for vdisk in node.vdisks %} -
- Vdisk {{ vdisk.id }} -
    - {% for replica in vdisk.replicas %} -
    -
  • {{ replica.disk }} at {{ replica.path }}
  • -
    -
- {% endfor %} -
- {% endfor %} -
-
-{% endfor %} -{% endblock content %} \ No newline at end of file diff --git a/bob-management-server/templates/shared/base.html.tera b/bob-management-server/templates/shared/base.html.tera deleted file mode 100644 index bd4eb144..00000000 --- a/bob-management-server/templates/shared/base.html.tera +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - Bob management - - {# Bootstrap #} - - - - - - - - {% include "shared/nav" %} -
- {% block content %}{% endblock content %} -
- - - - - - - \ No newline at end of file diff --git a/bob-management-server/templates/shared/nav.html.tera b/bob-management-server/templates/shared/nav.html.tera deleted file mode 100644 index 27d64b5a..00000000 --- a/bob-management-server/templates/shared/nav.html.tera +++ /dev/null @@ -1,15 +0,0 @@ -{# Copypaste from https://getbootstrap.com/docs/4.3/components/navbar/ #} - - \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..f328e4d9 --- /dev/null +++ b/build.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 00000000..25d39f46 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "cli" +description = "Bob Management GUI: CLI" +version.workspace = true +authors.workspace = true +license-file.workspace = true +edition.workspace = true +readme.workspace = true +repository.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9" +serde_with = "3.3" +humantime-serde = "1.1" +tracing = "0.1" + +# Error Handling +error-stack = "0.4" +thiserror = "1.0" + +# CLI +clap = { version = "4.4", features = ["derive", "cargo"] } + +# Meta-info +build-time = "0.1" +lazy_static="1.4" diff --git a/cli/build.rs b/cli/build.rs new file mode 100644 index 00000000..b6156cb7 --- /dev/null +++ b/cli/build.rs @@ -0,0 +1,38 @@ +use std::process::{Command, ExitStatus}; + +const GIT_HASH_VAR: &str = "BOBGUI_GIT_HASH"; +const BRANCH_TAG_VAR: &str = "BOBGUI_BUILD_BRANCH_TAG"; + +fn main() { + std::env::var(GIT_HASH_VAR).is_err().then(set_commit_hash); + std::env::var(BRANCH_TAG_VAR).is_err().then(set_branch_tag); +} + +fn set_commit_hash() { + set_env(GIT_HASH_VAR, "git", &["rev-parse", "HEAD"]); +} + +fn set_branch_tag() { + if !set_env(BRANCH_TAG_VAR, "git", &["describe", "--tags", "--abbrev=0"]).success() { + set_env( + BRANCH_TAG_VAR, + "git", + &["rev-parse", "--abbrev-ref", "HEAD"], + ); + } +} + +#[allow(clippy::unwrap_used)] +fn set_env(env_var: &str, cmd: &str, args: &[&str]) -> ExitStatus { + let mut command = Command::new(cmd); + command.args(args); + let output = command.output().unwrap(); + if command.status().unwrap().success() { + println!( + "cargo:rustc-env={env_var}={}", + String::from_utf8(output.stdout).unwrap() + ); + }; + + command.status().unwrap() +} diff --git a/cli/src/cli.rs b/cli/src/cli.rs new file mode 100644 index 00000000..7dd354fa --- /dev/null +++ b/cli/src/cli.rs @@ -0,0 +1,58 @@ +use crate::config::{Config, FromFile}; +use clap::{crate_authors, crate_version, Parser}; +use error_stack::Report; +use error_stack::ResultExt; +use std::path::PathBuf; +use thiserror::Error; + +lazy_static::lazy_static! { + static ref VERSION: String = { + format!( + concat!("BOB-GUI VERSION: {}\n", + "BUILT AT: {}\n", + "COMMIT HASH: {}\n", + "GIT BRANCH/TAG: {}\n"), + crate_version!(), + build_time::build_time_utc!(), + option_env!("BOBGUI_GIT_HASH").unwrap_or("-"), + option_env!("BOBGUI_BUILD_BRANCH_TAG").unwrap_or("-"), + ) + }; +} + +/// Bob configuration +#[derive(Debug, Parser, Clone)] +#[command(author = crate_authors!())] +#[command(version = VERSION.trim(), about, long_about)] +#[group(id = "configs", required = true, multiple = false)] +pub struct Args { + /// If set, passes default configuration to the server + #[clap(short, long)] + default: bool, + + /// Server configuration file + #[arg(short, long, value_name = "FILE")] + config_file: Option, +} + +impl TryFrom for Config { + type Error = Report; + + fn try_from(value: Args) -> Result { + if value.default { + Ok(Self::default()) + } else if let Some(config) = value.config_file { + Self::from_file(config).change_context(Error::Config) + } else { + unreachable!() + } + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("couldn't get logger configuration")] + Logger, + #[error("couldn't get server configuration")] + Config, +} diff --git a/cli/src/config.rs b/cli/src/config.rs new file mode 100644 index 00000000..10568eaf --- /dev/null +++ b/cli/src/config.rs @@ -0,0 +1,120 @@ +use error_stack::{Result, ResultExt}; +use serde::{de::DeserializeOwned, Deserialize}; +use serde_with::{serde_as, DisplayFromStr}; +use std::{fs::File, io::BufReader, net::SocketAddr, path::PathBuf, time::Duration}; +use thiserror::Error; + +/// Server Configuration passed on initialization +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Config { + /// Server address + pub address: SocketAddr, + + /// Enable Default Cors configuration + #[serde(default = "Config::default_cors")] + pub cors_allow_all: bool, + + /// Max Time to Responce, in milliseconds + #[serde(default = "Config::default_timeout")] + #[serde(with = "humantime_serde")] + pub request_timeout: Duration, + + /// [`Logger`](LoggerConfig) Configuration + #[serde(default)] + pub logger: LoggerConfig, +} + +/// Logger Configuration passed on initialization +#[allow(clippy::module_name_repetitions)] +#[serde_as] +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct LoggerConfig { + /// [Stub] File to save logs + pub log_file: Option, + + /// [Stub] Number of log files + #[serde(default = "LoggerConfig::default_log_amount")] + pub log_amount: usize, + + /// [Stub] Max size of a single log file, in bytes + #[serde(default = "LoggerConfig::default_log_size")] + pub log_size: u64, + + /// Tracing Level + #[serde(default = "LoggerConfig::tracing_default")] + #[serde_as(as = "DisplayFromStr")] + pub trace_level: tracing::Level, +} + +impl Default for Config { + fn default() -> Self { + Self { + address: SocketAddr::from(([0, 0, 0, 0], 7000)), + cors_allow_all: Self::default_cors(), + request_timeout: Self::default_timeout(), + logger: LoggerConfig::default(), + } + } +} + +impl Config { + pub const fn default_cors() -> bool { + false + } + + pub const fn default_timeout() -> Duration { + Duration::from_millis(5000) + } +} + +impl LoggerConfig { + pub const fn tracing_default() -> tracing::Level { + tracing::Level::INFO + } + + pub const fn default_log_amount() -> usize { + 5 + } + + pub const fn default_log_size() -> u64 { + 10u64.pow(6) + } +} + +impl Default for LoggerConfig { + fn default() -> Self { + Self { + log_file: None, + log_amount: Self::default_log_amount(), + log_size: Self::default_log_size(), + trace_level: Self::tracing_default(), + } + } +} + +pub trait FromFile { + /// Parses the file spcified in `path` + /// + /// # Errors + /// + /// The fucntion will fail if either it couldn't open config file + /// or failed to parse given file + fn from_file(path: PathBuf) -> Result + where + Self: Sized + DeserializeOwned, + { + let file = File::open(path).change_context(Error::FromFile)?; + let reader = BufReader::new(file); + serde_yaml::from_reader(reader).change_context(Error::FromFile) + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("configuration error: couldn't read from file")] + FromFile, +} + +impl FromFile for Config {} diff --git a/cli/src/lib.rs b/cli/src/lib.rs new file mode 100644 index 00000000..efdf62bc --- /dev/null +++ b/cli/src/lib.rs @@ -0,0 +1,6 @@ +pub mod cli; +mod config; + +pub use clap::Parser; +pub use cli::Args; +pub use config::{Config, FromFile, LoggerConfig}; diff --git a/config.yaml b/config.yaml new file mode 100644 index 00000000..5c7cc0cb --- /dev/null +++ b/config.yaml @@ -0,0 +1,3 @@ +address: 0.0.0.0:7000 +logger: + trace-level: INFO diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml new file mode 100644 index 00000000..ec7288af --- /dev/null +++ b/frontend/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "frontend" +build = "build.rs" +version.workspace = true +authors.workspace = true +license-file.workspace = true +edition.workspace = true +readme.workspace = true +repository.workspace = true + +[lib] +name = "frontend" +path = "build.rs" +crate-type = ["lib"] + +[build-dependencies] +tsync = "2" + +[dependencies] +tsync = "2" + + diff --git a/frontend/build.rs b/frontend/build.rs new file mode 100644 index 00000000..4aa58fcd --- /dev/null +++ b/frontend/build.rs @@ -0,0 +1,62 @@ +/// +/// Build Script +/// This is run as a pre-build step -- before the rust backend is compiled. +/// NOTE: Should be included in root's build script +/// +use std::fs::File; +use std::path::PathBuf; +use std::{io::Write, process::Command}; + +/* + * Note 1: this file was written for *nix systems -- it likely won't + * work on windows! + */ + +#[allow(dead_code)] +fn shell(command: &str) { + // println!("build.rs => {}", command); + + let output = Command::new("sh") + .arg("-c") + .arg(command) + .output() + .expect(format!("Failed to run {cmd}", cmd = command).as_str()); + + // println!("build.rs => {:?}", output.stdout); + let mut file = File::create("build.log").expect("Couldn't create file..."); + file.write(b"build log\n\n\n\nSTDOUT:\n") + .expect("Couldn't write to build log"); + file.write_all(&output.stdout) + .expect("Couldn't write to build log"); + file.write(b"\n\n\n\nSTDERR:\n") + .expect("Couldn't write to build log"); + file.write_all(&output.stderr) + .expect("Couldn't write to build log"); +} + +pub fn build_types() { + let mut inputs = vec![PathBuf::from(env!("CARGO_MANIFEST_DIR"))]; + let mut output = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + inputs[0].pop(); + + inputs[0].push("backend"); + output.push("src/types/rust.d.ts"); + + tsync::generate_typescript_defs(inputs, output, false); +} + +pub fn build_frontend() { + // Only install frontend dependencies when building release + // #[cfg(not(debug_assertions))] + shell("npm install --frozen-lockfile"); + + // Only build frontend when building a release + // #[cfg(not(debug_assertions))] + shell("npm build"); +} + +#[allow(dead_code)] +fn main() { + build_types(); + build_frontend(); +} diff --git a/frontend/src/types/rust.d.ts b/frontend/src/types/rust.d.ts new file mode 100644 index 00000000..32f45298 --- /dev/null +++ b/frontend/src/types/rust.d.ts @@ -0,0 +1 @@ +/* This file is generated and managed by tsync */