Skip to content

Commit

Permalink
feat: start network docker container with cli (#1107)
Browse files Browse the repository at this point in the history
* Add a subcommand to network to start a network docker container

* Add a network stop subcommand
---------

Co-authored-by: Leigh McCulloch <[email protected]>
Co-authored-by: Chad Ostrowski <[email protected]>
Co-authored-by: Willem Wyndham <[email protected]>
  • Loading branch information
4 people authored Mar 4, 2024
1 parent 0ccbab3 commit 5fe1b8d
Show file tree
Hide file tree
Showing 9 changed files with 582 additions and 3 deletions.
79 changes: 79 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions cmd/soroban-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ ureq = { version = "2.9.1", features = ["json"] }
tempfile = "3.8.1"
toml_edit = "0.21.0"
rust-embed = { version = "8.2.0", features = ["debug-embed"] }
bollard = "0.15.0"
futures-util = "0.3.30"
home = "0.5.9"
# For hyper-tls
[target.'cfg(unix)'.dependencies]
openssl = { version = "=0.10.55", features = ["vendored"] }
Expand Down
2 changes: 1 addition & 1 deletion cmd/soroban-cli/src/commands/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ impl Cmd {
pub async fn run(&self) -> Result<(), Error> {
match &self {
Cmd::Identity(identity) => identity.run().await?,
Cmd::Network(network) => network.run()?,
Cmd::Network(network) => network.run().await?,
}
Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/soroban-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl Root {
Cmd::Contract(contract) => contract.run(&self.global_args).await?,
Cmd::Events(events) => events.run().await?,
Cmd::Lab(lab) => lab.run().await?,
Cmd::Network(network) => network.run()?,
Cmd::Network(network) => network.run().await?,
Cmd::Version(version) => version.run(),
Cmd::Keys(id) => id.run().await?,
Cmd::Config(c) => c.run().await?,
Expand Down
24 changes: 23 additions & 1 deletion cmd/soroban-cli/src/commands/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ pub const LOCAL_NETWORK_PASSPHRASE: &str = "Standalone Network ; February 2017";
pub mod add;
pub mod ls;
pub mod rm;
pub mod shared;
pub mod start;
pub mod stop;

#[derive(Debug, Parser)]
pub enum Cmd {
Expand All @@ -26,6 +29,17 @@ pub enum Cmd {
Rm(rm::Cmd),
/// List networks
Ls(ls::Cmd),
/// Start network
///
/// Start a container running a Stellar node, RPC, API, and friendbot (faucet).
///
/// soroban network start <NETWORK> [OPTIONS]
///
/// By default, when starting a testnet container, without any optional arguments, it will run the equivalent of the following docker command:
/// docker run --rm -p 8000:8000 --name stellar stellar/quickstart:testing --testnet --enable-soroban-rpc
Start(start::Cmd),
/// Stop a network started with `network start`. For example, if you ran `soroban network start local`, you can use `soroban network stop local` to stop it.
Stop(stop::Cmd),
}

#[derive(thiserror::Error, Debug)]
Expand All @@ -39,6 +53,12 @@ pub enum Error {
#[error(transparent)]
Ls(#[from] ls::Error),

#[error(transparent)]
Start(#[from] start::Error),

#[error(transparent)]
Stop(#[from] stop::Error),

#[error(transparent)]
Config(#[from] locator::Error),

Expand All @@ -61,11 +81,13 @@ pub enum Error {
}

impl Cmd {
pub fn run(&self) -> Result<(), Error> {
pub async fn run(&self) -> Result<(), Error> {
match self {
Cmd::Add(cmd) => cmd.run()?,
Cmd::Rm(new) => new.run()?,
Cmd::Ls(cmd) => cmd.run()?,
Cmd::Start(cmd) => cmd.run().await?,
Cmd::Stop(cmd) => cmd.run().await?,
};
Ok(())
}
Expand Down
150 changes: 150 additions & 0 deletions cmd/soroban-cli/src/commands/network/shared.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use core::fmt;

use bollard::{ClientVersion, Docker};
use clap::ValueEnum;
#[allow(unused_imports)]
// Need to add this for windows, since we are only using this crate for the unix fn try_docker_desktop_socket
use home::home_dir;

pub const DOCKER_HOST_HELP: &str = "Optional argument to override the default docker host. This is useful when you are using a non-standard docker host path for your Docker-compatible container runtime, e.g. Docker Desktop defaults to $HOME/.docker/run/docker.sock instead of /var/run/docker.sock";

// DEFAULT_DOCKER_HOST is from the bollard crate on the main branch, which has not been released yet: https://github.com/fussybeaver/bollard/blob/0972b1aac0ad5c08798e100319ddd0d2ee010365/src/docker.rs#L64
#[cfg(unix)]
pub const DEFAULT_DOCKER_HOST: &str = "unix:///var/run/docker.sock";

#[cfg(windows)]
pub const DEFAULT_DOCKER_HOST: &str = "npipe:////./pipe/docker_engine";

// DEFAULT_TIMEOUT and API_DEFAULT_VERSION are from the bollard crate
const DEFAULT_TIMEOUT: u64 = 120;
const API_DEFAULT_VERSION: &ClientVersion = &ClientVersion {
major_version: 1,
minor_version: 40,
};

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("⛔ ️Failed to start container: {0}")]
BollardErr(#[from] bollard::errors::Error),

#[error("URI scheme is not supported: {uri}")]
UnsupportedURISchemeError { uri: String },
}

#[derive(ValueEnum, Debug, Clone, PartialEq)]
pub enum Network {
Local,
Testnet,
Futurenet,
Pubnet,
}

impl fmt::Display for Network {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let variant_str = match self {
Network::Local => "local",
Network::Testnet => "testnet",
Network::Futurenet => "futurenet",
Network::Pubnet => "pubnet",
};

write!(f, "{variant_str}")
}
}

pub async fn connect_to_docker(docker_host: &Option<String>) -> Result<Docker, Error> {
// if no docker_host is provided, use the default docker host:
// "unix:///var/run/docker.sock" on unix machines
// "npipe:////./pipe/docker_engine" on windows machines

let host = docker_host
.clone()
.unwrap_or(DEFAULT_DOCKER_HOST.to_string());

// this is based on the `connect_with_defaults` method which has not yet been released in the bollard crate
// https://github.com/fussybeaver/bollard/blob/0972b1aac0ad5c08798e100319ddd0d2ee010365/src/docker.rs#L660
let connection = match host.clone() {
// if tcp or http, use connect_with_http_defaults
// if unix and host starts with "unix://" use connect_with_unix
// if windows and host starts with "npipe://", use connect_with_named_pipe
// else default to connect_with_unix
h if h.starts_with("tcp://") || h.starts_with("http://") => {
Docker::connect_with_http_defaults()
}
#[cfg(unix)]
h if h.starts_with("unix://") => {
Docker::connect_with_unix(&h, DEFAULT_TIMEOUT, API_DEFAULT_VERSION)
}
#[cfg(windows)]
h if h.starts_with("npipe://") => {
Docker::connect_with_named_pipe(&h, DEFAULT_TIMEOUT, API_DEFAULT_VERSION)
}
_ => {
return Err(Error::UnsupportedURISchemeError {
uri: host.to_string(),
});
}
}?;

match check_docker_connection(&connection).await {
Ok(()) => Ok(connection),
// If we aren't able to connect with the defaults, or with the provided docker_host
// try to connect with the default docker desktop socket since that is a common use case for devs
#[allow(unused_variables)]
Err(e) => {
// if on unix, try to connect to the default docker desktop socket
#[cfg(unix)]
{
let docker_desktop_connection = try_docker_desktop_socket(&host)?;
match check_docker_connection(&docker_desktop_connection).await {
Ok(()) => Ok(docker_desktop_connection),
Err(err) => Err(err)?,
}
}

#[cfg(windows)]
{
Err(e)?
}
}
}
}

#[cfg(unix)]
fn try_docker_desktop_socket(host: &str) -> Result<Docker, bollard::errors::Error> {
let default_docker_desktop_host =
format!("{}/.docker/run/docker.sock", home_dir().unwrap().display());
println!("Failed to connect to DOCKER_HOST: {host}.\nTrying to connect to the default Docker Desktop socket at {default_docker_desktop_host}.");

Docker::connect_with_unix(
&default_docker_desktop_host,
DEFAULT_TIMEOUT,
API_DEFAULT_VERSION,
)
}

// When bollard is not able to connect to the docker daemon, it returns a generic ConnectionRefused error
// This method attempts to connect to the docker daemon and returns a more specific error message
async fn check_docker_connection(docker: &Docker) -> Result<(), bollard::errors::Error> {
// This is a bit hacky, but the `client_addr` field is not directly accessible from the `Docker` struct, but we can access it from the debug string representation of the `Docker` struct
let docker_debug_string = format!("{docker:#?}");
let start_of_client_addr = docker_debug_string.find("client_addr: ").unwrap();
let end_of_client_addr = docker_debug_string[start_of_client_addr..]
.find(',')
.unwrap();
// Extract the substring containing the value of client_addr
let client_addr = &docker_debug_string
[start_of_client_addr + "client_addr: ".len()..start_of_client_addr + end_of_client_addr]
.trim()
.trim_matches('"');

match docker.version().await {
Ok(_version) => Ok(()),
Err(err) => {
println!(
"⛔️ Failed to connect to the Docker daemon at {client_addr:?}. Is the docker daemon running?\nℹ️ Running a local Stellar network requires a Docker-compatible container runtime.\nℹ️ Please note that if you are using Docker Desktop, you may need to utilize the `--docker-host` flag to pass in the location of the docker socket on your machine.\n"
);
Err(err)
}
}
}
Loading

0 comments on commit 5fe1b8d

Please sign in to comment.