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