Compare commits

..

180 Commits

Author SHA1 Message Date
fatonramadani
76266f3efb Apply automatic changes 2024-01-08 10:37:39 +00:00
Faton Ramadani
2cdda619de Merge branch 'main' into operator-menu 2024-01-08 11:35:59 +01:00
Ruben Fiszel
d340fbc02f fix: delete is captured in more fields in app 2024-01-08 11:33:28 +01:00
Ruben Fiszel
bed2d1f417 chore: use dev instead of latest to publish to ecr 2024-01-08 10:12:24 +01:00
Ruben Fiszel
4377cb3bcc chore(main): release 1.236.2 (#2970)
* chore(main): release 1.236.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-01-08 10:05:25 +01:00
Ruben Fiszel
269afe41f7 fix: handle better non object result for app result loader 2024-01-08 10:02:24 +01:00
Ruben Fiszel
1d1f03691a chore(main): release 1.236.1 (#2969)
* chore(main): release 1.236.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-01-08 09:29:56 +01:00
Ruben Fiszel
6b075e92ef fix: fix app initialization 2024-01-08 09:27:50 +01:00
Ruben Fiszel
be28522eb7 chore(main): release 1.236.0 (#2968)
* chore(main): release 1.236.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-01-08 06:49:31 +01:00
Ruben Fiszel
5749a3083a fix script deployment 2024-01-08 06:37:08 +01:00
Ruben Fiszel
2034aebb17 fix tests following parser update 2024-01-08 00:57:11 +01:00
Ruben Fiszel
4384617f1e fix: reduce stack size needed for workflow transitions 2024-01-08 00:47:59 +01:00
Ruben Fiszel
94493812ac feat: improve git sync and recompute dependents when common python script is redeployed (#2967)
* fix: allow extraConfig for agGrid to set onGridReady

* depmap

* depmap

* depmap

* depmap
2024-01-07 15:18:27 +01:00
Ruben Fiszel
2fc21d46fa chore(main): release 1.235.1 (#2966)
* chore(main): release 1.235.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-01-07 14:15:40 +01:00
Ruben Fiszel
2e023003da fix: fix resultjobloader 2024-01-07 14:12:20 +01:00
Guillaume Bouvignies
30a30f81e0 chore: Update git sync scripts (#2965) 2024-01-06 18:23:01 +01:00
Ruben Fiszel
723744e41e chore(main): release 1.235.0 (#2964)
* chore(main): release 1.235.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-01-06 17:32:41 +01:00
Faton Ramadani
fffc5338ce feat(frontend): DB Explorer (#2892)
* feat(frontend): Make table cell editable

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): clean up

* fix(frontend): extract db schemas

* feat(frontend): v0 done

* feat(frontend): v0 done

* feat(frontend): v0 done

* feat(frontend): fix insert

* feat(frontend): fix insert

* feat(frontend): remove temp data

* feat(frontend): align insert button to the right

* feat(frontend): rework columns

* feat(frontend): rework insert

* feat(frontend): rework insert

* feat(frontend): rework insert

* feat(frontend): rework insert

* feat(frontend): fix jsonb display

* feat(frontend): fix reloading issues

* feat(frontend): fix reloading issues

* feat(frontend): fix reacticity issue

* feat(frontend): fix insert

* feat(frontend): support nullable default value

* feat(frontend): fix build

* update

* update

* db studio v0

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2024-01-06 17:27:15 +01:00
Henri Courdent
3c2f753eac "Author" to "Developer", Premium plans tab, Flow inputs (#2952)
* Premium plans tab & Flow inputs

* Fix build
2024-01-06 16:55:43 +01:00
Ruben Fiszel
aa3d802137 chore(main): release 1.234.1 (#2962)
* chore(main): release 1.234.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-01-05 18:30:26 +01:00
Ruben Fiszel
9efcb53bcd fix: fix double view runs button 2024-01-05 18:27:45 +01:00
Ruben Fiszel
4385edc6ff fix: fix typo "Wourker" -> "Worker" 2024-01-05 18:26:52 +01:00
Ruben Fiszel
3cadaa3f2b chore(main): release 1.234.0 (#2957)
* chore(main): release 1.234.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-01-05 18:24:22 +01:00
Guillaume Bouvignies
c05d119ae5 fix: Deleting workspace delete all workspace related values in DB (#2961)
* fix: Deleting workspace delete all workspace related values in DB

* sqlx prepare
2024-01-05 18:02:40 +01:00
Guillaume Bouvignies
fb5cd6a429 feat: git sync can now push commits to individual branches (#2959) 2024-01-05 15:52:18 +01:00
Guillaume Bouvignies
0710ce6ef2 feat: Detailed job memory footprint on job run page (#2956)
* feat: Detailed job memory footprint

* Address rubens comment

* double presion -> real

* sql query returns only metric_type
2024-01-05 09:13:05 +01:00
Martin Jul
8619f3102d Fix typo in README.md (#2955)
Fix typo
2024-01-04 11:24:33 +01:00
Guillaume Bouvignies
31d48533f4 S3 functions in Python SDK cannot return None (#2954) 2024-01-03 17:26:25 +01:00
Ruben Fiszel
6cd46ca7f8 chore(main): release 1.233.0 (#2926)
* chore(main): release 1.233.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-01-03 13:20:20 +01:00
Faton Ramadani
2494171b9c fix(frontend): handle case where large json has a file and a filename (#2951) 2024-01-03 09:17:30 +01:00
Guillaume Bouvignies
965571853c fix: Tag all docker images with latest and main tags (#2953) 2024-01-03 01:38:33 +09:00
Faton Ramadani
37744ff583 Merge branch 'main' into operator-menu 2024-01-02 06:38:21 +01:00
Faton Ramadani
dbe14757d4 Merge branch 'main' of https://github.com/windmill-labs/windmill 2024-01-02 06:38:12 +01:00
Ruben Fiszel
486b3aeb17 chore: improve e2e 2023-12-30 05:25:10 +01:00
Ruben Fiszel
5ee0040840 chore: improve e2e 2023-12-30 04:47:08 +01:00
Ruben Fiszel
0f774b4485 chore: improve e2e 2023-12-30 04:43:51 +01:00
Ruben Fiszel
0c40c1324b chore: improve integration tests 2023-12-30 04:21:34 +01:00
Ruben Fiszel
b17652ae60 chore: improve integration tests 2023-12-30 03:59:20 +01:00
Guillaume Bouvignies
b44618f35b feat: Job caching now works with S3 files (#2934) 2023-12-30 03:40:05 +01:00
Faton Ramadani
821d2d7ea1 feat(frontend): add the view runs buttons for operators (#2932)
* feat(frontend): add the view runs buttons for operators

* Update +page.svelte

* Update +page.svelte

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2023-12-30 03:39:22 +01:00
Ruben Fiszel
0299c656c3 fix: decision tree fix wording (#2941)
* fix(frontend): Fix decision tree (#2928)

* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): decision tree history

* fix(frontend): fix wording

* fix(frontend): fix wording

* fix(frontend): fix wording

* Carousel link and trigger frontend (#2929)

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
Co-authored-by: Henri Courdent <122811744+hcourdent@users.noreply.github.com>
2023-12-30 03:39:05 +01:00
Ruben Fiszel
e0140f175d feat: PIP_LOCAL_DEPENDENCIES now accepts regex and is configurable from the UI (#2940)
* fix(frontend): Fix decision tree (#2928)

* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): decision tree history

* fix(frontend): fix wording

* feat: PIP_LOCAL_DEPENDENCIES now accepts regex

* Add worker config FE

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
Co-authored-by: gbouv <guillaume@windmill.dev>
2023-12-30 03:35:04 +01:00
Ruben Fiszel
65ad840dc4 docs: Carousel link and trigger frontend (#2939)
* Carousel link and trigger frontend

* fix(frontend): Fix decision tree (#2928)

* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): decision tree history

* fix(frontend): fix wording

---------

Co-authored-by: hcourdent <henri@windmill.dev>
Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-12-30 03:32:14 +01:00
Ruben Fiszel
f13fb4505c docs: Example on how to monitor Windmill (#2938)
* docs: Example on how to track arbitrary job metrics

* Add how to monitor Windmill servers and workers

* Update README.md

---------

Co-authored-by: gbouv <guillaume@windmill.dev>
2023-12-30 03:30:48 +01:00
Ruben Fiszel
c80e4c5ffd chore: eetest (#2937)
* chore: Windmill integration tests

* Revert github workflow temp changes

* fix ee

---------

Co-authored-by: gbouv <guillaume@windmill.dev>
2023-12-30 03:29:00 +01:00
Ruben Fiszel
787017f53c fix: decision tree nits (#2936)
* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): decision tree history

* fix(frontend): fix wording

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-12-30 03:06:37 +01:00
Faton Ramadani
12bd4af3ee Merge branch 'main' into operator-menu 2023-12-29 16:54:12 +01:00
Faton Ramadani
9cdaf9c201 feat(frontend): wip operator menu 2023-12-29 16:52:28 +01:00
Faton Ramadani
ec094e8897 feat(frontend): wip operator menu 2023-12-29 16:48:09 +01:00
Faton Ramadani
cf61e09b90 feat(frontend): wip operator menu 2023-12-29 16:22:02 +01:00
Faton Ramadani
6a0ae83b49 feat(frontend): wip operator menu 2023-12-29 15:44:08 +01:00
Faton Ramadani
2f4be74e94 feat(frontend): wip operator menu 2023-12-29 15:42:03 +01:00
Guillaume Bouvignies
28811023a4 feat: PIP_LOCAL_DEPENDENCIES now accepts regex and is configurable from the UI (#2931)
* feat: PIP_LOCAL_DEPENDENCIES now accepts regex

* Add worker config FE
2023-12-29 22:02:46 +09:00
Faton Ramadani
e80534ca65 fix(frontend): fix wording (#2930)
* fix(frontend): fix wording

* fix(frontend): fix wording
2023-12-29 21:58:06 +09:00
Henri Courdent
95be0155dc Carousel link and trigger frontend (#2929)
Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-12-29 12:24:00 +01:00
Faton Ramadani
004781cd3d fix(frontend): Fix decision tree (#2928)
* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): decision tree history

* fix(frontend): fix wording
2023-12-29 03:33:23 +01:00
Ruben Fiszel
c085f5fa7e fix: persist state for agGrid 2023-12-29 03:28:07 +01:00
Ruben Fiszel
ad6c3e5cee small UX nits 2023-12-29 03:21:11 +01:00
Ruben Fiszel
74fc2d3f43 fix: allow extraConfig for agGrid to set onGridReady 2023-12-27 21:20:49 +01:00
Ruben Fiszel
3d1f707f36 chore(main): release 1.232.0 (#2924)
* chore(main): release 1.232.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-27 15:04:51 +01:00
Guillaume Bouvignies
d316af993d chore: update snippets for Polars 0.20 (#2925) 2023-12-27 15:02:06 +01:00
Ruben Fiszel
263f41cda3 fix: fix file input 2023-12-27 14:55:01 +01:00
Ruben Fiszel
b66fcbf626 fix: improve ata for bun 2023-12-27 02:29:38 +01:00
Ruben Fiszel
948b0da59e fix: use bun install in every case 2023-12-27 00:05:46 +01:00
Ruben Fiszel
49355280f6 feat: no overlap for flows togglable on schedules 2023-12-26 16:31:04 +01:00
Ruben Fiszel
606b03d761 feat: add summary to schedules 2023-12-26 14:15:46 +01:00
Ruben Fiszel
41062072d6 chore(main): release 1.231.0 (#2917)
* chore(main): release 1.231.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-26 07:10:27 +01:00
Ruben Fiszel
4b65594cbd fix: improve conditional wrapper 2023-12-26 07:02:01 +01:00
Guillaume Bouvignies
5fa5ab1514 feat: bun can read private npm package (#2915)
* feat: bun can read private package

* Add / at the end of url
2023-12-25 09:03:40 +01:00
Ruben Fiszel
e583dd7894 restrict assignable tags to superadmins 2023-12-24 13:44:15 +01:00
Ruben Fiszel
4a28e23c03 improve worker tag UX 2023-12-24 11:20:56 +01:00
Ruben Fiszel
d6237b9ecb itemslist nit 2023-12-23 05:12:54 +01:00
Ruben Fiszel
fc07d58868 feat(cli): support skip args for push --raw 2023-12-23 04:51:18 +01:00
Guillaume Bouvignies
2aecc4d34f chore: test merge (#2914) 2023-12-22 19:01:29 +01:00
Ruben Fiszel
eecca0fac2 chore(main): release 1.230.0 (#2905)
* chore(main): release 1.230.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-22 18:20:33 +01:00
Ruben Fiszel
b5f1f99daa fix: fix multiplayer 2023-12-22 18:17:36 +01:00
Ruben Fiszel
5698a0ebf2 feat: quick search in flow support workspace scripts 2023-12-22 17:54:44 +01:00
Ruben Fiszel
2a4c370669 improve search perf 2023-12-22 16:05:47 +01:00
Ruben Fiszel
408cef3d77 improve search perf 2023-12-22 15:59:01 +01:00
Ruben Fiszel
9667eb74f9 fix: edit schedule from trigger page 2023-12-22 15:32:36 +01:00
Guillaume Bouvignies
e40787c616 feat: move S3 file within bucket (#2913) 2023-12-22 15:02:16 +01:00
Guillaume Bouvignies
5f60d46836 feat: Retry for scheduled scripts (#2893)
* feat: Retry for scheduled scripts

* rename scheduldedscriptwithretry to singlescriptflow

* Schedule options under tabs
2023-12-22 14:59:46 +01:00
Faton Ramadani
d7b777a8e9 fix(frontend): Fix debug condition + decision tree + add missing date to job preview + prevent header node from being selected in the flow editor (#2912) 2023-12-22 13:25:12 +01:00
Guillaume Bouvignies
3cbdd890cd fix: Update S3 TS code snippets and Python SDK for Polars 0.20.X (#2911)
* fix: update S3 TS code snippets

* use S3Object type from SDK

* update python snippets as well

* Update python SDK for Polars 0.20.X
2023-12-22 08:39:35 +01:00
Guillaume Bouvignies
e27e887491 feat: S3 delete file and upload new (#2909) 2023-12-22 08:38:09 +01:00
Ruben Fiszel
6167cd65de ata nits 2023-12-22 07:59:13 +01:00
Ruben Fiszel
642c037c1a feat: add button for auto-formatting 2023-12-22 05:16:33 +01:00
Ruben Fiszel
6b570a779f fix: add relative path handling for bun ATA 2023-12-22 04:30:18 +01:00
Henri Courdent
2e180c8fe7 Helps in frontend (#2901)
* Helps in frontend

* fix doclink

* Fix build

* Removing tooltips Save

* fix: remove bad app type forcing (#2906)

* fix: remove bad type forcing

* fix: safety

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
Co-authored-by: HugoCasa <hugo@casademont.ch>
2023-12-21 17:00:02 +01:00
Ruben Fiszel
ea322d9c14 fix: pin deno windmill-client version to relase 2023-12-21 15:25:40 +01:00
Ruben Fiszel
b3527f5164 fix: improve javascript editors with semantic validation 2023-12-21 15:12:35 +01:00
HugoCasa
187c34a683 fix: remove bad app type forcing (#2906)
* fix: remove bad type forcing

* fix: safety
2023-12-21 13:28:26 +01:00
Guillaume Bouvignies
5f85b67dfc fix: Failing jobs in dedicated worker mode are now marked as failing (#2894)
* fix: Failing jobs in dedicated worker mode are now marked as failing

* remove log line

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2023-12-21 10:29:26 +01:00
Guillaume Bouvignies
4c3c988f7b fix: Python wrapper catches BaseException instead of Exception (#2902) 2023-12-21 10:26:13 +01:00
Ruben Fiszel
4e383e8404 chore(main): release 1.229.0 (#2898)
* chore(main): release 1.229.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-21 09:07:18 +01:00
Faton Ramadani
0170fcc338 fix(frontend): fix save draft when the app has no versions (#2904) 2023-12-21 09:03:22 +01:00
Guillaume Bouvignies
ebcc7ed2aa chore: add verify to Windmill python object for httpx (#2900)
* chore: add `verify` to Windmill python object for httpx

* move it before get_client
2023-12-21 02:38:16 +01:00
Ruben Fiszel
5a1902797c small ata fix 2023-12-21 02:15:16 +01:00
Ruben Fiszel
db05299795 fix audit logs 2023-12-21 01:56:53 +01:00
Ruben Fiszel
b43c5be108 improve ata 2023-12-20 18:36:06 +01:00
Ruben Fiszel
f2bcd723a5 improve ata 2023-12-20 18:35:58 +01:00
Guillaume Bouvignies
5fafd71135 fix: Re-add TS SDK changes for S3 (#2899) 2023-12-20 18:12:37 +01:00
Faton Ramadani
3f7c19f267 feat(frontend): add a quick button to open decision tree graph (#2897) 2023-12-20 17:21:59 +01:00
Ruben Fiszel
54cb6c4e88 chore(main): release 1.228.1 (#2896)
* chore(main): release 1.228.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-20 16:57:28 +01:00
Guillaume Bouvignies
46e9818c00 fix: TS SDK not found upon install error (#2895) 2023-12-20 16:53:42 +01:00
Ruben Fiszel
1291b91b9b chore(main): release 1.228.0 (#2875)
* chore(main): release 1.228.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-20 14:30:58 +01:00
Ruben Fiszel
83aaa30967 small ata fix 2023-12-20 12:55:21 +01:00
Ruben Fiszel
d507ce449b feat: bun automatic type acquisition in frontend directly (#2884)
* foo

* ata

* done

* remove bun from lsp

* update all
2023-12-20 12:52:01 +01:00
Guillaume Bouvignies
2155a6c133 feat: S3 objects are now typed in Python and TS SDK (#2878)
* feat: S3 objects are now typed in Python and TS SDK

* fix ts and python SDK after testing

* cleanup
2023-12-20 09:16:33 +01:00
Guillaume Bouvignies
4061bbf832 docs: How to query gRPC service using Bun (#2863)
* docs: How to query gRPC service using Bun

* move readme to blogpost
2023-12-20 09:14:01 +01:00
Guillaume Bouvignies
292b2309a7 fix: Python imports from git repository (#2886) 2023-12-20 13:00:51 +09:00
legalgig
afbea19421 feat: Add Zitadel IDP (#2888) 2023-12-20 13:00:06 +09:00
HugoCasa
e8b2cc8835 feat: not on latest app version warning (#2889) 2023-12-20 12:59:18 +09:00
Guillaume Bouvignies
d0c75002f8 docs: Processing Kafka messages with Windmill Perpetual scripts (#2859)
* docs: Processing Kafka messages with Windmill Perpetual scripts

* Add while loop and update readme

* move README to blog post
2023-12-19 12:55:38 +01:00
Guillaume Bouvignies
5dd58094fd fix: Persistent script spinner (#2879) 2023-12-18 16:16:25 +01:00
Ruben Fiszel
59397e7445 fix: remove 0x00 from logs automatically from bashoutput 2023-12-18 03:30:47 +01:00
Ruben Fiszel
032a8e3dd1 chore(main): release 1.227.1 (#2874)
* chore(main): release 1.227.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-17 20:18:10 +01:00
Ruben Fiszel
fa7261f273 fix: improve on click behavior of tree view 2023-12-17 20:15:09 +01:00
Ruben Fiszel
50a1d5830b chore(main): release 1.227.0 (#2855)
* chore(main): release 1.227.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-18 01:39:50 +09:00
Ruben Fiszel
b9120a9269 fix sqlx 2023-12-17 17:29:25 +01:00
Ruben Fiszel
471a35649f frontend nits 2023-12-17 17:07:45 +01:00
Ruben Fiszel
705ce5d1d7 improve clickable zone for home tree view 2023-12-17 16:53:33 +01:00
Ruben Fiszel
567fabb828 cache token for lsp 2023-12-17 16:36:38 +01:00
Ruben Fiszel
7fac60c37f feat: add filter for home screen to filter u/* 2023-12-17 16:17:29 +01:00
Ruben Fiszel
9b25faaa95 feat: add overridable tag directly from run page 2023-12-17 15:27:56 +01:00
Joshua May
40013cd6cb feat: Add Kanidm SSO support (#2871) 2023-12-17 02:05:15 +01:00
Ruben Fiszel
26e9f57099 agent mode is ee 2023-12-16 09:31:52 +01:00
Guillaume Bouvignies
0cc19b9f8c chore: Remove slow pull warning in worker agent mode (#2866) 2023-12-15 19:16:41 +01:00
Faton Ramadani
864e6a5b94 feat(frontend): Add submitted text prop (#2864)
* feat(frontend): add a prop to control the text after a file is uploaded

* feat(frontend): add a prop to control the text after a file is uploaded
2023-12-15 17:36:42 +01:00
HugoCasa
e04d8b0d51 fix: handle empty result (#2862)
* fix: handle empty result

* fix: nits
2023-12-15 16:49:53 +01:00
Faton Ramadani
c80544e8e4 feat(frontend): Decision tree (#2812)
* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): deicision tree v0

* feat(frontend): deicision tree v0

* feat(frontend): fix build

* feat(frontend): fix events

* feat(frontend): add branch

* feat(frontend): interactions done

* feat(frontend): fix graph display

* feat(frontend): fix rendering

* feat(frontend): add end props

* feat(frontend): working empty branches

* feat(frontend): code cleanup

* feat(frontend): code cleanup

* feat(frontend): fix subgrids sync

* feat(frontend): add missing events

* feat(frontend): clean up comments

* feat(frontend): fix inputValue key

* feat(frontend): clean up comments

* feat(frontend): compute first node

* feat(frontend): fix layout

* feat(frontend): fix actions

* feat(frontend): remove console.log

* feat(frontend): fix styling + add debug mode + use label in output panel

* feat(frontend): fix styling

* feat(frontend): fix styling

* feat(frontend): done

* feat(frontend): done

* feat(frontend): done

* feat(frontend): fix add

* feat(frontend): fix delete

* feat(frontend): minor fix

* feat(frontend): fix preivous buttons

* feat(frontend): Fix the eval preview

* feat(frontend): Remove log

* feat(frontend): fix wording

* feat(frontend): select the tab in the app editor when selecting the node
2023-12-15 15:19:59 +01:00
Guillaume Bouvignies
a84b4324d2 fix: Stopping perpetual scripts (#2860) 2023-12-15 15:19:30 +01:00
HugoCasa
6c809b8630 fix: postgres ssl mode (#2861) 2023-12-15 15:15:55 +01:00
Ruben Fiszel
65e18abe7d fix(cli): improve flow sync for windows 2023-12-15 02:10:17 +01:00
Ruben Fiszel
011813654d fix(cli): improve flow sync for windows 2023-12-15 01:59:10 +01:00
Ruben Fiszel
787af442a2 improve embeddings model api error 2023-12-15 01:45:35 +01:00
Ruben Fiszel
b014e724d8 improve cli error 2023-12-15 01:14:52 +01:00
Ruben Fiszel
a5f4bd1ed2 fix evalv2 connections rare freeze 2023-12-14 17:52:34 +01:00
Guillaume Bouvignies
f8c7a8edf8 feat: Add download button to S3 file picker (#2857) 2023-12-14 15:45:37 +01:00
Guillaume Bouvignies
70c2669b1e chore: Update S3 code snippets (#2854)
* chore: Update S3 code snippets

* update tooltip

* cleanup code
2023-12-14 15:35:24 +01:00
Guillaume Bouvignies
9b0b9197d7 feat: Cancel persistent script runs from drawer (#2847)
* feat: Cancel persistent script runs from drawer

* Add explicit buttons

* revert change to docker compose
2023-12-14 15:30:39 +01:00
Faton Ramadani
28b8c4be83 fix(frontend): fix tutorial typos (#2852)
* fix(frontend): fix tutorial typos

* feat(frontend): fix wording

* feat(frontend): fix wording
2023-12-14 15:27:23 +01:00
HugoCasa
fdd9374aa1 fix: powershell nsjail (#2856) 2023-12-14 13:34:40 +01:00
Ruben Fiszel
f9e7827934 fix sqlx 2023-12-14 12:37:22 +01:00
Ruben Fiszel
aa44a88960 fix(cli): remove is_expired from variables 2023-12-14 12:34:16 +01:00
Ruben Fiszel
92433f0f2c fix(cli_: avoid updating variable when unecessary 2023-12-14 09:31:11 +01:00
Ruben Fiszel
2e4408e6ee chore(main): release 1.226.1 (#2853)
* chore(main): release 1.226.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-14 09:24:00 +01:00
Ruben Fiszel
087c5167af fix: getVariable can now return the encrypted value 2023-12-14 09:21:02 +01:00
Ruben Fiszel
f039008dc0 fix(cli): check for existing resource even in raw mode 2023-12-14 09:00:38 +01:00
Ruben Fiszel
996bf64393 fix: cli uses await for every push call 2023-12-14 08:36:57 +01:00
Ruben Fiszel
c9423c5e41 chore(main): release 1.226.0 (#2850)
* chore(main): release 1.226.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-14 02:54:38 +01:00
Ruben Fiszel
17bb7d3991 fix: fix s3 test connection 2023-12-14 02:51:53 +01:00
dependabot[bot]
021cf70cc3 chore(deps): bump aws-sdk-s3 from 0.39.1 to 1.5.0 in /backend (#2836)
Bumps [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust) from 0.39.1 to 1.5.0.
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Changelog](https://github.com/awslabs/aws-sdk-rust/blob/main/CHANGELOG.md)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-14 02:29:47 +01:00
Guillaume Bouvignies
0f913d0df9 fix: Handle s3object in Deno and Bun (#2851) 2023-12-13 23:02:48 +01:00
HugoCasa
3c4e41c997 feat: ee-only stats (#2849) 2023-12-13 18:46:03 +01:00
Ruben Fiszel
4e0ea0097e chore(main): release 1.225.1 (#2846)
* chore(main): release 1.225.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-13 16:34:35 +01:00
Ruben Fiszel
63740adec8 fix: fix setting base_url from empty initial value 2023-12-13 16:24:07 +01:00
HugoCasa
b9397856a8 fix: pwsh modules nsjail (#2845) 2023-12-13 14:16:08 +01:00
Ruben Fiszel
56332f43ba ci: improve s3 build step 2023-12-13 12:45:54 +01:00
Ruben Fiszel
f13e144e76 ci: improve s3 build step 2023-12-13 11:59:43 +01:00
Ruben Fiszel
896b894728 ci: improve s3 build step 2023-12-13 11:44:34 +01:00
Ruben Fiszel
88a3658825 ci: improve s3 build step 2023-12-13 11:34:18 +01:00
Ruben Fiszel
26cd75b52b ci: improve s3 build step 2023-12-13 10:43:35 +01:00
Ruben Fiszel
0b7b84d590 Update README.md 2023-12-13 10:17:02 +01:00
Guillaume Bouvignies
48a3dea059 chore: Rename FF saml to enterprise_saml (#2844) 2023-12-13 09:19:24 +01:00
Ruben Fiszel
372aa3e639 chore(main): release 1.225.0 (#2837)
* chore(main): release 1.225.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-13 08:28:55 +01:00
Ruben Fiszel
18e07a781d fix: improve nested flow status viewer 2023-12-13 08:14:34 +01:00
HugoCasa
505b417f3b feat: powershell deps (#2820)
* feat: powershell deps v-1

* feat: powershell profile + editor helpers + arm img

* fix: make sure powershell cache dir exists
2023-12-13 07:09:08 +01:00
Guillaume Bouvignies
26670f62e5 feat: Add default timeout for instance and custom timeout for scripts (#2811)
* feat: Add default timeout for instance and custom timeout for scripts

* final

* sqlx prepare

* Logs a warning in the job logs

* Fix merge

* JOB_DEFAULT_TIMEOUT is an optional

* Fix JOB_DEFAULT_TIMEOUT monitor loading

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-12-13 07:08:07 +01:00
Guillaume Bouvignies
b401984514 fix: Simplify worker env var input (#2838)
* fix: simplify worker env var input

* Add preset buttons for AWS and SSL
2023-12-12 18:45:52 +01:00
Guillaume Bouvignies
dd1032fbc3 feat: Add jitter to exponential backoff (#2842) 2023-12-12 18:36:22 +01:00
HugoCasa
107bfa7252 fix: openai cache lock (#2841) 2023-12-12 15:52:04 +01:00
Guillaume Bouvignies
6d194e8e66 chore: Update git sync script (#2840) 2023-12-12 14:31:12 +01:00
Henri Courdent
3a26ef9035 More tooltips on frontend (#2839) 2023-12-12 11:20:19 +01:00
HugoCasa
4c7d64bc5f feat: oauth flow openai key (#2833)
* feat: oauth flow openai key

* fix: rawvalue insert
2023-12-12 07:57:18 +01:00
Guillaume Bouvignies
350c8e843f feat: EnvVar allow list is configurable from the UI (#2829)
* feat: EnvVar allow list is configurable from the UI

* variable renaming
2023-12-12 07:04:55 +01:00
Guillaume Bouvignies
c06a56ab78 feat: Resource type description is now displayed when creating a new resource (#2834) 2023-12-12 06:58:00 +01:00
Ruben Fiszel
bbf3a56e88 chore(main): release 1.224.1 (#2832)
* chore(main): release 1.224.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-11 17:29:06 +01:00
HugoCasa
64a452826f fix: sql types completions when monaco initialized (#2831) 2023-12-11 17:25:15 +01:00
Faton Ramadani
69abe27662 fix(frontend): add truncate to DrawerContent (#2830) 2023-12-11 17:19:37 +01:00
385 changed files with 65493 additions and 9719 deletions

25
.github/change-versions-mac.sh vendored Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
set -euo pipefail
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root_dirpath="$(cd "${script_dirpath}/.." && pwd)"
VERSION=$1
echo "Updating versions to: $VERSION"
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/backend/Cargo.toml
sed -i '' -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" ${root_dirpath}/cli/main.ts
sed -i '' -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" ${root_dirpath}/benchmarks/lib.ts
sed -i '' -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/backend/windmill-api/openapi.yaml
sed -i '' -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/openflow.openapi.yaml
sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-client/package.json
sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/package.json
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i '' -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
# sed -i '' -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill_pg/pyproject.toml
sed -i '' -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i '' -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i '' -E "s/name = \"windmill\"\nversion = \"[^\"]*\"\\n(.*)/name = \"windmill\"\nversion = \"$VERSION\"\\n\\1/" ${root_dirpath}/backend/Cargo.lock
cd ${root_dirpath}/frontend && npm i --package-lock-only

View File

@@ -1,22 +1,25 @@
#!/bin/bash
set -euo pipefail
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root_dirpath="$(cd "${script_dirpath}/.." && pwd)"
VERSION=$1
echo "Updating versions to: $VERSION"
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" backend/Cargo.toml
sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" cli/main.ts
sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" benchmarks/lib.ts
sed -i -e "/version: /s/: .*/: $VERSION/" backend/windmill-api/openapi.yaml
sed -i -e "/version: /s/: .*/: $VERSION/" openflow.openapi.yaml
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" typescript-client/package.json
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" frontend/package.json
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" python-client/wmill/pyproject.toml
sed -i -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill/pyproject.toml
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" python-client/wmill_pg/pyproject.toml
# sed -i -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill_pg/pyproject.toml
sed -i -e "/^wmill =/s/= .*/= \">=$VERSION\"/" lsp/Pipfile
sed -i -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" lsp/Pipfile
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/backend/Cargo.toml
sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" ${root_dirpath}/cli/main.ts
sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" ${root_dirpath}/benchmarks/lib.ts
sed -i -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/backend/windmill-api/openapi.yaml
sed -i -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/openflow.openapi.yaml
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-client/package.json
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/package.json
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
# sed -i -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
sed -i -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i -zE "s/name = \"windmill\"\nversion = \"[^\"]*\"\\n(.*)/name = \"windmill\"\nversion = \"$VERSION\"\\n\\1/" backend/Cargo.lock
sed -i -zE "s/name = \"windmill\"\nversion = \"[^\"]*\"\\n(.*)/name = \"windmill\"\nversion = \"$VERSION\"\\n\\1/" ${root_dirpath}/backend/Cargo.lock
cd frontend && npm i --package-lock-only
cd ${root_dirpath}/frontend && npm i --package-lock-only

View File

@@ -43,8 +43,9 @@ jobs:
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
@@ -56,7 +57,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
${{ steps.meta-public.outputs.tags }}
labels: |
${{ steps.meta-public.outputs.labels }}
@@ -79,8 +80,9 @@ jobs:
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee
flavor: |
latest=false
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
@@ -99,10 +101,10 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,saml
features=enterprise,enterprise_saml
nsjail=true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
${{ steps.meta-ee-public.outputs.tags }}
labels: |
${{ steps.meta-ee-public.outputs.labels }}
@@ -187,30 +189,6 @@ jobs:
# run: docker kill ${{ steps.docker-container.outputs.id }}
# if: always()
deploy_s3:
needs: [build_ee]
runs-on: ubicloud
if: github.event_name != 'pull_request'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- uses: shrink/actions-docker-extract@v3
id: extract
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
path: "/static_frontend/."
- uses: reggionick/s3-deploy@v3
with:
folder: ${{ steps.extract.outputs.destination }}
bucket: windmill-frontend
bucket-region: us-east-1
attach_amd64_binary_to_release:
needs: [build, build_ee]
runs-on: ubicloud
@@ -222,25 +200,25 @@ jobs:
- run: |
# pulling docker image with desired arch so that actions-docker-extract doesn't do it
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
- run: |
# Checks the image is in docker prior to running actions-docker-extract. It fails if not
# Also useful to visually check that the arch is the right opencontainers
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
- uses: shrink/actions-docker-extract@v3
id: extract
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
path: "/usr/src/app/windmill"
- uses: shrink/actions-docker-extract@v3
id: extract-ee
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
path: "/usr/src/app/windmill"
- name: Rename binary with corresponding architecture
@@ -266,25 +244,25 @@ jobs:
- run: |
# pulling docker image with desired arch so that actions-docker-extract doesn't do it
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
- run: |
# Checks the image is in docker prior to running actions-docker-extract. It fails if not
# Also useful to visually check that the arch is the right opencontainers
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
- uses: shrink/actions-docker-extract@v3
id: extract
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
path: "/usr/src/app/windmill"
- uses: shrink/actions-docker-extract@v3
id: extract-ee
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
path: "/usr/src/app/windmill"
- name: Rename binary with corresponding architecture
@@ -299,10 +277,13 @@ jobs:
${{ steps.extract.outputs.destination }}/*
${{ steps.extract-ee.outputs.destination }}/*
publish_ecr:
publish_ecr_s3:
needs: [build_ee]
runs-on: ubicloud
if: github.event_name != 'pull_request'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- uses: actions/checkout@v3
with:
@@ -321,8 +302,85 @@ jobs:
- name: Push image to ECR
if: github.event_name != 'pull_request'
id: push_ecr
run: |
git_hash=$(git rev-parse --short "$GITHUB_SHA")
docker buildx imagetools create \
--tag ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${git_hash:0:7} \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
echo "GIT_HASH=${git_hash:0:7}" >> "$GITHUB_OUTPUT"
- uses: shrink/actions-docker-extract@v3
if: github.event_name != 'pull_request'
id: extract
with:
image: |-
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ steps.push_ecr.outputs.GIT_HASH }}
path: "/static_frontend/."
- uses: reggionick/s3-deploy@v3
if: github.event_name != 'pull_request'
with:
folder: ${{ steps.extract.outputs.destination }}
bucket: windmill-frontend
bucket-region: us-east-1
run_integration_test:
runs-on: ubicloud
needs: [build_ee]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Prepare test run
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
run: cd integration_tests && ./build.sh
- name: Test run
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
timeout-minutes: 15
env:
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
run: cd integration_tests && ./run.sh
- name: Archive logs
uses: actions/upload-artifact@v3
if: always()
with:
name: Windmill Integration Tests Logs
path: |
integration_tests/logs
tag_latest:
runs-on: ubicloud
needs: [run_integration_test]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Login to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Tag main and latest
run: |
docker buildx imagetools create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker buildx imagetools create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main
tag_latest_ee:
runs-on: ubicloud
needs: [run_integration_test]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Login to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Tag main and latest for ee
run: |
docker buildx imagetools create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
docker buildx imagetools create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:main

View File

@@ -1,6 +1,265 @@
# Changelog
## [1.236.2](https://github.com/windmill-labs/windmill/compare/v1.236.1...v1.236.2) (2024-01-08)
### Bug Fixes
* handle better non object result for app result loader ([269afe4](https://github.com/windmill-labs/windmill/commit/269afe41f7e02da8d3a364e649e5fef00847d24a))
## [1.236.1](https://github.com/windmill-labs/windmill/compare/v1.236.0...v1.236.1) (2024-01-08)
### Bug Fixes
* fix app initialization ([6b075e9](https://github.com/windmill-labs/windmill/commit/6b075e92ef4e312e6a4ce582dc7c4d88456b2f5d))
## [1.236.0](https://github.com/windmill-labs/windmill/compare/v1.235.1...v1.236.0) (2024-01-08)
### Features
* improve git sync and recompute dependents when common python script is redeployed ([#2967](https://github.com/windmill-labs/windmill/issues/2967)) ([9449381](https://github.com/windmill-labs/windmill/commit/94493812ac0030cc6bba468d8ee24a14661716a8))
### Bug Fixes
* reduce stack size needed for workflow transitions ([4384617](https://github.com/windmill-labs/windmill/commit/4384617f1ec4c27de88ef6c895d6ef060a79f6bd))
## [1.235.1](https://github.com/windmill-labs/windmill/compare/v1.235.0...v1.235.1) (2024-01-07)
### Bug Fixes
* fix resultjobloader ([2e02300](https://github.com/windmill-labs/windmill/commit/2e023003da8bea9efa86c0fbc52bf9723c73907d))
## [1.235.0](https://github.com/windmill-labs/windmill/compare/v1.234.1...v1.235.0) (2024-01-06)
### Features
* **frontend:** DB Explorer ([#2892](https://github.com/windmill-labs/windmill/issues/2892)) ([fffc533](https://github.com/windmill-labs/windmill/commit/fffc5338ce76fda9a68b19f03cefe199cc09a905))
## [1.234.1](https://github.com/windmill-labs/windmill/compare/v1.234.0...v1.234.1) (2024-01-05)
### Bug Fixes
* fix double view runs button ([9efcb53](https://github.com/windmill-labs/windmill/commit/9efcb53bcd903bf756c4beb92a5c7634e8e016ad))
* fix typo "Wourker" -&gt; "Worker" ([4385edc](https://github.com/windmill-labs/windmill/commit/4385edc6ffdb1b2e4544347928b4cf08faf1eaa1))
## [1.234.0](https://github.com/windmill-labs/windmill/compare/v1.233.0...v1.234.0) (2024-01-05)
### Features
* Detailed job memory footprint on job run page ([#2956](https://github.com/windmill-labs/windmill/issues/2956)) ([0710ce6](https://github.com/windmill-labs/windmill/commit/0710ce6ef25d8f2344ac1350e96aa906f16d1be1))
* git sync can now push commits to individual branches ([#2959](https://github.com/windmill-labs/windmill/issues/2959)) ([fb5cd6a](https://github.com/windmill-labs/windmill/commit/fb5cd6a4298c88e26481b76258bf5b09a11cf4b0))
### Bug Fixes
* Deleting workspace delete all workspace related values in DB ([#2961](https://github.com/windmill-labs/windmill/issues/2961)) ([c05d119](https://github.com/windmill-labs/windmill/commit/c05d119ae5dfb6f1bae88fc9d53b48a707cd2f15))
## [1.233.0](https://github.com/windmill-labs/windmill/compare/v1.232.0...v1.233.0) (2024-01-03)
### Features
* **frontend:** add the view runs buttons for operators ([#2932](https://github.com/windmill-labs/windmill/issues/2932)) ([821d2d7](https://github.com/windmill-labs/windmill/commit/821d2d7ea15e828dbb3683c85025dbc09c7e8dc7))
* Job caching now works with S3 files ([#2934](https://github.com/windmill-labs/windmill/issues/2934)) ([b44618f](https://github.com/windmill-labs/windmill/commit/b44618f35b6fe4558a5cd9fddec1e82216a2cf5e))
* PIP_LOCAL_DEPENDENCIES now accepts regex and is configurable from the UI ([#2940](https://github.com/windmill-labs/windmill/issues/2940)) ([e0140f1](https://github.com/windmill-labs/windmill/commit/e0140f175d371e239f315613a537820b9a6ad25f))
### Bug Fixes
* allow extraConfig for agGrid to set onGridReady ([74fc2d3](https://github.com/windmill-labs/windmill/commit/74fc2d3f4397d976fec326893f8196008cb8b332))
* decision tree fix wording ([#2941](https://github.com/windmill-labs/windmill/issues/2941)) ([0299c65](https://github.com/windmill-labs/windmill/commit/0299c656c368876581dc228c76513f1171ffd682))
* decision tree nits ([#2936](https://github.com/windmill-labs/windmill/issues/2936)) ([787017f](https://github.com/windmill-labs/windmill/commit/787017f53c8227e1246b09e412e92316977937ae))
* **frontend:** handle case where large json has a file and a filename ([#2951](https://github.com/windmill-labs/windmill/issues/2951)) ([2494171](https://github.com/windmill-labs/windmill/commit/2494171b9cb3d1857235b6558d276451d5550560))
* persist state for agGrid ([c085f5f](https://github.com/windmill-labs/windmill/commit/c085f5fa7ec598d0abf6aad98de0e5c7b1c92985))
* Tag all docker images with latest and main tags ([#2953](https://github.com/windmill-labs/windmill/issues/2953)) ([9655718](https://github.com/windmill-labs/windmill/commit/965571853ca253b130bcc4473190b6c9c98f44e1))
## [1.232.0](https://github.com/windmill-labs/windmill/compare/v1.231.0...v1.232.0) (2023-12-27)
### Features
* add summary to schedules ([606b03d](https://github.com/windmill-labs/windmill/commit/606b03d7613cf15ab9a60ef34a82657ff49ccdbd))
* no overlap for flows togglable on schedules ([4935528](https://github.com/windmill-labs/windmill/commit/49355280f6442c342a46f90b2fe2cb089b3f4eff))
### Bug Fixes
* fix file input ([263f41c](https://github.com/windmill-labs/windmill/commit/263f41cda3dba24bcaa2bbabfc3f5bc54c70f9cb))
* improve ata for bun ([b66fcbf](https://github.com/windmill-labs/windmill/commit/b66fcbf6261f69a57e6711fa8b9d1d499c1b47ed))
* use bun install in every case ([948b0da](https://github.com/windmill-labs/windmill/commit/948b0da59e6bcd3b6312373a1bbe52d541e54d56))
## [1.231.0](https://github.com/windmill-labs/windmill/compare/v1.230.0...v1.231.0) (2023-12-26)
### Features
* bun can read private npm package ([#2915](https://github.com/windmill-labs/windmill/issues/2915)) ([5fa5ab1](https://github.com/windmill-labs/windmill/commit/5fa5ab1514a2d20083d30809c30e50bfcb29e82f))
* **cli:** support skip args for push --raw ([fc07d58](https://github.com/windmill-labs/windmill/commit/fc07d5886893c8d493c3ca507e0c9d4a066622fc))
### Bug Fixes
* improve conditional wrapper ([4b65594](https://github.com/windmill-labs/windmill/commit/4b65594cbd138452c471a339aaa548c27fc0a4ce))
## [1.230.0](https://github.com/windmill-labs/windmill/compare/v1.229.0...v1.230.0) (2023-12-22)
### Features
* add button for auto-formatting ([642c037](https://github.com/windmill-labs/windmill/commit/642c037c1a87efd8d63558cc0ca5e98fb7b51ff7))
* move S3 file within bucket ([#2913](https://github.com/windmill-labs/windmill/issues/2913)) ([e40787c](https://github.com/windmill-labs/windmill/commit/e40787c616ec288ffc83450fac6f6f7f127ac008))
* quick search in flow support workspace scripts ([5698a0e](https://github.com/windmill-labs/windmill/commit/5698a0ebf2052fd51b687951595a543f0b6c0308))
* Retry for scheduled scripts ([#2893](https://github.com/windmill-labs/windmill/issues/2893)) ([5f60d46](https://github.com/windmill-labs/windmill/commit/5f60d468365c65dc48e247107487f64adf77b123))
* S3 delete file and upload new ([#2909](https://github.com/windmill-labs/windmill/issues/2909)) ([e27e887](https://github.com/windmill-labs/windmill/commit/e27e8874918af84d8afffb26abd5828b4c43ed54))
### Bug Fixes
* add relative path handling for bun ATA ([6b570a7](https://github.com/windmill-labs/windmill/commit/6b570a779fa7592c527cee84b0ca238872d86347))
* edit schedule from trigger page ([9667eb7](https://github.com/windmill-labs/windmill/commit/9667eb74f99b74621e0f00e3f380bbd34dd9654f))
* Failing jobs in dedicated worker mode are now marked as failing ([#2894](https://github.com/windmill-labs/windmill/issues/2894)) ([5f85b67](https://github.com/windmill-labs/windmill/commit/5f85b67dfcf063fd8a3c3f69f0e7605fc40e473d))
* fix multiplayer ([b5f1f99](https://github.com/windmill-labs/windmill/commit/b5f1f99daa30627f224f262653e459799fc0d6fe))
* **frontend:** Fix debug condition + decision tree + add missing date to job preview + prevent header node from being selected in the flow editor ([#2912](https://github.com/windmill-labs/windmill/issues/2912)) ([d7b777a](https://github.com/windmill-labs/windmill/commit/d7b777a8e9b4a2a4f0772f9b29851476f9405dae))
* improve javascript editors with semantic validation ([b3527f5](https://github.com/windmill-labs/windmill/commit/b3527f5164f9e487840773df57846394f118ebbb))
* pin deno windmill-client version to relase ([ea322d9](https://github.com/windmill-labs/windmill/commit/ea322d9c14cc92b54cfc39791e162f8f248e74ab))
* Python wrapper catches BaseException instead of Exception ([#2902](https://github.com/windmill-labs/windmill/issues/2902)) ([4c3c988](https://github.com/windmill-labs/windmill/commit/4c3c988f7bd35f290b9d051369029e790b1e5c5b))
* remove bad app type forcing ([#2906](https://github.com/windmill-labs/windmill/issues/2906)) ([187c34a](https://github.com/windmill-labs/windmill/commit/187c34a6835e3e748bc609fb4096f42bf3e09a73))
* Update S3 TS code snippets and Python SDK for Polars 0.20.X ([#2911](https://github.com/windmill-labs/windmill/issues/2911)) ([3cbdd89](https://github.com/windmill-labs/windmill/commit/3cbdd890cd2aaef1d983da0270f8c230c9b9285e))
## [1.229.0](https://github.com/windmill-labs/windmill/compare/v1.228.1...v1.229.0) (2023-12-21)
### Features
* **frontend:** add a quick button to open decision tree graph ([#2897](https://github.com/windmill-labs/windmill/issues/2897)) ([3f7c19f](https://github.com/windmill-labs/windmill/commit/3f7c19f267a9965f21cf8a08040a45e088d24918))
### Bug Fixes
* **frontend:** fix save draft when the app has no versions ([#2904](https://github.com/windmill-labs/windmill/issues/2904)) ([0170fcc](https://github.com/windmill-labs/windmill/commit/0170fcc338e0b59f8af1894d868f3819cad8aca3))
* Re-add TS SDK changes for S3 ([#2899](https://github.com/windmill-labs/windmill/issues/2899)) ([5fafd71](https://github.com/windmill-labs/windmill/commit/5fafd71135c5d4d1764e9f96984bc5f5e5810465))
## [1.228.1](https://github.com/windmill-labs/windmill/compare/v1.228.0...v1.228.1) (2023-12-20)
### Bug Fixes
* TS SDK not found upon install error ([#2895](https://github.com/windmill-labs/windmill/issues/2895)) ([46e9818](https://github.com/windmill-labs/windmill/commit/46e9818c001b040c462ffe0b6f8681a97113ff04))
## [1.228.0](https://github.com/windmill-labs/windmill/compare/v1.227.1...v1.228.0) (2023-12-20)
### Features
* Add Zitadel IDP ([#2888](https://github.com/windmill-labs/windmill/issues/2888)) ([afbea19](https://github.com/windmill-labs/windmill/commit/afbea19421572f2986bf8ab1e5817f1902c0a22c))
* bun automatic type acquisition in frontend directly ([#2884](https://github.com/windmill-labs/windmill/issues/2884)) ([d507ce4](https://github.com/windmill-labs/windmill/commit/d507ce449b04bf0be5ea29ec2431ad1eea405f13))
* not on latest app version warning ([#2889](https://github.com/windmill-labs/windmill/issues/2889)) ([e8b2cc8](https://github.com/windmill-labs/windmill/commit/e8b2cc8835881a7b2c0a273cb8e505644e9be001))
* S3 objects are now typed in Python and TS SDK ([#2878](https://github.com/windmill-labs/windmill/issues/2878)) ([2155a6c](https://github.com/windmill-labs/windmill/commit/2155a6c1334b79272742ba4626abd9572c9130d2))
### Bug Fixes
* Persistent script spinner ([#2879](https://github.com/windmill-labs/windmill/issues/2879)) ([5dd5809](https://github.com/windmill-labs/windmill/commit/5dd58094fde38f13476e79b539e670ee150306d7))
* Python imports from git repository ([#2886](https://github.com/windmill-labs/windmill/issues/2886)) ([292b230](https://github.com/windmill-labs/windmill/commit/292b2309a7b7093c85614ac9764620c9dce0e316))
* remove 0x00 from logs automatically from bashoutput ([59397e7](https://github.com/windmill-labs/windmill/commit/59397e7445f78cca2e602d8b45591a0bb123a734))
## [1.227.1](https://github.com/windmill-labs/windmill/compare/v1.227.0...v1.227.1) (2023-12-17)
### Bug Fixes
* improve on click behavior of tree view ([fa7261f](https://github.com/windmill-labs/windmill/commit/fa7261f273b517acd85a8c3f465cd8095cdf9f3c))
## [1.227.0](https://github.com/windmill-labs/windmill/compare/v1.226.1...v1.227.0) (2023-12-17)
### Features
* Add download button to S3 file picker ([#2857](https://github.com/windmill-labs/windmill/issues/2857)) ([f8c7a8e](https://github.com/windmill-labs/windmill/commit/f8c7a8edf87226e020cec5e602e2dcb31f87d95e))
* add filter for home screen to filter u/* ([7fac60c](https://github.com/windmill-labs/windmill/commit/7fac60c37f638c0a717d2062f7e79db655506c4c))
* Add Kanidm SSO support ([#2871](https://github.com/windmill-labs/windmill/issues/2871)) ([40013cd](https://github.com/windmill-labs/windmill/commit/40013cd6cb00ebaf83eb75ec3f6907a727ba8f63))
* add overridable tag directly from run page ([9b25faa](https://github.com/windmill-labs/windmill/commit/9b25faaa95a4f659aa2c72b0b8ed46acee31a691))
* Cancel persistent script runs from drawer ([#2847](https://github.com/windmill-labs/windmill/issues/2847)) ([9b0b919](https://github.com/windmill-labs/windmill/commit/9b0b9197d717c6dad7b1f5e934158e7526455a47))
* **frontend:** Add submitted text prop ([#2864](https://github.com/windmill-labs/windmill/issues/2864)) ([864e6a5](https://github.com/windmill-labs/windmill/commit/864e6a5b9432e32ed1463972f7f980ae0b52745b))
* **frontend:** Decision tree ([#2812](https://github.com/windmill-labs/windmill/issues/2812)) ([c80544e](https://github.com/windmill-labs/windmill/commit/c80544e8e4f7b635847603e6e9ef9e661ce20c89))
### Bug Fixes
* **cli:** improve flow sync for windows ([65e18ab](https://github.com/windmill-labs/windmill/commit/65e18abe7d17334391b9326cd9968e64fa9f6586))
* **cli:** improve flow sync for windows ([0118136](https://github.com/windmill-labs/windmill/commit/011813654da596a8c7235408f1cd77e9abc63255))
* **cli:** remove is_expired from variables ([aa44a88](https://github.com/windmill-labs/windmill/commit/aa44a88960123204405e03efea87f0ba6bbe15ec))
* **frontend:** fix tutorial typos ([#2852](https://github.com/windmill-labs/windmill/issues/2852)) ([28b8c4b](https://github.com/windmill-labs/windmill/commit/28b8c4be833efd0b4c56584945b823c4e4fafa7a))
* handle empty result ([#2862](https://github.com/windmill-labs/windmill/issues/2862)) ([e04d8b0](https://github.com/windmill-labs/windmill/commit/e04d8b0d51ed57bd56b28c178dc6668be65fafbd))
* postgres ssl mode ([#2861](https://github.com/windmill-labs/windmill/issues/2861)) ([6c809b8](https://github.com/windmill-labs/windmill/commit/6c809b86300cd4cb461556eb570620f70ca0e26d))
* powershell nsjail ([#2856](https://github.com/windmill-labs/windmill/issues/2856)) ([fdd9374](https://github.com/windmill-labs/windmill/commit/fdd9374aa1cc4d21e9543771e9d1ad256e083461))
* Stopping perpetual scripts ([#2860](https://github.com/windmill-labs/windmill/issues/2860)) ([a84b432](https://github.com/windmill-labs/windmill/commit/a84b4324d2b39eea42fc8c0ed7c9df2a6c2207aa))
## [1.226.1](https://github.com/windmill-labs/windmill/compare/v1.226.0...v1.226.1) (2023-12-14)
### Bug Fixes
* cli uses await for every push call ([996bf64](https://github.com/windmill-labs/windmill/commit/996bf64393e221f3749e17c3c3755f45a73b616f))
* **cli:** check for existing resource even in raw mode ([f039008](https://github.com/windmill-labs/windmill/commit/f039008dc0d5e8f309148c1cab2998e03b4298c1))
* getVariable can now return the encrypted value ([087c516](https://github.com/windmill-labs/windmill/commit/087c5167afc90dacb4264bde73834d60a6ae2063))
## [1.226.0](https://github.com/windmill-labs/windmill/compare/v1.225.1...v1.226.0) (2023-12-14)
### Features
* ee-only stats ([#2849](https://github.com/windmill-labs/windmill/issues/2849)) ([3c4e41c](https://github.com/windmill-labs/windmill/commit/3c4e41c9975956339682ef35c298d294c55304f0))
### Bug Fixes
* fix s3 test connection ([17bb7d3](https://github.com/windmill-labs/windmill/commit/17bb7d3991d48abe08ab42b9381285112f2440f1))
* Handle s3object in Deno and Bun ([#2851](https://github.com/windmill-labs/windmill/issues/2851)) ([0f913d0](https://github.com/windmill-labs/windmill/commit/0f913d0df915a8189a036f06bd6684d9ed5ecadc))
## [1.225.1](https://github.com/windmill-labs/windmill/compare/v1.225.0...v1.225.1) (2023-12-13)
### Bug Fixes
* fix setting base_url from empty initial value ([63740ad](https://github.com/windmill-labs/windmill/commit/63740adec8b075069cbee942ae03c1c2cbec5de0))
* pwsh modules nsjail ([#2845](https://github.com/windmill-labs/windmill/issues/2845)) ([b939785](https://github.com/windmill-labs/windmill/commit/b9397856a8c2219d5315eb441b2b279a1dd8dab5))
## [1.225.0](https://github.com/windmill-labs/windmill/compare/v1.224.1...v1.225.0) (2023-12-13)
### Features
* Add default timeout for instance and custom timeout for scripts ([#2811](https://github.com/windmill-labs/windmill/issues/2811)) ([26670f6](https://github.com/windmill-labs/windmill/commit/26670f62e5e9f7bd50895fae9dd98bc75b61bb44))
* Add jitter to exponential backoff ([#2842](https://github.com/windmill-labs/windmill/issues/2842)) ([dd1032f](https://github.com/windmill-labs/windmill/commit/dd1032fbc3b589f00095c2f9523e1e3f7fca80d5))
* EnvVar allow list is configurable from the UI ([#2829](https://github.com/windmill-labs/windmill/issues/2829)) ([350c8e8](https://github.com/windmill-labs/windmill/commit/350c8e843f8facb154bb6b8223744e1c5ba65a28))
* oauth flow openai key ([#2833](https://github.com/windmill-labs/windmill/issues/2833)) ([4c7d64b](https://github.com/windmill-labs/windmill/commit/4c7d64bc5fdfeb33c63524ddd61173c655ab28e3))
* powershell deps ([#2820](https://github.com/windmill-labs/windmill/issues/2820)) ([505b417](https://github.com/windmill-labs/windmill/commit/505b417f3b07fd5b27256a6a01001aadba7c1f84))
* Resource type description is now displayed when creating a new resource ([#2834](https://github.com/windmill-labs/windmill/issues/2834)) ([c06a56a](https://github.com/windmill-labs/windmill/commit/c06a56ab785f072ff5525e685188d97c71f71da5))
### Bug Fixes
* improve nested flow status viewer ([18e07a7](https://github.com/windmill-labs/windmill/commit/18e07a781d453b867f325ae6086abe406767fd11))
* openai cache lock ([#2841](https://github.com/windmill-labs/windmill/issues/2841)) ([107bfa7](https://github.com/windmill-labs/windmill/commit/107bfa72520bca2df2b92adbbbb7e7f652b85a68))
* Simplify worker env var input ([#2838](https://github.com/windmill-labs/windmill/issues/2838)) ([b401984](https://github.com/windmill-labs/windmill/commit/b401984514f095a60bce913d245160dd8746a9a5))
## [1.224.1](https://github.com/windmill-labs/windmill/compare/v1.224.0...v1.224.1) (2023-12-11)
### Bug Fixes
* **frontend:** add truncate to DrawerContent ([#2830](https://github.com/windmill-labs/windmill/issues/2830)) ([69abe27](https://github.com/windmill-labs/windmill/commit/69abe27662c801d9ecbe2d31a51142e84d7dcfa2))
* sql types completions when monaco initialized ([#2831](https://github.com/windmill-labs/windmill/issues/2831)) ([64a4528](https://github.com/windmill-labs/windmill/commit/64a452826f3e00aaea6b5205c908f117327398c4))
## [1.224.0](https://github.com/windmill-labs/windmill/compare/v1.223.1...v1.224.0) (2023-12-10)

View File

@@ -125,23 +125,30 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
RUN if [ "$WITH_POWERSHELL" = "true" ]; then \
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then apt-get update -y && apt install libicu-dev -y && wget -O 'pwsh.deb' "https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell_${POWERSHELL_DEB_VERSION}.deb_amd64.deb" && \
dpkg --install 'pwsh.deb' && \
rm 'pwsh.deb'; else echo 'pwshell not on amd64'; fi; \
else echo 'Building the image without powershell'; fi
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then apt-get update -y && apt install libicu-dev -y && wget -O 'pwsh.deb' "https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell_${POWERSHELL_DEB_VERSION}.deb_amd64.deb" && \
dpkg --install 'pwsh.deb' && \
rm 'pwsh.deb'; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then apt-get update -y && apt install libicu-dev -y && wget -O powershell.tar.gz "https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell-${POWERSHELL_VERSION}-linux-arm64.tar.gz" && \
mkdir -p /opt/microsoft/powershell/7 && \
tar zxf powershell.tar.gz -C /opt/microsoft/powershell/7 && \
chmod +x /opt/microsoft/powershell/7/pwsh && \
ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh && \
rm powershell.tar.gz; \
else echo 'Could not install pwshell, not on amd64 or arm64'; fi; \
else echo 'Building the image without powershell'; fi
RUN if [ "$WITH_HELM" = "true" ]; then \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
wget "https://get.helm.sh/helm-v${HELM_VERSION}-linux-$arch.tar.gz" && \
tar -zxvf "helm-v${HELM_VERSION}-linux-$arch.tar.gz" && \
mv linux-$arch/helm /usr/local/bin/helm &&\
chmod +x /usr/local/bin/helm; \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
wget "https://get.helm.sh/helm-v${HELM_VERSION}-linux-$arch.tar.gz" && \
tar -zxvf "helm-v${HELM_VERSION}-linux-$arch.tar.gz" && \
mv linux-$arch/helm /usr/local/bin/helm &&\
chmod +x /usr/local/bin/helm; \
else echo 'Building the image without helm'; fi
RUN if [ "$WITH_KUBECTL" = "true" ]; then \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
curl -LO "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/$arch/kubectl" && \
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl; \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
curl -LO "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/$arch/kubectl" && \
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl; \
else echo 'Building the image without kubectl'; fi
RUN set -eux; \
@@ -160,10 +167,10 @@ RUN set -eux; \
./aws/install && rm awscliv2.zip
RUN if [ "$WITH_RCLONE" = "true" ]; then \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
curl -o rclone.zip "https://downloads.rclone.org/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-$arch.zip"; \
unzip -p rclone.zip rclone-v${RCLONE_VERSION}-linux-$arch/rclone > /usr/bin/rclone; rm rclone.zip; \
chown root:root /usr/bin/rclone; chmod 755 /usr/bin/rclone; \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
curl -o rclone.zip "https://downloads.rclone.org/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-$arch.zip"; \
unzip -p rclone.zip rclone-v${RCLONE_VERSION}-linux-$arch/rclone > /usr/bin/rclone; rm rclone.zip; \
chown root:root /usr/bin/rclone; chmod 755 /usr/bin/rclone; \
else echo 'Building the image without rclone'; fi
@@ -193,7 +200,7 @@ ENV TZ=Etc/UTC
RUN /usr/local/bin/python3 -m pip install pip-tools
COPY --from=frontend /frontend/build /static_frontend
COPY --from=builder /frontend/build /static_frontend
COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
@@ -202,7 +209,7 @@ RUN chmod 755 /usr/bin/deno
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
COPY --from=oven/bun:1.0.8 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.0.18 /usr/local/bin/bun /usr/bin/bun
# add the docker client to call docker from a worker if enabled
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/

239
README.md
View File

@@ -2,10 +2,10 @@
<a href="https://www.windmill.dev/"><img src="./imgs/windmill-banner.png" alt="windmill.dev"></a>
</p>
<p align=center>
Open-source developer infrastructure for internal tools (APIs, background jobs, workflows and UIs). Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIsm and custom UIs to trigger workflows and scripts as internal apps.
Open-source developer infrastructure for internal tools (APIs, background jobs, workflows and UIs). Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIs and custom UIs to trigger workflows and scripts as internal apps.
<p align=center>
Scripts are turned into UIs and no-code modules, no-code modules can be composed into rich flows, and script and flows can be triggered from internal UIs made with a low-code builder. Supported script languages supported are: Python, TypeScript, Go, Bash, SQL.
Scripts are turned into sharable UIs automatically, and can be composed together into flows or used into richer apps built with low-code. Supported script languages supported are: Python, TypeScript, Go, Bash, SQL, and GraphQL.
</p>
<p align="center">
@@ -71,28 +71,37 @@ https://github.com/windmill-labs/windmill/assets/122811744/0b132cd1-ee67-4505-82
## Main Concepts
1. Define a minimal and generic script in Python, TypeScript, Go or Bash that
solves a specific task. The code can be defined
in the [provided Web IDE](https://www.windmill.dev/docs/code_editor) or [synchronized with your own GitHub repo](https://www.windmill.dev/docs/advanced/cli/sync) (e.g. through [VS Code](https://www.windmill.dev/docs/cli_local_dev/vscode-extension) extension):
solves a specific task. The code can be defined in the
[provided Web IDE](https://www.windmill.dev/docs/code_editor) or
[synchronized with your own GitHub repo](https://www.windmill.dev/docs/advanced/cli/sync)
(e.g. through
[VS Code](https://www.windmill.dev/docs/cli_local_dev/vscode-extension)
extension):
![Step 1](./imgs/windmill-editor.png)
2. Your scripts parameters are automatically parsed and [generate a frontend](https://www.windmill.dev/docs/core_concepts/auto_generated_uis).
2. Your scripts parameters are automatically parsed and
[generate a frontend](https://www.windmill.dev/docs/core_concepts/auto_generated_uis).
![Step 2](./imgs/windmill-run.png)
![Step 3](./imgs/windmill-result.png)
![Step 2](./imgs/windmill-run.png)
3. Make it [flow](https://www.windmill.dev/docs/flows/flow_editor)! You can chain your scripts or scripts made by the community
shared on [WindmillHub](https://hub.windmill.dev).
![Step 3](./imgs/windmill-result.png)
3. Make it [flow](https://www.windmill.dev/docs/flows/flow_editor)! You can
chain your scripts or scripts made by the community shared on
[WindmillHub](https://hub.windmill.dev).
![Step 3](./imgs/windmill-flow.png)
4. Build [complex UIs](https://www.windmill.dev/docs/apps/app_editor) on top of your scripts and flows.
4. Build [complex UIs](https://www.windmill.dev/docs/apps/app_editor) on top of
your scripts and flows.
![Step 4](./imgs/windmill-builder.png)
Scripts and flows can also be triggered by a [cron schedule](https://www.windmill.dev/docs/core_concepts/scheduling) (e.g. '_/5 _ \* \* \*') or
through [webhooks](https://www.windmill.dev/docs/core_concepts/webhooks).
Scripts and flows can also be triggered by a
[cron schedule](https://www.windmill.dev/docs/core_concepts/scheduling) (e.g.
'_/5 _ \* \* \*') or through
[webhooks](https://www.windmill.dev/docs/core_concepts/webhooks).
You can build your entire infra on top of Windmill!
@@ -150,7 +159,8 @@ environment variables for the `wmill` client library to fetch resources and
variables from your instance if necessary. See more:
<https://www.windmill.dev/docs/advanced/local_development>.
To develop & test locally scripts & flows, we recommend using the Windmill VS Code extension: <https://www.windmill.dev/docs/cli_local_dev/vscode-extension>.
To develop & test locally scripts & flows, we recommend using the Windmill VS
Code extension: <https://www.windmill.dev/docs/cli_local_dev/vscode-extension>.
## Stack
@@ -173,9 +183,14 @@ To develop & test locally scripts & flows, we recommend using the Windmill VS Co
## Fastest Self-Hostable Workflow Engine
We have compared Windmill to other self-hostable workflow engines (Airflow, Prefect & Temporal) and Windmill is the most performant solution for both benchmarks: one flow composed of 40 lightweight tasks & one flow composed of 10 long-running tasks.
We have compared Windmill to other self-hostable workflow engines (Airflow,
Prefect & Temporal) and Windmill is the most performant solution for both
benchmarks: one flow composed of 40 lightweight tasks & one flow composed of 10
long-running tasks.
All methodology & results on our [Benchmarks](https://www.windmill.dev/docs/misc/benchmarks/competitors#airflow-setup) page.
All methodology & results on our
[Benchmarks](https://www.windmill.dev/docs/misc/benchmarks/competitors#airflow-setup)
page.
![Fastest workflow engine](./imgs/fastest.png)
@@ -220,7 +235,9 @@ compiling from source or using without a postgres super user, see
### Docker compose
Windmill can be deployed using 3 files: ([docker-compose.yml](./docker-compose.yml), [Caddyfile](./Caddyfile) and a [.env](./.env)) in a single command.
Windmill can be deployed using 3 files:
([docker-compose.yml](./docker-compose.yml), [Caddyfile](./Caddyfile) and a
[.env](./.env)) in a single command.
Make sure Docker is started, and run:
@@ -238,7 +255,8 @@ The default super-admin user is: admin@windmill.dev / changeme.
From there, you can follow the setup app and create other users.
More details in [Self-Host Documention](https://www.windmill.dev/docs/advanced/self_host#docker).
More details in
[Self-Host Documention](https://www.windmill.dev/docs/advanced/self_host#docker).
### Kubernetes (k8s) and Helm charts
@@ -262,22 +280,24 @@ wget "$ARTIFACT_URL" -O windmill
Windmill Community Edition allows to configure the OAuth, SSO (including Google
Workspace SSO, Microsoft/Azure and Okta) directly from the UI in the superadmin
settings. Do note that there is a limit of 50 SSO users on the community
settings. Do note that there is a limit of 10 SSO users on the community
edition.
[See documentation](https://www.windmill.dev/docs/misc/setup_oauth).
### Commercial license
To self-host Windmill, you must respect the terms of the [AGPLv3 license](https://www.gnu.org/licenses/agpl-3.0.en.html) which
you do not need to worry about for personal uses. For business uses, you should
be fine if you do not re-expose Windmill in any way to your users and are
comfortable with AGPLv3.
To self-host Windmill, you must respect the terms of the
[AGPLv3 license](https://www.gnu.org/licenses/agpl-3.0.en.html) which you do not
need to worry about for personal uses. For business uses, you should be fine if
you do not re-expose Windmill in any way to your users and are comfortable with
AGPLv3.
To [re-expose any Windmill parts to your users](https://www.windmill.dev/docs/misc/white_labelling) as a feature of your product, or
to build a feature on top of Windmill, to comply with AGPLv3 your product must
be AGPLv3 or you must get a commercial license. Contact us at
<ruben@windmill.dev> if you have any doubts.
To
[re-expose any Windmill parts to your users](https://www.windmill.dev/docs/misc/white_labelling)
as a feature of your product, or to build a feature on top of Windmill, to
comply with AGPLv3 your product must be AGPLv3 or you must get a commercial
license. Contact us at <ruben@windmill.dev> if you have any doubts.
In addition, a commercial license grants you a dedicated engineer to transition
your current infrastructure to Windmill, support with tight SLA, and our global
@@ -286,91 +306,94 @@ nodes to 200+ nodes.
### Integrations
In Windmill, integrations are referred to as [resources and resource types](https://www.windmill.dev/docs/core_concepts/resources_and_types). Each Resource has a Resource Type that defines the schema that the resource needs to implement.
In Windmill, integrations are referred to as
[resources and resource types](https://www.windmill.dev/docs/core_concepts/resources_and_types).
Each Resource has a Resource Type that defines the schema that the resource
needs to implement.
On self-hosted instances, you might want to import all the approved resource types from
[WindmillHub](https://hub.windmill.dev). A setup script will prompt you to have
it being synced automatically everyday.
On self-hosted instances, you might want to import all the approved resource
types from [WindmillHub](https://hub.windmill.dev). A setup script will prompt
you to have it being synced automatically everyday.
## Environment Variables
| Environment Variable name | Default | Description | Api Server/Worker/All |
| --------------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All |
| WORKER_GROUP | default | The worker group the worker belongs to and get its configuration pulled from | Worker |
| MODE | standalone | The mode if the binary. Possible values: standalone, worker, server | All |
| SERVER_BIND_ADDR | 0.0.0.0 | IP Address on which to bind listening socket | Server |
| PORT | 8000 | Exposed port | Server |
| METRICS_ADDR | None | (ee only) The socket addr at which to expose Prometheus metrics at the /metrics path. Set to "true" to expose it on port 8001 | All |
| JSON_FMT | false | Output the logs in json format instead of logfmt | All |
| BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance. Is overriden by the instance settings if any. | Server |
| TIMEOUT | 60 _ 60 _ 24 \* 7 (1 week) | The maximum time of execution of a script. When reached, the job is failed as having timedout. |
| SCRIPT_TOKEN_EXPIRY | 900 | The default duration period of the ephemeral-token generated at the beginning of a script | Worker |
| ZOMBIE_JOB_TIMEOUT | 30 | The timeout after which a job is considered to be zombie if the worker did not send pings about processing the job (every server check for zombie jobs every 30s) | Server |
| RESTART_ZOMBIE_JOBS | true | If true then a zombie job is restarted (in-place with the same uuid and some logs), if false the zombie job is failed | Server |
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
| MAX_LOG_SIZE | 500000 | The maximum number of characters a job can emit (log + result) | Worker |
| DISABLE_NUSER | false | If Nsjail is enabled, disable the nsjail's `clone_newuser` setting | Worker |
| KEEP_JOB_DIR | false | Keep the job directory after the job is done. Useful for debugging. | Worker |
| LICENSE_KEY (EE only) | None | License key checked at startup for the Enterprise Edition of Windmill | Worker |
| S3_CACHE_BUCKET (EE only) | None | The S3 bucket to sync the cache of the workers to | Worker |
| TAR_CACHE_RATE (EE only) | 100 | The rate at which to tar the cache of the workers. 100 means every 100th job in average (uniformly randomly distributed). | Worker |
| SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See [Slack documentation](https://api.slack.com/authentication/verifying-requests-from-slack) | Server |
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server |
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
| PYTHON_PATH | /usr/local/bin/python3 | The path to the python binary. | Worker |
| GO_PATH | /usr/bin/go | The path to the go binary. | Worker |
| GOPRIVATE | | The GOPRIVATE env variable to use private go modules | Worker |
| GOPROXY | | The GOPROXY env variable to use | Worker |
| NETRC | | The netrc content to use a private go registry | Worker |
| PIP_INDEX_URL | None | The index url to pass for pip. | Worker |
| PIP_EXTRA_INDEX_URL | None | The extra index url to pass to pip. | Worker |
| PIP_TRUSTED_HOST | None | The trusted host to pass to pip. | Worker |
| PATH | None | The path environment variable, usually inherited | Worker |
| HOME | None | The home directory to use for Go and Bash , usually inherited | Worker |
| DATABASE_CONNECTIONS | 50 (Server)/3 (Worker) | The max number of connections in the database connection pool | All |
| SUPERADMIN_SECRET | None | A token that would let the caller act as a virtual superadmin superadmin@windmill.dev | Server |
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
| DENO_EXTRA_IMPORT_MAP | None | extra import map to use to run deno scripts (format: `key=value,key=value`) | Worker |
| NPM_CONFIG_REGISTRY | None | Registry to use for NPM dependencies, set if you have a private repository you need to use instead of the default public NPM registry | Worker |
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | |
| ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker |
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |
| NUM_WORKERS | 1 | The number of worker per Worker instance (Set to 0 for API/Server instances, Set to 1 for normal workers, and > 1 for workers dedicated to native jobs) | Worker |
| INSTANCE_EVENTS_WEBHOOK | None | Webhook to notify of events such as new user added, signup/invite. Can hook back to windmill to send emails |
| GLOBAL_CACHE_INTERVAL | 10\*60 | (Enterprise Edition only) Interval in seconds in between bucket sync of the cache. This interval \* 2 is the time at which you're guaranteed all the worker's caches are synced together. | Worker |
| WORKER_TAGS | 'deno,go,python3,bash,flow,hub,dependency' | The worker groups assigned to that workers | Worker |
| DEDICATED_WORKER | None | Unique script to run on that worker. Has to be in the form of `<workspace>:<script_path>` | Worker |
| CUSTOM_TAGS | None | The custom tags assignable to scripts. | Server |
| JOB_RETENTION_SECS | 60*60*24\*60 //60 days | **Overriden by the instance settings UI** The time in seconds after which jobs get deleted. Set to 0 or -1 to never delete |
| WAIT_RESULT_FAST_POLL_INTERVAL_MS | 50 | The time in between polling for the run_wait_result endpoints in fast poll mode | Server |
| WAIT_RESULT_SLOW_POLL_INTERVAL_MS | 200 | The time in between polling for the run_wait_result endpoints in fast poll mode | Server |
| WAIT_RESULT_FAST_POLL_DURATION_SECS | 2 | The duration of fast poll mode before switching to slow poll | Server |
| EXIT_AFTER_NO_JOB_FOR_SECS | None | Exit worker if no job is received after duration in secs if defined | Worker |
| OAUTH_JSON_AS_BASE64 | None | Base64 encoded JSON of the OAuth configuration. e.g `OAUTH_JSON_AS_BASE64=$(base64 oauth.json \| tr -d '\n')` to encode it | Server |
| REQUEST_SIZE_LIMIT | 2097152 (2MB) | Max request size which impact the maximum size of resources and payload size of job args | Server |
| ACCEPT_INVALID_CERTS | false | Accept invalid certificates, including self-signed and expired certificates | Server |
| HTTP_PROXY | None | http_proxy | Server + Worker |
| HTTPS_PROXY | None | https_proxy | Server + Worker |
| NO_PROXY | None | no_proxy | Server + Worker |
| SMTP_FROM | None | the address to use as the from field for emails send | Server |
| SMTP_HOST | None | host for the smtp server to send invite emails | Server |
| SMTP_PORT | 587 | port for the smtp server to send invite emails | Server |
| SMTP_USERNAME | None | username for the smtp server to send invite emails | Server |
| SMTP_PASSWORD | None | password for the smtp server to send invite emails | Server |
| SMTP_TLS_IMPLICIT | false | https://docs.rs/mail-send/latest/mail_send/struct.SmtpClientBuilder.html#method.implicit_tlsemails | Server |
| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmin can create workspaces | Server |
| GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE | None | Path to a script or flow to run when a root job fails. The path needs to be prefixed with either `script/` or `flow/` to indicate the kind of error handler being used (assuming `script/` by default). The error handler will be run in and from the admins workspace | Server |
| WHITELIST_ENVS | None | List of envs variables, separated by a ',' that are whitelisted as being safe to passthrough the workers | Worker |
| SAML_METADATA | None | SAML Metadata URL to enable SAML SSO (EE only) | Server |
| SECRET_SALT | None | Secret Salt used for encryption and decryption of secrets. If defined, the secrets will not be decryptable unless the right salt is passed in, which is the case for the workers and the server | Server + Worker |
| OPENAI_AZURE_BASE_PATH | None | Azure OpenAI path to be used instead of the OpenAI path. All Windmill AI features will run on the specified deployed model. Format: `https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}` | Server |
| DISABLE_EMBEDDING | false | Disable local embedding search of hub scripts | Server |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
| DISABLE_SERVER | false | Disable the external API, operate as a worker only instance | Worker |
| Environment Variable name | Default | Description | Api Server/Worker/All |
| --------------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All |
| WORKER_GROUP | default | The worker group the worker belongs to and get its configuration pulled from | Worker |
| MODE | standalone | The mode if the binary. Possible values: standalone, worker, server | All |
| SERVER_BIND_ADDR | 0.0.0.0 | IP Address on which to bind listening socket | Server |
| PORT | 8000 | Exposed port | Server |
| METRICS_ADDR | None | (ee only) The socket addr at which to expose Prometheus metrics at the /metrics path. Set to "true" to expose it on port 8001 | All |
| JSON_FMT | false | Output the logs in json format instead of logfmt | All |
| BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance. Is overriden by the instance settings if any. | Server |
| TIMEOUT | 60 _ 60 _ 24 \* 7 (1 week) | The maximum time of execution of a script. When reached, the job is failed as having timedout. |
| SCRIPT_TOKEN_EXPIRY | 900 | The default duration period of the ephemeral-token generated at the beginning of a script | Worker |
| ZOMBIE_JOB_TIMEOUT | 30 | The timeout after which a job is considered to be zombie if the worker did not send pings about processing the job (every server check for zombie jobs every 30s) | Server |
| RESTART_ZOMBIE_JOBS | true | If true then a zombie job is restarted (in-place with the same uuid and some logs), if false the zombie job is failed | Server |
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
| MAX_LOG_SIZE | 500000 | The maximum number of characters a job can emit (log + result) | Worker |
| DISABLE_NUSER | false | If Nsjail is enabled, disable the nsjail's `clone_newuser` setting | Worker |
| KEEP_JOB_DIR | false | Keep the job directory after the job is done. Useful for debugging. | Worker |
| LICENSE_KEY (EE only) | None | License key checked at startup for the Enterprise Edition of Windmill | Worker |
| S3_CACHE_BUCKET (EE only) | None | The S3 bucket to sync the cache of the workers to | Worker |
| TAR_CACHE_RATE (EE only) | 100 | The rate at which to tar the cache of the workers. 100 means every 100th job in average (uniformly randomly distributed). | Worker |
| SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See [Slack documentation](https://api.slack.com/authentication/verifying-requests-from-slack) | Server |
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server |
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
| PYTHON_PATH | /usr/local/bin/python3 | The path to the python binary. | Worker |
| GO_PATH | /usr/bin/go | The path to the go binary. | Worker |
| GOPRIVATE | | The GOPRIVATE env variable to use private go modules | Worker |
| GOPROXY | | The GOPROXY env variable to use | Worker |
| NETRC | | The netrc content to use a private go registry | Worker |
| PIP_INDEX_URL | None | The index url to pass for pip. | Worker |
| PIP_EXTRA_INDEX_URL | None | The extra index url to pass to pip. | Worker |
| PIP_TRUSTED_HOST | None | The trusted host to pass to pip. | Worker |
| PATH | None | The path environment variable, usually inherited | Worker |
| HOME | None | The home directory to use for Go and Bash , usually inherited | Worker |
| DATABASE_CONNECTIONS | 50 (Server)/3 (Worker) | The max number of connections in the database connection pool | All |
| SUPERADMIN_SECRET | None | A token that would let the caller act as a virtual superadmin superadmin@windmill.dev | Server |
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
| DENO_EXTRA_IMPORT_MAP | None | extra import map to use to run deno scripts (format: `key=value,key=value`) | Worker |
| NPM_CONFIG_REGISTRY | None | Registry to use for NPM dependencies, set if you have a private repository you need to use instead of the default public NPM registry | Worker |
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | |
| ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker |
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |
| NUM_WORKERS | 1 | The number of worker per Worker instance (Set to 0 for API/Server instances, Set to 1 for normal workers, and > 1 for workers dedicated to native jobs) | Worker |
| INSTANCE_EVENTS_WEBHOOK | None | Webhook to notify of events such as new user added, signup/invite. Can hook back to windmill to send emails |
| GLOBAL_CACHE_INTERVAL | 10\*60 | (Enterprise Edition only) Interval in seconds in between bucket sync of the cache. This interval \* 2 is the time at which you're guaranteed all the worker's caches are synced together. | Worker |
| WORKER_TAGS | 'deno,go,python3,bash,flow,hub,dependency' | The worker groups assigned to that workers | Worker |
| DEDICATED_WORKER | None | Unique script to run on that worker. Has to be in the form of `<workspace>:<script_path>` | Worker |
| CUSTOM_TAGS | None | The custom tags assignable to scripts. | Server |
| JOB_RETENTION_SECS | 60*60*24\*60 //60 days | **Overriden by the instance settings UI** The time in seconds after which jobs get deleted. Set to 0 or -1 to never delete |
| WAIT_RESULT_FAST_POLL_INTERVAL_MS | 50 | The time in between polling for the run_wait_result endpoints in fast poll mode | Server |
| WAIT_RESULT_SLOW_POLL_INTERVAL_MS | 200 | The time in between polling for the run_wait_result endpoints in fast poll mode | Server |
| WAIT_RESULT_FAST_POLL_DURATION_SECS | 2 | The duration of fast poll mode before switching to slow poll | Server |
| EXIT_AFTER_NO_JOB_FOR_SECS | None | Exit worker if no job is received after duration in secs if defined | Worker |
| OAUTH_JSON_AS_BASE64 | None | Base64 encoded JSON of the OAuth configuration. e.g `OAUTH_JSON_AS_BASE64=$(base64 oauth.json \| tr -d '\n')` to encode it | Server |
| REQUEST_SIZE_LIMIT | 2097152 (2MB) | Max request size which impact the maximum size of resources and payload size of job args | Server |
| ACCEPT_INVALID_CERTS | false | Accept invalid certificates, including self-signed and expired certificates | Server |
| HTTP_PROXY | None | http_proxy | Server + Worker |
| HTTPS_PROXY | None | https_proxy | Server + Worker |
| NO_PROXY | None | no_proxy | Server + Worker |
| SMTP_FROM | None | the address to use as the from field for emails send | Server |
| SMTP_HOST | None | host for the smtp server to send invite emails | Server |
| SMTP_PORT | 587 | port for the smtp server to send invite emails | Server |
| SMTP_USERNAME | None | username for the smtp server to send invite emails | Server |
| SMTP_PASSWORD | None | password for the smtp server to send invite emails | Server |
| SMTP_TLS_IMPLICIT | false | https://docs.rs/mail-send/latest/mail_send/struct.SmtpClientBuilder.html#method.implicit_tlsemails | Server |
| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmin can create workspaces | Server |
| GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE | None | Path to a script or flow to run when a root job fails. The path needs to be prefixed with either `script/` or `flow/` to indicate the kind of error handler being used (assuming `script/` by default). The error handler will be run in and from the admins workspace | Server |
| WHITELIST_ENVS | None | List of envs variables, separated by a ',' that are whitelisted as being safe to passthrough the workers | Worker |
| SAML_METADATA | None | SAML Metadata URL to enable SAML SSO (EE only) | Server |
| SECRET_SALT | None | Secret Salt used for encryption and decryption of secrets. If defined, the secrets will not be decryptable unless the right salt is passed in, which is the case for the workers and the server | Server + Worker |
| OPENAI_AZURE_BASE_PATH | None | Azure OpenAI path to be used instead of the OpenAI path. All Windmill AI features will run on the specified deployed model. Format: `https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}` | Server |
| DISABLE_EMBEDDING | false | Disable local embedding search of hub scripts | Server |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
| DISABLE_SERVER | false | Disable the external API, operate as a worker only instance | Worker |
## Run a local dev setup

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM account WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "08c943c4b9b76da43b8b754e5c66ab3b6397e7edc82666662a8ea466496290fb"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH uuid_table as (\n select gen_random_uuid() as uuid from generate_series(1, $11)\n )\n INSERT INTO queue \n (id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s)\n (SELECT uuid, $1, $2, $3, $4, ('{ \"uuid\": \"' || uuid || '\" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13 FROM uuid_table) \n RETURNING id",
"query": "WITH uuid_table as (\n select gen_random_uuid() as uuid from generate_series(1, $11)\n )\n INSERT INTO queue \n (id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout)\n (SELECT uuid, $1, $2, $3, $4, ('{ \"uuid\": \"' || uuid || '\" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14 FROM uuid_table) \n RETURNING id",
"describe": {
"columns": [
{
@@ -31,7 +31,8 @@
"postgresql",
"noop",
"appdependencies",
"deploymentcallback"
"deploymentcallback",
"singlescriptflow"
]
}
}
@@ -47,6 +48,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -66,6 +68,7 @@
"Varchar",
"Int4",
"Int4",
"Int4",
"Int4"
]
},
@@ -73,5 +76,5 @@
false
]
},
"hash": "a9915d3c72a278ff08fbe6571c2a4fc993507e255004107345b7151322e063de"
"hash": "0a686ca61444d7ad7484071727aa039a6ea6697e5a49a633b767c052aa3e0a18"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value\n FROM global_settings\n WHERE name = 'openai_azure_base_path'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "0cc3618495d5d024b2a173c58a3a8bb2a9d69b7b6e7ed6b0d0064fa2ce9c2e31"
}

View File

@@ -46,7 +46,8 @@
"postgresql",
"noop",
"appdependencies",
"deploymentcallback"
"deploymentcallback",
"singlescriptflow"
]
}
}
@@ -68,6 +69,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT git_sync FROM workspace_settings WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "git_sync",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "266741894b9a6ed650cf999b6e5b7c797424f8291517d3abf096fe45548a0273"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE job_stats SET timestamps = timestamps || $4, timeseries_float = timeseries_float || $5 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text",
"TimestamptzArray",
"Float4Array"
]
},
"nullable": []
},
"hash": "2ce7a562b1de63f767758642a4864ed982b74b53a143901480058f957b9641dc"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use from script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND lock IS not NULL AND lock_error_logs IS NULL)",
"query": "select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, timeout FROM script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND archived = false)",
"describe": {
"columns": [
{
@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -66,8 +67,8 @@
},
{
"ordinal": 8,
"name": "delete_after_use",
"type_info": "Bool"
"name": "timeout",
"type_info": "Int4"
}
],
"parameters": {
@@ -88,5 +89,5 @@
true
]
},
"hash": "f760667ffd6727443f023480627f62728bf9b6284d92e5fa9b97dc4e0b1c3a93"
"hash": "2f42460fdd8aa125c8fd46b3cd02e47f57de0f073d3ce3bc7d21a7e404a83b5c"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM dependency_map\n WHERE importer_path = $1 AND importer_kind = 'script'\n AND workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "30e9250edd0c4c6025322f51b758174435d6df4f835cb0883ba40897cb99a410"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow_status->'cleanup_module' as cleanup_module\n FROM queue\n WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "cleanup_module",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "322038aae939d738f47b5bddaec2a6342e25e815dcd8b9fd620ddb81063c5d76"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT schedule.*, t.jobs FROM schedule, LATERAL ( SELECT ARRAY (SELECT json_build_object('id', id, 'success', success, 'duration_ms', duration_ms) FROM completed_job WHERE\n completed_job.schedule_path = schedule.path AND completed_job.workspace_id = $1 AND parent_job IS NULL ORDER BY started_at DESC LIMIT 20) AS jobs ) t\n WHERE schedule.workspace_id = $1 ORDER BY schedule.edited_at desc LIMIT $2 OFFSET $3",
"query": "SELECT schedule.*, t.jobs FROM schedule, LATERAL ( SELECT ARRAY (SELECT json_build_object('id', id, 'success', success, 'duration_ms', duration_ms) FROM completed_job WHERE\n completed_job.schedule_path = schedule.path AND completed_job.workspace_id = $1 AND parent_job IS NULL AND is_skipped = False ORDER BY started_at DESC LIMIT 20) AS jobs ) t\n WHERE schedule.workspace_id = $1 ORDER BY schedule.edited_at desc LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
@@ -110,6 +110,21 @@
},
{
"ordinal": 21,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 24,
"name": "jobs",
"type_info": "JsonArray"
}
@@ -143,8 +158,11 @@
true,
true,
false,
true,
true,
false,
null
]
},
"hash": "7a00843921db4ac1eb20a1045e83363587c1a2cb62d2df25d5564ae78a03b097"
"hash": "4422b7183ede17a9cbde4afae41be4da5447020e39398deedbcca9121492834a"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)",
"describe": {
"columns": [],
"parameters": {
@@ -28,6 +28,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -63,10 +64,11 @@
"Bool",
"Int2",
"Bool",
"Bool"
"Bool",
"Int4"
]
},
"nullable": []
},
"hash": "7db59e88a10150ab6a3e81f164343e6bb2be6149fb690e63b8a0c376f39e9924"
"hash": "4a393e61e33c1204c01044254692629e6ee2c053c7b1fa10f84462a327c739b3"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM token WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "4c5276bbf6aaad004a58aaff6b2abd7e78903a0354dd041fcc0cc7cca2b60cc1"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO dependency_map (workspace_id, importer_path, importer_kind, imported_path)\n VALUES ($1, $2, 'script', $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "5fbd5dfb8df9aecce023c5d482ee58efcbc23c8682a3f0ec94977eb56023ed72"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM input WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "60cd5ea09b2eba9df97ab011dd8575940a9dd5e244f3e877d3802fc88087234b"
}

View File

@@ -41,7 +41,8 @@
"postgresql",
"noop",
"appdependencies",
"deploymentcallback"
"deploymentcallback",
"singlescriptflow"
]
}
}
@@ -61,6 +62,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT true FROM job_stats WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a"
}

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE job_stats SET scalar_int = $4 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text",
"Int4"
]
},
"nullable": []
},
"hash": "699427e7e3e6cd3ba4f61f77e171c5c27234cfa30460696ac619760667d7553c"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use from script where hash = $1 AND workspace_id = $2",
"query": "select tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout from script where hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -37,6 +37,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -63,6 +64,11 @@
"ordinal": 7,
"name": "delete_after_use",
"type_info": "Bool"
},
{
"ordinal": 8,
"name": "timeout",
"type_info": "Int4"
}
],
"parameters": {
@@ -79,8 +85,9 @@
false,
true,
true,
true,
true
]
},
"hash": "0234079d04372b82ec8f2990916433bd8771c21d22a1db02eb1e388a8bfb28ff"
"hash": "6b313cc9a57ae3c943bda4a3213f7f6231a44b6ef5a52754074d136007f4f72a"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, ws_error_handler_muted) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING *",
"query": "INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, ws_error_handler_muted, retry, summary, no_flow_overlap ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21 ) RETURNING *",
"describe": {
"columns": [
{
@@ -107,6 +107,21 @@
"ordinal": 20,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 21,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
}
],
"parameters": {
@@ -128,6 +143,9 @@
"Varchar",
"Int4",
"Json",
"Bool",
"Jsonb",
"Varchar",
"Bool"
]
},
@@ -152,8 +170,11 @@
true,
true,
true,
false,
true,
true,
false
]
},
"hash": "47a01c0924df7bc2695f8238425aa7eadf845e4156da8cf645582737e046589d"
"hash": "6bb9e6cab7034bf28ef2597445dc617b60c49b42568fdb975cf2d2dc8702167e"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE job_stats SET timestamps = timestamps || $4, timeseries_int = timeseries_int || $5 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text",
"TimestamptzArray",
"Int4Array"
]
},
"nullable": []
},
"hash": "73a3a742702c1c8237a3216523dffe487146b14512d4882179978efe280956aa"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_stats WHERE job_id IN (SELECT id FROM completed_job WHERE created_at <= now() - ($1::bigint::text || ' s')::interval AND started_at + ((duration_ms/1000 + $1::bigint) || ' s')::interval <= now())",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "74949628ea78a5fecfd415e6406636a9442edd5b7939f61894288103be0c03ef"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM dependency_map WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "76d73f2fe5ac75e5033fb93d60f2ef5ab2b0b73a80dcb6a250c17c3b2f15c6b4"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM deployment_metadata WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "813d3c65d4c4bc10f1ec16ed4ee0954f4b938f45751ac03fd45479e665db1d40"
}

View File

@@ -107,6 +107,21 @@
"ordinal": 20,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 21,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
}
],
"parameters": {
@@ -137,6 +152,9 @@
true,
true,
true,
false,
true,
true,
false
]
},

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET canceled = true, canceled_by = $2, scheduled_for = now(), suspend = 0 WHERE workspace_id = $1 AND schedule_path IS NULL RETURNING id, running",
"query": "UPDATE queue SET canceled = true, canceled_by = $2, scheduled_for = now(), suspend = 0 WHERE scheduled_for < now() AND workspace_id = $1 AND schedule_path IS NULL RETURNING id, running",
"describe": {
"columns": [
{
@@ -25,5 +25,5 @@
false
]
},
"hash": "ce4733a0460cbef39fb50f4ca9746944d4cd5057d27d09de9c5dc494e3123ab4"
"hash": "917afc04613a7d4373c9152b8ecaa946a2abfb3e5be437ee3b0aa05b3bd39702"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM raw_app WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "96a77e839d6210629234879eeeaa23d9bddf5f751fe27d999659f6597f9759f1"
}

View File

@@ -107,6 +107,21 @@
"ordinal": 20,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 21,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
}
],
"parameters": {
@@ -138,6 +153,9 @@
true,
true,
true,
false,
true,
true,
false
]
},

View File

@@ -25,7 +25,8 @@
"postgresql",
"noop",
"appdependencies",
"deploymentcallback"
"deploymentcallback",
"singlescriptflow"
]
}
}
@@ -41,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -107,6 +107,21 @@
"ordinal": 20,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 21,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
}
],
"parameters": {
@@ -136,6 +151,9 @@
true,
true,
true,
false,
true,
true,
false
]
},

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT importer_path FROM dependency_map\n WHERE imported_path = $1 AND importer_kind = 'script'\n AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "importer_path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "aa44311f9bae8253c6c483d122597ce7e470c6ba1905e7e877dca35d9d68968f"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value\n FROM resource\n WHERE path = $1 AND workspace_id = $2",
"query": "SELECT value\n FROM resource\n WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -19,5 +19,5 @@
true
]
},
"hash": "500393aa7c3f8c96ec8d92869c5c4744eebad8e9e31336c26cf51e678c3556f8"
"hash": "acdaa5151f8f7f37bb8c8c5a7d146789887e47db9695fc26b1dfaedd735e1e60"
}

View File

@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT count(*) FROM queue WHERE schedule_path = $1 AND workspace_id = $2 AND id != $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "aeedd3d92f7c50fe49d14053afebd82837093636da2950915ce4364106f5679c"
}

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE job_stats SET scalar_float = $4 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text",
"Float4"
]
},
"nullable": []
},
"hash": "b56111c5b81de2449a1b687656a4636457da851a4db5e916cf4f6847c48c6bd3"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select path, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use from script where hash = $1 AND workspace_id = $2",
"query": "select path, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout from script where hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -68,6 +69,11 @@
"ordinal": 8,
"name": "delete_after_use",
"type_info": "Bool"
},
{
"ordinal": 9,
"name": "timeout",
"type_info": "Int4"
}
],
"parameters": {
@@ -85,8 +91,9 @@
false,
true,
true,
true,
true
]
},
"hash": "e541faa52c530c787d45c5ee70930687587c139606056bdc06cd860abeb0805b"
"hash": "b69891c25dd029b1a54e97ace292433e1485324ff7dc802fe75d21c8c6db1d42"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, on_recovery_extra_args = $10, ws_error_handler_muted = $11 \n WHERE path = $12 AND workspace_id = $13 RETURNING *",
"query": "UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12, summary = $13, no_flow_overlap = $14 WHERE path = $15 AND workspace_id = $16 RETURNING *",
"describe": {
"columns": [
{
@@ -107,6 +107,21 @@
"ordinal": 20,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 21,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
}
],
"parameters": {
@@ -122,6 +137,9 @@
"Int4",
"Json",
"Bool",
"Jsonb",
"Varchar",
"Bool",
"Text",
"Text"
]
@@ -147,8 +165,11 @@
true,
true,
true,
false,
true,
true,
false
]
},
"hash": "2157813137b383411227b06814143df2d2187d1d43b84f67d22c0a2365b42ada"
"hash": "b7ed211ec7611c994a49565e46e5893788bcf49b2c9357fbc45a8353a7c84f48"
}

View File

@@ -107,6 +107,21 @@
"ordinal": 20,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 21,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
}
],
"parameters": {
@@ -137,6 +152,9 @@
true,
true,
true,
false,
true,
true,
false
]
},

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_stats WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "d77223fc94257711512b6ff21ce109b97324f2917113f04492f53b20860e9588"
}

View File

@@ -107,6 +107,21 @@
"ordinal": 20,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 21,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
}
],
"parameters": {
@@ -137,6 +152,9 @@
true,
true,
true,
false,
true,
true,
false
]
},

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT content FROM script WHERE hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "content",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false
]
},
"hash": "deb2e4c21d4f9d0b9439780ca6ebee13450609b578c469cb437c5420ab520258"
}

View File

@@ -107,6 +107,21 @@
"ordinal": 20,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 21,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
}
],
"parameters": {
@@ -137,6 +152,9 @@
true,
true,
true,
false,
true,
true,
false
]
},

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority from script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND archived = false)",
"query": "select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout from script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND lock IS not NULL AND lock_error_logs IS NULL)",
"describe": {
"columns": [
{
@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -63,6 +64,16 @@
"ordinal": 7,
"name": "priority",
"type_info": "Int2"
},
{
"ordinal": 8,
"name": "delete_after_use",
"type_info": "Bool"
},
{
"ordinal": 9,
"name": "timeout",
"type_info": "Int4"
}
],
"parameters": {
@@ -79,8 +90,10 @@
true,
false,
true,
true,
true,
true
]
},
"hash": "c7febee7146a28481ce36f3570bceb38751a6527827525a582d9f8804981f552"
"hash": "ef132ac8d79579b08d7359789b6f22991f51e1c945efc2924df6253d62b83bba"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT no_flow_overlap FROM schedule WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "no_flow_overlap",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "f2173822c1b3d82afedf0f7dba31e218539f0b259eb5f2c98e4e8ada6665f5ee"
}

View File

@@ -40,7 +40,8 @@
"postgresql",
"noop",
"appdependencies",
"deploymentcallback"
"deploymentcallback",
"singlescriptflow"
]
}
}
@@ -63,7 +64,8 @@
"postgresql",
"noop",
"appdependencies",
"deploymentcallback"
"deploymentcallback",
"singlescriptflow"
]
}
}

View File

@@ -107,6 +107,21 @@
"ordinal": 20,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 21,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
}
],
"parameters": {
@@ -135,6 +150,9 @@
true,
true,
true,
false,
true,
true,
false
]
},

1047
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.224.0"
version = "1.236.2"
authors.workspace = true
edition.workspace = true
@@ -11,6 +11,7 @@ members = [
"./windmill-worker",
"./windmill-common",
"./windmill-audit",
"./windmill-git-sync",
"./parsers/windmill-parser",
"./parsers/windmill-parser-ts",
"./parsers/windmill-parser-wasm",
@@ -21,7 +22,7 @@ members = [
]
[workspace.package]
version = "1.224.0"
version = "1.236.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -34,8 +35,8 @@ opt-level = 0
incremental = true
[features]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise"]
saml = ["windmill-api/saml"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-git-sync/enterprise"]
enterprise_saml = ["windmill-api/enterprise_saml"]
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark"]
flamegraph = ["windmill-common/flamegraph", "windmill-worker/flamegraph"]
loki = ["windmill-common/loki"]
@@ -46,6 +47,7 @@ anyhow.workspace = true
tokio.workspace = true
dotenv.workspace = true
windmill-common = { workspace = true, features = ["tracing_init"] }
windmill-git-sync.workspace = true
windmill-api.workspace = true
windmill-worker.workspace = true
futures.workspace = true
@@ -66,7 +68,7 @@ uuid.workspace = true
gethostname.workspace = true
serde_json.workspace = true
serde.workspace = true
pg-embed = {git = "https://github.com/faokunega/pg-embed", optional = true}
pg-embed = {git = "https://github.com/faokunega/pg-embed", optional = true, default-features = false}
[dev-dependencies]
serde_json.workspace = true
@@ -83,6 +85,7 @@ windmill-queue = { path = "./windmill-queue" }
windmill-worker = { path = "./windmill-worker" }
windmill-common = { path = "./windmill-common" }
windmill-audit = { path = "./windmill-audit" }
windmill-git-sync = { path = "./windmill-git-sync" }
windmill-parser = { path = "./parsers/windmill-parser" }
windmill-parser-ts = { path = "./parsers/windmill-parser-ts" }
windmill-parser-py = { path = "./parsers/windmill-parser-py" }
@@ -153,7 +156,7 @@ swc_ecma_visit = "0.93.7"
base64 = "0.21.0"
hmac = "0.12.1"
sha2 = "0.10.6"
sqlx = { version = "^0", features = [
sqlx = { version = "0.7.3", features = [
"macros",
"migrate",
"uuid",
@@ -207,3 +210,8 @@ candle-core = "0.3.0"
candle-transformers = "0.3.0"
candle-nn = "0.3.0"
tiberius = { version = "0.12.2", default-features = false, features = ["rustls", "tds73", "chrono"] }
aws-sdk-s3 = "1.5.0"
aws-config = "1.0.3"
polars = { version = "0.35.2", features = ["lazy", "parquet", "aws", "csv", "dtype-full"] }
polars-io = { version = "0.35.2", features = ["csv"] }
object_store = { version = "0.8.0", features = ["aws"] }

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE schedule DROP COLUMN IF EXISTS retry;

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE schedule ADD COLUMN IF NOT EXISTS retry JSONB;
ALTER TYPE JOB_KIND ADD VALUE IF NOT EXISTS 'singlescriptflow';

View File

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE schedule ADD COLUMN summary VARCHAR(512);
ALTER TABLE schedule ADD COLUMN no_flow_overlap BOOLEAN NOT NULL DEFAULT FALSE;

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
DROP TABLE dependency_map;

View File

@@ -0,0 +1,14 @@
-- Add up migration script here
CREATE TYPE IMPORTER_KIND AS ENUM ('script', 'flow', 'app');
CREATE TABLE IF NOT EXISTS dependency_map (
workspace_id VARCHAR(50) NOT NULL,
importer_path VARCHAR(510) NOT NULL,
importer_kind IMPORTER_KIND NOT NULL,
imported_path VARCHAR(510) NOT NULL,
PRIMARY KEY (workspace_id, importer_path, importer_kind, imported_path)
);
CREATE UNIQUE INDEX IF NOT EXISTS dependency_map_imported_path_idx ON dependency_map (workspace_id, imported_path);

View File

@@ -0,0 +1,3 @@
-- Add down migration script here
DROP TABLE job_stats;
DROP TYPE METRIC_KIND;

View File

@@ -0,0 +1,16 @@
-- Add up migration script here
CREATE TYPE METRIC_KIND AS ENUM ('scalar_int', 'scalar_float', 'timeseries_int', 'timeseries_float');
CREATE TABLE IF NOT EXISTS job_stats (
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id),
job_id UUID NOT NULL,
metric_id VARCHAR(50) NOT NULL,
metric_name VARCHAR(255),
metric_kind METRIC_KIND NOT NULL,
scalar_int INTEGER,
scalar_float REAL,
timestamps TIMESTAMP WITH TIME ZONE[],
timeseries_int INTEGER[],
timeseries_float REAL[],
PRIMARY KEY (workspace_id, job_id, metric_id)
);

View File

@@ -30,7 +30,7 @@ pub fn parse_powershell_sig(code: &str) -> anyhow::Result<MainArgSignature> {
lazy_static::lazy_static! {
static ref RE_BASH: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+):-(.*)\})"(?:[\t ]*)?(?:#.*)?$"#).unwrap();
static ref RE_POWERSHELL_PARAM: Regex = Regex::new(r#"(?m)param[\t ]*\(([^)]*)\)"#).unwrap();
pub static ref RE_POWERSHELL_PARAM: Regex = Regex::new(r#"(?m)param[\t ]*\(([^)]*)\)"#).unwrap();
static ref RE_POWERSHELL_ARGS: Regex = Regex::new(r#"(?:\[(\w+)\])?\$(\w+)[\t ]*(?:=[\t ]*(?:(?:(?:"|')([^"\n\r\$]*)(?:"|'))|([\d.]+)))?"#).unwrap();
}

View File

@@ -63,7 +63,7 @@ lazy_static! {
fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<String> {
if level > 0 {
let mut imports = vec!["requests".to_string()];
let mut imports = vec![];
let splitted_path = path.split("/");
let base = splitted_path
.clone()
@@ -78,10 +78,7 @@ fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<Strin
} else if let Some(module) = module {
let imprt = module.split('.').next().unwrap_or("").replace("_", "-");
if imprt == "u" || imprt == "f" {
vec![
"requests".to_string(),
format!("relative:{}", module.replace(".", "/")),
]
vec![format!("relative:{}", module.replace(".", "/"))]
} else {
vec![imprt]
}
@@ -90,6 +87,57 @@ fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<Strin
}
}
pub fn parse_relative_imports(code: &str, path: &str) -> error::Result<Vec<String>> {
let nimports = parse_code_for_imports(code, path)?;
return Ok(nimports
.into_iter()
.filter_map(|x| {
if x.starts_with("relative:") {
Some(x.replace("relative:", ""))
} else {
None
}
})
.collect());
}
fn parse_code_for_imports(code: &str, path: &str) -> error::Result<Vec<String>> {
let code = code.split(DEF_MAIN).next().unwrap_or("");
let ast = Suite::parse(code, "main.py").map_err(|e| {
error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string()))
})?;
let nimports: Vec<String> = ast
.into_iter()
.filter_map(|x| match x {
Stmt::Import(StmtImport { names, .. }) => Some(
names
.into_iter()
.map(|x| {
let name = x.name.to_string();
process_import(Some(name), path, 0)
})
.flatten()
.collect::<Vec<String>>(),
),
Stmt::ImportFrom(StmtImportFrom { level: Some(i), module, .. }) if i.to_u32() > 0 => {
Some(process_import(
module.map(|x| x.to_string()),
path,
i.to_usize(),
))
}
Stmt::ImportFrom(StmtImportFrom { level: _, module, .. }) => {
Some(process_import(module.map(|x| x.to_string()), path, 0))
}
_ => None,
})
.flatten()
.filter(|x| !STDIMPORTS.contains(&x.as_str()))
.unique()
.collect();
return Ok(nimports);
}
#[async_recursion]
pub async fn parse_python_imports(
code: &str,
@@ -127,41 +175,7 @@ pub async fn parse_python_imports(
imports.extend(lines);
}
let code = code.split(DEF_MAIN).next().unwrap_or("");
let ast = Suite::parse(code, "main.py").map_err(|e| {
error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string()))
})?;
let nimports: Vec<String> = ast
.into_iter()
.filter_map(|x| match x {
Stmt::Import(StmtImport { names, .. }) => Some(
names
.into_iter()
.map(|x| {
let name = x.name.to_string();
process_import(Some(name), path, 0)
})
.flatten()
.collect::<Vec<String>>(),
),
Stmt::ImportFrom(StmtImportFrom { level: Some(i), module, .. })
if i.to_u32() > 0 =>
{
Some(process_import(
module.map(|x| x.to_string()),
path,
i.to_usize(),
))
}
Stmt::ImportFrom(StmtImportFrom { level: _, module, .. }) => {
Some(process_import(module.map(|x| x.to_string()), path, 0))
}
_ => None,
})
.flatten()
.filter(|x| !STDIMPORTS.contains(&x.as_str()))
.unique()
.collect();
let nimports = parse_code_for_imports(code, path)?;
for n in nimports.iter() {
let nested = if n.starts_with("relative:") {
let rpath = n.replace("relative:", "");

View File

@@ -20,7 +20,7 @@ def main():
";
let r = parse_python_imports(code, "test-workspace", "f/foo/bar", &db).await?;
// println!("{}", serde_json::to_string(&r)?);
assert_eq!(r, vec!["matplotlib", "requests", "wmill", "zanzibar"]);
assert_eq!(r, vec!["matplotlib", "wmill", "zanzibar"]);
Ok(())
}
@@ -73,7 +73,6 @@ def main():
"numpy",
"pandas",
"pandas2",
"requests"
]
);

View File

@@ -26,6 +26,53 @@ use swc_ecma_parser::{lexer::Lexer, EsConfig, Parser, StringInput, Syntax, TsCon
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
struct ImportsFinder {
imports: HashSet<String>,
}
impl Visit for ImportsFinder {
noop_visit_type!();
fn visit_import_decl(&mut self, n: &swc_ecma_ast::ImportDecl) {
if let Some(ref s) = n.src.raw {
let s = s.to_string();
if s.starts_with("'") && s.ends_with("'") {
self.imports.insert(s[1..s.len() - 1].to_string());
} else if s.starts_with("\"") && s.ends_with("\"") {
self.imports.insert(s[1..s.len() - 1].to_string());
}
}
}
}
pub fn parse_expr_for_imports(code: &str) -> anyhow::Result<Vec<String>> {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("main.d.ts".into()), code.into());
let lexer = Lexer::new(
Syntax::Typescript(TsConfig::default()),
// EsVersion defaults to es5
Default::default(),
StringInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let mut err_s = "".to_string();
for e in parser.take_errors() {
err_s += &e.into_kind().msg().to_string();
}
let expr = parser.parse_module().map_err(|e| {
anyhow::anyhow!("Error while parsing code, it is invalid TypeScript: {err_s}, {e:?}")
})?;
let mut visitor = ImportsFinder { imports: HashSet::new() };
swc_ecma_visit::visit_module(&mut visitor, &expr);
Ok(visitor.imports.into_iter().collect())
}
struct OutputFinder {
idents: HashSet<(String, String)>,
}
@@ -71,9 +118,9 @@ pub fn parse_expr_for_ids(code: &str) -> anyhow::Result<Vec<(String, String)>> {
err_s += &e.into_kind().msg().to_string();
}
let expr = parser
.parse_module()
.map_err(|_| anyhow::anyhow!("Error while parsing code, it is invalid TypeScript"))?;
let expr = parser.parse_module().map_err(|e| {
anyhow::anyhow!("Error while parsing code, it is invalid TypeScript: {err_s}, {e:?}")
})?;
let mut visitor = OutputFinder { idents: HashSet::new() };
swc_ecma_visit::visit_module(&mut visitor, &expr);
@@ -102,7 +149,9 @@ pub fn parse_deno_signature(code: &str, skip_dflt: bool) -> anyhow::Result<MainA
let ast = parser
.parse_module()
.map_err(|_| anyhow::anyhow!("Error while parsing code, it is invalid TypeScript"))?
.map_err(|e| {
anyhow::anyhow!("Error while parsing code, it is invalid TypeScript: {err_s}, {e:?}")
})?
.body;
let params = ast.into_iter().find_map(|x| match x {

View File

@@ -3,7 +3,7 @@
"collaborators": [
"Ruben Fiszel <ruben@windmill.dev>"
],
"version": "1.222.0",
"version": "1.226.9",
"files": [
"windmill_parser_wasm_bg.wasm",
"windmill_parser_wasm.js",
@@ -14,4 +14,4 @@
"sideEffects": [
"./snippets/*"
]
}
}

View File

@@ -14,6 +14,11 @@ export function parse_outputs(code: string): string;
* @param {string} code
* @returns {string}
*/
export function parse_ts_imports(code: string): string;
/**
* @param {string} code
* @returns {string}
*/
export function parse_bash(code: string): string;
/**
* @param {string} code
@@ -67,6 +72,7 @@ export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly parse_deno: (a: number, b: number, c: number) => void;
readonly parse_outputs: (a: number, b: number, c: number) => void;
readonly parse_ts_imports: (a: number, b: number, c: number) => void;
readonly parse_bash: (a: number, b: number, c: number) => void;
readonly parse_powershell: (a: number, b: number, c: number) => void;
readonly parse_go: (a: number, b: number, c: number) => void;

View File

@@ -97,6 +97,15 @@ function getInt32Memory0() {
return cachedInt32Memory0;
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
@@ -115,15 +124,6 @@ function getFloat64Memory0() {
return cachedFloat64Memory0;
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
let cachedBigInt64Memory0 = null;
function getBigInt64Memory0() {
@@ -243,6 +243,29 @@ export function parse_outputs(code) {
}
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_ts_imports(code) {
let deferred2_0;
let deferred2_1;
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
wasm.parse_ts_imports(retptr, ptr0, len0);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
deferred2_0 = r0;
deferred2_1 = r1;
return getStringFromWasm0(r0, r1);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
@@ -518,10 +541,6 @@ function __wbg_get_imports() {
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
imports.wbg.__wbg_eval_1081105c41705556 = function(arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof(obj) === 'string' ? obj : undefined;
@@ -530,6 +549,10 @@ function __wbg_get_imports() {
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_boolean_get = function(arg0) {
const v = getObject(arg0);
const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2;
@@ -566,8 +589,8 @@ function __wbg_get_imports() {
const ret = getObject(arg0) in getObject(arg1);
return ret;
};
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
imports.wbg.__wbg_eval_596393dc5ae50a1b = function(arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) {

View File

@@ -3,6 +3,7 @@
export const memory: WebAssembly.Memory;
export function parse_deno(a: number, b: number, c: number): void;
export function parse_outputs(a: number, b: number, c: number): void;
export function parse_ts_imports(a: number, b: number, c: number): void;
export function parse_bash(a: number, b: number, c: number): void;
export function parse_powershell(a: number, b: number, c: number): void;
export function parse_go(a: number, b: number, c: number): void;

View File

@@ -1,7 +1,7 @@
use serde_json::{self, json};
use wasm_bindgen::prelude::*;
use windmill_parser::MainArgSignature;
use windmill_parser_ts::parse_expr_for_ids;
use windmill_parser_ts::{parse_expr_for_ids, parse_expr_for_imports};
fn wrap_sig(r: anyhow::Result<MainArgSignature>) -> String {
if let Ok(r) = r {
@@ -27,6 +27,17 @@ pub fn parse_outputs(code: &str) -> String {
return serde_json::to_string(&r).unwrap();
}
#[wasm_bindgen]
pub fn parse_ts_imports(code: &str) -> String {
let parsed = parse_expr_for_imports(code);
let r = if let Ok(parsed) = parsed {
json!({ "imports": parsed })
} else {
json!({"error": parsed.err().unwrap().to_string()})
};
return serde_json::to_string(&r).unwrap();
}
#[wasm_bindgen]
pub fn parse_bash(code: &str) -> String {
wrap_sig(windmill_parser_bash::parse_bash_sig(code))

View File

@@ -1,7 +1,7 @@
use serde_json::json;
use wasm_bindgen_test::wasm_bindgen_test;
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
use windmill_parser_ts::{parse_deno_signature, parse_expr_for_ids};
use windmill_parser_ts::{parse_deno_signature, parse_expr_for_ids, parse_expr_for_imports};
#[wasm_bindgen_test]
fn test_parse_deno_sig() -> anyhow::Result<()> {
@@ -275,3 +275,38 @@ fn test_parse_extract_ident() -> anyhow::Result<()> {
Ok(())
}
#[wasm_bindgen_test]
fn test_parse_imports() -> anyhow::Result<()> {
let code = "
import * as foo from '@foo/bar';
import { bar } from \"./bar\";
import { bar } from \"bar/foo/d\";
import { bar as baroof } from \"bar\";
";
let mut l = parse_expr_for_imports(code)?;
l.sort();
assert_eq!(
l,
vec![
"./bar".to_string(),
"@foo/bar".to_string(),
"bar".to_string(),
"bar/foo/d".to_string()
]
);
Ok(())
}
#[wasm_bindgen_test]
fn test_parse_imports_dts() -> anyhow::Result<()> {
let code = "
export type foo = number
";
let mut l = parse_expr_for_imports(code)?;
l.sort();
assert_eq!(l, vec![] as Vec<String>);
Ok(())
}

View File

@@ -24,9 +24,9 @@ use windmill_common::{
global_settings::{
BASE_URL_SETTING, CUSTOM_TAGS_SETTING, DISABLE_STATS_SETTING, ENV_SETTINGS,
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
RETENTION_PERIOD_SECS_SETTING,
JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
},
stats::schedule_stats,
utils::{rd_string, Mode},
@@ -37,14 +37,14 @@ 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,
GO_CACHE_DIR, GO_TMP_CACHE_DIR, HUB_CACHE_DIR, HUB_TMP_CACHE_DIR, LOCK_CACHE_DIR,
PIP_CACHE_DIR, ROOT_TMP_CACHE_DIR, TAR_PIP_TMP_CACHE_DIR,
PIP_CACHE_DIR, POWERSHELL_CACHE_DIR, ROOT_TMP_CACHE_DIR, TAR_PIP_TMP_CACHE_DIR,
};
use crate::monitor::{
initial_load, load_keep_job_dir, load_require_preexisting_user, monitor_db, monitor_pool,
reload_base_url_setting, reload_extra_pip_index_url_setting, reload_license_key,
reload_npm_config_registry_setting, reload_retention_period_setting, reload_server_config,
reload_worker_config,
reload_base_url_setting, reload_extra_pip_index_url_setting,
reload_job_default_timeout_setting, reload_license_key, reload_npm_config_registry_setting,
reload_retention_period_setting, reload_server_config, reload_worker_config,
};
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
@@ -105,6 +105,12 @@ async fn main() -> anyhow::Result<()> {
if std::env::var("JOB_TOKEN").is_err() {
tracing::warn!("JOB_TOKEN is not passed, hence workers will still create one ephemeral token per job and the DATABASE_URL need to be of a role that can INSERT into the token table")
}
#[cfg(not(feature = "enterprise"))]
{
panic!("Agent mode is only available in the EE, ignoring...");
}
Mode::Agent
} else {
if &x != "standalone" {
@@ -330,6 +336,7 @@ Windmill Community Edition {GIT_VERSION}
num_workers,
base_internal_url.clone(),
rsmq.clone(),
mode.clone() == Mode::Agent,
)
.await?;
tracing::info!("All workers exited.");
@@ -410,6 +417,9 @@ Windmill Community Edition {GIT_VERSION}
RETENTION_PERIOD_SECS_SETTING => {
reload_retention_period_setting(&db).await
},
JOB_DEFAULT_TIMEOUT_SECS_SETTING => {
reload_job_default_timeout_setting(&db).await
},
EXTRA_PIP_INDEX_URL_SETTING => {
reload_extra_pip_index_url_setting(&db).await
},
@@ -421,7 +431,7 @@ Windmill Community Edition {GIT_VERSION}
},
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING => {
load_require_preexisting_user(&db).await;
}
},
EXPOSE_METRICS_SETTING | EXPOSE_DEBUG_METRICS_SETTING => {
if n.payload() != EXPOSE_DEBUG_METRICS_SETTING || worker_mode {
tracing::info!("Metrics setting changed, restarting");
@@ -494,7 +504,14 @@ Windmill Community Edition {GIT_VERSION}
};
let instance_name = rd_string(8);
schedule_stats(instance_name, mode, &db, &HTTP_CLIENT).await;
schedule_stats(
instance_name,
mode.clone(),
&db,
&HTTP_CLIENT,
cfg!(feature = "enterprise"),
)
.await;
futures::try_join!(shutdown_signal, workers_f, monitor_f, server_f, metrics_f)?;
} else {
@@ -561,6 +578,7 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
num_workers: i32,
base_internal_url: String,
rsmq: Option<R>,
agent_mode: bool,
) -> anyhow::Result<()> {
let instance_name = gethostname()
.to_str()
@@ -601,6 +619,7 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
GO_CACHE_DIR,
GO_BIN_CACHE_DIR,
HUB_CACHE_DIR,
POWERSHELL_CACHE_DIR,
TAR_PIP_TMP_CACHE_DIR,
DENO_TMP_CACHE_DIR,
DENO_TMP_CACHE_DIR_DEPS,
@@ -641,6 +660,7 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
&base_internal_url,
rsmq2,
sync_barrier,
agent_mode,
)
.await
})));

View File

@@ -22,9 +22,10 @@ use windmill_common::{
error,
global_settings::{
BASE_URL_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
EXTRA_PIP_INDEX_URL_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
EXTRA_PIP_INDEX_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING,
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
RETENTION_PERIOD_SECS_SETTING,
},
jobs::{JobKind, QueuedJob},
oauth2::REQUIRE_PREEXISTING_USER_FOR_OAUTH,
@@ -34,8 +35,8 @@ use windmill_common::{
BASE_URL, DB, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
};
use windmill_worker::{
create_token_for_owner, handle_job_error, AuthedClient, KEEP_JOB_DIR, NPM_CONFIG_REGISTRY,
PIP_EXTRA_INDEX_URL, SCRIPT_TOKEN_EXPIRY,
create_token_for_owner, handle_job_error, AuthedClient, SendResult, JOB_DEFAULT_TIMEOUT,
KEEP_JOB_DIR, NPM_CONFIG_REGISTRY, PIP_EXTRA_INDEX_URL, SCRIPT_TOKEN_EXPIRY,
};
#[cfg(feature = "enterprise")]
@@ -76,7 +77,6 @@ lazy_static::lazy_static! {
).unwrap();
static ref JOB_RETENTION_SECS: Arc<RwLock<i64>> = Arc::new(RwLock::new(0));
}
pub async fn initial_load(
@@ -167,6 +167,7 @@ pub async fn load_metrics_debug_enabled(db: &DB) -> error::Result<()> {
};
Ok(())
}
pub async fn load_keep_job_dir(db: &DB) {
let value = sqlx::query_scalar!(
"SELECT value FROM global_settings WHERE name = $1",
@@ -250,31 +251,58 @@ pub async fn delete_expired_items(db: &DB) -> () {
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 created_at <= now() - ($1::bigint::text || ' s')::interval AND started_at + ((duration_ms/1000 + $1::bigint) || ' s')::interval <= now() RETURNING id",
job_retention_secs
)
.fetch_all(db)
.await;
match db.begin().await {
Ok(mut tx) => {
let r = sqlx::query!(
"DELETE FROM job_stats WHERE job_id IN (SELECT id FROM completed_job WHERE created_at <= now() - ($1::bigint::text || ' s')::interval AND started_at + ((duration_ms/1000 + $1::bigint) || ' s')::interval <= now())",
job_retention_secs
)
.fetch_all(&mut *tx)
.await;
match r {
Ok(_) => {
let deleted_jobs = sqlx::query_scalar!(
"DELETE FROM completed_job WHERE created_at <= now() - ($1::bigint::text || ' s')::interval AND started_at + ((duration_ms/1000 + $1::bigint) || ' s')::interval <= now() RETURNING id",
job_retention_secs
)
.fetch_all(&mut *tx)
.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,
)
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 expired jobs: {:?}", e)
}
}
}
Err(err) => {
tracing::error!("Error deleting expired job stats: {:?}", err)
}
}
match tx.commit().await {
Ok(_) => (),
Err(err) => tracing::error!("Error deleting expired jobs: {:?}", err),
}
}
Err(e) => tracing::error!("Error deleting jobs: {}", e.to_string()),
Err(err) => {
tracing::error!("Error deleting expired jobs: {:?}", err)
}
}
}
}
pub async fn reload_extra_pip_index_url_setting(db: &DB) {
if let Err(e) = reload_option_string_setting(
if let Err(e) = reload_option_setting(
db,
EXTRA_PIP_INDEX_URL_SETTING,
"PIP_EXTRA_INDEX_URL",
@@ -287,7 +315,7 @@ pub async fn reload_extra_pip_index_url_setting(db: &DB) {
}
pub async fn reload_npm_config_registry_setting(db: &DB) {
if let Err(e) = reload_option_string_setting(
if let Err(e) = reload_option_setting(
db,
NPM_CONFIG_REGISTRY_SETTING,
"NPM_CONFIG_REGISTRY",
@@ -314,6 +342,19 @@ pub async fn reload_retention_period_setting(db: &DB) {
}
}
pub async fn reload_job_default_timeout_setting(db: &DB) {
if let Err(e) = reload_option_setting(
db,
JOB_DEFAULT_TIMEOUT_SECS_SETTING,
"JOB_DEFAULT_TIMEOUT_SECS",
JOB_DEFAULT_TIMEOUT.clone(),
)
.await
{
tracing::error!("Error reloading job default timeout: {:?}", e)
}
}
pub async fn reload_request_size(db: &DB) {
if let Err(e) = reload_setting(
db,
@@ -359,11 +400,11 @@ pub async fn reload_license_key(db: &DB) -> error::Result<()> {
Ok(())
}
pub async fn reload_option_string_setting(
pub async fn reload_option_setting<T: FromStr + DeserializeOwned>(
db: &DB,
setting_name: &str,
std_env_var: &str,
lock: Arc<RwLock<Option<String>>>,
lock: Arc<RwLock<Option<T>>>,
) -> error::Result<()> {
let q = sqlx::query!(
"SELECT value FROM global_settings WHERE name = $1",
@@ -372,10 +413,12 @@ pub async fn reload_option_string_setting(
.fetch_optional(db)
.await?;
let mut value = std::env::var(std_env_var).ok();
let mut value = std::env::var(std_env_var)
.ok()
.and_then(|x| x.parse::<T>().ok());
if let Some(q) = q {
if let Ok(v) = serde_json::from_value::<String>(q.value.clone()) {
if let Ok(v) = serde_json::from_value::<T>(q.value.clone()) {
tracing::info!(
"Loaded setting {setting_name} from db config: {:#?}",
&q.value
@@ -714,6 +757,7 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
// since the job is unrecoverable, the same worker queue should never be sent anything
let (same_worker_tx_never_used, _same_worker_rx_never_used) = mpsc::channel::<Uuid>(1);
let (send_result_never_used, _send_result_rx_never_used) = mpsc::channel::<SendResult>(1);
let token = create_token_for_owner(
&db,
@@ -752,6 +796,7 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
"",
rsmq.clone(),
worker_name,
send_result_never_used,
)
.await;
}

View File

@@ -994,6 +994,7 @@ fn spawn_test_worker(
&base_internal_url,
None,
Arc::new(RwLock::new(None)),
false,
)
.await
};
@@ -1804,7 +1805,7 @@ def main():
}
#[sqlx::test(fixtures("base"))]
async fn test_empty_loop(db: Pool<Postgres>) {
async fn test_empty_loop_1(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
@@ -2869,6 +2870,10 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
timezone: "UTC".to_string(),
schedule: format!("{} {} * * * *", then.second(), then.minute()).to_string(),
ws_error_handler_muted: None,
retry: None,
no_flow_overlap: None,
summary: None,
};
let _ = client.create_schedule("test-workspace", &schedule).await;
@@ -2929,6 +2934,9 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
timezone: "UTC".to_string(),
schedule: format!("{} {} * * * *", then.second(), then.minute()).to_string(),
ws_error_handler_muted: None,
retry: None,
summary: None,
no_flow_overlap: None
},
)
.await
@@ -3005,6 +3013,9 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
timezone: "UTC".to_string(),
schedule: format!("{} {} * * * *", then.second(), then.minute()).to_string(),
ws_error_handler_muted: None,
retry: None,
no_flow_overlap: None,
summary: None,
};
let _ = client.create_schedule("test-workspace", &schedule).await;
@@ -3066,6 +3077,9 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
timezone: "UTC".to_string(),
schedule: format!("{} {} * * * *", then.second(), then.minute()).to_string(),
ws_error_handler_muted: None,
retry: None,
summary: None,
no_flow_overlap: None
},
)
.await

View File

@@ -9,8 +9,8 @@ name = "windmill_api"
path = "src/lib.rs"
[features]
enterprise = ["windmill-queue/enterprise", "async-stripe", "windmill-audit/enterprise"]
saml = ["samael"]
enterprise = ["windmill-queue/enterprise", "async-stripe", "windmill-audit/enterprise", "windmill-git-sync/enterprise"]
enterprise_saml = ["samael"]
benchmark = []
[dependencies]
@@ -27,6 +27,7 @@ windmill-common = { workspace = true, features = [
windmill-audit.workspace = true
windmill-parser.workspace = true
windmill-parser-py-imports.workspace = true
windmill-git-sync.workspace = true
tokio.workspace = true
anyhow.workspace = true
argon2.workspace = true
@@ -83,8 +84,7 @@ tokenizers.workspace = true
candle-core.workspace = true
candle-transformers.workspace = true
candle-nn.workspace = true
aws-sdk-s3 = "0.39.1"
aws-config = "1.0.0"
polars = { version = "0.35.2", features = ["lazy", "parquet", "aws", "csv", "dtype-full"] }
polars-io = { version = "0.35.2", features = ["csv"] }
object_store = { version = "0.8.0", features = ["aws"] }
aws-sdk-s3.workspace = true
polars.workspace = true
polars-io.workspace = true
object_store.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.224.0
version: 1.236.2
title: Windmill API
contact:
@@ -1788,6 +1788,12 @@ paths:
in: query
schema:
type: boolean
- name: include_encrypted
description: |
ask to include the encrypted value if secret and decrypt secret is not true (default: false)
in: query
schema:
type: boolean
responses:
"200":
description: variable
@@ -3403,6 +3409,7 @@ paths:
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
- name: invisible_to_owner
description: make the run invisible to the the script owner (default false)
@@ -3465,6 +3472,7 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
@@ -3493,6 +3501,7 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
@@ -4441,6 +4450,7 @@ paths:
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
@@ -4501,6 +4511,7 @@ paths:
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
@@ -4547,6 +4558,7 @@ paths:
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
@@ -4655,6 +4667,7 @@ paths:
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/Success"
- $ref: "#/components/parameters/ScheduledForBeforeNow"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/Suspended"
- $ref: "#/components/parameters/Running"
@@ -4791,6 +4804,7 @@ paths:
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/CreatedOrStartedBefore"
- $ref: "#/components/parameters/Running"
- $ref: "#/components/parameters/ScheduledForBeforeNow"
- $ref: "#/components/parameters/CreatedOrStartedAfter"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/ArgsFilter"
@@ -5009,6 +5023,34 @@ paths:
schema:
type: string
/w/{workspace}/jobs_u/queue/cancel_persistent/{path}:
post:
summary: cancel all queued jobs for persistent script
operationId: cancelPersistentQueuedJobs
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: reason
required: true
content:
application/json:
schema:
type: object
properties:
reason:
type: string
responses:
"200":
description: persistent job scaled down to zero
content:
text/plain:
schema:
type: string
/w/{workspace}/jobs_u/queue/force_cancel/{id}:
post:
summary: force cancel queued job
@@ -6491,7 +6533,9 @@ paths:
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: S3 resource path to use to generate the connection settings. If empty, the S3 resource defined in the workspace settings will be used
description:
S3 resource path to use to generate the connection settings. If empty,
the S3 resource defined in the workspace settings will be used
required: true
content:
application/json:
@@ -6569,7 +6613,9 @@ paths:
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: S3 resource path to use to generate the connection settings. If empty, the S3 resource defined in the workspace settings will be used
description:
S3 resource path to use to generate the connection settings. If empty,
the S3 resource defined in the workspace settings will be used
required: true
content:
application/json:
@@ -6606,7 +6652,7 @@ paths:
- use_ssl
- cache_regions
- client_kwargs
polars_cloud_options:
storage_options:
type: object
properties:
aws_endpoint_url:
@@ -6618,14 +6664,42 @@ paths:
aws_region:
type: string
aws_allow_http:
type: boolean
type: string
required:
- aws_endpoint_url
- aws_region
- aws_allow_http
required:
- s3fs_args
- cloud_options
- storage_options
/w/{workspace}/job_helpers/v2/s3_resource_info:
post:
summary: Returns the s3 resource associated to the provided path, or the
workspace default S3 resource
operationId: s3ResourceInfo
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description:
S3 resource path to use. If empty, the S3 resource defined in the
workspace settings will be used
required: true
content:
application/json:
schema:
type: object
properties:
s3_resource_path:
type: string
responses:
"200":
description: Connection settings
content:
application/json:
schema:
$ref: "#/components/schemas/S3Resource"
/w/{workspace}/job_helpers/test_connection:
get:
@@ -6742,6 +6816,157 @@ paths:
schema:
$ref: "#/components/schemas/WindmillFilePreview"
/w/{workspace}/job_helpers/delete_s3_file:
delete:
summary: Permanently delete file from S3
operationId: deleteS3File
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: file_key
in: query
required: true
schema:
type: string
responses:
"200":
description: Confirmation
content:
application/json:
schema: {}
/w/{workspace}/job_helpers/move_s3_file:
get:
summary: Move a S3 file from one path to the other within the same bucket
operationId: moveS3File
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: src_file_key
in: query
required: true
schema:
type: string
- name: dest_file_key
in: query
required: true
schema:
type: string
responses:
"200":
description: Confirmation
content:
application/json:
schema: {}
/w/{workspace}/job_helpers/multipart_upload_s3_file:
post:
summary: Upload file to S3 bucket using multipart upload
operationId: multipartFileUpload
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: Query args for a multipart file upload to S3
required: true
content:
application/json:
schema:
type: object
properties:
file_key:
type: string
part_content:
type: array
items:
type: integer
upload_id:
type: string
parts:
type: array
items:
$ref: "#/components/schemas/UploadFilePart"
is_final:
type: boolean
cancel_upload:
type: boolean
required:
- file_key
- part_content_base64
- parts
- is_final
- cancel_upload
responses:
"200":
description: Chunk upload status
content:
application/json:
schema:
type: object
properties:
upload_id:
type: string
parts:
type: array
items:
$ref: "#/components/schemas/UploadFilePart"
is_done:
type: boolean
required:
- upload_id
- parts
- is_done
/w/{workspace}/job_metrics/get/{id}:
post:
summary: get job metrics
operationId: getJobMetrics
tags:
- metrics
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
requestBody:
description: parameters for statistics retrieval
required: true
content:
application/json:
schema:
type: object
properties:
timeseries_max_datapoints:
type: integer
from_timestamp:
type: string
format: date-time
to_timestamp:
type: string
format: date-time
responses:
"200":
description: job details
content:
application/json:
schema:
type: object
properties:
metrics_metadata:
type: array
items:
$ref: "#/components/schemas/MetricMetadata"
scalar_metrics:
type: array
items:
$ref: "#/components/schemas/ScalarMetric"
timeseries_metrics:
type: array
items:
$ref: "#/components/schemas/TimeseriesMetric"
components:
securitySchemes:
bearerAuth:
@@ -6865,6 +7090,12 @@ components:
schema:
type: string
format: uuid
WorkerTag:
name: tag
description: Override the tag to use
in: query
schema:
type: string
NewJobId:
name: job_id
description:
@@ -6960,6 +7191,12 @@ components:
in: query
schema:
type: boolean
ScheduledForBeforeNow:
name: scheduled_for_before_now
description: filter on jobs scheduled_for before now (hence waitinf for a worker)
in: query
schema:
type: boolean
Suspended:
name: suspended
description: filter on suspended jobs
@@ -7393,6 +7630,7 @@ components:
"script_hub",
"identity",
"deploymentcallback",
"singlescriptflow",
]
schedule_path:
type: string
@@ -7502,6 +7740,7 @@ components:
"script_hub",
"identity",
"deploymentcallback",
"singlescriptflow",
]
schedule_path:
type: string
@@ -8208,6 +8447,12 @@ components:
$ref: "#/components/schemas/ScriptArgs"
ws_error_handler_muted:
type: boolean
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
summary:
type: string
no_flow_overlap:
type: boolean
required:
- path
- edited_by
@@ -8275,6 +8520,12 @@ components:
$ref: "#/components/schemas/ScriptArgs"
ws_error_handler_muted:
type: boolean
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
no_flow_overlap:
type: boolean
summary:
type: string
required:
- path
- schedule
@@ -8309,6 +8560,12 @@ components:
$ref: "#/components/schemas/ScriptArgs"
ws_error_handler_muted:
type: boolean
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
no_flow_overlap:
type: boolean
summary:
type: string
required:
- schedule
- timezone
@@ -8819,6 +9076,8 @@ components:
content_type:
type: string
enum: ["RawText", "Csv", "Parquet", "Unknown"]
download_url:
type: string
required:
- content_type
@@ -8853,6 +9112,65 @@ components:
type: string
git_repo_resource_path:
type: string
use_individual_branch:
type: boolean
required:
- script_path
- git_repo_resource_path
UploadFilePart:
type: object
properties:
part_number:
type: integer
tag:
type: string
required:
- part_number
- tag
MetricMetadata:
type: object
properties:
id:
type: string
name:
type: string
required:
- id
ScalarMetric:
type: object
properties:
metric_id:
type: string
value:
type: number
required:
- id
- value
TimeseriesMetric:
type: object
properties:
metric_id:
type: string
values:
type: array
items:
$ref: "#/components/schemas/MetricDataPoint"
required:
- id
- values
MetricDataPoint:
type: object
properties:
timestamp:
type: string
format: date-time
value:
type: number
required:
- timestamp
- value

View File

@@ -9,7 +9,6 @@ use std::collections::HashMap;
*/
use crate::{
db::{ApiAuthed, DB},
deployment_metadata_helpers,
users::{require_owner_of_path, OptAuthed},
variables::build_crypt,
webhook_util::{WebhookMessage, WebhookShared},
@@ -588,12 +587,16 @@ async fn create_app(
)
.await?;
let mut tx = PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Transaction(tx);
let (dependency_job_uuid, new_tx) = push(
&db,
tx,
&w_id,
JobPayload::AppDependencies { path: app.path.clone(), version: v_id },
JobPayload::AppDependencies {
path: app.path.clone(),
version: v_id,
deployment_message: app.deployment_message,
},
PushArgs::empty(),
&authed.username,
&authed.email,
@@ -615,25 +618,7 @@ async fn create_app(
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
tx = PushIsolationLevel::Transaction(new_tx);
tx = deployment_metadata_helpers::handle_deployment_metadata(
tx,
&authed,
&db,
&w_id,
deployment_metadata_helpers::DeployedObject::App { path: app.path.clone(), version: v_id },
app.deployment_message,
)
.await?;
match tx {
PushIsolationLevel::Transaction(tx) => tx.commit().await?,
_ => {
return Err(Error::InternalErr(
"Expected a transaction here".to_string(),
));
}
}
new_tx.commit().await?;
webhook.send_message(
w_id.clone(),
@@ -849,14 +834,18 @@ async fn update_app(
)
.await?;
let mut tx: PushIsolationLevel<'_, rsmq_async::MultiplexedRsmq> =
let tx: PushIsolationLevel<'_, rsmq_async::MultiplexedRsmq> =
PushIsolationLevel::Transaction(tx);
if let Some(v_id) = v_id {
let (dependency_job_uuid, new_tx) = push(
&db,
tx,
&w_id,
JobPayload::AppDependencies { path: npath.clone(), version: v_id },
JobPayload::AppDependencies {
path: npath.clone(),
version: v_id,
deployment_message: ns.deployment_message,
},
PushArgs::empty(),
&authed.username,
&authed.email,
@@ -877,26 +866,7 @@ async fn update_app(
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
tx = PushIsolationLevel::Transaction(new_tx);
tx = deployment_metadata_helpers::handle_deployment_metadata(
tx,
&authed,
&db,
&w_id,
deployment_metadata_helpers::DeployedObject::App { path: npath.clone(), version: v_id },
ns.deployment_message,
)
.await?;
}
match tx {
PushIsolationLevel::Transaction(tx) => tx.commit().await?,
_ => {
return Err(Error::InternalErr(
"Expected a transaction here".to_string(),
));
}
new_tx.commit().await?;
}
webhook.send_message(

View File

@@ -25,10 +25,10 @@ pub fn workspaced_service() -> Router {
async fn get_audit(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(id): Path<i32>,
Path((w_id, id)): Path<(String, i32)>,
) -> JsonResult<AuditLog> {
let tx = user_db.begin(&authed).await?;
let audit = windmill_audit::get_audit(tx, id).await?;
let audit = windmill_audit::get_audit(tx, id, &w_id).await?;
Ok(Json(audit))
}
async fn list_audit(

View File

@@ -126,11 +126,28 @@ impl ModelInstance {
let (config_filename, tokenizer_filename, weights_filename) = (
cache
.get("config.json")
.or_else(|| api.get("config.json").ok())
.or_else(|| {
api.get("config.json")
.or_else(|e| {
tracing::error!("Failed to get config.json from hugging face: {}", e);
return Err(e);
})
.ok()
})
.ok_or(Error::msg("could not get config.json"))?,
cache
.get("tokenizer.json")
.or_else(|| api.get("tokenizer.json").ok())
.or_else(|| {
api.get("tokenizer.json")
.or_else(|e| {
tracing::error!(
"Failed to get tokenizer.json from hugging face: {}",
e
);
return Err(e);
})
.ok()
})
.ok_or(Error::msg("could not get tokenizer.json"))?,
cache
.get("model.safetensors")
@@ -140,10 +157,19 @@ impl ModelInstance {
})
.or_else(|| {
tracing::info!("Downloading embedding model...");
api.get("model.safetensors").ok().and_then(|p| {
tracing::info!("Downloaded embedding model");
Some(p)
})
api.get("model.safetensors")
.or_else(|e| {
tracing::error!(
"Failed to get model.safetensors from hugging face: {}",
e
);
return Err(e);
})
.ok()
.and_then(|p| {
tracing::info!("Downloaded embedding model");
Some(p)
})
})
.ok_or(Error::msg("could not get model.safetensors"))?,
);

View File

@@ -7,7 +7,6 @@
*/
use crate::db::ApiAuthed;
use crate::deployment_metadata_helpers;
use crate::{
db::DB,
schedule::clear_schedule,
@@ -355,7 +354,7 @@ async fn create_flow(
)
.await?;
let mut tx = PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Transaction(tx);
let (dependency_job_uuid, mut new_tx) = push(
&db,
tx,
@@ -363,6 +362,7 @@ async fn create_flow(
JobPayload::FlowDependencies {
path: nf.path.clone(),
dedicated_worker: nf.dedicated_worker,
deployment_message: nf.deployment_message,
},
PushArgs::empty(),
&authed.username,
@@ -393,26 +393,7 @@ async fn create_flow(
.execute(&mut new_tx)
.await?;
tx = PushIsolationLevel::Transaction(new_tx);
tx = deployment_metadata_helpers::handle_deployment_metadata(
tx,
&authed,
&db,
&w_id,
deployment_metadata_helpers::DeployedObject::Flow { path: nf.path.clone() },
nf.deployment_message,
)
.await?;
match tx {
PushIsolationLevel::Transaction(tx) => tx.commit().await?,
_ => {
return Err(Error::InternalErr(
"Expected a transaction here".to_string(),
));
}
}
new_tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateFlow { workspace: w_id.clone(), path: nf.path.clone() },
@@ -585,7 +566,7 @@ async fn update_flow(
},
);
let mut tx = PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Transaction(tx);
let (dependency_job_uuid, mut new_tx) = push(
&db,
@@ -594,6 +575,7 @@ async fn update_flow(
JobPayload::FlowDependencies {
path: nf.path.clone(),
dedicated_worker: nf.dedicated_worker,
deployment_message: nf.deployment_message,
},
PushArgs::empty(),
&authed.username,
@@ -631,25 +613,7 @@ async fn update_flow(
.await?;
}
tx = PushIsolationLevel::Transaction(new_tx);
tx = deployment_metadata_helpers::handle_deployment_metadata(
tx,
&authed,
&db,
&w_id,
deployment_metadata_helpers::DeployedObject::Flow { path: nf.path.clone() },
nf.deployment_message,
)
.await?;
match tx {
PushIsolationLevel::Transaction(tx) => tx.commit().await?,
_ => {
return Err(Error::InternalErr(
"Expected a transaction here".to_string(),
));
}
}
new_tx.commit().await?;
Ok(nf.path.to_string())
}
@@ -1035,7 +999,12 @@ mod tests {
assert_eq!(
Retry {
constant: Default::default(),
exponential: ExponentialDelay { attempts: 0, multiplier: 1, seconds: 123 }
exponential: ExponentialDelay {
attempts: 0,
multiplier: 1,
seconds: 123,
random_factor: None
}
},
serde_json::from_str(
r#"
@@ -1053,7 +1022,12 @@ mod tests {
fn retry_exponential() {
let retry = Retry {
constant: ConstantDelay::default(),
exponential: ExponentialDelay { attempts: 3, multiplier: 4, seconds: 3 },
exponential: ExponentialDelay {
attempts: 3,
multiplier: 4,
seconds: 3,
random_factor: None,
},
};
assert_eq!(
vec![
@@ -1074,7 +1048,12 @@ mod tests {
fn retry_both() {
let retry = Retry {
constant: ConstantDelay { attempts: 2, seconds: 4 },
exponential: ExponentialDelay { attempts: 2, multiplier: 1, seconds: 3 },
exponential: ExponentialDelay {
attempts: 2,
multiplier: 1,
seconds: 3,
random_factor: None,
},
};
assert_eq!(
vec![

View File

@@ -1,16 +1,19 @@
use std::cmp;
use std::{cmp, time::Duration};
use crate::{
db::DB, resources::get_resource_value_interpolated_internal, users::Tokened,
workspaces::LargeFileStorage,
use crate::{db::DB, resources::get_resource_value_interpolated_internal, users::Tokened};
use anyhow::Context;
use aws_sdk_s3::{
presigning::PresigningConfig,
primitives::ByteStream,
types::{CompletedMultipartUpload, CompletedPart},
};
use aws_sdk_s3::config::{BehaviorVersion, Credentials, Region};
use axum::{
extract::{Path, Query},
routing::{get, post},
routing::{delete, get, post},
Extension, Json, Router,
};
use hyper::http;
use itertools::Itertools;
use object_store::ClientConfigKey;
use polars::{
io::{
@@ -25,7 +28,11 @@ use polars::{
};
use serde::{Deserialize, Serialize};
use tower_http::cors::{Any, CorsLayer};
use windmill_common::{db::UserDB, error};
use windmill_common::{
db::UserDB,
error,
s3_helpers::{build_s3_client, render_endpoint, LargeFileStorage, S3Resource},
};
use crate::db::ApiAuthed;
@@ -53,8 +60,8 @@ pub fn workspaced_service() -> Router {
post(polars_connection_settings_v2).layer(cors.clone()),
)
.route(
"/blah",
post(polars_connection_settings_v2).layer(cors.clone()),
"/v2/s3_resource_info",
post(s3_resource_info).layer(cors.clone()),
)
.route("/test_connection", get(test_connection).layer(cors.clone()))
.route(
@@ -69,23 +76,15 @@ pub fn workspaced_service() -> Router {
"/load_file_preview",
get(load_file_preview).layer(cors.clone()),
)
}
#[derive(Debug, Deserialize, Clone)]
struct S3Resource {
#[serde(rename = "bucket")]
bucket: String,
region: String,
#[serde(rename = "endPoint")]
endpoint: String,
#[serde(rename = "useSSL")]
use_ssl: bool,
#[serde(rename = "accessKey")]
access_key: Option<String>,
#[serde(rename = "secretKey")]
secret_key: Option<String>,
#[serde(rename = "pathStyle")]
path_style: bool,
.route(
"/delete_s3_file",
delete(delete_s3_file).layer(cors.clone()),
)
.route("/move_s3_file", get(move_s3_file).layer(cors.clone()))
.route(
"/multipart_upload_s3_file",
post(multipart_upload_s3_file).layer(cors.clone()),
)
}
#[derive(Deserialize)]
@@ -198,7 +197,7 @@ struct PolarsConnectionSettingsQueryV2 {
#[derive(Serialize)]
struct PolarsConnectionSettingsResponse {
s3fs_args: S3fsArgs,
polars_cloud_options: PolarsCloudOptions,
storage_options: PolarsStorageOptions,
}
#[derive(Serialize)]
@@ -214,14 +213,14 @@ struct S3fsArgs {
}
#[derive(Serialize)]
struct PolarsCloudOptions {
struct PolarsStorageOptions {
aws_endpoint_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
aws_access_key_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
aws_secret_access_key: Option<String>,
aws_region: String,
aws_allow_http: bool,
aws_allow_http: String,
}
async fn polars_connection_settings_v2(
@@ -257,17 +256,50 @@ async fn polars_connection_settings_v2(
.0;
let response = PolarsConnectionSettingsResponse {
s3fs_args: s3fs,
polars_cloud_options: PolarsCloudOptions {
storage_options: PolarsStorageOptions {
aws_endpoint_url: render_endpoint(&s3_resource),
aws_access_key_id: s3_resource.access_key,
aws_secret_access_key: s3_resource.secret_key,
aws_region: s3_resource.region,
aws_allow_http: !s3_resource.use_ssl,
aws_allow_http: (!s3_resource.use_ssl).to_string(),
},
};
return Ok(Json(response));
}
#[derive(Deserialize)]
struct S3ResourceInfoQuery {
s3_resource_path: Option<String>,
}
async fn s3_resource_info(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Json(query): Json<S3ResourceInfoQuery>,
) -> error::JsonResult<S3Resource> {
let s3_resource_opt = match query.s3_resource_path {
Some(s3_resource_path) => {
get_s3_resource(
&authed,
&user_db,
&db,
&token,
&w_id,
s3_resource_path.as_str(),
)
.await?
}
None => get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?,
};
let s3_resource = s3_resource_opt.ok_or(error::Error::NotFound(
"No datasets storage resource defined at the workspace level".to_string(),
))?;
return Ok(Json(s3_resource));
}
#[derive(Serialize, Deserialize, Clone)]
struct WindmillLargeFile {
s3: String,
@@ -297,7 +329,10 @@ async fn test_connection(
.max_keys(1)
.send()
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
.map_err(|err| {
tracing::error!("Error testing connection to S3 bucket: {:?}", err);
error::Error::InternalErr(err.to_string())
})?;
return Ok(Json(()));
}
@@ -416,6 +451,7 @@ struct LoadFilePreviewResponse {
pub content: Option<String>,
pub content_type: WindmillContentType,
pub msg: Option<String>,
pub download_url: Option<String>,
}
#[derive(Serialize)]
@@ -510,6 +546,19 @@ async fn load_file_preview(
)
};
// URL expires 30 minutes after its generation
let presigned_config = PresigningConfig::expires_in(Duration::from_secs(60 * 30))
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
let download_url = s3_client
.get_object()
.bucket(&s3_bucket)
.key(&file_key)
.presigned(presigned_config)
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?
.uri()
.to_string();
let file_chunk_length = if s3_object_content_length.is_some() {
cmp::min(
query.read_bytes_length,
@@ -599,18 +648,260 @@ async fn load_file_preview(
content_type: content_type,
content: Some(content),
msg: None,
download_url: Some(download_url),
},
Err(err) => LoadFilePreviewResponse {
content_type: content_type,
content: None,
msg: Some(err.to_string()),
download_url: Some(download_url),
},
};
return Ok(Json(response));
}
#[derive(Deserialize)]
struct DeleteS3FileQuery {
pub file_key: String,
}
async fn delete_s3_file(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Query(query): Query<DeleteS3FileQuery>,
) -> error::JsonResult<()> {
let file_key = query.file_key.clone();
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
))?;
let s3_client = build_s3_client(&s3_resource);
let s3_bucket = s3_resource.bucket.clone();
s3_client
.delete_object()
.bucket(&s3_bucket)
.key(&file_key)
.send()
.await
.map_err(|err| {
tracing::error!("{:?}", err);
error::Error::InternalErr(err.to_string())
})?;
return Ok(Json(()));
}
#[derive(Deserialize)]
struct MoveS3FileQuery {
pub src_file_key: String,
pub dest_file_key: String,
}
async fn move_s3_file(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Query(query): Query<MoveS3FileQuery>,
) -> error::JsonResult<()> {
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
))?;
let s3_client = build_s3_client(&s3_resource);
let s3_bucket = s3_resource.bucket.clone();
let source_uri = format!("{}/{}", s3_bucket, query.src_file_key);
s3_client
.copy_object()
.copy_source(&source_uri)
.bucket(&s3_bucket)
.key(&query.dest_file_key)
.send()
.await
.map_err(|err| {
tracing::error!("{:?}", err);
error::Error::InternalErr(err.to_string())
})?;
s3_client
.delete_object()
.bucket(&s3_bucket)
.key(&query.src_file_key)
.send()
.await
.map_err(|err| {
tracing::error!("{:?}", err);
error::Error::InternalErr(err.to_string())
})?;
return Ok(Json(()));
}
#[derive(Deserialize)]
struct UploadFileQuery {
pub file_key: String,
pub part_content: Vec<u8>,
pub upload_id: Option<String>, // should be None for the first call to initiate the upload
pub parts: Vec<UploadFilePart>, // parts already uploaded, with their part_number and the tag associated
pub is_final: bool, // whether it's the final chunk
pub cancel_upload: bool, // whether the upload should be cancelled. upload_id should be set. subsequent calls with this upload_id will fail
}
#[derive(Deserialize, Serialize, Clone)]
struct UploadFilePart {
pub part_number: u16,
pub tag: String,
}
#[derive(Serialize)]
struct UploadFileResponse {
pub upload_id: String,
pub parts: Vec<UploadFilePart>, // parts already uploaded, with their part_number and the tag associated
pub is_done: bool, // whether the transfer is finished, either b/c it got cancelled or because the last chunk was uploaded
}
async fn multipart_upload_s3_file(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Json(query): Json<UploadFileQuery>,
) -> error::JsonResult<UploadFileResponse> {
tracing::debug!(
"Multi part file upload: part number: {} - is_final: {}",
query.parts.len(),
query.is_final
);
let file_key = query.file_key.clone();
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
))?;
let bucket = s3_resource.bucket.clone();
let s3_client = build_s3_client(&s3_resource);
if query.cancel_upload && query.upload_id.clone().is_some() {
let upload_id = query.upload_id.unwrap();
s3_client
.abort_multipart_upload()
.bucket(&bucket)
.key(&file_key)
.upload_id(&upload_id)
.send()
.await
.map_err(|err| {
tracing::error!("{:?}", err);
error::Error::InternalErr(err.to_string())
})?;
return Ok(Json(UploadFileResponse {
upload_id: upload_id,
parts: vec![], // empty parts as the transfer has been cancelled
is_done: true,
}));
}
let (upload_id, part_number) = match query {
UploadFileQuery { upload_id: Some(upload_id), ref parts, .. } if parts.len() > 0 => {
(upload_id, parts.len() + 1)
}
UploadFileQuery { upload_id: None, ref parts, .. } if parts.len() == 0 => {
let multipart_upload_res = s3_client
.create_multipart_upload()
.bucket(&bucket)
.key(&file_key)
.send()
.await
.map_err(|err| {
tracing::error!("{:?}", err);
error::Error::InternalErr(err.to_string())
})?;
let upload_id = multipart_upload_res
.upload_id
.context("Upload ID is missing in the response")?;
(upload_id, 1)
}
_ => {
return Err(error::Error::BadRequest(
"parts should be empty when upload_id is not provided, as a new upload will be created."
.to_string(),
))
}
};
let chunk_content = query.part_content.clone();
let chunk_content_stream = ByteStream::from(chunk_content);
let multipart_upload_res = s3_client
.upload_part()
.bucket(&bucket)
.key(&file_key)
.upload_id(&upload_id)
.body(chunk_content_stream)
.part_number(part_number as i32)
.send()
.await
.map_err(|err| {
tracing::error!("{:?}", err);
error::Error::InternalErr(err.to_string())
})?;
let mut new_parts = query.parts.clone();
new_parts.push(UploadFilePart {
part_number: part_number as u16,
tag: multipart_upload_res.e_tag.unwrap_or_default(),
});
if query.is_final {
let completed_parts = new_parts
.iter()
.map(|part| {
CompletedPart::builder()
.e_tag(&part.tag)
.part_number(part.part_number as i32)
.build()
})
.collect_vec();
let _complete_multipart_upload_res = s3_client
.complete_multipart_upload()
.bucket(&s3_resource.bucket)
.key(&query.file_key)
.upload_id(&upload_id)
.multipart_upload(
CompletedMultipartUpload::builder()
.set_parts(Some(completed_parts))
.build(),
)
.send()
.await
.map_err(|err| {
tracing::error!("{:?}", err);
error::Error::InternalErr(err.to_string())
})?;
}
return Ok(Json(UploadFileResponse {
upload_id: upload_id,
parts: new_parts,
is_done: query.is_final,
}));
}
#[derive(Deserialize)]
pub struct S3Object {
pub s3: String,
}
async fn get_workspace_s3_resource<'c>(
authed: &ApiAuthed,
user_db: &UserDB,
@@ -683,30 +974,6 @@ async fn get_s3_resource<'c>(
return Ok(Some(s3_resource));
}
fn build_s3_client(s3_resource_ref: &S3Resource) -> aws_sdk_s3::Client {
let s3_resource = s3_resource_ref.clone();
let endpoint = render_endpoint(&s3_resource);
let mut s3_config_builder = aws_sdk_s3::Config::builder()
.endpoint_url(endpoint)
.behavior_version(BehaviorVersion::latest())
.region(Region::new(s3_resource.region));
if s3_resource.access_key.is_some() {
s3_config_builder = s3_config_builder.credentials_provider(Credentials::new(
s3_resource.access_key.unwrap_or_default(),
s3_resource.secret_key.unwrap_or_default(),
None,
None,
"s3_storage",
));
}
if s3_resource.path_style {
s3_config_builder = s3_config_builder.force_path_style(true);
}
let s3_config = s3_config_builder.build();
return aws_sdk_s3::Client::from_conf(s3_config);
}
fn build_polars_s3_config(s3_resource_ref: &S3Resource) -> CloudOptions {
let s3_resource = s3_resource_ref.to_owned();
let mut s3_configs: Vec<(AmazonS3ConfigKey, String)> = vec![
@@ -734,16 +1001,6 @@ fn build_polars_s3_config(s3_resource_ref: &S3Resource) -> CloudOptions {
return CloudOptions::default().with_aws(s3_configs);
}
fn render_endpoint(s3_resource: &S3Resource) -> String {
if s3_resource.endpoint.starts_with("http://") || s3_resource.endpoint.starts_with("https://") {
s3_resource.endpoint.clone()
} else if s3_resource.use_ssl {
format!("https://{}", s3_resource.endpoint)
} else {
format!("http://{}", s3_resource.endpoint)
}
}
async fn read_s3_text_object_head(
s3_client: &aws_sdk_s3::Client,
s3_bucket: &str,

View File

@@ -0,0 +1,165 @@
use crate::db::DB;
use axum::{extract::Path, routing::post, Extension, Json, Router};
use hyper::http;
use serde::{Deserialize, Serialize};
use tower_http::cors::{Any, CorsLayer};
use uuid::Uuid;
use windmill_common::{
error,
job_metrics::{JobStatsRecord, MetricKind},
};
pub fn workspaced_service() -> Router {
let cors = CorsLayer::new()
.allow_methods([http::Method::GET, http::Method::POST])
.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION])
.allow_origin(Any);
Router::new().route("/get/:id", post(get_job_metrics).layer(cors.clone()))
}
#[derive(Deserialize)]
struct JobStatsRequest {
from_timestamp: Option<chrono::DateTime<chrono::Utc>>,
to_timestamp: Option<chrono::DateTime<chrono::Utc>>,
timeseries_max_datapoints: Option<u32>, // default to 100, any value lower than that will be ignored. Set to 0 to retrieve all
}
#[derive(Serialize)]
struct JobStatsResponse {
metrics_metadata: Vec<MetricsMetadata>,
scalar_metrics: Vec<ScalarMetric>,
timeseries_metrics: Vec<TimeseriesMetric>,
}
#[derive(Serialize)]
pub struct MetricsMetadata {
pub id: String,
pub name: Option<String>,
}
#[derive(Serialize)]
struct ScalarMetric {
metric_id: String,
value: f64,
}
#[derive(Serialize)]
struct TimeseriesMetric {
metric_id: String,
values: Vec<DataPoint>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DataPoint {
pub timestamp: chrono::DateTime<chrono::Utc>,
pub value: f64,
}
async fn get_job_metrics(
Extension(db): Extension<DB>,
Path((w_id, job_id)): Path<(String, Uuid)>,
Json(JobStatsRequest { from_timestamp, to_timestamp, timeseries_max_datapoints }): Json<
JobStatsRequest,
>,
) -> error::JsonResult<JobStatsResponse> {
let records = sqlx::query_as::<_, JobStatsRecord>(
"SELECT * FROM job_stats where workspace_id = $1 and job_id = $2",
)
.bind(w_id)
.bind(job_id)
.fetch_all(&db)
.await?;
let mut metrics_metadata: Vec<MetricsMetadata> = vec![];
let mut scalar_metrics: Vec<ScalarMetric> = vec![];
let mut timeseries_metrics: Vec<TimeseriesMetric> = vec![];
for record in records {
let metric_id = record.metric_id;
match record.metric_kind {
MetricKind::ScalarInt => {
let value = record.scalar_int.unwrap_or_default() as f64;
scalar_metrics.push(ScalarMetric { metric_id: metric_id.clone(), value });
}
MetricKind::ScalarFloat => {
let value = record.scalar_float.unwrap_or_default() as f64;
scalar_metrics.push(ScalarMetric { metric_id: metric_id.clone(), value });
}
MetricKind::TimeseriesInt => {
if record.timestamps.clone().unwrap_or_default().len()
!= record.timeseries_int.clone().unwrap_or_default().len()
{
tracing::warn!("Timeseries metric {} has an invalid shape. It doesn't have one timestamp per measurement. (timestamps: {:?}, measurements: {:?})", metric_id, record.timestamps, record.timeseries_int)
}
let (timestamps, timeseries_int) = timeseries_sample(
from_timestamp,
to_timestamp,
timeseries_max_datapoints,
record.timestamps.unwrap_or_default(),
record.timeseries_int.unwrap_or_default(),
);
let mut values: Vec<DataPoint> = vec![];
for (idx, value) in timeseries_int.iter().enumerate() {
values.push(DataPoint {
timestamp: timestamps[idx],
value: value.to_owned() as f64,
});
}
timeseries_metrics.push(TimeseriesMetric { metric_id: metric_id.clone(), values });
}
MetricKind::TimeseriesFloat => {
if record.timestamps.clone().unwrap_or_default().len()
!= record.timeseries_int.clone().unwrap_or_default().len()
{
tracing::warn!("Timeseries metric {} has an invalid shape. It doesn't have one timestamp per measurement. (timestamps: {:?}, measurements: {:?})", metric_id, record.timestamps, record.timeseries_float)
}
let (timestamps, timeseries_float) = timeseries_sample(
from_timestamp,
to_timestamp,
timeseries_max_datapoints,
record.timestamps.unwrap_or_default(),
record.timeseries_float.unwrap_or_default(),
);
let mut values: Vec<DataPoint> = vec![];
for (idx, value) in timeseries_float.iter().enumerate() {
values.push(DataPoint {
timestamp: timestamps[idx],
value: value.to_owned() as f64,
});
}
timeseries_metrics.push(TimeseriesMetric { metric_id: metric_id.clone(), values });
}
};
metrics_metadata.push(MetricsMetadata { id: metric_id, name: record.metric_name });
}
let response = JobStatsResponse { metrics_metadata, scalar_metrics, timeseries_metrics };
Ok(Json(response))
}
fn timeseries_sample<T: Copy>(
from: Option<chrono::DateTime<chrono::Utc>>,
to: Option<chrono::DateTime<chrono::Utc>>,
_datapoints: Option<u32>,
timestamps: Vec<chrono::DateTime<chrono::Utc>>,
values: Vec<T>,
) -> (Vec<chrono::DateTime<chrono::Utc>>, Vec<T>) {
if timestamps.len() != values.len() {
tracing::warn!("Timeseries metric has an invalid shape. It doesn't have one timestamp per measurement. (timestamps: {:?}, measurements: {:?})", timestamps.len(), values.len());
return (vec![], vec![]);
}
let mut filtered_timestamp: Vec<chrono::DateTime<chrono::Utc>> = vec![];
let mut filtered_values: Vec<T> = vec![];
for (idx, timestamp) in timestamps.iter().enumerate() {
if *timestamp > from.unwrap_or(chrono::DateTime::<chrono::Utc>::MIN_UTC)
&& *timestamp < to.unwrap_or(chrono::DateTime::<chrono::Utc>::MAX_UTC)
{
filtered_timestamp.push(timestamps[idx]);
filtered_values.push(values[idx]);
}
}
// TODO: implement sampling
return (filtered_timestamp, filtered_values);
}

View File

@@ -186,6 +186,10 @@ pub fn global_service() -> Router {
)
.route("/getupdate/:id", get(get_job_update))
.route("/queue/cancel/:id", post(cancel_job_api))
.route(
"/queue/cancel_persistent/*script_path",
post(cancel_persistent_script_api),
)
.route("/queue/force_cancel/:id", post(force_cancel))
}
@@ -253,6 +257,52 @@ async fn cancel_job_api(
}
}
async fn cancel_persistent_script_api(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Json(CancelJob { reason }): Json<CancelJob>,
) -> error::Result<()> {
let username = match opt_authed {
Some(authed) => authed.username,
None => "anonymous".to_string(),
};
let cancelled_job_ids = windmill_queue::cancel_persistent_script_jobs(
&username,
reason,
script_path.to_path(),
&w_id,
&db,
rsmq,
)
.await?;
audit_log(
&db,
&username,
"jobs.cancel_persistent",
ActionKind::Delete,
&w_id,
Some(script_path.to_path()),
Some(
[(
"job_ids",
cancelled_job_ids
.into_iter()
.map(|uuid: Uuid| uuid.to_string())
.collect::<Vec<_>>()
.join(",")
.as_str(),
)]
.into(),
),
)
.await?;
Ok(())
}
async fn force_cancel(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
OptAuthed(opt_authed): OptAuthed,
@@ -330,9 +380,10 @@ pub async fn get_path_tag_limits_cache_for_hash(
Option<bool>,
Option<i16>,
Option<bool>,
Option<i32>,
)> {
let script = sqlx::query!(
"select path, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use from script where hash = $1 AND workspace_id = $2",
"select path, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout from script where hash = $1 AND workspace_id = $2",
hash,
w_id
)
@@ -353,6 +404,7 @@ pub async fn get_path_tag_limits_cache_for_hash(
script.dedicated_worker,
script.priority,
script.delete_after_use,
script.timeout,
))
}
@@ -467,6 +519,7 @@ pub struct RunJobQuery {
queue_limit: Option<i64>,
payload: Option<String>,
job_id: Option<Uuid>,
tag: Option<String>,
}
impl RunJobQuery {
@@ -506,6 +559,7 @@ pub struct ListQueueQuery {
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
pub tag: Option<String>,
pub scheduled_for_before_now: Option<bool>,
}
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
@@ -568,7 +622,7 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
if *s {
sqlb.and_where_gt("suspend", 0);
} else {
sqlb.and_where_eq("suspend", 0);
sqlb.and_where_is_null("suspend_until");
}
}
@@ -583,6 +637,10 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
}
if lq.scheduled_for_before_now.is_some_and(|x| x) {
sqlb.and_where_le("scheduled_for", "now()");
}
sqlb
}
@@ -652,7 +710,7 @@ async fn cancel_all(
require_admin(authed.is_admin, &authed.username)?;
let mut jobs = sqlx::query!(
"UPDATE queue SET canceled = true, canceled_by = $2, scheduled_for = now(), suspend = 0 WHERE workspace_id = $1 AND schedule_path IS NULL RETURNING id, running",
"UPDATE queue SET canceled = true, canceled_by = $2, scheduled_for = now(), suspend = 0 WHERE scheduled_for < now() AND workspace_id = $1 AND schedule_path IS NULL RETURNING id, running",
w_id,
authed.username
)
@@ -811,6 +869,7 @@ async fn list_jobs(
args: lq.args,
tag: lq.tag,
schedule_path: lq.schedule_path,
scheduled_for_before_now: lq.scheduled_for_before_now,
},
&[
"'QueuedJob' as typ",
@@ -1691,6 +1750,8 @@ pub async fn run_flow_by_path(
.map(|x| (x.tag, x.dedicated_worker))
.unwrap_or_else(|| (None, None));
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -1814,10 +1875,11 @@ pub async fn run_job_by_path(
check_scopes(&authed, || format!("run:script/{script_path}"))?;
let (job_payload, tag, _delete_after_use) =
let (job_payload, tag, _delete_after_use, timeout) =
script_path_to_payload(script_path, &db, &w_id).await?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -1840,7 +1902,7 @@ pub async fn run_job_by_path(
None,
!run_query.invisible_to_owner.unwrap_or(false),
tag,
None,
timeout,
None,
None,
)
@@ -2088,8 +2150,10 @@ pub async fn run_wait_result_job_by_path_get(
let script_path = script_path.to_path();
check_scopes(&authed, || format!("run:script/{script_path}"))?;
let (job_payload, tag, delete_after_use) =
let (job_payload, tag, delete_after_use, timeout) =
script_path_to_payload(script_path, &db, &w_id).await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -2112,7 +2176,7 @@ pub async fn run_wait_result_job_by_path_get(
None,
!run_query.invisible_to_owner.unwrap_or(false),
tag,
None,
timeout,
None,
None,
)
@@ -2205,9 +2269,10 @@ async fn run_wait_result_script_by_path_internal(
let script_path = script_path.to_path();
check_scopes(&authed, || format!("run:script/{script_path}"))?;
let (job_payload, tag, delete_after_use) =
let (job_payload, tag, delete_after_use, timeout) =
script_path_to_payload(script_path, &db, &w_id).await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -2230,7 +2295,7 @@ async fn run_wait_result_script_by_path_internal(
None,
!run_query.invisible_to_owner.unwrap_or(false),
tag,
None,
timeout,
None,
None,
)
@@ -2269,9 +2334,11 @@ pub async fn run_wait_result_script_by_hash(
dedicated_worker,
priority,
delete_after_use,
timeout,
) = get_path_tag_limits_cache_for_hash(&db, &w_id, hash).await?;
check_scopes(&authed, || format!("run:script/{path}"))?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -2303,7 +2370,7 @@ pub async fn run_wait_result_script_by_hash(
None,
!run_query.invisible_to_owner.unwrap_or(false),
tag,
None,
timeout,
None,
None,
)
@@ -2362,6 +2429,7 @@ async fn run_wait_result_flow_by_path_internal(
.map(|x| (x.tag, x.dedicated_worker, x.early_return))
.unwrap_or_else(|| (None, None, None));
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -2413,7 +2481,8 @@ async fn run_preview_job(
));
}
let scheduled_for = run_query.get_scheduled_for(&db).await?;
check_tag_available_for_workspace(&w_id, &preview.tag).await?;
let tag = run_query.tag.clone().or(preview.tag.clone());
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let (uuid, tx) = push(
@@ -2447,7 +2516,7 @@ async fn run_preview_job(
false,
None,
true,
preview.tag,
tag,
None,
None,
None,
@@ -2483,6 +2552,7 @@ async fn add_batch_jobs(
dedicated_worker,
concurrent_limit,
concurrent_time_window_s,
timeout,
) = match batch_info.kind.as_str() {
"script" => {
if let Some(path) = batch_info.path {
@@ -2496,6 +2566,7 @@ async fn add_batch_jobs(
dedicated_worker,
_priority,
_delete_after_use,
timeout,
) = get_latest_deployed_hash_for_path(&db, &w_id, &path).await?;
(
Some(script_hash),
@@ -2505,6 +2576,7 @@ async fn add_batch_jobs(
dedicated_worker,
concurrent_limit,
concurrency_time_window_s,
timeout,
)
} else {
Err(anyhow::anyhow!(
@@ -2563,7 +2635,7 @@ async fn add_batch_jobs(
}
return Ok(Json(uuids));
}
"noop" => (None, None, JobKind::Noop, None, None, None, None),
"noop" => (None, None, JobKind::Noop, None, None, None, None, None),
_ => {
return Err(error::Error::BadRequest(format!(
"Invalid batch kind: {}",
@@ -2589,8 +2661,8 @@ async fn add_batch_jobs(
select gen_random_uuid() as uuid from generate_series(1, $11)
)
INSERT INTO queue
(id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s)
(SELECT uuid, $1, $2, $3, $4, ('{ "uuid": "' || uuid || '" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13 FROM uuid_table)
(id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout)
(SELECT uuid, $1, $2, $3, $4, ('{ "uuid": "' || uuid || '" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14 FROM uuid_table)
RETURNING id"#,
hash.map(|h| h.0),
path,
@@ -2604,7 +2676,8 @@ async fn add_batch_jobs(
w_id,
n,
concurrent_limit,
concurrent_time_window_s
concurrent_time_window_s,
timeout
)
.fetch_all(&db)
.await?;
@@ -2628,7 +2701,8 @@ async fn run_preview_flow_job(
));
}
let scheduled_for = run_query.get_scheduled_for(&db).await?;
check_tag_available_for_workspace(&w_id, &raw_flow.tag).await?;
let tag = run_query.tag.clone().or(raw_flow.tag.clone());
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let (uuid, tx) = push(
@@ -2653,7 +2727,7 @@ async fn run_preview_flow_job(
false,
None,
true,
raw_flow.tag,
tag,
None,
None,
None,
@@ -2688,10 +2762,12 @@ pub async fn run_job_by_hash(
dedicated_worker,
priority,
_delete_after_use, // not taken into account in async endpoints
timeout,
) = get_path_tag_limits_cache_for_hash(&db, &w_id, hash).await?;
check_scopes(&authed, || format!("run:script/{path}"))?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -2724,7 +2800,7 @@ pub async fn run_job_by_hash(
None,
!run_query.invisible_to_owner.unwrap_or(false),
tag,
None,
timeout,
None,
None,
)
@@ -2750,14 +2826,14 @@ pub struct JobUpdate {
async fn get_job_update(
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Path((w_id, job_id)): Path<(String, Uuid)>,
Query(JobUpdateQuery { running, log_offset }): Query<JobUpdateQuery>,
) -> error::JsonResult<JobUpdate> {
let record = sqlx::query!(
"SELECT running, substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3",
log_offset,
&w_id,
&id
&job_id
)
.fetch_optional(&db)
.await?;
@@ -2779,11 +2855,11 @@ async fn get_job_update(
$3",
log_offset,
&w_id,
&id
&job_id
)
.fetch_optional(&db)
.await?;
let logs = not_found_if_none(logs, "Job Update", id.to_string())?;
let logs = not_found_if_none(logs, "Job Update", job_id.to_string())?;
Ok(Json(JobUpdate {
running: Some(false),
completed: Some(true),
@@ -2896,6 +2972,7 @@ pub struct ListCompletedQuery {
// filter by matching a subset of the result using base64 encoded json subset
pub result: Option<String>,
pub tag: Option<String>,
pub scheduled_for_before_now: Option<bool>,
}
async fn list_completed_jobs(

View File

@@ -36,7 +36,7 @@ use tower_http::{
trace::TraceLayer,
};
use windmill_common::db::UserDB;
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use windmill_common::ee::{get_license_plan, LicensePlan};
use windmill_common::utils::rd_string;
use windmill_common::worker::ALL_TAGS;
@@ -49,7 +49,6 @@ mod audit;
mod capture;
mod configs;
mod db;
mod deployment_metadata_helpers;
mod drafts;
pub mod ee;
pub mod embeddings;
@@ -61,6 +60,7 @@ mod groups;
mod inputs;
mod integration;
pub mod job_helpers;
pub mod job_metrics;
pub mod jobs;
pub mod oauth2;
mod openai;
@@ -160,13 +160,13 @@ pub async fn run_server(
.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION])
.allow_origin(Any);
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
let sp_extension: (ServiceProviderExt, SamlSsoLogin) = match get_license_plan().await {
LicensePlan::Enterprise => saml::build_sp_extension().await?,
LicensePlan::Pro => (ServiceProviderExt(None), SamlSsoLogin(None)),
};
#[cfg(not(feature = "saml"))]
#[cfg(not(feature = "enterprise_saml"))]
let sp_extension = (ServiceProviderExt(), SamlSsoLogin(None));
let embeddings_db = if server_mode {
@@ -198,6 +198,7 @@ pub async fn run_server(
.nest("/folders", folders::workspaced_service())
.nest("/groups", groups::workspaced_service())
.nest("/inputs", inputs::workspaced_service())
.nest("/job_metrics", job_metrics::workspaced_service())
.nest("/job_helpers", job_helpers::workspaced_service())
.nest("/jobs", jobs::workspaced_service())
.nest("/oauth", oauth2::workspaced_service())

View File

@@ -874,6 +874,7 @@ async fn slack_command(
dedicated_worker,
priority,
_delete_after_use,
_timeout,
) = windmill_common::get_latest_deployed_hash_for_path(
&db,
&settings.workspace_id,

View File

@@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::{
db::{ApiAuthed, DB},
variables::build_crypt,
@@ -6,13 +8,14 @@ use crate::{
use axum::{
body::{Bytes, StreamBody},
extract::{Extension, Path},
extract::{Extension, Path, Query},
response::IntoResponse,
routing::post,
Router,
};
use magic_crypt::MagicCryptTrait;
use serde_json::json;
use quick_cache::sync::Cache;
use serde_json::value::RawValue;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::error::{to_anyhow, Error};
@@ -30,22 +33,30 @@ struct OpenaiResource {
organization_id: Option<String>,
}
fn create_openai_json_error(msg: String) -> Error {
Error::OpenAIError(
serde_json::to_string(&json!({
"error": {
"message": msg
}
}))
.unwrap(),
)
#[derive(Deserialize)]
struct OpenaiClientCredentialsOauthResource {
client_id: String,
client_secret: String,
token_url: String,
user: Option<String>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum OpenaiConfig {
Resource(OpenaiResource),
ClientCredentialsOauthResource(OpenaiClientCredentialsOauthResource),
}
struct Variable {
value: String,
is_secret: bool,
}
async fn get_variable(path: String, db: &DB, w_id: &String) -> Result<String, Error> {
async fn get_variable_or_self(path: String, db: &DB, w_id: &String) -> Result<String, Error> {
if !path.starts_with("$var:") {
return Ok(path);
}
let path = path.strip_prefix("$var:").unwrap().to_string();
let mut tx = db.begin().await?;
let mut variable = sqlx::query_as!(
Variable,
@@ -69,65 +80,217 @@ async fn get_variable(path: String, db: &DB, w_id: &String) -> Result<String, Er
lazy_static::lazy_static! {
pub static ref OPENAI_AZURE_BASE_PATH: Option<String> = std::env::var("OPENAI_AZURE_BASE_PATH").ok();
static ref OPENAI_KEY_CACHE: Cache<String, OpenaiKeyCache> = Cache::new(500);
}
#[derive(Deserialize)]
struct OpenaiCredentials {
access_token: String,
}
async fn get_openai_key_using_credentials_flow(
mut resource: OpenaiClientCredentialsOauthResource,
db: &DB,
w_id: &String,
) -> Result<String, Error> {
resource.client_id = get_variable_or_self(resource.client_id, &db, &w_id).await?;
resource.client_secret = get_variable_or_self(resource.client_secret, &db, &w_id).await?;
resource.token_url = get_variable_or_self(resource.token_url, &db, &w_id).await?;
let mut params = HashMap::new();
params.insert("grant_type", "client_credentials");
let response = HTTP_CLIENT
.post(resource.token_url)
.form(&params)
.basic_auth(resource.client_id, Some(resource.client_secret))
.send()
.await
.map_err(|err| {
Error::InternalErr(format!(
"Failed to get OpenAI credentials using credentials flow: {}",
err
))
})?;
let response = response.json::<OpenaiCredentials>().await.map_err(|err| {
Error::InternalErr(format!(
"Failed to parse OpenAI credentials from credentials flow: {}",
err
))
})?;
Ok(response.access_token)
}
#[derive(Clone)]
struct OpenaiKeyCache {
api_key: String,
organization_id: Option<String>,
azure_base_path: Option<String>,
user: Option<String>,
expires_at: std::time::Instant,
}
impl OpenaiKeyCache {
fn new(
api_key: String,
organization_id: Option<String>,
azure_base_path: Option<String>,
expires_at: std::time::Instant,
user: Option<String>,
) -> Self {
Self { api_key, organization_id, azure_base_path, expires_at, user }
}
fn is_expired(&self) -> bool {
self.expires_at < std::time::Instant::now()
}
}
#[derive(Deserialize)]
struct ProxyQueryParams {
no_cache: Option<bool>,
}
async fn proxy(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path((w_id, openai_path)): Path<(String, String)>,
body: Bytes,
Query(query_params): Query<ProxyQueryParams>,
mut body: Bytes,
) -> impl IntoResponse {
let mut tx = db.begin().await?;
let openai_resource_path = sqlx::query_scalar!(
"SELECT openai_resource_path FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
let workspace_cache = OPENAI_KEY_CACHE.get(&w_id);
let (api_key, organization_id, azure_base_path, user) = if query_params
.no_cache
.unwrap_or(false)
|| workspace_cache.is_none()
|| workspace_cache.clone().unwrap().is_expired()
{
let openai_resource_path = sqlx::query_scalar!(
"SELECT openai_resource_path FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&db)
.await?;
if openai_resource_path.is_none() {
return Err(create_openai_json_error(
"OpenAI resource not configured".to_string(),
));
if openai_resource_path.is_none() {
return Err(Error::InternalErr(
"OpenAI resource not configured".to_string(),
));
}
let openai_resource_path = openai_resource_path.unwrap();
let resource = sqlx::query_scalar!(
"SELECT value
FROM resource
WHERE path = $1 AND workspace_id = $2",
&openai_resource_path,
&w_id
)
.fetch_optional(&db)
.await?
.ok_or_else(|| {
Error::InternalErr(format!(
"Could not find the OpenAI resource at path {openai_resource_path}, update the resource path in the workspace settings"
))
})?;
if resource.is_none() {
return Err(Error::InternalErr(
"OpenAI resource missing value".to_string(),
));
}
let config: OpenaiConfig = serde_json::from_value(resource.unwrap())
.map_err(|e| Error::InternalErr(format!("validating openai resource {e}")))?;
let mut user = None::<String>;
let mut resource = match config {
OpenaiConfig::Resource(resource) => {
tracing::debug!("Getting OpenAI key from static resource");
resource
}
OpenaiConfig::ClientCredentialsOauthResource(resource) => {
tracing::debug!("Getting OpenAI key with client credentials flow");
user = resource.user.clone();
let token = get_openai_key_using_credentials_flow(resource, &db, &w_id).await?;
OpenaiResource { api_key: token, organization_id: None }
}
};
resource.api_key = get_variable_or_self(resource.api_key, &db, &w_id).await?;
if resource.organization_id.is_some() {
resource.organization_id =
Some(get_variable_or_self(resource.organization_id.unwrap(), &db, &w_id).await?);
}
if user.is_some() {
user = Some(get_variable_or_self(user.unwrap(), &db, &w_id).await?);
}
let expires_at = std::time::Instant::now() + std::time::Duration::from_secs(60);
let azure_base_path = sqlx::query_scalar!(
"SELECT value
FROM global_settings
WHERE name = 'openai_azure_base_path'",
)
.fetch_optional(&db)
.await?;
let azure_base_path = if let Some(azure_base_path) = azure_base_path {
Some(
serde_json::from_value::<String>(azure_base_path).map_err(|e| {
Error::InternalErr(format!("validating openai azure base path {e}"))
})?,
)
} else {
OPENAI_AZURE_BASE_PATH.clone()
};
let workspace_cache = OpenaiKeyCache::new(
resource.api_key.clone(),
resource.organization_id.clone(),
azure_base_path.clone(),
expires_at,
user.clone(),
);
OPENAI_KEY_CACHE.insert(w_id.clone(), workspace_cache);
(
resource.api_key,
resource.organization_id,
azure_base_path,
user,
)
} else {
tracing::debug!("Using cached OpenAI key");
let workspace_cache = workspace_cache.unwrap();
(
workspace_cache.api_key.clone(),
workspace_cache.organization_id.clone(),
workspace_cache.azure_base_path.clone(),
workspace_cache.user.clone(),
)
};
if user.is_some() {
tracing::debug!("Adding user to request body");
let mut json_body: HashMap<String, Box<RawValue>> = serde_json::from_slice(&body)
.map_err(|e| Error::InternalErr(format!("Failed to parse request body: {}", e)))?;
let user_json_string = serde_json::Value::String(user.unwrap()).to_string(); // makes sure to escape characters
json_body.insert(
"user".to_string(),
RawValue::from_string(user_json_string)
.map_err(|e| Error::InternalErr(format!("Failed to parse user: {}", e)))?,
);
body = serde_json::to_vec(&json_body)
.map_err(|e| Error::InternalErr(format!("Failed to reserialize request body: {}", e)))?
.into();
}
let openai_resource_path = openai_resource_path.unwrap();
tx = db.begin().await?;
let resource = sqlx::query_scalar!(
"SELECT value
FROM resource
WHERE path = $1 AND workspace_id = $2",
&openai_resource_path,
&w_id
)
.fetch_optional(&mut *tx)
.await?
.ok_or_else(|| {
create_openai_json_error(format!(
"Could not find the OpenAI resource at path {openai_resource_path}, update the resource path in the workspace settings"
))
})?;
if resource.is_none() {
return Err(create_openai_json_error(
"OpenAI resource missing value".to_string(),
));
}
let mut resource: OpenaiResource = serde_json::from_value(resource.unwrap())
.map_err(|e| Error::InternalErr(format!("validating openai resource {e}")))?;
if resource.api_key.starts_with("$var:") {
let openai_api_key_path = resource.api_key.strip_prefix("$var:").unwrap().to_string();
resource.api_key = get_variable(openai_api_key_path, &db, &w_id).await?;
}
let base_url = if let Some(base_url) = &*OPENAI_AZURE_BASE_PATH {
let base_url = if let Some(base_url) = azure_base_path {
base_url
} else {
"https://api.openai.com/v1"
"https://api.openai.com/v1".to_string()
};
let url = format!("{}/{}", base_url, openai_path);
@@ -138,29 +301,24 @@ async fn proxy(
if base_url != "https://api.openai.com/v1" {
request = request
.header("api-key", resource.api_key)
.header("api-key", api_key)
.query(&[("api-version", "2023-05-15")])
} else {
request = request.header("authorization", format!("Bearer {}", resource.api_key))
request = request.header("authorization", format!("Bearer {}", api_key))
}
if let Some(mut org_id) = resource.organization_id {
tracing::info!("org_id: {:?}", org_id);
if org_id.starts_with("$var:") {
let openai_organisation_path = org_id.strip_prefix("$var:").unwrap().to_string();
org_id = get_variable(openai_organisation_path, &db, &w_id).await?;
}
if let Some(org_id) = organization_id {
request = request.header("OpenAI-Organization", org_id);
}
let response = request.send().await.map_err(to_anyhow)?;
tx = db.begin().await?;
let mut tx = db.begin().await?;
audit_log(
&mut *tx,
&authed.username,
"openai.request",
ActionKind::Update,
ActionKind::Execute,
&w_id,
Some(&authed.email),
Some([("openai_path", &format!("{:?}", openai_path)[..])].into()),

View File

@@ -7,45 +7,45 @@
*/
#![allow(non_snake_case)]
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use axum::response::Redirect;
use axum::{routing::post, Router};
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use axum::{Extension, Form};
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use std::sync::Arc;
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use samael::metadata::{ContactPerson, ContactType, EntityDescriptor};
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use samael::service_provider::{ServiceProvider, ServiceProviderBuilder};
use serde::Deserialize;
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use tower_cookies::Cookies;
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use windmill_common::error::{Error, Result};
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use crate::db::DB;
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use crate::users::login_externally;
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use crate::BASE_URL;
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
#[derive(Clone)]
pub struct ServiceProviderExt(pub Option<ServiceProvider>);
#[cfg(not(feature = "saml"))]
#[cfg(not(feature = "enterprise_saml"))]
pub struct ServiceProviderExt();
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
use windmill_common::ee::{get_license_plan, LicensePlan};
pub struct SamlSsoLogin(pub Option<String>);
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
pub async fn build_sp_extension() -> anyhow::Result<(ServiceProviderExt, SamlSsoLogin)> {
if let Some(url_metadata) = std::env::var("SAML_METADATA").ok() {
//todo restrict for non ee
@@ -93,7 +93,7 @@ pub struct SamlForm {
pub SAMLResponse: Option<String>,
}
#[cfg(feature = "saml")]
#[cfg(feature = "enterprise_saml")]
pub async fn acs(
Extension(db): Extension<DB>,
cookies: Cookies,
@@ -129,7 +129,7 @@ pub async fn acs(
}
}
#[cfg(not(feature = "saml"))]
#[cfg(not(feature = "enterprise_saml"))]
pub async fn acs() -> String {
"SAML available only in enterprise version".to_string()
}

View File

@@ -54,6 +54,8 @@ pub struct NewSchedule {
pub path: String,
pub schedule: String,
pub timezone: String,
pub summary: Option<String>,
pub no_flow_overlap: Option<bool>,
pub script_path: String,
pub is_flow: bool,
pub args: Option<serde_json::Value>,
@@ -66,6 +68,7 @@ pub struct NewSchedule {
pub on_recovery_times: Option<i32>,
pub on_recovery_extra_args: Option<serde_json::Value>,
pub ws_error_handler_muted: Option<bool>,
pub retry: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
@@ -150,7 +153,12 @@ async fn create_schedule(
let schedule = sqlx::query_as!(
Schedule,
"INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, \
is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, ws_error_handler_muted) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING *",
is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, \
on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, \
ws_error_handler_muted, retry, summary, no_flow_overlap \
) VALUES ( \
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21 \
) RETURNING *",
w_id,
ns.path,
ns.schedule,
@@ -169,6 +177,9 @@ async fn create_schedule(
ns.on_recovery_times,
ns.on_recovery_extra_args,
ns.ws_error_handler_muted.unwrap_or(false),
ns.retry,
ns.summary,
ns.no_flow_overlap.unwrap_or(false),
)
.fetch_one(&mut tx)
.await
@@ -220,8 +231,10 @@ async fn edit_schedule(
clear_schedule(tx.transaction_mut(), path, &w_id).await?;
let schedule = sqlx::query_as!(
Schedule,
"UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, on_recovery_extra_args = $10, ws_error_handler_muted = $11
WHERE path = $12 AND workspace_id = $13 RETURNING *",
"UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, \
on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, \
on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12, summary = $13, no_flow_overlap = $14 \
WHERE path = $15 AND workspace_id = $16 RETURNING *",
es.schedule,
es.timezone,
es.args,
@@ -233,6 +246,9 @@ async fn edit_schedule(
es.on_recovery_times,
es.on_recovery_extra_args,
es.ws_error_handler_muted.unwrap_or(false),
es.retry,
es.summary,
es.no_flow_overlap.unwrap_or(false),
path,
w_id,
)
@@ -324,7 +340,10 @@ pub struct ScheduleWJobs {
pub on_recovery_times: Option<i32>,
pub on_recovery_extra_args: Option<serde_json::Value>,
pub ws_error_handler_muted: bool,
pub retry: Option<serde_json::Value>,
pub jobs: Option<Vec<serde_json::Value>>,
pub summary: Option<String>,
pub no_flow_overlap: bool,
}
async fn list_schedule_with_jobs(
@@ -337,7 +356,7 @@ async fn list_schedule_with_jobs(
let (per_page, offset) = paginate(pagination);
let rows = sqlx::query_as!(ScheduleWJobs,
"SELECT schedule.*, t.jobs FROM schedule, LATERAL ( SELECT ARRAY (SELECT json_build_object('id', id, 'success', success, 'duration_ms', duration_ms) FROM completed_job WHERE
completed_job.schedule_path = schedule.path AND completed_job.workspace_id = $1 AND parent_job IS NULL ORDER BY started_at DESC LIMIT 20) AS jobs ) t
completed_job.schedule_path = schedule.path AND completed_job.workspace_id = $1 AND parent_job IS NULL AND is_skipped = False ORDER BY started_at DESC LIMIT 20) AS jobs ) t
WHERE schedule.workspace_id = $1 ORDER BY schedule.edited_at desc LIMIT $2 OFFSET $3",
w_id,
per_page as i64,
@@ -662,6 +681,7 @@ pub struct EditSchedule {
pub schedule: String,
pub timezone: String,
pub args: Option<serde_json::Value>,
pub summary: Option<String>,
pub on_failure: Option<String>,
pub on_failure_times: Option<i32>,
pub on_failure_exact: Option<bool>,
@@ -670,6 +690,8 @@ pub struct EditSchedule {
pub on_recovery_times: Option<i32>,
pub on_recovery_extra_args: Option<serde_json::Value>,
pub ws_error_handler_muted: Option<bool>,
pub retry: Option<serde_json::Value>,
pub no_flow_overlap: Option<bool>,
}
pub async fn clear_schedule<'c>(

View File

@@ -8,7 +8,6 @@
use crate::{
db::{ApiAuthed, DB},
deployment_metadata_helpers,
schedule::clear_schedule,
users::{maybe_refresh_folders, require_owner_of_path, AuthCache},
webhook_util::{WebhookMessage, WebhookShared},
@@ -47,6 +46,7 @@ use windmill_common::{
not_found_if_none, paginate, query_elems_from_hub, require_admin, Pagination, StripPath,
},
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::{
self, schedule::push_scheduled_job, PushArgs, PushIsolationLevel, QueueTransaction,
};
@@ -86,6 +86,8 @@ pub struct ScriptWDraft {
pub restart_unless_cancelled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delete_after_use: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<i32>,
}
pub fn global_service() -> Router {
@@ -323,7 +325,7 @@ async fn create_script(
let script_path = ns.path.clone();
let hash = ScriptHash(hash_script(&ns));
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let mut tx: QueueTransaction<'_, _> = (rsmq.clone(), user_db.begin(&authed).await?).into();
if sqlx::query_scalar!(
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
@@ -474,8 +476,9 @@ async fn create_script(
"INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, \
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, \
dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)",
dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \
delete_after_use, timeout) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)",
&w_id,
&hash.0,
ns.path,
@@ -501,6 +504,7 @@ async fn create_script(
ns.priority,
ns.restart_unless_cancelled,
ns.delete_after_use,
ns.timeout,
)
.execute(&mut tx)
.await?;
@@ -600,37 +604,29 @@ async fn create_script(
);
}
let mut tx = PushIsolationLevel::Transaction(tx);
let permissioned_as = username_to_permissioned_as(&authed.username);
if needs_lock_gen {
let dependencies = match ns.language {
ScriptLang::Python3 => {
windmill_parser_py_imports::parse_python_imports(&ns.content, &w_id, &ns.path, &db)
.await?
.join("\n")
}
_ => ns.content,
};
let tag = if ns.dedicated_worker.is_some_and(|x| x) {
Some(format!("{}:{}", &w_id, &ns.path,))
} else {
ns.tag
};
let tx = PushIsolationLevel::Transaction(tx);
let (_, new_tx) = windmill_queue::push(
&db,
tx,
&w_id,
JobPayload::Dependencies {
hash,
dependencies,
language: ns.language,
path: ns.path,
dedicated_worker: ns.dedicated_worker,
deployment_message: ns.deployment_message,
},
PushArgs::empty(),
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
permissioned_as,
None,
None,
None,
@@ -646,26 +642,19 @@ async fn create_script(
None,
)
.await?;
tx = PushIsolationLevel::Transaction(new_tx);
}
tx = deployment_metadata_helpers::handle_deployment_metadata(
tx,
&authed,
&db,
&w_id,
deployment_metadata_helpers::DeployedObject::Script { hash: hash, path: script_path },
ns.deployment_message,
)
.await?;
match tx {
PushIsolationLevel::Transaction(tx) => tx.commit().await?,
_ => {
return Err(Error::InternalErr(
"Expected a transaction here".to_string(),
));
}
new_tx.commit().await?;
} else {
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Script { hash: hash, path: script_path },
ns.deployment_message,
rsmq,
)
.await?;
tx.commit().await?;
}
Ok((StatusCode::CREATED, format!("{}", hash)))
@@ -719,7 +708,7 @@ async fn get_script_by_path_w_draft(
let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, ScriptWDraft>(
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use FROM script LEFT JOIN draft ON
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout FROM script LEFT JOIN draft ON
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
WHERE script.path = $1 AND script.workspace_id = $2 \
AND script.created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \

View File

@@ -184,6 +184,7 @@ pub async fn send_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resu
&windmill_common::utils::Mode::Server,
&HTTP_CLIENT,
&db,
cfg!(feature = "enterprise"),
)
.await?;

View File

@@ -2073,7 +2073,7 @@ async fn create_token(
&mut *tx,
&email,
"users.token.create",
ActionKind::Delete,
ActionKind::Create,
&"global",
Some(&token[0..10]),
None,

View File

@@ -102,6 +102,7 @@ async fn list_variables(
#[derive(Deserialize)]
struct GetVariableQuery {
decrypt_secret: Option<bool>,
include_encrypted: Option<bool>,
}
async fn get_variable(
@@ -163,6 +164,8 @@ async fn get_variable(
mc.decrypt_base64_to_string(value)
.map_err(|e| Error::InternalErr(e.to_string()))?,
)
} else if q.include_encrypted.unwrap_or(false) {
Some(value)
} else {
None
},

View File

@@ -9,17 +9,17 @@
#[cfg(feature = "enterprise")]
use std::str::FromStr;
use crate::BASE_URL;
use crate::db::ApiAuthed;
use crate::BASE_URL;
use crate::{
apps::AppWithLastVersion,
db::DB,
folders::Folder,
resources::{Resource, ResourceType},
users::{WorkspaceInvite, VALID_USERNAME, send_email_if_possible},
users::{send_email_if_possible, WorkspaceInvite, VALID_USERNAME},
utils::require_super_admin,
variables::build_crypt,
webhook_util::{InstanceEvent, WebhookShared}
webhook_util::{InstanceEvent, WebhookShared},
};
#[cfg(feature = "enterprise")]
use axum::response::Redirect;
@@ -32,28 +32,29 @@ use axum::{
Json, Router,
};
use chrono::Utc;
#[cfg(feature = "enterprise")]
use chrono::{Datelike, TimeZone, Timelike};
use magic_crypt::MagicCryptTrait;
#[cfg(feature = "enterprise")]
use stripe::CustomerId;
#[cfg(feature = "enterprise")]
use chrono::{TimeZone, Datelike, Timelike};
use uuid::Uuid;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::db::UserDB;
use windmill_common::s3_helpers::LargeFileStorage;
use windmill_common::schedule::Schedule;
use windmill_common::users::username_to_permissioned_as;
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::workspaces::WorkspaceGitRepo;
use windmill_common::{
error::{to_anyhow, Error, JsonResult, Result},
flows::Flow,
oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH,
scripts::{Schema, Script, ScriptLang},
utils::{paginate, rd_string, require_admin, Pagination},
variables::ExportableListableVariable,
oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH,
};
use windmill_queue::QueueTransaction;
use hyper::{header, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::{json, Map};
@@ -73,7 +74,10 @@ pub fn workspaced_service() -> Router {
.route("/get_settings", get(get_settings))
.route("/get_deploy_to", get(get_deploy_to))
.route("/edit_slack_command", post(edit_slack_command))
.route("/run_slack_message_test_job", post(run_slack_message_test_job))
.route(
"/run_slack_message_test_job",
post(run_slack_message_test_job),
)
.route("/edit_webhook", post(edit_webhook))
.route("/edit_auto_invite", post(edit_auto_invite))
.route("/edit_deploy_to", post(edit_deploy_to))
@@ -81,22 +85,25 @@ pub fn workspaced_service() -> Router {
.route("/is_premium", get(is_premium))
.route("/premium_info", get(premium_info))
.route("/edit_copilot_config", post(edit_copilot_config))
.route("/get_copilot_info", get(get_copilot_info) )
.route("/get_copilot_info", get(get_copilot_info))
.route("/edit_error_handler", post(edit_error_handler))
.route("/edit_large_file_storage_config", post(edit_large_file_storage_config))
.route(
"/edit_large_file_storage_config",
post(edit_large_file_storage_config),
)
.route("/edit_git_sync_config", post(edit_git_sync_config))
.route("/leave", post(leave_workspace));
#[cfg(feature = "enterprise")]
{
{
if STRIPE_KEY.is_none() {
return router;
} else {
tracing::info!("stripe enabled");
return router
.route("/checkout", get(stripe_checkout))
.route("/billing_portal", get(stripe_portal));
.route("/checkout", get(stripe_checkout))
.route("/billing_portal", get(stripe_portal));
}
}
@@ -114,7 +121,10 @@ pub fn global_service() -> Router {
.route("/allowed_domain_auto_invite", get(is_allowed_auto_domain))
.route("/unarchive/:workspace", post(unarchive_workspace))
.route("/delete/:workspace", delete(delete_workspace))
.route("/create_workspace_require_superadmin", get(create_workspace_require_superadmin))
.route(
"/create_workspace_require_superadmin",
get(create_workspace_require_superadmin),
)
}
#[cfg(feature = "enterprise")]
@@ -122,7 +132,6 @@ lazy_static::lazy_static! {
pub static ref STRIPE_KEY: Option<String> = std::env::var("STRIPE_KEY").ok();
}
#[derive(FromRow, Serialize)]
struct Workspace {
id: String,
@@ -152,7 +161,7 @@ pub struct WorkspaceSettings {
pub error_handler_extra_args: Option<serde_json::Value>,
pub error_handler_muted_on_cancel: Option<bool>,
pub large_file_storage: Option<serde_json::Value>, // effectively: DatasetsStorage
pub git_sync: Option<serde_json::Value>, // effectively: WorkspaceGitRepo
pub git_sync: Option<serde_json::Value>, // effectively: WorkspaceGitRepo
}
#[derive(FromRow, Serialize, Debug)]
@@ -179,7 +188,7 @@ struct EditCommandScript {
struct RunSlackMessageTestJobRequest {
hub_script_path: String,
channel: String,
test_msg: String
test_msg: String,
}
#[derive(Serialize)]
@@ -274,18 +283,6 @@ pub struct EditErrorHandler {
pub error_handler_muted_on_cancel: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum LargeFileStorage {
S3Storage(S3Storage),
// TODO: Add a filesystem type here in the future if needed
}
#[derive(Serialize, Deserialize, Debug)]
pub struct S3Storage {
pub s3_resource_path: String,
}
async fn list_pending_invites(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -314,7 +311,6 @@ async fn is_premium(
let row = sqlx::query_scalar!(
"SELECT premium FROM workspace WHERE workspace.id = $1",
&w_id
)
.fetch_one(&mut *tx)
.await?;
@@ -322,7 +318,6 @@ async fn is_premium(
Ok(Json(row))
}
#[derive(Serialize)]
pub struct PremiumWorkspaceInfo {
pub premium: bool,
@@ -343,31 +338,60 @@ async fn premium_info(
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
let result = PremiumWorkspaceInfo {
premium: row.premium,
usage: row.usage,
seats: None,
};
let result = PremiumWorkspaceInfo { premium: row.premium, usage: row.usage, seats: None };
#[cfg(feature = "enterprise")]
let mut result = result;
#[cfg(feature = "enterprise")]
if row.premium && row.plan == Some("team".to_string()) {
let customer_id = row.customer_id.ok_or(Error::InternalErr(format!("no customer id for workspace {}", w_id)))?;
let client = stripe::Client::new(STRIPE_KEY.clone().ok_or(Error::InternalErr(format!("stripe key not set")))?);
let customer_id = row.customer_id.ok_or(Error::InternalErr(format!(
"no customer id for workspace {}",
w_id
)))?;
let client = stripe::Client::new(
STRIPE_KEY
.clone()
.ok_or(Error::InternalErr(format!("stripe key not set")))?,
);
let customer_id = CustomerId::from_str(&customer_id).map_err(to_anyhow)?;
let subscriptions = stripe::Subscription::list(
&client,
&stripe::ListSubscriptions { customer: Some(customer_id.clone()), limit: Some(1), ..Default::default() },
).await.map_err(to_anyhow)?;
&stripe::ListSubscriptions {
customer: Some(customer_id.clone()),
limit: Some(1),
..Default::default()
},
)
.await
.map_err(to_anyhow)?;
if subscriptions.data.len() > 1 {
return Err(Error::InternalErr(format!("multiple subscriptions for customer {}, please contact us at ccontact@windmill.dev", customer_id)));
}
let subscription = subscriptions.data.get(0).ok_or_else(|| Error::InternalErr(format!("no subscription for customer {}", customer_id)))?;
result.seats = subscription.items.data.iter().filter_map(|item|{
item.price.clone().map(|p| p.metadata.map(|m| m.get("plan").filter(|plan| plan == &"team").map(|_| item.quantity.map(|x| x as i32)))).flatten().flatten().flatten()
}).collect::<Vec<_>>().get(0).copied();
let subscription = subscriptions.data.get(0).ok_or_else(|| {
Error::InternalErr(format!("no subscription for customer {}", customer_id))
})?;
result.seats = subscription
.items
.data
.iter()
.filter_map(|item| {
item.price
.clone()
.map(|p| {
p.metadata.map(|m| {
m.get("plan")
.filter(|plan| plan == &"team")
.map(|_| item.quantity.map(|x| x as i32))
})
})
.flatten()
.flatten()
.flatten()
})
.collect::<Vec<_>>()
.get(0)
.copied();
}
Ok(Json(result))
}
@@ -388,7 +412,11 @@ async fn stripe_checkout(
// #[cfg(feature = "enterprise")]
{
require_admin(authed.is_admin, &authed.username)?;
let client = stripe::Client::new(STRIPE_KEY.clone().ok_or(Error::InternalErr(format!("stripe key not set")))?);
let client = stripe::Client::new(
STRIPE_KEY
.clone()
.ok_or(Error::InternalErr(format!("stripe key not set")))?,
);
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);
@@ -397,18 +425,18 @@ async fn stripe_checkout(
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
params.cancel_url = Some(&failure_rd);
params.line_items = match plan.plan.as_str() {
"team" => Some(vec![
stripe::CreateCheckoutSessionLineItems {
quantity: Some(plan.seats.unwrap_or(1) as u64),
price: Some("price_1NCNOgGU3NdFi9eLuG4fZuEP".to_string()),
adjustable_quantity: Some(stripe::CreateCheckoutSessionLineItemsAdjustableQuantity {
"team" => Some(vec![stripe::CreateCheckoutSessionLineItems {
quantity: Some(plan.seats.unwrap_or(1) as u64),
price: Some("price_1NCNOgGU3NdFi9eLuG4fZuEP".to_string()),
adjustable_quantity: Some(
stripe::CreateCheckoutSessionLineItemsAdjustableQuantity {
enabled: true,
minimum: Some(1),
..Default::default()
}),
..Default::default()
},
]),
},
),
..Default::default()
}]),
_ => Err(Error::BadRequest("invalid plan".to_string()))?,
};
params.client_reference_id = Some(&w_id);
@@ -425,7 +453,7 @@ async fn stripe_checkout(
}
_ => params.customer_email = Some(&authed.email),
}
let now = Utc::now();
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
metadata: {
@@ -434,22 +462,27 @@ async fn stripe_checkout(
Some(map)
},
billing_cycle_anchor: if now.day() == 1 && now.hour() < 12 {
// no need to prorate so close to the billing cycle renew date
None
} else {
// first of the next month (and possibly next year) at noon UTC
let date = if now.month() == 12 {
Utc.with_ymd_and_hms(now.year() + 1, 1, 1, 12, 0, 0).single().unwrap()
} else {
Utc.with_ymd_and_hms(now.year(), now.month() + 1, 1, 12, 0, 0).single().unwrap()
};
Some(date.timestamp())
},
// no need to prorate so close to the billing cycle renew date
None
} else {
// first of the next month (and possibly next year) at noon UTC
let date = if now.month() == 12 {
Utc.with_ymd_and_hms(now.year() + 1, 1, 1, 12, 0, 0)
.single()
.unwrap()
} else {
Utc.with_ymd_and_hms(now.year(), now.month() + 1, 1, 12, 0, 0)
.single()
.unwrap()
};
Some(date.timestamp())
},
..Default::default()
});
stripe::CheckoutSession::create(&client, params)
.await.map_err(to_anyhow)?
.await
.map_err(to_anyhow)?
};
let uri = checkout_session
.url
@@ -472,8 +505,15 @@ async fn stripe_portal(
.fetch_one(&db)
.await?
.ok_or_else(|| Error::InternalErr(format!("no customer id for workspace {}", w_id)))?;
let client = stripe::Client::new(STRIPE_KEY.clone().ok_or(Error::InternalErr(format!("stripe key not set")))?);
let success_rd = format!("{}/workspace_settings?tab=premium", BASE_URL.read().await.clone());
let client = stripe::Client::new(
STRIPE_KEY
.clone()
.ok_or(Error::InternalErr(format!("stripe key not set")))?,
);
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);
@@ -658,25 +698,29 @@ async fn run_slack_message_test_job(
let mut extra_args = Map::new();
extra_args.insert("channel".to_string(), json!(req.channel));
extra_args.insert("slack".to_string(), json!(format!("$res:{WORKSPACE_SLACK_BOT_TOKEN_PATH}")));
extra_args.insert(
"slack".to_string(),
json!(format!("$res:{WORKSPACE_SLACK_BOT_TOKEN_PATH}")),
);
let tx: QueueTransaction<'_, _> = (rsmq.clone(), db.begin().await?).into();
let (uuid, tx) = windmill_queue::handle_on_failure(
&db,
&db,
tx,
Uuid::parse_str("00000000-0000-0000-0000-000000000000")?,
"slack_message_test",
"slack_message_test",
false,
"slack_message_test",
"slack_message_test",
false,
w_id.as_str(),
&format!("script/{}", req.hub_script_path.as_str()),
&format!("script/{}", req.hub_script_path.as_str()),
sqlx::types::Json(&fake_result),
0,
Utc::now(),
0,
Utc::now(),
Some(json!(extra_args)),
authed.email.as_str(),
authed.email.as_str(),
None, // Note: we could mark it as high priority to return result quickly to the user
).await?;
)
.await?;
tx.commit().await?;
Ok(Json(RunSlackMessageTestJobResponse {
@@ -746,7 +790,7 @@ async fn edit_auto_invite(
Json(ea): Json<EditAutoInvite>,
) -> Result<String> {
require_admin(is_admin, &username)?;
// #[cfg(not(feature = "enterprise"))]
// {
// return Err(Error::BadRequest(
@@ -755,13 +799,13 @@ async fn edit_auto_invite(
// }
let domain = if ea.invite_all.is_some_and(|x| x) {
if *CLOUD_HOSTED {
if *CLOUD_HOSTED {
return Err(Error::BadRequest(
"invite_all is only available locally".to_string(),
));
} else {
"*"
}
}
} else {
email.split('@').last().unwrap()
};
@@ -902,7 +946,19 @@ async fn edit_copilot_config(
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("openai_resource_path", &format!("{:?}", eo.openai_resource_path)[..]), ("code_completion_enabled", &format!("{:?}", eo.code_completion_enabled)[..])].into()),
Some(
[
(
"openai_resource_path",
&format!("{:?}", eo.openai_resource_path)[..],
),
(
"code_completion_enabled",
&format!("{:?}", eo.code_completion_enabled)[..],
),
]
.into(),
),
)
.await?;
tx.commit().await?;
@@ -919,7 +975,6 @@ async fn get_copilot_info(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> JsonResult<CopilotInfo> {
let mut tx = db.begin().await?;
let record = sqlx::query!(
"SELECT openai_resource_path, code_completion_enabled FROM workspace_settings WHERE workspace_id = $1",
@@ -930,7 +985,6 @@ async fn get_copilot_info(
.map_err(|e| Error::InternalErr(format!("getting openai_resource_path and code_completion_enabled: {e}")))?;
tx.commit().await?;
Ok(Json(CopilotInfo {
exists_openai_resource_path: record.openai_resource_path.is_some(),
code_completion_enabled: record.code_completion_enabled,
@@ -981,7 +1035,10 @@ async fn edit_large_file_storage_config(
}
tx.commit().await?;
Ok(format!("Edit large file storage config for workspace {}", &w_id))
Ok(format!(
"Edit large file storage config for workspace {}",
&w_id
))
}
#[derive(Deserialize)]
@@ -989,12 +1046,6 @@ struct EditGitSyncConfig {
git_sync_settings: Option<WorkspaceGitRepo>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct WorkspaceGitRepo {
pub script_path: String,
pub git_repo_resource_path: String,
}
async fn edit_git_sync_config(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -1059,7 +1110,7 @@ async fn edit_error_handler(
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
sqlx::query_as!(
Group,
"INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
@@ -1102,7 +1153,6 @@ async fn edit_error_handler(
tx.commit().await?;
Ok(format!("Edit error_handler for workspace {}", &w_id))
}
async fn list_workspaces_as_super_admin(
@@ -1176,9 +1226,11 @@ async fn create_workspace_require_superadmin() -> String {
}
async fn _check_nb_of_workspaces(db: &DB) -> Result<()> {
let nb_workspaces = sqlx::query_scalar!("SELECT COUNT(*) FROM workspace WHERE id != 'admins' AND deleted = false",)
.fetch_one(db)
.await?;
let nb_workspaces = sqlx::query_scalar!(
"SELECT COUNT(*) FROM workspace WHERE id != 'admins' AND deleted = false",
)
.fetch_one(db)
.await?;
if nb_workspaces.unwrap_or(0) >= 2 {
return Err(Error::BadRequest(
"You have reached the maximum number of workspaces (2 outside of default workspace 'admins') without an enterprise license. Archive/delete another workspace to create a new one"
@@ -1188,13 +1240,11 @@ async fn _check_nb_of_workspaces(db: &DB) -> Result<()> {
return Ok(());
}
async fn create_workspace(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Json(nw): Json<CreateWorkspace>,
) -> Result<String> {
if *CREATE_WORKSPACE_REQUIRE_SUPERADMIN {
require_super_admin(&db, &authed.email).await?;
}
@@ -1383,9 +1433,13 @@ async fn leave_workspace(
ApiAuthed { email, username, .. }: ApiAuthed,
) -> Result<String> {
let mut tx = db.begin().await?;
sqlx::query!("DELETE FROM usr WHERE workspace_id = $1 AND email = $2", &w_id, &email)
.execute(&mut *tx)
.await?;
sqlx::query!(
"DELETE FROM usr WHERE workspace_id = $1 AND email = $2",
&w_id,
&email
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
@@ -1445,6 +1499,9 @@ async fn delete_workspace(
let mut tx = db.begin().await?;
require_super_admin(&db, &email).await?;
sqlx::query!("DELETE FROM dependency_map WHERE workspace_id = $1", &w_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM queue WHERE workspace_id = $1", &w_id)
.execute(&mut *tx)
.await?;
@@ -1463,6 +1520,12 @@ async fn delete_workspace(
sqlx::query!("DELETE FROM app WHERE workspace_id = $1", &w_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM raw_app WHERE workspace_id = $1", &w_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM input WHERE workspace_id = $1", &w_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM variable WHERE workspace_id = $1", &w_id)
.execute(&mut *tx)
.await?;
@@ -1478,6 +1541,17 @@ async fn delete_workspace(
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM job_stats WHERE workspace_id = $1", &w_id)
.execute(&mut *tx)
.await?;
sqlx::query!(
"DELETE FROM deployment_metadata WHERE workspace_id = $1",
&w_id
)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM usr WHERE workspace_id = $1", &w_id)
.execute(&mut *tx)
.await?;
@@ -1505,6 +1579,10 @@ async fn delete_workspace(
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM account WHERE workspace_id = $1", &w_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM workspace_key WHERE workspace_id = $1", &w_id)
.execute(&mut *tx)
.await?;
@@ -1516,6 +1594,10 @@ async fn delete_workspace(
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM token WHERE workspace_id = $1", &w_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM workspace WHERE id = $1", &w_id)
.execute(&mut *tx)
.await?;
@@ -1599,7 +1681,7 @@ If you do not have an account on {}, login with SSO or ask an admin to create an
),
&nu.email,
);
webhook.send_instance_event(InstanceEvent::UserInvitedWorkspace {
email: nu.email.clone(),
workspace: w_id,
@@ -1745,9 +1827,9 @@ struct ScriptMetadata {
#[serde(skip_serializing_if = "is_none_or_false")]
ws_error_handler_muted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
priority: Option<i16>,
priority: Option<i16>,
#[serde(skip_serializing_if = "Option::is_none")]
tag: Option<String>,
tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1845,7 +1927,7 @@ where
"archived",
"has_draft",
"draft_only",
"error"
"error",
] {
if obj.contains_key(key) {
obj.remove(key);
@@ -1962,7 +2044,6 @@ async fn tarball_workspace(
timeout: script.timeout,
delete_after_use: script.delete_after_use,
restart_unless_cancelled: script.restart_unless_cancelled,
};
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
archive
@@ -2025,16 +2106,15 @@ async fn tarball_workspace(
}
if !skip_variables.unwrap_or(false) {
let variables = sqlx::query_as::<_, ExportableListableVariable>(
if !skip_secrets.unwrap_or(false) {
"SELECT *, false as is_expired FROM variable WHERE workspace_id = $1"
let variables =
sqlx::query_as::<_, ExportableListableVariable>(if !skip_secrets.unwrap_or(false) {
"SELECT * FROM variable WHERE workspace_id = $1"
} else {
"SELECT *, false as is_expired FROM variable WHERE workspace_id = $1 AND is_secret = false"
}
)
.bind(&w_id)
.fetch_all(&db)
.await?;
"SELECT * FROM variable WHERE workspace_id = $1 AND is_secret = false"
})
.bind(&w_id)
.fetch_all(&db)
.await?;
let mc = build_crypt(&mut db.begin().await?, &w_id).await?;

View File

@@ -152,11 +152,17 @@ pub async fn list_audit(
Ok(rows)
}
pub async fn get_audit(mut tx: Transaction<'_, sqlx::Postgres>, id: i32) -> Result<AuditLog> {
let audit_o = sqlx::query_as::<_, AuditLog>("SELECT * FROM audit WHERE id = $1")
.bind(id)
.fetch_optional(&mut *tx)
.await?;
pub async fn get_audit(
mut tx: Transaction<'_, sqlx::Postgres>,
id: i32,
w_id: &str,
) -> Result<AuditLog> {
let audit_o =
sqlx::query_as::<_, AuditLog>("SELECT * FROM audit WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(w_id)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
let audit = windmill_common::utils::not_found_if_none(audit_o, "AuditLog", &id.to_string())?;
Ok(audit)

View File

@@ -48,4 +48,6 @@ itertools.workspace = true
regex.workspace = true
git-version.workspace = true
cron.workspace = true
tracing-loki = { version = "^0", optional = true }
tracing-loki = { version = "^0", optional = true }
aws-sdk-s3.workspace = true
aws-config.workspace = true

View File

@@ -9,8 +9,10 @@
use std::{
collections::{BTreeMap, HashMap},
time::Duration,
u8,
};
use rand::Rng;
use serde::{self, Deserialize, Serialize, Serializer};
use crate::{
@@ -129,7 +131,18 @@ impl Retry {
Some(Duration::from_secs(constant.seconds as u64))
} else if previous_attempts - constant.attempts < exponential.attempts {
let exp = previous_attempts.saturating_add(1) as u32;
let secs = exponential.multiplier * exponential.seconds.saturating_pow(exp);
let mut secs = exponential.multiplier * exponential.seconds.saturating_pow(exp);
if let Some(random_factor) = exponential.random_factor {
if random_factor > 0 {
let random_component =
rand::thread_rng().gen_range(0..(std::cmp::min(random_factor, 100) as u16));
secs = match rand::thread_rng().gen_bool(1.0 / 2.0) {
true => secs.saturating_add(secs * random_component / 100),
false => secs.saturating_sub(secs * random_component / 100),
};
}
}
tracing::warn!("Rescheduling job in {} seconds due to failure", secs);
Some(Duration::from_secs(secs as u64))
} else {
None
@@ -160,18 +173,19 @@ pub struct ConstantDelay {
pub seconds: u16,
}
/// multiplier * seconds ^ failures
/// multiplier * seconds ^ failures (+/- jitter of the previous value, if any)
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
#[serde(default)]
pub struct ExponentialDelay {
pub attempts: u16,
pub multiplier: u16,
pub seconds: u16,
pub random_factor: Option<i8>, // percentage, defaults to 0 for no jitter
}
impl Default for ExponentialDelay {
fn default() -> Self {
Self { attempts: 0, multiplier: 1, seconds: 0 }
Self { attempts: 0, multiplier: 1, seconds: 0, random_factor: None }
}
}

View File

@@ -3,6 +3,7 @@ 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 JOB_DEFAULT_TIMEOUT_SECS_SETTING: &str = "job_default_timeout";
pub const REQUEST_SIZE_LIMIT_SETTING: &str = "request_size_limit_mb";
pub const LICENSE_KEY_SETTING: &str = "license_key";
pub const NPM_CONFIG_REGISTRY_SETTING: &str = "npm_config_registry";

View File

@@ -0,0 +1,187 @@
use crate::{db::DB, error};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(sqlx::FromRow)]
pub struct JobStatsRecord {
pub workspace_id: String,
pub job_id: Uuid,
pub metric_id: String,
pub metric_name: Option<String>,
pub metric_kind: MetricKind,
pub scalar_int: Option<i32>,
pub scalar_float: Option<f32>,
pub timestamps: Option<Vec<chrono::DateTime<chrono::Utc>>>,
pub timeseries_int: Option<Vec<i32>>,
pub timeseries_float: Option<Vec<f32>>,
}
#[derive(sqlx::Type, Debug, PartialEq, Deserialize, Serialize)]
#[sqlx(type_name = "METRIC_KIND", rename_all = "snake_case")]
pub enum MetricKind {
ScalarInt,
ScalarFloat,
TimeseriesInt,
TimeseriesFloat,
}
pub enum MetricNumericValue {
Integer(i32),
Float(f32),
}
pub async fn register_metric_for_job(
db: &DB,
workspace_id: String,
job_id: Uuid,
metric_id: String,
metric_kind: MetricKind,
metric_name: Option<String>,
) -> error::Result<String> {
let exists = sqlx::query_scalar!(
"SELECT true FROM job_stats WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
workspace_id,
job_id,
metric_id
)
.fetch_optional(db)
.await?
.flatten();
if exists.unwrap_or(false) {
return Err(error::Error::BadRequest(format!(
"Metric {} is already registered for job {}",
metric_id, job_id
)));
}
let (scalar_int, scalar_float, timestamps, timeseries_int, timeseries_float) = match metric_kind
{
MetricKind::ScalarInt | MetricKind::ScalarFloat => {
(None as Option<i32>, None as Option<f32>, None, None, None)
}
MetricKind::TimeseriesInt => (
None,
None,
Some(&[] as &[chrono::DateTime<chrono::Utc>]),
Some(&[] as &[i32]),
None,
),
MetricKind::TimeseriesFloat => (
None,
None,
Some(&[] as &[chrono::DateTime<chrono::Utc>]),
None,
Some(&[] as &[f32]),
),
};
sqlx::query(
"INSERT INTO job_stats (workspace_id, job_id, metric_id, metric_name, metric_kind, scalar_int, scalar_float, timestamps, timeseries_int, timeseries_float) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
)
.bind(workspace_id)
.bind(job_id)
.bind(&metric_id)
.bind(metric_name)
.bind(metric_kind)
.bind(scalar_int)
.bind(scalar_float)
.bind(timestamps)
.bind(timeseries_int)
.bind(timeseries_float)
.execute(db)
.await?;
Ok(metric_id)
}
pub async fn record_metric(
db: &DB,
workspace_id: String,
job_id: Uuid,
metric_id: String,
value: MetricNumericValue,
) -> error::Result<()> {
let metric_kind_opt: Option<MetricKind> = sqlx::query_scalar(
"SELECT metric_kind FROM job_stats WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
)
.bind(&workspace_id)
.bind(&job_id)
.bind(&metric_id)
.fetch_optional(db)
.await?;
if metric_kind_opt.is_none() {
return Err(error::Error::BadRequest(format!(
"Metric {} not yet registered for job {}.",
metric_id, job_id
)));
}
let metric_kind = metric_kind_opt.unwrap();
let (value_int, value_float) = match value {
MetricNumericValue::Integer(val) => {
if metric_kind != MetricKind::TimeseriesInt && metric_kind != MetricKind::ScalarInt {
return Err(error::Error::BadRequest(format!(
"Metric {} is not a timeseries int metric.",
metric_id
)));
}
(val, 0 as f32)
}
MetricNumericValue::Float(val) => {
if metric_kind != MetricKind::TimeseriesFloat && metric_kind != MetricKind::ScalarFloat
{
return Err(error::Error::BadRequest(format!(
"Metric {} is not a timeseries float metric.",
metric_id
)));
}
(0 as i32, val)
}
};
let timestamp = chrono::Utc::now();
match metric_kind {
MetricKind::ScalarInt => {
sqlx::query!(
"UPDATE job_stats SET scalar_int = $4 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
&workspace_id,
&job_id,
&metric_id,
value_int,
).execute(db).await?;
}
MetricKind::ScalarFloat => {
sqlx::query!(
"UPDATE job_stats SET scalar_float = $4 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
&workspace_id,
&job_id,
&metric_id,
value_float,
).execute(db).await?;
}
MetricKind::TimeseriesInt => {
sqlx::query!(
"UPDATE job_stats SET timestamps = timestamps || $4, timeseries_int = timeseries_int || $5 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
&workspace_id,
&job_id,
&metric_id,
&[timestamp],
&[value_int]
).execute(db).await?;
}
MetricKind::TimeseriesFloat => {
sqlx::query!(
"UPDATE job_stats SET timestamps = timestamps || $4, timeseries_float = timeseries_float || $5 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
&workspace_id,
&job_id,
&metric_id,
&[timestamp],
&[value_float]
).execute(db).await?;
}
}
Ok(())
}

View File

@@ -8,7 +8,7 @@ use uuid::Uuid;
use crate::{
error::{self, Error},
flow_status::{FlowStatus, RestartedFrom},
flows::FlowValue,
flows::{FlowValue, Retry},
get_latest_deployed_hash_for_path,
scripts::{ScriptHash, ScriptLang},
};
@@ -24,6 +24,7 @@ pub enum JobKind {
Dependencies,
Flow,
FlowPreview,
SingleScriptFlow,
Identity,
FlowDependencies,
AppDependencies,
@@ -282,17 +283,19 @@ pub enum JobPayload {
Dependencies {
path: String,
hash: ScriptHash,
dependencies: String,
language: ScriptLang,
dedicated_worker: Option<bool>,
deployment_message: Option<String>,
},
FlowDependencies {
path: String,
dedicated_worker: Option<bool>,
deployment_message: Option<String>,
},
AppDependencies {
path: String,
version: i64,
deployment_message: Option<String>,
},
Flow {
path: String,
@@ -308,6 +311,16 @@ pub enum JobPayload {
path: Option<String>,
restarted_from: Option<RestartedFrom>,
},
SingleScriptFlow {
path: String,
hash: ScriptHash,
args: HashMap<String, serde_json::Value>,
retry: Retry, // for now only used to retry the script, so retry is necessarily present
concurrent_limit: Option<i32>,
concurrency_time_window_s: Option<i32>,
cache_ttl: Option<i32>,
priority: Option<i16>,
},
DeploymentCallback {
path: String,
},
@@ -335,12 +348,13 @@ pub async fn script_path_to_payload(
script_path: &str,
db: &DB,
w_id: &str,
) -> error::Result<(JobPayload, Option<Tag>, Option<bool>)> {
let (job_payload, tag, delete_after_use) = if script_path.starts_with("hub/") {
) -> error::Result<(JobPayload, Option<Tag>, Option<bool>, Option<i32>)> {
let (job_payload, tag, delete_after_use, script_timeout) = if script_path.starts_with("hub/") {
(
JobPayload::ScriptHub { path: script_path.to_owned() },
None,
None,
None,
)
} else {
let (
@@ -353,6 +367,7 @@ pub async fn script_path_to_payload(
dedicated_worker,
priority,
delete_after_use,
script_timeout,
) = get_latest_deployed_hash_for_path(db, w_id, script_path).await?;
(
JobPayload::ScriptHash {
@@ -367,9 +382,10 @@ pub async fn script_path_to_payload(
},
tag,
delete_after_use,
script_timeout,
)
};
Ok((job_payload, tag, delete_after_use))
Ok((job_payload, tag, delete_after_use, script_timeout))
}
pub async fn script_hash_to_tag_and_limits<'c>(
@@ -385,9 +401,10 @@ pub async fn script_hash_to_tag_and_limits<'c>(
Option<bool>,
Option<i16>,
Option<bool>,
Option<i32>,
)> {
let script = sqlx::query!(
"select tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use from script where hash = $1 AND workspace_id = $2",
"select tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout from script where hash = $1 AND workspace_id = $2",
script_hash.0,
w_id
)
@@ -407,6 +424,7 @@ pub async fn script_hash_to_tag_and_limits<'c>(
script.dedicated_worker,
script.priority,
script.delete_after_use,
script.timeout,
))
}
@@ -415,7 +433,7 @@ pub async fn get_payload_tag_from_prefixed_path(
db: &DB,
w_id: &str,
) -> Result<(JobPayload, Option<String>), Error> {
let (payload, tag, _) = if path.starts_with("script/") {
let (payload, tag, _, _) = if path.starts_with("script/") {
script_path_to_payload(path.strip_prefix("script/").unwrap(), &db, w_id).await?
} else if path.starts_with("flow/") {
let path = path.strip_prefix("flow/").unwrap().to_string();
@@ -429,7 +447,7 @@ pub async fn get_payload_tag_from_prefixed_path(
let (tag, dedicated_worker) = r
.map(|x| (x.tag, x.dedicated_worker))
.unwrap_or_else(|| (None, None));
(JobPayload::Flow { path, dedicated_worker }, tag, None)
(JobPayload::Flow { path, dedicated_worker }, tag, None, None)
} else {
return Err(Error::BadRequest(format!(
"path must start with script/ or flow/ (got {})",

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