diff --git a/backend/.sqlx/query-58aa2e6de6cb9724750dae7405d664080e8d810278ff0dd52a2b8bfb7270fe44.json b/backend/.sqlx/query-58aa2e6de6cb9724750dae7405d664080e8d810278ff0dd52a2b8bfb7270fe44.json new file mode 100644 index 0000000000..a607cf1065 --- /dev/null +++ b/backend/.sqlx/query-58aa2e6de6cb9724750dae7405d664080e8d810278ff0dd52a2b8bfb7270fe44.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM workspace_env WHERE workspace_id = $1 AND name = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "58aa2e6de6cb9724750dae7405d664080e8d810278ff0dd52a2b8bfb7270fe44" +} diff --git a/backend/.sqlx/query-73706be0610149682fa6131494212095e9494ade5e01b138c7fbbd16f42be791.json b/backend/.sqlx/query-73706be0610149682fa6131494212095e9494ade5e01b138c7fbbd16f42be791.json new file mode 100644 index 0000000000..c7ecbf1b56 --- /dev/null +++ b/backend/.sqlx/query-73706be0610149682fa6131494212095e9494ade5e01b138c7fbbd16f42be791.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE workspace_env SET workspace_id = $1 WHERE workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + }, + "nullable": [] + }, + "hash": "73706be0610149682fa6131494212095e9494ade5e01b138c7fbbd16f42be791" +} diff --git a/backend/.sqlx/query-b8c66d905a6c7ffa6441c84b14ea897040069dac7367895813cc2d64a9867193.json b/backend/.sqlx/query-b8c66d905a6c7ffa6441c84b14ea897040069dac7367895813cc2d64a9867193.json new file mode 100644 index 0000000000..265a3c96a4 --- /dev/null +++ b/backend/.sqlx/query-b8c66d905a6c7ffa6441c84b14ea897040069dac7367895813cc2d64a9867193.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO workspace_env (workspace_id, name, value) VALUES ($1, $2, $3) ON CONFLICT (workspace_id, name) DO UPDATE SET value = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "b8c66d905a6c7ffa6441c84b14ea897040069dac7367895813cc2d64a9867193" +} diff --git a/backend/migrations/20240322164216_add_workspace_envs.down.sql b/backend/migrations/20240322164216_add_workspace_envs.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20240322164216_add_workspace_envs.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20240322164216_add_workspace_envs.up.sql b/backend/migrations/20240322164216_add_workspace_envs.up.sql new file mode 100644 index 0000000000..3cc9510c30 --- /dev/null +++ b/backend/migrations/20240322164216_add_workspace_envs.up.sql @@ -0,0 +1,7 @@ +-- Add up migration script here +CREATE TABLE workspace_env ( + workspace_id varchar(50) not null, + name varchar(255) not null, + value varchar(1000) not null, + primary key (workspace_id, name) +) \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index f0e3e19cd2..aee87f2e07 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1841,6 +1841,35 @@ paths: schema: $ref: "#/components/schemas/WorkspaceDefaultScripts" + /w/{workspace}/workspaces/set_environment_variable: + post: + summary: set environment variable + operationId: setEnvironmentVariable + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: Workspace default app + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + value: + type: string + required: [name] + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + /w/{workspace}/workspaces/encryption_key: get: summary: retrieves the encryption key for this workspace @@ -8801,10 +8830,13 @@ components: type: string description: type: string + is_custom: + type: boolean required: - name - value - description + - is_custom CreateVariable: type: object diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index 20970f330b..4cf69725ef 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -385,9 +385,7 @@ async fn custom_component( let cc = not_found_if_none(cc_o, "Custom Component", name)?; let res = Response::builder().header(header::CONTENT_TYPE, "text/javascript"); - Ok(res - .body(Body::from(cc)) - .unwrap()) + Ok(res.body(Body::from(cc)).unwrap()) } #[derive(Deserialize)] @@ -537,6 +535,7 @@ pub async fn transform_json_value<'c>( }; let variables = variables::get_reserved_variables( + db, &job.workspace_id, token, &job.email, diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index 781856b1b0..61dfa5f695 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -57,9 +57,11 @@ pub fn workspaced_service() -> Router { async fn list_contextual_variables( Path(w_id): Path, ApiAuthed { username, email, .. }: ApiAuthed, + Extension(db): Extension, ) -> JsonResult> { Ok(Json( get_reserved_variables( + &db, &w_id, "q1A0qcPuO00yxioll7iph76N9CJDqn", &email, diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index d2e3326173..ca32323b50 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -104,6 +104,7 @@ pub fn workspaced_service() -> Router { "/default_scripts", post(edit_default_scripts).get(get_default_scripts), ) + .route("/set_environment_variable", post(set_environment_variable)) .route( "/encryption_key", get(get_encryption_key).post(set_encryption_key), @@ -1200,6 +1201,71 @@ async fn edit_error_handler( Ok(format!("Edit error_handler for workspace {}", &w_id)) } +#[derive(Deserialize)] +struct NewEnvironmentVariable { + name: String, + value: Option, +} + +async fn set_environment_variable( + authed: ApiAuthed, + Extension(db): Extension, + Path(w_id): Path, + Json(NewEnvironmentVariable { value, name }): Json, +) -> Result { + require_admin(authed.is_admin, &authed.username)?; + + let mut tx = db.begin().await?; + + match value { + Some(value) => { + sqlx::query!( + "INSERT INTO workspace_env (workspace_id, name, value) VALUES ($1, $2, $3) ON CONFLICT (workspace_id, name) DO UPDATE SET value = $3", + &w_id, + name, + value + ) + .execute(&mut *tx) + .await?; + + audit_log( + &mut *tx, + &authed.username, + "workspace.set_environment_variable", + ActionKind::Create, + &w_id, + Some(&authed.email), + None, + ) + .await?; + tx.commit().await?; + Ok(format!("Set environment variable {}", name)) + } + None => { + sqlx::query!( + "DELETE FROM workspace_env WHERE workspace_id = $1 AND name = $2", + &w_id, + name + ) + .execute(&mut *tx) + .await?; + + audit_log( + &mut *tx, + &authed.username, + "workspace.delete_environment_variable", + ActionKind::Delete, + &w_id, + Some(&authed.email), + None, + ) + .await?; + tx.commit().await?; + Ok(format!("Deleted environment variable {}", name)) + } + } +} + #[derive(Serialize)] pub struct GetEncryptionKeyResponse { key: String, @@ -2960,6 +3026,14 @@ async fn change_workspace_id( .execute(&mut *tx) .await?; + sqlx::query!( + "UPDATE workspace_env SET workspace_id = $1 WHERE workspace_id = $2", + &rw.new_id, + &old_id + ) + .execute(&mut *tx) + .await?; + sqlx::query!( "UPDATE workspace_invite SET workspace_id = $1 WHERE workspace_id = $2", &rw.new_id, diff --git a/backend/windmill-common/src/variables.rs b/backend/windmill-common/src/variables.rs index cafd68ea7e..17246ca936 100644 --- a/backend/windmill-common/src/variables.rs +++ b/backend/windmill-common/src/variables.rs @@ -22,10 +22,10 @@ pub struct ContextualVariable { pub name: String, pub value: String, pub description: String, + pub is_custom: bool, } -#[derive(Serialize, Deserialize)] -#[derive(sqlx::FromRow)] +#[derive(Serialize, Deserialize, sqlx::FromRow)] pub struct ListableVariable { pub workspace_id: String, @@ -42,8 +42,7 @@ pub struct ListableVariable { pub is_linked: Option, } -#[derive(Serialize, Deserialize)] -#[derive(sqlx::FromRow)] +#[derive(Serialize, Deserialize, sqlx::FromRow)] pub struct ExportableListableVariable { pub workspace_id: String, @@ -133,6 +132,7 @@ pub async fn get_secret_value_as_admin( } pub async fn get_reserved_variables( + db: &DB, w_id: &str, token: &str, email: &str, @@ -146,7 +146,7 @@ pub async fn get_reserved_variables( step_id: Option, root_flow_id: Option, jwt_token: Option, -) -> [ContextualVariable; 17] { +) -> Vec { let state_path = { let trigger = if schedule_path.is_some() { username.to_string() @@ -192,11 +192,12 @@ pub async fn get_reserved_variables( format!("{joined_script_path}/{joined_schedule_path}/{ts}_{job_id}") }; - [ + vec![ ContextualVariable { name: "WM_WORKSPACE".to_string(), value: w_id.to_string(), description: "Workspace id of the current script".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_TOKEN".to_string(), @@ -204,46 +205,55 @@ pub async fn get_reserved_variables( description: "Token ephemeral to the current script with equal permission to the \ permission of the run (Usable as a bearer token)" .to_string(), + is_custom: false, }, ContextualVariable { name: "WM_EMAIL".to_string(), value: email.to_string(), description: "Email of the user that executed the current script".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_USERNAME".to_string(), value: username.to_string(), description: "Username of the user that executed the current script".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_BASE_URL".to_string(), value: BASE_URL.read().await.clone(), description: "base url of this instance".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_JOB_ID".to_string(), value: job_id.to_string(), description: "Job id of the current script".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_JOB_PATH".to_string(), value: path.unwrap_or_else(|| "".to_string()), description: "Path of the script or flow being run if any".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_FLOW_JOB_ID".to_string(), value: flow_id.unwrap_or_else(|| "".to_string()), description: "Job id of the encapsulating flow if the job is a flow step".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_ROOT_FLOW_JOB_ID".to_string(), value: root_flow_id.unwrap_or_else(|| "".to_string()), description: "Job id of the root flow if the job is a flow step".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_FLOW_PATH".to_string(), value: flow_path.unwrap_or_else(|| "".to_string()), description: "Path of the encapsulating flow if the job is a flow step".to_string(), + is_custom: false, }, ContextualVariable { @@ -252,36 +262,55 @@ pub async fn get_reserved_variables( description: "Path of the schedule if the job of the step or encapsulating step has \ been triggered by a schedule" .to_string(), + is_custom: false, }, ContextualVariable { name: "WM_PERMISSIONED_AS".to_string(), value: permissioned_as.to_string(), description: "Fully Qualified (u/g) owner name of executor of the job".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_STATE_PATH".to_string(), value: state_path.clone(), description: "State resource path unique to a script and its trigger".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_STATE_PATH_NEW".to_string(), value: state_path, description: "State resource path unique to a script and its trigger (legacy)".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_FLOW_STEP_ID".to_string(), value: step_id.unwrap_or_else(|| "".to_string()), description: "The node id in a flow (like 'a', 'b', or 'f')".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_OBJECT_PATH".to_string(), value: object_path, description: "Script or flow step execution unique path, useful for storing results in an external service".to_string(), + is_custom: false, }, ContextualVariable { name: "WM_OIDC_JWT".to_string(), value: jwt_token.unwrap_or_else(|| "".to_string()), description: "OIDC JWT token (EE only)".to_string(), + is_custom: false, }, - ] + ].into_iter().chain( sqlx::query_as::<_, (String, String)>( + "SELECT name, value FROM workspace_env WHERE workspace_id = $1", + ) + .bind(w_id) + .fetch_all(db) + .await + .unwrap_or_default() + .into_iter().map(|(name, value)| ContextualVariable { + name, + value, + description: "Custom workspace environment variable".to_string(), + is_custom: true, + })).collect() } diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs index e8ca2da625..526f503abc 100644 --- a/backend/windmill-worker/src/bun_executor.rs +++ b/backend/windmill-worker/src/bun_executor.rs @@ -801,6 +801,7 @@ pub async fn start_worker( get_common_bun_proc_envs(&base_internal_url).await; let context = variables::get_reserved_variables( + db, w_id, &token, "dedicated_worker@windmill.dev", diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index 7a1d6b771f..106524cd45 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -284,6 +284,7 @@ pub async fn transform_json_value( }; let variables = variables::get_reserved_variables( + db, &job.workspace_id, &client.token, &job.email, @@ -380,6 +381,7 @@ pub async fn get_reserved_variables( }; let variables = variables::get_reserved_variables( + db, &job.workspace_id, token, &job.email, diff --git a/backend/windmill-worker/src/dedicated_worker.rs b/backend/windmill-worker/src/dedicated_worker.rs index c3491ee618..75e123d7d3 100644 --- a/backend/windmill-worker/src/dedicated_worker.rs +++ b/backend/windmill-worker/src/dedicated_worker.rs @@ -49,7 +49,7 @@ pub async fn handle_dedicated_process( job_dir: &str, context_envs: HashMap, envs: HashMap, - reserved_variables: [variables::ContextualVariable; 17], + reserved_variables: Vec, common_bun_proc_envs: HashMap, args: Vec<&str>, mut killpill_rx: tokio::sync::broadcast::Receiver<()>, diff --git a/backend/windmill-worker/src/deno_executor.rs b/backend/windmill-worker/src/deno_executor.rs index 7ac6912a82..d83632bfb0 100644 --- a/backend/windmill-worker/src/deno_executor.rs +++ b/backend/windmill-worker/src/deno_executor.rs @@ -421,6 +421,7 @@ pub async fn start_worker( let common_deno_proc_envs = get_common_deno_proc_envs(&token, base_internal_url).await; let context = variables::get_reserved_variables( + db, w_id, &token, "dedicated_worker@windmill.dev", diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 58f24ea64d..eab0d419f2 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -1046,6 +1046,7 @@ pub async fn start_worker( let mut mem_peak: i32 = 0; let mut canceled_by: Option = None; let context = variables::get_reserved_variables( + db, w_id, &token, "dedicated_worker@windmill.dev", @@ -1163,6 +1164,7 @@ for line in sys.stdin: } let reserved_variables = windmill_common::variables::get_reserved_variables( + db, w_id, token, "dedicated_worker", diff --git a/frontend/src/lib/components/ContextualVariableEditor.svelte b/frontend/src/lib/components/ContextualVariableEditor.svelte new file mode 100644 index 0000000000..3c20be5be7 --- /dev/null +++ b/frontend/src/lib/components/ContextualVariableEditor.svelte @@ -0,0 +1,76 @@ + + + + +
+ {#if !edit} +
+ +
+ {/if} +
+