1use std::path::PathBuf;
9
10use serde::{Deserialize, Deserializer};
11
12use crate::cloud::{
13 CloudCreateSandboxRequest, CloudDiskImageFormat, CloudHostPattern, CloudPatch, CloudPullPolicy,
14 CloudSandboxResources, CloudSandboxSpec, CloudSecretEntry, CloudSecretSource,
15 CloudSecretsConfig, CloudSnapshotLocation, CloudViolationAction,
16};
17use crate::compat::field::Field;
18use crate::compat::v0_5_0::local::secrets::SecretInjection;
19use crate::compat::v0_6_7::cloud::secrets;
20use crate::{SecretSubstitution, compat};
21
22#[derive(Deserialize)]
27struct SecretsConfig {
28 #[serde(default)]
29 entries: Vec<CloudSecretEntry>,
30 #[serde(default)]
31 passthrough_hosts: Field<Option<Vec<CloudHostPattern>>>,
32 #[serde(default)]
33 violation_action: Field<CloudViolationAction>,
34 #[serde(default)]
35 on_violation: Field<secrets::ViolationAction>,
36}
37
38#[derive(Deserialize)]
39struct SecretEntry {
40 env_var: String,
41 #[serde(default)]
42 value: String,
43 source: Option<CloudSecretSource>,
44 placeholder: String,
45 #[serde(default)]
46 allowed_hosts: Vec<CloudHostPattern>,
47 #[serde(default)]
48 substitution: Field<SecretSubstitution>,
49 #[serde(default)]
50 injection: Field<SecretInjection>,
51 #[serde(default)]
52 passthrough_hosts: Field<Vec<CloudHostPattern>>,
53 #[serde(default)]
54 violation_action: Field<Option<CloudViolationAction>>,
55 #[serde(default)]
56 on_violation: Field<Option<secrets::ViolationAction>>,
57 #[serde(default = "default_true")]
58 require_tls_identity: bool,
59}
60
61#[derive(Deserialize)]
62#[serde(untagged)]
63enum CreateRequest {
64 Tagged(TaggedRequest),
65 PreviousImage(PreviousImageRequest),
66 PreviousSnapshot(PreviousSnapshotRequest),
67}
68
69#[derive(Deserialize)]
70#[serde(tag = "source", rename_all = "snake_case")]
71enum TaggedRequest {
72 Oci {
73 #[serde(flatten)]
74 sandbox: CloudSandboxSpec,
75 reference: String,
76 #[serde(default)]
77 resources: CloudSandboxResources,
78 #[serde(default)]
79 patches: Vec<CloudPatch>,
80 #[serde(default)]
81 pull_policy: CloudPullPolicy,
82 },
83 Bind {
84 #[serde(flatten)]
85 sandbox: CloudSandboxSpec,
86 path: PathBuf,
87 #[serde(default)]
88 resources: CloudSandboxResources,
89 #[serde(default)]
90 patches: Vec<CloudPatch>,
91 },
92 DiskImage {
93 #[serde(flatten)]
94 sandbox: CloudSandboxSpec,
95 path: PathBuf,
96 format: CloudDiskImageFormat,
97 fstype: Option<String>,
98 #[serde(default)]
99 resources: CloudSandboxResources,
100 #[serde(default)]
101 patches: Vec<CloudPatch>,
102 },
103 DiskSnapshot {
104 #[serde(flatten)]
105 sandbox: CloudSandboxSpec,
106 disk_snapshot_ref: CloudSnapshotLocation,
107 #[serde(default)]
108 resources: CloudSandboxResources,
109 #[serde(default)]
110 pull_policy: CloudPullPolicy,
111 },
112}
113
114#[derive(Deserialize)]
115struct PreviousImageRequest {
116 #[serde(default)]
117 source: Field<serde::de::IgnoredAny>,
118 #[serde(default)]
119 disk_snapshot_ref: Field<serde::de::IgnoredAny>,
120 #[serde(flatten)]
121 request: compat::v0_6_5::cloud::create::CreateRequest,
122}
123
124#[derive(Deserialize)]
125struct PreviousSnapshotRequest {
126 #[serde(default)]
127 source: Field<serde::de::IgnoredAny>,
128 #[serde(default)]
129 image: Field<serde::de::IgnoredAny>,
130 disk_snapshot_ref: CloudSnapshotLocation,
131 #[serde(default)]
132 resources: CloudSandboxResources,
133 #[serde(default)]
134 pull_policy: CloudPullPolicy,
135 #[serde(flatten)]
136 sandbox: CloudSandboxSpec,
137}
138
139impl CreateRequest {
144 fn into_current(self) -> Result<CloudCreateSandboxRequest, String> {
145 let tagged = match self {
146 Self::Tagged(request) => request,
147 Self::PreviousImage(request) => {
148 reject_tagged_fallback(request.source)?;
149 if matches!(request.disk_snapshot_ref, Field::Present(_)) {
150 return Err("image and disk_snapshot_ref are mutually exclusive".into());
151 }
152 return request.request.try_into();
153 }
154 Self::PreviousSnapshot(request) => {
155 reject_tagged_fallback(request.source)?;
156 if matches!(request.image, Field::Present(_)) {
157 return Err("image and disk_snapshot_ref are mutually exclusive".into());
158 }
159 TaggedRequest::DiskSnapshot {
160 sandbox: request.sandbox,
161 disk_snapshot_ref: request.disk_snapshot_ref,
162 resources: request.resources,
163 pull_policy: request.pull_policy,
164 }
165 }
166 };
167 Ok(match tagged {
168 TaggedRequest::Oci {
169 sandbox,
170 reference,
171 resources,
172 patches,
173 pull_policy,
174 } => CloudCreateSandboxRequest::Oci {
175 sandbox,
176 reference,
177 resources,
178 patches,
179 pull_policy,
180 },
181 TaggedRequest::Bind {
182 sandbox,
183 path,
184 resources,
185 patches,
186 } => {
187 reject_disk_size(&resources)?;
188 CloudCreateSandboxRequest::Bind {
189 sandbox,
190 path,
191 resources: resources.into(),
192 patches,
193 }
194 }
195 TaggedRequest::DiskImage {
196 sandbox,
197 path,
198 format,
199 fstype,
200 resources,
201 patches,
202 } => {
203 reject_disk_size(&resources)?;
204 CloudCreateSandboxRequest::DiskImage {
205 sandbox,
206 path,
207 format,
208 fstype,
209 resources: resources.into(),
210 patches,
211 }
212 }
213 TaggedRequest::DiskSnapshot {
214 sandbox,
215 disk_snapshot_ref,
216 resources,
217 pull_policy,
218 } => {
219 reject_disk_size(&resources)?;
220 CloudCreateSandboxRequest::DiskSnapshot {
221 sandbox,
222 disk_snapshot_ref,
223 resources: resources.into(),
224 pull_policy,
225 }
226 }
227 })
228 }
229}
230
231impl<'de> Deserialize<'de> for CloudCreateSandboxRequest {
236 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
237 where
238 D: serde::Deserializer<'de>,
239 {
240 CreateRequest::deserialize(deserializer)?
241 .into_current()
242 .map_err(serde::de::Error::custom)
243 }
244}
245
246pub(crate) fn deserialize_secrets_config<'de, D: Deserializer<'de>>(
251 deserializer: D,
252) -> Result<CloudSecretsConfig, D::Error> {
253 let config = SecretsConfig::deserialize(deserializer).map_err(secret_error)?;
254 let (violation_action, on_violation_hosts) =
255 match (config.violation_action, config.on_violation) {
256 (Field::Present(_), Field::Present(_)) => {
257 return Err(secret_error("conflicting policies"));
258 }
259 (Field::Present(action), Field::Missing) => (Some(action), None),
260 (Field::Missing, Field::Present(action)) => action.into_current(),
261 (Field::Missing, Field::Missing) => (None, None),
262 };
263 let passthrough_hosts = match (config.passthrough_hosts, on_violation_hosts) {
264 (Field::Present(_), Some(_)) => return Err(secret_error("conflicting passthrough")),
265 (Field::Present(hosts), None) => hosts,
266 (Field::Missing, hosts) => hosts,
267 };
268 Ok(CloudSecretsConfig {
269 entries: config.entries,
270 passthrough_hosts,
271 violation_action: violation_action.unwrap_or_default(),
272 })
273}
274
275pub(crate) fn deserialize_secret_entry<'de, D: Deserializer<'de>>(
276 deserializer: D,
277) -> Result<CloudSecretEntry, D::Error> {
278 let entry = SecretEntry::deserialize(deserializer).map_err(secret_error)?;
279 let substitution = match (entry.substitution, entry.injection) {
280 (Field::Present(_), Field::Present(_)) => return Err(secret_error("conflicting scopes")),
281 (Field::Present(scopes), Field::Missing) => scopes,
282 (Field::Missing, Field::Present(scopes)) => scopes.into(),
283 (Field::Missing, Field::Missing) => SecretSubstitution::default(),
284 };
285 let (violation_action, on_violation_hosts) = match (entry.violation_action, entry.on_violation)
286 {
287 (Field::Present(_), Field::Present(_)) => return Err(secret_error("conflicting policies")),
288 (Field::Present(action), Field::Missing) => (action, None),
289 (Field::Missing, Field::Present(Some(action))) => action.into_current(),
290 (Field::Missing, Field::Present(None) | Field::Missing) => (None, None),
291 };
292 let passthrough_hosts = match (entry.passthrough_hosts, on_violation_hosts) {
293 (Field::Present(_), Some(_)) => return Err(secret_error("conflicting passthrough")),
294 (Field::Present(hosts), None) => hosts,
295 (Field::Missing, hosts) => hosts.unwrap_or_default(),
296 };
297 Ok(CloudSecretEntry {
298 env_var: entry.env_var,
299 value: entry.value,
300 source: entry.source,
301 placeholder: entry.placeholder,
302 allowed_hosts: entry.allowed_hosts,
303 substitution,
304 passthrough_hosts,
305 violation_action,
306 require_tls_identity: entry.require_tls_identity,
307 })
308}
309
310fn default_true() -> bool {
311 true
312}
313
314fn secret_error<E: serde::de::Error>(_: impl std::fmt::Display) -> E {
316 E::custom("invalid cloud secret configuration")
317}
318
319fn reject_disk_size(resources: &CloudSandboxResources) -> Result<(), String> {
320 if resources.disk_size_mib.is_some() {
321 return Err("resources.disk_size_mib is only valid for OCI source".into());
322 }
323 Ok(())
324}
325
326fn reject_tagged_fallback(source: Field<serde::de::IgnoredAny>) -> Result<(), String> {
327 if matches!(source, Field::Present(_)) {
328 return Err("invalid tagged cloud create request".into());
329 }
330 Ok(())
331}