Compare commits

...

164 Commits

Author SHA1 Message Date
Faton Ramadani
43caa4ee0b fix(frontend): fix sign in with Google button 2023-12-01 17:16:43 +01:00
Faton Ramadani
e2cac7c57f fix(frontend): fix sign in with Google button 2023-12-01 17:14:48 +01:00
Faton Ramadani
2f72be36e5 App menu improvements (#2753)
* fix(frontend): add dropdown menu result + fix splitpanes seperators

* fix(frontend): remove useless border

* fix(frontend): fix arg enum

* fix(frontend): remove useless div

* fix(frontend): remove duplicated code

* fix(frontend): remove duplicated code

* fix(frontend): fix reactivity

* fix(frontend): fix reactivity
2023-12-01 17:10:50 +01:00
HugoCasa
d47d4ccf85 fix: improve azure openai readme (#2754) 2023-12-01 17:10:19 +01:00
HugoCasa
583dae6a72 fix: string or enum (#2752) 2023-12-01 16:07:16 +01:00
HugoCasa
8fc0afce71 fix: do not prorate for team plan checkouts mornings of firsts (#2751) 2023-12-01 15:16:54 +01:00
Faton Ramadani
6eaec47162 fix(frontend): fix resource search (#2748) 2023-12-01 13:32:45 +01:00
Faton Ramadani
a3f1111ca7 feat(frontend): add before and after icons for text input components (#2746) 2023-12-01 13:26:50 +01:00
Ruben Fiszel
60c4860233 fix: fix early return sync on existing flows 2023-12-01 13:18:31 +01:00
Faton Ramadani
85805bdf82 fix(frontend): fix tutorials contols (#2745) 2023-12-01 10:20:53 +01:00
Ruben Fiszel
225f675fb3 app ux nits 2023-12-01 10:00:16 +01:00
Ruben Fiszel
a122b6cab6 fix: show bg runnables currently running 2023-12-01 09:49:33 +01:00
Ruben Fiszel
dd980ee296 display more info on backend tests 2023-12-01 08:08:58 +01:00
Ruben Fiszel
45648bbf7f fix componentoutputviewer reset 2023-12-01 07:55:33 +01:00
Ruben Fiszel
1d90434f12 chore(main): release 1.219.1 (#2742)
* chore(main): release 1.219.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-01 07:41:18 +01:00
Ruben Fiszel
cb42f10dc6 fix: fix editorContext in preview 2023-12-01 07:38:51 +01:00
Ruben Fiszel
62de305908 fix: maps mapRegion update on move 2023-12-01 07:12:09 +01:00
Ruben Fiszel
49803e26f6 chore(main): release 1.219.0 (#2739)
* chore(main): release 1.219.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-12-01 01:20:38 +01:00
Ruben Fiszel
592d7839d1 fix: limit log pull from queued jobs 2023-12-01 01:14:01 +01:00
Ruben Fiszel
f195320e99 fix aggrid classes 2023-12-01 00:24:09 +01:00
Ruben Fiszel
fdb546ea2a fix eval preview 2023-12-01 00:13:27 +01:00
Ruben Fiszel
772e3b8a32 fix eval preview 2023-12-01 00:07:44 +01:00
Ruben Fiszel
1bef60dd50 fix eval preview 2023-12-01 00:02:39 +01:00
Ruben Fiszel
d4374a0103 feat: eval preview for apps 2023-11-30 23:54:33 +01:00
Ruben Fiszel
fc5f054b94 fix: improve stat card 2023-11-30 22:09:57 +01:00
Ruben Fiszel
4774e03be5 fix: improve quickstyle to be in static ccomponents only 2023-11-30 21:09:27 +01:00
Ruben Fiszel
007d5be23e fix: support INET in pg 2023-11-30 20:18:43 +01:00
Faton Ramadani
f816ad01d3 fix(frontend): rename menu component (#2738) 2023-11-30 17:51:21 +01:00
Ruben Fiszel
bb7f11fcda chore(main): release 1.218.0 (#2731)
* chore(main): release 1.218.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-11-30 17:22:05 +01:00
Ruben Fiszel
830dec0f90 fix: make REST language support URLSearchParams, headers, FormData 2023-11-30 16:50:00 +01:00
Guillaume Bouvignies
920cc9a576 fix: Using latest gosyn (#2737) 2023-11-30 15:57:16 +01:00
HugoCasa
1f99fcd3af fix: update deployed even when draft only (#2694)
* fix: update deployed even when draft only

* feat: create new when draft only and saving draft

* fix: nit

* fix: final fixes
2023-11-30 15:46:44 +01:00
Roman Grazhdan
52a2746e50 Adding build options for images, utilities and utilities' versions (#2736)
* Adding build options for images, utilities and utilities' versions

* Adding build options for images, utilities and utilities' versions

---------

Co-authored-by: r.grazdhan <r.grazdhan@team.bumble.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2023-11-30 15:44:11 +01:00
Faton Ramadani
ec25856b7c feat(frontend): resource rework (#2725)
* feat(frontend): resource rework wip

* feat(frontend): Fix styling

* fix(frontend): fix loading state

* fix(frontend): correclty handle the case wihout any resouces
2023-11-30 15:43:03 +01:00
dependabot[bot]
2d72facbdf chore(deps): bump aws-sdk-s3 from 0.36.0 to 0.39.1 in /backend (#2672)
* chore(deps): bump aws-sdk-s3 from 0.36.0 to 0.39.1 in /backend

Bumps [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust) from 0.36.0 to 0.39.1.
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Changelog](https://github.com/awslabs/aws-sdk-rust/blob/main/CHANGELOG.md)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>

* fix compile breaks

* Add behavior version

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gbouv <guillaume@windmill.dev>
2023-11-30 14:01:20 +01:00
Ruben Fiszel
06411c8a63 build short sha for reports as well 2023-11-30 14:00:37 +01:00
Guillaume Bouvignies
0bf3685cbd fix: Pull patched version of gosyn (#2734) 2023-11-30 13:34:49 +01:00
Faton Ramadani
56c0d5a755 Fix app export (#2735)
* fix(frontend): fix app export

* fix(frontend): fix app export
2023-11-30 13:30:18 +01:00
Faton Ramadani
06656924ae feat(frontend): add menu component (#2721)
* feat(frontend): add menu component

* wip

* feat(frontend): menu component done

* feat(frontend): Fix styling + handle menu items everywhere

* feat(frontend): Fix styling + handle menu items everywhere
2023-11-30 12:39:18 +01:00
Guillaume Bouvignies
70c504edfa fix: Workspace error handler creation was not adding the slack resource (#2733)
Co-authored-by: admin <admin@windmill.dev>
2023-11-30 12:20:32 +01:00
Ruben Fiszel
0d93986b18 add reports image 2023-11-30 10:29:04 +01:00
Ruben Fiszel
651f74b00e improve python client 2023-11-29 21:41:44 +01:00
HugoCasa
16d10aedf7 fix: s3 resource openapi spec (#2730) 2023-11-29 21:18:17 +01:00
Ruben Fiszel
72bb15f6a0 chore(main): release 1.217.0 (#2705)
* chore(main): release 1.217.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-11-29 18:28:51 +01:00
Ruben Fiszel
772c04c9b3 update python-client 2023-11-29 18:19:47 +01:00
Ruben Fiszel
03d3bd33d0 restart dedicated worker regardless of config being present 2023-11-29 17:49:01 +01:00
HugoCasa
3789b34dae feat: scheduled app reports (#2714)
* feat: app preview discord

* feat: add custom, slack, email + worker groups

* fix: npm build

* fix: add assignable chromium tag

* feat: use workspace linked slack resource + hub scripts

* fix: nit
2023-11-29 16:35:42 +01:00
Guillaume Bouvignies
3d6fb15a90 fix: minor fixes to private NPM and python registries to get everything working (#2728) 2023-11-29 16:30:45 +01:00
Ruben Fiszel
e6d67f4e59 fix: make dedicated workers able to redeploy automatically 2023-11-29 12:28:06 +01:00
Faton Ramadani
1a1d1db96f fix(frontend): Fix table initial ordering (#2727)
* fix(frontend): Fix initial column order

* fix(frontend): Fix initial column order
2023-11-29 12:07:51 +01:00
Guillaume Bouvignies
15ebf46abc chore: Add examples on how to deploy private registries (#2719)
* chore: Add examples on how to deploy private registries

* chore: example for private pypiserver with https

* Allow DENO_CERT certificate with native workers

* Add BUN_TLS_REJECT_UNAUTHORIZED
2023-11-29 11:38:23 +01:00
Ruben Fiszel
e9a7f0cf17 fix benchmark 2023-11-29 11:08:23 +01:00
Ruben Fiszel
4eb25216f3 fix benchmarks for dedicated 2023-11-29 10:55:18 +01:00
Ruben Fiszel
b7fe5ddf15 fix benchmarks 2023-11-29 10:13:32 +01:00
Ruben Fiszel
f7d93c56df fix backend tests 2023-11-29 10:04:15 +01:00
Ruben Fiszel
f78f14e268 fix backend tests 2023-11-29 09:58:34 +01:00
Ruben Fiszel
5a66806c76 fix backend tests 2023-11-29 09:44:37 +01:00
Ruben Fiszel
c8046af9d0 fix: only list session and permanent token in user settings 2023-11-29 00:34:04 +01:00
HugoCasa
15b2c9f171 fix: better error for moved openai resource (#2724)
* fix: better error for moved openai resource

* fix: nit
2023-11-29 00:24:56 +01:00
Faton Ramadani
89abb68f63 feat(frontend): Stat card improvement (#2709)
* feat(frontend): add class+ style support + added support for custom images

* feat(frontend): add class+ style support + added support for custom images

* feat(frontend): fix styling

* feat(frontend): add quick style buttons

* feat(frontend): fix quick tailwind class

* feat(frontend): add space only if needed

* feat(frontend): fix build

* feat(frontend): fix line heights
2023-11-28 22:37:11 +01:00
Faton Ramadani
9525ab7bba fix(frontend): fix separator z-index (#2720) 2023-11-28 22:34:47 +01:00
Guillaume Bouvignies
bd31979a62 fix: generate cargo lock file (#2722) 2023-11-28 20:07:49 +01:00
HugoCasa
76a387f4a1 fix: ai fix popup placement + update edit/fix prompt to return complete code (#2715) 2023-11-28 18:51:30 +01:00
Faton Ramadani
46e0f91387 feat(frontend): add "hide schedules" filter (#2710)
* feat(frontend): add hide schedules filter

* feat(frontend): rework filters

* feat(frontend): fix filters

* feat(frontend): fix filter layout
2023-11-28 16:57:10 +01:00
Ruben Fiszel
55e49fbd4b migrate tests on ubicloud 2023-11-28 15:27:18 +01:00
Faton Ramadani
c990f856aa fix(frontend): Improve error message + fix overflow when file name is too long (#2691) 2023-11-28 11:31:33 +01:00
Faton Ramadani
2d3ce8a49c feat(frontend): Added tailwind classes auto-complete (#2712)
* feat(frontend): Added tailwind classes auto-complete

* feat(frontend): handle tailwindcss as a language
2023-11-28 11:13:49 +01:00
Guillaume Bouvignies
726866b410 fix: Deno can talk to private NPM registries behind HTTPS (#2713) 2023-11-27 21:46:19 +01:00
Ruben Fiszel
c3eaf0bf4a fix: improve autocomplete reactivity 2023-11-27 18:55:41 +01:00
dependabot[bot]
2bbd0b34b5 chore(deps): bump aws-config from 0.57.2 to 1.0.0 in /backend (#2671)
Bumps [aws-config](https://github.com/smithy-lang/smithy-rs) from 0.57.2 to 1.0.0.
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Guillaume Bouvignies <guillaume@windmill.dev>
2023-11-27 12:17:26 +01:00
Guillaume Bouvignies
36e46e2e47 fix: Error handler now supports flows (#2707)
* fix: Error handler now supports flows

* Update README

* remove unused import
2023-11-27 10:15:46 +00:00
Ikko Eltociear Ashimine
eec7d83d98 Update README.md (#2706)
chartss -> charts
2023-11-27 01:09:32 +01:00
Ruben Fiszel
d2b3026032 fix: trim .bun.ts for local imports 2023-11-27 00:59:14 +01:00
Ruben Fiszel
8275602372 feat: add support for raw query args 2023-11-26 16:47:32 +01:00
Ruben Fiszel
095da9e76e chore(main): release 1.216.0 (#2692)
* chore(main): release 1.216.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-11-26 14:40:33 +01:00
Ruben Fiszel
731f92b907 ux nits 2023-11-26 14:36:42 +01:00
Ruben Fiszel
dbaef0aa5f feat: add early return for flows 2023-11-26 14:30:49 +01:00
Ruben Fiszel
19ab924fa2 improve cli sync 2023-11-25 16:57:13 +01:00
Ruben Fiszel
bae0f45f21 remove transition in component output 2023-11-25 16:00:05 +01:00
Ruben Fiszel
5fa653d154 fix: improve multiselect from form 2023-11-25 15:26:05 +01:00
Ruben Fiszel
94e9b80e19 fix: expand enum type narrowing to forms 2023-11-25 11:03:14 +01:00
Faton Ramadani
0c0f43dd3a feat(frontend): add currency format + add enum autocomplete + fix run… (#2670)
* feat(frontend): add currency format + add enum autocomplete + fix runs page layout

* feat(frontend): use ArgEnum for array when using enums

* Fix currencies

* feat(frontend): fix currency input

* feat(frontend): fix multiselect

* feat(frontend): fix multiselect
2023-11-25 10:16:06 +01:00
Stephan Fitzpatrick
3bb2a0c960 Corrently handle terminal state when invoking job through run_script method (#2703) 2023-11-25 09:38:53 +01:00
Guillaume Bouvignies
55e34d8cdd fix: S3 bucket browser small improvements and fixes (#2700)
* fix: S3 bucket browser small improvements and fixes

* Bump file limits to 20 in CE and add a message in the FE
2023-11-24 20:18:02 +01:00
Faton Ramadani
81ef24b3ce feat(frontend): add stat card (#2687)
* feat(frontend): add stat card

* feat(frontend): fix layout
2023-11-24 20:17:45 +01:00
Stephan Fitzpatrick
603e7ff67f feat(python): Update return type for 'get_resource' function (#2695)
* Update return type for 'get_resource' function

Modified the 'get_resource' function in 'wmill/client.py' to additionally return a 'dict', providing support for dictionaries in addition to strings and None. This change caters towards use-cases where getting a dictionary resource is needed, enhancing flexibility of resource handling in Windmill client operations. Code updated in two locations for consistency.

* Update README.md for python-client

Expanded and updated the 'README.md' to include detailed explanations about the basic and advanced usage of the 'wmill' package. The previous version wasn't as comprehensive and lacked examples. Now it includes thorough usage guidance, additional explanations, and code examples. Change improves usability for new developers approaching the project.
2023-11-24 20:17:30 +01:00
HugoCasa
f1cabb40f6 feat: fill pg resource from string (#2699) 2023-11-24 20:17:17 +01:00
Guillaume Bouvignies
e8b1f220dd fix: Priority tags FE buggy when missing from config (#2702) 2023-11-24 19:42:27 +01:00
Ruben Fiszel
16be2300ff fix: improve resource pages 2023-11-23 19:51:34 +01:00
Ruben Fiszel
21765922f8 use nested flow keys 2023-11-23 19:20:58 +01:00
Faton Ramadani
f4aa76897e fix(frontend): correctly handle when result is undefined (#2693) 2023-11-23 18:21:58 +01:00
Ruben Fiszel
4c42836cfc fix: lighten monaco editor workers (#2690)
* all

* initializeVscode everywhere

* fix: db schema autocompletion

* fix: add missing initialized

---------

Co-authored-by: HugoCasa <hugo@casademont.ch>
2023-11-23 18:19:43 +01:00
Ruben Fiszel
9d32bdf5a6 chore(main): release 1.215.0 (#2689)
* chore(main): release 1.215.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-11-23 15:07:52 +01:00
HugoCasa
e3f2b43748 feat: query embeddings from s3 (#2683)
* feat: query embeddings from s3

* fix: better error handling + logging
2023-11-23 15:03:12 +01:00
HugoCasa
f5e098d03b fix: bigquery schema and date inputs (#2688) 2023-11-23 14:59:33 +01:00
Ruben Fiszel
cfd3da41ef fix: improve wmill go client 2023-11-23 14:59:16 +01:00
Ruben Fiszel
b4bbb794b5 chore(main): release 1.214.1 (#2673)
* chore(main): release 1.214.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-11-23 08:51:19 +01:00
Stephan Fitzpatrick
1c2abcda23 Feat(python): Quality of Life improvements to Python client (#2686)
* Fix references to `Windmill.start_script_async`

* Improve client usability and extend functionalities

This commit introduces several changes to improve the usability of the module and extend its functionalities. Line breaks have been added for long function calls to improve readability. A state setter has been added for the 'state' property to facilitate state updates. New functions 'cancel_running' and 'run_script' have been added to provide more control to the user over the script executions. The README file has been updated to reflect these changes and provide more comprehensive usage instructions including both basic and advanced usage of the module.

* Add line breaks and update README.md for readability

Inserted line breaks into function calls for improved readability in python-client/wmill/README.md. This enhances module usability by making code easier to follow. 'state' property setter, 'cancel_running' and 'run_script' functions have been added to enrich module's functionality. README was updated to reflect these changes and provide clearer instructions.
2023-11-23 08:39:38 +01:00
Stephan Fitzpatrick
9783abba0a Fix references to Windmill.start_script_async (#2685) 2023-11-23 07:48:48 +01:00
Henri Courdent
2859bbe7b4 Tiny change diagram architecture (#2681) 2023-11-22 19:46:39 +01:00
Ruben Fiszel
8f8ea227c8 fix: relax tags constraints 2023-11-22 18:30:43 +01:00
Ruben Fiszel
44775a4de6 v2 2023-11-22 18:29:51 +01:00
HugoCasa
217e69498f fix: graphql variables (#2682) 2023-11-22 18:17:37 +01:00
Stephan Fitzpatrick
449974404a Update method name in wmill README sample code (#2679)
In the README's example code for the wmill Python client, the method name `start_execution` has been changed to `run_script_async`. This change is in line with the recent updates we made to the client's public API. The naming adjustment adds more clarity to the method's functionality, and ensures that the sample code in the README is accurate and stays updated with the latest changes in the wmill Python client's API.
2023-11-22 17:16:07 +01:00
Faton Ramadani
7fe3bca624 fix(frontend): improve tutorial ux (#2677)
* fix(frontend): improve tutorial ux

* fix(frontend): small ui fix

* fix(frontend): prevent tutorial from running when an app is forked from the hub or a template
2023-11-22 17:15:54 +01:00
Henri Courdent
cbdaf3b1d8 Updating readme (#2680) 2023-11-22 17:15:42 +01:00
Faton Ramadani
31fbc5867c fix(frontend): use popover for schedule for later on the runs page (#2678) 2023-11-22 14:52:08 +01:00
Faton Ramadani
bfdb559b47 fix(frontend): disable active interaction to avoid broken state (#2675)
* fix(frontend): disable active interaction to avoid broken state

* fix(frontend): id tutorial id
2023-11-22 12:49:50 +01:00
Guillaume Bouvignies
c42c54e69b fix: Various fixes and improvements for Windmill S3 capabilities (#2674) 2023-11-22 12:29:50 +01:00
Ruben Fiszel
7ae84fce50 fix: add image base64 source kinds 2023-11-22 11:25:36 +01:00
Ruben Fiszel
45ee1d7703 fix: fix backend build 2023-11-22 10:40:59 +01:00
Ruben Fiszel
391f4ab551 chore(main): release 1.214.0 (#2669)
* chore(main): release 1.214.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-11-22 10:32:07 +01:00
Ruben Fiszel
6377605821 fix client 2023-11-22 10:28:17 +01:00
Ruben Fiszel
6797d4d114 improve client 2023-11-22 10:04:44 +01:00
Stephan Fitzpatrick
37ffdaed60 feat(python): Refactor Windmill Python client (remove windmill-api) (#2665)
* "feat(python): Refactor Windmill Python client for better encapsulation and maintainability"

This PR obviates the need for the `windmill-api` library. I believe this makes the client package is easier to understand, debug, build, (and test) without it.

Additional updates were made to improve logging and add more robust error handling.

Here's what Jetbrains' AI assistant came up with to describe the changes based on the diff--I think it did a decent job:

> Simplified the Windmill Python client by refactoring out repeated code into more compact, reusable methods. Transitioned the client functions into a Windmill client class, enabling a better encapsulation of the client's state. Updated the README example to reflect this change. This improves code maintainability by making the code easier to understand and update, and improves user experience by providing a more intuitive client interface.

* "Refactor post method in Windmill Python client"

Removed the hard-coded param 'refresh_client' from the post method in wmill/client.py. since it's no longer used.

* "Update build script for Python client"

Updated the build script for the Python client for the backend to now include scaffolding code for generating the OpenAPI client, making changes to the generated client, and building the client. .

* "Add raise_for_status option in http methods"

Enhanced 'get' and 'post' methods in the client class to include a new optional parameter 'raise_for_status'. This allows for better error handling by raising exceptions for 4XX and 5XX responses, if requested. This way, non critical API calls can continue execution even if they receive a client or server error.

* "Removed refresh_client condition in post method"

* Replace `create_job` with `start_execution` in wmill client

This commit changes the method `create_job` to `start_execution` in the wmill client. The change was made to better reflect the function's purpose and make the code even more self-explanatory. Additionally, references to this changed method in README.md and various portions of client.py are also updated. The change will enhance readability and make it easier for newcomers to understand the code.

* Fix type hints and default arguments in wmill client

This commit adjusts typing hints for several methods from Dict[str, Any] to Any in wmill client since the result of a script isn't always a dictionary. Null arguments are also adjusted from {} to None, ensuring better Python standard practices and less unpredicted behaviors. These changes are aimed to enhance maintainability and make the functions more resilient.

* Update client.py

* Update pyproject.toml

* Update client.py

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-11-22 09:49:50 +01:00
Ruben Fiszel
0317c26d5b fix script drawer editor extra config 2023-11-22 00:25:46 +01:00
Ruben Fiszel
3ebe6d7a62 fix: fix flow primary schedule clearing 2023-11-21 17:58:26 +01:00
HugoCasa
ad199afd06 feat: hub path scripts + nested inputs glue (#2668) 2023-11-21 17:16:19 +01:00
Ruben Fiszel
60d2f79677 chore(main): release 1.213.0 (#2658)
* chore(main): release 1.213.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-11-21 15:50:42 +01:00
Ruben Fiszel
26fdf20f49 fix warning flow changed 2023-11-21 15:46:41 +01:00
Ruben Fiszel
07dae13f44 fix redirection upon path change for apps 2023-11-21 15:36:05 +01:00
Ruben Fiszel
c1bb97d990 fix: fix error handling for list of errors 2023-11-21 15:24:38 +01:00
Ruben Fiszel
62a3d79266 fix evalClass crash 2023-11-21 14:37:23 +01:00
Ruben Fiszel
6dd91adb13 add more logs around completed job 2023-11-21 14:25:48 +01:00
Ruben Fiszel
51ce2f8cb3 fix: fix embedded approval step timeouts 2023-11-21 13:51:55 +01:00
Ruben Fiszel
89f42c0a51 fix sqlx 2023-11-21 09:49:26 +01:00
HugoCasa
ded0bb890b fix: set session code completion to enabled by default (#2664) 2023-11-20 22:53:16 +01:00
Jacob Mastel
f8a1bd8d69 Add official Atlassian Repo Mapping (#2661)
The official Atlassian python library is named atlassian-python-api,
which requires an installation under that name. It is imported as
'atlassian' though.
2023-11-20 21:04:44 +01:00
Guillaume Bouvignies
baac93f401 feat: Expanding an s3object result now opens the S3 file browser (#2656)
* feat: Expanding an s3object result now opens the S3 file browser

* Add s3 explorer link

* Revert changes in ArgInfo
2023-11-20 17:07:52 +01:00
HugoCasa
de1e1f545d fix: ask to return value (#2659) 2023-11-20 16:29:42 +01:00
HugoCasa
6d426b4ec4 feat: code completion UI + other nits (#2657)
* fix: nits

* feat: add code completion in UI
2023-11-20 16:24:33 +01:00
Ruben Fiszel
cd8919d46f chore(main): release 1.212.0 (#2653)
* chore(main): release 1.212.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-11-20 14:20:34 +01:00
HugoCasa
8ea98c2c8d feat: upgrade to gpt-4-turbo (#2655) 2023-11-20 12:54:22 +01:00
Ruben Fiszel
c2598b3304 feat: test an iteration 2023-11-20 12:26:24 +01:00
Ruben Fiszel
6000c5dbc0 use userStore email for Usermenu 2023-11-20 10:53:32 +01:00
Ruben Fiszel
d31559c56b improve auto-invites 2023-11-20 10:35:38 +01:00
Guillaume Bouvignies
624b4d0e98 feat: S3 file picker as a drawer (#2640)
* feat: Pick a S3 file drawer

* Add endpoint to generate simple file preview

* FE prototype for file preview

* More info on the preview

* Small fix

* Fix BE compile

* fix BE compile

* remove weird vite.config.js file

* Fix fronte NodeJS.Timer -> NodeJS.Timeout and rename dataset to file for S3

* Add EE restrictions and polish FE

* Fix merge conflicst

* replace dataset with s3 object

* Update wording

* BE throws an error when bucket too large in CE

* regenerate Cargo.lock

* Fix typo in error message
2023-11-20 10:26:36 +01:00
Ruben Fiszel
895fedc8cb agent v0.5 2023-11-20 10:10:10 +01:00
Ruben Fiszel
1acf78e782 chore(main): release 1.211.0 (#2651)
* chore(main): release 1.211.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-11-20 02:09:45 +01:00
Stephan Fitzpatrick
c7a30f7c9d feat(python): Add functionality and resiliency to wmill python client (#2650)
* Refine wmill client.py `run_script_sync` and `run_script_by_path_sync` with more features

This commit enriches the functionality of the `run_script_sync` and `run_script_by_path_sync` functions. New features introduced include script cancellation upon exit, logging capabilities,
and script execution timeout. These enhancements improve script execution control and provide better debug information. The `get_result` function was also adjusted to enable the control
of 'is not None' assertion on the job result. Consequently, user flexibility is enhanced, and the method can cater to cases where a `None` result is within the expected behavior.

* remove unnecessary local import and rename cancel_atexit to cleanup
2023-11-20 02:07:20 +01:00
Ruben Fiszel
aa6bf4027f fix base_internal_url 2023-11-20 02:03:41 +01:00
Ruben Fiszel
b667317d44 fix: token expiry is equal to timeout 2023-11-20 00:39:40 +01:00
Ruben Fiszel
cd260e7062 feat: agent mode v0 2023-11-20 00:36:31 +01:00
Ruben Fiszel
47ad8d6013 feat: agent mode v0 2023-11-20 00:32:17 +01:00
Guillaume Bouvignies
c90f7f167e fix: main broken tests (#2652) 2023-11-19 22:41:07 +01:00
Ruben Fiszel
4882d94dfe fix: add refresh button to item picker 2023-11-19 18:26:22 +01:00
Ruben Fiszel
19907e4012 feat: ai regex 2023-11-19 14:45:25 +01:00
Ruben Fiszel
2f15ebc5f9 feat: custom error messages for forms 2023-11-19 13:54:32 +01:00
Ruben Fiszel
70f121035e fix: fix base64 assignment (file input) 2023-11-19 12:36:07 +01:00
Ruben Fiszel
c476543570 improve flow preview content 2023-11-19 11:51:33 +01:00
Ruben Fiszel
4023bca192 improve flow preview content 2023-11-19 11:46:48 +01:00
Ruben Fiszel
eef6432637 chore(main): release 1.210.1 (#2647)
* chore(main): release 1.210.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-11-18 17:40:36 +01:00
Ruben Fiszel
2692737418 fix: migrate old state env variable to new env variable 2023-11-18 17:32:41 +01:00
Ruben Fiszel
6ae1a69b75 fix: make wmill compatible with python 3.7 2023-11-18 17:15:05 +01:00
Ruben Fiszel
5a72ca9b24 fix: improve error message for unauthorized variables/resources 2023-11-18 16:27:09 +01:00
Ruben Fiszel
75021c444e fix svelte-check errors 2023-11-18 15:19:17 +01:00
Ruben Fiszel
ee243dedc6 fix: leave workspace + instance api 2023-11-18 15:07:06 +01:00
Ruben Fiszel
36379d6db0 fix: add toggle to invites 2023-11-18 13:46:51 +01:00
Ruben Fiszel
cee8206c42 restrict global accounts to 50 2023-11-18 13:31:30 +01:00
Ruben Fiszel
06905b252f restrict nb of groups to 3 and workspaces to 2 2023-11-18 13:28:29 +01:00
Ruben Fiszel
ab9adacfad improve auto-invite UX 2023-11-18 13:17:39 +01:00
HugoCasa
08c14e51c7 fix: flow copilot arg types (#2648) 2023-11-18 13:16:25 +01:00
Ruben Fiszel
c07e9056f1 fix: auto-invite all instead of by domain 2023-11-18 12:36:08 +01:00
Ruben Fiszel
837d3716d3 fix package-lock json 2023-11-18 11:13:50 +01:00
284 changed files with 25456 additions and 274427 deletions

View File

@@ -15,7 +15,7 @@ on:
jobs:
cargo_test:
runs-on: [self-hosted, new]
runs-on: ubicloud-standard-8
container:
image: ghcr.io/windmill-labs/backend-tests
services:
@@ -31,15 +31,16 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions-rust-lang/setup-rust-toolchain@v1
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
backend
backend -> target
# - uses: Swatinem/rust-cache@v2
# with:
# workspaces: |
# backend
# backend -> target
- name: cargo test
timeout-minutes: 10
timeout-minutes: 15
run:
mkdir frontend/build && cd backend && touch
windmill-api/openapi-deref.yaml &&
DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill DISABLE_EMBEDDING=true cargo
test --features enterprise --all -- --nocapture
DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill
DISABLE_EMBEDDING=true RUST_LOG=info cargo test --features enterprise --all --
--nocapture

View File

@@ -7,14 +7,13 @@ on:
jobs:
benchmark:
runs-on: [self-hosted, new]
runs-on: ubicloud-standard-8
services:
postgres:
image: postgres
env:
POSTGRES_DB: windmill
POSTGRES_PASSWORD: changeme
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
--health-retries 5
@@ -35,12 +34,12 @@ jobs:
image: ghcr.io/windmill-labs/windmill-ee:main
env:
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
DISABLE_SERVER: true
MODE: worker
WORKER_GROUP: dedicated
DEDICATED_WORKER: "admins:f/benchmarks/dedicated"
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
options: >-
--pull always
--pull always --restart unless-stopped
steps:
- uses: denoland/setup-deno@v1
with:
@@ -50,9 +49,11 @@ jobs:
ref: benchmarks
- name: benchmark
timeout-minutes: 20
run: deno run --unstable -A -r
run:
deno run --unstable -A -r
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
-c https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_config.json
-c
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_config.json
- name: Push changes
run: |
pwd

View File

@@ -6,7 +6,7 @@ on:
- "version.txt"
jobs:
change_version:
runs-on: ubuntu-latest
runs-on: ubicloud
container: node:18
steps:
- uses: actions/checkout@v3

View File

@@ -1,13 +0,0 @@
name: Clean docker
on:
schedule:
# * is a special character in YAML so you have to quote this string
- cron: "0 0 */2 * *"
jobs:
build:
runs-on: [self-hosted, new]
steps:
- name: clean docker
run: |
sudo docker system prune -f

View File

@@ -8,7 +8,7 @@ env:
jobs:
build_deno_and_push_to_repo:
runs-on: ubuntu-latest
runs-on: ubicloud
steps:
- uses: actions/checkout@v3
- name: generate_deno
@@ -31,7 +31,7 @@ jobs:
tag_repo:
needs: [build_deno_and_push_to_repo]
runs-on: ubuntu-latest
runs-on: ubicloud
steps:
- uses: actions/checkout@v3
with:

View File

@@ -16,7 +16,7 @@ permissions:
jobs:
build_ee:
runs-on: ubuntu-22.04
runs-on: ubicloud
steps:
- uses: actions/checkout@v3
with:

View File

@@ -20,7 +20,7 @@ permissions:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubicloud
steps:
- uses: actions/checkout@v3
with:
@@ -63,7 +63,7 @@ jobs:
org.opencontainers.image.licenses=AGPLv3
build_ee:
runs-on: ubuntu-latest
runs-on: ubicloud
steps:
- uses: actions/checkout@v3
with:
@@ -108,6 +108,52 @@ jobs:
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
build_ee_reports_privately:
needs: [build_ee]
runs-on: ubicloud
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
uses: docker/metadata-action@v4
with:
images: |
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}-ee-reports
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,enable=true,priority=100,prefix=,suffix=,format=short
- name: Login to ECR
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: ${{ env.ECR_REGISTRY }}
username: ${{ secrets.AWS_ACCESS_KEY_ID }}
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Build and push publicly ee
uses: depot/build-push-action@v1
with:
context: .
platforms: linux/amd64
push: true
file: "./docker/DockerfileReports"
tags: |
${{ steps.meta-ee-public.outputs.tags }}
labels: |
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
# disabled until we make it 100% reliable and add more meaningful tests
# playwright:
# runs-on: [self-hosted, new]
@@ -143,7 +189,7 @@ jobs:
deploy_s3:
needs: [build_ee]
runs-on: ubuntu-latest
runs-on: ubicloud
if: github.event_name != 'pull_request'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -165,10 +211,9 @@ jobs:
bucket: windmill-frontend
bucket-region: us-east-1
attach_amd64_binary_to_release:
needs: [build, build_ee]
runs-on: ubuntu-latest
runs-on: ubicloud
if: ${{ startsWith(github.ref, 'refs/tags/') }}
env:
ARCH: amd64
@@ -203,7 +248,6 @@ jobs:
mv "${{ steps.extract.outputs.destination }}/windmill" "${{ steps.extract.outputs.destination }}/windmill-${ARCH}"
mv "${{ steps.extract-ee.outputs.destination }}/windmill" "${{ steps.extract-ee.outputs.destination }}/windmill-ee-${ARCH}"
- name: Attach binary to release
uses: softprops/action-gh-release@v1
with:
@@ -213,7 +257,7 @@ jobs:
attach_arm64_binary_to_release:
needs: [build, build_ee]
runs-on: ubuntu-latest
runs-on: ubicoud
if: ${{ startsWith(github.ref, 'refs/tags/') }}
env:
ARCH: arm64
@@ -248,7 +292,6 @@ jobs:
mv "${{ steps.extract.outputs.destination }}/windmill" "${{ steps.extract.outputs.destination }}/windmill-${ARCH}"
mv "${{ steps.extract-ee.outputs.destination }}/windmill" "${{ steps.extract-ee.outputs.destination }}/windmill-ee-${ARCH}"
- name: Attach binary to release
uses: softprops/action-gh-release@v1
with:
@@ -258,7 +301,7 @@ jobs:
publish_ecr:
needs: [build_ee]
runs-on: ubuntu-latest
runs-on: ubicloud
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v3

View File

@@ -5,8 +5,6 @@ env:
name: Build windmill:mssql
on:
workflow_dispatch:
schedule:
- cron: "0 0 */4 * *"
concurrency:
group: ${{ github.ref }}-mssql

View File

@@ -1,14 +1,14 @@
name: check frontend build
on:
pull_request:
types: [opened,synchronize,reopened,closed]
types: [opened, synchronize, reopened, closed]
paths:
- "frontend/**"
merge_group:
jobs:
npm_check:
runs-on: ubuntu-latest
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
@@ -16,4 +16,6 @@ jobs:
node-version: 18
- name: "npm check"
timeout-minutes: 2
run: cd frontend && npm ci && npm run generate-backend-client && npm run check
run:
cd frontend && npm ci && npm run generate-backend-client && npm run
check

View File

@@ -10,7 +10,7 @@ env:
jobs:
build_go_and_push_to_repo:
runs-on: ubuntu-latest
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4

View File

@@ -6,7 +6,7 @@ on:
jobs:
build_npm:
runs-on: ubuntu-latest
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3

View File

@@ -16,14 +16,17 @@ permissions:
packages: write
jobs:
publish_lsp:
runs-on: ubuntu-latest
sleep:
runs-on: ubicloud
steps:
- name: Sleep for 900 seconds waiting for pypi to update index
if: startsWith(github.ref, 'refs/tags/v')
run: sleep 900
shell: bash
publish_lsp:
needs: [sleep]
runs-on: ubicloud
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -62,7 +65,7 @@ jobs:
publish_lsp_private:
needs: [publish_lsp]
runs-on: ubuntu-latest
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v3
with:

View File

@@ -7,7 +7,7 @@ on:
jobs:
publish_pypi:
runs-on: ubuntu-latest
runs-on: ubicloud-standard-8
if: startsWith(github.ref, 'refs/tags/v')
container:
image: ghcr.io/windmill-labs/python-client-builder

View File

@@ -6,7 +6,7 @@ name: release-please
jobs:
release-please:
name: "Release please"
runs-on: ubuntu-latest
runs-on: ubicloud-standard-8
steps:
- uses: GoogleCloudPlatform/release-please-action@v3
with:

View File

@@ -1,6 +1,200 @@
# Changelog
## [1.219.1](https://github.com/windmill-labs/windmill/compare/v1.219.0...v1.219.1) (2023-12-01)
### Bug Fixes
* fix editorContext in preview ([cb42f10](https://github.com/windmill-labs/windmill/commit/cb42f10dc605f00ba70fadc61069aba98af8ec88))
* maps mapRegion update on move ([62de305](https://github.com/windmill-labs/windmill/commit/62de305908ba7e6ac75aa5e8ffba822522b08345))
## [1.219.0](https://github.com/windmill-labs/windmill/compare/v1.218.0...v1.219.0) (2023-12-01)
### Features
* eval preview for apps ([d4374a0](https://github.com/windmill-labs/windmill/commit/d4374a0103d2244b31b5cec7649dbfb96b2af1b4))
### Bug Fixes
* **frontend:** rename menu component ([#2738](https://github.com/windmill-labs/windmill/issues/2738)) ([f816ad0](https://github.com/windmill-labs/windmill/commit/f816ad01d3e39917714abadcf3833a4d8619e4f3))
* improve quickstyle to be in static ccomponents only ([4774e03](https://github.com/windmill-labs/windmill/commit/4774e03be5663017bccbe95f026cc39d4c43f536))
* improve stat card ([fc5f054](https://github.com/windmill-labs/windmill/commit/fc5f054b94fa58ee889d10d06e486b6e8c4f885e))
* limit log pull from queued jobs ([592d783](https://github.com/windmill-labs/windmill/commit/592d7839d183843b425521a319b802fb8cac3f21))
* support INET in pg ([007d5be](https://github.com/windmill-labs/windmill/commit/007d5be23e038fbf82907c3b335b3d747791295d))
## [1.218.0](https://github.com/windmill-labs/windmill/compare/v1.217.0...v1.218.0) (2023-11-30)
### Features
* **frontend:** add menu component ([#2721](https://github.com/windmill-labs/windmill/issues/2721)) ([0665692](https://github.com/windmill-labs/windmill/commit/06656924ae8173e906a243aab2af658a1689af01))
* **frontend:** resource rework ([#2725](https://github.com/windmill-labs/windmill/issues/2725)) ([ec25856](https://github.com/windmill-labs/windmill/commit/ec25856b7c8a7d33a50d1beb5f3e99c7b912e1ca))
### Bug Fixes
* make REST language support URLSearchParams, headers, FormData ([830dec0](https://github.com/windmill-labs/windmill/commit/830dec0f90e0189b4dea4de8c44a03e437acf440))
* Pull patched version of gosyn ([#2734](https://github.com/windmill-labs/windmill/issues/2734)) ([0bf3685](https://github.com/windmill-labs/windmill/commit/0bf3685cbdecae0a8e7a24a5198ae2fed98fe340))
* s3 resource openapi spec ([#2730](https://github.com/windmill-labs/windmill/issues/2730)) ([16d10ae](https://github.com/windmill-labs/windmill/commit/16d10aedf79cddf7cdc15d55c42c72f4948b3ee2))
* update deployed even when draft only ([#2694](https://github.com/windmill-labs/windmill/issues/2694)) ([1f99fcd](https://github.com/windmill-labs/windmill/commit/1f99fcd3af21208676aa90015559359740b0534a))
* Using latest gosyn ([#2737](https://github.com/windmill-labs/windmill/issues/2737)) ([920cc9a](https://github.com/windmill-labs/windmill/commit/920cc9a576db0112ffd9572480c3d0a8aa08055b))
* Workspace error handler creation was not adding the slack resource ([#2733](https://github.com/windmill-labs/windmill/issues/2733)) ([70c504e](https://github.com/windmill-labs/windmill/commit/70c504edfaf7c114c91af2b3ca39dfee073f04f2))
## [1.217.0](https://github.com/windmill-labs/windmill/compare/v1.216.0...v1.217.0) (2023-11-29)
### Features
* add support for raw query args ([8275602](https://github.com/windmill-labs/windmill/commit/82756023728fe392fee93a7bba0567ce582aad8c))
* **frontend:** add "hide schedules" filter ([#2710](https://github.com/windmill-labs/windmill/issues/2710)) ([46e0f91](https://github.com/windmill-labs/windmill/commit/46e0f913878c9f688594cfe1c9184438e4facf49))
* **frontend:** Added tailwind classes auto-complete ([#2712](https://github.com/windmill-labs/windmill/issues/2712)) ([2d3ce8a](https://github.com/windmill-labs/windmill/commit/2d3ce8a49c952accb4f1ceb0e6ea608df539f179))
* **frontend:** Stat card improvement ([#2709](https://github.com/windmill-labs/windmill/issues/2709)) ([89abb68](https://github.com/windmill-labs/windmill/commit/89abb68f635d062778485c15d88fd6272ebd40a2))
* scheduled app reports ([#2714](https://github.com/windmill-labs/windmill/issues/2714)) ([3789b34](https://github.com/windmill-labs/windmill/commit/3789b34dae72ff57bb9e0bb7e93439446c78095d))
### Bug Fixes
* ai fix popup placement + update edit/fix prompt to return complete code ([#2715](https://github.com/windmill-labs/windmill/issues/2715)) ([76a387f](https://github.com/windmill-labs/windmill/commit/76a387f4a181d21afad0138c07f947aa292978cc))
* better error for moved openai resource ([#2724](https://github.com/windmill-labs/windmill/issues/2724)) ([15b2c9f](https://github.com/windmill-labs/windmill/commit/15b2c9f171122ab074f8430a785f8ed577921fcc))
* Deno can talk to private NPM registries behind HTTPS ([#2713](https://github.com/windmill-labs/windmill/issues/2713)) ([726866b](https://github.com/windmill-labs/windmill/commit/726866b410863ca9583a8145ebd9f4c4557cef08))
* Error handler now supports flows ([#2707](https://github.com/windmill-labs/windmill/issues/2707)) ([36e46e2](https://github.com/windmill-labs/windmill/commit/36e46e2e47e1e949e1e235fa4ec0365b46d80de1))
* **frontend:** fix separator z-index ([#2720](https://github.com/windmill-labs/windmill/issues/2720)) ([9525ab7](https://github.com/windmill-labs/windmill/commit/9525ab7bbab9f820d1f6fee44f9a2e9ad19d1b54))
* **frontend:** Fix table initial ordering ([#2727](https://github.com/windmill-labs/windmill/issues/2727)) ([1a1d1db](https://github.com/windmill-labs/windmill/commit/1a1d1db96fd069434291f4b5f37ae1d0e21c5e44))
* **frontend:** Improve error message + fix overflow when file name is too long ([#2691](https://github.com/windmill-labs/windmill/issues/2691)) ([c990f85](https://github.com/windmill-labs/windmill/commit/c990f856aaf821899cc54db20a653af396ceae7f))
* generate cargo lock file ([#2722](https://github.com/windmill-labs/windmill/issues/2722)) ([bd31979](https://github.com/windmill-labs/windmill/commit/bd31979a62a30071e68385b5eb5dc28f37ea8fb5))
* improve autocomplete reactivity ([c3eaf0b](https://github.com/windmill-labs/windmill/commit/c3eaf0bf4aab531b52a1b3ac276c6840b0925786))
* make dedicated workers able to redeploy automatically ([e6d67f4](https://github.com/windmill-labs/windmill/commit/e6d67f4e5994360e7ae83c1303b31514b6062d1d))
* minor fixes to private NPM and python registries to get everything working ([#2728](https://github.com/windmill-labs/windmill/issues/2728)) ([3d6fb15](https://github.com/windmill-labs/windmill/commit/3d6fb15a90f4d5aa18a6d6158760a380295e3d9e))
* only list session and permanent token in user settings ([c8046af](https://github.com/windmill-labs/windmill/commit/c8046af9d01ade6891ea97e56974bb742bbf3e6e))
* trim .bun.ts for local imports ([d2b3026](https://github.com/windmill-labs/windmill/commit/d2b3026032d288e1e5de1f37979d92d02094b3e4))
## [1.216.0](https://github.com/windmill-labs/windmill/compare/v1.215.0...v1.216.0) (2023-11-26)
### Features
* add early return for flows ([dbaef0a](https://github.com/windmill-labs/windmill/commit/dbaef0aa5f22c55b691f6205ae053c155a0e025c))
* fill pg resource from string ([#2699](https://github.com/windmill-labs/windmill/issues/2699)) ([f1cabb4](https://github.com/windmill-labs/windmill/commit/f1cabb40f6cbec05b105563dad58da048d9187ec))
* **frontend:** add currency format + add enum autocomplete + fix run… ([#2670](https://github.com/windmill-labs/windmill/issues/2670)) ([0c0f43d](https://github.com/windmill-labs/windmill/commit/0c0f43dd3ac0541d889046c0c6df8dbf78650b9c))
* **frontend:** add stat card ([#2687](https://github.com/windmill-labs/windmill/issues/2687)) ([81ef24b](https://github.com/windmill-labs/windmill/commit/81ef24b3cec18f86e407b343b4d72ac566d268e1))
* **python:** Update return type for 'get_resource' function ([#2695](https://github.com/windmill-labs/windmill/issues/2695)) ([603e7ff](https://github.com/windmill-labs/windmill/commit/603e7ff67f5f68d291db37173b89325d5de911a1))
### Bug Fixes
* expand enum type narrowing to forms ([94e9b80](https://github.com/windmill-labs/windmill/commit/94e9b80e1993a217415118bf48b8575cd8363746))
* **frontend:** correctly handle when result is undefined ([#2693](https://github.com/windmill-labs/windmill/issues/2693)) ([f4aa768](https://github.com/windmill-labs/windmill/commit/f4aa76897ea0f261c1e47976f517058601c479f9))
* improve multiselect from form ([5fa653d](https://github.com/windmill-labs/windmill/commit/5fa653d154c6e8697119796a025bdc380f68de9a))
* improve resource pages ([16be230](https://github.com/windmill-labs/windmill/commit/16be2300ff66cad8b740856a6435d1c1e53bce99))
* lighten monaco editor workers ([#2690](https://github.com/windmill-labs/windmill/issues/2690)) ([4c42836](https://github.com/windmill-labs/windmill/commit/4c42836cfcad63da023a658213f03b45c925efb2))
* Priority tags FE buggy when missing from config ([#2702](https://github.com/windmill-labs/windmill/issues/2702)) ([e8b1f22](https://github.com/windmill-labs/windmill/commit/e8b1f220dd299922a32b04451756f2b7fa735d5d))
* S3 bucket browser small improvements and fixes ([#2700](https://github.com/windmill-labs/windmill/issues/2700)) ([55e34d8](https://github.com/windmill-labs/windmill/commit/55e34d8cdd64362615f2c1bc2698c0538c287784))
## [1.215.0](https://github.com/windmill-labs/windmill/compare/v1.214.1...v1.215.0) (2023-11-23)
### Features
* query embeddings from s3 ([#2683](https://github.com/windmill-labs/windmill/issues/2683)) ([e3f2b43](https://github.com/windmill-labs/windmill/commit/e3f2b43748e8e0853dd0dbae87cba37b950dc76e))
### Bug Fixes
* bigquery schema and date inputs ([#2688](https://github.com/windmill-labs/windmill/issues/2688)) ([f5e098d](https://github.com/windmill-labs/windmill/commit/f5e098d03b467002a8deab4d2c519945c01458d4))
* improve wmill go client ([cfd3da4](https://github.com/windmill-labs/windmill/commit/cfd3da41efe4e89e5d8672c08433442b05d11f37))
## [1.214.1](https://github.com/windmill-labs/windmill/compare/v1.214.0...v1.214.1) (2023-11-23)
### Features
* **python:** Quality of Life improvements to Python client ([#2686](https://github.com/windmill-labs/windmill/issues/2686)) ([1c2abcd](https://github.com/windmill-labs/windmill/commit/1c2abcda231b2d10e7a358a4e4b7973785cb6199))
### Bug Fixes
* add image base64 source kinds ([7ae84fc](https://github.com/windmill-labs/windmill/commit/7ae84fce5014d97bf17803e2ccefafee009b28bb))
* fix backend build ([45ee1d7](https://github.com/windmill-labs/windmill/commit/45ee1d770345f03c03cc48a3ddcc130ca3655a4c))
* **frontend:** disable active interaction to avoid broken state ([#2675](https://github.com/windmill-labs/windmill/issues/2675)) ([bfdb559](https://github.com/windmill-labs/windmill/commit/bfdb559b47786e37f135ca345c55828cc70e49e9))
* **frontend:** improve tutorial ux ([#2677](https://github.com/windmill-labs/windmill/issues/2677)) ([7fe3bca](https://github.com/windmill-labs/windmill/commit/7fe3bca624fa3b434e3fbe9b33b213449044accc))
* **frontend:** use popover for schedule for later on the runs page ([#2678](https://github.com/windmill-labs/windmill/issues/2678)) ([31fbc58](https://github.com/windmill-labs/windmill/commit/31fbc5867cc72befea5733811148126289ce8bb9))
* graphql variables ([#2682](https://github.com/windmill-labs/windmill/issues/2682)) ([217e694](https://github.com/windmill-labs/windmill/commit/217e69498fa5958e83c595003b2a086528e51388))
* relax tags constraints ([8f8ea22](https://github.com/windmill-labs/windmill/commit/8f8ea227c850f9d01f913660547b6a6394c6e142))
* Various fixes and improvements for Windmill S3 capabilities ([#2674](https://github.com/windmill-labs/windmill/issues/2674)) ([c42c54e](https://github.com/windmill-labs/windmill/commit/c42c54e69b5acdcbe44a0c26ae7fbaa2189e6b5f))
## [1.214.0](https://github.com/windmill-labs/windmill/compare/v1.213.0...v1.214.0) (2023-11-22)
### Features
* hub path scripts + nested inputs glue ([#2668](https://github.com/windmill-labs/windmill/issues/2668)) ([ad199af](https://github.com/windmill-labs/windmill/commit/ad199afd06814540bb7d36669709902be56eeb8a))
* **python:** Refactor Windmill Python client (remove `windmill-api`) ([#2665](https://github.com/windmill-labs/windmill/issues/2665)) ([37ffdae](https://github.com/windmill-labs/windmill/commit/37ffdaed60fb750f1466b440353e1d8409eaea90))
### Bug Fixes
* fix flow primary schedule clearing ([3ebe6d7](https://github.com/windmill-labs/windmill/commit/3ebe6d7a620e37fd6c81bcf4c8713a48eb081f81))
## [1.213.0](https://github.com/windmill-labs/windmill/compare/v1.212.0...v1.213.0) (2023-11-21)
### Features
* code completion UI + other nits ([#2657](https://github.com/windmill-labs/windmill/issues/2657)) ([6d426b4](https://github.com/windmill-labs/windmill/commit/6d426b4ec49d4749a89c86dc5eb1f11bf705ca26))
* Expanding an s3object result now opens the S3 file browser ([#2656](https://github.com/windmill-labs/windmill/issues/2656)) ([baac93f](https://github.com/windmill-labs/windmill/commit/baac93f40140ee37548a273885c028a8e6500b6d))
### Bug Fixes
* ask to return value ([#2659](https://github.com/windmill-labs/windmill/issues/2659)) ([de1e1f5](https://github.com/windmill-labs/windmill/commit/de1e1f545d4cd42f46d9af9a0349af86acf1901c))
* fix embedded approval step timeouts ([51ce2f8](https://github.com/windmill-labs/windmill/commit/51ce2f8cb308da285dab0dc433bf596caa5eeed0))
* fix error handling for list of errors ([c1bb97d](https://github.com/windmill-labs/windmill/commit/c1bb97d990810c9b3d909c9a045f0ce84be6c25e))
* set session code completion to enabled by default ([#2664](https://github.com/windmill-labs/windmill/issues/2664)) ([ded0bb8](https://github.com/windmill-labs/windmill/commit/ded0bb890bb54ef80c857395474e692865ee4717))
## [1.212.0](https://github.com/windmill-labs/windmill/compare/v1.211.0...v1.212.0) (2023-11-20)
### Features
* S3 file picker as a drawer ([#2640](https://github.com/windmill-labs/windmill/issues/2640)) ([624b4d0](https://github.com/windmill-labs/windmill/commit/624b4d0e9898dddcce3cb2ce989ce1f9e4736061))
* test an iteration ([c2598b3](https://github.com/windmill-labs/windmill/commit/c2598b330450f9885f7d10e2b5baa54d6ef88cc5))
* upgrade to gpt-4-turbo ([#2655](https://github.com/windmill-labs/windmill/issues/2655)) ([8ea98c2](https://github.com/windmill-labs/windmill/commit/8ea98c2c8d636209954a267116eb03ab13217ef8))
## [1.211.0](https://github.com/windmill-labs/windmill/compare/v1.210.1...v1.211.0) (2023-11-20)
### Features
* agent mode v0 ([cd260e7](https://github.com/windmill-labs/windmill/commit/cd260e7062802ab39115025577a2456c66435899))
* agent mode v0 ([47ad8d6](https://github.com/windmill-labs/windmill/commit/47ad8d6013e7144a5d018f43c827b171228b5f42))
* ai regex ([19907e4](https://github.com/windmill-labs/windmill/commit/19907e4012e88a8fd28a5f0564a7ea48ec12020c))
* custom error messages for forms ([2f15ebc](https://github.com/windmill-labs/windmill/commit/2f15ebc5f9628b0d26ec08b3527ddc96c6d42ba0))
* **python:** Add functionality and resiliency to wmill python client ([#2650](https://github.com/windmill-labs/windmill/issues/2650)) ([c7a30f7](https://github.com/windmill-labs/windmill/commit/c7a30f7c9db26252f0ee69e1276ddccc0d52acb3))
### Bug Fixes
* add refresh button to item picker ([4882d94](https://github.com/windmill-labs/windmill/commit/4882d94dfe18c156662fe483e1dd4f5d3e3be3af))
* fix base64 assignment (file input) ([70f1210](https://github.com/windmill-labs/windmill/commit/70f121035edd94b3940f530a5603b1ff4bf03839))
* main broken tests ([#2652](https://github.com/windmill-labs/windmill/issues/2652)) ([c90f7f1](https://github.com/windmill-labs/windmill/commit/c90f7f167e0b9c20008a62eeeef7819e24fc3da9))
* token expiry is equal to timeout ([b667317](https://github.com/windmill-labs/windmill/commit/b667317d44f37bb50f24e356a1a3d231ebe8b4b4))
## [1.210.1](https://github.com/windmill-labs/windmill/compare/v1.210.0...v1.210.1) (2023-11-18)
### Bug Fixes
* add toggle to invites ([36379d6](https://github.com/windmill-labs/windmill/commit/36379d6db05de170e0237b12e767b6d6f4a6f2ef))
* auto-invite all instead of by domain ([c07e905](https://github.com/windmill-labs/windmill/commit/c07e9056f1872cae70b8c3bebdbbf47daeee7ac5))
* flow copilot arg types ([#2648](https://github.com/windmill-labs/windmill/issues/2648)) ([08c14e5](https://github.com/windmill-labs/windmill/commit/08c14e51c792fe65d4f993379eff0e5c8a75215b))
* improve error message for unauthorized variables/resources ([5a72ca9](https://github.com/windmill-labs/windmill/commit/5a72ca9b24c5c4e9fe94c7865b9145283aceff53))
* leave workspace + instance api ([ee243de](https://github.com/windmill-labs/windmill/commit/ee243dedc6df28a64f15e0b274b7fa96f6428474))
* make wmill compatible with python 3.7 ([6ae1a69](https://github.com/windmill-labs/windmill/commit/6ae1a69b75fe039586956064880c274a21fc5970))
* migrate old state env variable to new env variable ([2692737](https://github.com/windmill-labs/windmill/commit/2692737418ed601c0a36c368f59ffb8d10d9ad38))
## [1.210.0](https://github.com/windmill-labs/windmill/compare/v1.209.0...v1.210.0) (2023-11-17)

View File

@@ -1,4 +1,8 @@
FROM debian:bookworm-slim as nsjail
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:slim-bookworm
ARG PYTHON_IMAGE=python:3.11.4-slim-bookworm
FROM ${DEBIAN_IMAGE} as nsjail
WORKDIR /nsjail
@@ -22,7 +26,7 @@ RUN if [ "$nsjail" = "true" ]; then git clone -b master --single-branch https://
&& git checkout dccf911fd2659e7b08ce9507c25b2b38ec2c5800; fi
RUN if [ "$nsjail" = "true" ]; then make; else touch nsjail; fi
FROM rust:slim-bookworm AS rust_base
FROM ${RUST_IMAGE} AS rust_base
RUN apt-get update && apt-get install -y git libssl-dev pkg-config npm
@@ -87,7 +91,7 @@ COPY .git/ .git/
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
FROM debian:bookworm-slim as downloader
FROM ${DEBIAN_IMAGE} as downloader
ARG TARGETPLATFORM
@@ -101,29 +105,44 @@ RUN [ "$TARGETPLATFORM" == "linux/amd64" ] && curl -Lsf https://github.com/denol
RUN unzip deno.zip && rm deno.zip
FROM python:3.11.4-slim-bookworm
FROM ${PYTHON_IMAGE}
ARG TARGETPLATFORM
ARG POWERSHELL_VERSION=7.3.5
ARG POWERSHELL_DEB_VERSION=7.3.5-1
ARG RCLONE_VERSION=1.60.1
ARG KUBECTL_VERSION=1.27.2
ARG HELM_VERSION=3.12.0
ARG APP=/usr/src/app
ARG WITH_POWERSHELL=true
ARG WITH_RCLONE=true
ARG WITH_KUBECTL=true
ARG WITH_HELM=true
RUN apt-get update \
&& apt-get install -y ca-certificates wget curl git jq libprotobuf-dev libnl-route-3-dev unzip build-essential unixodbc xmlsec1 \
&& rm -rf /var/lib/apt/lists/*
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then apt-get update -y && apt install libicu-dev -y && wget -O 'pwsh.deb' 'https://github.com/PowerShell/PowerShell/releases/download/v7.3.5/powershell_7.3.5-1.deb_amd64.deb' && \
dpkg --install 'pwsh.deb' && \
rm 'pwsh.deb'; else echo 'pwshell not on amd64'; fi
RUN if [ "$WITH_POWERSHELL" = "true" ]; then \
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then apt-get update -y && apt install libicu-dev -y && wget -O 'pwsh.deb' "https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell_${POWERSHELL_DEB_VERSION}.deb_amd64.deb" && \
dpkg --install 'pwsh.deb' && \
rm 'pwsh.deb'; else echo 'pwshell not on amd64'; fi; \
else echo 'Building the image without powershell'; fi
RUN arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
wget https://get.helm.sh/helm-v3.12.0-linux-$arch.tar.gz && \
tar -zxvf helm-v3.12.0-linux-$arch.tar.gz && \
mv linux-$arch/helm /usr/local/bin/helm &&\
chmod +x /usr/local/bin/helm
RUN if [ "$WITH_HELM" = "true" ]; then \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
wget "https://get.helm.sh/helm-v${HELM_VERSION}-linux-$arch.tar.gz" && \
tar -zxvf "helm-v${HELM_VERSION}-linux-$arch.tar.gz" && \
mv linux-$arch/helm /usr/local/bin/helm &&\
chmod +x /usr/local/bin/helm; \
else echo 'Building the image without helm'; fi
RUN arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
curl -LO "https://dl.k8s.io/release/v1.27.2/bin/linux/$arch/kubectl" && \
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
RUN if [ "$WITH_KUBECTL" = "true" ]; then \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
curl -LO "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/$arch/kubectl" && \
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl; \
else echo 'Building the image without kubectl'; fi
RUN set -eux; \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
@@ -140,10 +159,13 @@ RUN set -eux; \
unzip awscliv2.zip && \
./aws/install && rm awscliv2.zip
RUN arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
curl -o rclone.zip "https://downloads.rclone.org/v1.60.1/rclone-v1.60.1-linux-$arch.zip"; \
unzip -p rclone.zip rclone-v1.60.1-linux-$arch/rclone > /usr/bin/rclone; rm rclone.zip; \
chown root:root /usr/bin/rclone; chmod 755 /usr/bin/rclone
RUN if [ "$WITH_RCLONE" = "true" ]; then \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
curl -o rclone.zip "https://downloads.rclone.org/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-$arch.zip"; \
unzip -p rclone.zip rclone-v${RCLONE_VERSION}-linux-$arch/rclone > /usr/bin/rclone; rm rclone.zip; \
chown root:root /usr/bin/rclone; chmod 755 /usr/bin/rclone; \
else echo 'Building the image without rclone'; fi
RUN set -eux; \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \

View File

@@ -1,14 +1,11 @@
<p align="center">
<a href="https://www.windmill.dev/"><img src="./imgs/windmill-banner.png" alt="windmill.dev"></a>
</p>
<p align="center">
<em>.</em>
</p>
<p align=center>
Open-source developer infrastructure for internal tools. 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 UIsm and custom UIs to trigger workflows and scripts as internal apps.
<p align=center>
Scripts are turned into UIs and no-code modules, no-code modules can be composed into very rich flows, and script and flows can be triggered from internal UIs made with a low-code builder. The script languages supported are: Python, TypeScript, Go, Bash, SQL. Scripts can be generated by an AI assistant powered by OpenAI.
Scripts are turned into UIs and no-code modules, no-code modules can be composed into rich flows, and script and flows can be triggered from internal UIs made with a low-code builder. Supported script languages supported are: Python, TypeScript, Go, Bash, SQL.
</p>
<p align="center">
@@ -36,7 +33,7 @@ Scripts are turned into UIs and no-code modules, no-code modules can be composed
<a href="https://app.windmill.dev">Try it</a> - <a href="https://www.windmill.dev/docs/intro/">Docs</a> - <a href="https://discord.gg/V7PM2YHsPB">Discord</a> - <a href="https://hub.windmill.dev">Hub</a> - <a href="https://www.windmill.dev/docs/misc/contributing">Contributor's guide</a>
</p>
# Windmill - Turn scripts into workflows and UIs that you can share and run at scale
# Windmill - Developer platform for APIs, background jobs, workflows and UIs
Windmill is <b>fully open-sourced (AGPLv3)</b> and Windmill Labs offers
dedicated instance and commercial support and licenses.
@@ -45,12 +42,13 @@ dedicated instance and commercial support and licenses.
https://github.com/windmill-labs/windmill/assets/122811744/0b132cd1-ee67-4505-822f-0c7ee7104252
- [Windmill - Turn scripts into workflows and UIs that you can share and run at scale](#windmill---turn-scripts-into-workflows-and-uis-that-you-can-share-and-run-at-scale)
- [Windmill - Developer platform for APIs, background jobs, workflows and UIs](#windmill---developer-platform-for-apis-background-jobs-workflows-and-uis)
- [Main Concepts](#main-concepts)
- [Show me some actual script code](#show-me-some-actual-script-code)
- [CLI](#cli)
- [Running scripts locally](#running-scripts-locally)
- [Stack](#stack)
- [Fastest Self-Hostable Workflow Engine](#fastest-self-hostable-workflow-engine)
- [Security](#security)
- [Sandboxing](#sandboxing)
- [Secrets, credentials and sensitive values](#secrets-credentials-and-sensitive-values)
@@ -59,12 +57,10 @@ https://github.com/windmill-labs/windmill/assets/122811744/0b132cd1-ee67-4505-82
- [How to self-host](#how-to-self-host)
- [Docker compose](#docker-compose)
- [Kubernetes (k8s) and Helm charts](#kubernetes-k8s-and-helm-charts)
- [Postgres without superuser](#postgres-without-superuser)
- [Run from binaries](#run-from-binaries)
- [OAuth, SSO \& SMTP](#oauth-sso--smtp)
- [Commercial license](#commercial-license)
- [OAuth for self-hosting](#oauth-for-self-hosting)
- [smtp for self-hosting](#smtp-for-self-hosting)
- [Resource types](#resource-types)
- [Manually fetch latest Windmill binary](#manually-fetch-latest-windmill-binary)
- [Integrations](#integrations)
- [Environment Variables](#environment-variables)
- [Run a local dev setup](#run-a-local-dev-setup)
- [only Frontend](#only-frontend)
@@ -75,22 +71,28 @@ https://github.com/windmill-labs/windmill/assets/122811744/0b132cd1-ee67-4505-82
## Main Concepts
1. Define a minimal and generic script in Python, TypeScript, Go or Bash that
solves a specific task. Here sending a POST request. The code can be defined
in the provided Web IDE or synchronized with your own github repo:
solves a specific task. The code can be defined
in the [provided Web IDE](https://www.windmill.dev/docs/code_editor) or [synchronized with your own GitHub repo](https://www.windmill.dev/docs/advanced/cli/sync) (e.g. through [VS Code](https://www.windmill.dev/docs/cli_local_dev/vscode-extension) extension):
![Step 1](./imgs/windmill-editor.png)
2. Your scripts parameters are automatically parsed and generate a frontend.
![Step 2](./imgs/windmill-run.png) ![Step 3](./imgs/windmill-result.png)
2. Your scripts parameters are automatically parsed and [generate a frontend](https://www.windmill.dev/docs/core_concepts/auto_generated_uis).
3. Make it flow! You can chain your scripts or scripts made by the community
![Step 2](./imgs/windmill-run.png)
![Step 3](./imgs/windmill-result.png)
3. Make it [flow](https://www.windmill.dev/docs/flows/flow_editor)! You can chain your scripts or scripts made by the community
shared on [WindmillHub](https://hub.windmill.dev).
![Step 3](./imgs/windmill-flow.png)
4. Build complex UIs on top of your scripts and flows.
4. Build [complex UIs](https://www.windmill.dev/docs/apps/app_editor) on top of your scripts and flows.
![Step 4](./imgs/windmill-builder.png)
Scripts and flows can also be triggered by a cron schedule '_/5 _ \* \* \*' or
through webhooks.
Scripts and flows can also be triggered by a [cron schedule](https://www.windmill.dev/docs/core_concepts/scheduling) (e.g. '_/5 _ \* \* \*') or
through [webhooks](https://www.windmill.dev/docs/core_concepts/webhooks).
You can build your entire infra on top of Windmill!
@@ -146,7 +148,9 @@ instance from local commands. See
You can run your script locally easily, you simply need to pass the right
environment variables for the `wmill` client library to fetch resources and
variables from your instance if necessary. See more:
<https://www.windmill.dev/docs/advanced/local_development/>.
<https://www.windmill.dev/docs/advanced/local_development>.
To develop & test locally scripts & flows, we recommend using the Windmill VS Code extension: <https://www.windmill.dev/docs/cli_local_dev/vscode-extension>.
## Stack
@@ -167,6 +171,14 @@ variables from your instance if necessary. See more:
- python runtime is python3
- golang runtime is 1.19.1
## Fastest Self-Hostable Workflow Engine
We have compared Windmill to other self-hostable workflow engines (Airflow, Prefect & Temporal) and Windmill is the most performant solution for both benchmarks: one flow composed of 40 lightweight tasks & one flow composed of 10 long-running tasks.
All methodology & results on our [Benchmarks](https://www.windmill.dev/docs/misc/benchmarks/competitors#airflow-setup) page.
![Fastest workflow engine](./imgs/fastest.png)
## Security
### Sandboxing
@@ -204,10 +216,14 @@ back to the database is ~50ms. A typical lightweight deno job will take around
We only provide docker-compose setup here. For more advanced setups, like
compiling from source or using without a postgres super user, see
[documentation](https://www.windmill.dev/docs/advanced/self_host).
[Self-Host documentation](https://www.windmill.dev/docs/advanced/self_host).
### Docker compose
Windmill can be deployed using 3 files: ([docker-compose.yml](./docker-compose.yml), [Caddyfile](./Caddyfile) and a [.env](./.env)) in a single command.
Make sure Docker is started, and run:
```
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/Caddyfile -o Caddyfile
@@ -222,6 +238,8 @@ The default super-admin user is: admin@windmill.dev / changeme.
From there, you can follow the setup app and create other users.
More details in [Self-Host Documention](https://www.windmill.dev/docs/advanced/self_host#docker).
### Kubernetes (k8s) and Helm charts
We publish helm charts at:
@@ -247,14 +265,16 @@ Workspace SSO, Microsoft/Azure and Okta) directly from the UI in the superadmin
settings. Do note that there is a limit of 50 SSO users on the community
edition.
[See documentation](https://www.windmill.dev/docs/misc/setup_oauth).
### Commercial license
To self-host Windmill, you must respect the terms of the AGPLv3 license which
To self-host Windmill, you must respect the terms of the [AGPLv3 license](https://www.gnu.org/licenses/agpl-3.0.en.html) which
you do not need to worry about for personal uses. For business uses, you should
be fine if you do not re-expose it in any way Windmill to your users and are
be fine if you do not re-expose Windmill in any way to your users and are
comfortable with AGPLv3.
To re-expose any Windmill parts to your users as a feature of your product, or
To [re-expose any Windmill parts to your users](https://www.windmill.dev/docs/misc/white_labelling) as a feature of your product, or
to build a feature on top of Windmill, to comply with AGPLv3 your product must
be AGPLv3 or you must get a commercial license. Contact us at
<ruben@windmill.dev> if you have any doubts.
@@ -264,9 +284,11 @@ your current infrastructure to Windmill, support with tight SLA, and our global
cache sync for high-performance/no dependency cache miss of cluster from 10+
nodes to 200+ nodes.
### Resource types
### Integrations
You will also want to import all the approved resource types from
In Windmill, integrations are referred to as [resources and resource types](https://www.windmill.dev/docs/core_concepts/resources_and_types). Each Resource has a Resource Type that defines the schema that the resource needs to implement.
On self-hosted instances, you might want to import all the approved resource types from
[WindmillHub](https://hub.windmill.dev). A setup script will prompt you to have
it being synced automatically everyday.
@@ -341,11 +363,11 @@ it being synced automatically everyday.
| SMTP_PASSWORD | None | password for the smtp server to send invite emails | Server |
| SMTP_TLS_IMPLICIT | false | https://docs.rs/mail-send/latest/mail_send/struct.SmtpClientBuilder.html#method.implicit_tlsemails | Server |
| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmin can create workspaces | Server |
| GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE | None | Path to a script to run when a root job fails. The script will be run in and from the admins workspace | Server |
| GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE | None | Path to a script or flow to run when a root job fails. The path needs to be prefixed with either `script/` or `flow/` to indicate the kind of error handler being used (assuming `script/` by default). The error handler will be run in and from the admins workspace | Server |
| WHITELIST_ENVS | None | List of envs variables, separated by a ',' that are whitelisted as being safe to passthrough the workers | Worker |
| SAML_METADATA | None | SAML Metadata URL to enable SAML SSO (EE only) | Server |
| SECRET_SALT | None | Secret Salt used for encryption and decryption of secrets. If defined, the secrets will not be decryptable unless the right salt is passed in, which is the case for the workers and the server | Server + Worker |
| OPENAI_AZURE_BASE_PATH | None | Azure OpenAI API base path (no trailing slash) | Server |
| OPENAI_AZURE_BASE_PATH | None | Azure OpenAI path to be used instead of the OpenAI path. All Windmill AI features will run on the specified deployed model. Format: `https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}` | Server |
| DISABLE_EMBEDDING | false | Disable local embedding search of hub scripts | Server |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
| DISABLE_SERVER | false | Disable the external API, operate as a worker only instance | Worker |

View File

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

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n SELECT $1::text, email, false, $3 FROM password WHERE $2::text = '*' OR email LIKE CONCAT('%', $2::text) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING",
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n SELECT $1::text, email, false, $3 FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
@@ -12,5 +12,5 @@
},
"nullable": []
},
"hash": "9739c91f85f36367cec44b12c921e2917afbcb249dbf52c82f06c943f0e1185d"
"hash": "0360207b5fb2a7f877c2608566454f40f7cbbcd20bcb84e5968ac3e21b6ea0f6"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT extra_perms from resource WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "extra_perms",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "108e4c505168381b51ad298b5294aa73e84d35bb68a5911985139bbf94c1d231"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM password",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "67624d479cc293b0306398f9e855cf38a72dd3d7d75b01e93cfd3ac4875b6e37"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT extra_perms from folder WHERE name = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "extra_perms",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "7813908c080e239fd6b9df3711fd704d6bb5d5686a02b72de2cb61e3c127cb54"
}

View File

@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT tag, dedicated_worker, value->>'early_return' as early_return from flow WHERE path = $1 and workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "dedicated_worker",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "early_return",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true,
true,
null
]
},
"hash": "8e0679c2b1bd451691fe5c69a2841ddc9f211311316ec6b9d4699b2c70997a19"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes FROM token WHERE email = $1\n ORDER BY created_at DESC",
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes FROM token WHERE email = $1\n ORDER BY created_at DESC",
"describe": {
"columns": [
{
@@ -48,5 +48,5 @@
true
]
},
"hash": "773c145013623e4eb29a8df70e46d805dc5b15942a36a0d988521abb0cb34e41"
"hash": "9ce6eecfa10c2f71cc536957ead498ad829ea8023ce449eab27225ec66738525"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT extra_perms from variable WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "extra_perms",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "a90924854cbd98bb16a2ed02e7966b0726e11ddd7511f1d503b1df29dee71419"
}

View File

@@ -1,16 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET canceled = true, canceled_reason = 'http connection broke', canceled_by = $1 WHERE id = $2 AND workspace_id = $3",
"query": "UPDATE queue SET canceled = true, canceled_reason = 'http connection broke', canceled_by = queue.created_by WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "460a3c3161899e172d2d20004e12ad3f331c73f260d099749a9e8fb85ae5614e"
"hash": "a96dd57b127a1adbdca13867a76d7fd9f9a9c76b02bcebd2830d928821f3abc0"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT content FROM script WHERE path = $1 AND workspace_id = $2\n AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \n workspace_id = $2)\n ",
"query": "\n SELECT content FROM script WHERE path = $1 AND workspace_id = $2\n AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND\n workspace_id = $2)\n ",
"describe": {
"columns": [
{
@@ -19,5 +19,5 @@
false
]
},
"hash": "d581bfb507d3a875f07677187717a469ec6ab1db1585835c961409612d81ec7d"
"hash": "ac01e45d3335015f53f3d63fe159e631efb65c3d326b6b6ae8361a2116bff145"
}

View File

@@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes FROM token WHERE email = $1 AND label != 'ephemeral-script'\n ORDER BY created_at DESC",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "label",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "token_prefix",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "expiration",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "last_used_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "scopes",
"type_info": "TextArray"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true,
null,
true,
false,
false,
true
]
},
"hash": "bc1f2f169b4960dae02f62e37bb4ae081146e598109860ee1f42cd3778c5ebaf"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n VALUES ($1, $2, $3, $4)",
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, email)\n DO UPDATE SET is_admin = $3, operator = $4",
"describe": {
"columns": [],
"parameters": {
@@ -13,5 +13,5 @@
},
"nullable": []
},
"hash": "be7a99a5bb6858323ca61dd51077010f51ba58ae76b9a413339255024dcb524d"
"hash": "d970a0b07a2b5840d1feb1baacb834dbaf91c633d3e7e1e29c8eb7eedc53e888"
}

1313
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.210.0"
version = "1.219.1"
authors.workspace = true
edition.workspace = true
@@ -21,7 +21,7 @@ members = [
]
[workspace.package]
version = "1.210.0"
version = "1.219.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -97,8 +97,8 @@ headers = "^0"
hyper = { version = "^0", features = ["full"] }
tokio = { version = "^1", features = ["full", "tracing"] }
tower = "^0"
tower-http = { version = "^0", features = ["trace", "cors"] }
tower-cookies = "^0"
tower-http = { version = "^0.4", features = ["trace", "cors"] }
tower-cookies = "0.9.0"
serde = "^1"
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
uuid = { version = "^1", features = ["serde", "v4"] }
@@ -137,6 +137,7 @@ json-pointer = "^0"
itertools = "^0"
regex = "^1"
deno_fetch = "0.139.0"
deno_tls = "0.102.0"
deno_console = "0.115.0"
deno_url = "0.115.0"
deno_webidl = "0.115.0"
@@ -180,7 +181,7 @@ async-stripe = { version = "0.25.2", features = [
async_zip = { version = "0.0.11", features = ["full"] }
once_cell = "1.17.1"
rsmq_async = { version = "5.1.5" }
gosyn = "0.2.2"
gosyn = "0.2.6"
bytes = "1.4.0"
gethostname = "0.4.3"
wasm-bindgen = "0.2"

View File

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE queue ALTER COLUMN tag TYPE varchar(255);
ALTER TABLE completed_job ALTER COLUMN tag TYPE varchar(255);

View File

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
INSERT INTO config (name, config) VALUES
('worker__reports', '{"init_bash": "apt-get update\napt-get install -y chromium", "worker_tags": ["deno", "python3", "go", "bash", "powershell", "dependency", "flow", "hub", "other", "bun", "chromium"]}') ON CONFLICT DO NOTHING;

View File

@@ -45,7 +45,8 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
"shapefile" => "pyshp",
"sklearn" => "scikit-learn",
"umap" => "umap-learn",
"cv2" => "opencv-python"
"cv2" => "opencv-python",
"atlassian" => "atlassian-python-api"
};
fn replace_import(x: String) -> String {
@@ -167,7 +168,7 @@ pub async fn parse_python_imports(
let code = sqlx::query_scalar!(
r#"
SELECT content FROM script WHERE path = $1 AND workspace_id = $2
AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND
AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND
workspace_id = $2)
"#,
&rpath,

View File

@@ -311,6 +311,12 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
} else {
let literals = types
.into_iter()
.filter(|x| match ***x {
TsType::TsKeywordType(TsKeywordType { kind, .. }) => {
kind != TsKeywordTypeKind::TsStringKeyword
}
_ => true,
})
.map(|x| match &**x {
TsType::TsLitType(TsLitType {
lit: TsLit::Str(Str { value, .. }), ..

View File

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

View File

@@ -97,15 +97,6 @@ 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,6 +106,15 @@ function addHeapObject(obj) {
return idx;
}
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 cachedFloat64Memory0 = null;
function getFloat64Memory0() {
@@ -526,14 +526,10 @@ function __wbg_get_imports() {
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_eval_f742bccbdcf34b02 = function(arg0, arg1) {
imports.wbg.__wbg_eval_a7fb8f2e206bad71 = function(arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
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;
@@ -551,6 +547,14 @@ function __wbg_get_imports() {
const ret = getObject(arg0) === getObject(arg1);
return ret;
};
imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) {
const ret = BigInt.asUintN(64, arg0);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_number_get = function(arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof(obj) === 'number' ? obj : undefined;
@@ -566,19 +570,15 @@ function __wbg_get_imports() {
const ret = getObject(arg0) in getObject(arg1);
return ret;
};
imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) {
const ret = BigInt.asUintN(64, arg0);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) {
const ret = getObject(arg0) == getObject(arg1);
return ret;
};
imports.wbg.__wbg_get_4a9aa5157afeb382 = function(arg0, arg1) {
imports.wbg.__wbg_get_f01601b5a68d10e3 = function(arg0, arg1) {
const ret = getObject(arg0)[arg1 >>> 0];
return addHeapObject(ret);
};
imports.wbg.__wbg_length_cace2e0b3ddc0502 = function(arg0) {
imports.wbg.__wbg_length_1009b1af0c481d7b = function(arg0) {
const ret = getObject(arg0).length;
return ret;
};
@@ -586,39 +586,39 @@ function __wbg_get_imports() {
const ret = typeof(getObject(arg0)) === 'function';
return ret;
};
imports.wbg.__wbg_next_15da6a3df9290720 = function(arg0) {
imports.wbg.__wbg_next_9b877f231f476d01 = function(arg0) {
const ret = getObject(arg0).next;
return addHeapObject(ret);
};
imports.wbg.__wbg_next_1989a20442400aaa = function() { return handleError(function (arg0) {
imports.wbg.__wbg_next_6529ee0cca8d57ed = function() { return handleError(function (arg0) {
const ret = getObject(arg0).next();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_done_bc26bf4ada718266 = function(arg0) {
imports.wbg.__wbg_done_5fe336b092d60cf2 = function(arg0) {
const ret = getObject(arg0).done;
return ret;
};
imports.wbg.__wbg_value_0570714ff7d75f35 = function(arg0) {
imports.wbg.__wbg_value_0c248a78fdc8e19f = function(arg0) {
const ret = getObject(arg0).value;
return addHeapObject(ret);
};
imports.wbg.__wbg_iterator_7ee1a391d310f8e4 = function() {
imports.wbg.__wbg_iterator_db7ca081358d4fb2 = function() {
const ret = Symbol.iterator;
return addHeapObject(ret);
};
imports.wbg.__wbg_get_2aff440840bb6202 = function() { return handleError(function (arg0, arg1) {
imports.wbg.__wbg_get_7b48513de5dc5ea4 = function() { return handleError(function (arg0, arg1) {
const ret = Reflect.get(getObject(arg0), getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_call_669127b9d730c650 = function() { return handleError(function (arg0, arg1) {
imports.wbg.__wbg_call_90c26b09837aba1c = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).call(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_isArray_38525be7442aa21e = function(arg0) {
imports.wbg.__wbg_isArray_74fb723e24f76012 = function(arg0) {
const ret = Array.isArray(getObject(arg0));
return ret;
};
imports.wbg.__wbg_instanceof_ArrayBuffer_c7cc317e5c29cc0d = function(arg0) {
imports.wbg.__wbg_instanceof_ArrayBuffer_e7d53d51371448e2 = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof ArrayBuffer;
@@ -628,30 +628,30 @@ function __wbg_get_imports() {
const ret = result;
return ret;
};
imports.wbg.__wbg_isSafeInteger_c38b0a16d0c7cef7 = function(arg0) {
imports.wbg.__wbg_isSafeInteger_f93fde0dca9820f8 = function(arg0) {
const ret = Number.isSafeInteger(getObject(arg0));
return ret;
};
imports.wbg.__wbg_entries_6d727b73ee02b7ce = function(arg0) {
imports.wbg.__wbg_entries_9e2e2aa45aa5094a = function(arg0) {
const ret = Object.entries(getObject(arg0));
return addHeapObject(ret);
};
imports.wbg.__wbg_buffer_344d9b41efe96da7 = function(arg0) {
imports.wbg.__wbg_buffer_a448f833075b71ba = function(arg0) {
const ret = getObject(arg0).buffer;
return addHeapObject(ret);
};
imports.wbg.__wbg_new_d8a000788389a31e = function(arg0) {
imports.wbg.__wbg_new_8f67e318f15d7254 = function(arg0) {
const ret = new Uint8Array(getObject(arg0));
return addHeapObject(ret);
};
imports.wbg.__wbg_set_dcfd613a3420f908 = function(arg0, arg1, arg2) {
imports.wbg.__wbg_set_2357bf09366ee480 = function(arg0, arg1, arg2) {
getObject(arg0).set(getObject(arg1), arg2 >>> 0);
};
imports.wbg.__wbg_length_a5587d6cd79ab197 = function(arg0) {
imports.wbg.__wbg_length_1d25fa9e4ac21ce7 = function(arg0) {
const ret = getObject(arg0).length;
return ret;
};
imports.wbg.__wbg_instanceof_Uint8Array_19e6f142a5e7e1e1 = function(arg0) {
imports.wbg.__wbg_instanceof_Uint8Array_bced6f43aed8c1aa = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof Uint8Array;

View File

@@ -97,6 +97,15 @@ async fn main() -> anyhow::Result<()> {
} else if &x == "worker" {
tracing::info!("Binary is in 'worker' mode");
Mode::Worker
} else if &x == "agent" {
tracing::info!("Binary is in 'agent' mode");
if std::env::var("BASE_INTERNAL_URL").is_err() {
panic!("BASE_INTERNAL_URL is required in agent mode")
}
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")
}
Mode::Agent
} else {
if &x != "standalone" {
tracing::error!("mode not recognized, defaulting to standalone: {x}");
@@ -130,7 +139,7 @@ async fn main() -> anyhow::Result<()> {
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false)
&& mode != Mode::Worker;
&& (mode == Mode::Server || mode == Mode::Standalone);
let server_bind_address: IpAddr = if server_mode {
std::env::var("SERVER_BIND_ADDR")
@@ -191,31 +200,36 @@ async fn main() -> anyhow::Result<()> {
None
};
let last_mig_version =
sqlx::query_scalar!("select version from _sqlx_migrations order by version desc limit 1;")
.fetch_optional(&db)
.await
.ok()
.flatten();
let is_agent = mode == Mode::Agent;
tracing::info!(
if !is_agent {
let last_mig_version = sqlx::query_scalar!(
"select version from _sqlx_migrations order by version desc limit 1;"
)
.fetch_optional(&db)
.await
.ok()
.flatten();
tracing::info!(
"Last migration version: {last_mig_version:?}. Starting potential migration of the db if first connection on a new windmill version (can take a while depending on the migration) ...",
);
// migration code to avoid break
windmill_api::migrate_db(&db).await?;
// migration code to avoid break
windmill_api::migrate_db(&db).await?;
let last_mig_version =
sqlx::query_scalar!("select version from _sqlx_migrations order by version desc limit 1;")
.fetch_optional(&db)
.await
.ok()
.flatten();
tracing::info!(
"Completed potential migration of the db. Last migration version: {last_mig_version:?}",
);
let last_mig_version = sqlx::query_scalar!(
"select version from _sqlx_migrations order by version desc limit 1;"
)
.fetch_optional(&db)
.await
.ok()
.flatten();
tracing::info!(
"Completed potential migration of the db. Last migration version: {last_mig_version:?}",
);
}
let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
let shutdown_signal = windmill_common::shutdown_signal(tx.clone(), rx.resubscribe());
@@ -248,8 +262,18 @@ Windmill Community Edition {GIT_VERSION}
port_var.unwrap_or(0)
};
let default_base_internal_url = format!("http://localhost:{}", port.to_string());
// since it's only on server mode, the port is statically defined
let base_internal_url: String = format!("http://localhost:{}", port.to_string());
let base_internal_url: String = if let Ok(base_url) = std::env::var("BASE_INTERNAL_URL") {
if !is_agent {
tracing::warn!("BASE_INTERNAL_URL is now unecessary and ignored unless the mode is 'agent', you can remove it.");
default_base_internal_url.clone()
} else {
base_url
}
} else {
default_base_internal_url.clone()
};
initial_load(&db, tx.clone(), worker_mode, server_mode).await;
@@ -257,14 +281,10 @@ Windmill Community Edition {GIT_VERSION}
monitor_pool(&db).await;
if std::env::var("BASE_INTERNAL_URL").is_ok() {
tracing::warn!("BASE_INTERNAL_URL is now unecessary and ignored, you can remove it.");
}
let addr = SocketAddr::from((server_bind_address, port));
let rsmq2 = rsmq.clone();
let (port_tx, port_rx) = tokio::sync::oneshot::channel::<u16>();
let (base_internal_tx, base_internal_rx) = tokio::sync::oneshot::channel::<String>();
DirBuilder::new()
.recursive(true)
@@ -273,21 +293,28 @@ Windmill Community Edition {GIT_VERSION}
.expect("could not create initial server dir");
let server_f = async {
windmill_api::run_server(
db.clone(),
rsmq2,
addr,
rx.resubscribe(),
port_tx,
server_mode,
)
.await?;
if !is_agent {
windmill_api::run_server(
db.clone(),
rsmq2,
addr,
rx.resubscribe(),
base_internal_tx,
server_mode,
)
.await?;
} else {
base_internal_tx
.send(base_internal_url.clone())
.map_err(|e| {
anyhow::anyhow!("Could not send base_internal_url to agent: {e}")
})?;
}
Ok(()) as anyhow::Result<()>
};
let workers_f = async {
let port = port_rx.await?;
let base_internal_url: String = format!("http://localhost:{}", port.to_string());
let base_internal_url = base_internal_rx.await?;
if worker_mode {
run_workers(
db.clone(),

View File

@@ -719,7 +719,7 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
&db,
&job.workspace_id,
&job.permissioned_as,
"ephemeral-zombie-jobs",
"ephemeral-script",
*SCRIPT_TOKEN_EXPIRY,
&job.email,
)

View File

@@ -124,7 +124,7 @@ impl ApiServer {
let addr = sock.local_addr().unwrap();
drop(sock);
let (port_tx, _port_rx) = tokio::sync::oneshot::channel::<u16>();
let (port_tx, _port_rx) = tokio::sync::oneshot::channel::<String>();
let task = tokio::task::spawn(windmill_api::run_server(
db.clone(),
@@ -961,7 +961,7 @@ fn spawn_test_worker(
tokio::sync::broadcast::Sender<()>,
tokio::task::JoinHandle<()>,
) {
for x in [windmill_worker::LOCK_CACHE_DIR] {
for x in [windmill_worker::LOCK_CACHE_DIR, windmill_worker::GO_BIN_CACHE_DIR] {
std::fs::DirBuilder::new()
.recursive(true)
.create(x)

View File

@@ -82,5 +82,8 @@ tokenizers.workspace = true
candle-core.workspace = true
candle-transformers.workspace = true
candle-nn.workspace = true
aws-sdk-s3 = "0.36.0"
aws-config = "0.57.2"
aws-sdk-s3 = "0.39.1"
aws-config = "1.0.0"
polars = { version = "0.35.2", features = ["lazy", "parquet", "aws", "csv", "dtype-full"] }
polars-io = { version = "0.35.2", features = ["csv"] }
object_store = { version = "0.8.0", features = ["aws"] }

View File

@@ -1,6 +1,6 @@
openapi: 3.0.3
info:
version: 1.206.0
version: 1.213.0
title: Windmill API
contact:
name: Windmill Team
@@ -952,6 +952,19 @@ paths:
text/plain:
schema:
type: string
/users/leave_instance:
post:
summary: leave instance
operationId: leaveInstance
tags:
- user
responses:
'200':
description: status
content:
text/plain:
schema:
type: string
/users/usage:
get:
summary: get current usage outside of premium workspaces
@@ -1124,24 +1137,6 @@ paths:
- disabled
- folders
- folders_owners
/w/{workspace}/users/leave_workspace:
post:
summary: leave workspace
operationId: leaveWorkspace
tags:
- user
parameters:
- name: workspace
in: path
required: true
schema: *ref_0
responses:
'200':
description: status
content:
text/plain:
schema:
type: string
/users/accept_invite:
post:
summary: accept invite to workspace
@@ -1360,6 +1355,24 @@ paths:
text/plain:
schema:
type: string
/w/{workspace}/workspaces/leave:
post:
summary: leave workspace
operationId: leaveWorkspace
tags:
- workspace
parameters:
- name: workspace
in: path
required: true
schema: *ref_0
responses:
'200':
description: status
content:
text/plain:
schema:
type: string
/w/{workspace}/users/whois/{username}:
get:
summary: whois
@@ -1534,6 +1547,24 @@ paths:
properties:
deploy_to:
type: string
/w/{workspace}/workspaces/is_premium:
get:
summary: get if workspace is premium
operationId: getIsPremium
tags:
- workspace
parameters:
- name: workspace
in: path
required: true
schema: *ref_0
responses:
'200':
description: status
content:
application/json:
schema:
type: boolean
/w/{workspace}/workspaces/premium_info:
get:
summary: get premium info
@@ -1557,6 +1588,8 @@ paths:
type: boolean
usage:
type: number
seats:
type: number
required:
- premium
/w/{workspace}/workspaces/edit_slack_command:
@@ -1670,6 +1703,8 @@ paths:
properties:
operator:
type: boolean
invite_all:
type: boolean
responses:
'200':
description: status
@@ -3266,32 +3301,32 @@ paths:
id:
type: string
value:
oneOf: &ref_177
oneOf: &ref_182
- type: object
properties: &ref_161
properties: &ref_166
input_transforms:
type: object
additionalProperties:
oneOf: &ref_24
- type: object
properties: &ref_157
properties: &ref_162
value: {}
type:
type: string
enum:
- javascript
required: &ref_158
required: &ref_163
- expr
- type
- type: object
properties: &ref_159
properties: &ref_164
expr:
type: string
type:
type: string
enum:
- javascript
required: &ref_160
required: &ref_165
- expr
- type
discriminator: &ref_25
@@ -3331,13 +3366,13 @@ paths:
type: number
concurrency_time_window_s:
type: number
required: &ref_162
required: &ref_167
- type
- content
- language
- input_transforms
- type: object
properties: &ref_163
properties: &ref_168
input_transforms:
type: object
additionalProperties:
@@ -3351,12 +3386,12 @@ paths:
type: string
enum:
- script
required: &ref_164
required: &ref_169
- type
- path
- input_transforms
- type: object
properties: &ref_165
properties: &ref_170
input_transforms:
type: object
additionalProperties:
@@ -3368,12 +3403,12 @@ paths:
type: string
enum:
- flow
required: &ref_166
required: &ref_171
- type
- path
- input_transforms
- type: object
properties: &ref_167
properties: &ref_172
modules:
type: array
items:
@@ -3395,13 +3430,13 @@ paths:
type: boolean
parallelism:
type: integer
required: &ref_168
required: &ref_173
- modules
- iterator
- skip_failures
- type
- type: object
properties: &ref_169
properties: &ref_174
branches:
type: array
items:
@@ -3432,12 +3467,12 @@ paths:
type: string
enum:
- branchone
required: &ref_170
required: &ref_175
- branches
- default
- type
- type: object
properties: &ref_171
properties: &ref_176
branches:
type: array
items:
@@ -3462,28 +3497,28 @@ paths:
- branchall
parallel:
type: boolean
required: &ref_172
required: &ref_177
- branches
- type
- type: object
properties: &ref_173
properties: &ref_178
type:
type: string
enum:
- identity
flow:
type: boolean
required: &ref_174
required: &ref_179
- type
- type: object
properties: &ref_175
properties: &ref_180
type:
type: string
enum:
- graphql
required: &ref_176
required: &ref_181
- type
discriminator: &ref_178
discriminator: &ref_183
propertyName: type
mapping:
rawscript: '#/components/schemas/RawScript'
@@ -3539,7 +3574,7 @@ paths:
type: number
retry:
type: object
properties: &ref_179
properties: &ref_184
constant:
type: object
properties:
@@ -5132,6 +5167,8 @@ paths:
type: boolean
priority:
type: integer
dedicated_worker:
type: boolean
required: &ref_144
- path
- edited_by
@@ -5279,6 +5316,8 @@ paths:
type: boolean
priority:
type: integer
dedicated_worker:
type: boolean
required:
- path
- type: object
@@ -9448,6 +9487,57 @@ paths:
properties:
connection_settings_str:
type: string
/w/{workspace}/job_helpers/polars_connection_settings:
post:
summary: >-
Converts an S3 resource to the set of arguments necessary to connect
Polars to an S3 bucket
operationId: polarsConnectionSettings
tags:
- helpers
parameters:
- name: workspace
in: path
required: true
schema: *ref_0
requestBody:
description: S3 resource to connect to
required: true
content:
application/json:
schema:
s3_resource:
$ref: '#/components/schemas/S3Resource'
responses:
'200':
description: Connection settings
content:
application/json:
schema:
type: object
properties:
endpoint_url:
type: string
key:
type: string
secret:
type: string
use_ssl:
type: boolean
cache_regions:
type: boolean
client_kwargs:
type: object
properties: &ref_155
region_name:
type: string
required: &ref_156
- region_name
required:
- endpoint_url
- use_ssl
- cache_regions
- client_kwargs
/w/{workspace}/job_helpers/test_connection:
get:
summary: Test connection to the workspace datasets storage
@@ -9465,10 +9555,10 @@ paths:
content:
application/json:
schema: {}
/w/{workspace}/job_helpers/list_stored_datasets:
post:
summary: List the dataset keys available in the worspace datasets storage
operationId: polarsConnectionSettings
/w/{workspace}/job_helpers/list_stored_files:
get:
summary: List the file keys available in the worspace files storage (S3)
operationId: listStoredFiles
tags:
- helpers
parameters:
@@ -9476,25 +9566,129 @@ paths:
in: path
required: true
schema: *ref_0
- name: max_keys
in: query
required: true
schema:
type: integer
- name: marker
in: query
schema:
type: string
responses:
'200':
description: Connection settings
description: List of file keys
content:
application/json:
schema:
type: object
properties:
dataset_keys:
next_marker:
type: string
windmill_large_files:
type: array
items:
type: object
properties: &ref_155
properties: &ref_157
s3:
type: string
s3_bucket:
type: string
required: &ref_156
required: &ref_158
- s3
required:
- windmill_large_files
/w/{workspace}/job_helpers/load_file_metadata:
get:
summary: Load metadata of the file
operationId: loadFileMetadata
tags:
- helpers
parameters:
- name: workspace
in: path
required: true
schema: *ref_0
- name: file_key
in: query
required: true
schema:
type: string
responses:
'200':
description: FileMetadata
content:
application/json:
schema:
type: object
properties: &ref_159
mime_type:
type: string
size_in_bytes:
type: integer
last_modified:
type: string
format: date-time
expires:
type: string
format: date-time
version_id:
type: string
/w/{workspace}/job_helpers/load_file_preview:
get:
summary: Load a preview of the file
operationId: loadFilePreview
tags:
- helpers
parameters:
- name: workspace
in: path
required: true
schema: *ref_0
- name: file_key
in: query
required: true
schema:
type: string
- name: file_size_in_bytes
in: query
schema:
type: integer
- name: file_mime_type
in: query
schema:
type: string
- name: csv_separator
in: query
schema:
type: string
- name: read_bytes_from
in: query
schema:
type: integer
- name: read_bytes_length
in: query
schema:
type: integer
responses:
'200':
description: FilePreview
content:
application/json:
schema:
type: object
properties: &ref_160
msg:
type: string
content:
type: string
content_type:
type: string
enum:
- RawText
- Csv
- Parquet
- Unknown
required: &ref_161
- content_type
components:
securitySchemes:
bearerAuth:
@@ -10093,13 +10287,24 @@ components:
HubScriptKind:
name: kind
schema: *ref_29
PolarsClientKwargs:
type: object
properties: *ref_155
required: *ref_156
LargeFileStorage:
type: object
properties: *ref_15
WindmillLargeFile:
type: object
properties: *ref_155
required: *ref_156
properties: *ref_157
required: *ref_158
WindmillFileMetadata:
type: object
properties: *ref_159
WindmillFilePreview:
type: object
properties: *ref_160
required: *ref_161
S3Resource:
type: object
properties:
@@ -10125,57 +10330,57 @@ components:
- pathStyle
StaticTransform:
type: object
properties: *ref_157
required: *ref_158
properties: *ref_162
required: *ref_163
JavascriptTransform:
type: object
properties: *ref_159
required: *ref_160
properties: *ref_164
required: *ref_165
InputTransform:
oneOf: *ref_24
discriminator: *ref_25
RawScript:
type: object
properties: *ref_161
required: *ref_162
properties: *ref_166
required: *ref_167
PathScript:
type: object
properties: *ref_163
required: *ref_164
properties: *ref_168
required: *ref_169
PathFlow:
type: object
properties: *ref_165
required: *ref_166
properties: *ref_170
required: *ref_171
FlowModule:
type: object
properties: *ref_26
required: *ref_27
ForloopFlow:
type: object
properties: *ref_167
required: *ref_168
properties: *ref_172
required: *ref_173
BranchOne:
type: object
properties: *ref_169
required: *ref_170
properties: *ref_174
required: *ref_175
BranchAll:
type: object
properties: *ref_171
required: *ref_172
properties: *ref_176
required: *ref_177
Identity:
type: object
properties: *ref_173
required: *ref_174
properties: *ref_178
required: *ref_179
Graphql:
type: object
properties: *ref_175
required: *ref_176
properties: *ref_180
required: *ref_181
FlowModuleValue:
oneOf: *ref_177
discriminator: *ref_178
oneOf: *ref_182
discriminator: *ref_183
Retry:
type: object
properties: *ref_179
properties: *ref_184
FlowValue:
type: object
properties: *ref_48

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.210.0
version: 1.219.1
title: Windmill API
contact:
@@ -731,6 +731,20 @@ paths:
schema:
type: string
/users/leave_instance:
post:
summary: leave instance
operationId: leaveInstance
tags:
- user
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/users/usage:
get:
summary: get current usage outside of premium workspaces
@@ -827,22 +841,6 @@ paths:
schema:
$ref: "#/components/schemas/User"
/w/{workspace}/users/leave_workspace:
post:
summary: leave workspace
operationId: leaveWorkspace
tags:
- user
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/users/accept_invite:
post:
summary: accept invite to workspace
@@ -1051,6 +1049,22 @@ paths:
schema:
type: string
/w/{workspace}/workspaces/leave:
post:
summary: leave workspace
operationId: leaveWorkspace
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/w/{workspace}/users/whois/{username}:
get:
summary: whois
@@ -1349,6 +1363,8 @@ paths:
properties:
operator:
type: boolean
invite_all:
type: boolean
responses:
"200":
@@ -1618,6 +1634,11 @@ paths:
operationId: listTokens
tags:
- user
parameters:
- name: exclude_ephemeral
in: query
schema:
type: boolean
responses:
"200":
description: truncated token
@@ -6296,7 +6317,9 @@ paths:
/w/{workspace}/job_helpers/duckdb_connection_settings:
post:
summary: Converts an S3 resource to the set of instructions necessary to connect DuckDB to an S3 bucket
summary:
Converts an S3 resource to the set of instructions necessary to connect
DuckDB to an S3 bucket
operationId: duckdbConnectionSettings
tags:
- helpers
@@ -6308,8 +6331,10 @@ paths:
content:
application/json:
schema:
s3_resource:
$ref: "#/components/schemas/S3Resource"
type: object
properties:
s3_resource:
$ref: "#/components/schemas/S3Resource"
responses:
"200":
description: Connection settings
@@ -6320,6 +6345,51 @@ paths:
properties:
connection_settings_str:
type: string
/w/{workspace}/job_helpers/polars_connection_settings:
post:
summary:
Converts an S3 resource to the set of arguments necessary to connect
Polars to an S3 bucket
operationId: polarsConnectionSettings
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: S3 resource to connect to
required: true
content:
application/json:
schema:
type: object
properties:
s3_resource:
$ref: "#/components/schemas/S3Resource"
responses:
"200":
description: Connection settings
content:
application/json:
schema:
type: object
properties:
endpoint_url:
type: string
key:
type: string
secret:
type: string
use_ssl:
type: boolean
cache_regions:
type: boolean
client_kwargs:
$ref: "#/components/schemas/PolarsClientKwargs"
required:
- endpoint_url
- use_ssl
- cache_regions
- client_kwargs
/w/{workspace}/job_helpers/test_connection:
get:
@@ -6336,26 +6406,101 @@ paths:
application/json:
schema: {}
/w/{workspace}/job_helpers/list_stored_datasets:
post:
summary: List the dataset keys available in the worspace datasets storage
operationId: polarsConnectionSettings
/w/{workspace}/job_helpers/list_stored_files:
get:
summary: List the file keys available in the worspace files storage (S3)
operationId: listStoredFiles
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: max_keys
in: query
required: true
schema:
type: integer
- name: marker
in: query
schema:
type: string
responses:
"200":
description: Connection settings
description: List of file keys
content:
application/json:
schema:
type: object
properties:
dataset_keys:
next_marker:
type: string
windmill_large_files:
type: array
items:
$ref: "#/components/schemas/WindmillLargeFile"
required:
- windmill_large_files
/w/{workspace}/job_helpers/load_file_metadata:
get:
summary: Load metadata of the file
operationId: loadFileMetadata
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: file_key
in: query
required: true
schema:
type: string
responses:
"200":
description: FileMetadata
content:
application/json:
schema:
$ref: "#/components/schemas/WindmillFileMetadata"
/w/{workspace}/job_helpers/load_file_preview:
get:
summary: Load a preview of the file
operationId: loadFilePreview
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: file_key
in: query
required: true
schema:
type: string
- name: file_size_in_bytes
in: query
schema:
type: integer
- name: file_mime_type
in: query
schema:
type: string
- name: csv_separator
in: query
schema:
type: string
- name: read_bytes_from
in: query
schema:
type: integer
- name: read_bytes_length
in: query
schema:
type: integer
responses:
"200":
description: FilePreview
content:
application/json:
schema:
$ref: "#/components/schemas/WindmillFilePreview"
components:
securitySchemes:
@@ -8340,6 +8485,14 @@ components:
type: string
enum: [script, failure, trigger, approval]
PolarsClientKwargs:
type: object
properties:
region_name:
type: string
required:
- region_name
LargeFileStorage:
type: object
properties:
@@ -8354,11 +8507,38 @@ components:
properties:
s3:
type: string
s3_bucket:
type: string
required:
- s3
WindmillFileMetadata:
type: object
properties:
mime_type:
type: string
size_in_bytes:
type: integer
last_modified:
type: string
format: date-time
expires:
type: string
format: date-time
version_id:
type: string
WindmillFilePreview:
type: object
properties:
msg:
type: string
content:
type: string
content_type:
type: string
enum: ["RawText", "Csv", "Parquet", "Unknown"]
required:
- content_type
S3Resource:
type: object
properties:

View File

@@ -1,6 +1,6 @@
use std::{collections::HashMap, path::PathBuf, sync::Arc};
use anyhow::{self, Error, Result};
use anyhow::{anyhow, Error, Result};
use axum::{
extract::{Path, Query},
routing::get,
@@ -228,14 +228,31 @@ impl EmbeddingsDb {
self.db
.create_collection("resource_types".to_string(), 384, Distance::Cosine)?;
let response = http_get_from_hub(
&HTTP_CLIENT,
"https://hub.windmill.dev/scripts/embeddings",
false,
None,
pg_db,
)
.await?;
let response = HTTP_CLIENT
.get("https://bucket.windmillhub.com/embeddings/scripts_embeddings.json")
.send()
.await;
let response =
if response.is_err() || response.as_ref().unwrap().error_for_status_ref().is_err() {
tracing::warn!("Failed to get scripts embeddings from bucket, trying hub...");
http_get_from_hub(
&HTTP_CLIENT,
"https://hub.windmill.dev/scripts/embeddings",
false,
None,
pg_db,
)
.await?
} else {
response.unwrap()
};
if response.error_for_status_ref().is_err() {
return Err(anyhow!(
"Failed to get scripts embeddings from hub with error code: {}",
response.status()
));
}
let hub_scripts = response.json::<Vec<HubScript>>().await?;
for script in &hub_scripts {
@@ -257,14 +274,31 @@ impl EmbeddingsDb {
self.db.insert_into_collection("scripts", embedding)?;
}
let response = http_get_from_hub(
&HTTP_CLIENT,
"https://hub.windmill.dev/resource_types/embeddings",
false,
None,
pg_db,
)
.await?;
let response = HTTP_CLIENT
.get("https://bucket.windmillhub.com/embeddings/resource_types_embeddings.json")
.send()
.await;
let response = if response.is_err()
|| response.as_ref().unwrap().error_for_status_ref().is_err()
{
tracing::warn!("Failed to get resource types embeddings from bucket, trying hub...");
http_get_from_hub(
&HTTP_CLIENT,
"https://hub.windmill.dev/resource_types/embeddings",
false,
None,
pg_db,
)
.await?
} else {
response.unwrap()
};
if response.error_for_status_ref().is_err() {
return Err(anyhow!(
"Failed to get resource types embeddings from hub with error code: {}",
response.status()
));
}
let hub_resource_types = response.json::<Vec<HubResourceType>>().await?;
let resource_types: Vec<ResourceType> =

View File

@@ -520,6 +520,7 @@ async fn update_flow(
.await?;
if let Some(schedule) = schedule {
clear_schedule(tx.transaction_mut(), &flow_path, &w_id).await?;
schedulables.push(schedule);
}
@@ -896,8 +897,8 @@ mod tests {
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
priority: None,
early_return: None,
};
let expect = serde_json::json!({
"modules": [

View File

@@ -204,9 +204,9 @@ async fn _check_nb_of_groups(db: &DB) -> Result<()> {
let nb_groups = sqlx::query_scalar!("SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack'",)
.fetch_one(db)
.await?;
if nb_groups.unwrap_or(0) >= 5 {
if nb_groups.unwrap_or(0) >= 3 {
return Err(Error::BadRequest(
"You have reached the maximum number of groups (5 outside of native groups 'all', 'slack' and 'error_handler') without an enterprise license"
"You have reached the maximum number of groups (3 outside of native groups 'all', 'slack' and 'error_handler') without an enterprise license"
.to_string(),
));
}

View File

@@ -1,11 +1,27 @@
use crate::{resources::transform_json_value, users::Tokened, workspaces::LargeFileStorage};
use aws_sdk_s3::config::{Credentials, Region};
use std::cmp;
use crate::{
db::DB, resources::transform_json_value, users::Tokened, workspaces::LargeFileStorage,
};
use aws_sdk_s3::config::{BehaviorVersion, Credentials, Region};
use axum::{
extract::Path,
extract::{Path, Query},
routing::{get, post},
Extension, Json, Router,
};
use hyper::http;
use object_store::ClientConfigKey;
use polars::{
io::{
cloud::{AmazonS3ConfigKey, CloudOptions},
SerReader,
},
lazy::{
dsl::col,
frame::{LazyFrame, ScanArgsParquet},
},
prelude::CsvReader,
};
use serde::{Deserialize, Serialize};
use tower_http::cors::{Any, CorsLayer};
use windmill_common::{db::UserDB, error};
@@ -29,8 +45,16 @@ pub fn workspaced_service() -> Router {
)
.route("/test_connection", get(test_connection).layer(cors.clone()))
.route(
"/list_stored_datasets",
get(list_stored_datasets).layer(cors.clone()),
"/list_stored_files",
get(list_stored_files).layer(cors.clone()),
)
.route(
"/load_file_metadata",
get(load_file_metadata).layer(cors.clone()),
)
.route(
"/load_file_preview",
get(load_file_preview).layer(cors.clone()),
)
}
@@ -118,7 +142,7 @@ async fn polars_connection_settings(
let s3_resource = query.s3_resource;
let response = PolarsConnectionSettingsResponse {
endpoint_url: s3_resource.endpoint,
endpoint_url: render_endpoint(&s3_resource),
key: s3_resource.access_key,
secret: s3_resource.secret_key,
use_ssl: s3_resource.use_ssl,
@@ -129,31 +153,28 @@ async fn polars_connection_settings(
return Ok(Json(response));
}
#[derive(Serialize)]
struct ListStoredDatasetsResponse {
windmill_large_files: Vec<WindmillLargeFile>,
}
#[derive(Serialize, Clone)]
#[derive(Serialize, Deserialize, Clone)]
struct WindmillLargeFile {
s3: String,
s3_bucket: Option<String>,
}
async fn test_connection(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
) -> error::JsonResult<()> {
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &token, &w_id).await?;
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
if s3_resource_opt.is_none() {
return Err(error::Error::NotFound(
"No datasets storage resource defined at the workspace level".to_string(),
));
}
let s3_resource = s3_resource_opt.unwrap();
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);
s3_client
.list_objects()
@@ -165,64 +186,301 @@ async fn test_connection(
return Ok(Json(()));
}
async fn list_stored_datasets(
#[derive(Deserialize)]
struct ListStoredFilesQuery {
pub max_keys: i32,
pub marker: Option<String>,
}
#[derive(Serialize)]
struct ListStoredDatasetsResponse {
windmill_large_files: Vec<WindmillLargeFile>,
pub next_marker: Option<String>,
}
async fn list_stored_files(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Query(query): Query<ListStoredFilesQuery>,
) -> error::JsonResult<ListStoredDatasetsResponse> {
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &token, &w_id).await?;
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
let s3_resource = s3_resource_opt.unwrap();
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 mut stored_datasets = Vec::<WindmillLargeFile>::new();
let s3_bucket = s3_resource.bucket;
loop {
let mut list_object_query = s3_client
.list_objects()
.bucket(s3_bucket.clone())
.max_keys(32);
if let Some(last_dataset_name) = stored_datasets.last() {
// if stored_datasets already has some elements, it means we're looping through pages
// according to AWS SDK docs, we can set the marker to the last returned dataset
list_object_query = list_object_query.set_marker(Some(last_dataset_name.clone().s3))
}
let list_object_query = s3_client
.list_objects()
.bucket(s3_bucket.clone())
.max_keys(query.max_keys)
.set_marker(query.marker);
let bucket_objects = list_object_query
.send()
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
let bucket_objects = list_object_query
.send()
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
let page_datasets = bucket_objects
.contents()
.iter()
.filter(|object| object.key().is_some())
.map(|object| object.key())
.map(Option::unwrap)
.map(&str::to_string)
.map(|object_key| WindmillLargeFile {
s3: object_key.clone(),
s3_bucket: Some(s3_bucket.clone()),
})
.collect::<Vec<WindmillLargeFile>>();
let stored_datasets = bucket_objects
.contents()
.iter()
.filter(|object| object.key().is_some())
.map(|object| object.key())
.map(Option::unwrap)
.map(&str::to_string)
.map(|object_key| WindmillLargeFile { s3: object_key.clone() })
.collect::<Vec<WindmillLargeFile>>();
stored_datasets.extend(page_datasets.clone());
if !bucket_objects.is_truncated() {
break;
}
#[cfg(not(feature = "enterprise"))]
if stored_datasets.len() > 20 {
return Err(error::Error::ExecutionErr(
"The workspace s3 bucket contains more than 20 files. Consider upgrading to Windmill Enterprise Edition to continue to use this feature."
.to_string(),
));
}
let next_marker = if bucket_objects.is_truncated().unwrap_or(false) {
if bucket_objects.next_marker().is_some() {
// some S3 providers returns the next marker for us. If that's the case just re-use it
bucket_objects.next_marker().map(|v| v.to_owned())
} else {
// others, like AWS, doesn't and implicitly expect users to return the last key of the current page
stored_datasets.last().map(|v| v.s3.clone())
}
} else {
None
};
return Ok(Json(ListStoredDatasetsResponse {
windmill_large_files: stored_datasets,
next_marker: next_marker,
}));
}
#[derive(Deserialize)]
struct LoadFileMetadataQuery {
pub file_key: String,
}
#[derive(Serialize)]
struct LoadFileMetadataResponse {
pub mime_type: Option<String>,
pub size_in_bytes: Option<i64>,
pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
pub expires: Option<chrono::DateTime<chrono::Utc>>,
pub version_id: Option<String>,
}
#[derive(Deserialize)]
struct LoadFilePreviewQuery {
pub file_key: String,
// The two options below are requested from s3 with an additional query is not set
pub file_size_in_bytes: Option<i64>,
pub file_mime_type: Option<String>,
// For CSV files, the separator needs to be specify
pub csv_separator: Option<String>,
// Specify the content length to be read. Both will be taken into account when reading files, except for:
// - CSVs: only the length will be taken into account, a CSV file larger than this will be truncated.
// Note that truncated CSV files might not be valid CSV files anymore, and therefore the preview might fail
// - Parquet files: Parquet files are lazy-loaded. Therefore none of those params will be taken into account
pub read_bytes_from: i64,
pub read_bytes_length: i64,
}
#[derive(Serialize)]
struct LoadFilePreviewResponse {
pub content: Option<String>,
pub content_type: WindmillContentType,
pub msg: Option<String>,
}
#[derive(Serialize)]
enum WindmillContentType {
RawText,
Csv,
Parquet,
Unknown,
}
async fn load_file_metadata(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Query(query): Query<LoadFileMetadataQuery>,
) -> error::JsonResult<LoadFileMetadataResponse> {
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();
let s3_object_metadata = s3_client
.head_object()
.bucket(&s3_bucket)
.key(&file_key)
.send()
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
let response = LoadFileMetadataResponse {
mime_type: s3_object_metadata.content_type().map(&str::to_string),
size_in_bytes: s3_object_metadata.content_length(),
last_modified: s3_object_metadata
.last_modified()
.map(|dt| chrono::DateTime::from_timestamp(dt.secs(), dt.subsec_nanos()))
.flatten(),
expires: s3_object_metadata
.expires()
.map(|dt| chrono::DateTime::from_timestamp(dt.secs(), dt.subsec_nanos()))
.flatten(),
version_id: s3_object_metadata.version_id().map(&str::to_string),
};
return Ok(Json(response));
}
async fn load_file_preview(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Query(query): Query<LoadFilePreviewQuery>,
) -> error::JsonResult<LoadFilePreviewResponse> {
// query validation
if query.read_bytes_length > 8 * 1024 * 1024 {
return Err(error::Error::BadRequest(
"Cannot load file bigger than 8MB".to_string(),
));
}
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();
// if content length is provided in the request, use it, otherwise get it from s3
let (s3_object_mime_type, s3_object_content_length) =
if query.file_size_in_bytes.is_some() || query.file_mime_type.is_some() {
(query.file_mime_type.clone(), query.file_size_in_bytes)
} else {
let s3_object_metadata = s3_client
.head_object()
.bucket(&s3_bucket)
.key(&file_key)
.send()
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
(
s3_object_metadata.content_type().map(|v| v.to_owned()),
s3_object_metadata.content_length(),
)
};
let file_chunk_length = if s3_object_content_length.is_some() {
cmp::min(
query.read_bytes_length,
s3_object_content_length.unwrap() - query.read_bytes_from,
)
} else {
query.read_bytes_length
};
let content_type: WindmillContentType;
let content_preview = match s3_object_mime_type.as_deref() {
Some("text/csv") => {
content_type = WindmillContentType::Csv;
read_s3_csv_object_head(
&s3_client,
&s3_bucket,
&file_key,
file_chunk_length,
query.csv_separator,
)
.await
}
Some(mt)
if mt.starts_with("text/")
|| mt == "application/json"
|| mt == "application/x-yaml" =>
{
content_type = WindmillContentType::RawText;
read_s3_text_object_head(
&s3_client,
&s3_bucket,
&file_key,
query.read_bytes_from,
file_chunk_length,
)
.await
}
mt_opt => {
// sometimes S3 doesn't infer the content type on upload. Guess it from the file extension
if file_key.to_lowercase().ends_with(".parquet") {
content_type = WindmillContentType::Parquet;
read_s3_parquet_object_head(&s3_resource, &file_key).await
} else if file_key.to_lowercase().ends_with(".csv") {
content_type = WindmillContentType::Csv;
read_s3_csv_object_head(
&s3_client,
&s3_bucket,
&file_key,
file_chunk_length,
query.csv_separator,
)
.await
} else {
content_type = WindmillContentType::Unknown;
let msg = match mt_opt {
Some(mt) => {
format!("Preview is not available for content of type '{}'", mt).to_string()
}
None => "Preview is not available. Content type is unknown or not supported"
.to_string(),
};
Err(error::Error::ExecutionErr(msg))
}
}
};
let response: LoadFilePreviewResponse = match content_preview {
Ok(content) => LoadFilePreviewResponse {
content_type: content_type,
content: Some(content),
msg: None,
},
Err(err) => LoadFilePreviewResponse {
content_type: content_type,
content: None,
msg: Some(err.to_string()),
},
};
return Ok(Json(response));
}
async fn get_workspace_s3_resource<'c>(
authed: &ApiAuthed,
user_db: &UserDB,
db: &DB,
token: &str,
w_id: &str,
) -> error::Result<Option<S3Resource>> {
@@ -261,6 +519,7 @@ async fn get_workspace_s3_resource<'c>(
let interpolated_value = transform_json_value(
authed,
user_db,
db,
&w_id,
resource_path_json_value,
&Option::None,
@@ -276,18 +535,10 @@ async fn get_workspace_s3_resource<'c>(
fn build_s3_client(s3_resource_ref: &S3Resource) -> aws_sdk_s3::Client {
let s3_resource = s3_resource_ref.clone();
let endpoint_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)
};
let endpoint = render_endpoint(&s3_resource);
let mut s3_config_builder = aws_sdk_s3::Config::builder()
.endpoint_url(endpoint_with_prefix)
.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(
@@ -304,3 +555,176 @@ fn build_s3_client(s3_resource_ref: &S3Resource) -> aws_sdk_s3::Client {
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![
(AmazonS3ConfigKey::Region, s3_resource.region),
(AmazonS3ConfigKey::Bucket, s3_resource.bucket),
(
AmazonS3ConfigKey::Endpoint,
render_endpoint(s3_resource_ref),
),
(
AmazonS3ConfigKey::Client(ClientConfigKey::AllowHttp),
(!s3_resource.use_ssl).to_string(),
),
(
AmazonS3ConfigKey::VirtualHostedStyleRequest,
(!s3_resource.path_style).to_string(),
),
];
if let Some(access_key) = s3_resource.access_key {
s3_configs.push((AmazonS3ConfigKey::AccessKeyId, access_key));
}
if let Some(secret_key) = s3_resource.secret_key {
s3_configs.push((AmazonS3ConfigKey::SecretAccessKey, secret_key));
}
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,
file_key: &str,
from_char: i64,
length: i64,
) -> error::Result<String> {
let s3_object = s3_client
.get_object()
.range(format!("bytes={}-{}", from_char, length).to_string())
.bucket(s3_bucket)
.key(file_key)
.send()
.await
.map_err(|err| {
tracing::warn!("Error fetching text file from S3: {:?}", err);
error::Error::InternalErr(err.to_string())
})?;
let payload = s3_object
.body
.collect()
.await
.map_err(|err| {
tracing::warn!(
"Error reading raw text file {}. Error was: {:?}",
file_key,
err
);
error::Error::InternalErr("File encoding is not supported".to_string())
})?
.into_bytes()
.to_vec();
let file_header_str = String::from_utf8(payload).map_err(|err| {
tracing::warn!(
"Encoding of file {} unsupported. Error was: {:?}",
file_key,
err
);
error::Error::InternalErr("File encoding is not supported".to_string())
})?;
return Ok(file_header_str);
}
async fn read_s3_parquet_object_head(
s3_resource_ref: &S3Resource,
file_key: &str,
) -> error::Result<String> {
let s3_cloud_config = build_polars_s3_config(s3_resource_ref);
let args: ScanArgsParquet = ScanArgsParquet {
n_rows: Some(1),
cache: false,
parallel: polars::io::parquet::ParallelStrategy::Auto,
rechunk: false,
row_count: None,
low_memory: false,
use_statistics: false,
hive_partitioning: false,
cloud_options: Some(s3_cloud_config),
};
let file_key_clone = file_key.to_string();
let s3_bucket_clone = s3_resource_ref.bucket.to_string();
let polars_df_result = tokio::task::spawn_blocking(move || {
let s3_file_key = format!("s3://{}/{}", s3_bucket_clone, file_key_clone);
let lzdf_result = LazyFrame::scan_parquet(s3_file_key, args);
match lzdf_result {
Err(err) => {
tracing::warn!("Error fetching parquet file from S3: {:?}", err);
return Err(error::Error::InternalErr(err.to_string()));
}
Ok(lzdf) => {
let df = lzdf
.select(&[col("*")])
.limit(10) // for now read only first 10 lines
.collect()
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
return Ok(format!("{:?}", df).to_string());
}
}
})
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
return polars_df_result;
}
async fn read_s3_csv_object_head(
s3_client: &aws_sdk_s3::Client,
s3_bucket: &str,
file_key: &str,
length: i64,
separator: Option<String>,
) -> error::Result<String> {
let separator_final = if let Some(separator_char) = separator {
if separator_char.len() != 1 {
return Err(error::Error::BadRequest(
"Separator must be a single character".to_string(),
));
}
separator_char.as_bytes()[0]
} else {
",".as_bytes()[0] // polars uses the comma as default, doing the same here
};
let s3_object = s3_client
.get_object()
.bucket(s3_bucket)
.range(format!("bytes=0-{}", length).to_string())
.key(file_key)
.send()
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
// TODO: polars does not seem to support lazy csv reader, unfortunately. We can implement it ourselves if needed
// Right now it's fine b/c we limit the download from AWS to 32MB. We should recomment users to use parquet
// for larger files
let file_content_bytes = s3_object
.body
.collect()
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?
.into_bytes();
let cursor = std::io::Cursor::new(file_content_bytes);
let csv_df = CsvReader::new(cursor)
.with_n_rows(Some(10)) // for now read only first 10 lines
.with_separator(separator_final)
.finish()
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
return Ok(format!("{:?}", csv_df).to_string());
}

View File

@@ -36,7 +36,7 @@ use sqlx::{query_scalar, types::Uuid, FromRow, Postgres, Transaction};
use tower_http::cors::{Any, CorsLayer};
use urlencoding::encode;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::worker::{CUSTOM_TAGS_PER_WORKSPACE, SERVER_CONFIG};
use windmill_common::worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE, SERVER_CONFIG};
use windmill_common::{
db::UserDB,
error::{self, to_anyhow, Error},
@@ -50,8 +50,8 @@ use windmill_common::{
};
use windmill_common::{get_latest_deployed_hash_for_path, BASE_URL};
use windmill_queue::{
add_completed_job_error, empty_args, get_queued_job, job_is_complete, push, CanceledBy,
PushArgs, PushIsolationLevel,
add_completed_job_error, get_queued_job, get_result_by_id_from_running_flow, job_is_complete,
push, CanceledBy, PushArgs, PushIsolationLevel,
};
pub fn workspaced_service() -> Router {
@@ -364,7 +364,7 @@ async fn get_job(
async fn get_job_internal(db: &DB, workspace_id: &str, job_id: Uuid) -> error::Result<Job> {
let cjob_maybe = sqlx::query_as::<_, CompletedJob>("SELECT
id, workspace_id, parent_job, created_by, created_at, duration_ms, success, script_hash, script_path,
CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '{\"reason\": \"WINDMILL_TOO_BIG\"}'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '{\"reason\": \"WINDMILL_TOO_BIG\"}'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, right(logs, 20000000) as logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language, started_at, is_skipped,
raw_lock, email, visible_to_owner, mem_peak, tag, priority
FROM completed_job WHERE id = $1 AND workspace_id = $2")
@@ -378,7 +378,7 @@ async fn get_job_internal(db: &DB, workspace_id: &str, job_id: Uuid) -> error::R
} else {
let job_o = sqlx::query_as::<_, QueuedJob>(
"SELECT id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for, running,
script_hash, script_path, CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '{\"reason\": \"WINDMILL_TOO_BIG\"}'::jsonb END as args, logs, raw_code, canceled, canceled_by, canceled_reason, last_ping,
script_hash, script_path, CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '{\"reason\": \"WINDMILL_TOO_BIG\"}'::jsonb END as args, right(logs, 20000000) as logs, raw_code, canceled, canceled_by, canceled_reason, last_ping,
job_kind, env_id, schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language,
suspend, suspend_until, same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id, cache_ttl, priority
@@ -1564,6 +1564,53 @@ fn decode_payload<D: DeserializeOwned>(t: String) -> anyhow::Result<D> {
serde_json::from_slice(vec.as_slice()).context("invalid json")
}
#[derive(Deserialize)]
pub struct DecodeQuery {
pub include_query: Option<String>,
}
#[derive(Deserialize)]
pub struct IncludeQuery {
pub include_query: Option<String>,
}
pub struct DecodeQueries(pub HashMap<String, Box<RawValue>>);
#[axum::async_trait]
impl<S> FromRequest<S, axum::body::Body> for DecodeQueries
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(
req: Request<axum::body::Body>,
_state: &S,
) -> Result<Self, Self::Rejection> {
let query = req.uri().query().unwrap_or("");
let include_query = serde_urlencoded::from_str::<IncludeQuery>(query)
.map(|x| x.include_query)
.ok()
.flatten()
.unwrap_or_default();
let parse_query_args = include_query
.split(",")
.map(|s| s.to_string())
.collect::<Vec<_>>();
let mut args = HashMap::new();
if !parse_query_args.is_empty() {
let queries =
serde_urlencoded::from_str::<HashMap<String, String>>(query).unwrap_or_default();
parse_query_args.iter().for_each(|h| {
if let Some(v) = queries.get(h) {
args.insert(h.to_string(), to_raw_value(v));
}
});
}
Ok(DecodeQueries(args))
}
}
pub fn add_raw_string(
raw_string: Option<String>,
mut args: serde_json::Map<String, serde_json::Value>,
@@ -1802,33 +1849,25 @@ struct Guard {
done: bool,
id: Uuid,
w_id: String,
db: UserDB,
authed: ApiAuthed,
db: DB,
}
impl Drop for Guard {
fn drop(&mut self) {
if !&self.done {
let id = self.id;
let username = self.authed.username.clone();
let w_id = self.w_id.clone();
let db = self.db.clone();
let authed = self.authed.clone();
tracing::info!("http connection broke, marking job {id} as canceled");
tokio::spawn(async move {
let tx = db.begin(&authed).await.ok();
if let Some(mut tx) = tx {
let _ = sqlx::query!(
"UPDATE queue SET canceled = true, canceled_reason = 'http connection broke', canceled_by = $1 WHERE id = $2 AND workspace_id = $3",
username,
let _ = sqlx::query!(
"UPDATE queue SET canceled = true, canceled_reason = 'http connection broke', canceled_by = queue.created_by WHERE id = $1 AND workspace_id = $2",
id,
w_id
)
.execute(&mut *tx)
.execute(&db)
.await;
let _ = tx.commit().await;
}
});
}
}
@@ -1840,10 +1879,10 @@ pub struct WindmillStatusCode {
result: Option<Box<RawValue>>,
}
async fn run_wait_result<T>(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
db: &DB,
uuid: Uuid,
Path((w_id, _)): Path<(String, T)>,
node_id: Option<String>,
) -> error::Result<Response> {
let mut result;
let timeout = SERVER_CONFIG.read().await.timeout_wait_result.clone();
@@ -1853,27 +1892,29 @@ async fn run_wait_result<T>(
(timeout * 1000) as u64
};
let mut g = Guard {
done: false,
id: uuid,
w_id: w_id.clone(),
db: user_db.clone(),
authed: authed.clone(),
};
let mut g = Guard { done: false, id: uuid, w_id: w_id.clone(), db: db.clone() };
let fast_poll_duration = *WAIT_RESULT_FAST_POLL_DURATION_SECS as u64 * 1000;
let mut accumulated_delay = 0 as u64;
loop {
let mut tx = user_db.clone().begin(&authed).await?;
result =
sqlx::query("SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(uuid)
.bind(&w_id)
.fetch_optional(&mut *tx)
.await?;
drop(tx);
if let Some(node_id) = node_id.as_ref() {
result = get_result_by_id_from_running_flow(&db, &w_id, &uuid, node_id, None)
.await
.ok();
} else {
let row =
sqlx::query("SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(uuid)
.bind(&w_id)
.fetch_optional(db)
.await?;
if let Some(row) = row {
result = Some(RawResult::from_row(&row)?.result.to_owned());
} else {
result = None;
}
}
if result.is_some() {
break;
@@ -1892,7 +1933,6 @@ async fn run_wait_result<T>(
}
if let Some(result) = result {
g.done = true;
let result = RawResult::from_row(&result)?.result;
let status_code = serde_json::from_str::<WindmillStatusCode>(result.get());
match status_code {
@@ -1956,6 +1996,7 @@ pub async fn run_wait_result_job_by_path_get(
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
DecodeQueries(queries): DecodeQueries,
) -> error::Result<Response> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -1968,13 +2009,17 @@ pub async fn run_wait_result_job_by_path_get(
.map(decode_payload)
.map(|x| x.map_err(|e| Error::InternalErr(e.to_string())));
let payload_args = if let Some(payload) = payload_r {
let mut payload_args = if let Some(payload) = payload_r {
payload?
} else {
HashMap::new()
};
queries.iter().for_each(|(k, v)| {
payload_args.insert(k.to_string(), v.clone());
});
let args = PushArgs { extra: payload_args, args: sqlx::types::Json(empty_args()) };
let inner_args: HashMap<String, Box<RawValue>> = HashMap::new();
let args = PushArgs { extra: payload_args, args: sqlx::types::Json(inner_args) };
check_queue_too_long(&db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
let script_path = script_path.to_path();
@@ -2010,7 +2055,7 @@ pub async fn run_wait_result_job_by_path_get(
.await?;
tx.commit().await?;
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_path))).await
run_wait_result(&db, uuid, Path((w_id, script_path)), None).await
}
pub async fn run_wait_result_flow_by_path_get(
@@ -2020,8 +2065,8 @@ pub async fn run_wait_result_flow_by_path_get(
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
DecodeQueries(queries): DecodeQueries,
) -> error::Result<Response> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -2035,12 +2080,16 @@ pub async fn run_wait_result_flow_by_path_get(
.map(decode_payload)
.map(|x| x.map_err(|e| Error::InternalErr(e.to_string())));
let payload_args = if let Some(payload) = payload_r {
let mut payload_args = if let Some(payload) = payload_r {
payload?
} else {
HashMap::new()
};
queries.iter().for_each(|(k, v)| {
payload_args.insert(k.to_string(), v.clone());
});
let args = PushArgs { extra: payload_args, args: sqlx::types::Json(HashMap::new()) };
run_wait_result_flow_by_path_internal(
@@ -2119,7 +2168,7 @@ async fn run_wait_result_script_by_path_internal(
.await?;
tx.commit().await?;
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_path))).await
run_wait_result(&db, uuid, Path((w_id, script_path)), None).await
}
pub async fn run_wait_result_script_by_hash(
@@ -2187,7 +2236,7 @@ pub async fn run_wait_result_script_by_hash(
.await?;
tx.commit().await?;
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_hash))).await
run_wait_result(&db, uuid, Path((w_id, script_hash)), None).await
}
pub async fn run_wait_result_flow_by_path(
@@ -2225,15 +2274,15 @@ async fn run_wait_result_flow_by_path_internal(
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let (tag, dedicated_worker) = sqlx::query!(
"SELECT tag, dedicated_worker from flow WHERE path = $1 and workspace_id = $2",
let (tag, dedicated_worker, early_return) = sqlx::query!(
"SELECT tag, dedicated_worker, value->>'early_return' as early_return from flow WHERE path = $1 and workspace_id = $2",
flow_path,
w_id
)
.fetch_optional(&db)
.await?
.map(|x| (x.tag, x.dedicated_worker))
.unwrap_or_else(|| (None, None));
.map(|x| (x.tag, x.dedicated_worker, x.early_return))
.unwrap_or_else(|| (None, None, None));
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -2264,7 +2313,7 @@ async fn run_wait_result_flow_by_path_internal(
.await?;
tx.commit().await?;
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, flow_path))).await
run_wait_result(&db, uuid, Path((w_id, flow_path)), early_return).await
}
async fn run_preview_job(
@@ -2829,7 +2878,7 @@ async fn get_completed_job<'a>(
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
let job_o = sqlx::query("SELECT id, workspace_id, parent_job, created_by, created_at, duration_ms, success, script_hash, script_path,
CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, right(logs, 20000000) as logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language, started_at, is_skipped,
raw_lock, email, visible_to_owner, mem_peak, tag, priority FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(id)

View File

@@ -112,7 +112,7 @@ pub async fn run_server(
rsmq: Option<rsmq_async::MultiplexedRsmq>,
addr: SocketAddr,
mut rx: tokio::sync::broadcast::Receiver<()>,
port_tx: tokio::sync::oneshot::Sender<u16>,
port_tx: tokio::sync::oneshot::Sender<String>,
server_mode: bool,
) -> anyhow::Result<()> {
if let Some(mut rsmq) = rsmq.clone() {
@@ -284,7 +284,7 @@ pub async fn run_server(
);
port_tx
.send(server.local_addr().port())
.send(format!("http://localhost:{}", server.local_addr().port()))
.expect("Failed to send port");
let server = server.with_graceful_shutdown(async {

View File

@@ -102,9 +102,13 @@ async fn proxy(
&openai_resource_path,
&w_id
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
.fetch_optional(&mut *tx)
.await?
.ok_or_else(|| {
create_openai_json_error(format!(
"Could not find the OpenAI resource at path {openai_resource_path}, update the resource path in the workspace settings"
))
})?;
if resource.is_none() {
return Err(create_openai_json_error(
@@ -126,8 +130,9 @@ async fn proxy(
"https://api.openai.com/v1"
};
let url = format!("{}/{}", base_url, openai_path);
let mut request = HTTP_CLIENT
.post(base_url.to_string() + "/" + &openai_path)
.post(url)
.header("content-type", "application/json")
.body(body);

View File

@@ -247,6 +247,7 @@ async fn list_resources(
async fn get_resource(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<ListableResource> {
let path = path.to_path();
@@ -269,7 +270,9 @@ async fn get_resource(
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
if resource_o.is_none() {
explain_resource_perm_error(&path, &w_id, &db).await?;
}
let resource = not_found_if_none(resource_o, "Resource", path)?;
Ok(Json(resource))
}
@@ -295,6 +298,7 @@ async fn exists_resource(
async fn get_resource_value(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Option<serde_json::Value>> {
let path = path.to_path();
@@ -307,12 +311,55 @@ async fn get_resource_value(
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
if value_o.is_none() {
explain_resource_perm_error(&path, &w_id, &db).await?;
}
let value = not_found_if_none(value_o, "Resource", path)?;
Ok(Json(value))
}
async fn explain_resource_perm_error(
path: &str,
w_id: &str,
db: &sqlx::Pool<Postgres>,
) -> windmill_common::error::Result<()> {
let extra_perms = sqlx::query_scalar!(
"SELECT extra_perms from resource WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.fetch_optional(db)
.await?
.ok_or_else(|| Error::NotFound(format!("Resource {} not found", path)))?;
if path.starts_with("f/") {
let folder = path.split("/").nth(1).ok_or_else(|| {
Error::BadRequest(format!(
"path {} should have at least 2 components separated by /",
path
))
})?;
let folder_extra_perms = sqlx::query_scalar!(
"SELECT extra_perms from folder WHERE name = $1 AND workspace_id = $2",
folder,
w_id
)
.fetch_optional(db)
.await?;
return Err(Error::NotAuthorized(format!(
"Resource exists but you don't have access to it:\nresource perms: {}\nfolder perms: {}",
serde_json::to_string_pretty(&extra_perms).unwrap_or_default(), serde_json::to_string_pretty(&folder_extra_perms).unwrap_or_default()
)));
} else {
return Err(Error::NotAuthorized(format!(
"Resource exists but you don't have access to it:\nresource perms: {}",
serde_json::to_string_pretty(&extra_perms).unwrap_or_default()
)));
}
}
async fn custom_component(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -347,6 +394,7 @@ struct JobInfo {
async fn get_resource_value_interpolated(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path((w_id, path)): Path<(String, StripPath)>,
Query(job_info): Query<JobInfo>,
@@ -362,11 +410,23 @@ async fn get_resource_value_interpolated(
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
if value_o.is_none() {
explain_resource_perm_error(&path, &w_id, &db).await?;
}
let value = not_found_if_none(value_o, "Resource", path)?;
if let Some(value) = value {
Ok(Json(Some(
transform_json_value(&authed, &user_db, &w_id, value, &job_info.job_id, &token).await?,
transform_json_value(
&authed,
&user_db,
&db,
&w_id,
value,
&job_info.job_id,
&token,
)
.await?,
)))
} else {
Ok(Json(None))
@@ -379,6 +439,7 @@ use async_recursion::async_recursion;
pub async fn transform_json_value<'c>(
authed: &ApiAuthed,
user_db: &UserDB,
db: &DB,
workspace: &str,
v: Value,
job_id: &Option<Uuid>,
@@ -388,8 +449,8 @@ pub async fn transform_json_value<'c>(
Value::String(y) if y.starts_with("$var:") => {
let path = y.strip_prefix("$var:").unwrap();
let tx: Transaction<'_, Postgres> = user_db.clone().begin(authed).await?;
let v =
crate::variables::get_value_internal(tx, workspace, path, &authed.username).await?;
let v = crate::variables::get_value_internal(tx, db, workspace, path, &authed.username)
.await?;
Ok(Value::String(v))
}
Value::String(y) if y.starts_with("$res:") => {
@@ -408,7 +469,7 @@ pub async fn transform_json_value<'c>(
tx.commit().await?;
let v = not_found_if_none(v, "Resource", path)?;
if let Some(v) = v {
transform_json_value(authed, user_db, workspace, v, job_id, token).await
transform_json_value(authed, user_db, db, workspace, v, job_id, token).await
} else {
Ok(Value::Null)
}
@@ -466,7 +527,7 @@ pub async fn transform_json_value<'c>(
for (a, b) in m.clone().into_iter() {
m.insert(
a.clone(),
transform_json_value(authed, user_db, workspace, b, job_id, token).await?,
transform_json_value(authed, user_db, db, workspace, b, job_id, token).await?,
);
}
Ok(Value::Object(m))

View File

@@ -42,6 +42,7 @@ pub fn workspaced_service() -> Router {
.route("/delete/*path", delete(delete_schedule))
.route("/setenabled/*path", post(set_enabled))
.route("/setdefaulthandler", post(set_default_error_handler))
// .route("/catchup/*path", post(do_catchup).get(list_catchup))
}
pub fn global_service() -> Router {
@@ -455,6 +456,54 @@ pub async fn set_enabled(
))
}
// pub async fn do_catchup(
// authed: ApiAuthed,
// Extension(db): Extension<DB>,
// Extension(user_db): Extension<UserDB>,
// Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
// Path((w_id, path)): Path<(String, StripPath)>,
// Json(payload): Json<SetEnabled>,
// ) -> Result<String> {
// let mut tx: QueueTransaction<'_, rsmq_async::MultiplexedRsmq> =
// (rsmq, user_db.begin(&authed).await?).into();
// let path = path.to_path();
// let schedule_o = sqlx::query_as!(
// Schedule,
// "UPDATE schedule SET enabled = $1, email = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *",
// &payload.enabled,
// authed.email,
// path,
// w_id
// )
// .fetch_optional(&mut tx)
// .await?;
// let schedule = not_found_if_none(schedule_o, "Schedule", path)?;
// clear_schedule(tx.transaction_mut(), path, &w_id).await?;
// audit_log(
// &mut tx,
// &authed.username,
// "schedule.setenabled",
// ActionKind::Update,
// &w_id,
// Some(path),
// Some([("enabled", payload.enabled.to_string().as_ref())].into()),
// )
// .await?;
// if payload.enabled {
// tx = push_scheduled_job(&db, tx, schedule).await?;
// }
// tx.commit().await?;
// Ok(format!(
// "succesfully updated schedule at path {} to status {}",
// path, payload.enabled
// ))
// }
async fn delete_schedule(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -642,3 +691,9 @@ pub async fn clear_schedule<'c>(
pub struct SetEnabled {
pub enabled: bool,
}
#[derive(Deserialize)]
pub struct Catchup {
pub from: DateTime<Utc>,
pub to: Option<DateTime<Utc>>,
}

View File

@@ -814,6 +814,8 @@ async fn raw_script_by_path(
}
let path = path
.trim_end_matches(".py")
.trim_end_matches(".bun.ts")
.trim_end_matches(".deno.ts")
.trim_end_matches(".ts")
.trim_end_matches(".go")
.trim_end_matches(".sh");
@@ -1087,6 +1089,14 @@ async fn delete_script_by_path(
.await
.map_err(|e| Error::InternalErr(format!("deleting script by path {w_id}: {e}")))?;
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
path,
w_id
)
.execute(&db)
.await?;
audit_log(
&mut *tx,
&authed.username,

View File

@@ -94,6 +94,7 @@ pub fn global_service() -> Router {
"/tutorial_progress",
post(update_tutorial_progress).get(get_tutorial_progress),
)
.route("/leave_instance", post(leave_instance))
// .route("/list_invite_codes", get(list_invite_codes))
// .route("/create_invite_code", post(create_invite_code))
// .route("/signup", post(signup))
@@ -1416,6 +1417,29 @@ async fn add_user_to_workspace<'c>(
Ok(tx)
}
async fn leave_instance(
Extension(db): Extension<DB>,
ApiAuthed { email, username, .. }: ApiAuthed,
) -> Result<String> {
let mut tx = db.begin().await?;
sqlx::query!("DELETE FROM password WHERE email = $1", &email)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&username,
"workspaces.leave",
ActionKind::Delete,
"global",
Some(&email),
None,
)
.await?;
tx.commit().await?;
Ok(format!("Left instance",))
}
async fn update_workspace_user(
ApiAuthed { username, is_admin, .. }: ApiAuthed,
Extension(db): Extension<DB>,
@@ -1578,6 +1602,9 @@ async fn create_user(
));
}
#[cfg(not(feature = "enterprise"))]
_check_nb_of_user(&db).await?;
sqlx::query!(
"INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, \
company)
@@ -2100,19 +2127,37 @@ async fn impersonate(
Ok((StatusCode::CREATED, token))
}
#[derive(Deserialize)]
struct ListTokenQuery {
exclude_ephemeral: Option<bool>,
}
async fn list_tokens(
Extension(db): Extension<DB>,
ApiAuthed { email, .. }: ApiAuthed,
Query(query): Query<ListTokenQuery>,
) -> JsonResult<Vec<TruncatedToken>> {
let rows = sqlx::query_as!(
TruncatedToken,
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, \
last_used_at, scopes FROM token WHERE email = $1
ORDER BY created_at DESC",
email,
)
.fetch_all(&db)
.await?;
let rows = if query.exclude_ephemeral.unwrap_or(false) {
sqlx::query_as!(
TruncatedToken,
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, \
last_used_at, scopes FROM token WHERE email = $1 AND label != 'ephemeral-script'
ORDER BY created_at DESC",
email,
)
.fetch_all(&db)
.await?
} else {
sqlx::query_as!(
TruncatedToken,
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, \
last_used_at, scopes FROM token WHERE email = $1
ORDER BY created_at DESC",
email,
)
.fetch_all(&db)
.await?
};
Ok(Json(rows))
}
@@ -2290,16 +2335,26 @@ pub struct LoginUserInfo {
}
async fn _check_nb_of_user(db: &DB) -> Result<()> {
let nb_users =
let nb_users_sso =
sqlx::query_scalar!("SELECT COUNT(*) FROM password WHERE login_type != 'password'",)
.fetch_one(db)
.await?;
if nb_users.unwrap_or(0) >= 10 {
if nb_users_sso.unwrap_or(0) >= 10 {
return Err(Error::BadRequest(
"You have reached the maximum number of oauth users accounts (10) without an enterprise license"
.to_string(),
));
}
let nb_users = sqlx::query_scalar!("SELECT COUNT(*) FROM password",)
.fetch_one(db)
.await?;
if nb_users.unwrap_or(0) >= 50 {
return Err(Error::BadRequest(
"You have reached the maximum number of accounts (50) without an enterprise license"
.to_string(),
));
}
return Ok(());
}

View File

@@ -8,12 +8,12 @@
use windmill_common::{
error::{self, Error},
users::SUPERADMIN_SECRET_EMAIL,
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
DB,
};
pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> {
if email == SUPERADMIN_SECRET_EMAIL {
if email == SUPERADMIN_SECRET_EMAIL || email == SUPERADMIN_NOTIFICATION_EMAIL {
return Ok(());
}
let is_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)

View File

@@ -107,6 +107,7 @@ struct GetVariableQuery {
async fn get_variable(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Query(q): Query<GetVariableQuery>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<ListableVariable> {
@@ -128,7 +129,12 @@ async fn get_variable(
.fetch_optional(&mut *tx)
.await?;
let variable = not_found_if_none(variable_o, "Variable", &path)?;
let variable = if let Some(variable) = variable_o {
variable
} else {
explain_variable_perm_error(&path, &w_id, &db).await?;
unreachable!()
};
let decrypt_secret = q.decrypt_secret.unwrap_or(true);
@@ -172,17 +178,20 @@ async fn get_variable(
async fn get_value(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<String> {
let path = path.to_path();
let tx = user_db.begin(&authed).await?;
return get_value_internal(tx, &w_id, &path, &authed.username)
return get_value_internal(tx, &db, &w_id, &path, &authed.username)
.await
.map(Json);
}
pub async fn get_value_internal<'c>(
mut tx: Transaction<'c, Postgres>,
db: &DB,
w_id: &str,
path: &str,
username: &str,
@@ -194,7 +203,12 @@ pub async fn get_value_internal<'c>(
.fetch_optional(&mut *tx)
.await?;
let variable = not_found_if_none(variable_o, "Variable", &path)?;
let variable = if let Some(variable) = variable_o {
variable
} else {
explain_variable_perm_error(path, w_id, db).await?;
unreachable!()
};
let r = if variable.is_secret {
audit_log(
@@ -226,6 +240,45 @@ pub async fn get_value_internal<'c>(
Ok(r)
}
async fn explain_variable_perm_error(
path: &str,
w_id: &str,
db: &sqlx::Pool<Postgres>,
) -> windmill_common::error::Result<()> {
let extra_perms = sqlx::query_scalar!(
"SELECT extra_perms from variable WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.fetch_optional(db)
.await?
.ok_or_else(|| Error::NotFound(format!("Variable {} not found", path)))?;
if path.starts_with("f/") {
let folder = path.split("/").nth(1).ok_or_else(|| {
Error::BadRequest(format!(
"path {} should have at least 2 components separated by /",
path
))
})?;
let folder_extra_perms = sqlx::query_scalar!(
"SELECT extra_perms from folder WHERE name = $1 AND workspace_id = $2",
folder,
w_id
)
.fetch_optional(db)
.await?;
return Err(Error::NotAuthorized(format!(
"Variable exists but you don't have access to it:\nvariable perms: {}\nfolder perms: {}",
serde_json::to_string_pretty(&extra_perms).unwrap_or_default(), serde_json::to_string_pretty(&folder_extra_perms).unwrap_or_default()
)));
} else {
return Err(Error::NotAuthorized(format!(
"Variable exists but you don't have access to it:\nvariable perms: {}",
serde_json::to_string_pretty(&extra_perms).unwrap_or_default()
)));
}
}
async fn exists_variable(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,

View File

@@ -36,7 +36,7 @@ use magic_crypt::MagicCryptTrait;
#[cfg(feature = "enterprise")]
use stripe::CustomerId;
#[cfg(feature = "enterprise")]
use chrono::{TimeZone, Datelike};
use chrono::{TimeZone, Datelike, Timelike};
use uuid::Uuid;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::db::UserDB;
@@ -83,7 +83,8 @@ pub fn workspaced_service() -> Router {
.route("/edit_copilot_config", post(edit_copilot_config))
.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("/leave", post(leave_workspace));
#[cfg(feature = "enterprise")]
{
@@ -423,22 +424,25 @@ async fn stripe_checkout(
_ => params.customer_email = Some(&authed.email),
}
let now = Utc::now();
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
metadata: {
let mut map = std::collections::HashMap::new();
map.insert("workspace_id".to_string(), w_id.clone());
Some(map)
},
billing_cycle_anchor: Some({
// first of the next month (and possibly next year) at noon UTC
let now = Utc::now();
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()
};
date.timestamp()
}),
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())
},
..Default::default()
});
@@ -748,13 +752,17 @@ async fn edit_auto_invite(
// ));
// }
if ea.invite_all.is_some_and(|x| x) && *CLOUD_HOSTED {
let domain = if ea.invite_all.is_some_and(|x| x) {
if *CLOUD_HOSTED {
return Err(Error::BadRequest(
"invite_all is only available locally".to_string(),
));
}
let domain = email.split('@').last().unwrap();
} else {
"*"
}
} else {
email.split('@').last().unwrap()
};
let mut tx = db.begin().await?;
@@ -778,7 +786,7 @@ async fn edit_auto_invite(
sqlx::query!(
"INSERT INTO workspace_invite
(workspace_id, email, is_admin, operator)
SELECT $1::text, email, false, $3 FROM password WHERE $2::text = '*' OR email LIKE CONCAT('%', $2::text) AND NOT EXISTS (
SELECT $1::text, email, false, $3 FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS (
SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email
)
ON CONFLICT DO NOTHING",
@@ -1104,9 +1112,9 @@ 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?;
if nb_workspaces.unwrap_or(0) >= 3 {
if nb_workspaces.unwrap_or(0) >= 2 {
return Err(Error::BadRequest(
"You have reached the maximum number of workspaces (3 outside of default worskapce 'admins') without an enterprise license. Archive/delete another workspace to create a new one"
"You have reached the maximum number of workspaces (2 outside of default worskapce 'admins') without an enterprise license. Archive/delete another workspace to create a new one"
.to_string(),
));
}
@@ -1302,6 +1310,31 @@ async fn archive_workspace(
Ok(format!("Archived workspace {}", &w_id))
}
async fn leave_workspace(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
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?;
audit_log(
&mut *tx,
&username,
"workspaces.leave",
ActionKind::Delete,
&w_id,
Some(&email),
None,
)
.await?;
tx.commit().await?;
Ok(format!("Left workspace {}", &w_id))
}
async fn unarchive_workspace(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
@@ -1477,7 +1510,8 @@ async fn invite_user(
sqlx::query!(
"INSERT INTO workspace_invite
(workspace_id, email, is_admin, operator)
VALUES ($1, $2, $3, $4)",
VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, email)
DO UPDATE SET is_admin = $3, operator = $4",
&w_id,
nu.email,
nu.is_admin,

View File

@@ -95,7 +95,7 @@ pub struct FlowValue {
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_ttl: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ws_error_handler_muted: Option<bool>,
pub early_return: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
// Priority at the flow level
pub priority: Option<i16>,

View File

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

View File

@@ -167,6 +167,7 @@ pub async fn get_uid(db: &DB) -> Result<String> {
#[serde(rename_all = "lowercase")]
pub enum Mode {
Worker,
Agent,
Server,
Standalone,
}

View File

@@ -75,25 +75,6 @@ pub async fn get_reserved_variables(
step_id: Option<String>,
) -> [ContextualVariable; 15] {
let state_path = {
let flow_path = flow_path
.clone()
.unwrap_or_else(|| "NO_FLOW_PATH".to_string());
let script_path = path.clone().unwrap_or_else(|| "NO_JOB_PATH".to_string());
let schedule_path = schedule_path
.clone()
.map(|x| format!("/{x}"))
.unwrap_or_else(String::new);
let script_path = if script_path.ends_with("/") {
"NO_NAME".to_string()
} else {
script_path
};
format!("{permissioned_as}/{flow_path}/{script_path}{schedule_path}")
};
let state_path_2 = {
let trigger = if schedule_path.is_some() {
username.to_string()
} else {
@@ -200,13 +181,13 @@ pub async fn get_reserved_variables(
},
ContextualVariable {
name: "WM_STATE_PATH".to_string(),
value: state_path,
description: "State resource path unique to a script and its trigger (legacy, in a migration period against WM_STATE_PATH_NEW)".to_string(),
value: state_path.clone(),
description: "State resource path unique to a script and its trigger".to_string(),
},
ContextualVariable {
name: "WM_STATE_PATH_NEW".to_string(),
value: state_path_2,
description: "State resource path unique to a script and its trigger".to_string(),
value: state_path,
description: "State resource path unique to a script and its trigger (legacy)".to_string(),
},
ContextualVariable {
name: "WM_FLOW_STEP_ID".to_string(),

View File

@@ -38,4 +38,5 @@ futures-core.workspace = true
itertools.workspace = true
async-recursion.workspace = true
bigdecimal.workspace = true
axum.workspace = true
axum.workspace = true
serde_urlencoded.workspace = true

View File

@@ -50,13 +50,12 @@ use windmill_common::{
},
flows::{add_virtual_items_if_necessary, FlowModuleValue, FlowValue},
jobs::{
get_payload_tag_from_prefixed_path, script_path_to_payload, CompletedJob, JobKind,
JobPayload, QueuedJob, RawCode,
get_payload_tag_from_prefixed_path, CompletedJob, JobKind, JobPayload, QueuedJob, RawCode,
},
oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH,
schedule::Schedule,
scripts::{ScriptHash, ScriptLang},
users::SUPERADMIN_SECRET_EMAIL,
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
worker::{to_raw_value, WORKER_CONFIG},
DB, METRICS_ENABLED,
};
@@ -489,6 +488,10 @@ pub async fn add_completed_job<
}
tx.commit().await?;
tracing::info!(
"inserted completed job: {} (success: {success})",
queued_job.id
);
#[cfg(feature = "enterprise")]
if *CLOUD_HOSTED && !is_flow && _duration > 1000 {
@@ -516,7 +519,22 @@ pub async fn add_completed_job<
&& queued_job.parent_job.is_none()
&& !success
{
if let Err(e) = send_error_to_global_handler(rsmq.clone(), &queued_job, db, result).await {
let result = serde_json::from_str(
&serde_json::to_string(result.0).unwrap_or_else(|_| "{}".to_string()),
)
.unwrap_or_else(|_| json!({}));
let result = if result.is_object() || result.is_null() {
result
} else {
json!({ "error": result })
};
tracing::info!(
"Sending error of job {} to error handlers (if any)",
queued_job.id
);
if let Err(e) =
send_error_to_global_handler(rsmq.clone(), &queued_job, db, Json(&result)).await
{
tracing::error!(
"Could not run global error handler for job {}: {}",
&queued_job.id,
@@ -529,7 +547,7 @@ pub async fn add_completed_job<
&queued_job,
canceled_by.is_some(),
db,
result,
Json(&result),
)
.await
{
@@ -541,7 +559,6 @@ pub async fn add_completed_job<
}
}
tracing::debug!("Added completed job {}", queued_job.id);
// tracing::error!("4 {:?}", start.elapsed());
Ok(queued_job.id)
@@ -561,12 +578,13 @@ pub async fn run_error_handler<
is_global: bool,
) -> Result<(), Error> {
let w_id = &queued_job.workspace_id;
let script_w_id = if is_global { "admins" } else { w_id }; // script workspace id
let handler_w_id = if is_global { "admins" } else { w_id }; // script workspace id
let job_id = queued_job.id;
let (job_payload, tag) = script_path_to_payload(&error_handler_path, db, script_w_id).await?;
let (job_payload, tag) =
get_payload_tag_from_prefixed_path(&error_handler_path, db, handler_w_id).await?;
let mut extra = HashMap::new();
extra.insert("workspace_id".to_string(), to_raw_value(&w_id));
extra.insert("workspace_id".to_string(), to_raw_value(&handler_w_id));
extra.insert("job_id".to_string(), to_raw_value(&job_id));
extra.insert("path".to_string(), to_raw_value(&queued_job.script_path));
extra.insert(
@@ -586,7 +604,7 @@ pub async fn run_error_handler<
// TODO(gbouv): REMOVE THIS after December 1st 2023 and ping users to re-save their error handlers
if error_handler_path
.to_string()
.eq("hub/5792/workspace-or-schedule-error-handler-slack")
.eq("script/hub/5792/workspace-or-schedule-error-handler-slack")
{
// default slack error handler being used -> we need to inject the slack token
let slack_resource = format!("$res:{WORKSPACE_SLACK_BOT_TOKEN_PATH}");
@@ -610,7 +628,7 @@ pub async fn run_error_handler<
let (uuid, tx) = push(
&db,
tx,
script_w_id,
handler_w_id,
job_payload,
PushArgs { extra, args: result.to_owned() },
if is_global {
@@ -664,12 +682,19 @@ pub async fn send_error_to_global_handler<
result: Json<&'a T>,
) -> Result<(), Error> {
if let Some(ref global_error_handler) = *GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE {
let prefixed_global_error_handler_path = if global_error_handler.starts_with("script/")
|| global_error_handler.starts_with("flow/")
{
global_error_handler.clone()
} else {
format!("script/{}", global_error_handler)
};
run_error_handler(
rsmq,
queued_job,
db,
result,
global_error_handler,
&prefixed_global_error_handler_path,
None,
true,
)
@@ -728,13 +753,15 @@ pub async fn send_error_to_workspace_handler<
_ => None,
};
if !ws_error_handler_muted.unwrap_or(false) {
let muted = ws_error_handler_muted.unwrap_or(false);
if !muted {
tracing::info!("workspace error handled for job {}", &queued_job.id);
run_error_handler(
rsmq,
queued_job,
db,
result,
&error_handler.strip_prefix("script/").unwrap(),
&error_handler,
error_handler_extra_args,
false,
)
@@ -1430,7 +1457,13 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
, last_ping = now()
, suspend_until = null
WHERE id = $1
RETURNING *",
RETURNING id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for,
running, script_hash, script_path, args, right(logs, 20000000) as logs, raw_code, canceled, canceled_by,
canceled_reason, last_ping, job_kind, env_id, schedule_path, permissioned_as,
flow_status, raw_flow, is_flow_step, language, suspend, suspend_until,
same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s,
timeout, flow_step_id, cache_ttl, priority",
)
.bind(uuid)
.fetch_optional(db)
@@ -1481,7 +1514,13 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *")
RETURNING id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for,
running, script_hash, script_path, args, right(logs, 20000000) as logs, raw_code, canceled, canceled_by,
canceled_reason, last_ping, job_kind, env_id, schedule_path, permissioned_as,
flow_status, raw_flow, is_flow_step, language, suspend, suspend_until,
same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s,
timeout, flow_step_id, cache_ttl, priority")
.bind(tags)
.fetch_optional(db)
.await?
@@ -1513,7 +1552,13 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *",
RETURNING id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for,
running, script_hash, script_path, args, right(logs, 20000000) as logs, raw_code, canceled, canceled_by,
canceled_reason, last_ping, job_kind, env_id, schedule_path, permissioned_as,
flow_status, raw_flow, is_flow_step, language, suspend, suspend_until,
same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s,
timeout, flow_step_id, cache_ttl, priority",
)
.bind(priority_tags.tags.clone())
.fetch_optional(db)
@@ -1544,7 +1589,7 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
}
#[derive(FromRow)]
struct ResultR {
pub struct ResultR {
result: Option<Json<Box<RawValue>>>,
}
@@ -1596,7 +1641,7 @@ pub async fn get_result_by_id(
}
#[async_recursion]
async fn get_result_by_id_from_running_flow(
pub async fn get_result_by_id_from_running_flow(
db: &Pool<Postgres>,
w_id: &str,
flow_id: &Uuid,
@@ -1959,12 +2004,9 @@ where
let content_type_header = headers_map.get(CONTENT_TYPE);
let content_type = content_type_header.and_then(|value| value.to_str().ok());
let query = Query::<RequestQuery>::try_from_uri(req.uri()).unwrap().0;
let extra = build_extra(&headers_map, query.include_header);
let raw = query.raw.as_ref().is_some_and(|x| *x);
(
content_type,
build_extra(&headers_map, query.include_header),
raw,
)
(content_type, extra, raw)
};
if content_type.is_none() || content_type.unwrap().starts_with("application/json") {
@@ -2047,7 +2089,7 @@ impl PushArgs<HashMap<String, Box<RawValue>>> {
}
}
pub fn empty_args() -> Box<RawValue> {
pub fn empty_result() -> Box<RawValue> {
return JsonRawValue::from_string("{}".to_string()).unwrap();
}
@@ -2071,8 +2113,8 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
job_payload: JobPayload,
args: T,
user: &str,
email: &str,
permissioned_as: String,
mut email: &str,
mut permissioned_as: String,
scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
schedule_path: Option<String>,
parent_job: Option<Uuid>,
@@ -2225,6 +2267,10 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
priority,
),
JobPayload::ScriptHub { path } => {
if path == "hub/7771/slack" {
permissioned_as = SUPERADMIN_NOTIFICATION_EMAIL.to_string();
email = SUPERADMIN_NOTIFICATION_EMAIL;
}
(
None,
Some(path),

View File

@@ -60,6 +60,7 @@ deno_console.workspace = true
deno_url.workspace = true
deno_core.workspace = true
deno_ast.workspace = true
deno_tls.workspace = true
postgres-native-tls.workspace = true
native-tls.workspace = true
mysql_async.workspace = true

View File

@@ -220,7 +220,7 @@ fn convert_val(arg_t: String, arg_v: Value) -> Value {
match arg_t.as_str() {
"timestamp" | "datetime" => {
v = v + ":00";
v = v.trim_end_matches("Z").to_string();
}
"date" => {
let arr = v.split("T").collect::<Vec<&str>>();
@@ -235,7 +235,7 @@ fn convert_val(arg_t: String, arg_v: Value) -> Value {
let arr = v.split("T").collect::<Vec<&str>>();
match arr.as_slice() {
[_, time] => {
v = time.to_string() + ":00";
v = time.trim_end_matches("Z").to_string();
}
_ => {}
}

View File

@@ -16,7 +16,7 @@ use crate::{
start_child_process, write_file, write_file_binary,
},
AuthedClientBackgroundTask, BUN_CACHE_DIR, BUN_PATH, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV,
NPM_CONFIG_REGISTRY, NSJAIL_PATH, PATH_ENV, TZ_ENV,
NSJAIL_PATH, PATH_ENV, TZ_ENV,
};
use tokio::{
@@ -512,7 +512,7 @@ plugin(p)
}
pub async fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String, String> {
let mut bun_envs: HashMap<String, String> = HashMap::from([
let bun_envs: HashMap<String, String> = HashMap::from([
(String::from("PATH"), PATH_ENV.clone()),
(String::from("HOME"), HOME_ENV.clone()),
(String::from("TZ"), TZ_ENV.clone()),
@@ -529,10 +529,6 @@ pub async fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String
BUN_CACHE_DIR.to_string(),
),
]);
if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() {
bun_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
}
return bun_envs;
}

View File

@@ -41,6 +41,8 @@ lazy_static::lazy_static! {
.map(|x| format!(";{x}"))
.unwrap_or_else(|| String::new());
static ref DENO_CERT: String = std::env::var("DENO_CERT").ok().unwrap_or_else(|| String::new());
static ref DENO_TLS_CA_STORE: String = std::env::var("DENO_TLS_CA_STORE").ok().unwrap_or_else(|| String::new());
}
async fn get_common_deno_proc_envs(
@@ -70,6 +72,12 @@ async fn get_common_deno_proc_envs(
if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() {
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
}
if DENO_CERT.len() > 0 {
deno_envs.insert(String::from("DENO_CERT"), DENO_CERT.clone());
}
if DENO_TLS_CA_STORE.len() > 0 {
deno_envs.insert(String::from("DENO_TLS_CA_STORE"), DENO_TLS_CA_STORE.clone());
}
return deno_envs;
}

View File

@@ -50,7 +50,7 @@ pub async fn handle_go_job(
) -> Result<Box<RawValue>, Error> {
//go does not like executing modules at temp root
let job_dir = &format!("{job_dir}/go");
let bin_path = Some(format!(
let bin_path = format!(
"{}/{}",
GO_BIN_CACHE_DIR,
calculate_hash(&format!(
@@ -61,13 +61,8 @@ pub async fn handle_go_job(
.map(|x| x.to_string())
.unwrap_or_default()
))
));
let bin_exists = if let Some(bin_path) = bin_path.clone() {
tokio::fs::metadata(bin_path).await.is_ok()
} else {
false
};
);
let bin_exists = tokio::fs::metadata(&bin_path).await.is_ok();
let (skip_go_mod, skip_tidy) = if bin_exists {
create_dir(job_dir).await?;
@@ -215,12 +210,11 @@ func Run(req Req) (interface{{}}, error){{
)
.await?;
if let Some(bin_path) = bin_path.clone() {
tokio::fs::copy(format!("{job_dir}/main"), &bin_path).await?;
logs.push_str(&format!("write cached binary: {}\n", bin_path));
}
create_dir(&bin_path).await?;
tokio::fs::copy(format!("{job_dir}/main"), format!("{bin_path}/main")).await?;
logs.push_str(&format!("write cached binary: {}\n", bin_path));
} else {
let path = bin_path.unwrap().clone();
let path = format!("{bin_path}/main");
logs.push_str(&format!("found cached binary: {path}\n"));
tokio::fs::copy(&path, format!("{job_dir}/main"))
.await

View File

@@ -4,6 +4,7 @@ use serde_json::{json, value::RawValue};
use sqlx::types::Json;
use windmill_common::error::Error;
use windmill_common::jobs::QueuedJob;
use windmill_parser_graphql::parse_graphql_sig;
use windmill_queue::HTTP_CLIENT;
use serde::Deserialize;
@@ -48,9 +49,26 @@ pub async fn do_graphql(
return Err(Error::BadRequest("Missing api argument".to_string()));
};
// variables is job_args except for api
let mut variables = HashMap::new();
let sig = parse_graphql_sig(&query)
.map_err(|x| Error::ExecutionErr(x.to_string()))?
.args;
if let Some(job_args) = job_args {
for arg in &sig {
variables.insert(
arg.name.clone(),
job_args
.get(&arg.name)
.map(|x| x.to_owned())
.unwrap_or_default(),
);
}
}
let mut request = HTTP_CLIENT.post(api.base_url).json(&json!({
"query": query,
"variables": job_args
"variables": variables
}));
if let Some(token) = &api.bearer_token {

View File

@@ -6,16 +6,17 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc, env, io::{BufReader, self}};
use deno_ast::{ParseParams, SourceTextInfo};
use deno_core::{
op, serde_v8,
v8::IsolateHandle,
v8::{self},
Extension, JsRuntime, Op, OpState, RuntimeOptions, Snapshot,
Extension, JsRuntime, Op, OpState, RuntimeOptions, Snapshot, error::AnyError,
};
use deno_fetch::FetchPermissions;
use deno_tls::{rustls::RootCertStore, rustls_pemfile};
use deno_web::{BlobStore, TimersPermission};
use itertools::Itertools;
use lazy_static::lazy_static;
@@ -38,6 +39,33 @@ pub struct IdContext {
pub previous_id: String,
}
pub struct ContainerRootCertStoreProvider {
root_cert_store: RootCertStore,
}
impl ContainerRootCertStoreProvider {
fn new() -> ContainerRootCertStoreProvider {
return ContainerRootCertStoreProvider {
root_cert_store: deno_tls::create_default_root_cert_store(),
}
}
fn add_certificate(&mut self, cert_path: String) -> io::Result<()> {
let cert_file = std::fs::File::open(cert_path)?;
let mut reader = BufReader::new(cert_file);
let pem_file = rustls_pemfile::certs(&mut reader)?;
self.root_cert_store.add_parsable_certificates(&pem_file);
Ok(())
}
}
impl deno_tls::RootCertStoreProvider for ContainerRootCertStoreProvider {
fn get_or_try_init(&self) -> Result<&RootCertStore, AnyError> {
Ok(&self.root_cert_store)
}
}
pub struct PermissionsContainer;
impl FetchPermissions for PermissionsContainer {
@@ -532,6 +560,18 @@ pub async fn eval_fetch_timeout(
..Default::default()
};
let deno_fetch_options = if let Some(cert_path) = env::var("DENO_CERT").ok() {
let mut cert_store_provider = ContainerRootCertStoreProvider::new();
cert_store_provider.add_certificate(cert_path)?;
deno_fetch::Options {
root_cert_store_provider: Some(Arc::new(cert_store_provider)),
..Default::default()
}
} else {
Default::default()
};
let exts: Vec<Extension> = vec![
deno_webidl::deno_webidl::init_ops(),
deno_url::deno_url::init_ops(),
@@ -540,9 +580,7 @@ pub async fn eval_fetch_timeout(
Arc::new(BlobStore::default()),
None,
),
deno_fetch::deno_fetch::init_ops::<PermissionsContainer>(
Default::default(),
),
deno_fetch::deno_fetch::init_ops::<PermissionsContainer>(deno_fetch_options),
ext
];
@@ -645,8 +683,8 @@ import("file:///eval.ts").then((module) => module.main(...args)).then(JSON.strin
let local = v8::Local::new(scope, global);
// Deserialize a `v8` object into a Rust type using `serde_v8`,
// in this case deserialize to a JSON `Value`.
let r = serde_v8::from_v8::<String>(scope, local)?;
Ok(unsafe_raw(r))
let r = serde_v8::from_v8::<Option<String>>(scope, local)?;
Ok(unsafe_raw(r.unwrap_or_else(|| "null".to_string())))
}
#[op]

View File

@@ -1,3 +1,4 @@
use std::net::IpAddr;
use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::Arc;
use std::time::Duration;
@@ -404,6 +405,9 @@ pub fn pg_cell_to_json_value(
Type::UUID => get_basic(row, column, column_i, |a: uuid::Uuid| {
Ok(JSONValue::String(a.to_string()))
})?,
Type::INET => get_basic(row, column, column_i, |a: IpAddr| {
Ok(JSONValue::String(a.to_string()))
})?,
Type::JSON | Type::JSONB => get_basic(row, column, column_i, |a: JSONValue| Ok(a))?,
Type::FLOAT4 => get_basic(row, column, column_i, |a: f32| {
Ok(f64_to_json_number(a.into())?)

View File

@@ -30,6 +30,7 @@ lazy_static::lazy_static! {
static ref PIP_INDEX_URL: Option<String> = std::env::var("PIP_INDEX_URL").ok();
static ref PIP_TRUSTED_HOST: Option<String> = std::env::var("PIP_TRUSTED_HOST").ok();
static ref PIP_INDEX_CERT: Option<String> = std::env::var("PIP_INDEX_CERT").ok();
static ref RELATIVE_IMPORT_REGEX: Regex = Regex::new(r#"(import|from)\s(((u|f)\.)|\.)"#).unwrap();
@@ -121,6 +122,9 @@ pub async fn pip_compile(
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
args.extend(["--trusted-host", host]);
}
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
args.extend(["--cert", cert_path]);
}
let mut child_cmd = Command::new("pip-compile");
child_cmd
@@ -410,6 +414,13 @@ async fn prepare_wrapper(
.to_lowercase();
let module_dir = format!("{}/{}", job_dir, dirs);
tokio::fs::create_dir_all(format!("{module_dir}/")).await?;
let last = if last.starts_with(|x: char| x.is_ascii_digit()) {
format!("_{}", last)
} else {
last
};
let _ = write_file(&module_dir, &format!("{last}.py"), inner_content).await?;
if relative_imports {
let _ = write_file(job_dir, "loader.py", RELATIVE_PYTHON_LOADER).await?;
@@ -484,11 +495,6 @@ if args["{name}"] is None:
let module_dir_dot = dirs.replace("/", ".").replace("-", "_");
let last = if last.starts_with(|x: char| x.is_ascii_digit()) {
format!("_{}", last)
} else {
last
};
Ok((
import_loader,
import_base64,
@@ -603,6 +609,9 @@ pub async fn handle_python_reqs(
if let Some(url) = PIP_INDEX_URL.as_ref() {
vars.push(("INDEX_URL", url));
}
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
vars.push(("PIP_INDEX_CERT", cert_path));
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
vars.push(("TRUSTED_HOST", host));
}
@@ -702,6 +711,9 @@ pub async fn handle_python_reqs(
if let Some(url) = PIP_INDEX_URL.as_ref() {
command_args.extend(["--index-url", url]);
}
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
command_args.extend(["--cert", cert_path]);
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
command_args.extend(["--trusted-host", &host]);
}

View File

@@ -22,10 +22,15 @@ import "ext:deno_web/13_message_port.js";
import "ext:deno_web/14_compression.js";
import "ext:deno_web/15_performance.js";
globalThis.atob = base64.atob;
globalThis.btoa = base64.btoa;
globalThis.fetch = fetch.fetch;
globalThis.Request = request.Request;
globalThis.Blob = file.Blob;
globalThis.URL = url.URL;
globalThis.FormData = formData.FormData;
globalThis.URLSearchParams = url.URLSearchParams;
globalThis.Headers = headers.Headers;
globalThis.FileReader = fileReader.FileReader;
globalThis.console = new console.Console((msg, level) =>
globalThis.Deno.core.ops.op_log([msg])

View File

@@ -32,7 +32,7 @@ use windmill_common::{
flows::{FlowModule, FlowModuleValue, FlowValue},
jobs::{JobKind, QueuedJob},
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
users::SUPERADMIN_SECRET_EMAIL,
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
utils::{rd_string, StripPath},
worker::{
to_raw_value, to_raw_value_owned, update_ping, CLOUD_HOSTED, WORKER_CONFIG, WORKER_GROUP,
@@ -40,7 +40,7 @@ use windmill_common::{
DB, IS_READY, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
};
use windmill_queue::{
canceled_job_to_result, empty_args, get_queued_job, pull, push, register_metric, CanceledBy,
canceled_job_to_result, empty_result, get_queued_job, pull, push, register_metric, CanceledBy,
PushArgs, PushIsolationLevel, WrappedError, HTTP_CLIENT,
};
@@ -128,17 +128,22 @@ pub async fn create_token_for_owner(
w_id: &str,
owner: &str,
label: &str,
expires_in: i32,
expires_in: u64,
email: &str,
) -> error::Result<String> {
// TODO: Bad implementation. We should not have access to this DB here.
if let Some(token) = JOB_TOKEN.as_ref() {
return Ok(token.clone());
}
let token: String = rd_string(30);
let is_super_admin =
sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
.fetch_optional(db)
.await?
.unwrap_or(false)
|| email == SUPERADMIN_SECRET_EMAIL;
|| email == SUPERADMIN_SECRET_EMAIL
|| email == SUPERADMIN_NOTIFICATION_EMAIL;
sqlx::query_scalar!(
"INSERT INTO token
@@ -195,6 +200,8 @@ pub const MAX_BUFFERED_DEDICATED_JOBS: usize = 3;
lazy_static::lazy_static! {
static ref JOB_TOKEN: Option<String> = std::env::var("JOB_TOKEN").ok();
static ref SLEEP_QUEUE: u64 = std::env::var("SLEEP_QUEUE")
.ok()
.and_then(|x| x.parse::<u64>().ok())
@@ -267,10 +274,10 @@ lazy_static::lazy_static! {
pub static ref TIMEOUT_DURATION: Duration = Duration::from_secs(*TIMEOUT);
pub static ref SCRIPT_TOKEN_EXPIRY: i32 = std::env::var("SCRIPT_TOKEN_EXPIRY")
pub static ref SCRIPT_TOKEN_EXPIRY: u64 = std::env::var("SCRIPT_TOKEN_EXPIRY")
.ok()
.and_then(|x| x.parse::<i32>().ok())
.unwrap_or(900);
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or(*TIMEOUT);
pub static ref GLOBAL_CACHE_INTERVAL: u64 = std::env::var("GLOBAL_CACHE_INTERVAL")
.ok()
@@ -951,6 +958,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let worker_flow_transition_duration2 = worker_flow_transition_duration.clone();
let worker_name2 = worker_name.clone();
let killpill_tx2 = killpill_tx.clone();
let send_result = tokio::spawn(async move {
while let Some(jc) = job_completed_rx.recv().await {
if let Some(wj) = worker_job_completed_channel_queue2.as_ref() {
@@ -962,7 +970,6 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let same_worker_tx2 = same_worker_tx2.clone();
let rsmq2 = rsmq2.clone();
let worker_name = worker_name2.clone();
if matches!(jc.job.job_kind, JobKind::Noop) || is_dedicated_worker {
thread_count.fetch_add(1, Ordering::SeqCst);
let thread_count = thread_count.clone();
@@ -986,7 +993,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let worker_save_completed_job_duration2 =
worker_save_completed_job_duration.clone();
let worker_flow_transition_duration2 = worker_flow_transition_duration.clone();
let killpill_tx = killpill_tx2.clone();
tokio::spawn(async move {
#[cfg(feature = "benchmark")]
let process_start = Instant::now();
@@ -1049,6 +1056,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
.execute(&db2)
.await
.expect("update config to trigger restart of all dedicated workers at that config");
killpill_tx.send(()).unwrap_or_default();
}
});
} else {
@@ -1099,79 +1107,71 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let is_flow_worker;
if let Some(flow_path) = _wp.path.strip_prefix("flow/") {
is_flow_worker = true;
loop {
let value = sqlx::query_scalar!(
"SELECT value FROM flow WHERE path = $1 AND workspace_id = $2",
flow_path,
_wp.workspace_id
)
.fetch_optional(db)
.await;
if let Ok(v) = value {
if let Some(v) = v {
let value = serde_json::from_value::<FlowValue>(v).map_err(|err| {
Error::InternalErr(format!(
"could not convert json to flow for {flow_path}: {err:?}"
))
let value = sqlx::query_scalar!(
"SELECT value FROM flow WHERE path = $1 AND workspace_id = $2",
flow_path,
_wp.workspace_id
)
.fetch_optional(db)
.await;
if let Ok(v) = value {
if let Some(v) = v {
let value = serde_json::from_value::<FlowValue>(v).map_err(|err| {
Error::InternalErr(format!(
"could not convert json to flow for {flow_path}: {err:?}"
))
});
if let Ok(flow) = value {
let workers = spawn_dedicated_workers_for_flow(
&flow.modules,
&_wp.path,
&_wp.workspace_id,
killpill_tx.clone(),
&killpill_rx,
db,
&worker_dir,
base_internal_url,
&worker_name,
&job_completed_tx,
)
.await;
workers.into_iter().for_each(|(path, sender, handle)| {
dedicated_handles.push(handle);
hm.insert(path, sender);
});
if let Ok(flow) = value {
let workers = spawn_dedicated_workers_for_flow(
&flow.modules,
&_wp.path,
&_wp.workspace_id,
killpill_tx.clone(),
&killpill_rx,
db,
&worker_dir,
base_internal_url,
&worker_name,
&job_completed_tx,
)
.await;
workers.into_iter().for_each(|(path, sender, handle)| {
dedicated_handles.push(handle);
hm.insert(path, sender);
});
break;
}
} else {
tracing::error!(
"flow present but value not found for {}, waiting 10s",
flow_path
);
}
} else {
tracing::error!("flow not found for {}, waiting 10s,", flow_path);
tracing::error!(
"flow present but value not found for dedicated worker. {}",
flow_path
);
}
tokio::time::sleep(Duration::from_millis(10000)).await;
} else {
tracing::error!("flow not found for dedicated worker: {}. Waiting for dependency job and expected to restart.", flow_path);
}
} else {
is_flow_worker = false;
loop {
if let Some((path, sender, handle)) = spawn_dedicated_worker(
SpawnWorker::Script { path: _wp.path.clone(), hash: None },
&_wp.workspace_id,
killpill_tx.clone(),
&killpill_rx,
db,
&worker_dir,
base_internal_url,
&worker_name,
&job_completed_tx,
None,
)
.await
{
dedicated_handles.push(handle);
hm.insert(path, sender);
break;
} else {
tracing::error!(
"failed to spawn dedicated worker for {}, script found but not in a compatible language. Retrying 10s",
if let Some((path, sender, handle)) = spawn_dedicated_worker(
SpawnWorker::Script { path: _wp.path.clone(), hash: None },
&_wp.workspace_id,
killpill_tx.clone(),
&killpill_rx,
db,
&worker_dir,
base_internal_url,
&worker_name,
&job_completed_tx,
None,
)
.await
{
dedicated_handles.push(handle);
hm.insert(path, sender);
} else {
tracing::error!(
"failed to spawn dedicated worker for {}, script not found",
_wp.path
);
tokio::time::sleep(Duration::from_millis(10000)).await;
}
}
}
(hm, is_flow_worker)
@@ -1245,6 +1245,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
if (jobs_executed as u32 + vacuum_shift) % VACUUM_PERIOD == 0 {
if let Err(e) = sqlx::query!("VACUUM queue").execute(db).await {
jobs_executed += 1;
tracing::error!(worker = %worker_name, "failed to vacuum queue: {}", e);
}
tracing::info!(worker = %worker_name, "vacuumed queue and completed_job");
@@ -1460,7 +1461,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
.send(JobCompleted {
job: Arc::new(job),
success: true,
result: empty_args(),
result: empty_result(),
logs: String::new(),
mem_peak: 0,
cached_res_path: None,
@@ -1845,9 +1846,8 @@ async fn spawn_dedicated_worker(
{
let (dedicated_worker_tx, dedicated_worker_rx) =
mpsc::channel::<Arc<QueuedJob>>(MAX_BUFFERED_DEDICATED_JOBS);
let mut killpill_rx = killpill_rx.resubscribe();
let killpill_rx = killpill_rx.resubscribe();
let db = db.clone();
let worker_dir = worker_dir.clone();
let base_internal_url = base_internal_url.to_string();
let worker_name = worker_name.to_string();
let job_completed_tx = job_completed_tx.clone();
@@ -1865,16 +1865,12 @@ async fn spawn_dedicated_worker(
let (content, lock, language, envs) = match sw {
SpawnWorker::Script { path, hash } => {
let r;
loop {
let q = if let Some(hash) = hash {
get_script_content_by_hash(&hash, &w_id, &db).await.map(
|r: ContentReqLangEnvs| {
Some((r.content, r.lockfile, r.language, r.envs))
},
)
} else {
sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>, Option<Vec<String>>)>(
let q = if let Some(hash) = hash {
get_script_content_by_hash(&hash, &w_id, &db).await.map(
|r: ContentReqLangEnvs| Some((r.content, r.lockfile, r.language, r.envs)),
)
} else {
sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>, Option<Vec<String>>)>(
"SELECT content, lock, language, envs FROM script WHERE path = $1 AND workspace_id = $2 AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND
deleted = false AND lock IS not NULL AND lock_error_logs IS NULL)",
@@ -1884,37 +1880,23 @@ async fn spawn_dedicated_worker(
.fetch_optional(&db)
.await
.map_err(|e| Error::InternalErr(format!("expected content and lock: {e}")))
};
if let Ok(q) = q {
if let Some(wp) = q {
r = wp;
break;
} else {
tracing::error!(
"Failed to fetch script `{}` in workspace {} for dedicated worker. Retrying in 10s.",
path,
w_id
);
tokio::select! {
biased;
_ = killpill_rx.recv() => {
tracing::info!("Killing dedicated worker while it was attempting to fetch script");
return None;
}
_ = tokio::time::sleep(Duration::from_secs(10)) => {
continue;
}
}
}
};
if let Ok(q) = q {
if let Some(wp) = q {
wp
} else {
tracing::error!("Failed to fetch script for dedicated worker");
killpill_tx.send(()).expect("send");
tracing::error!(
"Failed to fetch script `{}` in workspace {} for dedicated worker.",
path,
w_id
);
return None;
}
} else {
tracing::error!("Failed to fetch script for dedicated worker");
killpill_tx.send(()).expect("send");
return None;
}
r
}
SpawnWorker::RawScript { content, lock, lang, .. } => (content, lock, Some(lang), None),
};
@@ -1925,21 +1907,26 @@ async fn spawn_dedicated_worker(
}
let handle = tokio::spawn(async move {
let token = rd_string(30);
if let Err(e) = sqlx::query_scalar!(
"INSERT INTO token
let token = if let Some(token) = JOB_TOKEN.as_ref() {
token.clone()
} else {
let token = rd_string(30);
if let Err(e) = sqlx::query_scalar!(
"INSERT INTO token
(token, label, super_admin, email)
VALUES ($1, $2, $3, $4)",
token,
"dedicated_worker",
true,
"dedicated_worker@windmill.dev"
)
.execute(&db)
.await
{
tracing::error!("failed to create token for dedicated worker: {:?}", e);
killpill_tx.clone().send(()).expect("send");
token,
"dedicated_worker",
true,
"dedicated_worker@windmill.dev"
)
.execute(&db)
.await
{
tracing::error!("failed to create token for dedicated worker: {:?}", e);
killpill_tx.clone().send(()).expect("send");
};
token
};
let worker_envs = build_envs(envs).expect("failed to build envs");

View File

@@ -1383,10 +1383,29 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
logs,
0,
canceled_by,
rsmq,
rsmq.clone(),
)
.await?;
if flow_job.is_flow_step {
if let Some(parent_job) = flow_job.parent_job {
update_flow_status_after_job_completion(
db,
client,
parent_job,
&flow_job.id,
&flow_job.workspace_id,
true,
&to_raw_value(&result),
false,
same_worker_tx.clone(),
&worker_dir,
None,
rsmq,
worker_name,
)
.await?;
}
}
return Ok(());
}
}
@@ -2348,8 +2367,8 @@ async fn compute_next_flow_transform(
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
priority: None,
early_return: None,
},
path: inner_path,
restarted_from: None,
@@ -2387,8 +2406,8 @@ async fn compute_next_flow_transform(
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
priority: None,
early_return: None,
},
path: Some(format!("{}/forloop", flow_job.script_path())),
restarted_from: None,
@@ -2483,8 +2502,8 @@ async fn compute_next_flow_transform(
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
priority: None,
early_return: None,
},
path: Some(format!(
"{}/branchone-{}",
@@ -2532,8 +2551,8 @@ async fn compute_next_flow_transform(
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
priority: None,
early_return: None,
},
path: Some(format!(
"{}/branchall-{}",
@@ -2597,8 +2616,8 @@ async fn compute_next_flow_transform(
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
ws_error_handler_muted: None,
priority: None,
early_return: None,
},
path: Some(format!(
"{}/branchall-{}",

View File

@@ -1,5 +1,5 @@
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
import { JSDOM } from "https://jspm.dev/jsdom";
import { JSDOM } from "https://jspm.dev/jsdom@22";
type DataPoint = {
value: number;

View File

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

View File

@@ -68,7 +68,7 @@ export async function pushFlow(
if (flow) {
if (isSuperset(localFlow, flow)) {
log.info(colors.bold.green("Flow is up to date"));
log.info(colors.green(`Flow ${remotePath} is up to date`));
return;
}
log.info(colors.bold.yellow(`Updating flow ${remotePath}...`));

View File

@@ -31,7 +31,7 @@ addEventListener("error", (event) => {
}
});
export const VERSION = "v1.210.0";
export const VERSION = "v1.219.1";
let command: any = new Command()
.name("wmill")

View File

@@ -124,14 +124,18 @@ export async function handleFile(
typed.is_template === remote.is_template &&
typed.kind == remote.kind &&
!remote.archived &&
remote?.lock == typed.lock?.join("\n") &&
JSON.stringify(typed.schema) == JSON.stringify(remote.schema))
(remote?.lock ?? "") == (typed.lock?.join("\n") ?? "") &&
JSON.stringify(typed.schema) == JSON.stringify(remote.schema) &&
typed.tag == remote.tag &&
(typed.ws_error_handler_muted ?? false) ==
remote.ws_error_handler_muted &&
typed.dedicated_worker == remote.dedicated_worker &&
typed.cache_ttl == remote.cache_ttl &&
typed.concurrency_time_window_s ==
remote.concurrency_time_window_s &&
typed.concurrent_limit == remote.concurrent_limit)
) {
log.info(
colors.yellow(
`No change to push for script ${remotePath}, skipping`
)
);
log.info(colors.green(`Script ${remotePath} is up to date`));
return true;
}
}

View File

@@ -561,8 +561,8 @@ async function pull(
}
}
log.info(
colors.green.underline(
`Done! All ${changes.length} changes applied locally.`
colors.bold.green.underline(
`\nDone! All ${changes.length} changes applied locally.`
)
);
}
@@ -831,8 +831,8 @@ async function push(
}
}
log.info(
colors.green.underline(
`Done! All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name}.`
colors.bold.green.underline(
`\nDone! All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name}.`
)
);
}

View File

@@ -89,7 +89,7 @@ export async function resolveDefaultResource(obj: any): Promise<any> {
}
export function getStatePath(): string {
const state_path = Deno.env.get("WM_STATE_PATH_NEW");
const state_path = Deno.env.get("WM_STATE_PATH_NEW") ?? Deno.env.get("WM_STATE_PATH");
if (state_path === undefined) {
throw Error("State path not set");
}

4
docker/DockerfileReports Normal file
View File

@@ -0,0 +1,4 @@
FROM ghcr.io/windmill-labs/windmill-ee:main
RUN apt-get update
RUN apt-get install -y chromium

View File

@@ -0,0 +1,16 @@
{$BASE_URL} {
bind {$ADDRESS}
tls /certs/caddy.crt /certs/caddy.key
handle_path /static/* {
root * /static/
file_server {
browse
}
}
handle_path /npm/* {
reverse_proxy * http://npm_registry:4873
}
handle_path /* {
reverse_proxy * http://pypi_server:8080
}
}

View File

@@ -0,0 +1,146 @@
Private package registry with self-signed certificates
==================================================
Setup a private NPM registry and Pypi server behind a revers proxy (Caddy) exposing an HTTPS endpoint with self signed certificates
### Setup
1. Generate self signed certificates (or bring your own)
```bash
cd certs
# feel free to read the file anc change the
./generate_certs.sh
# choose a password for the RootCA key. You'll need to input it for pretty much all following steps
```
At the end of the script, you should have multiple files in the `certs/` folder. The most important ones are:
- `windmill-root.key` (Root CA private key)
- `windmill-root.crt` (Root CA certificate)
- `caddy.key` (caddy server private key)
- `caddy.crt` (caddy registry server certificate)
2. Start the docker compose stack
```bash
docker compose up -d
```
This will start the private NPM registry, the private Pypi server and Caddy as a reverse proxy sitting in front of the two. Separately, for the purpose of proving an end to end setup, it will also start minimal Windmill stack composed of just one Windmill server/worker and the associated database.
For a more complete Windmill setup, we invite you to refer to the latest [docker compose](/docker-compose.yml) at the root of this repository to setup a more evolved Windmill stack.
For the private registries/repositories, it's using [Verdaccio](https://verdaccio.org/) as an easy-to-deploy NPM registry and [pypiserver](https://pypi.org/project/pypiserver/) for Python.
## Deno pulling package from private NPM registry
Upload the custom `helloworld_npm_package` package to the private NPM registry and use it in a Windmill script
```bash
cd helloworld_npm_package
# you need to first create a registry user
# you might need to run `npm config set strict-ssl false` if the below fails. If you do, then change it back to `true` after running the 2 commands
npm adduser --registry https://localhost/npm/
npm publish --registry https://localhost/npm/
```
Go to Windmill at `http://localhost:8000`. Create a simple Deno script:
```ts
import * as testpackage from "npm:@windmill/helloworld@0.0.1"
export async function main() {
console.log(testpackage.sayHello("Windmill"))
}
```
and execute it. It should return successfully with:
```
Hello Windmill
```
Note: Native fetches to HTTPS endpoints with self signed certificates also takes into account the DENO_CERT environment variable
Go to Windmill and create a new script of type "REST" with the following content:
```ts
export async function main() {
const res = await fetch("https://caddy/static/helloworld.json", {
headers: { "Content-Type": "application/json" },
});
return res.json();
}
```
It will fetch a static json file from Caddy exposed behind its HTTPS endpoint with self signed certificate.
## Python pulling package from private NPM registry
Upload the custom `helloworld_python_module` python module to the private Pypi server
```bash
cd helloworld_python_module
python setup.py sdist
# using twine to upload the module here, get it with `pip install twine`
twine upload --repository-url https://localhost/ dist/* --cert ../certs/windmill-root.crt
# no username and password, just press enter. For the purpose of the demo we're running pypiserver completely unauthenticated
```
You can check that the package is uploaded by visiting [https://localhost/simple](https://localhost/simple).
Go to Windmill at `http://localhost:8000`. Create a simple Python script:
```python
#requirements:
#windmill-helloworld==0.0.1
import windmill_helloworld
def main():
print(windmill_helloworld.say_hello("Windmill"))
```
and execute it. It should return successfully with:
```
Hello Windmill
```
### MISC
1. `DENO_TLS_CA_STORE` VS `DENO_CERT`
Both works. `DENO_CERT` is better b/c you just have to set it to the path of the trusted Root CA certificate, and deno will trust this certificate.
When using `DENO_TLS_CA_STORE=system`, you _have to_ make the server trust the custom Root CA certificate with the following commands:
```bash
# in the windmill-server container:
cp /custom-certs/windmill-root.crt /usr/local/share/ca-certificates/
update-ca-certificates # as root
# the output should tell (among other things): " ... 1 added, 0 removed; done. ..."
```
2. Running Deno scripts manually in the Windmill container
This could be useful for debugging purposes.
```bash
# log into the Windmill Server container
docker exec -it <WINDMILL_SERVER_CONTAINER> /bin/bash
# Go into Deno REPL
deno
# try to import the package
import * as testpackage from "npm:@windmill/helloworld@0.0.1"
> undefined
# if the above returns undefined, there's a good chance it's working. If you want to double check:
testpackage.sayHello("Windmill")
> Hello Windmill
# potentially inspect the env var available to DENO. Is you used DENO_CERT in the docker compose, check its value:
console.log(Deno.env.get("DENO_CERT"))
> /custom-certs/windmill-root.crt
```
3. Manually testing Pypi private server integration
Can be useful to debug as well:
```bash
# install the package
pip3 install --cert ../certs/windmill-root.crt -i https://localhost/simple/ windmill-helloworld
# go to python CLI and try to import it and use it
python
>>> import windmill_helloworld
>>> windmill_helloworld.say_hello("world")
'Hello world'
```

View File

@@ -0,0 +1,5 @@
*.crt
*.csr
*.key
*.ext
*.srl

View File

@@ -0,0 +1,35 @@
#!/bin/bash
set eou -pipefail
CANAME='windmill-root'
COUNTRY='FR'
STATE='Paris'
CITY='Paris'
ORGANIZATION='WindmillLabs'
ROOT_CA_CN='WindmillRootCA'
SERVER_CA_CN='caddy' # IMPORTANT: set this to the FQDN of the caddy server. Here in the docker compose stack, it will be caddy
echo "Generating RootCA key"
openssl genrsa -aes256 -out $CANAME.key 4096
echo "Generating RootCA certificate"
openssl req -x509 -new -nodes -key $CANAME.key -sha256 -days 1826 -out $CANAME.crt -subj "/CN=${ROOT_CA_CN}/C=${COUNTRY}/ST=${STATE}/L=${CITY}/O=${ORGANIZATION}"
CERTNAME=caddy
echo "Generating server certificate private key and cert signing request"
openssl req -new -nodes -out $CERTNAME.csr -newkey rsa:4096 -keyout $CERTNAME.key -subj "/CN=${SERVER_CA_CN}/C=${COUNTRY}/ST=${STATE}/L=${CITY}/O=${ORGANIZATION}"
echo "Generating server certificate"
cat > $CERTNAME.v3.ext << EOF
[v3_req]
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = caddy
DNS.2 = localhost
EOF
# ^ HERE ^ in the above alt_names, feel free to add any alternate CN
openssl x509 -req -in $CERTNAME.csr -CA $CANAME.crt -CAkey $CANAME.key -CAcreateserial -out $CERTNAME.crt -days 1826 -sha256 -extensions v3_req -extfile $CERTNAME.v3.ext

View File

@@ -0,0 +1,69 @@
version: "3.7"
services:
npm_registry:
image: verdaccio/verdaccio
environment:
- VERDACCIO_PROTOCOL=http
- VERDACCIO_PUBLIC_URL=http://caddy/npm/
volumes:
- ./verdaccio_conf:/verdaccio/conf
- npm_registry_data:/verdaccio/storage
pypi_server:
image: pypiserver/pypiserver:latest
platform: linux/x86_64
volumes:
- pypi_data:/data/packages
command: run -P . -a . /data/packages
caddy:
image: caddy:2.5.2-alpine
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./certs:/certs
- ./helloworld_static:/static/
ports:
- 443:443
environment:
- BASE_URL=":443"
db:
image: postgres:14
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: changeme
POSTGRES_DB: windmill
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
windmill_server:
image: ghcr.io/windmill-labs/windmill:main
ports:
- 8000:8000
environment:
- DATABASE_URL=postgres://postgres:changeme@db/windmill?sslmode=disable
- WORKER_TAGS=deno,go,python3,bash,flow,hub,dependency,nativets
# For DENO and REST scripts:
- NPM_CONFIG_REGISTRY=http://caddy/npm/
- DENO_CERT=/custom-certs/windmill-root.crt # this will make deno trust this RootCA for all sessions
# - DENO_TLS_CA_STORE=system # alternatively, you can use this but you'll need to manually trust the RootCA at the host level, see README.md
# For Python scripts:
- PIP_INDEX_URL=https://caddy/simple/
- PIP_INDEX_CERT=/custom-certs/windmill-root.crt # this will make pip trust this RootCA for all sessions
volumes:
- ./certs:/custom-certs
depends_on:
db:
condition: service_healthy
volumes:
npm_registry_data: null
pypi_data: null
db_data: null

View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -0,0 +1,4 @@
Hello world test package
========================
Dummy NPM package to test imports from a private NPM registry in Windmill

View File

@@ -0,0 +1,3 @@
exports.sayHello = function(x) {
return `Hello ${x}`
}

View File

@@ -0,0 +1,10 @@
{
"name": "@windmill/helloworld",
"version": "0.0.1",
"description": "hello world test package",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "Windmill",
"license": "Apache-2.0"
}

View File

@@ -0,0 +1,3 @@
dist/
__pycache__/
*.egg-info/

View File

@@ -0,0 +1,4 @@
Hello world test module
=======================
Dummy Python module to test imports from a private Pypi server in Windmill

View File

@@ -0,0 +1,30 @@
import os
import setuptools
module_path = os.path.join(os.path.dirname(__file__), "windmill_helloworld.py")
setuptools.setup(
name="windmill-helloworld",
version="0.0.1",
url="https://github.com/windmill-labs/windmill/blob//examples/deploy/private-package-registry-tls/README.md",
author="WindmillLabs",
author_email="contact@windmill.dev",
description="Simple hello world python module to host on a private Pypi server",
long_description=open("README.md").read(),
py_modules=["windmill_helloworld"],
zip_safe=False,
platforms="any",
install_requires=[],
classifiers=[
"Development Status :: 2 - Pre-Alpha",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
],
)

View File

@@ -0,0 +1,2 @@
def say_hello(x: str) -> str:
return "Hello {}".format(x)

View File

@@ -0,0 +1,3 @@
{
"hello": "world"
}

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