-
Notifications
You must be signed in to change notification settings - Fork 97
feat: support connecting to a pocket-ic server running inside a Docker container. #4271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b863ae2
3048741
679b976
c8c8619
b3f3840
23765b0
f4d30aa
5ee1bc6
f0e26b1
340016e
63bfb30
494c188
ee23529
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,6 +66,7 @@ pub struct Config { | |
pub shutdown_controller: Addr<ShutdownController>, | ||
pub logger: Option<Logger>, | ||
pub pocketic_proxy_config: PocketIcProxyConfig, | ||
pub docker: Option<String>, | ||
} | ||
|
||
#[derive(Clone)] | ||
|
@@ -115,16 +116,30 @@ impl PocketIc { | |
fn wait_for_ready( | ||
port_file_path: &Path, | ||
shutdown_signal: Receiver<()>, | ||
docker_container_id: Option<String>, | ||
) -> Result<u16, ControlFlow<(), DfxError>> { | ||
let mut retries = 0; | ||
loop { | ||
if let Ok(content) = std::fs::read_to_string(port_file_path) { | ||
if let Some(container_id) = docker_container_id.as_ref() { | ||
let output = std::process::Command::new("docker") | ||
.args(["exec", container_id.as_str(), "cat", "pocket-ic-port"]) | ||
.output(); | ||
if let Ok(output) = output { | ||
if let Ok(port) = String::from_utf8_lossy(&output.stdout) | ||
.trim() | ||
.parse::<u16>() | ||
{ | ||
return Ok(port); | ||
} | ||
} | ||
} else if let Ok(content) = std::fs::read_to_string(port_file_path) { | ||
if content.ends_with('\n') { | ||
if let Ok(port) = content.trim().parse::<u16>() { | ||
return Ok(port); | ||
} | ||
} | ||
} | ||
|
||
if shutdown_signal.try_recv().is_ok() { | ||
return Err(Break(())); | ||
} | ||
|
@@ -238,41 +253,82 @@ fn pocketic_start_thread( | |
) -> DfxResult<std::thread::JoinHandle<()>> { | ||
let thread_handler = move || { | ||
loop { | ||
// Start the process, then wait for the file. | ||
let pocketic_path = config.pocketic_path.as_os_str(); | ||
let mut bind_address = config.pocketic_proxy_config.bind; | ||
let (mut child, docker_container_id, last_start) = | ||
if let Some(docker_image) = config.docker.as_ref() { | ||
// Run the container. | ||
// TODO: Check if docker command is available. | ||
let mut cmd = std::process::Command::new("docker"); | ||
cmd.args(["run"]); | ||
bind_address = convert_to_docker_bind_address(bind_address); | ||
cmd.args([ | ||
"-p", | ||
format!("{}:{}", bind_address.port(), bind_address.port()).as_str(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inside the container a fixed port could be used for simplicity. |
||
"-p", | ||
"8081:8081", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assumes a fixed port on the host without an option to override. |
||
]); | ||
cmd.args(["-d", docker_image.as_str()]); | ||
cmd.stdout(std::process::Stdio::piped()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd also expect the |
||
|
||
let last_start = std::time::Instant::now(); | ||
let child = cmd.spawn().expect("Could not start PocketIC."); | ||
|
||
// Retrieve the container id. | ||
// TODO: The container id will be used to remove the container when the process stops. | ||
let output = child.wait_with_output().unwrap(); | ||
let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string(); | ||
|
||
// Stream the logs. | ||
let mut cmd = std::process::Command::new("docker"); | ||
cmd.args(["logs", "-f", container_id.as_ref()]); | ||
cmd.stdout(std::process::Stdio::inherit()); | ||
let child = cmd.spawn().expect("Could not stream logs."); | ||
|
||
(child, Some(container_id), last_start) | ||
} else { | ||
// Start the process, then wait for the file. | ||
let pocketic_path = config.pocketic_path.as_os_str(); | ||
|
||
// form the pocket-ic command here similar to the ic-starter command | ||
let mut cmd = std::process::Command::new(pocketic_path); | ||
if let Some(port) = config.port { | ||
cmd.args(["--port", &port.to_string()]); | ||
}; | ||
cmd.args([ | ||
"--port-file", | ||
&config.port_file.to_string_lossy(), | ||
"--ttl", | ||
"2592000", | ||
]); | ||
cmd.args(["--log-levels", "error"]); | ||
cmd.stdout(std::process::Stdio::inherit()); | ||
cmd.stderr(std::process::Stdio::inherit()); | ||
#[cfg(unix)] | ||
{ | ||
use std::os::unix::process::CommandExt; | ||
cmd.process_group(0); | ||
} | ||
let _ = std::fs::remove_file(&config.port_file); | ||
let last_start = std::time::Instant::now(); | ||
debug!(logger, "Starting PocketIC..."); | ||
let child = cmd.spawn().expect("Could not start PocketIC."); | ||
|
||
// form the pocket-ic command here similar to the ic-starter command | ||
let mut cmd = std::process::Command::new(pocketic_path); | ||
if let Some(port) = config.port { | ||
cmd.args(["--port", &port.to_string()]); | ||
}; | ||
cmd.args([ | ||
"--port-file", | ||
&config.port_file.to_string_lossy(), | ||
"--ttl", | ||
"2592000", | ||
]); | ||
cmd.args(["--log-levels", "error"]); | ||
cmd.stdout(std::process::Stdio::inherit()); | ||
cmd.stderr(std::process::Stdio::inherit()); | ||
#[cfg(unix)] | ||
{ | ||
use std::os::unix::process::CommandExt; | ||
cmd.process_group(0); | ||
} | ||
let _ = std::fs::remove_file(&config.port_file); | ||
let last_start = std::time::Instant::now(); | ||
debug!(logger, "Starting PocketIC..."); | ||
let mut child = cmd.spawn().expect("Could not start PocketIC."); | ||
if let Err(e) = std::fs::write(&config.pid_file, child.id().to_string()) { | ||
warn!( | ||
logger, | ||
"Failed to write PocketIC PID to {}: {e}", | ||
config.pid_file.display() | ||
); | ||
} | ||
if let Err(e) = std::fs::write(&config.pid_file, child.id().to_string()) { | ||
warn!( | ||
logger, | ||
"Failed to write PocketIC PID to {}: {e}", | ||
config.pid_file.display() | ||
); | ||
} | ||
|
||
(child, None, last_start) | ||
}; | ||
|
||
let port = match PocketIc::wait_for_ready(&config.port_file, receiver.clone()) { | ||
let port = match PocketIc::wait_for_ready( | ||
&config.port_file, | ||
receiver.clone(), | ||
docker_container_id.clone(), | ||
) { | ||
Ok(p) => p, | ||
Err(e) => { | ||
let _ = child.kill(); | ||
|
@@ -286,6 +342,8 @@ fn pocketic_start_thread( | |
} | ||
} | ||
}; | ||
println!("port: {}", port); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Left-over debug output? |
||
|
||
let server_instance = match initialize_pocketic( | ||
port, | ||
&config.effective_config_path, | ||
|
@@ -313,7 +371,7 @@ fn pocketic_start_thread( | |
format!("http://localhost:{port}").parse().unwrap(), | ||
server_instance, | ||
config.pocketic_proxy_config.domains.clone(), | ||
config.pocketic_proxy_config.bind, | ||
bind_address, | ||
logger.clone(), | ||
) { | ||
Err(e) => { | ||
|
@@ -336,9 +394,13 @@ fn pocketic_start_thread( | |
match wait_for_child_or_receiver(&mut child, &receiver) { | ||
ChildOrReceiver::Receiver => { | ||
debug!(logger, "Got signal to stop. Killing PocketIC process..."); | ||
if let Err(e) = | ||
shutdown_pocketic(port, server_instance, gateway_instance, logger.clone()) | ||
{ | ||
if let Err(e) = shutdown_pocketic( | ||
port, | ||
server_instance, | ||
gateway_instance, | ||
docker_container_id, | ||
logger.clone(), | ||
) { | ||
error!(logger, "Error shutting down PocketIC gracefully: {e}"); | ||
} | ||
let _ = child.kill(); | ||
|
@@ -367,6 +429,16 @@ fn pocketic_start_thread( | |
.map_err(DfxError::from) | ||
} | ||
|
||
fn convert_to_docker_bind_address(address: SocketAddr) -> SocketAddr { | ||
let mut bind_address = address; | ||
if bind_address.is_ipv6() { | ||
bind_address.set_ip(std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED)); | ||
} else { | ||
bind_address.set_ip(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)); | ||
} | ||
bind_address | ||
} | ||
|
||
#[cfg(unix)] | ||
#[tokio::main(flavor = "current_thread")] | ||
async fn initialize_pocketic( | ||
|
@@ -544,6 +616,7 @@ async fn shutdown_pocketic( | |
port: u16, | ||
server_instance: usize, | ||
gateway_instance: usize, | ||
docker_container_id: Option<String>, | ||
logger: Logger, | ||
) -> DfxResult { | ||
use reqwest::Client; | ||
|
@@ -563,10 +636,18 @@ async fn shutdown_pocketic( | |
.send() | ||
.await? | ||
.error_for_status()?; | ||
if let Some(docker_container_id) = docker_container_id { | ||
let output = std::process::Command::new("docker") | ||
.args(["rm", "-f", docker_container_id.as_str()]) | ||
.output(); | ||
if let Err(e) = output { | ||
error!(logger, "Failed to remove docker container: {e}"); | ||
} | ||
} | ||
Ok(()) | ||
} | ||
|
||
#[cfg(not(unix))] | ||
fn shutdown_pocketic(_: u16, _: usize, _: usize, _: Logger) -> DfxResult { | ||
fn shutdown_pocketic(_: u16, _: usize, _: usize, _: Option<String>, _: Logger) -> DfxResult { | ||
bail!("PocketIC not supported on this platform") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -82,6 +82,10 @@ pub struct StartOpts { | |
|
||
#[clap(long, hide = true)] | ||
replica: bool, | ||
|
||
/// Runs the given docker image which runs PocketIC process. | ||
#[arg(long)] | ||
docker: Option<String>, | ||
} | ||
|
||
// The frontend webserver is brought up by the bg process; thus, the fg process | ||
|
@@ -150,6 +154,7 @@ pub fn exec( | |
domain, | ||
pocketic: _, | ||
replica, | ||
docker, | ||
}: StartOpts, | ||
) -> DfxResult { | ||
ensure!(!replica, "The 'native' replica (--replica) is no longer supported. See the 0.27.0 migration guide for more information. | ||
|
@@ -161,6 +166,9 @@ https://github.com/dfinity/sdk/blob/0.27.0/docs/migration/dfx-0.27.0-migration-g | |
dfx_version_str() | ||
); | ||
} | ||
if docker.is_some() { | ||
check_docker_command_available()?; | ||
} | ||
let project_config = env.get_config()?; | ||
|
||
let network_descriptor_logger = if background { | ||
|
@@ -318,6 +326,7 @@ https://github.com/dfinity/sdk/blob/0.27.0/docs/migration/dfx-0.27.0-migration-g | |
shutdown_controller.clone(), | ||
pocketic_port_path, | ||
pocketic_proxy_config, | ||
docker, | ||
)?; | ||
|
||
let post_start = start_post_start_actor(env, running_in_background, Some(server), spinner)?; | ||
|
@@ -484,6 +493,7 @@ fn frontend_address( | |
address_and_port = | ||
get_reusable_socket_addr(address_and_port.ip(), address_and_port.port())?; | ||
} | ||
|
||
let ip = if address_and_port.is_ipv6() { | ||
format!("[{}]", address_and_port.ip()) | ||
} else { | ||
|
@@ -525,3 +535,16 @@ pub fn empty_writable_path(path: PathBuf) -> DfxResult<PathBuf> { | |
.with_context(|| format!("Unable to write to {}", path.to_string_lossy()))?; | ||
Ok(path) | ||
} | ||
|
||
fn check_docker_command_available() -> DfxResult<()> { | ||
if std::process::Command::new("docker") | ||
.args(["--version"]) | ||
Comment on lines
+540
to
+541
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even if the command is available, docker might still fail if the daemon is not running. |
||
.output() | ||
.is_err() | ||
{ | ||
bail!( | ||
"The 'docker' command is not available. Please install Docker or remove the --docker option." | ||
); | ||
} | ||
Ok(()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the (host) port retrieved from the docker container? I'd expect the host port to be stored on the host and mapped to a (fixed) port used in the docker container.