dofigen_lib/
dofigen_struct.rs

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