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#[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}