Skip to main content

elasticctl_api/fleet/agent_policy_ops/
artifact.rs

1//! Agent policy artifact validation and export.
2
3use super::{
4    AgentPolicySpec, AgentPolicySummary, BTreeMap, BTreeSet, ContentFormat, Error, ErrorKind,
5    ExportOutcome, Path, Result, Transport, collect, content_codec, is_platform_owned,
6    live_from_item, read_live, resolve_item,
7};
8
9/// Read, decode, validate, and sort a portable artifact.
10pub fn validate(path: &Path) -> Result<Vec<AgentPolicySpec>> {
11    let body = std::fs::read_to_string(path).map_err(|error| {
12        Error::new(
13            ErrorKind::Error,
14            format!("reading {}: {error}", path.display()),
15        )
16    })?;
17    let mut specs = content_codec::decode_sequence::<AgentPolicySpec>(
18        &body,
19        ContentFormat::from_path(path),
20        "agent policy",
21    )?;
22    let mut seen_ids = BTreeSet::new();
23    let mut duplicate_ids = BTreeSet::new();
24    let mut seen_names = BTreeSet::new();
25    let mut duplicate_names = BTreeSet::new();
26    for spec in &specs {
27        if !seen_ids.insert(spec.id.as_str()) {
28            duplicate_ids.insert(spec.id.as_str());
29        }
30        if !seen_names.insert(spec.name.as_str()) {
31            duplicate_names.insert(spec.name.as_str());
32        }
33    }
34    if !duplicate_ids.is_empty() {
35        return Err(Error::new(
36            ErrorKind::Error,
37            format!(
38                "duplicate agent policy ids: {}",
39                duplicate_ids.into_iter().collect::<Vec<_>>().join(", ")
40            ),
41        ));
42    }
43    if !duplicate_names.is_empty() {
44        return Err(Error::new(
45            ErrorKind::Error,
46            format!(
47                "duplicate agent policy names: {}",
48                duplicate_names.into_iter().collect::<Vec<_>>().join(", ")
49            ),
50        ));
51    }
52    specs.sort_by(|left, right| left.id.cmp(&right.id));
53    Ok(specs)
54}
55
56/// Export selected policies, or every custom policy, as a portable artifact.
57pub async fn export(
58    transport: &Transport,
59    selectors: &[String],
60    all_custom: bool,
61    format: ContentFormat,
62) -> Result<ExportOutcome> {
63    if selectors.is_empty() && !all_custom {
64        return Err(Error::new(
65            ErrorKind::Error,
66            "agent-policy export needs selectors or --all-custom",
67        ));
68    }
69    if !selectors.is_empty() && all_custom {
70        return Err(Error::new(
71            ErrorKind::Error,
72            "--all-custom cannot be combined with selectors",
73        ));
74    }
75    let mut resolved_items = BTreeMap::new();
76    let ids: BTreeSet<String> = if all_custom {
77        let mut ids = BTreeSet::new();
78        for item in collect(transport).await? {
79            if !is_platform_owned(&item)? {
80                ids.insert(AgentPolicySummary::from_item(&item)?.id);
81            }
82        }
83        ids
84    } else {
85        let mut ids = BTreeSet::new();
86        for selector in selectors {
87            let resolved = resolve_item(transport, selector).await?;
88            ids.insert(resolved.summary.id.clone());
89            resolved_items.insert(resolved.summary.id, resolved.item);
90        }
91        ids
92    };
93    let mut specs = Vec::with_capacity(ids.len());
94    for id in &ids {
95        let live = match resolved_items.get(id) {
96            Some(item) => live_from_item(item, id, transport.space())?,
97            None => read_live(transport, id).await?,
98        };
99        specs.push(live.spec);
100    }
101    specs.sort_by(|left, right| left.id.cmp(&right.id));
102    let body = content_codec::encode_sequence(&specs, format)?;
103    Ok(ExportOutcome {
104        body,
105        exported: specs.len() as u64,
106        missing: Vec::new(),
107    })
108}