systemprompt_cli/commands/cloud/profile/
templates.rs1use anyhow::{Context, Result};
11use regex::Regex;
12use std::path::Path;
13use systemprompt_cloud::constants::container;
14use systemprompt_logging::CliService;
15use systemprompt_models::{CliPaths, Profile};
16
17use crate::commands::cloud::init::templates::ai_config;
18
19use systemprompt_cloud::deploy::DockerfileBuilder;
20
21
22pub fn existing_geoip_database(profile_path: &Path) -> Option<String> {
23 let content = std::fs::read_to_string(profile_path).ok()?;
24 let geoip = match Profile::from_yaml(&content, profile_path) {
25 Ok(profile) => profile.paths.geoip_database,
26 Err(e) => {
27 tracing::warn!(
28 error = %e,
29 path = %profile_path.display(),
30 "existing profile unparseable; not carrying geoip_database forward"
31 );
32 None
33 },
34 }?;
35 if !geoip.starts_with(container::APP) {
36 tracing::warn!(
37 geoip_database = %geoip,
38 container_root = container::APP,
39 "geoip_database points outside the container app root; the container must provide \
40 this path or profile validation will fail at boot"
41 );
42 }
43 Some(geoip)
44}
45
46pub fn save_profile(profile: &Profile, profile_path: &Path) -> Result<()> {
47 let header = format!(
48 "# systemprompt.io Profile: {}\n# Generated by 'systemprompt cloud profile create'",
49 profile.display_name
50 );
51
52 crate::shared::profile::save_profile_yaml(profile, profile_path, Some(&header))
53}
54
55pub fn save_dockerfile(path: &Path, profile_name: &str, project_root: &Path) -> Result<()> {
56 let content = DockerfileBuilder::new(project_root)
57 .with_profile(profile_name)
58 .build();
59
60 std::fs::write(path, &content)
61 .with_context(|| format!("Failed to write {}", path.display()))?;
62
63 Ok(())
64}
65
66pub fn save_entrypoint(path: &Path) -> Result<()> {
67 let content = format!(
68 r"#!/bin/sh
69set -e
70
71exec {bin}/systemprompt {services_serve_cmd} --foreground
72",
73 bin = container::BIN,
74 services_serve_cmd = CliPaths::services_serve_cmd(),
75 );
76
77 if let Some(parent) = path.parent() {
78 std::fs::create_dir_all(parent)
79 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
80 }
81
82 std::fs::write(path, &content)
83 .with_context(|| format!("Failed to write {}", path.display()))?;
84
85 #[cfg(unix)]
86 {
87 use std::os::unix::fs::PermissionsExt;
88 let permissions = std::fs::Permissions::from_mode(0o755);
89 std::fs::set_permissions(path, permissions)
90 .with_context(|| format!("Failed to set permissions on {}", path.display()))?;
91 }
92
93 Ok(())
94}
95
96pub fn save_dockerignore(path: &Path) -> Result<()> {
97 let content = r".git
98.gitignore
99.gitmodules
100target/debug
101target/release/.fingerprint
102target/release/build
103target/release/deps
104target/release/examples
105target/release/incremental
106target/release/.cargo-lock
107.cargo
108.systemprompt/credentials.json
109.systemprompt/tenants.json
110.systemprompt/**/secrets.json
111.systemprompt/docker
112.systemprompt/storage
113.env*
114backup
115docs
116instructions
117*.md
118web/node_modules
119.vscode
120.idea
121logs
122*.log
123plan
124";
125
126 if let Some(parent) = path.parent() {
127 std::fs::create_dir_all(parent)
128 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
129 }
130
131 std::fs::write(path, content).with_context(|| format!("Failed to write {}", path.display()))?;
132
133 Ok(())
134}
135
136#[derive(Debug)]
137pub struct DatabaseUrls<'a> {
138 pub external: &'a str,
139 pub internal: Option<&'a str>,
140}
141
142pub fn save_secrets(
143 db_urls: &DatabaseUrls<'_>,
144 api_keys: &super::api_keys::ApiKeys,
145 secrets_path: &Path,
146 _is_cloud_tenant: bool,
147) -> Result<()> {
148 use serde_json::json;
149 use systemprompt_models::Profile;
150
151 if Profile::is_masked_database_url(db_urls.external) {
152 CliService::warning(
153 "Database URL appears to be masked. Credentials may not work correctly.",
154 );
155 CliService::warning(
156 "Run 'systemprompt cloud tenant refresh-credentials' to fetch real credentials.",
157 );
158 }
159
160 if let Some(internal) = db_urls.internal
161 && Profile::is_masked_database_url(internal)
162 {
163 CliService::warning(
164 "Internal database URL appears to be masked. Credentials may not work correctly.",
165 );
166 }
167
168 if let Some(parent) = secrets_path.parent() {
169 std::fs::create_dir_all(parent)
170 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
171 }
172
173 let identity = crate::shared::generate_identity()?;
174 let mut secrets = json!({
175 "oauth_at_rest_pepper": identity.oauth_at_rest_pepper,
176 "encryption_master_key": identity.encryption_master_key,
177 "manifest_signing_secret_seed": identity.manifest_signing_secret_seed,
178 "signing_key_pem": identity.signing_key_pem,
179 "database_url": db_urls.external,
180 "external_database_url": db_urls.external,
181 "gemini": api_keys.gemini,
182 "anthropic": api_keys.anthropic,
183 "openai": api_keys.openai
184 });
185
186 if let Some(internal) = db_urls.internal {
187 secrets["internal_database_url"] = json!(internal);
188 }
189
190 let content = serde_json::to_string_pretty(&secrets).context("Failed to serialize secrets")?;
191
192 std::fs::write(secrets_path, content)
193 .with_context(|| format!("Failed to write {}", secrets_path.display()))?;
194
195 #[cfg(unix)]
196 {
197 use std::os::unix::fs::PermissionsExt;
198 let permissions = std::fs::Permissions::from_mode(0o600);
199 std::fs::set_permissions(secrets_path, permissions)
200 .with_context(|| format!("Failed to set permissions on {}", secrets_path.display()))?;
201 }
202
203 Ok(())
204}
205
206pub fn get_services_path() -> Result<String> {
207 if let Ok(path) = std::env::var("SYSTEMPROMPT_SERVICES_PATH") {
208 return Ok(path);
209 }
210
211 let cwd = std::env::current_dir().context("Failed to get current directory")?;
212 let services_path = cwd.join("services");
213
214 Ok(services_path.to_string_lossy().to_string())
215}
216
217pub async fn validate_connection(db_url: &str) -> bool {
218 use tokio::time::{Duration, timeout};
219
220 let result = timeout(Duration::from_secs(5), async {
221 sqlx::postgres::PgPoolOptions::new()
222 .max_connections(1)
223 .connect(db_url)
224 .await
225 })
226 .await;
227
228 matches!(result, Ok(Ok(_)))
229}
230
231pub fn run_migrations_cmd(profile_path: &Path) -> Result<()> {
232 use std::process::Command;
233
234 CliService::info("Running database migrations...");
235
236 let current_exe = std::env::current_exe().context("Failed to get executable path")?;
237 let profile_path_str = profile_path.to_string_lossy();
238
239 let output = Command::new(¤t_exe)
240 .args(CliPaths::db_migrate_args())
241 .env("SYSTEMPROMPT_PROFILE", profile_path_str.as_ref())
242 .output()
243 .context("Failed to run migrations")?;
244
245 if output.status.success() {
246 CliService::success("Migrations completed");
247 return Ok(());
248 }
249
250 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
251 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
252
253 let error_output = if !stderr.is_empty() {
254 stderr
255 } else if !stdout.is_empty() {
256 stdout
257 } else {
258 "Unknown error (no output)".to_owned()
259 };
260
261 anyhow::bail!("Migration failed: {}", error_output)
262}
263
264pub fn update_ai_config_default_provider(provider: &str) -> Result<()> {
265 let services_path = get_services_path()?;
266 let ai_dir = Path::new(&services_path).join("ai");
267 let ai_config_path = ai_dir.join("config.yaml");
268
269 if !ai_config_path.exists() {
270 CliService::warning("AI config not found. Creating services/ai/config.yaml");
271 CliService::info("Run 'systemprompt cloud init' for full project setup");
272
273 std::fs::create_dir_all(&ai_dir)
274 .with_context(|| format!("Failed to create directory {}", ai_dir.display()))?;
275 std::fs::write(&ai_config_path, ai_config(provider)?)
276 .with_context(|| format!("Failed to write {}", ai_config_path.display()))?;
277 CliService::success(&format!("Created: {}", ai_config_path.display()));
278 return Ok(());
279 }
280
281 let content = std::fs::read_to_string(&ai_config_path)
282 .with_context(|| format!("Failed to read {}", ai_config_path.display()))?;
283 let re = Regex::new(r#"default_provider:\s*"?\w+"?"#).context("Failed to compile regex")?;
284 let updated = re.replace(&content, format!(r#"default_provider: "{}""#, provider));
285
286 std::fs::write(&ai_config_path, updated.as_ref())
287 .with_context(|| format!("Failed to write {}", ai_config_path.display()))?;
288 CliService::success(&format!(
289 "Updated default_provider to '{}' in AI config",
290 provider
291 ));
292 Ok(())
293}