diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index c89d55949b..c440617483 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -1492,94 +1492,27 @@ async fn fork_pg_database( Path(w_id): Path, Json(req): Json, ) -> Result { - require_admin(is_admin, &username)?; - if req.fork_behavior == DataTableForkBehavior::KeepOriginal { return Ok("No action needed for KeepOriginal behavior".to_string()); } - let schema_only = req.fork_behavior != DataTableForkBehavior::SchemaAndData || *CLOUD_HOSTED; + if req.fork_behavior == DataTableForkBehavior::SchemaAndData { + require_admin(is_admin, &username)?; + if *CLOUD_HOSTED { + return Err(Error::BadRequest( + "Forking schema and data is not available on cloud".to_string(), + )); + } + } + + let schema_only = req.fork_behavior == DataTableForkBehavior::SchemaOnly; let source_pg = resolve_pg_source(&db, &w_id, &req.source).await?; let target_dbname = &req.target_dbname; if req.source.starts_with("datatable://") { // Instance datatable: create custom instance DB, then dump/import - let db_exists = sqlx::query_scalar!( - "SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1)", - target_dbname - ) - .fetch_one(&db) - .await? - .unwrap_or(false); + windmill_common::create_custom_instance_database(&db, target_dbname, "datatable").await?; - if db_exists { - return Err(Error::BadRequest(format!( - "Instance database '{}' already exists", - target_dbname - ))); - } - - sqlx::query(&format!("CREATE DATABASE \"{}\"", target_dbname)) - .execute(&db) - .await - .map_err(|e| { - Error::internal_err(format!( - "Failed to create database '{}': {}", - target_dbname, e - )) - })?; - - // Grant permissions to custom_instance_user - let new_pg_creds = PgDatabase { - dbname: target_dbname.clone(), - ..PgDatabase::parse_uri(&get_database_url().await?.as_str().await)? - }; - let (client, connection) = new_pg_creds.connect().await?; - let join_handle = tokio::spawn(async move { connection.await }); - - if let Err(e) = client - .batch_execute(&format!( - "GRANT CONNECT ON DATABASE \"{target_dbname}\" TO custom_instance_user; - GRANT USAGE ON SCHEMA public TO custom_instance_user; - GRANT CREATE ON SCHEMA public TO custom_instance_user; - GRANT CREATE ON DATABASE \"{target_dbname}\" TO custom_instance_user; - ALTER DEFAULT PRIVILEGES IN SCHEMA public - GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO custom_instance_user;" - )) - .await - { - tracing::warn!( - "Failed to grant permissions on '{}': {}. Continuing.", - target_dbname, - e - ); - } - - drop(client); - join_handle - .await - .map_err(|e| Error::internal_err(format!("join error: {}", e)))? - .map_err(|e| Error::internal_err(format!("tokio_postgres error: {}", e)))?; - - // Register in global_settings - let status_json = serde_json::json!({ - "logs": { - "created_database": "OK", - "db_connect": "OK", - "grant_permissions": "OK" - }, - "success": true, - "error": null, - "tag": "datatable" - }); - sqlx::query!( - r#"UPDATE global_settings SET value = jsonb_set(value, '{databases}', (COALESCE(value->'databases', '{}'::jsonb) || to_jsonb($1::json))) WHERE name = 'custom_instance_pg_databases'"#, - serde_json::json!({ target_dbname: status_json }) - ) - .execute(&db) - .await?; - - // Dump source → import into new instance DB let target_pg = PgDatabase { dbname: target_dbname.clone(), ..PgDatabase::parse_uri(&get_database_url().await?.as_str().await)? diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index c8db2a37a5..0d84ef6c04 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -615,6 +615,87 @@ pub async fn drop_custom_instance_database(db: &DB, dbname: &str) -> error::Resu Ok(()) } +/// Create a custom instance database: CREATE DATABASE, grant permissions, register in global_settings. +/// The `tag` is stored in global_settings metadata (e.g. "datatable" or "ducklake"). +pub async fn create_custom_instance_database( + db: &DB, + dbname: &str, + tag: &str, +) -> error::Result<()> { + let db_exists = sqlx::query_scalar!( + "SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1)", + dbname + ) + .fetch_one(db) + .await? + .unwrap_or(false); + + if db_exists { + return Err(error::Error::BadRequest(format!( + "Database '{}' already exists", + dbname + ))); + } + + sqlx::query(&format!("CREATE DATABASE \"{}\"", dbname)) + .execute(db) + .await + .map_err(|e| { + error::Error::internal_err(format!("Failed to create database '{}': {}", dbname, e)) + })?; + + // Grant permissions to custom_instance_user + let wmill_pg_creds = PgDatabase::parse_uri(&get_database_url().await?.as_str().await)?; + let new_pg_creds = PgDatabase { dbname: dbname.to_string(), ..wmill_pg_creds }; + let (client, connection) = new_pg_creds.connect().await?; + let join_handle = tokio::spawn(async move { connection.await }); + + if let Err(e) = client + .batch_execute(&format!( + "GRANT CONNECT ON DATABASE \"{dbname}\" TO custom_instance_user; + GRANT USAGE ON SCHEMA public TO custom_instance_user; + GRANT CREATE ON SCHEMA public TO custom_instance_user; + GRANT CREATE ON DATABASE \"{dbname}\" TO custom_instance_user; + ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO custom_instance_user;" + )) + .await + { + tracing::warn!( + "Failed to grant permissions on '{}': {}. Continuing.", + dbname, + e + ); + } + + drop(client); + join_handle + .await + .map_err(|e| error::Error::internal_err(format!("join error: {}", e)))? + .map_err(|e| error::Error::internal_err(format!("tokio_postgres error: {}", e)))?; + + // Register in global_settings + let status_json = serde_json::json!({ + "logs": { + "created_database": "OK", + "db_connect": "OK", + "grant_permissions": "OK" + }, + "success": true, + "error": null, + "tag": tag + }); + sqlx::query!( + r#"UPDATE global_settings SET value = jsonb_set(value, '{databases}', (COALESCE(value->'databases', '{}'::jsonb) || to_jsonb($1::json))) WHERE name = 'custom_instance_pg_databases'"#, + serde_json::json!({ dbname: status_json }) + ) + .execute(db) + .await?; + + tracing::info!("Created custom instance database '{}'", dbname); + Ok(()) +} + #[derive(Clone)] pub enum DatabaseUrl { #[cfg(all(feature = "enterprise", feature = "private"))] diff --git a/frontend/src/lib/components/workspaceSettings/ForkDatatableSection.svelte b/frontend/src/lib/components/workspaceSettings/ForkDatatableSection.svelte index 1952409b92..5b7c8cf1f3 100644 --- a/frontend/src/lib/components/workspaceSettings/ForkDatatableSection.svelte +++ b/frontend/src/lib/components/workspaceSettings/ForkDatatableSection.svelte @@ -21,7 +21,8 @@