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 let builder_names = context.lint_session.get_sorted_builders();
474
475 for name in builder_names {
476 context.push_state(GenerationContextState {
477 stage_name: Some(name.clone()),
478 ..Default::default()
479 });
480 let builder = self
481 .builders
482 .get(&name)
483 .expect(format!("The builder '{}' not found", name).as_str());
484
485 lines.push(DockerfileLine::Empty);
486 lines.append(&mut Stage::generate_dockerfile_lines(builder, context)?);
487 context.pop_state();
488 }
489
490 context.push_state(GenerationContextState {
491 user: Some(Some(User::new("1000"))),
492 stage_name: Some("runtime".into()),
493 default_from: Some(FromContext::default()),
494 });
495 lines.push(DockerfileLine::Empty);
496 lines.append(&mut self.stage.generate_dockerfile_lines(context)?);
497 context.pop_state();
498
499 self.volume.iter().for_each(|volume| {
500 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
501 command: "VOLUME".into(),
502 content: volume.clone(),
503 options: vec![],
504 }))
505 });
506
507 self.expose.iter().for_each(|port| {
508 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
509 command: "EXPOSE".into(),
510 content: port.to_string(),
511 options: vec![],
512 }))
513 });
514 if let Some(healthcheck) = &self.healthcheck {
515 let mut options = vec![];
516 if let Some(interval) = &healthcheck.interval {
517 options.push(InstructionOption::WithValue(
518 "interval".into(),
519 interval.into(),
520 ));
521 }
522 if let Some(timeout) = &healthcheck.timeout {
523 options.push(InstructionOption::WithValue(
524 "timeout".into(),
525 timeout.into(),
526 ));
527 }
528 if let Some(start_period) = &healthcheck.start {
529 options.push(InstructionOption::WithValue(
530 "start-period".into(),
531 start_period.into(),
532 ));
533 }
534 if let Some(retries) = &healthcheck.retries {
535 options.push(InstructionOption::WithValue(
536 "retries".into(),
537 retries.to_string(),
538 ));
539 }
540 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
541 command: "HEALTHCHECK".into(),
542 content: format!("CMD {}", healthcheck.cmd.clone()),
543 options,
544 }))
545 }
546 if !self.entrypoint.is_empty() {
547 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
548 command: "ENTRYPOINT".into(),
549 content: string_vec_into(self.entrypoint.to_vec()),
550 options: vec![],
551 }))
552 }
553 if !self.cmd.is_empty() {
554 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
555 command: "CMD".into(),
556 content: string_vec_into(self.cmd.to_vec()),
557 options: vec![],
558 }))
559 }
560 Ok(lines)
561 }
562}
563
564impl DockerfileGenerator for Stage {
565 fn generate_dockerfile_lines(
566 &self,
567 context: &mut GenerationContext,
568 ) -> Result<Vec<DockerfileLine>> {
569 context.push_state(GenerationContextState {
570 user: Some(self.user(context)),
571 ..Default::default()
572 });
573 let stage_name = context.stage_name.clone();
574
575 let mut lines = vec![
577 DockerfileLine::Comment(stage_name.clone()),
578 DockerfileLine::Instruction(DockerfileInsctruction {
579 command: "FROM".into(),
580 content: format!(
581 "{image_name} AS {stage_name}",
582 image_name = self.from(context).to_string()
583 ),
584 options: vec![],
585 }),
586 ];
587
588 if !self.arg.is_empty() {
590 let mut keys = self.arg.keys().collect::<Vec<&String>>();
591 keys.sort();
592 keys.iter().for_each(|key| {
593 let value = self.arg.get(*key).unwrap();
594 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
595 command: "ARG".into(),
596 content: if value.is_empty() {
597 key.to_string()
598 } else {
599 format!("{}={}", key, value)
600 },
601 options: vec![],
602 }));
603 });
604 }
605
606 if !self.label.is_empty() {
608 let mut keys = self.label.keys().collect::<Vec<&String>>();
609 keys.sort();
610 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
611 command: "LABEL".into(),
612 content: keys
613 .iter()
614 .map(|&key| {
615 format!(
616 "{}=\"{}\"",
617 key,
618 self.label.get(key).unwrap().replace("\n", "\\\n")
619 )
620 })
621 .collect::<Vec<String>>()
622 .join(LINE_SEPARATOR),
623 options: vec![],
624 }));
625 }
626
627 if !self.env.is_empty() {
629 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
630 command: "ENV".into(),
631 content: self
632 .env
633 .iter()
634 .map(|(key, value)| format!("{}=\"{}\"", key, value))
635 .collect::<Vec<String>>()
636 .join(LINE_SEPARATOR),
637 options: vec![],
638 }));
639 }
640
641 if let Some(workdir) = &self.workdir {
643 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
644 command: "WORKDIR".into(),
645 content: workdir.clone(),
646 options: vec![],
647 }));
648 }
649
650 for copy in self.copy.iter() {
652 lines.append(&mut copy.generate_dockerfile_lines(context)?);
653 }
654
655 if let Some(root) = &self.root {
657 if !root.is_empty() {
658 let root_user = User::new("0");
659 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
661 command: "USER".into(),
662 content: root_user.to_string(),
663 options: vec![],
664 }));
665
666 context.push_state(GenerationContextState {
667 user: Some(Some(root_user)),
668 ..Default::default()
669 });
670 lines.append(&mut root.generate_dockerfile_lines(context)?);
672 context.pop_state();
673 }
674 }
675
676 if let Some(user) = self.user(context) {
678 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
679 command: "USER".into(),
680 content: user.to_string(),
681 options: vec![],
682 }));
683 }
684
685 lines.append(&mut self.run.generate_dockerfile_lines(context)?);
687
688 context.pop_state();
689
690 Ok(lines)
691 }
692}
693
694impl DockerfileGenerator for Run {
695 fn generate_dockerfile_lines(
696 &self,
697 context: &mut GenerationContext,
698 ) -> Result<Vec<DockerfileLine>> {
699 let script = &self.run;
700 if script.is_empty() {
701 return Ok(vec![]);
702 }
703 let script_lines = script
704 .iter()
705 .flat_map(|command| command.lines())
706 .collect::<Vec<&str>>();
707 let content = match script_lines.len() {
708 0 => {
709 return Ok(vec![]);
710 }
711 1 => script_lines[0].into(),
712 _ => format!("<<EOF\n{}\nEOF", script_lines.join("\n")),
713 };
714 let mut options = vec![];
715
716 self.bind.iter().for_each(|bind| {
718 let mut bind_options = vec![
719 InstructionOptionOption::new("type", "bind".into()),
720 InstructionOptionOption::new("target", bind.target.clone()),
721 ];
722 let from = match &bind.from {
723 FromContext::FromImage(image) => Some(image.to_string()),
724 FromContext::FromBuilder(builder) => Some(builder.clone()),
725 FromContext::FromContext(context) => context.clone(),
726 };
727 if let Some(from) = from {
728 bind_options.push(InstructionOptionOption::new("from", from));
729 }
730 if let Some(source) = bind.source.as_ref() {
731 bind_options.push(InstructionOptionOption::new("source", source.clone()));
732 }
733 if bind.readwrite.unwrap_or(false) {
734 bind_options.push(InstructionOptionOption::new_flag("readwrite"));
735 }
736 options.push(InstructionOption::WithOptions("mount".into(), bind_options));
737 });
738
739 for cache in self.cache.iter() {
741 let target = cache.target.clone();
742
743 let mut cache_options = vec![
744 InstructionOptionOption::new("type", "cache".into()),
745 InstructionOptionOption::new("target", target),
746 ];
747 if let Some(id) = cache.id.as_ref() {
748 cache_options.push(InstructionOptionOption::new("id", id.clone()));
749 }
750 let from = match &cache.from {
751 FromContext::FromImage(image) => Some(image.to_string()),
752 FromContext::FromBuilder(builder) => Some(builder.clone()),
753 FromContext::FromContext(context) => context.clone(),
754 };
755 if let Some(from) = from {
756 cache_options.push(InstructionOptionOption::new("from", from));
757 if let Some(source) = cache.source.as_ref() {
758 cache_options.push(InstructionOptionOption::new("source", source.clone()));
759 }
760 }
761 if let Some(user) = cache.chown.as_ref().or(context.user.as_ref()) {
762 if let Some(uid) = user.uid() {
763 cache_options.push(InstructionOptionOption::new("uid", uid.to_string()));
764 }
765 if let Some(gid) = user.gid() {
766 cache_options.push(InstructionOptionOption::new("gid", gid.to_string()));
767 }
768 }
769 if let Some(chmod) = cache.chmod.as_ref() {
770 cache_options.push(InstructionOptionOption::new("chmod", chmod.clone()));
771 }
772 cache_options.push(InstructionOptionOption::new(
773 "sharing",
774 cache.sharing.clone().unwrap_or_default().to_string(),
775 ));
776 if cache.readonly.unwrap_or(false) {
777 cache_options.push(InstructionOptionOption::new_flag("readonly"));
778 }
779
780 options.push(InstructionOption::WithOptions(
781 "mount".into(),
782 cache_options,
783 ));
784 }
785
786 let mut lines = vec![];
787
788 if !self.shell.is_empty() {
790 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
791 command: "SHELL".into(),
792 content: string_vec_into(self.shell.to_vec()),
793 options: vec![],
794 }));
795 }
796
797 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
798 command: "RUN".into(),
799 content,
800 options,
801 }));
802
803 Ok(lines)
804 }
805}
806
807fn copy_paths_into(paths: Vec<String>, target: &Option<String>) -> String {
808 let mut parts = paths.clone();
809 parts.push(target.clone().unwrap_or("./".into()));
810 parts
811 .iter()
812 .map(|p| format!("\"{}\"", p))
813 .collect::<Vec<String>>()
814 .join(" ")
815}
816
817fn string_vec_into(string_vec: Vec<String>) -> String {
818 format!(
819 "[{}]",
820 string_vec
821 .iter()
822 .map(|s| format!("\"{}\"", s))
823 .collect::<Vec<String>>()
824 .join(", ")
825 )
826}
827
828#[cfg(test)]
829mod test {
830 use super::*;
831 use pretty_assertions_sorted::assert_eq_sorted;
832
833 mod stage {
834 use std::collections::HashMap;
835
836 use super::*;
837
838 #[test]
839 fn user_with_user() {
840 let stage = Stage {
841 user: Some(User::new_without_group("my-user").into()),
842 ..Default::default()
843 };
844 let user = stage.user(&GenerationContext::default());
845 assert_eq_sorted!(
846 user,
847 Some(User {
848 user: "my-user".into(),
849 group: None,
850 })
851 );
852 }
853
854 #[test]
855 fn user_without_user() {
856 let stage = Stage::default();
857 let user = stage.user(&GenerationContext::default());
858 assert_eq_sorted!(user, None);
859 }
860
861 #[test]
862 fn stage_args() {
863 let stage = Stage {
864 arg: HashMap::from([("arg2".into(), "".into()), ("arg1".into(), "value1".into())]),
865 ..Default::default()
866 };
867
868 let lines = stage.generate_dockerfile_lines(&mut GenerationContext {
869 stage_name: "test".into(),
870 ..Default::default()
871 });
872
873 assert_eq_sorted!(
874 lines.unwrap(),
875 vec![
876 DockerfileLine::Comment("test".into()),
877 DockerfileLine::Instruction(DockerfileInsctruction {
878 command: "FROM".into(),
879 content: "scratch AS test".into(),
880 options: vec![],
881 }),
882 DockerfileLine::Instruction(DockerfileInsctruction {
883 command: "ARG".into(),
884 content: "arg1=value1".into(),
885 options: vec![],
886 }),
887 DockerfileLine::Instruction(DockerfileInsctruction {
888 command: "ARG".into(),
889 content: "arg2".into(),
890 options: vec![],
891 }),
892 ]
893 );
894 }
895 }
896
897 mod copy {
898 use super::*;
899
900 #[test]
901 fn with_chmod() {
902 let copy = Copy {
903 paths: vec!["/path/to/file".into()],
904 options: CopyOptions {
905 target: Some("/app/".into()),
906 chmod: Some("755".into()),
907 ..Default::default()
908 },
909 ..Default::default()
910 };
911
912 let lines = copy
913 .generate_dockerfile_lines(&mut GenerationContext::default())
914 .unwrap();
915
916 assert_eq_sorted!(
917 lines,
918 vec![DockerfileLine::Instruction(DockerfileInsctruction {
919 command: "COPY".into(),
920 content: "\"/path/to/file\" \"/app/\"".into(),
921 options: vec![
922 InstructionOption::WithValue("chmod".into(), "755".into()),
923 InstructionOption::Flag("link".into())
924 ],
925 })]
926 );
927 }
928
929 #[test]
930 fn from_content() {
931 let copy = CopyContent {
932 content: "echo hello".into(),
933 options: CopyOptions {
934 target: Some("test.sh".into()),
935 ..Default::default()
936 },
937 ..Default::default()
938 };
939
940 let lines = copy
941 .generate_dockerfile_lines(&mut GenerationContext::default())
942 .unwrap();
943
944 assert_eq_sorted!(
945 lines,
946 vec![DockerfileLine::Instruction(DockerfileInsctruction {
947 command: "COPY".into(),
948 content: "<<EOF test.sh\necho hello\nEOF".into(),
949 options: vec![InstructionOption::Flag("link".into())],
950 })]
951 );
952 }
953 }
954
955 mod image_name {
956 use super::*;
957
958 #[test]
959 fn user_with_user() {
960 let dofigen = Dofigen {
961 stage: Stage {
962 user: Some(User::new_without_group("my-user").into()),
963 from: FromContext::FromImage(ImageName {
964 path: String::from("my-image"),
965 ..Default::default()
966 }),
967 ..Default::default()
968 },
969 ..Default::default()
970 };
971 let user = dofigen.stage.user(&GenerationContext {
972 user: Some(User::new("1000")),
973 ..Default::default()
974 });
975 assert_eq_sorted!(
976 user,
977 Some(User {
978 user: String::from("my-user"),
979 group: None,
980 })
981 );
982 }
983
984 #[test]
985 fn user_without_user() {
986 let dofigen = Dofigen {
987 stage: Stage {
988 from: FromContext::FromImage(ImageName {
989 path: String::from("my-image"),
990 ..Default::default()
991 }),
992 ..Default::default()
993 },
994 ..Default::default()
995 };
996 let user = dofigen.stage.user(&GenerationContext {
997 user: Some(User::new("1000")),
998 ..Default::default()
999 });
1000 assert_eq_sorted!(
1001 user,
1002 Some(User {
1003 user: String::from("1000"),
1004 group: Some(String::from("1000")),
1005 })
1006 );
1007 }
1008 }
1009
1010 mod run {
1011 use super::*;
1012
1013 #[test]
1014 fn simple() {
1015 let builder = Run {
1016 run: vec!["echo Hello".into()].into(),
1017 ..Default::default()
1018 };
1019 assert_eq_sorted!(
1020 builder
1021 .generate_dockerfile_lines(&mut GenerationContext::default())
1022 .unwrap(),
1023 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1024 command: "RUN".into(),
1025 content: "echo Hello".into(),
1026 options: vec![],
1027 })]
1028 );
1029 }
1030
1031 #[test]
1032 fn without_run() {
1033 let builder = Run {
1034 ..Default::default()
1035 };
1036 assert_eq_sorted!(
1037 builder
1038 .generate_dockerfile_lines(&mut GenerationContext::default())
1039 .unwrap(),
1040 vec![]
1041 );
1042 }
1043
1044 #[test]
1045 fn with_empty_run() {
1046 let builder = Run {
1047 run: vec![].into(),
1048 ..Default::default()
1049 };
1050 assert_eq_sorted!(
1051 builder
1052 .generate_dockerfile_lines(&mut GenerationContext::default())
1053 .unwrap(),
1054 vec![]
1055 );
1056 }
1057
1058 #[test]
1059 fn with_script_and_caches_with_named_user() {
1060 let builder = Run {
1061 run: vec!["echo Hello".into()].into(),
1062 cache: vec![Cache {
1063 target: "/path/to/cache".into(),
1064 readonly: Some(true),
1065 ..Default::default()
1066 }]
1067 .into(),
1068 ..Default::default()
1069 };
1070 let mut context = GenerationContext {
1071 user: Some(User::new("test")),
1072 ..Default::default()
1073 };
1074 assert_eq_sorted!(
1075 builder.generate_dockerfile_lines(&mut context).unwrap(),
1076 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1077 command: "RUN".into(),
1078 content: "echo Hello".into(),
1079 options: vec![InstructionOption::WithOptions(
1080 "mount".into(),
1081 vec![
1082 InstructionOptionOption::new("type", "cache".into()),
1083 InstructionOptionOption::new("target", "/path/to/cache".into()),
1084 InstructionOptionOption::new("sharing", "locked".into()),
1085 InstructionOptionOption::new_flag("readonly"),
1086 ],
1087 )],
1088 })]
1089 );
1090 }
1091
1092 #[test]
1093 fn with_script_and_caches_with_uid_user() {
1094 let builder = Run {
1095 run: vec!["echo Hello".into()].into(),
1096 cache: vec![Cache {
1097 target: "/path/to/cache".into(),
1098 ..Default::default()
1099 }],
1100 ..Default::default()
1101 };
1102 let mut context = GenerationContext {
1103 user: Some(User::new("1000")),
1104 ..Default::default()
1105 };
1106 assert_eq_sorted!(
1107 builder.generate_dockerfile_lines(&mut context).unwrap(),
1108 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1109 command: "RUN".into(),
1110 content: "echo Hello".into(),
1111 options: vec![InstructionOption::WithOptions(
1112 "mount".into(),
1113 vec![
1114 InstructionOptionOption::new("type", "cache".into()),
1115 InstructionOptionOption::new("target", "/path/to/cache".into()),
1116 InstructionOptionOption::new("uid", "1000".into()),
1117 InstructionOptionOption::new("gid", "1000".into()),
1118 InstructionOptionOption::new("sharing", "locked".into()),
1119 ],
1120 )],
1121 })]
1122 );
1123 }
1124
1125 #[test]
1126 fn with_script_and_caches_with_uid_user_without_group() {
1127 let builder = Run {
1128 run: vec!["echo Hello".into()].into(),
1129 cache: vec![Cache {
1130 target: "/path/to/cache".into(),
1131 ..Default::default()
1132 }],
1133 ..Default::default()
1134 };
1135 let mut context = GenerationContext {
1136 user: Some(User::new_without_group("1000")),
1137 ..Default::default()
1138 };
1139 assert_eq_sorted!(
1140 builder.generate_dockerfile_lines(&mut context).unwrap(),
1141 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1142 command: "RUN".into(),
1143 content: "echo Hello".into(),
1144 options: vec![InstructionOption::WithOptions(
1145 "mount".into(),
1146 vec![
1147 InstructionOptionOption::new("type", "cache".into()),
1148 InstructionOptionOption::new("target", "/path/to/cache".into()),
1149 InstructionOptionOption::new("uid", "1000".into()),
1150 InstructionOptionOption::new("sharing", "locked".into()),
1151 ],
1152 )],
1153 })]
1154 );
1155 }
1156 }
1157
1158 mod label {
1159 use std::collections::HashMap;
1160
1161 use crate::{lock::Lock, DofigenContext};
1162
1163 use super::*;
1164
1165 #[test]
1166 fn with_label() {
1167 let stage = Stage {
1168 label: HashMap::from([("key".into(), "value".into())]),
1169 ..Default::default()
1170 };
1171 let lines = stage
1172 .generate_dockerfile_lines(&mut GenerationContext::default())
1173 .unwrap();
1174 assert_eq_sorted!(
1175 lines[2],
1176 DockerfileLine::Instruction(DockerfileInsctruction {
1177 command: "LABEL".into(),
1178 content: "key=\"value\"".into(),
1179 options: vec![],
1180 })
1181 );
1182 }
1183
1184 #[test]
1185 fn with_many_multiline_labels() {
1186 let stage = Stage {
1187 label: HashMap::from([
1188 ("key1".into(), "value1".into()),
1189 ("key2".into(), "value2\nligne2".into()),
1190 ]),
1191 ..Default::default()
1192 };
1193 let lines = stage
1194 .generate_dockerfile_lines(&mut GenerationContext::default())
1195 .unwrap();
1196 assert_eq_sorted!(
1197 lines[2],
1198 DockerfileLine::Instruction(DockerfileInsctruction {
1199 command: "LABEL".into(),
1200 content: "key1=\"value1\" \\\n key2=\"value2\\\nligne2\"".into(),
1201 options: vec![],
1202 })
1203 );
1204 }
1205
1206 #[test]
1207 fn locked_with_many_multiline_labels() {
1208 let dofigen = Dofigen {
1209 stage: Stage {
1210 label: HashMap::from([
1211 ("key1".into(), "value1".into()),
1212 ("key2".into(), "value2\nligne2".into()),
1213 ]),
1214 ..Default::default()
1215 },
1216 ..Default::default()
1217 };
1218 let dofigen = dofigen.lock(&mut DofigenContext::new()).unwrap();
1219 let lines = dofigen
1220 .generate_dockerfile_lines(&mut GenerationContext::default())
1221 .unwrap();
1222 assert_eq_sorted!(
1223 lines[4],
1224 DockerfileLine::Instruction(DockerfileInsctruction {
1225 command: "LABEL".into(),
1226 content: "io.dofigen.version=\"0.0.0\" \\\n key1=\"value1\" \\\n key2=\"value2\\\nligne2\"".into(),
1227 options: vec![],
1228 })
1229 );
1230 }
1231 }
1232}