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(default, skip_serializing_if = "BTreeMap::is_empty")]
248 pub trigger_dependencies: BTreeMap<String, Vec<LockedComponentDependency>>,
249 #[serde(
251 default,
252 skip_serializing_if = "ValuesMap::is_empty",
253 deserialize_with = "deserialize_host_requirements"
254 )]
255 pub host_requirements: ValuesMap,
256}
257
258#[derive(Clone, Debug, Serialize, Deserialize)]
260pub struct LockedComponentDependency {
261 pub source: LockedComponentSource,
263 pub export: Option<String>,
265 #[serde(default, skip_serializing_if = "InheritConfiguration::is_none")]
267 pub inherit: InheritConfiguration,
268}
269
270#[derive(Clone, Debug, Serialize, Deserialize)]
282pub enum InheritConfiguration {
283 All,
285 Some(Vec<String>),
288}
289
290impl Default for InheritConfiguration {
291 fn default() -> Self {
292 InheritConfiguration::Some(vec![])
293 }
294}
295
296impl InheritConfiguration {
297 fn is_none(&self) -> bool {
298 matches!(self, InheritConfiguration::Some(configs) if configs.is_empty())
299 }
300}
301
302#[derive(Clone, Debug, Serialize, Deserialize)]
304pub struct LockedComponentSource {
305 pub content_type: String,
307 #[serde(flatten)]
309 pub content: ContentRef,
310}
311
312#[derive(Clone, Debug, Serialize, Deserialize)]
314pub struct ContentPath {
315 #[serde(flatten)]
317 pub content: ContentRef,
318 pub path: PathBuf,
320}
321
322#[derive(Clone, Debug, Default, Serialize, Deserialize)]
327pub struct ContentRef {
328 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub source: Option<String>,
332 #[serde(
337 default,
338 skip_serializing_if = "Option::is_none",
339 with = "spin_serde::base64"
340 )]
341 pub inline: Option<Vec<u8>>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub digest: Option<String>,
345}
346
347#[derive(Clone, Debug, Serialize, Deserialize)]
349pub struct LockedTrigger {
350 pub id: String,
352 pub trigger_type: String,
354 pub trigger_config: Value,
356 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
358 pub trigger_dependencies: BTreeMap<String, Vec<LockedComponentDependency>>,
359}
360
361#[derive(Clone, Debug, Serialize, Deserialize)]
363pub struct Variable {
364 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub description: Option<String>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub default: Option<String>,
370 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
372 pub secret: bool,
373}
374
375#[cfg(test)]
376mod test {
377 use super::*;
378
379 use crate::values::ValuesMapBuilder;
380
381 #[test]
382 fn locked_app_with_no_host_reqs_serialises_as_v0_and_v0_deserialises_as_v1() {
383 let locked_app = LockedApp {
384 spin_lock_version: Default::default(),
385 must_understand: Default::default(),
386 metadata: Default::default(),
387 host_requirements: Default::default(),
388 variables: Default::default(),
389 triggers: Default::default(),
390 components: Default::default(),
391 };
392
393 let json = locked_app.to_json().unwrap();
394
395 assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 0"#));
396
397 let reloaded = LockedApp::from_json(&json).unwrap();
398
399 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
400 }
401
402 #[test]
403 fn locked_app_with_host_reqs_serialises_as_v1() {
404 let mut host_requirements = ValuesMapBuilder::new();
405 host_requirements.string(SERVICE_CHAINING_KEY, "bar");
406 let host_requirements = host_requirements.build();
407
408 let locked_app = LockedApp {
409 spin_lock_version: Default::default(),
410 must_understand: vec![MustUnderstand::HostRequirements],
411 metadata: Default::default(),
412 host_requirements,
413 variables: Default::default(),
414 triggers: Default::default(),
415 components: Default::default(),
416 };
417
418 let json = locked_app.to_json().unwrap();
419
420 assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 1"#));
421
422 let reloaded = LockedApp::from_json(&json).unwrap();
423
424 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
425 assert_eq!(1, reloaded.must_understand.len());
426 assert_eq!(1, reloaded.host_requirements.len());
427 }
428
429 #[test]
430 fn deserialising_ignores_unknown_fields() {
431 use serde_json::json;
432 let j = serde_json::to_vec_pretty(&json!({
433 "spin_lock_version": 1,
434 "triggers": [],
435 "components": [],
436 "never_create_field_with_this_name": 123
437 }))
438 .unwrap();
439 let locked = LockedApp::from_json(&j).unwrap();
440 assert_eq!(0, locked.triggers.len());
441 }
442
443 #[test]
444 fn deserialising_does_not_ignore_must_understand_unknown_fields() {
445 use serde_json::json;
446 let j = serde_json::to_vec_pretty(&json!({
447 "spin_lock_version": 1,
448 "must_understand": vec!["never_create_field_with_this_name"],
449 "triggers": [],
450 "components": [],
451 "never_create_field_with_this_name": 123
452 }))
453 .unwrap();
454 let err = LockedApp::from_json(&j).expect_err(
455 "Should have refused to deserialise due to non-understood must-understand field",
456 );
457 assert!(
458 err.to_string()
459 .contains("never_create_field_with_this_name")
460 );
461 }
462
463 #[test]
464 fn deserialising_accepts_must_understands_that_it_does_understand() {
465 use serde_json::json;
466 let j = serde_json::to_vec_pretty(&json!({
467 "spin_lock_version": 1,
468 "must_understand": vec!["host_requirements"],
469 "host_requirements": {
470 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
471 },
472 "triggers": [],
473 "components": [],
474 "never_create_field_with_this_name": 123
475 }))
476 .unwrap();
477 let locked = LockedApp::from_json(&j).unwrap();
478 assert_eq!(1, locked.must_understand.len());
479 assert_eq!(1, locked.host_requirements.len());
480 }
481
482 #[test]
483 fn deserialising_rejects_host_requirements_that_are_not_supported() {
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_REQUIRED
491 },
492 "triggers": [],
493 "components": []
494 }))
495 .unwrap();
496 let err = LockedApp::from_json(&j).expect_err(
497 "Should have refused to deserialise due to non-understood host requirement",
498 );
499 assert!(err.to_string().contains("accelerated_spline_reticulation"));
500 }
501
502 #[test]
503 fn deserialising_skips_optional_host_requirements() {
504 use serde_json::json;
505 let j = serde_json::to_vec_pretty(&json!({
506 "spin_lock_version": 1,
507 "must_understand": vec!["host_requirements"],
508 "host_requirements": {
509 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
510 "accelerated_spline_reticulation": HOST_REQ_OPTIONAL
511 },
512 "triggers": [],
513 "components": []
514 }))
515 .unwrap();
516 let locked = LockedApp::from_json(&j).unwrap();
517 assert_eq!(1, locked.must_understand.len());
518 assert_eq!(1, locked.host_requirements.len());
519 }
520}