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!(
113 "This version of Spin does not support the following features required by this application: {}",
114 unsupported.join(", ")
115 );
116 Err(serde::de::Error::custom(msg))
117 }
118}
119
120const SUPPORTED_HOST_REQS: &[&str] = &[SERVICE_CHAINING_KEY];
121
122impl Serialize for LockedApp {
123 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
124 where
125 S: serde::Serializer,
126 {
127 use serde::ser::SerializeStruct;
128
129 let version = if self.must_understand.is_empty() && self.host_requirements.is_empty() {
130 0
131 } else {
132 1
133 };
134
135 let mut la = serializer.serialize_struct("LockedApp", 7)?;
136 la.serialize_field("spin_lock_version", &version)?;
137 if !self.must_understand.is_empty() {
138 la.serialize_field("must_understand", &self.must_understand)?;
139 }
140 if !self.metadata.is_empty() {
141 la.serialize_field("metadata", &self.metadata)?;
142 }
143 if !self.host_requirements.is_empty() {
144 la.serialize_field("host_requirements", &self.host_requirements)?;
145 }
146 if !self.variables.is_empty() {
147 la.serialize_field("variables", &self.variables)?;
148 }
149 la.serialize_field("triggers", &self.triggers)?;
150 la.serialize_field("components", &self.components)?;
151 la.end()
152 }
153}
154
155impl LockedApp {
156 pub fn from_json(contents: &[u8]) -> serde_json::Result<Self> {
158 serde_json::from_slice(contents)
159 }
160
161 pub fn to_json(&self) -> serde_json::Result<Vec<u8>> {
163 serde_json::to_vec_pretty(&self)
164 }
165
166 pub fn get_metadata<'this, T: Deserialize<'this>>(
172 &'this self,
173 key: crate::MetadataKey<T>,
174 ) -> crate::Result<Option<T>> {
175 self.metadata.get_typed(key)
176 }
177
178 pub fn require_metadata<'this, T: Deserialize<'this>>(
183 &'this self,
184 key: crate::MetadataKey<T>,
185 ) -> crate::Result<T> {
186 self.metadata.require_typed(key)
187 }
188
189 pub fn ensure_needs_only(&self, trigger_type: &str, supported: &[&str]) -> Result<(), String> {
193 let app_host_requirements = self.host_requirements.keys();
194
195 let component_ids = self
196 .triggers
197 .iter()
198 .filter(|t| t.trigger_type == trigger_type)
199 .flat_map(|t| t.trigger_config.get("component"))
200 .filter_map(|v| v.as_str())
201 .collect::<HashSet<_>>();
202 let components = self
203 .components
204 .iter()
205 .filter(|c| component_ids.contains(c.id.as_str()));
206 let component_host_requirements = components.flat_map(|c| c.host_requirements.keys());
207
208 let all_host_requirements = app_host_requirements.chain(component_host_requirements);
209
210 let unmet_requirements = all_host_requirements
211 .unique()
212 .filter(|hr| !supported.contains(&hr.as_str()))
213 .map(|s| s.to_string())
214 .collect::<Vec<_>>();
215 if unmet_requirements.is_empty() {
216 Ok(())
217 } else {
218 let message = unmet_requirements.join(", ");
219 Err(message)
220 }
221 }
222}
223
224#[derive(Clone, Debug, Serialize, Deserialize)]
226pub struct LockedComponent {
227 pub id: String,
229 #[serde(default, skip_serializing_if = "ValuesMap::is_empty")]
231 pub metadata: ValuesMap,
232 pub source: LockedComponentSource,
234 #[serde(default, skip_serializing_if = "LockedMap::is_empty")]
236 pub env: LockedMap<String>,
237 #[serde(default, skip_serializing_if = "Vec::is_empty")]
239 pub files: Vec<ContentPath>,
240 #[serde(default, skip_serializing_if = "LockedMap::is_empty")]
242 pub config: LockedMap<String>,
243 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
245 pub dependencies: BTreeMap<DependencyName, LockedComponentDependency>,
246 #[serde(
248 default,
249 skip_serializing_if = "ValuesMap::is_empty",
250 deserialize_with = "deserialize_host_requirements"
251 )]
252 pub host_requirements: ValuesMap,
253}
254
255#[derive(Clone, Debug, Serialize, Deserialize)]
257pub struct LockedComponentDependency {
258 pub source: LockedComponentSource,
260 pub export: Option<String>,
262 #[serde(default, skip_serializing_if = "InheritConfiguration::is_none")]
264 pub inherit: InheritConfiguration,
265}
266
267#[derive(Clone, Debug, Serialize, Deserialize)]
269pub enum InheritConfiguration {
270 All,
272 Some(Vec<String>),
275}
276
277impl Default for InheritConfiguration {
278 fn default() -> Self {
279 InheritConfiguration::Some(vec![])
280 }
281}
282
283impl InheritConfiguration {
284 fn is_none(&self) -> bool {
285 matches!(self, InheritConfiguration::Some(configs) if configs.is_empty())
286 }
287}
288
289#[derive(Clone, Debug, Serialize, Deserialize)]
291pub struct LockedComponentSource {
292 pub content_type: String,
294 #[serde(flatten)]
296 pub content: ContentRef,
297}
298
299#[derive(Clone, Debug, Serialize, Deserialize)]
301pub struct ContentPath {
302 #[serde(flatten)]
304 pub content: ContentRef,
305 pub path: PathBuf,
307}
308
309#[derive(Clone, Debug, Default, Serialize, Deserialize)]
314pub struct ContentRef {
315 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub source: Option<String>,
319 #[serde(
324 default,
325 skip_serializing_if = "Option::is_none",
326 with = "spin_serde::base64"
327 )]
328 pub inline: Option<Vec<u8>>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub digest: Option<String>,
332}
333
334#[derive(Clone, Debug, Serialize, Deserialize)]
336pub struct LockedTrigger {
337 pub id: String,
339 pub trigger_type: String,
341 pub trigger_config: Value,
343}
344
345#[derive(Clone, Debug, Serialize, Deserialize)]
347pub struct Variable {
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub description: Option<String>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub default: Option<String>,
354 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
356 pub secret: bool,
357}
358
359#[cfg(test)]
360mod test {
361 use super::*;
362
363 use crate::values::ValuesMapBuilder;
364
365 #[test]
366 fn locked_app_with_no_host_reqs_serialises_as_v0_and_v0_deserialises_as_v1() {
367 let locked_app = LockedApp {
368 spin_lock_version: Default::default(),
369 must_understand: Default::default(),
370 metadata: Default::default(),
371 host_requirements: Default::default(),
372 variables: Default::default(),
373 triggers: Default::default(),
374 components: Default::default(),
375 };
376
377 let json = locked_app.to_json().unwrap();
378
379 assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 0"#));
380
381 let reloaded = LockedApp::from_json(&json).unwrap();
382
383 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
384 }
385
386 #[test]
387 fn locked_app_with_host_reqs_serialises_as_v1() {
388 let mut host_requirements = ValuesMapBuilder::new();
389 host_requirements.string(SERVICE_CHAINING_KEY, "bar");
390 let host_requirements = host_requirements.build();
391
392 let locked_app = LockedApp {
393 spin_lock_version: Default::default(),
394 must_understand: vec![MustUnderstand::HostRequirements],
395 metadata: Default::default(),
396 host_requirements,
397 variables: Default::default(),
398 triggers: Default::default(),
399 components: Default::default(),
400 };
401
402 let json = locked_app.to_json().unwrap();
403
404 assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 1"#));
405
406 let reloaded = LockedApp::from_json(&json).unwrap();
407
408 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
409 assert_eq!(1, reloaded.must_understand.len());
410 assert_eq!(1, reloaded.host_requirements.len());
411 }
412
413 #[test]
414 fn deserialising_ignores_unknown_fields() {
415 use serde_json::json;
416 let j = serde_json::to_vec_pretty(&json!({
417 "spin_lock_version": 1,
418 "triggers": [],
419 "components": [],
420 "never_create_field_with_this_name": 123
421 }))
422 .unwrap();
423 let locked = LockedApp::from_json(&j).unwrap();
424 assert_eq!(0, locked.triggers.len());
425 }
426
427 #[test]
428 fn deserialising_does_not_ignore_must_understand_unknown_fields() {
429 use serde_json::json;
430 let j = serde_json::to_vec_pretty(&json!({
431 "spin_lock_version": 1,
432 "must_understand": vec!["never_create_field_with_this_name"],
433 "triggers": [],
434 "components": [],
435 "never_create_field_with_this_name": 123
436 }))
437 .unwrap();
438 let err = LockedApp::from_json(&j).expect_err(
439 "Should have refused to deserialise due to non-understood must-understand field",
440 );
441 assert!(
442 err.to_string()
443 .contains("never_create_field_with_this_name")
444 );
445 }
446
447 #[test]
448 fn deserialising_accepts_must_understands_that_it_does_understand() {
449 use serde_json::json;
450 let j = serde_json::to_vec_pretty(&json!({
451 "spin_lock_version": 1,
452 "must_understand": vec!["host_requirements"],
453 "host_requirements": {
454 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
455 },
456 "triggers": [],
457 "components": [],
458 "never_create_field_with_this_name": 123
459 }))
460 .unwrap();
461 let locked = LockedApp::from_json(&j).unwrap();
462 assert_eq!(1, locked.must_understand.len());
463 assert_eq!(1, locked.host_requirements.len());
464 }
465
466 #[test]
467 fn deserialising_rejects_host_requirements_that_are_not_supported() {
468 use serde_json::json;
469 let j = serde_json::to_vec_pretty(&json!({
470 "spin_lock_version": 1,
471 "must_understand": vec!["host_requirements"],
472 "host_requirements": {
473 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
474 "accelerated_spline_reticulation": HOST_REQ_REQUIRED
475 },
476 "triggers": [],
477 "components": []
478 }))
479 .unwrap();
480 let err = LockedApp::from_json(&j).expect_err(
481 "Should have refused to deserialise due to non-understood host requirement",
482 );
483 assert!(err.to_string().contains("accelerated_spline_reticulation"));
484 }
485
486 #[test]
487 fn deserialising_skips_optional_host_requirements() {
488 use serde_json::json;
489 let j = serde_json::to_vec_pretty(&json!({
490 "spin_lock_version": 1,
491 "must_understand": vec!["host_requirements"],
492 "host_requirements": {
493 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
494 "accelerated_spline_reticulation": HOST_REQ_OPTIONAL
495 },
496 "triggers": [],
497 "components": []
498 }))
499 .unwrap();
500 let locked = LockedApp::from_json(&j).unwrap();
501 assert_eq!(1, locked.must_understand.len());
502 assert_eq!(1, locked.host_requirements.len());
503 }
504}