soroban-cli 27.1.0

Soroban CLI
Documentation
use std::env;
use std::process::Stdio;

use tokio::io::{AsyncBufReadExt, BufReader};

use crate::{
    commands::{
        container::shared::{Error as ConnectionError, Network},
        global,
    },
    print,
};

use super::shared::{Args, Name, RunArgs};

const DEFAULT_PORT_MAPPING: &str = "8000:8000";
const DOCKER_IMAGE: &str = "docker.io/stellar/quickstart";

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error(transparent)]
    Docker(#[from] ConnectionError),

    #[error("failed to create container: {0}")]
    CreateContainerFailed(String),

    #[error("a container named {0:?} already running")]
    ContainerAlreadyRunning(String),
}

#[derive(Debug, clap::Parser, Clone)]
pub struct Cmd {
    #[command(flatten)]
    pub container_args: Args,

    #[command(flatten)]
    pub run_args: RunArgs,

    /// Network to start. Default is `local`
    pub network: Option<Network>,

    /// Optional argument to specify the container name
    #[arg(long)]
    pub name: Option<String>,

    /// Optional argument to specify the limits for the local network only
    #[arg(short = 'l', long)]
    pub limits: Option<String>,

    /// Argument to specify the `HOST_PORT:CONTAINER_PORT` mapping
    #[arg(short = 'p', long, num_args = 1.., default_value = DEFAULT_PORT_MAPPING)]
    pub ports_mapping: Vec<String>,

    /// Optional argument to override the default docker image tag for the given network
    #[arg(short = 't', long)]
    pub image_tag_override: Option<String>,

    /// Optional argument to specify the protocol version for the local network only
    #[arg(long)]
    pub protocol_version: Option<String>,
}

impl Cmd {
    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
        let runner = Runner {
            args: self.clone(),
            network: self.network.unwrap_or(Network::Local),
            print: print::Print::new(global_args.quiet),
        };

        runner.run_docker_command().await
    }
}

struct Runner {
    args: Cmd,
    network: Network,
    print: print::Print,
}

impl Runner {
    async fn run_docker_command(&self) -> Result<(), Error> {
        self.print
            .infoln(format!("Starting {} network", self.network));

        self.args.container_args.warn_if_host_ignored(&self.print);

        let image = self.get_image_name();
        self.pull_image(&image).await;

        let container_name = self.container_name().get_internal_container_name();
        let mut cmd = self
            .args
            .container_args
            .run_command(&container_name, &self.args.ports_mapping);
        self.args.run_args.apply(&mut cmd);
        cmd.arg(&image);
        // Each element of `get_container_args` is passed as a single argv token (some elements,
        // such as "--enable rpc,horizon,lab", intentionally contain spaces).
        for arg in self.get_container_args() {
            cmd.arg(arg);
        }

        let output = cmd
            .output()
            .await
            .map_err(|e| self.args.container_args.io_error(e))?;
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            if self
                .args
                .container_args
                .is_container_already_running(&stderr)
            {
                return Err(Error::ContainerAlreadyRunning(container_name));
            }
            return Err(Error::CreateContainerFailed(stderr.trim().to_string()));
        }

        self.print.checkln("Started container");
        self.print_instructions();
        Ok(())
    }

    async fn pull_image(&self, image: &str) {
        let mut cmd = self.args.container_args.pull_command(image);
        cmd.stdout(Stdio::piped());

        // `docker pull` writes its progress to stderr; honor `--quiet` by discarding it.
        if self.print.quiet {
            cmd.stderr(Stdio::null());
        }

        let mut child = match cmd.spawn() {
            Ok(child) => child,
            // Let the main `docker run` invocation surface the "is docker installed?" hint
            // rather than warning about a failed image fetch.
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
            Err(_) => return self.warn_image_fetch(image),
        };

        if let Some(stdout) = child.stdout.take() {
            let mut lines = BufReader::new(stdout).lines();
            while let Ok(Some(line)) = lines.next_line().await {
                if line.contains("Pulling from")
                    || line.contains("Digest")
                    || line.contains("Status")
                {
                    self.print.infoln(line);
                }
            }
        }

        match child.wait().await {
            Ok(status) if status.success() => {}
            _ => self.warn_image_fetch(image),
        }
    }

    fn warn_image_fetch(&self, image: &str) {
        self.print
            .warnln(format!("Failed to fetch image: {image}."));
        self.print
            .warnln("Attempting to start local quickstart image. The image may be out-of-date.");
    }

    fn get_image_name(&self) -> String {
        let mut image_tag = match &self.network {
            Network::Futurenet => "future",
            // pubnet, local, and testnet all default to latest
            _ => "latest",
        };

        if let Some(image_override) = &self.args.image_tag_override {
            self.print.infoln(format!(
                "Overriding docker image tag to use '{image_override}' instead of '{image_tag}'"
            ));
            image_tag = image_override;
        }

        format!("{DOCKER_IMAGE}:{image_tag}")
    }

    fn get_container_args(&self) -> Vec<String> {
        let args = env::var("STELLAR_CONTAINER_ARGS").unwrap_or("rpc,horizon,lab".to_string());

        [
            format!("--{}", self.network),
            format!("--enable {args}"),
            self.get_protocol_version_arg(),
            self.get_limits_arg(),
        ]
        .iter()
        .filter(|&s| !s.is_empty())
        .cloned()
        .collect()
    }

    fn container_name(&self) -> Name {
        Name(self.args.name.clone().unwrap_or(self.network.to_string()))
    }

    fn print_instructions(&self) {
        let container_name = self.container_name().get_external_container_name().clone();
        let additional_flags = self.args.container_args.get_additional_flags().clone();
        let tail = format!("{container_name} {additional_flags}");

        self.print.searchln(format!(
            "Watch logs with `stellar container logs {}`",
            tail.trim()
        ));

        self.print.infoln(format!(
            "Stop the container with `stellar container stop {}`",
            tail.trim()
        ));
    }

    fn get_protocol_version_arg(&self) -> String {
        if self.network == Network::Local {
            if let Some(version) = self.args.protocol_version.as_ref() {
                return format!("--protocol-version {version}");
            }
        }

        String::new()
    }

    fn get_limits_arg(&self) -> String {
        if self.network == Network::Local {
            if let Some(limits) = self.args.limits.as_ref() {
                return format!("--limits {limits}");
            }
        }

        String::new()
    }
}