dofigen_lib/
generator.rs

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