use super::deploy::DeployLog;
use super::{AppSpec, Apps, Source};
use crate::error::{Error, Result};
use crate::image_check::{self, Probe};
impl Apps {
pub(super) fn resolve(&self, i: &str, log: &mut DeployLog) -> Result<(String, Option<String>)> {
let Some((_, registry)) = image_check::remote(i) else {
log.line("not a registry image; deploying by name");
return Ok((i.to_string(), None));
};
match (self.inner.probe)(i, image_check::DEPLOY_TIMEOUT) {
Probe::Found(Some(d)) => {
let pinned = super::pin(i, &d).unwrap_or_else(|| i.to_string());
log.line(&format!("resolved to {pinned}"));
Ok((pinned, Some(d)))
}
Probe::Found(None) => {
log.line("no digest found; deploying by tag");
Ok((i.to_string(), None))
}
Probe::NotFound(why) => {
let better = image_check::suggestion(i).filter(|s| {
matches!(
(self.inner.probe)(s, image_check::CHECK_TIMEOUT),
Probe::Found(_)
)
});
Err(Error::invalid(image_check::not_found(
i, ®istry, &why, better,
)))
}
Probe::Denied(why) | Probe::Unknown(why) => {
log.line(&format!(
"could not check the image on {registry} ({why}); deploying by tag"
));
Ok((i.to_string(), None))
}
}
}
pub fn check_image(&self, before: Option<&AppSpec>, spec: &AppSpec) -> Result<Option<String>> {
let Some(image) = image_of(spec) else {
return Ok(None);
};
if before.and_then(image_of).as_deref() == Some(image.as_str()) {
return Ok(None);
}
let probe = |i: &str| (self.inner.probe)(i, image_check::CHECK_TIMEOUT);
image_check::check(&image, &probe)
}
}
pub(super) fn image_of(spec: &AppSpec) -> Option<String> {
match &spec.source {
Source::Image(i) => Some(i.clone()),
Source::Database(db) => Some(db.image()),
Source::Git(_) => None,
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use std::time::Duration;
use serde_json::json;
use super::super::deploy::tests::apps;
use super::super::deploy::{Status, Trigger};
use crate::org::OrgId;
fn spec(v: serde_json::Value) -> super::AppSpec {
serde_json::from_value(v).unwrap()
}
#[test]
fn an_image_its_registry_lacks_is_refused_and_never_deployed() {
let dir = tempfile::tempdir().unwrap();
let gate = Arc::new((Mutex::new(true), std::sync::Condvar::new()));
let ap = apps(dir.path(), gate);
let org = OrgId::new("acme").unwrap();
ap.project_create(&org, "shop", "", &[]).unwrap();
let typo = spec(json!({
"name": "web", "project": "shop", "source": {"image": "docker:traefik:whoami"},
}));
let e = ap.check_image(None, &typo).unwrap_err().to_string();
assert!(e.contains("did you mean docker:traefik/whoami?"), "{e}");
let fine = spec(json!({
"name": "web", "project": "shop", "source": {"image": "docker:traefik/whoami"},
}));
assert_eq!(ap.check_image(None, &fine).unwrap(), None);
assert_eq!(ap.check_image(Some(&typo), &typo).unwrap(), None);
ap.create(&org, typo).unwrap();
ap.deploy(&org, "web", Trigger::Api, "t", None).unwrap();
let d = ap.wait(&org, "web", 1, Duration::from_secs(30)).unwrap();
assert_eq!(d.status, Status::Failed, "{d:?}");
let err = d.error.clone().unwrap_or_default();
assert!(
err.contains("image docker:traefik:whoami not found on Docker Hub (manifest unknown)"),
"{err}"
);
assert!(d.rendered.is_none(), "the stack was never deployed: {d:?}");
}
}