systemprompt_cli/commands/cloud/tenant/docker/
database.rs1use anyhow::{Context, Result, bail};
7use systemprompt_cloud::DockerCli;
8use systemprompt_logging::CliService;
9
10use super::config::{SHARED_ADMIN_USER, SHARED_CONTAINER_NAME};
11
12pub fn sanitize_database_name(name: &str) -> String {
13 name.chars()
14 .map(|c| {
15 if c.is_ascii_alphanumeric() || c == '_' {
16 c
17 } else {
18 '_'
19 }
20 })
21 .collect()
22}
23
24pub fn create_database_for_tenant(
25 docker: &DockerCli,
26 admin_password: &str,
27 port: u16,
28 db_name: &str,
29) -> Result<()> {
30 let database_url = format!(
31 "postgres://{}:{}@localhost:{}/postgres",
32 SHARED_ADMIN_USER, admin_password, port
33 );
34
35 let safe_db_name = sanitize_database_name(db_name);
36
37 let check_query = format!(
38 "SELECT 1 FROM pg_database WHERE datname = '{}'",
39 safe_db_name
40 );
41 let check_output = docker
42 .output(&[
43 "exec",
44 SHARED_CONTAINER_NAME,
45 "psql",
46 &database_url,
47 "-tAc",
48 &check_query,
49 ])
50 .with_context(|| {
51 format!(
52 "failed to run `docker exec {SHARED_CONTAINER_NAME} psql` checking for database \
53 {safe_db_name}"
54 )
55 })?;
56
57 let exists = !String::from_utf8_lossy(&check_output.stdout)
58 .trim()
59 .is_empty();
60
61 if exists {
62 CliService::info(&format!("Database '{}' already exists", safe_db_name));
63 return Ok(());
64 }
65
66 let create_query = format!("CREATE DATABASE \"{}\"", safe_db_name);
67 let status = docker
68 .status(&[
69 "exec",
70 SHARED_CONTAINER_NAME,
71 "psql",
72 &database_url,
73 "-c",
74 &create_query,
75 ])
76 .with_context(|| {
77 format!(
78 "failed to run `docker exec {SHARED_CONTAINER_NAME} psql` creating database \
79 {safe_db_name}"
80 )
81 })?;
82
83 if !status.success() {
84 bail!("Failed to create database '{}'", safe_db_name);
85 }
86
87 Ok(())
88}
89
90pub fn drop_database_for_tenant(
91 docker: &DockerCli,
92 admin_password: &str,
93 port: u16,
94 db_name: &str,
95) -> Result<()> {
96 let database_url = format!(
97 "postgres://{}:{}@localhost:{}/postgres",
98 SHARED_ADMIN_USER, admin_password, port
99 );
100
101 let safe_db_name = sanitize_database_name(db_name);
102
103 let terminate_query = format!(
104 "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{}' AND pid <> \
105 pg_backend_pid()",
106 safe_db_name
107 );
108 if let Err(e) = docker.status(&[
109 "exec",
110 SHARED_CONTAINER_NAME,
111 "psql",
112 &database_url,
113 "-c",
114 &terminate_query,
115 ]) {
116 tracing::debug!(
117 error = %e,
118 db = %safe_db_name,
119 "failed to run `docker exec {SHARED_CONTAINER_NAME} psql` terminating existing connections",
120 );
121 }
122
123 let drop_query = format!("DROP DATABASE IF EXISTS \"{}\"", safe_db_name);
124 let status = docker
125 .status(&[
126 "exec",
127 SHARED_CONTAINER_NAME,
128 "psql",
129 &database_url,
130 "-c",
131 &drop_query,
132 ])
133 .with_context(|| {
134 format!(
135 "failed to run `docker exec {SHARED_CONTAINER_NAME} psql` dropping database \
136 {safe_db_name}"
137 )
138 })?;
139
140 if !status.success() {
141 bail!("Failed to drop database '{}'", safe_db_name);
142 }
143
144 Ok(())
145}
146
147pub fn ensure_admin_role(docker: &DockerCli, admin_password: &str) -> Result<()> {
148 let role_check_query = format!(
149 "SELECT 1 FROM pg_roles WHERE rolname = '{}'",
150 SHARED_ADMIN_USER
151 );
152 let role_exists =
153 !admin_psql_capture(docker, &role_check_query, "checking admin role")?.is_empty();
154
155 if role_exists {
156 let alter_password_sql = format!(
157 "ALTER ROLE \"{}\" WITH PASSWORD '{}'",
158 SHARED_ADMIN_USER,
159 admin_password.replace('\'', "''")
160 );
161 if !admin_psql_execute(docker, &alter_password_sql, "updating admin role password")? {
162 bail!("Failed to update password for role '{}'", SHARED_ADMIN_USER);
163 }
164
165 return Ok(());
166 }
167
168 let create_role_sql = format!(
169 "CREATE ROLE \"{}\" WITH LOGIN CREATEDB SUPERUSER PASSWORD '{}'",
170 SHARED_ADMIN_USER,
171 admin_password.replace('\'', "''")
172 );
173 if !admin_psql_execute(docker, &create_role_sql, "creating admin role")? {
174 bail!("Failed to create role '{}'", SHARED_ADMIN_USER);
175 }
176
177 CliService::success(&format!("Created PostgreSQL role '{}'", SHARED_ADMIN_USER));
178 Ok(())
179}
180
181fn admin_psql_capture(docker: &DockerCli, sql: &str, action: &str) -> Result<String> {
182 let output = docker
183 .output(&[
184 "exec",
185 SHARED_CONTAINER_NAME,
186 "psql",
187 "-U",
188 SHARED_ADMIN_USER,
189 "-d",
190 "postgres",
191 "-tAc",
192 sql,
193 ])
194 .with_context(|| {
195 format!("failed to run `docker exec {SHARED_CONTAINER_NAME} psql` {action}")
196 })?;
197
198 Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
199}
200
201fn admin_psql_execute(docker: &DockerCli, sql: &str, action: &str) -> Result<bool> {
202 let status = docker
203 .status(&[
204 "exec",
205 SHARED_CONTAINER_NAME,
206 "psql",
207 "-U",
208 SHARED_ADMIN_USER,
209 "-d",
210 "postgres",
211 "-c",
212 sql,
213 ])
214 .with_context(|| {
215 format!("failed to run `docker exec {SHARED_CONTAINER_NAME} psql` {action}")
216 })?;
217
218 Ok(status.success())
219}