Repository: bottlerocket-os/bottlerocket
File: sources/api/apiserver/src/server/mod.rs (Rust)

Code before:
/// error, and can print a clear error and request the user use JSON input form to handle
/// situations with more complex types.g
///
/// If you have an idea for how to improve deserialization so we don't have to do this, please say!
fn massage_set_input(input_map: HashMap<Key, String>) -> Result<HashMap<Key, String>> {
    // Deserialize the given value into the matching Rust type.  When we find a matching type, we
    // serialize back out to the data store format, which is required to build a Settings object
    // through the data store deserialization code.
    let mut massaged_map = HashMap::with_capacity(input_map.len());
    for (key, in_val) in input_map {
        let serialized = if let Ok(b) = serde_json::from_str::<bool>(&in_val) {
            serialize_scalar(&b).context(error::SerializeSnafu)?
        } else if let Ok(u) = serde_json::from_str::<u64>(&in_val) {
            trace!("Serializing scalar of type u64");
            serialize_scalar(&u).context(error::SerializeSnafu)?
        } else if let Ok(f) = serde_json::from_str::<f64>(&in_val) {
            trace!("Serializing scalar of type f64");
            serialize_scalar(&f).context(error::SerializeSnafu)?
        } else {
            trace!("Serializing scalar of type string");
            // No deserialization, already a string, just serialize
            serialize_scalar(&in_val).context(error::SerializeSnafu)?
        };
        massaged_map.insert(key, serialized);
    }
    trace!("Massaged key=value input: {:#?}", massaged_map);
    Ok(massaged_map)
}

// Router

/// This is the primary interface of the module.  It defines the server and application that actix
/// spawns for requests.  It creates a shared datastore handle that can be used by handler methods
/// to interface with the controller.
pub async fn serve<P1, P2, P3>(
    socket_path: P1,
    datastore_path: P2,
    threads: usize,
    socket_gid: Option<Gid>,
    exec_socket_path: P3,
) -> Result<()>
where
    P1: AsRef<Path>,
    P2: AsRef<Path>,
    P3: Into<PathBuf>,
{
    // SharedData gives us a convenient way to make data available to handler methods when it
    // doesn't come from the request itself.  It's easier than the ownership tricks required to
    // pass parameters to the handler methods.
    let shared_data = web::Data::new(SharedData {
        ds: sync::RwLock::new(FilesystemDataStore::new(datastore_path)),

Review comment (suggestion):
Just checking - if we had a model with something like `settings.keypair.foo`, we wouldn't want this:
```
GET /settings/motd # returns value of the "motd" setting
GET /settings/keypair # returns value of the "keypair" setting
PATCH /settings/keypair # server-side route, special cased
PATCH /settings/motd # error
```

From a quick glance at the code, we don't support using the URI path to select a particular subset of settings.

If we did, it might be cleaner to have a top-level `settings-keypair` route instead to avoid ambiguity.

Code after:
) -> Result<()>
where
    P1: AsRef<Path>,
    P2: AsRef<Path>,
    P3: Into<PathBuf>,
{
    // SharedData gives us a convenient way to make data available to handler methods when it
    // doesn't come from the request itself.  It's easier than the ownership tricks required to
    // pass parameters to the handler methods.
    let shared_data = web::Data::new(SharedData {
        ds: sync::RwLock::new(FilesystemDataStore::new(datastore_path)),
        exec_socket_path: exec_socket_path.into(),
    });

    let http_server = HttpServer::new(move || {
        App::new()
            // This makes the data store available to API methods merely by having a Data
            // parameter.
            .app_data(shared_data.clone())
            // Retrieve the full API model; not all data is writable, so we only support GET.
            .route("/", web::get().to(get_model))
            .service(
                web::scope("/settings")
                    .route("", web::get().to(get_settings))
                    .route("", web::patch().to(patch_settings))
                    .route("/keypair", web::patch().to(patch_settings_key_pair)),
            )
            .service(
                // Transaction support
                web::scope("/tx")
                    .route("/list", web::get().to(get_transaction_list))
                    .route("", web::get().to(get_transaction))
                    .route("", web::delete().to(delete_transaction))
                    .route("/commit", web::post().to(commit_transaction))
                    .route("/apply", web::post().to(apply_changes))
                    .route(
                        "/commit_and_apply",
                        web::post().to(commit_transaction_and_apply),
                    ),
            )
            .service(web::scope("/os").route("", web::get().to(get_os_info)))
            .service(
                web::scope("/metadata")
                    .route("/affected-services", web::get().to(get_affected_services))
                    .route("/setting-generators", web::get().to(get_setting_generators))
                    .route("/templates", web::get().to(get_templates)),
            )
            .service(web::scope("/services").route("", web::get().to(get_services)))
            .service(
                web::scope("/configuration-files")
                    .route("", web::get().to(get_configuration_files)),