Compare commits

..

7 Commits

Author SHA1 Message Date
rubenfiszel
d9dc7807fb Apply automatic changes 2024-04-17 20:40:33 +00:00
Ruben Fiszel
4043831ba2 Merge branch 'main' into fix-autosize 2024-04-17 22:38:38 +02:00
Faton Ramadani
896206b0d5 fix(frontend): improve code 2024-04-17 16:48:33 +02:00
Faton Ramadani
fdc59b1ec7 fix(frontend): Fix autosize within hidden components 2024-04-17 16:46:44 +02:00
Faton Ramadani
68c8649a13 fix(frontend): Fix autosize within hidden components 2024-04-17 16:26:01 +02:00
Faton Ramadani
b9355b7f8f fix(frontend): Fix autosize within hidden components 2024-04-17 16:17:46 +02:00
Ruben Fiszel
1c9d6c2fb7 chore(main): release 1.308.2 (#3569)
* chore(main): release 1.308.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-04-17 14:11:13 +02:00
300 changed files with 4798 additions and 10806 deletions

View File

@@ -16,7 +16,6 @@ sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i '' -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
sed -i '' -e "/^ModuleVersion =/s/= .*/= '$VERSION'/" ${root_dirpath}/powershell-client/WindmillClient/WindmillClient.psd1
# sed -i '' -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill_pg/pyproject.toml
sed -i '' -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i '' -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile

View File

@@ -12,12 +12,10 @@ sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" ${root_dirpath}/be
sed -i -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/backend/windmill-api/openapi.yaml
sed -i -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/openflow.openapi.yaml
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-client/package.json
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-client/jsr.json
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/package.json
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
sed -i -e "/^ModuleVersion =/s/= .*/= '$VERSION'/" ${root_dirpath}/powershell-client/WindmillClient/WindmillClient.psd1
# sed -i -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
sed -i -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile

View File

@@ -136,7 +136,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
${{ steps.meta-ee-public.outputs.tags }}
@@ -199,7 +199,7 @@ jobs:
platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect
PYTHON_IMAGE=python:3.12.2-slim-bookworm
tags: |
${{ steps.meta-ee-public-py312.outputs.tags }}

View File

@@ -1,16 +0,0 @@
name: Publish powershell-client
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
publish_gallery:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
- run: . ./powershell-client/publish.ps1
shell: pwsh
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}

View File

@@ -1,16 +0,0 @@
name: Publish typescript-client on JSR
on:
push:
tags:
- "v*"
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: ${{ secrets.JSR_OIDC_ID_TOKEN }}
steps:
- uses: actions/checkout@v4
- run: cd typescript-client && ./publish.jsr.sh

File diff suppressed because it is too large Load Diff

View File

@@ -12,14 +12,12 @@ RUN apt-get -y update \
RUN rustup component add rustfmt
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install cargo-chef --version ^0.1
RUN cargo install sccache --version ^0.8
ENV RUSTC_WRAPPER=sccache SCCACHE_DIR=/backend/sccache
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install cargo-chef
WORKDIR /windmill
ENV SQLX_OFFLINE=true
# ENV CARGO_INCREMENTAL=1
ENV CARGO_INCREMENTAL=1
FROM node:20-alpine as frontend
@@ -48,9 +46,7 @@ FROM rust_base AS planner
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
COPY ./backend ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef prepare --recipe-path recipe.json
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef prepare --recipe-path recipe.json
FROM rust_base AS builder
ARG features=""
@@ -59,9 +55,7 @@ COPY --from=planner /windmill/recipe.json recipe.json
RUN apt-get update && apt-get install -y libxml2-dev libxmlsec1-dev clang libclang-dev cmake
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
COPY ./backend ./
@@ -70,9 +64,7 @@ COPY --from=frontend /frontend /frontend
COPY --from=frontend /backend/windmill-api/openapi-deref.yaml ./windmill-api/openapi-deref.yaml
COPY .git/ .git/
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
FROM ${DEBIAN_IMAGE} as downloader
@@ -171,7 +163,7 @@ COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
COPY --from=downloader --chmod=755 /deno /usr/bin/deno
COPY --from=oven/bun:1.1.7 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.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

@@ -13,7 +13,7 @@ any snippets 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 python-client/ deno-client/ go-client/ powershell-client/ are
snippets upon demand. 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

@@ -352,7 +352,6 @@ you to have it being synced automatically everyday.
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DISABLE_RESPONSE_LOGS | false | Disable response logs | Server |
## Run a local dev setup
### only Frontend

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30)",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)",
"describe": {
"columns": [],
"parameters": {
@@ -66,12 +66,10 @@
"Bool",
"Int4",
"Varchar",
"Bool",
"Bool",
"Varchar"
"Bool"
]
},
"nullable": []
},
"hash": "8e7ff45c5378c3a3406ba94dc0653afa5d28c072203617c598caefaaf1bafcfb"
"hash": "020d33ed5d47350b456783fd548422ea8dcf2d786d0e9fa849754db82c9fa378"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "0d7ba88a9810e434aa00fd63bbf416cbe222f2c67ccc8aa92e651c2bea4c2d7b"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS labeled_jobs_on_jobs",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "6f12be65a4fe3eb39292164363f557de9cef7017dcfbcd40370b849a288c52e3"
}

View File

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

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "CREATE INDEX CONCURRENTLY labeled_jobs_on_jobs ON completed_job USING GIN ((result -> 'wm_labels')) WHERE result ? 'wm_label';",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "89e72f997e27b9298bd923e7f2b546c5ff061bd9ed63798024306456ca148aec"
}

View File

@@ -1,16 +1,11 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COALESCE((SELECT MIN(started_at) as min_started_at\n FROM queue\n WHERE script_path = $1 AND job_kind != 'dependencies' AND running = true AND workspace_id = $2 AND canceled = false\n GROUP BY script_path), $3) as min_started_at, now() AS now",
"query": "SELECT COALESCE((SELECT MIN(started_at) as min_started_at\n FROM queue\n WHERE script_path = $1 AND job_kind != 'dependencies' AND running = true AND workspace_id = $2 AND canceled = false\n GROUP BY script_path), $3)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "min_started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 1,
"name": "now",
"name": "coalesce",
"type_info": "Timestamptz"
}
],
@@ -22,9 +17,8 @@
]
},
"nullable": [
null,
null
]
},
"hash": "3901cce744c9b246b661c817e068bdb3b1ab504ff8070fcccf6c909ad75f1f6d"
"hash": "abc7c72dfe9b01cde6f5b206300ed33e3d15b16bce2510160166a66fb7598e61"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT args FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT args FROM input WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "args",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "bbd5f968d7b62a55a7ebf7b98cfd411678ce544fdb9472d8d223235fb2818aaf"
}

