1mod executor;
26mod install;
27
28pub use executor::*;
29pub use install::{
30 current_platform, install_instructions, is_platform_supported, BuildahInstallation,
31 BuildahInstaller, InstallError,
32};
33
34use crate::dockerfile::{
35 AddInstruction, CopyInstruction, EnvInstruction, ExposeInstruction, HealthcheckInstruction,
36 Instruction, RunInstruction, ShellOrExec,
37};
38
39use std::collections::HashMap;
40
41#[derive(Debug, Clone)]
43pub struct BuildahCommand {
44 pub program: String,
46
47 pub args: Vec<String>,
49
50 pub env: HashMap<String, String>,
52}
53
54impl BuildahCommand {
55 #[must_use]
57 pub fn new(subcommand: &str) -> Self {
58 Self {
59 program: "buildah".to_string(),
60 args: vec![subcommand.to_string()],
61 env: HashMap::new(),
62 }
63 }
64
65 #[must_use]
67 pub fn arg(mut self, arg: impl Into<String>) -> Self {
68 self.args.push(arg.into());
69 self
70 }
71
72 #[must_use]
74 pub fn args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
75 self.args.extend(args.into_iter().map(Into::into));
76 self
77 }
78
79 #[must_use]
81 pub fn arg_opt(self, flag: &str, value: Option<impl Into<String>>) -> Self {
82 if let Some(v) = value {
83 self.arg(flag).arg(v)
84 } else {
85 self
86 }
87 }
88
89 #[must_use]
91 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
92 self.env.insert(key.into(), value.into());
93 self
94 }
95
96 #[must_use]
98 pub fn to_command_string(&self) -> String {
99 let mut parts = vec![self.program.clone()];
100 parts.extend(self.args.iter().map(|a| {
101 if a.contains(' ') || a.contains('"') {
102 format!("\"{}\"", a.replace('"', "\\\""))
103 } else {
104 a.clone()
105 }
106 }));
107 parts.join(" ")
108 }
109
110 #[must_use]
118 pub fn from_image(image: &str) -> Self {
119 Self::new("from").arg(image)
120 }
121
122 #[must_use]
126 pub fn from_image_named(image: &str, name: &str) -> Self {
127 Self::new("from").arg("--name").arg(name).arg(image)
128 }
129
130 #[must_use]
134 pub fn from_scratch() -> Self {
135 Self::new("from").arg("scratch")
136 }
137
138 #[must_use]
142 pub fn rm(container: &str) -> Self {
143 Self::new("rm").arg(container)
144 }
145
146 #[must_use]
150 pub fn commit(container: &str, image_name: &str) -> Self {
151 Self::new("commit").arg(container).arg(image_name)
152 }
153
154 #[must_use]
156 pub fn commit_with_opts(
157 container: &str,
158 image_name: &str,
159 format: Option<&str>,
160 squash: bool,
161 ) -> Self {
162 let mut cmd = Self::new("commit");
163
164 if let Some(fmt) = format {
165 cmd = cmd.arg("--format").arg(fmt);
166 }
167
168 if squash {
169 cmd = cmd.arg("--squash");
170 }
171
172 cmd.arg(container).arg(image_name)
173 }
174
175 #[must_use]
179 pub fn tag(image: &str, new_name: &str) -> Self {
180 Self::new("tag").arg(image).arg(new_name)
181 }
182
183 #[must_use]
187 pub fn rmi(image: &str) -> Self {
188 Self::new("rmi").arg(image)
189 }
190
191 #[must_use]
195 pub fn push(image: &str) -> Self {
196 Self::new("push").arg(image)
197 }
198
199 #[must_use]
203 pub fn push_to(image: &str, destination: &str) -> Self {
204 Self::new("push").arg(image).arg(destination)
205 }
206
207 #[must_use]
211 pub fn inspect(name: &str) -> Self {
212 Self::new("inspect").arg(name)
213 }
214
215 #[must_use]
219 pub fn inspect_format(name: &str, format: &str) -> Self {
220 Self::new("inspect").arg("--format").arg(format).arg(name)
221 }
222
223 #[must_use]
227 pub fn images() -> Self {
228 Self::new("images")
229 }
230
231 #[must_use]
235 pub fn containers() -> Self {
236 Self::new("containers")
237 }
238
239 #[must_use]
247 pub fn run_shell(container: &str, command: &str) -> Self {
248 Self::new("run")
249 .arg(container)
250 .arg("--")
251 .arg("/bin/sh")
252 .arg("-c")
253 .arg(command)
254 }
255
256 #[must_use]
260 pub fn run_exec(container: &str, args: &[String]) -> Self {
261 let mut cmd = Self::new("run").arg(container).arg("--");
262 for arg in args {
263 cmd = cmd.arg(arg);
264 }
265 cmd
266 }
267
268 #[must_use]
270 pub fn run(container: &str, command: &ShellOrExec) -> Self {
271 match command {
272 ShellOrExec::Shell(s) => Self::run_shell(container, s),
273 ShellOrExec::Exec(args) => Self::run_exec(container, args),
274 }
275 }
276
277 #[must_use]
284 pub fn run_with_mounts(container: &str, run: &RunInstruction) -> Self {
285 let mut cmd = Self::new("run");
286
287 for mount in &run.mounts {
289 cmd = cmd.arg(format!("--mount={}", mount.to_buildah_arg()));
290 }
291
292 cmd = cmd.arg(container).arg("--");
294
295 match &run.command {
296 ShellOrExec::Shell(s) => cmd.arg("/bin/sh").arg("-c").arg(s),
297 ShellOrExec::Exec(args) => {
298 for arg in args {
299 cmd = cmd.arg(arg);
300 }
301 cmd
302 }
303 }
304 }
305
306 #[must_use]
314 pub fn copy(container: &str, sources: &[String], dest: &str) -> Self {
315 let mut cmd = Self::new("copy").arg(container);
316 for src in sources {
317 cmd = cmd.arg(src);
318 }
319 cmd.arg(dest)
320 }
321
322 #[must_use]
326 pub fn copy_from(container: &str, from: &str, sources: &[String], dest: &str) -> Self {
327 let mut cmd = Self::new("copy").arg("--from").arg(from).arg(container);
328 for src in sources {
329 cmd = cmd.arg(src);
330 }
331 cmd.arg(dest)
332 }
333
334 #[must_use]
336 pub fn copy_instruction(container: &str, copy: &CopyInstruction) -> Self {
337 let mut cmd = Self::new("copy");
338
339 if let Some(ref from) = copy.from {
340 cmd = cmd.arg("--from").arg(from);
341 }
342
343 if let Some(ref chown) = copy.chown {
344 cmd = cmd.arg("--chown").arg(chown);
345 }
346
347 if let Some(ref chmod) = copy.chmod {
348 cmd = cmd.arg("--chmod").arg(chmod);
349 }
350
351 cmd = cmd.arg(container);
352
353 for src in ©.sources {
354 cmd = cmd.arg(src);
355 }
356
357 cmd.arg(©.destination)
358 }
359
360 #[must_use]
362 pub fn add(container: &str, sources: &[String], dest: &str) -> Self {
363 let mut cmd = Self::new("add").arg(container);
364 for src in sources {
365 cmd = cmd.arg(src);
366 }
367 cmd.arg(dest)
368 }
369
370 #[must_use]
372 pub fn add_instruction(container: &str, add: &AddInstruction) -> Self {
373 let mut cmd = Self::new("add");
374
375 if let Some(ref chown) = add.chown {
376 cmd = cmd.arg("--chown").arg(chown);
377 }
378
379 if let Some(ref chmod) = add.chmod {
380 cmd = cmd.arg("--chmod").arg(chmod);
381 }
382
383 cmd = cmd.arg(container);
384
385 for src in &add.sources {
386 cmd = cmd.arg(src);
387 }
388
389 cmd.arg(&add.destination)
390 }
391
392 #[must_use]
400 pub fn config_env(container: &str, key: &str, value: &str) -> Self {
401 Self::new("config")
402 .arg("--env")
403 .arg(format!("{key}={value}"))
404 .arg(container)
405 }
406
407 #[must_use]
409 pub fn config_envs(container: &str, env: &EnvInstruction) -> Vec<Self> {
410 env.vars
411 .iter()
412 .map(|(k, v)| Self::config_env(container, k, v))
413 .collect()
414 }
415
416 #[must_use]
420 pub fn config_workdir(container: &str, dir: &str) -> Self {
421 Self::new("config")
422 .arg("--workingdir")
423 .arg(dir)
424 .arg(container)
425 }
426
427 #[must_use]
431 pub fn config_expose(container: &str, expose: &ExposeInstruction) -> Self {
432 let port_spec = format!(
433 "{}/{}",
434 expose.port,
435 match expose.protocol {
436 crate::dockerfile::ExposeProtocol::Tcp => "tcp",
437 crate::dockerfile::ExposeProtocol::Udp => "udp",
438 }
439 );
440 Self::new("config")
441 .arg("--port")
442 .arg(port_spec)
443 .arg(container)
444 }
445
446 #[must_use]
450 pub fn config_entrypoint_shell(container: &str, command: &str) -> Self {
451 Self::new("config")
452 .arg("--entrypoint")
453 .arg(format!(
454 "[\"/bin/sh\", \"-c\", \"{}\"]",
455 escape_json_string(command)
456 ))
457 .arg(container)
458 }
459
460 #[must_use]
464 pub fn config_entrypoint_exec(container: &str, args: &[String]) -> Self {
465 let json_array = format!(
466 "[{}]",
467 args.iter()
468 .map(|a| format!("\"{}\"", escape_json_string(a)))
469 .collect::<Vec<_>>()
470 .join(", ")
471 );
472 Self::new("config")
473 .arg("--entrypoint")
474 .arg(json_array)
475 .arg(container)
476 }
477
478 #[must_use]
480 pub fn config_entrypoint(container: &str, command: &ShellOrExec) -> Self {
481 match command {
482 ShellOrExec::Shell(s) => Self::config_entrypoint_shell(container, s),
483 ShellOrExec::Exec(args) => Self::config_entrypoint_exec(container, args),
484 }
485 }
486
487 #[must_use]
489 pub fn config_cmd_shell(container: &str, command: &str) -> Self {
490 Self::new("config")
491 .arg("--cmd")
492 .arg(format!("/bin/sh -c \"{}\"", escape_json_string(command)))
493 .arg(container)
494 }
495
496 #[must_use]
498 pub fn config_cmd_exec(container: &str, args: &[String]) -> Self {
499 let json_array = format!(
500 "[{}]",
501 args.iter()
502 .map(|a| format!("\"{}\"", escape_json_string(a)))
503 .collect::<Vec<_>>()
504 .join(", ")
505 );
506 Self::new("config")
507 .arg("--cmd")
508 .arg(json_array)
509 .arg(container)
510 }
511
512 #[must_use]
514 pub fn config_cmd(container: &str, command: &ShellOrExec) -> Self {
515 match command {
516 ShellOrExec::Shell(s) => Self::config_cmd_shell(container, s),
517 ShellOrExec::Exec(args) => Self::config_cmd_exec(container, args),
518 }
519 }
520
521 #[must_use]
525 pub fn config_user(container: &str, user: &str) -> Self {
526 Self::new("config").arg("--user").arg(user).arg(container)
527 }
528
529 #[must_use]
533 pub fn config_label(container: &str, key: &str, value: &str) -> Self {
534 Self::new("config")
535 .arg("--label")
536 .arg(format!("{key}={value}"))
537 .arg(container)
538 }
539
540 #[must_use]
542 pub fn config_labels(container: &str, labels: &HashMap<String, String>) -> Vec<Self> {
543 labels
544 .iter()
545 .map(|(k, v)| Self::config_label(container, k, v))
546 .collect()
547 }
548
549 #[must_use]
553 pub fn config_volume(container: &str, path: &str) -> Self {
554 Self::new("config").arg("--volume").arg(path).arg(container)
555 }
556
557 #[must_use]
561 pub fn config_stopsignal(container: &str, signal: &str) -> Self {
562 Self::new("config")
563 .arg("--stop-signal")
564 .arg(signal)
565 .arg(container)
566 }
567
568 #[must_use]
572 pub fn config_shell(container: &str, shell: &[String]) -> Self {
573 let json_array = format!(
574 "[{}]",
575 shell
576 .iter()
577 .map(|a| format!("\"{}\"", escape_json_string(a)))
578 .collect::<Vec<_>>()
579 .join(", ")
580 );
581 Self::new("config")
582 .arg("--shell")
583 .arg(json_array)
584 .arg(container)
585 }
586
587 #[must_use]
589 pub fn config_healthcheck(container: &str, healthcheck: &HealthcheckInstruction) -> Self {
590 match healthcheck {
591 HealthcheckInstruction::None => Self::new("config")
592 .arg("--healthcheck")
593 .arg("NONE")
594 .arg(container),
595 HealthcheckInstruction::Check {
596 command,
597 interval,
598 timeout,
599 start_period,
600 retries,
601 ..
602 } => {
603 let mut cmd = Self::new("config");
604
605 let cmd_str = match command {
606 ShellOrExec::Shell(s) => format!("CMD {s}"),
607 ShellOrExec::Exec(args) => {
608 format!(
609 "CMD [{}]",
610 args.iter()
611 .map(|a| format!("\"{}\"", escape_json_string(a)))
612 .collect::<Vec<_>>()
613 .join(", ")
614 )
615 }
616 };
617
618 cmd = cmd.arg("--healthcheck").arg(cmd_str);
619
620 if let Some(i) = interval {
621 cmd = cmd
622 .arg("--healthcheck-interval")
623 .arg(format!("{}s", i.as_secs()));
624 }
625
626 if let Some(t) = timeout {
627 cmd = cmd
628 .arg("--healthcheck-timeout")
629 .arg(format!("{}s", t.as_secs()));
630 }
631
632 if let Some(sp) = start_period {
633 cmd = cmd
634 .arg("--healthcheck-start-period")
635 .arg(format!("{}s", sp.as_secs()));
636 }
637
638 if let Some(r) = retries {
639 cmd = cmd.arg("--healthcheck-retries").arg(r.to_string());
640 }
641
642 cmd.arg(container)
643 }
644 }
645 }
646
647 #[must_use]
655 pub fn manifest_create(name: &str) -> Self {
656 Self::new("manifest").arg("create").arg(name)
657 }
658
659 #[must_use]
663 pub fn manifest_add(list: &str, image: &str) -> Self {
664 Self::new("manifest").arg("add").arg(list).arg(image)
665 }
666
667 #[must_use]
671 pub fn manifest_push(list: &str, destination: &str) -> Self {
672 Self::new("manifest")
673 .arg("push")
674 .arg("--all")
675 .arg(list)
676 .arg(destination)
677 }
678
679 #[must_use]
683 pub fn manifest_rm(list: &str) -> Self {
684 Self::new("manifest").arg("rm").arg(list)
685 }
686
687 pub fn from_instruction(container: &str, instruction: &Instruction) -> Vec<Self> {
695 match instruction {
696 Instruction::Run(run) => {
697 if run.mounts.is_empty() {
699 vec![Self::run(container, &run.command)]
700 } else {
701 vec![Self::run_with_mounts(container, run)]
702 }
703 }
704
705 Instruction::Copy(copy) => {
706 vec![Self::copy_instruction(container, copy)]
707 }
708
709 Instruction::Add(add) => {
710 vec![Self::add_instruction(container, add)]
711 }
712
713 Instruction::Env(env) => Self::config_envs(container, env),
714
715 Instruction::Workdir(dir) => {
716 vec![
724 Self::run_exec(
725 container,
726 &["mkdir".to_string(), "-p".to_string(), dir.clone()],
727 ),
728 Self::config_workdir(container, dir),
729 ]
730 }
731
732 Instruction::Expose(expose) => {
733 vec![Self::config_expose(container, expose)]
734 }
735
736 Instruction::Label(labels) => Self::config_labels(container, labels),
737
738 Instruction::User(user) => {
739 vec![Self::config_user(container, user)]
740 }
741
742 Instruction::Entrypoint(cmd) => {
743 vec![Self::config_entrypoint(container, cmd)]
744 }
745
746 Instruction::Cmd(cmd) => {
747 vec![Self::config_cmd(container, cmd)]
748 }
749
750 Instruction::Volume(paths) => paths
751 .iter()
752 .map(|p| Self::config_volume(container, p))
753 .collect(),
754
755 Instruction::Shell(shell) => {
756 vec![Self::config_shell(container, shell)]
757 }
758
759 Instruction::Arg(_) => {
760 vec![]
762 }
763
764 Instruction::Stopsignal(signal) => {
765 vec![Self::config_stopsignal(container, signal)]
766 }
767
768 Instruction::Healthcheck(hc) => {
769 vec![Self::config_healthcheck(container, hc)]
770 }
771
772 Instruction::Onbuild(_) => {
773 tracing::warn!("ONBUILD instruction not supported in buildah conversion");
775 vec![]
776 }
777 }
778 }
779}
780
781fn escape_json_string(s: &str) -> String {
783 s.replace('\\', "\\\\")
784 .replace('"', "\\\"")
785 .replace('\n', "\\n")
786 .replace('\r', "\\r")
787 .replace('\t', "\\t")
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793 use crate::dockerfile::RunInstruction;
794
795 #[test]
796 fn test_from_image() {
797 let cmd = BuildahCommand::from_image("alpine:3.18");
798 assert_eq!(cmd.program, "buildah");
799 assert_eq!(cmd.args, vec!["from", "alpine:3.18"]);
800 }
801
802 #[test]
803 fn test_run_shell() {
804 let cmd = BuildahCommand::run_shell("container-1", "apt-get update");
805 assert_eq!(
806 cmd.args,
807 vec![
808 "run",
809 "container-1",
810 "--",
811 "/bin/sh",
812 "-c",
813 "apt-get update"
814 ]
815 );
816 }
817
818 #[test]
819 fn test_run_exec() {
820 let args = vec!["echo".to_string(), "hello".to_string()];
821 let cmd = BuildahCommand::run_exec("container-1", &args);
822 assert_eq!(cmd.args, vec!["run", "container-1", "--", "echo", "hello"]);
823 }
824
825 #[test]
826 fn test_copy() {
827 let sources = vec!["src/".to_string(), "Cargo.toml".to_string()];
828 let cmd = BuildahCommand::copy("container-1", &sources, "/app/");
829 assert_eq!(
830 cmd.args,
831 vec!["copy", "container-1", "src/", "Cargo.toml", "/app/"]
832 );
833 }
834
835 #[test]
836 fn test_copy_from() {
837 let sources = vec!["/app".to_string()];
838 let cmd = BuildahCommand::copy_from("container-1", "builder", &sources, "/app");
839 assert_eq!(
840 cmd.args,
841 vec!["copy", "--from", "builder", "container-1", "/app", "/app"]
842 );
843 }
844
845 #[test]
846 fn test_config_env() {
847 let cmd = BuildahCommand::config_env("container-1", "PATH", "/usr/local/bin");
848 assert_eq!(
849 cmd.args,
850 vec!["config", "--env", "PATH=/usr/local/bin", "container-1"]
851 );
852 }
853
854 #[test]
855 fn test_config_workdir() {
856 let cmd = BuildahCommand::config_workdir("container-1", "/app");
857 assert_eq!(
858 cmd.args,
859 vec!["config", "--workingdir", "/app", "container-1"]
860 );
861 }
862
863 #[test]
864 fn test_config_entrypoint_exec() {
865 let args = vec!["/app".to_string(), "--config".to_string()];
866 let cmd = BuildahCommand::config_entrypoint_exec("container-1", &args);
867 assert!(cmd.args.contains(&"--entrypoint".to_string()));
868 assert!(cmd
869 .args
870 .iter()
871 .any(|a| a.contains('[') && a.contains("/app")));
872 }
873
874 #[test]
875 fn test_commit() {
876 let cmd = BuildahCommand::commit("container-1", "myimage:latest");
877 assert_eq!(cmd.args, vec!["commit", "container-1", "myimage:latest"]);
878 }
879
880 #[test]
881 fn test_to_command_string() {
882 let cmd = BuildahCommand::config_env("container-1", "VAR", "value with spaces");
883 let s = cmd.to_command_string();
884 assert!(s.starts_with("buildah config"));
885 assert!(s.contains("VAR=value with spaces"));
886 }
887
888 #[test]
889 fn test_from_instruction_run() {
890 let instruction = Instruction::Run(RunInstruction {
891 command: ShellOrExec::Shell("echo hello".to_string()),
892 mounts: vec![],
893 network: None,
894 security: None,
895 });
896
897 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
898 assert_eq!(cmds.len(), 1);
899 assert!(cmds[0].args.contains(&"run".to_string()));
900 }
901
902 #[test]
903 fn test_from_instruction_workdir_creates_and_configures() {
904 let instruction = Instruction::Workdir("/workspace".to_string());
908 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
909
910 assert_eq!(cmds.len(), 2, "WORKDIR should emit mkdir + config");
911
912 let run_args = &cmds[0].args;
913 assert_eq!(run_args[0], "run");
914 assert_eq!(run_args[1], "container-1");
915 assert_eq!(run_args[2], "--");
916 assert_eq!(run_args[3], "mkdir");
917 assert_eq!(run_args[4], "-p");
918 assert_eq!(run_args[5], "/workspace");
919
920 assert_eq!(
921 cmds[1].args,
922 vec!["config", "--workingdir", "/workspace", "container-1"]
923 );
924 }
925
926 #[test]
927 fn test_from_instruction_env_multiple() {
928 let mut vars = HashMap::new();
929 vars.insert("FOO".to_string(), "bar".to_string());
930 vars.insert("BAZ".to_string(), "qux".to_string());
931
932 let instruction = Instruction::Env(EnvInstruction { vars });
933 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
934
935 assert_eq!(cmds.len(), 2);
937 for cmd in &cmds {
938 assert!(cmd.args.contains(&"config".to_string()));
939 assert!(cmd.args.contains(&"--env".to_string()));
940 }
941 }
942
943 #[test]
944 fn test_escape_json_string() {
945 assert_eq!(escape_json_string("hello"), "hello");
946 assert_eq!(escape_json_string("hello \"world\""), "hello \\\"world\\\"");
947 assert_eq!(escape_json_string("line1\nline2"), "line1\\nline2");
948 }
949
950 #[test]
951 fn test_run_with_mounts_cache() {
952 use crate::dockerfile::{CacheSharing, RunMount};
953
954 let run = RunInstruction {
955 command: ShellOrExec::Shell("apt-get update".to_string()),
956 mounts: vec![RunMount::Cache {
957 target: "/var/cache/apt".to_string(),
958 id: Some("apt-cache".to_string()),
959 sharing: CacheSharing::Shared,
960 readonly: false,
961 }],
962 network: None,
963 security: None,
964 };
965
966 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
967
968 let mount_idx = cmd
970 .args
971 .iter()
972 .position(|a| a.starts_with("--mount="))
973 .expect("should have --mount arg");
974 let container_idx = cmd
975 .args
976 .iter()
977 .position(|a| a == "container-1")
978 .expect("should have container id");
979
980 assert!(
981 mount_idx < container_idx,
982 "--mount should come before container ID"
983 );
984
985 assert!(cmd.args[mount_idx].contains("type=cache"));
987 assert!(cmd.args[mount_idx].contains("target=/var/cache/apt"));
988 assert!(cmd.args[mount_idx].contains("id=apt-cache"));
989 assert!(cmd.args[mount_idx].contains("sharing=shared"));
990 }
991
992 #[test]
993 fn test_run_with_multiple_mounts() {
994 use crate::dockerfile::{CacheSharing, RunMount};
995
996 let run = RunInstruction {
997 command: ShellOrExec::Shell("cargo build".to_string()),
998 mounts: vec![
999 RunMount::Cache {
1000 target: "/usr/local/cargo/registry".to_string(),
1001 id: Some("cargo-registry".to_string()),
1002 sharing: CacheSharing::Shared,
1003 readonly: false,
1004 },
1005 RunMount::Cache {
1006 target: "/app/target".to_string(),
1007 id: Some("cargo-target".to_string()),
1008 sharing: CacheSharing::Locked,
1009 readonly: false,
1010 },
1011 ],
1012 network: None,
1013 security: None,
1014 };
1015
1016 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
1017
1018 let mount_count = cmd
1020 .args
1021 .iter()
1022 .filter(|a| a.starts_with("--mount="))
1023 .count();
1024 assert_eq!(mount_count, 2, "should have 2 mount arguments");
1025
1026 let container_idx = cmd
1028 .args
1029 .iter()
1030 .position(|a| a == "container-1")
1031 .expect("should have container id");
1032
1033 for (idx, arg) in cmd.args.iter().enumerate() {
1034 if arg.starts_with("--mount=") {
1035 assert!(
1036 idx < container_idx,
1037 "--mount at index {idx} should come before container ID at {container_idx}",
1038 );
1039 }
1040 }
1041 }
1042
1043 #[test]
1044 fn test_from_instruction_run_with_mounts() {
1045 use crate::dockerfile::{CacheSharing, RunMount};
1046
1047 let instruction = Instruction::Run(RunInstruction {
1048 command: ShellOrExec::Shell("npm install".to_string()),
1049 mounts: vec![RunMount::Cache {
1050 target: "/root/.npm".to_string(),
1051 id: Some("npm-cache".to_string()),
1052 sharing: CacheSharing::Shared,
1053 readonly: false,
1054 }],
1055 network: None,
1056 security: None,
1057 });
1058
1059 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
1060 assert_eq!(cmds.len(), 1);
1061
1062 let cmd = &cmds[0];
1063 assert!(
1064 cmd.args.iter().any(|a| a.starts_with("--mount=")),
1065 "should include --mount argument"
1066 );
1067 }
1068
1069 #[test]
1070 fn test_run_with_mounts_exec_form() {
1071 use crate::dockerfile::{CacheSharing, RunMount};
1072
1073 let run = RunInstruction {
1074 command: ShellOrExec::Exec(vec![
1075 "pip".to_string(),
1076 "install".to_string(),
1077 "-r".to_string(),
1078 "requirements.txt".to_string(),
1079 ]),
1080 mounts: vec![RunMount::Cache {
1081 target: "/root/.cache/pip".to_string(),
1082 id: Some("pip-cache".to_string()),
1083 sharing: CacheSharing::Shared,
1084 readonly: false,
1085 }],
1086 network: None,
1087 security: None,
1088 };
1089
1090 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
1091
1092 assert!(cmd.args.contains(&"--".to_string()));
1094 assert!(cmd.args.contains(&"pip".to_string()));
1095 assert!(cmd.args.contains(&"install".to_string()));
1096 }
1097
1098 #[test]
1099 fn test_manifest_create() {
1100 let cmd = BuildahCommand::manifest_create("myapp:latest");
1101 assert_eq!(cmd.program, "buildah");
1102 assert_eq!(cmd.args, vec!["manifest", "create", "myapp:latest"]);
1103 }
1104
1105 #[test]
1106 fn test_manifest_add() {
1107 let cmd = BuildahCommand::manifest_add("myapp:latest", "myapp-amd64:latest");
1108 assert_eq!(
1109 cmd.args,
1110 vec!["manifest", "add", "myapp:latest", "myapp-amd64:latest"]
1111 );
1112 }
1113
1114 #[test]
1115 fn test_manifest_push() {
1116 let cmd =
1117 BuildahCommand::manifest_push("myapp:latest", "docker://registry.example.com/myapp");
1118 assert_eq!(
1119 cmd.args,
1120 vec![
1121 "manifest",
1122 "push",
1123 "--all",
1124 "myapp:latest",
1125 "docker://registry.example.com/myapp"
1126 ]
1127 );
1128 }
1129
1130 #[test]
1131 fn test_manifest_rm() {
1132 let cmd = BuildahCommand::manifest_rm("myapp:latest");
1133 assert_eq!(cmd.args, vec!["manifest", "rm", "myapp:latest"]);
1134 }
1135}