1use std::{collections::HashSet, path::PathBuf};
4
5use itertools::Itertools;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use spin_serde::{DependencyName, FixedVersionBackwardCompatible};
9use std::collections::BTreeMap;
10
11use crate::{
12 metadata::MetadataExt,
13 values::{ValuesMap, ValuesMapBuilder},
14};
15
16pub type LockedMap<T> = std::collections::BTreeMap<String, T>;
18
19pub const SERVICE_CHAINING_KEY: &str = "local_service_chaining";
22
23pub const HOST_REQ_OPTIONAL: &str = "optional";
26pub const HOST_REQ_REQUIRED: &str = "required";
28
29#[derive(Clone, Debug, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum MustUnderstand {
36 HostRequirements,
39 ComponentHostRequirements,
42}
43
44#[derive(Clone, Debug, Deserialize)]
46pub struct LockedApp {
47 pub spin_lock_version: FixedVersionBackwardCompatible<1>,
49 #[serde(default, skip_serializing_if = "Vec::is_empty")]
51 pub must_understand: Vec<MustUnderstand>,
52 #[serde(default, skip_serializing_if = "ValuesMap::is_empty")]
54 pub metadata: ValuesMap,
55 #[serde(
57 default,
58 skip_serializing_if = "ValuesMap::is_empty",
59 deserialize_with = "deserialize_host_requirements"
60 )]
61 pub host_requirements: ValuesMap,
62 #[serde(default, skip_serializing_if = "LockedMap::is_empty")]
64 pub variables: LockedMap<Variable>,
65 pub triggers: Vec<LockedTrigger>,
67 pub components: Vec<LockedComponent>,
69}
70
71fn deserialize_host_requirements<'de, D>(deserializer: D) -> Result<ValuesMap, D::Error>
72where
73 D: serde::Deserializer<'de>,
74{
75 struct HostRequirementsVisitor;
76 impl<'de> serde::de::Visitor<'de> for HostRequirementsVisitor {
77 type Value = ValuesMap;
78
79 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
80 formatter.write_str("struct ValuesMap")
81 }
82
83 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
84 where
85 A: serde::de::MapAccess<'de>,
86 {
87 use serde::de::Error;
88
89 let mut hr = ValuesMapBuilder::new();
90
91 while let Some(key) = map.next_key::<String>()? {
92 let value: serde_json::Value = map.next_value()?;
93 if value.as_str() == Some(HOST_REQ_OPTIONAL) {
94 continue;
95 }
96
97 hr.serializable(key, value).map_err(A::Error::custom)?;
98 }
99
100 Ok(hr.build())
101 }
102 }
103 let m = deserializer.deserialize_map(HostRequirementsVisitor)?;
104 let unsupported: Vec<_> = m
105 .keys()
106 .filter(|k| !SUPPORTED_HOST_REQS.contains(&k.as_str()))
107 .map(|k| k.to_string())
108 .collect();
109 if unsupported.is_empty() {
110 Ok(m)
111 } else {
112 let msg = format!("This version of Spin does not support the following features required by this application: {}", unsupported.join(", "));
113 Err(serde::de::Error::custom(msg))
114 }
115}
116
117const SUPPORTED_HOST_REQS: &[&str] = &[SERVICE_CHAINING_KEY];
118
119impl Serialize for LockedApp {
120 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121 where
122 S: serde::Serializer,
123 {
124 use serde::ser::SerializeStruct;
125
126 let version = if self.must_understand.is_empty() && self.host_requirements.is_empty() {
127 0
128 } else {
129 1
130 };
131
132 let mut la = serializer.serialize_struct("LockedApp", 7)?;
133 la.serialize_field("spin_lock_version", &version)?;
134 if !self.must_understand.is_empty() {
135 la.serialize_field("must_understand", &self.must_understand)?;
136 }
137 if !self.metadata.is_empty() {
138 la.serialize_field("metadata", &self.metadata)?;
139 }
140 if !self.host_requirements.is_empty() {
141 la.serialize_field("host_requirements", &self.host_requirements)?;
142 }
143 if !self.variables.is_empty() {
144 la.serialize_field("variables", &self.variables)?;
145 }
146 la.serialize_field("triggers", &self.triggers)?;
147 la.serialize_field("components", &self.components)?;
148 la.end()
149 }
150}
151
152impl LockedApp {
153 pub fn from_json(contents: &[u8]) -> serde_json::Result<Self> {
155 serde_json::from_slice(contents)
156 }
157
158 pub fn to_json(&self) -> serde_json::Result<Vec<u8>> {
160 serde_json::to_vec_pretty(&self)
161 }
162
163 pub fn get_metadata<'this, T: Deserialize<'this>>(
169 &'this self,
170 key: crate::MetadataKey<T>,
171 ) -> crate::Result<Option<T>> {
172 self.metadata.get_typed(key)
173 }
174
175 pub fn require_metadata<'this, T: Deserialize<'this>>(
180 &'this self,
181 key: crate::MetadataKey<T>,
182 ) -> crate::Result<T> {
183 self.metadata.require_typed(key)
184 }
185
186 pub fn ensure_needs_only(&self, trigger_type: &str, supported: &[&str]) -> Result<(), String> {
190 let app_host_requirements = self.host_requirements.keys();
191
192 let component_ids = self
193 .triggers
194 .iter()
195 .filter(|t| t.trigger_type == trigger_type)
196 .flat_map(|t| t.trigger_config.get("component"))
197 .filter_map(|v| v.as_str())
198 .collect::<HashSet<_>>();
199 let components = self
200 .components
201 .iter()
202 .filter(|c| component_ids.contains(c.id.as_str()));
203 let component_host_requirements = components.flat_map(|c| c.host_requirements.keys());
204
205 let all_host_requirements = app_host_requirements.chain(component_host_requirements);
206
207 let unmet_requirements = all_host_requirements
208 .unique()
209 .filter(|hr| !supported.contains(&hr.as_str()))
210 .map(|s| s.to_string())
211 .collect::<Vec<_>>();
212 if unmet_requirements.is_empty() {
213 Ok(())
214 } else {
215 let message = unmet_requirements.join(", ");
216 Err(message)
217 }
218 }
219}
220
221#[derive(Clone, Debug, Serialize, Deserialize)]
223pub struct LockedComponent {
224 pub id: String,
226 #[serde(default, skip_serializing_if = "ValuesMap::is_empty")]
228 pub metadata: ValuesMap,
229 pub source: LockedComponentSource,
231 #[serde(default, skip_serializing_if = "LockedMap::is_empty")]
233 pub env: LockedMap<String>,
234 #[serde(default, skip_serializing_if = "Vec::is_empty")]
236 pub files: Vec<ContentPath>,
237 #[serde(default, skip_serializing_if = "LockedMap::is_empty")]
239 pub config: LockedMap<String>,
240 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
242 pub dependencies: BTreeMap<DependencyName, LockedComponentDependency>,
243 #[serde(
245 default,
246 skip_serializing_if = "ValuesMap::is_empty",
247 deserialize_with = "deserialize_host_requirements"
248 )]
249 pub host_requirements: ValuesMap,
250}
251
252#[derive(Clone, Debug, Serialize, Deserialize)]
254pub struct LockedComponentDependency {
255 pub source: LockedComponentSource,
257 pub export: Option<String>,
259 #[serde(default, skip_serializing_if = "InheritConfiguration::is_none")]
261 pub inherit: InheritConfiguration,
262}
263
264#[derive(Clone, Debug, Serialize, Deserialize)]
266pub enum InheritConfiguration {
267 All,
269 Some(Vec<String>),
272}
273
274impl Default for InheritConfiguration {
275 fn default() -> Self {
276 InheritConfiguration::Some(vec![])
277 }
278}
279
280impl InheritConfiguration {
281 fn is_none(&self) -> bool {
282 matches!(self, InheritConfiguration::Some(configs) if configs.is_empty())
283 }
284}
285
286#[derive(Clone, Debug, Serialize, Deserialize)]
288pub struct LockedComponentSource {
289 pub content_type: String,
291 #[serde(flatten)]
293 pub content: ContentRef,
294}
295
296#[derive(Clone, Debug, Serialize, Deserialize)]
298pub struct ContentPath {
299 #[serde(flatten)]
301 pub content: ContentRef,
302 pub path: PathBuf,
304}
305
306#[derive(Clone, Debug, Default, Serialize, Deserialize)]
311pub struct ContentRef {
312 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub source: Option<String>,
316 #[serde(
321 default,
322 skip_serializing_if = "Option::is_none",
323 with = "spin_serde::base64"
324 )]
325 pub inline: Option<Vec<u8>>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub digest: Option<String>,
329}
330
331#[derive(Clone, Debug, Serialize, Deserialize)]
333pub struct LockedTrigger {
334 pub id: String,
336 pub trigger_type: String,
338 pub trigger_config: Value,
340}
341
342#[derive(Clone, Debug, Serialize, Deserialize)]
344pub struct Variable {
345 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub description: Option<String>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub default: Option<String>,
351 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
353 pub secret: bool,
354}
355
356#[cfg(test)]
357mod test {
358 use super::*;
359
360 use crate::values::ValuesMapBuilder;
361
362 #[test]
363 fn locked_app_with_no_host_reqs_serialises_as_v0_and_v0_deserialises_as_v1() {
364 let locked_app = LockedApp {
365 spin_lock_version: Default::default(),
366 must_understand: Default::default(),
367 metadata: Default::default(),
368 host_requirements: Default::default(),
369 variables: Default::default(),
370 triggers: Default::default(),
371 components: Default::default(),
372 };
373
374 let json = locked_app.to_json().unwrap();
375
376 assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 0"#));
377
378 let reloaded = LockedApp::from_json(&json).unwrap();
379
380 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
381 }
382
383 #[test]
384 fn locked_app_with_host_reqs_serialises_as_v1() {
385 let mut host_requirements = ValuesMapBuilder::new();
386 host_requirements.string(SERVICE_CHAINING_KEY, "bar");
387 let host_requirements = host_requirements.build();
388
389 let locked_app = LockedApp {
390 spin_lock_version: Default::default(),
391 must_understand: vec![MustUnderstand::HostRequirements],
392 metadata: Default::default(),
393 host_requirements,
394 variables: Default::default(),
395 triggers: Default::default(),
396 components: Default::default(),
397 };
398
399 let json = locked_app.to_json().unwrap();
400
401 assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 1"#));
402
403 let reloaded = LockedApp::from_json(&json).unwrap();
404
405 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
406 assert_eq!(1, reloaded.must_understand.len());
407 assert_eq!(1, reloaded.host_requirements.len());
408 }
409
410 #[test]
411 fn deserialising_ignores_unknown_fields() {
412 use serde_json::json;
413 let j = serde_json::to_vec_pretty(&json!({
414 "spin_lock_version": 1,
415 "triggers": [],
416 "components": [],
417 "never_create_field_with_this_name": 123
418 }))
419 .unwrap();
420 let locked = LockedApp::from_json(&j).unwrap();
421 assert_eq!(0, locked.triggers.len());
422 }
423
424 #[test]
425 fn deserialising_does_not_ignore_must_understand_unknown_fields() {
426 use serde_json::json;
427 let j = serde_json::to_vec_pretty(&json!({
428 "spin_lock_version": 1,
429 "must_understand": vec!["never_create_field_with_this_name"],
430 "triggers": [],
431 "components": [],
432 "never_create_field_with_this_name": 123
433 }))
434 .unwrap();
435 let err = LockedApp::from_json(&j).expect_err(
436 "Should have refused to deserialise due to non-understood must-understand field",
437 );
438 assert!(err
439 .to_string()
440 .contains("never_create_field_with_this_name"));
441 }
442
443 #[test]
444 fn deserialising_accepts_must_understands_that_it_does_understand() {
445 use serde_json::json;
446 let j = serde_json::to_vec_pretty(&json!({
447 "spin_lock_version": 1,
448 "must_understand": vec!["host_requirements"],
449 "host_requirements": {
450 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
451 },
452 "triggers": [],
453 "components": [],
454 "never_create_field_with_this_name": 123
455 }))
456 .unwrap();
457 let locked = LockedApp::from_json(&j).unwrap();
458 assert_eq!(1, locked.must_understand.len());
459 assert_eq!(1, locked.host_requirements.len());
460 }
461
462 #[test]
463 fn deserialising_rejects_host_requirements_that_are_not_supported() {
464 use serde_json::json;
465 let j = serde_json::to_vec_pretty(&json!({
466 "spin_lock_version": 1,
467 "must_understand": vec!["host_requirements"],
468 "host_requirements": {
469 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
470 "accelerated_spline_reticulation": HOST_REQ_REQUIRED
471 },
472 "triggers": [],
473 "components": []
474 }))
475 .unwrap();
476 let err = LockedApp::from_json(&j).expect_err(
477 "Should have refused to deserialise due to non-understood host requirement",
478 );
479 assert!(err.to_string().contains("accelerated_spline_reticulation"));
480 }
481
482 #[test]
483 fn deserialising_skips_optional_host_requirements() {
484 use serde_json::json;
485 let j = serde_json::to_vec_pretty(&json!({
486 "spin_lock_version": 1,
487 "must_understand": vec!["host_requirements"],
488 "host_requirements": {
489 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
490 "accelerated_spline_reticulation": HOST_REQ_OPTIONAL
491 },
492 "triggers": [],
493 "components": []
494 }))
495 .unwrap();
496 let locked = LockedApp::from_json(&j).unwrap();
497 assert_eq!(1, locked.must_understand.len());
498 assert_eq!(1, locked.host_requirements.len());
499 }
500}