dofigen_lib/
dofigen_struct.rs

1use crate::deserialize::*;
2#[cfg(feature = "json_schema")]
3use crate::json_schema::optional_string_or_number_schema;
4#[cfg(feature = "json_schema")]
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::{collections::HashMap, path::PathBuf};
8use struct_patch::Patch;
9use url::Url;
10
11/** Represents the Dockerfile main stage */
12#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
13#[serde(rename_all = "camelCase")]
14#[patch(
15    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
16    // attribute(serde(deny_unknown_fields)),
17    attribute(serde(default, rename_all = "camelCase"))
18)]
19#[cfg_attr(
20    feature = "json_schema",
21    patch(
22        attribute(derive(JsonSchema)),
23        attribute(schemars(
24            title = "Dofigen",
25            rename = "Dofigen",
26            extend("$id" = "https://json.schemastore.org/dofigen.json"),
27            description = "Dofigen is a Dockerfile generator using a simplified description in YAML or JSON format"
28        ))
29    )
30)]
31pub struct Dofigen {
32    /// The context of the Docker build
33    /// This is used to generate a .dockerignore file
34    #[patch(name = "VecPatch<String>")]
35    #[serde(skip_serializing_if = "Vec::is_empty")]
36    pub context: Vec<String>,
37
38    /// The elements to ignore from the build context
39    /// This is used to generate a .dockerignore file
40    #[patch(name = "VecPatch<String>")]
41    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "ignores"))))]
42    #[serde(skip_serializing_if = "Vec::is_empty")]
43    pub ignore: Vec<String>,
44
45    /// The global build args of the Dockerfile
46    /// See: https://docs.docker.com/build/building/variables/#scoping
47    #[patch(name = "HashMapPatch<String, String>")]
48    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "globalArgs"))))]
49    #[serde(skip_serializing_if = "HashMap::is_empty")]
50    pub global_arg: HashMap<String, String>,
51
52    /// The builder stages of the Dockerfile
53    #[patch(name = "HashMapDeepPatch<String, StagePatch>")]
54    #[serde(skip_serializing_if = "HashMap::is_empty")]
55    pub builders: HashMap<String, Stage>,
56
57    /// The runtime stage of the Dockerfile
58    #[patch(name = "StagePatch", attribute(serde(flatten)))]
59    #[serde(flatten)]
60    pub stage: Stage,
61
62    /// The entrypoint of the Dockerfile
63    /// See https://docs.docker.com/reference/dockerfile/#entrypoint
64    #[patch(name = "VecPatch<String>")]
65    #[serde(skip_serializing_if = "Vec::is_empty")]
66    pub entrypoint: Vec<String>,
67
68    /// The default command of the Dockerfile
69    /// See https://docs.docker.com/reference/dockerfile/#cmd
70    #[patch(name = "VecPatch<String>")]
71    #[serde(skip_serializing_if = "Vec::is_empty")]
72    pub cmd: Vec<String>,
73
74    /// Create volume mounts
75    /// See https://docs.docker.com/reference/dockerfile/#volume
76    #[patch(name = "VecPatch<String>")]
77    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "volumes"))))]
78    #[serde(skip_serializing_if = "Vec::is_empty")]
79    pub volume: Vec<String>,
80
81    /// The ports exposed by the Dockerfile
82    /// See https://docs.docker.com/reference/dockerfile/#expose
83    #[cfg_attr(
84        feature = "permissive",
85        patch(name = "VecDeepPatch<Port, ParsableStruct<PortPatch>>")
86    )]
87    #[cfg_attr(
88        not(feature = "permissive"),
89        patch(name = "VecDeepPatch<Port, PortPatch>")
90    )]
91    #[cfg_attr(
92        not(feature = "strict"),
93        patch(attribute(serde(alias = "port", alias = "ports")))
94    )]
95    #[serde(skip_serializing_if = "Vec::is_empty")]
96    pub expose: Vec<Port>,
97
98    /// The healthcheck of the Dockerfile
99    /// See https://docs.docker.com/reference/dockerfile/#healthcheck
100    #[patch(name = "Option<HealthcheckPatch>")]
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub healthcheck: Option<Healthcheck>,
103}
104
105/// Represents a Dockerfile stage
106#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
107#[patch(
108    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
109    // attribute(serde(deny_unknown_fields)),
110    attribute(serde(default)),
111)]
112#[cfg_attr(
113    feature = "json_schema",
114    patch(
115        attribute(derive(JsonSchema)),
116        attribute(schemars(title = "Stage", rename = "Stage"))
117    )
118)]
119pub struct Stage {
120    /// The base of the stage
121    /// See https://docs.docker.com/reference/dockerfile/#from
122    #[serde(flatten, skip_serializing_if = "FromContext::is_empty")]
123    #[patch(name = "FromContextPatch", attribute(serde(flatten, default)))]
124    pub from: FromContext,
125
126    /// Add metadata to an image
127    /// See https://docs.docker.com/reference/dockerfile/#label
128    #[cfg_attr(not(feature = "strict"), patch(name = "NestedMap<String>"))]
129    #[cfg_attr(feature = "strict", patch(name = "HashMapPatch<String, String>"))]
130    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "labels"))))]
131    #[serde(skip_serializing_if = "HashMap::is_empty")]
132    pub label: HashMap<String, String>,
133
134    /// The user and group of the stage
135    /// See https://docs.docker.com/reference/dockerfile/#user
136    #[cfg_attr(
137        feature = "permissive",
138        patch(name = "Option<ParsableStruct<UserPatch>>")
139    )]
140    #[cfg_attr(not(feature = "permissive"), patch(name = "Option<UserPatch>"))]
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub user: Option<User>,
143
144    /// The working directory of the stage
145    /// See https://docs.docker.com/reference/dockerfile/#workdir
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub workdir: Option<String>,
148
149    /// The build args that can be used in the stage
150    /// See https://docs.docker.com/reference/dockerfile/#arg
151    #[patch(name = "HashMapPatch<String, String>")]
152    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "args"))))]
153    #[serde(skip_serializing_if = "HashMap::is_empty")]
154    pub arg: HashMap<String, String>,
155
156    /// The environment variables of the stage
157    /// See https://docs.docker.com/reference/dockerfile/#env
158    #[patch(name = "HashMapPatch<String, String>")]
159    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "envs"))))]
160    #[serde(skip_serializing_if = "HashMap::is_empty")]
161    pub env: HashMap<String, String>,
162
163    /// The copy instructions of the stage
164    /// See https://docs.docker.com/reference/dockerfile/#copy and https://docs.docker.com/reference/dockerfile/#add
165    #[cfg_attr(
166        not(feature = "strict"),
167        patch(attribute(serde(
168            alias = "add",
169            alias = "adds",
170            alias = "artifact",
171            alias = "artifacts"
172        )))
173    )]
174    #[cfg_attr(
175        feature = "permissive",
176        patch(name = "VecDeepPatch<CopyResource, ParsableStruct<CopyResourcePatch>>")
177    )]
178    #[cfg_attr(
179        not(feature = "permissive"),
180        patch(name = "VecDeepPatch<CopyResource, CopyResourcePatch>")
181    )]
182    #[serde(skip_serializing_if = "Vec::is_empty")]
183    pub copy: Vec<CopyResource>,
184
185    /// The run instructions of the stage as root user
186    #[patch(name = "Option<RunPatch>")]
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub root: Option<Run>,
189
190    /// The run instructions of the stage
191    /// See https://docs.docker.com/reference/dockerfile/#run
192    #[patch(name = "RunPatch", attribute(serde(flatten)))]
193    #[serde(flatten)]
194    pub run: Run,
195}
196
197/// Represents a run command
198#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
199#[patch(
200    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
201    // attribute(serde(deny_unknown_fields)),
202    attribute(serde(default)),
203)]
204#[cfg_attr(
205    feature = "json_schema",
206    patch(
207        attribute(derive(JsonSchema)),
208        attribute(schemars(title = "Run", rename = "Run"))
209    )
210)]
211pub struct Run {
212    /// The commands to run
213    #[patch(name = "VecPatch<String>")]
214    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "script"))))]
215    #[serde(skip_serializing_if = "Vec::is_empty")]
216    pub run: Vec<String>,
217
218    /// The shell to use for the RUN command
219    /// See https://docs.docker.com/reference/dockerfile/#shell
220    #[patch(name = "VecPatch<String>")]
221    #[serde(skip_serializing_if = "Vec::is_empty")]
222    pub shell: Vec<String>,
223
224    /// The cache definitions during the run
225    /// See https://docs.docker.com/reference/dockerfile/#run---mounttypecache
226    #[cfg_attr(
227        feature = "permissive",
228        patch(name = "VecDeepPatch<Cache, ParsableStruct<CachePatch>>")
229    )]
230    #[cfg_attr(
231        not(feature = "permissive"),
232        patch(name = "VecDeepPatch<Cache, CachePatch>")
233    )]
234    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "caches"))))]
235    #[serde(skip_serializing_if = "Vec::is_empty")]
236    pub cache: Vec<Cache>,
237
238    /// The file system bindings during the run
239    /// This is used to mount a file or directory from the host into the container only during the run and it's faster than a copy
240    /// See https://docs.docker.com/reference/dockerfile/#run---mounttypebind
241    #[cfg_attr(
242        feature = "permissive",
243        patch(name = "VecDeepPatch<Bind, ParsableStruct<BindPatch>>")
244    )]
245    #[cfg_attr(
246        not(feature = "permissive"),
247        patch(name = "VecDeepPatch<Bind, BindPatch>")
248    )]
249    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "binds"))))]
250    #[serde(skip_serializing_if = "Vec::is_empty")]
251    pub bind: Vec<Bind>,
252}
253
254/// Represents a cache definition during a run
255/// See https://docs.docker.com/reference/dockerfile/#run---mounttypecache
256#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
257#[patch(
258    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
259    attribute(serde(default))
260)]
261#[cfg_attr(
262    feature = "json_schema",
263    patch(
264        attribute(derive(JsonSchema)),
265        attribute(schemars(title = "Cache", rename = "Cache"))
266    )
267)]
268pub struct Cache {
269    /// The id of the cache
270    /// This is used to share the cache between different stages
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub id: Option<String>,
273
274    /// The target path of the cache
275    #[cfg_attr(
276        not(feature = "strict"),
277        patch(attribute(serde(alias = "dst", alias = "destination")))
278    )]
279    pub target: String,
280
281    /// Defines if the cache is readonly
282    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "ro"))))]
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub readonly: Option<bool>,
285
286    /// The sharing strategy of the cache
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub sharing: Option<CacheSharing>,
289
290    /// Build stage, context, or image name to use as a base of the cache mount. Defaults to empty directory.
291    #[serde(flatten, skip_serializing_if = "FromContext::is_empty")]
292    #[patch(name = "FromContextPatch", attribute(serde(flatten)))]
293    pub from: FromContext,
294
295    /// Subpath in the from to mount. Defaults to the root of the from
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub source: Option<String>,
298
299    /// The permissions of the cache
300    #[cfg_attr(
301        feature = "permissive",
302        patch(attribute(serde(
303            deserialize_with = "deserialize_from_optional_string_or_number",
304            default
305        )))
306    )]
307    #[serde(skip_serializing_if = "Option::is_none")]
308    #[cfg_attr(
309        feature = "json_schema",
310        patch(attribute(schemars(schema_with = "optional_string_or_number_schema")))
311    )]
312    pub chmod: Option<String>,
313
314    /// The user and group that own the cache
315    #[patch(name = "Option<UserPatch>")]
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub chown: Option<User>,
318}
319
320/// Represents file system binding during a run
321/// See https://docs.docker.com/reference/dockerfile/#run---mounttypebind
322#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
323#[patch(
324    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
325    attribute(serde(default))
326)]
327#[cfg_attr(
328    feature = "json_schema",
329    patch(
330        attribute(derive(JsonSchema)),
331        attribute(schemars(title = "Bind", rename = "Bind"))
332    )
333)]
334pub struct Bind {
335    /// The target path of the bind
336    pub target: String,
337
338    /// The base of the bind
339    #[serde(flatten, skip_serializing_if = "FromContext::is_empty")]
340    #[patch(name = "FromContextPatch", attribute(serde(flatten)))]
341    pub from: FromContext,
342
343    /// Source path in the from. Defaults to the root of the from
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub source: Option<String>,
346
347    /// Defines if the bind is read and write
348    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "rw"))))]
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub readwrite: Option<bool>,
351}
352
353/// Represents the Dockerfile healthcheck instruction
354/// See https://docs.docker.com/reference/dockerfile/#healthcheck
355#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch)]
356#[patch(
357    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
358    attribute(serde(deny_unknown_fields, default))
359)]
360#[cfg_attr(
361    feature = "json_schema",
362    patch(
363        attribute(derive(JsonSchema)),
364        attribute(schemars(title = "Healthcheck", rename = "Healthcheck"))
365    )
366)]
367pub struct Healthcheck {
368    /// The test to run
369    pub cmd: String,
370
371    /// The interval between two tests
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub interval: Option<String>,
374
375    /// The timeout of the test
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub timeout: Option<String>,
378
379    /// The start period of the test
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub start: Option<String>,
382
383    /// The number of retries
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub retries: Option<u16>,
386}
387
388/// Represents a Docker image name
389#[derive(Serialize, Debug, Clone, PartialEq, Default, Patch, Hash, Eq, PartialOrd)]
390#[patch(
391    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
392    attribute(serde(deny_unknown_fields, default))
393)]
394#[cfg_attr(
395    feature = "json_schema",
396    patch(
397        attribute(derive(JsonSchema)),
398        attribute(schemars(title = "ImageName", rename = "ImageName"))
399    )
400)]
401pub struct ImageName {
402    /// The host of the image registry
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub host: Option<String>,
405
406    /// The port of the image registry
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub port: Option<u16>,
409
410    /// The path of the image repository
411    pub path: String,
412
413    /// The version of the image
414    #[serde(flatten, skip_serializing_if = "Option::is_none")]
415    #[patch(attribute(serde(flatten)))]
416    pub version: Option<ImageVersion>,
417
418    /// The optional platform option can be used to specify the platform of the image in case FROM references a multi-platform image.
419    /// For example, linux/amd64, linux/arm64, or windows/amd64.
420    /// By default, the target platform of the build request is used. Global build arguments can be used in the value of this flag, for example automatic platform ARGs allow you to force a stage to native build platform (platform: $BUILDPLATFORM), and use it to cross-compile to the target platform inside the stage.
421    /// See https://docs.docker.com/reference/dockerfile/#from
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub platform: Option<String>,
424}
425
426/// Represents the COPY instruction in a Dockerfile.
427/// See https://docs.docker.com/reference/dockerfile/#copy
428#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
429#[patch(
430    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
431    attribute(serde(deny_unknown_fields, default))
432)]
433#[cfg_attr(
434    feature = "json_schema",
435    patch(
436        attribute(derive(JsonSchema)),
437        attribute(schemars(title = "Copy", rename = "Copy"))
438    )
439)]
440pub struct Copy {
441    /// The origin of the copy
442    /// See https://docs.docker.com/reference/dockerfile/#copy---from
443    #[serde(flatten, skip_serializing_if = "FromContext::is_empty")]
444    #[patch(name = "FromContextPatch", attribute(serde(flatten)))]
445    pub from: FromContext,
446
447    /// The paths to copy
448    #[patch(name = "VecPatch<String>")]
449    #[cfg_attr(
450        not(feature = "strict"),
451        patch(attribute(serde(alias = "path", alias = "source")))
452    )]
453    #[serde(skip_serializing_if = "Vec::is_empty")]
454    pub paths: Vec<String>,
455
456    /// The options of the copy
457    #[serde(flatten)]
458    #[patch(name = "CopyOptionsPatch", attribute(serde(flatten)))]
459    pub options: CopyOptions,
460
461    /// See https://docs.docker.com/reference/dockerfile/#copy---exclude
462    #[patch(name = "VecPatch<String>")]
463    #[serde(skip_serializing_if = "Vec::is_empty")]
464    pub exclude: Vec<String>,
465
466    /// See https://docs.docker.com/reference/dockerfile/#copy---parents
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub parents: Option<bool>,
469}
470
471/// Represents the COPY instruction in a Dockerfile from file content.
472/// See https://docs.docker.com/reference/dockerfile/#example-creating-inline-files
473#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
474#[patch(
475    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
476    attribute(serde(deny_unknown_fields, default))
477)]
478#[cfg_attr(
479    feature = "json_schema",
480    patch(
481        attribute(derive(JsonSchema)),
482        attribute(schemars(title = "CopyContent", rename = "CopyContent"))
483    )
484)]
485pub struct CopyContent {
486    /// Content of the file to copy
487    pub content: String,
488
489    /// If true, replace variables in the content at build time. Default is true.
490    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "subst"))))]
491    #[serde(skip_serializing_if = "Option::is_none")]
492    pub substitute: Option<bool>,
493
494    /// The options of the copy
495    #[serde(flatten)]
496    #[patch(name = "CopyOptionsPatch", attribute(serde(flatten)))]
497    pub options: CopyOptions,
498}
499
500/// Represents the ADD instruction in a Dockerfile specific for Git repo.
501/// See https://docs.docker.com/reference/dockerfile/#adding-private-git-repositories
502#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
503#[patch(
504    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
505    attribute(serde(deny_unknown_fields, default, rename_all = "camelCase"))
506)]
507#[cfg_attr(
508    feature = "json_schema",
509    patch(
510        attribute(derive(JsonSchema)),
511        attribute(schemars(title = "AddGitRepo", rename = "AddGitRepo"))
512    )
513)]
514pub struct AddGitRepo {
515    /// The URL of the Git repository
516    pub repo: String,
517
518    /// The options of the copy
519    #[serde(flatten)]
520    #[patch(name = "CopyOptionsPatch", attribute(serde(flatten)))]
521    pub options: CopyOptions,
522
523    /// See https://docs.docker.com/reference/dockerfile/#copy---exclude
524    #[patch(name = "VecPatch<String>")]
525    #[serde(skip_serializing_if = "Vec::is_empty")]
526    pub exclude: Vec<String>,
527
528    /// Keep the git directory
529    /// See https://docs.docker.com/reference/dockerfile/#add---keep-git-dir
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub keep_git_dir: Option<bool>,
532}
533
534/// Represents the ADD instruction in a Dockerfile file from URLs or uncompress an archive.
535#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
536#[patch(
537    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
538    attribute(serde(deny_unknown_fields, default))
539)]
540#[cfg_attr(
541    feature = "json_schema",
542    patch(
543        attribute(derive(JsonSchema)),
544        attribute(schemars(title = "Add", rename = "Add"))
545    )
546)]
547pub struct Add {
548    /// The files to add
549    #[patch(name = "VecPatch<Resource>")]
550    #[cfg_attr(not(feature = "strict"), patch(attribute(serde(alias = "file"))))]
551    #[serde(skip_serializing_if = "Vec::is_empty")]
552    pub files: Vec<Resource>,
553
554    /// The options of the copy
555    #[serde(flatten)]
556    #[patch(name = "CopyOptionsPatch", attribute(serde(flatten)))]
557    pub options: CopyOptions,
558
559    /// The checksum of the files
560    /// See https://docs.docker.com/reference/dockerfile/#add---checksum
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub checksum: Option<String>,
563}
564
565/// Represents the options of a COPY/ADD instructions
566#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
567#[patch(
568    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
569    attribute(serde(deny_unknown_fields, default))
570)]
571#[cfg_attr(
572    feature = "json_schema",
573    patch(
574        attribute(derive(JsonSchema)),
575        attribute(schemars(title = "CopyOptions", rename = "CopyOptions"))
576    )
577)]
578pub struct CopyOptions {
579    /// The target path of the copied files
580    #[cfg_attr(
581        not(feature = "strict"),
582        patch(attribute(serde(alias = "destination")))
583    )]
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub target: Option<String>,
586
587    /// The user and group that own the copied files
588    /// See https://docs.docker.com/reference/dockerfile/#copy---chown---chmod
589    #[patch(name = "Option<UserPatch>")]
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub chown: Option<User>,
592
593    /// The permissions of the copied files
594    /// See https://docs.docker.com/reference/dockerfile/#copy---chown---chmod
595    #[cfg_attr(
596        feature = "permissive",
597        patch(attribute(serde(
598            deserialize_with = "deserialize_from_optional_string_or_number",
599            default
600        )))
601    )]
602    #[serde(skip_serializing_if = "Option::is_none")]
603    #[cfg_attr(
604        feature = "json_schema",
605        patch(attribute(schemars(schema_with = "optional_string_or_number_schema")))
606    )]
607    pub chmod: Option<String>,
608
609    /// Use of the link flag
610    /// See https://docs.docker.com/reference/dockerfile/#copy---link
611    #[serde(skip_serializing_if = "Option::is_none")]
612    pub link: Option<bool>,
613}
614
615/// Represents user and group definition
616#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
617#[patch(
618    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
619    attribute(serde(deny_unknown_fields, default))
620)]
621#[cfg_attr(
622    feature = "json_schema",
623    patch(
624        attribute(derive(JsonSchema)),
625        attribute(schemars(title = "User", rename = "User"))
626    )
627)]
628pub struct User {
629    /// The user name or ID
630    /// The ID is preferred
631    pub user: String,
632
633    /// The group name or ID
634    /// The ID is preferred
635    #[serde(skip_serializing_if = "Option::is_none")]
636    pub group: Option<String>,
637}
638
639/// Represents a port definition
640#[derive(Debug, Clone, PartialEq, Default, Serialize, Patch)]
641#[patch(
642    attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
643    attribute(serde(deny_unknown_fields, default))
644)]
645#[cfg_attr(
646    feature = "json_schema",
647    patch(
648        attribute(derive(JsonSchema)),
649        attribute(schemars(title = "Port", rename = "Port"))
650    )
651)]
652pub struct Port {
653    /// The port number
654    pub port: u16,
655
656    /// The protocol of the port
657    #[serde(skip_serializing_if = "Option::is_none")]
658    pub protocol: Option<PortProtocol>,
659}
660
661///////////////// Enums //////////////////
662
663/// Represents a Docker image version
664#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Hash, Eq, PartialOrd)]
665#[serde(rename_all = "camelCase")]
666#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
667pub enum ImageVersion {
668    Tag(String),
669    Digest(String),
670}
671
672/// Represents a copy origin
673#[derive(Serialize, Debug, Clone, PartialEq)]
674#[serde(rename_all = "camelCase")]
675pub enum FromContext {
676    FromImage(ImageName),
677    FromBuilder(String),
678    FromContext(Option<String>),
679}
680
681#[derive(Serialize, Debug, Clone, PartialEq)]
682#[serde(untagged)]
683pub enum CopyResource {
684    Copy(Copy),
685    Content(CopyContent),
686    AddGitRepo(AddGitRepo),
687    Add(Add),
688}
689
690/// Represents a cache sharing strategy
691#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
692#[serde(rename_all = "camelCase")]
693#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
694pub enum CacheSharing {
695    Shared,
696    Private,
697    Locked,
698}
699
700/// Represents a port protocol
701#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
702#[serde(rename_all = "camelCase")]
703#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
704pub enum PortProtocol {
705    Tcp,
706    Udp,
707}
708
709/// Represents a resource
710#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Hash, Eq, PartialOrd)]
711#[serde(untagged)]
712#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
713pub enum Resource {
714    Url(Url),
715    File(PathBuf),
716}
717
718///////////////// Enum Patches //////////////////
719
720#[derive(Debug, Clone, PartialEq, Deserialize)]
721#[serde(rename_all = "camelCase")]
722#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
723pub enum FromContextPatch {
724    #[cfg(not(feature = "permissive"))]
725    #[cfg_attr(not(feature = "strict"), serde(alias = "image"))]
726    FromImage(ImageNamePatch),
727
728    #[cfg(feature = "permissive")]
729    #[cfg_attr(not(feature = "strict"), serde(alias = "image"))]
730    FromImage(ParsableStruct<ImageNamePatch>),
731
732    #[cfg_attr(not(feature = "strict"), serde(alias = "builder"))]
733    FromBuilder(String),
734
735    #[cfg_attr(not(feature = "strict"), serde(alias = "from"))]
736    FromContext(Option<String>),
737}
738
739#[derive(Debug, Clone, PartialEq, Deserialize)]
740#[serde(untagged)]
741#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
742pub enum CopyResourcePatch {
743    Copy(CopyPatch),
744    Content(CopyContentPatch),
745    AddGitRepo(AddGitRepoPatch),
746    Add(AddPatch),
747    Unknown(UnknownPatch),
748}
749
750#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
751#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
752pub struct UnknownPatch {
753    #[serde(flatten)]
754    pub options: Option<CopyOptionsPatch>,
755    pub exclude: Option<VecPatch<String>>,
756}
757
758///////////////// Tests //////////////////
759
760#[cfg(test)]
761mod test {
762    use super::*;
763    use pretty_assertions_sorted::assert_eq_sorted;
764
765    mod deserialize {
766        use super::*;
767
768        mod dofigen {
769            use super::*;
770
771            #[test]
772            fn empty() {
773                let data = r#""#;
774
775                let dofigen: DofigenPatch = serde_yaml::from_str(data).unwrap();
776                let dofigen: Dofigen = dofigen.into();
777
778                assert_eq_sorted!(dofigen, Dofigen::default());
779            }
780
781            #[test]
782            fn from() {
783                let data = r#"
784                fromImage:
785                  path: ubuntu
786                "#;
787
788                let dofigen: DofigenPatch = serde_yaml::from_str(data).unwrap();
789                let dofigen: Dofigen = dofigen.into();
790
791                assert_eq_sorted!(
792                    dofigen,
793                    Dofigen {
794                        stage: Stage {
795                            from: FromContext::FromImage(ImageName {
796                                path: "ubuntu".into(),
797                                ..Default::default()
798                            }),
799                            ..Default::default()
800                        },
801                        ..Default::default()
802                    }
803                );
804            }
805
806            #[ignore = "Not managed yet by serde because of multilevel flatten: https://serde.rs/field-attrs.html#flatten"]
807            #[test]
808            fn duplicate_from() {
809                let data = r#"
810                from:
811                    path: ubuntu
812                from:
813                    path: alpine
814                "#;
815
816                let dofigen: serde_yaml::Result<DofigenPatch> = serde_yaml::from_str(data);
817
818                println!("{:?}", dofigen);
819
820                assert!(dofigen.is_err());
821            }
822
823            #[ignore = "Not managed yet by serde because of multilevel flatten: https://serde.rs/field-attrs.html#flatten"]
824            #[test]
825            fn duplicate_from_and_alias() {
826                let data = r#"
827                from:
828                  path: ubuntu
829                image:
830                  path: alpine
831                "#;
832
833                let dofigen: serde_yaml::Result<DofigenPatch> = serde_yaml::from_str(data);
834
835                println!("{:?}", dofigen);
836
837                assert!(dofigen.is_err());
838            }
839
840            #[test]
841            fn global_arg() {
842                let data = r#"
843                globalArg:
844                  IMAGE: ubuntu
845                fromContext: ${IMAGE}
846                "#;
847
848                let dofigen: DofigenPatch = serde_yaml::from_str(data).unwrap();
849                let dofigen: Dofigen = dofigen.into();
850
851                assert_eq_sorted!(
852                    dofigen,
853                    Dofigen {
854                        global_arg: HashMap::from([("IMAGE".into(), "ubuntu".into())]),
855                        stage: Stage {
856                            from: FromContext::FromContext(Some("${IMAGE}".into())).into(),
857                            ..Default::default()
858                        },
859                        ..Default::default()
860                    }
861                );
862            }
863        }
864
865        mod stage {
866            use super::*;
867
868            #[test]
869            fn empty() {
870                let data = r#""#;
871
872                let stage: StagePatch = serde_yaml::from_str(data).unwrap();
873                let stage: Stage = stage.into();
874
875                assert_eq_sorted!(stage, Stage::default());
876            }
877
878            #[test]
879            fn from() {
880                let data = r#"
881                fromImage:
882                  path: ubuntu
883                "#;
884
885                let stage: StagePatch = serde_yaml::from_str(data).unwrap();
886                let stage: Stage = stage.into();
887
888                assert_eq_sorted!(
889                    stage,
890                    Stage {
891                        from: FromContext::FromImage(ImageName {
892                            path: "ubuntu".into(),
893                            ..Default::default()
894                        })
895                        .into(),
896                        ..Default::default()
897                    }
898                );
899            }
900
901            #[ignore = "Not managed yet by serde because of multilevel flatten: https://serde.rs/field-attrs.html#flatten"]
902            #[test]
903            fn duplicate_from() {
904                let data = r#"
905                fromImage:
906                    path: ubuntu
907                fromImage:
908                    path: alpine
909                "#;
910
911                let stage: serde_yaml::Result<StagePatch> = serde_yaml::from_str(data);
912
913                assert!(stage.is_err());
914            }
915
916            #[ignore = "Not managed yet by serde because of multilevel flatten: https://serde.rs/field-attrs.html#flatten"]
917            #[test]
918            fn duplicate_from_and_alias() {
919                let data = r#"
920                fromImage:
921                  path: ubuntu
922                image:
923                  path: alpine
924                "#;
925
926                let stage: serde_yaml::Result<StagePatch> = serde_yaml::from_str(data);
927
928                assert!(stage.is_err());
929            }
930
931            #[test]
932            fn label() {
933                #[cfg(not(feature = "strict"))]
934                let data = r#"
935                label:
936                  io.dofigen:
937                    test: test
938                "#;
939                #[cfg(feature = "strict")]
940                let data = r#"
941                label:
942                  io.dofigen.test: test
943                "#;
944
945                let stage: StagePatch = serde_yaml::from_str(data).unwrap();
946                let stage: Stage = stage.into();
947
948                assert_eq_sorted!(
949                    stage,
950                    Stage {
951                        label: HashMap::from([("io.dofigen.test".into(), "test".into())]),
952                        ..Default::default()
953                    }
954                );
955            }
956        }
957
958        mod user {
959            use super::*;
960
961            #[test]
962            fn name_and_group() {
963                let json_data = r#"{
964    "user": "test",
965    "group": "test"
966}"#;
967
968                let user: UserPatch = serde_yaml::from_str(json_data).unwrap();
969                let user: User = user.into();
970
971                assert_eq_sorted!(
972                    user,
973                    User {
974                        user: "test".into(),
975                        group: Some("test".into())
976                    }
977                );
978            }
979        }
980
981        mod copy_resource {
982
983            use super::*;
984
985            #[test]
986            fn copy() {
987                let json_data = r#"{
988    "paths": ["file1.txt", "file2.txt"],
989    "target": "destination/",
990    "chown": {
991        "user": "root",
992        "group": "root"
993    },
994    "chmod": "755",
995    "link": true,
996    "fromImage": {"path": "my-image"}
997}"#;
998
999                let copy_resource: CopyResourcePatch = serde_yaml::from_str(json_data).unwrap();
1000                let copy_resource: CopyResource = copy_resource.into();
1001
1002                assert_eq_sorted!(
1003                    copy_resource,
1004                    CopyResource::Copy(Copy {
1005                        paths: vec!["file1.txt".into(), "file2.txt".into()].into(),
1006                        options: CopyOptions {
1007                            target: Some("destination/".into()),
1008                            chown: Some(User {
1009                                user: "root".into(),
1010                                group: Some("root".into())
1011                            }),
1012                            chmod: Some("755".into()),
1013                            link: Some(true),
1014                        },
1015                        from: FromContext::FromImage(ImageName {
1016                            path: "my-image".into(),
1017                            ..Default::default()
1018                        }),
1019                        exclude: vec![].into(),
1020                        parents: None,
1021                    })
1022                );
1023            }
1024
1025            #[test]
1026            fn copy_simple() {
1027                let json_data = r#"{
1028    "paths": ["file1.txt"]
1029}"#;
1030
1031                let copy_resource: CopyResourcePatch = serde_yaml::from_str(json_data).unwrap();
1032
1033                assert_eq_sorted!(
1034                    copy_resource,
1035                    CopyResourcePatch::Copy(CopyPatch {
1036                        paths: Some(vec!["file1.txt".into()].into_patch()),
1037                        options: Some(CopyOptionsPatch::default()),
1038                        ..Default::default()
1039                    })
1040                );
1041
1042                let copy_resource: CopyResource = copy_resource.into();
1043
1044                assert_eq_sorted!(
1045                    copy_resource,
1046                    CopyResource::Copy(Copy {
1047                        paths: vec!["file1.txt".into()].into(),
1048                        options: CopyOptions::default(),
1049                        ..Default::default()
1050                    })
1051                );
1052            }
1053
1054            #[cfg(feature = "permissive")]
1055            #[test]
1056            fn copy_chmod_int() {
1057                let json_data = r#"{
1058    "paths": ["file1.txt"],
1059    "chmod": 755
1060}"#;
1061
1062                let copy_resource: CopyPatch = serde_yaml::from_str(json_data).unwrap();
1063
1064                assert_eq_sorted!(
1065                    copy_resource,
1066                    CopyPatch {
1067                        paths: Some(vec!["file1.txt".into()].into_patch()),
1068                        options: Some(CopyOptionsPatch {
1069                            chmod: Some(Some("755".into())),
1070                            ..Default::default()
1071                        }),
1072                        ..Default::default()
1073                    }
1074                );
1075            }
1076
1077            #[cfg(feature = "permissive")]
1078            #[test]
1079            fn deserialize_copy_from_str() {
1080                let json_data = "file1.txt destination/";
1081
1082                let copy_resource: ParsableStruct<CopyResourcePatch> =
1083                    serde_yaml::from_str(json_data).unwrap();
1084                let copy_resource: CopyResource = copy_resource.into();
1085
1086                assert_eq_sorted!(
1087                    copy_resource,
1088                    CopyResource::Copy(Copy {
1089                        paths: vec!["file1.txt".into()].into(),
1090                        options: CopyOptions {
1091                            target: Some("destination/".into()),
1092                            ..Default::default()
1093                        },
1094                        ..Default::default()
1095                    })
1096                );
1097            }
1098
1099            #[test]
1100            fn copy_content() {
1101                let json_data = r#"{
1102    "content": "echo coucou",
1103    "substitute": false,
1104    "target": "test.sh",
1105    "chown": {
1106        "user": "1001",
1107        "group": "1001"
1108    },
1109    "chmod": "555",
1110    "link": true
1111}"#;
1112
1113                let copy_resource: CopyResourcePatch = serde_yaml::from_str(json_data).unwrap();
1114                let copy_resource: CopyResource = copy_resource.into();
1115
1116                assert_eq_sorted!(
1117                    copy_resource,
1118                    CopyResource::Content(CopyContent {
1119                        content: "echo coucou".into(),
1120                        substitute: Some(false),
1121                        options: CopyOptions {
1122                            target: Some("test.sh".into()),
1123                            chown: Some(User {
1124                                user: "1001".into(),
1125                                group: Some("1001".into())
1126                            }),
1127                            chmod: Some("555".into()),
1128                            link: Some(true),
1129                        }
1130                    })
1131                );
1132            }
1133
1134            #[test]
1135            fn add_git_repo() {
1136                let json_data = r#"{
1137            "repo": "https://github.com/example/repo.git",
1138            "target": "destination/",
1139            "chown": {
1140                "user": "root",
1141                "group": "root"
1142            },
1143            "chmod": "755",
1144            "link": true,
1145            "keepGitDir": true
1146        }"#;
1147
1148                let copy_resource: CopyResourcePatch = serde_yaml::from_str(json_data).unwrap();
1149                let copy_resource: CopyResource = copy_resource.into();
1150
1151                assert_eq_sorted!(
1152                    copy_resource,
1153                    CopyResource::AddGitRepo(AddGitRepo {
1154                        repo: "https://github.com/example/repo.git".into(),
1155                        options: CopyOptions {
1156                            target: Some("destination/".into()),
1157                            chown: Some(User {
1158                                user: "root".into(),
1159                                group: Some("root".into())
1160                            }),
1161                            chmod: Some("755".into()),
1162                            link: Some(true),
1163                        },
1164                        keep_git_dir: Some(true),
1165                        exclude: vec![].into(),
1166                    })
1167                );
1168            }
1169
1170            #[test]
1171            fn add() {
1172                let json_data = r#"{
1173            "files": ["file1.txt", "file2.txt"],
1174            "target": "destination/",
1175            "checksum": "sha256:abcdef123456",
1176            "chown": {
1177                "user": "root",
1178                "group": "root"
1179            },
1180            "chmod": "755",
1181            "link": true
1182        }"#;
1183
1184                let copy_resource: CopyResourcePatch = serde_yaml::from_str(json_data).unwrap();
1185                let copy_resource: CopyResource = copy_resource.into();
1186
1187                assert_eq_sorted!(
1188                    copy_resource,
1189                    CopyResource::Add(Add {
1190                        files: vec![
1191                            Resource::File("file1.txt".into()),
1192                            Resource::File("file2.txt".into())
1193                        ]
1194                        .into(),
1195                        options: CopyOptions {
1196                            target: Some("destination/".into()),
1197                            chown: Some(User {
1198                                user: "root".into(),
1199                                group: Some("root".into())
1200                            }),
1201                            chmod: Some("755".into()),
1202                            link: Some(true),
1203                        },
1204                        checksum: Some("sha256:abcdef123456".into()),
1205                    })
1206                );
1207            }
1208        }
1209
1210        mod builder {
1211            use super::*;
1212
1213            #[test]
1214            fn with_bind() {
1215                let json_data = r#"
1216fromImage:
1217  path: clux/muslrust:stable
1218workdir: /app
1219bind:
1220  - target: /app
1221run:
1222  - cargo build --release
1223  - mv target/x86_64-unknown-linux-musl/release/dofigen /app/
1224"#;
1225
1226                let builder: Stage = serde_yaml::from_str::<StagePatch>(json_data)
1227                    .unwrap()
1228                    .into();
1229
1230                assert_eq_sorted!(
1231                    builder,
1232                    Stage {
1233                        from: FromContext::FromImage(ImageName {
1234                            path: "clux/muslrust:stable".into(),
1235                            ..Default::default()
1236                        }),
1237                        workdir: Some("/app".into()),
1238                        run: Run {
1239                            bind: vec![Bind {
1240                                target: "/app".into(),
1241                                ..Default::default()
1242                            }],
1243                            run: vec![
1244                                "cargo build --release".into(),
1245                                "mv target/x86_64-unknown-linux-musl/release/dofigen /app/".into()
1246                            ],
1247                            ..Default::default()
1248                        },
1249                        ..Default::default()
1250                    }
1251                );
1252            }
1253        }
1254    }
1255}