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