diff --git a/Cargo.lock b/Cargo.lock index 554bcd2..d09e201 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -1293,6 +1299,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "harmony" version = "0.1.0" @@ -1310,7 +1335,7 @@ dependencies = [ "log", "opnsense-config", "opnsense-config-xml", - "reqwest", + "reqwest 0.11.27", "russh", "rust-ipmi", "semver", @@ -1529,7 +1554,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -1552,6 +1577,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.9", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -1627,6 +1653,22 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.11" @@ -1983,10 +2025,15 @@ version = "0.1.0" dependencies = [ "async-trait", "env_logger", + "futures-util", "log", "octocrab", "pretty_assertions", + "regex", + "reqwest 0.12.15", + "sha2", "tokio", + "url", ] [[package]] @@ -2095,7 +2142,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f0a8985e53d18c60dc82e7b5fa512fd194ea4c0d8bf1409b65cf44f8b0a8d9" dependencies = [ "log", - "reqwest", + "reqwest 0.11.27", "serde", "serde_derive", "serde_json", @@ -2976,11 +3023,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -2994,7 +3041,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tower-service", @@ -3005,6 +3052,52 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.9", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-tls 0.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "system-configuration 0.6.1", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -3780,6 +3873,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3800,7 +3896,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -3813,6 +3920,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -4378,6 +4495,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -4431,7 +4561,7 @@ dependencies = [ "windows-interface", "windows-link", "windows-result", - "windows-strings", + "windows-strings 0.4.0", ] [[package]] @@ -4462,6 +4592,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + [[package]] name = "windows-result" version = "0.3.2" @@ -4471,6 +4612,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.4.0" @@ -4531,13 +4681,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4550,6 +4716,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4562,6 +4734,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4574,12 +4752,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4592,6 +4782,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4604,6 +4800,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4616,6 +4818,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4628,6 +4836,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 1791b36..7219d71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ log = "0.4.22" env_logger = "0.11.5" derive-new = "0.7.0" async-trait = "0.1.82" -tokio = { version = "1.40.0", features = ["io-std", "fs"] } +tokio = { version = "1.40.0", features = ["io-std", "fs", "macros", "rt-multi-thread"] } cidr = "0.2.3" russh = "0.45.0" russh-keys = "0.45.0" diff --git a/k3d/Cargo.toml b/k3d/Cargo.toml index 8e5c781..633859d 100644 --- a/k3d/Cargo.toml +++ b/k3d/Cargo.toml @@ -15,9 +15,18 @@ env_logger = { workspace = true } async-trait = { workspace = true } tokio = { workspace = true } octocrab = "0.44.0" +regex = "1.11.1" +reqwest = { version = "0.12", features = ["stream"] } +#hyper-rustls = "0.27.5" +#hyper = { version = "1", features = [ "client" ] } +#hyper = { version = "1", features = ["full"] } +#http-body-util = "0.1" +#hyper-util = { version = "0.1", features = ["full"] } +url.workspace = true +sha2 = "0.10.8" +futures-util = "0.3.31" +#bytes = "1.10.1" #serde_json = "1.0.133" -#tokio-util = { version = "0.7.13", features = [ "codec" ] } -#tokio-stream = "0.1.17" [dev-dependencies] pretty_assertions = "1.4.1" diff --git a/k3d/src/downloadable_asset.rs b/k3d/src/downloadable_asset.rs new file mode 100644 index 0000000..cdf9bf2 --- /dev/null +++ b/k3d/src/downloadable_asset.rs @@ -0,0 +1,243 @@ +use futures_util::StreamExt; +use log::{debug, info, warn}; +use sha2::{Digest, Sha256}; +use std::io::Read; +use std::path::PathBuf; +use tokio::fs; +use tokio::fs::File; +use tokio::io::AsyncWriteExt; +use url::Url; + +#[derive(Debug)] +pub(crate) struct DownloadableAsset { + pub(crate) url: Url, + pub(crate) file_name: String, + pub(crate) checksum: String, +} + +impl DownloadableAsset { + fn verify_checksum(&self, file: PathBuf) -> bool { + if !file.exists() { + warn!("File does not exist: {:?}", file); + return false; + } + + let mut file = match std::fs::File::open(&file) { + Ok(file) => file, + Err(e) => { + warn!("Failed to open file for checksum verification: {:?}", e); + return false; + } + }; + + let mut hasher = Sha256::new(); + let mut buffer = [0; 1024 * 1024]; // 1MB buffer + + loop { + let bytes_read = match file.read(&mut buffer) { + Ok(0) => break, + Ok(n) => n, + Err(e) => { + warn!("Error reading file for checksum: {:?}", e); + return false; + } + }; + + hasher.update(&buffer[..bytes_read]); + } + + let result = hasher.finalize(); + let calculated_hash = format!("{:x}", result); + + debug!("Expected checksum: {}", self.checksum); + debug!("Calculated checksum: {}", calculated_hash); + + calculated_hash == self.checksum + } + + pub(crate) async fn download_to_path(&self, folder: PathBuf) -> Result { + if !folder.exists() { + fs::create_dir_all(&folder) + .await + .expect("Failed to create download directory"); + } + + let target_file_path = folder.join(&self.file_name); + debug!("Downloading to path: {:?}", target_file_path); + + if self.verify_checksum(target_file_path.clone()) { + debug!("File already exists with correct checksum, skipping download"); + return Ok(target_file_path); + } + + debug!("Downloading from URL: {}", self.url); + let client = reqwest::Client::new(); + let response = client + .get(self.url.clone()) + .send() + .await + .map_err(|e| format!("Failed to download file: {e}"))?; + + if !response.status().is_success() { + return Err(format!( + "Failed to download file, status: {}", + response.status() + )); + } + + let mut file = File::create(&target_file_path) + .await + .expect("Failed to create target file"); + + let mut stream = response.bytes_stream(); + while let Some(chunk_result) = stream.next().await { + let chunk = chunk_result.expect("Error while downloading file"); + file.write_all(&chunk) + .await + .expect("Failed to write data to file"); + } + + file.flush().await.expect("Failed to flush file"); + drop(file); + + if !self.verify_checksum(target_file_path.clone()) { + panic!("Downloaded file failed checksum verification"); + } + + info!( + "File downloaded and verified successfully: {}", + target_file_path.to_string_lossy() + ); + Ok(target_file_path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use std::net::TcpListener; + use std::sync::OnceLock; + use std::thread; + + const BASE_TEST_PATH: &str = "/tmp/harmony-test-k3d-download"; + const TEST_SERVER_PORT: u16 = 18452; + const TEST_CONTENT: &str = "This is a test file."; + const TEST_CONTENT_HASH: &str = + "f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de"; + + struct TestContext { + download_path: String, + domain: String, + } + + static TEST_SERVER: OnceLock<()> = OnceLock::new(); + + fn init_logs() { + let _ = env_logger::builder().try_init(); + } + + fn setup_test() -> TestContext { + init_logs(); + + TEST_SERVER.get_or_init(|| { + let listener = TcpListener::bind(format!("127.0.0.1:{}", TEST_SERVER_PORT)).unwrap(); + + thread::spawn(move || { + for stream in listener.incoming() { + thread::spawn(move || { + let mut stream = stream.expect("Stream opened correctly"); + let mut buffer = [0; 1024]; + let _ = stream.read(&mut buffer); + + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\n\r\n{}", + TEST_CONTENT.len(), + TEST_CONTENT + ); + + stream.write_all(response.as_bytes()).expect("Can write to stream"); + stream.flush().expect("Can flush stream"); + }); + } + }); + }); + + let test_id = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + let download_path = format!("{}/test_{}", BASE_TEST_PATH, test_id); + std::fs::create_dir_all(&download_path).unwrap(); + + assert!(wait_for_server_ready(1000), "Test server failed to start"); + + TestContext { + download_path, + domain: format!("127.0.0.1:{}", TEST_SERVER_PORT), + } + } + + fn wait_for_server_ready(timeout_ms: u64) -> bool { + let start = std::time::Instant::now(); + let timeout = std::time::Duration::from_millis(timeout_ms); + + while start.elapsed() < timeout { + if std::net::TcpStream::connect(format!("127.0.0.1:{}", TEST_SERVER_PORT)).is_ok() { + return true; + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + false + } + + #[tokio::test] + async fn test_download_to_path_success() { + let test = setup_test(); + + let asset = DownloadableAsset { + url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), + file_name: "test.txt".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), + }; + + let folder = PathBuf::from(&test.download_path); + let result = asset.download_to_path(folder).await.unwrap(); + + let downloaded_content = std::fs::read_to_string(result).unwrap(); + assert_eq!(downloaded_content, TEST_CONTENT); + } + + #[tokio::test] + async fn test_download_to_path_already_exists() { + let test = setup_test(); + let folder = PathBuf::from(&test.download_path); + + let asset = DownloadableAsset { + url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), + file_name: "test.txt".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), + }; + + let target_file_path = folder.join(&asset.file_name); + std::fs::write(&target_file_path, TEST_CONTENT).unwrap(); + + let result = asset.download_to_path(folder).await.unwrap(); + let content = std::fs::read_to_string(result).unwrap(); + assert_eq!(content, TEST_CONTENT); + } + + #[tokio::test] + async fn test_download_to_path_failure() { + let test = setup_test(); + + let asset = DownloadableAsset { + url: Url::parse("http://127.0.0.1:9999/test.txt").unwrap(), + file_name: "test.txt".to_string(), + checksum: "some_checksum".to_string(), + }; + + let result = asset.download_to_path(PathBuf::from(&test.download_path)).await; + assert!(result.is_err()); + } +} diff --git a/k3d/src/lib.rs b/k3d/src/lib.rs index 3227138..8e7fb72 100644 --- a/k3d/src/lib.rs +++ b/k3d/src/lib.rs @@ -1,12 +1,164 @@ +mod downloadable_asset; +use downloadable_asset::*; + +use log::{debug, info}; use std::path::PathBuf; +const K3D_BIN_FILE_NAME: &str = "k3d"; + pub struct K3d { + base_dir: PathBuf, } impl K3d { - pub async fn download_latest_release(&self) -> Result { + pub fn new(base_dir: PathBuf) -> Self { + Self { base_dir } + } + async fn get_binary_for_current_platform( + &self, + latest_release: octocrab::models::repos::Release, + ) -> DownloadableAsset { + let os = std::env::consts::OS; + let arch = std::env::consts::ARCH; + + debug!("Detecting platform: OS={}, ARCH={}", os, arch); + + // 2. Construct the binary name pattern based on platform + let binary_pattern = match (os, arch) { + ("linux", "x86") => "k3d-linux-386", + ("linux", "x86_64") => "k3d-linux-amd64", + ("linux", "arm") => "k3d-linux-arm", + ("linux", "aarch64") => "k3d-linux-arm64", + ("windows", "x86_64") => "k3d-windows-amd64.exe", + ("macos", "x86_64") => "k3d-darwin-amd64", + ("macos", "aarch64") => "k3d-darwin-arm64", + _ => panic!("Unsupported platform: {}-{}", os, arch), + }; + + debug!("Looking for binary matching pattern: {}", binary_pattern); + + // 3. Find the matching binary in release assets + let binary_asset = latest_release + .assets + .iter() + .find(|asset| asset.name == binary_pattern) + .unwrap_or_else(|| panic!("No matching binary found for {}", binary_pattern)); + + let binary_url = binary_asset.browser_download_url.clone(); + + // 4. Find and parse the checksums file + let checksums_asset = latest_release + .assets + .iter() + .find(|asset| asset.name == "checksums.txt") + .expect("Checksums file not found in release assets"); + + // 5. Download and parse checksums file + let checksums_url = checksums_asset.browser_download_url.clone(); + + let body = reqwest::get(checksums_url) + .await + .unwrap() + .text() + .await + .unwrap(); + println!("body: {body}"); + + // 6. Find the checksum for our binary + let checksum = body + .lines() + .find_map(|line| { + if line.ends_with(&binary_pattern) { + Some(line.split_whitespace().next().unwrap_or("").to_string()) + } else { + None + } + }) + .unwrap_or_else(|| panic!("Checksum not found for {}", binary_pattern)); + + debug!("Found binary at {} with checksum {}", binary_url, checksum); + + DownloadableAsset { + url: binary_url, + file_name: K3D_BIN_FILE_NAME.to_string(), + checksum, + } + } + + pub async fn download_latest_release(&self) -> Result { + let latest_release = self.get_latest_release_tag().await.unwrap(); + + let release_binary = self.get_binary_for_current_platform(latest_release).await; + info!("Foudn K3d binary to install : {release_binary:#?}"); + release_binary.download_to_path(self.base_dir.clone()).await + } + + // TODO : Make sure this will only find actual released versions, no prereleases or test + // builds + pub async fn get_latest_release_tag(&self) -> Result { + let octo = octocrab::instance(); + let latest_release = octo + .repos("k3d-io", "k3d") + .releases() + .get_latest() + .await + .map_err(|e| e.to_string())?; + // debug!("Got k3d releases {releases:#?}"); + println!("Got k3d first releases {latest_release:#?}"); + + Ok(latest_release) } } +#[cfg(test)] +mod test { + use regex::Regex; + use std::path::PathBuf; + use crate::{K3d, K3D_BIN_FILE_NAME}; + + #[tokio::test] + async fn k3d_latest_release_should_get_latest() { + let dir = get_clean_test_directory(); + + assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); + + let k3d = K3d::new(dir.clone()); + let latest_release = k3d.get_latest_release_tag().await.unwrap(); + + let tag_regex = Regex::new(r"^v\d+\.\d+\.\d+$").unwrap(); + assert!(tag_regex.is_match(&latest_release.tag_name)); + assert!(!latest_release.tag_name.is_empty()); + } + + #[tokio::test] + async fn k3d_download_latest_release_should_get_latest_bin() { + let dir = get_clean_test_directory(); + + assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); + + let k3d = K3d::new(dir.clone()); + let bin_file_path = k3d.download_latest_release().await.unwrap(); + assert_eq!(bin_file_path, dir.join(K3D_BIN_FILE_NAME)); + assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), true); + } + + fn get_clean_test_directory() -> PathBuf { + let dir = PathBuf::from("/tmp/harmony-k3d-test-dir"); + + if dir.exists() { + if let Err(e) = std::fs::remove_dir_all(&dir) { + // TODO sometimes this fails because of the race when running multiple tests at + // once + panic!("Failed to clean up test directory: {}", e); + } + } + + if let Err(e) = std::fs::create_dir_all(&dir) { + panic!("Failed to create test directory: {}", e); + } + + dir + } +}