Skip to content

Commit eb2d47e

Browse files
authored
feat(cli): Add Podman Support (#697)
## Description Adds Podman container runtime support as an alternative to Docker. Users can now configure `container_runtime = "podman"` in `helix.toml` to use Podman for deployments. **Changes:** - Added `ContainerRuntime` enum (`Docker`/`Podman`) in `config.rs` - Updated `DockerManager` to use runtime from project config - Added auto-start daemon functionality for both runtimes (macOS/Linux/Windows) - Renamed `check_docker_available()` to `check_runtime_available()` with runtime-agnostic logic - Updated all CLI commands to support both container runtimes ## Related Issues Closes #653 ## Checklist when merging to main - [x] No compiler warnings (if applicable) - [x] Code is formatted with `rustfmt` - [x] No useless or dead code (if applicable) - [x] Code is easy to understand - [x] Doc comments are used for all functions, enums, structs, and fields (where appropriate) - [x] All tests pass - [ ] Performance has not regressed (assuming change was not to fix a bug) - [ ] Version number has been updated in `helix-cli/Cargo.toml` and `helixdb/Cargo.toml` ## Additional Notes <!-- Add any additional information that would be helpful for reviewers --> <!-- greptile_comment --> <h3>Greptile Summary</h3> - Adds Podman container runtime support as an alternative to Docker by introducing a `ContainerRuntime` enum and updating `DockerManager` to use configured runtime from project settings - Implements comprehensive cross-platform daemon auto-start functionality for both Docker and Podman across macOS/Linux/Windows with platform-specific handling - Refactors all CLI commands to use runtime-agnostic container availability checks replacing hardcoded Docker dependencies <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | `helix-cli/src/config.rs` | Added `ContainerRuntime` enum and integration into project config with Docker as default for backward compatibility | | `helix-cli/src/docker.rs` | Major refactoring to support both Docker and Podman with platform-specific daemon management and runtime-agnostic operations | | `helix-cli/src/commands/status.rs` | Updated to use configurable runtime but still displays "Docker Status" label regardless of runtime choice - potential UX inconsistency | </details> <!-- greptile_other_comments_section --> **Context used:** - Context from `dashboard` - Main documentation for all of HelixDB, the SDKs, HelixQL, and the Helix CLI ([source](https://app.greptile.com/review/custom-context?memory=861865e1-0cf9-4602-a499-40c0c360142c)) - Context from `dashboard` - readme for helixdb ([source](https://app.greptile.com/review/custom-context?memory=128beba4-b562-4fe4-b7ba-aef1f5b3f204)) <!-- /greptile_comment -->
2 parents 48a55ce + 2dae43f commit eb2d47e

File tree

13 files changed

+411
-175
lines changed

13 files changed

+411
-175
lines changed

helix-cli/src/commands/add.rs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,20 @@ async fn run_add_inner(
6767
let helix_manager = HelixManager::new(&project_context);
6868

6969
// Create cloud instance configuration
70-
let cloud_config = helix_manager.create_instance_config(&instance_name, region).await?;
70+
let cloud_config = helix_manager
71+
.create_instance_config(&instance_name, region)
72+
.await?;
7173

7274
// Initialize the cloud cluster
73-
helix_manager.init_cluster(&instance_name, &cloud_config).await?;
75+
helix_manager
76+
.init_cluster(&instance_name, &cloud_config)
77+
.await?;
7478

7579
// Insert into project configuration
76-
project_context
77-
.config
78-
.cloud
79-
.insert(instance_name.clone(), CloudConfig::Helix(cloud_config.clone()));
80+
project_context.config.cloud.insert(
81+
instance_name.clone(),
82+
CloudConfig::Helix(cloud_config.clone()),
83+
);
8084

8185
print_status("CLOUD", "Helix cloud instance configuration added");
8286
}
@@ -95,7 +99,9 @@ async fn run_add_inner(
9599
.await?;
96100

97101
// Initialize the ECR repository
98-
ecr_manager.init_repository(&instance_name, &ecr_config).await?;
102+
ecr_manager
103+
.init_repository(&instance_name, &ecr_config)
104+
.await?;
99105

100106
// Save configuration to ecr.toml
101107
ecr_manager.save_config(&instance_name, &ecr_config).await?;
@@ -135,12 +141,14 @@ async fn run_add_inner(
135141
);
136142

137143
// Initialize the Fly.io app
138-
fly_manager.init_app(&instance_name, &instance_config).await?;
144+
fly_manager
145+
.init_app(&instance_name, &instance_config)
146+
.await?;
139147

140-
project_context
141-
.config
142-
.cloud
143-
.insert(instance_name.clone(), CloudConfig::FlyIo(instance_config.clone()));
148+
project_context.config.cloud.insert(
149+
instance_name.clone(),
150+
CloudConfig::FlyIo(instance_config.clone()),
151+
);
144152
}
145153
_ => {
146154
// Add local instance with default configuration

helix-cli/src/commands/build.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,10 @@ pub async fn run(instance_name: String, metrics_sender: &MetricsSender) -> Resul
8686

8787
// For local instances, build Docker image
8888
if instance_config.should_build_docker_image() {
89+
let runtime = project.config.project.container_runtime;
90+
DockerManager::check_runtime_available(runtime)?;
8991
let docker = DockerManager::new(&project);
90-
DockerManager::check_docker_available()?;
92+
9193
docker.build_image(&instance_name, instance_config.docker_build_target())?;
9294
}
9395

@@ -119,13 +121,19 @@ fn needs_cache_recreation(repo_cache: &std::path::Path) -> Result<bool> {
119121

120122
match (DEV_MODE, is_git_repo) {
121123
(true, true) => {
122-
print_status("CACHE", "Cache is git repo but DEV_MODE requires copy - recreating...");
124+
print_status(
125+
"CACHE",
126+
"Cache is git repo but DEV_MODE requires copy - recreating...",
127+
);
123128
Ok(true)
124-
},
129+
}
125130
(false, false) => {
126-
print_status("CACHE", "Cache is copy but production mode requires git repo - recreating...");
131+
print_status(
132+
"CACHE",
133+
"Cache is copy but production mode requires git repo - recreating...",
134+
);
127135
Ok(true)
128-
},
136+
}
129137
_ => Ok(false),
130138
}
131139
}
@@ -290,10 +298,9 @@ async fn generate_docker_files(
290298
return Ok(());
291299
}
292300

