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