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 Network {
304 fn to_string(&self) -> String {
305 match self {
306 Network::Default => "default".into(),
307 Network::None => "none".into(),
308 Network::Host => "host".into(),
309 }
310 }
311}
312
313impl ToString for Security {
314 fn to_string(&self) -> String {
315 match self {
316 Security::Sandbox => "sandbox".into(),
317 Security::Insecure => "insecure".into(),
318 }
319 }
320}
321
322impl ToString for FromContext {
323 fn to_string(&self) -> String {
324 match self {
325 FromContext::FromBuilder(name) => name.clone(),
326 FromContext::FromImage(image) => image.to_string(),
327 FromContext::FromContext(context) => context.clone().unwrap_or_default(),
328 }
329 }
330}
331
332impl DockerfileGenerator for Dofigen {
333 fn generate_dockerfile_lines(
334 &self,
335 context: &mut GenerationContext,
336 ) -> Result<Vec<DockerfileLine>> {
337 context.push_state(GenerationContextState {
338 default_from: Some(self.stage.from(context).clone()),
339 ..Default::default()
340 });
341 let mut lines = vec![DockerfileLine::Comment(format!(
342 "syntax=docker/dockerfile:{}",
343 DOCKERFILE_VERSION
344 ))];
345
346 if !self.global_arg.is_empty() {
348 lines.push(DockerfileLine::Empty);
349 let args = generate_arg_command(&self.global_arg);
350 lines.extend(args);
351 }
352
353 let builder_names = context.lint_session.get_sorted_builders();
354
355 for name in builder_names {
356 context.push_state(GenerationContextState {
357 stage_name: Some(name.clone()),
358 ..Default::default()
359 });
360 let builder = self
361 .builders
362 .get(&name)
363 .expect(format!("The builder '{}' not found", name).as_str());
364
365 lines.push(DockerfileLine::Empty);
366 lines.append(&mut Stage::generate_dockerfile_lines(builder, context)?);
367 context.pop_state();
368 }
369
370 context.push_state(GenerationContextState {
371 user: Some(Some(User::new("1000"))),
372 stage_name: Some("runtime".into()),
373 default_from: Some(FromContext::default()),
374 });
375 lines.push(DockerfileLine::Empty);
376 lines.append(&mut self.stage.generate_dockerfile_lines(context)?);
377 context.pop_state();
378
379 self.volume.iter().for_each(|volume| {
380 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
381 command: "VOLUME".into(),
382 content: volume.clone(),
383 options: vec![],
384 }))
385 });
386
387 self.expose.iter().for_each(|port| {
388 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
389 command: "EXPOSE".into(),
390 content: port.to_string(),
391 options: vec![],
392 }))
393 });
394 if let Some(healthcheck) = &self.healthcheck {
395 let mut options = vec![];
396 if let Some(interval) = &healthcheck.interval {
397 options.push(InstructionOption::WithValue(
398 "interval".into(),
399 interval.into(),
400 ));
401 }
402 if let Some(timeout) = &healthcheck.timeout {
403 options.push(InstructionOption::WithValue(
404 "timeout".into(),
405 timeout.into(),
406 ));
407 }
408 if let Some(start_period) = &healthcheck.start {
409 options.push(InstructionOption::WithValue(
410 "start-period".into(),
411 start_period.into(),
412 ));
413 }
414 if let Some(retries) = &healthcheck.retries {
415 options.push(InstructionOption::WithValue(
416 "retries".into(),
417 retries.to_string(),
418 ));
419 }
420 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
421 command: "HEALTHCHECK".into(),
422 content: format!("CMD {}", healthcheck.cmd.clone()),
423 options,
424 }))
425 }
426 if !self.entrypoint.is_empty() {
427 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
428 command: "ENTRYPOINT".into(),
429 content: string_vec_into(self.entrypoint.to_vec()),
430 options: vec![],
431 }))
432 }
433 if !self.cmd.is_empty() {
434 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
435 command: "CMD".into(),
436 content: string_vec_into(self.cmd.to_vec()),
437 options: vec![],
438 }))
439 }
440 Ok(lines)
441 }
442}
443
444impl DockerfileGenerator for Stage {
445 fn generate_dockerfile_lines(
446 &self,
447 context: &mut GenerationContext,
448 ) -> Result<Vec<DockerfileLine>> {
449 context.push_state(GenerationContextState {
450 user: Some(self.user(context)),
451 ..Default::default()
452 });
453 let stage_name = context.stage_name.clone();
454
455 let mut lines = vec![
457 DockerfileLine::Comment(stage_name.clone()),
458 DockerfileLine::Instruction(DockerfileInsctruction {
459 command: "FROM".into(),
460 content: format!(
461 "{image_name} AS {stage_name}",
462 image_name = self.from(context).to_string()
463 ),
464 options: match &self.from {
465 FromContext::FromImage(ImageName {
466 platform: Some(platform),
467 ..
468 }) => {
469 vec![InstructionOption::WithValue(
470 "platform".into(),
471 platform.clone(),
472 )]
473 }
474 _ => vec![],
475 },
476 }),
477 ];
478
479 if !self.arg.is_empty() {
481 let args = generate_arg_command(&self.arg);
482 lines.extend(args);
483 }
484
485 if !self.label.is_empty() {
487 let mut keys = self.label.keys().collect::<Vec<&String>>();
488 keys.sort();
489 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
490 command: "LABEL".into(),
491 content: keys
492 .iter()
493 .map(|&key| {
494 format!(
495 "{}=\"{}\"",
496 key,
497 self.label.get(key).unwrap().replace("\n", "\\\n")
498 )
499 })
500 .collect::<Vec<String>>()
501 .join(LINE_SEPARATOR),
502 options: vec![],
503 }));
504 }
505
506 if !self.env.is_empty() {
508 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
509 command: "ENV".into(),
510 content: self
511 .env
512 .iter()
513 .map(|(key, value)| format!("{}=\"{}\"", key, value))
514 .collect::<Vec<String>>()
515 .join(LINE_SEPARATOR),
516 options: vec![],
517 }));
518 }
519
520 if let Some(workdir) = &self.workdir {
522 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
523 command: "WORKDIR".into(),
524 content: workdir.clone(),
525 options: vec![],
526 }));
527 }
528
529 for copy in self.copy.iter() {
531 lines.append(&mut copy.generate_dockerfile_lines(context)?);
532 }
533
534 if let Some(root) = &self.root {
536 if !root.is_empty() {
537 let root_user = User::new("0");
538 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
540 command: "USER".into(),
541 content: root_user.to_string(),
542 options: vec![],
543 }));
544
545 context.push_state(GenerationContextState {
546 user: Some(Some(root_user)),
547 ..Default::default()
548 });
549 lines.append(&mut root.generate_dockerfile_lines(context)?);
551 context.pop_state();
552 }
553 }
554
555 if let Some(user) = self.user(context) {
557 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
558 command: "USER".into(),
559 content: user.to_string(),
560 options: vec![],
561 }));
562 }
563
564 lines.append(&mut self.run.generate_dockerfile_lines(context)?);
566
567 context.pop_state();
568
569 Ok(lines)
570 }
571}
572
573impl DockerfileGenerator for Run {
574 fn generate_dockerfile_lines(
575 &self,
576 context: &mut GenerationContext,
577 ) -> Result<Vec<DockerfileLine>> {
578 let script = &self.run;
579 if script.is_empty() {
580 return Ok(vec![]);
581 }
582 let script_lines = script
583 .iter()
584 .flat_map(|command| command.lines())
585 .collect::<Vec<&str>>();
586 let content = match script_lines.len() {
587 0 => {
588 return Ok(vec![]);
589 }
590 1 => script_lines[0].into(),
591 _ => format!("<<EOF\n{}\nEOF", script_lines.join("\n")),
592 };
593 let mut options = vec![];
594
595 self.bind.iter().for_each(|bind| {
597 let mut bind_options = vec![
598 InstructionOptionOption::new("type", "bind".into()),
599 InstructionOptionOption::new("target", bind.target.clone()),
600 ];
601 let from = match &bind.from {
602 FromContext::FromImage(image) => Some(image.to_string()),
603 FromContext::FromBuilder(builder) => Some(builder.clone()),
604 FromContext::FromContext(context) => context.clone(),
605 };
606 if let Some(from) = from {
607 bind_options.push(InstructionOptionOption::new("from", from));
608 }
609 if let Some(source) = bind.source.as_ref() {
610 bind_options.push(InstructionOptionOption::new("source", source.clone()));
611 }
612 if bind.readwrite.unwrap_or(false) {
613 bind_options.push(InstructionOptionOption::new_flag("readwrite"));
614 }
615 options.push(InstructionOption::WithOptions("mount".into(), bind_options));
616 });
617
618 for cache in self.cache.iter() {
620 let target = cache.target.clone();
621
622 let mut cache_options = vec![
623 InstructionOptionOption::new("type", "cache".into()),
624 InstructionOptionOption::new("target", target),
625 ];
626 if let Some(id) = cache.id.as_ref() {
627 cache_options.push(InstructionOptionOption::new("id", id.clone()));
628 }
629 let from = match &cache.from {
630 FromContext::FromImage(image) => Some(image.to_string()),
631 FromContext::FromBuilder(builder) => Some(builder.clone()),
632 FromContext::FromContext(context) => context.clone(),
633 };
634 if let Some(from) = from {
635 cache_options.push(InstructionOptionOption::new("from", from));
636 if let Some(source) = cache.source.as_ref() {
637 cache_options.push(InstructionOptionOption::new("source", source.clone()));
638 }
639 }
640 if let Some(user) = cache.chown.as_ref().or(context.user.as_ref()) {
641 if let Some(uid) = user.uid() {
642 cache_options.push(InstructionOptionOption::new("uid", uid.to_string()));
643 }
644 if let Some(gid) = user.gid() {
645 cache_options.push(InstructionOptionOption::new("gid", gid.to_string()));
646 }
647 }
648 if let Some(chmod) = cache.chmod.as_ref() {
649 cache_options.push(InstructionOptionOption::new("chmod", chmod.clone()));
650 }
651 cache_options.push(InstructionOptionOption::new(
652 "sharing",
653 cache.sharing.clone().unwrap_or_default().to_string(),
654 ));
655 if cache.readonly.unwrap_or(false) {
656 cache_options.push(InstructionOptionOption::new_flag("readonly"));
657 }
658
659 options.push(InstructionOption::WithOptions(
660 "mount".into(),
661 cache_options,
662 ));
663 }
664
665 self.tmpfs.iter().for_each(|mount| {
667 let mut mount_options = vec![
668 InstructionOptionOption::new("type", "tmpfs".into()),
669 InstructionOptionOption::new("target", mount.target.clone()),
670 ];
671 if let Some(size) = mount.size.as_ref() {
672 mount_options.push(InstructionOptionOption::new("size", size.clone()));
673 }
674 options.push(InstructionOption::WithOptions(
675 "mount".into(),
676 mount_options,
677 ));
678 });
679
680 self.secret.iter().for_each(|mount| {
682 let mut mount_options = vec![InstructionOptionOption::new("type", "secret".into())];
683 if let Some(id) = mount.id.as_ref() {
684 mount_options.push(InstructionOptionOption::new("id", id.clone()));
685 }
686 if let Some(target) = mount.target.as_ref() {
687 mount_options.push(InstructionOptionOption::new("target", target.clone()));
688 }
689 if let Some(env) = mount.env.as_ref() {
690 mount_options.push(InstructionOptionOption::new("env", env.clone()));
691 }
692 if let Some(required) = mount.required.as_ref() {
693 mount_options.push(InstructionOptionOption::new(
694 "required",
695 required.to_string(),
696 ));
697 }
698 if let Some(mode) = mount.mode.as_ref() {
699 mount_options.push(InstructionOptionOption::new("mode", mode.to_string()));
700 }
701 if let Some(uid) = mount.uid.as_ref() {
702 mount_options.push(InstructionOptionOption::new("uid", uid.to_string()));
703 }
704 if let Some(gid) = mount.gid.as_ref() {
705 mount_options.push(InstructionOptionOption::new("gid", gid.to_string()));
706 }
707
708 options.push(InstructionOption::WithOptions(
709 "mount".into(),
710 mount_options,
711 ));
712 });
713
714 self.ssh.iter().for_each(|mount| {
716 let mut mount_options = vec![InstructionOptionOption::new("type", "ssh".into())];
717 if let Some(id) = mount.id.as_ref() {
718 mount_options.push(InstructionOptionOption::new("id", id.clone()));
719 }
720 if let Some(target) = mount.target.as_ref() {
721 mount_options.push(InstructionOptionOption::new("target", target.clone()));
722 }
723 if let Some(required) = mount.required.as_ref() {
724 mount_options.push(InstructionOptionOption::new(
725 "required",
726 required.to_string(),
727 ));
728 }
729 if let Some(mode) = mount.mode.as_ref() {
730 mount_options.push(InstructionOptionOption::new("mode", mode.to_string()));
731 }
732 if let Some(uid) = mount.uid.as_ref() {
733 mount_options.push(InstructionOptionOption::new("uid", uid.to_string()));
734 }
735 if let Some(gid) = mount.gid.as_ref() {
736 mount_options.push(InstructionOptionOption::new("gid", gid.to_string()));
737 }
738 options.push(InstructionOption::WithOptions(
739 "mount".into(),
740 mount_options,
741 ));
742 });
743
744 let mut lines = vec![];
745
746 if !self.shell.is_empty() {
748 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
749 command: "SHELL".into(),
750 content: string_vec_into(self.shell.to_vec()),
751 options: vec![],
752 }));
753 }
754
755 if let Some(network) = &self.network {
756 options.push(InstructionOption::WithValue(
757 "network".into(),
758 network.to_string(),
759 ));
760 }
761
762 if let Some(security) = &self.security {
763 options.push(InstructionOption::WithValue(
764 "security".into(),
765 security.to_string(),
766 ));
767 }
768
769 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
770 command: "RUN".into(),
771 content,
772 options,
773 }));
774
775 Ok(lines)
776 }
777}
778
779impl DockerfileGenerator for CopyResource {
780 fn generate_dockerfile_lines(
781 &self,
782 context: &mut GenerationContext,
783 ) -> Result<Vec<DockerfileLine>> {
784 match self {
785 CopyResource::Copy(copy) => copy.generate_dockerfile_lines(context),
786 CopyResource::Content(content) => content.generate_dockerfile_lines(context),
787 CopyResource::Add(add_web_file) => add_web_file.generate_dockerfile_lines(context),
788 CopyResource::AddGitRepo(add_git_repo) => {
789 add_git_repo.generate_dockerfile_lines(context)
790 }
791 }
792 }
793}
794
795fn add_copy_options(
796 inst_options: &mut Vec<InstructionOption>,
797 copy_options: &CopyOptions,
798 context: &GenerationContext,
799) {
800 if let Some(chown) = copy_options.chown.as_ref().or(context.user.as_ref().into()) {
801 inst_options.push(InstructionOption::WithValue("chown".into(), chown.into()));
802 }
803 if let Some(chmod) = ©_options.chmod {
804 inst_options.push(InstructionOption::WithValue("chmod".into(), chmod.into()));
805 }
806 if *copy_options.link.as_ref().unwrap_or(&true) {
807 inst_options.push(InstructionOption::Flag("link".into()));
808 }
809}
810
811impl DockerfileGenerator for Copy {
812 fn generate_dockerfile_lines(
813 &self,
814 context: &mut GenerationContext,
815 ) -> Result<Vec<DockerfileLine>> {
816 let mut options: Vec<InstructionOption> = vec![];
817
818 let from = match &self.from {
819 FromContext::FromImage(image) => Some(image.to_string()),
820 FromContext::FromBuilder(builder) => Some(builder.clone()),
821 FromContext::FromContext(context) => context.clone(),
822 };
823 if let Some(from) = from {
824 options.push(InstructionOption::WithValue("from".into(), from));
825 }
826 add_copy_options(&mut options, &self.options, context);
827
828 for path in self.exclude.iter() {
829 options.push(InstructionOption::WithValue("exclude".into(), path.clone()));
830 }
831
832 if self.parents.unwrap_or(false) {
833 options.push(InstructionOption::Flag("parents".into()));
834 }
835
836 Ok(vec![DockerfileLine::Instruction(DockerfileInsctruction {
837 command: "COPY".into(),
838 content: copy_paths_into(self.paths.to_vec(), &self.options.target),
839 options,
840 })])
841 }
842}
843
844impl DockerfileGenerator for CopyContent {
845 fn generate_dockerfile_lines(
846 &self,
847 context: &mut GenerationContext,
848 ) -> Result<Vec<DockerfileLine>> {
849 let mut options: Vec<InstructionOption> = vec![];
850
851 add_copy_options(&mut options, &self.options, context);
852
853 let mut start_delimiter = "EOF".to_string();
854 if !self.substitute.clone().unwrap_or(true) {
855 start_delimiter = format!("\"{start_delimiter}\"");
856 }
857 let target = self.options.target.clone().ok_or(Error::Custom(
858 "The target file must be defined when coying content".into(),
859 ))?;
860 let content = format!(
861 "<<{start_delimiter} {target}\n{}\nEOF",
862 self.content.clone()
863 );
864
865 Ok(vec![DockerfileLine::Instruction(DockerfileInsctruction {
866 command: "COPY".into(),
867 content,
868 options,
869 })])
870 }
871}
872
873impl DockerfileGenerator for Add {
874 fn generate_dockerfile_lines(
875 &self,
876 context: &mut GenerationContext,
877 ) -> Result<Vec<DockerfileLine>> {
878 let mut options: Vec<InstructionOption> = vec![];
879 if let Some(checksum) = &self.checksum {
880 options.push(InstructionOption::WithValue(
881 "checksum".into(),
882 checksum.into(),
883 ));
884 }
885 if let Some(unpack) = &self.unpack {
886 options.push(InstructionOption::WithValue(
887 "unpack".into(),
888 unpack.to_string(),
889 ));
890 }
891 add_copy_options(&mut options, &self.options, context);
892
893 Ok(vec![DockerfileLine::Instruction(DockerfileInsctruction {
894 command: "ADD".into(),
895 content: copy_paths_into(
896 self.files
897 .iter()
898 .map(|file| file.to_string())
899 .collect::<Vec<String>>(),
900 &self.options.target,
901 ),
902 options,
903 })])
904 }
905}
906
907impl DockerfileGenerator for AddGitRepo {
908 fn generate_dockerfile_lines(
909 &self,
910 context: &mut GenerationContext,
911 ) -> Result<Vec<DockerfileLine>> {
912 let mut options: Vec<InstructionOption> = vec![];
913 add_copy_options(&mut options, &self.options, context);
914
915 for path in self.exclude.iter() {
916 options.push(InstructionOption::WithValue("exclude".into(), path.clone()));
917 }
918 if let Some(keep_git_dir) = &self.keep_git_dir {
919 options.push(InstructionOption::WithValue(
920 "keep-git-dir".into(),
921 keep_git_dir.to_string(),
922 ));
923 }
924 if let Some(checksum) = &self.checksum {
925 options.push(InstructionOption::WithValue(
926 "checksum".into(),
927 checksum.into(),
928 ));
929 }
930
931 Ok(vec![DockerfileLine::Instruction(DockerfileInsctruction {
932 command: "ADD".into(),
933 content: copy_paths_into(vec![self.repo.clone()], &self.options.target),
934 options,
935 })])
936 }
937}
938
939fn copy_paths_into(paths: Vec<String>, target: &Option<String>) -> String {
940 let mut parts = paths.clone();
941 parts.push(target.clone().unwrap_or("./".into()));
942 parts
943 .iter()
944 .map(|p| format!("\"{}\"", p))
945 .collect::<Vec<String>>()
946 .join(" ")
947}
948
949fn string_vec_into(string_vec: Vec<String>) -> String {
950 format!(
951 "[{}]",
952 string_vec
953 .iter()
954 .map(|s| format!("\"{}\"", s))
955 .collect::<Vec<String>>()
956 .join(", ")
957 )
958}
959
960fn generate_arg_command(arg: &HashMap<String, String>) -> Vec<DockerfileLine> {
961 let mut lines = vec![];
962 let mut keys = arg.keys().collect::<Vec<&String>>();
963 keys.sort();
964 keys.iter().for_each(|key| {
965 let value = arg.get(*key).unwrap();
966 lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
967 command: "ARG".into(),
968 content: if value.is_empty() {
969 key.to_string()
970 } else {
971 format!("{}={}", key, value)
972 },
973 options: vec![],
974 }));
975 });
976 lines
977}
978
979#[cfg(test)]
980mod test {
981 use super::*;
982 use pretty_assertions_sorted::assert_eq_sorted;
983
984 mod stage {
985 use std::collections::HashMap;
986
987 use super::*;
988
989 #[test]
990 fn user_with_user() {
991 let stage = Stage {
992 user: Some(User::new_without_group("my-user").into()),
993 ..Default::default()
994 };
995 let user = stage.user(&GenerationContext::default());
996 assert_eq_sorted!(
997 user,
998 Some(User {
999 user: "my-user".into(),
1000 group: None,
1001 })
1002 );
1003 }
1004
1005 #[test]
1006 fn user_without_user() {
1007 let stage = Stage::default();
1008 let user = stage.user(&GenerationContext::default());
1009 assert_eq_sorted!(user, None);
1010 }
1011
1012 #[test]
1013 fn stage_args() {
1014 let stage = Stage {
1015 arg: HashMap::from([("arg2".into(), "".into()), ("arg1".into(), "value1".into())]),
1016 ..Default::default()
1017 };
1018
1019 let lines = stage.generate_dockerfile_lines(&mut GenerationContext {
1020 stage_name: "test".into(),
1021 ..Default::default()
1022 });
1023
1024 assert_eq_sorted!(
1025 lines.unwrap(),
1026 vec![
1027 DockerfileLine::Comment("test".into()),
1028 DockerfileLine::Instruction(DockerfileInsctruction {
1029 command: "FROM".into(),
1030 content: "scratch AS test".into(),
1031 options: vec![],
1032 }),
1033 DockerfileLine::Instruction(DockerfileInsctruction {
1034 command: "ARG".into(),
1035 content: "arg1=value1".into(),
1036 options: vec![],
1037 }),
1038 DockerfileLine::Instruction(DockerfileInsctruction {
1039 command: "ARG".into(),
1040 content: "arg2".into(),
1041 options: vec![],
1042 }),
1043 ]
1044 );
1045 }
1046 }
1047
1048 mod copy {
1049 use super::*;
1050
1051 #[test]
1052 fn with_chmod() {
1053 let copy = Copy {
1054 paths: vec!["/path/to/file".into()],
1055 options: CopyOptions {
1056 target: Some("/app/".into()),
1057 chmod: Some("755".into()),
1058 ..Default::default()
1059 },
1060 ..Default::default()
1061 };
1062
1063 let lines = copy
1064 .generate_dockerfile_lines(&mut GenerationContext::default())
1065 .unwrap();
1066
1067 assert_eq_sorted!(
1068 lines,
1069 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1070 command: "COPY".into(),
1071 content: "\"/path/to/file\" \"/app/\"".into(),
1072 options: vec![
1073 InstructionOption::WithValue("chmod".into(), "755".into()),
1074 InstructionOption::Flag("link".into())
1075 ],
1076 })]
1077 );
1078 }
1079
1080 #[test]
1081 fn from_content() {
1082 let copy = CopyContent {
1083 content: "echo hello".into(),
1084 options: CopyOptions {
1085 target: Some("test.sh".into()),
1086 ..Default::default()
1087 },
1088 ..Default::default()
1089 };
1090
1091 let lines = copy
1092 .generate_dockerfile_lines(&mut GenerationContext::default())
1093 .unwrap();
1094
1095 assert_eq_sorted!(
1096 lines,
1097 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1098 command: "COPY".into(),
1099 content: "<<EOF test.sh\necho hello\nEOF".into(),
1100 options: vec![InstructionOption::Flag("link".into())],
1101 })]
1102 );
1103 }
1104 }
1105
1106 mod image_name {
1107 use super::*;
1108
1109 #[test]
1110 fn user_with_user() {
1111 let dofigen = Dofigen {
1112 stage: Stage {
1113 user: Some(User::new_without_group("my-user").into()),
1114 from: FromContext::FromImage(ImageName {
1115 path: String::from("my-image"),
1116 ..Default::default()
1117 }),
1118 ..Default::default()
1119 },
1120 ..Default::default()
1121 };
1122 let user = dofigen.stage.user(&GenerationContext {
1123 user: Some(User::new("1000")),
1124 ..Default::default()
1125 });
1126 assert_eq_sorted!(
1127 user,
1128 Some(User {
1129 user: String::from("my-user"),
1130 group: None,
1131 })
1132 );
1133 }
1134
1135 #[test]
1136 fn user_without_user() {
1137 let dofigen = Dofigen {
1138 stage: Stage {
1139 from: FromContext::FromImage(ImageName {
1140 path: String::from("my-image"),
1141 ..Default::default()
1142 }),
1143 ..Default::default()
1144 },
1145 ..Default::default()
1146 };
1147 let user = dofigen.stage.user(&GenerationContext {
1148 user: Some(User::new("1000")),
1149 ..Default::default()
1150 });
1151 assert_eq_sorted!(
1152 user,
1153 Some(User {
1154 user: String::from("1000"),
1155 group: Some(String::from("1000")),
1156 })
1157 );
1158 }
1159
1160 #[test]
1161 fn with_platform() {
1162 let stage = Stage {
1163 from: FromContext::FromImage(ImageName {
1164 path: String::from("alpine"),
1165 platform: Some("linux/amd64".into()),
1166 ..Default::default()
1167 }),
1168 ..Default::default()
1169 };
1170 assert_eq_sorted!(
1171 stage
1172 .generate_dockerfile_lines(&mut GenerationContext {
1173 stage_name: "runtime".into(),
1174 ..Default::default()
1175 })
1176 .unwrap(),
1177 vec![
1178 DockerfileLine::Comment("runtime".into()),
1179 DockerfileLine::Instruction(DockerfileInsctruction {
1180 command: "FROM".into(),
1181 content: "alpine AS runtime".into(),
1182 options: vec![InstructionOption::WithValue(
1183 "platform".into(),
1184 "linux/amd64".into()
1185 )],
1186 })
1187 ]
1188 );
1189 }
1190 }
1191
1192 mod run {
1193 use super::*;
1194
1195 #[test]
1196 fn simple() {
1197 let builder = Run {
1198 run: vec!["echo Hello".into()].into(),
1199 ..Default::default()
1200 };
1201 assert_eq_sorted!(
1202 builder
1203 .generate_dockerfile_lines(&mut GenerationContext::default())
1204 .unwrap(),
1205 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1206 command: "RUN".into(),
1207 content: "echo Hello".into(),
1208 options: vec![],
1209 })]
1210 );
1211 }
1212
1213 #[test]
1214 fn without_run() {
1215 let builder = Run {
1216 ..Default::default()
1217 };
1218 assert_eq_sorted!(
1219 builder
1220 .generate_dockerfile_lines(&mut GenerationContext::default())
1221 .unwrap(),
1222 vec![]
1223 );
1224 }
1225
1226 #[test]
1227 fn with_empty_run() {
1228 let builder = Run {
1229 run: vec![].into(),
1230 ..Default::default()
1231 };
1232 assert_eq_sorted!(
1233 builder
1234 .generate_dockerfile_lines(&mut GenerationContext::default())
1235 .unwrap(),
1236 vec![]
1237 );
1238 }
1239
1240 #[test]
1241 fn with_script_and_caches_with_named_user() {
1242 let builder = Run {
1243 run: vec!["echo Hello".into()].into(),
1244 cache: vec![Cache {
1245 target: "/path/to/cache".into(),
1246 readonly: Some(true),
1247 ..Default::default()
1248 }]
1249 .into(),
1250 ..Default::default()
1251 };
1252 let mut context = GenerationContext {
1253 user: Some(User::new("test")),
1254 ..Default::default()
1255 };
1256 assert_eq_sorted!(
1257 builder.generate_dockerfile_lines(&mut context).unwrap(),
1258 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1259 command: "RUN".into(),
1260 content: "echo Hello".into(),
1261 options: vec![InstructionOption::WithOptions(
1262 "mount".into(),
1263 vec![
1264 InstructionOptionOption::new("type", "cache".into()),
1265 InstructionOptionOption::new("target", "/path/to/cache".into()),
1266 InstructionOptionOption::new("sharing", "locked".into()),
1267 InstructionOptionOption::new_flag("readonly"),
1268 ],
1269 )],
1270 })]
1271 );
1272 }
1273
1274 #[test]
1275 fn with_script_and_caches_with_uid_user() {
1276 let builder = Run {
1277 run: vec!["echo Hello".into()].into(),
1278 cache: vec![Cache {
1279 target: "/path/to/cache".into(),
1280 ..Default::default()
1281 }],
1282 ..Default::default()
1283 };
1284 let mut context = GenerationContext {
1285 user: Some(User::new("1000")),
1286 ..Default::default()
1287 };
1288 assert_eq_sorted!(
1289 builder.generate_dockerfile_lines(&mut context).unwrap(),
1290 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1291 command: "RUN".into(),
1292 content: "echo Hello".into(),
1293 options: vec![InstructionOption::WithOptions(
1294 "mount".into(),
1295 vec![
1296 InstructionOptionOption::new("type", "cache".into()),
1297 InstructionOptionOption::new("target", "/path/to/cache".into()),
1298 InstructionOptionOption::new("uid", "1000".into()),
1299 InstructionOptionOption::new("gid", "1000".into()),
1300 InstructionOptionOption::new("sharing", "locked".into()),
1301 ],
1302 )],
1303 })]
1304 );
1305 }
1306
1307 #[test]
1308 fn with_script_and_caches_with_uid_user_without_group() {
1309 let builder = Run {
1310 run: vec!["echo Hello".into()].into(),
1311 cache: vec![Cache {
1312 target: "/path/to/cache".into(),
1313 ..Default::default()
1314 }],
1315 ..Default::default()
1316 };
1317 let mut context = GenerationContext {
1318 user: Some(User::new_without_group("1000")),
1319 ..Default::default()
1320 };
1321 assert_eq_sorted!(
1322 builder.generate_dockerfile_lines(&mut context).unwrap(),
1323 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1324 command: "RUN".into(),
1325 content: "echo Hello".into(),
1326 options: vec![InstructionOption::WithOptions(
1327 "mount".into(),
1328 vec![
1329 InstructionOptionOption::new("type", "cache".into()),
1330 InstructionOptionOption::new("target", "/path/to/cache".into()),
1331 InstructionOptionOption::new("uid", "1000".into()),
1332 InstructionOptionOption::new("sharing", "locked".into()),
1333 ],
1334 )],
1335 })]
1336 );
1337 }
1338
1339 #[test]
1340 fn with_tmpfs() {
1341 let builder = Run {
1342 run: vec!["echo Hello".into()].into(),
1343 tmpfs: vec![TmpFs {
1344 target: "/path/to/tmpfs".into(),
1345 ..Default::default()
1346 }],
1347 ..Default::default()
1348 };
1349 let mut context = GenerationContext {
1350 user: Some(User::new_without_group("1000")),
1351 ..Default::default()
1352 };
1353 assert_eq_sorted!(
1354 builder.generate_dockerfile_lines(&mut context).unwrap(),
1355 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1356 command: "RUN".into(),
1357 content: "echo Hello".into(),
1358 options: vec![InstructionOption::WithOptions(
1359 "mount".into(),
1360 vec![
1361 InstructionOptionOption::new("type", "tmpfs".into()),
1362 InstructionOptionOption::new("target", "/path/to/tmpfs".into()),
1363 ],
1364 )],
1365 })]
1366 );
1367 }
1368
1369 #[test]
1370 fn with_secret() {
1371 let builder = Run {
1372 run: vec!["echo Hello".into()].into(),
1373 secret: vec![Secret {
1374 id: Some("test".into()),
1375 ..Default::default()
1376 }],
1377 ..Default::default()
1378 };
1379 let mut context = GenerationContext {
1380 user: Some(User::new_without_group("1000")),
1381 ..Default::default()
1382 };
1383 assert_eq_sorted!(
1384 builder.generate_dockerfile_lines(&mut context).unwrap(),
1385 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1386 command: "RUN".into(),
1387 content: "echo Hello".into(),
1388 options: vec![InstructionOption::WithOptions(
1389 "mount".into(),
1390 vec![
1391 InstructionOptionOption::new("type", "secret".into()),
1392 InstructionOptionOption::new("id", "test".into()),
1393 ],
1394 )],
1395 })]
1396 );
1397 }
1398
1399 #[test]
1400 fn with_secret_empty() {
1401 let builder = Run {
1402 run: vec!["echo Hello".into()].into(),
1403 secret: vec![Secret::default()],
1404 ..Default::default()
1405 };
1406 let mut context = GenerationContext {
1407 user: Some(User::new_without_group("1000")),
1408 ..Default::default()
1409 };
1410 assert_eq_sorted!(
1411 builder.generate_dockerfile_lines(&mut context).unwrap(),
1412 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1413 command: "RUN".into(),
1414 content: "echo Hello".into(),
1415 options: vec![InstructionOption::WithOptions(
1416 "mount".into(),
1417 vec![InstructionOptionOption::new("type", "secret".into()),],
1418 )],
1419 })]
1420 );
1421 }
1422
1423 #[test]
1424 fn with_ssh() {
1425 let builder = Run {
1426 run: vec!["echo Hello".into()].into(),
1427 ssh: vec![Ssh {
1428 id: Some("test".into()),
1429 ..Default::default()
1430 }],
1431 ..Default::default()
1432 };
1433 let mut context = GenerationContext {
1434 user: Some(User::new_without_group("1000")),
1435 ..Default::default()
1436 };
1437 assert_eq_sorted!(
1438 builder.generate_dockerfile_lines(&mut context).unwrap(),
1439 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1440 command: "RUN".into(),
1441 content: "echo Hello".into(),
1442 options: vec![InstructionOption::WithOptions(
1443 "mount".into(),
1444 vec![
1445 InstructionOptionOption::new("type", "ssh".into()),
1446 InstructionOptionOption::new("id", "test".into()),
1447 ],
1448 )],
1449 })]
1450 );
1451 }
1452
1453 #[test]
1454 fn with_ssh_empty() {
1455 let builder = Run {
1456 run: vec!["echo Hello".into()].into(),
1457 ssh: vec![Ssh::default()],
1458 ..Default::default()
1459 };
1460 let mut context = GenerationContext {
1461 user: Some(User::new_without_group("1000")),
1462 ..Default::default()
1463 };
1464 assert_eq_sorted!(
1465 builder.generate_dockerfile_lines(&mut context).unwrap(),
1466 vec![DockerfileLine::Instruction(DockerfileInsctruction {
1467 command: "RUN".into(),
1468 content: "echo Hello".into(),
1469 options: vec![InstructionOption::WithOptions(
1470 "mount".into(),
1471 vec![InstructionOptionOption::new("type", "ssh".into()),],
1472 )],
1473 })]
1474 );
1475 }
1476 }
1477
1478 mod label {
1479 use std::collections::HashMap;
1480
1481 use crate::{DofigenContext, lock::Lock};
1482
1483 use super::*;
1484
1485 #[test]
1486 fn with_label() {
1487 let stage = Stage {
1488 label: HashMap::from([("key".into(), "value".into())]),
1489 ..Default::default()
1490 };
1491 let lines = stage
1492 .generate_dockerfile_lines(&mut GenerationContext::default())
1493 .unwrap();
1494 assert_eq_sorted!(
1495 lines[2],
1496 DockerfileLine::Instruction(DockerfileInsctruction {
1497 command: "LABEL".into(),
1498 content: "key=\"value\"".into(),
1499 options: vec![],
1500 })
1501 );
1502 }
1503
1504 #[test]
1505 fn with_many_multiline_labels() {
1506 let stage = Stage {
1507 label: HashMap::from([
1508 ("key1".into(), "value1".into()),
1509 ("key2".into(), "value2\nligne2".into()),
1510 ]),
1511 ..Default::default()
1512 };
1513 let lines = stage
1514 .generate_dockerfile_lines(&mut GenerationContext::default())
1515 .unwrap();
1516 assert_eq_sorted!(
1517 lines[2],
1518 DockerfileLine::Instruction(DockerfileInsctruction {
1519 command: "LABEL".into(),
1520 content: "key1=\"value1\" \\\n key2=\"value2\\\nligne2\"".into(),
1521 options: vec![],
1522 })
1523 );
1524 }
1525
1526 #[test]
1527 fn locked_with_many_multiline_labels() {
1528 let dofigen = Dofigen {
1529 stage: Stage {
1530 label: HashMap::from([
1531 ("key1".into(), "value1".into()),
1532 ("key2".into(), "value2\nligne2".into()),
1533 ]),
1534 ..Default::default()
1535 },
1536 ..Default::default()
1537 };
1538 let dofigen = dofigen.lock(&mut DofigenContext::new()).unwrap();
1539 let lines = dofigen
1540 .generate_dockerfile_lines(&mut GenerationContext::default())
1541 .unwrap();
1542 assert_eq_sorted!(
1543 lines[4],
1544 DockerfileLine::Instruction(DockerfileInsctruction {
1545 command: "LABEL".into(),
1546 content: "io.dofigen.version=\"0.0.0\" \\\n key1=\"value1\" \\\n key2=\"value2\\\nligne2\"".into(),
1547 options: vec![],
1548 })
1549 );
1550 }
1551 }
1552}