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 MIDDLEWARE_KEY: &str = "middleware";
26
27pub const HOST_REQ_OPTIONAL: &str = "optional";
30pub const HOST_REQ_REQUIRED: &str = "required";
32
33#[derive(Clone, Debug, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum MustUnderstand {
40 HostRequirements,
43 ComponentHostRequirements,
46}
47
48#[derive(Clone, Debug, Deserialize)]
50pub struct LockedApp {
51 pub spin_lock_version: FixedVersionBackwardCompatible<1>,
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 pub must_understand: Vec<MustUnderstand>,
56 #[serde(default, skip_serializing_if = "ValuesMap::is_empty")]
58 pub metadata: ValuesMap,
59 #[serde(
61 default,
62 skip_serializing_if = "ValuesMap::is_empty",
63 deserialize_with = "deserialize_host_requirements"
64 )]
65 pub host_requirements: ValuesMap,
66 #[serde(default, skip_serializing_if = "LockedMap::is_empty")]
68 pub variables: LockedMap<Variable>,
69 pub triggers: Vec<LockedTrigger>,
71 pub components: Vec<LockedComponent>,
73}
74
75fn deserialize_host_requirements<'de, D>(deserializer: D) -> Result<ValuesMap, D::Error>
76where
77 D: serde::Deserializer<'de>,
78{
79 struct HostRequirementsVisitor;
80 impl<'de> serde::de::Visitor<'de> for HostRequirementsVisitor {
81 type Value = ValuesMap;
82
83 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
84 formatter.write_str("struct ValuesMap")
85 }
86
87 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
88 where
89 A: serde::de::MapAccess<'de>,
90 {
91 use serde::de::Error;
92
93 let mut hr = ValuesMapBuilder::new();
94
95 while let Some(key) = map.next_key::<String>()? {
96 let value: serde_json::Value = map.next_value()?;
97 if value.as_str() == Some(HOST_REQ_OPTIONAL) {
98 continue;
99 }
100
101 hr.serializable(key, value).map_err(A::Error::custom)?;
102 }
103
104 Ok(hr.build())
105 }
106 }
107 let m = deserializer.deserialize_map(HostRequirementsVisitor)?;
108 let unsupported: Vec<_> = m
109 .keys()
110 .filter(|k| !SUPPORTED_HOST_REQS.contains(&k.as_str()))
111 .map(|k| k.to_string())
112 .collect();
113 if unsupported.is_empty() {
114 Ok(m)
115 } else {
116 let msg = format!(
117 "This version of Spin does not support the following features required by this application: {}",
118 unsupported.join(", ")
119 );
120 Err(serde::de::Error::custom(msg))
121 }
122}
123
124const SUPPORTED_HOST_REQS: &[&str] = &[SERVICE_CHAINING_KEY, MIDDLEWARE_KEY];
125
126impl Serialize for LockedApp {
127 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
128 where
129 S: serde::Serializer,
130 {
131 use serde::ser::SerializeStruct;
132
133 let version = if self.must_understand.is_empty() && self.host_requirements.is_empty() {
134 0
135 } else {
136 1
137 };
138
139 let mut la = serializer.serialize_struct("LockedApp", 7)?;
140 la.serialize_field("spin_lock_version", &version)?;
141 if !self.must_understand.is_empty() {
142 la.serialize_field("must_understand", &self.must_understand)?;
143 }
144 if !self.metadata.is_empty() {
145 la.serialize_field("metadata", &self.metadata)?;
146 }
147 if !self.host_requirements.is_empty() {
148 la.serialize_field("host_requirements", &self.host_requirements)?;
149 }
150 if !self.variables.is_empty() {
151 la.serialize_field("variables", &self.variables)?;
152 }
153 la.serialize_field("triggers", &self.triggers)?;
154 la.serialize_field("components", &self.components)?;
155 la.end()
156 }
157}
158
159impl LockedApp {
160 pub fn from_json(contents: &[u8]) -> serde_json::Result<Self> {
162 serde_json::from_slice(contents)
163 }
164
165 pub fn to_json(&self) -> serde_json::Result<Vec<u8>> {
167 serde_json::to_vec_pretty(&self)
168 }
169
170 pub fn get_metadata<'this, T: Deserialize<'this>>(
176 &'this self,
177 key: crate::MetadataKey<T>,
178 ) -> crate::Result<Option<T>> {
179 self.metadata.get_typed(key)
180 }
181
182 pub fn require_metadata<'this, T: Deserialize<'this>>(
187 &'this self,
188 key: crate::MetadataKey<T>,
189 ) -> crate::Result<T> {
190 self.metadata.require_typed(key)
191 }
192
193 pub fn ensure_needs_only(&self, trigger_type: &str, supported: &[&str]) -> Result<(), String> {
197 let app_host_requirements = self.host_requirements.keys();
198
199 let component_ids = self
200 .triggers
201 .iter()
202 .filter(|t| t.trigger_type == trigger_type)
203 .flat_map(|t| t.trigger_config.get("component"))
204 .filter_map(|v| v.as_str())
205 .collect::<HashSet<_>>();
206 let components = self
207 .components
208 .iter()
209 .filter(|c| component_ids.contains(c.id.as_str()));
210 let component_host_requirements = components.flat_map(|c| c.host_requirements.keys());
211
212 let all_host_requirements = app_host_requirements.chain(component_host_requirements);
213
214 let unmet_requirements = all_host_requirements
215 .unique()
216 .filter(|hr| !supported.contains(&hr.as_str()))
217 .map(|s| s.to_string())
218 .collect::<Vec<_>>();
219 if unmet_requirements.is_empty() {
220 Ok(())
221 } else {
222 let message = unmet_requirements.join(", ");
223 Err(message)
224 }
225 }
226}
227
228#[derive(Clone, Debug, Serialize, Deserialize)]
230pub struct LockedComponent {
231 pub id: String,
233 #[serde(default, skip_serializing_if = "ValuesMap::is_empty")]
235 pub metadata: ValuesMap,
236 pub source: LockedComponentSource,
238 #[serde(default, skip_serializing_if = "LockedMap::is_empty")]
240 pub env: LockedMap<String>,
241 #[serde(default, skip_serializing_if = "Vec::is_empty")]
243 pub files: Vec<ContentPath>,
244 #[serde(default, skip_serializing_if = "LockedMap::is_empty")]
246 pub config: LockedMap<String>,
247 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
249 pub dependencies: BTreeMap<DependencyName, LockedComponentDependency>,
250 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
252 pub trigger_dependencies: BTreeMap<String, Vec<LockedComponentDependency>>,
253 #[serde(
255 default,
256 skip_serializing_if = "ValuesMap::is_empty",
257 deserialize_with = "deserialize_host_requirements"
258 )]
259 pub host_requirements: ValuesMap,
260}
261
262#[derive(Clone, Debug, Serialize, Deserialize)]
264pub struct LockedComponentDependency {
265 pub source: LockedComponentSource,
267 pub export: Option<String>,
269 #[serde(default, skip_serializing_if = "InheritConfiguration::is_none")]
271 pub inherit: InheritConfiguration,
272}
273
274#[derive(Clone, Debug, Serialize, Deserialize)]
286pub enum InheritConfiguration {
287 All,
289 Some(Vec<String>),
292}
293
294impl Default for InheritConfiguration {
295 fn default() -> Self {
296 InheritConfiguration::Some(vec![])
297 }
298}
299
300impl InheritConfiguration {
301 fn is_none(&self) -> bool {
302 matches!(self, InheritConfiguration::Some(configs) if configs.is_empty())
303 }
304}
305
306#[derive(Clone, Debug, Serialize, Deserialize)]
308pub struct LockedComponentSource {
309 pub content_type: String,
311 #[serde(flatten)]
313 pub content: ContentRef,
314}
315
316#[derive(Clone, Debug, Serialize, Deserialize)]
318pub struct ContentPath {
319 #[serde(flatten)]
321 pub content: ContentRef,
322 pub path: PathBuf,
324}
325
326#[derive(Clone, Debug, Default, Serialize, Deserialize)]
331pub struct ContentRef {
332 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub source: Option<String>,
336 #[serde(
341 default,
342 skip_serializing_if = "Option::is_none",
343 with = "spin_serde::base64"
344 )]
345 pub inline: Option<Vec<u8>>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub digest: Option<String>,
349}
350
351#[derive(Clone, Debug, Serialize, Deserialize)]
353pub struct LockedTrigger {
354 pub id: String,
356 pub trigger_type: String,
358 pub trigger_config: Value,
360 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
362 pub trigger_dependencies: BTreeMap<String, Vec<LockedComponentDependency>>,
363}
364
365#[derive(Clone, Debug, Serialize, Deserialize)]
367pub struct Variable {
368 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub description: Option<String>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub default: Option<String>,
374 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
376 pub secret: bool,
377}
378
379#[cfg(test)]
380mod test {
381 use super::*;
382
383 use crate::values::ValuesMapBuilder;
384
385 #[test]
386 fn locked_app_with_no_host_reqs_serialises_as_v0_and_v0_deserialises_as_v1() {
387 let locked_app = LockedApp {
388 spin_lock_version: Default::default(),
389 must_understand: Default::default(),
390 metadata: Default::default(),
391 host_requirements: Default::default(),
392 variables: Default::default(),
393 triggers: Default::default(),
394 components: Default::default(),
395 };
396
397 let json = locked_app.to_json().unwrap();
398
399 assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 0"#));
400
401 let reloaded = LockedApp::from_json(&json).unwrap();
402
403 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
404 }
405
406 #[test]
407 fn locked_app_with_host_reqs_serialises_as_v1() {
408 let mut host_requirements = ValuesMapBuilder::new();
409 host_requirements.string(SERVICE_CHAINING_KEY, "bar");
410 let host_requirements = host_requirements.build();
411
412 let locked_app = LockedApp {
413 spin_lock_version: Default::default(),
414 must_understand: vec![MustUnderstand::HostRequirements],
415 metadata: Default::default(),
416 host_requirements,
417 variables: Default::default(),
418 triggers: Default::default(),
419 components: Default::default(),
420 };
421
422 let json = locked_app.to_json().unwrap();
423
424 assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 1"#));
425
426 let reloaded = LockedApp::from_json(&json).unwrap();
427
428 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
429 assert_eq!(1, reloaded.must_understand.len());
430 assert_eq!(1, reloaded.host_requirements.len());
431 }
432
433 #[test]
434 fn deserialising_ignores_unknown_fields() {
435 use serde_json::json;
436 let j = serde_json::to_vec_pretty(&json!({
437 "spin_lock_version": 1,
438 "triggers": [],
439 "components": [],
440 "never_create_field_with_this_name": 123
441 }))
442 .unwrap();
443 let locked = LockedApp::from_json(&j).unwrap();
444 assert_eq!(0, locked.triggers.len());
445 }
446
447 #[test]
448 fn deserialising_does_not_ignore_must_understand_unknown_fields() {
449 use serde_json::json;
450 let j = serde_json::to_vec_pretty(&json!({
451 "spin_lock_version": 1,
452 "must_understand": vec!["never_create_field_with_this_name"],
453 "triggers": [],
454 "components": [],
455 "never_create_field_with_this_name": 123
456 }))
457 .unwrap();
458 let err = LockedApp::from_json(&j).expect_err(
459 "Should have refused to deserialise due to non-understood must-understand field",
460 );
461 assert!(
462 err.to_string()
463 .contains("never_create_field_with_this_name")
464 );
465 }
466
467 #[test]
468 fn deserialising_accepts_must_understands_that_it_does_understand() {
469 use serde_json::json;
470 let j = serde_json::to_vec_pretty(&json!({
471 "spin_lock_version": 1,
472 "must_understand": vec!["host_requirements"],
473 "host_requirements": {
474 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
475 },
476 "triggers": [],
477 "components": [],
478 "never_create_field_with_this_name": 123
479 }))
480 .unwrap();
481 let locked = LockedApp::from_json(&j).unwrap();
482 assert_eq!(1, locked.must_understand.len());
483 assert_eq!(1, locked.host_requirements.len());
484 }
485
486 #[test]
487 fn deserialising_rejects_host_requirements_that_are_not_supported() {
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_REQUIRED
495 },
496 "triggers": [],
497 "components": []
498 }))
499 .unwrap();
500 let err = LockedApp::from_json(&j).expect_err(
501 "Should have refused to deserialise due to non-understood host requirement",
502 );
503 assert!(err.to_string().contains("accelerated_spline_reticulation"));
504 }
505
506 #[test]
507 fn deserialising_skips_optional_host_requirements() {
508 use serde_json::json;
509 let j = serde_json::to_vec_pretty(&json!({
510 "spin_lock_version": 1,
511 "must_understand": vec!["host_requirements"],
512 "host_requirements": {
513 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
514 "accelerated_spline_reticulation": HOST_REQ_OPTIONAL
515 },
516 "triggers": [],
517 "components": []
518 }))
519 .unwrap();
520 let locked = LockedApp::from_json(&j).unwrap();
521 assert_eq!(1, locked.must_understand.len());
522 assert_eq!(1, locked.host_requirements.len());
523 }
524}