Skip to main content

systemprompt_cli/commands/admin/setup/
docker_compose.rs

1use 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}