Skip to main content

ubuntu_ami/
lib.rs

1//! Get your fresh, farm-to-table, single-origin ec2 Ubuntu AMIs.
2//!
3//! # Example
4//! ```rust
5//! use ubuntu_ami::*;
6//!
7//! #[tokio::main]
8//! async fn main() -> Result<(), StdError> {
9//!     let res = get_latest(
10//!         "us-east-1",
11//!         Some("bionic"),
12//!         None,
13//!         Some("hvm:ebs-ssd"),
14//!         Some("amd64"),
15//!     )
16//!     .await?;
17//!     println!("us-east-1 ubuntu:bionic: {}", res);
18//!     Ok(())
19//! }
20//! ```
21
22static 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
38/// Get the most recent Ubuntu AMI that matches the given criteria.
39pub 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    // get rid of the trailing comma
49    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    // parse to json
55    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    // example:
127    // "<a href=\"https://console.aws.amazon.com/ec2/home?region=us-east-1#launchAmi=ami-085925f297f89fce1\">ami-085925f297f89fce1</a>"
128    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}