feat: support custom env variables (#1675)

* custom envs

* feat: custom envs

* only on non cloud

* remove unecessary imports
This commit is contained in:
Ruben Fiszel
2023-06-05 17:42:57 +02:00
committed by GitHub
parent 2273fa8be5
commit 8c47957a02
14 changed files with 199 additions and 76 deletions

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE script DROP COLUMN envs;

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE script ADD COLUMN envs VARCHAR(1000)[];

View File

@@ -3656,6 +3656,59 @@
},
"query": "SELECT EXISTS(SELECT 1 FROM variable WHERE path = $1 AND workspace_id = $2)"
},
"88a5f7d43e2775cdbc121e4f39f7d881af75c24740446ca86f26ba8b2e9e5405": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Int8",
"Varchar",
"Int8Array",
"Text",
"Text",
"Text",
"Varchar",
"Text",
"Bool",
"Jsonb",
"Text",
{
"Custom": {
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash"
]
},
"name": "script_lang"
}
},
{
"Custom": {
"kind": {
"Enum": [
"script",
"trigger",
"failure",
"command",
"approval"
]
},
"name": "script_kind"
}
},
"Varchar",
"Bool",
"VarcharArray"
]
}
},
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17)"
},
"88b7589a6416da8be4b26af3bf30fcfcd6aeae7bc5a37e9a735cabbe2691c570": {
"describe": {
"columns": [
@@ -4010,58 +4063,6 @@
},
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE email = $1"
},
"90c5f1eea5d2cceb157ce0667a625f0fa4506961e2ad181765022a20b11d314d": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Int8",
"Varchar",
"Int8Array",
"Text",
"Text",
"Text",
"Varchar",
"Text",
"Bool",
"Jsonb",
"Text",
{
"Custom": {
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash"
]
},
"name": "script_lang"
}
},
{
"Custom": {
"kind": {
"Enum": [
"script",
"trigger",
"failure",
"command",
"approval"
]
},
"name": "script_kind"
}
},
"Varchar",
"Bool"
]
}
},
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16)"
},
"911b1e1f2a5ba6d5159916e5598020e680c45043b0736ad0153ee261a151dd90": {
"describe": {
"columns": [

View File

@@ -5493,6 +5493,10 @@ components:
type: boolean
draft_only:
type: boolean
envs:
type: array
items:
type: string
required:
- hash
- path
@@ -5540,6 +5544,10 @@ components:
type: string
draft_only:
type: boolean
envs:
type: array
items:
type: string
required:
- path
- summary

View File

@@ -70,6 +70,8 @@ pub struct ScriptWDraft {
pub schema: Option<Schema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub envs: Option<Vec<String>>,
}
pub fn global_service() -> Router {
@@ -363,11 +365,18 @@ async fn create_script(
};
let needs_lock_gen = lock.is_none();
let envs = ns.envs.as_ref().map(|x| x.as_slice());
let envs = if ns.envs.is_none() || ns.envs.as_ref().unwrap().is_empty() {
None
} else {
envs
};
//::text::json is to ensure we use serde_json with preserve order
sqlx::query!(
"INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, \
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16)",
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17)",
&w_id,
&hash.0,
ns.path,
@@ -383,7 +392,8 @@ async fn create_script(
ns.language: ScriptLang,
ns.kind.unwrap_or(ScriptKind::Script): ScriptKind,
ns.tag,
ns.draft_only
ns.draft_only,
envs
)
.execute(&mut tx)
.await?;
@@ -550,7 +560,7 @@ async fn get_script_by_path_w_draft(
let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, ScriptWDraft>(
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, draft.value as draft FROM script LEFT JOIN draft ON
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, draft.value as draft FROM script LEFT JOIN draft ON
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
WHERE script.path = $1 AND script.workspace_id = $2 \
AND script.created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \

View File

@@ -139,6 +139,8 @@ pub struct Script {
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub envs: Option<Vec<String>>,
}
#[derive(Serialize)]
@@ -189,6 +191,7 @@ pub struct NewScript {
pub kind: Option<ScriptKind>,
pub tag: Option<String>,
pub draft_only: Option<bool>,
pub envs: Option<Vec<String>>,
}
#[derive(Deserialize)]

View File

@@ -1,4 +1,4 @@
use std::process::Stdio;
use std::{collections::HashMap, process::Stdio};
use itertools::Itertools;
use tokio::{
@@ -40,6 +40,7 @@ pub async fn handle_go_job(
shared_mount: &str,
base_internal_url: &str,
worker_name: &str,
envs: HashMap<String, String>,
) -> Result<serde_json::Value, Error> {
//go does not like executing modules at temp root
let job_dir = &format!("{job_dir}/go");
@@ -210,6 +211,7 @@ func Run(req Req) (interface{{}}, error){{
let mut cmd = Command::new(GO_PATH.as_str());
cmd.current_dir(job_dir)
.env_clear()
.envs(envs)
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)

View File

@@ -1,4 +1,4 @@
use std::process::Stdio;
use std::{collections::HashMap, process::Stdio};
use itertools::Itertools;
use regex::Regex;
@@ -165,6 +165,7 @@ pub async fn handle_python_job(
inner_content: &String,
shared_mount: &str,
base_internal_url: &str,
envs: HashMap<String, String>,
) -> windmill_common::error::Result<serde_json::Value> {
create_dependencies_dir(job_dir).await;
@@ -435,6 +436,7 @@ mount {{
Command::new(PYTHON_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.envs(envs)
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)

View File

@@ -1204,13 +1204,14 @@ async fn handle_code_execution_job(
worker_name: &str
) -> error::Result<serde_json::Value> {
let (inner_content, requirements_o, language) = match job.job_kind {
let (inner_content, requirements_o, language, envs) = match job.job_kind {
JobKind::Preview => (
job.raw_code
.clone()
.unwrap_or_else(|| "no raw code".to_owned()),
job.raw_lock.clone(),
job.language.to_owned(),
None,
),
JobKind::Script_Hub => {
let script_path = job.script_path.clone().ok_or_else(|| Error::InternalErr(format!("expected script path for hub script")))?;
@@ -1231,10 +1232,11 @@ async fn handle_code_execution_job(
(
script.content,
script.lockfile,
Some(script.language)
Some(script.language),
None
)},
JobKind::Script => sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>)>(
"SELECT content, lock, language FROM script WHERE hash = $1 AND workspace_id = $2",
JobKind::Script => sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>, Option<Vec<String>>)>(
"SELECT content, lock, language, envs FROM script WHERE hash = $1 AND workspace_id = $2",
)
.bind(&job.script_hash.unwrap_or(ScriptHash(0)).0)
.bind(&job.workspace_id)
@@ -1276,6 +1278,21 @@ mount {{
};
// println!("handle lang job {:?}", SystemTime::now());
let envs = if *CLOUD_HOSTED || envs.is_none() {
HashMap::new()
} else {
let mut hm = HashMap::new();
for s in envs.unwrap() {
let (k, v) = s.split_once('=').ok_or_else(|| {
Error::BadRequest(format!(
"Invalid env var: {}. Must be in the form of KEY=VALUE",
s
))
})?;
hm.insert(k.to_string(), v.to_string());
}
hm
};
let result: error::Result<serde_json::Value> = match language {
None => {
@@ -1296,6 +1313,7 @@ mount {{
&inner_content,
&shared_mount,
base_internal_url,
envs
)
.await
}
@@ -1308,7 +1326,8 @@ mount {{
job_dir,
&inner_content,
base_internal_url,
worker_name
worker_name,
envs
)
.await
}
@@ -1323,7 +1342,8 @@ mount {{
requirements_o,
&shared_mount,
base_internal_url,
worker_name
worker_name,
envs
)
.await
}
@@ -1337,7 +1357,8 @@ mount {{
job_dir,
&shared_mount,
base_internal_url,
worker_name
worker_name,
envs
)
.await
}
@@ -1368,6 +1389,7 @@ async fn handle_bash_job(
shared_mount: &str,
base_internal_url: &str,
worker_name: &str,
envs: HashMap<String, String>,
) -> Result<serde_json::Value, Error> {
logs.push_str("\n\n--- BASH CODE EXECUTION ---\n");
set_logs(logs, &job.id, db).await;
@@ -1422,6 +1444,7 @@ async fn handle_bash_job(
Command::new("/bin/bash")
.current_dir(job_dir)
.env_clear()
.envs(envs)
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
@@ -1469,7 +1492,8 @@ async fn handle_deno_job(
job_dir: &str,
inner_content: &String,
base_internal_url: &str,
worker_name: &str
worker_name: &str,
envs: HashMap<String, String>,
) -> error::Result<serde_json::Value> {
// let mut start = Instant::now();
@@ -1608,6 +1632,7 @@ run().catch(async (e) => {{
Command::new(DENO_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(common_deno_proc_envs)
.env("DENO_DIR", DENO_CACHE_DIR)

View File

@@ -10,13 +10,13 @@
import ScriptEditor from './ScriptEditor.svelte'
import ScriptSchema from './ScriptSchema.svelte'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import { Badge, Button, Drawer, Kbd } from './common'
import { faSave } from '@fortawesome/free-solid-svg-icons'
import { Alert, Badge, Button, Drawer, Kbd } from './common'
import { faPlus, faSave } from '@fortawesome/free-solid-svg-icons'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
import type { SupportedLanguage } from '$lib/common'
import Tooltip from './Tooltip.svelte'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import { Pen } from 'lucide-svelte'
import { Pen, X } from 'lucide-svelte'
import autosize from 'svelte-autosize'
import type Editor from './Editor.svelte'
import {
@@ -29,6 +29,8 @@
import { sendUserToast } from '$lib/toast'
import { isCloudHosted } from '$lib/cloud'
import Awareness from './Awareness.svelte'
import { Icon } from 'svelte-awesome'
import { fade } from 'svelte/transition'
export let script: NewScript
export let initialPath: string = ''
@@ -157,7 +159,8 @@
is_template: script.is_template,
language: script.language,
kind: script.kind,
tag: script.tag
tag: script.tag,
envs: script.envs
}
})
history.replaceState(history.state, '', `/scripts/edit/${script.path}`)
@@ -196,7 +199,8 @@
language: script.language,
kind: script.kind,
tag: script.tag,
draft_only: true
draft_only: true,
envs: script.envs
}
})
}
@@ -402,6 +406,57 @@
{/if}
{/if}
</div>
{#if !isCloudHosted()}
<h2 class="border-b pb-1 mt-10 mb-4"
>Custom env variables<Tooltip
>Additional static custom env variables to pass to the script.</Tooltip
></h2
>
<div class="w-full">
<span class="text-gray-600 text-xs pb-2">Format is: `{'<KEY>=<VALUE>'}`</span>
{#if Array.isArray(script.envs ?? [])}
{#each script.envs ?? [] as v, i}
<div class="flex max-w-md mt-1 w-full items-center">
<input type="text" bind:value={v} placeholder="<KEY>=<VALUE>" />
<button
transition:fade|local={{ duration: 50 }}
class="rounded-full p-1 bg-white/60 duration-200 hover:bg-gray-200"
aria-label="Clear"
on:click={() => {
script.envs && script.envs.splice(i, 1)
script.envs = script.envs
}}
>
<X size={14} />
</button>
</div>
{/each}
{#if script.envs && script.envs.length > 0}
<div class="pt-2" />
<Alert type="warning" title="Not passed in previews"
>Static envs variables are not passed in preview but solely on deployed scripts.</Alert
>
{/if}
{/if}
</div>
{/if}
<div class="flex mt-2">
<Button
variant="border"
color="dark"
size="xs"
btnClasses="mt-1"
on:click={() => {
if (script.envs == undefined || !Array.isArray(script.envs)) {
script.envs = []
}
script.envs = script.envs.concat('')
}}
>
<Icon data={faPlus} class="mr-2" />
Add item
</Button>
</div>
</DrawerContent>
</Drawer>

View File

@@ -10,7 +10,7 @@
export let id: string
export let componentInput: AppInput | undefined
export let initializing: boolean | undefined = undefined
export let initializing: boolean | undefined = false
export let customCss: ComponentCustomCSS<'flowstatuscomponent'> | undefined = undefined
export let render: boolean
@@ -21,6 +21,8 @@
loading: false
})
initializing = false
$: css = concatCustomCss($app.css?.flowstatuscomponent, customCss)
let jobId: string | undefined
@@ -34,7 +36,6 @@
{render}
{componentInput}
{id}
bind:initializing
>
<div class="flex flex-col w-full h-full">
<div

View File

@@ -12,7 +12,7 @@
export let id: string
export let componentInput: AppInput | undefined
export let initializing: boolean | undefined = undefined
export let initializing: boolean | undefined = false
export let customCss: ComponentCustomCSS<'logcomponent'> | undefined = undefined
export let render: boolean
@@ -23,6 +23,8 @@
loading: false
})
initializing = false
$: css = concatCustomCss($app.css?.logcomponent, customCss)
let testJobLoader: TestJobLoader | undefined = undefined
@@ -40,7 +42,6 @@
{render}
{componentInput}
{id}
bind:initializing
>
<div class="flex flex-col w-full h-full">
<div

View File

@@ -35,6 +35,7 @@
content: string
schema?: any
kind: 'script' | 'failure' | 'trigger' | 'command' | 'approval' | undefined
envs?: string[]
}
| undefined = undefined
@@ -62,7 +63,8 @@
schema: script.schema,
is_template: false,
language: script.language,
kind: script.kind as Script.kind | undefined
kind: script.kind as Script.kind | undefined,
envs: script.envs
}
})
callback?.()

View File

@@ -461,6 +461,15 @@
</Tabs>
</div>
{#if script.envs && script.envs.length > 0}
<h3>Static Env Variables</h3>
<ul>
{#each script.envs as e}
<li>{e}</li>
{/each}
</ul>
{/if}
<div class="max-w-2xl mt-12">
<h3 class="mb-4" bind:this={webhookElem} id="webhooks">
Webhooks