856
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.324.2"
version = "1.308.2"
authors.workspace = true
edition.workspace = true
@@ -24,7 +24,7 @@ members = [
]
[workspace.package]
version = "1.324.2"
version = "1.308.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -50,7 +50,6 @@ parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/p
prometheus = ["windmill-common/prometheus", "windmill-api/prometheus", "windmill-worker/prometheus", "windmill-queue/prometheus"]
flow_testing = ["windmill-worker/flow_testing"]
openidconnect = ["windmill-api/openidconnect"]
cloud = ["windmill-queue/cloud"]
[dependencies]
anyhow.workspace = true
@@ -109,7 +108,7 @@ windmill-parser-sql = { path = "./parsers/windmill-parser-sql" }
windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" }
windmill-api-client = { path = "./windmill-api-client" }
axum = { version = "^0.7", features = ["multipart"] }
axum = { version = "^0.7" }
headers = "^0"
hyper = { version = "^1", features = ["full"] }
tokio = { version = "^1", features = ["full", "tracing"] }
@@ -225,7 +224,7 @@ tokenizers = "0.14.1"
candle-core = "0.3.0"
candle-transformers = "0.3.0"
candle-nn = "0.3.0"
tiberius = { git = "https://github.com/prisma/tiberius", rev = "8f66a699dfa041e7b5f736c7e94f92c945453c9e", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"]}
tiberius = { version = "0.12.2", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"] }
pin-project = "1"
indexmap = { version = "2.2.5", features = ["serde"]}

View File

@@ -1 +1 @@
644b6f49f087790a728a7a0a82525997b1742738
66d9cbb158ab9a5869a45ba253bf57f2cbb5ecb6

View File

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

View File

@@ -1,2 +0,0 @@
-- Add up migration script here
ALTER TABLE script ADD COLUMN no_main_func BOOLEAN;

View File

@@ -1,2 +0,0 @@
-- Add down migration script here
DROP INDEX IF EXISTS labeled_jobs_on_completed_jobs;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
-- Add up migration script here
ALTER TABLE script ADD COLUMN codebase VARCHAR(255);

View File

@@ -51,8 +51,6 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
"tenable" => "pytenable",
"ns1" => "ns1-python",
"pymsql" => "PyMySQL",
"haystack" => "haystack-ai",
"github" => "PyGithub",
};
fn replace_import(x: String) -> String {
@@ -64,7 +62,7 @@ fn replace_import(x: String) -> String {
}
lazy_static! {
static ref RE: Regex = Regex::new(r"^\#\s?(\S+)\s*$").unwrap();
static ref RE: Regex = Regex::new(r"^\#\s?(\S+)$").unwrap();
}
fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<String> {

View File

@@ -292,30 +292,22 @@ pub fn parse_pg_typ(typ: &str) -> Typ {
Typ::List(Box::new(base_typ))
} else {
match typ {
"varchar" | "character varying" => Typ::Str(None),
"varchar" => Typ::Str(None),
"text" => Typ::Str(None),
"int" | "integer" | "int4" => Typ::Int,
"int" => Typ::Int,
"bigint" => Typ::Int,
"bool" | "boolean" => Typ::Bool,
"char" | "character" => Typ::Str(None),
"json" | "jsonb" => Typ::Object(vec![]),
"smallint" | "int2" => Typ::Int,
"smallserial" | "serial2" => Typ::Int,
"serial" | "serial4" => Typ::Int,
"bigserial" | "serial8" => Typ::Int,
"real" | "float4" => Typ::Float,
"double" | "double precision" | "float8" => Typ::Float,
"numeric" | "decimal" => Typ::Float,
"bool" => Typ::Bool,
"char" => Typ::Str(None),
"smallint" => Typ::Int,
"smallserial" => Typ::Int,
"serial" => Typ::Int,
"bigserial" => Typ::Int,
"real" => Typ::Float,
"double precision" => Typ::Float,
"numeric" => Typ::Float,
"decimal" => Typ::Float,
"oid" => Typ::Int,
"date"
| "time"
| "timetz"
| "time with time zone"
| "time without time zone"
| "timestamp"
| "timestamptz"
| "timestamp with time zone"
| "timestamp without time zone" => Typ::Datetime,
"date" | "time" | "timestamp" | "timestamptz" => Typ::Datetime,
_ => Typ::Str(None),
}
}

View File

@@ -3,7 +3,7 @@
"collaborators": [
"Ruben Fiszel <ruben@windmill.dev>"
],
"version": "1.318.0",
"version": "1.286.2",
"files": [
"windmill_parser_wasm_bg.wasm",
"windmill_parser_wasm.js",

View File

@@ -561,6 +561,10 @@ async function __wbg_load(module, imports) {
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_eval_89aaea39f7e976e8 = function(arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
@@ -612,10 +616,6 @@ function __wbg_get_imports() {
const ret = getObject(arg0) in getObject(arg1);
return ret;
};
imports.wbg.__wbg_eval_33c4985197d1feaf = function(arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) {
const ret = getObject(arg0) == getObject(arg1);
return ret;

View File

@@ -18,11 +18,11 @@ use tokio::fs::DirBuilder;
use windmill_api::HTTP_CLIENT;
use windmill_common::{
global_settings::{
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, ENV_SETTINGS,
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING,
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CUSTOM_TAGS_SETTING,
DEFAULT_TAGS_PER_WORKSPACE_SETTING, ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING,
EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING,
JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING,
},
@@ -47,11 +47,10 @@ use windmill_worker::{
use crate::monitor::{
initial_load, load_keep_job_dir, load_require_preexisting_user, load_tag_per_workspace_enabled,
monitor_db, monitor_pool, reload_base_url_setting, reload_bunfig_install_scopes_setting,
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
reload_hub_base_url_setting, reload_job_default_timeout_setting, reload_license_key,
reload_npm_config_registry_setting, reload_pip_index_url_setting,
reload_retention_period_setting, reload_scim_token_setting, reload_server_config,
reload_worker_config,
reload_extra_pip_index_url_setting, reload_hub_base_url_setting,
reload_job_default_timeout_setting, reload_license_key, reload_npm_config_registry_setting,
reload_pip_index_url_setting, reload_retention_period_setting, reload_scim_token_setting,
reload_server_config, reload_worker_config,
};
#[cfg(feature = "parquet")]
@@ -504,11 +503,6 @@ Windmill Community Edition {GIT_VERSION}
tracing::error!(error = %e, "Could not reload hub base url setting");
}
},
CRITICAL_ERROR_CHANNELS_SETTING => {
if let Err(e) = reload_critical_error_channels_setting(&db).await {
tracing::error!(error = %e, "Could not reload critical error emails setting");
}
},
a @_ => {
tracing::info!("Unrecognized Global Setting Change Payload: {:?}", a);
}

View File

@@ -22,17 +22,15 @@ use windmill_api::{
DEFAULT_BODY_LIMIT, IS_SECURE, OAUTH_CLIENTS, REQUEST_SIZE_LIMIT, SAML_METADATA, SCIM_TOKEN,
};
use windmill_common::{
ee::CriticalErrorChannel,
error,
flow_status::FlowStatusModule,
global_settings::{
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
DEFAULT_TAGS_PER_WORKSPACE_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING,
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING,
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING,
},
jobs::QueuedJob,
oauth2::REQUIRE_PREEXISTING_USER_FOR_OAUTH,
@@ -42,8 +40,7 @@ use windmill_common::{
load_worker_config, reload_custom_tags_setting, DEFAULT_TAGS_PER_WORKSPACE, SERVER_CONFIG,
WORKER_CONFIG,
},
BASE_URL, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL,
METRICS_DEBUG_ENABLED, METRICS_ENABLED,
BASE_URL, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
};
use windmill_queue::cancel_job;
use windmill_worker::{
@@ -146,10 +143,6 @@ pub async fn initial_load(
tracing::error!("Error reloading hub base url: {:?}", e)
}
if let Err(e) = reload_critical_error_channels_setting(&db).await {
tracing::error!("Could not reload critical error emails setting: {:?}", e);
}
#[cfg(feature = "parquet")]
if !_is_agent {
reload_s3_cache_setting(&db).await;
@@ -1081,27 +1074,3 @@ pub async fn reload_hub_base_url_setting(db: &DB, server_mode: bool) -> error::R
Ok(())
}
pub async fn reload_critical_error_channels_setting(db: &DB) -> error::Result<()> {
let critical_error_channels =
load_value_from_global_settings(db, CRITICAL_ERROR_CHANNELS_SETTING).await?;
let critical_error_channels = if let Some(q) = critical_error_channels {
if let Ok(v) = serde_json::from_value::<Vec<CriticalErrorChannel>>(q.clone()) {
v
} else {
tracing::error!(
"Could not parse critical_error_emails setting as an array of channels, found: {:#?}",
&q
);
vec![]
}
} else {
vec![]
};
let mut l = CRITICAL_ERROR_CHANNELS.write().await;
*l = critical_error_channels;
Ok(())
}

View File

@@ -71,7 +71,7 @@ if [ "$REVERT" == "YES" ]; then
ce_file="${ee_file/${EE_CODE_DIR}/.}"
ce_file="${root_dirpath}/backend/${ce_file}"
if [ "$REVERT_PREVIOUS" == "YES" ]; then
git checkout HEAD@{15} ${ce_file} || true
git checkout HEAD@{5} ${ce_file} || true
else
git restore --staged ${ce_file} || true
git restore ${ce_file} || true

View File

@@ -1026,7 +1026,7 @@ async fn listen_for_uuid_on(
async fn completed_job(uuid: Uuid, db: &Pool<Postgres>) -> CompletedJob {
sqlx::query_as::<_, CompletedJob>("SELECT *, result->'wm_labels' as labels 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()
@@ -1663,7 +1663,6 @@ func main(derp string) (string, error) {
.to_owned();
let result = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
lock: None,
@@ -1695,7 +1694,6 @@ echo "hello $msg"
.to_owned();
let job = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
lock: None,
@@ -1724,7 +1722,6 @@ def main():
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Python3,
@@ -1759,7 +1756,6 @@ def main():
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Python3,
@@ -1793,7 +1789,6 @@ def main():
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Python3,
@@ -3169,8 +3164,6 @@ async fn run_deployed_relative_imports(db: &Pool<Postgres>, script_content: Stri
deployment_message: None,
concurrency_key: None,
visible_to_runner_only: None,
no_main_func: None,
codebase: None
},
).await.unwrap();
@@ -3218,7 +3211,6 @@ async fn run_preview_relative_imports(db: &Pool<Postgres>, script_content: Strin
let db2 = db.clone();
in_test_worker(&db, async move {
let job = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content: script_content,
path: Some("f/system/test_import".to_string()),
language,

View File

@@ -72,6 +72,7 @@ async_zip.workspace = true
rsmq_async.workspace = true
regex.workspace = true
bytes.workspace = true
mail-send.workspace = true
samael = { workspace = true, optional = true }
async-recursion.workspace = true
rsa.workspace = true

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.324.2
version: 1.308.2
title: Windmill API
contact:
@@ -3401,13 +3401,6 @@ paths:
in: query
schema:
type: boolean
- name: hide_without_main
description: |
(default false)
hide the scripts without an exported main function
in: query
schema:
type: boolean
- name: is_template
description: |
(default regardless)
@@ -4907,8 +4900,6 @@ paths:
- language
force_viewer_static_fields:
type: object
force_viewer_one_of_fields:
type: object
required:
- args
- component
@@ -5250,11 +5241,6 @@ paths:
in: query
schema:
type: boolean
- name: is_not_schedule
description: is not a scheduled job
in: query
schema:
type: boolean
responses:
"200":
description: All queued jobs
@@ -5340,7 +5326,6 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/OrderDesc"
- $ref: "#/components/parameters/CreatedBy"
- $ref: "#/components/parameters/Label"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
@@ -5370,11 +5355,6 @@ paths:
in: query
schema:
type: boolean
- name: is_not_schedule
description: is not a scheduled job
in: query
schema:
type: boolean
responses:
"200":
description: All completed jobs
@@ -5394,7 +5374,6 @@ paths:
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/CreatedBy"
- $ref: "#/components/parameters/Label"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
@@ -5437,11 +5416,6 @@ paths:
in: query
schema:
type: boolean
- name: is_not_schedule
description: is not a scheduled job
in: query
schema:
type: boolean
responses:
"200":
description: All jobs
@@ -5580,6 +5554,7 @@ paths:
text/plain:
type: string
/w/{workspace}/jobs_u/get_flow_debug_info/{id}:
get:
summary: get flow debug info
@@ -7271,26 +7246,6 @@ paths:
items:
$ref: "#/components/schemas/Input"
/w/{workspace}/inputs/{jobOrInputId}/args:
get:
summary: Get args from history or saved input
operationId: getArgsFromHistoryOrSavedInput
tags:
- input
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: jobOrInputId
in: path
required: true
schema:
type: string
responses:
"200":
description: args
content:
application/json:
schema: {}
/w/{workspace}/inputs/list:
get:
summary: List saved Inputs for a Runnable
@@ -8071,12 +8026,6 @@ components:
in: query
schema:
type: string
Label:
name: label
description: mask to filter exact matching job's label (job labels are completed jobs with as a result an object containing a string in the array at key 'wm_labels')
in: query
schema:
type: string
ParentJob:
name: parent_job
description:
@@ -8408,11 +8357,6 @@ components:
type: boolean
visible_to_runner_only:
type: boolean
no_main_func:
type: boolean
codebase:
type: string
required:
- hash
- path
@@ -8428,8 +8372,6 @@ components:
- language
- kind
- starred
- no_main_func
NewScript:
type: object
@@ -8503,10 +8445,6 @@ components:
type: string
visible_to_runner_only:
type: boolean
no_main_func:
type: boolean
codebase:
type: string
required:
- path
- summary
@@ -8808,10 +8746,6 @@ components:
type: string
priority:
type: integer
labels:
type: array
items:
type: string
required:
- id
- created_by
@@ -8830,14 +8764,14 @@ components:
Job:
oneOf:
- allOf:
- allOf:
- $ref: "#/components/schemas/CompletedJob"
- type: object
properties:
type:
type: string
enum: [CompletedJob]
- allOf:
- allOf:
- $ref: "#/components/schemas/QueuedJob"
- type: object
properties:
@@ -9813,11 +9747,6 @@ components:
- $ref: "../../openflow.openapi.yaml#/components/schemas/OpenFlow"
- $ref: "#/components/schemas/FlowMetadata"
ExtraPerms:
type: object
additionalProperties:
type: boolean
FlowMetadata:
type: object
properties:
@@ -9833,7 +9762,9 @@ components:
archived:
type: boolean
extra_perms:
$ref: "#/components/schemas/ExtraPerms"
type: object
additionalProperties:
type: boolean
starred:
type: boolean
draft_only:
@@ -9916,10 +9847,6 @@ components:
type: object
additionalProperties:
type: object
triggerables_v2:
type: object
additionalProperties:
type: object
execution_mode:
type: string
enum: [viewer, publisher, anonymous]
@@ -10011,7 +9938,7 @@ components:
created_at:
type: string
format: date-time
value:
value:
type: object
policy:
$ref: "#/components/schemas/Policy"

View File

@@ -143,7 +143,6 @@ pub struct AppHistoryUpdate {
}
pub type StaticFields = HashMap<String, Box<RawValue>>;
pub type OneOfFields = HashMap<String, Vec<Box<RawValue>>>;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "lowercase")]
@@ -153,12 +152,6 @@ pub enum ExecutionMode {
Viewer,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PolicyTriggerableInputs {
static_inputs: StaticFields,
one_of_inputs: OneOfFields,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Policy {
pub on_behalf_of: Option<String>,
@@ -167,10 +160,7 @@ pub struct Policy {
// - script/<path>
// - flow/<path>
// - rawscript/<sha256>
#[serde(skip_serializing_if = "Option::is_none")]
pub triggerables: Option<HashMap<String, StaticFields>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub triggerables_v2: Option<HashMap<String, PolicyTriggerableInputs>>,
pub triggerables: HashMap<String, StaticFields>,
pub execution_mode: ExecutionMode,
}
@@ -944,7 +934,6 @@ pub struct ExecuteApp {
pub raw_code: Option<RawCode>,
// if set, the app is executed as viewer with the given static fields
pub force_viewer_static_fields: Option<StaticFields>,
pub force_viewer_one_of_fields: Option<OneOfFields>,
}
fn digest(code: &str) -> String {
@@ -977,56 +966,39 @@ async fn execute_component(
let path = path.to_path();
let policy = match payload.clone() {
ExecuteApp {
force_viewer_static_fields: Some(static_fields),
force_viewer_one_of_fields: Some(one_of_fields),
..
} => {
let mut hm = HashMap::new();
let policy = if let Some(static_fields) = payload.clone().force_viewer_static_fields {
let mut hm = HashMap::new();
if let Some(path) = payload.path.clone() {
hm.insert(
format!("{}:{path}", payload.component),
PolicyTriggerableInputs {
static_inputs: static_fields,
one_of_inputs: one_of_fields,
},
);
} else {
hm.insert(
format!(
"{}:{}",
payload.component,
digest(payload.raw_code.clone().unwrap().content.as_str())
),
PolicyTriggerableInputs {
static_inputs: static_fields,
one_of_inputs: one_of_fields,
},
);
}
Policy {
execution_mode: ExecutionMode::Viewer,
triggerables: None,
triggerables_v2: Some(hm),
on_behalf_of: None,
on_behalf_of_email: None,
}
if let Some(path) = payload.path.clone() {
hm.insert(format!("{}:{path}", payload.component), static_fields);
} else {
hm.insert(
format!(
"{}:{}",
payload.component,
digest(payload.raw_code.clone().unwrap().content.as_str())
),
static_fields,
);
}
_ => {
let policy_o = sqlx::query_scalar!(
"SELECT policy from app WHERE path = $1 AND workspace_id = $2",
path,
&w_id
)
.fetch_optional(&db)
.await?;
let policy = not_found_if_none(policy_o, "App", path)?;
serde_json::from_value::<Policy>(policy).map_err(to_anyhow)?
Policy {
execution_mode: ExecutionMode::Viewer,
triggerables: hm,
on_behalf_of: None,
on_behalf_of_email: None,
}
} else {
let policy_o = sqlx::query_scalar!(
"SELECT policy from app WHERE path = $1 AND workspace_id = $2",
path,
&w_id
)
.fetch_optional(&db)
.await?;
let policy = not_found_if_none(policy_o, "App", path)?;
serde_json::from_value::<Policy>(policy).map_err(to_anyhow)?
};
let (username, permissioned_as, email) = match policy.execution_mode {
@@ -1161,106 +1133,22 @@ fn build_args(
path: String,
args: HashMap<String, Box<RawValue>>,
) -> Result<PushArgs<HashMap<String, Box<RawValue>>>> {
let key = format!("{}:{}", component, &path);
let (static_inputs, one_of_inputs) = match policy {
Policy { triggerables_v2: Some(t), .. } => {
let PolicyTriggerableInputs { static_inputs, one_of_inputs } = t
.get(&key)
.or_else(|| t.get(&path))
.map(|x| x.clone())
.or_else(|| {
if matches!(policy.execution_mode, ExecutionMode::Viewer) {
Some(PolicyTriggerableInputs {
static_inputs: HashMap::new(),
one_of_inputs: HashMap::new(),
})
} else {
None
}
})
.ok_or_else(|| {
Error::BadRequest(format!("path {} is not allowed in the app policy", path))
})?;
(static_inputs, one_of_inputs)
}
Policy { triggerables: Some(t), .. } => {
let static_inputs = t
.get(&key)
.or_else(|| t.get(&path))
.map(|x| x.clone())
.or_else(|| {
if matches!(policy.execution_mode, ExecutionMode::Viewer) {
Some(HashMap::new())
} else {
None
}
})
.ok_or_else(|| {
Error::BadRequest(format!("path {} is not allowed in the app policy", path))
})?;
(static_inputs, HashMap::new())
}
_ => Err(Error::BadRequest(format!(
"Policy is missing triggerables for {}",
key
)))?,
};
let mut args = args.clone();
let mut safe_args = HashMap::<String, Box<RawValue>>::new();
for (k, v) in one_of_inputs {
if let Some(arg_val) = args.get(&k) {
let arg_str = arg_val.get();
let options_str_vec = v.iter().map(|x| x.get()).collect::<Vec<&str>>();
if options_str_vec.contains(&arg_str) {
safe_args.insert(k.to_string(), arg_val.clone());
args.remove(&k);
continue;
}
// check if multiselect
if let Ok(args_str_vec) = serde_json::from_str::<Vec<Box<RawValue>>>(arg_val.get()) {
if args_str_vec
.iter()
.all(|x| options_str_vec.contains(&x.get()))
{
safe_args.insert(k.to_string(), arg_val.clone());
args.remove(&k);
continue;
}
}
return Err(Error::BadRequest(format!(
"argument {} with value {} must be one of [{}]",
k,
arg_str,
options_str_vec.join(",")
)));
}
}
// disallow var and res access in args coming from the user for security reasons
let mut safe_args: HashMap<String, Box<RawValue>> = args.clone();
for (k, v) in args {
let arg_str = serde_json::to_string(&v).unwrap_or_else(|_| "".to_string());
if !arg_str.contains("$var:") && !arg_str.contains("$res:") {
safe_args.insert(k.to_string(), v);
} else {
let args_str = serde_json::to_string(&v).unwrap_or_else(|_| "".to_string());
if args_str.contains("$var:") || args_str.contains("$res:") {
safe_args.insert(
k.to_string(),
RawValue::from_string(
arg_str
args_str
.replace(
"$var:",
"The following variable has been omitted for security reasons: ",
"The following variable has been ommited for security reasons: ",
)
.replace(
"$res:",
"The following resource has been omitted for security reasons: ",
"The following resource has been ommited for security reasons: ",
),
)
.map_err(|e| {
@@ -1272,8 +1160,24 @@ fn build_args(
);
}
}
let key = format!("{}:{}", component, &path);
let static_args = policy
.triggerables
.get(&key)
.or_else(|| policy.triggerables.get(&path))
.map(|x| x.clone())
.or_else(|| {
if matches!(policy.execution_mode, ExecutionMode::Viewer) {
Some(HashMap::new())
} else {
None
}
})
.ok_or_else(|| {
Error::BadRequest(format!("path {} is not allowed in the app policy", path))
})?;
let mut extra = HashMap::new();
for (k, v) in static_inputs {
for (k, v) in static_args {
extra.insert(k.to_string(), v.to_owned());
}
Ok(PushArgs { extra, args: sqlx::types::Json(safe_args) })

View File

@@ -136,15 +136,6 @@ impl Migrate for CustomMigrator {
migration.version,
migration.description
);
if migration.version == 20240424083501 {
tracing::info!("Special migration to add index concurrently on job labels 2");
sqlx::query!(
"DROP INDEX CONCURRENTLY IF EXISTS labeled_jobs_on_jobs"
).execute(&mut *self.inner).await?;
sqlx::query!(
"CREATE INDEX CONCURRENTLY labeled_jobs_on_jobs ON completed_job USING GIN ((result -> 'wm_labels')) WHERE result ? 'wm_label';"
).execute(&mut *self.inner).await?;
}
let r = self.inner.apply(migration).await;
tracing::info!("Finished applying migration {}", migration.version);
r

View File

@@ -543,7 +543,7 @@ async fn update_flow(
clear_schedule(tx.transaction_mut(), &schedule.path, &w_id).await?;
if schedule.enabled {
tx = push_scheduled_job(&db, tx, &schedule).await?;
tx = push_scheduled_job(&db, tx, schedule).await?;
}
}
@@ -955,7 +955,7 @@ mod tests {
continue_on_error: None,
},
],
failure_module: Some(Box::new(FlowModule {
failure_module: Some(FlowModule {
id: "d".to_string(),
value: FlowModuleValue::Script {
path: "test".to_string(),
@@ -977,7 +977,7 @@ mod tests {
priority: None,
delete_after_use: None,
continue_on_error: None,
})),
}),
same_worker: false,
concurrent_limit: None,
concurrency_time_window_s: None,

View File

@@ -271,7 +271,7 @@ async fn update_folder(
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(mut ng): Json<UpdateFolder>,
Json(ng): Json<UpdateFolder>,
) -> Result<String> {
use sql_builder::prelude::*;
@@ -282,25 +282,6 @@ async fn update_folder(
if let Some(display_name) = ng.display_name {
sqlb.set("display_name", "?".bind(&display_name));
}
if !authed.is_admin {
let prefixed_username = format!("u/{}", authed.username);
if ng.owners.as_ref().is_some_and(|x| {
!x.contains(&prefixed_username)
&& !authed.groups.iter().any(|g| x.contains(&format!("g/{g}")))
}) {
ng.owners.as_mut().unwrap().push(prefixed_username.clone());
if ng.extra_perms.is_none() {
ng.extra_perms = Some(serde_json::Value::Object(serde_json::Map::new()));
}
ng.extra_perms
.as_mut()
.unwrap()
.as_object_mut()
.unwrap()
.insert(prefixed_username, serde_json::json!(true));
}
}
if let Some(owners) = ng.owners {
sqlb.set(
"owners",
@@ -329,16 +310,9 @@ async fn update_folder(
.sql()
.map_err(|e| error::Error::InternalErr(e.to_string()))?;
let nfolder = sqlx::query_as::<_, Folder>(&sql)
.fetch_optional(&mut *tx)
.fetch_one(&mut *tx)
.await?;
let nfolder = nfolder.ok_or_else(|| {
windmill_common::error::Error::NotAuthorized(format!(
"You are not an owner of {} and hence cannot modify it",
name
))
})?;
if let Some(extra_perms) = nfolder.extra_perms.as_object() {
for o in nfolder.owners {
if !extra_perms

View File

@@ -25,7 +25,7 @@ use windmill_common::{
error::JsonResult,
jobs::JobKind,
scripts::to_i64,
utils::{not_found_if_none, paginate, Pagination},
utils::{paginate, Pagination},
};
pub fn workspaced_service() -> Router {
Router::new()
@@ -34,10 +34,6 @@ pub fn workspaced_service() -> Router {
.route("/create", post(create_input))
.route("/update", post(update_input))
.route("/delete/:id", post(delete_input))
.route(
"/:job_or_input_id/args",
get(get_args_from_history_or_saved_input),
)
}
#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
@@ -127,7 +123,7 @@ async fn get_input_history(
let mut tx = user_db.begin(&authed).await?;
let sql = &format!(
"select id, created_at, created_by, CASE WHEN args is null or pg_column_size(args) < 40000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, success from completed_job \
"select id, created_at, created_by, args, success 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()
@@ -171,27 +167,6 @@ async fn get_input_history(
Ok(Json(inputs))
}
async fn get_args_from_history_or_saved_input(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, job_or_input_id)): Path<(String, Uuid)>,
) -> JsonResult<Option<Value>> {
let mut tx = user_db.begin(&authed).await?;
let result_o = sqlx::query_scalar!(
"SELECT args FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT args FROM input WHERE id = $1 AND workspace_id = $2",
job_or_input_id,
w_id
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
let result = not_found_if_none(result_o, "Input args", job_or_input_id.to_string())?;
Ok(Json(result))
}
async fn list_saved_inputs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -204,7 +179,7 @@ async fn list_saved_inputs(
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, InputRow>(
"select id, workspace_id, runnable_id, runnable_type, name, CASE WHEN pg_column_size(args) < 40000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, created_at, created_by, is_public from input \
"select * from input \
where runnable_id = $1 and runnable_type = $2 and workspace_id = $3 \
and (is_public IS true OR created_by = $4) \
order by created_at desc limit $5 offset $6",

View File

@@ -19,9 +19,6 @@ use windmill_common::jobs::{
format_completed_job_result, format_result, CompletedJobWithFormattedResult, FormattedResult,
ENTRYPOINT_OVERRIDE,
};
#[cfg(all(feature = "enterprise", feature = "parquet"))]
use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH;
use windmill_common::variables::get_workspace_key;
use crate::db::ApiAuthed;
@@ -166,7 +163,6 @@ pub fn workspaced_service() -> Router {
.layer(cors.clone()),
)
.route("/run/preview", post(run_preview_script))
.route("/run/preview_bundle", post(run_bundle_preview_script))
.route("/add_batch_jobs/:n", post(add_batch_jobs))
.route("/run/preview_flow", post(run_preview_flow_job))
.route(
@@ -589,7 +585,6 @@ fn generate_get_job_query(no_logs: bool, table: &str) -> String {
result,
deleted,
is_skipped,
result->'wm_labels' as labels,
CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result"
} else {
"scheduled_for,
@@ -784,8 +779,6 @@ pub struct ListableCompletedJob {
pub tag: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<i16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<serde_json::Value>,
}
#[derive(Deserialize, Clone)]
@@ -846,7 +839,6 @@ pub struct ListQueueQuery {
pub all_workspaces: Option<bool>,
pub is_flow_step: Option<bool>,
pub has_null_parent: Option<bool>,
pub is_not_schedule: Option<bool>,
}
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
@@ -951,10 +943,6 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
sqlb.and_where_le("scheduled_for", "now()");
}
if lq.is_not_schedule.unwrap_or(false) {
sqlb.and_where("schedule_path IS null");
}
sqlb
}
@@ -1183,14 +1171,13 @@ async fn list_jobs(
"null as concurrent_limit",
"null as concurrency_time_window_s",
"priority",
"result->'wm_labels' as labels",
],
))
} else {
None
};
let sql = if lq.success.is_none() && lq.label.is_none() {
let sql = if lq.success.is_none() {
let sqlq = list_queue_jobs_query(
&w_id,
&ListQueueQuery {
@@ -1216,7 +1203,6 @@ async fn list_jobs(
all_workspaces: lq.all_workspaces,
is_flow_step: lq.is_flow_step,
has_null_parent: lq.has_null_parent,
is_not_schedule: lq.is_not_schedule,
},
&[
"'QueuedJob' as typ",
@@ -1250,7 +1236,6 @@ async fn list_jobs(
"concurrent_limit",
"concurrency_time_window_s",
"priority",
"null as labels",
],
);
@@ -1983,7 +1968,6 @@ struct UnifiedJob {
concurrent_limit: Option<i32>,
concurrency_time_window_s: Option<i32>,
priority: Option<i16>,
labels: Option<serde_json::Value>,
}
impl<'a> From<UnifiedJob> for Job {
@@ -2021,7 +2005,6 @@ impl<'a> From<UnifiedJob> for Job {
mem_peak: uj.mem_peak,
tag: uj.tag,
priority: uj.priority,
labels: uj.labels,
}),
"QueuedJob" => Job::QueuedJob(QueuedJob {
workspace_id: uj.workspace_id,
@@ -2081,9 +2064,7 @@ enum PreviewKind {
Identity,
Http,
Noop,
Bundle,
}
#[derive(Deserialize)]
struct Preview {
content: Option<String>,
@@ -2304,7 +2285,7 @@ pub async fn restart_flow(
check_license_key_valid().await?;
let completed_job = sqlx::query_as::<_, CompletedJob>(
"SELECT *, result->'wm_labels' as labels from completed_job WHERE id = $1 and workspace_id = $2",
"SELECT * from completed_job WHERE id = $1 and workspace_id = $2",
)
.bind(job_id)
.bind(&w_id)
@@ -2426,7 +2407,6 @@ pub async fn run_workflow_as_code(
let (job_payload, tag, _delete_after_use, timeout) = match job.job_kind {
JobKind::Preview => (
JobPayload::Code(RawCode {
hash: None,
content: job.raw_code.unwrap_or_default(),
path: job.script_path,
language: job.language.unwrap_or_else(|| ScriptLang::Deno),
@@ -3090,7 +3070,6 @@ async fn run_preview_script(
Some(PreviewKind::Identity) => JobPayload::Identity,
Some(PreviewKind::Noop) => JobPayload::Noop,
_ => JobPayload::Code(RawCode {
hash: None,
content: preview.content.unwrap_or_default(),
path: preview.path,
language: preview.language.unwrap_or(ScriptLang::Deno),
@@ -3125,136 +3104,6 @@ async fn run_preview_script(
Ok((StatusCode::CREATED, uuid.to_string()))
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
async fn run_bundle_preview_script(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>,
mut multipart: axum::extract::Multipart,
) -> error::Result<(StatusCode, String)> {
check_license_key_valid().await?;
check_scopes(&authed, || format!("runscript"))?;
if authed.is_operator {
return Err(error::Error::NotAuthorized(
"Operators cannot run preview jobs for security reasons".to_string(),
));
}
let mut job_id = None;
let mut tx = None;
let mut uploaded = false;
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
let data = field.bytes().await.unwrap();
if name == "preview" {
let preview: Preview = serde_json::from_slice(&data).map_err(to_anyhow)?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(preview.tag.clone());
check_tag_available_for_workspace(&w_id, &tag).await?;
let ltx =
PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq.clone());
let args = preview.args.unwrap_or_default();
// hmap.insert("")
let (uuid, ntx) = push(
&db,
ltx,
&w_id,
match preview.kind {
Some(PreviewKind::Identity) => JobPayload::Identity,
Some(PreviewKind::Noop) => JobPayload::Noop,
_ => JobPayload::Code(RawCode {
hash: Some(PREVIEW_IS_CODEBASE_HASH),
content: preview.content.unwrap_or_default(),
path: preview.path,
language: preview.language.unwrap_or(ScriptLang::Deno),
lock: preview.lock,
concurrent_limit: None, // TODO(gbouv): once I find out how to store limits in the content of a script, should be easy to plug limits here
concurrency_time_window_s: None, // TODO(gbouv): same as above
cache_ttl: None,
dedicated_worker: preview.dedicated_worker,
}),
},
args,
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
None,
None,
run_query.job_id,
false,
false,
None,
true,
tag,
run_query.timeout,
None,
None,
)
.await?;
job_id = Some(uuid);
tx = Some(ntx);
}
if name == "file" {
let id = job_id
.as_ref()
.ok_or_else(|| {
Error::BadRequest(
"script need to be passed first in the multipart upload".to_string(),
)
})?
.to_string();
uploaded = true;
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
let path = windmill_common::s3_helpers::bundle(&w_id, &id);
if let Err(e) = os
.put(&object_store::path::Path::from(path.clone()), data)
.await
{
tracing::info!("Failed to put snapshot to s3 at {path}: {:?}", e);
return Err(Error::ExecutionErr(format!("Failed to put {path} to s3")));
}
} else {
return Err(Error::BadConfig("Object store is required for snapshot script and is not configured for servers".to_string()));
}
}
// println!("Length of `{}` is {} bytes", name, data.len());
}
if !uploaded {
return Err(Error::BadRequest("No file uploaded".to_string()));
}
if job_id.is_none() {
return Err(Error::BadRequest(
"No script found in the uploaded file".to_string(),
));
}
tx.unwrap().commit().await?;
Ok((StatusCode::CREATED, job_id.unwrap().to_string()))
}
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
async fn run_bundle_preview_script() -> error::Result<(StatusCode, String)> {
return Err(Error::BadRequest(
"bundle preview is an ee feature".to_string(),
));
}
#[derive(Deserialize)]
pub struct RunDependenciesRequest {
pub raw_scripts: Vec<RawScriptForDependencies>,
@@ -3883,17 +3732,6 @@ fn list_completed_jobs_query(
sqlb.and_where("result @> ?".bind(&result.replace("'", "''")));
}
if let Some(label) = &lq.label {
let mut wh = format!("result->'wm_labels' ? ");
wh.push_str(&format!("'{}'", &label.replace("'", "''")));
sqlb.and_where(&wh);
sqlb.and_where("result ? 'wm_labels'");
}
if lq.is_not_schedule.unwrap_or(false) {
sqlb.and_where("schedule_path IS null");
}
sqlb
}
#[derive(Deserialize, Clone)]
@@ -3925,8 +3763,6 @@ pub struct ListCompletedQuery {
pub scheduled_for_before_now: Option<bool>,
pub all_workspaces: Option<bool>,
pub has_null_parent: Option<bool>,
pub label: Option<String>,
pub is_not_schedule: Option<bool>,
}
async fn list_completed_jobs(
@@ -3974,7 +3810,6 @@ async fn list_completed_jobs(
"mem_peak",
"tag",
"priority",
"result->'wm_labels' as labels",
"'CompletedJob' as type",
],
)
@@ -3992,7 +3827,7 @@ async fn get_completed_job<'a>(
let job_o = sqlx::query("SELECT id, workspace_id, parent_job, created_by, created_at, duration_ms, success, script_hash, script_path,
CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language, started_at, is_skipped,
raw_lock, email, visible_to_owner, mem_peak, tag, priority, result->'wm_labels' as labels FROM completed_job WHERE id = $1 AND workspace_id = $2")
raw_lock, email, visible_to_owner, mem_peak, tag, priority FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(w_id)
.fetch_optional(&db)

View File

@@ -25,7 +25,6 @@ use db::DB;
use git_version::git_version;
use reqwest::Client;
use std::collections::HashMap;
use std::time::Duration;
use std::{net::SocketAddr, sync::Arc};
use tokio::sync::RwLock;
use tower::ServiceBuilder;
@@ -105,8 +104,6 @@ lazy_static::lazy_static! {
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(30))
.danger_accept_invalid_certs(std::env::var("ACCEPT_INVALID_CERTS").is_ok())
.build().unwrap();
@@ -144,12 +141,14 @@ pub async fn run_server(
));
let argon2 = Arc::new(Argon2::default());
let disable_response_logs = std::env::var("DISABLE_RESPONSE_LOGS")
.ok()
.map(|x| x == "true")
.unwrap_or(false);
let middleware_stack = ServiceBuilder::new()
.layer(
TraceLayer::new_for_http()
.on_response(MyOnResponse {})
.make_span_with(MyMakeSpan {})
.on_request(())
.on_failure(MyOnFailure {}),
)
.layer(Extension(db.clone()))
.layer(Extension(rsmq))
.layer(Extension(user_db))
@@ -286,18 +285,6 @@ pub async fn run_server(
.fallback(static_assets::static_handler)
.layer(middleware_stack);
let app = if disable_response_logs {
app
} else {
app.layer(
TraceLayer::new_for_http()
.on_response(MyOnResponse {})
.make_span_with(MyMakeSpan {})
.on_request(())
.on_failure(MyOnFailure {}),
)
};
let instance_name = rd_string(5);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
@@ -333,7 +320,6 @@ async fn is_up_to_date() -> Result<String, AppError> {
let error_reading_version = || anyhow::anyhow!("Error reading latest released version");
let version = HTTP_CLIENT
.get("https://api.github.com/repos/windmill-labs/windmill/releases/latest")
.timeout(Duration::from_secs(10))
.send()
.await
.context("Impossible to reach api.github")?

View File

@@ -221,7 +221,7 @@ async fn create_schedule(
.await?;
if ns.enabled.unwrap_or(true) {
tx = push_scheduled_job(&db, tx, &schedule).await?
tx = push_scheduled_job(&db, tx, schedule).await?
}
tx.commit().await?;
@@ -303,7 +303,7 @@ async fn edit_schedule(
.await?;
if schedule.enabled {
tx = push_scheduled_job(&db, tx, &schedule).await?;
tx = push_scheduled_job(&db, tx, schedule).await?;
}
tx.commit().await?;
@@ -512,7 +512,7 @@ pub async fn set_enabled(
.await?;
if payload.enabled {
tx = push_scheduled_job(&db, tx, &schedule).await?;
tx = push_scheduled_job(&db, tx, schedule).await?;
}
tx.commit().await?;
@@ -560,7 +560,7 @@ pub async fn set_enabled(
// .await?;
// if payload.enabled {
// tx = push_scheduled_job(&db, tx, &schedule).await?;
// tx = push_scheduled_job(&db, tx, schedule).await?;
// }
// tx.commit().await?;

View File

@@ -13,9 +13,6 @@ use crate::{
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
#[cfg(all(feature = "enterprise", feature = "parquet"))]
use axum::extract::Multipart;
use axum::{
extract::{Extension, Path, Query},
response::IntoResponse,
@@ -34,10 +31,6 @@ use std::{
};
use windmill_audit::audit_ee::audit_log;
use windmill_audit::ActionKind;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
use windmill_common::error::to_anyhow;
use windmill_common::{
db::UserDB,
error::{Error, JsonResult, Result},
@@ -98,8 +91,6 @@ pub struct ScriptWDraft {
pub concurrency_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visible_to_runner_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_main_func: Option<bool>,
}
pub fn global_service() -> Router {
@@ -123,7 +114,6 @@ pub fn workspaced_service() -> Router {
.route("/list", get(list_scripts))
.route("/list_search", get(list_search_scripts))
.route("/create", post(create_script))
.route("/create_snapshot", post(create_snapshot_script))
.route("/archive/p/*path", post(archive_script_by_path))
.route("/get/draft/*path", get(get_script_by_path_w_draft))
.route("/get/p/*path", get(get_script_by_path))
@@ -202,9 +192,7 @@ async fn list_scripts(
"tag",
"draft.path IS NOT NULL as has_draft",
"draft_only",
"ws_error_handler_muted",
"no_main_func",
"codebase IS NOT NULL as use_codebase"
"ws_error_handler_muted"
])
.left()
.join("favorite")
@@ -224,10 +212,6 @@ async fn list_scripts(
.limit(per_page)
.clone();
if authed.is_operator || lq.hide_without_main.unwrap_or(false) {
sqlb.and_where("o.no_main_func IS NOT TRUE");
}
if lq.show_archived.unwrap_or(false) {
sqlb.and_where_eq(
"o.created_at",
@@ -321,82 +305,6 @@ fn hash_script(ns: &NewScript) -> i64 {
dh.finish() as i64
}
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
async fn create_snapshot_script() -> Result<(StatusCode, String)> {
Err(Error::BadRequest("Upgrade to EE to use bundle".to_string()))
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
async fn create_snapshot_script(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
mut multipart: Multipart,
) -> Result<(StatusCode, String)> {
let mut script_hash = None;
let mut tx = None;
let mut uploaded = false;
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
let data = field.bytes().await.unwrap();
if name == "script" {
let ns = Some(serde_json::from_slice(&data).map_err(to_anyhow)?);
let (new_hash, ntx) = create_script_internal(
ns.unwrap(),
w_id.clone(),
authed.clone(),
db.clone(),
rsmq.clone(),
user_db.clone(),
webhook.clone(),
)
.await?;
script_hash = Some(new_hash.to_string());
tx = Some(ntx);
}
if name == "file" {
let hash = script_hash.as_ref().ok_or_else(|| {
Error::BadRequest(
"script need to be passed first in the multipart upload".to_string(),
)
})?;
uploaded = true;
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
let path = windmill_common::s3_helpers::bundle(&w_id, &hash);
if let Err(e) = os
.put(&object_store::path::Path::from(path.clone()), data)
.await
{
tracing::info!("Failed to put snapshot to s3 at {path}: {:?}", e);
return Err(Error::ExecutionErr(format!("Failed to put {path} to s3")));
}
} else {
return Err(Error::BadConfig("Object store is required for snapshot script and is not configured for servers".to_string()));
}
}
// println!("Length of `{}` is {} bytes", name, data.len());
}
if !uploaded {
return Err(Error::BadRequest("No file uploaded".to_string()));
}
if script_hash.is_none() {
return Err(Error::BadRequest(
"No script found in the uploaded file".to_string(),
));
}
tx.unwrap().commit().await?;
return Ok((StatusCode::CREATED, format!("{}", script_hash.unwrap())));
}
async fn create_script(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -406,24 +314,6 @@ async fn create_script(
Path(w_id): Path<String>,
Json(ns): Json<NewScript>,
) -> Result<(StatusCode, String)> {
let (hash, tx) = create_script_internal(ns, w_id, authed, db, rsmq, user_db, webhook).await?;
tx.commit().await?;
Ok((StatusCode::CREATED, format!("{}", hash)))
}
async fn create_script_internal<'c>(
ns: NewScript,
w_id: String,
authed: ApiAuthed,
db: sqlx::Pool<Postgres>,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
user_db: UserDB,
webhook: WebhookShared,
) -> Result<(
ScriptHash,
QueueTransaction<'c, rsmq_async::MultiplexedRsmq>,
)> {
let codebase = ns.codebase.as_ref();
#[cfg(not(feature = "enterprise"))]
if ns.ws_error_handler_muted.is_some_and(|val| val) {
return Err(Error::BadRequest(
@@ -431,10 +321,12 @@ async fn create_script_internal<'c>(
.to_string(),
));
}
let script_path = ns.path.clone();
let hash = ScriptHash(hash_script(&ns));
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, _> = (rsmq.clone(), user_db.begin(&authed).await?).into();
if sqlx::query_scalar!(
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
hash.0,
@@ -450,6 +342,7 @@ async fn create_script_internal<'c>(
.to_owned(),
));
};
let clashing_script = sqlx::query_as::<_, Script>(
"SELECT * FROM script WHERE path = $1 AND archived = false AND workspace_id = $2",
)
@@ -457,6 +350,7 @@ async fn create_script_internal<'c>(
.bind(&w_id)
.fetch_optional(&mut tx)
.await?;
struct ParentInfo {
p_hashes: Vec<i64>,
perms: serde_json::Value,
@@ -549,11 +443,13 @@ async fn create_script_internal<'c>(
r
}
}?;
let p_hashes = parent_hashes_and_perms.as_ref().map(|v| &v.p_hashes[..]);
let extra_perms = parent_hashes_and_perms
.as_ref()
.map(|v| v.perms.clone())
.unwrap_or(json!({}));
let lock = if !(ns.language == ScriptLang::Python3
|| ns.language == ScriptLang::Go
|| ns.language == ScriptLang::Bun
@@ -564,20 +460,23 @@ async fn create_script_internal<'c>(
ns.lock
.and_then(|e| if e.is_empty() { None } else { Some(e) })
};
let needs_lock_gen = lock.is_none();
let envs = ns.envs.as_ref().map(|x| x.as_slice());
let envs = if ns.envs.is_none() || ns.envs.as_ref().unwrap().is_empty() {
None
} else {
envs
};
//::text::json is to ensure we use serde_json with preserve order
sqlx::query!(
"INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, \
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, \
dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \
delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30)",
delete_after_use, timeout, concurrency_key, visible_to_runner_only) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)",
&w_id,
&hash.0,
ns.path,
@@ -606,11 +505,10 @@ async fn create_script_internal<'c>(
ns.timeout,
ns.concurrency_key,
ns.visible_to_runner_only,
ns.no_main_func,
codebase
)
.execute(&mut tx)
.await?;
let p_path_opt = parent_hashes_and_perms.as_ref().map(|x| x.p_path.clone());
if let Some(ref p_path) = p_path_opt {
sqlx::query!(
@@ -648,7 +546,7 @@ async fn create_script_internal<'c>(
clear_schedule(tx.transaction_mut(), &schedule.path, &w_id).await?;
if schedule.enabled {
tx = push_scheduled_job(&db, tx, &schedule).await?;
tx = push_scheduled_job(&db, tx, schedule).await?;
}
}
} else {
@@ -660,6 +558,7 @@ async fn create_script_internal<'c>(
.execute(&mut tx)
.await?;
}
if p_hashes.is_some() && !p_hashes.unwrap().is_empty() {
audit_log(
&mut tx,
@@ -705,6 +604,7 @@ async fn create_script_internal<'c>(
},
);
}
let permissioned_as = username_to_permissioned_as(&authed.username);
if needs_lock_gen {
let tag = if ns.dedicated_worker.is_some_and(|x| x) {
@@ -753,7 +653,7 @@ async fn create_script_internal<'c>(
None,
)
.await?;
Ok((hash, new_tx))
new_tx.commit().await?;
} else {
handle_deployment_metadata(
&authed.email,
@@ -770,8 +670,10 @@ async fn create_script_internal<'c>(
false,
)
.await?;
Ok((hash, tx))
tx.commit().await?;
}
Ok((StatusCode::CREATED, format!("{}", hash)))
}
pub async fn get_hub_script_by_path(
@@ -822,7 +724,7 @@ async fn get_script_by_path_w_draft(
let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, ScriptWDraft>(
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func FROM script LEFT JOIN draft ON
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only FROM script LEFT JOIN draft ON
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
WHERE script.path = $1 AND script.workspace_id = $2 \
AND script.created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \

View File

@@ -21,17 +21,15 @@ use axum::{
Json, Router,
};
use mail_send::{mail_builder::MessageBuilder, SmtpClientBuilder};
use serde::Deserialize;
use tokio::time::timeout;
use windmill_common::{
error::{self, JsonResult, Result},
error::{self, to_anyhow, JsonResult, Result},
global_settings::{AUTOMATE_USERNAME_CREATION_SETTING, ENV_SETTINGS, HUB_BASE_URL_SETTING},
server::Smtp,
utils::send_email,
};
#[cfg(feature = "parquet")]
use windmill_common::error::to_anyhow;
pub fn global_service() -> Router {
#[warn(unused_mut)]
let r = Router::new()
@@ -69,17 +67,34 @@ pub async fn test_email(
require_super_admin(&db, &authed.email).await?;
let smtp = test_email.smtp;
let to = test_email.to;
let client_timeout = Duration::from_secs(3);
send_email(
"Test email from Windmill",
"Test email content",
vec![to],
smtp,
Some(client_timeout),
)
.await?;
let mut client = SmtpClientBuilder::new(smtp.host, smtp.port)
.implicit_tls(smtp.tls_implicit.unwrap_or(false));
if std::env::var("ACCEPT_INVALID_CERTS").is_ok() {
client = client.allow_invalid_certs();
}
let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) {
if !username.is_empty() {
client.credentials((username, password))
} else {
client
}
} else {
client
};
let message = MessageBuilder::new()
.from(("Windmill", smtp.from.as_str()))
.to(to.clone())
.subject("Test email from Windmill")
.text_body("Test email content");
let dur = Duration::from_secs(3);
timeout(dur, client.connect())
.await
.map_err(to_anyhow)?
.map_err(to_anyhow)?
.send(message)
.await
.map_err(to_anyhow)?;
tracing::info!("Sent test email to {to}");
Ok("Sent test email".to_string())
}

View File

@@ -32,6 +32,8 @@ use axum::{
};
use hyper::{header::LOCATION, StatusCode};
use lazy_static::lazy_static;
use mail_send::mail_builder::MessageBuilder;
use mail_send::SmtpClientBuilder;
use quick_cache::sync::Cache;
use rand::rngs::OsRng;
use regex::Regex;
@@ -44,11 +46,10 @@ use windmill_audit::audit_ee::audit_log;
use windmill_audit::ActionKind;
use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING;
use windmill_common::users::truncate_token;
use windmill_common::utils::send_email;
use windmill_common::worker::{CLOUD_HOSTED, SERVER_CONFIG};
use windmill_common::{
db::UserDB,
error::{self, Error, JsonResult, Result},
error::{self, to_anyhow, Error, JsonResult, Result},
users::SUPERADMIN_SECRET_EMAIL,
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
};
@@ -1336,25 +1337,6 @@ async fn accept_invite(
.await?;
if let Some(r) = r {
let already_in_workspace = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)",
&nu.workspace_id,
&email,
)
.fetch_one(&mut *tx)
.await?
.unwrap_or(false);
if already_in_workspace {
tx.commit().await?;
return Ok((
StatusCode::CREATED,
format!(
"user {} accepted invite to workspace {}",
&email, nu.workspace_id
),
));
}
let username;
(tx, username) = add_user_to_workspace(
&nu.workspace_id,
@@ -1823,15 +1805,41 @@ pub fn send_email_if_possible(subject: &str, content: &str, to: &str) {
let content = content.to_string();
let to = to.to_string();
tokio::spawn(async move {
if let Err(e) = send_email_if_possible_intern(&subject, &content, to.clone()).await {
tracing::error!("Failed to send email to {}: {}", to, e);
if let Err(e) = send_email_if_possible_intern(&subject, &content, &to).await {
tracing::error!("Failed to send email to {}: {}", &to, e);
}
});
}
pub async fn send_email_if_possible_intern(subject: &str, content: &str, to: String) -> Result<()> {
pub async fn send_email_if_possible_intern(subject: &str, content: &str, to: &str) -> Result<()> {
if let Some(smtp) = SERVER_CONFIG.read().await.smtp.clone() {
send_email(subject, content, vec![to], smtp, None).await?;
let mut client = SmtpClientBuilder::new(smtp.host, smtp.port)
.implicit_tls(smtp.tls_implicit.unwrap_or(false));
if std::env::var("ACCEPT_INVALID_CERTS").is_ok() {
client = client.allow_invalid_certs();
}
let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) {
if !username.is_empty() {
client.credentials((username, password))
} else {
client
}
} else {
client
};
let message = MessageBuilder::new()
.from(("Windmill", smtp.from.as_str()))
.to(to)
.subject(subject)
.text_body(content);
client
.connect()
.await
.map_err(to_anyhow)?
.send(message)
.await
.map_err(to_anyhow)?;
tracing::info!("Sent email to {to}: {subject}");
}
return Ok(());
}

View File

@@ -71,7 +71,6 @@ impl WebhookShared {
let (tx, mut rx) = mpsc::unbounded_channel::<WebhookPayload>();
let _process = tokio::spawn(async move {
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(5))
// TODO: investigate pool timeouts and such if TCP load is high
.timeout(Duration::from_secs(5))
.build()

View File

@@ -53,6 +53,7 @@ use windmill_common::{
variables::ExportableListableVariable,
};
use windmill_git_sync::handle_deployment_metadata;
use windmill_queue::QueueTransaction;
use crate::oauth2_ee::InstanceEvent;
use crate::variables::{decrypt, encrypt};
@@ -487,25 +488,25 @@ async fn run_slack_message_test_job(
json!(format!("$res:{WORKSPACE_SLACK_BOT_TOKEN_PATH}")),
);
let uuid = windmill_queue::push_error_handler(
let tx: QueueTransaction<'_, _> = (rsmq.clone(), db.begin().await?).into();
let (uuid, tx) = windmill_queue::handle_on_failure(
&db,
rsmq,
tx,
Uuid::parse_str("00000000-0000-0000-0000-000000000000")?,
None,
Some("slack_message_test".to_string()),
"slack_message_test",
"slack_message_test",
false,
w_id.as_str(),
&format!("script/{}", req.hub_script_path.as_str()),
sqlx::types::Json(&fake_result),
None,
Some(Utc::now()),
0,
Utc::now(),
Some(json!(extra_args)),
authed.email.as_str(),
false,
false,
None, // Note: we could mark it as high priority to return result quickly to the user
)
.await?;
tx.commit().await?;
Ok(Json(RunSlackMessageTestJobResponse {
job_uuid: uuid.to_string(),
@@ -721,16 +722,6 @@ async fn edit_auto_invite(
for user in users_to_auto_add.as_ref().unwrap() {
auto_add_user(&user.email, &w_id, &operator, &mut tx).await?;
send_email_if_possible(
&format!("Added to Windmill's workspace: {w_id}"),
&format!(
"You have been granted access to Windmill's workspace {w_id} by {email}.
Access the workspace at {}/?workspace={w_id}",
BASE_URL.read().await.clone()
),
&user.email,
);
}
} else {
sqlx::query!(
@@ -1945,22 +1936,6 @@ async fn invite_user(
let mut tx = db.begin().await?;
let already_in_workspace = sqlx::query_scalar!(
"SELECT EXISTS (SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)",
&w_id,
nu.email
)
.fetch_one(&mut *tx)
.await?
.unwrap_or(false);
if already_in_workspace {
return Err(Error::BadRequest(format!(
"user with email {} already exists in workspace {}",
nu.email, w_id
)));
}
sqlx::query!(
"INSERT INTO workspace_invite
(workspace_id, email, is_admin, operator)
@@ -2071,14 +2046,6 @@ async fn add_user(
.execute(&mut *tx)
.await?;
sqlx::query!(
"DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2",
&w_id,
nu.email
)
.execute(&mut *tx)
.await?;
sqlx::query_as!(
Group,
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
@@ -2216,10 +2183,6 @@ struct ScriptMetadata {
pub restart_unless_cancelled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visible_to_runner_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_main_func: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub codebase: Option<String>,
}
pub fn is_none_or_false(val: &Option<bool>) -> bool {
@@ -2496,8 +2459,6 @@ async fn tarball_workspace(
delete_after_use: script.delete_after_use,
restart_unless_cancelled: script.restart_unless_cancelled,
visible_to_runner_only: script.visible_to_runner_only,
no_main_func: script.no_main_func,
codebase: script.codebase,
};
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
archive

View File

@@ -10,7 +10,7 @@ enterprise = []
prometheus = ["dep:prometheus"]
flamegraph = ["dep:tracing-flame"]
loki = ["dep:tracing-loki"]
parquet = ["dep:object_store", "dep:aws-config", "dep:aws-sdk-sts", "dep:bytes"]
parquet = ["dep:object_store", "dep:aws-config", "dep:aws-sdk-sts"]
[lib]
name = "windmill_common"
@@ -46,6 +46,4 @@ object_store = { workspace = true, optional = true }
prometheus = { workspace = true, optional = true }
aws-config = { workspace = true, optional = true }
aws-sdk-sts = { workspace = true, optional = true }
indexmap.workspace = true
bytes = { workspace = true, optional = true }
mail-send.workspace = true
indexmap.workspace = true

View File

@@ -1,5 +1,4 @@
use crate::ee::LicensePlan::Community;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -19,11 +18,3 @@ pub async fn get_license_plan() -> LicensePlan {
// Implementation is not open source
return Community;
}
pub async fn trigger_critical_error_channels(_x: String) {
//not open-source
()
}
#[derive(Serialize, Deserialize)]
pub enum CriticalErrorChannel {}

View File

@@ -8,7 +8,10 @@
use axum::body::Body;
use axum::response::Response;
use axum::{response::IntoResponse, response::Json};
use axum::{
response::IntoResponse,
response::Json,
};
use hyper::StatusCode;
use sqlx::migrate::MigrateError;
@@ -42,8 +45,6 @@ pub enum Error {
SqlErr(#[from] sqlx::Error),
#[error("Bad request: {0}")]
BadRequest(String),
#[error("Quota exceeded: {0}")]
QuotaExceeded(String),
#[error("Internal: {0}")]
InternalErr(String),
#[error("Hexadecimal decoding error: {0}")]
@@ -58,8 +59,6 @@ pub enum Error {
JsonErr(serde_json::Value),
#[error("{0}")]
OpenAIError(String),
#[error("{0}")]
AlreadyCompleted(String),
}
impl Error {
@@ -82,10 +81,9 @@ impl IntoResponse for Error {
Self::NotFound(_) => axum::http::StatusCode::NOT_FOUND,
Self::NotAuthorized(_) => axum::http::StatusCode::UNAUTHORIZED,
Self::RequireAdmin(_) => axum::http::StatusCode::FORBIDDEN,
Self::SqlErr(_)
| Self::BadRequest(_)
| Self::OpenAIError(_)
| Self::QuotaExceeded(_) => axum::http::StatusCode::BAD_REQUEST,
Self::SqlErr(_) | Self::BadRequest(_) | Self::OpenAIError(_) => {
axum::http::StatusCode::BAD_REQUEST
}
_ => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
};

View File

@@ -13,19 +13,16 @@
use std::time::Duration;
pub async fn get_ip() -> anyhow::Result<String> {
tokio::select! {
biased;
_ = tokio::time::sleep(Duration::from_secs(10)) => {
return Err(anyhow::anyhow!("Expected to get ip under 10s"))
},
ip = reqwest::ClientBuilder::new()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(5))
use reqwest::Result;
pub async fn get_ip() -> Result<String> {
reqwest::ClientBuilder::new()
.timeout(Duration::from_secs(3))
.build()?
.get("https://hub.windmill.dev/getip")
.send() => Ok(ip?
.error_for_status()?
.text().await?),
}
.send()
.await?
.error_for_status()?
.text()
.await
}

View File

@@ -29,7 +29,7 @@ pub fn is_retry_default(v: &RetryStatus) -> bool {
pub struct FlowStatus {
pub step: i32,
pub modules: Vec<FlowStatusModule>,
pub failure_module: Box<FlowStatusModuleWParent>,
pub failure_module: FlowStatusModuleWParent,
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(default)]
@@ -227,7 +227,7 @@ impl FlowStatus {
.iter()
.map(|m| FlowStatusModule::WaitingForPriorSteps { id: m.id.clone() })
.collect(),
failure_module: Box::new(FlowStatusModuleWParent {
failure_module: FlowStatusModuleWParent {
parent_module: None,
module_status: FlowStatusModule::WaitingForPriorSteps {
id: f
@@ -236,7 +236,7 @@ impl FlowStatus {
.map(|x| x.id.clone())
.unwrap_or_else(|| "failure".to_string()),
},
}),
},
cleanup_module: FlowCleanupModule { flow_jobs_to_clean: vec![] },
retry: RetryStatus { fail_count: 0, failed_jobs: vec![] },
restarted_from: None,

View File

@@ -87,7 +87,7 @@ pub struct FlowValue {
pub modules: Vec<FlowModule>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub failure_module: Option<Box<FlowModule>>,
pub failure_module: Option<FlowModule>,
#[serde(default)]
#[serde(skip_serializing_if = "is_default")]
pub same_worker: bool,

View File

@@ -24,7 +24,6 @@ pub const OBJECT_STORE_CACHE_CONFIG_SETTING: &str = "object_store_cache_config";
pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation";
pub const HUB_BASE_URL_SETTING: &str = "hub_base_url";
pub const CRITICAL_ERROR_CHANNELS_SETTING: &str = "critical_error_channels";
pub const ENV_SETTINGS: [&str; 50] = [
"DISABLE_NSJAIL",

View File

@@ -243,8 +243,6 @@ pub struct CompletedJob {
pub tag: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<i16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<serde_json::Value>,
}
impl CompletedJob {
@@ -345,7 +343,6 @@ pub enum JobPayload {
pub struct RawCode {
pub content: String,
pub path: Option<String>,
pub hash: Option<i64>,
pub language: ScriptLang,
pub lock: Option<String>,
pub concurrent_limit: Option<i32>,

View File

@@ -11,7 +11,6 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use ee::CriticalErrorChannel;
use error::Error;
use scripts::ScriptLang;
use sqlx::{Pool, Postgres};
@@ -45,7 +44,7 @@ pub mod workspaces;
pub mod tracing_init;
pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 5;
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 4;
pub const DEFAULT_HUB_BASE_URL: &str = "https://hub.windmill.dev";
@@ -74,9 +73,6 @@ lazy_static::lazy_static! {
pub static ref IS_READY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub static ref HUB_BASE_URL: Arc<RwLock<String>> = Arc::new(RwLock::new(DEFAULT_HUB_BASE_URL.to_string()));
pub static ref CRITICAL_ERROR_CHANNELS: Arc<RwLock<Vec<CriticalErrorChannel>>> = Arc::new(RwLock::new(vec![]));
}
pub async fn shutdown_signal(

View File

@@ -177,48 +177,6 @@ pub fn build_object_store_client(
}
}
#[cfg(feature = "parquet")]
pub async fn attempt_fetch_bytes(
client: Arc<dyn ObjectStore>,
path: &str,
) -> error::Result<bytes::Bytes> {
use object_store::path::Path;
let object = client.get(&Path::from(path)).await;
if let Err(e) = object {
tracing::info!(
"Failed to pull bytes from object store at path {path}. Error: {:?}",
e
);
return Err(error::Error::ExecutionErr(format!(
"Failed to pull bytes from object store: {path}"
)));
}
let bytes = object.unwrap().bytes().await;
if bytes.is_err() {
tracing::info!(
"Failed to read bytes from object store: {path}. Error: {:?}",
bytes.err()
);
return Err(error::Error::ExecutionErr(format!(
"Failed to read bytes from object store: {path}"
)));
}
let bytes = bytes.unwrap();
tracing::info!("{path} len: {}", bytes.len());
if bytes.len() == 0 {
tracing::info!("object {path} not found in bucket, bytes empty",);
return Err(error::Error::ExecutionErr(format!(
"object {path} does not exist in bucket"
)));
}
return Ok(bytes);
}
#[cfg(feature = "parquet")]
use aws_config::{default_provider::credentials::DefaultCredentialsChain, Region};
#[cfg(feature = "parquet")]
@@ -450,7 +408,3 @@ impl CredentialProvider for AwsCredentialAdapter {
}))
}
}
pub fn bundle(w_id: &str, hash: &str) -> String {
format!("script_bundle/{}/{}", w_id, hash)
}

View File

@@ -128,8 +128,6 @@ impl Display for ScriptKind {
}
}
pub const PREVIEW_IS_CODEBASE_HASH: i64 = -42;
#[derive(Serialize, sqlx::FromRow)]
pub struct Script {
pub workspace_id: String,
@@ -177,10 +175,6 @@ pub struct Script {
pub concurrency_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visible_to_runner_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_main_func: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub codebase: Option<String>,
}
#[derive(Serialize, sqlx::FromRow)]
@@ -200,14 +194,6 @@ pub struct ListableScript {
pub draft_only: Option<bool>,
pub has_deploy_errors: bool,
pub ws_error_handler_muted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_main_func: Option<bool>,
#[serde(skip_serializing_if = "is_false")]
pub use_codebase: bool,
}
fn is_false(x: &bool) -> bool {
return !x;
}
#[derive(Serialize)]
@@ -265,8 +251,6 @@ pub struct NewScript {
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency_key: Option<String>,
pub visible_to_runner_only: Option<bool>,
pub no_main_func: Option<bool>,
pub codebase: Option<String>,
}
fn lock_deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
@@ -336,7 +320,6 @@ pub struct ListScriptQuery {
pub is_template: Option<bool>,
pub kinds: Option<String>,
pub starred_only: Option<bool>,
pub hide_without_main: Option<bool>,
}
pub fn to_i64(s: &str) -> crate::error::Result<i64> {

View File

@@ -53,13 +53,7 @@ pub fn initialize_tracing() {
match json_fmt {
true => ts_base.with(json_layer().flatten_event(true)).init(),
false => ts_base
.with(
compact_layer()
.with_ansi(style.to_lowercase() != "never")
.with_file(true)
.with_line_number(true)
.with_target(false),
)
.with(compact_layer().with_ansi(style.to_lowercase() != "never"))
.init(),
}
}

View File

@@ -6,16 +6,11 @@
* LICENSE-AGPL for a copy of the license.
*/
#[cfg(feature = "enterprise")]
use crate::ee::trigger_critical_error_channels;
use crate::ee::LICENSE_KEY_ID;
use crate::error::{to_anyhow, Error, Result};
use crate::global_settings::UNIQUE_ID_SETTING;
use crate::server::Smtp;
use crate::DB;
use git_version::git_version;
use mail_send::mail_builder::MessageBuilder;
use mail_send::SmtpClientBuilder;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
@@ -90,20 +85,12 @@ pub async fn query_elems_from_hub(
url: &str,
query_params: Option<Vec<(&str, String)>>,
db: &DB,
) -> Result<(
reqwest::StatusCode,
reqwest::header::HeaderMap,
axum::body::Body,
)> {
) -> Result<(reqwest::StatusCode, reqwest::header::HeaderMap, axum::body::Body)> {
let response = http_get_from_hub(http_client, url, false, query_params, db).await?;
let status = response.status();
Ok((
status,
response.headers().clone(),
axum::body::Body::from_stream(response.bytes_stream()),
))
Ok((status, response.headers().clone(), axum::body::Body::from_stream(response.bytes_stream())))
}
pub async fn http_get_from_hub(
@@ -180,61 +167,3 @@ pub enum Mode {
Server,
Standalone,
}
pub async fn send_email(
subject: &str,
content: &str,
to: Vec<String>,
smtp: Smtp,
client_timeout: Option<tokio::time::Duration>,
) -> Result<()> {
let mut client = SmtpClientBuilder::new(smtp.host, smtp.port)
.implicit_tls(smtp.tls_implicit.unwrap_or(false));
if std::env::var("ACCEPT_INVALID_CERTS").is_ok() {
client = client.allow_invalid_certs();
}
let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) {
if !username.is_empty() {
client.credentials((username, password))
} else {
client
}
} else {
client
};
let message = MessageBuilder::new()
.from(("Windmill", smtp.from.as_str()))
.to(to.clone())
.subject(subject)
.text_body(content);
match client_timeout {
Some(timeout) => {
tokio::time::timeout(timeout, client.connect())
.await
.map_err(to_anyhow)?
.map_err(to_anyhow)?
.send(message)
.await
.map_err(to_anyhow)?;
}
None => {
client
.connect()
.await
.map_err(to_anyhow)?
.send(message)
.await
.map_err(to_anyhow)?;
}
}
tracing::info!("Sent email to {:#?}: {subject}", to);
return Ok(());
}
pub async fn report_critical_error(error_message: String) -> () {
tracing::error!("CRITICAL ERROR: {error_message}");
#[cfg(feature = "enterprise")]
trigger_critical_error_channels(error_message).await;
}

View File

@@ -10,7 +10,7 @@ use magic_crypt::{MagicCrypt256, MagicCryptTrait};
use serde::{Deserialize, Serialize};
use sqlx::{Postgres, Transaction};
use crate::{worker::WORKER_GROUP, BASE_URL, DB};
use crate::{BASE_URL, DB};
lazy_static::lazy_static! {
pub static ref SECRET_SALT: Option<String> = std::env::var("SECRET_SALT").ok();
@@ -300,12 +300,6 @@ pub async fn get_reserved_variables(
description: "OIDC JWT token (EE only)".to_string(),
is_custom: false,
},
ContextualVariable {
name: "WM_WORKER_GROUP".to_string(),
value: WORKER_GROUP.clone(),
description: "name of the worker group the job is running on".to_string(),
is_custom: false,
},
].into_iter().chain( sqlx::query_as::<_, (String, String)>(
"SELECT name, value FROM workspace_env WHERE workspace_id = $1",
)

View File

@@ -11,7 +11,6 @@ path = "src/lib.rs"
[features]
default = []
enterprise = ["windmill-common/enterprise"]
cloud = []
benchmark = []
prometheus = ["dep:prometheus"]

File diff suppressed because it is too large Load Diff

View File

@@ -26,9 +26,9 @@ use windmill_common::{
pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
db: &DB,
mut tx: QueueTransaction<'c, R>,
schedule: &Schedule,
schedule: Schedule,
) -> Result<QueueTransaction<'c, R>> {
let sched = cron::Schedule::from_str(schedule.schedule.as_ref())
let sched = cron::Schedule::from_str(&schedule.schedule)
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
let tz = chrono_tz::Tz::from_str(&schedule.timezone)
@@ -68,9 +68,9 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
let mut args: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
if let Some(args_v) = &schedule.args {
if let Some(args_v) = schedule.args {
if let serde_json::Value::Object(args_m) = args_v {
args = args_m.clone()
args = args_m
} else {
return Err(error::Error::ExecutionErr(
"args of scripts needs to be dict".to_string(),
@@ -90,7 +90,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
.map(|x| (x.tag, x.dedicated_worker))
.unwrap_or_else(|| (None, None));
(
JobPayload::Flow { path: schedule.script_path.clone(), dedicated_worker },
JobPayload::Flow { path: schedule.script_path, dedicated_worker },
tag,
None,
)
@@ -113,8 +113,8 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
.await?;
if schedule.retry.is_some() {
let parsed_retry = serde_json::from_value::<Retry>(schedule.retry.clone().unwrap())
.map_err(|err| {
let parsed_retry =
serde_json::from_value::<Retry>(schedule.retry.unwrap()).map_err(|err| {
error::Error::InternalErr(format!(
"Unable to parse retry information from schedule: {}",
err.to_string(),
@@ -127,7 +127,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
// if retry is set, we wrap the script into a one step flow with a retry on the module
(
JobPayload::SingleScriptFlow {
path: schedule.script_path.clone(),
path: schedule.script_path,
hash: hash,
retry: parsed_retry,
args: static_args,
@@ -144,7 +144,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
(
JobPayload::ScriptHash {
hash,
path: schedule.script_path.clone(),
path: schedule.script_path,
concurrent_limit: concurrent_limit,
concurrency_time_window_s: concurrency_time_window_s,
cache_ttl: cache_ttl,
@@ -153,7 +153,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
priority,
},
if schedule.tag.as_ref().is_some_and(|x| x != "") {
schedule.tag.clone()
schedule.tag
} else {
tag
},

View File

@@ -27,7 +27,7 @@ const p = {
const cdir = resolve("./");
const cdirNoPrivate = cdir.replace(/^\/private/, ""); // for macos
const filter = new RegExp(
`^(?!\\.\/main\\.ts)(?!${cdir}\/main\\.ts)(?!(?:/private)?${cdirNoPrivate}\/wrapper\\.mjs).*\\.ts$`
`^(?!\\.\/main\\.ts)(?!${cdir}\/main\\.ts)(?!(?:/private)?${cdirNoPrivate}\/wrapper\\.ts).*\\.ts$`
);
build.onResolve({ filter }, (args) => {
const file_path =

View File

@@ -21,7 +21,7 @@ if (!bo.success) {
content.replaceAll("__require", "require")
);
const dependencies = {};
const dependencies: Record<string, string[]> = {};
for (const i of imports) {
let [_, name, version] = i.path.match(captureVersion) ?? [];
if (name == undefined) {
@@ -46,7 +46,7 @@ if (!bo.success) {
}
}
}
const resolvedDeps = {};
const resolvedDeps: Record<string, string> = {};
for (const i in dependencies) {
const versions = dependencies[i];
resolvedDeps[i] =
@@ -61,7 +61,7 @@ if (!bo.success) {
JSON.stringify({ dependencies: resolvedDeps }, null, 2)
);
function reduceIntersect(versions, name) {
function reduceIntersect(versions: string[], name: string): string {
console.log(
`multiple versions detected for ${name}: ${versions.join(", ")}`
);

View File

@@ -66,7 +66,7 @@ mount {
src: "{JOB_DIR}/package.json"
dst: "/tmp/{LANG}/package.json"
is_bind: true
mandatory: false
mandatory: true
}
mount {
@@ -77,6 +77,13 @@ mount {
}
mount {
src: "{JOB_DIR}/wrapper.ts"
dst: "/tmp/{LANG}/wrapper.ts"
is_bind: true
mandatory: false
}
mount {
src: "{JOB_DIR}/wrapper.mjs"
dst: "/tmp/{LANG}/wrapper.mjs"
@@ -107,13 +114,6 @@ mount {
mandatory: false
}
mount {
src: "{JOB_DIR}/main.js"
dst: "/tmp/{LANG}/main.js"
is_bind: true
mandatory: false
}
mount {
src: "{JOB_DIR}/args.json"

View File

@@ -35,15 +35,11 @@ use windmill_common::{
error::{self, to_anyhow, Result},
jobs::QueuedJob,
};
#[cfg(all(feature = "enterprise", feature = "parquet"))]
use windmill_common::s3_helpers::attempt_fetch_bytes;
use windmill_parser::Typ;
const RELATIVE_BUN_LOADER: &str = include_str!("../loader.bun.js");
const RELATIVE_BUN_LOADER: &str = include_str!("../loader.bun.ts");
const RELATIVE_BUN_BUILDER: &str = include_str!("../loader_builder.bun.js");
const RELATIVE_BUN_BUILDER: &str = include_str!("../loader_builder.bun.ts");
const NSJAIL_CONFIG_RUN_BUN_CONTENT: &str = include_str!("../nsjail/run.bun.config.proto");
@@ -80,7 +76,7 @@ pub async fn gen_lockfile(
} else {
let _ = write_file(
&job_dir,
"build.js",
"build.ts",
&format!(
r#"
{}
@@ -104,7 +100,7 @@ pub async fn gen_lockfile(
.current_dir(job_dir)
.env_clear()
.envs(common_bun_proc_envs.clone())
.args(vec!["run", "build.js"])
.args(vec!["run", "build.ts"])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child_process = start_child_process(child_cmd, &*BUN_PATH).await?;
@@ -342,158 +338,9 @@ fn get_annotation(inner_content: &str) -> Annotations {
Annotations { npm_mode, nodejs_mode }
}
pub async fn build_loader(
job_dir: &str,
base_internal_url: &str,
token: &str,
w_id: &str,
current_path: &str,
nodejs_mode: bool,
) -> Result<()> {
let loader = RELATIVE_BUN_LOADER
.replace("W_ID", w_id)
.replace("BASE_INTERNAL_URL", base_internal_url)
.replace("TOKEN", token)
.replace("CURRENT_PATH", current_path)
.replace("RAW_GET_ENDPOINT", "raw_unpinned");
if nodejs_mode {
write_file(
&job_dir,
"node_builder.ts",
&format!(
r#"
{}
import {{ readdir }} from "node:fs/promises";
let fileNames = []
try {{
fileNames = await readdir("{job_dir}/node_modules")
}} catch (e) {{
}}
const bo = await Bun.build({{
entrypoints: ["{job_dir}/wrapper.mjs"],
outdir: "./",
target: "node",
plugins: [p],
external: fileNames,
}});
if (!bo.success) {{
bo.logs.forEach((l) => console.log(l));
process.exit(1);
}}
"#,
loader
),
)
.await?;
} else {
write_file(
&job_dir,
"loader.bun.js",
&format!(
r#"
import {{ plugin }} from "bun";
{}
plugin(p)
"#,
loader
),
)
.await?;
};
Ok(())
}
pub async fn generate_wrapper_mjs(
job_dir: &str,
w_id: &str,
job_id: &Uuid,
worker_name: &str,
db: &sqlx::Pool<sqlx::Postgres>,
timeout: Option<i32>,
mem_peak: &mut i32,
canceled_by: &mut Option<CanceledBy>,
common_bun_proc_envs: &HashMap<String, String>,
) -> Result<()> {
let mut child = Command::new(&*BUN_PATH);
child
.current_dir(job_dir)
.env_clear()
.envs(common_bun_proc_envs.clone())
.env("PATH", PATH_ENV.as_str())
.args(vec!["run", "node_builder.ts"])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child_process = start_child_process(child, &*BUN_PATH).await?;
handle_child(
job_id,
db,
mem_peak,
canceled_by,
child_process,
false,
worker_name,
w_id,
"bun build",
timeout,
false,
)
.await?;
tokio::fs::rename(
format!("{job_dir}/wrapper.js"),
format!("{job_dir}/wrapper.mjs"),
)
.await
.map_err(|e| error::Error::InternalErr(format!("Could not move wrapper to mjs: {e}")))?;
Ok(())
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
pub async fn pull_codebase(w_id: &str, id: &str, job_dir: &str) -> Result<()> {
let path = windmill_common::s3_helpers::bundle(&w_id, &id);
let bun_cache_path = format!("{}/{}", BUN_CACHE_DIR, path);
let dst = format!("{job_dir}/main.js");
let dirs_splitted = bun_cache_path.split("/").collect_vec();
tokio::fs::create_dir_all(dirs_splitted[..dirs_splitted.len() - 1].join("/")).await?;
if tokio::fs::metadata(&bun_cache_path).await.is_ok() {
tracing::info!("loading {bun_cache_path} from cache");
tokio::fs::symlink(&bun_cache_path, dst).await?;
} else if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
let bytes = attempt_fetch_bytes(os, &path).await?;
if *windmill_common::worker::CLOUD_HOSTED {
tokio::fs::write(dst, &bytes).await?;
} else {
tokio::fs::write(&bun_cache_path, &bytes).await?;
tokio::fs::symlink(bun_cache_path, dst).await?;
}
// extract_tar(bytes, job_dir).await?;
}
return Ok(());
}
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
pub async fn pull_codebase(_w_id: &str, _id: &str, _job_dir: &str) -> Result<()> {
return Err(error::Error::ExecutionErr(
"codebase is an EE feature".to_string(),
));
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_bun_job(
requirements_o: Option<String>,
codebase: Option<String>,
mem_peak: &mut i32,
canceled_by: &mut Option<CanceledBy>,
job: &QueuedJob,
@@ -506,20 +353,13 @@ pub async fn handle_bun_job(
envs: HashMap<String, String>,
shared_mount: &str,
) -> error::Result<Box<RawValue>> {
if !codebase.is_some() {
let _ = write_file(job_dir, "main.ts", inner_content).await?;
} else {
let _ = write_file(job_dir, "package.json", r#"{ "type": "module" }"#).await?;
}
let _ = write_file(job_dir, "main.ts", inner_content).await?;
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url).await;
let mut annotation = get_annotation(inner_content);
let annotation = get_annotation(inner_content);
if codebase.is_some() {
annotation.nodejs_mode = true
}
let main_override = get_main_override(job.args.as_ref());
#[cfg(not(feature = "enterprise"))]
@@ -529,9 +369,7 @@ pub async fn handle_bun_job(
));
}
if let Some(codebase) = codebase.as_ref() {
pull_codebase(&job.workspace_id, codebase, job_dir).await?;
} else if let Some(reqs) = requirements_o {
if let Some(reqs) = requirements_o {
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
if splitted.len() != 2 {
return Err(error::Error::ExecutionErr(
@@ -597,11 +435,10 @@ pub async fn handle_bun_job(
// }
}
let _ = write_file(job_dir, "main.ts", &remove_pinned_imports(inner_content)?).await?;
let main_code = remove_pinned_imports(inner_content)?;
let _ = write_file(job_dir, "main.ts", &main_code).await?;
let init_logs = if codebase.is_some() {
"\n\n--- NODE SNAPSHOT EXECUTION ---\n".to_string()
} else if annotation.nodejs_mode {
let init_logs = if annotation.nodejs_mode {
"\n\n--- NODE CODE EXECUTION ---\n".to_string()
} else {
"\n\n--- BUN CODE EXECUTION ---\n".to_string()
@@ -632,17 +469,11 @@ pub async fn handle_bun_job(
// we cannot use Bun.read and Bun.write because it results in an EBADF error on cloud
let main_name = main_override.unwrap_or("main".to_string());
let main_import = if codebase.is_some() {
"./main.js"
} else {
"./main.ts"
};
let wrapper_content: String = format!(
r#"
import {{ {main_name} }} from "{main_import}";
import {{ {main_name} }} from "./main.ts";
import * as fs from "fs/promises";
const fs = require('fs/promises');
const args = await fs.readFile('args.json', {{ encoding: 'utf8' }}).then(JSON.parse)
.then(({{ {spread} }}) => [ {spread} ])
@@ -653,7 +484,7 @@ BigInt.prototype.toJSON = function () {{
{dates}
async function run() {{
let res = await {main_name}(...args);
let res: any = await {main_name}(...args);
const res_json = JSON.stringify(res ?? null, (key, value) => typeof value === 'undefined' ? null : value);
await fs.writeFile("result.json", res_json);
process.exit(0);
@@ -671,7 +502,7 @@ try {{
}}
"#,
);
write_file(job_dir, "wrapper.mjs", &wrapper_content).await?;
write_file(job_dir, "wrapper.ts", &wrapper_content).await?;
Ok(()) as error::Result<()>
};
@@ -689,19 +520,65 @@ try {{
Ok(reserved_variables) as error::Result<HashMap<String, String>>
};
let write_loader_f = async {
if !codebase.is_some() {
build_loader(
job_dir,
base_internal_url,
&client.get_token().await,
&job.workspace_id,
&job.script_path(),
annotation.nodejs_mode,
let loader = RELATIVE_BUN_LOADER
.replace("W_ID", &job.workspace_id)
.replace("BASE_INTERNAL_URL", base_internal_url)
.replace("TOKEN", &client.get_token().await)
.replace("CURRENT_PATH", job.script_path())
.replace("RAW_GET_ENDPOINT", "raw_unpinned");
let write_loader_f = async move {
if annotation.nodejs_mode {
write_file(
&job_dir,
"node_builder.ts",
&format!(
r#"
{}
import {{ readdir }} from "node:fs/promises";
let fileNames = []
try {{
fileNames = await readdir("{job_dir}/node_modules")
}} catch (e) {{
}}
const bo = await Bun.build({{
entrypoints: ["{job_dir}/wrapper.ts"],
outdir: "./",
target: "node",
plugins: [p],
external: fileNames,
}});
if (!bo.success) {{
bo.logs.forEach((l) => console.log(l));
process.exit(1);
}}
"#,
loader
),
)
.await
.await?;
Ok(()) as error::Result<()>
} else {
Ok(())
write_file(
&job_dir,
"loader.bun.ts",
&format!(
r#"
import {{ plugin }} from "bun";
{}
plugin(p)
"#,
loader
),
)
.await?;
Ok(()) as error::Result<()>
}
};
@@ -711,19 +588,37 @@ try {{
write_loader_f
)?;
if annotation.nodejs_mode && !codebase.is_some() {
generate_wrapper_mjs(
job_dir,
&job.workspace_id,
if annotation.nodejs_mode {
let mut child = Command::new(&*BUN_PATH);
child
.current_dir(job_dir)
.env_clear()
.envs(common_bun_proc_envs.clone())
.env("PATH", PATH_ENV.as_str())
.args(vec!["run", "node_builder.ts"])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child_process = start_child_process(child, &*BUN_PATH).await?;
handle_child(
&job.id,
worker_name,
db,
job.timeout,
mem_peak,
canceled_by,
&common_bun_proc_envs,
child_process,
false,
worker_name,
&job.workspace_id,
"bun build",
job.timeout,
false,
)
.await?;
tokio::fs::rename(
format!("{job_dir}/wrapper.js"),
format!("{job_dir}/wrapper.mjs"),
)
.await
.map_err(|e| error::Error::InternalErr(format!("Could not move wrapper to mjs: {e}")))?;
}
//do not cache local dependencies
@@ -764,18 +659,8 @@ try {{
"run.config.proto",
"--",
&NODE_PATH,
"--experimental-default-type=module",
"/tmp/nodejs/wrapper.mjs",
]
} else if codebase.is_some() {
vec![
"--config",
"run.config.proto",
"--",
&BUN_PATH,
"run",
"/tmp/bun/wrapper.mjs",
]
} else {
vec![
"--config",
@@ -786,8 +671,8 @@ try {{
"-i",
"--prefer-offline",
"-r",
"/tmp/bun/loader.bun.js",
"/tmp/bun/wrapper.mjs",
"/tmp/bun/loader.bun.ts",
"/tmp/bun/wrapper.ts",
]
};
nsjail_cmd
@@ -812,33 +697,28 @@ try {{
.envs(envs)
.envs(reserved_variables)
.envs(common_bun_proc_envs)
.args(vec!["--experimental-default-type=module", &script_path])
.args(vec![&script_path])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
bun_cmd
} else {
let script_path = format!("{job_dir}/wrapper.mjs");
let script_path = format!("{job_dir}/wrapper.ts");
let mut bun_cmd = Command::new(&*BUN_PATH);
let args = if codebase.is_some() {
vec!["run", &script_path]
} else {
vec![
"run",
"-i",
"--prefer-offline",
"-r",
"./loader.bun.js",
&script_path,
]
};
bun_cmd
.current_dir(job_dir)
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(common_bun_proc_envs)
.args(args)
.args(vec![
"run",
"-i",
"--prefer-offline",
"-r",
"./loader.bun.ts",
&script_path,
])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
bun_cmd
@@ -900,7 +780,6 @@ use std::sync::Arc;
#[cfg(feature = "enterprise")]
pub async fn start_worker(
requirements_o: Option<String>,
codebase: Option<String>,
db: &sqlx::Pool<sqlx::Postgres>,
inner_content: &str,
base_internal_url: &str,
@@ -917,21 +796,10 @@ pub async fn start_worker(
let mut logs = "".to_string();
let mut mem_peak: i32 = 0;
let mut canceled_by: Option<CanceledBy> = None;
tracing::info!("Starting worker {w_id};{script_path} (codebase: {codebase:?}");
if !codebase.is_some() {
let _ = write_file(job_dir, "main.ts", inner_content).await?;
} else {
let _ = write_file(job_dir, "package.json", r#"{ "type": "module" }"#).await?;
}
let _ = write_file(job_dir, "main.ts", inner_content).await?;
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url).await;
let mut annotation = get_annotation(inner_content);
//TODO: remove this when bun dedicated workers work without issues
annotation.nodejs_mode = true;
let context = variables::get_reserved_variables(
db,
w_id,
@@ -950,10 +818,8 @@ pub async fn start_worker(
)
.await;
let context_envs = build_envs_map(context.to_vec()).await;
if let Some(codebase) = codebase.as_ref() {
pull_codebase(w_id, codebase, job_dir).await?;
} else if let Some(reqs) = requirements_o {
let annotation = get_annotation(inner_content);
if let Some(reqs) = requirements_o {
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
if splitted.len() != 2 {
return Err(error::Error::ExecutionErr(
@@ -1036,23 +902,9 @@ pub async fn start_worker(
let spread = args.into_iter().map(|x| x.name).join(",");
// logs.push_str(format!("infer args: {:?}\n", start.elapsed().as_micros()).as_str());
// we cannot use Bun.read and Bun.write because it results in an EBADF error on cloud
let is_debug = std::env::var("RUST_LOG").is_ok_and(|x| x == "windmill=debug");
let print_lines = if is_debug {
r#"console.log(line);"#
} else {
""
};
let main_import = if codebase.is_some() {
"./main.js"
} else {
"./main.ts"
};
let wrapper_content: String = format!(
r#"
import {{ main }} from "{main_import}";
import {{ createInterface }} from "node:readline"
import {{ main }} from "./main.ts";
BigInt.prototype.toJSON = function () {{
return this.toString();
@@ -1060,100 +912,77 @@ BigInt.prototype.toJSON = function () {{
{dates}
console.log('start');
let stdout = Bun.stdout.writer();
stdout.write('start\n');
for await (const line of createInterface({{ input: process.stdin }})) {{
{print_lines}
if (line === "end") {{
process.exit(0);
for await (const chunk of Bun.stdin.stream()) {{
const lines = Buffer.from(chunk).toString();
let exit = false;
for (const line of lines.trim().split("\n")) {{
if (line === "end") {{
exit = true;
break;
}}
try {{
let {{ {spread} }} = JSON.parse(line)
let res: any = await main(...[ {spread} ]);
stdout.write("wm_res[success]:" + JSON.stringify(res ?? null, (key, value) => typeof value === 'undefined' ? null : value) + '\n');
}} catch (e) {{
stdout.write("wm_res[error]:" + JSON.stringify({{ message: e.message, name: e.name, stack: e.stack, line: line }}) + '\n');
}}
stdout.flush();
}}
try {{
let {{ {spread} }} = JSON.parse(line)
let res = await main(...[ {spread} ]);
console.log("wm_res[success]:" + JSON.stringify(res ?? null, (key, value) => typeof value === 'undefined' ? null : value));
}} catch (e) {{
console.log("wm_res[error]:" + JSON.stringify({{ message: e.message, name: e.name, stack: e.stack, line: line }}));
if (exit) {{
break;
}}
}}
"#,
);
write_file(job_dir, "wrapper.mjs", &wrapper_content).await?;
write_file(job_dir, "wrapper.ts", &wrapper_content).await?;
}
if !codebase.is_some() {
build_loader(
job_dir,
base_internal_url,
token,
w_id,
script_path,
annotation.nodejs_mode,
)
.await?;
}
let _ = write_file(
&job_dir,
"loader.bun.ts",
&format!(
r#"
import {{ plugin }} from "bun";
if annotation.nodejs_mode && !codebase.is_some() {
generate_wrapper_mjs(
job_dir,
w_id,
&Uuid::nil(),
worker_name,
db,
None,
&mut mem_peak,
&mut canceled_by,
&common_bun_proc_envs,
)
.await?;
}
{}
if annotation.nodejs_mode {
let script_path = format!("{job_dir}/wrapper.mjs");
plugin(p)
"#,
RELATIVE_BUN_LOADER
.replace("W_ID", &w_id)
.replace("BASE_INTERNAL_URL", base_internal_url)
.replace("TOKEN", token)
.replace("CURRENT_PATH", script_path)
.replace("RAW_GET_ENDPOINT", "raw_unpinned")
),
)
.await?;
handle_dedicated_process(
&*NODE_PATH,
job_dir,
context_envs,
envs,
context,
common_bun_proc_envs,
vec!["--experimental-default-type=module", &script_path],
killpill_rx,
job_completed_tx,
token,
jobs_rx,
worker_name,
db,
&script_path,
"nodejs",
)
.await
} else {
handle_dedicated_process(
&*BUN_PATH,
job_dir,
context_envs,
envs,
context,
common_bun_proc_envs,
vec![
"run",
"-i",
"--prefer-offline",
"-r",
"./loader.bun.js",
&format!("{job_dir}/wrapper.mjs"),
],
killpill_rx,
job_completed_tx,
token,
jobs_rx,
worker_name,
db,
script_path,
"bun",
)
.await
}
handle_dedicated_process(
&*BUN_PATH,
job_dir,
context_envs,
envs,
context,
common_bun_proc_envs,
vec![
"run",
"-i",
"--prefer-offline",
"-r",
"./loader.bun.ts",
&format!("{job_dir}/wrapper.ts"),
],
killpill_rx,
job_completed_tx,
token,
jobs_rx,
worker_name,
db,
)
.await
}

View File

@@ -453,26 +453,6 @@ async fn get_mem_peak(pid: Option<u32>, nsjail: bool) -> i32 {
}
}
pub fn sizeof_val(v: &serde_json::Value) -> usize {
std::mem::size_of::<serde_json::Value>()
+ match v {
serde_json::Value::Null => 0,
serde_json::Value::Bool(_) => 0,
serde_json::Value::Number(_) => 4, // Incorrect if arbitrary_precision is enabled. oh well
serde_json::Value::String(s) => s.capacity(),
serde_json::Value::Array(a) => a.iter().map(sizeof_val).sum(),
serde_json::Value::Object(o) => o
.iter()
.map(|(k, v)| {
std::mem::size_of::<String>()
+ k.capacity()
+ sizeof_val(v)
+ std::mem::size_of::<usize>() * 3
})
.sum(),
}
}
pub async fn run_future_with_polling_update_job_poller<Fut, T>(
job_id: Uuid,
timeout: Option<i32>,
@@ -482,7 +462,7 @@ pub async fn run_future_with_polling_update_job_poller<Fut, T>(
result_f: Fut,
worker_name: &str,
w_id: &str,
) -> error::Result<T>
) -> anyhow::Result<T>
where
Fut: Future<Output = anyhow::Result<T>>,
{
@@ -514,22 +494,12 @@ where
tracing::error!("Query timeout: {}", e);
Error::ExecutionErr(format!("Query timeout after (>{}s)", timeout_ms/1000))
})?,
ex = update_job, if job_id != Uuid::nil() => {
match ex {
UpdateJobPollingExit::Done => Err(Error::ExecutionErr("Job cancelled".to_string())).map_err(to_anyhow)?,
UpdateJobPollingExit::AlreadyCompleted => Err(Error::AlreadyCompleted("Job already completed".to_string())).map_err(to_anyhow)?,
}
}
_ = update_job, if job_id != Uuid::nil() => Err(Error::ExecutionErr("Job cancelled".to_string())).map_err(to_anyhow)?,
}?;
drop(tx);
Ok(rows)
}
pub enum UpdateJobPollingExit {
Done,
AlreadyCompleted,
}
pub async fn update_job_poller<F, Fut>(
job_id: Uuid,
db: &DB,
@@ -539,14 +509,13 @@ pub async fn update_job_poller<F, Fut>(
worker_name: &str,
w_id: &str,
mut rx: broadcast::Receiver<()>,
) -> UpdateJobPollingExit
where
) where
F: Fn() -> Fut,
Fut: Future<Output = i32>,
{
let update_job_interval = Duration::from_millis(500);
if job_id == Uuid::nil() {
return UpdateJobPollingExit::Done;
return;
}
let db = db.clone();
@@ -604,36 +573,28 @@ where
}
}
let (canceled, canceled_by, canceled_reason, already_completed) = sqlx::query_as::<_, (bool, Option<String>, Option<String>, bool)>("UPDATE queue SET mem_peak = $1, last_ping = now() WHERE id = $2 RETURNING canceled, canceled_by, canceled_reason, false")
let (canceled, canceled_by, canceled_reason) = sqlx::query_as::<_, (bool, Option<String>, Option<String>)>("UPDATE queue SET mem_peak = $1, last_ping = now() WHERE id = $2 RETURNING canceled, canceled_by, canceled_reason")
.bind(*mem_peak)
.bind(job_id)
.fetch_optional(&db)
.await
.unwrap_or_else(|e| {
tracing::error!(%e, "error updating job {job_id}: {e}");
Some((false, None, None, false))
Some((false, None, None))
})
.unwrap_or_else(|| {
// if the job is not in queue, it can only be in the completed_job so it is already complete
(false, None, None, true)
});
if already_completed {
return UpdateJobPollingExit::AlreadyCompleted
}
.unwrap_or((false, None, None));
if canceled {
canceled_by_ref.replace(CanceledBy {
username: canceled_by.clone(),
reason: canceled_reason.clone(),
});
break
break;
}
}
},
);
}
tracing::info!("job {job_id} finished");
UpdateJobPollingExit::Done
}
pub enum CompactLogs {
@@ -916,7 +877,6 @@ pub async fn handle_child(
TooManyLogs,
Timeout,
Cancelled,
AlreadyCompleted,
}
let (timeout_duration, timeout_warn_msg) =
@@ -934,10 +894,7 @@ pub async fn handle_child(
result = child.wait() => return result.map(Ok),
Ok(()) = too_many_logs.changed() => KillReason::TooManyLogs,
_ = sleep(timeout_duration) => KillReason::Timeout,
ex = update_job, if job_id != Uuid::nil() => match ex {
UpdateJobPollingExit::Done => KillReason::Cancelled,
UpdateJobPollingExit::AlreadyCompleted => KillReason::AlreadyCompleted,
},
_ = update_job, if job_id != Uuid::nil() => KillReason::Cancelled,
};
tx.send(()).expect("rx should never be dropped");
drop(tx);
@@ -1183,7 +1140,7 @@ pub async fn resolve_job_timeout(
custom_timeout_secs: Option<i32>,
) -> (Duration, Option<String>) {
let mut warn_msg: Option<String> = None;
#[cfg(feature = "cloud")]
#[cfg(feature = "enterprise")]
let cloud_premium_workspace = *CLOUD_HOSTED
&& sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", _w_id)
.fetch_one(_db)
@@ -1192,7 +1149,7 @@ pub async fn resolve_job_timeout(
tracing::error!(%e, "error getting premium workspace for job {_job_id}: {e}");
})
.unwrap_or(false);
#[cfg(not(feature = "cloud"))]
#[cfg(not(feature = "enterprise"))]
let cloud_premium_workspace = false;
// compute global max timeout

View File

@@ -58,8 +58,6 @@ pub async fn handle_dedicated_process(
mut jobs_rx: Receiver<Arc<QueuedJob>>,
worker_name: &str,
db: &DB,
script_path: &str,
mode: &str,
) -> std::result::Result<(), error::Error> {
//do not cache local dependencies
let mut child = {
@@ -116,7 +114,7 @@ pub async fn handle_dedicated_process(
// let mut j = 0;
let mut alive = true;
let init_log = format!("dedicated worker {mode}: {worker_name}\n\n");
let init_log = format!("dedicated worker: {worker_name}\n\n");
let mut logs = init_log.clone();
loop {
tokio::select! {
@@ -127,11 +125,9 @@ pub async fn handle_dedicated_process(
if let Err(e) = write_stdin(&mut stdin, "end").await {
tracing::info!("Could not write end message to stdin: {e:?}")
}
stdin.flush().await.context("stdin flush")?;
},
line = err_reader.next_line() => {
if let Some(line) = line.expect("line is ok") {
tracing::debug!("stderr dedicated worker: {line}");
logs.push_str("[stderr] ");
logs.push_str(&line);
logs.push_str("\n");
@@ -148,10 +144,9 @@ pub async fn handle_dedicated_process(
tracing::info!("dedicated worker process started");
continue;
}
tracing::debug!("processed job: |{line}|");
tracing::debug!("processed job: {line}");
if line.starts_with("wm_res[") {
let job: Arc<QueuedJob> = jobs.pop_front().expect("pop");
tracing::info!("job completed on dedicated worker {script_path}: {}", job.id);
match serde_json::from_str::<Box<serde_json::value::RawValue>>(&line.replace("wm_res[success]:", "").replace("wm_res[error]:", "")) {
Ok(result) => {
append_logs(job.id, job.workspace_id.clone(), logs.clone(), db).await;
@@ -179,9 +174,8 @@ pub async fn handle_dedicated_process(
job = conditional_polling(jobs_rx.recv(), alive && jobs.len() < MAX_BUFFERED_DEDICATED_JOBS) => {
// i += 1;
if let Some(job) = job {
tracing::debug!("received job");
jobs.push_back(job.clone());
tracing::info!("received job and adding to queue on dedicated worker for {script_path}: {} (queue_size: {})", job.id, jobs.len());
// write_stdin(&mut stdin, &serde_json::to_string(&job.args.unwrap_or_else(|| serde_json::json!({"x": job.id}))).expect("serialize")).await?;
write_stdin(&mut stdin, &serde_json::to_string(&job.args).expect("serialize")).await?;
stdin.flush().await.context("stdin flush")?;

View File

@@ -527,8 +527,6 @@ for await (const chunk of Deno.stdin.readable) {{
jobs_rx,
worker_name,
db,
script_path,
"deno",
)
.await
}

View File

@@ -73,19 +73,47 @@ pub async fn build_tar_and_push(
#[cfg(all(feature = "enterprise", feature = "parquet"))]
pub async fn pull_from_tar(client: Arc<dyn ObjectStore>, folder: String) -> error::Result<()> {
use windmill_common::s3_helpers::attempt_fetch_bytes;
use object_store::path::Path;
let folder_name = folder.split("/").last().unwrap();
tracing::info!("Attempting to pull piptar {folder_name} from bucket");
let start = Instant::now();
let tar_path = format!("tar/pip/{folder_name}.tar");
let bytes = attempt_fetch_bytes(client, &tar_path).await?;
let object = client.get(&Path::from(tar_path.clone())).await;
if let Err(e) = object {
tracing::info!("Failed to pull tar from s3: {tar_path}. Error: {:?}", e);
return Err(error::Error::ExecutionErr(format!(
"Failed to pull tar from s3: {tar_path}"
)));
}
let bytes = object.unwrap().bytes().await;
if bytes.is_err() {
tracing::info!(
"Failed to read tar from s3: {tar_path}. Error: {:?}",
bytes.err()
);
return Err(error::Error::ExecutionErr(format!(
"Failed to read tar from s3: {tar_path}"
)));
}
let bytes = bytes.unwrap();
tracing::info!("{tar_path} len: {}", bytes.len());
if bytes.len() == 0 {
tracing::info!(
"piptar {folder_name} not found in bucket. Took {:?}ms",
start.elapsed().as_millis()
);
return Err(error::Error::ExecutionErr(format!(
"tar {folder_name} does not exist in bucket"
)));
}
// tracing::info!("B: {target} {folder}");
extract_tar(bytes, &folder).await.map_err(|e| {
extract_pip_tar(bytes, &folder).await.map_err(|e| {
tracing::error!("Failed to extract piptar {folder_name}. Error: {:?}", e);
e
})?;
@@ -99,7 +127,7 @@ pub async fn pull_from_tar(client: Arc<dyn ObjectStore>, folder: String) -> erro
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
pub async fn extract_tar(tar: bytes::Bytes, folder: &str) -> error::Result<()> {
pub async fn extract_pip_tar(tar: bytes::Bytes, folder: &str) -> error::Result<()> {
use bytes::Buf;
use tokio::fs::{self};
@@ -109,14 +137,14 @@ pub async fn extract_tar(tar: bytes::Bytes, folder: &str) -> error::Result<()> {
let mut ar = tar::Archive::new(tar.reader());
if let Err(e) = ar.unpack(folder) {
tracing::info!("Failed to untar to {folder}. Error: {:?}", e);
tracing::info!("Failed to untar piptar. Error: {:?}", e);
fs::remove_dir_all(&folder).await?;
return Err(error::Error::ExecutionErr(format!(
"Failed to untar piptar {folder}"
)));
}
tracing::info!(
"Finished extracting tar to {folder}. Took {}ms",
"Finished extracting pip tar {folder}. Took {}ms",
start.elapsed().as_millis(),
);
Ok(())

View File

@@ -33,111 +33,6 @@ lazy_static::lazy_static! {
static ref GO_PATH: String = std::env::var("GO_PATH").unwrap_or_else(|_| "/usr/bin/go".to_string());
}
pub async fn save_cache(
bin_path: &str,
job_dir: &str,
_hash: &str,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
) -> windmill_common::error::Result<()> {
let job_main_path = format!("{job_dir}/main");
let mut _cached_to_s3 = false;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
use object_store::path::Path;
let hash_path = hash_to_os_path(_hash);
if let Err(e) = os
.put(
&Path::from(hash_path.clone()),
bytes::Bytes::from(std::fs::read(&job_main_path)?),
)
.await
{
tracing::error!(
"Failed to put go bin to object store: {hash_path}. Error: {:?}",
e
);
} else {
_cached_to_s3 = true;
}
}
if !*CLOUD_HOSTED {
tokio::fs::copy(&job_main_path, bin_path).await?;
append_logs(
job.id.clone(),
job.workspace_id.to_string(),
format!(
"\nwrite cached binary: {} (backed by object store: {_cached_to_s3})\n",
bin_path
),
db,
)
.await;
} else if _cached_to_s3 {
append_logs(
job.id.clone(),
job.workspace_id.to_string(),
format!("write cached binary to object store {}\n", bin_path),
db,
)
.await;
}
Ok(())
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
async fn write_binary_file(main_path: &str, byts: &mut bytes::Bytes) -> error::Result<()> {
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
use tokio::io::AsyncWriteExt;
let mut file = File::create(main_path).await?;
file.write_buf(byts).await?;
file.set_permissions(Permissions::from_mode(0o755)).await?;
file.flush().await?;
Ok(())
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
fn hash_to_os_path(hash: &str) -> String {
format!("gobin/{hash}")
}
async fn load_cache(bin_path: &str, _hash: &str) -> (bool, String) {
if tokio::fs::metadata(&bin_path).await.is_ok() {
(true, format!("loaded bin from local cache: {}\n", bin_path))
} else {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
use windmill_common::s3_helpers::attempt_fetch_bytes;
if let Ok(mut x) = attempt_fetch_bytes(os, &hash_to_os_path(_hash)).await {
if let Err(e) = write_binary_file(bin_path, &mut x).await {
tracing::error!("could not write binary file: {e:?}");
return (
false,
"error writing binary file from object store".to_string(),
);
}
tracing::info!("loaded bin from object store {}", bin_path);
return (true, format!("loaded bin from object store {}", bin_path));
}
}
(false, "".to_string())
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_go_job(
mem_peak: &mut i32,
@@ -155,19 +50,21 @@ pub async fn handle_go_job(
) -> Result<Box<RawValue>, Error> {
//go does not like executing modules at temp root
let job_dir = &format!("{job_dir}/go");
let hash = calculate_hash(&format!(
"{}{}v2",
inner_content,
requirements_o
.as_ref()
.map(|x| x.to_string())
.unwrap_or_default()
));
let bin_path = format!("{}/{hash}", GO_BIN_CACHE_DIR,);
let bin_path = format!(
"{}/{}",
GO_BIN_CACHE_DIR,
calculate_hash(&format!(
"{}{}",
inner_content,
requirements_o
.as_ref()
.map(|x| x.to_string())
.unwrap_or_default()
))
);
let bin_exists = !*CLOUD_HOSTED && tokio::fs::metadata(&bin_path).await.is_ok();
let (cache, cache_logs) = load_cache(&bin_path, &hash).await;
let (skip_go_mod, skip_tidy) = if cache {
let (skip_go_mod, skip_tidy) = if bin_exists {
create_dir(job_dir).await?;
(true, true)
} else if let Some(requirements) = requirements_o {
@@ -176,8 +73,8 @@ pub async fn handle_go_job(
(false, false)
};
let cache_logs = if !cache {
let logs1 = format!("{cache_logs}\n\n--- GO DEPENDENCIES SETUP ---\n");
if !bin_exists {
let logs1 = "\n\n--- GO DEPENDENCIES SETUP ---\n".to_string();
append_logs(job.id.clone(), job.workspace_id.to_string(), logs1, db).await;
install_go_dependencies(
@@ -195,6 +92,9 @@ pub async fn handle_go_job(
)
.await?;
let logs2 = "\n\n--- GO CODE EXECUTION ---\n".to_string();
append_logs(job.id.clone(), job.workspace_id.to_string(), logs2, db).await;
create_args_and_out_file(client, job, job_dir, db).await?;
{
let sig = windmill_parser_go::parse_go_sig(&inner_content)?;
@@ -309,24 +209,33 @@ func Run(req Req) (interface{{}}, error){{
)
.await?;
if let Err(e) = save_cache(&bin_path, &job_dir, &hash, &job, db).await {
tracing::error!("could not save {bin_path} to go cache: {e:?}");
if !*CLOUD_HOSTED {
create_dir(&bin_path).await?;
let target = format!("{bin_path}/main");
tokio::fs::copy(format!("{job_dir}/main"), &target).await?;
append_logs(
job.id.clone(),
job.workspace_id.to_string(),
format!("write cached binary: {}\n", bin_path),
db,
)
.await;
}
"".to_string()
} else {
let path = format!("{bin_path}/main");
let mut logs2 = "".to_string();
logs2.push_str(&format!("found cached binary: {path}\n"));
let target = format!("{job_dir}/main");
tokio::fs::symlink(&bin_path, &target).await.map_err(|e| {
tokio::fs::symlink(&path, &target).await.map_err(|e| {
Error::ExecutionErr(format!(
"could not copy cached binary from {bin_path} to {job_dir}/main: {e:?}"
"could not copy cached binary from {path} to {job_dir}/main: {e:?}"
))
})?;
logs2.push_str("\n\n--- GO CODE EXECUTION ---\n");
append_logs(job.id.clone(), job.workspace_id.to_string(), logs2, db).await;
create_args_and_out_file(client, job, job_dir, db).await?;
cache_logs
};
let logs2 = format!("{cache_logs}\n\n--- GO CODE EXECUTION ---\n");
append_logs(job.id.clone(), job.workspace_id.to_string(), logs2, db).await;
}
let client = &client.get_authed().await;
@@ -394,13 +303,6 @@ func Run(req Req) (interface{{}}, error){{
false,
)
.await?;
if cache && *CLOUD_HOSTED {
//do not keep the binary in the cache if it is cloud hosted to avoid filling up the disk
if let Err(e) = tokio::fs::remove_file(&bin_path).await {
tracing::error!("could not remove {bin_path} from go cache: {e:?}");
}
}
read_result(job_dir).await
}

View File

@@ -139,18 +139,14 @@ pub async fn eval_timeout(
}
}
let p_ids = by_id.as_ref().map(|x| {
[
format!("results.{}", x.previous_id),
format!("results?.{}", x.previous_id),
format!("results[\"{}\"]", x.previous_id),
format!("results?.[\"{}\"]", x.previous_id),
]
});
let p_id = by_id.as_ref().map(|x| format!("results.{}", x.previous_id));
let p_id2 = by_id
.as_ref()
.map(|x| format!("results[\"{}\"]", x.previous_id));
if p_ids.is_some()
if p_id.is_some()
&& transform_context.contains_key("previous_result")
&& p_ids.as_ref().unwrap().iter().any(|x| x == &expr)
&& &expr == p_id.as_ref().unwrap()
{
// tracing::error!("PREVIOUS_RESULT");
return Ok(transform_context
@@ -160,6 +156,17 @@ pub async fn eval_timeout(
.clone());
}
if p_id2.is_some()
&& transform_context.contains_key("previous_result")
&& &expr == p_id2.as_ref().unwrap()
{
return Ok(transform_context
.get("previous_result")
.unwrap()
.as_ref()
.clone());
}
if by_id.is_some() && authed_client.is_some() {
if let Some(x) = RE_FULL
.captures(&expr)
@@ -220,10 +227,10 @@ pub async fn eval_timeout(
.collect_vec();
if !context_keys.contains(&"previous_result".to_string())
&& (p_ids.is_some() && p_ids.as_ref().unwrap().iter().any(|x| expr.contains(x)))
&& (p_id.is_some() && expr.contains(p_id.as_ref().unwrap()))
|| expr.contains("error")
|| (p_id2.is_some() && expr.contains(p_id2.as_ref().unwrap()))
{
// tracing::error!("PREVIOUS_RESULT");
context_keys.push("previous_result".to_string());
}
let has_flow_input = expr.contains("flow_input");
@@ -767,7 +774,6 @@ fn op_get_static_args(op_state: Rc<RefCell<OpState>>) -> Vec<Option<String>> {
#[op2(fast)]
fn op_log(op_state: Rc<RefCell<OpState>>, #[string] log: &str) {
// tracing::error!("log: |{}|", log);
op_state
.borrow_mut()
.borrow_mut::<LogString>()

View File

@@ -1,7 +1,6 @@
use base64::{engine::general_purpose, Engine as _};
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use futures::TryFutureExt;
use regex::Regex;
use serde::Deserialize;
use serde_json::value::RawValue;
use serde_json::{Map, Value};
@@ -13,7 +12,7 @@ use windmill_common::error::{self, Error};
use windmill_common::worker::to_raw_value;
use windmill_common::{error::to_anyhow, jobs::QueuedJob};
use windmill_parser_sql::{parse_db_resource, parse_mssql_sig};
use windmill_queue::{append_logs, CanceledBy};
use windmill_queue::CanceledBy;
use crate::common::{build_args_values, run_future_with_polling_update_job_poller};
use crate::AuthedClientBackgroundTask;
@@ -28,10 +27,6 @@ struct MssqlDatabase {
instance_name: Option<String>,
}
lazy_static::lazy_static! {
static ref RE_MSSQL_READONLY_INTENT: Regex = Regex::new(r#"(?mi)^-- ApplicationIntent=ReadOnly *(?:\r|\n|$)"#).unwrap();
}
pub async fn do_mssql(
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
@@ -79,14 +74,6 @@ pub async fn do_mssql(
config.port(port);
}
let readonly_intent = RE_MSSQL_READONLY_INTENT.is_match(query);
config.readonly(readonly_intent);
if readonly_intent {
let logs = format!("\nSetting ApplicationIntent to ReadOnly");
append_logs(job.id, job.workspace_id.clone(), logs, db).await;
}
// Using SQL Server authentication.
config.authentication(AuthMethod::sql_server(database.user, database.password));
config.trust_cert(); // on production, it is not a good idea to do this

View File

@@ -31,8 +31,8 @@ use windmill_parser::Typ;
use windmill_parser_sql::{parse_db_resource, parse_pgsql_sig};
use windmill_queue::CanceledBy;
use crate::common::{build_args_values, run_future_with_polling_update_job_poller, sizeof_val};
use crate::{AuthedClientBackgroundTask, MAX_RESULT_SIZE};
use crate::common::{build_args_values, run_future_with_polling_update_job_poller};
use crate::AuthedClientBackgroundTask;
use bytes::{Buf, BytesMut};
use lazy_static::lazy_static;
use urlencoding::encode;
@@ -230,32 +230,15 @@ pub async fn do_postgresql(
.unwrap_or_default(),
);
let mut siz = 0;
let mut res: Vec<serde_json::Value> = vec![];
for row in rows.into_iter() {
let r = postgres_row_to_json_value(row);
if let Ok(v) = r.as_ref() {
let size = sizeof_val(v);
siz += size;
}
if *CLOUD_HOSTED && siz > MAX_RESULT_SIZE * 4 {
return Err(anyhow::anyhow!(
"Query result too large for cloud (size = {} > {})",
siz,
MAX_RESULT_SIZE & 4
));
}
if let Ok(v) = r {
res.push(v);
} else {
return Err(to_anyhow(r.err().unwrap()));
}
}
let result = rows
.into_iter()
.map(|x: Row| postgres_row_to_json_value(x))
.collect::<Result<Vec<_>, _>>()?;
Ok((res, siz))
Ok(result)
};
let (result, size) = run_future_with_polling_update_job_poller(
let result = run_future_with_polling_update_job_poller(
job.id,
job.timeout,
db,
@@ -267,8 +250,6 @@ pub async fn do_postgresql(
)
.await?;
*mem_peak = size as i32;
RUNNING.store(false, std::sync::atomic::Ordering::Relaxed);
if let Some(handle) = handle {
@@ -342,7 +323,6 @@ enum PgType {
Timestamp(chrono::NaiveDateTime),
None(Option<bool>),
Array(Vec<PgType>),
Json(serde_json::Value),
}
impl ToSql for PgType {
@@ -368,7 +348,6 @@ impl ToSql for PgType {
PgType::Timestamp(ref val) => val.to_sql(ty, out),
PgType::None(ref val) => val.to_sql(ty, out),
PgType::Array(ref val) => val.to_sql(ty, out),
PgType::Json(ref val) => val.to_sql(ty, out),
}
}
@@ -431,7 +410,7 @@ fn convert_val(value: &Value, arg_t: &String, typ: &Typ) -> windmill_common::err
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%3fZ").unwrap_or_default();
Ok(PgType::Date(date))
}
Value::String(s) if arg_t == "time" || arg_t == "timetz" => {
Value::String(s) if arg_t == "time" => {
let time =
chrono::NaiveTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%3fZ").unwrap_or_default();
Ok(PgType::Time(time))
@@ -441,7 +420,6 @@ fn convert_val(value: &Value, arg_t: &String, typ: &Typ) -> windmill_common::err
.unwrap_or_default();
Ok(PgType::Timestamp(datetime))
}
Value::Object(_) => Ok(PgType::Json(value.clone())),
Value::String(s) => Ok(PgType::String(s.clone())),
_ => Err(Error::ExecutionErr(format!(
"Unsupported type in query: {:?} and signature {arg_t:?}",

View File

@@ -391,6 +391,8 @@ mount {{
}
tracing::info!(
worker_name = %worker_name,
job_id = %job.id,
workspace_id = %job.workspace_id,
"started python code execution {}",
job.id
@@ -472,13 +474,7 @@ async fn prepare_wrapper(
let relative_imports = RELATIVE_IMPORT_REGEX.is_match(&inner_content);
let script_path_splitted = script_path.split("/").map(|x| {
if x.starts_with(|x: char| x.is_ascii_digit()) {
format!("_{}", x)
} else {
x.to_string()
}
});
let script_path_splitted = script_path.split("/");
let dirs_full = script_path_splitted
.clone()
.take(script_path_splitted.clone().count() - 1)
@@ -503,6 +499,12 @@ async fn prepare_wrapper(
let module_dir = format!("{}/{}", job_dir, dirs);
tokio::fs::create_dir_all(format!("{module_dir}/")).await?;
let last = if last.starts_with(|x: char| x.is_ascii_digit()) {
format!("_{}", last)
} else {
last
};
let _ = write_file(&module_dir, &format!("{last}.py"), inner_content).await?;
if relative_imports {
let _ = write_file(job_dir, "loader.py", RELATIVE_PYTHON_LOADER).await?;
@@ -612,7 +614,7 @@ async fn replace_pip_secret(
}
let secret = get_secret_value_as_admin(db, w_id, variable).await?;
tracing::info!(
worker = %worker_name,
worker_name = %worker_name,
job_id = %job_id,
workspace_id = %w_id,
"found secret variable in pip requirements: {}",
@@ -876,12 +878,16 @@ pub async fn handle_python_reqs(
append_logs(job_id.clone(), w_id.to_string(), logs1, db).await;
tracing::info!(
worker_name = %worker_name,
job_id = %job_id,
workspace_id = %w_id,
"started setup python dependencies"
);
let child = if !*DISABLE_NSJAIL {
tracing::info!(
worker_name = %worker_name,
job_id = %job_id,
workspace_id = %w_id,
"starting nsjail"
);
@@ -986,6 +992,8 @@ pub async fn handle_python_reqs(
)
.await;
tracing::info!(
worker_name = %worker_name,
job_id = %job_id,
workspace_id = %w_id,
is_ok = child.is_ok(),
"finished setting up python dependencies {}",
@@ -1178,10 +1186,6 @@ for line in sys.stdin:
proc_envs.insert("PYTHONPATH".to_string(), additional_python_paths_folders);
proc_envs.insert("PATH".to_string(), PATH_ENV.to_string());
proc_envs.insert("TZ".to_string(), TZ_ENV.to_string());
proc_envs.insert(
"BASE_INTERNAL_URL".to_string(),
base_internal_url.to_string(),
);
proc_envs.insert("BASE_URL".to_string(), base_internal_url.to_string());
handle_dedicated_process(
&*PYTHON_PATH,
@@ -1197,8 +1201,6 @@ for line in sys.stdin:
jobs_rx,
worker_name,
db,
script_path,
"python",
)
.await
}

View File

@@ -34,7 +34,7 @@ globalThis.URLSearchParams = url.URLSearchParams;
globalThis.Headers = headers.Headers;
globalThis.FileReader = fileReader.FileReader;
globalThis.console = new console.Console((msg, level) =>
globalThis.Deno.core.ops.op_log(msg)
globalThis.Deno.core.ops.op_log([msg])
);
// Object.assign(globalThis, {
// console: nonEnumerable(

View File

@@ -13,7 +13,6 @@ use prometheus::{
core::{AtomicI64, GenericGauge},
IntCounter,
};
use tracing::Instrument;
#[cfg(feature = "prometheus")]
use windmill_common::METRICS_DEBUG_ENABLED;
#[cfg(feature = "prometheus")]
@@ -41,7 +40,7 @@ use windmill_common::{
flows::{FlowModule, FlowModuleValue, FlowValue},
get_latest_deployed_hash_for_path,
jobs::{JobKind, JobPayload, QueuedJob},
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang, PREVIEW_IS_CODEBASE_HASH},
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL, SUPERADMIN_SYNC_EMAIL},
utils::{rd_string, StripPath},
worker::{
@@ -89,8 +88,8 @@ use crate::{
bash_executor::{handle_bash_job, handle_powershell_job, ANSI_ESCAPE_RE},
bun_executor::{gen_lockfile, get_trusted_deps, handle_bun_job},
common::{
build_args_map, get_cached_resource_value_if_valid, get_reserved_variables, hash_args,
read_result, save_in_cache, write_file, NO_LOGS_AT_ALL, SLOW_LOGS,
build_args_map, get_cached_resource_value_if_valid, hash_args, read_result, save_in_cache,
write_file, NO_LOGS_AT_ALL, SLOW_LOGS,
},
deno_executor::{generate_deno_lock, handle_deno_job},
go_executor::{handle_go_job, install_go_dependencies},
@@ -198,7 +197,6 @@ pub const DENO_CACHE_DIR_NPM: &str = concatcp!(ROOT_CACHE_DIR, "deno/npm");
pub const GO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "go");
pub const BUN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "bun");
pub const HUB_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "hub");
pub const GO_BIN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "gobin");
pub const POWERSHELL_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "powershell");
@@ -586,7 +584,6 @@ impl JobCompletedSender {
}
}
#[tracing::instrument(name = "worker", level = "info", skip_all, fields(worker = %worker_name))]
pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
db: &Pool<Postgres>,
worker_instance: &str,
@@ -611,7 +608,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let start_time = Instant::now();
let worker_dir = format!("{TMP_DIR}/{worker_name}");
tracing::debug!(worker_dir = %worker_dir, "Creating worker dir");
tracing::debug!(worker_dir = %worker_dir, worker_name = %worker_name, "Creating worker dir");
if let Some(ref netrc) = *NETRC {
tracing::info!("Writing netrc at {}/.netrc", HOME_ENV.as_str());
@@ -1025,7 +1022,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let worker_name2 = worker_name.clone();
let killpill_tx2 = killpill_tx.clone();
let job_completed_sender = job_completed_tx.0.clone();
let send_result = tokio::spawn((async move {
let send_result = tokio::spawn(async move {
while let Some(sr) = job_completed_rx.recv().await {
match sr {
SendResult::JobCompleted(jc) => {
@@ -1039,7 +1036,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let same_worker_tx2 = same_worker_tx2.clone();
let rsmq2 = rsmq2.clone();
let worker_name = worker_name2.clone();
if matches!(jc.job.job_kind, JobKind::Noop) {
if matches!(jc.job.job_kind, JobKind::Noop) || is_dedicated_worker {
thread_count.fetch_add(1, Ordering::SeqCst);
let thread_count = thread_count.clone();
@@ -1131,19 +1128,15 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
.await
.expect("update config to trigger restart of all dedicated workers at that config");
killpill_tx.send(()).unwrap_or_default();
}
});
} else {
let is_init_script_and_failure =
!jc.success && jc.job.tag.as_str() == INIT_SCRIPT_TAG;
let is_dependency_job = matches!(
jc.job.job_kind,
JobKind::Dependencies | JobKind::FlowDependencies);
handle_receive_completed_job(
jc,
base_internal_url2,
db2.clone(),
db2,
worker_dir2,
same_worker_tx2,
rsmq2,
@@ -1157,15 +1150,6 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
tracing::error!("init script errored, exiting");
killpill_tx2.send(()).unwrap_or_default();
}
if is_dependency_job && is_dedicated_worker {
tracing::error!("Dedicated worker executed a dependency job, a new script has been deployed. Exiting expecting to be restarted.");
sqlx::query!("UPDATE config SET config = config WHERE name = $1", format!("worker__{}", *WORKER_GROUP))
.execute(&db2)
.await
.expect("update config to trigger restart of all dedicated workers at that config");
killpill_tx2.send(()).unwrap_or_default();
}
}
}
SendResult::UpdateFlow {
@@ -1216,7 +1200,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
tokio::time::sleep(Duration::from_millis(50)).await;
}
tracing::info!("finished processing all completed jobs");
}).instrument(tracing::Span::current()));
});
let mut last_executed_job: Option<Instant> = None;
let mut last_checked_suspended = Instant::now();
@@ -1230,11 +1214,7 @@ 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!(
"listening for jobs, WORKER_GROUP: {}, config: {:?}",
*WORKER_GROUP,
WORKER_CONFIG.read().await
);
tracing::info!(worker = %worker_name, "listening for jobs, WORKER_GROUP: {}, config: {:?}", *WORKER_GROUP, WORKER_CONFIG.read().await);
// (dedi_path, dedicated_worker_tx, dedicated_worker_handle)
// Option<Sender<Arc<QueuedJob>>>,
@@ -1398,27 +1378,24 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
tracing::error!("failed to update worker ping, exiting: {}", e);
killpill_tx.send(()).unwrap_or_default();
}
tracing::info!("updating last ping");
tracing::debug!("set last ping");
last_ping = Instant::now();
}
if (jobs_executed as u32 + vacuum_shift) % VACUUM_PERIOD == 0 {
let db2 = db.clone();
let current_span = tracing::Span::current();
tokio::task::spawn(
(async move {
tracing::info!("vacuuming queue and completed_job");
if let Err(e) = sqlx::query!("VACUUM (skip_locked) queue")
.execute(&db2)
.await
{
tracing::error!("failed to vacuum queue: {}", e);
}
tracing::info!("vacuumed queue and completed_job");
})
.instrument(current_span),
);
let worker_name2 = worker_name.clone();
tokio::task::spawn(async move {
tracing::info!(worker = %worker_name2, "vacuuming queue and completed_job");
if let Err(e) = sqlx::query!("VACUUM (skip_locked) queue")
.execute(&db2)
.await
{
tracing::error!(worker = %worker_name2, "failed to vacuum queue: {}", e);
}
tracing::info!(worker = %worker_name2, "vacuumed queue and completed_job");
});
jobs_executed += 1;
}
@@ -1522,7 +1499,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
last_executed_job = None;
jobs_executed += 1;
tracing::debug!("started handling of job {}", job.id);
tracing::debug!(worker = %worker_name, "started handling of job {}", job.id);
if matches!(job.job_kind, JobKind::Script | JobKind::Preview) {
if !dedicated_workers.is_empty() {
@@ -1634,12 +1611,10 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
.unwrap_or_else(|| "none".to_string());
if job.id == Uuid::nil() {
tracing::info!("running warmup job");
tracing::info!(worker = %worker_name, "running warmup job");
} else {
tracing::info!(workspace_id = %job.workspace_id, job_id = %job.id, root_id = %job_root, "fetched job {}, root job: {}", job.id, job_root);
} // Here we can't remove the job id, but maybe with the
// fields macro we can make a job id that only appears when
// the job is defined?
tracing::info!(worker = %worker_name, workspace_id = %job.workspace_id, id = %job.id, root_id = %job_root, "fetched job {}, root job: {}", job.id, job_root);
}
let job_dir = format!("{worker_dir}/{}", job.id);
@@ -1685,7 +1660,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let tag = job.tag.clone();
let arc_job = Arc::new(job);
if let Err(err) = handle_queued_job(
if let Some(err) = handle_queued_job(
arc_job.clone(),
db,
&authed_client,
@@ -1700,6 +1675,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
worker_code_execution_duration.clone(),
)
.await
.err()
{
let is_init_script = arc_job.tag.as_str() == INIT_SCRIPT_TAG;
handle_job_error(
@@ -1763,7 +1739,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
if let Some(secs) = *EXIT_AFTER_NO_JOB_FOR_SECS {
if let Some(lj) = last_executed_job {
if lj.elapsed().as_secs() > secs {
tracing::info!("no job for {} seconds, exiting", secs);
tracing::info!(worker = %worker_name, "no job for {} seconds, exiting", secs);
break;
}
} else {
@@ -1792,7 +1768,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
});
}
Err(err) => {
tracing::error!("Failed to pull jobs: {}", err);
tracing::error!(worker = %worker_name, "Failed to pull jobs: {}", err);
}
};
}
@@ -1821,6 +1797,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
send_result.await.expect("send result failed");
tracing::info!("worker {} exited", worker_name);
println!("worker {} exited", worker_name);
}
type DedicatedWorker = (String, Sender<Arc<QueuedJob>>, Option<JoinHandle<()>>);
@@ -2039,21 +2016,18 @@ async fn spawn_dedicated_worker(
SpawnWorker::RawScript { path, .. } => path.to_string(),
SpawnWorker::Script { path, .. } => path.to_string(),
};
let path2 = path.clone();
let w_id = w_id.to_string();
let (content, lock, language, envs, codebase) = match sw {
let (content, lock, language, envs) = match sw {
SpawnWorker::Script { path, hash } => {
let q = if let Some(hash) = hash {
get_script_content_by_hash(&hash, &w_id, &db).await.map(
|r: ContentReqLangEnvs| {
Some((r.content, r.lockfile, r.language, r.envs, r.codebase))
},
|r: ContentReqLangEnvs| Some((r.content, r.lockfile, r.language, r.envs)),
)
} else {
sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>, Option<Vec<String>>, bool, Option<ScriptHash>)>(
"SELECT content, lock, language, envs, codebase IS NOT NULL, hash FROM script WHERE path = $1 AND workspace_id = $2 AND
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)",
)
@@ -2062,7 +2036,6 @@ async fn spawn_dedicated_worker(
.fetch_optional(&db)
.await
.map_err(|e| Error::InternalErr(format!("expected content and lock: {e}")))
.map(|x| x.map(|y| (y.0, y.1, y.2, y.3, if y.4 { y.5.map(|z| z.to_string()) } else { None })))
};
if let Ok(q) = q {
if let Some(wp) = q {
@@ -2081,9 +2054,7 @@ async fn spawn_dedicated_worker(
return None;
}
}
SpawnWorker::RawScript { content, lock, lang, .. } => {
(content, lock, Some(lang), None, None)
}
SpawnWorker::RawScript { content, lock, lang, .. } => (content, lock, Some(lang), None),
};
match language {
@@ -2138,7 +2109,6 @@ async fn spawn_dedicated_worker(
Some(ScriptLang::Bun) => {
crate::bun_executor::start_worker(
lock,
codebase,
&db,
&content,
&base_internal_url,
@@ -2175,9 +2145,6 @@ async fn spawn_dedicated_worker(
} {
tracing::error!("error in dedicated worker: {:?}", e);
};
if let Err(e) = killpill_tx.clone().send(()) {
tracing::error!("failed to send final killpill to dedicated worker: {:?}", e);
}
});
return Some((node_id.unwrap_or(path2), dedicated_worker_tx, Some(handle)));
// (Some(dedi_path), Some(dedicated_worker_tx), Some(handle))
@@ -2197,7 +2164,6 @@ async fn queue_init_bash_maybe<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
tx,
"admins",
windmill_common::jobs::JobPayload::Code(windmill_common::jobs::RawCode {
hash: None,
content: content.clone(),
path: Some(format!("init_script_{worker_name}")),
language: ScriptLang::Bash,
@@ -2252,7 +2218,6 @@ async fn queue_init_bash_maybe<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
// logs: String,
// ) -> error::Result<()> {
#[tracing::instrument(name = "completed_job", level = "info", skip_all, fields(job_id = %job.id))]
pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
JobCompleted { job, result, mem_peak, success, cached_res_path, canceled_by, .. }: JobCompleted,
client: &AuthedClient,
@@ -2394,8 +2359,6 @@ pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync +
// *barrier = None;
// tracing::debug!("leader worker done waiting for");
// }
#[tracing::instrument(name = "job_error", level = "info", skip_all, fields(job_id = %job.id))]
pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
db: &Pool<Postgres>,
client: &AuthedClient,
@@ -2591,7 +2554,7 @@ pub struct PreviousResult<'a> {
pub previous_result: Option<&'a RawValue>,
}
#[tracing::instrument(name = "job", level = "info", skip_all, fields(job_id = %job.id))]
#[tracing::instrument(level = "trace", skip_all)]
async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
job: Arc<QueuedJob>,
db: &DB,
@@ -2768,6 +2731,8 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
}
tracing::debug!(
worker = %worker_name,
job_id = %job.id,
workspace_id = %job.workspace_id,
"handling job {}",
job.id
@@ -2852,14 +2817,6 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
if job.as_ref().workspace_id == "" {
return Ok(());
}
if result
.as_ref()
.is_err_and(|err| matches!(err, &Error::AlreadyCompleted(_)))
{
return Ok(());
}
process_result(
job,
result,
@@ -3004,7 +2961,6 @@ struct ContentReqLangEnvs {
lockfile: Option<String>,
language: Option<ScriptLang>,
envs: Option<Vec<String>>,
codebase: Option<String>,
}
async fn get_hub_script_content_and_requirements(
@@ -3041,7 +2997,6 @@ async fn get_hub_script_content_and_requirements(
lockfile: script.lockfile,
language: Some(script.language),
envs: None,
codebase: None,
})
}
@@ -3074,27 +3029,16 @@ async fn get_script_content_by_hash(
Option<String>,
Option<ScriptLang>,
Option<Vec<String>>,
bool,
),
>(
"SELECT content, lock, language, envs, codebase IS NOT NULL FROM script WHERE hash = $1 AND workspace_id = $2",
"SELECT content, lock, language, envs FROM script WHERE hash = $1 AND workspace_id = $2",
)
.bind(script_hash.0)
.bind(w_id)
.fetch_optional(db)
.await?
.ok_or_else(|| Error::InternalErr(format!("expected content and lock")))?;
Ok(ContentReqLangEnvs {
content: r.0,
lockfile: r.1,
language: r.2,
envs: r.3,
codebase: if r.4 {
Some(script_hash.to_string())
} else {
None
},
})
Ok(ContentReqLangEnvs { content: r.0, lockfile: r.1, language: r.2, envs: r.3 })
}
#[tracing::instrument(level = "trace", skip_all)]
@@ -3110,48 +3054,35 @@ async fn handle_code_execution_job(
worker_name: &str,
column_order: &mut Option<Vec<String>>,
) -> error::Result<Box<RawValue>> {
let ContentReqLangEnvs {
content: inner_content,
lockfile: requirements_o,
language,
envs,
codebase,
} = match job.job_kind {
JobKind::Preview => ContentReqLangEnvs {
content: job
.raw_code
.clone()
.unwrap_or_else(|| "no raw code".to_owned()),
lockfile: job.raw_lock.clone(),
language: job.language.to_owned(),
envs: None,
codebase: if job
.script_hash
.is_some_and(|y| y.0 == PREVIEW_IS_CODEBASE_HASH)
{
Some(job.id.to_string())
} else {
None
let ContentReqLangEnvs { content: inner_content, lockfile: requirements_o, language, envs } =
match job.job_kind {
JobKind::Preview => ContentReqLangEnvs {
content: job
.raw_code
.clone()
.unwrap_or_else(|| "no raw code".to_owned()),
lockfile: job.raw_lock.clone(),
language: job.language.to_owned(),
envs: None,
},
},
JobKind::Script_Hub => {
get_hub_script_content_and_requirements(job.script_path.clone(), db).await?
}
JobKind::Script => {
get_script_content_by_hash(
&job.script_hash.unwrap_or(ScriptHash(0)),
&job.workspace_id,
db,
)
.await?
}
JobKind::DeploymentCallback => {
get_script_content_by_path(job.script_path.clone(), &job.workspace_id, db).await?
}
_ => unreachable!(
"handle_code_execution_job should never be reachable with a non-code execution job"
),
};
JobKind::Script_Hub => {
get_hub_script_content_and_requirements(job.script_path.clone(), db).await?
}
JobKind::Script => {
get_script_content_by_hash(
&job.script_hash.unwrap_or(ScriptHash(0)),
&job.workspace_id,
db,
)
.await?
}
JobKind::DeploymentCallback => {
get_script_content_by_path(job.script_path.clone(), &job.workspace_id, db).await?
}
_ => unreachable!(
"handle_code_execution_job should never be reachable with a non-code execution job"
),
};
if language == Some(ScriptLang::Postgresql) {
return do_postgresql(
@@ -3261,16 +3192,9 @@ async fn handle_code_execution_job(
db,
)
.await;
let reserved_variables = get_reserved_variables(job, &client.get_token().await, db).await?;
let code = format!(
"const BASE_URL = '{base_internal_url}';\nconst BASE_INTERNAL_URL = '{base_internal_url}';\n{}\n{}",
reserved_variables
.iter()
.map(|(k, v)| format!("const {} = '{}';\n", k, v))
.collect::<Vec<String>>()
.join("\n"),
"const BASE_URL = '{base_internal_url}';\nconst WM_TOKEN = '{}';\n{}",
&client.get_token().await,
inner_content
);
let (result, ts_logs) =
@@ -3286,6 +3210,8 @@ async fn handle_code_execution_job(
.unwrap_or_else(|| "NO_LANG".to_string());
tracing::debug!(
worker_name = %worker_name,
job_id = %job.id,
workspace_id = %job.workspace_id,
"started {} job {}",
&lang_str,
@@ -3354,7 +3280,6 @@ mount {{
Some(ScriptLang::Bun) => {
handle_bun_job(
requirements_o,
codebase,
mem_peak,
canceled_by,
job,
@@ -3421,6 +3346,8 @@ mount {{
_ => panic!("unreachable, language is not supported: {language:#?}"),
};
tracing::info!(
worker_name = %worker_name,
job_id = %job.id,
workspace_id = %job.workspace_id,
is_ok = result.is_ok(),
"finished {} job {}",
@@ -4004,14 +3931,6 @@ async fn lock_modules_app(
.unwrap_or_default()
.to_string();
let mut logs = "".to_string();
if v.get("lock")
.is_some_and(|x| !x.as_str().unwrap().trim().is_empty())
{
logs.push_str(
"Found already locked inline script. Skipping lock...\n",
);
return Ok(Value::Object(m.clone()));
}
logs.push_str("Found lockable inline script. Generating lock...\n");
let new_lock = capture_dependency_job(
&job.id,

View File

@@ -42,7 +42,6 @@ use windmill_common::{
},
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform, Retry, Suspend},
};
use windmill_queue::schedule::get_schedule_opt;
use windmill_queue::{
add_completed_job, add_completed_job_error, append_logs, get_queued_job,
handle_maybe_scheduled_job, CanceledBy, PushIsolationLevel, WrappedError,
@@ -672,7 +671,7 @@ pub async fn update_flow_status_after_job_completion_internal<
.root_job
.map(|x| x.to_string())
.unwrap_or_else(|| "none".to_string());
tracing::info!(id = %flow_job.id, root_id = %job_root, "update flow status");
tracing::info!(id = %flow_job.id, root_id = %job_root, worker_name = %worker_name, "update flow status");
let module = get_module(&flow_job, module_index);
@@ -870,7 +869,7 @@ pub async fn update_flow_status_after_job_completion_internal<
}
if let Some(parent_job) = flow_job.parent_job {
tracing::info!(subflow_id = %flow_job.id, parent_id = %parent_job, "subflow is finished, updating parent flow status");
tracing::info!(subflow_id = %flow_job.id, parent_id = %parent_job, worker_name = %worker_name, "subflow is finished, updating parent flow status");
return Ok(Some(RecUpdateFlowStatusAfterJobCompletion {
flow: parent_job,
@@ -932,7 +931,7 @@ fn get_module(flow_job: &QueuedJob, module_index: Option<usize>) -> Option<FlowM
if let Some(module) = raw_flow.modules.get(i) {
Some(module.clone())
} else {
raw_flow.failure_module.map(|x| *x.clone())
raw_flow.failure_module
}
} else {
None
@@ -1202,37 +1201,23 @@ pub async fn handle_flow<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
&& flow_job.script_path.is_some()
&& status.step == 0
{
let mut tx: QueueTransaction<'_, R> = (rsmq.clone(), db.begin().await?).into();
let tx: QueueTransaction<'_, R> = (rsmq.clone(), db.begin().await?).into();
let schedule_path = flow_job.schedule_path.as_ref().unwrap();
let schedule =
get_schedule_opt(tx.transaction_mut(), &flow_job.workspace_id, schedule_path).await?;
tx.commit().await?;
if let Some(schedule) = schedule {
if let Err(err) = handle_maybe_scheduled_job(
rsmq.clone(),
db,
flow_job,
&schedule,
flow_job.script_path.as_ref().unwrap(),
&flow_job.workspace_id,
)
.await
{
match err {
Error::QuotaExceeded(_) => return Err(err.into()),
// scheduling next job failed and could not disable schedule => make zombie job to retry
_ => return Ok(()),
}
};
} else {
tracing::error!(
"Schedule {schedule_path} in {} not found. Impossible to schedule again",
&flow_job.workspace_id
);
match handle_maybe_scheduled_job(
tx,
db,
flow_job.schedule_path.as_ref().unwrap(),
flow_job.script_path.as_ref().unwrap(),
&flow_job.workspace_id,
)
.await
{
Ok(tx) => {
tx.commit().await?;
}
Err(e) => {
tracing::error!("Error during handle_maybe_scheduled_job: {e}");
}
}
}
@@ -1689,7 +1674,7 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
let mut module: &FlowModule = flow
.modules
.get(i)
.or_else(|| flow.failure_module.as_deref())
.or_else(|| flow.failure_module.as_ref())
.with_context(|| format!("no module at index {}", status.step))?;
let current_id = &module.id;
@@ -3124,7 +3109,6 @@ fn raw_script_to_payload(
) -> JobPayloadWithTag {
JobPayloadWithTag {
payload: JobPayload::Code(RawCode {
hash: None,
path,
content: content.clone(),
language: language.clone(),

View File

@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.324.2";
export const VERSION = "v1.308.2";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -5,13 +5,15 @@ import {
colors,
Command,
ListableApp,
log,
Policy,
SEP,
Table,
yamlParse,
} from "./deps.ts";
import { GlobalOptions, isSuperset } from "./types.ts";
import {
GlobalOptions,
isSuperset,
parseFromFile,
removeType,
} from "./types.ts";
export interface AppFile {
value: any;
@@ -19,20 +21,14 @@ export interface AppFile {
policy: Policy;
}
const alreadySynced: string[] = [];
export async function pushApp(
workspace: string,
remotePath: string,
localPath: string,
filePath: string,
app: AppFile | undefined,
newApp: AppFile,
message?: string
): Promise<void> {
if (alreadySynced.includes(localPath)) {
return;
}
alreadySynced.push(localPath);
let app: any = undefined;
const remotePath = removeType(filePath, "app");
// deleting old app if it exists in raw mode
try {
app = await AppService.getAppByPath({
@@ -43,61 +39,28 @@ export async function pushApp(
//ignore
}
if (!localPath.endsWith(SEP)) {
localPath += SEP;
}
const localAppRaw = await Deno.readTextFile(localPath + "app.yaml");
const localApp = yamlParse(localAppRaw) as AppFile;
function replaceInlineScripts(rec: any) {
if (!rec) {
return;
}
if (typeof rec == "object") {
return Object.entries(rec).flatMap(([k, v]) => {
if (k == "inlineScript" && typeof v == "object") {
const o: Record<string, any> = v as any;
if (o["content"] && o["content"].startsWith("!inline")) {
const basePath = localPath + o["content"].split(" ")[1];
o["content"] = Deno.readTextFileSync(basePath);
}
if (o["lock"] && o["lock"].startsWith("!inline")) {
const basePath = localPath + o["lock"].split(" ")[1];
o["lock"] = Deno.readTextFileSync(basePath);
}
} else {
replaceInlineScripts(v);
}
});
}
return [];
}
replaceInlineScripts(localApp.value);
if (app) {
if (isSuperset(localApp, app)) {
log.info(colors.green(`App ${remotePath} is up to date`));
if (isSuperset(newApp, app)) {
return;
}
log.info(colors.bold.yellow(`Updating app ${remotePath}...`));
await AppService.updateApp({
workspace,
path: remotePath.replaceAll("\\", "/"),
requestBody: {
deployment_message: message,
...localApp,
...newApp,
},
});
} else {
log.info(colors.yellow.bold("Creating new app..."));
console.log(colors.yellow.bold("Creating new app..."));
console.log(message);
await AppService.createApp({
workspace,
requestBody: {
path: remotePath.replaceAll("\\", "/"),
deployment_message: message,
...localApp,
...newApp,
},
});
}
@@ -131,22 +94,32 @@ async function list(opts: GlobalOptions) {
.render();
}
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
async function push(opts: GlobalOptions, filePath: string) {
const remotePath = filePath.split(".")[0];
if (!validatePath(remotePath)) {
return;
}
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
await pushApp(workspace.workspaceId, remotePath, filePath);
log.info(colors.bold.underline.green("Flow pushed"));
<<<<<<< Updated upstream
await pushApp(
workspace.workspaceId,
filePath,
undefined,
parseFromFile(filePath)
);
=======
await pushApp(workspace.workspaceId, filePath, parseFromFile(filePath));
>>>>>>> Stashed changes
console.log(colors.bold.underline.green("App pushed"));
}
const command = new Command()
.description("app related commands")
.action(list as any)
.command("push", "push a local app ")
.arguments("<file_path:string> <remote_path:string>")
.arguments("<file_path:file>")
.action(push as any);
export default command;

View File

@@ -1,21 +0,0 @@
import { Codebase, SyncOptions } from "./conf.ts";
import { log } from "./deps.ts";
import { digestDir } from "./utils.ts";
export type SyncCodebase = Codebase & { digest: string };
export async function listSyncCodebases(
options: SyncOptions
): Promise<SyncCodebase[]> {
const res: SyncCodebase[] = [];
const nb_codebase = options?.codebases?.length ?? 0;
if (nb_codebase > 0) {
log.info(`Found ${nb_codebase} codebases:`);
}
for (const codebase of options?.codebases ?? []) {
const digest = await digestDir(codebase.relative_path);
log.info(`Codebase ${codebase.relative_path}, digest: ${digest}`);
res.push({ ...codebase, digest });
}
return res;
}

View File

@@ -20,13 +20,6 @@ export interface SyncOptions {
extraIncludes?: string[];
excludes?: string[];
defaultTs?: "bun" | "deno";
codebases?: Codebase[];
}
export interface Codebase {
relative_path: string;
includes?: string[];
excludes?: string[];
}
export async function readConfigFile(): Promise<SyncOptions> {

View File

@@ -1,5 +1,6 @@
// windmill
export * from "npm:windmill-client@1.319.1";
export { setClient } from "npm:windmill-client@1.304.2";
export * from "npm:windmill-client@1.304.2";
// cliffy
export { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts";
@@ -27,11 +28,6 @@ export { DelimiterStream } from "https://deno.land/std@0.207.0/streams/mod.ts";
export { iterateReader } from "https://deno.land/std@0.207.0/streams/iterate_reader.ts";
export { writeAllSync } from "https://deno.land/std@0.207.0/streams/mod.ts";
export { encodeHex } from "https://deno.land/std@0.207.0/encoding/hex.ts";
export * as log from "https://deno.land/std@0.207.0/log/mod.ts";
export {
stringify as yamlStringify,
parse as yamlParse,
} from "https://deno.land/std@0.207.0/yaml/mod.ts";
// other
export { Application, Router } from "https://deno.land/x/oak@v12.5.0/mod.ts";
@@ -47,6 +43,10 @@ export { default as microdiff } from "https://deno.land/x/microdiff@v1.3.1/index
export { default as objectHash } from "https://deno.land/x/object_hash@2.0.3.1/mod.ts";
export { minimatch } from "npm:minimatch";
export { default as JSZip } from "npm:jszip@3.7.1";
export * as log from "https://deno.land/std@0.186.0/log/mod.ts";
export {
stringify as yamlStringify,
parse as yamlParse,
} from "https://deno.land/std@0.184.0/yaml/mod.ts";
export { open } from "https://deno.land/x/open@v0.0.5/index.ts";
export { default as gitignore_parser } from "npm:gitignore-parser";

View File

@@ -27,13 +27,13 @@ const alreadySynced: string[] = [];
export async function pushFlow(
workspace: string,
remotePath: string,
localPath: string,
localFlowPath: string,
message?: string
): Promise<void> {
if (alreadySynced.includes(localPath)) {
if (alreadySynced.includes(localFlowPath)) {
return;
}
alreadySynced.push(localPath);
alreadySynced.push(localFlowPath);
let flow: Flow | undefined = undefined;
try {
flow = await FlowService.getFlowByPath({
@@ -44,31 +44,17 @@ export async function pushFlow(
// flow doesn't exist
}
if (!localPath.endsWith(SEP)) {
localPath += SEP;
if (!localFlowPath.endsWith(SEP)) {
localFlowPath += SEP;
}
const localFlowRaw = await Deno.readTextFile(localPath + "flow.yaml");
const localFlowRaw = await Deno.readTextFile(localFlowPath + "flow.yaml");
const localFlow = yamlParse(localFlowRaw) as FlowFile;
function replaceInlineScripts(modules: FlowModule[]) {
modules.forEach((m) => {
if (m.value.type == "rawscript") {
const path = m.value.content.split(" ")[1];
m.value.content = Deno.readTextFileSync(localPath + path);
const lock = m.value.lock;
if (
lock &&
typeof lock == "string" &&
lock.trimStart().startsWith("!inline ")
) {
const path = lock.split(" ")[1];
try {
m.value.lock = Deno.readTextFileSync(localPath + path);
} catch {
log.error(`Lock file ${path} not found`);
}
}
m.value.content = Deno.readTextFileSync(localFlowPath + path);
} else if (m.value.type == "forloopflow") {
replaceInlineScripts(m.value.modules);
} else if (m.value.type == "branchall") {
@@ -208,15 +194,12 @@ async function run(
log.info(jobInfo.result ?? {});
}
export function bootstrap(
opts: GlobalOptions & { summary: string; description: string },
flowPath: string
) {
export function bootstrap(opts: GlobalOptions & {summary: string, description: string}, flowPath: string) {
if (!validatePath(flowPath)) {
return;
}
const flowDirFullPath = `${flowPath}.flow`;
const flowDirFullPath = `${flowPath}.flow`
Deno.mkdirSync(flowDirFullPath, { recursive: false });
const newFlowDefinition = defaultFlowDefinition();
@@ -227,12 +210,13 @@ export function bootstrap(
newFlowDefinition.description = opts.description;
}
const newFlowDefinitionYaml = yamlStringify(
newFlowDefinition as Record<string, any>
);
const newFlowDefinitionYaml = yamlStringify(newFlowDefinition as Record<string, any>);
const flowYamlPath = `${flowDirFullPath}/flow.yaml`;
Deno.writeTextFile(flowYamlPath, newFlowDefinitionYaml, { createNew: true });
Deno.writeTextFile(
flowYamlPath,
newFlowDefinitionYaml,
{ createNew: true });
}
const command = new Command()

View File

@@ -17,16 +17,16 @@ async function pull(opts: GlobalOptions) {
const userInfo = await requireLogin(opts);
const uid = (await SettingService.getGlobal({
const uid = await SettingService.getGlobal({
key: "uid",
})) as string;
});
const hubBaseUrl =
(await SettingService.getGlobal({
key: "hubBaseUrl",
})) ?? "https://hub.windmill.dev";
const headers: Record<string, string> = {
const headers = {
Accept: "application/json",
"X-email": userInfo.email,
};
@@ -99,7 +99,8 @@ async function pull(opts: GlobalOptions) {
workspace.workspaceId,
x.name + ".resource-type.json",
undefined,
x
x,
true
);
}
}

View File

@@ -3,9 +3,7 @@ import {
CompletionsCommand,
DenoLandProvider,
UpgradeCommand,
colors,
log,
yamlStringify,
} from "./deps.ts";
import flow from "./flow.ts";
import app from "./apps.ts";
@@ -33,7 +31,7 @@ addEventListener("error", (event) => {
}
});
export const VERSION = "v1.324.2";
export const VERSION = "v1.308.2";
let command: any = new Command()
.name("wmill")
@@ -60,23 +58,6 @@ let command: any = new Command()
"Specify headers to use for all requests. e.g: \"HEADERS='h1: v1, h2: v2'\""
)
.version(VERSION)
.command("init", "Bootstrap a windmill project with a wmill.yaml file")
.action(async () => {
if (await Deno.stat("wmill.yaml").catch(() => null)) {
log.error(colors.red("wmill.yaml already exists"));
return;
}
await Deno.writeTextFile(
"wmill.yaml",
yamlStringify({
defaultTs: "bun",
includes: [],
excludes: [],
codebases: [],
})
);
log.info(colors.green("wmill.yaml created"));
})
.command("app", app)
.command("flow", flow)
.command("script", script)
@@ -89,7 +70,6 @@ let command: any = new Command()
.command("schedule", schedule)
.command("dev", dev)
.command("sync", sync)
.command("version", "Show version information")
.action(async (opts) => {
console.log("CLI build against " + VERSION);

View File

@@ -1,6 +1,6 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions } from "./types.ts";
import { colors, log, yamlParse, yamlStringify } from "./deps.ts";
import { colors, encodeHex, log, yamlParse, yamlStringify } from "./deps.ts";
import {
ScriptMetadata,
defaultScriptMetadata,
@@ -25,7 +25,6 @@ import { ScriptLanguage } from "./script_common.ts";
import { inferContentTypeFromFilePath } from "./script_common.ts";
import { GlobalDeps } from "./script.ts";
import { yamlOptions } from "./sync.ts";
import { generateHash } from "./utils.ts";
export async function generateAllMetadata() {}
@@ -179,16 +178,6 @@ async function updateScriptLock(
metadataContent: Record<string, any>,
rawDeps: string | undefined
): Promise<void> {
if (
!(
language == "bun" ||
language == "python3" ||
language == "go" ||
language == "deno"
)
) {
return;
}
// generate the script lock running a dependency job in Windmill and update it inplace
// TODO: update this once the client is released
const rawResponse = await fetch(
@@ -225,9 +214,7 @@ async function updateScriptLock(
)}`
);
}
const lockPath = remotePath + ".script.lock";
await Deno.writeTextFile(lockPath, lock);
metadataContent.lock = "!inline " + lockPath;
metadataContent.lock = lock;
} catch (e) {
throw new Error(
`Failed to generate lockfile. Status was: ${rawResponse.statusText}, ${responseText}, ${e}`
@@ -439,30 +426,10 @@ function argSigToJsonSchemaType(
// end of refactoring TODO //
////////////////////////////////////////////////////////////////////////////////////////////
export async function replaceLock(o?: { lock?: string | string[] }) {
if (Array.isArray(o?.lock)) {
o.lock = o.lock.join("\n");
}
if (o?.lock?.startsWith("!inline ")) {
try {
const lockPath = o?.lock?.split(" ")[1];
o.lock = await Deno.readTextFile(lockPath);
} catch (e) {
log.info(
colors.yellow(`Failed to read lockfile, doing as if it was empty: ${e}`)
);
o.lock = "";
}
}
}
export async function parseMetadataFile(
scriptPath: string,
generateMetadataIfMissing:
| (GlobalOptions & {
path: string;
workspaceRemote: Workspace;
schemaOnly?: boolean;
})
| (GlobalOptions & { path: string; workspaceRemote: Workspace })
| undefined,
globalDeps: GlobalDeps
): Promise<{ isJson: boolean; payload: any; path: string }> {
@@ -479,8 +446,9 @@ export async function parseMetadataFile(
metadataFilePath = scriptPath + ".script.yaml";
await Deno.stat(metadataFilePath);
const payload: any = yamlParse(await Deno.readTextFile(metadataFilePath));
await replaceLock(payload);
if (Array.isArray(payload?.["lock"])) {
payload["lock"] = payload["lock"].join("\n");
}
return {
path: metadataFilePath,
payload,
@@ -497,7 +465,7 @@ export async function parseMetadataFile(
scriptInitialMetadata as Record<string, any>,
yamlOptions
);
await Deno.writeTextFile(metadataFilePath, scriptInitialMetadataYaml, {
Deno.writeTextFile(metadataFilePath, scriptInitialMetadataYaml, {
createNew: true,
});
@@ -514,10 +482,10 @@ export async function parseMetadataFile(
false,
globalDeps
);
scriptInitialMetadata = yamlParse(
await Deno.readTextFile(metadataFilePath)
) as ScriptMetadata;
await replaceLock(scriptInitialMetadata);
} catch (e) {
log.info(
colors.yellow(
@@ -556,6 +524,12 @@ export async function readLockfile(): Promise<Lock> {
}
}
async function generateHash(content: string): Promise<string> {
const messageBuffer = new TextEncoder().encode(content);
const hashBuffer = await crypto.subtle.digest("SHA-256", messageBuffer);
return encodeHex(hashBuffer);
}
export async function checkifMetadataUptodate(
path: string,
requirement: string

View File

@@ -16,8 +16,7 @@ export async function downloadZip(
includeSettings?: boolean,
defaultTs?: "bun" | "deno"
): Promise<JSZip | undefined> {
const requestHeaders: HeadersInit & { set(x: string, y: string): void } =
new Headers();
const requestHeaders: HeadersInit = new Headers();
requestHeaders.set("Authorization", "Bearer " + workspace.token);
requestHeaders.set("Content-Type", "application/octet-stream");

View File

@@ -1,8 +1,8 @@
// deno-lint-ignore-file no-explicit-any
import { colors, Command, log } from "./deps.ts";
import { colors, Command } from "./deps.ts";
import { GlobalOptions } from "./types.ts";
function stub(_opts: GlobalOptions, _dir?: string) {
async function stub(_opts: GlobalOptions, _dir?: string) {
log.info(
colors.red.underline(
'Push is deprecated. Use "sync push --raw" instead. See <TODO_LINK_HERE> for more information.'

View File

@@ -7,6 +7,7 @@ import {
Confirm,
JobService,
log,
NewScript,
readAll,
Script,
ScriptService,
@@ -28,7 +29,6 @@ import {
} from "./script_common.ts";
import {
elementsToMap,
findCodebase,
readDirRecursiveWithIgnore,
yamlOptions,
} from "./sync.ts";
@@ -39,7 +39,6 @@ import {
mergeConfigWithConfigFile,
readConfigFile,
} from "./conf.ts";
import { SyncCodebase, listSyncCodebases } from "./codebase.ts";
export interface ScriptFile {
parent_hash?: string;
@@ -72,18 +71,8 @@ async function push(opts: PushOptions, filePath: string) {
}
await requireLogin(opts);
const codebases = await listSyncCodebases(opts as SyncOptions);
const globalDeps = await findGlobalDeps(codebases);
await handleFile(
filePath,
workspace,
[],
undefined,
opts,
globalDeps,
codebases
);
const globalDeps = await findGlobalDeps();
await handleFile(filePath, workspace, [], undefined, opts, globalDeps);
log.info(colors.bold.underline.green(`Script ${filePath} pushed`));
}
@@ -92,24 +81,17 @@ export async function handleScriptMetadata(
workspace: Workspace,
alreadySynced: string[],
message: string | undefined,
globalDeps: GlobalDeps,
codebases: SyncCodebase[],
opts: GlobalOptions
globalDeps: GlobalDeps
): Promise<boolean> {
if (
path.endsWith(".script.json") ||
path.endsWith(".script.yaml") ||
path.endsWith(".script.lock")
) {
if (path.endsWith(".script.json") || path.endsWith(".script.yaml")) {
const contentPath = await findContentFile(path);
return handleFile(
contentPath,
workspace,
alreadySynced,
message,
opts,
globalDeps,
codebases
undefined,
globalDeps
);
} else {
return false;
@@ -122,8 +104,7 @@ export async function handleFile(
alreadySynced: string[],
message: string | undefined,
opts: (GlobalOptions & { defaultTs?: "bun" | "deno" }) | undefined,
globalDeps: GlobalDeps,
codebases: SyncCodebase[]
globalDeps: GlobalDeps
): Promise<boolean> {
if (
!path.includes(".inline_script.") &&
@@ -138,40 +119,14 @@ export async function handleFile(
const remotePath = path
.substring(0, path.indexOf("."))
.replaceAll("\\", "/");
const language = inferContentTypeFromFilePath(path, opts?.defaultTs);
const codebase =
language == "bun" ? findCodebase(path, codebases) : undefined;
let bundleContent: string | undefined = undefined;
if (codebase) {
const esbuild = await import("npm:esbuild");
log.info(`Starting building the bundle for ${path}`);
const out = await esbuild.build({
entryPoints: [path],
format: "esm",
bundle: true,
write: false,
});
bundleContent = out.outputFiles[0].text;
log.info(`Finished building the bundle for ${path}`);
}
const typed = (
await parseMetadataFile(
remotePath,
opts
? {
...opts,
path,
workspaceRemote: workspace,
schemaOnly: codebase ? true : undefined,
}
: undefined,
opts ? { ...opts, path, workspaceRemote: workspace } : undefined,
globalDeps
)
)?.payload;
const language = inferContentTypeFromFilePath(path, opts?.defaultTs);
const workspaceId = workspace.workspaceId;
@@ -187,29 +142,6 @@ export async function handleFile(
}
const content = await Deno.readTextFile(path);
const requestBodyCommon = {
content,
description: typed?.description ?? "",
language: language,
path: remotePath.replaceAll("\\", "/"),
summary: typed?.summary ?? "",
kind: typed?.kind,
lock: typed?.lock,
schema: typed?.schema,
tag: typed?.tag,
ws_error_handler_muted: typed?.ws_error_handler_muted,
dedicated_worker: typed?.dedicated_worker,
cache_ttl: typed?.cache_ttl,
concurrency_time_window_s: typed?.concurrency_time_window_s,
concurrent_limit: typed?.concurrent_limit,
deployment_message: message,
restart_unless_cancelled: typed?.restart_unless_cancelled,
visible_to_runner_only: typed?.visible_to_runner_only,
no_main_func: typed?.no_main_func,
priority: typed?.priority,
codebase: codebase?.digest,
};
if (remote) {
if (content === remote.content) {
if (
@@ -235,7 +167,6 @@ export async function handleFile(
Boolean(remote.restart_unless_cancelled) &&
Boolean(typed.visible_to_runner_only) ==
Boolean(remote.visible_to_runner_only) &&
Boolean(typed.no_main_func) == Boolean(remote.no_main_func) &&
typed.priority == Boolean(remote.priority))
) {
log.info(colors.green(`Script ${remotePath} is up to date`));
@@ -246,90 +177,68 @@ export async function handleFile(
log.info(
colors.yellow.bold(`Creating script with a parent ${remotePath}`)
);
const body = {
...requestBodyCommon,
parent_hash: remote.hash,
};
await createScript(bundleContent, workspaceId, body, workspace);
await ScriptService.createScript({
workspace: workspaceId,
requestBody: {
content,
description: typed?.description ?? "",
language: language as NewScript.language,
path: remotePath.replaceAll("\\", "/"),
summary: typed?.summary ?? "",
kind: typed?.kind,
lock: typed?.lock,
parent_hash: remote.hash,
schema: typed?.schema,
tag: typed?.tag,
ws_error_handler_muted: typed?.ws_error_handler_muted,
dedicated_worker: typed?.dedicated_worker,
cache_ttl: typed?.cache_ttl,
concurrency_time_window_s: typed?.concurrency_time_window_s,
concurrent_limit: typed?.concurrent_limit,
deployment_message: message,
restart_unless_cancelled: typed?.restart_unless_cancelled,
visible_to_runner_only: typed?.visible_to_runner_only,
priority: typed?.priority,
},
});
} else {
log.info(
colors.yellow.bold(`Creating script without parent ${remotePath}`)
);
const body = {
...requestBodyCommon,
parent_hash: undefined,
};
await createScript(bundleContent, workspaceId, body, workspace);
// no parent hash
await ScriptService.createScript({
workspace: workspaceId,
requestBody: {
content,
description: typed?.description ?? "",
language: language as NewScript.language,
path: remotePath.replaceAll("\\", "/"),
summary: typed?.summary ?? "",
kind: typed?.kind,
lock: typed?.lock,
parent_hash: undefined,
schema: typed?.schema,
tag: typed?.tag,
ws_error_handler_muted: typed?.ws_error_handler_muted,
dedicated_worker: typed?.dedicated_worker,
cache_ttl: typed?.cache_ttl,
concurrency_time_window_s: typed?.concurrency_time_window_s,
concurrent_limit: typed?.concurrent_limit,
deployment_message: message,
restart_unless_cancelled: typed?.restart_unless_cancelled,
visible_to_runner_only: typed?.visible_to_runner_only,
priority: typed?.priority,
},
});
}
return true;
}
return false;
}
async function createScript(
bundleContent: string | undefined,
workspaceId: string,
body: {
parent_hash: string | undefined;
content: string;
description: any;
language: ScriptLanguage;
path: string;
summary: any;
kind: any;
lock: any;
schema: any;
tag: any;
ws_error_handler_muted: any;
dedicated_worker: any;
cache_ttl: any;
concurrency_time_window_s: any;
concurrent_limit: any;
deployment_message: string | undefined;
restart_unless_cancelled: any;
visible_to_runner_only: any;
no_main_func: any;
priority: any;
},
workspace: Workspace
) {
if (!bundleContent) {
// no parent hash
await ScriptService.createScript({
workspace: workspaceId,
requestBody: body,
});
} else {
const form = new FormData();
form.append("script", JSON.stringify(body));
form.append("file", bundleContent);
const url =
workspace.remote +
"api/w/" +
workspace.workspaceId +
"/scripts/create_snapshot";
const req = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${workspace.token}` },
body: form,
});
if (req.status != 201) {
throw Error(
`Script snapshot creation was not successful: ${req.status} - ${
req.statusText
} - ${await req.text()}`
);
}
}
}
export async function findContentFile(filePath: string) {
const candidates = filePath.endsWith("script.json")
? exts.map((x) => filePath.replace(".script.json", x))
: filePath.endsWith("script.lock")
? exts.map((x) => filePath.replace(".script.lock", x))
: exts.map((x) => filePath.replace(".script.yaml", x));
const validCandidates = (
@@ -670,12 +579,10 @@ export type GlobalDeps = {
pkgs: Record<string, string>;
reqs: Record<string, string>;
};
export async function findGlobalDeps(
codebases: SyncCodebase[]
): Promise<GlobalDeps> {
export async function findGlobalDeps(): Promise<GlobalDeps> {
const pkgs: { [key: string]: string } = {};
const reqs: { [key: string]: string } = {};
const els = await FSFSElement(Deno.cwd(), codebases);
const els = await FSFSElement(Deno.cwd());
for await (const entry of readDirRecursiveWithIgnore((p, isDir) => {
p = "/" + p;
return (
@@ -698,7 +605,7 @@ async function generateMetadata(
schemaOnly?: boolean;
yes?: boolean;
} & SyncOptions,
scriptPath: string | undefined
scriptPath?: string
) {
if (scriptPath == "") {
scriptPath = undefined;
@@ -710,9 +617,8 @@ async function generateMetadata(
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
opts = await mergeConfigWithConfigFile(opts);
const codebases = await listSyncCodebases(opts);
const globalDeps = await findGlobalDeps(codebases);
const globalDeps = await findGlobalDeps();
if (scriptPath) {
// read script metadata file
await generateMetadataInternal(
@@ -726,13 +632,12 @@ async function generateMetadata(
} else {
const ignore = await ignoreF(opts);
const elems = await elementsToMap(
await FSFSElement(Deno.cwd(), codebases),
await FSFSElement(Deno.cwd()),
(p, isD) => {
return (
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
ignore(p, isD) ||
p.includes(".flow/") ||
p.includes(".app/")
p.includes(".flow/")
);
},
false,

View File

@@ -43,9 +43,8 @@ import {
import { handleFile } from "./script.ts";
import { deepEqual } from "./utils.ts";
import { Codebase, SyncOptions, mergeConfigWithConfigFile } from "./conf.ts";
import { SyncOptions, mergeConfigWithConfigFile } from "./conf.ts";
import { removePathPrefix } from "./types.ts";
import { SyncCodebase, listSyncCodebases } from "./codebase.ts";
type DynFSElement = {
isDirectory: boolean;
@@ -55,85 +54,8 @@ type DynFSElement = {
getChildren(): AsyncIterable<DynFSElement>;
};
export function findCodebase(
path: string,
codebases: SyncCodebase[]
): SyncCodebase | undefined {
for (const c of codebases) {
let included = false;
let excluded = false;
if (c.includes == undefined || c.includes == null) {
included = true;
}
if (typeof c.includes == "string") {
c.includes = [c.includes];
}
for (const r of c.includes ?? []) {
if (included) {
break;
}
if (minimatch(path, r)) {
included = true;
}
}
if (typeof c.excludes == "string") {
c.excludes = [c.excludes];
}
for (const r of c.excludes ?? []) {
if (minimatch(path, r)) {
excluded = true;
}
}
if (included && !excluded) {
return c;
}
}
}
async function addCodebaseDigestIfRelevant(
path: string,
content: string,
codebases: SyncCodebase[]
): Promise<string> {
const isScript = path.endsWith(".script.yaml");
if (!isScript) {
return content;
}
let isTs = true;
try {
await Deno.stat(path.replace(".script.yaml", ".ts"));
} catch {
isTs = false;
}
if (!isTs) {
return content;
}
log.info(`isScript: ${isScript}, isTs: ${isTs}; ${path}`);
if (isTs) {
const c = findCodebase(path, codebases);
if (c) {
const parsed: any = yamlParse(content);
if (parsed && typeof parsed == "object") {
parsed["codebase"] = c.digest;
return yamlStringify(parsed, yamlOptions);
} else {
throw Error(
`Expected local yaml ${path} to be an object, found: ${content} instead`
);
}
}
}
return content;
}
export async function FSFSElement(
p: string,
codebases: SyncCodebase[]
): Promise<DynFSElement> {
function _internal_element(
localP: string,
isDir: boolean,
codebases: SyncCodebase[]
): DynFSElement {
export async function FSFSElement(p: string): Promise<DynFSElement> {
function _internal_element(localP: string, isDir: boolean): DynFSElement {
return {
isDirectory: isDir,
path: localP.substring(p.length + 1),
@@ -141,11 +63,7 @@ export async function FSFSElement(
if (!isDir) return [];
try {
for await (const e of Deno.readDir(localP)) {
yield _internal_element(
path.join(localP, e.name),
e.isDirectory,
codebases
);
yield _internal_element(path.join(localP, e.name), e.isDirectory);
}
} catch (e) {
log.warning(`Error reading dir: ${localP}, ${e}`);
@@ -156,12 +74,11 @@ export async function FSFSElement(
// },
async getContentText(): Promise<string> {
const content = await Deno.readTextFile(localP);
return await addCodebaseDigestIfRelevant(localP, content, codebases);
return content;
},
};
}
return _internal_element(p, (await Deno.stat(p)).isDirectory, codebases);
return _internal_element(p, (await Deno.stat(p)).isDirectory);
}
function prioritizeName(name: string): string {
@@ -191,19 +108,17 @@ export const yamlOptions = {
};
function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
async function _internal_file(
p: string,
f: JSZip.JSZipObject
): Promise<DynFSElement[]> {
const kind: "flow" | "app" | "script" | "other" = p.endsWith("flow.json")
? "flow"
: p.endsWith("app.json")
? "app"
: p.endsWith("script.json")
? "script"
: "other";
const isJson = p.endsWith(".json");
function _internal_file(p: string, f: JSZip.JSZipObject): DynFSElement {
const isFlow = p.endsWith("flow.json");
function transformPath() {
if (isFlow) {
return p.replace("flow.json", "flow");
} else {
return useYaml && p.endsWith(".json")
? p.replaceAll(".json", ".yaml")
: p;
}
}
interface InlineScript {
path: string;
@@ -214,8 +129,8 @@ function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
const seen_names = new Set<string>();
function assignPath(
summary: string | undefined,
language: RawScript["language"]
): [string, string] {
language: RawScript.language
): string {
let name;
const INLINE_SCRIPT = "inline_script";
@@ -248,43 +163,27 @@ function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
else if (language == "graphql") ext = "gql";
else if (language == "bun") ext = "bun.ts";
else if (language == "nativets") ext = "native.ts";
else if (language == "frontend") ext = "frontend.js";
else ext = "no_ext";
return [`${name}.inline_script.`, ext];
return `${name}.inline_script.${ext}`;
}
function extractInlineScriptsForFlows(
modules: FlowModule[]
): InlineScript[] {
function extractInlineScripts(modules: FlowModule[]): InlineScript[] {
return modules.flatMap((m) => {
if (m.value.type == "rawscript") {
const [basePath, ext] = assignPath(m.summary, m.value.language);
const path = basePath + ext;
const path = assignPath(m.summary, m.value.language);
const content = m.value.content;
const r = [{ path: path, content: content }];
m.value.content = "!inline " + path;
const lock = m.value.lock;
if (lock) {
const lockPath = basePath + "lock";
m.value.lock = "!inline " + lockPath;
r.push({ path: lockPath, content: lock });
}
return r;
return [{ path: path, content: content }];
} else if (m.value.type == "forloopflow") {
return extractInlineScriptsForFlows(m.value.modules);
return extractInlineScripts(m.value.modules);
} else if (m.value.type == "branchall") {
return m.value.branches.flatMap((b) =>
extractInlineScriptsForFlows(b.modules)
extractInlineScripts(b.modules)
);
} else if (m.value.type == "whileloopflow") {
return extractInlineScriptsForFlows(m.value.modules);
} else if (m.value.type == "branchone") {
return [
...m.value.branches.flatMap((b) =>
extractInlineScriptsForFlows(b.modules)
),
...extractInlineScriptsForFlows(m.value.default),
...m.value.branches.flatMap((b) => extractInlineScripts(b.modules)),
...extractInlineScripts(m.value.default),
];
} else {
return [];
@@ -292,147 +191,47 @@ function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
});
}
function extractInlineScriptsForApps(rec: any): InlineScript[] {
if (!rec) {
return [];
}
if (typeof rec == "object") {
return Object.entries(rec).flatMap(([k, v]) => {
if (k == "inlineScript" && typeof v == "object") {
const o: Record<string, any> = v as any;
const name = rec["name"];
const [basePath, ext] = assignPath(name, o["language"]);
const r = [];
if (o["content"]) {
const content = o["content"];
o["content"] = "!inline " + basePath + ext;
r.push({
path: basePath + ext,
content: content,
});
}
if (o["lock"]) {
const lock = o["lock"];
o["lock"] = "!inline " + basePath + "lock";
r.push({
path: basePath + "lock",
content: lock,
});
}
return r;
} else {
return extractInlineScriptsForApps(v);
}
});
}
return [];
}
function transformPath() {
if (kind == "flow") {
return p.replace("flow.json", "flow");
} else if (kind == "app") {
return p.replace("app.json", "app");
} else {
return useYaml && isJson ? p.replaceAll(".json", ".yaml") : p;
}
}
const finalPath = transformPath();
const r = [
{
isDirectory: kind == "flow" || kind == "app",
path: finalPath,
async *getChildren(): AsyncIterable<DynFSElement> {
if (kind == "flow") {
const flow: OpenFlow = JSON.parse(await f.async("text"));
const inlineScripts = extractInlineScriptsForFlows(
flow.value.modules
);
for (const s of inlineScripts) {
yield {
isDirectory: false,
path: path.join(finalPath, s.path),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return s.content;
},
};
}
const flowPath = transformPath();
return {
isDirectory: isFlow,
path: flowPath,
async *getChildren(): AsyncIterable<DynFSElement> {
if (isFlow) {
const flow: OpenFlow = JSON.parse(await f.async("text"));
const inlineScripts = extractInlineScripts(flow.value.modules);
for (const s of inlineScripts) {
yield {
isDirectory: false,
path: path.join(finalPath, "flow.yaml"),
path: path.join(flowPath, s.path),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return yamlStringify(flow, yamlOptions);
},
};
} else if (kind == "app") {
const app = JSON.parse(await f.async("text"));
const inlineScripts = extractInlineScriptsForApps(app?.["value"]);
for (const s of inlineScripts) {
yield {
isDirectory: false,
path: path.join(finalPath, s.path),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return s.content;
},
};
}
yield {
isDirectory: false,
path: path.join(finalPath, "app.yaml"),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return yamlStringify(app, yamlOptions);
return s.content;
},
};
}
},
async getContentText(): Promise<string> {
const content = await f.async("text");
if (kind == "script") {
const parsed = JSON.parse(content);
if (parsed["lock"]) {
parsed["lock"] = "!inline " + removeSuffix(p, ".json") + ".lock";
}
return useYaml
? yamlStringify(parsed, yamlOptions)
: JSON.stringify(parsed, null, 2);
}
return useYaml && isJson
? yamlStringify(JSON.parse(content), yamlOptions)
: content;
},
yield {
isDirectory: false,
path: path.join(flowPath, "flow.yaml"),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return yamlStringify(flow, yamlOptions);
},
};
}
},
];
if (kind == "script") {
const content = await f.async("text");
const parsed = JSON.parse(content);
const lock = parsed["lock"];
if (lock) {
r.push({
isDirectory: false,
path: removeSuffix(finalPath, ".json") + ".lock",
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return lock;
},
});
}
}
return r;
// async getContentBytes(): Promise<Uint8Array> {
// return await f.async("uint8array");
// },
async getContentText(): Promise<string> {
const content = await f.async("text");
return useYaml && p.endsWith(".json")
? yamlStringify(JSON.parse(content), yamlOptions)
: content;
},
};
}
function _internal_folder(p: string, zip: JSZip): DynFSElement {
return {
@@ -446,10 +245,7 @@ function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
const e = zip.folder(file.name)!;
yield _internal_folder(totalPath, e);
} else {
const fs = await _internal_file(totalPath, file);
for (const f of fs) {
yield f;
}
yield _internal_file(totalPath, file);
}
}
},
@@ -543,19 +339,9 @@ export async function elementsToMap(
if (skips.skipResources && path.endsWith(".resource" + ext)) continue;
if (
![
"json",
"yaml",
"go",
"sh",
"ts",
"py",
"sql",
"gql",
"ps1",
"js",
"lock",
].includes(path.split(".").pop() ?? "")
!["json", "yaml", "go", "sh", "ts", "py", "sql", "gql", "ps1"].includes(
path.split(".").pop() ?? ""
)
)
continue;
const content = await entry.getContentText();
@@ -810,8 +596,6 @@ async function pull(opts: GlobalOptions & SyncOptions) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
const codebases = await listSyncCodebases(opts);
log.info(
colors.gray(
"Computing the files to update locally to match remote (taking wmill.yaml into account)"
@@ -833,8 +617,8 @@ async function pull(opts: GlobalOptions & SyncOptions) {
!opts.json
);
const local = !opts.stateful
? await FSFSElement(Deno.cwd(), codebases)
: await FSFSElement(path.join(Deno.cwd(), ".wmill"), []);
? await FSFSElement(Deno.cwd())
: await FSFSElement(path.join(Deno.cwd(), ".wmill"));
const changes = await compareDynFSElement(
remote,
local,
@@ -946,7 +730,7 @@ async function pull(opts: GlobalOptions & SyncOptions) {
showConflict(conflict.path, conflict.local, conflict.change.after);
}
log.info(
colors.red(`Please resolve these conflicts manually by either:
colors.red(`Please resolve theses conflicts manually by either:
- reverting the content back to its remote (\`wmill pull\` and refuse to preserve local when prompted)
- pushing the changes with \`wmill push --skip-pull\` to override wmill with all your local changes
`)
@@ -1009,7 +793,6 @@ function removeSuffix(str: string, suffix: string) {
async function push(opts: GlobalOptions & SyncOptions) {
opts = await mergeConfigWithConfigFile(opts);
const codebases = await listSyncCodebases(opts);
if (opts.raw) {
log.info("--raw is now the default, you can remove it as a flag");
}
@@ -1048,7 +831,7 @@ async function push(opts: GlobalOptions & SyncOptions) {
!opts.json
);
const local = await FSFSElement(path.join(Deno.cwd(), ""), codebases);
const local = await FSFSElement(path.join(Deno.cwd(), ""));
const changes = await compareDynFSElement(
local,
remote,
@@ -1077,11 +860,10 @@ async function push(opts: GlobalOptions & SyncOptions) {
) {
return;
}
log.info(colors.gray(`Applying changes to files ...`));
const alreadySynced: string[] = [];
const globalDeps = await findGlobalDeps(codebases);
const globalDeps = await findGlobalDeps();
for await (const change of changes) {
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
@@ -1099,9 +881,7 @@ async function push(opts: GlobalOptions & SyncOptions) {
workspace,
alreadySynced,
opts.message,
globalDeps,
codebases,
opts
globalDeps
)
) {
if (opts.stateful && stateExists) {
@@ -1115,8 +895,7 @@ async function push(opts: GlobalOptions & SyncOptions) {
alreadySynced,
opts.message,
opts,
globalDeps,
codebases
globalDeps
)
) {
if (opts.stateful && stateExists) {
@@ -1146,8 +925,7 @@ async function push(opts: GlobalOptions & SyncOptions) {
} else if (change.name === "added") {
if (
change.path.endsWith(".script.json") ||
change.path.endsWith(".script.yaml") ||
change.path.endsWith(".lock")
change.path.endsWith(".script.yaml")
) {
continue;
} else if (
@@ -1157,8 +935,7 @@ async function push(opts: GlobalOptions & SyncOptions) {
alreadySynced,
opts.message,
opts,
globalDeps,
codebases
globalDeps
)
) {
continue;
@@ -1181,9 +958,6 @@ async function push(opts: GlobalOptions & SyncOptions) {
await Deno.writeTextFile(stateTarget, change.content);
}
} else if (change.name === "deleted") {
if (change.path.endsWith(".lock")) {
continue;
}
const typ = getTypeStrFromPath(change.path);
if (typ == "script") {
@@ -1231,7 +1005,7 @@ async function push(opts: GlobalOptions & SyncOptions) {
case "app":
await AppService.deleteApp({
workspace: workspaceId,
path: removeSuffix(change.path, ".app/app.json"),
path: removeSuffix(change.path, ".app.json"),
});
break;
case "schedule":

View File

@@ -111,8 +111,7 @@ export async function pushObj(
const typeEnding = getTypeStrFromPath(p);
if (typeEnding === "app") {
const appName = p.split(".app" + path.sep)[0];
await pushApp(workspace, appName, appName + ".app", message);
await pushApp(workspace, p, befObj, newObj, message);
} else if (typeEnding === "folder") {
await pushFolder(workspace, p, befObj, newObj);
} else if (typeEnding === "variable") {
@@ -172,9 +171,6 @@ export function getTypeStrFromPath(
if (p.includes(".flow" + path.sep)) {
return "flow";
}
if (p.includes(".app" + path.sep)) {
return "app";
}
const parsed = path.parse(p);
if (
parsed.ext == ".go" ||
@@ -183,8 +179,7 @@ export function getTypeStrFromPath(
parsed.ext == ".py" ||
parsed.ext == ".sql" ||
parsed.ext == ".gql" ||
parsed.ext == ".ps1" ||
parsed.ext == ".js"
parsed.ext == ".ps1"
) {
return "script";
}

View File

@@ -2,7 +2,7 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck This file is copied from a JS project, so it's not type-safe.
import { log, encodeHex } from "./deps.ts";
import { log } from "./deps.ts";
export function deepEqual<T>(a: T, b: T): boolean {
if (a === b) return true;
@@ -92,28 +92,3 @@ export function getHeaders(): Record<string, string> | undefined {
return undefined;
}
}
export async function digestDir(path: string) {
const hashes: string = [];
for await (const e of Deno.readDir(path)) {
const npath = path + "/" + e.name;
if (e.isFile) {
hashes.push(await generateHashFromBuffer(await Deno.readFile(npath)));
} else if (e.isDirectory && !e.isSymlink) {
hashes.push(await digestDir(npath));
}
}
return await generateHash(hashes.join(""));
}
export async function generateHash(content: string): Promise<string> {
const messageBuffer = new TextEncoder().encode(content);
return await generateHashFromBuffer(messageBuffer);
}
export async function generateHashFromBuffer(
content: BufferSource
): Promise<string> {
const hashBuffer = await crypto.subtle.digest("SHA-256", content);
return encodeHex(hashBuffer);
}

View File

@@ -242,9 +242,9 @@ export async function add(
)
);
const automateUsernameCreation: boolean =
((await SettingService.getGlobal({
(await SettingService.getGlobal({
key: "automate_username_creation",
})) as any) ?? false;
})) ?? false;
await WorkspaceService.createWorkspace({
requestBody: {
id: workspaceId,

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