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