1use anyhow::{anyhow, Error, Result};
2use maelstrom_base::{
3 ArtifactType, EnumSet, GroupId, JobDevice, JobDeviceListDeserialize, JobMount, JobSpec, Layer,
4 NonEmpty, Sha256Digest, UserId, Utf8PathBuf,
5};
6use maelstrom_client::spec::{
7 incompatible, substitute, Image, ImageConfig, ImageOption, ImageUse, PossiblyImage,
8};
9use serde::{de, Deserialize, Deserializer};
10use std::{collections::BTreeMap, io::Read};
11
12struct JobSpecIterator<InnerT, LayerMapperT, EnvLookupT, ImageLookupT> {
13 inner: InnerT,
14 layer_mapper: LayerMapperT,
15 env_lookup: EnvLookupT,
16 image_lookup: ImageLookupT,
17}
18
19impl<InnerT, LayerMapperT, EnvLookupT, ImageLookupT> Iterator
20 for JobSpecIterator<InnerT, LayerMapperT, EnvLookupT, ImageLookupT>
21where
22 InnerT: Iterator<Item = serde_json::Result<Job>>,
23 LayerMapperT: Fn(Layer) -> Result<(Sha256Digest, ArtifactType)>,
24 EnvLookupT: Fn(&str) -> Result<Option<String>>,
25 ImageLookupT: FnMut(&str) -> Result<ImageConfig>,
26{
27 type Item = Result<JobSpec>;
28
29 fn next(&mut self) -> Option<Self::Item> {
30 match self.inner.next() {
31 None => None,
32 Some(Err(err)) => Some(Err(Error::new(err))),
33 Some(Ok(job)) => Some(job.into_job_spec(
34 &self.layer_mapper,
35 &self.env_lookup,
36 &mut self.image_lookup,
37 )),
38 }
39 }
40}
41
42pub fn job_spec_iter_from_reader(
43 reader: impl Read,
44 layer_mapper: impl Fn(Layer) -> Result<(Sha256Digest, ArtifactType)>,
45 env_lookup: impl Fn(&str) -> Result<Option<String>>,
46 image_lookup: impl FnMut(&str) -> Result<ImageConfig>,
47) -> impl Iterator<Item = Result<JobSpec>> {
48 let inner = serde_json::Deserializer::from_reader(reader).into_iter::<Job>();
49 JobSpecIterator {
50 inner,
51 layer_mapper,
52 env_lookup,
53 image_lookup,
54 }
55}
56
57#[derive(Debug, Eq, PartialEq)]
58struct Job {
59 program: Utf8PathBuf,
60 arguments: Option<Vec<String>>,
61 environment: Option<PossiblyImage<BTreeMap<String, String>>>,
62 added_environment: BTreeMap<String, String>,
63 layers: PossiblyImage<NonEmpty<Layer>>,
64 added_layers: Vec<Layer>,
65 devices: Option<EnumSet<JobDeviceListDeserialize>>,
66 mounts: Option<Vec<JobMount>>,
67 enable_loopback: Option<bool>,
68 enable_writable_file_system: Option<bool>,
69 working_directory: Option<PossiblyImage<Utf8PathBuf>>,
70 user: Option<UserId>,
71 group: Option<GroupId>,
72 image: Option<String>,
73}
74
75impl Job {
76 #[cfg(test)]
77 fn new(program: Utf8PathBuf, layers: NonEmpty<Layer>) -> Self {
78 Job {
79 program,
80 layers: PossiblyImage::Explicit(layers),
81 added_layers: Default::default(),
82 arguments: None,
83 environment: None,
84 added_environment: Default::default(),
85 devices: None,
86 mounts: None,
87 enable_loopback: None,
88 enable_writable_file_system: None,
89 working_directory: None,
90 user: None,
91 group: None,
92 image: None,
93 }
94 }
95
96 fn into_job_spec(
97 self,
98 layer_mapper: impl Fn(Layer) -> Result<(Sha256Digest, ArtifactType)>,
99 env_lookup: impl Fn(&str) -> Result<Option<String>>,
100 image_lookup: impl FnMut(&str) -> Result<ImageConfig>,
101 ) -> Result<JobSpec> {
102 let image = ImageOption::new(&self.image, image_lookup)?;
103 let mut environment = match self.environment {
104 None => BTreeMap::default(),
105 Some(PossiblyImage::Explicit(environment)) => environment
106 .into_iter()
107 .map(|(k, v)| -> Result<_> {
108 Ok((
109 k,
110 substitute::substitute(&v, &env_lookup, |_| Option::<String>::None)?
111 .into_owned(),
112 ))
113 })
114 .collect::<Result<BTreeMap<_, _>>>()?,
115 Some(PossiblyImage::Image) => image.environment()?,
116 };
117 let added_environment = self
118 .added_environment
119 .into_iter()
120 .map(|(k, v)| -> Result<_> {
121 Ok((
122 k,
123 substitute::substitute(&v, &env_lookup, |var| {
124 environment.get(var).map(|v| v.as_str())
125 })?
126 .into_owned(),
127 ))
128 })
129 .collect::<Result<BTreeMap<_, _>>>()?;
130 environment.extend(added_environment);
131 let environment = Vec::from_iter(environment.into_iter().map(|(k, v)| k + "=" + &v));
132 let mut layers = match self.layers {
133 PossiblyImage::Explicit(layers) => layers,
134 PossiblyImage::Image => NonEmpty::from_vec(image.layers()?.collect())
135 .ok_or_else(|| anyhow!("image {} has no layers to use", image.name()))?,
136 };
137 layers.extend(self.added_layers);
138 let layers = layers.try_map(layer_mapper)?;
139 let working_directory = match self.working_directory {
140 None => Utf8PathBuf::from("/"),
141 Some(PossiblyImage::Explicit(working_directory)) => working_directory,
142 Some(PossiblyImage::Image) => image.working_directory()?,
143 };
144 Ok(JobSpec {
145 program: self.program,
146 arguments: self.arguments.unwrap_or_default(),
147 environment,
148 layers,
149 devices: self
150 .devices
151 .unwrap_or(EnumSet::EMPTY)
152 .into_iter()
153 .map(JobDevice::from)
154 .collect(),
155 mounts: self.mounts.unwrap_or_default(),
156 enable_loopback: self.enable_loopback.unwrap_or_default(),
157 enable_writable_file_system: self.enable_writable_file_system.unwrap_or_default(),
158 working_directory,
159 user: self.user.unwrap_or(UserId::from(0)),
160 group: self.group.unwrap_or(GroupId::from(0)),
161 })
162 }
163}
164
165#[derive(Deserialize)]
166#[serde(field_identifier, rename_all = "snake_case")]
167enum JobField {
168 Program,
169 Arguments,
170 Environment,
171 AddedEnvironment,
172 Layers,
173 AddedLayers,
174 Devices,
175 Mounts,
176 EnableLoopback,
177 EnableWritableFileSystem,
178 WorkingDirectory,
179 User,
180 Group,
181 Image,
182}
183
184struct JobVisitor;
185
186fn must_be_image<T, E>(
187 var: &Option<PossiblyImage<T>>,
188 if_none: &str,
189 if_explicit: &str,
190) -> std::result::Result<(), E>
191where
192 E: de::Error,
193{
194 match var {
195 None => Err(E::custom(format_args!("{}", if_none))),
196 Some(PossiblyImage::Explicit(_)) => Err(E::custom(format_args!("{}", if_explicit))),
197 Some(PossiblyImage::Image) => Ok(()),
198 }
199}
200
201impl<'de> de::Visitor<'de> for JobVisitor {
202 type Value = Job;
203
204 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 write!(formatter, "Job")
206 }
207
208 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
209 where
210 A: de::MapAccess<'de>,
211 {
212 let mut program = None;
213 let mut arguments = None;
214 let mut environment = None;
215 let mut added_environment = None;
216 let mut layers = None;
217 let mut added_layers = None;
218 let mut devices = None;
219 let mut mounts = None;
220 let mut enable_loopback = None;
221 let mut enable_writable_file_system = None;
222 let mut working_directory = None;
223 let mut user = None;
224 let mut group = None;
225 let mut image = None;
226 while let Some(key) = map.next_key()? {
227 match key {
228 JobField::Program => {
229 program = Some(map.next_value()?);
230 }
231 JobField::Arguments => {
232 arguments = Some(map.next_value()?);
233 }
234 JobField::Environment => {
235 incompatible(
236 &environment,
237 concat!(
238 "field `environment` cannot be set if `image` with a `use` of ",
239 "`environment` is also set (try `added_environment` instead)"
240 ),
241 )?;
242 environment = Some(PossiblyImage::Explicit(map.next_value()?));
243 }
244 JobField::AddedEnvironment => {
245 must_be_image(
246 &environment,
247 "field `added_environment` set before `image` with a `use` of `environment`",
248 "field `added_environment` cannot be set with `environment` field",
249 )?;
250 added_environment = Some(map.next_value()?);
251 }
252 JobField::Layers => {
253 incompatible(
254 &layers,
255 concat!(
256 "field `layers` cannot be set if `image` with a `use` of ",
257 "`layers` is also set (try `added_layers` instead)"
258 ),
259 )?;
260 layers = Some(PossiblyImage::Explicit(
261 NonEmpty::from_vec(map.next_value()?).ok_or_else(|| {
262 de::Error::custom(format_args!("field `layers` cannot be empty"))
263 })?,
264 ));
265 }
266 JobField::AddedLayers => {
267 must_be_image(
268 &layers,
269 "field `added_layers` set before `image` with a `use` of `layers`",
270 "field `added_layers` cannot be set with `layer` field",
271 )?;
272 added_layers = Some(map.next_value()?);
273 }
274 JobField::Devices => {
275 devices = Some(map.next_value()?);
276 }
277 JobField::Mounts => {
278 mounts = Some(map.next_value()?);
279 }
280 JobField::EnableLoopback => {
281 enable_loopback = Some(map.next_value()?);
282 }
283 JobField::EnableWritableFileSystem => {
284 enable_writable_file_system = Some(map.next_value()?);
285 }
286 JobField::WorkingDirectory => {
287 incompatible(
288 &working_directory,
289 concat!(
290 "field `working_directory` cannot be set if `image` with a `use` of ",
291 "`working_directory` is also set"
292 ),
293 )?;
294 working_directory = Some(PossiblyImage::Explicit(map.next_value()?));
295 }
296 JobField::User => {
297 user = Some(map.next_value()?);
298 }
299 JobField::Group => {
300 group = Some(map.next_value()?);
301 }
302 JobField::Image => {
303 let i = map.next_value::<Image>()?;
304 image = Some(i.name);
305 for use_ in i.use_ {
306 match use_ {
307 ImageUse::WorkingDirectory => {
308 incompatible(
309 &working_directory,
310 "field `image` cannot use `working_directory` if field `working_directory` is also set",
311 )?;
312 working_directory = Some(PossiblyImage::Image);
313 }
314 ImageUse::Layers => {
315 incompatible(
316 &layers,
317 "field `image` cannot use `layers` if field `layers` is also set",
318 )?;
319 layers = Some(PossiblyImage::Image);
320 }
321 ImageUse::Environment => {
322 incompatible(
323 &environment,
324 "field `image` cannot use `environment` if field `environment` is also set",
325 )?;
326 environment = Some(PossiblyImage::Image);
327 }
328 }
329 }
330 }
331 }
332 }
333 Ok(Job {
334 program: program.ok_or_else(|| de::Error::missing_field("program"))?,
335 arguments,
336 environment,
337 added_environment: added_environment.unwrap_or_default(),
338 layers: layers.ok_or_else(|| de::Error::missing_field("layers"))?,
339 added_layers: added_layers.unwrap_or_default(),
340 devices,
341 mounts,
342 enable_loopback,
343 enable_writable_file_system,
344 working_directory,
345 user,
346 group,
347 image,
348 })
349 }
350}
351
352impl<'de> de::Deserialize<'de> for Job {
353 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
354 where
355 D: Deserializer<'de>,
356 {
357 deserializer.deserialize_any(JobVisitor)
358 }
359}
360
361#[cfg(test)]
362mod test {
363 use super::*;
364 use assert_matches::assert_matches;
365 use maelstrom_base::{enum_set, nonempty, JobMountFsType};
366 use maelstrom_test::{digest, path_buf_vec, string, string_vec, tar_layer, utf8_path_buf};
367
368 fn layer_mapper(layer: Layer) -> Result<(Sha256Digest, ArtifactType)> {
369 assert_matches!(layer, Layer::Tar { path } => {
370 Ok((
371 Sha256Digest::from(path.as_str().parse::<u64>()?),
372 ArtifactType::Tar,
373 ))
374 })
375 }
376
377 fn env(var: &str) -> Result<Option<String>> {
378 match var {
379 "FOO" => Ok(Some(string!("foo-env"))),
380 "err" => Err(anyhow!("error converting value to UTF-8")),
381 _ => Ok(None),
382 }
383 }
384
385 fn images(name: &str) -> Result<ImageConfig> {
386 match name {
387 "image1" => Ok(ImageConfig {
388 layers: path_buf_vec!["42", "43"],
389 working_directory: Some("/foo".into()),
390 environment: Some(string_vec!["FOO=image-foo", "BAZ=image-baz",]),
391 }),
392 "image-with-env-substitutions" => Ok(ImageConfig {
393 environment: Some(string_vec!["PATH=$env{PATH}"]),
394 ..Default::default()
395 }),
396 "empty" => Ok(Default::default()),
397 _ => Err(anyhow!("no container named {name} found")),
398 }
399 }
400
401 #[test]
402 fn minimum_into_job_spec() {
403 assert_eq!(
404 Job::new(utf8_path_buf!("program"), nonempty![tar_layer!("1")])
405 .into_job_spec(layer_mapper, env, images)
406 .unwrap(),
407 JobSpec::new("program", nonempty![(digest!(1), ArtifactType::Tar)]),
408 );
409 }
410
411 #[test]
412 fn most_into_job_spec() {
413 assert_eq!(
414 Job {
415 arguments: Some(string_vec!["arg1", "arg2"]),
416 environment: Some(PossiblyImage::Explicit(BTreeMap::from([
417 (string!("FOO"), string!("foo")),
418 (string!("BAR"), string!("bar")),
419 ]))),
420 devices: Some(enum_set! {JobDeviceListDeserialize::Null}),
421 mounts: Some(vec![JobMount {
422 fs_type: JobMountFsType::Tmp,
423 mount_point: utf8_path_buf!("/tmp"),
424 }]),
425 working_directory: Some(PossiblyImage::Explicit("/working-directory".into())),
426 user: Some(UserId::from(101)),
427 group: Some(GroupId::from(202)),
428 ..Job::new(utf8_path_buf!("program"), nonempty![tar_layer!("1")])
429 }
430 .into_job_spec(layer_mapper, env, images)
431 .unwrap(),
432 JobSpec::new("program", nonempty![(digest!(1), ArtifactType::Tar)])
433 .arguments(["arg1", "arg2"])
434 .environment(["BAR=bar", "FOO=foo"])
435 .devices(enum_set! {JobDevice::Null})
436 .mounts([JobMount {
437 fs_type: JobMountFsType::Tmp,
438 mount_point: utf8_path_buf!("/tmp"),
439 }])
440 .working_directory("/working-directory")
441 .user(101)
442 .group(202),
443 );
444 }
445
446 #[test]
447 fn enable_loopback_into_job_spec() {
448 assert_eq!(
449 Job {
450 enable_loopback: Some(true),
451 ..Job::new(utf8_path_buf!("program"), nonempty![tar_layer!("1")])
452 }
453 .into_job_spec(layer_mapper, env, images)
454 .unwrap(),
455 JobSpec::new("program", nonempty![(digest!(1), ArtifactType::Tar)])
456 .enable_loopback(true),
457 );
458 }
459
460 #[test]
461 fn enable_writable_file_system_into_job_spec() {
462 assert_eq!(
463 Job {
464 enable_writable_file_system: Some(true),
465 ..Job::new(utf8_path_buf!("program"), nonempty![tar_layer!("1")])
466 }
467 .into_job_spec(layer_mapper, env, images)
468 .unwrap(),
469 JobSpec::new("program", nonempty![(digest!(1), ArtifactType::Tar)])
470 .enable_writable_file_system(true),
471 );
472 }
473
474 fn parse_job(str_: &str) -> serde_json::Result<Job> {
475 serde_json::from_str(str_)
476 }
477
478 fn assert_error(err: serde_json::Error, expected: &str) {
479 let message = format!("{err}");
480 assert!(
481 message.starts_with(expected),
482 "message: {message:?}, expected: {expected:?}"
483 );
484 }
485
486 fn assert_anyhow_error(err: Error, expected: &str) {
487 let message = format!("{err}");
488 assert!(
489 message == expected,
490 "message: {message:?}, expected: {expected:?}"
491 );
492 }
493
494 #[test]
495 fn basic() {
496 assert_eq!(
497 parse_job(
498 r#"{
499 "program": "/bin/sh",
500 "layers": [ { "tar": "1" } ]
501 }"#
502 )
503 .unwrap()
504 .into_job_spec(layer_mapper, env, images)
505 .unwrap(),
506 JobSpec::new(
507 string!("/bin/sh"),
508 nonempty![(digest!(1), ArtifactType::Tar)]
509 ),
510 );
511 }
512
513 #[test]
514 fn missing_program() {
515 assert_error(
516 parse_job(
517 r#"{
518 "layers": [ { "tar": "1" } ]
519 }"#,
520 )
521 .unwrap_err(),
522 "missing field `program`",
523 );
524 }
525
526 #[test]
527 fn missing_layers() {
528 assert_error(
529 parse_job(
530 r#"{
531 "program": "/bin/sh"
532 }"#,
533 )
534 .unwrap_err(),
535 "missing field `layers`",
536 );
537 }
538
539 #[test]
540 fn empty_layers() {
541 assert_error(
542 parse_job(
543 r#"{
544 "program": "/bin/sh",
545 "layers": []
546 }"#,
547 )
548 .unwrap_err(),
549 "field `layers` cannot be empty",
550 );
551 }
552
553 #[test]
554 fn layers_from_image() {
555 assert_eq!(
556 parse_job(
557 r#"{
558 "program": "/bin/sh",
559 "image": {
560 "name": "image1",
561 "use": [ "layers" ]
562 }
563 }"#
564 )
565 .unwrap()
566 .into_job_spec(layer_mapper, env, images)
567 .unwrap(),
568 JobSpec::new(
569 string!("/bin/sh"),
570 nonempty![
571 (digest!(42), ArtifactType::Tar),
572 (digest!(43), ArtifactType::Tar)
573 ]
574 ),
575 );
576 }
577
578 #[test]
579 fn empty_layers_from_image() {
580 assert_anyhow_error(
581 parse_job(
582 r#"{
583 "program": "/bin/sh",
584 "image": {
585 "name": "empty",
586 "use": [ "layers" ]
587 }
588 }"#,
589 )
590 .unwrap()
591 .into_job_spec(layer_mapper, env, images)
592 .unwrap_err(),
593 "image empty has no layers to use",
594 );
595 }
596
597 #[test]
598 fn layers_after_layers_from_image() {
599 assert_error(
600 parse_job(
601 r#"{
602 "program": "/bin/sh",
603 "image": {
604 "name": "image1",
605 "use": [ "layers" ]
606 },
607 "layers": [ { "tar": "1" } ]
608 }"#,
609 )
610 .unwrap_err(),
611 "field `layers` cannot be set if `image` with a `use` of `layers` is also set",
612 );
613 }
614
615 #[test]
616 fn layers_from_image_after_layers() {
617 assert_error(
618 parse_job(
619 r#"{
620 "program": "/bin/sh",
621 "layers": [ { "tar": "1" } ],
622 "image": {
623 "name": "image1",
624 "use": [ "layers" ]
625 }
626 }"#,
627 )
628 .unwrap_err(),
629 "field `image` cannot use `layers` if field `layers` is also set",
630 );
631 }
632
633 #[test]
634 fn added_layers_after_layers_from_image() {
635 assert_eq!(
636 parse_job(
637 r#"{
638 "program": "/bin/sh",
639 "image": {
640 "name": "image1",
641 "use": [ "layers" ]
642 },
643 "added_layers": [ { "tar": "1" } ]
644 }"#
645 )
646 .unwrap()
647 .into_job_spec(layer_mapper, env, images)
648 .unwrap(),
649 JobSpec::new(
650 string!("/bin/sh"),
651 nonempty![
652 (digest!(42), ArtifactType::Tar),
653 (digest!(43), ArtifactType::Tar),
654 (digest!(1), ArtifactType::Tar)
655 ]
656 ),
657 );
658 }
659
660 #[test]
661 fn added_layers_only() {
662 assert_error(
663 parse_job(
664 r#"{
665 "program": "/bin/sh",
666 "added_layers": [ { "tar": "1" } ]
667 }"#,
668 )
669 .unwrap_err(),
670 "field `added_layers` set before `image` with a `use` of `layers`",
671 );
672 }
673
674 #[test]
675 fn added_layers_before_layers() {
676 assert_error(
677 parse_job(
678 r#"{
679 "program": "/bin/sh",
680 "added_layers": [ { "tar": "3" } ],
681 "layers": [ { "tar": "1" }, { "tar": "2" } ]
682 }"#,
683 )
684 .unwrap_err(),
685 "field `added_layers` set before `image` with a `use` of `layers`",
686 );
687 }
688
689 #[test]
690 fn added_layers_after_layers() {
691 assert_error(
692 parse_job(
693 r#"{
694 "program": "/bin/sh",
695 "layers": [ { "tar": "1" }, { "tar": "2" } ],
696 "added_layers": [ { "tar": "3" } ]
697 }"#,
698 )
699 .unwrap_err(),
700 "field `added_layers` cannot be set with `layer` field",
701 );
702 }
703
704 #[test]
705 fn added_layers_before_image_with_layers() {
706 assert_error(
707 parse_job(
708 r#"{
709 "program": "/bin/sh",
710 "added_layers": [ "3" ],
711 "image": { "name": "image1", "use": [ "layers" ] }
712 }"#,
713 )
714 .unwrap_err(),
715 "field `added_layers` set before `image` with a `use` of `layers`",
716 );
717 }
718
719 #[test]
720 fn arguments() {
721 assert_eq!(
722 parse_job(
723 r#"{
724 "program": "/bin/sh",
725 "layers": [ { "tar": "1" } ],
726 "arguments": [ "-e", "echo foo" ]
727 }"#,
728 )
729 .unwrap()
730 .into_job_spec(layer_mapper, env, images)
731 .unwrap(),
732 JobSpec::new(
733 string!("/bin/sh"),
734 nonempty![(digest!(1), ArtifactType::Tar)]
735 )
736 .arguments(["-e", "echo foo"]),
737 )
738 }
739
740 #[test]
741 fn environment() {
742 assert_eq!(
743 parse_job(
744 r#"{
745 "program": "/bin/sh",
746 "layers": [ { "tar": "1" } ],
747 "environment": { "FOO": "foo", "BAR": "bar" }
748 }"#,
749 )
750 .unwrap()
751 .into_job_spec(layer_mapper, env, images)
752 .unwrap(),
753 JobSpec::new(
754 string!("/bin/sh"),
755 nonempty![(digest!(1), ArtifactType::Tar)]
756 )
757 .environment(["BAR=bar", "FOO=foo"]),
758 )
759 }
760
761 #[test]
762 fn environment_with_env_substitution() {
763 assert_eq!(
764 parse_job(
765 r#"{
766 "program": "/bin/sh",
767 "layers": [ { "tar": "1" } ],
768 "environment": { "FOO": "pre-$env{FOO}-post", "BAR": "bar" }
769 }"#,
770 )
771 .unwrap()
772 .into_job_spec(layer_mapper, env, images)
773 .unwrap(),
774 JobSpec::new(
775 string!("/bin/sh"),
776 nonempty![(digest!(1), ArtifactType::Tar)]
777 )
778 .environment(["BAR=bar", "FOO=pre-foo-env-post"]),
779 )
780 }
781
782 #[test]
783 fn environment_with_prev_substitution() {
784 assert_eq!(
785 parse_job(
786 r#"{
787 "program": "/bin/sh",
788 "layers": [ { "tar": "1" } ],
789 "environment": { "FOO": "pre-$prev{FOO:-no-prev}-post", "BAR": "bar" }
790 }"#,
791 )
792 .unwrap()
793 .into_job_spec(layer_mapper, env, images)
794 .unwrap(),
795 JobSpec::new(
796 string!("/bin/sh"),
797 nonempty![(digest!(1), ArtifactType::Tar)]
798 )
799 .environment(["BAR=bar", "FOO=pre-no-prev-post"]),
800 )
801 }
802
803 #[test]
804 fn environment_from_image() {
805 assert_eq!(
806 parse_job(
807 r#"{
808 "program": "/bin/sh",
809 "layers": [ { "tar": "1" } ],
810 "image": { "name": "image1", "use": [ "environment" ] }
811 }"#,
812 )
813 .unwrap()
814 .into_job_spec(layer_mapper, env, images)
815 .unwrap(),
816 JobSpec::new(
817 string!("/bin/sh"),
818 nonempty![(digest!(1), ArtifactType::Tar)]
819 )
820 .environment(["BAZ=image-baz", "FOO=image-foo"]),
821 )
822 }
823
824 #[test]
825 fn environment_from_image_ignores_env_substitutions() {
826 assert_eq!(
827 parse_job(
828 r#"{
829 "program": "/bin/sh",
830 "layers": [ { "tar": "1" } ],
831 "image": { "name": "image-with-env-substitutions", "use": [ "environment" ] }
832 }"#,
833 )
834 .unwrap()
835 .into_job_spec(layer_mapper, env, images)
836 .unwrap(),
837 JobSpec::new(
838 string!("/bin/sh"),
839 nonempty![(digest!(1), ArtifactType::Tar)]
840 )
841 .environment(["PATH=$env{PATH}"]),
842 )
843 }
844
845 #[test]
846 fn environment_from_image_after_environment() {
847 assert_error(
848 parse_job(
849 r#"{
850 "program": "/bin/sh",
851 "layers": [ { "tar": "1" } ],
852 "environment": { "FOO": "foo", "BAR": "bar" },
853 "image": { "name": "image1", "use": [ "environment" ] }
854 }"#,
855 )
856 .unwrap_err(),
857 "field `image` cannot use `environment` if field `environment` is also set",
858 )
859 }
860
861 #[test]
862 fn environment_after_environment_from_image() {
863 assert_error(
864 parse_job(
865 r#"{
866 "program": "/bin/sh",
867 "layers": [ { "tar": "1" } ],
868 "image": { "name": "image1", "use": [ "environment" ] },
869 "environment": { "FOO": "foo", "BAR": "bar" }
870 }"#,
871 )
872 .unwrap_err(),
873 "field `environment` cannot be set if `image` with a `use` of `environment` is also set",
874 )
875 }
876
877 #[test]
878 fn added_environment_after_environment_from_image() {
879 assert_eq!(
880 parse_job(
881 r#"{
882 "program": "/bin/sh",
883 "layers": [ { "tar": "1" } ],
884 "image": { "name": "image1", "use": [ "environment" ] },
885 "added_environment": { "FOO": "foo", "BAR": "bar" }
886 }"#,
887 )
888 .unwrap()
889 .into_job_spec(layer_mapper, env, images)
890 .unwrap(),
891 JobSpec::new(
892 string!("/bin/sh"),
893 nonempty![(digest!(1), ArtifactType::Tar)]
894 )
895 .environment(["BAR=bar", "BAZ=image-baz", "FOO=foo"]),
896 )
897 }
898
899 #[test]
900 fn added_environment_after_environment_from_image_with_substitutions() {
901 assert_eq!(
902 parse_job(
903 r#"{
904 "program": "/bin/sh",
905 "layers": [ { "tar": "1" } ],
906 "image": { "name": "image1", "use": [ "environment" ] },
907 "added_environment": {
908 "FOO": "$env{FOO:-no-env-foo}:$prev{FOO:-no-prev-foo}",
909 "BAR": "$env{BAR:-no-env-bar}:$prev{BAR:-no-prev-bar}",
910 "BAZ": "$env{BAZ:-no-env-baz}:$prev{BAZ:-no-prev-baz}"
911 }
912 }"#,
913 )
914 .unwrap()
915 .into_job_spec(layer_mapper, env, images)
916 .unwrap(),
917 JobSpec::new(
918 string!("/bin/sh"),
919 nonempty![(digest!(1), ArtifactType::Tar)]
920 )
921 .environment([
922 "BAR=no-env-bar:no-prev-bar",
923 "BAZ=no-env-baz:image-baz",
924 "FOO=foo-env:image-foo"
925 ]),
926 )
927 }
928
929 #[test]
930 fn added_environment_without_environment_from_image() {
931 assert_error(
932 parse_job(
933 r#"{
934 "program": "/bin/sh",
935 "layers": [ { "tar": "1" } ],
936 "added_environment": { "FOO": "foo", "BAR": "bar" }
937 }"#,
938 )
939 .unwrap_err(),
940 "field `added_environment` set before `image` with a `use` of `environment`",
941 )
942 }
943
944 #[test]
945 fn added_environment_before_environment_from_image() {
946 assert_error(
947 parse_job(
948 r#"{
949 "program": "/bin/sh",
950 "layers": [ { "tar": "1" } ],
951 "added_environment": { "FOO": "foo", "BAR": "bar" },
952 "image": { "name": "image1", "use": [ "environment" ] }
953 }"#,
954 )
955 .unwrap_err(),
956 "field `added_environment` set before `image` with a `use` of `environment`",
957 )
958 }
959
960 #[test]
961 fn added_environment_before_environment() {
962 assert_error(
963 parse_job(
964 r#"{
965 "program": "/bin/sh",
966 "layers": [ { "tar": "1" } ],
967 "added_environment": { "FOO": "foo", "BAR": "bar" },
968 "environment": { "FOO": "foo", "BAR": "bar" }
969 }"#,
970 )
971 .unwrap_err(),
972 "field `added_environment` set before `image` with a `use` of `environment`",
973 )
974 }
975
976 #[test]
977 fn added_environment_after_environment() {
978 assert_error(
979 parse_job(
980 r#"{
981 "program": "/bin/sh",
982 "layers": [ { "tar": "1" } ],
983 "environment": { "FOO": "foo", "BAR": "bar" },
984 "added_environment": { "FOO": "foo", "BAR": "bar" }
985 }"#,
986 )
987 .unwrap_err(),
988 "field `added_environment` cannot be set with `environment` field",
989 )
990 }
991
992 #[test]
993 fn devices() {
994 assert_eq!(
995 parse_job(
996 r#"{
997 "program": "/bin/sh",
998 "layers": [ { "tar": "1" } ],
999 "devices": [ "null", "zero" ]
1000 }"#,
1001 )
1002 .unwrap()
1003 .into_job_spec(layer_mapper, env, images)
1004 .unwrap(),
1005 JobSpec::new(
1006 string!("/bin/sh"),
1007 nonempty![(digest!(1), ArtifactType::Tar)]
1008 )
1009 .devices(enum_set! {JobDevice::Null | JobDevice::Zero}),
1010 )
1011 }
1012
1013 #[test]
1014 fn mounts() {
1015 assert_eq!(
1016 parse_job(
1017 r#"{
1018 "program": "/bin/sh",
1019 "layers": [ { "tar": "1" } ],
1020 "mounts": [
1021 { "fs_type": "tmp", "mount_point": "/tmp" }
1022 ]
1023 }"#,
1024 )
1025 .unwrap()
1026 .into_job_spec(layer_mapper, env, images)
1027 .unwrap(),
1028 JobSpec::new(
1029 string!("/bin/sh"),
1030 nonempty![(digest!(1), ArtifactType::Tar)]
1031 )
1032 .mounts([JobMount {
1033 fs_type: JobMountFsType::Tmp,
1034 mount_point: utf8_path_buf!("/tmp"),
1035 }])
1036 )
1037 }
1038
1039 #[test]
1040 fn enable_loopback() {
1041 assert_eq!(
1042 parse_job(
1043 r#"{
1044 "program": "/bin/sh",
1045 "layers": [ { "tar": "1" } ],
1046 "enable_loopback": true
1047 }"#,
1048 )
1049 .unwrap()
1050 .into_job_spec(layer_mapper, env, images)
1051 .unwrap(),
1052 JobSpec::new(
1053 string!("/bin/sh"),
1054 nonempty![(digest!(1), ArtifactType::Tar)]
1055 )
1056 .enable_loopback(true),
1057 )
1058 }
1059
1060 #[test]
1061 fn enable_writable_file_system() {
1062 assert_eq!(
1063 parse_job(
1064 r#"{
1065 "program": "/bin/sh",
1066 "layers": [ { "tar": "1" } ],
1067 "enable_writable_file_system": true
1068 }"#,
1069 )
1070 .unwrap()
1071 .into_job_spec(layer_mapper, env, images)
1072 .unwrap(),
1073 JobSpec::new(
1074 string!("/bin/sh"),
1075 nonempty![(digest!(1), ArtifactType::Tar)]
1076 )
1077 .enable_writable_file_system(true),
1078 )
1079 }
1080
1081 #[test]
1082 fn working_directory() {
1083 assert_eq!(
1084 parse_job(
1085 r#"{
1086 "program": "/bin/sh",
1087 "layers": [ { "tar": "1" } ],
1088 "working_directory": "/foo/bar"
1089 }"#,
1090 )
1091 .unwrap()
1092 .into_job_spec(layer_mapper, env, images)
1093 .unwrap(),
1094 JobSpec::new(
1095 string!("/bin/sh"),
1096 nonempty![(digest!(1), ArtifactType::Tar)]
1097 )
1098 .working_directory("/foo/bar"),
1099 )
1100 }
1101
1102 #[test]
1103 fn working_directory_from_image() {
1104 assert_eq!(
1105 parse_job(
1106 r#"{
1107 "program": "/bin/sh",
1108 "layers": [ { "tar": "1" } ],
1109 "image": {
1110 "name": "image1",
1111 "use": [ "working_directory" ]
1112 }
1113 }"#,
1114 )
1115 .unwrap()
1116 .into_job_spec(layer_mapper, env, images)
1117 .unwrap(),
1118 JobSpec::new(
1119 string!("/bin/sh"),
1120 nonempty![(digest!(1), ArtifactType::Tar)]
1121 )
1122 .working_directory("/foo"),
1123 )
1124 }
1125
1126 #[test]
1127 fn working_directory_from_image_after_working_directory() {
1128 assert_error(
1129 parse_job(
1130 r#"{
1131 "program": "/bin/sh",
1132 "layers": [ { "tar": "1" } ],
1133 "working_directory": "/foo/bar",
1134 "image": {
1135 "name": "image1",
1136 "use": [ "working_directory" ]
1137 }
1138 }"#,
1139 )
1140 .unwrap_err(),
1141 "field `image` cannot use `working_directory` if field `working_directory` is also set",
1142 )
1143 }
1144
1145 #[test]
1146 fn working_directory_after_working_directory_from_image() {
1147 assert_error(
1148 parse_job(
1149 r#"{
1150 "program": "/bin/sh",
1151 "layers": [ { "tar": "1" } ],
1152 "image": {
1153 "name": "image1",
1154 "use": [ "working_directory" ]
1155 },
1156 "working_directory": "/foo/bar"
1157 }"#,
1158 )
1159 .unwrap_err(),
1160 "field `working_directory` cannot be set if `image` with a `use` of `working_directory` is also set",
1161 )
1162 }
1163
1164 #[test]
1165 fn user() {
1166 assert_eq!(
1167 parse_job(
1168 r#"{
1169 "program": "/bin/sh",
1170 "layers": [ { "tar": "1" } ],
1171 "user": 1234
1172 }"#,
1173 )
1174 .unwrap()
1175 .into_job_spec(layer_mapper, env, images)
1176 .unwrap(),
1177 JobSpec::new(
1178 string!("/bin/sh"),
1179 nonempty![(digest!(1), ArtifactType::Tar)]
1180 )
1181 .user(1234),
1182 )
1183 }
1184
1185 #[test]
1186 fn group() {
1187 assert_eq!(
1188 parse_job(
1189 r#"{
1190 "program": "/bin/sh",
1191 "layers": [ { "tar": "1" } ],
1192 "group": 4321
1193 }"#,
1194 )
1195 .unwrap()
1196 .into_job_spec(layer_mapper, env, images)
1197 .unwrap(),
1198 JobSpec::new(
1199 string!("/bin/sh"),
1200 nonempty![(digest!(1), ArtifactType::Tar)]
1201 )
1202 .group(4321),
1203 )
1204 }
1205}