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 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 r"|[^\S\r\n]*",
54 r"|[^\S\r\n]*# ?(?<comment>.+)",
56 r")(?:\r?\n|$)",
58);
59
60#[derive(Debug, Clone, PartialEq, Deserialize)]
61pub enum DockerFileCommand {
62 FROM,
63 ARG,
64 LABEL,
65 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}