1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub enum ShellOrExec {
12 Shell(String),
15
16 Exec(Vec<String>),
19}
20
21impl ShellOrExec {
22 pub fn is_shell(&self) -> bool {
24 matches!(self, Self::Shell(_))
25 }
26
27 pub fn is_exec(&self) -> bool {
29 matches!(self, Self::Exec(_))
30 }
31
32 pub fn to_exec_args(&self, shell: &[String]) -> Vec<String> {
34 match self {
35 Self::Shell(cmd) => {
36 let mut args = shell.to_vec();
37 args.push(cmd.clone());
38 args
39 }
40 Self::Exec(args) => args.clone(),
41 }
42 }
43}
44
45impl Default for ShellOrExec {
46 fn default() -> Self {
47 Self::Exec(Vec::new())
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub enum Instruction {
54 Run(RunInstruction),
56
57 Copy(CopyInstruction),
59
60 Add(AddInstruction),
62
63 Env(EnvInstruction),
65
66 Workdir(String),
68
69 Expose(ExposeInstruction),
71
72 Label(HashMap<String, String>),
74
75 User(String),
77
78 Entrypoint(ShellOrExec),
80
81 Cmd(ShellOrExec),
83
84 Volume(Vec<String>),
86
87 Shell(Vec<String>),
89
90 Arg(ArgInstruction),
92
93 Stopsignal(String),
95
96 Healthcheck(HealthcheckInstruction),
98
99 Onbuild(Box<Instruction>),
101}
102
103impl Instruction {
104 pub fn name(&self) -> &'static str {
106 match self {
107 Self::Run(_) => "RUN",
108 Self::Copy(_) => "COPY",
109 Self::Add(_) => "ADD",
110 Self::Env(_) => "ENV",
111 Self::Workdir(_) => "WORKDIR",
112 Self::Expose(_) => "EXPOSE",
113 Self::Label(_) => "LABEL",
114 Self::User(_) => "USER",
115 Self::Entrypoint(_) => "ENTRYPOINT",
116 Self::Cmd(_) => "CMD",
117 Self::Volume(_) => "VOLUME",
118 Self::Shell(_) => "SHELL",
119 Self::Arg(_) => "ARG",
120 Self::Stopsignal(_) => "STOPSIGNAL",
121 Self::Healthcheck(_) => "HEALTHCHECK",
122 Self::Onbuild(_) => "ONBUILD",
123 }
124 }
125
126 pub fn creates_layer(&self) -> bool {
128 matches!(self, Self::Run(_) | Self::Copy(_) | Self::Add(_))
129 }
130
131 pub fn cache_key(&self) -> String {
151 use std::collections::hash_map::DefaultHasher;
152 use std::hash::{Hash, Hasher};
153
154 let mut hasher = DefaultHasher::new();
155
156 match self {
157 Self::Run(run) => {
158 "RUN".hash(&mut hasher);
159 match &run.command {
161 ShellOrExec::Shell(s) => {
162 "shell".hash(&mut hasher);
163 s.hash(&mut hasher);
164 }
165 ShellOrExec::Exec(args) => {
166 "exec".hash(&mut hasher);
167 args.hash(&mut hasher);
168 }
169 }
170 for mount in &run.mounts {
172 format!("{:?}", mount).hash(&mut hasher);
173 }
174 if let Some(network) = &run.network {
176 format!("{:?}", network).hash(&mut hasher);
177 }
178 if let Some(security) = &run.security {
180 format!("{:?}", security).hash(&mut hasher);
181 }
182 }
183 Self::Copy(copy) => {
184 "COPY".hash(&mut hasher);
185 copy.sources.hash(&mut hasher);
186 copy.destination.hash(&mut hasher);
187 copy.from.hash(&mut hasher);
188 copy.chown.hash(&mut hasher);
189 copy.chmod.hash(&mut hasher);
190 copy.link.hash(&mut hasher);
191 copy.exclude.hash(&mut hasher);
192 }
193 Self::Add(add) => {
194 "ADD".hash(&mut hasher);
195 add.sources.hash(&mut hasher);
196 add.destination.hash(&mut hasher);
197 add.chown.hash(&mut hasher);
198 add.chmod.hash(&mut hasher);
199 add.link.hash(&mut hasher);
200 add.checksum.hash(&mut hasher);
201 add.keep_git_dir.hash(&mut hasher);
202 }
203 Self::Env(env) => {
204 "ENV".hash(&mut hasher);
205 let mut keys: Vec<_> = env.vars.keys().collect();
207 keys.sort();
208 for key in keys {
209 key.hash(&mut hasher);
210 env.vars.get(key).hash(&mut hasher);
211 }
212 }
213 Self::Workdir(path) => {
214 "WORKDIR".hash(&mut hasher);
215 path.hash(&mut hasher);
216 }
217 Self::Expose(expose) => {
218 "EXPOSE".hash(&mut hasher);
219 expose.port.hash(&mut hasher);
220 format!("{:?}", expose.protocol).hash(&mut hasher);
221 }
222 Self::Label(labels) => {
223 "LABEL".hash(&mut hasher);
224 let mut keys: Vec<_> = labels.keys().collect();
226 keys.sort();
227 for key in keys {
228 key.hash(&mut hasher);
229 labels.get(key).hash(&mut hasher);
230 }
231 }
232 Self::User(user) => {
233 "USER".hash(&mut hasher);
234 user.hash(&mut hasher);
235 }
236 Self::Entrypoint(cmd) => {
237 "ENTRYPOINT".hash(&mut hasher);
238 match cmd {
239 ShellOrExec::Shell(s) => {
240 "shell".hash(&mut hasher);
241 s.hash(&mut hasher);
242 }
243 ShellOrExec::Exec(args) => {
244 "exec".hash(&mut hasher);
245 args.hash(&mut hasher);
246 }
247 }
248 }
249 Self::Cmd(cmd) => {
250 "CMD".hash(&mut hasher);
251 match cmd {
252 ShellOrExec::Shell(s) => {
253 "shell".hash(&mut hasher);
254 s.hash(&mut hasher);
255 }
256 ShellOrExec::Exec(args) => {
257 "exec".hash(&mut hasher);
258 args.hash(&mut hasher);
259 }
260 }
261 }
262 Self::Volume(paths) => {
263 "VOLUME".hash(&mut hasher);
264 paths.hash(&mut hasher);
265 }
266 Self::Shell(shell) => {
267 "SHELL".hash(&mut hasher);
268 shell.hash(&mut hasher);
269 }
270 Self::Arg(arg) => {
271 "ARG".hash(&mut hasher);
272 arg.name.hash(&mut hasher);
273 arg.default.hash(&mut hasher);
274 }
275 Self::Stopsignal(signal) => {
276 "STOPSIGNAL".hash(&mut hasher);
277 signal.hash(&mut hasher);
278 }
279 Self::Healthcheck(health) => {
280 "HEALTHCHECK".hash(&mut hasher);
281 match health {
282 HealthcheckInstruction::None => {
283 "none".hash(&mut hasher);
284 }
285 HealthcheckInstruction::Check {
286 command,
287 interval,
288 timeout,
289 start_period,
290 start_interval,
291 retries,
292 } => {
293 "check".hash(&mut hasher);
294 match command {
295 ShellOrExec::Shell(s) => {
296 "shell".hash(&mut hasher);
297 s.hash(&mut hasher);
298 }
299 ShellOrExec::Exec(args) => {
300 "exec".hash(&mut hasher);
301 args.hash(&mut hasher);
302 }
303 }
304 interval.map(|d| d.as_nanos()).hash(&mut hasher);
305 timeout.map(|d| d.as_nanos()).hash(&mut hasher);
306 start_period.map(|d| d.as_nanos()).hash(&mut hasher);
307 start_interval.map(|d| d.as_nanos()).hash(&mut hasher);
308 retries.hash(&mut hasher);
309 }
310 }
311 }
312 Self::Onbuild(inner) => {
313 "ONBUILD".hash(&mut hasher);
314 inner.cache_key().hash(&mut hasher);
316 }
317 }
318
319 format!("{:016x}", hasher.finish())
320 }
321}
322
323#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325pub struct RunInstruction {
326 pub command: ShellOrExec,
328
329 pub mounts: Vec<RunMount>,
331
332 pub network: Option<RunNetwork>,
334
335 pub security: Option<RunSecurity>,
337}
338
339impl RunInstruction {
340 pub fn shell(cmd: impl Into<String>) -> Self {
342 Self {
343 command: ShellOrExec::Shell(cmd.into()),
344 mounts: Vec::new(),
345 network: None,
346 security: None,
347 }
348 }
349
350 pub fn exec(args: Vec<String>) -> Self {
352 Self {
353 command: ShellOrExec::Exec(args),
354 mounts: Vec::new(),
355 network: None,
356 security: None,
357 }
358 }
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363pub enum RunMount {
364 Bind {
366 target: String,
367 source: Option<String>,
368 from: Option<String>,
369 readonly: bool,
370 },
371 Cache {
373 target: String,
374 id: Option<String>,
375 sharing: CacheSharing,
376 readonly: bool,
377 },
378 Tmpfs { target: String, size: Option<u64> },
380 Secret {
382 target: String,
383 id: String,
384 required: bool,
385 },
386 Ssh {
388 target: String,
389 id: Option<String>,
390 required: bool,
391 },
392}
393
394impl RunMount {
395 pub fn to_buildah_arg(&self) -> String {
417 match self {
418 Self::Bind {
419 target,
420 source,
421 from,
422 readonly,
423 } => {
424 let mut parts = vec![format!("type=bind,target={}", target)];
425 if let Some(src) = source {
426 parts.push(format!("source={}", src));
427 }
428 if let Some(from_stage) = from {
429 parts.push(format!("from={}", from_stage));
430 }
431 if *readonly {
432 parts.push("ro".to_string());
433 }
434 parts.join(",")
435 }
436 Self::Cache {
437 target,
438 id,
439 sharing,
440 readonly,
441 } => {
442 let mut parts = vec![format!("type=cache,target={}", target)];
443 if let Some(cache_id) = id {
444 parts.push(format!("id={}", cache_id));
445 }
446 if *sharing != CacheSharing::Locked {
448 parts.push(format!("sharing={}", sharing.as_str()));
449 }
450 if *readonly {
451 parts.push("ro".to_string());
452 }
453 parts.join(",")
454 }
455 Self::Tmpfs { target, size } => {
456 let mut parts = vec![format!("type=tmpfs,target={}", target)];
457 if let Some(sz) = size {
458 parts.push(format!("tmpfs-size={}", sz));
459 }
460 parts.join(",")
461 }
462 Self::Secret {
463 target,
464 id,
465 required,
466 } => {
467 let mut parts = vec![format!("type=secret,id={}", id)];
468 if !target.is_empty() {
470 parts.push(format!("target={}", target));
471 }
472 if *required {
473 parts.push("required".to_string());
474 }
475 parts.join(",")
476 }
477 Self::Ssh {
478 target,
479 id,
480 required,
481 } => {
482 let mut parts = vec!["type=ssh".to_string()];
483 if let Some(ssh_id) = id {
484 parts.push(format!("id={}", ssh_id));
485 }
486 if !target.is_empty() {
487 parts.push(format!("target={}", target));
488 }
489 if *required {
490 parts.push("required".to_string());
491 }
492 parts.join(",")
493 }
494 }
495 }
496}
497
498#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
500pub enum CacheSharing {
501 #[default]
503 Locked,
504 Shared,
506 Private,
508}
509
510impl CacheSharing {
511 pub fn as_str(&self) -> &'static str {
513 match self {
514 Self::Locked => "locked",
515 Self::Shared => "shared",
516 Self::Private => "private",
517 }
518 }
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
523pub enum RunNetwork {
524 Default,
526 None,
528 Host,
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
534pub enum RunSecurity {
535 Sandbox,
537 Insecure,
539}
540
541#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
543pub struct CopyInstruction {
544 pub sources: Vec<String>,
546
547 pub destination: String,
549
550 pub from: Option<String>,
552
553 pub chown: Option<String>,
555
556 pub chmod: Option<String>,
558
559 pub link: bool,
561
562 pub exclude: Vec<String>,
564}
565
566impl CopyInstruction {
567 pub fn new(sources: Vec<String>, destination: String) -> Self {
569 Self {
570 sources,
571 destination,
572 from: None,
573 chown: None,
574 chmod: None,
575 link: false,
576 exclude: Vec::new(),
577 }
578 }
579
580 pub fn from_stage(mut self, stage: impl Into<String>) -> Self {
582 self.from = Some(stage.into());
583 self
584 }
585
586 pub fn chown(mut self, owner: impl Into<String>) -> Self {
588 self.chown = Some(owner.into());
589 self
590 }
591}
592
593#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
595pub struct AddInstruction {
596 pub sources: Vec<String>,
598
599 pub destination: String,
601
602 pub chown: Option<String>,
604
605 pub chmod: Option<String>,
607
608 pub link: bool,
610
611 pub checksum: Option<String>,
613
614 pub keep_git_dir: bool,
616}
617
618impl AddInstruction {
619 pub fn new(sources: Vec<String>, destination: String) -> Self {
621 Self {
622 sources,
623 destination,
624 chown: None,
625 chmod: None,
626 link: false,
627 checksum: None,
628 keep_git_dir: false,
629 }
630 }
631}
632
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
635pub struct EnvInstruction {
636 pub vars: HashMap<String, String>,
638}
639
640impl EnvInstruction {
641 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
643 let mut vars = HashMap::new();
644 vars.insert(key.into(), value.into());
645 Self { vars }
646 }
647
648 pub fn from_vars(vars: HashMap<String, String>) -> Self {
650 Self { vars }
651 }
652}
653
654#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656pub struct ExposeInstruction {
657 pub port: u16,
659
660 pub protocol: ExposeProtocol,
662}
663
664impl ExposeInstruction {
665 pub fn tcp(port: u16) -> Self {
667 Self {
668 port,
669 protocol: ExposeProtocol::Tcp,
670 }
671 }
672
673 pub fn udp(port: u16) -> Self {
675 Self {
676 port,
677 protocol: ExposeProtocol::Udp,
678 }
679 }
680}
681
682#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
684pub enum ExposeProtocol {
685 #[default]
686 Tcp,
687 Udp,
688}
689
690#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
692pub struct ArgInstruction {
693 pub name: String,
695
696 pub default: Option<String>,
698}
699
700impl ArgInstruction {
701 pub fn new(name: impl Into<String>) -> Self {
703 Self {
704 name: name.into(),
705 default: None,
706 }
707 }
708
709 pub fn with_default(name: impl Into<String>, default: impl Into<String>) -> Self {
711 Self {
712 name: name.into(),
713 default: Some(default.into()),
714 }
715 }
716}
717
718#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
720pub enum HealthcheckInstruction {
721 None,
723
724 Check {
726 command: ShellOrExec,
728
729 interval: Option<std::time::Duration>,
731
732 timeout: Option<std::time::Duration>,
734
735 start_period: Option<std::time::Duration>,
737
738 start_interval: Option<std::time::Duration>,
740
741 retries: Option<u32>,
743 },
744}
745
746impl HealthcheckInstruction {
747 pub fn cmd(command: ShellOrExec) -> Self {
749 Self::Check {
750 command,
751 interval: None,
752 timeout: None,
753 start_period: None,
754 start_interval: None,
755 retries: None,
756 }
757 }
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763
764 #[test]
765 fn test_shell_or_exec() {
766 let shell = ShellOrExec::Shell("echo hello".to_string());
767 assert!(shell.is_shell());
768 assert!(!shell.is_exec());
769
770 let exec = ShellOrExec::Exec(vec!["echo".to_string(), "hello".to_string()]);
771 assert!(exec.is_exec());
772 assert!(!exec.is_shell());
773 }
774
775 #[test]
776 fn test_shell_to_exec_args() {
777 let shell = ShellOrExec::Shell("echo hello".to_string());
778 let default_shell = vec!["/bin/sh".to_string(), "-c".to_string()];
779 let args = shell.to_exec_args(&default_shell);
780 assert_eq!(
781 args,
782 vec!["/bin/sh", "-c", "echo hello"]
783 .into_iter()
784 .map(String::from)
785 .collect::<Vec<_>>()
786 );
787 }
788
789 #[test]
790 fn test_instruction_names() {
791 assert_eq!(
792 Instruction::Run(RunInstruction::shell("test")).name(),
793 "RUN"
794 );
795 assert_eq!(
796 Instruction::Copy(CopyInstruction::new(vec![], ".".into())).name(),
797 "COPY"
798 );
799 assert_eq!(Instruction::Workdir("/app".into()).name(), "WORKDIR");
800 }
801
802 #[test]
803 fn test_creates_layer() {
804 assert!(Instruction::Run(RunInstruction::shell("test")).creates_layer());
805 assert!(Instruction::Copy(CopyInstruction::new(vec![], ".".into())).creates_layer());
806 assert!(!Instruction::Env(EnvInstruction::new("KEY", "value")).creates_layer());
807 assert!(!Instruction::Workdir("/app".into()).creates_layer());
808 }
809
810 #[test]
811 fn test_copy_instruction_builder() {
812 let copy = CopyInstruction::new(vec!["src".into()], "/app".into())
813 .from_stage("builder")
814 .chown("1000:1000");
815
816 assert_eq!(copy.from, Some("builder".to_string()));
817 assert_eq!(copy.chown, Some("1000:1000".to_string()));
818 }
819
820 #[test]
821 fn test_arg_instruction() {
822 let arg = ArgInstruction::new("VERSION");
823 assert_eq!(arg.name, "VERSION");
824 assert!(arg.default.is_none());
825
826 let arg_with_default = ArgInstruction::with_default("VERSION", "1.0");
827 assert_eq!(arg_with_default.default, Some("1.0".to_string()));
828 }
829
830 #[test]
831 fn test_cache_sharing_as_str() {
832 assert_eq!(CacheSharing::Locked.as_str(), "locked");
833 assert_eq!(CacheSharing::Shared.as_str(), "shared");
834 assert_eq!(CacheSharing::Private.as_str(), "private");
835 }
836
837 #[test]
838 fn test_run_mount_cache_to_buildah_arg() {
839 let mount = RunMount::Cache {
840 target: "/var/cache/apt".to_string(),
841 id: Some("apt-cache".to_string()),
842 sharing: CacheSharing::Shared,
843 readonly: false,
844 };
845 assert_eq!(
846 mount.to_buildah_arg(),
847 "type=cache,target=/var/cache/apt,id=apt-cache,sharing=shared"
848 );
849
850 let mount_locked = RunMount::Cache {
852 target: "/cache".to_string(),
853 id: None,
854 sharing: CacheSharing::Locked,
855 readonly: false,
856 };
857 assert_eq!(mount_locked.to_buildah_arg(), "type=cache,target=/cache");
858
859 let mount_ro = RunMount::Cache {
861 target: "/cache".to_string(),
862 id: Some("mycache".to_string()),
863 sharing: CacheSharing::Locked,
864 readonly: true,
865 };
866 assert_eq!(
867 mount_ro.to_buildah_arg(),
868 "type=cache,target=/cache,id=mycache,ro"
869 );
870 }
871
872 #[test]
873 fn test_run_mount_bind_to_buildah_arg() {
874 let mount = RunMount::Bind {
875 target: "/app".to_string(),
876 source: Some("/src".to_string()),
877 from: Some("builder".to_string()),
878 readonly: true,
879 };
880 assert_eq!(
881 mount.to_buildah_arg(),
882 "type=bind,target=/app,source=/src,from=builder,ro"
883 );
884
885 let mount_minimal = RunMount::Bind {
887 target: "/app".to_string(),
888 source: None,
889 from: None,
890 readonly: false,
891 };
892 assert_eq!(mount_minimal.to_buildah_arg(), "type=bind,target=/app");
893 }
894
895 #[test]
896 fn test_run_mount_tmpfs_to_buildah_arg() {
897 let mount = RunMount::Tmpfs {
898 target: "/tmp".to_string(),
899 size: Some(1048576),
900 };
901 assert_eq!(
902 mount.to_buildah_arg(),
903 "type=tmpfs,target=/tmp,tmpfs-size=1048576"
904 );
905
906 let mount_no_size = RunMount::Tmpfs {
907 target: "/tmp".to_string(),
908 size: None,
909 };
910 assert_eq!(mount_no_size.to_buildah_arg(), "type=tmpfs,target=/tmp");
911 }
912
913 #[test]
914 fn test_run_mount_secret_to_buildah_arg() {
915 let mount = RunMount::Secret {
916 target: "/run/secrets/mysecret".to_string(),
917 id: "mysecret".to_string(),
918 required: true,
919 };
920 assert_eq!(
921 mount.to_buildah_arg(),
922 "type=secret,id=mysecret,target=/run/secrets/mysecret,required"
923 );
924
925 let mount_no_target = RunMount::Secret {
927 target: String::new(),
928 id: "mysecret".to_string(),
929 required: false,
930 };
931 assert_eq!(mount_no_target.to_buildah_arg(), "type=secret,id=mysecret");
932 }
933
934 #[test]
935 fn test_run_mount_ssh_to_buildah_arg() {
936 let mount = RunMount::Ssh {
937 target: "/root/.ssh".to_string(),
938 id: Some("default".to_string()),
939 required: true,
940 };
941 assert_eq!(
942 mount.to_buildah_arg(),
943 "type=ssh,id=default,target=/root/.ssh,required"
944 );
945
946 let mount_minimal = RunMount::Ssh {
948 target: String::new(),
949 id: None,
950 required: false,
951 };
952 assert_eq!(mount_minimal.to_buildah_arg(), "type=ssh");
953 }
954
955 #[test]
956 fn test_cache_key_length() {
957 let run = Instruction::Run(RunInstruction::shell("echo hello"));
959 assert_eq!(run.cache_key().len(), 16);
960
961 let copy = Instruction::Copy(CopyInstruction::new(vec!["src".into()], "/app".into()));
962 assert_eq!(copy.cache_key().len(), 16);
963
964 let workdir = Instruction::Workdir("/app".into());
965 assert_eq!(workdir.cache_key().len(), 16);
966 }
967
968 #[test]
969 fn test_cache_key_deterministic() {
970 let run1 = Instruction::Run(RunInstruction::shell("apt-get update"));
972 let run2 = Instruction::Run(RunInstruction::shell("apt-get update"));
973 assert_eq!(run1.cache_key(), run2.cache_key());
974
975 let copy1 = Instruction::Copy(CopyInstruction::new(vec!["src".into()], "/app".into()));
976 let copy2 = Instruction::Copy(CopyInstruction::new(vec!["src".into()], "/app".into()));
977 assert_eq!(copy1.cache_key(), copy2.cache_key());
978 }
979
980 #[test]
981 fn test_cache_key_different_for_different_instructions() {
982 let run = Instruction::Run(RunInstruction::shell("echo hello"));
984 let workdir = Instruction::Workdir("echo hello".into());
985 assert_ne!(run.cache_key(), workdir.cache_key());
986
987 let run1 = Instruction::Run(RunInstruction::shell("apt-get update"));
989 let run2 = Instruction::Run(RunInstruction::shell("apt-get upgrade"));
990 assert_ne!(run1.cache_key(), run2.cache_key());
991
992 let copy1 = Instruction::Copy(CopyInstruction::new(vec!["src".into()], "/app".into()));
994 let copy2 = Instruction::Copy(CopyInstruction::new(vec!["src".into()], "/opt".into()));
995 assert_ne!(copy1.cache_key(), copy2.cache_key());
996 }
997
998 #[test]
999 fn test_cache_key_shell_vs_exec() {
1000 let shell = Instruction::Run(RunInstruction::shell("echo hello"));
1002 let exec = Instruction::Run(RunInstruction::exec(vec![
1003 "echo".to_string(),
1004 "hello".to_string(),
1005 ]));
1006 assert_ne!(shell.cache_key(), exec.cache_key());
1007 }
1008
1009 #[test]
1010 fn test_cache_key_with_mounts() {
1011 let run_no_mount = Instruction::Run(RunInstruction::shell("apt-get install -y curl"));
1013
1014 let mut run_with_mount = RunInstruction::shell("apt-get install -y curl");
1015 run_with_mount.mounts = vec![RunMount::Cache {
1016 target: "/var/cache/apt".to_string(),
1017 id: Some("apt-cache".to_string()),
1018 sharing: CacheSharing::Shared,
1019 readonly: false,
1020 }];
1021 let run_mounted = Instruction::Run(run_with_mount);
1022
1023 assert_ne!(run_no_mount.cache_key(), run_mounted.cache_key());
1024 }
1025
1026 #[test]
1027 fn test_cache_key_env_ordering() {
1028 let mut vars1 = HashMap::new();
1031 vars1.insert("A".to_string(), "1".to_string());
1032 vars1.insert("B".to_string(), "2".to_string());
1033 let env1 = Instruction::Env(EnvInstruction::from_vars(vars1));
1034
1035 let mut vars2 = HashMap::new();
1036 vars2.insert("B".to_string(), "2".to_string());
1037 vars2.insert("A".to_string(), "1".to_string());
1038 let env2 = Instruction::Env(EnvInstruction::from_vars(vars2));
1039
1040 assert_eq!(env1.cache_key(), env2.cache_key());
1041 }
1042
1043 #[test]
1044 fn test_cache_key_onbuild() {
1045 let inner = RunInstruction::shell("echo hello");
1047 let onbuild = Instruction::Onbuild(Box::new(Instruction::Run(inner.clone())));
1048
1049 let run_key = Instruction::Run(inner).cache_key();
1051 let onbuild_key = onbuild.cache_key();
1052 assert_ne!(run_key, onbuild_key);
1053 }
1054
1055 #[test]
1056 fn test_cache_key_copy_with_options() {
1057 let copy_simple = Instruction::Copy(CopyInstruction::new(
1059 vec!["binary".into()],
1060 "/app/binary".into(),
1061 ));
1062
1063 let copy_from = Instruction::Copy(
1064 CopyInstruction::new(vec!["binary".into()], "/app/binary".into()).from_stage("builder"),
1065 );
1066
1067 assert_ne!(copy_simple.cache_key(), copy_from.cache_key());
1068 }
1069}