use crate::bootstrap::state::AppState;
use bollard::{
container::RemoveContainerOptions, image::RemoveImageOptions, volume::RemoveVolumeOptions,
};
use shared::Response as RpResponse;
fn owning_service<'a>(
labels: &std::collections::HashMap<String, String>,
services: &'a [shared::Service],
) -> Option<&'a shared::Service> {
services.iter().find(|s| {
labels
.get("rustploy.service_id")
.is_some_and(|sid| crate::engine::containers::label_owned_by(sid, &s.id))
|| labels
.get("com.docker.compose.project")
.is_some_and(|cp| s.compose_project.as_deref() == Some(cp.as_str()))
})
}
pub async fn stop_container(state: AppState, id: String) -> RpResponse {
let info = match state.docker.inner.inspect_container(&id, None).await {
Ok(i) => i,
Err(e) => return RpResponse::err("RuntimeError", e.to_string()),
};
let labels = info
.config
.as_ref()
.and_then(|c| c.labels.clone())
.unwrap_or_default();
let services = crate::store::services::list_all(&state.db)
.await
.unwrap_or_default();
let owner = owning_service(&labels, &services);
if let Some(s) = owner {
return RpResponse::err(
"InvalidInput",
format!(
"este container pertence ao serviço '{}': pare-o pelo serviço (senão o watchdog o religa)",
s.spec.name
),
);
}
match state
.docker
.inner
.stop_container(
&id,
Some(bollard::container::StopContainerOptions { t: 10 }),
)
.await
{
Ok(()) => {
state.docker_cache.df.invalidate().await;
RpResponse::Ok
}
Err(e) => RpResponse::err("RuntimeError", e.to_string()),
}
}
pub async fn remove_container(state: AppState, id: String) -> RpResponse {
let opts = RemoveContainerOptions {
v: true,
..Default::default()
};
match state.docker.inner.remove_container(&id, Some(opts)).await {
Ok(()) => {
state.docker_cache.df.invalidate().await;
state.docker_cache.networks.invalidate().await;
RpResponse::Ok
}
Err(e) => RpResponse::err("RuntimeError", e.to_string()),
}
}
pub async fn remove_image(state: AppState, id: String) -> RpResponse {
let result = match remove_image_by_name(&state, &id).await {
Err(e) if needs_untag(&e) => remove_image_tag_by_tag(&state, &id).await,
other => other,
};
state.docker_cache.df.invalidate().await;
match result {
Ok(()) => RpResponse::Ok,
Err(e) => RpResponse::err("RuntimeError", e.to_string()),
}
}
async fn remove_image_by_name(state: &AppState, name: &str) -> Result<(), bollard::errors::Error> {
let opts = RemoveImageOptions {
force: false,
noprune: false,
};
state
.docker
.inner
.remove_image(name, Some(opts), None)
.await
.map(|_| ())
}
async fn remove_image_tag_by_tag(state: &AppState, id: &str) -> Result<(), bollard::errors::Error> {
let tags = state
.docker
.inner
.inspect_image(id)
.await?
.repo_tags
.unwrap_or_default();
for tag in tags {
remove_image_by_name(state, &tag).await?;
}
Ok(())
}
fn needs_untag(e: &bollard::errors::Error) -> bool {
let msg = e.to_string();
msg.contains("more than one tag") || msg.contains("multiple repositories")
}
pub async fn remove_volume(state: AppState, name: String) -> RpResponse {
match state
.docker
.inner
.remove_volume(&name, Some(RemoveVolumeOptions { force: false }))
.await
{
Ok(()) => {
state.docker_cache.df.invalidate().await;
RpResponse::Ok
}
Err(e) => RpResponse::err("RuntimeError", e.to_string()),
}
}
pub async fn remove_network(state: AppState, id: String) -> RpResponse {
match state.docker.inner.remove_network(&id).await {
Ok(()) => {
state.docker_cache.networks.invalidate().await;
RpResponse::Ok
}
Err(e) => RpResponse::err("RuntimeError", e.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn svc(id: &str, compose: Option<&str>) -> shared::Service {
let mut s = crate::features::wizard::app_spec(id.into(), "p".into());
s.name = id.into();
shared::Service {
id: id.into(),
spec: s,
compose_project: compose.map(String::from),
status: shared::ServiceStatus::Stopped,
live_container_id: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
}
}
fn labels(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn container_de_servico_vivo_nao_pode_ser_parado_por_aqui() {
let services = vec![svc("svc_app", None), svc("svc_db", Some("rp_ab12cd34_db"))];
assert!(
owning_service(&labels(&[("rustploy.service_id", "svc_app")]), &services).is_some()
);
let o = owning_service(
&labels(&[("com.docker.compose.project", "rp_ab12cd34_db")]),
&services,
);
assert_eq!(o.map(|s| s.id.as_str()), Some("svc_db"));
assert!(
owning_service(&labels(&[("rustploy.service_id", "svc_velho")]), &services).is_none()
);
assert!(
owning_service(
&labels(&[("com.docker.compose.project", "rp_outro")]),
&services
)
.is_none()
);
assert!(owning_service(&labels(&[]), &services).is_none());
}
}