manta_server/service/
ephemeral_env.rs

1//! Ephemeral CFS environment provisioning — launches a temporary IMS
2//! customize container booted from an existing IMS image and returns
3//! its SSH hostname.
4//!
5//! Unlike most service modules this one bypasses the backend
6//! dispatcher and talks to CSM via [`csm_rs::ShastaClient`] directly:
7//! the IMS `jobs.customize` API is CSM-specific and isn't exposed
8//! through `manta-backend-dispatcher`. The handler is wired only on
9//! the CSM site.
10//!
11//! Flow:
12//!
13//! 1. Read the JWT's `preferred_username` claim.
14//! 2. Look the user's public SSH key up in IMS — registered keys are
15//!    keyed by username on the IMS side.
16//! 3. POST an IMS customize job referencing `image_id` and the key id.
17//! 4. Extract the SSH hostname from the response and return it.
18
19use csm_rs::ShastaClient;
20use manta_backend_dispatcher::error::Error;
21
22use crate::server::common::app_context::InfraContext;
23use crate::server::common::jwt_ops;
24use crate::wire_conv;
25
26const EPHEMERAL_IMAGE_NAME: &str = "__ephemeral_image";
27
28/// Launch an ephemeral IMS customize container against `image_id` and
29/// return its SSH hostname.
30///
31/// The caller's preferred username is read from the JWT and used to
32/// look up their registered SSH public key in IMS. If no key is
33/// registered, returns `NotFound` with a message pointing the user
34/// at platform admins. The hostname is plucked from the IMS response
35/// at `/ssh_containers/0/connection_info/customer_access/host`; a
36/// missing field is reported as `MissingField` rather than a generic
37/// error so operators can tell schema drift from real failures.
38///
39/// # Errors
40///
41/// - [`Error::JwtMalformed`] (via `wire_conv::to_backend`) when the
42///   JWT carries no `preferred_username`.
43/// - [`Error::BadRequest`] if the Shasta HTTP client cannot be built
44///   or the IMS customize job submission fails.
45/// - [`Error::NotFound`] when the caller has no SSH public key
46///   registered in IMS.
47/// - [`Error::MissingField`] when the IMS response is missing the
48///   server-generated key id or the SSH container host field.
49pub async fn exec(
50  infra: &InfraContext<'_>,
51  token: &str,
52  image_id: &str,
53) -> Result<String, Error> {
54  let user_public_key_name =
55    jwt_ops::get_preferred_username(token).map_err(wire_conv::to_backend)?;
56
57  tracing::info!("Looking for user '{}' public SSH key", user_public_key_name);
58
59  let shasta = ShastaClient::new(
60    infra.shasta_base_url,
61    infra.shasta_root_cert.to_vec(),
62    infra.socks5_proxy.map(|s| s.to_string()),
63  )
64  .map_err(|e| {
65    Error::BadRequest(format!("Could not build Shasta HTTP client: {e}"))
66  })?;
67
68  let user_public_ssh_id = if let Ok(Some(user_public_ssh_key)) = shasta
69    .ims_public_keys_v3_get_single(token, &user_public_key_name)
70    .await
71  {
72    user_public_ssh_key.id.ok_or_else(|| {
73      Error::MissingField(
74        "IMS public-key response missing server-generated 'id'".to_string(),
75      )
76    })?
77  } else {
78    return Err(Error::NotFound(format!(
79      "User '{user_public_key_name}' does not have an SSH public key in Alps. \
80       Please contact platform sys admins."
81    )));
82  };
83
84  tracing::info!("SSH key found with ID {}", user_public_ssh_id);
85  tracing::info!(
86    "Creating ephemeral environment based on image ID {}",
87    image_id
88  );
89
90  let resp_json = shasta
91    .ims_job_post_customize(
92      token,
93      EPHEMERAL_IMAGE_NAME,
94      image_id,
95      &user_public_ssh_id,
96    )
97    .await
98    .map_err(|e| {
99      Error::BadRequest(format!(
100        "Could not create ephemeral environment based on image ID {image_id}: {e}"
101      ))
102    })?;
103
104  let hostname = resp_json
105    .pointer("/ssh_containers/0/connection_info/customer_access/host")
106    .and_then(|v| v.as_str())
107    .ok_or_else(|| {
108      Error::MissingField(
109        "Failed to get SSH container hostname from ephemeral env response"
110          .to_string(),
111      )
112    })?
113    .to_string();
114
115  tracing::info!("Ephemeral environment created — SSH hostname: {}", hostname);
116
117  Ok(hostname)
118}