Skip to main content

elasticctl_api/fleet/integration_policy_ops/
import_shared.rs

1use super::common::*;
2use super::read::collect;
3
4const IMPORT_RACE_WARNING: &str =
5    "warning  Fleet can change after the final recheck and before the write";
6
7pub struct IntegrationPolicyImportArtifact {
8    pub(super) source: PathBuf,
9    pub(super) canonical: Vec<IntegrationPolicySpec>,
10}
11
12impl fmt::Debug for IntegrationPolicyImportArtifact {
13    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
14        formatter
15            .debug_struct("IntegrationPolicyImportArtifact")
16            .field("policy_count", &self.canonical.len())
17            .finish()
18    }
19}
20
21#[derive(Clone, PartialEq)]
22pub struct IntegrationPolicyImportPlan {
23    pub preview: MutationPlan,
24    pub skipped: Vec<Value>,
25    pub package_installs: Vec<String>,
26    pub total: usize,
27    pub(super) source: PathBuf,
28    pub(super) host: String,
29    pub(super) space: String,
30    pub(super) canonical: Vec<IntegrationPolicySpec>,
31    pub(super) name_owners: BTreeMap<String, BTreeSet<String>>,
32    pub(super) name_owners_snapshot: BTreeMap<String, BTreeSet<String>>,
33    pub(super) parent_snapshots: BTreeMap<String, agent_policy_ops::AgentPolicyParentSnapshot>,
34    pub(super) skipped_snapshot: Vec<Value>,
35    pub(super) existing_snapshot: BTreeMap<String, Option<Map<String, Value>>>,
36    pub(super) targets: Vec<IntegrationPolicyImportTarget>,
37    pub(super) package_groups: BTreeMap<String, IntegrationPackageGroup>,
38    pub(super) overwrite: bool,
39    pub(super) skip_existing: bool,
40}
41
42impl fmt::Debug for IntegrationPolicyImportPlan {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        formatter
45            .debug_struct("IntegrationPolicyImportPlan")
46            .field("target_count", &self.preview.targets.len())
47            .field("skipped_count", &self.skipped.len())
48            .field("package_install_count", &self.package_installs.len())
49            .field("existing_snapshot_count", &self.existing_snapshot.len())
50            .field("total", &self.total)
51            .finish()
52    }
53}
54
55#[derive(Debug, Clone, PartialEq)]
56pub(super) struct IntegrationPolicyImportTarget {
57    pub(super) effective: IntegrationPolicySpec,
58    pub(super) current: Option<IntegrationPolicyCurrentSnapshot>,
59    pub(super) parents: BTreeMap<String, agent_policy_ops::AgentPolicyParentSnapshot>,
60    pub(super) replacement_body: Option<Value>,
61}
62
63#[derive(Debug, Clone, PartialEq)]
64pub(super) struct IntegrationPolicyCurrentSnapshot {
65    pub(super) item: Map<String, Value>,
66    pub(super) spec: IntegrationPolicySpec,
67    pub(super) parent_ids: Vec<String>,
68}
69
70#[derive(Debug, Clone, PartialEq)]
71pub(super) struct IntegrationPackageGroup {
72    pub(super) package: IntegrationPackageSpec,
73    pub(super) state: PackageDependencySnapshot,
74    pub(super) state_snapshot: PackageDependencySnapshot,
75    pub(super) metadata: Map<String, Value>,
76    pub(super) metadata_snapshot: Map<String, Value>,
77}
78
79pub(super) fn validate_requested_package_versions(specs: &[IntegrationPolicySpec]) -> Result<()> {
80    let mut versions = BTreeMap::new();
81    for spec in specs {
82        match versions.entry(spec.package.name.as_str()) {
83            std::collections::btree_map::Entry::Vacant(entry) => {
84                entry.insert(spec.package.version.as_str());
85            }
86            std::collections::btree_map::Entry::Occupied(entry)
87                if entry.get() != &spec.package.version.as_str() =>
88            {
89                return Err(Error::new(
90                    ErrorKind::Conflict,
91                    format!(
92                        "integration package '{}' is requested at more than one version",
93                        spec.package.name
94                    ),
95                ));
96            }
97            std::collections::btree_map::Entry::Occupied(_) => {}
98        }
99    }
100    Ok(())
101}
102
103pub(super) async fn relevant_name_owners(
104    transport: &Transport,
105    names: &BTreeSet<String>,
106) -> Result<BTreeMap<String, BTreeSet<String>>> {
107    let mut owners = names
108        .iter()
109        .map(|name| (name.clone(), BTreeSet::new()))
110        .collect::<BTreeMap<_, _>>();
111    if names.is_empty() {
112        return Ok(owners);
113    }
114    for item in collect(transport).await? {
115        let Some(name) = item.get("name").and_then(Value::as_str) else {
116            continue;
117        };
118        let Some(owners) = owners.get_mut(name) else {
119            continue;
120        };
121        owners.insert(required_string(&item, "id", "integration policies list")?);
122    }
123    Ok(owners)
124}
125
126pub(super) fn target_name_owners_match(
127    target: &IntegrationPolicyImportTarget,
128    owners: &BTreeMap<String, BTreeSet<String>>,
129) -> bool {
130    let Some(actual) = owners.get(&target.effective.name) else {
131        return false;
132    };
133    let expected = match &target.current {
134        None => BTreeSet::new(),
135        Some(current) if current.spec.name == target.effective.name => {
136            BTreeSet::from([target.effective.id.clone()])
137        }
138        Some(_) => BTreeSet::new(),
139    };
140    actual == &expected
141}
142
143pub(super) async fn read_import_parent_snapshots(
144    transport: &Transport,
145    integration_id: &str,
146    current_parent_ids: &[String],
147    desired_parent_ids: &[String],
148) -> Result<BTreeMap<String, agent_policy_ops::AgentPolicyParentSnapshot>> {
149    let parent_ids = current_parent_ids
150        .iter()
151        .chain(desired_parent_ids)
152        .cloned()
153        .collect::<BTreeSet<_>>();
154    let mut parents = BTreeMap::new();
155    for parent_id in parent_ids {
156        let parent = agent_policy_ops::read_parent_snapshot(transport, &parent_id).await?;
157        if current_parent_ids.binary_search(&parent_id).is_ok()
158            && parent
159                .attached_integrations
160                .binary_search_by(|attached| attached.as_str().cmp(integration_id))
161                .is_err()
162        {
163            return Err(http(format!(
164                "decoding integration policy '{integration_id}': parent '{parent_id}' is missing its attachment"
165            )));
166        }
167        parents.insert(parent_id, parent);
168    }
169    Ok(parents)
170}
171
172pub(super) fn effective_import_spec(
173    canonical: &IntegrationPolicySpec,
174    parents: &BTreeMap<String, agent_policy_ops::AgentPolicyParentSnapshot>,
175) -> Result<IntegrationPolicySpec> {
176    canonical.validate()?;
177    for parent in parents.values() {
178        if parent.platform_owned {
179            return unsupported(format!(
180                "integration policy '{}' is not portable: parent {} is platform-owned",
181                canonical.id, parent.id
182            ));
183        }
184        if parent.protected {
185            return unsupported(format!(
186                "integration policy '{}' is not portable: parent {} is_protected",
187                canonical.id, parent.id
188            ));
189        }
190    }
191    let selected = canonical
192        .policy_ids
193        .iter()
194        .map(|id| {
195            parents.get(id).ok_or_else(|| {
196                Error::new(
197                    ErrorKind::Error,
198                    format!(
199                        "integration policy '{}' has no parent snapshot for '{id}'",
200                        canonical.id
201                    ),
202                )
203            })
204        })
205        .collect::<Result<Vec<_>>>()?;
206    let mut effective = canonical.clone();
207    if let Some(namespace) = &effective.namespace {
208        if selected.iter().any(|parent| &parent.namespace != namespace) {
209            return unsupported(format!(
210                "integration policy '{}' is not portable: namespace does not match every parent",
211                canonical.id
212            ));
213        }
214    } else {
215        let namespaces = selected
216            .iter()
217            .map(|parent| parent.namespace.as_str())
218            .collect::<BTreeSet<_>>();
219        if namespaces.len() != 1 {
220            return Err(Error::new(
221                ErrorKind::Conflict,
222                format!(
223                    "integration policy '{}' is not portable: parents have different namespaces",
224                    canonical.id
225                ),
226            ));
227        }
228        effective.namespace = namespaces.into_iter().next().map(str::to_owned);
229    }
230    Ok(effective)
231}
232
233pub(super) fn replace_wire_body(spec: &IntegrationPolicySpec) -> Result<Value> {
234    spec.validate()?;
235    let mut body = serde_json::to_value(spec)
236        .map_err(|error| {
237            Error::new(
238                ErrorKind::Error,
239                format!("encoding integration policy: {error}"),
240            )
241        })?
242        .as_object()
243        .cloned()
244        .expect("integration policy specs serialize to objects");
245    body.remove("id");
246    Ok(Value::Object(body))
247}
248
249pub(super) fn planned_package_installs(
250    groups: &BTreeMap<String, IntegrationPackageGroup>,
251) -> Vec<String> {
252    groups
253        .values()
254        .filter_map(|group| match group.state.state {
255            PackageDependencyState::NotInstalled => {
256                Some(format!("{}@{}", group.package.name, group.package.version))
257            }
258            PackageDependencyState::Installed { .. } => None,
259        })
260        .collect()
261}
262
263pub(super) fn import_preview(
264    path: &Path,
265    targets: &[IntegrationPolicyImportTarget],
266    package_installs: &[String],
267) -> MutationPlan {
268    let mut details = targets
269        .iter()
270        .map(|target| {
271            let parents = target
272                .parents
273                .values()
274                .map(|parent| format!("{} ({})", parent.id, parent.name))
275                .collect::<Vec<_>>()
276                .join(", ");
277            let agents = target
278                .parents
279                .values()
280                .map(|parent| parent.agents)
281                .sum::<u64>();
282            let action = match &target.current {
283                None => "create".to_owned(),
284                Some(current) if current.spec == target.effective => "unchanged".to_owned(),
285                Some(current) if current.spec.name == target.effective.name => "replace".to_owned(),
286                Some(current) => format!(
287                    "replace  {} -> {}",
288                    current.spec.name, target.effective.name
289                ),
290            };
291            format!(
292                "{}  {action}  {}  parents {parents}  agents {agents}",
293                target.effective.id, target.effective.name
294            )
295        })
296        .collect::<Vec<_>>();
297    details.extend(
298        package_installs
299            .iter()
300            .map(|package| format!("package install  {package}")),
301    );
302    details.push(IMPORT_RACE_WARNING.to_owned());
303    MutationPlan {
304        preview_action: format!(
305            "Import {} integration policy(ies) from {}",
306            targets.len(),
307            path.display()
308        ),
309        preview_details: details,
310        targets: targets
311            .iter()
312            .map(|target| target.effective.id.clone())
313            .collect(),
314    }
315}
316
317pub(super) fn import_remote_error(error: Error, context: &str) -> Error {
318    let message = format!("integration-policy import {context} failed");
319    match error.http_status {
320        Some(status) => Error::with_status(error.kind, status, message),
321        None => Error::new(error.kind, message),
322    }
323}