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 name: String,
18
19 pub id: TenantId,
21
22 pub db_name: String,
24
25 pub db_secret_name: String,
27
28 pub s3_name: String,
30
31 pub os_index_name: String,
33
34 pub event_queue_url: Option<String>,
36
37 pub origins: Vec<String>,
39
40 pub s3_queue_arn: Option<String>,
43}
44
45#[derive(Debug, Error)]
46pub enum InitTenantError {
47 #[error("failed to connect")]
49 ConnectDb(#[from] DbConnectErr),
50
51 #[error(transparent)]
53 Database(#[from] DbErr),
54
55 #[error("tenant already exists")]
57 TenantAlreadyExist,
58
59 #[error("failed to create tenant s3 bucket: {0}")]
61 CreateS3Bucket(anyhow::Error),
62
63 #[error("failed to setup s3 notification rules: {0}")]
65 SetupS3Notifications(anyhow::Error),
66
67 #[error("failed to setup s3 CORS rules: {0}")]
69 SetupS3Cors(anyhow::Error),
70
71 #[error("failed to create tenant search index: {0}")]
73 CreateSearchIndex(anyhow::Error),
74}
75
76pub async fn create_tenant(
78 root_db: &DbPool,
79 tenant_db: &DbPool,
80
81 search: &SearchIndexFactory,
82 storage: &StorageLayerFactory,
83 create: CreateTenant,
84) -> Result<Tenant, InitTenantError> {
85 let mut root_transaction = root_db
87 .begin()
88 .await
89 .inspect_err(|error| tracing::error!(?error, "failed to begin root transaction"))?;
90
91 let tenant: Tenant = Tenant::create(
93 root_transaction.deref_mut(),
94 docbox_database::models::tenant::CreateTenant {
95 id: create.id,
96 name: create.name,
97 db_name: create.db_name,
98 db_secret_name: create.db_secret_name,
99 s3_name: create.s3_name,
100 os_index_name: create.os_index_name,
101 event_queue_url: create.event_queue_url,
102 env: create.env,
103 },
104 )
105 .await
106 .map_err(|err| {
107 if let Some(db_err) = err.as_database_error() {
108 if db_err.is_unique_violation() {
110 return InitTenantError::TenantAlreadyExist;
111 }
112 }
113
114 InitTenantError::Database(err)
115 })
116 .inspect_err(|error| tracing::error!(?error, "failed to create tenant"))?;
117
118 let mut tenant_transaction = tenant_db
120 .begin()
121 .await
122 .inspect_err(|error| tracing::error!(?error, "failed to begin tenant transaction"))?;
123
124 apply_tenant_migrations(
126 &mut root_transaction,
127 &mut tenant_transaction,
128 &tenant,
129 None,
130 )
131 .await
132 .inspect_err(|error| tracing::error!(?error, "failed to create tenant tables"))?;
133
134 tracing::debug!("creating tenant storage");
136 let storage =
137 create_tenant_storage(&tenant, storage, create.s3_queue_arn, create.origins).await?;
138
139 tracing::debug!("creating tenant search index");
141 let search = create_tenant_search(&tenant, search).await?;
142
143 tenant_transaction
145 .commit()
146 .await
147 .inspect_err(|error| tracing::error!(?error, "failed to commit tenant transaction"))?;
148 root_transaction
149 .commit()
150 .await
151 .inspect_err(|error| tracing::error!(?error, "failed to commit root transaction"))?;
152
153 storage.commit();
155 search.commit();
156
157 Ok(tenant)
158}
159
160async fn create_tenant_storage(
162 tenant: &Tenant,
163 storage: &StorageLayerFactory,
164 s3_queue_arn: Option<String>,
165 origins: Vec<String>,
166) -> Result<RollbackGuard<impl FnOnce()>, InitTenantError> {
167 let storage = storage.create_storage_layer(tenant);
168 storage
169 .create_bucket()
170 .await
171 .inspect_err(|error| tracing::error!(?error, "failed to create tenant bucket"))
172 .map_err(InitTenantError::CreateS3Bucket)?;
173
174 let rollback = RollbackGuard::new({
175 let storage = storage.clone();
176 move || {
177 tokio::spawn(async move {
178 if let Err(error) = storage.delete_bucket().await {
179 tracing::error!(?error, "failed to rollback created tenant storage bucket");
180 }
181 });
182 }
183 });
184
185 if let Some(s3_queue_arn) = s3_queue_arn {
187 storage
188 .add_bucket_notifications(&s3_queue_arn)
189 .await
190 .inspect_err(|error| {
191 tracing::error!(?error, "failed to add bucket notification configuration")
192 })
193 .map_err(InitTenantError::SetupS3Notifications)?;
194 }
195
196 if !origins.is_empty() {
198 storage
199 .add_bucket_cors(origins)
200 .await
201 .inspect_err(|error| tracing::error!(?error, "failed to add bucket cors rules"))
202 .map_err(InitTenantError::SetupS3Cors)?;
203 }
204
205 Ok(rollback)
206}
207
208async fn create_tenant_search(
210 tenant: &Tenant,
211 search: &SearchIndexFactory,
212) -> Result<RollbackGuard<impl FnOnce()>, InitTenantError> {
213 let search = search.create_search_index(tenant);
215 search
216 .create_index()
217 .await
218 .map_err(InitTenantError::CreateSearchIndex)
219 .inspect_err(|error| tracing::error!(?error, "failed to create search index"))?;
220
221 let rollback = RollbackGuard::new(move || {
222 tokio::spawn(async move {
224 if let Err(error) = search.delete_index().await {
225 tracing::error!(?error, "failed to rollback created tenant search index");
226 }
227 });
228 });
229
230 Ok(rollback)
231}