1use crate::errors::Error;
2
3use crate::lock::DEFAULT_PORT;
4use crate::{
5 dockerfile_struct::*, dofigen_struct::*, LintMessage, LintSession, Result, DOCKERFILE_VERSION,
6 FILE_HEADER_COMMENTS,
7};
8
9pub const LINE_SEPARATOR: &str = " \\\n ";
10pub const DEFAULT_FROM: &str = "scratch";
11
12#[derive(Debug, Clone, PartialEq, Default)]
13pub struct GenerationContext {
14 dofigen: Dofigen,
15 pub(crate) user: Option<User>,
16 pub(crate) stage_name: String,
17 pub(crate) default_from: FromContext,
18 state_stack: Vec<GenerationContextState>,
19 pub(crate) lint_session: LintSession,
20}
21
22impl GenerationContext {
23 pub fn get_lint_messages(&self) -> Vec<LintMessage> {
24 self.lint_session.messages()
25 }
26
27 fn push_state(&mut self, state: GenerationContextState) {
28 let mut prev_state = GenerationContextState::default();
29 if let Some(user) = &state.user {
30 prev_state.user = Some(self.user.clone());
31 self.user = user.clone();
32 }
33 if let Some(stage_name) = &state.stage_name {
34 prev_state.stage_name = Some(self.stage_name.clone());
35 self.stage_name = stage_name.clone();
36 }
37 if let Some(default_from) = &state.default_from {
38 prev_state.default_from = Some(self.default_from.clone());
39 self.default_from = default_from.clone();
40 }
41 self.state_stack.push(prev_state);
42 }
43
44 fn pop_state(&mut self) {
45 let prev_state = self.state_stack.pop().expect("The state stack is empty");
46 if let Some(user) = prev_state.user {
47 self.user = user;
48 }
49 if let Some(stage_name) = prev_state.stage_name {
50 self.stage_name = stage_name;
51 }
52 if let Some(default_from) = prev_state.default_from {
53 self.default_from = default_from;
54 }
55 }
56
57 pub fn from(dofigen: Dofigen) -> Self {
58 let lint_session = LintSession::analyze(&dofigen);
59 Self {
60 dofigen,
61 lint_session,
62 ..Default::default()
63 }
64 }
65
66 pub fn generate_dockerfile(&mut self) -> Result<String> {
67 let mut lines = self.dofigen.clone().generate_dockerfile_lines(self)?;
68 let mut line_number = 1;
69
70 for line in FILE_HEADER_COMMENTS {
71 lines.insert(line_number, DockerfileLine::Comment(line.to_string()));
72 line_number += 1;
73 }
74
75 Ok(format!(
76 "{}\n",
77 lines
78 .iter()
79 .map(DockerfileLine::generate_content)
80 .collect::<Vec<String>>()
81 .join("\n")
82 ))
83 }
84
85 pub fn generate_dockerignore(&self) -> Result<String> {
86 let mut content = String::new();
87
88 for line in FILE_HEADER_COMMENTS {
89 content.push_str("# ");
90 content.push_str(line);
91 content.push_str("\n");
92 }
93 content.push_str("\n");
94
95 if !self.dofigen.context.is_empty() {
96 content.push_str("**\n");
97 self.dofigen.context.iter().for_each(|path| {
98 content.push_str("!");
99 content.push_str(path);
100 content.push_str("\n");
101 });
102 }
103 if !self.dofigen.ignore.is_empty() {
104 self.dofigen.ignore.iter().for_each(|path| {
105 content.push_str(path);
106 content.push_str("\n");
107 });
108 }
109 Ok(content)
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Default)]
114pub struct GenerationContextState {
115 user: Option<Option<User>>,
116 stage_name: Option<String>,
117 default_from: Option<FromContext>,
118}
119
120pub trait DockerfileGenerator {
121 fn generate_dockerfile_lines(
122 &self,
123 context: &mut GenerationContext,
124 ) -> Result<Vec<DockerfileLine>>;
125}
126
127impl Dofigen {
128 pub fn get_base_image(&self) -> Option<ImageName> {
129 let mut stage = &self.stage;
130 while let FromContext::FromBuilder(builder_name) = &stage.from {
131 if let Some(builder) = self.builders.get(builder_name) {
132 stage = builder;
133 } else {
134 return None;
135 }
136 }
137 match &stage.from {
138 FromContext::FromImage(image) => Some(image.clone()),
139 FromContext::FromContext(_) => None,
141 FromContext::FromBuilder(_) => unreachable!(),
142 }
143 }
144}
145
146impl Stage {
147 pub fn from(&self, context: &GenerationContext) -> FromContext {
148 match &self.from {
149 FromContext::FromImage(image) => FromContext::FromImage(image.clone()),
150 FromContext::FromBuilder(builder) => FromContext::FromBuilder(builder.clone()),
151 FromContext::FromContext(Some(context)) => {
152 FromContext::FromContext(Some(context.clone()))
153 }
154 _ => match &context.default_from {
155 FromContext::FromImage(image) => FromContext::FromImage(image.clone()),
156 FromContext::FromBuilder(builder) => FromContext::FromBuilder(builder.clone()),
157 FromContext::FromContext(context) => {
158 FromContext::FromContext(context.clone().or(Some(DEFAULT_FROM.to_string())))
159 }
160 },
161 }
162 }
163
164 pub fn user(&self, context: &GenerationContext) -> Option<User> {
165 self.user.clone().or(context.user.clone())
166 }
167}
168
169impl Run {
170 pub fn is_empty(&self) -> bool {
171 self.run.is_empty()
172 }
173}
174
175impl User {
176 pub fn uid(&self) -> Option<u16> {
177 self.user.parse::<u16>().ok()
178 }
179
180 pub fn gid(&self) -> Option<u16> {
181 self.group
182 .as_ref()
183 .map(|group| group.parse::<u16>().ok())
184 .flatten()
185 }
186
187 pub fn into(&self) -> String {
188 let name = self.user.clone();
189 match &self.group {
190 Some(group) => format!("{}:{}", name, group),
191 _ => name,
192 }
193 }
194
195 pub fn new(user: &str) -> Self {
198 Self {
199 user: user.into(),
200 group: Some(user.into()),
201 }
202 }
203
204 pub fn new_without_group(user: &str) -> Self {
205 Self {
206 user: user.into(),
207 group: None,
208 }
209 }
210}
211
212impl ToString for ImageName {
213 fn to_string(&self) -> String {
214 let mut registry = String::new();
215 if let Some(host) = &self.host {
216 registry.push_str(host);
217 if let Some(port) = self.port.clone() {
218 if port != DEFAULT_PORT {
219 registry.push_str(":");
220 registry.push_str(port.to_string().as_str());
221 }
222 }
223 registry.push_str("/");
224 }
225 let mut version = String::new();
226 match &self.version {
227 Some(ImageVersion::Tag(tag)) => {
228 version.push_str(":");
229 version.push_str(tag);
230 }
231 Some(ImageVersion::Digest(digest)) => {
232 version.push_str("@");
233 version.push_str(digest);
234 }
235 _ => {}
236 }
237 format!(
238 "{registry}{path}{version}",
239 path = self.path,
240 registry = registry,
241 version = version
242 )
243 }
244}
245
246impl ToString for User {
247 fn to_string(&self) -> String {
248 let mut chown = String::new();
249 chown.push_str(self.user.as_str());
250 if let Some(group) = &self.group {
251 chown.push_str(":");
252 chown.push_str(group);
253 }
254 chown
255 }
256}
257
258impl ToString for Port {
259 fn to_string(&self) -> String {
260 match &self.protocol {
261 Some(protocol) => {
262 format!(
263 "{port}/{protocol}",
264 port = self.port,
265 protocol = protocol.to_string()
266 )
267 }
268 _ => self.port.to_string(),
269 }
270 }
271}
272
273impl ToString for PortProtocol {
274 fn to_string(&self) -> String {
275 match self {
276 PortProtocol::Tcp => "tcp".into(),
277 PortProtocol::Udp => "udp".into(),
278 }
279 }
280}
281
282impl ToString for Resource {
283 fn to_string(&self) -> String {
284 match self {
285 Resource::File(file) => file.to_string_lossy().to_string(),
286 Resource::Url(url) => url.to_string(),
287 }
288 }
289}
290
291impl ToString for CacheSharing {
292 fn to_string(&self) -> String {
293 match self {
294 CacheSharing::Shared => "shared".into(),
295 CacheSharing::Private => "private".into(),
296 CacheSharing::Locked => "locked".into(),
297 }
298 }
299}
300
301impl ToString for FromContext {
302 fn to_string(&self) -> String {
303 match self {
304 FromContext::FromBuilder(name) => name.clone(),
305 FromContext::FromImage(image) => image.to_string(),
306 FromContext::FromContext(context) => context.clone().unwrap_or_default(),
307 }
308 }
309}
310
311impl DockerfileGenerator for CopyResource {
312 fn generate_dockerfile_lines(
313 &self,
314 context: &mut GenerationContext,
315 ) -> Result<Vec<DockerfileLine>> {
316 match self {
317 CopyResource::Copy(copy) => copy.generate_dockerfile_lines(context),
318 CopyResource::Content(content) => content.generate_dockerfile_lines(context),
319 CopyResource::Add(add_web_file) => add_web_file.generate_dockerfile_lines(context),
320 CopyResource::AddGitRepo(add_git_repo) => {
321 add_git_repo.generate_dockerfile_lines(context)
322 }
323 }
324 }
325}
326
327fn add_copy_options(
328 inst_options: &mut Vec<InstructionOption>,
329 copy_options: &CopyOptions,
330 context: &GenerationContext,
331) {
332 if let Some(chown) = copy_options.chown.as_ref().or(context.user.as_ref().into()) {
333 inst_options.push(InstructionOption::WithValue("chown".into(), chown.into()));
334 }
335 if let Some(chmod) = ©_options.chmod {
336 inst_options.push(InstructionOption::WithValue("chmod".into(), chmod.into()));
337 }
338 if *copy_options.link.as_ref().unwrap_or(&true) {
339 inst_options.push(InstructionOption::Flag("link".into()));
340 }
341}
342
343impl DockerfileGenerator for Copy {
344 fn generate_dockerfile_lines(
345 &self,
346 context: &mut GenerationContext,
347 ) -> Result<Vec<DockerfileLine>> {
348 let mut options: Vec<InstructionOption> = vec![];
349
350 let from = match &self.from {
351 FromContext::FromImage(image) => Some(image.to_string()),
352 FromContext::FromBuilder(builder) => Some(builder.clone()),
353 FromContext::FromContext(context) => context.clone(),
354 };
355 if let Some(from) = from {
356 options.push(InstructionOption::WithValue("from".into(), from));
357 }
358 add_copy_options(&mut options, &self.options, context);
359
360 for path in self.exclude.iter() {
361 options.push(InstructionOption::WithValue("exclude".into(), path.clone()));
362 }
363
364 if self.parents.unwrap_or(false) {
365 options.push(InstructionOption::Flag("parents".into()));
366 }
367
368 Ok(vec![DockerfileLine::Instruction(DockerfileInsctruction {
369 command: "COPY".into(),
370 content: copy_paths_into(self.paths.to_vec(), &self.options.target),
371 options,
372 })])
373 }
374}
375
376impl DockerfileGenerator for CopyContent {
377 fn generate_dockerfile_lines(
378 &self,
379 context: &mut GenerationContext,
380 ) -> Result<Vec<DockerfileLine>> {
381 let mut options: Vec<InstructionOption> = vec![];
382
383 add_copy_options(&mut options, &self.options, context);
384
385 let mut start_delimiter = "EOF".to_string();
386 if !self.substitute.clone().unwrap_or(true) {
387 start_delimiter = format!("\"{start_delimiter}\"");
388 }
389 let target = self.options.target.clone().ok_or(Error::Custom(
390 "The target file must be defined when coying content".into(),
391 ))?;
392 let content = format!(
393 "<<{start_delimiter} {target}\n{}\nEOF",
394 self.content.clone()
395 );
396
397 Ok(vec![DockerfileLine::Instruction(DockerfileInsctruction {
398 command: "COPY".into(),
399 content,
400 options,
401 })])
402 }
403}
404
405impl DockerfileGenerator for Add {
406 fn generate_dockerfile_lines(
407 &self,
408 context: &mut GenerationContext,
409 ) -> Result<Vec<DockerfileLine>> {
410 let mut options: Vec<InstructionOption> = vec![];
411 if let Some(checksum) = &self.checksum {
412 options.push(InstructionOption::WithValue(
413 "checksum".into(),
414 checksum.into(),
415 ));
416 }
417 add_copy_options(&mut options, &self.options, context);
418
419 Ok(vec![DockerfileLine::Instruction(DockerfileInsctruction {
420 command: "ADD".into(),
421 content: copy_paths_into(
422 self.files
423 .iter()
424 .map(|file| file.to_string())
425 .collect::<Vec<String>>(),
426 &self.options.target,
427 ),
428 options,
429 })])
430 }
431}
432
433impl DockerfileGenerator for AddGitRepo {
434 fn generate_dockerfile_lines(
435 &self,
436 context: &mut GenerationContext,
437 ) -> Result<Vec<DockerfileLine>> {
438 let mut options: Vec<InstructionOption> = vec![];
439 add_copy_options(&mut options, &self.options, context);
440
441 for path in self.exclude.iter() {
442 options.push(InstructionOption::WithValue("exclude".into(), path.clone()));
443 }
444 if let Some(keep_git_dir) = &self.keep_git_dir {
445 options.push(InstructionOption::WithValue(
446 "keep-git-dir".into(),
447 keep_git_dir.to_string(),
448 ));
449 }
450
451 Ok(vec![DockerfileLine::Instruction(DockerfileInsctruction {
452 command: "ADD".into(),
453 content: copy_paths_into(vec![self.repo.clone()], &self.options.target),
454 options,
455 })])
456 }
457}
458
459impl DockerfileGenerator for Dofigen {
460 fn generate_dockerfile_lines(
461 &self,
462 context: &mut GenerationContext,
463 ) -> Result<Vec<DockerfileLine>> {
464 context.push_state(GenerationContextState {
465 default_from: Some(self.stage.from(context).clone()),
466 ..Default::default()
467 });
468 let mut lines = vec![DockerfileLine::Comment(format!(
469 "syntax=docker/dockerfile:{}",
470 DOCKERFILE_VERSION
471 ))];
472
473 for name in context.lint_session.get_sorted_builders() {
474 context.push_state(GenerationContextState {
475 stage_name: Some(name.clone()),
476 ..Default::default()
477 });
478 let builder = self
479 .builders
480 .get(&name)
481 .expect(format!("The builder '{}' not found", name).as_str());
482
483 lines.push(DockerfileLine::Empty);
484 lines.append(&mut Stage::generate_dockerfile_lines(builder, context)?);
485 context.pop_state();
486 }
487
488 context.push_state(GenerationContextState {
489 user: Some(Some(User::new("1000"))),
490 stage_name: Some("runtime".into()),
491 default_from: Some(FromContext::default()),
492 });
493 lines.push(DockerfileLine::Empty);
494 lines.append(&mut self.stage.generate_dockerfile_lines(context)?);
495 context.pop_state();
496
497 self.volume.iter().for_each(|volume| {
498 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
499 command: "VOLUME".into(),
500 content: volume.clone(),
501 options: vec![],
502 }))
503 });
504
505 self.expose.iter().for_each(|port| {
506 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
507 command: "EXPOSE".into(),
508 content: port.to_string(),
509 options: vec![],
510 }))
511 });
512 if let Some(healthcheck) = &self.healthcheck {
513 let mut options = vec![];
514 if let Some(interval) = &healthcheck.interval {
515 options.push(InstructionOption::WithValue(
516 "interval".into(),
517 interval.into(),
518 ));
519 }
520 if let Some(timeout) = &healthcheck.timeout {
521 options.push(InstructionOption::WithValue(
522 "timeout".into(),
523 timeout.into(),
524 ));
525 }
526 if let Some(start_period) = &healthcheck.start {
527 options.push(InstructionOption::WithValue(
528 "start-period".into(),
529 start_period.into(),
530 ));
531 }
532 if let Some(retries) = &healthcheck.retries {
533 options.push(InstructionOption::WithValue(
534 "retries".into(),
535 retries.to_string(),
536 ));
537 }
538 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
539 command: "HEALTHCHECK".into(),
540 content: format!("CMD {}", healthcheck.cmd.clone()),
541 options,
542 }))
543 }
544 if !self.entrypoint.is_empty() {
545 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
546 command: "ENTRYPOINT".into(),
547 content: string_vec_into(self.entrypoint.to_vec()),
548 options: vec![],
549 }))
550 }
551 if !self.cmd.is_empty() {
552 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
553 command: "CMD".into(),
554 content: string_vec_into(self.cmd.to_vec()),
555 options: vec![],
556 }))
557 }
558 Ok(lines)
559 }
560}
561
562impl DockerfileGenerator for Stage {
563 fn generate_dockerfile_lines(
564 &self,
565 context: &mut GenerationContext,
566 ) -> Result<Vec<DockerfileLine>> {
567 context.push_state(GenerationContextState {
568 user: Some(self.user(context)),
569 ..Default::default()
570 });
571 let stage_name = context.stage_name.clone();
572
573 let mut lines = vec![
575 DockerfileLine::Comment(stage_name.clone()),
576 DockerfileLine::Instruction(DockerfileInsctruction {
577 command: "FROM".into(),
578 content: format!(
579 "{image_name} AS {stage_name}",
580 image_name = self.from(context).to_string()
581 ),
582 options: vec![],
583 }),
584 ];
585
586 if !self.arg.is_empty() {
588 let mut keys = self.arg.keys().collect::<Vec<&String>>();
589 keys.sort();
590 keys.iter().for_each(|key| {
591 let value = self.arg.get(*key).unwrap();
592 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
593 command: "ARG".into(),
594 content: if value.is_empty() {
595 key.to_string()
596 } else {
597 format!("{}={}", key, value)
598 },
599 options: vec![],
600 }));
601 });
602 }
603
604 if !self.label.is_empty() {
606 let mut keys = self.label.keys().collect::<Vec<&String>>();
607 keys.sort();
608 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
609 command: "LABEL".into(),
610 content: keys
611 .iter()
612 .map(|&key| {
613 format!(
614 "{}=\"{}\"",
615 key,
616 self.label.get(key).unwrap().replace("\n", "\\\n")
617 )
618 })
619 .collect::<Vec<String>>()
620 .join(LINE_SEPARATOR),
621 options: vec![],
622 }));
623 }
624
625 if !self.env.is_empty() {
627 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
628 command: "ENV".into(),
629 content: self
630 .env
631 .iter()
632 .map(|(key, value)| format!("{}=\"{}\"", key, value))
633 .collect::<Vec<String>>()
634 .join(LINE_SEPARATOR),
635 options: vec![],
636 }));
637 }
638
639 if let Some(workdir) = &self.workdir {
641 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
642 command: "WORKDIR".into(),
643 content: workdir.clone(),
644 options: vec![],
645 }));
646 }
647
648 for copy in self.copy.iter() {
650 lines.append(&mut copy.generate_dockerfile_lines(context)?);
651 }
652
653 if let Some(root) = &self.root {
655 if !root.is_empty() {
656 let root_user = User::new("0");
657 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
659 command: "USER".into(),
660 content: root_user.to_string(),
661 options: vec![],
662 }));
663
664 context.push_state(GenerationContextState {
665 user: Some(Some(root_user)),
666 ..Default::default()
667 });
668 lines.append(&mut root.generate_dockerfile_lines(context)?);
670 context.pop_state();
671 }
672 }
673
674 if let Some(user) = self.user(context) {
676 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
677 command: "USER".into(),
678 content: user.to_string(),
679 options: vec![],
680 }));
681 }
682
683 lines.append(&mut self.run.generate_dockerfile_lines(context)?);
685
686 context.pop_state();
687
688 Ok(lines)
689 }
690}
691
692impl DockerfileGenerator for Run {
693 fn generate_dockerfile_lines(
694 &self,
695 context: &mut GenerationContext,
696 ) -> Result<Vec<DockerfileLine>> {
697 let script = &self.run;
698 if script.is_empty() {
699 return Ok(vec![]);
700 }
701 let script_lines = script
702 .iter()
703 .flat_map(|command| command.lines())
704 .collect::<Vec<&str>>();
705 let content = match script_lines.len() {
706 0 => {
707 return Ok(vec![]);
708 }
709 1 => script_lines[0].into(),
710 _ => format!("<<EOF\n{}\nEOF", script_lines.join("\n")),
711 };
712 let mut options = vec![];
713
714 self.bind.iter().for_each(|bind| {
716 let mut bind_options = vec![
717 InstructionOptionOption::new("type", "bind".into()),
718 InstructionOptionOption::new("target", bind.target.clone()),
719 ];
720 let from = match &bind.from {
721 FromContext::FromImage(image) => Some(image.to_string()),
722 FromContext::FromBuilder(builder) => Some(builder.clone()),
723 FromContext::FromContext(context) => context.clone(),
724 };
725 if let Some(from) = from {
726 bind_options.push(InstructionOptionOption::new("from", from));
727 }
728 if let Some(source) = bind.source.as_ref() {
729 bind_options.push(InstructionOptionOption::new("source", source.clone()));
730 }
731 if bind.readwrite.unwrap_or(false) {
732 bind_options.push(InstructionOptionOption::new_flag("readwrite"));
733 }
734 options.push(InstructionOption::WithOptions("mount".into(), bind_options));
735 });
736
737 for cache in self.cache.iter() {
739 let target = cache.target.clone();
740
741 let mut cache_options = vec![
742 InstructionOptionOption::new("type", "cache".into()),
743 InstructionOptionOption::new("target", target),
744 ];
745 if let Some(id) = cache.id.as_ref() {
746 cache_options.push(InstructionOptionOption::new("id", id.clone()));
747 }
748 let from = match &cache.from {
749 FromContext::FromImage(image) => Some(image.to_string()),
750 FromContext::FromBuilder(builder) => Some(builder.clone()),
751 FromContext::FromContext(context) => context.clone(),
752 };
753 if let Some(from) = from {
754 cache_options.push(InstructionOptionOption::new("from", from));
755 if let Some(source) = cache.source.as_ref() {
756 cache_options.push(InstructionOptionOption::new("source", source.clone()));
757 }
758 }
759 if let Some(user) = cache.chown.as_ref().or(context.user.as_ref()) {
760 if let Some(uid) = user.uid() {
761 cache_options.push(InstructionOptionOption::new("uid", uid.to_string()));
762 }
763 if let Some(gid) = user.gid() {
764 cache_options.push(InstructionOptionOption::new("gid", gid.to_string()));
765 }
766 }
767 if let Some(chmod) = cache.chmod.as_ref() {
768 cache_options.push(InstructionOptionOption::new("chmod", chmod.clone()));
769 }
770 cache_options.push(InstructionOptionOption::new(
771 "sharing",
772 cache.sharing.clone().unwrap_or_default().to_string(),
773 ));
774 if cache.readonly.unwrap_or(false) {
775 cache_options.push(InstructionOptionOption::new_flag("readonly"));
776 }
777
778 options.push(InstructionOption::WithOptions(
779 "mount".into(),
780 cache_options,
781 ));
782 }
783
784 Ok(vec![DockerfileLine::Instruction(DockerfileInsctruction {
785 command: "RUN".into(),
786 content,
787 options,
788 })])
789 }
790}
791
792fn copy_paths_into(paths: Vec<String>, target: &Option<String>) -> String {
793 let mut parts = paths.clone();
794 parts.push(target.clone().unwrap_or("./".into()));
795 parts
796 .iter()
797 .map(|p| format!("\"{}\"", p))
798 .collect::<Vec<String>>()
799 .join(" ")
800}
801
802fn string_vec_into(string_vec: Vec<String>) -> String {
803 format!(
804 "[{}]",
805 string_vec
806 .iter()
807 .map(|s| format!("\"{}\"", s))
808 .collect::<Vec<String>>()
809 .join(", ")
810 )
811}
812
813#[cfg(test)]
814mod test {
815 use super::*;
816 use pretty_assertions_sorted::assert_eq_sorted;
817
818 mod stage {
819 use std::collections::HashMap;
820
821 use super::*;
822
823 #[test]
824 fn user_with_user() {
825 let stage = Stage {
826 user: Some(User::new_without_group("my-user").into()),
827 ..Default::default()
828 };
829 let user = stage.user(&GenerationContext::default());
830 assert_eq_sorted!(
831 user,
832 Some(User {
833 user: "my-user".into(),
834 group: None,
835 })
836 );
837 }
838
839 #[test]
840 fn user_without_user() {
841 let stage = Stage::default();
842 let user = stage.user(&GenerationContext::default());
843 assert_eq_sorted!(user, None);
844 }
845
846 #[test]
847 fn stage_args() {
848 let stage = Stage {
849 arg: HashMap::from([("arg2".into(), "".into()), ("arg1".into(), "value1".into())]),
850 ..Default::default()
851 };
852
853 let lines = stage.generate_dockerfile_lines(&mut GenerationContext {
854 stage_name: "test".into(),
855 ..Default::default()
856 });
857
858 assert_eq_sorted!(
859 lines.unwrap(),
860 vec![
861 DockerfileLine::Comment("test".into()),
862 DockerfileLine::Instruction(DockerfileInsctruction {
863 command: "FROM".into(),
864 content: "scratch AS test".into(),
865 options: vec![],
866 }),
867 DockerfileLine::Instruction(DockerfileInsctruction {
868 command: "ARG".into(),
869 content: "arg1=value1".into(),
870 options: vec![],
871 }),
872 DockerfileLine::Instruction(DockerfileInsctruction {
873 command: "ARG".into(),
874 content: "arg2".into(),
875 options: vec![],
876 }),
877 ]
878 );
879 }
880 }
881
882 mod copy {
883 use super::*;
884
885 #[test]
886 fn with_chmod() {
887 let copy = Copy {
888 paths: vec!["/path/to/file".into()],
889 options: CopyOptions {
890 target: Some("/app/".into()),
891 chmod: Some("755".into()),
892 ..Default::default()
893 },
894 ..Default::default()
895 };
896
897 let lines = copy
898 .generate_dockerfile_lines(&mut GenerationContext::default())
899 .unwrap();
900
901 assert_eq_sorted!(
902 lines,
903 vec![DockerfileLine::Instruction(DockerfileInsctruction {
904 command: "COPY".into(),
905 content: "\"/path/to/file\" \"/app/\"".into(),
906 options: vec![
907 InstructionOption::WithValue("chmod".into(), "755".into()),
908 InstructionOption::Flag("link".into())
909 ],
910 })]
911 );
912 }
913
914 #[test]
915 fn from_content() {
916 let copy = CopyContent {
917 content: "echo hello".into(),
918 options: CopyOptions {
919 target: Some("test.sh".into()),
920 ..Default::default()
921 },
922 ..Default::default()
923 };
924
925 let lines = copy
926 .generate_dockerfile_lines(&mut GenerationContext::default())
927 .unwrap();
928
929 assert_eq_sorted!(
930 lines,
931 vec![DockerfileLine::Instruction(DockerfileInsctruction {
932 command: "COPY".into(),
933 content: "<<EOF test.sh\necho hello\nEOF".into(),
934 options: vec![InstructionOption::Flag("link".into())],
935 })]
936 );
937 }
938 }
939
940 mod image_name {
941 use super::*;
942
943 #[test]
944 fn user_with_user() {
945 let dofigen = Dofigen {
946 stage: Stage {
947 user: Some(User::new_without_group("my-user").into()),
948 from: FromContext::FromImage(ImageName {
949 path: String::from("my-image"),
950 ..Default::default()
951 }),
952 ..Default::default()
953 },
954 ..Default::default()
955 };
956 let user = dofigen.stage.user(&GenerationContext {
957 user: Some(User::new("1000")),
958 ..Default::default()
959 });
960 assert_eq_sorted!(
961 user,
962 Some(User {
963 user: String::from("my-user"),
964 group: None,
965 })
966 );
967 }
968
969 #[test]
970 fn user_without_user() {
971 let dofigen = Dofigen {
972 stage: Stage {
973 from: FromContext::FromImage(ImageName {
974 path: String::from("my-image"),
975 ..Default::default()
976 }),
977 ..Default::default()
978 },
979 ..Default::default()
980 };
981 let user = dofigen.stage.user(&GenerationContext {
982 user: Some(User::new("1000")),
983 ..Default::default()
984 });
985 assert_eq_sorted!(
986 user,
987 Some(User {
988 user: String::from("1000"),
989 group: Some(String::from("1000")),
990 })
991 );
992 }
993 }
994
995 mod run {
996 use super::*;
997
998 #[test]
999 fn simple() {
1000 let builder = Run {
1001 run: vec!["echo Hello".into()].into(),
1002 ..Default::default()
1003 };
1004 assert_eq_sorted!(
1005 builder
1006 .generate_dockerfile_lines(&mut GenerationContext::default())
1007 .unwrap(),
1008 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1009 command: "RUN".into(),
1010 content: "echo Hello".into(),
1011 options: vec![],
1012 })]
1013 );
1014 }
1015
1016 #[test]
1017 fn without_run() {
1018 let builder = Run {
1019 ..Default::default()
1020 };
1021 assert_eq_sorted!(
1022 builder
1023 .generate_dockerfile_lines(&mut GenerationContext::default())
1024 .unwrap(),
1025 vec![]
1026 );
1027 }
1028
1029 #[test]
1030 fn with_empty_run() {
1031 let builder = Run {
1032 run: vec![].into(),
1033 ..Default::default()
1034 };
1035 assert_eq_sorted!(
1036 builder
1037 .generate_dockerfile_lines(&mut GenerationContext::default())
1038 .unwrap(),
1039 vec![]
1040 );
1041 }
1042
1043 #[test]
1044 fn with_script_and_caches_with_named_user() {
1045 let builder = Run {
1046 run: vec!["echo Hello".into()].into(),
1047 cache: vec![Cache {
1048 target: "/path/to/cache".into(),
1049 readonly: Some(true),
1050 ..Default::default()
1051 }]
1052 .into(),
1053 ..Default::default()
1054 };
1055 let mut context = GenerationContext {
1056 user: Some(User::new("test")),
1057 ..Default::default()
1058 };
1059 assert_eq_sorted!(
1060 builder.generate_dockerfile_lines(&mut context).unwrap(),
1061 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1062 command: "RUN".into(),
1063 content: "echo Hello".into(),
1064 options: vec![InstructionOption::WithOptions(
1065 "mount".into(),
1066 vec![
1067 InstructionOptionOption::new("type", "cache".into()),
1068 InstructionOptionOption::new("target", "/path/to/cache".into()),
1069 InstructionOptionOption::new("sharing", "locked".into()),
1070 InstructionOptionOption::new_flag("readonly"),
1071 ],
1072 )],
1073 })]
1074 );
1075 }
1076
1077 #[test]
1078 fn with_script_and_caches_with_uid_user() {
1079 let builder = Run {
1080 run: vec!["echo Hello".into()].into(),
1081 cache: vec![Cache {
1082 target: "/path/to/cache".into(),
1083 ..Default::default()
1084 }],
1085 ..Default::default()
1086 };
1087 let mut context = GenerationContext {
1088 user: Some(User::new("1000")),
1089 ..Default::default()
1090 };
1091 assert_eq_sorted!(
1092 builder.generate_dockerfile_lines(&mut context).unwrap(),
1093 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1094 command: "RUN".into(),
1095 content: "echo Hello".into(),
1096 options: vec![InstructionOption::WithOptions(
1097 "mount".into(),
1098 vec![
1099 InstructionOptionOption::new("type", "cache".into()),
1100 InstructionOptionOption::new("target", "/path/to/cache".into()),
1101 InstructionOptionOption::new("uid", "1000".into()),
1102 InstructionOptionOption::new("gid", "1000".into()),
1103 InstructionOptionOption::new("sharing", "locked".into()),
1104 ],
1105 )],
1106 })]
1107 );
1108 }
1109
1110 #[test]
1111 fn with_script_and_caches_with_uid_user_without_group() {
1112 let builder = Run {
1113 run: vec!["echo Hello".into()].into(),
1114 cache: vec![Cache {
1115 target: "/path/to/cache".into(),
1116 ..Default::default()
1117 }],
1118 ..Default::default()
1119 };
1120 let mut context = GenerationContext {
1121 user: Some(User::new_without_group("1000")),
1122 ..Default::default()
1123 };
1124 assert_eq_sorted!(
1125 builder.generate_dockerfile_lines(&mut context).unwrap(),
1126 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1127 command: "RUN".into(),
1128 content: "echo Hello".into(),
1129 options: vec![InstructionOption::WithOptions(
1130 "mount".into(),
1131 vec![
1132 InstructionOptionOption::new("type", "cache".into()),
1133 InstructionOptionOption::new("target", "/path/to/cache".into()),
1134 InstructionOptionOption::new("uid", "1000".into()),
1135 InstructionOptionOption::new("sharing", "locked".into()),
1136 ],
1137 )],
1138 })]
1139 );
1140 }
1141 }
1142
1143 mod label {
1144 use std::collections::HashMap;
1145
1146 use crate::{lock::Lock, DofigenContext};
1147
1148 use super::*;
1149
1150 #[test]
1151 fn with_label() {
1152 let stage = Stage {
1153 label: HashMap::from([("key".into(), "value".into())]),
1154 ..Default::default()
1155 };
1156 let lines = stage
1157 .generate_dockerfile_lines(&mut GenerationContext::default())
1158 .unwrap();
1159 assert_eq_sorted!(
1160 lines[2],
1161 DockerfileLine::Instruction(DockerfileInsctruction {
1162 command: "LABEL".into(),
1163 content: "key=\"value\"".into(),
1164 options: vec![],
1165 })
1166 );
1167 }
1168
1169 #[test]
1170 fn with_many_multiline_labels() {
1171 let stage = Stage {
1172 label: HashMap::from([
1173 ("key1".into(), "value1".into()),
1174 ("key2".into(), "value2\nligne2".into()),
1175 ]),
1176 ..Default::default()
1177 };
1178 let lines = stage
1179 .generate_dockerfile_lines(&mut GenerationContext::default())
1180 .unwrap();
1181 assert_eq_sorted!(
1182 lines[2],
1183 DockerfileLine::Instruction(DockerfileInsctruction {
1184 command: "LABEL".into(),
1185 content: "key1=\"value1\" \\\n key2=\"value2\\\nligne2\"".into(),
1186 options: vec![],
1187 })
1188 );
1189 }
1190
1191 #[test]
1192 fn locked_with_many_multiline_labels() {
1193 let dofigen = Dofigen {
1194 stage: Stage {
1195 label: HashMap::from([
1196 ("key1".into(), "value1".into()),
1197 ("key2".into(), "value2\nligne2".into()),
1198 ]),
1199 ..Default::default()
1200 },
1201 ..Default::default()
1202 };
1203 let dofigen = dofigen.lock(&mut DofigenContext::new()).unwrap();
1204 let lines = dofigen
1205 .generate_dockerfile_lines(&mut GenerationContext::default())
1206 .unwrap();
1207 assert_eq_sorted!(
1208 lines[4],
1209 DockerfileLine::Instruction(DockerfileInsctruction {
1210 command: "LABEL".into(),
1211 content: "io.dofigen.version=\"0.0.0\" \\\n key1=\"value1\" \\\n key2=\"value2\\\nligne2\"".into(),
1212 options: vec![],
1213 })
1214 );
1215 }
1216 }
1217}