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        let builder_names = context.lint_session.get_sorted_builders();
474
475        for name in builder_names {
476            context.push_state(GenerationContextState {
477                stage_name: Some(name.clone()),
478                ..Default::default()
479            });
480            let builder = self
481                .builders
482                .get(&name)
483                .expect(format!("The builder '{}' not found", name).as_str());
484
485            lines.push(DockerfileLine::Empty);
486            lines.append(&mut Stage::generate_dockerfile_lines(builder, context)?);
487            context.pop_state();
488        }
489
490        context.push_state(GenerationContextState {
491            user: Some(Some(User::new("1000"))),
492            stage_name: Some("runtime".into()),
493            default_from: Some(FromContext::default()),
494        });
495        lines.push(DockerfileLine::Empty);
496        lines.append(&mut self.stage.generate_dockerfile_lines(context)?);
497        context.pop_state();
498
499        self.volume.iter().for_each(|volume| {
500            lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
501                command: "VOLUME".into(),
502                content: volume.clone(),
503                options: vec![],
504            }))
505        });
506
507        self.expose.iter().for_each(|port| {
508            lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
509                command: "EXPOSE".into(),
510                content: port.to_string(),
511                options: vec![],
512            }))
513        });
514        if let Some(healthcheck) = &self.healthcheck {
515            let mut options = vec![];
516            if let Some(interval) = &healthcheck.interval {
517                options.push(InstructionOption::WithValue(
518                    "interval".into(),
519                    interval.into(),
520                ));
521            }
522            if let Some(timeout) = &healthcheck.timeout {
523                options.push(InstructionOption::WithValue(
524                    "timeout".into(),
525                    timeout.into(),
526                ));
527            }
528            if let Some(start_period) = &healthcheck.start {
529                options.push(InstructionOption::WithValue(
530                    "start-period".into(),
531                    start_period.into(),
532                ));
533            }
534            if let Some(retries) = &healthcheck.retries {
535                options.push(InstructionOption::WithValue(
536                    "retries".into(),
537                    retries.to_string(),
538                ));
539            }
540            lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
541                command: "HEALTHCHECK".into(),
542                content: format!("CMD {}", healthcheck.cmd.clone()),
543                options,
544            }))
545        }
546        if !self.entrypoint.is_empty() {
547            lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
548                command: "ENTRYPOINT".into(),
549                content: string_vec_into(self.entrypoint.to_vec()),
550                options: vec![],
551            }))
552        }
553        if !self.cmd.is_empty() {
554            lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
555                command: "CMD".into(),
556                content: string_vec_into(self.cmd.to_vec()),
557                options: vec![],
558            }))
559        }
560        Ok(lines)
561    }
562}
563
564impl DockerfileGenerator for Stage {
565    fn generate_dockerfile_lines(
566        &self,
567        context: &mut GenerationContext,
568    ) -> Result<Vec<DockerfileLine>> {
569        context.push_state(GenerationContextState {
570            user: Some(self.user(context)),
571            ..Default::default()
572        });
573        let stage_name = context.stage_name.clone();
574
575        // From
576        let mut lines = vec![
577            DockerfileLine::Comment(stage_name.clone()),
578            DockerfileLine::Instruction(DockerfileInsctruction {
579                command: "FROM".into(),
580                content: format!(
581                    "{image_name} AS {stage_name}",
582                    image_name = self.from(context).to_string()
583                ),
584                options: vec![],
585            }),
586        ];
587
588        // Arg
589        if !self.arg.is_empty() {
590            let mut keys = self.arg.keys().collect::<Vec<&String>>();
591            keys.sort();
592            keys.iter().for_each(|key| {
593                let value = self.arg.get(*key).unwrap();
594                lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
595                    command: "ARG".into(),
596                    content: if value.is_empty() {
597                        key.to_string()
598                    } else {
599                        format!("{}={}", key, value)
600                    },
601                    options: vec![],
602                }));
603            });
604        }
605
606        // Label
607        if !self.label.is_empty() {
608            let mut keys = self.label.keys().collect::<Vec<&String>>();
609            keys.sort();
610            lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
611                command: "LABEL".into(),
612                content: keys
613                    .iter()
614                    .map(|&key| {
615                        format!(
616                            "{}=\"{}\"",
617                            key,
618                            self.label.get(key).unwrap().replace("\n", "\\\n")
619                        )
620                    })
621                    .collect::<Vec<String>>()
622                    .join(LINE_SEPARATOR),
623                options: vec![],
624            }));
625        }
626
627        // Env
628        if !self.env.is_empty() {
629            lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
630                command: "ENV".into(),
631                content: self
632                    .env
633                    .iter()
634                    .map(|(key, value)| format!("{}=\"{}\"", key, value))
635                    .collect::<Vec<String>>()
636                    .join(LINE_SEPARATOR),
637                options: vec![],
638            }));
639        }
640
641        // Workdir
642        if let Some(workdir) = &self.workdir {
643            lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
644                command: "WORKDIR".into(),
645                content: workdir.clone(),
646                options: vec![],
647            }));
648        }
649
650        // Copy resources
651        for copy in self.copy.iter() {
652            lines.append(&mut copy.generate_dockerfile_lines(context)?);
653        }
654
655        // Root
656        if let Some(root) = &self.root {
657            if !root.is_empty() {
658                let root_user = User::new("0");
659                // User
660                lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
661                    command: "USER".into(),
662                    content: root_user.to_string(),
663                    options: vec![],
664                }));
665
666                context.push_state(GenerationContextState {
667                    user: Some(Some(root_user)),
668                    ..Default::default()
669                });
670                // Run
671                lines.append(&mut root.generate_dockerfile_lines(context)?);
672                context.pop_state();
673            }
674        }
675
676        // User
677        if let Some(user) = self.user(context) {
678            lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
679                command: "USER".into(),
680                content: user.to_string(),
681                options: vec![],
682            }));
683        }
684
685        // Run
686        lines.append(&mut self.run.generate_dockerfile_lines(context)?);
687
688        context.pop_state();
689
690        Ok(lines)
691    }
692}
693
694impl DockerfileGenerator for Run {
695    fn generate_dockerfile_lines(
696        &self,
697        context: &mut GenerationContext,
698    ) -> Result<Vec<DockerfileLine>> {
699        let script = &self.run;
700        if script.is_empty() {
701            return Ok(vec![]);
702        }
703        let script_lines = script
704            .iter()
705            .flat_map(|command| command.lines())
706            .collect::<Vec<&str>>();
707        let content = match script_lines.len() {
708            0 => {
709                return Ok(vec![]);
710            }
711            1 => script_lines[0].into(),
712            _ => format!("<<EOF\n{}\nEOF", script_lines.join("\n")),
713        };
714        let mut options = vec![];
715
716        // Mount binds
717        self.bind.iter().for_each(|bind| {
718            let mut bind_options = vec![
719                InstructionOptionOption::new("type", "bind".into()),
720                InstructionOptionOption::new("target", bind.target.clone()),
721            ];
722            let from = match &bind.from {
723                FromContext::FromImage(image) => Some(image.to_string()),
724                FromContext::FromBuilder(builder) => Some(builder.clone()),
725                FromContext::FromContext(context) => context.clone(),
726            };
727            if let Some(from) = from {
728                bind_options.push(InstructionOptionOption::new("from", from));
729            }
730            if let Some(source) = bind.source.as_ref() {
731                bind_options.push(InstructionOptionOption::new("source", source.clone()));
732            }
733            if bind.readwrite.unwrap_or(false) {
734                bind_options.push(InstructionOptionOption::new_flag("readwrite"));
735            }
736            options.push(InstructionOption::WithOptions("mount".into(), bind_options));
737        });
738
739        // Mount caches
740        for cache in self.cache.iter() {
741            let target = cache.target.clone();
742
743            let mut cache_options = vec![
744                InstructionOptionOption::new("type", "cache".into()),
745                InstructionOptionOption::new("target", target),
746            ];
747            if let Some(id) = cache.id.as_ref() {
748                cache_options.push(InstructionOptionOption::new("id", id.clone()));
749            }
750            let from = match &cache.from {
751                FromContext::FromImage(image) => Some(image.to_string()),
752                FromContext::FromBuilder(builder) => Some(builder.clone()),
753                FromContext::FromContext(context) => context.clone(),
754            };
755            if let Some(from) = from {
756                cache_options.push(InstructionOptionOption::new("from", from));
757                if let Some(source) = cache.source.as_ref() {
758                    cache_options.push(InstructionOptionOption::new("source", source.clone()));
759                }
760            }
761            if let Some(user) = cache.chown.as_ref().or(context.user.as_ref()) {
762                if let Some(uid) = user.uid() {
763                    cache_options.push(InstructionOptionOption::new("uid", uid.to_string()));
764                }
765                if let Some(gid) = user.gid() {
766                    cache_options.push(InstructionOptionOption::new("gid", gid.to_string()));
767                }
768            }
769            if let Some(chmod) = cache.chmod.as_ref() {
770                cache_options.push(InstructionOptionOption::new("chmod", chmod.clone()));
771            }
772            cache_options.push(InstructionOptionOption::new(
773                "sharing",
774                cache.sharing.clone().unwrap_or_default().to_string(),
775            ));
776            if cache.readonly.unwrap_or(false) {
777                cache_options.push(InstructionOptionOption::new_flag("readonly"));
778            }
779
780            options.push(InstructionOption::WithOptions(
781                "mount".into(),
782                cache_options,
783            ));
784        }
785
786        let mut lines = vec![];
787
788        // Shell
789        if !self.shell.is_empty() {
790            lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
791                command: "SHELL".into(),
792                content: string_vec_into(self.shell.to_vec()),
793                options: vec![],
794            }));
795        }
796
797        lines.push(DockerfileLine::Instruction(DockerfileInsctruction {
798            command: "RUN".into(),
799            content,
800            options,
801        }));
802
803        Ok(lines)
804    }
805}
806
807fn copy_paths_into(paths: Vec<String>, target: &Option<String>) -> String {
808    let mut parts = paths.clone();
809    parts.push(target.clone().unwrap_or("./".into()));
810    parts
811        .iter()
812        .map(|p| format!("\"{}\"", p))
813        .collect::<Vec<String>>()
814        .join(" ")
815}
816
817fn string_vec_into(string_vec: Vec<String>) -> String {
818    format!(
819        "[{}]",
820        string_vec
821            .iter()
822            .map(|s| format!("\"{}\"", s))
823            .collect::<Vec<String>>()
824            .join(", ")
825    )
826}
827
828#[cfg(test)]
829mod test {
830    use super::*;
831    use pretty_assertions_sorted::assert_eq_sorted;
832
833    mod stage {
834        use std::collections::HashMap;
835
836        use super::*;
837
838        #[test]
839        fn user_with_user() {
840            let stage = Stage {
841                user: Some(User::new_without_group("my-user").into()),
842                ..Default::default()
843            };
844            let user = stage.user(&GenerationContext::default());
845            assert_eq_sorted!(
846                user,
847                Some(User {
848                    user: "my-user".into(),
849                    group: None,
850                })
851            );
852        }
853
854        #[test]
855        fn user_without_user() {
856            let stage = Stage::default();
857            let user = stage.user(&GenerationContext::default());
858            assert_eq_sorted!(user, None);
859        }
860
861        #[test]
862        fn stage_args() {
863            let stage = Stage {
864                arg: HashMap::from([("arg2".into(), "".into()), ("arg1".into(), "value1".into())]),
865                ..Default::default()
866            };
867
868            let lines = stage.generate_dockerfile_lines(&mut GenerationContext {
869                stage_name: "test".into(),
870                ..Default::default()
871            });
872
873            assert_eq_sorted!(
874                lines.unwrap(),
875                vec![
876                    DockerfileLine::Comment("test".into()),
877                    DockerfileLine::Instruction(DockerfileInsctruction {
878                        command: "FROM".into(),
879                        content: "scratch AS test".into(),
880                        options: vec![],
881                    }),
882                    DockerfileLine::Instruction(DockerfileInsctruction {
883                        command: "ARG".into(),
884                        content: "arg1=value1".into(),
885                        options: vec![],
886                    }),
887                    DockerfileLine::Instruction(DockerfileInsctruction {
888                        command: "ARG".into(),
889                        content: "arg2".into(),
890                        options: vec![],
891                    }),
892                ]
893            );
894        }
895    }
896
897    mod copy {
898        use super::*;
899
900        #[test]
901        fn with_chmod() {
902            let copy = Copy {
903                paths: vec!["/path/to/file".into()],
904                options: CopyOptions {
905                    target: Some("/app/".into()),
906                    chmod: Some("755".into()),
907                    ..Default::default()
908                },
909                ..Default::default()
910            };
911
912            let lines = copy
913                .generate_dockerfile_lines(&mut GenerationContext::default())
914                .unwrap();
915
916            assert_eq_sorted!(
917                lines,
918                vec![DockerfileLine::Instruction(DockerfileInsctruction {
919                    command: "COPY".into(),
920                    content: "\"/path/to/file\" \"/app/\"".into(),
921                    options: vec![
922                        InstructionOption::WithValue("chmod".into(), "755".into()),
923                        InstructionOption::Flag("link".into())
924                    ],
925                })]
926            );
927        }
928
929        #[test]
930        fn from_content() {
931            let copy = CopyContent {
932                content: "echo hello".into(),
933                options: CopyOptions {
934                    target: Some("test.sh".into()),
935                    ..Default::default()
936                },
937                ..Default::default()
938            };
939
940            let lines = copy
941                .generate_dockerfile_lines(&mut GenerationContext::default())
942                .unwrap();
943
944            assert_eq_sorted!(
945                lines,
946                vec![DockerfileLine::Instruction(DockerfileInsctruction {
947                    command: "COPY".into(),
948                    content: "<<EOF test.sh\necho hello\nEOF".into(),
949                    options: vec![InstructionOption::Flag("link".into())],
950                })]
951            );
952        }
953    }
954
955    mod image_name {
956        use super::*;
957
958        #[test]
959        fn user_with_user() {
960            let dofigen = Dofigen {
961                stage: Stage {
962                    user: Some(User::new_without_group("my-user").into()),
963                    from: FromContext::FromImage(ImageName {
964                        path: String::from("my-image"),
965                        ..Default::default()
966                    }),
967                    ..Default::default()
968                },
969                ..Default::default()
970            };
971            let user = dofigen.stage.user(&GenerationContext {
972                user: Some(User::new("1000")),
973                ..Default::default()
974            });
975            assert_eq_sorted!(
976                user,
977                Some(User {
978                    user: String::from("my-user"),
979                    group: None,
980                })
981            );
982        }
983
984        #[test]
985        fn user_without_user() {
986            let dofigen = Dofigen {
987                stage: Stage {
988                    from: FromContext::FromImage(ImageName {
989                        path: String::from("my-image"),
990                        ..Default::default()
991                    }),
992                    ..Default::default()
993                },
994                ..Default::default()
995            };
996            let user = dofigen.stage.user(&GenerationContext {
997                user: Some(User::new("1000")),
998                ..Default::default()
999            });
1000            assert_eq_sorted!(
1001                user,
1002                Some(User {
1003                    user: String::from("1000"),
1004                    group: Some(String::from("1000")),
1005                })
1006            );
1007        }
1008    }
1009
1010    mod run {
1011        use super::*;
1012
1013        #[test]
1014        fn simple() {
1015            let builder = Run {
1016                run: vec!["echo Hello".into()].into(),
1017                ..Default::default()
1018            };
1019            assert_eq_sorted!(
1020                builder
1021                    .generate_dockerfile_lines(&mut GenerationContext::default())
1022                    .unwrap(),
1023                vec![DockerfileLine::Instruction(DockerfileInsctruction {
1024                    command: "RUN".into(),
1025                    content: "echo Hello".into(),
1026                    options: vec![],
1027                })]
1028            );
1029        }
1030
1031        #[test]
1032        fn without_run() {
1033            let builder = Run {
1034                ..Default::default()
1035            };
1036            assert_eq_sorted!(
1037                builder
1038                    .generate_dockerfile_lines(&mut GenerationContext::default())
1039                    .unwrap(),
1040                vec![]
1041            );
1042        }
1043
1044        #[test]
1045        fn with_empty_run() {
1046            let builder = Run {
1047                run: vec![].into(),
1048                ..Default::default()
1049            };
1050            assert_eq_sorted!(
1051                builder
1052                    .generate_dockerfile_lines(&mut GenerationContext::default())
1053                    .unwrap(),
1054                vec![]
1055            );
1056        }
1057
1058        #[test]
1059        fn with_script_and_caches_with_named_user() {
1060            let builder = Run {
1061                run: vec!["echo Hello".into()].into(),
1062                cache: vec![Cache {
1063                    target: "/path/to/cache".into(),
1064                    readonly: Some(true),
1065                    ..Default::default()
1066                }]
1067                .into(),
1068                ..Default::default()
1069            };
1070            let mut context = GenerationContext {
1071                user: Some(User::new("test")),
1072                ..Default::default()
1073            };
1074            assert_eq_sorted!(
1075                builder.generate_dockerfile_lines(&mut context).unwrap(),
1076                vec![DockerfileLine::Instruction(DockerfileInsctruction {
1077                    command: "RUN".into(),
1078                    content: "echo Hello".into(),
1079                    options: vec![InstructionOption::WithOptions(
1080                        "mount".into(),
1081                        vec![
1082                            InstructionOptionOption::new("type", "cache".into()),
1083                            InstructionOptionOption::new("target", "/path/to/cache".into()),
1084                            InstructionOptionOption::new("sharing", "locked".into()),
1085                            InstructionOptionOption::new_flag("readonly"),
1086                        ],
1087                    )],
1088                })]
1089            );
1090        }
1091
1092        #[test]
1093        fn with_script_and_caches_with_uid_user() {
1094            let builder = Run {
1095                run: vec!["echo Hello".into()].into(),
1096                cache: vec![Cache {
1097                    target: "/path/to/cache".into(),
1098                    ..Default::default()
1099                }],
1100                ..Default::default()
1101            };
1102            let mut context = GenerationContext {
1103                user: Some(User::new("1000")),
1104                ..Default::default()
1105            };
1106            assert_eq_sorted!(
1107                builder.generate_dockerfile_lines(&mut context).unwrap(),
1108                vec![DockerfileLine::Instruction(DockerfileInsctruction {
1109                    command: "RUN".into(),
1110                    content: "echo Hello".into(),
1111                    options: vec![InstructionOption::WithOptions(
1112                        "mount".into(),
1113                        vec![
1114                            InstructionOptionOption::new("type", "cache".into()),
1115                            InstructionOptionOption::new("target", "/path/to/cache".into()),
1116                            InstructionOptionOption::new("uid", "1000".into()),
1117                            InstructionOptionOption::new("gid", "1000".into()),
1118                            InstructionOptionOption::new("sharing", "locked".into()),
1119                        ],
1120                    )],
1121                })]
1122            );
1123        }
1124
1125        #[test]
1126        fn with_script_and_caches_with_uid_user_without_group() {
1127            let builder = Run {
1128                run: vec!["echo Hello".into()].into(),
1129                cache: vec![Cache {
1130                    target: "/path/to/cache".into(),
1131                    ..Default::default()
1132                }],
1133                ..Default::default()
1134            };
1135            let mut context = GenerationContext {
1136                user: Some(User::new_without_group("1000")),
1137                ..Default::default()
1138            };
1139            assert_eq_sorted!(
1140                builder.generate_dockerfile_lines(&mut context).unwrap(),
1141                vec![DockerfileLine::Instruction(DockerfileInsctruction {
1142                    command: "RUN".into(),
1143                    content: "echo Hello".into(),
1144                    options: vec![InstructionOption::WithOptions(
1145                        "mount".into(),
1146                        vec![
1147                            InstructionOptionOption::new("type", "cache".into()),
1148                            InstructionOptionOption::new("target", "/path/to/cache".into()),
1149                            InstructionOptionOption::new("uid", "1000".into()),
1150                            InstructionOptionOption::new("sharing", "locked".into()),
1151                        ],
1152                    )],
1153                })]
1154            );
1155        }
1156    }
1157
1158    mod label {
1159        use std::collections::HashMap;
1160
1161        use crate::{lock::Lock, DofigenContext};
1162
1163        use super::*;
1164
1165        #[test]
1166        fn with_label() {
1167            let stage = Stage {
1168                label: HashMap::from([("key".into(), "value".into())]),
1169                ..Default::default()
1170            };
1171            let lines = stage
1172                .generate_dockerfile_lines(&mut GenerationContext::default())
1173                .unwrap();
1174            assert_eq_sorted!(
1175                lines[2],
1176                DockerfileLine::Instruction(DockerfileInsctruction {
1177                    command: "LABEL".into(),
1178                    content: "key=\"value\"".into(),
1179                    options: vec![],
1180                })
1181            );
1182        }
1183
1184        #[test]
1185        fn with_many_multiline_labels() {
1186            let stage = Stage {
1187                label: HashMap::from([
1188                    ("key1".into(), "value1".into()),
1189                    ("key2".into(), "value2\nligne2".into()),
1190                ]),
1191                ..Default::default()
1192            };
1193            let lines = stage
1194                .generate_dockerfile_lines(&mut GenerationContext::default())
1195                .unwrap();
1196            assert_eq_sorted!(
1197                lines[2],
1198                DockerfileLine::Instruction(DockerfileInsctruction {
1199                    command: "LABEL".into(),
1200                    content: "key1=\"value1\" \\\n    key2=\"value2\\\nligne2\"".into(),
1201                    options: vec![],
1202                })
1203            );
1204        }
1205
1206        #[test]
1207        fn locked_with_many_multiline_labels() {
1208            let dofigen = Dofigen {
1209                stage: Stage {
1210                    label: HashMap::from([
1211                        ("key1".into(), "value1".into()),
1212                        ("key2".into(), "value2\nligne2".into()),
1213                    ]),
1214                    ..Default::default()
1215                },
1216                ..Default::default()
1217            };
1218            let dofigen = dofigen.lock(&mut DofigenContext::new()).unwrap();
1219            let lines = dofigen
1220                .generate_dockerfile_lines(&mut GenerationContext::default())
1221                .unwrap();
1222            assert_eq_sorted!(
1223                lines[4],
1224                DockerfileLine::Instruction(DockerfileInsctruction {
1225                    command: "LABEL".into(),
1226                    content: "io.dofigen.version=\"0.0.0\" \\\n    key1=\"value1\" \\\n    key2=\"value2\\\nligne2\"".into(),
1227                    options: vec![],
1228                })
1229            );
1230        }
1231    }
1232}