Skip to main content

elasticctl_api/value_lists/
plan.rs

1use 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(&current).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}