Skip to main content

systemprompt_cli/commands/admin/setup/
docker.rs

1//! Docker availability checks for setup.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use anyhow::{Context, Result};
7use systemprompt_cloud::constants::docker::{COMPOSE_PATH, container_name};
8use systemprompt_logging::CliService;
9use tokio::process::Command;
10
11use super::SetupArgs;
12use super::common::{
13    PostgresConfig, detect_postgresql, enable_extensions, generate_password, test_connection,
14};
15use super::docker_compose::{
16    create_compose_files_if_missing, is_compose_available, is_container_running,
17    is_docker_available, start_compose, wait_for_postgres_ready,
18};
19use super::docker_database::create_database_in_docker;
20use crate::interactive::Prompter;
21
22pub async fn setup_docker_postgres_non_interactive(
23    config: &PostgresConfig,
24    env_name: &str,
25) -> Result<PostgresConfig> {
26    if test_connection(config).await {
27        CliService::success(&format!(
28            "Reusing PostgreSQL already listening at {}:{}",
29            config.host, config.port
30        ));
31        enable_extensions(config).await?;
32        return Ok(config.clone());
33    }
34
35    if !is_docker_available() {
36        anyhow::bail!("Docker is not installed or not in PATH.");
37    }
38    if !is_compose_available() {
39        anyhow::bail!("Docker Compose is not available.");
40    }
41
42    let compose_dir = std::env::current_dir()?.join(COMPOSE_PATH);
43    let container = container_name(env_name);
44
45    if is_container_running(&container) {
46        create_compose_files_if_missing(&compose_dir, &container, config.port)?;
47        create_database_in_docker(config, &container).await?;
48        enable_extensions(config).await?;
49        return Ok(config.clone());
50    }
51
52    ensure_port_free(config)?;
53    create_compose_files_if_missing(&compose_dir, &container, config.port)?;
54    start_compose(config, &compose_dir, &container)?;
55    wait_for_postgres_ready(config, &container);
56    enable_extensions(config).await?;
57
58    Ok(config.clone())
59}
60
61async fn remove_container(container: &str) {
62    for action in ["stop", "rm"] {
63        if let Err(e) = Command::new("docker")
64            .args([action, container])
65            .output()
66            .await
67        {
68            tracing::warn!(container = %container, action, error = %e, "docker container removal step failed");
69        }
70    }
71}
72
73pub(super) async fn setup_docker_postgres_interactive(
74    args: &SetupArgs,
75    prompter: &dyn Prompter,
76    env_name: &str,
77) -> Result<PostgresConfig> {
78    CliService::info("Setting up PostgreSQL with Docker...");
79
80    if !is_docker_available() {
81        anyhow::bail!(
82            "Docker is not installed or not in PATH.\nInstall Docker: https://docs.docker.com/get-docker/"
83        );
84    }
85
86    if !is_compose_available() {
87        anyhow::bail!(
88            "Docker Compose is not available.\nEnsure Docker Desktop is installed or install \
89             docker-compose."
90        );
91    }
92
93    CliService::success("Docker and Docker Compose are available");
94
95    let user = prompter.input_with_default("Database user", &args.effective_db_user(env_name))?;
96
97    let password = args.db_password.clone().unwrap_or_else(generate_password);
98    CliService::success(&format!("Generated password: {}", password));
99
100    let database =
101        prompter.input_with_default("Database name", &args.effective_db_name(env_name))?;
102
103    let port_input = prompter.input_with_default("PostgreSQL port", &args.db_port.to_string())?;
104    let port: u16 = port_input
105        .trim()
106        .parse()
107        .with_context(|| format!("Invalid PostgreSQL port: {}", port_input))?;
108
109    let config = PostgresConfig {
110        host: "localhost".to_owned(),
111        port,
112        user,
113        password,
114        database,
115    };
116
117    if test_connection(&config).await {
118        CliService::success(&format!(
119            "Reusing PostgreSQL already listening at {}:{}",
120            config.host, config.port
121        ));
122        enable_extensions(&config).await?;
123        return Ok(config);
124    }
125
126    let compose_dir = std::env::current_dir()?.join(COMPOSE_PATH);
127    let container = container_name(env_name);
128    create_compose_files_if_missing(&compose_dir, &container, port)?;
129    if is_container_running(&container) {
130        CliService::info(&format!(
131            "PostgreSQL container '{}' is already running",
132            container
133        ));
134
135        let reuse = prompter.confirm("Use existing container?", true)?;
136
137        if reuse {
138            if !test_connection(&config).await {
139                CliService::info("Creating database and user in existing container...");
140                create_database_in_docker(&config, &container).await?;
141            }
142            enable_extensions(&config).await?;
143            return Ok(config);
144        }
145
146        CliService::info("Stopping existing container...");
147        remove_container(&container).await;
148    }
149
150    ensure_port_free(&config)?;
151    start_compose(&config, &compose_dir, &container)?;
152
153    wait_for_postgres_ready(&config, &container);
154
155    enable_extensions(&config).await?;
156
157    Ok(config)
158}
159
160fn ensure_port_free(config: &PostgresConfig) -> Result<()> {
161    if detect_postgresql(&config.host, config.port) {
162        anyhow::bail!(
163            "Port {}:{} is already in use by a server that does not accept the configured \
164             credentials. Reuse that database (fix the credentials) or stop whatever is bound to \
165             the port, then re-run setup.",
166            config.host,
167            config.port
168        );
169    }
170    Ok(())
171}