use super::storage::PushedTag;
use crate::bootstrap::state::AppState;
use shared::{Event, Service, ServiceSource};
use tokio::sync::mpsc::UnboundedReceiver;
use tracing::{info, warn};
fn repo_and_tag(image: &str, is_embedded: bool) -> Option<(String, String)> {
if !is_embedded {
return None;
}
let (_host, rest) = image.split_once('/')?;
if rest.contains('@') {
return None;
}
match rest.rsplit_once(':') {
Some((repo, tag)) if !tag.contains('/') && !repo.is_empty() => {
Some((repo.to_string(), tag.to_string()))
}
_ => Some((rest.to_string(), "latest".to_string())),
}
}
pub fn services_to_redeploy<'a>(
services: &'a [Service],
push: &PushedTag,
port: u16,
domain: Option<&str>,
) -> Vec<&'a Service> {
services
.iter()
.filter(|s| s.spec.registry_auto_deploy)
.filter(|s| match &s.spec.source {
ServiceSource::Registry { image } => {
let embedded = crate::features::deploy::executor::is_embedded_registry_image(
image, port, domain,
);
repo_and_tag(image, embedded).is_some_and(|(r, t)| r == push.repo && t == push.tag)
}
_ => false,
})
.collect()
}
pub async fn run(state: AppState, mut rx: UnboundedReceiver<PushedTag>) {
while let Some(push) = rx.recv().await {
state.bus.publish(Event::RegistryPush {
repo: push.repo.clone(),
tag: push.tag.clone(),
digest: push.digest.clone(),
});
let services = match crate::store::services::list_all(&state.db).await {
Ok(s) => s,
Err(e) => {
warn!(error = %e, "push-to-deploy: não consegui listar os serviços");
continue;
}
};
let port = crate::platform::config::RustployConfig::global()
.registry
.port;
let mut domain = crate::platform::config::RustployConfig::global()
.registry
.domain
.clone();
if let Ok(Some(d)) = crate::store::daemon_settings::get(
&state.db,
crate::store::daemon_settings::KEY_REGISTRY_DOMAIN,
)
.await
&& !d.trim().is_empty()
{
domain = Some(d);
}
for svc in services_to_redeploy(&services, &push, port, domain.as_deref()) {
info!(
service = %svc.spec.name, repo = %push.repo, tag = %push.tag,
"push-to-deploy: imagem nova no registry, disparando deploy"
);
let (ctx, id) = (state.deploy_ctx(), svc.id.clone());
tokio::spawn(async move {
crate::features::deploy::start::handle(ctx, id).await;
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn push(repo: &str, tag: &str) -> PushedTag {
PushedTag {
repo: repo.into(),
tag: tag.into(),
digest: "sha256:abc".into(),
}
}
fn svc(image: &str, auto: bool) -> Service {
serde_json::from_value(serde_json::json!({
"id": format!("svc_{image}"),
"spec": {
"name": "api", "project_id": "prj", "port": 80,
"source": { "Registry": { "image": image } },
"env_vars": [], "volumes": [], "domain": null, "host_port": null,
"tls_enabled": false, "replicas": 1,
"healthcheck": { "kind": "None", "interval_secs": 5, "timeout_secs": 3, "retries": 3, "start_period_secs": 0 },
"resources": { "cpu_shares": 0, "mem_limit_bytes": 0 },
"registry_auto_deploy": auto,
},
"status": "Stopped",
"live_container_id": null,
"created_at": "2026-11-07T00:00:00Z",
"updated_at": "2026-11-07T00:00:00Z",
}))
.expect("Service de teste")
}
fn imagens(servicos: Vec<&Service>) -> Vec<String> {
servicos
.into_iter()
.map(|s| match &s.spec.source {
ServiceSource::Registry { image } => image.clone(),
_ => unreachable!(),
})
.collect()
}
#[test]
fn so_redeploya_quem_pediu_e_casa_repo_e_tag() {
let todos = vec![
svc("127.0.0.1:5100/app/api:prod", true),
svc("127.0.0.1:5100/app/api:prod", false), svc("127.0.0.1:5100/app/api:dev", true), svc("127.0.0.1:5100/app/web:prod", true), svc("registry.exemplo.com/app/api:prod", true), svc("docker.io/app/api:prod", true), ];
let p = push("app/api", "prod");
assert_eq!(
imagens(services_to_redeploy(
&todos,
&p,
5100,
Some("registry.exemplo.com")
)),
[
"127.0.0.1:5100/app/api:prod",
"registry.exemplo.com/app/api:prod"
]
);
assert_eq!(
imagens(services_to_redeploy(&todos, &p, 5100, None)).len(),
1
);
}
#[test]
fn extrai_repo_e_tag_sem_o_host() {
assert_eq!(
repo_and_tag("127.0.0.1:5100/app/api:prod", true),
Some(("app/api".into(), "prod".into()))
);
assert_eq!(
repo_and_tag("registry.exemplo.com/api", true),
Some(("api".into(), "latest".into())),
"sem tag vale latest"
);
assert_eq!(repo_and_tag("127.0.0.1:5100/api@sha256:aa", true), None);
assert_eq!(repo_and_tag("docker.io/library/nginx:1", false), None);
}
}