elasticctl_api/value_lists/
lifecycle.rs1use 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}