1use super::deploy::DeployLog;
6use super::{AppSpec, Apps, Source};
7use crate::error::{Error, Result};
8use crate::image_check::{self, Probe};
9
10impl Apps {
11 pub(super) fn resolve(&self, i: &str, log: &mut DeployLog) -> Result<(String, Option<String>)> {
15 let Some((_, registry)) = image_check::remote(i) else {
16 log.line("not a registry image; deploying by name");
17 return Ok((i.to_string(), None));
18 };
19 match (self.inner.probe)(i, image_check::DEPLOY_TIMEOUT) {
20 Probe::Found(Some(d)) => {
21 let pinned = super::pin(i, &d).unwrap_or_else(|| i.to_string());
22 log.line(&format!("resolved to {pinned}"));
23 Ok((pinned, Some(d)))
24 }
25 Probe::Found(None) => {
26 log.line("no digest found; deploying by tag");
27 Ok((i.to_string(), None))
28 }
29 Probe::NotFound(why) => {
30 let better = image_check::suggestion(i).filter(|s| {
31 matches!(
32 (self.inner.probe)(s, image_check::CHECK_TIMEOUT),
33 Probe::Found(_)
34 )
35 });
36 Err(Error::invalid(image_check::not_found(
37 i, ®istry, &why, better,
38 )))
39 }
40 Probe::Denied(why) | Probe::Unknown(why) => {
41 log.line(&format!(
42 "could not check the image on {registry} ({why}); deploying by tag"
43 ));
44 Ok((i.to_string(), None))
45 }
46 }
47 }
48
49 pub fn check_image(&self, before: Option<&AppSpec>, spec: &AppSpec) -> Result<Option<String>> {
54 let Some(image) = image_of(spec) else {
55 return Ok(None);
56 };
57 if before.and_then(image_of).as_deref() == Some(image.as_str()) {
58 return Ok(None);
59 }
60 let probe = |i: &str| (self.inner.probe)(i, image_check::CHECK_TIMEOUT);
61 image_check::check(&image, &probe)
62 }
63}
64
65pub(super) fn image_of(spec: &AppSpec) -> Option<String> {
67 match &spec.source {
68 Source::Image(i) => Some(i.clone()),
69 Source::Database(db) => Some(db.image()),
70 Source::Git(_) => None,
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use std::sync::{Arc, Mutex};
77 use std::time::Duration;
78
79 use serde_json::json;
80
81 use super::super::deploy::tests::apps;
82 use super::super::deploy::{Status, Trigger};
83 use crate::org::OrgId;
84
85 fn spec(v: serde_json::Value) -> super::AppSpec {
86 serde_json::from_value(v).unwrap()
87 }
88
89 #[test]
90 fn an_image_its_registry_lacks_is_refused_and_never_deployed() {
91 let dir = tempfile::tempdir().unwrap();
92 let gate = Arc::new((Mutex::new(true), std::sync::Condvar::new()));
93 let ap = apps(dir.path(), gate);
94 let org = OrgId::new("acme").unwrap();
95 ap.project_create(&org, "shop", "", &[]).unwrap();
96 let typo = spec(json!({
97 "name": "web", "project": "shop", "source": {"image": "docker:traefik:whoami"},
98 }));
99 let e = ap.check_image(None, &typo).unwrap_err().to_string();
100 assert!(e.contains("did you mean docker:traefik/whoami?"), "{e}");
101 let fine = spec(json!({
102 "name": "web", "project": "shop", "source": {"image": "docker:traefik/whoami"},
103 }));
104 assert_eq!(ap.check_image(None, &fine).unwrap(), None);
105 assert_eq!(ap.check_image(Some(&typo), &typo).unwrap(), None);
107
108 ap.create(&org, typo).unwrap();
111 ap.deploy(&org, "web", Trigger::Api, "t", None).unwrap();
112 let d = ap.wait(&org, "web", 1, Duration::from_secs(30)).unwrap();
113 assert_eq!(d.status, Status::Failed, "{d:?}");
114 let err = d.error.clone().unwrap_or_default();
115 assert!(
116 err.contains("image docker:traefik:whoami not found on Docker Hub (manifest unknown)"),
117 "{err}"
118 );
119 assert!(d.rendered.is_none(), "the stack was never deployed: {d:?}");
120 }
121}