Skip to main content

elasticctl_api/fleet/integration_policy_ops/
delete_apply.rs

1use 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/// The result of applying an integration-policy deletion plan.
9#[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
18/// Recheck the exact planning snapshots, then delete each independent target.
19/// An acknowledged wrong-id response is deliberately not treated as a clean
20/// deletion, and never advances shared parent expectations.
21pub 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}