diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index 14c7054c3a..492be601cc 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -38,10 +38,13 @@ use windmill_common::{ db::{UserDB, UserDbWithAuthed, UserDbWithOptAuthed}, error::{self, Error, JsonResult, Result}, get_database_url, parse_postgres_url, - utils::get_custom_pg_instance_password, - utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath}, + utils::{ + get_custom_pg_instance_password, not_found_if_none, paginate, require_admin, Pagination, + StripPath, + }, variables, worker::{CLOUD_HOSTED, TMP_DIR}, + workspaces::{get_datatable_from_db_unchecked, DataTableCatalogResourceType}, }; pub fn workspaced_service() -> Router { @@ -469,6 +472,25 @@ pub async fn get_resource_value_interpolated_internal( token: &str, allow_cache: bool, ) -> Result> { + let (user_db, path) = if let Some(name) = path.strip_prefix("datatable://") { + let datatable = get_datatable_from_db_unchecked(db, workspace, name).await?; + if datatable.database.resource_type == DataTableCatalogResourceType::Instance { + let pg_creds = parse_postgres_url(&get_database_url().await?.as_str().await)?; + return Ok(Some(serde_json::json!({ + "dbname": datatable.database.resource_path, + "host": pg_creds.host, + "port": pg_creds.port, + "user": "custom_instance_user", + "sslmode": pg_creds.ssl_mode, + "password": get_custom_pg_instance_password(&db).await?, + }))); + } else { + (None, datatable.database.resource_path) + } + } else { + (user_db, path.to_string()) + }; + // This is a special syntax to help debugging custom instance databases if let Some(dbname) = path.strip_prefix("CUSTOM_INSTANCE_DB/") { require_super_admin(db, &authed.email).await?; @@ -499,10 +521,10 @@ pub async fn get_resource_value_interpolated_internal( .await?; tx.commit().await?; if value_o.is_none() { - explain_resource_perm_error(path, workspace, db, &authed).await?; + explain_resource_perm_error(&path, workspace, db, &authed).await?; } - let value = not_found_if_none(value_o, "Resource", path)?; + let value = not_found_if_none(value_o, "Resource", &path)?; if let Some(value) = value { let r = transform_json_value( authed, diff --git a/backend/windmill-common/src/workspaces.rs b/backend/windmill-common/src/workspaces.rs index 75d731c3e0..ae04ae44f4 100644 --- a/backend/windmill-common/src/workspaces.rs +++ b/backend/windmill-common/src/workspaces.rs @@ -170,6 +170,24 @@ pub enum DataTableCatalogResourceType { Instance, } +pub async fn get_datatable_from_db_unchecked(db: &DB, w_id: &str, name: &str) -> Result { + let datatable = sqlx::query_scalar!( + r#" + SELECT ws.datatable->'datatables'->$2 AS config + FROM workspace_settings ws + WHERE ws.workspace_id = $1 + "#, + &w_id, + name + ) + .fetch_one(db) + .await + .map_err(|err| Error::internal_err(format!("getting datatable {name}: {err}")))? + .ok_or_else(|| Error::internal_err(format!("datatable {name} not found")))?; + let datatable = serde_json::from_value::(datatable)?; + Ok(datatable) +} + #[derive(Deserialize, Serialize, Debug)] pub struct Ducklake { pub catalog: DucklakeCatalog, diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index 06085f5f68..196484a9f2 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -127,7 +127,7 @@ pub async fn write_file_binary(dir: &str, path: &str, content: &[u8]) -> error:: } lazy_static::lazy_static! { - static ref RE_RES_VAR: Regex = Regex::new(r#"\$(?:var|res|encrypted)\:"#).unwrap(); + static ref RE_RES_VAR: Regex = Regex::new(r#"(?:\$var|\$res|\$encrypted|datatable|CUSTOM_INSTANCE_DB)\:"#).unwrap(); } pub async fn transform_json<'a>( @@ -137,14 +137,7 @@ pub async fn transform_json<'a>( job: &MiniPulledJob, db: &Connection, ) -> error::Result>>> { - let mut has_match = false; - for (_, v) in vs { - let inner_vs = v.get(); - if (*RE_RES_VAR).is_match(inner_vs) { - has_match = true; - break; - } - } + let has_match = vs.iter().any(|(_, v)| (*RE_RES_VAR).is_match(v.get())); if !has_match { return Ok(None); } @@ -243,15 +236,24 @@ pub async fn transform_json_value( ))); } client - .get_resource_value_interpolated::( - path, - Some(job.id.to_string()), - ) + .get_resource_value_interpolated(path, Some(job.id.to_string())) .await .map_err(|e| { Error::NotFound(format!("Resource {path} not found for `{name}`: {e:#}")) }) } + Value::String(ref path) + if path.starts_with("CUSTOM_INSTANCE_DB://") || path.starts_with("datatable://") => + { + client + .get_resource_value_interpolated(path, Some(job.id.to_string())) + .await + .map_err(|e| { + Error::NotFound(format!( + "Custom database {path} not found for `{name}`: {e:#}" + )) + }) + } Value::String(y) if y.starts_with("$encrypted:") => { match conn { Connection::Sql(db) => {