pub mod config;
use crate::provider::docker::Docker;
use crate::provider::docker_compose::DockerCompose;
use crate::provider::podman::Podman;
use crate::provider::podman_compose::PodmanCompose;
use crate::provider::Provider;
use crate::settings::Settings;
use config::Config;
use std::path::Path;
use std::path::PathBuf;
pub struct Devcontainer {
config: Config,
provider: Box<dyn Provider>,
settings: Settings,
}
impl Devcontainer {
pub fn load(directory: PathBuf) -> Self {
let file = directory.join(".devcontainer").join("devcontainer.json");
let config = Config::parse(&file).expect("could not find devcontainer.json");
let settings = Settings::load();
let provider = build_provider(&directory, &settings, &config);
Self {
config: config.clone(),
provider,
settings,
}
}
pub fn run(&self, use_cache: bool) -> std::io::Result<()> {
let provider = &self.provider;
self.create(use_cache)?;
if !provider.running()? {
provider.start()?;
}
self.post_create()?;
provider.restart()?;
provider.attach()?;
if self.config.should_shutdown() {
provider.stop()?;
}
Ok(())
}
pub fn rebuild(&self, use_cache: bool) -> std::io::Result<()> {
let provider = &self.provider;
if provider.exists()? {
provider.stop()?;
provider.rm()?;
}
self.run(use_cache)
}
fn create(&self, use_cache: bool) -> std::io::Result<()> {
let provider = &self.provider;
if !provider.exists()? {
provider.build(use_cache)?;
provider.create(self.create_args())?;
}
Ok(())
}
fn post_create(&self) -> std::io::Result<()> {
let provider = &self.provider;
if let Some(command) = self.config.on_create_command.clone() {
provider.exec(command)?;
}
if let Some(command) = self.config.update_content_command.clone() {
provider.exec(command)?;
}
if let Some(command) = self.config.post_create_command.clone() {
provider.exec(command)?;
}
self.copy_gitconfig()?;
self.copy_dotfiles()?;
Ok(())
}
fn copy(&self, source: &Path, dest: &str) -> std::io::Result<bool> {
if source.exists() {
let provider = &self.provider;
let destpath = PathBuf::from(dest);
let basedir = destpath.parent().and_then(|p| p.to_str()).unwrap();
let destination = if source.is_dir() { basedir } else { dest };
provider.exec(format!("mkdir -p {}", basedir))?;
provider.cp(
source.to_string_lossy().to_string(),
destination.to_string(),
)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("File not found {:?}", source),
))
}
}
fn copy_dotfiles(&self) -> std::io::Result<()> {
let homedir = if self.config.remote_user == "root" {
PathBuf::from("/root")
} else {
PathBuf::from("/home").join(&self.config.remote_user)
};
for file in &self.settings.dotfiles {
let tilded = format!("~/{}", file);
let expanded = shellexpand::tilde(&tilded).to_string();
let source = PathBuf::from(expanded);
let dest = homedir.join(file.clone());
self.copy(&source, dest.to_str().unwrap())?;
}
Ok(())
}
fn copy_gitconfig(&self) -> std::io::Result<bool> {
let path = shellexpand::tilde("~/.gitconfig").to_string();
let file = PathBuf::from(path);
let dest = format!("/home/{}/.gitconfig", self.config.remote_user);
self.copy(&file, &dest)
}
pub fn create_args(&self) -> Vec<String> {
let mut args = vec![];
for (key, value) in &self.config.remote_env {
args.push("-e".to_string());
args.push(format!("{}={}", key, value));
}
let workspace_folder = self.config.workspace_folder.clone();
args.push("-w".to_string());
args.push(workspace_folder);
for arg in self.config.run_args.clone() {
args.push(arg);
}
args
}
}
fn build_provider(directory: &Path, settings: &Settings, config: &Config) -> Box<dyn Provider> {
match settings.provider {
crate::settings::Provider::Docker => {
if config.is_compose() {
let composefile = directory
.join(".devcontainer")
.join(config.docker_compose_file.as_ref().unwrap());
Box::new(DockerCompose {
build_args: config.build_args(),
directory: directory.to_str().map(|d| d.to_string()).unwrap(),
command: "docker".to_string(),
file: composefile.to_str().unwrap().to_string(),
name: config.safe_name(),
forward_ports: config.forward_ports.clone(),
run_args: config.run_args.clone(),
service: config.service.as_ref().unwrap().to_string(),
user: config.remote_user.clone(),
workspace_folder: config.workspace_folder.clone(),
})
} else {
let dockerfile = directory
.join(".devcontainer")
.join(config.dockerfile().unwrap());
Box::new(Docker {
build_args: config.build_args(),
directory: directory.to_str().map(|d| d.to_string()).unwrap(),
command: "docker".to_string(),
file: dockerfile.to_str().unwrap().to_string(),
forward_ports: config.forward_ports.clone(),
name: config.safe_name(),
run_args: config.run_args.clone(),
user: config.remote_user.clone(),
workspace_folder: config.workspace_folder.clone(),
})
}
}
crate::settings::Provider::Podman => {
if config.is_compose() {
let composefile = directory
.join(".devcontainer")
.join(config.docker_compose_file.as_ref().unwrap());
Box::new(PodmanCompose {
build_args: config.build_args(),
directory: directory.to_str().map(|d| d.to_string()).unwrap(),
command: "podman-compose".to_string(),
file: composefile.to_str().unwrap().to_string(),
forward_ports: config.forward_ports.clone(),
name: config.safe_name(),
podman_command: "podman".to_string(),
run_args: config.run_args.clone(),
service: config.service.as_ref().unwrap().to_string(),
user: config.remote_user.clone(),
workspace_folder: config.workspace_folder.clone(),
})
} else {
let dockerfile = directory
.join(".devcontainer")
.join(config.dockerfile().unwrap());
Box::new(Podman {
build_args: config.build_args(),
directory: directory.to_str().map(|d| d.to_string()).unwrap(),
command: "podman".to_string(),
file: dockerfile.to_str().unwrap().to_string(),
forward_ports: config.forward_ports.clone(),
name: config.safe_name(),
run_args: config.run_args.clone(),
user: config.remote_user.clone(),
workspace_folder: config.workspace_folder.clone(),
})
}
}
}
}