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