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::backend::ImageOs;
35use crate::dockerfile::{
36 AddInstruction, CopyInstruction, EnvInstruction, ExposeInstruction, HealthcheckInstruction,
37 Instruction, RunInstruction, ShellOrExec,
38};
39
40use std::collections::HashMap;
41
42const LINUX_DEFAULT_SHELL: &[&str] = &["/bin/sh", "-c"];
48
49const WINDOWS_DEFAULT_SHELL: &[&str] = &["cmd.exe", "/S", "/C"];
54
55fn default_shell_for(os: ImageOs) -> Vec<String> {
58 let raw: &[&str] = match os {
59 ImageOs::Linux => LINUX_DEFAULT_SHELL,
60 ImageOs::Windows => WINDOWS_DEFAULT_SHELL,
61 };
62 raw.iter().map(|s| (*s).to_string()).collect()
63}
64
65#[derive(Debug, Clone)]
67pub struct BuildahCommand {
68 pub program: String,
70
71 pub args: Vec<String>,
73
74 pub env: HashMap<String, String>,
76}
77
78impl BuildahCommand {
79 #[must_use]
81 pub fn new(subcommand: &str) -> Self {
82 Self {
83 program: "buildah".to_string(),
84 args: vec![subcommand.to_string()],
85 env: HashMap::new(),
86 }
87 }
88
89 #[must_use]
91 pub fn arg(mut self, arg: impl Into<String>) -> Self {
92 self.args.push(arg.into());
93 self
94 }
95
96 #[must_use]
98 pub fn args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
99 self.args.extend(args.into_iter().map(Into::into));
100 self
101 }
102
103 #[must_use]
105 pub fn arg_opt(self, flag: &str, value: Option<impl Into<String>>) -> Self {
106 if let Some(v) = value {
107 self.arg(flag).arg(v)
108 } else {
109 self
110 }
111 }
112
113 #[must_use]
115 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
116 self.env.insert(key.into(), value.into());
117 self
118 }
119
120 #[must_use]
122 pub fn to_command_string(&self) -> String {
123 let mut parts = vec![self.program.clone()];
124 parts.extend(self.args.iter().map(|a| {
125 if a.contains(' ') || a.contains('"') {
126 format!("\"{}\"", a.replace('"', "\\\""))
127 } else {
128 a.clone()
129 }
130 }));
131 parts.join(" ")
132 }
133
134 #[must_use]
142 pub fn from_image(image: &str) -> Self {
143 Self::new("from").arg(image)
144 }
145
146 #[must_use]
150 pub fn from_image_named(image: &str, name: &str) -> Self {
151 Self::new("from").arg("--name").arg(name).arg(image)
152 }
153
154 #[must_use]
158 pub fn from_scratch() -> Self {
159 Self::new("from").arg("scratch")
160 }
161
162 #[must_use]
166 pub fn rm(container: &str) -> Self {
167 Self::new("rm").arg(container)
168 }
169
170 #[must_use]
174 pub fn commit(container: &str, image_name: &str) -> Self {
175 Self::new("commit").arg(container).arg(image_name)
176 }
177
178 #[must_use]
180 pub fn commit_with_opts(
181 container: &str,
182 image_name: &str,
183 format: Option<&str>,
184 squash: bool,
185 ) -> Self {
186 let mut cmd = Self::new("commit");
187
188 if let Some(fmt) = format {
189 cmd = cmd.arg("--format").arg(fmt);
190 }
191
192 if squash {
193 cmd = cmd.arg("--squash");
194 }
195
196 cmd.arg(container).arg(image_name)
197 }
198
199 #[must_use]
203 pub fn tag(image: &str, new_name: &str) -> Self {
204 Self::new("tag").arg(image).arg(new_name)
205 }
206
207 #[must_use]
211 pub fn rmi(image: &str) -> Self {
212 Self::new("rmi").arg(image)
213 }
214
215 #[must_use]
219 pub fn push(image: &str) -> Self {
220 Self::new("push").arg(image)
221 }
222
223 #[must_use]
227 pub fn push_to(image: &str, destination: &str) -> Self {
228 Self::new("push").arg(image).arg(destination)
229 }
230
231 #[must_use]
235 pub fn inspect(name: &str) -> Self {
236 Self::new("inspect").arg(name)
237 }
238
239 #[must_use]
243 pub fn inspect_format(name: &str, format: &str) -> Self {
244 Self::new("inspect").arg("--format").arg(format).arg(name)
245 }
246
247 #[must_use]
251 pub fn images() -> Self {
252 Self::new("images")
253 }
254
255 #[must_use]
259 pub fn containers() -> Self {
260 Self::new("containers")
261 }
262
263 #[must_use]
275 pub fn run_shell(container: &str, command: &str) -> Self {
276 Self::run_shell_custom(container, LINUX_DEFAULT_SHELL, command)
277 }
278
279 #[must_use]
287 pub fn run_shell_custom(
288 container: &str,
289 shell: impl IntoIterator<Item = impl AsRef<str>>,
290 command: &str,
291 ) -> Self {
292 let mut cmd = Self::new("run").arg(container).arg("--");
293 for s in shell {
294 cmd = cmd.arg(s.as_ref().to_string());
295 }
296 cmd.arg(command)
297 }
298
299 #[must_use]
303 pub fn run_shell_for_os(container: &str, command: &str, os: ImageOs) -> Self {
304 let shell = default_shell_for(os);
305 Self::run_shell_custom(container, &shell, command)
306 }
307
308 #[must_use]
312 pub fn run_exec(container: &str, args: &[String]) -> Self {
313 let mut cmd = Self::new("run").arg(container).arg("--");
314 for arg in args {
315 cmd = cmd.arg(arg);
316 }
317 cmd
318 }
319
320 #[must_use]
322 pub fn run(container: &str, command: &ShellOrExec) -> Self {
323 match command {
324 ShellOrExec::Shell(s) => Self::run_shell(container, s),
325 ShellOrExec::Exec(args) => Self::run_exec(container, args),
326 }
327 }
328
329 #[must_use]
339 pub fn run_with_mounts(container: &str, run: &RunInstruction) -> Self {
340 Self::run_with_mounts_shell(container, run, LINUX_DEFAULT_SHELL)
341 }
342
343 #[must_use]
349 pub fn run_with_mounts_shell(
350 container: &str,
351 run: &RunInstruction,
352 shell: impl IntoIterator<Item = impl AsRef<str>>,
353 ) -> Self {
354 let mut cmd = Self::new("run");
355
356 for mount in &run.mounts {
358 cmd = cmd.arg(format!("--mount={}", mount.to_buildah_arg()));
359 }
360
361 cmd = cmd.arg(container).arg("--");
363
364 match &run.command {
365 ShellOrExec::Shell(s) => {
366 for part in shell {
367 cmd = cmd.arg(part.as_ref().to_string());
368 }
369 cmd.arg(s)
370 }
371 ShellOrExec::Exec(args) => {
372 for arg in args {
373 cmd = cmd.arg(arg);
374 }
375 cmd
376 }
377 }
378 }
379
380 #[must_use]
388 pub fn copy(container: &str, sources: &[String], dest: &str) -> Self {
389 let mut cmd = Self::new("copy").arg(container);
390 for src in sources {
391 cmd = cmd.arg(src);
392 }
393 cmd.arg(dest)
394 }
395
396 #[must_use]
400 pub fn copy_from(container: &str, from: &str, sources: &[String], dest: &str) -> Self {
401 let mut cmd = Self::new("copy").arg("--from").arg(from).arg(container);
402 for src in sources {
403 cmd = cmd.arg(src);
404 }
405 cmd.arg(dest)
406 }
407
408 #[must_use]
410 pub fn copy_instruction(container: &str, copy: &CopyInstruction) -> Self {
411 let mut cmd = Self::new("copy");
412
413 if let Some(ref from) = copy.from {
414 cmd = cmd.arg("--from").arg(from);
415 }
416
417 if let Some(ref chown) = copy.chown {
418 cmd = cmd.arg("--chown").arg(chown);
419 }
420
421 if let Some(ref chmod) = copy.chmod {
422 cmd = cmd.arg("--chmod").arg(chmod);
423 }
424
425 cmd = cmd.arg(container);
426
427 for src in ©.sources {
428 cmd = cmd.arg(src);
429 }
430
431 cmd.arg(©.destination)
432 }
433
434 #[must_use]
436 pub fn add(container: &str, sources: &[String], dest: &str) -> Self {
437 let mut cmd = Self::new("add").arg(container);
438 for src in sources {
439 cmd = cmd.arg(src);
440 }
441 cmd.arg(dest)
442 }
443
444 #[must_use]
446 pub fn add_instruction(container: &str, add: &AddInstruction) -> Self {
447 let mut cmd = Self::new("add");
448
449 if let Some(ref chown) = add.chown {
450 cmd = cmd.arg("--chown").arg(chown);
451 }
452
453 if let Some(ref chmod) = add.chmod {
454 cmd = cmd.arg("--chmod").arg(chmod);
455 }
456
457 cmd = cmd.arg(container);
458
459 for src in &add.sources {
460 cmd = cmd.arg(src);
461 }
462
463 cmd.arg(&add.destination)
464 }
465
466 #[must_use]
474 pub fn config_env(container: &str, key: &str, value: &str) -> Self {
475 Self::new("config")
476 .arg("--env")
477 .arg(format!("{key}={value}"))
478 .arg(container)
479 }
480
481 #[must_use]
483 pub fn config_envs(container: &str, env: &EnvInstruction) -> Vec<Self> {
484 env.vars
485 .iter()
486 .map(|(k, v)| Self::config_env(container, k, v))
487 .collect()
488 }
489
490 #[must_use]
494 pub fn config_workdir(container: &str, dir: &str) -> Self {
495 Self::new("config")
496 .arg("--workingdir")
497 .arg(dir)
498 .arg(container)
499 }
500
501 #[must_use]
505 pub fn config_expose(container: &str, expose: &ExposeInstruction) -> Self {
506 let port_spec = format!(
507 "{}/{}",
508 expose.port,
509 match expose.protocol {
510 crate::dockerfile::ExposeProtocol::Tcp => "tcp",
511 crate::dockerfile::ExposeProtocol::Udp => "udp",
512 }
513 );
514 Self::new("config")
515 .arg("--port")
516 .arg(port_spec)
517 .arg(container)
518 }
519
520 #[must_use]
524 pub fn config_entrypoint_shell(container: &str, command: &str) -> Self {
525 Self::new("config")
526 .arg("--entrypoint")
527 .arg(format!(
528 "[\"/bin/sh\", \"-c\", \"{}\"]",
529 escape_json_string(command)
530 ))
531 .arg(container)
532 }
533
534 #[must_use]
538 pub fn config_entrypoint_exec(container: &str, args: &[String]) -> Self {
539 let json_array = format!(
540 "[{}]",
541 args.iter()
542 .map(|a| format!("\"{}\"", escape_json_string(a)))
543 .collect::<Vec<_>>()
544 .join(", ")
545 );
546 Self::new("config")
547 .arg("--entrypoint")
548 .arg(json_array)
549 .arg(container)
550 }
551
552 #[must_use]
554 pub fn config_entrypoint(container: &str, command: &ShellOrExec) -> Self {
555 match command {
556 ShellOrExec::Shell(s) => Self::config_entrypoint_shell(container, s),
557 ShellOrExec::Exec(args) => Self::config_entrypoint_exec(container, args),
558 }
559 }
560
561 #[must_use]
563 pub fn config_cmd_shell(container: &str, command: &str) -> Self {
564 Self::new("config")
565 .arg("--cmd")
566 .arg(format!("/bin/sh -c \"{}\"", escape_json_string(command)))
567 .arg(container)
568 }
569
570 #[must_use]
572 pub fn config_cmd_exec(container: &str, args: &[String]) -> Self {
573 let json_array = format!(
574 "[{}]",
575 args.iter()
576 .map(|a| format!("\"{}\"", escape_json_string(a)))
577 .collect::<Vec<_>>()
578 .join(", ")
579 );
580 Self::new("config")
581 .arg("--cmd")
582 .arg(json_array)
583 .arg(container)
584 }
585
586 #[must_use]
588 pub fn config_cmd(container: &str, command: &ShellOrExec) -> Self {
589 match command {
590 ShellOrExec::Shell(s) => Self::config_cmd_shell(container, s),
591 ShellOrExec::Exec(args) => Self::config_cmd_exec(container, args),
592 }
593 }
594
595 #[must_use]
599 pub fn config_user(container: &str, user: &str) -> Self {
600 Self::new("config").arg("--user").arg(user).arg(container)
601 }
602
603 #[must_use]
607 pub fn config_label(container: &str, key: &str, value: &str) -> Self {
608 Self::new("config")
609 .arg("--label")
610 .arg(format!("{key}={value}"))
611 .arg(container)
612 }
613
614 #[must_use]
616 pub fn config_labels(container: &str, labels: &HashMap<String, String>) -> Vec<Self> {
617 labels
618 .iter()
619 .map(|(k, v)| Self::config_label(container, k, v))
620 .collect()
621 }
622
623 #[must_use]
627 pub fn config_volume(container: &str, path: &str) -> Self {
628 Self::new("config").arg("--volume").arg(path).arg(container)
629 }
630
631 #[must_use]
635 pub fn config_stopsignal(container: &str, signal: &str) -> Self {
636 Self::new("config")
637 .arg("--stop-signal")
638 .arg(signal)
639 .arg(container)
640 }
641
642 #[must_use]
646 pub fn config_shell(container: &str, shell: &[String]) -> Self {
647 let json_array = format!(
648 "[{}]",
649 shell
650 .iter()
651 .map(|a| format!("\"{}\"", escape_json_string(a)))
652 .collect::<Vec<_>>()
653 .join(", ")
654 );
655 Self::new("config")
656 .arg("--shell")
657 .arg(json_array)
658 .arg(container)
659 }
660
661 #[must_use]
663 pub fn config_healthcheck(container: &str, healthcheck: &HealthcheckInstruction) -> Self {
664 match healthcheck {
665 HealthcheckInstruction::None => Self::new("config")
666 .arg("--healthcheck")
667 .arg("NONE")
668 .arg(container),
669 HealthcheckInstruction::Check {
670 command,
671 interval,
672 timeout,
673 start_period,
674 retries,
675 ..
676 } => {
677 let mut cmd = Self::new("config");
678
679 let cmd_str = match command {
680 ShellOrExec::Shell(s) => format!("CMD {s}"),
681 ShellOrExec::Exec(args) => {
682 format!(
683 "CMD [{}]",
684 args.iter()
685 .map(|a| format!("\"{}\"", escape_json_string(a)))
686 .collect::<Vec<_>>()
687 .join(", ")
688 )
689 }
690 };
691
692 cmd = cmd.arg("--healthcheck").arg(cmd_str);
693
694 if let Some(i) = interval {
695 cmd = cmd
696 .arg("--healthcheck-interval")
697 .arg(format!("{}s", i.as_secs()));
698 }
699
700 if let Some(t) = timeout {
701 cmd = cmd
702 .arg("--healthcheck-timeout")
703 .arg(format!("{}s", t.as_secs()));
704 }
705
706 if let Some(sp) = start_period {
707 cmd = cmd
708 .arg("--healthcheck-start-period")
709 .arg(format!("{}s", sp.as_secs()));
710 }
711
712 if let Some(r) = retries {
713 cmd = cmd.arg("--healthcheck-retries").arg(r.to_string());
714 }
715
716 cmd.arg(container)
717 }
718 }
719 }
720
721 #[must_use]
729 pub fn manifest_create(name: &str) -> Self {
730 Self::new("manifest").arg("create").arg(name)
731 }
732
733 #[must_use]
737 pub fn manifest_add(list: &str, image: &str) -> Self {
738 Self::new("manifest").arg("add").arg(list).arg(image)
739 }
740
741 #[must_use]
745 pub fn manifest_push(list: &str, destination: &str) -> Self {
746 Self::new("manifest")
747 .arg("push")
748 .arg("--all")
749 .arg(list)
750 .arg(destination)
751 }
752
753 #[must_use]
757 pub fn manifest_rm(list: &str) -> Self {
758 Self::new("manifest").arg("rm").arg(list)
759 }
760
761 #[must_use]
781 pub fn from_instruction(container: &str, instruction: &Instruction) -> Vec<Self> {
782 DockerfileTranslator::new(ImageOs::Linux).translate(container, instruction)
783 }
784}
785
786#[derive(Debug, Clone)]
807pub struct DockerfileTranslator {
808 target_os: ImageOs,
809 shell_override: Option<Vec<String>>,
812}
813
814impl DockerfileTranslator {
815 #[must_use]
817 pub fn new(target_os: ImageOs) -> Self {
818 Self {
819 target_os,
820 shell_override: None,
821 }
822 }
823
824 #[must_use]
826 pub fn target_os(&self) -> ImageOs {
827 self.target_os
828 }
829
830 #[must_use]
834 pub fn active_shell(&self) -> Vec<String> {
835 match &self.shell_override {
836 Some(s) => s.clone(),
837 None => default_shell_for(self.target_os),
838 }
839 }
840
841 pub fn set_shell_override(&mut self, shell: Vec<String>) {
845 self.shell_override = Some(shell);
846 }
847
848 #[allow(clippy::too_many_lines)]
855 pub fn translate(&mut self, container: &str, instruction: &Instruction) -> Vec<BuildahCommand> {
856 match instruction {
857 Instruction::Run(run) => {
858 let shell = self.active_shell();
859 if run.mounts.is_empty() {
860 match &run.command {
861 ShellOrExec::Shell(s) => {
862 vec![BuildahCommand::run_shell_custom(container, &shell, s)]
863 }
864 ShellOrExec::Exec(args) => vec![BuildahCommand::run_exec(container, args)],
865 }
866 } else {
867 vec![BuildahCommand::run_with_mounts_shell(
868 container, run, &shell,
869 )]
870 }
871 }
872
873 Instruction::Copy(copy) => {
874 vec![BuildahCommand::copy_instruction(container, copy)]
875 }
876
877 Instruction::Add(add) => {
878 vec![BuildahCommand::add_instruction(container, add)]
879 }
880
881 Instruction::Env(env) => BuildahCommand::config_envs(container, env),
882
883 Instruction::Workdir(dir) => self.translate_workdir(container, dir),
884
885 Instruction::Expose(expose) => {
886 vec![BuildahCommand::config_expose(container, expose)]
887 }
888
889 Instruction::Label(labels) => BuildahCommand::config_labels(container, labels),
890
891 Instruction::User(user) => {
892 vec![BuildahCommand::config_user(container, user)]
893 }
894
895 Instruction::Entrypoint(cmd) => {
896 vec![BuildahCommand::config_entrypoint(container, cmd)]
897 }
898
899 Instruction::Cmd(cmd) => {
900 vec![BuildahCommand::config_cmd(container, cmd)]
901 }
902
903 Instruction::Volume(paths) => paths
904 .iter()
905 .map(|p| BuildahCommand::config_volume(container, p))
906 .collect(),
907
908 Instruction::Shell(shell) => {
909 self.set_shell_override(shell.clone());
916 vec![BuildahCommand::config_shell(container, shell)]
917 }
918
919 Instruction::Arg(_) => {
920 vec![]
922 }
923
924 Instruction::Stopsignal(signal) => {
925 vec![BuildahCommand::config_stopsignal(container, signal)]
926 }
927
928 Instruction::Healthcheck(hc) => {
929 vec![BuildahCommand::config_healthcheck(container, hc)]
930 }
931
932 Instruction::Onbuild(_) => {
933 tracing::warn!("ONBUILD instruction not supported in buildah conversion");
935 vec![]
936 }
937 }
938 }
939
940 fn translate_workdir(&self, container: &str, dir: &str) -> Vec<BuildahCommand> {
957 match self.target_os {
958 ImageOs::Linux => {
959 vec![
960 BuildahCommand::run_exec(
961 container,
962 &["mkdir".to_string(), "-p".to_string(), dir.to_string()],
963 ),
964 BuildahCommand::config_workdir(container, dir),
965 ]
966 }
967 ImageOs::Windows => {
968 let guarded = format!(r#"if not exist "{dir}" mkdir "{dir}""#);
973 vec![
974 BuildahCommand::run_shell_custom(container, WINDOWS_DEFAULT_SHELL, &guarded),
975 BuildahCommand::config_workdir(container, dir),
976 ]
977 }
978 }
979 }
980}
981
982fn escape_json_string(s: &str) -> String {
984 s.replace('\\', "\\\\")
985 .replace('"', "\\\"")
986 .replace('\n', "\\n")
987 .replace('\r', "\\r")
988 .replace('\t', "\\t")
989}
990
991#[cfg(test)]
992mod tests {
993 use super::*;
994 use crate::dockerfile::RunInstruction;
995
996 #[test]
997 fn test_from_image() {
998 let cmd = BuildahCommand::from_image("alpine:3.18");
999 assert_eq!(cmd.program, "buildah");
1000 assert_eq!(cmd.args, vec!["from", "alpine:3.18"]);
1001 }
1002
1003 #[test]
1004 fn test_run_shell() {
1005 let cmd = BuildahCommand::run_shell("container-1", "apt-get update");
1006 assert_eq!(
1007 cmd.args,
1008 vec![
1009 "run",
1010 "container-1",
1011 "--",
1012 "/bin/sh",
1013 "-c",
1014 "apt-get update"
1015 ]
1016 );
1017 }
1018
1019 #[test]
1020 fn test_run_exec() {
1021 let args = vec!["echo".to_string(), "hello".to_string()];
1022 let cmd = BuildahCommand::run_exec("container-1", &args);
1023 assert_eq!(cmd.args, vec!["run", "container-1", "--", "echo", "hello"]);
1024 }
1025
1026 #[test]
1027 fn test_copy() {
1028 let sources = vec!["src/".to_string(), "Cargo.toml".to_string()];
1029 let cmd = BuildahCommand::copy("container-1", &sources, "/app/");
1030 assert_eq!(
1031 cmd.args,
1032 vec!["copy", "container-1", "src/", "Cargo.toml", "/app/"]
1033 );
1034 }
1035
1036 #[test]
1037 fn test_copy_from() {
1038 let sources = vec!["/app".to_string()];
1039 let cmd = BuildahCommand::copy_from("container-1", "builder", &sources, "/app");
1040 assert_eq!(
1041 cmd.args,
1042 vec!["copy", "--from", "builder", "container-1", "/app", "/app"]
1043 );
1044 }
1045
1046 #[test]
1047 fn test_config_env() {
1048 let cmd = BuildahCommand::config_env("container-1", "PATH", "/usr/local/bin");
1049 assert_eq!(
1050 cmd.args,
1051 vec!["config", "--env", "PATH=/usr/local/bin", "container-1"]
1052 );
1053 }
1054
1055 #[test]
1056 fn test_config_workdir() {
1057 let cmd = BuildahCommand::config_workdir("container-1", "/app");
1058 assert_eq!(
1059 cmd.args,
1060 vec!["config", "--workingdir", "/app", "container-1"]
1061 );
1062 }
1063
1064 #[test]
1065 fn test_config_entrypoint_exec() {
1066 let args = vec!["/app".to_string(), "--config".to_string()];
1067 let cmd = BuildahCommand::config_entrypoint_exec("container-1", &args);
1068 assert!(cmd.args.contains(&"--entrypoint".to_string()));
1069 assert!(cmd
1070 .args
1071 .iter()
1072 .any(|a| a.contains('[') && a.contains("/app")));
1073 }
1074
1075 #[test]
1076 fn test_commit() {
1077 let cmd = BuildahCommand::commit("container-1", "myimage:latest");
1078 assert_eq!(cmd.args, vec!["commit", "container-1", "myimage:latest"]);
1079 }
1080
1081 #[test]
1082 fn test_to_command_string() {
1083 let cmd = BuildahCommand::config_env("container-1", "VAR", "value with spaces");
1084 let s = cmd.to_command_string();
1085 assert!(s.starts_with("buildah config"));
1086 assert!(s.contains("VAR=value with spaces"));
1087 }
1088
1089 #[test]
1090 fn test_from_instruction_run() {
1091 let instruction = Instruction::Run(RunInstruction {
1092 command: ShellOrExec::Shell("echo hello".to_string()),
1093 mounts: vec![],
1094 network: None,
1095 security: None,
1096 });
1097
1098 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
1099 assert_eq!(cmds.len(), 1);
1100 assert!(cmds[0].args.contains(&"run".to_string()));
1101 }
1102
1103 #[test]
1104 fn test_from_instruction_workdir_creates_and_configures() {
1105 let instruction = Instruction::Workdir("/workspace".to_string());
1109 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
1110
1111 assert_eq!(cmds.len(), 2, "WORKDIR should emit mkdir + config");
1112
1113 let run_args = &cmds[0].args;
1114 assert_eq!(run_args[0], "run");
1115 assert_eq!(run_args[1], "container-1");
1116 assert_eq!(run_args[2], "--");
1117 assert_eq!(run_args[3], "mkdir");
1118 assert_eq!(run_args[4], "-p");
1119 assert_eq!(run_args[5], "/workspace");
1120
1121 assert_eq!(
1122 cmds[1].args,
1123 vec!["config", "--workingdir", "/workspace", "container-1"]
1124 );
1125 }
1126
1127 #[test]
1128 fn test_from_instruction_env_multiple() {
1129 let mut vars = HashMap::new();
1130 vars.insert("FOO".to_string(), "bar".to_string());
1131 vars.insert("BAZ".to_string(), "qux".to_string());
1132
1133 let instruction = Instruction::Env(EnvInstruction { vars });
1134 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
1135
1136 assert_eq!(cmds.len(), 2);
1138 for cmd in &cmds {
1139 assert!(cmd.args.contains(&"config".to_string()));
1140 assert!(cmd.args.contains(&"--env".to_string()));
1141 }
1142 }
1143
1144 #[test]
1145 fn test_escape_json_string() {
1146 assert_eq!(escape_json_string("hello"), "hello");
1147 assert_eq!(escape_json_string("hello \"world\""), "hello \\\"world\\\"");
1148 assert_eq!(escape_json_string("line1\nline2"), "line1\\nline2");
1149 }
1150
1151 #[test]
1152 fn test_run_with_mounts_cache() {
1153 use crate::dockerfile::{CacheSharing, RunMount};
1154
1155 let run = RunInstruction {
1156 command: ShellOrExec::Shell("apt-get update".to_string()),
1157 mounts: vec![RunMount::Cache {
1158 target: "/var/cache/apt".to_string(),
1159 id: Some("apt-cache".to_string()),
1160 sharing: CacheSharing::Shared,
1161 readonly: false,
1162 }],
1163 network: None,
1164 security: None,
1165 };
1166
1167 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
1168
1169 let mount_idx = cmd
1171 .args
1172 .iter()
1173 .position(|a| a.starts_with("--mount="))
1174 .expect("should have --mount arg");
1175 let container_idx = cmd
1176 .args
1177 .iter()
1178 .position(|a| a == "container-1")
1179 .expect("should have container id");
1180
1181 assert!(
1182 mount_idx < container_idx,
1183 "--mount should come before container ID"
1184 );
1185
1186 assert!(cmd.args[mount_idx].contains("type=cache"));
1188 assert!(cmd.args[mount_idx].contains("target=/var/cache/apt"));
1189 assert!(cmd.args[mount_idx].contains("id=apt-cache"));
1190 assert!(cmd.args[mount_idx].contains("sharing=shared"));
1191 }
1192
1193 #[test]
1194 fn test_run_with_multiple_mounts() {
1195 use crate::dockerfile::{CacheSharing, RunMount};
1196
1197 let run = RunInstruction {
1198 command: ShellOrExec::Shell("cargo build".to_string()),
1199 mounts: vec![
1200 RunMount::Cache {
1201 target: "/usr/local/cargo/registry".to_string(),
1202 id: Some("cargo-registry".to_string()),
1203 sharing: CacheSharing::Shared,
1204 readonly: false,
1205 },
1206 RunMount::Cache {
1207 target: "/app/target".to_string(),
1208 id: Some("cargo-target".to_string()),
1209 sharing: CacheSharing::Locked,
1210 readonly: false,
1211 },
1212 ],
1213 network: None,
1214 security: None,
1215 };
1216
1217 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
1218
1219 let mount_count = cmd
1221 .args
1222 .iter()
1223 .filter(|a| a.starts_with("--mount="))
1224 .count();
1225 assert_eq!(mount_count, 2, "should have 2 mount arguments");
1226
1227 let container_idx = cmd
1229 .args
1230 .iter()
1231 .position(|a| a == "container-1")
1232 .expect("should have container id");
1233
1234 for (idx, arg) in cmd.args.iter().enumerate() {
1235 if arg.starts_with("--mount=") {
1236 assert!(
1237 idx < container_idx,
1238 "--mount at index {idx} should come before container ID at {container_idx}",
1239 );
1240 }
1241 }
1242 }
1243
1244 #[test]
1245 fn test_from_instruction_run_with_mounts() {
1246 use crate::dockerfile::{CacheSharing, RunMount};
1247
1248 let instruction = Instruction::Run(RunInstruction {
1249 command: ShellOrExec::Shell("npm install".to_string()),
1250 mounts: vec![RunMount::Cache {
1251 target: "/root/.npm".to_string(),
1252 id: Some("npm-cache".to_string()),
1253 sharing: CacheSharing::Shared,
1254 readonly: false,
1255 }],
1256 network: None,
1257 security: None,
1258 });
1259
1260 let cmds = BuildahCommand::from_instruction("container-1", &instruction);
1261 assert_eq!(cmds.len(), 1);
1262
1263 let cmd = &cmds[0];
1264 assert!(
1265 cmd.args.iter().any(|a| a.starts_with("--mount=")),
1266 "should include --mount argument"
1267 );
1268 }
1269
1270 #[test]
1271 fn test_run_with_mounts_exec_form() {
1272 use crate::dockerfile::{CacheSharing, RunMount};
1273
1274 let run = RunInstruction {
1275 command: ShellOrExec::Exec(vec![
1276 "pip".to_string(),
1277 "install".to_string(),
1278 "-r".to_string(),
1279 "requirements.txt".to_string(),
1280 ]),
1281 mounts: vec![RunMount::Cache {
1282 target: "/root/.cache/pip".to_string(),
1283 id: Some("pip-cache".to_string()),
1284 sharing: CacheSharing::Shared,
1285 readonly: false,
1286 }],
1287 network: None,
1288 security: None,
1289 };
1290
1291 let cmd = BuildahCommand::run_with_mounts("container-1", &run);
1292
1293 assert!(cmd.args.contains(&"--".to_string()));
1295 assert!(cmd.args.contains(&"pip".to_string()));
1296 assert!(cmd.args.contains(&"install".to_string()));
1297 }
1298
1299 #[test]
1300 fn test_manifest_create() {
1301 let cmd = BuildahCommand::manifest_create("myapp:latest");
1302 assert_eq!(cmd.program, "buildah");
1303 assert_eq!(cmd.args, vec!["manifest", "create", "myapp:latest"]);
1304 }
1305
1306 #[test]
1307 fn test_manifest_add() {
1308 let cmd = BuildahCommand::manifest_add("myapp:latest", "myapp-amd64:latest");
1309 assert_eq!(
1310 cmd.args,
1311 vec!["manifest", "add", "myapp:latest", "myapp-amd64:latest"]
1312 );
1313 }
1314
1315 #[test]
1316 fn test_manifest_push() {
1317 let cmd =
1318 BuildahCommand::manifest_push("myapp:latest", "docker://registry.example.com/myapp");
1319 assert_eq!(
1320 cmd.args,
1321 vec![
1322 "manifest",
1323 "push",
1324 "--all",
1325 "myapp:latest",
1326 "docker://registry.example.com/myapp"
1327 ]
1328 );
1329 }
1330
1331 #[test]
1332 fn test_manifest_rm() {
1333 let cmd = BuildahCommand::manifest_rm("myapp:latest");
1334 assert_eq!(cmd.args, vec!["manifest", "rm", "myapp:latest"]);
1335 }
1336
1337 #[test]
1342 fn test_run_shell_for_os_linux() {
1343 let cmd = BuildahCommand::run_shell_for_os("c1", "echo hello", ImageOs::Linux);
1344 assert_eq!(
1345 cmd.args,
1346 vec!["run", "c1", "--", "/bin/sh", "-c", "echo hello"]
1347 );
1348 }
1349
1350 #[test]
1351 fn test_run_shell_for_os_windows() {
1352 let cmd = BuildahCommand::run_shell_for_os("c1", "echo hello", ImageOs::Windows);
1353 assert_eq!(
1354 cmd.args,
1355 vec!["run", "c1", "--", "cmd.exe", "/S", "/C", "echo hello"]
1356 );
1357 }
1358
1359 #[test]
1360 fn test_run_shell_custom_powershell() {
1361 let shell = ["powershell", "-Command"];
1362 let cmd = BuildahCommand::run_shell_custom("c1", shell, "Get-Process");
1363 assert_eq!(
1364 cmd.args,
1365 vec!["run", "c1", "--", "powershell", "-Command", "Get-Process"]
1366 );
1367 }
1368
1369 #[test]
1370 fn test_translator_linux_run_shell_default() {
1371 let mut t = DockerfileTranslator::new(ImageOs::Linux);
1372 let instr = Instruction::Run(RunInstruction::shell("apt-get update"));
1373 let cmds = t.translate("c1", &instr);
1374 assert_eq!(cmds.len(), 1);
1375 assert_eq!(
1376 cmds[0].args,
1377 vec!["run", "c1", "--", "/bin/sh", "-c", "apt-get update"]
1378 );
1379 }
1380
1381 #[test]
1382 fn test_translator_windows_run_shell_default() {
1383 let mut t = DockerfileTranslator::new(ImageOs::Windows);
1384 let instr = Instruction::Run(RunInstruction::shell("dir C:\\"));
1385 let cmds = t.translate("c1", &instr);
1386 assert_eq!(cmds.len(), 1);
1387 assert_eq!(
1388 cmds[0].args,
1389 vec!["run", "c1", "--", "cmd.exe", "/S", "/C", "dir C:\\"]
1390 );
1391 }
1392
1393 #[test]
1394 fn test_translator_shell_override_linux_bash() {
1395 let mut t = DockerfileTranslator::new(ImageOs::Linux);
1397
1398 let shell_instr = Instruction::Shell(vec!["/bin/bash".to_string(), "-lc".to_string()]);
1399 let shell_cmds = t.translate("c1", &shell_instr);
1400 assert_eq!(shell_cmds.len(), 1);
1402 assert!(shell_cmds[0].args.contains(&"--shell".to_string()));
1403
1404 let run_instr = Instruction::Run(RunInstruction::shell("set -e; echo $SHELL"));
1405 let run_cmds = t.translate("c1", &run_instr);
1406 assert_eq!(run_cmds.len(), 1);
1407 assert_eq!(
1408 run_cmds[0].args,
1409 vec!["run", "c1", "--", "/bin/bash", "-lc", "set -e; echo $SHELL"]
1410 );
1411 }
1412
1413 #[test]
1414 fn test_translator_shell_override_windows_powershell() {
1415 let mut t = DockerfileTranslator::new(ImageOs::Windows);
1418
1419 let shell_instr =
1420 Instruction::Shell(vec!["powershell".to_string(), "-Command".to_string()]);
1421 t.translate("c1", &shell_instr);
1422
1423 let run_instr = Instruction::Run(RunInstruction::shell("Get-Process"));
1424 let run_cmds = t.translate("c1", &run_instr);
1425 assert_eq!(run_cmds.len(), 1);
1426 assert_eq!(
1427 run_cmds[0].args,
1428 vec!["run", "c1", "--", "powershell", "-Command", "Get-Process"]
1429 );
1430 }
1431
1432 #[test]
1433 fn test_translator_shell_override_persists_across_runs() {
1434 let mut t = DockerfileTranslator::new(ImageOs::Linux);
1436 t.translate(
1437 "c1",
1438 &Instruction::Shell(vec!["/bin/bash".to_string(), "-c".to_string()]),
1439 );
1440
1441 for _ in 0..2 {
1442 let cmds = t.translate("c1", &Instruction::Run(RunInstruction::shell("echo hi")));
1443 assert_eq!(
1444 cmds[0].args,
1445 vec!["run", "c1", "--", "/bin/bash", "-c", "echo hi"]
1446 );
1447 }
1448 }
1449
1450 #[test]
1451 fn test_translator_exec_form_ignores_shell_override() {
1452 let mut t = DockerfileTranslator::new(ImageOs::Windows);
1455 t.translate(
1456 "c1",
1457 &Instruction::Shell(vec!["powershell".to_string(), "-Command".to_string()]),
1458 );
1459
1460 let run = Instruction::Run(RunInstruction::exec(vec![
1461 "myapp.exe".to_string(),
1462 "--flag".to_string(),
1463 ]));
1464 let cmds = t.translate("c1", &run);
1465 assert_eq!(cmds[0].args, vec!["run", "c1", "--", "myapp.exe", "--flag"]);
1466 }
1467
1468 #[test]
1469 fn test_translator_workdir_linux() {
1470 let mut t = DockerfileTranslator::new(ImageOs::Linux);
1471 let cmds = t.translate("c1", &Instruction::Workdir("/app".to_string()));
1472 assert_eq!(cmds.len(), 2);
1473 assert_eq!(cmds[0].args, vec!["run", "c1", "--", "mkdir", "-p", "/app"]);
1474 assert_eq!(cmds[1].args, vec!["config", "--workingdir", "/app", "c1"]);
1475 }
1476
1477 #[test]
1478 fn test_translator_workdir_windows() {
1479 let mut t = DockerfileTranslator::new(ImageOs::Windows);
1480 let cmds = t.translate("c1", &Instruction::Workdir("C:\\app".to_string()));
1481 assert_eq!(cmds.len(), 2);
1482 assert_eq!(
1485 cmds[0].args,
1486 vec![
1487 "run",
1488 "c1",
1489 "--",
1490 "cmd.exe",
1491 "/S",
1492 "/C",
1493 r#"if not exist "C:\app" mkdir "C:\app""#
1494 ]
1495 );
1496 assert_eq!(
1497 cmds[1].args,
1498 vec!["config", "--workingdir", "C:\\app", "c1"]
1499 );
1500 }
1501
1502 #[test]
1503 fn test_translator_workdir_windows_path_with_spaces() {
1504 let mut t = DockerfileTranslator::new(ImageOs::Windows);
1508 let cmds = t.translate(
1509 "c1",
1510 &Instruction::Workdir("C:\\Program Files\\app".to_string()),
1511 );
1512 assert_eq!(cmds.len(), 2);
1513 let mkdir_cmd = &cmds[0].args[6];
1514 assert_eq!(
1515 mkdir_cmd,
1516 r#"if not exist "C:\Program Files\app" mkdir "C:\Program Files\app""#
1517 );
1518 }
1519
1520 #[test]
1521 fn test_from_instruction_preserves_linux_byte_identical_output() {
1522 let run = Instruction::Run(RunInstruction::shell("echo hello"));
1527 let legacy = BuildahCommand::from_instruction("c1", &run);
1528 let via_translator = DockerfileTranslator::new(ImageOs::Linux).translate("c1", &run);
1529 assert_eq!(legacy.len(), via_translator.len());
1530 for (a, b) in legacy.iter().zip(via_translator.iter()) {
1531 assert_eq!(a.args, b.args);
1532 assert_eq!(a.program, b.program);
1533 }
1534
1535 let workdir = Instruction::Workdir("/workspace".to_string());
1537 let legacy = BuildahCommand::from_instruction("c1", &workdir);
1538 assert_eq!(legacy.len(), 2);
1539 assert_eq!(
1540 legacy[0].args,
1541 vec!["run", "c1", "--", "mkdir", "-p", "/workspace"]
1542 );
1543 assert_eq!(
1544 legacy[1].args,
1545 vec!["config", "--workingdir", "/workspace", "c1"]
1546 );
1547 }
1548
1549 #[test]
1550 fn test_translator_active_shell_reflects_override() {
1551 let mut t = DockerfileTranslator::new(ImageOs::Linux);
1552 assert_eq!(t.active_shell(), vec!["/bin/sh", "-c"]);
1553
1554 t.set_shell_override(vec!["/bin/bash".to_string(), "-lc".to_string()]);
1555 assert_eq!(t.active_shell(), vec!["/bin/bash", "-lc"]);
1556 }
1557
1558 #[test]
1559 fn test_translator_target_os_accessor() {
1560 assert_eq!(
1561 DockerfileTranslator::new(ImageOs::Linux).target_os(),
1562 ImageOs::Linux
1563 );
1564 assert_eq!(
1565 DockerfileTranslator::new(ImageOs::Windows).target_os(),
1566 ImageOs::Windows
1567 );
1568 }
1569
1570 #[test]
1571 fn test_translator_windows_run_with_mounts_uses_cmd_exe() {
1572 use crate::dockerfile::{CacheSharing, RunMount};
1573
1574 let mut t = DockerfileTranslator::new(ImageOs::Windows);
1575 let run = RunInstruction {
1576 command: ShellOrExec::Shell("echo cached".to_string()),
1577 mounts: vec![RunMount::Cache {
1578 target: "C:\\cache".to_string(),
1579 id: Some("win-cache".to_string()),
1580 sharing: CacheSharing::Shared,
1581 readonly: false,
1582 }],
1583 network: None,
1584 security: None,
1585 };
1586
1587 let cmds = t.translate("c1", &Instruction::Run(run));
1588 assert_eq!(cmds.len(), 1);
1589
1590 let mount_idx = cmds[0]
1592 .args
1593 .iter()
1594 .position(|a| a.starts_with("--mount="))
1595 .expect("mount arg present");
1596 let container_idx = cmds[0]
1597 .args
1598 .iter()
1599 .position(|a| a == "c1")
1600 .expect("container ID present");
1601 assert!(mount_idx < container_idx);
1602
1603 assert!(cmds[0].args.iter().any(|a| a == "cmd.exe"));
1605 assert!(cmds[0].args.iter().any(|a| a == "/S"));
1606 assert!(cmds[0].args.iter().any(|a| a == "/C"));
1607 assert!(!cmds[0].args.iter().any(|a| a == "/bin/sh"));
1608 }
1609}