aws_volume_provisioner_installer/
github.rs1use std::{
2 env, fmt,
3 fs::{self, File},
4 io::{self, copy, Cursor, Error, ErrorKind},
5 os::unix::fs::PermissionsExt,
6};
7
8use reqwest::ClientBuilder;
9use serde::{Deserialize, Serialize};
10use tokio::time::{sleep, Duration};
11
12pub async fn download_latest(
14 arch: Option<Arch>,
15 os: Option<Os>,
16 target_file_path: &str,
17) -> io::Result<()> {
18 download(arch, os, None, target_file_path).await
19}
20
21pub const DEFAULT_TAG_NAME: &str = "latest";
22
23pub async fn download(
33 arch: Option<Arch>,
34 os: Option<Os>,
35 release_tag: Option<String>,
36 target_file_path: &str,
37) -> io::Result<()> {
38 let tag_name = if let Some(v) = release_tag {
40 v
41 } else {
42 log::info!("fetching the latest git tags");
43 let mut release_info = ReleaseResponse::default();
44 for round in 0..20 {
45 let info = match crate::github::fetch_latest_release("ava-labs", "volume-manager").await
46 {
47 Ok(v) => v,
48 Err(e) => {
49 log::warn!(
50 "failed fetch_latest_release {} -- retrying {}...",
51 e,
52 round + 1
53 );
54 sleep(Duration::from_secs((round + 1) * 3)).await;
55 continue;
56 }
57 };
58
59 release_info = info;
60 if release_info.tag_name.is_some() {
61 break;
62 }
63
64 log::warn!("release_info.tag_name is None -- retrying {}...", round + 1);
65 sleep(Duration::from_secs((round + 1) * 3)).await;
66 }
67
68 if release_info.tag_name.is_none() {
69 log::warn!("release_info.tag_name not found -- defaults to {DEFAULT_TAG_NAME}");
70 release_info.tag_name = Some(DEFAULT_TAG_NAME.to_string());
71 }
72
73 if release_info.prerelease {
74 log::warn!(
75 "latest release '{}' is prerelease, falling back to default tag name '{}'",
76 release_info.tag_name.unwrap(),
77 DEFAULT_TAG_NAME
78 );
79 DEFAULT_TAG_NAME.to_string()
80 } else {
81 release_info.tag_name.unwrap()
82 }
83 };
84
85 log::info!(
87 "detecting arch and platform for the release version tag {}",
88 tag_name
89 );
90 let arch = {
91 if arch.is_none() {
92 match env::consts::ARCH {
93 "x86_64" => String::from("x86_64"),
94 "aarch64" => String::from("aarch64"),
95 _ => String::from(""),
96 }
97 } else {
98 let arch = arch.unwrap();
99 arch.to_string()
100 }
101 };
102
103 let (file_name, fallback_file) = {
106 if os.is_none() {
107 if cfg!(target_os = "macos") {
108 (format!("aws-volume-provisioner.{arch}-apple-darwin"), None)
109 } else if cfg!(unix) {
110 (
111 format!("aws-volume-provisioner.{arch}-unknown-linux-gnu"),
112 None,
113 )
114 } else {
115 (String::new(), None)
116 }
117 } else {
118 let os = os.unwrap();
119 match os {
120 Os::MacOs => (format!("aws-volume-provisioner.{arch}-apple-darwin"), None),
121 Os::Linux => (
122 format!("aws-volume-provisioner.{arch}-unknown-linux-gnu"),
123 None,
124 ),
125 Os::Ubuntu2004 => (
126 format!("aws-volume-provisioner.{arch}-ubuntu20.04-linux-gnu"),
127 Some(format!("aws-volume-provisioner.{arch}-unknown-linux-gnu")),
128 ),
129 }
130 }
131 };
132 if file_name.is_empty() {
133 return Err(Error::new(
134 ErrorKind::Other,
135 format!("unknown platform '{}'", env::consts::OS),
136 ));
137 }
138
139 let download_url = format!(
140 "https://github.com/ava-labs/volume-manager/releases/download/{tag_name}/{file_name}",
141 );
142 log::info!("downloading {download_url}");
143 let tmp_file_path = random_manager::tmp_path(10, None)?;
144 match download_file(&download_url, &tmp_file_path).await {
145 Ok(_) => {}
146 Err(e) => {
147 log::warn!("failed to download {:?}", e);
148 if let Some(fallback) = fallback_file {
149 let download_url = format!(
150 "https://github.com/ava-labs/volume-manager/releases/download/{tag_name}/{fallback}",
151 );
152 log::warn!("falling back to {download_url}");
153 download_file(&download_url, &tmp_file_path).await?;
154 } else {
155 return Err(e);
156 }
157 }
158 }
159
160 {
161 let f = File::open(&tmp_file_path)?;
162 f.set_permissions(PermissionsExt::from_mode(0o777))?;
163 }
164 log::info!("copying {tmp_file_path} to {target_file_path}");
165 fs::copy(&tmp_file_path, &target_file_path)?;
166 fs::remove_file(&tmp_file_path)?;
167
168 Ok(())
169}
170
171pub async fn fetch_latest_release(org: &str, repo: &str) -> io::Result<ReleaseResponse> {
174 let ep = format!(
175 "https://api.github.com/repos/{}/{}/releases/latest",
176 org, repo
177 );
178 log::info!("fetching {}", ep);
179
180 let cli = ClientBuilder::new()
181 .user_agent(env!("CARGO_PKG_NAME"))
182 .danger_accept_invalid_certs(true)
183 .timeout(Duration::from_secs(15))
184 .connection_verbose(true)
185 .build()
186 .map_err(|e| {
187 Error::new(
188 ErrorKind::Other,
189 format!("failed ClientBuilder build {}", e),
190 )
191 })?;
192 let resp =
193 cli.get(&ep).send().await.map_err(|e| {
194 Error::new(ErrorKind::Other, format!("failed ClientBuilder send {}", e))
195 })?;
196 let out = resp
197 .bytes()
198 .await
199 .map_err(|e| Error::new(ErrorKind::Other, format!("failed ClientBuilder send {}", e)))?;
200 let out: Vec<u8> = out.into();
201
202 let resp: ReleaseResponse = match serde_json::from_slice(&out) {
203 Ok(p) => p,
204 Err(e) => {
205 return Err(Error::new(
206 ErrorKind::Other,
207 format!("failed to decode {}", e),
208 ));
209 }
210 };
211 Ok(resp)
212}
213
214#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
216#[serde(rename_all = "snake_case")]
217pub struct ReleaseResponse {
218 pub tag_name: Option<String>,
220 pub assets: Option<Vec<Asset>>,
222
223 #[serde(default)]
224 pub prerelease: bool,
225}
226
227impl Default for ReleaseResponse {
228 fn default() -> Self {
229 Self::default()
230 }
231}
232
233impl ReleaseResponse {
234 pub fn default() -> Self {
235 Self {
236 tag_name: None,
237 assets: None,
238 prerelease: false,
239 }
240 }
241}
242
243#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
245#[serde(rename_all = "snake_case")]
246pub struct Asset {
247 pub name: String,
248 pub browser_download_url: String,
249}
250
251#[derive(Eq, PartialEq, Clone)]
253pub enum Arch {
254 Amd64,
255 Arm64,
256}
257
258impl fmt::Display for Arch {
262 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
263 match self {
264 Arch::Amd64 => write!(f, "amd64"),
265 Arch::Arm64 => write!(f, "arm64"),
266 }
267 }
268}
269
270impl Arch {
271 pub fn new(arch: &str) -> io::Result<Self> {
272 match arch {
273 "amd64" => Ok(Arch::Amd64),
274 "arm64" => Ok(Arch::Arm64),
275 _ => Err(Error::new(
276 ErrorKind::InvalidInput,
277 format!("unknown arch {}", arch),
278 )),
279 }
280 }
281}
282
283#[derive(Eq, PartialEq, Clone)]
285pub enum Os {
286 MacOs,
287 Linux,
288 Ubuntu2004,
289}
290
291impl fmt::Display for Os {
295 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
296 match self {
297 Os::MacOs => write!(f, "macos"),
298 Os::Linux => write!(f, "linux"),
299 Os::Ubuntu2004 => write!(f, "ubuntu20.04"),
300 }
301 }
302}
303
304impl Os {
305 pub fn new(os: &str) -> io::Result<Self> {
306 match os {
307 "macos" => Ok(Os::MacOs),
308 "linux" => Ok(Os::Linux),
309 "ubuntu20.04" => Ok(Os::Ubuntu2004),
310 _ => Err(Error::new(
311 ErrorKind::InvalidInput,
312 format!("unknown os {}", os),
313 )),
314 }
315 }
316}
317
318pub async fn download_file(ep: &str, file_path: &str) -> io::Result<()> {
320 log::info!("downloading the file via {}", ep);
321 let resp = reqwest::get(ep)
322 .await
323 .map_err(|e| Error::new(ErrorKind::Other, format!("failed reqwest::get {}", e)))?;
324
325 let mut content = Cursor::new(
326 resp.bytes()
327 .await
328 .map_err(|e| Error::new(ErrorKind::Other, format!("failed bytes {}", e)))?,
329 );
330
331 let mut f = File::create(file_path)?;
332 copy(&mut content, &mut f)?;
333
334 Ok(())
335}