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