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