rcs_backend 0.2.1

otafablab rust code server backend
Documentation
use crate::config::Config;
use crate::utils::wait_for_http_service;
use docker_api::api::ContainerCreateOpts;
use docker_api::api::ContainerDetails;
use docker_api::volume::VolumeCreateOpts;
use docker_api::Docker;
use http::uri::Authority;
use unidecode::unidecode;

/// Transliterates a string into a restricted ASCII-only character
/// set. Heavy lifting done in [`unidecode::unidecode`].
pub fn transliterate(s: impl AsRef<str>) -> String {
    unidecode(s.as_ref())
        .chars()
        .map(|c| match c {
            '@' => '_',
            c => c,
        })
        .filter(|&c| c.is_alphanumeric() || "_.-".contains(c))
        .collect()
}

fn get_host_port(details: &ContainerDetails) -> String {
    details
        .network_settings
        .ports
        .clone() // TODO fix
        .unwrap()
        .get("3000/tcp")
        .unwrap()
        .as_ref()
        .unwrap()[0]
        .host_port
        .clone() // TODO fix
}

/// Idempotently starts the rust-code-server instance for the use
/// identified by their email. Returns the an [`Authority`] specifying
/// the host and port of the server instance.
///
/// On first run:
///
/// - a volume named by the `email`
/// ([`transliterated`](transliterate)) is created
///
/// - then a rust-code-server container is started which mounts the
/// volume into `/home/workspace`. The container publishes a random
/// port for connecting to the vscode (returned in the
/// `Authority`). The container's name the same as the volume's.
///
/// # Panics
///
/// The function will panic if:
///
/// - the [`Config::socket_path`] is invalid
///
/// - it cannot connect to the socket
///
/// - it cannot create the volume or the container
pub async fn start_rcs(email: &str, config: &Config) -> Authority {
    let email = transliterate(email);
    let docker = Docker::new(&config.socket_path).unwrap();

    let volumes = docker.volumes();
    let volume = volumes.get(&email);

    let volume_opts = VolumeCreateOpts::builder().name(&email).build();
    // assumes all Errs = service not running
    if volume.inspect().await.is_err() {
        volumes.create(&volume_opts).await.unwrap();
        println!("created volume {}", &email);
    }

    let containers = docker.containers();
    let container = containers.get(&email);
    let inspect = container.inspect().await;
    let authority = if let Ok(details) = inspect {
        let port = get_host_port(&details);
        Authority::try_from(format!("127.0.0.1:{}", port)).unwrap()
    } else {
        let opts = ContainerCreateOpts::builder(&config.rcs_image)
            .name(&email)
            .publish_all_ports()
            .volumes([format!("{}:/home/workspace", &email)])
            .build();

        let ctr = containers.create(&opts).await.unwrap();
        println!("created container {}", &email);
        ctr.start().await.unwrap();
        let details = ctr.inspect().await.unwrap();
        let port = get_host_port(&details);
        Authority::try_from(format!("127.0.0.1:{}", port)).unwrap()
    };

    wait_for_http_service(authority.clone()).await;

    authority
}