Skip to main content

elasticctl_api/fleet/agent_policy_ops/
delete.rs

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