systemprompt_cli/commands/admin/setup/
docker.rs1use 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}