Skip to main content

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