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