Skip to main content

elasticctl_api/state/
values.rs

1mod cone;
2mod pull;
3
4pub(super) use pull::pull_lists;
5
6use crate::diff::FieldChange;
7use crate::model::{ExceptionItem, ExceptionList, ListKey, Rule};
8use crate::value_lists::{
9    self as vl, InstalledReference, ListPlan, LocalPair, RemoteList, Warning,
10};
11use elasticctl_core::{Error, ErrorKind, Result, Transport};
12use serde::Serialize;
13use serde_json::Value;
14use std::collections::BTreeSet;
15
16#[derive(Clone, Debug, PartialEq, Serialize)]
17pub struct ValueListChange {
18    pub id: String,
19    pub change: String,
20    pub fields: Vec<FieldChange>,
21    pub adds: Vec<String>,
22    pub removes: Vec<String>,
23}
24
25#[derive(Clone, Debug, PartialEq, Serialize)]
26pub struct ValueListIssue {
27    pub id: String,
28    pub kind: String,
29    pub message: String,
30}
31
32#[derive(Clone, Debug, Default, PartialEq, Serialize)]
33pub struct ValueListDrift {
34    pub local: usize,
35    pub remote: usize,
36    pub changes: Vec<ValueListChange>,
37    pub refused: Vec<ValueListIssue>,
38    pub warnings: Vec<ValueListIssue>,
39    pub unreferenced: Vec<String>,
40}
41
42impl ValueListDrift {
43    pub fn is_clean(&self) -> bool {
44        self.refused.is_empty()
45            && self
46                .changes
47                .iter()
48                .all(|change| change.change == "unchanged")
49    }
50}
51
52#[derive(Clone, Debug, Default)]
53pub(super) struct ValuePlan {
54    pub plans: Vec<ListPlan>,
55    pub drift: Option<ValueListDrift>,
56}
57
58pub(super) fn item_refs(items: &[ExceptionItem]) -> Result<Vec<InstalledReference>> {
59    let mut refs = Vec::new();
60    for item in items {
61        let key = ListKey {
62            list_id: item.list_id()?.into(),
63            namespace_type: item.namespace_type().into(),
64        };
65        let mut item = item.clone();
66        item.as_map_mut().insert(
67            "namespace_type".into(),
68            Value::String(key.namespace_type.clone()),
69        );
70        refs.extend(vl::item_references(&key, &item)?);
71    }
72    Ok(refs)
73}
74
75pub(super) struct Inputs<'a> {
76    pub pairs: &'a [LocalPair],
77    pub has_root: bool,
78    pub lists: &'a [ExceptionList],
79    pub items: &'a [ExceptionItem],
80    pub local: &'a [Rule],
81    pub remote: &'a [Rule],
82    pub exception_plan: &'a super::diff::ExceptionPlan,
83}
84
85pub(super) async fn plan(t: &Transport, input: Inputs<'_>, pushing: bool) -> Result<ValuePlan> {
86    let active = super::referenced_keys(input.local);
87    let mirrored: BTreeSet<_> = input
88        .lists
89        .iter()
90        .map(ExceptionList::key)
91        .collect::<Result<_>>()?;
92    let wanted: BTreeSet<_> = active
93        .iter()
94        .cloned()
95        .chain(super::referenced_keys(input.remote))
96        .collect();
97    let managed: BTreeSet<_> = wanted.intersection(&mirrored).cloned().collect();
98    let selected_items: Vec<_> = input
99        .items
100        .iter()
101        .filter(|i| {
102            managed.contains(&ListKey {
103                list_id: i.list_id().unwrap_or_default().into(),
104                namespace_type: i.namespace_type().into(),
105            })
106        })
107        .cloned()
108        .collect();
109    let selected_refs = item_refs(&selected_items)?;
110    let selected: BTreeSet<_> = selected_refs.iter().map(|r| r.list_id.clone()).collect();
111    let mut retained = Vec::new();
112    for key in active.difference(&mirrored) {
113        match crate::exceptions::get_list(t, key).await {
114            Ok(_) => retained.extend(crate::exceptions::find_items(t, key).await?),
115            Err(error) if error.kind == ErrorKind::NotFound => {}
116            Err(error) => return Err(error),
117        }
118    }
119    let mut refs = selected_refs;
120    refs.extend(item_refs(&retained)?);
121    let unreferenced = input
122        .pairs
123        .iter()
124        .filter(|p| !selected.contains(&p.container.id))
125        .map(|p| p.container.id.clone())
126        .collect();
127    let mut drift = ValueListDrift {
128        unreferenced,
129        ..Default::default()
130    };
131    if refs.is_empty() {
132        return Ok(ValuePlan {
133            drift: input.has_root.then_some(drift),
134            ..Default::default()
135        });
136    }
137    if pushing {
138        vl::require_bootstrapped(t).await?;
139    }
140    let installed = vl::scan_references(t).await?;
141    let mut desired: Vec<_> = installed
142        .iter()
143        .filter(|r| !managed.contains(&r.container))
144        .cloned()
145        .collect();
146    desired.extend(item_refs(&selected_items)?);
147    let ids: BTreeSet<_> = refs.iter().map(|r| r.list_id.clone()).collect();
148    let mut plans = Vec::new();
149    for id in ids {
150        let local = input
151            .pairs
152            .iter()
153            .find(|p| p.container.id == id && selected.contains(&id));
154        let container = vl::read_container(t, &id).await?;
155        let before = match container {
156            Some(container) if container.container.list_type.require_supported().is_ok() => {
157                let stored = vl::export_values(t, &id, &container.container.list_type).await?;
158                Some(RemoteList { container, stored })
159            }
160            Some(container) => {
161                let error = container
162                    .container
163                    .list_type
164                    .require_supported()
165                    .unwrap_err();
166                issue(&mut drift, &id, error);
167                None
168            }
169            None => None,
170        };
171        if input.has_root && selected.contains(&id) {
172            drift.local += usize::from(local.is_some());
173            drift.remote += usize::from(before.is_some());
174            drift.changes.push(change(&id, local, before.as_ref()));
175        }
176        if let Some(before) = &before {
177            warnings(
178                &mut drift,
179                &id,
180                vl::stored_warnings(&before.container.container.list_type, &before.stored),
181            );
182        }
183        let source = local
184            .map(|p| &p.container)
185            .or_else(|| before.as_ref().map(|r| &r.container.container));
186        for reference in refs.iter().filter(|r| r.list_id == id) {
187            match source {
188                None => issue(
189                    &mut drift,
190                    &id,
191                    Error::new(
192                        ErrorKind::NotFound,
193                        format!(
194                            "value list {id} named by {} has no local or stack source",
195                            reference.name()
196                        ),
197                    ),
198                ),
199                Some(source) if source.list_type != reference.list_type => issue(
200                    &mut drift,
201                    &id,
202                    Error::new(
203                        ErrorKind::Conflict,
204                        format!(
205                            "value list {id} named by {} has entry type {}, source type {}",
206                            reference.name(),
207                            reference.list_type,
208                            source.list_type
209                        ),
210                    ),
211                ),
212                Some(_) => {}
213            }
214        }
215        let pair = local.cloned().or_else(|| {
216            before.as_ref().map(|r| LocalPair {
217                stem: vl::file_stem(&id),
218                container: r.container.container.clone(),
219                values: r.stored.set(),
220            })
221        });
222        if let Some(pair) = pair {
223            match vl::plan_list(pair, before.clone(), &installed, &desired) {
224                Ok(plan) => {
225                    warnings(&mut drift, &id, plan.warnings.clone());
226                    plans.push(plan);
227                }
228                Err(error)
229                    if !pushing
230                        && error.kind == ErrorKind::Error
231                        && error.message.contains("non-canonical") =>
232                {
233                    drift.warnings.push(ValueListIssue {
234                        id: id.clone(),
235                        kind: "non_canonical".into(),
236                        message: error.message,
237                    });
238                }
239                Err(error) => issue(&mut drift, &id, error),
240            }
241        }
242        if let Some(before) = &before {
243            if before.container.container.list_type == vl::ValueListType::IpRange
244                && before.stored.dash_count() > 200
245                && !pushing
246            {
247                issue(
248                    &mut drift,
249                    &id,
250                    Error::new(
251                        ErrorKind::Unsupported,
252                        format!(
253                            "value list {id} holds {} stored dash values, above 200",
254                            before.stored.dash_count()
255                        ),
256                    ),
257                );
258            }
259            if before.stored.values.is_empty() && !pushing {
260                issue(
261                    &mut drift,
262                    &id,
263                    Error::new(
264                        ErrorKind::Unsupported,
265                        format!("referenced value list {id} is empty"),
266                    ),
267                );
268            }
269        }
270    }
271    if let Err(error) = cone::check(
272        &plans,
273        &installed,
274        &desired,
275        input.local,
276        input.remote,
277        input.exception_plan,
278    ) {
279        let id = plans
280            .iter()
281            .find(|p| !p.adds.is_empty() || !p.removes.is_empty())
282            .map(|p| p.local.container.id.as_str())
283            .unwrap_or_default();
284        issue(&mut drift, id, error);
285    }
286    if pushing && let Some(refusal) = drift.refused.first() {
287        return Err(Error::new(kind(&refusal.kind), refusal.message.clone()));
288    }
289    let show = input.has_root || !drift.refused.is_empty();
290    Ok(ValuePlan {
291        plans,
292        drift: show.then_some(drift),
293    })
294}
295
296fn kind(kind: &str) -> ErrorKind {
297    match kind {
298        "not_found" => ErrorKind::NotFound,
299        "conflict" => ErrorKind::Conflict,
300        "unsupported" => ErrorKind::Unsupported,
301        _ => ErrorKind::Error,
302    }
303}
304
305fn issue(drift: &mut ValueListDrift, id: &str, error: Error) {
306    drift.refused.push(ValueListIssue {
307        id: id.into(),
308        kind: error.kind.as_str().into(),
309        message: error.message,
310    });
311}
312
313fn warnings(drift: &mut ValueListDrift, id: &str, warnings: Vec<Warning>) {
314    for warning in warnings {
315        let issue = ValueListIssue {
316            id: id.into(),
317            kind: serde_json::to_value(warning.kind)
318                .expect("warning kind serializes")
319                .as_str()
320                .unwrap()
321                .into(),
322            message: warning.message,
323        };
324        if !drift.warnings.contains(&issue) {
325            drift.warnings.push(issue);
326        }
327    }
328}
329
330fn change(id: &str, local: Option<&LocalPair>, remote: Option<&RemoteList>) -> ValueListChange {
331    let current = remote.map(|r| r.stored.set()).unwrap_or_default();
332    let target = local
333        .map(|p| p.values.clone())
334        .unwrap_or_else(|| current.clone());
335    let mut fields = Vec::new();
336    if let (Some(local), Some(remote)) = (local, remote) {
337        let before = serde_json::to_value(&remote.container.container).unwrap();
338        let after = serde_json::to_value(&local.container).unwrap();
339        for field in ["type", "name", "description", "meta"] {
340            if field == "meta" && local.container.meta.is_none() {
341                continue;
342            }
343            if before.get(field) != after.get(field) {
344                fields.push(FieldChange {
345                    field: field.into(),
346                    before: before.get(field).cloned().unwrap_or(Value::Null),
347                    after: after.get(field).cloned().unwrap_or(Value::Null),
348                });
349            }
350        }
351    }
352    let adds: Vec<_> = target.difference(&current).cloned().collect();
353    let removes: Vec<_> = current.difference(&target).cloned().collect();
354    let change = match (local, remote) {
355        (Some(_), None) => "local_only",
356        (None, Some(_)) => "remote_only",
357        _ if !fields.is_empty() || !adds.is_empty() || !removes.is_empty() => "changed",
358        _ => "unchanged",
359    };
360    ValueListChange {
361        id: id.into(),
362        change: change.into(),
363        fields,
364        adds,
365        removes,
366    }
367}
368
369pub(super) async fn reread(t: &Transport, plan: &ValuePlan) -> Result<()> {
370    for list in &plan.plans {
371        let id = &list.local.container.id;
372        let read = vl::read_container(t, id).await.map_err(|error| {
373            if error.kind == ErrorKind::Http && error.http_status.is_none() {
374                Error::new(
375                    ErrorKind::Conflict,
376                    format!(
377                        "value list {id} container cannot be validated since preview; re-run: {}",
378                        error.message
379                    ),
380                )
381            } else {
382                error
383            }
384        })?;
385        if !read.is_some_and(|r| {
386            r.container.id == *id && r.container.list_type == list.local.container.list_type
387        }) {
388            return Err(Error::new(
389                ErrorKind::Conflict,
390                format!("value list {id} id or type changed since preview; re-run"),
391            ));
392        }
393    }
394    Ok(())
395}
396
397#[cfg(test)]
398pub(super) mod tests;