feat: Autoinstall docker
All checks were successful
Run Check Script / check (pull_request) Successful in 1m25s
All checks were successful
Run Check Script / check (pull_request) Successful in 1m25s
This commit is contained in:
300
harmony_tools/src/docker.rs
Normal file
300
harmony_tools/src/docker.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
use crate::downloadable_asset::DownloadableAsset;
|
||||
use inquire::Select;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use url::Url;
|
||||
|
||||
pub struct Docker {
|
||||
base_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DockerVariant {
|
||||
Standard,
|
||||
Rootless,
|
||||
Manual,
|
||||
}
|
||||
|
||||
impl fmt::Display for DockerVariant {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DockerVariant::Standard => write!(f, "Standard Docker (requires sudo)"),
|
||||
DockerVariant::Rootless => write!(f, "Rootless Docker (no sudo required)"),
|
||||
DockerVariant::Manual => {
|
||||
write!(f, "Exit and install manually (Docker or podman-docker)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Docker {
|
||||
pub fn new(base_dir: PathBuf) -> Self {
|
||||
Self { base_dir }
|
||||
}
|
||||
|
||||
/// Provides the DOCKER_HOST and DOCKER_SOCK env vars for local usage.
|
||||
///
|
||||
/// If a rootless Docker installation is detected in the user's home directory,
|
||||
/// it returns the appropriate `DOCKER_HOST` pointing to the user's Docker socket.
|
||||
/// Otherwise, it returns an empty HashMap, assuming the standard system-wide
|
||||
/// Docker installation is used.
|
||||
pub fn get_docker_env(&self) -> HashMap<String, String> {
|
||||
let mut env = HashMap::new();
|
||||
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
let rootless_sock = PathBuf::from(&home).join(".docker/run/docker.sock");
|
||||
let rootless_bin = PathBuf::from(&home).join("bin/docker");
|
||||
|
||||
if rootless_bin.exists() && rootless_sock.exists() {
|
||||
let docker_host = format!("unix://{}", rootless_sock.display());
|
||||
debug!(
|
||||
"Detected rootless Docker, setting DOCKER_HOST={}",
|
||||
docker_host
|
||||
);
|
||||
env.insert("DOCKER_HOST".to_string(), docker_host);
|
||||
}
|
||||
}
|
||||
|
||||
env
|
||||
}
|
||||
|
||||
/// Gets the path to the docker binary
|
||||
pub fn get_bin_path(&self) -> PathBuf {
|
||||
// Check standard PATH first
|
||||
if let Ok(path) = std::process::Command::new("which")
|
||||
.arg("docker")
|
||||
.output()
|
||||
.map(|o| PathBuf::from(String::from_utf8_lossy(&o.stdout).trim()))
|
||||
{
|
||||
if path.exists() {
|
||||
debug!("Found Docker in PATH: {:?}", path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Check common rootless location
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
let rootless_path = PathBuf::from(home).join("bin/docker");
|
||||
if rootless_path.exists() {
|
||||
debug!("Found rootless Docker at: {:?}", rootless_path);
|
||||
return rootless_path;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Docker not found in PATH or rootless location, using 'docker' from PATH");
|
||||
PathBuf::from("docker")
|
||||
}
|
||||
|
||||
/// Checks if docker is installed and available in the PATH
|
||||
pub fn is_installed(&self) -> bool {
|
||||
let bin_path = self.get_bin_path();
|
||||
trace!("Checking if Docker is installed at: {:?}", bin_path);
|
||||
|
||||
std::process::Command::new(&bin_path)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|output| {
|
||||
if output.status.success() {
|
||||
trace!("Docker version check successful");
|
||||
true
|
||||
} else {
|
||||
trace!(
|
||||
"Docker version check failed with status: {:?}",
|
||||
output.status
|
||||
);
|
||||
false
|
||||
}
|
||||
})
|
||||
.map_err(|e| {
|
||||
trace!("Failed to execute Docker version check: {}", e);
|
||||
e
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Prompts the user to choose an installation method
|
||||
fn prompt_for_installation(&self) -> DockerVariant {
|
||||
let options = vec![
|
||||
DockerVariant::Standard,
|
||||
DockerVariant::Rootless,
|
||||
DockerVariant::Manual,
|
||||
];
|
||||
|
||||
Select::new(
|
||||
"Docker binary was not found. How would you like to proceed?",
|
||||
options,
|
||||
)
|
||||
.with_help_message("Standard requires sudo. Rootless runs in user space.")
|
||||
.prompt()
|
||||
.unwrap_or(DockerVariant::Manual)
|
||||
}
|
||||
|
||||
/// Installs docker using the official shell script
|
||||
pub async fn install(&self, variant: DockerVariant) -> Result<(), String> {
|
||||
let (script_url, script_name, use_sudo) = match variant {
|
||||
DockerVariant::Standard => ("https://get.docker.com", "get-docker.sh", true),
|
||||
DockerVariant::Rootless => (
|
||||
"https://get.docker.com/rootless",
|
||||
"get-docker-rootless.sh",
|
||||
false,
|
||||
),
|
||||
DockerVariant::Manual => return Err("Manual installation selected".to_string()),
|
||||
};
|
||||
|
||||
info!("Installing {}...", variant);
|
||||
debug!("Downloading installation script from: {}", script_url);
|
||||
|
||||
// Download the installation script
|
||||
let asset = DownloadableAsset {
|
||||
url: Url::parse(script_url).map_err(|e| {
|
||||
error!("Failed to parse installation script URL: {}", e);
|
||||
format!("Failed to parse installation script URL: {}", e)
|
||||
})?,
|
||||
file_name: script_name.to_string(),
|
||||
checksum: Some(String::new()), // Skip checksum verification for official scripts
|
||||
};
|
||||
|
||||
let downloaded_script = asset
|
||||
.download_to_path(self.base_dir.join("scripts"))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to download installation script: {}", e);
|
||||
format!("Failed to download installation script: {}", e)
|
||||
})?;
|
||||
|
||||
debug!("Installation script downloaded to: {:?}", downloaded_script);
|
||||
|
||||
// Execute the installation script
|
||||
let mut cmd = std::process::Command::new("sh");
|
||||
if use_sudo {
|
||||
cmd.arg("sudo").arg("sh");
|
||||
}
|
||||
cmd.arg(&downloaded_script);
|
||||
|
||||
debug!("Executing installation command: {:?}", cmd);
|
||||
|
||||
let status = cmd.status().map_err(|e| {
|
||||
error!("Failed to execute docker installation script: {}", e);
|
||||
format!("Failed to execute docker installation script: {}", e)
|
||||
})?;
|
||||
|
||||
if status.success() {
|
||||
info!("{} installed successfully", variant);
|
||||
if variant == DockerVariant::Rootless {
|
||||
warn!("Please follow the instructions above to finish rootless setup (environment variables).");
|
||||
}
|
||||
|
||||
// Validate the installation by running hello-world
|
||||
self.validate_installation()?;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
error!(
|
||||
"{} installation script failed with exit code: {:?}",
|
||||
variant,
|
||||
status.code()
|
||||
);
|
||||
Err(format!("{} installation script failed", variant))
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates the Docker installation by running a test container.
|
||||
///
|
||||
/// This method runs `docker run --rm hello-world` to verify that Docker
|
||||
/// is properly installed and functional.
|
||||
fn validate_installation(&self) -> Result<(), String> {
|
||||
info!("Validating Docker installation by running hello-world container...");
|
||||
|
||||
let output = self
|
||||
.command()
|
||||
.args(["run", "--rm", "hello-world"])
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
error!("Failed to execute hello-world validation: {}", e);
|
||||
format!("Failed to execute hello-world validation: {}", e)
|
||||
})?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if stdout.contains("Hello from Docker!") {
|
||||
info!("Docker installation validated successfully");
|
||||
trace!("Validation output: {}", stdout);
|
||||
Ok(())
|
||||
} else {
|
||||
warn!("Hello-world container ran but expected output not found");
|
||||
debug!("Output was: {}", stdout);
|
||||
Err("Docker validation failed: unexpected output from hello-world".to_string())
|
||||
}
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
error!(
|
||||
"Hello-world validation failed with exit code: {:?}",
|
||||
output.status.code()
|
||||
);
|
||||
debug!("Validation stderr: {}", stderr);
|
||||
if !stderr.is_empty() {
|
||||
Err(format!("Docker validation failed: {}", stderr.trim()))
|
||||
} else {
|
||||
Err(
|
||||
"Docker validation failed: hello-world container did not run successfully"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures docker is installed, prompting if necessary
|
||||
pub async fn ensure_installed(&self) -> Result<(), String> {
|
||||
if self.is_installed() {
|
||||
debug!("Docker is already installed at: {:?}", self.get_bin_path());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!("Docker is not installed, prompting for installation method");
|
||||
match self.prompt_for_installation() {
|
||||
DockerVariant::Manual => {
|
||||
info!("User chose manual installation");
|
||||
Err("Docker installation cancelled by user. Please install docker or podman-docker manually.".to_string())
|
||||
}
|
||||
variant => self.install(variant).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a pre-configured Command for running Docker commands.
|
||||
///
|
||||
/// The returned Command is set up with:
|
||||
/// - The correct Docker binary path (handles rootless installations)
|
||||
/// - Appropriate environment variables (e.g., DOCKER_HOST for rootless)
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use harmony_tools::Docker;
|
||||
/// # use std::path::PathBuf;
|
||||
/// # let docker = Docker::new(PathBuf::from("."));
|
||||
/// let mut cmd = docker.command();
|
||||
/// cmd.args(["ps", "-a"]);
|
||||
/// // Now cmd is ready to be executed
|
||||
/// ```
|
||||
pub fn command(&self) -> std::process::Command {
|
||||
let bin_path = self.get_bin_path();
|
||||
trace!("Creating Docker command with binary: {:?}", bin_path);
|
||||
|
||||
let mut cmd = std::process::Command::new(&bin_path);
|
||||
|
||||
// Add Docker-specific environment variables
|
||||
let env = self.get_docker_env();
|
||||
if !env.is_empty() {
|
||||
trace!("Setting Docker environment variables: {:?}", env);
|
||||
for (key, value) in env {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
} else {
|
||||
trace!("No Docker-specific environment variables to set");
|
||||
}
|
||||
|
||||
cmd
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user