1use std::collections::BTreeMap;
4
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use crate::flex;
9
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
12#[serde(rename_all = "snake_case")]
13pub enum MountType {
14 #[default]
16 Bind,
17 Volume,
19}
20
21#[derive(Debug, Clone, Default, PartialEq, Serialize)]
24pub struct VolumeSpec {
25 #[serde(rename = "type")]
27 pub mount_type: MountType,
28 pub source: String,
30 pub target: String,
32 #[serde(skip_serializing_if = "std::ops::Not::not")]
33 pub read_only: bool,
34 #[serde(skip_serializing_if = "std::ops::Not::not")]
35 pub external: bool,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub pool: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub owner: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub mode: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub device: Option<String>,
44 #[serde(skip_serializing_if = "VolumeOptions::is_default")]
45 pub volume: VolumeOptions,
46 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
47 pub options: BTreeMap<String, String>,
48}
49
50#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
52#[serde(deny_unknown_fields)]
53pub struct VolumeOptions {
54 #[serde(
58 default,
59 deserialize_with = "flex::bool",
60 skip_serializing_if = "std::ops::Not::not"
61 )]
62 #[schemars(with = "flex::BoolOrString")]
63 pub nocopy: bool,
64}
65
66impl VolumeOptions {
67 fn is_default(&self) -> bool {
68 *self == Self::default()
69 }
70}
71
72#[derive(Deserialize, JsonSchema)]
74#[serde(deny_unknown_fields)]
75#[allow(dead_code)]
76pub(crate) struct VolumeMount {
77 #[serde(default, rename = "type")]
80 mount_type: Option<MountType>,
81
82 source: String,
86
87 target: String,
89
90 #[serde(default, deserialize_with = "flex::bool")]
92 #[schemars(with = "flex::BoolOrString")]
93 read_only: bool,
94
95 #[serde(default, deserialize_with = "flex::bool")]
97 #[schemars(with = "flex::BoolOrString")]
98 external: bool,
99
100 #[serde(default)]
103 pool: Option<String>,
104
105 #[serde(default, deserialize_with = "flex::opt_string")]
110 #[schemars(with = "Option<flex::IntOrString>")]
111 owner: Option<String>,
112
113 #[serde(default, deserialize_with = "flex::opt_string")]
116 #[schemars(with = "Option<flex::IntOrString>")]
117 mode: Option<String>,
118
119 #[serde(default)]
122 device: Option<String>,
123
124 #[serde(default)]
126 volume: VolumeOptions,
127
128 #[serde(default, deserialize_with = "flex::string_map")]
130 #[schemars(with = "BTreeMap<String, flex::Scalar>")]
131 options: BTreeMap<String, String>,
132}
133
134pub(crate) fn is_host_path(source: &str) -> bool {
136 source.starts_with('/') || source.starts_with('.') || source.starts_with('~')
137}
138
139impl From<VolumeMount> for VolumeSpec {
140 fn from(m: VolumeMount) -> Self {
141 let mount_type = m.mount_type.unwrap_or(if is_host_path(&m.source) {
142 MountType::Bind
143 } else {
144 MountType::Volume
145 });
146 VolumeSpec {
147 mount_type,
148 source: m.source,
149 target: m.target,
150 read_only: m.read_only,
151 external: m.external,
152 pool: m.pool,
153 owner: m.owner,
154 mode: m.mode,
155 device: m.device,
156 volume: m.volume,
157 options: m.options,
158 }
159 }
160}
161
162impl<'de> Deserialize<'de> for VolumeSpec {
163 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
164 use serde::de::Error as _;
165 match serde_json::Value::deserialize(d)? {
166 serde_json::Value::String(s) => {
167 crate::shorthand::volume(&s).map_err(|e| D::Error::custom(e.to_string()))
168 }
169 v @ serde_json::Value::Object(_) => serde_json::from_value::<VolumeMount>(v)
170 .map(Into::into)
171 .map_err(|e| D::Error::custom(format!("volume: {e}"))),
172 other => Err(D::Error::custom(format!(
173 "volume: expected SOURCE:TARGET[:OPTIONS] or {{type, source, target, ...}}, got {other}"
174 ))),
175 }
176 }
177}
178
179impl JsonSchema for VolumeSpec {
180 fn schema_name() -> std::borrow::Cow<'static, str> {
181 "VolumeSpec".into()
182 }
183
184 fn json_schema(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
185 let long = g.subschema_for::<VolumeMount>();
186 schemars::json_schema!({
187 "description": "A mount: `SOURCE:TARGET[:OPTIONS]` or the long form.",
188 "oneOf": [
189 {
190 "type": "string",
191 "description": "SOURCE:TARGET[:OPTIONS]. OPTIONS is a comma list of ro, rw, owner=USER, mode=MODE, device=NAME, pool=POOL, external."
192 },
193 long
194 ]
195 })
196 }
197}