1use super::common::*;
2use super::import_shared::{
3 IntegrationPolicyImportPlan, IntegrationPolicyImportTarget, import_remote_error,
4 relevant_name_owners,
5};
6use super::import_validation::validate_import_plan;
7use super::normalization::normalize;
8
9#[derive(Debug, Clone, PartialEq, Serialize)]
10pub struct IntegrationPolicyImportReport {
11 pub applied: bool,
12 pub succeeded: Vec<Value>,
13 pub unchanged: Vec<Value>,
14 pub skipped: Vec<Value>,
15 pub failed: Vec<Value>,
16 pub total: usize,
17 pub affected_agents: u64,
18 pub package_installs: Vec<String>,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22enum ImportAction {
23 Create,
24 Replace,
25 Unchanged,
26}
27
28pub async fn apply_import(
31 transport: &Transport,
32 plan: &IntegrationPolicyImportPlan,
33) -> Result<IntegrationPolicyImportReport> {
34 validate_import_plan(plan)?;
35 if plan.host != transport.kibana_url() || plan.space != transport.space() {
36 return Err(Error::new(
37 ErrorKind::Conflict,
38 "integration import target changed since preview",
39 ));
40 }
41 let mut succeeded = Vec::new();
42 let mut unchanged = Vec::new();
43 let mut failed = Vec::new();
44 let mut expected_groups = plan.package_groups.clone();
45 let mut expected_parents = plan.parent_snapshots.clone();
46 let mut blocked_packages = BTreeMap::<String, String>::new();
47 let mut affected_parents = BTreeMap::<String, u64>::new();
48 let mut observed_installs = BTreeSet::new();
49
50 for target in &plan.targets {
51 let package_name = &target.effective.package.name;
52 if let Some(error) = blocked_packages.get(package_name) {
53 failed.push(import_failed_row(
54 &target.effective.id,
55 false,
56 format!("package dependency is unavailable: {error}"),
57 ));
58 continue;
59 }
60
61 let action = match recheck_import_object(transport, target).await {
62 Ok(action) => action,
63 Err(error) => {
64 failed.push(import_failed_row(
65 &target.effective.id,
66 false,
67 error.message,
68 ));
69 continue;
70 }
71 };
72 if let Err(error) = recheck_import_name_owner(transport, target, &plan.name_owners).await {
73 failed.push(import_failed_row(
74 &target.effective.id,
75 false,
76 error.message,
77 ));
78 continue;
79 }
80 if let Err(error) = recheck_import_parents(transport, target, &expected_parents).await {
81 failed.push(import_failed_row(
82 &target.effective.id,
83 false,
84 error.message,
85 ));
86 continue;
87 }
88
89 let group = expected_groups
90 .get_mut(package_name)
91 .expect("validated target package group");
92 let actual_state = match read_dependencies(transport, &target.effective.package).await {
93 Ok(state) if state == group.state => state,
94 Ok(_) => {
95 let message = "package changed since preview".to_owned();
96 blocked_packages.insert(package_name.clone(), message.clone());
97 failed.push(import_failed_row(&target.effective.id, false, message));
98 continue;
99 }
100 Err(error) => {
101 let message = import_remote_error(error, "apply package read").message;
102 blocked_packages.insert(package_name.clone(), message.clone());
103 failed.push(import_failed_row(&target.effective.id, false, message));
104 continue;
105 }
106 };
107 debug_assert_eq!(actual_state, group.state);
108
109 if action == ImportAction::Unchanged {
110 unchanged.push(json!({"id": target.effective.id}));
111 continue;
112 }
113
114 let (label, applied, route_error) = match action {
115 ImportAction::Create => {
116 match integration_policies::create(transport, &target.effective).await {
117 Ok(_) => ("created", true, None),
118 Err(error) => (
119 "created",
120 false,
121 Some(import_remote_error(error, "create request").message),
122 ),
123 }
124 }
125 ImportAction::Replace => {
126 let _body = target
127 .replacement_body
128 .as_ref()
129 .expect("validated replacement body");
130 match integration_policies::update(
131 transport,
132 &target.effective.id,
133 &target.effective,
134 )
135 .await
136 {
137 Ok(_) => ("replaced", true, None),
138 Err(error) => (
139 "replaced",
140 false,
141 Some(import_remote_error(error, "update request").message),
142 ),
143 }
144 }
145 ImportAction::Unchanged => unreachable!("unchanged rows continue above"),
146 };
147
148 if applied {
149 record_affected_parents(&mut affected_parents, target);
150 advance_parent_snapshots(&mut expected_parents, target);
151 }
152
153 let mut observed_after_create = None;
157 let mut package_observation_error = None;
158 if action == ImportAction::Create
159 && matches!(group.state.state, PackageDependencyState::NotInstalled)
160 {
161 match read_dependencies(transport, &target.effective.package).await {
162 Ok(after) => {
163 if is_exact_installed(&after, &target.effective.package) {
164 group.state = after.clone();
165 observed_installs.insert(format!(
166 "{}@{}",
167 target.effective.package.name, target.effective.package.version
168 ));
169 } else if !matches!(after.state, PackageDependencyState::NotInstalled) {
170 let message =
171 "package installed a different version after create".to_owned();
172 blocked_packages.insert(package_name.clone(), message.clone());
173 package_observation_error = Some(message);
174 }
175 observed_after_create = Some(after);
176 }
177 Err(error) => {
178 let message = import_remote_error(error, "post-create package read").message;
179 blocked_packages.insert(package_name.clone(), message.clone());
180 package_observation_error = Some(message);
181 }
182 }
183 }
184
185 let mut errors = route_error.into_iter().collect::<Vec<_>>();
186 if applied {
187 if let Err(error) = verify_import_stored(transport, &target.effective).await {
188 errors.push(error.message);
189 }
190 let package_result = match observed_after_create.as_ref() {
191 Some(after) => verify_exact_installed(after, &target.effective.package),
192 None => match read_dependencies(transport, &target.effective.package).await {
193 Ok(after) => verify_exact_installed(&after, &target.effective.package),
194 Err(error) => Err(import_remote_error(error, "package verification").message),
195 },
196 };
197 if let Err(message) = package_result {
198 blocked_packages
199 .entry(package_name.clone())
200 .or_insert_with(|| message.clone());
201 errors.push(message);
202 }
203 }
204 if let Some(error) = package_observation_error
205 && !errors.contains(&error)
206 {
207 errors.push(error);
208 }
209
210 if errors.is_empty() {
211 succeeded.push(json!({"id": target.effective.id, "action": label}));
212 } else {
213 failed.push(import_failed_row(
214 &target.effective.id,
215 applied,
216 errors.join("; "),
217 ));
218 }
219 }
220
221 Ok(IntegrationPolicyImportReport {
222 applied: true,
223 succeeded,
224 unchanged,
225 skipped: plan.skipped.clone(),
226 failed,
227 total: plan.total,
228 affected_agents: affected_parents.values().sum(),
229 package_installs: observed_installs.into_iter().collect(),
230 })
231}
232
233async fn recheck_import_object(
234 transport: &Transport,
235 target: &IntegrationPolicyImportTarget,
236) -> Result<ImportAction> {
237 match &target.current {
238 None => match integration_policies::get(transport, &target.effective.id).await {
239 Err(error) if error.kind == ErrorKind::NotFound => Ok(ImportAction::Create),
240 Ok(_) => Err(Error::new(
241 ErrorKind::Conflict,
242 "integration policy appeared since preview",
243 )),
244 Err(error) => Err(import_remote_error(error, "apply integration-policy read")),
245 },
246 Some(expected) => match integration_policies::get(transport, &target.effective.id).await {
247 Ok(actual) if actual.item == expected.item => {
248 if expected.spec == target.effective {
249 Ok(ImportAction::Unchanged)
250 } else {
251 Ok(ImportAction::Replace)
252 }
253 }
254 Ok(_) => Err(Error::new(
255 ErrorKind::Conflict,
256 "integration policy changed since preview",
257 )),
258 Err(error) if error.kind == ErrorKind::NotFound => Err(Error::new(
259 ErrorKind::Conflict,
260 "integration policy disappeared since preview",
261 )),
262 Err(error) => Err(import_remote_error(error, "apply integration-policy read")),
263 },
264 }
265}
266
267async fn recheck_import_name_owner(
268 transport: &Transport,
269 target: &IntegrationPolicyImportTarget,
270 expected_owners: &BTreeMap<String, BTreeSet<String>>,
271) -> Result<()> {
272 let names = BTreeSet::from([target.effective.name.clone()]);
273 let owners = relevant_name_owners(transport, &names)
274 .await
275 .map_err(|error| import_remote_error(error, "apply name read"))?;
276 if owners.get(&target.effective.name) == expected_owners.get(&target.effective.name) {
277 Ok(())
278 } else {
279 Err(Error::new(
280 ErrorKind::Conflict,
281 "integration policy name ownership changed since preview",
282 ))
283 }
284}
285
286async fn recheck_import_parents(
287 transport: &Transport,
288 target: &IntegrationPolicyImportTarget,
289 expected_parents: &BTreeMap<String, agent_policy_ops::AgentPolicyParentSnapshot>,
290) -> Result<()> {
291 for parent_id in target.parents.keys() {
292 let expected = expected_parents.get(parent_id).ok_or_else(|| {
293 Error::new(
294 ErrorKind::Error,
295 "integration import lost a shared parent snapshot",
296 )
297 })?;
298 let actual = agent_policy_ops::read_parent_snapshot(transport, parent_id)
299 .await
300 .map_err(|error| import_remote_error(error, "apply parent read"))?;
301 if actual != *expected {
302 return Err(Error::new(
303 ErrorKind::Conflict,
304 "integration policy parent changed since preview",
305 ));
306 }
307 }
308 Ok(())
309}
310
311fn record_affected_parents(
312 affected: &mut BTreeMap<String, u64>,
313 target: &IntegrationPolicyImportTarget,
314) {
315 for parent in target.parents.values() {
316 affected.entry(parent.id.clone()).or_insert(parent.agents);
317 }
318}
319
320fn advance_parent_snapshots(
321 parents: &mut BTreeMap<String, agent_policy_ops::AgentPolicyParentSnapshot>,
322 target: &IntegrationPolicyImportTarget,
323) {
324 let desired = target
325 .effective
326 .policy_ids
327 .iter()
328 .map(String::as_str)
329 .collect::<BTreeSet<_>>();
330 for parent_id in target.parents.keys() {
331 let parent = parents
332 .get_mut(parent_id)
333 .expect("validated shared parent snapshot");
334 if desired.contains(parent_id.as_str()) {
335 if parent
336 .attached_integrations
337 .binary_search_by(|attached| attached.as_str().cmp(&target.effective.id))
338 .is_err()
339 {
340 parent
341 .attached_integrations
342 .push(target.effective.id.clone());
343 parent.attached_integrations.sort();
344 }
345 } else {
346 parent
347 .attached_integrations
348 .retain(|attached| attached != &target.effective.id);
349 }
350 }
351}
352
353async fn verify_import_stored(
354 transport: &Transport,
355 desired: &IntegrationPolicySpec,
356) -> Result<()> {
357 let stored = integration_policies::get(transport, &desired.id)
358 .await
359 .map_err(|error| import_remote_error(error, "stored-policy read"))?;
360 let stored = normalize(&stored.item, transport.space())
361 .map_err(|error| import_remote_error(error, "stored-policy read"))?;
362 if stored == *desired {
363 Ok(())
364 } else {
365 Err(Error::new(
366 ErrorKind::Http,
367 "server stored a different integration-policy spec",
368 ))
369 }
370}
371
372fn import_failed_row(id: &str, applied: bool, error: impl Into<String>) -> Value {
373 json!({"id": id, "applied": applied, "error": error.into()})
374}