docbox_core/tenant/
create_tenant.rs1use crate::storage::StorageLayerFactory;
2use crate::utils::rollback::RollbackGuard;
3use docbox_database::migrations::apply_tenant_migrations;
4use docbox_database::models::tenant::TenantId;
5use docbox_database::{DbConnectErr, DbPool};
6use docbox_database::{DbErr, models::tenant::Tenant};
7use docbox_search::SearchIndexFactory;
8use std::ops::DerefMut;
9use thiserror::Error;
10
11pub struct CreateTenant {
13 pub env: String,
15
16 pub id: TenantId,
18
19 pub db_name: String,
21
22 pub db_secret_name: String,
24
25 pub s3_name: String,
27
28 pub os_index_name: String,
30
31 pub event_queue_url: Option<String>,
33
34 pub origins: Vec<String>,
36
37 pub s3_queue_arn: Option<String>,
40}
41
42#[derive(Debug, Error)]
43pub enum InitTenantError {
44 #[error("failed to connect")]
46 ConnectDb(#[from] DbConnectErr),
47
48 #[error(transparent)]
50 Database(#[from] DbErr),
51
52 #[error("tenant already exists")]
54 TenantAlreadyExist,
55
56 #[error("failed to create tenant s3 bucket: {0}")]
58 CreateS3Bucket(anyhow::Error),
59
60 #[error("failed to setup s3 notification rules: {0}")]
62 SetupS3Notifications(anyhow::Error),
63
64 #[error("failed to setup s3 CORS rules: {0}")]
66 SetupS3Cors(anyhow::Error),
67
68 #[error("failed to create tenant search index: {0}")]
70 CreateSearchIndex(anyhow::Error),
71}
72
73pub async fn create_tenant(
75 root_db: &DbPool,
76 tenant_db: &DbPool,
77
78 search: &SearchIndexFactory,
79 storage: &StorageLayerFactory,
80 create: CreateTenant,
81) -> Result<Tenant, InitTenantError> {
82 let mut root_transaction = root_db
84 .begin()
85 .await
86 .inspect_err(|error| tracing::error!(?error, "failed to begin root transaction"))?;
87
88 let tenant: Tenant = Tenant::create(
90 root_transaction.deref_mut(),
91 docbox_database::models::tenant::CreateTenant {
92 id: create.id,
93 db_name: create.db_name,
94 db_secret_name: create.db_secret_name,
95 s3_name: create.s3_name,
96 os_index_name: create.os_index_name,
97 event_queue_url: create.event_queue_url,
98 env: create.env,
99 },
100 )
101 .await
102 .map_err(|err| {
103 if let Some(db_err) = err.as_database_error() {
104 if db_err.is_unique_violation() {
106 return InitTenantError::TenantAlreadyExist;
107 }
108 }
109
110 InitTenantError::Database(err)
111 })
112 .inspect_err(|error| tracing::error!(?error, "failed to create tenant"))?;
113
114 let mut tenant_transaction = tenant_db
116 .begin()
117 .await
118 .inspect_err(|error| tracing::error!(?error, "failed to begin tenant transaction"))?;
119
120 apply_tenant_migrations(
122 &mut root_transaction,
123 &mut tenant_transaction,
124 &tenant,
125 None,
126 )
127 .await
128 .inspect_err(|error| tracing::error!(?error, "failed to create tenant tables"))?;
129
130 tracing::debug!("creating tenant storage");
132 let storage =
133 create_tenant_storage(&tenant, storage, create.s3_queue_arn, create.origins).await?;
134
135 tracing::debug!("creating tenant search index");
137 let search = create_tenant_search(&tenant, search).await?;
138
139 tenant_transaction
141 .commit()
142 .await
143 .inspect_err(|error| tracing::error!(?error, "failed to commit tenant transaction"))?;
144 root_transaction
145 .commit()
146 .await
147 .inspect_err(|error| tracing::error!(?error, "failed to commit root transaction"))?;
148
149 storage.commit();
151 search.commit();
152
153 Ok(tenant)
154}
155
156async fn create_tenant_storage(
158 tenant: &Tenant,
159 storage: &StorageLayerFactory,
160 s3_queue_arn: Option<String>,
161 origins: Vec<String>,
162) -> Result<RollbackGuard<impl FnOnce()>, InitTenantError> {
163 let storage = storage.create_storage_layer(tenant);
164 storage
165 .create_bucket()
166 .await
167 .inspect_err(|error| tracing::error!(?error, "failed to create tenant bucket"))
168 .map_err(InitTenantError::CreateS3Bucket)?;
169
170 let rollback = RollbackGuard::new({
171 let storage = storage.clone();
172 move || {
173 tokio::spawn(async move {
174 if let Err(error) = storage.delete_bucket().await {
175 tracing::error!(?error, "failed to rollback created tenant storage bucket");
176 }
177 });
178 }
179 });
180
181 if let Some(s3_queue_arn) = s3_queue_arn {
183 storage
184 .add_bucket_notifications(&s3_queue_arn)
185 .await
186 .inspect_err(|error| {
187 tracing::error!(?error, "failed to add bucket notification configuration")
188 })
189 .map_err(InitTenantError::SetupS3Notifications)?;
190 }
191
192 if !origins.is_empty() {
194 storage
195 .add_bucket_cors(origins)
196 .await
197 .inspect_err(|error| tracing::error!(?error, "failed to add bucket cors rules"))
198 .map_err(InitTenantError::SetupS3Cors)?;
199 }
200
201 Ok(rollback)
202}
203
204async fn create_tenant_search(
206 tenant: &Tenant,
207 search: &SearchIndexFactory,
208) -> Result<RollbackGuard<impl FnOnce()>, InitTenantError> {
209 let search = search.create_search_index(tenant);
211 search
212 .create_index()
213 .await
214 .map_err(InitTenantError::CreateSearchIndex)
215 .inspect_err(|error| tracing::error!(?error, "failed to create search index"))?;
216
217 let rollback = RollbackGuard::new(move || {
218 tokio::spawn(async move {
220 if let Err(error) = search.delete_index().await {
221 tracing::error!(?error, "failed to rollback created tenant search index");
222 }
223 });
224 });
225
226 Ok(rollback)
227}