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 std::process::Command;
8use systemprompt_cloud::constants::docker::{COMPOSE_PATH, container_name};
9use systemprompt_logging::CliService;
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
61pub(super) async fn setup_docker_postgres_interactive(
62    args: &SetupArgs,
63    prompter: &dyn Prompter,
64    env_name: &str,
65) -> Result<PostgresConfig> {
66    CliService::info("Setting up PostgreSQL with Docker...");
67
68    if !is_docker_available() {
69        anyhow::bail!(
70            "Docker is not installed or not in PATH.\nInstall Docker: https://docs.docker.com/get-docker/"
71        );
72    }
73
74    if !is_compose_available() {
75        anyhow::bail!(
76            "Docker Compose is not available.\nEnsure Docker Desktop is installed or install \
77             docker-compose."
78        );
79    }
80
81    CliService::success("Docker and Docker Compose are available");
82
83    let user = prompter.input_with_default("Database user", &args.effective_db_user(env_name))?;
84
85    let password = args.db_password.clone().unwrap_or_else(generate_password);
86    CliService::success(&format!("Generated password: {}", password));
87
88    let database =
89        prompter.input_with_default("Database name", &args.effective_db_name(env_name))?;
90
91    let port_input = prompter.input_with_default("PostgreSQL port", &args.db_port.to_string())?;
92    let port: u16 = port_input
93        .trim()
94        .parse()
95        .with_context(|| format!("Invalid PostgreSQL port: {}", port_input))?;
96
97    let config = PostgresConfig {
98        host: "localhost".to_owned(),
99        port,
100        user,
101        password,
102        database,
103    };
104
105    if test_connection(&config).await {
106        CliService::success(&format!(
107            "Reusing PostgreSQL already listening at {}:{}",
108            config.host, config.port
109        ));
110        enable_extensions(&config).await?;
111        return Ok(config);
112    }
113
114    let compose_dir = std::env::current_dir()?.join(COMPOSE_PATH);
115    let container = container_name(env_name);
116    create_compose_files_if_missing(&compose_dir, &container, port)?;
117    if is_container_running(&container) {
118        CliService::info(&format!(
119            "PostgreSQL container '{}' is already running",
120            container
121        ));
122
123        let reuse = prompter.confirm("Use existing container?", true)?;
124
125        if reuse {
126            if !test_connection(&config).await {
127                CliService::info("Creating database and user in existing container...");
128                create_database_in_docker(&config, &container).await?;
129            }
130            enable_extensions(&config).await?;
131            return Ok(config);
132        }
133
134        CliService::info("Stopping existing container...");
135        if let Err(e) = Command::new("docker").args(["stop", &container]).output() {
136            tracing::warn!(container = %container, error = %e, "docker stop failed");
137        }
138        if let Err(e) = Command::new("docker").args(["rm", &container]).output() {
139            tracing::warn!(container = %container, error = %e, "docker rm failed");
140        }
141    }
142
143    ensure_port_free(&config)?;
144    start_compose(&config, &compose_dir, &container)?;
145
146    wait_for_postgres_ready(&config, &container);
147
148    enable_extensions(&config).await?;
149
150    Ok(config)
151}
152
153fn ensure_port_free(config: &PostgresConfig) -> Result<()> {
154    if detect_postgresql(&config.host, config.port) {
155        anyhow::bail!(
156            "Port {}:{} is already in use by a server that does not accept the configured \
157             credentials. Reuse that database (fix the credentials) or stop whatever is bound to \
158             the port, then re-run setup.",
159            config.host,
160            config.port
161        );
162    }
163    Ok(())
164}