feat: download and install k3d latest release
- Implemented functionality to fetch the latest k3d release tag from GitHub. - Added logic to determine the appropriate binary URL based on the current platform. - Implemented downloading and saving the binary to a specified directory. - Included unit tests to verify the download and installation process. - Added a `K3D_BIN_FILE_NAME` constant for clarity. - Added logging for better debugging.
This commit is contained in:
parent
847d84b46f
commit
15785dd219
232
Cargo.lock
generated
232
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
243
k3d/src/downloadable_asset.rs
Normal file
243
k3d/src/downloadable_asset.rs
Normal file
@ -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<PathBuf, String> {
|
||||
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());
|
||||
}
|
||||
}
|
154
k3d/src/lib.rs
154
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<PathBuf, String> {
|
||||
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<PathBuf, String> {
|
||||
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<octocrab::models::repos::Release, String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user