use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::flex;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MountType {
#[default]
Bind,
Volume,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct VolumeSpec {
#[serde(rename = "type")]
pub mount_type: MountType,
pub source: String,
pub target: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub read_only: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub external: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub pool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device: Option<String>,
#[serde(skip_serializing_if = "VolumeOptions::is_default")]
pub volume: VolumeOptions,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub options: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VolumeOptions {
#[serde(
default,
deserialize_with = "flex::bool",
skip_serializing_if = "std::ops::Not::not"
)]
#[schemars(with = "flex::BoolOrString")]
pub nocopy: bool,
}
impl VolumeOptions {
fn is_default(&self) -> bool {
*self == Self::default()
}
}
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[allow(dead_code)]
pub(crate) struct VolumeMount {
#[serde(default, rename = "type")]
mount_type: Option<MountType>,
source: String,
target: String,
#[serde(default, deserialize_with = "flex::bool")]
#[schemars(with = "flex::BoolOrString")]
read_only: bool,
#[serde(default, deserialize_with = "flex::bool")]
#[schemars(with = "flex::BoolOrString")]
external: bool,
#[serde(default)]
pool: Option<String>,
#[serde(default, deserialize_with = "flex::opt_string")]
#[schemars(with = "Option<flex::IntOrString>")]
owner: Option<String>,
#[serde(default, deserialize_with = "flex::opt_string")]
#[schemars(with = "Option<flex::IntOrString>")]
mode: Option<String>,
#[serde(default)]
device: Option<String>,
#[serde(default)]
volume: VolumeOptions,
#[serde(default, deserialize_with = "flex::string_map")]
#[schemars(with = "BTreeMap<String, flex::Scalar>")]
options: BTreeMap<String, String>,
}
pub(crate) fn is_host_path(source: &str) -> bool {
source.starts_with('/') || source.starts_with('.') || source.starts_with('~')
}
impl From<VolumeMount> for VolumeSpec {
fn from(m: VolumeMount) -> Self {
let mount_type = m.mount_type.unwrap_or(if is_host_path(&m.source) {
MountType::Bind
} else {
MountType::Volume
});
VolumeSpec {
mount_type,
source: m.source,
target: m.target,
read_only: m.read_only,
external: m.external,
pool: m.pool,
owner: m.owner,
mode: m.mode,
device: m.device,
volume: m.volume,
options: m.options,
}
}
}
impl<'de> Deserialize<'de> for VolumeSpec {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
use serde::de::Error as _;
match serde_json::Value::deserialize(d)? {
serde_json::Value::String(s) => {
crate::shorthand::volume(&s).map_err(|e| D::Error::custom(e.to_string()))
}
v @ serde_json::Value::Object(_) => serde_json::from_value::<VolumeMount>(v)
.map(Into::into)
.map_err(|e| D::Error::custom(format!("volume: {e}"))),
other => Err(D::Error::custom(format!(
"volume: expected SOURCE:TARGET[:OPTIONS] or {{type, source, target, ...}}, got {other}"
))),
}
}
}
impl JsonSchema for VolumeSpec {
fn schema_name() -> std::borrow::Cow<'static, str> {
"VolumeSpec".into()
}
fn json_schema(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
let long = g.subschema_for::<VolumeMount>();
schemars::json_schema!({
"description": "A mount: `SOURCE:TARGET[:OPTIONS]` or the long form.",
"oneOf": [
{
"type": "string",
"description": "SOURCE:TARGET[:OPTIONS]. OPTIONS is a comma list of ro, rw, owner=USER, mode=MODE, device=NAME, pool=POOL, external."
},
long
]
})
}
}