Skip to main content

dofigen_lib/parse/
mod.rs

1mod context;
2mod insctruction;
3
4use colored::{Color, Colorize};
5use struct_patch::Patch;
6
7use crate::{
8    DockerFile, DockerFileCommand, DockerFileInsctruction, DockerFileLine, DockerIgnore,
9    DockerIgnoreLine, Dofigen, Error, FromContext, MessageLevel, Result, User,
10    parse::context::ParseContext,
11};
12
13impl Dofigen {
14    pub fn from_dockerfile(
15        dockerfile: DockerFile,
16        dockerignore: Option<DockerIgnore>,
17    ) -> Result<Self> {
18        let mut context = ParseContext::default();
19
20        if let Some(dockerignore) = dockerignore {
21            context.parse_dockerignore(dockerignore)?;
22        }
23
24        context.parse_dockerfile(dockerfile)?;
25
26        Ok(context.dofigen.into())
27    }
28}
29
30impl ParseContext {
31    pub fn parse_dockerignore(&mut self, dockerignore: DockerIgnore) -> Result<()> {
32        if !self.dofigen.ignore.is_empty() {
33            return Err(Error::Custom(
34                "A .dockerignore have already been parsed by this context".to_string(),
35            ));
36        }
37        // TODO: If there is a negate pattern with **, then manage context field
38        let ignores: Vec<String> = dockerignore
39            .lines
40            .iter()
41            .filter(|line| {
42                matches!(line, DockerIgnoreLine::Pattern(_))
43                    || matches!(line, DockerIgnoreLine::NegatePattern(_))
44            })
45            .map(|line| match line {
46                DockerIgnoreLine::Pattern(pattern) => pattern.clone(),
47                DockerIgnoreLine::NegatePattern(pattern) => format!("!{pattern}"),
48                _ => unreachable!(),
49            })
50            .collect();
51        self.dofigen.ignore = ignores;
52        Ok(())
53    }
54
55    pub fn parse_dockerfile(&mut self, dockerfile: DockerFile) -> Result<()> {
56        if !self.stage_names.is_empty() {
57            return Err(Error::Custom(
58                "A Dockerfile have already been parsed by this context".to_string(),
59            ));
60        }
61        let instructions: Vec<_> = dockerfile
62            .lines
63            .iter()
64            .filter(|line| matches!(line, DockerFileLine::Instruction(_)))
65            .collect();
66
67        self.stage_names = instructions
68            .iter()
69            .filter(|&line| {
70                matches!(
71                    line,
72                    DockerFileLine::Instruction(DockerFileInsctruction {
73                        command: DockerFileCommand::FROM,
74                        ..
75                    })
76                )
77            })
78            .map(|line| match line {
79                DockerFileLine::Instruction(DockerFileInsctruction {
80                    command: DockerFileCommand::FROM,
81                    content,
82                    ..
83                }) => content,
84                _ => unreachable!(),
85            })
86            .map(|from_content| split_from(from_content).1.unwrap_or("runtime").to_string())
87            .collect();
88
89        for line in instructions {
90            self.apply(line)?;
91        }
92
93        self.apply_root()?;
94
95        // Get runtime informations
96        let mut runtime_stage = self.current_stage.clone().ok_or(Error::Custom(
97            "No FROM instruction found in Dockerfile".to_string(),
98        ))?;
99        let runtime_name = self
100            .current_stage_name
101            .clone()
102            .unwrap_or("runtime".to_string());
103
104        // Get base instructions in from builders
105        let mut dofigen_patches = self
106            .builder_dofigen_patches
107            .remove(&runtime_name)
108            .into_iter()
109            .collect::<Vec<_>>();
110        let mut searching_stage = runtime_stage.clone();
111        while let FromContext::FromBuilder(builder_name) = searching_stage.from.clone() {
112            if let Some(builder_dofigen_patch) = self.builder_dofigen_patches.remove(&builder_name)
113            {
114                dofigen_patches.insert(0, builder_dofigen_patch);
115            }
116            searching_stage = self
117                .dofigen
118                .builders
119                .get(&builder_name)
120                .ok_or(Error::Custom(format!(
121                    "Builder '{}' not found",
122                    builder_name
123                )))?
124                .clone();
125        }
126
127        // Apply merged patches
128        if !dofigen_patches.is_empty() {
129            dofigen_patches.iter().for_each(|dofigen_patch| {
130                self.dofigen.apply(dofigen_patch.clone());
131            });
132        }
133
134        // If user is set as default, remove it
135        if let Some(user) = runtime_stage.user.as_ref() {
136            let default_user = User::new("1000");
137            if user.eq(&default_user) {
138                runtime_stage.user = None;
139            }
140        }
141
142        self.dofigen.stage = runtime_stage;
143
144        // Handle lint messages
145        self.messages.iter().for_each(|message| {
146            eprintln!(
147                "{}[path={}]: {}",
148                match message.level {
149                    MessageLevel::Error => "error".color(Color::Red).bold(),
150                    MessageLevel::Warn => "warning".color(Color::Yellow).bold(),
151                },
152                message.path.join(".").color(Color::Blue).bold(),
153                message.message
154            );
155        });
156
157        Ok(())
158    }
159}
160
161pub(crate) fn split_from(content: &str) -> (&str, Option<&str>) {
162    let pos = content.to_lowercase().find(" as ");
163    if let Some(pos) = pos {
164        let (from, name) = content.split_at(pos);
165        let name = name[4..].trim();
166        (from, Some(name))
167    } else {
168        (content, None)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::DofigenContext;
176    use crate::GenerationContext;
177    use crate::dockerfile_struct::*;
178    use crate::dofigen_struct::*;
179    use pretty_assertions_sorted::assert_eq_sorted;
180    use std::collections::HashMap;
181
182    #[test]
183    fn php_dockerfile() {
184        let dockerfile_content = r#"# syntax=docker/dockerfile:1.19.0
185# This file is generated by Dofigen v0.0.0
186# See https://github.com/lenra-io/dofigen
187
188# get-composer
189FROM composer:latest AS get-composer
190
191# install-deps
192FROM php:8.3-fpm-alpine AS install-deps
193USER 0:0
194RUN <<EOF
195apt-get update
196apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev icu-dev mysql-client
197EOF
198
199# install-php-ext
200FROM install-deps AS install-php-ext
201USER 0:0
202RUN <<EOF
203docker-php-ext-configure zip
204docker-php-ext-install bcmath gd intl pdo_mysql zip
205EOF
206
207# runtime
208FROM install-php-ext AS runtime
209WORKDIR /
210COPY \
211    --from=get-composer \
212    --chown=www-data \
213    --link \
214    "/usr/bin/composer" "/bin/"
215ADD \
216    --chown=www-data \
217    --link \
218    "https://github.com/pelican-dev/panel.git" "/tmp/pelican"
219USER www-data
220RUN <<EOF
221cd /tmp/pelican
222cp .env.example .env
223mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache
224chmod 777 -R bootstrap storage
225composer install --no-dev --optimize-autoloader
226rm -rf .env bootstrap/cache/*.php
227mkdir -p /app/storage/logs/
228chown -R nginx:nginx .
229rm /usr/local/etc/php-fpm.conf
230echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root
231mkdir -p /var/run/php /var/run/nginx
232mv .github/docker/default.conf /etc/nginx/http.d/default.conf
233mv .github/docker/supervisord.conf /etc/supervisord.conf
234EOF
235"#;
236
237        let yaml = r#"builders:
238  install-deps:
239    fromImage: php:8.3-fpm-alpine
240    root:
241      run:
242      - apt-get update
243      - >-
244        apk add --no-cache --update
245        ca-certificates
246        dcron
247        curl
248        git
249        supervisor
250        tar
251        unzip
252        nginx
253        libpng-dev
254        libxml2-dev
255        libzip-dev
256        icu-dev
257        mysql-client
258  install-php-ext:
259    fromBuilder: install-deps
260    root:
261      run:
262      # - docker-php-ext-configure gd --with-freetype --with-jpeg
263      # - docker-php-ext-install -j$(nproc) gd zip intl curl mbstring mysqli
264        - docker-php-ext-configure zip
265        - docker-php-ext-install bcmath gd intl pdo_mysql zip
266  get-composer:
267    name: composer
268    fromImage: composer:latest
269fromBuilder: install-php-ext
270workdir: /
271user:
272  user: www-data
273copy:
274- fromBuilder: get-composer
275  paths: "/usr/bin/composer"
276  target: "/bin/"
277  chown:
278    user: www-data
279  link: true
280- repo: 'https://github.com/pelican-dev/panel.git'
281  target: '/tmp/pelican'
282  chown:
283    user: www-data
284  link: true
285run:
286  - cd /tmp/pelican
287  - cp .env.example .env
288  - mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache
289  - chmod 777 -R bootstrap storage
290  - composer install --no-dev --optimize-autoloader
291  - rm -rf .env bootstrap/cache/*.php
292  - mkdir -p /app/storage/logs/
293  - chown -R nginx:nginx .
294  - rm /usr/local/etc/php-fpm.conf
295  - echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root
296  - mkdir -p /var/run/php /var/run/nginx
297  - mv .github/docker/default.conf /etc/nginx/http.d/default.conf
298  - mv .github/docker/supervisord.conf /etc/supervisord.conf
299"#;
300
301        let dockerfile: DockerFile = dockerfile_content.parse().unwrap();
302
303        let result = Dofigen::from_dockerfile(dockerfile, None);
304
305        let dofigen_from_dockerfile = result.unwrap();
306
307        assert_eq_sorted!(dofigen_from_dockerfile, Dofigen {
308                builders: HashMap::from([
309                    ("get-composer".to_string(), Stage {
310                        from: FromContext::FromImage(
311                            ImageName {
312                                path: "composer".to_string(),
313                                version: Some(
314                                    ImageVersion::Tag(
315                                        "latest".to_string(),
316                                    ),
317                                ),
318                ..Default::default()
319                            },
320                        ),
321                ..Default::default()
322                    }),
323                    ("install-deps".to_string(), Stage {
324                        from: FromContext::FromImage(
325                            ImageName {
326                                path: "php".to_string(),
327                                version: Some(
328                                    ImageVersion::Tag(
329                                        "8.3-fpm-alpine".to_string(),
330                                    ),
331                                ),
332                ..Default::default()
333                            },
334                        ),
335                        root: Some(
336                            Run {
337                                run: vec![
338                                    "apt-get update".to_string(),
339                                    "apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev icu-dev mysql-client".to_string(),
340                                ],
341                ..Default::default()
342                            },
343                        ),
344                ..Default::default()
345                    }),
346                    ("install-php-ext".to_string(), Stage {
347                        from: FromContext::FromBuilder(
348                            "install-deps".to_string(),
349                        ),
350                        root: Some(
351                            Run {
352                                run: vec![
353                                    "docker-php-ext-configure zip".to_string(),
354                                    "docker-php-ext-install bcmath gd intl pdo_mysql zip".to_string(),
355                                ],
356                ..Default::default()
357                            },
358                        ),
359                ..Default::default()
360                    })
361                    ]),
362                stage: Stage {
363                    from: FromContext::FromBuilder(
364                        "install-php-ext".to_string(),
365                    ),
366                    user: Some(
367                        User {
368                            user: "www-data".to_string(),
369                            group: None,
370                        },
371                    ),
372                    workdir: Some(
373                        "/".to_string(),
374                    ),
375                    copy: vec![
376                        CopyResource::Copy(
377                            Copy {
378                                from: FromContext::FromBuilder(
379                                    "get-composer".to_string(),
380                                ),
381                                paths: vec![
382                                    "/usr/bin/composer".to_string(),
383                                ],
384                                options: CopyOptions {
385                                   target: Some(
386                                       "/bin/".to_string(),
387                                   ),
388                                   chown: Some(
389                                       User {
390                                           user: "www-data".to_string(),
391                                           group: None,
392                                       },
393                                   ),
394                                   link: Some(
395                                       true,
396                                   ),
397                                    ..Default::default()
398                                },
399                                ..Default::default()
400                            },
401                        ),
402                        CopyResource::AddGitRepo(
403                            AddGitRepo {
404                                repo: "https://github.com/pelican-dev/panel.git".to_string(),
405                                options: CopyOptions {
406                                   target: Some(
407                                       "/tmp/pelican".to_string(),
408                                   ),
409                                   chown: Some(
410                                       User {
411                                           user: "www-data".to_string(),
412                                           group: None,
413                                       },
414                                   ),
415                                   link: Some(
416                                       true,
417                                   ),
418                ..Default::default()
419                                },
420                ..Default::default()
421                            },
422                        ),
423                    ],
424                    run: Run {
425                        run: vec![
426                            "cd /tmp/pelican".to_string(),
427                            "cp .env.example .env".to_string(),
428                            "mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache".to_string(),
429                            "chmod 777 -R bootstrap storage".to_string(),
430                            "composer install --no-dev --optimize-autoloader".to_string(),
431                            "rm -rf .env bootstrap/cache/*.php".to_string(),
432                            "mkdir -p /app/storage/logs/".to_string(),
433                            "chown -R nginx:nginx .".to_string(),
434                            "rm /usr/local/etc/php-fpm.conf".to_string(),
435                            "echo \"* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1\" >> /var/spool/cron/crontabs/root".to_string(),
436                            "mkdir -p /var/run/php /var/run/nginx".to_string(),
437                            "mv .github/docker/default.conf /etc/nginx/http.d/default.conf".to_string(),
438                            "mv .github/docker/supervisord.conf /etc/supervisord.conf".to_string(),
439                        ],
440                ..Default::default()
441                    },
442                                ..Default::default()
443                },
444                ..Default::default()
445            });
446
447        let dofigen_from_string: Dofigen = DofigenContext::new()
448            .parse_from_string(yaml)
449            .map_err(Error::from)
450            .unwrap();
451
452        assert_eq_sorted!(dofigen_from_dockerfile, dofigen_from_string);
453
454        let mut context = GenerationContext::from(dofigen_from_string.clone());
455
456        let generated_dockerfile = context.generate_dockerfile().unwrap();
457
458        assert_eq_sorted!(dockerfile_content.to_string(), generated_dockerfile);
459    }
460}