systemprompt_cli/commands/admin/setup/
postgres.rs1use anyhow::{Context, Result, anyhow};
13use sqlx::postgres::PgPoolOptions;
14use std::time::Duration;
15use systemprompt_logging::CliService;
16
17use super::SetupArgs;
18use super::common::{
19 PostgresConfig, detect_postgresql, enable_extensions, generate_password, test_connection,
20};
21use crate::CliConfig;
22use crate::interactive::Prompter;
23
24pub async fn setup_non_interactive(
25 args: &SetupArgs,
26 env_name: &str,
27 cli_config: &CliConfig,
28) -> Result<PostgresConfig> {
29 if !cli_config.is_json_output() {
30 CliService::section(&format!("PostgreSQL Setup ({})", env_name));
31 }
32
33 let password = args.db_password.clone().unwrap_or_else(generate_password);
34 let config = PostgresConfig {
35 host: args.db_host.clone(),
36 port: args.db_port,
37 user: args.effective_db_user(env_name),
38 password,
39 database: args.effective_db_name(env_name),
40 };
41
42 if !cli_config.is_json_output() {
43 CliService::key_value("Host", &config.host);
44 CliService::key_value("Port", &config.port.to_string());
45 CliService::key_value("User", &config.user);
46 CliService::key_value("Database", &config.database);
47 }
48
49 if args.docker {
50 if !cli_config.is_json_output() {
51 CliService::info("Setting up PostgreSQL with Docker...");
52 }
53 return super::docker::setup_docker_postgres_non_interactive(&config, env_name).await;
54 }
55
56 if detect_postgresql(&config.host, config.port) {
57 if !cli_config.is_json_output() {
58 CliService::success(&format!(
59 "PostgreSQL reachable at {}:{}",
60 config.host, config.port
61 ));
62 }
63 } else if !cli_config.is_json_output() {
64 CliService::warning(&format!(
65 "PostgreSQL not reachable at {}:{}",
66 config.host, config.port
67 ));
68 CliService::info("Continuing with provided configuration...");
69 }
70
71 if test_connection(&config).await {
72 if !cli_config.is_json_output() {
73 CliService::success("Database connection successful");
74 }
75 enable_extensions(&config).await?;
76 } else if !cli_config.is_json_output() {
77 CliService::warning("Cannot connect to database - it may need to be created manually");
78 }
79
80 Ok(config)
81}
82
83pub async fn setup_interactive(
84 args: &SetupArgs,
85 prompter: &dyn Prompter,
86 env_name: &str,
87 _cli_config: &CliConfig,
88) -> Result<PostgresConfig> {
89 CliService::section(&format!("PostgreSQL Setup ({})", env_name));
90 CliService::info("Configure PostgreSQL database for your local environment.");
91
92 let options = vec![
93 "Use existing PostgreSQL installation".to_owned(),
94 "Start PostgreSQL with Docker".to_owned(),
95 ];
96
97 let selection = prompter.select("How would you like to set up PostgreSQL?", &options)?;
98
99 match selection {
100 0 => setup_existing_postgres(args, prompter, env_name).await,
101 1 => super::docker::setup_docker_postgres_interactive(args, prompter, env_name).await,
102 _ => Err(anyhow!("Invalid PostgreSQL setup option selected")),
103 }
104}
105
106async fn setup_existing_postgres(
107 args: &SetupArgs,
108 prompter: &dyn Prompter,
109 env_name: &str,
110) -> Result<PostgresConfig> {
111 CliService::info("Configuring existing PostgreSQL connection...");
112
113 let (host, port) = prompt_host_port(args, prompter)?;
114
115 let user = prompter.input_with_default("Database user", &args.effective_db_user(env_name))?;
116
117 let password = prompt_password(args, prompter)?;
118
119 let database =
120 prompter.input_with_default("Database name", &args.effective_db_name(env_name))?;
121
122 let config = PostgresConfig {
123 host,
124 port,
125 user,
126 password,
127 database,
128 };
129
130 verify_or_create_database(&config, prompter).await?;
131
132 Ok(config)
133}
134
135fn prompt_host_port(args: &SetupArgs, prompter: &dyn Prompter) -> Result<(String, u16)> {
136 let host = prompter.input_with_default("PostgreSQL host", &args.db_host)?;
137
138 let port_input = prompter.input_with_default("PostgreSQL port", &args.db_port.to_string())?;
139 let port: u16 = port_input
140 .trim()
141 .parse()
142 .with_context(|| format!("Invalid PostgreSQL port: {}", port_input))?;
143
144 if detect_postgresql(&host, port) {
145 CliService::success(&format!("PostgreSQL reachable at {}:{}", host, port));
146 } else {
147 CliService::warning(&format!("Cannot reach PostgreSQL at {}:{}", host, port));
148 let continue_anyway = prompter.confirm("Continue anyway?", false)?;
149
150 if !continue_anyway {
151 anyhow::bail!("PostgreSQL not reachable. Please start PostgreSQL and try again.");
152 }
153 }
154
155 Ok((host, port))
156}
157
158fn prompt_password(args: &SetupArgs, prompter: &dyn Prompter) -> Result<String> {
159 let password = if let Some(ref pw) = args.db_password {
160 pw.clone()
161 } else {
162 let use_generated = prompter.confirm("Generate a secure password automatically?", true)?;
163
164 if use_generated {
165 let generated = generate_password();
166 CliService::success(&format!("Generated password: {}", generated));
167 generated
168 } else {
169 prompter.password("Database password")?
170 }
171 };
172
173 if password.is_empty() {
174 anyhow::bail!("Password is required");
175 }
176
177 Ok(password)
178}
179
180async fn verify_or_create_database(config: &PostgresConfig, prompter: &dyn Prompter) -> Result<()> {
181 if test_connection(config).await {
182 CliService::success("Successfully connected to database!");
183 enable_extensions(config).await?;
184 return Ok(());
185 }
186
187 CliService::warning("Cannot connect with provided credentials.");
188 CliService::info("The database or user may not exist yet.");
189
190 let create_db = prompter.confirm("Create database and user now? (requires superuser)", true)?;
191
192 if create_db {
193 create_database_interactive(config, prompter).await?;
194 enable_extensions(config).await?;
195 } else {
196 CliService::warning("Skipping database creation. You may need to create it manually.");
197 }
198
199 Ok(())
200}
201
202async fn create_database_interactive(
203 config: &PostgresConfig,
204 prompter: &dyn Prompter,
205) -> Result<()> {
206 CliService::info("Enter PostgreSQL superuser credentials (typically 'postgres'):");
207
208 let superuser = prompter.input_with_default("Superuser name", "postgres")?;
209
210 let superpass = prompter.password("Superuser password")?;
211
212 if superpass.is_empty() {
213 anyhow::bail!("Superuser password is required");
214 }
215
216 let super_url = format!(
217 "postgres://{}:{}@{}:{}/postgres",
218 superuser, superpass, config.host, config.port
219 );
220
221 let pool = PgPoolOptions::new()
222 .max_connections(1)
223 .acquire_timeout(Duration::from_secs(5))
224 .connect(&super_url)
225 .await
226 .context("Failed to connect with superuser credentials")?;
227
228 let user_exists: bool = sqlx::query_scalar!(
229 "SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = $1)",
230 &config.user
231 )
232 .fetch_one(&pool)
233 .await?
234 .unwrap_or(false);
235
236 if !user_exists {
237 CliService::info(&format!("Creating user '{}'...", config.user));
238 let create_user_sql = super::ddl::build_create_user_sql(&config.user, &config.password);
239 sqlx::query(sqlx::AssertSqlSafe(create_user_sql))
240 .execute(&pool)
241 .await?;
242 CliService::success(&format!("Created user '{}'", config.user));
243 }
244
245 let db_exists: bool = sqlx::query_scalar!(
246 "SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)",
247 &config.database
248 )
249 .fetch_one(&pool)
250 .await?
251 .unwrap_or(false);
252
253 if !db_exists {
254 CliService::info(&format!("Creating database '{}'...", config.database));
255 let create_db_sql = super::ddl::build_create_db_sql(&config.database, &config.user);
256 sqlx::query(sqlx::AssertSqlSafe(create_db_sql))
257 .execute(&pool)
258 .await?;
259 CliService::success(&format!("Created database '{}'", config.database));
260 }
261
262 let grant_sql = super::ddl::build_grant_sql(&config.database, &config.user);
263 sqlx::query(sqlx::AssertSqlSafe(grant_sql))
264 .execute(&pool)
265 .await?;
266
267 pool.close().await;
268
269 CliService::success("Database and user setup complete");
270 Ok(())
271}