Compare commits

..

492 Commits

Author SHA1 Message Date
Faton Ramadani
5131ee8b89 feat(frontend): add a dev docker-compose file + update readme 2023-04-12 21:46:13 +02:00
Ruben Fiszel
a5f6d73f7d fix(backend): do not fail on schedule not existing anymore 2023-04-12 20:06:50 +02:00
Ruben Fiszel
670c84b901 exclude deno/gen/file from global cache 2023-04-12 19:18:48 +02:00
Oliver Veal
92a293488e feat: inputs library on run page
* display previous script inputs on script run page

* parallelise loading

* parallelise loading

* also working for flows

* separate endpoints for scripts and flows

* Splitpanes and Saved Inputs (UI)

* Saved inputs API endpoints

* Editable Input name

* Narrow width styling

* feat(frontend): Add a toggle to open the saved inputs (#1401)

* feat(frontend): Add a toggle to open the saved inputs

* feat(frontend): Add a toggle to open the saved inputs

* feat(frontend): Move toggle

* update all

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-04-12 17:29:06 +02:00
Ruben Fiszel
0cd1e65e46 frontend apps rename improvements 2023-04-12 14:38:13 +02:00
Faton Ramadani
6aa1008933 fix(frontend): Remove output when deleting a component (#1397) 2023-04-12 10:28:54 +02:00
Ruben Fiszel
9434bbb18b fix script explorer 2023-04-11 22:12:28 +02:00
Ruben Fiszel
e6632a32c9 chore(main): release 1.87.0 (#1375)
* chore(main): release 2.0.0

* Apply automatic changes

* Update version.txt

* Update CHANGELOG.md

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-04-11 21:11:11 +02:00
Ruben Fiszel
54c4d03173 fix search input for app propagation 2023-04-11 20:04:34 +02:00
Ruben Fiszel
3932e5dfb9 add GOPRIVATE and NETRC for private modules 2023-04-11 19:55:38 +02:00
Ruben Fiszel
9b3d6a3dd9 fix hub compatible export 2023-04-11 18:33:45 +02:00
Faton Ramadani
58d4b556eb fix(frontend): Add missing stopPropagation (#1394)
* fix(frontend): Add missing stopPropagation

* fix(frontend): fix component selection
2023-04-11 16:22:22 +02:00
Ruben Fiszel
8552c92837 fix relative url deno loading2 2023-04-11 15:57:59 +02:00
Ruben Fiszel
955a213a50 fix(backend): nested deno relative imports 2023-04-11 15:16:55 +02:00
Ruben Fiszel
e82f5388b6 compile 2023-04-11 13:40:58 +02:00
Ádám Kovács
8a44f8e779 fix(frontend): Fix icon picker input (#1389) 2023-04-11 13:29:55 +02:00
Kai Jellinghaus
d45e6c94ab feat(backend): Redis based queue (#1324)
* Merge?

* Fix V8 breaking change

* WIP

* WIP

* Cleanup

* Move to git reference

* Fix Merge conflict

* update

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-04-11 12:24:13 +02:00
Ruben Fiszel
60da67a725 restore int default argument parsing in dneo 2023-04-11 12:15:16 +02:00
Ruben Fiszel
0718931616 fix tests + scheduled_for uses now instead of system time 2023-04-11 11:48:14 +02:00
Faton Ramadani
41831d58ed fix(frontend): Fix mac shortcuts (#1381)
* fix(frontend): fix app init issue

* fix(frontend): Fix mac shortcuts

* Update NonRunnableComponent.svelte

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-04-11 08:52:40 +02:00
Ruben Fiszel
36816877b4 remove initialized 2023-04-11 08:51:09 +02:00
dependabot[bot]
0040e15805 chore(deps-dev): bump @types/vscode from 1.74.0 to 1.77.0 in /frontend (#1358)
Bumps [@types/vscode](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/vscode) from 1.74.0 to 1.77.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/vscode)

---
updated-dependencies:
- dependency-name: "@types/vscode"
  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-04-11 08:24:34 +02:00
dependabot[bot]
72317e9b54 chore(deps-dev): bump svelte2tsx from 0.6.10 to 0.6.11 in /frontend (#1382)
Bumps [svelte2tsx](https://github.com/sveltejs/language-tools) from 0.6.10 to 0.6.11.
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte2tsx-0.6.10...svelte2tsx-0.6.11)

---
updated-dependencies:
- dependency-name: svelte2tsx
  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-04-11 08:24:19 +02:00
dependabot[bot]
4cdad376b3 chore(deps): bump python from 3.11.2-slim-buster to 3.11.3-slim-buster (#1383)
Bumps python from 3.11.2-slim-buster to 3.11.3-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-04-11 08:24:02 +02:00
dependabot[bot]
866228b663 chore(deps-dev): bump eslint from 8.37.0 to 8.38.0 in /frontend (#1384)
Bumps [eslint](https://github.com/eslint/eslint) from 8.37.0 to 8.38.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.37.0...v8.38.0)

---
updated-dependencies:
- dependency-name: eslint
  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-04-11 08:23:42 +02:00
dependabot[bot]
7eacca4caa chore(deps-dev): bump pdfjs-dist from 3.4.120 to 3.5.141 in /frontend (#1385)
Bumps [pdfjs-dist](https://github.com/mozilla/pdfjs-dist) from 3.4.120 to 3.5.141.
- [Release notes](https://github.com/mozilla/pdfjs-dist/releases)
- [Commits](https://github.com/mozilla/pdfjs-dist/commits)

---
updated-dependencies:
- dependency-name: pdfjs-dist
  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-04-11 08:23:21 +02:00
dependabot[bot]
1526537f56 chore(deps): bump svelte-select from 5.5.2 to 5.6.0 in /frontend (#1386)
Bumps [svelte-select](https://github.com/rob-balfre/svelte-select) from 5.5.2 to 5.6.0.
- [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.5.2...v5.6.0)

---
updated-dependencies:
- dependency-name: svelte-select
  dependency-type: direct:production
  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-04-11 08:22:03 +02:00
Ruben Fiszel
1f705cab2c fix(cli): overwrite archived scripts 2023-04-11 08:21:37 +02:00
Ruben Fiszel
f2d3c8208b feat(backend): add instance events webhook 2023-04-11 01:45:56 +02:00
Ruben Fiszel
1b04537c9a more app fixes 2023-04-10 22:34:09 +02:00
Ruben Fiszel
23e374b10d more app fixes 2023-04-10 21:57:07 +02:00
Ruben Fiszel
5fc72ea2e6 fix app first debounce for improve trigger on load 2023-04-10 20:27:37 +02:00
Ruben Fiszel
3f5df1ee41 fix home + refreshOnStart 2023-04-10 18:02:27 +02:00
Ádám Kovács
3dabac153f feat(frontend)!: Add option to return file names (#1380) 2023-04-10 17:32:03 +02:00
Ruben Fiszel
d0e0e1fdf2 fix(frontend): fix app init issue 2023-04-10 17:27:48 +02:00
Faton Ramadani
8e9c491650 fix(frontend): Fix frontend dependencies (#1379)
* fix(frontend): Fix frontend dependencies

* fix(frontend): Fix frontend dependencies

* fix(frontend): Fix frontend dependencies
2023-04-10 15:34:34 +02:00
Ruben Fiszel
df4c6289ac improve metrics 2023-04-10 11:58:44 +02:00
Ruben Fiszel
72c8d3921d improve metrics 2023-04-10 11:53:32 +02:00
Ruben Fiszel
e911869d99 perf: parallelize more operations for deno jobs 2023-04-10 10:55:56 +02:00
Ruben Fiszel
e1712e63a6 improve token lock 2023-04-10 08:00:41 +02:00
Faton Ramadani
2031e1ebd0 fix(frontend): fix popover configuration to avoid content shift (#1377) 2023-04-10 07:24:57 +02:00
Faton Ramadani
de8dc1e9cd fix(frontend): remove stopPropagation that was preventing components dnd (#1378) 2023-04-10 07:23:15 +02:00
Ruben Fiszel
de87d7ac27 fetch token in the background 2023-04-10 01:16:15 +02:00
Ruben Fiszel
2b003c684f make create_token_for_owner not a transaction anymore 2023-04-09 21:47:05 +02:00
Ruben Fiszel
3097510550 only register prometheus metrics if they are enabled 2023-04-09 15:39:25 +02:00
Ruben Fiszel
0c0b2d88cc deno optimization v0 2023-04-09 14:10:56 +02:00
Ruben Fiszel
1ffed41cf9 fix backend tests 2023-04-09 11:20:15 +02:00
Ruben Fiszel
dac61d1c98 feat(backend): extend cached resolution for go 2023-04-09 10:58:45 +02:00
Ruben Fiszel
facb67093c feat(python): cache dependency resolution 2023-04-08 23:25:46 +02:00
Ruben Fiszel
341a9662b7 add debug log line on execute 2023-04-08 22:21:22 +02:00
Ruben Fiszel
e80454e7fd chore(main): release 1.86.0 (#1352)
* chore(main): release 1.86.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-04-08 22:01:22 +02:00
Ruben Fiszel
3a232dbb57 feat(cli): add getFullResource 2023-04-08 21:57:40 +02:00
Ruben Fiszel
ee45f1ca7b show status code when possible 2023-04-08 20:09:27 +02:00
Ruben Fiszel
bbb6ee978d reduce verbosity of cache sync 2023-04-08 15:55:09 +02:00
Ruben Fiszel
5961995e80 reduce verbosity of cache sync 2023-04-08 15:48:07 +02:00
Ruben Fiszel
78f5fd275e remove deno nsjail in favor of deno sandboxing 2023-04-08 15:25:10 +02:00
Ruben Fiszel
a7c4c5d0a8 use skeleton for workers page 2023-04-08 14:08:56 +02:00
Ruben Fiszel
617220d75a update caddyfile 2023-04-08 14:00:51 +02:00
Ruben Fiszel
09042583c7 update self-host 2023-04-08 13:52:31 +02:00
Ruben Fiszel
2e80404e5e allow back navigation for home tabs 2023-04-08 13:20:46 +02:00
Ruben Fiszel
8ef29099f8 fix README and fix inline editor 2023-04-08 13:05:40 +02:00
Ruben Fiszel
1097dccfe5 change default recomputeOnInputChanges 2023-04-08 12:33:03 +02:00
Ádám Kovács
76a2a1db36 fix(frontend): Minor fixes (#1374)
* fixes

* reverse wm logo spin

* remove component tab label

* updates
2023-04-08 09:52:41 +02:00
Ruben Fiszel
65721b3b20 fix lsp relative imports 2023-04-08 00:11:10 +02:00
Ruben Fiszel
7675f08b7b feat(frontend): add impersonate api + local resolution of import by lsp v0 2023-04-07 22:54:55 +02:00
Ádám Kovács
b962ae3578 various app updates (#1373) 2023-04-07 13:58:02 +02:00
Ruben Fiszel
34a8b01b76 feat(frontend): add flow expand button 2023-04-07 12:36:29 +02:00
Ruben Fiszel
179382afbd improve further monaco assistant 2023-04-07 09:16:10 +02:00
Ruben Fiszel
37ee631363 experimental: make deno a separate language in the editor (#1370)
* all

* all

* all
2023-04-07 07:47:21 +02:00
Ruben Fiszel
dba37c2771 fix open drawer script builder 2023-04-07 01:44:36 +02:00
Ruben Fiszel
33f2bad8d9 add GOPATH to go mod tidy 2023-04-07 00:49:57 +02:00
Faton Ramadani
368cdefd91 Menu refactor (#1372)
* wip menu refactor

* wip

* wip

* feat(frontend): finish menu refactor

* feat(frontend): finish menu refactor
2023-04-07 00:08:53 +02:00
Ruben Fiszel
266b5b00da fix(frontend): make croninput a builder rather than a tab 2023-04-06 23:57:42 +02:00
Ruben Fiszel
8fe68c832b set schema on script template change 2023-04-06 23:28:03 +02:00
Faton Ramadani
92be102a07 feat(frontend): Improve login + toasts (#1363)
* feat(frontend): Improve login + toasts

* feat(frontend): Improve workspace selection

* feat(frontend): restore package.lock

* feat(frontend): restore package.lock

* feat(frontend): Set gray background + smal fixes

* feat(frontend): fix login modal margin

* feat(frontend): update color
2023-04-06 17:23:58 +02:00
Faton Ramadani
a344928f25 fix(frontend): Fix button poppup (#1368)
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-04-06 16:09:26 +02:00
Faton Ramadani
f214d5f96b feat(frontend): Tone down text + display whole text (#1366) 2023-04-06 16:07:34 +02:00
Faton Ramadani
4af39f081b fix(frontend): Fix connected property (#1371)
* fix(frontend): Fix connected property

* fix(frontend): fix connected property

* fix(frontend): fix connected property
2023-04-06 16:02:10 +02:00
Ruben Fiszel
3a6b655ba8 fix server starting failed 2023-04-06 11:46:47 +02:00
Ruben Fiszel
2f156d09bf fix loading script from hub 2023-04-06 09:13:15 +02:00
Ruben Fiszel
b84be60c53 feat(frontend): make script editor a single page 2023-04-06 01:00:42 +02:00
Ruben Fiszel
eef9017a05 remove import from template 2023-04-05 22:14:15 +02:00
Ruben Fiszel
eff61bb8d3 fix(backend): allow longer name/company 2023-04-05 22:01:18 +02:00
Ruben Fiszel
8a594a89ad fix(backend): allow cors 2023-04-05 20:36:01 +02:00
Ruben Fiszel
fb60768cf3 fix star hover issue 2023-04-05 19:38:20 +02:00
Ruben Fiszel
8f7a11b896 feat(frontend): add workspace to ctx 2023-04-05 19:31:12 +02:00
Ruben Fiszel
0b4da1a97c minor schedule nits 2023-04-05 19:21:13 +02:00
Ruben Fiszel
f6d14f7fc3 minor schedule nits 2023-04-05 19:16:00 +02:00
Ruben Fiszel
449e7de71a fix search outputs in apps with null object 2023-04-05 18:53:57 +02:00
Ruben Fiszel
95ed99a1d2 fix search outputs in apps with null object 2023-04-05 18:52:41 +02:00
Ruben Fiszel
8c72722710 fix(backend): inline script app python fix 2023-04-05 18:26:27 +02:00
Oliver Veal
17176bb8d1 feat: improved cron/schedule editor (#1362)
* basic cron schedule editing ui

* schedules run in a user-specified timezone

* fix other uses of CronInput component

* use now() from database to schedule next job

* offset -> IANA timezone conversion on db migration

* sqlx ci

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-04-05 16:37:24 +02:00
Ruben Fiszel
4fb7468cf3 do no keep tar around and move it from the tmp folder 2023-04-05 15:02:37 +02:00
Faton Ramadani
51fc436456 fix(frontend): Fix flow templateEditor (#1367) 2023-04-05 13:06:56 +02:00
Ruben Fiszel
7f9050b285 feat(backend): lowercase all emails in relevant endpoints (#1361)
* all

* also modify invite_user
2023-04-05 11:36:17 +02:00
Ruben Fiszel
94eecea02b feat(backend): add /ready endpoint for workers 2023-04-05 10:34:25 +02:00
jneeee
4ec035b09a fix: no need to map internal ports to hosts (#1365) 2023-04-05 08:01:43 +02:00
Ruben Fiszel
922682c4d0 initialize cache if from tar 2023-04-04 16:31:47 +02:00
Faton Ramadani
831ff60bdf App input (#1353)
* feat(frontend): record frontend errors

* feat(frontend): display result

* feat(frontend): Fix name shadowing

* feat(frontend): fix typo

* fix(frontend): better display frontend errors

* fix(frontend): wip

* wip

* feat(frontend): text input

* feat(frontend): enable double click to open + disabled mode when input is computed or connected

* feat(frontend): fix monaco setCode

* feat(frontend): revert package.json changes

* feat(frontend): fix syncing issues

* feat(frontend): adapt style

* feat(frontend): fix event propagation
2023-04-04 16:00:50 +02:00
Ruben Fiszel
b86ca29fde fix polar handling 2023-04-04 14:25:27 +02:00
Ruben Fiszel
15c75d9d00 feat(backend): add GET endpoint to trigger scripts 2023-04-04 13:56:55 +02:00
Ruben Fiszel
096bf2022c kill the background bucket sync if necessary 2023-04-04 11:33:07 +02:00
Ruben Fiszel
bc4dd0eeaa only sync after the cache has been initialized 2023-04-04 11:03:25 +02:00
Ruben Fiszel
ae219eb3be create the first dirs properly 2023-04-04 09:29:08 +02:00
Ruben Fiszel
b851a5c65a first sync of the cache is at least after the first sync duration 2023-04-04 09:06:19 +02:00
Ruben Fiszel
bc36f5b309 log the entirecache.tar length 2023-04-04 08:32:20 +02:00
Ruben Fiszel
5b0a4d7838 only copy cache from bucket in background the first time 2023-04-04 08:24:02 +02:00
dependabot[bot]
f358aa5fe2 chore(deps-dev): bump cssnano from 5.1.15 to 6.0.0 in /frontend (#1359)
Bumps [cssnano](https://github.com/cssnano/cssnano) from 5.1.15 to 6.0.0.
- [Release notes](https://github.com/cssnano/cssnano/releases)
- [Commits](https://github.com/cssnano/cssnano/compare/cssnano@5.1.15...cssnano@6.0.0)

---
updated-dependencies:
- dependency-name: cssnano
  dependency-type: direct:development
  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-04-04 01:34:22 +02:00
Ruben Fiszel
3e5ff8682a feat: do cache bucket syncing in background + check tar before pushing it (#1360)
* all

* improve tar caching
2023-04-04 01:34:11 +02:00
dependabot[bot]
0cbefd8214 chore(deps-dev): bump @sveltejs/adapter-static in /frontend (#1357)
Bumps [@sveltejs/adapter-static](https://github.com/sveltejs/kit/tree/HEAD/packages/adapter-static) from 1.0.6 to 2.0.1.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/master/packages/adapter-static/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/adapter-static@2.0.1/packages/adapter-static)

---
updated-dependencies:
- dependency-name: "@sveltejs/adapter-static"
  dependency-type: direct:development
  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-04-03 21:07:56 +02:00
Ruben Fiszel
517b2c9cca fix(backend): always flush bash output 2023-04-03 20:07:43 +02:00
Ruben Fiszel
7a9091fed6 fix(backend): always flush bash output 2023-04-03 19:56:55 +02:00
Faton Ramadani
2819b09ce5 fix(frontend): Add FlowGraph display on Safari (#1351) 2023-04-03 16:39:57 +02:00
Ruben Fiszel
ef0165e419 chore(main): release 1.85.0 (#1348)
* chore(main): release 1.85.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-04-03 13:30:18 +02:00
Ruben Fiszel
be97be2c58 update lsp to not report imports errors 2023-04-03 13:26:00 +02:00
Ádám Kovács
daf827666b fix(frontend): PDF reader header positioning (#1350) 2023-04-03 11:56:38 +02:00
Ruben Fiszel
8c53598aba fix(backend): improve handling subflow with many depth using tailrec 2023-04-03 08:37:24 +02:00
Ruben Fiszel
6f33d549f0 improve mem handling for update_flow_status 2023-04-03 01:13:39 +02:00
Ruben Fiszel
390a988d4c fix(backend): improve subflow processing 2023-04-03 00:32:13 +02:00
Ruben Fiszel
8a8316c316 remove unecessary debug inputtransform 2023-04-02 22:20:05 +02:00
Ruben Fiszel
c638c511ca set ready to undefined for agGridTable 2023-04-02 18:29:49 +02:00
dependabot[bot]
59403fbe5d chore(deps): bump actions/setup-go from 3 to 4 (#1309)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  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-04-02 17:42:56 +02:00
Ruben Fiszel
7eed0b4666 update highlight + fix transform editor suggestions 2023-04-02 17:41:43 +02:00
Ruben Fiszel
4127ffe00c chore: update search library 2023-04-02 17:16:23 +02:00
Ruben Fiszel
3d9dfa645b update svelte-awsome 2023-04-02 17:08:37 +02:00
Ruben Fiszel
7c0de93b3d remove all frontend warnings and unused imports 2023-04-02 17:02:57 +02:00
Ruben Fiszel
de1e29492c feat(frontend): add agGrid api hooks + ready 2023-04-02 16:21:04 +02:00
Ádám Kovács
0b8a08cb49 feat(frontend): Add ID renaming popup (#1344)
* feat(frontend): Add id renaming popup

* fix(frontend): State reset

* actually do it

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-04-02 15:46:24 +02:00
Ruben Fiszel
4f7c45118b remove backend warnings & fix test 2023-04-02 13:56:16 +02:00
Ruben Fiszel
cdbab5c807 remove all frontend warnings 2023-04-02 13:52:15 +02:00
Ruben Fiszel
d4927cf757 signatures that cannot be parsed return 200 2023-04-02 12:47:38 +02:00
Ruben Fiszel
492f22526a refresh folders more aggressively in case a non non just created one 2023-04-02 12:08:52 +02:00
Ruben Fiszel
89c2fb41dd can read all folders if admin or superadmin + refresh user in background 2023-04-02 10:35:22 +02:00
Ruben Fiszel
018b051781 feat: add local cache for folder path used + invalidate cache on folder creation 2023-04-01 19:17:29 +02:00
Ruben Fiszel
c19be7a2fa ci: extract to s3 directly 2023-04-01 15:38:58 +02:00
Ruben Fiszel
aa3a3f6612 ci: extract to s3 directly 2023-04-01 15:36:26 +02:00
Ruben Fiszel
5b8c6bb35d fix compile 2023-04-01 15:07:32 +02:00
Ruben Fiszel
8d487c0ddb fix lack of suggestions in flow editor 2023-04-01 14:53:45 +02:00
Ruben Fiszel
efea19496f update vite and sveltekit 2023-04-01 13:22:12 +02:00
Ruben Fiszel
ab99950c5d fix cycle in imports 2023-04-01 01:40:19 +02:00
Ruben Fiszel
2062dc6c44 chore(main): release 1.84.1 (#1345)
* chore(main): release 1.84.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-31 21:07:04 +02:00
Ruben Fiszel
b6d5eef547 fix(cli): overwrite instead of smart diff 2023-03-31 21:02:37 +02:00
Ruben Fiszel
46d2c86b37 when pulling, overwrite or not but do not merge 2023-03-31 21:01:40 +02:00
Ruben Fiszel
8d73c9276e chore(main): release 1.84.0 (#1336)
* chore(main): release 1.84.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-31 20:53:24 +02:00
Faton Ramadani
57f8dd9570 feat(frontend): Display frontend execution result in Debug Runs (#1341)
* feat(frontend): record frontend errors

* feat(frontend): display result

* feat(frontend): Fix name shadowing

* feat(frontend): fix typo

* fix(frontend): better display frontend errors
2023-03-31 15:20:16 +02:00
Ruben Fiszel
5a8e00d285 remove unecessary deno lock file 2023-03-31 11:53:31 +02:00
Ruben Fiszel
f217a2c368 fix input_transform -> input_transforms tests 2023-03-31 11:27:37 +02:00
Ádám Kovács
2779891411 fix(frontend): Export python code as string (#1339) 2023-03-31 11:09:53 +02:00
Ryan Rich
dfd2abc764 fix(backend): backend compatability on macos (#1340) 2023-03-31 09:00:54 +02:00
Ruben Fiszel
624279e568 fix compile 2023-03-31 00:30:10 +02:00
Ruben Fiszel
cd1f9b6baa fix(frontend): improve app tables 2023-03-31 00:19:14 +02:00
Ruben Fiszel
8b4e828e64 add license 2023-03-30 19:48:18 +02:00
Ruben Fiszel
2e7871439a consistent sort + minor nits 2023-03-30 17:27:21 +02:00
Ruben Fiszel
82578ef836 minor ux nits 2023-03-30 16:15:02 +02:00
Ruben Fiszel
b3254938fe fix(frontend): improve loading of big jobs in run form 2023-03-30 13:14:39 +02:00
Ruben Fiszel
71619acdfa fix(frontend): improve loading of big args in job details 2023-03-30 12:59:11 +02:00
Ruben Fiszel
c7506e4dae feat(backend): reduce memory allocation for big forloops of flows 2023-03-30 11:52:54 +02:00
Ruben Fiszel
2368da2146 feat: add the ability to edit previous versions 2023-03-30 08:25:00 +02:00
Ruben Fiszel
7fc97e274e only decrypt secret value 2023-03-30 07:58:51 +02:00
Ruben Fiszel
4f75a5840a redeploy 2023-03-30 01:41:37 +02:00
Ruben Fiszel
8b80b3cb74 fix cancel job 2023-03-30 01:20:54 +02:00
Ruben Fiszel
31d0d102eb use requests for . imports 2023-03-30 01:07:24 +02:00
Ruben Fiszel
fbe5c18da0 feat: add force cancel 2023-03-30 00:42:13 +02:00
Ruben Fiszel
8d0d996bbb fix flow connection 2023-03-29 22:01:12 +02:00
Ruben Fiszel
2b09fead4f Update deploy-to-s3.yml (#1338) 2023-03-29 20:31:09 +02:00
Ruben Fiszel
fccb3d8088 truncate file too big 2023-03-29 20:04:31 +02:00
Ruben Fiszel
9aaeaf4ee0 truncate file too big 2023-03-29 20:03:17 +02:00
Ádám Kovács
01564f0a1c feat(frontend): App component style input grouping (#1334)
* feat(frontend): App component style input grouping

* fix: default value

* fix

* update styling

* fix property toggling

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-03-29 19:24:20 +02:00
Ruben Fiszel
78085a8a12 deploy main to s3 2023-03-29 17:09:56 +02:00
Ruben Fiszel
5abd9854ad deploy main to s3 2023-03-29 17:05:42 +02:00
Ruben Fiszel
85e9aa983b deploy main to s3 2023-03-29 16:50:30 +02:00
Ruben Fiszel
9853380df6 deploy main to s3 2023-03-29 16:46:36 +02:00
Ruben Fiszel
5aa14562a0 handle space and uppercase in script_path 2023-03-29 16:16:14 +02:00
Ruben Fiszel
c07a17ff8a improve focusedGrid in tabs and drawers 2023-03-29 10:34:31 +02:00
Faton Ramadani
5ac646e859 feat(frontend): improve input connection UI (#1333)
* feat(frontend): improve input connection UI

* feat(frontend): prevent pointerup from bubbling + refactor the code

* feat(frontend): remove unnecessary alert + fix null display

* feat(frontend): restore hoveredComponent when connecting + properly open deeply nested component

* feat(frontend): fix ObjectViewer display
2023-03-29 09:05:50 +02:00
Ruben Fiszel
bb61cef0e5 fix(backend): add a refresh button to workspace script/hub 2023-03-29 01:06:24 +02:00
Ruben Fiszel
f73664759f chore(main): release 1.83.1 (#1335)
* chore(main): release 1.83.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-28 23:38:12 +02:00
Ruben Fiszel
569a55e45b fix(cli): plain secrets might be undefined 2023-03-28 23:32:41 +02:00
Ruben Fiszel
5d53967ba4 fix compile issue swc_common 2023-03-28 23:13:43 +02:00
Ruben Fiszel
a24a3b4787 chore(main): release 1.83.0 (#1327)
* chore(main): release 1.83.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-28 22:50:27 +02:00
Ruben Fiszel
98d51e219d fix(cli): add --plain-secrets 2023-03-28 22:46:43 +02:00
Ruben Fiszel
11431a75f4 fix python imports 2023-03-28 21:51:12 +02:00
Ruben Fiszel
32ef18bffe fix tests 2023-03-28 21:40:36 +02:00
Ruben Fiszel
8497d1d1c0 fix autogen script names 2023-03-28 21:07:21 +02:00
Ruben Fiszel
015f8e893f fix autogen script names 2023-03-28 20:43:51 +02:00
Ruben Fiszel
07ab2dbb0a fix autogen script names 2023-03-28 20:39:58 +02:00
Ruben Fiszel
a5500ea40a feat(backend): allow relative imports for python 2023-03-28 20:27:40 +02:00
Ruben Fiszel
5eab9431bd fix integer handling as field type 2023-03-28 17:36:21 +02:00
Ruben Fiszel
33c07d3e63 feat(frontend): add pagination 2023-03-28 16:58:43 +02:00
Ruben Fiszel
5f335d2464 more flow logs 2023-03-28 14:01:47 +02:00
Ruben Fiszel
c0d72e2881 improve logging to trace job from root_job and fetching flow details from a separate workspace 2023-03-28 07:58:35 +02:00
Ruben Fiszel
e40f16c969 minor backend fixes 2023-03-27 22:57:29 +02:00
Ruben Fiszel
1d63877a69 improve flow renaming 2023-03-27 21:16:43 +02:00
Ruben Fiszel
6764c519b2 add more sanity checks to api 2023-03-27 21:07:34 +02:00
Ruben Fiszel
3463bfe36f fix array list removal 2023-03-27 20:51:52 +02:00
Ruben Fiszel
2b31653a8a feat(frontend): add textareacomponent + fix multiselect style + select multi components 2023-03-27 18:33:50 +02:00
Ruben Fiszel
449d3ae5dd feat(frontend): add recompute as a primitive 2023-03-27 17:41:37 +02:00
Ruben Fiszel
cfa3f9ce7c fix recomputeOthers 2023-03-27 15:49:24 +02:00
Ruben Fiszel
201aa6d088 improve app setup 2023-03-27 15:24:13 +02:00
Ruben Fiszel
021fa23f9f feat(backend): execute /bin/bash instead of /bin/sh for bash scripts 2023-03-27 13:08:34 +02:00
Ruben Fiszel
b95afaa9bb feat(frontend): use rich json editor for arrays of objects and for object in ArgInput 2023-03-27 13:03:06 +02:00
Ruben Fiszel
83e982e84d minor fix 2023-03-27 11:54:30 +02:00
Ádám Kovács
fa457bb709 fix(frontend): Revert app upload input (#1330)
* fix(frontend): Revert app upload input

* fix default value
2023-03-27 11:51:30 +02:00
Faton Ramadani
75306c8316 fix(frontend): Small app fixes (#1331)
* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): merge main

* fix(frontend): fix build

* fix(frontend): revert css

* fix(frontend): fix alignement + remove id from  if deleted

* fix(frontend): fix connection height

* fix(frontend): done
2023-03-27 11:50:40 +02:00
Ruben Fiszel
eaac598af3 feat(backend): improve relative importsfor deno 2023-03-27 08:45:35 +02:00
Ruben Fiszel
00b70d9aaa feat(backend): increase timeout for premium workspace 2023-03-26 18:56:40 +02:00
Ruben Fiszel
9b09fac27a add more variants to shadow 2023-03-26 18:17:47 +02:00
Ruben Fiszel
6ed7268258 many apps small fixes 2023-03-26 18:14:37 +02:00
Ruben Fiszel
014765c83b fix redraw issues 2023-03-25 20:35:28 +01:00
Ruben Fiszel
577dec5c57 feat(frontend): multiselect components for apps 2023-03-25 16:54:50 +01:00
Ruben Fiszel
9ab087a20c make cancel api an optauthed 2023-03-25 08:39:56 +01:00
Ruben Fiszel
1a4867302f fix(frontend): persist description for schemas 2023-03-25 08:28:27 +01:00
Ruben Fiszel
8e3d8acc80 fix(apps): improve app table actionButtons behavior under many clicks 2023-03-25 07:59:32 +01:00
Ádám Kovács
ac2486219c feat(frontend): Add quick style settings to app editor (#1308)
* feat(frontend): Add app secondary settings menu

* refactor(frontend): Separate color picker

* save

* feat(frontend): Add quick style options

* fix(frontend): Handle overflow

* feat(frontend): Add suggestions to quick styles

* save wip

* feat(frontend): Add UI for quick styling

* fix(frontend): Handle multi value properties

* fix(frontend): Convenience updates

* feat(frontend): Add styling properties to components

* fix(frontend): Parse inner colors

* fix(frontend): Multi value sync

* fix(frontend): Correct unit handling

* fix(frontend): Correct multivalue handling

* remove comments

* fix color picker label

* feat(frontend): Add box-shadow property

* feat(frontend): Add concise unit selector

* feat(frontend): Update app labels

* remove unused imports

* fix width

* fix(frontend): App freezing

* fix(frontend): Remove unused imports

* fix(frontend): Conditional chaining

* fix

* revert

* minor updates

* feat(frontend): Add 'apply style to all' button

* fix(frontend): Update styling properties

* fix(frontend): Styling menu typography update
2023-03-24 18:26:16 +01:00
Faton Ramadani
a527cb8222 fix(frontend): add a modal that is always mounted to make sure compon… (#1328)
* fix(frontend): add a modal that is always mounted to make sure component binding are properly set

* fix(frontend): remove uselss open prop
2023-03-24 17:30:04 +01:00
Ádám Kovács
da24e9ab06 fix(frontend): Disable app keyboard navigation on focused inputs (#1326) 2023-03-24 16:48:34 +01:00
Ruben Fiszel
4dc00c2587 improve require super_admin check error 2023-03-24 14:55:56 +01:00
Ruben Fiszel
45c52f7723 make superadmin_secret work also for apis 2023-03-24 14:53:28 +01:00
Ruben Fiszel
70a7089352 make superadmin_email permission be inherited by ephemeral tokens 2023-03-24 13:22:56 +01:00
Ruben Fiszel
895609f0d2 fix backend compile 2023-03-24 12:54:20 +01:00
Ruben Fiszel
dd06c05046 chore(main): release 1.82.0 (#1316)
* chore(main): release 1.82.0

* Apply automatic changes

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-24 12:33:33 +01:00
Ruben Fiszel
a86fad6a9e fix change versions for cli 2023-03-24 12:25:30 +01:00
Ruben Fiszel
f7c30b5d2f fix(backend): do not consider FlowPreview as potential zombie job 2023-03-24 12:12:51 +01:00
Ruben Fiszel
b999c9894b fix(cli): improve diff speed + fix replacing cli 2023-03-24 09:28:45 +01:00
Ruben Fiszel
a2de6c7d5f recompute others for modal work even if no script attached 2023-03-23 21:10:18 +01:00
Ruben Fiszel
15812b4cec fix compile 2023-03-23 21:05:51 +01:00
Faton Ramadani
b22600e9c3 Fix settings panel v2 (#1325)
* fix(frontend): fix settings panel

* fix(frontend): fix settings panel

* fix(frontend): fix settings panel
2023-03-23 20:55:57 +01:00
Faton Ramadani
c15bc8a7bf fix(frontend): Fix AppTable error display + clear errors when removing a component + properly detect that latest component run had an error (#1322) 2023-03-23 20:54:02 +01:00
Faton Ramadani
30b8e474df fix(frontend): fix settings panel (#1323)
* fix(frontend): fix settings panel

* fix(frontend): fix settings panel

* fix(frontend): fix settings panel
2023-03-23 20:53:07 +01:00
Ruben Fiszel
f420999363 fix recomputeOthers 2023-03-23 17:55:21 +01:00
Ruben Fiszel
984c9a1191 fix backend timeout issues 2023-03-23 17:22:35 +01:00
Ruben Fiszel
a2df650936 fix backend timeout issues 2023-03-23 15:59:28 +01:00
Ruben Fiszel
c0076c652d fix same worker query 2023-03-23 15:57:52 +01:00
Faton Ramadani
addabcceb0 fix(frontend): Fix transformer (#1321) 2023-03-23 14:20:15 +01:00
Ruben Fiszel
47a7f7163a feat(backend): introduce RESTART_ZOMBIE_JOBS and ZOMBIE_JOB_TIMEOUT 2023-03-23 12:47:49 +01:00
Ruben Fiszel
34e25f0f96 fix(backend): increase dynamic js timeout + improve client passing 2023-03-23 10:32:09 +01:00
Ruben Fiszel
93ce252954 fix caddyfile to re-enable lsp 2023-03-22 21:55:59 +01:00
Ruben Fiszel
d3effe953b re-enable transformers 2023-03-22 21:53:28 +01:00
Ruben Fiszel
d935dba28b fix add property not in portal 2023-03-22 21:47:59 +01:00
Ruben Fiszel
58167a16cd refresh in more cases 2023-03-22 21:37:49 +01:00
Ruben Fiszel
9e9683c6f7 refresh in more cases 2023-03-22 21:29:24 +01:00
Ruben Fiszel
7511f0b18e add red toggle 2023-03-22 20:06:38 +01:00
Ruben Fiszel
c5d305bad8 fix runnableComponent even if not changed on inputs 2023-03-22 20:01:17 +01:00
Ruben Fiszel
b4008e62fd clarify trigger list 2023-03-22 19:41:33 +01:00
Ruben Fiszel
bb227b69c8 add keys to switch scripts 2023-03-22 19:33:37 +01:00
Ruben Fiszel
5518eab7b7 fix disappearing scripts 2023-03-22 19:30:11 +01:00
Faton Ramadani
a47031a41e fix(frontend): fix refresh with manual dependencies (#1319)
* fix(frontend): fix refresh with manual dependencies

* fix(frontend): fix id generation

* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): Fix binding + hide toggle for frontend scripts
2023-03-22 18:27:38 +01:00
Faton Ramadani
e193a0bcdf fix(frontend): remove unnecessary div (#1318) 2023-03-22 14:40:29 +01:00
Ruben Fiszel
2df1373a69 fix compile error 2023-03-22 12:03:48 +01:00
Ruben Fiszel
c6bf67605d chore(main): release 1.81.0 (#1314)
* chore(main): release 1.81.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-21 23:15:55 +01:00
Faton Ramadani
0086f99dcc fix(frontend): fix tabs height (#1315) 2023-03-21 23:15:22 +01:00
Ruben Fiszel
bba09fdaeb fix(cli): make --skip-pull work even if no state is present 2023-03-21 23:14:56 +01:00
Ruben Fiszel
942d2b2244 remove do from acceptable ids 2023-03-21 21:20:21 +01:00
Ruben Fiszel
8a2e6365a0 improve load time for tabs 2023-03-21 20:56:54 +01:00
Ruben Fiszel
527f4b543a improve load time for text and button components 2023-03-21 19:55:35 +01:00
Ruben Fiszel
2593218cbf feat(apps): add action on form/button/formbutton 2023-03-21 18:22:20 +01:00
dependabot[bot]
bfb5c1b5a4 chore(deps-dev): bump svelte2tsx from 0.6.1 to 0.6.10 in /frontend (#1311)
Bumps [svelte2tsx](https://github.com/sveltejs/language-tools) from 0.6.1 to 0.6.10.
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte2tsx-0.6.1...svelte2tsx-0.6.10)

---
updated-dependencies:
- dependency-name: svelte2tsx
  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-03-21 13:05:04 +01:00
Faton Ramadani
a7c4f1a12e fix(frontend): Remove action outline on preview mode (#1313) 2023-03-21 13:04:43 +01:00
Ruben Fiszel
7cb363845e chore(main): release 1.80.1 (#1312)
* chore(main): release 1.80.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-21 02:13:02 +01:00
Ruben Fiszel
652d3c3889 fix initialization for app with forms 2023-03-21 02:11:34 +01:00
Ruben Fiszel
42f6d2e0ee fix(cli): add support for non metadataed scripts 2023-03-21 02:01:34 +01:00
Ruben Fiszel
d2cccd98e0 chore(main): release 1.80.0 (#1305)
* chore(main): release 1.80.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-21 01:00:29 +01:00
Ruben Fiszel
cc4d61b6bd small fix 2023-03-20 19:54:04 +01:00
Ruben Fiszel
fbdda1a4dd make top bar togglable 2023-03-20 19:25:39 +01:00
Faton Ramadani
48413a78c5 feat(frontend): App set tab (#1307)
* feat(frontend): Set tab

* feat(frontend): Set tab

* feat(frontend): fix styling

* feat(frontend): update how set Tab is stored

* feat(frontend): clean up

* feat(frontend): update setTab to an array structure

* feat(frontend): revert

* feat(frontend): support all components

* feat(frontend): fix typing

* feat(frontend): group success side effects

* feat(frontend): add missing tooltip + remove duplicate code
2023-03-20 19:09:56 +01:00
Ruben Fiszel
be1d987b41 fix table action input fields 2023-03-20 16:04:50 +01:00
Ruben Fiszel
0f64859961 fix minor app issues 2023-03-20 15:27:30 +01:00
Ruben Fiszel
b762de1eae revert caddyfileremote change 2023-03-20 14:04:18 +01:00
Ruben Fiszel
b00bde0a63 fix preview of ontextfocus 2023-03-20 14:01:34 +01:00
Ruben Fiszel
f972e4bb06 improve drag for apps 2023-03-20 10:58:20 +01:00
Faton Ramadani
29b1cc6ff0 fix(frontend): add missing optional chaining (#1306) 2023-03-20 09:49:11 +01:00
Ruben Fiszel
b51246411f use skeleton for loading in apps 2023-03-20 08:49:51 +01:00
Ruben Fiszel
f26c7ff62b minor app improvements 2023-03-20 08:35:44 +01:00
Ruben Fiszel
211ad52edb minor app improvements 2023-03-20 08:26:02 +01:00
Ruben Fiszel
1392bebf87 improve move animation 2023-03-19 23:12:01 +01:00
Ruben Fiszel
874cf412a1 minor app fixes 2023-03-19 22:37:00 +01:00
Ruben Fiszel
b497c3463f minor app fixes 2023-03-19 15:35:40 +01:00
Ruben Fiszel
fbe2f0ca93 fix when loading is set to true 2023-03-19 15:25:46 +01:00
Ruben Fiszel
0abacac06c feat(apps): add transformers for data sources 2023-03-19 15:16:31 +01:00
Ruben Fiszel
8fab191a7f apps load when all outputs are initialized 2023-03-18 20:25:21 +01:00
Ruben Fiszel
f8fad8326d more typescript type safety tricks 2023-03-18 18:15:15 +01:00
Ruben Fiszel
3b84672363 minor app fixes 2023-03-18 16:25:34 +01:00
Ruben Fiszel
e45917c020 fix lock + reactivity on ctrl + fix optionValuesKeys 2023-03-18 15:52:46 +01:00
Ruben Fiszel
d570ef58ac improve type definition of apps and sync them with static components + purge app content of unecessary data 2023-03-18 15:29:42 +01:00
Faton Ramadani
cf2d031e8e fix(frontend): App button triggered by (#1304)
* feat(frontend): add trigger list

* feat(frontend): add trigger list

* feat(frontend): add support for refreshOn

* feat(frontend): add support for refreshOn

* feat(frontend): rework

* feat(frontend): rework

* feat(frontend): code cleanup

* fix(frontend): add support for triggerOnAppLoad
2023-03-17 21:55:40 +01:00
Ruben Fiszel
9657cc9c7e fix compile error after deno upgrade 2023-03-17 21:45:54 +01:00
Ruben Fiszel
200adec32f chore(main): release 1.79.0 (#1295)
* chore(main): release 1.79.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-17 21:10:44 +01:00
Ruben Fiszel
7625782038 fix apps bar not being persistent 2023-03-17 21:08:45 +01:00
Ruben Fiszel
4242f1bb63 fix cli failing on script with non json files 2023-03-17 21:01:44 +01:00
Ruben Fiszel
f1e718e718 fix cli failing on script with non json files 2023-03-17 20:58:13 +01:00
Ruben Fiszel
f3dfad5b94 fix loading nested resource in client fetched resources 2023-03-17 20:54:37 +01:00
Faton Ramadani
078cb1bf3e feat(frontend): App component triggers (#1303)
* feat(frontend): add trigger list

* feat(frontend): add trigger list

* feat(frontend): add support for refreshOn

* feat(frontend): add support for refreshOn

* feat(frontend): rework

* feat(frontend): rework

* feat(frontend): code cleanup
2023-03-17 20:51:00 +01:00
Ruben Fiszel
97e3bb4aa8 fix reactivity for static inputs 2023-03-17 20:50:34 +01:00
Faton Ramadani
c1e43de4ea Fix sub grid (#1302)
* fix(frontend): fix horizontal splitpanes

* fix(frontend): fix subgrid height

* fix(frontend): fix subgrid height
2023-03-17 15:56:26 +01:00
Faton Ramadani
ea3dab411b fix(frontend): fix horizontal splitpanes (#1301) 2023-03-17 15:46:45 +01:00
Ruben Fiszel
a76f6f7bd9 apps rich configuration v0 + resource picker clear fix for apps 2023-03-17 12:25:03 +01:00
Ruben Fiszel
4305670d90 fix app inputs wrt to subgrids 2023-03-16 16:50:01 +01:00
Ruben Fiszel
597e38ef36 feat(frontend): add listeners for frontend scripts 2023-03-16 14:56:16 +01:00
Ruben Fiszel
ca3e3624c0 fix key left navigation 2023-03-16 12:48:31 +01:00
Ádám Kovács
c1dd35c3f0 fix(frontend): App panel styling (#1284)
* fix(frontend): App panel styling

* remove

* revert

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-03-16 12:42:41 +01:00
Faton Ramadani
bd927a27ed feat(frontend): Component control (#1293)
* fix(frontend): fix app tabs

* fix(frontend): app controls

* fix(frontend): app controls

* fix(frontend): align output panel

* fix(frontend): clean up

* fix(frontend): refactor events

* fix(frontend): fix display

* fix(frontend): fix indentation

* fix(frontend): merge main
2023-03-16 12:42:10 +01:00
Faton Ramadani
00927210fd fix(frontend): fix map render (#1297)
* fix(frontend): fix map render

* fix(frontend): fix map render
2023-03-16 12:39:12 +01:00
Ádám Kovács
bd3ee81b14 fix(frontend): Display app context search on top (#1300)
Fixing z-index issue of the search bar in the outputs panel
2023-03-16 12:38:35 +01:00
Ádám Kovács
bac831b23c fix(frontend): Hide archive toggle with empty list (#1296) 2023-03-16 12:36:56 +01:00
Faton Ramadani
c3ba1a6ab9 feat(frontend): add table actions navigation (#1298)
* feat(frontend): add table actions navigation

* feat(frontend): add table actions navigation

* feat(frontend): add table actions navigation
2023-03-16 12:36:33 +01:00
Ruben Fiszel
52157faf72 chore(main): release 1.78.0 (#1292)
* chore(main): release 1.78.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-16 03:11:38 +01:00
Ruben Fiszel
a9e8aa0f1c update cli error message for folders 2023-03-16 03:10:45 +01:00
Ruben Fiszel
50c1c614ef context panel transitions 2023-03-16 03:00:01 +01:00
Ruben Fiszel
11567d6280 feat(frontend): app textcomponent editable + tooltip 2023-03-16 02:34:39 +01:00
Ruben Fiszel
a6e1510405 fix minor flow UX 2023-03-16 01:18:11 +01:00
Ruben Fiszel
b7d8fd1a4d apps minor fixes 2023-03-16 00:55:45 +01:00
Ruben Fiszel
e27de7fb5d lightarginput for apps forms 2023-03-16 00:13:09 +01:00
Ruben Fiszel
99ec12e10c various minor fixes 2023-03-15 23:31:35 +01:00
Ruben Fiszel
9bfd471439 context panel refactor 2023-03-15 22:24:13 +01:00
Ruben Fiszel
dbdfd62638 fix(frontend): remove staticOutputs from apps 2023-03-15 17:22:46 +01:00
Ruben Fiszel
6f890f2120 fix(frontend): improve rendering performance after component moving 2023-03-15 12:35:11 +01:00
Ruben Fiszel
183a4591df fix(backend): whitelist for include_header was ignored in some cases 2023-03-14 23:47:57 +01:00
Ruben Fiszel
646c0f23da chore(main): release 1.77.0 (#1286)
* chore(main): release 1.77.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-14 22:27:08 +01:00
Ádám Kovács
dea12e8870 fix(frontend): Update setting accordion (#1285)
* fix(frontend): Update setting accordion

* remove empty tooltip

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-03-14 22:24:23 +01:00
Ryan Rich
944795f6ee feat(deno): add support for custom npm repo (#1291)
* feat(deno): add support for custom npm repo

* feat(deno): add support for custom npm repo

* feat(deno): fix comment being on wrong line

* feat(deno): review feedback
2023-03-14 22:23:44 +01:00
Ruben Fiszel
65d4bc519c add env build configuration 2023-03-14 22:20:45 +01:00
Ruben Fiszel
4d3507aec2 add .env.production to .gitignore 2023-03-14 22:19:08 +01:00
Ruben Fiszel
1d395ccc17 add env build configuration 2023-03-14 22:16:22 +01:00
hcourdent
3a7129de4b Added tooltips to App editor (#1289) 2023-03-14 22:05:34 +01:00
Ruben Fiszel
2f0acb9ffa feat(apps): state can be used as input in apps 2023-03-14 20:44:14 +01:00
Ruben Fiszel
81f989837b improve reactivity of apps 2023-03-14 18:46:31 +01:00
Faton Ramadani
c71a577fea fix(frontend): fix app tabs (#1288) 2023-03-14 16:11:07 +01:00
Faton Ramadani
bc870bd03e fix(frontend): fix container deletion (#1287)
* fix(frontend): fix container deletion

* fix(frontend): fix container deletion
2023-03-14 14:57:44 +01:00
Faton Ramadani
751edcf9b8 feat(frontend): app components output panel (#1283)
* feat(frontend): hierarchical output panel WIP

* feat(frontend): wip

* feat(frontend): working animations

* feat(frontend): working animations

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): improving connection

* feat(frontend): fix search

* feat(frontend): output panel v2

* feat(frontend): support table actions

* feat(frontend): support table actions

* feat(frontend): support background script

* feat(frontend): fix background scripts

* feat(frontend): simplify code

* feat(frontend): fix animation

* feat(frontend): fix wording

* feat(frontend): fix bg script click

* feat(frontend): fix bg script click

* feat(frontend): fix bg script click
2023-03-14 14:42:40 +01:00
Ruben Fiszel
c2a97c53cf feat(frontend): add setTab to frontend scripts 2023-03-14 14:42:26 +01:00
Ruben Fiszel
eb73f2a687 fix(backend): do not cache reference to workspace scripts 2023-03-14 14:16:07 +01:00
Ruben Fiszel
cd645d0935 feat(apps): tabs can be made pages or invisible + better frontend scripts reactivity 2023-03-14 12:34:08 +01:00
Ruben Fiszel
ac9bd7ef8c fix flow viewer select event 2023-03-14 00:11:37 +01:00
Ruben Fiszel
5dae6577b8 chore(main): release 1.76.0 (#1280)
* chore(main): release 1.76.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-13 23:41:53 +01:00
Ruben Fiszel
372f53b7fe improve refresh and background script behavior 2023-03-13 23:38:14 +01:00
Ruben Fiszel
5662fa0d09 fix compile 2023-03-13 20:59:27 +01:00
Ruben Fiszel
c958480ce8 fix(backend): improve worker ping api 2023-03-13 20:28:11 +01:00
Ruben Fiszel
f0b1b1f752 feat(frontend): add frontend (JS) scripts to apps 2023-03-13 19:29:35 +01:00
Ruben Fiszel
b8e6767cca improve error messages for no workspace 2023-03-13 16:44:38 +01:00
Ruben Fiszel
75f87e7e11 feat(frontend): disabled for action buttons can now depend on row 2023-03-13 14:36:42 +01:00
Ádám Kovács
3e5a179eb8 fix(frontend): General fixes and updates (#1281)
* fix(frontend): App icon select double click issue

* fix(frontend): Update script metadata page

* fix(frontend): Set dropdown default icon to vertical dots

* fix(frontend): Clean up

* fix(frontend): Update table styles

* fix(frontend): Add spacing to secondary menu items

* fix(frontend): Scale down full path

* fix(frontend): Table loading state

* fix(frontend): Hide script kind setting by default
2023-03-13 14:17:57 +01:00
Ruben Fiszel
c082c6350e fix runs reload 2023-03-13 13:00:23 +01:00
Ruben Fiszel
cfd489a550 feat(frontend): improve drag-n-drop behavior 2023-03-13 12:44:39 +01:00
Ruben Fiszel
1f4ae53fb4 integrate svelte-grid in codebase 2023-03-13 10:01:02 +01:00
Ruben Fiszel
0dcbf270da small app fixes 2023-03-12 21:32:41 +01:00
Faton Ramadani
82c139ed09 feat(frontend): Copy, Cut and Paste (#1279)
* feat(frontend): add copy, paste and cut

* feat(frontend): simplify code

* feat(frontend): add apple modifiers

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-03-12 20:58:06 +01:00
Ruben Fiszel
0789bef120 chore(main): release 1.75.0 (#1278)
* chore(main): release 1.75.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-11 20:54:54 +01:00
Ruben Fiszel
1a7dc0a3bd more cli fixes 2023-03-11 20:53:34 +01:00
Ruben Fiszel
ce323709a9 fix(cli): many small fixes 2023-03-11 20:16:56 +01:00
Ruben Fiszel
61a5e1f1ac feat(frontend): make runs filters synced with query args 2023-03-11 17:13:49 +01:00
Ruben Fiszel
3b44f9a72c feat: add filter jobs by args or result 2023-03-11 15:30:46 +01:00
Ruben Fiszel
b349308ff7 handle better linked variables renaming 2023-03-11 11:25:47 +01:00
Ruben Fiszel
f87b722a21 apps improvements 2023-03-10 23:27:55 +01:00
Ruben Fiszel
0e9be7f300 fix for app viewer 2023-03-10 20:26:49 +01:00
Ruben Fiszel
8681e83b57 feat(apps): add resource picker 2023-03-10 20:01:00 +01:00
Ruben Fiszel
bc440f8d41 feat(frontend-apps): add variable picker for static string input on apps 2023-03-10 18:41:27 +01:00
Ruben Fiszel
1d5c194f09 feat(bash): add default argument handling for bash 2023-03-10 15:04:34 +01:00
Ruben Fiszel
7a9d230459 disable playwright for now 2023-03-10 12:54:00 +01:00
Ruben Fiszel
4d5e2499cf cleanup .workflows 2023-03-10 12:48:02 +01:00
Ruben Fiszel
686275fd46 trim tailwindcss 2023-03-10 12:44:08 +01:00
Ruben Fiszel
99399f4f77 fix serde test 2023-03-10 12:19:56 +01:00
Ruben Fiszel
6e09194313 fix compile 2023-03-10 12:12:10 +01:00
Ruben Fiszel
7c825c212d fix(backend): add killpill for lines reading 2023-03-10 12:04:05 +01:00
Ruben Fiszel
480fd781b6 worker ping at least every 5s even when running long jobs 2023-03-10 01:38:12 +01:00
Ruben Fiszel
4f2079f624 trim tailwind safelist 2023-03-10 01:06:55 +01:00
Ruben Fiszel
43c45d930c chore(main): release 1.74.2 (#1277)
* chore(main): release 1.74.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-10 00:55:13 +01:00
Faton Ramadani
8d5c5b88a3 fix(frontend): fix splitpanes navigation (#1276) 2023-03-10 00:32:33 +01:00
Ruben Fiszel
cc8bedd0c7 make frontend configurable through consts.ts 2023-03-09 22:54:25 +01:00
Ruben Fiszel
74c3d6443c chore(main): release 1.74.1 (#1275)
* chore(main): release 1.74.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-09 22:45:53 +01:00
Ruben Fiszel
c7be313210 fix importjson 2023-03-09 22:42:47 +01:00
Ruben Fiszel
ae53bafaf6 fix(apps): proper reactivity for non rendered static components 2023-03-09 22:29:19 +01:00
Ruben Fiszel
2ea15d5035 fix(ci): make windmill compile again by pinning swc deps 2023-03-09 22:20:31 +01:00
Ruben Fiszel
0f187d66dd show backtrace for cook 2023-03-09 21:22:36 +01:00
Ruben Fiszel
6691b19b24 chore(main): release 1.74.0 (#1269)
* chore(main): release 1.74.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-09 19:58:21 +01:00
Ruben Fiszel
2f9ccff65c app nits 2023-03-09 19:56:47 +01:00
Ruben Fiszel
09db6fd867 fix key navigation 2023-03-09 19:41:12 +01:00
Ruben Fiszel
fd52740d5d improve reactivity check for big objects on app 2023-03-09 18:34:36 +01:00
Faton Ramadani
6b0fb75d23 feat(frontend): Add key navigation in app editor (#1273)
* feat(frontend): add expand

* feat(frontend): fix container height

* feat(frontend): remove code duplication

* feat(frontend): add historic

* feat(frontend): add key navigation

* feat(frontend): simplfiy

* feat(frontend): add support for subgrids

* feat(frontend): update key navigation

* feat(frontend): update key navigation

* feat(frontend): fix nested component

* feat(frontend): fix build

* feat(frontend): remove code duplication

* feat(frontend): support tabs

* feat(frontend): support tabs

* feat(frontend): Fix AppTabs + handle tab navigation

* feat(frontend): support splitpanes
2023-03-09 18:23:12 +01:00
Ruben Fiszel
b1a45b1e70 feat(frontend): add hash to ctx in apps 2023-03-09 14:57:43 +01:00
Ruben Fiszel
b2de531a46 fix(frontend): simplify input bindings 2023-03-09 14:25:08 +01:00
Ruben Fiszel
a4adcb5192 fix(frontend): add confirmation modal to delete script/flow/app 2023-03-09 13:19:03 +01:00
Ruben Fiszel
0c2cf92dd3 feat: add delete by path for scripts 2023-03-09 12:44:49 +01:00
Ruben Fiszel
e6344dac6d fix(cli): improve visibility of the active workspace 2023-03-09 11:21:16 +01:00
Ruben Fiszel
8fb2454e83 enforce on_behalf_of by the backend, not frontend 2023-03-09 11:12:28 +01:00
Ádám Kovács
3b6ae0cc49 fix(frontend): Minor changes (#1272)
* fix(frontend): Output seach fixed on top

* fix(frontend): Use undo-redo component in flows
2023-03-09 09:42:51 +01:00
Ruben Fiszel
96ff2eebc1 fix publishing app as a superadmin 2023-03-09 02:08:52 +01:00
Ruben Fiszel
ed29d51c36 fix app json import 2023-03-09 01:16:57 +01:00
Ádám Kovács
88e537ad1f feat(frontend): Add color picker input to app (#1270)
* feat(frontend): Add color picker input to app

* fix(frontend): Add color input to dividers
2023-03-08 22:57:34 +01:00
Faton Ramadani
b854ee3439 feat(frontend): add expand (#1268)
* feat(frontend): add expand

* feat(frontend): fix container height

* feat(frontend): remove code duplication

* feat(frontend): add historic
2023-03-08 22:56:30 +01:00
Ádám Kovács
0a5e181a3a fix(frontend): Clean up app editor (#1267)
* fix(frontend): Clean up app editor

* fix(frontend): Add outputs search empty state

* fix(frontend): Add remove button to icon input

* label

* fix(frontend): Iconed app button
2023-03-08 19:02:19 +01:00
Ruben Fiszel
8cc59225d8 improve resource picker 2023-03-08 19:00:26 +01:00
Ruben Fiszel
9c41346dde fix subtle plotly import bug 2023-03-08 18:27:19 +01:00
Ruben Fiszel
41a398f50e fix frontend build error 2023-03-08 16:59:14 +01:00
Ruben Fiszel
3436061ad4 make windmill compatible with arm64 2023-03-08 16:55:00 +01:00
Ruben Fiszel
569b5d2516 improve rendering performances for non visible elements 2023-03-08 16:39:16 +01:00
Ruben Fiszel
a08cdd7b86 chore(main): release 1.73.1 (#1266)
* chore(main): release 1.73.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-08 00:41:42 +01:00
Ruben Fiszel
719d475262 fix(frontend): load flow is not initialized 2023-03-08 00:37:58 +01:00
Ruben Fiszel
5b3e1183e5 revert import on tailwind colors for faster builds 2023-03-08 00:20:12 +01:00
Ruben Fiszel
7ed301b186 chore(main): release 1.73.0 (#1257)
* chore(main): release 1.73.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-07 22:35:56 +01:00
Ruben Fiszel
46b6e4371b improve undo/redo + keybindings 2023-03-07 21:16:15 +01:00
Ruben Fiszel
e0d3465b07 fix z-stacking on chrome for flow builder 2023-03-07 19:27:35 +01:00
Ruben Fiszel
7f8fe8dc17 fix z-stacking on chrome for flow builder 2023-03-07 19:23:41 +01:00
Faton Ramadani
24f58efd99 feat(frontend): add a way to automatically resize (#1259)
* feat(frontend): add a way to automatically resize (wip) + add automatic resizable component

* feat(frontend): fix text resize

* feat(frontend): remvove useless softWrap

* feat(frontend): remove useless softWrap

* feat(frontend): Fix recomputeIds + app table

* feat(frontend): Fix app preview error display
2023-03-07 16:53:29 +01:00
Ruben Fiszel
67d8009dcf fix multiple app nits 2023-03-07 15:51:02 +01:00
Ruben Fiszel
95ccc9edf8 fix z-index for flowbuilder 2023-03-07 14:57:51 +01:00
Ruben Fiszel
9e4d90ad37 feat(frontend): add history to flows and apps 2023-03-07 14:47:17 +01:00
Ádám Kovács
c638897fdc fix(frontend): Side menu z-index issue (#1265) 2023-03-07 09:53:19 +01:00
Ruben Fiszel
71305e5154 show archived + fix graphs 2023-03-07 01:59:35 +01:00
Ruben Fiszel
9e9f8efb8e feat(frontend): add fork/save buttons + consistent styling for slider/range 2023-03-06 22:35:25 +01:00
Ádám Kovács
3e5d09ef0b feat(frontend): Add app PDF viewer (#1254)
* feat(frontend): Add app PDF viewer (wip)

* fix(frontend): Handle file upload

* fix(frontend): Handle multi page pdf

* feat(frontend): Add pdf page numbering

* feat(frontend): Add more pdf viewer controls

* save

* fix(frontend): Pdf loading

* fix(frontend): Resize PDF in small window

* fix(frontend): Minor fixes

* feat(frontend): Add pdf zoom configuration

* fix wip

* save

* bg color

* save progress

* pdf scaling

* feat(frontend): fix zoom synchro

* fix(frontend): Pdf scroll tracking

* fix(frontend): Double scrollbar

* nits

* fixes

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-03-06 20:17:36 +01:00
Ruben Fiszel
614fb5022a feat(frontend): add ability to move nodes 2023-03-06 18:41:20 +01:00
Ruben Fiszel
0beadfd1ac fix z-index of inputransformform 2023-03-06 16:04:32 +01:00
Ruben Fiszel
25580c1272 add trigger button 2023-03-06 14:11:17 +01:00
Faton Ramadani
2557e136bd fix(frontend): fix app map reactivity (#1260) 2023-03-06 11:26:00 +01:00
Ruben Fiszel
200cb69d82 make default branch non removable for branchone 2023-03-06 11:19:56 +01:00
Ruben Fiszel
9ee261fe1a Update docker-compose.yml 2023-03-06 10:39:00 +01:00
Ruben Fiszel
8e563a42f5 Update docker-compose.yml with oauth example 2023-03-06 10:38:01 +01:00
Faton Ramadani
a999eb2112 fix(frontend): fix branch deletion (#1261)
* fix(frontend): fix branch deletion

* fix(frontend): fix branch deletion

* fix(frontend): fix branch deletion
2023-03-06 09:01:19 +01:00
Ruben Fiszel
e5dbe7076c handle larger sized graphs 2023-03-06 08:33:53 +01:00
Ruben Fiszel
2ac51b0af0 feat(frontend): refactor entire flow builder UX 2023-03-05 23:00:43 +01:00
Ruben Fiszel
f3232062c3 make tailwind inputs class configurable 2023-03-03 22:32:35 +01:00
Ruben Fiszel
b11a5a2df6 only bind the staticoutputs of the first row 2023-03-03 18:00:09 +01:00
Ruben Fiszel
e2c4545240 fix(frontend): arginput + apppreview fixes 2023-03-03 17:34:08 +01:00
Faton Ramadani
70dd6f759c App small fixes (#1258)
* fix(frontend): Fix runnable editor

* fix(frontend): remove isopenstore

* fix(frontend): add output searchbar

* fix(frontend): fix build

* fix(frontend): add missing clear button
2023-03-03 15:15:29 +01:00
Ruben Fiszel
dcfb29fb80 fix sqlx offline 2023-03-03 13:04:24 +01:00
Faton Ramadani
94f1aadef2 feat(frontend): Fix object viewer style (#1255)
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-03-03 12:47:55 +01:00
Ruben Fiszel
58300eb6ac introduce root_job and leaf_jobs for efficient result_by_id 2023-03-03 12:44:44 +01:00
Ashutosh Narang
304dea4b74 update build instructions (#1256) 2023-03-03 11:19:12 +01:00
Ruben Fiszel
f4fe71e074 chore(main): release 1.72.0 (#1250)
* chore(main): release 1.72.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-03 11:12:18 +01:00
Ruben Fiszel
fd4e18f62f fix minSize for app splitpanes to 20 2023-03-03 01:08:20 +01:00
Ruben Fiszel
e428662481 feat(frontend): add creatable select 2023-03-03 01:06:36 +01:00
Ruben Fiszel
b796aeef7a calculate all previous ids inside flows 2023-03-02 20:54:44 +01:00
Ruben Fiszel
55eb48c553 fix(frontend): background script not showing inputs 2023-03-02 17:54:05 +01:00
Ruben Fiszel
a43139fe53 flow improvements 2023-03-02 17:37:51 +01:00
Ruben Fiszel
c4463bb029 fix(backend): improve result retrieval 2023-03-02 16:33:24 +01:00
Ruben Fiszel
cc6eaaf473 fix tailwind JIT for devmode + graph fixes 2023-03-02 14:49:36 +01:00
Ádám Kovács
ed25d9f186 feat(frontend): Add app map component (#1251)
* feat(frontend): Add app map component (wip)

* fix(frontend): Revert

* feat(frontend): sync map configuration (#1252)

* fix(frontend): Map markers

* fix(frontend): Switching between input types

* fix(frontend): Customize map controls

* feat(frontend): Fix output + add set region button

* feat(frontend): Fix output + add set region button

* feat(frontend): Fix output + add set region button

* feat(frontend): Only display set region button on edit mode

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-03-02 14:25:30 +01:00
Ruben Fiszel
35ea2b27b1 fix(cli): fix workspace option + run script/flow + whoami 2023-03-02 13:21:50 +01:00
Ruben Fiszel
2c1e3b3372 UX nits 2023-03-02 12:08:20 +01:00
Ruben Fiszel
4101d587de remove slide causing issues 2023-03-02 11:59:10 +01:00
Ruben Fiszel
e6ff3ab6cc remove slide causing issues 2023-03-02 11:49:57 +01:00
Ruben Fiszel
8fc6c39129 remove bg-gray-50 from viewed apps 2023-03-02 11:06:11 +01:00
Ruben Fiszel
fcb5cf4d41 revert caddyfileremote target change 2023-03-02 10:39:01 +01:00
Ruben Fiszel
2679386bf8 fix(frontend): fix table bindings 2023-03-02 09:54:30 +01:00
Ryan Rich
580388ce19 Add support for binding server listener to a specific IP address (#1253) 2023-03-02 08:01:50 +01:00
Ruben Fiszel
4e6e66d7b1 fix splitpanes 2023-03-02 02:31:39 +01:00
Faton Ramadani
f4d79ee263 feat(frontend): app splitpanes (#1248)
* feat(frontend): app splitpanes

* feat(frontend): app splitpanes vertical

* feat(frontend): support both splitpanes

* done

* done

* default select value

* container height

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-03-02 01:30:07 +01:00
Ruben Fiszel
38fb3450c8 fix apps tabs + make inputvalue more resilient 2023-03-01 22:15:25 +01:00
Ruben Fiszel
94b20d2f5e fix(frontend): rework app reactivity 2023-03-01 21:33:23 +01:00
Ruben Fiszel
1753cb7da6 fix(frontend): rework app reactivity 2023-03-01 20:01:59 +01:00
Ruben Fiszel
2a75cd250e fix(backend): incorrect get_result_by_id for list_result job 2023-03-01 12:43:00 +01:00
Ruben Fiszel
29f3fe2663 update sqlx-data.json 2023-03-01 12:01:22 +01:00
Ruben Fiszel
4c913dc4b6 feat(backend): get_result_by_id do a downward pass to find node at any depth (#1249)
* downwardRec

* downwardRec

* any node

* any node

* any node
2023-03-01 11:33:48 +01:00
Ruben Fiszel
5c40ff4290 Update LICENSE 2023-03-01 09:53:26 +01:00
Ruben Fiszel
2bbe112444 handle more undefined cases in app 2023-03-01 08:46:20 +01:00
Ruben Fiszel
90a12f6131 drawer focus 2023-03-01 01:21:32 +01:00
Ruben Fiszel
f3f95fa865 active grid border-dashed for apps 2023-03-01 01:18:10 +01:00
Ruben Fiszel
26784464a4 revert pips change 2023-02-28 22:53:44 +01:00
Ruben Fiszel
c96e2351d9 chore(main): release 1.71.0 (#1242)
* chore(main): release 1.71.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-02-28 22:51:57 +01:00
Ruben Fiszel
ddb4916a2e fix app fields 2023-02-28 22:41:21 +01:00
Ádám Kovács
1bb5ed9ae0 fix(frontend): Add more app custom css (#1247)
* fix(frontend): Add number input custom css

* fix(frontend): Add currency input custom css

* fix(frontend): Add slider custom css

* fix(frontend): Add range custom css

* fix(frontend): Add password input custom css

* fix(frontend): Add date input custom css

* fix(frontend): Add tabs custom css

* fix(frontend): Minor stylings

* fix(frontend): Add icon custom css

* fix(frontend): Add dividers custom css

* fix(frontend): Add file input custom css

* fix(frontend): Add image custom css
2023-02-28 21:05:58 +01:00
Ruben Fiszel
b5b32f00b3 fix overflow-y on debug runs 2023-02-28 19:54:20 +01:00
Ruben Fiszel
c06311faf8 add workspace_add events 2023-02-28 19:41:01 +01:00
Ruben Fiszel
8a639b6e7d select input fix 2023-02-28 19:28:36 +01:00
Ruben Fiszel
05f568fb8c display startup info in all cases 2023-02-28 17:52:47 +01:00
Ruben Fiszel
e515c70e71 fix incorrect user sttings redirect 2023-02-28 16:57:00 +01:00
Ruben Fiszel
6adc875610 feat(frontend): drawer for editing workspace scripts in flows 2023-02-28 15:51:56 +01:00
Faton Ramadani
8a0d1158c4 feat(frontend): App drawer (#1246)
* feat(frontend): app drawer

* feat(frontend): app drawer

* feat(frontend): app drawer

* feat(frontend): app drawer wip

* feat(frontend): drawer wip

* feat(frontend): drawer wip

* feat(frontend): app missing prop

* feat(frontend): revert drawer changes

* feat(frontend): highlight subgrid
2023-02-28 15:49:57 +01:00
Ruben Fiszel
ea2ebfa92e fix compile issue 2023-02-28 11:18:09 +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
2244e83b9d fix(frontend): invisible subgrids have h-0 + app policies fix 2023-02-27 18:38:22 +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
541 changed files with 277426 additions and 18832 deletions

7
.env
View File

@@ -1,2 +1,7 @@
DB_PASSWORD=changeme
WM_BASE_URL=localhost
# this is the url that your instance is publicly exposed to
WM_BASE_URL=http://localhost
# this is the url that caddy will reverse proxy from. It might be different than WM_BASE_URL if you re using a second proxy/load balancer in front
CADDY_REVERSE_PROXY=http://localhost

View File

@@ -4,7 +4,7 @@ VERSION=$1
echo "Updating versions to: $VERSION"
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" backend/Cargo.toml
sed -i -e "/^const VERSION =/s/= .*/= \"v$VERSION\";/" cli/main.ts
sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" cli/main.ts
sed -i -e "/version: /s/: .*/: $VERSION/" backend/windmill-api/openapi.yaml
sed -i -e "/version: /s/: .*/: $VERSION/" openflow.openapi.yaml
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" frontend/package.json

View File

@@ -1,20 +0,0 @@
name: Deploy to windmill.dev
on:
push:
branches: [main]
paths:
- "community/**"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to windmill.dev
uses: windmill-labs/windmill-gh-action-deploy@v2.0.0
with:
dry_run: false
input_dir: community
windmill_workspace: starter
windmill_token: ${{ secrets.WINDMILL_API_TOKEN }}

View File

@@ -18,7 +18,7 @@ permissions:
contents: read
id-token: write
packages: write
jobs:
build:
runs-on: ubuntu-22.04
@@ -50,7 +50,6 @@ jobs:
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push publicly
uses: depot/build-push-action@v1
with:
@@ -63,7 +62,7 @@ jobs:
labels: |
${{ steps.meta-public.outputs.labels }}
org.opencontainers.image.licenses=AGPLv3
build_ee:
runs-on: ubuntu-22.04
steps:
@@ -109,40 +108,61 @@ jobs:
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
# disabled until we make it 100% reliable and add more meaningful tests
# playwright:
# runs-on: [self-hosted, new]
# needs: [build]
# services:
# postgres:
# image: postgres
# env:
# POSTGRES_DB: windmill
# POSTGRES_USER: admin
# POSTGRES_PASSWORD: changeme
# ports:
# - 5432:5432
# options: >-
# --health-cmd pg_isready
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# 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.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 && export BASE_URL=http://localhost:8000 && npm run test
# - name: "Clean up"
# run: docker kill ${{ steps.docker-container.outputs.id }}
# if: always()
playwright:
runs-on: [self-hosted, new]
needs: [build]
services:
postgres:
image: postgres
env:
POSTGRES_DB: windmill
POSTGRES_USER: admin
POSTGRES_PASSWORD: changeme
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
deploy_s3:
needs: [build_ee]
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
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.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 && export BASE_URL=http://localhost:8000 && npm run test
- name: "Clean up"
run: docker kill ${{ steps.docker-container.outputs.id }}
if: always()
node-version: 18
- uses: shrink/actions-docker-extract@v2
id: extract
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
path: "/static_frontend/."
- uses: reggionick/s3-deploy@v3
with:
folder: ${{ steps.extract.outputs.destination }}
bucket: windmill-frontend
bucket-region: us-east-1
publish_privately_heavy:
needs: [build_ee]
runs-on: [self-hosted, new]
@@ -182,7 +202,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: .
@@ -191,8 +211,12 @@ jobs:
tags: |
${{ steps.meta-heavy.outputs.tags }}
labels: ${{ steps.meta-heavy.outputs.labels }}
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-heavy:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-heavy:buildcache,mode=max
cache-from:
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
}}-heavy:buildcache
cache-to:
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
}}-heavy:buildcache,mode=max
publish_privately_helm:
runs-on: [self-hosted, new]
@@ -204,7 +228,6 @@ jobs:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to registry
uses: docker/login-action@v2
@@ -220,9 +243,9 @@ jobs:
registry: ${{ env.ECR_REGISTRY }}
username: ${{ secrets.AWS_ACCESS_KEY_ID }}
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: .
@@ -230,5 +253,9 @@ jobs:
file: ./docker/DockerfileHelm
tags: |
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:helm
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-helm:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-helm:buildcache,mode=max
cache-from:
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
}}-helm:buildcache
cache-to:
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
}}-helm:buildcache,mode=max

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
- name: generate_go
run: |
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.11.0

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

View File

@@ -25,12 +25,12 @@ jobs:
run: echo "UUID_TAG_APP=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v3
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@v2
uses: docker/build-push-action@v4
with:
push: true
context: ./

View File

@@ -1,6 +1,391 @@
# Changelog
## [1.87.0](https://github.com/windmill-labs/windmill/compare/v1.86.0...v2.0.0) (2023-04-11)
### ⚠ BREAKING CHANGES
* **frontend:** Add option to return file names ([#1380](https://github.com/windmill-labs/windmill/issues/1380))
### Features
* **backend:** add instance events webhook ([f2d3c82](https://github.com/windmill-labs/windmill/commit/f2d3c8208b6daa49f304f355752145de47138a3c))
* **backend:** extend cached resolution for go ([dac61d1](https://github.com/windmill-labs/windmill/commit/dac61d1c982576d7589e16ab01c8cc8bad6e1686))
* **backend:** Redis based queue ([#1324](https://github.com/windmill-labs/windmill/issues/1324)) ([d45e6c9](https://github.com/windmill-labs/windmill/commit/d45e6c94abed609357b18d4daa7de6b2ea0ba978))
* **frontend:** Add option to return file names ([#1380](https://github.com/windmill-labs/windmill/issues/1380)) ([3dabac1](https://github.com/windmill-labs/windmill/commit/3dabac153f302f48210d15ebaec514e72717300f))
* **python:** cache dependency resolution ([facb670](https://github.com/windmill-labs/windmill/commit/facb67093ce7d3b0874d0d559fb272ed822ce360))
### Bug Fixes
* **backend:** nested deno relative imports ([955a213](https://github.com/windmill-labs/windmill/commit/955a213a504c1f3b8811c930823e87fe7dba101a))
* **cli:** overwrite archived scripts ([1f705ca](https://github.com/windmill-labs/windmill/commit/1f705cab2ce8c79829f22fc6af9e06ecba7450b1))
* **frontend:** Add missing stopPropagation ([#1394](https://github.com/windmill-labs/windmill/issues/1394)) ([58d4b55](https://github.com/windmill-labs/windmill/commit/58d4b556ebbd76c6f07f1a16d601a9d824b99f7e))
* **frontend:** fix app init issue ([d0e0e1f](https://github.com/windmill-labs/windmill/commit/d0e0e1fdf27d9a7fb86c66e43398786b64d8b6b7))
* **frontend:** Fix frontend dependencies ([#1379](https://github.com/windmill-labs/windmill/issues/1379)) ([8e9c491](https://github.com/windmill-labs/windmill/commit/8e9c49165060a4a7f831b8be075593f89d867784))
* **frontend:** Fix icon picker input ([#1389](https://github.com/windmill-labs/windmill/issues/1389)) ([8a44f8e](https://github.com/windmill-labs/windmill/commit/8a44f8e7796f13698e2a99af9f3772f5e676604b))
* **frontend:** Fix mac shortcuts ([#1381](https://github.com/windmill-labs/windmill/issues/1381)) ([41831d5](https://github.com/windmill-labs/windmill/commit/41831d58ed593bb283600b76170f6e76783e0eae))
* **frontend:** fix popover configuration to avoid content shift ([#1377](https://github.com/windmill-labs/windmill/issues/1377)) ([2031e1e](https://github.com/windmill-labs/windmill/commit/2031e1ebd0dc020da104ee84a0294c86babcefaf))
* **frontend:** remove stopPropagation that was preventing components dnd ([#1378](https://github.com/windmill-labs/windmill/issues/1378)) ([de8dc1e](https://github.com/windmill-labs/windmill/commit/de8dc1e9cd7beea2ce62656e9e7676214f77a110))
### Performance Improvements
* parallelize more operations for deno jobs ([e911869](https://github.com/windmill-labs/windmill/commit/e911869d990956463834ac9ff35c52ba8236e362))
## [1.86.0](https://github.com/windmill-labs/windmill/compare/v1.85.0...v1.86.0) (2023-04-08)
### Features
* **backend:** add /ready endpoint for workers ([94eecea](https://github.com/windmill-labs/windmill/commit/94eecea02b6295ad5674db4b010bf6ab7984fa17))
* **backend:** add GET endpoint to trigger scripts ([15c75d9](https://github.com/windmill-labs/windmill/commit/15c75d9d00a69ae97123ed371b9657e298345bdb))
* **backend:** lowercase all emails in relevant endpoints ([#1361](https://github.com/windmill-labs/windmill/issues/1361)) ([7f9050b](https://github.com/windmill-labs/windmill/commit/7f9050b285cf8f7f6baf05452b673f58988c452c))
* **cli:** add getFullResource ([3a232db](https://github.com/windmill-labs/windmill/commit/3a232dbb5792c28b26747e1ba260fffcdd4a8416))
* do cache bucket syncing in background + check tar before pushing it ([#1360](https://github.com/windmill-labs/windmill/issues/1360)) ([3e5ff86](https://github.com/windmill-labs/windmill/commit/3e5ff8682a298ba9e59b2662c4c04c5698447204))
* **frontend:** add flow expand button ([34a8b01](https://github.com/windmill-labs/windmill/commit/34a8b01b762c0b210d76101e7da7bd2397258e8d))
* **frontend:** add impersonate api + local resolution of import by lsp v0 ([7675f08](https://github.com/windmill-labs/windmill/commit/7675f08b7bfe319e496a86a7ef1ab7cc8c1d12d2))
* **frontend:** add workspace to ctx ([8f7a11b](https://github.com/windmill-labs/windmill/commit/8f7a11b8964e2c3405ce3689f9cf2298f9e71c75))
* **frontend:** Improve login + toasts ([#1363](https://github.com/windmill-labs/windmill/issues/1363)) ([92be102](https://github.com/windmill-labs/windmill/commit/92be102a070b1f17b9d3e40524cd21b54301b5a7))
* **frontend:** make script editor a single page ([b84be60](https://github.com/windmill-labs/windmill/commit/b84be60c53ca1ef65826123f39099d33c1f549c0))
* **frontend:** Tone down text + display whole text ([#1366](https://github.com/windmill-labs/windmill/issues/1366)) ([f214d5f](https://github.com/windmill-labs/windmill/commit/f214d5f96b6ac26cd3ef90a6ab696a6dfe02b3f0))
* improved cron/schedule editor ([#1362](https://github.com/windmill-labs/windmill/issues/1362)) ([17176bb](https://github.com/windmill-labs/windmill/commit/17176bb8d112b35228ce9183f4b2f81abe9e5b6e))
### Bug Fixes
* **backend:** allow cors ([8a594a8](https://github.com/windmill-labs/windmill/commit/8a594a89adba9915508884f900f58c4ab53cdfec))
* **backend:** allow longer name/company ([eff61bb](https://github.com/windmill-labs/windmill/commit/eff61bb8d3496bc1c5be4b1051f99ed4470a47ff))
* **backend:** always flush bash output ([517b2c9](https://github.com/windmill-labs/windmill/commit/517b2c9cca54628c8ee692d65c05bc2513eaaf22))
* **backend:** always flush bash output ([7a9091f](https://github.com/windmill-labs/windmill/commit/7a9091fed6aa99201b75bab88d4faddbe041eee4))
* **backend:** inline script app python fix ([8c72722](https://github.com/windmill-labs/windmill/commit/8c72722710db8e3720b01180b504cbc66e79f5ca))
* **frontend:** Add FlowGraph display on Safari ([#1351](https://github.com/windmill-labs/windmill/issues/1351)) ([2819b09](https://github.com/windmill-labs/windmill/commit/2819b09ce5011a467e994ee8b1f09cf33145003d))
* **frontend:** Fix button poppup ([#1368](https://github.com/windmill-labs/windmill/issues/1368)) ([a344928](https://github.com/windmill-labs/windmill/commit/a344928f251d697f53e40c517b0b86bd90e0ad52))
* **frontend:** Fix connected property ([#1371](https://github.com/windmill-labs/windmill/issues/1371)) ([4af39f0](https://github.com/windmill-labs/windmill/commit/4af39f081bf3d07aaade39e5a5a221741fe8f973))
* **frontend:** Fix flow templateEditor ([#1367](https://github.com/windmill-labs/windmill/issues/1367)) ([51fc436](https://github.com/windmill-labs/windmill/commit/51fc436456104c2d6a3cd6f6d62f08929e40d450))
* **frontend:** make croninput a builder rather than a tab ([266b5b0](https://github.com/windmill-labs/windmill/commit/266b5b00da3bd7643eaa5dba1b8c1456f11c5e30))
* **frontend:** Minor fixes ([#1374](https://github.com/windmill-labs/windmill/issues/1374)) ([76a2a1d](https://github.com/windmill-labs/windmill/commit/76a2a1db363facbaf9a0e9618f169d6cc66e946f))
* no need to map internal ports to hosts ([#1365](https://github.com/windmill-labs/windmill/issues/1365)) ([4ec035b](https://github.com/windmill-labs/windmill/commit/4ec035b09a58f8859bc576b03c24cc73f335f32d))
## [1.85.0](https://github.com/windmill-labs/windmill/compare/v1.84.1...v1.85.0) (2023-04-03)
### Features
* add local cache for folder path used + invalidate cache on folder creation ([018b051](https://github.com/windmill-labs/windmill/commit/018b051781e3f40b9d1da8ccdd5edb1cd49877ba))
* **frontend:** add agGrid api hooks + ready ([de1e294](https://github.com/windmill-labs/windmill/commit/de1e29492c9aefdfc59f605ba81f7c51a96bf2f3))
* **frontend:** Add ID renaming popup ([#1344](https://github.com/windmill-labs/windmill/issues/1344)) ([0b8a08c](https://github.com/windmill-labs/windmill/commit/0b8a08cb49644da7c354c3631751e925ac5353b9))
### Bug Fixes
* **backend:** improve handling subflow with many depth using tailrec ([8c53598](https://github.com/windmill-labs/windmill/commit/8c53598aba3fb89f4174d1c0ab3912096ac07c96))
* **backend:** improve subflow processing ([390a988](https://github.com/windmill-labs/windmill/commit/390a988d4c96256a4fbd6a9302fc47a5648c2c43))
* **frontend:** PDF reader header positioning ([#1350](https://github.com/windmill-labs/windmill/issues/1350)) ([daf8276](https://github.com/windmill-labs/windmill/commit/daf827666b13917f8c9abeab5bb2b072bd0fef0b))
## [1.84.1](https://github.com/windmill-labs/windmill/compare/v1.84.0...v1.84.1) (2023-03-31)
### Bug Fixes
* **cli:** overwrite instead of smart diff ([b6d5eef](https://github.com/windmill-labs/windmill/commit/b6d5eef5479e38cc36af2db67d4c45f78c622b9a))
## [1.84.0](https://github.com/windmill-labs/windmill/compare/v1.83.1...v1.84.0) (2023-03-31)
### Features
* add force cancel ([fbe5c18](https://github.com/windmill-labs/windmill/commit/fbe5c18da02763371e6f32c898b31a6a29984b45))
* add the ability to edit previous versions ([2368da2](https://github.com/windmill-labs/windmill/commit/2368da214660ff1835b49b4c2c87256c9bd565cf))
* **backend:** reduce memory allocation for big forloops of flows ([c7506e4](https://github.com/windmill-labs/windmill/commit/c7506e4daec5b12bf908e6954bf6f3521a97b3ba))
* **frontend:** App component style input grouping ([#1334](https://github.com/windmill-labs/windmill/issues/1334)) ([01564f0](https://github.com/windmill-labs/windmill/commit/01564f0a1c26ee9f065bb0adeb7d5e8df0b2e5b5))
* **frontend:** Display frontend execution result in Debug Runs ([#1341](https://github.com/windmill-labs/windmill/issues/1341)) ([57f8dd9](https://github.com/windmill-labs/windmill/commit/57f8dd9570577a58fe91d93c7a9d1a9b4dc69598))
* **frontend:** improve input connection UI ([#1333](https://github.com/windmill-labs/windmill/issues/1333)) ([5ac646e](https://github.com/windmill-labs/windmill/commit/5ac646e859a07efb65542aae9365aa7791ce1097))
### Bug Fixes
* **backend:** add a refresh button to workspace script/hub ([bb61cef](https://github.com/windmill-labs/windmill/commit/bb61cef0e56bf7fa7f8a5f91dabd590afd5db791))
* **backend:** backend compatability on macos ([#1340](https://github.com/windmill-labs/windmill/issues/1340)) ([dfd2abc](https://github.com/windmill-labs/windmill/commit/dfd2abc76466cddca98f93fd82be91ba5d3076e0))
* **frontend:** Export python code as string ([#1339](https://github.com/windmill-labs/windmill/issues/1339)) ([2779891](https://github.com/windmill-labs/windmill/commit/277989141100b033b26b496b8a55d97d48cf7e81))
* **frontend:** improve app tables ([cd1f9b6](https://github.com/windmill-labs/windmill/commit/cd1f9b6baa0dadfb14fee3a586a4b6b164e5e402))
* **frontend:** improve loading of big args in job details ([71619ac](https://github.com/windmill-labs/windmill/commit/71619acdfac010822c1eac496a6f3f869e6ca6fb))
* **frontend:** improve loading of big jobs in run form ([b325493](https://github.com/windmill-labs/windmill/commit/b3254938fe58d8c00a0c4347e7ef519e3a6e4031))
## [1.83.1](https://github.com/windmill-labs/windmill/compare/v1.83.0...v1.83.1) (2023-03-28)
### Bug Fixes
* **cli:** plain secrets might be undefined ([569a55e](https://github.com/windmill-labs/windmill/commit/569a55e45b34641b0fb4569387166f3aa89ce35f))
## [1.83.0](https://github.com/windmill-labs/windmill/compare/v1.82.0...v1.83.0) (2023-03-28)
### Features
* **backend:** allow relative imports for python ([a5500ea](https://github.com/windmill-labs/windmill/commit/a5500ea40a77b2e0408e2a644190a8f65b18cd1d))
* **backend:** execute /bin/bash instead of /bin/sh for bash scripts ([021fa23](https://github.com/windmill-labs/windmill/commit/021fa23f9ffcd11548977a4589eb9bc2815243cf))
* **backend:** improve relative importsfor deno ([eaac598](https://github.com/windmill-labs/windmill/commit/eaac598af308cedea8f0f8fc7c189a4640b4366b))
* **backend:** increase timeout for premium workspace ([00b70d9](https://github.com/windmill-labs/windmill/commit/00b70d9aaac8ae979782492d7754060a3c2c9567))
* **frontend:** add pagination ([33c07d3](https://github.com/windmill-labs/windmill/commit/33c07d3e63f96673719ecb15e45f4cd9e18be80e))
* **frontend:** Add quick style settings to app editor ([#1308](https://github.com/windmill-labs/windmill/issues/1308)) ([ac24862](https://github.com/windmill-labs/windmill/commit/ac2486219cd91df3a7fe11d37894797a881cac6c))
* **frontend:** add recompute as a primitive ([449d3ae](https://github.com/windmill-labs/windmill/commit/449d3ae5ddeceef3fbcb7a815a4dba16c9639fb3))
* **frontend:** add textareacomponent + fix multiselect style + select multi components ([2b31653](https://github.com/windmill-labs/windmill/commit/2b31653a8aa06807678e8609cfa62cf0f2f55dce))
* **frontend:** multiselect components for apps ([577dec5](https://github.com/windmill-labs/windmill/commit/577dec5c5733cdf88e8586ce6c27159920c69c8a))
* **frontend:** use rich json editor for arrays of objects and for object in ArgInput ([b95afaa](https://github.com/windmill-labs/windmill/commit/b95afaa9bb41b102181657453a564f44f4511983))
### Bug Fixes
* **apps:** improve app table actionButtons behavior under many clicks ([8e3d8ac](https://github.com/windmill-labs/windmill/commit/8e3d8acc80de971ee115d6903d24864d8263f08b))
* **cli:** add --plain-secrets ([98d51e2](https://github.com/windmill-labs/windmill/commit/98d51e219df1680507114f9b57ec0b0a4a234b5c))
* **frontend:** add a modal that is always mounted to make sure compon… ([#1328](https://github.com/windmill-labs/windmill/issues/1328)) ([a527cb8](https://github.com/windmill-labs/windmill/commit/a527cb8222a2ff80dae38ebae7dc5ea0979d74c5))
* **frontend:** Disable app keyboard navigation on focused inputs ([#1326](https://github.com/windmill-labs/windmill/issues/1326)) ([da24e9a](https://github.com/windmill-labs/windmill/commit/da24e9ab0625a7503c498c179022ea4011a03170))
* **frontend:** persist description for schemas ([1a48673](https://github.com/windmill-labs/windmill/commit/1a4867302f72aaae8f422ac8f53812c116cc383d))
* **frontend:** Revert app upload input ([#1330](https://github.com/windmill-labs/windmill/issues/1330)) ([fa457bb](https://github.com/windmill-labs/windmill/commit/fa457bb7099bd31c2315eaf7f7f2c40900b2ae39))
* **frontend:** Small app fixes ([#1331](https://github.com/windmill-labs/windmill/issues/1331)) ([75306c8](https://github.com/windmill-labs/windmill/commit/75306c831616d9a01cc3a4681732aab93153f1a9))
## [1.82.0](https://github.com/windmill-labs/windmill/compare/v1.81.0...v1.82.0) (2023-03-24)
### Features
* **backend:** introduce RESTART_ZOMBIE_JOBS and ZOMBIE_JOB_TIMEOUT ([47a7f71](https://github.com/windmill-labs/windmill/commit/47a7f7163aae3fe807e766c824085b4d1b75c8c8))
### Bug Fixes
* **backend:** do not consider FlowPreview as potential zombie job ([f7c30b5](https://github.com/windmill-labs/windmill/commit/f7c30b5d2f16e15f36208e07126557fd7ed84801))
* **backend:** increase dynamic js timeout + improve client passing ([34e25f0](https://github.com/windmill-labs/windmill/commit/34e25f0f96fe637cc42f4017a064c40def5d67ef))
* **cli:** improve diff speed + fix replacing cli ([b999c98](https://github.com/windmill-labs/windmill/commit/b999c9894b4011b735f37df485fe403c22c00512))
* **frontend:** Fix AppTable error display + clear errors when removing a component + properly detect that latest component run had an error ([#1322](https://github.com/windmill-labs/windmill/issues/1322)) ([c15bc8a](https://github.com/windmill-labs/windmill/commit/c15bc8a7bfb3bef2634e6093088967137cd06239))
* **frontend:** fix refresh with manual dependencies ([#1319](https://github.com/windmill-labs/windmill/issues/1319)) ([a47031a](https://github.com/windmill-labs/windmill/commit/a47031a41e6a3392101e280dcd1aea098f898447))
* **frontend:** fix settings panel ([#1323](https://github.com/windmill-labs/windmill/issues/1323)) ([30b8e47](https://github.com/windmill-labs/windmill/commit/30b8e474df5b71b7e7b36d3fe5974a289cf0dfae))
* **frontend:** Fix transformer ([#1321](https://github.com/windmill-labs/windmill/issues/1321)) ([addabcc](https://github.com/windmill-labs/windmill/commit/addabcceb0c90782ba4a934bb3822f8cc9865069))
* **frontend:** remove unnecessary div ([#1318](https://github.com/windmill-labs/windmill/issues/1318)) ([e193a0b](https://github.com/windmill-labs/windmill/commit/e193a0bcdf6690b007594d2f1325a7ec26603129))
## [1.81.0](https://github.com/windmill-labs/windmill/compare/v1.80.1...v1.81.0) (2023-03-21)
### Features
* **apps:** add action on form/button/formbutton ([2593218](https://github.com/windmill-labs/windmill/commit/2593218cbf07c05521a270797055ddb22dc22b8d))
### Bug Fixes
* **frontend:** Remove action outline on preview mode ([#1313](https://github.com/windmill-labs/windmill/issues/1313)) ([a7c4f1a](https://github.com/windmill-labs/windmill/commit/a7c4f1a12e02e8627a5955b75d572e9cf11d8122))
## [1.80.1](https://github.com/windmill-labs/windmill/compare/v1.80.0...v1.80.1) (2023-03-21)
### Bug Fixes
* **cli:** add support for non metadataed scripts ([42f6d2e](https://github.com/windmill-labs/windmill/commit/42f6d2e0ee6294f8a1d97f5f62f2adb6edfd2fed))
## [1.80.0](https://github.com/windmill-labs/windmill/compare/v1.79.0...v1.80.0) (2023-03-20)
### Features
* **apps:** add transformers for data sources ([0abacac](https://github.com/windmill-labs/windmill/commit/0abacac06c7dd586b48c66ff47b7589fe692205b))
* **frontend:** App set tab ([#1307](https://github.com/windmill-labs/windmill/issues/1307)) ([48413a7](https://github.com/windmill-labs/windmill/commit/48413a78c5e7e0ee8208711f15135d81136b7386))
### Bug Fixes
* **frontend:** add missing optional chaining ([#1306](https://github.com/windmill-labs/windmill/issues/1306)) ([29b1cc6](https://github.com/windmill-labs/windmill/commit/29b1cc6ff0ebc5edcad24a1780113889c507075d))
* **frontend:** App button triggered by ([#1304](https://github.com/windmill-labs/windmill/issues/1304)) ([cf2d031](https://github.com/windmill-labs/windmill/commit/cf2d031e8e89faa2cd7fa58436cbe7cf4d9045f9))
## [1.79.0](https://github.com/windmill-labs/windmill/compare/v1.78.0...v1.79.0) (2023-03-17)
### Features
* **frontend:** add listeners for frontend scripts ([597e38e](https://github.com/windmill-labs/windmill/commit/597e38ef367d38fa97fc443ccb2c721e5964fece))
* **frontend:** add table actions navigation ([#1298](https://github.com/windmill-labs/windmill/issues/1298)) ([c3ba1a6](https://github.com/windmill-labs/windmill/commit/c3ba1a6ab97484a08a5a20187bb858a5af7025cb))
* **frontend:** App component triggers ([#1303](https://github.com/windmill-labs/windmill/issues/1303)) ([078cb1b](https://github.com/windmill-labs/windmill/commit/078cb1bf3e4de08cb018578f04d24392a6462f69))
* **frontend:** Component control ([#1293](https://github.com/windmill-labs/windmill/issues/1293)) ([bd927a2](https://github.com/windmill-labs/windmill/commit/bd927a27ed9581dbf67ea3694f9d989f8d71d2ed))
### Bug Fixes
* **frontend:** App panel styling ([#1284](https://github.com/windmill-labs/windmill/issues/1284)) ([c1dd35c](https://github.com/windmill-labs/windmill/commit/c1dd35c3f0fcbc1be43273f82a873c3c07863417))
* **frontend:** Display app context search on top ([#1300](https://github.com/windmill-labs/windmill/issues/1300)) ([bd3ee81](https://github.com/windmill-labs/windmill/commit/bd3ee81b14846f16ccd16461de99b46fe68be6ba))
* **frontend:** fix horizontal splitpanes ([#1301](https://github.com/windmill-labs/windmill/issues/1301)) ([ea3dab4](https://github.com/windmill-labs/windmill/commit/ea3dab411b3d5dd772e04c8831e789e2470aaf28))
* **frontend:** fix map render ([#1297](https://github.com/windmill-labs/windmill/issues/1297)) ([0092721](https://github.com/windmill-labs/windmill/commit/00927210fd68c31cb793ef4f0efea05711ebcf00))
* **frontend:** Hide archive toggle with empty list ([#1296](https://github.com/windmill-labs/windmill/issues/1296)) ([bac831b](https://github.com/windmill-labs/windmill/commit/bac831b23ce85a683ddbd4537900670a0b7d12a8))
## [1.78.0](https://github.com/windmill-labs/windmill/compare/v1.77.0...v1.78.0) (2023-03-16)
### Features
* **frontend:** app textcomponent editable + tooltip ([11567d6](https://github.com/windmill-labs/windmill/commit/11567d6280ea60f1a8c3c6607c724179775cbbe3))
### Bug Fixes
* **backend:** whitelist for include_header was ignored in some cases ([183a459](https://github.com/windmill-labs/windmill/commit/183a4591df700ab4720de6e92a83631256940089))
* **frontend:** improve rendering performance after component moving ([6f890f2](https://github.com/windmill-labs/windmill/commit/6f890f2120885f90d986fbd655096b45bf9de539))
* **frontend:** remove staticOutputs from apps ([dbdfd62](https://github.com/windmill-labs/windmill/commit/dbdfd626386398180ecba7976714f86365eeccd8))
## [1.77.0](https://github.com/windmill-labs/windmill/compare/v1.76.0...v1.77.0) (2023-03-14)
### Features
* **apps:** state can be used as input in apps ([2f0acb9](https://github.com/windmill-labs/windmill/commit/2f0acb9ffa8dace4a886527dcee49809d019b271))
* **apps:** tabs can be made pages or invisible + better frontend scripts reactivity ([cd645d0](https://github.com/windmill-labs/windmill/commit/cd645d0935f2d06e0ff71f14d2cf63accd378ff3))
* **deno:** add support for custom npm repo ([#1291](https://github.com/windmill-labs/windmill/issues/1291)) ([944795f](https://github.com/windmill-labs/windmill/commit/944795f6eeaa7d01ab1a35a80570a55c363723e6))
* **frontend:** add setTab to frontend scripts ([c2a97c5](https://github.com/windmill-labs/windmill/commit/c2a97c53cfff0fdb35dd8bc249490566eebdc1a9))
* **frontend:** app components output panel ([#1283](https://github.com/windmill-labs/windmill/issues/1283)) ([751edcf](https://github.com/windmill-labs/windmill/commit/751edcf9b8e0976a1d073603c9eff5dc6e714490))
### Bug Fixes
* **backend:** do not cache reference to workspace scripts ([eb73f2a](https://github.com/windmill-labs/windmill/commit/eb73f2a687f6faad301b9038ab8585450bec7481))
* **frontend:** fix app tabs ([#1288](https://github.com/windmill-labs/windmill/issues/1288)) ([c71a577](https://github.com/windmill-labs/windmill/commit/c71a577fead90c9cd01a736b54d859ec4f0b7807))
* **frontend:** fix container deletion ([#1287](https://github.com/windmill-labs/windmill/issues/1287)) ([bc870bd](https://github.com/windmill-labs/windmill/commit/bc870bd03eb76cb8bc0e0c861f6cd8a9c661186b))
* **frontend:** Update setting accordion ([#1285](https://github.com/windmill-labs/windmill/issues/1285)) ([dea12e8](https://github.com/windmill-labs/windmill/commit/dea12e8870ece998bb6607723cbaab9b9a958f22))
## [1.76.0](https://github.com/windmill-labs/windmill/compare/v1.75.0...v1.76.0) (2023-03-13)
### Features
* **frontend:** add frontend (JS) scripts to apps ([f0b1b1f](https://github.com/windmill-labs/windmill/commit/f0b1b1f752731ba434b960a75624118152f53c00))
* **frontend:** Copy, Cut and Paste ([#1279](https://github.com/windmill-labs/windmill/issues/1279)) ([82c139e](https://github.com/windmill-labs/windmill/commit/82c139ed0992be401e250cfb7ecc0fca61b76772))
* **frontend:** disabled for action buttons can now depend on row ([75f87e7](https://github.com/windmill-labs/windmill/commit/75f87e7e1117a9c12afcf626379e94b134a9a493))
* **frontend:** improve drag-n-drop behavior ([cfd489a](https://github.com/windmill-labs/windmill/commit/cfd489a55059e7b6843f99bab261c70b3852e6a2))
### Bug Fixes
* **backend:** improve worker ping api ([c958480](https://github.com/windmill-labs/windmill/commit/c958480ce83844a989f58dd5a70eb288582e2194))
* **frontend:** General fixes and updates ([#1281](https://github.com/windmill-labs/windmill/issues/1281)) ([3e5a179](https://github.com/windmill-labs/windmill/commit/3e5a179eb8cd8001f49c92305141dade1571e20f))
## [1.75.0](https://github.com/windmill-labs/windmill/compare/v1.74.2...v1.75.0) (2023-03-11)
### Features
* add filter jobs by args or result ([3b44f9a](https://github.com/windmill-labs/windmill/commit/3b44f9a72ca0466a44963a4b9657a0ee59b44753))
* **apps:** add resource picker ([8681e83](https://github.com/windmill-labs/windmill/commit/8681e83b574141acbf7e5a389a9e8a4f340336d1))
* **bash:** add default argument handling for bash ([1d5c194](https://github.com/windmill-labs/windmill/commit/1d5c194f09ffba963d52e418c5954843d84ae337))
* **frontend-apps:** add variable picker for static string input on apps ([bc440f8](https://github.com/windmill-labs/windmill/commit/bc440f8d4154ce464c0e027d93b7a0a3b76d782e))
* **frontend:** make runs filters synced with query args ([61a5e1f](https://github.com/windmill-labs/windmill/commit/61a5e1f1accc988628b785b3b9be04c4ea719874))
### Bug Fixes
* **backend:** add killpill for lines reading ([7c825c2](https://github.com/windmill-labs/windmill/commit/7c825c212dd0f1e8be427eabd9a9756303241d1b))
* **cli:** many small fixes ([ce32370](https://github.com/windmill-labs/windmill/commit/ce323709a94d27fb24214719180ea1aafc66d646))
## [1.74.2](https://github.com/windmill-labs/windmill/compare/v1.74.1...v1.74.2) (2023-03-09)
### Bug Fixes
* **frontend:** fix splitpanes navigation ([#1276](https://github.com/windmill-labs/windmill/issues/1276)) ([8d5c5b8](https://github.com/windmill-labs/windmill/commit/8d5c5b88a35d7a3bad1d8ddf2d940026825241eb))
## [1.74.1](https://github.com/windmill-labs/windmill/compare/v1.74.0...v1.74.1) (2023-03-09)
### Bug Fixes
* **apps:** proper reactivity for non rendered static components ([ae53baf](https://github.com/windmill-labs/windmill/commit/ae53bafaf6777f928113f84b2c6ed6a2ed341844))
* **ci:** make windmill compile again by pinning swc deps ([2ea15d5](https://github.com/windmill-labs/windmill/commit/2ea15d5035e5e15473968db3c0501a4dddff5cd0))
## [1.74.0](https://github.com/windmill-labs/windmill/compare/v1.73.1...v1.74.0) (2023-03-09)
### Features
* add delete by path for scripts ([0c2cf92](https://github.com/windmill-labs/windmill/commit/0c2cf92dd3df9610e649f15e23921a4ca0d94e6a))
* **frontend:** Add color picker input to app ([#1270](https://github.com/windmill-labs/windmill/issues/1270)) ([88e537a](https://github.com/windmill-labs/windmill/commit/88e537ad1fb4c207f38fbe951c82106bef6491a3))
* **frontend:** add expand ([#1268](https://github.com/windmill-labs/windmill/issues/1268)) ([b854ee3](https://github.com/windmill-labs/windmill/commit/b854ee34393534bde104e2e6f606108fd66d38dc))
* **frontend:** add hash to ctx in apps ([b1a45b1](https://github.com/windmill-labs/windmill/commit/b1a45b1e708aa6f19f8be9c949507083e044f2d8))
* **frontend:** Add key navigation in app editor ([#1273](https://github.com/windmill-labs/windmill/issues/1273)) ([6b0fb75](https://github.com/windmill-labs/windmill/commit/6b0fb75d23e2151c88b07814139d203c1bd0578d))
### Bug Fixes
* **cli:** improve visibility of the active workspace ([e6344da](https://github.com/windmill-labs/windmill/commit/e6344dac6d1be04b46231fa8ef8579fd12ca8f37))
* **frontend:** add confirmation modal to delete script/flow/app ([a4adcb5](https://github.com/windmill-labs/windmill/commit/a4adcb5192c11f7bf47a0d259825e474779378d7))
* **frontend:** Clean up app editor ([#1267](https://github.com/windmill-labs/windmill/issues/1267)) ([0a5e181](https://github.com/windmill-labs/windmill/commit/0a5e181a3aa966fb8211bee0d9174fc16353b31f))
* **frontend:** Minor changes ([#1272](https://github.com/windmill-labs/windmill/issues/1272)) ([3b6ae0c](https://github.com/windmill-labs/windmill/commit/3b6ae0cc49461b858d9cfff79eae9a7569465235))
* **frontend:** simplify input bindings ([b2de531](https://github.com/windmill-labs/windmill/commit/b2de531a46e4b120d7106d361b727746bec516dd))
## [1.73.1](https://github.com/windmill-labs/windmill/compare/v1.73.0...v1.73.1) (2023-03-07)
### Bug Fixes
* **frontend:** load flow is not initialized ([719d475](https://github.com/windmill-labs/windmill/commit/719d4752621d462b1cfaa0d27930fba7586be779))
## [1.73.0](https://github.com/windmill-labs/windmill/compare/v1.72.0...v1.73.0) (2023-03-07)
### Features
* **frontend:** add a way to automatically resize ([#1259](https://github.com/windmill-labs/windmill/issues/1259)) ([24f58ef](https://github.com/windmill-labs/windmill/commit/24f58efd9994a2201c1b1d9bbfb11734c57068e3))
* **frontend:** add ability to move nodes ([614fb50](https://github.com/windmill-labs/windmill/commit/614fb5022aa7d5428fb96b7ee3a20794edd1e9d3))
* **frontend:** Add app PDF viewer ([#1254](https://github.com/windmill-labs/windmill/issues/1254)) ([3e5d09e](https://github.com/windmill-labs/windmill/commit/3e5d09ef0b5619186bee5ec6d442cbfd12a6e8d5))
* **frontend:** add fork/save buttons + consistent styling for slider/range ([9e9f8ef](https://github.com/windmill-labs/windmill/commit/9e9f8efb8ee389ea75e99b67ef720756959ca737))
* **frontend:** add history to flows and apps ([9e4d90a](https://github.com/windmill-labs/windmill/commit/9e4d90ad37a57ff1f515eea0c82cf603649e915d))
* **frontend:** Fix object viewer style ([#1255](https://github.com/windmill-labs/windmill/issues/1255)) ([94f1aad](https://github.com/windmill-labs/windmill/commit/94f1aadef2b09ac1962478f11b27cc708b8328f1))
* **frontend:** refactor entire flow builder UX ([2ac51b0](https://github.com/windmill-labs/windmill/commit/2ac51b0af08bdef7ce3c7e874e9983b9fc00478a))
### Bug Fixes
* **frontend:** arginput + apppreview fixes ([e2c4545](https://github.com/windmill-labs/windmill/commit/e2c45452401022b00285b21551ffaf35a114be33))
* **frontend:** fix app map reactivity ([#1260](https://github.com/windmill-labs/windmill/issues/1260)) ([2557e13](https://github.com/windmill-labs/windmill/commit/2557e136bd0df1a023819b7d9b2235e30d7140b6))
* **frontend:** fix branch deletion ([#1261](https://github.com/windmill-labs/windmill/issues/1261)) ([a999eb2](https://github.com/windmill-labs/windmill/commit/a999eb21121a7c0010621448324e0c77caf2b3f6))
* **frontend:** Side menu z-index issue ([#1265](https://github.com/windmill-labs/windmill/issues/1265)) ([c638897](https://github.com/windmill-labs/windmill/commit/c638897fdcd58f55b0929f91641b21a6f9d25ead))
## [1.72.0](https://github.com/windmill-labs/windmill/compare/v1.71.0...v1.72.0) (2023-03-02)
### Features
* **backend:** get_result_by_id do a downward pass to find node at any depth ([#1249](https://github.com/windmill-labs/windmill/issues/1249)) ([4c913dc](https://github.com/windmill-labs/windmill/commit/4c913dc4b6be03571a015c97a13829adffb61479))
* **frontend:** Add app map component ([#1251](https://github.com/windmill-labs/windmill/issues/1251)) ([ed25d9f](https://github.com/windmill-labs/windmill/commit/ed25d9f186d9925f75404cb193a025d8a41c4540))
* **frontend:** app splitpanes ([#1248](https://github.com/windmill-labs/windmill/issues/1248)) ([f4d79ee](https://github.com/windmill-labs/windmill/commit/f4d79ee2633e6cdab0fa2410108b31cfa77e10da))
### Bug Fixes
* **backend:** improve result retrieval ([c4463bb](https://github.com/windmill-labs/windmill/commit/c4463bb029907f3c8d77abb194f872aae7876bf6))
* **backend:** incorrect get_result_by_id for list_result job ([2a75cd2](https://github.com/windmill-labs/windmill/commit/2a75cd250ea5e01849fc8bbb69bf44f147d0acb8))
* **cli:** fix workspace option + run script/flow + whoami ([35ea2b2](https://github.com/windmill-labs/windmill/commit/35ea2b27b12159c68c8507ec1f8686028c975387))
* **frontend:** background script not showing inputs ([55eb48c](https://github.com/windmill-labs/windmill/commit/55eb48c55332431304cedbf3bcbbbcff61ec3645))
* **frontend:** fix table bindings ([2679386](https://github.com/windmill-labs/windmill/commit/2679386bf87a56352269911bd89e52df5ee9f314))
* **frontend:** rework app reactivity ([94b20d2](https://github.com/windmill-labs/windmill/commit/94b20d2f5e3b551974c57ea82b6e3dc16e97b9b8))
* **frontend:** rework app reactivity ([1753cb7](https://github.com/windmill-labs/windmill/commit/1753cb7da658f47be974c15da82c71a8e19309a6))
## [1.71.0](https://github.com/windmill-labs/windmill/compare/v1.70.1...v1.71.0) (2023-02-28)
### Features
* **backend:** use counter for sleep/execution/pull durations ([e568690](https://github.com/windmill-labs/windmill/commit/e56869092a03fec4703ddd9ef65c89edb8122962))
* **cli:** add autocompletions ([287b2db](https://github.com/windmill-labs/windmill/commit/287b2db22f7b56e90bcd0c4727c00096695c2e0d))
* **frontend:** App drawer ([#1246](https://github.com/windmill-labs/windmill/issues/1246)) ([8a0d115](https://github.com/windmill-labs/windmill/commit/8a0d1158c4d7e970cb91e1adf4838e5efdbb39ff))
* **frontend:** drawer for editing workspace scripts in flows ([6adc875](https://github.com/windmill-labs/windmill/commit/6adc87561070d8aceaba1838008cd7e6be2e2660))
### Bug Fixes
* **frontend:** Add more app custom css ([#1229](https://github.com/windmill-labs/windmill/issues/1229)) ([a4e4d18](https://github.com/windmill-labs/windmill/commit/a4e4d188ad10443dd0b7f104389594efc768dc59))
* **frontend:** Add more app custom css ([#1247](https://github.com/windmill-labs/windmill/issues/1247)) ([1bb5ed9](https://github.com/windmill-labs/windmill/commit/1bb5ed9ae01fd7998b06833b6222e5dd5d774d35))
* **frontend:** display currently selected filter even if not in list ([42d1cd6](https://github.com/windmill-labs/windmill/commit/42d1cd6456620ba917c560c87d736dc93634adff))
* **frontend:** Fix deeply nested move ([#1245](https://github.com/windmill-labs/windmill/issues/1245)) ([a67f10e](https://github.com/windmill-labs/windmill/commit/a67f10eeb6fdb44bbb3a510badcc5ad0ae187a2b))
* **frontend:** invisible subgrids have h-0 + app policies fix ([2244e83](https://github.com/windmill-labs/windmill/commit/2244e83b9da803a4cf46ab0825d7cb6cb0e24872))
## [1.70.1](https://github.com/windmill-labs/windmill/compare/v1.70.0...v1.70.1) (2023-02-27)

View File

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

View File

@@ -73,7 +73,7 @@ ARG features=""
COPY --from=planner /windmill/recipe.json recipe.json
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef cook --release --features "$features" --recipe-path recipe.json
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
COPY ./backend ./
@@ -85,7 +85,8 @@ COPY .git/ .git/
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
FROM python:3.11.2-slim-buster
FROM python:3.11.3-slim-buster
ARG TARGETPLATFORM
ARG APP=/usr/src/app
@@ -123,12 +124,17 @@ ENV TZ=Etc/UTC
RUN /usr/local/bin/python3 -m pip install pip-tools
COPY --from=frontend /frontend/build /static_frontend
COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
COPY --from=denoland/deno:latest /usr/bin/deno /usr/bin/deno
# docker does not support conditional COPY and we want to use the same Dockerfile for both amd64 and arm64 and privilege the official image
COPY --from=lukechannings/deno:latest /usr/bin/deno /usr/bin/deno-arm
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then rm /usr/bin/deno-arm; elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then mv /usr/bin/deno-arm /usr/bin/deno; fi
RUN mkdir -p ${APP}
WORKDIR ${APP}

View File

@@ -8,5 +8,9 @@ or belonging to one of the below cases:
The files under backend/ are AGPL Licensed.
The files under frontend/ are AGPL Licensed.
The files under python-client/ are Apache 2.0 Licensed.
The files under community/ are Apache 2.0 Licensed.
The files under python-client/ deno-client/ go-client/ are Apache 2.0 Licensed.
The openapi files, including the OpenFlow spec is Apache 2.0 Licensed.
All third party components incorporated into the Windmill Software are licensed under the
original license provided by the owner of the applicable component.

View File

@@ -26,7 +26,8 @@ Open-source developer infrastructure for internal tools. Self-hostable alternati
# Windmill - Turn scripts into workflows and UIs that you can share and run at scale
Windmill is <b>fully open-sourced (AGPLv3)</b> and Windmill Labs offers dedicated instance and commercial support and licenses.
Windmill is <b>fully open-sourced (AGPLv3)</b> and Windmill Labs offers
dedicated instance and commercial support and licenses.
![Windmill Diagram](/imgs/stacks.svg)
@@ -69,12 +70,12 @@ https://user-images.githubusercontent.com/275584/218350457-bc2fdc3b-e667-4da5-a2
3. Make it flow! You can chain your scripts or scripts made by the community
shared on [WindmillHub](https://hub.windmill.dev).
![Step 4](./imgs/windmill-flow.png)
![Step 3](./imgs/windmill-flow.png)
4. Build complex UI on top of your scripts and flows.
![Step 5](./imgs/windmill-builder.png)
![Step 4](./imgs/windmill-builder.png)
Scripts and flows can also be triggered by a cron schedule '*/5 * * * *' or
Scripts and flows can also be triggered by a cron schedule '_/5 _ \* \* \*' or
through webhooks.
You can build your entire infra on top of Windmill!
@@ -82,46 +83,49 @@ 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 * 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'
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">) {
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());
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 };
// 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 local files, github repos and to run scripts and flows on the instance from local commands. 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)
### 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/>
You can run your script locally easily, you simply need to pass the right
environment variables for the `wmill` client library to fetch resources and
variables from your instance if necessary. See more:
<https://docs.windmill.dev/docs/advanced/local_development/>
## Stack
@@ -182,15 +186,19 @@ compiling from source or using without a postgres super user, see
### Docker compose
`docker compose up` with the following docker-compose is sufficient:
<https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml>
```
curl https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml -o docker-compose.yml
curl https://github.com/windmill-labs/windmill/blob/main/CaddyFile -o Caddyfile
curl https://github.com/windmill-labs/windmill/blob/main/.env -o .env
docker compose up -d --pull always
```
Go to http://localhost et voilà :)
The default super-admin user is: admin@windmill.dev / changeme
From there, you can create other users (do not forget to change the password!)
From there, you can follow the setup app and creat other users.
### Kubernetes (k8s) and Helm charts
@@ -199,9 +207,9 @@ We publish helm charts at:
### 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.
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
@@ -275,8 +283,8 @@ 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). A setup script will prompt
you to have it being synced automatically everyday.
[WindmillHub](https://hub.windmill.dev). A setup script will prompt you to have
it being synced automatically everyday.
## Environment Variables
@@ -284,14 +292,17 @@ you to have it being synced automatically everyday.
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
| PORT | 8000 | Exposed port | Server | |
| SERVER_BIND_ADDR | 0.0.0.0 | IP Address on which to bind listening socket | Server |
| 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 |
| TIMEOUT | 300 | The maximum time of execution of a script. When reached, the job is failed as having timedout. | Worker |
| ZOMBIE_JOB_TIMEOUT | 30 | The timeout after which a job is considered to be zombie if the worker did not send pings about processing the job (every server check for zombie jobs every 30s) | Server |
| RESTART_ZOMBIE_JOBS | true | If true then a zombie job is restarted (in-place with the same uuid and some logs), if false the zombie job is failed | Server |
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
| MAX_LOG_SIZE | 500000 | The maximum number of characters a job can emit (log + result) | Worker |
| DISABLE_NUSER | false | If Nsjail is enabled, disable the nsjail's `clone_newuser` setting | Worker |
@@ -300,10 +311,12 @@ you to have it being synced automatically everyday.
| 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 | |
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server |
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
| PYTHON_PATH | /usr/local/bin/python3 | The path to the python binary. | Worker |
| GO_PATH | /usr/bin/go | The path to the go binary. | Worker |
| GOPRIVATE | | The GOPRIVATE env variable to use private go modules | Worker |
| NETRC | | The netrc content to use a private go registry | Worker |
| PIP_INDEX_URL | None | The index url to pass for pip. | Worker |
| PIP_EXTRA_INDEX_URL | None | The extra index url to pass to pip. | Worker |
| PIP_TRUSTED_HOST | None | The trusted host to pass to pip. | Worker |
@@ -315,12 +328,13 @@ you to have it being synced automatically everyday.
| 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 |
| NPM_CONFIG_REGISTRY | None | Registry to use for NPM dependencies, set if you have a private repository you need to use instead of the default public NPM registry | Worker |
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | |
| ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker |
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |
| 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 |
| INSTANCE_EVENTS_WEBHOOK | None | Webhook to notify of events such as new user added, signup/invite. Can hook back to windmill to send emails | Server |
## Run a local dev setup

View File

@@ -8,3 +8,14 @@ rustflags = [
]
incremental = true
[target.x86_64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
[target.aarch64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]

1432
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.70.1"
version = "1.87.0"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.70.1"
version = "1.87.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -28,11 +28,7 @@ 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
@@ -52,12 +48,16 @@ git-version.workspace = true
rsa.workspace = true
base64.workspace = true
sha2.workspace = true
rsmq_async.workspace = true
url.workspace = true
[dev-dependencies]
serde_json.workspace = true
reqwest.workspace = true
windmill-queue.workspace = true
axum.workspace = true
serde.workspace = true
[workspace.dependencies]
windmill-api = { path = "./windmill-api" }
@@ -76,7 +76,7 @@ headers = "^0"
hyper = { version = "^0", features = ["full"] }
tokio = { version = "^1", features = ["full", "tracing"] }
tower = "^0"
tower-http = { version = "^0", features = ["trace"] }
tower-http = { version = "^0", features = ["trace", "cors"] }
tower-cookies = "^0"
serde = "^1"
serde_json = { version = "^1", features = ["preserve_order"] }
@@ -84,6 +84,7 @@ uuid = { version = "^1", features = ["serde", "v4"] }
thiserror = "^1"
anyhow = "^1"
chrono = { version = "^0", features = ["serde"] }
chrono-tz = "^0"
tracing = "^0"
tracing-subscriber = { version = "^0", features = ["env-filter", "json"] }
prometheus = { version = "^0", default-features = false }
@@ -122,9 +123,9 @@ itertools = "^0"
regex = "^1"
deno_core = "^0"
async-recursion = "^1"
swc_common = "^0"
swc_ecma_parser = "^0"
swc_ecma_ast = "^0"
swc_common = "0.29.39"
swc_ecma_parser = "0.128.2"
swc_ecma_ast = "0.98.1"
base64 = "0.21.0"
unicode-general-category = "^0"
hmac = "0.12.1"
@@ -142,6 +143,7 @@ sqlx = { version = "^0", features = [
dotenv = "^0"
ulid = { version = "^1", features = ["uuid"] }
futures = "^0"
futures-core = "^0"
tokio-metrics = "0.1.0"
lazy_static = "1.4.0"
serde_derive = "1.0.147"
@@ -153,3 +155,8 @@ async-stripe = { version = "0.14", features = [
"checkout",
] }
async_zip = { version = "0.0.11", features = ["full"] }
once_cell = "1.17.1"
rsmq_async = { version = "5.1.5" }
gosyn = "0.2.2"
[patch.crates-io]

View File

@@ -1165,18 +1165,3 @@ VALUES
}') RETURNING id)
UPDATE app SET versions = ARRAY((select id from _insert)), policy = '{ "execution_mode": "viewer", "triggerables": {} }'
WHERE workspace_id = 'admins' AND path = 'g/all/setup_app';
UPDATE script SET content = 'import wmill from "https://deno.land/x/wmill@v1.69.3/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

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

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE queue ADD COLUMN root_job uuid;
ALTER TABLE queue ADD COLUMN leaf_jobs jsonb;

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,11 @@
-- Add up migration script here
CREATE POLICY see_extra_perms_user ON app FOR ALL
USING (extra_perms ? CONCAT('u/', current_setting('session.user')))
WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_groups ON app FOR ALL
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
WITH CHECK (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));

View File

@@ -0,0 +1,2 @@
ALTER TABLE schedule DROP COLUMN timezone;
ALTER TABLE schedule ADD COLUMN offset_ INTEGER NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,26 @@
ALTER TABLE schedule ADD COLUMN timezone VARCHAR(255) NOT NULL DEFAULT 'UTC';
-- INSERT the correct IANA timezone string for each offset value
UPDATE schedule SET timezone = 'Pacific/Honolulu' WHERE offset_ = 600;
UPDATE schedule SET timezone = 'America/Anchorage' WHERE offset_ = 540;
UPDATE schedule SET timezone = 'America/Los_Angeles' WHERE offset_ = 480;
UPDATE schedule SET timezone = 'America/Chicago' WHERE offset_ = 360;
UPDATE schedule SET timezone = 'America/New_York' WHERE offset_ = 300;
UPDATE schedule SET timezone = 'America/Halifax' WHERE offset_ = 240;
UPDATE schedule SET timezone = 'America/Sao_Paulo' WHERE offset_ = 180;
UPDATE schedule SET timezone = 'Atlantic/South_Georgia' WHERE offset_ = 120;
UPDATE schedule SET timezone = 'Atlantic/Cape_Verde' WHERE offset_ = 60;
UPDATE schedule SET timezone = 'Europe/London' WHERE offset_ = 0;
UPDATE schedule SET timezone = 'Europe/Berlin' WHERE offset_ = -60;
UPDATE schedule SET timezone = 'Europe/Athens' WHERE offset_ = -120;
UPDATE schedule SET timezone = 'Europe/Moscow' WHERE offset_ = -180;
UPDATE schedule SET timezone = 'Asia/Dubai' WHERE offset_ = -240;
UPDATE schedule SET timezone = 'Asia/Aqtau' WHERE offset_ = -300;
UPDATE schedule SET timezone = 'Asia/Almaty' WHERE offset_ = -360;
UPDATE schedule SET timezone = 'Asia/Bangkok' WHERE offset_ = -420;
UPDATE schedule SET timezone = 'Asia/Hong_Kong' WHERE offset_ = -480;
UPDATE schedule SET timezone = 'Asia/Tokyo' WHERE offset_ = -540;
UPDATE schedule SET timezone = 'Australia/Sydney' WHERE offset_ = -600;
ALTER TABLE schedule DROP COLUMN offset_;

View File

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE password ALTER COLUMN company TYPE VARCHAR(255);
ALTER TABLE password ALTER COLUMN name TYPE VARCHAR(255);

View File

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

View File

@@ -0,0 +1,6 @@
-- Add up migration script here
CREATE TABLE pip_resolution_cache(
hash VARCHAR(255) PRIMARY KEY,
expiration TIMESTAMP NOT NULL,
lockfile TEXT NOT NULL
);

View File

@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS input;
DROP TYPE RUNNABLE_TYPE;

View File

@@ -0,0 +1,13 @@
CREATE TYPE RUNNABLE_TYPE AS ENUM ('ScriptHash', 'ScriptPath', 'FlowPath');
CREATE TABLE IF NOT EXISTS input (
id UUID PRIMARY KEY,
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id),
runnable_id VARCHAR(255) NOT NULL,
runnable_type RUNNABLE_TYPE NOT NULL,
name TEXT NOT NULL,
args JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by VARCHAR(50) NOT NULL,
is_public BOOLEAN NOT NULL DEFAULT FALSE
);

View File

@@ -16,4 +16,5 @@ unicode-general-category.workspace = true
itertools.workspace = true
anyhow.workspace = true
regex.workspace = true
lazy_static.workspace = true
lazy_static.workspace = true
serde_json.workspace = true

View File

@@ -1,6 +1,8 @@
#![allow(non_snake_case)] // TODO: switch to parse_* function naming
use anyhow::anyhow;
use regex::Regex;
use serde_json::json;
use std::collections::HashMap;
use windmill_parser::{Arg, MainArgSignature, Typ};
@@ -17,19 +19,32 @@ pub fn parse_bash_sig(code: &str) -> windmill_common::error::Result<MainArgSigna
}
}
lazy_static::lazy_static! {
static ref RE: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+):-(.*)\})"$"#).unwrap();
}
fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let mut hm = HashMap::new();
let re = Regex::new(r#"(?m)^(\w+)="\$(\d+)"$"#).unwrap();
for cap in re.captures_iter(code) {
hm.insert(cap[2].parse::<i32>()?, cap[1].to_string());
let mut hm: HashMap<i32, (String, Option<String>)> = HashMap::new();
for cap in RE.captures_iter(code) {
hm.insert(
cap.get(2)
.or(cap.get(3))
.and_then(|x| x.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Impossible to parse arg digit"))?,
(
cap[1].to_string(),
cap.get(4).map(|x| x.as_str().to_string()),
),
);
}
let mut args = vec![];
for i in 1..20 {
if hm.contains_key(&i) {
let (name, default) = hm.get(&i).unwrap();
args.push(Arg {
name: hm[&i].clone(),
name: name.clone(),
typ: Typ::Str(None),
default: None,
default: default.clone().map(|x| json!(x)),
otyp: None,
has_default: false,
});
@@ -43,6 +58,8 @@ fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
@@ -50,8 +67,7 @@ mod tests {
let code = r#"
token="$1"
image="$2"
digest="${3:-latest}"
foo="$4"
digest="${3:-latest with spaces}"
"#;
//println!("{}", serde_json::to_string()?);
@@ -74,6 +90,13 @@ foo="$4"
typ: Typ::Str(None),
default: None,
has_default: false
},
Arg {
otyp: None,
name: "digest".to_string(),
typ: Typ::Str(None),
default: Some(json!("latest with spaces")),
has_default: false
}
]
}

View File

@@ -15,3 +15,4 @@ phf.workspace = true
unicode-general-category.workspace = true
itertools.workspace = true
anyhow.workspace = true
gosyn.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,347 +0,0 @@
#![allow(clippy::large_enum_variant)] // TODO: we allow large enum variant for now, let's profile properly to see if we want to box.
use crate::parser_go_token::{Position, Token};
use std::collections::BTreeMap;
// https://pkg.go.dev/go/ast#CommentGroup
#[derive(Debug)]
pub struct CommentGroup {
// List []*Comment // len(List) > 0
}
// https://pkg.go.dev/go/ast#FieldList
#[derive(Debug)]
pub struct FieldList<'a> {
pub opening: Option<Position<'a>>, // position of opening parenthesis/brace, if any
pub list: Vec<Field<'a>>, // field list; or nil
pub closing: Option<Position<'a>>, // position of closing parenthesis/brace, if any
}
// https://pkg.go.dev/go/ast#Field
#[derive(Debug)]
pub struct Field<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub names: Option<Vec<Ident<'a>>>, // field/method/(type) parameter names, or type "type"; or nil
pub type_: Option<Expr<'a>>, // field/method/parameter type, type list type; or nil
pub tag: Option<BasicLit<'a>>, // field tag; or nil
pub comment: Option<CommentGroup>, // line comments; or nil
}
// https://pkg.go.dev/go/ast#File
#[derive(Debug)]
pub struct File<'a> {
// package name
pub decls: Vec<Decl<'a>>, // top-level declarations; or nil // list of all comments in the source file
}
// https://pkg.go.dev/go/ast#FuncDecl
#[derive(Debug)]
pub struct FuncDecl<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub recv: Option<FieldList<'a>>, // receiver (methods); or nil (functions)
pub name: Ident<'a>, // function/method name
pub type_: FuncType<'a>, // function signature: type and value parameters, results, and position of "func" keyword
pub body: Option<BlockStmt<'a>>, // function body; or nil for external (non-Go) function
}
// https://pkg.go.dev/go/ast#BlockStmt
#[derive(Debug)]
pub struct BlockStmt<'a> {
pub lbrace: Position<'a>, // position of "{"
pub list: Vec<Stmt>,
pub rbrace: Position<'a>, // position of "}", if any (may be absent due to syntax error)
}
// https://pkg.go.dev/go/ast#FuncType
#[derive(Debug)]
pub struct FuncType<'a> {
pub func: Option<Position<'a>>, // position of "func" keyword (token.NoPos if there is no "func")
pub params: FieldList<'a>, // (incoming) parameters; non-nil
pub results: Option<FieldList<'a>>, // (outgoing) results; or nil
}
// https://pkg.go.dev/go/ast#Ident
#[derive(Debug)]
pub struct Ident<'a> {
pub name_pos: Position<'a>, // identifier position
pub name: &'a str, // identifier name
pub obj: Option<Box<Object<'a>>>, // denoted object; or nil
}
// https://pkg.go.dev/go/ast#ValueSpec
#[derive(Debug)]
pub struct ValueSpec<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub names: Vec<Ident<'a>>, // value names (len(Names) > 0)
pub type_: Option<Expr<'a>>, // value type; or nil
pub values: Option<Vec<Expr<'a>>>, // initial values; or nil
pub comment: Option<CommentGroup>, // line comments; or nil
}
// https://pkg.go.dev/go/ast#BasicLit
#[derive(Debug)]
pub struct BasicLit<'a> {
pub value_pos: Position<'a>, // literal position
pub kind: Token, // token.INT, token.FLOAT, token.IMAG, token.CHAR, or token.STRING
pub value: &'a str, // literal string; e.g. 42, 0x7f, 3.14, 1e-9, 2.4i, 'a', '\x7f', "foo" or `\m\n\o`
}
// https://pkg.go.dev/go/ast#Object
#[derive(Debug)]
pub struct Object<'a> {
pub kind: ObjKind,
pub name: &'a str, // declared name
pub decl: Option<ObjDecl>, // corresponding Field, XxxSpec, FuncDecl, LabeledStmt, AssignStmt, Scope; or nil
pub data: Option<usize>, // object-specific data; or nil
pub type_: Option<()>, // placeholder for type information; may be nil
}
// https://pkg.go.dev/go/ast#Ellipsis
#[derive(Debug)]
pub struct Ellipsis<'a> {
pub ellipsis: Position<'a>, // position of "..."
pub elt: Option<Box<Expr<'a>>>, // ellipsis element type (parameter lists only); or nil
}
// https://pkg.go.dev/go/ast#Ellipsis
#[derive(Debug)]
pub struct TypeAssertExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub lparen: Position<'a>, // position of "("
pub type_: Box<Expr<'a>>, // asserted type; nil means type switch X.(type)
pub rparen: Position<'a>, // position of ")"
}
// https://pkg.go.dev/go/ast#SliceExpr
#[derive(Debug)]
pub struct SliceExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub lbrack: Position<'a>, // position of "["
pub low: Option<Box<Expr<'a>>>, // begin of slice range; or nil
pub high: Option<Box<Expr<'a>>>, // end of slice range; or nil
pub max: Option<Box<Expr<'a>>>, // maximum capacity of slice; or nil
pub slice3: bool, // true if 3-index slice (2 colons present)
pub rbrack: Position<'a>, // position of "]"
}
// https://pkg.go.dev/go/ast#ObjKind
#[derive(Debug)]
pub enum ObjKind {}
#[derive(Debug)]
pub enum ObjDecl {}
// https://pkg.go.dev/go/ast#Decl
#[derive(Debug)]
pub enum Decl<'a> {
FuncDecl(FuncDecl<'a>),
}
// https://pkg.go.dev/go/ast#Scope
#[derive(Debug)]
pub struct Scope<'a> {
pub outer: Option<Box<Scope<'a>>>,
pub objects: BTreeMap<&'a str, Object<'a>>,
}
// https://pkg.go.dev/go/ast#GenDecl
#[derive(Debug)]
pub struct GenDecl<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub tok_pos: Position<'a>, // position of Tok
pub tok: Token, // IMPORT, CONST, TYPE, or VAR
pub lparen: Option<Position<'a>>, // position of '(', if any
pub specs: Vec<Spec>,
pub rparen: Option<Position<'a>>, // position of ')', if any
}
// https://pkg.go.dev/go/ast#AssignStmt
#[derive(Debug)]
pub struct AssignStmt<'a> {
pub lhs: Vec<Expr<'a>>,
pub tok_pos: Position<'a>, // position of Tok
pub tok: Token, // assignment token, DEFINE
pub rhs: Vec<Expr<'a>>,
}
// https://pkg.go.dev/go/ast#BinaryExpr
#[derive(Debug)]
pub struct BinaryExpr<'a> {
pub x: Box<Expr<'a>>, // left operand
pub op_pos: Position<'a>, // position of Op
pub op: Token, // operator
pub y: Box<Expr<'a>>, // right operand
}
// https://pkg.go.dev/go/ast#ReturnStmt
#[derive(Debug)]
pub struct ReturnStmt<'a> {
pub return_: Position<'a>, // position of "return" keyword
pub results: Vec<Expr<'a>>, // result expressions; or nil
}
// https://pkg.go.dev/go/ast#TypeSpec
#[derive(Debug)]
pub struct TypeSpec<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub name: Option<Ident<'a>>, // type name
pub assign: Option<Position<'a>>, // position of '=', if any
pub type_: Expr<'a>, // *Ident, *ParenExpr, *SelectorExpr, *StarExpr, or any of the *XxxTypes
pub comment: Option<CommentGroup>, // line comments; or nil
}
// https://pkg.go.dev/go/ast#StructType
#[derive(Debug)]
pub struct StructType<'a> {
pub struct_: Position<'a>, // position of "struct" keyword
pub fields: Option<FieldList<'a>>, // list of field declarations
pub incomplete: bool, // true if (source) fields are missing in the Fields list
}
// https://pkg.go.dev/go/ast#StarExpr
#[derive(Debug)]
pub struct StarExpr<'a> {
pub star: Position<'a>, // position of "*"
pub x: Box<Expr<'a>>, // operand
}
// https://pkg.go.dev/go/ast#InterfaceType
#[derive(Debug)]
pub struct InterfaceType<'a> {
pub interface: Position<'a>, // position of "interface" keyword
pub methods: Option<FieldList<'a>>, // list of embedded interfaces, methods, or types
pub incomplete: bool, // true if (source) methods or types are missing in the Methods list
}
// https://pkg.go.dev/go/ast#UnaryExpr
#[derive(Debug)]
pub struct UnaryExpr<'a> {
pub op_pos: Position<'a>, // position of Op
pub op: Token, // operator
pub x: Box<Expr<'a>>, // operand
}
// https://pkg.go.dev/go/ast#CallExpr
#[derive(Debug)]
pub struct CallExpr<'a> {
pub fun: Box<Expr<'a>>, // function expression
pub lparen: Position<'a>, // position of "("
pub args: Option<Vec<Expr<'a>>>, // function arguments; or nil
pub ellipsis: Option<Position<'a>>, // position of "..." (token.NoPos if there is no "...")
pub rparen: Position<'a>, // position of ")"
}
// https://pkg.go.dev/go/ast#SelectorExpr
#[derive(Debug)]
pub struct SelectorExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub sel: Ident<'a>, // field selector
}
// https://pkg.go.dev/go/ast#ParenExpr
#[derive(Debug)]
pub struct ParenExpr<'a> {
pub lparen: Position<'a>, // position of "("
pub x: Box<Expr<'a>>, // parenthesized expression
pub rparen: Position<'a>, // position of ")"
}
// https://pkg.go.dev/go/ast#FuncLit
#[derive(Debug)]
pub struct FuncLit<'a> {
pub type_: FuncType<'a>, // function type
pub body: BlockStmt<'a>, // function body
}
// https://pkg.go.dev/go/ast#ChanType
#[derive(Debug)]
pub struct ChanType<'a> {
pub begin: Position<'a>, // position of "chan" keyword or "<-" (whichever comes first)
pub arrow: Option<Position<'a>>, // position of "<-" (token.NoPos if there is no "<-")
pub dir: u8, // channel direction
pub value: Box<Expr<'a>>, // value type
}
// htt/opt/visual-studio-code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.htmlps://pkg.go.dev/go/ast#IndexExpr
#[derive(Debug)]
pub struct IndexExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub lbrack: Position<'a>, // position of "["
pub index: Box<Expr<'a>>, // index expression
pub rbrack: Position<'a>, // position of "]"
}
// https://pkg.go.dev/go/ast#MapType
#[derive(Debug)]
pub struct MapType<'a> {
pub map: Position<'a>,
pub key: Box<Expr<'a>>,
pub value: Box<Expr<'a>>,
}
// https://pkg.go.dev/go/ast#CompositeLit
#[derive(Debug)]
pub struct CompositeLit<'a> {
pub type_: Box<Expr<'a>>, // literal type; or nil
pub lbrace: Position<'a>, // position of "{"
pub elts: Option<Vec<Expr<'a>>>, // list of composite elements; or nil
pub rbrace: Position<'a>, // position of "}"
pub incomplete: bool, // true if (source) expressions are missing in the Elts list
}
// https://pkg.go.dev/go/ast#KeyValueExpr
#[derive(Debug)]
pub struct KeyValueExpr<'a> {
pub key: Box<Expr<'a>>,
pub colon: Position<'a>, // position of ":"
pub value: Box<Expr<'a>>,
}
// https://pkg.go.dev/go/ast#ArrayType
#[derive(Debug)]
pub struct ArrayType<'a> {
pub lbrack: Position<'a>, // position of "["
pub len: Option<Box<Expr<'a>>>, // Ellipsis node for [...]T array types, nil for slice types
pub elt: Box<Expr<'a>>, // element type
}
// https://pkg.go.dev/go/ast#ChanDir
#[derive(Debug)]
pub enum ChanDir {
SEND = 1 << 0,
RECV = 1 << 1,
}
// https://pkg.go.dev/go/ast#Spec
#[derive(Debug)]
pub enum Spec {}
// https://pkg.go.dev/go/ast#Expr
#[derive(Debug)]
pub enum Expr<'a> {
ArrayType(ArrayType<'a>),
BasicLit(BasicLit<'a>),
BinaryExpr(BinaryExpr<'a>),
CallExpr(CallExpr<'a>),
ChanType(ChanType<'a>),
CompositeLit(CompositeLit<'a>),
Ellipsis(Ellipsis<'a>),
FuncLit(FuncLit<'a>),
FuncType(FuncType<'a>),
Ident(Ident<'a>),
IndexExpr(IndexExpr<'a>),
InterfaceType(InterfaceType<'a>),
KeyValueExpr(KeyValueExpr<'a>),
MapType(MapType<'a>),
ParenExpr(ParenExpr<'a>),
SelectorExpr(SelectorExpr<'a>),
SliceExpr(SliceExpr<'a>),
StarExpr(StarExpr<'a>),
StructType(StructType<'a>),
TypeAssertExpr(TypeAssertExpr<'a>),
UnaryExpr(UnaryExpr<'a>),
}
// https://pkg.go.dev/go/ast#Stmt
#[derive(Debug)]
pub enum Stmt {}

View File

@@ -1,948 +0,0 @@
// https://golang.org/ref/spec#Lexical_elements
use crate::parser_go_token::{Position, Token};
use phf::{phf_map, Map};
use std::fmt;
use unicode_general_category::{get_general_category, GeneralCategory};
pub type Step<'a> = (Position<'a>, Token, &'a str);
#[derive(Debug)]
pub enum ScannerError {
HexadecimalNotFound,
OctalNotFound,
UnterminatedComment,
UnterminatedEscapedChar,
UnterminatedRune,
UnterminatedString,
InvalidDirective,
}
impl std::error::Error for ScannerError {}
impl fmt::Display for ScannerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "scanner error: {:?}", self)
}
}
pub type Result<T> = std::result::Result<T, ScannerError>;
#[derive(Debug)]
pub struct Scanner<'a> {
directory: &'a str,
file: &'a str,
buffer: &'a str,
//
chars: std::iter::Peekable<std::str::Chars<'a>>,
current_char: Option<char>,
current_char_len: usize,
//
offset: usize,
line: usize,
column: usize,
start_offset: usize,
start_line: usize,
start_column: usize,
//
hide_column: bool,
insert_semi: bool,
pending_line_info: Option<LineInfo<'a>>,
}
type LineInfo<'a> = (Option<&'a str>, usize, Option<usize>, bool);
impl<'a> Scanner<'a> {
pub fn new(filename: &'a str, buffer: &'a str) -> Self {
let (directory, file) = filename.rsplit_once('/').unwrap_or(("", filename));
let mut s = Scanner {
directory,
file,
buffer,
//
chars: buffer.chars().peekable(),
current_char: None,
current_char_len: 0,
//
offset: 0,
line: 1,
column: 1,
start_offset: 0,
start_line: 1,
start_column: 1,
//
hide_column: false,
insert_semi: false,
pending_line_info: None,
};
s.next(); // read the first character
s
}
#[allow(clippy::cognitive_complexity)] // Allow complex scan function
pub fn scan(&mut self) -> Result<Step<'a>> {
let insert_semi = self.insert_semi;
self.insert_semi = false;
while let Some(c) = self.current_char {
self.reset_start();
match c {
' ' | '\t' | '\r' => {
self.next();
}
'\n' => {
self.next();
if insert_semi {
return Ok((self.position(), Token::SEMICOLON, "\n"));
}
}
_ => break,
}
}
if let Some(c) = self.current_char {
match c {
'+' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::ADD_ASSIGN, ""));
}
Some('+') => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::INC, ""));
}
_ => return Ok((self.position(), Token::ADD, "")),
}
}
'-' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::SUB_ASSIGN, ""));
}
Some('-') => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::DEC, ""));
}
_ => return Ok((self.position(), Token::SUB, "")),
}
}
'*' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::MUL_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::MUL, "")),
}
}
'/' => match self.peek() {
Some('=') => {
self.next();
self.next();
return Ok((self.position(), Token::QUO_ASSIGN, ""));
}
Some('/') => {
if insert_semi {
return Ok((self.position(), Token::SEMICOLON, "\n"));
}
return self.scan_line_comment();
}
Some('*') => {
if insert_semi && self.find_line_end() {
return Ok((self.position(), Token::SEMICOLON, "\n"));
}
return self.scan_general_comment();
}
_ => {
self.next();
return Ok((self.position(), Token::QUO, ""));
}
},
'%' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::REM_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::REM, "")),
}
}
'&' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::AND_ASSIGN, ""));
}
Some('&') => {
self.next();
return Ok((self.position(), Token::LAND, ""));
}
Some('^') => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::AND_NOT_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::AND_NOT, "")),
}
}
_ => return Ok((self.position(), Token::AND, "")),
}
}
'|' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::OR_ASSIGN, ""));
}
Some('|') => {
self.next();
return Ok((self.position(), Token::LOR, ""));
}
_ => return Ok((self.position(), Token::OR, "")),
}
}
'^' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::XOR_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::XOR, "")),
}
}
'<' => {
self.next();
match self.current_char {
Some('<') => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::SHL_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::SHL, "")),
}
}
Some('=') => {
self.next();
return Ok((self.position(), Token::LEQ, ""));
}
Some('-') => {
self.next();
return Ok((self.position(), Token::ARROW, ""));
}
_ => return Ok((self.position(), Token::LSS, "")),
}
}
'>' => {
self.next();
match self.current_char {
Some('>') => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::SHR_ASSIGN, ""));
}
_ => {
return Ok((self.position(), Token::SHR, ""));
}
}
}
Some('=') => {
self.next();
return Ok((self.position(), Token::GEQ, ""));
}
_ => return Ok((self.position(), Token::GTR, "")),
}
}
':' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::DEFINE, ""));
}
_ => return Ok((self.position(), Token::COLON, "")),
}
}
'!' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::NEQ, ""));
}
_ => return Ok((self.position(), Token::NOT, "")),
}
}
',' => {
self.next();
return Ok((self.position(), Token::COMMA, ""));
}
'(' => {
self.next();
return Ok((self.position(), Token::LPAREN, ""));
}
')' => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::RPAREN, ""));
}
'[' => {
self.next();
return Ok((self.position(), Token::LBRACK, ""));
}
']' => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::RBRACK, ""));
}
'{' => {
self.next();
return Ok((self.position(), Token::LBRACE, ""));
}
'}' => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::RBRACE, ""));
}
';' => {
self.next();
return Ok((self.position(), Token::SEMICOLON, ";"));
}
'.' => {
self.next();
match self.current_char {
Some('0'..='9') => return self.scan_int_or_float_or_imag(true),
Some('.') => match self.peek() {
Some('.') => {
self.next();
self.next();
return Ok((self.position(), Token::ELLIPSIS, ""));
}
_ => return Ok((self.position(), Token::PERIOD, "")),
},
_ => return Ok((self.position(), Token::PERIOD, "")),
}
}
'=' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::EQL, ""));
}
_ => return Ok((self.position(), Token::ASSIGN, "")),
}
}
'0'..='9' => return self.scan_int_or_float_or_imag(false),
'\'' => return self.scan_rune(),
'"' => return self.scan_interpreted_string(),
'`' => return self.scan_raw_string(),
_ => return self.scan_pkg_or_keyword_or_ident(),
};
}
self.reset_start();
if insert_semi {
Ok((self.position(), Token::SEMICOLON, "\n"))
} else {
Ok((self.position(), Token::EOF, ""))
}
}
// https://golang.org/ref/spec#Keywords
// https://golang.org/ref/spec#Identifiers
fn scan_pkg_or_keyword_or_ident(&mut self) -> Result<Step<'a>> {
self.next();
while let Some(c) = self.current_char {
if !(is_letter(c) || is_unicode_digit(c)) {
break;
}
self.next()
}
let pos = self.position();
let literal = self.literal();
if literal.len() > 1 {
if let Some(&token) = KEYWORDS.get(literal) {
self.insert_semi = matches!(
token,
Token::BREAK | Token::CONTINUE | Token::FALLTHROUGH | Token::RETURN
);
return Ok((pos, token, literal));
}
}
self.insert_semi = true;
Ok((pos, Token::IDENT, literal))
}
// https://golang.org/ref/spec#Integer_literals
// https://golang.org/ref/spec#Floating-point_literals
// https://golang.org/ref/spec#Imaginary_literals
fn scan_int_or_float_or_imag(&mut self, preceding_dot: bool) -> Result<Step<'a>> {
self.insert_semi = true;
let mut token = Token::INT;
let mut digits = "_0123456789";
let mut exp = "eE";
if !preceding_dot {
if matches!(self.current_char, Some('0')) {
self.next();
match self.current_char {
Some('b' | 'B') => {
digits = "_01";
exp = "";
self.next();
}
Some('o' | 'O') => {
digits = "_01234567";
exp = "";
self.next();
}
Some('x' | 'X') => {
digits = "_0123456789abcdefABCDEF";
exp = "pP";
self.next();
}
_ => {}
};
}
while let Some(c) = self.current_char {
if !digits.contains(c) {
break;
}
self.next();
}
}
if preceding_dot || matches!(self.current_char, Some('.')) {
token = Token::FLOAT;
self.next();
while let Some(c) = self.current_char {
if !digits.contains(c) {
break;
}
self.next();
}
}
if !exp.is_empty() {
if let Some(c) = self.current_char {
if exp.contains(c) {
token = Token::FLOAT;
self.next();
if matches!(self.current_char, Some('-' | '+')) {
self.next();
}
while let Some(c) = self.current_char {
if !matches!(c, '_' | '0'..='9') {
break;
}
self.next();
}
}
}
}
if matches!(self.current_char, Some('i')) {
token = Token::IMAG;
self.next();
}
Ok((self.position(), token, self.literal()))
}
// https://golang.org/ref/spec#Rune_literals
fn scan_rune(&mut self) -> Result<Step<'a>> {
self.insert_semi = true;
self.next();
match self.current_char {
Some('\\') => self.require_escaped_char::<'\''>()?,
Some(_) => self.next(),
_ => return Err(ScannerError::UnterminatedRune),
}
if matches!(self.current_char, Some('\'')) {
self.next();
return Ok((self.position(), Token::CHAR, self.literal()));
}
Err(ScannerError::UnterminatedRune)
}
// https://golang.org/ref/spec#String_literals
fn scan_interpreted_string(&mut self) -> Result<Step<'a>> {
self.insert_semi = true;
self.next();
while let Some(c) = self.current_char {
match c {
'"' => {
self.next();
return Ok((self.position(), Token::STRING, self.literal()));
}
'\\' => self.require_escaped_char::<'"'>()?,
_ => self.next(),
}
}
Err(ScannerError::UnterminatedString)
}
// https://golang.org/ref/spec#String_literals
fn scan_raw_string(&mut self) -> Result<Step<'a>> {
self.insert_semi = true;
self.next();
while let Some(c) = self.current_char {
match c {
'`' => {
self.next();
return Ok((self.position(), Token::STRING, self.literal()));
}
_ => self.next(),
}
}
Err(ScannerError::UnterminatedString)
}
// https://golang.org/ref/spec#Comments
fn scan_general_comment(&mut self) -> Result<Step<'a>> {
self.next();
self.next();
while let Some(c) = self.current_char {
match c {
'*' => {
self.next();
if matches!(self.current_char, Some('/')) {
self.next();
let pos = self.position();
let lit = self.literal();
// look for compiler directives
self.directive(&lit["/*".len()..lit.len() - "*/".len()], true)?;
return Ok((pos, Token::COMMENT, lit));
}
}
_ => self.next(),
}
}
Err(ScannerError::UnterminatedComment)
}
// https://golang.org/ref/spec#Comments
fn scan_line_comment(&mut self) -> Result<Step<'a>> {
self.next();
self.next();
while let Some(c) = self.current_char {
if is_newline(c) {
break;
}
self.next();
}
let pos = self.position();
let lit = self.literal();
// look for compiler directives (at the beginning of line)
if self.start_column == 1 {
self.directive(lit["//".len()..].trim_end(), false)?;
}
Ok((pos, Token::COMMENT, self.literal()))
}
// https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives
fn directive(&mut self, input: &'a str, immediate: bool) -> Result<()> {
if let Some(line_directive) = input.strip_prefix("line ") {
self.pending_line_info = self.parse_line_directive(line_directive)?;
if immediate {
self.consume_pending_line_info();
}
}
Ok(())
}
fn parse_line_directive(&mut self, line_directive: &'a str) -> Result<Option<LineInfo<'a>>> {
if let Some((file, line)) = line_directive.rsplit_once(':') {
let line = line.parse().map_err(|_| ScannerError::InvalidDirective)?;
if let Some((file, l)) = file.rsplit_once(':') {
if let Ok(l) = l.parse() {
//line :line:col
//line filename:line:col
/*line :line:col*/
/*line filename:line:col*/
let file = if !file.is_empty() { Some(file) } else { None };
let col = Some(line);
let line = l;
let hide_column = false;
return Ok(Some((file, line, col, hide_column)));
}
}
//line :line
//line filename:line
/*line :line*/
/*line filename:line*/
Ok(Some((Some(file), line, None, true)))
} else {
Ok(None)
}
}
const fn find_line_end(&self) -> bool {
let buffer = self.buffer.as_bytes();
let mut in_comment = true;
let mut i = self.offset;
let max = self.buffer.len();
while i < max {
let c = buffer[i] as char;
if i < max - 1 {
let n = buffer[i + 1] as char;
if !in_comment && c == '/' && n == '/' {
return true;
}
if c == '/' && n == '*' {
i += 2;
in_comment = true;
continue;
}
if c == '*' && n == '/' {
i += 2;
in_comment = false;
continue;
}
}
if is_newline(c) {
return true;
}
if !in_comment && !matches!(c, ' ' | '\t' | '\r') {
return false;
}
i += 1;
}
!in_comment
}
fn consume_pending_line_info(&mut self) {
if let Some(line_info) = self.pending_line_info.take() {
if let Some(file) = line_info.0 {
self.file = file;
}
self.line = line_info.1;
if let Some(column) = line_info.2 {
self.column = column;
}
self.hide_column = line_info.3;
}
}
fn peek(&mut self) -> Option<char> {
self.chars.peek().copied()
}
fn next(&mut self) {
self.offset += self.current_char_len;
self.column += self.current_char_len;
let last_char = self.current_char;
self.current_char = self.chars.next();
if let Some(c) = self.current_char {
self.current_char_len = c.len_utf8();
if matches!(last_char, Some('\n')) {
self.line += 1;
self.column = 1;
self.consume_pending_line_info();
}
} else {
self.current_char_len = 0
}
}
const fn position(&self) -> Position<'a> {
Position {
directory: self.directory,
file: self.file,
offset: self.start_offset,
line: self.start_line,
column: if self.hide_column {
0
} else {
self.start_column
},
}
}
fn reset_start(&mut self) {
self.start_offset = self.offset;
self.start_line = self.line;
self.start_column = self.column;
}
fn literal(&self) -> &'a str {
&self.buffer[self.start_offset..self.offset]
}
fn require_escaped_char<const DELIM: char>(&mut self) -> Result<()> {
self.next();
let c = self
.current_char
.ok_or(ScannerError::UnterminatedEscapedChar)?;
// TODO: move this to the match when const generics can be referenced in patterns
if c == DELIM {
self.next();
return Ok(());
}
match c {
'a' | 'b' | 'f' | 'n' | 'r' | 't' | 'v' | '\\' => self.next(),
'x' => {
self.next();
self.require_hex_digits::<2>()?
}
'u' => {
self.next();
self.require_hex_digits::<4>()?;
}
'U' => {
self.next();
self.require_hex_digits::<8>()?;
}
'0'..='7' => self.require_octal_digits::<3>()?,
_ => return Err(ScannerError::UnterminatedEscapedChar),
}
Ok(())
}
fn require_octal_digits<const COUNT: usize>(&mut self) -> Result<()> {
for _ in 0..COUNT {
let c = self.current_char.ok_or(ScannerError::OctalNotFound)?;
if !is_octal_digit(c) {
return Err(ScannerError::OctalNotFound);
}
self.next();
}
Ok(())
}
fn require_hex_digits<const COUNT: usize>(&mut self) -> Result<()> {
for _ in 0..COUNT {
let c = self.current_char.ok_or(ScannerError::HexadecimalNotFound)?;
if !is_hex_digit(c) {
return Err(ScannerError::HexadecimalNotFound);
}
self.next();
}
Ok(())
}
}
impl<'a> IntoIterator for Scanner<'a> {
type Item = Result<Step<'a>>;
type IntoIter = IntoIter<'a>;
fn into_iter(self) -> Self::IntoIter {
Self::IntoIter::new(self)
}
}
pub struct IntoIter<'a> {
scanner: Scanner<'a>,
done: bool,
}
impl<'a> IntoIter<'a> {
const fn new(scanner: Scanner<'a>) -> Self {
Self { scanner, done: false }
}
}
impl<'a> Iterator for IntoIter<'a> {
type Item = Result<Step<'a>>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
match self.scanner.scan() {
Ok((pos, tok, lit)) => {
if tok == Token::EOF {
self.done = true;
}
Some(Ok((pos, tok, lit)))
}
Err(err) => {
self.done = true;
Some(Err(err))
}
}
}
}
// https://golang.org/ref/spec#Letters_and_digits
fn is_letter(c: char) -> bool {
c == '_' || is_unicode_letter(c)
}
//const fn is_decimal_digit(c: char) -> bool {
//matches!(c, '0'..='9')
//}
//const fn is_binary_digit(c: char) -> bool {
//matches!(c, '0'..='1')
//}
const fn is_octal_digit(c: char) -> bool {
matches!(c, '0'..='7')
}
const fn is_hex_digit(c: char) -> bool {
matches!(c, '0'..='9' | 'A'..='F' | 'a'..='f')
}
// https://golang.org/ref/spec#Characters
const fn is_newline(c: char) -> bool {
c == '\n'
}
//const fn is_unicode_char(c: char) -> bool {
//c != '\n'
//}
fn is_unicode_letter(c: char) -> bool {
matches!(
get_general_category(c),
GeneralCategory::UppercaseLetter
| GeneralCategory::LowercaseLetter
| GeneralCategory::TitlecaseLetter
| GeneralCategory::ModifierLetter
| GeneralCategory::OtherLetter
)
}
fn is_unicode_digit(c: char) -> bool {
get_general_category(c) == GeneralCategory::DecimalNumber
}
// https://golang.org/ref/spec#Keywords
static KEYWORDS: Map<&'static str, Token> = phf_map! {
"break" => Token::BREAK,
"case" => Token::CASE,
"chan" => Token::CHAN,
"const" => Token::CONST,
"continue" => Token::CONTINUE,
"default" => Token::DEFAULT,
"defer" => Token::DEFER,
"else" => Token::ELSE,
"fallthrough" => Token::FALLTHROUGH,
"for" => Token::FOR,
"func" => Token::FUNC,
"go" => Token::GO,
"goto" => Token::GOTO,
"if" => Token::IF,
"import" => Token::IMPORT,
"interface" => Token::INTERFACE,
"map" => Token::MAP,
"package" => Token::PACKAGE,
"range" => Token::RANGE,
"return" => Token::RETURN,
"select" => Token::SELECT,
"struct" => Token::STRUCT,
"switch" => Token::SWITCH,
"type" => Token::TYPE,
"var" => Token::VAR,
};
#[cfg(test)]
mod tests {
use super::Scanner;
#[test] // fuzz
fn it_should_return_an_error_on_missing_line_number() {
let input = "/*line :*/";
let mut out: Vec<_> = Scanner::new(file!(), input).into_iter().collect();
assert!(out.pop().unwrap().is_err());
}
}

View File

@@ -1,273 +0,0 @@
// https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:src/go/token/token.go
#![allow(non_camel_case_types)] // For consistency with the Go tokens
use std::fmt;
#[derive(Clone, Copy, Debug, Default)]
pub struct Position<'a> {
pub directory: &'a str,
pub file: &'a str,
pub offset: usize,
pub line: usize,
pub column: usize,
}
impl<'a> fmt::Display for Position<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.file.is_empty() {
write!(f, ":{}:{}", self.line, self.column)
} else if self.file.starts_with('/') {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
} else {
write!(
f,
"{}/{}:{}:{}",
self.directory, self.file, self.line, self.column
)
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Token {
EOF,
COMMENT,
IDENT, // main
INT, // 12345
FLOAT, // 123.45
IMAG, // 123.45i
CHAR, // 'a'
STRING, // "abc"
ADD, // +
SUB, // -
MUL, // *
QUO, // /
REM, // %
AND, // &
OR, // |
XOR, // ^
SHL, // <<
SHR, // >>
AND_NOT, // &^
ADD_ASSIGN, // +=
SUB_ASSIGN, // -=
MUL_ASSIGN, // *=
QUO_ASSIGN, // /=
REM_ASSIGN, // %=
AND_ASSIGN, // &=
OR_ASSIGN, // |=
XOR_ASSIGN, // ^=
SHL_ASSIGN, // <<=
SHR_ASSIGN, // >>=
AND_NOT_ASSIGN, // &^=
LAND, // &&
LOR, // ||
ARROW, // <-
INC, // ++
DEC, // --
EQL, // ==
LSS, // <
GTR, // >
ASSIGN, // =
NOT, // !
NEQ, // !=
LEQ, // <=
GEQ, // >=
DEFINE, // :=
ELLIPSIS, // ...
LPAREN, // (
LBRACK, // [
LBRACE, // {
COMMA, // ,
PERIOD, // .
RPAREN, // )
RBRACK, // ]
RBRACE, // }
SEMICOLON, // ;
COLON, // :
BREAK,
CASE,
CHAN,
CONST,
CONTINUE,
DEFAULT,
DEFER,
ELSE,
FALLTHROUGH,
FOR,
FUNC,
GO,
GOTO,
IF,
IMPORT,
INTERFACE,
MAP,
PACKAGE,
RANGE,
RETURN,
SELECT,
STRUCT,
SWITCH,
TYPE,
VAR,
}
impl Token {
pub const fn is_assign_op(&self) -> bool {
use Token::*;
matches!(
self,
ADD_ASSIGN
| SUB_ASSIGN
| MUL_ASSIGN
| QUO_ASSIGN
| REM_ASSIGN
| AND_ASSIGN
| OR_ASSIGN
| XOR_ASSIGN
| SHL_ASSIGN
| SHR_ASSIGN
| AND_NOT_ASSIGN
)
}
// https://go.dev/ref/spec#Operator_precedence
pub fn precedence(&self) -> u8 {
use Token::*;
match self {
MUL | QUO | REM | SHL | SHR | AND | AND_NOT => 5,
ADD | SUB | OR | XOR => 4,
EQL | NEQ | LSS | LEQ | GTR | GEQ => 3,
LAND => 2,
LOR => 1,
_ => unreachable!(
"precedence() is only supported for binary operators, called with: {:?}",
self
),
}
}
pub const fn lowest_precedence() -> u8 {
0
}
}
impl From<&Token> for &'static str {
fn from(token: &Token) -> Self {
use Token::*;
match token {
EOF => "EOF",
COMMENT => "COMMENT",
IDENT => "IDENT",
INT => "INT",
FLOAT => "FLOAT",
IMAG => "IMAG",
CHAR => "CHAR",
STRING => "STRING",
ADD => "+",
SUB => "-",
MUL => "*",
QUO => "/",
REM => "%",
AND => "&",
OR => "|",
XOR => "^",
SHL => "<<",
SHR => ">>",
AND_NOT => "&^",
ADD_ASSIGN => "+=",
SUB_ASSIGN => "-=",
MUL_ASSIGN => "*=",
QUO_ASSIGN => "/=",
REM_ASSIGN => "%=",
AND_ASSIGN => "&=",
OR_ASSIGN => "|=",
XOR_ASSIGN => "^=",
SHL_ASSIGN => "<<=",
SHR_ASSIGN => ">>=",
AND_NOT_ASSIGN => "&^=",
LAND => "&&",
LOR => "||",
ARROW => "<-",
INC => "++",
DEC => "--",
EQL => "==",
LSS => "<",
GTR => ">",
ASSIGN => "=",
NOT => "!",
NEQ => "!=",
LEQ => "<=",
GEQ => ">=",
DEFINE => ":=",
ELLIPSIS => "...",
LPAREN => "(",
LBRACK => "[",
LBRACE => "{",
COMMA => ",",
PERIOD => ".",
RPAREN => ")",
RBRACK => "]",
RBRACE => "}",
SEMICOLON => ";",
COLON => ":",
BREAK => "break",
CASE => "case",
CHAN => "chan",
CONST => "const",
CONTINUE => "continue",
DEFAULT => "default",
DEFER => "defer",
ELSE => "else",
FALLTHROUGH => "fallthrough",
FOR => "for",
FUNC => "func",
GO => "go",
GOTO => "goto",
IF => "if",
IMPORT => "import",
INTERFACE => "interface",
MAP => "map",
PACKAGE => "package",
RANGE => "range",
RETURN => "return",
SELECT => "select",
STRUCT => "struct",
SWITCH => "switch",
TYPE => "type",
VAR => "var",
}
}
}

View File

@@ -181,6 +181,7 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
"git" => "GitPython",
"u" => "requests",
"f" => "requests",
"." => "requests",
"shopify" => "ShopifyAPI",
"seleniumwire" => "selenium-wire",
"openbb-terminal" => "openbb[all]",
@@ -217,20 +218,29 @@ pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
let ast = parser::parse_program(code, "main.py").map_err(|e| {
error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string()))
})?;
let imports = ast
let mut imports: Vec<String> = ast
.into_iter()
.filter_map(|x| match x {
Located { node, .. } => match node {
StmtKind::Import { names } => Some(
names
.into_iter()
.map(|x| x.node.name.split('.').next().unwrap_or("").to_string())
.map(|x| {
let name = x.node.name;
if name.starts_with('.') {
".".to_string()
} else {
name.split('.').next().unwrap_or("").to_string()
}
})
.map(replace_import)
.collect::<Vec<String>>(),
),
StmtKind::ImportFrom { level: Some(i), .. } if i > 0 => {
Some(vec!["requests".to_string()])
}
StmtKind::ImportFrom { level: _, module: Some(mod_), names: _ } => {
let imprt = mod_.split('.').next().unwrap_or("").replace("_", "-");
Some(vec![replace_import(imprt)])
}
_ => None,
@@ -240,7 +250,7 @@ pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
.filter(|x| !STDIMPORTS.contains(&x.as_str()))
.unique()
.collect();
imports.sort();
Ok(imports)
}
}
@@ -442,6 +452,7 @@ import os
import wmill
from zanzibar.estonie import talin
import matplotlib.pyplot as plt
from . import tests
def main():
pass
@@ -449,7 +460,7 @@ def main():
";
let r = parse_python_imports(code)?;
// println!("{}", serde_json::to_string(&r)?);
assert_eq!(r, vec!["wmill", "zanzibar", "matplotlib"]);
assert_eq!(r, vec!["matplotlib", "requests", "wmill", "zanzibar"]);
Ok(())
}

View File

@@ -6,21 +6,22 @@
* LICENSE-AGPL for a copy of the license.
*/
use deno_core::{serde_v8, v8, JsRuntime, RuntimeOptions};
use serde_json::Value;
use windmill_common::error;
use windmill_parser::{json_to_typ, Arg, MainArgSignature, ObjectProperty, Typ};
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Spanned};
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Span, Spanned};
use swc_ecma_ast::{
ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, FnDecl, Ident, Lit,
ModuleDecl, ModuleItem, Number, ObjectLit, Pat, Str, TsArrayType, TsEntityName, TsKeywordType,
TsKeywordTypeKind, TsLit, TsLitType, TsOptionalType, TsPropertySignature, TsType,
TsTypeElement, TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
ModuleDecl, ModuleItem, Number, ObjectLit, Param, Pat, Str, TsArrayType, TsEntityName,
TsKeywordType, TsKeywordTypeKind, TsLit, TsLitType, TsOptionalType, TsPropertySignature,
TsType, TsTypeElement, TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
};
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
pub fn parse_deno_signature(code: &str) -> error::Result<MainArgSignature> {
pub fn parse_deno_signature(code: &str, skip_dflt: bool) -> error::Result<MainArgSignature> {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("test.ts".into()), code.into());
let fm = cm.new_source_file(FileName::Custom("main.ts".into()), code.into());
let lexer = Lexer::new(
// We want to parse ecmascript
Syntax::Typescript(TsConfig::default()),
@@ -54,65 +55,17 @@ pub fn parse_deno_signature(code: &str) -> error::Result<MainArgSignature> {
})) if &sym.to_string() == "main" => Some(function.params),
_ => None,
});
if let Some(params) = params {
Ok(MainArgSignature {
let r = MainArgSignature {
star_args: false,
star_kwargs: false,
args: params
.into_iter()
.map(|x| match x.pat {
Pat::Ident(ident) => {
let (name, typ, nullable) = binding_ident_to_arg(&ident);
Ok(Arg {
otyp: None,
name,
typ,
default: None,
has_default: ident.id.optional || nullable,
})
}
Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable) =
left.as_ident().map(binding_ident_to_arg).ok_or_else(|| {
error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(left.span())
.unwrap_or_else(|_| cm.span_to_string(left.span()))
))
})?;
let span = match *right {
Expr::Lit(Lit::Str(Str { span, .. })) => Some(span),
Expr::Lit(Lit::Num(Number { span, .. })) => Some(span),
Expr::Lit(Lit::BigInt(BigInt { span, .. })) => Some(span),
Expr::Lit(Lit::Bool(Bool { span, .. })) => Some(span),
Expr::Object(ObjectLit { span, .. }) => Some(span),
Expr::Array(ArrayLit { span, .. }) => Some(span),
_ => None,
};
let expr = span
.and_then(|x| cm.span_to_snippet(x).ok())
.map(|x| serde_json::from_str(&x).map_err(|_| x));
let default = match expr.clone() {
Some(Ok(x)) => Some(x),
Some(Err(x)) => eval_sync(&x).ok(),
None => None,
};
if typ == Typ::Unknown && default.is_some() {
typ = json_to_typ(default.as_ref().unwrap());
}
Ok(Arg { otyp: None, name, typ, default, has_default: true })
}
_ => Err(error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(x.span())
.unwrap_or_else(|_| cm.span_to_string(x.span()))
))),
})
.map(|x| parse_param(x, &cm, skip_dflt))
.collect::<Result<Vec<Arg>, error::Error>>()?,
})
};
Ok(r)
} else {
Err(error::Error::ExecutionErr(
"main function was not findable (expected to find 'export function main(...)'"
@@ -121,6 +74,75 @@ pub fn parse_deno_signature(code: &str) -> error::Result<MainArgSignature> {
}
}
fn parse_param(x: Param, cm: &Lrc<SourceMap>, skip_dflt: bool) -> error::Result<Arg> {
let r = match x.pat {
Pat::Ident(ident) => {
let (name, typ, nullable) = binding_ident_to_arg(&ident);
Ok(Arg {
otyp: None,
name,
typ,
default: None,
has_default: ident.id.optional || nullable,
})
}
Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable) =
left.as_ident().map(binding_ident_to_arg).ok_or_else(|| {
error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(left.span())
.unwrap_or_else(|_| cm.span_to_string(left.span()))
))
})?;
let dflt = if skip_dflt {
None
} else {
match *right {
Expr::Lit(Lit::Str(Str { value, .. })) => {
Some(Value::String(value.to_string()))
}
Expr::Lit(Lit::Num(Number { value, .. }))
if (value == (value as u64) as f64) =>
{
Some(serde_json::json!(value as u64))
}
Expr::Lit(Lit::Num(Number { value, .. })) => Some(serde_json::json!(value)),
Expr::Lit(Lit::BigInt(BigInt { value, .. })) => Some(serde_json::json!(value)),
Expr::Lit(Lit::Bool(Bool { value, .. })) => Some(Value::Bool(value)),
Expr::Object(ObjectLit { span, .. }) => eval_span(span, cm),
Expr::Array(ArrayLit { span, .. }) => eval_span(span, cm),
_ => None,
}
};
if typ == Typ::Unknown && dflt.is_some() {
typ = json_to_typ(dflt.as_ref().unwrap());
}
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true })
}
_ => Err(error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(x.span())
.unwrap_or_else(|_| cm.span_to_string(x.span()))
))),
};
r
}
fn eval_span(span: Span, cm: &Lrc<SourceMap>) -> Option<Value> {
let expr = cm
.span_to_snippet(span)
.ok()
.map(|x| serde_json::from_str(&x).map_err(|_| x));
match expr {
Some(Ok(x)) => Some(x),
Some(Err(x)) => eval_sync(&x).ok(),
None => None,
}
}
fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String, Typ, bool) {
let (typ, nullable) = type_ann
.as_ref()
@@ -246,7 +268,7 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
pub fn eval_sync(code: &str) -> Result<serde_json::Value, String> {
let mut context = JsRuntime::new(RuntimeOptions::default());
let code = format!("let x = {}; x", code);
let res = context.execute_script("<anon>", &code);
let res = context.execute_script("<anon>", code);
match res {
Ok(global) => {
let scope = &mut context.handle_scope();
@@ -282,7 +304,7 @@ export function main(test1?: string, test2: string = \"burkina\",
}
";
assert_eq!(
parse_deno_signature(code)?,
parse_deno_signature(code, false)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
@@ -394,7 +416,7 @@ export function main(test2 = \"burkina\",
}
";
assert_eq!(
parse_deno_signature(code)?,
parse_deno_signature(code, false)?,
MainArgSignature {
star_args: false,
star_kwargs: false,

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
#[cfg(feature = "enterprise")]
use base64::Engine;
#[cfg(feature = "enterprise")]
use rsa::{pkcs8::DecodePublicKey, signature::Verifier};
#[cfg(feature = "enterprise")]
use sha2::Sha256;
@@ -11,9 +13,9 @@ pub fn verify_license_key(license_key: Option<String>) -> anyhow::Result<()> {
.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)?;
&base64::engine::general_purpose::STANDARD.decode("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgVShzcLSPiOi+8ET8fggob1kmi47/cE12JaidPkwfGnScZItghkqtiLsct0U4kJhlp5gO89DYTBmIKadvxwY7kMsLlZzmi2emVH7c27cByGASY8QmWDNdG4Ggy/NDflGGBdAtN6gHawZAg4zHv3qpbPQGHH1/6sXIohcXhOnouwIDAQAB")?)?;
let msg = base64::engine::general_purpose::STANDARD.decode(splitted_lk.0)?;
let signature = base64::engine::general_purpose::STANDARD.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()))?;

View File

@@ -6,15 +6,16 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::net::SocketAddr;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use git_version::git_version;
use sqlx::{Pool, Postgres};
use windmill_common::utils::rd_string;
use windmill_common::{utils::rd_string, METRICS_ADDR};
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
const DEFAULT_NUM_WORKERS: usize = 3;
const DEFAULT_PORT: u16 = 8000;
const DEFAULT_SERVER_BIND_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
mod ee;
@@ -29,15 +30,12 @@ async fn main() -> anyhow::Result<()> {
.and_then(|x| x.parse::<i32>().ok())
.unwrap_or(DEFAULT_NUM_WORKERS as i32);
let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
let metrics_addr: Option<SocketAddr> = *METRICS_ADDR;
let server_bind_address: IpAddr = std::env::var("SERVER_BIND_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();
.and_then(|x| x.parse().ok())
.unwrap_or(IpAddr::from(DEFAULT_SERVER_BIND_ADDR));
let port: u16 = std::env::var("PORT")
.ok()
@@ -51,8 +49,40 @@ async fn main() -> anyhow::Result<()> {
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false);
let rsmq_config = std::env::var("REDIS_URL").ok().map(|x| {
let url = x.parse::<url::Url>().unwrap();
let mut config = rsmq_async::RsmqOptions { ..Default::default() };
config.host = url.host_str().expect("redis host required").to_owned();
config.password = url.password().map(|s| s.to_owned());
config.db = url
.path_segments()
.and_then(|mut segments| segments.next())
.and_then(|segment| segment.parse().ok())
.unwrap_or(0);
config.ns = url
.query_pairs()
.find(|s| s.0 == "rsmq_namespace")
.map(|s| s.1)
.unwrap_or(std::borrow::Cow::Borrowed("rsmq"))
.into_owned();
config.port = url.port().unwrap_or(6379).to_string();
config
});
let db = windmill_common::connect_db(server_mode).await?;
let rsmq = if let Some(config) = rsmq_config {
let mut rsmq = rsmq_async::MultiplexedRsmq::new(config).await.unwrap();
let _ = rsmq_async::RsmqConnection::create_queue(&mut rsmq, "main_queue", None, None, None)
.await;
Some(rsmq)
} else {
None
};
if server_mode {
windmill_api::migrate_db(&db).await?;
}
@@ -60,100 +90,113 @@ async fn main() -> anyhow::Result<()> {
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], port));
#[cfg(feature = "enterprise")]
tracing::info!(
"
##############################
Windmill Enterprise Edition {GIT_VERSION}
##############################"
);
#[cfg(not(feature = "enterprise"))]
tracing::info!(
"
##############################
Windmill Community Edition {GIT_VERSION}
##############################"
);
display_config(vec![
"DISABLE_NSJAIL",
"DISABLE_SERVER",
"NUM_WORKERS",
"METRICS_ADDR",
"JSON_FMT",
"BASE_URL",
"BASE_INTERNAL_URL",
"TIMEOUT",
"ZOMBIE_JOB_TIMEOUT",
"RESTART_ZOMBIE_JOBS",
"SLEEP_QUEUE",
"MAX_LOG_SIZE",
"SERVER_BIND_ADDR",
"PORT",
"KEEP_JOB_DIR",
"S3_CACHE_BUCKET",
"TAR_CACHE_RATE",
"COOKIE_DOMAIN",
"PYTHON_PATH",
"DENO_PATH",
"GO_PATH",
"GOPRIVATE",
"NETRC",
"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",
"NPM_CONFIG_REGISTRY",
"PIP_LOCAL_DEPENDENCIES",
"ADDITIONAL_PYTHON_PATHS",
"INCLUDE_HEADERS",
"WHITELIST_WORKSPACES",
"BLACKLIST_WORKSPACES",
"INSTANCE_EVENTS_WEBHOOK",
"CLOUD_HOSTED",
]);
if server_mode || num_workers > 0 {
let addr = SocketAddr::from((server_bind_address, port));
let rsmq2 = rsmq.clone();
let server_f = async {
if server_mode {
windmill_api::run_server(db.clone(), addr, rx.resubscribe()).await?;
windmill_api::run_server(db.clone(), rsmq2, addr, rx.resubscribe()).await?;
}
Ok(()) as anyhow::Result<()>
};
let workers_f = async {
if num_workers > 0 {
#[cfg(feature = "enterprise")]
tracing::info!(
"
##############################
Windmill Enterprise Edition {GIT_VERSION}
##############################"
);
#[cfg(not(feature = "enterprise"))]
tracing::info!(
"
##############################
Windmill Community Edition {GIT_VERSION}
##############################"
);
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(),
rx.resubscribe(),
num_workers,
base_internal_url.clone(),
rsmq.clone(),
)
.await?;
}
Ok(()) as anyhow::Result<()>
};
let rsmq2 = rsmq.clone();
let monitor_f = async {
if server_mode {
monitor_db(&db, rx.resubscribe(), &base_internal_url);
monitor_db(&db, rx.resubscribe(), &base_internal_url, rsmq2);
}
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),
Some(addr) => {
windmill_common::serve_metrics(addr, rx.resubscribe(), num_workers > 0)
.await
.map_err(anyhow::Error::from)
}
None => Ok(()),
}
};
futures::try_join!(shutdown_signal, server_f, metrics_f, workers_f, monitor_f)?;
} else {
tracing::info!("Nothing to do, exiting.");
}
Ok(())
}
@@ -175,10 +218,11 @@ fn display_config(envs: Vec<&str>) {
)
}
pub fn monitor_db(
pub fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
db: &Pool<Postgres>,
rx: tokio::sync::broadcast::Receiver<()>,
base_internal_url: &str,
rsmq: Option<R>,
) {
let db1 = db.clone();
let db2 = db.clone();
@@ -186,16 +230,17 @@ pub fn monitor_db(
let rx2 = rx.resubscribe();
let base_internal_url = base_internal_url.to_string();
tokio::spawn(async move {
windmill_worker::handle_zombie_jobs_periodically(&db1, rx, &base_internal_url).await
windmill_worker::handle_zombie_jobs_periodically(&db1, rx, &base_internal_url, rsmq).await
});
tokio::spawn(async move { windmill_api::delete_expired_items_perdiodically(&db2, rx2).await });
}
pub async fn run_workers(
pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
db: Pool<Postgres>,
rx: tokio::sync::broadcast::Receiver<()>,
num_workers: i32,
base_internal_url: String,
rsmq: Option<R>,
) -> anyhow::Result<()> {
let license_key = std::env::var("LICENSE_KEY").ok();
#[cfg(feature = "enterprise")]
@@ -225,6 +270,7 @@ pub async fn run_workers(
let ip = ip.clone();
let rx = rx.resubscribe();
let base_internal_url = base_internal_url.clone();
let rsmq2 = rsmq.clone();
handles.push(tokio::spawn(monitor.instrument(async move {
tracing::info!(worker = %worker_name, "starting worker");
windmill_worker::run_worker(
@@ -235,6 +281,7 @@ pub async fn run_workers(
&ip,
rx,
&base_internal_url,
rsmq2,
)
.await
})));

View File

@@ -1,4 +1,5 @@
use futures::{stream, Stream};
use serde::Deserialize;
use serde_json::json;
use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres, Transaction};
use windmill_api::jobs::{CompletedJob, Job};
@@ -87,7 +88,7 @@ impl ApiServer {
let addr = sock.local_addr().unwrap();
drop(sock);
let task = tokio::task::spawn(windmill_api::run_server(db.clone(), addr, rx));
let task = tokio::task::spawn(windmill_api::run_server(db.clone(), None, addr, rx));
return Self { addr, tx, task };
}
@@ -154,12 +155,12 @@ mod suspend_resume {
serde_json::from_value(serde_json::json!({
"modules": [{
"id": "a",
"input_transform": {
"n": { "type": "javascript", "expr": "flow_input.n", },
"port": { "type": "javascript", "expr": "flow_input.port", },
"op": { "type": "javascript", "expr": "flow_input.op ?? 'resume'", },
},
"value": {
"input_transforms": {
"n": { "type": "javascript", "expr": "flow_input.n", },
"port": { "type": "javascript", "expr": "flow_input.port", },
"op": { "type": "javascript", "expr": "flow_input.op ?? 'resume'", },
},
"type": "rawscript",
"language": "deno",
"content": "\
@@ -193,12 +194,12 @@ mod suspend_resume {
},
}, {
"id": "b",
"input_transform": {
"n": { "type": "javascript", "expr": "results.a", },
"resume": { "type": "javascript", "expr": "resume", },
"resumes": { "type": "javascript", "expr": "resumes", },
},
"value": {
"input_transforms": {
"n": { "type": "javascript", "expr": "results.a", },
"resume": { "type": "javascript", "expr": "resume", },
"resumes": { "type": "javascript", "expr": "resumes", },
},
"type": "rawscript",
"language": "deno",
"content": "export function main(n, resume, resumes) { return { n: n + 1, resume, resumes } }"
@@ -207,12 +208,12 @@ mod suspend_resume {
"required_events": 1
},
}, {
"input_transform": {
"last": { "type": "javascript", "expr": "results.b", },
"resume": { "type": "javascript", "expr": "resume", },
"resumes": { "type": "javascript", "expr": "resumes", },
},
"value": {
"input_transforms": {
"last": { "type": "javascript", "expr": "results.b", },
"resume": { "type": "javascript", "expr": "resume", },
"resumes": { "type": "javascript", "expr": "resumes", },
},
"type": "rawscript",
"language": "deno",
"content": "export function main(last, resume, resumes) { return { last, resume, resumes } }"
@@ -252,9 +253,7 @@ mod suspend_resume {
let second = completed.next().await.unwrap();
// print_job(second, &db).await;
let tx = db.begin().await.unwrap();
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
tx.commit().await.unwrap();
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben"
))
@@ -355,9 +354,7 @@ mod suspend_resume {
/* ... and send a request resume it. */
let second = completed.next().await.unwrap();
let tx = db.begin().await.unwrap();
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
tx.commit().await.unwrap();
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}"
))
@@ -477,11 +474,11 @@ def main(last, port):
"iterator": { "type": "javascript", "expr": "flow_input.items" },
"skip_failures": false,
"modules": [{
"input_transform": {
"index": { "type": "javascript", "expr": "flow_input.iter.index" },
"port": { "type": "javascript", "expr": "flow_input.port" },
},
"value": {
"input_transforms": {
"index": { "type": "javascript", "expr": "flow_input.iter.index" },
"port": { "type": "javascript", "expr": "flow_input.port" },
},
"type": "rawscript",
"language": "deno",
"content": inner_step(),
@@ -490,11 +487,11 @@ def main(last, port):
},
"retry": { "constant": { "attempts": 2, "seconds": 0 } },
}, {
"input_transform": {
"last": { "type": "javascript", "expr": "results.a" },
"port": { "type": "javascript", "expr": "flow_input.port" },
},
"value": {
"input_transforms": {
"last": { "type": "javascript", "expr": "results.a" },
"port": { "type": "javascript", "expr": "flow_input.port" },
},
"type": "rawscript",
"language": "python3",
"content": last_step(),
@@ -632,7 +629,7 @@ def main(last, port):
"modules": [{
"id": "a",
"value": {
"input_transform": { "port": { "type": "javascript", "expr": "flow_input.port" } },
"input_transforms": { "port": { "type": "javascript", "expr": "flow_input.port" } },
"type": "rawscript",
"language": "python3",
"content": r#"
@@ -645,7 +642,7 @@ def main(port):
}],
"failure_module": {
"value": {
"input_transform": { "error": { "type": "javascript", "expr": "previous_result", },
"input_transforms": { "error": { "type": "javascript", "expr": "previous_result", },
"port": { "type": "javascript", "expr": "flow_input.port" } },
"type": "rawscript",
"language": "python3",
@@ -659,7 +656,7 @@ def main(error, port):
},
}))
.unwrap();
let (attempts, responses) = [
let (_attempts, responses) = [
/* fail the first step twice */
(0x00, None),
(0x00, None),
@@ -681,14 +678,20 @@ def main(error, port):
_ => panic!("expected failure module"),
}
assert_eq!(server.close().await, attempts);
println!("result: {:#?}", result);
assert_eq!(
result,
json!({
"recv": 42,
"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"}},
})
result
.get("from failure module")
.unwrap()
.get("error")
.unwrap()
.get("name")
.unwrap()
.clone(),
json!("IndexError")
);
assert_eq!(result.get("recv").unwrap().clone(), json!(42));
}
}
@@ -705,13 +708,13 @@ async fn test_iteration(db: Pool<Postgres>) {
"iterator": { "type": "javascript", "expr": "result.items" },
"skip_failures": false,
"modules": [{
"input_transform": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
"value": {
"input_transforms": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
"type": "rawscript",
"language": "python3",
"content": "def main(n):\n if 1 < n:\n raise StopIteration(n)",
@@ -762,13 +765,13 @@ async fn test_iteration_parallel(db: Pool<Postgres>) {
"skip_failures": false,
"parallel": true,
"modules": [{
"input_transform": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
"value": {
"input_transforms": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
"type": "rawscript",
"language": "python3",
"content": "def main(n):\n if 1 < n:\n raise StopIteration(n)",
@@ -824,9 +827,8 @@ impl RunJob {
async fn push(self, db: &Pool<Postgres>) -> Uuid {
let RunJob { payload, args } = self;
let tx = db.begin().await.unwrap();
let (uuid, tx) = windmill_queue::push(
tx,
let (uuid, tx) = windmill_queue::push::<rsmq_async::MultiplexedRsmq>(
(None, db.begin().await.unwrap()).into(),
"test-workspace",
payload,
args,
@@ -836,6 +838,7 @@ impl RunJob {
/* scheduled_for_o */ None,
/* schedule_path */ None,
/* parent_job */ None,
/* root job */ None,
/* is_flow_step */ false,
/* running */ false,
None,
@@ -843,8 +846,7 @@ impl RunJob {
)
.await
.expect("push has to succeed");
tx.commit().await.expect("push has to commit");
tx.commit().await.unwrap();
uuid
}
@@ -914,7 +916,7 @@ fn spawn_test_worker(
let ip: &str = Default::default();
let future = async move {
let base_internal_url = format!("http://localhost:{}", port);
windmill_worker::run_worker(
windmill_worker::run_worker::<rsmq_async::MultiplexedRsmq>(
&db,
worker_instance,
worker_name,
@@ -922,6 +924,7 @@ fn spawn_test_worker(
ip,
rx,
&base_internal_url,
None,
)
.await
};
@@ -1000,7 +1003,6 @@ async fn test_deno_flow(db: Pool<Postgres>) {
path: None,
lock: None,
},
input_transforms: Default::default(),
stop_after_if: Default::default(),
summary: Default::default(),
suspend: Default::default(),
@@ -1028,7 +1030,6 @@ async fn test_deno_flow(db: Pool<Postgres>) {
path: None,
lock: None,
},
input_transforms: Default::default(),
stop_after_if: Default::default(),
summary: Default::default(),
suspend: Default::default(),
@@ -1036,7 +1037,6 @@ async fn test_deno_flow(db: Pool<Postgres>) {
sleep: None,
}],
},
input_transforms: Default::default(),
stop_after_if: Default::default(),
summary: Default::default(),
suspend: Default::default(),
@@ -1130,7 +1130,6 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
path: None,
lock: None,
},
input_transforms: Default::default(),
stop_after_if: Default::default(),
summary: Default::default(),
suspend: Default::default(),
@@ -1146,25 +1145,24 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
modules: vec![
FlowModule {
id: "d".to_string(),
input_transforms: [
(
"i".to_string(),
InputTransform::Javascript {
expr: "flow_input.iter.value".to_string(),
},
),
(
"loop".to_string(),
InputTransform::Static { value: json!(true) },
),
(
"path".to_string(),
InputTransform::Static { value: json!("inner.txt") },
),
]
.into(),
value: FlowModuleValue::RawScript {
input_transforms: [].into(),
input_transforms: [
(
"i".to_string(),
InputTransform::Javascript {
expr: "flow_input.iter.value".to_string(),
},
),
(
"loop".to_string(),
InputTransform::Static { value: json!(true) },
),
(
"path".to_string(),
InputTransform::Static { value: json!("inner.txt") },
),
]
.into(),
language: ScriptLang::Deno,
content: write_file,
path: None,
@@ -1195,7 +1193,6 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
path: None,
lock: None,
},
input_transforms: [].into(),
stop_after_if: Default::default(),
summary: Default::default(),
suspend: Default::default(),
@@ -1204,7 +1201,6 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
},
],
},
input_transforms: Default::default(),
stop_after_if: Default::default(),
summary: Default::default(),
suspend: Default::default(),
@@ -1239,7 +1235,6 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
path: None,
lock: None,
},
input_transforms: [].into(),
stop_after_if: Default::default(),
summary: Default::default(),
suspend: Default::default(),
@@ -1436,7 +1431,6 @@ async fn test_python_flow(db: Pool<Postgres>) {
let doubles = "def main(n): return n * 2";
let flow: FlowValue = serde_json::from_value(serde_json::json!( {
"input_transform": {},
"modules": [
{
"value": {
@@ -1452,16 +1446,16 @@ async fn test_python_flow(db: Pool<Postgres>) {
"skip_failures": false,
"modules": [{
"value": {
"input_transforms": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
"type": "rawscript",
"language": "python3",
"content": doubles,
},
"input_transform": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
}],
},
},
@@ -1494,11 +1488,11 @@ async fn test_python_flow_2(db: Pool<Postgres>) {
"modules": [
{
"value": {
"input_transforms": {},
"type": "rawscript",
"content": "import wmill\ndef main(): return \"Hello\"",
"language": "python3"
},
"input_transform": {}
}
]
}))
@@ -1526,6 +1520,8 @@ async fn test_go_job(db: Pool<Postgres>) {
let port = server.addr.port();
let content = r#"
package inner
import "fmt"
func main(derp string) (string, error) {
@@ -1677,7 +1673,7 @@ async fn test_empty_loop(db: Pool<Postgres>) {
"modules": [
{
"value": {
"input_transform": {
"input_transforms": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
@@ -1693,7 +1689,7 @@ async fn test_empty_loop(db: Pool<Postgres>) {
},
{
"value": {
"input_transform": {
"input_transforms": {
"items": {
"type": "javascript",
"expr": "results.a",
@@ -1770,13 +1766,13 @@ async fn test_empty_loop_2(db: Pool<Postgres>) {
"iterator": { "type": "static", "value": [] },
"modules": [
{
"input_transform": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
"value": {
"input_transforms": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
"type": "rawscript",
"language": "python3",
"content": "def main(n): return n",
@@ -1812,13 +1808,13 @@ async fn test_step_after_loop(db: Pool<Postgres>) {
"iterator": { "type": "static", "value": [2,3,4] },
"modules": [
{
"input_transform": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
"value": {
"input_transforms": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
"type": "rawscript",
"language": "python3",
"content": "def main(n): return n",
@@ -1828,13 +1824,13 @@ async fn test_step_after_loop(db: Pool<Postgres>) {
},
},
{
"input_transform": {
"items": {
"type": "javascript",
"expr": "results.a",
},
},
"value": {
"input_transforms": {
"items": {
"type": "javascript",
"expr": "results.a",
},
},
"type": "rawscript",
"language": "python3",
"content": "def main(items): return sum(items)",
@@ -1856,17 +1852,17 @@ async fn test_step_after_loop(db: Pool<Postgres>) {
fn module_add_item_to_list(i: i32, id: &str) -> serde_json::Value {
json!({
"id": format!("id_{}", i.to_string().replace("-", "_")),
"input_transform": {
"array": {
"type": "javascript",
"expr": format!("results.{id}"),
},
"i": {
"type": "static",
"value": json!(i),
}
},
"value": {
"input_transforms": {
"array": {
"type": "javascript",
"expr": format!("results.{id}"),
},
"i": {
"type": "static",
"value": json!(i),
}
},
"type": "rawscript",
"language": "deno",
"content": "export function main(array, i){ array.push(i); return array }",
@@ -1876,8 +1872,8 @@ fn module_add_item_to_list(i: i32, id: &str) -> serde_json::Value {
fn module_failure() -> serde_json::Value {
json!({
"input_transform": {},
"value": {
"input_transforms": {},
"type": "rawscript",
"language": "deno",
"content": "export function main(){ throw Error('failure') }",
@@ -2032,6 +2028,16 @@ async fn test_branchall_simple(db: Pool<Postgres>) {
assert_eq!(result, serde_json::json!([[1, 2], [1, 3]]));
}
#[derive(Deserialize)]
struct ErrorResult {
error: NamedError,
}
#[derive(Deserialize)]
struct NamedError {
name: String,
}
#[sqlx::test(fixtures("base"))]
async fn test_branchall_skip_failure(db: Pool<Postgres>) {
initialize_tracing().await;
@@ -2067,8 +2073,11 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
.unwrap();
assert_eq!(
result,
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]])
serde_json::from_value::<ErrorResult>(result.get(0).unwrap().clone())
.unwrap()
.error
.name,
"Error"
);
let flow: FlowValue = serde_json::from_value(json!({
@@ -2101,8 +2110,11 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
.unwrap();
assert_eq!(
result,
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]])
serde_json::from_value::<ErrorResult>(result.get(0).unwrap().clone())
.unwrap()
.error
.name,
"Error"
);
}
@@ -2235,7 +2247,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
"modules": [{
"id": "a",
"value": {
"input_transform": {
"input_transforms": {
"l": { "type": "javascript", "expr": "[]", },
"n": { "type": "javascript", "expr": "flow_input.n", },
},
@@ -2246,7 +2258,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
}, {
"id": "b",
"value": {
"input_transform": {
"input_transforms": {
"l": { "type": "javascript", "expr": "results.a.l", },
"n": { "type": "javascript", "expr": "flow_input.n", },
},
@@ -2256,7 +2268,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
},
}, {
"value": {
"input_transform": {
"input_transforms": {
"l": { "type": "javascript", "expr": "results.b.l", },
"n": { "type": "javascript", "expr": "flow_input.n", },
},
@@ -2266,8 +2278,8 @@ async fn test_failure_module(db: Pool<Postgres>) {
},
}],
"failure_module": {
"input_transform": { "error": { "type": "javascript", "expr": "previous_result", } },
"value": {
"input_transforms": { "error": { "type": "javascript", "expr": "previous_result", } },
"type": "rawscript",
"language": "deno",
"content": "export function main(error) { return { 'from failure module': error } }",

View File

@@ -47,6 +47,7 @@ tracing.workspace = true
sql-builder.workspace = true
serde_json.workspace = true
chrono.workspace = true
chrono-tz.workspace = true
hex.workspace = true
base64.workspace = true
serde_urlencoded.workspace = true
@@ -69,3 +70,5 @@ async-stripe = { workspace = true, optional = true }
lazy_static.workspace = true
prometheus.workspace = true
async_zip.workspace = true
rsmq_async.workspace = true
regex.workspace = true

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.70.1
version: 1.87.0
title: Windmill API
contact:
@@ -117,9 +117,10 @@ paths:
responses:
"200":
description: >
Successfully authenticated.
The session ID is returned in a cookie named `token` and as plaintext response.
Preferred method of authorization is through the bearer token. The cookie is only for browser convenience.
Successfully authenticated. The session ID is returned in a cookie
named `token` and as plaintext response. Preferred method of
authorization is through the bearer token. The cookie is only for
browser convenience.
headers:
Set-Cookie:
@@ -204,7 +205,6 @@ paths:
schema:
type: string
/w/{workspace}/users/is_owner/{path}:
get:
summary: is owner of path
@@ -1048,6 +1048,27 @@ paths:
schema:
type: string
/users/tokens/impersonate:
post:
summary: create token to impersonate a user (require superadmin)
operationId: createTokenImpersonate
tags:
- user
requestBody:
description: new token
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewTokenImpersonate"
responses:
"201":
description: token created
content:
text/plain:
schema:
type: string
/users/tokens/delete/{token_prefix}:
delete:
summary: delete token
@@ -1258,9 +1279,10 @@ paths:
responses:
"200":
description: >
Successfully authenticated.
The session ID is returned in a cookie named `token` and as plaintext response.
Preferred method of authorization is through the bearer token. The cookie is only for browser convenience.
Successfully authenticated. The session ID is returned in a cookie
named `token` and as plaintext response. Preferred method of
authorization is through the bearer token. The cookie is only for
browser convenience.
headers:
Set-Cookie:
@@ -1549,7 +1571,7 @@ paths:
required: true
content:
application/json:
schema:
schema:
type: object
properties:
value: {}
@@ -2103,7 +2125,6 @@ paths:
items:
type: string
/w/{workspace}/scripts/create:
post:
summary: create script
@@ -2278,7 +2299,7 @@ paths:
/w/{workspace}/scripts/delete/h/{hash}:
post:
summary: delete script by hash (erase content but keep hash)
summary: delete script by hash (erase content but keep hash, require admin)
operationId: deleteScriptByHash
tags:
- script
@@ -2293,6 +2314,23 @@ paths:
schema:
$ref: "#/components/schemas/Script"
/w/{workspace}/scripts/delete/p/{path}:
post:
summary: delete all scripts at a given path (require admin)
operationId: deleteScriptByPath
tags:
- script
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: script path
content:
application/json:
schema:
type: string
/w/{workspace}/scripts/get/p/{path}:
get:
summary: get script by path
@@ -2327,6 +2365,24 @@ paths:
schema:
type: string
/scripts_u/tokened_raw/{workspace}/{token}/{path}:
get:
summary: raw script by path with a token (mostly used by lsp to be used with import maps to resolve scripts)
operationId: rawScriptByPathTokened
tags:
- script
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Token"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: script content
content:
text/plain:
schema:
type: string
/w/{workspace}/scripts/exists/p/{path}:
get:
summary: exists script by path
@@ -2452,17 +2508,6 @@ paths:
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/ParentJob"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
@@ -2482,6 +2527,26 @@ paths:
application/json:
schema: {}
get:
summary: run script by path with get
operationId: runWaitResultScriptByPathGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
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
@@ -2491,17 +2556,6 @@ paths:
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"
@@ -2538,11 +2592,6 @@ paths:
required: true
schema:
type: string
- name: skip_direct
description: Skip checking that the node is part of the given flow.
in: query
schema:
type: boolean
responses:
"200":
description: job result
@@ -2733,6 +2782,27 @@ paths:
schema:
type: string
/w/{workspace}/flows/input_history/p/{path}:
get:
summary: list inputs for previous completed flow jobs
operationId: getFlowInputHistoryByPath
tags:
- flow
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: input history for completed jobs with this flow path
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Input"
/w/{workspace}/apps/list:
get:
summary: list all available apps
@@ -2823,7 +2893,6 @@ paths:
schema:
$ref: "#/components/schemas/AppWithLastVersion"
/w/{workspace}/apps/secret_of/{path}:
get:
summary: get public secret of app
@@ -3156,12 +3225,14 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/Success"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/Suspended"
- $ref: "#/components/parameters/Running"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
responses:
"200":
description: All available queued jobs
@@ -3186,10 +3257,12 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/Success"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
- name: is_skipped
description: is the job skipped
in: query
@@ -3223,9 +3296,11 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
- name: is_skipped
description: is the job skipped
in: query
@@ -3268,23 +3343,6 @@ paths:
schema:
$ref: "#/components/schemas/Job"
# /w/{workspace}/jobs/flow/current_state/{id}:
# get:
# summary: get flow current step state
# operationId: getJob
# tags:
# - job
# parameters:
# - $ref: "#/components/parameters/WorkspaceId"
# - $ref: "#/components/parameters/JobId"
# responses:
# "200":
# description: state details
# content:
# application/json:
# schema:
# type: string
/w/{workspace}/jobs_u/getupdate/{id}:
get:
summary: get job updates
@@ -3337,6 +3395,22 @@ paths:
schema:
$ref: "#/components/schemas/CompletedJob"
/w/{workspace}/jobs/completed/get_result/{id}:
get:
summary: get completed job result
operationId: getCompletedJobResult
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
responses:
"200":
description: result
content:
application/json:
schema: {}
/w/{workspace}/jobs/completed/delete/{id}:
post:
summary: delete completed job (erase content but keep run id)
@@ -3354,7 +3428,7 @@ paths:
schema:
$ref: "#/components/schemas/CompletedJob"
/w/{workspace}/jobs/queue/cancel/{id}:
/w/{workspace}/jobs_u/queue/cancel/{id}:
post:
summary: cancel queued job
operationId: cancelQueuedJob
@@ -3382,6 +3456,34 @@ paths:
schema:
type: string
/w/{workspace}/jobs_u/queue/force_cancel/{id}:
post:
summary: force cancel queued job
operationId: forceCancelQueuedJob
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
requestBody:
description: reason
required: true
content:
application/json:
schema:
type: object
properties:
reason:
type: string
responses:
"200":
description: job canceled
content:
text/plain:
schema:
type: string
/w/{workspace}/jobs/job_signature/{id}/{resume_id}:
get:
summary: create an HMac signature given a job id and a resume id
@@ -3410,7 +3512,7 @@ paths:
/w/{workspace}/jobs/resume_urls/{id}/{resume_id}:
get:
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
operationId: getResumeUrls
tags:
- job
@@ -3436,7 +3538,7 @@ paths:
properties:
approvalPage:
type: string
resume:
resume:
type: string
cancel:
type: string
@@ -3454,6 +3556,7 @@ paths:
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
- $ref: "#/components/parameters/Payload"
- name: resume_id
in: path
required: true
@@ -3667,13 +3770,14 @@ paths:
properties:
schedule:
type: string
offset:
type: integer
timezone:
type: string
required:
- schedule
- timezone
responses:
"200":
description: the preview of the next 10 time this schedule would apply to
description: List of 5 estimated upcoming execution events (in UTC)
content:
application/json:
schema:
@@ -4081,7 +4185,7 @@ paths:
type: string
owners:
type: array
items:
items:
type: string
extra_perms:
additionalProperties:
@@ -4115,7 +4219,7 @@ paths:
properties:
owners:
type: array
items:
items:
type: string
extra_perms:
additionalProperties:
@@ -4286,7 +4390,8 @@ paths:
required: true
schema:
type: string
enum: [script, group_, resource, schedule, variable, flow, folder, app]
enum:
[script, group_, resource, schedule, variable, flow, folder, app]
responses:
"200":
description: acls
@@ -4311,7 +4416,8 @@ paths:
required: true
schema:
type: string
enum: [script, group_, resource, schedule, variable, flow, folder, app]
enum:
[script, group_, resource, schedule, variable, flow, folder, app]
requestBody:
description: acl to add
required: true
@@ -4347,7 +4453,8 @@ paths:
required: true
schema:
type: string
enum: [script, group_, resource, schedule, variable, flow, folder, app]
enum:
[script, group_, resource, schedule, variable, flow, folder, app]
requestBody:
description: acl to add
required: true
@@ -4367,19 +4474,18 @@ 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
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:
@@ -4456,6 +4562,118 @@ paths:
"200":
description: unstar item
/w/{workspace}/inputs/history:
get:
summary: List Inputs used in previously completed jobs
operationId: getInputHistory
tags:
- input
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/RunnableId"
- $ref: "#/components/parameters/RunnableType"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: Input history for completed jobs
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Input"
/w/{workspace}/inputs/list:
get:
summary: List saved Inputs for a Runnable
operationId: listInputs
tags:
- input
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/RunnableId"
- $ref: "#/components/parameters/RunnableType"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: Saved Inputs for a Runnable
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Input"
/w/{workspace}/inputs/create:
post:
summary: Create an Input for future use in a script or flow
operationId: createInput
tags:
- input
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/RunnableId"
- $ref: "#/components/parameters/RunnableType"
requestBody:
description: Input
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateInput"
responses:
"201":
description: Input created
content:
text/plain:
schema:
type: string
format: uuid
/w/{workspace}/inputs/update:
post:
summary: Update an Input
operationId: updateInput
tags:
- input
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: UpdateInput
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateInput"
responses:
"201":
description: Input updated
content:
text/plain:
schema:
type: string
format: uuid
/w/{workspace}/inputs/delete/{input}:
post:
summary: Delete a Saved Input
operationId: deleteInput
tags:
- input
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/InputId"
responses:
"200":
description: Input deleted
content:
text/plain:
schema:
type: string
format: uuid
components:
securitySchemes:
bearerAuth:
@@ -4465,6 +4683,7 @@ components:
type: apiKey
in: cookie
name: token
parameters:
WorkspaceId:
name: workspace
@@ -4472,6 +4691,12 @@ components:
required: true
schema:
type: string
Token:
name: token
in: path
required: true
schema:
type: string
AccountId:
name: id
in: path
@@ -4547,7 +4772,9 @@ components:
type: string
ParentJob:
name: parent_job
description: The parent job that is at the origin and responsible for the execution of this script if any
description:
The parent job that is at the origin and responsible for the execution
of this script if any
in: query
schema:
type: string
@@ -4567,7 +4794,14 @@ components:
in: query
schema:
type: string
Payload:
name: payload
description: |
The base64 encoded payload that has been encoded as a JSON. e.g how to encode such payload encodeURIComponent
`encodeURIComponent(btoa(JSON.stringify({a: 2})))`
in: query
schema:
type: string
ScriptStartPath:
name: script_path_start
description: mask to filter matching starting path
@@ -4586,15 +4820,15 @@ components:
in: query
schema:
type: string
CreatedBefore:
name: created_before
StartedBefore:
name: started_before
description: filter on created before (inclusive) timestamp
in: query
schema:
type: string
format: date-time
CreatedAfter:
name: created_after
StartedAfter:
name: started_after
description: filter on created after (exclusive) timestamp
in: query
schema:
@@ -4618,6 +4852,18 @@ components:
in: query
schema:
type: boolean
ArgsFilter:
name: args
description: filter on jobs containing those args as a json subset (@> in postgres)
in: query
schema:
type: string
ResultFilter:
name: result
description: filter on jobs containing those result as a json subset (@> in postgres)
in: query
schema:
type: string
After:
name: after
description: filter on created after (exclusive) timestamp
@@ -4659,7 +4905,9 @@ components:
enum: [Create, Update, Delete, Execute]
JobKinds:
name: job_kinds
description: filter on job kind (values 'preview', 'script', 'dependencies', 'flow') separated by,
description:
filter on job kind (values 'preview', 'script', 'dependencies', 'flow')
separated by,
in: query
schema:
type: string
@@ -4669,9 +4917,26 @@ components:
# type: string
# enum: ["preview", "script", "dependencies"]
# explode: false
RunnableId:
name: runnable_id
in: query
schema:
type: string
RunnableType:
name: runnable_type
in: query
schema:
$ref: "#/components/schemas/RunnableType"
InputId:
name: input
in: path
required: true
schema:
type: string
schemas:
$ref: "../../openflow.openapi.yaml#/components/schemas"
Script:
type: object
properties:
@@ -4742,6 +5007,60 @@ components:
type: object
additionalProperties: {}
Input:
type: object
properties:
id:
type: string
name:
type: string
args:
type: object
created_by:
type: string
created_at:
type: string
format: date-time
is_public:
type: boolean
required:
- id
- name
- args
- created_by
- created_at
- is_public
CreateInput:
type: object
properties:
name:
type: string
args:
type: object
required:
- name
- args
- created_by
UpdateInput:
type: object
properties:
id:
type: string
name:
type: string
is_public:
type: boolean
required:
- id
- name
- is_public
RunnableType:
type: string
enum: ["ScriptHash", "ScriptPath", "FlowPath"]
QueuedJob:
type: object
properties:
@@ -5044,6 +5363,19 @@ components:
type: string
format: date-time
NewTokenImpersonate:
type: object
properties:
label:
type: string
expiration:
type: string
format: date-time
impersonate_email:
type: string
required:
- impersonate_email
ListableVariable:
type: object
properties:
@@ -5169,6 +5501,11 @@ components:
MainArgSignature:
type: object
properties:
type:
type: string
enum: ["Valid", "Invalid"]
error:
type: string
star_args:
type: boolean
star_kwargs:
@@ -5278,6 +5615,8 @@ components:
- star_args
- start_kwargs
- args
- type
- error
Preview:
type: object
@@ -5412,8 +5751,8 @@ components:
format: date-time
schedule:
type: string
offset_:
type: integer
timezone:
type: string
enabled:
type: boolean
script_path:
@@ -5436,7 +5775,7 @@ components:
- edited_at
- schedule
- script_path
- offset_
- timezone
- extra_perms
- is_flow
- enabled
@@ -5449,8 +5788,8 @@ components:
type: string
schedule:
type: string
offset:
type: integer
timezone:
type: string
script_path:
type: string
is_flow:
@@ -5462,6 +5801,7 @@ components:
required:
- path
- schedule
- timezone
- script_path
- is_flow
- args
@@ -5471,10 +5811,13 @@ components:
properties:
schedule:
type: string
timezone:
type: string
args:
$ref: "#/components/schemas/ScriptArgs"
required:
- schedule
- timezone
- script_path
- is_flow
- args
@@ -5522,9 +5865,8 @@ components:
type: string
worker_instance:
type: string
ping_at:
type: string
format: date-time
last_ping:
type: number
started_at:
type: string
format: date-time
@@ -5740,6 +6082,7 @@ components:
- extra_perms
- edited_at
- execution_mode
AppWithLastVersion:
type: object
properties:

View File

@@ -37,7 +37,7 @@ use windmill_common::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
},
};
use windmill_queue::{push, JobPayload, RawCode};
use windmill_queue::{push, JobPayload, QueueTransaction, RawCode};
pub fn workspaced_service() -> Router {
Router::new()
@@ -313,10 +313,17 @@ async fn create_app(
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(app): Json<CreateApp>,
Json(mut app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
app.policy.on_behalf_of_email = Some(authed.email);
if &app.path == "" {
return Err(Error::BadRequest("App path cannot be empty".to_string()));
}
let id = sqlx::query_scalar!(
"INSERT INTO app
(workspace_id, path, summary, policy, versions)
@@ -463,7 +470,9 @@ async fn update_app(
sqlb.set_str("summary", nsummary);
}
if let Some(npolicy) = ns.policy {
if let Some(mut npolicy) = ns.policy {
npolicy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
npolicy.on_behalf_of_email = Some(authed.email);
sqlb.set(
"policy",
&format!(
@@ -556,6 +565,7 @@ fn digest(code: &str) -> String {
async fn execute_component(
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<ExecuteApp>,
) -> Result<String> {
@@ -574,7 +584,7 @@ async fn execute_component(
};
let path = path.to_path();
let mut tx = db.begin().await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, db.begin().await?).into();
let policy = if let Some(static_fields) = payload.clone().force_viewer_static_fields {
let mut hm = HashMap::new();
@@ -643,8 +653,12 @@ async fn execute_component(
}
ExecuteApp { args, raw_code: None, path: Some(path), .. } => {
let payload = if path.starts_with("script/") {
script_path_to_payload(path.strip_prefix("script/").unwrap(), &mut tx, &w_id)
.await?
script_path_to_payload(
path.strip_prefix("script/").unwrap(),
tx.transaction_mut(),
&w_id,
)
.await?
} else if path.starts_with("flow/") {
JobPayload::Flow(path.strip_prefix("flow/").unwrap().to_string())
} else {
@@ -670,14 +684,15 @@ async fn execute_component(
None,
None,
None,
None,
false,
false,
None,
true,
)
.await?;
tx.commit().await?;
Ok(uuid.to_string())
}
@@ -727,6 +742,9 @@ fn build_args(
path: String,
args: &Map<String, Value>,
) -> Result<Map<String, Value>> {
// disallow var and res access in args coming from the user for security reasons
args.into_iter()
.try_for_each(|x| disallow_var_res_access(x.1))?;
let static_args = policy
.triggerables
.get(&path)
@@ -747,3 +765,20 @@ fn build_args(
}
Ok(args)
}
fn disallow_var_res_access(args: &serde_json::Value) -> Result<()> {
match args {
Value::Object(v) => v.into_iter().try_for_each(|x| disallow_var_res_access(x.1)),
Value::Array(arr) => arr.into_iter().try_for_each(|v| disallow_var_res_access(v)),
Value::String(s) => {
if s.starts_with("$var:") || s.starts_with("$res:") {
Err(Error::BadRequest(format!(
"For security reasons, variable or resource access is not allowed as dynamic argument"
)))
} else {
Ok(())
}
}
_ => Ok(()),
}
}

View File

@@ -7,11 +7,12 @@
*/
use axum::{
extract::{Extension, Path},
extract::{Extension, Path, Query},
routing::{get, post, put},
Json, Router,
};
use hyper::StatusCode;
use hyper::{HeaderMap, StatusCode};
use serde::Deserialize;
use windmill_common::{
error::{JsonResult, Result},
utils::{not_found_if_none, StripPath},
@@ -19,6 +20,7 @@ use windmill_common::{
use crate::{
db::{UserDB, DB},
jobs::add_include_headers,
users::Authed,
};
@@ -83,13 +85,21 @@ pub async fn new_payload(
Ok(StatusCode::CREATED)
}
#[derive(Deserialize, Clone)]
pub struct IncludeHeaderQuery {
include_header: Option<String>,
}
pub async fn update_payload(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<serde_json::Value>,
Query(run_query): Query<IncludeHeaderQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> Result<StatusCode> {
let mut tx = db.begin().await?;
let args = add_include_headers(&run_query.include_header, headers, args.unwrap_or_default());
sqlx::query!(
"
UPDATE capture
@@ -99,7 +109,7 @@ pub async fn update_payload(
",
&w_id,
&path.to_path(),
&payload,
serde_json::json!(args),
)
.execute(&mut tx)
.await?;

View File

@@ -6,14 +6,20 @@
* LICENSE-AGPL for a copy of the license.
*/
use hyper::StatusCode;
use sql_builder::prelude::*;
use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{maybe_refresh_folders, require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
use axum::{
extract::{Extension, Path, Query},
routing::{delete, get, post},
Json, Router,
};
use hyper::StatusCode;
use sql_builder::prelude::*;
use sql_builder::SqlBuilder;
use sqlx::{Postgres, Transaction};
use windmill_audit::{audit_log, ActionKind};
@@ -25,15 +31,7 @@ use windmill_common::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
},
};
use windmill_queue::{push, schedule::push_scheduled_job, JobPayload};
use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
use windmill_queue::{push, schedule::push_scheduled_job, JobPayload, QueueTransaction};
pub fn workspaced_service() -> Router {
Router::new()
@@ -178,16 +176,20 @@ async fn check_path_conflict<'c>(
async fn create_flow(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(nf): Json<NewFlow>,
) -> Result<(StatusCode, String)> {
// cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
let mut tx = user_db.clone().begin(&authed).await?;
let authed = maybe_refresh_folders(&nf.path, &w_id, authed, &db).await;
check_path_conflict(&mut tx, &w_id, &nf.path).await?;
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
check_path_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
check_schedule_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
sqlx::query!(
"INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, \
@@ -219,13 +221,11 @@ 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(
tx,
&w_id,
@@ -237,12 +237,14 @@ async fn create_flow(
None,
None,
None,
None,
false,
false,
None,
true,
)
.await?;
sqlx::query!(
"UPDATE flow SET dependency_job = $1 WHERE path = $2 AND workspace_id = $3",
dependency_job_uuid,
@@ -282,15 +284,18 @@ async fn check_schedule_conflict<'c>(
async fn update_flow(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Json(nf): Json<NewFlow>,
) -> Result<String> {
let mut tx = user_db.clone().begin(&authed).await?;
let flow_path = flow_path.to_path();
check_schedule_conflict(&mut tx, &w_id, flow_path).await?;
let authed = maybe_refresh_folders(&flow_path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
check_schedule_conflict(tx.transaction_mut(), &w_id, flow_path).await?;
let schema = nf.schema.map(|x| x.0);
let old_dep_job = sqlx::query_scalar!(
@@ -317,13 +322,13 @@ async fn update_flow(
.await?;
if nf.path != flow_path {
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
check_schedule_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &flow_path, &db).await?;
}
let mut schedulables = sqlx::query_as!(
let mut schedulables: Vec<Schedule> = sqlx::query_as!(
Schedule,
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS true RETURNING *",
nf.path,
@@ -346,32 +351,32 @@ async fn update_flow(
schedulables.push(schedule);
}
for schedule in schedulables {
clear_schedule(&mut tx, flow_path, true).await?;
for schedule in schedulables.into_iter() {
// TODO: Why is this in the loop in the first place? Seems like it's just doing nothing after the first iteration? Should this use schedule.path?
clear_schedule(tx.transaction_mut(), flow_path, true).await?;
if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
}
audit_log(
&mut tx,
&authed.username,
"flows.update",
ActionKind::Create,
&w_id,
Some(&nf.path.to_string()),
Some(
[Some(("flow", nf.path.as_str()))]
.into_iter()
.flatten()
.collect(),
),
)
.await?;
}
audit_log(
&mut tx,
&authed.username,
"flows.update",
ActionKind::Create,
&w_id,
Some(&nf.path.to_string()),
Some(
[Some(("flow", nf.path.as_str()))]
.into_iter()
.flatten()
.collect(),
),
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFlow {
@@ -381,7 +386,6 @@ async fn update_flow(
},
);
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push(
tx,
&w_id,
@@ -393,6 +397,7 @@ async fn update_flow(
None,
None,
None,
None,
false,
false,
None,
@@ -550,7 +555,6 @@ mod tests {
modules: vec![
FlowModule {
id: "a".to_string(),
input_transforms: [].into(),
value: FlowModuleValue::Script {
path: "test".to_string(),
input_transforms: [(
@@ -568,7 +572,6 @@ mod tests {
},
FlowModule {
id: "b".to_string(),
input_transforms: HashMap::new(),
value: FlowModuleValue::RawScript {
input_transforms: HashMap::new(),
content: "test".to_string(),
@@ -587,7 +590,6 @@ mod tests {
},
FlowModule {
id: "c".to_string(),
input_transforms: HashMap::new(),
value: FlowModuleValue::ForloopFlow {
iterator: InputTransform::Static { value: serde_json::json!([1, 2, 3]) },
modules: vec![],
@@ -606,7 +608,6 @@ mod tests {
],
failure_module: Some(FlowModule {
id: "d".to_string(),
input_transforms: HashMap::new(),
value: FlowModuleValue::Script {
path: "test".to_string(),
input_transforms: HashMap::new(),
@@ -627,7 +628,6 @@ mod tests {
"modules": [
{
"id": "a",
"input_transforms": {},
"value": {
"input_transforms": {
"test": {
@@ -638,29 +638,22 @@ mod tests {
"type": "script",
"path": "test"
},
"stop_after_if": null,
"summary": null
},
{
"id": "b",
"input_transforms": {},
"value": {
"input_transforms": {},
"type": "rawscript",
"content": "test",
"lock": null,
"path": null,
"language": "deno"
},
"stop_after_if": {
"expr": "foo = 'bar'",
"skip_if_stopped": false
},
"summary": null
}
},
{
"id": "c",
"input_transforms": {},
"value": {
"type": "forloopflow",
"iterator": {
@@ -678,13 +671,11 @@ mod tests {
"stop_after_if": {
"expr": "previous.isEmpty()",
"skip_if_stopped": false,
},
"summary": null
}
}
],
"failure_module": {
"id": "d",
"input_transforms": {},
"value": {
"input_transforms": {},
"type": "script",
@@ -693,38 +684,12 @@ mod tests {
"stop_after_if": {
"expr": "previous.isEmpty()",
"skip_if_stopped": false
},
"summary": null
}
}
});
assert_eq!(dbg!(serde_json::json!(fv)), dbg!(expect));
}
#[test]
fn test_back_compat() {
/* renamed input_transform -> input_transforms but should deserialize old name */
let s = r#"
{
"value": {
"type": "rawscript",
"content": "def main(n): return",
"language": "python3"
},
"input_transform": {
"n": {
"expr": "flow_input.iter.value",
"type": "javascript"
}
}
}
"#;
let module: FlowModule = serde_json::from_str(s).unwrap();
assert_eq!(
module.input_transforms["n"],
InputTransform::Javascript { expr: "flow_input.iter.value".to_string() }
);
}
#[test]
fn retry_serde() {
assert_eq!(Retry::default(), serde_json::from_str(r#"{}"#).unwrap());

View File

@@ -6,9 +6,11 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::sync::Arc;
use crate::{
db::{UserDB, DB},
users::Authed,
users::{AuthCache, Authed, Tokened},
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
@@ -16,10 +18,11 @@ use axum::{
routing::{delete, get, post},
Json, Router,
};
use itertools::Itertools;
use lazy_static::lazy_static;
use regex::Regex;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, Error, JsonResult, Result},
error::{self, to_anyhow, Error, JsonResult, Result},
users::username_to_permissioned_as,
utils::{not_found_if_none, paginate, Pagination},
};
@@ -137,16 +140,28 @@ async fn check_name_conflict<'c>(
return Ok(());
}
lazy_static! {
static ref VALID_FOLDER_NAME: Regex = Regex::new(r#"^[a-zA-Z_0-9]+$"#).unwrap();
}
async fn create_folder(
authed: Authed,
Tokened { token }: Tokened,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(cache): Extension<Arc<AuthCache>>,
Path(w_id): Path<String>,
Json(ng): Json<NewFolder>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
if !VALID_FOLDER_NAME.is_match(&ng.name) {
return Err(windmill_common::error::Error::BadRequest(format!(
"Folder name can only contain alphanumeric characters, underscores"
)));
}
check_name_conflict(&mut tx, &w_id, &ng.name).await?;
cache.invalidate(&w_id, token).await;
let owner = username_to_permissioned_as(&authed.username);
let owners = &ng.owners.unwrap_or(vec![owner.clone()]);
@@ -262,13 +277,26 @@ async fn update_folder(
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
if let Some(display_name) = ng.display_name {
sqlb.set("display_name", display_name);
sqlb.set("display_name", "?".bind(&display_name));
}
if let Some(owners) = ng.owners {
sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(",")));
sqlb.set(
"owners",
"?".bind(&format!(
"{{{}}}",
owners
.iter()
.map(|x| format!("\"{x}\""))
.collect::<Vec<_>>()
.join(","),
)),
);
}
if let Some(extra_perms) = ng.extra_perms {
sqlb.set_str("extra_perms", extra_perms.to_string());
sqlb.set(
"extra_perms",
"?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?),
);
}
sqlb.returning("*");

View File

@@ -0,0 +1,282 @@
/*
* 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 crate::{db::UserDB, jobs::CompletedJob, users::Authed};
use axum::{
extract::{Path, Query},
routing::{get, post},
Extension, Json, Router,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sqlx::types::Uuid;
use std::{
fmt::{Display, Formatter},
vec,
};
use windmill_common::{
error::JsonResult,
scripts::to_i64,
utils::{paginate, Pagination},
};
use windmill_queue::JobKind;
pub fn workspaced_service() -> Router {
Router::new()
.route("/history", get(get_input_history))
.route("/list", get(list_saved_inputs))
.route("/create", post(create_input))
.route("/update", post(update_input))
.route("/delete/:id", post(delete_input))
}
#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
pub struct InputRow {
pub id: Uuid,
pub workspace_id: String,
pub runnable_id: String,
pub runnable_type: RunnableType,
pub name: String,
pub args: Value,
pub created_at: DateTime<Utc>,
pub created_by: String,
pub is_public: bool,
}
#[derive(Debug, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "runnable_type")]
pub enum RunnableType {
ScriptHash,
ScriptPath,
FlowPath,
}
impl Display for RunnableType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
RunnableType::ScriptHash => write!(f, "ScriptHash"),
RunnableType::ScriptPath => write!(f, "ScriptPath"),
RunnableType::FlowPath => write!(f, "FlowPath"),
}
}
}
impl RunnableType {
fn job_kind(&self) -> JobKind {
match self {
RunnableType::ScriptHash => JobKind::Script,
RunnableType::ScriptPath => JobKind::Script,
RunnableType::FlowPath => JobKind::Flow,
}
}
fn column_name(&self) -> &'static str {
match self {
RunnableType::ScriptHash => "script_hash",
RunnableType::ScriptPath => "script_path",
RunnableType::FlowPath => "script_path",
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RunnableParams {
pub runnable_id: String,
pub runnable_type: RunnableType,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Input {
id: Uuid,
name: String,
created_at: chrono::DateTime<chrono::Utc>,
args: serde_json::Value,
created_by: String,
is_public: bool,
}
async fn get_input_history(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(r): Query<RunnableParams>,
) -> JsonResult<Vec<Input>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.begin(&authed).await?;
let sql = &format!(
"select distinct on (args) * from completed_job \
where {} = $1 and job_kind = $2 and workspace_id = $3 \
order by args, started_at desc limit $4 offset $5",
r.runnable_type.column_name()
);
let query = sqlx::query_as::<_, CompletedJob>(sql);
let query = match r.runnable_type {
RunnableType::ScriptHash => query.bind(to_i64(&r.runnable_id)?),
_ => query.bind(&r.runnable_id),
};
let rows = query
.bind(r.runnable_type.job_kind())
.bind(&w_id)
.bind(per_page as i32)
.bind(offset as i32)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
let mut inputs = vec![];
for row in rows {
inputs.push(Input {
id: row.id,
name: format!(
"{} {}",
row.created_at.format("%H:%M %-d/%-m"),
row.created_by
),
created_at: row.created_at,
args: row.args.unwrap_or(serde_json::json!({})),
created_by: row.created_by,
is_public: true,
});
}
Ok(Json(inputs))
}
async fn list_saved_inputs(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(r): Query<RunnableParams>,
) -> JsonResult<Vec<Input>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, InputRow>(
"select * from input \
where runnable_id = $1 and runnable_type = $2 and workspace_id = $3 \
and is_public IS true OR created_by = $4 \
order by created_at desc limit $5 offset $6",
)
.bind(&r.runnable_id)
.bind(&r.runnable_type)
.bind(&w_id)
.bind(&authed.username)
.bind(per_page as i32)
.bind(offset as i32)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
let mut inputs: Vec<Input> = Vec::new();
for row in rows {
inputs.push(Input {
id: row.id,
name: row.name,
args: row.args,
created_by: row.created_by,
created_at: row.created_at,
is_public: row.is_public,
})
}
Ok(Json(inputs))
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateInput {
name: String,
args: serde_json::Value,
}
async fn create_input(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(r): Query<RunnableParams>,
Json(input): Json<CreateInput>,
) -> JsonResult<String> {
let mut tx = user_db.begin(&authed).await?;
let id = Uuid::new_v4();
sqlx::query(
"INSERT INTO input (id, workspace_id, runnable_id, runnable_type, name, args, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7)",
)
.bind(&id)
.bind(&w_id)
.bind(&r.runnable_id)
.bind(&r.runnable_type)
.bind(&input.name)
.bind(&input.args)
.bind(&authed.username)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(id.to_string()))
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateInput {
id: Uuid,
name: String,
is_public: bool,
}
async fn update_input(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(input): Json<UpdateInput>,
) -> JsonResult<String> {
let mut tx = user_db.begin(&authed).await?;
sqlx::query("UPDATE input SET name = $1, is_public = $2 WHERE id = $3 and workspace_id = $4")
.bind(&input.name)
.bind(&input.is_public)
.bind(&input.id)
.bind(&w_id)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(input.id.to_string()))
}
async fn delete_input(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, i_id)): Path<(String, Uuid)>,
) -> JsonResult<String> {
let mut tx = user_db.begin(&authed).await?;
sqlx::query("DELETE FROM input WHERE id = $1 and workspace_id = $2")
.bind(&i_id)
.bind(&w_id)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(i_id.to_string()))
}

View File

@@ -6,6 +6,12 @@
* LICENSE-AGPL for a copy of the license.
*/
use crate::{
db::{UserDB, DB},
users::{require_owner_of_path, Authed, OptAuthed},
variables::get_workspace_key,
BASE_URL,
};
use anyhow::Context;
use axum::{
extract::{FromRequest, Json, Path, Query},
@@ -30,13 +36,8 @@ use windmill_common::{
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};
use crate::{
db::{UserDB, DB},
users::{require_owner_of_path, Authed},
variables::get_workspace_key,
BASE_URL,
use windmill_queue::{
get_queued_job, push, JobKind, JobPayload, QueueTransaction, QueuedJob, RawCode,
};
pub fn workspaced_service() -> Router {
@@ -47,6 +48,10 @@ pub fn workspaced_service() -> Router {
"/run_wait_result/p/*script_path",
post(run_wait_result_job_by_path),
)
.route(
"/run_wait_result/p/*script_path",
get(run_wait_result_job_by_path_get),
)
.route(
"/run_wait_result/h/:hash",
post(run_wait_result_job_by_hash),
@@ -60,7 +65,7 @@ pub fn workspaced_service() -> Router {
.route("/run/preview_flow", post(run_preview_flow_job))
.route("/list", get(list_jobs))
.route("/queue/list", get(list_queue_jobs))
.route("/queue/cancel/:id", post(cancel_job_api))
.route("/queue/count", get(count_queue_jobs))
.route("/completed/list", get(list_completed_jobs))
.route("/completed/get/:id", get(get_completed_job))
.route("/completed/get_result/:id", get(get_completed_job_result))
@@ -98,32 +103,39 @@ pub fn global_service() -> Router {
)
.route("/get/:id", get(get_job))
.route("/getupdate/:id", get(get_job_update))
.route("/queue/cancel/:id", post(cancel_job_api))
.route("/queue/force_cancel/:id", post(force_cancel))
}
async fn get_result_by_id(
Extension(db): Extension<DB>,
Query(ResultByIdQuery { skip_direct }): Query<ResultByIdQuery>,
Path((w_id, flow_id, node_id)): Path<(String, String, String)>,
Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>,
) -> windmill_common::error::JsonResult<serde_json::Value> {
let res = windmill_queue::get_result_by_id(db, skip_direct, w_id, flow_id, node_id).await?;
let res = windmill_queue::get_result_by_id(db, w_id, flow_id, node_id).await?;
Ok(Json(res))
}
async fn cancel_job_api(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Json(CancelJob { reason }): Json<CancelJob>,
) -> error::Result<String> {
let tx = user_db.begin(&authed).await?;
let tx = db.begin().await?;
let username = match opt_authed {
Some(authed) => authed.username,
None => "anonymous".to_string(),
};
let (mut tx, job_option) =
windmill_queue::cancel_job(&authed.username, reason, id, &w_id, tx).await?;
windmill_queue::cancel_job(&username, reason, id, &w_id, tx, rsmq, false).await?;
if let Some(id) = job_option {
audit_log(
&mut tx,
&authed.username,
&username,
"jobs.cancel",
ActionKind::Delete,
&w_id,
@@ -146,6 +158,49 @@ async fn cancel_job_api(
}
}
async fn force_cancel(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Json(CancelJob { reason }): Json<CancelJob>,
) -> error::Result<String> {
let tx = db.begin().await?;
let username = match opt_authed {
Some(authed) => authed.username,
None => "anonymous".to_string(),
};
let (mut tx, job_option) =
windmill_queue::cancel_job(&username, reason, id, &w_id, tx, rsmq, true).await?;
if let Some(id) = job_option {
audit_log(
&mut tx,
&username,
"jobs.force_cancel",
ActionKind::Delete,
&w_id,
Some(&id.to_string()),
None,
)
.await?;
tx.commit().await?;
Ok(id.to_string())
} else {
let (job_o, tx) = get_job_by_id(tx, &w_id, id).await?;
tx.commit().await?;
let err = match job_o {
Some(Job::CompletedJob(_)) => {
return Ok(format!("queued job id {} is already completed", id))
}
_ => error::Error::NotFound(format!("queued job id {} does not exist", id)),
};
Err(err)
}
}
pub async fn get_path_for_hash<'c>(
db: &mut Transaction<'c, Postgres>,
w_id: &str,
@@ -177,11 +232,6 @@ async fn get_job(
Ok(Json(job))
}
#[derive(Deserialize)]
pub struct ResultByIdQuery {
pub skip_direct: bool,
}
pub async fn get_job_by_id<'c>(
mut tx: Transaction<'c, Postgres>,
w_id: &str,
@@ -267,6 +317,7 @@ pub struct RunJobQuery {
include_header: Option<String>,
invisible_to_owner: Option<bool>,
queue_limit: Option<i64>,
payload: Option<String>,
}
lazy_static::lazy_static! {
@@ -295,41 +346,51 @@ impl RunJobQuery {
fn add_include_headers(
&self,
headers: HeaderMap,
mut args: serde_json::Map<String, serde_json::Value>,
args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
let whitelist = self
.include_header
.as_ref()
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
.unwrap_or_default();
whitelist
.iter()
.chain(INCLUDE_HEADERS.iter())
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
h.to_string().to_lowercase().replace('-', "_"),
serde_json::Value::String(v.to_str().unwrap().to_string()),
);
}
});
args
return add_include_headers(&self.include_header, headers, args);
}
}
pub fn add_include_headers(
include_header: &Option<String>,
headers: HeaderMap,
mut args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
let whitelist = include_header
.as_ref()
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
.unwrap_or_default();
whitelist
.iter()
.chain(INCLUDE_HEADERS.iter())
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
h.to_string().to_lowercase().replace('-', "_"),
serde_json::Value::String(v.to_str().unwrap().to_string()),
);
}
});
args
}
#[derive(Deserialize)]
pub struct ListQueueQuery {
pub script_path_start: Option<String>,
pub script_path_exact: Option<String>,
pub script_hash: Option<String>,
pub created_by: Option<String>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
pub running: Option<bool>,
pub parent_job: Option<String>,
pub order_desc: Option<bool>,
pub job_kinds: Option<String>,
pub suspended: Option<bool>,
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
}
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
@@ -358,11 +419,11 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
if let Some(pj) = &lq.parent_job {
sqlb.and_where_eq("parent_job", "?".bind(pj));
}
if let Some(dt) = &lq.created_before {
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.started_before {
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(dt) = &lq.created_after {
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.started_after {
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(s) = &lq.suspended {
@@ -372,6 +433,7 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
sqlb.and_where_eq("suspend", 0);
}
}
if let Some(jk) = &lq.job_kinds {
sqlb.and_where_in(
"job_kind",
@@ -379,6 +441,10 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
);
}
if let Some(args) = &lq.args {
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
}
sqlb
}
@@ -434,6 +500,26 @@ async fn list_queue_jobs(
Ok(Json(jobs))
}
#[derive(Serialize, Debug, FromRow)]
struct QueueStats {
database_length: i64,
}
async fn count_queue_jobs(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> error::JsonResult<QueueStats> {
Ok(Json(
sqlx::query_as!(
QueueStats,
"SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE workspace_id = $1",
w_id
)
.fetch_one(&db)
.await?,
))
}
async fn list_jobs(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -451,13 +537,14 @@ async fn list_jobs(
script_path_exact: lq.script_path_exact,
script_hash: lq.script_hash,
created_by: lq.created_by,
created_before: lq.created_before,
created_after: lq.created_after,
started_before: lq.started_before,
started_after: lq.started_after,
running: None,
parent_job: lq.parent_job,
order_desc: Some(true),
job_kinds: lq.job_kinds,
suspended: lq.suspended,
args: lq.args,
},
&[
"'QueuedJob' as typ",
@@ -730,6 +817,7 @@ async fn get_suspended_flow_info<'c>(
pub async fn cancel_suspended_job(
/* unauthed */
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>,
Query(approver): Query<QueryApprover>,
) -> error::Result<String> {
@@ -752,6 +840,8 @@ pub async fn cancel_suspended_job(
parent_flow,
&w_id,
tx,
rsmq,
false,
)
.await?;
if job.is_some() {
@@ -1068,6 +1158,8 @@ impl From<UnifiedJob> for Job {
visible_to_owner: uj.visible_to_owner,
suspend: uj.suspend,
mem_peak: uj.mem_peak,
root_job: None,
leaf_jobs: None,
}),
t => panic!("job type {} not valid", t),
}
@@ -1130,26 +1222,27 @@ where
struct InPayload {
payload: Option<String>,
}
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
let vec = base64::engine::general_purpose::URL_SAFE
.decode(t)
.context("invalid base64")?;
serde_json::from_slice(vec.as_slice()).context("invalid json")
}
}
}
fn decode_payload<D: DeserializeOwned>(t: String) -> anyhow::Result<D> {
let vec = base64::engine::general_purpose::URL_SAFE
.decode(t)
.context("invalid base64")?;
serde_json::from_slice(vec.as_slice()).context("invalid json")
}
pub async fn run_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
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::Result<(StatusCode, String)> {
let flow_path = flow_path.to_path();
let mut tx = user_db.begin(&authed).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
@@ -1163,6 +1256,7 @@ pub async fn run_flow_by_path(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1176,15 +1270,16 @@ pub async fn run_flow_by_path(
pub async fn run_job_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, script_path)): Path<(String, StripPath)>,
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?;
let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let job_payload = script_path_to_payload(script_path, tx.transaction_mut(), &w_id).await?;
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
@@ -1198,6 +1293,7 @@ pub async fn run_job_by_path(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1322,9 +1418,65 @@ lazy_static::lazy_static! {
.and_then(|x| x.parse().ok())
.unwrap_or(20);
}
pub async fn run_wait_result_job_by_path_get(
authed: Authed,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
) -> error::JsonResult<serde_json::Value> {
let payload_r = run_query
.payload
.map(decode_payload)
.map(|x| x.map_err(|e| Error::InternalErr(e.to_string())));
let args = if let Some(payload) = payload_r {
payload?
} else {
serde_json::Map::new()
};
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: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
let job_payload = script_path_to_payload(script_path, tx.transaction_mut(), &w_id).await?;
let (uuid, tx) = push(
tx,
&w_id,
job_payload,
args,
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
None,
None,
run_query.parent_job,
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, script_path)),
)
.await
}
pub async fn run_wait_result_job_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
@@ -1333,9 +1485,8 @@ pub async fn run_wait_result_job_by_path(
) -> 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?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
let job_payload = script_path_to_payload(script_path, tx.transaction_mut(), &w_id).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
@@ -1347,8 +1498,9 @@ pub async fn run_wait_result_job_by_path(
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
@@ -1371,6 +1523,7 @@ pub async fn run_wait_result_job_by_path(
pub async fn run_wait_result_job_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
Query(run_query): Query<RunJobQuery>,
@@ -1380,9 +1533,9 @@ pub async fn run_wait_result_job_by_hash(
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?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
let path = get_path_for_hash(tx.transaction_mut(), &w_id, hash).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
@@ -1393,8 +1546,9 @@ pub async fn run_wait_result_job_by_hash(
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
@@ -1417,6 +1571,7 @@ pub async fn run_wait_result_job_by_hash(
pub async fn run_wait_result_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
@@ -1426,8 +1581,8 @@ pub async fn run_wait_result_flow_by_path(
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 mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
@@ -1441,13 +1596,13 @@ pub async fn run_wait_result_flow_by_path(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
)
.await?;
tx.commit().await?;
run_wait_result(
@@ -1478,13 +1633,14 @@ pub async fn script_path_to_payload<'c>(
async fn run_preview_job(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
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?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
let args = run_query.add_include_headers(headers, preview.args.unwrap_or_default());
let (uuid, tx) = push(
@@ -1503,6 +1659,7 @@ async fn run_preview_job(
scheduled_for,
None,
None,
None,
false,
false,
None,
@@ -1510,19 +1667,21 @@ async fn run_preview_job(
)
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, uuid.to_string()))
}
async fn run_preview_flow_job(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
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?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
let args = run_query.add_include_headers(headers, raw_flow.args.unwrap_or_default());
let (uuid, tx) = push(
@@ -1536,6 +1695,7 @@ async fn run_preview_flow_job(
scheduled_for,
None,
None,
None,
false,
false,
None,
@@ -1543,21 +1703,23 @@ async fn run_preview_flow_job(
)
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, uuid.to_string()))
}
pub async fn run_job_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
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?;
let path = get_path_for_hash(&mut tx, &w_id, hash).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let path = get_path_for_hash(tx.transaction_mut(), &w_id, hash).await?;
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
@@ -1571,6 +1733,7 @@ pub async fn run_job_by_hash(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1578,6 +1741,7 @@ pub async fn run_job_by_hash(
)
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, uuid.to_string()))
}
@@ -1673,11 +1837,11 @@ fn list_completed_jobs_query(
if let Some(pj) = &lq.parent_job {
sqlb.and_where_eq("parent_job", "?".bind(pj));
}
if let Some(dt) = &lq.created_before {
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.started_before {
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(dt) = &lq.created_after {
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.started_after {
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(sk) = &lq.is_skipped {
sqlb.and_where_eq("is_skipped", sk);
@@ -1692,6 +1856,14 @@ fn list_completed_jobs_query(
);
}
if let Some(args) = &lq.args {
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
}
if let Some(result) = &lq.result {
sqlb.and_where("result @> ?".bind(&result.replace("'", "''")));
}
sqlb
}
#[derive(Deserialize, Clone)]
@@ -1700,8 +1872,8 @@ pub struct ListCompletedQuery {
pub script_path_exact: Option<String>,
pub script_hash: Option<String>,
pub created_by: Option<String>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
pub success: Option<bool>,
pub parent_job: Option<String>,
pub order_desc: Option<bool>,
@@ -1709,6 +1881,10 @@ pub struct ListCompletedQuery {
pub is_skipped: Option<bool>,
pub is_flow_step: Option<bool>,
pub suspended: Option<bool>,
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
// filter by matching a subset of the result using base64 encoded json subset
pub result: Option<String>,
}
async fn list_completed_jobs(
@@ -1775,6 +1951,7 @@ async fn get_completed_job(
.fetch_optional(&db)
.await?;
tracing::info!("job_o: {:?}", job_o);
let job = not_found_if_none(job_o, "Completed Job", id.to_string())?;
Ok(Json(job))
}

View File

@@ -7,17 +7,6 @@
*/
use crate::oauth2::AllClients;
use argon2::Argon2;
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::utils::rd_string;
use crate::{
db::UserDB,
oauth2::{build_oauth_clients, SlackVerifier},
@@ -25,6 +14,20 @@ use crate::{
users::{Authed, OptAuthed},
webhook_util::WebhookShared,
};
use argon2::Argon2;
use axum::{middleware::from_extractor, routing::get, Extension, Router};
use db::DB;
use git_version::git_version;
use hyper::Method;
use reqwest::Client;
use std::{net::SocketAddr, sync::Arc};
use tower::ServiceBuilder;
use tower_cookies::CookieManagerLayer;
use tower_http::{
cors::{Any, CorsLayer},
trace::TraceLayer,
};
use windmill_common::utils::rd_string;
mod apps;
mod audit;
@@ -35,6 +38,7 @@ mod flows;
mod folders;
mod granular_acls;
mod groups;
mod inputs;
pub mod jobs;
mod oauth2;
mod resources;
@@ -77,6 +81,7 @@ lazy_static::lazy_static! {
pub async fn run_server(
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
addr: SocketAddr,
mut rx: tokio::sync::broadcast::Receiver<()>,
) -> anyhow::Result<()> {
@@ -96,10 +101,16 @@ pub async fn run_server(
.on_request(()),
)
.layer(Extension(db.clone()))
.layer(Extension(rsmq))
.layer(Extension(user_db))
.layer(Extension(auth_cache.clone()))
.layer(CookieManagerLayer::new())
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())));
let cors = CorsLayer::new()
.allow_methods([Method::GET, Method::POST])
.allow_origin(Any);
// build our application with a route
let app = Router::new()
.nest(
@@ -108,25 +119,27 @@ pub async fn run_server(
.nest(
"/w/:workspace_id",
Router::new()
// Reordered alphabetically
.nest("/acls", granular_acls::workspaced_service())
.nest("/apps", apps::workspaced_service())
.nest("/audit", audit::workspaced_service())
.nest("/capture", capture::workspaced_service())
.nest("/favorites", favorite::workspaced_service())
.nest("/flows", flows::workspaced_service())
.nest("/folders", folders::workspaced_service())
.nest("/groups", groups::workspaced_service())
.nest("/inputs", inputs::workspaced_service())
.nest("/jobs", jobs::workspaced_service().layer(cors.clone()))
.nest("/oauth", oauth2::workspaced_service())
.nest("/resources", resources::workspaced_service())
.nest("/schedules", schedule::workspaced_service())
.nest("/scripts", scripts::workspaced_service())
.nest("/jobs", jobs::workspaced_service())
.nest(
"/users",
users::workspaced_service().layer(Extension(argon2.clone())),
)
.nest("/variables", variables::workspaced_service())
.nest("/oauth", oauth2::workspaced_service())
.nest("/resources", resources::workspaced_service())
.nest("/schedules", schedule::workspaced_service())
.nest("/groups", groups::workspaced_service())
.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("/favorites", favorite::workspaced_service())
.nest("/folders", folders::workspaced_service()),
.nest("/workspaces", workspaces::workspaced_service()),
)
.nest("/workspaces", workspaces::global_service())
.nest(
@@ -136,16 +149,25 @@ pub async fn run_server(
.nest("/workers", worker_ping::global_service())
.nest("/scripts", scripts::global_service())
.nest("/flows", flows::global_service())
.nest("/apps", apps::global_service())
.nest("/apps", apps::global_service().layer(cors.clone()))
.nest("/schedules", schedule::global_service())
.route_layer(from_extractor::<Authed>())
.route_layer(from_extractor::<users::Tokened>())
.nest("/scripts_u", scripts::global_unauthed_service())
.nest(
"/w/:workspace_id/apps_u",
apps::unauthed_service().layer(from_extractor::<OptAuthed>()),
apps::unauthed_service()
.layer(from_extractor::<OptAuthed>())
.layer(cors.clone()),
)
.nest(
"/w/:workspace_id/jobs_u",
jobs::global_service().layer(cors.clone()),
)
.nest(
"/w/:workspace_id/capture_u",
capture::global_service().layer(cors),
)
.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)),

View File

@@ -32,7 +32,8 @@ 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, NEW_USER_WEBHOOK};
use crate::users::{truncate_token, Authed};
use crate::webhook_util::{InstanceEvent, WebhookShared};
use crate::workspaces::invite_user_to_all_auto_invite_worspaces;
use crate::{
db::{UserDB, DB},
@@ -43,7 +44,7 @@ use crate::{BASE_URL, HTTP_CLIENT, IS_SECURE, OAUTH_CLIENTS, SLACK_SIGNING_SECRE
use windmill_common::error::{self, to_anyhow, Error};
use windmill_common::oauth2::*;
use windmill_queue::JobPayload;
use windmill_queue::{JobPayload, QueueTransaction};
use std::{fs, str};
@@ -728,23 +729,20 @@ where
async fn slack_command(
SlackSig { sig, ts }: SlackSig,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
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_SIGNING_SECRET
.as_ref()
.as_ref()
.map(|sv| sv.verify(&ts, &body, &sig).ok())
.flatten()
.is_none()
{
return Err(error::Error::BadRequest("verification failed".to_owned()));
if let Some(sv) = SLACK_SIGNING_SECRET.as_ref() {
if sv.verify(&ts, &body, &sig).ok().is_none() {
return Err(error::Error::BadRequest("verification failed".to_owned()));
}
}
let mut tx = db.begin().await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, db.begin().await?).into();
let settings = sqlx::query_as!(
WorkspaceSettings,
"SELECT * FROM workspace_settings WHERE slack_team_id = $1",
@@ -760,7 +758,7 @@ async fn slack_command(
} else {
let path = path.strip_prefix("script/").unwrap_or_else(|| path);
let script_hash = windmill_common::get_latest_hash_for_path(
&mut tx,
tx.transaction_mut(),
&settings.workspace_id,
path,
)
@@ -785,20 +783,22 @@ async fn slack_command(
None,
None,
None,
None,
false,
false,
None,
true,
)
.await?;
tx.commit().await?;
let url = BASE_URL.to_owned();
tx.commit().await?;
return Ok(format!(
"Job launched. See details at {url}/run/{uuid}?workspace={}",
&settings.workspace_id
));
}
}
tx.commit().await?;
return Ok(format!(
"workspace not properly configured (did you set the script to trigger in the settings?)"
@@ -818,6 +818,7 @@ async fn login_callback(
Path(client_name): Path<String>,
cookies: Cookies,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Json(callback): Json<OAuthCallback>,
) -> error::Result<String> {
let client_w_config = &OAUTH_CLIENTS
@@ -852,7 +853,8 @@ async fn login_callback(
_ => user.email.ok_or_else(|| {
error::Error::BadRequest("email address not fetchable from user info".to_string())
})?,
};
}
.to_lowercase();
if let Some(domains) = &client_w_config.allowed_domains {
if !domains.iter().any(|d| email.ends_with(d)) {
@@ -942,14 +944,7 @@ 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()));
}
webhook.send_instance_event(InstanceEvent::UserSignupOAuth { email: email.clone() });
Ok("Successfully logged in".to_string())
} else {

View File

@@ -8,7 +8,7 @@
use crate::{
db::{UserDB, DB},
users::{require_owner_of_path, Authed},
users::{maybe_refresh_folders, require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
@@ -264,9 +264,12 @@ async fn create_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(resource): Json<CreateResource>,
) -> Result<(StatusCode, String)> {
let authed = maybe_refresh_folders(&resource.path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;
check_path_conflict(&mut tx, &w_id, &resource.path).await?;
@@ -375,6 +378,7 @@ async fn update_resource(
}
sqlb.returning("path");
let authed = maybe_refresh_folders(path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;

View File

@@ -6,27 +6,26 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::str::FromStr;
use crate::{
db::{UserDB, DB},
users::Authed,
users::{maybe_refresh_folders, Authed},
};
use axum::{
extract::{Extension, Path, Query},
routing::{delete, get, post},
Json, Router,
};
use chrono::{DateTime, FixedOffset};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use sqlx::{Postgres, Transaction};
use std::str::FromStr;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
schedule::Schedule,
utils::{not_found_if_none, paginate, Pagination, StripPath},
};
use windmill_queue::{self, schedule::push_scheduled_job, JobKind};
use windmill_queue::{self, schedule::push_scheduled_job, JobKind, QueueTransaction};
pub fn workspaced_service() -> Router {
Router::new()
@@ -47,7 +46,7 @@ pub fn global_service() -> Router {
pub struct NewSchedule {
pub path: String,
pub schedule: String,
pub offset: i32,
pub timezone: String,
pub script_path: String,
pub is_flow: bool,
pub args: Option<serde_json::Value>,
@@ -78,23 +77,34 @@ async fn check_path_conflict<'c>(
async fn create_schedule(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(ns): Json<NewSchedule>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
check_path_conflict(&mut tx, &w_id, &ns.path).await?;
check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?;
check_path_conflict(tx.transaction_mut(), &w_id, &ns.path).await?;
check_flow_conflict(
tx.transaction_mut(),
&w_id,
&ns.path,
ns.is_flow,
&ns.script_path,
)
.await?;
let schedule = sqlx::query_as!(
Schedule,
"INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, \
"INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, \
is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *",
w_id,
ns.path,
ns.schedule,
ns.offset,
ns.timezone,
&authed.username,
ns.script_path,
ns.is_flow,
@@ -135,13 +145,18 @@ async fn create_schedule(
async fn edit_schedule(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(es): Json<EditSchedule>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
let authed = maybe_refresh_folders(&path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, rsmq_async::MultiplexedRsmq> =
(rsmq, user_db.begin(&authed).await?).into();
cron::Schedule::from_str(&es.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
let is_flow = sqlx::query_scalar!(
@@ -152,12 +167,13 @@ async fn edit_schedule(
.fetch_one(&mut tx)
.await?;
clear_schedule(&mut tx, path, is_flow).await?;
clear_schedule(tx.transaction_mut(), path, is_flow).await?;
let schedule = sqlx::query_as!(
Schedule,
"UPDATE schedule SET schedule = $1, args = $2 WHERE path \
= $3 AND workspace_id = $4 RETURNING *",
"UPDATE schedule SET schedule = $1, timezone = $2, args = $3 WHERE path \
= $4 AND workspace_id = $5 RETURNING *",
es.schedule,
es.timezone,
es.args,
path,
w_id,
@@ -166,10 +182,6 @@ async fn edit_schedule(
.await
.map_err(|e| Error::InternalErr(format!("updating schedule in {w_id}: {e}")))?;
if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
audit_log(
&mut tx,
&authed.username,
@@ -185,6 +197,10 @@ async fn edit_schedule(
),
)
.await?;
if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
tx.commit().await?;
Ok(path.to_string())
@@ -235,15 +251,26 @@ async fn exists_schedule(
Ok(Json(res))
}
#[derive(Deserialize)]
pub struct PreviewPayload {
pub schedule: String,
pub timezone: String,
}
pub async fn preview_schedule(
Json(payload): Json<PreviewPayload>,
) -> JsonResult<Vec<DateTime<chrono::Utc>>> {
) -> JsonResult<Vec<DateTime<Utc>>> {
let schedule = cron::Schedule::from_str(&payload.schedule)
.map_err(|e| Error::BadRequest(e.to_string()))?;
let upcoming: Vec<DateTime<chrono::Utc>> = schedule
.upcoming(get_offset(payload.offset))
.take(10)
.map(|x| x.into())
let tz =
chrono_tz::Tz::from_str(&payload.timezone).map_err(|e| Error::BadRequest(e.to_string()))?;
let upcoming: Vec<DateTime<Utc>> = schedule
.upcoming(tz)
.take(5)
// Convert back to UTC for a standardised API response. The client will convert to the local timezone.
.map(|x| x.with_timezone(&Utc))
.collect();
Ok(Json(upcoming))
@@ -252,10 +279,12 @@ pub async fn preview_schedule(
pub async fn set_enabled(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<SetEnabled>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
let mut tx: QueueTransaction<'_, rsmq_async::MultiplexedRsmq> =
(rsmq, user_db.begin(&authed).await?).into();
let path = path.to_path();
let schedule_o = sqlx::query_as!(
Schedule,
@@ -270,11 +299,8 @@ pub async fn set_enabled(
let schedule = not_found_if_none(schedule_o, "Schedule", path)?;
clear_schedule(&mut tx, path, schedule.is_flow).await?;
clear_schedule(tx.transaction_mut(), path, schedule.is_flow).await?;
if payload.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
audit_log(
&mut tx,
&authed.username,
@@ -285,7 +311,12 @@ pub async fn set_enabled(
Some([("enabled", payload.enabled.to_string().as_ref())].into()),
)
.await?;
if payload.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
tx.commit().await?;
Ok(format!(
"succesfully updated schedule at path {} to status {}",
path, payload.enabled
@@ -353,6 +384,7 @@ async fn check_flow_conflict<'c>(
#[derive(Deserialize)]
pub struct EditSchedule {
pub schedule: String,
pub timezone: String,
pub args: Option<serde_json::Value>,
}
@@ -376,16 +408,6 @@ pub async fn clear_schedule<'c>(
Ok(())
}
#[derive(Deserialize)]
pub struct PreviewPayload {
pub schedule: String,
pub offset: Option<i32>,
}
fn get_offset(offset: Option<i32>) -> FixedOffset {
FixedOffset::west_opt(offset.unwrap_or(0) * 60).expect("Invalid offset")
}
#[derive(Deserialize)]
pub struct SetEnabled {
pub enabled: bool,

View File

@@ -6,13 +6,10 @@
* LICENSE-AGPL for a copy of the license.
*/
use sql_builder::prelude::*;
use windmill_audit::{audit_log, ActionKind};
use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
users::{maybe_refresh_folders, require_owner_of_path, AuthCache, Authed},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
@@ -24,12 +21,15 @@ use axum::{
use hyper::StatusCode;
use serde::Serialize;
use serde_json::json;
use sql_builder::prelude::*;
use sql_builder::SqlBuilder;
use sqlx::{FromRow, Postgres, Transaction};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
sync::Arc,
};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
schedule::Schedule,
@@ -42,7 +42,8 @@ use windmill_common::{
list_elems_from_hub, not_found_if_none, paginate, require_admin, Pagination, StripPath,
},
};
use windmill_queue::{self, schedule::push_scheduled_job};
use windmill_parser::MainArgSignature;
use windmill_queue::{self, schedule::push_scheduled_job, QueueTransaction};
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
@@ -60,6 +61,13 @@ pub fn global_service() -> Router {
.route("/hub/get_full/*path", get(get_full_hub_script_by_path))
}
pub fn global_unauthed_service() -> Router {
Router::new().route(
"/tokened_raw/:workspace/:token/*path",
get(get_tokened_raw_script_by_path),
)
}
pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_scripts))
@@ -70,11 +78,13 @@ pub fn workspaced_service() -> Router {
.route("/exists/p/*path", get(exists_script_by_path))
.route("/archive/h/:hash", post(archive_script_by_hash))
.route("/delete/h/:hash", post(delete_script_by_hash))
.route("/delete/p/*path", post(delete_script_by_path))
.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,
Extension(user_db): Extension<UserDB>,
@@ -123,6 +133,7 @@ async fn list_scripts(
AND workspace_id = ?)"
.bind(&w_id),
);
sqlb.and_where_eq("archived", true);
} else {
sqlb.and_where_eq("archived", false);
}
@@ -182,20 +193,22 @@ fn hash_script(ns: &NewScript) -> i64 {
async fn create_script(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(ns): Json<NewScript>,
) -> Result<(StatusCode, String)> {
let hash = ScriptHash(hash_script(&ns));
let mut tx = user_db.begin(&authed).await?;
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
if sqlx::query_scalar!(
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
hash.0,
&w_id
)
.fetch_optional(&mut tx)
.fetch_optional(tx.transaction_mut())
.await?
.is_some()
{
@@ -256,7 +269,7 @@ async fn create_script(
)));
};
let ps = get_script_by_hash_internal(&mut tx, &w_id, p_hash).await?;
let ps = get_script_by_hash_internal(tx.transaction_mut(), &w_id, p_hash).await?;
if ps.path != ns.path {
if !authed.is_admin {
@@ -351,7 +364,7 @@ async fn create_script(
.await?;
for schedule in schedulables {
clear_schedule(&mut tx, &schedule.path, false).await?;
clear_schedule(tx.transaction_mut(), &schedule.path, false).await?;
if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?;
@@ -359,35 +372,6 @@ async fn create_script(
}
}
let mut tx = if needs_lock_gen {
let dependencies = match ns.language {
ScriptLang::Python3 => {
windmill_parser_py::parse_python_imports(&ns.content)?.join("\n")
}
_ => ns.content,
};
let (_, tx) = windmill_queue::push(
tx,
&w_id,
windmill_queue::JobPayload::Dependencies { hash, dependencies, language: ns.language },
serde_json::Map::new(),
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
None,
None,
None,
false,
false,
None,
true,
)
.await?;
tx
} else {
tx
};
if p_hashes.is_some() && !p_hashes.unwrap().is_empty() {
audit_log(
&mut tx,
@@ -402,7 +386,7 @@ async fn create_script(
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateScript {
workspace: w_id,
workspace: w_id.clone(),
path: ns.path.clone(),
hash: hash.to_string(),
},
@@ -427,13 +411,41 @@ async fn create_script(
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateScript {
workspace: w_id,
workspace: w_id.clone(),
path: ns.path.clone(),
hash: hash.to_string(),
},
);
}
if needs_lock_gen {
let dependencies = match ns.language {
ScriptLang::Python3 => {
windmill_parser_py::parse_python_imports(&ns.content)?.join("\n")
}
_ => ns.content,
};
let (_, new_tx) = windmill_queue::push(
tx,
&w_id,
windmill_queue::JobPayload::Dependencies { hash, dependencies, language: ns.language },
serde_json::Map::new(),
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
None,
None,
None,
None,
false,
false,
None,
true,
)
.await?;
tx = new_tx;
}
tx.commit().await?;
Ok((StatusCode::CREATED, format!("{}", hash)))
@@ -493,6 +505,18 @@ async fn list_paths(
Ok(Json(scripts))
}
async fn get_tokened_raw_script_by_path(
Extension(user_db): Extension<UserDB>,
Path((w_id, token, path)): Path<(String, String, StripPath)>,
Extension(cache): Extension<Arc<AuthCache>>,
) -> Result<String> {
let authed = cache
.get_authed(Some(w_id.clone()), &token)
.await
.ok_or_else(|| Error::NotAuthorized("Invalid token".to_string()))?;
return raw_script_by_path(authed, Extension(user_db), Path((w_id, path))).await;
}
async fn raw_script_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -553,11 +577,10 @@ async fn get_script_by_hash_internal<'c>(
}
async fn get_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
let mut tx = user_db.begin(&authed).await?;
let mut tx = db.begin().await?;
let r = get_script_by_hash_internal(&mut tx, &w_id, &hash).await?;
tx.commit().await?;
@@ -565,11 +588,10 @@ async fn get_script_by_hash(
}
async fn raw_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, hash_str)): Path<(String, String)>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
let mut tx = db.begin().await?;
let hash = ScriptHash(to_i64(hash_str.strip_suffix(".ts").ok_or_else(|| {
Error::BadRequest("Raw script path must end with .ts".to_string())
})?)?);
@@ -585,11 +607,10 @@ struct DeploymentStatus {
lock_error_logs: Option<String>,
}
async fn get_deployment_status(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<DeploymentStatus> {
let mut tx = user_db.begin(&authed).await?;
let mut tx = db.begin().await?;
let status_o: Option<DeploymentStatus> = sqlx::query_as!(
DeploymentStatus,
"SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND workspace_id = $2",
@@ -718,25 +739,71 @@ async fn delete_script_by_hash(
Ok(Json(script))
}
async fn parse_python_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<windmill_parser::MainArgSignature> {
windmill_parser_py::parse_python_signature(&code).map(Json)
async fn delete_script_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
require_admin(authed.is_admin, &authed.username)?;
let script = sqlx::query_scalar!(
"DELETE FROM script WHERE path = $1 AND workspace_id = $2 RETURNING path",
path,
w_id
)
.fetch_one(&db)
.await
.map_err(|e| Error::InternalErr(format!("deleting script by path {w_id}: {e}")))?;
audit_log(
&mut tx,
&authed.username,
"scripts.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::DeleteScriptPath { workspace: w_id, path: path.to_string() },
);
Ok(Json(script))
}
async fn parse_deno_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<windmill_parser::MainArgSignature> {
windmill_parser_ts::parse_deno_signature(&code).map(Json)
}
async fn parse_go_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<windmill_parser::MainArgSignature> {
windmill_parser_go::parse_go_sig(&code).map(Json)
#[derive(Debug, Serialize)]
#[serde(tag = "type")]
enum SigParsing {
Valid(MainArgSignature),
Invalid { error: String },
}
async fn parse_bash_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<windmill_parser::MainArgSignature> {
windmill_parser_bash::parse_bash_sig(&code).map(Json)
fn result_to_sig_parsing(result: Result<MainArgSignature>) -> Json<SigParsing> {
match result {
Ok(sig) => Json(SigParsing::Valid(sig)),
Err(e) => Json(SigParsing::Invalid { error: e.to_string() }),
}
}
async fn parse_python_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_py::parse_python_signature(&code))
}
async fn parse_deno_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_ts::parse_deno_signature(&code, false))
}
async fn parse_go_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_go::parse_go_sig(&code))
}
async fn parse_bash_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_bash::parse_bash_sig(&code))
}

View File

@@ -12,8 +12,9 @@ use crate::{
db::{UserDB, DB},
folders::get_folders_for_user,
utils::require_super_admin,
webhook_util::{InstanceEvent, WebhookShared},
workspaces::invite_user_to_all_auto_invite_worspaces,
COOKIE_DOMAIN, HTTP_CLIENT, IS_SECURE,
COOKIE_DOMAIN, IS_SECURE,
};
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use axum::{
@@ -25,7 +26,9 @@ use axum::{
Json, Router,
};
use hyper::{header::LOCATION, StatusCode};
use lazy_static::lazy_static;
use rand::rngs::OsRng;
use regex::Regex;
use retainer::Cache;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
@@ -35,6 +38,7 @@ use tracing::{Instrument, Span};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, Error, JsonResult, Result},
users::SUPERADMIN_SECRET_EMAIL,
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
};
use windmill_queue::CLOUD_HOSTED;
@@ -73,6 +77,7 @@ pub fn global_service() -> Router {
.route("/tokens/create", post(create_token))
.route("/tokens/delete/:token_prefix", delete(delete_token))
.route("/tokens/list", get(list_tokens))
.route("/tokens/impersonate", post(impersonate))
.route("/usage", get(get_usage))
// .route("/list_invite_codes", get(list_invite_codes))
// .route("/create_invite_code", post(create_invite_code))
@@ -99,6 +104,10 @@ impl AuthCache {
AuthCache { cache: Cache::new(), db, superadmin_secret }
}
pub async fn invalidate(&self, w_id: &str, token: String) {
self.cache.remove(&(w_id.to_string(), token)).await;
}
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<Authed> {
let key = (
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
@@ -266,7 +275,7 @@ impl AuthCache {
.unwrap_or(false)
{
Some(Authed {
email: "superadmin_secret@windmill.dev".to_string(),
email: SUPERADMIN_SECRET_EMAIL.to_string(),
username: "superadmin_secret".to_string(),
is_admin: true,
groups: Vec::new(),
@@ -353,6 +362,31 @@ pub struct Authed {
pub folders: Vec<(String, bool)>,
}
pub async fn maybe_refresh_folders(path: &str, w_id: &str, authed: Authed, db: &DB) -> Authed {
if authed.is_admin {
return authed;
}
let splitted = path.split('/').collect::<Vec<_>>();
if splitted.len() >= 2
&& splitted[0] == "f"
&& !authed.folders.iter().any(|(f, _)| f == splitted[1])
{
let name = &authed.username;
let groups = get_groups_for_user(w_id, name, db)
.await
.ok()
.unwrap_or_default();
let folders = get_folders_for_user(w_id, name, &groups, db)
.await
.ok()
.unwrap_or_default();
Authed { folders, ..authed }
} else {
authed
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Authed
where
@@ -561,6 +595,7 @@ pub struct TruncatedToken {
pub struct NewToken {
pub label: Option<String>,
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
pub impersonate_email: Option<String>,
}
#[derive(Deserialize)]
@@ -997,14 +1032,22 @@ async fn decline_invite(
}
}
lazy_static! {
pub static ref VALID_USERNAME: Regex = Regex::new(r#"^[a-zA-Z_0-9]+$"#).unwrap();
}
async fn accept_invite(
Authed { email, .. }: Authed,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Json(nu): Json<AcceptInvite>,
) -> Result<(StatusCode, String)> {
if &nu.username == "bot" {
return Err(Error::BadRequest("bot is a reserved username".to_string()));
if !VALID_USERNAME.is_match(&nu.username) {
return Err(windmill_common::error::Error::BadRequest(format!(
"Usermame can only contain alphanumeric characters and underscores"
)));
}
let mut tx = db.begin().await?;
let r = sqlx::query!(
@@ -1041,6 +1084,11 @@ async fn accept_invite(
}
if is_some {
webhook.send_instance_event(InstanceEvent::UserJoinedWorkspace {
email: email.clone(),
workspace: nu.workspace_id.clone(),
username: nu.username.clone(),
});
Ok((
StatusCode::CREATED,
format!(
@@ -1077,6 +1125,12 @@ async fn add_user_to_workspace<'c>(
)));
}
if !VALID_USERNAME.is_match(username) {
return Err(windmill_common::error::Error::BadRequest(format!(
"Usermame can only contain alphanumeric characters and underscores"
)));
}
let already_exists_email = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)",
&w_id,
@@ -1275,12 +1329,20 @@ lazy_static::lazy_static! {
async fn create_user(
Authed { email, .. }: Authed,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Extension(argon2): Extension<Arc<Argon2<'_>>>,
Json(nu): Json<NewUser>,
Json(mut nu): Json<NewUser>,
) -> Result<(StatusCode, String)> {
let mut tx = db.begin().await?;
require_super_admin(&mut tx, &email).await?;
nu.email = nu.email.to_lowercase();
if nu.email == SUPERADMIN_SECRET_EMAIL {
return Err(Error::BadRequest(
"The superadmin email is a reserved email".into(),
));
}
sqlx::query!(
"INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, \
@@ -1308,17 +1370,9 @@ 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?;
webhook.send_instance_event(InstanceEvent::UserAdded { email: nu.email.clone() });
Ok((StatusCode::CREATED, format!("email {} created", nu.email)))
}
@@ -1564,7 +1618,7 @@ async fn login(
Json(Login { email, password }): Json<Login>,
) -> Result<String> {
let mut tx = db.begin().await?;
let email = email.to_lowercase();
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'",
@@ -1686,6 +1740,58 @@ async fn create_token(
Ok((StatusCode::CREATED, token))
}
async fn impersonate(
Extension(db): Extension<DB>,
Authed { email, username, .. }: Authed,
Json(new_token): Json<NewToken>,
) -> Result<(StatusCode, String)> {
let token = rd_string(30);
let mut tx = db.begin().await?;
require_super_admin(&mut tx, &email).await?;
if new_token.impersonate_email.is_none() {
return Err(Error::BadRequest(
"impersonate_username is required".to_string(),
));
}
let impersonated = new_token.impersonate_email.unwrap();
let is_super_admin = sqlx::query_scalar!(
"SELECT super_admin FROM password WHERE email = $1",
impersonated
)
.fetch_optional(&mut tx)
.await?
.unwrap_or(false);
sqlx::query!(
"INSERT INTO token
(token, email, label, expiration, super_admin)
VALUES ($1, $2, $3, $4, $5)",
token,
impersonated,
new_token.label,
new_token.expiration,
is_super_admin
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&username,
"users.impersonate",
ActionKind::Delete,
&"global",
Some(&token[0..10]),
Some([("impersonated", &format!("{impersonated}")[..])].into()),
)
.instrument(tracing::info_span!("token", email = &impersonated))
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, token))
}
async fn list_tokens(
Extension(db): Extension<DB>,
Authed { email, .. }: Authed,
@@ -1786,6 +1892,17 @@ pub async fn delete_expired_items_perdiodically(
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
}
let pip_resolution_r = sqlx::query_scalar!(
"DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash",
)
.fetch_all(db)
.await;
match pip_resolution_r {
Ok(res) => tracing::debug!("deleted {} pip_resolution: {:?}", res.len(), res),
Err(e) => tracing::error!("Error deleting pip_resolution: {}", e.to_string()),
}
let magic_links_deleted_r: std::result::Result<Vec<String>, _> = sqlx::query_scalar(
"DELETE FROM magic_link WHERE expiration <= now()
RETURNING concat(substring(token for 10), '*****')",

View File

@@ -7,16 +7,24 @@
*/
use sqlx::{Postgres, Transaction};
use windmill_common::error::{self, Error};
use windmill_common::{
error::{self, Error},
users::SUPERADMIN_SECRET_EMAIL,
};
pub async fn require_super_admin<'c>(
db: &mut Transaction<'c, Postgres>,
email: &str,
) -> error::Result<()> {
if email == SUPERADMIN_SECRET_EMAIL {
return Ok(());
}
let is_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
.fetch_one(db)
.fetch_optional(db)
.await
.map_err(|e| Error::InternalErr(format!("fetching super admin: {e}")))?;
.map_err(|e| Error::InternalErr(format!("fetching super admin: {e}")))?
.unwrap_or(false);
if !is_admin {
Err(Error::NotAuthorized(
"This endpoint require caller to be a super admin".to_owned(),

View File

@@ -9,7 +9,7 @@
use crate::{
db::{UserDB, DB},
oauth2::_refresh_token,
users::{require_owner_of_path, Authed},
users::{maybe_refresh_folders, require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
/*
@@ -26,6 +26,7 @@ use axum::{
Json, Router,
};
use hyper::StatusCode;
use serde_json::Value;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
@@ -205,12 +206,15 @@ async fn check_path_conflict<'c>(
async fn create_variable(
authed: Authed,
Extension(db): Extension<DB>,
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 authed = maybe_refresh_folders(&variable.path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;
check_path_conflict(&mut tx, &w_id, &variable.path).await?;
@@ -329,6 +333,7 @@ async fn update_variable(
use sql_builder::prelude::*;
let path = path.to_path();
let authed = maybe_refresh_folders(&path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;
@@ -378,9 +383,27 @@ async fn update_variable(
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
let mut v = sqlx::query_scalar!(
"SELECT value FROM resource WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.fetch_optional(&mut tx)
.await?
.flatten();
if let Some(old_v) = v {
v = Some(replace_path(
old_v,
&format!("$var:{path}"),
&format!("$var:{npath}"),
))
}
sqlx::query!(
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
"UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4",
npath,
v,
path,
w_id
)
@@ -419,6 +442,23 @@ async fn update_variable(
Ok(format!("variable {} updated (npath: {:?})", path, npath))
}
fn replace_path(v: serde_json::Value, path: &str, npath: &str) -> Value {
match v {
Value::Object(v) => Value::Object(
v.into_iter()
.map(|(k, v)| (k, replace_path(v, path, npath)))
.collect(),
),
Value::Array(arr) => Value::Array(
arr.into_iter()
.map(|v| replace_path(v, path, npath))
.collect(),
),
Value::String(s) if s == path => Value::String(npath.to_owned()),
_ => v,
}
}
pub async fn build_crypt<'c>(
db: &mut Transaction<'c, Postgres>,
w_id: &str,

View File

@@ -2,6 +2,7 @@ use std::time::Duration;
use serde::Serialize;
use tokio::{select, sync::mpsc, time::interval};
use windmill_common::METRICS_ENABLED;
use crate::db::DB;
@@ -12,6 +13,26 @@ lazy_static::lazy_static! {
"Histogram of webhook requests made"
)
.unwrap();
pub static ref INSTANCE_EVENTS_WEBHOOK: Option<String> = std::env::var("INSTANCE_EVENTS_WEBHOOK").ok();
}
pub enum WebhookPayload {
WorkspaceEvent(String, WebhookMessage),
InstanceEvent(InstanceEvent),
}
#[derive(Serialize)]
#[serde(tag = "type")]
pub enum InstanceEvent {
UserSignupOAuth { email: String },
UserAdded { email: String },
// UserDeleted { email: String },
// UserDeletedWorkspace { workspace: String, email: String },
UserAddedWorkspace { workspace: String, email: String },
UserInvitedWorkspace { workspace: String, email: String },
UserJoinedWorkspace { workspace: String, email: String, username: String },
}
#[derive(Serialize)]
@@ -37,6 +58,7 @@ pub enum WebhookMessage {
CreateScript { workspace: String, path: String, hash: String },
UpdateScript { workspace: String, path: String, hash: String },
DeleteScript { workspace: String, hash: String },
DeleteScriptPath { workspace: String, path: String },
CreateVariable { workspace: String, path: String },
UpdateVariable { workspace: String, old_path: String, new_path: String },
DeleteVariable { workspace: String, path: String },
@@ -44,12 +66,12 @@ pub enum WebhookMessage {
#[derive(Clone)]
pub struct WebhookShared {
pub channel: mpsc::UnboundedSender<(String, WebhookMessage)>,
pub channel: mpsc::UnboundedSender<WebhookPayload>,
}
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 (tx, mut rx) = mpsc::unbounded_channel::<WebhookPayload>();
let _process = tokio::spawn(async move {
let client = reqwest::Client::builder()
// TODO: investigate pool timeouts and such if TCP load is high
@@ -64,7 +86,7 @@ impl WebhookShared {
biased;
_ = shutdown_rx.recv() => break,
r = rx.recv() => match r {
Some((workspace_id, message)) => {
Some(WebhookPayload::WorkspaceEvent(workspace_id, message)) => {
let url_guard = match cache.get(&workspace_id).await {
Some(guard) => {
guard
@@ -88,12 +110,19 @@ impl WebhookShared {
};
let webook_opt = url_guard.value();
if let Some(url) = webook_opt {
let timer = WEBHOOK_REQUEST_COUNT.start_timer();
let timer = if *METRICS_ENABLED { Some(WEBHOOK_REQUEST_COUNT.start_timer()) } else { None };
let _ = client.post(url).json(&message).send().await;
timer.stop_and_record();
timer.map(|x| x.stop_and_record());
drop(url_guard);
}
},
Some(WebhookPayload::InstanceEvent(event)) => {
if *METRICS_ENABLED { Some(WEBHOOK_REQUEST_COUNT.start_timer()) } else { None };
let r = client.post(INSTANCE_EVENTS_WEBHOOK.as_ref().unwrap()).json(&event).send().await;
if let Err(e) = r {
tracing::error!("Error sending instance event: {}", e);
}
},
None => break,
},
_ = futures::future::poll_fn(|cx| cache_purge_interval.poll_tick(cx)) => {
@@ -108,6 +137,16 @@ impl WebhookShared {
}
pub fn send_message(&self, workspace_id: String, message: WebhookMessage) {
let _ = self.channel.send((workspace_id.clone(), message));
let _ = self.channel.send(WebhookPayload::WorkspaceEvent(
workspace_id.clone(),
message,
));
}
pub fn send_instance_event(&self, event: InstanceEvent) {
if INSTANCE_EVENTS_WEBHOOK.is_none() {
return;
}
let _ = self.channel.send(WebhookPayload::InstanceEvent(event));
}
}

View File

@@ -28,7 +28,7 @@ pub fn global_service() -> Router {
struct WorkerPing {
worker: String,
worker_instance: String,
ping_at: chrono::DateTime<chrono::Utc>,
last_ping: Option<i32>,
started_at: chrono::DateTime<chrono::Utc>,
ip: String,
jobs_executed: i32,
@@ -45,7 +45,7 @@ async fn list_worker_pings(
let rows = sqlx::query_as!(
WorkerPing,
"SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
"SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
per_page as i64,
offset as i64
)

View File

@@ -16,9 +16,10 @@ use crate::{
db::{UserDB, DB},
folders::Folder,
resources::{Resource, ResourceType},
users::{Authed, WorkspaceInvite, NEW_USER_WEBHOOK},
users::{Authed, WorkspaceInvite, VALID_USERNAME},
utils::require_super_admin,
HTTP_CLIENT,
variables::build_crypt,
webhook_util::{InstanceEvent, WebhookShared},
};
#[cfg(feature = "enterprise")]
use axum::response::Redirect;
@@ -30,6 +31,7 @@ use axum::{
routing::{delete, get, post},
Json, Router,
};
use magic_crypt::MagicCryptTrait;
#[cfg(feature = "enterprise")]
use stripe::CustomerId;
use windmill_audit::{audit_log, ActionKind};
@@ -941,11 +943,14 @@ pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> R
async fn invite_user(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(nu): Json<NewWorkspaceInvite>,
Json(mut nu): Json<NewWorkspaceInvite>,
) -> Result<(StatusCode, String)> {
require_admin(is_admin, &username)?;
nu.email = nu.email.to_lowercase();
let mut tx = db.begin().await?;
sqlx::query!(
@@ -962,14 +967,10 @@ 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()));
}
webhook.send_instance_event(InstanceEvent::UserInvitedWorkspace {
email: nu.email.clone(),
workspace: w_id,
});
Ok((
StatusCode::CREATED,
@@ -980,12 +981,19 @@ async fn invite_user(
async fn add_user(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(nu): Json<NewWorkspaceUser>,
Json(mut nu): Json<NewWorkspaceUser>,
) -> Result<(StatusCode, String)> {
require_admin(is_admin, &username)?;
nu.email = nu.email.to_lowercase();
let mut tx = db.begin().await?;
if !VALID_USERNAME.is_match(&nu.username) {
return Err(windmill_common::error::Error::BadRequest(format!(
"Usermame can only contain alphanumeric characters and underscores"
)));
}
sqlx::query!(
"INSERT INTO usr
@@ -1000,8 +1008,23 @@ async fn add_user(
.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,
nu.username,
"all",
)
.execute(&mut tx)
.await?;
tx.commit().await?;
webhook.send_instance_event(InstanceEvent::UserAddedWorkspace {
workspace: w_id.clone(),
email: nu.email.clone(),
});
Ok((
StatusCode::CREATED,
format!("user with email {} added", nu.email),
@@ -1064,6 +1087,7 @@ struct ScriptMetadata {
schema: Option<Schema>,
is_template: bool,
lock: Vec<String>,
kind: String,
}
enum ArchiveImpl {
@@ -1115,6 +1139,7 @@ impl ArchiveImpl {
#[derive(Deserialize)]
struct ArchiveQueryParams {
archive_type: Option<String>,
plain_secret: Option<bool>,
}
#[inline]
@@ -1160,7 +1185,7 @@ async fn tarball_workspace(
authed: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Query(ArchiveQueryParams { archive_type }): Query<ArchiveQueryParams>,
Query(ArchiveQueryParams { archive_type, plain_secret }): Query<ArchiveQueryParams>,
) -> Result<([(headers::HeaderName, String); 2], impl IntoResponse)> {
require_admin(authed.is_admin, &authed.username)?;
@@ -1226,6 +1251,7 @@ async fn tarball_workspace(
description: script.description,
schema: script.schema,
is_template: script.is_template,
kind: script.kind.to_string(),
lock,
};
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
@@ -1296,7 +1322,15 @@ async fn tarball_workspace(
.fetch_all(&db)
.await?;
for var in variables {
let mc = build_crypt(&mut db.begin().await?, &w_id).await?;
for mut var in variables {
if plain_secret.unwrap_or(false) && var.value.is_some() && var.is_secret {
var.value = Some(
mc.decrypt_base64_to_string(var.value.unwrap())
.map_err(|e| Error::InternalErr(e.to_string()))?,
);
}
let var_str = &to_string_without_metadata(&var, false).unwrap();
archive
.write_to_archive(&var_str, &format!("{}.variable.json", var.path))

View File

@@ -41,8 +41,8 @@ pub struct AuditLog {
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn audit_log<'c>(
db: &mut Transaction<'c, Postgres>,
pub async fn audit_log<'c, E: sqlx::Executor<'c, Database = Postgres>>(
db: E,
username: &str,
operation: &str,
action_kind: ActionKind,

View File

@@ -125,7 +125,7 @@ pub enum FlowStatusModule {
},
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum JobResult {
SingleJob(Uuid),
ListJob(Vec<Uuid>),

View File

@@ -6,9 +6,12 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::{collections::HashMap, time::Duration};
use std::{
collections::{BTreeMap, HashMap},
time::Duration,
};
use serde::{self, Deserialize, Serialize};
use serde::{self, Deserialize, Serialize, Serializer};
use crate::{
more_serde::{
@@ -149,9 +152,6 @@ pub struct Suspend {
pub struct FlowModule {
#[serde(default = "default_id")]
pub id: String,
#[serde(default)]
#[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>,
@@ -245,7 +245,7 @@ pub enum FlowModuleValue {
},
RawScript {
#[serde(default)]
#[serde(alias = "input_transform")]
#[serde(alias = "input_transform", serialize_with = "ordered_map")]
input_transforms: HashMap<String, InputTransform>,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -257,6 +257,14 @@ pub enum FlowModuleValue {
Identity,
}
fn ordered_map<S>(value: &HashMap<String, InputTransform>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let ordered: BTreeMap<_, _> = value.iter().collect();
ordered.serialize(serializer)
}
#[derive(Deserialize)]
pub struct ListFlowQuery {
pub path_start: Option<String>,

View File

@@ -0,0 +1 @@

View File

@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::net::SocketAddr;
use std::{net::SocketAddr, sync::Arc};
use error::Error;
@@ -30,7 +30,19 @@ pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 3;
lazy_static::lazy_static! {
pub static ref 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().ok()
.flatten()
.flatten();
pub static ref METRICS_ENABLED: bool = METRICS_ADDR.is_some();
pub static ref BASE_URL: String = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
pub static ref IS_READY: Arc<std::sync::atomic::AtomicBool> = Arc::new(std::sync::atomic::AtomicBool::new(false));
}
#[cfg(feature = "tokio")]
@@ -58,14 +70,31 @@ pub async fn shutdown_signal(tx: tokio::sync::broadcast::Sender<()>) -> anyhow::
pub async fn serve_metrics(
addr: SocketAddr,
mut rx: tokio::sync::broadcast::Receiver<()>,
ready_worker_endpoint: bool,
) -> Result<(), hyper::Error> {
use std::sync::atomic::Ordering;
use axum::{routing::get, Router};
axum::Server::bind(&addr)
.serve(
Router::new()
.route("/metrics", get(metrics))
.into_make_service(),
use hyper::StatusCode;
let router = Router::new().route("/metrics", get(metrics));
let router = if ready_worker_endpoint {
router.route(
"/ready",
get(|| async {
if IS_READY.load(Ordering::Relaxed) {
(StatusCode::OK, "ready")
} else {
(StatusCode::INTERNAL_SERVER_ERROR, "not ready")
}
}),
)
} else {
router
};
axum::Server::bind(&addr)
.serve(router.into_make_service())
.with_graceful_shutdown(async {
rx.recv().await.ok();
println!("Graceful shutdown of metrics");

View File

@@ -17,7 +17,7 @@ pub struct Schedule {
pub edited_by: String,
pub edited_at: DateTime<chrono::Utc>,
pub schedule: String,
pub offset_: i32,
pub timezone: String,
pub enabled: bool,
pub script_path: String,
pub is_flow: bool,

View File

@@ -7,7 +7,7 @@
*/
use std::{
fmt::Display,
fmt::{self, Display},
hash::{Hash, Hasher},
};
@@ -17,7 +17,7 @@ use serde_json::to_string_pretty;
use crate::utils::StripPath;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Hash)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Hash, Eq)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(
feature = "sqlx",
@@ -103,6 +103,18 @@ pub enum ScriptKind {
Approval,
}
impl Display for ScriptKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(match self {
ScriptKind::Trigger => "trigger",
ScriptKind::Failure => "failure",
ScriptKind::Script => "script",
ScriptKind::Approval => "approval",
})?;
Ok(())
}
}
#[derive(Serialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct Script {

View File

@@ -6,6 +6,8 @@
* LICENSE-AGPL for a copy of the license.
*/
pub const SUPERADMIN_SECRET_EMAIL: &str = "superadmin_secret@windmill.dev";
pub fn username_to_permissioned_as(user: &str) -> String {
if user.contains('@') {
user.to_string()

View File

@@ -6,10 +6,10 @@
* LICENSE-AGPL for a copy of the license.
*/
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::Deserialize;
use crate::error::{Error, Result};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const MAX_PER_PAGE: usize = 10000;
pub const DEFAULT_PER_PAGE: usize = 1000;
@@ -19,7 +19,8 @@ pub struct Pagination {
pub page: Option<usize>,
pub per_page: Option<usize>,
}
#[derive(Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct StripPath(pub String);
impl StripPath {
@@ -51,8 +52,8 @@ pub fn paginate(pagination: Pagination) -> (usize, usize) {
}
#[cfg(feature = "sqlx")]
pub async fn now_from_db<'c>(
db: &mut sqlx::Transaction<'c, sqlx::Postgres>,
pub async fn now_from_db<'c, E: sqlx::PgExecutor<'c>>(
db: E,
) -> Result<chrono::DateTime<chrono::Utc>> {
Ok(sqlx::query_scalar!("SELECT now()")
.fetch_one(db)
@@ -118,3 +119,9 @@ pub fn rd_string(len: usize) -> String {
.map(char::from)
.collect()
}
pub fn calculate_hash(s: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(s);
format!("{:x}", hasher.finalize())
}

View File

@@ -25,8 +25,12 @@ serde_json.workspace = true
ulid.workspace = true
uuid.workspace = true
chrono.workspace = true
chrono-tz.workspace = true
hex.workspace = true
reqwest.workspace = true
lazy_static.workspace = true
prometheus.workspace = true
cron.workspace = true
rsmq_async.workspace = true
tokio.workspace = true
futures-core.workspace = true

View File

@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::{collections::HashMap, str::FromStr};
use std::collections::HashMap;
use anyhow::Context;
use reqwest::Client;
@@ -22,8 +22,11 @@ use windmill_common::{
flows::{FlowModule, FlowModuleValue, FlowValue},
scripts::{get_full_hub_script_by_path, HubScript, ScriptHash, ScriptLang},
utils::StripPath,
METRICS_ENABLED,
};
use crate::QueueTransaction;
lazy_static::lazy_static! {
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
@@ -51,32 +54,43 @@ lazy_static::lazy_static! {
const MAX_FREE_EXECS: i32 = 1000;
const MAX_FREE_CONCURRENT_RUNS: i32 = 15;
const RSMQ_MAIN_QUEUE: &'static str = "main_queue";
pub async fn cancel_job<'c>(
pub async fn cancel_job<'c, R: rsmq_async::RsmqConnection + Clone>(
username: &str,
reason: Option<String>,
id: Uuid,
w_id: &str,
mut tx: Transaction<'c, Postgres>,
rsmq: Option<R>,
force_rerun: bool,
) -> error::Result<(Transaction<'c, Postgres>, Option<Uuid>)> {
let job_option = sqlx::query_scalar!(
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, scheduled_for = now(), suspend = 0 WHERE id = $3 \
AND workspace_id = $4 RETURNING id",
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, scheduled_for = now(), suspend = 0, running = CASE WHEN $3 THEN false ELSE running END WHERE id = $4 \
AND workspace_id = $5 RETURNING id",
username,
reason,
force_rerun,
id,
w_id
)
.fetch_optional(&mut tx)
.await?;
if let Some(mut rsmq) = rsmq {
rsmq.change_message_visibility(RSMQ_MAIN_QUEUE, &id.to_string(), 0)
.await
.map_err(|e| anyhow::anyhow!(e))?;
}
let mut jobs = job_option.map(|j| vec![j]).unwrap_or_default();
while !jobs.is_empty() {
let p_job = jobs.pop();
let new_jobs = sqlx::query_scalar!(
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2 WHERE parent_job = $3 \
AND workspace_id = $4 RETURNING id",
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, running = CASE WHEN $3 THEN false ELSE running END WHERE parent_job = $4 \
AND workspace_id = $5 RETURNING id",
username,
reason,
force_rerun,
p_job,
w_id
)
@@ -87,10 +101,11 @@ pub async fn cancel_job<'c>(
Ok((tx, job_option))
}
pub async fn pull(
pub async fn pull<R: rsmq_async::RsmqConnection + Clone>(
db: &Pool<Postgres>,
whitelist_workspaces: Option<Vec<String>>,
blacklist_workspaces: Option<Vec<String>>,
rsmq: Option<R>,
) -> windmill_common::error::Result<Option<QueuedJob>> {
let mut workspaces_filter = String::new();
if let Some(whitelist) = whitelist_workspaces {
@@ -102,6 +117,9 @@ pub async fn pull(
.collect::<Vec<String>>()
.join(",")
));
if let Some(_rsmq) = rsmq {
todo!("REDIS: Implement workspace filters for redis");
}
}
if let Some(blacklist) = blacklist_workspaces {
workspaces_filter.push_str(&format!(
@@ -112,16 +130,50 @@ pub async fn pull(
.collect::<Vec<String>>()
.join(",")
));
if let Some(_rsmq) = rsmq {
todo!("REDIS: Implement workspace filters for redis");
}
}
/* Jobs can be started if they:
* - haven't been started before,
* running = false
* - are flows with a step that needed resume,
* 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>(&format!(
"UPDATE queue
let job: Option<QueuedJob> = if let Some(mut rsmq) = rsmq {
// TODO: REDIS: Race conditions / replace last_ping
let msg = rsmq
.pop_message::<Vec<u8>>(RSMQ_MAIN_QUEUE)
.await
.map_err(|e| anyhow::anyhow!(e))?;
if let Some(msg) = msg {
let uuid = Uuid::from_bytes_le(
msg.message
.try_into()
.map_err(|_| anyhow::anyhow!("Failed to parsed Redis message"))?,
);
sqlx::query_as::<_, QueuedJob>(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
, last_ping = now()
, suspend_until = null
WHERE id = $1
RETURNING *",
)
.bind(uuid)
.fetch_optional(db)
.await?
} else {
None
}
} else {
/* Jobs can be started if they:
* - haven't been started before,
* running = false
* - are flows with a step that needed resume,
* suspend_until is non-null
* and suspend = 0 when the resume messages are received
* or suspend_until <= now() if it has timed out */
sqlx::query_as::<_, QueuedJob>(&format!(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
, last_ping = now()
@@ -139,11 +191,12 @@ pub async fn pull(
LIMIT 1
)
RETURNING *"
))
.fetch_optional(db)
.await?;
))
.fetch_optional(db)
.await?
};
if job.is_some() {
if job.is_some() && *METRICS_ENABLED {
QUEUE_PULL_COUNT.inc();
}
@@ -152,55 +205,24 @@ pub async fn pull(
pub async fn get_result_by_id(
db: Pool<Postgres>,
mut skip_direct: bool,
w_id: String,
flow_id: String,
flow_id: Uuid,
node_id: String,
) -> error::Result<serde_json::Value> {
let mut result_id: Option<JobResult> = None;
let mut parent_id = Uuid::from_str(&flow_id).ok();
while result_id.is_none() && parent_id.is_some() {
if !skip_direct {
let r = sqlx::query!(
"SELECT flow_status, parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT flow_status, parent_job FROM queue WHERE id = $1 AND workspace_id = $2 ",
parent_id.unwrap(),
w_id,
)
.fetch_optional(&db)
.await?;
if let Some(r) = r {
let value = r
.flow_status
.as_ref()
.ok_or_else(|| Error::InternalErr(format!("requiring a flow status value")))?
.to_owned();
parent_id = r.parent_job;
let status_o = serde_json::from_value::<FlowStatus>(value).ok();
result_id = status_o.and_then(|status| {
status
.modules
.iter()
.find(|m| m.id() == node_id)
.and_then(|m| m.job_result())
});
} else {
parent_id = None;
}
} else {
let q_parent = sqlx::query_scalar!(
"SELECT parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT parent_job FROM queue WHERE id = $1 AND workspace_id = $2",
parent_id.unwrap(),
w_id,
)
.fetch_optional(&db)
.await?
.flatten();
parent_id = q_parent;
skip_direct = false
}
}
let job_result: Option<JobResult> = sqlx::query_scalar!(
"SELECT leaf_jobs->$1::text FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3",
node_id,
flow_id,
w_id,
)
.fetch_optional(&db)
.await?
.flatten()
.map(|x| serde_json::from_value(x).ok())
.flatten();
let result_id = windmill_common::utils::not_found_if_none(
result_id,
job_result,
"Flow result by id",
format!("{}, {}", flow_id, node_id),
)?;
@@ -234,24 +256,26 @@ pub async fn get_result_by_id(
}
#[instrument(level = "trace", skip_all)]
pub async fn delete_job(
db: &Pool<Postgres>,
pub async fn delete_job<'c, R: rsmq_async::RsmqConnection + Clone + Send>(
mut tx: QueueTransaction<'c, R>,
w_id: &str,
job_id: Uuid,
) -> windmill_common::error::Result<()> {
QUEUE_DELETE_COUNT.inc();
) -> windmill_common::error::Result<QueueTransaction<'c, R>> {
if *METRICS_ENABLED {
QUEUE_DELETE_COUNT.inc();
}
let job_removed = sqlx::query_scalar!(
"DELETE FROM queue WHERE workspace_id = $1 AND id = $2 RETURNING 1",
w_id,
job_id
)
.fetch_one(db)
.fetch_one(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("Error during deletion of job {job_id}: {e}")))?
.unwrap_or(0)
== 1;
tracing::debug!("Job {job_id} deleted: {job_removed}");
Ok(())
Ok(tx)
}
pub async fn get_queued_job<'c>(
@@ -270,9 +294,9 @@ pub async fn get_queued_job<'c>(
Ok(r)
}
#[instrument(level = "trace", skip_all)]
pub async fn push<'c>(
mut tx: Transaction<'c, Postgres>,
// #[instrument(level = "trace", skip_all)]
pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
mut tx: QueueTransaction<'c, R>,
workspace_id: &str,
job_payload: JobPayload,
args: serde_json::Map<String, serde_json::Value>,
@@ -282,18 +306,18 @@ pub async fn push<'c>(
scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
schedule_path: Option<String>,
parent_job: Option<Uuid>,
root_job: Option<Uuid>,
is_flow_step: bool,
mut same_worker: bool,
pre_run_error: Option<&windmill_common::error::Error>,
visible_to_owner: bool,
) -> Result<(Uuid, Transaction<'c, Postgres>), Error> {
let scheduled_for = scheduled_for_o.unwrap_or_else(chrono::Utc::now);
) -> Result<(Uuid, QueueTransaction<'c, R>), Error> {
let args_json = serde_json::Value::Object(args);
let job_id: Uuid = Ulid::new().into();
if cfg!(feature = "enterprise") {
let premium_workspace =
sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", workspace_id)
let premium_workspace = *CLOUD_HOSTED
&& sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", workspace_id)
.fetch_one(&mut tx)
.await
.map_err(|e| {
@@ -516,7 +540,6 @@ pub async fn push<'c>(
modules.push(FlowModule {
id: format!("{}-v", flow.modules[flow.modules.len() - 1].id),
value: FlowModuleValue::Identity,
input_transforms: HashMap::new(),
stop_after_if: None,
summary: Some(
"Virtual module needed for suspend/sleep when last module".to_string(),
@@ -534,12 +557,13 @@ pub async fn push<'c>(
.unwrap_or_else(|| (None, None));
let flow_status = raw_flow.as_ref().map(FlowStatus::new);
let uuid = sqlx::query_scalar!(
"INSERT INTO queue
(workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for,
script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22) \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, now()), $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23) \
RETURNING id",
workspace_id,
job_id,
@@ -547,7 +571,7 @@ pub async fn push<'c>(
parent_job,
user,
permissioned_as,
scheduled_for,
scheduled_for_o,
script_hash,
script_path.clone(),
raw_code,
@@ -562,13 +586,16 @@ pub async fn push<'c>(
same_worker,
pre_run_error.map(|e| e.to_string()),
email,
visible_to_owner
visible_to_owner,
root_job
)
.fetch_one(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("Could not insert into queue {job_id}: {e}")))?;
// TODO: technically the job isn't queued yet, as the transaction can be rolled back. Should be solved when moving these metrics to the queue abstraction.
QUEUE_PUSH_COUNT.inc();
if *METRICS_ENABLED {
QUEUE_PUSH_COUNT.inc();
}
{
let uuid_string = job_id.to_string();
@@ -603,6 +630,10 @@ pub async fn push<'c>(
.instrument(tracing::info_span!("job_run", email = &email))
.await?;
}
if let Some(ref mut rsmq) = tx.rsmq {
rsmq.send_message(job_id.to_bytes_le().to_vec(), scheduled_for_o);
}
Ok((uuid, tx))
}
@@ -675,6 +706,10 @@ pub struct QueuedJob {
pub suspend: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub root_job: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub leaf_jobs: Option<serde_json::Value>,
}
impl QueuedJob {
@@ -682,7 +717,7 @@ impl QueuedJob {
self.script_path
.as_ref()
.map(String::as_str)
.unwrap_or("NO_FLOW_PATH")
.unwrap_or("tmp/main")
}
}

View File

@@ -7,6 +7,8 @@
*/
mod jobs;
mod queue_transaction;
pub mod schedule;
pub use jobs::*;
pub use queue_transaction::*;

View File

@@ -0,0 +1,161 @@
use std::fmt::Debug;
use futures_core::{future::BoxFuture, stream::BoxStream};
use rsmq_async::{RedisBytes, RsmqConnection};
use sqlx::{Postgres, Transaction};
pub enum RedisOp {
SendMessage(RedisBytes, Option<chrono::DateTime<chrono::Utc>>),
DeleteMessage(String),
}
impl RedisOp {
pub async fn apply<R: RsmqConnection>(self, rsmq: &mut R) -> Result<(), rsmq_async::RsmqError> {
match self {
RedisOp::SendMessage(bytes, time) => {
rsmq.send_message(
"main_queue",
bytes,
time.map(|t| (t - chrono::Utc::now()).num_seconds())
.and_then(|e| e.try_into().ok()),
)
.await?;
}
RedisOp::DeleteMessage(id) => {
rsmq.delete_message("main_queue", &id).await?;
}
};
Ok(())
}
}
pub struct RedisTransaction<R: RsmqConnection> {
rsmq: R,
queued_ops: Vec<RedisOp>,
}
impl<R: RsmqConnection> From<R> for RedisTransaction<R> {
fn from(value: R) -> Self {
Self { rsmq: value, queued_ops: Vec::new() }
}
}
impl<R: RsmqConnection> RedisTransaction<R> {
pub async fn commit(self) -> Result<(), rsmq_async::RsmqError> {
let mut rsmq = self.rsmq;
for op in self.queued_ops {
op.apply(&mut rsmq).await?;
}
Ok(())
}
pub fn send_message<E: Into<RedisBytes>>(
&mut self,
bytes: E,
delay_until: Option<chrono::DateTime<chrono::Utc>>,
) {
self.queued_ops
.push(RedisOp::SendMessage(bytes.into(), delay_until))
}
pub fn delete_message(&mut self, id: String) {
self.queued_ops.push(RedisOp::DeleteMessage(id))
}
}
pub struct QueueTransaction<'c, R: RsmqConnection> {
pub rsmq: Option<RedisTransaction<R>>,
transaction: Transaction<'c, Postgres>,
}
impl<'c, R: RsmqConnection> From<(Option<R>, Transaction<'c, Postgres>)>
for QueueTransaction<'c, R>
{
fn from(value: (Option<R>, Transaction<'c, Postgres>)) -> Self {
Self { rsmq: value.0.map(|e| e.into()), transaction: value.1 }
}
}
impl<'c, R: RsmqConnection> Debug for QueueTransaction<'c, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("QueueTransaction")
.field("rsmq", &self.rsmq.as_ref().map(|_| ())) // do not require R: Debug
.field("transaction", &self.transaction)
.finish()
}
}
impl<'c, R: RsmqConnection> QueueTransaction<'c, R> {
pub async fn commit(self) -> Result<(), windmill_common::error::Error> {
self.transaction.commit().await?;
if let Some(rsmq) = self.rsmq {
rsmq.commit().await.map_err(|e| anyhow::anyhow!(e))?;
}
Ok(())
}
pub fn transaction_mut<'a>(&'a mut self) -> &'a mut Transaction<'c, Postgres> {
&mut self.transaction
}
}
impl<'c, 'b, R: RsmqConnection + Send> sqlx::Executor<'b> for &'b mut QueueTransaction<'c, R> {
type Database = Postgres;
fn fetch_many<'e, 'q: 'e, E: 'q>(
self,
query: E,
) -> BoxStream<
'e,
Result<
sqlx::Either<
<Self::Database as sqlx::Database>::QueryResult,
<Self::Database as sqlx::Database>::Row,
>,
sqlx::Error,
>,
>
where
'b: 'e,
E: sqlx::Execute<'q, Self::Database>,
{
self.transaction.fetch_many(query)
}
fn fetch_optional<'e, 'q: 'e, E: 'q>(
self,
query: E,
) -> BoxFuture<'e, Result<Option<<Self::Database as sqlx::Database>::Row>, sqlx::Error>>
where
'b: 'e,
E: sqlx::Execute<'q, Self::Database>,
{
self.transaction.fetch_optional(query)
}
fn prepare_with<'e, 'q: 'e>(
self,
sql: &'q str,
parameters: &'e [<Self::Database as sqlx::Database>::TypeInfo],
) -> BoxFuture<
'e,
Result<<Self::Database as sqlx::database::HasStatement<'q>>::Statement, sqlx::Error>,
>
where
'b: 'e,
{
self.transaction.prepare_with(sql, parameters)
}
fn describe<'e, 'q: 'e>(
self,
sql: &'q str,
) -> BoxFuture<'e, Result<sqlx::Describe<Self::Database>, sqlx::Error>>
where
'b: 'e,
{
self.transaction.describe(sql)
}
}

View File

@@ -6,33 +6,39 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::str::FromStr;
use chrono::Duration;
use crate::{push, JobPayload};
use sqlx::{query_scalar, Postgres, Transaction};
use std::str::FromStr;
use windmill_common::{
error::{self, Result},
schedule::Schedule,
users::username_to_permissioned_as,
utils::{now_from_db, StripPath},
};
use crate::{QueueTransaction};
use crate::{push, JobPayload};
pub async fn push_scheduled_job<'c>(
mut tx: Transaction<'c, Postgres>,
pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
mut tx: QueueTransaction<'c, R>,
schedule: Schedule,
) -> Result<Transaction<'c, Postgres>> {
) -> Result<QueueTransaction<'c, R>> {
let sched = cron::Schedule::from_str(&schedule.schedule)
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
let offset = Duration::minutes(schedule.offset_.into());
let now = now_from_db(&mut tx).await?;
let tz = chrono_tz::Tz::from_str(&schedule.timezone)
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
let now = now_from_db(&mut tx).await?.with_timezone(&tz);
let next = sched
.after(&(now - offset + Duration::seconds(1)))
.after(&now)
.next()
.expect("a schedule should have a next event")
+ offset;
.expect("a schedule should have a next event");
// println!("next event ({:?}): {}", tz, next);
// println!("next event(UTC): {}", next.with_timezone(&chrono::Utc));
// Scheduled events must be stored in the database in UTC
let next = next.with_timezone(&chrono::Utc);
let already_exists: bool = query_scalar!(
"SELECT EXISTS (SELECT 1 FROM queue WHERE workspace_id = $1 AND schedule_path = $2 AND scheduled_for = $3)",
@@ -65,7 +71,7 @@ pub async fn push_scheduled_job<'c>(
} else {
JobPayload::ScriptHash {
hash: windmill_common::get_latest_hash_for_path(
&mut tx,
tx.transaction_mut(),
&schedule.workspace_id,
&schedule.script_path,
)
@@ -74,7 +80,15 @@ pub async fn push_scheduled_job<'c>(
}
};
let (_, mut tx) = push(
sqlx::query!(
"UPDATE schedule SET error = NULL WHERE workspace_id = $1 AND path = $2",
&schedule.workspace_id,
&schedule.path
)
.execute(&mut tx)
.await?;
let (_, tx) = push(
tx,
&schedule.workspace_id,
payload,
@@ -85,20 +99,14 @@ pub async fn push_scheduled_job<'c>(
Some(next),
Some(schedule.path.clone()),
None,
None,
false,
false,
None,
true,
)
.await?;
sqlx::query!(
"UPDATE schedule SET error = NULL WHERE workspace_id = $1 AND path = $2",
&schedule.workspace_id,
&schedule.path
)
.execute(&mut tx)
.await?;
Ok(tx)
Ok(tx) // TODO: Bubble up pushed UUID from here
}
pub async fn get_schedule_opt<'c>(

View File

@@ -48,3 +48,5 @@ deno_core.workspace = true
const_format.workspace = true
git-version.workspace = true
dyn-iter.workspace = true
once_cell.workspace = true
rsmq_async.workspace = true

View File

@@ -1,135 +0,0 @@
name: "deno run script"
mode: ONCE
hostname: "deno"
log_level: ERROR
rlimit_as: 16000
rlimit_cpu: 1000
rlimit_fsize: 1000
rlimit_nofile: 10000
cwd: "/tmp"
clone_newnet: false
clone_newuser: {CLONE_NEWUSER}
keep_caps: false
keep_env: true
mount_proc: true
mount {
src: "/bin"
dst: "/bin"
is_bind: true
}
mount {
src: "/lib"
dst: "/lib"
is_bind: true
}
mount {
src: "/lib64"
dst: "/lib64"
is_bind: true
}
mount {
src: "/usr"
dst: "/usr"
is_bind: true
}
mount {
src: "/dev/null"
dst: "/dev/null"
is_bind: true
rw: true
}
mount {
dst: "/tmp"
fstype: "tmpfs"
rw: true
options: "size=500000000"
}
mount {
src: "{JOB_DIR}/inner.ts"
dst: "/tmp/inner.ts"
is_bind: true
}
mount {
src: "{JOB_DIR}/main.ts"
dst: "/tmp/main.ts"
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"
is_bind: true
}
mount {
src: "{JOB_DIR}/args.json"
dst: "/tmp/args.json"
is_bind: true
}
mount {
src: "{JOB_DIR}/result.json"
dst: "/tmp/result.json"
is_bind: true
rw: true
}
mount {
src: "/etc"
dst: "/etc"
is_bind: true
}
mount {
src: "/dev/random"
dst: "/dev/random"
is_bind: true
}
mount {
src: "/dev/urandom"
dst: "/dev/urandom"
is_bind: true
}
mount {
src: "{CACHE_DIR}"
dst: "/tmp/.cache/deno"
is_bind: true
rw: true
mandatory: false
}
{SHARED_MOUNT}
iface_no_lo: true
envar: "DENO_DIR=/tmp/.cache/deno"
envar: "NO_COLOR=true"
envar: "HOME=/tmp"

View File

@@ -66,8 +66,8 @@ mount {
}
mount {
src: "{JOB_DIR}/inner.py"
dst: "/tmp/inner.py"
src: "{JOB_DIR}/{MAIN}.py"
dst: "/tmp/{MAIN}.py"
is_bind: true
}
@@ -79,8 +79,8 @@ mount {
}
mount {
src: "{JOB_DIR}/main.py"
dst: "/tmp/main.py"
src: "{JOB_DIR}/wrapper.py"
dst: "/tmp/wrapper.py"
is_bind: true
}

View File

@@ -6,23 +6,30 @@
* LICENSE-AGPL for a copy of the license.
*/
use sqlx::{Pool, Postgres, Transaction};
use sqlx::{Pool, Postgres};
use tracing::instrument;
use uuid::Uuid;
use windmill_common::{error::Error, flow_status::FlowStatusModule, schedule::Schedule};
use windmill_queue::{delete_job, schedule::get_schedule_opt, JobKind, QueuedJob};
use windmill_common::{
error::Error, flow_status::FlowStatusModule, schedule::Schedule, METRICS_ENABLED,
};
use windmill_queue::{
delete_job, schedule::get_schedule_opt, JobKind, QueueTransaction, QueuedJob, CLOUD_HOSTED,
};
#[instrument(level = "trace", skip_all)]
pub async fn add_completed_job_error(
pub async fn add_completed_job_error<R: rsmq_async::RsmqConnection + Clone + Send>(
db: &Pool<Postgres>,
queued_job: &QueuedJob,
logs: String,
e: serde_json::Value,
metrics: Option<crate::worker::Metrics>,
rsmq: Option<R>,
) -> Result<serde_json::Value, Error> {
metrics.map(|m| m.worker_execution_failed.inc());
if *METRICS_ENABLED {
metrics.map(|m| m.worker_execution_failed.inc());
}
let result = serde_json::json!({ "error": e });
let _ = add_completed_job(db, &queued_job, false, false, result.clone(), logs).await?;
let _ = add_completed_job(db, &queued_job, false, false, result.clone(), logs, rsmq).await?;
Ok(result)
}
@@ -45,13 +52,14 @@ fn flatten_jobs(modules: Vec<FlowStatusModule>) -> Vec<Uuid> {
}
#[instrument(level = "trace", skip_all)]
pub async fn add_completed_job(
pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
db: &Pool<Postgres>,
queued_job: &QueuedJob,
success: bool,
skipped: bool,
result: serde_json::Value,
logs: String,
rsmq: Option<R>,
) -> Result<Uuid, Error> {
let duration =
if queued_job.job_kind == JobKind::Flow || queued_job.job_kind == JobKind::FlowPreview {
@@ -83,7 +91,7 @@ pub async fn add_completed_job(
.ok()
.flatten()
.flatten();
let mut tx = db.begin().await?;
let mut tx: QueueTransaction<'_, R> = (rsmq, db.begin().await?).into();
let job_id = queued_job.id.clone();
sqlx::query!(
"INSERT INTO completed_job AS cj
@@ -153,7 +161,7 @@ pub async fn add_completed_job(
.execute(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("Could not add completed job {job_id}: {e}")))?;
let _ = delete_job(db, &queued_job.workspace_id, job_id).await?;
tx = delete_job(tx, &queued_job.workspace_id, job_id).await?;
if !queued_job.is_flow_step
&& queued_job.job_kind != JobKind::Flow
&& queued_job.job_kind != JobKind::FlowPreview
@@ -175,8 +183,8 @@ pub async fn add_completed_job(
let additional_usage = duration.unwrap() as i32 / 1000;
let w_id = &queued_job.workspace_id;
let premium_workspace =
sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", w_id)
let premium_workspace = *CLOUD_HOSTED
&& sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", w_id)
.fetch_one(db)
.await
.map_err(|e| Error::InternalErr(format!("fetching if {w_id} is premium: {e}")))?;
@@ -208,21 +216,24 @@ pub async fn add_completed_job(
}
#[instrument(level = "trace", skip_all)]
pub async fn schedule_again_if_scheduled<'c>(
mut tx: Transaction<'c, Postgres>,
pub async fn schedule_again_if_scheduled<'c, R: rsmq_async::RsmqConnection + Clone + Send + 'c>(
mut tx: QueueTransaction<'c, R>,
db: &Pool<Postgres>,
schedule_path: &str,
script_path: &str,
w_id: &str,
) -> windmill_common::error::Result<Transaction<'c, Postgres>> {
let schedule = get_schedule_opt(&mut tx, w_id, schedule_path)
.await?
.ok_or_else(|| {
Error::InternalErr(format!(
"Could not find schedule {:?} for workspace {}",
schedule_path, w_id
))
})?;
) -> windmill_common::error::Result<QueueTransaction<'c, R>> {
let schedule = get_schedule_opt(tx.transaction_mut(), w_id, schedule_path).await?;
if schedule.is_none() {
tracing::error!(
"Schedule {schedule_path} in {w_id} not found. Impossible to schedule again"
);
return Ok(tx);
}
let schedule = schedule.unwrap();
if schedule.enabled && script_path == schedule.script_path {
let res = windmill_queue::schedule::push_scheduled_job(
tx,
@@ -232,7 +243,7 @@ pub async fn schedule_again_if_scheduled<'c>(
edited_by: schedule.edited_by,
edited_at: schedule.edited_at,
schedule: schedule.schedule,
offset_: schedule.offset_,
timezone: schedule.timezone,
enabled: schedule.enabled,
script_path: schedule.script_path,
is_flow: schedule.is_flow,

View File

@@ -6,9 +6,11 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::collections::HashMap;
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use deno_core::{op, serde_v8, v8, v8::IsolateHandle, Extension, JsRuntime, RuntimeOptions};
use deno_core::{
op, serde_v8, v8, v8::IsolateHandle, Extension, JsRuntime, OpState, RuntimeOptions,
};
use itertools::Itertools;
use lazy_static::lazy_static;
use regex::Regex;
@@ -17,10 +19,7 @@ use tokio::{sync::oneshot, time::timeout};
use uuid::Uuid;
use windmill_common::{error::Error, flow_status::JobResult};
pub struct EvalCreds {
pub workspace: String,
pub token: String,
}
use crate::AuthedClient;
#[derive(Debug, Clone)]
pub struct IdContext {
@@ -29,22 +28,23 @@ pub struct IdContext {
pub previous_id: String,
}
pub struct OptAuthedClient(Option<AuthedClient>);
pub async fn eval_timeout(
expr: String,
env: Vec<(String, serde_json::Value)>,
creds: Option<EvalCreds>,
authed_client: Option<&AuthedClient>,
by_id: Option<IdContext>,
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();
let has_client = authed_client.is_some();
let authed_client = authed_client.cloned();
timeout(
std::time::Duration::from_millis(2000),
std::time::Duration::from_millis(10000),
tokio::task::spawn_blocking(move || {
let mut ops = vec![];
if creds.is_some() {
if authed_client.is_some() {
ops.extend([
// An op for summing an array of numbers
// The op-layer automatically deserializes inputs
@@ -54,12 +54,12 @@ pub async fn eval_timeout(
])
}
if by_id.is_some() {
if by_id.is_some() && authed_client.is_some() {
ops.push(op_get_result::decl());
ops.push(op_get_id::decl());
}
let ext = Extension::builder().ops(ops).build();
let ext = Extension::builder("js_eval").ops(ops).build();
// Use our snapshot to provision our new runtime
let options = RuntimeOptions {
extensions: vec![ext],
@@ -68,6 +68,11 @@ pub async fn eval_timeout(
};
let mut js_runtime = JsRuntime::new(options);
{
let op_state = js_runtime.op_state();
let mut op_state = op_state.borrow_mut();
op_state.put(OptAuthedClient(authed_client.clone()));
}
sender
.send(js_runtime.v8_isolate().thread_safe_handle())
@@ -86,14 +91,7 @@ pub async fn eval_timeout(
let expr = replace_with_await_result(expr);
let r = runtime.block_on(eval(
&mut js_runtime,
&expr,
env,
creds,
by_id,
&base_internal_url,
))?;
let r = runtime.block_on(eval(&mut js_runtime, &expr, env, by_id, has_client))?;
Ok(r) as anyhow::Result<Value>
}),
@@ -104,7 +102,7 @@ pub async fn eval_timeout(
isolate.terminate_execution();
};
Error::ExecutionErr(format!(
"The expression of evaluation `{expr2}` took too long to execute (>2000ms)"
"The expression of evaluation `{expr2}` took too long to execute (>10000ms)"
))
})??
}
@@ -145,24 +143,30 @@ fn add_closing_bracket(s: &str) -> String {
s
}
const SPLIT_PAT: &str = ";\n";
const SPLIT_PAT: &str = ";";
async fn eval(
context: &mut JsRuntime,
expr: &str,
env: Vec<(String, serde_json::Value)>,
creds: Option<EvalCreds>,
by_id: Option<IdContext>,
base_internal_url: &str,
has_client: bool,
) -> anyhow::Result<serde_json::Value> {
let expr = expr.trim();
let expr = format!(
"{}\nreturn {};",
expr.split(SPLIT_PAT)
.take(expr.split(SPLIT_PAT).count() - 1)
.join("\n"),
expr.split(SPLIT_PAT).last().unwrap_or_else(|| "")
);
let (api_code, by_id_code) = if let Some(EvalCreds { workspace, token }) = creds {
let exprs = expr
.trim()
.split(SPLIT_PAT)
.map(|x| x.trim())
.filter(|x| !x.is_empty())
.collect::<Vec<&str>>();
let expr = if exprs.is_empty() {
"return undefined;".to_string()
} else {
format!(
"{};\n return {};",
exprs.iter().take(exprs.len() - 1).join(";\n"),
exprs.last().unwrap()
)
};
let (api_code, by_id_code) = if has_client {
let by_id_code = if let Some(by_id) = by_id {
format!(
r#"
@@ -179,12 +183,12 @@ async function result_by_id(node_id) {{
}}
}} else {{
let flow_job_id = "{}";
return await Deno.core.opAsync("op_get_id", [workspace, flow_job_id, token, base_url, node_id]);
return await Deno.core.opAsync("op_get_id", [ flow_job_id, node_id]);
}}
}}
async function get_result(id) {{
return await Deno.core.opAsync("op_get_result", [workspace, id, token, base_url]);
return await Deno.core.opAsync("op_get_result", [id]);
}}
const results = new Proxy({{}}, {{
get: function(target, name, receiver) {{
@@ -198,12 +202,12 @@ const results = new Proxy({{}}, {{
.into_iter()
.map(|(k, v)| {
let v_str = match v {
JobResult::SingleJob(x) => x.to_string(),
JobResult::SingleJob(x) => format!("\"{x}\""),
JobResult::ListJob(x) => {
format!("[{}]", x.iter().map(|x| x.to_string()).join(","))
format!("[{}]", x.iter().map(|x| format!("\"{x}\"")).join(","))
}
};
format!("\"{k}\": \"{v_str}\"")
format!("\"{k}\": {v_str}")
})
.join(","),
by_id.previous_id,
@@ -215,17 +219,13 @@ const results = new Proxy({{}}, {{
let api_code = format!(
r#"
let workspace = "{workspace}";
let base_url = "{}";
let token = "{token}";
async function variable(path) {{
return await Deno.core.opAsync("op_variable", [workspace, path, token, base_url]);
return await Deno.core.opAsync("op_variable", [path]);
}}
async function resource(path) {{
return await Deno.core.opAsync("op_resource", [workspace, path, token, base_url]);
return await Deno.core.opAsync("op_resource", [path]);
}}
"#,
base_internal_url,
);
(api_code, by_id_code)
} else {
@@ -252,7 +252,7 @@ async function resource(path) {{
.join(""),
);
tracing::debug!("{}", code);
let global = context.execute_script("<anon>", &code)?;
let global = context.execute_script("<anon>", code)?;
let global = context.resolve_value(global).await?;
let scope = &mut context.handle_scope();
@@ -274,60 +274,83 @@ async function resource(path) {{
// TODO: Can we a) share the api configuration here somehow or b) just implement this natively in deno, via the deno client?
#[op]
async fn op_variable(args: Vec<String>) -> Result<String, anyhow::Error> {
let workspace = &args[0];
let path = &args[1];
let token = &args[2];
let base_url = &args[3];
let client = windmill_api_client::create_client(base_url, token.clone());
let result = client.get_variable(workspace, path, None).await?;
Ok(result.into_inner().value.unwrap_or_else(|| "".to_owned()))
async fn op_variable(
op_state: Rc<RefCell<OpState>>,
args: Vec<String>,
) -> Result<String, anyhow::Error> {
let path = &args[0];
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
if let Some(client) = client {
let result = client
.get_client()
.get_variable(&client.workspace, path, None)
.await?;
Ok(result.into_inner().value.unwrap_or_else(|| "".to_owned()))
} else {
anyhow::bail!("No client found in op state");
}
}
#[op]
async fn op_get_result(args: Vec<String>) -> Result<serde_json::Value, anyhow::Error> {
let workspace = &args[0];
let id = &args[1];
let token = &args[2];
let base_url = &args[3];
let client = windmill_api_client::create_client(base_url, token.clone());
let result = client
.get_completed_job(workspace, &id.parse()?)
.await?
.result
.clone();
Ok(serde_json::json!(result))
async fn op_get_result(
op_state: Rc<RefCell<OpState>>,
args: Vec<String>,
) -> Result<serde_json::Value, anyhow::Error> {
let id = &args[0];
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
if let Some(client) = client {
let result = client
.get_client()
.get_completed_job_result(&client.workspace, &id.parse()?)
.await?
.clone();
Ok(serde_json::json!(result))
} else {
anyhow::bail!("No client found in op state");
}
}
#[op]
async fn op_get_id(args: Vec<String>) -> Result<Option<serde_json::Value>, anyhow::Error> {
let workspace = &args[0];
let flow_job_id = &args[1];
let token = &args[2];
let base_url = &args[3];
let node_id = &args[4];
async fn op_get_id(
op_state: Rc<RefCell<OpState>>,
args: Vec<String>,
) -> Result<Option<serde_json::Value>, anyhow::Error> {
let flow_job_id = &args[0];
let node_id = &args[1];
let client = windmill_api_client::create_client(base_url, token.clone());
let result = client
.result_by_id(workspace, flow_job_id, node_id, Some(true))
.await
.map_or(None, |e| Some(e.into_inner()));
Ok(result)
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
if let Some(client) = client {
let result = client
.get_client()
.result_by_id(&client.workspace, flow_job_id, node_id)
.await
.map_or(None, |e| Some(e.into_inner()));
Ok(result)
} else {
anyhow::bail!("No client found in op state");
}
}
#[op]
async fn op_resource(args: Vec<String>) -> Result<serde_json::Value, anyhow::Error> {
let workspace = &args[0];
let path = &args[1];
let token = &args[2];
let base_url = &args[3];
let client = windmill_api_client::create_client(base_url, token.clone());
let result = client.get_resource(workspace, path).await?;
Ok(result
.into_inner()
.value
.unwrap_or_else(|| serde_json::json!({})))
async fn op_resource(
op_state: Rc<RefCell<OpState>>,
args: Vec<String>,
) -> Result<serde_json::Value, anyhow::Error> {
let path = &args[0];
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
if let Some(client) = client {
let result = client
.get_client()
.get_resource(&client.workspace, path)
.await?;
Ok(result
.into_inner()
.value
.unwrap_or_else(|| serde_json::json!({})))
} else {
anyhow::bail!("No client found in op state");
}
}
#[cfg(test)]
@@ -347,7 +370,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, String::new().as_str()).await?;
let res = eval(&mut runtime, code, env, None, false).await?;
assert_eq!(res, json!(4));
Ok(())
}
@@ -360,7 +383,7 @@ mod tests {
multiline template`";
let mut runtime = JsRuntime::new(RuntimeOptions::default());
let res = eval(&mut runtime, code, env, None, None, String::new().as_str()).await?;
let res = eval(&mut runtime, code, env, None, false).await?;
assert_eq!(res, json!("my 5\nmultiline template"));
Ok(())
}
@@ -373,7 +396,7 @@ multiline template`";
];
let code = r#"params.test"#;
let res = eval_timeout(code.to_string(), env, None, None, String::new().as_str()).await?;
let res = eval_timeout(code.to_string(), env, None, None).await?;
assert_eq!(res, json!(2));
Ok(())
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +0,0 @@
{
"type": "RANDOM",
"actions": [
{
"weight": 1,
"action": {
"type": "RANDOM",
"actions": [
{
"weight": 1,
"action": {
"type": "PREVIEW_SCRIPT",
"workspace": "demo",
"language": "deno",
"args": {},
"content": "export async function main() { return \"Hello World\"; }"
}
},
{
"weight": 1,
"action": {
"type": "PREVIEW_SCRIPT",
"workspace": "demo",
"language": "python3",
"args": {},
"content": "def main(): return \"Hello World\";"
}
},
{
"weight": 1,
"action": {
"type": "PREVIEW_SCRIPT",
"workspace": "demo",
"language": "go",
"args": {},
"content": "func main() { return \"Hello World\" }"
}
}
]
}
},
{
"weight": 0.5,
"action": {
"type": "PREVIEW_SCRIPT",
"workspace": "demo",
"language": "deno",
"args": {},
"content": "import { delay } from \"https://deno.land/std@0.131.0/async/delay.ts\"; export async function main() { await delay(1000); return \"Hello World\"; }"
}
}
]
}

7
benchmarks/identity.json Normal file
View File

@@ -0,0 +1,7 @@
{
"type": "PREVIEW_SCRIPT",
"workspace": "demo",
"language": "deno",
"args": {},
"content": "export async function main() { return \"Hello World\"; }"
}

View File

@@ -27,60 +27,60 @@ await new Command()
"The number of workers to run at once.",
{
default: 1,
}
},
)
.option(
"-s --seconds <seconds:number>",
"How long to run the benchmark for (in seconds).",
{
default: 30,
}
},
)
.option("--max <max:number>", "Maximum number of operations performed.")
.option("-e --email <email:string>", "The email to use to login.")
.option("-p --password <password:string>", "The password to use to login.")
.env(
"WM_TOKEN=<token:string>",
"The token to use when talking to the API server. Preferred over manual login."
"The token to use when talking to the API server. Preferred over manual login.",
)
.option(
"-t --token <token:string>",
"The token to use when talking to the API server. Preferred over manual login."
"The token to use when talking to the API server. Preferred over manual login.",
)
.env(
"WM_WORKSPACE=<workspace:string>",
"The workspace to spawn scripts from."
"The workspace to spawn scripts from.",
)
.option(
"-w --workspace <workspace:string>",
"The workspace to spawn scripts from.",
{ default: "starter" }
{ default: "starter" },
)
.option("-m --metrics <metrics:string>", "The url to scrape metrics from.", {
default: "http://localhost:8001/metrics",
})
.option(
"--export-json <export_json:string>",
"If set, exports will be into a JSON file."
"If set, exports will be into a JSON file.",
)
.option(
"--export-csv <export_csv:string>",
"If set, exports will be into a csv file."
"If set, exports will be into a csv file.",
)
.option(
"--export-histograms [histograms...:string]",
"Mark metrics (without label) that are reported as histograms to export."
"Mark metrics (without label) that are reported as histograms to export.",
)
.option(
"--export-simple [simple...:string]",
"Mark metrics (without label) that are reported as simple values."
"Mark metrics (without label) that are reported as simple values.",
)
.option(
"--maximum-throughput <maximum_throughput:number>",
"Maximum number of jobs/flows to start in one second.",
{
default: Infinity,
}
},
)
.option("--use-flows", "Run flows instead of jobs.")
.option("--custom <custom_path:string>", "Use custom actions during bench")
@@ -89,11 +89,11 @@ await new Command()
"The maximum time in ms to wait for jobs to complete.",
{
default: 90000,
}
},
)
.option(
"--continous",
"Run the benchmark forever. This effectively disables metric collection & exports. No zombie jobs will be tracked."
"Run the benchmark forever. This effectively disables metric collection & exports. No zombie jobs will be tracked.",
)
.option(
"--histogram-buckets [buckets...:string]",
@@ -114,7 +114,7 @@ await new Command()
"0.01",
"0.005",
],
}
},
)
.action(
async ({
@@ -162,7 +162,7 @@ await new Command()
new URL("./scraper.ts", import.meta.url).href,
{
type: "module",
}
},
);
metrics_worker.postMessage({
@@ -192,8 +192,8 @@ await new Command()
zombieTimeout,
},
null,
4
)
4,
),
);
const config = {
@@ -245,19 +245,22 @@ await new Command()
const updateState = setInterval(async () => {
const elapsed = start ? Math.ceil((Date.now() - start) / 1000) : 0;
const sum = jobsSent.reduce((a, b) => a + b, 0);
const queue_length = (
await windmill.JobService.listQueue({
workspace: config.workspace_id,
})
).length;
const queue_length = (await (await fetch(
host + "/api/w/" + config.workspace_id + "/jobs/queue/count",
{ headers: { ["Authorization"]: "Bearer " + config.token } },
)).json()).database_length;
await Deno.stdout.write(
enc(
`elapsed: ${elapsed}/${seconds} | jobs sent: ${JSON.stringify(
jobsSent
)} (sum: ${sum} thr: ${(sum / elapsed).toFixed(
2
)}) | queue: ${queue_length} \r`
)
`elapsed: ${elapsed}/${seconds} | jobs sent: ${
JSON.stringify(
jobsSent,
)
} (sum: ${sum} thr: ${
(sum / elapsed).toFixed(
2,
)
}) | queue: ${queue_length} \r`,
),
);
}, 100);
@@ -284,7 +287,7 @@ await new Command()
const sum = jobsSent.reduce((a, b) => a + b, 0);
await Deno.stdout.write(
enc(" ".padStart(30) + `\rduration: ${seconds} | jobs sent: ${sum}\n`)
enc(" ".padStart(30) + `\rduration: ${seconds} | jobs sent: ${sum}\n`),
);
const shutdown_start = Date.now();
@@ -302,7 +305,7 @@ await new Command()
};
worker.addEventListener("message", l);
worker.postMessage(
Number.isSafeInteger(zombieTimeout) ? zombieTimeout : 90000
Number.isSafeInteger(zombieTimeout) ? zombieTimeout : 90000,
);
});
@@ -318,11 +321,10 @@ await new Command()
console.log("incorrect results: ", incorrect_results);
console.log(
"queue length:",
(
await windmill.JobService.listQueue({
workspace: config.workspace_id,
})
).length
(await (await fetch(
host + "/api/w/" + config.workspace_id + "/jobs/queue/count",
{ headers: { ["Authorization"]: "Bearer " + config.token } },
)).json()).database_length,
);
metrics_worker!.postMessage("stop");
@@ -346,7 +348,7 @@ await new Command()
const value = values[i]!;
const mean = value.reduce((acc, e) => acc + e, 0) / values.length;
const stdev = Math.sqrt(
value.reduce((acc, e) => acc + (e - mean) ** 2) / values.length
value.reduce((acc, e) => acc + (e - mean) ** 2) / values.length,
);
obj[name] = { mean, stdev };
}
@@ -374,6 +376,6 @@ await new Command()
f.close();
}
console.log("done");
}
},
)
.parse();

View File

@@ -13,6 +13,8 @@ const promise = new Promise<{
continous: boolean;
max_per_worker: number;
custom: Action | undefined;
server: string;
token: string;
}>((resolve, _reject) => {
self.onmessage = (evt) => {
const sharedConfig = evt.data;
@@ -24,6 +26,8 @@ const promise = new Promise<{
continous: sharedConfig.continous,
max_per_worker: sharedConfig.max_per_worker,
custom: sharedConfig.custom,
server: sharedConfig.server,
token: sharedConfig.token,
};
self.name = "Worker " + sharedConfig.i;
resolve(config);
@@ -46,19 +50,20 @@ const updateStatusInterval = setInterval(() => {
}, 100);
while (cont) {
const queue_length = (
await windmill.JobService.listQueue({ workspace: config.workspace_id })
).length;
if (queue_length > 500) {
const queue_length = (await (await fetch(
config.server + "/api/w/" + config.workspace_id + "/jobs/queue/count",
{ headers: { ["Authorization"]: "Bearer " + config.token } },
)).json()).database_length;
if (queue_length > 2500) {
console.log(
`queue length: ${queue_length} > 500. waiting... `
`queue length: ${queue_length} > 2500. waiting... `,
);
await sleep(0.5);
continue;
}
if (
(total_spawned * 1000) / (Date.now() - start_time) >
config.per_worker_throughput
config.per_worker_throughput
) {
console.log("at maximum throughput. waiting...");
await sleep(0.1);
@@ -138,13 +143,13 @@ while (outstanding.length > 0 && Date.now() < end_time) {
await Deno.stdout.write(
enc(
`uuid: ${uuid}, queue length: ${
(
await windmill.JobService.listQueue({
workspace: config.workspace_id,
})
).length
} \r`
)
(await (await fetch(
config.server + "/api/w/" + config.workspace_id +
"/jobs/queue/count",
{ headers: { ["Authorization"]: "Bearer " + config.token } },
)).json()).database_length
} \r`,
),
);
} else if (!config.useFlows) {
r = r as api.CompletedJob;
@@ -156,7 +161,7 @@ while (outstanding.length > 0 && Date.now() < end_time) {
" != " +
uuid +
"job: \n" +
JSON.stringify(r, null, 2)
JSON.stringify(r, null, 2),
);
incorrect_results++;
}

View File

@@ -1,12 +1,22 @@
import { Any, model, property } from "./decoverto.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { Any, decoverto, model, property } from "./decoverto.ts";
import {
AppService,
AppWithLastVersion,
colors,
Command,
ListableApp,
microdiff,
Policy,
Table,
} from "./deps.ts";
import { Difference, PushDiffs, Resource, setValueByPath } from "./types.ts";
import {
Difference,
GlobalOptions,
PushDiffs,
Resource,
setValueByPath,
} from "./types.ts";
@model()
export class AppFile implements Resource, PushDiffs {
@@ -17,7 +27,6 @@ export class AppFile implements Resource, PushDiffs {
@property(Any)
policy: Policy;
constructor(value: string, summary: string, policy: Policy) {
this.value = value;
this.summary = summary;
@@ -26,16 +35,20 @@ export class AppFile implements Resource, PushDiffs {
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
diffs: Difference[]
): Promise<void> {
if (await AppService.existsApp({ workspace, path: remotePath })) {
let app: AppWithLastVersion | undefined = undefined;
try {
app = await AppService.getAppByPath({ workspace, path: remotePath });
} catch (e) {}
if (app) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing app...`,
),
`Applying ${diffs.length} diffs to existing app... ${remotePath}`
)
);
const changeset: {
path?: string | undefined;
summary?: string | undefined;
value?: any;
policy?: Policy | undefined;
@@ -43,14 +56,10 @@ export class AppFile implements Resource, PushDiffs {
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path[0] !== "value" && diff.path[0] !== "policy" && (
diff.path.length !== 1 ||
!["path", "summary"].includes(
diff.path[0] as string,
)
)
)
diff.path[0] !== "value" &&
diff.path[0] !== "policy" &&
(diff.path.length !== 1 ||
!["summary"].includes(diff.path[0] as string))
) {
throw new Error("Invalid app diff with path " + diff.path);
}
@@ -61,8 +70,21 @@ export class AppFile implements Resource, PushDiffs {
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
if (
(!changeset?.policy ||
JSON.stringify(changeset?.policy) == JSON.stringify(app.policy)) &&
(!changeset?.value ||
JSON.stringify(changeset?.value) == JSON.stringify(app.value)) &&
(!changeset?.summary || changeset.summary == app.summary)
) {
console.log(
colors.yellow(`No changes to push for app ${remotePath}, skipping`)
);
return;
}
const hasChanges = Object.values(changeset).some(
(v) => v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
return;
@@ -75,6 +97,7 @@ export class AppFile implements Resource, PushDiffs {
});
} else {
console.log(colors.yellow.bold("Creating new app..."));
await AppService.createApp({
workspace,
requestBody: {
@@ -87,19 +110,70 @@ export class AppFile implements Resource, PushDiffs {
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let existing: AppWithLastVersion | undefined;
try {
existing = await AppService.getAppByPath({
workspace: workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false })
);
}
}
async function list(opts: GlobalOptions) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
let page = 0;
const perPage = 10;
const total: ListableApp[] = [];
while (true) {
const res = await AppService.listApps({
workspace: workspace.workspaceId,
page,
perPage,
});
page += 1;
total.push(...res);
if (res.length < perPage) {
break;
}
}
new Table()
.header(["path", "summary"])
.padding(2)
.border(true)
.body(total.map((x) => [x.path, x.summary]))
.render();
}
async function push(opts: GlobalOptions, filePath: string) {
const remotePath = filePath.split(".")[0];
if (!validatePath(remotePath)) {
return;
}
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
await pushApp(filePath, workspace.workspaceId, remotePath);
console.log(colors.bold.underline.green("App pushed"));
}
export async function pushApp(
filePath: string,
workspace: string,
remotePath: string
) {
const data = decoverto
.type(AppFile)
.rawToInstance(await Deno.readTextFile(filePath));
await data.push(workspace, remotePath);
}
const command = new Command()
.description("app related commands")
.action(list as any)
.command("push", "push a local app ")
.arguments("<file_path:file>")
.action(push as any);
export default command;

View File

@@ -18,12 +18,12 @@ export type Context = {
};
async function tryResolveWorkspace(
opts: GlobalOptions,
opts: GlobalOptions
): Promise<
{ isError: false; value: Workspace } | { isError: true; error: string }
> {
const cache = (opts as any).__secret_workspace;
if (cache) return cache;
if (cache) return { isError: false, value: cache };
if (opts.workspace) {
const e = await getWorkspaceByName(opts.workspace);
@@ -49,21 +49,23 @@ async function tryResolveWorkspace(
}
export async function resolveWorkspace(
opts: GlobalOptions,
opts: GlobalOptions
): Promise<Workspace> {
const res = await tryResolveWorkspace(opts);
if (res.isError) {
console.log(res.error);
console.log(colors.red.bold(res.error));
return Deno.exit(-1);
} else {
return res.value;
}
}
export async function requireLogin(opts: GlobalOptions): Promise<GlobalUserInfo> {
export async function requireLogin(
opts: GlobalOptions
): Promise<GlobalUserInfo> {
const workspace = await resolveWorkspace(opts);
let token = await tryGetLoginInfo(opts);
if (!token) {
token = workspace.token;
}
@@ -74,26 +76,26 @@ export async function requireLogin(opts: GlobalOptions): Promise<GlobalUserInfo>
return await UserService.globalWhoami();
} catch {
console.log(
"! Could not reach API given existing credentials. Attempting to reauth...",
"! Could not reach API given existing credentials. Attempting to reauth..."
);
const newToken = await loginInteractive(workspace.remote);
if (!newToken) {
throw new Error("Could not reauth");
}
removeWorkspace(workspace.name);
removeWorkspace(workspace.name, false, opts);
workspace.token = newToken;
addWorkspace(workspace);
addWorkspace(workspace, opts);
setClient(
token,
workspace.remote.substring(0, workspace.remote.length - 1),
workspace.remote.substring(0, workspace.remote.length - 1)
);
return await UserService.globalWhoami();
}
}
export async function tryResolveVersion(
opts: GlobalOptions,
opts: GlobalOptions
): Promise<number | undefined> {
if ((opts as any).__cache_version) {
return (opts as any).__cache_version;
@@ -103,41 +105,24 @@ export async function tryResolveVersion(
if (workspaceRes.isError) return undefined;
const response = await fetch(
new URL(new URL(workspaceRes.value.remote).origin + "/api/version"),
new URL(new URL(workspaceRes.value.remote).origin + "/api/version")
);
const version = await response.text();
try {
return Number.parseInt(
version.split("-", 1)[0].replaceAll(".", "").replace("v", ""),
version.split("-", 1)[0].replaceAll(".", "").replace("v", "")
);
} catch {
return undefined;
}
}
export async function validatePath(
opts: GlobalOptions,
path: string,
): Promise<boolean> {
const backendVersion = await tryResolveVersion(opts);
if (path.startsWith("f")) {
if (!backendVersion || backendVersion >= 1550) {
return true;
}
console.log(
`Attempting to use folders, but the current remote does not have support. Remote version is ${backendVersion} but folders are supported from 1560.`,
);
return false;
}
if (
!(path.startsWith("g") ||
path.startsWith("u"))
) {
export function validatePath(path: string): boolean {
if (!(path.startsWith("g") || path.startsWith("u") || path.startsWith("f"))) {
console.log(
colors.red(
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/...",
),
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/..."
)
);
return false;
}

View File

@@ -14,7 +14,7 @@ export {
DenoLandProvider,
UpgradeCommand,
} from "https://deno.land/x/cliffy@v0.25.7/command/upgrade/mod.ts";
export { CompletionsCommand } from "https://deno.land/x/cliffy@v0.25.7/command/completions/mod.ts";
// std
export * as path from "https://deno.land/std@0.176.0/path/mod.ts";
export { ensureDir } from "https://deno.land/std@0.176.0/fs/ensure_dir.ts";
@@ -33,9 +33,7 @@ export { passwordGenerator } from "https://deno.land/x/password_generator@latest
export { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts";
export * as cbor from "https://deno.land/x/cbor@v1.4.1/index.js";
export { default as Murmurhash3 } from "https://deno.land/x/murmurhash@v1.0.0/mod.ts";
export {
default as microdiff,
} from "https://deno.land/x/microdiff@v1.3.1/index.ts";
export { default as microdiff } from "https://deno.land/x/microdiff@v1.3.1/index.ts";
export { default as objectHash } from "https://deno.land/x/object_hash@2.0.3.1/mod.ts";
export { default as gitignore_parser } from "npm:gitignore-parser";
export { default as JSZip } from "npm:jszip@3.7.1";

View File

@@ -20,7 +20,6 @@ import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { resolve, track_job } from "./script.ts";
import { Any, decoverto, model, property } from "./decoverto.ts";
// this is effectively "OpenFlow" but a copy as it is accepted by the CLI
@model()
export class FlowFile implements Resource, PushDiffs {
@@ -40,7 +39,7 @@ export class FlowFile implements Resource, PushDiffs {
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
diffs: Difference[]
): Promise<void> {
if (
await FlowService.existsFlowByPath({
@@ -50,8 +49,8 @@ export class FlowFile implements Resource, PushDiffs {
) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing flow... ${remotePath}`,
),
`Applying ${diffs.length} diffs to existing flow... ${remotePath}`
)
);
// TODO: Make these optional in backend (not path ofc)
@@ -66,14 +65,11 @@ export class FlowFile implements Resource, PushDiffs {
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path[0] !== "value" && (
diff.path.length !== 1 ||
!["summary", "description", "schema"].includes(
diff.path[0] as string,
)
)
)
diff.path[0] !== "value" &&
(diff.path.length !== 1 ||
!["summary", "description", "schema"].includes(
diff.path[0] as string
))
) {
throw new Error("Invalid flow diff with path " + diff.path);
}
@@ -83,8 +79,8 @@ export class FlowFile implements Resource, PushDiffs {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
const hasChanges = Object.values(changeset).some(
(v) => v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
return;
@@ -93,7 +89,8 @@ export class FlowFile implements Resource, PushDiffs {
const update = {
...changeset,
...base_changeset,
}
};
await FlowService.updateFlow({
workspace: workspace,
path: remotePath,
@@ -114,20 +111,10 @@ export class FlowFile implements Resource, PushDiffs {
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let remote: Flow | undefined;
try {
remote = await FlowService.getFlowByPath({
workspace,
path: remotePath,
});
} catch {
remote = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(remote ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false })
);
}
}
@@ -135,24 +122,24 @@ export class FlowFile implements Resource, PushDiffs {
type Options = GlobalOptions;
async function push(opts: Options, filePath: string, remotePath: string) {
if (!await validatePath(opts, remotePath)) {
if (!validatePath(remotePath)) {
return;
}
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
await pushFlow(filePath, workspace.remote, remotePath);
await pushFlow(filePath, workspace.workspaceId, remotePath);
console.log(colors.bold.underline.green("Flow pushed"));
}
export async function pushFlow(
filePath: string,
workspace: string,
remotePath: string,
remotePath: string
) {
const data = decoverto.type(FlowFile).rawToInstance(
await Deno.readTextFile(filePath),
);
const data = decoverto
.type(FlowFile)
.rawToInstance(await Deno.readTextFile(filePath));
await data.push(workspace, remotePath);
}
@@ -181,26 +168,20 @@ async function list(opts: GlobalOptions & { showArchived?: boolean }) {
.header(["path", "summary", "edited by"])
.padding(2)
.border(true)
.body(
total.map((x) => [
x.path,
x.summary,
x.edited_by,
]),
)
.body(total.map((x) => [x.path, x.summary, x.edited_by]))
.render();
}
async function run(
opts: GlobalOptions & {
input: string[];
data?: string;
silent: boolean;
},
path: string,
path: string
) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
const input = await resolve(opts.input);
const input = opts.data ? await resolve(opts.data) : {};
const id = await JobService.runFlowByPath({
workspace: workspace.workspaceId,
@@ -236,6 +217,7 @@ async function run(
if (!opts.silent) {
console.log(colors.green.underline.bold("Flow ran to completion"));
console.log();
}
const jobInfo = await JobService.getCompletedJob({
workspace: workspace.workspaceId,
@@ -250,19 +232,19 @@ const command = new Command()
.action(list as any)
.command(
"push",
"push a local flow spec. This overrides any remote versions.",
"push a local flow spec. This overrides any remote versions."
)
.arguments("<file_path:string> <remote_path:string>")
.action(push as any)
.command("run", "run a flow by path.")
.arguments("<path:string>")
.option(
"-i --input [inputs...:string]",
"Inputs specified as JSON objects or simply as <name>=<value>. Supports file inputs using @<filename> and stdin using @- these also need to be formatted as JSON. Later inputs override earlier ones.",
"-d --data <data:string>",
"Inputs specified as a JSON string or a file using @<filename> or stdin using @-."
)
.option(
"-s --silent",
"Do not ouput anything other then the final output. Useful for scripting.",
"Do not ouput anything other then the final output. Useful for scripting."
)
.action(run as any);

View File

@@ -1,4 +1,4 @@
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
import { colors, Command, FolderService, microdiff } from "./deps.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
Difference,
@@ -20,8 +20,16 @@ import {
export class FolderFile implements Resource, PushDiffs {
@property(array(() => String))
owners: Array<string> | undefined;
@property(map(() => String, () => Boolean, { shape: MapShape.Object }))
@property(
map(
() => String,
() => Boolean,
{ shape: MapShape.Object }
)
)
extra_perms: Map<string, boolean> | undefined;
@property(() => String)
display_name: string | undefined;
async push(workspace: string, remotePath: string): Promise<void> {
if (remotePath.startsWith("/")) {
@@ -31,23 +39,17 @@ export class FolderFile implements Resource, PushDiffs {
remotePath = remotePath.substring(2);
}
let existing: Folder | undefined;
try {
existing = await FolderService.getFolder({ workspace, name: remotePath });
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false })
);
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
diffs: Difference[]
): Promise<void> {
if (remotePath.startsWith("/")) {
remotePath = remotePath.substring(1);
@@ -59,29 +61,34 @@ export class FolderFile implements Resource, PushDiffs {
// TODO: Support this in backend
let exists: boolean;
try {
exists = !!await FolderService.getFolder({ workspace, name: remotePath });
exists = !!(await FolderService.getFolder({
workspace,
name: remotePath,
}));
} catch {
exists = false;
}
if (exists) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing folder...`,
),
`Applying ${diffs.length} diffs to existing folder... ${remotePath}`
)
);
const changeset: {
owners?: string[] | undefined;
extra_perms?: any;
display_name?: string | undefined;
} = {};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["owners", "extra_perms"].includes(diff.path[0] as string)
)
(diff.path.length !== 1 ||
!["owners", "extra_perms", "display_name"].includes(
diff.path[0] as string
))
) {
console.log(diff.path);
throw new Error("Invalid folder diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
@@ -91,18 +98,27 @@ export class FolderFile implements Resource, PushDiffs {
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
const hasChanges = Object.values(changeset).some(
(v) => v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
return;
}
await FolderService.updateFolder({
workspace: workspace,
name: remotePath,
requestBody: changeset,
});
try {
await FolderService.updateFolder({
workspace: workspace,
name: remotePath,
requestBody: {
...changeset,
extra_perms: changeset.extra_perms
? Object.fromEntries(this.extra_perms?.entries() ?? [])
: undefined,
},
});
} catch (e) {
console.error(colors.red.bold(e.body));
throw e;
}
} else {
console.log(colors.bold.yellow("Creating new folder: " + remotePath));
await FolderService.createFolder({
@@ -121,7 +137,7 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
if (!await validatePath(opts, remotePath)) {
if (!validatePath(remotePath)) {
return;
}
@@ -139,11 +155,11 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
export async function pushFolder(
workspace: string,
filePath: string,
remotePath: string,
remotePath: string
) {
const data = decoverto.type(FolderFile).rawToInstance(
await Deno.readTextFile(filePath),
);
const data = decoverto
.type(FolderFile)
.rawToInstance(await Deno.readTextFile(filePath));
data.push(workspace, remotePath);
}
@@ -151,7 +167,7 @@ const command = new Command()
.description("resource related commands")
.command(
"push",
"push a local folder spec. This overrides any remote versions.",
"push a local folder spec. This overrides any remote versions."
)
.arguments("<file_path:string> <remote_path:string>")
.action(push as any);

View File

@@ -1,5 +1,11 @@
import { Command, DenoLandProvider, UpgradeCommand } from "./deps.ts";
import {
Command,
CompletionsCommand,
DenoLandProvider,
UpgradeCommand,
} from "./deps.ts";
import flow from "./flow.ts";
import app from "./apps.ts";
import script from "./script.ts";
import workspace from "./workspace.ts";
import resource from "./resource.ts";
@@ -13,21 +19,21 @@ import sync from "./sync.ts";
import { tryResolveVersion } from "./context.ts";
import { GlobalOptions } from "./types.ts";
const VERSION = "v1.70.1";
export const VERSION = "v1.87.0";
let command: any = new Command()
.name("wmill")
.description("A simple CLI tool for windmill.")
.action(() => command.showHelp())
.globalOption(
"--workspace <workspace:string>",
"Specify the target workspace. This overrides the default workspace.",
"Specify the target workspace. This overrides the default workspace."
)
.globalOption(
"--token <token:string>",
"Specify an API token. This will override any stored token.",
"Specify an API token. This will override any stored token."
)
.version(VERSION)
.command("app", app)
.command("flow", flow)
.command("script", script)
.command("workspace", workspace)
@@ -59,13 +65,12 @@ let command: any = new Command()
"--unstable",
],
provider: new DenoLandProvider({ name: "wmill" }),
}),
);
})
)
.command("completions", new CompletionsCommand());
if (Number.parseInt(VERSION.replace("v", "").replace(".", "")) > 1700) {
command = command
.command("push", push)
.command("pull", pull);
command = command.command("push", push).command("pull", pull);
}
try {

View File

@@ -5,25 +5,27 @@ import { Workspace } from "./workspace.ts";
export async function downloadZip(
workspace: Workspace,
plainSecrets: boolean | undefined
): Promise<JSZip | undefined> {
const requestHeaders: HeadersInit = new Headers();
requestHeaders.set("Authorization", "Bearer " + workspace.token);
requestHeaders.set("Content-Type", "application/octet-stream");
const zipResponse = await fetch(
workspace.remote + "api/w/" + workspace.workspaceId +
"/workspaces/tarball?archive_type=zip",
workspace.remote +
"api/w/" +
workspace.workspaceId +
"/workspaces/tarball?archive_type=zip&plain_secret=" +
(plainSecrets ?? false),
{
headers: requestHeaders,
method: "GET",
},
}
);
if (!zipResponse.ok) {
console.log(
colors.red(
"Failed to request tarball from API " + zipResponse.statusText,
),
colors.red("Failed to request tarball from API " + zipResponse.statusText)
);
throw new Error(await zipResponse.text());
}
@@ -33,18 +35,18 @@ export async function downloadZip(
async function stub(
_opts: GlobalOptions & { override: boolean },
_dir: string,
_dir: string
) {
console.log(
colors.red.underline(
'Pull is deprecated. Use "sync pull --raw" instead. See <TODO_LINK_HERE> for more information.',
),
'Pull is deprecated. Use "sync pull --raw" instead. See <TODO_LINK_HERE> for more information.'
)
);
}
const command = new Command()
.description(
"Pull all definitions in the current workspace from the API and write them to disk.",
"Pull all definitions in the current workspace from the API and write them to disk."
)
.arguments("<dir:string>")
.action(stub as any);

View File

@@ -13,7 +13,6 @@ import {
EditResourceType,
microdiff,
ResourceService,
ResourceType,
Table,
} from "./deps.ts";
import { Any, decoverto, model, property } from "./decoverto.ts";
@@ -26,19 +25,10 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
description?: string;
async push(workspace: string, remotePath: string): Promise<void> {
let existing: ResourceType | undefined;
try {
existing = await ResourceService.getResourceType({
workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
this.pushDiffs(
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false }),
);
}
@@ -65,8 +55,8 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
return;
}
console.log(
colors.yellow(
`Applying ${diffs.length} diffs to existing resource type...`,
colors.yellow.bold(
`Applying ${diffs.length} diffs to existing resource type... ${remotePath}`,
),
);
const changeset: EditResourceType = {};

View File

@@ -34,7 +34,7 @@ export class ResourceFile implements Resource2, PushDiffs {
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
diffs: Difference[]
): Promise<void> {
if (
await ResourceService.existsResource({
@@ -43,7 +43,9 @@ export class ResourceFile implements Resource2, PushDiffs {
})
) {
console.log(
colors.yellow(`Applying ${diffs.length} diffs to existing resource...`),
colors.yellow.bold(
`Applying ${diffs.length} diffs to existing resource... ${remotePath}`
)
);
const changeset: EditResource = {
@@ -51,22 +53,18 @@ export class ResourceFile implements Resource2, PushDiffs {
};
for (const diff of diffs) {
if (diff.path[0] === "is_oauth") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
//is_oauth is not updatable
continue;
}
if (
diff.type !== "REMOVE" &&
(
diff.path[0] !== "value" && (
diff.path.length !== 1 ||
diff.path[0] !== "description"
)
)
diff.path[0] !== "value" &&
(diff.path.length !== 1 || diff.path[0] !== "description") &&
diff.path[0] !== "resource_type"
) {
console.log(
colors.red("Invalid variable diff with path " + diff.path)
);
throw new Error("Invalid folder diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
@@ -76,8 +74,8 @@ export class ResourceFile implements Resource2, PushDiffs {
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
const hasChanges = Object.values(changeset).some(
(v) => v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
return;
@@ -92,8 +90,8 @@ export class ResourceFile implements Resource2, PushDiffs {
if (typeof this.is_oauth !== "undefined") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
"! is_oauth has been removed in newer versions. Ignoring."
)
);
}
@@ -110,19 +108,10 @@ export class ResourceFile implements Resource2, PushDiffs {
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let existing: Resource | undefined;
try {
existing = await ResourceService.getResource({
workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false })
);
}
}
@@ -130,11 +119,11 @@ export class ResourceFile implements Resource2, PushDiffs {
export async function pushResource(
workspace: string,
filePath: string,
remotePath: string,
remotePath: string
) {
const data = decoverto.type(ResourceFile).rawToInstance(
await Deno.readTextFile(filePath),
);
const data = decoverto
.type(ResourceFile)
.rawToInstance(await Deno.readTextFile(filePath));
await data.push(workspace, remotePath);
}
@@ -143,7 +132,7 @@ async function push(opts: PushOptions, filePath: string, remotePath: string) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
if (!await validatePath(opts, remotePath)) {
if (!validatePath(remotePath)) {
return;
}
@@ -190,7 +179,7 @@ const command = new Command()
.action(list as any)
.command(
"push",
"push a local resource spec. This overrides any remote versions.",
"push a local resource spec. This overrides any remote versions."
)
.arguments("<file_path:string> <remote_path:string>")
.action(push as any);

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