manta_server/service/runtime_configuration.rs
1//! Business logic for `PUT /v2/runtime-configuration`.
2//!
3//! Sets a CFS configuration as the `desired_configuration` on every
4//! CFS component matching a hosts expression, and writes the
5//! component's `enabled` flag in the same call. This is a narrow
6//! subset of [`super::boot_parameters::persist_boot_config`] — the
7//! latter also touches BSS boot parameters; this one only writes CFS
8//! component state.
9
10use manta_backend_dispatcher::{error::Error, interfaces::cfs::CfsTrait as _};
11
12use crate::server::common::app_context::InfraContext;
13
14use super::{authorization::validate_user_group_members_access, node_ops};
15
16/// Assign `cfs_configuration_name` as the desired runtime config on
17/// every node resolved from `hosts_expression`, setting each
18/// component's `enabled` flag to `enabled`.
19///
20/// Returns the sorted, deduped xname list that was written (or would
21/// have been written, in dry-run mode).
22///
23/// Ordering:
24/// 1. Cheap validation of non-empty inputs.
25/// 2. Resolve `hosts_expression` to xnames (backend call).
26/// 3. Enforce access — the caller must be able to reach every xname
27/// (backend call).
28/// 4. Verify the CFS configuration exists (backend call). Runs *after*
29/// the access check so unauthorized callers cannot probe config
30/// existence by watching this endpoint's status codes.
31/// 5. Patch each CFS component's `desired_configuration` and
32/// `enabled` — skipped when `dry_run` is true.
33///
34/// # Errors
35///
36/// - [`Error::BadRequest`] — empty inputs, or `hosts_expression`
37/// resolves to zero nodes.
38/// - [`Error::InvalidPattern`] / [`Error::InvalidNodeId`] — malformed
39/// `hosts_expression` (surfaced by
40/// [`node_ops::from_user_hosts_expression_to_xname_vec`]).
41/// - [`Error::Unauthorized`] — caller cannot reach one or more xnames.
42/// - [`Error::NotFound`] — CFS configuration name does not exist.
43/// - Backend errors from the final CFS component PATCH.
44pub(crate) async fn apply_runtime_configuration(
45 infra: &InfraContext<'_>,
46 token: &str,
47 cfs_configuration_name: &str,
48 hosts_expression: &str,
49 enabled: bool,
50 dry_run: bool,
51) -> Result<Vec<String>, Error> {
52 if cfs_configuration_name.trim().is_empty() {
53 return Err(Error::BadRequest(
54 "cfs_configuration_name must not be empty".into(),
55 ));
56 }
57 if hosts_expression.trim().is_empty() {
58 return Err(Error::BadRequest(
59 "hosts_expression must not be empty".into(),
60 ));
61 }
62
63 let xnames = node_ops::from_user_hosts_expression_to_xname_vec(
64 infra,
65 token,
66 hosts_expression,
67 false,
68 )
69 .await?;
70
71 if xnames.is_empty() {
72 return Err(Error::BadRequest(format!(
73 "hosts_expression '{hosts_expression}' resolved to zero nodes"
74 )));
75 }
76
77 validate_user_group_members_access(infra, token, &xnames).await?;
78
79 let configuration_name_owned = cfs_configuration_name.to_string();
80 let configs = infra
81 .backend
82 .get_configuration(token, Some(&configuration_name_owned))
83 .await?;
84 if configs.is_empty() {
85 return Err(Error::NotFound(format!(
86 "CFS configuration '{cfs_configuration_name}'"
87 )));
88 }
89
90 if dry_run {
91 tracing::info!(
92 "dry_run: skipping CFS component PATCH for {} nodes",
93 xnames.len()
94 );
95 return Ok(xnames);
96 }
97
98 infra
99 .backend
100 .update_runtime_configuration(
101 token,
102 &xnames,
103 cfs_configuration_name,
104 enabled,
105 )
106 .await?;
107
108 Ok(xnames)
109}