elasticctl_api/fleet/agent_policy_ops/
delete.rs1use 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 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}