Compare commits

..

146 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
353 changed files with 63668 additions and 8951 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}}
@@ -102,7 +104,7 @@ jobs:
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 }}
@@ -198,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
@@ -242,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
@@ -305,7 +307,7 @@ jobs:
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
@@ -322,3 +324,63 @@ jobs:
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,208 @@
# 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)

View File

@@ -209,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/

View File

@@ -2,7 +2,7 @@
<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 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.

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

@@ -31,7 +31,8 @@
"postgresql",
"noop",
"appdependencies",
"deploymentcallback"
"deploymentcallback",
"singlescriptflow"
]
}
}
@@ -47,6 +48,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

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

@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

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,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

@@ -28,6 +28,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

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

@@ -37,6 +37,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

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

@@ -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

@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

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

@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

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
]
},

929
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.226.1"
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.226.1"
version = "1.236.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -34,7 +35,7 @@ opt-level = 0
incremental = true
[features]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise"]
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"]
@@ -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",
@@ -208,4 +211,7 @@ 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"
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

@@ -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

@@ -47,10 +47,9 @@ impl Visit for ImportsFinder {
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.ts".into()), code.into());
let fm = cm.new_source_file(FileName::Custom("main.d.ts".into()), code.into());
let lexer = Lexer::new(
// We want to parse ecmascript
Syntax::Es(EsConfig { jsx: false, ..Default::default() }),
Syntax::Typescript(TsConfig::default()),
// EsVersion defaults to es5
Default::default(),
StringInput::from(&*fm),
@@ -64,9 +63,9 @@ pub fn parse_expr_for_imports(code: &str) -> anyhow::Result<Vec<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 = ImportsFinder { imports: HashSet::new() };
swc_ecma_visit::visit_module(&mut visitor, &expr);
@@ -119,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);
@@ -150,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

@@ -31,7 +31,7 @@ pub fn parse_outputs(code: &str) -> String {
pub fn parse_ts_imports(code: &str) -> String {
let parsed = parse_expr_for_imports(code);
let r = if let Ok(parsed) = parsed {
json!({ "imports6": parsed })
json!({ "imports": parsed })
} else {
json!({"error": parsed.err().unwrap().to_string()})
};

View File

@@ -298,3 +298,15 @@ fn test_parse_imports() -> anyhow::Result<()> {
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

@@ -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.");
@@ -424,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");
@@ -499,7 +506,7 @@ Windmill Community Edition {GIT_VERSION}
let instance_name = rd_string(8);
schedule_stats(
instance_name,
mode,
mode.clone(),
&db,
&HTTP_CLIENT,
cfg!(feature = "enterprise"),
@@ -571,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()
@@ -652,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

@@ -35,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, JOB_DEFAULT_TIMEOUT, 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")]
@@ -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,25 +251,52 @@ 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)
}
}
}
}
@@ -729,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,
@@ -767,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,7 +9,7 @@ name = "windmill_api"
path = "src/lib.rs"
[features]
enterprise = ["windmill-queue/enterprise", "async-stripe", "windmill-audit/enterprise"]
enterprise = ["windmill-queue/enterprise", "async-stripe", "windmill-audit/enterprise", "windmill-git-sync/enterprise"]
enterprise_saml = ["samael"]
benchmark = []
@@ -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
@@ -84,7 +85,6 @@ candle-core.workspace = true
candle-transformers.workspace = true
candle-nn.workspace = true
aws-sdk-s3.workspace = true
aws-config.workspace = true
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"] }
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.226.1
version: 1.236.2
title: Windmill API
contact:
@@ -2231,8 +2231,7 @@ paths:
/w/{workspace}/resources/get_value_interpolated/{path}:
get:
summary:
get resource interpolated (variables and resources are fully unrolled)
summary: get resource interpolated (variables and resources are fully unrolled)
operationId: getResourceValueInterpolated
tags:
- resource
@@ -2920,8 +2919,7 @@ paths:
schema:
type: string
- name: first_parent_hash
description:
mask to filter scripts whom first direct parent has exact hash
description: mask to filter scripts whom first direct parent has exact hash
in: query
schema:
type: string
@@ -3116,8 +3114,7 @@ paths:
/workers/custom_tags:
get:
summary:
get all instance custom tags (tags are used to dispatch jobs to
summary: get all instance custom tags (tags are used to dispatch jobs to
different worker groups)
operationId: getCustomTags
tags:
@@ -3168,8 +3165,7 @@ paths:
/w/{workspace}/scripts/delete/h/{hash}:
post:
summary:
delete script by hash (erase content but keep hash, require admin)
summary: delete script by hash (erase content but keep hash, require admin)
operationId: deleteScriptByHash
tags:
- script
@@ -3408,16 +3404,15 @@ paths:
type: string
format: date-time
- name: scheduled_in_secs
description:
schedule the script to execute in the number of seconds starting now
description: schedule the script to execute in the number of seconds starting now
in: query
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)
description: make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -3477,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"
@@ -3505,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"
@@ -4448,17 +4445,16 @@ paths:
type: string
format: date-time
- name: scheduled_in_secs
description:
schedule the script to execute in the number of seconds starting now
description: schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description:
make the run invisible to the the flow owner (default false)
description: make the run invisible to the the flow owner (default false)
in: query
schema:
type: boolean
@@ -4510,17 +4506,16 @@ paths:
type: string
format: date-time
- name: scheduled_in_secs
description:
schedule the script to execute in the number of seconds starting now
description: schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description:
make the run invisible to the the flow owner (default false)
description: make the run invisible to the the flow owner (default false)
in: query
schema:
type: boolean
@@ -4558,17 +4553,16 @@ paths:
type: string
format: date-time
- name: scheduled_in_secs
description:
schedule the script to execute in the number of seconds starting now
description: schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description:
make the run invisible to the the script owner (default false)
description: make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -4599,8 +4593,7 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description:
make the run invisible to the the script owner (default false)
description: make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -4633,8 +4626,7 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description:
make the run invisible to the the script owner (default false)
description: make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -4675,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"
@@ -4811,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"
@@ -4849,8 +4843,7 @@ paths:
- job
responses:
"200":
description:
the timestamp of the db that can be used to compute the drift
description: the timestamp of the db that can be used to compute the drift
content:
application/json:
schema:
@@ -5030,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
@@ -5086,8 +5107,7 @@ paths:
/w/{workspace}/jobs/resume_urls/{id}/{resume_id}:
get:
summary:
get resume urls given a job_id, resume_id and a nonce to resume a flow
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
operationId: getResumeUrls
tags:
- job
@@ -5638,8 +5658,7 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- name: only_member_of
in: query
description:
only list the groups the user is member of (default false)
description: only list the groups the user is member of (default false)
schema:
type: boolean
responses:
@@ -5827,8 +5846,7 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- name: only_member_of
in: query
description:
only list the folders the user is member of (default false)
description: only list the folders the user is member of (default false)
schema:
type: boolean
responses:
@@ -6634,7 +6652,7 @@ paths:
- use_ssl
- cache_regions
- client_kwargs
polars_cloud_options:
storage_options:
type: object
properties:
aws_endpoint_url:
@@ -6646,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:
@@ -6770,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:
@@ -6868,8 +7065,7 @@ components:
type: integer
PerPage:
name: per_page
description:
number of items to return for a given page (default 30, max 100)
description: number of items to return for a given page (default 30, max 100)
in: query
schema:
type: integer
@@ -6894,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:
@@ -6989,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
@@ -7003,8 +7211,7 @@ components:
type: boolean
ArgsFilter:
name: args
description:
filter on jobs containing those args as a json subset (@> in postgres)
description: filter on jobs containing those args as a json subset (@> in postgres)
in: query
schema:
type: string
@@ -7016,8 +7223,7 @@ components:
type: string
ResultFilter:
name: result
description:
filter on jobs containing those result as a json subset (@> in postgres)
description: filter on jobs containing those result as a json subset (@> in postgres)
in: query
schema:
type: string
@@ -7424,6 +7630,7 @@ components:
"script_hub",
"identity",
"deploymentcallback",
"singlescriptflow",
]
schedule_path:
type: string
@@ -7533,6 +7740,7 @@ components:
"script_hub",
"identity",
"deploymentcallback",
"singlescriptflow",
]
schedule_path:
type: string
@@ -8239,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
@@ -8306,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
@@ -8340,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
@@ -8850,6 +9076,8 @@ components:
content_type:
type: string
enum: ["RawText", "Csv", "Parquet", "Unknown"]
download_url:
type: string
required:
- content_type
@@ -8884,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())
}

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,
@@ -469,6 +519,7 @@ pub struct RunJobQuery {
queue_limit: Option<i64>,
payload: Option<String>,
job_id: Option<Uuid>,
tag: Option<String>,
}
impl RunJobQuery {
@@ -508,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 {
@@ -570,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");
}
}
@@ -585,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
}
@@ -654,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
)
@@ -813,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",
@@ -1693,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);
@@ -1820,6 +1879,7 @@ pub async fn run_job_by_path(
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);
@@ -2092,6 +2152,8 @@ pub async fn run_wait_result_job_by_path_get(
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);
@@ -2210,6 +2272,7 @@ async fn run_wait_result_script_by_path_internal(
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);
@@ -2275,6 +2338,7 @@ pub async fn run_wait_result_script_by_hash(
) = 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);
@@ -2365,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);
@@ -2416,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(
@@ -2450,7 +2516,7 @@ async fn run_preview_job(
false,
None,
true,
preview.tag,
tag,
None,
None,
None,
@@ -2635,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(
@@ -2660,7 +2727,7 @@ async fn run_preview_flow_job(
false,
None,
true,
raw_flow.tag,
tag,
None,
None,
None,
@@ -2700,6 +2767,7 @@ pub async fn run_job_by_hash(
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);
@@ -2758,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?;
@@ -2787,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),
@@ -2904,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

@@ -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;
@@ -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

@@ -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,
};
@@ -325,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",
@@ -604,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,
@@ -650,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)))

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

