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![Self::config_workdir(container, dir)]
717 }
718
719 Instruction::Expose(expose) => {
720 vec![Self::config_expose(container, expose)]
721 }
722
723 Instruction::Label(labels) => Self::config_labels(container, labels),
724
725 Instruction::User(user) => {
726 vec![Self::config_user(container, user)]
727 }
728
729 Instruction::Entrypoint(cmd) => {
730 vec![Self::config_entrypoint(container, cmd)]
731 }
732
733 Instruction::Cmd(cmd) => {
734 vec![Self::config_cmd(container, cmd)]
735 }
736
737 Instruction::Volume(paths) => paths
738 .iter()
739 .map(|p| Self::config_volume(container, p))
740 .collect(),
741
742 Instruction::Shell(shell) => {
743 vec![Self::config_shell(container, shell)]
744 }
745
746 Instruction::Arg(_) => {
747 vec![]
749 }
750
751 Instruction::Stopsignal(signal) => {
752 vec![Self::config_stopsignal(container, signal)]
753 }
754
755 Instruction::Healthcheck(hc) => {
756 vec![Self::config_healthcheck(container, hc)]
757 }
758
759 Instruction::Onbuild(_) => {
760 tracing::warn!("ONBUILD instruction not supported in buildah conversion");
762 vec![]
763 }
764 }
765 }
766}
767
768fn escape_json_string(s: &str) -> String {
770 s.replace('\\', "\\\\")
771 .replace('"', "\\\"")
772 .replace('\n', "\\n")
773 .replace('\r', "\\r")
774 .replace('\t', "\\t")
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780 use crate::dockerfile::RunInstruction;
781
782 #[test]
783 fn test_from_image() {
784 let cmd = BuildahCommand::from_image("alpine:3.18");
785 assert_eq!(cmd.program, "buildah");
786 assert_eq!(cmd.args, vec!["from", "alpine:3.18"]);
787 }
788
789 #[test]
790 fn test_run_shell() {
791 let cmd = BuildahCommand::run_shell("container-1", "apt-get update");
792 assert_eq!(
793 cmd.args,
794 vec![
795 "run",
796 "container-1",
797 "--",
798 "/bin/sh",
799 "-c",
800 "apt-get update"
801 ]
802 );
803 }
804
805 #[test]
806 fn test_run_exec() {
807 let args = vec!["echo".to_string(), "hello".to_string()];
808 let cmd = BuildahCommand::run_exec("container-1", &args);
809 assert_eq!(cmd.args, vec!["run", "container-1", "--", "echo", "hello"]);
810 }
811
812 #[test]
813 fn test_copy() {
814 let sources = vec!["src/".to_string(), "Cargo.toml".to_string()];
815 let cmd = BuildahCommand::copy("container-1", &sources, "/app/");
816 assert_eq!(
817 cmd.args,
818 vec!["copy", "container-1", "src/", "Cargo.toml", "/app/"]
819 );
820 }
821
822 #[test]
823 fn test_copy_from() {
824 let sources = vec!["/app".to_string()];
825 let cmd = BuildahCommand::copy_from("container-1", "builder", &sources, "/app");
826 assert_eq!(
827 cmd.args,
828 vec!["copy", "--from", "builder", "container-1", "/app", "/app"]
829 );
830 }
831
832 #[test]
833 fn test_config_env() {
834 let cmd = BuildahCommand::config_env("container-1", "PATH", "/usr/local/bin");
835 assert_eq!(
836 cmd.args,
837 vec!["config", "--env", "PATH=/usr/local/bin", "container-1"]
838 );
839 }
840
841 #[test]
842 fn test_config_workdir() {
843 let cmd = BuildahCommand::config_workdir("container-1", "/app");
844 assert_eq!(
845 cmd.args,
846 vec!["config", "--workingdir", "/app", "container-1"]
847 );
848 }
849
850 #[test]
851 fn test_config_entrypoint_exec() {
852 let args = vec!["/app".to_string(), "--config".to_string()];
853 let cmd = BuildahCommand::config_entrypoint_exec("container-1", &args);
854 assert!(cmd.args.contains(&"--entrypoint".to_string()));
855 assert!(cmd
856 .args
857 .iter()
858 .any(|a| a.contains('[') && a.contains("/app")));
859 }
860
861 #[test]
862 fn test_commit() {
863 let cmd = BuildahCommand::commit("container-1", "myimage:latest");
864 assert_eq!(cmd.args, vec!["commit", "container-1", "myimage:latest"]);
865 }
866
867 #[test]
868 fn test_to_command_string() {
869 let cmd = BuildahCommand::config_env("container-1", "VAR", "value with spaces");
870 let s = cmd.to_command_string();
871 assert!(s.starts_with("buildah config"));
872 assert!(s.contains("VAR=value with spaces"));
873 }
874
875 #[test]
876 fn test_from_instruction_run() {
877 let instruction = Instruction::Run(RunInstruction {
878 command: ShellOrExec::Shell("echo hello".to_string()),
879 mounts: vec![],
880 network: None,
881 security: None,
882 });
883
884 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
885 assert_eq!(cmds.len(), 1);
886 assert!(cmds[0].args.contains(&"run".to_string()));
887 }
888
889 #[test]
890 fn test_from_instruction_env_multiple() {
891 let mut vars = HashMap::new();
892 vars.insert("FOO".to_string(), "bar".to_string());
893 vars.insert("BAZ".to_string(), "qux".to_string());
894
895 let instruction = Instruction::Env(EnvInstruction { vars });
896 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
897
898 assert_eq!(cmds.len(), 2);
900 for cmd in &cmds {
901 assert!(cmd.args.contains(&"config".to_string()));
902 assert!(cmd.args.contains(&"--env".to_string()));
903 }
904 }
905
906 #[test]
907 fn test_escape_json_string() {
908 assert_eq!(escape_json_string("hello"), "hello");
909 assert_eq!(escape_json_string("hello \"world\""), "hello \\\"world\\\"");
910 assert_eq!(escape_json_string("line1\nline2"), "line1\\nline2");
911 }
912
913 #[test]
914 fn test_run_with_mounts_cache() {
915 use crate::dockerfile::{CacheSharing, RunMount};
916
917 let run = RunInstruction {
918 command: ShellOrExec::Shell("apt-get update".to_string()),
919 mounts: vec![RunMount::Cache {
920 target: "/var/cache/apt".to_string(),
921 id: Some("apt-cache".to_string()),
922 sharing: CacheSharing::Shared,
923 readonly: false,
924 }],
925 network: None,
926 security: None,
927 };
928
929 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
930
931 let mount_idx = cmd
933 .args
934 .iter()
935 .position(|a| a.starts_with("--mount="))
936 .expect("should have --mount arg");
937 let container_idx = cmd
938 .args
939 .iter()
940 .position(|a| a == "container-1")
941 .expect("should have container id");
942
943 assert!(
944 mount_idx < container_idx,
945 "--mount should come before container ID"
946 );
947
948 assert!(cmd.args[mount_idx].contains("type=cache"));
950 assert!(cmd.args[mount_idx].contains("target=/var/cache/apt"));
951 assert!(cmd.args[mount_idx].contains("id=apt-cache"));
952 assert!(cmd.args[mount_idx].contains("sharing=shared"));
953 }
954
955 #[test]
956 fn test_run_with_multiple_mounts() {
957 use crate::dockerfile::{CacheSharing, RunMount};
958
959 let run = RunInstruction {
960 command: ShellOrExec::Shell("cargo build".to_string()),
961 mounts: vec![
962 RunMount::Cache {
963 target: "/usr/local/cargo/registry".to_string(),
964 id: Some("cargo-registry".to_string()),
965 sharing: CacheSharing::Shared,
966 readonly: false,
967 },
968 RunMount::Cache {
969 target: "/app/target".to_string(),
970 id: Some("cargo-target".to_string()),
971 sharing: CacheSharing::Locked,
972 readonly: false,
973 },
974 ],
975 network: None,
976 security: None,
977 };
978
979 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
980
981 let mount_count = cmd
983 .args
984 .iter()
985 .filter(|a| a.starts_with("--mount="))
986 .count();
987 assert_eq!(mount_count, 2, "should have 2 mount arguments");
988
989 let container_idx = cmd
991 .args
992 .iter()
993 .position(|a| a == "container-1")
994 .expect("should have container id");
995
996 for (idx, arg) in cmd.args.iter().enumerate() {
997 if arg.starts_with("--mount=") {
998 assert!(
999 idx < container_idx,
1000 "--mount at index {idx} should come before container ID at {container_idx}",
1001 );
1002 }
1003 }
1004 }
1005
1006 #[test]
1007 fn test_from_instruction_run_with_mounts() {
1008 use crate::dockerfile::{CacheSharing, RunMount};
1009
1010 let instruction = Instruction::Run(RunInstruction {
1011 command: ShellOrExec::Shell("npm install".to_string()),
1012 mounts: vec![RunMount::Cache {
1013 target: "/root/.npm".to_string(),
1014 id: Some("npm-cache".to_string()),
1015 sharing: CacheSharing::Shared,
1016 readonly: false,
1017 }],
1018 network: None,
1019 security: None,
1020 });
1021
1022 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
1023 assert_eq!(cmds.len(), 1);
1024
1025 let cmd = &cmds[0];
1026 assert!(
1027 cmd.args.iter().any(|a| a.starts_with("--mount=")),
1028 "should include --mount argument"
1029 );
1030 }
1031
1032 #[test]
1033 fn test_run_with_mounts_exec_form() {
1034 use crate::dockerfile::{CacheSharing, RunMount};
1035
1036 let run = RunInstruction {
1037 command: ShellOrExec::Exec(vec![
1038 "pip".to_string(),
1039 "install".to_string(),
1040 "-r".to_string(),
1041 "requirements.txt".to_string(),
1042 ]),
1043 mounts: vec![RunMount::Cache {
1044 target: "/root/.cache/pip".to_string(),
1045 id: Some("pip-cache".to_string()),
1046 sharing: CacheSharing::Shared,
1047 readonly: false,
1048 }],
1049 network: None,
1050 security: None,
1051 };
1052
1053 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
1054
1055 assert!(cmd.args.contains(&"--".to_string()));
1057 assert!(cmd.args.contains(&"pip".to_string()));
1058 assert!(cmd.args.contains(&"install".to_string()));
1059 }
1060
1061 #[test]
1062 fn test_manifest_create() {
1063 let cmd = BuildahCommand::manifest_create("myapp:latest");
1064 assert_eq!(cmd.program, "buildah");
1065 assert_eq!(cmd.args, vec!["manifest", "create", "myapp:latest"]);
1066 }
1067
1068 #[test]
1069 fn test_manifest_add() {
1070 let cmd = BuildahCommand::manifest_add("myapp:latest", "myapp-amd64:latest");
1071 assert_eq!(
1072 cmd.args,
1073 vec!["manifest", "add", "myapp:latest", "myapp-amd64:latest"]
1074 );
1075 }
1076
1077 #[test]
1078 fn test_manifest_push() {
1079 let cmd =
1080 BuildahCommand::manifest_push("myapp:latest", "docker://registry.example.com/myapp");
1081 assert_eq!(
1082 cmd.args,
1083 vec![
1084 "manifest",
1085 "push",
1086 "--all",
1087 "myapp:latest",
1088 "docker://registry.example.com/myapp"
1089 ]
1090 );
1091 }
1092
1093 #[test]
1094 fn test_manifest_rm() {
1095 let cmd = BuildahCommand::manifest_rm("myapp:latest");
1096 assert_eq!(cmd.args, vec!["manifest", "rm", "myapp:latest"]);
1097 }
1098}