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