diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..01def94 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.github +.env +target/ +tests/ +Dockerfile +web/dist +api/config/local.* diff --git a/Cargo.lock b/Cargo.lock index 2486487..71ad56b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,29 @@ dependencies = [ "tokio-util 0.7.0", ] +[[package]] +name = "actix-files" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81bde9a79336aa51ebed236e91fc1a0528ff67cfdf4f68ca4c61ede9fd26fb5" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "askama_escape", + "bitflags", + "bytes", + "derive_more", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", +] + [[package]] name = "actix-http" version = "3.0.4" @@ -243,6 +266,12 @@ version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + [[package]] name = "async-trait" version = "0.1.52" @@ -456,6 +485,7 @@ dependencies = [ name = "contextswitch-api" version = "0.1.0" dependencies = [ + "actix-files", "actix-http", "actix-web", "anyhow", @@ -490,6 +520,7 @@ dependencies = [ "reqwasm", "serde", "uikit-rs", + "wasm-bindgen", "wasm-bindgen-futures", "yew", ] @@ -939,6 +970,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.6.0" @@ -1152,6 +1189,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2202,6 +2249,15 @@ dependencies = [ "yew", ] +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.7" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8df9ff3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM lukemathwalker/cargo-chef:latest-rust-1.57.0 as chef +WORKDIR /app + +FROM chef as planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +FROM chef as builder +RUN cargo install cargo-make +RUN cargo install trunk +RUN rustup target add wasm32-unknown-unknown +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook -p contextswitch --release --recipe-path recipe.json +RUN cargo chef cook -p contextswitch-api --release --recipe-path recipe.json +RUN cargo chef cook -p contextswitch-web --release --recipe-path recipe.json --target wasm32-unknown-unknown +COPY . . +RUN cargo make build-release +RUN sed -i 's#http://localhost:8000/api#/api#' web/dist/snippets/contextswitch-web-*/js/api.js + +FROM debian:bullseye-slim AS runtime +WORKDIR /app +RUN mkdir /data +RUN apt-get update -y \ + && apt-get install -y --no-install-recommends openssl taskwarrior \ + && apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* +COPY --from=builder /app/target/release/contextswitch-api contextswitch +COPY --from=builder /app/api/config/default.toml config/default.toml +COPY --from=builder /app/web/dist/ . +ENV CS_TASKWARRIOR.DATA_LOCATION /data +ENV CS_APPLICATION.API_PATH /api +ENV CS_APPLICATION.STATIC_PATH / +CMD ["/app/contextswitch"] diff --git a/Makefile.toml b/Makefile.toml index 8621708..7b50e8b 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -5,7 +5,7 @@ CARGO_MAKE_CLIPPY_ARGS = "--tests -- -D warnings" [tasks.default] clear = true -alias = "run" +alias = "watch" [tasks.test] install_crate = "cargo-nextest" @@ -21,17 +21,14 @@ condition = {} workspace = false [tasks.run-api] -install_crate = { crate_name = "bunyan", binary = "bunyan" } -env = { "CONFIG_PATH" = "api/config", "TASKRC" = "$PWD/api/taskrc" } command = "bash" -args = ["-c", "cargo run -p contextswitch-api | bunyan"] +args = ["-c", "cd api; cargo make run"] workspace = false watch = { watch = ["./api/"], no_git_ignore = true } [tasks.run-web] -install_crate = { crate_name = "trunk", binary = "trunk" } command = "bash" -args = ["-c", "cd web; trunk serve"] +args = ["-c", "cd web; cargo make run"] workspace = false [tasks.run] @@ -39,16 +36,16 @@ run_task = { name = ["run-api", "run-web"], parallel = true, fork = true } workspace = false [tasks.watch-api] -watch = { watch = ["./api/"], no_git_ignore = true } command = "bash" -args = ["-c", "cd api; cargo make watch-flow"] +args = ["-c", "cd api; cargo make dev-test-flow"] workspace = false +watch = { watch = ["./api/"], no_git_ignore = true } [tasks.watch-web] -watch = { watch = ["./web/"], no_git_ignore = true } command = "bash" -args = ["-c", "cd web; cargo make watch-flow"] +args = ["-c", "cd web; cargo make dev-test-flow"] workspace = false +watch = { watch = ["./web/"], no_git_ignore = true } [tasks.watch-root] watch = { watch = ["./src/"], no_git_ignore = true } diff --git a/api/Cargo.toml b/api/Cargo.toml index 94d0eb6..9aaaea3 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -38,6 +38,7 @@ thiserror = "1.0" anyhow = "1.0" http = "0.2.0" config = "0.12.0" +actix-files = "0.6.0" [dev-dependencies] proptest = "1.0.0" diff --git a/api/Makefile.toml b/api/Makefile.toml index 8ee118d..b1456ab 100644 --- a/api/Makefile.toml +++ b/api/Makefile.toml @@ -1 +1,8 @@ extend = "../Makefile.toml" + +[tasks.run] +clear = true +install_crate = { crate_name = "bunyan", binary = "bunyan" } +env = { "TASKRC" = "$PWD/taskrc" } +command = "bash" +args = ["-c", "cargo run | bunyan"] diff --git a/api/config/default.toml b/api/config/default.toml index e84e4f7..46af7b0 100644 --- a/api/config/default.toml +++ b/api/config/default.toml @@ -2,6 +2,8 @@ port = 8000 # See https://docs.rs/tracing-subscriber/latest/tracing_subscriber/struct.EnvFilter.html log_directive = "info" +api_path = "" +front_base_url = "http://localhost:8080" [taskwarrior] data_location = "/tmp" diff --git a/api/config/dev.toml b/api/config/dev.toml index 30b7ad6..17a6814 100644 --- a/api/config/dev.toml +++ b/api/config/dev.toml @@ -1,2 +1,5 @@ [application] log_directive = "debug" +static_dir = "../web/dist" +api_path = "/api" +static_path = "" diff --git a/api/src/configuration.rs b/api/src/configuration.rs index ec10929..5c04d1f 100644 --- a/api/src/configuration.rs +++ b/api/src/configuration.rs @@ -12,6 +12,10 @@ pub struct Settings { pub struct ApplicationSettings { pub port: u16, pub log_directive: String, + pub front_base_url: String, + pub api_path: String, + pub static_path: Option, + pub static_dir: Option, } #[derive(Deserialize)] @@ -23,13 +27,21 @@ pub struct TaskwarriorSettings { impl Settings { pub fn new_from_file(file: Option) -> Result { let config_file_required = file.is_some(); - let config_file = - file.unwrap_or_else(|| env::var("CONFIG").unwrap_or_else(|_| "dev".into())); let config_path = env::var("CONFIG_PATH").unwrap_or_else(|_| "config".into()); + let config_file = file.unwrap_or_else(|| { + env::var("CONFIG_FILE").unwrap_or_else(|_| format!("{}/dev", &config_path).into()) + }); + + let default_config_file = format!("{}/default", config_path); + let local_config_file = format!("{}/local", config_path); + println!( + "Trying to load {:?} config files", + vec![&default_config_file, &local_config_file, &config_file] + ); let config = Config::builder() - .add_source(File::with_name(&format!("{}/default", config_path))) - .add_source(File::with_name(&format!("{}/local", config_path)).required(false)) + .add_source(File::with_name(&default_config_file)) + .add_source(File::with_name(&local_config_file).required(false)) .add_source(File::with_name(&config_file).required(config_file_required)) .add_source(Environment::with_prefix("cs")) .build()?; diff --git a/api/src/lib.rs b/api/src/lib.rs index 1677630..5f8e53f 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1,7 +1,9 @@ +use actix_files as fs; use actix_web::{dev::Server, http, middleware, web, App, HttpServer}; +use configuration::Settings; use core::time::Duration; -use std::env; use std::net::TcpListener; +use tracing::info; use tracing_actix_web::TracingLogger; #[macro_use] @@ -12,30 +14,55 @@ pub mod contextswitch; pub mod observability; pub mod routes; -pub fn run(listener: TcpListener) -> Result { - let cs_front_base_url = - env::var("CS_FRONT_BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); +pub fn run(listener: TcpListener, settings: &Settings) -> Result { + let api_path = settings.application.api_path.clone(); + let front_base_url = settings.application.front_base_url.clone(); + let static_path = settings.application.static_path.clone(); + let static_dir = settings + .application + .static_dir + .clone() + .unwrap_or_else(|| ".".to_string()); + let server = HttpServer::new(move || { - App::new() - .wrap(TracingLogger::default()) - .wrap(middleware::Compress::default()) + info!( + "Mounting API on {}", + if api_path.is_empty() { "/" } else { &api_path } + ); + let api_scope = web::scope(&api_path) .wrap( middleware::DefaultHeaders::new() - .add(("Access-Control-Allow-Origin", cs_front_base_url.as_bytes())) + .add(("Access-Control-Allow-Origin", front_base_url.as_bytes())) .add(( "Access-Control-Allow-Methods", "POST, GET, OPTIONS".as_bytes(), )) .add(("Access-Control-Allow-Headers", "content-type".as_bytes())), ) - .route("/ping", web::get().to(routes::ping)) .route("/tasks", web::get().to(routes::list_tasks)) .route("/tasks", web::post().to(routes::add_task)) .route("/tasks/{task_id}", web::put().to(routes::update_task)) .route( "/tasks", web::method(http::Method::OPTIONS).to(routes::option_task), - ) + ); + + let mut app = App::new() + .wrap(TracingLogger::default()) + .wrap(middleware::Compress::default()) + .route("/ping", web::get().to(routes::ping)) + .service(api_scope); + if let Some(path) = &static_path { + info!( + "Mounting static files on {}", + if path.is_empty() { "/" } else { &path } + ); + let static_scope = fs::Files::new(&path, &static_dir) + .use_last_modified(true) + .index_file("index.html"); + app = app.service(static_scope); + } + app }) .keep_alive(http::KeepAlive::Timeout(Duration::from_secs(60))) .shutdown_timeout(60) diff --git a/api/src/main.rs b/api/src/main.rs index 6313308..e45222c 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -13,5 +13,5 @@ async fn main() -> std::io::Result<()> { let listener = TcpListener::bind(format!("0.0.0.0:{}", settings.application.port)) .expect("Failed to bind port"); - run(listener)?.await + run(listener, &settings)?.await } diff --git a/api/tests/api/helpers.rs b/api/tests/api/helpers.rs index 0cf6f35..2500f22 100644 --- a/api/tests/api/helpers.rs +++ b/api/tests/api/helpers.rs @@ -12,12 +12,12 @@ fn setup_tracing(settings: &Settings) { init_subscriber(subscriber); } -fn setup_server() -> String { +fn setup_server(settings: &Settings) -> String { info!("Setting up server"); let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port"); let port = listener.local_addr().unwrap().port(); - let server = contextswitch_api::run(listener).expect("Failed to bind address"); + let server = contextswitch_api::run(listener, &settings).expect("Failed to bind address"); let _ = tokio::spawn(server); format!("http://127.0.0.1:{}", port) } @@ -38,6 +38,7 @@ pub fn app_address() -> String { let settings = Settings::new_from_file(Some("config/test".to_string())) .expect("Cannot load test configuration"); setup_tracing(&settings); + let address = setup_server(&settings); setup_taskwarrior(settings); - setup_server() + address } diff --git a/web/Cargo.toml b/web/Cargo.toml index 585e1bc..2b27557 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -18,3 +18,4 @@ reqwasm = "0.5" serde = { version = "1.0", features = ["derive"] } wasm-bindgen-futures = "0.4" uikit-rs = { git = "https://github.com/dax/uikit-rs.git" } +wasm-bindgen = "0.2.79" diff --git a/web/Makefile.toml b/web/Makefile.toml index 8ee118d..7c470ca 100644 --- a/web/Makefile.toml +++ b/web/Makefile.toml @@ -1 +1,12 @@ extend = "../Makefile.toml" + +[tasks.build-release] +install_crate = { crate_name = "trunk", binary = "trunk" } +command = "trunk" +args = ["build", "--release"] + +[tasks.run] +clear = true +install_crate = { crate_name = "trunk", binary = "trunk" } +command = "trunk" +args = ["serve"] diff --git a/web/js/api.js b/web/js/api.js new file mode 100644 index 0000000..3943d64 --- /dev/null +++ b/web/js/api.js @@ -0,0 +1,3 @@ +export function get_api_base_url() { + return "http://localhost:8000/api"; +} diff --git a/web/src/lib.rs b/web/src/lib.rs index 0f0fcf9..c190549 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -2,10 +2,16 @@ use components::tasks_list::TasksList; use contextswitch::Task; use reqwasm::http::Request; use uikit_rs as uk; +use wasm_bindgen::prelude::*; use yew::prelude::*; mod components; +#[wasm_bindgen(module = "/js/api.js")] +extern "C" { + fn get_api_base_url() -> String; +} + #[function_component(App)] pub fn app() -> Html { let tasks = use_state(Vec::new); @@ -15,7 +21,7 @@ pub fn app() -> Html { move |_| { wasm_bindgen_futures::spawn_local(async move { let fetched_tasks: Vec = - Request::get("http://localhost:8000/tasks?filter=task") + Request::get(&format!("{}/tasks?filter=task", get_api_base_url())) .send() .await .unwrap() // TODO