1use std::path::{Path, PathBuf};
2
3use tracing::info;
4
5use crate::error::{CiabError, CiabResult};
6
7#[derive(Debug, Clone)]
8pub enum ResourceSource {
9 FilePath(PathBuf),
10 Url(String),
11 Git {
12 url: String,
13 path: String,
14 ref_: Option<String>,
15 },
16 Builtin(String),
17}
18
19pub fn parse_source_string(s: &str) -> ResourceSource {
20 if let Some(rest) = s.strip_prefix("git::") {
21 parse_git_source(rest)
22 } else if s.starts_with("http://") || s.starts_with("https://") {
23 ResourceSource::Url(s.to_string())
24 } else if let Some(name) = s.strip_prefix("builtin://") {
25 ResourceSource::Builtin(name.to_string())
26 } else {
27 ResourceSource::FilePath(PathBuf::from(s))
28 }
29}
30
31fn parse_git_source(s: &str) -> ResourceSource {
32 let (url_and_path, ref_) = if let Some(idx) = s.find("?ref=") {
33 (&s[..idx], Some(s[idx + 5..].to_string()))
34 } else {
35 (s, None)
36 };
37
38 let search_start = if let Some(proto_end) = url_and_path.find("://") {
40 proto_end + 3
41 } else {
42 0
43 };
44
45 let (url, path) = if let Some(rel_idx) = url_and_path[search_start..].find("//") {
46 let idx = search_start + rel_idx;
47 (
48 url_and_path[..idx].to_string(),
49 url_and_path[idx + 2..].to_string(),
50 )
51 } else {
52 (url_and_path.to_string(), String::new())
53 };
54
55 ResourceSource::Git { url, path, ref_ }
56}
57
58pub async fn resolve_resource(source: &ResourceSource) -> CiabResult<String> {
59 match source {
60 ResourceSource::FilePath(path) => resolve_file(path).await,
61 ResourceSource::Url(url) => resolve_url(url).await,
62 ResourceSource::Git { url, path, ref_ } => {
63 resolve_git(url, path, ref_.as_deref()).await
64 }
65 ResourceSource::Builtin(name) => resolve_builtin(name),
66 }
67}
68
69async fn resolve_file(path: &Path) -> CiabResult<String> {
70 tokio::fs::read_to_string(path).await.map_err(|e| {
71 CiabError::ResourceResolutionError(format!(
72 "Failed to read file {}: {}",
73 path.display(),
74 e
75 ))
76 })
77}
78
79async fn resolve_url(url: &str) -> CiabResult<String> {
80 info!(url = url, "Fetching resource from URL");
81 let response = reqwest::get(url).await.map_err(|e| {
82 CiabError::ResourceResolutionError(format!("Failed to fetch {}: {}", url, e))
83 })?;
84
85 if !response.status().is_success() {
86 return Err(CiabError::ResourceResolutionError(format!(
87 "HTTP {} fetching {}",
88 response.status(),
89 url
90 )));
91 }
92
93 response.text().await.map_err(|e| {
94 CiabError::ResourceResolutionError(format!(
95 "Failed to read response from {}: {}",
96 url, e
97 ))
98 })
99}
100
101async fn resolve_git(url: &str, subpath: &str, ref_: Option<&str>) -> CiabResult<String> {
102 let url = url.to_string();
103 let subpath = subpath.to_string();
104 let ref_ = ref_.map(|s| s.to_string());
105
106 tokio::task::spawn_blocking(move || {
107 let tmp = tempfile::tempdir().map_err(|e| {
108 CiabError::ResourceResolutionError(format!("Failed to create temp dir: {}", e))
109 })?;
110
111 info!(url = %url, subpath = %subpath, ref_ = ?ref_, "Cloning git resource");
112
113 let mut builder = git2::build::RepoBuilder::new();
114 let mut fetch_opts = git2::FetchOptions::new();
115 fetch_opts.depth(1);
116 builder.fetch_options(fetch_opts);
117
118 if let Some(ref branch) = ref_ {
119 builder.branch(branch);
120 }
121
122 let repo = builder.clone(&url, tmp.path()).map_err(|e| {
123 CiabError::ResourceResolutionError(format!("Git clone failed for {}: {}", url, e))
124 })?;
125
126 let file_path = if subpath.is_empty() {
127 repo.workdir()
128 .ok_or_else(|| {
129 CiabError::ResourceResolutionError("Bare repository".to_string())
130 })?
131 .to_path_buf()
132 } else {
133 repo.workdir()
134 .ok_or_else(|| {
135 CiabError::ResourceResolutionError("Bare repository".to_string())
136 })?
137 .join(&subpath)
138 };
139
140 std::fs::read_to_string(&file_path).map_err(|e| {
141 CiabError::ResourceResolutionError(format!(
142 "Failed to read {} from cloned repo: {}",
143 file_path.display(),
144 e
145 ))
146 })
147 })
148 .await
149 .map_err(|e| CiabError::ResourceResolutionError(format!("Git task panicked: {}", e)))?
150}
151
152fn resolve_builtin(name: &str) -> CiabResult<String> {
153 match name {
154 "default-ec2" => Ok(
155 include_str!("../templates/default-ec2.pkr.hcl").to_string(),
156 ),
157 "default-config" => Ok(include_str!("../config.default.toml").to_string()),
158 _ => Err(CiabError::ResourceResolutionError(format!(
159 "Unknown builtin resource: {}",
160 name
161 ))),
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_parse_file_path() {
171 let source = parse_source_string("/path/to/file.toml");
172 assert!(
173 matches!(source, ResourceSource::FilePath(p) if p == PathBuf::from("/path/to/file.toml"))
174 );
175 }
176
177 #[test]
178 fn test_parse_url() {
179 let source = parse_source_string("https://example.com/config.toml");
180 assert!(
181 matches!(source, ResourceSource::Url(u) if u == "https://example.com/config.toml")
182 );
183 }
184
185 #[test]
186 fn test_parse_builtin() {
187 let source = parse_source_string("builtin://default-ec2");
188 assert!(matches!(source, ResourceSource::Builtin(n) if n == "default-ec2"));
189 }
190
191 #[test]
192 fn test_parse_git_full() {
193 let source = parse_source_string(
194 "git::https://github.com/org/repo.git//path/to/file.hcl?ref=main",
195 );
196 match source {
197 ResourceSource::Git { url, path, ref_ } => {
198 assert_eq!(url, "https://github.com/org/repo.git");
199 assert_eq!(path, "path/to/file.hcl");
200 assert_eq!(ref_, Some("main".to_string()));
201 }
202 _ => panic!("Expected Git source"),
203 }
204 }
205
206 #[test]
207 fn test_parse_git_no_ref() {
208 let source =
209 parse_source_string("git::https://github.com/org/repo.git//template.hcl");
210 match source {
211 ResourceSource::Git { url, path, ref_ } => {
212 assert_eq!(url, "https://github.com/org/repo.git");
213 assert_eq!(path, "template.hcl");
214 assert_eq!(ref_, None);
215 }
216 _ => panic!("Expected Git source"),
217 }
218 }
219
220 #[test]
221 fn test_parse_git_no_subpath() {
222 let source =
223 parse_source_string("git::https://github.com/org/repo.git?ref=v1.0");
224 match source {
225 ResourceSource::Git { url, path, ref_ } => {
226 assert_eq!(url, "https://github.com/org/repo.git");
227 assert_eq!(path, "");
228 assert_eq!(ref_, Some("v1.0".to_string()));
229 }
230 _ => panic!("Expected Git source"),
231 }
232 }
233}