feat: set instance settings from UI (#2314)

This commit is contained in:
Ruben Fiszel
2023-09-20 23:15:19 +02:00
committed by GitHub
parent ea6919d20b
commit 79c4ee5e6f
56 changed files with 1921 additions and 597 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ RUN apt-get update && apt-get install -y git libssl-dev pkg-config npm
RUN apt-get -y update \
&& apt-get install -y \
curl nodejs npm
curl nodejs
RUN rustup component add rustfmt
+16 -11
View File
@@ -1,17 +1,22 @@
Source code in this repository is variously licensed under the Apache License
Version 2.0 (see file ./LICENSE-APACHE), or the AGPLv3 License (see file ./LICENSE-AGPL)
Version 2.0 (see file ./LICENSE-APACHE), or the AGPLv3 License (see file
./LICENSE-AGPL)
Every file is under copyright (c) Windmill Labs, Inc 2022 unless otherwise specified.
Every file is under License AGPL unless otherwise specified
or belonging to one of the below cases:
Every file is under copyright (c) Windmill Labs, Inc 2022 unless otherwise
specified. Every file is under License AGPL unless otherwise specified or
belonging to one of the below cases:
The files under backend/ are AGPLv3 Licensed, except any snippets of code under the compile flag "enterprise". Those snippets and files are under a proprietary and commerccial license.
The files under frontend/ are AGPLv3 Licensed, except any snippers of code that require a positive license check to be activated. Those snippets and files are under a proprietary and commercial license.
Private and public forks MUST not include any of the above proprietary and commercial code. Windmill Labs, Inc. provide tools to clean the codebase from those snippets upon demand.
The files under python-client/ deno-client/ go-client/ are Apache 2.0 Licensed.
The files under backend/ are AGPLv3 Licensed, except any snippets of code under
the compile flag "enterprise". Those snippets and files are under a proprietary
and commercial license. The files under frontend/ are AGPLv3 Licensed, except
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/ are
Apache 2.0 Licensed.
The openapi files, including the OpenFlow spec is Apache 2.0 Licensed.
All third party components incorporated into the Windmill Software are licensed under the
original license provided by the owner of the applicable component.
All third party components incorporated into the Windmill Software are licensed
under the original license provided by the owner of the applicable component.
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM worker_group_config WHERE name = $1 RETURNING name",
"query": "DELETE FROM config WHERE name = $1 RETURNING name",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
false
]
},
"hash": "210b7fa50246d9b8fd1a24ade787bad6882c86e9422b715844aa92b67ed05174"
"hash": "0d407b9e18a6ee7f4be4fb017e7ed278f2070a939ad8a0680cedd0216d53505a"
}
@@ -37,6 +37,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -67,6 +67,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -46,6 +46,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -28,6 +28,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT config FROM config WHERE name = 'server'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "config",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
true
]
},
"hash": "8ca76fcadc659d7069b97930cdde2d5200f3cef9dedb83b76c8a6a433d2a4de3"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO worker_group_config (name, config) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET config = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Jsonb"
]
},
"nullable": []
},
"hash": "903f2f62f3829274f5dfa0cb1ebbb9e132d310d19ca744531a42ca2c7ac56f56"
}
@@ -60,6 +60,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO config (name, config) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET config = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Jsonb"
]
},
"nullable": []
},
"hash": "af00c212f509076e37538be52f582ba09e47db50ba93af322649ccddbb05cc49"
}
@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT config FROM worker_group_config WHERE name = $1",
"query": "SELECT config FROM config WHERE name = $1",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
true
]
},
"hash": "6c0136f7965f1e01620a7d5efd7edbc78f0bf3f815676d8b343152bfce2dfc4c"
"hash": "bbd4136dfcfac26296fb6c52be1a74fd4874f8f74d9f06c018db8867af18e429"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM worker_group_config",
"query": "SELECT * FROM config WHERE name LIKE 'worker__%'",
"describe": {
"columns": [
{
@@ -22,5 +22,5 @@
true
]
},
"hash": "7998b23eb72f5967a0fd376fa1015bf29ba5150cd59732288ceb72ce9cecf987"
"hash": "ce9e56ff451bae10af2c396352f5f93f78658e57b79dc5295553cacc328eb2b7"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM config WHERE name = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "config",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "d233e07d19e8e339e1378c1bfc5d78d592c00ffb6f42c3d072f56305b40e50f9"
}
+75 -88
View File
@@ -64,9 +64,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.0"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0"
checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783"
dependencies = [
"memchr",
]
@@ -204,7 +204,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -334,7 +334,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -411,7 +411,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -582,19 +582,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "bigdecimal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "454bca3db10617b88b566f205ed190aedb0e0e6dd4cad61d3988a72e8c5594cb"
dependencies = [
"autocfg",
"libm",
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "bincode"
version = "1.3.3"
@@ -623,7 +610,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.37",
"syn 2.0.34",
"which",
]
@@ -644,7 +631,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -992,9 +979,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.4"
version = "4.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136"
checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1002,9 +989,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.4.4"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56"
checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
dependencies = [
"anstream",
"anstyle",
@@ -1021,7 +1008,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -1328,7 +1315,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -1350,7 +1337,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core 0.20.3",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -1408,7 +1395,7 @@ checksum = "3c65c2ffdafc1564565200967edc4851c7b55422d3913466688907efd05ea26f"
dependencies = [
"deno-proc-macro-rules-macros",
"proc-macro2",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -1420,7 +1407,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -1540,7 +1527,7 @@ dependencies = [
"strum",
"strum_macros",
"syn 1.0.109",
"syn 2.0.37",
"syn 2.0.34",
"thiserror",
]
@@ -1761,9 +1748,9 @@ dependencies = [
[[package]]
name = "dyn-clone"
version = "1.0.14"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd"
checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555"
[[package]]
name = "dyn-iter"
@@ -1950,7 +1937,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"swc_macros_common",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -1978,7 +1965,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e"
dependencies = [
"frunk_proc_macro_helpers",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -1990,7 +1977,7 @@ dependencies = [
"frunk_core",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -2002,7 +1989,7 @@ dependencies = [
"frunk_core",
"frunk_proc_macro_helpers",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -2103,7 +2090,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -2376,9 +2363,9 @@ dependencies = [
[[package]]
name = "hermit-abi"
version = "0.3.3"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hex"
@@ -2634,7 +2621,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -3162,7 +3149,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
"termcolor",
"thiserror",
]
@@ -3207,7 +3194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57349d5a326b437989b6ee4dc8f2f34b0cc131202748414712a8e7d98952fc8c"
dependencies = [
"base64 0.21.4",
"bigdecimal 0.3.1",
"bigdecimal",
"bindgen 0.68.1",
"bitflags 2.4.0",
"bitvec",
@@ -3429,7 +3416,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -3688,7 +3675,7 @@ dependencies = [
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -3726,7 +3713,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -3798,7 +3785,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -3907,7 +3894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d"
dependencies = [
"proc-macro2",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -4027,7 +4014,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"syn 2.0.37",
"syn 2.0.34",
"thiserror",
"typify",
"unicode-ident",
@@ -4047,7 +4034,7 @@ dependencies = [
"serde_json",
"serde_tokenstream",
"serde_yaml",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -4494,7 +4481,7 @@ dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.37",
"syn 2.0.34",
"walkdir",
]
@@ -4767,9 +4754,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.15"
version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c"
checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161"
dependencies = [
"chrono",
"dyn-clone",
@@ -4781,9 +4768,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.15"
version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737"
dependencies = [
"proc-macro2",
"quote",
@@ -4932,7 +4919,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -5008,7 +4995,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -5337,7 +5324,7 @@ checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53"
dependencies = [
"ahash 0.8.3",
"atoi",
"bigdecimal 0.3.1",
"bigdecimal",
"byteorder",
"bytes",
"chrono",
@@ -5422,7 +5409,7 @@ checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482"
dependencies = [
"atoi",
"base64 0.21.4",
"bigdecimal 0.3.1",
"bigdecimal",
"bitflags 2.4.0",
"byteorder",
"bytes",
@@ -5467,7 +5454,7 @@ checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e"
dependencies = [
"atoi",
"base64 0.21.4",
"bigdecimal 0.3.1",
"bigdecimal",
"bitflags 2.4.0",
"byteorder",
"chrono",
@@ -5587,7 +5574,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -5626,7 +5613,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -5708,7 +5695,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -5757,7 +5744,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -5840,7 +5827,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -5944,7 +5931,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -5956,7 +5943,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -5980,7 +5967,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -5996,9 +5983,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.37"
version = "2.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
checksum = "88ec6cdb6a4c16306eccf52ccd8d492e4ab64705a15a5016acb205251001bf72"
dependencies = [
"proc-macro2",
"quote",
@@ -6043,9 +6030,9 @@ dependencies = [
[[package]]
name = "termcolor"
version = "1.3.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
@@ -6076,7 +6063,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -6180,7 +6167,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -6433,7 +6420,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
]
[[package]]
@@ -6571,7 +6558,7 @@ dependencies = [
"regress",
"schemars",
"serde_json",
"syn 2.0.37",
"syn 2.0.34",
"thiserror",
"unicode-ident",
]
@@ -6588,7 +6575,7 @@ dependencies = [
"serde",
"serde_json",
"serde_tokenstream",
"syn 2.0.37",
"syn 2.0.34",
"typify-impl",
]
@@ -6716,9 +6703,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"
version = "0.1.11"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode-xid"
@@ -6906,7 +6893,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
"wasm-bindgen-shared",
]
@@ -6940,7 +6927,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"syn 2.0.34",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -7151,7 +7138,7 @@ dependencies = [
"hex",
"hmac",
"hyper",
"itertools 0.11.0",
"itertools 0.10.5",
"lazy_static",
"magic-crypt",
"mail-send",
@@ -7229,7 +7216,7 @@ dependencies = [
"hex",
"hmac",
"hyper",
"itertools 0.11.0",
"itertools 0.10.5",
"lazy_static",
"prometheus",
"rand 0.8.5",
@@ -7272,7 +7259,7 @@ version = "1.175.0"
dependencies = [
"anyhow",
"gosyn",
"itertools 0.11.0",
"itertools 0.10.5",
"lazy_static",
"regex",
"windmill-parser",
@@ -7294,7 +7281,7 @@ name = "windmill-parser-py"
version = "1.175.0"
dependencies = [
"anyhow",
"itertools 0.11.0",
"itertools 0.10.5",
"rustpython-parser",
"serde_json",
"windmill-parser",
@@ -7306,7 +7293,7 @@ version = "1.175.0"
dependencies = [
"anyhow",
"async-recursion",
"itertools 0.11.0",
"itertools 0.10.5",
"lazy_static",
"phf 0.11.2",
"regex",
@@ -7369,14 +7356,14 @@ version = "1.175.0"
dependencies = [
"anyhow",
"async-recursion",
"bigdecimal 0.4.1",
"bigdecimal",
"chrono",
"chrono-tz",
"cron",
"futures-core",
"hex",
"hmac",
"itertools 0.11.0",
"itertools 0.10.5",
"lazy_static",
"prometheus",
"reqwest",
@@ -7414,7 +7401,7 @@ dependencies = [
"futures",
"gcp_auth",
"git-version",
"itertools 0.11.0",
"itertools 0.10.5",
"jsonwebtoken",
"lazy_static",
"mysql_async",
+2
View File
@@ -64,6 +64,8 @@ once_cell.workspace = true
prometheus.workspace = true
uuid.workspace = true
gethostname.workspace = true
serde_json.workspace = true
serde.workspace = true
[dev-dependencies]
serde_json.workspace = true
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE IF exists config RENAME TO worker_group_config ;
@@ -0,0 +1,29 @@
-- Add up migration script here
ALTER TABLE IF exists worker_group_config RENAME TO config;
UPDATE config SET name = 'worker__' || name;
CREATE FUNCTION "notify_config_change" ()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('notify_config_change', NEW.name::text);
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
CREATE TRIGGER "notify_config_change"
AFTER INSERT OR UPDATE ON "config"
FOR EACH ROW
EXECUTE FUNCTION "notify_config_change" ();
CREATE FUNCTION "notify_global_setting_change" ()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('notify_global_setting_change', NEW.name::text);
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
CREATE TRIGGER "notify_global_setting_change"
AFTER INSERT OR UPDATE ON "global_settings"
FOR EACH ROW
EXECUTE FUNCTION "notify_global_setting_change" ();
+116 -33
View File
@@ -9,7 +9,7 @@
use gethostname::gethostname;
use git_version::git_version;
use rand::Rng;
use sqlx::{Pool, Postgres};
use sqlx::{postgres::PgListener, Pool, Postgres};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
@@ -19,8 +19,16 @@ use tokio::{
fs::{metadata, DirBuilder},
sync::RwLock,
};
use windmill_api::{LICENSE_KEY, OAUTH_CLIENTS, SMTP_CLIENT};
use windmill_common::{global_settings::ENV_SETTINGS, utils::rd_string, METRICS_ADDR};
use windmill_api::LICENSE_KEY;
use windmill_common::{
global_settings::{
BASE_URL_SETTING, CUSTOM_TAGS_SETTING, ENV_SETTINGS, OAUTH_SETTING,
REQUEST_SIZE_LIMIT_SETTING, RETENTION_PERIOD_SECS_SETTING,
},
utils::rd_string,
worker::{reload_custom_tags_setting, WORKER_GROUP},
METRICS_ADDR,
};
use windmill_worker::{
BUN_CACHE_DIR, BUN_TMP_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM,
DENO_TMP_CACHE_DIR, DENO_TMP_CACHE_DIR_DEPS, DENO_TMP_CACHE_DIR_NPM, GO_BIN_CACHE_DIR,
@@ -28,7 +36,10 @@ use windmill_worker::{
PIP_CACHE_DIR, ROOT_TMP_CACHE_DIR, TAR_PIP_TMP_CACHE_DIR,
};
use crate::monitor::monitor_db;
use crate::monitor::{
initial_load, monitor_db, reload_base_url_setting, reload_retention_period_setting,
reload_server_config, reload_worker_config,
};
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
const DEFAULT_NUM_WORKERS: usize = 1;
@@ -128,16 +139,6 @@ Windmill Community Edition {GIT_VERSION}
display_config(&ENV_SETTINGS);
tracing::info!("Loading OAuth providers...: {:#?}", *OAUTH_CLIENTS);
if let Some(ref smtp) = *SMTP_CLIENT {
tracing::info!("Smtp client defined. Testing connection...");
if let Err(e) = smtp.connect().await {
tracing::error!("Failed to connect to smtp server: {}", e);
} else {
tracing::info!("Smtp client connected.");
}
}
let worker_mode = num_workers > 0;
if server_mode || worker_mode {
@@ -152,15 +153,9 @@ Windmill Community Edition {GIT_VERSION}
// since it's only on server mode, the port is statically defined
let base_internal_url: String = format!("http://localhost:{}", port.to_string());
monitor_db(
&db,
tx.clone(),
&base_internal_url,
rsmq.clone(),
worker_mode,
server_mode,
)
.await;
monitor_db(&db, &base_internal_url, rsmq.clone(), server_mode).await;
initial_load(&db, tx.clone(), worker_mode, server_mode).await;
if std::env::var("BASE_INTERNAL_URL").is_ok() {
tracing::warn!("BASE_INTERNAL_URL is now unecessary and ignored, you can remove it.");
@@ -206,18 +201,106 @@ Windmill Community Edition {GIT_VERSION}
tokio::spawn(async move {
//monitor_db is applied at start, no need to apply it twice
tokio::time::sleep(Duration::from_secs(rd_delay)).await;
let mut listener = match PgListener::connect_with(&db).await {
Ok(l) => l,
Err(e) => {
tracing::error!(error = %e, "Could not connect to database");
return;
}
};
if let Err(e) = listener
.listen_all(vec!["notify_config_change", "notify_global_setting_change"])
.await
{
tracing::error!(error = %e, "Could not listen to database");
return;
}
loop {
monitor_db(
&db,
tx.clone(),
&base_internal_url,
rsmq.clone(),
worker_mode,
server_mode,
)
.await;
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(30)) => (),
_ = tokio::time::sleep(Duration::from_secs(30)) => {
monitor_db(
&db,
&base_internal_url,
rsmq.clone(),
server_mode,
)
.await;
},
notification = listener.recv() => {
match notification {
Ok(n) => {
tracing::info!("Received new pg notification: {n:?}");
match n.channel() {
"notify_config_change" => {
tracing::info!("Config change detected");
match n.payload() {
"server" if server_mode => {
tracing::info!("Server config change detected");
reload_server_config(&db).await;
},
a@ _ if worker_mode && a == format!("worker__{}", *WORKER_GROUP) => {
tracing::info!("Worker config change detected");
reload_worker_config(&db, tx.clone(), true).await;
},
_ => {
()
}
}
},
"notify_global_setting_change" => {
tracing::info!("Global setting change detected");
match n.payload() {
BASE_URL_SETTING => {
tracing::info!("Base URL setting change detected");
if let Err(e) = reload_base_url_setting(&db).await {
tracing::error!(error = %e, "Could not reload base url setting");
}
},
OAUTH_SETTING => {
tracing::info!("OAuth setting change detected");
if let Err(e) = reload_base_url_setting(&db).await {
tracing::error!(error = %e, "Could not reload oauth setting");
}
},
CUSTOM_TAGS_SETTING => {
tracing::info!("Custom tags setting change detected");
if let Err(e) = reload_custom_tags_setting(&db).await {
tracing::error!(error = %e, "Could not reload custom tags setting");
}
},
RETENTION_PERIOD_SECS_SETTING => {
tracing::info!("Retention period setting change detected");
reload_retention_period_setting(&db).await
},
REQUEST_SIZE_LIMIT_SETTING => {
tracing::info!("Request limit size change detected, killing server expecting to be restarted");
// we wait a bit randomly to avoid having all servers shutdown at same time
let rd_delay = rand::thread_rng().gen_range(0..4);
tokio::time::sleep(Duration::from_secs(rd_delay)).await;
if let Err(e) = tx.send(()) {
tracing::error!(error = %e, "Could not send killpill to server");
}
}
a @_ => {
tracing::info!("Unrecognized Global Setting Change Payload: {:?}", a);
}
}
},
_ => {
tracing::warn!("Unknown notification received");
continue;
}
}
},
Err(e) => {
tracing::error!(error = %e, "Could not receive notification");
continue;
}
};
},
_ = rx.recv() => {
println!("received killpill for monitor job");
break;
+305 -26
View File
@@ -1,12 +1,26 @@
use std::{collections::HashMap, fmt::Display, ops::Mul, str::FromStr, sync::Arc};
use once_cell::sync::OnceCell;
use serde::de::DeserializeOwned;
use sqlx::{Pool, Postgres};
use tokio::{join, sync::mpsc};
use tokio::{
join,
sync::{mpsc, RwLock},
};
use uuid::Uuid;
use windmill_api::{
oauth2::{build_oauth_clients, OAuthClient},
DEFAULT_BODY_LIMIT, IS_SECURE, OAUTH_CLIENTS, REQUEST_SIZE_LIMIT,
};
use windmill_common::{
error,
global_settings::{
BASE_URL_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING, RETENTION_PERIOD_SECS_SETTING,
},
jobs::{JobKind, QueuedJob},
worker::{load_worker_config, reload_custom_tags_setting, WORKER_CONFIG},
METRICS_ENABLED,
server::load_server_config,
worker::{load_worker_config, reload_custom_tags_setting, SERVER_CONFIG, WORKER_CONFIG},
BASE_URL, DB, METRICS_ENABLED,
};
use windmill_worker::{
create_token_for_owner, handle_job_error, AuthedClient, SCRIPT_TOKEN_EXPIRY,
@@ -40,29 +54,20 @@ lazy_static::lazy_static! {
"Number of jobs in the queue",
&["tag"]
).unwrap();
static ref JOB_RETENTION_SECS: Arc<RwLock<i64>> = Arc::new(RwLock::new(0));
}
pub async fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
pub async fn initial_load(
db: &Pool<Postgres>,
tx: tokio::sync::broadcast::Sender<()>,
base_internal_url: &str,
rsmq: Option<R>,
worker_mode: bool,
server_mode: bool,
) {
let zombie_jobs_f = async {
if server_mode {
handle_zombie_jobs(db, base_internal_url, rsmq.clone()).await;
}
};
let expired_items_f = async {
if server_mode {
windmill_api::delete_expired_items(&db).await;
}
};
let reload_worker_config_f = async {
if worker_mode {
reload_worker_config(&db, tx).await;
reload_worker_config(&db, tx, false).await;
}
};
let reload_custom_tags_f = async {
@@ -72,18 +77,206 @@ pub async fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
}
}
};
let reload_base_url_f = async {
if server_mode {
if let Err(e) = reload_base_url_setting(db).await {
tracing::error!("Error reloading custom tags: {:?}", e)
}
}
};
let reload_server_config_f = async {
if server_mode {
reload_server_config(&db).await;
}
};
let reload_retention_period_f = async {
if server_mode {
reload_retention_period_setting(&db).await;
}
};
let reload_request_size_f = async {
if server_mode {
reload_request_size(&db).await;
}
};
join!(
reload_worker_config_f,
reload_server_config_f,
reload_custom_tags_f,
reload_request_size_f,
reload_base_url_f,
reload_retention_period_f
);
}
pub async fn delete_expired_items(db: &DB) -> () {
let tokens_deleted_r: std::result::Result<Vec<String>, _> = sqlx::query_scalar(
"DELETE FROM token WHERE expiration <= now()
RETURNING concat(substring(token for 10), '*****')",
)
.fetch_all(db)
.await;
match tokens_deleted_r {
Ok(tokens) => {
if tokens.len() > 0 {
tracing::info!("deleted {} tokens: {:?}", tokens.len(), tokens)
}
}
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
}
let pip_resolution_r = sqlx::query_scalar!(
"DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash",
)
.fetch_all(db)
.await;
match pip_resolution_r {
Ok(res) => {
if res.len() > 0 {
tracing::info!("deleted {} pip_resolution: {:?}", res.len(), res)
}
}
Err(e) => tracing::error!("Error deleting pip_resolution: {}", e.to_string()),
}
let deleted_cache = sqlx::query_scalar!(
"DELETE FROM resource WHERE resource_type = 'cache' AND to_timestamp((value->>'expire')::int) < now() RETURNING path",
)
.fetch_all(db)
.await;
match deleted_cache {
Ok(res) => {
if res.len() > 0 {
tracing::info!("deleted {} cache resource: {:?}", res.len(), res)
}
}
Err(e) => tracing::error!("Error deleting cache resource {}", e.to_string()),
}
let job_retention_secs = *JOB_RETENTION_SECS.read().await;
if job_retention_secs > 0 {
let deleted_jobs = sqlx::query_scalar!(
"DELETE FROM completed_job WHERE started_at + ((duration_ms/1000 + $1) || ' s')::interval <= now() RETURNING id",
job_retention_secs
)
.fetch_all(db)
.await;
match deleted_jobs {
Ok(deleted_jobs) => {
if deleted_jobs.len() > 0 {
tracing::info!(
"deleted {} jobs completed JOB_RETENTION_SECS {} ago: {:?}",
deleted_jobs.len(),
job_retention_secs,
deleted_jobs,
)
}
}
Err(e) => tracing::error!("Error deleting jobs: {}", e.to_string()),
}
}
}
pub async fn reload_retention_period_setting(db: &DB) {
if let Err(e) = reload_setting(
db,
RETENTION_PERIOD_SECS_SETTING,
"JOB_RETENTION_SECS",
60 * 60 * 24 * 60,
JOB_RETENTION_SECS.clone(),
|x| x,
)
.await
{
tracing::error!("Error reloading retention period: {:?}", e)
}
}
pub async fn reload_request_size(db: &DB) {
if let Err(e) = reload_setting(
db,
REQUEST_SIZE_LIMIT_SETTING,
"REQUEST_SIZE_LIMIT",
DEFAULT_BODY_LIMIT,
REQUEST_SIZE_LIMIT.clone(),
|x| x.mul(1024 * 1024),
)
.await
{
tracing::error!("Error reloading retention period: {:?}", e)
}
}
pub async fn reload_setting<T: FromStr + DeserializeOwned + Display>(
db: &DB,
setting_name: &str,
std_env_var: &str,
default: T,
lock: Arc<RwLock<T>>,
transformer: fn(T) -> T,
) -> error::Result<()> {
let q = sqlx::query!(
"SELECT value FROM global_settings WHERE name = $1",
setting_name
)
.fetch_optional(db)
.await?;
let mut value = std::env::var(std_env_var)
.ok()
.and_then(|x| x.parse::<T>().ok())
.unwrap_or(default);
if let Some(q) = q {
if let Ok(v) = serde_json::from_value::<T>(q.value.clone()) {
tracing::info!(
"Loaded setting {setting_name} from db config: {:#?}",
&q.value
);
value = transformer(v);
} else {
tracing::error!("Could not parse {setting_name} found: {:#?}", &q.value);
}
};
{
let mut l = lock.write().await;
*l = value;
}
Ok(())
}
pub async fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
db: &Pool<Postgres>,
base_internal_url: &str,
rsmq: Option<R>,
server_mode: bool,
) {
let zombie_jobs_f = async {
if server_mode {
handle_zombie_jobs(db, base_internal_url, rsmq.clone()).await;
}
};
let expired_items_f = async {
if server_mode {
delete_expired_items(&db).await;
}
};
let expose_queue_metrics_f = async {
if *METRICS_ENABLED && server_mode {
expose_queue_metrics(&db).await;
}
};
join!(
expired_items_f,
zombie_jobs_f,
reload_worker_config_f,
reload_custom_tags_f,
expose_queue_metrics_f
);
join!(expired_items_f, zombie_jobs_f, expose_queue_metrics_f);
}
pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
@@ -104,7 +297,22 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
}
}
pub async fn reload_worker_config(db: &Pool<Postgres>, tx: tokio::sync::broadcast::Sender<()>) {
pub async fn reload_server_config(db: &Pool<Postgres>) {
let config = load_server_config(&db).await;
if let Err(e) = config {
tracing::error!("Error reloading server config: {:?}", e)
} else {
let mut wc = SERVER_CONFIG.write().await;
tracing::info!("Reloading server config...");
*wc = config.unwrap()
}
}
pub async fn reload_worker_config(
db: &DB,
tx: tokio::sync::broadcast::Sender<()>,
kill_if_change: bool,
) {
let config = load_worker_config(&db).await;
if let Err(e) = config {
tracing::error!("Error reloading worker config: {:?}", e)
@@ -112,7 +320,7 @@ pub async fn reload_worker_config(db: &Pool<Postgres>, tx: tokio::sync::broadcas
let wc = WORKER_CONFIG.read().await;
let config = config.unwrap();
if *wc != config {
if (*wc).dedicated_worker != config.dedicated_worker {
if kill_if_change && (*wc).dedicated_worker != config.dedicated_worker {
tracing::info!("Dedicated worker config changed, sending killpill. Expecting to be restarted by supervisor.");
let _ = tx.send(());
}
@@ -125,6 +333,77 @@ pub async fn reload_worker_config(db: &Pool<Postgres>, tx: tokio::sync::broadcas
}
}
pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> {
let q_base_url = sqlx::query!(
"SELECT value FROM global_settings WHERE name = $1",
BASE_URL_SETTING
)
.fetch_optional(db)
.await?;
let base_url = if let Some(q) = q_base_url {
if let Ok(v) = serde_json::from_value::<String>(q.value.clone()) {
v
} else {
tracing::error!(
"Could not parse base_url setting as a string, found: {:#?}",
&q.value
);
std::env::var("BASE_URL")
.ok()
.unwrap_or_else(|| "http://localhost".to_string())
}
} else {
std::env::var("BASE_URL")
.ok()
.unwrap_or_else(|| "http://localhost".to_string())
};
let q_oauth = sqlx::query!(
"SELECT value FROM global_settings WHERE name = $1",
OAUTH_SETTING
)
.fetch_optional(db)
.await?;
let oauths = if let Some(q) = q_oauth {
if let Ok(v) =
serde_json::from_value::<Option<HashMap<String, OAuthClient>>>(q.value.clone())
{
v
} else {
tracing::error!(
"Could not parse oauth setting as a json, found: {:#?}",
&q.value
);
None
}
} else {
None
};
let is_secure = base_url.starts_with("https://");
{
let mut l = OAUTH_CLIENTS.write().await;
*l = build_oauth_clients(&base_url, oauths)
.map_err(|e| tracing::error!("Error building oauth clients (is the oauth.json mounted and in correct format? Use '{}' as minimal oauth.json): {}", "{}", e))
.unwrap();
}
{
let mut l = BASE_URL.write().await;
*l = base_url
}
{
let mut l = IS_SECURE.write().await;
*l = is_secure;
}
Ok(())
}
async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
db: &Pool<Postgres>,
base_internal_url: &str,
+77 -13
View File
@@ -575,6 +575,55 @@ paths:
application/json:
schema: {}
/settings/test_smtp:
post:
summary: test smtp
operationId: testSmtp
tags:
- setting
requestBody:
description: test smtp payload
required: true
content:
application/json:
schema:
type: object
properties:
to:
type: string
smtp:
type: object
properties:
host:
type: string
username:
type: string
password:
type: string
port:
type: integer
from:
type: string
tls_implicit:
type: boolean
required:
- host
- username
- password
- port
- from
- tls_implicit
required:
- to
- smtp
responses:
"200":
description: status
content:
text/plain::
schema:
type: string
/users/email:
get:
summary: get current user email (if logged in)
@@ -5268,15 +5317,15 @@ paths:
items:
$ref: "#/components/schemas/WorkerPing"
/workers/list_worker_groups:
/configs/list_worker_groups:
get:
summary: list workers
summary: list worker groups
operationId: listWorkerGroups
tags:
- worker
- config
responses:
"200":
description: a list of workers
description: a list of worker group configs
content:
application/json:
schema:
@@ -5291,12 +5340,27 @@ paths:
- name
- config
/workers/worker_group/{name}:
post:
summary: Update Worker Group
operationId: updateWorkerGroup
/configs/get/{name}:
get:
summary: get config
operationId: get config
tags:
- worker
- config
parameters:
- $ref: "#/components/parameters/Name"
responses:
"200":
description: a config
content:
application/json:
schema: {}
/configs/update/{name}:
post:
summary: Update config
operationId: updateConfig
tags:
- config
parameters:
- $ref: "#/components/parameters/Name"
requestBody:
@@ -5313,15 +5377,15 @@ paths:
schema:
type: string
delete:
summary: Delete Worker Group
operationId: deleteWorkerGroup
summary: Delete Config
operationId: deleteConfig
tags:
- worker
- config
parameters:
- $ref: "#/components/parameters/Name"
responses:
"200":
description: Delete a worker group
description: Delete config
content:
text/plain:
schema:
+114
View File
@@ -0,0 +1,114 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use axum::{
extract::{Extension, Path},
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use windmill_common::{
db::UserDB,
error::{self},
DB,
};
use crate::{db::ApiAuthed, utils::require_super_admin};
pub fn global_service() -> Router {
Router::new()
.route("/list_worker_groups", get(list_worker_groups))
.route("/update/:name", post(update_config).delete(delete_config))
.route("/get/:name", get(get_config))
}
#[derive(Serialize, Deserialize, FromRow)]
struct Config {
name: String,
config: serde_json::Value,
}
async fn list_worker_groups(
authed: ApiAuthed,
Extension(db): Extension<DB>,
) -> error::JsonResult<Vec<Config>> {
require_super_admin(&db, &authed.email).await?;
let rows = sqlx::query_as!(Config, "SELECT * FROM config WHERE name LIKE 'worker__%'")
.fetch_all(&db)
.await?;
Ok(Json(rows))
}
async fn get_config(
authed: ApiAuthed,
Path(name): Path<String>,
Extension(db): Extension<DB>,
) -> error::JsonResult<Option<serde_json::Value>> {
require_super_admin(&db, &authed.email).await?;
let config = sqlx::query_as!(Config, "SELECT * FROM config WHERE name = $1", name)
.fetch_optional(&db)
.await?
.map(|c| c.config);
Ok(Json(config))
}
async fn update_config(
Path(name): Path<String>,
Extension(db): Extension<DB>,
authed: ApiAuthed,
Json(config): Json<serde_json::Value>,
) -> error::Result<String> {
require_super_admin(&db, &authed.email).await?;
#[cfg(not(feature = "enterprise"))]
if name.starts_with("worker__") {
return Err(error::Error::BadRequest(
"Worker groups configurable from UI available only in the enterprise version"
.to_string(),
));
}
sqlx::query!(
"INSERT INTO config (name, config) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET config = $2",
&name,
config
)
.execute(&db)
.await?;
Ok(format!("Updated config {name}"))
}
async fn delete_config(
Path(name): Path<String>,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
authed: ApiAuthed,
) -> error::Result<String> {
let tx = user_db.begin(&authed).await?;
require_super_admin(&db, &authed.email).await?;
tx.commit().await?;
let deleted = sqlx::query!("DELETE FROM config WHERE name = $1 RETURNING name", name)
.fetch_all(&db)
.await?;
if deleted.len() == 0 {
return Err(error::Error::NotFound(format!(
"Config {name} not found",
name = name
)));
}
Ok(format!("Deleted config {name}"))
}
+9 -40
View File
@@ -13,7 +13,6 @@ use crate::{
users::{check_scopes, require_owner_of_path, OptAuthed},
utils::require_super_admin,
variables::get_workspace_key,
BASE_URL,
};
use anyhow::Context;
use axum::{
@@ -34,7 +33,8 @@ use sqlx::{query_scalar, types::Uuid, FromRow, Postgres, Transaction};
use tower_http::cors::{Any, CorsLayer};
use urlencoding::encode;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::worker::CUSTOM_TAGS_PER_WORKSPACE;
use windmill_common::worker::{CUSTOM_TAGS_PER_WORKSPACE, SERVER_CONFIG};
use windmill_common::BASE_URL;
use windmill_common::{
db::UserDB,
error::{self, to_anyhow, Error},
@@ -1296,7 +1296,8 @@ pub async fn get_resume_urls(
.map(|x| format!("?approver={}", encode(x)))
.unwrap_or_else(String::new);
let base_url = BASE_URL.as_str();
let base_url_str = BASE_URL.read().await.clone();
let base_url = base_url_str.as_str();
let res = ResumeUrls {
approvalPage: format!(
"{base_url}/approve/{w_id}/{job_id}/{resume_id}/{signature}{approver}"
@@ -1775,11 +1776,11 @@ impl Drop for Guard {
async fn run_wait_result<T>(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
timeout: i32,
uuid: Uuid,
Path((w_id, _)): Path<(String, T)>,
) -> error::JsonResult<serde_json::Value> {
let mut result;
let timeout = SERVER_CONFIG.read().await.timeout_wait_result.clone();
let timeout_ms = if timeout <= 0 {
2000
} else {
@@ -1863,10 +1864,6 @@ lazy_static::lazy_static! {
pub static ref QUEUE_LIMIT_WAIT_RESULT: Option<i64> = std::env::var("QUEUE_LIMIT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse().ok());
pub static ref TIMEOUT_WAIT_RESULT: i32 = std::env::var("TIMEOUT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(20);
pub static ref WAIT_RESULT_FAST_POLL_INTERVAL_MS: u64 = std::env::var("WAIT_RESULT_FAST_POLL_INTERVAL_MS")
.ok()
.and_then(|x| x.parse().ok())
@@ -1937,14 +1934,7 @@ pub async fn run_wait_result_job_by_path_get(
.await?;
tx.commit().await?;
run_wait_result(
authed,
Extension(user_db),
*TIMEOUT_WAIT_RESULT,
uuid,
Path((w_id, script_path)),
)
.await
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_path))).await
}
pub async fn run_wait_result_flow_by_path_get(
@@ -2108,14 +2098,7 @@ async fn run_wait_result_script_by_path_internal(
.await?;
tx.commit().await?;
run_wait_result(
authed,
Extension(user_db),
*TIMEOUT_WAIT_RESULT,
uuid,
Path((w_id, script_path)),
)
.await
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_path))).await
}
pub async fn run_wait_result_script_by_hash(
@@ -2180,14 +2163,7 @@ pub async fn run_wait_result_script_by_hash(
.await?;
tx.commit().await?;
run_wait_result(
authed,
Extension(user_db),
*TIMEOUT_WAIT_RESULT,
uuid,
Path((w_id, script_hash)),
)
.await
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_hash))).await
}
pub async fn openai_sync_flow_by_path(
@@ -2287,14 +2263,7 @@ async fn run_wait_result_flow_by_path_internal(
.await?;
tx.commit().await?;
run_wait_result(
authed,
Extension(user_db),
*TIMEOUT_WAIT_RESULT,
uuid,
Path((w_id, flow_path)),
)
.await
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, flow_path))).await
}
async fn run_preview_job(
+20 -57
View File
@@ -12,7 +12,7 @@ use crate::saml::{SamlSsoLogin, ServiceProviderExt};
use crate::scim::has_scim_token;
use crate::tracing_init::MyOnFailure;
use crate::{
oauth2::{build_oauth_clients, SlackVerifier},
oauth2::SlackVerifier,
tracing_init::{MyMakeSpan, MyOnResponse},
users::OptAuthed,
webhook_util::WebhookShared,
@@ -24,9 +24,10 @@ use axum::{middleware::from_extractor, routing::get, Extension, Router};
use db::DB;
use git_version::git_version;
use hyper::{http, Method};
use mail_send::SmtpClientBuilder;
use reqwest::Client;
use std::collections::HashMap;
use std::{net::SocketAddr, sync::Arc};
use tokio::sync::RwLock;
use tower::ServiceBuilder;
use tower_cookies::CookieManagerLayer;
use tower_http::{
@@ -36,12 +37,14 @@ use tower_http::{
use windmill_common::db::UserDB;
use windmill_common::utils::rd_string;
use windmill_common::worker::ALL_TAGS;
use windmill_common::BASE_URL;
use windmill_common::error::AppError;
mod apps;
mod audit;
mod capture;
mod configs;
mod db;
mod drafts;
mod favorite;
@@ -51,7 +54,7 @@ mod granular_acls;
mod groups;
mod inputs;
pub mod jobs;
mod oauth2;
pub mod oauth2;
mod openai;
mod raw_apps;
mod resources;
@@ -72,16 +75,11 @@ mod workspaces;
pub const GIT_VERSION: &str =
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
pub use users::delete_expired_items;
pub const DEFAULT_BODY_LIMIT: usize = 2097152 * 100; // 200MB
pub const DEFAULT_BODY_LIMIT: usize = 2097152; // 2MB
lazy_static::lazy_static! {
pub static ref BASE_URL: String = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
pub static ref REQUEST_SIZE_LIMIT: usize = std::env::var("REQUEST_SIZE_LIMIT")
.ok()
.and_then(|x| x.parse::<usize>().ok())
.unwrap_or(DEFAULT_BODY_LIMIT);
pub static ref REQUEST_SIZE_LIMIT: Arc<RwLock<usize>> = Arc::new(RwLock::new(DEFAULT_BODY_LIMIT));
pub static ref COOKIE_DOMAIN: Option<String> = std::env::var("COOKIE_DOMAIN").ok();
@@ -90,61 +88,22 @@ lazy_static::lazy_static! {
.ok()
.map(|x| SlackVerifier::new(x).unwrap());
static ref IS_SECURE: bool = BASE_URL.starts_with("https://");
pub static ref IS_SECURE: Arc<RwLock<bool>> = Arc::new(RwLock::new(false));
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.danger_accept_invalid_certs(std::env::var("ACCEPT_INVALID_CERTS").is_ok())
.build().unwrap();
pub static ref OAUTH_CLIENTS: AllClients = build_oauth_clients(&BASE_URL)
.map_err(|e| tracing::error!("Error building oauth clients (is the oauth.json mounted and in correct format? Use '{}' as minimal oauth.json): {}", "{}", e))
.unwrap();
pub static ref SMTP_CLIENT: Option<SmtpClientBuilder<String>> = {
let smtp = parse_smtp();
if let Some(smtp) = smtp {
match smtp {
Ok(smtp) => Some(smtp),
Err(e) => {
tracing::error!("SMTP is not configured correctly, emails will not be sent: {}", e);
None
}
}
} else {
tracing::warn!("SMTP is not configured, emails will not be sent");
None
}
};
pub static ref SMTP_FROM: String = std::env::var("SMTP_FROM").unwrap_or_else(|_| "noreply@getwindmill.com".to_string());
pub static ref OAUTH_CLIENTS: Arc<RwLock<AllClients>> = Arc::new(RwLock::new(AllClients {
logins: HashMap::new(),
connects: HashMap::new(),
slack: None
}));
pub static ref LICENSE_KEY: Option<String> = std::env::var("LICENSE_KEY").ok();
}
pub fn parse_smtp() -> Option<windmill_common::error::Result<SmtpClientBuilder<String>>> {
let username = std::env::var("SMTP_USERNAME").ok();
let port = std::env::var("SMTP_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(587);
let password = std::env::var("SMTP_PASSWORD").ok();
let host = std::env::var("SMTP_HOST").ok();
let tls_implicit = std::env::var("SMTP_TLS_IMPLICIT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(false);
if username.is_some() && password.is_some() && host.is_some() {
let smtp = SmtpClientBuilder::new(host.unwrap(), port)
.implicit_tls(tls_implicit)
.credentials((username.unwrap(), password.unwrap()));
Some(Ok(smtp))
} else {
None
}
}
pub async fn run_server(
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
@@ -185,7 +144,9 @@ pub async fn run_server(
.layer(Extension(auth_cache.clone()))
.layer(CookieManagerLayer::new())
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())))
.layer(DefaultBodyLimit::max(*REQUEST_SIZE_LIMIT));
.layer(DefaultBodyLimit::max(
REQUEST_SIZE_LIMIT.read().await.clone(),
));
let cors = CorsLayer::new()
.allow_methods([Method::GET, Method::POST])
@@ -239,6 +200,7 @@ pub async fn run_server(
.nest("/settings", settings::global_service())
.nest("/jobs", jobs::global_root_service())
.nest("/workers", workers::global_service())
.nest("/configs", configs::global_service())
.nest("/scripts", scripts::global_service())
.nest("/groups", groups::global_service())
.nest("/flows", flows::global_service())
@@ -287,15 +249,16 @@ pub async fn run_server(
let instance_name = rd_string(5);
tracing::info!(addr = %addr.to_string(), instance = %instance_name, "server started listening");
let server = axum::Server::bind(&addr).serve(app.into_make_service());
let port = server.local_addr().port();
tracing::info!(
instance = %instance_name,
"server started on port={} and addr={}",
port,
server.local_addr().ip()
);
port_tx
.send(server.local_addr().port())
.expect("Failed to send port");
+66 -35
View File
@@ -81,7 +81,7 @@ pub fn workspaced_service() -> Router {
.route("/connect_slack_callback", post(connect_slack_callback))
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ClientWithScopes {
client: OClient,
scopes: Vec<String>,
@@ -120,7 +120,10 @@ pub struct AllClients {
pub slack: Option<OClient>,
}
pub fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
pub fn build_oauth_clients(
base_url: &str,
oauths_from_config: Option<HashMap<String, OAuthClient>>,
) -> anyhow::Result<AllClients> {
let connect_configs = serde_json::from_str::<HashMap<String, OAuthConfig>>(include_str!(
"../../oauth_connect.json"
))?;
@@ -128,26 +131,36 @@ pub fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
"../../oauth_login.json"
))?;
let path = "./oauth.json";
let content: String = if let Ok(e) = std::env::var("OAUTH_JSON_AS_BASE64") {
str::from_utf8(
&base64::engine::general_purpose::STANDARD
.decode(e)
.map_err(to_anyhow)?,
)?
.to_string()
} else if std::path::Path::new(path).exists() {
fs::read_to_string(path).map_err(to_anyhow)?
let oauths = if let Some(oauths) = oauths_from_config {
oauths
} else {
tracing::warn!("oauth.json not found, no OAuth clients loaded");
return Ok(AllClients { logins: HashMap::new(), connects: HashMap::new(), slack: None });
};
let path = "./oauth.json";
let content: String = if let Ok(e) = std::env::var("OAUTH_JSON_AS_BASE64") {
str::from_utf8(
&base64::engine::general_purpose::STANDARD
.decode(e)
.map_err(to_anyhow)?,
)?
.to_string()
} else if std::path::Path::new(path).exists() {
fs::read_to_string(path).map_err(to_anyhow)?
} else {
tracing::warn!("oauth.json not found, no OAuth clients loaded");
return Ok(AllClients {
logins: HashMap::new(),
connects: HashMap::new(),
slack: None,
});
};
if content.is_empty() {
tracing::warn!("oauth.json is empty, no OAuth clients loaded");
return Ok(AllClients { logins: HashMap::new(), connects: HashMap::new(), slack: None });
};
let oauths: HashMap<String, OAuthClient> =
if content.is_empty() {
tracing::warn!("oauth.json is empty, no OAuth clients loaded");
return Ok(AllClients {
logins: HashMap::new(),
connects: HashMap::new(),
slack: None,
});
};
match serde_json::from_str::<HashMap<String, OAuthClient>>(&content) {
Ok(clients) => clients,
Err(e) => {
@@ -156,7 +169,8 @@ pub fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
}
}
.into_iter()
.collect();
.collect()
};
tracing::info!("OAuth loaded clients: {}", oauths.keys().join(", "));
@@ -314,7 +328,7 @@ async fn connect(
cookies: Cookies,
) -> error::Result<Redirect> {
let mut query = query.clone();
let connects = &OAUTH_CLIENTS.connects;
let connects = &OAUTH_CLIENTS.read().await.connects;
let scopes = query
.get("scopes")
.map(|x| x.split('+').map(|x| x.to_owned()).collect());
@@ -330,7 +344,7 @@ async fn connect(
cookies,
scopes,
extra_params,
*IS_SECURE,
IS_SECURE.read().await.clone(),
)
}
@@ -406,6 +420,8 @@ struct Logins {
async fn list_logins(Extension(sso): Extension<Arc<SamlSsoLogin>>) -> error::JsonResult<Logins> {
Ok(Json(Logins {
oauth: OAUTH_CLIENTS
.read()
.await
.logins
.keys()
.map(|x| x.to_owned())
@@ -421,7 +437,7 @@ struct ScopesAndParams {
}
async fn list_connects() -> error::JsonResult<HashMap<String, ScopesAndParams>> {
Ok(Json(
(&OAUTH_CLIENTS.connects)
(&OAUTH_CLIENTS.read().await.connects)
.into_iter()
.map(|(k, v)| {
(
@@ -438,6 +454,8 @@ async fn list_connects() -> error::JsonResult<HashMap<String, ScopesAndParams>>
async fn connect_slack(cookies: Cookies) -> error::Result<Redirect> {
let mut client = OAUTH_CLIENTS
.read()
.await
.slack
.as_ref()
.ok_or_else(|| {
@@ -453,7 +471,7 @@ async fn connect_slack(cookies: Cookies) -> error::Result<Redirect> {
client.add_scope("commands");
let url = client.authorize_url(&state);
set_cookie(&state, cookies, *IS_SECURE);
set_cookie(&state, cookies, IS_SECURE.read().await.clone());
Ok(Redirect::to(url.as_str()))
}
@@ -496,8 +514,15 @@ async fn disconnect_slack(
}
async fn login(Path(client_name): Path<String>, cookies: Cookies) -> error::Result<Redirect> {
let clients = &OAUTH_CLIENTS.logins;
oauth_redirect(clients, client_name, cookies, None, None, *IS_SECURE)
let clients = &OAUTH_CLIENTS.read().await.logins;
oauth_redirect(
clients,
client_name,
cookies,
None,
None,
IS_SECURE.read().await.clone(),
)
}
#[derive(Deserialize)]
@@ -532,6 +557,8 @@ pub async fn _refresh_token<'c>(
.await?;
let account = not_found_if_none(account, "Account", &id.to_string())?;
let client = (&OAUTH_CLIENTS
.read()
.await
.connects
.get(&account.client)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?
@@ -622,11 +649,10 @@ async fn connect_callback(
Path(client_name): Path<String>,
Json(callback): Json<OAuthCallback>,
) -> error::JsonResult<TokenResponse> {
let client_w_scopes = OAUTH_CLIENTS
.connects
let connects = &OAUTH_CLIENTS.read().await.connects;
let client_w_scopes = connects
.get(&client_name)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?;
let client = client_w_scopes.client.to_owned();
let extra_params = client_w_scopes.extra_params_callback.clone();
let token_response =
@@ -644,6 +670,8 @@ async fn connect_slack_callback(
Json(callback): Json<OAuthCallback>,
) -> error::Result<String> {
let client = OAUTH_CLIENTS
.read()
.await
.slack
.as_ref()
.ok_or_else(|| {
@@ -858,7 +886,7 @@ async fn slack_command(
None,
)
.await?;
let url = BASE_URL.to_owned();
let url = BASE_URL.read().await.clone();
tx.commit().await?;
return Ok(format!(
"Job launched. See details at {url}/run/{uuid}?workspace={}",
@@ -880,10 +908,13 @@ async fn login_callback(
Extension(webhook): Extension<WebhookShared>,
Json(callback): Json<OAuthCallback>,
) -> error::Result<String> {
let client_w_config = &OAUTH_CLIENTS
.logins
.get(&client_name)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?;
let client_w_config = {
let clients = OAUTH_CLIENTS.read().await.logins.clone();
clients
.get(&client_name)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?
.clone()
};
let client = client_w_config.client.to_owned();
let token_res =
exchange_code::<TokenResponse>(callback, &cookies, client, &HTTP_CLIENT, None).await;
+1 -1
View File
@@ -71,7 +71,7 @@ pub async fn build_sp_extension() -> anyhow::Result<(ServiceProviderExt, SamlSso
..ContactPerson::default()
})
.idp_metadata(idp_metadata)
.acs_url(format!("{}/api/saml/acs", *BASE_URL))
.acs_url(format!("{}/api/saml/acs", BASE_URL.read().await.clone()))
.build()?;
tracing::info!("SAML Configured, sso login link at: {:?}", url);
+43 -2
View File
@@ -6,6 +6,8 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::time::Duration;
use crate::{
db::{ApiAuthed, DB},
utils::require_super_admin,
@@ -17,18 +19,57 @@ use axum::{
Json, Router,
};
use mail_send::{mail_builder::MessageBuilder, SmtpClientBuilder};
use serde::Deserialize;
use tokio::time::timeout;
use windmill_common::{
error::{self, JsonResult},
error::{self, to_anyhow, JsonResult},
global_settings::ENV_SETTINGS,
server::Smtp,
};
pub fn global_service() -> Router {
Router::new()
.route("/local", get(get_local_settings))
.route("/envs", get(get_local_settings))
.route(
"/global/:key",
post(set_global_setting).get(get_global_setting),
)
.route("/test_smtp", post(test_email))
}
#[derive(Deserialize)]
pub struct TestEmail {
pub to: String,
pub smtp: Smtp,
}
pub async fn test_email(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Json(test_email): Json<TestEmail>,
) -> error::Result<String> {
require_super_admin(&db, &authed.email).await?;
let smtp = test_email.smtp;
let to = test_email.to;
let client = SmtpClientBuilder::new(smtp.host, smtp.port)
.implicit_tls(smtp.tls_implicit)
.credentials((smtp.username, smtp.password));
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())
}
pub async fn get_local_settings(
+1
View File
@@ -68,6 +68,7 @@ async fn list_contextual_variables(
Some("u/user/triggering_flow_path".to_string()),
Some("c".to_string()),
)
.await
.to_vec(),
))
}
+4 -84
View File
@@ -7,7 +7,7 @@
*/
use axum::{
extract::{Extension, Path, Query},
extract::{Extension, Query},
routing::get,
Json, Router,
};
@@ -16,27 +16,17 @@ use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use windmill_common::{
db::UserDB,
error::{self, JsonResult},
error::JsonResult,
utils::{paginate, Pagination},
worker::ALL_TAGS,
DB,
};
use crate::{db::ApiAuthed, utils::require_super_admin};
use crate::db::ApiAuthed;
pub fn global_service() -> Router {
use axum::routing::post;
let router = Router::new()
Router::new()
.route("/list", get(list_worker_pings))
.route("/custom_tags", get(get_custom_tags))
.route("/list_worker_groups", get(get_worker_groups))
.route(
"/worker_group/:name",
post(update_worker_group).delete(delete_worker_group),
);
return router;
}
#[derive(FromRow, Serialize, Deserialize)]
@@ -80,73 +70,3 @@ async fn list_worker_pings(
async fn get_custom_tags() -> Json<Vec<String>> {
Json(ALL_TAGS.read().await.clone().into())
}
#[derive(Serialize, Deserialize, FromRow)]
struct WorkerGroup {
name: String,
config: serde_json::Value,
}
async fn get_worker_groups(
authed: ApiAuthed,
Extension(db): Extension<DB>,
) -> error::JsonResult<Vec<WorkerGroup>> {
require_super_admin(&db, &authed.email).await?;
let rows = sqlx::query_as!(WorkerGroup, "SELECT * FROM worker_group_config")
.fetch_all(&db)
.await?;
Ok(Json(rows))
}
#[cfg(feature = "enterprise")]
async fn update_worker_group(
Path(name): Path<String>,
Extension(db): Extension<DB>,
authed: ApiAuthed,
Json(config): Json<serde_json::Value>,
) -> error::Result<String> {
require_super_admin(&db, &authed.email).await?;
sqlx::query!(
"INSERT INTO worker_group_config (name, config) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET config = $2",
&name,
config
)
.execute(&db)
.await?;
Ok(format!("Updated worker group {name}"))
}
#[cfg(not(feature = "enterprise"))]
async fn update_worker_group() -> String {
"Worker groups available only in enterprise version".to_string()
}
async fn delete_worker_group(
Path(name): Path<String>,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
authed: ApiAuthed,
) -> error::Result<String> {
let tx = user_db.begin(&authed).await?;
require_super_admin(&db, &authed.email).await?;
tx.commit().await?;
let deleted = sqlx::query!(
"DELETE FROM worker_group_config WHERE name = $1 RETURNING name",
name,
)
.fetch_all(&db)
.await?;
if deleted.len() == 0 {
return Err(error::Error::NotFound(format!(
"Worker group {name} not found",
name = name
)));
}
Ok(format!("Deleted worker group {name}"))
}
+6 -5
View File
@@ -284,8 +284,9 @@ async fn stripe_checkout(
require_admin(authed.is_admin, &authed.username)?;
let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY"));
let success_rd = format!("{}/workspace_settings/checkout?success=true", *BASE_URL);
let failure_rd = format!("{}/workspace_settings/checkout?success=false", *BASE_URL);
let base_url = BASE_URL.read().await.clone();
let success_rd = format!("{}/workspace_settings/checkout?success=true", base_url);
let failure_rd = format!("{}/workspace_settings/checkout?success=false", base_url);
let checkout_session = {
let mut params = stripe::CreateCheckoutSession::new(&failure_rd, &success_rd);
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
@@ -332,7 +333,7 @@ async fn stripe_portal(
.await?
.ok_or_else(|| Error::InternalErr(format!("no customer id for workspace {}", w_id)))?;
let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY"));
let success_rd = format!("{}/workspace_settings?tab=premium", *BASE_URL);
let success_rd = format!("{}/workspace_settings?tab=premium", BASE_URL.read().await.clone());
let portal_session = {
let customer_id = CustomerId::from_str(&customer_id).unwrap();
let mut params = stripe::CreateBillingPortalSession::new(customer_id);
@@ -1221,7 +1222,7 @@ async fn invite_user(
"You have been granted access to Windmill's workspace {w_id}
If you do not have an account on {}, login with SSO or ask an admin to create an account for you.",
*BASE_URL
BASE_URL.read().await.clone()
),
&nu.email,
);
@@ -1285,7 +1286,7 @@ async fn add_user(
"You have been granted access to Windmill's workspace {w_id} by {email}
If you do not have an account on {}, login with SSO or ask an admin to create an account for you.",
*BASE_URL
BASE_URL.read().await.clone()
),
&nu.email,
);
+1 -1
View File
@@ -45,4 +45,4 @@ tracing-subscriber = { workspace = true, optional = true }
lazy_static.workspace = true
tracing-flame = { version = "^0", optional = true }
itertools.workspace = true
regex.workspace = true
regex.workspace = true
@@ -1,5 +1,9 @@
pub const WORKER_S3_BUCKET_SYNC: &str = "worker_s3_bucket_sync";
pub const CUSTOM_TAGS_SETTING: &str = "custom_tags";
pub const BASE_URL_SETTING: &str = "base_url";
pub const OAUTH_SETTING: &str = "oauths";
pub const RETENTION_PERIOD_SECS_SETTING: &str = "retention_period_secs";
pub const REQUEST_SIZE_LIMIT_SETTING: &str = "request_size_limit_mb";
pub const ENV_SETTINGS: [&str; 54] = [
"DISABLE_NSJAIL",
+4 -2
View File
@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::net::SocketAddr;
use std::{net::SocketAddr, sync::Arc};
use error::Error;
use scripts::ScriptLang;
@@ -24,6 +24,7 @@ pub mod more_serde;
pub mod oauth2;
pub mod schedule;
pub mod scripts;
pub mod server;
pub mod users;
pub mod utils;
pub mod variables;
@@ -47,7 +48,7 @@ lazy_static::lazy_static! {
.flatten()
.flatten();
pub static ref METRICS_ENABLED: bool = METRICS_ADDR.is_some();
pub static ref BASE_URL: String = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
pub static ref BASE_URL: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
pub static ref IS_READY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
}
@@ -78,6 +79,7 @@ pub async fn shutdown_signal(
Ok(())
}
use tokio::sync::RwLock;
#[cfg(feature = "prometheus")]
use tokio::task::JoinHandle;
+112
View File
@@ -0,0 +1,112 @@
use serde::{Deserialize, Serialize};
use crate::{error, DB};
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct Smtp {
pub host: String,
pub username: String,
pub password: String,
pub port: u16,
pub from: String,
pub tls_implicit: bool,
}
#[derive(Serialize, Deserialize, PartialEq)]
pub struct ServerConfigOpt {
pub smtp_host: Option<String>,
pub smtp_username: Option<String>,
pub smtp_password: Option<String>,
pub smtp_port: Option<u16>,
pub smtp_from: Option<String>,
pub smtp_tls_implicit: Option<bool>,
pub timeout_wait_result: Option<u64>,
}
pub async fn load_server_config(db: &DB) -> error::Result<ServerConfig> {
let config: ServerConfigOpt =
sqlx::query_scalar!("SELECT config FROM config WHERE name = 'server'",)
.fetch_optional(db)
.await?
.flatten()
.map(|x| serde_json::from_value(x).ok())
.flatten()
.unwrap_or_default();
let config_smtp = if let (Some(host), Some(username), Some(password)) =
(config.smtp_host, config.smtp_username, config.smtp_password)
{
Some(Smtp {
host,
username,
password,
tls_implicit: config.smtp_tls_implicit.unwrap_or(false),
port: config.smtp_port.unwrap_or(587),
from: config
.smtp_from
.unwrap_or_else(|| "noreply@getwindmill.com".to_string()),
})
} else {
None
};
let smtp = config_smtp.or(
if let (Some(host), Some(username), Some(password)) = (
std::env::var("SMTP_HOST").ok(),
std::env::var("SMTP_USERNAME").ok(),
std::env::var("SMTP_PASSWORD").ok(),
) {
Some(Smtp {
host,
username,
password,
tls_implicit: std::env::var("SMTP_TLS_IMPLICIT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(false),
port: std::env::var("SMTP_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(587),
from: std::env::var("SMTP_FROM")
.unwrap_or_else(|_| "noreply@getwindmill.com".to_string()),
})
} else {
None
},
);
if smtp.is_none() {
tracing::warn!("SMTP not configured");
}
Ok(ServerConfig {
smtp,
timeout_wait_result: config
.timeout_wait_result
.ok_or(
std::env::var("TIMEOUT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse::<u64>().ok()),
)
.unwrap_or(600),
})
}
impl Default for ServerConfigOpt {
fn default() -> Self {
Self {
smtp_from: None,
smtp_host: None,
smtp_password: None,
smtp_port: None,
smtp_tls_implicit: None,
smtp_username: None,
timeout_wait_result: Default::default(),
}
}
}
#[derive(PartialEq, Clone, Debug)]
pub struct ServerConfig {
pub smtp: Option<Smtp>,
pub timeout_wait_result: u64,
}
+2 -2
View File
@@ -61,7 +61,7 @@ pub struct CreateVariable {
pub is_oauth: Option<bool>,
}
pub fn get_reserved_variables(
pub async fn get_reserved_variables(
w_id: &str,
token: &str,
email: &str,
@@ -163,7 +163,7 @@ pub fn get_reserved_variables(
},
ContextualVariable {
name: "WM_BASE_URL".to_string(),
value: BASE_URL.clone(),
value: BASE_URL.read().await.clone(),
description: "base url of this instance".to_string(),
},
ContextualVariable {
+6 -7
View File
@@ -5,7 +5,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use crate::{error, global_settings::CUSTOM_TAGS_SETTING, DB};
use crate::{error, global_settings::CUSTOM_TAGS_SETTING, server::ServerConfig, DB};
lazy_static::lazy_static! {
pub static ref WORKER_GROUP: String = std::env::var("WORKER_GROUP").unwrap_or_else(|_| "default".to_string());
@@ -35,6 +35,9 @@ lazy_static::lazy_static! {
dedicated_worker: Default::default(),
}));
pub static ref SERVER_CONFIG: Arc<RwLock<ServerConfig>> = Arc::new(RwLock::new(ServerConfig { smtp: Default::default(), timeout_wait_result: 20 }));
pub static ref CLOUD_HOSTED: bool = std::env::var("CLOUD_HOSTED").is_ok();
@@ -97,10 +100,6 @@ pub async fn reload_custom_tags_setting(db: &DB) -> error::Result<()> {
.concat();
}
Ok(())
// pub static ref CUSTOM_TAGS_PER_WORKSPACE: (Vec<String>, HashMap<String, Vec<String>>) = process_custom_tags(std::env::var("CUSTOM_TAGS")
// .ok());
// pub static ref ALL_TAGS: Vec<String> = [CUSTOM_TAGS_PER_WORKSPACE.0.clone(), CUSTOM_TAGS_PER_WORKSPACE.1.keys().map(|x| x.to_string()).collect_vec()].concat();
}
fn process_custom_tags(tags: Vec<String>) -> (Vec<String>, HashMap<String, Vec<String>>) {
@@ -144,8 +143,8 @@ pub async fn update_ping(worker_instance: &str, worker_name: &str, ip: &str, db:
pub async fn load_worker_config(db: &DB) -> error::Result<WorkerConfig> {
let mut config: WorkerConfigOpt = sqlx::query_scalar!(
"SELECT config FROM worker_group_config WHERE name = $1",
*WORKER_GROUP
"SELECT config FROM config WHERE name = $1",
format!("worker__{}", *WORKER_GROUP)
)
.fetch_optional(db)
.await?
+3 -1
View File
@@ -481,6 +481,7 @@ pub async fn start_worker(
None,
None,
)
.await
.to_vec();
let context_envs = build_envs_map(context);
if let Some(reqs) = requirements_o {
@@ -603,7 +604,8 @@ for await (const chunk of Bun.stdin.stream()) {{
None,
None,
None,
);
)
.await;
let _ = write_file(
&job_dir,
+1
View File
@@ -192,6 +192,7 @@ pub async fn get_reserved_variables(
job.schedule_path.clone(),
job.flow_step_id.clone(),
)
.await
.to_vec();
Ok(build_envs_map(variables))
+7 -3
View File
@@ -41,8 +41,12 @@ lazy_static::lazy_static! {
}
fn get_common_deno_proc_envs(token: &str, base_internal_url: &str) -> HashMap<String, String> {
let hostname_base = BASE_URL.split("://").last().unwrap_or("localhost");
async fn get_common_deno_proc_envs(
token: &str,
base_internal_url: &str,
) -> HashMap<String, String> {
let hostname = BASE_URL.read().await.clone();
let hostname_base = hostname.split("://").last().unwrap_or("localhost");
let hostname_internal = base_internal_url.split("://").last().unwrap_or("localhost");
let deno_auth_tokens_base = DENO_AUTH_TOKENS.as_str();
let deno_auth_tokens =
@@ -259,7 +263,7 @@ run().catch(async (e) => {{
write_import_map_f
)?;
let common_deno_proc_envs = get_common_deno_proc_envs(&token, base_internal_url);
let common_deno_proc_envs = get_common_deno_proc_envs(&token, base_internal_url).await;
//do not cache local dependencies
let reload = format!("--reload={base_internal_url}");
@@ -0,0 +1,372 @@
<script lang="ts">
import type { Setting, SettingStorage } from './instanceSettings'
import { Button, Tab, TabContent, Tabs } from '$lib/components/common'
import { ConfigService, SettingService } from '$lib/gen'
import Toggle from '$lib/components/Toggle.svelte'
import SecondsInput from '$lib/components/common/seconds/SecondsInput.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { sendUserToast } from '$lib/toast'
import OAuthSetting from '$lib/components/OAuthSetting.svelte'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { deepEqual } from 'fast-equals'
import OktaSetting from './OktaSetting.svelte'
import CloseButton from './common/CloseButton.svelte'
import KeycloakSetting from './KeycloakSetting.svelte'
import Alert from './common/alert/Alert.svelte'
import { isCloudHosted } from '$lib/cloud'
import { createEventDispatcher } from 'svelte'
export const settings: Record<string, Setting[]> = {
Core: [
{
label: 'Base Url',
description: 'Public base url of the instance',
key: 'base_url',
fieldType: 'text',
placeholder: 'https://windmill.com',
storage: 'setting'
},
{
label: 'Request Size Limit In MB',
description: 'Maximum size of HTTP requests in MB.',
cloudonly: true,
key: 'request_size_limit_mb',
fieldType: 'number',
placeholder: '50',
storage: 'setting'
},
{
label: 'Retention Period in secs',
key: 'retention_period_secs',
description: 'How long to keep the jobs data in the database.',
fieldType: 'seconds',
placeholder: '60',
storage: 'setting',
cloudonly: true
},
{
label: 'Max Timeout for sync endpoints',
key: 'timeout_wait_result',
cloudonly: false,
fieldType: 'seconds',
placeholder: '60',
storage: 'config'
}
],
SMTP: [
{
label: 'Host',
key: 'smtp_host',
fieldType: 'text',
placeholder: 'smtp.gmail.com',
storage: 'config'
},
{
label: 'Port',
key: 'smtp_port',
fieldType: 'number',
placeholder: '587',
storage: 'config'
},
{
label: 'Username',
key: 'smtp_username',
fieldType: 'text',
placeholder: 'ruben@windmill.dev',
storage: 'config'
},
{
label: 'Password',
key: 'smtp_password',
fieldType: 'password',
storage: 'config'
},
{
label: 'From Address',
key: 'smtp_from',
placeholder: 'noreply@windmill.dev',
fieldType: 'email',
storage: 'config'
},
{
label: 'Implicit TLS',
key: 'smtp_tls_implicit',
fieldType: 'boolean',
storage: 'config'
}
]
}
let values: Record<string, any> = {}
let initialOauths: Record<string, any> = {}
let serverConfig = {}
let initialValues: Record<string, any> = {}
loadSettings()
async function loadSettings() {
try {
serverConfig = (await ConfigService.getConfig({ name: 'server' })) ?? {}
} catch (e) {
console.log("Sever config not found, assuming it's first setup")
}
async function getValue(key: string, storage: SettingStorage) {
if (storage == 'setting') {
return SettingService.getGlobal({ key })
} else if (storage == 'config') {
return serverConfig[key]
}
}
initialOauths = (await SettingService.getGlobal({ key: 'oauths' })) ?? {}
oauths = { ...initialOauths }
initialValues = Object.fromEntries(
(
await Promise.all(
Object.entries(settings).map(
async ([_, y]) =>
await Promise.all(y.map(async (x) => [x.key, await getValue(x.key, x.storage)]))
)
)
).flat()
)
values = { ...initialValues }
if (values['retention_period_secs'] == undefined) {
values['retention_period_secs'] = 60 * 60 * 24 * 60
}
}
async function saveSettings() {
if (values) {
const allSettings = Object.values(settings).flatMap((x) => Object.entries(x))
const newServerConfig = Object.fromEntries(
allSettings
.filter((x) => x[1].storage == 'config')
.map((x) => [x[1].key, values?.[x[1].key]])
)
if (!deepEqual(newServerConfig, serverConfig)) {
await ConfigService.updateConfig({
name: 'server',
requestBody: newServerConfig
})
serverConfig = { ...newServerConfig }
}
await Promise.all(
allSettings
.filter(
(x) =>
x[1].storage == 'setting' &&
!deepEqual(initialValues?.[x[1].key], values?.[x[1].key]) &&
values?.[x[1].key] != undefined &&
values?.[x[1].key] != null &&
values?.[x[1].key] != ''
)
.map(async ([_, x]) => {
await SettingService.setGlobal({ key: x.key, requestBody: { value: values?.[x.key] } })
})
)
initialValues = { ...values }
if (!deepEqual(initialOauths, oauths)) {
await SettingService.setGlobal({
key: 'oauths',
requestBody: {
value: oauths
}
})
initialOauths = { ...oauths }
}
} else {
console.error('Values not loaded')
}
}
const dispatch = createEventDispatcher()
let oauths: Record<string, any> = {}
let resourceName = ''
let tab: 'Core' | 'SMTP' | 'OAuth' = 'Core'
let to: string = ''
</script>
<div class="pb-8">
<Tabs bind:selected={tab}>
{#each Object.keys(settings) as category}
<Tab value={category}>{category}</Tab>
{/each}
<Tab value="oauth">SSO/OAuth</Tab>
<svelte:fragment slot="content">
<div class="pt-4" />
{#each Object.keys(settings) as category}
<TabContent value={category}>
{#if category == 'SMTP'}
<div class="text-secondary pb-4 text-xs"
>Setting SMTP unlock sending emails upon adding new users to the workspace or the
instance.</div
>
{/if}
<div>
<div class="flex-col flex gap-2 pb-4">
{#each settings[category] as setting}
{#if !setting.cloudonly || isCloudHosted()}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">{setting.label}</span>
{#if setting.description}
<span class="text-secondary text-xs">{setting.description}</span>
{/if}
{#if setting.tooltip}
<Tooltip>{setting.tooltip}</Tooltip>
{/if}
{#if values}
{#if setting.fieldType == 'text'}
<input
type="text"
placeholder={setting.placeholder}
bind:value={values[setting.key]}
/>
{:else if setting.fieldType == 'email'}
<input
type="email"
placeholder={setting.placeholder}
bind:value={values[setting.key]}
/>
{:else if setting.fieldType == 'number'}
<input
type="number"
placeholder={setting.placeholder}
bind:value={values[setting.key]}
/>
{:else if setting.fieldType == 'password'}
<input
type="password"
placeholder={setting.placeholder}
bind:value={values[setting.key]}
/>
{:else if setting.fieldType == 'boolean'}
<div>
<Toggle bind:checked={values[setting.key]} />
</div>
{:else if setting.fieldType == 'seconds'}
<div>
<SecondsInput bind:seconds={values[setting.key]} />
</div>
{/if}
{:else}
<input disabled placeholder="Loading..." />
{/if}
</label>
{/if}
{/each}
</div>
</div>
{#if category == 'SMTP'}
<div class="flex gap-4"
><input type="email" bind:value={to} placeholder="contact@windmill.dev" />
<Button
disabled={to == ''}
on:click={async () => {
await SettingService.testSmtp({
requestBody: {
to,
smtp: {
host: values['smtp_host'],
username: values['smtp_username'],
password: values['smtp_password'],
port: values['smtp_port'],
from: values['smtp_from'],
tls_implicit: values['smtp_tls_implicit']
}
}
})
sendUserToast('Test email sent')
}}>Test SMTP settings</Button
></div
>
{/if}
</TabContent>
{/each}
<TabContent value={'oauth'}>
<div>
<h4 class="pb-4">SSO</h4>
<Alert type="warning" title="Limited to 50 SSO users">
Without EE, the number of SSO users is limited to 50. SCIM/SAML is available on EE
</Alert>
<div class="flex flex-col gap-2 py-4">
<OAuthSetting name="google" bind:value={oauths['google']} />
<OAuthSetting name="microsoft" bind:value={oauths['microsoft']} />
<OktaSetting bind:value={oauths['okta']} />
<OAuthSetting name="github" bind:value={oauths['github']} />
<OAuthSetting name="gitlab" bind:value={oauths['gitlab']} />
<OAuthSetting name="jumpcloud" bind:value={oauths['jumpcloud']} />
<KeycloakSetting bind:value={oauths['keycloak']} />
</div>
<h4 class="py-4">OAuth</h4>
<OAuthSetting login={false} name="slack" bind:value={oauths['slack']} />
<div class="py-1" />
{#each Object.keys(oauths) as k}
{#if !['google', 'microsoft', 'github', 'gitlab', 'jumpcloud', 'okta', 'keycloak'].includes(k)}
{#if oauths[k]}
<div class="flex flex-col gap-2 pb-4">
<div class="flex flex-row items-center gap-2">
<label class="text-md font-medium text-gray-700">{k}</label>
<CloseButton
on:close={() => {
delete oauths[k]
oauths = { ...oauths }
}}
/>
</div>
<div class="p-2 border rounded">
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input type="text" placeholder="Client Id" bind:value={oauths[k]['id']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Secret</span>
<input
type="text"
placeholder="Client Secret"
bind:value={oauths[k]['secret']}
/>
</label>
</div>
</div>
{/if}
{/if}
{/each}
<div class="flex gap-2">
<input type="text" placeholder="slack" bind:value={resourceName} />
<Button
variant="border"
color="blue"
hover="yo"
size="sm"
endIcon={{ icon: faPlus }}
disabled={resourceName == ''}
on:click={() => {
oauths[resourceName] = { id: '', secret: '' }
resourceName = ''
}}
>
Add OAuth client
</Button>
</div>
</div>
</TabContent>
</svelte:fragment>
</Tabs>
</div>
<div class="py-4" />
<Button
on:click={async () => {
await saveSettings()
sendUserToast('Settings updated')
dispatch('save')
}}>Save</Button
>
<div class="pb-8" />
@@ -0,0 +1,64 @@
<script lang="ts">
import IconedResourceType from './IconedResourceType.svelte'
import Toggle from './Toggle.svelte'
export let value: any
$: enabled = value != undefined
let org = ''
$: changeOrg(org)
function changeOrg(org) {
if (value) {
value = {
...value,
connect_config: {
auth_url: `${org}/protocol/openid-connect/auth`,
token_url: `${org}/protocol/openid-connect/token`,
scopes: ['openid', 'offline_access']
},
login_config: {
auth_url: `${org}/protocol/openid-connect/auth`,
token_url: `${org}/protocol/openid-connect/token`,
userinfo_url: `${org}/protocol/openid-connect/userinfo`,
scopes: ['openid', 'offline_access']
}
}
}
}
</script>
<div class="flex flex-col gap-1">
<label class="text-sm font-medium text-gray-700 flex gap-4 items-center"
><div class="w-[120px]"><IconedResourceType name={'keycloak'} after={true} /></div><Toggle
checked={enabled}
on:change={(e) => {
if (e.detail) {
value = { id: '', secret: '' }
} else {
value = undefined
}
}}
/></label
>
{#if enabled}
<div class="border rounded p-2">
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Realm Url ({'REALM_URL/protocol/openid-connect/auth'})</span
>
<input type="text" placeholder="yourorg" bind:value={org} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input type="text" placeholder="Client Id" bind:value={value['id']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Secret </span>
<input type="text" placeholder="Client Secret" bind:value={value['secret']} />
</label>
</div>
{/if}
</div>
@@ -0,0 +1,85 @@
<script lang="ts">
import CollapseLink from './CollapseLink.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import Toggle from './Toggle.svelte'
export let name: string
export let value: any
export let login = true
$: if (value && value?.['allowed_domains'] == undefined) {
value = { ...(value ?? {}), allowed_domains: [] }
}
$: enabled = value != undefined
</script>
<div class="flex flex-col gap-1">
<label class="text-sm flex gap-4 items-center font-medium text-gray-700"
><div class="w-[120px]"><IconedResourceType {name} after={true} /></div><Toggle
checked={enabled}
on:change={(e) => {
if (e.detail) {
value = { id: '', secret: '' }
} else {
value = undefined
}
}}
/></label
>
{#if enabled}
<div class="p-2 rounded border">
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input type="text" placeholder="Client Id" bind:value={value['id']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Secret</span>
<input type="text" placeholder="Client Secret" bind:value={value['secret']} />
</label>
{#if login}
<label class="block pb-2">
{#if value['allowed_domains']}
<span class="text-primary font-semibold text-sm">Allowed domain</span>
<input
type="text"
placeholder="windmill.dev"
bind:value={value['allowed_domains'][0]}
/>
{/if}
</label>
{/if}
{#if name == 'google'}
<CollapseLink text="Instructions">
<div class="text-sm text-secondary border p-2">
Create a new OAuth 2.0 Client <a
href="https://console.cloud.google.com/apis/credentials"
target="_blank">in google console</a
>
and set the redirect URI to <code>BASE_URL/user/login_callback/google</code>
where BASE_URL is what you configured as core BASE_URL
</div>
</CollapseLink>
{:else if name == 'slack'}
<a class="text-xs" href="https://www.windmill.dev/docs/misc/setup_oauth#slack"
>Read more about Slack OAuth on the docs</a
>
{:else if name == 'microsoft'}
<CollapseLink text="Instructions">
<div class="text-sm text-secondary border p-2">
Create a new OAuth 2.0 Client <a
href="https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade"
target="_blank">in microsoft portal</a
>
and in the "Authentication" tab, set the redirect URI to
<code>BASE_URL/user/login_callback/microsoft</code>, the logout channel to
<code>BASE_URL/auth/logout</code>where BASE_URL is what you configured as core BASE_URL.
Also set "Accounts in any organizational directory (Any Microsoft Entra ID tenant -
Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)", you can restrict the
emails directly in windmill using the "allowed_domains" setting.
</div>
</CollapseLink>
{/if}
</div>
{/if}
</div>
@@ -0,0 +1,85 @@
<script lang="ts">
import CollapseLink from './CollapseLink.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
export let value: any
$: enabled = value != undefined
let org = ''
$: changeOrg(org)
function changeOrg(org) {
if (value) {
value = {
...value,
login_config: {
auth_url: `https://${org}.okta.com/oauth2/v1/authorize`,
token_url: `https://${org}.okta.com/oauth2/v1/token`,
userinfo_url: `https://${org}.okta.com/oauth2/v1/userinfo`,
scopes: ['openid', 'profile', 'email']
},
connect_config: {
auth_url: `https://${org}.okta.com/oauth2/v1/authorize`,
token_url: `https://${org}.okta.com/oauth2/v1/token`,
scopes: ['openid', 'profile', 'email']
}
}
}
}
</script>
<div class="flex flex-col gap-1">
<label class="text-sm font-medium text-gray-700 flex gap-4 items-center"
><div class="w-[120px]"><IconedResourceType name="okta" after={true} /></div><Toggle
checked={enabled}
on:change={(e) => {
if (e.detail) {
value = { id: '', secret: '' }
} else {
value = undefined
}
}}
/></label
>
{#if enabled}
<div class="p-2 rounded border">
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Org ({'https://<your org>.okta.com'})</span
>
<input type="text" placeholder="yourorg" bind:value={org} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Client Id <Tooltip
>Client credential from the client ID section of the okta service configuration</Tooltip
></span
>
<input type="text" placeholder="Client Id" bind:value={value['id']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Client Secret <Tooltip
>from the CLIENT SECRETS section of the okta service configuration</Tooltip
></span
>
<input type="text" placeholder="Client Secret" bind:value={value['secret']} />
</label>
<CollapseLink text="Instructions">
<div class="text-sm text-secondary border p-2">
From your Admin page, setup windmill using the service flow Create a new app integration
a. For "sign-in method" select "OIDC - Open ID Connect" b. For "application type" select
"Web Appliction" Select all of the following options for Grant type of "Client acting on
behalf of a user" Authorization Code Refresh Token Implicit (hybrid) Allow ID Token with
implicit grant type Allow Access Token with implicit grant type For Refresh Token, select
"Rotate token after every use" Under "LOGIN", set the following: "Sign-in redirect URIs"
BASE_URL/user/login_callback/okta "Sign-out redirect URIs" BASE_URL/auth/logout "Login
initiated by" App Only "Initiate login URI" BASE_URL/user/login
</div>
</CollapseLink>
</div>
{/if}
</div>
@@ -1,15 +1,16 @@
<script lang="ts">
import { UserService, GlobalUserInfo } from '$lib/gen'
import TableCustom from '$lib/components/TableCustom.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import InviteGlobalUser from '$lib/components/InviteGlobalUser.svelte'
import { Badge, Drawer, DrawerContent } from '$lib/components/common'
import { Badge, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
import { sendUserToast } from '$lib/toast'
import SearchItems from './SearchItems.svelte'
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import Version from './Version.svelte'
import Uptodate from './Uptodate.svelte'
import TabContent from './common/tabs/TabContent.svelte'
import InstanceSettings from './InstanceSettings.svelte'
let drawer: Drawer
let filter = ''
@@ -37,6 +38,8 @@
async function listUsers(): Promise<void> {
users = await UserService.listUsersAsSuperAdmin({ perPage: 100000 })
}
let tab: 'users' | 'settings' = 'users'
</script>
<SearchItems
@@ -46,76 +49,93 @@
f={(x) => x.email + ' ' + x.name + ' ' + x.company}
/>
<Drawer bind:this={drawer} on:open={listUsers} size="900px" on:clickAway={removeHash}>
<DrawerContent overflow_y={false} title="Superadmin Settings" on:close={closeDrawer}>
<Drawer bind:this={drawer} on:open={listUsers} size="1200px" on:clickAway={removeHash}>
<DrawerContent overflow_y={true} title="Superadmin Settings" on:close={closeDrawer}>
<div class="flex flex-col h-full">
<div>
<div class="text-xs pt-1 text-tertiary flex flex-col">
<div>Windmill <Version /></div><div><Uptodate /></div>
</div>
<PageHeader title="All global users" primary={false} />
<div class="p-2 border mb-4">
<InviteGlobalUser on:new={listUsers} />
</div>
<div class="pb-1" />
<input placeholder="Search users" bind:value={filter} class="input mt-1" />
<div class="flex justify-between">
<div class="text-xs pt-1 text-tertiary flex flex-col">
<div>Windmill <Version /></div>
</div>
<div><Uptodate /></div></div
>
</div>
<div class="mt-2 overflow-auto">
<TableCustom>
<tr slot="header-row" class="sticky top-0 bg-surface border-b">
<th>email</th>
<th>auth</th>
<th>name</th>
<th>company</th>
<th />
<th />
</tr>
<tbody slot="body" class="overflow-y-auto w-full h-full max-h-full">
{#if filteredUsers && users}
{#each filteredUsers as { email, super_admin, login_type, name, company } (email)}
<tr class="border">
<td>{email}</td>
<td>{login_type}</td>
<td><span class="break-words">{name ?? ''}</span></td>
<td><span class="break-words">{company ?? ''}</span></td>
<td
>{#if super_admin}<Badge>Superadmin</Badge>{/if}</td
>
<td>
<div class="flex flex-row gap-x-1">
<button
class="text-blue-500 whitespace-nowrap"
on:click={async () => {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: !super_admin
}
})
sendUserToast('User updated')
listUsers()
}}>make {super_admin ? 'non-superadmin' : 'superadmin'}</button
>
|
<button
class="text-red-500 whitespace-nowrap"
on:click={async () => {
await UserService.globalUserDelete({ email })
sendUserToast(`User ${email} removed`)
listUsers()
}}>remove</button
>
</div>
</td>
</tr>
{/each}
{/if}
</tbody>
</TableCustom>
<div class="pt-4 h-full">
<Tabs bind:selected={tab}>
<Tab value="users">Global Users</Tab>
<Tab value="settings">Instance Settings</Tab>
<svelte:fragment slot="content">
<div class="pt-4" />
<TabContent value="users">
<div class="h-full">
<div class="p-2 border mb-4">
<InviteGlobalUser on:new={listUsers} />
</div>
<div class="pb-1" />
<div>
<input placeholder="Search users" bind:value={filter} class="input mt-1" />
</div>
<div class="mt-2 overflow-auto">
<TableCustom>
<tr slot="header-row" class="sticky top-0 bg-surface border-b">
<th>email</th>
<th>auth</th>
<th>name</th>
<th>company</th>
<th />
<th />
</tr>
<tbody slot="body" class="overflow-y-auto w-full h-full max-h-full">
{#if filteredUsers && users}
{#each filteredUsers as { email, super_admin, login_type, name, company } (email)}
<tr class="border">
<td>{email}</td>
<td>{login_type}</td>
<td><span class="break-words">{name ?? ''}</span></td>
<td><span class="break-words">{company ?? ''}</span></td>
<td
>{#if super_admin}<Badge>Superadmin</Badge>{/if}</td
>
<td>
<div class="flex flex-row gap-x-1">
<button
class="text-blue-500 whitespace-nowrap"
on:click={async () => {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: !super_admin
}
})
sendUserToast('User updated')
listUsers()
}}>make {super_admin ? 'non-superadmin' : 'superadmin'}</button
>
|
<button
class="text-red-500 whitespace-nowrap"
on:click={async () => {
await UserService.globalUserDelete({ email })
sendUserToast(`User ${email} removed`)
listUsers()
}}>remove</button
>
</div>
</td>
</tr>
{/each}
{/if}
</tbody>
</TableCustom>
</div>
</div>
</TabContent>
<TabContent value="settings">
<div class="h-full overflow-auto"> <InstanceSettings /> </div>
</TabContent>
</svelte:fragment>
</Tabs>
</div>
</div>
</DrawerContent>
</div></DrawerContent
>
</Drawer>
@@ -3,7 +3,7 @@
import { Button, Popup } from './common'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import { WorkerService } from '$lib/gen'
import { ConfigService } from '$lib/gen'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import { createEventDispatcher } from 'svelte'
import { sendUserToast } from '$lib/toast'
@@ -49,7 +49,7 @@
const dispatch = createEventDispatcher()
async function deleteWorkerGroup() {
await WorkerService.deleteWorkerGroup({ name })
await ConfigService.deleteConfig({ name: 'worker__' + name })
dispatch('reload')
}
let dirty = false
@@ -120,10 +120,10 @@
</ToggleButtonGroup>
{#if selected == 'normal'}
{#if nconfig?.worker_tags != undefined}
<div class="flex flex-col gap-1 pb-2">
<div class="flex gap-3 gap-y-2 flex-wrap pb-2">
{#each nconfig.worker_tags as tag}
<div class="flex gap-1 items-center"
><div class="text-sm">- {tag}</div>
<div class="flex gap-0.5 items-center"
><div class="text-2xs p-1 rounded border text-primary">{tag}</div>
<button
class="z-10 rounded-full p-1 duration-200 hover:bg-gray-200"
aria-label="Remove item"
@@ -133,7 +133,7 @@
}
}}
>
<X size={14} />
<X size={12} />
</button></div
>
{/each}
@@ -221,10 +221,8 @@
color="dark"
size="xs"
on:click={async () => {
await WorkerService.updateWorkerGroup({ name, requestBody: nconfig })
sendUserToast(
'Setting configuration, it can take up to 30s to get propagated to all workers'
)
await ConfigService.updateConfig({ name: 'worker__' + name, requestBody: nconfig })
sendUserToast('Configuration set')
dispatch('reload')
}}
disabled={!dirty || !$enterpriseLicense}
@@ -44,7 +44,11 @@
<div>
{#if !hideDisplay}
<input
value={disabled ? '' : seconds + ' second' + (seconds === 1 ? '' : 's')}
value={seconds == null || seconds == undefined
? 'Not set'
: disabled
? ''
: seconds + ' second' + (seconds === 1 ? '' : 's')}
{disabled}
readonly
type="text"
@@ -254,71 +254,72 @@ done`
</ToggleButtonGroup>
</div>
</div>
{#key token}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Tabs selected="rest">
<Tab value="rest" size="xs">REST</Tab>
{#if SCRIPT_VIEW_SHOW_EXAMPLE_CURL}
<Tab value="curl" size="xs">Curl</Tab>
{/if}
<Tab value="fetch" size="xs">Fetch</Tab>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Tabs selected="rest">
<Tab value="rest" size="xs">REST</Tab>
{#if SCRIPT_VIEW_SHOW_EXAMPLE_CURL}
<Tab value="curl" size="xs">Curl</Tab>
{/if}
<Tab value="fetch" size="xs">Fetch</Tab>
<svelte:fragment slot="content">
<TabContent value="rest" class="flex flex-col flex-1 h-full ">
<div class="flex flex-col gap-2">
<ClipboardPanel title="Url" content={url} />
<svelte:fragment slot="content">
<TabContent value="rest" class="flex flex-col flex-1 h-full ">
<div class="flex flex-col gap-2">
<ClipboardPanel title="Url" content={url} />
{#if requestType !== 'get_path'}
<ClipboardPanel title="Body" content={JSON.stringify(args, null, 2)} />
{/if}
{#key requestType}
{#key tokenType}
<ClipboardPanel title="Headers" content={JSON.stringify(headers(), null, 2)} />
{#if requestType !== 'get_path'}
<ClipboardPanel title="Body" content={JSON.stringify(args, null, 2)} />
{/if}
{#key requestType}
{#key tokenType}
<ClipboardPanel title="Headers" content={JSON.stringify(headers(), null, 2)} />
{/key}
{/key}
{/key}
</div>
</TabContent>
<TabContent value="curl" class="flex flex-col flex-1 h-full">
<div class="relative">
</div>
</TabContent>
<TabContent value="curl" class="flex flex-col flex-1 h-full">
<div class="relative">
{#key args}
{#key requestType}
{#key webhookType}
{#key tokenType}
<div
class="flex flex-row flex-1 h-full border p-2 rounded-md overflow-auto relative"
on:click={(e) => {
e.preventDefault()
copyToClipboard(curlCode())
}}
>
<Highlight language={bash} code={curlCode()} class="" />
<Clipboard size={14} class="w-8 top-2 right-2 absolute" />
</div>
{/key}
{/key}
{/key}
{/key}
</div>
</TabContent>
<TabContent value="fetch">
{#key args}
{#key requestType}
{#key webhookType}
{#key tokenType}
<div
class="flex flex-row flex-1 h-full border p-2 rounded-md overflow-auto relative"
on:click={(e) => {
e.preventDefault()
copyToClipboard(curlCode())
}}
>
<Highlight language={bash} code={curlCode()} class="" />
<Clipboard size={14} class="w-8 top-2 right-2 absolute" />
</div>
{/key}
{/key}
{/key}
{#key token}
<div
class="flex flex-row flex-1 h-full border p-2 rounded-md overflow-auto relative"
on:click={(e) => {
e.preventDefault()
copyToClipboard(fetchCode())
}}
>
<Highlight language={typescript} code={fetchCode()} />
<Clipboard size={14} class="w-8 top-2 right-2 absolute" />
</div>
{/key}{/key}{/key}{/key}
{/key}
</div>
</TabContent>
<TabContent value="fetch">
{#key args}
{#key requestType}
{#key webhookType}
{#key tokenType}
{#key token}
<div
class="flex flex-row flex-1 h-full border p-2 rounded-md overflow-auto relative"
on:click={(e) => {
e.preventDefault()
copyToClipboard(fetchCode())
}}
>
<Highlight language={typescript} code={fetchCode()} />
<Clipboard size={14} class="w-8 top-2 right-2 absolute" />
</div>
{/key}{/key}{/key}{/key}
{/key}
</TabContent>
</svelte:fragment>
</Tabs>
</TabContent>
</svelte:fragment>
</Tabs>
{/key}
</div>
+2 -2
View File
@@ -132,7 +132,7 @@ export const APP_TO_ICON_COMPONENT = {
bigquery: BigQueryIcon,
graphql: GraphqlIcon,
nocodb: NocoDbIcon,
azure: AzureIcon,
azure: AzureIcon
} as const
export {
@@ -196,5 +196,5 @@ export {
BigQueryIcon,
GraphqlIcon,
NocoDbIcon,
AzureIcon,
AzureIcon
}
@@ -0,0 +1,20 @@
export interface Setting {
label: string
description?: string
placeholder?: string
cloudonly?: boolean
tooltip?: string
key: string
fieldType:
| 'text'
| 'number'
| 'boolean'
| 'password'
| 'select'
| 'textarea'
| 'seconds'
| 'email'
storage: SettingStorage
}
export type SettingStorage = 'setting' | 'config'
@@ -7,7 +7,7 @@
async function startSetup(): Promise<void> {
$workspaceStore = 'admins'
goto('/apps/get/g/all/setup_app')
goto('/user/instance_settings')
}
async function decline(): Promise<void> {
@@ -16,18 +16,12 @@
</script>
<CenteredModal title="Welcome to Windmill">
<p class="text-center text-lg mt-4">
This is a brand new instance. Go the superadmins app to setup the default superadmin user and
enable hub resource type sync?
<p class="text-center text-lg mt-4 mb-4">
This is a brand new instance. Setup the instance settings, then set the default superadmin user
and enable hub resource type sync
</p>
<div class="flex flex-row justify-between pt-4 gap-x-1">
<Button variant="border" size="sm" on:click={decline}>Skip setup</Button>
<button
class="place-items-end bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 border rounded"
type="button"
on:click={startSetup}
>
Setup
</button>
<Button color="dark" variant="border" on:click={decline}>Skip setup</Button>
<Button color="dark" on:click={startSetup}>Setup</Button>
</div>
</CenteredModal>
@@ -0,0 +1,9 @@
<script lang="ts">
import { goto } from '$app/navigation'
import CenteredModal from '$lib/components/CenteredModal.svelte'
import InstanceSettings from '$lib/components/InstanceSettings.svelte'
</script>
<CenteredModal title="Instance Settings">
<InstanceSettings on:save={() => goto('/apps/get/g/all/setup_app')} />
</CenteredModal>
@@ -224,6 +224,7 @@
<Icon data={faUserCog} class="mr-1" scale={1} />
User settings
</Button>
<Button
variant="border"
color="blue"
@@ -9,7 +9,7 @@
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import WorkspaceGroup from '$lib/components/WorkspaceGroup.svelte'
import { WorkerService, type WorkerPing, SettingService } from '$lib/gen'
import { WorkerService, type WorkerPing, SettingService, ConfigService } from '$lib/gen'
import { enterpriseLicense, superadmin } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { displayDate, groupBy, truncate } from '$lib/utils'
@@ -53,10 +53,10 @@
async function loadWorkerGroups(): Promise<void> {
try {
workerGroups = Object.fromEntries(
(await WorkerService.listWorkerGroups()).map((x) => [x.name, x.config])
(await ConfigService.listWorkerGroups()).map((x) => [x.name.substring(8), x.config])
)
} catch (err) {
sendUserToast(`Could not load workers: ${err}`, true)
sendUserToast(`Could not load worker groups: ${err}`, true)
}
}
@@ -103,7 +103,7 @@
let newGroupName = ''
async function addGroup() {
await WorkerService.updateWorkerGroup({ name: newGroupName, requestBody: {} })
await ConfigService.updateConfig({ name: 'worker__' + newGroupName, requestBody: {} })
loadWorkerGroups()
}
@@ -168,10 +168,10 @@
{#if customTags == undefined}
<Loader2 class="animate-spin" />
{:else}
<div class="flex flex-col">
<div class="flex flex-wrap gap-3 gap-y-2">
{#each customTags as customTag}
<div class="font-mono flex items-center gap-2 w-full">
<div class="w-full">- {customTag}</div>
<div class="flex gap-0.5 items-center"
><div class="text-2xs p-1 rounded border text-primary">{customTag}</div>
<button
class="z-10 rounded-full p-1 duration-200 hover:bg-gray-200"
aria-label="Remove item"
@@ -184,9 +184,9 @@
sendUserToast('Tag removed')
}}
>
<X size={14} />
</button>
</div>
<X size={12} />
</button></div
>
{/each}
</div>
<input type="text" bind:value={newTag} />