Skip to main content

systemprompt_cli/commands/cloud/tenant/create/
local.rs

1//! Local tenant creation flows.
2//!
3//! [`create_local_tenant`] provisions a Docker `PostgreSQL` container owned by
4//! that tenant alone; [`create_external_tenant`] registers a user-supplied
5//! database after validating the connection. Both then scaffold a local
6//! profile.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use anyhow::{Context, Result, bail};
12use systemprompt_cloud::{DockerCli, ProjectContext, StoredTenant};
13use systemprompt_logging::CliService;
14
15use crate::cloud::init::ensure_project_scaffolding;
16use crate::cloud::profile::templates::validate_connection;
17use crate::cloud::profile::{
18    collect_api_keys, create_profile_for_tenant, handle_local_tenant_setup,
19};
20use crate::interactive::Prompter;
21
22use super::super::docker::{
23    TenantContainer, generate_admin_password, is_project_running, nanoid, new_local_tenant_id,
24    remove_project, start_project,
25};
26
27use super::sanitize_database_name;
28
29pub async fn create_local_tenant(prompter: &dyn Prompter) -> Result<StoredTenant> {
30    CliService::section("Create Local PostgreSQL Tenant");
31
32    let name = prompter.input_with_default("Tenant name", "local")?;
33
34    if name.is_empty() {
35        bail!("Tenant name cannot be empty");
36    }
37
38    let project = format!("{}_{}", sanitize_database_name(&name), nanoid());
39
40    let port: u16 = prompter
41        .input_with_default("PostgreSQL port", "5432")?
42        .parse()
43        .context("PostgreSQL port must be a number")?;
44
45    let docker = DockerCli::new();
46
47    if is_project_running(&docker, &project) {
48        bail!("A container for project '{project}' is already running");
49    }
50
51    let container = TenantContainer::new(project.clone(), generate_admin_password(), port);
52
53    let spinner = CliService::spinner("Starting PostgreSQL container...");
54    let started = start_project(&docker, &container).await;
55    spinner.finish_and_clear();
56
57    if let Err(e) = started {
58        remove_project(&docker, &project).ok();
59        return Err(e);
60    }
61    CliService::success(&format!("PostgreSQL container '{project}' is ready"));
62
63    let database_url = container.database_url();
64
65    let id = new_local_tenant_id();
66    let tenant = StoredTenant::new_local_docker(id, name.clone(), database_url.clone(), project);
67
68    setup_local_profile(&tenant, &name, &database_url, prompter).await?;
69
70    Ok(tenant)
71}
72
73pub async fn create_external_tenant(prompter: &dyn Prompter) -> Result<StoredTenant> {
74    CliService::section("Create Local Tenant (External PostgreSQL)");
75
76    let name = prompter.input_with_default("Tenant name", "local")?;
77
78    if name.is_empty() {
79        bail!("Tenant name cannot be empty");
80    }
81
82    let database_url = prompter.input("PostgreSQL connection URL")?;
83
84    if database_url.is_empty() {
85        bail!("Database URL cannot be empty");
86    }
87
88    let spinner = CliService::spinner("Validating connection...");
89    let valid = validate_connection(&database_url).await;
90    spinner.finish_and_clear();
91
92    if !valid {
93        bail!("Could not connect to database. Check your connection URL and try again.");
94    }
95    CliService::success("Database connection verified");
96
97    let id = new_local_tenant_id();
98    let tenant = StoredTenant::new_local(id, name.clone(), database_url.clone());
99
100    setup_local_profile(&tenant, &name, &database_url, prompter).await?;
101
102    Ok(tenant)
103}
104
105async fn setup_local_profile(
106    tenant: &StoredTenant,
107    name: &str,
108    database_url: &str,
109    prompter: &dyn Prompter,
110) -> Result<()> {
111    CliService::section("Profile Setup");
112    let profile_name = prompter.input_with_default("Profile name", name)?;
113
114    CliService::section("API Keys");
115    let api_keys = collect_api_keys(prompter)?;
116
117    let profile = create_profile_for_tenant(prompter, tenant, &api_keys, &profile_name, None)?;
118    CliService::success(&format!("Profile '{}' created", profile.name));
119
120    let ctx = ProjectContext::discover();
121    ensure_project_scaffolding(ctx.root())?;
122
123    let profile_path = ctx.profile_dir(&profile.name).join("profile.yaml");
124    handle_local_tenant_setup(prompter, database_url, name, &profile_path).await?;
125
126    Ok(())
127}