Skip to main content

dofigen_lib/
dockerfile_struct.rs

1use std::{str::FromStr, vec};
2
3use regex::Regex;
4use serde::Deserialize;
5
6use crate::{Error, generator::*};
7
8macro_rules! simple_whitespace {
9    () => {
10        r"[\f ]"
11    };
12}
13
14macro_rules! escaped_newline {
15    () => {
16        r"\\\r?\n"
17    };
18}
19
20macro_rules! whitespace_or_escaped_newline {
21    () => {
22        concat!(simple_whitespace!(), "|", escaped_newline!())
23    };
24}
25macro_rules! whitespace_regex {
26    () => {
27        concat!(r"(?:", whitespace_or_escaped_newline!(), r")+")
28    };
29}
30macro_rules! option_regex {
31    () => {
32        concat!(
33            r"(?<option>--(?<name>\w+)(?:=(?<value>[^\s\\]+))?",
34            whitespace_regex!(),
35            r")"
36        )
37    };
38}
39
40const DOCKERFILE_LINE_REGEX: &str = concat!(
41    r"(?:",
42    // Instruction
43    r"[^\S\r\n]*",
44    r"(?<command>[A-Z]+)",
45    whitespace_regex!(),
46    r"(?<options>",
47    option_regex!(),
48    r"*)",
49    r"(?<content>(?:",
50    escaped_newline!(),
51    r"|.)+)",
52    // Blank lines
53    r"|[^\S\r\n]*",
54    // Comments
55    r"|[^\S\r\n]*# ?(?<comment>.+)",
56    // Empty lines
57    r")(?:\r?\n|$)",
58);
59
60#[derive(Debug, Clone, PartialEq, Deserialize)]
61pub enum DockerFileCommand {
62    FROM,
63    ARG,
64    LABEL,
65    // Deprecated, see https://docs.docker.com/engine/reference/builder/#maintainer-deprecated
66    MAINTAINER,
67    RUN,
68    COPY,
69    ADD,
70    WORKDIR,
71    ENV,
72    EXPOSE,
73    USER,
74    VOLUME,
75    SHELL,
76    HEALTHCHECK,
77    CMD,
78    ENTRYPOINT,
79    Unknown(String),
80}
81
82impl ToString for DockerFileCommand {
83    fn to_string(&self) -> String {
84        match self {
85            DockerFileCommand::FROM => "FROM".to_string(),
86            DockerFileCommand::LABEL => "LABEL".to_string(),
87            DockerFileCommand::MAINTAINER => "MAINTAINER".to_string(),
88            DockerFileCommand::ARG => "ARG".to_string(),
89            DockerFileCommand::RUN => "RUN".to_string(),
90            DockerFileCommand::COPY => "COPY".to_string(),
91            DockerFileCommand::ADD => "ADD".to_string(),
92            DockerFileCommand::WORKDIR => "WORKDIR".to_string(),
93            DockerFileCommand::ENV => "ENV".to_string(),
94            DockerFileCommand::EXPOSE => "EXPOSE".to_string(),
95            DockerFileCommand::USER => "USER".to_string(),
96            DockerFileCommand::VOLUME => "VOLUME".to_string(),
97            DockerFileCommand::SHELL => "SHELL".to_string(),
98            DockerFileCommand::HEALTHCHECK => "HEALTHCHECK".to_string(),
99            DockerFileCommand::CMD => "CMD".to_string(),
100            DockerFileCommand::ENTRYPOINT => "ENTRYPOINT".to_string(),
101            DockerFileCommand::Unknown(command) => command.clone(),
102        }
103    }
104}
105
106impl FromStr for DockerFileCommand {
107    type Err = Error;
108
109    fn from_str(string: &str) -> Result<Self, Self::Err> {
110        match string.to_uppercase().as_str() {
111            "FROM" => Ok(DockerFileCommand::FROM),
112            "ARG" => Ok(DockerFileCommand::ARG),
113            "LABEL" => Ok(DockerFileCommand::LABEL),
114            "MAINTAINER" => Ok(DockerFileCommand::MAINTAINER),
115            "RUN" => Ok(DockerFileCommand::RUN),
116            "COPY" => Ok(DockerFileCommand::COPY),
117            "ADD" => Ok(DockerFileCommand::ADD),
118            "WORKDIR" => Ok(DockerFileCommand::WORKDIR),
119            "ENV" => Ok(DockerFileCommand::ENV),
120            "EXPOSE" => Ok(DockerFileCommand::EXPOSE),
121            "USER" => Ok(DockerFileCommand::USER),
122            "VOLUME" => Ok(DockerFileCommand::VOLUME),
123            "HEALTHCHECK" => Ok(DockerFileCommand::HEALTHCHECK),
124            "CMD" => Ok(DockerFileCommand::CMD),
125            "ENTRYPOINT" => Ok(DockerFileCommand::ENTRYPOINT),
126            _ => Ok(DockerFileCommand::Unknown(string.into())),
127        }
128    }
129}
130
131#[derive(Debug, Clone, PartialEq)]
132pub struct DockerFile {
133    pub lines: Vec<DockerFileLine>,
134}
135
136#[derive(Debug, Clone, PartialEq)]
137pub enum DockerFileLine {
138    Instruction(DockerFileInsctruction),
139    Comment(String),
140    Empty,
141}
142
143pub struct DockerIgnore {
144    pub lines: Vec<DockerIgnoreLine>,
145}
146
147#[derive(Debug, Clone, PartialEq)]
148pub enum DockerIgnoreLine {
149    Pattern(String),
150    NegatePattern(String),
151    Comment(String),
152    Empty,
153}
154
155pub trait DockerfileContent {
156    fn generate_content(&self) -> String;
157}
158
159#[derive(Debug, Clone, PartialEq)]
160pub struct DockerFileInsctruction {
161    pub command: DockerFileCommand,
162    pub content: String,
163    pub options: Vec<InstructionOption>,
164}
165
166#[derive(Debug, Clone, PartialEq)]
167pub enum InstructionOption {
168    Flag(String),
169    WithValue(String, String),
170    WithOptions(String, Vec<InstructionOptionOption>),
171}
172
173#[derive(Debug, Clone, PartialEq)]
174pub struct InstructionOptionOption {
175    pub name: String,
176    pub value: Option<String>,
177}
178
179struct Heredoc {
180    name: String,
181    content: String,
182    ignore_leading: bool,
183    quoted_name: bool,
184}
185
186impl InstructionOptionOption {
187    pub fn new(name: &str, value: String) -> Self {
188        Self {
189            name: name.into(),
190            value: Some(value.into()),
191        }
192    }
193
194    pub fn new_flag(name: &str) -> Self {
195        Self {
196            name: name.into(),
197            value: None,
198        }
199    }
200}
201
202impl DockerfileContent for DockerFileLine {
203    fn generate_content(&self) -> String {
204        match self {
205            DockerFileLine::Instruction(instruction) => instruction.generate_content(),
206            DockerFileLine::Comment(comment) => comment
207                .lines()
208                .map(|l| format!("# {}", l))
209                .collect::<Vec<String>>()
210                .join("\n"),
211            DockerFileLine::Empty => String::new(),
212        }
213    }
214}
215
216impl DockerfileContent for DockerFileInsctruction {
217    fn generate_content(&self) -> String {
218        let separator = if !self.options.is_empty() || self.content.contains("\\\n") {
219            LINE_SEPARATOR
220        } else {
221            " "
222        };
223        let mut content = vec![self.command.to_string()];
224        for option in &self.options {
225            content.push(option.generate_content());
226        }
227        content.push(self.content.clone());
228        content.join(separator)
229    }
230}
231
232impl DockerfileContent for InstructionOption {
233    fn generate_content(&self) -> String {
234        match self {
235            InstructionOption::Flag(name) => format!("--{}", name),
236            InstructionOption::WithValue(name, value) => format!("--{}={}", name, value),
237            InstructionOption::WithOptions(name, options) => format!(
238                "--{}={}",
239                name,
240                options
241                    .iter()
242                    .map(|o| o.generate_content())
243                    .collect::<Vec<String>>()
244                    .join(",")
245            ),
246        }
247    }
248}
249
250impl DockerfileContent for InstructionOptionOption {
251    fn generate_content(&self) -> String {
252        if let Some(value) = &self.value {
253            if value.contains(" ") || value.contains(",") || value.contains("=") {
254                format!("{}='{}'", self.name, value)
255            } else {
256                format!("{}={}", self.name, value)
257            }
258        } else {
259            self.name.clone()
260        }
261    }
262}
263
264impl FromStr for DockerFile {
265    type Err = Error;
266
267    fn from_str(file_content: &str) -> Result<Self, Self::Err> {
268        let mut heredocs: Vec<Heredoc> = vec![];
269        let mut file_content = file_content.to_string();
270        while let Some(pos) = file_content.find("<<") {
271            log::debug!("Found heredoc at position: {}", pos);
272            let subcontent = &file_content[pos..];
273            let len = subcontent.find('\n').expect("Heredoc must have a newline");
274            log::debug!("Heredoc line length: {}", len);
275            let line_end = pos + len;
276            let content_start = line_end + 1;
277            let line = file_content[pos..line_end].to_string();
278            log::debug!("Heredoc line: {}", line);
279            let ignore_leading = "-".eq(file_content[pos + 2..pos + 3].to_string().as_str());
280            let name_start = if ignore_leading { 3 } else { 2 };
281            let name_end = line.find(" ").unwrap_or(line.len());
282            let name = line[name_start..name_end].to_string();
283            let (name, quoted_name) = if name.starts_with('"') {
284                (name[1..name.len() - 1].to_string(), true)
285            } else {
286                (name, false)
287            };
288            log::debug!("Heredoc name: {}", name);
289            let subcontent = &file_content[content_start..];
290            let len = subcontent
291                .find(format!("\n{}\n", name).as_str())
292                .expect(format!("Heredoc end not found for name '{}'", name).as_str());
293            let content_end = content_start + len;
294            let heredoc_block_end = content_end + name.len() + 2;
295            let heredoc_content = file_content[content_start..content_end].to_string();
296            log::debug!("Heredoc content: {}", heredoc_content);
297            let heredoc_id = heredocs.len();
298            file_content = format!(
299                "{}heredoc<{}>{}{}",
300                &file_content[..pos],
301                heredoc_id,
302                &file_content[pos + name_end..content_start],
303                &file_content[heredoc_block_end..]
304            );
305            heredocs.push(Heredoc {
306                name,
307                content: heredoc_content,
308                ignore_leading,
309                quoted_name,
310            });
311        }
312        log::debug!("Final content: {}", file_content);
313        let mut lines = vec![];
314        let regex = Regex::new(DOCKERFILE_LINE_REGEX).expect("Failed to compile regex");
315        log::debug!("Regex: {}", regex);
316        let option_content_regex = Regex::new(option_regex!()).expect("Failed to compile regex");
317
318        let file_content = file_content.as_str();
319
320        for m in regex.find_iter(file_content) {
321            let m = m.as_str();
322            let captures = regex.captures(m).unwrap();
323            if let Some(command) = captures.name("command") {
324                let command = command.as_str();
325                let mut content = captures
326                    .name("content")
327                    .map(|c| c.as_str().to_string())
328                    .ok_or(Error::Custom("Content not found".to_string()))?;
329                while let Some(pos) = content.find("heredoc<") {
330                    let close = content[pos..].find('>').unwrap();
331                    let heredoc_id = &content[pos + 8..pos + close];
332                    let heredoc_id: usize = heredoc_id.parse().unwrap();
333                    let heredoc = &heredocs[heredoc_id];
334                    log::debug!("Heredoc id: {} => {}", heredoc_id, heredoc.name);
335                    let mut heredoc_replacement = heredoc.name.clone();
336                    if heredoc.quoted_name {
337                        heredoc_replacement = format!("\"{}\"", heredoc_replacement);
338                    }
339                    if heredoc.ignore_leading {
340                        heredoc_replacement = format!("-{}", heredoc_replacement);
341                    }
342                    content = format!(
343                        "{}<<{}{}\n{}\n{}",
344                        &content[..pos],
345                        heredoc_replacement,
346                        &content[pos + close + 1..],
347                        heredoc.content,
348                        heredoc.name
349                    );
350                }
351                let options = captures
352                    .name("options")
353                    .map(|o| {
354                        option_content_regex
355                            .find_iter(o.as_str())
356                            .map(|option| {
357                                let option = option.as_str();
358                                let captures = option_content_regex.captures(option).unwrap();
359                                let name = captures.name("name").unwrap().as_str();
360                                let value =
361                                    captures.name("value").map(|v| v.as_str()).unwrap_or("");
362                                if value.is_empty() {
363                                    InstructionOption::Flag(name.to_string())
364                                } else {
365                                    InstructionOption::WithValue(
366                                        name.to_string(),
367                                        value.to_string(),
368                                    )
369                                }
370                            })
371                            .collect::<Vec<_>>()
372                    })
373                    .unwrap_or_default();
374
375                lines.push(DockerFileLine::Instruction(DockerFileInsctruction {
376                    command: command.parse()?,
377                    content,
378                    options,
379                }));
380            } else if m.trim().is_empty() {
381                lines.push(DockerFileLine::Empty);
382            } else if let Some(comment) = captures.name("comment") {
383                lines.push(DockerFileLine::Comment(comment.as_str().to_string()));
384            }
385        }
386        Ok(Self { lines })
387    }
388}
389
390impl ToString for DockerFile {
391    fn to_string(&self) -> String {
392        self.lines
393            .iter()
394            .map(|line| line.generate_content())
395            .collect::<Vec<String>>()
396            .join("\n")
397    }
398}
399
400impl FromStr for DockerIgnore {
401    type Err = Error;
402
403    fn from_str(string: &str) -> Result<Self, Self::Err> {
404        Ok(Self {
405            lines: string
406                .lines()
407                .map(|line| {
408                    let line = line.trim();
409                    if line.is_empty() {
410                        DockerIgnoreLine::Empty
411                    } else if line.starts_with('#') {
412                        DockerIgnoreLine::Comment(line[1..].trim().to_string())
413                    } else if line.starts_with('!') {
414                        DockerIgnoreLine::NegatePattern(line[1..].trim().to_string())
415                    } else {
416                        DockerIgnoreLine::Pattern(line.to_string())
417                    }
418                })
419                .collect(),
420        })
421    }
422}
423
424impl ToString for DockerIgnore {
425    fn to_string(&self) -> String {
426        self.lines
427            .iter()
428            .map(|line| match line {
429                DockerIgnoreLine::Pattern(pattern) => pattern.clone(),
430                DockerIgnoreLine::NegatePattern(pattern) => format!("!{}", pattern),
431                DockerIgnoreLine::Comment(comment) => format!("# {}", comment),
432                DockerIgnoreLine::Empty => String::new(),
433            })
434            .collect::<Vec<String>>()
435            .join("\n")
436    }
437}
438
439#[cfg(test)]
440mod test {
441    use pretty_assertions_sorted::assert_eq_sorted;
442
443    use super::*;
444
445    mod generate {
446        use super::*;
447
448        #[test]
449        fn instruction() {
450            let instruction = DockerFileInsctruction {
451                command: DockerFileCommand::RUN,
452                content: "echo 'Hello, World!'".into(),
453                options: vec![
454                    InstructionOption::Flag("arg1".into()),
455                    InstructionOption::WithValue("arg2".into(), "value2".into()),
456                ],
457            };
458            assert_eq_sorted!(
459                instruction.generate_content(),
460                "RUN \\\n    --arg1 \\\n    --arg2=value2 \\\n    echo 'Hello, World!'"
461            );
462        }
463
464        #[test]
465        fn comment() {
466            let comment = DockerFileLine::Comment("This is a comment".into());
467            assert_eq_sorted!(comment.generate_content(), "# This is a comment");
468        }
469
470        #[test]
471        fn empty() {
472            let empty = DockerFileLine::Empty;
473            assert_eq_sorted!(empty.generate_content(), "");
474        }
475
476        #[test]
477        fn name_only_option() {
478            let option = InstructionOption::Flag("arg1".into());
479            assert_eq_sorted!(option.generate_content(), "--arg1");
480        }
481
482        #[test]
483        fn with_value_option() {
484            let option = InstructionOption::WithValue("arg1".into(), "value1".into());
485            assert_eq_sorted!(option.generate_content(), "--arg1=value1");
486        }
487
488        #[test]
489        fn with_options_option() {
490            let sub_option1 = InstructionOptionOption::new("sub_arg1", "sub_value1".into());
491            let sub_option2 = InstructionOptionOption::new("sub_arg2", "sub_value2".into());
492            let options = vec![sub_option1, sub_option2];
493            let option = InstructionOption::WithOptions("arg1".into(), options);
494            let expected = "--arg1=sub_arg1=sub_value1,sub_arg2=sub_value2";
495            assert_eq_sorted!(option.generate_content(), expected);
496        }
497
498        #[test]
499        fn instruction_option_option() {
500            let option = InstructionOptionOption::new("arg1", "value1".into());
501            let expected = "arg1=value1";
502            assert_eq_sorted!(option.generate_content(), expected);
503        }
504    }
505
506    mod parse {
507        use super::*;
508
509        #[test]
510        fn simple() {
511            let dockerfile: DockerFile = r#"FROM alpine:3.11 as builder
512RUN echo "hello world" > /hello-world
513# This is a comment
514
515FROM scratch
516COPY --from=builder /hello-world /hello-world
517"#
518            .parse()
519            .unwrap();
520
521            let lines = dockerfile.lines;
522            assert_eq!(lines.len(), 6);
523
524            assert_eq_sorted!(
525                lines[0],
526                DockerFileLine::Instruction(DockerFileInsctruction {
527                    command: DockerFileCommand::FROM,
528                    content: "alpine:3.11 as builder".to_string(),
529                    options: vec![]
530                })
531            );
532            assert_eq_sorted!(
533                lines[1],
534                DockerFileLine::Instruction(DockerFileInsctruction {
535                    command: DockerFileCommand::RUN,
536                    content: "echo \"hello world\" > /hello-world".to_string(),
537                    options: vec![]
538                })
539            );
540            assert_eq_sorted!(
541                lines[2],
542                DockerFileLine::Comment("This is a comment".to_string())
543            );
544            assert_eq!(lines[3], DockerFileLine::Empty);
545            assert_eq_sorted!(
546                lines[4],
547                DockerFileLine::Instruction(DockerFileInsctruction {
548                    command: DockerFileCommand::FROM,
549                    content: "scratch".to_string(),
550                    options: vec![]
551                })
552            );
553            assert_eq_sorted!(
554                lines[5],
555                DockerFileLine::Instruction(DockerFileInsctruction {
556                    command: DockerFileCommand::COPY,
557                    content: "/hello-world /hello-world".to_string(),
558                    options: vec![InstructionOption::WithValue(
559                        "from".to_string(),
560                        "builder".to_string()
561                    )]
562                })
563            );
564        }
565
566        #[test]
567        fn args() {
568            let dockerfile: DockerFile = r#"FROM alpine:3.11 as builder
569ARG arg1 \
570    arg2=value2
571ARG arg3=3
572"#
573            .parse()
574            .unwrap();
575
576            let lines = dockerfile.lines;
577            assert_eq!(lines.len(), 3);
578
579            assert_eq_sorted!(
580                lines[0],
581                DockerFileLine::Instruction(DockerFileInsctruction {
582                    command: DockerFileCommand::FROM,
583                    content: "alpine:3.11 as builder".to_string(),
584                    options: vec![]
585                })
586            );
587            assert_eq_sorted!(
588                lines[1],
589                DockerFileLine::Instruction(DockerFileInsctruction {
590                    command: DockerFileCommand::ARG,
591                    content: "arg1 \\\n    arg2=value2".to_string(),
592                    options: vec![]
593                })
594            );
595            assert_eq_sorted!(
596                lines[2],
597                DockerFileLine::Instruction(DockerFileInsctruction {
598                    command: DockerFileCommand::ARG,
599                    content: "arg3=3".to_string(),
600                    options: vec![]
601                })
602            );
603        }
604
605        mod full_file {
606            use super::*;
607
608            #[test]
609            fn php_dockerfile() {
610                let dockerfile: DockerFile = r#"# syntax=docker/dockerfile:1.11
611# This file is generated by Dofigen v0.0.0
612# See https://github.com/lenra-io/dofigen
613
614# get-composer
615FROM composer:latest AS get-composer
616
617# install-deps
618FROM php:8.3-fpm-alpine AS install-deps
619USER 0:0
620RUN <<EOF
621apt-get update
622apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev icu-dev mysql-client
623EOF
624
625# install-php-ext
626FROM install-deps AS install-php-ext
627USER 0:0
628RUN <<EOF
629docker-php-ext-configure zip
630docker-php-ext-install bcmath gd intl pdo_mysql zip
631EOF
632
633# runtime
634FROM install-php-ext AS runtime
635WORKDIR /
636COPY \
637    --from=get-composer \
638    --chown=www-data \
639    --link \
640    "/usr/bin/composer" "/bin/"
641ADD \
642    --chown=www-data \
643    --link \
644    "https://github.com/pelican-dev/panel.git" "/tmp/pelican"
645USER www-data
646RUN <<EOF
647cd /tmp/pelican
648cp .env.example .env
649mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache
650chmod 777 -R bootstrap storage
651composer install --no-dev --optimize-autoloader
652rm -rf .env bootstrap/cache/*.php
653mkdir -p /app/storage/logs/
654chown -R nginx:nginx .
655rm /usr/local/etc/php-fpm.conf
656echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root
657mkdir -p /var/run/php /var/run/nginx
658mv .github/docker/default.conf /etc/nginx/http.d/default.conf
659mv .github/docker/supervisord.conf /etc/supervisord.conf
660EOF
661"#.parse().unwrap();
662
663                let lines = dockerfile.lines;
664                let mut line = 0;
665
666                assert_eq_sorted!(
667                    lines[line],
668                    DockerFileLine::Comment("syntax=docker/dockerfile:1.11".to_string())
669                );
670                line += 1;
671                assert_eq_sorted!(
672                    lines[line],
673                    DockerFileLine::Comment("This file is generated by Dofigen v0.0.0".to_string())
674                );
675                line += 1;
676                assert_eq_sorted!(
677                    lines[line],
678                    DockerFileLine::Comment("See https://github.com/lenra-io/dofigen".to_string())
679                );
680                line += 1;
681                assert_eq_sorted!(lines[line], DockerFileLine::Empty);
682                line += 1;
683                assert_eq_sorted!(
684                    lines[line],
685                    DockerFileLine::Comment("get-composer".to_string())
686                );
687                line += 1;
688                assert_eq_sorted!(
689                    lines[line],
690                    DockerFileLine::Instruction(DockerFileInsctruction {
691                        command: DockerFileCommand::FROM,
692                        content: "composer:latest AS get-composer".to_string(),
693                        options: vec![]
694                    })
695                );
696                line += 1;
697                assert_eq_sorted!(lines[line], DockerFileLine::Empty);
698                line += 1;
699                assert_eq_sorted!(
700                    lines[line],
701                    DockerFileLine::Comment("install-deps".to_string())
702                );
703                line += 1;
704                assert_eq_sorted!(
705                    lines[line],
706                    DockerFileLine::Instruction(DockerFileInsctruction {
707                        command: DockerFileCommand::FROM,
708                        content: "php:8.3-fpm-alpine AS install-deps".to_string(),
709                        options: vec![]
710                    })
711                );
712                line += 1;
713                assert_eq_sorted!(
714                    lines[line],
715                    DockerFileLine::Instruction(DockerFileInsctruction {
716                        command: DockerFileCommand::USER,
717                        content: "0:0".to_string(),
718                        options: vec![]
719                    })
720                );
721                line += 1;
722                assert_eq_sorted!(
723                    lines[line],
724                    DockerFileLine::Instruction(DockerFileInsctruction {
725                        command: DockerFileCommand::RUN,
726                        content: r#"<<EOF
727apt-get update
728apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev icu-dev mysql-client
729EOF"#.to_string(),
730                        options: vec![]
731                    })
732                );
733                line += 1;
734                assert_eq_sorted!(lines[line], DockerFileLine::Empty);
735                line += 1;
736                assert_eq_sorted!(
737                    lines[line],
738                    DockerFileLine::Comment("install-php-ext".to_string())
739                );
740                line += 1;
741                assert_eq_sorted!(
742                    lines[line],
743                    DockerFileLine::Instruction(DockerFileInsctruction {
744                        command: DockerFileCommand::FROM,
745                        content: "install-deps AS install-php-ext".to_string(),
746                        options: vec![]
747                    })
748                );
749                line += 1;
750                assert_eq_sorted!(
751                    lines[line],
752                    DockerFileLine::Instruction(DockerFileInsctruction {
753                        command: DockerFileCommand::USER,
754                        content: "0:0".to_string(),
755                        options: vec![]
756                    })
757                );
758                line += 1;
759                assert_eq_sorted!(
760                    lines[line],
761                    DockerFileLine::Instruction(DockerFileInsctruction {
762                        command: DockerFileCommand::RUN,
763                        content: r#"<<EOF
764docker-php-ext-configure zip
765docker-php-ext-install bcmath gd intl pdo_mysql zip
766EOF"#
767                            .to_string(),
768                        options: vec![]
769                    })
770                );
771                line += 1;
772                assert_eq_sorted!(lines[line], DockerFileLine::Empty);
773                line += 1;
774                assert_eq_sorted!(lines[line], DockerFileLine::Comment("runtime".to_string()));
775                line += 1;
776                assert_eq_sorted!(
777                    lines[line],
778                    DockerFileLine::Instruction(DockerFileInsctruction {
779                        command: DockerFileCommand::FROM,
780                        content: "install-php-ext AS runtime".to_string(),
781                        options: vec![]
782                    })
783                );
784                line += 1;
785                assert_eq_sorted!(
786                    lines[line],
787                    DockerFileLine::Instruction(DockerFileInsctruction {
788                        command: DockerFileCommand::WORKDIR,
789                        content: "/".to_string(),
790                        options: vec![]
791                    })
792                );
793                line += 1;
794                assert_eq_sorted!(
795                    lines[line],
796                    DockerFileLine::Instruction(DockerFileInsctruction {
797                        command: DockerFileCommand::COPY,
798                        content: "\"/usr/bin/composer\" \"/bin/\"".to_string(),
799                        options: vec![
800                            InstructionOption::WithValue(
801                                "from".to_string(),
802                                "get-composer".to_string()
803                            ),
804                            InstructionOption::WithValue(
805                                "chown".to_string(),
806                                "www-data".to_string()
807                            ),
808                            InstructionOption::Flag("link".to_string())
809                        ]
810                    })
811                );
812                line += 1;
813                assert_eq_sorted!(
814                    lines[line],
815                    DockerFileLine::Instruction(DockerFileInsctruction {
816                        command: DockerFileCommand::ADD,
817                        content: r#""https://github.com/pelican-dev/panel.git" "/tmp/pelican""#
818                            .to_string(),
819                        options: vec![
820                            InstructionOption::WithValue(
821                                "chown".to_string(),
822                                "www-data".to_string()
823                            ),
824                            InstructionOption::Flag("link".to_string())
825                        ]
826                    })
827                );
828                line += 1;
829                assert_eq_sorted!(
830                    lines[line],
831                    DockerFileLine::Instruction(DockerFileInsctruction {
832                        command: DockerFileCommand::USER,
833                        content: "www-data".to_string(),
834                        options: vec![]
835                    })
836                );
837                line += 1;
838                assert_eq_sorted!(
839                    lines[line],
840                    DockerFileLine::Instruction(DockerFileInsctruction {
841                        command: DockerFileCommand::RUN,
842                        content: r#"<<EOF
843cd /tmp/pelican
844cp .env.example .env
845mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache
846chmod 777 -R bootstrap storage
847composer install --no-dev --optimize-autoloader
848rm -rf .env bootstrap/cache/*.php
849mkdir -p /app/storage/logs/
850chown -R nginx:nginx .
851rm /usr/local/etc/php-fpm.conf
852echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root
853mkdir -p /var/run/php /var/run/nginx
854mv .github/docker/default.conf /etc/nginx/http.d/default.conf
855mv .github/docker/supervisord.conf /etc/supervisord.conf
856EOF"#.to_string(),
857                        options: vec![]
858                    })
859                );
860            }
861        }
862    }
863}