elasticctl_api/value_lists/
plan.rs1use elasticctl_core::{Error, ErrorKind, Result, Transport};
2use serde::Serialize;
3
4use super::scan::sort_references;
5use super::validation::MAX_VALUE_BYTES_WITH_LF;
6use super::{
7 InstalledReference, LocalPair, PhaseOrder, ReferenceOperator, ReferenceUse, RemoteList,
8 StoredValues, ValueSet, Warning, file_stem, project_states, read_list, require_bootstrapped,
9 scan_references, stored_warnings, validate_additions, validate_projected,
10};
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
13#[serde(rename_all = "snake_case")]
14pub enum ContainerAction {
15 Create,
16 Update,
17 None,
18}
19
20#[derive(Clone, Debug, Serialize)]
21pub struct ListPlan {
22 pub local: LocalPair,
23 pub before: Option<RemoteList>,
24 pub installed: Vec<InstalledReference>,
25 pub desired: Vec<InstalledReference>,
26 pub container_action: ContainerAction,
27 pub adds: ValueSet,
28 pub removes: ValueSet,
29 pub order: PhaseOrder,
30 pub order_reason: String,
31 pub warnings: Vec<Warning>,
32}
33
34pub fn container_mismatches(
35 expected: &super::Container,
36 observed: &super::Container,
37) -> Vec<String> {
38 let mut fields = Vec::new();
39 if expected.id != observed.id {
40 fields.push("id".into());
41 }
42 if expected.list_type != observed.list_type {
43 fields.push("type".into());
44 }
45 if expected.name != observed.name {
46 fields.push("name".into());
47 }
48 if expected.description != observed.description {
49 fields.push("description".into());
50 }
51 if expected.meta.is_some() && expected.meta != observed.meta {
52 fields.push("meta".into());
53 }
54 fields
55}
56
57pub fn plan_list(
58 local: LocalPair,
59 before: Option<RemoteList>,
60 installed: &[InstalledReference],
61 desired: &[InstalledReference],
62) -> Result<ListPlan> {
63 super::stems::validate_id(&local.container.id)?;
64 if local.stem != file_stem(&local.container.id) {
65 return Err(super::invalid("value-list file stem does not match its id"));
66 }
67 local.container.list_type.require_supported()?;
68 let id = &local.container.id;
69 let container_action = match &before {
70 None => ContainerAction::Create,
71 Some(remote) => {
72 if remote.container.container.id != *id {
73 return Err(Error::new(
74 ErrorKind::Conflict,
75 "local and remote value-list ids differ",
76 ));
77 }
78 if remote.container.container.list_type != local.container.list_type {
79 return Err(Error::new(
80 ErrorKind::Conflict,
81 format!(
82 "value list {id}: type change from {} to {} is refused",
83 remote.container.container.list_type, local.container.list_type
84 ),
85 ));
86 }
87 if container_mismatches(&local.container, &remote.container.container).is_empty() {
88 ContainerAction::None
89 } else {
90 ContainerAction::Update
91 }
92 }
93 };
94 let stored = before
95 .as_ref()
96 .map(|r| r.stored.clone())
97 .unwrap_or_default();
98 let current = stored.set();
99 let adds: ValueSet = local.values.difference(¤t).cloned().collect();
100 let removes: ValueSet = current.difference(&local.values).cloned().collect();
101 let additions = validate_additions(&local.container.list_type, &adds);
102 if let Some(error) = additions.refusals.into_iter().next() {
103 return Err(error);
104 }
105 let mut installed: Vec<_> = installed
106 .iter()
107 .filter(|r| r.list_id == *id)
108 .cloned()
109 .collect();
110 let mut desired: Vec<_> = desired
111 .iter()
112 .filter(|r| r.list_id == *id)
113 .cloned()
114 .collect();
115 sort_references(&mut installed);
116 sort_references(&mut desired);
117 for reference in installed.iter().chain(&desired) {
118 if reference.list_type != local.container.list_type {
119 return Err(Error::new(
120 ErrorKind::Conflict,
121 format!(
122 "{} names list {id} as {}, source type is {}",
123 reference.name(),
124 reference.list_type,
125 local.container.list_type
126 ),
127 ));
128 }
129 }
130 if !adds.is_empty() || !removes.is_empty() {
131 let mut items = std::collections::BTreeSet::new();
132 for reference in &installed {
133 if !items.insert((&reference.container, &reference.item_id)) {
134 return Err(Error::new(
135 ErrorKind::Conflict,
136 format!(
137 "list {id} is referenced more than once by {}",
138 reference.name()
139 ),
140 ));
141 }
142 }
143 }
144 let removes_first = installed
145 .iter()
146 .all(|r| r.operator == ReferenceOperator::Included)
147 && current.difference(&removes).next().is_some();
148 let (order, order_reason) = if removes_first {
149 (
150 PhaseOrder::RemovesFirst,
151 "every installed reference is included and removing values keeps the list non-empty",
152 )
153 } else {
154 (
155 PhaseOrder::AddsFirst,
156 "an installed reference is excluded or removing values first would empty the list",
157 )
158 };
159 let states = project_states(&stored, &adds, &removes, order);
160 let checks = validate_projected(
161 &local.container.list_type,
162 &states,
163 &ReferenceUse {
164 installed: installed.iter().map(InstalledReference::name).collect(),
165 desired: desired.iter().map(InstalledReference::name).collect(),
166 creating: before.is_none(),
167 },
168 );
169 if let Some(error) = checks.refusals.into_iter().next() {
170 return Err(error);
171 }
172 let mut warnings = stored_warnings(&local.container.list_type, &stored);
173 if let Some(warning) = super::size_warning(
174 &local.container.list_type,
175 states.final_values.values.iter().map(String::as_str),
176 ) && !warnings.contains(&warning)
177 {
178 warnings.push(warning);
179 }
180 warnings.extend(
181 checks
182 .warnings
183 .into_iter()
184 .filter(|w| !warnings.iter().any(|existing| existing == w))
185 .collect::<Vec<_>>(),
186 );
187 Ok(ListPlan {
188 local,
189 before,
190 installed,
191 desired,
192 container_action,
193 adds,
194 removes,
195 order,
196 order_reason: order_reason.into(),
197 warnings,
198 })
199}
200
201pub async fn plan_import(t: &Transport, local: LocalPair, overwrite: bool) -> Result<ListPlan> {
202 let validation = validate_additions(&local.container.list_type, &ValueSet::new());
203 if let Some(error) = validation.refusals.into_iter().next() {
204 return Err(error);
205 }
206 super::stems::validate_id(&local.container.id)?;
207 require_bootstrapped(t).await?;
208 let remote = read_list(t, &local.container.id).await?;
209 if remote.is_some() && !overwrite {
210 return Err(Error::new(
211 ErrorKind::Conflict,
212 format!(
213 "value list {} already exists; use --overwrite",
214 local.container.id
215 ),
216 ));
217 }
218 let installed = scan_references(t).await?;
219 plan_list(local, remote, &installed, &[])
220}
221
222pub fn pack_imports(values: &ValueSet) -> Result<Vec<String>> {
223 let mut chunks = Vec::new();
224 let mut chunk = String::new();
225 for value in values {
226 if value.len() >= MAX_VALUE_BYTES_WITH_LF {
227 return Err(super::invalid(
228 "one value with its LF exceeds the 8,000,000-byte import bound",
229 ));
230 }
231 if chunk.len() + value.len() + 1 > MAX_VALUE_BYTES_WITH_LF {
232 chunks.push(std::mem::take(&mut chunk));
233 }
234 chunk.push_str(value);
235 chunk.push('\n');
236 }
237 if !chunk.is_empty() {
238 chunks.push(chunk);
239 }
240 Ok(chunks)
241}
242
243pub(super) fn stored_equal(a: &StoredValues, b: &StoredValues) -> bool {
244 let mut a: Vec<_> = a.values.iter().collect();
245 let mut b: Vec<_> = b.values.iter().collect();
246 a.sort();
247 b.sort();
248 a == b
249}