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