Skip to main content

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