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