systemprompt_cli/commands/admin/setup/
docker_compose.rs1use anyhow::{Context, Result};
2use std::path::Path;
3use std::process::Command;
4use systemprompt_logging::CliService;
5
6use super::common::PostgresConfig;
7
8pub fn is_docker_available() -> bool {
9 Command::new("docker").arg("--version").output().is_ok()
10}
11
12pub fn is_compose_available() -> bool {
13 Command::new("docker")
14 .args(["compose", "version"])
15 .output()
16 .is_ok_and(|o| o.status.success())
17}
18
19pub fn is_container_running(container_name: &str) -> bool {
20 Command::new("docker")
21 .args([
22 "ps",
23 "--filter",
24 &format!("name=^{}$", container_name),
25 "--format",
26 "{{.Names}}",
27 ])
28 .output()
29 .is_ok_and(|o| !String::from_utf8_lossy(&o.stdout).trim().is_empty())
30}
31
32pub fn create_compose_files_if_missing(
33 compose_dir: &Path,
34 container_name: &str,
35 port: u16,
36) -> Result<()> {
37 const INIT_SCRIPT: &str = r#"CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
38CREATE EXTENSION IF NOT EXISTS "pgcrypto";
39"#;
40
41 let compose_file = compose_dir.join("docker-compose.yaml");
42
43 std::fs::create_dir_all(compose_dir).context("Failed to create infrastructure/docker")?;
44
45 let init_scripts_dir = compose_dir.join("init-scripts");
46 std::fs::create_dir_all(&init_scripts_dir).context("Failed to create init-scripts")?;
47
48 let compose_content = format!(
49 r#"services:
50 postgres:
51 image: postgres:18-alpine
52 container_name: {container_name}
53 environment:
54 POSTGRES_USER: ${{POSTGRES_USER:-systemprompt}}
55 POSTGRES_PASSWORD: ${{POSTGRES_PASSWORD}}
56 POSTGRES_DB: ${{POSTGRES_DB:-systemprompt}}
57 ports:
58 - "{port}:5432"
59 volumes:
60 - {container_name}_data:/var/lib/postgresql
61 - ./init-scripts:/docker-entrypoint-initdb.d
62 healthcheck:
63 test: ["CMD-SHELL", "pg_isready -U ${{POSTGRES_USER:-systemprompt}}"]
64 interval: 5s
65 timeout: 5s
66 retries: 5
67 networks:
68 - {container_name}_network
69
70volumes:
71 {container_name}_data:
72
73networks:
74 {container_name}_network:
75 driver: bridge
76"#
77 );
78
79 std::fs::write(&compose_file, compose_content)
80 .context("Failed to write docker-compose.yaml")?;
81
82 std::fs::write(init_scripts_dir.join("01-extensions.sql"), INIT_SCRIPT)
83 .context("Failed to write init script")?;
84
85 CliService::success(&format!("Created {}", compose_file.display()));
86
87 Ok(())
88}
89
90pub fn start_compose(
91 config: &PostgresConfig,
92 compose_dir: &Path,
93 container_name: &str,
94) -> Result<()> {
95 CliService::info("Starting PostgreSQL...");
96
97 let result = Command::new("docker")
98 .args(["compose", "up", "-d"])
99 .current_dir(compose_dir)
100 .env("POSTGRES_USER", &config.user)
101 .env("POSTGRES_PASSWORD", &config.password)
102 .env("POSTGRES_DB", &config.database)
103 .env("CONTAINER_NAME", container_name)
104 .output()
105 .context("Failed to run docker compose")?;
106
107 if !result.status.success() {
108 let stderr = String::from_utf8_lossy(&result.stderr);
109 anyhow::bail!("Failed to start PostgreSQL: {}", stderr);
110 }
111
112 CliService::success("PostgreSQL container started");
113 Ok(())
114}
115
116pub fn wait_for_postgres_ready(config: &PostgresConfig, container_name: &str) {
117 CliService::info("Waiting for PostgreSQL to be ready...");
118
119 for _ in 0..30 {
120 std::thread::sleep(std::time::Duration::from_secs(1));
121
122 let health = Command::new("docker")
123 .args([
124 "exec",
125 container_name,
126 "pg_isready",
127 "-U",
128 &config.user,
129 "-d",
130 &config.database,
131 ])
132 .output();
133
134 if health.is_ok_and(|o| o.status.success()) {
135 CliService::success("PostgreSQL is ready");
136 return;
137 }
138 }
139
140 CliService::warning("PostgreSQL started but health check timed out");
141}