293-
print_status("DOCKER", "Generating Docker configuration...");
294-
295301
let docker = DockerManager::new(project);
296302

303+
print_status(docker.runtime.label(), "Generating configuration...");
297304
// Generate Dockerfile
298305
let dockerfile_content = docker.generate_dockerfile(instance_name, instance_config.clone())?;
299306
let dockerfile_path = project.dockerfile_path(instance_name);

helix-cli/src/commands/delete.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ pub async fn run(instance_name: String) -> Result<()> {
3737
print_status("DELETE", &format!("Deleting instance '{instance_name}'"));
3838

3939
// Stop and remove Docker containers and volumes
40-
if DockerManager::check_docker_available().is_ok() {
40+
let runtime = project.config.project.container_runtime;
41+
if DockerManager::check_runtime_available(runtime).is_ok() {
4142
let docker = DockerManager::new(&project);
4243

4344
// Remove containers and Docker volumes

helix-cli/src/commands/integrations/ecr.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ impl<'a> EcrManager<'a> {
9292
format!("helix-{}-{instance_name}", self.project.config.project.name)
9393
}
9494

95-
9695
fn image_name(&self, repository_name: &str, build_mode: BuildMode) -> String {
9796
let tag = match build_mode {
9897
BuildMode::Debug => "debug",
@@ -343,7 +342,7 @@ impl<'a> EcrManager<'a> {
343342
.to_string();
344343

345344
use tokio::io::AsyncWriteExt;
346-
let mut login_cmd = tokio::process::Command::new("docker");
345+
let mut login_cmd = tokio::process::Command::new(docker.runtime.binary());
347346
login_cmd.args([
348347
"login",
349348
"--username",

helix-cli/src/commands/migrate.rs

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::config::{
2-
BuildMode, DbConfig, GraphConfig, HelixConfig, LocalInstanceConfig, ProjectConfig, VectorConfig,
2+
BuildMode, ContainerRuntime, DbConfig, GraphConfig, HelixConfig, LocalInstanceConfig,
3+
ProjectConfig, VectorConfig,
34
};
45
use crate::errors::{CliError, project_error};
56
use crate::utils::{
@@ -231,7 +232,10 @@ fn find_hx_files(project_dir: &Path) -> Result<Vec<PathBuf>> {
231232
let entry = entry?;
232233
let path = entry.path();
233234

234-
if let Some(extension) = path.extension() && extension == "hx" && path.file_name() != Some("schema.hx".as_ref()) {
235+
if let Some(extension) = path.extension()
236+
&& extension == "hx"
237+
&& path.file_name() != Some("schema.hx".as_ref())
238+
{
235239
hx_files.push(path);
236240
}
237241
}
@@ -274,18 +278,28 @@ fn show_migration_plan(ctx: &MigrationContext) -> Result<()> {
274278

275279
print_newline();
276280
print_header("🏠 Home Directory Migration:");
277-
let home_dir = dirs::home_dir().ok_or_else(|| CliError::new("Could not find home directory"))?;
281+
let home_dir =
282+
dirs::home_dir().ok_or_else(|| CliError::new("Could not find home directory"))?;
278283
let v1_helix_dir = home_dir.join(".helix");
279284
if v1_helix_dir.exists() {
280285
let v2_marker = v1_helix_dir.join(".v2");
281286
if v2_marker.exists() {
282-
print_field("Already migrated", "~/.helix directory already migrated to v2");
287+
print_field(
288+
"Already migrated",
289+
"~/.helix directory already migrated to v2",
290+
);
283291
} else {
284292
print_field("Create backup", "~/.helix → ~/.helix-v1-backup");
285293
if v1_helix_dir.join("dockerdev").exists() {
286-
print_field("Clean up Docker", "Stop/remove helix-dockerdev containers and images");
294+
print_field(
295+
"Clean up Docker",
296+
"Stop/remove helix-dockerdev containers and images",
297+
);
287298
}
288-
print_field("Clean directory", "Remove all except ~/.helix/credentials and ~/.helix/repo");
299+
print_field(
300+
"Clean directory",
301+
"Remove all except ~/.helix/credentials and ~/.helix/repo",
302+
);
289303
if v1_helix_dir.join("credentials").exists() {
290304
print_field("Preserve file", "~/.helix/credentials");
291305
}
@@ -428,6 +442,7 @@ fn create_v2_config(ctx: &MigrationContext) -> Result<()> {
428442
let project_config = ProjectConfig {
429443
name: ctx.project_name.clone(),
430444
queries: PathBuf::from(&ctx.queries_dir),
445+
container_runtime: ContainerRuntime::Docker,
431446
};
432447

433448
// Create final helix config
@@ -500,9 +515,8 @@ fn provide_post_migration_guidance(ctx: &MigrationContext) -> Result<()> {
500515
fn migrate_home_directory(_ctx: &MigrationContext) -> Result<()> {
501516
print_status("HOME", "Migrating ~/.helix directory");
502517

503-
let home_dir = dirs::home_dir().ok_or_else(|| {
504-
CliError::new("Could not find home directory")
505-
})?;
518+
let home_dir =
519+
dirs::home_dir().ok_or_else(|| CliError::new("Could not find home directory"))?;
506520

507521
let v1_helix_dir = home_dir.join(".helix");
508522

@@ -529,8 +543,7 @@ fn migrate_home_directory(_ctx: &MigrationContext) -> Result<()> {
529543

530544
// Use the utility function to copy the directory without exclusions
531545
crate::utils::copy_dir_recursively(&v1_helix_dir, &backup_dir).map_err(|e| {
532-
CliError::new("Failed to backup ~/.helix directory")
533-
.with_caused_by(e.to_string())
546+
CliError::new("Failed to backup ~/.helix directory").with_caused_by(e.to_string())
534547
})?;
535548

536549
print_success("Created backup: ~/.helix-v1-backup");
@@ -549,8 +562,7 @@ fn migrate_home_directory(_ctx: &MigrationContext) -> Result<()> {
549562
let temp_credentials = if credentials_path.exists() {
550563
let temp_path = home_dir.join(".helix-credentials-temp");
551564
fs::rename(&credentials_path, &temp_path).map_err(|e| {
552-
CliError::new("Failed to backup credentials")
553-
.with_caused_by(e.to_string())
565+
CliError::new("Failed to backup credentials").with_caused_by(e.to_string())
554566
})?;
555567
Some(temp_path)
556568
} else {
@@ -559,48 +571,40 @@ fn migrate_home_directory(_ctx: &MigrationContext) -> Result<()> {
559571

560572
let temp_repo = if repo_path.exists() {
561573
let temp_path = home_dir.join(".helix-repo-temp");
562-
fs::rename(&repo_path, &temp_path).map_err(|e| {
563-
CliError::new("Failed to backup repo")
564-
.with_caused_by(e.to_string())
565-
})?;
574+
fs::rename(&repo_path, &temp_path)
575+
.map_err(|e| CliError::new("Failed to backup repo").with_caused_by(e.to_string()))?;
566576
Some(temp_path)
567577
} else {
568578
None
569579
};
570580

571581
// Remove the entire .helix directory
572582
fs::remove_dir_all(&v1_helix_dir).map_err(|e| {
573-
CliError::new("Failed to remove ~/.helix directory")
574-
.with_caused_by(e.to_string())
583+
CliError::new("Failed to remove ~/.helix directory").with_caused_by(e.to_string())
575584
})?;
576585

577586
// Recreate .helix directory
578587
fs::create_dir_all(&v1_helix_dir).map_err(|e| {
579-
CliError::new("Failed to recreate ~/.helix directory")
580-
.with_caused_by(e.to_string())
588+
CliError::new("Failed to recreate ~/.helix directory").with_caused_by(e.to_string())
581589
})?;
582590

583591
// Restore credentials and repo
584592
if let Some(temp_creds) = temp_credentials {
585593
fs::rename(&temp_creds, &credentials_path).map_err(|e| {
586-
CliError::new("Failed to restore credentials")
587-
.with_caused_by(e.to_string())
594+
CliError::new("Failed to restore credentials").with_caused_by(e.to_string())
588595
})?;
589596
print_info("Preserved ~/.helix/credentials");
590597
}
591598

592599
if let Some(temp_repo) = temp_repo {
593-
fs::rename(&temp_repo, &repo_path).map_err(|e| {
594-
CliError::new("Failed to restore repo")
595-
.with_caused_by(e.to_string())
596-
})?;
600+
fs::rename(&temp_repo, &repo_path)
601+
.map_err(|e| CliError::new("Failed to restore repo").with_caused_by(e.to_string()))?;
597602
print_info("Preserved ~/.helix/repo");
598603
}
599604

600605
// Create .v2 marker file to indicate migration is complete
601606
fs::write(&v2_marker, "").map_err(|e| {
602-
CliError::new("Failed to create v2 marker file")
603-
.with_caused_by(e.to_string())
607+
CliError::new("Failed to create v2 marker file").with_caused_by(e.to_string())
604608
})?;
605609

606610
print_success("Cleaned up ~/.helix directory, preserving credentials and repo");
@@ -625,7 +629,13 @@ fn cleanup_dockerdev() -> Result<()> {
625629

626630
// Try to remove any helix-related images
627631
let output = std::process::Command::new("docker")
628-
.args(["images", "--format", "{{.Repository}}:{{.Tag}}", "--filter", "reference=helix*"])
632+
.args([
633+
"images",
634+
"--format",
635+
"{{.Repository}}:{{.Tag}}",
636+
"--filter",
637+
"reference=helix*",
638+
])
629639
.output();
630640

631641
if let Ok(output) = output {
@@ -639,7 +649,14 @@ fn cleanup_dockerdev() -> Result<()> {
639649

640650
// Try to remove helix volumes
641651
let output = std::process::Command::new("docker")
642-
.args(["volume", "ls", "--format", "{{.Name}}", "--filter", "name=helix"])
652+
.args([
653+
"volume",
654+
"ls",
655+
"--format",
656+
"{{.Name}}",
657+
"--filter",
658+
"name=helix",
659+
])
643660
.output();
644661

645662
if let Ok(output) = output {

0 commit comments

Comments
 (0)