srtool_lib/
lib.rs

1mod container_engine;
2mod error;
3
4pub use container_engine::ContainerEngine;
5pub use error::*;
6
7use log::{debug, info};
8use std::{
9	env,
10	fs::{self, File},
11	io::Write,
12	process::Command,
13	time::{Duration, SystemTime},
14};
15
16const CACHE_FILE: &str = "srtool-tag-latest.txt";
17
18/// Fetch the latest image tag
19#[allow(clippy::result_large_err)]
20pub fn fetch_image_tag() -> Result<String> {
21	debug!("Fetching latest version from github");
22	let url = "https://raw.githubusercontent.com/paritytech/srtool/master/RUSTC_VERSION";
23
24	let tag = ureq::get(url).call()?.body_mut().read_to_string()?.trim().to_string();
25	debug!("tag: {}", tag);
26	Ok(tag)
27}
28
29/// Get the latest image. it is fetched from cache we have a version that is younger than `cache_validity` in seconds.
30#[allow(clippy::result_large_err)]
31pub fn get_image_tag(cache_validity: Option<u64>) -> Result<String> {
32	let env_tag = env::var("SRTOOL_TAG");
33	if let Ok(tag) = env_tag {
34		info!("using tag from ENV: {:?}", tag);
35		return Ok(tag);
36	}
37
38	let cache_location = std::env::temp_dir().join(CACHE_FILE);
39	debug!("cache_location = {:?}", cache_location);
40
41	let mut use_cache: bool = false;
42	if cache_location.exists() {
43		let metadata = fs::metadata(&cache_location).unwrap();
44		let last_modif = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
45		let validity = cache_validity.map(Duration::from_secs);
46
47		if let Some(duration) = validity {
48			let elapsed = last_modif.elapsed().unwrap();
49			if elapsed < duration {
50				use_cache = true;
51				debug!("cache is {:.2?} old (< max {:.2?})", elapsed, duration);
52			}
53		}
54	} else {
55		use_cache = false;
56		debug!("no cache");
57	}
58
59	let cached_value = fs::read_to_string(&cache_location).map(|s| s.trim_end().to_string());
60
61	match cached_value {
62		Ok(value) if use_cache => {
63			info!("using tag from cached value: {:?}", value);
64			Ok(value)
65		}
66		_ => {
67			let value = fetch_image_tag()?;
68			let mut file = File::create(cache_location)?;
69			file.write_all(value.as_bytes())?;
70			info!("using tag from fetched value: {:?}", value);
71
72			Ok(value)
73		}
74	}
75}
76
77pub fn clear_cache() -> Result<()> {
78	let cache_location = std::env::temp_dir().join(CACHE_FILE);
79	debug!("Deleting cache from {}", cache_location.display());
80	fs::remove_file(cache_location).map_err(SrtoolLibError::IO)
81}
82
83// docker inspect paritytech/srtool:1.53.0 | jq -r '.[0].RepoDigests[0]'
84pub fn get_image_digest(image: &str, tag: &str) -> Option<String> {
85	let command = format!("docker inspect {image}:{tag}");
86
87	let output = if cfg!(target_os = "windows") {
88		Command::new("cmd").args(["/C", command.as_str()]).output()
89	} else {
90		Command::new("sh").arg("-c").arg(command).output()
91	};
92
93	let output_str = String::from_utf8(output.unwrap().stdout).unwrap_or_else(|_| "".into());
94	let json: serde_json::Value = serde_json::from_str(&output_str).unwrap_or_default();
95	let digest_str = json[0]["RepoDigests"][0].as_str().unwrap_or_default();
96	let digest = digest_str.split(':').nth(1);
97	digest.map(String::from)
98}
99
100#[cfg(test)]
101mod tests {
102	use crate::{fetch_image_tag, get_image_digest};
103
104	#[test]
105	fn it_fetches_the_version() {
106		let tag = fetch_image_tag().unwrap();
107		println!("current tag = {tag:?}");
108		assert!(!tag.is_empty());
109	}
110
111	#[test]
112	#[ignore = "require docker installed + pulling the image"]
113	fn it_gets_the_image_digest() {
114		let image = "paritytech/srtool";
115		let tag = fetch_image_tag().unwrap();
116		println!("Checking digest for {image}:{tag}");
117		let result = get_image_digest(image, &tag);
118		assert!(result.is_some());
119	}
120}