diff --git a/Cargo.toml b/Cargo.toml index 4d94bc9..4014dce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-std = { version = "1", features = ["attributes"] } +chrono = "0.4.15" +serde = { version = "1.0", features = ["derive"] } +dotenv = "~0.15.0" +driftwood = "0" +handlebars = { version = "~3.4.0", features = ["dir_source"] } +html-escape = "~0.2.6" +log = "~0.4.8" +pretty_env_logger = "~0.3.1" +serde_json = "~1.0.0" +sqlx = { version = "~0.5.1", features = ["chrono", "json", "offline", "sqlite", "uuid", "runtime-async-std-rustls"] } +tide = "0" +uuid = { version = "~0.8.1", features = ["v4", "serde"]} diff --git a/README.adoc b/README.adoc index 1dac47b..c97c51c 100644 --- a/README.adoc +++ b/README.adoc @@ -3,3 +3,38 @@ Janky is a simple CI system built in Rust. This is performative coding and not intended to be a production system you can actually use. + + +* Two binaries: + * `janky-server` + * Listens HTTP + * Does web shit + * Interacts with agents + * `janky-agent`: + * Run workloads + * Listen HTTP + * executes commands + +[source] +---- +┌────────┐ │ │http +│ │sqlite3 │ Agent │ws +│ Server │http │ │ +│ │ws └──────┬─────┘ +└───┬────┘ │ + │ │ + │ What are your caps? │ + ├──────────────────────────────────────────────────►│ + │ │ + │ │ + │ git,svn,bash,rustc,cargo,websocket │ + │◄──────────────────────────────────────────────────┤ + │ │ + │ great, here's some commands │ + ├──────────────────────────────────────────────────►│ + │ │ + │ │ + │ kewl, here's the logs, id, etc │ + │◄──────────────────────────────────────────────────┤ + │ │ +---- diff --git a/src/main.rs b/src/main.rs index e7a11a9..3afa2a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,144 @@ -fn main() { - println!("Hello, world!"); +/* + * This is the main Janky entrypoint for the server" + */ + +use async_std::sync::{Arc, RwLock}; +use dotenv::dotenv; +use handlebars::Handlebars; +use log::*; + +#[derive(Clone, Debug)] +pub struct AppState<'a> { + hb: Arc>>, +} + +impl AppState<'_> { + fn new() -> Self { + Self { + hb: Arc::new(RwLock::new(Handlebars::new())), + } + } + + pub async fn register_templates(&self) -> Result<(), handlebars::TemplateFileError> { + let mut hb = self.hb.write().await; + hb.clear_templates(); + hb.register_templates_directory(".hbs", "views") + } + + pub async fn render( + &self, + name: &str, + data: &serde_json::Value, + ) -> Result { + /* + * In debug mode, reload the templates on ever render to avoid + * needing a restart + */ + #[cfg(debug_assertions)] + { + self.register_templates().await; + } + let hb = self.hb.read().await; + let view = hb.render(name, data)?; + Ok(tide::Body::from_string(view)) + } +} + +/** + * The routes module contains all the tide routes and the logic to fulfill the responses for each + * route. + * + * Modules are nested for cleaner organization here + */ +mod routes { + use crate::AppState; + use log::*; + use std::collections::HashMap; + use tide::{Body, Request, StatusCode}; + use uuid::Uuid; + + /** + * Helper function to pull out a :uuid parameter from the path + */ + fn get_uuid_param(req: &Request>) -> Result { + let uuid = req.param::("uuid"); + + if uuid.is_err() { + return Err(tide::Error::from_str( + StatusCode::BadRequest, + "No uuid specified", + )); + } + + debug!("Fetching poll: {:?}", uuid); + + match Uuid::parse_str(&uuid.unwrap()) { + Err(_) => Err(tide::Error::from_str( + StatusCode::BadRequest, + "Invalid uuid specified", + )), + Ok(uuid) => Ok(uuid), + } + } + + /** + * GET / + */ + pub async fn index(req: Request>) -> Result { + let params = json!({ + "page": "home" + }); + let mut body = req.state().render("index", ¶ms).await?; + body.set_mime("text/html"); + Ok(body) + } + + pub mod api { + use log::*; + use tide::{Body, Request, Response, StatusCode}; + + use crate::AppState; + } +} + +#[async_std::main] +async fn main() -> Result<(), tide::Error> { + pretty_env_logger::init(); + dotenv().ok(); + + //let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let state = AppState::new(); + state.register_templates().await; + let mut app = tide::with_state(state); + + #[cfg(not(debug_assertions))] + { + info!("Activating RELEASE mode configuration"); + app.with(driftwood::ApacheCombinedLogger); + } + + #[cfg(debug_assertions)] + { + info!("Activating DEBUG mode configuration"); + info!("Enabling a very liberal CORS policy for debug purposes"); + use tide::security::{CorsMiddleware, Origin}; + let cors = CorsMiddleware::new() + .allow_methods( + "GET, POST, PUT, OPTIONS" + .parse::() + .unwrap(), + ) + .allow_origin(Origin::from("*")) + .allow_credentials(false); + + app.with(cors); + } + /* + * All builds will have apidocs, since they're handy + */ + app.at("/static").serve_dir("static/")?; + debug!("Configuring routes"); + app.at("/").get(routes::index); + app.listen("0.0.0.0:8000").await?; + Ok(()) } diff --git a/static/.gitignore b/static/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/views/.gitignore b/views/.gitignore new file mode 100644 index 0000000..e69de29