dofigen_lib/
lock.rs

1use crate::{dofigen_struct::*, DofigenContext, Error, Result};
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                            },
86                            digest,
87                        );
88                    }
89                }
90            }
91        }
92        images
93    }
94
95    fn resources(&self) -> HashMap<Resource, ResourceVersion> {
96        self.resources
97            .clone()
98            .into_iter()
99            .map(|(path, content)| (path.parse().unwrap(), content))
100            .collect()
101    }
102
103    pub fn to_context(&self) -> DofigenContext {
104        DofigenContext::from(self.resources(), self.images())
105    }
106
107    pub fn from_context(effective: &Dofigen, context: &DofigenContext) -> Result<LockFile> {
108        let mut images = HashMap::new();
109        for (image, docker_tag) in context.used_image_tags() {
110            let host = image
111                .host
112                .ok_or(Error::Custom("Image host is not set".to_string()))?;
113            let port = image
114                .port
115                .ok_or(Error::Custom("Image port is not set".to_string()))?;
116            let host = if port == DEFAULT_PORT {
117                host
118            } else {
119                format!("{}:{}", host, port)
120            };
121            let (namespace, repository) = if image.path.contains("/") {
122                let mut parts = image.path.split("/");
123                let namespace = parts.next().unwrap();
124                let repository = parts.collect::<Vec<&str>>().join("/");
125                (namespace, repository)
126            } else {
127                (DEFAULT_NAMESPACE, image.path)
128            };
129            let tag = match image.version.unwrap() {
130                ImageVersion::Tag(tag) => Ok(tag),
131                _ => Err(Error::Custom("Image version is not a tag".to_string())),
132            }?;
133            images
134                .entry(host)
135                .or_insert_with(HashMap::new)
136                .entry(namespace.to_string())
137                .or_insert_with(HashMap::new)
138                .entry(repository.to_string())
139                .or_insert_with(HashMap::new)
140                .insert(tag, docker_tag);
141        }
142
143        let files = context
144            .used_resource_contents()
145            .iter()
146            .map(|(resource, content)| (resource.to_string(), content.clone()))
147            .collect();
148
149        Ok(LockFile {
150            effective: serde_yaml::to_string(effective).map_err(Error::from)?,
151            images,
152            resources: files,
153        })
154    }
155}
156
157pub trait Lock: Sized {
158    fn lock(&self, context: &mut DofigenContext) -> Result<Self>;
159}
160
161impl<T> Lock for Option<T>
162where
163    T: Lock,
164{
165    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
166        match self {
167            Some(t) => Ok(Some(t.lock(context)?)),
168            None => Ok(None),
169        }
170    }
171}
172
173impl<T> Lock for Vec<T>
174where
175    T: Lock,
176{
177    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
178        self.iter().map(|t| t.lock(context)).collect()
179    }
180}
181
182impl<K, V> Lock for HashMap<K, V>
183where
184    K: Eq + std::hash::Hash + Clone,
185    V: Lock,
186{
187    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
188        self.iter()
189            .map(|(key, value)| {
190                value
191                    .lock(context)
192                    .map(|locked_value| (key.clone(), locked_value))
193            })
194            .collect()
195    }
196}
197
198impl Lock for Dofigen {
199    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
200        let mut stage = self.stage.lock(context)?;
201        if !context.no_default_labels {
202            stage.label.insert(
203                "io.dofigen.version".into(),
204                env!("CARGO_PKG_VERSION").into(),
205            );
206        }
207        Ok(Self {
208            builders: self.builders.lock(context)?,
209            stage,
210            ..self.clone()
211        })
212    }
213}
214
215impl Lock for Stage {
216    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
217        let mut label = self.label.clone();
218        let from = match &self.from {
219            FromContext::FromImage(image_name) => {
220                let image_name_filled = image_name.fill();
221                let version = image_name_filled.version.clone().ok_or(Error::Custom(
222                    "Version must be set in filled image name".into(),
223                ))?;
224                FromContext::FromImage(match version {
225                    ImageVersion::Tag(_) => {
226                        if !context.no_default_labels {
227                            label.insert(
228                                "org.opencontainers.image.base.name".into(),
229                                image_name_filled.to_string(),
230                            );
231                        }
232                        let locked = image_name.lock(context)?;
233                        if !context.no_default_labels {
234                            match &locked.version {
235                                Some(ImageVersion::Digest(digest)) => {
236                                    label.insert(
237                                        "org.opencontainers.image.base.digest".into(),
238                                        digest.clone(),
239                                    );
240                                }
241                                _ => unreachable!("Version must be a digest in locked image name"),
242                            }
243                        }
244                        locked
245                    }
246                    ImageVersion::Digest(digest) => {
247                        if !context.no_default_labels {
248                            label.insert(
249                                "org.opencontainers.image.base.digest".into(),
250                                digest.clone(),
251                            );
252                        }
253                        image_name_filled
254                    }
255                })
256            }
257            from => from.clone(),
258        };
259        Ok(Self {
260            from,
261            label,
262            copy: self.copy.lock(context)?,
263            run: self.run.lock(context)?,
264            root: self
265                .root
266                .as_ref()
267                .map(|root| root.lock(context))
268                .transpose()?,
269            ..self.clone()
270        })
271    }
272}
273
274impl Lock for FromContext {
275    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
276        match self {
277            Self::FromImage(image_name) => Ok(Self::FromImage(image_name.lock(context)?)),
278            other => Ok(other.clone()),
279        }
280    }
281}
282
283impl Lock for ImageName {
284    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
285        match &self.version {
286            Some(ImageVersion::Digest(_)) => Ok(self.clone()),
287            _ => Ok(Self {
288                version: Some(ImageVersion::Digest(
289                    context.get_image_tag(self)?.digest.clone(),
290                )),
291                ..self.clone()
292            }),
293        }
294    }
295}
296
297impl Lock for CopyResource {
298    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
299        match self {
300            Self::Copy(resource) => Ok(Self::Copy(resource.lock(context)?)),
301            other => Ok(other.clone()),
302        }
303    }
304}
305
306impl Lock for Copy {
307    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
308        Ok(Self {
309            from: self.from.lock(context)?,
310            ..self.clone()
311        })
312    }
313}
314
315impl Lock for Run {
316    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
317        Ok(Self {
318            bind: self.bind.lock(context)?,
319            cache: self.cache.lock(context)?,
320            ..self.clone()
321        })
322    }
323}
324
325impl Lock for Bind {
326    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
327        Ok(Self {
328            from: self.from.lock(context)?,
329            ..self.clone()
330        })
331    }
332}
333
334impl Lock for Cache {
335    fn lock(&self, context: &mut DofigenContext) -> Result<Self> {
336        Ok(Self {
337            from: self.from.lock(context)?,
338            ..self.clone()
339        })
340    }
341}
342
343impl Ord for DockerTag {
344    fn cmp(&self, _other: &Self) -> std::cmp::Ordering {
345        panic!("DockerTag cannot be ordered")
346    }
347}
348
349impl Ord for ResourceVersion {
350    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
351        self.hash.cmp(&other.hash)
352    }
353}