From cec84fa8d3fff44e01448796b78d4f7bc8588683 Mon Sep 17 00:00:00 2001 From: theMackabu <theMackabu@gmail.com> Date: Mon, 16 Oct 2023 22:59:27 -0700 Subject: [PATCH] client: commuication changes server: implement container system build: update gitignore --- .gitignore | 1 + Cargo.lock | 42 +++++ Maidfile.toml | 17 +- crates/maid/client/src/cli/mod.rs | 10 +- crates/maid/client/src/cli/tasks.rs | 5 +- crates/maid/client/src/helpers/logger.rs | 2 + crates/maid/client/src/server/cli.rs | 43 +++-- crates/maid/client/src/structs.rs | 19 +- crates/maid/server/Cargo.toml | 21 ++- crates/maid/server/src/docker/exec.rs | 0 crates/maid/server/src/docker/mod.rs | 2 +- crates/maid/server/src/docker/run.rs | 229 +++++++++++++++++++++++ crates/maid/server/src/globals.rs | 5 + crates/maid/server/src/helpers/file.rs | 65 +++++++ crates/maid/server/src/helpers/logger.rs | 25 +++ crates/maid/server/src/helpers/mod.rs | 15 +- crates/maid/server/src/helpers/string.rs | 17 ++ crates/maid/server/src/main.rs | 29 +-- crates/maid/server/src/structs.rs | 90 +++++++++ crates/maid/server/src/table.rs | 71 +++++++ scripts/build.toml | 62 +++--- 21 files changed, 691 insertions(+), 79 deletions(-) delete mode 100644 crates/maid/server/src/docker/exec.rs create mode 100644 crates/maid/server/src/docker/run.rs create mode 100644 crates/maid/server/src/helpers/file.rs create mode 100644 crates/maid/server/src/helpers/logger.rs create mode 100644 crates/maid/server/src/helpers/string.rs create mode 100644 crates/maid/server/src/table.rs diff --git a/.gitignore b/.gitignore index 3f67697..a3656a8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ target TODO.md bin .idea +build testing # prevent duplicate README diff --git a/Cargo.lock b/Cargo.lock index 58569c2..d12d6d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -641,6 +641,21 @@ dependencies = [ "libc", ] +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -657,6 +672,17 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.28" @@ -692,6 +718,7 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1184,20 +1211,35 @@ dependencies = [ name = "maid_server" version = "1.0.0" dependencies = [ + "anyhow", "bollard", + "bytes", "chrono", "clap", "clap-verbosity-flag", + "colored", "env_logger", "flate2", + "futures", + "futures-core", "futures-util", + "global_placeholders", + "home", + "indicatif", "libc", "log", "macros-rs", "ntapi", + "serde", + "serde_derive", "serde_json", + "tar", + "termcolor", + "text_placeholder", "tokio", + "tokio-util", "tungstenite", + "uuid", "warp", "winapi", ] diff --git a/Maidfile.toml b/Maidfile.toml index 484bdf8..ab79692 100644 --- a/Maidfile.toml +++ b/Maidfile.toml @@ -22,8 +22,7 @@ VERSION='1.0.0' info = "Build binaries" depends = ["clean"] script = [ - # "maid clean -q", - "cargo build --release", + "cargo zigbuild --release", "mv target/release/maid bin/maid", "mv target/release/maid_server bin/maid_server", "mv target/release/exit_test bin/exit_test" @@ -38,17 +37,15 @@ target = [ ] [tasks.build.remote] -image = "rust" +silent = true +exclusive = false +shell = "/bin/bash" +image = "rust:1.73" push = ["crates", "Cargo.toml", "Cargo.lock"] -pull = [ - "bin/maid", - "bin/exit_test", - "bin/maid_server" -] +pull = "bin" # basic task definition [tasks] api_server = { depends = ["build"], script = "./maid_server", path = "bin" } clean = { info = "Clean binary files", script = ["rm -rf bin", "mkdir bin"] } -install = { info = "Move binary file", script = ["sudo cp bin/maid /usr/local/bin", "echo Copied binary!"], depends = ["build"] } -buildall = { info = "build all", script = ["rm -rf build", "mkdir build", "maid _build_macos", "maid _build_linux", "maid _build_windows"] } \ No newline at end of file +install = { info = "Move binary file", script = ["sudo cp bin/maid /usr/local/bin", "echo Copied binary!"], depends = ["build"] } \ No newline at end of file diff --git a/crates/maid/client/src/cli/mod.rs b/crates/maid/client/src/cli/mod.rs index 19ceca5..432be7b 100644 --- a/crates/maid/client/src/cli/mod.rs +++ b/crates/maid/client/src/cli/mod.rs @@ -44,7 +44,11 @@ pub fn exec(task: &str, args: &Vec<String>, path: &String, silent: bool, is_dep: log::info!("Starting maid {}", env!("CARGO_PKG_VERSION")); if task.is_empty() { - tasks::List::all(path, silent, log_level); + if is_remote { + tasks::List::remote(path, silent, log_level); + } else { + tasks::List::all(path, silent, log_level); + } } else { let values = helpers::maidfile::merge(path); let project_root = parse::file::find_maidfile_root(path); @@ -58,6 +62,10 @@ pub fn exec(task: &str, args: &Vec<String>, path: &String, silent: bool, is_dep: crashln!("Maid could not find the remote task '{task}'. Does it exist?"); } + if is_remote && values.tasks.get(task).unwrap().remote.as_ref().unwrap().exclusive { + crashln!("Task '{task}' is remote only."); + } + if !is_remote { match &values.tasks[task].depends { Some(deps) => { diff --git a/crates/maid/client/src/cli/tasks.rs b/crates/maid/client/src/cli/tasks.rs index 5d33c81..e679aed 100644 --- a/crates/maid/client/src/cli/tasks.rs +++ b/crates/maid/client/src/cli/tasks.rs @@ -41,7 +41,10 @@ impl List { true => true, false => match task.hide { Some(val) => val, - None => false, + None => match task.remote.as_ref() { + Some(val) => val.exclusive, + None => false, + }, }, }; diff --git a/crates/maid/client/src/helpers/logger.rs b/crates/maid/client/src/helpers/logger.rs index 992efb1..bb072c7 100644 --- a/crates/maid/client/src/helpers/logger.rs +++ b/crates/maid/client/src/helpers/logger.rs @@ -7,6 +7,8 @@ macro_rules! log { ("warning", ("WARN", "yellow")), ("success", ("SUCCESS", "green")), ("notice", ("NOTICE", "bright blue")), + ("docker", ("DOCKER", "bright yellow")), + ("build", ("BUILD", "bright green")), ("info", ("INFO", "cyan")), ("debug", ("DEBUG", "magenta")), ] diff --git a/crates/maid/client/src/server/cli.rs b/crates/maid/client/src/server/cli.rs index 76504d4..ef637b0 100644 --- a/crates/maid/client/src/server/cli.rs +++ b/crates/maid/client/src/server/cli.rs @@ -1,14 +1,12 @@ use crate::helpers; use crate::server; -use crate::structs::{Maidfile, Task, Websocket}; -use crate::table; +use crate::structs::{ConnectionData, ConnectionInfo, Maidfile, Task, Websocket}; use colored::Colorize; use macros_rs::{crashln, fmtstr, then}; use reqwest::blocking::Client; -use text_placeholder::Template; use tungstenite::protocol::frame::{coding::CloseCode::Normal, CloseFrame}; -use tungstenite::{client::IntoClientRequest, connect as connectWSS, Message}; +use tungstenite::{client::connect_with_config, client::IntoClientRequest, protocol::WebSocketConfig, Message}; fn health(client: Client, values: Maidfile) -> server::api::health::Route { let address = server::parse::address(&values); @@ -56,7 +54,6 @@ pub fn connect(path: &String) { } pub fn remote(task: Task) { - let args = &task.args.clone(); let mut script: Vec<&str> = vec![]; if task.script.is_str() { @@ -90,28 +87,31 @@ pub fn remote(task: Task) { crate::log!("info", "connecting to {host}:{port}"); if body.status.healthy.data == "yes" { - crate::log!("info", "connected successfully"); + crate::log!("notice", "server reports healthy"); } else { crate::log!("warning", "failed to connect"); } + let websocket_config = WebSocketConfig { + max_frame_size: Some(314572800), + ..Default::default() + }; + let mut request = websocket.into_client_request().expect("Can't connect"); request.headers_mut().insert("Authorization", fmtstr!("Bearer {token}").parse().unwrap()); - let (mut socket, response) = connectWSS(request).expect("Can't connect"); + let (mut socket, response) = connect_with_config(request, Some(websocket_config), 3).expect("Can't connect"); log::debug!("response code: {}", response.status()); - let connection_data = serde_json::json!({ - "info": { - "name": &task.name, - "args": &task.args, - "remote": &task.remote, - "script": script, + let connection_data = ConnectionData { + info: ConnectionInfo { + name: task.name.clone(), + args: task.args.clone(), + remote: task.remote.clone().unwrap(), + script: script.clone().iter().map(|&s| s.to_string()).collect(), }, - "maidfile": Template::new_with_placeholder( - &task.maidfile.clone().to_json(), "%{", "}" - ).fill_with_hashmap(&table::create(task.maidfile.clone(), args, task.project)), - }); + maidfile: task.maidfile.clone(), + }; let file_name = match server::file::write_tar(&task.remote.unwrap().push) { Ok(name) => name, @@ -123,9 +123,6 @@ pub fn remote(task: Task) { log::debug!("sending information"); socket.send(Message::Text(serde_json::to_string(&connection_data).unwrap())).unwrap(); - log::debug!("sending archive"); - socket.send(Message::Binary(std::fs::read(&file_name).unwrap())).unwrap(); - loop { match socket.read() { Ok(Message::Text(text)) => { @@ -135,6 +132,12 @@ pub fn remote(task: Task) { crate::log!(level.as_str(), "{}", msg); } }); + + if data.get("binary").map_or(false, |d| d.as_bool().unwrap_or(false)) { + log::debug!("sending archive"); + socket.send(Message::Binary(std::fs::read(&file_name).unwrap())).unwrap(); + } + then!(data.get("done").map_or(false, |d| d.as_bool().unwrap_or(false)), break); } } diff --git a/crates/maid/client/src/structs.rs b/crates/maid/client/src/structs.rs index 6175911..3d8c778 100644 --- a/crates/maid/client/src/structs.rs +++ b/crates/maid/client/src/structs.rs @@ -70,8 +70,11 @@ pub struct CacheConfig { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Remote { pub push: Vec<String>, - pub pull: Vec<String>, + pub pull: String, pub image: String, + pub shell: String, + pub silent: bool, + pub exclusive: bool, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -113,3 +116,17 @@ pub struct Websocket { pub time: i64, pub data: JsonValue, } + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ConnectionInfo { + pub name: String, + pub remote: Remote, + pub args: Vec<String>, + pub script: Vec<String>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ConnectionData { + pub info: ConnectionInfo, + pub maidfile: Maidfile, +} diff --git a/crates/maid/server/Cargo.toml b/crates/maid/server/Cargo.toml index 180b8b7..509d0d5 100644 --- a/crates/maid/server/Cargo.toml +++ b/crates/maid/server/Cargo.toml @@ -7,21 +7,36 @@ repository.workspace = true description = "๐จ Build server for maid" [dependencies] -clap.workspace = true -clap-verbosity-flag.workspace = true -env_logger.workspace = true +tar.workspace = true log.workspace = true +uuid.workspace = true +clap.workspace = true flate2.workspace = true +colored.workspace = true +env_logger.workspace = true +clap-verbosity-flag.workspace = true +global_placeholders.workspace = true # make workspace +home = "0.5.5" warp = "0.3.6" +bytes = "1.5.0" ntapi = "0.4.1" libc = "0.2.149" winapi = "0.3.9" +anyhow = "1.0.75" +serde = "1.0.188" chrono = "0.4.31" +futures = "0.3.28" bollard = "0.15.0" +termcolor = "1.3.0" macros-rs = "0.4.0" +indicatif = "0.17.7" +tokio-util = "0.7.9" tungstenite = "0.20.1" serde_json = "1.0.107" +futures-core = "0.3.28" futures-util = "0.3.28" +serde_derive = "1.0.188" +text_placeholder = "0.5.0" tokio = { version = "1.33.0", features = ["full"] } \ No newline at end of file diff --git a/crates/maid/server/src/docker/exec.rs b/crates/maid/server/src/docker/exec.rs deleted file mode 100644 index e69de29..0000000 diff --git a/crates/maid/server/src/docker/mod.rs b/crates/maid/server/src/docker/mod.rs index 52f443c..25fb1fe 100644 --- a/crates/maid/server/src/docker/mod.rs +++ b/crates/maid/server/src/docker/mod.rs @@ -1,2 +1,2 @@ -pub mod exec; +pub mod run; pub mod container; \ No newline at end of file diff --git a/crates/maid/server/src/docker/run.rs b/crates/maid/server/src/docker/run.rs new file mode 100644 index 0000000..d6c3fd7 --- /dev/null +++ b/crates/maid/server/src/docker/run.rs @@ -0,0 +1,229 @@ +use crate::table; +use crate::structs::ConnectionData; + +use bytes::Bytes; +use bollard::{Docker, errors::Error}; +use macros_rs::{string, fmtstr, str}; +use warp::ws::{Message, WebSocket}; +use bollard::image::CreateImageOptions; +use futures_util::{SinkExt, StreamExt}; +use futures_core::Stream; +use futures_util::stream::{SplitSink, SplitStream, TryStreamExt}; +use bollard::exec::{CreateExecOptions, StartExecResults}; +use bollard::container::{Config, RemoveContainerOptions, UploadToContainerOptions, DownloadFromContainerOptions}; +use std::default::Default; +use warp::hyper::Body; +use std::sync::Arc; +use tokio::sync::Mutex; +use std::path::PathBuf; +use text_placeholder::Template; +use flate2::{Compression, write::GzEncoder}; +use std::io::Write; + +pub async fn concat_byte_stream<S>(s: S) -> Result<Vec<u8>, Error> +where + S: Stream<Item = Result<Bytes, Error>>, +{ + s.try_fold(Vec::new(), |mut acc, chunk| async move { + acc.extend_from_slice(&chunk[..]); + Ok(acc) + }) + .await +} + +// add error handling to all the unwraps +pub async fn exec(tx: SplitSink<WebSocket, Message>, mut rx: SplitStream<WebSocket>, docker: Docker) -> Result<(), Box<dyn std::error::Error + 'static>> { + let mut parsed: Option<ConnectionData> = None; + let tx_ref = Arc::new(Mutex::new(tx)); + + while parsed.is_none() { + if let Some(result) = rx.next().await { + let msg = result.unwrap(); + match serde_json::from_str::<ConnectionData>(msg.to_str().unwrap()) { + Ok(value) => { + parsed = Some(value); + } + Err(err) => { + eprintln!("Failed to deserialize JSON: {:?}", err); + } + } + } + } + + let parsed = parsed.unwrap(); + let name = &parsed.info.name; + + println!("creating container for task [{name}]"); + docker + .create_image( + Some(CreateImageOptions { + from_image: str!(parsed.info.remote.image.clone()), + ..Default::default() + }), + None, + None, + ) + .for_each(|msg| { + let tx_ref = Arc::clone(&tx_ref); + + async move { + let msg = msg.as_ref().expect("Failed to get CreateImageInfo"); + let formatted = format!( + "{} {}", + msg.status.clone().unwrap_or_else(|| string!("Waiting")), + msg.progress.clone().unwrap_or_else(|| string!("")) + ); + + let mut tx_lock = tx_ref.lock().await; + tx_lock.send(Message::text( + serde_json::to_string(&serde_json::json!({ + "level": "docker", + "time": chrono::Utc::now().timestamp_millis(), + "data": { "message": formatted }, + })) + .unwrap(), + )) + .await; + } + }) + .await; + + let config = Config { + image: Some(parsed.info.remote.image), + tty: Some(true), + ..Default::default() + }; + + let id = docker.create_container::<&str, String>(None, config).await?.id; + println!("created container"); + + docker.start_container::<String>(&id, None).await?; + println!("started container"); + + let tx_ref = Arc::clone(&tx_ref); + let mut tx_lock = tx_ref.lock().await; + + tx_lock.send(Message::text( + serde_json::to_string(&serde_json::json!({ + "level": "success", + "time": chrono::Utc::now().timestamp_millis(), + "data": { "binary": true }, + })) + .unwrap(), + )) + .await + .unwrap(); + + if let Some(result) = rx.next().await { + println!("received message: binary"); + + let msg = result.unwrap(); + fn bytes_to_body(bytes: &[u8]) -> Body { + Body::from(bytes.to_vec()) + } + + // note: this `Result` may be an `Err` variant, which should be handled + // help: use `let _ = ...` to ignore the resulting value + docker.upload_to_container(&id, Some(UploadToContainerOptions{ path: "/opt", ..Default::default() }), bytes_to_body(&msg.as_bytes())).await; + println!("wrote tarfile to container"); + } + + let dependencies = match &parsed.maidfile.tasks[&parsed.info.name].depends { + Some(deps) => { + let mut dep_script: Vec<String> = vec![]; + for item in deps.iter() { + dep_script.push( + parsed.maidfile.tasks[item] + .script + .as_array() + .map(|arr| { + arr.iter() + .map(|val| val.as_str().unwrap_or_default()) + .collect::<Vec<_>>() + .join("\n") + }) + .unwrap_or_default() + ); + }; + dep_script.join("\n") + } + None => { string!("") } + }; + + // move common things such as structs and helpers to seperate crate + let table = table::create(parsed.maidfile.clone(), &parsed.info.args, PathBuf::new().join("/opt")); + let script = Template::new_with_placeholder(str!(parsed.info.script.join("\n")), "%{", "}").fill_with_hashmap(&table); + let dependencies = Template::new_with_placeholder(str!(dependencies), "%{", "}").fill_with_hashmap(&table); + + let exec = docker + .create_exec( + &id, + CreateExecOptions { + attach_stdout: Some(true), + attach_stderr: Some(true), + cmd: Some(vec![str!(parsed.info.remote.shell), "-c", fmtstr!("cd /opt && touch script.sh && echo '{dependencies}\n{script}' > script.sh && chmod +x script.sh && ./script.sh")]), + ..Default::default() + }, + ) + .await? + .id; + + if let StartExecResults::Attached { mut output, .. } = docker.start_exec(&exec, None).await? { + tx_lock.send(Message::text( + serde_json::to_string(&serde_json::json!({ + "level": "build", + "time": chrono::Utc::now().timestamp_millis(), + "data": { "message": "waiting for build to finish..." }, + })) + .unwrap(), + )) + .await + .unwrap(); + + while let Some(Ok(msg)) = output.next().await { + if !parsed.info.remote.silent { + let parsed = format!("{msg}"); + if parsed != "" { + tx_lock.send(Message::text( + serde_json::to_string(&serde_json::json!({ + "level": "build", + "time": chrono::Utc::now().timestamp_millis(), + "data": { "message": parsed.trim() }, + })) + .unwrap(), + )) + .await + .unwrap(); + } + } + } + } + + let res = docker.download_from_container(&id, Some(DownloadFromContainerOptions{ path: fmtstr!("/opt/{}", parsed.info.remote.pull.clone()) })); + let bytes = concat_byte_stream(res).await?; + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + + encoder.write_all(&bytes)?; + let compressed_data = encoder.finish()?; + + tx_lock.send(Message::binary(compressed_data)).await.unwrap(); + println!("sent message: binary, from [{}]", parsed.info.remote.pull); + + tx_lock.send(Message::text( + serde_json::to_string(&serde_json::json!({ + "level": "success", + "time": chrono::Utc::now().timestamp_millis(), + "data": { "done": true, "message": "" }, + })) + .unwrap(), + )) + .await + .unwrap(); + println!("sent message: [done]"); + + + println!("deleted old container"); + // delete container if socket closed + docker.remove_container(&id, Some(RemoveContainerOptions { force: true, ..Default::default() })).await?; + Ok(()) +} diff --git a/crates/maid/server/src/globals.rs b/crates/maid/server/src/globals.rs index e69de29..e39512f 100644 --- a/crates/maid/server/src/globals.rs +++ b/crates/maid/server/src/globals.rs @@ -0,0 +1,5 @@ +use global_placeholders::init; + +pub fn init() { + init!("maid.temp_dir", "/usr/tmp/maid"); +} diff --git a/crates/maid/server/src/helpers/file.rs b/crates/maid/server/src/helpers/file.rs new file mode 100644 index 0000000..498eb20 --- /dev/null +++ b/crates/maid/server/src/helpers/file.rs @@ -0,0 +1,65 @@ +use crate::helpers; + +use flate2::{read::GzDecoder, write::GzEncoder, Compression}; +use global_placeholders::global; +use macros_rs::crashln; +use std::{fs::write, fs::File, path::PathBuf}; +use tar::{Archive, Builder}; +use uuid::Uuid; + +fn append_to_tar(builder: &mut Builder<GzEncoder<File>>, path: &String) -> Result<(), std::io::Error> { + let pathbuf = PathBuf::from(path); + + if pathbuf.is_file() { + builder.append_path(&pathbuf)?; + } else if pathbuf.is_dir() { + builder.append_dir_all(&pathbuf, &pathbuf)?; + } + Ok(()) +} + +pub fn remove_tar(file: &String) { + if let Err(_) = std::fs::remove_file(file) { + crashln!("Unable to remove temporary archive. does it exist?"); + } +} + +pub fn read_tar(archive: &Vec<u8>) -> Result<String, std::io::Error> { + if !helpers::Exists::folder(global!("maid.temp_dir")).unwrap() { + std::fs::create_dir_all(global!("maid.temp_dir")).unwrap(); + log::info!("created maid temp dir"); + } + + let file_name = format!("{}/{}.tgz", global!("maid.temp_dir"), Uuid::new_v4()); + write(&file_name, archive)?; + + Ok(file_name) +} + +pub fn unpack_tar(path: &String) -> std::io::Result<()> { + let archive = File::open(&path)?; + let tar = GzDecoder::new(archive); + let mut archive = Archive::new(tar); + + archive.unpack(".") +} + +pub fn write_tar(files: &Vec<String>) -> Result<String, std::io::Error> { + if !helpers::Exists::folder(global!("maid.temp_dir")).unwrap() { + std::fs::create_dir_all(global!("maid.temp_dir")).unwrap(); + log::info!("created maid temp dir"); + } + + let file_name = format!("{}/{}.tgz", global!("maid.temp_dir"), Uuid::new_v4()); + let archive = File::create(&file_name)?; + let enc = GzEncoder::new(archive, Compression::default()); + let mut tar = Builder::new(enc); + + log::info!("compressing to {}", &file_name); + for path in files { + append_to_tar(&mut tar, path)?; + log::info!("{} {:?}", helpers::string::add_icon(), path); + } + + Ok(file_name) +} diff --git a/crates/maid/server/src/helpers/logger.rs b/crates/maid/server/src/helpers/logger.rs new file mode 100644 index 0000000..992efb1 --- /dev/null +++ b/crates/maid/server/src/helpers/logger.rs @@ -0,0 +1,25 @@ +#[macro_export] +macro_rules! log { + ($level:expr, $($arg:tt)*) => { + let level_colors: std::collections::HashMap<&str, (&str, &str)> = [ + ("fatal", ("FATAL", "bright red")), + ("error", ("ERROR", "red")), + ("warning", ("WARN", "yellow")), + ("success", ("SUCCESS", "green")), + ("notice", ("NOTICE", "bright blue")), + ("info", ("INFO", "cyan")), + ("debug", ("DEBUG", "magenta")), + ] + .iter() + .cloned() + .collect(); + + match level_colors.get($level) { + Some((level_text, color_func)) => { + let level_text = level_text.color(color_func.to_string()); + println!("{} {}", level_text, format_args!($($arg)*).to_string()) + } + None => println!("Unknown log level: {}", $level), + } + }; +} diff --git a/crates/maid/server/src/helpers/mod.rs b/crates/maid/server/src/helpers/mod.rs index 265a13a..34eaa3e 100644 --- a/crates/maid/server/src/helpers/mod.rs +++ b/crates/maid/server/src/helpers/mod.rs @@ -1,2 +1,15 @@ +use anyhow::Error; +use macros_rs::str; +use std::path::Path; + +pub struct Exists; +impl Exists { + pub fn folder(dir_name: String) -> Result<bool, Error> { Ok(Path::new(str!(dir_name)).is_dir()) } + pub fn file(file_name: String) -> Result<bool, Error> { Ok(Path::new(str!(file_name)).exists()) } +} + pub mod os; -pub mod format; \ No newline at end of file +pub mod file; +pub mod format; +pub mod string; +pub mod logger; \ No newline at end of file diff --git a/crates/maid/server/src/helpers/string.rs b/crates/maid/server/src/helpers/string.rs new file mode 100644 index 0000000..b02f4af --- /dev/null +++ b/crates/maid/server/src/helpers/string.rs @@ -0,0 +1,17 @@ +use colored::{ColoredString, Colorize}; +use std::path::Path; + +pub fn seperator() -> ColoredString { ":".white() } +pub fn arrow_icon() -> ColoredString { "ยป".white() } +pub fn add_icon() -> ColoredString { "+".green() } +pub fn cross_icon() -> ColoredString { "โ".red() } +pub fn check_icon() -> ColoredString { "โ".green() } + +pub fn path_to_str(path: &Path) -> &'static str { Box::leak(String::from(path.to_string_lossy()).into_boxed_str()) } + +pub fn trim_start_end(value: &str) -> &str { + let mut chars = value.chars(); + chars.next(); + chars.next_back(); + chars.as_str() +} diff --git a/crates/maid/server/src/main.rs b/crates/maid/server/src/main.rs index 880331f..881ef78 100644 --- a/crates/maid/server/src/main.rs +++ b/crates/maid/server/src/main.rs @@ -1,5 +1,8 @@ mod helpers; mod docker; +mod globals; +mod structs; +mod table; use futures_util::{SinkExt, StreamExt}; use macros_rs::{fmtstr, ternary}; @@ -11,6 +14,8 @@ use std::env; #[tokio::main] async fn main() { + globals::init(); + let port = 3500; let token = "test_token".to_string(); @@ -19,7 +24,8 @@ async fn main() { let gateway = warp::path!("ws" / "gateway").and(warp::ws()).map(|ws: warp::ws::Ws| { ws.on_upgrade(|websocket| async { - let (mut tx, mut rx) = websocket.split(); + let (mut tx, rx) = websocket.split(); + let socket = Docker::connect_with_socket_defaults().unwrap(); tx.send(Message::text( serde_json::to_string(&serde_json::json!({ @@ -32,22 +38,7 @@ async fn main() { .await .unwrap(); - tx.send(Message::binary(tokio::fs::read("../testing/test.tgz").await.unwrap())).await.unwrap(); - tx.send(Message::text( - serde_json::to_string(&serde_json::json!({ - "level": "success", - "time": chrono::Utc::now().timestamp_millis(), - "data": { "done": true, "message": "" }, - })) - .unwrap(), - )) - .await - .unwrap(); - - while let Some(result) = rx.next().await { - let message = result.unwrap(); - println!("received message: {:?}", message); - } + docker::run::exec(tx, rx, socket).await.unwrap(); }) }); @@ -61,9 +52,7 @@ async fn health_handler() -> Result<impl Reply, Infallible> { let uptime = helpers::format::duration(helpers::os::uptime()); let version = format!("Docker v{} (build {})", &info.version.clone().unwrap(), &info.git_commit.clone().unwrap()); - - println!("{:#?}", info.clone()); - + Ok(warp::reply::json(&serde_json::json!({ "version": { "data": format!("v{}", env!("CARGO_PKG_VERSION")), diff --git a/crates/maid/server/src/structs.rs b/crates/maid/server/src/structs.rs index e69de29..aba6238 100644 --- a/crates/maid/server/src/structs.rs +++ b/crates/maid/server/src/structs.rs @@ -0,0 +1,90 @@ +use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::BTreeMap; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ConnectionInfo { + pub name: String, + pub args: Vec<String>, + pub remote: Remote, + pub script: Vec<String>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ConnectionData { + pub info: ConnectionInfo, + pub maidfile: Maidfile, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Maidfile { + #[serde(skip_serializing_if = "Option::is_none")] + pub import: Option<Vec<String>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub env: Option<BTreeMap<String, Value>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub project: Option<Project>, + pub tasks: BTreeMap<String, Tasks>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Project { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub server: Option<Server>, // wip +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Server { + pub address: Address, // wip + pub token: String, // wip +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Address { + pub host: String, + pub port: i64, + pub ssl: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Tasks { + pub script: Value, + #[serde(skip_serializing_if = "Option::is_none")] + pub hide: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub info: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cache: Option<Cache>, + #[serde(skip_serializing_if = "Option::is_none")] + pub remote: Option<Remote>, + #[serde(skip_serializing_if = "Option::is_none")] + pub depends: Option<Vec<String>>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Cache { + pub path: String, + pub target: Vec<String>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CacheConfig { + pub target: Vec<String>, + pub hash: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Remote { + pub push: Vec<String>, + pub pull: String, + pub image: String, + pub shell: String, + pub silent: bool, + pub exclusive: bool, +} diff --git a/crates/maid/server/src/table.rs b/crates/maid/server/src/table.rs new file mode 100644 index 0000000..8001057 --- /dev/null +++ b/crates/maid/server/src/table.rs @@ -0,0 +1,71 @@ +use crate::helpers; +use crate::structs::Maidfile; + +use colored::Colorize; +use macros_rs::{errorln, str, ternary}; +use serde_json::{json, Value}; +use std::path::PathBuf; +use std::{collections::BTreeMap, collections::HashMap, env}; +use text_placeholder::Template; + +pub fn create(values: Maidfile, args: &Vec<String>, project: PathBuf) -> HashMap<&str, &str> { + let mut table = HashMap::new(); + let empty_env: BTreeMap<String, Value> = BTreeMap::new(); + + table.insert("os.platform", env::consts::OS); + table.insert("os.arch", env::consts::ARCH); + + log::info!("{} os.platform: '{}'", helpers::string::add_icon(), env::consts::OS.yellow()); + log::info!("{} os.arch: '{}'", helpers::string::add_icon(), env::consts::ARCH.yellow()); + + match env::current_dir() { + Ok(path) => { + table.insert("dir.current", helpers::string::path_to_str(&path)); + log::info!("{} dir.current: '{}'", helpers::string::add_icon(), helpers::string::path_to_str(&path).yellow()); + } + Err(err) => { + log::warn!("{err}"); + errorln!("Current directory could not be added as script variable."); + } + } + + match home::home_dir() { + Some(path) => { + table.insert("dir.home", helpers::string::path_to_str(&path)); + log::info!("{} dir.home: '{}'", helpers::string::add_icon(), helpers::string::path_to_str(&path).yellow()); + } + None => { + errorln!("Home directory could not be added as script variable."); + } + } + + let project_root = helpers::string::path_to_str(&project); + table.insert("dir.project", project_root); + log::info!("{} dir.project: '{}'", helpers::string::add_icon(), project_root.yellow()); + + for (pos, arg) in args.iter().enumerate() { + log::info!("{} arg.{pos}: '{}'", helpers::string::add_icon(), arg.yellow()); + table.insert(str!(format!("arg.{pos}")), arg); + } + + let user_env = match &values.env { + Some(env) => env.iter(), + None => empty_env.iter(), + }; + + for (key, value) in user_env { + let value_formatted = ternary!( + value.to_string().starts_with("\""), + helpers::string::trim_start_end(str!(Template::new_with_placeholder(&value.to_string(), "%{", "}").fill_with_hashmap(&table))).replace("\"", "\\\""), + str!(Template::new_with_placeholder(&value.to_string(), "%{", "}").fill_with_hashmap(&table)).replace("\"", "\\\"") + ); + + env::set_var(key, value_formatted.clone()); + log::info!("{} env.{key}: '{}'", helpers::string::add_icon(), value_formatted.yellow()); + table.insert(str!(format!("env.{}", key.clone())), str!(value_formatted)); + } + + log::trace!("{}", json!({ "env": table })); + + return table; +} diff --git a/scripts/build.toml b/scripts/build.toml index 7defecf..1ee5bd0 100644 --- a/scripts/build.toml +++ b/scripts/build.toml @@ -1,24 +1,44 @@ -# hidden tasks (can also hide with `hide = true`) -[tasks._build_macos] +# task intended to run on remote only +[tasks.build_all] +info = "build all" script = [ -"cargo build --release", -"mv target/release/exact-maid build/maid", -"zip build/maid_%{env.VERSION}_darwin_amd64.zip build/maid", -"rm build/maid", + # install packages + "apt-get update", + "apt-get install zip mingw-w64 -y", + "mkdir build", + + # build linux (x86_64) + "cargo zigbuild -r -p maid", + "mv target/release/maid build/maid", + "zip build/maid_%{env.VERSION}_linux_amd64.zip build/maid", + "rm build/maid", + + # build windows (x86_64) + "cargo zigbuild -r -p maid --target x86_64-pc-windows-gnu", + "mv target/x86_64-pc-windows-gnu/release/maid.exe build/maid.exe", + "zip build/maid_%{env.VERSION}_windows_amd64.zip build/maid.exe", + "rm build/maid.exe", + + # build macos (x86_64) + "cargo zigbuild -r -p maid --target x86_64-apple-darwin", + "mv target/x86_64-apple-darwin/release/maid build/maid", + "zip build/maid_%{env.VERSION}_darwin_amd64.zip build/maid", + "rm build/maid", + + # build macos (aarch64) + "cargo zigbuild -r -p maid --target aarch64-apple-darwin", + "mv target/aarch64-apple-darwin/release/maid build/maid", + "zip build/maid_%{env.VERSION}_darwin_arm.zip build/maid", + "rm build/maid", + + # post build + "ls -sh build", ] -[tasks._build_linux] -script = [ -"cargo build --release --target x86_64-unknown-linux-musl", -"mv target/release/exact-maid build/maid", -"zip build/maid_%{env.VERSION}_linux_amd64.zip build/maid", -"rm build/maid", -] - -[tasks._build_windows] -script = [ -"cargo build --release --target x86_64-pc-windows-gnu", -"mv target/release/exact-maid build/maid", -"zip build/maid_%{env.VERSION}_windows_amd64.zip build/maid", -"rm build/maid", -] +[tasks.build_all.remote] +silent = false +exclusive = true +shell = "/bin/bash" +image = "messense/cargo-zigbuild:latest" +push = ["crates", "Cargo.toml", "Cargo.lock"] +pull = "build" -- GitLab