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