Skip to main content

isb_core/sandbox/
images.rs

1//! The images a project can create instances from without a remote, and
2//! what to say when a spec names one that is not there.
3
4use serde_json::Value;
5
6use crate::client::Client;
7use crate::error::Error;
8
9/// A remote image that every host can pull: what to suggest when a local
10/// one is missing.
11pub const REMOTE_DEFAULT: &str = "images:ubuntu/24.04";
12
13/// A local image by alias, with its description.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct LocalImage {
16    pub alias: String,
17    pub description: String,
18}
19
20/// The aliases of `/1.0/images?recursion=1`, sorted (an image with two
21/// aliases is listed twice; one without any is left out).
22pub fn aliases(images: &Value) -> Vec<LocalImage> {
23    let mut out: Vec<LocalImage> = images
24        .as_array()
25        .into_iter()
26        .flatten()
27        .flat_map(|i| {
28            let description = i["properties"]["description"]
29                .as_str()
30                .unwrap_or_default()
31                .to_string();
32            i["aliases"]
33                .as_array()
34                .into_iter()
35                .flatten()
36                .filter_map(|a| a["name"].as_str())
37                .map(move |a| LocalImage {
38                    alias: a.to_string(),
39                    description: description.clone(),
40                })
41        })
42        .collect();
43    out.sort_by(|a, b| a.alias.cmp(&b.alias));
44    out
45}
46
47/// The images the client's project can use by alias.
48pub fn local(client: &Client) -> crate::error::Result<Vec<LocalImage>> {
49    Ok(aliases(&client.get("/1.0/images?recursion=1")?))
50}
51
52/// The error for a local image that is not there: which ones are, and how
53/// to name a remote one instead.
54pub(crate) fn missing(alias: &str, have: &[LocalImage]) -> Error {
55    const SHOWN: usize = 12;
56    let list = if have.is_empty() {
57        "this host has no local images".to_string()
58    } else {
59        let names: Vec<&str> = have.iter().take(SHOWN).map(|i| i.alias.as_str()).collect();
60        let more = have.len().saturating_sub(SHOWN);
61        format!(
62            "local images: {}{}",
63            names.join(", "),
64            if more > 0 {
65                format!(" and {more} more")
66            } else {
67                String::new()
68            }
69        )
70    };
71    Error::invalid(format!(
72        "image {alias:?} not found locally ({list}); name a remote image with its server instead, e.g. {REMOTE_DEFAULT}"
73    ))
74}
75
76/// [`missing`] with the local images read from `client` (none if they
77/// cannot be read).
78pub(crate) fn missing_on(client: &Client, alias: &str) -> Error {
79    missing(alias, &local(client).unwrap_or_default())
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use serde_json::json;
86
87    #[test]
88    fn lists_aliases_and_says_how_to_pick_a_remote_image() {
89        let v = json!([
90            {"aliases": [{"name": "dev-base"}], "properties": {"description": "Ubuntu for dev"}},
91            {"aliases": [], "properties": {}},
92            {"aliases": [{"name": "alpine"}, {"name": "a2"}]}
93        ]);
94        let have = aliases(&v);
95        let names: Vec<&str> = have.iter().map(|i| i.alias.as_str()).collect();
96        assert_eq!(names, ["a2", "alpine", "dev-base"]);
97        assert_eq!(have[2].description, "Ubuntu for dev");
98
99        let m = missing("dev-base", &have[..2]).to_string();
100        assert!(
101            m.starts_with("image \"dev-base\" not found locally (local images: a2, alpine)"),
102            "{m}"
103        );
104        assert!(m.contains("e.g. images:ubuntu/24.04"), "{m}");
105        let m = missing("dev-base", &[]).to_string();
106        assert!(m.contains("(this host has no local images)"), "{m}");
107    }
108}