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 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 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 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 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 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 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}