@@ -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

@@ -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,
},

View File

@@ -23,9 +23,11 @@ pub mod external_ip;
pub mod flow_status;
pub mod flows;
pub mod global_settings;
pub mod job_metrics;
pub mod jobs;
pub mod more_serde;
pub mod oauth2;
pub mod s3_helpers;
pub mod schedule;
pub mod scripts;
pub mod server;
@@ -34,6 +36,7 @@ pub mod users;
pub mod utils;
pub mod variables;
pub mod worker;
pub mod workspaces;
#[cfg(feature = "tracing_init")]
pub mod tracing_init;

View File

@@ -0,0 +1,96 @@
use aws_config::{BehaviorVersion, Region};
use aws_sdk_s3::config::Credentials;
use serde::{Deserialize, Serialize};
#[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,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct S3Resource {
#[serde(rename = "bucket")]
pub bucket: String,
pub region: String,
#[serde(rename = "endPoint")]
pub endpoint: String,
#[serde(rename = "useSSL")]
pub use_ssl: bool,
#[serde(rename = "accessKey")]
pub access_key: Option<String>,
#[serde(rename = "secretKey")]
pub secret_key: Option<String>,
#[serde(rename = "pathStyle")]
pub path_style: bool,
pub port: Option<u16>,
}
#[derive(Deserialize, Clone)]
pub struct S3Object {
pub s3: String,
}
pub async fn get_etag_or_empty(s3_resource: &S3Resource, s3_object: S3Object) -> Option<String> {
let s3_client = build_s3_client(s3_resource);
let s3_bucket = s3_resource.bucket.clone();
let s3_key = s3_object.s3;
let s3_object_metadata = s3_client
.head_object()
.bucket(s3_bucket)
.key(s3_key)
.send()
.await;
s3_object_metadata
.ok()
.map(|res| res.e_tag().map(|et| et.to_string()))
.flatten()
}
pub fn render_endpoint(s3_resource: &S3Resource) -> String {
let url_with_prefix = 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)
};
if s3_resource.port.is_some() {
format!("{}:{}", url_with_prefix, s3_resource.port.unwrap())
} else {
url_with_prefix
}
}
pub 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);
}

