avalanche_telemetry_cloudwatch_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", "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 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 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
180pub 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#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
225#[serde(rename_all = "snake_case")]
226pub struct ReleaseResponse {
227 pub tag_name: Option<String>,
229 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#[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#[derive(Eq, PartialEq, Clone)]
262pub enum Arch {
263 Amd64,
264 Arm64,
265}
266
267impl 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#[derive(Eq, PartialEq, Clone)]
294pub enum Os {
295 MacOs,
296 Linux,
297 Ubuntu2004,
298}
299
300impl 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
327pub 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}