Skip to main content

elasticctl_api/value_lists/
lifecycle.rs

1use super::{
2    Action, Evidence, InstalledReference, ListOutcome, Outcome, RemoteList, Sent, bootstrap_state,
3    decode_remote_container, read_container, read_list, require_bootstrapped, scan_references,
4};
5use super::{
6    apply::{record, same_read},
7    reads::{read_unchanged_list, required_string, response_error},
8};
9use crate::model::ListKey;
10use elasticctl_core::{Error, ErrorKind, Result, Transport, urlencode};
11use serde::Serialize;
12use serde_json::{Value, json};
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
15#[serde(rename_all = "snake_case")]
16pub enum InitOutcome {
17    AlreadyBootstrapped,
18    Bootstrapped,
19}
20
21pub async fn apply_init(t: &Transport) -> Result<InitOutcome> {
22    if bootstrap_state(t).await?.ready() {
23        return Ok(InitOutcome::AlreadyBootstrapped);
24    }
25    let (status, body) = t
26        .post_once_response(
27            "/api/security_solution/initialize",
28            Some(&json!({"flows": ["create-list-indices"]})),
29        )
30        .await?;
31    if status >= 400 {
32        return Err(response_error(status, &body));
33    }
34    let flow = body
35        .pointer("/flows/create-list-indices/status")
36        .and_then(Value::as_str);
37    if flow != Some("ready") {
38        return Err(Error::with_status(
39            ErrorKind::Http,
40            status,
41            format!(
42                "create-list-indices flow status is {}; expected ready",
43                flow.unwrap_or("unreadable")
44            ),
45        ));
46    }
47    if !bootstrap_state(t).await?.ready() {
48        return Err(Error::new(
49            ErrorKind::Http,
50            "create-list-indices flow is ready but both list streams do not exist",
51        ));
52    }
53    Ok(InitOutcome::Bootstrapped)
54}
55
56#[derive(Clone, Debug, Serialize)]
57pub struct DeletePlan {
58    pub before: Vec<RemoteList>,
59    pub installed: Vec<InstalledReference>,
60}
61
62#[derive(Clone, Debug, Serialize)]
63pub struct DeleteReference {
64    pub container: ListKey,
65    pub item_id: String,
66}
67
68#[derive(Clone, Debug, Serialize)]
69pub struct DeleteOutcome {
70    pub result: ListOutcome,
71    pub references: Vec<DeleteReference>,
72}
73
74pub async fn plan_delete(t: &Transport, ids: &[String]) -> Result<DeletePlan> {
75    require_bootstrapped(t).await?;
76    let ids: std::collections::BTreeSet<_> = ids.iter().collect();
77    let mut before = Vec::new();
78    for id in ids {
79        let list = read_list(t, id)
80            .await?
81            .ok_or_else(|| Error::new(ErrorKind::NotFound, format!("value list {id} not found")))?;
82        before.push(list);
83    }
84    let installed = scan_references(t).await?;
85    Ok(DeletePlan { before, installed })
86}
87
88pub async fn apply_delete(t: &Transport, plan: &DeletePlan) -> Vec<DeleteOutcome> {
89    let mut outcomes: Vec<_> = plan
90        .before
91        .iter()
92        .map(|before| {
93            let c = &before.container.container;
94            let mut entry = Evidence::new(Action::NotReached, &c.id, &c.name);
95            entry.values = None;
96            entry.planned_adds = Some(0);
97            entry.planned_removes = Some(before.stored.set().len());
98            DeleteOutcome {
99                result: ListOutcome {
100                    id: c.id.clone(),
101                    outcome: Outcome::NotReached,
102                    evidence: vec![entry],
103                    error: None,
104                },
105                references: Vec::new(),
106            }
107        })
108        .collect();
109    if outcomes.is_empty() {
110        return outcomes;
111    }
112    if let Err((index, error)) = delete_preflight(t, plan).await {
113        outcomes[index].result.finish(Outcome::Failed, Some(error));
114        return outcomes;
115    }
116    for (index, before) in plan.before.iter().enumerate() {
117        outcomes[index] = delete_one(t, before).await;
118        if outcomes[index].result.outcome != Outcome::Applied {
119            break;
120        }
121    }
122    outcomes
123}
124
125async fn delete_preflight(
126    t: &Transport,
127    plan: &DeletePlan,
128) -> std::result::Result<(), (usize, Error)> {
129    require_bootstrapped(t).await.map_err(|e| (0, e))?;
130    for (index, before) in plan.before.iter().enumerate() {
131        let read = read_unchanged_list(t, &before.container.container.id, Some(before))
132            .await
133            .map_err(|e| (index, e))?;
134        if !same_read(&Some(before.clone()), &read) {
135            return Err((
136                index,
137                Error::new(
138                    ErrorKind::Conflict,
139                    "value list changed since delete preview; re-run",
140                ),
141            ));
142        }
143    }
144    let references = scan_references(t).await.map_err(|e| (0, e))?;
145    let selected: std::collections::BTreeSet<_> = plan
146        .before
147        .iter()
148        .map(|r| &r.container.container.id)
149        .collect();
150    let relevant = |refs: &[InstalledReference]| {
151        refs.iter()
152            .filter(|r| selected.contains(&r.list_id))
153            .cloned()
154            .collect::<Vec<_>>()
155    };
156    if relevant(&plan.installed) != relevant(&references) {
157        return Err((
158            0,
159            Error::new(
160                ErrorKind::Conflict,
161                "installed references changed since delete preview; re-run",
162            ),
163        ));
164    }
165    Ok(())
166}
167
168pub(super) async fn delete_one(t: &Transport, before: &RemoteList) -> DeleteOutcome {
169    let c = &before.container.container;
170    let mut entry = Evidence::new(Action::DeleteValueList, &c.id, &c.name);
171    let response = t
172        .delete_once_response(&format!("/api/lists?id={}", urlencode(&c.id)))
173        .await;
174    let references = match &response {
175        Ok((409, body)) => delete_references(body),
176        _ => Vec::new(),
177    };
178    let sent = record(response, &mut entry, false, |body| {
179        decode_remote_container(body)?;
180        Ok(())
181    });
182    let mut result = ListOutcome {
183        id: c.id.clone(),
184        outcome: Outcome::NotReached,
185        evidence: vec![entry],
186        error: None,
187    };
188    match sent {
189        Err(mut error) => {
190            for reference in &references {
191                error.message.push_str(&format!(
192                    "; {} ({}) item {}",
193                    reference.container.list_id,
194                    reference.container.namespace_type,
195                    reference.item_id
196                ));
197            }
198            result.finish(Outcome::Failed, Some(error));
199        }
200        Ok(Sent::Uncertain) => {
201            let mut read = Evidence::new(Action::ReadBack, &c.id, &c.name);
202            read.of = Some(super::ReadOf::Container);
203            read.values = None;
204            read.sent = None;
205            match read_container(t, &c.id).await {
206                Ok(None) => {
207                    result.evidence[0].phase_verified = true;
208                    result.finish(Outcome::Applied, None);
209                }
210                observed => {
211                    read.observed = Some(observed.is_ok());
212                    match observed {
213                        Ok(Some(_)) => read.mismatched = Some(vec!["id".into()]),
214                        Err(error) => read.read_error = Some(error.into()),
215                        Ok(None) => unreachable!(),
216                    }
217                    result.evidence.push(read);
218                    result.finish(
219                        Outcome::Unknown,
220                        Some(Error::new(
221                            ErrorKind::Http,
222                            "value-list delete outcome is unknown",
223                        )),
224                    );
225                }
226            }
227        }
228        Ok(_) => {
229            result.evidence[0].phase_verified = true;
230            result.finish(Outcome::Applied, None);
231        }
232    }
233    DeleteOutcome { result, references }
234}
235
236fn delete_references(body: &Value) -> Vec<DeleteReference> {
237    body.pointer("/message/error/references")
238        .and_then(Value::as_array)
239        .into_iter()
240        .flatten()
241        .filter_map(|reference| {
242            let item = reference.get("exception_item")?;
243            let decode = || -> Result<DeleteReference> {
244                Ok(DeleteReference {
245                    container: ListKey {
246                        list_id: required_string(item, "list_id")?.into(),
247                        namespace_type: required_string(item, "namespace_type")?.into(),
248                    },
249                    item_id: required_string(item, "item_id")?.into(),
250                })
251            };
252            decode().ok()
253        })
254        .collect()
255}