dofigen_lib/
generator.rs

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            // For basic context we can't know if it's an image, a builder not strongly typed or a build context
142            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    // Static methods
198
199    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        // Arg
328        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        // From
437        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        // Arg
461        if !self.arg.is_empty() {
462            let args = generate_arg_command(&self.arg);
463            lines.extend(args);
464        }
465
466        // Label
467        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        // Env
488        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        // Workdir
502        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        // Copy resources
511        for copy in self.copy.iter() {
512            lines.append(&mut copy.generate_dockerfile_lines(context)?);
513        }
514
515        // Root
516        if let Some(root) = &self.root {
517            if !root.is_empty() {
518                let root_user = User::new("0");
519                // User
520                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                // Run
531                lines.append(&mut root.generate_dockerfile_lines(context)?);
532                context.pop_state();
533            }
534        }
535
536        // User
537        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        // Run
546        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        // Mount binds
577        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        // Mount caches
600        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        // Shell
649        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) = &copy_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}