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