zlayer_builder/pipeline/
mod.rs1pub mod executor;
74pub mod types;
75
76pub use executor::{PipelineExecutor, PipelineResult};
77pub use types::{PipelineCacheConfig, PipelineDefaults, PipelineImage, PushConfig, ZPipeline};
78
79use crate::error::{BuildError, Result};
80
81pub fn parse_pipeline(content: &str) -> Result<ZPipeline> {
110 serde_yaml::from_str(content)
111 .map_err(|e| BuildError::zimagefile_parse(format!("Pipeline parse error: {e}")))
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_parse_minimal_pipeline() {
120 let yaml = r"
121images:
122 app:
123 file: Dockerfile
124";
125 let pipeline = parse_pipeline(yaml).unwrap();
126 assert_eq!(pipeline.images.len(), 1);
127 assert!(pipeline.images.contains_key("app"));
128 }
129
130 #[test]
131 fn test_parse_full_pipeline() {
132 let yaml = r#"
133version: "1"
134vars:
135 VERSION: "1.0.0"
136defaults:
137 format: oci
138images:
139 base:
140 file: images/Dockerfile.base
141 tags:
142 - "myapp/base:${VERSION}"
143 app:
144 file: images/Dockerfile.app
145 context: "."
146 depends_on: [base]
147 tags:
148 - "myapp/app:${VERSION}"
149push:
150 after_all: true
151"#;
152 let pipeline = parse_pipeline(yaml).unwrap();
153 assert_eq!(pipeline.version, Some("1".to_string()));
154 assert_eq!(pipeline.vars.get("VERSION"), Some(&"1.0.0".to_string()));
155 assert_eq!(pipeline.defaults.format, Some("oci".to_string()));
156 assert_eq!(pipeline.images.len(), 2);
157 assert!(pipeline.push.after_all);
158
159 let app = &pipeline.images["app"];
160 assert_eq!(app.depends_on, vec!["base"]);
161 }
162
163 #[test]
164 fn test_rejects_unknown_fields() {
165 let yaml = r#"
166images:
167 app:
168 file: Dockerfile
169 unknown_field: "should fail"
170"#;
171 assert!(parse_pipeline(yaml).is_err());
172 }
173
174 #[test]
175 fn test_image_order_preserved() {
176 let yaml = r"
177images:
178 third:
179 file: Dockerfile.third
180 first:
181 file: Dockerfile.first
182 second:
183 file: Dockerfile.second
184";
185 let pipeline = parse_pipeline(yaml).unwrap();
186 let keys: Vec<&String> = pipeline.images.keys().collect();
187 assert_eq!(keys, vec!["third", "first", "second"]);
188 }
189
190 #[test]
191 fn test_vars_and_defaults() {
192 let yaml = r#"
193vars:
194 REGISTRY: "ghcr.io/myorg"
195 VERSION: "v1.2.3"
196defaults:
197 format: docker
198 build_args:
199 RUST_VERSION: "1.90"
200 no_cache: true
201images:
202 app:
203 file: Dockerfile
204"#;
205 let pipeline = parse_pipeline(yaml).unwrap();
206 assert_eq!(
207 pipeline.vars.get("REGISTRY"),
208 Some(&"ghcr.io/myorg".to_string())
209 );
210 assert_eq!(pipeline.vars.get("VERSION"), Some(&"v1.2.3".to_string()));
211 assert_eq!(pipeline.defaults.format, Some("docker".to_string()));
212 assert_eq!(
213 pipeline.defaults.build_args.get("RUST_VERSION"),
214 Some(&"1.90".to_string())
215 );
216 assert!(pipeline.defaults.no_cache);
217 }
218
219 #[test]
220 fn test_image_with_all_fields() {
221 let yaml = r#"
222images:
223 app:
224 file: images/Dockerfile.app
225 context: "./app"
226 tags:
227 - "myapp:latest"
228 - "myapp:v1.0.0"
229 build_args:
230 NODE_ENV: production
231 DEBUG: "false"
232 depends_on:
233 - base
234 - utils
235 no_cache: true
236 format: oci
237"#;
238 let pipeline = parse_pipeline(yaml).unwrap();
239 let app = &pipeline.images["app"];
240
241 assert_eq!(app.file.to_string_lossy(), "images/Dockerfile.app");
242 assert_eq!(app.context.to_string_lossy(), "./app");
243 assert_eq!(app.tags.len(), 2);
244 assert_eq!(
245 app.build_args.get("NODE_ENV"),
246 Some(&"production".to_string())
247 );
248 assert_eq!(app.depends_on, vec!["base", "utils"]);
249 assert_eq!(app.no_cache, Some(true));
250 assert_eq!(app.format, Some("oci".to_string()));
251 }
252
253 #[test]
254 fn test_empty_vars_and_defaults() {
255 let yaml = r"
256images:
257 app:
258 file: Dockerfile
259";
260 let pipeline = parse_pipeline(yaml).unwrap();
261 assert!(pipeline.vars.is_empty());
262 assert!(pipeline.defaults.format.is_none());
263 assert!(pipeline.defaults.build_args.is_empty());
264 assert!(!pipeline.defaults.no_cache);
265 assert!(!pipeline.push.after_all);
266 }
267
268 #[test]
269 fn test_roundtrip_serialization() {
270 let yaml = r#"
271version: "1"
272vars:
273 VERSION: "1.0.0"
274images:
275 app:
276 file: Dockerfile
277 tags:
278 - "myapp:latest"
279"#;
280 let pipeline = parse_pipeline(yaml).unwrap();
281 let serialized = serde_yaml::to_string(&pipeline).unwrap();
282 let pipeline2 = parse_pipeline(&serialized).unwrap();
283
284 assert_eq!(pipeline.version, pipeline2.version);
285 assert_eq!(pipeline.vars, pipeline2.vars);
286 assert_eq!(pipeline.images.len(), pipeline2.images.len());
287 }
288
289 #[test]
290 fn test_parse_error_message() {
291 let yaml = r"
292images:
293 - this is invalid yaml structure
294";
295 let result = parse_pipeline(yaml);
296 assert!(result.is_err());
297 let err = result.unwrap_err();
298 assert!(err.to_string().contains("Pipeline parse error"));
299 }
300}