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 default: Option<String>,
348 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
350 pub secret: bool,
351}
352
353#[cfg(test)]
354mod test {
355 use super::*;
356
357 use crate::values::ValuesMapBuilder;
358
359 #[test]
360 fn locked_app_with_no_host_reqs_serialises_as_v0_and_v0_deserialises_as_v1() {
361 let locked_app = LockedApp {
362 spin_lock_version: Default::default(),
363 must_understand: Default::default(),
364 metadata: Default::default(),
365 host_requirements: Default::default(),
366 variables: Default::default(),
367 triggers: Default::default(),
368 components: Default::default(),
369 };
370
371 let json = locked_app.to_json().unwrap();
372
373 assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 0"#));
374
375 let reloaded = LockedApp::from_json(&json).unwrap();
376
377 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
378 }
379
380 #[test]
381 fn locked_app_with_host_reqs_serialises_as_v1() {
382 let mut host_requirements = ValuesMapBuilder::new();
383 host_requirements.string(SERVICE_CHAINING_KEY, "bar");
384 let host_requirements = host_requirements.build();
385
386 let locked_app = LockedApp {
387 spin_lock_version: Default::default(),
388 must_understand: vec![MustUnderstand::HostRequirements],
389 metadata: Default::default(),
390 host_requirements,
391 variables: Default::default(),
392 triggers: Default::default(),
393 components: Default::default(),
394 };
395
396 let json = locked_app.to_json().unwrap();
397
398 assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 1"#));
399
400 let reloaded = LockedApp::from_json(&json).unwrap();
401
402 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
403 assert_eq!(1, reloaded.must_understand.len());
404 assert_eq!(1, reloaded.host_requirements.len());
405 }
406
407 #[test]
408 fn deserialising_ignores_unknown_fields() {
409 use serde_json::json;
410 let j = serde_json::to_vec_pretty(&json!({
411 "spin_lock_version": 1,
412 "triggers": [],
413 "components": [],
414 "never_create_field_with_this_name": 123
415 }))
416 .unwrap();
417 let locked = LockedApp::from_json(&j).unwrap();
418 assert_eq!(0, locked.triggers.len());
419 }
420
421 #[test]
422 fn deserialising_does_not_ignore_must_understand_unknown_fields() {
423 use serde_json::json;
424 let j = serde_json::to_vec_pretty(&json!({
425 "spin_lock_version": 1,
426 "must_understand": vec!["never_create_field_with_this_name"],
427 "triggers": [],
428 "components": [],
429 "never_create_field_with_this_name": 123
430 }))
431 .unwrap();
432 let err = LockedApp::from_json(&j).expect_err(
433 "Should have refused to deserialise due to non-understood must-understand field",
434 );
435 assert!(err
436 .to_string()
437 .contains("never_create_field_with_this_name"));
438 }
439
440 #[test]
441 fn deserialising_accepts_must_understands_that_it_does_understand() {
442 use serde_json::json;
443 let j = serde_json::to_vec_pretty(&json!({
444 "spin_lock_version": 1,
445 "must_understand": vec!["host_requirements"],
446 "host_requirements": {
447 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
448 },
449 "triggers": [],
450 "components": [],
451 "never_create_field_with_this_name": 123
452 }))
453 .unwrap();
454 let locked = LockedApp::from_json(&j).unwrap();
455 assert_eq!(1, locked.must_understand.len());
456 assert_eq!(1, locked.host_requirements.len());
457 }
458
459 #[test]
460 fn deserialising_rejects_host_requirements_that_are_not_supported() {
461 use serde_json::json;
462 let j = serde_json::to_vec_pretty(&json!({
463 "spin_lock_version": 1,
464 "must_understand": vec!["host_requirements"],
465 "host_requirements": {
466 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
467 "accelerated_spline_reticulation": HOST_REQ_REQUIRED
468 },
469 "triggers": [],
470 "components": []
471 }))
472 .unwrap();
473 let err = LockedApp::from_json(&j).expect_err(
474 "Should have refused to deserialise due to non-understood host requirement",
475 );
476 assert!(err.to_string().contains("accelerated_spline_reticulation"));
477 }
478
479 #[test]
480 fn deserialising_skips_optional_host_requirements() {
481 use serde_json::json;
482 let j = serde_json::to_vec_pretty(&json!({
483 "spin_lock_version": 1,
484 "must_understand": vec!["host_requirements"],
485 "host_requirements": {
486 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
487 "accelerated_spline_reticulation": HOST_REQ_OPTIONAL
488 },
489 "triggers": [],
490 "components": []
491 }))
492 .unwrap();
493 let locked = LockedApp::from_json(&j).unwrap();
494 assert_eq!(1, locked.must_understand.len());
495 assert_eq!(1, locked.host_requirements.len());
496 }
497}