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