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