Compare commits

..

448 Commits

Author SHA1 Message Date
Ruben Fiszel
1ea08f1e8f inline script drawer 2023-02-28 15:46:06 +01:00
Ruben Fiszel
d302c92766 Merge branch 'main' into rf/flowFix 2023-02-28 11:18:22 +01:00
Ruben Fiszel
ea2ebfa92e fix compile issue 2023-02-28 11:18:09 +01:00
Ruben Fiszel
128622117d progress 2023-02-28 11:15:56 +01:00
dependabot[bot]
ba856be10d chore(deps): bump docker/metadata-action from 3 to 4 (#1243)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 3 to 4.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md)
- [Commits](https://github.com/docker/metadata-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-28 11:02:14 +01:00
dependabot[bot]
333b873ee9 chore(deps): bump docker/build-push-action from 3 to 4 (#1186)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-28 11:02:00 +01:00
dependabot[bot]
2785b05064 chore(deps-dev): bump @playwright/test in /frontend (#1244)
Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.29.2 to 1.31.1.
- [Release notes](https://github.com/Microsoft/playwright/releases)
- [Commits](https://github.com/Microsoft/playwright/compare/v1.29.2...v1.31.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-28 11:01:33 +01:00
Faton Ramadani
a67f10eeb6 fix(frontend): Fix deeply nested move (#1245)
* fix(frontend): Fix deeply nested move

* fix(frontend): update comment
2023-02-28 10:53:51 +01:00
Ruben Fiszel
287b2db22f feat(cli): add autocompletions 2023-02-28 10:32:34 +01:00
Ádám Kovács
a4e4d188ad fix(frontend): Add more app custom css (#1229)
* fix(frontend): Add container custom css

* fix(frontend): Add form custom css

* fix(frontend): Add form button custom css

* feat(frontend): Add css quick reset button

* feat(frontend): Filter component css by usage

* fix(frontend): Save state of unused component display

* fix(frontend): Add pie chart custom css

* fix(frontend): Add bar chart custom css

* fix(frontend): Update vega lite and plotly loading

* fix(frontend): Add html and timeseries custom css

* fix(frontend): Add scatter chart custom css

* fix(frontend): Add table custom css

* fix(frontend): Revert container custom styles

* fix(frontend): Add toggle custom css

* fix(frontend): Add text input custom css

* fix(frontend): Update

* fix(frontend): Remove undefined css customizations
2023-02-28 09:21:56 +01:00
Ruben Fiszel
150ce7f731 Merge branch 'main' into rf/flowFix 2023-02-27 18:54:26 +01:00
Ruben Fiszel
2244e83b9d fix(frontend): invisible subgrids have h-0 + app policies fix 2023-02-27 18:38:22 +01:00
Ruben Fiszel
fae406ee8f update docs 2023-02-27 18:26:59 +01:00
Ruben Fiszel
42d1cd6456 fix(frontend): display currently selected filter even if not in list 2023-02-27 16:20:31 +01:00
Ruben Fiszel
4b64e75bd1 add back all tailwind colors to tailwind config 2023-02-27 15:11:23 +01:00
Ruben Fiszel
51a7eaaeb0 rename counters 2023-02-27 14:52:24 +01:00
Ruben Fiszel
8589b70ccf apps improvements 2023-02-27 14:37:32 +01:00
Ruben Fiszel
0bf6f23c9e fix setup app to use updated version of the CLI 2023-02-27 14:16:57 +01:00
Ruben Fiszel
e56869092a feat(backend): use counter for sleep/execution/pull durations 2023-02-27 12:00:32 +01:00
Ruben Fiszel
6b8758f4a5 chore(main): release 1.70.1 (#1241) 2023-02-27 10:30:41 +01:00
Ruben Fiszel
fbc929ba1b chore(main): release 1.70.1 (#1239)
* chore(main): release 1.70.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-27 10:07:26 +01:00
Faton Ramadani
97602ac6db fix(frontend): Fix inline scripts list (#1240) 2023-02-27 10:06:42 +01:00
Faton Ramadani
8ee9d67f4f fix(frontend): Fix subgrid lock (#1232)
* fix(frontend): Fix subgrid lock

* feat(frontend): restore
2023-02-27 10:00:18 +01:00
Ruben Fiszel
4bf6e753f1 fix findGridItemById 2023-02-27 09:56:17 +01:00
Faton Ramadani
70eab303bd fix(frontend): Disable move in nested subgrid (#1238)
* fix(frontend): Disable move in nested subgrid

* fix(frontend): Disable move in nested subgrid
2023-02-27 09:50:39 +01:00
Ruben Fiszel
c051ffeb42 fix(cli): make cli resilient to systems without openable browsers 2023-02-27 09:48:52 +01:00
Ruben Fiszel
ebb68e5320 chore(main): release 1.70.0 (#1236)
* chore(main): release 1.70.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-27 09:25:03 +01:00
Ruben Fiszel
04a076f1db fix(cli): bump cli to non broken client 1.69.3 2023-02-27 09:24:18 +01:00
Ruben Fiszel
ebd2e0323e fix stripe checkout 2023-02-27 08:49:36 +01:00
Ruben Fiszel
cd25570003 fix minor editor bugs 2023-02-27 00:53:49 +01:00
Ruben Fiszel
e95f8ef6bf add none color for buttons 2023-02-27 00:27:20 +01:00
Ruben Fiszel
c3d1c8ac39 fix many apps bugs 2023-02-26 23:43:00 +01:00
Ruben Fiszel
d38aff2fe2 fix: app setup and sync now uses 1.69.3 2023-02-26 14:09:16 +01:00
Ruben Fiszel
95851ea486 minor app nits 2023-02-26 13:58:51 +01:00
Ruben Fiszel
b690d801d4 feat(apps): add ag grid 2023-02-25 13:11:50 +01:00
Faton Ramadani
104e4ac5e7 feat(frontend): move to other grid (#1230)
* feat(frontend): move to other grid

* feat(frontend): wip

* feat(frontend): Fix component move

* fix(frontend): Fix build

* feat(frontend): Fix moving

* fix(frontend): Simplify code
2023-02-25 11:31:12 +01:00
Faton Ramadani
e87f4fc44b fix(frontend): Fix duplication (#1237) 2023-02-25 10:18:21 +01:00
Ruben Fiszel
e1f686d850 fix(frontend): fix graph viewer id assignment 2023-02-25 08:48:24 +01:00
Ruben Fiszel
7da7dac3ac chore(main): release 1.69.3 (#1234) 2023-02-24 21:11:58 +01:00
Ruben Fiszel
c2e5afd4e0 fix(deno): fix denoify buffer handling 2023-02-24 21:09:37 +01:00
Ruben Fiszel
ad9c386f41 making slider/range values connectable 2023-02-24 20:50:01 +01:00
Ruben Fiszel
a4e3f98b7d chore(main): release 1.69.2 (#1233)
* chore(main): release 1.69.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-24 19:57:50 +01:00
Ruben Fiszel
dd28308c3c fix(app): fix all nested behavior 2023-02-24 19:52:20 +01:00
Ruben Fiszel
833c2655ea push 2023-02-24 19:52:20 +01:00
Faton Ramadani
a8295d0b5a fix(frontend): Fix findGridItem 2023-02-24 19:52:20 +01:00
Ruben Fiszel
897e2f6b53 flatten 2023-02-24 19:52:20 +01:00
Faton Ramadani
5bb77edf45 fix(frontend): Fix findGridItem 2023-02-24 19:52:20 +01:00
Faton Ramadani
8ddcf4d9c1 fix(frontend): Fix next id 2023-02-24 19:52:20 +01:00
Faton Ramadani
33ebe2da8e fix(frontend): wip 2023-02-24 19:52:20 +01:00
Ruben Fiszel
b3ee747014 pushed 2023-02-24 19:52:20 +01:00
Faton Ramadani
fa105b4cae fix(frontend): gridtab 2023-02-24 19:52:20 +01:00
Faton Ramadani
483407cdf0 fix(frontend): duplicate 2023-02-24 19:52:20 +01:00
Faton Ramadani
008c30fcaa fix(frontend): delete grid item 2023-02-24 19:52:20 +01:00
Ruben Fiszel
3387bb0d83 script editor panel 2023-02-24 19:52:20 +01:00
Ruben Fiszel
e08e7e4ae6 settings panel 2023-02-24 19:52:20 +01:00
Faton Ramadani
ea1b2c29b9 fix(frontend): rewrote utils 2023-02-24 19:52:20 +01:00
Ruben Fiszel
4ad6fbefd3 flatten 2023-02-24 19:52:20 +01:00
Ruben Fiszel
397ecd64d4 chore(main): release 1.69.1 (#1231)
* chore(main): release 1.69.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-24 17:26:53 +01:00
Ruben Fiszel
dd7e8c742c fix(deno): remove mysql support waiting for deno fix 2023-02-24 17:19:13 +01:00
Ruben Fiszel
2f78132e08 fix(deno): remove mysql support waiting for deno fix 2023-02-24 17:11:53 +01:00
Ruben Fiszel
0041411c06 subgrids editor panel iter 1 2023-02-24 14:48:28 +01:00
Ruben Fiszel
54955b710c fix subgrids final2 2023-02-24 13:11:06 +01:00
Ruben Fiszel
1eb5a0d1d3 fix subgrids final 2023-02-24 11:59:28 +01:00
Ruben Fiszel
ddda14c52b app fixes 2023-02-24 11:39:50 +01:00
Ruben Fiszel
5123c9365c fix subgrids final 2023-02-24 07:27:22 +01:00
Ruben Fiszel
834e7b1d1c todo: improve deleting tab 2023-02-24 01:04:26 +01:00
Ruben Fiszel
6e9a5b026e tab rework v2 2023-02-23 23:39:57 +01:00
Ruben Fiszel
7ad8879b09 fix binding 2023-02-23 22:14:23 +01:00
Ruben Fiszel
27cac3ffe6 fix(frontend): containers and tab fixes v1 2023-02-23 21:42:21 +01:00
Ruben Fiszel
705703a5e2 fix(frontend): containers and tab fixes v1 2023-02-23 21:01:47 +01:00
Ruben Fiszel
fac31c6628 fix(frontend): containers and tab fixes v1 2023-02-23 19:59:23 +01:00
Ruben Fiszel
90c0e140a1 chore(main): release 1.69.0 (#1226)
* chore(main): release 1.69.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-23 18:07:56 +01:00
Ruben Fiszel
d543650b31 fix(cli): .wmillignore whitelist behavior 2023-02-23 18:02:42 +01:00
Faton Ramadani
089a6b6ae5 feat(frontend): Duplicate component (#1228)
* feat(frontend): Duplicate component

* feat(frontend): add missing types
2023-02-23 17:43:27 +01:00
Faton Ramadani
857ee5f318 feat(frontend): Properly delete tab content (#1227) 2023-02-23 16:02:15 +01:00
Faton Ramadani
6ad876ebb4 feat(frontend): Support deeply nested components (#1225) 2023-02-23 15:33:14 +01:00
Ádám Kovács
ab4137640e refactor(frontend): Reorganise app components (#1224)
* refactor(frontend): Reorganise app components

* refactor(frontend): Restructure app component
2023-02-23 14:15:45 +01:00
Ruben Fiszel
2bd8fabcf7 chore(main): release 1.68.0 (#1223)
* chore(main): release 1.68.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-23 13:42:42 +01:00
Ruben Fiszel
3b7160e84a fix(cli): better ergonomics around workspaces 2023-02-23 13:31:08 +01:00
Ruben Fiszel
40c12e6139 fix(cli): better ergonomics around workspace add 2023-02-23 13:19:14 +01:00
Ádám Kovács
6044e3b6ef feat(frontend): Add more app component CSS customisation (#1218)
* fix(frontend): Update app style editor

* fix(frontend): Update component custom css typings

* fix(frontend): Generalise custom CSS inputs

* feat(frontend): Add display CSS customization
2023-02-23 11:42:32 +01:00
Ruben Fiszel
18ff5c7cef chore(main): release 1.67.4 (#1222)
* chore(main): release 1.67.4

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-23 09:06:34 +01:00
Ruben Fiszel
e54dc3ff97 fix(backend): workflow check for has_failure_module 2023-02-23 09:01:02 +01:00
Ruben Fiszel
4d5aae69c8 chore(main): release 1.67.3 (#1221)
* chore(main): release 1.67.3

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-23 08:34:22 +01:00
Ruben Fiszel
ec57c5977f fix(cli): ignone non wmill looking files 2023-02-23 08:26:22 +01:00
Ruben Fiszel
df1b724626 do not create default variables on workspace creation 2023-02-23 08:07:35 +01:00
Ruben Fiszel
268dfbf831 chore(main): release 1.67.2 (#1220)
* chore(main): release 1.67.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-23 08:00:15 +01:00
Ruben Fiszel
969e89f8bb fix(cli): ignone non wmill looking files 2023-02-23 07:40:04 +01:00
Ruben Fiszel
5997503961 chore(main): release 1.67.1 (#1219) 2023-02-22 23:47:37 +01:00
Ruben Fiszel
3fa24adad0 fix(cli): coloring nits 2023-02-22 23:46:08 +01:00
Ruben Fiszel
7471be1d81 update rustpython parser 2023-02-22 23:43:06 +01:00
Ruben Fiszel
d64e1c116a nits on cli output 2023-02-22 23:28:50 +01:00
Ruben Fiszel
9267b1fb90 nits on webhook sync 2023-02-22 22:44:19 +01:00
Ruben Fiszel
6528a68668 chore(main): release 1.67.0 (#1216)
* chore(main): release 1.67.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-22 22:39:14 +01:00
Ruben Fiszel
4fd4d17a0d update README 2023-02-22 21:49:13 +01:00
Ruben Fiszel
0548803ab7 fix flow rendering 2023-02-22 21:27:58 +01:00
Ruben Fiszel
0085b46c1e fix(cli): add --fail-conflicts to ci push 2023-02-22 21:00:48 +01:00
Kai Jellinghaus
81ffd49bef Remove Stripe (#1217) 2023-02-22 19:15:10 +01:00
Faton Ramadani
dbc59e9521 feat(frontend): Add app sub grids (#1208)
* feat(frontend): Add app sub grids

* feat(frontend): remove temp file

* feat(frontend): update package.json

* feat(frontend): fix build

* fixes

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): update dependencies

* feat(frontend): update dependencies

* feat(frontend): fix scrolling issues

* feat(frontend): fix preview + delete

* feat(frontend): done

* feat(frontend): remove dead code

* feat(frontend): fix selection

* feat(frontend): add container

* feat(frontend): fix package.json

* feat(frontend): fix package.json

* feat(frontend): fix package.json

* feat(frontend): fix package.json

* feat(frontend): fix package.json

* feat(frontend): restore npm package

* feat(frontend): add missing dependencies

* feat(frontend): remove missing dependencies

* feat(frontend): fix package.json

* feat(frontend): fix package.json

* feat(frontend): fix package.json

* feat(frontend): revert

* feat(frontend): fix package-lock

* feat(frontend): fix package-lock

* fix package-lock.json

* update

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-02-22 17:52:10 +01:00
Ruben Fiszel
121b3e9060 chore(main): release 1.66.1 (#1215)
* chore(main): release 1.66.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-22 15:55:37 +01:00
Ruben Fiszel
70dfc8b8d0 fix(cli): delete workspace instead of archiving them 2023-02-22 15:50:42 +01:00
Ruben Fiszel
ca3572a2a1 chore(main): release 1.66.0 (#1214)
* chore(main): release 1.66.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-22 15:38:01 +01:00
Ruben Fiszel
32c3c591d7 update README 2023-02-22 15:30:42 +01:00
Ruben Fiszel
1f4bc55e5c ux nits 2023-02-22 15:11:27 +01:00
Ruben Fiszel
a00ff45ccf UX nits 2023-02-22 15:06:52 +01:00
Ruben Fiszel
0160ce978d cli skip pull 2023-02-22 14:29:37 +01:00
Ruben Fiszel
867c00047a cli nits 2023-02-22 14:26:36 +01:00
Ruben Fiszel
e31d2ae27f feat: CLI refactor v1 2023-02-22 14:21:53 +01:00
Ruben Fiszel
441f087d42 remove references to starter workspace' 2023-02-22 13:56:24 +01:00
Ruben Fiszel
4671558e6b remove extra_perms from being encoded in non-folder zip export 2023-02-22 12:55:40 +01:00
Ruben Fiszel
08519f4099 removed edited_by from zip metadata 2023-02-22 09:53:45 +01:00
Ruben Fiszel
2727699d91 remove archived from zip 2023-02-22 09:23:23 +01:00
Ruben Fiszel
0c43b68b23 remove metadata from zip 2023-02-22 09:18:01 +01:00
Ruben Fiszel
c280f6e798 remove metadata from zip 2023-02-22 09:12:22 +01:00
Ruben Fiszel
e81f7bd723 feat: add delete flows 2023-02-22 08:40:11 +01:00
Ruben Fiszel
2213500210 remove versions from serialized apps 2023-02-22 01:13:42 +01:00
Ruben Fiszel
7558fb83d2 update cli 2023-02-22 01:07:48 +01:00
Ruben Fiszel
3d7a5a4520 v0 of cli refactor 2023-02-22 00:15:32 +01:00
Ruben Fiszel
be6f052ba4 extend zip permissions 2023-02-22 00:06:22 +01:00
Ruben Fiszel
4f1bcbb1c3 superadmin can see all 2023-02-22 00:00:43 +01:00
Ádám Kovács
a4b773af29 feat(frontend): Add image app component (#1213)
* fix(frontend): hard type app component config

* feat(frontend): Add image app component

* feat(frontend): Add upload app input type
2023-02-21 19:14:55 +01:00
Ruben Fiszel
61e6e1a4c5 fix go-client with new openapi 2023-02-21 12:25:54 +01:00
Ruben Fiszel
cf7dc3c01a chore(main): release 1.65.0 (#1210)
* chore(main): release 1.65.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-21 10:17:35 +01:00
Kai Jellinghaus
41c8ea92fe CLI fixes (#1204)
* Add default workspace URL

* R1 WIP

* Improve help docs slightly

* Rework tracking state

* WIP Rework
Remaining bug: Not returning state-only files (no local file) from *getFiles()

* Create newly found files

* Finish ZIP & new tracking code

* Fix two minor bugs

* do not consider conflict if same content

* add more logs to cli writing

* progress

* progress

* iteration

* Add most basic App support

* fix folder frontend bug

* fix folder frontend bug

* init done by default

* sqlx merge

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-02-21 10:13:21 +01:00
Ruben Fiszel
b6b0880f2f edit main video 2023-02-21 01:28:07 +01:00
Ádám Kovács
d4b6d69126 feat(frontend): Add file input app component (#1211)
* feat(frontend): Add file input app component

* fix(frontend): Handle multiple file selects

* fix(frontend): File input styling
2023-02-20 18:05:15 +01:00
Ruben Fiszel
75edeab35e rename panel headers 2023-02-19 22:47:30 +01:00
Ruben Fiszel
71d6dad37c feat(apps): add asJson for customcss 2023-02-19 13:10:57 +01:00
Ruben Fiszel
7f00e1c1a8 feat(apps): add custom css for apps 2023-02-19 12:51:59 +01:00
Ruben Fiszel
a39f8e2123 added loadOnAppLoading & iconBefore and after for app buttons 2023-02-17 22:22:41 +01:00
Ruben Fiszel
2de660fef6 fix app select 2023-02-17 21:52:04 +01:00
Ruben Fiszel
dc1be9cf55 fix app select 2023-02-17 21:28:04 +01:00
Ruben Fiszel
91e1781dc1 fixes 2023-02-17 21:16:51 +01:00
Ádám Kovács
e4791c2b7e feat(frontend): Add icon app component (#1207)
* feat(frontend): Add icon app component

* fix(frontend): Import only one icon at a time

* fix(frontend): Handle file names correctly

* feat(frontend): Use all arrow keys in popup navigation

* fix(frontend): Close popup after icon select

* fix(frontend): Position picker correctly

* fix(frontend): Handle empty search

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-02-17 20:20:28 +01:00
Ádám Kovács
c33e79e0b8 feat(frontend): Add divider app component (#1209)
* feat(frontend): Add divider app component

* fix(frontend): Separate horizontal and vertical

* fix(frontend): Update aligments

* fix(frontend): Remove unused static value
2023-02-17 19:27:14 +01:00
Kai Jellinghaus
5d109b3cd4 feat(backend): Zip Workspace Export (#1201)
* Allow different formats

* Allow Zip format in workspace export

* Fix axum error

* Use ZIP in frontend

* File ending fix

* Fix empty query
2023-02-17 19:21:06 +01:00
Ruben Fiszel
8074b26bfb fix currency issues 2023-02-17 16:40:18 +01:00
Ruben Fiszel
98c1806369 new app component: currency + improved select 2023-02-16 23:40:58 +01:00
Ruben Fiszel
7120d6b35b frontend nits 2023-02-16 18:15:42 +01:00
Ruben Fiszel
772bb602b0 chore(main): release 1.64.0 (#1206)
* chore(main): release 1.64.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-16 04:31:32 +01:00
Ruben Fiszel
5c8789b730 fix add_user 2023-02-16 04:26:36 +01:00
Ruben Fiszel
c7cd8e22d0 added connection tester 2023-02-15 21:36:13 +01:00
Ruben Fiszel
11c2c2704d display IPs to whitelist 2023-02-15 19:51:58 +01:00
Ruben Fiszel
06a8fcf666 dynamically reload schema if it changes 2023-02-15 17:56:43 +01:00
Ádám Kovács
8445697e31 feat(frontend): Trigger settings drawer with URL hash (#1185)
* feat(frontend): Trigger settings drawer by URL hash

* fix(frontend): Add setting auto-open to workspace selector

* fix(frontend): full path redirects

* fix(frontend): Slice safety check
2023-02-15 14:47:47 +01:00
Ruben Fiszel
e0b12f88d5 chore(main): release 1.63.2 (#1205)
* chore(main): release 1.63.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-15 12:21:27 +01:00
Ruben Fiszel
a2fbc57025 fix(psql): update pg client 2023-02-15 12:17:42 +01:00
Ruben Fiszel
3e5950a396 fix ee 2023-02-15 11:27:53 +01:00
Ruben Fiszel
c0b87cc7d7 send a webhook on new user + lazy static refactor (#1203)
* supercharge

* supercharge

* progress

* progress

* display config

* display config

* display config

* display config

* fix extensions

* fix build

* disable nsjail = false for test
2023-02-15 10:59:50 +01:00
Ruben Fiszel
81f64a4028 UX nits 2023-02-14 17:58:38 +01:00
Ruben Fiszel
6eecae6857 chore(main): release 1.63.1 (#1202)
* chore(main): release 1.63.1

* update versions

* fix change versions

* fix change versions

* Apply automatic changes

* update base64

* fix versions

* improve app setup

* improve app setup

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-14 15:54:25 +01:00
Ruben Fiszel
03eb1444c4 fix: update hub sync script 2023-02-14 15:00:10 +01:00
Ruben Fiszel
7f68ae888c chore(main): release 1.63.0 (#1183) 2023-02-14 14:34:35 +01:00
dependabot[bot]
1db407d983 chore(deps): bump python from 3.11.1-slim-buster to 3.11.2-slim-buster (#1197)
Bumps python from 3.11.1-slim-buster to 3.11.2-slim-buster.

---
updated-dependencies:
- dependency-name: python
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-14 14:30:17 +01:00
Ruben Fiszel
9f6bffab72 add email to cli resource fetching 2023-02-14 14:27:24 +01:00
Ruben Fiszel
b835c58427 implement goto for app buttons 2023-02-14 03:33:08 +01:00
Ruben Fiszel
d2eb7a40c5 remove unused dependencies 2023-02-14 02:44:41 +01:00
Ruben Fiszel
f446ca14f5 fix run dev 2023-02-14 02:42:19 +01:00
Ruben Fiszel
5b7ce39496 implement double ended slider 2023-02-13 22:46:42 +01:00
Ruben Fiszel
2789dc2e5f fix skipping setup 2023-02-13 18:49:13 +01:00
Ruben Fiszel
6b70dbcc61 update README 2023-02-13 17:58:49 +01:00
Ruben Fiszel
3474cd0687 fix initialization app 2023-02-13 17:18:01 +01:00
Ruben Fiszel
dabceae2ea fix linguo 2023-02-13 15:10:48 +01:00
Ruben Fiszel
64e5bcf4b6 fix admins workspace link 2023-02-13 15:05:27 +01:00
Ruben Fiszel
9767980ca0 fix minor UX bugs 2023-02-13 13:17:20 +01:00
Ruben Fiszel
100943443b update flow viewer 2023-02-13 12:36:04 +01:00
Ruben Fiszel
77a7b8a539 remove technical architecture 2023-02-13 02:35:07 +01:00
Kai Jellinghaus
69001bd61a Setup Experience (#1194)
* Squash

* Update SQLX
2023-02-13 02:32:29 +01:00
Ruben Fiszel
13b1055a5f update README 2023-02-13 02:26:06 +01:00
Ruben Fiszel
e5c4e2a754 Add video to README 2023-02-13 02:24:05 +01:00
Ruben Fiszel
5c0b0529df update README 2023-02-13 02:22:13 +01:00
dependabot[bot]
e825bc94dc chore(deps-dev): bump svelte from 3.55.0 to 3.55.1 in /frontend (#1189)
Bumps [svelte](https://github.com/sveltejs/svelte) from 3.55.0 to 3.55.1.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v3.55.0...v3.55.1)

---
updated-dependencies:
- dependency-name: svelte
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-13 00:11:25 +01:00
dependabot[bot]
4d558640a9 chore(deps-dev): bump svelte2tsx from 0.5.22 to 0.6.1 in /frontend (#1188)
Bumps [svelte2tsx](https://github.com/sveltejs/language-tools) from 0.5.22 to 0.6.1.
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte2tsx-0.5.22...svelte2tsx-0.6.1)

---
updated-dependencies:
- dependency-name: svelte2tsx
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-13 00:11:01 +01:00
Ruben Fiszel
996efa1ff2 better redirect on slack callback 2023-02-10 18:53:01 +01:00
Ruben Fiszel
3f2754b330 fix(python): return none if argument is missing 2023-02-10 18:47:45 +01:00
Ruben Fiszel
4aaa5d8fb8 fix BASE_INTERNAL_URL 2023-02-10 18:29:10 +01:00
Ruben Fiszel
c5c979b7d7 fix slack command 2023-02-10 01:51:42 +01:00
Ádám Kovács
a574270bc2 fix: Update references to the docs (#1191) 2023-02-10 00:08:15 +01:00
Ruben Fiszel
c8f0e23eae add clousql compatibility 2023-02-09 18:15:13 +01:00
Ruben Fiszel
42b94947c4 small flow improvements 2023-02-09 16:44:28 +01:00
Faton Ramadani
b03b3be154 feat(frontend): Minimal support for custom filenames (#1190) 2023-02-07 18:29:23 +01:00
Ruben Fiszel
3f8916cbc2 add MAX_RESULT_SIZE for CLOUD_HOSTED 2023-02-07 01:52:55 +01:00
Ruben Fiszel
ac991dddbc deprecate row connection in favor of connected + selectedRow 2023-02-05 20:57:47 +01:00
Ruben Fiszel
083a304645 display token fully 2023-02-05 17:28:33 +01:00
Ruben Fiszel
91491055fa add svg display result support 2023-02-05 17:15:40 +01:00
Ruben Fiszel
ae440203f0 script UX nits 2023-02-05 16:30:35 +01:00
Ruben Fiszel
ab432d628a import python code as raw to avoid reindent 2023-02-05 16:03:03 +01:00
Ruben Fiszel
e1b9247e11 use VmHwm mem instead of VmPeak for more accurate memory report 2023-02-05 15:30:02 +01:00
Ruben Fiszel
07c756f460 mem_peak use greatest 2023-02-05 14:59:31 +01:00
Ruben Fiszel
2ef6af4546 fix python default tabSize 2023-02-05 14:51:33 +01:00
Ruben Fiszel
a939771059 fix python type inference 2023-02-05 14:09:21 +01:00
Ruben Fiszel
8dc467b87a toast improvements 2023-02-05 10:12:49 +01:00
Ruben Fiszel
2ece1eb475 frontend misc 2023-02-05 10:04:26 +01:00
Ruben Fiszel
7a4da3907f frontend misc 2023-02-05 09:03:35 +01:00
Ruben Fiszel
b9d6e67791 frontend misc 2023-02-05 00:05:16 +01:00
Ruben Fiszel
f584062f13 feat: add mem peak info 2023-02-04 20:26:15 +01:00
Ruben Fiszel
265fbc5835 feat(worker): set oom_adj to 1000 to prioritize killing subprocess 2023-02-04 16:17:47 +01:00
Ruben Fiszel
2e7e57b62d update docker-compose 2023-02-04 15:50:28 +01:00
Ruben Fiszel
d17eeeecdc use unsecure websocket if unsecure connection 2023-02-04 15:50:28 +01:00
Ruben Fiszel
21c2007ebd use unsecure websocket if unsecure connection 2023-02-04 15:29:55 +01:00
Ruben Fiszel
90668902f5 avoid https on docker-compose 2023-02-04 15:20:12 +01:00
Ruben Fiszel
784aac9d1b typo 2023-02-04 01:55:05 +01:00
Ruben Fiszel
d4207db880 app checkbox nits 2023-02-04 01:50:09 +01:00
Ruben Fiszel
4ac9484305 app checkbox and text nits 2023-02-04 01:43:17 +01:00
Ruben Fiszel
0a8f177e02 chore(main): release 1.62.0 (#1170)
* chore(main): release 1.62.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-03 22:52:54 +01:00
Ruben Fiszel
cfa1e6f1e8 increase max concurrent runs 2023-02-03 22:39:36 +01:00
Ruben Fiszel
be526b2f23 prune args and not kwargs for function call 2023-02-03 22:37:26 +01:00
Ruben Fiszel
8bc97e0041 remove templates 2023-02-03 22:34:36 +01:00
Kai Jellinghaus
b9ac60f8bb feat: Add workspace webhook (#1158)
* Add workspace webhook

* Update SQLX

* Add webhook request histogram

* Move workspace webhook retrieval OOP

* Add removal to frontend, cache removed webhooks

* Remove WebhookUtil

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-02-03 19:50:18 +01:00
dependabot[bot]
c0a8545704 chore(deps-dev): bump svelte-preprocess from 5.0.0 to 5.0.1 in /frontend (#1164)
Bumps [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/sveltejs/svelte-preprocess/releases)
- [Changelog](https://github.com/sveltejs/svelte-preprocess/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte-preprocess/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: svelte-preprocess
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-03 19:50:04 +01:00
Kai Jellinghaus
cdd16195ae feat(cli): 2-Way sync (#1071)
* Export file type from each file

* Fix example scripts

* Strongly type CLI files

* Allow bash files

* Update API version

* Remove useless files

* WIP: Diff based push

* Fixup other code

* Implement Flow diffing

* Implement resource type

* Remaining impls

* WIP

* Fix missing file error

* Fix misstyping

* Improve error message

* Fix type inferrence

* Allow REMOVE everywhere

* Fix empty changeset

* Fix error message

* Fix type inferrence 2

* Fix variable diffs

* Fix include checks

* Move push & pull

* Handle script in sync

* Handle scripts

* Allow multi-path creation

* Fix merge conflicts

* Fix #1173

* Update Dependencies

* Add missing await

* Apply review comments

* Fix diff

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-02-03 19:49:46 +01:00
Ruben Fiszel
406cba4e73 improve instructions 2023-02-03 02:19:58 +01:00
Ruben Fiszel
8d6a8386be refactor components to ease adding custom components 2023-02-03 02:11:06 +01:00
Ruben Fiszel
1a626980df revert single file components change 2023-02-03 00:00:28 +01:00
Ruben Fiszel
23007f7a71 feat: adding worker_busy 2023-02-02 23:57:53 +01:00
Ádám Kovács
9f5500c196 fix(frontend): Various fixes and improvements (#1177)
* fix(frontend): Editor bar

* fix(frontend): Keep settings tabs always visible

* fix(frontend): Keep app header on top in preview
2023-02-02 23:00:43 +01:00
Ruben Fiszel
a82a2efa6a fix nosniff html component 2023-02-02 22:10:13 +01:00
Ádám Kovács
3305481d5d feat(frontend): App initial loading animations (#1176)
* feat(frontend): App initial loading animations

* fix(frontend): Optional prop
2023-02-02 17:55:11 +01:00
Kai Jellinghaus
42691bc1bd CLI hints (#1140)
* various improvements

* passing to kai

* passing to kai

* Outline InlineCodeCopy

* Add CLI setup steps

* Workaround #1173

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-02-02 05:30:12 -08:00
Ruben Fiszel
99568eaa47 feat: add WHITELIST_WORKSPACES and BLACKLIST_WORKSPACES 2023-02-02 07:57:03 +01:00
Ruben Fiszel
68500b12b2 improve README 2023-02-02 07:01:18 +01:00
Ruben Fiszel
f171cd8b7c fix: navigate to new script page before saving script 2023-02-02 00:31:31 +01:00
Ruben Fiszel
0ca431b6cb add privacy policy and terms of service links 2023-02-01 23:10:43 +01:00
Ruben Fiszel
cb9c0846ac fix graph viewer for negative ids 2023-02-01 20:56:58 +01:00
Ádám Kovács
bc8d1a375e fix(frontend): Render popups above components in app editor (#1171)
* fix(frontend): Render popups above items in apps

* Add explanation
2023-02-01 17:52:09 +01:00
Ruben Fiszel
0921ba0085 feat: add INCLUDE_HEADERS env variable to pass value from request headers 2023-02-01 02:22:28 +01:00
Ruben Fiszel
14b32be8b2 feat(python): add ADDITIONAL_PYTHON_PATHS 2023-02-01 02:04:53 +01:00
Ruben Fiszel
b67550a790 fix nextId for components 2023-01-31 20:49:02 +01:00
dependabot[bot]
06347746c0 chore(deps-dev): bump @sveltejs/package from 1.0.1 to 1.0.2 in /frontend (#1165)
Bumps [@sveltejs/package](https://github.com/sveltejs/kit/tree/HEAD/packages/package) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/master/packages/package/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/package@1.0.2/packages/package)

---
updated-dependencies:
- dependency-name: "@sveltejs/package"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-31 20:05:16 +01:00
dependabot[bot]
0b4288ad9c chore(deps-dev): bump vite from 4.0.3 to 4.0.4 in /frontend (#1166)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.0.3 to 4.0.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.0.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-31 20:05:00 +01:00
Ruben Fiszel
a6dcb218ff ci: seperate openbb group from main 2023-01-31 20:04:20 +01:00
Ruben Fiszel
6fe93dac58 chore(main): release 1.61.1 (#1169)
* chore(main): release 1.61.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-01-31 20:02:29 +01:00
Ruben Fiszel
202b04bb08 fix nextId for components 2023-01-31 20:01:48 +01:00
Ruben Fiszel
df8cc1f248 fix(backend): compile issue 2023-01-31 19:50:22 +01:00
Ruben Fiszel
61e3214f9e chore(main): release 1.61.0 (#1091)
* chore(main): release 1.61.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-01-31 18:51:42 +01:00
Ruben Fiszel
d663884a99 more folders permission 2023-01-31 16:01:13 +01:00
Ruben Fiszel
a2ed69f87f fix demoting users 2023-01-31 15:23:58 +01:00
Ruben Fiszel
f377c84f5a feat: add sync webhook for flows 2023-01-31 10:21:48 +01:00
Ruben Fiszel
772c768cda feat(python): add support for extra args in python 2023-01-31 09:52:29 +01:00
Ruben Fiszel
a4f8f9e1cf feat(frontend): add plotly support 2023-01-31 08:04:46 +01:00
dependabot[bot]
dfae34d9b4 chore(deps): bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 (#1163)
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.3.5 to 1.3.6.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.3.5...v1.3.6)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-30 18:32:13 +00:00
Ruben Fiszel
0bc995bd07 clarify exponential backoff 2023-01-30 08:58:09 +01:00
Ruben Fiszel
161e76c500 clarify exponential backoff 2023-01-30 08:49:58 +01:00
Ruben Fiszel
c186db4e3b handle backquotes in template 2023-01-30 08:31:21 +01:00
Ruben Fiszel
652eb65082 allow whitespace in requirements override 2023-01-30 05:06:13 +01:00
Ruben Fiszel
a98ba6dc41 parseJson on resource changes 2023-01-30 03:55:01 +01:00
Ruben Fiszel
080ecb04d7 feat: add resource and resource type from json 2023-01-29 16:27:51 +01:00
Ruben Fiszel
8de3804d4a flow builder args handling improvements 2023-01-28 23:22:51 +01:00
Ruben Fiszel
008bd32ec1 fix entirecachetar path 2023-01-28 18:54:04 +01:00
Ruben Fiszel
a2f679ffc9 add support for openbb 2023-01-28 03:57:25 +01:00
Ruben Fiszel
2d3fe64bc4 add python-310 target 2023-01-28 03:34:40 +01:00
Ruben Fiszel
b7db4c78c4 feat: add PIP_LOCAL_DEPENDENCIES 2023-01-28 03:09:47 +01:00
Ruben Fiszel
d0fc40b483 grant all privilege to table favorite 2023-01-27 22:08:46 +01:00
Ruben Fiszel
1efae55d36 update stripe products references 2023-01-27 20:59:05 +01:00
Ruben Fiszel
791a772199 increase token size for refresh token 2023-01-27 18:56:42 +01:00
Ruben Fiszel
285b896a1d add stripe integration 2023-01-27 07:58:16 +01:00
Ruben Fiszel
098cd96655 add stripe integration 2023-01-27 07:29:51 +01:00
Adam Vollrath
4eee71a8fb Enable GitHub OAuth for Uffizzi Previews. (#1157)
* Enable GitHub OAuth for Uffizzi Previews.

* empty

* more specific build conditions

* Increase memory resources for `windmill` container.
2023-01-26 18:48:28 +01:00
Ádám Kovács
c01bf70f62 feat(frontend): Add HTML result rendering (#1160)
* feat(frontend): Add HTML result rendering

* fix(frontend): Add reference for XSS

* fix(frontend): Require approval only in apps

* fix(frontend): Require approval in public apps
2023-01-26 18:03:25 +01:00
Faton Ramadani
3ed16b88a4 feat(frontend): Add app inputs configurations (#1142)
* feat(frontend): Add app inputs configurations

* feat(frontend): only display resource input by default

* feat(frontend): Fix no resource field empty display

* feat(frontend): open the drawer when forked from the hub
2023-01-26 15:51:38 +01:00
Ádám Kovács
18f30c8286 fix(frontend): Update app scripts pane (#1146)
* fix(frontend): Update app scripts pane

* fix wrapping content

* import capitalize util

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-01-26 15:51:09 +01:00
Ádám Kovács
f221a6c17f fix(frontend): Add highlight to selected workspace (#1159) 2023-01-26 15:11:29 +01:00
Ruben Fiszel
51a710ffa0 do not copy necessarily for the flow object viewer 2023-01-26 03:20:01 +01:00
Ruben Fiszel
ad0c836129 add instructions for triggers on flows 2023-01-26 01:06:53 +01:00
Ruben Fiszel
be6707b461 for python change default editor tab to 4 2023-01-25 20:40:40 +01:00
Ruben Fiszel
c227ba2794 loader for flowjobresult 2023-01-25 20:33:33 +01:00
Ruben Fiszel
d60a770eb7 flow preview status viewer display more info 2023-01-25 14:14:14 +01:00
Ruben Fiszel
30d83a942c restrict number of jobs in the queue for free users 2023-01-25 13:34:08 +01:00
Ruben Fiszel
98bf9355dc implement MAX_FREE_CONCURRENT_RUNS 2023-01-25 12:58:29 +01:00
Ruben Fiszel
a3928101af worker client should use internal url 2023-01-25 00:10:37 +01:00
Ruben Fiszel
6e77757939 nodraft for editing flow from run 2023-01-24 22:57:34 +01:00
Ruben Fiszel
66c5d6e400 fix arg inputs 2023-01-24 22:44:52 +01:00
Ruben Fiszel
b0c184767c update pricing page 2023-01-24 21:58:44 +01:00
Ruben Fiszel
1165cf6e24 UX improvements for flows 2023-01-24 21:53:41 +01:00
Ruben Fiszel
a9640107f3 background scripts now part of the policy 2023-01-24 15:09:27 +01:00
Faton Ramadani
73bc13bb7d feat(frontend): Open debug runs from component (#1155)
* feat(frontend): Add debug runs

* feat(frontend): Fix build

* feat(frontend): revert implementation

* feat(frontend): Fix error displa

* feat(frontend): Fix error display

* feat(frontend): Fix preview mode

* feat(frontend): Fix error display
2023-01-24 14:34:19 +01:00
Ádám Kovács
e98966283d fix(frontend): App table action button cell (#1149)
Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-01-24 12:20:27 +01:00
Ruben Fiszel
8ad2344ec6 improve cancel approval error message 2023-01-24 12:11:39 +01:00
Ruben Fiszel
b77c239f30 fix: improvements for error handling as first step of flow 2023-01-24 11:16:44 +01:00
Ruben Fiszel
15cb83936b error handler improvements 2023-01-24 07:30:07 +01:00
Ruben Fiszel
e2157ee1c0 fix listing runs from schedule 2023-01-24 06:55:11 +01:00
dependabot[bot]
bdfd50b970 chore(deps): bump @leeoniya/ufuzzy from 0.9.0 to 0.9.1 in /frontend (#1153)
Bumps [@leeoniya/ufuzzy](https://github.com/leeoniya/uFuzzy) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/leeoniya/uFuzzy/releases)
- [Commits](https://github.com/leeoniya/uFuzzy/compare/0.9.0...0.9.1)

---
updated-dependencies:
- dependency-name: "@leeoniya/ufuzzy"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 04:04:34 +01:00
dependabot[bot]
4debfd68dc chore(deps-dev): bump @typescript-eslint/eslint-plugin in /frontend (#1151)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.46.1 to 5.49.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.49.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 04:03:59 +01:00
dependabot[bot]
410032060a chore(deps): bump chartjs-adapter-date-fns in /frontend (#1154)
Bumps [chartjs-adapter-date-fns](https://github.com/chartjs/chartjs-adapter-date-fns) from 2.0.0 to 3.0.0.
- [Release notes](https://github.com/chartjs/chartjs-adapter-date-fns/releases)
- [Commits](https://github.com/chartjs/chartjs-adapter-date-fns/compare/v2.0.0...v3.0.0)

---
updated-dependencies:
- dependency-name: chartjs-adapter-date-fns
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 04:03:32 +01:00
Faton Ramadani
22b8fed9d9 feat(frontend): Display error as an icon in order to avoid clutter wh… (#1143)
* feat(frontend): Display error as an icon in order to avoid clutter when an error occures

* feat(frontend): Fix build

* feat(frontend): Fix error icon position
2023-01-23 18:03:31 +01:00
Ruben Fiszel
0172587b12 feat: add sql as a valid type in Python 2023-01-23 15:58:33 +01:00
Faton Ramadani
f05f9e4edb feat(frontend): add selectedRowIndex to the table outputs (#1145) 2023-01-23 15:39:15 +01:00
Faton Ramadani
24b1fa0ae3 fix(frontend): Make sure AppSelect items are an array (#1144)
* fix(frontend): Make sure AppSelect items are an array

* fix(frontend): restore placeholder
2023-01-23 05:37:38 -08:00
Ruben Fiszel
fedb9fe74c update pricing page 2023-01-22 18:34:05 +01:00
Ruben Fiszel
5408da829c update pricing page 2023-01-22 18:20:06 +01:00
Ruben Fiszel
448e45b7e4 fix privilege on cloud 2023-01-22 00:53:29 +01:00
Ruben Fiszel
156f257619 add import_map to nsjail 2023-01-21 23:28:11 +01:00
Ruben Fiszel
7f40373fd6 feat(deno): allow overriding deno sandboxing with DENO_FLAGS' 2023-01-21 22:54:14 +01:00
Ruben Fiszel
f2ec03ba1b implement relative imports for deno 2023-01-21 19:12:22 +01:00
Ruben Fiszel
9a79c5537e fix compile 2023-01-21 18:49:26 +01:00
Ruben Fiszel
cf156b0591 support BigInt serialization 2023-01-21 18:41:58 +01:00
Ruben Fiszel
832ddabdf2 feat(deno): add support for DENO_AUTH_TOKENS 2023-01-21 18:36:17 +01:00
Ruben Fiszel
22080312f0 description at bottom 2023-01-21 17:33:23 +01:00
Ruben Fiszel
165095376d description at bottom 2023-01-21 10:19:05 +01:00
Ruben Fiszel
2a816b9e7b remove square from oauth 2023-01-21 09:37:10 +01:00
Ruben Fiszel
a7d03f4e25 square use req_body_auth 2023-01-21 09:26:39 +01:00
Ruben Fiszel
5e18e580f7 fix superadmins permissions 2023-01-21 08:09:49 +01:00
Felipe Albertao
9b7906e43d Added Square API OAuth (#1141) 2023-01-21 07:41:52 +01:00
Ruben Fiszel
2143bbdc94 add support for python import shopifyAPI 2023-01-20 19:52:18 +01:00
Ruben Fiszel
51a8810aa0 feat: add QUEUE_LIMIT_WAIT_RESULT 2023-01-20 19:39:31 +01:00
Faton Ramadani
8f906026b3 fix(frontend): Add default value for text, number and date input + fix issues with number input + add date input in the settings panel (#1135)
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-01-20 18:31:10 +01:00
Faton Ramadani
aa6de3bb57 fix(frontend): Prevent modal from hijacking all keypress event (#1136) 2023-01-20 15:39:37 +01:00
Faton Ramadani
71a443e3c5 fix(frontend): Check if hiddenInlineScripts are undefined before iterating over them (#1134) 2023-01-20 11:49:43 +01:00
Ruben Fiszel
d1526d9c72 fix deno nsjail vmem 2023-01-20 11:49:08 +01:00
Ruben Fiszel
2456e35908 install nodejs only on heavy 2023-01-19 21:01:30 +01:00
Ruben Fiszel
f359015d89 bump default bash tmp size 2023-01-19 20:32:08 +01:00
Ruben Fiszel
b8155f4d27 update npm used in heavy 2023-01-19 20:00:01 +01:00
Ruben Fiszel
2ce903d7e7 add more details about signals termination 2023-01-19 18:20:24 +01:00
Ruben Fiszel
d4765dee16 add nodejs npm to heavy image 2023-01-19 16:22:33 +01:00
Faton Ramadani
a78486d7e0 fix(frontend): Make sure that old apps are rendering properly (#1132) 2023-01-19 15:14:15 +01:00
Faton Ramadani
e969af9e44 feat(frontend): add support for background scripts + add FormButtonCo… (#1124)
* feat(frontend): add support for background scripts + add FormButtonComponnet

* feat(frontend): add background script fields

* feat(frontend): Handle deletion + display of results

* feat(frontend): Fix background script ids: Runnable ids needs to be valid js expression, because they are used in the evaluator.

* feat(frontend): add a portal to display modal

* feat(frontend): fix build

* feat(frontend): Add missing props

* feat(frontend): Fix run button + add tooltip + fix icon
2023-01-19 14:52:32 +01:00
Ruben Fiszel
e371fda1ce relax nsjail limits 2023-01-19 14:51:43 +01:00
Ruben Fiszel
ee14bed3d9 relax nsjail limits 2023-01-19 14:35:06 +01:00
Faton Ramadani
3d873ed51c fix(frontend): add missing condition to properly select first row (#1128) 2023-01-19 14:08:32 +01:00
Ruben Fiszel
d85f550611 handle empty flow module input transforms 2023-01-19 12:22:37 +01:00
Faton Ramadani
6c9daf7002 fix(frontend): fix first row selection (#1125)
* fix(frontend): fix first row selection

* fix(frontend): fix order of conditions
2023-01-19 11:52:34 +01:00
Ádám Kovács
6a88e8c4f4 feat(frontend): Add app preview lock (#1127)
* feat(frontend): Add locked state to app preview

* change color

* add transition
2023-01-19 11:27:16 +01:00
Ruben Fiszel
99246382ee minor UI fixes 2023-01-18 17:56:40 +01:00
Ruben Fiszel
73e1018f65 minor UI fixes 2023-01-18 17:08:34 +01:00
Ruben Fiszel
f1157e3be2 remove vega lite actions 2023-01-18 16:06:56 +01:00
Ruben Fiszel
bd79938bed feat(frontend): add vega-lite component 2023-01-18 15:49:47 +01:00
Ádám Kovács
173093a403 fix(frontend): Prepare app scripts code for export (#1123) 2023-01-18 12:43:24 +01:00
Ruben Fiszel
ed61df6d8a UX nits 2023-01-18 11:30:08 +01:00
Ruben Fiszel
24feb64dfe fix reporting duration above 60s 2023-01-18 11:23:25 +01:00
Ruben Fiszel
b3eaf86e40 HTMLComponent refresh on content change 2023-01-18 10:24:29 +01:00
Ádám Kovács
4dfdf374af feat(frontend): Add tooltip to app recompute (#1122) 2023-01-18 10:10:47 +01:00
Ruben Fiszel
45191621d2 UX nits on schedule 2023-01-18 09:42:49 +01:00
Ruben Fiszel
4d60be738e UX nits on schedule 2023-01-18 09:38:54 +01:00
Ruben Fiszel
eef3bab6e4 feat(backend): add queue_limit + configurable timeout + fix timeout cancel 2023-01-18 09:02:38 +01:00
Ruben Fiszel
14ebb6dc32 fix all cases where schema has been set to undefined 2023-01-18 06:56:44 +01:00
Ruben Fiszel
9e602691b6 do not cancel the job on leaving the preview page 2023-01-18 00:31:55 +01:00
Ruben Fiszel
02045001d2 do not cancel the job on leaving the preview page 2023-01-18 00:14:29 +01:00
Faton Ramadani
3c483f5337 fix(frontend): Select the first row by default, and remove the abilit… (#1121)
* fix(frontend): Select the first row by default, and remove the ability to deselect + fix table header z-index

* fix(frontend): restore z-index, under the floating tags
2023-01-17 19:10:06 +01:00
Ruben Fiszel
bcb5c3697d inline tailwindcss for html component in apps 2023-01-17 17:48:40 +01:00
Ruben Fiszel
0e50579089 frontend(apps): html component uses iframe for security 2023-01-17 17:29:01 +01:00
Ádám Kovács
59c4cc2058 fix(frontend): App table header z-index (#1120) 2023-01-17 14:00:54 +01:00
Ádám Kovács
462adbe42f fix(frontend): Runnable table overflow (#1119) 2023-01-17 12:51:14 +01:00
Ruben Fiszel
d708b6faa5 fix permissioned_as from username 2023-01-17 12:49:24 +01:00
Ruben Fiszel
40d566ab8e remove unecessary badge width 2023-01-17 12:32:30 +01:00
Ruben Fiszel
c5d712c977 ci: build both amd and arm on same workflow leveraging depot 2023-01-17 12:16:36 +01:00
Ádám Kovács
631a3da17f fix(frontend): Show app builder header always on top (#1118)
* fix(frontend): Keep app header always on top

* various fixes

* fix
2023-01-17 12:02:21 +01:00
Ruben Fiszel
ff5145d7bd UI nits 2023-01-17 12:00:17 +01:00
Ruben Fiszel
d1131c1a16 various fixes 2023-01-17 11:30:37 +01:00
Ruben Fiszel
9b89df9ecf various fixes 2023-01-17 11:30:22 +01:00
Ruben Fiszel
acff87a09f all 2023-01-17 08:09:46 +01:00
Ruben Fiszel
a182ce2d2c all 2023-01-17 08:09:39 +01:00
Ruben Fiszel
0954b5a4f2 ci: npm run check generate backend client 2023-01-17 07:40:40 +01:00
Ruben Fiszel
d297223fa5 ci: npm check on frontend change and uffizzi only on backend changes 2023-01-17 07:38:05 +01:00
dependabot[bot]
9bccb90c7a chore(deps-dev): bump prettier from 2.8.1 to 2.8.3 in /frontend (#1111)
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.1 to 2.8.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.1...2.8.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-01-17 07:17:42 +01:00
dependabot[bot]
6403259e2a chore(deps): bump @tanstack/svelte-table in /frontend (#1112)
Bumps [@tanstack/svelte-table](https://github.com/tanstack/table) from 8.7.2 to 8.7.6.
- [Release notes](https://github.com/tanstack/table/releases)
- [Commits](https://github.com/tanstack/table/compare/v8.7.2...v8.7.6)

---
updated-dependencies:
- dependency-name: "@tanstack/svelte-table"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-01-17 06:53:11 +01:00
Ruben Fiszel
fa367f4128 point uffizzi lsp to 60d image 2023-01-17 06:51:17 +01:00
Ruben Fiszel
164f281dd0 fix uffizzi preview 2023-01-17 06:46:37 +01:00
Adam Vollrath
760cb31bae Use Uffizzi workflow v2 and other minor improvements. (#1114)
* Use Uffizzi workflow `v2` and other minor improvements.

* Specify Uffizzi Preview URL as target for `playwright`.

Answers https://github.com/windmill-labs/windmill/pull/1106#issuecomment-1384334128

* recheck
2023-01-16 20:32:46 +01:00
Faton Ramadani
4f869811fe fix(frontend): Fix id generation when a second action (#1110) 2023-01-16 19:07:22 +01:00
Ruben Fiszel
0f26a3b111 playwright against uffizzi (#1109)
* all

* fix perms

* move build to depot

* login

* revert to docker build

* revert to docker build

* fix playwright
2023-01-16 18:01:45 +01:00
Vibhav Bobade
fadae2d2b7 create preview environments for every PR using Uffizzi (#1106)
* Integrate Uffizzi

* remove lsp build

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-01-16 17:03:31 +01:00
Faton Ramadani
12b00a808d feat(frontend): Add Supabase resource (#1107)
* feat(frontend): Add Supabase resource

* feat(frontend): Added an image
2023-01-16 16:54:06 +01:00
Ádám Kovács
f0435f5f81 fix(frontend): Playwright (#1108)
* fix(frontend): Playwright

* fix(frontend): Update test base url

* feat(ci): Add base url to playwright
2023-01-16 16:51:14 +01:00
Ruben Fiszel
f018d025a2 fix schedule permissions 2023-01-16 16:42:42 +01:00
Ruben Fiszel
5d720e96e6 add row to eval input 2023-01-16 13:23:31 +01:00
Ruben Fiszel
e1b77b0d5f migrate more builds to depot.dev 2023-01-16 10:56:20 +01:00
Ruben Fiszel
2db6424f9d add dynamic disabled field to button 2023-01-16 01:52:18 +01:00
Ruben Fiszel
9a58145f72 add selenium-wire exception 2023-01-15 22:48:49 +01:00
Kyle Galbraith
51c1ae937c add packages: write for ghcr (#1105) 2023-01-15 18:29:18 +01:00
Ruben Fiszel
26bd9ac9aa remove comingsoon for UIs 2023-01-15 17:58:40 +01:00
Kyle Galbraith
3ba28b8156 Switch to Depot for Arm image build (#1104)
* switch to Depot for native arm builds without emulation

* set Depot project id
2023-01-15 17:55:25 +01:00
Ruben Fiszel
3f318b4ac4 add license key check 2023-01-15 12:06:37 +01:00
Ruben Fiszel
452c6de7e3 script_helpers nit 2023-01-15 08:32:37 +01:00
Ruben Fiszel
b1a86db58b nit error handling 2023-01-15 07:22:35 +01:00
Ruben Fiszel
b29eccf4bb : 2023-01-15 01:39:07 +01:00
Ruben Fiszel
ba28f6c62a error improvements 2023-01-15 01:28:13 +01:00
Ruben Fiszel
279f671dcf improve default connection for failure module 2023-01-14 19:22:59 +01:00
Ruben Fiszel
954cc1bb95 fix tests wrt to new error handling 2023-01-14 18:47:37 +01:00
Ruben Fiszel
776d4c9ca1 error handling improvements 2023-01-14 17:46:03 +01:00
Ruben Fiszel
6949516e63 improve error handling 2023-01-14 15:08:47 +01:00
Ruben Fiszel
f6c13b0324 avoid annoying panel resizing on value change 2023-01-14 14:29:12 +01:00
Ádám Kovács
d811675882 feat(frontend): Add surreal db logo (#1102) 2023-01-14 14:17:43 +01:00
Ruben Fiszel
735aee952a add hub compatible json export to apps 2023-01-14 09:10:39 +01:00
Ruben Fiszel
9432f4c1fa minor tooltip fix 2023-01-14 07:45:15 +01:00
Ruben Fiszel
5bf5a4f86e sqlx fix 2023-01-13 22:27:18 +01:00
Ruben Fiszel
60ef8f569b run_wait_result improvements: cancel + early tx drop 2023-01-13 21:30:29 +01:00
Ádám Kovács
7bc59d9d26 fix(frontend): App script list panel overflow (#1101) 2023-01-13 20:24:18 +01:00
Ruben Fiszel
bd4c7b7957 minor improvements 2023-01-13 18:52:34 +01:00
Ádám Kovács
172b5dba8f feat(frontend): Update app table component styles (#1100)
* feat(frontend): Update app table component styles

* feat(frontend): Update table search style
2023-01-13 18:44:21 +01:00
Ruben Fiszel
b478ba8c45 clarify run detail page + allow i64 for MAX_LOG_SIZE 2023-01-13 18:39:17 +01:00
Ruben Fiszel
71708b7566 add MAX_LOG_SIZE env params 2023-01-13 14:46:07 +01:00
Ádám Kovács
c50c7406f2 feat(frontend): Add default codes to app editor (#1099) 2023-01-13 13:48:28 +01:00
Ruben Fiszel
00357261d5 add psycopg-pool to psycopg default install 2023-01-13 12:16:48 +01:00
Ruben Fiszel
c121908716 fix table static exports 2023-01-13 12:08:37 +01:00
Ádám Kovács
2191e85231 feat(frontend): Add more integration icons (#1097)
* feat(frontend): Add more integration icons

* fix

* fix
2023-01-13 08:48:31 +01:00
Ruben Fiszel
a85a964819 add psycopg3 to the list of support imports 2023-01-13 07:54:05 +01:00
Ruben Fiszel
2ee418b6bf add polars to lsp 2023-01-12 23:12:07 +01:00
Ruben Fiszel
8f72c32fbe various improvements 2023-01-12 22:20:54 +01:00
Ruben Fiszel
1cc5059179 fix execute_component tokens 2023-01-12 16:53:54 +01:00
Ruben Fiszel
9280a5132e minor app fixes 2023-01-12 13:04:17 +01:00
Ruben Fiszel
a626aa6175 minor app fixes 2023-01-12 12:51:27 +01:00
Ádám Kovács
339742ca77 fix(frontend): App form component display (#1096) 2023-01-12 11:41:05 +01:00
Ádám Kovács
43e069eb96 fix(frontend): Use absolute path on connect images (#1095) 2023-01-12 11:40:41 +01:00
Ruben Fiszel
8e1fecfda0 make openapi viewer rely on local assets only 2023-01-11 21:02:53 +01:00
Ruben Fiszel
3e1c9463d6 make openapi viewer rely on local assets only 2023-01-11 20:41:52 +01:00
Ruben Fiszel
1337811438 feat: add openapi viewer (#1094)
* all

* all

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix typecheck

* fix tests

* fix tests

* fix tests

* all
2023-01-11 20:28:50 +01:00
Ruben Fiszel
a028772bf4 fix typecheck 2023-01-11 17:21:02 +01:00
Ádám Kovács
0419e7e1c9 fix(frontend): Revert component input panel change (#1092) 2023-01-11 16:56:58 +01:00
Ádám Kovács
bdfc38d954 feat(frontend): Add copy button option to app text display component (#1090)
* feat(frontend): Add copy button to app text

* feat(frontend): Add tooltip to component configs

* fix(frontend): Remove copy button text
2023-01-11 16:05:19 +01:00
Ruben Fiszel
dbd632375c fix backend tests 2023-01-11 14:56:05 +01:00
Ruben Fiszel
2dfbf84d62 chore(main): release 1.60.0 (#1086)
* chore(main): release 1.60.0

* Update CHANGELOG.md

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-01-11 12:14:48 +01:00
Ádám Kovács
cb25f88300 fix(frontend): Keep pane resizer under open drawer (#1089) 2023-01-11 12:06:05 +01:00
Ádám Kovács
3cbe5d1f66 fix(frontend): Remove comment remnant (#1088) 2023-01-11 11:38:55 +01:00
Ádám Kovács
e93c511bb5 fix(frontend): App form component (#1087)
* fix(frontend): Make app form usage more clear

* fix(frontend): Form orientation

* fix(frontend): Form message format

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-01-11 11:24:46 +01:00
Ruben Fiszel
6bc0320bff add Use as template for apps 2023-01-11 11:00:10 +01:00
Ruben Fiszel
3418a07c45 various app fixes 2023-01-11 10:57:30 +01:00
Ruben Fiszel
c0df9a5e20 update axum 2023-01-11 08:19:20 +01:00
Ruben Fiszel
b4e9468461 update change-versions 2023-01-11 04:54:55 +01:00
Ruben Fiszel
a14623feaa feat: add 'add user to workspace' 2023-01-11 00:51:04 +01:00
Ádám Kovács
342ea5609b fix(frontend): App minor fixes (#1085)
* fix(frontend): Tooltip contrast

* fix(frontend): App button padding
2023-01-10 22:53:11 +01:00
Ádám Kovács
8e72007368 feat(frontend): Add frost to color palette (#1084) 2023-01-10 22:52:37 +01:00
Ruben Fiszel
48d87ab12c various improvements 2023-01-10 11:21:16 +01:00
dependabot[bot]
4e0c2d06fc chore(deps-dev): bump simple-svelte-autocomplete in /frontend (#1083)
Bumps [simple-svelte-autocomplete](https://github.com/pstanoev/simple-svelte-autocomplete) from 2.5.0 to 2.5.1.
- [Release notes](https://github.com/pstanoev/simple-svelte-autocomplete/releases)
- [Commits](https://github.com/pstanoev/simple-svelte-autocomplete/compare/v2.5.0...v2.5.1)

---
updated-dependencies:
- dependency-name: simple-svelte-autocomplete
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 20:00:03 +01:00
Ruben Fiszel
cbddd6b98b chore: update svelte-check 2023-01-09 19:59:17 +01:00
dependabot[bot]
35297d4b52 chore(deps): bump svelte-select from 5.0.1 to 5.0.2 in /frontend (#1081)
Bumps [svelte-select](https://github.com/rob-balfre/svelte-select) from 5.0.1 to 5.0.2.
- [Release notes](https://github.com/rob-balfre/svelte-select/releases)
- [Changelog](https://github.com/rob-balfre/svelte-select/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rob-balfre/svelte-select/compare/v5.0.1...v5.0.2)

---
updated-dependencies:
- dependency-name: svelte-select
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 18:54:41 +00:00
Ruben Fiszel
2c717e00e5 fix computing policy prefix for hub scripts 2023-01-09 19:46:24 +01:00
dependabot[bot]
3bc1e6248b chore(deps): bump @leeoniya/ufuzzy from 0.8.0 to 0.9.0 in /frontend (#1080)
Bumps [@leeoniya/ufuzzy](https://github.com/leeoniya/uFuzzy) from 0.8.0 to 0.9.0.
- [Release notes](https://github.com/leeoniya/uFuzzy/releases)
- [Commits](https://github.com/leeoniya/uFuzzy/compare/0.8.0...0.9.0)

---
updated-dependencies:
- dependency-name: "@leeoniya/ufuzzy"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 18:30:33 +00:00
Ruben Fiszel
2e2067fe13 compute policy also for action buttons 2023-01-09 19:24:16 +01:00
dependabot[bot]
367903e359 chore(deps-dev): bump @playwright/test in /frontend (#1079)
Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.29.1 to 1.29.2.
- [Release notes](https://github.com/Microsoft/playwright/releases)
- [Commits](https://github.com/Microsoft/playwright/compare/v1.29.1...v1.29.2)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 18:23:48 +00:00
dependabot[bot]
37f2de6196 chore(deps-dev): bump svelte-dnd-action in /frontend (#1076)
Bumps [svelte-dnd-action](https://github.com/isaacHagoel/svelte-dnd-action) from 0.9.21 to 0.9.22.
- [Release notes](https://github.com/isaacHagoel/svelte-dnd-action/releases)
- [Changelog](https://github.com/isaacHagoel/svelte-dnd-action/blob/master/release-notes.md)
- [Commits](https://github.com/isaacHagoel/svelte-dnd-action/commits)

---
updated-dependencies:
- dependency-name: svelte-dnd-action
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 18:21:24 +00:00
Ruben Fiszel
5505d0a7c9 fix infering schema for inlineeditor 2023-01-09 18:57:30 +01:00
Ruben Fiszel
c61f9d4d66 fix infering schema for inlineeditor 2023-01-09 18:46:59 +01:00
Ruben Fiszel
18946918c3 also update schema when moving flow 2023-01-09 17:26:45 +01:00
Ruben Fiszel
e676c18b5f fix approval page approve endpoint empty payload 2023-01-09 17:19:56 +01:00
Ruben Fiszel
a9cce15b0c nit: close drawer when connecting oauth in new window 2023-01-09 16:52:05 +01:00
Ruben Fiszel
eb49dd8216 nits: clarify resume messages 2023-01-09 16:46:36 +01:00
Ruben Fiszel
e2f2db2d7f edit operatorOnly on toggle change 2023-01-09 16:42:02 +01:00
Ruben Fiszel
6e9c61a7fb (frontend) remove duplicate of folders from folder list name 2023-01-09 16:14:38 +01:00
Ruben Fiszel
4d5acf6f52 creator of group is also automatically a member 2023-01-09 16:08:10 +01:00
Ruben Fiszel
55e2bf9fce bump backend versions 2023-01-09 15:35:28 +01:00
Ruben Fiszel
daf8e2e8be fix backend tests 2023-01-09 14:51:27 +01:00
Ruben Fiszel
b5b13518da chore(main): release 1.59.0 (#1074)
* chore(main): release 1.59.0

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-01-09 14:03:36 +01:00
Ruben Fiszel
d3e2cf0779 fix backend tests 2023-01-09 14:00:17 +01:00
Ruben Fiszel
5347cd46a9 feat: add relative imports for python scripts (#1075)
* all

* all
2023-01-09 13:49:30 +01:00
Ruben Fiszel
32d90d4f18 add svelte as peer dep 2023-01-09 11:48:38 +01:00
Ruben Fiszel
1076c4ffc1 bump versions 2023-01-09 11:46:30 +01:00
Ádám Kovács
a84eb9b1f7 fix(frontend): Iconed resource height issue (#1073) 2023-01-09 11:44:16 +01:00
Ruben Fiszel
9540e6cec3 add appwrite and linkding logos 2023-01-07 19:16:41 +01:00
440 changed files with 27129 additions and 7670 deletions

5
.github/uffizzi/caddy/Caddyfile vendored Normal file
View File

@@ -0,0 +1,5 @@
localhost {
bind 0.0.0.0
reverse_proxy /ws/* http://0.0.0.0:3001
reverse_proxy /* http://0.0.0.0:8000
}

View File

@@ -0,0 +1,53 @@
version: '3.7'
x-uffizzi:
ingress:
service: windmill
port: 8000
services:
db:
image: postgres:14
environment:
POSTGRES_PASSWORD: changeme
POSTGRES_DB: windmill
windmill:
image: '${WINDMILL_IMAGE}'
ports:
- 8000:8000
entrypoint: ['/bin/sh', '-c']
command: 'echo ${OAUTH_JSON_BASE64} | base64 --decode > /usr/src/app/oauth.json && ./windmill'
environment:
- DATABASE_URL=postgres://postgres:changeme@localhost/windmill?sslmode=disable
- BASE_URL=${EXPECTED_URL}
- BASE_INTERNAL_URL=http://localhost:8000
- RUST_LOG=info
- NUM_WORKERS=3
- KEEP_JOB_DIR=false
- DENO_PATH=/usr/bin/deno
- PYTHON_PATH=/usr/local/bin/python3
- METRICS_ADDR=false
- OAUTH_JSON_BASE64=${OAUTH_JSON_BASE64}
volumes:
- worker_dependency_cache:/tmp/windmill/cache
deploy:
resources:
limits:
memory: 250M
lsp:
image: '${LSP_IMAGE}'
ports:
- 3001:3001
# caddy:
# image: caddy:2.5.2-alpine
# restart: unless-stopped
# volumes:
# - ./.github/uffizzi/caddy:/etc/caddy
# environment:
# - BASE_URL=localhost
volumes:
worker_dependency_cache:

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1.3.5
uses: dependabot/fetch-metadata@v1.3.6
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs

View File

@@ -40,4 +40,4 @@ jobs:
backend -> target
- name: cargo test
timeout-minutes: 10
run: mkdir frontend/build && cd backend && DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill cargo test --all -- --nocapture
run: mkdir frontend/build && cd backend && touch windmill-api/openapi-deref.yaml && DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill DISABLE_NSJAIL=false cargo test --all -- --nocapture

View File

@@ -10,6 +10,12 @@ jobs:
container: node:18
steps:
- uses: actions/checkout@v3
- run: git config --system --add safe.directory /__w/windmill/windmill
- name: Change versions
run: ./.github/change-versions.sh "$(cat version.txt)"
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: update lockfile
run: cd backend && cargo generate-lockfile
- uses: stefanzweifel/git-auto-commit-action@v4

47
.github/workflows/docker-310.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
name: Build and push windmill with python 3.10 and openbb
on: workflow_dispatch
concurrency:
group: ${{ github.ref }}-openbb
cancel-in-progress: true
permissions:
contents: read
id-token: write
packages: write
jobs:
build_ee:
runs-on: ubuntu-22.04
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: Login to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
uses: depot/build-push-action@v1
with:
context: .
push: true
file: ./docker/DockerfileOpenbb
build-args: |
features=enterprise
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:openbb
labels: |
org.opencontainers.image.licenses=Windmill-Enterprise-License

View File

@@ -1,62 +1,60 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
LOCAL_REGISTRY: registry.wimill.xyz
# env:
# REGISTRY: ghcr.io
# IMAGE_NAME: ${{ github.repository }}
name: Build and push arm docker image
on:
push:
branches: [main]
tags: ["*"]
# name: Build and push arm docker image
# on:
# push:
# branches: [main]
# tags: ["*"]
concurrency:
group: ${{ github.ref }}-arm
cancel-in-progress: true
# concurrency:
# group: ${{ github.ref }}-arm
# cancel-in-progress: true
jobs:
publish_arm:
runs-on: [self-hosted, new]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
# permissions:
# contents: read
# id-token: write
# packages: write
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
# jobs:
# publish_arm:
# runs-on: ubuntu-22.04
# 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-slim-public
uses: docker/metadata-action@v4
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
# - name: Docker meta
# id: meta-slim-public
# uses: docker/metadata-action@v4
# with:
# images: |
# ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# tags: |
# type=ref,event=branch
# type=ref,event=pr
# type=semver,pattern={{version}}
# type=semver,pattern={{major}}.{{minor}}
- name: Login to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# - name: Login to registry
# uses: docker/login-action@v2
# with:
# registry: ${{ env.REGISTRY }}
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly
uses: docker/build-push-action@v3
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ steps.meta-slim-public.outputs.tags }}
labels: |
${{ steps.meta-slim-public.outputs.labels }}
org.opencontainers.image.licenses=AGPLv3
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-arm:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-arm:buildcache,mode=max
# - name: Build and push publicly
# uses: depot/build-push-action@v1
# with:
# context: .
# push: true
# platforms: linux/amd64,linux/arm64
# tags: |
# ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
# ${{ steps.meta-slim-public.outputs.tags }}
# labels: |
# ${{ steps.meta-slim-public.outputs.labels }}
# org.opencontainers.image.licenses=AGPLv3

View File

@@ -9,22 +9,27 @@ on:
push:
branches: [main]
tags: ["*"]
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
id-token: write
packages: write
jobs:
build:
runs-on: [self-hosted, new]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Login to registry
uses: docker/login-action@v2
@@ -33,36 +38,8 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta-slim
if: github.event_name == 'pull_request'
uses: docker/metadata-action@v4
with:
images: |
${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push publicly
uses: docker/build-push-action@v3
if: github.event_name == 'pull_request'
with:
context: .
push: true
tags: |
${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:latest
${{ steps.meta-slim.outputs.tags }}
labels: |
${{ steps.meta-slim.outputs.labels }}
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:buildcache,mode=max
- name: Docker meta
id: meta-public
if: github.event_name != 'pull_request'
uses: docker/metadata-action@v4
with:
images: |
@@ -75,47 +52,31 @@ jobs:
- name: Build and push publicly
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v3
uses: depot/build-push-action@v1
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:latest
${{ steps.meta-public.outputs.tags }}
labels: |
${{ steps.meta-public.outputs.labels }}
org.opencontainers.image.licenses=AGPLv3
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:buildcache,mode=max
build_ee:
runs-on: [self-hosted, new]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta
id: meta-slim
if: github.event_name == 'pull_request'
uses: docker/metadata-action@v4
with:
images: |
${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
if: github.event_name != 'pull_request'
uses: docker/metadata-action@v4
with:
images: |
@@ -128,15 +89,13 @@ jobs:
- name: Login to registry
uses: docker/login-action@v2
if: github.event_name != 'pull_request'
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v3
uses: depot/build-push-action@v1
with:
context: .
push: true
@@ -149,8 +108,7 @@ jobs:
labels: |
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-ee:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-ee:buildcache,mode=max
playwright:
runs-on: [self-hosted, new]
@@ -172,14 +130,14 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: "Docker"
run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:latest)"
run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest)"
id: docker-container
- uses: actions/setup-node@v3
with:
node-version: 16
- name: "Playwright run"
timeout-minutes: 2
run: cd frontend && npm ci @playwright/test && npx playwright install && npm run test
run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=http://localhost:8000 && npm run test
- name: "Clean up"
run: docker kill ${{ steps.docker-container.outputs.id }}
if: always()
@@ -188,7 +146,6 @@ jobs:
publish_privately_heavy:
needs: [build_ee]
runs-on: [self-hosted, new]
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v3
with:
@@ -202,7 +159,6 @@ jobs:
with:
images: |
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}
${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
@@ -226,7 +182,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push privately
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
if: github.event_name != 'pull_request'
with:
context: .
@@ -266,7 +222,7 @@ jobs:
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Build and push privately
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
if: github.event_name != 'pull_request'
with:
context: .

17
.github/workflows/frontend-check.yml vendored Normal file
View File

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

View File

@@ -65,7 +65,7 @@ jobs:
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Build and push publicly
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: "{{defaultContext}}:lsp"
push: true
@@ -74,6 +74,7 @@ jobs:
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ steps.metalocal.outputs.tags }}
${{ steps.meta.outputs.tags }}
registry.uffizzi.com/windmill-lsp:60d
labels: ${{ steps.metalocal.outputs.labels }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max

93
.github/workflows/uffizzi-build.yml vendored Normal file
View File

@@ -0,0 +1,93 @@
name: Build PR Image
on:
pull_request:
types: [opened,synchronize,reopened,closed]
paths:
- "backend/**"
- ".github/uffizzi/**"
- ".github/workflows/**"
workflow_dispatch:
jobs:
build-windmill:
name: Build and Push `windmill`
runs-on: ubuntu-latest
if: ${{ (github.event_name != 'pull_request' || github.event.action != 'closed')}}
outputs:
tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_TAG_APP=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: registry.uffizzi.com/${{ env.UUID_TAG_APP }}
tags: type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
uses: docker/build-push-action@v4
with:
push: true
context: ./
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
render-compose-file:
name: Render Docker Compose File
# Pass output of this workflow to another triggered by `workflow_run` event.
runs-on: ubuntu-latest
needs:
- build-windmill
outputs:
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Render Compose File
run: |
WINDMILL_IMAGE=${{ needs.build-windmill.outputs.tags }}
export WINDMILL_IMAGE
LSP_IMAGE=registry.uffizzi.com/windmill-lsp:60d
export LSP_IMAGE
envsubst '${WINDMILL_IMAGE} ${LSP_IMAGE}' < ./.github/uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2
delete-preview:
name: Call for Preview Deletion
runs-on: ubuntu-latest
if: ${{ github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Serialize PR Event to File
run: echo '${{ toJSON(github.event) }}' > event.json
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2

115
.github/workflows/uffizzi-preview.yml vendored Normal file
View File

@@ -0,0 +1,115 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "Build PR Image"
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
pr-number: ${{ env.PR_NUMBER }}
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
if (matchArtifact === undefined) {
throw TypeError('Build Artifact not found!');
}
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: Predict Deployment URL
id: url
# Replace dots in the repo name with the plus sign
run: |
REPO=$(echo ${{ github.repository }} | sed 's/\./+/g')
echo "EXPECTED_URL=https://app.uffizzi.com/github.com/$REPO/pull/$PR_NUMBER" >> $GITHUB_ENV
- name: Re-Render Compose File
run: |
OAUTH_JSON_BASE64=${{ secrets.OAUTH_JSON_BASE64 }}
export OAUTH_JSON_BASE64
envsubst '${OAUTH_JSON_BASE64} ${EXPECTED_URL}' < docker-compose.rendered.yml > docker-compose.uffizzi.yml
# cat docker-compose.uffizzi.yml
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.uffizzi.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
with:
path: docker-compose.uffizzi.yml
key: ${{ env.COMPOSE_FILE_HASH }}
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Compose file hash: ${{ env.COMPOSE_FILE_HASH }}"
cat event.json
deploy-uffizzi-preview:
name: Use Remote Workflow to Preview on Uffizzi
needs:
- cache-compose-file
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.uffizzi.yml
server: https://app.uffizzi.com
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:
contents: read
pull-requests: write
id-token: write
playwright:
runs-on: ubuntu-latest
needs:
- deploy-uffizzi-preview
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: "Playwright run"
timeout-minutes: 2
run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=${{ needs.deploy-uffizzi-preview.outputs.url }} && npm run test

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.analysis.typeCheckingMode": "basic"
}

View File

@@ -1,6 +1,314 @@
# Changelog
## [1.70.1](https://github.com/windmill-labs/windmill/compare/v1.70.0...v1.70.1) (2023-02-27)
### Bug Fixes
* **cli:** make cli resilient to systems without openable browsers ([c051ffe](https://github.com/windmill-labs/windmill/commit/c051ffeb42c1cff609f93da7745036ea722e17d4))
* **frontend:** Disable move in nested subgrid ([#1238](https://github.com/windmill-labs/windmill/issues/1238)) ([70eab30](https://github.com/windmill-labs/windmill/commit/70eab303bd45111ae198d9b710bfd6f9f59e53b0))
* **frontend:** Fix inline scripts list ([#1240](https://github.com/windmill-labs/windmill/issues/1240)) ([97602ac](https://github.com/windmill-labs/windmill/commit/97602ac6db1404d36d160a431ffcea6c0f567a48))
* **frontend:** Fix subgrid lock ([#1232](https://github.com/windmill-labs/windmill/issues/1232)) ([8ee9d67](https://github.com/windmill-labs/windmill/commit/8ee9d67f4faa91446338b41c664ef91913eb8b81))
## [1.70.1](https://github.com/windmill-labs/windmill/compare/v1.70.0...v1.70.1) (2023-02-27)
### Bug Fixes
* **cli:** make cli resilient to systems without openable browsers ([c051ffe](https://github.com/windmill-labs/windmill/commit/c051ffeb42c1cff609f93da7745036ea722e17d4))
* **frontend:** Disable move in nested subgrid ([#1238](https://github.com/windmill-labs/windmill/issues/1238)) ([70eab30](https://github.com/windmill-labs/windmill/commit/70eab303bd45111ae198d9b710bfd6f9f59e53b0))
* **frontend:** Fix subgrid lock ([#1232](https://github.com/windmill-labs/windmill/issues/1232)) ([8ee9d67](https://github.com/windmill-labs/windmill/commit/8ee9d67f4faa91446338b41c664ef91913eb8b81))
## [1.70.0](https://github.com/windmill-labs/windmill/compare/v1.69.3...v1.70.0) (2023-02-27)
### Features
* **apps:** add ag grid ([b690d80](https://github.com/windmill-labs/windmill/commit/b690d801d4aa5695ee558e81d1ed114074dfcb83))
* **frontend:** move to other grid ([#1230](https://github.com/windmill-labs/windmill/issues/1230)) ([104e4ac](https://github.com/windmill-labs/windmill/commit/104e4ac5e790c30e6fb6b27726776693038d4f19))
### Bug Fixes
* app setup and sync now uses 1.69.3 ([d38aff2](https://github.com/windmill-labs/windmill/commit/d38aff2fe228f23eb18c3991392928c064e6aca2))
* **frontend:** Fix duplication ([#1237](https://github.com/windmill-labs/windmill/issues/1237)) ([e87f4fc](https://github.com/windmill-labs/windmill/commit/e87f4fc44b847a573f5acafc0348fbcbfcb2258f))
* **frontend:** fix graph viewer id assignment ([e1f686d](https://github.com/windmill-labs/windmill/commit/e1f686d8508cfc1f73c43be08facc44217ca8de0))
## [1.69.3](https://github.com/windmill-labs/windmill/compare/v1.69.2...v1.69.3) (2023-02-24)
### Bug Fixes
* **deno:** fix denoify buffer handling ([c2e5afd](https://github.com/windmill-labs/windmill/commit/c2e5afd4e07fb63375832f308da8c744616ee188))
## [1.69.2](https://github.com/windmill-labs/windmill/compare/v1.69.1...v1.69.2) (2023-02-24)
### Bug Fixes
* **app:** fix all nested behavior ([dd28308](https://github.com/windmill-labs/windmill/commit/dd28308c3cf1877ba3f19dcd2bd20bf1c7896a99))
* **frontend:** delete grid item ([008c30f](https://github.com/windmill-labs/windmill/commit/008c30fcaad64af512407f9889a9881fafac0868))
* **frontend:** duplicate ([483407c](https://github.com/windmill-labs/windmill/commit/483407cdf0e1ed61de180a904934e950fed4adc3))
* **frontend:** Fix findGridItem ([a8295d0](https://github.com/windmill-labs/windmill/commit/a8295d0b5acd08cec42b7939d907df5c25132644))
* **frontend:** Fix findGridItem ([5bb77ed](https://github.com/windmill-labs/windmill/commit/5bb77edf45740a75e969b1bef31580271c9d5505))
* **frontend:** Fix next id ([8ddcf4d](https://github.com/windmill-labs/windmill/commit/8ddcf4d9c1a8d6dd20ee241a3f308811c49e58f1))
* **frontend:** gridtab ([fa105b4](https://github.com/windmill-labs/windmill/commit/fa105b4caeaa2d0e9704a48f6caf8d846839c23e))
* **frontend:** rewrote utils ([ea1b2c2](https://github.com/windmill-labs/windmill/commit/ea1b2c29b95282df347ef9c5973917fa3880e843))
* **frontend:** wip ([33ebe2d](https://github.com/windmill-labs/windmill/commit/33ebe2da8e81476be62a2567d5012573a8a010b6))
## [1.69.1](https://github.com/windmill-labs/windmill/compare/v1.69.0...v1.69.1) (2023-02-24)
### Bug Fixes
* **deno:** remove mysql support waiting for deno fix ([dd7e8c7](https://github.com/windmill-labs/windmill/commit/dd7e8c742c83f6a1d13e4343ca626c0b5efc06fb))
* **deno:** remove mysql support waiting for deno fix ([2f78132](https://github.com/windmill-labs/windmill/commit/2f78132e081bdf3d7468e022f0e981ebfa52cfb3))
* **frontend:** containers and tab fixes v1 ([27cac3f](https://github.com/windmill-labs/windmill/commit/27cac3ffe69c4dac160e9e55ffd1eb8ea348d487))
* **frontend:** containers and tab fixes v1 ([705703a](https://github.com/windmill-labs/windmill/commit/705703a5e2f2dc7ceb4c215221f72bf624799841))
* **frontend:** containers and tab fixes v1 ([fac31c6](https://github.com/windmill-labs/windmill/commit/fac31c6628b289ad6aae92434e312c4be281a4d2))
## [1.69.0](https://github.com/windmill-labs/windmill/compare/v1.68.0...v1.69.0) (2023-02-23)
### Features
* **frontend:** Duplicate component ([#1228](https://github.com/windmill-labs/windmill/issues/1228)) ([089a6b6](https://github.com/windmill-labs/windmill/commit/089a6b6ae52e8d28dd15e2f9a6ad900c5853d0a1))
* **frontend:** Properly delete tab content ([#1227](https://github.com/windmill-labs/windmill/issues/1227)) ([857ee5f](https://github.com/windmill-labs/windmill/commit/857ee5f318466d12bf0d41515451798df087ab74))
* **frontend:** Support deeply nested components ([#1225](https://github.com/windmill-labs/windmill/issues/1225)) ([6ad876e](https://github.com/windmill-labs/windmill/commit/6ad876ebb45a934b7a4dc980cf38a5228d7d11f1))
### Bug Fixes
* **cli:** .wmillignore whitelist behavior ([d543650](https://github.com/windmill-labs/windmill/commit/d543650b313c434e794ad800aefe4aeda83c0fed))
## [1.68.0](https://github.com/windmill-labs/windmill/compare/v1.67.4...v1.68.0) (2023-02-23)
### Features
* **frontend:** Add more app component CSS customisation ([#1218](https://github.com/windmill-labs/windmill/issues/1218)) ([6044e3b](https://github.com/windmill-labs/windmill/commit/6044e3b6ef92e89b8f15f38bc2d0986ec64105d5))
### Bug Fixes
* **cli:** better ergonomics around workspace add ([40c12e6](https://github.com/windmill-labs/windmill/commit/40c12e6139c7b42d7ab169bab2dd37f8b43bea06))
* **cli:** better ergonomics around workspaces ([3b7160e](https://github.com/windmill-labs/windmill/commit/3b7160e84aa454bdb5f343da99cfd97a6b319937))
## [1.67.4](https://github.com/windmill-labs/windmill/compare/v1.67.3...v1.67.4) (2023-02-23)
### Bug Fixes
* **backend:** workflow check for has_failure_module ([e54dc3f](https://github.com/windmill-labs/windmill/commit/e54dc3ff97e4454a15b9efe25cc12f6c9e1e176b))
## [1.67.3](https://github.com/windmill-labs/windmill/compare/v1.67.2...v1.67.3) (2023-02-23)
### Bug Fixes
* **cli:** ignone non wmill looking files ([ec57c59](https://github.com/windmill-labs/windmill/commit/ec57c5977f122b629a07e05bc3551662d518ce30))
## [1.67.2](https://github.com/windmill-labs/windmill/compare/v1.67.1...v1.67.2) (2023-02-23)
### Bug Fixes
* **cli:** ignone non wmill looking files ([969e89f](https://github.com/windmill-labs/windmill/commit/969e89f8bbc10f6712920321b70ede35f19ab9ed))
## [1.67.1](https://github.com/windmill-labs/windmill/compare/v1.67.0...v1.67.1) (2023-02-22)
### Bug Fixes
* **cli:** coloring nits ([3fa24ad](https://github.com/windmill-labs/windmill/commit/3fa24adad0a07ba2f469c545b28251b035efdf90))
## [1.67.0](https://github.com/windmill-labs/windmill/compare/v1.66.1...v1.67.0) (2023-02-22)
### Features
* **frontend:** Add app sub grids ([#1208](https://github.com/windmill-labs/windmill/issues/1208)) ([dbc59e9](https://github.com/windmill-labs/windmill/commit/dbc59e952143ee5813780ad13794cef4e036911c))
### Bug Fixes
* **cli:** add --fail-conflicts to ci push ([0085b46](https://github.com/windmill-labs/windmill/commit/0085b46c1e3b8267fcafcb06ce72b4d820e49df5))
## [1.66.1](https://github.com/windmill-labs/windmill/compare/v1.66.0...v1.66.1) (2023-02-22)
### Bug Fixes
* **cli:** delete workspace instead of archiving them ([70dfc8b](https://github.com/windmill-labs/windmill/commit/70dfc8b8d0293d80da7db14caa1b9eb0ed67653d))
## [1.66.0](https://github.com/windmill-labs/windmill/compare/v1.65.0...v1.66.0) (2023-02-22)
### Features
* add delete flows ([e81f7bd](https://github.com/windmill-labs/windmill/commit/e81f7bd7239b73710da2a4ddec0da7805c13da06))
* CLI refactor v1 ([e31d2ae](https://github.com/windmill-labs/windmill/commit/e31d2ae27f886e774ffc429eea80057f4f9f4213))
* **frontend:** Add image app component ([#1213](https://github.com/windmill-labs/windmill/issues/1213)) ([a4b773a](https://github.com/windmill-labs/windmill/commit/a4b773af294554c5787f02ebda363c8d9a3eff1b))
## [1.65.0](https://github.com/windmill-labs/windmill/compare/v1.64.0...v1.65.0) (2023-02-21)
### Features
* **apps:** add asJson for customcss ([71d6dad](https://github.com/windmill-labs/windmill/commit/71d6dad37cc239952ce7799609c02474b0b1fc81))
* **apps:** add custom css for apps ([7f00e1c](https://github.com/windmill-labs/windmill/commit/7f00e1c1a8f2e905b0677d82ba547f55dc23b3e0))
* **backend:** Zip Workspace Export ([#1201](https://github.com/windmill-labs/windmill/issues/1201)) ([5d109b3](https://github.com/windmill-labs/windmill/commit/5d109b3cd4b7749788f9cb9fcbe1949c45eedf1f))
* **frontend:** Add divider app component ([#1209](https://github.com/windmill-labs/windmill/issues/1209)) ([c33e79e](https://github.com/windmill-labs/windmill/commit/c33e79e0b8d5ba1103d87fdd47fcd0e1071e19de))
* **frontend:** Add file input app component ([#1211](https://github.com/windmill-labs/windmill/issues/1211)) ([d4b6d69](https://github.com/windmill-labs/windmill/commit/d4b6d691264bf21e4e2c97548aaad9aa80678a6b))
* **frontend:** Add icon app component ([#1207](https://github.com/windmill-labs/windmill/issues/1207)) ([e4791c2](https://github.com/windmill-labs/windmill/commit/e4791c2b7e3a0e6b90c37bc1200f9cd0ab3b6845))
## [1.64.0](https://github.com/windmill-labs/windmill/compare/v1.63.2...v1.64.0) (2023-02-16)
### Features
* **frontend:** Trigger settings drawer with URL hash ([#1185](https://github.com/windmill-labs/windmill/issues/1185)) ([8445697](https://github.com/windmill-labs/windmill/commit/8445697e31394ac11f3b8aa10af1546cc9c0041c))
## [1.63.2](https://github.com/windmill-labs/windmill/compare/v1.63.1...v1.63.2) (2023-02-15)
### Bug Fixes
* **psql:** update pg client ([a2fbc57](https://github.com/windmill-labs/windmill/commit/a2fbc5702509bb259bae106baa9a6146360ec5dd))
## [1.63.1](https://github.com/windmill-labs/windmill/compare/v1.63.0...v1.63.1) (2023-02-14)
### Bug Fixes
* update hub sync script ([03eb144](https://github.com/windmill-labs/windmill/commit/03eb1444c4a5dfbd170ba8d200784e530ca2f771))
## [1.63.0](https://github.com/windmill-labs/windmill/compare/v1.62.0...v1.63.0) (2023-02-14)
### Features
* add mem peak info ([f584062](https://github.com/windmill-labs/windmill/commit/f584062f13aa7da8e767fd35de1aef7bbb67c3c8))
* **frontend:** Minimal support for custom filenames ([#1190](https://github.com/windmill-labs/windmill/issues/1190)) ([b03b3be](https://github.com/windmill-labs/windmill/commit/b03b3be154efb0984f9623c27acc05617f125bc5))
* **worker:** set oom_adj to 1000 to prioritize killing subprocess ([265fbc5](https://github.com/windmill-labs/windmill/commit/265fbc5835d029d510a794e171392884cb20bdae))
### Bug Fixes
* **python:** return none if argument is missing ([3f2754b](https://github.com/windmill-labs/windmill/commit/3f2754b3305f6cb65373d532ff0db6020bf07e45))
* Update references to the docs ([#1191](https://github.com/windmill-labs/windmill/issues/1191)) ([a574270](https://github.com/windmill-labs/windmill/commit/a574270bc259f423c984259cd7d9a6d91b77815c))
## [1.62.0](https://github.com/windmill-labs/windmill/compare/v1.61.1...v1.62.0) (2023-02-03)
### Features
* add INCLUDE_HEADERS env variable to pass value from request headers ([0921ba0](https://github.com/windmill-labs/windmill/commit/0921ba008535e945f2ec3255728c2e8c1f4c36dc))
* add WHITELIST_WORKSPACES and BLACKLIST_WORKSPACES ([99568ea](https://github.com/windmill-labs/windmill/commit/99568eaa473d57123a7dde4007f8812e0053fb3f))
* Add workspace webhook ([#1158](https://github.com/windmill-labs/windmill/issues/1158)) ([b9ac60f](https://github.com/windmill-labs/windmill/commit/b9ac60f8bb0662e364606c4b7b8a6e3c1e7e4041))
* adding worker_busy ([23007f7](https://github.com/windmill-labs/windmill/commit/23007f7a71630fc2040e1be39db83ba56689e3c4))
* **cli:** 2-Way sync ([#1071](https://github.com/windmill-labs/windmill/issues/1071)) ([cdd1619](https://github.com/windmill-labs/windmill/commit/cdd16195aeaf32e1f1d0648f48e4843954d16d9c))
* **frontend:** App initial loading animations ([#1176](https://github.com/windmill-labs/windmill/issues/1176)) ([3305481](https://github.com/windmill-labs/windmill/commit/3305481d5d4ce598ceb57256cea851869cdaf25e))
* **python:** add ADDITIONAL_PYTHON_PATHS ([14b32be](https://github.com/windmill-labs/windmill/commit/14b32be8b229372c57a167fd74cb958a96f0e8e6))
### Bug Fixes
* **frontend:** Render popups above components in app editor ([#1171](https://github.com/windmill-labs/windmill/issues/1171)) ([bc8d1a3](https://github.com/windmill-labs/windmill/commit/bc8d1a375ec7886357ce0ef5971bb35013c94d61))
* **frontend:** Various fixes and improvements ([#1177](https://github.com/windmill-labs/windmill/issues/1177)) ([9f5500c](https://github.com/windmill-labs/windmill/commit/9f5500c1965ea50796d3bf289c0f9e0c929427f4))
* navigate to new script page before saving script ([f171cd8](https://github.com/windmill-labs/windmill/commit/f171cd8b7c46677173572bac256cbb489a1b8526))
## [1.61.1](https://github.com/windmill-labs/windmill/compare/v1.61.0...v1.61.1) (2023-01-31)
### Bug Fixes
* **backend:** compile issue ([df8cc1f](https://github.com/windmill-labs/windmill/commit/df8cc1f2482b3d8b1530cdaef1361303ff5cadff))
## [1.61.0](https://github.com/windmill-labs/windmill/compare/v1.60.0...v1.61.0) (2023-01-31)
### Features
* add openapi viewer ([#1094](https://github.com/windmill-labs/windmill/issues/1094)) ([1337811](https://github.com/windmill-labs/windmill/commit/1337811438d48e23133f68e9157bd185d5fe4a82))
* add PIP_LOCAL_DEPENDENCIES ([b7db4c7](https://github.com/windmill-labs/windmill/commit/b7db4c78c4629f1fd2dfd7a338f783b16f07b24d))
* add QUEUE_LIMIT_WAIT_RESULT ([51a8810](https://github.com/windmill-labs/windmill/commit/51a8810aa0a9ab7702df459dd270278d42bd3899))
* add resource and resource type from json ([080ecb0](https://github.com/windmill-labs/windmill/commit/080ecb04d7a08d035fe07f179975b52bc0f77297))
* add sql as a valid type in Python ([0172587](https://github.com/windmill-labs/windmill/commit/0172587b129ce54d96dc99336a1f56c66ebdbef5))
* add sync webhook for flows ([f377c84](https://github.com/windmill-labs/windmill/commit/f377c84f5a2148a2bbb7c16e93f13e1d85ceb17e))
* **backend:** add queue_limit + configurable timeout + fix timeout cancel ([eef3bab](https://github.com/windmill-labs/windmill/commit/eef3bab6e4d9f1af1435db868c707a692558ab74))
* **deno:** add support for DENO_AUTH_TOKENS ([832ddab](https://github.com/windmill-labs/windmill/commit/832ddabdf2239521368e5f96df144abce0db31c2))
* **deno:** allow overriding deno sandboxing with DENO_FLAGS' ([7f40373](https://github.com/windmill-labs/windmill/commit/7f40373fd64005d87972854a565c6cf521232982))
* **frontend:** Add app inputs configurations ([#1142](https://github.com/windmill-labs/windmill/issues/1142)) ([3ed16b8](https://github.com/windmill-labs/windmill/commit/3ed16b88a42e4db6e12f8557c5bbaa2d832b1c17))
* **frontend:** Add app preview lock ([#1127](https://github.com/windmill-labs/windmill/issues/1127)) ([6a88e8c](https://github.com/windmill-labs/windmill/commit/6a88e8c4f4d6fa5c393ce27b2040784a74a73b06))
* **frontend:** Add copy button option to app text display component ([#1090](https://github.com/windmill-labs/windmill/issues/1090)) ([bdfc38d](https://github.com/windmill-labs/windmill/commit/bdfc38d954a3c5548fb7f9ee6f80f741eff8cb67))
* **frontend:** Add default codes to app editor ([#1099](https://github.com/windmill-labs/windmill/issues/1099)) ([c50c740](https://github.com/windmill-labs/windmill/commit/c50c7406f267b480af2a01b47e3fcfa1d763db7a))
* **frontend:** Add HTML result rendering ([#1160](https://github.com/windmill-labs/windmill/issues/1160)) ([c01bf70](https://github.com/windmill-labs/windmill/commit/c01bf70f62680a4b77812ac6eb64aca2b15d9a8d))
* **frontend:** Add more integration icons ([#1097](https://github.com/windmill-labs/windmill/issues/1097)) ([2191e85](https://github.com/windmill-labs/windmill/commit/2191e852318f069489f77a4f1c44aadf248c7f53))
* **frontend:** add plotly support ([a4f8f9e](https://github.com/windmill-labs/windmill/commit/a4f8f9e1cf80395d5cd1229c8dd5dda244e2ba7f))
* **frontend:** add selectedRowIndex to the table outputs ([#1145](https://github.com/windmill-labs/windmill/issues/1145)) ([f05f9e4](https://github.com/windmill-labs/windmill/commit/f05f9e4edb928e7a8e3e66a62de9c6487684a14b))
* **frontend:** Add Supabase resource ([#1107](https://github.com/windmill-labs/windmill/issues/1107)) ([12b00a8](https://github.com/windmill-labs/windmill/commit/12b00a808d1f12827a7bc26518cc6f972bdde917))
* **frontend:** add support for background scripts + add FormButtonCo… ([#1124](https://github.com/windmill-labs/windmill/issues/1124)) ([e969af9](https://github.com/windmill-labs/windmill/commit/e969af9e44d1b4409064080e8662552ee3e262e8))
* **frontend:** Add surreal db logo ([#1102](https://github.com/windmill-labs/windmill/issues/1102)) ([d811675](https://github.com/windmill-labs/windmill/commit/d81167588227f2cc433aab64551d96d21a589c5b))
* **frontend:** Add tooltip to app recompute ([#1122](https://github.com/windmill-labs/windmill/issues/1122)) ([4dfdf37](https://github.com/windmill-labs/windmill/commit/4dfdf374af358ef46ee8057373546719c6570067))
* **frontend:** add vega-lite component ([bd79938](https://github.com/windmill-labs/windmill/commit/bd79938bed6da3875a4a2dd72dad14dedbf25ddf))
* **frontend:** Display error as an icon in order to avoid clutter wh… ([#1143](https://github.com/windmill-labs/windmill/issues/1143)) ([22b8fed](https://github.com/windmill-labs/windmill/commit/22b8fed9d904a37aae66f6d957f4987f6ca9955c))
* **frontend:** Open debug runs from component ([#1155](https://github.com/windmill-labs/windmill/issues/1155)) ([73bc13b](https://github.com/windmill-labs/windmill/commit/73bc13bb7d4b1eb25a3a726ac9e6bb80120a495f))
* **frontend:** Update app table component styles ([#1100](https://github.com/windmill-labs/windmill/issues/1100)) ([172b5db](https://github.com/windmill-labs/windmill/commit/172b5dba8f4c3aaf11569c72313ad74845c668a6))
* **python:** add support for extra args in python ([772c768](https://github.com/windmill-labs/windmill/commit/772c768cda094f208a5efb7aab03eee3a8f38f68))
### Bug Fixes
* **frontend:** Add default value for text, number and date input + fix issues with number input + add date input in the settings panel ([#1135](https://github.com/windmill-labs/windmill/issues/1135)) ([8f90602](https://github.com/windmill-labs/windmill/commit/8f906026b3203702c3b6a30bcac9fb2aca985c29))
* **frontend:** Add highlight to selected workspace ([#1159](https://github.com/windmill-labs/windmill/issues/1159)) ([f221a6c](https://github.com/windmill-labs/windmill/commit/f221a6c17f145d0c42f7faf785c37f4037308973))
* **frontend:** add missing condition to properly select first row ([#1128](https://github.com/windmill-labs/windmill/issues/1128)) ([3d873ed](https://github.com/windmill-labs/windmill/commit/3d873ed51c769005981a8d8dfb95faa3ca33bb83))
* **frontend:** App form component display ([#1096](https://github.com/windmill-labs/windmill/issues/1096)) ([339742c](https://github.com/windmill-labs/windmill/commit/339742ca77dd0fda19d5a262617e42c341ef5871))
* **frontend:** App script list panel overflow ([#1101](https://github.com/windmill-labs/windmill/issues/1101)) ([7bc59d9](https://github.com/windmill-labs/windmill/commit/7bc59d9d2650b623a2b481a727ffc495b4216f22))
* **frontend:** App table action button cell ([#1149](https://github.com/windmill-labs/windmill/issues/1149)) ([e989662](https://github.com/windmill-labs/windmill/commit/e98966283dd9b57cc07da34876a90d19210c2927))
* **frontend:** App table header z-index ([#1120](https://github.com/windmill-labs/windmill/issues/1120)) ([59c4cc2](https://github.com/windmill-labs/windmill/commit/59c4cc2058f86deea793b61de59e2936e50e5577))
* **frontend:** Check if hiddenInlineScripts are undefined before iterating over them ([#1134](https://github.com/windmill-labs/windmill/issues/1134)) ([71a443e](https://github.com/windmill-labs/windmill/commit/71a443e3c56d2b8c951de6e3701a411ad1a0ce34))
* **frontend:** fix first row selection ([#1125](https://github.com/windmill-labs/windmill/issues/1125)) ([6c9daf7](https://github.com/windmill-labs/windmill/commit/6c9daf70021859dcd7cef717bc3acdfa88cffd02))
* **frontend:** Fix id generation when a second action ([#1110](https://github.com/windmill-labs/windmill/issues/1110)) ([4f86981](https://github.com/windmill-labs/windmill/commit/4f869811fee73826b2b10965241d2d8dba59dc2a))
* **frontend:** Make sure AppSelect items are an array ([#1144](https://github.com/windmill-labs/windmill/issues/1144)) ([24b1fa0](https://github.com/windmill-labs/windmill/commit/24b1fa0ae327c984841f9ed8b163b3fccc6da258))
* **frontend:** Make sure that old apps are rendering properly ([#1132](https://github.com/windmill-labs/windmill/issues/1132)) ([a78486d](https://github.com/windmill-labs/windmill/commit/a78486d7e08f76e22406063288b35e9030974d7a))
* **frontend:** Playwright ([#1108](https://github.com/windmill-labs/windmill/issues/1108)) ([f0435f5](https://github.com/windmill-labs/windmill/commit/f0435f5f81941c5b49500003aa27956d627daadb))
* **frontend:** Prepare app scripts code for export ([#1123](https://github.com/windmill-labs/windmill/issues/1123)) ([173093a](https://github.com/windmill-labs/windmill/commit/173093a40321f6ad35bf766a5554b21cea388771))
* **frontend:** Prevent modal from hijacking all keypress event ([#1136](https://github.com/windmill-labs/windmill/issues/1136)) ([aa6de3b](https://github.com/windmill-labs/windmill/commit/aa6de3bb5746b9d99c8e3a52e6a9fff10d97bc6a))
* **frontend:** Revert component input panel change ([#1092](https://github.com/windmill-labs/windmill/issues/1092)) ([0419e7e](https://github.com/windmill-labs/windmill/commit/0419e7e1c9239fd3cbc49acf82a73e9c01938153))
* **frontend:** Runnable table overflow ([#1119](https://github.com/windmill-labs/windmill/issues/1119)) ([462adbe](https://github.com/windmill-labs/windmill/commit/462adbe42f823646413a5003fd71f3dd473c0728))
* **frontend:** Select the first row by default, and remove the abilit… ([#1121](https://github.com/windmill-labs/windmill/issues/1121)) ([3c483f5](https://github.com/windmill-labs/windmill/commit/3c483f533759b9b4e589055dbddb31f294bea8fa))
* **frontend:** Show app builder header always on top ([#1118](https://github.com/windmill-labs/windmill/issues/1118)) ([631a3da](https://github.com/windmill-labs/windmill/commit/631a3da17f05a3d29defdf96a50d7e96a9f8baad))
* **frontend:** Update app scripts pane ([#1146](https://github.com/windmill-labs/windmill/issues/1146)) ([18f30c8](https://github.com/windmill-labs/windmill/commit/18f30c8286f8240158643ade8b0ef4607a80fbb0))
* **frontend:** Use absolute path on connect images ([#1095](https://github.com/windmill-labs/windmill/issues/1095)) ([43e069e](https://github.com/windmill-labs/windmill/commit/43e069eb96c0af7d3a1fe1db4f4b69f8e31e7438))
* improvements for error handling as first step of flow ([b77c239](https://github.com/windmill-labs/windmill/commit/b77c239f307a37777acb083b0cdb5c0d214a9dd8))
## [1.60.0](https://github.com/windmill-labs/windmill/compare/v1.59.0...v1.60.0) (2023-01-11)
### Features
* add 'add user to workspace' ([a14623f](https://github.com/windmill-labs/windmill/commit/a14623feaab4a36c01d558b775a42e587a74cdc9))
* **frontend:** Add frost to color palette ([#1084](https://github.com/windmill-labs/windmill/issues/1084)) ([8e72007](https://github.com/windmill-labs/windmill/commit/8e7200736827e8f6e593f900124b1bd1bc0bd5f2))
### Bug Fixes
* **frontend:** Keep pane resizer under open drawer ([#1089](https://github.com/windmill-labs/windmill/issues/1089)) ([cb25f88](https://github.com/windmill-labs/windmill/commit/cb25f883005b99b4ce98e8ae7b8253a8a2fedb5b))
## [1.59.0](https://github.com/windmill-labs/windmill/compare/v1.58.0...v1.59.0) (2023-01-09)
### Features
* add relative imports for python scripts ([#1075](https://github.com/windmill-labs/windmill/issues/1075)) ([5347cd4](https://github.com/windmill-labs/windmill/commit/5347cd46a996b4cf48a96fbb873e4d029ca4f75f))
### Bug Fixes
* **frontend:** Iconed resource height issue ([#1073](https://github.com/windmill-labs/windmill/issues/1073)) ([a84eb9b](https://github.com/windmill-labs/windmill/commit/a84eb9b1f7e1b10c960ee1594ef476e7ba146f5e))
## [1.58.0](https://github.com/windmill-labs/windmill/compare/v1.57.1...v1.58.0) (2023-01-07)

View File

@@ -1,5 +1,15 @@
{$BASE_URL} {
{
auto_https off
}
http://{$BASE_URL} {
bind {$ADDRESS}
reverse_proxy /ws/* http://lsp:3001
reverse_proxy /* http://windmill_server:8000
reverse_proxy /* http://windmill:8000
}
https://{$BASE_URL} {
bind {$ADDRESS}
reverse_proxy /ws/* http://localhost:3001
}
}

View File

@@ -51,12 +51,16 @@ COPY frontend .
RUN mkdir /backend
COPY /backend/windmill-api/openapi.yaml /backend/windmill-api/openapi.yaml
COPY /openflow.openapi.yaml /openflow.openapi.yaml
COPY /backend/windmill-api/build_openapi.sh /backend/windmill-api/build_openapi.sh
RUN cd /backend/windmill-api && . ./build_openapi.sh
RUN npm run generate-backend-client
ENV NODE_OPTIONS "--max-old-space-size=8192"
RUN npm run build
RUN npm run check
FROM rust_base AS planner
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
@@ -75,12 +79,13 @@ COPY ./openflow.openapi.yaml /openflow.openapi.yaml
COPY ./backend ./
COPY --from=frontend /frontend /frontend
COPY --from=frontend /backend/windmill-api/openapi-deref.yaml ./windmill-api/openapi-deref.yaml
COPY .git/ .git/
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
FROM python:3.11.1-slim-buster
FROM python:3.11.2-slim-buster
ARG APP=/usr/src/app

153
README.md
View File

@@ -5,7 +5,7 @@
<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 UIs to trigger workflows and scripts as internal apps. 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.
Open-source developer infrastructure for internal tools. Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIs to trigger workflows and scripts as internal apps. 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.
</p>
<p align="center">
@@ -20,55 +20,35 @@ Open-source developer infrastructure for internal tools. Self-hostable alternati
</a>
</p>
---
**Try it (personal workspaces are free forever)**: <https://app.windmill.dev>
**Documentation**: <https://docs.windmill.dev>
**Discord**: <https://discord.gg/V7PM2YHsPB>
**Hub**: <https://hub.windmill.dev>
**Contributor's guide**: <https://docs.windmill.dev/docs/contributors_guide>
**Roadmap**: <https://github.com/orgs/windmill-labs/projects/2>
You can show your support for the project by starring this repo.
Windmill Labs offers commercial licenses, an enterprise edition, local hub
mirrors, and support: contact ruben@windmill.dev.
---
# Windmill
<p align="center">
<b>Disclaimer: </b>Windmill is in <b>BETA</b>. It is secure to run in production but we are still <a href="https://github.com/orgs/windmill-labs/projects/2">improving the product fast<a/>.
<a href="https://app.windmill.dev">Try it</a> - <a href="https://docs.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://docs.windmill.dev/docs/misc/contributing">Contributor's guide</a>
</p>
![Windmill Screenshot](./imgs/windmill-flow.png)
![Windmill Screenshot](./imgs/windmill.png)
# Windmill - Turn scripts into workflows and UIs that you can share and run at scale
Windmill is <b>fully open-sourced (AGPLv3)</b>:
Windmill is <b>fully open-sourced (AGPLv3)</b> and Windmill Labs offers dedicated instance and commercial support and licenses.
- [Windmill](#windmill)
![Windmill Diagram](/imgs/stacks.svg)
https://user-images.githubusercontent.com/275584/218350457-bc2fdc3b-e667-4da5-a2bd-3bacc1f0ec79.mp4
- [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)
- [Main Concepts](#main-concepts)
- [Show me some actual script code](#show-me-some-actual-script-code)
- [CLI](#cli)
- [Layout](#layout)
- [Running scripts locally](#running-scripts-locally)
- [Stack](#stack)
- [Security](#security)
- [Sandboxing and workload isolation](#sandboxing-and-workload-isolation)
- [Sandboxing](#sandboxing)
- [Secrets, credentials and sensitive values](#secrets-credentials-and-sensitive-values)
- [Performance](#performance)
- [Architecture](#architecture)
- [Big-picture Architecture](#big-picture-architecture)
- [Technical Architecture](#technical-architecture)
- [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)
- [Commercial license](#commercial-license)
- [OAuth for self-hosting (very optional)](#oauth-for-self-hosting-very-optional)
- [OAuth for self-hosting](#oauth-for-self-hosting)
- [Resource types](#resource-types)
- [Environment Variables](#environment-variables)
- [Run a local dev setup](#run-a-local-dev-setup)
@@ -91,7 +71,7 @@ Windmill is <b>fully open-sourced (AGPLv3)</b>:
shared on [WindmillHub](https://hub.windmill.dev).
![Step 4](./imgs/windmill-flow.png)
4. (Coming soon) Build complex UI on top of your scripts and flows.
4. Build complex UI on top of your scripts and flows.
![Step 5](./imgs/windmill-builder.png)
Scripts and flows can also be triggered by a cron schedule '*/5 * * * *' or
@@ -99,20 +79,49 @@ through webhooks.
You can build your entire infra on top of Windmill!
## Show me some actual script code
```typescript
import * as wmill from "https://deno.land/x/windmill@v1.62.0/mod.ts"
//import any dependency from npm
import cowsay from 'npm:cowsay@1.5.0'
export async function main(
a: number,
// unions generate enums
b: "my" | "enum",
// default parameters prefill the field
d = "default arg",
// nested objects work c = { nested: "object" },
// permissioned and typed json
db: wmill.Resource<"postgresql">) {
const email = Deno.env.get('WM_EMAIL')
// variables are permissioned and by path
let variable = await wmill.getVariable('f/company-folder/my_secret')
const lastTimeRun = await wmill.getState()
// logs are printed and always inspectable
console.log(cowsay.say({ text: "hello " + email + " " + lastTimeRun }))
await wmill.setState(Date.now())
// return is serialized as JSON
return { foo: d, variable };
}
```
## CLI
We have a powerful CLI to interact with the windmill platform and sync your
scripts from your own github repo. See
scripts from local files, github repos and to run scripts and flows on the instance from local commands. See
[more details](https://github.com/windmill-labs/windmill/tree/main/cli)
![CLI Screencast](./cli/vhs/output/setup.gif)
## Layout
- `backend/`: Rust backend
- `frontend`: Svelte frontend
- `lsp/`: Lsp asssistant for the monaco editor
- `<lang>-client/`: Windmill client for the given `<lang>`
### Running scripts locally
You can run your script locally easily, you simply need to pass the right environment variables for the `wmill` client library to fetch resource and variables from your instance if necessary. See more: <https://docs.windmill.dev/docs/advanced/local_development/>
## Stack
@@ -135,7 +144,7 @@ scripts from your own github repo. See
## Security
### Sandboxing and workload isolation
### Sandboxing
Windmill uses [nsjail](https://github.com/google/nsjail) on top of the deno
sandboxing. It is production multi-tenant grade secure. Do not take our word for
@@ -161,33 +170,23 @@ back to the database is ~50ms. A typical lightweight deno job will take around
<p align="center">
### Big-picture Architecture
<img src="./imgs/diagram.svg">
### Technical Architecture
<img src="./imgs/architecture.svg">
</p>
## How to self-host
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://docs.windmill.dev/docs/how-tos/self_host)
[documentation](https://docs.windmill.dev/docs/advanced/self_host)
### Docker compose
`docker compose up` with the following docker-compose is sufficient:
<https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml>
Go to https://localhost et voilà :)
Go to http://localhost et voilà :)
For older kernels < 4.18, set `DISABLE_NUSER=true` as env variable, otherwise
nsjail will not be able to launch the isolated scripts.
To disable nsjail altogether, set `DISABLE_NSJAIL=true`.
The default super-admin user is: admin@windmill.dev / changeme
@@ -195,7 +194,14 @@ From there, you can create other users (do not forget to change the password!)
### Kubernetes (k8s) and Helm charts
We publish helm charts at: <https://github.com/windmill-labs/windmill-helm-charts>
We publish helm charts at:
<https://github.com/windmill-labs/windmill-helm-charts>
### Postgres without superuser
If you do not want, or cannot (for instance, in AWS Aurora or Cloud sql) use a postgres superuser,
you can run `./init-db-as-superuser.sql` to init the required users for windmill.
### Commercial license
@@ -207,14 +213,14 @@ comfortable with AGPLv3.
To re-expose any Windmill parts to your users 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
<license@windmill.dev> if you have any doubts.
<ruben@windmill.dev> if you have any doubts.
In addition, a commercial license grants you a dedicated engineer to transition
your current infrastructure to Windmill, support with tight SLA, audit logs
export features, SSO, unlimited users creation, advanced permission managing
features such as groups and the ability to create more than one workspace.
### OAuth for self-hosting (very optional)
### OAuth for self-hosting
To get the same oauth integrations as Windmill Cloud, mount `oauth.json` with
the following format:
@@ -231,12 +237,13 @@ the following format:
and mount it at `/usr/src/app/oauth.json`.
The redirect url for the oauth clients is: `<instance_url>/user/login_callback/<client>`
The redirect url for the oauth clients is:
`<instance_url>/user/login_callback/<client>`
[The list of all possible "connect an app" oauth clients](https://github.com/windmill-labs/windmill/blob/main/backend/oauth_connect.json)
To add more "connect an app" OAuth clients to the Windmill project, read the
[Contributor's guide](https://docs.windmill.dev/docs/contributors_guide). We
[Contributor's guide](https://docs.windmill.dev/docs/misc/contributing). We
welcome contributions!
You may also add your own custom OAuth2 IdP and OAuth2 Resource provider:
@@ -268,31 +275,32 @@ You may also add your own custom OAuth2 IdP and OAuth2 Resource provider:
### Resource types
You will also want to import all the approved resource types from
[WindmillHub](https://hub.windmill.dev). There is no automatic way to do this
automatically currently, but it will be possible using a command with the
upcoming CLI tool.
[WindmillHub](https://hub.windmill.dev). A setup script will prompt
you to have it being synced automatically everyday.
## Environment Variables
| Environment Variable name | Default | Description | Api Server/Worker/All |
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | |
| NUM_WORKERS | 3 | The number of worker per Worker instance (set to 1 on Eks to have 1 pod = 1 worker) | Worker |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
| PORT | 8000 | Exposed port | Server | |
| NUM_WORKERS | 3 | The number of worker per Worker instance (set to 1 on Eks to have 1 pod = 1 worker, set to 0 for an API only instance) | Worker |
| DISABLE_SERVER | false | Binary would operate as a worker only instance | Worker |
| METRICS_ADDR | None | The socket addr at which to expose Prometheus metrics at the /metrics path. Set to "true" to expose it on port 8001 | All |
| JSON_FMT | false | Output the logs in json format instead of logfmt | All |
| BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance | Server |
| BASE_INTERNAL_URL | http://localhost:8000 | The base url that is reachable by your workers to talk to the Servers. This help avoiding going through the external load balancer for VPC-internal requests. | Worker |
| TIMEOUT | 300 | The timeout in seconds for the execution of a script | Worker |
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
| MAX_LOG_SIZE | 500000 | The maximum number of characters a job can emit (log + result) | Worker |
| DISABLE_NUSER | false | If Nsjail is enabled, disable the nsjail's `clone_newuser` setting | Worker |
| KEEP_JOB_DIR | false | Keep the job directory after the job is done. Useful for debugging. | Worker |
| LICENSE_KEY (EE only) | None | License key checked at startup for the Enterprise Edition of Windmill | Worker |
| S3_CACHE_BUCKET (EE only) | None | The S3 bucket to sync the cache of the workers to | Worker |
| TAR_CACHE_RATE (EE only) | 100 | The rate at which to tar the cache of the workers. 100 means every 100th job in average (uniformly randomly distributed). | Worker |
| SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See [Slack documentation](https://api.slack.com/authentication/verifying-requests-from-slack) | Server |
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server |
| SERVE_CSP | None | The CSP directives to use when serving the frontend static assets | Server |
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server | |
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
| PYTHON_PATH | /usr/local/bin/python3 | The path to the python binary. | Worker |
| GO_PATH | /usr/bin/go | The path to the go binary. | Worker |
@@ -303,7 +311,16 @@ upcoming CLI tool.
| HOME | None | The home directory to use for Go and Bash , usually inherited | Worker |
| DATABASE_CONNECTIONS | 50 (Server)/3 (Worker) | The max number of connections in the database connection pool | All |
| SUPERADMIN_SECRET | None | A token that would let the caller act as a virtual superadmin superadmin@windmill.dev | Server |
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | |
| ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker |
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |
| WHITELIST_WORKSPACES | None | Whitelist of workspaces this worker takes job from | Worker |
| BLACKLIST_WORKSPACES | None | Blacklist of workspaces this worker takes job from | Worker |
| NEW_USER_WEBHOOK | None | Webhook to notify of a new user added, signup/invite. Can hook back to windmill to send emails | Server |
## Run a local dev setup
@@ -350,4 +367,4 @@ running options.
## Copyright
Windmill Labs, Inc 2022
Windmill Labs, Inc 2023

1
backend/.gitignore vendored
View File

@@ -1,3 +1,4 @@
target/
.env
oauth.json
windmill-api/openapi-deref.yaml

3
backend/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.analysis.typeCheckingMode": "basic"
}

1689
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.58.0"
version = "1.70.1"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.58.0"
version = "1.70.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -28,7 +28,11 @@ name = "windmill"
path = "./src/main.rs"
[features]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise"]
enterprise = [
"windmill-worker/enterprise",
"windmill-queue/enterprise",
"windmill-api/enterprise",
]
[dependencies]
anyhow.workspace = true
@@ -45,6 +49,9 @@ tokio-metrics.workspace = true
rand.workspace = true
chrono.workspace = true
git-version.workspace = true
rsa.workspace = true
base64.workspace = true
sha2.workspace = true
[dev-dependencies]
serde_json.workspace = true
@@ -80,7 +87,7 @@ chrono = { version = "^0", features = ["serde"] }
tracing = "^0"
tracing-subscriber = { version = "^0", features = ["env-filter", "json"] }
prometheus = { version = "^0", default-features = false }
cookie = { version = "^0" }
cookie = { version = "0.17.0" }
phf = { version = "0.11", features = ["macros"] }
rust-embed = "^6"
mime_guess = "^2"
@@ -118,7 +125,7 @@ async-recursion = "^1"
swc_common = "^0"
swc_ecma_parser = "^0"
swc_ecma_ast = "^0"
base64 = "^0"
base64 = "0.21.0"
unicode-general-category = "^0"
hmac = "0.12.1"
sha2 = "0.10.6"
@@ -140,3 +147,9 @@ lazy_static = "1.4.0"
serde_derive = "1.0.147"
const_format = { version = "0.2", features = ["rust_1_64", "rust_1_51"] }
dyn-iter = "0.2.0"
rsa = "0.7.2"
async-stripe = { version = "0.14", features = [
"runtime-tokio-hyper",
"checkout",
] }
async_zip = { version = "0.0.11", features = ["full"] }

View File

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

View File

@@ -0,0 +1,12 @@
-- Add up migration script here
ALTER TABLE
password
ADD
first_time_user boolean NOT NULL DEFAULT (false);
UPDATE
password
SET
first_time_user = true
WHERE
email = 'admin@windmill.dev';

View File

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE workspace_settings ADD COLUMN customer_id VARCHAR(100);
ALTER TABLE workspace_settings ADD COLUMN plan VARCHAR(40);

View File

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

View File

@@ -0,0 +1,5 @@
-- Add up migration script here
ALTER TABLE
workspace_settings
ADD
COLUMN webhook text;

View File

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

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE account ALTER COLUMN refresh_token TYPE VARCHAR(1500);

View File

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

View File

@@ -0,0 +1,4 @@
-- Add up migration script here
GRANT ALL PRIVILEGES ON TABLE favorite TO windmill_admin;
GRANT ALL PRIVILEGES ON TABLE favorite TO windmill_user;

View File

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

View File

@@ -0,0 +1,25 @@
-- Add up migration script here
CREATE POLICY see_folder_extra_perms_user ON capture FOR ALL
USING (SPLIT_PART(capture.path, '/', 1) = 'f' AND SPLIT_PART(capture.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]))
WITH CHECK (SPLIT_PART(capture.path, '/', 1) = 'f' AND SPLIT_PART(capture.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
DO
$do$
DECLARE
i text;
arr text[] := array['queue', 'completed_job'];
BEGIN
FOREACH i IN ARRAY arr
LOOP
EXECUTE FORMAT(
$$
CREATE POLICY see_folder_extra_perms_user ON %1$I FOR ALL
USING (%1$I.visible_to_owner IS true AND SPLIT_PART(%1$I.script_path, '/', 1) = 'f' AND SPLIT_PART(%1$I.script_path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]));
$$,
i
);
END LOOP;
END
$do$;

View File

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

View File

@@ -0,0 +1,5 @@
-- Add up migration script here
CREATE POLICY see_folder_extra_perms_user ON account FOR ALL
USING (SPLIT_PART(account.owner, '/', 1) = 'f' AND SPLIT_PART(account.owner, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]))
WITH CHECK (SPLIT_PART(account.owner, '/', 1) = 'f' AND SPLIT_PART(account.owner, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));

View File

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE queue ADD COLUMN mem_peak INTEGER;
ALTER TABLE completed_job ADD COLUMN mem_peak INTEGER;

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,16 @@
-- Add up migration script here
-- Add up migration script here
UPDATE script SET content = 'import wmill from "https://deno.land/x/wmill@v1.63.1/main.ts";
export async function main() {
await run(
"workspace", "add", "__automation", "admins", Deno.env.get("BASE_INTERNAL_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
await run("hub", "pull");
}
async function run(...cmd: string[]) {
console.log("Running \"" + cmd.join('' '') + "\"");
await wmill.parse(cmd);
}', summary = 'Synchronize Hub Resource types with admins workspace',
description = 'Basic administrative script to sync latest resource types from hub to share to every workspace. Recommended to run at least once. On a schedule by default.'
WHERE hash = -28028598712388162 AND workspace_id = 'admins';

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
-- Add up migration script here
UPDATE script SET content = 'import wmill from "https://deno.land/x/wmill@v1.70.1/main.ts";
export async function main() {
await run(
"workspace", "add", "__automation", "admins", Deno.env.get("BASE_INTERNAL_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
await run("hub", "pull");
}
async function run(...cmd: string[]) {
console.log("Running \"" + cmd.join('' '') + "\"");
await wmill.parse(cmd);
}', summary = 'Synchronize Hub Resource types with admins workspace',
description = 'Basic administrative script to sync latest resource types from hub to share to every workspace. Recommended to run at least once. On a schedule by default.'
WHERE hash = -28028598712388162 AND workspace_id = 'admins';

View File

@@ -17,10 +17,8 @@ use serde_json::json;
use windmill_common::error;
use windmill_parser::{json_to_typ, Arg, MainArgSignature, Typ};
use rustpython_parser::{
ast::{Constant, ExprKind, Located, StmtKind},
parser,
};
use rustpython_parser as parser;
use rustpython_parser::ast::{Constant, ExprKind, Located, StmtKind};
const DEF_MAIN: &str = "def main(";
const FUNCTION_CALL: &str = "<function call>";
@@ -80,7 +78,7 @@ pub fn parse_python_signature(code: &str) -> error::Result<MainArgSignature> {
let def_arg_start = params.args.len() - params.defaults.len();
Ok(MainArgSignature {
star_args: params.vararg.is_some(),
star_kwargs: params.vararg.is_some(),
star_kwargs: params.kwarg.is_some(),
args: params
.args
.into_iter()
@@ -104,6 +102,7 @@ pub fn parse_python_signature(code: &str) -> error::Result<MainArgSignature> {
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"datetime.datetime" => Typ::Datetime,
"Sql" | "sql" => Typ::Sql,
_ => Typ::Resource(id),
},
_ => Typ::Unknown,
@@ -135,7 +134,10 @@ fn to_value(et: &ExprKind) -> Option<serde_json::Value> {
.into_iter()
.zip(values)
.map(|(k, v)| {
let key = to_value(&k.node)
let key = k
.as_ref()
.map(|x| x.node.clone())
.and_then(|n| to_value(&n))
.and_then(|x| match x {
serde_json::Value::String(s) => Some(s),
_ => None,
@@ -174,8 +176,14 @@ fn constant_to_value(c: &Constant) -> serde_json::Value {
static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_map! {
"psycopg2" => "psycopg2-binary",
"psycopg" => "psycopg[binary, pool]",
"yaml" => "pyyaml",
"git" => "GitPython"
"git" => "GitPython",
"u" => "requests",
"f" => "requests",
"shopify" => "ShopifyAPI",
"seleniumwire" => "selenium-wire",
"openbb-terminal" => "openbb[all]",
};
fn replace_import(x: String) -> String {
@@ -187,13 +195,13 @@ fn replace_import(x: String) -> String {
}
lazy_static! {
static ref RE: Regex = Regex::new(r"^\#(\S+)$").unwrap();
static ref RE: Regex = Regex::new(r"^\#\s?(\S+)$").unwrap();
}
pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
let find_requirements = code
.lines()
.find_position(|x| x.starts_with("#requirements:"));
.find_position(|x| x.starts_with("#requirements:") || x.starts_with("# requirements:"));
if let Some((pos, _)) = find_requirements {
let lines = code
.lines()

File diff suppressed because it is too large Load Diff

24
backend/src/ee.rs Normal file
View File

@@ -0,0 +1,24 @@
#[cfg(feature = "enterprise")]
use rsa::{pkcs8::DecodePublicKey, signature::Verifier};
#[cfg(feature = "enterprise")]
use sha2::Sha256;
#[cfg(feature = "enterprise")]
pub fn verify_license_key(license_key: Option<String>) -> anyhow::Result<()> {
if let Some(license_key) = license_key {
let splitted_lk = license_key
.split_once(".")
.expect("license_key can be splitted with a .");
let pub_key = rsa::RsaPublicKey::from_public_key_der(
&base64::decode("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgVShzcLSPiOi+8ET8fggob1kmi47/cE12JaidPkwfGnScZItghkqtiLsct0U4kJhlp5gO89DYTBmIKadvxwY7kMsLlZzmi2emVH7c27cByGASY8QmWDNdG4Ggy/NDflGGBdAtN6gHawZAg4zHv3qpbPQGHH1/6sXIohcXhOnouwIDAQAB")?)?;
let msg = base64::decode(splitted_lk.0)?;
let signature = base64::decode(splitted_lk.1)?;
rsa::pss::VerifyingKey::<Sha256>::new(pub_key)
.verify(&msg, &rsa::pss::Signature::from(signature))
.map_err(|_| anyhow::anyhow!("Invalid license key".to_string()))?;
} else {
panic!("License key is required for the enterprise edition");
}
Ok(())
}

View File

@@ -11,9 +11,12 @@ use std::net::SocketAddr;
use git_version::git_version;
use sqlx::{Pool, Postgres};
use windmill_common::utils::rd_string;
use windmill_worker::WorkerConfig;
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
const DEFAULT_NUM_WORKERS: usize = 3;
const DEFAULT_PORT: u16 = 8000;
mod ee;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
@@ -24,7 +27,7 @@ async fn main() -> anyhow::Result<()> {
let num_workers = std::env::var("NUM_WORKERS")
.ok()
.and_then(|x| x.parse::<i32>().ok())
.unwrap_or(windmill_common::DEFAULT_NUM_WORKERS as i32);
.unwrap_or(DEFAULT_NUM_WORKERS as i32);
let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
.ok()
@@ -36,6 +39,13 @@ async fn main() -> anyhow::Result<()> {
.transpose()?
.flatten();
let port: u16 = std::env::var("PORT")
.ok()
.and_then(|x| x.parse::<u16>().ok())
.unwrap_or(DEFAULT_PORT as u16);
let base_internal_url: String = std::env::var("BASE_INTERNAL_URL")
.unwrap_or_else(|_| format!("http://localhost:{}", port.to_string()));
let server_mode = !std::env::var("DISABLE_SERVER")
.ok()
.and_then(|x| x.parse::<bool>().ok())
@@ -50,58 +60,25 @@ async fn main() -> anyhow::Result<()> {
let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
let shutdown_signal = windmill_common::shutdown_signal(tx);
let base_url = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
let base_internal_url =
std::env::var("BASE_INTERNAL_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
let timeout = std::env::var("TIMEOUT")
.ok()
.and_then(|x| x.parse::<i32>().ok())
.unwrap_or(windmill_common::DEFAULT_TIMEOUT);
if server_mode || num_workers > 0 {
let addr = SocketAddr::from(([0, 0, 0, 0], 8000));
let addr = SocketAddr::from(([0, 0, 0, 0], port));
let base_url2 = base_url.clone();
let server_f = async {
if server_mode {
windmill_api::run_server(db.clone(), addr, base_url, rx.resubscribe()).await?;
windmill_api::run_server(db.clone(), addr, rx.resubscribe()).await?;
}
Ok(()) as anyhow::Result<()>
};
let base_url = base_url2.clone();
let workers_f = async {
if num_workers > 0 {
let sleep_queue = std::env::var("SLEEP_QUEUE")
.ok()
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or(windmill_common::DEFAULT_SLEEP_QUEUE);
let disable_nuser = std::env::var("DISABLE_NUSER")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false);
let disable_nsjail = std::env::var("DISABLE_NSJAIL")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(true);
let keep_job_dir = std::env::var("KEEP_JOB_DIR")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false);
let license_key = std::env::var("LICENSE_KEY").ok();
let sync_bucket = std::env::var("S3_CACHE_BUCKET")
.ok()
.map(|e| Some(e))
.unwrap_or(None);
#[cfg(feature = "enterprise")]
tracing::info!(
"
"
##############################
Windmill Enterprise Edition {GIT_VERSION} LICENSE_KEY: {license_key:?}, S3_CACHE_BUCKET: {sync_bucket:?}
Windmill Enterprise Edition {GIT_VERSION}
##############################"
);
);
#[cfg(not(feature = "enterprise"))]
tracing::info!(
@@ -111,38 +88,58 @@ Windmill Community Edition {GIT_VERSION}
##############################"
);
tracing::info!(
"DISABLE_NSJAIL: {disable_nsjail}, DISABLE_NUSER: {disable_nuser}, BASE_URL: \
{base_url}, SLEEP_QUEUE: {sleep_queue}, NUM_WORKERS: {num_workers}, TIMEOUT: \
{timeout}, KEEP_JOB_DIR: {keep_job_dir}"
);
display_config(vec![
"DISABLE_NSJAIL",
"DISABLE_SERVER",
"NUM_WORKERS",
"METRICS_ADDR",
"JSON_FMT",
"BASE_URL",
"BASE_INTERNAL_URL",
"TIMEOUT",
"SLEEP_QUEUE",
"MAX_LOG_SIZE",
"PORT",
"KEEP_JOB_DIR",
"S3_CACHE_BUCKET",
"TAR_CACHE_RATE",
"COOKIE_DOMAIN",
"PYTHON_PATH",
"DENO_PATH",
"GO_PATH",
"PIP_INDEX_URL",
"PIP_EXTRA_INDEX_URL",
"PIP_TRUSTED_HOST",
"PATH",
"HOME",
"DATABASE_CONNECTIONS",
"TIMEOUT_WAIT_RESULT",
"QUEUE_LIMIT_WAIT_RESULT",
"DENO_AUTH_TOKENS",
"DENO_FLAGS",
"PIP_LOCAL_DEPENDENCIES",
"ADDITIONAL_PYTHON_PATHS",
"INCLUDE_HEADERS",
"WHITELIST_WORKSPACES",
"BLACKLIST_WORKSPACES",
"NEW_USER_WEBHOOK",
"CLOUD_HOSTED",
]);
run_workers(
db.clone(),
addr,
timeout,
num_workers,
sleep_queue,
WorkerConfig {
disable_nsjail,
disable_nuser,
base_internal_url,
base_url,
keep_job_dir,
},
rx.resubscribe(),
sync_bucket,
license_key,
num_workers,
base_internal_url.clone(),
)
.await?;
}
Ok(()) as anyhow::Result<()>
};
let base_url = base_url2;
let monitor_f = async {
if server_mode {
monitor_db(&db, timeout, base_url, rx.resubscribe());
monitor_db(&db, rx.resubscribe(), &base_internal_url);
}
Ok(()) as anyhow::Result<()>
};
@@ -161,55 +158,54 @@ Windmill Community Edition {GIT_VERSION}
Ok(())
}
fn display_config(envs: Vec<&str>) {
tracing::info!(
"config: {}",
envs.iter()
.filter(|env| std::env::var(env).is_ok())
.map(|env| {
format!(
"{}: {}",
env,
std::env::var(env).unwrap_or_else(|_| "not set".to_string())
)
})
.collect::<Vec<String>>()
.join(", ")
)
}
pub fn monitor_db(
db: &Pool<Postgres>,
timeout: i32,
base_url: String,
rx: tokio::sync::broadcast::Receiver<()>,
base_internal_url: &str,
) {
let db1 = db.clone();
let db2 = db.clone();
let rx2 = rx.resubscribe();
let base_internal_url = base_internal_url.to_string();
tokio::spawn(async move {
windmill_worker::handle_zombie_jobs_periodically(&db1, timeout, &base_url, rx).await
windmill_worker::handle_zombie_jobs_periodically(&db1, rx, &base_internal_url).await
});
tokio::spawn(async move { windmill_api::delete_expired_items_perdiodically(&db2, rx2).await });
}
pub async fn run_workers(
db: Pool<Postgres>,
addr: SocketAddr,
timeout: i32,
num_workers: i32,
sleep_queue: u64,
worker_config: WorkerConfig,
rx: tokio::sync::broadcast::Receiver<()>,
mut periodic_script: Option<String>,
license_key: Option<String>,
num_workers: i32,
base_internal_url: String,
) -> anyhow::Result<()> {
let license_key = std::env::var("LICENSE_KEY").ok();
#[cfg(feature = "enterprise")]
if let Some(license_key) = license_key {
if license_key != "REQUIRED_DEC1" {
panic!("Invalid license key");
}
} else {
panic!("License key is required for the enterprise edition");
}
ee::verify_license_key(license_key)?;
#[cfg(not(feature = "enterprise"))]
if license_key.is_some() {
panic!("License key is required ONLY for the enterprise edition");
}
#[cfg(not(feature = "enterprise"))]
if !worker_config.disable_nsjail {
tracing::warn!(
"NSJAIL to sandbox process in untrusted environments is an enterprise feature but allowed to be used for testing purposes"
);
}
let instance_name = rd_string(5);
let monitor = tokio_metrics::TaskMonitor::new();
@@ -228,22 +224,17 @@ pub async fn run_workers(
let worker_name = format!("dt-worker-{}-{}", &instance_name, rd_string(5));
let ip = ip.clone();
let rx = rx.resubscribe();
let worker_config = worker_config.clone();
let wp = periodic_script.take();
let base_internal_url = base_internal_url.clone();
handles.push(tokio::spawn(monitor.instrument(async move {
tracing::info!(addr = %addr.to_string(), worker = %worker_name, "starting worker");
tracing::info!(worker = %worker_name, "starting worker");
windmill_worker::run_worker(
&db1,
timeout,
&instance_name,
worker_name,
i as u64,
num_workers as u64,
&ip,
sleep_queue,
worker_config,
wp,
rx,
&base_internal_url,
)
.await
})));

View File

@@ -6,10 +6,8 @@ use windmill_common::{
flow_status::{FlowStatus, FlowStatusModule},
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform},
scripts::ScriptLang,
DEFAULT_SLEEP_QUEUE,
};
use windmill_queue::{get_queued_job, JobPayload, RawCode};
use windmill_worker::WorkerConfig;
async fn initialize_tracing() {
use std::sync::Once;
@@ -89,14 +87,7 @@ impl ApiServer {
let addr = sock.local_addr().unwrap();
drop(sock);
let task = tokio::task::spawn({
windmill_api::run_server(
db.clone(),
addr,
format!("http://localhost:{}", addr.port()),
rx,
)
});
let task = tokio::task::spawn(windmill_api::run_server(db.clone(), addr, rx));
return Self { addr, tx, task };
}
@@ -186,7 +177,7 @@ mod suspend_resume {
const secret = await r.text();\
console.log('Secret: ' + secret + ' ' + job + ' ' + token);\
const r2 = await fetch(
`http://localhost:${port}/api/w/test-workspace/jobs/${op}/${job}/0/${secret}?approver=ruben`,\
`http://localhost:${port}/api/w/test-workspace/jobs_u/${op}/${job}/0/${secret}?approver=ruben`,\
{\
method: 'POST',\
body: JSON.stringify('from job'),\
@@ -276,7 +267,7 @@ mod suspend_resume {
/* ImZyb20gdGVzdCIK = base64 "from test" */
reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/resume/{second}/0/{secret}?payload=ImZyb20gdGVzdCIK&approver=ruben"
"http://localhost:{port}/api/w/test-workspace/jobs_u/resume/{second}/0/{secret}?payload=ImZyb20gdGVzdCIK&approver=ruben"
))
.await
.unwrap()
@@ -333,7 +324,7 @@ mod suspend_resume {
server.close().await.unwrap();
assert_eq!(
json!({"error": "Job canceled: approval request disapproved by ruben" }),
json!({"error": {"name": "Canceled", "reason": "approval request disapproved", "message": "Job canceled: approval request disapproved by ruben", "canceler": "ruben"}}),
result
);
}
@@ -379,7 +370,7 @@ mod suspend_resume {
/* ImZyb20gdGVzdCIK = base64 "from test" */
reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/cancel/{second}/0/{secret}?payload=ImZyb20gdGVzdCIK"
"http://localhost:{port}/api/w/test-workspace/jobs_u/cancel/{second}/0/{secret}?payload=ImZyb20gdGVzdCIK"
))
.await
.unwrap()
@@ -395,7 +386,7 @@ mod suspend_resume {
let result = completed_job(flow, &db).await.result.unwrap();
assert_eq!(
json!({"error": "Job canceled: approval request disapproved by unknown" }),
json!( {"error": {"name": "Canceled", "reason": "approval request disapproved", "message": "Job canceled: approval request disapproved by unknown", "canceler": "unknown"}}),
result
);
}
@@ -462,7 +453,7 @@ export async function main(index, port) {
const buf = new Uint8Array([0]);
const sock = await Deno.connect({ port });
await sock.write(new Uint8Array([index]));
if (await sock.read(buf) != 1) throw "read";
if (await sock.read(buf) != 1) throw Error("read");
return buf[0];
}
"#
@@ -581,10 +572,17 @@ def main(last, port):
.unwrap();
assert_eq!(server.close().await, attempts);
assert!(result[1]["error"]
.as_str()
.unwrap()
.contains(r#"Uncaught (in promise) "read""#));
assert!(
result[1]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
== "read"
);
}
#[sqlx::test(fixtures("base"))]
@@ -615,6 +613,10 @@ def main(last, port):
let result = job.result.unwrap();
assert_eq!(server.close().await, attempts);
assert!(result["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("index out of range"));
@@ -684,9 +686,7 @@ def main(error, port):
result,
json!({
"recv": 42,
"from failure module": {
"error": "Error during execution of the script:\n\nTraceback (most recent call last):\n File \"/tmp/main.py\", line 14, in <module>\n res = inner_script.main(**kwargs)\n File \"/tmp/inner.py\", line 5, in main\n return sock.recv(1)[0]\nIndexError: index out of range",
}
"from failure module": {"error": {"name": "IndexError", "stack": " File \"/tmp/inner.py\", line 5, in main\n return sock.recv(1)[0]\n", "message": "index out of range"}},
})
);
}
@@ -739,9 +739,13 @@ async fn test_iteration(db: Pool<Postgres>) {
.unwrap();
assert!(matches!(result, serde_json::Value::Array(_)));
assert!(result[2]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("StopIteration: 2"));
.contains("2"));
}
#[sqlx::test(fixtures("base"))]
@@ -792,9 +796,13 @@ async fn test_iteration_parallel(db: Pool<Postgres>) {
let result = job.result.unwrap();
assert!(matches!(result, serde_json::Value::Array(_)));
assert!(result[2]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("StopIteration: 2"));
.contains("2"));
}
struct RunJob {
@@ -900,43 +908,20 @@ fn spawn_test_worker(
) {
let (tx, rx) = tokio::sync::broadcast::channel(1);
let db = db.to_owned();
let timeout = 4_000;
let worker_instance: &str = "test worker instance";
let worker_name: String = next_worker_name();
let i_worker: u64 = Default::default();
let num_workers: u64 = 2;
let ip: &str = Default::default();
let sleep_queue: u64 = DEFAULT_SLEEP_QUEUE / num_workers;
let port = port;
let worker_config = WorkerConfig {
base_internal_url: format!("http://localhost:{port}"),
base_url: format!("http://localhost:{port}"),
disable_nuser: std::env::var("DISABLE_NUSER")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false),
disable_nsjail: std::env::var("DISABLE_NSJAIL")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false),
keep_job_dir: std::env::var("KEEP_JOB_DIR")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false),
};
let future = async move {
let base_internal_url = format!("http://localhost:{}", port);
windmill_worker::run_worker(
&db,
timeout,
worker_instance,
worker_name,
i_worker,
num_workers,
ip,
sleep_queue,
worker_config,
None,
rx,
&base_internal_url,
)
.await
};
@@ -1767,7 +1752,7 @@ async fn test_invalid_first_step(db: Pool<Postgres>) {
assert_eq!(
job.result.unwrap(),
serde_json::json!({"error":"Expected an array value, found: {}"})
serde_json::json!( {"error": {"name": "InternalErr", "message": "Expected an array value, found: {}"}})
);
}
@@ -2083,7 +2068,7 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
assert_eq!(
result,
serde_json::json!([{"error": "Error during execution of the script:\n\nerror: Uncaught (in promise) Error: failure\nexport function main(){ throw Error('failure') }\n ^\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1"}, [1,3]])
serde_json::json!([{"error": {"name": "Error", "stack": "Error: failure\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1", "message": "failure"}}, [1,3]])
);
let flow: FlowValue = serde_json::from_value(json!({
@@ -2117,7 +2102,7 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
assert_eq!(
result,
serde_json::json!([{"error": "Error during execution of the script:\n\nerror: Uncaught (in promise) Error: failure\nexport function main(){ throw Error('failure') }\n ^\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1"}, [1, 2]])
serde_json::json!([ {"error": {"name": "Error", "stack": "Error: failure\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1", "message": "failure"}}, [1, 2]])
);
}
@@ -2256,7 +2241,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
},
"type": "rawscript",
"language": "deno",
"content": "export function main(n, l) { if (n == 0) throw l; return { l: [...l, 0] } }",
"content": "export function main(n, l) { if (n == 0) throw Error(JSON.stringify(l)); return { l: [...l, 0] } }",
},
}, {
"id": "b",
@@ -2267,7 +2252,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
},
"type": "rawscript",
"language": "deno",
"content": "export function main(n, l) { if (n == 1) throw l; return { l: [...l, 1] } }",
"content": "export function main(n, l) { if (n == 1) throw Error(JSON.stringify(l)); return { l: [...l, 1] } }",
},
}, {
"value": {
@@ -2277,7 +2262,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
},
"type": "rawscript",
"language": "deno",
"content": "export function main(n, l) { if (n == 2) throw l; return { l: [...l, 2] } }",
"content": "export function main(n, l) { if (n == 2) throw Error(JSON.stringify(l)); return { l: [...l, 2] } }",
},
}],
"failure_module": {
@@ -2297,10 +2282,15 @@ async fn test_failure_module(db: Pool<Postgres>) {
.await
.result
.unwrap();
assert!(result["from failure module"]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("Uncaught (in promise) []"));
.contains("[]"));
let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None })
.arg("n", json!(1))
@@ -2308,10 +2298,15 @@ async fn test_failure_module(db: Pool<Postgres>) {
.await
.result
.unwrap();
assert!(result["from failure module"]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("Uncaught (in promise) [ 0 ]"));
.contains("[0]"));
let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None })
.arg("n", json!(2))
@@ -2319,10 +2314,15 @@ async fn test_failure_module(db: Pool<Postgres>) {
.await
.result
.unwrap();
assert!(result["from failure module"]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("Uncaught (in promise) [ 0, 1 ]"));
.contains("[0,1]"));
let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None })
.arg("n", json!(3))

View File

@@ -8,12 +8,8 @@ edition.workspace = true
name = "windmill_api"
path = "src/lib.rs"
[[bin]]
name = "windmill_api"
path = "src/main.rs"
[features]
enterprise = ["windmill-queue/enterprise"]
enterprise = ["windmill-queue/enterprise", "async-stripe"]
[dependencies]
windmill-queue.workspace = true
@@ -69,3 +65,7 @@ hmac.workspace = true
cookie.workspace = true
sha2.workspace = true
urlencoding.workspace = true
async-stripe = { workspace = true, optional = true }
lazy_static.workspace = true
prometheus.workspace = true
async_zip.workspace = true

View File

@@ -0,0 +1,9 @@
#!/bin/bash
set -e
npx @redocly/openapi-cli@latest bundle openapi.yaml > openapi-bundled.yaml
npx @redocly/openapi-cli@latest bundle openapi-bundled.yaml --ext yaml -d > openapi-deref.yaml
rm openapi-bundled.yaml

View File

View File

@@ -1,10 +1,11 @@
openapi: "3.0.3"
info:
version: 1.58.0
title: Windmill server API
version: 1.70.1
title: Windmill API
contact:
name: Windmill contact
name: Windmill Team
email: contact@windmill.dev
url: https://windmill.dev
@@ -12,6 +13,8 @@ info:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
x-logo:
url: https://docs.windmill.dev/img/windmill.svg
externalDocs:
description: documentation portal
url: https://docs.windmill.dev
@@ -670,6 +673,43 @@ paths:
schema:
type: string
/w/{workspace}/workspaces/add_user:
post:
summary: add user to workspace
operationId: addUser
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: WorkspaceInvite
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
is_admin:
type: boolean
username:
type: string
operator:
type: boolean
required:
- email
- is_admin
- operator
- username
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/w/{workspace}/workspaces/delete_invite:
post:
summary: delete user invite
@@ -839,6 +879,12 @@ paths:
type: string
auto_invite_operator:
type: boolean
plan:
type: string
customer_id:
type: string
webhook:
type: string
/w/{workspace}/workspaces/premium_info:
get:
@@ -918,6 +964,33 @@ paths:
schema:
type: string
/w/{workspace}/workspaces/edit_webhook:
post:
summary: edit webhook
operationId: editWebhook
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: WorkspaceWebhook
required: true
content:
application/json:
schema:
type: object
properties:
webhook:
type: string
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/w/{workspace}/users/list:
get:
summary: list users
@@ -1019,6 +1092,10 @@ paths:
- variable
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: already_encrypted
in: query
schema:
type: boolean
requestBody:
description: new variable
required: true
@@ -1060,6 +1137,10 @@ paths:
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
- name: already_encrypted
in: query
schema:
type: boolean
requestBody:
description: updated variable
required: true
@@ -2004,6 +2085,25 @@ paths:
items:
$ref: "#/components/schemas/Script"
/w/{workspace}/scripts/list_paths:
get:
summary: list all available scripts paths
operationId: listScriptPaths
tags:
- script
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: list of script paths
content:
text/plain:
schema:
type: array
items:
type: string
/w/{workspace}/scripts/create:
post:
summary: create script
@@ -2365,6 +2465,45 @@ paths:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
requestBody:
description: script args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: job result
content:
application/json:
schema: {}
/w/{workspace}/jobs/run_wait_result/f/{path}:
post:
summary: run flow by path and wait until completion
operationId: runWaitResultFlowByPath
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- name: scheduled_for
description: when to schedule this job (leave empty for immediate run)
in: query
schema:
type: string
format: date-time
- name: scheduled_in_secs
description: schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
requestBody:
description: script args
@@ -2411,6 +2550,24 @@ paths:
application/json:
schema: {}
/w/{workspace}/flows/list_paths:
get:
summary: list all available flow paths
operationId: listFlowPaths
tags:
- flow
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: list of flow paths
content:
text/plain:
schema:
type: array
items:
type: string
/w/{workspace}/flows/list:
get:
summary: list all available flows
@@ -2559,6 +2716,23 @@ paths:
schema:
type: string
/w/{workspace}/flows/delete/{path}:
delete:
summary: delete flow by path
operationId: deleteFlowByPath
tags:
- flow
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: flow delete
content:
text/plain:
schema:
type: string
/w/{workspace}/apps/list:
get:
summary: list all available apps
@@ -2632,7 +2806,7 @@ paths:
schema:
$ref: "#/components/schemas/AppWithLastVersion"
/w/{workspace}/apps/public_app/{path}:
/w/{workspace}/apps_u/public_app/{path}:
get:
summary: get public app by secret
operationId: getPublicAppBySecret
@@ -2769,7 +2943,7 @@ paths:
schema:
type: string
/w/{workspace}/apps/execute_component/{path}:
/w/{workspace}/apps_u/execute_component/{path}:
post:
summary: executeComponent
operationId: executeComponent
@@ -2986,7 +3160,8 @@ paths:
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/Success"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/Suspended"
- $ref: "#/components/parameters/Running"
responses:
"200":
description: All available queued jobs
@@ -3076,7 +3251,7 @@ paths:
items:
$ref: "#/components/schemas/Job"
/w/{workspace}/jobs/get/{id}:
/w/{workspace}/jobs_u/get/{id}:
get:
summary: get job
operationId: getJob
@@ -3110,7 +3285,7 @@ paths:
# schema:
# type: string
/w/{workspace}/jobs/getupdate/{id}:
/w/{workspace}/jobs_u/getupdate/{id}:
get:
summary: get job updates
operationId: getJobUpdates
@@ -3142,6 +3317,8 @@ paths:
type: boolean
new_logs:
type: string
mem_peak:
type: integer
/w/{workspace}/jobs/completed/get/{id}:
get:
@@ -3268,7 +3445,7 @@ paths:
- resume
- cancel
/w/{workspace}/jobs/resume/{id}/{resume_id}/{signature}:
/w/{workspace}/jobs_u/resume/{id}/{resume_id}/{signature}:
get:
summary: resume a job for a suspended flow
operationId: resumeSuspendedJobGet
@@ -3358,7 +3535,7 @@ paths:
schema:
type: string
/w/{workspace}/jobs/cancel/{id}/{resume_id}/{signature}:
/w/{workspace}/jobs_u/cancel/{id}/{resume_id}/{signature}:
get:
summary: cancel a job for a suspended flow
operationId: cancelSuspendedJobGet
@@ -3425,7 +3602,7 @@ paths:
schema:
type: string
/w/{workspace}/jobs/get_flow/{id}/{resume_id}/{signature}:
/w/{workspace}/jobs_u/get_flow/{id}/{resume_id}/{signature}:
get:
summary: get parent flow job of suspended job
operationId: getSuspendedJobFlow
@@ -4190,6 +4367,20 @@ paths:
schema:
type: string
/w/{workspace}/capture_u/{path}:
post:
summary: update flow preview capture
operationId: updateCapture
tags:
- capture
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"204":
description: flow preview captured
/w/{workspace}/capture/{path}:
put:
summary: create flow preview capture
@@ -4202,17 +4393,6 @@ paths:
responses:
"201":
description: flow preview capture created
post:
summary: update flow preview capture
operationId: updateCapture
tags:
- capture
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"204":
description: flow preview captured
get:
summary: get flow preview capture
operationId: getCapture
@@ -4380,6 +4560,13 @@ components:
in: query
schema:
type: string
QueueLimit:
name: queue_limit
description: |
The maximum size of the queue for which the request would get rejected if that job would push it above that limit
in: query
schema:
type: string
ScriptStartPath:
name: script_path_start
@@ -4419,6 +4606,18 @@ components:
in: query
schema:
type: boolean
Suspended:
name: suspended
description: filter on suspended jobs
in: query
schema:
type: boolean
Running:
name: running
description: filter on running jobs
in: query
schema:
type: boolean
After:
name: after
description: filter on created after (exclusive) timestamp
@@ -4618,6 +4817,8 @@ components:
type: string
visible_to_owner:
type: boolean
mem_peak:
type: integer
required:
- id
- running
@@ -4704,6 +4905,8 @@ components:
type: string
visible_to_owner:
type: boolean
mem_peak:
type: integer
required:
- id
- created_by

View File

@@ -12,15 +12,16 @@ use crate::{
jobs::script_path_to_payload,
users::{require_owner_of_path, Authed, OptAuthed},
variables::build_crypt,
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
use axum::{
extract::{Extension, Path, Query},
extract::{Extension, Json, Path, Query},
routing::{delete, get, post},
Json, Router,
Router,
};
use hyper::StatusCode;
use magic_crypt::MagicCryptTrait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use sha2::{Digest, Sha256};
@@ -31,7 +32,7 @@ use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
apps::ListAppQuery,
error::{to_anyhow, Error, JsonResult, Result},
users::owner_to_token_owner,
users::username_to_permissioned_as,
utils::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
},
@@ -137,10 +138,10 @@ pub struct EditApp {
async fn list_apps(
authed: Authed,
Query(pagination): Query<Pagination>,
Query(lq): Query<ListAppQuery>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(lq): Query<ListAppQuery>,
) -> JsonResult<Vec<ListableApp>> {
let (per_page, offset) = paginate(pagination);
@@ -169,7 +170,7 @@ async fn list_apps(
)
.order_desc("favorite.path IS NOT NULL")
.order_by("app_version.created_at", true)
.and_where("app.workspace_id = ? OR app.workspace_id = 'starter'".bind(&w_id))
.and_where("app.workspace_id = ?".bind(&w_id))
.offset(offset)
.limit(per_page)
.clone();
@@ -310,6 +311,7 @@ async fn get_secret_id(
async fn create_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
@@ -356,17 +358,19 @@ async fn create_app(
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateApp { workspace: w_id, path: app.path.clone() },
);
Ok((StatusCode::CREATED, app.path))
}
async fn list_hub_apps(
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
async fn list_hub_apps(Authed { email, .. }: Authed) -> JsonResult<serde_json::Value> {
let flows = list_elems_from_hub(
http_client,
&HTTP_CLIENT,
"https://hub.windmill.dev/searchUiData?approved=true",
&email,
)
@@ -377,10 +381,9 @@ async fn list_hub_apps(
pub async fn get_hub_app_by_id(
Authed { email, .. }: Authed,
Path(id): Path<i32>,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
let value = http_get_from_hub(
http_client,
&HTTP_CLIENT,
&format!("https://hub.windmill.dev/apps/{id}/json"),
&email,
false,
@@ -395,6 +398,7 @@ pub async fn get_hub_app_by_id(
async fn delete_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -418,6 +422,10 @@ async fn delete_app(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone().clone(),
WebhookMessage::DeleteApp { workspace: w_id, path: path.to_owned() },
);
Ok(format!("app {} deleted", path))
}
@@ -425,6 +433,7 @@ async fn delete_app(
async fn update_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditApp>,
@@ -514,6 +523,14 @@ async fn update_app(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateApp {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("app {} updated (npath: {:?})", path, npath))
}
@@ -610,7 +627,7 @@ async fn execute_component(
})?;
(
username.clone(),
owner_to_token_owner(&username, false),
username_to_permissioned_as(&username),
email,
)
}

View File

@@ -7,12 +7,11 @@
*/
use hyper::StatusCode;
use reqwest::Client;
use sql_builder::prelude::*;
use axum::{
extract::{Extension, Path, Query},
routing::{get, post},
routing::{delete, get, post},
Json, Router,
};
use sql_builder::SqlBuilder;
@@ -32,6 +31,8 @@ use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
pub fn workspaced_service() -> Router {
@@ -40,8 +41,10 @@ pub fn workspaced_service() -> Router {
.route("/create", post(create_flow))
.route("/update/*path", post(update_flow))
.route("/archive/*path", post(archive_flow_by_path))
.route("/delete/*path", delete(delete_flow_by_path))
.route("/get/*path", get(get_flow_by_path))
.route("/exists/*path", get(exists_flow_by_path))
.route("/list_paths", get(list_paths))
}
pub fn global_service() -> Router {
@@ -79,7 +82,7 @@ async fn list_flows(
)
.order_desc("favorite.path IS NOT NULL")
.order_by("edited_at", lq.order_desc.unwrap_or(true))
.and_where("o.workspace_id = ? OR o.workspace_id = 'starter'".bind(&w_id))
.and_where("o.workspace_id = ?".bind(&w_id))
.offset(offset)
.limit(per_page)
.clone();
@@ -109,12 +112,9 @@ async fn list_flows(
Ok(Json(rows))
}
async fn list_hub_flows(
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
async fn list_hub_flows(Authed { email, .. }: Authed) -> JsonResult<serde_json::Value> {
let flows = list_elems_from_hub(
http_client,
&HTTP_CLIENT,
"https://hub.windmill.dev/searchFlowData?approved=true",
&email,
)
@@ -122,13 +122,30 @@ async fn list_hub_flows(
Ok(Json(flows))
}
async fn list_paths(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
) -> JsonResult<Vec<String>> {
let mut tx = user_db.begin(&authed).await?;
let flows = sqlx::query_scalar!(
"SELECT distinct(path) FROM flow WHERE workspace_id = $1",
w_id
)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(flows))
}
pub async fn get_hub_flow_by_id(
Authed { email, .. }: Authed,
Path(id): Path<i32>,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
let value = http_get_from_hub(
http_client,
&HTTP_CLIENT,
&format!("https://hub.windmill.dev/flows/{id}/json"),
&email,
false,
@@ -162,6 +179,7 @@ async fn check_path_conflict<'c>(
async fn create_flow(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(nf): Json<NewFlow>,
) -> Result<(StatusCode, String)> {
@@ -202,6 +220,10 @@ async fn create_flow(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateFlow { workspace: w_id.clone(), path: nf.path.clone() },
);
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push(
@@ -211,7 +233,7 @@ async fn create_flow(
serde_json::Map::new(),
&authed.username,
&authed.email,
windmill_common::users::owner_to_token_owner(&authed.username, false),
windmill_common::users::username_to_permissioned_as(&authed.username),
None,
None,
None,
@@ -261,6 +283,7 @@ async fn update_flow(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Json(nf): Json<NewFlow>,
) -> Result<String> {
@@ -349,6 +372,14 @@ async fn update_flow(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFlow {
workspace: w_id.clone(),
old_path: flow_path.to_owned(),
new_path: nf.path.clone(),
},
);
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push(
@@ -358,7 +389,7 @@ async fn update_flow(
serde_json::Map::new(),
&authed.username,
&authed.email,
windmill_common::users::owner_to_token_owner(&authed.username, false),
windmill_common::users::username_to_permissioned_as(&authed.username),
None,
None,
None,
@@ -396,13 +427,12 @@ async fn get_flow_by_path(
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let flow_o = sqlx::query_as::<_, Flow>(
"SELECT * FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
)
.bind(path)
.bind(w_id)
.fetch_optional(&mut tx)
.await?;
let flow_o =
sqlx::query_as::<_, Flow>("SELECT * FROM flow WHERE path = $1 AND workspace_id = $2")
.bind(path)
.bind(w_id)
.fetch_optional(&mut tx)
.await?;
tx.commit().await?;
let flow = not_found_if_none(flow_o, "Flow", path)?;
@@ -416,8 +446,7 @@ async fn exists_flow_by_path(
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id \
= 'starter'))",
"SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
@@ -431,6 +460,7 @@ async fn exists_flow_by_path(
async fn archive_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -455,10 +485,50 @@ async fn archive_flow_by_path(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::ArchiveFlow { workspace: w_id, path: path.to_owned() },
);
Ok(format!("Flow {path} archived"))
}
async fn delete_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"DELETE FROM flow WHERE path = $1 AND workspace_id = $2",
path,
&w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
"flows.delete",
ActionKind::Delete,
&w_id,
Some(path),
Some([("workspace", w_id.as_str())].into()),
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteFlow { workspace: w_id, path: path.to_owned() },
);
Ok(format!("Flow {path} deleted"))
}
#[cfg(test)]
mod tests {

View File

@@ -9,6 +9,7 @@
use crate::{
db::{UserDB, DB},
users::Authed,
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
extract::{Extension, Path, Query},
@@ -19,7 +20,7 @@ use itertools::Itertools;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, Error, JsonResult, Result},
users::owner_to_token_owner,
users::username_to_permissioned_as,
utils::{not_found_if_none, paginate, Pagination},
};
@@ -139,13 +140,14 @@ async fn check_name_conflict<'c>(
async fn create_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(ng): Json<NewFolder>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
check_name_conflict(&mut tx, &w_id, &ng.name).await?;
let owner = owner_to_token_owner(&authed.username, false);
let owner = username_to_permissioned_as(&authed.username);
let owners = &ng.owners.unwrap_or(vec![owner.clone()]);
if let Some(extra_perms) = ng.extra_perms.clone() {
@@ -193,8 +195,12 @@ async fn create_folder(
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateFolder { workspace: w_id, name: ng.name.clone() },
);
Ok(format!("Created folder {}", ng.name))
}
@@ -245,6 +251,7 @@ pub async fn require_is_owner(
async fn update_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(ng): Json<UpdateFolder>,
) -> Result<String> {
@@ -298,8 +305,12 @@ async fn update_folder(
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone().clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.to_owned() },
);
Ok(format!("Updated folder {}", name))
}
@@ -416,6 +427,7 @@ async fn get_folder_usage(
async fn delete_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
@@ -440,6 +452,12 @@ async fn delete_folder(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("delete folder at name {}", name))
}
@@ -447,6 +465,7 @@ async fn add_owner(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>,
) -> Result<String> {
@@ -477,6 +496,12 @@ async fn add_owner(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("Added {} to folder {}", owner, name))
}
@@ -510,6 +535,7 @@ async fn remove_owner(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>,
) -> Result<String> {
@@ -540,5 +566,11 @@ async fn remove_owner(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("Removed {} to folder {}", owner, name))
}

View File

@@ -16,9 +16,9 @@ use axum::{
Json, Router,
};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::users::username_to_permissioned_as;
use windmill_common::{
error::{Error, JsonResult, Result},
users::owner_to_token_owner,
utils::{not_found_if_none, paginate, Pagination},
};
@@ -199,7 +199,17 @@ async fn create_group(
w_id,
ng.name,
ng.summary,
serde_json::json!({owner_to_token_owner(&authed.username, false): true})
serde_json::json!({username_to_permissioned_as(&authed.username): true})
)
.execute(&mut tx)
.await?;
sqlx::query_as!(
Group,
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
&w_id,
&authed.username,
ng.name,
)
.execute(&mut tx)
.await?;
@@ -282,7 +292,9 @@ async fn delete_group(
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
sqlx::query!(
@@ -321,8 +333,9 @@ async fn update_group(
Json(eg): Json<EditGroup>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
sqlx::query_as!(
@@ -357,8 +370,9 @@ async fn add_user(
Json(Username { username: user_username }): Json<Username>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
@@ -394,7 +408,9 @@ async fn remove_user(
Json(Username { username: user_username }): Json<Username>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
if &name == "all" {

View File

@@ -6,17 +6,16 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::sync::Arc;
use anyhow::Context;
use axum::{
extract::{FromRequest, Path, Query},
extract::{FromRequest, Json, Path, Query},
response::{IntoResponse, Response},
routing::{get, post},
Extension, Json, Router,
Extension, Router,
};
use base64::Engine;
use hmac::Mac;
use hyper::{HeaderMap, StatusCode};
use hyper::{HeaderMap, Request, StatusCode};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sql_builder::{prelude::*, quote, SqlBuilder};
use sqlx::{query_scalar, types::Uuid, FromRow, Postgres, Transaction};
@@ -28,7 +27,7 @@ use windmill_common::{
flows::FlowValue,
oauth2::HmacSha256,
scripts::{ScriptHash, ScriptLang},
users::owner_to_token_owner,
users::username_to_permissioned_as,
utils::{not_found_if_none, now_from_db, paginate, require_admin, Pagination, StripPath},
};
use windmill_queue::{get_queued_job, push, JobKind, JobPayload, QueuedJob, RawCode};
@@ -37,7 +36,7 @@ use crate::{
db::{UserDB, DB},
users::{require_owner_of_path, Authed},
variables::get_workspace_key,
BaseUrl,
BASE_URL,
};
pub fn workspaced_service() -> Router {
@@ -52,6 +51,10 @@ pub fn workspaced_service() -> Router {
"/run_wait_result/h/:hash",
post(run_wait_result_job_by_hash),
)
.route(
"/run_wait_result/f/*script_path",
post(run_wait_result_flow_by_path),
)
.route("/run/h/:hash", post(run_job_by_hash))
.route("/run/preview", post(run_preview_job))
.route("/run/preview_flow", post(run_preview_flow_job))
@@ -149,8 +152,7 @@ pub async fn get_path_for_hash<'c>(
hash: i64,
) -> error::Result<String> {
let path = sqlx::query_scalar!(
"select path from script where hash = $1 AND (workspace_id = $2 OR workspace_id = \
'starter')",
"select path from script where hash = $1 AND workspace_id = $2",
hash,
w_id
)
@@ -215,32 +217,46 @@ pub async fn get_job_by_id<'c>(
pub struct CompletedJob {
pub workspace_id: String,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub started_at: chrono::DateTime<chrono::Utc>,
pub duration_ms: i32,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logs: Option<String>,
pub deleted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_code: Option<String>,
pub canceled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_reason: Option<String>,
pub job_kind: JobKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule_path: Option<String>,
pub permissioned_as: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_status: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_flow: Option<serde_json::Value>,
pub is_flow_step: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<ScriptLang>,
pub is_skipped: bool,
pub email: String,
pub visible_to_owner: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
}
#[derive(Deserialize, Clone)]
@@ -250,6 +266,15 @@ pub struct RunJobQuery {
parent_job: Option<Uuid>,
include_header: Option<String>,
invisible_to_owner: Option<bool>,
queue_limit: Option<i64>,
}
lazy_static::lazy_static! {
static ref INCLUDE_HEADERS: Vec<String> = std::env::var("INCLUDE_HEADERS")
.ok().map(|x| x
.split(',')
.map(|s| s.to_string())
.collect()).unwrap_or_default();
}
impl RunJobQuery {
@@ -272,11 +297,14 @@ impl RunJobQuery {
headers: HeaderMap,
mut args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
self.include_header
let whitelist = self
.include_header
.as_ref()
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
.unwrap_or_default()
.unwrap_or_default();
whitelist
.iter()
.chain(INCLUDE_HEADERS.iter())
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
@@ -336,6 +364,7 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
if let Some(dt) = &lq.created_after {
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(s) = &lq.suspended {
if *s {
sqlb.and_where_gt("suspend", 0);
@@ -442,7 +471,7 @@ async fn list_jobs(
"running",
"script_hash",
"script_path",
"args",
"CASE WHEN pg_column_size(args) > 1000 THEN '\"too large args\"'::jsonb ELSE args END",
"null as duration_ms",
"null as success",
"false as deleted",
@@ -451,13 +480,13 @@ async fn list_jobs(
"job_kind",
"schedule_path",
"permissioned_as",
"flow_status",
"is_flow_step",
"language",
"false as is_skipped",
"email",
"visible_to_owner",
"suspend",
"mem_peak",
],
);
let sqlc = list_completed_jobs_query(
@@ -477,7 +506,7 @@ async fn list_jobs(
"null as running",
"script_hash",
"script_path",
"args",
"CASE WHEN pg_column_size(args) > 1000 THEN '\"too large args\"'::jsonb ELSE args END",
"duration_ms",
"success",
"deleted",
@@ -486,13 +515,13 @@ async fn list_jobs(
"job_kind",
"schedule_path",
"permissioned_as",
"flow_status",
"is_flow_step",
"language",
"is_skipped",
"email",
"visible_to_owner",
"null as suspend",
"mem_peak",
],
);
let sql = format!(
@@ -541,8 +570,8 @@ pub async fn resume_suspended_job(
/* unauthed */
Extension(db): Extension<DB>,
Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
Query(approver): Query<QueryApprover>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
) -> error::Result<StatusCode> {
let value = value.unwrap_or(serde_json::Value::Null);
let mut tx = db.begin().await?;
@@ -885,7 +914,7 @@ fn build_resume_url(
approver: &str,
base_url: &str,
) -> String {
format!("{base_url}/api/w/{w_id}/jobs/{op}/{job_id}/{resume_id}/{signature}{approver}")
format!("{base_url}/api/w/{w_id}/jobs_u/{op}/{job_id}/{resume_id}/{signature}{approver}")
}
pub async fn get_resume_urls(
@@ -893,16 +922,16 @@ pub async fn get_resume_urls(
Extension(user_db): Extension<UserDB>,
Path((w_id, job_id, resume_id)): Path<(String, Uuid, u32)>,
Query(approver): Query<QueryApprover>,
Extension(base_url): Extension<Arc<BaseUrl>>,
) -> error::JsonResult<ResumeUrls> {
let key = get_workspace_key(&w_id, &mut user_db.begin(&authed).await?).await?;
let signature = create_signature(key, job_id, resume_id, approver.approver.clone())?;
let base_url = base_url.0.clone();
let approver = approver
.approver
.as_ref()
.map(|x| format!("?approver={}", encode(x)))
.unwrap_or_else(String::new);
let base_url = BASE_URL.as_str();
let res = ResumeUrls {
approvalPage: format!(
"{base_url}/approve/{w_id}/{job_id}/{resume_id}/{signature}{approver}"
@@ -964,13 +993,13 @@ struct UnifiedJob {
job_kind: JobKind,
schedule_path: Option<String>,
permissioned_as: String,
flow_status: Option<serde_json::Value>,
is_flow_step: bool,
language: Option<ScriptLang>,
is_skipped: bool,
email: String,
visible_to_owner: bool,
suspend: Option<i32>,
mem_peak: Option<i32>,
}
impl From<UnifiedJob> for Job {
@@ -990,6 +1019,7 @@ impl From<UnifiedJob> for Job {
args: uj.args,
result: None,
logs: None,
flow_status: None,
deleted: uj.deleted,
canceled: uj.canceled,
canceled_by: uj.canceled_by,
@@ -998,13 +1028,13 @@ impl From<UnifiedJob> for Job {
job_kind: uj.job_kind,
schedule_path: uj.schedule_path,
permissioned_as: uj.permissioned_as,
flow_status: uj.flow_status,
raw_flow: None,
is_flow_step: uj.is_flow_step,
language: uj.language,
is_skipped: uj.is_skipped,
email: uj.email,
visible_to_owner: uj.visible_to_owner,
mem_peak: uj.mem_peak,
}),
"QueuedJob" => Job::QueuedJob(QueuedJob {
workspace_id: uj.workspace_id,
@@ -1019,6 +1049,7 @@ impl From<UnifiedJob> for Job {
running: uj.running.unwrap(),
scheduled_for: uj.scheduled_for.unwrap(),
logs: None,
flow_status: None,
raw_code: None,
raw_lock: None,
canceled: uj.canceled,
@@ -1028,7 +1059,6 @@ impl From<UnifiedJob> for Job {
job_kind: uj.job_kind,
schedule_path: uj.schedule_path,
permissioned_as: uj.permissioned_as,
flow_status: uj.flow_status,
raw_flow: None,
is_flow_step: uj.is_flow_step,
language: uj.language,
@@ -1037,6 +1067,7 @@ impl From<UnifiedJob> for Job {
email: uj.email,
visible_to_owner: uj.visible_to_owner,
suspend: uj.suspend,
mem_peak: uj.mem_peak,
}),
t => panic!("job type {} not valid", t),
}
@@ -1065,20 +1096,19 @@ struct PreviewFlow {
pub struct QueryOrBody<D>(pub Option<D>);
#[axum::async_trait]
impl<D, B> FromRequest<B> for QueryOrBody<D>
impl<S, D> FromRequest<S, axum::body::Body> for QueryOrBody<D>
where
D: DeserializeOwned,
B: Send + axum::body::HttpBody,
<B as axum::body::HttpBody>::Data: Send,
<B as axum::body::HttpBody>::Error: Into<axum::BoxError>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(
req: &mut axum::extract::RequestParts<B>,
req: Request<axum::body::Body>,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
return if req.method() == axum::http::Method::GET {
let Query(InPayload { payload }) = Query::from_request(req)
let Query(InPayload { payload }) = Query::from_request(req, state)
.await
.map_err(IntoResponse::into_response)?;
payload
@@ -1090,7 +1120,7 @@ where
})
.unwrap_or(Ok(QueryOrBody(None)))
} else {
Json::from_request(req)
Json::from_request(req, state)
.await
.map(|Json(v)| QueryOrBody(Some(v)))
.map_err(IntoResponse::into_response)
@@ -1102,7 +1132,9 @@ where
}
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
let vec = base64::decode_config(&t, base64::URL_SAFE).context("invalid base64")?;
let vec = base64::engine::general_purpose::URL_SAFE
.decode(t)
.context("invalid base64")?;
serde_json::from_slice(vec.as_slice()).context("invalid json")
}
}
@@ -1111,9 +1143,9 @@ pub async fn run_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
axum::Json(args): axum::Json<Option<serde_json::Map<String, serde_json::Value>>>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::Result<(StatusCode, String)> {
let flow_path = flow_path.to_path();
let mut tx = user_db.begin(&authed).await?;
@@ -1127,7 +1159,7 @@ pub async fn run_flow_by_path(
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
run_query.parent_job,
@@ -1145,9 +1177,9 @@ pub async fn run_job_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
axum::Json(args): axum::Json<Option<serde_json::Map<String, serde_json::Value>>>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::Result<(StatusCode, String)> {
let script_path = script_path.to_path();
let mut tx = user_db.begin(&authed).await?;
@@ -1162,7 +1194,7 @@ pub async fn run_job_by_path(
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
run_query.parent_job,
@@ -1176,16 +1208,66 @@ pub async fn run_job_by_path(
Ok((StatusCode::CREATED, uuid.to_string()))
}
struct Guard {
done: bool,
id: Uuid,
w_id: String,
db: UserDB,
authed: Authed,
}
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,
id,
w_id
)
.execute(&mut tx)
.await;
let _ = tx.commit().await;
}
});
}
}
}
async fn run_wait_result<T>(
authed: Authed,
Extension(user_db): Extension<UserDB>,
timeout: i32,
uuid: Uuid,
Path((w_id, _)): Path<(String, T)>,
) -> error::JsonResult<serde_json::Value> {
let mut result = None;
for i in 0..48 {
let iters = if timeout <= 0 {
20
} else if timeout <= 1 {
timeout * 10
} else {
10 + ((timeout - 1) * 2)
};
let mut g = Guard {
done: false,
id: uuid,
w_id: w_id.clone(),
db: user_db.clone(),
authed: authed.clone(),
};
for i in 0..iters {
let mut tx = user_db.clone().begin(&authed).await?;
result = sqlx::query_scalar!(
"SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2",
uuid,
@@ -1194,6 +1276,7 @@ async fn run_wait_result<T>(
.fetch_optional(&mut tx)
.await?
.flatten();
drop(tx);
if result.is_some() {
break;
@@ -1202,20 +1285,53 @@ async fn run_wait_result<T>(
tokio::time::sleep(core::time::Duration::from_millis(delay)).await;
}
if let Some(result) = result {
g.done = true;
Ok(Json(result))
} else {
Err(Error::ExecutionErr("timeout after 20s".to_string()))
Err(Error::ExecutionErr(format!("timeout after {}s", timeout)))
}
}
pub async fn check_queue_too_long(db: DB, queue_limit: Option<i64>) -> error::Result<()> {
if let Some(limit) = queue_limit {
let count = sqlx::query_scalar!(
"SELECT COUNT(*) FROM queue WHERE canceled = false AND (scheduled_for <= now()
OR (suspend_until IS NOT NULL
AND ( suspend <= 0
OR suspend_until <= now())))",
)
.fetch_one(&db)
.await?
.unwrap_or(0);
if count > queue_limit.unwrap() {
return Err(Error::InternalErr(format!(
"Number of queued job is too high: {count} > {limit}"
)));
}
}
Ok(())
}
lazy_static::lazy_static! {
pub static ref QUEUE_LIMIT_WAIT_RESULT: Option<i64> = std::env::var("QUEUE_LIMIT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse().ok());
pub static ref TIMEOUT_WAIT_RESULT: i32 = std::env::var("TIMEOUT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(20);
}
pub async fn run_wait_result_job_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
axum::Json(args): axum::Json<Option<serde_json::Map<String, serde_json::Value>>>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::JsonResult<serde_json::Value> {
check_queue_too_long(db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
let script_path = script_path.to_path();
let mut tx = user_db.clone().begin(&authed).await?;
let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
@@ -1230,7 +1346,7 @@ pub async fn run_wait_result_job_by_path(
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
run_query.parent_job,
@@ -1242,17 +1358,27 @@ pub async fn run_wait_result_job_by_path(
.await?;
tx.commit().await?;
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_path))).await
run_wait_result(
authed,
Extension(user_db),
*TIMEOUT_WAIT_RESULT,
uuid,
Path((w_id, script_path)),
)
.await
}
pub async fn run_wait_result_job_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
axum::Json(args): axum::Json<Option<serde_json::Map<String, serde_json::Value>>>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::JsonResult<serde_json::Value> {
check_queue_too_long(db, run_query.queue_limit).await?;
let hash = script_hash.0;
let mut tx = user_db.clone().begin(&authed).await?;
let path = get_path_for_hash(&mut tx, &w_id, hash).await?;
@@ -1266,7 +1392,7 @@ pub async fn run_wait_result_job_by_hash(
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
run_query.parent_job,
@@ -1278,7 +1404,60 @@ pub async fn run_wait_result_job_by_hash(
.await?;
tx.commit().await?;
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_hash))).await
run_wait_result(
authed,
Extension(user_db),
*TIMEOUT_WAIT_RESULT,
uuid,
Path((w_id, script_hash)),
)
.await
}
pub async fn run_wait_result_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::JsonResult<serde_json::Value> {
check_queue_too_long(db, run_query.queue_limit).await?;
let flow_path = flow_path.to_path();
let mut tx = user_db.clone().begin(&authed).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
tx,
&w_id,
JobPayload::Flow(flow_path.to_string()),
args,
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
run_query.parent_job,
false,
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
)
.await?;
tx.commit().await?;
run_wait_result(
authed,
Extension(user_db),
*TIMEOUT_WAIT_RESULT,
uuid,
Path((w_id, flow_path)),
)
.await
}
// a similar function exists on the worker
@@ -1300,9 +1479,9 @@ async fn run_preview_job(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(preview): Json<Preview>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(preview): Json<Preview>,
) -> error::Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
@@ -1320,7 +1499,7 @@ async fn run_preview_job(
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
None,
@@ -1338,9 +1517,9 @@ async fn run_preview_flow_job(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(raw_flow): Json<PreviewFlow>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(raw_flow): Json<PreviewFlow>,
) -> error::Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
@@ -1353,7 +1532,7 @@ async fn run_preview_flow_job(
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
None,
@@ -1371,9 +1550,9 @@ pub async fn run_job_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
axum::Json(args): axum::Json<Option<serde_json::Map<String, serde_json::Value>>>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::Result<(StatusCode, String)> {
let hash = script_hash.0;
let mut tx = user_db.begin(&authed).await?;
@@ -1388,7 +1567,7 @@ pub async fn run_job_by_hash(
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
run_query.parent_job,
@@ -1413,6 +1592,7 @@ pub struct JobUpdate {
pub running: Option<bool>,
pub completed: Option<bool>,
pub new_logs: Option<String>,
pub mem_peak: Option<i32>,
}
async fn get_job_update(
@@ -1422,8 +1602,8 @@ async fn get_job_update(
) -> error::JsonResult<JobUpdate> {
let mut tx = db.begin().await?;
let logs = query_scalar!(
"SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3",
let record = sqlx::query!(
"SELECT substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3",
log_offset,
&w_id,
&id
@@ -1431,12 +1611,13 @@ async fn get_job_update(
.fetch_optional(&mut tx)
.await?;
if let Some(logs) = logs {
if let Some(record) = record {
tx.commit().await?;
Ok(Json(JobUpdate {
running: if !running { Some(true) } else { None },
completed: None,
new_logs: logs,
new_logs: record.logs,
mem_peak: record.mem_peak,
}))
} else {
let logs = query_scalar!(
@@ -1454,6 +1635,7 @@ async fn get_job_update(
running: Some(false),
completed: Some(true),
new_logs: logs,
mem_peak: record.map(|r| r.mem_peak).flatten(),
}))
}
}
@@ -1571,6 +1753,7 @@ async fn list_completed_jobs(
"is_skipped",
"email",
"visible_to_owner",
"mem_peak",
],
)
.sql()?;
@@ -1621,7 +1804,7 @@ async fn delete_completed_job(
require_admin(authed.is_admin, &authed.username)?;
let job_o = sqlx::query_as::<_, CompletedJob>(
"UPDATE completed_job SET logs = '', deleted = true WHERE id = $1 AND workspace_id = $2 \
"UPDATE completed_job SET logs = '', result = null, deleted = true WHERE id = $1 AND workspace_id = $2 \
RETURNING *",
)
.bind(id)

View File

@@ -6,21 +6,24 @@
* LICENSE-AGPL for a copy of the license.
*/
use crate::oauth2::AllClients;
use argon2::Argon2;
use axum::{handler::Handler, middleware::from_extractor, routing::get, Extension, Router};
use axum::{middleware::from_extractor, routing::get, Extension, Router};
use db::DB;
use git_version::git_version;
use reqwest::Client;
use std::{net::SocketAddr, sync::Arc};
use tower::ServiceBuilder;
use tower_cookies::CookieManagerLayer;
use tower_http::trace::TraceLayer;
use windmill_common::{error::to_anyhow, utils::rd_string};
use windmill_common::utils::rd_string;
use crate::{
db::UserDB,
oauth2::{build_oauth_clients, SlackVerifier},
tracing_init::{MyMakeSpan, MyOnResponse},
users::{Authed, OptAuthed},
webhook_util::WebhookShared,
};
mod apps;
@@ -42,24 +45,39 @@ mod tracing_init;
mod users;
mod utils;
mod variables;
mod webhook_util;
mod worker_ping;
mod workspaces;
pub const GIT_VERSION: &str =
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
pub struct BaseUrl(String);
pub struct IsSecure(bool);
pub struct CookieDomain(Option<String>);
pub struct CloudHosted(bool);
pub struct ContentSecurityPolicy(String);
pub use users::delete_expired_items_perdiodically;
lazy_static::lazy_static! {
pub static ref BASE_URL: String = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
pub static ref COOKIE_DOMAIN: Option<String> = std::env::var("COOKIE_DOMAIN").ok();
pub static ref SLACK_SIGNING_SECRET: Option<SlackVerifier> = std::env::var("SLACK_SIGNING_SECRET")
.ok()
.map(|x| SlackVerifier::new(x).unwrap());
static ref IS_SECURE: bool = BASE_URL.starts_with("https://");
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.build().unwrap();
pub static ref OAUTH_CLIENTS: AllClients = build_oauth_clients(&BASE_URL)
.map_err(|e| tracing::error!("Error building oauth clients: {}", e))
.unwrap();
}
pub async fn run_server(
db: DB,
addr: SocketAddr,
base_url: String,
mut rx: tokio::sync::broadcast::Receiver<()>,
) -> anyhow::Result<()> {
let user_db = UserDB::new(db.clone());
@@ -69,16 +87,7 @@ pub async fn run_server(
std::env::var("SUPERADMIN_SECRET").ok(),
));
let argon2 = Arc::new(Argon2::default());
let basic_clients = Arc::new(build_oauth_clients(&base_url).await?);
let slack_verifier = Arc::new(
std::env::var("SLACK_SIGNING_SECRET")
.ok()
.map(|x| SlackVerifier::new(x).unwrap()),
);
let http_client = reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.build()
.map_err(to_anyhow)?;
let middleware_stack = ServiceBuilder::new()
.layer(
TraceLayer::new_for_http()
@@ -89,22 +98,8 @@ pub async fn run_server(
.layer(Extension(db.clone()))
.layer(Extension(user_db))
.layer(Extension(auth_cache.clone()))
.layer(Extension(basic_clients))
.layer(Extension(Arc::new(BaseUrl(base_url.to_string()))))
.layer(Extension(Arc::new(CloudHosted(
std::env::var("CLOUD_HOSTED").is_ok(),
))))
.layer(Extension(Arc::new(IsSecure(
base_url.starts_with("https://"),
))))
.layer(Extension(Arc::new(ContentSecurityPolicy(
std::env::var("SERVE_CSP").unwrap_or("".to_owned()),
))))
.layer(Extension(Arc::new(CookieDomain(
std::env::var("COOKIE_DOMAIN").ok(),
))))
.layer(Extension(http_client))
.layer(CookieManagerLayer::new());
.layer(CookieManagerLayer::new())
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())));
// build our application with a route
let app = Router::new()
.nest(
@@ -127,9 +122,9 @@ pub async fn run_server(
.nest("/audit", audit::workspaced_service())
.nest("/acls", granular_acls::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/apps", apps::workspaced_service())
.nest("/flows", flows::workspaced_service())
.nest("/capture", capture::workspaced_service())
.nest("/apps", apps::workspaced_service())
.nest("/favorites", favorite::workspaced_service())
.nest("/folders", folders::workspaced_service()),
)
@@ -146,23 +141,20 @@ pub async fn run_server(
.route_layer(from_extractor::<Authed>())
.route_layer(from_extractor::<users::Tokened>())
.nest(
"/w/:workspace_id/apps",
"/w/:workspace_id/apps_u",
apps::unauthed_service().layer(from_extractor::<OptAuthed>()),
)
.nest("/w/:workspace_id/jobs", jobs::global_service())
.nest("/w/:workspace_id/capture", capture::global_service())
.nest("/w/:workspace_id/jobs_u", jobs::global_service())
.nest("/w/:workspace_id/capture_u", capture::global_service())
.nest(
"/auth",
users::make_unauthed_service().layer(Extension(argon2)),
)
.nest(
"/oauth",
oauth2::global_service().layer(Extension(slack_verifier)),
)
.nest("/oauth", oauth2::global_service())
.route("/version", get(git_v))
.route("/openapi.yaml", get(openapi)),
)
.fallback(static_assets::static_handler.into_service())
.fallback(static_assets::static_handler)
.layer(middleware_stack);
let instance_name = rd_string(5);
@@ -186,7 +178,7 @@ async fn git_v() -> &'static str {
}
async fn openapi() -> &'static str {
include_str!("../openapi.yaml")
include_str!("../openapi-deref.yaml")
}
pub async fn migrate_db(db: &DB) -> anyhow::Result<()> {
db::migrate(db).await?;

View File

@@ -1,70 +0,0 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use std::net::SocketAddr;
use anyhow::Ok;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
windmill_common::tracing_init::initialize_tracing();
let db = windmill_common::connect_db(true).await?;
let num_workers = std::env::var("NUM_WORKERS")
.ok()
.and_then(|x| x.parse::<i32>().ok())
.unwrap_or(windmill_common::DEFAULT_NUM_WORKERS as i32);
let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
.ok()
.map(|s| {
s.parse::<bool>()
.map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
.or_else(|_| s.parse::<SocketAddr>().map(Some))
})
.transpose()?
.flatten();
let server_mode = !std::env::var("DISABLE_SERVER")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false);
if server_mode {
windmill_api::migrate_db(&db).await?;
}
let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
let shutdown_signal = windmill_common::shutdown_signal(tx);
let base_url = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
if server_mode || num_workers > 0 {
let addr = SocketAddr::from(([0, 0, 0, 0], 8000));
let server_f = async {
if server_mode {
windmill_api::run_server(db.clone(), addr, base_url, rx.resubscribe()).await?;
}
Ok(()) as anyhow::Result<()>
};
let metrics_f = async {
match metrics_addr {
Some(addr) => windmill_common::serve_metrics(addr, rx.resubscribe())
.await
.map_err(anyhow::Error::from),
None => Ok(()),
}
};
futures::try_join!(shutdown_signal, server_f, metrics_f)?;
}
Ok(())
}

View File

@@ -0,0 +1,69 @@
// /*
// * Author: Ruben Fiszel
// * Copyright: Windmill Labs, Inc 2022
// * This file and its contents are licensed under the AGPLv3 License.
// * Please see the included NOTICE for copyright information and
// * LICENSE-AGPL for a copy of the license.
// */
// use std::net::SocketAddr;
// use anyhow::Ok;
// pub const DEFAULT_NUM_WORKERS: usize = 3;
// #[tokio::main]
// async fn main() -> anyhow::Result<()> {
// windmill_common::tracing_init::initialize_tracing();
// let db = windmill_common::connect_db(true).await?;
// let num_workers = std::env::var("NUM_WORKERS")
// .ok()
// .and_then(|x| x.parse::<i32>().ok())
// .unwrap_or(DEFAULT_NUM_WORKERS as i32);
// let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
// .ok()
// .map(|s| {
// s.parse::<bool>()
// .map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
// .or_else(|_| s.parse::<SocketAddr>().map(Some))
// })
// .transpose()?
// .flatten();
// let server_mode = !std::env::var("DISABLE_SERVER")
// .ok()
// .and_then(|x| x.parse::<bool>().ok())
// .unwrap_or(false);
// if server_mode {
// windmill_api::migrate_db(&db).await?;
// }
// let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
// let shutdown_signal = windmill_common::shutdown_signal(tx);
// if server_mode || num_workers > 0 {
// let addr = SocketAddr::from(([0, 0, 0, 0], 8000));
// let server_f = async {
// if server_mode {
// windmill_api::run_server(db.clone(), addr, rx.resubscribe()).await?;
// }
// Ok(()) as anyhow::Result<()>
// };
// let metrics_f = async {
// match metrics_addr {
// Some(addr) => windmill_common::serve_metrics(addr, rx.resubscribe())
// .await
// .map_err(anyhow::Error::from),
// None => Ok(()),
// }
// };
// futures::try_join!(shutdown_signal, server_f, metrics_f)?;
// }
// Ok(())
// }

View File

@@ -8,13 +8,13 @@
use std::{collections::HashMap, fmt::Debug};
use std::sync::Arc;
use anyhow::Context;
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use axum::{
async_trait,
body::Bytes,
extract::{Extension, FromRequest, Path, Query, RequestParts},
extract::{Extension, Path, Query},
response::Redirect,
routing::{get, post},
Json, Router,
@@ -27,26 +27,25 @@ use oauth2::{Client as OClient, *};
use reqwest::Client;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sqlx::{Postgres, Transaction};
use tokio::{fs::File, io::AsyncReadExt};
use tower_cookies::{Cookie, Cookies};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::users::username_to_permissioned_as;
use windmill_common::utils::{not_found_if_none, now_from_db};
use crate::users::{truncate_token, Authed};
use crate::users::{truncate_token, Authed, NEW_USER_WEBHOOK};
use crate::workspaces::invite_user_to_all_auto_invite_worspaces;
use crate::{
db::{UserDB, DB},
variables::{build_crypt, encrypt},
workspaces::WorkspaceSettings,
BaseUrl,
};
use crate::{CookieDomain, IsSecure};
use crate::{BASE_URL, HTTP_CLIENT, IS_SECURE, OAUTH_CLIENTS, SLACK_SIGNING_SECRET};
use windmill_common::error::{self, to_anyhow, Error};
use windmill_common::oauth2::*;
use windmill_queue::JobPayload;
use std::str;
use std::{fs, str};
pub fn global_service() -> Router {
Router::new()
@@ -109,7 +108,7 @@ pub struct AllClients {
pub slack: Option<OClient>,
}
pub async fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
pub fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
let connect_configs = serde_json::from_str::<HashMap<String, OAuthConfig>>(include_str!(
"../../oauth_connect.json"
))?;
@@ -117,14 +116,12 @@ pub async fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
"../../oauth_login.json"
))?;
let mut content = String::new();
let path = "./oauth.json";
if std::path::Path::new(path).exists() {
let mut file = File::open(path).await?;
file.read_to_string(&mut content).await?;
let content = if std::path::Path::new(path).exists() {
fs::read_to_string(path).map_err(to_anyhow)?
} else {
content.push_str("{}");
}
"{}".to_string()
};
let oauths: HashMap<String, OAuthClient> =
match serde_json::from_str::<HashMap<String, OAuthClient>>(&content) {
@@ -286,12 +283,10 @@ pub struct SlackBotToken {
async fn connect(
Path(client_name): Path<String>,
Query(query): Query<HashMap<String, String>>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(is_secure): Extension<Arc<IsSecure>>,
cookies: Cookies,
) -> error::Result<Redirect> {
let mut query = query.clone();
let connects = &clients.connects;
let connects = &OAUTH_CLIENTS.connects;
let scopes = query
.get("scopes")
.map(|x| x.split('+').map(|x| x.to_owned()).collect());
@@ -307,7 +302,7 @@ async fn connect(
cookies,
scopes,
extra_params,
is_secure.0,
*IS_SECURE,
)
}
@@ -374,11 +369,9 @@ async fn delete_account(
Ok(format!("Deleted account id {id}"))
}
async fn list_logins(
Extension(clients): Extension<Arc<AllClients>>,
) -> error::JsonResult<Vec<String>> {
async fn list_logins() -> error::JsonResult<Vec<String>> {
Ok(Json(
clients
OAUTH_CLIENTS
.logins
.keys()
.map(|x| x.to_owned())
@@ -391,11 +384,9 @@ struct ScopesAndParams {
scopes: Vec<String>,
extra_params: Option<HashMap<String, String>>,
}
async fn list_connects(
Extension(clients): Extension<Arc<AllClients>>,
) -> error::JsonResult<HashMap<String, ScopesAndParams>> {
async fn list_connects() -> error::JsonResult<HashMap<String, ScopesAndParams>> {
Ok(Json(
(&clients.connects)
(&OAUTH_CLIENTS.connects)
.into_iter()
.map(|(k, v)| {
(
@@ -410,12 +401,8 @@ async fn list_connects(
))
}
async fn connect_slack(
Extension(clients): Extension<Arc<AllClients>>,
Extension(is_secure): Extension<Arc<IsSecure>>,
cookies: Cookies,
) -> error::Result<Redirect> {
let mut client = clients
async fn connect_slack(cookies: Cookies) -> error::Result<Redirect> {
let mut client = OAUTH_CLIENTS
.slack
.as_ref()
.ok_or_else(|| error::Error::BadRequest("slack client not setup".to_string()))?
@@ -426,7 +413,7 @@ async fn connect_slack(
client.add_scope("commands");
let url = client.authorize_url(&state);
set_cookie(&state, cookies, is_secure.0);
set_cookie(&state, cookies, *IS_SECURE);
Ok(Redirect::to(url.as_str()))
}
@@ -468,14 +455,9 @@ async fn disconnect_slack(
Ok(format!("slack disconnected"))
}
async fn login(
Extension(clients): Extension<Arc<AllClients>>,
Extension(is_secure): Extension<Arc<IsSecure>>,
Path(client_name): Path<String>,
cookies: Cookies,
) -> error::Result<Redirect> {
let clients = &clients.logins;
oauth_redirect(clients, client_name, cookies, None, None, is_secure.0)
async fn login(Path(client_name): Path<String>, cookies: Cookies) -> error::Result<Redirect> {
let clients = &OAUTH_CLIENTS.logins;
oauth_redirect(clients, client_name, cookies, None, None, *IS_SECURE)
}
#[derive(Deserialize)]
@@ -485,14 +467,12 @@ struct VariablePath {
async fn refresh_token(
authed: Authed,
Path((w_id, id)): Path<(String, i32)>,
Json(VariablePath { path }): Json<VariablePath>,
Extension(user_db): Extension<UserDB>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
Json(VariablePath { path }): Json<VariablePath>,
) -> error::Result<String> {
let tx = user_db.begin(&authed).await?;
_refresh_token(tx, &path, w_id, id, clients, http_client).await?;
_refresh_token(tx, &path, w_id, id).await?;
Ok(format!("Token at path {path} refreshed"))
}
@@ -502,8 +482,6 @@ pub async fn _refresh_token<'c>(
path: &str,
w_id: String,
id: i32,
clients: Arc<AllClients>,
http_client: Client,
) -> error::Result<String> {
let account = sqlx::query!(
"SELECT client, refresh_token FROM account WHERE workspace_id = $1 AND id = $2",
@@ -513,14 +491,14 @@ pub async fn _refresh_token<'c>(
.fetch_optional(&mut tx)
.await?;
let account = not_found_if_none(account, "Account", &id.to_string())?;
let client = (&clients
let client = (&OAUTH_CLIENTS
.connects
.get(&account.client)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?
.client)
.to_owned();
let token = _exchange_token(client, &account.refresh_token, http_client).await;
let token = _exchange_token(client, &account.refresh_token).await;
if let Err(token_err) = token {
sqlx::query!(
@@ -578,14 +556,10 @@ pub async fn _refresh_token<'c>(
Ok(token_str)
}
async fn _exchange_token(
client: OClient,
refresh_token: &str,
http_client: Client,
) -> Result<TokenResponse, Error> {
async fn _exchange_token(client: OClient, refresh_token: &str) -> Result<TokenResponse, Error> {
let token_json = client
.exchange_refresh_token(&RefreshToken::from(refresh_token.clone()))
.with_client(&http_client)
.with_client(&HTTP_CLIENT)
.execute::<serde_json::Value>()
.await
.map_err(to_anyhow)?;
@@ -607,10 +581,8 @@ async fn connect_callback(
cookies: Cookies,
Path(client_name): Path<String>,
Json(callback): Json<OAuthCallback>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
) -> error::JsonResult<TokenResponse> {
let client_w_scopes = &clients
let client_w_scopes = OAUTH_CLIENTS
.connects
.get(&client_name)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?;
@@ -618,7 +590,7 @@ async fn connect_callback(
let client = client_w_scopes.client.to_owned();
let extra_params = client_w_scopes.extra_params_callback.clone();
let token_response =
exchange_code::<TokenResponse>(callback, &cookies, client, &http_client, extra_params)
exchange_code::<TokenResponse>(callback, &cookies, client, &HTTP_CLIENT, extra_params)
.await?;
Ok(Json(token_response))
@@ -628,18 +600,16 @@ async fn connect_slack_callback(
Path(w_id): Path<String>,
authed: Authed,
cookies: Cookies,
Json(callback): Json<OAuthCallback>,
Extension(user_db): Extension<UserDB>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
Json(callback): Json<OAuthCallback>,
) -> error::Result<String> {
let client = clients
let client = OAUTH_CLIENTS
.slack
.as_ref()
.ok_or_else(|| error::Error::BadRequest("slack client not setup".to_string()))?
.to_owned();
let token =
exchange_code::<SlackTokenResponse>(callback, &cookies, client, &http_client, None).await?;
exchange_code::<SlackTokenResponse>(callback, &cookies, client, &HTTP_CLIENT, None).await?;
let mut tx = user_db.begin(&authed).await?;
@@ -655,20 +625,31 @@ async fn connect_slack_callback(
)
.execute(&mut tx)
.await?;
sqlx::query!(
"INSERT INTO group_
(workspace_id, name, summary)
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
&w_id,
sqlx::query_as!(
Group,
"INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
w_id,
"slack",
"The group that runs the script triggered by the slack /windmill command.
Share scripts to this group to make them executable from slack and add
members to this group to let them manage the slack related owner space."
"The group slack commands act on belhalf of",
serde_json::json!({username_to_permissioned_as(&authed.username): true})
)
.execute(&mut tx)
.await?;
let token_path = "g/slack/bot_token";
sqlx::query!(
"INSERT INTO folder
(workspace_id, name, display_name, owners, extra_perms)
VALUES ($1, $2, $3, $4, $5) ON CONFLICT DO NOTHING",
&w_id,
"slack_bot",
"Slack bot",
&["g/slack".to_string()],
serde_json::json!({"g/slack": true})
)
.execute(&mut tx)
.await?;
let token_path = "f/slack_bot/bot_token";
let mc = build_crypt(&mut tx, &w_id).await?;
let value = encrypt(&mc, &token.bot.bot_access_token);
sqlx::query!(
@@ -718,14 +699,17 @@ pub struct SlackSig {
}
#[async_trait]
impl<B> FromRequest<B> for SlackSig
impl<S> FromRequestParts<S> for SlackSig
where
B: Send,
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> std::result::Result<Self, Self::Rejection> {
let hm = req.headers();
async fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> std::result::Result<Self, Self::Rejection> {
let hm = &parts.headers;
Ok(Self {
sig: hm
.get("X-Slack-Signature")
@@ -743,16 +727,14 @@ where
async fn slack_command(
SlackSig { sig, ts }: SlackSig,
Extension(slack_verifier): Extension<Arc<Option<SlackVerifier>>>,
Extension(db): Extension<DB>,
Extension(base_url): Extension<Arc<BaseUrl>>,
body: Bytes,
) -> error::Result<String> {
let form: SlackCommand = serde_urlencoded::from_bytes(&body)
.map_err(|_| error::Error::BadRequest("invalid payload".to_string()))?;
let body = String::from_utf8_lossy(&body);
if slack_verifier
if SLACK_SIGNING_SECRET
.as_ref()
.as_ref()
.map(|sv| sv.verify(&ts, &body, &sig).ok())
@@ -810,7 +792,7 @@ async fn slack_command(
)
.await?;
tx.commit().await?;
let url = base_url.0.to_owned();
let url = BASE_URL.to_owned();
return Ok(format!(
"Job launched. See details at {url}/run/{uuid}?workspace={}",
&settings.workspace_id
@@ -834,32 +816,28 @@ pub struct UserInfo {
async fn login_callback(
Path(client_name): Path<String>,
Json(callback): Json<OAuthCallback>,
cookies: Cookies,
Extension(clients): Extension<Arc<AllClients>>,
Extension(db): Extension<DB>,
Extension(http_client): Extension<Client>,
Extension(is_secure): Extension<Arc<IsSecure>>,
Extension(cookie_domain): Extension<Arc<CookieDomain>>,
Json(callback): Json<OAuthCallback>,
) -> error::Result<String> {
let client_w_config = &clients
let client_w_config = &OAUTH_CLIENTS
.logins
.get(&client_name)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?;
let client = client_w_config.client.to_owned();
let token_res =
exchange_code::<TokenResponse>(callback, &cookies, client, &http_client, None).await;
exchange_code::<TokenResponse>(callback, &cookies, client, &HTTP_CLIENT, None).await;
if let Ok(token) = token_res {
let token = &token.access_token.to_string();
let userinfo_url = client_w_config.userinfo_url.as_ref().ok_or_else(|| {
Error::BadConfig(format!("Missing userinfo_url in client {client_name}"))
})?;
let user = http_get_user_info::<UserInfo>(&http_client, userinfo_url, token).await?;
let user = http_get_user_info::<UserInfo>(&HTTP_CLIENT, userinfo_url, token).await?;
let email = match client_name.as_str() {
"github" => http_get_user_info::<Vec<GHEmailInfo>>(
&http_client,
&HTTP_CLIENT,
"https://api.github.com/user/emails",
token,
)
@@ -895,15 +873,7 @@ async fn login_callback(
if let Some((email, login_type, super_admin)) = login {
let login_type = serde_json::json!(login_type);
if login_type == client_name {
crate::users::create_session_token(
&email,
super_admin,
&mut tx,
cookies,
is_secure.0,
&cookie_domain.as_ref().0,
)
.await?;
crate::users::create_session_token(&email, super_admin, &mut tx, cookies).await?;
} else {
return Err(error::Error::BadRequest(format!(
"an user with the email associated to this login exists but with a different \
@@ -938,15 +908,7 @@ async fn login_callback(
tx.commit().await?;
invite_user_to_all_auto_invite_worspaces(&db, &email).await?;
tx = db.begin().await?;
crate::users::create_session_token(
&email,
false,
&mut tx,
cookies,
is_secure.0,
&cookie_domain.as_ref().0,
)
.await?;
crate::users::create_session_token(&email, false, &mut tx, cookies).await?;
audit_log(
&mut tx,
&email,
@@ -957,6 +919,7 @@ async fn login_callback(
Some([("method", &client_name[..])].into()),
)
.await?;
let demo_exists =
sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM workspace WHERE id = 'demo')")
.fetch_one(&mut tx)
@@ -978,6 +941,16 @@ async fn login_callback(
}
}
tx.commit().await?;
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
let _ = HTTP_CLIENT
.post(&new_user_webhook)
.json(&serde_json::json!({"email" : &email, "event": "oauth_signup"}))
.send()
.await
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
}
Ok("Successfully logged in".to_string())
} else {
Err(error::Error::BadRequest(format!(
@@ -1099,7 +1072,7 @@ fn set_cookie(state: &State, cookies: Cookies, is_secure: bool) {
let csrf = state.to_base64();
let mut cookie = Cookie::new("csrf", csrf);
cookie.set_secure(is_secure);
cookie.set_same_site(cookie::SameSite::Lax);
cookie.set_same_site(Some(cookie::SameSite::Lax));
cookie.set_http_only(true);
cookie.set_path("/");
cookies.add(cookie);

View File

@@ -9,6 +9,7 @@
use crate::{
db::{UserDB, DB},
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
extract::{Extension, Path, Query},
@@ -141,7 +142,7 @@ async fn list_resources(
.join("account")
.on("variable.account = account.id AND account.workspace_id = variable.workspace_id")
.order_by("path", true)
.and_where("resource.workspace_id = ? OR resource.workspace_id = 'starter'".bind(&w_id))
.and_where("resource.workspace_id = ?".bind(&w_id))
.offset(offset)
.limit(per_page)
.clone();
@@ -186,7 +187,7 @@ async fn get_resource(
FROM resource
LEFT JOIN variable ON variable.path = resource.path AND variable.workspace_id = resource.workspace_id
LEFT JOIN account ON variable.account = account.id AND account.workspace_id = resource.workspace_id
WHERE resource.path = $1 AND (resource.workspace_id = $2 OR resource.workspace_id = 'starter')",
WHERE resource.path = $1 AND resource.workspace_id = $2",
path.to_owned(),
&w_id
)
@@ -225,8 +226,7 @@ async fn get_resource_value(
let mut tx = user_db.begin(&authed).await?;
let value_o = sqlx::query_scalar!(
"SELECT value from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = \
'starter')",
"SELECT value from resource WHERE path = $1 AND workspace_id = $2",
path.to_owned(),
&w_id
)
@@ -263,6 +263,7 @@ async fn check_path_conflict<'c>(
async fn create_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(resource): Json<CreateResource>,
) -> Result<(StatusCode, String)> {
@@ -293,6 +294,11 @@ async fn create_resource(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateResource { workspace: w_id, path: resource.path.clone() },
);
Ok((
StatusCode::CREATED,
format!("resource {} created", resource.path),
@@ -302,6 +308,7 @@ async fn create_resource(
async fn delete_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -333,12 +340,18 @@ async fn delete_resource(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteResource { workspace: w_id, path: path.to_owned() },
);
Ok(format!("resource {} deleted", path))
}
async fn update_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditResource>,
@@ -400,6 +413,15 @@ async fn update_resource(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResource {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("resource {} updated (npath: {:?})", path, npath))
}
@@ -411,6 +433,7 @@ struct UpdateResource {
async fn update_resource_value(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(nv): Json<UpdateResource>,
) -> Result<String> {
@@ -436,6 +459,14 @@ async fn update_resource_value(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResource {
workspace: w_id,
old_path: path.to_owned(),
new_path: path.to_owned(),
},
);
Ok(format!("value of resource {} updated", path))
}
@@ -446,7 +477,7 @@ async fn list_resource_types(
) -> JsonResult<Vec<ResourceType>> {
let rows = sqlx::query_as!(
ResourceType,
"SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter' OR workspace_id = 'admins') ORDER \
"SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') ORDER \
BY name",
&w_id
)
@@ -461,7 +492,7 @@ async fn list_resource_types_names(
Path(w_id): Path<String>,
) -> JsonResult<Vec<String>> {
let rows = sqlx::query_scalar!(
"SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter' OR workspace_id = 'admins') \
"SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') \
ORDER BY name",
&w_id
)
@@ -480,8 +511,7 @@ async fn get_resource_type(
let resource_type_o = sqlx::query_as!(
ResourceType,
"SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = \
'starter' OR workspace_id = 'admins')",
"SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins')",
&name,
&w_id
)
@@ -498,8 +528,7 @@ async fn exists_resource_type(
Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<bool> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = \
'starter' OR workspace_id = 'admins'))",
"SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins'))",
name,
w_id
)
@@ -513,6 +542,7 @@ async fn exists_resource_type(
async fn create_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(resource_type): Json<CreateResourceType>,
) -> Result<(StatusCode, String)> {
@@ -543,6 +573,11 @@ async fn create_resource_type(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateResourceType { name: resource_type.name.clone() },
);
Ok((
StatusCode::CREATED,
format!("resource_type {} created", resource_type.name),
@@ -574,6 +609,7 @@ async fn check_rt_path_conflict<'c>(
async fn delete_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
require_admin(authed.is_admin, &authed.username)?;
@@ -598,6 +634,10 @@ async fn delete_resource_type(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteResourceType { name: name.clone() },
);
Ok(format!("resource_type {} deleted", name))
}
@@ -605,6 +645,7 @@ async fn delete_resource_type(
async fn update_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(ns): Json<EditResourceType>,
) -> Result<String> {
@@ -634,6 +675,10 @@ async fn update_resource_type(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResourceType { name: name.clone() },
);
Ok(format!("resource_type {} updated", name))
}

View File

@@ -383,7 +383,7 @@ pub struct PreviewPayload {
}
fn get_offset(offset: Option<i32>) -> FixedOffset {
FixedOffset::west(offset.unwrap_or(0) * 60)
FixedOffset::west_opt(offset.unwrap_or(0) * 60).expect("Invalid offset")
}
#[derive(Deserialize)]

View File

@@ -6,7 +6,6 @@
* LICENSE-AGPL for a copy of the license.
*/
use reqwest::Client;
use sql_builder::prelude::*;
use windmill_audit::{audit_log, ActionKind};
@@ -14,6 +13,8 @@ use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
use axum::{
extract::{Extension, Path, Query},
@@ -36,7 +37,7 @@ use windmill_common::{
to_i64, HubScript, ListScriptQuery, ListableScript, NewScript, Script, ScriptHash,
ScriptKind, ScriptLang,
},
users::owner_to_token_owner,
users::username_to_permissioned_as,
utils::{
list_elems_from_hub, not_found_if_none, paginate, require_admin, Pagination, StripPath,
},
@@ -72,6 +73,7 @@ pub fn workspaced_service() -> Router {
.route("/get/h/:hash", get(get_script_by_hash))
.route("/raw/h/:hash", get(raw_script_by_hash))
.route("/deployment_status/h/:hash", get(get_deployment_status))
.route("/list_paths", get(list_paths))
}
async fn list_scripts(
authed: Authed,
@@ -109,7 +111,7 @@ async fn list_scripts(
)
.order_desc("favorite.path IS NOT NULL")
.order_by("created_at", lq.order_desc.unwrap_or(true))
.and_where("o.workspace_id = ? OR o.workspace_id = 'starter'".bind(&w_id))
.and_where("o.workspace_id = ?".bind(&w_id))
.offset(offset)
.limit(per_page)
.clone();
@@ -118,7 +120,7 @@ async fn list_scripts(
sqlb.and_where_eq(
"created_at",
"(select max(created_at) from script where o.path = path
AND (workspace_id = ? OR workspace_id = 'starter'))"
AND workspace_id = ?)"
.bind(&w_id),
);
} else {
@@ -161,12 +163,9 @@ async fn list_scripts(
Ok(Json(rows))
}
async fn list_hub_scripts(
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
async fn list_hub_scripts(Authed { email, .. }: Authed) -> JsonResult<serde_json::Value> {
let asks = list_elems_from_hub(
http_client,
&HTTP_CLIENT,
"https://hub.windmill.dev/searchData?approved=true",
&email,
)
@@ -183,6 +182,7 @@ fn hash_script(ns: &NewScript) -> i64 {
async fn create_script(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(ns): Json<NewScript>,
@@ -373,7 +373,7 @@ async fn create_script(
serde_json::Map::new(),
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
username_to_permissioned_as(&authed.username),
None,
None,
None,
@@ -399,6 +399,14 @@ async fn create_script(
Some([("hash", hash.to_string().as_str())].into()),
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateScript {
workspace: w_id,
path: ns.path.clone(),
hash: hash.to_string(),
},
);
} else {
audit_log(
&mut tx,
@@ -416,6 +424,14 @@ async fn create_script(
),
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateScript {
workspace: w_id,
path: ns.path.clone(),
hash: hash.to_string(),
},
);
}
tx.commit().await?;
@@ -423,21 +439,16 @@ async fn create_script(
Ok((StatusCode::CREATED, format!("{}", hash)))
}
pub async fn get_hub_script_by_path(
authed: Authed,
Path(path): Path<StripPath>,
Extension(http_client): Extension<Client>,
) -> Result<String> {
windmill_common::scripts::get_hub_script_by_path(&authed.email, path, http_client).await
pub async fn get_hub_script_by_path(authed: Authed, Path(path): Path<StripPath>) -> Result<String> {
windmill_common::scripts::get_hub_script_by_path(&authed.email, path, &HTTP_CLIENT).await
}
pub async fn get_full_hub_script_by_path(
Authed { email, .. }: Authed,
Path(path): Path<StripPath>,
Extension(http_client): Extension<Client>,
) -> JsonResult<HubScript> {
Ok(Json(
windmill_common::scripts::get_full_hub_script_by_path(&email, path, http_client).await?,
windmill_common::scripts::get_full_hub_script_by_path(&email, path, &HTTP_CLIENT).await?,
))
}
@@ -450,9 +461,9 @@ async fn get_script_by_path(
let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, Script>(
"SELECT * FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') \
"SELECT * 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 OR workspace_id = 'starter'))",
workspace_id = $2)",
)
.bind(path)
.bind(w_id)
@@ -464,23 +475,39 @@ async fn get_script_by_path(
Ok(Json(script))
}
async fn list_paths(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
) -> JsonResult<Vec<String>> {
let mut tx = user_db.begin(&authed).await?;
let scripts = sqlx::query_scalar!(
"SELECT distinct(path) FROM script WHERE workspace_id = $1",
w_id
)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(scripts))
}
async fn raw_script_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path
.to_path()
.strip_suffix(".ts")
.ok_or_else(|| Error::BadRequest("Raw script path must end with .ts".to_string()))?;
let path = path.to_path().split(".").next().unwrap_or_default();
let mut tx = user_db.begin(&authed).await?;
let content_o = sqlx::query_scalar!(
"SELECT content FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') \
"SELECT content FROM script WHERE path = $1 AND workspace_id = $2 \
AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND archived = false AND \
(workspace_id = $2 OR workspace_id = 'starter'))",
path, w_id
workspace_id = $2)",
path,
w_id
)
.fetch_optional(&mut tx)
.await?;
@@ -497,10 +524,8 @@ async fn exists_script_by_path(
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND (workspace_id = $2 OR \
workspace_id = 'starter') AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 \
OR workspace_id = 'starter')))",
"SELECT EXISTS(SELECT 1 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))",
path,
w_id
)
@@ -516,13 +541,12 @@ async fn get_script_by_hash_internal<'c>(
workspace_id: &str,
hash: &ScriptHash,
) -> Result<Script> {
let script_o = sqlx::query_as::<_, Script>(
"SELECT * FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
)
.bind(hash)
.bind(workspace_id)
.fetch_optional(db)
.await?;
let script_o =
sqlx::query_as::<_, Script>("SELECT * FROM script WHERE hash = $1 AND workspace_id = $2")
.bind(hash)
.bind(workspace_id)
.fetch_optional(db)
.await?;
let script = not_found_if_none(script_o, "Script", hash.to_string())?;
Ok(script)
@@ -568,8 +592,7 @@ async fn get_deployment_status(
let mut tx = user_db.begin(&authed).await?;
let status_o: Option<DeploymentStatus> = sqlx::query_as!(
DeploymentStatus,
"SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND (workspace_id = $2 OR \
workspace_id = 'starter')",
"SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND workspace_id = $2",
hash.0,
w_id,
)
@@ -584,6 +607,7 @@ async fn get_deployment_status(
async fn archive_script_by_path(
authed: Authed,
Extension(webhook): Extension<WebhookShared>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -610,6 +634,10 @@ async fn archive_script_by_path(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(())
}
@@ -617,6 +645,7 @@ async fn archive_script_by_path(
async fn archive_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
let mut tx = user_db.begin(&authed).await?;
@@ -641,12 +670,18 @@ async fn archive_script_by_hash(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(Json(script))
}
async fn delete_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
@@ -675,6 +710,11 @@ async fn delete_script_by_hash(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(Json(script))
}

View File

@@ -8,56 +8,42 @@
use axum::{
body::{self, BoxBody},
http::{header, response::Builder, Response, Uri},
extract::OriginalUri,
http::{header, Response},
response::IntoResponse,
Extension,
};
use crate::{CloudHosted, ContentSecurityPolicy, IsSecure};
use hyper::Uri;
use mime_guess::mime;
use rust_embed::RustEmbed;
use std::sync::Arc;
// static_handler is a handler that serves static files from the
pub async fn static_handler(
uri: Uri,
Extension(is_secure): Extension<Arc<IsSecure>>,
Extension(is_cloud_hosted): Extension<Arc<CloudHosted>>,
Extension(csp): Extension<Arc<ContentSecurityPolicy>>,
) -> impl IntoResponse {
let path = uri.path().trim_start_matches('/').to_string();
StaticFile(path, is_secure.0, is_cloud_hosted.0, csp)
pub async fn static_handler(OriginalUri(original_uri): OriginalUri) -> StaticFile {
StaticFile(original_uri)
}
#[derive(RustEmbed)]
#[folder = "../../frontend/build/"]
struct Asset;
pub struct StaticFile<T>(pub T, pub bool, pub bool, pub Arc<ContentSecurityPolicy>);
pub struct StaticFile(Uri);
impl<T> IntoResponse for StaticFile<T>
where
T: Into<String>,
{
impl IntoResponse for StaticFile {
fn into_response(self) -> Response<BoxBody> {
let path = self.0.into();
let can_set_security_headers = self.1 && self.2;
let csp = self.3;
serve_path(path, can_set_security_headers, csp)
let path = self.0.path().trim_start_matches('/');
serve_path(path)
}
}
fn serve_path(
path: String,
can_set_security_headers: bool,
csp: Arc<ContentSecurityPolicy>,
) -> Response<BoxBody> {
const TWO_HUNDRED: &str = "200.html";
fn serve_path(path: &str) -> Response<BoxBody> {
if path.starts_with("api/") {
return Response::builder()
.status(404)
.body(body::boxed(body::Empty::new()))
.unwrap();
}
match Asset::get(path.as_str()) {
match Asset::get(path) {
Some(content) => {
let body = body::boxed(body::Full::from(content.data));
let mime = mime_guess::from_path(path).first_or_octet_stream();
@@ -72,26 +58,12 @@ fn serve_path(
res = res.header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate");
}
if can_set_security_headers {
res = set_security_headers(res, csp);
}
res.body(body).unwrap()
}
None if path.as_str().starts_with("_app/") => Response::builder()
None if path.starts_with("_app/") => Response::builder()
.status(404)
.body(body::boxed(body::Empty::new()))
.unwrap(),
None => serve_path("200.html".to_owned(), can_set_security_headers, csp),
None => serve_path(TWO_HUNDRED),
}
}
fn set_security_headers(mut res: Builder, csp: Arc<ContentSecurityPolicy>) -> Builder {
res = res.header("X-Frame-Options", "DENY");
res = res.header("X-Content-Type-Options", "nosniff");
if !csp.0.is_empty() {
res = res.header("Content-Security-Policy", &csp.0);
}
res
}

View File

@@ -13,13 +13,13 @@ use crate::{
folders::get_folders_for_user,
utils::require_super_admin,
workspaces::invite_user_to_all_auto_invite_worspaces,
CookieDomain, IsSecure,
COOKIE_DOMAIN, HTTP_CLIENT, IS_SECURE,
};
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use axum::{
async_trait,
extract::{Extension, FromRequest, Path, Query, RequestParts},
http,
extract::{Extension, FromRequestParts, OriginalUri, Path, Query},
http::{self, request::Parts},
response::{IntoResponse, Response},
routing::{delete, get, post},
Json, Router,
@@ -177,7 +177,16 @@ impl AuthCache {
})
}
} else {
None
let groups = vec![];
let folders = vec![];
Some(Authed {
email: email
.unwrap_or_else(|| "missing@email.xyz".to_string()),
username: owner,
is_admin: super_admin,
groups,
folders,
})
}
}
(_, Some(email), super_admin) => {
@@ -275,16 +284,16 @@ impl AuthCache {
}
}
async fn extract_token<B: Send>(req: &mut RequestParts<B>) -> Option<String> {
let auth_header = req
.headers()
async fn extract_token<S: Send + Sync>(parts: &mut Parts, state: &S) -> Option<String> {
let auth_header = parts
.headers
.get(http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "));
let from_cookie = match auth_header {
Some(x) => Some(x.to_owned()),
None => Extension::<Cookies>::from_request(req)
None => Extension::<Cookies>::from_request_parts(parts, state)
.await
.ok()
.and_then(|cookies| cookies.get(COOKIE_NAME).map(|c| c.value().to_owned())),
@@ -296,7 +305,7 @@ async fn extract_token<B: Send>(req: &mut RequestParts<B>) -> Option<String> {
}
match from_cookie {
Some(token) => Some(token),
None => Query::<Token>::from_request(req)
None => Query::<Token>::from_request_parts(parts, state)
.await
.ok()
.and_then(|token| token.token.clone()),
@@ -309,21 +318,24 @@ pub struct Tokened {
}
#[async_trait]
impl<B> FromRequest<B> for Tokened
impl<S> FromRequestParts<S> for Tokened
where
B: Send,
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> std::result::Result<Self, Self::Rejection> {
let already_tokened = req.extensions().get::<Tokened>();
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
let already_tokened = parts.extensions.get::<Tokened>();
if let Some(tokened) = already_tokened {
Ok(tokened.clone())
} else {
let token_o = extract_token(req).await;
let token_o = extract_token(parts, state).await;
if let Some(token) = token_o {
let tokened = Self { token };
req.extensions_mut().insert(tokened.clone());
parts.extensions.insert(tokened.clone());
Ok(tokened)
} else {
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
@@ -342,33 +354,44 @@ pub struct Authed {
}
#[async_trait]
impl<B> FromRequest<B> for Authed
impl<S> FromRequestParts<S> for Authed
where
B: Send,
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> std::result::Result<Self, Self::Rejection> {
let already_authed = req.extensions().get::<Authed>();
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
let already_authed = parts.extensions.get::<Authed>();
if let Some(authed) = already_authed {
Ok(authed.clone())
} else {
let already_tokened = req.extensions().get::<Tokened>();
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
} else {
extract_token(req).await
extract_token(parts, state).await
};
let path_vec: Vec<&str> = req.uri().path().split("/").collect();
let workspace_id = if path_vec[0] == "" && path_vec[1] == "w" {
Some(path_vec[2].to_owned())
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else {
None
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) = Extension::<Arc<AuthCache>>::from_request(req).await {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
{
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
req.extensions_mut().insert(authed.clone());
parts.extensions.insert(authed.clone());
Span::current().record("username", &authed.username.as_str());
Span::current().record("email", &authed.email);
@@ -388,14 +411,17 @@ where
pub struct OptAuthed(pub Option<Authed>);
#[async_trait]
impl<B> FromRequest<B> for OptAuthed
impl<S> FromRequestParts<S> for OptAuthed
where
B: Send,
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> std::result::Result<Self, Self::Rejection> {
Authed::from_request(req)
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
Authed::from_request_parts(parts, state)
.await
.map(|authed| Self(Some(authed)))
.or_else(|_| Ok(Self(None)))
@@ -694,14 +720,12 @@ async fn logout(
Tokened { token }: Tokened,
cookies: Cookies,
Extension(db): Extension<DB>,
Extension(cookie_domain): Extension<Arc<CookieDomain>>,
Query(LogoutQuery { rd }): Query<LogoutQuery>,
) -> Result<Response> {
let mut cookie = Cookie::new(COOKIE_NAME, "");
cookie.set_path(COOKIE_PATH);
let domain = cookie_domain.0.clone();
if domain.is_some() {
cookie.set_domain(domain.clone().unwrap());
if COOKIE_DOMAIN.is_some() {
cookie.set_domain(COOKIE_DOMAIN.clone().unwrap());
}
cookies.remove(cookie);
let mut tx = db.begin().await?;
@@ -1244,6 +1268,10 @@ async fn delete_user(
Ok(format!("email {} deleted", &email_to_delete))
}
lazy_static::lazy_static! {
pub static ref NEW_USER_WEBHOOK: Option<String> = std::env::var("NEW_USER_WEBHOOK").ok();
}
async fn create_user(
Authed { email, .. }: Authed,
Extension(db): Extension<DB>,
@@ -1279,6 +1307,16 @@ async fn create_user(
)
.await?;
tx.commit().await?;
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
let _ = HTTP_CLIENT
.post(&new_user_webhook)
.json(&serde_json::json!({"email" : &nu.email, "name": &nu.name, "event": "new_user"}))
.send()
.await
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
}
invite_user_to_all_auto_invite_worspaces(&db, &nu.email).await?;
Ok((StatusCode::CREATED, format!("email {} created", nu.email)))
@@ -1523,21 +1561,19 @@ async fn login(
cookies: Cookies,
Extension(db): Extension<DB>,
Extension(argon2): Extension<Arc<Argon2<'_>>>,
Extension(is_secure): Extension<Arc<IsSecure>>,
Extension(cookie_domain): Extension<Arc<CookieDomain>>,
Json(Login { email, password }): Json<Login>,
) -> Result<String> {
let mut tx = db.begin().await?;
let email_w_h: Option<(String, String, bool)> = sqlx::query_as(
"SELECT email, password_hash, super_admin FROM password WHERE email = $1 AND login_type = \
let email_w_h: Option<(String, String, bool, bool)> = sqlx::query_as(
"SELECT email, password_hash, super_admin, first_time_user FROM password WHERE email = $1 AND login_type = \
'password'",
)
.bind(&email)
.fetch_optional(&mut tx)
.await?;
if let Some((email, hash, super_admin)) = email_w_h {
if let Some((email, hash, super_admin, first_time_user)) = email_w_h {
let parsed_hash =
PasswordHash::new(&hash).map_err(|e| Error::InternalErr(e.to_string()))?;
if argon2
@@ -1546,15 +1582,27 @@ async fn login(
{
Err(Error::BadRequest("Invalid login".to_string()))
} else {
let token = create_session_token(
&email,
super_admin,
&mut tx,
cookies,
is_secure.0,
&cookie_domain.as_ref().0,
)
.await?;
if first_time_user {
sqlx::query_scalar!(
"UPDATE password SET first_time_user = false WHERE email = $1",
&email
)
.execute(&mut tx)
.await?;
let mut c = Cookie::new("first_time", "1");
if let Some(domain) = COOKIE_DOMAIN.as_ref() {
c.set_domain(domain);
}
c.set_secure(false);
c.set_expires(time::OffsetDateTime::now_utc() + time::Duration::minutes(15));
c.set_http_only(false);
c.set_path("/");
cookies.add(c);
}
let token = create_session_token(&email, super_admin, &mut tx, cookies).await?;
tx.commit().await?;
Ok(token)
}
@@ -1568,8 +1616,6 @@ pub async fn create_session_token<'c>(
super_admin: bool,
tx: &mut sqlx::Transaction<'c, sqlx::Postgres>,
cookies: Cookies,
is_secure: bool,
domain: &Option<String>,
) -> Result<String> {
let token = rd_string(30);
sqlx::query!(
@@ -1585,12 +1631,12 @@ pub async fn create_session_token<'c>(
.execute(tx)
.await?;
let mut cookie = Cookie::new(COOKIE_NAME, token.clone());
cookie.set_secure(is_secure);
cookie.set_same_site(cookie::SameSite::Lax);
cookie.set_secure(*IS_SECURE);
cookie.set_same_site(Some(cookie::SameSite::Lax));
cookie.set_http_only(true);
cookie.set_path(COOKIE_PATH);
if domain.is_some() {
cookie.set_domain(domain.clone().unwrap());
if COOKIE_DOMAIN.is_some() {
cookie.set_domain(COOKIE_DOMAIN.clone().unwrap());
}
let mut expire: OffsetDateTime = time::OffsetDateTime::now_utc();
expire += time::Duration::days(3);

View File

@@ -6,13 +6,11 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::sync::Arc;
use crate::{
db::{UserDB, DB},
oauth2::{AllClients, _refresh_token},
oauth2::_refresh_token,
users::{require_owner_of_path, Authed},
BaseUrl,
webhook_util::{WebhookMessage, WebhookShared},
};
/*
* Author: Ruben Fiszel
@@ -36,7 +34,6 @@ use windmill_common::{
};
use magic_crypt::{MagicCrypt256, MagicCryptTrait};
use reqwest::Client;
use serde::Deserialize;
use sqlx::{Postgres, Transaction};
@@ -53,7 +50,6 @@ pub fn workspaced_service() -> Router {
async fn list_contextual_variables(
Path(w_id): Path<String>,
Extension(base_url): Extension<Arc<BaseUrl>>,
Authed { username, email, .. }: Authed,
) -> JsonResult<Vec<ContextualVariable>> {
Ok(Json(
@@ -64,7 +60,6 @@ async fn list_contextual_variables(
&username,
"017e0ad5-f499-73b6-5488-92a61c5196dd",
format!("u/{username}").as_str(),
&base_url.0,
Some("u/user/script_path".to_string()),
Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()),
Some("u/user/encapsulating_flow_path".to_string()),
@@ -90,7 +85,7 @@ async fn list_variables(
from variable
LEFT JOIN account ON variable.account = account.id AND account.workspace_id = variable.workspace_id
LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id
WHERE variable.workspace_id = $1 OR (is_secret IS NOT TRUE AND variable.workspace_id = 'starter') ORDER BY path",
WHERE variable.workspace_id = $1 ORDER BY path",
)
.bind(&w_id)
.fetch_all(&mut tx)
@@ -110,8 +105,6 @@ async fn get_variable(
Extension(user_db): Extension<UserDB>,
Query(q): Query<GetVariableQuery>,
Path((w_id, path)): Path<(String, StripPath)>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
) -> JsonResult<ListableVariable> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
@@ -123,8 +116,7 @@ async fn get_variable(
from variable
LEFT JOIN account ON variable.account = account.id
LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id
WHERE variable.path = $1 AND (variable.workspace_id = $2 OR (is_secret IS NOT TRUE AND \
variable.workspace_id = 'starter'))
WHERE variable.path = $1 AND variable.workspace_id = $2
LIMIT 1",
)
.bind(&path)
@@ -150,17 +142,7 @@ async fn get_variable(
let value = variable.value.unwrap_or_else(|| "".to_string());
ListableVariable {
value: if variable.is_expired.unwrap_or(false) && variable.account.is_some() {
Some(
_refresh_token(
tx,
&variable.path,
w_id,
variable.account.unwrap(),
clients,
http_client,
)
.await?,
)
Some(_refresh_token(tx, &variable.path, w_id, variable.account.unwrap()).await?)
} else if !value.is_empty() && decrypt_secret {
let mc = build_crypt(&mut tx, &w_id).await?;
tx.commit().await?;
@@ -224,13 +206,15 @@ async fn check_path_conflict<'c>(
async fn create_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Query(AlreadyEncrypted { already_encrypted }): Query<AlreadyEncrypted>,
Json(variable): Json<CreateVariable>,
) -> Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
check_path_conflict(&mut tx, &w_id, &variable.path).await?;
let value = if variable.is_secret {
let value = if variable.is_secret && !already_encrypted.unwrap_or(false) {
let mc = build_crypt(&mut tx, &w_id).await?;
encrypt(&mc, &variable.value)
} else {
@@ -265,6 +249,11 @@ async fn create_variable(
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateVariable { workspace: w_id, path: variable.path.clone() },
);
Ok((
StatusCode::CREATED,
format!("variable {} created", variable.path),
@@ -274,6 +263,7 @@ async fn create_variable(
async fn delete_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -306,6 +296,11 @@ async fn delete_variable(
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteVariable { workspace: w_id, path: path.to_owned() },
);
Ok(format!("variable {} deleted", path))
}
@@ -317,11 +312,18 @@ struct EditVariable {
description: Option<String>,
}
#[derive(Deserialize)]
struct AlreadyEncrypted {
already_encrypted: Option<bool>,
}
async fn update_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Query(AlreadyEncrypted { already_encrypted }): Query<AlreadyEncrypted>,
Json(ns): Json<EditVariable>,
) -> Result<String> {
use sql_builder::prelude::*;
@@ -347,7 +349,7 @@ async fn update_variable(
.await?
.unwrap_or(false);
let value = if is_secret {
let value = if is_secret && !already_encrypted.unwrap_or(false) {
let mc = build_crypt(&mut tx, &w_id).await?;
encrypt(&mc, &nvalue)
} else {
@@ -405,6 +407,15 @@ async fn update_variable(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateVariable {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("variable {} updated (npath: {:?})", path, npath))
}

View File

@@ -0,0 +1,113 @@
use std::time::Duration;
use serde::Serialize;
use tokio::{select, sync::mpsc, time::interval};
use crate::db::DB;
lazy_static::lazy_static! {
// TODO: these aren't synced, they should be moved into the queue abstraction once/if that happens.
static ref WEBHOOK_REQUEST_COUNT: prometheus::Histogram = prometheus::register_histogram!(
"webhook_request",
"Histogram of webhook requests made"
)
.unwrap();
}
#[derive(Serialize)]
#[serde(tag = "type")]
pub enum WebhookMessage {
// See https://serde.rs/enum-representations.html#internally-tagged for how this looks in JSON
CreateApp { workspace: String, path: String },
DeleteApp { workspace: String, path: String },
UpdateApp { workspace: String, old_path: String, new_path: String },
CreateFlow { workspace: String, path: String },
UpdateFlow { workspace: String, old_path: String, new_path: String },
ArchiveFlow { workspace: String, path: String },
DeleteFlow { workspace: String, path: String },
CreateFolder { workspace: String, name: String },
UpdateFolder { workspace: String, name: String },
DeleteFolder { workspace: String, name: String },
DeleteResource { workspace: String, path: String },
CreateResource { workspace: String, path: String },
UpdateResource { workspace: String, old_path: String, new_path: String },
CreateResourceType { name: String },
DeleteResourceType { name: String },
UpdateResourceType { name: String },
CreateScript { workspace: String, path: String, hash: String },
UpdateScript { workspace: String, path: String, hash: String },
DeleteScript { workspace: String, hash: String },
CreateVariable { workspace: String, path: String },
UpdateVariable { workspace: String, old_path: String, new_path: String },
DeleteVariable { workspace: String, path: String },
}
#[derive(Clone)]
pub struct WebhookShared {
pub channel: mpsc::UnboundedSender<(String, WebhookMessage)>,
}
impl WebhookShared {
pub fn new(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, db: DB) -> Self {
let (tx, mut rx) = mpsc::unbounded_channel::<(String, WebhookMessage)>();
let _process = tokio::spawn(async move {
let client = reqwest::Client::builder()
// TODO: investigate pool timeouts and such if TCP load is high
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let cache = retainer::Cache::new();
let mut cache_purge_interval = interval(Duration::from_secs(30));
loop {
select! {
biased;
_ = shutdown_rx.recv() => break,
r = rx.recv() => match r {
Some((workspace_id, message)) => {
let url_guard = match cache.get(&workspace_id).await {
Some(guard) => {
guard
},
None => {
let Ok(webook_opt) =
sqlx::query_scalar!(
"SELECT webhook FROM workspace_settings WHERE workspace_id = $1",
workspace_id
)
.fetch_one(
&db,
)
.await else {
tracing::error!("Webhook Message to send - but cannot get workspace settings! Workspace: {workspace_id}");
continue;
};
cache.insert(workspace_id.clone(), webook_opt, Duration::from_secs(30)).await;
cache.get(&workspace_id).await.unwrap()
}
};
let webook_opt = url_guard.value();
if let Some(url) = webook_opt {
let timer = WEBHOOK_REQUEST_COUNT.start_timer();
let _ = client.post(url).json(&message).send().await;
timer.stop_and_record();
drop(url_guard);
}
},
None => break,
},
_ = futures::future::poll_fn(|cx| cache_purge_interval.poll_tick(cx)) => {
tracing::trace!("Purging Webhook Cache");
cache.purge(10, 0.50).await;
},
}
}
});
Self { channel: tx }
}
pub fn send_message(&self, workspace_id: String, message: WebhookMessage) {
let _ = self.channel.send((workspace_id.clone(), message));
}
}

View File

@@ -6,13 +6,22 @@
* LICENSE-AGPL for a copy of the license.
*/
#[cfg(feature = "enterprise")]
use std::str::FromStr;
#[cfg(feature = "enterprise")]
use crate::BASE_URL;
use crate::{
apps::AppWithLastVersion,
db::{UserDB, DB},
folders::Folder,
resources::{Resource, ResourceType},
users::{Authed, WorkspaceInvite},
users::{Authed, WorkspaceInvite, NEW_USER_WEBHOOK},
utils::require_super_admin,
HTTP_CLIENT,
};
#[cfg(feature = "enterprise")]
use axum::response::Redirect;
use axum::{
body::StreamBody,
extract::{Extension, Path, Query},
@@ -21,9 +30,11 @@ use axum::{
routing::{delete, get, post},
Json, Router,
};
#[cfg(feature = "enterprise")]
use stripe::CustomerId;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
error::{to_anyhow, Error, JsonResult, Result},
flows::Flow,
scripts::{Schema, Script, ScriptLang},
utils::{paginate, rd_string, require_admin, Pagination},
@@ -38,17 +49,29 @@ use tokio::fs::File;
use tokio_util::io::ReaderStream;
pub fn workspaced_service() -> Router {
Router::new()
let router = Router::new()
.route("/list_pending_invites", get(list_pending_invites))
.route("/update", post(edit_workspace))
.route("/archive", post(archive_workspace))
.route("/invite_user", post(invite_user))
.route("/add_user", post(add_user))
.route("/delete_invite", post(delete_invite))
.route("/get_settings", get(get_settings))
.route("/edit_slack_command", post(edit_slack_command))
.route("/edit_webhook", post(edit_webhook))
.route("/edit_auto_invite", post(edit_auto_invite))
.route("/tarball", get(tarball_workspace))
.route("/premium_info", get(premium_info))
.route("/premium_info", get(premium_info));
#[cfg(feature = "enterprise")]
tracing::info!("stripe enabled");
#[cfg(feature = "enterprise")]
let router = router
.route("/checkout", get(stripe_checkout))
.route("/billing_portal", get(stripe_portal));
router
}
pub fn global_service() -> Router {
Router::new()
@@ -81,6 +104,9 @@ pub struct WorkspaceSettings {
pub slack_email: String,
pub auto_invite_domain: Option<String>,
pub auto_invite_operator: Option<bool>,
pub customer_id: Option<String>,
pub plan: Option<String>,
pub webhook: Option<String>,
}
#[derive(FromRow, Serialize, Debug)]
@@ -108,6 +134,11 @@ struct EditAutoInvite {
operator: Option<bool>,
}
#[derive(Deserialize)]
struct EditWebhook {
webhook: Option<String>,
}
#[derive(Deserialize)]
struct CreateWorkspace {
id: String,
@@ -152,6 +183,14 @@ pub struct NewWorkspaceInvite {
pub operator: bool,
}
#[derive(Deserialize)]
pub struct NewWorkspaceUser {
pub email: String,
pub username: String,
pub is_admin: bool,
pub operator: bool,
}
async fn list_pending_invites(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -192,6 +231,149 @@ async fn premium_info(
Ok(Json(row))
}
#[cfg(feature = "enterprise")]
#[derive(Deserialize)]
struct PlanQuery {
plan: String,
}
#[cfg(feature = "enterprise")]
async fn stripe_checkout(
authed: Authed,
Path(w_id): Path<String>,
Query(plan): Query<PlanQuery>,
) -> Result<Redirect> {
// #[cfg(feature = "enterprise")]
{
require_admin(authed.is_admin, &authed.username)?;
let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY"));
let success_rd = format!("{}/workspace_settings/checkout?success=true", *BASE_URL);
let failure_rd = format!("{}/workspace_settings/checkout?success=false", *BASE_URL);
let checkout_session = {
let mut params = stripe::CreateCheckoutSession::new(&failure_rd, &success_rd);
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
params.line_items = match plan.plan.as_str() {
"team" => Some(vec![
stripe::CreateCheckoutSessionLineItems {
quantity: Some(1),
price: Some("price_1MUlrWGU3NdFi9eLE9GBZhoY".to_string()),
..Default::default()
},
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MUlreGU3NdFi9eLi6sOyvVa".to_string()),
..Default::default()
},
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MUlrlGU3NdFi9eLFLggSXZV".to_string()),
..Default::default()
},
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MUlr3GU3NdFi9eLbZYFjR9p".to_string()),
..Default::default()
},
]),
// "enterprise" => Some(vec![
// stripe::CreateCheckoutSessionLineItems {
// quantity: None,
// price: Some("price_1MSdf6GU3NdFi9eLJFRkntlx".to_string()),
// ..Default::default()
// },
// stripe::CreateCheckoutSessionLineItems {
// quantity: None,
// price: Some("price_1MShsNGU3NdFi9eLJMEZUW8b".to_string()),
// ..Default::default()
// },
// ]),
_ => Err(Error::BadRequest("invalid plan".to_string()))?,
};
params.customer_email = Some(&authed.email);
params.client_reference_id = Some(&w_id);
stripe::CheckoutSession::create(&client, params)
.await
.unwrap()
};
let uri = checkout_session
.url
.ok_or_else(|| Error::InternalErr(format!("stripe checkout redirect issue")))?;
Ok(Redirect::to(&uri))
}
}
#[cfg(feature = "enterprise")]
async fn stripe_portal(
authed: Authed,
Path(w_id): Path<String>,
Extension(db): Extension<DB>,
) -> Result<Redirect> {
require_admin(authed.is_admin, &authed.username)?;
let customer_id = sqlx::query_scalar!(
"SELECT customer_id FROM workspace_settings WHERE workspace_id = $1",
w_id
)
.fetch_one(&db)
.await?
.ok_or_else(|| Error::InternalErr(format!("no customer id for workspace {}", w_id)))?;
let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY"));
let success_rd = format!("{}/workspace_settings?tab=premium", *BASE_URL);
let portal_session = {
let customer_id = CustomerId::from_str(&customer_id).unwrap();
let mut params = stripe::CreateBillingPortalSession::new(customer_id);
params.return_url = Some(&success_rd);
stripe::BillingPortalSession::create(&client, params)
.await
.map_err(to_anyhow)?
};
Ok(Redirect::to(&portal_session.url))
}
// async fn stripe_usage(
// authed: Authed,
// Path(w_id): Path<String>,
// Extension(db): Extension<DB>,
// Extension(base_url): Extension<Arc<BaseUrl>>,
// ) -> Result<Redirect> {
// require_admin(authed.is_admin, &authed.username)?;
// let customer_id = sqlx::query_scalar!(
// "SELECT customer_id FROM workspace_settings WHERE workspace_id = $1",
// w_id
// )
// .fetch_one(&db)
// .await?
// .ok_or_else(|| Error::InternalErr(format!("no customer id for workspace {}", w_id)))?;
// let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY"));
// let success_rd = format!("{}/workspace_settings?tab=premium", base_url.0);
// let portal_session = {
// let customer_id = CustomerId::from_str(&customer_id).unwrap();
// let subscriptions = stripe::Subscription::list(
// &client,
// stripe::ListSubscriptions { customer: Some(customer_id), ..Default::default() },
// )
// .await
// .map_err(to_anyhow)?
// .data[0];
// let getUsage =
// stripe::SubscriptionItem::list(
// &client,
// stripe::ListSubscriptionItems {
// subscription: subscription.id,
// ..Default::default()
// },
// )
// .await
// .map_err(to_anyhow)
// };
// let mut params = stripe::ListSubscriptionItems::new(customer_id);
// params.return_url = Some(&success_rd);
// stripe::BillingPortalSession::create(&client, params)
// .await
// .map_err(to_anyhow)?
// };
// }
async fn exists_workspace(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -354,7 +536,52 @@ async fn edit_auto_invite(
.await?;
tx.commit().await?;
Ok(format!("Edit command script {}", &w_id))
Ok(format!(
"Edit auto-invite for workspace {} to {}",
&w_id, domain
))
}
async fn edit_webhook(
authed: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { is_admin, username, .. }: Authed,
Json(ew): Json<EditWebhook>,
) -> Result<String> {
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
if let Some(webhook) = &ew.webhook {
sqlx::query!(
"UPDATE workspace_settings SET webhook = $1 WHERE workspace_id = $2",
webhook,
&w_id
)
.execute(&mut tx)
.await?;
} else {
sqlx::query!(
"UPDATE workspace_settings SET webhook = NULL WHERE workspace_id = $1",
&w_id,
)
.execute(&mut tx)
.await?;
}
audit_log(
&mut tx,
&authed.username,
"workspaces.edit_webhook",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("webhook", &format!("{:?}", ew.webhook)[..])].into()),
)
.await?;
tx.commit().await?;
Ok(format!("Edit webhook for workspace {}", &w_id))
}
async fn list_workspaces_as_super_admin(
@@ -450,19 +677,19 @@ async fn create_workspace(
.execute(&mut tx)
.await?;
let mc = magic_crypt::new_magic_crypt!(key, 256);
sqlx::query!(
"INSERT INTO variable
(workspace_id, path, value, is_secret, description)
VALUES ($1, 'g/all/pretty_secret', $2, true, 'This item is secret'),
($3, 'g/all/not_secret', $4, false, 'This item is not secret')",
nw.id,
crate::variables::encrypt(&mc, "pretty secret value"),
nw.id,
"finland does not actually exist",
)
.execute(&mut tx)
.await?;
// let mc = magic_crypt::new_magic_crypt!(key, 256);
// sqlx::query!(
// "INSERT INTO variable
// (workspace_id, path, value, is_secret, description)
// VALUES ($1, 'g/all/pretty_secret', $2, true, 'This item is secret'),
// ($3, 'g/all/not_secret', $4, false, 'This item is not secret')",
// nw.id,
// crate::variables::encrypt(&mc, "pretty secret value"),
// nw.id,
// "finland does not actually exist",
// )
// .execute(&mut tx)
// .await?;
sqlx::query!(
"INSERT INTO usr
@@ -735,12 +962,52 @@ async fn invite_user(
tx.commit().await?;
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
let _ = &HTTP_CLIENT
.post(&new_user_webhook)
.json(&serde_json::json!({"email" : &nu.email, "event": "new_invite"}))
.send()
.await
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
}
Ok((
StatusCode::CREATED,
format!("user with email {} invited", nu.email),
))
}
async fn add_user(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(nu): Json<NewWorkspaceUser>,
) -> Result<(StatusCode, String)> {
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
sqlx::query!(
"INSERT INTO usr
(workspace_id, email, username, is_admin, operator)
VALUES ($1, $2, $3, $4, $5)",
&w_id,
nu.email,
nu.username,
nu.is_admin,
nu.operator
)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok((
StatusCode::CREATED,
format!("user with email {} added", nu.email),
))
}
async fn delete_invite(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
@@ -799,20 +1066,118 @@ struct ScriptMetadata {
lock: Vec<String>,
}
enum ArchiveImpl {
Zip(async_zip::write::ZipFileWriter<File>),
Tar(tokio_tar::Builder<File>),
}
impl ArchiveImpl {
async fn write_to_archive(&mut self, content: &str, path: &str) -> Result<()> {
match self {
ArchiveImpl::Tar(t) => {
let bytes = content.as_bytes();
let mut header = tokio_tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mtime(0);
header.set_uid(0);
header.set_gid(0);
header.set_mode(0o777);
header.set_cksum();
t.append_data(&mut header, path, bytes).await?;
}
ArchiveImpl::Zip(z) => {
let header = async_zip::ZipEntryBuilder::new(
path.to_owned(),
async_zip::Compression::Deflate,
)
.last_modification_date(Default::default())
.unix_permissions(0o777)
.build();
z.write_entry_whole(header, content.as_bytes())
.await
.map_err(to_anyhow)?;
}
}
Ok(())
}
async fn finish(self) -> Result<()> {
match self {
ArchiveImpl::Tar(t) => t.into_inner().await?,
ArchiveImpl::Zip(z) => z.close().await.map_err(to_anyhow)?,
}
.sync_all()
.await?;
Ok(())
}
}
#[derive(Deserialize)]
struct ArchiveQueryParams {
archive_type: Option<String>,
}
#[inline]
pub fn to_string_without_metadata<T>(value: &T, preserve_extra_perms: bool) -> Result<String>
where
T: ?Sized + Serialize,
{
let value = serde_json::to_value(value).map_err(to_anyhow)?;
value
.as_object()
.map(|obj| {
let mut obj = obj.clone();
for key in [
"workspace_id",
"path",
"name",
"versions",
"id",
"created_at",
"updated_at",
"created_by",
"updated_by",
"edited_at",
"edited_by",
"archived",
] {
if obj.contains_key(key) {
obj.remove(key);
}
}
if !preserve_extra_perms && obj.contains_key("extra_perms") {
obj.remove("extra_perms");
}
serde_json::to_string_pretty(&obj).ok()
})
.flatten()
.ok_or_else(|| Error::BadRequest("Impossible to serialize value".to_string()))
}
async fn tarball_workspace(
authed: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Query(ArchiveQueryParams { archive_type }): Query<ArchiveQueryParams>,
) -> Result<([(headers::HeaderName, String); 2], impl IntoResponse)> {
require_admin(authed.is_admin, &authed.username)?;
let tmp_dir = TempDir::new_in(".")?;
let name = format!("windmill-{w_id}.tar");
let name = match archive_type.as_deref() {
Some("tar") | None => Ok(format!("windmill-{w_id}.tar")),
Some("zip") => Ok(format!("windmill-{w_id}.zip")),
Some(t) => Err(Error::BadRequest(format!("Invalid Archive Type {t}"))),
}?;
let file_path = tmp_dir.path().join(&name);
let file = File::create(&file_path).await?;
let mut a = tokio_tar::Builder::new(file);
let mut archive = match archive_type.as_deref() {
Some("tar") | None => Ok(ArchiveImpl::Tar(tokio_tar::Builder::new(file))),
Some("zip") => Ok(ArchiveImpl::Zip(async_zip::write::ZipFileWriter::new(file))),
Some(t) => Err(Error::BadRequest(format!("Invalid Archive Type {t}"))),
}?;
{
let folders = sqlx::query_as::<_, Folder>("SELECT * FROM folder WHERE workspace_id = $1")
.bind(&w_id)
@@ -820,12 +1185,12 @@ async fn tarball_workspace(
.await?;
for folder in folders {
write_to_archive(
serde_json::to_string_pretty(&folder).unwrap(),
format!("f/{}/folder.meta.json", folder.name),
&mut a,
)
.await?;
archive
.write_to_archive(
&to_string_without_metadata(&folder, true).unwrap(),
&format!("f/{}/folder.meta.json", folder.name),
)
.await?;
}
}
@@ -846,7 +1211,9 @@ async fn tarball_workspace(
ScriptLang::Go => "go",
ScriptLang::Bash => "sh",
};
write_to_archive(script.content, format!("{}.{}", script.path, ext), &mut a).await?;
archive
.write_to_archive(&script.content, &format!("{}.{}", script.path, ext))
.await?;
let lock = script
.lock
@@ -862,7 +1229,9 @@ async fn tarball_workspace(
lock,
};
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
write_to_archive(metadata_str, format!("{}.script.json", script.path), &mut a).await?;
archive
.write_to_archive(&metadata_str, &format!("{}.script.json", script.path))
.await?;
}
}
@@ -876,13 +1245,10 @@ async fn tarball_workspace(
.await?;
for resource in resources {
let resource_str = serde_json::to_string_pretty(&resource).unwrap();
write_to_archive(
resource_str,
format!("{}.resource.json", resource.path),
&mut a,
)
.await?;
let resource_str = &to_string_without_metadata(&resource, false).unwrap();
archive
.write_to_archive(&resource_str, &format!("{}.resource.json", resource.path))
.await?;
}
}
@@ -896,13 +1262,13 @@ async fn tarball_workspace(
.await?;
for resource_type in resource_types {
let resource_str = serde_json::to_string_pretty(&resource_type).unwrap();
write_to_archive(
resource_str,
format!("{}.resource-type.json", resource_type.name),
&mut a,
)
.await?;
let resource_str = &to_string_without_metadata(&resource_type, false).unwrap();
archive
.write_to_archive(
&resource_str,
&format!("{}.resource-type.json", resource_type.name),
)
.await?;
}
}
@@ -915,25 +1281,49 @@ async fn tarball_workspace(
.await?;
for flow in flows {
let flow_str = serde_json::to_string_pretty(&flow).unwrap();
write_to_archive(flow_str, format!("{}.flow.json", flow.path), &mut a).await?;
let flow_str = &to_string_without_metadata(&flow, false).unwrap();
archive
.write_to_archive(&flow_str, &format!("{}.flow.json", flow.path))
.await?;
}
}
{
let variables = sqlx::query_as::<_, ExportableListableVariable>(
"SELECT *, false as is_expired FROM variable WHERE workspace_id = $1 AND is_secret = false",
"SELECT *, false as is_expired FROM variable WHERE workspace_id = $1",
)
.bind(&w_id)
.fetch_all(&db)
.await?;
for var in variables {
let flow_str = serde_json::to_string_pretty(&var).unwrap();
write_to_archive(flow_str, format!("{}.variable.json", var.path), &mut a).await?;
let var_str = &to_string_without_metadata(&var, false).unwrap();
archive
.write_to_archive(&var_str, &format!("{}.variable.json", var.path))
.await?;
}
}
a.into_inner().await?;
{
let apps = sqlx::query_as!(
AppWithLastVersion,
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by from app, app_version
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
&w_id
)
.fetch_all(&db)
.await?;
for app in apps {
let app_str = &to_string_without_metadata(&app, false).unwrap();
archive
.write_to_archive(&app_str, &format!("{}.app.json", app.path))
.await?;
}
}
archive.finish().await?;
let file = tokio::fs::File::open(file_path).await?;
@@ -950,20 +1340,3 @@ async fn tarball_workspace(
Ok((headers, body))
}
async fn write_to_archive(
content: String,
path: String,
a: &mut tokio_tar::Builder<File>,
) -> Result<()> {
let bytes = content.as_bytes();
let mut header = tokio_tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mtime(0);
header.set_uid(0);
header.set_gid(0);
header.set_mode(0o777);
header.set_cksum();
a.append_data(&mut header, path, bytes).await?;
Ok(())
}

View File

@@ -41,3 +41,4 @@ hyper = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }
reqwest = { workspace = true, optional = true }
tracing-subscriber = { workspace = true, optional = true }
lazy_static.workspace = true

View File

@@ -10,7 +10,7 @@
use axum::{
body::{self, BoxBody},
response::IntoResponse,
Json,
response::Json,
};
#[cfg(feature = "sqlx")]
@@ -58,6 +58,8 @@ pub enum Error {
ExitStatus(i32),
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
#[error("Error: {0:#?}")]
JsonErr(serde_json::Value),
}
impl Error {

View File

@@ -11,7 +11,9 @@ use std::{collections::HashMap, time::Duration};
use serde::{self, Deserialize, Serialize};
use crate::{
more_serde::{default_false, default_id, default_true, is_default},
more_serde::{
default_empty_string, default_false, default_id, default_null, default_true, is_default,
},
scripts::{Schema, ScriptHash, ScriptLang},
};
@@ -57,6 +59,7 @@ pub struct NewFlow {
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct FlowValue {
pub modules: Vec<FlowModule>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub failure_module: Option<FlowModule>,
#[serde(default)]
@@ -150,7 +153,9 @@ pub struct FlowModule {
#[serde(alias = "input_transform")]
pub input_transforms: HashMap<String, InputTransform>,
pub value: FlowModuleValue,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_after_if: Option<StopAfterIf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suspend: Option<Suspend>,
@@ -172,8 +177,14 @@ impl FlowModule {
rename_all(serialize = "lowercase", deserialize = "lowercase")
)]
pub enum InputTransform {
Static { value: serde_json::Value },
Javascript { expr: String },
Static {
#[serde(default = "default_null")]
value: serde_json::Value,
},
Javascript {
#[serde(default = "default_empty_string")]
expr: String,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -237,7 +248,9 @@ pub enum FlowModuleValue {
#[serde(alias = "input_transform")]
input_transforms: HashMap<String, InputTransform>,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
lock: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
language: ScriptLang,
},

View File

@@ -26,12 +26,13 @@ pub mod variables;
#[cfg(feature = "tracing_init")]
pub mod tracing_init;
pub const DEFAULT_NUM_WORKERS: usize = 3;
pub const DEFAULT_TIMEOUT: i32 = 300;
pub const DEFAULT_SLEEP_QUEUE: u64 = 50;
pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 3;
lazy_static::lazy_static! {
pub static ref BASE_URL: String = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
}
#[cfg(feature = "tokio")]
pub async fn shutdown_signal(tx: tokio::sync::broadcast::Sender<()>) -> anyhow::Result<()> {
use std::io;
@@ -123,10 +124,8 @@ pub async fn get_latest_hash_for_path<'c>(
script_path: &str,
) -> error::Result<scripts::ScriptHash> {
let script_hash_o = sqlx::query_scalar!(
"select hash from script where path = $1 AND (workspace_id = $2 OR workspace_id = \
'starter') AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR \
workspace_id = 'starter')) AND
"select hash 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",
script_path,
w_id

View File

@@ -18,6 +18,14 @@ pub fn default_false() -> bool {
false
}
pub fn default_null() -> serde_json::Value {
serde_json::Value::Null
}
pub fn default_empty_string() -> String {
String::new()
}
pub fn default_id() -> String {
rd_string(6)
}

View File

@@ -210,7 +210,7 @@ pub fn to_hex_string(i: &i64) -> String {
pub async fn get_hub_script_by_path(
email: &str,
path: StripPath,
http_client: reqwest::Client,
http_client: &reqwest::Client,
) -> crate::error::Result<String> {
use crate::{
error::{to_anyhow, Error},
@@ -239,7 +239,7 @@ pub async fn get_hub_script_by_path(
pub async fn get_full_hub_script_by_path(
email: &str,
path: StripPath,
http_client: reqwest::Client,
http_client: &reqwest::Client,
) -> crate::error::Result<HubScript> {
use crate::{
error::{to_anyhow, Error},

View File

@@ -42,7 +42,11 @@ pub fn initialize_tracing() {
match json_fmt {
true => ts_base
.with(json_layer().with_filter(filter_fn(filter_metadata)))
.with(
json_layer()
.flatten_event(true)
.with_filter(filter_fn(filter_metadata)),
)
.init(),
false => ts_base
.with(

View File

@@ -6,7 +6,10 @@
* LICENSE-AGPL for a copy of the license.
*/
pub fn owner_to_token_owner(user: &str, is_group: bool) -> String {
let prefix = if is_group { 'g' } else { 'u' };
format!("{}/{}", prefix, user)
pub fn username_to_permissioned_as(user: &str) -> String {
if user.contains('@') {
user.to_string()
} else {
format!("u/{}", user)
}
}

View File

@@ -72,13 +72,9 @@ pub fn not_found_if_none<T, U: AsRef<str>>(opt: Option<T>, kind: &str, name: U)
}
}
pub fn get_owner_from_path(path: &str) -> String {
path.split('/').take(2).collect::<Vec<_>>().join("/")
}
#[cfg(feature = "reqwest")]
pub async fn list_elems_from_hub(
http_client: reqwest::Client,
http_client: &reqwest::Client,
url: &str,
email: &str,
) -> Result<serde_json::Value> {
@@ -92,7 +88,7 @@ pub async fn list_elems_from_hub(
#[cfg(feature = "reqwest")]
pub async fn http_get_from_hub(
http_client: reqwest::Client,
http_client: &reqwest::Client,
url: &str,
email: &str,
plain: bool,

View File

@@ -8,6 +8,8 @@
use serde::{Deserialize, Serialize};
use crate::BASE_URL;
#[derive(Serialize, Clone)]
pub struct ContextualVariable {
@@ -66,7 +68,6 @@ pub fn get_reserved_variables(
username: &str,
job_id: &str,
permissioned_as: &str,
base_url: &str,
path: Option<String>,
flow_id: Option<String>,
flow_path: Option<String>,
@@ -114,7 +115,7 @@ pub fn get_reserved_variables(
},
ContextualVariable {
name: "WM_BASE_URL".to_string(),
value: base_url.to_string(),
value: BASE_URL.clone(),
description: "base url of this instance".to_string(),
},
ContextualVariable {

View File

@@ -8,6 +8,8 @@
use std::{collections::HashMap, str::FromStr};
use anyhow::Context;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres, Transaction};
use tracing::{instrument, Instrument};
@@ -15,7 +17,7 @@ use ulid::Ulid;
use uuid::Uuid;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, to_anyhow, Error},
error::{self, Error},
flow_status::{FlowStatus, JobResult, MAX_RETRY_ATTEMPTS, MAX_RETRY_INTERVAL},
flows::{FlowModule, FlowModuleValue, FlowValue},
scripts::{get_full_hub_script_by_path, HubScript, ScriptHash, ScriptLang},
@@ -23,6 +25,10 @@ use windmill_common::{
};
lazy_static::lazy_static! {
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.build().unwrap();
// TODO: these aren't synced, they should be moved into the queue abstraction once/if that happens.
static ref QUEUE_PUSH_COUNT: prometheus::IntCounter = prometheus::register_int_counter!(
"queue_push_count",
@@ -44,6 +50,7 @@ lazy_static::lazy_static! {
}
const MAX_FREE_EXECS: i32 = 1000;
const MAX_FREE_CONCURRENT_RUNS: i32 = 15;
pub async fn cancel_job<'c>(
username: &str,
@@ -80,7 +87,32 @@ pub async fn cancel_job<'c>(
Ok((tx, job_option))
}
pub async fn pull(db: &Pool<Postgres>) -> windmill_common::error::Result<Option<QueuedJob>> {
pub async fn pull(
db: &Pool<Postgres>,
whitelist_workspaces: Option<Vec<String>>,
blacklist_workspaces: Option<Vec<String>>,
) -> windmill_common::error::Result<Option<QueuedJob>> {
let mut workspaces_filter = String::new();
if let Some(whitelist) = whitelist_workspaces {
workspaces_filter.push_str(&format!(
" AND workspace_id IN ({})",
whitelist
.into_iter()
.map(|x| format!("'{x}'"))
.collect::<Vec<String>>()
.join(",")
));
}
if let Some(blacklist) = blacklist_workspaces {
workspaces_filter.push_str(&format!(
" AND workspace_id NOT IN ({})",
blacklist
.into_iter()
.map(|x| format!("'{x}'"))
.collect::<Vec<String>>()
.join(",")
));
}
/* Jobs can be started if they:
* - haven't been started before,
* running = false
@@ -88,7 +120,7 @@ pub async fn pull(db: &Pool<Postgres>) -> windmill_common::error::Result<Option<
* suspend_until is non-null
* and suspend = 0 when the resume messages are received
* or suspend_until <= now() if it has timed out */
let job: Option<QueuedJob> = sqlx::query_as::<_, QueuedJob>(
let job: Option<QueuedJob> = sqlx::query_as::<_, QueuedJob>(&format!(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
@@ -97,17 +129,17 @@ pub async fn pull(db: &Pool<Postgres>) -> windmill_common::error::Result<Option<
WHERE id = (
SELECT id
FROM queue
WHERE ( running = false
WHERE ((running = false
AND scheduled_for <= now())
OR (suspend_until IS NOT NULL
AND ( suspend <= 0
OR suspend_until <= now()))
OR suspend_until <= now()))) {workspaces_filter}
ORDER BY scheduled_for
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *",
)
RETURNING *"
))
.fetch_optional(db)
.await?;
@@ -172,7 +204,6 @@ pub async fn get_result_by_id(
"Flow result by id",
format!("{}, {}", flow_id, node_id),
)?;
println!("result_id: {:#?}, {node_id}", result_id);
let value = match result_id {
JobResult::ListJob(x) => {
@@ -318,10 +349,41 @@ pub async fn push<'c>(
.fetch_optional(&mut tx)
.await?
.unwrap_or(false);
if !is_super_admin && usage > MAX_FREE_EXECS {
return Err(error::Error::BadRequest(format!(
if !is_super_admin {
if usage > MAX_FREE_EXECS
&& !matches!(job_payload, JobPayload::Dependencies { .. })
&& !matches!(job_payload, JobPayload::FlowDependencies { .. })
{
return Err(error::Error::BadRequest(format!(
"User {email} has exceeded the free usage limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
)));
}
let in_queue =
sqlx::query_scalar!("SELECT COUNT(id) FROM queue WHERE email = $1", email)
.fetch_one(&mut tx)
.await?
.unwrap_or(0);
if in_queue > MAX_FREE_EXECS.into() {
return Err(error::Error::BadRequest(format!(
"User {email} has exceeded the jobs in queue limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
)));
}
let concurrent_runs = sqlx::query_scalar!(
"SELECT COUNT(id) FROM queue WHERE running = true AND email = $1",
email
)
.fetch_one(&mut tx)
.await?
.unwrap_or(0);
if concurrent_runs > MAX_FREE_CONCURRENT_RUNS.into() {
return Err(error::Error::BadRequest(format!(
"User {email} has exceeded the concurrent runs limit of {MAX_FREE_CONCURRENT_RUNS} that applies outside of premium workspaces."
)));
}
}
}
}
@@ -330,8 +392,7 @@ pub async fn push<'c>(
match job_payload {
JobPayload::ScriptHash { hash, path } => {
let language = sqlx::query_scalar!(
"SELECT language as \"language: ScriptLang\" FROM script WHERE hash = $1 AND \
(workspace_id = $2 OR workspace_id = 'starter')",
"SELECT language as \"language: ScriptLang\" FROM script WHERE hash = $1 AND workspace_id = $2",
hash.0,
workspace_id
)
@@ -352,7 +413,9 @@ pub async fn push<'c>(
)
}
JobPayload::ScriptHub { path } => {
let script = get_hub_script(path.clone(), email).await?;
let script = get_hub_script(&HTTP_CLIENT, path.clone(), email)
.await
.context("error fetching hub script")?;
(
None,
Some(path),
@@ -380,11 +443,10 @@ pub async fn push<'c>(
),
JobPayload::FlowDependencies { path } => {
let value_json = sqlx::query_scalar!(
"SELECT value FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = \
'starter')",
path,
workspace_id
)
"SELECT value FROM flow WHERE path = $1 AND workspace_id = $2",
path,
workspace_id
)
.fetch_optional(&mut tx)
.await?
.ok_or_else(|| Error::InternalErr(format!("not found flow at path {:?}", path)))?;
@@ -407,11 +469,10 @@ pub async fn push<'c>(
}
JobPayload::Flow(flow) => {
let value_json = sqlx::query_scalar!(
"SELECT value FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = \
'starter')",
flow,
workspace_id
)
"SELECT value FROM flow WHERE path = $1 AND workspace_id = $2",
flow,
workspace_id
)
.fetch_optional(&mut tx)
.await?
.ok_or_else(|| Error::InternalErr(format!("not found flow at path {:?}", flow)))?;
@@ -545,60 +606,75 @@ pub async fn push<'c>(
Ok((uuid, tx))
}
pub fn canceled_job_to_result(job: &QueuedJob) -> String {
pub fn canceled_job_to_result(job: &QueuedJob) -> serde_json::Value {
let reason = job
.canceled_reason
.as_deref()
.unwrap_or_else(|| "no reason given");
let canceler = job.canceled_by.as_deref().unwrap_or_else(|| "unknown");
format!("Job canceled: {reason} by {canceler}")
serde_json::json!({"message": format!("Job canceled: {reason} by {canceler}"), "name": "Canceled", "reason": reason, "canceler": canceler})
}
pub async fn get_hub_script(path: String, email: &str) -> error::Result<HubScript> {
get_full_hub_script_by_path(
email,
StripPath(path),
reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.build()
.map_err(to_anyhow)?,
)
.await
.map(|e| e)
pub async fn get_hub_script(
client: &reqwest::Client,
path: String,
email: &str,
) -> error::Result<HubScript> {
get_full_hub_script_by_path(email, StripPath(path), client)
.await
.map(|e| e)
}
#[derive(Debug, sqlx::FromRow, Serialize, Clone)]
pub struct QueuedJob {
pub workspace_id: String,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub scheduled_for: chrono::DateTime<chrono::Utc>,
pub running: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_lock: Option<String>,
pub canceled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_ping: Option<chrono::DateTime<chrono::Utc>>,
pub job_kind: JobKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule_path: Option<String>,
pub permissioned_as: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_status: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_flow: Option<serde_json::Value>,
pub is_flow_step: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<ScriptLang>,
pub same_worker: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_run_error: Option<String>,
pub email: String,
pub visible_to_owner: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub suspend: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
}
impl QueuedJob {

View File

@@ -13,7 +13,8 @@ use sqlx::{query_scalar, Postgres, Transaction};
use windmill_common::{
error::{self, Result},
schedule::Schedule,
utils::{get_owner_from_path, now_from_db, StripPath},
users::username_to_permissioned_as,
utils::{now_from_db, StripPath},
};
use crate::{push, JobPayload};
@@ -80,7 +81,7 @@ pub async fn push_scheduled_job<'c>(
args,
&schedule_to_user(&schedule.path),
&schedule.email,
get_owner_from_path(&schedule.path),
username_to_permissioned_as(&schedule.edited_by),
Some(next),
Some(schedule.path.clone()),
None,

View File

@@ -3,11 +3,10 @@ name = "windmill-worker"
version.workspace = true
authors.workspace = true
edition.workspace = true
default-run = "worker"
[[bin]]
name = "worker"
path = "./src/main.rs"
[lib]
name = "windmill_worker"
path = "src/lib.rs"
[features]
default = []

View File

@@ -0,0 +1,55 @@
import sys
import os
from importlib.abc import MetaPathFinder, Loader
from importlib.machinery import ModuleSpec, SourceFileLoader
class WindmillLoader(Loader):
def __init__(self, path):
self.path = path
def create_module(self, spec):
return None
def exec_module(self, module):
module.__path__ = self.path
return None
class WindmillFinder(MetaPathFinder):
@classmethod
def find_spec(cls, name, path, target=None):
splitted = name.split(".")
if splitted[0] != "f" and splitted[0] != "u":
return None
l = len(splitted)
if l <= 2:
return ModuleSpec(name, WindmillLoader(name))
elif l == 3:
script_path = "/".join(splitted)
import requests
url = f"{os.environ.get('BASE_INTERNAL_URL')}/api/w/{os.environ.get('WM_WORKSPACE')}/scripts/raw/p/{script_path}"
r = requests.get(
url, headers={"Authorization": f"Bearer {os.environ.get('WM_TOKEN')}"}
)
if r.status_code == 200:
folder = os.getcwd() + "/tmp/" + "/".join(splitted[:-1])
fullpath = folder + "/" + splitted[-1] + ".py"
os.makedirs(folder, exist_ok=True)
with open(fullpath, "w+") as f:
f.write(r.text)
return ModuleSpec(name, SourceFileLoader(name, fullpath))
else:
print(r.text, r.status_code)
raise ImportError(f"Script {script_path} not found")
else:
raise ImportError(
"Import can only be done at the top level of a folder or user space"
)
sys.meta_path.append(WindmillFinder)

View File

@@ -3,12 +3,11 @@ name: "bash run script"
mode: ONCE
hostname: "bash"
log_level: ERROR
time_limit: 300
rlimit_as: 2048
rlimit_as: 4096
rlimit_cpu: 1000
rlimit_fsize: 1024
rlimit_nofile: 64
rlimit_fsize: 1000
rlimit_nofile: 10000
cwd: "/tmp"
@@ -55,7 +54,7 @@ mount {
dst: "/tmp"
fstype: "tmpfs"
rw: true
options: "size=500000000"
options: "size=800000000"
}
mount {
@@ -87,4 +86,4 @@ iface_no_lo: true
{SHARED_MOUNT}
envar: "HOME=/tmp"

View File

@@ -3,12 +3,11 @@ name: "deno run script"
mode: ONCE
hostname: "deno"
log_level: ERROR
time_limit: 300
rlimit_as: 16000
rlimit_cpu: 1000
rlimit_fsize: 1024
rlimit_nofile: 64
rlimit_fsize: 1000
rlimit_nofile: 10000
cwd: "/tmp"
@@ -73,6 +72,12 @@ mount {
is_bind: true
}
mount {
src: "{JOB_DIR}/import_map.json"
dst: "/tmp/import_map.json"
is_bind: true
}
mount {
src: "{JOB_DIR}/lock.json"
dst: "/tmp/lock.json"

View File

@@ -3,12 +3,11 @@ name: "go run script"
mode: ONCE
hostname: "go"
log_level: ERROR
time_limit: 300
rlimit_as: 2048
rlimit_as: 4096
rlimit_cpu: 1000
rlimit_fsize: 1024
rlimit_nofile: 64
rlimit_fsize: 1000
rlimit_nofile: 10000
cwd: "/tmp/go"

View File

@@ -3,12 +3,11 @@ name: "python run script"
mode: ONCE
hostname: "python"
log_level: ERROR
time_limit: 300
rlimit_as: 2048
rlimit_as: 4096
rlimit_cpu: 1000
rlimit_fsize: 1024
rlimit_nofile: 64
rlimit_fsize: 1000
rlimit_nofile: 10000
cwd: "/tmp"
@@ -72,6 +71,13 @@ mount {
is_bind: true
}
mount {
src: "{JOB_DIR}/loader.py"
dst: "/tmp/loader.py"
is_bind: true
mandatory: false
}
mount {
src: "{JOB_DIR}/main.py"
dst: "/tmp/main.py"

View File

@@ -6,7 +6,6 @@
* LICENSE-AGPL for a copy of the license.
*/
use serde_json::{Map, Value};
use sqlx::{Pool, Postgres, Transaction};
use tracing::instrument;
use uuid::Uuid;
@@ -14,36 +13,17 @@ use windmill_common::{error::Error, flow_status::FlowStatusModule, schedule::Sch
use windmill_queue::{delete_job, schedule::get_schedule_opt, JobKind, QueuedJob};
#[instrument(level = "trace", skip_all)]
pub async fn add_completed_job_error<E: ToString + std::fmt::Debug>(
pub async fn add_completed_job_error(
db: &Pool<Postgres>,
queued_job: &QueuedJob,
logs: String,
e: E,
e: serde_json::Value,
metrics: Option<crate::worker::Metrics>,
) -> Result<(Uuid, serde_json::Map<String, serde_json::Value>), Error> {
) -> Result<serde_json::Value, Error> {
metrics.map(|m| m.worker_execution_failed.inc());
let mut output_map = Map::new();
error_to_result(&mut output_map, &e);
let a = add_completed_job(
db,
&queued_job,
false,
false,
serde_json::Value::Object(output_map.clone()),
logs,
)
.await?;
Ok((a, output_map))
}
pub fn error_to_result<E: ToString + std::fmt::Debug>(
output_map: &mut Map<String, Value>,
err: &E,
) {
output_map.insert(
"error".to_string(),
serde_json::Value::String(err.to_string()),
);
let result = serde_json::json!({ "error": e });
let _ = add_completed_job(db, &queued_job, false, false, result.clone(), logs).await?;
Ok(result)
}
fn flatten_jobs(modules: Vec<FlowStatusModule>) -> Vec<Uuid> {
@@ -97,6 +77,12 @@ pub async fn add_completed_job(
None
};
let mem_peak = sqlx::query_scalar!("SELECT mem_peak FROM queue WHERE id = $1", &queued_job.id)
.fetch_optional(db)
.await
.ok()
.flatten()
.flatten();
let mut tx = db.begin().await?;
let job_id = queued_job.id.clone();
sqlx::query!(
@@ -129,9 +115,10 @@ pub async fn add_completed_job(
, language
, email
, visible_to_owner
, mem_peak
)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, EXTRACT(milliseconds FROM (now() - $6))), $7, $8, $9,\
$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, (EXTRACT('epoch' FROM (now())) - EXTRACT('epoch' FROM (COALESCE($6, now()))))*1000), $7, $8, $9,\
$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28, $29)
ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)",
queued_job.workspace_id,
queued_job.id,
@@ -160,7 +147,8 @@ pub async fn add_completed_job(
queued_job.language: ScriptLang,
duration: Option<i64>,
queued_job.email,
queued_job.visible_to_owner
queued_job.visible_to_owner,
mem_peak
)
.execute(&mut tx)
.await

View File

@@ -34,10 +34,11 @@ pub async fn eval_timeout(
env: Vec<(String, serde_json::Value)>,
creds: Option<EvalCreds>,
by_id: Option<IdContext>,
base_internal_url: String,
base_internal_url: &str,
) -> anyhow::Result<serde_json::Value> {
let expr2 = expr.clone();
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
let base_internal_url: String = base_internal_url.to_string();
timeout(
std::time::Duration::from_millis(2000),
tokio::task::spawn_blocking(move || {
@@ -346,7 +347,7 @@ mod tests {
let code = "value.test + params.test";
let mut runtime = JsRuntime::new(RuntimeOptions::default());
let res = eval(&mut runtime, code, env, None, None, "").await?;
let res = eval(&mut runtime, code, env, None, None, String::new().as_str()).await?;
assert_eq!(res, json!(4));
Ok(())
}
@@ -359,7 +360,7 @@ mod tests {
multiline template`";
let mut runtime = JsRuntime::new(RuntimeOptions::default());
let res = eval(&mut runtime, code, env, None, None, "").await?;
let res = eval(&mut runtime, code, env, None, None, String::new().as_str()).await?;
assert_eq!(res, json!("my 5\nmultiline template"));
Ok(())
}
@@ -372,7 +373,7 @@ multiline template`";
];
let code = r#"params.test"#;
let res = eval_timeout(code.to_string(), env, None, None, "".to_string()).await?;
let res = eval_timeout(code.to_string(), env, None, None, String::new().as_str()).await?;
assert_eq!(res, json!(2));
Ok(())
}

View File

@@ -1,138 +0,0 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use std::{net::SocketAddr, time::Duration};
use anyhow::Context;
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
use windmill_common::{
error::{self, Error},
utils::rd_string,
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// dotenv().ok();
windmill_common::tracing_init::initialize_tracing();
let db = async {
let database_url = std::env::var("DATABASE_URL")
.map_err(|_| Error::BadConfig("DATABASE_URL env var is missing".to_string()))?;
let max_connections = match std::env::var("DATABASE_CONNECTIONS") {
Ok(n) => n.parse::<u32>().context("invalid DATABASE_CONNECTIONS")?,
Err(_) => 10,
};
Ok::<Pool<Postgres>, error::Error>(
PgPoolOptions::new()
.max_connections(max_connections)
.max_lifetime(Duration::from_secs(30 * 60)) // 30 mins
.connect(&database_url)
.await
.map_err(|err| Error::ConnectingToDatabase(err.to_string()))?,
)
}
.await?;
let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
.ok()
.map(|s| {
s.parse::<bool>()
.map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
.or_else(|_| s.parse::<SocketAddr>().map(Some))
})
.transpose()?
.flatten();
let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
let shutdown_signal = windmill_common::shutdown_signal(tx);
let base_internal_url =
std::env::var("BASE_INTERNAL_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
let base_url = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
let timeout = std::env::var("TIMEOUT")
.ok()
.and_then(|x| x.parse::<i32>().ok())
.unwrap_or(windmill_common::DEFAULT_TIMEOUT);
let workers_f = async {
let sleep_queue = std::env::var("SLEEP_QUEUE")
.ok()
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or(windmill_common::DEFAULT_SLEEP_QUEUE);
let disable_nuser = std::env::var("DISABLE_NUSER")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false);
let disable_nsjail = std::env::var("DISABLE_NSJAIL")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(true);
let keep_job_dir = std::env::var("KEEP_JOB_DIR")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false);
let sync_bucket = std::env::var("S3_CACHE_BUCKET")
.ok()
.map(|e| Some(e))
.unwrap_or(None);
tracing::info!(
"DISABLE_NSJAIL: {disable_nsjail}, DISABLE_NUSER: {disable_nuser}, BASE_URL: \
{base_url}, SLEEP_QUEUE: {sleep_queue}, TIMEOUT: \
{timeout}, KEEP_JOB_DIR: {keep_job_dir}"
);
let instance_name = rd_string(5);
let ip = windmill_common::external_ip::get_ip()
.await
.unwrap_or_else(|e| {
tracing::warn!(error = e.to_string(), "failed to get external IP");
"unretrievable IP".to_string()
});
let worker_name = format!("dt-worker-{}-{}", &instance_name, rd_string(5));
windmill_worker::run_worker(
&db.clone(),
timeout,
&instance_name,
worker_name,
1,
1,
&ip,
sleep_queue,
windmill_worker::WorkerConfig {
disable_nsjail,
disable_nuser,
base_internal_url,
base_url,
keep_job_dir,
},
sync_bucket,
rx.resubscribe(),
)
.await;
Ok(()) as anyhow::Result<()>
};
let metrics_f = async {
match metrics_addr {
Some(addr) => windmill_common::serve_metrics(addr, rx.resubscribe())
.await
.map_err(anyhow::Error::from),
None => Ok(()),
}
};
futures::try_join!(shutdown_signal, workers_f, metrics_f)?;
Ok(())
}

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