elasticctl_api/rules_ops/
create.rs1use super::*;
4use crate::Preconditions;
5
6#[derive(Debug, Clone, PartialEq)]
8pub struct RuleCreatePlan {
9 pub rule: Rule,
11 pub preview: MutationPlan,
13}
14
15#[derive(Debug, Clone, PartialEq, serde::Serialize)]
17pub struct RuleCreateOutcome {
18 pub applied: bool,
20 pub rule_id: String,
22 pub enabled: bool,
24}
25
26pub 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
127pub 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}