elasticctl_api/fleet/integration_policy_ops/
delete_apply.rs1use super::common::*;
2use super::delete_plan::{
3 IntegrationPolicyDeletePlan, IntegrationPolicyDeleteTarget, delete_remote_error,
4 validate_delete_plan,
5};
6use super::metadata::validate_package_metadata_snapshot;
7
8#[derive(Debug, Clone, PartialEq, Serialize)]
10pub struct IntegrationPolicyDeleteReport {
11 pub applied: bool,
12 pub deleted: Vec<Value>,
13 pub failed: Vec<Value>,
14 pub total: usize,
15 pub affected_agents: u64,
16}
17
18pub async fn apply_delete(
22 transport: &Transport,
23 plan: &IntegrationPolicyDeletePlan,
24) -> Result<IntegrationPolicyDeleteReport> {
25 validate_delete_plan(plan)?;
26 if plan.host != transport.kibana_url() || plan.space != transport.space() {
27 return Err(Error::new(
28 ErrorKind::Conflict,
29 "integration delete target changed since preview",
30 ));
31 }
32
33 let mut expected_parents = plan.parent_snapshots.clone();
34 let mut affected = BTreeMap::new();
35 let mut deleted = Vec::new();
36 let mut failed = Vec::new();
37
38 for target in &plan.targets {
39 match integration_policies::get(transport, &target.id).await {
40 Ok(actual) if actual.item == target.item => {}
41 Ok(_) => {
42 failed.push(delete_failed_row(
43 &target.id,
44 false,
45 "integration policy changed since preview",
46 ));
47 continue;
48 }
49 Err(error) if error.kind == ErrorKind::NotFound => {
50 failed.push(delete_failed_row(
51 &target.id,
52 false,
53 "integration policy disappeared since preview",
54 ));
55 continue;
56 }
57 Err(error) => {
58 failed.push(delete_failed_row(
59 &target.id,
60 false,
61 delete_remote_error(error, "apply integration-policy read").message,
62 ));
63 continue;
64 }
65 }
66
67 if let Err(error) = recheck_delete_parents(transport, target, &expected_parents).await {
68 failed.push(delete_failed_row(&target.id, false, error.message));
69 continue;
70 }
71 match read_dependencies(transport, &target.package).await {
72 Ok(actual) if actual == target.dependency => {}
73 Ok(_) => {
74 failed.push(delete_failed_row(
75 &target.id,
76 false,
77 "integration policy package changed since preview",
78 ));
79 continue;
80 }
81 Err(error) => {
82 failed.push(delete_failed_row(
83 &target.id,
84 false,
85 delete_remote_error(error, "apply package read").message,
86 ));
87 continue;
88 }
89 }
90 if let Err(error) = recheck_delete_metadata(transport, target).await {
91 failed.push(delete_failed_row(&target.id, false, error.message));
92 continue;
93 }
94
95 match integration_policies::delete(transport, &target.id).await {
96 Ok(()) => {
97 record_delete_affected_parents(&mut affected, target, &expected_parents);
98 advance_delete_parent_snapshots(&mut expected_parents, target);
99 deleted.push(json!({"id": target.id}));
100 }
101 Err(error) => {
102 let applied = error
103 .http_status
104 .is_some_and(|status| (200..300).contains(&status));
105 let message = if applied {
106 "integration-policy delete response did not confirm the requested id"
107 } else {
108 "integration-policy delete request failed"
109 };
110 failed.push(delete_failed_row(&target.id, applied, message));
111 }
112 }
113 }
114
115 Ok(IntegrationPolicyDeleteReport {
116 applied: true,
117 deleted,
118 failed,
119 total: plan.total,
120 affected_agents: affected.values().sum(),
121 })
122}
123
124async fn recheck_delete_parents(
125 transport: &Transport,
126 target: &IntegrationPolicyDeleteTarget,
127 expected_parents: &BTreeMap<String, agent_policy_ops::AgentPolicyParentSnapshot>,
128) -> Result<()> {
129 for parent_id in target.parents.keys() {
130 let expected = expected_parents.get(parent_id).ok_or_else(|| {
131 Error::new(
132 ErrorKind::Error,
133 "integration delete lost a shared parent snapshot",
134 )
135 })?;
136 match agent_policy_ops::read_parent_snapshot(transport, parent_id).await {
137 Ok(actual) if actual == *expected => {}
138 Ok(_) => {
139 return Err(Error::new(
140 ErrorKind::Conflict,
141 "integration policy parent changed since preview",
142 ));
143 }
144 Err(error) if error.kind == ErrorKind::NotFound => {
145 return Err(Error::new(
146 ErrorKind::NotFound,
147 "integration policy parent disappeared since preview",
148 ));
149 }
150 Err(error) => return Err(delete_remote_error(error, "apply parent read")),
151 }
152 }
153 Ok(())
154}
155
156async fn recheck_delete_metadata(
157 transport: &Transport,
158 target: &IntegrationPolicyDeleteTarget,
159) -> Result<()> {
160 let metadata = integration_policies::package_metadata(
161 transport,
162 &target.package.name,
163 &target.package.version,
164 )
165 .await
166 .map_err(|error| delete_remote_error(error, "apply package metadata read"))?
167 .item;
168 validate_package_metadata_snapshot(&metadata, &target.package)
169 .map_err(|error| delete_remote_error(error, "apply package metadata read"))?;
170 if metadata != target.metadata {
171 return Err(Error::new(
172 ErrorKind::Conflict,
173 "integration policy package metadata changed since preview",
174 ));
175 }
176 Ok(())
177}
178
179fn record_delete_affected_parents(
180 affected: &mut BTreeMap<String, u64>,
181 target: &IntegrationPolicyDeleteTarget,
182 expected_parents: &BTreeMap<String, agent_policy_ops::AgentPolicyParentSnapshot>,
183) {
184 for parent_id in target.parents.keys() {
185 let parent = expected_parents
186 .get(parent_id)
187 .expect("validated delete target parent exists in shared snapshots");
188 affected.entry(parent.id.clone()).or_insert(parent.agents);
189 }
190}
191
192fn advance_delete_parent_snapshots(
193 parents: &mut BTreeMap<String, agent_policy_ops::AgentPolicyParentSnapshot>,
194 target: &IntegrationPolicyDeleteTarget,
195) {
196 for parent_id in target.parents.keys() {
197 let parent = parents
198 .get_mut(parent_id)
199 .expect("validated delete target parent exists in shared snapshots");
200 parent
201 .attached_integrations
202 .retain(|attached| attached != &target.id);
203 }
204}
205
206fn delete_failed_row(id: &str, applied: bool, error: impl Into<String>) -> Value {
207 json!({"id": id, "applied": applied, "error": error.into()})
208}