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 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 },
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}