dofigen_lib/
lock.rs

1use crate::{DofigenContext, Error, Result, dofigen_struct::*};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5pub(crate) const DOCKER_HUB_HOST: &str = "registry.hub.docker.com";
6pub(crate) const DOCKER_IO_HOST: &str = "docker.io";
7pub(crate) const DEFAULT_NAMESPACE: &str = "library";
8pub(crate) const DEFAULT_TAG: &str = "latest";
9pub(crate) const DEFAULT_PORT: u16 = 443;
10
11#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq)]
12pub struct DockerTag {
13    pub digest: String,
14}
15
16#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq)]
17pub struct ResourceVersion {
18    pub hash: String,
19    pub content: String,
20}
21
22impl ImageName {
23    pub fn fill(&self) -> Self {
24        Self {
25            host: self.host.clone().or(Some(DOCKER_IO_HOST.to_string())),
26            port: self.port.clone().or(Some(DEFAULT_PORT)),
27            version: self
28                .version
29                .clone()
30                .or(Some(ImageVersion::Tag(DEFAULT_TAG.to_string()))),
31            ..self.clone()
32        }
33    }
34}
35
36#[derive(Debug, Deserialize, Serialize)]
37pub struct LockFile {
38    /// The effective Dofigen configuration
39    pub effective: String,
40
41    /// The digests of the images used in the Dofigen file
42    /// The first level key is the host
43    /// The second level key is the namespace
44    /// The third level key is the repository
45    /// The fourth level key is the tag
46    pub images: HashMap<String, HashMap<String, HashMap<String, HashMap<String, DockerTag>>>>,
47
48    /// The files used in the Dofigen file for 'extend' fields
49    pub resources: HashMap<String, ResourceVersion>,
50}
51
52impl LockFile {
53    fn images(&self) -> HashMap<ImageName, DockerTag> {
54        let mut images = HashMap::new();
55        for (host, namespaces) in self.images.clone() {
56            let (host, port) = if host.contains(":") {
57                let mut parts = host.split(":");
58                (
59                    parts.next().unwrap().to_string(),
60                    Some(parts.next().unwrap().parse().unwrap()),
61                )
62            } else {
63                (host, Some(DEFAULT_PORT))
64            };
65            // In order to do not create breaking changes, we replace the Docker hub host with docker.io
66            let host = if host == DOCKER_HUB_HOST {
67                DOCKER_IO_HOST.to_string()
68            } else {
69                host
70            };
71            for (namespace, repositories) in namespaces {
72                for (repository, tags) in repositories {
73                    let path = if namespace == DEFAULT_NAMESPACE {
74                        repository.clone()
75                    } else {
76                        format!("{}/{}", namespace, repository)
77                    };
78                    for (tag, digest) in tags {
79                        images.insert(
80                            ImageName {
81                                host: Some(host.clone()),
82                                port,
83                                path: path.clone(),
84                                version: Some(ImageVersion::Tag(tag)),
85                                platform: None,
86                            },
87                            digest,
88                        );
89                    }
90                }
91            }
92        }
93        images
94    }
95
96    fn resources(&self) -> HashMap<Resource, ResourceVersion> {
97        self.resources
98            .clone()
99            .into_iter()
100            .map(|(path, content)| (path.parse().unwrap(), content))
101            .collect()
102    }
103
104    pub fn to_context(&self) -> DofigenContext {
105        DofigenContext::from(self.resources(), self.images())
106    }
107
108    pub fn from_context(effective: &Dofigen, context: &DofigenContext) -> Result<LockFile> {
109        let mut images = HashMap::new();
110        for (image, docker_tag) in context.used_image_tags() {
111            let host = image
112                .host
113                .ok_or(Error::Custom("Image host is not set".to_string()))?;
114            let port = image
115                .port
116                .ok_or(Error::Custom("Image port is not set".to_string()))?;
117            let host = if port == DEFAULT_PORT {
118                host
119            } else {
120                format!("{}:{}", host, port)
121            };
122            let (namespace, repository) = if image.path.contains("/") {
123                let mut parts = image.path.split("/");
124                let namespace = parts.next().unwrap();
125                let repository = parts.collect::<Vec<&str>>().join("/");
126                (namespace, repository)
127            } else {
128                (DEFAULT_NAMESPACE, image.path)
129            };
130            let tag = match image.version.unwrap() {
131                ImageVersion::Tag(tag) => Ok(tag),
132                _ => Err(Error::Custom("Image version is not a tag".to_string())),
133            }?;
134            images
135                .entry(host)
136                .or_insert_with(HashMap::new)
137                .entry(namespace.to_string())
138                .or_insert_with(HashMap::new)
139                .entry(repository.to_string())
140                .or_insert_with(HashMap::new)
141                .insert(tag, docker_tag);
142        }
143
144        let files = context
145            .used_resource_contents()
146            .iter()
147            .map(|(resource, content)| (resource.to_string(), content.clone()))
148            .collect();
149
150        Ok(LockFile {
151            effective: serde_yaml::to_string(effective).map_err(Error::from)?,
152            images,
153            resources: files,
154        })
155    }
156}
157
158pub trait Lock: Sized {
159    fn lock(&self, context: &mut DofigenContext) -> Result<Self>;
160}
161
162impl<T> Lock for Option<T>
163where
164    T: Lock,
165{
166    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
167        match self {
168            Some(t) => Ok(Some(t.lock(context)?)),
169            None => Ok(None),
170        }
171    }
172}
173
174impl<T> Lock for Vec<T>
175where
176    T: Lock,
177{
178    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
179        self.iter().map(|t| t.lock(context)).collect()
180    }
181}
182
183impl<K, V> Lock for HashMap<K, V>
184where
185    K: Eq + std::hash::Hash + Clone,
186    V: Lock,
187{
188    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
189        self.iter()
190            .map(|(key, value)| {
191                value
192                    .lock(context)
193                    .map(|locked_value| (key.clone(), locked_value))
194            })
195            .collect()
196    }
197}
198
199impl Lock for Dofigen {
200    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
201        let mut stage = self.stage.lock(context)?;
202        if !context.no_default_labels {
203            stage.label.insert(
204                "io.dofigen.version".into(),
205                env!("CARGO_PKG_VERSION").into(),
206            );
207        }
208        Ok(Self {
209            builders: self.builders.lock(context)?,
210            stage,
211            ..self.clone()
212        })
213    }
214}
215
216impl Lock for Stage {
217    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
218        let mut label = self.label.clone();
219        let from = match &self.from {
220            FromContext::FromImage(image_name) => {
221                let image_name_filled = image_name.fill();
222                let version = image_name_filled.version.clone().ok_or(Error::Custom(
223                    "Version must be set in filled image name".into(),
224                ))?;
225                FromContext::FromImage(match version {
226                    ImageVersion::Tag(_) => {
227                        if !context.no_default_labels {
228                            label.insert(
229                                "org.opencontainers.image.base.name".into(),
230                                image_name_filled.to_string(),
231                            );
232                        }
233                        let locked = image_name.lock(context)?;
234                        if !context.no_default_labels {
235                            match &locked.version {
236                                Some(ImageVersion::Digest(digest)) => {
237                                    label.insert(
238                                        "org.opencontainers.image.base.digest".into(),
239                                        digest.clone(),
240                                    );
241                                }
242                                _ => unreachable!("Version must be a digest in locked image name"),
243                            }
244                        }
245                        locked
246                    }
247                    ImageVersion::Digest(digest) => {
248                        if !context.no_default_labels {
249                            label.insert(
250                                "org.opencontainers.image.base.digest".into(),
251                                digest.clone(),
252                            );
253                        }
254                        image_name_filled
255                    }
256                })
257            }
258            from => from.clone(),
259        };
260        Ok(Self {
261            from,
262            label,
263            copy: self.copy.lock(context)?,
264            run: self.run.lock(context)?,
265            root: self
266                .root
267                .as_ref()
268                .map(|root| root.lock(context))
269                .transpose()?,
270            ..self.clone()
271        })
272    }
273}
274
275impl Lock for FromContext {
276    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
277        match self {
278            Self::FromImage(image_name) => Ok(Self::FromImage(image_name.lock(context)?)),
279            other => Ok(other.clone()),
280        }
281    }
282}
283
284impl Lock for ImageName {
285    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
286        match &self.version {
287            Some(ImageVersion::Digest(_)) => Ok(self.clone()),
288            _ => Ok(Self {
289                version: Some(ImageVersion::Digest(
290                    context.get_image_tag(self)?.digest.clone(),
291                )),
292                ..self.clone()
293            }),
294        }
295    }
296}
297
298impl Lock for CopyResource {
299    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
300        match self {
301            Self::Copy(resource) => Ok(Self::Copy(resource.lock(context)?)),
302            other => Ok(other.clone()),
303        }
304    }
305}
306
307impl Lock for Copy {
308    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
309        Ok(Self {
310            from: self.from.lock(context)?,
311            ..self.clone()
312        })
313    }
314}
315
316impl Lock for Run {
317    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
318        Ok(Self {
319            bind: self.bind.lock(context)?,
320            cache: self.cache.lock(context)?,
321            ..self.clone()
322        })
323    }
324}
325
326impl Lock for Bind {
327    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
328        Ok(Self {
329            from: self.from.lock(context)?,
330            ..self.clone()
331        })
332    }
333}
334
335impl Lock for Cache {
336    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
337        Ok(Self {
338            from: self.from.lock(context)?,
339            ..self.clone()
340        })
341    }
342}
343
344impl Ord for DockerTag {
345    fn cmp(&self, _other: &Self) -> std::cmp::Ordering {
346        panic!("DockerTag cannot be ordered")
347    }
348}
349
350impl Ord for ResourceVersion {
351    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
352        self.hash.cmp(&other.hash)
353    }
354}