Skip to content

Commit 5501abc

Browse files
committed
Create ImageRef type to reference local images
Local images may be referenced by URI or by Id. This reference is used when reading service information from containers and mapping a container to the backend image URI Change-type: patch
1 parent b05cfb1 commit 5501abc

File tree

5 files changed

+125
-54
lines changed

5 files changed

+125
-54
lines changed

helios-oci/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ mod container;
1212
pub use container::{Container, LocalContainer};
1313

1414
mod models;
15-
pub use models::{ImageUri, InvalidImageUriError};
15+
pub use models::{ImageRef, ImageUri, InvalidImageUriError};
1616

1717
mod registry;
1818
pub use registry::{RegistryAuth, RegistryAuthClient, RegistryAuthError};

helios-oci/src/models.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,45 @@ impl<'de> Deserialize<'de> for ImageUri {
178178
}
179179
}
180180

181+
/// An image reference is either a image URI or a content addressable image ID
182+
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
183+
#[serde(untagged)]
184+
pub enum ImageRef {
185+
/// A URI reference
186+
Uri(ImageUri),
187+
188+
/// A content addressable image id
189+
Id(String),
190+
}
191+
192+
impl ImageRef {
193+
/// Convenience method to get the digest of an image ref
194+
///
195+
/// Returns None if the image is not a Uri or the Uri does not have a digest
196+
pub fn digest(&self) -> &Option<String> {
197+
if let Self::Uri(uri) = self {
198+
uri.digest()
199+
} else {
200+
&None
201+
}
202+
}
203+
}
204+
205+
impl<'de> Deserialize<'de> for ImageRef {
206+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
207+
where
208+
D: serde::Deserializer<'de>,
209+
{
210+
let s: String = String::deserialize(deserializer)?;
211+
if s.starts_with("sha256:") {
212+
return Ok(ImageRef::Id(s));
213+
}
214+
215+
let uri: ImageUri = s.parse().map_err(serde::de::Error::custom)?;
216+
Ok(ImageRef::Uri(uri))
217+
}
218+
}
219+
181220
#[cfg(test)]
182221
mod tests {
183222
use super::*;

helios-state/src/models/service.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::str::FromStr;
55

66
use serde::{Deserialize, Serialize};
77

8-
use crate::oci::ImageUri;
8+
use crate::oci::ImageRef;
99
use crate::util::types::Uuid;
1010

1111
// We don't want to fail if the service is supervised but it doesn't have an app-uuid,
@@ -92,7 +92,7 @@ impl FromStr for ServiceContainerName {
9292
#[derive(Serialize, Deserialize, Debug, Clone)]
9393
pub struct Service {
9494
pub id: u32,
95-
pub image: ImageUri,
95+
pub image: ImageRef,
9696
}
9797

9898
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
@@ -101,8 +101,8 @@ pub struct TargetService {
101101
#[serde(default)]
102102
pub id: u32,
103103

104-
/// Service image URI
105-
pub image: ImageUri,
104+
/// Service image
105+
pub image: ImageRef,
106106
}
107107

108108
impl From<Service> for TargetService {

helios-state/src/read.rs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use thiserror::Error;
44
use tracing::instrument;
55

66
use crate::oci::{
7-
Client as Docker, Error as DockerError, ImageUri, InvalidImageUriError, WithContext,
7+
Client as Docker, Error as DockerError, ImageRef, ImageUri, InvalidImageUriError, WithContext,
88
};
99
use crate::util::state;
1010
use crate::util::types::{OperatingSystem, Uuid};
@@ -99,18 +99,19 @@ pub async fn read(
9999

100100
// Insert the service and link it to the image if there is state
101101
// metadata about the image
102-
// FIXME: the in-memory service should exist if the container exists so
103-
// we need to use an image reference if the state variable does not exist
104-
if let Some(image) = svc_img {
105-
let svc_id: u32 = labels
106-
.get("io.balena.service-id")
107-
.and_then(|id| id.parse().ok())
108-
.unwrap_or(0);
109-
110-
release
111-
.services
112-
.insert(service_name, Service { id: svc_id, image });
113-
}
102+
let image = if let Some(svc_img) = svc_img {
103+
ImageRef::Uri(svc_img)
104+
} else {
105+
ImageRef::Id(local_container.image_id)
106+
};
107+
108+
let svc_id: u32 = labels
109+
.get("io.balena.service-id")
110+
.and_then(|id| id.parse().ok())
111+
.unwrap_or(0);
112+
release
113+
.services
114+
.insert(service_name, Service { id: svc_id, image });
114115
}
115116

116117
Ok(device)

helios-state/src/worker.rs

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use mahler::{
1111
use tokio::sync::RwLock;
1212

1313
use super::util::state;
14-
use crate::oci::{Client as Docker, Error as DockerError, ImageUri};
14+
use crate::oci::{Client as Docker, Error as DockerError, ImageRef, ImageUri};
1515
use crate::oci::{Credentials, RegistryAuth, RegistryAuthClient, RegistryAuthError};
1616
use crate::util::types::Uuid;
1717

@@ -209,23 +209,34 @@ fn request_registry_token_for_new_images(
209209
release
210210
.services
211211
.iter()
212-
.filter(|(service_name, svc)| {
213-
// if the service image has not been downloaded
214-
!device.images.contains_key(&svc.image) &&
215-
// and there is no service from a different release
212+
.filter_map(|(svc_name, svc)| {
213+
if let ImageRef::Uri(img_uri) = &svc.image {
214+
Some((svc_name, img_uri))
215+
} else {
216+
None
217+
}
218+
})
219+
.filter(|(svc_name, svc_img)| {
220+
// ignore the image if it already exists
221+
if device.images.contains_key(svc_img) {
222+
return false;
223+
}
224+
216225
find_installed_service(
217226
&device,
218227
app_uuid.clone(),
219228
commit.clone(),
220-
(*service_name).clone(),
229+
(*svc_name).clone(),
221230
)
222-
// or there is a service and the digest doesn't match the target digest
231+
// if the image is for a new service or the existing service has a
232+
// different digest (which means a new download), then add it to the
233+
// authorization list
223234
.is_none_or(|s| {
224-
s.image.digest().is_none() || s.image.digest() != svc.image.digest()
235+
s.image.digest().is_none() || s.image.digest() != svc_img.digest()
225236
})
226237
})
227238
// then select the image
228-
.map(|(_, svc)| &svc.image)
239+
.map(|(_, svc_img)| svc_img)
229240
})
230241
})
231242
.collect();
@@ -265,29 +276,36 @@ fn fetch_release_images(
265276
let images_to_install: Vec<ImageUri> = tgt_release
266277
.services
267278
.into_iter()
268-
.filter(|(service_name, svc)| {
269-
// if the service image has not been downloaded
270-
!device.images.contains_key(&svc.image) &&
271-
// and there is no service from a different release
279+
.filter_map(|(svc_name, svc)| {
280+
if let ImageRef::Uri(img_uri) = svc.image {
281+
Some((svc_name, img_uri))
282+
} else {
283+
None
284+
}
285+
})
286+
.filter(|(svc_name, svc_img)| {
287+
// ignore the image if it already exists
288+
if device.images.contains_key(svc_img) {
289+
return false;
290+
}
291+
272292
find_installed_service(
273-
&device,
274-
app_uuid.clone(),
275-
commit.clone(),
276-
(*service_name).clone(),
277-
)
278-
// or if there is a service, then only chose it if it has the same digest
279-
// as the target service
280-
.is_none_or(|s| {
281-
s.image.digest().is_some() && svc.image.digest() == s.image.digest()
282-
})
293+
&device,
294+
app_uuid.clone(),
295+
commit.clone(),
296+
(*svc_name).clone(),
297+
)
298+
// select the image if it is for a new service or the existing service image has the
299+
// same digest (which means we are just re-tagging)
300+
.is_none_or(|s| s.image.digest().is_some() && svc_img.digest() == s.image.digest())
283301
})
284302
// remove duplicate digests
285-
.fold(Vec::<ImageUri>::new(), |mut acc, (_, svc)| {
303+
.fold(Vec::<ImageUri>::new(), |mut acc, (_, svc_img)| {
286304
if acc
287305
.iter()
288-
.all(|img| img.digest().is_none() || img.digest() != svc.image.digest())
306+
.all(|img| img.digest().is_none() || img.digest() != svc_img.digest())
289307
{
290-
acc.push(svc.image);
308+
acc.push(svc_img);
291309
}
292310
acc
293311
});
@@ -415,18 +433,24 @@ fn fetch_service_image(
415433
Target(tgt): Target<TargetService>,
416434
Args((app_uuid, commit, service_name)): Args<(Uuid, Uuid, String)>,
417435
) -> Option<Task> {
418-
// tell the planner to skip this task if the image already exists
419-
if device.images.contains_key(&tgt.image) {
436+
let tgt_img = if let ImageRef::Uri(img) = tgt.image {
437+
// Skip this task if the image already exists
438+
if device.images.contains_key(&img) {
439+
return None;
440+
}
441+
img
442+
} else {
443+
// also skip if the target image is not a Uri
420444
return None;
421-
}
445+
};
422446

423447
// If there is a service with the same name in any other releases of the service
424448
// and the service image has as different digest then we skip the fetch as
425449
// that pull will need deltas and needs to be handled by another task
426450
if find_installed_service(&device, app_uuid, commit, service_name)
427-
.is_none_or(|svc| svc.image.digest().is_some() && tgt.image.digest() == svc.image.digest())
451+
.is_none_or(|svc| svc.image.digest().is_some() && tgt_img.digest() == svc.image.digest())
428452
{
429-
return Some(create_image.with_arg("image_name", tgt.image));
453+
return Some(create_image.with_arg("image_name", tgt_img));
430454
}
431455

432456
None
@@ -443,7 +467,11 @@ fn install_service(
443467
Path(job_path): Path,
444468
) -> IO<Option<Service>, state::ReadWriteError> {
445469
// Skip the task if the image for the service doesn't exist yet
446-
if device.images.keys().all(|img| img != &tgt.image) {
470+
if let ImageRef::Uri(tgt_img) = &tgt.image {
471+
if !device.images.contains_key(tgt_img) {
472+
return svc.into();
473+
}
474+
} else {
447475
return svc.into();
448476
}
449477

@@ -472,10 +500,13 @@ fn remove_image(
472500
// only remove the image if it not being used by any service
473501
if device.apps.values().any(|app| {
474502
app.releases.values().any(|release| {
475-
release
476-
.services
477-
.values()
478-
.any(|s| s.image == image_name.clone())
503+
release.services.values().any(|s| {
504+
if let ImageRef::Uri(s_img) = &s.image {
505+
s_img == &image_name
506+
} else {
507+
false
508+
}
509+
})
479510
})
480511
}) {
481512
return img_ptr.into();

0 commit comments

Comments
 (0)