Compare commits

..

1 Commits

Author SHA1 Message Date
Faton Ramadani
b3fbf69b35 fix(frontend): fix chartjs options 2023-09-07 17:57:50 +02:00
188 changed files with 1821 additions and 5695 deletions

View File

@@ -37,10 +37,9 @@ RUN apt-get update \
make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libxml2-dev \
libxmlsec1-dev libffi-dev liblzma-dev mecab-ipadic-utf8 libgdbm-dev libc6-dev git libprotobuf-dev=3.6.* libnl-route-3-dev=3.4.* \
libv8-dev tesseract-ocr nodejs npm clang libclang-dev\
libv8-dev tesseract-ocr nodejs npm\
&& rm -rf /var/lib/apt/lists/*
RUN wget https://golang.org/dl/go1.19.1.linux-amd64.tar.gz && tar -C /usr/local -xzf go1.19.1.linux-amd64.tar.gz
ENV PATH="${PATH}:/usr/local/go/bin"
ENV GO_PATH=/usr/local/go/bin/go

View File

@@ -42,4 +42,4 @@ jobs:
mkdir frontend/build && cd backend && touch
windmill-api/openapi-deref.yaml &&
DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill cargo
--features enterprise test --all -- --nocapture
test --all -- --nocapture

View File

@@ -40,7 +40,6 @@ jobs:
run: deno run --unstable -A
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
--host http://localhost:8000
-r
-e admin@windmill.dev
-p changeme
-c https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_config.json

View File

@@ -1,85 +1,6 @@
# Changelog
## [1.173.0](https://github.com/windmill-labs/windmill/compare/v1.172.1...v1.173.0) (2023-09-14)
### Features
* cli sync on windows ([#2283](https://github.com/windmill-labs/windmill/issues/2283)) ([c371cb3](https://github.com/windmill-labs/windmill/commit/c371cb397ab3d0c534e2c553d1dfb1ad5176d2a6))
### Bug Fixes
* accept jobs whose duration > 24 days ([2c00894](https://github.com/windmill-labs/windmill/commit/2c00894122aa8caee59b20625935284de6902950))
## [1.172.1](https://github.com/windmill-labs/windmill/compare/v1.172.0...v1.172.1) (2023-09-14)
### Bug Fixes
* improve splitpane + improve deleting conditional tab ([1629008](https://github.com/windmill-labs/windmill/commit/1629008eb2eb48ff9cc2cf6b3a351efcf682244d))
* update to svelte 4 ([#2280](https://github.com/windmill-labs/windmill/issues/2280)) ([90c10d8](https://github.com/windmill-labs/windmill/commit/90c10d803b4c47a9e1ac5b9e49e2a614344299a9))
## [1.172.0](https://github.com/windmill-labs/windmill/compare/v1.171.0...v1.172.0) (2023-09-13)
### Features
* improve ai flow ([#2270](https://github.com/windmill-labs/windmill/issues/2270)) ([b23417a](https://github.com/windmill-labs/windmill/commit/b23417ab5b9938bbdf9db6449102760ff8c80152))
* worker groups admin panel ([#2277](https://github.com/windmill-labs/windmill/issues/2277)) ([070b162](https://github.com/windmill-labs/windmill/commit/070b16222bc666866284180b3878f4d4f27bfa85))
### Bug Fixes
* ai flow nits ([#2272](https://github.com/windmill-labs/windmill/issues/2272)) ([8f6f46d](https://github.com/windmill-labs/windmill/commit/8f6f46de199d58133b9faa77cdbcbcfd6cb962f7))
## [1.171.0](https://github.com/windmill-labs/windmill/compare/v1.170.0...v1.171.0) (2023-09-12)
### Features
* attempt to SIGTERM before SIGKILL for bash ([f40bbba](https://github.com/windmill-labs/windmill/commit/f40bbba519a97cbb1ec142c335f038dbebcd4e7c))
* zero copy result for job result ([#2263](https://github.com/windmill-labs/windmill/issues/2263)) ([22a7da5](https://github.com/windmill-labs/windmill/commit/22a7da58b1d20721892906cba2dee6fbeb1cc1fd))
### Bug Fixes
* 2257 TIME convertion in pg_executor.rs ([#2267](https://github.com/windmill-labs/windmill/issues/2267)) ([3d71253](https://github.com/windmill-labs/windmill/commit/3d71253abdb0dff1670a796d07a53ecd0a98414e))
* fix field duplicate in app background settings ([164cdaf](https://github.com/windmill-labs/windmill/commit/164cdaf09464646dee4e70a699222a454eb0d898))
* improve bun lockfile resolution ([9103ec4](https://github.com/windmill-labs/windmill/commit/9103ec445db81395a5851202eecb87301d0b4987))
* remove result and args from list completed and list queue jobs ([e7e63e1](https://github.com/windmill-labs/windmill/commit/e7e63e111a73e0986050a8fe7fdc18784ba902b0))
## [1.170.0](https://github.com/windmill-labs/windmill/compare/v1.169.0...v1.170.0) (2023-09-08)
### Features
* display jobs currently waiting for a worker ([3c950c0](https://github.com/windmill-labs/windmill/commit/3c950c03de0bc71974eb29985381adba8c098660))
* snowflake schema explorer + refactoring ([#2260](https://github.com/windmill-labs/windmill/issues/2260)) ([5cca583](https://github.com/windmill-labs/windmill/commit/5cca5833e94fc4c8a80e210164da09f2a1ceb677))
### Bug Fixes
* fix get_result for python-client ([fe41f4f](https://github.com/windmill-labs/windmill/commit/fe41f4ff4ce596cf394bd69a0ba48e88db8d2328))
## [1.169.0](https://github.com/windmill-labs/windmill/compare/v1.168.3...v1.169.0) (2023-09-08)
### Features
* benchmarks graph ([#2244](https://github.com/windmill-labs/windmill/issues/2244)) ([c496602](https://github.com/windmill-labs/windmill/commit/c496602e9e2e0dfecaaffe731e58e551d039d02f))
* big query schema explorer ([#2247](https://github.com/windmill-labs/windmill/issues/2247)) ([ec7d923](https://github.com/windmill-labs/windmill/commit/ec7d923cca0f6050855473ababd1bb27d668711b))
* flow copilot ([#2219](https://github.com/windmill-labs/windmill/issues/2219)) ([2f3138c](https://github.com/windmill-labs/windmill/commit/2f3138c65d9d3f0161bf3e069c6eec0c32ac3b86))
* **frontend:** fix runs page when the row has a parent ([#2255](https://github.com/windmill-labs/windmill/issues/2255)) ([2271263](https://github.com/windmill-labs/windmill/commit/22712632f683fb63ad6d4b475a01c63800a9559d))
* introduce container groups ([49c5553](https://github.com/windmill-labs/windmill/commit/49c5553f3b496c2aaf03376689ee0fd42ecbd2bf))
### Bug Fixes
* benchmark svg ([#2249](https://github.com/windmill-labs/windmill/issues/2249)) ([24c5802](https://github.com/windmill-labs/windmill/commit/24c580211572d6447ca502db141e90c5e084d790))
* pass TZ from env to runtimes ([75a1490](https://github.com/windmill-labs/windmill/commit/75a149009a5a13230b4d6de6eac8bba0618629d6))
## [1.168.3](https://github.com/windmill-labs/windmill/compare/v1.168.2...v1.168.3) (2023-09-07)

View File

@@ -180,7 +180,7 @@ RUN chmod 755 /usr/bin/deno
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
COPY --from=oven/bun:1.0.0 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:0.8.0 /usr/local/bin/bun /usr/bin/bun
# add the docker client to call docker from a worker if enabled
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/

View File

@@ -6,9 +6,8 @@ Every file is under copyright (c) Windmill Labs, Inc 2022 unless otherwise speci
Every file is under License AGPL unless otherwise specified
or belonging to one of the below cases:
The files under backend/ are AGPLv3 Licensed, except any snippets of code under the compile flag "enterprise". Those snippets and files are under a proprietary and commerccial license.
The files under frontend/ are AGPLv3 Licensed, except any snippers of code that require a positive license check to be activated. Those snippets and files are under a proprietary and commercial license.
Private and public forks MUST not include any of the above proprietary and commercial code. Windmill Labs, Inc. provide tools to clean the codebase from those snippets upon demand.
The files under backend/ are AGPLv3 Licensed.
The files under frontend/ are AGPLv3 Licensed.
The files under python-client/ deno-client/ go-client/ are Apache 2.0 Licensed.
The openapi files, including the OpenFlow spec is Apache 2.0 Licensed.

View File

@@ -1,19 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags, worker_group, dedicated_worker) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (worker) DO UPDATE set ip = $3, custom_tags = $4, worker_group = $5",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"TextArray",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "03c7f098ad795d216d58ded0bf4cf6473960377455b5fd7ac3b578a1d36c0cc6"
}

View File

@@ -1,16 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE worker_ping SET ping_at = now(), jobs_executed = $1, custom_tags = $2 WHERE worker = $3",
"query": "UPDATE worker_ping SET ping_at = now(), jobs_executed = $1 WHERE worker = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"TextArray",
"Text"
]
},
"nullable": []
},
"hash": "47beea5cd6324b53bfb349665fb215280f32b70a617fde87f70ea53ca9ade39f"
"hash": "07551a32c49da8c0693dd39c6a63b5b2a596ccc0e52e8918160604a5e133dd32"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT script_path FROM completed_job WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "script_path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true
]
},
"hash": "16be0560028361d46bf3b842a5fa07472994d8942c684f5b75339fe71ea23cdd"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM worker_group_config WHERE name = $1 RETURNING name",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "210b7fa50246d9b8fd1a24ade787bad6882c86e9422b715844aa92b67ed05174"
}

View File

@@ -6,7 +6,7 @@
{
"ordinal": 0,
"name": "duration_ms",
"type_info": "Int8"
"type_info": "Int4"
}
],
"parameters": {

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed, custom_tags, worker_group FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
"query": "SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed, custom_tags FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
@@ -37,11 +37,6 @@
"ordinal": 6,
"name": "custom_tags",
"type_info": "TextArray"
},
{
"ordinal": 7,
"name": "worker_group",
"type_info": "Varchar"
}
],
"parameters": {
@@ -57,9 +52,8 @@
false,
false,
false,
true,
false
true
]
},
"hash": "240ce8c9b5c7530999642190c6f7915ae2734b90b8c4cd35fe37783b1d4dd0b0"
"hash": "4f6b3b472b4b78c0325cf3755f9ef1806d2e82328ceccbeade8cc2333c6dfe47"
}

View File

@@ -11,7 +11,7 @@
],
"parameters": {
"Left": [
"Int8"
"Int4"
]
},
"nullable": [

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags) VALUES ($1, $2, $3, $4) ON CONFLICT (worker) DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"TextArray"
]
},
"nullable": []
},
"hash": "61e6aac871b482b6e36f866b4ec9148a75e1bd130e7614463487e2ba6957dfdf"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT config FROM worker_group_config WHERE name = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "config",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "6c0136f7965f1e01620a7d5efd7edbc78f0bf3f815676d8b343152bfce2dfc4c"
}

View File

@@ -1,26 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM worker_group_config",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "config",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
true
]
},
"hash": "7998b23eb72f5967a0fd376fa1015bf29ba5150cd59732288ceb72ce9cecf987"
}

View File

@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM password WHERE login_type != 'password'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "8b8c8a9ada87f6cb2bee23ba5b707af4f1ad8819cca2335031126041a55f9483"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO worker_group_config (name, config) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET config = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Jsonb"
]
},
"nullable": []
},
"hash": "903f2f62f3829274f5dfa0cb1ebbb9e132d310d19ca744531a42ca2c7ac56f56"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT CAST(ROUND(AVG(duration_ms) / 1000, 0) AS INT) AS avg_duration_s FROM\n (SELECT duration_ms FROM completed_job WHERE script_path = $1\n ORDER BY started_at\n DESC LIMIT 10) AS t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "avg_duration_s",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "967ac1b17291ca80d40acc77e3ba01210bfbd0da18ea7daa61d7f3d6aa6bbcc1"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM completed_job WHERE id = $1 AND workspace_id = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "9f16a61d6a9a42f3fd3e30a1e7776503cee1b45eba150c3082eb246ea3f98d47"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT CAST(ROUND(AVG(duration_ms) / 1000, 0) AS BIGINT) AS avg_duration_s FROM\n (SELECT duration_ms FROM completed_job WHERE script_path = $1\n ORDER BY started_at\n DESC LIMIT 10) AS t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "avg_duration_s",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "a0ce703def7e976947513029874fb571893c75730e0a8feeb609852423404cf0"
}

View File

@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM workspace WHERE id != 'admins' AND deleted = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "e87d4e8d6b52a92537b02a02631a393a2f8b17a5678a1f4f9326db86b417e468"
}

View File

@@ -6,7 +6,7 @@
{
"ordinal": 0,
"name": "duration",
"type_info": "Numeric"
"type_info": "Int8"
}
],
"parameters": {

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM completed_job WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "database_length!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "f5681df5935ec753f4a6d9ab8b927bbf890f0b4fe2de16dc764b24f24d7bbdca"
}

324
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.173.0"
version = "1.168.3"
authors.workspace = true
edition.workspace = true
@@ -22,7 +22,7 @@ members = [
]
[workspace.package]
version = "1.173.0"
version = "1.168.3"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -96,7 +96,7 @@ tower = "^0"
tower-http = { version = "^0", features = ["trace", "cors"] }
tower-cookies = "^0"
serde = "^1"
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
serde_json = { version = "^1", features = ["preserve_order"] }
uuid = { version = "^1", features = ["serde", "v4"] }
thiserror = "^1"
anyhow = "^1"
@@ -155,9 +155,7 @@ sqlx = { version = "^0", features = [
"chrono",
"postgres",
"runtime-tokio-rustls",
"bigdecimal"
] }
bigdecimal = "^0"
dotenv = "^0"
ulid = { version = "^1", features = ["uuid"] }
futures = "^0"
@@ -189,7 +187,6 @@ postgres-native-tls = "^0"
native-tls = "^0"
samael = { version = "0.0.12", features = ["xmlsec"] }
gcp_auth = "0.9.0"
rust_decimal = { version = "1.31.0", features = ["db-postgres"]}
rust_decimal = {version = "1.31.0", features = ["db-postgres"]}
jsonwebtoken = "8.3.0"
pem = "3.0.1"
nix = { version = "0.27.1", features = ["process", "signal"] }

View File

@@ -1,4 +0,0 @@
-- Add down migration script here
DROP TABLE worker_group_config;
ALTER TABLE worker_ping DROP COLUMN worker_group;

View File

@@ -1,8 +0,0 @@
-- Add up migration script here
CREATE TABLE worker_group_config (
name VARCHAR(255) PRIMARY KEY,
config JSONB DEFAULT '{}'::jsonb
);
ALTER TABLE worker_ping ADD COLUMN IF NOT EXISTS worker_group VARCHAR(255) NOT NULL DEFAULT 'default';
ALTER TABLE worker_ping ADD COLUMN IF NOT EXISTS dedicated_worker VARCHAR(255);

View File

@@ -1 +0,0 @@
-- Add down migration script here

View File

@@ -1 +0,0 @@
-- Add up migration script here

View File

@@ -1 +0,0 @@
-- Add down migration script here

View File

@@ -1,5 +0,0 @@
-- Add up migration script here
ALTER TABLE completed_job ALTER COLUMN duration_ms TYPE bigint;
ALTER TABLE completed_job ALTER COLUMN email TYPE VARCHAR(255);
ALTER TABLE queue ALTER COLUMN email TYPE VARCHAR(255);
ALTER TABLE queue ALTER COLUMN canceled_by TYPE VARCHAR(255);

View File

@@ -35,7 +35,6 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
"dateutil" => "python-dateutil",
"mailparser" => "mail-parser",
"mailparser-reply" => "mail-parser-reply",
"gitlab" => "python-gitlab"
};
fn replace_import(x: String) -> String {

View File

@@ -8,14 +8,15 @@
use gethostname::gethostname;
use git_version::git_version;
use monitor::handle_zombie_jobs_periodically;
use sqlx::{Pool, Postgres};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
time::Duration,
};
use tokio::{
fs::{metadata, DirBuilder},
join,
sync::RwLock,
};
use windmill_api::{LICENSE_KEY, OAUTH_CLIENTS, SMTP_CLIENT};
@@ -27,8 +28,6 @@ use windmill_worker::{
PIP_CACHE_DIR, ROOT_TMP_CACHE_DIR, TAR_PIP_TMP_CACHE_DIR,
};
use crate::monitor::monitor_db;
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
const DEFAULT_NUM_WORKERS: usize = 1;
const DEFAULT_PORT: u16 = 8000;
@@ -136,10 +135,7 @@ Windmill Community Edition {GIT_VERSION}
tracing::info!("Smtp client connected.");
}
}
let worker_mode = num_workers > 0;
if server_mode || worker_mode {
if server_mode || num_workers > 0 {
let port_var = std::env::var("PORT").ok().and_then(|x| x.parse().ok());
let port = if server_mode {
@@ -148,19 +144,6 @@ Windmill Community Edition {GIT_VERSION}
port_var.unwrap_or(0)
};
// since it's only on server mode, the port is statically defined
let base_internal_url: String = format!("http://localhost:{}", port.to_string());
monitor_db(
&db,
tx.clone(),
&base_internal_url,
rsmq.clone(),
worker_mode,
server_mode,
)
.await;
if std::env::var("BASE_INTERNAL_URL").is_ok() {
tracing::warn!("BASE_INTERNAL_URL is now unecessary and ignored, you can remove it.");
}
@@ -178,11 +161,10 @@ Windmill Community Edition {GIT_VERSION}
let workers_f = async {
let port = port_rx.await?;
let base_internal_url: String = format!("http://localhost:{}", port.to_string());
if worker_mode {
if num_workers > 0 {
run_workers(
db.clone(),
rx.resubscribe(),
tx.clone(),
num_workers,
base_internal_url.clone(),
rsmq.clone(),
@@ -194,48 +176,29 @@ Windmill Community Edition {GIT_VERSION}
Ok(()) as anyhow::Result<()>
};
let rsmq2 = rsmq.clone();
let monitor_f = async {
let db = db.clone();
let tx = tx.clone();
let rsmq = rsmq.clone();
let mut rx = rx.resubscribe();
let base_internal_url = base_internal_url.to_string();
tokio::spawn(async move {
//monitor_db is applied at start, no need to apply it twice
tokio::time::sleep(Duration::from_secs(30)).await;
loop {
monitor_db(
&db,
tx.clone(),
&base_internal_url,
rsmq.clone(),
worker_mode,
server_mode,
)
.await;
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(30)) => (),
_ = rx.recv() => {
println!("received killpill for monitor job");
break;
}
}
}
});
if server_mode {
// since it's only on server mode, the port is statically defined
let base_internal_url: String = format!("http://localhost:{}", port.to_string());
monitor_db(&db, rx.resubscribe(), &base_internal_url, rsmq2).await;
}
Ok(()) as anyhow::Result<()>
};
let metrics_f = async {
if let Some(_addr) = metrics_addr {
#[cfg(not(feature = "enterprise"))]
panic!("Metrics are only available in the Enterprise Edition");
match metrics_addr {
Some(_addr) => {
#[cfg(not(feature = "enterprise"))]
panic!("Metrics are only available in the Enterprise Edition");
#[cfg(feature = "enterprise")]
windmill_common::serve_metrics(_addr, rx.resubscribe(), num_workers > 0).await;
#[cfg(feature = "enterprise")]
windmill_common::serve_metrics(_addr, rx.resubscribe(), num_workers > 0)
.await
.map_err(anyhow::Error::from)
}
None => Ok(()),
}
Ok(()) as anyhow::Result<()>
};
futures::try_join!(shutdown_signal, server_f, metrics_f, workers_f, monitor_f)?;
@@ -262,10 +225,28 @@ fn display_config(envs: &[&str]) {
)
}
pub async fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
db: &Pool<Postgres>,
rx: tokio::sync::broadcast::Receiver<()>,
base_internal_url: &str,
rsmq: Option<R>,
) -> tokio::task::JoinHandle<()> {
let db1 = db.clone();
let db2 = db.clone();
let rx2 = rx.resubscribe();
let base_internal_url = base_internal_url.to_string();
tokio::spawn(async move {
join!(
handle_zombie_jobs_periodically(&db1, rx, &base_internal_url, rsmq),
windmill_api::delete_expired_items_perdiodically(&db2, rx2)
);
})
}
pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
db: Pool<Postgres>,
rx: tokio::sync::broadcast::Receiver<()>,
tx: tokio::sync::broadcast::Sender<()>,
num_workers: i32,
base_internal_url: String,
rsmq: Option<R>,
@@ -339,7 +320,6 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
let worker_name = format!("wk-{}-{}", &instance_name, rd_string(5));
let ip = ip.clone();
let rx = rx.resubscribe();
let tx = tx.clone();
let base_internal_url = base_internal_url.clone();
let rsmq2 = rsmq.clone();
let sync_barrier = sync_barrier.clone();
@@ -353,7 +333,6 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
num_workers as u32,
&ip,
rx,
tx,
&base_internal_url,
rsmq2,
sync_barrier,

View File

@@ -1,11 +1,12 @@
use std::time::Duration;
use once_cell::sync::OnceCell;
use sqlx::{Pool, Postgres};
use tokio::{join, sync::mpsc};
use tokio::sync::mpsc;
use uuid::Uuid;
use windmill_common::{
error,
jobs::{JobKind, QueuedJob},
worker::{load_worker_config, reload_custom_tags_setting, WORKER_CONFIG},
METRICS_ENABLED,
};
use windmill_worker::{
@@ -36,61 +37,23 @@ lazy_static::lazy_static! {
.unwrap();
}
pub async fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
pub async fn handle_zombie_jobs_periodically<
R: rsmq_async::RsmqConnection + Send + Sync + Clone,
>(
db: &Pool<Postgres>,
tx: tokio::sync::broadcast::Sender<()>,
mut rx: tokio::sync::broadcast::Receiver<()>,
base_internal_url: &str,
rsmq: Option<R>,
worker_mode: bool,
server_mode: bool,
) {
let zombie_jobs_f = async {
if server_mode {
handle_zombie_jobs(db, base_internal_url, rsmq.clone()).await;
}
};
let expired_items_f = async {
if server_mode {
windmill_api::delete_expired_items(&db).await;
}
};
let reload_worker_config_f = async {
if worker_mode {
reload_worker_config(&db, tx).await;
}
};
let reload_custom_tags_f = async {
if server_mode {
if let Err(e) = reload_custom_tags_setting(db).await {
tracing::error!("Error reloading custom tags: {:?}", e)
}
}
};
join!(
expired_items_f,
zombie_jobs_f,
reload_worker_config_f,
reload_custom_tags_f
);
}
loop {
handle_zombie_jobs(db, base_internal_url, rsmq.clone()).await;
pub async fn reload_worker_config(db: &Pool<Postgres>, tx: tokio::sync::broadcast::Sender<()>) {
let config = load_worker_config(&db).await;
if let Err(e) = config {
tracing::error!("Error reloading worker config: {:?}", e)
} else {
let wc = WORKER_CONFIG.read().await;
let config = config.unwrap();
if *wc != config {
if (*wc).dedicated_worker != config.dedicated_worker {
tracing::info!("Dedicated worker config changed, sending killpill. Expecting to be restarted by supervisor.");
let _ = tx.send(());
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(30)) => (),
_ = rx.recv() => {
println!("received killpill for monitor job");
break;
}
drop(wc);
let mut wc = WORKER_CONFIG.write().await;
tracing::info!("Reloading worker config...");
*wc = config
}
}
}

View File

@@ -5,64 +5,22 @@ use futures::StreamExt;
use futures::{stream, Stream};
use serde::Deserialize;
use serde_json::json;
use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres, query};
use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres, Transaction};
use tokio::{
sync::RwLock,
time::{timeout, Duration},
};
use windmill_api::jobs::{CompletedJob, Job};
use windmill_api_client::types::{
CreateFlowBody, EditSchedule, NewSchedule, RawScript, ScriptArgs,
};
use windmill_common::worker::{WORKER_CONFIG, load_worker_config};
use windmill_common::{
flow_status::{FlowStatus, FlowStatusModule},
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform},
jobs::{JobPayload, RawCode, JobKind},
scripts::{ScriptLang, ScriptHash}
jobs::{JobPayload, RawCode},
scripts::ScriptLang,
};
use windmill_queue::PushIsolationLevel;
use serde::Serialize;
#[derive(Debug, sqlx::FromRow, Serialize)]
pub struct CompletedJob {
pub workspace_id: String,
pub id: Uuid,
pub parent_job: Option<Uuid>,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub started_at: chrono::DateTime<chrono::Utc>,
pub duration_ms: i32,
pub success: bool,
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
pub result: Option<serde_json::Value>,
pub logs: Option<String>,
pub deleted: bool,
pub raw_code: Option<String>,
pub canceled: bool,
pub canceled_by: Option<String>,
pub canceled_reason: Option<String>,
pub schedule_path: Option<String>,
pub permissioned_as: String,
pub flow_status: Option<serde_json::Value>,
pub raw_flow: Option<serde_json::Value>,
pub is_flow_step: bool,
pub is_skipped: bool,
pub email: String,
pub visible_to_owner: bool,
pub mem_peak: Option<i32>,
pub tag: String,
pub script_hash: Option<ScriptHash>,
pub language: Option<ScriptLang>,
pub job_kind: JobKind,
}
impl CompletedJob {
pub fn json_result(&self) -> Option<serde_json::Value> {
self.result.clone()
}
}
use windmill_queue::{get_queued_job, PushIsolationLevel};
async fn initialize_tracing() {
use std::sync::Once;
@@ -96,6 +54,37 @@ fn next_worker_name() -> String {
format!("{id}/{thread_name}")
}
pub async fn get_job_by_id<'c>(
mut tx: Transaction<'c, Postgres>,
w_id: &str,
id: Uuid,
) -> windmill_common::error::Result<(Option<Job>, Transaction<'c, Postgres>)> {
let cjob_option = sqlx::query_as::<_, CompletedJob>(
"SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
.bind(w_id)
.fetch_optional(&mut *tx)
.await?;
let job_option = match cjob_option {
Some(job) => Some(Job::CompletedJob(job)),
None => get_queued_job(id, w_id, &mut tx).await?.map(Job::QueuedJob),
};
if job_option.is_some() {
Ok((job_option, tx))
} else {
// check if a job had been moved in-between queries
let cjob_option = sqlx::query_as::<_, CompletedJob>(
"SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
.bind(w_id)
.fetch_optional(&mut *tx)
.await?;
Ok((cjob_option.map(Job::CompletedJob), tx))
}
}
pub struct ApiServer {
pub addr: std::net::SocketAddr,
tx: tokio::sync::broadcast::Sender<()>,
@@ -132,15 +121,15 @@ impl ApiServer {
}
}
// async fn _print_job(id: Uuid, db: &Pool<Postgres>) -> Result<(), anyhow::Error> {
// tracing::info!(
// "{:#?}",
// get_job_by_id(db.begin().await?, "test-workspace", id)
// .await?
// .0
// );
// Ok(())
// }
async fn _print_job(id: Uuid, db: &Pool<Postgres>) -> Result<(), anyhow::Error> {
tracing::info!(
"{:#?}",
get_job_by_id(db.begin().await?, "test-workspace", id)
.await?
.0
);
Ok(())
}
fn get_module(cjob: &CompletedJob, id: &str) -> Option<FlowStatusModule> {
cjob.flow_status.clone().and_then(|fs| {
@@ -310,7 +299,7 @@ mod suspend_resume {
server.close().await.unwrap();
let result = completed_job(flow, &db).await.json_result().unwrap();
let result = completed_job(flow, &db).await.result.unwrap();
assert_eq!(
json!({
@@ -348,7 +337,7 @@ mod suspend_resume {
.arg("port", json!(port))
.run_until_complete(&db, port)
.await
.json_result()
.result
.unwrap();
server.close().await.unwrap();
@@ -411,7 +400,7 @@ mod suspend_resume {
server.close().await.unwrap();
let result = completed_job(flow, &db).await.json_result().unwrap();
let result = completed_job(flow, &db).await.result.unwrap();
assert_eq!(
json!( {"error": {"name": "Canceled", "reason": "approval request disapproved", "message": "Job canceled: approval request disapproved by unknown", "canceler": "unknown"}}),
@@ -566,7 +555,7 @@ def main(last, port):
.arg("port", json!(server.addr.port()))
.run_until_complete(&db, server.addr.port())
.await
.json_result()
.result
.unwrap();
assert_eq!(server.close().await, attempts);
@@ -595,7 +584,7 @@ def main(last, port):
.arg("port", json!(server.addr.port()))
.run_until_complete(&db, server.addr.port())
.await
.json_result()
.result
.unwrap();
assert_eq!(server.close().await, attempts);
@@ -637,7 +626,7 @@ def main(last, port):
.run_until_complete(&db, server.addr.port())
.await;
let result = job.json_result().unwrap();
let result = job.result.unwrap();
assert_eq!(server.close().await, attempts);
assert!(result["error"]
.as_object()
@@ -701,7 +690,7 @@ def main(error, port):
.arg("port", json!(server.addr.port()))
.run_until_complete(&db, server.addr.port())
.await;
let result = cjob.json_result().clone().unwrap();
let result = cjob.result.clone().unwrap();
let failed_module = get_module(&cjob, "a").unwrap();
match failed_module {
FlowStatusModule::Failure { .. } => {}
@@ -759,7 +748,7 @@ async fn test_iteration(db: Pool<Postgres>) {
.arg("items", json!([]))
.run_until_complete(&db, server.addr.port())
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!([]));
@@ -768,7 +757,7 @@ async fn test_iteration(db: Pool<Postgres>) {
.arg("items", json!((0..257).collect::<Vec<_>>()))
.run_until_complete(&db, server.addr.port())
.await
.json_result()
.result
.unwrap();
assert!(matches!(result, serde_json::Value::Array(_)));
assert!(result[2]["error"]
@@ -816,7 +805,7 @@ async fn test_iteration_parallel(db: Pool<Postgres>) {
.arg("items", json!([]))
.run_until_complete(&db, server.addr.port())
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!([]));
@@ -826,7 +815,7 @@ async fn test_iteration_parallel(db: Pool<Postgres>) {
.run_until_complete(&db, server.addr.port())
.await;
// println!("{:#?}", job);
let result = job.json_result().unwrap();
let result = job.result.unwrap();
assert!(matches!(result, serde_json::Value::Array(_)));
assert!(result[2]["error"]
.as_object()
@@ -957,12 +946,8 @@ fn spawn_test_worker(
let worker_name: String = next_worker_name();
let ip: &str = Default::default();
let tx2 = tx.clone();
let future = async move {
let base_internal_url = format!("http://localhost:{}", port);
let mut wc = WORKER_CONFIG.write().await;
*wc = load_worker_config(&db).await.unwrap();
drop(wc);
windmill_worker::run_worker::<rsmq_async::MultiplexedRsmq>(
&db,
worker_instance,
@@ -971,7 +956,6 @@ fn spawn_test_worker(
1,
ip,
rx,
tx2,
&base_internal_url,
None,
Arc::new(RwLock::new(None)),
@@ -1011,8 +995,8 @@ async fn listen_for_uuid_on(
}
async fn completed_job(uuid: Uuid, db: &Pool<Postgres>) -> CompletedJob {
sqlx::query_as::<_, CompletedJob>("SELECT * FROM completed_job WHERE id = $1").bind(uuid)
sqlx::query_as::<_, CompletedJob>("SELECT * FROM completed_job WHERE id = $1")
.bind(uuid)
.fetch_one(db)
.await
.unwrap()
@@ -1122,7 +1106,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
println!("deno flow iteration: {}", i);
let job = run_job_in_new_worker_until_complete(&db, job.clone(), port).await;
// println!("job: {:#?}", job.flow_status);
let result = job.json_result().unwrap();
let result = job.result.unwrap();
assert_eq!(result, serde_json::json!([2, 4, 6]), "iteration: {}", i);
}
}
@@ -1158,7 +1142,7 @@ async fn test_identity(db: Pool<Postgres>) {
let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None })
.run_until_complete(&db, server.addr.port())
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!(42));
}
@@ -1347,7 +1331,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
let result = run_job_in_new_worker_until_complete(&db, job.clone(), server.addr.port())
.await
.json_result()
.result
.unwrap();
assert_eq!(
result,
@@ -1402,7 +1386,7 @@ async fn test_flow_result_by_id(db: Pool<Postgres>) {
let job = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, job.clone(), port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!([[42]]));
}
@@ -1447,7 +1431,7 @@ async fn test_stop_after_if(db: Pool<Postgres>) {
.arg("n", json!(123))
.run_until_complete(&db, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(json!("last step saw 123"), result);
@@ -1456,7 +1440,7 @@ async fn test_stop_after_if(db: Pool<Postgres>) {
.run_until_complete(&db, port)
.await;
let result = cjob.json_result().unwrap();
let result = cjob.result.unwrap();
assert_eq!(json!(-123), result);
}
@@ -1505,7 +1489,7 @@ async fn test_stop_after_if_nested(db: Pool<Postgres>) {
.arg("n", json!(123))
.run_until_complete(&db, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(json!("last step saw [123]"), result);
@@ -1514,7 +1498,7 @@ async fn test_stop_after_if_nested(db: Pool<Postgres>) {
.run_until_complete(&db, port)
.await;
let result = cjob.json_result().unwrap();
let result = cjob.result.unwrap();
assert_eq!(json!([-123]), result);
}
@@ -1568,7 +1552,7 @@ async fn test_python_flow(db: Pool<Postgres>) {
port,
)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!([2, 4, 6]), "iteration: {i}");
@@ -1603,7 +1587,7 @@ async fn test_python_flow_2(db: Pool<Postgres>) {
port,
)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!("Hello"), "iteration: {i}");
@@ -1640,7 +1624,7 @@ func main(derp string) (string, error) {
.arg("derp", json!("world"))
.run_until_complete(&db, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!("hello world"));
@@ -1671,7 +1655,7 @@ echo "hello $msg"
.run_until_complete(&db, port)
.await;
assert_eq!(job.json_result(), Some(json!("hello world")));
assert_eq!(job.result, Some(json!("hello world")));
}
#[sqlx::test(fixtures("base"))]
@@ -1698,7 +1682,7 @@ def main():
let result = run_job_in_new_worker_until_complete(&db, job, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!("hello world"));
@@ -1731,7 +1715,7 @@ def main():
let result = run_job_in_new_worker_until_complete(&db, job, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!(3));
@@ -1763,7 +1747,7 @@ def main():
let result = run_job_in_new_worker_until_complete(&db, job, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!("test-workspace"));
@@ -1819,7 +1803,7 @@ async fn test_empty_loop(db: Pool<Postgres>) {
let flow = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, flow, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!(0));
@@ -1859,7 +1843,7 @@ async fn test_invalid_first_step(db: Pool<Postgres>) {
let job = run_job_in_new_worker_until_complete(&db, flow, port).await;
assert_eq!(
job.json_result().unwrap(),
job.result.unwrap(),
serde_json::json!( {"error": {"name": "InternalErr", "message": "Expected an array value, found: {}"}})
);
}
@@ -1900,7 +1884,7 @@ async fn test_empty_loop_2(db: Pool<Postgres>) {
let flow = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, flow, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!([]));
@@ -1955,7 +1939,7 @@ async fn test_step_after_loop(db: Pool<Postgres>) {
let flow = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, flow, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!(9));
@@ -2023,7 +2007,7 @@ async fn test_branchone_simple(db: Pool<Postgres>) {
let flow = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, flow, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!([1, 2]));
@@ -2059,7 +2043,7 @@ async fn test_branchone_with_cond(db: Pool<Postgres>) {
let flow = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, flow, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!([1, 3]));
@@ -2097,7 +2081,7 @@ async fn test_branchall_sequential(db: Pool<Postgres>) {
let flow = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, flow, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!([[1, 2], [1, 3]]));
@@ -2134,7 +2118,7 @@ async fn test_branchall_simple(db: Pool<Postgres>) {
let flow = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, flow, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!([[1, 2], [1, 3]]));
@@ -2181,7 +2165,7 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
let flow = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, flow, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(
@@ -2218,7 +2202,7 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
let flow = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, flow, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(
@@ -2282,7 +2266,7 @@ async fn test_branchone_nested(db: Pool<Postgres>) {
let flow = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, flow, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(result, serde_json::json!([1, 2, 3]));
@@ -2339,7 +2323,7 @@ async fn test_branchall_nested(db: Pool<Postgres>) {
let flow = JobPayload::RawFlow { value: flow, path: None };
let result = run_job_in_new_worker_until_complete(&db, flow, port)
.await
.json_result()
.result
.unwrap();
println!("{:#?}", result);
@@ -2404,7 +2388,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
.arg("n", json!(0))
.run_until_complete(&db, port)
.await
.json_result()
.result
.unwrap();
assert!(result["from failure module"]["error"]
@@ -2420,7 +2404,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
.arg("n", json!(1))
.run_until_complete(&db, port)
.await
.json_result()
.result
.unwrap();
assert!(result["from failure module"]["error"]
@@ -2436,7 +2420,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
.arg("n", json!(2))
.run_until_complete(&db, port)
.await
.json_result()
.result
.unwrap();
assert!(result["from failure module"]["error"]
@@ -2452,7 +2436,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
.arg("n", json!(3))
.run_until_complete(&db, port)
.await
.json_result()
.result
.unwrap();
assert_eq!(json!({ "l": [0, 1, 2] }), result);
}
@@ -2602,7 +2586,6 @@ async fn test_rust_client(db: Pool<Postgres>) {
.unwrap();
}
#[sqlx::test(fixtures("base"))]
async fn test_script_schedule_handlers(db: Pool<Postgres>) {
initialize_tracing().await;
@@ -2659,7 +2642,8 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
let uuid = uuid.unwrap().unwrap();
let completed_job =
query!("SELECT script_path FROM completed_job WHERE id = $1", uuid)
sqlx::query_as::<_, CompletedJob>("SELECT * FROM completed_job WHERE id = $1")
.bind(uuid)
.fetch_one(&db2)
.await
.unwrap();
@@ -2718,7 +2702,8 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
let uuid = uuid.unwrap().unwrap();
let completed_job =
query!("SELECT script_path FROM completed_job WHERE id = $1", uuid)
sqlx::query_as::<_, CompletedJob>("SELECT * FROM completed_job WHERE id = $1")
.bind(uuid)
.fetch_one(&db2)
.await
.unwrap();
@@ -2793,7 +2778,8 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
let uuid = uuid.unwrap().unwrap();
let completed_job =
query!("SELECT script_path FROM completed_job WHERE id = $1", uuid)
sqlx::query_as::<_, CompletedJob>("SELECT * FROM completed_job WHERE id = $1")
.bind(uuid)
.fetch_one(&db2)
.await
.unwrap();
@@ -2853,7 +2839,8 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
let uuid = uuid.unwrap().unwrap();
let completed_job =
query!("SELECT script_path FROM completed_job WHERE id = $1", uuid)
sqlx::query_as::<_, CompletedJob>("SELECT * FROM completed_job WHERE id = $1")
.bind(uuid)
.fetch_one(&db2)
.await
.unwrap();

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.173.0
version: 1.168.3
title: Windmill API
contact:
@@ -2386,46 +2386,6 @@ paths:
- content
- language
/scripts/hub/query:
get:
summary: query hub scripts by similarity
operationId: queryHubScripts
tags:
- script
parameters:
- name: text
description: query text
in: query
required: true
schema:
type: string
- name: kind
description: query scripts kind
in: query
required: false
schema:
type: string
- name: limit
description: query limit
in: query
required: false
schema:
type: number
responses:
"200":
description: script details
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: string
required:
- id
/w/{workspace}/scripts/list:
get:
summary: list all available scripts
@@ -3954,27 +3914,6 @@ paths:
required:
- database_length
/w/{workspace}/jobs/completed/count:
get:
summary: get completed count
operationId: getCompletedCount
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: completed count
content:
application/json:
schema:
type: object
properties:
database_length:
type: integer
required:
- database_length
/w/{workspace}/jobs/queue/cancel_all:
post:
summary: cancel all jobs
@@ -5252,65 +5191,6 @@ paths:
items:
$ref: "#/components/schemas/WorkerPing"
/workers/list_worker_groups:
get:
summary: list workers
operationId: listWorkerGroups
tags:
- worker
responses:
"200":
description: a list of workers
content:
application/json:
schema:
type: array
items:
type: object
properties:
name:
type: string
config: {}
required:
- name
- config
/workers/worker_group/{name}:
post:
summary: Update Worker Group
operationId: updateWorkerGroup
tags:
- worker
parameters:
- $ref: "#/components/parameters/Name"
requestBody:
description: worker group
required: true
content:
application/json:
schema: {}
responses:
"200":
description: Update a worker group
content:
text/plain:
schema:
type: string
delete:
summary: Delete Worker Group
operationId: deleteWorkerGroup
tags:
- worker
parameters:
- $ref: "#/components/parameters/Name"
responses:
"200":
description: Delete a worker group
content:
text/plain:
schema:
type: string
/w/{workspace}/acls/get/{kind}/{path}:
get:
summary: get granular acls
@@ -7231,8 +7111,6 @@ components:
type: array
items:
type: string
worker_group:
type: string
required:
- worker
- worker_instance
@@ -7240,7 +7118,6 @@ components:
- started_at
- ip
- jobs_executed
- worker_group
UserWorkspaceList:
type: object

View File

@@ -508,7 +508,6 @@ pub async fn get_hub_app_by_id(
&format!("https://hub.windmill.dev/apps/{id}/json"),
&email,
false,
None,
)
.await?
.json()

View File

@@ -160,7 +160,6 @@ pub async fn get_hub_flow_by_id(
&format!("https://hub.windmill.dev/flows/{id}/json"),
&email,
false,
None,
)
.await?
.json()

View File

@@ -15,7 +15,6 @@ use axum::{
Json, Router,
};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{db::UserDB, users::username_to_permissioned_as};
use windmill_common::{
error::{Error, JsonResult, Result},
@@ -24,6 +23,7 @@ use windmill_common::{
use serde::{Deserialize, Serialize};
use sqlx::{query_scalar, FromRow, Postgres, Transaction};
use windmill_queue::CLOUD_HOSTED;
pub fn workspaced_service() -> Router {
Router::new()
@@ -212,7 +212,6 @@ async fn _check_nb_of_groups(db: &DB) -> Result<()> {
}
return Ok(());
}
async fn create_group(
authed: ApiAuthed,
Extension(_db): Extension<DB>,

View File

@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use crate::db::ApiAuthed;
use crate::{db::ApiAuthed, jobs::CompletedJob};
use axum::{
extract::{Path, Query},
routing::{get, post},
@@ -15,7 +15,7 @@ use axum::{
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sqlx::{types::Uuid, FromRow};
use sqlx::types::Uuid;
use std::{
fmt::{Display, Formatter},
vec,
@@ -102,15 +102,6 @@ pub struct Input {
success: bool,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct CompletedJobMini {
id: Uuid,
created_at: chrono::DateTime<chrono::Utc>,
args: Option<serde_json::Value>,
created_by: String,
success: bool,
}
async fn get_input_history(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -123,13 +114,13 @@ async fn get_input_history(
let mut tx = user_db.begin(&authed).await?;
let sql = &format!(
"select id, created_at, created_by, args, success from completed_job \
"select * from completed_job \
where {} = $1 and job_kind = $2 and workspace_id = $3 \
order by created_at desc limit $4 offset $5",
r.runnable_type.column_name()
);
let query = sqlx::query_as::<_, CompletedJobMini>(sql);
let query = sqlx::query_as::<_, CompletedJob>(sql);
let query = match r.runnable_type {
RunnableType::ScriptHash => query.bind(to_i64(&r.runnable_id)?),

View File

@@ -13,6 +13,7 @@ use crate::{
users::{check_scopes, require_owner_of_path, OptAuthed},
utils::require_super_admin,
variables::get_workspace_key,
workers::{CUSTOM_TAGS, CUSTOM_TAGS_PER_WORKSPACE},
BASE_URL,
};
use anyhow::Context;
@@ -28,12 +29,10 @@ use hmac::Mac;
use hyper::{header::CONTENT_TYPE, http, HeaderMap, Request, StatusCode};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sql_builder::{prelude::*, quote, SqlBuilder};
use sqlx::types::JsonRawValue;
use sqlx::{query_scalar, types::Uuid, FromRow, Postgres, Transaction};
use tower_http::cors::{Any, CorsLayer};
use urlencoding::encode;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::worker::CUSTOM_TAGS_PER_WORKSPACE;
use windmill_common::{
db::UserDB,
error::{self, to_anyhow, Error},
@@ -45,7 +44,7 @@ use windmill_common::{
users::username_to_permissioned_as,
utils::{not_found_if_none, now_from_db, paginate, require_admin, Pagination, StripPath},
};
use windmill_queue::{job_is_complete, push, PushIsolationLevel};
use windmill_queue::{get_queued_job, push, PushIsolationLevel};
pub fn workspaced_service() -> Router {
let cors = CorsLayer::new()
@@ -111,7 +110,6 @@ pub fn workspaced_service() -> Router {
.route("/queue/list", get(list_queue_jobs))
.route("/queue/count", get(count_queue_jobs))
.route("/queue/cancel_all", post(cancel_all))
.route("/completed/count", get(count_completed_jobs))
.route(
"/completed/list",
get(list_completed_jobs).layer(cors.clone()),
@@ -232,15 +230,15 @@ async fn cancel_job_api(
tx.commit().await?;
Ok(id.to_string())
} else {
let (job_o, tx) = get_job_by_id(tx, &w_id, id).await?;
tx.commit().await?;
if job_is_complete(&db, id, &w_id).await.unwrap_or(false) {
return Ok(format!("queued job id {} is already completed", id));
} else {
return Err(error::Error::NotFound(format!(
"queued job id {} does not exist",
id
)));
}
let err = match job_o {
Some(Job::CompletedJob(_)) => {
return Ok(format!("queued job id {} is already completed", id))
}
_ => error::Error::NotFound(format!("queued job id {} does not exist", id)),
};
Err(err)
}
}
@@ -275,15 +273,15 @@ async fn force_cancel(
tx.commit().await?;
Ok(id.to_string())
} else {
let (job_o, tx) = get_job_by_id(tx, &w_id, id).await?;
tx.commit().await?;
if job_is_complete(&db, id, &w_id).await.unwrap_or(false) {
return Ok(format!("queued job id {} is already completed", id));
} else {
return Err(error::Error::NotFound(format!(
"queued job id {} does not exist",
id
)));
}
let err = match job_o {
Some(Job::CompletedJob(_)) => {
return Ok(format!("queued job id {} is already completed", id))
}
_ => error::Error::NotFound(format!("queued job id {} does not exist", id)),
};
Err(err)
}
}
@@ -346,29 +344,12 @@ pub async fn get_path_tag_limits_cache_for_hash(
async fn get_job(
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
let cjob_option =
sqlx::query("SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(&w_id)
.fetch_optional(&db)
.await?;
if let Some(job) = cjob_option {
let job = Job::CompletedJob(CompletedJob::from_row(&job)?);
Ok(Json(job).into_response())
} else {
let job_o = sqlx::query_as::<_, QueuedJob>(
"SELECT *
FROM queue WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
.bind(&w_id)
.fetch_optional(&db)
.await?
.map(Job::QueuedJob);
let job: Job<'_> = not_found_if_none(job_o, "Job", id.to_string())?;
Ok(Json(job).into_response())
}
) -> error::JsonResult<Job> {
let tx = db.begin().await?;
let (job_o, tx) = get_job_by_id(tx, &w_id, id).await?;
let job = not_found_if_none(job_o, "Job", id.to_string())?;
tx.commit().await?;
Ok(Json(job))
}
async fn get_job_logs(
@@ -387,8 +368,39 @@ async fn get_job_logs(
Ok(text)
}
pub async fn get_job_by_id<'c>(
mut tx: Transaction<'c, Postgres>,
w_id: &str,
id: Uuid,
) -> error::Result<(Option<Job>, Transaction<'c, Postgres>)> {
let cjob_option = sqlx::query_as::<_, CompletedJob>(
"SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
.bind(w_id)
.fetch_optional(&mut *tx)
.await?;
let job_option = match cjob_option {
Some(job) => Some(Job::CompletedJob(job)),
None => get_queued_job(id, w_id, &mut tx).await?.map(Job::QueuedJob),
};
if job_option.is_some() {
Ok((job_option, tx))
} else {
// check if a job had been moved in-between queries
let cjob_option = sqlx::query_as::<_, CompletedJob>(
"SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
.bind(w_id)
.fetch_optional(&mut *tx)
.await?;
Ok((cjob_option.map(Job::CompletedJob), tx))
}
}
#[derive(Debug, sqlx::FromRow, Serialize)]
pub struct CompletedJob<'rows> {
pub struct CompletedJob {
pub workspace_id: String,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -396,15 +408,15 @@ pub struct CompletedJob<'rows> {
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub started_at: chrono::DateTime<chrono::Utc>,
pub duration_ms: i64,
pub duration_ms: i32,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub result: Option<&'rows JsonRawValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logs: Option<String>,
pub deleted: bool,
@@ -434,64 +446,6 @@ pub struct CompletedJob<'rows> {
pub tag: String,
}
impl<'row> CompletedJob<'row> {
pub fn json_result(&self) -> Option<serde_json::Value> {
self.result
.as_ref()
.map(|r| serde_json::from_str(r.get()).ok())
.flatten()
}
}
#[derive(Debug, sqlx::FromRow, Serialize)]
pub struct ListableCompletedJob {
pub r#type: String,
pub workspace_id: String,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub started_at: chrono::DateTime<chrono::Utc>,
pub duration_ms: i64,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub deleted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_code: Option<String>,
pub canceled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_reason: Option<String>,
pub job_kind: JobKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule_path: Option<String>,
pub permissioned_as: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_status: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_flow: Option<serde_json::Value>,
pub is_flow_step: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<ScriptLang>,
pub is_skipped: bool,
pub email: String,
pub visible_to_owner: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
pub tag: String,
}
impl<'a> IntoResponse for CompletedJob<'a> {
fn into_response(self) -> Response {
Json(self).into_response()
}
}
#[derive(Deserialize, Clone)]
pub struct RunJobQuery {
scheduled_for: Option<chrono::DateTime<chrono::Utc>>,
@@ -677,7 +631,6 @@ struct ListableQueuedJob {
pub language: Option<ScriptLang>,
pub email: String,
pub suspend: Option<i32>,
pub tag: String,
}
async fn list_queue_jobs(
@@ -696,7 +649,7 @@ async fn list_queue_jobs(
"scheduled_for",
"script_hash",
"script_path",
"null as args",
"args",
"job_kind",
"schedule_path",
"permissioned_as",
@@ -705,7 +658,6 @@ async fn list_queue_jobs(
"same_worker",
"email",
"suspend",
"tag",
],
)
.sql()?;
@@ -752,33 +704,72 @@ async fn count_queue_jobs(
))
}
async fn count_completed_jobs(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> error::JsonResult<QueueStats> {
Ok(Json(
sqlx::query_as!(
QueueStats,
"SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM completed_job WHERE workspace_id = $1",
w_id
)
.fetch_one(&db)
.await?,
))
}
async fn list_jobs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(lq): Query<ListCompletedQuery>,
) -> error::JsonResult<Vec<Job<'static>>> {
) -> error::JsonResult<Vec<Job>> {
check_scopes(&authed, || format!("listjobs"))?;
// TODO: todo!("rewrite this to just run list_queue_jobs and list_completed_jobs separately and return as one");
let (per_page, offset) = paginate(pagination);
let lqc = lq.clone();
let sqlq = list_queue_jobs_query(
&w_id,
&ListQueueQuery {
script_path_start: lq.script_path_start,
script_path_exact: lq.script_path_exact,
script_hash: lq.script_hash,
created_by: lq.created_by,
started_before: lq.started_before,
started_after: lq.started_after,
created_before: lq.created_before,
created_after: lq.created_after,
created_or_started_before: lq.created_or_started_before,
created_or_started_after: lq.created_or_started_after,
running: None,
parent_job: lq.parent_job,
order_desc: Some(true),
job_kinds: lq.job_kinds,
suspended: lq.suspended,
args: lq.args,
tag: lq.tag,
schedule_path: lq.schedule_path,
},
&[
"'QueuedJob' as typ",
"id",
"workspace_id",
"parent_job",
"created_by",
"created_at",
"started_at",
"scheduled_for",
"running",
"script_hash",
"script_path",
"null as args",
"null as duration_ms",
"null as success",
"false as deleted",
"canceled",
"canceled_by",
"job_kind",
"schedule_path",
"permissioned_as",
"is_flow_step",
"language",
"false as is_skipped",
"email",
"visible_to_owner",
"suspend",
"mem_peak",
"tag",
"concurrent_limit",
"concurrency_time_window_s",
],
);
let sqlc = list_completed_jobs_query(
&w_id,
per_page + offset,
@@ -817,74 +808,13 @@ async fn list_jobs(
"null as concurrency_time_window_s",
],
);
let sql = if lq.success.is_none() {
let sqlq = list_queue_jobs_query(
&w_id,
&ListQueueQuery {
script_path_start: lq.script_path_start,
script_path_exact: lq.script_path_exact,
script_hash: lq.script_hash,
created_by: lq.created_by,
started_before: lq.started_before,
started_after: lq.started_after,
created_before: lq.created_before,
created_after: lq.created_after,
created_or_started_before: lq.created_or_started_before,
created_or_started_after: lq.created_or_started_after,
running: None,
parent_job: lq.parent_job,
order_desc: Some(true),
job_kinds: lq.job_kinds,
suspended: lq.suspended,
args: lq.args,
tag: lq.tag,
schedule_path: lq.schedule_path,
},
&[
"'QueuedJob' as typ",
"id",
"workspace_id",
"parent_job",
"created_by",
"created_at",
"started_at",
"scheduled_for",
"running",
"script_hash",
"script_path",
"null as args",
"null as duration_ms",
"null as success",
"false as deleted",
"canceled",
"canceled_by",
"job_kind",
"schedule_path",
"permissioned_as",
"is_flow_step",
"language",
"false as is_skipped",
"email",
"visible_to_owner",
"suspend",
"mem_peak",
"tag",
"concurrent_limit",
"concurrency_time_window_s",
],
);
format!(
"{} UNION ALL {} LIMIT {} OFFSET {};",
&sqlq.subquery()?,
&sqlc.subquery()?,
per_page,
offset
)
} else {
sqlc.query()?
};
let sql = format!(
"{} UNION ALL {} ORDER BY created_at DESC LIMIT {} OFFSET {};",
&sqlq.subquery()?,
&sqlc.subquery()?,
per_page,
offset
);
let mut tx = user_db.begin(&authed).await?;
let jobs: Vec<UnifiedJob> = sqlx::query_as(&sql).fetch_all(&mut *tx).await?;
tx.commit().await?;
@@ -1131,8 +1061,8 @@ pub async fn cancel_suspended_job(
}
#[derive(Serialize)]
pub struct SuspendedJobFlow<'a> {
pub job: Job<'a>,
pub struct SuspendedJobFlow {
pub job: Job,
pub approvers: Vec<Approval>,
}
@@ -1146,7 +1076,7 @@ pub async fn get_suspended_job_flow(
Extension(db): Extension<DB>,
Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>,
Query(approver): Query<QueryApprover>,
) -> error::Result<Response> {
) -> error::JsonResult<SuspendedJobFlow> {
let mut tx = db.begin().await?;
let key = get_workspace_key(&w_id, &mut tx).await?;
let mut mac = HmacSha256::new_from_slice(key.as_bytes()).map_err(to_anyhow)?;
@@ -1174,29 +1104,7 @@ pub async fn get_suspended_job_flow(
.await?
.flatten()
.ok_or_else(|| anyhow::anyhow!("parent flow job not found"))?;
let cjob_option =
sqlx::query("SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(flow_id)
.bind(&w_id)
.fetch_optional(&db)
.await?;
let mut _rows = None;
let flow_o = if let Some(job) = cjob_option {
_rows = Some(job);
Some(Job::CompletedJob(CompletedJob::from_row(
_rows.as_ref().unwrap(),
)?))
} else {
sqlx::query_as::<_, QueuedJob>(
"SELECT *
FROM queue WHERE id = $1 AND workspace_id = $2",
)
.bind(flow_id)
.bind(&w_id)
.fetch_optional(&db)
.await?
.map(Job::QueuedJob)
};
let (flow_o, mut tx) = get_job_by_id(tx, &w_id, flow_id).await?;
let flow = not_found_if_none(flow_o, "Parent Flow", job.to_string())?;
let flow_status = flow
@@ -1233,7 +1141,7 @@ pub async fn get_suspended_job_flow(
approvers_from_status
};
Ok(Json(SuspendedJobFlow { job: flow, approvers }).into_response())
Ok(Json(SuspendedJobFlow { job: flow, approvers }))
}
pub async fn create_job_signature(
@@ -1313,12 +1221,12 @@ pub async fn get_resume_urls(
#[derive(Serialize, Debug)]
#[serde(tag = "type")]
pub enum Job<'a> {
pub enum Job {
QueuedJob(QueuedJob),
CompletedJob(CompletedJob<'a>),
CompletedJob(CompletedJob),
}
impl<'a> Job<'a> {
impl Job {
pub fn raw_flow(&self) -> Option<FlowValue> {
let value = match self {
Job::QueuedJob(job) => job.raw_flow.clone(),
@@ -1348,7 +1256,8 @@ struct UnifiedJob {
running: Option<bool>,
script_hash: Option<ScriptHash>,
script_path: Option<String>,
duration_ms: Option<i64>,
args: Option<serde_json::Value>,
duration_ms: Option<i32>,
success: Option<bool>,
deleted: bool,
canceled: bool,
@@ -1368,7 +1277,7 @@ struct UnifiedJob {
concurrency_time_window_s: Option<i32>,
}
impl<'a> From<UnifiedJob> for Job<'a> {
impl From<UnifiedJob> for Job {
fn from(uj: UnifiedJob) -> Self {
match uj.typ.as_ref() {
"CompletedJob" => Job::CompletedJob(CompletedJob {
@@ -1382,7 +1291,7 @@ impl<'a> From<UnifiedJob> for Job<'a> {
success: uj.success.unwrap(),
script_hash: uj.script_hash,
script_path: uj.script_path,
args: None,
args: uj.args,
result: None,
logs: None,
flow_status: None,
@@ -1412,7 +1321,7 @@ impl<'a> From<UnifiedJob> for Job<'a> {
started_at: uj.started_at,
script_hash: uj.script_hash,
script_path: uj.script_path,
args: None,
args: uj.args,
running: uj.running.unwrap(),
scheduled_for: uj.scheduled_for.unwrap(),
logs: None,
@@ -1607,12 +1516,12 @@ fn add_raw_string(
return args;
}
async fn check_tag_available_for_workspace(w_id: &str, tag: &Option<String>) -> error::Result<()> {
fn check_tag_available_for_workspace(w_id: &str, tag: &Option<String>) -> error::Result<()> {
if let Some(tag) = tag {
if tag == "" {
return Ok(());
}
let custom_tags_per_w = CUSTOM_TAGS_PER_WORKSPACE.read().await;
let custom_tags_per_w = &*CUSTOM_TAGS_PER_WORKSPACE;
if custom_tags_per_w.0.contains(&tag.to_string()) {
Ok(())
} else if custom_tags_per_w.1.contains_key(tag)
@@ -1626,7 +1535,7 @@ async fn check_tag_available_for_workspace(w_id: &str, tag: &Option<String>) ->
} else {
return Err(error::Error::BadRequest(format!(
"Tag {tag} cannot be used on workspace {w_id}: (CUSTOM_TAGS: {:?})",
custom_tags_per_w
*CUSTOM_TAGS
)));
}
} else {
@@ -1655,7 +1564,7 @@ pub async fn run_flow_by_path(
.fetch_optional(&db)
.await?
.flatten();
check_tag_available_for_workspace(&w_id, &tag).await?;
check_tag_available_for_workspace(&w_id, &tag)?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
@@ -1705,7 +1614,7 @@ pub async fn run_job_by_path(
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
check_tag_available_for_workspace(&w_id, &tag).await?;
check_tag_available_for_workspace(&w_id, &tag)?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let (uuid, tx) = push(
@@ -1908,7 +1817,7 @@ pub async fn run_wait_result_job_by_path_get(
check_scopes(&authed, || format!("run:script/{script_path}"))?;
let (job_payload, tag) = script_path_to_payload(script_path, &db, &w_id).await?;
check_tag_available_for_workspace(&w_id, &tag).await?;
check_tag_available_for_workspace(&w_id, &tag)?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let (uuid, tx) = push(
@@ -2079,7 +1988,7 @@ async fn run_wait_result_script_by_path_internal(
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
check_tag_available_for_workspace(&w_id, &tag).await?;
check_tag_available_for_workspace(&w_id, &tag)?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let (uuid, tx) = push(
@@ -2143,7 +2052,7 @@ pub async fn run_wait_result_script_by_hash(
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
check_tag_available_for_workspace(&w_id, &tag).await?;
check_tag_available_for_workspace(&w_id, &tag)?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let (uuid, tx) = push(
@@ -2258,7 +2167,7 @@ async fn run_wait_result_flow_by_path_internal(
.fetch_optional(&db)
.await?
.flatten();
check_tag_available_for_workspace(&w_id, &tag).await?;
check_tag_available_for_workspace(&w_id, &tag)?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let (uuid, tx) = push(
@@ -2314,7 +2223,7 @@ async fn run_preview_job(
}
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let args = run_query.add_include_headers(headers, preview.args.unwrap_or_default());
check_tag_available_for_workspace(&w_id, &preview.tag).await?;
check_tag_available_for_workspace(&w_id, &preview.tag)?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let (uuid, tx) = push(
@@ -2422,7 +2331,7 @@ async fn run_preview_flow_job(
}
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let args = run_query.add_include_headers(headers, raw_flow.args.unwrap_or_default());
check_tag_available_for_workspace(&w_id, &raw_flow.tag).await?;
check_tag_available_for_workspace(&w_id, &raw_flow.tag)?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let (uuid, tx) = push(
@@ -2479,7 +2388,7 @@ pub async fn run_job_by_hash(
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
check_tag_available_for_workspace(&w_id, &tag).await?;
check_tag_available_for_workspace(&w_id, &tag)?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let (uuid, tx) = push(
@@ -2687,7 +2596,7 @@ async fn list_completed_jobs(
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(lq): Query<ListCompletedQuery>,
) -> error::JsonResult<Vec<ListableCompletedJob>> {
) -> error::JsonResult<Vec<CompletedJob>> {
check_scopes(&authed, || format!("listjobs"))?;
let (per_page, offset) = paginate(pagination);
@@ -2708,6 +2617,9 @@ async fn list_completed_jobs(
"success",
"script_hash",
"script_path",
"args",
"result",
"null as logs",
"deleted",
"canceled",
"canceled_by",
@@ -2725,92 +2637,83 @@ async fn list_completed_jobs(
"visible_to_owner",
"mem_peak",
"tag",
"'CompletedJob' as type",
],
)
.sql()?;
let jobs = sqlx::query_as::<_, ListableCompletedJob>(&sql)
let jobs = sqlx::query_as::<_, CompletedJob>(&sql)
.fetch_all(&db)
.await?;
Ok(Json(jobs))
}
async fn get_completed_job<'a>(
async fn get_completed_job(
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
let job_o = sqlx::query("SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(w_id)
.fetch_optional(&db)
.await?;
) -> error::JsonResult<CompletedJob> {
let job_o = sqlx::query_as::<_, CompletedJob>(
"SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
.bind(w_id)
.fetch_optional(&db)
.await?;
let job = not_found_if_none(job_o, "Completed Job", id.to_string())?;
Ok(CompletedJob::from_row(&job)?.into_response())
}
#[derive(FromRow)]
pub struct RawResult<'a> {
pub result: &'a JsonRawValue,
}
impl<'a> IntoResponse for RawResult<'a> {
fn into_response(self) -> Response {
Json(self.result).into_response()
}
Ok(Json(job))
}
async fn get_completed_job_result(
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
let result_o =
sqlx::query("SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(w_id)
.fetch_optional(&db)
.await?;
) -> error::JsonResult<Option<serde_json::Value>> {
let result_o = sqlx::query_scalar!(
"SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2",
id,
w_id,
)
.fetch_optional(&db)
.await?;
let result = not_found_if_none(result_o, "Completed Job", id.to_string())?;
Ok(RawResult::from_row(&result)?.into_response())
Ok(Json(result))
}
#[derive(Serialize)]
struct CompletedJobResult<'c> {
struct CompletedJobResult {
completed: bool,
result: Option<&'c JsonRawValue>,
result: Option<serde_json::Value>,
}
async fn get_completed_job_result_maybe(
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
let result_o =
sqlx::query("SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(w_id)
.fetch_optional(&db)
.await?;
) -> error::JsonResult<CompletedJobResult> {
let result_o = sqlx::query_scalar!(
"SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2",
id,
w_id,
)
.fetch_optional(&db)
.await?;
if let Some(result) = result_o {
let res = RawResult::from_row(&result)?;
Ok(Json(CompletedJobResult { completed: true, result: Some(res.result) }).into_response())
Ok(Json(CompletedJobResult { completed: true, result }))
} else {
Ok(Json(CompletedJobResult { completed: false, result: None }).into_response())
Ok(Json(CompletedJobResult { completed: false, result: None }))
}
}
async fn delete_completed_job<'a>(
async fn delete_completed_job(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
) -> error::JsonResult<CompletedJob> {
check_scopes(&authed, || format!("deletejob"))?;
let mut tx = user_db.begin(&authed).await?;
require_admin(authed.is_admin, &authed.username)?;
let job_o = sqlx::query(
let job_o = sqlx::query_as::<_, CompletedJob>(
"UPDATE completed_job SET logs = '', result = null, deleted = true WHERE id = $1 AND workspace_id = $2 \
RETURNING *",
)
@@ -2833,5 +2736,5 @@ async fn delete_completed_job<'a>(
.await?;
tx.commit().await?;
Ok(CompletedJob::from_row(&job)?.into_response())
Ok(Json(job))
}

View File

@@ -11,6 +11,7 @@ use crate::oauth2::AllClients;
use crate::saml::{SamlSsoLogin, ServiceProviderExt};
use crate::scim::has_scim_token;
use crate::tracing_init::MyOnFailure;
use crate::workers::ALL_TAGS;
use crate::{
oauth2::{build_oauth_clients, SlackVerifier},
tracing_init::{MyMakeSpan, MyOnResponse},
@@ -35,7 +36,6 @@ use tower_http::{
};
use windmill_common::db::UserDB;
use windmill_common::utils::rd_string;
use windmill_common::worker::ALL_TAGS;
use windmill_common::error::AppError;
@@ -72,7 +72,7 @@ mod workspaces;
pub const GIT_VERSION: &str =
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
pub use users::delete_expired_items;
pub use users::delete_expired_items_perdiodically;
pub const DEFAULT_BODY_LIMIT: usize = 2097152; // 2MB
lazy_static::lazy_static! {
@@ -153,7 +153,7 @@ pub async fn run_server(
port_tx: tokio::sync::oneshot::Sender<u16>,
) -> anyhow::Result<()> {
if let Some(mut rsmq) = rsmq.clone() {
for tag in ALL_TAGS.read().await.iter() {
for tag in ALL_TAGS.clone() {
let r =
rsmq_async::RsmqConnection::create_queue(&mut rsmq, &tag, None, None, None).await;
if let Err(e) = r {

View File

@@ -341,7 +341,6 @@ struct CreateAccount {
refresh_token: Option<String>,
expires_in: i64,
}
async fn create_account(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,

View File

@@ -97,21 +97,6 @@ async fn create_schedule(
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
#[cfg(not(feature = "enterprise"))]
if ns.on_recovery.is_some() {
return Err(Error::BadRequest(
"on_recovery is only available in enterprise version".to_string(),
));
}
#[cfg(not(feature = "enterprise"))]
if ns.on_failure_times.is_some() && ns.on_failure_times.unwrap() > 1 {
return Err(Error::BadRequest(
"on_failure with a number of times > 1 is only available in enterprise version"
.to_string(),
));
}
cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
check_path_conflict(tx.transaction_mut(), &w_id, &ns.path).await?;
check_flow_conflict(

View File

@@ -19,7 +19,7 @@ use axum::{
Json, Router,
};
use hyper::StatusCode;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use serde_json::json;
use sql_builder::prelude::*;
use sql_builder::SqlBuilder;
@@ -41,8 +41,7 @@ use windmill_common::{
},
users::username_to_permissioned_as,
utils::{
list_elems_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin,
Pagination, StripPath,
list_elems_from_hub, not_found_if_none, paginate, require_admin, Pagination, StripPath,
},
};
use windmill_queue::{self, schedule::push_scheduled_job, PushIsolationLevel, QueueTransaction};
@@ -81,7 +80,6 @@ pub fn global_service() -> Router {
.route("/hub/list", get(list_hub_scripts))
.route("/hub/get/*path", get(get_hub_script_by_path))
.route("/hub/get_full/*path", get(get_full_hub_script_by_path))
.route("/hub/query", get(query_hub_scripts))
}
pub fn global_unauthed_service() -> Router {
@@ -211,28 +209,6 @@ async fn list_hub_scripts(ApiAuthed { email, .. }: ApiAuthed) -> JsonResult<serd
Ok(Json(asks))
}
#[derive(Deserialize)]
struct HubScriptsQuery {
text: String,
kind: Option<String>,
limit: Option<i64>,
}
async fn query_hub_scripts(
ApiAuthed { email, .. }: ApiAuthed,
Query(query): Query<HubScriptsQuery>,
) -> JsonResult<serde_json::Value> {
let asks = query_elems_from_hub(
&HTTP_CLIENT,
"https://hub.windmill.dev/scripts/query",
&email,
&query.text,
&query.kind,
&query.limit,
)
.await?;
Ok(Json(asks))
}
fn hash_script(ns: &NewScript) -> i64 {
let mut dh = DefaultHasher::new();
ns.hash(&mut dh);

View File

@@ -41,13 +41,13 @@ use time::OffsetDateTime;
use tower_cookies::{Cookie, Cookies};
use tracing::{Instrument, Span};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{
db::UserDB,
error::{self, to_anyhow, Error, JsonResult, Result},
users::SUPERADMIN_SECRET_EMAIL,
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
};
use windmill_queue::CLOUD_HOSTED;
const TTL_TOKEN_CACHE_S: u64 = 60; // 60s
pub const TTL_TOKEN_DB_H: u32 = 72;
@@ -712,7 +712,7 @@ async fn list_users(
SELECT usr.*, usage.*
FROM usr
, LATERAL (
SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0)::BIGINT executions
SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0) executions
FROM completed_job
WHERE workspace_id = $1
AND job_kind NOT IN ('flow', 'flowpreview')
@@ -2240,20 +2240,6 @@ pub struct LoginUserInfo {
pub displayName: Option<String>,
}
async fn _check_nb_of_user(db: &DB) -> Result<()> {
let nb_groups =
sqlx::query_scalar!("SELECT COUNT(*) FROM password WHERE login_type != 'password'",)
.fetch_one(db)
.await?;
if nb_groups.unwrap_or(0) >= 50 {
return Err(Error::BadRequest(
"You have reached the maximum number of oauth users accounts (50) without an enterprise license"
.to_string(),
));
}
return Ok(());
}
pub async fn login_externally(
db: DB,
email: &String,
@@ -2306,10 +2292,6 @@ pub async fn login_externally(
if (name.is_none() || name == Some(String::new())) && user.is_some() {
name = user.clone().unwrap().displayName;
}
#[cfg(not(feature = "enterprise"))]
_check_nb_of_user(&db).await?;
sqlx::query(&format!(
"INSERT INTO password (email, name, company, login_type, verified) VALUES ($1, \
$2, $3, '{}', true)",
@@ -2359,58 +2341,71 @@ pub async fn login_externally(
Ok(())
}
pub async fn delete_expired_items(db: &DB) -> () {
let tokens_deleted_r: std::result::Result<Vec<String>, _> = sqlx::query_scalar(
"DELETE FROM token WHERE expiration <= now()
pub async fn delete_expired_items_perdiodically(
db: &DB,
mut rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
loop {
let tokens_deleted_r: std::result::Result<Vec<String>, _> = sqlx::query_scalar(
"DELETE FROM token WHERE expiration <= now()
RETURNING concat(substring(token for 10), '*****')",
)
.fetch_all(db)
.await;
)
.fetch_all(db)
.await;
match tokens_deleted_r {
Ok(tokens) => tracing::debug!("deleted {} tokens: {:?}", tokens.len(), tokens),
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
}
match tokens_deleted_r {
Ok(tokens) => tracing::debug!("deleted {} tokens: {:?}", tokens.len(), tokens),
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
}
let pip_resolution_r = sqlx::query_scalar!(
"DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash",
)
.fetch_all(db)
.await;
let pip_resolution_r = sqlx::query_scalar!(
"DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash",
)
.fetch_all(db)
.await;
match pip_resolution_r {
Ok(res) => tracing::debug!("deleted {} pip_resolution: {:?}", res.len(), res),
Err(e) => tracing::error!("Error deleting pip_resolution: {}", e.to_string()),
}
match pip_resolution_r {
Ok(res) => tracing::debug!("deleted {} pip_resolution: {:?}", res.len(), res),
Err(e) => tracing::error!("Error deleting pip_resolution: {}", e.to_string()),
}
let deleted_cache = sqlx::query_scalar!(
let deleted_cache = sqlx::query_scalar!(
"DELETE FROM resource WHERE resource_type = 'cache' AND to_timestamp((value->>'expire')::int) < now() RETURNING path",
)
.fetch_all(db)
.await;
match deleted_cache {
Ok(res) => tracing::debug!("deleted {} cache resource: {:?}", res.len(), res),
Err(e) => tracing::error!("Error deleting cache resource {}", e.to_string()),
}
match deleted_cache {
Ok(res) => tracing::debug!("deleted {} cache resource: {:?}", res.len(), res),
Err(e) => tracing::error!("Error deleting cache resource {}", e.to_string()),
}
if *JOB_RETENTION_SECS > 0 {
let deleted_jobs = sqlx::query_scalar!(
if *JOB_RETENTION_SECS > 0 {
let deleted_jobs = sqlx::query_scalar!(
"DELETE FROM completed_job WHERE started_at + ((duration_ms/1000 + $1) || ' s')::interval <= now() RETURNING id",
*JOB_RETENTION_SECS as i64
)
.fetch_all(db)
.await;
match deleted_jobs {
Ok(deleted_jobs) => {
tracing::info!(
"deleted {} jobs completed JOB_RETENTION_SECS ago: {:?}",
deleted_jobs.len(),
deleted_jobs
)
match deleted_jobs {
Ok(deleted_jobs) => {
tracing::info!(
"deleted {} jobs completed JOB_RETENTION_SECS ago: {:?}",
deleted_jobs.len(),
deleted_jobs
)
}
Err(e) => tracing::error!("Error deleting jobs: {}", e.to_string()),
}
}
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(600)) => (),
_ = rx. recv() => {
println!("received killpill for delete expired tokens");
break;
}
Err(e) => tracing::error!("Error deleting jobs: {}", e.to_string()),
}
}
}

View File

@@ -7,44 +7,74 @@
*/
use axum::{
extract::{Extension, Path, Query},
extract::{Extension, Query},
routing::get,
Json, Router,
};
use itertools::Itertools;
use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use windmill_common::{
db::UserDB,
error::{self, JsonResult},
error::JsonResult,
utils::{paginate, Pagination},
worker::ALL_TAGS,
DB,
};
use std::collections::HashMap;
#[cfg(feature = "benchmark")]
use std::sync::atomic::Ordering;
#[cfg(feature = "benchmark")]
use windmill_queue::IDLE_WORKERS;
use crate::{db::ApiAuthed, utils::require_super_admin};
use crate::db::ApiAuthed;
#[cfg(not(feature = "benchmark"))]
pub fn global_service() -> Router {
use axum::routing::post;
let router = Router::new()
Router::new()
.route("/list", get(list_worker_pings))
.route("/custom_tags", get(get_custom_tags))
.route("/list_worker_groups", get(get_worker_groups))
.route(
"/worker_group/:name",
post(update_worker_group).delete(delete_worker_group),
);
#[cfg(feature = "benchmark")]
return router.route("/toggle", get(toggle));
}
#[cfg(not(feature = "benchmark"))]
return router;
#[cfg(feature = "benchmark")]
pub fn global_service() -> Router {
Router::new()
.route("/toggle", get(toggle))
.route("/list", get(list_worker_pings))
.route("/custom_tags", get(get_custom_tags))
}
lazy_static::lazy_static! {
pub static ref CUSTOM_TAGS: Vec<String> = std::env::var("CUSTOM_TAGS")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect::<Vec<_>>()).unwrap_or_default();
pub static ref CUSTOM_TAGS_PER_WORKSPACE: (Vec<String>, HashMap<String, Vec<String>>) = process_custom_tags(std::env::var("CUSTOM_TAGS")
.ok());
pub static ref ALL_TAGS: Vec<String> = [CUSTOM_TAGS_PER_WORKSPACE.0.clone(), CUSTOM_TAGS_PER_WORKSPACE.1.keys().map(|x| x.to_string()).collect_vec()].concat();
}
fn process_custom_tags(o: Option<String>) -> (Vec<String>, HashMap<String, Vec<String>>) {
let regex = Regex::new(r"^(\w+)\(((?:\w+)\+?)+\)$").unwrap();
if let Some(s) = o {
let mut global = vec![];
let mut specific: HashMap<String, Vec<String>> = HashMap::new();
for e in s.split(",") {
if let Some(cap) = regex.captures(e) {
let tag = cap.get(1).unwrap().as_str().to_string();
let workspaces = cap.get(2).unwrap().as_str().split("+");
specific.insert(tag, workspaces.map(|x| x.to_string()).collect_vec());
} else {
global.push(e.to_string());
}
}
(global, specific)
} else {
(vec![], HashMap::new())
}
}
#[derive(FromRow, Serialize, Deserialize)]
@@ -56,7 +86,6 @@ struct WorkerPing {
ip: String,
jobs_executed: i32,
custom_tags: Option<Vec<String>>,
worker_group: String,
}
#[derive(Serialize, Deserialize)]
@@ -75,7 +104,7 @@ async fn list_worker_pings(
let rows = sqlx::query_as!(
WorkerPing,
"SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed, custom_tags, worker_group FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
"SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed, custom_tags FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
per_page as i64,
offset as i64
)
@@ -92,75 +121,5 @@ async fn toggle(Query(query): Query<EnableWorkerQuery>) -> JsonResult<bool> {
}
async fn get_custom_tags() -> Json<Vec<String>> {
Json(ALL_TAGS.read().await.clone().into())
}
#[derive(Serialize, Deserialize, FromRow)]
struct WorkerGroup {
name: String,
config: serde_json::Value,
}
async fn get_worker_groups(
authed: ApiAuthed,
Extension(db): Extension<DB>,
) -> error::JsonResult<Vec<WorkerGroup>> {
require_super_admin(&db, &authed.email).await?;
let rows = sqlx::query_as!(WorkerGroup, "SELECT * FROM worker_group_config")
.fetch_all(&db)
.await?;
Ok(Json(rows))
}
#[cfg(feature = "enterprise")]
async fn update_worker_group(
Path(name): Path<String>,
Extension(db): Extension<DB>,
authed: ApiAuthed,
Json(config): Json<serde_json::Value>,
) -> error::Result<String> {
require_super_admin(&db, &authed.email).await?;
sqlx::query!(
"INSERT INTO worker_group_config (name, config) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET config = $2",
&name,
config
)
.execute(&db)
.await?;
Ok(format!("Updated worker group {name}"))
}
#[cfg(not(feature = "enterprise"))]
async fn update_worker_group() -> String {
"Worker groups available only in enterprise version".to_string()
}
async fn delete_worker_group(
Path(name): Path<String>,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
authed: ApiAuthed,
) -> error::Result<String> {
let tx = user_db.begin(&authed).await?;
require_super_admin(&db, &authed.email).await?;
tx.commit().await?;
let deleted = sqlx::query!(
"DELETE FROM worker_group_config WHERE name = $1 RETURNING name",
name,
)
.fetch_all(&db)
.await?;
if deleted.len() == 0 {
return Err(error::Error::NotFound(format!(
"Worker group {name} not found",
name = name
)));
}
Ok(format!("Deleted worker group {name}"))
Json(ALL_TAGS.clone())
}

View File

@@ -74,19 +74,13 @@ pub fn workspaced_service() -> Router {
.route("/edit_error_handler", post(edit_error_handler));
#[cfg(feature = "enterprise")]
{
if std::env::var("STRIPE_KEY").is_err() {
return router;
} else {
tracing::info!("stripe enabled");
return router
.route("/checkout", get(stripe_checkout))
.route("/billing_portal", get(stripe_portal));
}
}
tracing::info!("stripe enabled");
#[cfg(feature = "enterprise")]
let router = router
.route("/checkout", get(stripe_checkout))
.route("/billing_portal", get(stripe_portal));
#[cfg(not(feature = "enterprise"))]
router
}
pub fn global_service() -> Router {
@@ -849,20 +843,6 @@ lazy_static::lazy_static! {
}
async fn _check_nb_of_workspaces(db: &DB) -> Result<()> {
let nb_workspaces = sqlx::query_scalar!("SELECT COUNT(*) FROM workspace WHERE id != 'admins' AND deleted = false",)
.fetch_one(db)
.await?;
if nb_workspaces.unwrap_or(0) >= 3 {
return Err(Error::BadRequest(
"You have reached the maximum number of workspaces (3 outside of default group 'admins') without an enterprise license. Archive/delete another workspace to create a new one"
.to_string(),
));
}
return Ok(());
}
async fn create_workspace(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -872,10 +852,6 @@ async fn create_workspace(
if *CREATE_WORKSPACE_REQUIRE_SUPERADMIN {
require_super_admin(&db, &authed.email).await?;
}
#[cfg(not(feature = "enterprise"))]
_check_nb_of_workspaces(&db).await?;
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
check_name_conflict(&mut tx, &nw.id).await?;

View File

@@ -44,5 +44,3 @@ reqwest = { workspace = true, optional = true }
tracing-subscriber = { workspace = true, optional = true }
lazy_static.workspace = true
tracing-flame = { version = "^0", optional = true }
itertools.workspace = true
regex.workspace = true

View File

@@ -1,5 +1,4 @@
pub const WORKER_S3_BUCKET_SYNC: &str = "worker_s3_bucket_sync";
pub const CUSTOM_TAGS_SETTING: &str = "custom_tags";
pub const ENV_SETTINGS: [&str; 54] = [
"DISABLE_NSJAIL",
@@ -42,6 +41,8 @@ pub const ENV_SETTINGS: [&str; 54] = [
"INSTANCE_EVENTS_WEBHOOK",
"CLOUD_HOSTED",
"GLOBAL_CACHE_INTERVAL",
"WORKER_TAGS",
"CUSTOM_TAGS",
"JOB_RETENTION_SECS",
"WAIT_RESULT_FAST_POLL_DURATION_SECS",
"WAIT_RESULT_SLOW_POLL_INTERVAL_MS",
@@ -54,6 +55,4 @@ pub const ENV_SETTINGS: [&str; 54] = [
"SMTP_TLS_IMPLICIT",
"CREATE_WORKSPACE_REQUIRE_SUPERADMIN",
"GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE",
"MAX_WAIT_FOR_SIGTERM",
"WORKER_GROUP",
];

View File

@@ -27,7 +27,6 @@ pub mod scripts;
pub mod users;
pub mod utils;
pub mod variables;
pub mod worker;
#[cfg(feature = "tracing_init")]
pub mod tracing_init;
@@ -78,15 +77,12 @@ pub async fn shutdown_signal(
Ok(())
}
#[cfg(feature = "prometheus")]
use tokio::task::JoinHandle;
#[cfg(feature = "prometheus")]
pub async fn serve_metrics(
addr: SocketAddr,
mut rx: tokio::sync::broadcast::Receiver<()>,
ready_worker_endpoint: bool,
) -> JoinHandle<()> {
) -> Result<(), hyper::Error> {
use std::sync::atomic::Ordering;
use axum::{routing::get, Router};
@@ -108,18 +104,13 @@ pub async fn serve_metrics(
router
};
tokio::spawn(async move {
if let Err(e) = axum::Server::bind(&addr)
.serve(router.into_make_service())
.with_graceful_shutdown(async {
rx.recv().await.ok();
println!("Graceful shutdown of metrics");
})
.await
{
tracing::error!("Error serving metrics: {}", e);
}
})
axum::Server::bind(&addr)
.serve(router.into_make_service())
.with_graceful_shutdown(async {
rx.recv().await.ok();
println!("Graceful shutdown of metrics");
})
.await
}
async fn metrics() -> Result<String, Error> {

View File

@@ -274,7 +274,6 @@ pub async fn get_hub_script_by_path(
&format!("https://hub.windmill.dev/raw/{path}.ts"),
email,
true,
None,
)
.await?
.text()
@@ -304,7 +303,6 @@ pub async fn get_full_hub_script_by_path(
&format!("https://hub.windmill.dev/raw2/{path}"),
email,
true,
None,
)
.await?
.json::<HubScript>()

View File

@@ -79,32 +79,7 @@ pub async fn list_elems_from_hub(
url: &str,
email: &str,
) -> Result<serde_json::Value> {
let rows = http_get_from_hub(http_client, url, email, false, None)
.await?
.json::<serde_json::Value>()
.await
.map_err(crate::error::to_anyhow)?;
Ok(rows)
}
#[cfg(feature = "reqwest")]
pub async fn query_elems_from_hub(
http_client: &reqwest::Client,
url: &str,
email: &str,
query_text: &str,
query_kind: &Option<String>,
query_limit: &Option<i64>,
) -> Result<serde_json::Value> {
let mut query_params = vec![("text", query_text)];
if let Some(query_kind) = query_kind {
query_params.push(("kind", query_kind.as_str()));
}
let query_limit = query_limit.unwrap_or(0).to_string();
if query_limit.parse::<i64>().unwrap() > 0 {
query_params.push(("limit", query_limit.as_str()));
}
let rows = http_get_from_hub(http_client, url, email, false, Some(query_params))
let rows = http_get_from_hub(http_client, url, email, false)
.await?
.json::<serde_json::Value>()
.await
@@ -118,9 +93,8 @@ pub async fn http_get_from_hub(
url: &str,
email: &str,
plain: bool,
query_params: Option<Vec<(&str, &str)>>,
) -> Result<reqwest::Response> {
let mut request = http_client
let response = http_client
.get(url)
.header(
"Accept",
@@ -130,15 +104,10 @@ pub async fn http_get_from_hub(
"application/json"
},
)
.header("X-email", email);
if let Some(query_params) = query_params {
for (key, value) in query_params {
request = request.query(&[(key, value)]);
}
}
let response = request.send().await.map_err(crate::error::to_anyhow)?;
.header("X-email", email)
.send()
.await
.map_err(crate::error::to_anyhow)?;
Ok(response)
}

View File

@@ -1,200 +0,0 @@
use std::{collections::HashMap, sync::Arc};
use itertools::Itertools;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use crate::{error, global_settings::CUSTOM_TAGS_SETTING, DB};
lazy_static::lazy_static! {
pub static ref WORKER_GROUP: String = std::env::var("WORKER_GROUP").unwrap_or_else(|_| "default".to_string());
pub static ref DEFAULT_TAGS : Vec<String> = vec![
"deno".to_string(),
"python3".to_string(),
"go".to_string(),
"bash".to_string(),
"powershell".to_string(),
"nativets".to_string(),
"mysql".to_string(),
"graphql".to_string(),
"bun".to_string(),
"postgresql".to_string(),
"bigquery".to_string(),
"snowflake".to_string(),
"graphql".to_string(),
"dependency".to_string(),
"flow".to_string(),
"hub".to_string(),
"other".to_string()];
pub static ref WORKER_CONFIG: Arc<RwLock<WorkerConfig>> = Arc::new(RwLock::new(WorkerConfig {
worker_tags: Default::default(),
dedicated_worker: Default::default(),
}));
pub static ref CLOUD_HOSTED: bool = std::env::var("CLOUD_HOSTED").is_ok();
pub static ref CUSTOM_TAGS: Vec<String> = std::env::var("CUSTOM_TAGS")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect::<Vec<_>>()).unwrap_or_default();
pub static ref CUSTOM_TAGS_PER_WORKSPACE: Arc<RwLock<(Vec<String>, HashMap<String, Vec<String>>)>> = Arc::new(RwLock::new((vec![], HashMap::new())));
pub static ref ALL_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(vec![]));
static ref CUSTOM_TAG_REGEX: Regex = Regex::new(r"^(\w+)\(((?:\w+)\+?)+\)$").unwrap();
}
pub async fn reload_custom_tags_setting(db: &DB) -> error::Result<()> {
let q = sqlx::query!(
"SELECT value FROM global_settings WHERE name = $1",
CUSTOM_TAGS_SETTING
)
.fetch_optional(db)
.await?;
let tags = if let Some(q) = q {
if let Ok(v) = serde_json::from_value::<Vec<String>>(q.value.clone()) {
v
} else {
tracing::error!(
"Could not parse custom tags setting as vec of strings, found: {:#?}",
&q.value
);
vec![]
}
} else {
CUSTOM_TAGS.clone()
};
let custom_tags = process_custom_tags(tags);
{
let l = CUSTOM_TAGS_PER_WORKSPACE.read().await;
if l.clone() == custom_tags {
tracing::info!("Custom tags setting unchanged, skipping update");
return Ok(());
} else {
tracing::info!("Custom tags setting changed, updating");
}
}
{
let mut l = CUSTOM_TAGS_PER_WORKSPACE.write().await;
*l = custom_tags.clone()
}
{
let mut l = ALL_TAGS.write().await;
*l = [
custom_tags.0.clone(),
custom_tags.1.keys().map(|x| x.to_string()).collect_vec(),
]
.concat();
}
Ok(())
// pub static ref CUSTOM_TAGS_PER_WORKSPACE: (Vec<String>, HashMap<String, Vec<String>>) = process_custom_tags(std::env::var("CUSTOM_TAGS")
// .ok());
// pub static ref ALL_TAGS: Vec<String> = [CUSTOM_TAGS_PER_WORKSPACE.0.clone(), CUSTOM_TAGS_PER_WORKSPACE.1.keys().map(|x| x.to_string()).collect_vec()].concat();
}
fn process_custom_tags(tags: Vec<String>) -> (Vec<String>, HashMap<String, Vec<String>>) {
let mut global = vec![];
let mut specific: HashMap<String, Vec<String>> = HashMap::new();
for e in tags {
if let Some(cap) = CUSTOM_TAG_REGEX.captures(&e) {
let tag = cap.get(1).unwrap().as_str().to_string();
let workspaces = cap.get(2).unwrap().as_str().split("+");
specific.insert(tag, workspaces.map(|x| x.to_string()).collect_vec());
} else {
global.push(e.to_string());
}
}
(global, specific)
}
pub async fn update_ping(worker_instance: &str, worker_name: &str, ip: &str, db: &DB) {
let wc = WORKER_CONFIG.read().await;
let tags = wc.worker_tags.as_slice();
sqlx::query!(
"INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags, worker_group, dedicated_worker) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (worker) DO UPDATE set ip = $3, custom_tags = $4, worker_group = $5",
worker_instance,
worker_name,
ip,
tags,
*WORKER_GROUP,
wc.dedicated_worker.as_ref().map(|x| format!("{}:{}", x.workspace_id, x.path))
)
.execute(db)
.await
.expect("insert worker_ping initial value");
}
pub async fn load_worker_config(db: &DB) -> error::Result<WorkerConfig> {
let config: WorkerConfigOpt = sqlx::query_scalar!(
"SELECT config FROM worker_group_config WHERE name = $1",
*WORKER_GROUP
)
.fetch_optional(db)
.await?
.flatten()
.map(|x| serde_json::from_value(x).ok())
.flatten()
.unwrap_or_default();
let dedicated_worker = config.dedicated_worker.map(|x| {
let splitted = x.split(':').to_owned().collect_vec();
if splitted.len() != 2 {
panic!("DEDICATED_WORKER setting should be in the form of <workspace>:<script_path>")
} else {
let workspace = splitted[0];
let script_path = splitted[1];
WorkspacedPath { workspace_id: workspace.to_string(), path: script_path.to_string() }
}
});
Ok(WorkerConfig {
worker_tags: config
.worker_tags
.or_else(|| {
if let Some(ref dedicated_worker) = dedicated_worker.as_ref() {
Some(vec![format!(
"{}:{}",
dedicated_worker.workspace_id, dedicated_worker.path
)])
} else {
std::env::var("WORKER_TAGS")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect())
}
})
.unwrap_or_else(|| DEFAULT_TAGS.clone()),
dedicated_worker,
})
}
#[derive(Clone, PartialEq, Debug)]
pub struct WorkspacedPath {
pub workspace_id: String,
pub path: String,
}
#[derive(Serialize, Deserialize)]
pub struct WorkerConfigOpt {
pub worker_tags: Option<Vec<String>>,
pub dedicated_worker: Option<String>,
}
impl Default for WorkerConfigOpt {
fn default() -> Self {
Self { worker_tags: Default::default(), dedicated_worker: Default::default() }
}
}
#[derive(PartialEq, Debug)]
pub struct WorkerConfig {
pub worker_tags: Vec<String>,
pub dedicated_worker: Option<WorkspacedPath>,
}

View File

@@ -37,4 +37,3 @@ tokio.workspace = true
futures-core.workspace = true
itertools.workspace = true
async-recursion.workspace = true
bigdecimal.workspace = true

View File

@@ -8,16 +8,17 @@
use std::{collections::HashMap, sync::atomic::AtomicBool, vec};
#[cfg(feature = "benchmark")]
use std::time::Instant;
use anyhow::Context;
use async_recursion::async_recursion;
use bigdecimal::ToPrimitive;
use chrono::{DateTime, Duration, Utc};
use itertools::Itertools;
use reqwest::Client;
use rsmq_async::RsmqConnection;
use serde_json::json;
use sqlx::{Pool, Postgres, Transaction};
#[cfg(feature = "benchmark")]
use std::time::Instant;
use tracing::{instrument, Instrument};
use ulid::Ulid;
use uuid::Uuid;
@@ -36,13 +37,9 @@ use windmill_common::{
schedule::{schedule_to_user, Schedule},
scripts::{ScriptHash, ScriptLang},
users::{username_to_permissioned_as, SUPERADMIN_SECRET_EMAIL},
worker::WORKER_CONFIG,
DB, METRICS_ENABLED,
};
#[cfg(feature = "enterprise")]
use windmill_common::worker::CLOUD_HOSTED;
use crate::{
schedule::{get_schedule_opt, push_scheduled_job},
QueueTransaction,
@@ -69,6 +66,57 @@ lazy_static::lazy_static! {
"Total number of jobs pulled from the queue."
)
.unwrap();
pub static ref CLOUD_HOSTED: bool = std::env::var("CLOUD_HOSTED").is_ok();
pub static ref DEFAULT_TAGS : Vec<String> = vec![
"deno".to_string(),
"python3".to_string(),
"go".to_string(),
"bash".to_string(),
"powershell".to_string(),
"nativets".to_string(),
"mysql".to_string(),
"graphql".to_string(),
"bun".to_string(),
"postgresql".to_string(),
"bigquery".to_string(),
"snowflake".to_string(),
"graphql".to_string(),
"dependency".to_string(),
"flow".to_string(),
"hub".to_string(),
"other".to_string()];
pub static ref DEDICATED_WORKER: Option<(String, String)> = std::env::var("DEDICATED_WORKER")
.ok()
.map(|x| {
let splitted = x.split(':').to_owned().collect_vec();
if splitted.len() != 2 {
panic!("DEDICATED_WORKER should be in the form of <workspace>:<script_path>")
} else {
let workspace = splitted[0];
let script_path = splitted[1];
(workspace.to_string(), script_path.to_string())
}
});
pub static ref ACCEPTED_TAGS: Vec<String> = {
let worker_tags = std::env::var("WORKER_TAGS")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect())
.unwrap_or_else(|| DEFAULT_TAGS.clone());
if let Some(ref dedicated_worker) = DEDICATED_WORKER.as_ref() {
vec![format!("{}:{}", dedicated_worker.0, dedicated_worker.1)]
} else {
worker_tags
}
};
pub static ref IS_WORKER_TAGS_DEFINED: bool = std::env::var("WORKER_TAGS").ok().is_some();
// When compiled in 'benchmark' mode, this flags is exposed via the /workers/toggle endpoint
// and make it possible to disable to current active workers (such that they don't pull any)
@@ -236,8 +284,6 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
.await
.ok()
.flatten()
.map(|x| x.to_i64())
.flatten()
} else {
tracing::warn!("Could not parse flow status");
None
@@ -254,7 +300,7 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
.flatten();
let mut tx: QueueTransaction<'_, R> = (rsmq.clone(), db.begin().await?).into();
let job_id = queued_job.id.clone();
let _duration: i64 = sqlx::query_scalar!(
let _duration = sqlx::query_scalar!(
"INSERT INTO completed_job AS cj
( workspace_id
, id
@@ -379,7 +425,7 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
if !is_flow && _duration > 1000 {
let additional_usage = _duration / 1000;
let w_id = &queued_job.workspace_id;
let premium_workspace = *windmill_common::worker::CLOUD_HOSTED
let premium_workspace = *CLOUD_HOSTED
&& sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", w_id)
.fetch_one(db)
.await
@@ -390,7 +436,7 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $3",
if premium_workspace { w_id } else { &queued_job.email },
premium_workspace,
additional_usage as i32)
additional_usage)
.execute(db)
.await
.map_err(|e| Error::InternalErr(format!("updating usage: {e}")));
@@ -1026,8 +1072,8 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Send + Clone>(
let job_uuid: Uuid = pulled_job.id;
let min_started_at: Option<DateTime<Utc>> = script_path_live_stats.min_started_at;
let avg_script_duration: Option<i64> = sqlx::query_scalar!(
"SELECT CAST(ROUND(AVG(duration_ms) / 1000, 0) AS BIGINT) AS avg_duration_s FROM
let avg_script_duration: Option<i32> = sqlx::query_scalar!(
"SELECT CAST(ROUND(AVG(duration_ms) / 1000, 0) AS INT) AS avg_duration_s FROM
(SELECT duration_ms FROM completed_job WHERE script_path = $1
ORDER BY started_at
DESC LIMIT 10) AS t",
@@ -1107,7 +1153,7 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
// TODO: REDIS: Race conditions / replace last_ping
// TODO: shuffle this list to have fairness
let mut all_tags = WORKER_CONFIG.read().await.worker_tags.clone();
let mut all_tags = ACCEPTED_TAGS.clone();
let mut msg: Option<_> = None;
let mut tag = None;
@@ -1164,8 +1210,6 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
* suspend_until is non-null
* and suspend = 0 when the resume messages are received
* or suspend_until <= now() if it has timed out */
let config = WORKER_CONFIG.read().await;
let tags = config.worker_tags.as_slice();
let r = if suspend_first {
sqlx::query_as::<_, QueuedJob>("UPDATE queue
@@ -1182,21 +1226,17 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
LIMIT 1
)
RETURNING *")
.bind(tags)
.bind(ACCEPTED_TAGS.as_slice())
.fetch_optional(db)
.await?
} else {
None
};
drop(config);
if r.is_none() {
// #[cfg(feature = "benchmark")]
// let instant = Instant::now();
let config = WORKER_CONFIG.read().await;
let tags = config.worker_tags.as_slice();
let r = sqlx::query_as::<_, QueuedJob>(
"UPDATE queue
SET running = true
@@ -1213,9 +1253,10 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
)
RETURNING *",
)
.bind(tags)
.bind(ACCEPTED_TAGS.as_slice())
.fetch_optional(db)
.await?;
// #[cfg(feature = "benchmark")]
// println!("pull query: {:?}", instant.elapsed());
@@ -1312,35 +1353,15 @@ pub async fn delete_job<'c, R: rsmq_async::RsmqConnection + Clone + Send>(
w_id,
job_id
)
.fetch_optional(&mut tx)
.await;
if let Err(job_removed) = job_removed {
tracing::error!(
"Job {job_id} could not be deleted: {job_removed}. This is not necessarily an error, as the job might have been deleted by another process such as in the case of cancelling"
);
} else {
let job_removed = job_removed.unwrap().flatten().unwrap_or(0);
if job_removed != 1 {
tracing::error!("Job {job_id} could not be deleted, returned not 1: {job_removed}. This is not necessarily an error, as the job might have been deleted by another process such as in the case of cancelling");
}
}
tracing::debug!("Job {job_id} deleted");
.fetch_one(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("Error during deletion of job {job_id}: {e}")))?
.unwrap_or(0)
== 1;
tracing::debug!("Job {job_id} deleted: {job_removed}");
Ok(tx)
}
pub async fn job_is_complete(db: &DB, id: Uuid, w_id: &str) -> error::Result<bool> {
Ok(sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM completed_job WHERE id = $1 AND workspace_id = $2)",
id,
w_id
)
.fetch_one(db)
.await?
.unwrap_or(false))
}
pub async fn get_queued_job<'c>(
id: Uuid,
w_id: &str,

View File

@@ -71,7 +71,6 @@ jsonwebtoken = { workspace = true, optional = true }
sha2 = { workspace = true, optional = true }
pem = { workspace = true, optional = true }
urlencoding.workspace = true
nix.workspace = true
[build-dependencies]
deno_fetch.workspace = true

View File

@@ -7,8 +7,6 @@ const bo = await Bun.build({
const fs = require("fs/promises");
const captureVersion = /(^\@?[^\@]+)(?:\@(.+))?/;
if (!bo.success) {
bo.logs.forEach((l) => console.log(l));
process.exit(1);
@@ -19,7 +17,7 @@ if (!bo.success) {
const { intersect } = require("semver-intersect");
const dependencies: Record<string, string[]> = {};
for (const i of imports) {
let [_, name, version] = i.path.match(captureVersion) ?? [];
let [name, version] = i.path.split("@");
if (version == undefined) {
if (dependencies[name] == undefined) {
dependencies[name] = [];

View File

@@ -123,7 +123,6 @@ pub async fn handle_bash_job(
&job.workspace_id,
"bash run",
job.timeout,
true,
)
.await?;
@@ -261,7 +260,6 @@ pub async fn handle_powershell_job(
&job.workspace_id,
"bash/powershell run",
job.timeout,
false,
)
.await?;

View File

@@ -105,7 +105,6 @@ pub async fn gen_lockfile(
w_id,
"bun build",
None,
false,
)
.await?;
@@ -172,7 +171,6 @@ pub async fn install_lockfile(
w_id,
"bun install",
None,
false,
)
.await?;
Ok(())
@@ -412,7 +410,6 @@ plugin(p)
&job.workspace_id,
"bun run",
job.timeout,
false,
)
.await?;
read_result(job_dir).await

View File

@@ -1,16 +1,14 @@
use async_recursion::async_recursion;
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;
use serde_json::{json, Value};
use sqlx::{Pool, Postgres};
use tokio::{fs::File, io::AsyncReadExt};
use windmill_api_client::{types::CreateResource, Client};
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{
error::{self, Error},
jobs::QueuedJob,
variables::ContextualVariable,
};
use windmill_queue::CLOUD_HOSTED;
use anyhow::Result;
use std::{
@@ -39,9 +37,7 @@ use futures::{
stream, StreamExt,
};
use crate::{
AuthedClient, MAX_RESULT_SIZE, MAX_WAIT_FOR_SIGTERM, TIMEOUT_DURATION, WHITELIST_ENVS,
};
use crate::{AuthedClient, MAX_RESULT_SIZE, TIMEOUT_DURATION, WHITELIST_ENVS};
#[tracing::instrument(level = "trace", skip_all)]
pub async fn create_args_and_out_file(
@@ -254,7 +250,6 @@ pub async fn handle_child(
_w_id: &str,
child_name: &str,
custom_timeout: Option<i32>,
sigterm: bool,
) -> error::Result<()> {
let start = Instant::now();
let update_job_interval = Duration::from_millis(500);
@@ -393,21 +388,6 @@ pub async fn handle_child(
}
};
if sigterm {
if let Some(id) = child.id() {
signal::kill(Pid::from_raw(id as i32), Signal::SIGTERM).unwrap();
for _ in 0..*MAX_WAIT_FOR_SIGTERM {
if child.try_wait().is_ok_and(|x| x.is_some()) {
break;
}
sleep(Duration::from_secs(1)).await;
}
if child.try_wait().is_ok_and(|x| x.is_some()) {
set_reason.await;
return Ok(Err(kill_reason));
}
}
}
/* send SIGKILL and reap child process */
let (_, kill) = future::join(set_reason, child.kill()).await;
kill.map(|()| Err(kill_reason))

View File

@@ -112,7 +112,6 @@ pub async fn generate_deno_lock(
w_id,
"deno cache",
None,
false,
)
.await?;
@@ -316,7 +315,6 @@ run().catch(async (e) => {{
&job.workspace_id,
"deno run",
job.timeout,
false,
)
.await?;
// logs.push_str(format!("execute: {:?}\n", start.elapsed().as_millis()).as_str());

View File

@@ -198,7 +198,6 @@ func Run(req Req) (interface{{}}, error){{
&job.workspace_id,
"go build",
None,
false,
)
.await?;
@@ -275,7 +274,6 @@ func Run(req Req) (interface{{}}, error){{
&job.workspace_id,
"go run",
job.timeout,
false,
)
.await?;
read_result(job_dir).await
@@ -333,7 +331,6 @@ pub async fn install_go_dependencies(
w_id,
"go init",
None,
false,
)
.await?;
@@ -394,7 +391,6 @@ pub async fn install_go_dependencies(
&w_id,
&format!("go {mod_command}"),
None,
false,
)
.await
.map_err(|e| Error::ExecutionErr(format!("Lockfile generation failed: {e:?}")))?;

View File

@@ -6,7 +6,6 @@ mod snowflake_executor;
mod bash_executor;
mod bun_executor;
mod common;
mod config;
mod dedicated_worker;
mod deno_executor;
mod global_cache;
@@ -18,4 +17,5 @@ mod pg_executor;
mod python_executor;
mod worker;
mod worker_flow;
pub use worker::*;

View File

@@ -218,15 +218,15 @@ pub fn pg_cell_to_json_value(
Type::DATE => get_basic(row, column, column_i, |a: chrono::NaiveDate| {
Ok(JSONValue::String(a.to_string()))
})?,
Type::TIME => get_basic(row, column, column_i, |a: chrono::NaiveTime| {
Ok(JSONValue::String(a.to_string()))
})?,
Type::TIMESTAMPTZ => get_basic(row, column, column_i, |a: chrono::DateTime<Utc>| {
Ok(JSONValue::String(a.to_string()))
})?,
Type::UUID => get_basic(row, column, column_i, |a: uuid::Uuid| {
Ok(JSONValue::String(a.to_string()))
})?,
// Type::DATE => get_basic(row, column, column_i, |a: chrono::NaiveDate| {
// Ok(JSONValue::String(a.to_string()))
// })?,
Type::JSON | Type::JSONB => get_basic(row, column, column_i, |a: JSONValue| Ok(a))?,
Type::FLOAT4 => get_basic(row, column, column_i, |a: f32| {
Ok(f64_to_json_number(a.into())?)

View File

@@ -143,7 +143,6 @@ pub async fn pip_compile(
&w_id,
"pip-compile",
None,
false,
)
.await
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
@@ -479,7 +478,6 @@ mount {{
&job.workspace_id,
"python run",
job.timeout,
false,
)
.await?;
read_result(job_dir).await
@@ -639,7 +637,6 @@ pub async fn handle_python_reqs(
&w_id,
&format!("pip install {req}"),
None,
false,
)
.await;
tracing::info!(

View File

@@ -28,10 +28,12 @@ use windmill_common::{
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
users::SUPERADMIN_SECRET_EMAIL,
utils::{rd_string, StripPath},
worker::{update_ping, CLOUD_HOSTED, WORKER_CONFIG},
DB, IS_READY, METRICS_ENABLED,
};
use windmill_queue::{canceled_job_to_result, get_queued_job, pull, HTTP_CLIENT};
use windmill_queue::{
canceled_job_to_result, get_queued_job, pull, ACCEPTED_TAGS, CLOUD_HOSTED, DEDICATED_WORKER,
HTTP_CLIENT, IS_WORKER_TAGS_DEFINED,
};
use serde_json::{json, Value};
@@ -251,11 +253,6 @@ lazy_static::lazy_static! {
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or_else(|| if *CLOUD_HOSTED { DEFAULT_CLOUD_TIMEOUT } else { DEFAULT_SELFHOSTED_TIMEOUT });
pub static ref MAX_WAIT_FOR_SIGTERM: u64 = std::env::var("MAX_WAIT_FOR_SIGTERM")
.ok()
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or_else(|| 5);
pub static ref TIMEOUT_DURATION: Duration = Duration::from_secs(*TIMEOUT);
pub static ref SCRIPT_TOKEN_EXPIRY: i32 = std::env::var("SCRIPT_TOKEN_EXPIRY")
@@ -348,7 +345,6 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
_num_workers: u32,
ip: &str,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
killpill_tx: tokio::sync::broadcast::Sender<()>,
base_internal_url: &str,
rsmq: Option<R>,
_sync_barrier: Arc<RwLock<Option<Barrier>>>,
@@ -387,7 +383,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let mut last_ping = Instant::now() - Duration::from_secs(NUM_SECS_PING + 1);
update_ping(worker_instance, &worker_name, ip, db).await;
insert_initial_ping(worker_instance, &worker_name, ip, db).await;
let uptime_metric =
prometheus::register_counter!(WORKER_UPTIME_OPTS.clone().const_label("name", &worker_name))
@@ -608,23 +604,19 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let vacuum_shift = rand::thread_rng().gen_range(0..VACUUM_PERIOD);
IS_READY.store(true, Ordering::Relaxed);
tracing::info!(worker = %worker_name, "listening for jobs, config: {:#?}", WORKER_CONFIG.read().await);
tracing::info!(worker = %worker_name, "listening for jobs");
let (dedicated_worker_tx, dedicated_worker_handle) = if let Some(_wp) =
WORKER_CONFIG.read().await.dedicated_worker.clone()
let (dedicated_worker_tx, dedicated_worker_handle) = if let Some((_workspace, _script_path)) =
DEDICATED_WORKER.clone()
{
#[cfg(not(feature = "enterprise"))]
{
tracing::error!("Dedicated worker is an enterprise feature");
killpill_tx.send(()).expect("send");
return;
}
panic!("Dedicated worker is an enterprise feature");
#[cfg(feature = "enterprise")]
{
let (dedicated_worker_tx, dedicated_worker_rx) =
mpsc::channel::<QueuedJob>(MAX_BUFFERED_DEDICATED_JOBS);
let mut killpill_rx = killpill_rx.resubscribe();
let killpill_rx = killpill_rx.resubscribe();
let db = db.clone();
let worker_dir = worker_dir.clone();
let base_internal_url = base_internal_url.to_string();
@@ -648,52 +640,19 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
.execute(&db)
.await
{
tracing::error!("failed to create token for dedicated worker: {:?}", e);
killpill_tx.send(()).expect("send");
panic!("failed to create token for dedicated worker: {:?}", e)
};
let (content, lock, _language, envs) = {
let r;
loop {
let q = sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>, Option<Vec<String>>)>(
let (content, lock, _language, envs) = sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>, Option<Vec<String>>)>(
"SELECT content, lock, language, envs FROM script WHERE path = $1 AND workspace_id = $2 AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND
deleted = false AND lock IS not NULL AND lock_error_logs IS NULL)",
)
.bind(&_wp.path)
.bind(&_wp.workspace_id)
.bind(&_script_path)
.bind(&_workspace)
.fetch_optional(&db)
.await;
if let Ok(q) = q {
if let Some(wp) = q {
r = wp;
break;
} else {
tracing::error!(
"Failed to fetch script `{}` in workspace {} for dedicated worker. Retrying in 10s.",
_wp.path,
_wp.workspace_id
);
tokio::select! {
biased;
_ = killpill_rx.recv() => {
tracing::info!("Killing dedicated worker while it was attempting to fetch script");
return;
}
_ = tokio::time::sleep(Duration::from_secs(10)) => {
continue;
}
}
}
} else {
tracing::error!("Failed to fetch script for dedicated worker");
killpill_tx.send(()).expect("send");
return;
}
}
r
};
.await.expect("Failed to fetch script for dedicated worker")
.expect(&format!("Failed to fetch script `{_script_path}` in workspace {_workspace} for dedicated worker"));
let worker_envs = build_envs(envs).expect("failed to build envs");
if let Err(e) = start_worker(
@@ -704,8 +663,8 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
&job_dir,
&worker_name,
worker_envs,
&_wp.workspace_id,
&_wp.path,
&_workspace,
&_script_path,
&token,
job_completed_tx,
dedicated_worker_rx,
@@ -742,13 +701,9 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let copy_tx = _copy_to_bucket_tx.clone();
if last_ping.elapsed().as_secs() > NUM_SECS_PING {
let wc = WORKER_CONFIG.read().await;
let tags = wc.worker_tags.as_slice();
sqlx::query!(
"UPDATE worker_ping SET ping_at = now(), jobs_executed = $1, custom_tags = $2 WHERE worker = $3",
"UPDATE worker_ping SET ping_at = now(), jobs_executed = $1 WHERE worker = $2",
jobs_executed,
tags,
&worker_name
)
.execute(db)
@@ -1309,6 +1264,25 @@ pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clon
tracing::error!(job_id = %job.id, "error handling job: {err:?} {} {} {}", job.id, job.workspace_id, job.created_by);
}
async fn insert_initial_ping(
worker_instance: &str,
worker_name: &str,
ip: &str,
db: &Pool<Postgres>,
) {
let tags = ACCEPTED_TAGS.clone();
sqlx::query!(
"INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags) VALUES ($1, $2, $3, $4) ON CONFLICT (worker) DO NOTHING",
worker_instance,
worker_name,
ip,
if *IS_WORKER_TAGS_DEFINED { Some(tags.as_slice()) } else { None }
)
.execute(db)
.await
.expect("insert worker_ping initial value");
}
fn extract_error_value(log_lines: &str, i: i32) -> serde_json::Value {
return json!({"message": format!("ExitCode: {i}, last log lines:\n{}", ANSI_ESCAPE_RE.replace_all(log_lines.trim(), "").to_string()), "name": "ExecutionErr"});
}

View File

@@ -22,7 +22,7 @@ async function login(email: string, password: string): Promise<string> {
});
}
export const VERSION = "v1.173.0";
export const VERSION = "v1.168.3";
export async function main({
host,
@@ -242,28 +242,20 @@ export async function main({
const jobsSent = Array(num_workers).fill(0);
const enc = (s: string) => new TextEncoder().encode(s);
async function getQueueCount() {
return (
await (
await fetch(
config.server + "/api/w/" + config.workspace_id + "/jobs/queue/count",
{ headers: { ["Authorization"]: "Bearer " + config.token } }
)
).json()
).database_length;
}
const initial_queue_length = await getQueueCount();
console.log("Initial queue length:", initial_queue_length);
const updateState = setInterval(async () => {
const elapsed = start ? Math.ceil((Date.now() - start) / 1000) : 0;
const sum = jobsSent.reduce((a, b) => a + b, 0);
let queue_length = -1;
while (queue_length === -1) {
try {
queue_length = await getQueueCount();
queue_length = (
await (
await fetch(
host + "/api/w/" + config.workspace_id + "/jobs/queue/count",
{ headers: { ["Authorization"]: "Bearer " + config.token } }
)
).json()
).database_length;
} catch (e) {
console.log(
`queue count not reachable. waiting... `
@@ -276,9 +268,7 @@ export async function main({
enc(
`elapsed: ${elapsed}/${seconds} | jobs sent: ${JSON.stringify(
jobsSent
)} (sum: ${sum} thr: ${(sum / elapsed).toFixed(2)}) - processed (sum: ${
sum - queue_length
} thr: ${((sum - queue_length) / elapsed).toFixed(
)} (sum: ${sum} thr: ${(sum / elapsed).toFixed(
2
)}) | queue: ${queue_length} \r`
)

View File

@@ -33,10 +33,7 @@ export async function pushApp(
if (raw) {
// deleting old app if it exists in raw mode
try {
app = await AppService.getAppByPath({
workspace,
path: remotePath.replaceAll("\\", "/"),
});
app = await AppService.getAppByPath({ workspace, path: remotePath });
} catch {
//ignore
}
@@ -48,7 +45,7 @@ export async function pushApp(
}
await AppService.updateApp({
workspace,
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
requestBody: {
...newApp,
},
@@ -59,7 +56,7 @@ export async function pushApp(
await AppService.createApp({
workspace,
requestBody: {
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
...newApp,
},
});

View File

@@ -1,7 +1,7 @@
// windmill
export { setClient } from "https://deno.land/x/windmill@v1.95.1/mod.ts";
export * from "https://deno.land/x/windmill@v1.95.1/windmill-api/index.ts";
export { SEP } from "https://deno.land/std@0.201.0/path/separator.ts";
// cliffy
export { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.2/command/mod.ts";
export { Table } from "https://deno.land/x/cliffy@v1.0.0-rc.2/table/table.ts";

View File

@@ -1,6 +1,6 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions, isSuperset } from "./types.ts";
import { SEP, log } from "./deps.ts";
import { log } from "./deps.ts";
import {
colors,
Command,
@@ -42,8 +42,8 @@ export async function pushFlow(
// flow doesn't exist
}
if (!localFlowPath.endsWith(SEP)) {
localFlowPath += SEP;
if (!localFlowPath.endsWith("/")) {
localFlowPath += "/";
}
const localFlowRaw = await Deno.readTextFile(localFlowPath + "flow.yaml");
const localFlow = yamlParse(localFlowRaw) as FlowFile;
@@ -74,9 +74,9 @@ export async function pushFlow(
log.info(colors.bold.yellow(`Updating flow ${remotePath}...`));
await FlowService.updateFlow({
workspace: workspace,
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
requestBody: {
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
...localFlow,
},
});
@@ -85,7 +85,7 @@ export async function pushFlow(
await FlowService.createFlow({
workspace: workspace,
requestBody: {
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
...localFlow,
},
});

View File

@@ -1,13 +1,5 @@
// deno-lint-ignore-file no-explicit-any
import {
colors,
Command,
Folder,
FolderService,
log,
SEP,
Table,
} from "./deps.ts";
import { colors, Command, Folder, FolderService, log, Table } from "./deps.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { GlobalOptions, isSuperset, parseFromFile } from "./types.ts";
@@ -46,13 +38,13 @@ export async function pushFolder(
localFolder: FolderFile,
raw: boolean
): Promise<void> {
if (name.startsWith(SEP)) {
if (name.startsWith("/")) {
name = name.substring(1);
}
if (name.startsWith("f" + SEP)) {
if (name.startsWith("f/")) {
name = name.substring(2);
}
name = name.split(SEP)[0];
name = name.split("/")[0];
log.debug(`Processing local folder ${name}`);
if (raw) {

View File

@@ -31,7 +31,7 @@ addEventListener("error", (event) => {
}
});
export const VERSION = "v1.173.0";
export const VERSION = "v1.168.3";
let command: any = new Command()
.name("wmill")

View File

@@ -1,6 +1,6 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions } from "./types.ts";
import { colors, Command, JSZip, log } from "./deps.ts";
import { colors, Command, JSZip } from "./deps.ts";
import { Workspace } from "./workspace.ts";
import { getHeaders } from "./utils.ts";
@@ -45,8 +45,6 @@ export async function downloadZip(
colors.red("Failed to request tarball from API " + zipResponse.statusText)
);
throw new Error(await zipResponse.text());
} else {
log.debug(`Downloaded zip/tarball successfully`);
}
const blob = await zipResponse.blob();
return await JSZip.loadAsync(blob as any);

View File

@@ -34,7 +34,7 @@ export async function pushResource(
try {
resource = await ResourceService.getResource({
workspace: workspace,
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
});
} catch {
// flow doesn't exist
@@ -47,7 +47,7 @@ export async function pushResource(
await ResourceService.updateResource({
workspace: workspace,
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
requestBody: { ...localResource },
});
} else {
@@ -63,7 +63,7 @@ export async function pushResource(
await ResourceService.createResource({
workspace: workspace,
requestBody: {
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
...localResource,
},
});

View File

@@ -83,9 +83,7 @@ export async function handleFile(
log.debug(`Processing local script ${path}`);
alreadySynced.push(path);
const remotePath = path
.substring(0, path.indexOf("."))
.replaceAll("\\", "/");
const remotePath = path.substring(0, path.indexOf("."));
const metaPath = remotePath + ".script.json";
let typed = undefined;
try {
@@ -107,7 +105,7 @@ export async function handleFile(
try {
remote = await ScriptService.getScriptByPath({
workspace,
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
});
log.debug(`Script ${remotePath} exists on remote`);
} catch {
@@ -144,7 +142,7 @@ export async function handleFile(
content,
description: typed?.description ?? "",
language: language as NewScript.language,
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
summary: typed?.summary ?? "",
is_template: typed?.is_template,
kind: typed?.kind,
@@ -164,7 +162,7 @@ export async function handleFile(
content,
description: typed?.description ?? "",
language: language as NewScript.language,
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
summary: typed?.summary ?? "",
is_template: typed?.is_template,
kind: typed?.kind,

View File

@@ -20,7 +20,6 @@ import {
yamlStringify,
yamlParse,
ScheduleService,
SEP,
} from "./deps.ts";
import {
getTypeStrFromPath,
@@ -238,7 +237,7 @@ function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
},
};
}
return _internal_folder("." + SEP, zip);
return _internal_folder("./", zip);
}
async function* readDirRecursiveWithIgnore(
@@ -354,27 +353,19 @@ async function compareDynFSElement(
}
const isNotWmillFile = (p: string, isDirectory: boolean) => {
if (p.endsWith(SEP)) {
if (p.endsWith("/")) {
return false;
}
if (isDirectory) {
return (
!p.startsWith("u" + SEP) &&
!p.startsWith("f" + SEP) &&
!p.startsWith("g" + SEP)
);
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/");
}
try {
const typ = getTypeStrFromPath(p);
if (typ == "resource-type") {
return p.includes(SEP);
return p.includes("/");
} else {
return (
!p.startsWith("u" + SEP) &&
!p.startsWith("f" + SEP) &&
!p.startsWith("g" + SEP)
);
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/");
}
} catch {
return true;
@@ -382,7 +373,7 @@ const isNotWmillFile = (p: string, isDirectory: boolean) => {
};
export const isWhitelisted = (p: string) => {
return p == "." + SEP || p == "" || p == "u" || p == "f" || p == "g";
return p == "./" || p == "" || p == "u" || p == "f" || p == "g";
};
export async function ignoreF() {
try {

View File

@@ -62,7 +62,7 @@ export async function pushVariable(
try {
variable = await VariableService.getVariable({
workspace: workspace,
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
decryptSecret: plainSecrets,
});
log.debug(`Variable ${remotePath} exists on remote`);
@@ -80,7 +80,7 @@ export async function pushVariable(
await VariableService.updateVariable({
workspace,
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
alreadyEncrypted: !plainSecrets,
requestBody: {
...localVariable,
@@ -94,7 +94,7 @@ export async function pushVariable(
workspace,
alreadyEncrypted: !plainSecrets,
requestBody: {
path: remotePath.replaceAll("\\", "/"),
path: remotePath,
...localVariable,
},
});

View File

@@ -95,7 +95,6 @@ services:
- METRICS_ADDR=false # (ee only, if set to true, metrics will be exposed on port 8001)
- NUM_WORKERS=4
- WORKER_TAGS=nativets,postgresql,mysql,graphql,snowflake
- WORKER_GROUP=native
# LICENSE_KEY is only needed for the enterprise edition
# - LICENSE_KEY=${WM_LICENSE_KEY}
depends_on:

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.173.0",
"version": "1.168.3",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -17,11 +17,12 @@
},
"devDependencies": {
"@floating-ui/core": "^1.3.1",
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@playwright/test": "^1.34.3",
"@rgossiaux/svelte-headlessui": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.25.0",
"@sveltejs/package": "^2.2.2",
"@rgossiaux/svelte-headlessui": "^1.0.2",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.20.1",
"@sveltejs/package": "^2.2.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/d3": "^7.4.0",
@@ -37,7 +38,7 @@
"d3-dag": "^0.11.5",
"eslint": "^8.47.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-svelte": "^2.33.1",
"eslint-plugin-svelte3": "^4.0.0",
"monaco-editor-workers": "~0.39.1",
"ol": "^7.4.0",
"openapi-typescript-codegen": "^0.25.0",
@@ -50,7 +51,7 @@
"simple-svelte-autocomplete": "^2.5.1",
"style-to-object": "^0.4.1",
"stylelint-config-recommended": "^13.0.0",
"svelte": "^4.0.0",
"svelte": "^3.59.2",
"svelte-awesome": "^3.2.1",
"svelte-awesome-color-picker": "^2.4.5",
"svelte-check": "^3.4.3",
@@ -61,7 +62,7 @@
"svelte-popperjs": "^1.3.2",
"svelte-preprocess": "^5.0.1",
"svelte-range-slider-pips": "^2.1.1",
"svelte-splitpanes": "^0.8.0",
"svelte-splitpanes": "^0.7.14",
"svelte2tsx": "^0.6.16",
"tailwindcss": "^3.3.2",
"tslib": "^2.6.1",
@@ -70,20 +71,6 @@
"vite-plugin-monaco-editor": "^1.1.0",
"yootils": "^0.3.1"
},
"overrides": {
"@rgossiaux/svelte-headlessui": {
"svelte": "$svelte"
},
"ag-grid-svelte": {
"svelte": "$svelte"
},
"svelte-chartjs": {
"svelte": "$svelte"
},
"svelte-timezone-picker": {
"svelte": "$svelte"
}
},
"type": "module",
"dependencies": {
"@aws-crypto/sha256-js": "^4.0.0",
@@ -92,7 +79,7 @@
"@leeoniya/ufuzzy": "^1.0.8",
"@popperjs/core": "^2.11.6",
"@redocly/json-to-json-schema": "^0.0.1",
"@tanstack/svelte-table": "^8.9.9",
"@tanstack/svelte-table": "^8.9.2",
"ag-grid-svelte": "^0.3.0",
"ansi_up": "^5.2.1",
"chart.js": "^4.3.0",
@@ -106,14 +93,14 @@
"graphql": "^16.7.1",
"highlight.js": "^11.8.0",
"lodash": "^4.17.21",
"lucide-svelte": "^0.277.0",
"lucide-svelte": "^0.246.0",
"monaco-graphql": "^1.3.0",
"monaco-languageclient": "~6.0.3",
"openai": "^4.3.0",
"openai": "^4.0.0-beta.12",
"quill": "^1.3.7",
"svelte-autosize": "^1.0.1",
"svelte-carousel": "^1.0.25",
"svelte-chartjs": "^3.1.2",
"svelte-chartjs": "^3.1.0",
"svelte-dnd-action": "^0.9.22",
"svelte-exmarkdown": "^2.1.0",
"svelte-infinite-loading": "^1.3.8",
@@ -128,8 +115,8 @@
"yjs": "^13.6.7"
},
"peerDependencies": {
"@sveltejs/kit": "^1.25.0",
"svelte": "^4.0.0"
"@sveltejs/kit": "^1.20.1",
"svelte": "^3.59.2"
},
"exports": {
"./package.json": "./package.json",

View File

@@ -369,7 +369,7 @@
size="sm"
variant="border"
color={key === resource_type ? 'blue' : 'light'}
btnClasses={key === resource_type ? '!border-2' : 'm-[1px]'}
btnClasses={key === resource_type ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
manual = false
resource_type = key

View File

@@ -1,12 +1,6 @@
<script lang="ts">
import { JobService, Preview } from '$lib/gen'
import {
dbSchemas,
workspaceStore,
type DBSchema,
type GraphqlSchema,
type SQLSchema
} from '$lib/stores'
import { dbSchemas, workspaceStore, type DBSchema, type GraphqlSchema } from '$lib/stores'
import Button from './common/button/Button.svelte'
import Drawer from './common/drawer/Drawer.svelte'
import DrawerContent from './common/drawer/DrawerContent.svelte'
@@ -186,51 +180,6 @@ GROUP BY
cols.append(col)
schema[dataset.dataset_id][row[0]] = cols
return schema
`,
lang: 'python3'
},
snowflake: {
code: `# requirements:
# snowflake-connector-python==3.2.0
from typing import Any
import snowflake.connector as sf
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
def main(args):
if not args["database"]:
raise Exception("a selected database is required for the schema explorer")
p_key = serialization.load_pem_private_key(
args["private_key"].encode(), password=None, backend=default_backend()
)
pkb = p_key.private_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
ctx = sf.connect(
user=args["username"],
account=args["account_identifier"],
private_key=pkb,
warehouse=args["warehouse"],
database=args["database"],
schema=args["schema"],
role=args["role"],
)
cs = ctx.cursor()
rows = cs.execute("select TABLE_SCHEMA, TABLE_NAME, DATA_TYPE, COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE from information_schema.columns where table_schema != 'INFORMATION_SCHEMA'")
schema = dict()
for row in rows:
if row[0] not in schema:
schema[row[0]] = dict()
if row[1] not in schema[row[0]]:
schema[row[0]][row[1]] = dict()
schema[row[0]][row[1]][row[3]] = {
"type": row[2],
"required": row[5] == "YES",
}
if row[4] is not None:
schema[row[0]][row[1]][row[3]]["default"] = row[4]
return schema
`,
lang: 'python3'
}
@@ -261,18 +210,19 @@ def main(args):
if (!testResult.success) {
console.error(testResult.result?.['error']?.['message'])
} else {
if (resourceType !== undefined) {
if (resourceType !== 'graphql') {
$dbSchemas[resourcePath] = {
lang: resourceType as SQLSchema['lang'],
schema: testResult.result,
publicOnly: !!testResult.result.public || !!testResult.result.PUBLIC
}
} else {
$dbSchemas[resourcePath] = {
lang: 'graphql',
schema: testResult.result
}
if (resourceType === 'postgresql') {
$dbSchemas[resourcePath] = {
lang: 'postgresql',
schema: testResult.result,
publicOnly: true
}
} else if (
resourceType !== undefined &&
['mysql', 'graphql', 'bigquery'].includes(resourceType)
) {
$dbSchemas[resourcePath] = {
lang: resourceType as 'mysql' | 'graphql' | 'bigquery',
schema: testResult.result
}
}
}
@@ -303,8 +253,8 @@ def main(args):
}
function formatSchema(dbSchema: DBSchema) {
if (dbSchema.lang !== 'graphql' && dbSchema.publicOnly) {
return dbSchema.schema.public || dbSchema.schema.PUBLIC || dbSchema
if (dbSchema.lang === 'postgresql' && dbSchema.publicOnly) {
return dbSchema.schema.public || dbSchema
} else if (dbSchema.lang === 'mysql' && Object.keys(dbSchema.schema).length === 1) {
return dbSchema.schema[Object.keys(dbSchema.schema)[0]]
} else {
@@ -349,7 +299,7 @@ def main(args):
>Refresh
</Button>
</svelte:fragment>
{#if dbSchema.lang !== 'graphql' && (dbSchema.schema?.public || dbSchema.schema?.PUBLIC)}
{#if dbSchema.lang === 'postgresql'}
<ToggleButtonGroup class="mb-4" bind:selected={dbSchema.publicOnly}>
<ToggleButton value={true} label="Public" />
<ToggleButton value={false} label="All" />

View File

@@ -70,7 +70,6 @@
export let yContent: Text | undefined = undefined
export let awareness: any | undefined = undefined
export let folding = false
export let args: Record<string, any> | undefined = undefined
languages.typescript.typescriptDefaults.setModeConfiguration({
completionItems: false,
@@ -189,27 +188,6 @@
}
}
export function append(code): void {
if (editor) {
const lineCount = editor.getModel()?.getLineCount() || 0
const lastLineLength = editor.getModel()?.getLineLength(lineCount) || 0
const range: IRange = {
startLineNumber: lineCount,
startColumn: lastLineLength + 1,
endLineNumber: lineCount,
endColumn: lastLineLength + 1
}
editor.executeEdits('append', [
{
range,
text: code,
forceMoveMarkers: true
}
])
editor.revealLine(lineCount)
}
}
export function format() {
if (editor) {
code = getCode()
@@ -225,8 +203,7 @@
let command: Disposable | undefined = undefined
let sqlSchemaCompletor: Disposable | undefined = undefined
$: args &&
(dbSchema = $dbSchemas[(lang === 'graphql' ? args.api : args.database)?.replace('$res:', '')])
$: dbSchema = $dbSchemas[Object.keys($dbSchemas)[0]]
$: dbSchema && ['sql', 'graphql'].includes(lang) && addDBSchemaCompletions()
$: (!dbSchema || lang !== 'sql') && sqlSchemaCompletor && sqlSchemaCompletor.dispose()
$: (!dbSchema || lang !== 'graphql') && graphqlService && graphqlService.setSchemaConfig([])
@@ -244,7 +221,7 @@
introspectionJSON: schema
}
])
} else {
} else if (schemaLang === 'mysql' || schemaLang === 'postgresql') {
if (sqlSchemaCompletor) {
sqlSchemaCompletor.dispose()
}

View File

@@ -37,7 +37,7 @@
import { capitalize, toCamel } from '$lib/utils'
import type { Schema, SchemaProperty, SupportedLanguage } from '$lib/common'
import ScriptVersionHistory from './ScriptVersionHistory.svelte'
import ScriptGen from './copilot/ScriptGen.svelte'
import { ScriptGen } from './codeGen'
import type DiffEditor from './DiffEditor.svelte'
import { getResetCode } from '$lib/script_helpers'
import type { Script } from '$lib/gen'
@@ -62,7 +62,6 @@
export let collabUsers: { name: string }[] = []
export let scriptPath: string | undefined = undefined
export let diffEditor: DiffEditor | undefined = undefined
export let args: Record<string, any>
let contextualVariablePicker: ItemPicker
let variablePicker: ItemPicker
@@ -531,7 +530,7 @@
</div>
{/if}
<ScriptGen {editor} {diffEditor} {lang} {iconOnly} {args} />
<ScriptGen {editor} {diffEditor} {lang} {iconOnly} />
<!-- <Popover
notClickable

View File

@@ -5,24 +5,15 @@
type Flow,
type FlowModule,
DraftService,
type PathScript,
ScriptService,
Script
type PathScript
} from '$lib/gen'
import { initHistory, push, redo, undo } from '$lib/history'
import {
enterpriseLicense,
existsOpenaiResourcePath,
hubScripts,
userStore,
workspaceStore
} from '$lib/stores'
import { encodeState, formatCron, sleep } from '$lib/utils'
import { initHistory, redo, undo } from '$lib/history'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { encodeState, formatCron } from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import type { Drawer } from '$lib/components/common'
import { faCalendarAlt, faSave } from '@fortawesome/free-solid-svg-icons'
import { setContext, tick } from 'svelte'
import { setContext } from 'svelte'
import { writable, type Writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { Badge, Button, Kbd, UndoRedo } from './common'
@@ -30,30 +21,17 @@
import FlowEditor from './flows/FlowEditor.svelte'
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
import type { FlowState } from './flows/flowState'
import { dfs as dfsApply } from './flows/flowStore'
import { dfs, getPreviousIds } from './flows/previousResults'
import { dfs } from './flows/flowStore'
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
import type { FlowEditorContext } from './flows/types'
import { cleanInputs, emptyFlowModuleState } from './flows/utils'
import { cleanInputs } from './flows/utils'
import { Pen } from 'lucide-svelte'
import { loadHubScripts } from '$lib/scripts'
import { createEventDispatcher } from 'svelte'
import Awareness from './Awareness.svelte'
import { getAllModules } from './flows/flowExplorer'
import {
stepCopilot,
type FlowCopilotModule,
glueCopilot,
type FlowCopilotContext
} from './copilot/flow'
import type { Schema, SchemaProperty } from '$lib/common'
import FlowCopilotDrawer from './copilot/FlowCopilotDrawer.svelte'
import FlowCopilotStatus from './copilot/FlowCopilotStatus.svelte'
import { fade } from 'svelte/transition'
import { loadFlowModuleState } from './flows/flowStateUtils'
import FlowCopilotInputsModal from './copilot/FlowCopilotInputsModal.svelte'
export let initialPath: string = ''
export let selectedId: string | undefined
@@ -350,7 +328,7 @@
return [
'settings-metadata',
'constants',
...dfsApply($flowStore.value.modules, (module) => module.id)
...dfs($flowStore.value.modules, (module) => module.id)
]
}
@@ -370,500 +348,18 @@
onClick: () => window.open(`/flows/add?template=${initialPath}`)
})
}
let flowCopilotContext: FlowCopilotContext = {
drawerStore: writable<Drawer | undefined>(undefined),
modulesStore: writable<FlowCopilotModule[]>([]),
currentStepStore: writable<string | undefined>(undefined),
genFlow: undefined
}
setContext('FlowCopilotContext', flowCopilotContext)
const {
drawerStore: copilotDrawerStore,
modulesStore: copilotModulesStore,
currentStepStore: copilotCurrentStepStore
} = flowCopilotContext
let doneTs = 0
async function getHubCompletions(text: string, idx: number, type: 'trigger' | 'script') {
try {
// make sure we display the results of the last request last
const ts = Date.now()
const scriptIds = await ScriptService.queryHubScripts({
text: `${text}`,
limit: 3,
kind: type
})
if (ts < doneTs) return
doneTs = ts
const scripts = scriptIds
.map((qs) => {
const s = $hubScripts?.find((hs) => hs.ask_id === Number(qs.id))
return s
})
.filter((s) => !!s)
$copilotModulesStore[idx].hubCompletions = scripts as {
path: string
summary: string
approved: boolean
kind: string
app: string
ask_id: number
}[]
} catch (err) {
if (err.name !== 'CancelError') throw err
}
}
let abortController: AbortController | undefined = undefined
let copilotLoading = false
let flowCopilotMode: 'trigger' | 'sequence' = 'trigger'
let copilotStatus: string = ''
let copilotFlowInputs: Record<string, SchemaProperty> = {}
let copilotFlowRequiredInputs: string[] = []
let openCopilotInputsModal = false
function setInitCopilotModules(mode: typeof flowCopilotMode) {
$copilotModulesStore = [
{
id: 'a',
type: mode === 'trigger' ? 'trigger' : 'script',
description: '',
code: '',
hubCompletions: [],
selectedCompletion: undefined,
source: undefined,
lang: undefined
},
{
id: 'b',
type: 'script',
description: '',
code: '',
hubCompletions: [],
selectedCompletion: undefined,
source: undefined,
lang: undefined
}
]
}
$: setInitCopilotModules(flowCopilotMode)
function applyCopilotFlowInputs() {
const properties = {
...($flowStore.schema?.properties as Record<string, SchemaProperty> | undefined),
...copilotFlowInputs
}
const required = [
...(($flowStore.schema?.required as string[] | undefined) ?? []),
...copilotFlowRequiredInputs
]
$flowStore.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties,
required,
type: 'object'
}
}
function clearFlowInputsFromStep(id: string | undefined) {
const module: FlowModule | undefined = dfs(id, $flowStore)[0]
if (module?.value.type === 'rawscript') {
// clear step inputs that start with flow_input. but not flow_input.iter
for (const key in module.value.input_transforms) {
const input = module.value.input_transforms[key]
if (
input.type === 'javascript' &&
input.expr.includes('flow_input.') &&
!input.expr.includes('flow_input.iter')
) {
module.value.input_transforms[key] = {
type: 'static',
value: undefined
}
}
}
}
$flowStore = $flowStore
}
async function finishStepGen() {
copilotFlowInputs = {}
copilotFlowRequiredInputs = []
setInitCopilotModules(flowCopilotMode)
copilotStatus = "Done! Just check the step's inputs and you're good to go!"
await sleep(3000)
copilotStatus = ''
}
async function genFlow(idx: number, flowModules: FlowModule[], stepOnly = false) {
try {
push(history, $flowStore)
let module = stepOnly ? $copilotModulesStore[0] : $copilotModulesStore[idx]
copilotLoading = true
copilotStatus = "Generating code for step '" + module.id + "'..."
$copilotCurrentStepStore = module.id
focusCopilot()
if (!stepOnly && flowModules.length > idx) {
select('')
await tick()
flowModules.splice(idx, flowModules.length - idx)
$flowStore = $flowStore
focusCopilot()
}
if (idx === 0 && !stepOnly) {
$flowStore.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties: {},
required: [],
type: 'object'
}
}
if (module.type === 'trigger') {
if (!$scheduleStore.cron) {
$scheduleStore.cron = '0 */15 * * *'
}
$scheduleStore.enabled = true
}
let hubScript:
| {
content: string
lockfile?: string | undefined
schema?: any
language: string
summary?: string | undefined
}
| undefined = undefined
if (module.source === 'hub' && module.selectedCompletion) {
hubScript = await ScriptService.getHubScriptByPath({
path: module.selectedCompletion.path
})
}
const flowModule = {
id: module.id,
stop_after_if:
module.type === 'trigger'
? {
expr: 'result == undefined || Array.isArray(result) && result.length == 0',
skip_if_stopped: true
}
: undefined,
value: {
input_transforms: {},
content: '',
language: (hubScript ? hubScript.language : module.lang ?? 'bun') as Script.language,
type: 'rawscript' as const
},
summary: module.selectedCompletion?.summary ?? module.description
}
$flowStateStore[module.id] = emptyFlowModuleState()
if (stepOnly) {
flowModules.splice(idx, 0, flowModule)
} else if (idx === 1 && $copilotModulesStore[idx - 1].type === 'trigger') {
const loopModule: FlowModule = {
id: module.id + '_loop',
value: {
type: 'forloopflow',
iterator: {
type: 'javascript',
expr: 'results.a'
},
skip_failures: true,
modules: [flowModule]
}
}
const loopState = await loadFlowModuleState(loopModule)
$flowStateStore[loopModule.id] = loopState
flowModules.push(loopModule)
} else {
flowModules.push(flowModule)
}
$copilotDrawerStore?.closeDrawer()
select(module.id)
await tick()
focusCopilot()
let isFirstInLoop = false
const parents = dfs(module.id, $flowStore).slice(1)
if (
parents[0]?.value.type === 'forloopflow' &&
parents[0].value.modules[0].id === module.id
) {
isFirstInLoop = true
}
const prevNodeId = getPreviousIds(module.id, $flowStore, false)[0]
const pastModule: FlowModule | undefined = dfs(prevNodeId, $flowStore, false)[0]
if (hubScript) {
module.editor?.setCode(hubScript.content)
} else if (module.source === 'custom') {
module.editor?.setCode('')
const deltaStore = writable<string>('')
const unsubscribe = deltaStore.subscribe(async (delta) => {
module.editor?.append(delta)
})
abortController = new AbortController()
await stepCopilot(
module,
deltaStore,
pastModule?.value.type === 'rawscript' ? pastModule.value.content : '',
pastModule?.value.type === 'rawscript' ? pastModule.value.language : undefined,
pastModule === undefined,
isFirstInLoop,
abortController
)
unsubscribe()
} else {
throw new Error('Invalid copilot module source')
}
copilotStatus = "Generating inputs for step '" + module.id + "'..."
await sleep(500) // make sure code was parsed
try {
if (flowModule.value.type === 'rawscript') {
const stepSchema: Schema = JSON.parse(JSON.stringify($flowStateStore[module.id].schema)) // deep copy
if (module.source === 'hub' && pastModule !== undefined && $existsOpenaiResourcePath) {
// ask AI to set step inputs
abortController = new AbortController()
const inputs = await glueCopilot(
Object.keys(flowModule.value.input_transforms),
pastModule.value.type === 'rawscript' ? pastModule.value.content : '',
pastModule.value.type === 'rawscript' ? pastModule.value.language : undefined,
isFirstInLoop,
abortController
)
// create flow inputs used by AI for autocompletion
copilotFlowInputs = {}
copilotFlowRequiredInputs = []
Object.entries(inputs).forEach(([key, expr]) => {
if (
key in stepSchema.properties &&
expr.includes('flow_input.') &&
!expr.includes('flow_input.iter') &&
(!$flowStore.schema || !(key in $flowStore.schema.properties)) // prevent overriding flow inputs
) {
copilotFlowInputs[key] = stepSchema.properties[key]
if (stepSchema.required.includes(key)) {
copilotFlowRequiredInputs.push(key)
}
}
})
if (!stepOnly) {
applyCopilotFlowInputs()
}
// set step inputs
Object.entries(inputs).forEach(([key, expr]) => {
flowModule.value.input_transforms[key] = {
type: 'javascript',
expr
}
})
} else {
if (module.source === 'hub' && pastModule !== undefined && !$existsOpenaiResourcePath) {
sendUserToast(
'For better input generation, enable Windmill AI in the workspace settings',
true
)
}
// create possible flow inputs for autocompletion
copilotFlowInputs = {}
copilotFlowRequiredInputs = []
Object.keys(flowModule.value.input_transforms).forEach((key) => {
if (key !== 'prev_output') {
const schema = $flowStateStore[module.id].schema
const schemaProperty = Object.entries(schema.properties).find(
(x) => x[0] === key
)?.[1]
if (
schemaProperty &&
(!$flowStore.schema || !(key in $flowStore.schema.properties)) // prevent overriding flow inputs
) {
copilotFlowInputs[key] = schemaProperty
if (schema.required.includes(key)) {
copilotFlowRequiredInputs.push(key)
}
}
}
})
if (!stepOnly) {
applyCopilotFlowInputs()
}
// programatically set step inputs
for (const key of Object.keys(flowModule.value.input_transforms)) {
flowModule.value.input_transforms[key] = {
type: 'javascript',
expr:
key === 'prev_output'
? isFirstInLoop
? 'flow_input.iter.value'
: pastModule
? 'results.' + pastModule.id
: 'flow_input.' + key
: 'flow_input.' + key
}
}
}
$flowStore = $flowStore // force rerendering
}
} catch (err) {
console.error(err)
}
if (stepOnly) {
$copilotCurrentStepStore = undefined
copilotLoading = false
copilotStatus = ''
if (Object.keys(copilotFlowInputs).length > 0) {
openCopilotInputsModal = true
} else {
finishStepGen()
}
} else {
copilotStatus =
"Waiting for the user to validate code and inputs of step '" + module.id + "'"
}
} catch (err) {
if (stepOnly) {
copilotStatus = ''
$copilotCurrentStepStore = undefined
setInitCopilotModules(flowCopilotMode)
}
if (err?.message) {
sendUserToast('Failed to generate code: ' + err.message, true)
} else {
sendUserToast('Failed to generate code', true)
console.error(err)
}
} finally {
copilotLoading = false
}
}
flowCopilotContext.genFlow = genFlow
async function finishCopilotFlowBuilder() {
copilotLoading = true
select('Input')
$copilotCurrentStepStore = 'Input'
copilotStatus = 'Setting flow inputs...'
// filter out unused flow inputs
const flowInputs: Record<string, SchemaProperty> = {}
const required = new Set<string>()
function getFlowInputs(modules: FlowModule[]) {
for (const module of modules) {
if (module.value.type === 'rawscript') {
for (const moduleAttr of Object.keys(module.value.input_transforms)) {
const input = module.value.input_transforms[moduleAttr]
if (
input.type === 'javascript' &&
input.expr.includes('flow_input.') &&
!input.expr.includes('flow_input.iter')
) {
const flowAttr = input.expr.split('.')[1]
const schema = $flowStateStore[module.id].schema
const schemaProperty = Object.entries(schema.properties).find(
(x) => x[0] === moduleAttr
)?.[1]
if (schemaProperty) {
flowInputs[flowAttr] = schemaProperty
required.add(flowAttr)
}
}
}
} else if (module.value.type === 'forloopflow') {
getFlowInputs(module.value.modules)
}
}
}
getFlowInputs($flowStore.value.modules)
$flowStore.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties: flowInputs,
required: Array.from(required),
type: 'object'
}
copilotStatus = "Done! Just check the flow's inputs and you're good to go!"
$copilotCurrentStepStore = undefined
copilotLoading = false
await sleep(3000)
copilotStatus = ''
}
function focusCopilot() {
document.querySelectorAll('.splitpanes__splitter').forEach((el) => {
el.classList.add('hidden')
})
document.querySelectorAll('#flow-graph *').forEach((el) => {
if (el instanceof HTMLElement) {
el.style.pointerEvents = 'none'
}
})
}
function blurCopilot() {
document.querySelectorAll('.splitpanes__splitter').forEach((el) => {
el.classList.remove('hidden')
})
document.querySelectorAll('#flow-graph *').forEach((el) => {
if (el instanceof HTMLElement) {
el.style.pointerEvents = ''
}
})
}
$: $copilotCurrentStepStore === undefined && blurCopilot()
</script>
<svelte:window on:keydown={onKeyDown} />
{#if !$userStore?.operator}
<FlowCopilotDrawer {getHubCompletions} {genFlow} bind:flowCopilotMode />
<FlowCopilotInputsModal
on:confirmed={async () => {
applyCopilotFlowInputs()
finishStepGen()
}}
on:canceled={async () => {
clearFlowInputsFromStep($copilotModulesStore[0]?.id)
finishStepGen()
}}
bind:open={openCopilotInputsModal}
inputs={Object.keys(copilotFlowInputs)}
/>
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />
<div class="flex flex-col flex-1 h-screen">
<!-- Nav between steps-->
<div
class="justify-between flex flex-row items-center pl-2.5 pr-6 space-x-4 scrollbar-hidden max-h-12 h-full relative"
class="justify-between flex flex-row items-center pl-2.5 pr-6 space-x-4 scrollbar-hidden max-h-12 h-full"
>
{#if $copilotCurrentStepStore !== undefined}
<div transition:fade class="absolute inset-0 bg-gray-500 bg-opacity-75 z-[900] !m-0" />
{/if}
<div class="flex w-full max-w-md gap-4 items-center">
<div class="min-w-64 w-full">
<input
@@ -933,14 +429,6 @@
<Awareness />
{/if}
<FlowCopilotStatus
{copilotLoading}
bind:copilotStatus
{genFlow}
{finishCopilotFlowBuilder}
{abortController}
/>
<FlowImportExportMenu />
<FlowPreviewButtons />

View File

@@ -47,12 +47,6 @@
let propertyType = getPropertyType(arg)
function updatePropertyType() {
propertyType = arg.type
}
$: arg?.type && arg.type !== propertyType && updatePropertyType()
function getPropertyType(arg: InputTransform | any): 'static' | 'javascript' {
let type: 'static' | 'javascript' = arg?.type ?? 'static'

View File

@@ -1,37 +0,0 @@
<script lang="ts">
import { createPopperActions } from 'svelte-popperjs'
import type { PopoverPlacement } from './Popover.model'
import Portal from 'svelte-portal'
import { fade } from 'svelte/transition'
export let placement: PopoverPlacement = 'bottom'
const [popperRef, popperContent, getInstance] = createPopperActions({ placement })
export async function refresh() {
await getInstance()?.update()
}
let showTooltip = false
export function open() {
showTooltip = true
}
export function close() {
showTooltip = false
}
</script>
<fragment use:popperRef>
<slot />
</fragment>
{#if showTooltip}
<Portal>
<div
use:popperContent
class="z-[901] rounded-lg shadow-md border p-4 bg-surface"
transition:fade={{ duration: 200 }}
>
<slot name="content" />
</div>
</Portal>
{/if}

View File

@@ -0,0 +1,110 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let open: boolean = false
export let z = 'z-30'
const dispatch = createEventDispatcher()
export function closeDrawer(): void {
document.body.style.overflow = 'auto'
open = false
dispatch('close')
}
export function openDrawer(): void {
document.body.style.overflow = 'hidden'
open = true
dispatch('open')
}
function handleKeyUp(event: KeyboardEvent): void {
const key = event.key
if (key === 'Escape' || key === 'Esc') {
if (open) {
event.preventDefault()
closeDrawer()
}
}
}
</script>
<svelte:window on:keyup={handleKeyUp} />
{#if open}
<div class="blurred-background" />
<div class="fixed top-0 w-screen h-screen {z}">
<div
class="fixed right-0 top-0 flex flex-col w-3/4 sm:w-2/3 lg:w-1/2 h-screen border border-gray-300 shadow-xl"
>
{#if open}
<div class="flex flex-row justify-between p-2 bg-surface border-b border-gray-200">
<button
on:click={() => {
open = false
closeDrawer()
}}
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<p class="font-semibold text-gray-800"><slot name="title" /></p>
<div />
</div>
<div class="flex flex-col bg-gray-50 pt-3 px-6 grow overflow-y-auto">
<slot name="content" />
</div>
<div class="flex flex-col bg-surface border-gray-200 p-2">
<div class="flex flex-row justify-between p-2">
<button
on:click={() => {
closeDrawer()
}}
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<span class="mr-4"><slot name="submission">&nbsp;</slot></span>
</div>
</div>
{/if}
</div>
</div>
{/if}
<style lang="postcss">
.blurred-background {
/* @apply absolute sm:top-6 lg:top-8 left-28 sm:left-40 md:left-48; */ /* If we wanted to make the navbars visible */
@apply fixed top-0 left-0;
@apply bg-gray-400 opacity-75;
@apply w-screen;
@apply h-screen;
z-index: 10;
}
</style>

View File

@@ -19,7 +19,7 @@
import type { PickableProperties } from './flows/previousResults'
import type DiffEditor from './DiffEditor.svelte'
import type Editor from './Editor.svelte'
import ScriptFix from './copilot/ScriptFix.svelte'
import ScriptFix from './codeGen/ScriptFix.svelte'
export let mod: FlowModule
export let schema: Schema
@@ -135,13 +135,12 @@
jobId={testJob?.id}
result={testJob.result}>
<svelte:fragment slot="copilot-fix">
{#if lang && editor && diffEditor && stepArgs && testJob?.result?.error}
{#if lang && editor && diffEditor && testJob?.result?.error}
<ScriptFix
error={JSON.stringify(testJob.result.error)}
{lang}
{editor}
{diffEditor}
args={stepArgs}
/>
{/if}
</svelte:fragment>

View File

@@ -12,7 +12,7 @@
<span class="flex items-center space-x-2">
<h1 class="!text-2xl font-semibold leading-6 tracking-tight">{title}</h1>
{#if tooltip != '' || documentationLink}
<Tooltip {documentationLink}>
<Tooltip light {documentationLink} scale={0.9} wrapperClass="flex items-center">
{tooltip}
</Tooltip>
{/if}
@@ -21,7 +21,7 @@
<span class="flex items-center space-x-2">
<h2 class="!text-sm font-semibold">{title}</h2>
{#if tooltip != '' || documentationLink}
<Tooltip {documentationLink}>
<Tooltip light {documentationLink} scale={0.9} wrapperClass="flex items-center">
{tooltip}
</Tooltip>
{/if}

View File

@@ -36,7 +36,7 @@
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses={isPicked ? '!border-2' : 'm-[1px]'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
disabled={notPickable}
on:click={() => onClick(undefined)}
>
@@ -48,8 +48,8 @@
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'light'}
btnClasses={isPicked ? '!border-2' : 'm-[1px]'}
color={isPicked ? 'blue' : 'dark'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
disabled={notPickable}
on:click={() => onClick(r)}
>

View File

@@ -10,7 +10,7 @@
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { FlowService, ScheduleService, Script, ScriptService, type Flow } from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptySchema, emptyString, formatCron, sendUserToast } from '$lib/utils'
import { faList, faSave } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
@@ -324,7 +324,7 @@
<div class="flex flex-row items-center mb-2 gap-1">
<div class="text-xl font-extrabold">Schedule</div>
<Tooltip>Schedules use CRON syntax. Seconds are mandatory.</Tooltip>
<Tooltip light>Schedules use CRON syntax. Seconds are mandatory.</Tooltip>
</div>
<CronInput disabled={!can_write} bind:schedule bind:timezone bind:validCRON />
@@ -372,7 +372,7 @@
{/if}
</div>
<h2 class="border-b pb-1 mt-8 mb-2"
>Error Handler <Tooltip>
>Error Handler <Tooltip light>
<div class="flex gap-20 items-start mt-3">
<div class="text-tertiary text-sm"
>The following args will be passed to the error handler:
@@ -429,21 +429,12 @@
<div class="flex flex-row items-center justify-between">
<div class="flex flex-row items-center mt-4 font-semibold text-sm gap-2">
<p
>{#if !$enterpriseLicense}<span class="text-normal text-2xs">(ee only)</span>{/if} Triggered
when schedule failed</p
>
<select class="!w-14" bind:value={failedExact} disabled={!$enterpriseLicense}>
<p>Triggered when schedule failed</p>
<select class="!w-14" bind:value={failedExact}>
<option value={false}>&gt;=</option>
<option value={true}>==</option>
</select>
<input
type="number"
class="!w-14 text-center"
bind:value={failedTimes}
disabled={!$enterpriseLicense}
min="1"
/>
<input type="number" class="!w-14 text-center" bind:value={failedTimes} min="1" />
<p>time{failedTimes > 1 ? 's in a row' : ''}</p>
</div>
</div>
@@ -464,10 +455,7 @@
{/if}
<h2 class="border-b pb-1 mt-8 mb-2"
>Recovery Handler {#if !$enterpriseLicense}<span class="text-normal text-2xs"
>(ee only)</span
>{/if}
<Tooltip
>Recovery Handler <Tooltip light
><div class="text-tertiary text-sm"
>The following args will be passed to the recovery handler:
<ul class="mt-1 ml-2">
@@ -495,7 +483,7 @@
{#if recoveryHandlerSelected === 'custom'}
<div class="flex flex-row mb-2">
<ScriptPicker
disabled={!can_write || !$enterpriseLicense}
disabled={!can_write}
initialPath={recoveryHandlerPath}
kind={Script.kind.SCRIPT}
allowFlow={true}

View File

@@ -160,19 +160,19 @@
<Button
size="sm"
variant="border"
color={isSelected ? 'blue' : 'light'}
btnClasses={isSelected ? '!border-2' : 'm-[1px]'}
color={isSelected ? 'blue' : 'dark'}
btnClasses={isSelected ? '!bg-blue-50/75 dark:!bg-frost-900/50 ' : ''}
on:click={() => {
property.selectedType = argType;
property.format = undefined;
property.contentEncoding = undefined;
property.enum_ = undefined;
property.pattern = undefined;
property.default = undefined;
property.selectedType = argType
property.format = undefined
property.contentEncoding = undefined
property.enum_ = undefined
property.pattern = undefined
property.default = undefined
if (argType == 'array') {
property.items = { type: 'string' }
} else {
property.items = undefined;
property.items = undefined
}
}}
>
@@ -182,15 +182,14 @@
<Button
size="sm"
variant="border"
color={!property.selectedType ? 'blue' : 'light'}
btnClasses={!property.selectedType ? '!border-2' : 'm-[1px]'}
color={!property.selectedType ? 'blue' : 'dark'}
btnClasses={!property.selectedType ? '!bg-blue-50/75 dark:!bg-frost-900/50' : ']'}
on:click={() => {
property.selectedType = undefined;
property.selectedType = undefined
}}
>
any
</Button>
</div>
</div>
<div>

View File

@@ -483,7 +483,7 @@
>
<Toggle
disabled={!$enterpriseLicense ||
isCloudHosted() ||
!isCloudHosted() ||
script.language != Script.language.BUN}
size="xs"
checked={Boolean(script.dedicated_worker)}
@@ -680,7 +680,7 @@
>
{title}
{#if desc}
<Tooltip {documentationLink}>
<Tooltip {documentationLink} class="mb-0.5 ml-1">
{desc}
</Tooltip>
{/if}

View File

@@ -249,7 +249,6 @@
kind={asKind(kind)}
{template}
{diffEditor}
{args}
/>
{#if !noSyncFromGithub}
<div class="py-1">
@@ -303,7 +302,6 @@
deno={lang == 'deno'}
automaticLayout={true}
{fixedOverflowWidgets}
{args}
/>
<DiffEditor
bind:this={diffEditor}
@@ -366,7 +364,6 @@
previewIsLoading={testIsLoading}
{editor}
{diffEditor}
{args}
/>
</Pane>
</Splitpanes>

View File

@@ -18,7 +18,6 @@
export let size: 'sm' | 'xs' = 'sm'
const id = (Math.random() + 1).toString(36).substring(10)
const dispatch = createEventDispatcher()
const bothOptions = Boolean(options.left) && Boolean(options.right);
</script>
<span class="{$$props.class} z-auto">
@@ -29,17 +28,17 @@
: 'cursor-pointer'}"
>
{#if Boolean(options?.left)}
<span
class={twMerge(
'mr-2 font-medium duration-50',
bothOptions ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.left}
</span>
<span
class={twMerge(
'mr-2 font-medium duration-50',
disabled ? 'text-disabled' : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.left}
</span>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -67,20 +66,20 @@
/>
</div>
{#if Boolean(options?.right)}
<span
class={twMerge(
'ml-2 font-medium duration-50',
bothOptions ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.right}
{#if options?.rightTooltip}
<Tooltip>{options?.rightTooltip}</Tooltip>
{/if}
</span>
<span
class={twMerge(
'ml-2 font-medium duration-50',
disabled ? 'text-disabled' : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.right}
{#if options?.rightTooltip}
<Tooltip>{options?.rightTooltip}</Tooltip>
{/if}
</span>
{/if}
</label>
</span>

View File

@@ -1,22 +1,25 @@
<script lang="ts">
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import type { PopoverPlacement } from './Popover.model'
import Popover from './Popover.svelte'
import { ExternalLink, InfoIcon } from 'lucide-svelte'
import { ExternalLink } from 'lucide-svelte'
export let light = false
export let scale = 0.8
export let wrapperClass = ''
export let placement: PopoverPlacement | undefined = undefined
export let documentationLink: string | undefined = undefined
</script>
<Popover notClickable {placement} class={wrapperClass}>
<div
class="inline-flex w-3 mx-0.5 {light
? 'text-tertiary-inverse'
: 'text-tertiary'} {$$props.class} relative"
>
<InfoIcon class="-bottom-0.5 absolute" size={16} />
</div>
<Icon
class="{light
? 'text-gray-400 dark:text-gray-200 hover:text-tertiary dark:hover:text-gray-300'
: ' text-tertiary dark:text-gray-300 hover:text-tertiary dark:hover:text-gray-400'} cursor-pointer transition-all font-thin flex h-4 p-0.5 w-4 justify-center items-center {$$props.class}"
data={faInfoCircle}
{scale}
/>
<svelte:fragment slot="text">
<slot />
{#if documentationLink}

Some files were not shown because too many files have changed in this diff Show More