aws_volume_provisioner_installer/
github.rs

1use 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
12/// Downloads the latest from the github release page.
13pub 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
23/// Downloads the official binaries from the GitHub release page.
24/// Returns the path to the binary path.
25///
26/// Leave "release_tag" none to download the latest.
27///
28/// Leave "arch" and "os" empty to auto-detect from its local system.
29/// "arch" must be either "amd64" or "arm64".
30/// "os" must be either "macos", "linux", or "win".
31/// ref. <https://github.com/ava-labs/volume-manager/releases>
32pub 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    // e.g., "v0.0.45"
39    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    // ref. <https://github.com/ava-labs/volume-manager/releases>
86    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    // ref. <https://github.com/ava-labs/volume-manager/releases>
104    // e.g., "aws-volume-provisioner.aarch64-ubuntu20.04-linux-gnu"
105    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
171/// ref. <https://github.com/ava-labs/volume-manager/releases>
172/// ref. <https://api.github.com/repos/ava-labs/volume-manager/releases/latest>
173pub 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/// ref. <https://api.github.com/repos/ava-labs/volume-manager/releases/latest>
215#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
216#[serde(rename_all = "snake_case")]
217pub struct ReleaseResponse {
218    /// Sometimes empty for github API consistency issue.
219    pub tag_name: Option<String>,
220    /// Sometimes empty for github API consistency issue.
221    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/// ref. <https://api.github.com/repos/ava-labs/volume-manager/releases/latest>
244#[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/// Represents the release "arch".
252#[derive(Eq, PartialEq, Clone)]
253pub enum Arch {
254    Amd64,
255    Arm64,
256}
257
258/// ref. https://doc.rust-lang.org/std/string/trait.ToString.html
259/// ref. https://doc.rust-lang.org/std/fmt/trait.Display.html
260/// Use "Self.to_string()" to directly invoke this
261impl 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/// Represents the release "os".
284#[derive(Eq, PartialEq, Clone)]
285pub enum Os {
286    MacOs,
287    Linux,
288    Ubuntu2004,
289}
290
291/// ref. https://doc.rust-lang.org/std/string/trait.ToString.html
292/// ref. https://doc.rust-lang.org/std/fmt/trait.Display.html
293/// Use "Self.to_string()" to directly invoke this
294impl 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
318/// Downloads a file to the "file_path".
319pub 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}