1static URL: &str = "https://cloud-images.ubuntu.com/locator/ec2/releasesTable?_=1588199609256";
23
24pub type StdError = Box<dyn std::error::Error + Send + Sync + 'static>;
25
26#[derive(Debug, Clone)]
27struct Entry {
28 region: String,
29 release_name: String,
30 release_number: String,
31 architecture: String,
32 instance_type: String,
33 date: String,
34 ami: String,
35 hvm: String,
36}
37
38pub async fn get_latest(
40 region: &str,
41 release_name: Option<&str>,
42 release_number: Option<&str>,
43 instance_type: Option<&str>,
44 architecture: Option<&str>,
45) -> Result<String, StdError> {
46 let mut r = reqwest::get(URL).await?.text().await?;
47
48 let len = r.len();
50 let (first, last) = r.split_at_mut(len - 10);
51 let mut r = first.to_string();
52 r.extend(last.replace(',', " ").chars());
53
54 let j: serde_json::Value = serde_json::from_str(&r)?;
56 let amis = j
57 .as_object()
58 .ok_or_else(|| String::from("Value not a JSON object"))?
59 .values()
60 .next()
61 .unwrap();
62
63 let mut amis: Vec<Entry> = amis
64 .as_array()
65 .ok_or_else(|| String::from("Value not a JSON array"))?
66 .into_iter()
67 .map(|v| {
68 let fs: Vec<&str> = v
69 .as_array()
70 .unwrap()
71 .into_iter()
72 .map(|s| s.as_str().unwrap())
73 .collect();
74 Entry {
75 region: fs[0].to_string(),
76 release_name: fs[1].to_string(),
77 release_number: fs[2].to_string(),
78 architecture: fs[3].to_string(),
79 instance_type: fs[4].to_string(),
80 date: fs[5].to_string(),
81 ami: fs[6].to_string(),
82 hvm: fs[7].to_string(),
83 }
84 })
85 .filter(|e| e.region == region)
86 .filter(|e| {
87 if let Some(release_name) = release_name {
88 e.release_name == release_name
89 } else {
90 true
91 }
92 })
93 .filter(|e| {
94 if let Some(release_number) = release_number {
95 e.release_number == release_number
96 } else {
97 true
98 }
99 })
100 .filter(|e| {
101 if let Some(instance_type) = instance_type {
102 e.instance_type == instance_type
103 } else {
104 true
105 }
106 })
107 .filter(|e| {
108 if let Some(architecture) = architecture {
109 e.architecture == architecture
110 } else {
111 true
112 }
113 })
114 .collect();
115 amis.sort_by_key(|e| e.date.clone());
116 let entry = amis
117 .pop()
118 .ok_or_else(|| anyhow::anyhow!("Could not find ami for criteria"))?;
119
120 Ok(parse_ami(&entry.ami)
121 .ok_or_else(|| anyhow::anyhow!("Failure parsing ami"))?
122 .to_string())
123}
124
125fn parse_ami(a_tag: &str) -> Option<&str> {
126 let start_idx = a_tag.find("ami-")?;
129 let end_idx = a_tag[start_idx + 4..].find(|c: char| !c.is_alphanumeric())? + start_idx + 4;
130 Some(&a_tag[start_idx..end_idx])
131}
132
133#[cfg(test)]
134mod test {
135 use super::parse_ami;
136
137 #[test]
138 fn test_ami_parse() {
139 let html = "<a href=\"https://console.aws.amazon.com/ec2/home?region=us-east-1#launchAmi=ami-085925f297f89fce1\">ami-085925f297f89fce1</a>";
140 let ami = parse_ami(html).unwrap();
141 assert_eq!(ami, "ami-085925f297f89fce1");
142 }
143}