docbox_core/tenant/
create_tenant.rs

1use 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
11/// Request to create a tenant
12pub struct CreateTenant {
13    /// Environment to create the tenant within
14    pub env: String,
15
16    /// Name to give the created tenant
17    pub name: String,
18
19    /// Unique ID for the tenant
20    pub id: TenantId,
21
22    /// Database name for the tenant
23    pub db_name: String,
24
25    /// Database secret credentials name for the tenant
26    pub db_secret_name: String,
27
28    /// Name of the tenant s3 bucket
29    pub s3_name: String,
30
31    /// Name of the tenant search index
32    pub os_index_name: String,
33
34    /// URL for the SQS event queue
35    pub event_queue_url: Option<String>,
36
37    /// CORS Origins for setting up presigned uploads with S3
38    pub origins: Vec<String>,
39
40    /// ARN for the S3 queue to publish S3 notifications, required
41    /// for presigned uploads
42    pub s3_queue_arn: Option<String>,
43}
44
45#[derive(Debug, Error)]
46pub enum InitTenantError {
47    /// Failed to connect to a database
48    #[error("failed to connect")]
49    ConnectDb(#[from] DbConnectErr),
50
51    /// Database error
52    #[error(transparent)]
53    Database(#[from] DbErr),
54
55    /// Tenant already exists
56    #[error("tenant already exists")]
57    TenantAlreadyExist,
58
59    /// Failed to create the S3 bucket
60    #[error("failed to create tenant s3 bucket: {0}")]
61    CreateS3Bucket(anyhow::Error),
62
63    /// Failed to setup the S3 bucket CORS rules
64    #[error("failed to setup s3 notification rules: {0}")]
65    SetupS3Notifications(anyhow::Error),
66
67    /// Failed to setup the S3 bucket CORS rules
68    #[error("failed to setup s3 CORS rules: {0}")]
69    SetupS3Cors(anyhow::Error),
70
71    /// Failed to create the search index
72    #[error("failed to create tenant search index: {0}")]
73    CreateSearchIndex(anyhow::Error),
74}
75
76/// Attempts to initialize a new tenant
77pub 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    // Enter a database transaction
86    let mut root_transaction = root_db
87        .begin()
88        .await
89        .inspect_err(|error| tracing::error!(?error, "failed to begin root transaction"))?;
90
91    // Create the tenant
92    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            // Handle attempts at a duplicate tenant creation
109            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    // Enter a database transaction
119    let mut tenant_transaction = tenant_db
120        .begin()
121        .await
122        .inspect_err(|error| tracing::error!(?error, "failed to begin tenant transaction"))?;
123
124    // Setup the tenant database
125    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    // Setup the tenant storage bucket
135    tracing::debug!("creating tenant storage");
136    let storage =
137        create_tenant_storage(&tenant, storage, create.s3_queue_arn, create.origins).await?;
138
139    // Setup the tenant search index
140    tracing::debug!("creating tenant search index");
141    let search = create_tenant_search(&tenant, search).await?;
142
143    // Commit database changes
144    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    // Commit search and storage
154    storage.commit();
155    search.commit();
156
157    Ok(tenant)
158}
159
160/// Create and setup the tenant storage
161async 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    // Connect the S3 bucket for file upload notifications
186    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    // Setup bucket allowed origins for presigned uploads
197    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
208/// Create and setup the tenant search
209async fn create_tenant_search(
210    tenant: &Tenant,
211    search: &SearchIndexFactory,
212) -> Result<RollbackGuard<impl FnOnce()>, InitTenantError> {
213    // Setup the tenant search index
214    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        // Search was not committed, roll back
223        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}