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    /// Unique ID for the tenant
17    pub id: TenantId,
18
19    /// Database name for the tenant
20    pub db_name: String,
21
22    /// Database secret credentials name for the tenant
23    pub db_secret_name: String,
24
25    /// Name of the tenant s3 bucket
26    pub s3_name: String,
27
28    /// Name of the tenant search index
29    pub os_index_name: String,
30
31    /// URL for the SQS event queue
32    pub event_queue_url: Option<String>,
33
34    /// CORS Origins for setting up presigned uploads with S3
35    pub origins: Vec<String>,
36
37    /// ARN for the S3 queue to publish S3 notifications, required
38    /// for presigned uploads
39    pub s3_queue_arn: Option<String>,
40}
41
42#[derive(Debug, Error)]
43pub enum InitTenantError {
44    /// Failed to connect to a database
45    #[error("failed to connect")]
46    ConnectDb(#[from] DbConnectErr),
47
48    /// Database error
49    #[error(transparent)]
50    Database(#[from] DbErr),
51
52    /// Tenant already exists
53    #[error("tenant already exists")]
54    TenantAlreadyExist,
55
56    /// Failed to create the S3 bucket
57    #[error("failed to create tenant s3 bucket: {0}")]
58    CreateS3Bucket(anyhow::Error),
59
60    /// Failed to setup the S3 bucket CORS rules
61    #[error("failed to setup s3 notification rules: {0}")]
62    SetupS3Notifications(anyhow::Error),
63
64    /// Failed to setup the S3 bucket CORS rules
65    #[error("failed to setup s3 CORS rules: {0}")]
66    SetupS3Cors(anyhow::Error),
67
68    /// Failed to create the search index
69    #[error("failed to create tenant search index: {0}")]
70    CreateSearchIndex(anyhow::Error),
71}
72
73/// Attempts to initialize a new tenant
74pub 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    // Enter a database transaction
83    let mut root_transaction = root_db
84        .begin()
85        .await
86        .inspect_err(|error| tracing::error!(?error, "failed to begin root transaction"))?;
87
88    // Create the tenant
89    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            // Handle attempts at a duplicate tenant creation
105            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    // Enter a database transaction
115    let mut tenant_transaction = tenant_db
116        .begin()
117        .await
118        .inspect_err(|error| tracing::error!(?error, "failed to begin tenant transaction"))?;
119
120    // Setup the tenant database
121    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    // Setup the tenant storage bucket
131    tracing::debug!("creating tenant storage");
132    let storage =
133        create_tenant_storage(&tenant, storage, create.s3_queue_arn, create.origins).await?;
134
135    // Setup the tenant search index
136    tracing::debug!("creating tenant search index");
137    let search = create_tenant_search(&tenant, search).await?;
138
139    // Commit database changes
140    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    // Commit search and storage
150    storage.commit();
151    search.commit();
152
153    Ok(tenant)
154}
155
156/// Create and setup the tenant storage
157async 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    // Connect the S3 bucket for file upload notifications
182    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    // Setup bucket allowed origins for presigned uploads
193    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
204/// Create and setup the tenant search
205async fn create_tenant_search(
206    tenant: &Tenant,
207    search: &SearchIndexFactory,
208) -> Result<RollbackGuard<impl FnOnce()>, InitTenantError> {
209    // Setup the tenant search index
210    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        // Search was not committed, roll back
219        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}