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 =
60    ShastaClient::new(infra.shasta_base_url, infra.shasta_root_cert.to_vec())
61      .map_err(|e| {
62      Error::BadRequest(format!("Could not build Shasta HTTP client: {e}"))
63    })?;
64
65  let user_public_ssh_id = if let Ok(Some(user_public_ssh_key)) = shasta
66    .ims_public_keys_v3_get_single(token, &user_public_key_name)
67    .await
68  {
69    user_public_ssh_key.id.ok_or_else(|| {
70      Error::MissingField(
71        "IMS public-key response missing server-generated 'id'".to_string(),
72      )
73    })?
74  } else {
75    return Err(Error::NotFound(format!(
76      "User '{user_public_key_name}' does not have an SSH public key in Alps. \
77       Please contact platform sys admins."
78    )));
79  };
80
81  tracing::info!("SSH key found with ID {}", user_public_ssh_id);
82  tracing::info!(
83    "Creating ephemeral environment based on image ID {}",
84    image_id
85  );
86
87  let resp_json = shasta
88    .ims_job_post_customize(
89      token,
90      EPHEMERAL_IMAGE_NAME,
91      image_id,
92      &user_public_ssh_id,
93    )
94    .await
95    .map_err(|e| {
96      Error::BadRequest(format!(
97        "Could not create ephemeral environment based on image ID {image_id}: {e}"
98      ))
99    })?;
100
101  let hostname = resp_json
102    .pointer("/ssh_containers/0/connection_info/customer_access/host")
103    .and_then(|v| v.as_str())
104    .ok_or_else(|| {
105      Error::MissingField(
106        "Failed to get SSH container hostname from ephemeral env response"
107          .to_string(),
108      )
109    })?
110    .to_string();
111
112  tracing::info!("Ephemeral environment created — SSH hostname: {}", hostname);
113
114  Ok(hostname)
115}