mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 00:01:34 +00:00
feat: set instance settings from UI (#2314)
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
+1
@@ -37,6 +37,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+1
@@ -67,6 +67,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+1
@@ -46,6 +46,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+1
@@ -28,6 +28,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+1
@@ -42,6 +42,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+1
@@ -42,6 +42,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+20
@@ -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"
|
||||
}
|
||||
-15
@@ -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"
|
||||
}
|
||||
+1
@@ -60,6 +60,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+15
@@ -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"
|
||||
}
|
||||
+1
@@ -42,6 +42,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
+28
@@ -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"
|
||||
}
|
||||
Generated
+75
-88
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}"))
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -68,6 +68,7 @@ async fn list_contextual_variables(
|
||||
Some("u/user/triggering_flow_path".to_string()),
|
||||
Some("c".to_string()),
|
||||
)
|
||||
.await
|
||||
.to_vec(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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}"))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user