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(¤t).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;