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 let mut env_keys: Vec<_> = run.env.keys().collect();
193 env_keys.sort();
194 for key in env_keys {
195 key.hash(&mut hasher);
196 run.env.get(key).hash(&mut hasher);
197 }
198 }
199 Self::Copy(copy) => {
200 "COPY".hash(&mut hasher);
201 copy.sources.hash(&mut hasher);
202 copy.destination.hash(&mut hasher);
203 copy.from.hash(&mut hasher);
204 copy.chown.hash(&mut hasher);
205 copy.chmod.hash(&mut hasher);
206 copy.link.hash(&mut hasher);
207 copy.exclude.hash(&mut hasher);
208 }
209 Self::Add(add) => {
210 "ADD".hash(&mut hasher);
211 add.sources.hash(&mut hasher);
212 add.destination.hash(&mut hasher);
213 add.chown.hash(&mut hasher);
214 add.chmod.hash(&mut hasher);
215 add.link.hash(&mut hasher);
216 add.checksum.hash(&mut hasher);
217 add.keep_git_dir.hash(&mut hasher);
218 }
219 Self::Env(env) => {
220 "ENV".hash(&mut hasher);
221 let mut keys: Vec<_> = env.vars.keys().collect();
223 keys.sort();
224 for key in keys {
225 key.hash(&mut hasher);
226 env.vars.get(key).hash(&mut hasher);
227 }
228 }
229 Self::Workdir(path) => {
230 "WORKDIR".hash(&mut hasher);
231 path.hash(&mut hasher);
232 }
233 Self::Expose(expose) => {
234 "EXPOSE".hash(&mut hasher);
235 expose.port.hash(&mut hasher);
236 format!("{:?}", expose.protocol).hash(&mut hasher);
237 }
238 Self::Label(labels) => {
239 "LABEL".hash(&mut hasher);
240 let mut keys: Vec<_> = labels.keys().collect();
242 keys.sort();
243 for key in keys {
244 key.hash(&mut hasher);
245 labels.get(key).hash(&mut hasher);
246 }
247 }
248 Self::User(user) => {
249 "USER".hash(&mut hasher);
250 user.hash(&mut hasher);
251 }
252 Self::Entrypoint(cmd) => {
253 "ENTRYPOINT".hash(&mut hasher);
254 match cmd {
255 ShellOrExec::Shell(s) => {
256 "shell".hash(&mut hasher);
257 s.hash(&mut hasher);
258 }
259 ShellOrExec::Exec(args) => {
260 "exec".hash(&mut hasher);
261 args.hash(&mut hasher);
262 }
263 }
264 }
265 Self::Cmd(cmd) => {
266 "CMD".hash(&mut hasher);
267 match cmd {
268 ShellOrExec::Shell(s) => {
269 "shell".hash(&mut hasher);
270 s.hash(&mut hasher);
271 }
272 ShellOrExec::Exec(args) => {
273 "exec".hash(&mut hasher);
274 args.hash(&mut hasher);
275 }
276 }
277 }
278 Self::Volume(paths) => {
279 "VOLUME".hash(&mut hasher);
280 paths.hash(&mut hasher);
281 }
282 Self::Shell(shell) => {
283 "SHELL".hash(&mut hasher);
284 shell.hash(&mut hasher);
285 }
286 Self::Arg(arg) => {
287 "ARG".hash(&mut hasher);
288 arg.name.hash(&mut hasher);
289 arg.default.hash(&mut hasher);
290 }
291 Self::Stopsignal(signal) => {
292 "STOPSIGNAL".hash(&mut hasher);
293 signal.hash(&mut hasher);
294 }
295 Self::Healthcheck(health) => {
296 "HEALTHCHECK".hash(&mut hasher);
297 match health {
298 HealthcheckInstruction::None => {
299 "none".hash(&mut hasher);
300 }
301 HealthcheckInstruction::Check {
302 command,
303 interval,
304 timeout,
305 start_period,
306 start_interval,
307 retries,
308 } => {
309 "check".hash(&mut hasher);
310 match command {
311 ShellOrExec::Shell(s) => {
312 "shell".hash(&mut hasher);
313 s.hash(&mut hasher);
314 }
315 ShellOrExec::Exec(args) => {
316 "exec".hash(&mut hasher);
317 args.hash(&mut hasher);
318 }
319 }
320 interval.map(|d| d.as_nanos()).hash(&mut hasher);
321 timeout.map(|d| d.as_nanos()).hash(&mut hasher);
322 start_period.map(|d| d.as_nanos()).hash(&mut hasher);
323 start_interval.map(|d| d.as_nanos()).hash(&mut hasher);
324 retries.hash(&mut hasher);
325 }
326 }
327 }
328 Self::Onbuild(inner) => {
329 "ONBUILD".hash(&mut hasher);
330 inner.cache_key().hash(&mut hasher);
332 }
333 }
334
335 format!("{:016x}", hasher.finish())
336 }
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341pub struct RunInstruction {
342 pub command: ShellOrExec,
344
345 pub mounts: Vec<RunMount>,
347
348 pub network: Option<RunNetwork>,
350
351 pub security: Option<RunSecurity>,
353
354 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
358 pub env: HashMap<String, String>,
359}
360
361impl RunInstruction {
362 pub fn shell(cmd: impl Into<String>) -> Self {
364 Self {
365 command: ShellOrExec::Shell(cmd.into()),
366 mounts: Vec::new(),
367 network: None,
368 security: None,
369 env: HashMap::new(),
370 }
371 }
372
373 #[must_use]
375 pub fn exec(args: Vec<String>) -> Self {
376 Self {
377 command: ShellOrExec::Exec(args),
378 mounts: Vec::new(),
379 network: None,
380 security: None,
381 env: HashMap::new(),
382 }
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388pub enum RunMount {
389 Bind {
391 target: String,
392 source: Option<String>,
393 from: Option<String>,
394 readonly: bool,
395 },
396 Cache {
398 target: String,
399 id: Option<String>,
400 sharing: CacheSharing,
401 readonly: bool,
402 },
403 Tmpfs { target: String, size: Option<u64> },
405 Secret {
407 target: String,
408 id: String,
409 required: bool,
410 },
411 Ssh {
413 target: String,
414 id: Option<String>,
415 required: bool,
416 },
417}
418
419impl RunMount {
420 #[must_use]
442 pub fn to_buildah_arg(&self) -> String {
443 match self {
444 Self::Bind {
445 target,
446 source,
447 from,
448 readonly,
449 } => {
450 let mut parts = vec![format!("type=bind,target={}", target)];
451 if let Some(src) = source {
452 parts.push(format!("source={src}"));
453 }
454 if let Some(from_stage) = from {
455 parts.push(format!("from={from_stage}"));
456 }
457 if *readonly {
458 parts.push("ro".to_string());
459 }
460 parts.join(",")
461 }
462 Self::Cache {
463 target,
464 id,
465 sharing,
466 readonly,
467 } => {
468 let mut parts = vec![format!("type=cache,target={}", target)];
469 if let Some(cache_id) = id {
470 parts.push(format!("id={cache_id}"));
471 }
472 if *sharing != CacheSharing::Locked {
474 parts.push(format!("sharing={}", sharing.as_str()));
475 }
476 if *readonly {
477 parts.push("ro".to_string());
478 }
479 parts.join(",")
480 }
481 Self::Tmpfs { target, size } => {
482 let mut parts = vec![format!("type=tmpfs,target={}", target)];
483 if let Some(sz) = size {
484 parts.push(format!("tmpfs-size={sz}"));
485 }
486 parts.join(",")
487 }
488 Self::Secret {
489 target,
490 id,
491 required,
492 } => {
493 let mut parts = vec![format!("type=secret,id={}", id)];
494 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 Self::Ssh {
504 target,
505 id,
506 required,
507 } => {
508 let mut parts = vec!["type=ssh".to_string()];
509 if let Some(ssh_id) = id {
510 parts.push(format!("id={ssh_id}"));
511 }
512 if !target.is_empty() {
513 parts.push(format!("target={target}"));
514 }
515 if *required {
516 parts.push("required".to_string());
517 }
518 parts.join(",")
519 }
520 }
521 }
522}
523
524#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
526pub enum CacheSharing {
527 #[default]
529 Locked,
530 Shared,
532 Private,
534}
535
536impl CacheSharing {
537 #[must_use]
539 pub fn as_str(&self) -> &'static str {
540 match self {
541 Self::Locked => "locked",
542 Self::Shared => "shared",
543 Self::Private => "private",
544 }
545 }
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
550pub enum RunNetwork {
551 Default,
553 None,
555 Host,
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
561pub enum RunSecurity {
562 Sandbox,
564 Insecure,
566}
567
568#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
570pub struct CopyInstruction {
571 pub sources: Vec<String>,
573
574 pub destination: String,
576
577 pub from: Option<String>,
579
580 pub chown: Option<String>,
582
583 pub chmod: Option<String>,
585
586 pub link: bool,
588
589 pub exclude: Vec<String>,
591}
592
593impl CopyInstruction {
594 #[must_use]
596 pub fn new(sources: Vec<String>, destination: String) -> Self {
597 Self {
598 sources,
599 destination,
600 from: None,
601 chown: None,
602 chmod: None,
603 link: false,
604 exclude: Vec::new(),
605 }
606 }
607
608 #[must_use]
610 pub fn from_stage(mut self, stage: impl Into<String>) -> Self {
611 self.from = Some(stage.into());
612 self
613 }
614
615 #[must_use]
617 pub fn chown(mut self, owner: impl Into<String>) -> Self {
618 self.chown = Some(owner.into());
619 self
620 }
621}
622
623#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
625pub struct AddInstruction {
626 pub sources: Vec<String>,
628
629 pub destination: String,
631
632 pub chown: Option<String>,
634
635 pub chmod: Option<String>,
637
638 pub link: bool,
640
641 pub checksum: Option<String>,
643
644 pub keep_git_dir: bool,
646}
647
648impl AddInstruction {
649 #[must_use]
651 pub fn new(sources: Vec<String>, destination: String) -> Self {
652 Self {
653 sources,
654 destination,
655 chown: None,
656 chmod: None,
657 link: false,
658 checksum: None,
659 keep_git_dir: false,
660 }
661 }
662}
663
664#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
666pub struct EnvInstruction {
667 pub vars: HashMap<String, String>,
669}
670
671impl EnvInstruction {
672 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
674 let mut vars = HashMap::new();
675 vars.insert(key.into(), value.into());
676 Self { vars }
677 }
678
679 #[must_use]
681 pub fn from_vars(vars: HashMap<String, String>) -> Self {
682 Self { vars }
683 }
684}
685
686#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
688pub struct ExposeInstruction {
689 pub port: u16,
691
692 pub protocol: ExposeProtocol,
694}
695
696impl ExposeInstruction {
697 #[must_use]
699 pub fn tcp(port: u16) -> Self {
700 Self {
701 port,
702 protocol: ExposeProtocol::Tcp,
703 }
704 }
705
706 #[must_use]
708 pub fn udp(port: u16) -> Self {
709 Self {
710 port,
711 protocol: ExposeProtocol::Udp,
712 }
713 }
714}
715
716#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
718pub enum ExposeProtocol {
719 #[default]
720 Tcp,
721 Udp,
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
726pub struct ArgInstruction {
727 pub name: String,
729
730 pub default: Option<String>,
732}
733
734impl ArgInstruction {
735 pub fn new(name: impl Into<String>) -> Self {
737 Self {
738 name: name.into(),
739 default: None,
740 }
741 }
742
743 pub fn with_default(name: impl Into<String>, default: impl Into<String>) -> Self {
745 Self {
746 name: name.into(),
747 default: Some(default.into()),
748 }
749 }
750}
751
752#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
754pub enum HealthcheckInstruction {
755 None,
757
758 Check {
760 command: ShellOrExec,
762
763 interval: Option<std::time::Duration>,
765
766 timeout: Option<std::time::Duration>,
768
769 start_period: Option<std::time::Duration>,
771
772 start_interval: Option<std::time::Duration>,
774
775 retries: Option<u32>,
777 },
778}
779
780impl HealthcheckInstruction {
781 #[must_use]
783 pub fn cmd(command: ShellOrExec) -> Self {
784 Self::Check {
785 command,
786 interval: None,
787 timeout: None,
788 start_period: None,
789 start_interval: None,
790 retries: None,
791 }
792 }
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798
799 #[test]
800 fn test_shell_or_exec() {
801 let shell = ShellOrExec::Shell("echo hello".to_string());
802 assert!(shell.is_shell());
803 assert!(!shell.is_exec());
804
805 let exec = ShellOrExec::Exec(vec!["echo".to_string(), "hello".to_string()]);
806 assert!(exec.is_exec());
807 assert!(!exec.is_shell());
808 }
809
810 #[test]
811 fn test_shell_to_exec_args() {
812 let shell = ShellOrExec::Shell("echo hello".to_string());
813 let default_shell = vec!["/bin/sh".to_string(), "-c".to_string()];
814 let args = shell.to_exec_args(&default_shell);
815 assert_eq!(
816 args,
817 vec!["/bin/sh", "-c", "echo hello"]
818 .into_iter()
819 .map(String::from)
820 .collect::<Vec<_>>()
821 );
822 }
823
824 #[test]
825 fn test_instruction_names() {
826 assert_eq!(
827 Instruction::Run(RunInstruction::shell("test")).name(),
828 "RUN"
829 );
830 assert_eq!(
831 Instruction::Copy(CopyInstruction::new(vec![], ".".into())).name(),
832 "COPY"
833 );
834 assert_eq!(Instruction::Workdir("/app".into()).name(), "WORKDIR");
835 }
836
837 #[test]
838 fn test_creates_layer() {
839 assert!(Instruction::Run(RunInstruction::shell("test")).creates_layer());
840 assert!(Instruction::Copy(CopyInstruction::new(vec![], ".".into())).creates_layer());
841 assert!(!Instruction::Env(EnvInstruction::new("KEY", "value")).creates_layer());
842 assert!(!Instruction::Workdir("/app".into()).creates_layer());
843 }
844
845 #[test]
846 fn test_copy_instruction_builder() {
847 let copy = CopyInstruction::new(vec!["src".into()], "/app".into())
848 .from_stage("builder")
849 .chown("1000:1000");
850
851 assert_eq!(copy.from, Some("builder".to_string()));
852 assert_eq!(copy.chown, Some("1000:1000".to_string()));
853 }
854
855 #[test]
856 fn test_arg_instruction() {
857 let arg = ArgInstruction::new("VERSION");
858 assert_eq!(arg.name, "VERSION");
859 assert!(arg.default.is_none());
860
861 let arg_with_default = ArgInstruction::with_default("VERSION", "1.0");
862 assert_eq!(arg_with_default.default, Some("1.0".to_string()));
863 }
864
865 #[test]
866 fn test_cache_sharing_as_str() {
867 assert_eq!(CacheSharing::Locked.as_str(), "locked");
868 assert_eq!(CacheSharing::Shared.as_str(), "shared");
869 assert_eq!(CacheSharing::Private.as_str(), "private");
870 }
871
872 #[test]
873 fn test_run_mount_cache_to_buildah_arg() {
874 let mount = RunMount::Cache {
875 target: "/var/cache/apt".to_string(),
876 id: Some("apt-cache".to_string()),
877 sharing: CacheSharing::Shared,
878 readonly: false,
879 };
880 assert_eq!(
881 mount.to_buildah_arg(),
882 "type=cache,target=/var/cache/apt,id=apt-cache,sharing=shared"
883 );
884
885 let mount_locked = RunMount::Cache {
887 target: "/cache".to_string(),
888 id: None,
889 sharing: CacheSharing::Locked,
890 readonly: false,
891 };
892 assert_eq!(mount_locked.to_buildah_arg(), "type=cache,target=/cache");
893
894 let mount_ro = RunMount::Cache {
896 target: "/cache".to_string(),
897 id: Some("mycache".to_string()),
898 sharing: CacheSharing::Locked,
899 readonly: true,
900 };
901 assert_eq!(
902 mount_ro.to_buildah_arg(),
903 "type=cache,target=/cache,id=mycache,ro"
904 );
905 }
906
907 #[test]
908 fn test_run_mount_bind_to_buildah_arg() {
909 let mount = RunMount::Bind {
910 target: "/app".to_string(),
911 source: Some("/src".to_string()),
912 from: Some("builder".to_string()),
913 readonly: true,
914 };
915 assert_eq!(
916 mount.to_buildah_arg(),
917 "type=bind,target=/app,source=/src,from=builder,ro"
918 );
919
920 let mount_minimal = RunMount::Bind {
922 target: "/app".to_string(),
923 source: None,
924 from: None,
925 readonly: false,
926 };
927 assert_eq!(mount_minimal.to_buildah_arg(), "type=bind,target=/app");
928 }
929
930 #[test]
931 fn test_run_mount_tmpfs_to_buildah_arg() {
932 let mount = RunMount::Tmpfs {
933 target: "/tmp".to_string(),
934 size: Some(1_048_576),
935 };
936 assert_eq!(
937 mount.to_buildah_arg(),
938 "type=tmpfs,target=/tmp,tmpfs-size=1048576"
939 );
940
941 let mount_no_size = RunMount::Tmpfs {
942 target: "/tmp".to_string(),
943 size: None,
944 };
945 assert_eq!(mount_no_size.to_buildah_arg(), "type=tmpfs,target=/tmp");
946 }
947
948 #[test]
949 fn test_run_mount_secret_to_buildah_arg() {
950 let mount = RunMount::Secret {
951 target: "/run/secrets/mysecret".to_string(),
952 id: "mysecret".to_string(),
953 required: true,
954 };
955 assert_eq!(
956 mount.to_buildah_arg(),
957 "type=secret,id=mysecret,target=/run/secrets/mysecret,required"
958 );
959
960 let mount_no_target = RunMount::Secret {
962 target: String::new(),
963 id: "mysecret".to_string(),
964 required: false,
965 };
966 assert_eq!(mount_no_target.to_buildah_arg(), "type=secret,id=mysecret");
967 }
968
969 #[test]
970 fn test_run_mount_ssh_to_buildah_arg() {
971 let mount = RunMount::Ssh {
972 target: "/root/.ssh".to_string(),
973 id: Some("default".to_string()),
974 required: true,
975 };
976 assert_eq!(
977 mount.to_buildah_arg(),
978 "type=ssh,id=default,target=/root/.ssh,required"
979 );
980
981 let mount_minimal = RunMount::Ssh {
983 target: String::new(),
984 id: None,
985 required: false,
986 };
987 assert_eq!(mount_minimal.to_buildah_arg(), "type=ssh");
988 }
989
990 #[test]
991 fn test_cache_key_length() {
992 let run = Instruction::Run(RunInstruction::shell("echo hello"));
994 assert_eq!(run.cache_key().len(), 16);
995
996 let copy = Instruction::Copy(CopyInstruction::new(vec!["src".into()], "/app".into()));
997 assert_eq!(copy.cache_key().len(), 16);
998
999 let workdir = Instruction::Workdir("/app".into());
1000 assert_eq!(workdir.cache_key().len(), 16);
1001 }
1002
1003 #[test]
1004 fn test_cache_key_deterministic() {
1005 let run1 = Instruction::Run(RunInstruction::shell("apt-get update"));
1007 let run2 = Instruction::Run(RunInstruction::shell("apt-get update"));
1008 assert_eq!(run1.cache_key(), run2.cache_key());
1009
1010 let copy1 = Instruction::Copy(CopyInstruction::new(vec!["src".into()], "/app".into()));
1011 let copy2 = Instruction::Copy(CopyInstruction::new(vec!["src".into()], "/app".into()));
1012 assert_eq!(copy1.cache_key(), copy2.cache_key());
1013 }
1014
1015 #[test]
1016 fn test_cache_key_different_for_different_instructions() {
1017 let run = Instruction::Run(RunInstruction::shell("echo hello"));
1019 let workdir = Instruction::Workdir("echo hello".into());
1020 assert_ne!(run.cache_key(), workdir.cache_key());
1021
1022 let run1 = Instruction::Run(RunInstruction::shell("apt-get update"));
1024 let run2 = Instruction::Run(RunInstruction::shell("apt-get upgrade"));
1025 assert_ne!(run1.cache_key(), run2.cache_key());
1026
1027 let copy1 = Instruction::Copy(CopyInstruction::new(vec!["src".into()], "/app".into()));
1029 let copy2 = Instruction::Copy(CopyInstruction::new(vec!["src".into()], "/opt".into()));
1030 assert_ne!(copy1.cache_key(), copy2.cache_key());
1031 }
1032
1033 #[test]
1034 fn test_cache_key_shell_vs_exec() {
1035 let shell = Instruction::Run(RunInstruction::shell("echo hello"));
1037 let exec = Instruction::Run(RunInstruction::exec(vec![
1038 "echo".to_string(),
1039 "hello".to_string(),
1040 ]));
1041 assert_ne!(shell.cache_key(), exec.cache_key());
1042 }
1043
1044 #[test]
1045 fn test_cache_key_with_mounts() {
1046 let run_no_mount = Instruction::Run(RunInstruction::shell("apt-get install -y curl"));
1048
1049 let mut run_with_mount = RunInstruction::shell("apt-get install -y curl");
1050 run_with_mount.mounts = vec![RunMount::Cache {
1051 target: "/var/cache/apt".to_string(),
1052 id: Some("apt-cache".to_string()),
1053 sharing: CacheSharing::Shared,
1054 readonly: false,
1055 }];
1056 let run_mounted = Instruction::Run(run_with_mount);
1057
1058 assert_ne!(run_no_mount.cache_key(), run_mounted.cache_key());
1059 }
1060
1061 #[test]
1062 fn test_cache_key_env_ordering() {
1063 let mut vars1 = HashMap::new();
1066 vars1.insert("A".to_string(), "1".to_string());
1067 vars1.insert("B".to_string(), "2".to_string());
1068 let env1 = Instruction::Env(EnvInstruction::from_vars(vars1));
1069
1070 let mut vars2 = HashMap::new();
1071 vars2.insert("B".to_string(), "2".to_string());
1072 vars2.insert("A".to_string(), "1".to_string());
1073 let env2 = Instruction::Env(EnvInstruction::from_vars(vars2));
1074
1075 assert_eq!(env1.cache_key(), env2.cache_key());
1076 }
1077
1078 #[test]
1079 fn test_run_instruction_env_field_default_empty() {
1080 let s = RunInstruction::shell("echo hi");
1082 assert!(s.env.is_empty());
1083
1084 let e = RunInstruction::exec(vec!["echo".to_string(), "hi".to_string()]);
1085 assert!(e.env.is_empty());
1086 }
1087
1088 #[test]
1089 fn test_cache_key_env_field_affects_hash_and_is_order_invariant() {
1090 let mut env1 = HashMap::new();
1092 env1.insert("A".to_string(), "1".to_string());
1093 env1.insert("B".to_string(), "2".to_string());
1094 let r1 = RunInstruction {
1095 command: ShellOrExec::Shell("echo".to_string()),
1096 mounts: Vec::new(),
1097 network: None,
1098 security: None,
1099 env: env1,
1100 };
1101
1102 let mut env2 = HashMap::new();
1103 env2.insert("B".to_string(), "2".to_string());
1104 env2.insert("A".to_string(), "1".to_string());
1105 let r2 = RunInstruction {
1106 command: ShellOrExec::Shell("echo".to_string()),
1107 mounts: Vec::new(),
1108 network: None,
1109 security: None,
1110 env: env2,
1111 };
1112
1113 assert_eq!(
1114 Instruction::Run(r1.clone()).cache_key(),
1115 Instruction::Run(r2).cache_key()
1116 );
1117
1118 let r_no_env = RunInstruction::shell("echo");
1120 assert_ne!(
1121 Instruction::Run(r1).cache_key(),
1122 Instruction::Run(r_no_env).cache_key()
1123 );
1124 }
1125
1126 #[test]
1127 fn test_cache_key_onbuild() {
1128 let inner = RunInstruction::shell("echo hello");
1130 let onbuild = Instruction::Onbuild(Box::new(Instruction::Run(inner.clone())));
1131
1132 let run_key = Instruction::Run(inner).cache_key();
1134 let onbuild_key = onbuild.cache_key();
1135 assert_ne!(run_key, onbuild_key);
1136 }
1137
1138 #[test]
1139 fn test_cache_key_copy_with_options() {
1140 let copy_simple = Instruction::Copy(CopyInstruction::new(
1142 vec!["binary".into()],
1143 "/app/binary".into(),
1144 ));
1145
1146 let copy_from = Instruction::Copy(
1147 CopyInstruction::new(vec!["binary".into()], "/app/binary".into()).from_stage("builder"),
1148 );
1149
1150 assert_ne!(copy_simple.cache_key(), copy_from.cache_key());
1151 }
1152}