Skip to main content

elasticctl_api/fleet/agent_policy_ops/
delete.rs

1use super::{
2    BTreeMap, Error, ErrorKind, LiveAgentPolicy, MutationPlan, Result, Serialize, Transport, Value,
3    agent_policies, json, live_from_item, read_live, resolve_item,
4};
5
6#[derive(Debug, Clone, PartialEq)]
7pub struct AgentPolicyDeleteTarget {
8    pub id: String,
9    pub name: String,
10    pub snapshot: LiveAgentPolicy,
11}
12
13#[derive(Debug, Clone, PartialEq)]
14pub struct AgentPolicyDeletePlan {
15    pub preview: MutationPlan,
16    pub targets: Vec<AgentPolicyDeleteTarget>,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize)]
20pub struct AgentPolicyDeleteReport {
21    pub applied: bool,
22    pub deleted: Vec<Value>,
23    pub failed: Vec<Value>,
24    pub total: usize,
25    pub affected_agents: u64,
26}
27
28pub async fn plan_delete(
29    transport: &Transport,
30    selectors: &[String],
31) -> Result<AgentPolicyDeletePlan> {
32    if selectors.is_empty() {
33        return Err(Error::new(
34            ErrorKind::Error,
35            "agent-policy delete needs at least one selector",
36        ));
37    }
38    let mut resolved_items = BTreeMap::new();
39    for selector in selectors {
40        let resolved = resolve_item(transport, selector).await?;
41        resolved_items.insert(resolved.summary.id, resolved.item);
42    }
43    let mut targets = Vec::new();
44    let mut conflicts = Vec::new();
45    for (id, item) in resolved_items {
46        let live = live_from_item(&item, &id, transport.space())?;
47        if live.agents > 0 {
48            conflicts.push(format!(
49                "agent policy '{id}' has {} assigned agents",
50                live.agents
51            ));
52        }
53        if !live.attached.is_empty() {
54            conflicts.push(format!(
55                "agent policy '{id}' has attached integrations: {}",
56                live.attached.join(", ")
57            ));
58        }
59        targets.push(AgentPolicyDeleteTarget {
60            id: id.clone(),
61            name: live.spec.name.clone(),
62            snapshot: live,
63        });
64    }
65    if !conflicts.is_empty() {
66        return Err(Error::new(ErrorKind::Conflict, conflicts.join("; ")));
67    }
68    Ok(AgentPolicyDeletePlan {
69        preview: delete_preview(&targets),
70        targets,
71    })
72}
73
74fn delete_preview(targets: &[AgentPolicyDeleteTarget]) -> MutationPlan {
75    MutationPlan {
76        preview_action: format!("Delete {} agent policy(ies)", targets.len()),
77        preview_details: targets
78            .iter()
79            .map(|target| {
80                format!(
81                    "{}  {}  agents {}  integrations {}",
82                    target.id,
83                    target.name,
84                    target.snapshot.agents,
85                    target.snapshot.attached.len()
86                )
87            })
88            .collect(),
89        targets: targets.iter().map(|target| target.id.clone()).collect(),
90    }
91}
92
93pub async fn apply_delete(
94    transport: &Transport,
95    plan: &AgentPolicyDeletePlan,
96) -> Result<AgentPolicyDeleteReport> {
97    validate_delete_plan(plan)?;
98    let mut deleted = Vec::new();
99    let mut failed = Vec::new();
100    for target in &plan.targets {
101        let live = match read_live(transport, &target.id).await {
102            Ok(live) => live,
103            Err(error) if error.kind == ErrorKind::NotFound => {
104                failed.push(failed_delete_row(
105                    &target.id,
106                    false,
107                    "agent policy disappeared since preview",
108                ));
109                continue;
110            }
111            Err(error) => {
112                failed.push(failed_delete_row(&target.id, false, error.message));
113                continue;
114            }
115        };
116        if live != target.snapshot {
117            failed.push(failed_delete_row(
118                &target.id,
119                false,
120                "agent policy changed since preview",
121            ));
122            continue;
123        }
124        match agent_policies::delete(transport, &target.id).await {
125            Ok(()) => deleted.push(json!({"id": target.id})),
126            Err(error) => {
127                let applied =
128                    matches!(error.http_status, Some(status) if (200..300).contains(&status));
129                failed.push(failed_delete_row(&target.id, applied, error.message));
130            }
131        }
132    }
133    Ok(AgentPolicyDeleteReport {
134        applied: true,
135        deleted,
136        failed,
137        total: plan.targets.len(),
138        affected_agents: 0,
139    })
140}
141
142fn failed_delete_row(id: &str, applied: bool, error: impl Into<String>) -> Value {
143    json!({"id": id, "applied": applied, "error": error.into()})
144}
145
146fn validate_delete_plan(plan: &AgentPolicyDeletePlan) -> Result<()> {
147    if plan.targets.is_empty() || plan.preview != delete_preview(&plan.targets) {
148        return Err(Error::new(
149            ErrorKind::Error,
150            "invalid agent-policy delete plan",
151        ));
152    }
153    let mut previous: Option<&str> = None;
154    for target in &plan.targets {
155        target.snapshot.spec.validate()?;
156        if target.id != target.snapshot.spec.id
157            || target.name != target.snapshot.spec.name
158            || target.snapshot.agents != 0
159            || !target.snapshot.attached.is_empty()
160            || previous.is_some_and(|previous| previous >= target.id.as_str())
161        {
162            return Err(Error::new(
163                ErrorKind::Error,
164                "invalid agent-policy delete plan",
165            ));
166        }
167        previous = Some(&target.id);
168    }
169    Ok(())
170}