View File

@@ -10,7 +10,9 @@ use chrono::DateTime;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
#[derive(FromRow, Serialize, Deserialize, Debug)]
use crate::flows::Retry;
#[derive(FromRow, Serialize, Deserialize, Debug, Clone)]
pub struct Schedule {
pub workspace_id: String,
pub path: String,
@@ -34,6 +36,15 @@ pub struct Schedule {
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 no_flow_overlap: bool,
pub summary: Option<String>,
}
impl Schedule {
pub fn parse_retry(self) -> Option<Retry> {
self.retry.map(|r| serde_json::from_value(r).ok()).flatten()
}
}
pub fn schedule_to_user(path: &str) -> String {

View File

@@ -8,6 +8,7 @@
pub const SUPERADMIN_SECRET_EMAIL: &str = "superadmin_secret@windmill.dev";
pub const SUPERADMIN_NOTIFICATION_EMAIL: &str = "superadmin_notification@windmill.dev";
pub const SUPERADMIN_SYNC_EMAIL: &str = "superadmin_sync@windmill.dev";
pub fn username_to_permissioned_as(user: &str) -> String {
if user.contains('@') {

View File

@@ -48,7 +48,6 @@ pub struct ExportableListableVariable {
pub extra_perms: serde_json::Value,
pub account: Option<i32>,
pub is_oauth: Option<bool>,
pub is_expired: Option<bool>,
}
#[derive(Deserialize)]

View File

@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct WorkspaceGitRepo {
pub script_path: String,
pub git_repo_resource_path: String,
pub use_individual_branch: Option<bool>,
}

View File

@@ -0,0 +1,22 @@
[package]
name = "windmill-git-sync"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_git_sync"
path = "./src/lib.rs"
[features]
enterprise = ["windmill-queue/enterprise"]
default = []
[dependencies]
serde.workspace = true
sqlx.workspace = true
serde_json.workspace = true
tracing.workspace = true
windmill-common = { workspace = true, features = ["axum"] }
windmill-queue.workspace = true
rsmq_async.workspace = true

View File

@@ -8,17 +8,18 @@
use std::collections::HashMap;
use crate::db::ApiAuthed;
use crate::db::DB;
use crate::workspaces::{WorkspaceGitRepo, WorkspaceSettings};
use sqlx::{Pool, Postgres};
use windmill_common::users::SUPERADMIN_SYNC_EMAIL;
use windmill_common::workspaces::WorkspaceGitRepo;
use serde_json::json;
use windmill_common::error::{Error, Result};
use windmill_common::jobs::JobPayload;
use windmill_common::scripts::ScriptHash;
use windmill_common::users::username_to_permissioned_as;
use windmill_queue::PushIsolationLevel;
pub type DB = Pool<Postgres>;
#[derive(Clone)]
pub enum DeployedObject {
Script { hash: ScriptHash, path: String },
@@ -37,13 +38,14 @@ impl DeployedObject {
}
pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
mut tx: PushIsolationLevel<'c, R>,
authed: &ApiAuthed,
email: &str,
created_by: &str,
db: &DB,
w_id: &str,
obj: DeployedObject,
deployment_message: Option<String>,
) -> Result<PushIsolationLevel<'c, R>> {
rsmq: Option<R>,
) -> Result<()> {
let skip_git_sync = if obj.get_path().starts_with("u/") {
tracing::debug!(
"Ignoring {} from git sync as it's in a private user folder",
@@ -54,10 +56,10 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
false
};
let workspace_git_repo_setting = sqlx::query_as::<_, WorkspaceSettings>(
"SELECT * FROM workspace_settings WHERE workspace_id = $1",
let workspace_git_repo_setting = sqlx::query_scalar!(
"SELECT git_sync FROM workspace_settings WHERE workspace_id = $1",
w_id
)
.bind(&w_id)
.fetch_optional(db)
.await?;
@@ -69,11 +71,10 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
let workspace_git_repo = workspace_git_repo_setting
.unwrap()
.git_sync
.map(|conf| serde_json::from_value::<WorkspaceGitRepo>(conf).ok())
.flatten();
let (git_sync_job_uuid, mut new_tx) = if !skip_git_sync && workspace_git_repo.is_some() {
let git_sync_job_uuid = if !skip_git_sync && workspace_git_repo.is_some() {
let workspace_git_repo = workspace_git_repo.unwrap();
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
@@ -84,35 +85,56 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
.strip_prefix("$res:")),
);
let default_commit_msg: String;
match obj.clone() {
let message = match obj.clone() {
DeployedObject::Script { path, .. } => {
args.insert("path".to_string(), json!(path.to_string()));
default_commit_msg = format!("Script '{}' deployed", path);
if deployment_message.as_ref().is_none()
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
{
format!("Script '{}' deployed", path)
} else {
deployment_message.clone().unwrap()
}
}
DeployedObject::Flow { path } => {
args.insert("path".to_string(), json!(path.to_string()));
default_commit_msg = format!("Flow '{}' deployed", path);
if deployment_message.as_ref().is_none()
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
{
format!("Flow '{}' deployed", path)
} else {
deployment_message.clone().unwrap()
}
}
DeployedObject::App { path, .. } => {
args.insert("path".to_string(), json!(path.to_string()));
default_commit_msg = format!("App '{}' deployed", path);
if deployment_message.as_ref().is_none()
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
{
format!("App '{}' deployed", path)
} else {
deployment_message.clone().unwrap()
}
}
}
};
args.insert("commit_msg".to_string(), json!(message));
args.insert(
"commit_msg".to_string(),
json!(deployment_message.clone().unwrap_or(default_commit_msg)),
"use_individual_branch".to_string(),
json!(workspace_git_repo.use_individual_branch.unwrap_or(false)),
);
let tx: PushIsolationLevel<'_, R> = PushIsolationLevel::IsolatedRoot(db.clone(), rsmq);
let (job_uuid, new_tx) = windmill_queue::push(
&db,
db,
tx,
&w_id,
JobPayload::DeploymentCallback { path: workspace_git_repo.script_path.clone() },
args,
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
created_by,
&email,
SUPERADMIN_SYNC_EMAIL.to_string(),
None,
None,
None,
@@ -128,16 +150,10 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
None,
)
.await?;
(Some(job_uuid), new_tx)
new_tx.commit().await?;
Some(job_uuid)
} else {
let new_tx = match tx {
PushIsolationLevel::Isolated(user_db, authed, rsmq) => {
(rsmq, user_db.begin(&authed).await?).into()
}
PushIsolationLevel::IsolatedRoot(db, rsmq) => (rsmq, db.begin().await?).into(),
PushIsolationLevel::Transaction(tx) => tx,
};
(None, new_tx)
None
};
// We're not persisting the default commit msg as it's pretty useless. We will persist the ones manually set by users
@@ -149,28 +165,27 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
if deployment_message.is_some() || job_uuids.len() > 0 {
// if the git sync job hasn't been triggered, and there is not custom deployment message, there's not point adding an entry to the table
match obj.clone() {
DeployedObject::Script { path, hash, .. } => {
sqlx::query!(
"INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
w_id, path, hash.0, &job_uuids, deployment_message,
)
},
DeployedObject::Flow { path } => {
sqlx::query!(
"INSERT INTO deployment_metadata (workspace_id, path, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, path) WHERE script_hash IS NULL AND app_version IS NULL DO UPDATE SET callback_job_ids = $3, deployment_msg = $4",
w_id, path, &job_uuids, deployment_message,
)
}
DeployedObject::App { path, version } => {
sqlx::query!(
"INSERT INTO deployment_metadata (workspace_id, path, app_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
w_id, path, version, &job_uuids, deployment_message,
)
}
}.execute(&mut new_tx)
.await?;
DeployedObject::Script { path, hash, .. } => {
sqlx::query!(
"INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
w_id, path, hash.0, &job_uuids, deployment_message,
)
},
DeployedObject::Flow { path } => {
sqlx::query!(
"INSERT INTO deployment_metadata (workspace_id, path, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, path) WHERE script_hash IS NULL AND app_version IS NULL DO UPDATE SET callback_job_ids = $3, deployment_msg = $4",
w_id, path, &job_uuids, deployment_message,
)
}
DeployedObject::App { path, version } => {
sqlx::query!(
"INSERT INTO deployment_metadata (workspace_id, path, app_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
w_id, path, version, &job_uuids, deployment_message,
)
}
}.execute(db)
.await?;
}
tx = PushIsolationLevel::Transaction(new_tx);
return Ok(tx);
return Ok(());
}

View File

@@ -23,6 +23,7 @@ use axum::{
};
use bigdecimal::ToPrimitive;
use chrono::{DateTime, Duration, Utc};
use itertools::Itertools;
use prometheus::IntCounter;
use reqwest::{
header::{HeaderMap, CONTENT_TYPE},
@@ -34,7 +35,7 @@ use serde_json::{json, value::RawValue};
use sqlx::{types::Json, FromRow, Pool, Postgres, Transaction};
#[cfg(feature = "benchmark")]
use std::time::Instant;
use tokio::sync::RwLock;
use tokio::{sync::RwLock, time::sleep};
use tracing::{instrument, Instrument};
use ulid::Ulid;
use uuid::Uuid;
@@ -48,7 +49,9 @@ use windmill_common::{
BranchAllStatus, FlowCleanupModule, FlowStatus, FlowStatusModule, FlowStatusModuleWParent,
Iterator, JobResult, RestartedFrom, RetryStatus, MAX_RETRY_ATTEMPTS, MAX_RETRY_INTERVAL,
},
flows::{add_virtual_items_if_necessary, FlowModuleValue, FlowValue},
flows::{
add_virtual_items_if_necessary, FlowModule, FlowModuleValue, FlowValue, InputTransform,
},
jobs::{
get_payload_tag_from_prefixed_path, CompletedJob, JobKind, JobPayload, QueuedJob, RawCode,
},
@@ -147,7 +150,7 @@ pub async fn cancel_job<'c: 'async_recursion>(
tracing::info!("Soft cancelling job {}", id);
}
} else {
let reason = reason
let reason: String = reason
.clone()
.unwrap_or_else(|| "unexplicited reasons".to_string());
let e = serde_json::json!({"message": format!("Job canceled: {reason} by {username}"), "name": "Canceled", "reason": reason, "canceler": username});
@@ -203,11 +206,113 @@ pub async fn cancel_job<'c: 'async_recursion>(
Ok((tx, Some(id)))
}
pub async fn cancel_persistent_script_jobs<'c>(
username: &str,
reason: Option<String>,
script_path: &str,
w_id: &str,
db: &Pool<Postgres>,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
) -> error::Result<Vec<Uuid>> {
// There can be only one perpetual script run per 10 seconds window. We execute the cancel twice with 5s interval
// to avoid the case of a job _about to be restarted_ when the first cancel is run.
let cancelled_job_ids_first_batch = cancel_persistent_script_jobs_internal(
username,
reason.clone(),
script_path,
w_id,
db,
rsmq.clone(),
)
.await?;
sleep(std::time::Duration::from_secs(5)).await;
let cancelled_job_ids_second_batch = cancel_persistent_script_jobs_internal(
username,
reason.clone(),
script_path,
w_id,
db,
rsmq.clone(),
)
.await?;
return Ok(cancelled_job_ids_first_batch
.into_iter()
.chain(cancelled_job_ids_second_batch)
.collect_vec());
}
async fn cancel_persistent_script_jobs_internal<'c>(
username: &str,
reason: Option<String>,
script_path: &str,
w_id: &str,
db: &Pool<Postgres>,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
) -> error::Result<Vec<Uuid>> {
let mut tx = db.begin().await?;
// we could have retrieved the job IDs in the first query where we retrieve the hashes, but just in case a job was inserted in the queue right in-between the two above query, we re-do the fetch here
let jobs_to_cancel = sqlx::query_scalar::<_, Uuid>(
"SELECT id FROM queue WHERE workspace_id = $1 AND script_path = $2 AND canceled = false",
)
.bind(w_id)
.bind(script_path)
.fetch_all(&mut *tx)
.await?;
// Then we cancel all the jobs currently in the queue, one by one
for queued_job_id in jobs_to_cancel.clone() {
let (new_tx, _) = cancel_job(
username,
reason.clone(),
queued_job_id,
w_id,
tx,
db,
rsmq.clone(),
false,
)
.await?;
tx = new_tx;
}
tx.commit().await?;
return Ok(jobs_to_cancel);
}
#[derive(Serialize, Debug)]
pub struct WrappedError {
pub error: serde_json::Value,
}
pub trait ValidableJson {
fn is_valid_json(&self) -> bool;
}
impl ValidableJson for WrappedError {
fn is_valid_json(&self) -> bool {
true
}
}
impl ValidableJson for Box<RawValue> {
fn is_valid_json(&self) -> bool {
!self.get().is_empty()
}
}
impl ValidableJson for serde_json::Value {
fn is_valid_json(&self) -> bool {
true
}
}
impl<T: ValidableJson> ValidableJson for Json<T> {
fn is_valid_json(&self) -> bool {
self.0.is_valid_json()
}
}
pub async fn register_metric<T, F, F2, R>(
l: &Arc<RwLock<HashMap<String, T>>>,
s: &str,
@@ -312,7 +417,7 @@ lazy_static::lazy_static! {
#[instrument(level = "trace", skip_all, name = "add_completed_job")]
pub async fn add_completed_job<
T: Serialize + Send + Sync,
T: Serialize + Send + Sync + ValidableJson,
R: rsmq_async::RsmqConnection + Clone + Send,
>(
db: &Pool<Postgres>,
@@ -328,6 +433,12 @@ pub async fn add_completed_job<
// tracing::error!("Start");
// let start = tokio::time::Instant::now();
if !result.is_valid_json() {
return Err(Error::InternalErr(
"Result of job is invalid json (empty)".to_string(),
));
}
let is_flow =
queued_job.job_kind == JobKind::Flow || queued_job.job_kind == JobKind::FlowPreview;
let duration = if is_flow {
@@ -574,6 +685,21 @@ pub async fn add_completed_job<
if p {
let tx = PushIsolationLevel::IsolatedRoot(db.clone(), rsmq);
// perpetual jobs can run one job per 10s max. If the job was faster than 10s, schedule the next one with the appropriate delay
let now = chrono::Utc::now();
let scheduled_for = if now
.signed_duration_since(queued_job.started_at.unwrap_or(now))
.num_seconds()
< 10
{
let next_run =
queued_job.started_at.unwrap_or(now) + chrono::Duration::seconds(10);
tracing::warn!("Perpetual script {:?} is running too fast, only 1 job per 10s it supported. Scheduling next run for {:?}", queued_job.script_path, next_run);
Some(next_run)
} else {
None
};
let (_uuid, tx) = push(
db,
tx,
@@ -595,7 +721,7 @@ pub async fn add_completed_job<
&queued_job.created_by,
&queued_job.email,
queued_job.permissioned_as.clone(),
None,
scheduled_for,
queued_job.schedule_path.clone(),
None,
None,
@@ -876,6 +1002,9 @@ pub async fn handle_maybe_scheduled_job<'c, R: rsmq_async::RsmqConnection + Clon
on_recovery_times: schedule.on_recovery_times,
on_recovery_extra_args: schedule.on_recovery_extra_args,
ws_error_handler_muted: schedule.ws_error_handler_muted,
retry: schedule.retry,
summary: schedule.summary,
no_flow_overlap: schedule.no_flow_overlap,
},
)
.await;
@@ -2367,21 +2496,23 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
dedicated_worker,
None,
),
JobPayload::Dependencies { hash, dependencies, language, path, dedicated_worker } => (
Some(hash.0),
Some(path),
Some((dependencies, None)),
JobKind::Dependencies,
None,
None,
Some(language),
None,
None,
None,
dedicated_worker,
None,
),
JobPayload::FlowDependencies { path, dedicated_worker } => {
JobPayload::Dependencies { hash, language, path, dedicated_worker, deployment_message } => {
(
Some(hash.0),
Some(path),
Some((deployment_message.unwrap_or_else(String::new), None)),
JobKind::Dependencies,
None,
None,
Some(language),
None,
None,
None,
dedicated_worker,
None,
)
}
JobPayload::FlowDependencies { path, dedicated_worker, deployment_message } => {
let value_json = fetch_scalar_isolated!(
sqlx::query_scalar!(
"SELECT value FROM flow WHERE path = $1 AND workspace_id = $2",
@@ -2399,7 +2530,7 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
(
None,
Some(path),
None,
Some((deployment_message.unwrap_or_else(String::new), None)),
JobKind::FlowDependencies,
Some(value.clone()),
Some(FlowStatus::new(&value)), // this is a new flow being pushed, flow_status is set to flow_value
@@ -2411,10 +2542,10 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
None,
)
}
JobPayload::AppDependencies { path, version } => (
JobPayload::AppDependencies { path, version, deployment_message } => (
Some(version),
Some(path),
None,
Some((deployment_message.unwrap_or_else(String::new), None)),
JobKind::AppDependencies,
None,
None,
@@ -2478,6 +2609,63 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
value.priority,
)
}
JobPayload::SingleScriptFlow {
path,
hash,
retry,
args,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
priority,
} => {
let mut input_transforms = HashMap::<String, InputTransform>::new();
for (arg_name, arg_value) in args {
input_transforms.insert(arg_name, InputTransform::Static { value: arg_value });
}
let flow_value = FlowValue {
modules: vec![FlowModule {
id: "a".to_string(),
value: windmill_common::flows::FlowModuleValue::Script {
input_transforms: input_transforms,
path: path.clone(),
hash: Some(hash),
},
stop_after_if: None,
summary: None,
suspend: None,
mock: None,
retry: Some(retry),
sleep: None,
cache_ttl: None,
timeout: None,
priority: None,
delete_after_use: None,
}],
same_worker: false,
failure_module: None,
concurrency_time_window_s: concurrency_time_window_s,
concurrent_limit: concurrent_limit,
skip_expr: None,
cache_ttl: cache_ttl.map(|val| val as u32),
early_return: None,
priority: priority,
};
(
None,
Some(path),
None,
JobKind::SingleScriptFlow,
Some(flow_value.clone()),
Some(FlowStatus::new(&flow_value)), // this is a new flow being pushed, flow_status is set to flow_value
None,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
None,
priority,
)
}
JobPayload::Flow { path, dedicated_worker } => {
let value_json = fetch_scalar_isolated!(
sqlx::query_scalar!(
@@ -2783,6 +2971,7 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
}
JobKind::Flow => "jobs.run.flow",
JobKind::FlowPreview => "jobs.run.flow_preview",
JobKind::SingleScriptFlow => "jobs.run.single_script_flow",
JobKind::Script_Hub => "jobs.run.script_hub",
JobKind::Dependencies => "jobs.run.dependencies",
JobKind::Identity => "jobs.run.identity",

View File

@@ -10,7 +10,9 @@ use crate::push;
use crate::PushIsolationLevel;
use crate::QueueTransaction;
use sqlx::{query_scalar, Postgres, Transaction};
use std::collections::HashMap;
use std::str::FromStr;
use windmill_common::flows::Retry;
use windmill_common::jobs::JobPayload;
use windmill_common::schedule::schedule_to_user;
use windmill_common::DB;
@@ -104,20 +106,50 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
&schedule.script_path,
)
.await?;
(
JobPayload::ScriptHash {
hash,
path: schedule.script_path,
concurrent_limit: concurrent_limit,
concurrency_time_window_s: concurrency_time_window_s,
cache_ttl: cache_ttl,
dedicated_worker,
language,
priority,
},
tag,
timeout,
)
if schedule.retry.is_some() {
let parsed_retry =
serde_json::from_value::<Retry>(schedule.retry.unwrap()).map_err(|err| {
error::Error::InternalErr(format!(
"Unable to parse retry information from schedule: {}",
err.to_string(),
))
})?;
let mut static_args = HashMap::<String, serde_json::Value>::new();
for (arg_name, arg_value) in args.clone() {
static_args.insert(arg_name, arg_value);
}
// if retry is set, we wrap the script into a one step flow with a retry on the module
(
JobPayload::SingleScriptFlow {
path: schedule.script_path,
hash: hash,
retry: parsed_retry,
args: static_args,
concurrent_limit: concurrent_limit,
concurrency_time_window_s: concurrency_time_window_s,
cache_ttl: cache_ttl,
priority: priority,
},
Some("flow".to_string()),
timeout,
)
} else {
(
JobPayload::ScriptHash {
hash,
path: schedule.script_path,
concurrent_limit: concurrent_limit,
concurrency_time_window_s: concurrency_time_window_s,
cache_ttl: cache_ttl,
dedicated_worker,
language,
priority,
},
tag,
timeout,
)
}
};
sqlx::query!(

View File

@@ -10,7 +10,7 @@ path = "src/lib.rs"
[features]
default = []
enterprise = ["windmill-queue/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:pem", "dep:sha2", "dep:tiberius", "dep:tokio-util"]
enterprise = ["windmill-queue/enterprise", "windmill-git-sync/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:pem", "dep:sha2", "dep:tiberius", "dep:tokio-util"]
benchmark = ["windmill-queue/benchmark"]
flamegraph = []
@@ -31,6 +31,7 @@ windmill-parser-py-imports.workspace = true
windmill-parser-bash.workspace = true
windmill-parser-sql.workspace = true
windmill-parser-graphql.workspace = true
windmill-git-sync.workspace = true
sqlx.workspace = true
uuid.workspace = true
tracing.workspace = true

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