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 pub effective: String,
40
41 pub images: HashMap<String, HashMap<String, HashMap<String, HashMap<String, DockerTag>>>>,
47
48 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 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}