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 pub fn from_instruction(container: &str, instruction: &Instruction) -> Vec<Self> {
655 match instruction {
656 Instruction::Run(run) => {
657 if run.mounts.is_empty() {
659 vec![Self::run(container, &run.command)]
660 } else {
661 vec![Self::run_with_mounts(container, run)]
662 }
663 }
664
665 Instruction::Copy(copy) => {
666 vec![Self::copy_instruction(container, copy)]
667 }
668
669 Instruction::Add(add) => {
670 vec![Self::add_instruction(container, add)]
671 }
672
673 Instruction::Env(env) => Self::config_envs(container, env),
674
675 Instruction::Workdir(dir) => {
676 vec![Self::config_workdir(container, dir)]
677 }
678
679 Instruction::Expose(expose) => {
680 vec![Self::config_expose(container, expose)]
681 }
682
683 Instruction::Label(labels) => Self::config_labels(container, labels),
684
685 Instruction::User(user) => {
686 vec![Self::config_user(container, user)]
687 }
688
689 Instruction::Entrypoint(cmd) => {
690 vec![Self::config_entrypoint(container, cmd)]
691 }
692
693 Instruction::Cmd(cmd) => {
694 vec![Self::config_cmd(container, cmd)]
695 }
696
697 Instruction::Volume(paths) => paths
698 .iter()
699 .map(|p| Self::config_volume(container, p))
700 .collect(),
701
702 Instruction::Shell(shell) => {
703 vec![Self::config_shell(container, shell)]
704 }
705
706 Instruction::Arg(_) => {
707 vec![]
709 }
710
711 Instruction::Stopsignal(signal) => {
712 vec![Self::config_stopsignal(container, signal)]
713 }
714
715 Instruction::Healthcheck(hc) => {
716 vec![Self::config_healthcheck(container, hc)]
717 }
718
719 Instruction::Onbuild(_) => {
720 tracing::warn!("ONBUILD instruction not supported in buildah conversion");
722 vec![]
723 }
724 }
725 }
726}
727
728fn escape_json_string(s: &str) -> String {
730 s.replace('\\', "\\\\")
731 .replace('"', "\\\"")
732 .replace('\n', "\\n")
733 .replace('\r', "\\r")
734 .replace('\t', "\\t")
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740 use crate::dockerfile::RunInstruction;
741
742 #[test]
743 fn test_from_image() {
744 let cmd = BuildahCommand::from_image("alpine:3.18");
745 assert_eq!(cmd.program, "buildah");
746 assert_eq!(cmd.args, vec!["from", "alpine:3.18"]);
747 }
748
749 #[test]
750 fn test_run_shell() {
751 let cmd = BuildahCommand::run_shell("container-1", "apt-get update");
752 assert_eq!(
753 cmd.args,
754 vec![
755 "run",
756 "container-1",
757 "--",
758 "/bin/sh",
759 "-c",
760 "apt-get update"
761 ]
762 );
763 }
764
765 #[test]
766 fn test_run_exec() {
767 let args = vec!["echo".to_string(), "hello".to_string()];
768 let cmd = BuildahCommand::run_exec("container-1", &args);
769 assert_eq!(cmd.args, vec!["run", "container-1", "--", "echo", "hello"]);
770 }
771
772 #[test]
773 fn test_copy() {
774 let sources = vec!["src/".to_string(), "Cargo.toml".to_string()];
775 let cmd = BuildahCommand::copy("container-1", &sources, "/app/");
776 assert_eq!(
777 cmd.args,
778 vec!["copy", "container-1", "src/", "Cargo.toml", "/app/"]
779 );
780 }
781
782 #[test]
783 fn test_copy_from() {
784 let sources = vec!["/app".to_string()];
785 let cmd = BuildahCommand::copy_from("container-1", "builder", &sources, "/app");
786 assert_eq!(
787 cmd.args,
788 vec!["copy", "--from", "builder", "container-1", "/app", "/app"]
789 );
790 }
791
792 #[test]
793 fn test_config_env() {
794 let cmd = BuildahCommand::config_env("container-1", "PATH", "/usr/local/bin");
795 assert_eq!(
796 cmd.args,
797 vec!["config", "--env", "PATH=/usr/local/bin", "container-1"]
798 );
799 }
800
801 #[test]
802 fn test_config_workdir() {
803 let cmd = BuildahCommand::config_workdir("container-1", "/app");
804 assert_eq!(
805 cmd.args,
806 vec!["config", "--workingdir", "/app", "container-1"]
807 );
808 }
809
810 #[test]
811 fn test_config_entrypoint_exec() {
812 let args = vec!["/app".to_string(), "--config".to_string()];
813 let cmd = BuildahCommand::config_entrypoint_exec("container-1", &args);
814 assert!(cmd.args.contains(&"--entrypoint".to_string()));
815 assert!(cmd
816 .args
817 .iter()
818 .any(|a| a.contains("[") && a.contains("/app")));
819 }
820
821 #[test]
822 fn test_commit() {
823 let cmd = BuildahCommand::commit("container-1", "myimage:latest");
824 assert_eq!(cmd.args, vec!["commit", "container-1", "myimage:latest"]);
825 }
826
827 #[test]
828 fn test_to_command_string() {
829 let cmd = BuildahCommand::config_env("container-1", "VAR", "value with spaces");
830 let s = cmd.to_command_string();
831 assert!(s.starts_with("buildah config"));
832 assert!(s.contains("VAR=value with spaces"));
833 }
834
835 #[test]
836 fn test_from_instruction_run() {
837 let instruction = Instruction::Run(RunInstruction {
838 command: ShellOrExec::Shell("echo hello".to_string()),
839 mounts: vec![],
840 network: None,
841 security: None,
842 });
843
844 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
845 assert_eq!(cmds.len(), 1);
846 assert!(cmds[0].args.contains(&"run".to_string()));
847 }
848
849 #[test]
850 fn test_from_instruction_env_multiple() {
851 let mut vars = HashMap::new();
852 vars.insert("FOO".to_string(), "bar".to_string());
853 vars.insert("BAZ".to_string(), "qux".to_string());
854
855 let instruction = Instruction::Env(EnvInstruction { vars });
856 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
857
858 assert_eq!(cmds.len(), 2);
860 for cmd in &cmds {
861 assert!(cmd.args.contains(&"config".to_string()));
862 assert!(cmd.args.contains(&"--env".to_string()));
863 }
864 }
865
866 #[test]
867 fn test_escape_json_string() {
868 assert_eq!(escape_json_string("hello"), "hello");
869 assert_eq!(escape_json_string("hello \"world\""), "hello \\\"world\\\"");
870 assert_eq!(escape_json_string("line1\nline2"), "line1\\nline2");
871 }
872
873 #[test]
874 fn test_run_with_mounts_cache() {
875 use crate::dockerfile::{CacheSharing, RunMount};
876
877 let run = RunInstruction {
878 command: ShellOrExec::Shell("apt-get update".to_string()),
879 mounts: vec![RunMount::Cache {
880 target: "/var/cache/apt".to_string(),
881 id: Some("apt-cache".to_string()),
882 sharing: CacheSharing::Shared,
883 readonly: false,
884 }],
885 network: None,
886 security: None,
887 };
888
889 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
890
891 let mount_idx = cmd
893 .args
894 .iter()
895 .position(|a| a.starts_with("--mount="))
896 .expect("should have --mount arg");
897 let container_idx = cmd
898 .args
899 .iter()
900 .position(|a| a == "container-1")
901 .expect("should have container id");
902
903 assert!(
904 mount_idx < container_idx,
905 "--mount should come before container ID"
906 );
907
908 assert!(cmd.args[mount_idx].contains("type=cache"));
910 assert!(cmd.args[mount_idx].contains("target=/var/cache/apt"));
911 assert!(cmd.args[mount_idx].contains("id=apt-cache"));
912 assert!(cmd.args[mount_idx].contains("sharing=shared"));
913 }
914
915 #[test]
916 fn test_run_with_multiple_mounts() {
917 use crate::dockerfile::{CacheSharing, RunMount};
918
919 let run = RunInstruction {
920 command: ShellOrExec::Shell("cargo build".to_string()),
921 mounts: vec![
922 RunMount::Cache {
923 target: "/usr/local/cargo/registry".to_string(),
924 id: Some("cargo-registry".to_string()),
925 sharing: CacheSharing::Shared,
926 readonly: false,
927 },
928 RunMount::Cache {
929 target: "/app/target".to_string(),
930 id: Some("cargo-target".to_string()),
931 sharing: CacheSharing::Locked,
932 readonly: false,
933 },
934 ],
935 network: None,
936 security: None,
937 };
938
939 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
940
941 let mount_count = cmd
943 .args
944 .iter()
945 .filter(|a| a.starts_with("--mount="))
946 .count();
947 assert_eq!(mount_count, 2, "should have 2 mount arguments");
948
949 let container_idx = cmd
951 .args
952 .iter()
953 .position(|a| a == "container-1")
954 .expect("should have container id");
955
956 for (idx, arg) in cmd.args.iter().enumerate() {
957 if arg.starts_with("--mount=") {
958 assert!(
959 idx < container_idx,
960 "--mount at index {} should come before container ID at {}",
961 idx,
962 container_idx
963 );
964 }
965 }
966 }
967
968 #[test]
969 fn test_from_instruction_run_with_mounts() {
970 use crate::dockerfile::{CacheSharing, RunMount};
971
972 let instruction = Instruction::Run(RunInstruction {
973 command: ShellOrExec::Shell("npm install".to_string()),
974 mounts: vec![RunMount::Cache {
975 target: "/root/.npm".to_string(),
976 id: Some("npm-cache".to_string()),
977 sharing: CacheSharing::Shared,
978 readonly: false,
979 }],
980 network: None,
981 security: None,
982 });
983
984 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
985 assert_eq!(cmds.len(), 1);
986
987 let cmd = &cmds[0];
988 assert!(
989 cmd.args.iter().any(|a| a.starts_with("--mount=")),
990 "should include --mount argument"
991 );
992 }
993
994 #[test]
995 fn test_run_with_mounts_exec_form() {
996 use crate::dockerfile::{CacheSharing, RunMount};
997
998 let run = RunInstruction {
999 command: ShellOrExec::Exec(vec![
1000 "pip".to_string(),
1001 "install".to_string(),
1002 "-r".to_string(),
1003 "requirements.txt".to_string(),
1004 ]),
1005 mounts: vec![RunMount::Cache {
1006 target: "/root/.cache/pip".to_string(),
1007 id: Some("pip-cache".to_string()),
1008 sharing: CacheSharing::Shared,
1009 readonly: false,
1010 }],
1011 network: None,
1012 security: None,
1013 };
1014
1015 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
1016
1017 assert!(cmd.args.contains(&"--".to_string()));
1019 assert!(cmd.args.contains(&"pip".to_string()));
1020 assert!(cmd.args.contains(&"install".to_string()));
1021 }
1022}