1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use spin_serde::{DependencyName, FixedVersionBackwardCompatible};
8use std::collections::BTreeMap;
9
10use crate::{
11 metadata::MetadataExt,
12 values::{ValuesMap, ValuesMapBuilder},
13};
14
15pub type LockedMap<T> = std::collections::BTreeMap<String, T>;
17
18pub const SERVICE_CHAINING_KEY: &str = "local_service_chaining";
21
22pub const HOST_REQ_OPTIONAL: &str = "optional";
25pub const HOST_REQ_REQUIRED: &str = "required";
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum MustUnderstand {
32 HostRequirements,
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum HostRequirement {
41 LocalServiceChaining,
43}
44
45#[derive(Clone, Debug, Deserialize)]
47pub struct LockedApp {
48 pub spin_lock_version: FixedVersionBackwardCompatible<1>,
50 #[serde(default, skip_serializing_if = "Vec::is_empty")]
52 pub must_understand: Vec<MustUnderstand>,
53 #[serde(default, skip_serializing_if = "ValuesMap::is_empty")]
55 pub metadata: ValuesMap,
56 #[serde(
58 default,
59 skip_serializing_if = "ValuesMap::is_empty",
60 deserialize_with = "deserialize_host_requirements"
61 )]
62 pub host_requirements: ValuesMap,
63 #[serde(default, skip_serializing_if = "LockedMap::is_empty")]
65 pub variables: LockedMap<Variable>,
66 pub triggers: Vec<LockedTrigger>,
68 pub components: Vec<LockedComponent>,
70}
71
72fn deserialize_host_requirements<'de, D>(deserializer: D) -> Result<ValuesMap, D::Error>
73where
74 D: serde::Deserializer<'de>,
75{
76 struct HostRequirementsVisitor;
77 impl<'de> serde::de::Visitor<'de> for HostRequirementsVisitor {
78 type Value = ValuesMap;
79
80 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
81 formatter.write_str("struct ValuesMap")
82 }
83
84 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
85 where
86 A: serde::de::MapAccess<'de>,
87 {
88 use serde::de::Error;
89
90 let mut hr = ValuesMapBuilder::new();
91
92 while let Some(key) = map.next_key::<String>()? {
93 let value: serde_json::Value = map.next_value()?;
94 if value.as_str() == Some(HOST_REQ_OPTIONAL) {
95 continue;
96 }
97
98 hr.serializable(key, value).map_err(A::Error::custom)?;
99 }
100
101 Ok(hr.build())
102 }
103 }
104 let m = deserializer.deserialize_map(HostRequirementsVisitor)?;
105 let unsupported: Vec<_> = m
106 .keys()
107 .filter(|k| !SUPPORTED_HOST_REQS.contains(&k.as_str()))
108 .map(|k| k.to_string())
109 .collect();
110 if unsupported.is_empty() {
111 Ok(m)
112 } else {
113 let msg = format!("This version of Spin does not support the following features required by this application: {}", unsupported.join(", "));
114 Err(serde::de::Error::custom(msg))
115 }
116}
117
118const SUPPORTED_HOST_REQS: &[&str] = &[SERVICE_CHAINING_KEY];
119
120impl Serialize for LockedApp {
121 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
122 where
123 S: serde::Serializer,
124 {
125 use serde::ser::SerializeStruct;
126
127 let version = if self.must_understand.is_empty() && self.host_requirements.is_empty() {
128 0
129 } else {
130 1
131 };
132
133 let mut la = serializer.serialize_struct("LockedApp", 7)?;
134 la.serialize_field("spin_lock_version", &version)?;
135 if !self.must_understand.is_empty() {
136 la.serialize_field("must_understand", &self.must_understand)?;
137 }
138 if !self.metadata.is_empty() {
139 la.serialize_field("metadata", &self.metadata)?;
140 }
141 if !self.host_requirements.is_empty() {
142 la.serialize_field("host_requirements", &self.host_requirements)?;
143 }
144 if !self.variables.is_empty() {
145 la.serialize_field("variables", &self.variables)?;
146 }
147 la.serialize_field("triggers", &self.triggers)?;
148 la.serialize_field("components", &self.components)?;
149 la.end()
150 }
151}
152
153impl LockedApp {
154 pub fn from_json(contents: &[u8]) -> serde_json::Result<Self> {
156 serde_json::from_slice(contents)
157 }
158
159 pub fn to_json(&self) -> serde_json::Result<Vec<u8>> {
161 serde_json::to_vec_pretty(&self)
162 }
163
164 pub fn get_metadata<'this, T: Deserialize<'this>>(
170 &'this self,
171 key: crate::MetadataKey<T>,
172 ) -> crate::Result<Option<T>> {
173 self.metadata.get_typed(key)
174 }
175
176 pub fn require_metadata<'this, T: Deserialize<'this>>(
181 &'this self,
182 key: crate::MetadataKey<T>,
183 ) -> crate::Result<T> {
184 self.metadata.require_typed(key)
185 }
186
187 pub fn ensure_needs_only(&self, supported: &[&str]) -> Result<(), String> {
191 let unmet_requirements = self
192 .host_requirements
193 .keys()
194 .filter(|hr| !supported.contains(&hr.as_str()))
195 .map(|s| s.to_string())
196 .collect::<Vec<_>>();
197 if unmet_requirements.is_empty() {
198 Ok(())
199 } else {
200 let message = unmet_requirements.join(", ");
201 Err(message)
202 }
203 }
204}
205
206#[derive(Clone, Debug, Serialize, Deserialize)]
208pub struct LockedComponent {
209 pub id: String,
211 #[serde(default, skip_serializing_if = "ValuesMap::is_empty")]
213 pub metadata: ValuesMap,
214 pub source: LockedComponentSource,
216 #[serde(default, skip_serializing_if = "LockedMap::is_empty")]
218 pub env: LockedMap<String>,
219 #[serde(default, skip_serializing_if = "Vec::is_empty")]
221 pub files: Vec<ContentPath>,
222 #[serde(default, skip_serializing_if = "LockedMap::is_empty")]
224 pub config: LockedMap<String>,
225 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
227 pub dependencies: BTreeMap<DependencyName, LockedComponentDependency>,
228}
229
230#[derive(Clone, Debug, Serialize, Deserialize)]
232pub struct LockedComponentDependency {
233 pub source: LockedComponentSource,
235 pub export: Option<String>,
237 #[serde(default, skip_serializing_if = "InheritConfiguration::is_none")]
239 pub inherit: InheritConfiguration,
240}
241
242#[derive(Clone, Debug, Serialize, Deserialize)]
244pub enum InheritConfiguration {
245 All,
247 Some(Vec<String>),
250}
251
252impl Default for InheritConfiguration {
253 fn default() -> Self {
254 InheritConfiguration::Some(vec![])
255 }
256}
257
258impl InheritConfiguration {
259 fn is_none(&self) -> bool {
260 matches!(self, InheritConfiguration::Some(configs) if configs.is_empty())
261 }
262}
263
264#[derive(Clone, Debug, Serialize, Deserialize)]
266pub struct LockedComponentSource {
267 pub content_type: String,
269 #[serde(flatten)]
271 pub content: ContentRef,
272}
273
274#[derive(Clone, Debug, Serialize, Deserialize)]
276pub struct ContentPath {
277 #[serde(flatten)]
279 pub content: ContentRef,
280 pub path: PathBuf,
282}
283
284#[derive(Clone, Debug, Default, Serialize, Deserialize)]
289pub struct ContentRef {
290 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub source: Option<String>,
294 #[serde(
299 default,
300 skip_serializing_if = "Option::is_none",
301 with = "spin_serde::base64"
302 )]
303 pub inline: Option<Vec<u8>>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub digest: Option<String>,
307}
308
309#[derive(Clone, Debug, Serialize, Deserialize)]
311pub struct LockedTrigger {
312 pub id: String,
314 pub trigger_type: String,
316 pub trigger_config: Value,
318}
319
320#[derive(Clone, Debug, Serialize, Deserialize)]
322pub struct Variable {
323 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub default: Option<String>,
326 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
328 pub secret: bool,
329}
330
331#[cfg(test)]
332mod test {
333 use super::*;
334
335 use crate::values::ValuesMapBuilder;
336
337 #[test]
338 fn locked_app_with_no_host_reqs_serialises_as_v0_and_v0_deserialises_as_v1() {
339 let locked_app = LockedApp {
340 spin_lock_version: Default::default(),
341 must_understand: Default::default(),
342 metadata: Default::default(),
343 host_requirements: Default::default(),
344 variables: Default::default(),
345 triggers: Default::default(),
346 components: Default::default(),
347 };
348
349 let json = locked_app.to_json().unwrap();
350
351 assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 0"#));
352
353 let reloaded = LockedApp::from_json(&json).unwrap();
354
355 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
356 }
357
358 #[test]
359 fn locked_app_with_host_reqs_serialises_as_v1() {
360 let mut host_requirements = ValuesMapBuilder::new();
361 host_requirements.string(SERVICE_CHAINING_KEY, "bar");
362 let host_requirements = host_requirements.build();
363
364 let locked_app = LockedApp {
365 spin_lock_version: Default::default(),
366 must_understand: vec![MustUnderstand::HostRequirements],
367 metadata: Default::default(),
368 host_requirements,
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": 1"#));
377
378 let reloaded = LockedApp::from_json(&json).unwrap();
379
380 assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
381 assert_eq!(1, reloaded.must_understand.len());
382 assert_eq!(1, reloaded.host_requirements.len());
383 }
384
385 #[test]
386 fn deserialising_ignores_unknown_fields() {
387 use serde_json::json;
388 let j = serde_json::to_vec_pretty(&json!({
389 "spin_lock_version": 1,
390 "triggers": [],
391 "components": [],
392 "never_create_field_with_this_name": 123
393 }))
394 .unwrap();
395 let locked = LockedApp::from_json(&j).unwrap();
396 assert_eq!(0, locked.triggers.len());
397 }
398
399 #[test]
400 fn deserialising_does_not_ignore_must_understand_unknown_fields() {
401 use serde_json::json;
402 let j = serde_json::to_vec_pretty(&json!({
403 "spin_lock_version": 1,
404 "must_understand": vec!["never_create_field_with_this_name"],
405 "triggers": [],
406 "components": [],
407 "never_create_field_with_this_name": 123
408 }))
409 .unwrap();
410 let err = LockedApp::from_json(&j).expect_err(
411 "Should have refused to deserialise due to non-understood must-understand field",
412 );
413 assert!(err
414 .to_string()
415 .contains("never_create_field_with_this_name"));
416 }
417
418 #[test]
419 fn deserialising_accepts_must_understands_that_it_does_understand() {
420 use serde_json::json;
421 let j = serde_json::to_vec_pretty(&json!({
422 "spin_lock_version": 1,
423 "must_understand": vec!["host_requirements"],
424 "host_requirements": {
425 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
426 },
427 "triggers": [],
428 "components": [],
429 "never_create_field_with_this_name": 123
430 }))
431 .unwrap();
432 let locked = LockedApp::from_json(&j).unwrap();
433 assert_eq!(1, locked.must_understand.len());
434 assert_eq!(1, locked.host_requirements.len());
435 }
436
437 #[test]
438 fn deserialising_rejects_host_requirements_that_are_not_supported() {
439 use serde_json::json;
440 let j = serde_json::to_vec_pretty(&json!({
441 "spin_lock_version": 1,
442 "must_understand": vec!["host_requirements"],
443 "host_requirements": {
444 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
445 "accelerated_spline_reticulation": HOST_REQ_REQUIRED
446 },
447 "triggers": [],
448 "components": []
449 }))
450 .unwrap();
451 let err = LockedApp::from_json(&j).expect_err(
452 "Should have refused to deserialise due to non-understood host requirement",
453 );
454 assert!(err.to_string().contains("accelerated_spline_reticulation"));
455 }
456
457 #[test]
458 fn deserialising_skips_optional_host_requirements() {
459 use serde_json::json;
460 let j = serde_json::to_vec_pretty(&json!({
461 "spin_lock_version": 1,
462 "must_understand": vec!["host_requirements"],
463 "host_requirements": {
464 SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
465 "accelerated_spline_reticulation": HOST_REQ_OPTIONAL
466 },
467 "triggers": [],
468 "components": []
469 }))
470 .unwrap();
471 let locked = LockedApp::from_json(&j).unwrap();
472 assert_eq!(1, locked.must_understand.len());
473 assert_eq!(1, locked.host_requirements.len());
474 }
475}