systemprompt_cloud/tenants/
mod.rs1mod provisioning;
13mod tenant_store;
14
15use serde::{Deserialize, Serialize};
16use systemprompt_identifiers::TenantId;
17use validator::Validate;
18
19use crate::api_client::TenantInfo;
20
21pub use provisioning::{
22 ProvisionedTenant, ProvisioningProgress, ProvisioningProgressEvent, TenantCreatePlan,
23 TenantProvisioningService, swap_to_external_host,
24};
25pub use tenant_store::TenantStore;
26
27#[derive(Debug)]
28pub struct NewCloudTenantParams {
29 pub id: TenantId,
30 pub name: String,
31 pub app_id: Option<String>,
32 pub hostname: Option<String>,
33 pub region: Option<String>,
34 pub database_url: Option<String>,
35 pub internal_database_url: String,
36 pub external_db_access: bool,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
40#[serde(rename_all = "snake_case")]
41pub enum TenantType {
42 #[default]
43 Local,
44 Cloud,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
48pub struct StoredTenant {
49 pub id: TenantId,
50
51 #[validate(length(min = 1, message = "Tenant name cannot be empty"))]
52 pub name: String,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub app_id: Option<String>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub hostname: Option<String>,
59
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub region: Option<String>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub database_url: Option<String>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub internal_database_url: Option<String>,
68
69 #[serde(default)]
70 pub tenant_type: TenantType,
71
72 #[serde(default)]
73 pub external_db_access: bool,
74
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub shared_container_db: Option<String>,
77}
78
79impl StoredTenant {
80 #[must_use]
81 pub fn new(id: TenantId, name: String) -> Self {
82 Self {
83 id,
84 name,
85 app_id: None,
86 hostname: None,
87 region: None,
88 database_url: None,
89 internal_database_url: None,
90 tenant_type: TenantType::default(),
91 external_db_access: false,
92 shared_container_db: None,
93 }
94 }
95
96 #[must_use]
97 pub const fn new_local(id: TenantId, name: String, database_url: String) -> Self {
98 Self {
99 id,
100 name,
101 app_id: None,
102 hostname: None,
103 region: None,
104 database_url: Some(database_url),
105 internal_database_url: None,
106 tenant_type: TenantType::Local,
107 external_db_access: false,
108 shared_container_db: None,
109 }
110 }
111
112 #[must_use]
113 pub const fn new_local_shared(
114 id: TenantId,
115 name: String,
116 database_url: String,
117 shared_container_db: String,
118 ) -> Self {
119 Self {
120 id,
121 name,
122 app_id: None,
123 hostname: None,
124 region: None,
125 database_url: Some(database_url),
126 internal_database_url: None,
127 tenant_type: TenantType::Local,
128 external_db_access: false,
129 shared_container_db: Some(shared_container_db),
130 }
131 }
132
133 #[must_use]
134 pub fn new_cloud(params: NewCloudTenantParams) -> Self {
135 Self {
136 id: params.id,
137 name: params.name,
138 app_id: params.app_id,
139 hostname: params.hostname,
140 region: params.region,
141 database_url: params.database_url,
142 internal_database_url: Some(params.internal_database_url),
143 tenant_type: TenantType::Cloud,
144 external_db_access: params.external_db_access,
145 shared_container_db: None,
146 }
147 }
148
149 #[must_use]
150 pub fn from_tenant_info(info: &TenantInfo) -> Self {
151 Self {
152 id: TenantId::new(info.id.clone()),
153 name: info.name.clone(),
154 app_id: info.app_id.clone(),
155 hostname: info.hostname.clone(),
156 region: info.region.clone(),
157 database_url: None,
158 internal_database_url: Some(info.database_url.clone()),
159 tenant_type: TenantType::Cloud,
160 external_db_access: info.external_db_access,
161 shared_container_db: None,
162 }
163 }
164
165 #[must_use]
166 pub const fn uses_shared_container(&self) -> bool {
167 self.shared_container_db.is_some()
168 }
169
170 #[must_use]
171 pub fn has_database_url(&self) -> bool {
172 match self.tenant_type {
173 TenantType::Cloud => self
174 .internal_database_url
175 .as_ref()
176 .is_some_and(|url| !url.is_empty()),
177 TenantType::Local => self
178 .database_url
179 .as_ref()
180 .is_some_and(|url| !url.is_empty()),
181 }
182 }
183
184 #[must_use]
185 pub fn get_local_database_url(&self) -> Option<&String> {
186 self.database_url
187 .as_ref()
188 .or(self.internal_database_url.as_ref())
189 }
190
191 #[must_use]
192 pub const fn is_cloud(&self) -> bool {
193 matches!(self.tenant_type, TenantType::Cloud)
194 }
195
196 #[must_use]
197 pub const fn is_local(&self) -> bool {
198 matches!(self.tenant_type, TenantType::Local)
199 }
200
201 pub fn update_from_tenant_info(&mut self, info: &TenantInfo) {
202 self.name.clone_from(&info.name);
203 self.app_id.clone_from(&info.app_id);
204 self.hostname.clone_from(&info.hostname);
205 self.region.clone_from(&info.region);
206 self.external_db_access = info.external_db_access;
207
208 if !info.database_url.contains(":***@") {
209 self.internal_database_url = Some(info.database_url.clone());
210 }
211 }
212
213 #[must_use]
214 pub fn is_database_url_masked(&self) -> bool {
215 self.internal_database_url
216 .as_ref()
217 .is_some_and(|url| url.contains(":***@") || url.contains(":********@"))
218 }
219
220 #[must_use]
221 pub fn has_missing_credentials(&self) -> bool {
222 self.tenant_type == TenantType::Cloud && self.is_database_url_masked()
223 }
224}