aws_ip_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", "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 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 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
163pub 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#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
208#[serde(rename_all = "snake_case")]
209pub struct ReleaseResponse {
210 pub tag_name: Option<String>,
212 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#[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#[derive(Eq, PartialEq, Clone)]
245pub enum Arch {
246 Amd64,
247 Arm64,
248}
249
250impl 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#[derive(Eq, PartialEq, Clone)]
277pub enum Os {
278 MacOs,
279 Linux,
280 Ubuntu2004,
281}
282
283impl 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
310pub 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}