1mod allowed_http_hosts;
4
5use crate::{
6 error::Error,
7 schema::{v1, v2},
8};
9use allowed_http_hosts::{parse_allowed_http_hosts, AllowedHttpHosts};
10
11pub fn v1_to_v2_app(manifest: v1::AppManifestV1) -> Result<v2::AppManifest, Error> {
13 let trigger_type = manifest.trigger.trigger_type.clone();
14 let trigger_global_configs = [(trigger_type.clone(), manifest.trigger.config)]
15 .into_iter()
16 .collect();
17
18 let application = v2::AppDetails {
19 name: manifest.name,
20 version: manifest.version,
21 description: manifest.description,
22 authors: manifest.authors,
23 trigger_global_configs,
24 tool: Default::default(),
25 };
26
27 let app_variables = manifest
28 .variables
29 .into_iter()
30 .map(|(key, var)| Ok((id_from_string(key)?, var)))
31 .collect::<Result<_, Error>>()?;
32
33 let mut triggers = v2::Map::<String, Vec<v2::Trigger>>::default();
34 let mut components = v2::Map::default();
35 for component in manifest.components {
36 let component_id = component_id_from_string(component.id)?;
37
38 let variables = component
39 .config
40 .into_iter()
41 .map(|(key, var)| Ok((id_from_string(key)?, var)))
42 .collect::<Result<_, Error>>()?;
43
44 let ai_models = component
45 .ai_models
46 .into_iter()
47 .map(id_from_string)
48 .collect::<Result<_, Error>>()?;
49 let allowed_http = convert_allowed_http_to_allowed_hosts(
50 &component.allowed_http_hosts,
51 component.allowed_outbound_hosts.is_none(),
52 )
53 .map_err(Error::ValidationError)?;
54 let allowed_outbound_hosts = match component.allowed_outbound_hosts {
55 Some(mut hs) => {
56 hs.extend(allowed_http);
57 hs
58 }
59 None => allowed_http,
60 };
61 components.insert(
62 component_id.clone(),
63 #[allow(deprecated)]
64 v2::Component {
65 source: component.source,
66 description: component.description,
67 variables,
68 environment: component.environment,
69 files: component.files,
70 exclude_files: component.exclude_files,
71 key_value_stores: component.key_value_stores,
72 sqlite_databases: component.sqlite_databases,
73 ai_models,
74 build: component.build,
75 tool: Default::default(),
76 allowed_outbound_hosts,
77 allowed_http_hosts: Vec::new(),
78 dependencies_inherit_configuration: false,
79 dependencies: Default::default(),
80 },
81 );
82 triggers
83 .entry(trigger_type.clone())
84 .or_default()
85 .push(v2::Trigger {
86 id: format!("trigger-{component_id}"),
87 component: Some(v2::ComponentSpec::Reference(component_id)),
88 components: Default::default(),
89 config: component.trigger,
90 });
91 }
92 Ok(v2::AppManifest {
93 spin_manifest_version: Default::default(),
94 application,
95 variables: app_variables,
96 triggers,
97 components,
98 })
99}
100
101pub fn convert_allowed_http_to_allowed_hosts(
106 allowed_http_hosts: &[impl AsRef<str>],
107 allow_database_access: bool,
108) -> anyhow::Result<Vec<String>> {
109 let http_hosts = parse_allowed_http_hosts(allowed_http_hosts)?;
110 let mut outbound_hosts = if allow_database_access {
111 vec![
112 "redis://*:*".into(),
113 "mysql://*:*".into(),
114 "postgres://*:*".into(),
115 ]
116 } else {
117 Vec::new()
118 };
119 match http_hosts {
120 AllowedHttpHosts::AllowAll => outbound_hosts.extend([
121 "http://*:*".into(),
122 "https://*:*".into(),
123 "http://self".into(),
124 ]),
125 AllowedHttpHosts::AllowSpecific(specific) => {
126 outbound_hosts.extend(specific.into_iter().flat_map(|s| {
127 if s.domain == "self" {
128 vec!["http://self".into()]
129 } else {
130 let port = s.port.map(|p| format!(":{p}")).unwrap_or_default();
131 vec![
132 format!("http://{}{}", s.domain, port),
133 format!("https://{}{}", s.domain, port),
134 ]
135 }
136 }))
137 }
138 };
139 Ok(outbound_hosts)
140}
141
142fn component_id_from_string(id: String) -> Result<v2::KebabId, Error> {
143 if let Ok(id) = id.clone().try_into() {
145 return Ok(id);
146 }
147 let id = id.replace('_', "-").to_lowercase();
149 id.clone()
150 .try_into()
151 .map_err(|err: String| Error::InvalidID { id, reason: err })
152}
153
154fn id_from_string<const DELIM: char, const LOWER: bool>(
155 id: String,
156) -> Result<spin_serde::id::Id<DELIM, LOWER>, Error> {
157 id.clone()
158 .try_into()
159 .map_err(|err: String| Error::InvalidID { id, reason: err })
160}