Skip to main content

elasticctl_api/rules_ops/
create.rs

1//! Rule creation plans and outcomes.
2
3use super::*;
4use crate::Preconditions;
5
6/// A rule creation plan with its preview.
7#[derive(Debug, Clone, PartialEq)]
8pub struct RuleCreatePlan {
9    /// The rule to create.
10    pub rule: Rule,
11    /// The mutation preview.
12    pub preview: MutationPlan,
13}
14
15/// The outcome of creating a disabled rule.
16#[derive(Debug, Clone, PartialEq, serde::Serialize)]
17pub struct RuleCreateOutcome {
18    /// Whether the rule was created.
19    pub applied: bool,
20    /// The created rule ID.
21    pub rule_id: String,
22    /// Whether the created rule is enabled.
23    pub enabled: bool,
24}
25
26/// Plan creation of a disabled rule and read its absence.
27pub async fn plan_create(t: &Transport, mut rule: Rule) -> Result<(RuleCreatePlan, Preconditions)> {
28    validate_local(&rule)?;
29    let id = rule.rule_id()?.to_owned();
30    rule.as_map_mut().insert("enabled".into(), json!(false));
31    match rules::get(t, &id).await {
32        Ok(_) => {
33            return Err(Error::new(
34                ErrorKind::Conflict,
35                "a rule with this rule_id already exists",
36            ));
37        }
38        Err(err) if err.kind == ErrorKind::NotFound => {}
39        Err(err) => return Err(err),
40    }
41    let mut keys: Vec<_> = rule.as_map().keys().collect();
42    keys.sort_unstable();
43    let preview_details = keys
44        .into_iter()
45        .map(|key| {
46            let mut value = rule.as_map()[key].clone();
47            sort_value(&mut value);
48            let escaped_key = serde_json::to_string(key).expect("JSON keys serialize");
49            let escaped_key = &escaped_key[1..escaped_key.len() - 1];
50            format!(
51                "{escaped_key}: {}",
52                serde_json::to_string(&value).expect("JSON values serialize")
53            )
54        })
55        .collect();
56    let preview = MutationPlan {
57        preview_action: "Create 1 rule (disabled)".into(),
58        targets: vec![id.clone()],
59        preview_details,
60    };
61    let seen = Preconditions(vec![crate::observe::Observed {
62        object: "rule",
63        id,
64        state: None,
65    }]);
66    Ok((RuleCreatePlan { rule, preview }, seen))
67}
68
69fn validate_local(rule: &Rule) -> Result<()> {
70    rule.rule_id()?;
71    for key in ["name", "type"] {
72        if !rule.as_map().get(key).is_some_and(Value::is_string) {
73            return Err(Error::new(
74                ErrorKind::Error,
75                "name and type must be strings",
76            ));
77        }
78    }
79    if crate::model::VOLATILE_FIELDS
80        .iter()
81        .any(|key| rule.as_map().contains_key(*key))
82    {
83        return Err(Error::new(
84            ErrorKind::Error,
85            "rule contains a volatile field",
86        ));
87    }
88    if rule
89        .as_map()
90        .get("enabled")
91        .is_some_and(|v| v != &json!(false))
92    {
93        return Err(Error::new(ErrorKind::Error, "rule must be disabled"));
94    }
95    for key in ["exceptions_list", "actions", "response_actions"] {
96        if rule
97            .as_map()
98            .get(key)
99            .is_some_and(|v| !v.as_array().is_some_and(Vec::is_empty))
100        {
101            return Err(Error::new(
102                ErrorKind::Unsupported,
103                "rule dependencies must be empty arrays",
104            ));
105        }
106    }
107    Ok(())
108}
109
110fn sort_value(value: &mut Value) {
111    match value {
112        Value::Object(map) => {
113            for value in map.values_mut() {
114                sort_value(value);
115            }
116            map.sort_keys();
117        }
118        Value::Array(values) => {
119            for value in values {
120                sort_value(value);
121            }
122        }
123        _ => {}
124    }
125}
126
127/// Create the disabled rule in the plan.
128pub async fn apply_create(t: &Transport, plan: &RuleCreatePlan) -> Result<RuleCreateOutcome> {
129    validate_local(&plan.rule)?;
130    if plan.rule.as_map().get("enabled") != Some(&json!(false)) {
131        return Err(Error::new(ErrorKind::Error, "rule must be disabled"));
132    }
133    let id = plan.rule.rule_id()?;
134    let response = rules::create(t, &plan.rule).await?;
135    if response.rule_id().ok() != Some(id)
136        || response.as_map().get("enabled") != Some(&json!(false))
137    {
138        return Err(Error::new(
139            ErrorKind::Http,
140            "created rule has unexpected identity or enabled state",
141        ));
142    }
143    Ok(RuleCreateOutcome {
144        applied: true,
145        rule_id: id.to_owned(),
146        enabled: false,
147    })
148}