use serde_json::{Value, json};
use super::OrgId;
use super::limits::{DEFAULT_ROOT_SIZE, bytes};
use crate::client::{Client, encode_segment};
use crate::error::{Error, Result};
pub fn disk_limited(c: &Client) -> bool {
let path = format!("/1.0/projects/{}", encode_segment(c.project_name()));
c.clone()
.project("default")
.get_opt(&path)
.ok()
.flatten()
.is_some_and(|p| p["config"]["limits.disk"].is_string())
}
pub fn sized_root(c: &Client, mut body: Value) -> Value {
let root = &body["devices"]["root"];
if root.is_object() && root.get("size").is_none() && disk_limited(c) {
body["devices"]["root"]["size"] = json!(DEFAULT_ROOT_SIZE);
}
body
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Unsized {
pub name: String,
pub usage: Option<i64>,
}
pub(crate) fn unsized_roots(oc: &Client) -> Result<Vec<Unsized>> {
let list = oc.get("/1.0/instances?recursion=1")?;
let names: Vec<String> = list
.as_array()
.into_iter()
.flatten()
.filter(|i| i["expanded_devices"]["root"].get("size").is_none())
.filter_map(|i| i["name"].as_str().map(String::from))
.collect();
Ok(names
.into_iter()
.enumerate()
.map(|(n, name)| {
let usage = (n < 20)
.then(|| {
oc.get_opt(&format!("/1.0/instances/{}/state", encode_segment(&name)))
.ok()
.flatten()
})
.flatten()
.and_then(|s| s["disk"]["root"]["usage"].as_i64());
Unsized { name, usage }
})
.collect())
}
pub(crate) fn refuse_unsized(org: &OrgId, list: &[Unsized]) -> Error {
let each: Vec<String> = list
.iter()
.map(|u| match u.usage {
Some(b) => format!("{} (holds {})", u.name, bytes(b)),
None => u.name.clone(),
})
.collect();
Error::invalid(format!(
"org {org}: a disk limit needs every instance's root disk to have a size, and {} {} none: {}. \
Give each one's service `raw_devices: {{root: {{size: ...}}}}` (at least what it holds) \
and redeploy it, or delete the instance; then set the limit again",
each.len(),
if each.len() == 1 { "has" } else { "have" },
each.join(", ")
))
}
pub(crate) fn on_profile_root(oc: &Client) -> Result<Vec<String>> {
let list = oc.get("/1.0/instances?recursion=1")?;
Ok(list
.as_array()
.into_iter()
.flatten()
.filter(|i| {
i["devices"].get("root").is_none()
&& i["profiles"]
.as_array()
.is_some_and(|p| p.iter().any(|p| p == "default"))
})
.filter_map(|i| i["name"].as_str().map(String::from))
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::fake::{Route, serve};
fn project(limited: bool) -> Route {
let config = if limited {
json!({"limits.disk": "100GiB"})
} else {
json!({})
};
Route {
prefix: "GET /1.0/projects/isb-lab",
status: 200,
body: json!({"name": "isb-lab", "config": config}),
}
}
fn body(root: Value) -> Value {
json!({"name": "web-1", "config": {"user.isb.rev": "d59025b7"}, "devices": {"root": root}})
}
#[test]
fn a_new_root_disk_is_sized_only_under_a_disk_limit() {
let root = json!({"type": "disk", "path": "/", "pool": "default"});
let (_d, c) = serve(vec![project(false)]);
let c = c.project("isb-lab");
let b = sized_root(&c, body(root.clone()));
assert!(b["devices"]["root"].get("size").is_none(), "{b}");
let (_d, c) = serve(vec![project(true)]);
let c = c.project("isb-lab");
let b = sized_root(&c, body(root.clone()));
assert_eq!(b["devices"]["root"]["size"], "10GiB");
assert_eq!(b["config"], body(root.clone())["config"]);
assert_eq!(b["devices"]["root"]["pool"], "default");
let mut own = root.clone();
own["size"] = json!("30GiB");
let b = sized_root(&c, body(own));
assert_eq!(b["devices"]["root"]["size"], "30GiB");
let b = sized_root(&c, json!({"name": "x", "devices": {}}));
assert!(b["devices"].get("root").is_none(), "{b}");
}
#[test]
fn an_unreadable_project_has_no_disk_limit() {
let (_d, c) = serve(vec![]);
assert!(!disk_limited(&c.project("isb-lab")));
}
#[test]
fn instances_without_a_root_size_are_named_with_what_they_hold() {
let (_d, c) = serve(vec![
Route {
prefix: "GET /1.0/instances?recursion=1",
status: 200,
body: json!([
{"name": "sized", "profiles": ["default"], "devices": {"root": {"size": "10GiB"}},
"expanded_devices": {"root": {"size": "10GiB"}}},
{"name": "by-profile", "profiles": ["default"], "devices": {},
"expanded_devices": {"root": {"size": "10GiB"}}},
{"name": "web-1", "profiles": ["default"], "devices": {"root": {"path": "/"}},
"expanded_devices": {"root": {"path": "/"}}},
{"name": "db-1", "profiles": ["default"], "devices": {},
"expanded_devices": {"root": {"path": "/"}}},
]),
},
Route {
prefix: "GET /1.0/instances/web-1/state",
status: 200,
body: json!({"disk": {"root": {"usage": 3_221_225_472_i64}}}),
},
]);
let c = c.project("isb-lab");
let u = unsized_roots(&c).unwrap();
assert_eq!(
u,
vec![
Unsized {
name: "web-1".into(),
usage: Some(3_221_225_472)
},
Unsized {
name: "db-1".into(),
usage: None
},
]
);
let e = refuse_unsized(&OrgId::new("lab").unwrap(), &u).to_string();
assert!(e.contains("2 have none: web-1 (holds 3GiB), db-1."), "{e}");
assert!(e.contains("raw_devices: {root: {size: ...}}"), "{e}");
assert_eq!(on_profile_root(&c).unwrap(), vec!["by-profile", "db-1"]);
}
}