1use elasticctl_core::{Error, ErrorKind, Feature, Result, Transport, urlencode};
4use serde::{Deserialize, Deserializer, Serialize};
5use serde_json::{Map, Value};
6
7const BASE: &str = "/api/fleet/package_policies";
8const PACKAGES: &str = "/api/fleet/epm/packages";
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(deny_unknown_fields)]
13pub struct IntegrationPackageSpec {
14 pub name: String,
15 pub version: String,
16}
17
18#[derive(Debug, Clone, PartialEq, Serialize)]
20pub struct IntegrationPolicySpec {
21 pub id: String,
22 pub name: String,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub description: Option<String>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub namespace: Option<String>,
27 pub policy_ids: Vec<String>,
28 pub package: IntegrationPackageSpec,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub vars: Option<Map<String, Value>>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub var_group_selections: Option<Map<String, Value>>,
33 pub inputs: Map<String, Value>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub condition: Option<String>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub additional_datastreams_permissions: Option<Vec<String>>,
38}
39
40#[derive(Deserialize)]
41#[serde(deny_unknown_fields)]
42struct RawIntegrationPolicySpec {
43 id: String,
44 name: String,
45 #[serde(default)]
46 description: Option<String>,
47 #[serde(default)]
48 namespace: Option<String>,
49 policy_ids: Vec<String>,
50 package: IntegrationPackageSpec,
51 #[serde(default)]
52 vars: Option<Map<String, Value>>,
53 #[serde(default)]
54 var_group_selections: Option<Map<String, Value>>,
55 inputs: Map<String, Value>,
56 #[serde(default)]
57 condition: Option<String>,
58 #[serde(default)]
59 additional_datastreams_permissions: Option<Vec<String>>,
60}
61
62impl IntegrationPolicySpec {
63 fn from_raw(raw: RawIntegrationPolicySpec) -> Result<Self> {
64 let spec = Self {
65 id: raw.id,
66 name: raw.name,
67 description: raw.description,
68 namespace: raw.namespace,
69 policy_ids: raw.policy_ids,
70 package: raw.package,
71 vars: raw.vars,
72 var_group_selections: raw.var_group_selections,
73 inputs: raw.inputs,
74 condition: raw.condition,
75 additional_datastreams_permissions: raw.additional_datastreams_permissions,
76 };
77 spec.validate()?;
78 Ok(spec)
79 }
80
81 pub fn validate(&self) -> Result<()> {
83 for (field, value) in [
84 ("id", &self.id),
85 ("name", &self.name),
86 ("package.name", &self.package.name),
87 ("package.version", &self.package.version),
88 ] {
89 if value.trim().is_empty() {
90 return Err(Error::new(
91 ErrorKind::Error,
92 format!("integration policy {field} must not be empty"),
93 ));
94 }
95 }
96 if self.policy_ids.is_empty() {
97 return Err(Error::new(
98 ErrorKind::Error,
99 "integration policy policy_ids must not be empty",
100 ));
101 }
102 let mut previous = None;
103 for policy_id in &self.policy_ids {
104 if policy_id.trim().is_empty() {
105 return Err(Error::new(
106 ErrorKind::Error,
107 "integration policy policy_ids must not contain an empty id",
108 ));
109 }
110 if previous.is_some_and(|previous: &String| previous >= policy_id) {
111 return Err(Error::new(
112 ErrorKind::Error,
113 "integration policy policy_ids must be sorted and duplicate-free",
114 ));
115 }
116 previous = Some(policy_id);
117 }
118 if let Some(selections) = &self.var_group_selections {
119 for (name, selection) in selections {
120 if !selection.is_string() {
121 return Err(Error::new(
122 ErrorKind::Error,
123 format!("integration policy var_group_selections.{name} must be a string"),
124 ));
125 }
126 }
127 }
128 Ok(())
129 }
130}
131
132impl TryFrom<Value> for IntegrationPolicySpec {
133 type Error = Error;
134
135 fn try_from(value: Value) -> Result<Self> {
136 serde_json::from_value(value).map_err(|error| {
137 Error::new(
138 ErrorKind::Error,
139 format!("decoding integration policy: {error}"),
140 )
141 })
142 }
143}
144
145impl<'de> Deserialize<'de> for IntegrationPolicySpec {
146 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
147 where
148 D: Deserializer<'de>,
149 {
150 let raw = RawIntegrationPolicySpec::deserialize(deserializer)?;
151 Self::from_raw(raw).map_err(serde::de::Error::custom)
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
157pub struct IntegrationPolicySummary {
158 pub id: String,
159 pub name: String,
160 pub namespace: String,
161 pub description: Option<String>,
162 pub policy_ids: Vec<String>,
163 pub package: IntegrationPackageSpec,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
168pub struct IntegrationPolicyDetail {
169 pub id: String,
170 pub name: String,
171 pub namespace: String,
172 pub description: Option<String>,
173 pub policy_ids: Vec<String>,
174 pub package: IntegrationPackageSpec,
175 pub affected_agents: u64,
176 pub blocked_by: Vec<String>,
177}
178
179#[derive(Debug, Clone, PartialEq)]
181pub struct IntegrationPolicyPage {
182 pub items: Vec<Map<String, Value>>,
183 pub total: u64,
184 pub page: u64,
185 pub per_page: u64,
186}
187
188#[derive(Debug, Clone, PartialEq)]
190pub struct IntegrationPolicy {
191 pub item: Map<String, Value>,
192}
193
194#[derive(Debug, Clone, PartialEq)]
196pub struct PackageMetadata {
197 pub(crate) item: Map<String, Value>,
198}
199
200pub async fn list_page(transport: &Transport, page: u64) -> Result<IntegrationPolicyPage> {
202 transport.require_feature(Feature::FleetPolicies).await?;
203 let body = transport
204 .get(&format!(
205 "{BASE}?page={page}&perPage=1000&sortField=created_at&sortOrder=asc&format=simplified"
206 ))
207 .await?;
208 let envelope: PageEnvelope = decode(&body, "integration policies list")?;
209 Ok(IntegrationPolicyPage {
210 items: envelope.items,
211 total: envelope.total,
212 page: envelope.page,
213 per_page: envelope.per_page,
214 })
215}
216
217pub async fn get(transport: &Transport, id: &str) -> Result<IntegrationPolicy> {
219 transport.require_feature(Feature::FleetPolicies).await?;
220 decode_item(
221 &transport
222 .get(&format!("{}?format=simplified", policy_path(id)))
223 .await?,
224 "integration policy get",
225 )
226}
227
228pub async fn create(
230 transport: &Transport,
231 spec: &IntegrationPolicySpec,
232) -> Result<IntegrationPolicy> {
233 transport.require_feature(Feature::FleetPolicies).await?;
234 spec.validate()?;
235 let body = encode_spec(spec, "create")?;
236 decode_item(
237 &transport.post(BASE, Some(&body)).await?,
238 "integration policy create",
239 )
240}
241
242pub async fn update(
244 transport: &Transport,
245 id: &str,
246 spec: &IntegrationPolicySpec,
247) -> Result<IntegrationPolicy> {
248 transport.require_feature(Feature::FleetPolicies).await?;
249 spec.validate()?;
250 let mut body = encode_spec(spec, "update")?;
251 let object = body
252 .as_object_mut()
253 .expect("integration policy serialization is an object");
254 object.remove("id");
255 decode_item(
256 &transport.put(&policy_path(id), &body).await?,
257 "integration policy update",
258 )
259}
260
261pub async fn delete(transport: &Transport, id: &str) -> Result<()> {
263 transport.require_feature(Feature::FleetPolicies).await?;
264 let response = transport.delete(&policy_path(id)).await?;
265 let deleted: DeleteEnvelope = decode(&response, "integration policy delete")?;
266 if deleted.id != id {
267 return Err(Error::with_status(
268 ErrorKind::Http,
269 200,
270 format!(
271 "decoding integration policy delete: expected id '{id}', got '{}'",
272 deleted.id
273 ),
274 ));
275 }
276 Ok(())
277}
278
279pub async fn package_metadata(
281 transport: &Transport,
282 name: &str,
283 version: &str,
284) -> Result<PackageMetadata> {
285 transport.require_feature(Feature::FleetPolicies).await?;
286 for (field, value) in [("name", name), ("version", version)] {
287 if value.trim().is_empty() {
288 return Err(Error::new(
289 ErrorKind::Error,
290 format!("integration package metadata {field} must not be empty"),
291 ));
292 }
293 }
294 let body = transport
295 .get(&format!(
296 "{PACKAGES}/{}/{}",
297 urlencode(name),
298 urlencode(version)
299 ))
300 .await?;
301 let envelope: ItemEnvelope = decode(&body, "integration package metadata")?;
302 let returned_name = required_string(&envelope.item, "name", "integration package metadata")?;
303 let returned_version =
304 required_string(&envelope.item, "version", "integration package metadata")?;
305 if returned_name != name || returned_version != version {
306 return Err(Error::new(
307 ErrorKind::Http,
308 format!(
309 "decoding integration package metadata: expected {name}@{version}, got {returned_name}@{returned_version}"
310 ),
311 ));
312 }
313 Ok(PackageMetadata {
314 item: envelope.item,
315 })
316}
317
318fn encode_spec(spec: &IntegrationPolicySpec, context: &str) -> Result<Value> {
319 serde_json::to_value(spec).map_err(|error| {
320 Error::new(
321 ErrorKind::Error,
322 format!("encoding integration policy {context}: {error}"),
323 )
324 })
325}
326
327fn policy_path(id: &str) -> String {
328 format!("{BASE}/{}", urlencode(id))
329}
330
331fn decode_item(body: &Value, context: &str) -> Result<IntegrationPolicy> {
332 let envelope: ItemEnvelope = decode(body, context)?;
333 Ok(IntegrationPolicy {
334 item: envelope.item,
335 })
336}
337
338fn decode<T: serde::de::DeserializeOwned>(body: &Value, context: &str) -> Result<T> {
339 serde_json::from_value(body.clone())
340 .map_err(|error| Error::new(ErrorKind::Http, format!("decoding {context}: {error}")))
341}
342
343fn required_string(item: &Map<String, Value>, field: &str, context: &str) -> Result<String> {
344 item.get(field)
345 .and_then(Value::as_str)
346 .filter(|value| !value.trim().is_empty())
347 .map(str::to_owned)
348 .ok_or_else(|| {
349 Error::new(
350 ErrorKind::Http,
351 format!("decoding {context}: {field} must be a non-empty string"),
352 )
353 })
354}
355
356#[derive(Deserialize)]
357#[serde(deny_unknown_fields)]
358struct PageEnvelope {
359 items: Vec<Map<String, Value>>,
360 total: u64,
361 page: u64,
362 #[serde(rename = "perPage")]
363 per_page: u64,
364}
365
366#[derive(Deserialize)]
367#[serde(deny_unknown_fields)]
368struct ItemEnvelope {
369 item: Map<String, Value>,
370}
371
372#[derive(Deserialize)]
373#[serde(deny_unknown_fields)]
374struct DeleteEnvelope {
375 id: String,
376}